Repository: ClassicOldSong/Apollo Branch: master Commit: 003393ee1800 Files: 430 Total size: 3.7 MB Directory structure: gitextract_caung7mp/ ├── .clang-format ├── .dockerignore ├── .flake8 ├── .gitattributes ├── .github/ │ ├── CONTRIBUTING.md │ ├── copilot-instructions.md │ └── matchers/ │ ├── copr-ci.json │ ├── docker.json │ ├── gcc-strip3.json │ └── gcc.json ├── .gitignore ├── .gitmodules ├── .prettierrc.json ├── .readthedocs.yaml ├── CMakeLists.txt ├── DOCKER_README.md ├── LICENSE ├── NOTICE ├── README.md ├── apollo.icns ├── cmake/ │ ├── FindLIBCAP.cmake │ ├── FindLIBDRM.cmake │ ├── FindLibva.cmake │ ├── FindSystemd.cmake │ ├── FindUdev.cmake │ ├── FindWayland.cmake │ ├── compile_definitions/ │ │ ├── common.cmake │ │ ├── linux.cmake │ │ ├── macos.cmake │ │ ├── unix.cmake │ │ └── windows.cmake │ ├── dependencies/ │ │ ├── Boost_Sunshine.cmake │ │ ├── common.cmake │ │ ├── libevdev_Sunshine.cmake │ │ ├── linux.cmake │ │ ├── macos.cmake │ │ ├── nlohmann_json.cmake │ │ ├── unix.cmake │ │ └── windows.cmake │ ├── macros/ │ │ ├── common.cmake │ │ ├── linux.cmake │ │ ├── macos.cmake │ │ ├── unix.cmake │ │ └── windows.cmake │ ├── packaging/ │ │ ├── common.cmake │ │ ├── linux.cmake │ │ ├── macos.cmake │ │ ├── unix.cmake │ │ ├── windows.cmake │ │ ├── windows_nsis.cmake │ │ └── windows_wix.cmake │ ├── prep/ │ │ ├── build_version.cmake │ │ ├── constants.cmake │ │ ├── init.cmake │ │ ├── options.cmake │ │ └── special_package_configuration.cmake │ └── targets/ │ ├── common.cmake │ ├── linux.cmake │ ├── macos.cmake │ ├── unix.cmake │ └── windows.cmake ├── codecov.yml ├── crowdin.yml ├── docker/ │ ├── archlinux.dockerfile │ ├── clion-toolchain.dockerfile │ ├── debian-trixie.dockerfile │ ├── ubuntu-22.04.dockerfile │ └── ubuntu-24.04.dockerfile ├── docs/ │ ├── Doxyfile │ ├── api.js │ ├── api.md │ ├── app_examples.md │ ├── awesome_sunshine.md │ ├── building.md │ ├── changelog.md │ ├── configuration.js │ ├── configuration.md │ ├── contributing.md │ ├── doc-styles.css │ ├── gamestream_migration.md │ ├── getting_started.md │ ├── guides.md │ ├── legal.md │ ├── performance_tuning.md │ ├── third_party_packages.md │ └── troubleshooting.md ├── gh-pages-template/ │ ├── .readthedocs.yaml │ ├── _config.yml │ └── index.html ├── package.json ├── packaging/ │ ├── linux/ │ │ ├── AppImage/ │ │ │ ├── AppRun │ │ │ └── dev.lizardbyte.app.Sunshine.desktop │ │ ├── Arch/ │ │ │ ├── PKGBUILD │ │ │ └── sunshine.install │ │ ├── dev.lizardbyte.app.Sunshine.desktop │ │ ├── dev.lizardbyte.app.Sunshine.metainfo.xml │ │ ├── dev.lizardbyte.app.Sunshine.terminal.desktop │ │ ├── fedora/ │ │ │ └── Sunshine.spec │ │ ├── flatpak/ │ │ │ ├── README.md │ │ │ ├── apps.json │ │ │ ├── dev.lizardbyte.app.Sunshine.desktop │ │ │ ├── dev.lizardbyte.app.Sunshine.yml │ │ │ ├── exceptions.json │ │ │ ├── flathub.json │ │ │ ├── modules/ │ │ │ │ ├── avahi.json │ │ │ │ ├── boost.json │ │ │ │ ├── cuda.json │ │ │ │ ├── libevdev.json │ │ │ │ ├── libnotify.json │ │ │ │ ├── miniupnpc.json │ │ │ │ ├── nlohmann_json.json │ │ │ │ ├── numactl.json │ │ │ │ └── xvfb/ │ │ │ │ ├── xvfb-run │ │ │ │ └── xvfb.json │ │ │ └── scripts/ │ │ │ ├── additional-install.sh │ │ │ ├── remove-additional-install.sh │ │ │ └── sunshine.sh │ │ ├── patches/ │ │ │ ├── aarch64/ │ │ │ │ └── 01-math_functions.patch │ │ │ └── x86_64/ │ │ │ └── 01-math_functions.patch │ │ └── sunshine.service.in │ └── sunshine.rb ├── scripts/ │ ├── _locale.py │ ├── icons/ │ │ └── convert_and_pack.sh │ ├── linux_build.sh │ ├── requirements.txt │ └── update_clang_format.py ├── src/ │ ├── audio.cpp │ ├── audio.h │ ├── cbs.cpp │ ├── cbs.h │ ├── config.cpp │ ├── config.h │ ├── confighttp.cpp │ ├── confighttp.h │ ├── crypto.cpp │ ├── crypto.h │ ├── display_device.cpp │ ├── display_device.h │ ├── entry_handler.cpp │ ├── entry_handler.h │ ├── file_handler.cpp │ ├── file_handler.h │ ├── globals.cpp │ ├── globals.h │ ├── httpcommon.cpp │ ├── httpcommon.h │ ├── input.cpp │ ├── input.h │ ├── logging.cpp │ ├── logging.h │ ├── main.cpp │ ├── main.h │ ├── move_by_copy.h │ ├── network.cpp │ ├── network.h │ ├── nvenc/ │ │ ├── nvenc_base.cpp │ │ ├── nvenc_base.h │ │ ├── nvenc_colorspace.h │ │ ├── nvenc_config.h │ │ ├── nvenc_d3d11.cpp │ │ ├── nvenc_d3d11.h │ │ ├── nvenc_d3d11_native.cpp │ │ ├── nvenc_d3d11_native.h │ │ ├── nvenc_d3d11_on_cuda.cpp │ │ ├── nvenc_d3d11_on_cuda.h │ │ ├── nvenc_encoded_frame.h │ │ ├── nvenc_utils.cpp │ │ └── nvenc_utils.h │ ├── nvhttp.cpp │ ├── nvhttp.h │ ├── platform/ │ │ ├── common.h │ │ ├── linux/ │ │ │ ├── audio.cpp │ │ │ ├── cuda.cpp │ │ │ ├── cuda.cu │ │ │ ├── cuda.h │ │ │ ├── graphics.cpp │ │ │ ├── graphics.h │ │ │ ├── input/ │ │ │ │ ├── inputtino.cpp │ │ │ │ ├── inputtino_common.h │ │ │ │ ├── inputtino_gamepad.cpp │ │ │ │ ├── inputtino_gamepad.h │ │ │ │ ├── inputtino_keyboard.cpp │ │ │ │ ├── inputtino_keyboard.h │ │ │ │ ├── inputtino_mouse.cpp │ │ │ │ ├── inputtino_mouse.h │ │ │ │ ├── inputtino_pen.cpp │ │ │ │ ├── inputtino_pen.h │ │ │ │ ├── inputtino_touch.cpp │ │ │ │ └── inputtino_touch.h │ │ │ ├── kmsgrab.cpp │ │ │ ├── misc.cpp │ │ │ ├── misc.h │ │ │ ├── publish.cpp │ │ │ ├── vaapi.cpp │ │ │ ├── vaapi.h │ │ │ ├── wayland.cpp │ │ │ ├── wayland.h │ │ │ ├── wlgrab.cpp │ │ │ ├── x11grab.cpp │ │ │ └── x11grab.h │ │ ├── macos/ │ │ │ ├── av_audio.h │ │ │ ├── av_audio.m │ │ │ ├── av_img_t.h │ │ │ ├── av_video.h │ │ │ ├── av_video.m │ │ │ ├── display.mm │ │ │ ├── input.cpp │ │ │ ├── microphone.mm │ │ │ ├── misc.h │ │ │ ├── misc.mm │ │ │ ├── nv12_zero_device.cpp │ │ │ ├── nv12_zero_device.h │ │ │ └── publish.cpp │ │ └── windows/ │ │ ├── PolicyConfig.h │ │ ├── audio.cpp │ │ ├── display.h │ │ ├── display_base.cpp │ │ ├── display_ram.cpp │ │ ├── display_vram.cpp │ │ ├── display_wgc.cpp │ │ ├── input.cpp │ │ ├── keylayout.h │ │ ├── misc.cpp │ │ ├── misc.h │ │ ├── nvprefs/ │ │ │ ├── driver_settings.cpp │ │ │ ├── driver_settings.h │ │ │ ├── nvapi_opensource_wrapper.cpp │ │ │ ├── nvprefs_common.cpp │ │ │ ├── nvprefs_common.h │ │ │ ├── nvprefs_interface.cpp │ │ │ ├── nvprefs_interface.h │ │ │ ├── undo_data.cpp │ │ │ ├── undo_data.h │ │ │ ├── undo_file.cpp │ │ │ └── undo_file.h │ │ ├── publish.cpp │ │ ├── utils.cpp │ │ ├── utils.h │ │ ├── virtual_display.cpp │ │ ├── virtual_display.h │ │ └── windows.rc │ ├── process.cpp │ ├── process.h │ ├── round_robin.h │ ├── rswrapper.c │ ├── rswrapper.h │ ├── rtsp.cpp │ ├── rtsp.h │ ├── stat_trackers.cpp │ ├── stat_trackers.h │ ├── stream.cpp │ ├── stream.h │ ├── sync.h │ ├── system_tray.cpp │ ├── system_tray.h │ ├── task_pool.h │ ├── thread_pool.h │ ├── thread_safe.h │ ├── upnp.cpp │ ├── upnp.h │ ├── utility.h │ ├── uuid.h │ ├── video.cpp │ ├── video.h │ ├── video_colorspace.cpp │ ├── video_colorspace.h │ └── zwpad.h ├── src_assets/ │ ├── common/ │ │ └── assets/ │ │ └── web/ │ │ ├── Checkbox.vue │ │ ├── ClientCard.vue │ │ ├── Navbar.vue │ │ ├── PlatformLayout.vue │ │ ├── ResourceCard.vue │ │ ├── ThemeToggle.vue │ │ ├── apollo_version.js │ │ ├── apps.html │ │ ├── config.html │ │ ├── configs/ │ │ │ └── tabs/ │ │ │ ├── Advanced.vue │ │ │ ├── AudioVideo.vue │ │ │ ├── ContainerEncoders.vue │ │ │ ├── Files.vue │ │ │ ├── General.vue │ │ │ ├── Inputs.vue │ │ │ ├── Network.vue │ │ │ ├── audiovideo/ │ │ │ │ ├── AdapterNameSelector.vue │ │ │ │ ├── DisplayDeviceOptions.vue │ │ │ │ ├── DisplayModesSettings.vue │ │ │ │ └── DisplayOutputSelector.vue │ │ │ └── encoders/ │ │ │ ├── AmdAmfEncoder.vue │ │ │ ├── IntelQuickSyncEncoder.vue │ │ │ ├── NvidiaNvencEncoder.vue │ │ │ ├── SoftwareEncoder.vue │ │ │ ├── VAAPIEncoder.vue │ │ │ └── VideotoolboxEncoder.vue │ │ ├── index.html │ │ ├── init.js │ │ ├── locale.js │ │ ├── login.html │ │ ├── password.html │ │ ├── pin.html │ │ ├── platform-i18n.js │ │ ├── public/ │ │ │ └── assets/ │ │ │ ├── css/ │ │ │ │ └── apollo.css │ │ │ └── locale/ │ │ │ ├── bg.json │ │ │ ├── cs.json │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ ├── en_GB.json │ │ │ ├── en_US.json │ │ │ ├── es.json │ │ │ ├── fr.json │ │ │ ├── hu.json │ │ │ ├── it.json │ │ │ ├── ja.json │ │ │ ├── ko.json │ │ │ ├── pl.json │ │ │ ├── pt.json │ │ │ ├── pt_BR.json │ │ │ ├── ru.json │ │ │ ├── sv.json │ │ │ ├── tr.json │ │ │ ├── uk.json │ │ │ ├── vi.json │ │ │ ├── zh.json │ │ │ └── zh_TW.json │ │ ├── template_header.html │ │ ├── theme.js │ │ ├── troubleshooting.html │ │ └── welcome.html │ ├── linux/ │ │ ├── assets/ │ │ │ ├── apps.json │ │ │ └── shaders/ │ │ │ └── opengl/ │ │ │ ├── ConvertUV.frag │ │ │ ├── ConvertUV.vert │ │ │ ├── ConvertY.frag │ │ │ ├── Scene.frag │ │ │ └── Scene.vert │ │ └── misc/ │ │ ├── 60-sunshine.conf │ │ ├── 60-sunshine.rules │ │ └── postinst │ ├── macos/ │ │ ├── assets/ │ │ │ ├── Info.plist │ │ │ └── apps.json │ │ └── misc/ │ │ └── uninstall_pkg.sh │ └── windows/ │ ├── assets/ │ │ ├── apps.json │ │ └── shaders/ │ │ └── directx/ │ │ ├── convert_yuv420_packed_uv_type0_ps.hlsl │ │ ├── convert_yuv420_packed_uv_type0_ps_linear.hlsl │ │ ├── convert_yuv420_packed_uv_type0_ps_perceptual_quantizer.hlsl │ │ ├── convert_yuv420_packed_uv_type0_vs.hlsl │ │ ├── convert_yuv420_packed_uv_type0s_ps.hlsl │ │ ├── convert_yuv420_packed_uv_type0s_ps_linear.hlsl │ │ ├── convert_yuv420_packed_uv_type0s_ps_perceptual_quantizer.hlsl │ │ ├── convert_yuv420_packed_uv_type0s_vs.hlsl │ │ ├── convert_yuv420_planar_y_ps.hlsl │ │ ├── convert_yuv420_planar_y_ps_linear.hlsl │ │ ├── convert_yuv420_planar_y_ps_perceptual_quantizer.hlsl │ │ ├── convert_yuv420_planar_y_vs.hlsl │ │ ├── convert_yuv444_packed_ayuv_ps.hlsl │ │ ├── convert_yuv444_packed_ayuv_ps_linear.hlsl │ │ ├── convert_yuv444_packed_vs.hlsl │ │ ├── convert_yuv444_packed_y410_ps.hlsl │ │ ├── convert_yuv444_packed_y410_ps_linear.hlsl │ │ ├── convert_yuv444_packed_y410_ps_perceptual_quantizer.hlsl │ │ ├── convert_yuv444_planar_ps.hlsl │ │ ├── convert_yuv444_planar_ps_linear.hlsl │ │ ├── convert_yuv444_planar_ps_perceptual_quantizer.hlsl │ │ ├── convert_yuv444_planar_vs.hlsl │ │ ├── cursor_ps.hlsl │ │ ├── cursor_ps_normalize_white.hlsl │ │ ├── cursor_vs.hlsl │ │ └── include/ │ │ ├── base_vs.hlsl │ │ ├── base_vs_types.hlsl │ │ ├── common.hlsl │ │ ├── convert_base.hlsl │ │ ├── convert_linear_base.hlsl │ │ ├── convert_perceptual_quantizer_base.hlsl │ │ ├── convert_yuv420_packed_uv_ps_base.hlsl │ │ ├── convert_yuv420_planar_y_ps_base.hlsl │ │ └── convert_yuv444_ps_base.hlsl │ ├── drivers/ │ │ └── sudovda/ │ │ ├── SudoVDA.inf │ │ ├── install.bat │ │ ├── sudovda.cat │ │ ├── sudovda.cer │ │ └── uninstall.bat │ └── misc/ │ ├── autostart/ │ │ └── autostart-service.bat │ ├── firewall/ │ │ ├── add-firewall-rule.bat │ │ └── delete-firewall-rule.bat │ ├── gamepad/ │ │ ├── install-gamepad.ps1 │ │ └── uninstall-gamepad.ps1 │ ├── migration/ │ │ └── migrate-config.bat │ ├── path/ │ │ └── update-path.bat │ └── service/ │ ├── install-service.bat │ └── uninstall-service.bat ├── tests/ │ ├── CMakeLists.txt │ ├── fixtures/ │ │ └── http/ │ │ ├── hello-redirect.txt │ │ └── hello.txt │ ├── integration/ │ │ ├── test_config_consistency.cpp │ │ ├── test_external_commands.cpp │ │ └── test_locale_consistency.cpp │ ├── tests_common.h │ ├── tests_environment.h │ ├── tests_events.h │ ├── tests_log_checker.h │ ├── tests_main.cpp │ └── unit/ │ ├── platform/ │ │ └── test_common.cpp │ ├── test_audio.cpp │ ├── test_display_device.cpp │ ├── test_entry_handler.cpp │ ├── test_file_handler.cpp │ ├── test_http_pairing.cpp │ ├── test_httpcommon.cpp │ ├── test_logging.cpp │ ├── test_mouse.cpp │ ├── test_network.cpp │ ├── test_rswrapper.cpp │ ├── test_stream.cpp │ └── test_video.cpp ├── third-party/ │ ├── .clang-format-ignore │ ├── glad/ │ │ ├── include/ │ │ │ ├── EGL/ │ │ │ │ └── eglplatform.h │ │ │ ├── KHR/ │ │ │ │ └── khrplatform.h │ │ │ └── glad/ │ │ │ ├── egl.h │ │ │ └── gl.h │ │ └── src/ │ │ ├── egl.c │ │ └── gl.c │ ├── nvfbc/ │ │ ├── NvFBC.h │ │ └── helper_math.h │ └── sudovda/ │ ├── sudovda-ioctl.h │ └── sudovda.h ├── tools/ │ ├── CMakeLists.txt │ ├── audio.cpp │ ├── dxgi.cpp │ ├── sunshinesvc.cpp │ ├── utils.cpp │ └── utils.h └── vite.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clang-format ================================================ --- # This file is centrally managed in https://github.com//.github/ # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in # the above-mentioned repo. # Generated from CLion C/C++ Code Style settings BasedOnStyle: LLVM AccessModifierOffset: -2 AlignAfterOpenBracket: BlockIndent AlignConsecutiveAssignments: None AlignEscapedNewlines: DontAlign AlignOperands: Align AllowAllArgumentsOnNextLine: false AllowAllConstructorInitializersOnNextLine: false AllowAllParametersOfDeclarationOnNextLine: false AllowShortBlocksOnASingleLine: Empty AllowShortCaseLabelsOnASingleLine: false AllowShortEnumsOnASingleLine: false AllowShortFunctionsOnASingleLine: Empty AllowShortIfStatementsOnASingleLine: Never AllowShortLambdasOnASingleLine: None AllowShortLoopsOnASingleLine: true AlignTrailingComments: false AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: true AlwaysBreakTemplateDeclarations: MultiLine BinPackArguments: false BinPackParameters: false BracedInitializerIndentWidth: 2 BraceWrapping: AfterCaseLabel: false AfterClass: false AfterControlStatement: Never AfterEnum: false AfterExternBlock: true AfterFunction: false AfterNamespace: false AfterObjCDeclaration: false AfterUnion: false BeforeCatch: true BeforeElse: true IndentBraces: false SplitEmptyFunction: false SplitEmptyRecord: true BreakArrays: true BreakBeforeBinaryOperators: None BreakBeforeBraces: Attach BreakBeforeTernaryOperators: false BreakConstructorInitializers: AfterColon BreakInheritanceList: AfterColon ColumnLimit: 0 CompactNamespaces: false ContinuationIndentWidth: 2 Cpp11BracedListStyle: true EmptyLineAfterAccessModifier: Never EmptyLineBeforeAccessModifier: Always ExperimentalAutoDetectBinPacking: true FixNamespaceComments: true IncludeBlocks: Regroup IndentAccessModifiers: false IndentCaseBlocks: true IndentCaseLabels: true IndentExternBlock: Indent IndentGotoLabels: true IndentPPDirectives: BeforeHash IndentWidth: 2 IndentWrappedFunctionNames: true InsertBraces: true InsertNewlineAtEOF: true KeepEmptyLinesAtTheStartOfBlocks: false MaxEmptyLinesToKeep: 1 NamespaceIndentation: All ObjCBinPackProtocolList: Never ObjCSpaceAfterProperty: true ObjCSpaceBeforeProtocolList: true PackConstructorInitializers: Never PenaltyBreakBeforeFirstCallParameter: 1 PenaltyBreakComment: 1 PenaltyBreakString: 1 PenaltyBreakFirstLessLess: 0 PenaltyExcessCharacter: 1000000 PenaltyReturnTypeOnItsOwnLine: 100000000 PointerAlignment: Right ReferenceAlignment: Pointer ReflowComments: true RemoveBracesLLVM: false RemoveSemicolon: false SeparateDefinitionBlocks: Always SortIncludes: CaseInsensitive SortUsingDeclarations: Lexicographic SpaceAfterCStyleCast: true SpaceAfterLogicalNot: false SpaceAfterTemplateKeyword: false SpaceBeforeAssignmentOperators: true SpaceBeforeCaseColon: false SpaceBeforeCpp11BracedList: true SpaceBeforeCtorInitializerColon: false SpaceBeforeInheritanceColon: false SpaceBeforeJsonColon: false SpaceBeforeParens: ControlStatements SpaceBeforeRangeBasedForLoopColon: true SpaceBeforeSquareBrackets: false SpaceInEmptyBlock: false SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 2 SpacesInAngles: Never SpacesInCStyleCastParentheses: false SpacesInContainerLiterals: false SpacesInLineCommentPrefix: Maximum: 3 Minimum: 1 SpacesInParentheses: false SpacesInSquareBrackets: false TabWidth: 2 UseTab: Never ================================================ FILE: .dockerignore ================================================ # ignore hidden files .* # do not ignore .git, needed for versioning !/.git # do not ignore .rstcheck.cfg, needed to test building docs !/.rstcheck.cfg # ignore repo directories and files docker/ gh-pages-template/ scripts/ tools/ crowdin.yml # don't ignore linux build script !scripts/linux_build.sh # ignore dev directories build/ cmake-*/ venv/ # ignore artifacts artifacts/ ================================================ FILE: .flake8 ================================================ [flake8] filename = *.py max-line-length = 120 extend-exclude = .venv/ venv/ ================================================ FILE: .gitattributes ================================================ # ensure Linux specific files are checked out with LF line endings Dockerfile text eol=lf *.dockerfile text eol=lf *flatpak-lint-*.json text eol=lf *.sh text eol=lf ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing Guidelines Thank you for your interest in contributing to this project! We welcome contributions from the community and appreciate your efforts to make this project better. ## How to Contribute ### Reporting Issues - Use the GitHub issue tracker to report bugs or request features - Before creating a new issue, please search existing issues to avoid duplicates - Provide clear, detailed descriptions with steps to reproduce bugs - Include relevant system information and logs when applicable ### Code Contributions #### Getting Started 1. Fork the repository 2. Create a new branch for your feature or bugfix: `git checkout -b feature/your-feature-name` 3. Make your changes 4. Test your changes thoroughly 5. Commit your changes with clear, descriptive messages 6. Push your branch to your fork 7. Create a pull request #### Code Standards - Follow the existing code style and formatting conventions - Write clear, readable code with appropriate comments - Ensure all changes are sound - Keep commits focused and atomic #### Pull Request Guidelines - Provide a clear description of what your PR does - Reference any related issues using keywords like "Fixes #123" - Include screenshots or examples if your changes affect the UI - Be responsive to feedback and suggestions during code review ## Important Rules ### AI-Generated Code Policy **AI-generated code is acceptable, but please make sure you have thoroughly reviewed and understand what it does.** When using AI tools to generate code: - Review every line of generated code carefully - Understand the logic and potential implications - Test the code thoroughly in your environment - Ensure it follows project conventions and best practices - Take responsibility for any issues that may arise from the generated code ***IMPORTANT: Don't trust AI generated tests. Test each modifications manually.*** ### Code Review Process - All contributions must go through code review - Maintainers will review your pull request and provide feedback - Address all feedback before the PR can be merged - Be patient and respectful during the review process ## Development Setup Please refer to the README.md file for instructions on setting up your development environment. ## Questions? If you have questions about contributing, feel free to: - Open an issue for discussion - Contact the maintainers - Check existing documentation Thank you for contributing! ================================================ FILE: .github/copilot-instructions.md ================================================ On Windows we use msys2 and ucrt64 to compile. You need to prefix commands with `C:\msys64\msys2_shell.cmd -defterm -here -no-start -ucrt64 -c`. Prefix build directories with `cmake-build-`. The test executable is named `test_sunshine` and will be located inside the `tests` directory within the build directory. The project uses gtest as a test framework. Always follow the style guidelines defined in .clang-format for c/c++ code. ================================================ FILE: .github/matchers/copr-ci.json ================================================ { "problemMatcher": [ { "owner": "copr-ci-gcc", "pattern": [ { "regexp": "^/?(?:[^/]+/){5}([^:]+):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", "file": 1, "line": 2, "column": 3, "severity": 4, "message": 5 } ] } ] } ================================================ FILE: .github/matchers/docker.json ================================================ { "problemMatcher": [ { "owner": "docker-gcc", "pattern": [ { "regexp": "^(?:#\\d+\\s+\\d+\\.\\d+\\s+)?/?(?:[^/]+/){2}([^:]+):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", "file": 1, "line": 2, "column": 3, "severity": 4, "message": 5 } ] } ] } ================================================ FILE: .github/matchers/gcc-strip3.json ================================================ { "problemMatcher": [ { "owner": "gcc-strip3", "pattern": [ { "regexp": "^/?(?:[^/]+/){3}([^:]+):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", "file": 1, "line": 2, "column": 3, "severity": 4, "message": 5 } ] } ] } ================================================ FILE: .github/matchers/gcc.json ================================================ { "problemMatcher": [ { "owner": "gcc", "pattern": [ { "regexp": "^(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", "file": 1, "line": 2, "column": 3, "severity": 4, "message": 5 } ] }, { "owner": "doxygen", "pattern": [ { "regexp": "^.*?([A-Za-z]:[\\\\/][^:]+|[\\\\/][^:]+):(\\d+): ([a-zA-Z]+): (.+)$", "file": 1, "line": 2, "severity": 3, "message": 4 } ] } ] } ================================================ FILE: .gitignore ================================================ # Prerequisites *.d # Compiled Object files *.slo *.lo *.o *.obj # Precompiled Headers *.gch *.pch # Compiled Dynamic libraries *.so *.dylib *.dll # Fortran module files *.mod *.smod # Compiled Static libraries *.lai *.la *.a *.lib # Executables *.exe *.out *.app # JetBrains IDE .idea/ # VSCode IDE .vscode/ # build directories build/ cmake-*/ docs/doxyconfig* # npm node_modules/ package-lock.json # Translations *.mo *.pot # Dummy macOS files .DS_Store # Python *.pyc venv/ ================================================ FILE: .gitmodules ================================================ [submodule "packaging/linux/flatpak/deps/flatpak-builder-tools"] path = packaging/linux/flatpak/deps/flatpak-builder-tools url = https://github.com/flatpak/flatpak-builder-tools.git branch = master [submodule "packaging/linux/flatpak/deps/shared-modules"] path = packaging/linux/flatpak/deps/shared-modules url = https://github.com/flathub/shared-modules.git branch = master [submodule "third-party/build-deps"] path = third-party/build-deps url = https://github.com/LizardByte/build-deps.git branch = dist [submodule "third-party/doxyconfig"] path = third-party/doxyconfig url = https://github.com/LizardByte/doxyconfig.git branch = master [submodule "third-party/googletest"] path = third-party/googletest url = https://github.com/google/googletest.git branch = main [submodule "third-party/inputtino"] path = third-party/inputtino url = https://github.com/games-on-whales/inputtino.git branch = stable [submodule "third-party/libdisplaydevice"] path = third-party/libdisplaydevice url = https://github.com/LizardByte/libdisplaydevice.git branch = master [submodule "third-party/nanors"] path = third-party/nanors url = https://github.com/sleepybishop/nanors.git branch = master [submodule "third-party/nv-codec-headers"] path = third-party/nv-codec-headers url = https://github.com/FFmpeg/nv-codec-headers.git branch = sdk/12.0 [submodule "third-party/nvapi-open-source-sdk"] path = third-party/nvapi-open-source-sdk url = https://github.com/LizardByte/nvapi-open-source-sdk.git branch = sdk [submodule "third-party/Simple-Web-Server"] path = third-party/Simple-Web-Server url = https://github.com/ClassicOldSong/Simple-Web-Server branch = master [submodule "third-party/TPCircularBuffer"] path = third-party/TPCircularBuffer url = https://github.com/michaeltyson/TPCircularBuffer.git branch = master [submodule "third-party/tray"] path = third-party/tray url = https://github.com/LizardByte/tray.git branch = master [submodule "third-party/ViGEmClient"] path = third-party/ViGEmClient url = https://github.com/LizardByte/Virtual-Gamepad-Emulation-Client.git branch = master [submodule "third-party/wayland-protocols"] path = third-party/wayland-protocols url = https://gitlab.freedesktop.org/wayland/wayland-protocols.git branch = main [submodule "third-party/wlr-protocols"] path = third-party/wlr-protocols url = https://gitlab.freedesktop.org/wlroots/wlr-protocols.git branch = master [submodule "third-party/moonlight-common-c"] path = third-party/moonlight-common-c url = https://github.com/ClassicOldSong/moonlight-common-c branch = master ================================================ FILE: .prettierrc.json ================================================ {} ================================================ FILE: .readthedocs.yaml ================================================ --- # .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 build: os: ubuntu-24.04 tools: python: "miniconda-latest" commands: - | if [ -f readthedocs_build.sh ]; then doxyconfig_dir="." else doxyconfig_dir="./third-party/doxyconfig" fi chmod +x "${doxyconfig_dir}/readthedocs_build.sh" export DOXYCONFIG_DIR="${doxyconfig_dir}" "${doxyconfig_dir}/readthedocs_build.sh" # using conda, we can get newer doxygen and graphviz than ubuntu provide # https://github.com/readthedocs/readthedocs.org/issues/8151#issuecomment-890359661 conda: environment: third-party/doxyconfig/environment.yml submodules: include: all recursive: true ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.20) # `CMAKE_CUDA_ARCHITECTURES` requires 3.18 # `set_source_files_properties` requires 3.18 # `cmake_path(CONVERT ... TO_NATIVE_PATH_LIST ...)` requires 3.20 # todo - set this conditionally project(Apollo VERSION 0.0.0 DESCRIPTION "Self-hosted game stream host for Artemis" HOMEPAGE_URL "https://github.com/ClassicOldSong/Apollo") set(PROJECT_LICENSE "GPL-3.0-only") set(PROJECT_FQDN "dev.lizardbyte.app.Sunshine") set(PROJECT_BRIEF_DESCRIPTION "GameStream host for Artemis") # must be <= 35 characters set(PROJECT_LONG_DESCRIPTION "Offering low latency, cloud gaming server capabilities with support for AMD, Intel, \ and Nvidia GPUs for hardware encoding. Software encoding is also available. You can connect to Apollo from any \ Artemis client on a variety of devices. A web UI is provided to allow configuration, and client pairing, from \ your favorite web browser. Pair from the local server or any mobile device.") if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) message(STATUS "Setting build type to 'Release' as none was specified.") set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) endif() # set the module path, used for includes set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") # export compile_commands.json set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # set version info for this build include(${CMAKE_MODULE_PATH}/prep/build_version.cmake) # cmake build flags include(${CMAKE_MODULE_PATH}/prep/options.cmake) # initial prep include(${CMAKE_MODULE_PATH}/prep/init.cmake) # configure special package files, such as sunshine.desktop, Flatpak manifest, Portfile , etc. include(${CMAKE_MODULE_PATH}/prep/special_package_configuration.cmake) # Exit early if END_BUILD is ON, i.e. when only generating package manifests if(${END_BUILD}) return() endif() # project constants include(${CMAKE_MODULE_PATH}/prep/constants.cmake) # load macros include(${CMAKE_MODULE_PATH}/macros/common.cmake) # load dependencies include(${CMAKE_MODULE_PATH}/dependencies/common.cmake) # setup compile definitions include(${CMAKE_MODULE_PATH}/compile_definitions/common.cmake) # target definitions include(${CMAKE_MODULE_PATH}/targets/common.cmake) # packaging include(${CMAKE_MODULE_PATH}/packaging/common.cmake) ================================================ FILE: DOCKER_README.md ================================================ # Docker ## Important note Starting with v0.18.0, tag names have changed. You may no longer use `latest`, `master`, `vX.X.X`. ## Build your own containers This image provides a method for you to easily use the latest Sunshine release in your own docker projects. It is not intended to use as a standalone container at this point, and should be considered experimental. ```dockerfile ARG SUNSHINE_VERSION=latest ARG SUNSHINE_OS=ubuntu-22.04 FROM lizardbyte/sunshine:${SUNSHINE_VERSION}-${SUNSHINE_OS} # install Steam, Wayland, etc. ENTRYPOINT steam && sunshine ``` ### SUNSHINE_VERSION - `latest`, `master`, `vX.X.X` - commit hash ### SUNSHINE_OS Sunshine images are available with the following tag suffixes, based on their respective base images. - `archlinux` - `debian-bookworm` - `ubuntu-22.04` - `ubuntu-24.04` ### Tags You must combine the `SUNSHINE_VERSION` and `SUNSHINE_OS` to determine the tag to pull. The format should be `-`. For example, `latest-ubuntu-24.04`. See all our available tags on [docker hub](https://hub.docker.com/r/lizardbyte/sunshine/tags) or [ghcr](https://github.com/LizardByte/Sunshine/pkgs/container/sunshine/versions) for more info. ## Where used This is a list of docker projects using Sunshine. Something missing? Let us know about it! - [Games on Whales](https://games-on-whales.github.io) ## Port and Volume mappings Examples are below of the required mappings. The configuration file will be saved to `/config` in the container. ### Using docker run Create and run the container (substitute your ``): ```bash docker run -d \ --device /dev/dri/ \ --name= \ --restart=unless-stopped \ --ipc=host \ -e PUID= \ -e PGID= \ -e TZ= \ -v :/config \ -p 47984-47990:47984-47990/tcp \ -p 48010:48010 \ -p 47998-48000:47998-48000/udp \ ``` ### Using docker-compose Create a `docker-compose.yml` file with the following contents (substitute your ``): ```yaml version: '3' services: : image: container_name: sunshine restart: unless-stopped volumes: - :/config environment: - PUID= - PGID= - TZ= ipc: host ports: - "47984-47990:47984-47990/tcp" - "48010:48010" - "47998-48000:47998-48000/udp" ``` ### Using podman run Create and run the container (substitute your ``): ```bash podman run -d \ --device /dev/dri/ \ --name= \ --restart=unless-stopped \ --userns=keep-id \ -e PUID= \ -e PGID= \ -e TZ= \ -v :/config \ -p 47984-47990:47984-47990/tcp \ -p 48010:48010 \ -p 47998-48000:47998-48000/udp \ ``` ### Parameters You must substitute the `` with your own settings. Parameters are split into two halves separated by a colon. The left side represents the host and the right side the container. **Example:** `-p external:internal` - This shows the port mapping from internal to external of the container. Therefore `-p 47990:47990` would expose port `47990` from inside the container to be accessible from the host's IP on port `47990` (e.g. `http://:47990`). The internal port must be `47990`, but the external port may be changed (e.g. `-p 8080:47990`). All the ports listed in the `docker run` and `docker-compose` examples are required. | Parameter | Function | Example Value | Required | |-----------------------------|----------------------|--------------------|----------| | `-p :47990` | Web UI Port | `47990` | True | | `-v :/config` | Volume mapping | `/home/sunshine` | True | | `-e PUID=` | User ID | `1001` | False | | `-e PGID=` | Group ID | `1001` | False | | `-e TZ=` | Lookup [TZ value][1] | `America/New_York` | False | For additional configuration, it is recommended to reference the *Games on Whales* [sunshine config](https://github.com/games-on-whales/gow/blob/2e442292d79b9d996f886b8a03d22b6eb6bddf7b/compose/streamers/sunshine.yml). [1]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones #### User / Group Identifiers: When using data volumes (-v flags) permissions issues can arise between the host OS and the container. To avoid this issue you can specify the user PUID and group PGID. Ensure the data volume directory on the host is owned by the same user you specify. In this instance `PUID=1001` and `PGID=1001`. To find yours use id user as below: ```bash $ id dockeruser uid=1001(dockeruser) gid=1001(dockergroup) groups=1001(dockergroup) ``` If you want to change the PUID or PGID after the image has been built, it will require rebuilding the image. ## Supported Architectures Specifying `lizardbyte/sunshine:latest-` or `ghcr.io/lizardbyte/sunshine:latest-` should retrieve the correct image for your architecture. The architectures supported by these images are shown in the table below. | tag suffix | amd64/x86_64 | arm64/aarch64 | |-----------------|--------------|---------------| | archlinux | ✅ | ❌ | | debian-bookworm | ✅ | ✅ | | ubuntu-22.04 | ✅ | ✅ | | ubuntu-24.04 | ✅ | ✅ |
| Previous | Next | |:-------------------------------|-----------------------------------------------------:| | [Changelog](docs/changelog.md) | [Third-Party Packages](docs/third_party_packages.md) |
[TOC]
================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: NOTICE ================================================ ©2018 Valve Corporation. Steam and the Steam logo are trademarks and/or registered trademarks of Valve Corporation in the U.S. and/or other countries. All rights reserved. ================================================ FILE: README.md ================================================ # Apollo Apollo is a self-hosted desktop stream host for [Artemis(Moonlight Noir)](https://github.com/ClassicOldSong/moonlight-android). Offering low latency, native client resolution, cloud gaming server capabilities with support for AMD, Intel, and Nvidia GPUs for hardware encoding. Software encoding is also available. A web UI is provided to allow configuration and client pairing from your favorite web browser. Pair from the local server or any mobile device. Major features: - [x] Built-in Virtual Display with HDR support that matches the resolution/framerate config of your client automatically - [x] Permission management for clients - [x] Clipboard sync - [x] Commands for client connection/disconnection (checkout [Auto pause/resume games](https://github.com/ClassicOldSong/Apollo/wiki/Auto-pause-resume-games)) - [x] Input only mode ## Usage Refer to LizardByte's documentation hosted on [Read the Docs](https://docs.lizardbyte.dev/projects/sunshine) for now. Currently Virtual Display support is Windows only, Linux support is planned and will be implemented in the future. ## About Permission System Check out the [Wiki](https://github.com/ClassicOldSong/Apollo/wiki/Permission-System) > [!NOTE] > The **FIRST** client paired with Apollo will be granted with FULL permissions, then other newly paired clients will only be granted with `View Streams` and `List Apps` permission. If you encounter `Permission Denied` error when trying to launch any app, go check the permission for that device and grant `Launch Apps` permission. The same applies to the situation when you find that you can't move mouse or type with keyboard on newly paired clients, grant the corresponding client `Mouse Input` and `Keyboard Input` permissions. ## About Virtual Display > [!WARNING] > ***It is highly recommend to remove any other virtual display solutions from your system and Apollo/Sunshine config, to reduce confusions and compatibility issues.*** > [!NOTE] > **TL;DR** Just treat your Artemis/Moonlight client like a dedicated PnP monitor with Apollo. Apollo uses SudoVDA for virtual display. It features auto resolution and framerate matching for your Artemis/Moonlight clients. The virtual display is created upon the stream starts and removed once the app quits. **If you do not see a new virtual display added or removed when the stream starts or stops, there may be a driver misconfiguration, or another persistent virtual display might still be active.** The virtual display works just like any physically attached monitors with SudoVDA, there's completely no need for a super complicated solution to "fix" resolution configurations for your devices. Unlike all other solutions that reuses one identity or generate a random one each time for any virtual display sessions, **Apollo assigns a fixed identity for each Artemis/Moonlight client, so your display configuration will be automatically remembered and managed by Windows natively.** ## Configuration for dual GPU laptops Apollo supports dual GPUs seamlessly. If you want to use your dGPU, just set the `Adapter Name` to your dGPU and enable `Headless mode` in `Audio/Video` tab, save and restart your computer. No dummy plug is needed any more, the image will be rendered and encoded directly from your dGPU. ## About HDR HDR starts supporting from Windows 11 23H2 and generally supported on 24H2. Some systems might not have HDR toggle on 23H2 and you just need to upgrade to 24H2. Any system lower than 23H2/Windows 10 will not have HDR option available. > [!NOTE] > The below section is written for professional media workers. It doesn't stop you from enabling HDR if you know what you're doing and have deep understanding about how HDR works. > > Apollo and SudoVDA can handle HDR just fine like any other streaming solutions. > > If you have had good experience with HDR previously, you can safely ignore this section. > > If you're curious, read on, but don't blame Apollo for poor HDR support. Whether HDR streaming looks good, it depends completely on your client. In short, ICC color correction should be totally useless while streaming HDR. It's your client's job to get HDR content displayed right, not the host. But in fact, it does affect the captured video stream and reflect changes on devices that can handle HDR correctly. On other devices that can't, the info is not respected at all. It's very complicated to explain why HDR is a total mess, and why enabling HDR makes the image appear dark/yellow. If it's your first time got HDR streaming working, and thinks HDR looks awful, you're right, but that's not Apollo's fault, it's your device that tone mapped SDR content to the maximum of the capability of its screen, there's no headroom for anything beyond that actual peak brightness for HDR. For details, please take a look [here](https://github.com/ClassicOldSong/Apollo/issues/164). For client devices, usually Apple products that have HDR capability can be trusted to have good results, other than that, your luck depends.
DEPRECATION ALERT Enabling HDR is **generally not recommended** with **ANY streaming solutions** at this moment, probably in the long term. The issue with **HDR itself** is huge, with loads of semi-incompatible standards, and massive variance between device configurations and capabilities. Game support for HDR is still choppy. SDR actually provides much more stable color accuracy, and are widely supported throughout most devices you can imagine. For games, art style can easily overcome the shortcoming with no HDR, and SDR has pretty standard workflows to ensure their visual performance. So HDR isn't *that* important in most of the cases.
## How to run multiple instances of Apollo for multiple virtual displays Follow the instructions in the [Wiki](https://github.com/ClassicOldSong/Apollo/wiki/How-to-start-multiple-instances-of-Apollo). ## FAQ Moved to [WiKi](https://github.com/ClassicOldSong/Apollo/wiki/FAQ) ## Stuttering Clinic Here're some common causes and solutions for stutters: [WiKi](https://github.com/ClassicOldSong/Apollo/wiki/Stuttering-Clinic). ## Device specific setups - Pixel devices might not be able to use native resolution: - Change the device resolution to High: https://github.com/ClassicOldSong/Apollo/issues/700 ## System Requirements > **Warning**: This table is a work in progress. Do not purchase hardware based on this. **Minimum Requirements** | **Component** | **Description** | |---------------|-----------------| | GPU | AMD: VCE 1.0 or higher, see: [obs-amd hardware support](https://github.com/obsproject/obs-amd-encoder/wiki/Hardware-Support) | | | Intel: VAAPI-compatible, see: [VAAPI hardware support](https://www.intel.com/content/www/us/en/developer/articles/technical/linuxmedia-vaapi.html) | | | Nvidia: NVENC enabled cards, see: [nvenc support matrix](https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new) | | CPU | AMD: Ryzen 3 or higher | | | Intel: Core i3 or higher | | RAM | 4GB or more | | OS | Windows: 10+ (Windows Server requires [manual installation](https://github.com/nefarius/ViGEmBus/issues/153) for gamepad support) | | | macOS: 12+ | | | Linux/Debian: 11 (bullseye) | | | Linux/Fedora: 39+ | | | Linux/Ubuntu: 22.04+ (jammy) | | Network | Host: 5GHz, 802.11ac | | | Client: 5GHz, 802.11ac | **4k Suggestions** | **Component** | **Description** | |---------------|-----------------| | GPU | AMD: Video Coding Engine 3.1 or higher | | | Intel: HD Graphics 510 or higher | | | Nvidia: GeForce GTX 1080 or higher | | CPU | AMD: Ryzen 5 or higher | | | Intel: Core i5 or higher | | Network | Host: CAT5e ethernet or better | | | Client: CAT5e ethernet or better | **HDR Suggestions** | **Component** | **Description** | |---------------|-----------------| | GPU | AMD: Video Coding Engine 3.4 or higher | | | Intel: UHD Graphics 730 or higher | | | Nvidia: Pascal-based GPU (GTX 10-series) or higher | | CPU | AMD: todo | | | Intel: todo | | Network | Host: CAT5e ethernet or better | | | Client: CAT5e ethernet or better | ## Integrations SudoVDA: Virtual Display Adapter Driver used in Apollo [Artemis](https://github.com/ClassicOldSong/moonlight-android): Integrated Virtual Display options control from client side **NOTE**: Artemis currently supports Android only. Other platforms will come later. ## Support Currently support is only provided via GitHub Issues/Discussions. No real time chat support will ever be provided for Apollo and Artemis. Including but not limited to: - Discord - Telegram - Whatsapp - QQ - WeChat > When there's a chat, there're dramas. -- Confucius ## Downloads ### Direct Download **Recommended** [Releases](https://github.com/ClassicOldSong/Apollo/releases) ### WinGet **Note:** Community maintained In an elevated PowerShell window, run ```pwsh winget install ClassicOldSong.Apollo ``` You'll need WinGet installed first. ### Chocolatey **Note:** Community maintained You can also install the apollo streaming server with chocolatey. Install Chocolatey if you don't have it, then run the following command in an elevated PowerShell/CMD window: ```pwsh choco upgrade apollo -y ``` Same command can be used to upgrade, add to a scheduled task to automate updates. See more details on the chocolatey package [here](https://community.chocolatey.org/packages/apollo) ## Disclaimer I got kicked from Moonlight and Sunshine's Discord server and banned from Sunshine's GitHub repo literally for helping people out. This is what I got for finding a bug, opened an issue, getting no response, troubleshoot myself, fixed the issue myself, shared it by PR to the main repo hoping my efforts can help someone else during the maintenance gap. Yes, I'm going away. [Apollo](https://github.com/ClassicOldSong/Apollo) and [Artemis(Moonlight Noir)](https://github.com/ClassicOldSong/moonlight-android) will no longer be compatible with OG Sunshine and OG Moonlight eventually, but they'll work even better with much more carefully designed features. The Moonlight repo had stayed silent for 5 months, with nobody actually responding to issues, and people are getting totally no help besides the limited FAQ in their Discord server. I tried to answer issues and questions, solve problems within my ability but I got kicked out just for helping others. **PRs for feature improvements are welcomed here unlike the main repo, your ideas are more likely to be appreciated and your efforts are actually being respected. We welcome people who can and willing to share their efforts, helping yourselves and other people in need.** **Update**: They have contacted me and apologized for this incident, but the fact it **happened** still motivated me to start my own fork. ## License GPLv3 ================================================ FILE: cmake/FindLIBCAP.cmake ================================================ # - Try to find Libcap # Once done this will define # # LIBCAP_FOUND - system has Libcap # LIBCAP_INCLUDE_DIRS - the Libcap include directory # LIBCAP_LIBRARIES - the libraries needed to use Libcap # LIBCAP_DEFINITIONS - Compiler switches required for using Libcap # Use pkg-config to get the directories and then use these values # in the find_path() and find_library() calls find_package(PkgConfig) pkg_check_modules(PC_LIBCAP libcap) set(LIBCAP_DEFINITIONS ${PC_LIBCAP_CFLAGS}) find_path(LIBCAP_INCLUDE_DIRS sys/capability.h PATHS ${PC_LIBCAP_INCLUDEDIR} ${PC_LIBCAP_INCLUDE_DIRS}) find_library(LIBCAP_LIBRARIES NAMES libcap.so PATHS ${PC_LIBCAP_LIBDIR} ${PC_LIBCAP_LIBRARY_DIRS}) mark_as_advanced(LIBCAP_INCLUDE_DIRS LIBCAP_LIBRARIES) include(FindPackageHandleStandardArgs) find_package_handle_standard_args(LIBCAP REQUIRED_VARS LIBCAP_LIBRARIES LIBCAP_INCLUDE_DIRS) ================================================ FILE: cmake/FindLIBDRM.cmake ================================================ # - Try to find Libdrm # Once done this will define # # LIBDRM_FOUND - system has Libdrm # LIBDRM_INCLUDE_DIRS - the Libdrm include directory # LIBDRM_LIBRARIES - the libraries needed to use Libdrm # LIBDRM_DEFINITIONS - Compiler switches required for using Libdrm # Use pkg-config to get the directories and then use these values # in the find_path() and find_library() calls find_package(PkgConfig) pkg_check_modules(PC_LIBDRM libdrm) set(LIBDRM_DEFINITIONS ${PC_LIBDRM_CFLAGS}) find_path(LIBDRM_INCLUDE_DIRS drm.h PATHS ${PC_LIBDRM_INCLUDEDIR} ${PC_LIBDRM_INCLUDE_DIRS} PATH_SUFFIXES libdrm) find_library(LIBDRM_LIBRARIES NAMES libdrm.so PATHS ${PC_LIBDRM_LIBDIR} ${PC_LIBDRM_LIBRARY_DIRS}) mark_as_advanced(LIBDRM_INCLUDE_DIRS LIBDRM_LIBRARIES) include(FindPackageHandleStandardArgs) find_package_handle_standard_args(LIBDRM REQUIRED_VARS LIBDRM_LIBRARIES LIBDRM_INCLUDE_DIRS) ================================================ FILE: cmake/FindLibva.cmake ================================================ # - Try to find Libva # This module defines the following variables: # # * LIBVA_FOUND - The component was found # * LIBVA_INCLUDE_DIRS - The component include directory # * LIBVA_LIBRARIES - The component library Libva # * LIBVA_DRM_LIBRARIES - The component library Libva DRM # Use pkg-config to get the directories and then use these values in the # find_path() and find_library() calls # cmake-format: on find_package(PkgConfig QUIET) if(PKG_CONFIG_FOUND) pkg_check_modules(_LIBVA libva) pkg_check_modules(_LIBVA_DRM libva-drm) endif() find_path( LIBVA_INCLUDE_DIR NAMES va/va.h va/va_drm.h HINTS ${_LIBVA_INCLUDE_DIRS} PATHS /usr/include /usr/local/include /opt/local/include) find_library( LIBVA_LIB NAMES ${_LIBVA_LIBRARIES} libva HINTS ${_LIBVA_LIBRARY_DIRS} PATHS /usr/lib /usr/local/lib /opt/local/lib) find_library( LIBVA_DRM_LIB NAMES ${_LIBVA_DRM_LIBRARIES} libva-drm HINTS ${_LIBVA_DRM_LIBRARY_DIRS} PATHS /usr/lib /usr/local/lib /opt/local/lib) include(FindPackageHandleStandardArgs) find_package_handle_standard_args(Libva REQUIRED_VARS LIBVA_INCLUDE_DIR LIBVA_LIB LIBVA_DRM_LIB) mark_as_advanced(LIBVA_INCLUDE_DIR LIBVA_LIB LIBVA_DRM_LIB) if(LIBVA_FOUND) set(LIBVA_INCLUDE_DIRS ${LIBVA_INCLUDE_DIR}) set(LIBVA_LIBRARIES ${LIBVA_LIB}) set(LIBVA_DRM_LIBRARIES ${LIBVA_DRM_LIB}) if(NOT TARGET Libva::va) if(IS_ABSOLUTE "${LIBVA_LIBRARIES}") add_library(Libva::va UNKNOWN IMPORTED) set_target_properties(Libva::va PROPERTIES IMPORTED_LOCATION "${LIBVA_LIBRARIES}") else() add_library(Libva::va INTERFACE IMPORTED) set_target_properties(Libva::va PROPERTIES IMPORTED_LIBNAME "${LIBVA_LIBRARIES}") endif() set_target_properties(Libva::va PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${LIBVA_INCLUDE_DIRS}") endif() if(NOT TARGET Libva::drm) if(IS_ABSOLUTE "${LIBVA_DRM_LIBRARIES}") add_library(Libva::drm UNKNOWN IMPORTED) set_target_properties(Libva::drm PROPERTIES IMPORTED_LOCATION "${LIBVA_DRM_LIBRARIES}") else() add_library(Libva::drm INTERFACE IMPORTED) set_target_properties(Libva::drm PROPERTIES IMPORTED_LIBNAME "${LIBVA_DRM_LIBRARIES}") endif() set_target_properties(Libva::drm PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${LIBVA_INCLUDE_DIRS}") endif() endif() ================================================ FILE: cmake/FindSystemd.cmake ================================================ # - Try to find Systemd # Once done this will define # # SYSTEMD_FOUND - system has systemd # SYSTEMD_USER_UNIT_INSTALL_DIR - the systemd system unit install directory # SYSTEMD_SYSTEM_UNIT_INSTALL_DIR - the systemd user unit install directory # SYSTEMD_MODULES_LOAD_DIR - the systemd modules-load.d directory IF (NOT WIN32) find_package(PkgConfig QUIET) if(PKG_CONFIG_FOUND) pkg_check_modules(SYSTEMD "systemd") endif() if (SYSTEMD_FOUND) execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} --variable=systemd_user_unit_dir systemd OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE SYSTEMD_USER_UNIT_INSTALL_DIR) execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} --variable=systemd_system_unit_dir systemd OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE SYSTEMD_SYSTEM_UNIT_INSTALL_DIR) execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} --variable=modules_load_dir systemd OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE SYSTEMD_MODULES_LOAD_DIR) mark_as_advanced(SYSTEMD_USER_UNIT_INSTALL_DIR SYSTEMD_SYSTEM_UNIT_INSTALL_DIR SYSTEMD_MODULES_LOAD_DIR) endif () ENDIF () ================================================ FILE: cmake/FindUdev.cmake ================================================ # - Try to find Udev # Once done this will define # # UDEV_FOUND - system has udev # UDEV_RULES_INSTALL_DIR - the udev rules install directory # UDEVADM_EXECUTABLE - path to udevadm executable # UDEV_VERSION - version of udev/systemd if(NOT WIN32) find_package(PkgConfig QUIET) if(PKG_CONFIG_FOUND) pkg_check_modules(UDEV "udev") endif() if(UDEV_FOUND) if(UDEV_VERSION) message(STATUS "Found udev/systemd version: ${UDEV_VERSION}") else() message(WARNING "Could not determine udev/systemd version") set(UDEV_VERSION "0") endif() execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} --variable=udev_dir udev OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE UDEV_RULES_INSTALL_DIR) set(UDEV_RULES_INSTALL_DIR "${UDEV_RULES_INSTALL_DIR}/rules.d") mark_as_advanced(UDEV_RULES_INSTALL_DIR) # Check if udevadm is available find_program(UDEVADM_EXECUTABLE udevadm PATHS /usr/bin /bin /usr/sbin /sbin DOC "Path to udevadm executable") mark_as_advanced(UDEVADM_EXECUTABLE) # Handle version requirements if(Udev_FIND_VERSION) if(UDEV_VERSION VERSION_LESS Udev_FIND_VERSION) set(UDEV_FOUND FALSE) if(Udev_FIND_REQUIRED) message(FATAL_ERROR "Udev version ${UDEV_VERSION} less than required version ${Udev_FIND_VERSION}") else() message(STATUS "Udev version ${UDEV_VERSION} less than required version ${Udev_FIND_VERSION}") endif() else() message(STATUS "Udev version ${UDEV_VERSION} meets requirement (>= ${Udev_FIND_VERSION})") endif() endif() endif() endif() ================================================ FILE: cmake/FindWayland.cmake ================================================ # Try to find Wayland on a Unix system # # This will define: # # WAYLAND_FOUND - True if Wayland is found # WAYLAND_LIBRARIES - Link these to use Wayland # WAYLAND_INCLUDE_DIRS - Include directory for Wayland # WAYLAND_DEFINITIONS - Compiler flags for using Wayland # # In addition the following more fine grained variables will be defined: # # Wayland_Client_FOUND WAYLAND_CLIENT_INCLUDE_DIRS WAYLAND_CLIENT_LIBRARIES # Wayland_Server_FOUND WAYLAND_SERVER_INCLUDE_DIRS WAYLAND_SERVER_LIBRARIES # Wayland_EGL_FOUND WAYLAND_EGL_INCLUDE_DIRS WAYLAND_EGL_LIBRARIES # Wayland_Cursor_FOUND WAYLAND_CURSOR_INCLUDE_DIRS WAYLAND_CURSOR_LIBRARIES # # Copyright (c) 2013 Martin Gräßlin # 2020 Georges Basile Stavracas Neto # # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. IF (NOT WIN32) # Use pkg-config to get the directories and then use these values # in the find_path() and find_library() calls find_package(PkgConfig) PKG_CHECK_MODULES(PKG_WAYLAND QUIET wayland-client wayland-server wayland-egl wayland-cursor) set(WAYLAND_DEFINITIONS ${PKG_WAYLAND_CFLAGS}) find_path(WAYLAND_CLIENT_INCLUDE_DIRS NAMES wayland-client.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS}) find_library(WAYLAND_CLIENT_LIBRARIES NAMES wayland-client HINTS ${PKG_WAYLAND_LIBRARY_DIRS}) if(WAYLAND_CLIENT_INCLUDE_DIRS AND WAYLAND_CLIENT_LIBRARIES) set(Wayland_Client_FOUND TRUE) # cmake-lint: disable=C0103 else() set(Wayland_Client_FOUND FALSE) # cmake-lint: disable=C0103 endif() mark_as_advanced(WAYLAND_CLIENT_INCLUDE_DIRS WAYLAND_CLIENT_LIBRARIES) find_path(WAYLAND_CURSOR_INCLUDE_DIRS NAMES wayland-cursor.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS}) find_library(WAYLAND_CURSOR_LIBRARIES NAMES wayland-cursor HINTS ${PKG_WAYLAND_LIBRARY_DIRS}) if(WAYLAND_CURSOR_INCLUDE_DIRS AND WAYLAND_CURSOR_LIBRARIES) set(Wayland_Cursor_FOUND TRUE) # cmake-lint: disable=C0103 else() set(Wayland_Cursor_FOUND FALSE) # cmake-lint: disable=C0103 endif() mark_as_advanced(WAYLAND_CURSOR_INCLUDE_DIRS WAYLAND_CURSOR_LIBRARIES) find_path(WAYLAND_EGL_INCLUDE_DIRS NAMES wayland-egl.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS}) find_library(WAYLAND_EGL_LIBRARIES NAMES wayland-egl HINTS ${PKG_WAYLAND_LIBRARY_DIRS}) if(WAYLAND_EGL_INCLUDE_DIRS AND WAYLAND_EGL_LIBRARIES) set(Wayland_EGL_FOUND TRUE) # cmake-lint: disable=C0103 else() set(Wayland_EGL_FOUND FALSE) # cmake-lint: disable=C0103 endif() mark_as_advanced(WAYLAND_EGL_INCLUDE_DIRS WAYLAND_EGL_LIBRARIES) find_path(WAYLAND_SERVER_INCLUDE_DIRS NAMES wayland-server.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS}) find_library(WAYLAND_SERVER_LIBRARIES NAMES wayland-server HINTS ${PKG_WAYLAND_LIBRARY_DIRS}) if(WAYLAND_SERVER_INCLUDE_DIRS AND WAYLAND_SERVER_LIBRARIES) set(Wayland_Server_FOUND TRUE) # cmake-lint: disable=C0103 else() set(Wayland_Server_FOUND FALSE) # cmake-lint: disable=C0103 endif() mark_as_advanced(WAYLAND_SERVER_INCLUDE_DIRS WAYLAND_SERVER_LIBRARIES) set(WAYLAND_INCLUDE_DIRS ${WAYLAND_CLIENT_INCLUDE_DIRS} ${WAYLAND_SERVER_INCLUDE_DIRS} ${WAYLAND_EGL_INCLUDE_DIRS} ${WAYLAND_CURSOR_INCLUDE_DIRS}) set(WAYLAND_LIBRARIES ${WAYLAND_CLIENT_LIBRARIES} ${WAYLAND_SERVER_LIBRARIES} ${WAYLAND_EGL_LIBRARIES} ${WAYLAND_CURSOR_LIBRARIES}) mark_as_advanced(WAYLAND_INCLUDE_DIRS WAYLAND_LIBRARIES) list(REMOVE_DUPLICATES WAYLAND_INCLUDE_DIRS) include(FindPackageHandleStandardArgs) find_package_handle_standard_args(Wayland REQUIRED_VARS WAYLAND_LIBRARIES WAYLAND_INCLUDE_DIRS HANDLE_COMPONENTS) ENDIF () ================================================ FILE: cmake/compile_definitions/common.cmake ================================================ # common compile definitions # this file will also load platform specific definitions list(APPEND SUNSHINE_COMPILE_OPTIONS -Wall -Wno-sign-compare) # Wall - enable all warnings # Werror - treat warnings as errors # Wno-maybe-uninitialized/Wno-uninitialized - disable warnings for maybe uninitialized variables # Wno-sign-compare - disable warnings for signed/unsigned comparisons # Wno-restrict - disable warnings for memory overlap if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") # GCC specific compile options # GCC 12 and higher will complain about maybe-uninitialized if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 12) list(APPEND SUNSHINE_COMPILE_OPTIONS -Wno-maybe-uninitialized) # Disable the bogus warning that may prevent compilation (only for GCC 12). # See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105651. if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 13) list(APPEND SUNSHINE_COMPILE_OPTIONS -Wno-restrict) endif() endif() elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") # Clang specific compile options # Clang doesn't actually complain about this this, so disabling for now # list(APPEND SUNSHINE_COMPILE_OPTIONS -Wno-uninitialized) endif() if(BUILD_WERROR) list(APPEND SUNSHINE_COMPILE_OPTIONS -Werror) endif() # setup assets directory if(NOT SUNSHINE_ASSETS_DIR) set(SUNSHINE_ASSETS_DIR "assets") endif() # platform specific compile definitions if(WIN32) include(${CMAKE_MODULE_PATH}/compile_definitions/windows.cmake) elseif(UNIX) include(${CMAKE_MODULE_PATH}/compile_definitions/unix.cmake) if(APPLE) include(${CMAKE_MODULE_PATH}/compile_definitions/macos.cmake) else() include(${CMAKE_MODULE_PATH}/compile_definitions/linux.cmake) endif() endif() include_directories(BEFORE SYSTEM "${CMAKE_SOURCE_DIR}/third-party/nv-codec-headers/include") file(GLOB NVENC_SOURCES CONFIGURE_DEPENDS "src/nvenc/*.cpp" "src/nvenc/*.h") list(APPEND PLATFORM_TARGET_FILES ${NVENC_SOURCES}) set(SUNSHINE_TARGET_FILES "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/Input.h" "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/Rtsp.h" "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/RtspParser.c" "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/Video.h" "${CMAKE_SOURCE_DIR}/third-party/tray/src/tray.h" "${CMAKE_SOURCE_DIR}/src/upnp.cpp" "${CMAKE_SOURCE_DIR}/src/upnp.h" "${CMAKE_SOURCE_DIR}/src/cbs.cpp" "${CMAKE_SOURCE_DIR}/src/utility.h" "${CMAKE_SOURCE_DIR}/src/uuid.h" "${CMAKE_SOURCE_DIR}/src/config.h" "${CMAKE_SOURCE_DIR}/src/config.cpp" "${CMAKE_SOURCE_DIR}/src/display_device.h" "${CMAKE_SOURCE_DIR}/src/display_device.cpp" "${CMAKE_SOURCE_DIR}/src/entry_handler.cpp" "${CMAKE_SOURCE_DIR}/src/entry_handler.h" "${CMAKE_SOURCE_DIR}/src/file_handler.cpp" "${CMAKE_SOURCE_DIR}/src/file_handler.h" "${CMAKE_SOURCE_DIR}/src/globals.cpp" "${CMAKE_SOURCE_DIR}/src/globals.h" "${CMAKE_SOURCE_DIR}/src/logging.cpp" "${CMAKE_SOURCE_DIR}/src/logging.h" "${CMAKE_SOURCE_DIR}/src/main.cpp" "${CMAKE_SOURCE_DIR}/src/main.h" "${CMAKE_SOURCE_DIR}/src/crypto.cpp" "${CMAKE_SOURCE_DIR}/src/crypto.h" "${CMAKE_SOURCE_DIR}/src/nvhttp.cpp" "${CMAKE_SOURCE_DIR}/src/nvhttp.h" "${CMAKE_SOURCE_DIR}/src/httpcommon.cpp" "${CMAKE_SOURCE_DIR}/src/httpcommon.h" "${CMAKE_SOURCE_DIR}/src/confighttp.cpp" "${CMAKE_SOURCE_DIR}/src/confighttp.h" "${CMAKE_SOURCE_DIR}/src/rtsp.cpp" "${CMAKE_SOURCE_DIR}/src/rtsp.h" "${CMAKE_SOURCE_DIR}/src/stream.cpp" "${CMAKE_SOURCE_DIR}/src/stream.h" "${CMAKE_SOURCE_DIR}/src/video.cpp" "${CMAKE_SOURCE_DIR}/src/video.h" "${CMAKE_SOURCE_DIR}/src/video_colorspace.cpp" "${CMAKE_SOURCE_DIR}/src/video_colorspace.h" "${CMAKE_SOURCE_DIR}/src/input.cpp" "${CMAKE_SOURCE_DIR}/src/input.h" "${CMAKE_SOURCE_DIR}/src/audio.cpp" "${CMAKE_SOURCE_DIR}/src/audio.h" "${CMAKE_SOURCE_DIR}/src/platform/common.h" "${CMAKE_SOURCE_DIR}/src/process.cpp" "${CMAKE_SOURCE_DIR}/src/process.h" "${CMAKE_SOURCE_DIR}/src/network.cpp" "${CMAKE_SOURCE_DIR}/src/network.h" "${CMAKE_SOURCE_DIR}/src/move_by_copy.h" "${CMAKE_SOURCE_DIR}/src/system_tray.cpp" "${CMAKE_SOURCE_DIR}/src/system_tray.h" "${CMAKE_SOURCE_DIR}/src/task_pool.h" "${CMAKE_SOURCE_DIR}/src/thread_pool.h" "${CMAKE_SOURCE_DIR}/src/thread_safe.h" "${CMAKE_SOURCE_DIR}/src/sync.h" "${CMAKE_SOURCE_DIR}/src/round_robin.h" "${CMAKE_SOURCE_DIR}/src/stat_trackers.h" "${CMAKE_SOURCE_DIR}/src/stat_trackers.cpp" "${CMAKE_SOURCE_DIR}/src/rswrapper.h" "${CMAKE_SOURCE_DIR}/src/rswrapper.c" ${PLATFORM_TARGET_FILES}) if(NOT SUNSHINE_ASSETS_DIR_DEF) set(SUNSHINE_ASSETS_DIR_DEF "${SUNSHINE_ASSETS_DIR}") endif() list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_ASSETS_DIR="${SUNSHINE_ASSETS_DIR_DEF}") list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_TRAY=${SUNSHINE_TRAY}) # Publisher metadata list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_PUBLISHER_NAME="${SUNSHINE_PUBLISHER_NAME}") list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_PUBLISHER_WEBSITE="${SUNSHINE_PUBLISHER_WEBSITE}") list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_PUBLISHER_ISSUE_URL="${SUNSHINE_PUBLISHER_ISSUE_URL}") include_directories(BEFORE "${CMAKE_SOURCE_DIR}") include_directories( BEFORE SYSTEM "${CMAKE_SOURCE_DIR}/third-party" "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/enet/include" "${CMAKE_SOURCE_DIR}/third-party/nanors" "${CMAKE_SOURCE_DIR}/third-party/nanors/deps/obl" ${FFMPEG_INCLUDE_DIRS} ${Boost_INCLUDE_DIRS} # has to be the last, or we get runtime error on macOS ffmpeg encoder ) list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${MINIUPNP_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} enet libdisplaydevice::display_device nlohmann_json::nlohmann_json opus ${FFMPEG_LIBRARIES} ${Boost_LIBRARIES} ${OPENSSL_LIBRARIES} ${PLATFORM_LIBRARIES}) ================================================ FILE: cmake/compile_definitions/linux.cmake ================================================ # linux specific compile definitions add_compile_definitions(SUNSHINE_PLATFORM="linux") # AppImage if(${SUNSHINE_BUILD_APPIMAGE}) # use relative assets path for AppImage string(REPLACE "${CMAKE_INSTALL_PREFIX}" ".${CMAKE_INSTALL_PREFIX}" SUNSHINE_ASSETS_DIR_DEF ${SUNSHINE_ASSETS_DIR}) endif() # cuda set(CUDA_FOUND OFF) if(${SUNSHINE_ENABLE_CUDA}) include(CheckLanguage) check_language(CUDA) if(CMAKE_CUDA_COMPILER) set(CUDA_FOUND ON) enable_language(CUDA) message(STATUS "CUDA Compiler Version: ${CMAKE_CUDA_COMPILER_VERSION}") set(CMAKE_CUDA_ARCHITECTURES "") # https://docs.nvidia.com/cuda/archive/12.0.0/cuda-compiler-driver-nvcc/index.html if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 12.0) list(APPEND CMAKE_CUDA_ARCHITECTURES 75 80 86 87 89 90) else() message(FATAL_ERROR "Sunshine requires a minimum CUDA Compiler version of 12.0. Found version: ${CMAKE_CUDA_COMPILER_VERSION}" ) endif() # https://docs.nvidia.com/cuda/archive/12.8.0/cuda-compiler-driver-nvcc/index.html if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 12.8) list(APPEND CMAKE_CUDA_ARCHITECTURES 100 101 120) endif() # https://docs.nvidia.com/cuda/archive/12.9.0/cuda-compiler-driver-nvcc/index.html if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 12.9) list(APPEND CMAKE_CUDA_ARCHITECTURES 103 121) endif() # https://docs.nvidia.com/cuda/archive/13.0.0/cuda-compiler-driver-nvcc/index.html if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 13.0) list(REMOVE_ITEM CMAKE_CUDA_ARCHITECTURES 101) list(APPEND CMAKE_CUDA_ARCHITECTURES 110) else() list(APPEND CMAKE_CUDA_ARCHITECTURES 50 52 53 60 61 62 70 72) endif() # sort the architectures list(SORT CMAKE_CUDA_ARCHITECTURES COMPARE NATURAL) # message(STATUS "CUDA NVCC Flags: ${CUDA_NVCC_FLAGS}") message(STATUS "CUDA Architectures: ${CMAKE_CUDA_ARCHITECTURES}") elseif(${CUDA_FAIL_ON_MISSING}) message(FATAL_ERROR "CUDA not found. If this is intentional, set '-DSUNSHINE_ENABLE_CUDA=OFF' or '-DCUDA_FAIL_ON_MISSING=OFF'" ) endif() endif() if(CUDA_FOUND) include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/nvfbc") list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/linux/cuda.h" "${CMAKE_SOURCE_DIR}/src/platform/linux/cuda.cu" "${CMAKE_SOURCE_DIR}/src/platform/linux/cuda.cpp" "${CMAKE_SOURCE_DIR}/third-party/nvfbc/NvFBC.h") add_compile_definitions(SUNSHINE_BUILD_CUDA) endif() # libdrm is required for both DRM (KMS) and Wayland if(${SUNSHINE_ENABLE_DRM} OR ${SUNSHINE_ENABLE_WAYLAND}) find_package(LIBDRM REQUIRED) else() set(LIBDRM_FOUND OFF) endif() if(LIBDRM_FOUND) include_directories(SYSTEM ${LIBDRM_INCLUDE_DIRS}) list(APPEND PLATFORM_LIBRARIES ${LIBDRM_LIBRARIES}) endif() # drm if(${SUNSHINE_ENABLE_DRM}) find_package(LIBCAP REQUIRED) else() set(LIBCAP_FOUND OFF) endif() if(LIBDRM_FOUND AND LIBCAP_FOUND) add_compile_definitions(SUNSHINE_BUILD_DRM) include_directories(SYSTEM ${LIBCAP_INCLUDE_DIRS}) list(APPEND PLATFORM_LIBRARIES ${LIBCAP_LIBRARIES}) list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/linux/kmsgrab.cpp") list(APPEND SUNSHINE_DEFINITIONS EGL_NO_X11=1) endif() # evdev include(dependencies/libevdev_Sunshine) # vaapi if(${SUNSHINE_ENABLE_VAAPI}) find_package(Libva REQUIRED) else() set(LIBVA_FOUND OFF) endif() if(LIBVA_FOUND) add_compile_definitions(SUNSHINE_BUILD_VAAPI) include_directories(SYSTEM ${LIBVA_INCLUDE_DIR}) list(APPEND PLATFORM_LIBRARIES ${LIBVA_LIBRARIES} ${LIBVA_DRM_LIBRARIES}) list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/linux/vaapi.h" "${CMAKE_SOURCE_DIR}/src/platform/linux/vaapi.cpp") endif() # wayland if(${SUNSHINE_ENABLE_WAYLAND}) find_package(Wayland REQUIRED) else() set(WAYLAND_FOUND OFF) endif() if(WAYLAND_FOUND) add_compile_definitions(SUNSHINE_BUILD_WAYLAND) if(NOT SUNSHINE_SYSTEM_WAYLAND_PROTOCOLS) set(WAYLAND_PROTOCOLS_DIR "${CMAKE_SOURCE_DIR}/third-party/wayland-protocols") else() pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) pkg_check_modules(WAYLAND_PROTOCOLS wayland-protocols REQUIRED) endif() GEN_WAYLAND("${WAYLAND_PROTOCOLS_DIR}" "unstable/xdg-output" xdg-output-unstable-v1) GEN_WAYLAND("${WAYLAND_PROTOCOLS_DIR}" "unstable/linux-dmabuf" linux-dmabuf-unstable-v1) GEN_WAYLAND("${CMAKE_SOURCE_DIR}/third-party/wlr-protocols" "unstable" wlr-screencopy-unstable-v1) include_directories( SYSTEM ${WAYLAND_INCLUDE_DIRS} ${CMAKE_BINARY_DIR}/generated-src ) list(APPEND PLATFORM_LIBRARIES ${WAYLAND_LIBRARIES} gbm) list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/linux/wlgrab.cpp" "${CMAKE_SOURCE_DIR}/src/platform/linux/wayland.h" "${CMAKE_SOURCE_DIR}/src/platform/linux/wayland.cpp") endif() # x11 if(${SUNSHINE_ENABLE_X11}) find_package(X11 REQUIRED) else() set(X11_FOUND OFF) endif() if(X11_FOUND) add_compile_definitions(SUNSHINE_BUILD_X11) include_directories(SYSTEM ${X11_INCLUDE_DIR}) list(APPEND PLATFORM_LIBRARIES ${X11_LIBRARIES}) list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/linux/x11grab.h" "${CMAKE_SOURCE_DIR}/src/platform/linux/x11grab.cpp") endif() if(NOT ${CUDA_FOUND} AND NOT ${WAYLAND_FOUND} AND NOT ${X11_FOUND} AND NOT (${LIBDRM_FOUND} AND ${LIBCAP_FOUND}) AND NOT ${LIBVA_FOUND}) message(FATAL_ERROR "Couldn't find either cuda, wayland, x11, (libdrm and libcap), or libva") endif() # tray icon if(${SUNSHINE_ENABLE_TRAY}) pkg_check_modules(APPINDICATOR ayatana-appindicator3-0.1) if(APPINDICATOR_FOUND) list(APPEND SUNSHINE_DEFINITIONS TRAY_AYATANA_APPINDICATOR=1) else() pkg_check_modules(APPINDICATOR appindicator3-0.1) if(APPINDICATOR_FOUND) list(APPEND SUNSHINE_DEFINITIONS TRAY_LEGACY_APPINDICATOR=1) endif () endif() pkg_check_modules(LIBNOTIFY libnotify) if(NOT APPINDICATOR_FOUND OR NOT LIBNOTIFY_FOUND) message(STATUS "APPINDICATOR_FOUND: ${APPINDICATOR_FOUND}") message(STATUS "LIBNOTIFY_FOUND: ${LIBNOTIFY_FOUND}") message(FATAL_ERROR "Couldn't find either appindicator or libnotify") else() include_directories(SYSTEM ${APPINDICATOR_INCLUDE_DIRS} ${LIBNOTIFY_INCLUDE_DIRS}) link_directories(${APPINDICATOR_LIBRARY_DIRS} ${LIBNOTIFY_LIBRARY_DIRS}) list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/third-party/tray/src/tray_linux.c") list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${APPINDICATOR_LIBRARIES} ${LIBNOTIFY_LIBRARIES}) endif() # flatpak icons must be prefixed with the app id or they will not be included in the flatpak if(${SUNSHINE_BUILD_FLATPAK}) set(SUNSHINE_TRAY_PREFIX "${PROJECT_FQDN}") else() set(SUNSHINE_TRAY_PREFIX "apollo") endif() list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_TRAY_PREFIX="${SUNSHINE_TRAY_PREFIX}") else() set(SUNSHINE_TRAY 0) message(STATUS "Tray icon disabled") endif() # These need to be set before adding the inputtino subdirectory in order for them to be picked up set(LIBEVDEV_CUSTOM_INCLUDE_DIR "${EVDEV_INCLUDE_DIR}") set(LIBEVDEV_CUSTOM_LIBRARY "${EVDEV_LIBRARY}") add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/inputtino") list(APPEND SUNSHINE_EXTERNAL_LIBRARIES inputtino::libinputtino) file(GLOB_RECURSE INPUTTINO_SOURCES ${CMAKE_SOURCE_DIR}/src/platform/linux/input/inputtino*.h ${CMAKE_SOURCE_DIR}/src/platform/linux/input/inputtino*.cpp) list(APPEND PLATFORM_TARGET_FILES ${INPUTTINO_SOURCES}) # build libevdev before the libinputtino target if(EXTERNAL_PROJECT_LIBEVDEV_USED) add_dependencies(libinputtino libevdev) endif() # AppImage and Flatpak if (${SUNSHINE_BUILD_APPIMAGE}) list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_BUILD_APPIMAGE=1) endif () if (${SUNSHINE_BUILD_FLATPAK}) list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_BUILD_FLATPAK=1) endif () list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/linux/publish.cpp" "${CMAKE_SOURCE_DIR}/src/platform/linux/graphics.h" "${CMAKE_SOURCE_DIR}/src/platform/linux/graphics.cpp" "${CMAKE_SOURCE_DIR}/src/platform/linux/misc.h" "${CMAKE_SOURCE_DIR}/src/platform/linux/misc.cpp" "${CMAKE_SOURCE_DIR}/src/platform/linux/audio.cpp" "${CMAKE_SOURCE_DIR}/third-party/glad/src/egl.c" "${CMAKE_SOURCE_DIR}/third-party/glad/src/gl.c" "${CMAKE_SOURCE_DIR}/third-party/glad/include/EGL/eglplatform.h" "${CMAKE_SOURCE_DIR}/third-party/glad/include/KHR/khrplatform.h" "${CMAKE_SOURCE_DIR}/third-party/glad/include/glad/gl.h" "${CMAKE_SOURCE_DIR}/third-party/glad/include/glad/egl.h") list(APPEND PLATFORM_LIBRARIES dl pulse pulse-simple) include_directories( SYSTEM "${CMAKE_SOURCE_DIR}/third-party/glad/include") ================================================ FILE: cmake/compile_definitions/macos.cmake ================================================ # macos specific compile definitions add_compile_definitions(SUNSHINE_PLATFORM="macos") set(MACOS_LINK_DIRECTORIES /opt/homebrew/lib /opt/local/lib /usr/local/lib) foreach(dir ${MACOS_LINK_DIRECTORIES}) if(EXISTS ${dir}) link_directories(${dir}) endif() endforeach() if(NOT BOOST_USE_STATIC AND NOT FETCH_CONTENT_BOOST_USED) ADD_DEFINITIONS(-DBOOST_LOG_DYN_LINK) endif() list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${APP_KIT_LIBRARY} ${APP_SERVICES_LIBRARY} ${AV_FOUNDATION_LIBRARY} ${CORE_MEDIA_LIBRARY} ${CORE_VIDEO_LIBRARY} ${FOUNDATION_LIBRARY} ${VIDEO_TOOLBOX_LIBRARY}) set(APPLE_PLIST_FILE "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/Info.plist") set(PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/macos/av_audio.h" "${CMAKE_SOURCE_DIR}/src/platform/macos/av_audio.m" "${CMAKE_SOURCE_DIR}/src/platform/macos/av_img_t.h" "${CMAKE_SOURCE_DIR}/src/platform/macos/av_video.h" "${CMAKE_SOURCE_DIR}/src/platform/macos/av_video.m" "${CMAKE_SOURCE_DIR}/src/platform/macos/display.mm" "${CMAKE_SOURCE_DIR}/src/platform/macos/input.cpp" "${CMAKE_SOURCE_DIR}/src/platform/macos/microphone.mm" "${CMAKE_SOURCE_DIR}/src/platform/macos/misc.mm" "${CMAKE_SOURCE_DIR}/src/platform/macos/misc.h" "${CMAKE_SOURCE_DIR}/src/platform/macos/nv12_zero_device.cpp" "${CMAKE_SOURCE_DIR}/src/platform/macos/nv12_zero_device.h" "${CMAKE_SOURCE_DIR}/src/platform/macos/publish.cpp" "${CMAKE_SOURCE_DIR}/third-party/TPCircularBuffer/TPCircularBuffer.c" "${CMAKE_SOURCE_DIR}/third-party/TPCircularBuffer/TPCircularBuffer.h" ${APPLE_PLIST_FILE}) if(SUNSHINE_ENABLE_TRAY) list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${COCOA}) list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/third-party/tray/src/tray_darwin.m") endif() ================================================ FILE: cmake/compile_definitions/unix.cmake ================================================ # unix specific compile definitions # put anything here that applies to both linux and macos list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${CURL_LIBRARIES}) # add install prefix to assets path if not already there if(NOT SUNSHINE_ASSETS_DIR MATCHES "^${CMAKE_INSTALL_PREFIX}") set(SUNSHINE_ASSETS_DIR "${CMAKE_INSTALL_PREFIX}/${SUNSHINE_ASSETS_DIR}") endif() ================================================ FILE: cmake/compile_definitions/windows.cmake ================================================ # windows specific compile definitions add_compile_definitions(SUNSHINE_PLATFORM="windows") enable_language(RC) set(CMAKE_RC_COMPILER windres) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static") # gcc complains about misleading indentation in some mingw includes list(APPEND SUNSHINE_COMPILE_OPTIONS -Wno-misleading-indentation) # see gcc bug 98723 add_definitions(-DUSE_BOOST_REGEX) # curl add_definitions(-DCURL_STATICLIB) include_directories(SYSTEM ${CURL_STATIC_INCLUDE_DIRS}) link_directories(${CURL_STATIC_LIBRARY_DIRS}) # miniupnpc add_definitions(-DMINIUPNP_STATICLIB) # extra tools/binaries for audio/display devices add_subdirectory(tools) # todo - this is temporary, only tools for Windows are needed, for now # nvidia include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/nvapi-open-source-sdk") file(GLOB NVPREFS_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/third-party/nvapi-open-source-sdk/*.h" "${CMAKE_SOURCE_DIR}/src/platform/windows/nvprefs/*.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/nvprefs/*.h") # vigem include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include") # apollo icon if(NOT DEFINED PROJECT_ICON_PATH) set(PROJECT_ICON_PATH "${CMAKE_SOURCE_DIR}/apollo.ico") endif() # Create a separate object library for the RC file with minimal includes add_library(sunshine_rc_object OBJECT "${CMAKE_SOURCE_DIR}/src/platform/windows/windows.rc") # Set minimal properties for RC compilation - only what's needed for the resource file # Otherwise compilation can fail due to "line too long" errors set_target_properties(sunshine_rc_object PROPERTIES COMPILE_DEFINITIONS "PROJECT_ICON_PATH=${PROJECT_ICON_PATH};PROJECT_NAME=${PROJECT_NAME};PROJECT_VENDOR=${SUNSHINE_PUBLISHER_NAME};PROJECT_VERSION=${PROJECT_VERSION};PROJECT_VERSION_MAJOR=${PROJECT_VERSION_MAJOR};PROJECT_VERSION_MINOR=${PROJECT_VERSION_MINOR};PROJECT_VERSION_PATCH=${PROJECT_VERSION_PATCH}" # cmake-lint: disable=C0301 INCLUDE_DIRECTORIES "" ) set(PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/windows/publish.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/misc.h" "${CMAKE_SOURCE_DIR}/src/platform/windows/misc.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/input.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/display.h" "${CMAKE_SOURCE_DIR}/src/platform/windows/display_base.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/display_vram.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/display_ram.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/display_wgc.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/audio.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/virtual_display.h" "${CMAKE_SOURCE_DIR}/src/platform/windows/virtual_display.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/utils.h" "${CMAKE_SOURCE_DIR}/src/platform/windows/utils.cpp" "${CMAKE_SOURCE_DIR}/third-party/sudovda/sudovda-ioctl.h" "${CMAKE_SOURCE_DIR}/third-party/sudovda/sudovda.h" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Client.h" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Common.h" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Util.h" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/km/BusShared.h" ${NVPREFS_FILES}) set(OPENSSL_LIBRARIES libssl.a libcrypto.a) list(PREPEND PLATFORM_LIBRARIES ${CURL_STATIC_LIBRARIES} avrt d3d11 D3DCompiler dwmapi dxgi iphlpapi ksuser libssp.a libstdc++.a libwinpthread.a minhook::minhook ntdll setupapi shlwapi synchronization.lib userenv ws2_32 wsock32 ) if(SUNSHINE_ENABLE_TRAY) list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/third-party/tray/src/tray_windows.c") endif() ================================================ FILE: cmake/dependencies/Boost_Sunshine.cmake ================================================ # # Loads the boost library giving the priority to the system package first, with a fallback to FetchContent. # include_guard(GLOBAL) set(BOOST_VERSION "1.89.0") set(BOOST_COMPONENTS filesystem locale log program_options system ) # system is not used by Sunshine, but by Simple-Web-Server, added here for convenience # algorithm, preprocessor, scope, and uuid are not used by Sunshine, but by libdisplaydevice, added here for convenience if(WIN32) list(APPEND BOOST_COMPONENTS algorithm preprocessor scope uuid ) endif() if(BOOST_USE_STATIC) set(Boost_USE_STATIC_LIBS ON) # cmake-lint: disable=C0103 endif() if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.30") cmake_policy(SET CMP0167 NEW) # Get BoostConfig.cmake from upstream endif() find_package(Boost CONFIG ${BOOST_VERSION} EXACT COMPONENTS ${BOOST_COMPONENTS}) if(NOT Boost_FOUND) message(STATUS "Boost v${BOOST_VERSION} package not found in the system. Falling back to FetchContent.") include(FetchContent) if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") cmake_policy(SET CMP0135 NEW) # Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24 endif() if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.31.0") cmake_policy(SET CMP0174 NEW) # Handle empty variables endif() # more components required for compiling boost targets list(APPEND BOOST_COMPONENTS asio crc format process property_tree) set(BOOST_ENABLE_CMAKE ON) # Limit boost to the required libraries only set(BOOST_INCLUDE_LIBRARIES ${BOOST_COMPONENTS}) set(BOOST_URL "https://github.com/boostorg/boost/releases/download/boost-${BOOST_VERSION}/boost-${BOOST_VERSION}-cmake.tar.xz") # cmake-lint: disable=C0301 set(BOOST_HASH "SHA256=f48b48390380cfb94a629872346e3a81370dc498896f16019ade727ab72eb1ec") if(CMAKE_VERSION VERSION_LESS "3.24.0") FetchContent_Declare( Boost URL ${BOOST_URL} URL_HASH ${BOOST_HASH} ) elseif(APPLE AND CMAKE_VERSION VERSION_GREATER_EQUAL "3.25.0") # add SYSTEM to FetchContent_Declare, this fails on debian bookworm FetchContent_Declare( Boost URL ${BOOST_URL} URL_HASH ${BOOST_HASH} SYSTEM # requires CMake 3.25+ OVERRIDE_FIND_PACKAGE # requires CMake 3.24+, but we have a macro to handle it for other versions ) elseif(CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") FetchContent_Declare( Boost URL ${BOOST_URL} URL_HASH ${BOOST_HASH} OVERRIDE_FIND_PACKAGE # requires CMake 3.24+, but we have a macro to handle it for other versions ) endif() FetchContent_MakeAvailable(Boost) set(FETCH_CONTENT_BOOST_USED TRUE) set(Boost_FOUND TRUE) # cmake-lint: disable=C0103 set(Boost_INCLUDE_DIRS # cmake-lint: disable=C0103 "$") if(WIN32) # Windows build is failing to create .h file in this directory file(MAKE_DIRECTORY ${Boost_BINARY_DIR}/libs/log/src/windows) endif() set(Boost_LIBRARIES "") # cmake-lint: disable=C0103 foreach(component ${BOOST_COMPONENTS}) list(APPEND Boost_LIBRARIES "Boost::${component}") endforeach() endif() message(STATUS "Boost include dirs: ${Boost_INCLUDE_DIRS}") message(STATUS "Boost libraries: ${Boost_LIBRARIES}") ================================================ FILE: cmake/dependencies/common.cmake ================================================ # load common dependencies # this file will also load platform specific dependencies # boost, this should be before Simple-Web-Server as it also depends on boost include(dependencies/Boost_Sunshine) # submodules # moonlight common library set(ENET_NO_INSTALL ON CACHE BOOL "Don't install any libraries built for enet") add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/enet") # web server add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/Simple-Web-Server") # libdisplaydevice add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/libdisplaydevice") # common dependencies include("${CMAKE_MODULE_PATH}/dependencies/nlohmann_json.cmake") find_package(OpenSSL REQUIRED) find_package(PkgConfig REQUIRED) find_package(Threads REQUIRED) pkg_check_modules(CURL REQUIRED libcurl) # miniupnp pkg_check_modules(MINIUPNP miniupnpc REQUIRED) include_directories(SYSTEM ${MINIUPNP_INCLUDE_DIRS}) # ffmpeg pre-compiled binaries if(NOT DEFINED FFMPEG_PREPARED_BINARIES) if(WIN32) set(FFMPEG_PLATFORM_LIBRARIES mfplat ole32 strmiids mfuuid vpl) elseif(UNIX AND NOT APPLE) set(FFMPEG_PLATFORM_LIBRARIES numa va va-drm va-x11 X11) endif() set(FFMPEG_PREPARED_BINARIES "${CMAKE_SOURCE_DIR}/third-party/build-deps/dist/${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}") # check if the directory exists if(NOT EXISTS "${FFMPEG_PREPARED_BINARIES}") message(FATAL_ERROR "FFmpeg pre-compiled binaries not found at ${FFMPEG_PREPARED_BINARIES}. \ Please consider contributing to the LizardByte/build-deps repository. \ Optionally, you can use the FFMPEG_PREPARED_BINARIES option to specify the path to the \ system-installed FFmpeg libraries") endif() if(EXISTS "${FFMPEG_PREPARED_BINARIES}/lib/libhdr10plus.a") set(HDR10_PLUS_LIBRARY "${FFMPEG_PREPARED_BINARIES}/lib/libhdr10plus.a") endif() set(FFMPEG_LIBRARIES "${FFMPEG_PREPARED_BINARIES}/lib/libavcodec.a" "${FFMPEG_PREPARED_BINARIES}/lib/libswscale.a" "${FFMPEG_PREPARED_BINARIES}/lib/libavutil.a" "${FFMPEG_PREPARED_BINARIES}/lib/libcbs.a" "${FFMPEG_PREPARED_BINARIES}/lib/libSvtAv1Enc.a" "${FFMPEG_PREPARED_BINARIES}/lib/libx264.a" "${FFMPEG_PREPARED_BINARIES}/lib/libx265.a" ${HDR10_PLUS_LIBRARY} ${FFMPEG_PLATFORM_LIBRARIES}) else() set(FFMPEG_LIBRARIES "${FFMPEG_PREPARED_BINARIES}/lib/libavcodec.a" "${FFMPEG_PREPARED_BINARIES}/lib/libswscale.a" "${FFMPEG_PREPARED_BINARIES}/lib/libavutil.a" "${FFMPEG_PREPARED_BINARIES}/lib/libcbs.a" ${FFMPEG_PLATFORM_LIBRARIES}) endif() set(FFMPEG_INCLUDE_DIRS "${FFMPEG_PREPARED_BINARIES}/include") # platform specific dependencies if(WIN32) include("${CMAKE_MODULE_PATH}/dependencies/windows.cmake") elseif(UNIX) include("${CMAKE_MODULE_PATH}/dependencies/unix.cmake") if(APPLE) include("${CMAKE_MODULE_PATH}/dependencies/macos.cmake") else() include("${CMAKE_MODULE_PATH}/dependencies/linux.cmake") endif() endif() ================================================ FILE: cmake/dependencies/libevdev_Sunshine.cmake ================================================ # # Loads the libevdev library giving the priority to the system package first, with a fallback to ExternalProject # include_guard(GLOBAL) set(LIBEVDEV_VERSION libevdev-1.13.2) pkg_check_modules(PC_EVDEV libevdev) if(PC_EVDEV_FOUND) find_path(EVDEV_INCLUDE_DIR libevdev/libevdev.h HINTS ${PC_EVDEV_INCLUDE_DIRS} ${PC_EVDEV_INCLUDEDIR}) find_library(EVDEV_LIBRARY NAMES evdev libevdev) else() include(ExternalProject) ExternalProject_Add(libevdev URL http://www.freedesktop.org/software/libevdev/${LIBEVDEV_VERSION}.tar.xz PREFIX ${LIBEVDEV_VERSION} CONFIGURE_COMMAND /configure --prefix= BUILD_COMMAND "make" INSTALL_COMMAND "" ) ExternalProject_Get_Property(libevdev SOURCE_DIR) message(STATUS "libevdev source dir: ${SOURCE_DIR}") set(EVDEV_INCLUDE_DIR "${SOURCE_DIR}") ExternalProject_Get_Property(libevdev BINARY_DIR) message(STATUS "libevdev binary dir: ${BINARY_DIR}") set(EVDEV_LIBRARY "${BINARY_DIR}/libevdev/.libs/libevdev.a") # compile libevdev before sunshine set(SUNSHINE_TARGET_DEPENDENCIES ${SUNSHINE_TARGET_DEPENDENCIES} libevdev) set(EXTERNAL_PROJECT_LIBEVDEV_USED TRUE) endif() if(EVDEV_INCLUDE_DIR AND EVDEV_LIBRARY) message(STATUS "Found libevdev library: ${EVDEV_LIBRARY}") message(STATUS "Found libevdev include directory: ${EVDEV_INCLUDE_DIR}") include_directories(SYSTEM ${EVDEV_INCLUDE_DIR}) list(APPEND PLATFORM_LIBRARIES ${EVDEV_LIBRARY}) else() message(FATAL_ERROR "Couldn't find or fetch libevdev") endif() ================================================ FILE: cmake/dependencies/linux.cmake ================================================ # linux specific dependencies ================================================ FILE: cmake/dependencies/macos.cmake ================================================ # macos specific dependencies FIND_LIBRARY(APP_KIT_LIBRARY AppKit) FIND_LIBRARY(APP_SERVICES_LIBRARY ApplicationServices) FIND_LIBRARY(AV_FOUNDATION_LIBRARY AVFoundation) FIND_LIBRARY(CORE_MEDIA_LIBRARY CoreMedia) FIND_LIBRARY(CORE_VIDEO_LIBRARY CoreVideo) FIND_LIBRARY(FOUNDATION_LIBRARY Foundation) FIND_LIBRARY(VIDEO_TOOLBOX_LIBRARY VideoToolbox) if(SUNSHINE_ENABLE_TRAY) FIND_LIBRARY(COCOA Cocoa REQUIRED) endif() ================================================ FILE: cmake/dependencies/nlohmann_json.cmake ================================================ # # Loads the nlohmann_json library giving the priority to the system package first, with a fallback to FetchContent. # include_guard(GLOBAL) find_package(nlohmann_json 3.11 QUIET GLOBAL) if(NOT nlohmann_json_FOUND) message(STATUS "nlohmann_json v3.11.x package not found in the system. Falling back to FetchContent.") include(FetchContent) if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") cmake_policy(SET CMP0135 NEW) # Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24 endif() if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.31.0") cmake_policy(SET CMP0174 NEW) # Handle empty variables endif() FetchContent_Declare( json URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz URL_HASH MD5=c23a33f04786d85c29fda8d16b5f0efd DOWNLOAD_EXTRACT_TIMESTAMP ) FetchContent_MakeAvailable(json) endif() ================================================ FILE: cmake/dependencies/unix.cmake ================================================ # unix specific dependencies # put anything here that applies to both linux and macos ================================================ FILE: cmake/dependencies/windows.cmake ================================================ # windows specific dependencies # Make sure MinHook is installed find_library(MINHOOK_LIBRARY libMinHook.a REQUIRED) find_path(MINHOOK_INCLUDE_DIR MinHook.h PATH_SUFFIXES include REQUIRED) add_library(minhook::minhook STATIC IMPORTED) set_property(TARGET minhook::minhook PROPERTY IMPORTED_LOCATION ${MINHOOK_LIBRARY}) target_include_directories(minhook::minhook INTERFACE ${MINHOOK_INCLUDE_DIR}) ================================================ FILE: cmake/macros/common.cmake ================================================ # common macros # this file will also load platform specific macros # platform specific macros if(WIN32) include(${CMAKE_MODULE_PATH}/macros/windows.cmake) elseif(UNIX) include(${CMAKE_MODULE_PATH}/macros/unix.cmake) if(APPLE) include(${CMAKE_MODULE_PATH}/macros/macos.cmake) else() include(${CMAKE_MODULE_PATH}/macros/linux.cmake) endif() endif() # override find_package function macro(find_package) # cmake-lint: disable=C0103 string(TOLOWER "${ARGV0}" ARGV0_LOWER) if( (("${ARGV0_LOWER}" STREQUAL "boost") AND DEFINED FETCH_CONTENT_BOOST_USED) OR (("${ARGV0_LOWER}" STREQUAL "libevdev") AND DEFINED EXTERNAL_PROJECT_LIBEVDEV_USED) ) # Do nothing, as the package has already been fetched else() # Call the original find_package function _find_package(${ARGV}) endif() endmacro() ================================================ FILE: cmake/macros/linux.cmake ================================================ # linux specific macros # GEN_WAYLAND: args = `filename` macro(GEN_WAYLAND wayland_directory subdirectory filename) file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/generated-src) message("wayland-scanner private-code \ ${wayland_directory}/${subdirectory}/${filename}.xml \ ${CMAKE_BINARY_DIR}/generated-src/${filename}.c") message("wayland-scanner client-header \ ${wayland_directory}/${subdirectory}/${filename}.xml \ ${CMAKE_BINARY_DIR}/generated-src/${filename}.h") execute_process( COMMAND wayland-scanner private-code ${wayland_directory}/${subdirectory}/${filename}.xml ${CMAKE_BINARY_DIR}/generated-src/${filename}.c COMMAND wayland-scanner client-header ${wayland_directory}/${subdirectory}/${filename}.xml ${CMAKE_BINARY_DIR}/generated-src/${filename}.h RESULT_VARIABLE EXIT_INT ) if(NOT ${EXIT_INT} EQUAL 0) message(FATAL_ERROR "wayland-scanner failed") endif() list(APPEND PLATFORM_TARGET_FILES ${CMAKE_BINARY_DIR}/generated-src/${filename}.c ${CMAKE_BINARY_DIR}/generated-src/${filename}.h) endmacro() ================================================ FILE: cmake/macros/macos.cmake ================================================ # macos specific macros # ADD_FRAMEWORK: args = `fwname`, `appname` macro(ADD_FRAMEWORK fwname appname) find_library(FRAMEWORK_${fwname} NAMES ${fwname} PATHS ${CMAKE_OSX_SYSROOT}/System/Library PATH_SUFFIXES Frameworks NO_DEFAULT_PATH) if( ${FRAMEWORK_${fwname}} STREQUAL FRAMEWORK_${fwname}-NOTFOUND) MESSAGE(ERROR ": Framework ${fwname} not found") else() TARGET_LINK_LIBRARIES(${appname} "${FRAMEWORK_${fwname}}/${fwname}") MESSAGE(STATUS "Framework ${fwname} found at ${FRAMEWORK_${fwname}}") endif() endmacro(ADD_FRAMEWORK) ================================================ FILE: cmake/macros/unix.cmake ================================================ # unix specific macros # put anything here that applies to both linux and macos ================================================ FILE: cmake/macros/windows.cmake ================================================ # windows specific macros ================================================ FILE: cmake/packaging/common.cmake ================================================ # common packaging # common cpack options set(CPACK_PACKAGE_NAME ${CMAKE_PROJECT_NAME}) set(CPACK_PACKAGE_VENDOR "SudoMaker") set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) set(CPACK_PACKAGE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/cpack_artifacts) set(CPACK_PACKAGE_CONTACT "https://www.sudomaker.com") set(CPACK_PACKAGE_DESCRIPTION ${CMAKE_PROJECT_DESCRIPTION}) set(CPACK_PACKAGE_HOMEPAGE_URL ${CMAKE_PROJECT_HOMEPAGE_URL}) set(CPACK_RESOURCE_FILE_LICENSE ${PROJECT_SOURCE_DIR}/LICENSE) set(CPACK_PACKAGE_ICON ${PROJECT_SOURCE_DIR}/apollo.png) set(CPACK_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME}") set(CPACK_STRIP_FILES YES) # install common assets install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/" DESTINATION "${SUNSHINE_ASSETS_DIR}" PATTERN "web" EXCLUDE) # copy assets to build directory, for running without install file(GLOB_RECURSE ALL_ASSETS RELATIVE "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/" "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/*") list(FILTER ALL_ASSETS EXCLUDE REGEX "^web/.*$") # Filter out the web directory foreach(asset ${ALL_ASSETS}) # Copy assets to build directory, excluding the web directory file(COPY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/${asset}" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/assets") endforeach() # install built vite assets install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/assets/web" DESTINATION "${SUNSHINE_ASSETS_DIR}") # platform specific packaging if(WIN32) include(${CMAKE_MODULE_PATH}/packaging/windows.cmake) elseif(UNIX) include(${CMAKE_MODULE_PATH}/packaging/unix.cmake) if(APPLE) include(${CMAKE_MODULE_PATH}/packaging/macos.cmake) else() include(${CMAKE_MODULE_PATH}/packaging/linux.cmake) endif() endif() include(CPack) ================================================ FILE: cmake/packaging/linux.cmake ================================================ # linux specific packaging install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/assets/" DESTINATION "${SUNSHINE_ASSETS_DIR}") # copy assets (excluding shaders) to build directory, for running without install file(COPY "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/assets/" DESTINATION "${CMAKE_BINARY_DIR}/assets" PATTERN "shaders" EXCLUDE) # use symbolic link for shaders directory file(CREATE_LINK "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/assets/shaders" "${CMAKE_BINARY_DIR}/assets/shaders" COPY_ON_ERROR SYMBOLIC) if(${SUNSHINE_BUILD_APPIMAGE} OR ${SUNSHINE_BUILD_FLATPAK}) install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.rules" DESTINATION "${SUNSHINE_ASSETS_DIR}/udev/rules.d") install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.conf" DESTINATION "${SUNSHINE_ASSETS_DIR}/modules-load.d") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine.service" DESTINATION "${SUNSHINE_ASSETS_DIR}/systemd/user") else() find_package(Systemd) find_package(Udev) if(UDEV_FOUND) install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.rules" DESTINATION "${UDEV_RULES_INSTALL_DIR}") endif() if(SYSTEMD_FOUND) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine.service" DESTINATION "${SYSTEMD_USER_UNIT_INSTALL_DIR}") install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.conf" DESTINATION "${SYSTEMD_MODULES_LOAD_DIR}") endif() endif() # Post install set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/postinst") set(CPACK_RPM_POST_INSTALL_SCRIPT_FILE "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/postinst") # Apply setcap for RPM # https://github.com/coreos/rpm-ostree/discussions/5036#discussioncomment-10291071 set(CPACK_RPM_USER_FILELIST "%caps(cap_sys_admin+p) ${SUNSHINE_EXECUTABLE_PATH}") # Dependencies set(CPACK_DEB_COMPONENT_INSTALL ON) set(CPACK_DEBIAN_PACKAGE_DEPENDS "\ ${CPACK_DEB_PLATFORM_PACKAGE_DEPENDS} \ debianutils, \ libcap2, \ libcurl4, \ libdrm2, \ libgbm1, \ libevdev2, \ libnuma1, \ libopus0, \ libpulse0, \ libva2, \ libva-drm2, \ libwayland-client0, \ libx11-6, \ miniupnpc, \ openssl | libssl3") set(CPACK_RPM_PACKAGE_REQUIRES "\ ${CPACK_RPM_PLATFORM_PACKAGE_REQUIRES} \ libcap >= 2.22, \ libcurl >= 7.0, \ libdrm >= 2.4.97, \ libevdev >= 1.5.6, \ libopusenc >= 0.2.1, \ libva >= 2.14.0, \ libwayland-client >= 1.20.0, \ libX11 >= 1.7.3.1, \ mesa-libgbm >= 25.0.7, \ miniupnpc >= 2.2.4, \ numactl-libs >= 2.0.14, \ openssl >= 3.0.2, \ pulseaudio-libs >= 10.0, \ which >= 2.21") if(NOT BOOST_USE_STATIC) set(CPACK_DEBIAN_PACKAGE_DEPENDS "\ ${CPACK_DEBIAN_PACKAGE_DEPENDS}, \ libboost-filesystem${Boost_VERSION}, \ libboost-locale${Boost_VERSION}, \ libboost-log${Boost_VERSION}, \ libboost-program-options${Boost_VERSION}") set(CPACK_RPM_PACKAGE_REQUIRES "\ ${CPACK_RPM_PACKAGE_REQUIRES}, \ boost-filesystem >= ${Boost_VERSION}, \ boost-locale >= ${Boost_VERSION}, \ boost-log >= ${Boost_VERSION}, \ boost-program-options >= ${Boost_VERSION}") endif() # This should automatically figure out dependencies, doesn't work with the current config set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS OFF) # application icon if(NOT ${SUNSHINE_BUILD_FLATPAK}) install(FILES "${CMAKE_SOURCE_DIR}/apollo.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps") else() install(FILES "${CMAKE_SOURCE_DIR}/apollo.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps" RENAME "${PROJECT_FQDN}.svg") endif() # tray icon if(${SUNSHINE_TRAY} STREQUAL 1) if(NOT ${SUNSHINE_BUILD_FLATPAK}) install(FILES "${CMAKE_SOURCE_DIR}/apollo.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status" RENAME "apollo-tray.svg") install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/apollo-playing.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status") install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/apollo-pausing.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status") install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/apollo-locked.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status") else() # flatpak icons must be prefixed with the app id or they will not be included in the flatpak install(FILES "${CMAKE_SOURCE_DIR}/apollo.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status" RENAME "${PROJECT_FQDN}-tray.svg") install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/apollo-playing.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status" RENAME "${PROJECT_FQDN}-playing.svg") install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/apollo-pausing.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status" RENAME "${PROJECT_FQDN}-pausing.svg") install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/apollo-locked.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status" RENAME "${PROJECT_FQDN}-locked.svg") endif() set(CPACK_DEBIAN_PACKAGE_DEPENDS "\ ${CPACK_DEBIAN_PACKAGE_DEPENDS}, \ libayatana-appindicator3-1, \ libnotify4") set(CPACK_RPM_PACKAGE_REQUIRES "\ ${CPACK_RPM_PACKAGE_REQUIRES}, \ libappindicator-gtk3 >= 12.10.0") endif() # desktop file install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_FQDN}.desktop" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications") if(NOT ${SUNSHINE_BUILD_APPIMAGE} AND NOT ${SUNSHINE_BUILD_FLATPAK}) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_FQDN}.terminal.desktop" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications") endif() # metadata file install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_FQDN}.metainfo.xml" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/metainfo") ================================================ FILE: cmake/packaging/macos.cmake ================================================ # macos specific packaging # todo - bundle doesn't produce a valid .app use cpack -G DragNDrop set(CPACK_BUNDLE_NAME "${CMAKE_PROJECT_NAME}") set(CPACK_BUNDLE_PLIST "${APPLE_PLIST_FILE}") set(CPACK_BUNDLE_ICON "${PROJECT_SOURCE_DIR}/sunshine.icns") # set(CPACK_BUNDLE_STARTUP_COMMAND "${INSTALL_RUNTIME_DIR}/sunshine") if(SUNSHINE_PACKAGE_MACOS) # todo set(MAC_PREFIX "${CMAKE_PROJECT_NAME}.app/Contents") set(INSTALL_RUNTIME_DIR "${MAC_PREFIX}/MacOS") install(TARGETS sunshine BUNDLE DESTINATION . COMPONENT Runtime RUNTIME DESTINATION ${INSTALL_RUNTIME_DIR} COMPONENT Runtime) else() install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/misc/uninstall_pkg.sh" DESTINATION "${SUNSHINE_ASSETS_DIR}") endif() install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/" DESTINATION "${SUNSHINE_ASSETS_DIR}") # copy assets to build directory, for running without install file(COPY "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/" DESTINATION "${CMAKE_BINARY_DIR}/assets") ================================================ FILE: cmake/packaging/unix.cmake ================================================ # unix specific packaging # put anything here that applies to both linux and macos # return here if building a macos package if(SUNSHINE_PACKAGE_MACOS) return() endif() # Installation destination dir set(CPACK_SET_DESTDIR true) if(NOT CMAKE_INSTALL_PREFIX) set(CMAKE_INSTALL_PREFIX "/usr/share/sunshine") endif() install(TARGETS sunshine RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") ================================================ FILE: cmake/packaging/windows.cmake ================================================ # windows specific packaging install(TARGETS sunshine RUNTIME DESTINATION "." COMPONENT application) # Hardening: include zlib1.dll (loaded via LoadLibrary() in openssl's libcrypto.a) install(FILES "${ZLIB}" DESTINATION "." COMPONENT application) # ViGEmBus installer set(VIGEMBUS_INSTALLER "${CMAKE_BINARY_DIR}/vigembus_installer.exe") file(DOWNLOAD "https://github.com/nefarius/ViGEmBus/releases/download/v1.21.442.0/ViGEmBus_1.21.442_x64_x86_arm64.exe" ${VIGEMBUS_INSTALLER} SHOW_PROGRESS EXPECTED_HASH SHA256=155c50f1eec07bdc28d2f61a3e3c2c6c132fee7328412de224695f89143316bc TIMEOUT 60 ) install(FILES ${VIGEMBUS_INSTALLER} DESTINATION "scripts" RENAME "vigembus_installer.exe" COMPONENT gamepad) # Adding tools install(TARGETS dxgi-info RUNTIME DESTINATION "tools" COMPONENT dxgi) install(TARGETS audio-info RUNTIME DESTINATION "tools" COMPONENT audio) # Mandatory tools install(TARGETS sunshinesvc RUNTIME DESTINATION "tools" COMPONENT application) # Drivers install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/drivers/sudovda" DESTINATION "drivers" COMPONENT sudovda) # Mandatory scripts install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/service/" DESTINATION "scripts" COMPONENT assets) install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/migration/" DESTINATION "scripts" COMPONENT assets) install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/path/" DESTINATION "scripts" COMPONENT assets) # Configurable options for the service install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/autostart/" DESTINATION "scripts" COMPONENT autostart) # scripts install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/firewall/" DESTINATION "scripts" COMPONENT firewall) install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/gamepad/" DESTINATION "scripts" COMPONENT gamepad) # Sunshine assets install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/assets/" DESTINATION "${SUNSHINE_ASSETS_DIR}" COMPONENT assets) # copy assets (excluding shaders) to build directory, for running without install file(COPY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/assets/" DESTINATION "${CMAKE_BINARY_DIR}/assets" PATTERN "shaders" EXCLUDE) # use junction for shaders directory cmake_path(CONVERT "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/assets/shaders" TO_NATIVE_PATH_LIST shaders_in_build_src_native) cmake_path(CONVERT "${CMAKE_BINARY_DIR}/assets/shaders" TO_NATIVE_PATH_LIST shaders_in_build_dest_native) execute_process(COMMAND cmd.exe /c mklink /J "${shaders_in_build_dest_native}" "${shaders_in_build_src_native}") set(CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}\\\\apollo.ico") # The name of the directory that will be created in C:/Program files/ set(CPACK_PACKAGE_INSTALL_DIRECTORY "${CPACK_PACKAGE_NAME}") # Setting components groups and dependencies set(CPACK_COMPONENT_GROUP_CORE_EXPANDED true) # sunshine binary set(CPACK_COMPONENT_APPLICATION_DISPLAY_NAME "${CMAKE_PROJECT_NAME}") set(CPACK_COMPONENT_APPLICATION_DESCRIPTION "${CMAKE_PROJECT_NAME} main application and required components.") set(CPACK_COMPONENT_APPLICATION_GROUP "Core") set(CPACK_COMPONENT_APPLICATION_REQUIRED true) set(CPACK_COMPONENT_APPLICATION_DEPENDS assets) # service auto-start script set(CPACK_COMPONENT_AUTOSTART_DISPLAY_NAME "Launch on Startup") set(CPACK_COMPONENT_AUTOSTART_DESCRIPTION "If enabled, launches Apollo automatically on system startup.") set(CPACK_COMPONENT_AUTOSTART_GROUP "Core") # assets set(CPACK_COMPONENT_ASSETS_DISPLAY_NAME "Required Assets") set(CPACK_COMPONENT_ASSETS_DESCRIPTION "Shaders, default box art, and web UI.") set(CPACK_COMPONENT_ASSETS_GROUP "Core") set(CPACK_COMPONENT_ASSETS_REQUIRED true) # drivers set(CPACK_COMPONENT_SUDOVDA_DISPLAY_NAME "SudoVDA") set(CPACK_COMPONENT_SUDOVDA_DESCRIPTION "Driver required for Virtual Display to function.") set(CPACK_COMPONENT_SUDOVDA_GROUP "Drivers") set(CPACK_COMPONENT_SUDOVDA_REQUIRED true) # audio tool set(CPACK_COMPONENT_AUDIO_DISPLAY_NAME "audio-info") set(CPACK_COMPONENT_AUDIO_DESCRIPTION "CLI tool providing information about sound devices.") set(CPACK_COMPONENT_AUDIO_GROUP "Tools") # display tool set(CPACK_COMPONENT_DXGI_DISPLAY_NAME "dxgi-info") set(CPACK_COMPONENT_DXGI_DESCRIPTION "CLI tool providing information about graphics cards and displays.") set(CPACK_COMPONENT_DXGI_GROUP "Tools") # firewall scripts set(CPACK_COMPONENT_FIREWALL_DISPLAY_NAME "Add Firewall Exclusions") set(CPACK_COMPONENT_FIREWALL_DESCRIPTION "Scripts to enable or disable firewall rules.") set(CPACK_COMPONENT_FIREWALL_GROUP "Scripts") # gamepad scripts set(CPACK_COMPONENT_GAMEPAD_DISPLAY_NAME "Virtual Gamepad") set(CPACK_COMPONENT_GAMEPAD_DESCRIPTION "Scripts to install and uninstall Virtual Gamepad.") set(CPACK_COMPONENT_GAMEPAD_GROUP "Scripts") # include specific packaging include(${CMAKE_MODULE_PATH}/packaging/windows_nsis.cmake) include(${CMAKE_MODULE_PATH}/packaging/windows_wix.cmake) ================================================ FILE: cmake/packaging/windows_nsis.cmake ================================================ # NSIS Packaging # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.html set(CPACK_NSIS_INSTALLED_ICON_NAME "${PROJECT__DIR}\\\\${PROJECT_EXE}") # Extra install commands # Restores permissions on the install directory # Migrates config files from the root into the new config folder # Install service SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS} IfSilent +2 0 # ExecShell 'open' 'https://docs.lizardbyte.dev/projects/sunshine' nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\" /reset' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\update-path.bat\\\" add' nsExec::ExecToLog '\\\"$INSTDIR\\\\drivers\\\\sudovda\\\\install.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\migrate-config.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\add-firewall-rule.bat\\\"' nsExec::ExecToLog \ 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File \\\"$INSTDIR\\\\scripts\\\\install-gamepad.ps1\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-service.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\autostart-service.bat\\\"' NoController: ") # Extra uninstall commands # Uninstall service set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS "${CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS} nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\delete-firewall-rule.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\uninstall-service.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\sunshine.exe\\\" --restore-nvprefs-undo' MessageBox MB_YESNO|MB_ICONQUESTION \ 'Do you want to remove Virtual Gamepad?' \ /SD IDNO IDNO NoGamepad nsExec::ExecToLog \ 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File \ \\\"$INSTDIR\\\\scripts\\\\uninstall-gamepad.ps1\\\"'; \ skipped if no NoGamepad: MessageBox MB_YESNO|MB_ICONQUESTION \ 'Do you want to remove SudoVDA Virtual Display Driver?' \ /SD IDNO IDNO NoSudoVDA nsExec::ExecToLog '\\\"$INSTDIR\\\\drivers\\\\sudovda\\\\uninstall.bat\\\"'; skipped if no NoSudoVDA: MessageBox MB_YESNO|MB_ICONQUESTION \ 'Do you want to remove $INSTDIR (this includes the configuration, cover images, and settings)?' \ /SD IDNO IDNO NoDelete RMDir /r \\\"$INSTDIR\\\"; skipped if no nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\update-path.bat\\\" remove' NoDelete: ") # Adding an option for the start menu set(CPACK_NSIS_MODIFY_PATH OFF) set(CPACK_NSIS_EXECUTABLES_DIRECTORY ".") # This will be shown on the installed apps Windows settings set(CPACK_NSIS_INSTALLED_ICON_NAME "sunshine.exe") set(CPACK_NSIS_CREATE_ICONS_EXTRA "${CPACK_NSIS_CREATE_ICONS_EXTRA} SetOutPath '\$INSTDIR' CreateShortCut '\$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${CMAKE_PROJECT_NAME}.lnk' \ '\$INSTDIR\\\\sunshine.exe' '--shortcut' ") set(CPACK_NSIS_DELETE_ICONS_EXTRA "${CPACK_NSIS_DELETE_ICONS_EXTRA} Delete '\$SMPROGRAMS\\\\$MUI_TEMP\\\\${CMAKE_PROJECT_NAME}.lnk' ") # Checking for previous installed versions set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL "ON") # set(CPACK_NSIS_HELP_LINK "https://docs.lizardbyte.dev/projects/sunshine/latest/md_docs_2getting__started.html") # set(CPACK_NSIS_URL_INFO_ABOUT "${CMAKE_PROJECT_HOMEPAGE_URL}") # set(CPACK_NSIS_CONTACT "${CMAKE_PROJECT_HOMEPAGE_URL}/support") # set(CPACK_NSIS_MENU_LINKS # "https://docs.lizardbyte.dev/projects/sunshine" "Sunshine documentation" # "https://app.lizardbyte.dev" "LizardByte Web Site" # "https://app.lizardbyte.dev/support" "LizardByte Support") set(CPACK_NSIS_MANIFEST_DPI_AWARE true) ================================================ FILE: cmake/packaging/windows_wix.cmake ================================================ # WIX Packaging # see options at: https://cmake.org/cmake/help/latest/cpack_gen/wix.html # TODO: Replace nsis with wix ================================================ FILE: cmake/prep/build_version.cmake ================================================ # Set build variables if env variables are defined # These are used in configured files such as manifests for different packages if(DEFINED ENV{BRANCH}) set(GITHUB_BRANCH $ENV{BRANCH}) endif() if(DEFINED ENV{BUILD_VERSION}) # cmake-lint: disable=W0106 set(BUILD_VERSION $ENV{BUILD_VERSION}) endif() if(DEFINED ENV{CLONE_URL}) set(GITHUB_CLONE_URL $ENV{CLONE_URL}) endif() if(DEFINED ENV{COMMIT}) set(GITHUB_COMMIT $ENV{COMMIT}) endif() if(DEFINED ENV{TAG}) set(GITHUB_TAG $ENV{TAG}) endif() # Check if env vars are defined before attempting to access them, variables will be defined even if blank if((DEFINED ENV{BRANCH}) AND (DEFINED ENV{BUILD_VERSION})) # cmake-lint: disable=W0106 if((DEFINED ENV{BRANCH}) AND (NOT $ENV{BUILD_VERSION} STREQUAL "")) # If BRANCH is defined and BUILD_VERSION is not empty, then we are building from CI # If BRANCH is master we are building a push/release build MESSAGE("Got from CI '$ENV{BRANCH}' branch and version '$ENV{BUILD_VERSION}'") set(PROJECT_VERSION $ENV{BUILD_VERSION}) string(REGEX REPLACE "^v" "" PROJECT_VERSION ${PROJECT_VERSION}) # remove the v prefix if it exists set(CMAKE_PROJECT_VERSION ${PROJECT_VERSION}) # cpack will use this to set the binary versions endif() else() # Generate Sunshine Version based of the git tag # https://github.com/nocnokneo/cmake-git-versioning-example/blob/master/LICENSE find_package(Git) if(GIT_EXECUTABLE) MESSAGE("${CMAKE_SOURCE_DIR}") get_filename_component(SRC_DIR "${CMAKE_SOURCE_DIR}" DIRECTORY) #Get current Branch execute_process( COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD OUTPUT_VARIABLE GIT_DESCRIBE_BRANCH RESULT_VARIABLE GIT_DESCRIBE_ERROR_CODE OUTPUT_STRIP_TRAILING_WHITESPACE ) # Gather current commit execute_process( COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD OUTPUT_VARIABLE GIT_DESCRIBE_VERSION RESULT_VARIABLE GIT_DESCRIBE_ERROR_CODE OUTPUT_STRIP_TRAILING_WHITESPACE ) # Check if Dirty execute_process( COMMAND ${GIT_EXECUTABLE} diff -b --quiet --exit-code RESULT_VARIABLE GIT_IS_DIRTY OUTPUT_STRIP_TRAILING_WHITESPACE ) if(NOT GIT_DESCRIBE_ERROR_CODE) MESSAGE("Sunshine Branch: ${GIT_DESCRIBE_BRANCH}") if(NOT GIT_DESCRIBE_BRANCH STREQUAL "master") set(PROJECT_VERSION ${PROJECT_VERSION}.${GIT_DESCRIBE_VERSION}) MESSAGE("Sunshine Version: ${GIT_DESCRIBE_VERSION}") endif() if(GIT_IS_DIRTY) set(PROJECT_VERSION ${PROJECT_VERSION}.dirty) MESSAGE("Git tree is dirty!") endif() else() MESSAGE(ERROR ": Got git error while fetching tags: ${GIT_DESCRIBE_ERROR_CODE}") endif() else() MESSAGE(WARNING ": Git not found, cannot find git version") endif() endif() # set date variables set(PROJECT_YEAR "1990") set(PROJECT_MONTH "01") set(PROJECT_DAY "01") # Extract year, month, and day (do this AFTER version parsing) # Note: Cmake doesn't support "{}" regex syntax if(PROJECT_VERSION MATCHES "^([0-9][0-9][0-9][0-9])\\.([0-9][0-9][0-9][0-9]?)\\.([0-9]+)$") message("Extracting year and month/day from PROJECT_VERSION: ${PROJECT_VERSION}") # First capture group is the year set(PROJECT_YEAR "${CMAKE_MATCH_1}") # Second capture group contains month and day set(MONTH_DAY "${CMAKE_MATCH_2}") # Extract month (first 1-2 digits) and day (last 2 digits) string(LENGTH "${MONTH_DAY}" MONTH_DAY_LENGTH) if(MONTH_DAY_LENGTH EQUAL 3) # Format: MDD (e.g., 703 = month 7, day 03) string(SUBSTRING "${MONTH_DAY}" 0 1 PROJECT_MONTH) string(SUBSTRING "${MONTH_DAY}" 1 2 PROJECT_DAY) elseif(MONTH_DAY_LENGTH EQUAL 4) # Format: MMDD (e.g., 1203 = month 12, day 03) string(SUBSTRING "${MONTH_DAY}" 0 2 PROJECT_MONTH) string(SUBSTRING "${MONTH_DAY}" 2 2 PROJECT_DAY) endif() # Ensure month is two digits if(PROJECT_MONTH LESS 10 AND NOT PROJECT_MONTH MATCHES "^0") set(PROJECT_MONTH "0${PROJECT_MONTH}") endif() # Ensure day is two digits if(PROJECT_DAY LESS 10 AND NOT PROJECT_DAY MATCHES "^0") set(PROJECT_DAY "0${PROJECT_DAY}") endif() endif() # Parse PROJECT_VERSION to extract major, minor, and patch components if(PROJECT_VERSION MATCHES "([0-9]+)\\.([0-9]+)\\.([0-9]+)") set(PROJECT_VERSION_MAJOR "${CMAKE_MATCH_1}") set(CMAKE_PROJECT_VERSION_MAJOR "${CMAKE_MATCH_1}") set(PROJECT_VERSION_MINOR "${CMAKE_MATCH_2}") set(CMAKE_PROJECT_VERSION_MINOR "${CMAKE_MATCH_2}") set(PROJECT_VERSION_PATCH "${CMAKE_MATCH_3}") set(CMAKE_PROJECT_VERSION_PATCH "${CMAKE_MATCH_3}") endif() message("PROJECT_NAME: ${PROJECT_NAME}") message("PROJECT_VERSION: ${PROJECT_VERSION}") message("PROJECT_VERSION_MAJOR: ${PROJECT_VERSION_MAJOR}") message("PROJECT_VERSION_MINOR: ${PROJECT_VERSION_MINOR}") message("PROJECT_VERSION_PATCH: ${PROJECT_VERSION_PATCH}") message("CMAKE_PROJECT_VERSION: ${CMAKE_PROJECT_VERSION}") message("CMAKE_PROJECT_VERSION_MAJOR: ${CMAKE_PROJECT_VERSION_MAJOR}") message("CMAKE_PROJECT_VERSION_MINOR: ${CMAKE_PROJECT_VERSION_MINOR}") message("CMAKE_PROJECT_VERSION_PATCH: ${CMAKE_PROJECT_VERSION_PATCH}") message("PROJECT_YEAR: ${PROJECT_YEAR}") message("PROJECT_MONTH: ${PROJECT_MONTH}") message("PROJECT_DAY: ${PROJECT_DAY}") list(APPEND SUNSHINE_DEFINITIONS PROJECT_NAME="${PROJECT_NAME}") list(APPEND SUNSHINE_DEFINITIONS PROJECT_VERSION="${PROJECT_VERSION}") list(APPEND SUNSHINE_DEFINITIONS PROJECT_VERSION_MAJOR="${PROJECT_VERSION_MAJOR}") list(APPEND SUNSHINE_DEFINITIONS PROJECT_VERSION_MINOR="${PROJECT_VERSION_MINOR}") list(APPEND SUNSHINE_DEFINITIONS PROJECT_VERSION_PATCH="${PROJECT_VERSION_PATCH}") list(APPEND SUNSHINE_DEFINITIONS PROJECT_VERSION_COMMIT="${GITHUB_COMMIT}") ================================================ FILE: cmake/prep/constants.cmake ================================================ # source assets will be installed from this directory set(SUNSHINE_SOURCE_ASSETS_DIR "${CMAKE_SOURCE_DIR}/src_assets") # enable system tray, we will disable this later if we cannot find the required package config on linux set(SUNSHINE_TRAY 1) ================================================ FILE: cmake/prep/init.cmake ================================================ if (WIN32) if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 15.0) add_compile_options($<$:-Wno-template-body>) # Workaround for WinRT headers endif() endif() elseif (APPLE) elseif (UNIX) include(GNUInstallDirs) if(NOT DEFINED SUNSHINE_EXECUTABLE_PATH) set(SUNSHINE_EXECUTABLE_PATH "sunshine") endif() if(SUNSHINE_BUILD_FLATPAK) set(SUNSHINE_SERVICE_START_COMMAND "ExecStart=flatpak run --command=sunshine ${PROJECT_FQDN}") set(SUNSHINE_SERVICE_STOP_COMMAND "ExecStop=flatpak kill ${PROJECT_FQDN}") else() set(SUNSHINE_SERVICE_START_COMMAND "ExecStart=${SUNSHINE_EXECUTABLE_PATH}") set(SUNSHINE_SERVICE_STOP_COMMAND "") endif() endif() ================================================ FILE: cmake/prep/options.cmake ================================================ # Publisher Metadata set(SUNSHINE_PUBLISHER_NAME "SudoMaker" CACHE STRING "The name of the publisher (or fork developer) of the application.") set(SUNSHINE_PUBLISHER_WEBSITE "https://www.sudomaker.com" CACHE STRING "The URL of the publisher's website.") set(SUNSHINE_PUBLISHER_ISSUE_URL "https://github.com/ClassicOldSong/Apollo/issues" CACHE STRING "The URL of the publisher's support site or issue tracker. If you provide a modified version of Sunshine, we kindly request that you use your own url.") option(BUILD_DOCS "Build documentation" OFF) option(BUILD_TESTS "Build tests" OFF) option(NPM_OFFLINE "Use offline npm packages. You must ensure packages are in your npm cache." OFF) option(BUILD_WERROR "Enable -Werror flag." OFF) # if this option is set, the build will exit after configuring special package configuration files option(SUNSHINE_CONFIGURE_ONLY "Configure special files only, then exit." OFF) option(SUNSHINE_ENABLE_TRAY "Enable system tray icon." ON) option(SUNSHINE_SYSTEM_WAYLAND_PROTOCOLS "Use system installation of wayland-protocols rather than the submodule." OFF) if(APPLE) option(BOOST_USE_STATIC "Use static boost libraries." OFF) else() option(BOOST_USE_STATIC "Use static boost libraries." ON) endif() option(CUDA_FAIL_ON_MISSING "Fail the build if CUDA is not found." ON) option(CUDA_INHERIT_COMPILE_OPTIONS "When building CUDA code, inherit compile options from the the main project. You may want to disable this if your IDE throws errors about unknown flags after running cmake." ON) if(UNIX) option(SUNSHINE_BUILD_HOMEBREW "Enable a Homebrew build." OFF) option(SUNSHINE_CONFIGURE_HOMEBREW "Configure Homebrew formula. Recommended to use with SUNSHINE_CONFIGURE_ONLY" OFF) endif() if(APPLE) option(SUNSHINE_CONFIGURE_PORTFILE "Configure macOS Portfile. Recommended to use with SUNSHINE_CONFIGURE_ONLY" OFF) option(SUNSHINE_PACKAGE_MACOS "Should only be used when creating a macOS package/dmg." OFF) elseif(UNIX) # Linux option(SUNSHINE_BUILD_APPIMAGE "Enable an AppImage build." OFF) option(SUNSHINE_BUILD_FLATPAK "Enable a Flatpak build." OFF) option(SUNSHINE_CONFIGURE_PKGBUILD "Configure files required for AUR. Recommended to use with SUNSHINE_CONFIGURE_ONLY" OFF) option(SUNSHINE_CONFIGURE_FLATPAK_MAN "Configure manifest file required for Flatpak build. Recommended to use with SUNSHINE_CONFIGURE_ONLY" OFF) # Linux capture methods option(SUNSHINE_ENABLE_CUDA "Enable cuda specific code." ON) option(SUNSHINE_ENABLE_DRM "Enable KMS grab if available." ON) option(SUNSHINE_ENABLE_VAAPI "Enable building vaapi specific code." ON) option(SUNSHINE_ENABLE_WAYLAND "Enable building wayland specific code." ON) option(SUNSHINE_ENABLE_X11 "Enable X11 grab if available." ON) endif() ================================================ FILE: cmake/prep/special_package_configuration.cmake ================================================ if(UNIX) if(${SUNSHINE_CONFIGURE_HOMEBREW}) configure_file(packaging/sunshine.rb sunshine.rb @ONLY) endif() endif() if(APPLE) if(${SUNSHINE_CONFIGURE_PORTFILE}) configure_file(packaging/macos/Portfile Portfile @ONLY) endif() elseif(UNIX) # configure the .desktop file set(SUNSHINE_DESKTOP_ICON "apollo.svg") if(${SUNSHINE_BUILD_APPIMAGE}) configure_file(packaging/linux/AppImage/${PROJECT_FQDN}.desktop ${PROJECT_FQDN}.desktop @ONLY) elseif(${SUNSHINE_BUILD_FLATPAK}) set(SUNSHINE_DESKTOP_ICON "${PROJECT_FQDN}") configure_file(packaging/linux/flatpak/${PROJECT_FQDN}.desktop ${PROJECT_FQDN}.desktop @ONLY) else() configure_file(packaging/linux/${PROJECT_FQDN}.desktop ${PROJECT_FQDN}.desktop @ONLY) configure_file(packaging/linux/${PROJECT_FQDN}.terminal.desktop ${PROJECT_FQDN}.terminal.desktop @ONLY) endif() # configure metadata file configure_file(packaging/linux/${PROJECT_FQDN}.metainfo.xml ${PROJECT_FQDN}.metainfo.xml @ONLY) # configure service configure_file(packaging/linux/sunshine.service.in sunshine.service @ONLY) # configure the arch linux pkgbuild if(${SUNSHINE_CONFIGURE_PKGBUILD}) configure_file(packaging/linux/Arch/PKGBUILD PKGBUILD @ONLY) configure_file(packaging/linux/Arch/sunshine.install sunshine.install @ONLY) endif() # configure the flatpak manifest if(${SUNSHINE_CONFIGURE_FLATPAK_MAN}) configure_file(packaging/linux/flatpak/${PROJECT_FQDN}.yml ${PROJECT_FQDN}.yml @ONLY) file(COPY packaging/linux/flatpak/deps/ DESTINATION ${CMAKE_BINARY_DIR}) file(COPY packaging/linux/flatpak/modules DESTINATION ${CMAKE_BINARY_DIR}) file(COPY generated-sources.json DESTINATION ${CMAKE_BINARY_DIR}) file(COPY package-lock.json DESTINATION ${CMAKE_BINARY_DIR}) endif() endif() # return if configure only is set if(${SUNSHINE_CONFIGURE_ONLY}) # message message(STATUS "SUNSHINE_CONFIGURE_ONLY: ON, exiting...") set(END_BUILD ON) else() set(END_BUILD OFF) endif() ================================================ FILE: cmake/targets/common.cmake ================================================ # common target definitions # this file will also load platform specific macros add_executable(sunshine ${SUNSHINE_TARGET_FILES}) foreach(dep ${SUNSHINE_TARGET_DEPENDENCIES}) add_dependencies(sunshine ${dep}) # compile these before sunshine endforeach() # platform specific target definitions if(WIN32) include(${CMAKE_MODULE_PATH}/targets/windows.cmake) elseif(UNIX) include(${CMAKE_MODULE_PATH}/targets/unix.cmake) if(APPLE) include(${CMAKE_MODULE_PATH}/targets/macos.cmake) else() include(${CMAKE_MODULE_PATH}/targets/linux.cmake) endif() endif() # todo - is this necessary? ... for anything except linux? if(NOT DEFINED CMAKE_CUDA_STANDARD) set(CMAKE_CUDA_STANDARD 17) set(CMAKE_CUDA_STANDARD_REQUIRED ON) endif() target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES} ${EXTRA_LIBS}) target_compile_definitions(sunshine PUBLIC ${SUNSHINE_DEFINITIONS}) set_target_properties(sunshine PROPERTIES CXX_STANDARD 23 VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR}) # CLion complains about unknown flags after running cmake, and cannot add symbols to the index for cuda files if(CUDA_INHERIT_COMPILE_OPTIONS) foreach(flag IN LISTS SUNSHINE_COMPILE_OPTIONS) list(APPEND SUNSHINE_COMPILE_OPTIONS_CUDA "$<$:--compiler-options=${flag}>") endforeach() endif() target_compile_options(sunshine PRIVATE $<$:${SUNSHINE_COMPILE_OPTIONS}>;$<$:${SUNSHINE_COMPILE_OPTIONS_CUDA};-std=c++17>) # cmake-lint: disable=C0301 # Homebrew build fails the vite build if we set these environment variables if(${SUNSHINE_BUILD_HOMEBREW}) set(NPM_SOURCE_ASSETS_DIR "") set(NPM_ASSETS_DIR "") set(NPM_BUILD_HOMEBREW "true") else() set(NPM_SOURCE_ASSETS_DIR ${SUNSHINE_SOURCE_ASSETS_DIR}) set(NPM_ASSETS_DIR ${CMAKE_BINARY_DIR}) set(NPM_BUILD_HOMEBREW "") endif() #WebUI build find_program(NPM npm REQUIRED) if (NPM_OFFLINE) set(NPM_INSTALL_FLAGS "--offline") else() set(NPM_INSTALL_FLAGS "") endif() add_custom_target(web-ui ALL WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" COMMENT "Installing NPM Dependencies and Building the Web UI" COMMAND "$<$:cmd;/C>" "${NPM}" install ${NPM_INSTALL_FLAGS} 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}" "$<$:cmd;/C>" "${NPM}" run build # cmake-lint: disable=C0301 COMMAND_EXPAND_LISTS VERBATIM) # docs if(BUILD_DOCS) add_subdirectory(third-party/doxyconfig docs) endif() # tests if(BUILD_TESTS) add_subdirectory(tests) endif() # custom compile flags, must be after adding tests if (NOT BUILD_TESTS) set(TEST_DIR "") else() set(TEST_DIR "${CMAKE_SOURCE_DIR}/tests") endif() # src/upnp set_source_files_properties("${CMAKE_SOURCE_DIR}/src/upnp.cpp" DIRECTORY "${CMAKE_SOURCE_DIR}" "${TEST_DIR}" PROPERTIES COMPILE_FLAGS -Wno-pedantic) # third-party/nanors set_source_files_properties("${CMAKE_SOURCE_DIR}/src/rswrapper.c" DIRECTORY "${CMAKE_SOURCE_DIR}" "${TEST_DIR}" PROPERTIES COMPILE_FLAGS "-ftree-vectorize -funroll-loops") # third-party/ViGEmClient set(VIGEM_COMPILE_FLAGS "") string(APPEND VIGEM_COMPILE_FLAGS "-Wno-unknown-pragmas ") string(APPEND VIGEM_COMPILE_FLAGS "-Wno-misleading-indentation ") string(APPEND VIGEM_COMPILE_FLAGS "-Wno-class-memaccess ") string(APPEND VIGEM_COMPILE_FLAGS "-Wno-unused-function ") string(APPEND VIGEM_COMPILE_FLAGS "-Wno-unused-variable ") set_source_files_properties("${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp" DIRECTORY "${CMAKE_SOURCE_DIR}" "${TEST_DIR}" PROPERTIES COMPILE_DEFINITIONS "UNICODE=1;ERROR_INVALID_DEVICE_OBJECT_PARAMETER=650" COMPILE_FLAGS ${VIGEM_COMPILE_FLAGS}) # src/nvhttp string(TOUPPER "x${CMAKE_BUILD_TYPE}" BUILD_TYPE) if("${BUILD_TYPE}" STREQUAL "XDEBUG") if(WIN32) if (NOT BUILD_TESTS) set_source_files_properties("${CMAKE_SOURCE_DIR}/src/nvhttp.cpp" DIRECTORY "${CMAKE_SOURCE_DIR}" PROPERTIES COMPILE_FLAGS -O2) else() set_source_files_properties("${CMAKE_SOURCE_DIR}/src/nvhttp.cpp" DIRECTORY "${CMAKE_SOURCE_DIR}" "${CMAKE_SOURCE_DIR}/tests" PROPERTIES COMPILE_FLAGS -O2) endif() endif() else() add_definitions(-DNDEBUG) endif() ================================================ FILE: cmake/targets/linux.cmake ================================================ # linux specific target definitions ================================================ FILE: cmake/targets/macos.cmake ================================================ # macos specific target definitions target_link_options(sunshine PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,${APPLE_PLIST_FILE}) # Tell linker to dynamically load these symbols at runtime, in case they're unavailable: target_link_options(sunshine PRIVATE -Wl,-U,_CGPreflightScreenCaptureAccess -Wl,-U,_CGRequestScreenCaptureAccess) ================================================ FILE: cmake/targets/unix.cmake ================================================ # unix specific target definitions # put anything here that applies to both linux and macos ================================================ FILE: cmake/targets/windows.cmake ================================================ # windows specific target definitions set_target_properties(sunshine PROPERTIES LINK_SEARCH_START_STATIC 1) set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll") find_library(ZLIB ZLIB1) list(APPEND SUNSHINE_EXTERNAL_LIBRARIES $ Windowsapp.lib Wtsapi32.lib) ================================================ FILE: codecov.yml ================================================ --- codecov: branch: master coverage: status: project: default: target: auto threshold: 10% comment: layout: "diff, flags, files" behavior: default require_changes: false # if true: only post the comment if coverage changes ignore: - "tests" - "third-party" ================================================ FILE: crowdin.yml ================================================ --- "base_path": "." "base_url": "https://api.crowdin.com" # optional (for Crowdin Enterprise only) "preserve_hierarchy": true # false will flatten tree on crowdin, but doesn't work with dest option "pull_request_title": "chore(l10n): update translations" "pull_request_labels": [ "crowdin", "l10n" ] "files": [ { "source": "/locale/*.po", "dest": "/%original_file_name%", "translation": "/locale/%two_letters_code%/LC_MESSAGES/%original_file_name%", "languages_mapping": { "two_letters_code": { # map non-two letter codes here, left side is crowdin designation, right side is babel designation "en-GB": "en_GB", "en-US": "en_US", "pt-BR": "pt_BR", "zh-TW": "zh_TW" } }, "update_option": "update_as_unapproved" }, { "source": "/src_assets/common/assets/web/public/assets/locale/en.json", "dest": "/sunshine.json", "translation": "/src_assets/common/assets/web/public/assets/locale/%two_letters_code%.%file_extension%", "update_option": "update_as_unapproved" } ] ================================================ FILE: docker/archlinux.dockerfile ================================================ # syntax=docker/dockerfile:1 # artifacts: true # platforms: linux/amd64 # archlinux does not have an arm64 base image # no-cache-filters: artifacts,sunshine ARG BASE=archlinux/archlinux ARG TAG=base-devel FROM ${BASE}:${TAG} AS sunshine-base # Update keyring to avoid signature errors, and update system RUN <<_DEPS #!/bin/bash set -e pacman -Syy --disable-download-timeout --needed --noconfirm \ archlinux-keyring pacman -Syu --disable-download-timeout --noconfirm pacman -Scc --noconfirm _DEPS FROM sunshine-base AS sunshine-deps SHELL ["/bin/bash", "-o", "pipefail", "-c"] # Install dependencies first - this layer will be cached RUN <<_SETUP #!/bin/bash set -e # Setup builder user, arch prevents running makepkg as root useradd -m builder echo 'builder ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers # patch the build flags # shellcheck disable=SC2016 sed -i 's,#MAKEFLAGS="-j2",MAKEFLAGS="-j$(nproc)",g' /etc/makepkg.conf # install dependencies pacman -Syu --disable-download-timeout --needed --noconfirm \ base-devel \ cmake \ cuda \ git \ namcap \ xorg-server-xvfb pacman -Scc --noconfirm _SETUP FROM sunshine-deps AS sunshine-build ARG BRANCH ARG BUILD_VERSION ARG COMMIT ARG CLONE_URL # note: BUILD_VERSION may be blank ENV BRANCH=${BRANCH} ENV BUILD_VERSION=${BUILD_VERSION} ENV COMMIT=${COMMIT} ENV CLONE_URL=${CLONE_URL} # PKGBUILD options ENV _use_cuda=true ENV _run_unit_tests=true ENV _support_headless_testing=true SHELL ["/bin/bash", "-o", "pipefail", "-c"] # Setup builder user USER builder # copy repository WORKDIR /build/sunshine/ COPY --link .. . # setup build directory WORKDIR /build/sunshine/build # configure PKGBUILD file RUN <<_MAKE #!/bin/bash set -e sub_version="" if [[ "${BRANCH}" != "master" ]]; then sub_version=".r${COMMIT}" fi cmake \ -DSUNSHINE_CONFIGURE_ONLY=ON \ -DSUNSHINE_CONFIGURE_PKGBUILD=ON \ -DSUNSHINE_SUB_VERSION="${sub_version}" \ /build/sunshine _MAKE WORKDIR /build/sunshine/pkg RUN <<_PACKAGE mv /build/sunshine/build/PKGBUILD . mv /build/sunshine/build/sunshine.install . makepkg --printsrcinfo > .SRCINFO _PACKAGE # create a PKGBUILD archive USER root RUN <<_REPO #!/bin/bash set -e tar -czf /build/sunshine/sunshine.pkg.tar.gz . _REPO # namcap and build PKGBUILD file USER builder RUN <<_PKGBUILD #!/bin/bash set -e # shellcheck source=/dev/null source /etc/profile # ensure cuda is in the PATH export DISPLAY=:1 Xvfb ${DISPLAY} -screen 0 1024x768x24 & namcap -i PKGBUILD makepkg -si --noconfirm rm -f /build/sunshine/pkg/sunshine-debug*.pkg.tar.zst ls -a _PKGBUILD FROM sunshine-base AS sunshine COPY --link --from=sunshine-build /build/sunshine/pkg/sunshine*.pkg.tar.zst /sunshine.pkg.tar.zst # artifacts to be extracted in CI COPY --link --from=sunshine-build /build/sunshine/pkg/sunshine*.pkg.tar.zst /artifacts/sunshine.pkg.tar.zst COPY --link --from=sunshine-build /build/sunshine/sunshine.pkg.tar.gz /artifacts/sunshine.pkg.tar.gz # install sunshine RUN <<_INSTALL_SUNSHINE #!/bin/bash set -e pacman -U --disable-download-timeout --needed --noconfirm \ /sunshine.pkg.tar.zst pacman -Scc --noconfirm _INSTALL_SUNSHINE # network setup EXPOSE 47984-47990/tcp EXPOSE 48010 EXPOSE 47998-48000/udp # setup user ARG PGID=1000 ENV PGID=${PGID} ARG PUID=1000 ENV PUID=${PUID} ENV TZ="UTC" ARG UNAME=lizard ENV UNAME=${UNAME} ENV HOME=/home/$UNAME # setup user RUN <<_SETUP_USER #!/bin/bash set -e groupadd -f -g "${PGID}" "${UNAME}" useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -u "${PUID}" "${UNAME}" mkdir -p ${HOME}/.config/sunshine ln -s ${HOME}/.config/sunshine /config chown -R ${UNAME} ${HOME} _SETUP_USER USER ${UNAME} WORKDIR ${HOME} # entrypoint ENTRYPOINT ["/usr/bin/sunshine"] ================================================ FILE: docker/clion-toolchain.dockerfile ================================================ # syntax=docker/dockerfile:1 # artifacts: false # platforms: linux/amd64 # platforms_pr: linux/amd64 # no-cache-filters: toolchain-base,toolchain ARG BASE=debian ARG TAG=trixie-slim FROM ${BASE}:${TAG} AS toolchain-base ENV DEBIAN_FRONTEND=noninteractive FROM toolchain-base AS toolchain ARG TARGETPLATFORM RUN echo "target_platform: ${TARGETPLATFORM}" ENV DISPLAY=:0 SHELL ["/bin/bash", "-o", "pipefail", "-c"] # install dependencies RUN <<_DEPS #!/bin/bash set -e apt-get update -y apt-get install -y --no-install-recommends \ build-essential \ cmake=3.31.* \ ca-certificates \ doxygen \ gcc=4:14.2.* \ g++=4:14.2.* \ gdb \ git \ graphviz \ libayatana-appindicator3-dev \ libcap-dev \ libcurl4-openssl-dev \ libdrm-dev \ libevdev-dev \ libgbm-dev \ libminiupnpc-dev \ libnotify-dev \ libnuma-dev \ libopus-dev \ libpulse-dev \ libssl-dev \ libva-dev \ libwayland-dev \ libx11-dev \ libxcb-shm0-dev \ libxcb-xfixes0-dev \ libxcb1-dev \ libxfixes-dev \ libxrandr-dev \ libxtst-dev \ npm \ udev \ wget \ x11-xserver-utils \ xvfb apt-get clean rm -rf /var/lib/apt/lists/* _DEPS # install cuda WORKDIR /build/cuda # versions: https://developer.nvidia.com/cuda-toolkit-archive ENV CUDA_VERSION="12.9.1" ENV CUDA_BUILD="575.57.08" RUN <<_INSTALL_CUDA #!/bin/bash set -e cuda_prefix="https://developer.download.nvidia.com/compute/cuda/" cuda_suffix="" if [[ "${TARGETPLATFORM}" == 'linux/arm64' ]]; then cuda_suffix="_sbsa" fi url="${cuda_prefix}${CUDA_VERSION}/local_installers/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux${cuda_suffix}.run" echo "cuda url: ${url}" tmpfile="/tmp/cuda.run" wget "$url" --progress=bar:force:noscroll --show-progress -O "$tmpfile" chmod a+x "${tmpfile}" "${tmpfile}" --silent --toolkit --toolkitpath=/usr/local --no-opengl-libs --no-man-page --no-drm rm -f "${tmpfile}" _INSTALL_CUDA WORKDIR /toolchain # Create a shell script that starts Xvfb and then runs a shell RUN <<_ENTRYPOINT #!/bin/bash set -e cat < entrypoint.sh #!/bin/bash Xvfb ${DISPLAY} -screen 0 1024x768x24 & if [ "\$#" -eq 0 ]; then exec "/bin/bash" else exec "\$@" fi EOF # Make the script executable chmod +x entrypoint.sh # Note about CLion echo "ATTENTION: CLion will override the entrypoint, you can disable this in the toolchain settings" _ENTRYPOINT # Use the shell script as the entrypoint ENTRYPOINT ["/toolchain/entrypoint.sh"] ================================================ FILE: docker/debian-trixie.dockerfile ================================================ # syntax=docker/dockerfile:1 # artifacts: true # platforms: linux/amd64,linux/arm64/v8 # platforms_pr: linux/amd64 # no-cache-filters: sunshine-base,artifacts,sunshine ARG BASE=debian ARG TAG=trixie FROM ${BASE}:${TAG} AS sunshine-base ENV DEBIAN_FRONTEND=noninteractive FROM sunshine-base AS sunshine-deps SHELL ["/bin/bash", "-o", "pipefail", "-c"] # Copy only the build script and necessary files first for better layer caching WORKDIR /build/sunshine/ COPY --link scripts/linux_build.sh ./scripts/linux_build.sh COPY --link packaging/linux/patches/ ./packaging/linux/patches/ # Install dependencies first - this layer will be cached RUN <<_DEPS #!/bin/bash set -e chmod +x ./scripts/linux_build.sh ./scripts/linux_build.sh \ --step=deps \ --cuda-patches \ --sudo-off apt-get clean rm -rf /var/lib/apt/lists/* _DEPS FROM sunshine-deps AS sunshine-build ARG BRANCH ARG BUILD_VERSION ARG COMMIT # note: BUILD_VERSION may be blank ENV BRANCH=${BRANCH} ENV BUILD_VERSION=${BUILD_VERSION} ENV COMMIT=${COMMIT} # Now copy the full repository COPY --link .. . # Configure, validate, build and package RUN <<_BUILD #!/bin/bash set -e ./scripts/linux_build.sh \ --step=cmake \ --publisher-name='LizardByte' \ --publisher-website='https://app.lizardbyte.dev' \ --publisher-issue-url='https://app.lizardbyte.dev/support' \ --sudo-off ./scripts/linux_build.sh \ --step=validation \ --sudo-off ./scripts/linux_build.sh \ --step=build \ --sudo-off ./scripts/linux_build.sh \ --step=package \ --sudo-off _BUILD # run tests WORKDIR /build/sunshine/build/tests RUN <<_TEST #!/bin/bash set -e export DISPLAY=:1 Xvfb ${DISPLAY} -screen 0 1024x768x24 & ./test_sunshine --gtest_color=yes _TEST FROM sunshine-base AS sunshine ARG BASE ARG TAG ARG TARGETARCH # artifacts to be extracted in CI COPY --link --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.deb /artifacts/sunshine-${BASE}-${TAG}-${TARGETARCH}.deb # copy deb from builder COPY --link --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.deb /sunshine.deb # install sunshine RUN <<_INSTALL_SUNSHINE #!/bin/bash set -e apt-get update -y apt-get install -y --no-install-recommends /sunshine.deb apt-get clean rm -rf /var/lib/apt/lists/* _INSTALL_SUNSHINE # network setup EXPOSE 47984-47990/tcp EXPOSE 48010 EXPOSE 47998-48000/udp # setup user ARG PGID=1000 ENV PGID=${PGID} ARG PUID=1000 ENV PUID=${PUID} ENV TZ="UTC" ARG UNAME=lizard ENV UNAME=${UNAME} ENV HOME=/home/$UNAME # setup user RUN <<_SETUP_USER #!/bin/bash set -e groupadd -f -g "${PGID}" "${UNAME}" useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -u "${PUID}" "${UNAME}" mkdir -p ${HOME}/.config/sunshine ln -s ${HOME}/.config/sunshine /config chown -R ${UNAME} ${HOME} _SETUP_USER USER ${UNAME} WORKDIR ${HOME} # entrypoint ENTRYPOINT ["/usr/bin/sunshine"] ================================================ FILE: docker/ubuntu-22.04.dockerfile ================================================ # syntax=docker/dockerfile:1 # artifacts: true # platforms: linux/amd64,linux/arm64/v8 # platforms_pr: linux/amd64 # no-cache-filters: sunshine-base,artifacts,sunshine ARG BASE=ubuntu ARG TAG=22.04 FROM ${BASE}:${TAG} AS sunshine-base ENV DEBIAN_FRONTEND=noninteractive FROM sunshine-base AS sunshine-deps SHELL ["/bin/bash", "-o", "pipefail", "-c"] # Copy only the build script first for better layer caching WORKDIR /build/sunshine/ COPY --link scripts/linux_build.sh ./scripts/linux_build.sh # Install dependencies first - this layer will be cached RUN <<_DEPS #!/bin/bash set -e chmod +x ./scripts/linux_build.sh ./scripts/linux_build.sh \ --step=deps \ --ubuntu-test-repo \ --sudo-off apt-get clean rm -rf /var/lib/apt/lists/* _DEPS FROM sunshine-deps AS sunshine-build ARG BRANCH ARG BUILD_VERSION ARG COMMIT # note: BUILD_VERSION may be blank ENV BRANCH=${BRANCH} ENV BUILD_VERSION=${BUILD_VERSION} ENV COMMIT=${COMMIT} # Now copy the full repository COPY --link .. . # Configure, validate, build and package RUN <<_BUILD #!/bin/bash set -e ./scripts/linux_build.sh \ --step=cmake \ --publisher-name='LizardByte' \ --publisher-website='https://app.lizardbyte.dev' \ --publisher-issue-url='https://app.lizardbyte.dev/support' \ --sudo-off ./scripts/linux_build.sh \ --step=validation \ --sudo-off ./scripts/linux_build.sh \ --step=build \ --sudo-off ./scripts/linux_build.sh \ --step=package \ --sudo-off _BUILD # run tests WORKDIR /build/sunshine/build/tests RUN <<_TEST #!/bin/bash set -e export DISPLAY=:1 Xvfb ${DISPLAY} -screen 0 1024x768x24 & ./test_sunshine --gtest_color=yes _TEST FROM sunshine-base AS sunshine ARG BASE ARG TAG ARG TARGETARCH # artifacts to be extracted in CI COPY --link --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.deb /artifacts/sunshine-${BASE}-${TAG}-${TARGETARCH}.deb # copy deb from builder COPY --link --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.deb /sunshine.deb # install sunshine RUN <<_INSTALL_SUNSHINE #!/bin/bash set -e apt-get update -y apt-get install -y --no-install-recommends /sunshine.deb apt-get clean rm -rf /var/lib/apt/lists/* _INSTALL_SUNSHINE # network setup EXPOSE 47984-47990/tcp EXPOSE 48010 EXPOSE 47998-48000/udp # setup user ARG PGID=1000 ENV PGID=${PGID} ARG PUID=1000 ENV PUID=${PUID} ENV TZ="UTC" ARG UNAME=lizard ENV UNAME=${UNAME} ENV HOME=/home/$UNAME # setup user RUN <<_SETUP_USER #!/bin/bash set -e groupadd -f -g "${PGID}" "${UNAME}" useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -u "${PUID}" "${UNAME}" mkdir -p ${HOME}/.config/sunshine ln -s ${HOME}/.config/sunshine /config chown -R ${UNAME} ${HOME} _SETUP_USER USER ${UNAME} WORKDIR ${HOME} # entrypoint ENTRYPOINT ["/usr/bin/sunshine"] ================================================ FILE: docker/ubuntu-24.04.dockerfile ================================================ # syntax=docker/dockerfile:1 # artifacts: true # platforms: linux/amd64,linux/arm64/v8 # platforms_pr: linux/amd64 # no-cache-filters: sunshine-base,artifacts,sunshine ARG BASE=ubuntu ARG TAG=24.04 FROM ${BASE}:${TAG} AS sunshine-base ENV DEBIAN_FRONTEND=noninteractive FROM sunshine-base AS sunshine-deps SHELL ["/bin/bash", "-o", "pipefail", "-c"] # Copy only the build script first for better layer caching WORKDIR /build/sunshine/ COPY --link scripts/linux_build.sh ./scripts/linux_build.sh # Install dependencies first - this layer will be cached RUN <<_DEPS #!/bin/bash set -e chmod +x ./scripts/linux_build.sh ./scripts/linux_build.sh \ --step=deps \ --sudo-off apt-get clean rm -rf /var/lib/apt/lists/* _DEPS FROM sunshine-deps AS sunshine-build ARG BRANCH ARG BUILD_VERSION ARG COMMIT # note: BUILD_VERSION may be blank ENV BRANCH=${BRANCH} ENV BUILD_VERSION=${BUILD_VERSION} ENV COMMIT=${COMMIT} # Now copy the full repository COPY --link .. . # Configure, validate, build and package RUN <<_BUILD #!/bin/bash set -e ./scripts/linux_build.sh \ --step=cmake \ --publisher-name='LizardByte' \ --publisher-website='https://app.lizardbyte.dev' \ --publisher-issue-url='https://app.lizardbyte.dev/support' \ --sudo-off ./scripts/linux_build.sh \ --step=validation \ --sudo-off ./scripts/linux_build.sh \ --step=build \ --sudo-off ./scripts/linux_build.sh \ --step=package \ --sudo-off _BUILD # run tests WORKDIR /build/sunshine/build/tests RUN <<_TEST #!/bin/bash set -e export DISPLAY=:1 Xvfb ${DISPLAY} -screen 0 1024x768x24 & ./test_sunshine --gtest_color=yes _TEST FROM sunshine-base AS sunshine ARG BASE ARG TAG ARG TARGETARCH # artifacts to be extracted in CI COPY --link --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.deb /artifacts/sunshine-${BASE}-${TAG}-${TARGETARCH}.deb # copy deb from builder COPY --link --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.deb /sunshine.deb # install sunshine RUN <<_INSTALL_SUNSHINE #!/bin/bash set -e apt-get update -y apt-get install -y --no-install-recommends /sunshine.deb apt-get clean rm -rf /var/lib/apt/lists/* _INSTALL_SUNSHINE # network setup EXPOSE 47984-47990/tcp EXPOSE 48010 EXPOSE 47998-48000/udp # setup user ARG PGID=1001 ENV PGID=${PGID} ARG PUID=1001 ENV PUID=${PUID} ENV TZ="UTC" ARG UNAME=lizard ENV UNAME=${UNAME} ENV HOME=/home/$UNAME # setup user RUN <<_SETUP_USER #!/bin/bash set -e groupadd -f -g "${PGID}" "${UNAME}" useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -u "${PUID}" "${UNAME}" mkdir -p ${HOME}/.config/sunshine ln -s ${HOME}/.config/sunshine /config chown -R ${UNAME} ${HOME} _SETUP_USER USER ${UNAME} WORKDIR ${HOME} # entrypoint ENTRYPOINT ["/usr/bin/sunshine"] ================================================ FILE: docs/Doxyfile ================================================ # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project. # # All text after a double hash (##) is considered a comment and is placed in # front of the TAG it is preceding. # # All text after a single hash (#) is considered a comment and will be ignored. # The format is: # TAG = value [value, ...] # For lists, items can also be appended using: # TAG += value [value, ...] # Values that contain spaces should be placed between quotes (\" \"). # # Note: # # Use doxygen to compare the used configuration file with the template # configuration file: # doxygen -x [configFile] # Use doxygen to compare the used configuration file with the template # configuration file without replacing the environment variables or CMake type # replacement variables: # doxygen -x_noenv [configFile] # project metadata DOCSET_BUNDLE_ID = dev.lizardbyte.Sunshine DOCSET_PUBLISHER_ID = dev.lizardbyte.Sunshine.documentation PROJECT_BRIEF = "Self-hosted game stream host for Moonlight." PROJECT_ICON = ../sunshine.ico PROJECT_LOGO = ../sunshine.png PROJECT_NAME = Sunshine # project specific settings DOT_GRAPH_MAX_NODES = 60 # IMAGE_PATH = ../docs/images PREDEFINED += SUNSHINE_BUILD_WAYLAND PREDEFINED += SUNSHINE_TRAY=1 # TODO: Enable this when we have complete documentation WARN_IF_UNDOCUMENTED = NO # files and directories to process USE_MDFILE_AS_MAINPAGE = ../README.md INPUT = ../README.md \ getting_started.md \ changelog.md \ ../DOCKER_README.md \ third_party_packages.md \ gamestream_migration.md \ legal.md \ configuration.md \ app_examples.md \ awesome_sunshine.md \ guides.md \ performance_tuning.md \ api.md \ troubleshooting.md \ building.md \ contributing.md \ ../third-party/doxyconfig/docs/source_code.md \ ../src # extra css HTML_EXTRA_STYLESHEET += doc-styles.css # extra js HTML_EXTRA_FILES += api.js HTML_EXTRA_FILES += configuration.js # custom aliases ALIASES += api_examples{3|}="@htmlonly@endhtmlonly" ================================================ FILE: docs/api.js ================================================ function generateExamples(endpoint, method, body = null) { let curlBodyString = ''; let psBodyString = ''; if (body) { const curlJsonString = JSON.stringify(body).replace(/"/g, '\\"'); curlBodyString = ` -d "${curlJsonString}"`; psBodyString = `-Body (ConvertTo-Json ${JSON.stringify(body)})`; } return { cURL: `curl -u user:pass -H "Content-Type: application/json" -X ${method.trim()} -k https://localhost:47990${endpoint.trim()}${curlBodyString}`, Python: `import json import requests from requests.auth import HTTPBasicAuth requests.${method.trim().toLowerCase()}( auth=HTTPBasicAuth('user', 'pass'), url='https://localhost:47990${endpoint.trim()}', verify=False,${body ? `\n json=${JSON.stringify(body)},` : ''} ).json()`, JavaScript: `fetch('https://localhost:47990${endpoint.trim()}', { method: '${method.trim()}', headers: { 'Authorization': 'Basic ' + btoa('user:pass'), 'Content-Type': 'application/json', }${body ? `,\n body: JSON.stringify(${JSON.stringify(body)}),` : ''} }) .then(response => response.json()) .then(data => console.log(data));`, PowerShell: `Invoke-RestMethod \` -SkipCertificateCheck \` -ContentType 'application/json' \` -Uri 'https://localhost:47990${endpoint.trim()}' \` -Method ${method.trim()} \` -Headers @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes('user:pass'))} ${psBodyString}` }; } function hashString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash |= 0; // Convert to 32bit integer } return hash; } function createTabs(examples) { const languages = Object.keys(examples); let tabs = '
'; let content = '
'; languages.forEach((lang, index) => { const hash = hashString(examples[lang]); tabs += ``; content += `
${examples[lang].split('\n').map(line => `
${line}
`).join('')}
`; }); tabs += '
'; content += '
'; setTimeout(() => { languages.forEach((lang, index) => { const hash = hashString(examples[lang]); const copyButton = document.getElementById(`copy-button-${lang}-${hash}`); copyButton.addEventListener('click', copyContent); }); }, 0); return tabs + content; } function copyContent() { const content = this.previousElementSibling.cloneNode(true); if (content instanceof Element) { // filter out line number from file listings content.querySelectorAll(".lineno, .ttc").forEach((node) => { node.remove(); }); let textContent = Array.from(content.querySelectorAll('.line')) .map(line => line.innerText) .join('\n') .trim(); // Join lines with newline characters and trim leading/trailing whitespace navigator.clipboard.writeText(textContent); this.classList.add("success"); this.innerHTML = ``; window.setTimeout(() => { this.classList.remove("success"); this.innerHTML = ``; }, 980); } else { console.error('Failed to copy: content is not a DOM element'); } } function openTab(evt, lang) { const tabcontent = document.getElementsByClassName("tabcontent"); for (const content of tabcontent) { content.style.display = "none"; } const tablinks = document.getElementsByClassName("tab-button"); for (const link of tablinks) { link.className = link.className.replace(" active", ""); } const selectedTabs = document.querySelectorAll(`#${lang}`); for (const tab of selectedTabs) { tab.style.display = "block"; } const selectedButtons = document.querySelectorAll(`.tab-button[onclick*="${lang}"]`); for (const button of selectedButtons) { button.className += " active"; } } ================================================ FILE: docs/api.md ================================================ # API Sunshine has a RESTful API which can be used to interact with the service. Unless otherwise specified, authentication is required for all API calls. You can authenticate using basic authentication with the admin username and password. @htmlonly @endhtmlonly ## GET /api/apps @copydoc confighttp::getApps() ## POST /api/apps @copydoc confighttp::saveApp() ## POST /api/apps/close @copydoc confighttp::closeApp() ## DELETE /api/apps/{index} @copydoc confighttp::deleteApp() ## GET /api/clients/list @copydoc confighttp::getClients() ## POST /api/clients/unpair @copydoc confighttp::unpair() ## POST /api/clients/unpair-all @copydoc confighttp::unpairAll() ## GET /api/config @copydoc confighttp::getConfig() ## GET /api/configLocale @copydoc confighttp::getLocale() ## POST /api/config @copydoc confighttp::saveConfig() ## POST /api/covers/upload @copydoc confighttp::uploadCover() ## GET /api/logs @copydoc confighttp::getLogs() ## POST /api/password @copydoc confighttp::savePassword() ## POST /api/pin @copydoc confighttp::savePin() ## POST /api/reset-display-device-persistence @copydoc confighttp::resetDisplayDevicePersistence() ## POST /api/restart @copydoc confighttp::restart()
| Previous | Next | |:--------------------------------------------|--------------------------------------:| | [Performance Tuning](performance_tuning.md) | [Troubleshooting](troubleshooting.md) |
[TOC]
================================================ FILE: docs/app_examples.md ================================================ # App Examples Since not all applications behave the same, we decided to create some examples to help you get started adding games and applications to Sunshine. > [!TIP] > Throughout these examples, any fields not shown are left blank. You can enhance your experience by > adding an image or a log file (via the `Output` field). > [!WARNING] > When a working directory is not specified, it defaults to the folder where the target application resides. ## Common Examples ### Desktop | Field | Value | |------------------|----------------------------| | Application Name | @code{}Desktop@endcode | | Image | @code{}desktop.png@endcode | ### Steam Big Picture > [!NOTE] > Steam is launched as a detached command because Steam starts with a process that self updates itself and the original > process is killed. @tabs{ @tab{Linux | \| Field \| Value \| \|------------------------------\|------------------------------------------------------\| \| Application Name \| @code{}Steam Big Picture@endcode \| \| Command Preporations -> Undo \| @code{}setsid steam steam://close/bigpicture@endcode \| \| Detached Commands \| @code{}setsid steam steam://open/bigpicture@endcode \| \| Image \| @code{}steam.png@endcode \| } @tab{macOS | \| Field \| Value \| \|------------------------------\|------------------------------------------------\| \| Application Name \| @code{}Steam Big Picture@endcode \| \| Command Preporations -> Undo \| @code{}open steam://close/bigpicture@endcode \| \| Detached Commands \| @code{}open steam://open/bigpicture@endcode \| \| Image \| @code{}steam.png@endcode \| } @tab{Windows | \| Field \| Value \| \|------------------------------\|-------------------------------------------\| \| Application Name \| @code{}Steam Big Picture@endcode \| \| Command Preporations -> Undo \| @code{}steam://close/bigpicture@endcode \| \| Detached Commands \| @code{}steam://open/bigpicture@endcode \| \| Image \| @code{}steam.png@endcode \| } } ### Epic Game Store game > [!NOTE] > Using the URI method will be the most consistent between various games. #### URI @tabs{ @tab{Windows | \| Field \| Value \| \|------------------\|-------------------------------------------------------------------------------------------------------------------------------------------------------\| \| Application Name \| @code{}Surviving Mars@endcode \| \| Commands \| @code{}com.epicgames.launcher://apps/d759128018124dcabb1fbee9bb28e178%3A20729b9176c241f0b617c5723e70ec2d%3AOvenbird?action=launch&silent=true@endcode \| } } #### Binary (w/ working directory @tabs{ @tab{Windows | \| Field \| Value \| \|-------------------\|------------------------------------------------------------\| \| Application Name \| @code{}Surviving Mars@endcode \| \| Command \| @code{}MarsEpic.exe@endcode \| \| Working Directory \| @code{}"C:\Program Files\Epic Games\SurvivingMars"@endcode \| } } #### Binary (w/o working directory) @tabs{ @tab{Windows | \| Field \| Value \| \|-------------------\|-------------------------------------------------------------------------\| \| Application Name \| @code{}Surviving Mars@endcode \| \| Command \| @code{}"C:\Program Files\Epic Games\SurvivingMars\MarsEpic.exe"@endcode \| } } ### Steam game > [!NOTE] > Using the URI method will be the most consistent between various games. #### URI @tabs{ @tab{Linux | \| Field \| Value \| \|-------------------\|------------------------------------------------------\| \| Application Name \| @code{}Surviving Mars@endcode \| \| Detached Commands \| @code{}setsid steam steam://rungameid/464920@endcode \| } @tab{macOS | \| Field \| Value \| \|-------------------\|----------------------------------------------\| \| Application Name \| @code{}Surviving Mars@endcode \| \| Detached Commands \| @code{}open steam://rungameid/464920@endcode \| } @tab{Windows | \| Field \| Value \| \|-------------------\|-----------------------------------------\| \| Application Name \| @code{}Surviving Mars@endcode \| \| Detached Commands \| @code{}steam://rungameid/464920@endcode \| } } #### Binary (w/ working directory @tabs{ @tab{Linux | \| Field \| Value \| \|-------------------\|--------------------------------------------------------------\| \| Application Name \| @code{}Surviving Mars@endcode \| \| Command \| @code{}MarsSteam@endcode \| \| Working Directory \| @code{}~/.steam/steam/SteamApps/common/Survivng Mars@endcode \| } @tab{macOS | \| Field \| Value \| \|-------------------\|--------------------------------------------------------------\| \| Application Name \| @code{}Surviving Mars@endcode \| \| Command \| @code{}MarsSteam@endcode \| \| Working Directory \| @code{}~/.steam/steam/SteamApps/common/Survivng Mars@endcode \| } @tab{Windows | \| Field \| Value \| \|-------------------\|-------------------------------------------------------------------------------\| \| Application Name \| @code{}Surviving Mars@endcode \| \| Command \| @code{}MarsSteam.exe@endcode \| \| Working Directory \| @code{}"C:\Program Files (x86)\Steam\steamapps\common\Surviving Mars"@endcode \| } } #### Binary (w/o working directory) @tabs{ @tab{Linux | \| Field \| Value \| \|-------------------\|------------------------------------------------------------------------\| \| Application Name \| @code{}Surviving Mars@endcode \| \| Command \| @code{}~/.steam/steam/SteamApps/common/Survivng Mars/MarsSteam@endcode \| } @tab{macOS | \| Field \| Value \| \|-------------------\|------------------------------------------------------------------------\| \| Application Name \| @code{}Surviving Mars@endcode \| \| Command \| @code{}~/.steam/steam/SteamApps/common/Survivng Mars/MarsSteam@endcode \| } @tab{Windows | \| Field \| Value \| \|-------------------\|---------------------------------------------------------------------------------------------\| \| Application Name \| @code{}Surviving Mars@endcode \| \| Command \| @code{}"C:\Program Files (x86)\Steam\steamapps\common\Surviving Mars\MarsSteam.exe"@endcode \| } } ### Prep Commands #### Changing Resolution and Refresh Rate ##### Linux ###### X11 | Prep Step | Command | |-----------|---------------------------------------------------------------------------------------------------------------------------------------| | Do | @code{}sh -c "xrandr --output HDMI-1 --mode ${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT} --rate ${SUNSHINE_CLIENT_FPS}"@endcode | | Undo | @code{}xrandr --output HDMI-1 --mode 3840x2160 --rate 120@endcode | > [!TIP] > The above only works if the xrandr mode already exists. You will need to create new modes to stream to macOS > and iOS devices, since they use non-standard resolutions. > > You can update the ``Do`` command to this: > ```bash > bash -c "${HOME}/scripts/set-custom-res.sh \"${SUNSHINE_CLIENT_WIDTH}\" \"${SUNSHINE_CLIENT_HEIGHT}\" \"${SUNSHINE_CLIENT_FPS}\"" > ``` > > The `set-custom-res.sh` will have this content: > ```bash > #!/bin/bash > set -e > > # Get params and set any defaults > width=${1:-1920} > height=${2:-1080} > refresh_rate=${3:-60} > > # You may need to adjust the scaling differently so the UI/text isn't too small / big > scale=${4:-0.55} > > # Get the name of the active display > display_output=$(xrandr | grep " connected" | awk '{ print $1 }') > > # Get the modeline info from the 2nd row in the cvt output > modeline=$(cvt ${width} ${height} ${refresh_rate} | awk 'FNR == 2') > xrandr_mode_str=${modeline//Modeline \"*\" /} > mode_alias="${width}x${height}" > > echo "xrandr setting new mode ${mode_alias} ${xrandr_mode_str}" > xrandr --newmode ${mode_alias} ${xrandr_mode_str} > xrandr --addmode ${display_output} ${mode_alias} > > # Reset scaling > xrandr --output ${display_output} --scale 1 > > # Apply new xrandr mode > xrandr --output ${display_output} --primary --mode ${mode_alias} --pos 0x0 --rotate normal --scale ${scale} > > # Optional reset your wallpaper to fit to new resolution > # xwallpaper --zoom /path/to/wallpaper.png > ``` ###### Wayland (wlroots, e.g. hyprland) | Prep Step | Command | |-----------|------------------------------------------------------------------------------------------------------------------------------------------| | Do | @code{}sh -c "wlr-xrandr --output HDMI-1 --mode \"${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}@${SUNSHINE_CLIENT_FPS}Hz\""@endcode | | Undo | @code{}wlr-xrandr --output HDMI-1 --mode 3840x2160@120Hz@endcode | > [!TIP] > `wlr-xrandr` only works with wlroots-based compositors. ###### Gnome (X11) | Prep Step | Command | |-----------|---------------------------------------------------------------------------------------------------------------------------------------| | Do | @code{}sh -c "xrandr --output HDMI-1 --mode ${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT} --rate ${SUNSHINE_CLIENT_FPS}"@endcode | | Undo | @code{}xrandr --output HDMI-1 --mode 3840x2160 --rate 120@endcode | ###### Gnome (Wayland) | Prep Step | Command | |-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Do | @code{}sh -c "displayconfig-mutter set --connector HDMI-1 --resolution ${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT} --refresh-rate ${SUNSHINE_CLIENT_FPS} --hdr ${SUNSHINE_CLIENT_HDR}"@endcode | | Undo | @code{}displayconfig-mutter set --connector HDMI-1 --resolution 3840x2160 --refresh-rate 120 --hdr false@endcode | Installation instructions for displayconfig-mutter can be [found here](https://github.com/eaglesemanation/displayconfig-mutter). Alternatives include [gnome-randr-rust](https://github.com/maxwellainatchi/gnome-randr-rust) and [gnome-randr.py](https://gitlab.com/Oschowa/gnome-randr), but both of those are unmaintained and do not support newer Mutter features such as HDR and VRR. > [!TIP] > HDR support has been added to Gnome 48, to check if your display supports it, you can run this: > ``` > displayconfig-mutter list > ``` > If it doesn't, then remove ``--hdr`` flag from both ``Do`` and ``Undo`` steps. ###### KDE Plasma (Wayland, X11) | Prep Step | Command | |-----------|--------------------------------------------------------------------------------------------------------------------------------------| | Do | @code{}sh -c "kscreen-doctor output.HDMI-A-1.mode.${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}@${SUNSHINE_CLIENT_FPS}"@endcode | | Undo | @code{}kscreen-doctor output.HDMI-A-1.mode.3840x2160@120@endcode | > [!CAUTION] > The names of your displays will differ between X11 and Wayland. > Be sure to use the correct name, depending on your session manager. > e.g., On X11, the monitor may be called ``HDMI-A-0``, but on Wayland, it may be called ``HDMI-A-1``. > [!TIP] > Replace ``HDMI-A-1`` with the display name of the monitor you would like to use for Moonlight. > You can list the monitors available to you with: > ``` > kscreen-doctor -o > ``` > > These will also give you the supported display properties for each monitor. You can select them either by > hard-coding their corresponding number (e.g. ``kscreen-doctor output.HDMI-A1.mode.0``) or using the above > ``do`` command to fetch the resolution requested by your Moonlight client > (which has a chance of not being supported by your monitor). ###### NVIDIA | Prep Step | Command | |-----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Do | @code{}sh -c "nvidia-settings -a CurrentMetaMode=\"HDMI-1: nvidia-auto-select { ViewPortIn=${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}, ViewPortOut=${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}+0+0 }\""@endcode | | Undo | @code{}nvidia-settings -a CurrentMetaMode=\"HDMI-1: nvidia-auto-select { ViewPortIn=3840x2160, ViewPortOut=3840x2160+0+0 }"@endcode | ##### macOS ###### displayplacer > [!NOTE] > This example uses the `displayplacer` tool to change the resolution. > This tool can be installed following instructions in their > [GitHub repository](https://github.com/jakehilborn/displayplacer). | Prep Step | Command | |-----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Do | @code{}sh -c "displayplacer \"id: res:${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT} hz:${SUNSHINE_CLIENT_FPS} scaling:on origin:(0,0) degree:0\""@endcode | | Undo | @code{}displayplacer "id: res:3840x2160 hz:120 scaling:on origin:(0,0) degree:0"@endcode | ##### Windows Sunshine has built-in support for changing the resolution and refresh rate on Windows. If you prefer to use a third-party tool, you can use *QRes* as an example. ###### QRes > [!NOTE] > This example uses the *QRes* tool to change the resolution and refresh rate. > This tool can be downloaded from their [SourceForge repository](https://sourceforge.net/projects/qres). | Prep Step | Command | |-----------|---------------------------------------------------------------------------------------------------------------------------| | Do | @code{}cmd /C "FullPath\qres.exe /x:%SUNSHINE_CLIENT_WIDTH% /y:%SUNSHINE_CLIENT_HEIGHT% /r:%SUNSHINE_CLIENT_FPS%"@endcode | | Undo | @code{}FullPath\qres.exe /x:3840 /y:2160 /r:120@endcode | ### Additional Considerations #### Linux (Flatpak) > [!CAUTION] > Because Flatpak packages run in a sandboxed environment and do not normally have access to the > host, the Flatpak of Sunshine requires commands to be prefixed with `flatpak-spawn --host`. #### Windows **Elevating Commands (Windows)** If you've installed Sunshine as a service (default), you can specify if a command should be elevated with administrative privileges. Simply enable the elevated option in the WEB UI, or add it to the JSON configuration. This is an option for both prep-cmd and regular commands and will launch the process with the current user without a UAC prompt. **Example** ```json { "name": "Game With AntiCheat that Requires Admin", "output": "", "cmd": "ping 127.0.0.1", "exclude-global-prep-cmd": false, "elevated": true, "prep-cmd": [ { "do": "powershell.exe -command \"Start-Streaming\"", "undo": "powershell.exe -command \"Stop-Streaming\"", "elevated": false } ], "image-path": "" } ```
| Previous | Next | |:----------------------------------|----------------------------------------:| | [Configuration](configuration.md) | [Awesome-Sunshine](awesome_sunshine.md) |
[TOC]
================================================ FILE: docs/awesome_sunshine.md ================================================ # Awesome-Sunshine @htmlonly @endhtmlonly
| Previous | Next | |:--------------------------------|--------------------:| | [App Examples](app_examples.md) | [Guides](guides.md) |
[TOC]
================================================ FILE: docs/building.md ================================================ # Building Sunshine binaries are built using [CMake](https://cmake.org) and requires `cmake` > 3.25. ## Building Locally ### Compiler It is recommended to use one of the following compilers: | Compiler | Version | |:------------|:--------| | GCC | 13+ | | Clang | 17+ | | Apple Clang | 15+ | ### Dependencies #### Linux Dependencies vary depending on the distribution. You can reference our [linux_build.sh](https://github.com/LizardByte/Sunshine/blob/master/scripts/linux_build.sh) script for a list of dependencies we use in Debian-based and Fedora-based distributions. Please submit a PR if you would like to extend the script to support other distributions. ##### CUDA Toolkit Sunshine requires CUDA Toolkit for NVFBC capture. There are two caveats to CUDA: 1. The version installed depends on the version of GCC. 2. The version of CUDA you use will determine compatibility with various GPU generations. At the time of writing, the recommended version to use is CUDA ~12.9. See [CUDA compatibility](https://docs.nvidia.com/deploy/cuda-compatibility/index.html) for more info. > [!NOTE] > To install older versions, select the appropriate run file based on your desired CUDA version and architecture > according to [CUDA Toolkit Archive](https://developer.nvidia.com/cuda-toolkit-archive) #### macOS You can either use [Homebrew](https://brew.sh) or [MacPorts](https://www.macports.org) to install dependencies. ##### Homebrew ```bash dependencies=( "boost" # Optional "cmake" "doxygen" # Optional, for docs "graphviz" # Optional, for docs "icu4c" # Optional, if boost is not installed "miniupnpc" "ninja" "node" "openssl@3" "opus" "pkg-config" ) brew install "${dependencies[@]}" ``` If there are issues with an SSL header that is not found: @tabs{ @tab{ Intel | ```bash ln -s /usr/local/opt/openssl/include/openssl /usr/local/include/openssl ```} @tab{ Apple Silicon | ```bash ln -s /opt/homebrew/opt/openssl/include/openssl /opt/homebrew/include/openssl ``` } } ##### MacPorts ```bash dependencies=( "cmake" "curl" "doxygen" # Optional, for docs "graphviz" # Optional, for docs "libopus" "miniupnpc" "ninja" "npm9" "pkgconfig" ) sudo port install "${dependencies[@]}" ``` #### Windows First you need to install [MSYS2](https://www.msys2.org), then startup "MSYS2 UCRT64" and execute the following commands. ##### Update all packages ```bash pacman -Syu ``` ##### Install dependencies ```bash dependencies=( "git" "mingw-w64-ucrt-x86_64-boost" # Optional "mingw-w64-ucrt-x86_64-cmake" "mingw-w64-ucrt-x86_64-cppwinrt" "mingw-w64-ucrt-x86_64-curl-winssl" "mingw-w64-ucrt-x86_64-doxygen" # Optional, for docs... better to install official Doxygen "mingw-w64-ucrt-x86_64-graphviz" # Optional, for docs "mingw-w64-ucrt-x86_64-MinHook" "mingw-w64-ucrt-x86_64-miniupnpc" "mingw-w64-ucrt-x86_64-nodejs" "mingw-w64-ucrt-x86_64-nsis" "mingw-w64-ucrt-x86_64-onevpl" "mingw-w64-ucrt-x86_64-openssl" "mingw-w64-ucrt-x86_64-opus" "mingw-w64-ucrt-x86_64-toolchain" "mingw-w64-ucrt-x86_64-nlohmann_json" ) pacman -S "${dependencies[@]}" ``` ### Clone Ensure [git](https://git-scm.com) is installed on your system, then clone the repository using the following command: ```bash git clone https://github.com/ClassicOldSong/Apollo.git --recurse-submodules cd Apollo mkdir build ``` ### Build ```bash cmake -B build -G Ninja -S . ninja -C build ``` > [!TIP] > Available build options can be found in > [options.cmake](https://github.com/LizardByte/Sunshine/blob/master/cmake/prep/options.cmake). ### Package @tabs{ @tab{Linux | @tabs{ @tab{deb | ```bash cpack -G DEB --config ./build/CPackConfig.cmake ```} @tab{rpm | ```bash cpack -G RPM --config ./build/CPackConfig.cmake ```} }} @tab{macOS | @tabs{ @tab{DragNDrop | ```bash cpack -G DragNDrop --config ./build/CPackConfig.cmake ```} }} @tab{Windows | @tabs{ @tab{Installer | ```bash cpack -G NSIS --config ./build/CPackConfig.cmake ```} @tab{Portable | ```bash cpack -G ZIP --config ./build/CPackConfig.cmake ```} }} } ### Remote Build It may be beneficial to build remotely in some cases. This will enable easier building on different operating systems. 1. Fork the project 2. Activate workflows 3. Trigger the *CI* workflow manually 4. Download the artifacts/binaries from the workflow run summary
| Previous | Next | |:--------------------------------------|--------------------------------:| | [Troubleshooting](troubleshooting.md) | [Contributing](contributing.md) |
[TOC]
================================================ FILE: docs/changelog.md ================================================ # Changelog @htmlonly @endhtmlonly
| Previous | Next | |:--------------------------------------|------------------------------:| | [Getting Started](getting_started.md) | [Docker](../DOCKER_README.md) |
[TOC]
================================================ FILE: docs/configuration.js ================================================ /** * @brief Add a button to open the configuration option for each table */ document.addEventListener("DOMContentLoaded", function() { const tables = document.querySelectorAll("table"); tables.forEach(table => { if (table.className !== "doxtable") { return; } let previousElement = table.previousElementSibling; while (previousElement && previousElement.tagName !== "H2") { previousElement = previousElement.previousElementSibling; } if (previousElement && previousElement.textContent) { const sectionId = previousElement.textContent.trim().toLowerCase(); const newRow = document.createElement("tr"); const newCell = document.createElement("td"); newCell.setAttribute("colspan", "3"); const newCode = document.createElement("code"); newCode.className = "open-button"; newCode.setAttribute("onclick", `window.open('https://${document.getElementById('host-authority').value}/config/#${sectionId}', '_blank')`); newCode.textContent = "Open"; newCell.appendChild(newCode); newRow.appendChild(newCell); // get the table body const tbody = table.querySelector("tbody"); // Insert at the beginning of the table tbody.insertBefore(newRow, tbody.firstChild); } }); }); ================================================ FILE: docs/configuration.md ================================================ # Configuration @admonition{ Host authority | @htmlonly By providing the host authority (URI + port), you can easily open each configuration option in the config UI.
Host authority: @endhtmlonly } Sunshine will work with the default settings for most users. In some cases you may want to configure Sunshine further. The default location for the configuration file is listed below. You can use another location if you choose, by passing in the full configuration file path as the first argument when you start Sunshine. **Example** ```bash sunshine ~/sunshine_config.conf ``` The default location of the `apps.json` is the same as the configuration file. You can use a custom location by modifying the configuration file. **Default Config Directory** | OS | Location | |---------|-------------------------------------------------| | Docker | @code{}/config@endcode | | Linux | @code{}~/.config/sunshine@endcode | | macOS | @code{}~/.config/sunshine@endcode | | Windows | @code{}%ProgramFiles%\\Sunshine\\config@endcode | Although it is recommended to use the configuration UI, it is possible manually configure Sunshine by editing the `conf` file in a text editor. Use the examples as reference. ## General ### locale
Description The locale used for Sunshine's user interface.
Default @code{} en @endcode
Example @code{} locale = en @endcode
Choices bg Bulgarian
cs Czech
de German
en English
en_GB English (UK)
en_US English (United States)
es Spanish
fr French
it Italian
ja Japanese
ko Korean
pl Polish
pt Portuguese
pt_BR Portuguese (Brazilian)
ru Russian
sv Swedish
tr Turkish
uk Ukranian
zh Chinese (Simplified)
zh_TW Chinese (Traditional)
### sunshine_name
Description The name displayed by Moonlight.
Default PC hostname
Example @code{} sunshine_name = Sunshine @endcode
### min_log_level
Description The minimum log level printed to standard out.
Default @code{} info @endcode
Example @code{} min_log_level = info @endcode
Choices verbose All logging message. @attention{This may negatively affect streaming performance.}
debug Debug log messages and higher. @attention{This may negatively affect streaming performance.}
info Informational log messages and higher.
warning Warning log messages and higher.
error Error log messages and higher.
fatal Only fatal log messages.
none No log messages.
### global_prep_cmd
Description A list of commands to be run before/after all applications. If any of the prep-commands fail, starting the application is aborted.
Default @code{} [] @endcode
Example @code{} global_prep_cmd = [{"do":"nircmd.exe setdisplay 1280 720 32 144","elevated":true,"undo":"nircmd.exe setdisplay 2560 1440 32 144"}] @endcode
### notify_pre_releases
Description Whether to be notified of new pre-release versions of Sunshine.
Default @code{} disabled @endcode
Example @code{} notify_pre_releases = disabled @endcode
### system_tray
Description Show icon in system tray and display desktop notifications
Default @code{} enabled @endcode
Example @code{} system_tray = enabled @endcode
## Input ### controller
Description Whether to allow controller input from the client.
Default @code{} enabled @endcode
Example @code{} controller = enabled @endcode
### gamepad
Description The type of gamepad to emulate on the host.
Default @code{} auto @endcode
Example @code{} gamepad = auto @endcode
Choices ds4 DualShock 4 controller (PS4) @note{This option applies to Windows only.}
ds5 DualShock 5 controller (PS5) @note{This option applies to Linux only.}
switch Switch Pro controller @note{This option applies to Linux only.}
x360 Xbox 360 controller @note{This option applies to Windows only.}
xone Xbox One controller @note{This option applies to Linux only.}
### ds4_back_as_touchpad_click
Description Allow Select/Back inputs to also trigger DS4 touchpad click. Useful for clients looking to emulate touchpad click on Xinput devices. @hint{Only applies when gamepad is set to ds4 manually. Unused in other gamepad modes.}
Default @code{} enabled @endcode
Example @code{} ds4_back_as_touchpad_click = enabled @endcode
### motion_as_ds4
Description If a client reports that a connected gamepad has motion sensor support, emulate it on the host as a DS4 controller.

When disabled, motion sensors will not be taken into account during gamepad type selection. @hint{Only applies when gamepad is set to auto.}
Default @code{} enabled @endcode
Example @code{} motion_as_ds4 = enabled @endcode
### touchpad_as_ds4
Description If a client reports that a connected gamepad has a touchpad, emulate it on the host as a DS4 controller.

When disabled, touchpad presence will not be taken into account during gamepad type selection. @hint{Only applies when gamepad is set to auto.}
Default @code{} enabled @endcode
Example @code{} touchpad_as_ds4 = enabled @endcode
### ds5_inputtino_randomize_mac
Description Randomize the MAC-Address for the generated virtual controller. @hint{Only applies on linux for gamepads created as PS5-style controllers}
Default @code{} enabled @endcode
Example @code{} ds5_inputtino_randomize_mac = enabled @endcode
### back_button_timeout
Description If the Back/Select button is held down for the specified number of milliseconds, a Home/Guide button press is emulated. @tip{If back_button_timeout < 0, then the Home/Guide button will not be emulated.}
Default @code{} -1 @endcode
Example @code{} back_button_timeout = 2000 @endcode
### keyboard
Description Whether to allow keyboard input from the client.
Default @code{} enabled @endcode
Example @code{} keyboard = enabled @endcode
### key_repeat_delay
Description The initial delay, in milliseconds, before repeating keys. Controls how fast keys will repeat themselves.
Default @code{} 500 @endcode
Example @code{} key_repeat_delay = 500 @endcode
### key_repeat_frequency
Description How often keys repeat every second. @tip{This configurable option supports decimals.}
Default @code{} 24.9 @endcode
Example @code{} key_repeat_frequency = 24.9 @endcode
### always_send_scancodes
Description 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. @caution{Applies to Windows only.}
Default @code{} enabled @endcode
Example @code{} always_send_scancodes = enabled @endcode
### key_rightalt_to_key_win
Description 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.
Default @code{} disabled @endcode
Example @code{} key_rightalt_to_key_win = enabled @endcode
### mouse
Description Whether to allow mouse input from the client.
Default @code{} enabled @endcode
Example @code{} mouse = enabled @endcode
### high_resolution_scrolling
Description 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.
Default @code{} enabled @endcode
Example @code{} high_resolution_scrolling = enabled @endcode
### native_pen_touch
Description 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.
Default @code{} enabled @endcode
Example @code{} native_pen_touch = enabled @endcode
### keybindings
Description Sometimes it may be useful to map keybindings. Wayland won't allow clients to capture the Win Key for example. @tip{See [virtual key codes](https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes)} @hint{keybindings needs to have a multiple of two elements.} @note{This option is not available in the UI. A PR would be welcome.}
Default @code{} [ 0x10, 0xA0, 0x11, 0xA2, 0x12, 0xA4 ] @endcode
Example @code{} keybindings = [ 0x10, 0xA0, 0x11, 0xA2, 0x12, 0xA4, 0x4A, 0x4B ] @endcode
## Audio/Video ### audio_sink
Description The name of the audio sink used for audio loopback. @tip{To find the name of the audio sink follow these instructions.

**Linux + pulseaudio:**
@code{} pacmd list-sinks | grep "name:" @endcode

**Linux + pipewire:**
@code{} pactl info | grep Source # in some causes you'd need to use the `Sink` device, if `Source` doesn't work, so try: pactl info | grep Sink @endcode

**macOS:**
Sunshine can only access microphones on macOS due to system limitations. To stream system audio use [Soundflower](https://github.com/mattingalls/Soundflower) or [BlackHole](https://github.com/ExistentialAudio/BlackHole).

**Windows:**
Enter the following command in command prompt or PowerShell. @code{} %ProgramFiles%\Sunshine\tools\audio-info.exe @endcode If you have multiple audio devices with identical names, use the Device ID instead. } @attention{If you want to mute the host speakers, use [virtual_sink](#virtual_sink) instead.}
Default Sunshine will select the default audio device.
Example (Linux) @code{} audio_sink = alsa_output.pci-0000_09_00.3.analog-stereo @endcode
Example (macOS) @code{} audio_sink = BlackHole 2ch @endcode
Example (Windows) @code{} audio_sink = Speakers (High Definition Audio Device) @endcode
### virtual_sink
Description The audio device that's virtual, like Steam Streaming Speakers. This allows Sunshine to stream audio, while muting the speakers. @tip{See [audio_sink](#audio_sink)!} @tip{These are some options for virtual sound devices. * Stream Streaming Speakers (Linux, macOS, Windows) * Steam must be installed. * Enable [install_steam_audio_drivers](#install_steam_audio_drivers) or use Steam Remote Play at least once to install the drivers. * [Virtual Audio Cable](https://vb-audio.com/Cable) (macOS, Windows) }
Default n/a
Example @code{} virtual_sink = Steam Streaming Speakers @endcode
### stream_audio
Description Whether to stream audio or not. Disabling this can be useful for streaming headless displays as second monitors.
Default @code{} enabled @endcode
Example @code{} stream_audio = disabled @endcode
### install_steam_audio_drivers
Description Installs the Steam Streaming Speakers driver (if Steam is installed) to support surround sound and muting host audio. @note{This option is only supported on Windows.}
Default @code{} enabled @endcode
Example @code{} install_steam_audio_drivers = enabled @endcode
### adapter_name
Description Select the video card you want to stream. @tip{To find the appropriate values follow these instructions.

**Linux + VA-API:**
Unlike with *amdvce* and *nvenc*, it doesn't matter if video encoding is done on a different GPU. @code{} ls /dev/dri/renderD* # to find all devices capable of VAAPI # replace ``renderD129`` with the device from above to list the name and capabilities of the device vainfo --display drm --device /dev/dri/renderD129 | \ grep -E "((VAProfileH264High|VAProfileHEVCMain|VAProfileHEVCMain10).*VAEntrypointEncSlice)|Driver version" @endcode To be supported by Sunshine, it needs to have at the very minimum: `VAProfileH264High : VAEntrypointEncSlice`

**Windows:**
Enter the following command in command prompt or PowerShell. @code{} %ProgramFiles%\Sunshine\tools\dxgi-info.exe @endcode For hybrid graphics systems, DXGI reports the outputs are connected to whichever graphics adapter that the application is configured to use, so it's not a reliable indicator of how the display is physically connected. }
Default Sunshine will select the default video card.
Example (Linux) @code{} adapter_name = /dev/dri/renderD128 @endcode
Example (Windows) @code{} adapter_name = Radeon RX 580 Series @endcode
### output_name
Description Select the display number you want to stream. @tip{To find the appropriate values follow these instructions.

**Linux:**
During Sunshine startup, you should see the list of detected displays: @code{} Info: Detecting displays Info: Detected display: DVI-D-0 (id: 0) connected: false Info: Detected display: HDMI-0 (id: 1) connected: true Info: Detected display: DP-0 (id: 2) connected: true Info: Detected display: DP-1 (id: 3) connected: false Info: Detected display: DVI-D-1 (id: 4) connected: false @endcode You need to use the id value inside the parenthesis, e.g. `1`.

**macOS:**
During Sunshine startup, you should see the list of detected displays: @code{} Info: Detecting displays Info: Detected display: Monitor-0 (id: 3) connected: true Info: Detected display: Monitor-1 (id: 2) connected: true @endcode You need to use the id value inside the parenthesis, e.g. `3`.

**Windows:**
During Sunshine startup, you should see the list of detected displays: @code{} Info: Currently available display devices: [ { "device_id": "{64243705-4020-5895-b923-adc862c3457e}", "display_name": "", "friendly_name": "IDD HDR", "info": null }, { "device_id": "{77f67f3e-754f-5d31-af64-ee037e18100a}", "display_name": "", "friendly_name": "SunshineHDR", "info": null }, { "device_id": "{daeac860-f4db-5208-b1f5-cf59444fb768}", "display_name": "\\\\.\\DISPLAY1", "friendly_name": "ROG PG279Q", "info": { "hdr_state": null, "origin_point": { "x": 0, "y": 0 }, "primary": true, "refresh_rate": { "type": "rational", "value": { "denominator": 1000, "numerator": 119998 } }, "resolution": { "height": 1440, "width": 2560 }, "resolution_scale": { "type": "rational", "value": { "denominator": 100, "numerator": 100 } } } } ] @endcode You need to use the `device_id` value. }
Default Sunshine will select the default display.
Example (Linux) @code{} output_name = 0 @endcode
Example (macOS) @code{} output_name = 3 @endcode
Example (Windows) @code{} output_name = {daeac860-f4db-5208-b1f5-cf59444fb768} @endcode
### isolated_virtual_display_option
Description Isolates the virtual display. @note{Applies to Windows only.}
Default @code{}disabled@endcode
enabled Change the position of the virtual display (and other displays if there is a hole)
### dd_configuration_option
Description Perform mandatory verification and additional configuration for the display device. @note{Applies to Windows only.}
Default @code{} disabled @endcode
Example @code{} dd_configuration_option = ensure_only_display @endcode
Choices disabled Perform no additional configuration (disables all `dd_` configuration options).
verify_only Verify that display is active only (this is a mandatory step without any extra steps to verify display state).
ensure_active Activate the display if it's currently inactive.
ensure_primary Activate the display if it's currently inactive and make it primary.
ensure_only_display Activate the display if it's currently inactive and disable all others.
### dd_resolution_option
Description Perform additional resolution configuration for the display device. @note{"Optimize game settings" must be enabled in Moonlight for this option to work.} @note{Applies to Windows only.}
Default @code{}auto@endcode
Example @code{} dd_resolution_option = manual @endcode
Choices disabled Perform no additional configuration.
auto Change resolution to the requested resolution from the client.
manual Change resolution to the user specified one (set via [dd_manual_resolution](#dd_manual_resolution)).
### dd_manual_resolution
Description Specify manual resolution to be used. @note{[dd_resolution_option](#dd_resolution_option) must be set to `manual`} @note{Applies to Windows only.}
Default n/a
Example @code{} dd_manual_resolution = 1920x1080 @endcode
### dd_refresh_rate_option
Description Perform additional refresh rate configuration for the display device. @note{Applies to Windows only.}
Default @code{}auto@endcode
Example @code{} dd_refresh_rate_option = manual @endcode
Choices disabled Perform no additional configuration.
auto Change refresh rate to the requested FPS value from the client.
manual Change refresh rate to the user specified one (set via [dd_manual_refresh_rate](#dd_manual_refresh_rate)).
### dd_manual_refresh_rate
Description Specify manual refresh rate to be used. @note{[dd_refresh_rate_option](#dd_refresh_rate_option) must be set to `manual`} @note{Applies to Windows only.}
Default n/a
Example @code{} dd_manual_resolution = 120 dd_manual_resolution = 59.95 @endcode
### dd_hdr_option
Description Perform additional HDR configuration for the display device. @note{Applies to Windows only.}
Default @code{}auto@endcode
Example @code{} dd_hdr_option = disabled @endcode
Choices disabled Perform no additional configuration.
auto Change HDR to the requested state from the client if the display supports it.
### dd_wa_hdr_toggle_delay
Description 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.
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.
DO NOT use this workaround unless you actually have issues with HDR as it directly impacts stream start time! @note{This option works independently of [dd_hdr_option](#dd_hdr_option)} @note{Applies to Windows only.}
Default @code{} 0 @endcode
Example @code{} dd_wa_hdr_toggle_delay = 500 @endcode
### dd_config_revert_delay
Description Additional delay in milliseconds to wait before reverting configuration when the app has been closed or the last session terminated. Main purpose is to provide a smoother transition when quickly switching between apps. @note{Applies to Windows only.}
Default @code{}3000@endcode
Example @code{} dd_config_revert_delay = 1500 @endcode
### dd_config_revert_on_disconnect
Description When enabled, display configuration is reverted upon disconnect of all clients instead of app close or last session termination. This can be useful for returning to physical usage of the host machine without closing the active app. @warning{Some applications may not function properly when display configuration is changed while active.} @note{Applies to Windows only.}
Default @code{}disabled@endcode
Example @code{} dd_config_revert_on_disconnect = enabled @endcode
### dd_mode_remapping
Description Remap the requested resolution and FPS to another display mode.
Depending on the [dd_resolution_option](#dd_resolution_option) and [dd_refresh_rate_option](#dd_refresh_rate_option) values, the following mapping groups are available:
  • `mixed` - both options are set to `auto`.
  • `resolution_only` - only [dd_resolution_option](#dd_resolution_option) is set to `auto`.
  • `refresh_rate_only` - only [dd_refresh_rate_option](#dd_refresh_rate_option) is set to `auto`.
For each of those groups, a list of fields can be configured to perform remapping:
  • `requested_resolution` - resolution that needs to be matched in order to use this remapping entry.
  • `requested_fps` - FPS that needs to be matched in order to use this remapping entry.
  • `final_resolution` - resolution value to be used if the entry was matched.
  • `final_refresh_rate` - refresh rate value to be used if the entry was matched.
If `requested_*` field is left empty, it will match everything.
If `final_*` field is left empty, the original value will not be remapped and either a requested, manual or current value is used. However, at least one `final_*` must be set, otherwise the entry is considered invalid.
@note{"Optimize game settings" must be enabled on client side for ANY entry with `resolution` field to be considered.} @note{First entry to be matched in the list is the one that will be used.} @tip{`requested_resolution` and `final_resolution` can be omitted for `refresh_rate_only` group.} @tip{`requested_fps` and `final_refresh_rate` can be omitted for `resolution_only` group.} @note{Applies to Windows only.}
Default @code{} dd_mode_remapping = { "mixed": [], "resolution_only": [], "refresh_rate_only": [] } @endcode
Example @code{} dd_mode_remapping = { "mixed": [ { "requested_fps": "60", "final_refresh_rate": "119.95", "requested_resolution": "1920x1080", "final_resolution": "2560x1440" }, { "requested_fps": "60", "final_refresh_rate": "120", "requested_resolution": "", "final_resolution": "" } ], "resolution_only": [ { "requested_resolution": "1920x1080", "final_resolution": "2560x1440" } ], "refresh_rate_only": [ { "requested_fps": "60", "final_refresh_rate": "119.95" } ] }@endcode
### max_bitrate
Description 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.
Default @code{} 0 @endcode
Example @code{} max_bitrate = 5000 @endcode
### minimum_fps_target
Description Sunshine tries to save bandwidth when content on screen is static or a low framerate. Because many clients expect a constant stream of video frames, a certain amount of duplicate frames are sent when this happens. This setting controls the lowest effective framerate a stream can reach.
Default @code{} 0 @endcode
Choices 0 Use half the stream's FPS as the minimum target.
1-1000 Specify your own value. The real minimum may differ from this value.
## Network ### upnp
Description Sunshine will attempt to open ports for streaming over the internet.
Default @code{} disabled @endcode
Example @code{} upnp = enabled @endcode
### address_family
Description Set the address family that Sunshine will use.
Default @code{} ipv4 @endcode
Example @code{} address_family = both @endcode
Choices ipv4 IPv4 only
both IPv4+IPv6
### port
Description Set the family of ports used by Sunshine. Changing this value will offset other ports as shown in config UI.
Default @code{} 47989 @endcode
Range 1029-65514
Example @code{} port = 47989 @endcode
### origin_web_ui_allowed
Description The origin of the remote endpoint address that is not denied for HTTPS Web UI.
Default @code{} lan @endcode
Example @code{} origin_web_ui_allowed = lan @endcode
Choices pc Only localhost may access the web ui
lan Only LAN devices may access the web ui
wan Anyone may access the web ui
### external_ip
Description If no external IP address is given, Sunshine will attempt to automatically detect external ip-address.
Default Automatic
Example @code{} external_ip = 123.456.789.12 @endcode
### lan_encryption_mode
Description This determines when encryption will be used when streaming over your local network. @warning{Encryption can reduce streaming performance, particularly on less powerful hosts and clients.}
Default @code{} 0 @endcode
Example @code{} lan_encryption_mode = 0 @endcode
Choices 0 encryption will not be used
1 encryption will be used if the client supports it
2 encryption is mandatory and unencrypted connections are rejected
### wan_encryption_mode
Description This determines when encryption will be used when streaming over the Internet. @warning{Encryption can reduce streaming performance, particularly on less powerful hosts and clients.}
Default @code{} 1 @endcode
Example @code{} wan_encryption_mode = 1 @endcode
Choices 0 encryption will not be used
1 encryption will be used if the client supports it
2 encryption is mandatory and unencrypted connections are rejected
### ping_timeout
Description How long to wait, in milliseconds, for data from Moonlight before shutting down the stream.
Default @code{} 10000 @endcode
Example @code{} ping_timeout = 10000 @endcode
## Config Files ### file_apps
Description The application configuration file path. The file contains a JSON formatted list of applications that can be started by Moonlight.
Default @code{} apps.json @endcode
Example @code{} file_apps = apps.json @endcode
### credentials_file
Description The file where user credentials for the UI are stored.
Default @code{} sunshine_state.json @endcode
Example @code{} credentials_file = sunshine_state.json @endcode
### log_path
Description The path where the Sunshine log is stored.
Default @code{} sunshine.log @endcode
Example @code{} log_path = sunshine.log @endcode
### pkey
Description The private key used for the web UI and Moonlight client pairing. For best compatibility, this should be an RSA-2048 private key. @warning{Not all Moonlight clients support ECDSA keys or RSA key lengths other than 2048 bits.}
Default @code{} credentials/cakey.pem @endcode
Example @code{} pkey = /dir/pkey.pem @endcode
### cert
Description The certificate used for the web UI and Moonlight client pairing. For best compatibility, this should have an RSA-2048 public key. @warning{Not all Moonlight clients support ECDSA keys or RSA key lengths other than 2048 bits.}
Default @code{} credentials/cacert.pem @endcode
Example @code{} cert = /dir/cert.pem @endcode
### file_state
Description The file where current state of Sunshine is stored.
Default @code{} sunshine_state.json @endcode
Example @code{} file_state = sunshine_state.json @endcode
## Advanced ### fec_percentage
Description Percentage of error correcting packets per data packet in each video frame. @warning{Higher values can correct for more network packet loss, but at the cost of increasing bandwidth usage.}
Default @code{} 20 @endcode
Range 1-255
Example @code{} fec_percentage = 20 @endcode
### qp
Description Quantization Parameter. Some devices don't support Constant Bit Rate. For those devices, QP is used instead. @warning{Higher value means more compression, but less quality.}
Default @code{} 28 @endcode
Example @code{} qp = 28 @endcode
### min_threads
Description Minimum number of CPU threads used for encoding. @note{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.}
Default @code{} 2 @endcode
Example @code{} min_threads = 2 @endcode
### hevc_mode
Description Allows the client to request HEVC Main or HEVC Main10 video streams. @warning{HEVC is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.}
Default @code{} 0 @endcode
Example @code{} hevc_mode = 2 @endcode
Choices 0 advertise support for HEVC based on encoder capabilities (recommended)
1 do not advertise support for HEVC
2 advertise support for HEVC Main profile
3 advertise support for HEVC Main and Main10 (HDR) profiles
### av1_mode
Description Allows the client to request AV1 Main 8-bit or 10-bit video streams. @warning{AV1 is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.}
Default @code{} 0 @endcode
Example @code{} av1_mode = 2 @endcode
Choices 0 advertise support for AV1 based on encoder capabilities (recommended)
1 do not advertise support for AV1
2 advertise support for AV1 Main 8-bit profile
3 advertise support for AV1 Main 8-bit and 10-bit (HDR) profiles
### capture
Description Force specific screen capture method.
Default Automatic. Sunshine will use the first capture method available in the order of the table above.
Example @code{} capture = kms @endcode
Choices nvfbc Use NVIDIA Frame Buffer Capture to capture direct to GPU memory. This is usually the fastest method for NVIDIA cards. NvFBC does not have native Wayland support and does not work with XWayland. @note{Applies to Linux only.}
wlr Capture for wlroots based Wayland compositors via wlr-screencopy-unstable-v1. It is possible to capture virtual displays in e.g. Hyprland using this method. @note{Applies to Linux only.}
kms DRM/KMS screen capture from the kernel. This requires that Sunshine has `cap_sys_admin` capability. @note{Applies to Linux only.}
x11 Uses XCB. This is the slowest and most CPU intensive so should be avoided if possible. @note{Applies to Linux only.}
ddx Use DirectX Desktop Duplication API to capture the display. This is well-supported on Windows machines. @note{Applies to Windows only.}
wgc (beta feature) Use Windows.Graphics.Capture to capture the display. @note{Applies to Windows only.} @attention{This capture method is not compatible with the Sunshine service.}
### encoder
Description Force a specific encoder.
Default Sunshine will use the first encoder that is available.
Example @code{} encoder = nvenc @endcode
Choices nvenc For NVIDIA graphics cards
quicksync For Intel graphics cards
amdvce For AMD graphics cards
vaapi Use Linux VA-API (AMD, Intel)
software Encoding occurs on the CPU
## NVIDIA NVENC Encoder ### nvenc_preset
Description NVENC encoder performance preset. 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. @note{This option only applies when using NVENC [encoder](#encoder).}
Default @code{} 1 @endcode
Example @code{} nvenc_preset = 1 @endcode
Choices 1 P1 (fastest)
2 P2
3 P3
4 P4
5 P5
6 P6
7 P7 (slowest)
### nvenc_twopass
Description Enable two-pass mode in NVENC encoder. 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. @note{This option only applies when using NVENC [encoder](#encoder).}
Default @code{} quarter_res @endcode
Example @code{} nvenc_twopass = quarter_res @endcode
Choices disabled One pass (fastest)
quarter_res Two passes, first pass at quarter resolution (faster)
full_res Two passes, first pass at full resolution (slower)
### nvenc_spatial_aq
Description Assign higher QP values to flat regions of the video. Recommended to enable when streaming at lower bitrates. @note{This option only applies when using NVENC [encoder](#encoder).} @warning{Enabling this option may reduce performance.}
Default @code{} disabled @endcode
Example @code{} nvenc_spatial_aq = disabled @endcode
### nvenc_vbv_increase
Description Single-frame VBV/HRD percentage increase. 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. @note{This option only applies when using NVENC [encoder](#encoder).} @warning{Can lead to network packet loss.}
Default @code{} 0 @endcode
Range 0-400
Example @code{} nvenc_vbv_increase = 0 @endcode
### nvenc_realtime_hags
Description Use realtime gpu scheduling priority in NVENC when hardware accelerated gpu scheduling (HAGS) is enabled in Windows. 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. @note{This option only applies when using NVENC [encoder](#encoder).} @note{Applies to Windows only.}
Default @code{} enabled @endcode
Example @code{} nvenc_realtime_hags = enabled @endcode
### nvenc_latency_over_power
Description Adaptive P-State algorithm which NVIDIA drivers employ doesn't work well with low latency streaming, so Sunshine requests high power mode explicitly. @note{This option only applies when using NVENC [encoder](#encoder).} @warning{Disabling this is not recommended since this can lead to significantly increased encoding latency.} @note{Applies to Windows only.}
Default @code{} enabled @endcode
Example @code{} nvenc_latency_over_power = enabled @endcode
### nvenc_opengl_vulkan_on_dxgi
Description Sunshine can't capture fullscreen OpenGL and Vulkan programs at full frame rate unless they present on top of DXGI. With this option enabled Sunshine changes global Vulkan/OpenGL present method to "Prefer layered on DXGI Swapchain". This is system-wide setting that is reverted on Sunshine program exit. @note{This option only applies when using NVENC [encoder](#encoder).} @note{Applies to Windows only.}
Default @code{} enabled @endcode
Example @code{} nvenc_opengl_vulkan_on_dxgi = enabled @endcode
### nvenc_h264_cavlc
Description Prefer CAVLC entropy coding over CABAC in H.264 when using NVENC. CAVLC is outdated and needs around 10% more bitrate for same quality, but provides slightly faster decoding when using software decoder. @note{This option only applies when using H.264 format with the NVENC [encoder](#encoder).}
Default @code{} disabled @endcode
Example @code{} nvenc_h264_cavlc = disabled @endcode
## Intel QuickSync Encoder ### qsv_preset
Description The encoder preset to use. @note{This option only applies when using quicksync [encoder](#encoder).}
Default @code{} medium @endcode
Example @code{} qsv_preset = medium @endcode
Choices veryfast fastest (lowest quality)
faster faster (lower quality)
fast fast (low quality)
medium medium (default)
slow slow (good quality)
slower slower (better quality)
veryslow slowest (best quality)
### qsv_coder
Description The entropy encoding to use. @note{This option only applies when using H.264 with the quicksync [encoder](#encoder).}
Default @code{} auto @endcode
Example @code{} qsv_coder = auto @endcode
Choices auto let ffmpeg decide
cabac context adaptive binary arithmetic coding - higher quality
cavlc context adaptive variable-length coding - faster decode
### qsv_slow_hevc
Description This options enables use of HEVC on older Intel GPUs that only support low power encoding for H.264. @note{This option only applies when using quicksync [encoder](#encoder).} @caution{Streaming performance may be significantly reduced when this option is enabled.}
Default @code{} disabled @endcode
Example @code{} qsv_slow_hevc = disabled @endcode
## AMD AMF Encoder ### amd_usage
Description The encoder usage profile is used to set the base set of encoding parameters. @note{This option only applies when using amdvce [encoder](#encoder).} @note{The other AMF options that follow will override a subset of the settings applied by your usage profile, but there are hidden parameters set in usage profiles that cannot be overridden elsewhere.}
Default @code{} ultralowlatency @endcode
Example @code{} amd_usage = ultralowlatency @endcode
Choices transcoding transcoding (slowest)
webcam webcam (slow)
lowlatency_high_quality low latency, high quality (fast)
lowlatency low latency (faster)
ultralowlatency ultra low latency (fastest)
### amd_rc
Description The encoder rate control. @note{This option only applies when using amdvce [encoder](#encoder).} @warning{The `vbr_latency` option generally works best, but some bitrate overshoots may still occur. Enabling HRD allows all bitrate based rate controls to better constrain peak bitrate, but may result in encoding artifacts depending on your card.}
Default @code{} vbr_latency @endcode
Example @code{} amd_rc = vbr_latency @endcode
Choices cqp constant qp mode
cbr constant bitrate
vbr_latency variable bitrate, latency constrained
vbr_peak variable bitrate, peak constrained
### amd_enforce_hrd
Description Enable Hypothetical Reference Decoder (HRD) enforcement to help constrain the target bitrate. @note{This option only applies when using amdvce [encoder](#encoder).} @warning{HRD is known to cause encoding artifacts or negatively affect encoding quality on certain cards.}
Default @code{} disabled @endcode
Example @code{} amd_enforce_hrd = disabled @endcode
### amd_quality
Description The quality profile controls the tradeoff between speed and quality of encoding. @note{This option only applies when using amdvce [encoder](#encoder).}
Default @code{} balanced @endcode
Example @code{} amd_quality = balanced @endcode
Choices speed prefer speed
balanced balanced
quality prefer quality
### amd_preanalysis
Description Preanalysis can increase encoding quality at the cost of latency. @note{This option only applies when using amdvce [encoder](#encoder).}
Default @code{} disabled @endcode
Example @code{} amd_preanalysis = disabled @endcode
### amd_vbaq
Description Variance Based Adaptive Quantization (VBAQ) can increase subjective visual quality by prioritizing allocation of more bits to smooth areas compared to more textured areas. @note{This option only applies when using amdvce [encoder](#encoder).}
Default @code{} enabled @endcode
Example @code{} amd_vbaq = enabled @endcode
### amd_coder
Description The entropy encoding to use. @note{This option only applies when using H.264 with the amdvce [encoder](#encoder).}
Default @code{} auto @endcode
Example @code{} amd_coder = auto @endcode
Choices auto let ffmpeg decide
cabac context adaptive binary arithmetic coding - faster decode
cavlc context adaptive variable-length coding - higher quality
## VideoToolbox Encoder ### vt_coder
Description The entropy encoding to use. @note{This option only applies when using macOS.}
Default @code{} auto @endcode
Example @code{} vt_coder = auto @endcode
Choices auto let ffmpeg decide
cabac context adaptive binary arithmetic coding - faster decode
cavlc context adaptive variable-length coding - higher quality
### vt_software
Description Force Video Toolbox to use software encoding. @note{This option only applies when using macOS.}
Default @code{} auto @endcode
Example @code{} vt_software = auto @endcode
Choices auto let ffmpeg decide
disabled disable software encoding
allowed allow software encoding
forced force software encoding
### vt_realtime
Description Realtime encoding. @note{This option only applies when using macOS.} @warning{Disabling realtime encoding might result in a delayed frame encoding or frame drop.}
Default @code{} enabled @endcode
Example @code{} vt_realtime = enabled @endcode
## VA-API Encoder ### vaapi_strict_rc_buffer
Description Enabling this option can avoid dropped frames over the network during scene changes, but video quality may be reduced during motion. @note{This option only applies for H.264 and HEVC when using VA-API [encoder](#encoder) on AMD GPUs.}
Default @code{} disabled @endcode
Example @code{} vaapi_strict_rc_buffer = enabled @endcode
## Software Encoder ### sw_preset
Description The encoder preset to use. @note{This option only applies when using software [encoder](#encoder).} @note{From [FFmpeg](https://trac.ffmpeg.org/wiki/Encode/H.264#preset).

A preset is a collection of options that will provide a certain encoding speed to compression ratio. A slower preset will provide better compression (compression is quality per filesize). This means that, for example, if you target a certain file size or constant bit rate, you will achieve better quality with a slower preset. Similarly, for constant quality encoding, you will simply save bitrate by choosing a slower preset.

Use the slowest preset that you have patience for.}
Default @code{} superfast @endcode
Example @code{} sw_preset = superfast @endcode
Choices ultrafast fastest
superfast
veryfast
faster
fast
medium
slow
slower
veryslow slowest
### sw_tune
Description The tuning preset to use. @note{This option only applies when using software [encoder](#encoder).} @note{From [FFmpeg](https://trac.ffmpeg.org/wiki/Encode/H.264#preset).

You can optionally use -tune to change settings based upon the specifics of your input. }
Default @code{} zerolatency @endcode
Example @code{} sw_tune = zerolatency @endcode
Choices film use for high quality movie content; lowers deblocking
animation good for cartoons; uses higher deblocking and more reference frames
grain preserves the grain structure in old, grainy film material
stillimage good for slideshow-like content
fastdecode allows faster decoding by disabling certain filters
zerolatency good for fast encoding and low-latency streaming
| Previous | Next | |:------------------|--------------------------------:| | [Legal](legal.md) | [App Examples](app_examples.md) |
[TOC]
================================================ FILE: docs/contributing.md ================================================ # Contributing Read our contribution guide in our organization level [docs](https://docs.lizardbyte.dev/latest/developers/contributing.html). ## Recommended Tools | Tool | Description | |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------| |
CLion | Recommended IDE for C and C++ development. Free for non-commercial use. | ## Project Patterns ### Web UI * The Web UI uses [Vite](https://vitejs.dev) as its build system. * The HTML pages used by the Web UI are found in `./src_assets/common/assets/web`. * [EJS](https://www.npmjs.com/package/vite-plugin-ejs) is used as a templating system for the pages (check `template_header.html` and `template_header_main.html`). * The Style System is provided by [Bootstrap](https://getbootstrap.com). * The JS framework used by the more interactive pages is [Vue.js](https://vuejs.org). #### Building @tabs{ @tab{CMake | ```bash cmake -B build -G Ninja -S . --target web-ui ninja -C build web-ui ```} @tab{Manual | ```bash npm run dev ```} } ### Localization Sunshine and related LizardByte projects are being localized into various languages. The default language is `en` (English). ![](https://app.lizardbyte.dev/dashboard/crowdin/LizardByte_graph.svg) @admonition{Community | We are looking for language coordinators to help approve translations. The goal is to have the bars above filled with green! If you are interesting, please reach out to us on our Discord server.} #### CrowdIn The translations occur on [CrowdIn][crowdin-url]. Anyone is free to contribute to the localization there. ##### Translation Basics * The brand names *LizardByte* and *Sunshine* should never be translated. * Other brand names should never be translated. Examples include *AMD*, *Intel*, and *NVIDIA*. ##### CrowdIn Integration How does it work? When a change is made to Sunshine source code, a workflow generates new translation templates that get pushed to CrowdIn automatically. When translations are updated on CrowdIn, a push gets made to the *l10n_master* branch and a PR is made against the *master* branch. Once the PR is merged, all updated translations are part of the project and will be included in the next release. #### Extraction ##### Web UI Sunshine uses [Vue I18n](https://vue-i18n.intlify.dev) for localizing the UI. The following is a simple example of how to use it. * Add the string to the `./src_assets/common/assets/web/public/assets/locale/en.json` file, in English. ```json { "index": { "welcome": "Hello, Sunshine!" } } ``` > [!NOTE] > The JSON keys should be sorted alphabetically. You can use [jsonabc](https://novicelab.org/jsonabc) > to sort the keys. > [!IMPORTANT] > Due to the integration with Crowdin, it is important to only add strings to the *en.json* file, > and to not modify any other language files. After the PR is merged, the translations can take place > on [CrowdIn][crowdin-url]. Once the translations are complete, a PR will be made > to merge the translations into Sunshine. * Use the string in the Vue component. ```html ``` > [!TIP] > More formatting examples can be found in the > [Vue I18n guide](https://kazupon.github.io/vue-i18n/guide/formatting.html). ##### C++ There should be minimal cases where strings need to be extracted from C++ source code; however it may be necessary in some situations. For example the system tray icon could be localized as it is user interfacing. * Wrap the string to be extracted in a function as shown. ```cpp #include #include std::string msg = boost::locale::translate("Hello world!"); ``` > [!TIP] > More examples can be found in the documentation for > [boost locale](https://www.boost.org/doc/libs/1_70_0/libs/locale/doc/html/messages_formatting.html). > [!WARNING] > The below is for information only. Contributors should never include manually updated template files, or > manually compiled language files in Pull Requests. Strings are automatically extracted from the code to the `locale/sunshine.po` template file. The generated file is used by CrowdIn to generate language specific template files. The file is generated using the `.github/workflows/localize.yml` workflow and is run on any push event into the `master` branch. Jobs are only run if any of the following paths are modified. ```yaml - 'src/**' ``` When testing locally it may be desirable to manually extract, initialize, update, and compile strings. Python is required for this, along with the python dependencies in the `./scripts/requirements.txt` file. Additionally, [xgettext](https://www.gnu.org/software/gettext) must be installed. * Extract, initialize, and update ```bash python ./scripts/_locale.py --extract --init --update ``` * Compile ```bash python ./scripts/_locale.py --compile ``` > [!IMPORTANT] > Due to the integration with CrowdIn, it is important to not include any extracted or compiled files in > Pull Requests. The files are automatically generated and updated by the workflow. Once the PR is merged, the > translations can take place on [CrowdIn][crowdin-url]. Once the translations are > complete, a PR will be made to merge the translations into Sunshine. ### Testing #### Clang Format Source code is tested against the `.clang-format` file for linting errors. The workflow file responsible for clang format testing is `.github/workflows/cpp-clang-format-lint.yml`. Option 1: ```bash find ./ -iname *.cpp -o -iname *.h -iname *.m -iname *.mm | xargs clang-format -i ``` Option 2 (will modify files): ```bash python ./scripts/update_clang_format.py ``` #### Unit Testing Sunshine uses [Google Test](https://github.com/google/googletest) for unit testing. Google Test is included in the repo as a submodule. The test sources are located in the `./tests` directory. The tests need to be compiled into an executable, and then run. The tests are built using the normal build process, but can be disabled by setting the `BUILD_TESTS` CMake option to `OFF`. To run the tests, execute the following command. ```bash ./build/tests/test_sunshine ``` To see all available options, run the tests with the `--help` flag. ```bash ./build/tests/test_sunshine --help ``` > [!TIP] > See the googletest [FAQ](https://google.github.io/googletest/faq.html) for more information on how to use Google Test. We use [gcovr](https://www.gcovr.com) to generate code coverage reports, and [Codecov](https://about.codecov.io) to analyze the reports for all PRs and commits. Codecov will fail a PR if the total coverage is reduced too much, or if not enough of the diff is covered by tests. In some cases, the code cannot be covered when running the tests inside of GitHub runners. For example, any test that needs access to the GPU will not be able to run. In these cases, the coverage can be omitted by adding comments to the code. See the [gcovr documentation](https://gcovr.com/en/stable/guide/exclusion-markers.html#exclusion-markers) for more information. Even if your changes cannot be covered in the CI, we still encourage you to write the tests for them. This will allow maintainers to run the tests locally. [crowdin-url]: https://translate.lizardbyte.dev
| Previous | Next | |:------------------------|-------------------------------------------------------------:| | [Building](building.md) | [Source Code](../third-party/doxyconfig/docs/source_code.md) |
[TOC]
================================================ FILE: docs/doc-styles.css ================================================ /* A fake button as doxygen doesn't allow button elements */ .open-button { background: var(--primary-color); color: white; cursor: pointer; } ================================================ FILE: docs/gamestream_migration.md ================================================ # GameStream Migration Nvidia announced that their GameStream service for Nvidia Games clients will be discontinued in February 2023. Luckily, Sunshine performance is now equal to or better than Nvidia GameStream. ## Migration We have developed a simple migration tool to help you migrate your GameStream games and apps to Sunshine automatically. Please check out our [GSMS](https://github.com/LizardByte/GSMS) project if you're interested in an automated migration option. GSMS offers the ability to migrate your custom and auto-detected games and apps. The working directory, command, and image are all set in Sunshine's `apps.json` file. The box-art image is also copied to a specified directory. ## Internet Streaming If you are using the Moonlight Internet Hosting Tool, you can remove it from your system when you migrate to Sunshine. To stream over the Internet with Sunshine and a UPnP-capable router, enable the UPnP option in the Sunshine Web UI. > [!NOTE] > Running Sunshine together with versions of the Moonlight Internet Hosting Tool prior to v5.6 will cause UPnP > port forwarding to become unreliable. Either uninstall the tool entirely or update it to v5.6 or later. ## Limitations Sunshine does have some limitations, as compared to Nvidia GameStream. * Automatic game/application list. * Changing game settings automatically to optimize streaming.
| Previous | Next | |:------------------------------------------------|------------------:| | [Third-party Packages](third_party_packages.md) | [Legal](legal.md) |
[TOC]
================================================ FILE: docs/getting_started.md ================================================ # Getting Started The recommended method for running Sunshine is to use the [binaries](#binaries) included in the [latest release][latest-release], unless otherwise specified. [Pre-releases](https://github.com/LizardByte/Sunshine/releases) are also available. These should be considered beta, and release artifacts may be missing when merging changes on a faster cadence. ## Binaries Binaries of Sunshine are created for each release. They are available for Linux, macOS, and Windows. Binaries can be found in the [latest release][latest-release]. > [!NOTE] > Some third party packages also exist. > See [Third Party Packages](third_party_packages.md) for more information. > No support will be provided for third party packages! ## Install ### Docker > [!WARNING] > The Docker images are not recommended for most users. Docker images are available on [Dockerhub.io](https://hub.docker.com/repository/docker/lizardbyte/sunshine) and [ghcr.io](https://github.com/orgs/LizardByte/packages?repo_name=sunshine). See [Docker](../DOCKER_README.md) for more information. ### Linux **CUDA Compatibility** CUDA is used for NVFBC capture. > [!NOTE] > See [CUDA GPUS](https://developer.nvidia.com/cuda-gpus) to cross-reference Compute Capability to your GPU. > The table below applies to packages provided by LizardByte. If you use an official LizardByte package, then you do not > need to install CUDA.
CUDA Compatibility
CUDA Version Min Driver CUDA Compute Capabilities Package
12.9.1 575.57.08 50;52;60;61;62;70;72;75;80;86;87;89;90;100;101;103;120;121 sunshine.AppImage
sunshine-ubuntu-22.04-{arch}.deb
sunshine-ubuntu-24.04-{arch}.deb
sunshine-debian-trixie-{arch}.deb
sunshine_{arch}.flatpak
Sunshine (copr - Fedora 41)
Sunshine (copr - Fedora 42)
sunshine.pkg.tar.zst
#### AppImage > [!CAUTION] > Use distro-specific packages instead of the AppImage if they are available. According to AppImageLint the supported distro matrix of the AppImage is below. - ✖ Debian bullseye - ✔ Debian bookworm - ✔ Debian trixie - ✔ Debian sid - ✔ Ubuntu plucky - ✔ Ubuntu noble - ✔ Ubuntu jammy - ✖ Ubuntu focal - ✖ Ubuntu bionic - ✖ Ubuntu xenial - ✖ Ubuntu trusty - ✖ Rocky Linux 8 - ✖ Rocky Linux 9 ##### Install 1. Download [sunshine.AppImage](https://github.com/LizardByte/Sunshine/releases/latest/download/sunshine.AppImage) into your home directory. ```bash cd ~ wget https://github.com/LizardByte/Sunshine/releases/latest/download/sunshine.AppImage ``` 2. Open terminal and run the following command. ```bash ./sunshine.AppImage --install ``` ##### Run ```bash ./sunshine.AppImage --install && ./sunshine.AppImage ``` ##### Uninstall ```bash ./sunshine.AppImage --remove ``` #### ArchLinux > [!CAUTION] > Use AUR packages at your own risk. ##### Install Prebuilt Packages Follow the instructions at LizardByte's [pacman-repo](https://github.com/LizardByte/pacman-repo) to add the repository. Then run the following command. ```bash pacman -S sunshine ``` ##### Install PKGBUILD Archive Open terminal and run the following command. ```bash wget https://github.com/LizardByte/Sunshine/releases/latest/download/sunshine.pkg.tar.gz tar -xvf sunshine.pkg.tar.gz cd sunshine # install optional dependencies pacman -S cuda # Nvidia GPU encoding support pacman -S libva-mesa-driver # AMD GPU encoding support makepkg -si ``` ##### Uninstall ```bash pacman -R sunshine ``` #### Debian/Ubuntu ##### Install Download `sunshine-{distro}-{distro-version}-{arch}.deb` and run the following command. ```bash sudo dpkg -i ./sunshine-{distro}-{distro-version}-{arch}.deb ``` > [!NOTE] > The `{distro-version}` is the version of the distro we built the package on. The `{arch}` is the > architecture of your operating system. > [!TIP] > You can double-click the deb file to see details about the package and begin installation. ##### Uninstall ```bash sudo apt remove sunshine ``` #### Fedora > [!TIP] > The package name is case-sensitive. ##### Install 1. Enable copr repository. ```bash sudo dnf copr enable lizardbyte/stable ``` or ```bash sudo dnf copr enable lizardbyte/beta ``` 2. Install the package. ```bash sudo dnf install Sunshine ``` ##### Uninstall ```bash sudo dnf remove Sunshine ``` #### Flatpak > [!CAUTION] > Use distro-specific packages instead of the Flatpak if they are available. Using this package requires that you have [Flatpak](https://flatpak.org/setup) installed. ##### Download (local option) 1. Download `sunshine_{arch}.flatpak` and run the following command. > [!NOTE] > Replace `{arch}` with your system architecture. ##### Install (system level) **Flathub** ```bash flatpak install --system flathub dev.lizardbyte.app.Sunshine ``` **Local** ```bash flatpak install --system ./sunshine_{arch}.flatpak ``` ##### Install (user level) **Flathub** ```bash flatpak install --user flathub dev.lizardbyte.app.Sunshine ``` **Local** ```bash flatpak install --user ./sunshine_{arch}.flatpak ``` ##### Additional installation (required) ```bash flatpak run --command=additional-install.sh dev.lizardbyte.app.Sunshine ``` ##### Run with NVFBC capture (X11 Only) ```bash flatpak run dev.lizardbyte.app.Sunshine ``` ##### Run with KMS capture (Wayland & X11) ```bash sudo -i PULSE_SERVER=unix:/run/user/$(id -u $whoami)/pulse/native flatpak run dev.lizardbyte.app.Sunshine ``` ##### Uninstall ```bash flatpak run --command=remove-additional-install.sh dev.lizardbyte.app.Sunshine flatpak uninstall --delete-data dev.lizardbyte.app.Sunshine ``` #### Homebrew > [!IMPORTANT] > The Homebrew package is experimental on Linux. This package requires that you have [Homebrew](https://docs.brew.sh/Installation) installed. ##### Install ```bash brew update brew upgrade brew tap LizardByte/homebrew brew install sunshine ``` ##### Uninstall ```bash brew uninstall sunshine ``` ### macOS > [!IMPORTANT] > Sunshine on macOS is experimental. Gamepads do not work. #### Homebrew This package requires that you have [Homebrew](https://docs.brew.sh/Installation) installed. ##### Install ```bash brew tap LizardByte/homebrew brew install sunshine ``` ##### Uninstall ```bash brew uninstall sunshine ``` > [!TIP] > For beta you can replace `sunshine` with `sunshine-beta` in the above commands. ### Windows #### Installer (recommended) 1. Download and install [Sunshine-Windows-AMD64-installer.exe](https://github.com/LizardByte/Sunshine/releases/latest/download/Sunshine-Windows-AMD64-installer.exe) > [!CAUTION] > You should carefully select or unselect the options you want to install. Do not blindly install or > enable features. To uninstall, find Sunshine in the list here and select "Uninstall" from the overflow menu. Different versions of Windows may provide slightly different steps for uninstall. #### Standalone (lite version) > [!WARNING] > By using this package instead of the installer, performance will be reduced. This package is not > recommended for most users. No support will be provided! 1. Download and extract [Sunshine-Windows-AMD64-portable.zip](https://github.com/LizardByte/Sunshine/releases/latest/download/Sunshine-Windows-AMD64-portable.zip) 2. Open command prompt as administrator 3. Firewall rules Install: ```bash cd /d {path to extracted directory} scripts/add-firewall-rule.bat ``` Uninstall: ```bash cd /d {path to extracted directory} scripts/delete-firewall-rule.bat ``` 4. Virtual Gamepad Support Install: ```bash cd /d {path to extracted directory} scripts/install-gamepad.bat ``` Uninstall: ```bash cd /d {path to extracted directory} scripts/uninstall-gamepad.bat ``` 5. Windows service Install: ```bash cd /d {path to extracted directory} scripts/install-service.bat scripts/autostart-service.bat ``` Uninstall: ```bash cd /d {path to extracted directory} scripts/uninstall-service.bat ``` ## Initial Setup After installation, some initial setup is required. ### Linux #### KMS Capture > [!WARNING] > Capture of most Wayland-based desktop environments will fail unless this step is performed. > [!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 > allow Sunshine to use KMS capture. ##### Enable ```bash sudo setcap cap_sys_admin+p $(readlink -f $(which sunshine)) ``` #### X11 Capture For X11 capture to work, you may need to disable the capabilities that were set for KMS capture. ```bash sudo setcap -r $(readlink -f $(which sunshine)) ``` #### Service **Start once** ```bash systemctl --user start sunshine ``` **Start on boot** ```bash systemctl --user enable sunshine ``` ### macOS The first time you start Sunshine, you will be asked to grant access to screen recording and your microphone. Sunshine can only access microphones on macOS due to system limitations. To stream system audio use [Soundflower](https://github.com/mattingalls/Soundflower) or [BlackHole](https://github.com/ExistentialAudio/BlackHole). > [!NOTE] > Command Keys are not forwarded by Moonlight. Right Option-Key is mapped to CMD-Key. > [!CAUTION] > Gamepads are not currently supported. ## Usage ### Basic usage If Sunshine is not installed/running as a service, then start Sunshine with the following command, unless a start command is listed in the specified package [install](#install) instructions above. > [!NOTE] > A service is a process that runs in the background. This is the default when installing Sunshine from the > Windows installer. Running multiple instances of Sunshine is not advised. ```bash sunshine ``` ### Specify config file ```bash sunshine /sunshine.conf ``` > [!NOTE] > You do not need to specify a config file. If no config file is entered, the default location will be used. > [!TIP] > The configuration file specified will be created if it doesn't exist. ### Start Sunshine over SSH (Linux/X11) Assuming you are already logged into the host, you can use this command ```bash ssh @ 'export DISPLAY=:0; sunshine' ``` If you are logged into the host with only a tty (teletypewriter), you can use `startx` to start the X server prior to executing Sunshine. You nay need to add `sleep` between `startx` and `sunshine` to allow more time for the display to be ready. ```bash ssh @ 'startx &; export DISPLAY=:0; sunshine' ``` > [!TIP] > You could also use the `~/.bash_profile` or `~/.bashrc` files to set up the `DISPLAY` variable. @seealso{See [Remote SSH Headless Setup](https://app.lizardbyte.dev/2023-09-14-remote-ssh-headless-sunshine-setup) on how to set up a headless streaming server without autologin and dummy plugs (X11 + NVidia GPUs)} ### Configuration Sunshine is configured via the web ui, which is available on [https://localhost:47990](https://localhost:47990) by default. You may replace *localhost* with your internal ip address. > [!NOTE] > Ignore any warning given by your browser about "insecure website". This is due to the SSL certificate > being self-signed. > [!CAUTION] > If running for the first time, make sure to note the username and password that you created. 1. Add games and applications. 2. Adjust any configuration settings as needed. 3. In Moonlight, you may need to add the PC manually. 4. When Moonlight requests for you insert the pin: - Login to the web ui - Go to "PIN" in the Navbar - Type in your PIN and press Enter, you should get a Success Message - In Moonlight, select one of the Applications listed ### Arguments To get a list of available arguments, run the following command. @tabs{ @tab{ General | ```bash sunshine --help ```} @tab{ AppImage | ```bash ./sunshine.AppImage --help ```} @tab{ Flatpak | ```bash flatpak run --command=sunshine dev.lizardbyte.app.Sunshine --help ```} } ### Shortcuts All shortcuts start with `Ctrl+Alt+Shift`, just like Moonlight. * `Ctrl+Alt+Shift+N`: Hide/Unhide the cursor (This may be useful for Remote Desktop Mode for Moonlight) * `Ctrl+Alt+Shift+F1/F12`: Switch to different monitor for Streaming ### Application List * Applications should be configured via the web UI * A basic understanding of working directories and commands is required * You can use Environment variables in place of values * `$(HOME)` will be replaced by the value of `$HOME` * `$$` will be replaced by `$`, e.g. `$$(HOME)` will be become `$(HOME)` * `env` - Adds or overwrites Environment variables for the commands/applications run by Sunshine. This can only be changed by modifying the `apps.json` file directly. ### Considerations * On Windows, Sunshine uses the Desktop Duplication API which only supports capturing from the GPU used for display. If you want to capture and encode on the eGPU, connect a display or HDMI dummy display dongle to it and run the games on that display. * When an application is started, if there is an application already running, it will be terminated. * If any of the prep-commands fail, starting the application is aborted. * When the application has been shutdown, the stream shuts down as well. * For example, if you attempt to run `steam` as a `cmd` instead of `detached` the stream will immediately fail. This is due to the method in which the steam process is executed. Other applications may behave similarly. * This does not apply to `detached` applications. * The "Desktop" app works the same as any other application except it has no commands. It does not start an application, instead it simply starts a stream. If you removed it and would like to get it back, just add a new application with the name "Desktop" and "desktop.png" as the image path. * For the Linux flatpak you must prepend commands with `flatpak-spawn --host`. * If inputs (mouse, keyboard, gamepads...) aren't working after connecting, add the user running sunshine to the `input` group. ### HDR Support Streaming HDR content is officially supported on Windows hosts and experimentally supported for Linux hosts. * General HDR support information and requirements: * HDR must be activated in the host OS, which may require an HDR-capable display or EDID emulator dongle connected to your host PC. * You must also enable the HDR option in your Moonlight client settings, otherwise the stream will be SDR (and probably overexposed if your host is HDR). * A good HDR experience relies on proper HDR display calibration both in the OS and in game. HDR calibration can differ significantly between client and host displays. * You may also need to tune the brightness slider or HDR calibration options in game to the different HDR brightness capabilities of your client's display. * Some GPUs video encoders can produce lower image quality or encoding performance when streaming in HDR compared to SDR. Additional information: @tabs{ @tab{ Windows | - HDR streaming is supported for Intel, AMD, and NVIDIA GPUs that support encoding HEVC Main 10 or AV1 10-bit profiles. - 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. - Older games that use NVIDIA-specific NVAPI HDR rather than native Windows HDR support may not display properly in HDR. } @tab{ Linux | - HDR streaming is supported for Intel and AMD GPUs that support encoding HEVC Main 10 or AV1 10-bit profiles using VAAPI. - The KMS capture backend is required for HDR capture. Other capture methods, like NvFBC or X11, do not support HDR. - You will need a desktop environment with a compositor that supports HDR rendering, such as Gamescope or KDE Plasma 6. @seealso{[Arch wiki on HDR Support for Linux](https://wiki.archlinux.org/title/HDR_monitor_support) and [Reddit Guide for HDR Support for AMD GPUs](https://www.reddit.com/r/linux_gaming/comments/10m2gyx/guide_alpha_test_hdr_on_linux)} } } ### Tutorials and Guides Tutorial videos are available [here](https://www.youtube.com/playlist?list=PLMYr5_xSeuXAbhxYHz86hA1eCDugoxXY0). Guides are available [here](guides.md). @admonition{Community! | Tutorials and Guides are community generated. Want to contribute? Reach out to us on our discord server.}
| Previous | Next | |:-------------------------|--------------------------:| | [Overview](../README.md) | [Changelog](changelog.md) |
[TOC]
[latest-release]: https://github.com/LizardByte/Sunshine/releases/latest ================================================ FILE: docs/guides.md ================================================ # Guides @admonition{Community | A collection of guides written by the community is available on our [blog](https://app.lizardbyte.dev/blog). Feel free to contribute your own tips and trips by making a PR to [LizardByte.github.io](https://github.com/LizardByte/LizardByte.github.io).}
| Previous | Next | |:----------------------------------------|--------------------------------------------:| | [Awesome-Sunshine](awesome_sunshine.md) | [Performance Tuning](performance_tuning.md) |
[TOC]
================================================ FILE: docs/legal.md ================================================ # Legal > [!CAUTION] > This documentation is for informational purposes only and is not intended as legal advice. If you have > any legal questions or concerns about using Sunshine, we recommend consulting with a lawyer. Sunshine is licensed under the GPL-3.0 license, which allows for free use and modification of the software. The full text of the license can be reviewed [here](https://github.com/LizardByte/Sunshine/blob/master/LICENSE). ## Commercial Use Sunshine can be used in commercial applications without any limitations. This means that businesses and organizations can use Sunshine to create and sell products or services without needing to seek permission or pay a fee. However, it is important to note that the GPL-3.0 license does not grant any rights to distribute or sell the encoders contained within Sunshine. If you plan to sell access to Sunshine as part of their distribution, you are responsible for obtaining the necessary licenses to do so. This may include obtaining a license from the Motion Picture Experts Group (MPEG-LA) and/or any other necessary licensing requirements. In summary, while Sunshine is free to use, it is the user's responsibility to ensure compliance with all applicable licensing requirements when redistributing the software as part of a commercial offering. If you have any questions or concerns about using Sunshine in a commercial setting, we recommend consulting with a lawyer.
| Previous | Next | |:------------------------------------------------|----------------------------------:| | [Gamestream Migration](gamestream_migration.md) | [Configuration](configuration.md) |
[TOC]
================================================ FILE: docs/performance_tuning.md ================================================ # Performance Tuning In addition to the options available in the [Configuration](configuration.md) section, there are a few additional system options that can be used to help improve the performance of Sunshine. ## AMD In Windows, enabling *Enhanced Sync* in AMD's settings may help reduce the latency by an additional frame. This applies to `amfenc` and `libx264`. ## NVIDIA Enabling *Fast Sync* in Nvidia settings may help reduce latency.
| Previous | Next | |:--------------------|--------------:| | [Guides](guides.md) | [API](api.md) |
[TOC]
================================================ FILE: docs/third_party_packages.md ================================================ # Third-Party Packages > [!WARNING] > These packages are not maintained by LizardByte. Use at your own risk. ## Chocolatey [![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) ## nixpkgs [![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) ## Scoop [![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) ## Solus [![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)
| Previous | Next | |:------------------------------|------------------------------------------------:| | [Docker](../DOCKER_README.md) | [Gamestream Migration](gamestream_migration.md) |
[TOC]
================================================ FILE: docs/troubleshooting.md ================================================ # Troubleshooting ## General ### Forgotten Credentials If you forgot your credentials to the web UI, try this. @tabs{ @tab{General | ```bash sunshine --creds {new-username} {new-password} ``` } @tab{AppImage | ```bash ./sunshine.AppImage --creds {new-username} {new-password} ``` } @tab{Flatpak | ```bash flatpak run --command=sunshine dev.lizardbyte.app.Sunshine --creds {new-username} {new-password} ``` } } > [!TIP] > Remember to replace `{new-username}` and `{new-password}` with your new credentials. > Do not include the curly braces. ### Unusual Mouse Behavior If you experience unusual mouse behavior, try attaching a physical mouse to the Sunshine host. ### Web UI Access Can't access the web UI? 1. Check firewall rules. ### Controller works on Steam but not in games One trick might be to change Steam settings and check or uncheck the configuration to support Xbox/PlayStation controllers and leave only support for Generic controllers. Also, if you have many controllers already directly connected to the host, it might help to disable them so that the Sunshine-provided controller (connected to the guest) is the "first" one. In Linux this can be achieved on USB devices by finding the device in `/sys/bus/usb/devices/` and writing `0` to the `authorized` file. ### Network performance test For real-time game streaming the most important characteristic of the network path between server and client is not pure bandwidth but rather stability and consistency (low latency with low variance, minimal or no packet loss). The network can be tested using the multi-platform tool [iPerf3](https://iperf.fr). On the Sunshine host `iperf3` is started in server mode: ```bash iperf3 -s ``` On the client device iperf3 is asked to perform a 60-second UDP test in a reverse direction (from server to client) at a given bitrate (e.g. 50 Mbps): ```bash iperf3 -c {HostIpAddress} -t 60 -u -R -b 50M ``` Watch the output on the client for packet loss and jitter values. Both should be (very) low. Ideally, packet loss remains less than 5% and jitter below 1 ms. For Android clients use [PingMaster](https://play.google.com/store/apps/details?id=com.appplanex.pingmasternetworktools). For iOS clients use [HE.NET Network Tools](https://apps.apple.com/us/app/he-net-network-tools/id858241710). If you are testing a remote connection (over the internet), you will need to forward the port 5201 (TCP and UDP) from your host. ### Packet loss (Buffer overrun) If the host PC (running Sunshine) has a much faster connection to the network than the slowest segment of the network path to the client device (running Moonlight), massive packet loss can occur: Sunshine emits its stream in bursts every 16 ms (for 60 fps), but those bursts can't be passed on fast enough to the client and must be buffered by one of the network devices inbetween. If the bitrate is high enough, these buffers will overflow and data will be discarded. This can easily happen if e.g., the host has a 2.5 Gbit/s connection and the client only 1 Gbit/s or Wi-Fi. Similarly, a 1 Gbps host may be too fast for a client having only a 100 Mbps interface. As a workaround the transmission speed of the host NIC can be reduced: 1 Gbps instead of 2.5 or 100 Mbps instead of 1 Gbps. A technically more advanced solution would be to configure traffic shaping rules at the OS level, so that only Sunshine's traffic is slowed down. Such a solution on Linux could look like that: ```bash # 1) Remove existing qdisc (pfifo_fast) sudo tc qdisc del dev root # 2) Add HTB root qdisc with default class 1:1 sudo tc qdisc add dev root handle 1: htb default 1 # 3) Create class 1:1 for full 10 Gbit/s (all other traffic) sudo tc class add dev parent 1: classid 1:1 htb \ rate 10000mbit ceil 10000mbit burst 32k # 4) Create class 1:10 for Sunshine game stream at 1 Gbit/s sudo tc class add dev parent 1: classid 1:10 htb \ rate 1000mbit ceil 1000mbit burst 32k # 5) Filter UDP source port 47998 into class 1:10 sudo tc filter add dev protocol ip parent 1: prio 1 \ u32 match ip protocol 17 0xff \ match ip sport 47998 0xffff flowid 1:10 ``` In that way only the Sunshine traffic is limited by 1 Gbit. This is not persistent on reboots. If you use a different port for the game stream, you need to adjust the last command. Sunshine versions > 0.23.1 include improved networking code that should alleviate or even solve this issue (without reducing the NIC speed). ### Packet loss (MTU) Although unlikely, some guests might work better with a lower [MTU](https://en.wikipedia.org/wiki/Maximum_transmission_unit) from the host. For example, an LG TV was found to have 30–60% packet loss when the host had MTU set to 1500 and 1472, but 0% packet loss with a MTU of 1428 set in the network card serving the stream (a Linux PC). It's unclear how that helped precisely, so it's a last resort suggestion. ## Linux ### Hardware Encoding fails Due to legal concerns, Mesa has disabled hardware decoding and encoding by default. ```txt Error: Could not open codec [h264_vaapi]: Function not implemented ``` If you see the above error in the Sunshine logs, compiling *Mesa* manually may be required. See the official Mesa3D [Compiling and Installing](https://docs.mesa3d.org/install.html) documentation for instructions. > [!IMPORTANT] > You must re-enable the disabled encoders. You can do so by passing the following argument to the build > system. You may also want to enable decoders, however, that is not required for Sunshine and is not covered here. > ```bash > -Dvideo-codecs=h264enc,h265enc > ``` > [!NOTE] > Other build options are listed in the > [meson options](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/meson_options.txt) file. ### Input not working After installation, the `udev` rules need to be reloaded. Our post-install script tries to do this for you automatically, but if it fails, you may need to restart your system. If the input is still not working, you may need to add your user to the `input` group. ```bash sudo usermod -aG input $USER ``` ### KMS Streaming fails If screencasting fails with KMS, you may need to run the following to force unprivileged screencasting. ```bash sudo setcap -r $(readlink -f $(which sunshine)) ``` > [!NOTE] > The above command will not work with the AppImage or Flatpak packages. Please refer to the > [AppImage setup](md_docs_2getting__started.html#appimage) or > [Flatpak setup](md_docs_2getting__started.html#flatpak) for more specific instructions. ### KMS streaming fails on Nvidia GPUs If KMS screen capture results in a black screen being streamed, you may need to set the parameter `modeset=1` for Nvidia's kernel module. This can be done by adding the following directive to the kernel command line: ```bash nvidia_drm.modeset=1 ``` Consult your distribution's documentation for details on how to do this. (Most often grub is used to load the kernel and set its command line.) ### AMD encoding latency issues If you notice unexpectedly high encoding latencies (e.g., in Moonlight's performance overlay) or strong fluctuations thereof, your system's Mesa libraries are outdated (<24.2). This is particularly problematic at higher resolutions (4K). Starting with Mesa-24.2, applications can request a [low-latency mode](https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/30039) by running them with a special [environment variable](https://docs.mesa3d.org/envvars.html#envvar-AMD_DEBUG): ```bash export AMD_DEBUG=lowlatencyenc ``` Sunshine sets this variable automatically, no manual configuration is needed. To check whether low-latency mode is being used, one can watch the VCLK and DCLK frequencies in amdgpu_top. Without this encoder tuning both clock frequencies will fluctuate strongly, whereas with active low-latency encoding they will stay high as long as the encoder is used. ### Gamescope compatibility Some users have reported stuttering issues when streaming games running within Gamescope. ## macOS ### Dynamic session lookup failed If you get this error: > Dynamic session lookup supported but failed: launchd did not provide a socket path, verify that > org.freedesktop.dbus-session.plist is loaded! Try this. ```bash launchctl load -w /Library/LaunchAgents/org.freedesktop.dbus-session.plist ``` ## Windows ### No gamepad detected Verify that you've installed [Nefarius Virtual Gamepad](https://github.com/nefarius/ViGEmBus/releases/latest). ### Permission denied Since Sunshine runs as a service on Windows, it may not have the same level of access that your regular user account has. You may get permission denied errors when attempting to launch a game or application from a non-system drive. You will need to modify the security permissions on your disk. Ensure that user/principal SYSTEM has full permissions on the disk. ### Stuttering If you experience stuttering using NVIDIA, try disabling `vsync:fast` in the NVIDIA Control Panel.
| Previous | Next | |:--------------|------------------------:| | [API](api.md) | [Building](building.md) |
[TOC]
================================================ FILE: gh-pages-template/.readthedocs.yaml ================================================ --- # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 build: os: ubuntu-24.04 tools: ruby: "3.3" apt_packages: - 7zip - jq jobs: install: - | mkdir -p "./tmp" branch="master" base_url="https://raw.githubusercontent.com/LizardByte/LizardByte.github.io" url="${base_url}/refs/heads/${branch}/scripts/readthedocs_build.sh" curl -sSL -o "./tmp/readthedocs_build.sh" "${url}" chmod +x "./tmp/readthedocs_build.sh" build: html: - "./tmp/readthedocs_build.sh" ================================================ FILE: gh-pages-template/_config.yml ================================================ --- # See https://github.com/daattali/beautiful-jekyll/blob/master/_config.yml for documented options avatar: "/Sunshine/assets/img/navbar-avatar.png" ================================================ FILE: gh-pages-template/index.html ================================================ --- title: Sunshine subtitle: A LizardByte project layout: page full-width: true after-content: - donate.html - support.html cover-img: - /Sunshine/assets/img/banners/AdobeStock_305732536_1920x1280.jpg - /Sunshine/assets/img/banners/AdobeStock_231616343_1920x1280.jpg - /Sunshine/assets/img/banners/AdobeStock_303330124_1920x1280.jpg ext-js: - https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js ---

Sunshine is a self-hosted game stream host for Moonlight. Offering low latency, cloud gaming server capabilities with support for AMD, Intel, and Nvidia GPUs for hardware encoding. Software encoding is also available. You can connect to Sunshine from any Moonlight client on a variety of devices. A web UI is provided to allow configuration, and client pairing, from your favorite web browser. Pair from the local server or any mobile device.

Features

Self-hosted

Run Sunshine on your own hardware. No need to pay monthly fees to a cloud gaming provider.

Moonlight
Moonlight Support

Connect to Sunshine from any Moonlight client. Moonlight is available for Windows, macOS, Linux, Android, iOS, Xbox, and more. See clients for more information.

Hardware Encoding

Sunshine supports AMD, Intel, and Nvidia GPUs for hardware encoding. Software encoding is also available.

Low Latency

Sunshine is designed to provide the lowest latency possible to achieve optimal gaming performance.

Control

Sunshine emulates an Xbox, PlayStation, or Nintendo Switch controller. Use nearly any controller on your Moonlight client!

  • Nintendo Switch emulation is only available on Linux.
  • Gamepad emulation is not currently supported on macOS.

Configurable

Sunshine offers many configuration options to customize your experience.

Documentation

Read the documentation to learn how to install, use, and configure Sunshine.

Download

Download Sunshine for your platform.

================================================ FILE: package.json ================================================ { "name": "sunshine", "version": "0.0.0", "scripts": { "build": "vite build --debug", "build-clean": "vite build --debug --emptyOutDir", "dev": "vite build --watch", "serve": "serve ./tests/fixtures/http --no-port-switching" }, "type": "module", "dependencies": { "@lizardbyte/shared-web": "2025.922.181114", "vue": "3.5.22", "vue-i18n": "11.1.12" }, "devDependencies": { "@codecov/vite-plugin": "1.9.1", "@vitejs/plugin-vue": "6.0.1", "serve": "14.2.5", "vite": "6.3.6", "vite-plugin-ejs": "1.7.0" } } ================================================ FILE: packaging/linux/AppImage/AppRun ================================================ #!/bin/bash # custom AppRun for Sunshine AppImage # path of the extracted AppRun HERE="$(dirname "$(readlink -f "${0}")")" SUNSHINE_PATH=/usr/bin/sunshine SUNSHINE_BIN_HERE=$HERE/usr/bin/sunshine SUNSHINE_SHARE_HERE=$HERE/usr/share/sunshine # Set APPDIR when running directly from the AppDir: if [ -z "$APPDIR" ]; then ARGV0="AppRun" fi cd "$HERE" || exit 1 function help() { echo " ------------------------------ Sunshine AppImage package. ------------------------------ sunshine.AppImage options ------------------------ Usage: $ARGV0 --help, -h ------ # This message $ARGV0 --install, -i # Install input rules sunshine.service files. Restart required. $ARGV0 --remove, -r # Remove input rules sunshine.service files. $ARGV0 --appimage-help # Show available AppImage options sunshine options ---------------- " # print sunshine binary help, replacing the sunshine command in usage statement "$SUNSHINE_BIN_HERE" --help | sed -e "s#$SUNSHINE_BIN_HERE#$ARGV0#g" } function install() { # user input rules # shellcheck disable=SC2002 cat "$SUNSHINE_SHARE_HERE/udev/rules.d/60-sunshine.rules" | sudo tee /etc/udev/rules.d/60-sunshine.rules cat "$SUNSHINE_SHARE_HERE/modules-load.d/60-sunshine.conf" | sudo tee /etc/modules-load.d/60-sunshine.conf sudo modprobe uhid sudo udevadm control --reload-rules sudo udevadm trigger --property-match=DEVNAME=/dev/uinput sudo udevadm trigger --property-match=DEVNAME=/dev/uhid # sunshine service mkdir -p ~/.config/systemd/user cp -r "$SUNSHINE_SHARE_HERE/systemd/user/" ~/.config/systemd/ # patch service executable path sed -i -e "s#$SUNSHINE_PATH#$(readlink -f $ARGV0)#g" ~/.config/systemd/user/sunshine.service # setcap sudo setcap cap_sys_admin+p "$(readlink -f "$SUNSHINE_BIN_HERE")" } function remove() { # remove input rules sudo rm -f /etc/udev/rules.d/60-sunshine.rules # remove uhid module loading config sudo rm -f /etc/modules-load.d/60-sunshine.conf # remove service sudo rm -f ~/.config/systemd/user/sunshine.service } # process arguments if [ "x$1" == "xhelp" ] || [ "x$1" == "x--help" ] || [ "x$1" == "x-h" ] ; then help exit $? fi if [ "x$1" == "xinstall" ] || [ "x$1" == "x--install" ] || [ "x$1" == "x-i" ] ; then install exit $? fi if [ "x$1" == "xremove" ] || [ "x$1" == "x--remove" ] || [ "x$1" == "x-r" ] ; then remove exit $? fi # create config directory if it doesn't exist # https://github.com/LizardByte/Sunshine/issues/324 mkdir -p ~/.config/sunshine # run sunshine "$SUNSHINE_BIN_HERE" $@ ================================================ FILE: packaging/linux/AppImage/dev.lizardbyte.app.Sunshine.desktop ================================================ [Desktop Entry] Categories=RemoteAccess;Network; Comment=@PROJECT_DESCRIPTION@ Exec=sunshine Icon=sunshine Keywords=gamestream;stream;moonlight;remote play; Name=@PROJECT_NAME@ Terminal=true Type=Application Version=1.0 X-AppImage-Arch=x86_64 X-AppImage-Name=sunshine X-AppImage-Version=@PROJECT_VERSION@ ================================================ FILE: packaging/linux/Arch/PKGBUILD ================================================ # Edit on github: https://github.com/LizardByte/Sunshine/blob/master/packaging/linux/Arch/PKGBUILD # Reference: https://wiki.archlinux.org/title/PKGBUILD ## options : "${_run_unit_tests:=false}" # if set to true; unit tests will be executed post build; useful in CI : "${_support_headless_testing:=false}" : "${_use_cuda:=detect}" # nvenc : "${_commit:=@GITHUB_COMMIT@}" pkgname='sunshine' pkgver=@PROJECT_VERSION@@SUNSHINE_SUB_VERSION@ pkgrel=1 pkgdesc="@PROJECT_DESCRIPTION@" arch=('x86_64' 'aarch64') url=@PROJECT_HOMEPAGE_URL@ license=('GPL-3.0-only') install=sunshine.install _gcc_version=14 depends=( 'avahi' 'curl' 'libayatana-appindicator' 'libcap' 'libdrm' 'libevdev' 'libmfx' 'libnotify' 'libpulse' 'libva' 'libx11' 'libxcb' 'libxfixes' 'libxrandr' 'libxtst' 'miniupnpc' 'numactl' 'openssl' 'opus' 'udev' 'which' ) makedepends=( 'appstream' 'appstream-glib' 'cmake' 'desktop-file-utils' "gcc${_gcc_version}" 'git' 'make' 'nodejs' 'npm' ) optdepends=( 'libva-mesa-driver: AMD GPU encoding support' ) provides=() conflicts=() source=("$pkgname::git+@GITHUB_CLONE_URL@#commit=${_commit}") sha256sums=('SKIP') # Options Handling if [[ "${_use_cuda::1}" == "d" ]] && pacman -Qi cuda &> /dev/null; then _use_cuda=true fi if [[ "${_use_cuda::1}" == "t" ]]; then optdepends+=( 'cuda: Nvidia GPU encoding support' ) fi if [[ "${_support_headless_testing::1}" == "t" ]]; then optdepends+=( 'xorg-server-xvfb: Virtual X server for headless testing' ) fi # Ensure makedepends, checkdepends, optdepends are sorted if [ -n "${makedepends+x}" ]; then mapfile -t tmp_array < <(printf '%s\n' "${makedepends[@]}" | sort) makedepends=("${tmp_array[@]}") unset tmp_array fi if [ -n "${optdepends+x}" ]; then mapfile -t tmp_array < <(printf '%s\n' "${optdepends[@]}" | sort) optdepends=("${tmp_array[@]}") unset tmp_array fi prepare() { cd "$pkgname" git submodule update --recursive --init } build() { export BRANCH="@GITHUB_BRANCH@" export BUILD_VERSION="@BUILD_VERSION@" export COMMIT="${_commit}" export CC="gcc-${_gcc_version}" export CXX="g++-${_gcc_version}" export CFLAGS="${CFLAGS/-Werror=format-security/}" export CXXFLAGS="${CXXFLAGS/-Werror=format-security/}" export MAKEFLAGS="${MAKEFLAGS:--j$(nproc)}" local _cmake_options=( -S "$pkgname" -B build -Wno-dev -D BUILD_DOCS=OFF -D BUILD_WERROR=ON -D CMAKE_INSTALL_PREFIX=/usr -D SUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine -D SUNSHINE_ASSETS_DIR="share/sunshine" -D SUNSHINE_PUBLISHER_NAME='LizardByte' -D SUNSHINE_PUBLISHER_WEBSITE='https://app.lizardbyte.dev' -D SUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support' ) if [[ "${_use_cuda::1}" != "t" ]]; then _cmake_options+=(-DSUNSHINE_ENABLE_CUDA=OFF -DCUDA_FAIL_ON_MISSING=OFF) else # If cuda has just been installed, its variables will not be available in the environment # therefore, set them manually to the expected values on Arch Linux if [ -z "${CUDA_PATH:-}" ] && pacman -Qi cuda &> /dev/null; then local _cuda_gcc_version _cuda_gcc_version="$(LC_ALL=C pacman -Si cuda | grep -Pom1 '^Depends On\s*:.*\bgcc\K[0-9]+\b')" export CUDA_PATH=/opt/cuda export NVCC_CCBIN="/usr/bin/g++-${_cuda_gcc_version}" fi fi if [[ "${_run_unit_tests::1}" != "t" ]]; then _cmake_options+=(-DBUILD_TESTS=OFF) fi cmake "${_cmake_options[@]}" appstreamcli validate "build/dev.lizardbyte.app.Sunshine.metainfo.xml" appstream-util validate "build/dev.lizardbyte.app.Sunshine.metainfo.xml" desktop-file-validate "build/dev.lizardbyte.app.Sunshine.desktop" desktop-file-validate "build/dev.lizardbyte.app.Sunshine.terminal.desktop" make -C build } check() { cd "${srcdir}/build" ./sunshine --version if [[ "${_run_unit_tests::1}" == "t" ]]; then export CC="gcc-${_gcc_version}" export CXX="g++-${_gcc_version}" cd "${srcdir}/build/tests" ./test_sunshine --gtest_color=yes fi } package() { export MAKEFLAGS="${MAKEFLAGS:--j$(nproc)}" make -C build install DESTDIR="$pkgdir" } ================================================ FILE: packaging/linux/Arch/sunshine.install ================================================ do_setcap() { setcap cap_sys_admin+p $(readlink -f usr/bin/sunshine) } do_udev_reload() { udevadm control --reload-rules udevadm trigger --property-match=DEVNAME=/dev/uinput udevadm trigger --property-match=DEVNAME=/dev/uhid modprobe uinput || true modprobe uhid || true } post_install() { do_setcap do_udev_reload modprobe uhid } post_upgrade() { do_setcap do_udev_reload modprobe uhid } ================================================ FILE: packaging/linux/dev.lizardbyte.app.Sunshine.desktop ================================================ [Desktop Entry] Actions=RunInTerminal; Categories=RemoteAccess;Network; Comment=@PROJECT_DESCRIPTION@ Exec=/usr/bin/env systemctl start --u sunshine Icon=@SUNSHINE_DESKTOP_ICON@ Keywords=gamestream;stream;moonlight;remote play; Name=@PROJECT_NAME@ Type=Application Version=1.0 [Desktop Action RunInTerminal] Exec=gio launch @CMAKE_INSTALL_FULL_DATAROOTDIR@/applications/@PROJECT_FQDN@.terminal.desktop Icon=application-x-executable Name=Run in Terminal ================================================ FILE: packaging/linux/dev.lizardbyte.app.Sunshine.metainfo.xml ================================================ @PROJECT_FQDN@ @PROJECT_NAME@ @PROJECT_BRIEF_DESCRIPTION@ CC0-1.0 @PROJECT_LICENSE@ pointing keyboard touch gamepad @PROJECT_HOMEPAGE_URL@ https://github.com/LizardByte/Sunshine/issues https://docs.lizardbyte.dev/projects/sunshine/latest/md_docs_2troubleshooting.html https://docs.lizardbyte.dev/projects/sunshine https://app.lizardbyte.dev/#Donate https://translate.lizardbyte.dev https://app.lizardbyte.dev/support

@PROJECT_LONG_DESCRIPTION@

NOTE: Sunshine requires additional installation steps (Flatpak).

flatpak run --command=additional-install.sh @PROJECT_FQDN@

NOTE: Sunshine uses a self-signed certificate. The web browser will report it as not secure, but it is safe.

NOTE: KMS Grab (Flatpak)

sudo -i PULSE_SERVER=unix:/run/user/$(id -u $whoami)/pulse/native flatpak run @PROJECT_FQDN@

See the full changelog on GitHub

LizardByte https://app.lizardbyte.dev/Sunshine/assets/img/screenshots/01-sunshine-welcome-page.png Sunshine welcome page moderate mild mild @PROJECT_FQDN@.desktop
================================================ FILE: packaging/linux/dev.lizardbyte.app.Sunshine.terminal.desktop ================================================ [Desktop Entry] Name=@PROJECT_NAME@ Exec=sunshine Terminal=true Type=Application NoDisplay=true ================================================ FILE: packaging/linux/fedora/Sunshine.spec ================================================ %global build_timestamp %(date +"%Y%m%d") # use sed to replace these values %global build_version 0 %global branch 0 %global commit 0 %undefine _hardened_build Name: Sunshine Version: %{build_version} Release: 1%{?dist} Summary: Self-hosted game stream host for Moonlight. License: GPLv3-only URL: https://github.com/LizardByte/Sunshine Source0: tarball.tar.gz BuildRequires: appstream # BuildRequires: boost-devel >= 1.86.0 BuildRequires: cmake >= 3.25.0 BuildRequires: desktop-file-utils BuildRequires: libappstream-glib BuildRequires: libayatana-appindicator3-devel BuildRequires: libcap-devel BuildRequires: libcurl-devel BuildRequires: libdrm-devel BuildRequires: libevdev-devel BuildRequires: libgudev BuildRequires: libnotify-devel BuildRequires: libva-devel BuildRequires: libX11-devel BuildRequires: libxcb-devel BuildRequires: libXcursor-devel BuildRequires: libXfixes-devel BuildRequires: libXi-devel BuildRequires: libXinerama-devel BuildRequires: libXrandr-devel BuildRequires: libXtst-devel BuildRequires: git BuildRequires: mesa-libGL-devel BuildRequires: mesa-libgbm-devel BuildRequires: miniupnpc-devel BuildRequires: npm BuildRequires: numactl-devel BuildRequires: openssl-devel BuildRequires: opus-devel BuildRequires: pulseaudio-libs-devel BuildRequires: rpm-build BuildRequires: systemd-udev BuildRequires: systemd-rpm-macros %{?sysusers_requires_compat} BuildRequires: wget BuildRequires: which # for unit tests BuildRequires: xorg-x11-server-Xvfb # Conditional BuildRequires for cuda-gcc based on Fedora version %if 0%{?fedora} <= 41 BuildRequires: gcc13 BuildRequires: gcc13-c++ %global gcc_version 13 %global cuda_version 12.9.1 %global cuda_build 575.57.08 %elif %{?fedora} >= 42 BuildRequires: gcc14 BuildRequires: gcc14-c++ %global gcc_version 14 %global cuda_version 12.9.1 %global cuda_build 575.57.08 %endif %global cuda_dir %{_builddir}/cuda Requires: libayatana-appindicator3 >= 0.5.3 Requires: libcap >= 2.22 Requires: libcurl >= 7.0 Requires: libdrm > 2.4.97 Requires: libevdev >= 1.5.6 Requires: libopusenc >= 0.2.1 Requires: libva >= 2.14.0 Requires: libwayland-client >= 1.20.0 Requires: libX11 >= 1.7.3.1 Requires: miniupnpc >= 2.2.4 Requires: numactl-libs >= 2.0.14 Requires: openssl >= 3.0.2 Requires: pulseaudio-libs >= 10.0 Requires: which >= 2.21 %description Self-hosted game stream host for Moonlight. %prep # extract tarball to current directory mkdir -p %{_builddir}/Sunshine tar -xzf %{SOURCE0} -C %{_builddir}/Sunshine # list directory ls -a %{_builddir}/Sunshine %build # exit on error set -e # Detect the architecture and Fedora version architecture=$(uname -m) cuda_supported_architectures=("x86_64" "aarch64") # prepare CMAKE args cmake_args=( "-B=%{_builddir}/Sunshine/build" "-G=Unix Makefiles" "-S=." "-DBUILD_DOCS=OFF" "-DBUILD_WERROR=ON" "-DCMAKE_BUILD_TYPE=Release" "-DCMAKE_INSTALL_PREFIX=%{_prefix}" "-DSUNSHINE_ASSETS_DIR=%{_datadir}/sunshine" "-DSUNSHINE_EXECUTABLE_PATH=%{_bindir}/sunshine" "-DSUNSHINE_ENABLE_WAYLAND=ON" "-DSUNSHINE_ENABLE_X11=ON" "-DSUNSHINE_ENABLE_DRM=ON" "-DSUNSHINE_PUBLISHER_NAME=LizardByte" "-DSUNSHINE_PUBLISHER_WEBSITE=https://app.lizardbyte.dev" "-DSUNSHINE_PUBLISHER_ISSUE_URL=https://app.lizardbyte.dev/support" ) export CC=gcc-%{gcc_version} export CXX=g++-%{gcc_version} function install_cuda() { # check if we need to install cuda if [ -f "%{cuda_dir}/bin/nvcc" ]; then echo "cuda already installed" return fi local cuda_prefix="https://developer.download.nvidia.com/compute/cuda/" local cuda_suffix="" if [ "$architecture" == "aarch64" ]; then local cuda_suffix="_sbsa" fi local url="${cuda_prefix}%{cuda_version}/local_installers/cuda_%{cuda_version}_%{cuda_build}_linux${cuda_suffix}.run" echo "cuda url: ${url}" wget \ "$url" \ --progress=bar:force:noscroll \ --retry-connrefused \ --tries=3 \ -q -O "%{_builddir}/cuda.run" chmod a+x "%{_builddir}/cuda.run" "%{_builddir}/cuda.run" \ --no-drm \ --no-man-page \ --no-opengl-libs \ --override \ --silent \ --toolkit \ --toolkitpath="%{cuda_dir}" rm "%{_builddir}/cuda.run" # we need to patch math_functions.h on fedora 42 # see https://forums.developer.nvidia.com/t/error-exception-specification-is-incompatible-for-cospi-sinpi-cospif-sinpif-with-glibc-2-41/323591/3 if [ "%{?fedora}" -eq 42 ]; then echo "Original math_functions.h:" find "%{cuda_dir}" -name math_functions.h -exec cat {} \; # Apply the patch patch -p2 \ --backup \ --directory="%{cuda_dir}" \ --verbose \ < "%{_builddir}/Sunshine/packaging/linux/patches/${architecture}/01-math_functions.patch" fi } if [ -n "%{cuda_version}" ] && [[ " ${cuda_supported_architectures[@]} " =~ " ${architecture} " ]]; then install_cuda cmake_args+=("-DSUNSHINE_ENABLE_CUDA=ON") cmake_args+=("-DCMAKE_CUDA_COMPILER:PATH=%{cuda_dir}/bin/nvcc") cmake_args+=("-DCMAKE_CUDA_HOST_COMPILER=gcc-%{gcc_version}") else cmake_args+=("-DSUNSHINE_ENABLE_CUDA=OFF") fi # setup the version export BRANCH=%{branch} export BUILD_VERSION=v%{build_version} export COMMIT=%{commit} # cmake cd %{_builddir}/Sunshine echo "cmake args:" echo "${cmake_args[@]}" cmake "${cmake_args[@]}" make -j$(nproc) -C "%{_builddir}/Sunshine/build" %check # validate the metainfo file appstreamcli validate %{buildroot}%{_metainfodir}/*.metainfo.xml appstream-util validate %{buildroot}%{_metainfodir}/*.metainfo.xml desktop-file-validate %{buildroot}%{_datadir}/applications/*.desktop # run tests cd %{_builddir}/Sunshine/build xvfb-run ./tests/test_sunshine %install cd %{_builddir}/Sunshine/build %make_install %post # Note: this is copied from the postinst script # Load uhid (DS5 emulation) echo "Loading uhid kernel module for DS5 emulation." modprobe uhid # Check if we're in an rpm-ostree environment if [ ! -x "$(command -v rpm-ostree)" ]; then echo "Not in an rpm-ostree environment, proceeding with post install steps." # Trigger udev rule reload for /dev/uinput and /dev/uhid path_to_udevadm=$(which udevadm) if [ -x "$path_to_udevadm" ]; then echo "Reloading udev rules." $path_to_udevadm control --reload-rules $path_to_udevadm trigger --property-match=DEVNAME=/dev/uinput $path_to_udevadm trigger --property-match=DEVNAME=/dev/uhid echo "Udev rules reloaded successfully." else echo "error: udevadm not found or not executable." fi else echo "rpm-ostree environment detected, skipping post install steps. Restart to apply the changes." fi %files # Executables %caps(cap_sys_admin+p) %{_bindir}/sunshine %caps(cap_sys_admin+p) %{_bindir}/sunshine-* # Systemd unit file for user services %{_userunitdir}/sunshine.service # Udev rules %{_udevrulesdir}/*-sunshine.rules # Modules-load configuration %{_modulesloaddir}/*-sunshine.conf # Desktop entries %{_datadir}/applications/*.desktop # Icons %{_datadir}/icons/hicolor/scalable/apps/apollo.svg %{_datadir}/icons/hicolor/scalable/status/apollo*.svg # Metainfo %{_datadir}/metainfo/*.metainfo.xml # Assets %{_datadir}/sunshine/** %changelog ================================================ FILE: packaging/linux/flatpak/README.md ================================================

Sunshine

Self-hosted game stream host for Moonlight.

Flathub installs Flathub Version
## ℹ️ About Sunshine is a self-hosted game stream host for Moonlight. LizardByte has the full documentation hosted on [Read the Docs](https://docs.lizardbyte.dev/projects/sunshine) * [Stable](https://docs.lizardbyte.dev/projects/sunshine/latest/) * [Beta](https://docs.lizardbyte.dev/projects/sunshine/master/) This repo is synced from the upstream [Sunshine](https://github.com/LizardByte/Sunshine) repo. Please report issues and contribute to the upstream repo. ================================================ FILE: packaging/linux/flatpak/apps.json ================================================ { "env": { "PATH": "$(PATH):$(HOME)/.local/bin" }, "apps": [ { "name": "Desktop", "image-path": "desktop.png" } ] } ================================================ FILE: packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.desktop ================================================ [Desktop Entry] Categories=RemoteAccess;Network; Comment=@PROJECT_DESCRIPTION@ Exec=sunshine.sh Icon=@SUNSHINE_DESKTOP_ICON@ Keywords=gamestream;stream;moonlight;remote play; Name=@PROJECT_NAME@ Type=Application Version=1.0 ================================================ FILE: packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml ================================================ --- app-id: "@PROJECT_FQDN@" runtime: org.freedesktop.Platform runtime-version: "23.08" # requires CUDA >= 12.2 sdk: org.freedesktop.Sdk sdk-extensions: - org.freedesktop.Sdk.Extension.node20 command: sunshine separate-locales: false finish-args: - --device=all # access all devices - --env=PULSE_PROP_media.category=Manager # allow sunshine to manage audio sinks - --env=SUNSHINE_MIGRATE_CONFIG=1 # migrate config files to the new location - --filesystem=home # need to save files in user's home directory - --share=ipc # required for X11 shared memory extension - --share=network # access network - --socket=pulseaudio # play sounds using pulseaudio - --socket=wayland # show windows using Wayland - --socket=fallback-x11 # show windows using X11 - --system-talk-name=org.freedesktop.Avahi # talk to avahi on the system bus - --talk-name=org.freedesktop.Flatpak # talk to flatpak on the session bus cleanup: - /include - /lib/cmake - /lib/pkgconfig - /lib/*.la - /lib/*.a - /share/man modules: # Test dependencies - "modules/xvfb/xvfb.json" # Build dependencies - "modules/nlohmann_json.json" # Runtime dependencies - shared-modules/libayatana-appindicator/libayatana-appindicator-gtk3.json - "modules/avahi.json" - "modules/boost.json" - "modules/libevdev.json" - "modules/libnotify.json" - "modules/miniupnpc.json" - "modules/numactl.json" # Caching is configured until here, not including CUDA, since it is too large for GitHub cache - "modules/cuda.json" - name: sunshine builddir: true build-options: append-path: /usr/lib/sdk/node20/bin env: BUILD_VERSION: "@BUILD_VERSION@" BRANCH: "@GITHUB_BRANCH@" COMMIT: "@GITHUB_COMMIT@" XDG_CACHE_HOME: /run/build/sunshine/flatpak-node/cache npm_config_cache: /run/build/sunshine/flatpak-node/npm-cache npm_config_nodedir: /usr/lib/sdk/node20 npm_config_offline: 'true' NPM_CONFIG_LOGLEVEL: info buildsystem: cmake-ninja config-opts: - -DBOOST_USE_STATIC=OFF - -DBUILD_DOCS=OFF - -DBUILD_WERROR=ON - -DCMAKE_BUILD_TYPE=Release - -DCMAKE_CUDA_COMPILER=/app/cuda/bin/nvcc - -DSUNSHINE_ASSETS_DIR=share/sunshine - -DSUNSHINE_BUILD_FLATPAK=ON - -DSUNSHINE_EXECUTABLE_PATH=/app/bin/sunshine - -DSUNSHINE_ENABLE_WAYLAND=ON - -DSUNSHINE_ENABLE_X11=ON - -DSUNSHINE_ENABLE_DRM=ON - -DSUNSHINE_ENABLE_CUDA=ON - -DSUNSHINE_PUBLISHER_NAME='LizardByte' - -DSUNSHINE_PUBLISHER_WEBSITE='https://app.lizardbyte.dev' - -DSUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support' no-make-install: false post-install: - install -D $FLATPAK_BUILDER_BUILDDIR/packaging/linux/flatpak/scripts/* /app/bin - install -D $FLATPAK_BUILDER_BUILDDIR/packaging/linux/flatpak/apps.json /app/share/sunshine/apps.json run-tests: true test-rule: "" # empty to disable test-commands: - npm run serve & xvfb-run tests/test_sunshine --gtest_color=yes sources: - generated-sources.json - type: git url: "@GITHUB_CLONE_URL@" commit: "@GITHUB_COMMIT@" - type: file path: package-lock.json ================================================ FILE: packaging/linux/flatpak/exceptions.json ================================================ { "dev.lizardbyte.app.Sunshine": [ "appstream-external-screenshot-url", "appstream-screenshots-not-mirrored-in-ostree", "external-gitmodule-url-found", "finish-args-flatpak-spawn-access", "finish-args-home-filesystem-access" ] } ================================================ FILE: packaging/linux/flatpak/flathub.json ================================================ { "disable-external-data-checker": true } ================================================ FILE: packaging/linux/flatpak/modules/avahi.json ================================================ { "name": "avahi", "cleanup": [ "/bin", "/lib/avahi", "/share" ], "config-opts": [ "--with-distro=none", "--disable-gobject", "--disable-introspection", "--disable-qt3", "--disable-qt4", "--disable-qt5", "--disable-gtk", "--disable-core-docs", "--disable-manpages", "--disable-libdaemon", "--disable-python", "--disable-pygobject", "--disable-mono", "--disable-monodoc", "--disable-autoipd", "--disable-doxygen-doc", "--disable-doxygen-dot", "--disable-doxygen-xml", "--disable-doxygen-html", "--disable-manpages", "--disable-xmltoman", "--disable-libevent" ], "sources": [ { "type": "git", "url": "https://github.com/avahi/avahi.git", "commit": "f060abee2807c943821d88839c013ce15db17b58", "tag": "v0.8", "x-checker-data": { "type": "git", "tag-pattern": "^v([\\d.]+)$" } }, { "type": "shell", "commands": [ "autoreconf -ivf" ] } ] } ================================================ FILE: packaging/linux/flatpak/modules/boost.json ================================================ { "name": "boost", "buildsystem": "simple", "build-commands": [ "cd tools/build && bison -y -d -o src/engine/jamgram.cpp src/engine/jamgram.y", "./bootstrap.sh --prefix=$FLATPAK_DEST --with-libraries=filesystem,locale,log,program_options,system", "./b2 install variant=release link=shared runtime-link=shared cxxflags=\"$CXXFLAGS\"" ], "sources": [ { "type": "archive", "url": "https://github.com/boostorg/boost/releases/download/boost-1.88.0/boost-1.88.0-cmake.tar.xz", "sha256": "f48b48390380cfb94a629872346e3a81370dc498896f16019ade727ab72eb1ec" } ] } ================================================ FILE: packaging/linux/flatpak/modules/cuda.json ================================================ { "name": "cuda", "build-options": { "no_debuginfo": true }, "buildsystem": "simple", "cleanup": [ "*" ], "build-commands": [ "chmod u+x ./cuda.run", "./cuda.run --silent --toolkit --toolkitpath=$FLATPAK_DEST/cuda --no-opengl-libs --no-man-page --no-drm --tmpdir=$FLATPAK_BUILDER_BUILDDIR", "rm -r $FLATPAK_DEST/cuda/nsight-systems-*", "rm ./cuda.run" ], "sources": [ { "type": "file", "only-arches": [ "x86_64" ], "url": "https://developer.download.nvidia.com/compute/cuda/12.9.1/local_installers/cuda_12.9.1_575.57.08_linux.run", "sha256": "0f6d806ddd87230d2adbe8a6006a9d20144fdbda9de2d6acc677daa5d036417a", "dest-filename": "cuda.run" }, { "type": "file", "only-arches": [ "aarch64" ], "url": "https://developer.download.nvidia.com/compute/cuda/12.9.1/local_installers/cuda_12.9.1_575.57.08_linux_sbsa.run", "sha256": "64f47ab791a76b6889702425e0755385f5fa216c5a9f061875c7deed5f08cdb6", "dest-filename": "cuda.run" } ] } ================================================ FILE: packaging/linux/flatpak/modules/libevdev.json ================================================ { "name": "libevdev", "buildsystem": "meson", "config-opts": [ "-Ddocumentation=disabled", "-Dtests=disabled" ], "sources": [ { "type": "git", "url": "https://github.com/LizardByte-infrastructure/libevdev.git", "commit": "ac0056961c3332a260db063ab4fccc7747638a1d", "tag": "libevdev-1.13.4", "x-checker-data": { "type": "anitya", "project-id": 20540, "stable-only": true, "tag-template": "libevdev-$version" } } ], "cleanup": [ "/bin", "/include", "/lib/pkgconfig", "/share" ] } ================================================ FILE: packaging/linux/flatpak/modules/libnotify.json ================================================ { "name": "libnotify", "buildsystem": "meson", "config-opts": [ "-Dtests=false", "-Dintrospection=disabled", "-Dman=false", "-Dgtk_doc=false", "-Ddocbook_docs=disabled" ], "sources": [ { "type": "git", "url": "https://github.com/LizardByte-infrastructure/libnotify.git", "commit": "131aad01ff5f563b4863becbb6ed84dac6e75d5a", "tag": "0.8.6", "x-checker-data": { "type": "git", "tag-pattern": "^([\\d.]+)$" } } ] } ================================================ FILE: packaging/linux/flatpak/modules/miniupnpc.json ================================================ { "name": "miniupnpc", "buildsystem": "cmake-ninja", "builddir": true, "subdir": "miniupnpc", "config-opts": [ "-DCMAKE_BUILD_TYPE=RelWithDebInfo", "-DUPNPC_BUILD_STATIC=OFF", "-DUPNPC_BUILD_SHARED=ON", "-DUPNPC_BUILD_TESTS=OFF", "-DUPNPC_BUILD_SAMPLE=OFF" ], "sources": [ { "type": "git", "url": "https://github.com/miniupnp/miniupnp.git", "tag": "miniupnpc_2_3_3", "commit": "bf4215a7574f88aa55859db9db00e3ae58cf42d6", "x-checker-data": { "type": "anitya", "project-id": 1986, "stable-only": true, "tag-template": "miniupnpc_${version0}_${version1}_${version2}" } } ], "cleanup": [ "/share/man", "/lib/pkgconfig", "/lib/libminiupnpc.so", "/lib/cmake", "/include", "/bin/external-ip.sh" ] } ================================================ FILE: packaging/linux/flatpak/modules/nlohmann_json.json ================================================ { "name": "nlohmann_json", "buildsystem": "cmake-ninja", "config-opts": [ "-DJSON_MultipleHeaders=OFF", "-DJSON_BuildTests=OFF" ], "sources": [ { "type": "archive", "url": "https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz", "sha256": "d6c65aca6b1ed68e7a182f4757257b107ae403032760ed6ef121c9d55e81757d" } ] } ================================================ FILE: packaging/linux/flatpak/modules/numactl.json ================================================ { "name": "numactl", "sources": [ { "type": "git", "url": "https://github.com/numactl/numactl.git", "tag": "v2.0.19", "commit": "3bc85e37d5a30da6790cb7e8bb488bb8f679170f", "x-checker-data": { "type": "git", "tag-pattern": "^v([\\d.]+)$" } } ], "rm-configure": true, "cleanup": [ "/include", "/lib/pkgconfig", "/lib/*.a", "/lib/*.la", "/lib/*.so", "/share/man" ] } ================================================ FILE: packaging/linux/flatpak/modules/xvfb/xvfb-run ================================================ #!/bin/sh # This script starts an instance of Xvfb, the "fake" X server, runs a command # with that server available, and kills the X server when done. The return # value of the command becomes the return value of this script. # # If anyone is using this to build a Debian package, make sure the package # Build-Depends on xvfb and xauth. set -e PROGNAME=xvfb-run SERVERNUM=99 AUTHFILE= ERRORFILE=/dev/null XVFBARGS="-screen 0 1280x1024x24" LISTENTCP="-nolisten tcp" XAUTHPROTO=. # Query the terminal to establish a default number of columns to use for # displaying messages to the user. This is used only as a fallback in the event # the COLUMNS variable is not set. ($COLUMNS can react to SIGWINCH while the # script is running, and this cannot, only being calculated once.) DEFCOLUMNS=$(stty size 2>/dev/null | awk '{print $2}') || true case "$DEFCOLUMNS" in *[!0-9]*|'') DEFCOLUMNS=80 ;; esac # Display a message, wrapping lines at the terminal width. message () { echo "$PROGNAME: $*" | fmt -t -w ${COLUMNS:-$DEFCOLUMNS} } # Display an error message. error () { message "error: $*" >&2 } # Display a usage message. usage () { if [ -n "$*" ]; then message "usage error: $*" fi cat <>"$ERRORFILE" 2>&1 fi if [ -n "$XVFB_RUN_TMPDIR" ]; then if ! rm -r "$XVFB_RUN_TMPDIR"; then error "problem while cleaning up temporary directory" exit 5 fi fi if [ -n "$XVFBPID" ]; then kill "$XVFBPID" >>"$ERRORFILE" 2>&1 fi } # Parse the command line. ARGS=$(getopt --options +ae:f:hn:lp:s:w: \ --long auto-servernum,error-file:,auth-file:,help,server-num:,listen-tcp,xauth-protocol:,server-args:,wait: \ --name "$PROGNAME" -- "$@") GETOPT_STATUS=$? if [ $GETOPT_STATUS -ne 0 ]; then error "internal error; getopt exited with status $GETOPT_STATUS" exit 6 fi eval set -- "$ARGS" while :; do case "$1" in -a|--auto-servernum) SERVERNUM=$(find_free_servernum); AUTONUM="yes" ;; -e|--error-file) ERRORFILE="$2"; shift ;; -f|--auth-file) AUTHFILE="$2"; shift ;; -h|--help) SHOWHELP="yes" ;; -n|--server-num) SERVERNUM="$2"; shift ;; -l|--listen-tcp) LISTENTCP="" ;; -p|--xauth-protocol) XAUTHPROTO="$2"; shift ;; -s|--server-args) XVFBARGS="$2"; shift ;; -w|--wait) shift ;; --) shift; break ;; *) error "internal error; getopt permitted \"$1\" unexpectedly" exit 6 ;; esac shift done if [ "$SHOWHELP" ]; then usage exit 0 fi if [ -z "$*" ]; then usage "need a command to run" >&2 exit 2 fi if ! command -v xauth >/dev/null; then error "xauth command not found" exit 3 fi # tidy up after ourselves trap clean_up EXIT # If the user did not specify an X authorization file to use, set up a temporary # directory to house one. if [ -z "$AUTHFILE" ]; then XVFB_RUN_TMPDIR="$(mktemp -d -t $PROGNAME.XXXXXX)" AUTHFILE="$XVFB_RUN_TMPDIR/Xauthority" # Create empty file to avoid xauth warning touch "$AUTHFILE" fi # Start Xvfb. MCOOKIE=$(mcookie) tries=10 while [ $tries -gt 0 ]; do tries=$(( $tries - 1 )) XAUTHORITY=$AUTHFILE xauth source - << EOF >>"$ERRORFILE" 2>&1 add :$SERVERNUM $XAUTHPROTO $MCOOKIE EOF # handle SIGUSR1 so Xvfb knows to send a signal when it's ready to accept # connections trap : USR1 (trap '' USR1; exec Xvfb ":$SERVERNUM" $XVFBARGS $LISTENTCP -auth $AUTHFILE >>"$ERRORFILE" 2>&1) & XVFBPID=$! wait || : if kill -0 $XVFBPID 2>/dev/null; then break elif [ -n "$AUTONUM" ]; then # The display is in use so try another one (if '-a' was specified). SERVERNUM=$((SERVERNUM + 1)) SERVERNUM=$(find_free_servernum) continue fi error "Xvfb failed to start" >&2 XVFBPID= exit 1 done # Start the command and save its exit status. set +e DISPLAY=:$SERVERNUM XAUTHORITY=$AUTHFILE "$@" RETVAL=$? set -e # Return the executed command's exit status. exit $RETVAL # vim:set ai et sts=4 sw=4 tw=80: ================================================ FILE: packaging/linux/flatpak/modules/xvfb/xvfb.json ================================================ { "name": "xvfb", "buildsystem": "meson", "config-opts": [ "-Dxorg=true", "-Dxvfb=true", "-Dhal=false" ], "build-commands": [ "install -Dm755 ../xvfb-run /app/bin/xvfb-run" ], "sources": [ { "type": "git", "url": "https://github.com/LizardByte-infrastructure/xserver.git", "tag": "xorg-server-21.1.13", "commit": "be2767845d6ed3c6dbd25a151051294d0908a995", "x-checker-data": { "type": "anitya", "project-id": 5250, "stable-only": true, "tag-template": "xorg-server-$version" } }, { "type": "file", "path": "xvfb-run" } ], "modules": [ { "name": "libxcvt", "buildsystem": "meson", "sources": [ { "type": "git", "url": "https://github.com/LizardByte-infrastructure/libxcvt.git", "tag": "libxcvt-0.1.2", "commit": "d9ca87eea9eecddaccc3a77227bcb3acf84e89df", "x-checker-data": { "type": "anitya", "project-id": 235147, "stable-only": true, "tag-template": "libxcvt-$version" } } ] }, { "name": "libXmu", "sources": [ { "type": "git", "url": "https://github.com/LizardByte-infrastructure/libxmu.git", "tag": "libXmu-1.2.1", "commit": "792f80402ee06ce69bca3a8f2a84295999c3a170", "x-checker-data": { "type": "anitya", "project-id": 1785, "stable-only": true, "tag-template": "libXmu-$version" } } ] }, { "name": "font-util", "sources": [ { "type": "git", "url": "https://github.com/LizardByte-infrastructure/font-util.git", "tag": "font-util-1.4.1", "commit": "b5ca142f81a6f14eddb23be050291d1c25514777", "x-checker-data": { "type": "anitya", "project-id": 15055, "stable-only": true, "tag-template": "font-util-$version" } } ] }, { "name": "libfontenc", "sources": [ { "type": "git", "url": "https://github.com/LizardByte-infrastructure/libfontenc.git", "tag": "libfontenc-1.1.8", "commit": "92a85fda2acb4e14ec0b2f6d8fe3eaf2b687218c", "x-checker-data": { "type": "anitya", "project-id": 1613, "stable-only": true, "tag-template": "libfontenc-$version" } } ] }, { "name": "libtirpc", "config-opts": [ "--disable-gssapi" ], "sources": [ { "type": "archive", "url": "https://downloads.sourceforge.net/sourceforge/libtirpc/libtirpc-1.3.4.tar.bz2", "sha256": "1e0b0c7231c5fa122e06c0609a76723664d068b0dba3b8219b63e6340b347860", "x-checker-data": { "type": "anitya", "project-id": 1740, "stable-only": true, "url-template": "https://downloads.sourceforge.net/sourceforge/libtirpc/libtirpc-$version.tar.bz2" } } ] }, { "name": "xvfb-libXfont2", "sources": [ { "type": "git", "url": "https://github.com/LizardByte-infrastructure/libxfont.git", "tag": "libXfont2-2.0.6", "commit": "d54aaf2483df6a1f98fadc09004157e657b7f73e", "x-checker-data": { "type": "anitya", "project-id": 17165, "stable-only": true, "tag-template": "libXfont2-$version" } } ] }, { "name": "xvfb-xauth", "sources": [ { "type": "git", "url": "https://github.com/LizardByte-infrastructure/xauth.git", "tag": "xauth-1.1.3", "commit": "c29eef23683f0e3575a3c60d9314de8156fbe2c2", "x-checker-data": { "type": "anitya", "project-id": 5253, "stable-only": true, "tag-template": "xauth-$version" } } ] } ] } ================================================ FILE: packaging/linux/flatpak/scripts/additional-install.sh ================================================ #!/bin/sh # User Service mkdir -p ~/.config/systemd/user cp "/app/share/sunshine/systemd/user/sunshine.service" "$HOME/.config/systemd/user/sunshine.service" echo "Sunshine User Service has been installed." echo "Use [systemctl --user enable sunshine] once to autostart Sunshine on login." # Load uhid (DS5 emulation) UHID=$(cat /app/share/sunshine/modules-load.d/60-sunshine.conf) echo "Enabling DS5 emulation." flatpak-spawn --host pkexec sh -c "echo '$UHID' > /etc/modules-load.d/60-sunshine.conf" flatpak-spawn --host pkexec modprobe uhid # Udev rule UDEV=$(cat /app/share/sunshine/udev/rules.d/60-sunshine.rules) echo "Configuring mouse permission." flatpak-spawn --host pkexec sh -c "echo '$UDEV' > /etc/udev/rules.d/60-sunshine.rules" echo "Restart computer for mouse permission to take effect." ================================================ FILE: packaging/linux/flatpak/scripts/remove-additional-install.sh ================================================ #!/bin/sh # User Service systemctl --user stop sunshine rm "$HOME/.config/systemd/user/sunshine.service" systemctl --user daemon-reload echo "Sunshine User Service has been removed." # Remove rules flatpak-spawn --host pkexec sh -c "rm /etc/modules-load.d/60-sunshine.conf" flatpak-spawn --host pkexec sh -c "rm /etc/udev/rules.d/60-sunshine.rules" echo "Input rules removed. Restart computer to take effect." ================================================ FILE: packaging/linux/flatpak/scripts/sunshine.sh ================================================ #!/bin/sh PORT=47990 if ! curl -k https://localhost:$PORT > /dev/null 2>&1; then (sleep 3 && xdg-open https://localhost:$PORT) & exec sunshine "$@" else echo "Sunshine is already running, opening the web interface..." xdg-open https://localhost:$PORT fi ================================================ FILE: packaging/linux/patches/aarch64/01-math_functions.patch ================================================ diff '--color=auto' -ur a/cuda/targets/sbsa-linux/include/crt/math_functions.h b/cuda/targets/sbsa-linux/include/crt/math_functions.h --- a/cuda/targets/sbsa-linux/include/crt/math_functions.h 2024-08-23 00:25:39.000000000 +0200 +++ b/cuda/targets/sbsa-linux/include/crt/math_functions.h 2025-02-17 01:19:44.270292640 +0100 @@ -2553,7 +2553,7 @@ * * \note_accuracy_double */ -extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double sinpi(double x); +extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double sinpi(double x) noexcept (true); /** * \ingroup CUDA_MATH_SINGLE * \brief Calculate the sine of the input argument @@ -2576,7 +2576,7 @@ * * \note_accuracy_single */ -extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float sinpif(float x); +extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float sinpif(float x) noexcept (true); /** * \ingroup CUDA_MATH_DOUBLE * \brief Calculate the cosine of the input argument @@ -2598,7 +2598,7 @@ * * \note_accuracy_double */ -extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double cospi(double x); +extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double cospi(double x) noexcept (true); /** * \ingroup CUDA_MATH_SINGLE * \brief Calculate the cosine of the input argument @@ -2620,7 +2620,7 @@ * * \note_accuracy_single */ -extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float cospif(float x); +extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float cospif(float x) noexcept (true); /** * \ingroup CUDA_MATH_DOUBLE * \brief Calculate the sine and cosine of the first input argument ================================================ FILE: packaging/linux/patches/x86_64/01-math_functions.patch ================================================ diff '--color=auto' -ur a/cuda/targets/x86_64-linux/include/crt/math_functions.h b/cuda/targets/x86_64-linux/include/crt/math_functions.h --- a/cuda/targets/x86_64-linux/include/crt/math_functions.h 2024-08-23 00:25:39.000000000 +0200 +++ b/cuda/targets/x86_64-linux/include/crt/math_functions.h 2025-02-17 01:19:44.270292640 +0100 @@ -2553,7 +2553,7 @@ * * \note_accuracy_double */ -extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double sinpi(double x); +extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double sinpi(double x) noexcept (true); /** * \ingroup CUDA_MATH_SINGLE * \brief Calculate the sine of the input argument @@ -2576,7 +2576,7 @@ * * \note_accuracy_single */ -extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float sinpif(float x); +extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float sinpif(float x) noexcept (true); /** * \ingroup CUDA_MATH_DOUBLE * \brief Calculate the cosine of the input argument @@ -2598,7 +2598,7 @@ * * \note_accuracy_double */ -extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double cospi(double x); +extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double cospi(double x) noexcept (true); /** * \ingroup CUDA_MATH_SINGLE * \brief Calculate the cosine of the input argument @@ -2620,7 +2620,7 @@ * * \note_accuracy_single */ -extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float cospif(float x); +extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float cospif(float x) noexcept (true); /** * \ingroup CUDA_MATH_DOUBLE * \brief Calculate the sine and cosine of the first input argument ================================================ FILE: packaging/linux/sunshine.service.in ================================================ [Unit] Description=@PROJECT_DESCRIPTION@ StartLimitIntervalSec=500 StartLimitBurst=5 [Service] # Avoid starting Sunshine before the desktop is fully initialized. ExecStartPre=/bin/sleep 5 @SUNSHINE_SERVICE_START_COMMAND@ @SUNSHINE_SERVICE_STOP_COMMAND@ Restart=on-failure RestartSec=5s [Install] WantedBy=xdg-desktop-autostart.target ================================================ FILE: packaging/sunshine.rb ================================================ require "language/node" class @PROJECT_NAME@ < Formula # conflicts_with "sunshine", because: "sunshine and sunshine-beta cannot be installed at the same time" desc "@PROJECT_DESCRIPTION@" homepage "@PROJECT_HOMEPAGE_URL@" url "@GITHUB_CLONE_URL@", tag: "@GITHUB_TAG@" version "@BUILD_VERSION@" license all_of: ["GPL-3.0-only"] head "@GITHUB_CLONE_URL@", branch: "@GITHUB_DEFAULT_BRANCH@" # https://docs.brew.sh/Brew-Livecheck#githublatest-strategy-block livecheck do url :stable regex(/^v?(\d+\.\d+\.\d+)$/i) strategy :github_latest do |json, regex| match = json["tag_name"]&.match(regex) next if match.blank? match[1] end end option "with-docs", "Enable docs" option "with-static-boost", "Enable static link of Boost libraries" option "without-static-boost", "Disable static link of Boost libraries" # default option depends_on "cmake" => :build depends_on "doxygen" => :build depends_on "graphviz" => :build depends_on "node" => :build depends_on "pkgconf" => :build depends_on "curl" depends_on "miniupnpc" depends_on "openssl" depends_on "opus" depends_on "icu4c" => :recommended on_linux do depends_on "avahi" depends_on "gnu-which" depends_on "libayatana-appindicator" depends_on "libcap" depends_on "libdrm" depends_on "libnotify" depends_on "libva" depends_on "libx11" depends_on "libxcb" depends_on "libxcursor" depends_on "libxfixes" depends_on "libxi" depends_on "libxinerama" depends_on "libxrandr" depends_on "libxtst" depends_on "mesa" depends_on "numactl" depends_on "pulseaudio" depends_on "systemd" depends_on "wayland" end fails_with :clang do build 1400 cause "Requires C++23 support" end fails_with :gcc do version "12" # fails with GCC 12.x and earlier cause "Requires C++23 support" end def install ENV["BRANCH"] = "@GITHUB_BRANCH@" ENV["BUILD_VERSION"] = "@BUILD_VERSION@" ENV["COMMIT"] = "@GITHUB_COMMIT@" args = %W[ -DBUILD_WERROR=ON -DCMAKE_CXX_STANDARD=23 -DCMAKE_INSTALL_PREFIX=#{prefix} -DHOMEBREW_ALLOW_FETCHCONTENT=ON -DOPENSSL_ROOT_DIR=#{Formula["openssl"].opt_prefix} -DSUNSHINE_ASSETS_DIR=sunshine/assets -DSUNSHINE_BUILD_HOMEBREW=ON -DSUNSHINE_PUBLISHER_NAME='LizardByte' -DSUNSHINE_PUBLISHER_WEBSITE='https://app.lizardbyte.dev' -DSUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support' ] if build.with? "docs" ohai "Building docs: enabled" args << "-DBUILD_DOCS=ON" else ohai "Building docs: disabled" args << "-DBUILD_DOCS=OFF" end if build.without? "static-boost" args << "-DBOOST_USE_STATIC=OFF" ohai "Disabled statically linking Boost libraries" else args << "-DBOOST_USE_STATIC=ON" ohai "Enabled statically linking Boost libraries" unless Formula["icu4c"].any_version_installed? odie <<~EOS icu4c must be installed to link against static Boost libraries, either install icu4c or use brew install sunshine --with-static-boost instead EOS end ENV.append "CXXFLAGS", "-I#{Formula["icu4c"].opt_include}" icu4c_lib_path = Formula["icu4c"].opt_lib.to_s ENV.append "LDFLAGS", "-L#{icu4c_lib_path}" ENV["LIBRARY_PATH"] = icu4c_lib_path ohai "Linking against ICU libraries at: #{icu4c_lib_path}" end args << "-DCUDA_FAIL_ON_MISSING=OFF" if OS.linux? system "cmake", "-S", ".", "-B", "build", "-G", "Unix Makefiles", *std_cmake_args, *args system "make", "-C", "build" system "make", "-C", "build", "install" bin.install "build/tests/test_sunshine" # codesign the binary on intel macs system "codesign", "-s", "-", "--force", "--deep", bin/"sunshine" if OS.mac? && Hardware::CPU.intel? bin.install "src_assets/linux/misc/postinst" if OS.linux? end service do run [opt_bin/"sunshine", "~/.config/sunshine/sunshine.conf"] end def post_install if OS.linux? opoo <<~EOS ATTENTION: To complete installation, you must run the following command: `sudo #{bin}/postinst` EOS end if OS.mac? opoo <<~EOS Sunshine can only access microphones on macOS due to system limitations. To stream system audio use "Soundflower" or "BlackHole". Gamepads are not currently supported on macOS. EOS end end def caveats <<~EOS Thanks for installing @PROJECT_NAME@! To get started, review the documentation at: https://docs.lizardbyte.dev/projects/sunshine EOS end test do # test that the binary runs at all system bin/"sunshine", "--version" # run the test suite system bin/"test_sunshine", "--gtest_color=yes", "--gtest_output=xml:test_results.xml" assert_path_exists testpath/"test_results.xml" end end ================================================ FILE: scripts/_locale.py ================================================ """ .. _locale.py Functions related to building, initializing, updating, and compiling localization translations. Borrowed from RetroArcher. """ # standard imports import argparse import datetime import os import subprocess project_name = 'Sunshine' project_owner = 'LizardByte' script_dir = os.path.dirname(os.path.abspath(__file__)) root_dir = os.path.dirname(script_dir) locale_dir = os.path.join(root_dir, 'locale') project_dir = os.path.join(root_dir, 'src') year = datetime.datetime.now().year # target locales target_locales = [ 'bg', # Bulgarian 'cs', # Czech 'de', # German 'en', # English 'en_GB', # English (United Kingdom) 'en_US', # English (United States) 'es', # Spanish 'fr', # French 'it', # Italian 'ja', # Japanese 'ko', # Korean 'pl', # Polish 'pt', # Portuguese 'pt_BR', # Portuguese (Brazil) 'ru', # Russian 'sv', # Swedish 'tr', # Turkish 'uk', # Ukrainian 'zh', # Chinese 'zh_TW', # Chinese (Traditional) ] def x_extract(): """Executes `xgettext extraction` in subprocess.""" pot_filepath = os.path.join(locale_dir, f'{project_name.lower()}.po') commands = [ 'xgettext', '--keyword=translate:1,1t', '--keyword=translate:1c,2,2t', '--keyword=translate:1,2,3t', '--keyword=translate:1c,2,3,4t', '--keyword=gettext:1', '--keyword=pgettext:1c,2', '--keyword=ngettext:1,2', '--keyword=npgettext:1c,2,3', f'--default-domain={project_name.lower()}', f'--output={pot_filepath}', '--language=C++', '--boost', '--from-code=utf-8', '-F', f'--msgid-bugs-address=github.com/{project_owner.lower()}/{project_name.lower()}', f'--copyright-holder={project_owner}', f'--package-name={project_name}', '--package-version=v0' ] extensions = ['cpp', 'h', 'm', 'mm'] # find input files for root, dirs, files in os.walk(project_dir, topdown=True): for name in files: filename = os.path.join(root, name) extension = filename.rsplit('.', 1)[-1] if extension in extensions: # append input files commands.append(filename) print(commands) subprocess.check_output(args=commands, cwd=root_dir) try: # fix header body = "" with open(file=pot_filepath, mode='r') as file: for line in file.readlines(): if line != '"Language: \\n"\n': # do not include this line if line == '# SOME DESCRIPTIVE TITLE.\n': body += f'# Translations template for {project_name}.\n' elif line.startswith('#') and 'YEAR' in line: body += line.replace('YEAR', str(year)) elif line.startswith('#') and 'PACKAGE' in line: body += line.replace('PACKAGE', project_name) else: body += line # rewrite pot file with updated header with open(file=pot_filepath, mode='w+') as file: file.write(body) except FileNotFoundError: pass def babel_init(locale_code: str): """Executes `pybabel init` in subprocess. :param locale_code: str - locale code """ commands = [ 'pybabel', 'init', '-i', os.path.join(locale_dir, f'{project_name.lower()}.po'), '-d', locale_dir, '-D', project_name.lower(), '-l', locale_code ] print(commands) subprocess.check_output(args=commands, cwd=root_dir) def babel_update(): """Executes `pybabel update` in subprocess.""" commands = [ 'pybabel', 'update', '-i', os.path.join(locale_dir, f'{project_name.lower()}.po'), '-d', locale_dir, '-D', project_name.lower(), '--update-header-comment' ] print(commands) subprocess.check_output(args=commands, cwd=root_dir) def babel_compile(): """Executes `pybabel compile` in subprocess.""" commands = [ 'pybabel', 'compile', '-d', locale_dir, '-D', project_name.lower() ] print(commands) subprocess.check_output(args=commands, cwd=root_dir) if __name__ == '__main__': # Set up and gather command line arguments parser = argparse.ArgumentParser( description='Script helps update locale translations. Translations must be done manually.') parser.add_argument('--extract', action='store_true', help='Extract messages from c++ files.') parser.add_argument('--init', action='store_true', help='Initialize any new locales specified in target locales.') parser.add_argument('--update', action='store_true', help='Update existing locales.') parser.add_argument('--compile', action='store_true', help='Compile translated locales.') args = parser.parse_args() if args.extract: x_extract() if args.init: for locale_id in target_locales: if not os.path.isdir(os.path.join(locale_dir, locale_id)): babel_init(locale_code=locale_id) if args.update: babel_update() if args.compile: babel_compile() ================================================ FILE: scripts/icons/convert_and_pack.sh ================================================ #!/bin/bash if ! [ -x "$(command -v ./go-png2ico)" ]; then echo "./go-png2ico not found" echo "download the executable from https://github.com/J-Siu/go-png2ico" echo "and drop it in this folder" exit 1 fi if ! [ -x "$(command -v ./oxipng)" ]; then echo "./oxipng executable not found" echo "download the executable from https://github.com/shssoichiro/oxipng" echo "and drop it in this folder" exit 1 fi if ! [ -x "$(command -v inkscape)" ]; then echo "inkscape executable not found" exit 1 fi icon_base_sizes=(16 64) icon_sizes_keys=() # associative array to prevent duplicates icon_sizes_keys[256]=1 for icon_base_size in "${icon_base_sizes[@]}"; do # increment in 25% till 400% icon_size_increment=$((icon_base_size / 4)) for ((i = 0; i <= 12; i++)); do icon_sizes_keys[icon_base_size + i * icon_size_increment]=1 done done # convert to normal array icon_sizes=("${!icon_sizes_keys[@]}") echo "using icon sizes:" # shellcheck disable=SC2068 # intentionally word split echo ${icon_sizes[@]} src_vectors=("../../src_assets/common/assets/web/public/images/apollo-locked.svg" "../../src_assets/common/assets/web/public/images/apollo-pausing.svg" "../../src_assets/common/assets/web/public/images/apollo-playing.svg" "../../apollo.svg") echo "using sources vectors:" # shellcheck disable=SC2068 # intentionally word split echo ${src_vectors[@]} for src_vector in "${src_vectors[@]}"; do file_name=$(basename "${src_vector}" .svg) png_files=() for icon_size in "${icon_sizes[@]}"; do png_file="${file_name}${icon_size}.png" echo "converting ${png_file}" inkscape -w "${icon_size}" -h "${icon_size}" "${src_vector}" --export-filename "${png_file}" && ./oxipng -o max --strip safe --alpha "${png_file}" && png_files+=("${png_file}") done echo "packing ${file_name}.ico" ./go-png2ico "${png_files[@]}" "${file_name}.ico" done ================================================ FILE: scripts/linux_build.sh ================================================ #!/bin/bash set -e # Version requirements - centralized for easy maintenance cmake_min="3.25.0" target_cmake_version="3.30.1" doxygen_min="1.10.0" _doxygen_min="${doxygen_min//\./_}" # Convert dots to underscores for URL doxygen_max="1.12.0" # Default value for arguments appimage_build=0 cuda_patches=0 num_processors=$(nproc) publisher_name="Third Party Publisher" publisher_website="" publisher_issue_url="https://app.lizardbyte.dev/support" skip_cleanup=0 skip_cuda=0 skip_libva=0 skip_package=0 sudo_cmd="sudo" ubuntu_test_repo=0 step="all" # common variables gcc_alternative_files=( "gcc" "g++" "gcov" "gcc-ar" "gcc-ranlib" ) # Reusable function to detect nvcc path function detect_nvcc_path() { local nvcc_path="" # First check for system-installed CUDA nvcc_path=$(command -v nvcc 2>/dev/null) || true if [ -n "$nvcc_path" ]; then echo "$nvcc_path" return 0 fi # Then check for locally installed CUDA in build directory if [ -f "${build_dir}/cuda/bin/nvcc" ]; then echo "${build_dir}/cuda/bin/nvcc" return 0 fi # No CUDA found return 1 } # Reusable function to setup NVM environment function setup_nvm_environment() { # Only setup NVM if it should be used for this distro if [ "$nvm_node" == 1 ]; then # Check if NVM is installed and source it if [ -f "$HOME/.nvm/nvm.sh" ]; then # shellcheck source=/dev/null source "$HOME/.nvm/nvm.sh" # Use the default node version installed by NVM nvm use default 2>/dev/null || nvm use node 2>/dev/null || true echo "Using NVM Node.js version: $(node --version 2>/dev/null || echo 'not available')" echo "Using NVM npm version: $(npm --version 2>/dev/null || echo 'not available')" else echo "NVM not found, using system Node.js if available" fi fi } function _usage() { local exit_code=$1 cat <&2 _usage 1 ;; esac ;; \? ) echo "Invalid option: -${OPTARG}" 1>&2 _usage 1 ;; esac done shift $((OPTIND -1)) # dependencies array to build out dependencies=() function add_arch_deps() { dependencies+=( 'avahi' 'base-devel' 'cmake' 'curl' 'doxygen' "gcc${gcc_version}" "gcc${gcc_version}-libs" 'git' 'graphviz' 'libayatana-appindicator' 'libcap' 'libdrm' 'libevdev' 'libmfx' 'libnotify' 'libpulse' 'libva' 'libx11' 'libxcb' 'libxfixes' 'libxrandr' 'libxtst' 'miniupnpc' 'ninja' 'nodejs' 'npm' 'numactl' 'openssl' 'opus' 'udev' 'wayland' ) if [ "$skip_libva" == 0 ]; then dependencies+=( "libva" # VA-API ) fi if [ "$skip_cuda" == 0 ]; then dependencies+=( "cuda" # VA-API ) fi } function add_debian_based_deps() { dependencies+=( "appstream" "appstream-util" "bison" # required if we need to compile doxygen "build-essential" "cmake" "desktop-file-utils" "doxygen" "flex" # required if we need to compile doxygen "gcc-${gcc_version}" "g++-${gcc_version}" "git" "graphviz" "libcap-dev" # KMS "libcurl4-openssl-dev" "libdrm-dev" # KMS "libevdev-dev" "libgbm-dev" "libminiupnpc-dev" "libnotify-dev" "libnuma-dev" "libopus-dev" "libpulse-dev" "libssl-dev" "libwayland-dev" # Wayland "libx11-dev" # X11 "libxcb-shm0-dev" # X11 "libxcb-xfixes0-dev" # X11 "libxcb1-dev" # X11 "libxfixes-dev" # X11 "libxrandr-dev" # X11 "libxtst-dev" # X11 "ninja-build" "npm" # web-ui "udev" "wget" # necessary for cuda install with `run` file "xvfb" # necessary for headless unit testing ) if [ "$skip_libva" == 0 ]; then dependencies+=( "libva-dev" # VA-API ) fi } function add_test_ppa() { if [ "$ubuntu_test_repo" == 1 ]; then $package_install_command "software-properties-common" ${sudo_cmd} add-apt-repository ppa:ubuntu-toolchain-r/test -y fi } function add_debian_deps() { add_test_ppa add_debian_based_deps dependencies+=( "libayatana-appindicator3-dev" ) } function add_ubuntu_deps() { add_test_ppa add_debian_based_deps dependencies+=( "libappindicator3-dev" ) } function add_fedora_deps() { dependencies+=( "appstream" "cmake" "desktop-file-utils" "doxygen" "gcc${gcc_version}" "gcc${gcc_version}-c++" "git" "graphviz" "libappindicator-gtk3-devel" "libappstream-glib" "libcap-devel" "libcurl-devel" "libdrm-devel" "libevdev-devel" "libnotify-devel" "libX11-devel" # X11 "libxcb-devel" # X11 "libXcursor-devel" # X11 "libXfixes-devel" # X11 "libXi-devel" # X11 "libXinerama-devel" # X11 "libXrandr-devel" # X11 "libXtst-devel" # X11 "mesa-libGL-devel" "mesa-libgbm-devel" "miniupnpc-devel" "ninja-build" "npm" "numactl-devel" "openssl-devel" "opus-devel" "pulseaudio-libs-devel" "rpm-build" # if you want to build an RPM binary package "wget" # necessary for cuda install with `run` file "which" # necessary for cuda install with `run` file "xorg-x11-server-Xvfb" # necessary for headless unit testing ) if [ "$skip_libva" == 0 ]; then dependencies+=( "libva-devel" # VA-API ) fi } function install_cuda() { # Check if CUDA is already available if detect_nvcc_path > /dev/null 2>&1; then return fi local cuda_prefix="https://developer.download.nvidia.com/compute/cuda/" local cuda_suffix="" if [ "$architecture" == "aarch64" ]; then local cuda_suffix="_sbsa" fi if [ "$architecture" == "aarch64" ]; then # we need to patch the math-vector.h file for aarch64 fedora # back up /usr/include/bits/math-vector.h math_vector_file="" if [ "$distro" == "ubuntu" ] || [ "$version" == "24.04" ]; then math_vector_file="/usr/include/aarch64-linux-gnu/bits/math-vector.h" elif [ "$distro" == "fedora" ]; then math_vector_file="/usr/include/bits/math-vector.h" fi if [ -n "$math_vector_file" ]; then # patch headers https://bugs.launchpad.net/ubuntu/+source/mumax3/+bug/2032624 ${sudo_cmd} cp "$math_vector_file" "$math_vector_file.bak" ${sudo_cmd} sed -i 's/__Float32x4_t/int/g' "$math_vector_file" ${sudo_cmd} sed -i 's/__Float64x2_t/int/g' "$math_vector_file" ${sudo_cmd} sed -i 's/__SVFloat32_t/float/g' "$math_vector_file" ${sudo_cmd} sed -i 's/__SVFloat64_t/float/g' "$math_vector_file" ${sudo_cmd} sed -i 's/__SVBool_t/int/g' "$math_vector_file" fi fi local url="${cuda_prefix}${cuda_version}/local_installers/cuda_${cuda_version}_${cuda_build}_linux${cuda_suffix}.run" echo "cuda url: ${url}" wget "$url" --progress=bar:force:noscroll -q --show-progress -O "${build_dir}/cuda.run" chmod a+x "${build_dir}/cuda.run" "${build_dir}/cuda.run" --silent --toolkit --toolkitpath="${build_dir}/cuda" --no-opengl-libs --no-man-page --no-drm rm "${build_dir}/cuda.run" # run cuda patches if [ "$cuda_patches" == 1 ]; then echo "Applying CUDA patches" local patch_dir="${script_dir}/../packaging/linux/patches/${architecture}" if [ -d "$patch_dir" ]; then for patch in "$patch_dir"/*.patch; do echo "Applying patch: $patch" patch -p2 \ --backup \ --directory="${build_dir}/cuda" \ --verbose \ < "$patch" done else echo "No patches found for architecture: $architecture" fi fi } function check_version() { local package_name=$1 local min_version=$2 local max_version=$3 local installed_version echo "Checking if $package_name is installed and at least version $min_version" if [ "$distro" == "debian" ] || [ "$distro" == "ubuntu" ]; then installed_version=$(dpkg -s "$package_name" 2>/dev/null | grep '^Version:' | awk '{print $2}') elif [ "$distro" == "fedora" ]; then installed_version=$(rpm -q --queryformat '%{VERSION}' "$package_name" 2>/dev/null) elif [ "$distro" == "arch" ]; then installed_version=$(pacman -Q "$package_name" | awk '{print $2}' ) else echo "Unsupported Distro" return 1 fi if [ -z "$installed_version" ]; then echo "Package not installed" return 1 fi if [[ "$(printf '%s\n' "$installed_version" "$min_version" | sort -V | head -n1)" = "$min_version" ]] && \ [[ "$(printf '%s\n' "$installed_version" "$max_version" | sort -V | head -n1)" = "$installed_version" ]]; then echo "Installed version is within range" return 0 else echo "$package_name version $installed_version is out of range" return 1 fi } function run_step_deps() { echo "Running step: Install dependencies" # Update the package list $package_update_command if [ "$distro" == "arch" ]; then add_arch_deps elif [ "$distro" == "debian" ]; then add_debian_deps elif [ "$distro" == "ubuntu" ]; then add_ubuntu_deps elif [ "$distro" == "fedora" ]; then add_fedora_deps ${sudo_cmd} dnf group install "$dev_tools_group" -y fi # Install the dependencies $package_install_command "${dependencies[@]}" # reload the environment # shellcheck source=/dev/null source ~/.bashrc #set gcc version based on distros if [ "$distro" == "arch" ]; then export CC=gcc-14 export CXX=g++-14 elif [ "$distro" == "debian" ] || [ "$distro" == "ubuntu" ]; then for file in "${gcc_alternative_files[@]}"; do file_path="/etc/alternatives/$file" if [ -e "$file_path" ]; then ${sudo_cmd} mv "$file_path" "$file_path.bak" fi done ${sudo_cmd} update-alternatives --install \ /usr/bin/gcc gcc /usr/bin/gcc-${gcc_version} 100 \ --slave /usr/bin/g++ g++ /usr/bin/g++-${gcc_version} \ --slave /usr/bin/gcov gcov /usr/bin/gcov-${gcc_version} \ --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-${gcc_version} \ --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-${gcc_version} fi # compile cmake if the version is too low if ! check_version "cmake" "$cmake_min" "inf"; then cmake_prefix="https://github.com/Kitware/CMake/releases/download/v" if [ "$architecture" == "x86_64" ]; then cmake_arch="x86_64" elif [ "$architecture" == "aarch64" ]; then cmake_arch="aarch64" fi url="${cmake_prefix}${target_cmake_version}/cmake-${target_cmake_version}-linux-${cmake_arch}.sh" echo "cmake url: ${url}" wget "$url" --progress=bar:force:noscroll -q --show-progress -O "${build_dir}/cmake.sh" ${sudo_cmd} sh "${build_dir}/cmake.sh" --skip-license --prefix=/usr/local echo "cmake installed, version:" cmake --version fi # compile doxygen if version is too low if ! check_version "doxygen" "$doxygen_min" "$doxygen_max"; then if [ "${SUNSHINE_COMPILE_DOXYGEN}" == "true" ]; then echo "Compiling doxygen" doxygen_url="https://github.com/doxygen/doxygen/releases/download/Release_${_doxygen_min}/doxygen-${doxygen_min}.src.tar.gz" echo "doxygen url: ${doxygen_url}" pushd "${build_dir}" wget "$doxygen_url" --progress=bar:force:noscroll -q --show-progress -O "doxygen.tar.gz" tar -xzf "doxygen.tar.gz" cd "doxygen-${doxygen_min}" cmake -DCMAKE_BUILD_TYPE=Release -G="Ninja" -B="build" -S="." ninja -C "build" -j"${num_processors}" ${sudo_cmd} ninja -C "build" install popd else echo "Doxygen version not in range, skipping docs" # Note: cmake_args will be set in cmake step fi fi # install node from nvm if [ "$nvm_node" == 1 ]; then nvm_url="https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh" echo "nvm url: ${nvm_url}" wget -qO- ${nvm_url} | bash # shellcheck source=/dev/null # we don't care that shellcheck cannot find nvm.sh source "$HOME/.nvm/nvm.sh" nvm install node nvm use node fi # run the cuda install if [ "$skip_cuda" == 0 ]; then install_cuda fi } function run_step_cmake() { echo "Running step: CMake configure" # Setup NVM environment if needed (for web UI builds) setup_nvm_environment # Detect CUDA path using the reusable function nvcc_path="" if [ "$skip_cuda" == 0 ]; then nvcc_path=$(detect_nvcc_path) fi # prepare CMAKE args cmake_args=( "-B=build" "-G=Ninja" "-S=." "-DBUILD_WERROR=ON" "-DCMAKE_BUILD_TYPE=Release" "-DCMAKE_INSTALL_PREFIX=/usr" "-DSUNSHINE_ASSETS_DIR=share/sunshine" "-DSUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine" "-DSUNSHINE_ENABLE_WAYLAND=ON" "-DSUNSHINE_ENABLE_X11=ON" "-DSUNSHINE_ENABLE_DRM=ON" ) if [ "$appimage_build" == 1 ]; then cmake_args+=("-DSUNSHINE_BUILD_APPIMAGE=ON") fi # Publisher metadata if [ -n "$publisher_name" ]; then cmake_args+=("-DSUNSHINE_PUBLISHER_NAME='${publisher_name}'") fi if [ -n "$publisher_website" ]; then cmake_args+=("-DSUNSHINE_PUBLISHER_WEBSITE='${publisher_website}'") fi if [ -n "$publisher_issue_url" ]; then cmake_args+=("-DSUNSHINE_PUBLISHER_ISSUE_URL='${publisher_issue_url}'") fi # Handle doxygen docs flag if ! check_version "doxygen" "$doxygen_min" "$doxygen_max"; then if [ "${SUNSHINE_COMPILE_DOXYGEN}" != "true" ]; then cmake_args+=("-DBUILD_DOCS=OFF") fi fi # Handle CUDA if [ "$skip_cuda" == 0 ]; then cmake_args+=("-DSUNSHINE_ENABLE_CUDA=ON") if [ -n "$nvcc_path" ]; then cmake_args+=("-DCMAKE_CUDA_COMPILER:PATH=$nvcc_path") fi else cmake_args+=("-DSUNSHINE_ENABLE_CUDA=OFF") fi # Cmake stuff here mkdir -p "build" echo "cmake args:" echo "${cmake_args[@]}" cmake "${cmake_args[@]}" } function run_step_validation() { echo "Running step: Validation" # Run appstream validation, etc. appstreamcli validate "build/dev.lizardbyte.app.Sunshine.metainfo.xml" appstream-util validate "build/dev.lizardbyte.app.Sunshine.metainfo.xml" desktop-file-validate "build/dev.lizardbyte.app.Sunshine.desktop" if [ "$appimage_build" == 0 ]; then desktop-file-validate "build/dev.lizardbyte.app.Sunshine.terminal.desktop" fi } function run_step_build() { echo "Running step: Build" # Setup NVM environment if needed (for web UI builds) setup_nvm_environment # Build the project ninja -C "build" } function run_step_package() { echo "Running step: Package" # Create the package if [ "$skip_package" == 0 ]; then if [ "$distro" == "debian" ] || [ "$distro" == "ubuntu" ]; then cpack -G DEB --config ./build/CPackConfig.cmake elif [ "$distro" == "fedora" ]; then cpack -G RPM --config ./build/CPackConfig.cmake fi fi } function run_step_cleanup() { echo "Running step: Cleanup" if [ "$skip_cleanup" == 0 ]; then # Restore the original gcc alternatives if [ "$distro" == "debian" ] || [ "$distro" == "ubuntu" ]; then for file in "${gcc_alternative_files[@]}"; do if [ -e "/etc/alternatives/$file.bak" ]; then ${sudo_cmd} mv "/etc/alternatives/$file.bak" "/etc/alternatives/$file" else ${sudo_cmd} rm "/etc/alternatives/$file" fi done fi # restore the math-vector.h file if [ "$architecture" == "aarch64" ] && [ -n "$math_vector_file" ]; then ${sudo_cmd} mv -f "$math_vector_file.bak" "$math_vector_file" fi fi } function run_install() { case "$step" in deps) run_step_deps ;; cmake) run_step_cmake ;; validation) run_step_validation ;; build) run_step_build ;; package) run_step_package ;; cleanup) run_step_cleanup ;; all) run_step_deps run_step_cmake run_step_validation run_step_build run_step_package run_step_cleanup ;; *) echo "Invalid step: $step" echo "Valid steps are: deps, cmake, validation, build, package, cleanup, all" exit 1 ;; esac } # Determine the OS and call the appropriate function cat /etc/os-release if grep -q "Arch Linux" /etc/os-release; then distro="arch" version="" package_update_command="${sudo_cmd} pacman -Syu --noconfirm" package_install_command="${sudo_cmd} pacman -Sy --needed" nvm_node=0 gcc_version="14" elif grep -q "Debian GNU/Linux 12 (bookworm)" /etc/os-release; then distro="debian" version="12" package_update_command="${sudo_cmd} apt-get update" package_install_command="${sudo_cmd} apt-get install -y" cuda_version="12.9.1" cuda_build="575.57.08" gcc_version="13" nvm_node=0 elif grep -q "Debian GNU/Linux 13 (trixie)" /etc/os-release; then distro="debian" version="13" package_update_command="${sudo_cmd} apt-get update" package_install_command="${sudo_cmd} apt-get install -y" cuda_version="12.9.1" cuda_build="575.57.08" gcc_version="14" nvm_node=0 elif grep -q "PLATFORM_ID=\"platform:f41\"" /etc/os-release; then distro="fedora" version="41" package_update_command="${sudo_cmd} dnf update -y" package_install_command="${sudo_cmd} dnf install -y" cuda_version="12.9.1" cuda_build="575.57.08" gcc_version="13" nvm_node=0 dev_tools_group="development-tools" elif grep -q "PLATFORM_ID=\"platform:f42\"" /etc/os-release; then distro="fedora" version="42" package_update_command="${sudo_cmd} dnf update -y" package_install_command="${sudo_cmd} dnf install -y" cuda_version="12.9.1" cuda_build="575.57.08" gcc_version="14" nvm_node=0 dev_tools_group="development-tools" elif grep -q "Ubuntu 22.04" /etc/os-release; then distro="ubuntu" version="22.04" package_update_command="${sudo_cmd} apt-get update" package_install_command="${sudo_cmd} apt-get install -y" cuda_version="12.9.1" cuda_build="575.57.08" gcc_version="13" nvm_node=1 elif grep -q "Ubuntu 24.04" /etc/os-release; then distro="ubuntu" version="24.04" package_update_command="${sudo_cmd} apt-get update" package_install_command="${sudo_cmd} apt-get install -y" cuda_version="12.9.1" cuda_build="575.57.08" gcc_version="14" nvm_node=1 elif grep -q "Ubuntu 25.04" /etc/os-release; then distro="ubuntu" version="25.04" package_update_command="${sudo_cmd} apt-get update" package_install_command="${sudo_cmd} apt-get install -y" cuda_version="12.9.1" cuda_build="575.57.08" gcc_version="14" nvm_node=0 else echo "Unsupported Distro or Version" exit 1 fi architecture=$(uname -m) echo "Detected Distro: $distro" echo "Detected Version: $version" echo "Detected Architecture: $architecture" if [ "$architecture" != "x86_64" ] && [ "$architecture" != "aarch64" ]; then echo "Unsupported Architecture" exit 1 fi # get directory of this script script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" build_dir="$script_dir/../build" echo "Script Directory: $script_dir" echo "Build Directory: $build_dir" mkdir -p "$build_dir" run_install ================================================ FILE: scripts/requirements.txt ================================================ Babel==2.17.0 clang-format==20.* ================================================ FILE: scripts/update_clang_format.py ================================================ # standard imports import os import subprocess # variables directories = [ 'src', 'tests', 'tools', ] file_types = [ 'cpp', 'cu', 'h', 'hpp', 'm', 'mm' ] def clang_format(file: str): print(f'Formatting {file} ...') subprocess.run(['clang-format', '-i', file]) def main(): """ Main entry point. """ # walk the directories for directory in directories: for root, dirs, files in os.walk(directory): for file in files: file_path = os.path.join(root, file) if os.path.isfile(file_path) and file.rsplit('.')[-1] in file_types: clang_format(file=file_path) if __name__ == '__main__': main() ================================================ FILE: src/audio.cpp ================================================ /** * @file src/audio.cpp * @brief Definitions for audio capture and encoding. */ // standard includes #include // lib includes #include // local includes #include "audio.h" #include "config.h" #include "globals.h" #include "logging.h" #include "platform/common.h" #include "thread_safe.h" #include "utility.h" namespace audio { using namespace std::literals; using opus_t = util::safe_ptr; using sample_queue_t = std::shared_ptr>>; static int start_audio_control(audio_ctx_t &ctx); static void stop_audio_control(audio_ctx_t &); static void apply_surround_params(opus_stream_config_t &stream, const stream_params_t ¶ms); int map_stream(int channels, bool quality); constexpr auto SAMPLE_RATE = 48000; // NOTE: If you adjust the bitrates listed here, make sure to update the // corresponding bitrate adjustment logic in rtsp_stream::cmd_announce() opus_stream_config_t stream_configs[MAX_STREAM_CONFIG] { { SAMPLE_RATE, 2, 1, 1, platf::speaker::map_stereo, 96000, }, { SAMPLE_RATE, 2, 1, 1, platf::speaker::map_stereo, 512000, }, { SAMPLE_RATE, 6, 4, 2, platf::speaker::map_surround51, 256000, }, { SAMPLE_RATE, 6, 6, 0, platf::speaker::map_surround51, 1536000, }, { SAMPLE_RATE, 8, 5, 3, platf::speaker::map_surround71, 450000, }, { SAMPLE_RATE, 8, 8, 0, platf::speaker::map_surround71, 2048000, }, }; void encodeThread(sample_queue_t samples, config_t config, void *channel_data) { auto packets = mail::man->queue(mail::audio_packets); auto stream = stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])]; if (config.flags[config_t::CUSTOM_SURROUND_PARAMS]) { apply_surround_params(stream, config.customStreamParams); } // Encoding takes place on this thread platf::adjust_thread_priority(platf::thread_priority_e::high); opus_t opus {opus_multistream_encoder_create( stream.sampleRate, stream.channelCount, stream.streams, stream.coupledStreams, stream.mapping, OPUS_APPLICATION_RESTRICTED_LOWDELAY, nullptr )}; opus_multistream_encoder_ctl(opus.get(), OPUS_SET_BITRATE(stream.bitrate)); opus_multistream_encoder_ctl(opus.get(), OPUS_SET_VBR(0)); BOOST_LOG(info) << "Opus initialized: "sv << stream.sampleRate / 1000 << " kHz, "sv << stream.channelCount << " channels, "sv << stream.bitrate / 1000 << " kbps (total), LOWDELAY"sv; auto frame_size = config.packetDuration * stream.sampleRate / 1000; while (auto sample = samples->pop()) { buffer_t packet {1400}; int bytes = opus_multistream_encode_float(opus.get(), sample->data(), frame_size, std::begin(packet), packet.size()); if (bytes < 0) { BOOST_LOG(error) << "Couldn't encode audio: "sv << opus_strerror(bytes); packets->stop(); return; } packet.fake_resize(bytes); packets->raise(channel_data, std::move(packet)); } } void capture(safe::mail_t mail, config_t config, void *channel_data) { auto shutdown_event = mail->event(mail::shutdown); if (!config::audio.stream || config.input_only) { shutdown_event->view(); return; } auto stream = stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])]; if (config.flags[config_t::CUSTOM_SURROUND_PARAMS]) { apply_surround_params(stream, config.customStreamParams); } auto ref = get_audio_ctx_ref(); if (!ref) { return; } auto init_failure_fg = util::fail_guard([&shutdown_event]() { BOOST_LOG(error) << "Unable to initialize audio capture. The stream will not have audio."sv; // Wait for shutdown to be signalled if we fail init. // This allows streaming to continue without audio. shutdown_event->view(); return; }); auto &control = ref->control; if (!control) { return; } // Order of priority: // 1. Virtual sink // 2. Audio sink // 3. Host std::string *sink = &ref->sink.host; if (!config::audio.sink.empty()) { sink = &config::audio.sink; } // Prefer the virtual sink if host playback is disabled or there's no other sink if (ref->sink.null && (!config.flags[config_t::HOST_AUDIO] || sink->empty())) { auto &null = *ref->sink.null; switch (stream.channelCount) { case 2: sink = &null.stereo; break; case 6: sink = &null.surround51; break; case 8: sink = &null.surround71; break; } } BOOST_LOG(info) << "Selected audio sink: "sv << *sink; // Only the first to start a session may change the default sink if (!ref->sink_flag->exchange(true, std::memory_order_acquire)) { // If the selected sink is different than the current one, change sinks. ref->restore_sink = ref->sink.host != *sink; if (ref->restore_sink) { if (control->set_sink(*sink)) { return; } } } auto frame_size = config.packetDuration * stream.sampleRate / 1000; auto mic = control->microphone(stream.mapping, stream.channelCount, stream.sampleRate, frame_size); if (!mic) { return; } // Audio is initialized, so we don't want to print the failure message init_failure_fg.disable(); // Capture takes place on this thread platf::adjust_thread_priority(platf::thread_priority_e::critical); auto samples = std::make_shared(30); std::thread thread {encodeThread, samples, config, channel_data}; auto fg = util::fail_guard([&]() { samples->stop(); thread.join(); shutdown_event->view(); }); int samples_per_frame = frame_size * stream.channelCount; while (!shutdown_event->peek()) { std::vector sample_buffer; sample_buffer.resize(samples_per_frame); auto status = mic->sample(sample_buffer); switch (status) { case platf::capture_e::ok: break; case platf::capture_e::timeout: continue; case platf::capture_e::reinit: if (config::audio.auto_capture) { BOOST_LOG(info) << "Reinitializing audio capture"sv; mic.reset(); do { mic = control->microphone(stream.mapping, stream.channelCount, stream.sampleRate, frame_size); if (!mic) { BOOST_LOG(warning) << "Couldn't re-initialize audio input"sv; } } while (!mic && !shutdown_event->view(5s)); } continue; default: return; } samples->raise(std::move(sample_buffer)); } } audio_ctx_ref_t get_audio_ctx_ref() { static auto control_shared {safe::make_shared(start_audio_control, stop_audio_control)}; return control_shared.ref(); } bool is_audio_ctx_sink_available(const audio_ctx_t &ctx) { if (!ctx.control) { return false; } const std::string &sink = ctx.sink.host.empty() ? config::audio.sink : ctx.sink.host; if (sink.empty()) { return false; } return ctx.control->is_sink_available(sink); } int map_stream(int channels, bool quality) { int shift = quality ? 1 : 0; switch (channels) { case 2: return STEREO + shift; case 6: return SURROUND51 + shift; case 8: return SURROUND71 + shift; } return STEREO; } int start_audio_control(audio_ctx_t &ctx) { auto fg = util::fail_guard([]() { BOOST_LOG(warning) << "There will be no audio"sv; }); ctx.sink_flag = std::make_unique(false); // The default sink has not been replaced yet. ctx.restore_sink = false; if (!(ctx.control = platf::audio_control())) { return 0; } auto sink = ctx.control->sink_info(); if (!sink) { // Let the calling code know it failed ctx.control.reset(); return 0; } ctx.sink = std::move(*sink); fg.disable(); return 0; } void stop_audio_control(audio_ctx_t &ctx) { // restore audio-sink if applicable if (!ctx.restore_sink) { return; } // Change back to the host sink, unless there was none const std::string &sink = ctx.sink.host.empty() ? config::audio.sink : ctx.sink.host; if (!sink.empty()) { // Best effort, it's allowed to fail ctx.control->set_sink(sink); } } void apply_surround_params(opus_stream_config_t &stream, const stream_params_t ¶ms) { stream.channelCount = params.channelCount; stream.streams = params.streams; stream.coupledStreams = params.coupledStreams; stream.mapping = params.mapping; } } // namespace audio ================================================ FILE: src/audio.h ================================================ /** * @file src/audio.h * @brief Declarations for audio capture and encoding. */ #pragma once // local includes #include "platform/common.h" #include "thread_safe.h" #include "utility.h" #include namespace audio { enum stream_config_e : int { STEREO, ///< Stereo HIGH_STEREO, ///< High stereo SURROUND51, ///< Surround 5.1 HIGH_SURROUND51, ///< High surround 5.1 SURROUND71, ///< Surround 7.1 HIGH_SURROUND71, ///< High surround 7.1 MAX_STREAM_CONFIG ///< Maximum audio stream configuration }; struct opus_stream_config_t { std::int32_t sampleRate; int channelCount; int streams; int coupledStreams; const std::uint8_t *mapping; int bitrate; }; struct stream_params_t { int channelCount; int streams; int coupledStreams; std::uint8_t mapping[8]; }; extern opus_stream_config_t stream_configs[MAX_STREAM_CONFIG]; struct config_t { enum flags_e : int { HIGH_QUALITY, ///< High quality audio HOST_AUDIO, ///< Host audio CUSTOM_SURROUND_PARAMS, ///< Custom surround parameters MAX_FLAGS ///< Maximum number of flags }; int packetDuration; int channels; int mask; stream_params_t customStreamParams; std::bitset flags; // Who TF knows what Sunshine did // putting input_only at the end of flags will always be over written to true uint64_t __padding; bool input_only; }; struct audio_ctx_t { // We want to change the sink for the first stream only std::unique_ptr sink_flag; std::unique_ptr control; bool restore_sink; platf::sink_t sink; }; using buffer_t = util::buffer_t; using packet_t = std::pair; using audio_ctx_ref_t = safe::shared_t::ptr_t; void capture(safe::mail_t mail, config_t config, void *channel_data); /** * @brief Get the reference to the audio context. * @returns A shared pointer reference to audio context. * @note Aside from the configuration purposes, it can be used to extend the * audio sink lifetime to capture sink earlier and restore it later. * * @examples * audio_ctx_ref_t audio = get_audio_ctx_ref() * @examples_end */ audio_ctx_ref_t get_audio_ctx_ref(); /** * @brief Check if the audio sink held by audio context is available. * @returns True if available (and can probably be restored), false otherwise. * @note Useful for delaying the release of audio context shared pointer (which * tries to restore original sink). * * @examples * audio_ctx_ref_t audio = get_audio_ctx_ref() * if (audio.get()) { * return is_audio_ctx_sink_available(*audio.get()); * } * return false; * @examples_end */ bool is_audio_ctx_sink_available(const audio_ctx_t &ctx); } // namespace audio ================================================ FILE: src/cbs.cpp ================================================ /** * @file src/cbs.cpp * @brief Definitions for FFmpeg Coded Bitstream API. */ extern "C" { // lib includes #include #include #include #include #include } // local includes #include "cbs.h" #include "logging.h" #include "utility.h" using namespace std::literals; namespace cbs { void close(CodedBitstreamContext *c) { ff_cbs_close(&c); } using ctx_t = util::safe_ptr; class frag_t: public CodedBitstreamFragment { public: frag_t(frag_t &&o) { std::copy((std::uint8_t *) &o, (std::uint8_t *) (&o + 1), (std::uint8_t *) this); o.data = nullptr; o.units = nullptr; }; frag_t() { std::fill_n((std::uint8_t *) this, sizeof(*this), 0); } frag_t &operator=(frag_t &&o) { std::copy((std::uint8_t *) &o, (std::uint8_t *) (&o + 1), (std::uint8_t *) this); o.data = nullptr; o.units = nullptr; return *this; }; ~frag_t() { if (data || units) { ff_cbs_fragment_free(this); } } }; util::buffer_t write(cbs::ctx_t &cbs_ctx, std::uint8_t nal, void *uh, AVCodecID codec_id) { cbs::frag_t frag; auto err = ff_cbs_insert_unit_content(&frag, -1, nal, uh, nullptr); if (err < 0) { char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Could not insert NAL unit SPS: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return {}; } err = ff_cbs_write_fragment_data(cbs_ctx.get(), &frag); if (err < 0) { char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Could not write fragment data: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return {}; } // frag.data_size * 8 - frag.data_bit_padding == bits in fragment util::buffer_t data {frag.data_size}; std::copy_n(frag.data, frag.data_size, std::begin(data)); return data; } util::buffer_t write(std::uint8_t nal, void *uh, AVCodecID codec_id) { cbs::ctx_t cbs_ctx; ff_cbs_init(&cbs_ctx, codec_id, nullptr); return write(cbs_ctx, nal, uh, codec_id); } h264_t make_sps_h264(const AVCodecContext *avctx, const AVPacket *packet) { cbs::ctx_t ctx; if (ff_cbs_init(&ctx, AV_CODEC_ID_H264, nullptr)) { return {}; } cbs::frag_t frag; int err = ff_cbs_read_packet(ctx.get(), &frag, packet); if (err < 0) { char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return {}; } auto sps_p = ((CodedBitstreamH264Context *) ctx->priv_data)->active_sps; // This is a very large struct that cannot safely be stored on the stack auto sps = std::make_unique(*sps_p); if (avctx->refs > 0) { sps->max_num_ref_frames = avctx->refs; } sps->vui_parameters_present_flag = 1; auto &vui = sps->vui; std::memset(&vui, 0, sizeof(vui)); vui.video_format = 5; vui.colour_description_present_flag = 1; vui.video_signal_type_present_flag = 1; vui.video_full_range_flag = avctx->color_range == AVCOL_RANGE_JPEG; vui.colour_primaries = avctx->color_primaries; vui.transfer_characteristics = avctx->color_trc; vui.matrix_coefficients = avctx->colorspace; vui.low_delay_hrd_flag = 1 - vui.fixed_frame_rate_flag; vui.bitstream_restriction_flag = 1; vui.motion_vectors_over_pic_boundaries_flag = 1; vui.log2_max_mv_length_horizontal = 16; vui.log2_max_mv_length_vertical = 16; vui.max_num_reorder_frames = 0; vui.max_dec_frame_buffering = sps->max_num_ref_frames; cbs::ctx_t write_ctx; ff_cbs_init(&write_ctx, AV_CODEC_ID_H264, nullptr); return h264_t { write(write_ctx, sps->nal_unit_header.nal_unit_type, (void *) &sps->nal_unit_header, AV_CODEC_ID_H264), write(ctx, sps_p->nal_unit_header.nal_unit_type, (void *) &sps_p->nal_unit_header, AV_CODEC_ID_H264) }; } hevc_t make_sps_hevc(const AVCodecContext *avctx, const AVPacket *packet) { cbs::ctx_t ctx; if (ff_cbs_init(&ctx, AV_CODEC_ID_H265, nullptr)) { return {}; } cbs::frag_t frag; int err = ff_cbs_read_packet(ctx.get(), &frag, packet); if (err < 0) { char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return {}; } auto vps_p = ((CodedBitstreamH265Context *) ctx->priv_data)->active_vps; auto sps_p = ((CodedBitstreamH265Context *) ctx->priv_data)->active_sps; // These are very large structs that cannot safely be stored on the stack auto sps = std::make_unique(*sps_p); auto vps = std::make_unique(*vps_p); vps->profile_tier_level.general_profile_compatibility_flag[4] = 1; sps->profile_tier_level.general_profile_compatibility_flag[4] = 1; auto &vui = sps->vui; std::memset(&vui, 0, sizeof(vui)); sps->vui_parameters_present_flag = 1; // skip sample aspect ratio vui.video_format = 5; vui.colour_description_present_flag = 1; vui.video_signal_type_present_flag = 1; vui.video_full_range_flag = avctx->color_range == AVCOL_RANGE_JPEG; vui.colour_primaries = avctx->color_primaries; vui.transfer_characteristics = avctx->color_trc; vui.matrix_coefficients = avctx->colorspace; vui.vui_timing_info_present_flag = vps->vps_timing_info_present_flag; vui.vui_num_units_in_tick = vps->vps_num_units_in_tick; vui.vui_time_scale = vps->vps_time_scale; vui.vui_poc_proportional_to_timing_flag = vps->vps_poc_proportional_to_timing_flag; vui.vui_num_ticks_poc_diff_one_minus1 = vps->vps_num_ticks_poc_diff_one_minus1; vui.vui_hrd_parameters_present_flag = 0; vui.bitstream_restriction_flag = 1; vui.motion_vectors_over_pic_boundaries_flag = 1; vui.restricted_ref_pic_lists_flag = 1; vui.max_bytes_per_pic_denom = 0; vui.max_bits_per_min_cu_denom = 0; vui.log2_max_mv_length_horizontal = 15; vui.log2_max_mv_length_vertical = 15; cbs::ctx_t write_ctx; ff_cbs_init(&write_ctx, AV_CODEC_ID_H265, nullptr); return hevc_t { nal_t { write(write_ctx, vps->nal_unit_header.nal_unit_type, (void *) &vps->nal_unit_header, AV_CODEC_ID_H265), write(ctx, vps_p->nal_unit_header.nal_unit_type, (void *) &vps_p->nal_unit_header, AV_CODEC_ID_H265), }, nal_t { write(write_ctx, sps->nal_unit_header.nal_unit_type, (void *) &sps->nal_unit_header, AV_CODEC_ID_H265), write(ctx, sps_p->nal_unit_header.nal_unit_type, (void *) &sps_p->nal_unit_header, AV_CODEC_ID_H265), }, }; } /** * This function initializes a Coded Bitstream Context and reads the packet into a Coded Bitstream Fragment. * It then checks if the SPS->VUI (Video Usability Information) is present in the active SPS of the packet. * This is done for both H264 and H265 codecs. */ bool validate_sps(const AVPacket *packet, int codec_id) { cbs::ctx_t ctx; if (ff_cbs_init(&ctx, (AVCodecID) codec_id, nullptr)) { return false; } cbs::frag_t frag; int err = ff_cbs_read_packet(ctx.get(), &frag, packet); if (err < 0) { char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return false; } if (codec_id == AV_CODEC_ID_H264) { auto h264 = (CodedBitstreamH264Context *) ctx->priv_data; if (!h264->active_sps->vui_parameters_present_flag) { return false; } return true; } return ((CodedBitstreamH265Context *) ctx->priv_data)->active_sps->vui_parameters_present_flag; } } // namespace cbs ================================================ FILE: src/cbs.h ================================================ /** * @file src/cbs.h * @brief Declarations for FFmpeg Coded Bitstream API. */ #pragma once // local includes #include "utility.h" struct AVPacket; struct AVCodecContext; namespace cbs { struct nal_t { util::buffer_t _new; util::buffer_t old; }; struct hevc_t { nal_t vps; nal_t sps; }; struct h264_t { nal_t sps; }; hevc_t make_sps_hevc(const AVCodecContext *ctx, const AVPacket *packet); h264_t make_sps_h264(const AVCodecContext *ctx, const AVPacket *packet); /** * @brief Validates the Sequence Parameter Set (SPS) of a given packet. * @param packet The packet to validate. * @param codec_id The ID of the codec used (either AV_CODEC_ID_H264 or AV_CODEC_ID_H265). * @return True if the SPS->VUI is present in the active SPS of the packet, false otherwise. */ bool validate_sps(const AVPacket *packet, int codec_id); } // namespace cbs ================================================ FILE: src/config.cpp ================================================ /** * @file src/config.cpp * @brief Definitions for the configuration of Sunshine. */ // standard includes #include #include #include #include #include #include #include #include // lib includes #include #include #include #include // local includes #include "config.h" #include "entry_handler.h" #include "file_handler.h" #include "logging.h" #include "nvhttp.h" #include "platform/common.h" #include "rtsp.h" #include "video.h" #include "utility.h" #ifdef _WIN32 #include #include "platform/windows/utils.h" #endif #if !defined(__ANDROID__) && !defined(__APPLE__) // For NVENC legacy constants #include #endif namespace fs = std::filesystem; using namespace std::literals; #define CA_DIR "credentials" #define PRIVATE_KEY_FILE CA_DIR "/cakey.pem" #define CERTIFICATE_FILE CA_DIR "/cacert.pem" #define APPS_JSON_PATH platf::appdata().string() + "/apps.json" namespace config { namespace nv { nvenc::nvenc_two_pass twopass_from_view(const ::std::string_view &preset) { if (preset == "disabled") { return nvenc::nvenc_two_pass::disabled; } if (preset == "quarter_res") { return nvenc::nvenc_two_pass::quarter_resolution; } if (preset == "full_res") { return nvenc::nvenc_two_pass::full_resolution; } BOOST_LOG(warning) << "config: unknown nvenc_twopass value: " << preset; return nvenc::nvenc_two_pass::quarter_resolution; } } // namespace nv namespace amd { #if !defined(_WIN32) || defined(DOXYGEN) // values accurate as of 27/12/2022, but aren't strictly necessary for MacOS build #define AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_SPEED 100 #define AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_QUALITY 30 #define AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_BALANCED 70 #define AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_SPEED 10 #define AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_QUALITY 0 #define AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_BALANCED 5 #define AMF_VIDEO_ENCODER_QUALITY_PRESET_SPEED 1 #define AMF_VIDEO_ENCODER_QUALITY_PRESET_QUALITY 2 #define AMF_VIDEO_ENCODER_QUALITY_PRESET_BALANCED 0 #define AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CONSTANT_QP 0 #define AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CBR 3 #define AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR 2 #define AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR 1 #define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CONSTANT_QP 0 #define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CBR 3 #define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR 2 #define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR 1 #define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CONSTANT_QP 0 #define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR 1 #define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR 2 #define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR 3 #define AMF_VIDEO_ENCODER_AV1_USAGE_TRANSCODING 0 #define AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY 1 #define AMF_VIDEO_ENCODER_AV1_USAGE_ULTRA_LOW_LATENCY 2 #define AMF_VIDEO_ENCODER_AV1_USAGE_WEBCAM 3 #define AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY_HIGH_QUALITY 5 #define AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCODING 0 #define AMF_VIDEO_ENCODER_HEVC_USAGE_ULTRA_LOW_LATENCY 1 #define AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY 2 #define AMF_VIDEO_ENCODER_HEVC_USAGE_WEBCAM 3 #define AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY_HIGH_QUALITY 5 #define AMF_VIDEO_ENCODER_USAGE_TRANSCODING 0 #define AMF_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY 1 #define AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY 2 #define AMF_VIDEO_ENCODER_USAGE_WEBCAM 3 #define AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY_HIGH_QUALITY 5 #define AMF_VIDEO_ENCODER_UNDEFINED 0 #define AMF_VIDEO_ENCODER_CABAC 1 #define AMF_VIDEO_ENCODER_CALV 2 #else #ifdef _GLIBCXX_USE_C99_INTTYPES #undef _GLIBCXX_USE_C99_INTTYPES #endif #include #include #include #endif enum class quality_av1_e : int { speed = AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_SPEED, ///< Speed preset quality = AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_QUALITY, ///< Quality preset balanced = AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_BALANCED ///< Balanced preset }; enum class quality_hevc_e : int { speed = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_SPEED, ///< Speed preset quality = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_QUALITY, ///< Quality preset balanced = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_BALANCED ///< Balanced preset }; enum class quality_h264_e : int { speed = AMF_VIDEO_ENCODER_QUALITY_PRESET_SPEED, ///< Speed preset quality = AMF_VIDEO_ENCODER_QUALITY_PRESET_QUALITY, ///< Quality preset balanced = AMF_VIDEO_ENCODER_QUALITY_PRESET_BALANCED ///< Balanced preset }; enum class rc_av1_e : int { cbr = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CBR, ///< CBR cqp = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CONSTANT_QP, ///< CQP vbr_latency = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR, ///< VBR with latency constraints vbr_peak = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR ///< VBR with peak constraints }; enum class rc_hevc_e : int { cbr = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CBR, ///< CBR cqp = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CONSTANT_QP, ///< CQP vbr_latency = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR, ///< VBR with latency constraints vbr_peak = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR ///< VBR with peak constraints }; enum class rc_h264_e : int { cbr = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR, ///< CBR cqp = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CONSTANT_QP, ///< CQP vbr_latency = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR, ///< VBR with latency constraints vbr_peak = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR ///< VBR with peak constraints }; enum class usage_av1_e : int { transcoding = AMF_VIDEO_ENCODER_AV1_USAGE_TRANSCODING, ///< Transcoding preset webcam = AMF_VIDEO_ENCODER_AV1_USAGE_WEBCAM, ///< Webcam preset lowlatency_high_quality = AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY_HIGH_QUALITY, ///< Low latency high quality preset lowlatency = AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY, ///< Low latency preset ultralowlatency = AMF_VIDEO_ENCODER_AV1_USAGE_ULTRA_LOW_LATENCY ///< Ultra low latency preset }; enum class usage_hevc_e : int { transcoding = AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCODING, ///< Transcoding preset webcam = AMF_VIDEO_ENCODER_HEVC_USAGE_WEBCAM, ///< Webcam preset lowlatency_high_quality = AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY_HIGH_QUALITY, ///< Low latency high quality preset lowlatency = AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY, ///< Low latency preset ultralowlatency = AMF_VIDEO_ENCODER_HEVC_USAGE_ULTRA_LOW_LATENCY ///< Ultra low latency preset }; enum class usage_h264_e : int { transcoding = AMF_VIDEO_ENCODER_USAGE_TRANSCODING, ///< Transcoding preset webcam = AMF_VIDEO_ENCODER_USAGE_WEBCAM, ///< Webcam preset lowlatency_high_quality = AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY_HIGH_QUALITY, ///< Low latency high quality preset lowlatency = AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY, ///< Low latency preset ultralowlatency = AMF_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY ///< Ultra low latency preset }; enum coder_e : int { _auto = AMF_VIDEO_ENCODER_UNDEFINED, ///< Auto cabac = AMF_VIDEO_ENCODER_CABAC, ///< CABAC cavlc = AMF_VIDEO_ENCODER_CALV ///< CAVLC }; template ::std::optional quality_from_view(const ::std::string_view &quality_type, const ::std::optional(&original)) { #define _CONVERT_(x) \ if (quality_type == #x##sv) \ return (int) T::x _CONVERT_(balanced); _CONVERT_(quality); _CONVERT_(speed); #undef _CONVERT_ return original; } template ::std::optional rc_from_view(const ::std::string_view &rc, const ::std::optional(&original)) { #define _CONVERT_(x) \ if (rc == #x##sv) \ return (int) T::x _CONVERT_(cbr); _CONVERT_(cqp); _CONVERT_(vbr_latency); _CONVERT_(vbr_peak); #undef _CONVERT_ return original; } template ::std::optional usage_from_view(const ::std::string_view &usage, const ::std::optional(&original)) { #define _CONVERT_(x) \ if (usage == #x##sv) \ return (int) T::x _CONVERT_(lowlatency); _CONVERT_(lowlatency_high_quality); _CONVERT_(transcoding); _CONVERT_(ultralowlatency); _CONVERT_(webcam); #undef _CONVERT_ return original; } int coder_from_view(const ::std::string_view &coder) { if (coder == "auto"sv) { return _auto; } if (coder == "cabac"sv || coder == "ac"sv) { return cabac; } if (coder == "cavlc"sv || coder == "vlc"sv) { return cavlc; } return _auto; } } // namespace amd namespace qsv { enum preset_e : int { veryslow = 1, ///< veryslow preset slower = 2, ///< slower preset slow = 3, ///< slow preset medium = 4, ///< medium preset fast = 5, ///< fast preset faster = 6, ///< faster preset veryfast = 7 ///< veryfast preset }; enum cavlc_e : int { _auto = false, ///< Auto enabled = true, ///< Enabled disabled = false ///< Disabled }; ::std::optional preset_from_view(const ::std::string_view &preset) { #define _CONVERT_(x) \ if (preset == #x##sv) \ return x _CONVERT_(veryslow); _CONVERT_(slower); _CONVERT_(slow); _CONVERT_(medium); _CONVERT_(fast); _CONVERT_(faster); _CONVERT_(veryfast); #undef _CONVERT_ return std::nullopt; } ::std::optional coder_from_view(const ::std::string_view &coder) { if (coder == "auto"sv) { return _auto; } if (coder == "cabac"sv || coder == "ac"sv) { return disabled; } if (coder == "cavlc"sv || coder == "vlc"sv) { return enabled; } return std::nullopt; } } // namespace qsv namespace vt { enum coder_e : int { _auto = 0, ///< Auto cabac, ///< CABAC cavlc ///< CAVLC }; int coder_from_view(const ::std::string_view &coder) { if (coder == "auto"sv) { return _auto; } if (coder == "cabac"sv || coder == "ac"sv) { return cabac; } if (coder == "cavlc"sv || coder == "vlc"sv) { return cavlc; } return -1; } int allow_software_from_view(const ::std::string_view &software) { if (software == "allowed"sv || software == "forced") { return 1; } return 0; } int force_software_from_view(const ::std::string_view &software) { if (software == "forced") { return 1; } return 0; } int rt_from_view(const ::std::string_view &rt) { if (rt == "disabled" || rt == "off" || rt == "0") { return 0; } return 1; } } // namespace vt namespace sw { int svtav1_preset_from_view(const ::std::string_view &preset) { #define _CONVERT_(x, y) \ if (preset == #x##sv) \ return y _CONVERT_(veryslow, 1); _CONVERT_(slower, 2); _CONVERT_(slow, 4); _CONVERT_(medium, 5); _CONVERT_(fast, 7); _CONVERT_(faster, 9); _CONVERT_(veryfast, 10); _CONVERT_(superfast, 11); _CONVERT_(ultrafast, 12); #undef _CONVERT_ return 11; // Default to superfast } } // namespace sw namespace dd { video_t::dd_t::config_option_e config_option_from_view(const ::std::string_view value) { #define _CONVERT_(x) \ if (value == #x##sv) \ return video_t::dd_t::config_option_e::x _CONVERT_(disabled); _CONVERT_(verify_only); _CONVERT_(ensure_active); _CONVERT_(ensure_primary); _CONVERT_(ensure_only_display); #undef _CONVERT_ return video_t::dd_t::config_option_e::disabled; // Default to this if value is invalid } video_t::dd_t::resolution_option_e resolution_option_from_view(const ::std::string_view value) { #define _CONVERT_2_ARG_(str, val) \ if (value == #str##sv) \ return video_t::dd_t::resolution_option_e::val #define _CONVERT_(x) _CONVERT_2_ARG_(x, x) _CONVERT_(disabled); _CONVERT_2_ARG_(auto, automatic); _CONVERT_(manual); #undef _CONVERT_ #undef _CONVERT_2_ARG_ return video_t::dd_t::resolution_option_e::disabled; // Default to this if value is invalid } video_t::dd_t::refresh_rate_option_e refresh_rate_option_from_view(const ::std::string_view value) { #define _CONVERT_2_ARG_(str, val) \ if (value == #str##sv) \ return video_t::dd_t::refresh_rate_option_e::val #define _CONVERT_(x) _CONVERT_2_ARG_(x, x) _CONVERT_(disabled); _CONVERT_2_ARG_(auto, automatic); _CONVERT_(manual); #undef _CONVERT_ #undef _CONVERT_2_ARG_ return video_t::dd_t::refresh_rate_option_e::disabled; // Default to this if value is invalid } video_t::dd_t::hdr_option_e hdr_option_from_view(const ::std::string_view value) { #define _CONVERT_2_ARG_(str, val) \ if (value == #str##sv) \ return video_t::dd_t::hdr_option_e::val #define _CONVERT_(x) _CONVERT_2_ARG_(x, x) _CONVERT_(disabled); _CONVERT_2_ARG_(auto, automatic); #undef _CONVERT_ #undef _CONVERT_2_ARG_ return video_t::dd_t::hdr_option_e::disabled; // Default to this if value is invalid } video_t::dd_t::mode_remapping_t mode_remapping_from_view(const ::std::string_view value) { const auto parse_entry_list {[](const auto &entry_list, auto &output_field) { for (auto &[_, entry] : entry_list) { auto requested_resolution = entry.template get_optional("requested_resolution"s); auto requested_fps = entry.template get_optional("requested_fps"s); auto final_resolution = entry.template get_optional("final_resolution"s); auto final_refresh_rate = entry.template get_optional("final_refresh_rate"s); output_field.push_back(video_t::dd_t::mode_remapping_entry_t {requested_resolution.value_or(""), requested_fps.value_or(""), final_resolution.value_or(""), final_refresh_rate.value_or("")}); } }}; // We need to add a wrapping object to make it valid JSON, otherwise ptree cannot parse it. std::stringstream json_stream; json_stream << "{\"dd_mode_remapping\":" << value << "}"; boost::property_tree::ptree json_tree; boost::property_tree::read_json(json_stream, json_tree); video_t::dd_t::mode_remapping_t output; parse_entry_list(json_tree.get_child("dd_mode_remapping.mixed"), output.mixed); parse_entry_list(json_tree.get_child("dd_mode_remapping.resolution_only"), output.resolution_only); parse_entry_list(json_tree.get_child("dd_mode_remapping.refresh_rate_only"), output.refresh_rate_only); return output; } } // namespace dd video_t video { false, // headless_mode true, // limit_framerate false, // double_refreshrate 28, // qp 0, // hevc_mode 0, // av1_mode 2, // min_threads { "superfast"s, // preset "zerolatency"s, // tune 11, // superfast }, // software {}, // nv true, // nv_realtime_hags true, // nv_opengl_vulkan_on_dxgi true, // nv_sunshine_high_power_mode {}, // nv_legacy { qsv::medium, // preset qsv::_auto, // cavlc false, // slow_hevc }, // qsv { (int) amd::usage_h264_e::ultralowlatency, // usage (h264) (int) amd::usage_hevc_e::ultralowlatency, // usage (hevc) (int) amd::usage_av1_e::ultralowlatency, // usage (av1) (int) amd::rc_h264_e::vbr_latency, // rate control (h264) (int) amd::rc_hevc_e::vbr_latency, // rate control (hevc) (int) amd::rc_av1_e::vbr_latency, // rate control (av1) 0, // enforce_hrd (int) amd::quality_h264_e::balanced, // quality (h264) (int) amd::quality_hevc_e::balanced, // quality (hevc) (int) amd::quality_av1_e::balanced, // quality (av1) 0, // preanalysis 1, // vbaq (int) amd::coder_e::_auto, // coder }, // amd { 0, 0, 1, -1, }, // vt { false, // strict_rc_buffer }, // vaapi {}, // capture {}, // encoder {}, // adapter_name {}, // output_name { video_t::dd_t::config_option_e::disabled, // configuration_option video_t::dd_t::resolution_option_e::automatic, // resolution_option {}, // manual_resolution video_t::dd_t::refresh_rate_option_e::automatic, // refresh_rate_option {}, // manual_refresh_rate video_t::dd_t::hdr_option_e::automatic, // hdr_option 3s, // config_revert_delay {}, // config_revert_on_disconnect {}, // mode_remapping {} // wa }, // display_device 0, // max_bitrate 0, // minimum_fps_target (0 = framerate) "1920x1080x60", // fallback_mode false, // isolated Display false, // ignore_encoder_probe_failure }; audio_t audio { {}, // audio_sink {}, // virtual_sink true, // stream audio true, // install_steam_drivers true, // keep_sink_default true, // auto_capture }; stream_t stream { 10s, // ping_timeout APPS_JSON_PATH, 20, // fecPercentage ENCRYPTION_MODE_NEVER, // lan_encryption_mode ENCRYPTION_MODE_OPPORTUNISTIC, // wan_encryption_mode }; nvhttp_t nvhttp { "lan", // origin web manager PRIVATE_KEY_FILE, CERTIFICATE_FILE, platf::get_host_name(), // sunshine_name, "sunshine_state.json"s, // file_state {}, // external_ip }; input_t input { { {0x10, 0xA0}, {0x11, 0xA2}, {0x12, 0xA4}, }, -1ms, // back_button_timeout 500ms, // key_repeat_delay std::chrono::duration {1 / 24.9}, // key_repeat_period { platf::supported_gamepads(nullptr).front().name.data(), platf::supported_gamepads(nullptr).front().name.size(), }, // Default gamepad true, // back as touchpad click enabled (manual DS4 only) true, // client gamepads with motion events are emulated as DS4 true, // client gamepads with touchpads are emulated as DS4 true, // ds5_inputtino_randomize_mac true, // keyboard enabled true, // mouse enabled true, // controller enabled true, // always send scancodes true, // high resolution scrolling true, // native pen/touch support false, // enable input only mode true, // forward_rumble }; sunshine_t sunshine { false, // hide_tray_controls true, // enable_pairing true, // enable_discovery false, // envvar_compatibility_mode "en", // locale 2, // min_log_level 0, // flags {}, // User file {}, // Username {}, // Password {}, // Password Salt platf::appdata().string() + "/sunshine.conf", // config file {}, // cmd args 47989, // Base port number "ipv4", // Address family platf::appdata().string() + "/sunshine.log", // log file false, // notify_pre_releases false, // legacy_ordering true, // system_tray {}, // prep commands {}, // state commands {}, // server commands }; bool endline(char ch) { return ch == '\r' || ch == '\n'; } bool space_tab(char ch) { return ch == ' ' || ch == '\t'; } bool whitespace(char ch) { return space_tab(ch) || endline(ch); } std::string to_string(const char *begin, const char *end) { std::string result; KITTY_WHILE_LOOP(auto pos = begin, pos != end, { auto comment = std::find(pos, end, '#'); auto endl = std::find_if(comment, end, endline); result.append(pos, comment); pos = endl; }) return result; } template It skip_list(It skipper, It end) { int stack = 1; while (skipper != end && stack) { if (*skipper == '[') { ++stack; } if (*skipper == ']') { --stack; } ++skipper; } return skipper; } std::pair< ::std::string_view::const_iterator, ::std::optional>> parse_option(::std::string_view::const_iterator begin, ::std::string_view::const_iterator end) { begin = std::find_if_not(begin, end, whitespace); auto endl = std::find_if(begin, end, endline); auto endc = std::find(begin, endl, '#'); endc = std::find_if(std::make_reverse_iterator(endc), std::make_reverse_iterator(begin), std::not_fn(whitespace)).base(); auto eq = std::find(begin, endc, '='); if (eq == endc || eq == begin) { return std::make_pair(endl, std::nullopt); } auto end_name = std::find_if_not(std::make_reverse_iterator(eq), std::make_reverse_iterator(begin), space_tab).base(); auto begin_val = std::find_if_not(eq + 1, endc, space_tab); if (begin_val == endl) { return std::make_pair(endl, std::nullopt); } // Lists might contain newlines if (*begin_val == '[') { endl = skip_list(begin_val + 1, end); // Check if we reached the end of the file without finding a closing bracket // We know we have a valid closing bracket if: // 1. We didn't reach the end, or // 2. We reached the end but the last character was the matching closing bracket if (endl == end && end == begin_val + 1) { BOOST_LOG(warning) << "config: Missing ']' in config option: " << to_string(begin, end_name); return std::make_pair(endl, std::nullopt); } } return std::make_pair( endl, std::make_pair(to_string(begin, end_name), to_string(begin_val, endl)) ); } std::unordered_map parse_config(const ::std::string_view &file_content) { std::unordered_map vars; auto pos = std::begin(file_content); auto end = std::end(file_content); while (pos < end) { // auto newline = std::find_if(pos, end, [](auto ch) { return ch == '\n' || ch == '\r'; }); TUPLE_2D(endl, var, parse_option(pos, end)); pos = endl; if (pos != end) { pos += (*pos == '\r') ? 2 : 1; } if (!var) { continue; } vars.emplace(std::move(*var)); } return vars; } void string_f(std::unordered_map &vars, const std::string &name, std::string &input) { auto it = vars.find(name); if (it == std::end(vars)) { return; } input = std::move(it->second); vars.erase(it); } template void generic_f(std::unordered_map &vars, const std::string &name, T &input, F &&f) { std::string tmp; string_f(vars, name, tmp); if (!tmp.empty()) { input = f(tmp); } } void string_restricted_f(std::unordered_map &vars, const std::string &name, std::string &input, const std::vector<::std::string_view> &allowed_vals) { std::string temp; string_f(vars, name, temp); for (auto &allowed_val : allowed_vals) { if (temp == allowed_val) { input = std::move(temp); return; } } } void path_f(std::unordered_map &vars, const std::string &name, fs::path &input) { // appdata needs to be retrieved once only static auto appdata = platf::appdata(); std::string temp; string_f(vars, name, temp); if (!temp.empty()) { input = temp; } if (input.is_relative()) { input = appdata / input; } auto dir = input; dir.remove_filename(); // Ensure the directories exists if (!fs::exists(dir)) { fs::create_directories(dir); } } void path_f(std::unordered_map &vars, const std::string &name, std::string &input) { fs::path temp = input; path_f(vars, name, temp); input = temp.string(); } void int_f(std::unordered_map &vars, const std::string &name, int &input) { auto it = vars.find(name); if (it == std::end(vars)) { return; } ::std::string_view val = it->second; // If value is something like: "756" instead of 756 if (val.size() >= 2 && val[0] == '"') { val = val.substr(1, val.size() - 2); } // If that integer is in hexadecimal if (val.size() >= 2 && val.substr(0, 2) == "0x"sv) { input = util::from_hex(val.substr(2)); } else { input = util::from_view(val); } vars.erase(it); } void int_f(std::unordered_map &vars, const std::string &name, ::std::optional &input) { auto it = vars.find(name); if (it == std::end(vars)) { return; } ::std::string_view val = it->second; // If value is something like: "756" instead of 756 if (val.size() >= 2 && val[0] == '"') { val = val.substr(1, val.size() - 2); } // If that integer is in hexadecimal if (val.size() >= 2 && val.substr(0, 2) == "0x"sv) { input = util::from_hex(val.substr(2)); } else { input = util::from_view(val); } vars.erase(it); } template void int_f(std::unordered_map &vars, const std::string &name, int &input, F &&f) { std::string tmp; string_f(vars, name, tmp); if (!tmp.empty()) { input = f(tmp); } } template void int_f(std::unordered_map &vars, const std::string &name, ::std::optional &input, F &&f) { std::string tmp; string_f(vars, name, tmp); if (!tmp.empty()) { input = f(tmp); } } void int_between_f(std::unordered_map &vars, const std::string &name, int &input, const std::pair &range) { int temp = input; int_f(vars, name, temp); TUPLE_2D_REF(lower, upper, range); if (temp >= lower && temp <= upper) { input = temp; } } bool to_bool(std::string &boolean) { std::for_each(std::begin(boolean), std::end(boolean), [](char ch) { return (char) std::tolower(ch); }); return boolean == "true"sv || boolean == "yes"sv || boolean == "enable"sv || boolean == "enabled"sv || boolean == "on"sv || (std::find(std::begin(boolean), std::end(boolean), '1') != std::end(boolean)); } void bool_f(std::unordered_map &vars, const std::string &name, bool &input) { std::string tmp; string_f(vars, name, tmp); if (tmp.empty()) { return; } input = to_bool(tmp); } void double_f(std::unordered_map &vars, const std::string &name, double &input) { std::string tmp; string_f(vars, name, tmp); if (tmp.empty()) { return; } char *c_str_p; auto val = std::strtod(tmp.c_str(), &c_str_p); if (c_str_p == tmp.c_str()) { return; } input = val; } void double_between_f(std::unordered_map &vars, const std::string &name, double &input, const std::pair &range) { double temp = input; double_f(vars, name, temp); TUPLE_2D_REF(lower, upper, range); if (temp >= lower && temp <= upper) { input = temp; } } void list_string_f(std::unordered_map &vars, const std::string &name, std::vector &input) { std::string string; string_f(vars, name, string); if (string.empty()) { return; } input.clear(); auto begin = std::cbegin(string); if (*begin == '[') { ++begin; } begin = std::find_if_not(begin, std::cend(string), whitespace); if (begin == std::cend(string)) { return; } auto pos = begin; while (pos < std::cend(string)) { if (*pos == '[') { pos = skip_list(pos + 1, std::cend(string)) + 1; } else if (*pos == ']') { break; } else if (*pos == ',') { input.emplace_back(begin, pos); pos = begin = std::find_if_not(pos + 1, std::cend(string), whitespace); } else { ++pos; } } if (pos != begin) { input.emplace_back(begin, pos); } } void list_prep_cmd_f(std::unordered_map &vars, const std::string &name, std::vector &input) { std::string string; string_f(vars, name, string); std::stringstream jsonStream; // check if string is empty, i.e. when the value doesn't exist in the config file if (string.empty()) { return; } // We need to add a wrapping object to make it valid JSON, otherwise ptree cannot parse it. jsonStream << "{\"prep_cmd\":" << string << "}"; boost::property_tree::ptree jsonTree; boost::property_tree::read_json(jsonStream, jsonTree); for (auto &[_, prep_cmd] : jsonTree.get_child("prep_cmd"s)) { auto do_cmd = prep_cmd.get_optional("do"s); auto undo_cmd = prep_cmd.get_optional("undo"s); auto elevated = prep_cmd.get_optional("elevated"s); input.emplace_back(do_cmd.value_or(""), undo_cmd.value_or(""), elevated.value_or(false)); } } void list_server_cmd_f(std::unordered_map &vars, const std::string &name, std::vector &input) { std::string string; string_f(vars, name, string); std::stringstream jsonStream; // check if string is empty, i.e. when the value doesn't exist in the config file if (string.empty()) { return; } // We need to add a wrapping object to make it valid JSON, otherwise ptree cannot parse it. jsonStream << "{\"server_cmd\":" << string << "}"; boost::property_tree::ptree jsonTree; boost::property_tree::read_json(jsonStream, jsonTree); for (auto &[_, prep_cmd] : jsonTree.get_child("server_cmd"s)) { auto cmd_name = prep_cmd.get_optional("name"s); auto cmd_val = prep_cmd.get_optional("cmd"s); auto elevated = prep_cmd.get_optional("elevated"s); input.emplace_back(cmd_name.value_or(""), cmd_val.value_or(""), elevated.value_or(false)); } } void list_int_f(std::unordered_map &vars, const std::string &name, std::vector &input) { std::vector list; list_string_f(vars, name, list); // check if list is empty, i.e. when the value doesn't exist in the config file if (list.empty()) { return; } // The framerate list must be cleared before adding values from the file configuration. // If the list is not cleared, then the specified parameters do not affect the behavior of the sunshine server. // 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. input.clear(); for (auto &el : list) { ::std::string_view val = el; // If value is something like: "756" instead of 756 if (val.size() >= 2 && val[0] == '"') { val = val.substr(1, val.size() - 2); } int tmp; // If the integer is a hexadecimal if (val.size() >= 2 && val.substr(0, 2) == "0x"sv) { tmp = util::from_hex(val.substr(2)); } else { tmp = util::from_view(val); } input.emplace_back(tmp); } } void map_int_int_f(std::unordered_map &vars, const std::string &name, std::unordered_map &input) { std::vector list; list_int_f(vars, name, list); // The list needs to be a multiple of 2 if (list.size() % 2) { BOOST_LOG(warning) << "config: expected "sv << name << " to have a multiple of two elements --> not "sv << list.size(); return; } int x = 0; while (x < list.size()) { auto key = list[x++]; auto val = list[x++]; input.emplace(key, val); } } int apply_flags(const char *line) { int ret = 0; while (*line != '\0') { switch (*line) { case '0': config::sunshine.flags[config::flag::PIN_STDIN].flip(); break; case '1': config::sunshine.flags[config::flag::FRESH_STATE].flip(); break; case '2': config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE].flip(); break; case 'p': config::sunshine.flags[config::flag::UPNP].flip(); break; default: BOOST_LOG(warning) << "config: Unrecognized flag: ["sv << *line << ']' << std::endl; ret = -1; } ++line; } return ret; } std::vector<::std::string_view> &get_supported_gamepad_options() { const auto options = platf::supported_gamepads(nullptr); static std::vector<::std::string_view> opts {}; opts.reserve(options.size()); for (auto &opt : options) { opts.emplace_back(opt.name); } return opts; } void apply_config(std::unordered_map &&vars) { #ifndef __ANDROID__ // TODO: Android can possibly support this if (!fs::exists(stream.file_apps.c_str())) { fs::copy_file(SUNSHINE_ASSETS_DIR "/apps.json", stream.file_apps); } #endif for (auto &[name, val] : vars) { #ifdef _WIN32 BOOST_LOG(info) << "config: ["sv << name << "] -- ["sv << utf8ToAcp(val) << ']'; #else BOOST_LOG(info) << "config: ["sv << name << "] -- ["sv << val << ']'; #endif modified_config_settings[name] = val; } bool_f(vars, "headless_mode", video.headless_mode); bool_f(vars, "limit_framerate", video.limit_framerate); bool_f(vars, "double_refreshrate", video.double_refreshrate); int_f(vars, "qp", video.qp); int_between_f(vars, "hevc_mode", video.hevc_mode, {0, 3}); int_between_f(vars, "av1_mode", video.av1_mode, {0, 3}); int_f(vars, "min_threads", video.min_threads); string_f(vars, "sw_preset", video.sw.sw_preset); if (!video.sw.sw_preset.empty()) { video.sw.svtav1_preset = sw::svtav1_preset_from_view(video.sw.sw_preset); } string_f(vars, "sw_tune", video.sw.sw_tune); int_between_f(vars, "nvenc_preset", video.nv.quality_preset, {1, 7}); int_between_f(vars, "nvenc_vbv_increase", video.nv.vbv_percentage_increase, {0, 400}); bool_f(vars, "nvenc_spatial_aq", video.nv.adaptive_quantization); generic_f(vars, "nvenc_twopass", video.nv.two_pass, nv::twopass_from_view); bool_f(vars, "nvenc_h264_cavlc", video.nv.h264_cavlc); bool_f(vars, "nvenc_intra_refresh", video.nv.intra_refresh); bool_f(vars, "nvenc_realtime_hags", video.nv_realtime_hags); bool_f(vars, "nvenc_opengl_vulkan_on_dxgi", video.nv_opengl_vulkan_on_dxgi); bool_f(vars, "nvenc_latency_over_power", video.nv_sunshine_high_power_mode); #if !defined(__ANDROID__) && !defined(__APPLE__) video.nv_legacy.preset = video.nv.quality_preset + 11; video.nv_legacy.multipass = video.nv.two_pass == nvenc::nvenc_two_pass::quarter_resolution ? NV_ENC_TWO_PASS_QUARTER_RESOLUTION : video.nv.two_pass == nvenc::nvenc_two_pass::full_resolution ? NV_ENC_TWO_PASS_FULL_RESOLUTION : NV_ENC_MULTI_PASS_DISABLED; video.nv_legacy.h264_coder = video.nv.h264_cavlc ? NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC : NV_ENC_H264_ENTROPY_CODING_MODE_CABAC; video.nv_legacy.aq = video.nv.adaptive_quantization; video.nv_legacy.vbv_percentage_increase = video.nv.vbv_percentage_increase; #endif int_f(vars, "qsv_preset", video.qsv.qsv_preset, qsv::preset_from_view); int_f(vars, "qsv_coder", video.qsv.qsv_cavlc, qsv::coder_from_view); bool_f(vars, "qsv_slow_hevc", video.qsv.qsv_slow_hevc); std::string quality; string_f(vars, "amd_quality", quality); if (!quality.empty()) { video.amd.amd_quality_h264 = amd::quality_from_view(quality, video.amd.amd_quality_h264); video.amd.amd_quality_hevc = amd::quality_from_view(quality, video.amd.amd_quality_hevc); video.amd.amd_quality_av1 = amd::quality_from_view(quality, video.amd.amd_quality_av1); } std::string rc; string_f(vars, "amd_rc", rc); int_f(vars, "amd_coder", video.amd.amd_coder, amd::coder_from_view); if (!rc.empty()) { video.amd.amd_rc_h264 = amd::rc_from_view(rc, video.amd.amd_rc_h264); video.amd.amd_rc_hevc = amd::rc_from_view(rc, video.amd.amd_rc_hevc); video.amd.amd_rc_av1 = amd::rc_from_view(rc, video.amd.amd_rc_av1); } std::string usage; string_f(vars, "amd_usage", usage); if (!usage.empty()) { video.amd.amd_usage_h264 = amd::usage_from_view(usage, video.amd.amd_usage_h264); video.amd.amd_usage_hevc = amd::usage_from_view(usage, video.amd.amd_usage_hevc); video.amd.amd_usage_av1 = amd::usage_from_view(usage, video.amd.amd_usage_av1); } bool_f(vars, "amd_preanalysis", (bool &) video.amd.amd_preanalysis); bool_f(vars, "amd_vbaq", (bool &) video.amd.amd_vbaq); bool_f(vars, "amd_enforce_hrd", (bool &) video.amd.amd_enforce_hrd); int_f(vars, "vt_coder", video.vt.vt_coder, vt::coder_from_view); int_f(vars, "vt_software", video.vt.vt_allow_sw, vt::allow_software_from_view); int_f(vars, "vt_software", video.vt.vt_require_sw, vt::force_software_from_view); int_f(vars, "vt_realtime", video.vt.vt_realtime, vt::rt_from_view); bool_f(vars, "vaapi_strict_rc_buffer", video.vaapi.strict_rc_buffer); string_f(vars, "capture", video.capture); string_f(vars, "encoder", video.encoder); string_f(vars, "adapter_name", video.adapter_name); string_f(vars, "output_name", video.output_name); generic_f(vars, "dd_configuration_option", video.dd.configuration_option, dd::config_option_from_view); generic_f(vars, "dd_resolution_option", video.dd.resolution_option, dd::resolution_option_from_view); string_f(vars, "dd_manual_resolution", video.dd.manual_resolution); generic_f(vars, "dd_refresh_rate_option", video.dd.refresh_rate_option, dd::refresh_rate_option_from_view); string_f(vars, "dd_manual_refresh_rate", video.dd.manual_refresh_rate); generic_f(vars, "dd_hdr_option", video.dd.hdr_option, dd::hdr_option_from_view); { int value = -1; int_between_f(vars, "dd_config_revert_delay", value, {0, std::numeric_limits::max()}); if (value >= 0) { video.dd.config_revert_delay = std::chrono::milliseconds {value}; } } bool_f(vars, "dd_config_revert_on_disconnect", video.dd.config_revert_on_disconnect); generic_f(vars, "dd_mode_remapping", video.dd.mode_remapping, dd::mode_remapping_from_view); { int value = 0; int_between_f(vars, "dd_wa_hdr_toggle_delay", value, {0, 3000}); video.dd.wa.hdr_toggle_delay = std::chrono::milliseconds {value}; } int_f(vars, "max_bitrate", video.max_bitrate); double_between_f(vars, "minimum_fps_target", video.minimum_fps_target, {0.0, 1000.0}); string_f(vars, "fallback_mode", video.fallback_mode); bool_f(vars, "isolated_virtual_display_option", video.isolated_virtual_display_option); bool_f(vars, "ignore_encoder_probe_failure", video.ignore_encoder_probe_failure); path_f(vars, "pkey", nvhttp.pkey); path_f(vars, "cert", nvhttp.cert); string_f(vars, "sunshine_name", nvhttp.sunshine_name); path_f(vars, "log_path", config::sunshine.log_file); path_f(vars, "file_state", nvhttp.file_state); // Must be run after "file_state" config::sunshine.credentials_file = config::nvhttp.file_state; path_f(vars, "credentials_file", config::sunshine.credentials_file); string_f(vars, "external_ip", nvhttp.external_ip); list_prep_cmd_f(vars, "global_prep_cmd", config::sunshine.prep_cmds); list_prep_cmd_f(vars, "global_state_cmd", config::sunshine.state_cmds); list_server_cmd_f(vars, "server_cmd", config::sunshine.server_cmds); string_f(vars, "audio_sink", audio.sink); string_f(vars, "virtual_sink", audio.virtual_sink); bool_f(vars, "stream_audio", audio.stream); bool_f(vars, "install_steam_audio_drivers", audio.install_steam_drivers); bool_f(vars, "keep_sink_default", audio.keep_default); bool_f(vars, "auto_capture_sink", audio.auto_capture); string_restricted_f(vars, "origin_web_ui_allowed", nvhttp.origin_web_ui_allowed, {"pc"sv, "lan"sv, "wan"sv}); int to = -1; int_between_f(vars, "ping_timeout", to, {-1, std::numeric_limits::max()}); if (to != -1) { stream.ping_timeout = std::chrono::milliseconds(to); } int_between_f(vars, "lan_encryption_mode", stream.lan_encryption_mode, {0, 2}); int_between_f(vars, "wan_encryption_mode", stream.wan_encryption_mode, {0, 2}); path_f(vars, "file_apps", stream.file_apps); int_between_f(vars, "fec_percentage", stream.fec_percentage, {1, 255}); map_int_int_f(vars, "keybindings"s, input.keybindings); // This config option will only be used by the UI // When editing in the config file itself, use "keybindings" bool map_rightalt_to_win = false; bool_f(vars, "key_rightalt_to_key_win", map_rightalt_to_win); if (map_rightalt_to_win) { input.keybindings.emplace(0xA5, 0x5B); } to = std::numeric_limits::min(); int_f(vars, "back_button_timeout", to); if (to > std::numeric_limits::min()) { input.back_button_timeout = std::chrono::milliseconds {to}; } double repeat_frequency {0}; double_between_f(vars, "key_repeat_frequency", repeat_frequency, {0, std::numeric_limits::max()}); if (repeat_frequency > 0) { config::input.key_repeat_period = std::chrono::duration {1 / repeat_frequency}; } to = -1; int_f(vars, "key_repeat_delay", to); if (to >= 0) { input.key_repeat_delay = std::chrono::milliseconds {to}; } string_restricted_f(vars, "gamepad"s, input.gamepad, get_supported_gamepad_options()); bool_f(vars, "ds4_back_as_touchpad_click", input.ds4_back_as_touchpad_click); bool_f(vars, "motion_as_ds4", input.motion_as_ds4); bool_f(vars, "touchpad_as_ds4", input.touchpad_as_ds4); bool_f(vars, "ds5_inputtino_randomize_mac", input.ds5_inputtino_randomize_mac); bool_f(vars, "mouse", input.mouse); bool_f(vars, "keyboard", input.keyboard); bool_f(vars, "controller", input.controller); bool_f(vars, "always_send_scancodes", input.always_send_scancodes); bool_f(vars, "high_resolution_scrolling", input.high_resolution_scrolling); bool_f(vars, "native_pen_touch", input.native_pen_touch); bool_f(vars, "enable_input_only_mode", input.enable_input_only_mode); bool_f(vars, "system_tray", sunshine.system_tray); bool_f(vars, "hide_tray_controls", sunshine.hide_tray_controls); bool_f(vars, "enable_pairing", sunshine.enable_pairing); bool_f(vars, "enable_discovery", sunshine.enable_discovery); bool_f(vars, "envvar_compatibility_mode", sunshine.envvar_compatibility_mode); bool_f(vars, "notify_pre_releases", sunshine.notify_pre_releases); bool_f(vars, "legacy_ordering", sunshine.legacy_ordering); bool_f(vars, "forward_rumble", input.forward_rumble); int port = sunshine.port; int_between_f(vars, "port"s, port, {1024 + nvhttp::PORT_HTTPS, 65535 - rtsp_stream::RTSP_SETUP_PORT}); sunshine.port = (std::uint16_t) port; string_restricted_f(vars, "address_family", sunshine.address_family, {"ipv4"sv, "both"sv}); bool upnp = false; bool_f(vars, "upnp"s, upnp); if (upnp) { config::sunshine.flags[config::flag::UPNP].flip(); } string_restricted_f(vars, "locale", config::sunshine.locale, { "bg"sv, // Bulgarian "cs"sv, // Czech "de"sv, // German "en"sv, // English "en_GB"sv, // English (UK) "en_US"sv, // English (US) "es"sv, // Spanish "fr"sv, // French "hu"sv, // Hungarian "it"sv, // Italian "ja"sv, // Japanese "ko"sv, // Korean "pl"sv, // Polish "pt"sv, // Portuguese "pt_BR"sv, // Portuguese (Brazilian) "ru"sv, // Russian "sv"sv, // Swedish "tr"sv, // Turkish "uk"sv, // Ukrainian "vi"sv, // Vietnamese "zh"sv, // Chinese "zh_TW"sv, // Chinese (Traditional) }); std::string log_level_string; string_f(vars, "min_log_level", log_level_string); if (!log_level_string.empty()) { if (log_level_string == "verbose"sv) { sunshine.min_log_level = 0; } else if (log_level_string == "debug"sv) { sunshine.min_log_level = 1; } else if (log_level_string == "info"sv) { sunshine.min_log_level = 2; } else if (log_level_string == "warning"sv) { sunshine.min_log_level = 3; } else if (log_level_string == "error"sv) { sunshine.min_log_level = 4; } else if (log_level_string == "fatal"sv) { sunshine.min_log_level = 5; } else if (log_level_string == "none"sv) { sunshine.min_log_level = 6; } else { // accept digit directly auto val = log_level_string[0]; if (val >= '0' && val < '7') { sunshine.min_log_level = val - '0'; } } } auto it = vars.find("flags"s); if (it != std::end(vars)) { apply_flags(it->second.c_str()); vars.erase(it); } if (sunshine.min_log_level <= 3) { for (auto &[var, _] : vars) { std::cout << "Warning: Unrecognized configurable option ["sv << var << ']' << std::endl; } } ::video::active_hevc_mode = video.hevc_mode; ::video::active_av1_mode = video.av1_mode; } int parse(int argc, char *argv[]) { std::unordered_map cmd_vars; #ifdef _WIN32 bool shortcut_launch = false; bool service_admin_launch = false; #endif for (auto x = 1; x < argc; ++x) { auto line = argv[x]; if (line == "--help"sv) { logging::print_help(*argv); return 1; } #ifdef _WIN32 else if (line == "--shortcut"sv) { shortcut_launch = true; } else if (line == "--shortcut-admin"sv) { service_admin_launch = true; } #endif else if (*line == '-') { if (*(line + 1) == '-') { sunshine.cmd.name = line + 2; sunshine.cmd.argc = argc - x - 1; sunshine.cmd.argv = argv + x + 1; break; } if (apply_flags(line + 1)) { logging::print_help(*argv); return -1; } } else { auto line_end = line + strlen(line); auto pos = std::find(line, line_end, '='); if (pos == line_end) { sunshine.config_file = line; } else { TUPLE_EL(var, 1, parse_option(line, line_end)); if (!var) { logging::print_help(*argv); return -1; } TUPLE_EL_REF(name, 0, *var); auto it = cmd_vars.find(name); if (it != std::end(cmd_vars)) { cmd_vars.erase(it); } cmd_vars.emplace(std::move(*var)); } } } bool config_loaded = false; try { // Create appdata folder if it does not exist file_handler::make_directory(platf::appdata().string()); // Create empty config file if it does not exist if (!fs::exists(sunshine.config_file)) { auto cfg_file = std::ofstream {sunshine.config_file}; #ifdef _WIN32 cfg_file << "server_cmd = [{\"name\":\"Bubbles\",\"cmd\":\"bubbles.scr\",\"elevated\":false}]\n"; #endif } // Read config file auto vars = parse_config(file_handler::read_file(sunshine.config_file.c_str())); for (auto &[name, value] : cmd_vars) { vars.insert_or_assign(std::move(name), std::move(value)); } // Apply the config. Note: This will try to create any paths // referenced in the config, so we may receive exceptions if // the path is incorrect or inaccessible. apply_config(std::move(vars)); config_loaded = true; } catch (const std::filesystem::filesystem_error &err) { BOOST_LOG(fatal) << "Failed to apply config: "sv << err.what(); } catch (const boost::filesystem::filesystem_error &err) { BOOST_LOG(fatal) << "Failed to apply config: "sv << err.what(); } #ifdef _WIN32 // UCRT64 raises an access denied exception if launching from the shortcut // as non-admin and the config folder is not yet present; we can defer // so that service instance will do the work instead. if (!config_loaded && !shortcut_launch) { BOOST_LOG(fatal) << "To relaunch Apollo successfully, use the shortcut in the Start Menu. Do not run sunshine.exe manually."sv; std::this_thread::sleep_for(10s); #else if (!config_loaded) { #endif return -1; } #ifdef _WIN32 // We have to wait until the config is loaded to handle these launches, // because we need to have the correct base port loaded in our config. // Exception: UCRT64 shortcut_launch instances may have no config loaded due to // insufficient permissions to create folder; port defaults will be acceptable. if (service_admin_launch) { // This is a relaunch as admin to start the service service_ctrl::start_service(); // Always return 1 to ensure Sunshine doesn't start normally return 1; } if (shortcut_launch) { if (!service_ctrl::is_service_running()) { // If the service isn't running, relaunch ourselves as admin to start it WCHAR executable[MAX_PATH]; GetModuleFileNameW(nullptr, executable, ARRAYSIZE(executable)); SHELLEXECUTEINFOW shell_exec_info {}; shell_exec_info.cbSize = sizeof(shell_exec_info); shell_exec_info.fMask = SEE_MASK_NOASYNC | SEE_MASK_NO_CONSOLE | SEE_MASK_NOCLOSEPROCESS; shell_exec_info.lpVerb = L"runas"; shell_exec_info.lpFile = executable; shell_exec_info.lpParameters = L"--shortcut-admin"; shell_exec_info.nShow = SW_NORMAL; if (!ShellExecuteExW(&shell_exec_info)) { auto winerr = GetLastError(); BOOST_LOG(error) << "Failed executing shell command: " << winerr << std::endl; return 1; } // Wait for the elevated process to finish starting the service WaitForSingleObject(shell_exec_info.hProcess, INFINITE); CloseHandle(shell_exec_info.hProcess); // Wait for the UI to be ready for connections service_ctrl::wait_for_ui_ready(); } // Launch the web UI launch_ui(); // Always return 1 to ensure Sunshine doesn't start normally return 1; } #endif return 0; } } // namespace config ================================================ FILE: src/config.h ================================================ /** * @file src/config.h * @brief Declarations for the configuration of Sunshine. */ #pragma once // standard includes #include #include #include #include #include #include // local includes #include "nvenc/nvenc_config.h" namespace config { // track modified config options inline std::unordered_map modified_config_settings; struct video_t { bool headless_mode; bool limit_framerate; bool double_refreshrate; // ffmpeg params int qp; // higher == more compression and less quality int hevc_mode; int av1_mode; int min_threads; // Minimum number of threads/slices for CPU encoding struct { std::string sw_preset; std::string sw_tune; std::optional svtav1_preset; } sw; nvenc::nvenc_config nv; bool nv_realtime_hags; bool nv_opengl_vulkan_on_dxgi; bool nv_sunshine_high_power_mode; struct { int preset; int multipass; int h264_coder; int aq; int vbv_percentage_increase; } nv_legacy; struct { std::optional qsv_preset; std::optional qsv_cavlc; bool qsv_slow_hevc; } qsv; struct { std::optional amd_usage_h264; std::optional amd_usage_hevc; std::optional amd_usage_av1; std::optional amd_rc_h264; std::optional amd_rc_hevc; std::optional amd_rc_av1; std::optional amd_enforce_hrd; std::optional amd_quality_h264; std::optional amd_quality_hevc; std::optional amd_quality_av1; std::optional amd_preanalysis; std::optional amd_vbaq; int amd_coder; } amd; struct { int vt_allow_sw; int vt_require_sw; int vt_realtime; int vt_coder; } vt; struct { bool strict_rc_buffer; } vaapi; std::string capture; std::string encoder; std::string adapter_name; std::string output_name; struct dd_t { struct workarounds_t { std::chrono::milliseconds hdr_toggle_delay; ///< Specify whether to apply HDR high-contrast color workaround and what delay to use. }; enum class config_option_e { disabled, ///< Disable the configuration for the device. verify_only, ///< @seealso{display_device::SingleDisplayConfiguration::DevicePreparation} ensure_active, ///< @seealso{display_device::SingleDisplayConfiguration::DevicePreparation} ensure_primary, ///< @seealso{display_device::SingleDisplayConfiguration::DevicePreparation} ensure_only_display ///< @seealso{display_device::SingleDisplayConfiguration::DevicePreparation} }; enum class resolution_option_e { disabled, ///< Do not change resolution. automatic, ///< Change resolution and use the one received from Moonlight. manual ///< Change resolution and use the manually provided one. }; enum class refresh_rate_option_e { disabled, ///< Do not change refresh rate. automatic, ///< Change refresh rate and use the one received from Moonlight. manual ///< Change refresh rate and use the manually provided one. }; enum class hdr_option_e { disabled, ///< Do not change HDR settings. automatic ///< Change HDR settings and use the state requested by Moonlight. }; struct mode_remapping_entry_t { std::string requested_resolution; std::string requested_fps; std::string final_resolution; std::string final_refresh_rate; }; struct mode_remapping_t { std::vector mixed; ///< To be used when `resolution_option` and `refresh_rate_option` is set to `automatic`. std::vector resolution_only; ///< To be use when only `resolution_option` is set to `automatic`. std::vector refresh_rate_only; ///< To be use when only `refresh_rate_option` is set to `automatic`. }; config_option_e configuration_option; resolution_option_e resolution_option; std::string manual_resolution; ///< Manual resolution in case `resolution_option == resolution_option_e::manual`. refresh_rate_option_e refresh_rate_option; std::string manual_refresh_rate; ///< Manual refresh rate in case `refresh_rate_option == refresh_rate_option_e::manual`. hdr_option_e hdr_option; std::chrono::milliseconds config_revert_delay; ///< Time to wait until settings are reverted (after stream ends/app exists). bool config_revert_on_disconnect; ///< Specify whether to revert display configuration on client disconnect. mode_remapping_t mode_remapping; workarounds_t wa; } dd; int max_bitrate; // Maximum bitrate, sets ceiling in kbps for bitrate requested from client double minimum_fps_target; ///< Lowest framerate that will be used when streaming. Range 0-1000, 0 = half of client's requested framerate. std::string fallback_mode; bool isolated_virtual_display_option; bool ignore_encoder_probe_failure; }; struct audio_t { std::string sink; std::string virtual_sink; bool stream; bool install_steam_drivers; bool keep_default; bool auto_capture; }; constexpr int ENCRYPTION_MODE_NEVER = 0; // Never use video encryption, even if the client supports it constexpr int ENCRYPTION_MODE_OPPORTUNISTIC = 1; // Use video encryption if available, but stream without it if not supported constexpr int ENCRYPTION_MODE_MANDATORY = 2; // Always use video encryption and refuse clients that can't encrypt struct stream_t { std::chrono::milliseconds ping_timeout; std::string file_apps; int fec_percentage; // Video encryption settings for LAN and WAN streams int lan_encryption_mode; int wan_encryption_mode; }; struct nvhttp_t { // Could be any of the following values: // pc|lan|wan std::string origin_web_ui_allowed; std::string pkey; std::string cert; std::string sunshine_name; std::string file_state; std::string external_ip; }; struct input_t { std::unordered_map keybindings; std::chrono::milliseconds back_button_timeout; std::chrono::milliseconds key_repeat_delay; std::chrono::duration key_repeat_period; std::string gamepad; bool ds4_back_as_touchpad_click; bool motion_as_ds4; bool touchpad_as_ds4; bool ds5_inputtino_randomize_mac; bool keyboard; bool mouse; bool controller; bool always_send_scancodes; bool high_resolution_scrolling; bool native_pen_touch; bool enable_input_only_mode; bool forward_rumble; }; namespace flag { enum flag_e : std::size_t { PIN_STDIN = 0, ///< Read PIN from stdin instead of http FRESH_STATE, ///< Do not load or save state FORCE_VIDEO_HEADER_REPLACE, ///< force replacing headers inside video data UPNP, ///< Try Universal Plug 'n Play CONST_PIN, ///< Use "universal" pin FLAG_SIZE ///< Number of flags }; } // namespace flag struct prep_cmd_t { prep_cmd_t(std::string &&do_cmd, std::string &&undo_cmd, bool &&elevated): do_cmd(std::move(do_cmd)), undo_cmd(std::move(undo_cmd)), elevated(std::move(elevated)) { } explicit prep_cmd_t(std::string &&do_cmd, bool &&elevated): do_cmd(std::move(do_cmd)), elevated(std::move(elevated)) { } std::string do_cmd; std::string undo_cmd; bool elevated; }; struct server_cmd_t { server_cmd_t(std::string &&cmd_name, std::string &&cmd_val, bool &&elevated): cmd_name(std::move(cmd_name)), cmd_val(std::move(cmd_val)), elevated(std::move(elevated)) { } std::string cmd_name; std::string cmd_val; bool elevated; }; struct sunshine_t { bool hide_tray_controls; bool enable_pairing; bool enable_discovery; bool envvar_compatibility_mode; std::string locale; int min_log_level; std::bitset flags; std::string credentials_file; std::string username; std::string password; std::string salt; std::string config_file; struct cmd_t { std::string name; int argc; char **argv; } cmd; std::uint16_t port; std::string address_family; std::string log_file; bool notify_pre_releases; bool legacy_ordering; bool system_tray; std::vector prep_cmds; std::vector state_cmds; std::vector server_cmds; }; extern video_t video; extern audio_t audio; extern stream_t stream; extern nvhttp_t nvhttp; extern input_t input; extern sunshine_t sunshine; int parse(int argc, char *argv[]); std::unordered_map parse_config(const std::string_view &file_content); } // namespace config ================================================ FILE: src/confighttp.cpp ================================================ /** * @file src/confighttp.cpp * @brief Definitions for the Web UI Config HTTPS server. * * @todo Authentication, better handling of routes common to nvhttp, cleanup */ #define BOOST_BIND_GLOBAL_PLACEHOLDERS // standard includes #include #include #include #include #include #include #include #include // lib includes #include #include #include #include #include #include // local includes #include "config.h" #include "confighttp.h" #include "crypto.h" #include "display_device.h" #include "file_handler.h" #include "globals.h" #include "httpcommon.h" #include "logging.h" #include "network.h" #include "nvhttp.h" #include "platform/common.h" #include "process.h" #include "utility.h" #include "uuid.h" #ifdef _WIN32 #include "platform/windows/utils.h" #endif using namespace std::literals; namespace confighttp { namespace fs = std::filesystem; using https_server_t = SimpleWeb::Server; using args_t = SimpleWeb::CaseInsensitiveMultimap; using resp_https_t = std::shared_ptr::Response>; using req_https_t = std::shared_ptr::Request>; // Keep the base enum for client operations. enum class op_e { ADD, ///< Add client REMOVE ///< Remove client }; // SESSION COOKIE std::string sessionCookie; static std::chrono::time_point cookie_creation_time; /** * @brief Log the request details. * @param request The HTTP request object. */ void print_req(const req_https_t &request) { BOOST_LOG(debug) << "METHOD :: "sv << request->method; BOOST_LOG(debug) << "DESTINATION :: "sv << request->path; for (auto &[name, val] : request->header) { BOOST_LOG(debug) << name << " -- " << (name == "Authorization" ? "CREDENTIALS REDACTED" : val); } BOOST_LOG(debug) << " [--] "sv; for (auto &[name, val] : request->parse_query_string()) { BOOST_LOG(debug) << name << " -- " << val; } BOOST_LOG(debug) << " [--] "sv; } /** * @brief Send a response. * @param response The HTTP response object. * @param output_tree The JSON tree to send. */ void send_response(resp_https_t response, const nlohmann::json &output_tree) { SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "application/json"); headers.emplace("X-Frame-Options", "DENY"); headers.emplace("Content-Security-Policy", "frame-ancestors 'none';"); response->write(output_tree.dump(), headers); } /** * @brief Send a 401 Unauthorized response. * @param response The HTTP response object. * @param request The HTTP request object. */ void send_unauthorized(resp_https_t response, req_https_t request) { auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv; constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_unauthorized; nlohmann::json tree; tree["status_code"] = code; tree["status"] = false; tree["error"] = "Unauthorized"; const SimpleWeb::CaseInsensitiveMultimap headers { {"Content-Type", "application/json"}, {"X-Frame-Options", "DENY"}, {"Content-Security-Policy", "frame-ancestors 'none';"} }; response->write(code, tree.dump(), headers); } /** * @brief Send a redirect response. * @param response The HTTP response object. * @param request The HTTP request object. * @param path The path to redirect to. */ void send_redirect(resp_https_t response, req_https_t request, const char *path) { auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); BOOST_LOG(info) << "Web UI: ["sv << address << "] -- redirecting"sv; const SimpleWeb::CaseInsensitiveMultimap headers { {"Location", path}, {"X-Frame-Options", "DENY"}, {"Content-Security-Policy", "frame-ancestors 'none';"} }; response->write(SimpleWeb::StatusCode::redirection_temporary_redirect, headers); } /** * @brief Retrieve the value of a key from a cookie string. * @param cookieString The cookie header string. * @param key The key to search. * @return The value if found, empty string otherwise. */ std::string getCookieValue(const std::string& cookieString, const std::string& key) { std::string keyWithEqual = key + "="; std::size_t startPos = cookieString.find(keyWithEqual); if (startPos == std::string::npos) return ""; startPos += keyWithEqual.length(); std::size_t endPos = cookieString.find(";", startPos); if (endPos == std::string::npos) return cookieString.substr(startPos); return cookieString.substr(startPos, endPos - startPos); } /** * @brief Check if the IP origin is allowed. * @param response The HTTP response object. * @param request The HTTP request object. * @return True if allowed, false otherwise. */ bool checkIPOrigin(resp_https_t response, req_https_t request) { auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); auto ip_type = net::from_address(address); if (ip_type > http::origin_web_ui_allowed) { BOOST_LOG(info) << "Web UI: ["sv << address << "] -- denied"sv; response->write(SimpleWeb::StatusCode::client_error_forbidden); return false; } return true; } /** * @brief Authenticate the request. * @param response The HTTP response object. * @param request The HTTP request object. * @param needsRedirect Whether to redirect on failure. * @return True if authenticated, false otherwise. * * This function uses session cookies (if set) and ensures they have not expired. */ bool authenticate(resp_https_t response, req_https_t request, bool needsRedirect = false) { if (!checkIPOrigin(response, request)) return false; // If credentials not set, redirect to welcome. if (config::sunshine.username.empty()) { send_redirect(response, request, "/welcome"); return false; } // Guard: on failure, redirect if requested. auto fg = util::fail_guard([&]() { if (needsRedirect) { std::string redir_path = "/login?redir=."; redir_path += request->path; send_redirect(response, request, redir_path.c_str()); } else { send_unauthorized(response, request); } }); if (sessionCookie.empty()) return false; // Check for expiry if (std::chrono::steady_clock::now() - cookie_creation_time > SESSION_EXPIRE_DURATION) { sessionCookie.clear(); return false; } auto cookies = request->header.find("cookie"); if (cookies == request->header.end()) return false; auto authCookie = getCookieValue(cookies->second, "auth"); if (authCookie.empty() || util::hex(crypto::hash(authCookie + config::sunshine.salt)).to_string() != sessionCookie) return false; fg.disable(); return true; } /** * @brief Send a 404 Not Found response. * @param response The HTTP response object. * @param request The HTTP request object. */ void not_found(resp_https_t response, [[maybe_unused]] req_https_t request) { constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_not_found; nlohmann::json tree; tree["status_code"] = static_cast(code); tree["error"] = "Not Found"; SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "application/json"); headers.emplace("X-Frame-Options", "DENY"); headers.emplace("Content-Security-Policy", "frame-ancestors 'none';"); response->write(code, tree.dump(), headers); } /** * @brief Send a 400 Bad Request response. * @param response The HTTP response object. * @param request The HTTP request object. * @param error_message The error message. */ void bad_request(resp_https_t response, [[maybe_unused]] req_https_t request, const std::string &error_message = "Bad Request") { constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_bad_request; nlohmann::json tree; tree["status_code"] = static_cast(code); tree["status"] = false; tree["error"] = error_message; SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "application/json"); headers.emplace("X-Frame-Options", "DENY"); headers.emplace("Content-Security-Policy", "frame-ancestors 'none';"); response->write(code, tree.dump(), headers); } /** * @brief Validate the request content type and send bad request when mismatch. * @param response The HTTP response object. * @param request The HTTP request object. * @param contentType The required content type. */ bool validateContentType(resp_https_t response, req_https_t request, const std::string_view& contentType) { auto requestContentType = request->header.find("content-type"); if (requestContentType == request->header.end()) { bad_request(response, request, "Content type not provided"); return false; } // Extract the media type part before any parameters (e.g., charset) std::string actualContentType = requestContentType->second; size_t semicolonPos = actualContentType.find(';'); if (semicolonPos != std::string::npos) { actualContentType = actualContentType.substr(0, semicolonPos); } // Trim whitespace and convert to lowercase for case-insensitive comparison boost::algorithm::trim(actualContentType); boost::algorithm::to_lower(actualContentType); std::string expectedContentType(contentType); boost::algorithm::to_lower(expectedContentType); if (actualContentType != expectedContentType) { bad_request(response, request, "Content type mismatch"); return false; } return true; return true; } /** * @brief Get the index page. * @param response The HTTP response object. * @param request The HTTP request object. */ void getIndexPage(resp_https_t response, req_https_t request) { if (!authenticate(response, request, true)) { return; } print_req(request); std::string content = file_handler::read_file(WEB_DIR "index.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("X-Frame-Options", "DENY"); headers.emplace("Content-Security-Policy", "frame-ancestors 'none';"); response->write(content, headers); } /** * @brief Get the PIN page. * @param response The HTTP response object. * @param request The HTTP request object. */ void getPinPage(resp_https_t response, req_https_t request) { if (!authenticate(response, request, true)) { return; } print_req(request); std::string content = file_handler::read_file(WEB_DIR "pin.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("X-Frame-Options", "DENY"); headers.emplace("Content-Security-Policy", "frame-ancestors 'none';"); response->write(content, headers); } /** * @brief Get the apps page. * @param response The HTTP response object. * @param request The HTTP request object. */ void getAppsPage(resp_https_t response, req_https_t request) { if (!authenticate(response, request, true)) { return; } print_req(request); std::string content = file_handler::read_file(WEB_DIR "apps.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("X-Frame-Options", "DENY"); headers.emplace("Content-Security-Policy", "frame-ancestors 'none';"); headers.emplace("Access-Control-Allow-Origin", "https://images.igdb.com/"); response->write(content, headers); } /** * @brief Get the clients page. * @param response The HTTP response object. * @param request The HTTP request object. */ void getClientsPage(resp_https_t response, req_https_t request) { if (!authenticate(response, request, true)) { return; } print_req(request); std::string content = file_handler::read_file(WEB_DIR "clients.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("X-Frame-Options", "DENY"); headers.emplace("Content-Security-Policy", "frame-ancestors 'none';"); response->write(content, headers); } /** * @brief Get the configuration page. * @param response The HTTP response object. * @param request The HTTP request object. */ void getConfigPage(resp_https_t response, req_https_t request) { if (!authenticate(response, request, true)) { return; } print_req(request); std::string content = file_handler::read_file(WEB_DIR "config.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("X-Frame-Options", "DENY"); headers.emplace("Content-Security-Policy", "frame-ancestors 'none';"); response->write(content, headers); } /** * @brief Get the password page. * @param response The HTTP response object. * @param request The HTTP request object. */ void getPasswordPage(resp_https_t response, req_https_t request) { if (!authenticate(response, request, true)) { return; } print_req(request); std::string content = file_handler::read_file(WEB_DIR "password.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("X-Frame-Options", "DENY"); headers.emplace("Content-Security-Policy", "frame-ancestors 'none';"); response->write(content, headers); } /** * @brief Get the login page. * @param response The HTTP response object. * @param request The HTTP request object. * * @todo Combine this function with getWelcomePage if appropriate. */ void getLoginPage(resp_https_t response, req_https_t request) { if (!checkIPOrigin(response, request)) { return; } if (config::sunshine.username.empty()) { send_redirect(response, request, "/welcome"); return; } std::string content = file_handler::read_file(WEB_DIR "login.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("X-Frame-Options", "DENY"); headers.emplace("Content-Security-Policy", "frame-ancestors 'none';"); response->write(content, headers); } /** * @brief Get the welcome page. * @param response The HTTP response object. * @param request The HTTP request object. */ void getWelcomePage(resp_https_t response, req_https_t request) { print_req(request); if (!config::sunshine.username.empty()) { send_redirect(response, request, "/"); return; } std::string content = file_handler::read_file(WEB_DIR "welcome.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("X-Frame-Options", "DENY"); headers.emplace("Content-Security-Policy", "frame-ancestors 'none';"); response->write(content, headers); } /** * @brief Get the troubleshooting page. * @param response The HTTP response object. * @param request The HTTP request object. */ void getTroubleshootingPage(resp_https_t response, req_https_t request) { if (!authenticate(response, request, true)) { return; } print_req(request); std::string content = file_handler::read_file(WEB_DIR "troubleshooting.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("X-Frame-Options", "DENY"); headers.emplace("Content-Security-Policy", "frame-ancestors 'none';"); response->write(content, headers); } /** * @brief Get the favicon image. * @param response The HTTP response object. * @param request The HTTP request object. */ void getFaviconImage(resp_https_t response, req_https_t request) { print_req(request); std::ifstream in(WEB_DIR "images/apollo.ico", std::ios::binary); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "image/x-icon"); headers.emplace("X-Frame-Options", "DENY"); headers.emplace("Content-Security-Policy", "frame-ancestors 'none';"); response->write(SimpleWeb::StatusCode::success_ok, in, headers); } /** * @brief Get the Apollo logo image. * @param response The HTTP response object. * @param request The HTTP request object. * * @todo combine function with getFaviconImage and possibly getNodeModules * @todo use mime_types map */ void getApolloLogoImage(resp_https_t response, req_https_t request) { print_req(request); std::ifstream in(WEB_DIR "images/logo-apollo-45.png", std::ios::binary); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "image/png"); headers.emplace("X-Frame-Options", "DENY"); headers.emplace("Content-Security-Policy", "frame-ancestors 'none';"); response->write(SimpleWeb::StatusCode::success_ok, in, headers); } /** * @brief Check if a path is a child of another path. * @param base The base path. * @param query The path to check. * @return True if the path is a child of the base path, false otherwise. */ bool isChildPath(fs::path const &base, fs::path const &query) { auto relPath = fs::relative(base, query); return *(relPath.begin()) != fs::path(".."); } /** * @brief Get an asset from the node_modules directory. * @param response The HTTP response object. * @param request The HTTP request object. */ void getNodeModules(resp_https_t response, req_https_t request) { print_req(request); fs::path webDirPath(WEB_DIR); fs::path nodeModulesPath(webDirPath / "assets"); // .relative_path is needed to shed any leading slash that might exist in the request path auto filePath = fs::weakly_canonical(webDirPath / fs::path(request->path).relative_path()); // Don't do anything if file does not exist or is outside the assets directory if (!isChildPath(filePath, nodeModulesPath)) { BOOST_LOG(warning) << "Someone requested a path " << filePath << " that is outside the assets folder"; bad_request(response, request); return; } if (!fs::exists(filePath)) { not_found(response, request); return; } auto relPath = fs::relative(filePath, webDirPath); // get the mime type from the file extension mime_types map // remove the leading period from the extension auto mimeType = mime_types.find(relPath.extension().string().substr(1)); if (mimeType == mime_types.end()) { bad_request(response, request); return; } SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", mimeType->second); headers.emplace("X-Frame-Options", "DENY"); headers.emplace("Content-Security-Policy", "frame-ancestors 'none';"); std::ifstream in(filePath.string(), std::ios::binary); response->write(SimpleWeb::StatusCode::success_ok, in, headers); } /** * @brief Get the list of available applications. * @param response The HTTP response object. * @param request The HTTP request object. * * @api_examples{/api/apps| GET| null} */ void getApps(resp_https_t response, req_https_t request) { if (!authenticate(response, request)) { return; } print_req(request); try { std::string content = file_handler::read_file(config::stream.file_apps.c_str()); nlohmann::json file_tree = nlohmann::json::parse(content); file_tree["current_app"] = proc::proc.get_running_app_uuid(); file_tree["host_uuid"] = http::unique_id; file_tree["host_name"] = config::nvhttp.sunshine_name; send_response(response, file_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "GetApps: "sv << e.what(); bad_request(response, request, e.what()); } } /** * @brief Save an application. To save a new application the UUID must be empty. * To update an existing application, you must provide the current UUID of the application. * @param response The HTTP response object. * @param request The HTTP request object. * The body for the post request should be JSON serialized in the following format: * @code{.json} * { * "name": "Application Name", * "output": "Log Output Path", * "cmd": "Command to run the application", * "exclude-global-prep-cmd": false, * "elevated": false, * "auto-detach": true, * "wait-all": true, * "exit-timeout": 5, * "prep-cmd": [ * { * "do": "Command to prepare", * "undo": "Command to undo preparation", * "elevated": false * } * ], * "detached": [ * "Detached command" * ], * "image-path": "Full path to the application image. Must be a png file.", * "uuid": "aaaa-bbbb" * } * @endcode * * @api_examples{/api/apps| POST| {"name":"Hello, World!","uuid": "aaaa-bbbb"}} */ void saveApp(resp_https_t response, req_https_t request) { if (!validateContentType(response, request, "application/json") || !authenticate(response, request)) { return; } print_req(request); std::stringstream ss; ss << request->content.rdbuf(); BOOST_LOG(info) << config::stream.file_apps; try { // TODO: Input Validation // Read the input JSON from the request body. nlohmann::json inputTree = nlohmann::json::parse(ss.str()); // Read the existing apps file. std::string content = file_handler::read_file(config::stream.file_apps.c_str()); nlohmann::json fileTree = nlohmann::json::parse(content); // Migrate/merge the new app into the file tree. proc::migrate_apps(&fileTree, &inputTree); // Write the updated file tree back to disk. file_handler::write_file(config::stream.file_apps.c_str(), fileTree.dump(4)); proc::refresh(config::stream.file_apps); // Prepare and send the output response. nlohmann::json outputTree; outputTree["status"] = true; send_response(response, outputTree); } catch (std::exception &e) { BOOST_LOG(warning) << "SaveApp: "sv << e.what(); bad_request(response, request, e.what()); } } /** * @brief Close the currently running application. * @param response The HTTP response object. * @param request The HTTP request object. * * @api_examples{/api/apps/close| POST| null} */ void closeApp(resp_https_t response, req_https_t request) { if (!validateContentType(response, request, "application/json") || !authenticate(response, request)) { return; } print_req(request); proc::proc.terminate(); nlohmann::json output_tree; output_tree["status"] = true; send_response(response, output_tree); } /** * @brief Reorder applications. * @param response The HTTP response object. * @param request The HTTP request object. * * @api_examples{/api/apps/reorder| POST| {"order": ["aaaa-bbbb", "cccc-dddd"]}} */ void reorderApps(resp_https_t response, req_https_t request) { if (!validateContentType(response, request, "application/json") || !authenticate(response, request)) { return; } print_req(request); try { std::stringstream ss; ss << request->content.rdbuf(); nlohmann::json input_tree = nlohmann::json::parse(ss.str()); nlohmann::json output_tree; // Read the existing apps file. std::string content = file_handler::read_file(config::stream.file_apps.c_str()); nlohmann::json fileTree = nlohmann::json::parse(content); // Get the desired order of UUIDs from the request. if (!input_tree.contains("order") || !input_tree["order"].is_array()) { throw std::runtime_error("Missing or invalid 'order' array in request body"); } const auto& order_uuids_json = input_tree["order"]; // Get the original apps array from the fileTree. // Default to an empty array if "apps" key is missing or if it's present but not an array (after logging an error). nlohmann::json original_apps_list = nlohmann::json::array(); if (fileTree.contains("apps")) { if (fileTree["apps"].is_array()) { original_apps_list = fileTree["apps"]; } else { // "apps" key exists but is not an array. This is a malformed state. BOOST_LOG(error) << "ReorderApps: 'apps' key in apps configuration file ('" << config::stream.file_apps << "') is present but not an array."; throw std::runtime_error("'apps' in file is not an array, cannot reorder."); } } else { // "apps" key is missing. Treat as an empty list. Reordering an empty list is valid. BOOST_LOG(debug) << "ReorderApps: 'apps' key missing in apps configuration file ('" << config::stream.file_apps << "'). Treating as an empty list for reordering."; // original_apps_list is already an empty array, so no specific action needed here. } nlohmann::json reordered_apps_list = nlohmann::json::array(); std::vector item_moved(original_apps_list.size(), false); // Phase 1: Place apps according to the 'order' array from the request. // Iterate through the desired order of UUIDs. for (const auto& uuid_json_value : order_uuids_json) { if (!uuid_json_value.is_string()) { BOOST_LOG(warning) << "ReorderApps: Encountered a non-string UUID in the 'order' array. Skipping this entry."; continue; } std::string target_uuid = uuid_json_value.get(); bool found_match_for_ordered_uuid = false; // Find the first unmoved app in the original list that matches the current target_uuid. for (size_t i = 0; i < original_apps_list.size(); ++i) { if (item_moved[i]) { continue; // This specific app object has already been placed. } const auto& app_item = original_apps_list[i]; // Ensure the app item is an object and has a UUID to match against. if (app_item.is_object() && app_item.contains("uuid") && app_item["uuid"].is_string()) { if (app_item["uuid"].get() == target_uuid) { reordered_apps_list.push_back(app_item); // Add the found app object to the new list. item_moved[i] = true; // Mark this specific object as moved. found_match_for_ordered_uuid = true; break; // Found an app for this UUID, move to the next UUID in the 'order' array. } } } if (!found_match_for_ordered_uuid) { // This means a UUID specified in the 'order' array was not found in the original_apps_list // among the currently available (unmoved) app objects. // Per instruction "If the uuid is missing from the original json file, omit it." BOOST_LOG(debug) << "ReorderApps: UUID '" << target_uuid << "' from 'order' array not found in available apps list or its matching app was already processed. Omitting."; } } // Phase 2: Append any remaining apps from the original list that were not explicitly ordered. // These are app objects that were not marked 'item_moved' in Phase 1. for (size_t i = 0; i < original_apps_list.size(); ++i) { if (!item_moved[i]) { reordered_apps_list.push_back(original_apps_list[i]); } } // Update the fileTree with the new, reordered list of apps. fileTree["apps"] = reordered_apps_list; // Write the modified fileTree back to the apps configuration file. file_handler::write_file(config::stream.file_apps.c_str(), fileTree.dump(4)); // Notify relevant parts of the system that the apps configuration has changed. proc::refresh(config::stream.file_apps); output_tree["status"] = true; send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "ReorderApps: "sv << e.what(); bad_request(response, request, e.what()); } } /** * @brief Delete an application. * @param response The HTTP response object. * @param request The HTTP request object. * * @api_examples{/api/apps/delete | POST| { uuid: 'aaaa-bbbb' }} */ void deleteApp(resp_https_t response, req_https_t request) { if (!validateContentType(response, request, "application/json") || !authenticate(response, request)) { return; } print_req(request); try { std::stringstream ss; ss << request->content.rdbuf(); nlohmann::json input_tree = nlohmann::json::parse(ss.str()); // Check for required uuid field in body if (!input_tree.contains("uuid") || !input_tree["uuid"].is_string()) { bad_request(response, request, "Missing or invalid uuid in request body"); return; } auto uuid = input_tree["uuid"].get(); // Read the apps file into a nlohmann::json object. std::string content = file_handler::read_file(config::stream.file_apps.c_str()); nlohmann::json fileTree = nlohmann::json::parse(content); // Remove any app with the matching uuid directly from the "apps" array. if (fileTree.contains("apps") && fileTree["apps"].is_array()) { auto& apps = fileTree["apps"]; apps.erase( std::remove_if(apps.begin(), apps.end(), [&uuid](const nlohmann::json& app) { return app.value("uuid", "") == uuid; }), apps.end() ); } // Write the updated JSON back to the file. file_handler::write_file(config::stream.file_apps.c_str(), fileTree.dump(4)); proc::refresh(config::stream.file_apps); // Prepare and send the response. nlohmann::json outputTree; outputTree["status"] = true; send_response(response, outputTree); } catch (std::exception &e) { BOOST_LOG(warning) << "DeleteApp: "sv << e.what(); bad_request(response, request, e.what()); } } /** * @brief Get the list of paired clients. * @param response The HTTP response object. * @param request The HTTP request object. * * @api_examples{/api/clients/list| GET| null} */ void getClients(resp_https_t response, req_https_t request) { if (!authenticate(response, request)) { return; } print_req(request); nlohmann::json named_certs = nvhttp::get_all_clients(); nlohmann::json output_tree; output_tree["named_certs"] = named_certs; #ifdef _WIN32 output_tree["platform"] = "windows"; #endif output_tree["status"] = true; send_response(response, output_tree); } /** * @brief Update client information. * @param response The HTTP response object. * @param request The HTTP request object. * * The body for the POST request should be JSON serialized in the following format: * @code{.json} * { * "uuid": "", * "name": "", * "display_mode": "1920x1080x59.94", * "do": [ { "cmd": "", "elevated": false }, ... ], * "undo": [ { "cmd": "", "elevated": false }, ... ], * "perm": * } * @endcode */ void updateClient(resp_https_t response, req_https_t request) { if (!validateContentType(response, request, "application/json") || !authenticate(response, request)) { return; } print_req(request); std::stringstream ss; ss << request->content.rdbuf(); try { nlohmann::json input_tree = nlohmann::json::parse(ss.str()); nlohmann::json output_tree; std::string uuid = input_tree.value("uuid", ""); std::string name = input_tree.value("name", ""); std::string display_mode = input_tree.value("display_mode", ""); bool enable_legacy_ordering = input_tree.value("enable_legacy_ordering", true); bool allow_client_commands = input_tree.value("allow_client_commands", true); bool always_use_virtual_display = input_tree.value("always_use_virtual_display", false); auto do_cmds = nvhttp::extract_command_entries(input_tree, "do"); auto undo_cmds = nvhttp::extract_command_entries(input_tree, "undo"); auto perm = static_cast(input_tree.value("perm", static_cast(crypto::PERM::_no)) & static_cast(crypto::PERM::_all)); output_tree["status"] = nvhttp::update_device_info( uuid, name, display_mode, do_cmds, undo_cmds, perm, enable_legacy_ordering, allow_client_commands, always_use_virtual_display ); send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "Update Client: "sv << e.what(); bad_request(response, request, e.what()); } } /** * @brief Unpair a client. * @param response The HTTP response object. * @param request The HTTP request object. * * The body for the POST request should be JSON serialized in the following format: * @code{.json} * { * "uuid": "" * } * @endcode * * @api_examples{/api/clients/unpair| POST| {"uuid":"1234"}} */ void unpair(resp_https_t response, req_https_t request) { if (!validateContentType(response, request, "application/json") || !authenticate(response, request)) { return; } print_req(request); std::stringstream ss; ss << request->content.rdbuf(); try { nlohmann::json input_tree = nlohmann::json::parse(ss.str()); nlohmann::json output_tree; std::string uuid = input_tree.value("uuid", ""); output_tree["status"] = nvhttp::unpair_client(uuid); send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "Unpair: "sv << e.what(); bad_request(response, request, e.what()); } } /** * @brief Unpair all clients. * @param response The HTTP response object. * @param request The HTTP request object. * * @api_examples{/api/clients/unpair-all| POST| null} */ void unpairAll(resp_https_t response, req_https_t request) { if (!validateContentType(response, request, "application/json") || !authenticate(response, request)) { return; } print_req(request); nvhttp::erase_all_clients(); proc::proc.terminate(); nlohmann::json output_tree; output_tree["status"] = true; send_response(response, output_tree); } /** * @brief Get the configuration settings. * @param response The HTTP response object. * @param request The HTTP request object. */ void getConfig(resp_https_t response, req_https_t request) { if (!authenticate(response, request)) { return; } print_req(request); nlohmann::json output_tree; output_tree["status"] = true; output_tree["platform"] = SUNSHINE_PLATFORM; output_tree["version"] = PROJECT_VERSION; #ifdef _WIN32 output_tree["vdisplayStatus"] = (int)proc::vDisplayDriverStatus; #endif auto vars = config::parse_config(file_handler::read_file(config::sunshine.config_file.c_str())); for (auto &[name, value] : vars) { output_tree[name] = value; } send_response(response, output_tree); } /** * @brief Get the locale setting. * @param response The HTTP response object. * @param request The HTTP request object. * * @api_examples{/api/configLocale| GET| null} */ void getLocale(resp_https_t response, req_https_t request) { print_req(request); nlohmann::json output_tree; output_tree["status"] = true; output_tree["locale"] = config::sunshine.locale; send_response(response, output_tree); } /** * @brief Save the configuration settings. * @param response The HTTP response object. * @param request The HTTP request object. * The body for the post request should be JSON serialized in the following format: * @code{.json} * { * "key": "value" * } * @endcode * * @attention{It is recommended to ONLY save the config settings that differ from the default behavior.} * * @api_examples{/api/config| POST| {"key":"value"}} */ void saveConfig(resp_https_t response, req_https_t request) { if (!validateContentType(response, request, "application/json") || !authenticate(response, request)) { return; } print_req(request); std::stringstream ss; ss << request->content.rdbuf(); try { // TODO: Input Validation std::stringstream config_stream; nlohmann::json output_tree; nlohmann::json input_tree = nlohmann::json::parse(ss); for (const auto &[k, v] : input_tree.items()) { if (v.is_null() || (v.is_string() && v.get().empty())) { continue; } // v.dump() will dump valid json, which we do not want for strings in the config right now // we should migrate the config file to straight json and get rid of all this nonsense config_stream << k << " = " << (v.is_string() ? v.get() : v.dump()) << std::endl; } file_handler::write_file(config::sunshine.config_file.c_str(), config_stream.str()); output_tree["status"] = true; send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "SaveConfig: "sv << e.what(); bad_request(response, request, e.what()); } } /** * @brief Upload a cover image. * @param response The HTTP response object. * @param request The HTTP request object. * * @api_examples{/api/covers/upload| POST| {"key":"igdb_1234","url":"https://images.igdb.com/igdb/image/upload/t_cover_big_2x/abc123.png"}} */ void uploadCover(resp_https_t response, req_https_t request) { if (!validateContentType(response, request, "application/json") || !authenticate(response, request)) { return; } std::stringstream ss; ss << request->content.rdbuf(); try { nlohmann::json input_tree = nlohmann::json::parse(ss.str()); nlohmann::json output_tree; std::string key = input_tree.value("key", ""); if (key.empty()) { bad_request(response, request, "Cover key is required"); return; } std::string url = input_tree.value("url", ""); const std::string coverdir = platf::appdata().string() + "/covers/"; file_handler::make_directory(coverdir); std::string path = coverdir + http::url_escape(key) + ".png"; if (!url.empty()) { if (http::url_get_host(url) != "images.igdb.com") { bad_request(response, request, "Only images.igdb.com is allowed"); return; } if (!http::download_file(url, path)) { bad_request(response, request, "Failed to download cover"); return; } } else { auto data = SimpleWeb::Crypto::Base64::decode(input_tree.value("data", "")); std::ofstream imgfile(path); imgfile.write(data.data(), static_cast(data.size())); } output_tree["status"] = true; output_tree["path"] = path; send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "UploadCover: "sv << e.what(); bad_request(response, request, e.what()); } } /** * @brief Get the logs from the log file. * @param response The HTTP response object. * @param request The HTTP request object. * * @api_examples{/api/logs| GET| null} */ void getLogs(resp_https_t response, req_https_t request) { if (!authenticate(response, request)) { return; } print_req(request); std::string content = file_handler::read_file(config::sunshine.log_file.c_str()); SimpleWeb::CaseInsensitiveMultimap headers; std::string contentType = "text/plain"; #ifdef _WIN32 contentType += "; charset="; contentType += currentCodePageToCharset(); #endif headers.emplace("Content-Type", contentType); headers.emplace("X-Frame-Options", "DENY"); headers.emplace("Content-Security-Policy", "frame-ancestors 'none';"); response->write(SimpleWeb::StatusCode::success_ok, content, headers); } /** * @brief Update existing credentials. * @param response The HTTP response object. * @param request The HTTP request object. * * The body for the POST request should be JSON serialized in the following format: * @code{.json} * { * "currentUsername": "Current Username", * "currentPassword": "Current Password", * "newUsername": "New Username", * "newPassword": "New Password", * "confirmNewPassword": "Confirm New Password" * } * @endcode * * @api_examples{/api/password| POST| {"currentUsername":"admin","currentPassword":"admin","newUsername":"admin","newPassword":"admin","confirmNewPassword":"admin"}} */ void savePassword(resp_https_t response, req_https_t request) { if ((!config::sunshine.username.empty() && !authenticate(response, request)) || !validateContentType(response, request, "application/json")) return; print_req(request); std::vector errors; std::stringstream ss; ss << request->content.rdbuf(); try { nlohmann::json input_tree = nlohmann::json::parse(ss.str()); nlohmann::json output_tree; std::string username = input_tree.value("currentUsername", ""); std::string newUsername = input_tree.value("newUsername", ""); std::string password = input_tree.value("currentPassword", ""); std::string newPassword = input_tree.value("newPassword", ""); std::string confirmPassword = input_tree.value("confirmNewPassword", ""); if (newUsername.empty()) newUsername = username; if (newUsername.empty()) { errors.push_back("Invalid Username"); } else { auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string(); if (config::sunshine.username.empty() || (boost::iequals(username, config::sunshine.username) && hash == config::sunshine.password)) { if (newPassword.empty() || newPassword != confirmPassword) errors.push_back("Password Mismatch"); else { http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword); http::reload_user_creds(config::sunshine.credentials_file); sessionCookie.clear(); // force re-login output_tree["status"] = true; } } else { errors.push_back("Invalid Current Credentials"); } } if (!errors.empty()) { std::string error = std::accumulate(errors.begin(), errors.end(), std::string(), [](const std::string &a, const std::string &b) { return a.empty() ? b : a + ", " + b; }); bad_request(response, request, error); return; } send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "SavePassword: "sv << e.what(); bad_request(response, request, e.what()); } } /** * @brief Get a one-time password (OTP). * @param response The HTTP response object. * @param request The HTTP request object. * * @api_examples{/api/otp| GET| null} */ void getOTP(resp_https_t response, req_https_t request) { if (!validateContentType(response, request, "application/json") || !authenticate(response, request)) { return; } print_req(request); nlohmann::json output_tree; try { std::stringstream ss; ss << request->content.rdbuf(); nlohmann::json input_tree = nlohmann::json::parse(ss.str()); std::string passphrase = input_tree.value("passphrase", ""); if (passphrase.empty()) throw std::runtime_error("Passphrase not provided!"); if (passphrase.size() < 4) throw std::runtime_error("Passphrase too short!"); std::string deviceName = input_tree.value("deviceName", ""); output_tree["otp"] = nvhttp::request_otp(passphrase, deviceName); output_tree["ip"] = platf::get_local_ip_for_gateway(); output_tree["name"] = config::nvhttp.sunshine_name; output_tree["status"] = true; output_tree["message"] = "OTP created, effective within 3 minutes."; send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "OTP creation failed: "sv << e.what(); bad_request(response, request, e.what()); } } /** * @brief Send a PIN code to the host. * @param response The HTTP response object. * @param request The HTTP request object. * * The body for the POST request should be JSON serialized in the following format: * @code{.json} * { * "pin": "", * "name": "Friendly Client Name" * } * @endcode * * @api_examples{/api/pin| POST| {"pin":"1234","name":"My PC"}} */ void savePin(resp_https_t response, req_https_t request) { if (!validateContentType(response, request, "application/json") || !authenticate(response, request)) { return; } print_req(request); try { std::stringstream ss; ss << request->content.rdbuf(); nlohmann::json input_tree = nlohmann::json::parse(ss.str()); nlohmann::json output_tree; std::string pin = input_tree.value("pin", ""); std::string name = input_tree.value("name", ""); output_tree["status"] = nvhttp::pin(pin, name); send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "SavePin: "sv << e.what(); bad_request(response, request, e.what()); } } /** * @brief Reset the display device persistence. * @param response The HTTP response object. * @param request The HTTP request object. * * @api_examples{/api/reset-display-device-persistence| POST| null} */ void resetDisplayDevicePersistence(resp_https_t response, req_https_t request) { if (!validateContentType(response, request, "application/json") || !authenticate(response, request)) { return; } print_req(request); nlohmann::json output_tree; output_tree["status"] = display_device::reset_persistence(); send_response(response, output_tree); } /** * @brief Restart Apollo. * @param response The HTTP response object. * @param request The HTTP request object. * * @api_examples{/api/restart| POST| null} */ void restart(resp_https_t response, req_https_t request) { if (!validateContentType(response, request, "application/json") || !authenticate(response, request)) { return; } print_req(request); proc::proc.terminate(); // We may not return from this call platf::restart(); } /** * @brief Quit Apollo. * @param response The HTTP response object. * @param request The HTTP request object. * * On Windows, if running in a service, a special shutdown code is returned. */ void quit(resp_https_t response, req_https_t request) { if (!authenticate(response, request)) { return; } print_req(request); BOOST_LOG(warning) << "Requested quit from config page!"sv; proc::proc.terminate(); #ifdef _WIN32 if (GetConsoleWindow() == NULL) { lifetime::exit_sunshine(ERROR_SHUTDOWN_IN_PROGRESS, true); } else #endif { lifetime::exit_sunshine(0, true); } // If exit fails, write a response after 5 seconds. std::thread write_resp([response]{ std::this_thread::sleep_for(5s); response->write(); }); write_resp.detach(); } /** * @brief Launch an application. * @param response The HTTP response object. * @param request The HTTP request object. */ void launchApp(resp_https_t response, req_https_t request) { if (!validateContentType(response, request, "application/json") || !authenticate(response, request)) { return; } print_req(request); try { std::stringstream ss; ss << request->content.rdbuf(); nlohmann::json input_tree = nlohmann::json::parse(ss.str()); // Check for required uuid field in body if (!input_tree.contains("uuid") || !input_tree["uuid"].is_string()) { bad_request(response, request, "Missing or invalid uuid in request body"); return; } std::string uuid = input_tree["uuid"].get(); nlohmann::json output_tree; const auto &apps = proc::proc.get_apps(); for (auto &app : apps) { if (app.uuid == uuid) { crypto::named_cert_t named_cert { .name = "", .uuid = http::unique_id, .perm = crypto::PERM::_all, }; BOOST_LOG(info) << "Launching app ["sv << app.name << "] from web UI"sv; auto launch_session = nvhttp::make_launch_session(true, false, request->parse_query_string(), &named_cert); auto err = proc::proc.execute(app, launch_session); if (err) { bad_request(response, request, err == 503 ? "Failed to initialize video capture/encoding. Is a display connected and turned on?" : "Failed to start the specified application"); } else { output_tree["status"] = true; send_response(response, output_tree); } return; } } BOOST_LOG(error) << "Couldn't find app with uuid ["sv << uuid << ']'; bad_request(response, request, "Cannot find requested application"); } catch (std::exception &e) { BOOST_LOG(warning) << "LaunchApp: "sv << e.what(); bad_request(response, request, e.what()); } } /** * @brief Disconnect a client. * @param response The HTTP response object. * @param request The HTTP request object. */ void disconnect(resp_https_t response, req_https_t request) { if (!validateContentType(response, request, "application/json") || !authenticate(response, request)) { return; } print_req(request); try { std::stringstream ss; ss << request->content.rdbuf(); nlohmann::json output_tree; nlohmann::json input_tree = nlohmann::json::parse(ss.str()); std::string uuid = input_tree.value("uuid", ""); output_tree["status"] = nvhttp::find_and_stop_session(uuid, true); send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "Disconnect: "sv << e.what(); bad_request(response, request, e.what()); } } /** * @brief Login the user. * @param response The HTTP response object. * @param request The HTTP request object. * * The body for the POST request should be JSON serialized in the following format: * @code{.json} * { * "username": "", * "password": "" * } * @endcode */ void login(resp_https_t response, req_https_t request) { if (!checkIPOrigin(response, request) || !validateContentType(response, request, "application/json")) { return; } auto fg = util::fail_guard([&]{ response->write(SimpleWeb::StatusCode::client_error_unauthorized); }); try { std::stringstream ss; ss << request->content.rdbuf(); nlohmann::json input_tree = nlohmann::json::parse(ss.str()); std::string username = input_tree.value("username", ""); std::string password = input_tree.value("password", ""); std::string hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string(); if (!boost::iequals(username, config::sunshine.username) || hash != config::sunshine.password) return; std::string sessionCookieRaw = crypto::rand_alphabet(64); sessionCookie = util::hex(crypto::hash(sessionCookieRaw + config::sunshine.salt)).to_string(); cookie_creation_time = std::chrono::steady_clock::now(); const SimpleWeb::CaseInsensitiveMultimap headers { { "Set-Cookie", "auth=" + sessionCookieRaw + "; Secure; SameSite=Strict; Max-Age=2592000; Path=/" } }; response->write(headers); fg.disable(); } catch (std::exception &e) { BOOST_LOG(warning) << "Web UI Login failed: ["sv << net::addr_to_normalized_string(request->remote_endpoint().address()) << "]: "sv << e.what(); response->write(SimpleWeb::StatusCode::server_error_internal_server_error); fg.disable(); return; } } /** * @brief Start the HTTPS server. */ void start() { auto shutdown_event = mail::man->event(mail::shutdown); auto port_https = net::map_port(PORT_HTTPS); auto address_family = net::af_from_enum_string(config::sunshine.address_family); https_server_t server { config::nvhttp.cert, config::nvhttp.pkey }; server.default_resource["DELETE"] = [](resp_https_t response, req_https_t request) { bad_request(response, request); }; server.default_resource["PATCH"] = [](resp_https_t response, req_https_t request) { bad_request(response, request); }; server.default_resource["POST"] = [](resp_https_t response, req_https_t request) { bad_request(response, request); }; server.default_resource["PUT"] = [](resp_https_t response, req_https_t request) { bad_request(response, request); }; server.default_resource["GET"] = not_found; server.resource["^/$"]["GET"] = getIndexPage; server.resource["^/pin/?$"]["GET"] = getPinPage; server.resource["^/apps/?$"]["GET"] = getAppsPage; server.resource["^/config/?$"]["GET"] = getConfigPage; server.resource["^/password/?$"]["GET"] = getPasswordPage; server.resource["^/welcome/?$"]["GET"] = getWelcomePage; server.resource["^/login/?$"]["GET"] = getLoginPage; server.resource["^/troubleshooting/?$"]["GET"] = getTroubleshootingPage; server.resource["^/api/login"]["POST"] = login; server.resource["^/api/pin$"]["POST"] = savePin; server.resource["^/api/otp$"]["POST"] = getOTP; server.resource["^/api/apps$"]["GET"] = getApps; server.resource["^/api/apps$"]["POST"] = saveApp; server.resource["^/api/apps/reorder$"]["POST"] = reorderApps; server.resource["^/api/apps/delete$"]["POST"] = deleteApp; server.resource["^/api/apps/launch$"]["POST"] = launchApp; server.resource["^/api/apps/close$"]["POST"] = closeApp; server.resource["^/api/logs$"]["GET"] = getLogs; server.resource["^/api/config$"]["GET"] = getConfig; server.resource["^/api/config$"]["POST"] = saveConfig; server.resource["^/api/configLocale$"]["GET"] = getLocale; server.resource["^/api/restart$"]["POST"] = restart; server.resource["^/api/quit$"]["POST"] = quit; server.resource["^/api/reset-display-device-persistence$"]["POST"] = resetDisplayDevicePersistence; server.resource["^/api/password$"]["POST"] = savePassword; server.resource["^/api/clients/unpair-all$"]["POST"] = unpairAll; server.resource["^/api/clients/list$"]["GET"] = getClients; server.resource["^/api/clients/update$"]["POST"] = updateClient; server.resource["^/api/clients/unpair$"]["POST"] = unpair; server.resource["^/api/clients/disconnect$"]["POST"] = disconnect; server.resource["^/api/covers/upload$"]["POST"] = uploadCover; server.resource["^/images/apollo.ico$"]["GET"] = getFaviconImage; server.resource["^/images/logo-apollo-45.png$"]["GET"] = getApolloLogoImage; server.resource["^/assets\\/.+$"]["GET"] = getNodeModules; server.config.reuse_address = true; server.config.address = net::af_to_any_address_string(address_family); server.config.port = port_https; auto accept_and_run = [&](auto *server) { try { server->start([port_https](unsigned short port) { BOOST_LOG(info) << "Configuration UI available at [https://localhost:"sv << port << "]"; }); } catch (boost::system::system_error &err) { // It's possible the exception gets thrown after calling server->stop() from a different thread if (shutdown_event->peek()) return; BOOST_LOG(fatal) << "Couldn't start Configuration HTTPS server on port ["sv << port_https << "]: "sv << err.what(); shutdown_event->raise(true); return; } }; std::thread tcp { accept_and_run, &server }; // Wait for any event shutdown_event->view(); server.stop(); tcp.join(); } } // namespace confighttp ================================================ FILE: src/confighttp.h ================================================ /** * @file src/confighttp.h * @brief Declarations for the Web UI Config HTTP server. */ #pragma once // standard includes #include #include #include // local includes #include "thread_safe.h" #define WEB_DIR SUNSHINE_ASSETS_DIR "/web/" using namespace std::chrono_literals; namespace confighttp { constexpr auto PORT_HTTPS = 1; constexpr auto SESSION_EXPIRE_DURATION = 24h * 15; void start(); } // namespace confighttp // mime types map const std::map mime_types = { {"css", "text/css"}, {"gif", "image/gif"}, {"htm", "text/html"}, {"html", "text/html"}, {"ico", "image/x-icon"}, {"jpeg", "image/jpeg"}, {"jpg", "image/jpeg"}, {"js", "application/javascript"}, {"json", "application/json"}, {"png", "image/png"}, {"svg", "image/svg+xml"}, {"ttf", "font/ttf"}, {"txt", "text/plain"}, {"woff2", "font/woff2"}, {"xml", "text/xml"}, }; ================================================ FILE: src/crypto.cpp ================================================ /** * @file src/crypto.cpp * @brief Definitions for cryptography functions. */ // lib includes #include #include // local includes #include "crypto.h" namespace crypto { using asn1_string_t = util::safe_ptr; cert_chain_t::cert_chain_t(): _certs {}, _cert_ctx { X509_STORE_CTX_new() } { } void cert_chain_t::add(p_named_cert_t& named_cert_p) { x509_store_t x509_store { X509_STORE_new() }; X509_STORE_add_cert(x509_store.get(), x509(named_cert_p->cert).get()); _certs.emplace_back(std::make_pair(named_cert_p, std::move(x509_store))); } void cert_chain_t::clear() { _certs.clear(); } static int openssl_verify_cb(int ok, X509_STORE_CTX *ctx) { int err_code = X509_STORE_CTX_get_error(ctx); switch (err_code) { // Expired or not-yet-valid certificates are fine. Sometimes Moonlight is running on embedded devices // that don't have accurate clocks (or haven't yet synchronized by the time Moonlight first runs). // This behavior also matches what GeForce Experience does. // 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 case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY: case X509_V_ERR_CERT_NOT_YET_VALID: case X509_V_ERR_CERT_HAS_EXPIRED: return 1; default: return ok; } } /** * @brief Verify the certificate chain. * When certificates from two or more instances of Moonlight have been added to x509_store_t, * only one of them will be verified by X509_verify_cert, resulting in only a single instance of * Moonlight to be able to use Sunshine * * To circumvent this, x509_store_t instance will be created for each instance of the certificates. * @param cert The certificate to verify. * @return nullptr if the certificate is valid, otherwise an error string. */ const char * cert_chain_t::verify(x509_t::element_type *cert, p_named_cert_t& named_cert_out) { int err_code = 0; for (auto &[named_cert_p, x509_store] : _certs) { auto fg = util::fail_guard([this]() { X509_STORE_CTX_cleanup(_cert_ctx.get()); }); X509_STORE_CTX_init(_cert_ctx.get(), x509_store.get(), cert, nullptr); X509_STORE_CTX_set_verify_cb(_cert_ctx.get(), openssl_verify_cb); // We don't care to validate the entire chain for the purposes of client auth. // Some versions of clients forked from Moonlight Embedded produce client certs // that OpenSSL doesn't detect as self-signed due to some X509v3 extensions. X509_STORE_CTX_set_flags(_cert_ctx.get(), X509_V_FLAG_PARTIAL_CHAIN); auto err = X509_verify_cert(_cert_ctx.get()); if (err == 1) { named_cert_out = named_cert_p; return nullptr; } err_code = X509_STORE_CTX_get_error(_cert_ctx.get()); if (err_code != X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT && err_code != X509_V_ERR_INVALID_CA) { return X509_verify_cert_error_string(err_code); } } return X509_verify_cert_error_string(err_code); } namespace cipher { static int init_decrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) { ctx.reset(EVP_CIPHER_CTX_new()); if (!ctx) { return -1; } if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) { return -1; } if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv->size(), nullptr) != 1) { return -1; } if (EVP_DecryptInit_ex(ctx.get(), nullptr, nullptr, key->data(), iv->data()) != 1) { return -1; } EVP_CIPHER_CTX_set_padding(ctx.get(), padding); return 0; } static int init_encrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) { ctx.reset(EVP_CIPHER_CTX_new()); // Gen 7 servers use 128-bit AES ECB if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) { return -1; } if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv->size(), nullptr) != 1) { return -1; } if (EVP_EncryptInit_ex(ctx.get(), nullptr, nullptr, key->data(), iv->data()) != 1) { return -1; } EVP_CIPHER_CTX_set_padding(ctx.get(), padding); return 0; } static int init_encrypt_cbc(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) { ctx.reset(EVP_CIPHER_CTX_new()); // Gen 7 servers use 128-bit AES ECB if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_cbc(), nullptr, key->data(), iv->data()) != 1) { return -1; } EVP_CIPHER_CTX_set_padding(ctx.get(), padding); return 0; } int gcm_t::decrypt(const std::string_view &tagged_cipher, std::vector &plaintext, aes_t *iv) { if (!decrypt_ctx && init_decrypt_gcm(decrypt_ctx, &key, iv, padding)) { return -1; } // Calling with cipher == nullptr results in a parameter change // without requiring a reallocation of the internal cipher ctx. if (EVP_DecryptInit_ex(decrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) { return false; } auto cipher = tagged_cipher.substr(tag_size); auto tag = tagged_cipher.substr(0, tag_size); plaintext.resize(round_to_pkcs7_padded(cipher.size())); int update_outlen, final_outlen; if (EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &update_outlen, (const std::uint8_t *) cipher.data(), cipher.size()) != 1) { return -1; } if (EVP_CIPHER_CTX_ctrl(decrypt_ctx.get(), EVP_CTRL_GCM_SET_TAG, tag.size(), const_cast(tag.data())) != 1) { return -1; } if (EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data() + update_outlen, &final_outlen) != 1) { return -1; } plaintext.resize(update_outlen + final_outlen); return 0; } /** * This function encrypts the given plaintext using the AES key in GCM mode. The initialization vector (IV) is also provided. * The function handles the creation and initialization of the encryption context, and manages the encryption process. * The resulting ciphertext and the GCM tag are written into the tagged_cipher buffer. */ int gcm_t::encrypt(const std::string_view &plaintext, std::uint8_t *tag, std::uint8_t *ciphertext, aes_t *iv) { if (!encrypt_ctx && init_encrypt_gcm(encrypt_ctx, &key, iv, padding)) { return -1; } // Calling with cipher == nullptr results in a parameter change // without requiring a reallocation of the internal cipher ctx. if (EVP_EncryptInit_ex(encrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) { return -1; } int update_outlen, final_outlen; // Encrypt into the caller's buffer if (EVP_EncryptUpdate(encrypt_ctx.get(), ciphertext, &update_outlen, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) { return -1; } // GCM encryption won't ever fill ciphertext here but we have to call it anyway if (EVP_EncryptFinal_ex(encrypt_ctx.get(), ciphertext + update_outlen, &final_outlen) != 1) { return -1; } if (EVP_CIPHER_CTX_ctrl(encrypt_ctx.get(), EVP_CTRL_GCM_GET_TAG, tag_size, tag) != 1) { return -1; } return update_outlen + final_outlen; } int gcm_t::encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv) { // This overload handles the common case of [GCM tag][cipher text] buffer layout return encrypt(plaintext, tagged_cipher, tagged_cipher + tag_size, iv); } int ecb_t::decrypt(const std::string_view &cipher, std::vector &plaintext) { auto fg = util::fail_guard([this]() { EVP_CIPHER_CTX_reset(decrypt_ctx.get()); }); // Gen 7 servers use 128-bit AES ECB if (EVP_DecryptInit_ex(decrypt_ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) { return -1; } EVP_CIPHER_CTX_set_padding(decrypt_ctx.get(), padding); plaintext.resize(round_to_pkcs7_padded(cipher.size())); int update_outlen, final_outlen; if (EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &update_outlen, (const std::uint8_t *) cipher.data(), cipher.size()) != 1) { return -1; } if (EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data() + update_outlen, &final_outlen) != 1) { return -1; } plaintext.resize(update_outlen + final_outlen); return 0; } int ecb_t::encrypt(const std::string_view &plaintext, std::vector &cipher) { auto fg = util::fail_guard([this]() { EVP_CIPHER_CTX_reset(encrypt_ctx.get()); }); // Gen 7 servers use 128-bit AES ECB if (EVP_EncryptInit_ex(encrypt_ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) { return -1; } EVP_CIPHER_CTX_set_padding(encrypt_ctx.get(), padding); cipher.resize(round_to_pkcs7_padded(plaintext.size())); int update_outlen, final_outlen; // Encrypt into the caller's buffer if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher.data(), &update_outlen, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) { return -1; } if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher.data() + update_outlen, &final_outlen) != 1) { return -1; } cipher.resize(update_outlen + final_outlen); return 0; } /** * This function encrypts the given plaintext using the AES key in CBC mode. The initialization vector (IV) is also provided. * The function handles the creation and initialization of the encryption context, and manages the encryption process. * The resulting ciphertext is written into the cipher buffer. */ int cbc_t::encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv) { if (!encrypt_ctx && init_encrypt_cbc(encrypt_ctx, &key, iv, padding)) { return -1; } // Calling with cipher == nullptr results in a parameter change // without requiring a reallocation of the internal cipher ctx. if (EVP_EncryptInit_ex(encrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) { return false; } int update_outlen, final_outlen; // Encrypt into the caller's buffer if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &update_outlen, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) { return -1; } if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + update_outlen, &final_outlen) != 1) { return -1; } return update_outlen + final_outlen; } ecb_t::ecb_t(const aes_t &key, bool padding): cipher_t {EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_new(), key, padding} { } cbc_t::cbc_t(const aes_t &key, bool padding): cipher_t {nullptr, nullptr, key, padding} { } gcm_t::gcm_t(const crypto::aes_t &key, bool padding): cipher_t {nullptr, nullptr, key, padding} { } } // namespace cipher aes_t gen_aes_key(const std::array &salt, const std::string_view &pin) { aes_t key(16); std::string salt_pin; salt_pin.reserve(salt.size() + pin.size()); salt_pin.insert(std::end(salt_pin), std::begin(salt), std::end(salt)); salt_pin.insert(std::end(salt_pin), std::begin(pin), std::end(pin)); auto hsh = hash(salt_pin); std::copy(std::begin(hsh), std::begin(hsh) + key.size(), std::begin(key)); return key; } sha256_t hash(const std::string_view &plaintext) { sha256_t hsh; EVP_Digest(plaintext.data(), plaintext.size(), hsh.data(), nullptr, EVP_sha256(), nullptr); return hsh; } x509_t x509(const std::string_view &x) { bio_t io {BIO_new(BIO_s_mem())}; BIO_write(io.get(), x.data(), x.size()); x509_t p; PEM_read_bio_X509(io.get(), &p, nullptr, nullptr); return p; } pkey_t pkey(const std::string_view &k) { bio_t io {BIO_new(BIO_s_mem())}; BIO_write(io.get(), k.data(), k.size()); pkey_t p = nullptr; PEM_read_bio_PrivateKey(io.get(), &p, nullptr, nullptr); return p; } std::string pem(x509_t &x509) { bio_t bio {BIO_new(BIO_s_mem())}; PEM_write_bio_X509(bio.get(), x509.get()); BUF_MEM *mem_ptr; BIO_get_mem_ptr(bio.get(), &mem_ptr); return {mem_ptr->data, mem_ptr->length}; } std::string pem(pkey_t &pkey) { bio_t bio {BIO_new(BIO_s_mem())}; PEM_write_bio_PrivateKey(bio.get(), pkey.get(), nullptr, nullptr, 0, nullptr, nullptr); BUF_MEM *mem_ptr; BIO_get_mem_ptr(bio.get(), &mem_ptr); return {mem_ptr->data, mem_ptr->length}; } std::string_view signature(const x509_t &x) { // X509_ALGOR *_ = nullptr; const ASN1_BIT_STRING *asn1 = nullptr; X509_get0_signature(&asn1, nullptr, x.get()); return {(const char *) asn1->data, (std::size_t) asn1->length}; } std::string rand(std::size_t bytes) { std::string r; r.resize(bytes); RAND_bytes((uint8_t *) r.data(), r.size()); return r; } std::vector sign(const pkey_t &pkey, const std::string_view &data, const EVP_MD *md) { md_ctx_t ctx {EVP_MD_CTX_create()}; if (EVP_DigestSignInit(ctx.get(), nullptr, md, nullptr, (EVP_PKEY *) pkey.get()) != 1) { return {}; } if (EVP_DigestSignUpdate(ctx.get(), data.data(), data.size()) != 1) { return {}; } std::size_t slen; if (EVP_DigestSignFinal(ctx.get(), nullptr, &slen) != 1) { return {}; } std::vector digest(slen); if (EVP_DigestSignFinal(ctx.get(), digest.data(), &slen) != 1) { return {}; } return digest; } creds_t gen_creds(const std::string_view &cn, std::uint32_t key_bits) { x509_t x509 {X509_new()}; pkey_ctx_t ctx {EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr)}; pkey_t pkey; EVP_PKEY_keygen_init(ctx.get()); EVP_PKEY_CTX_set_rsa_keygen_bits(ctx.get(), key_bits); EVP_PKEY_keygen(ctx.get(), &pkey); X509_set_version(x509.get(), 2); // Generate a real serial number to avoid SEC_ERROR_REUSED_ISSUER_AND_SERIAL with Firefox bignum_t serial {BN_new()}; BN_rand(serial.get(), 159, BN_RAND_TOP_ANY, BN_RAND_BOTTOM_ANY); // 159 bits to fit in 20 bytes in DER format BN_set_negative(serial.get(), 0); // Serial numbers must be positive BN_to_ASN1_INTEGER(serial.get(), X509_get_serialNumber(x509.get())); constexpr auto year = 60 * 60 * 24 * 365; #if OPENSSL_VERSION_NUMBER < 0x10100000L X509_gmtime_adj(X509_get_notBefore(x509.get()), 0); X509_gmtime_adj(X509_get_notAfter(x509.get()), 20 * year); #else asn1_string_t not_before {ASN1_STRING_dup(X509_get0_notBefore(x509.get()))}; asn1_string_t not_after {ASN1_STRING_dup(X509_get0_notAfter(x509.get()))}; X509_gmtime_adj(not_before.get(), 0); X509_gmtime_adj(not_after.get(), 20 * year); X509_set1_notBefore(x509.get(), not_before.get()); X509_set1_notAfter(x509.get(), not_after.get()); #endif X509_set_pubkey(x509.get(), pkey.get()); auto name = X509_get_subject_name(x509.get()); X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (const std::uint8_t *) cn.data(), cn.size(), -1, 0); X509_set_issuer_name(x509.get(), name); X509_sign(x509.get(), pkey.get(), EVP_sha256()); return {pem(x509), pem(pkey)}; } std::vector sign256(const pkey_t &pkey, const std::string_view &data) { return sign(pkey, data, EVP_sha256()); } bool verify(const x509_t &x509, const std::string_view &data, const std::string_view &signature, const EVP_MD *md) { auto pkey = X509_get0_pubkey(x509.get()); md_ctx_t ctx {EVP_MD_CTX_create()}; if (EVP_DigestVerifyInit(ctx.get(), nullptr, md, nullptr, pkey) != 1) { return false; } if (EVP_DigestVerifyUpdate(ctx.get(), data.data(), data.size()) != 1) { return false; } if (EVP_DigestVerifyFinal(ctx.get(), (const uint8_t *) signature.data(), signature.size()) != 1) { return false; } return true; } bool verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature) { return verify(x509, data, signature, EVP_sha256()); } void md_ctx_destroy(EVP_MD_CTX *ctx) { EVP_MD_CTX_destroy(ctx); } std::string rand_alphabet(std::size_t bytes, const std::string_view &alphabet) { auto value = rand(bytes); for (std::size_t i = 0; i != value.size(); ++i) { value[i] = alphabet[value[i] % alphabet.length()]; } return value; } } // namespace crypto ================================================ FILE: src/crypto.h ================================================ /** * @file src/crypto.h * @brief Declarations for cryptography functions. */ #pragma once // standard includes #include // lib includes #include #include #include #include #include #include // local includes #include "utility.h" namespace crypto { struct creds_t { std::string x509; std::string pkey; }; void md_ctx_destroy(EVP_MD_CTX *); using sha256_t = std::array; using aes_t = std::vector; using x509_t = util::safe_ptr; using x509_store_t = util::safe_ptr; using x509_store_ctx_t = util::safe_ptr; using cipher_ctx_t = util::safe_ptr; using md_ctx_t = util::safe_ptr; using bio_t = util::safe_ptr; using pkey_t = util::safe_ptr; using pkey_ctx_t = util::safe_ptr; using bignum_t = util::safe_ptr; /** * @brief The permissions of a client. */ enum class PERM: uint32_t { _reserved = 1, _input = _reserved << 8, // Input permission group input_controller = _input << 0, // Allow controller input input_touch = _input << 1, // Allow touch input input_pen = _input << 2, // Allow pen input input_mouse = _input << 3, // Allow mouse input input_kbd = _input << 4, // Allow keyboard input _all_inputs = input_controller | input_touch | input_pen | input_mouse | input_kbd, _operation = _input << 8, // Operation permission group clipboard_set = _operation << 0, // Allow set clipboard from client clipboard_read = _operation << 1, // Allow read clipboard from host file_upload = _operation << 2, // Allow upload files to host file_dwnload = _operation << 3, // Allow download files from host server_cmd = _operation << 4, // Allow execute server cmd _all_opeiations = clipboard_set | clipboard_read | file_upload | file_dwnload | server_cmd, _action = _operation << 8, // Action permission group list = _action << 0, // Allow list apps view = _action << 1, // Allow view streams launch = _action << 2, // Allow launch apps _allow_view = view | launch, // If no view permission is granted, disconnect the device upon permission update _all_actions = list | view | launch, _default = view | list, // Default permissions for new clients _no = 0, // No permissions are granted _all = _all_inputs | _all_opeiations | _all_actions, // All current permissions }; inline constexpr PERM operator&(PERM x, PERM y) { return static_cast(static_cast(x) & static_cast(y)); } inline constexpr bool operator!(PERM p) { return static_cast(p) == 0; } struct command_entry_t { std::string cmd; bool elevated; // Serialize method using nlohmann::json static inline nlohmann::json serialize(const command_entry_t& entry) { nlohmann::json node; node["cmd"] = entry.cmd; node["elevated"] = entry.elevated; return node; } }; struct named_cert_t { std::string name; std::string uuid; std::string cert; std::string display_mode; std::list do_cmds; std::list undo_cmds; PERM perm; bool enable_legacy_ordering; bool allow_client_commands; bool always_use_virtual_display; }; using p_named_cert_t = std::shared_ptr; /** * @brief Hashes the given plaintext using SHA-256. * @param plaintext * @return The SHA-256 hash of the plaintext. */ sha256_t hash(const std::string_view &plaintext); aes_t gen_aes_key(const std::array &salt, const std::string_view &pin); x509_t x509(const std::string_view &x); pkey_t pkey(const std::string_view &k); std::string pem(x509_t &x509); std::string pem(pkey_t &pkey); std::vector sign256(const pkey_t &pkey, const std::string_view &data); bool verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature); creds_t gen_creds(const std::string_view &cn, std::uint32_t key_bits); std::string_view signature(const x509_t &x); std::string rand(std::size_t bytes); std::string rand_alphabet(std::size_t bytes, const std::string_view &alphabet = std::string_view {"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!%&()=-"}); class cert_chain_t { public: KITTY_DECL_CONSTR(cert_chain_t) void add(p_named_cert_t& named_cert_p); void clear(); const char *verify(x509_t::element_type *cert, p_named_cert_t& named_cert_out); private: std::vector> _certs; x509_store_ctx_t _cert_ctx; }; namespace cipher { constexpr std::size_t tag_size = 16; constexpr std::size_t round_to_pkcs7_padded(std::size_t size) { return ((size + 15) / 16) * 16; } class cipher_t { public: cipher_ctx_t decrypt_ctx; cipher_ctx_t encrypt_ctx; aes_t key; bool padding; }; class ecb_t: public cipher_t { public: ecb_t() = default; ecb_t(ecb_t &&) noexcept = default; ecb_t &operator=(ecb_t &&) noexcept = default; ecb_t(const aes_t &key, bool padding = true); int encrypt(const std::string_view &plaintext, std::vector &cipher); int decrypt(const std::string_view &cipher, std::vector &plaintext); }; class gcm_t: public cipher_t { public: gcm_t() = default; gcm_t(gcm_t &&) noexcept = default; gcm_t &operator=(gcm_t &&) noexcept = default; gcm_t(const crypto::aes_t &key, bool padding = true); /** * @brief Encrypts the plaintext using AES GCM mode. * @param plaintext The plaintext data to be encrypted. * @param tag The buffer where the GCM tag will be written. * @param ciphertext The buffer where the resulting ciphertext will be written. * @param iv The initialization vector to be used for the encryption. * @return The total length of the ciphertext and GCM tag. Returns -1 in case of an error. */ int encrypt(const std::string_view &plaintext, std::uint8_t *tag, std::uint8_t *ciphertext, aes_t *iv); /** * @brief Encrypts the plaintext using AES GCM mode. * length of cipher must be at least: round_to_pkcs7_padded(plaintext.size()) + crypto::cipher::tag_size * @param plaintext The plaintext data to be encrypted. * @param tagged_cipher The buffer where the resulting ciphertext and GCM tag will be written. * @param iv The initialization vector to be used for the encryption. * @return The total length of the ciphertext and GCM tag written into tagged_cipher. Returns -1 in case of an error. */ int encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv); int decrypt(const std::string_view &cipher, std::vector &plaintext, aes_t *iv); }; class cbc_t: public cipher_t { public: cbc_t() = default; cbc_t(cbc_t &&) noexcept = default; cbc_t &operator=(cbc_t &&) noexcept = default; cbc_t(const crypto::aes_t &key, bool padding = true); /** * @brief Encrypts the plaintext using AES CBC mode. * length of cipher must be at least: round_to_pkcs7_padded(plaintext.size()) * @param plaintext The plaintext data to be encrypted. * @param cipher The buffer where the resulting ciphertext will be written. * @param iv The initialization vector to be used for the encryption. * @return The total length of the ciphertext written into cipher. Returns -1 in case of an error. */ int encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv); }; } // namespace cipher } // namespace crypto ================================================ FILE: src/display_device.cpp ================================================ /** * @file src/display_device.cpp * @brief Definitions for display device handling. */ // header include #include "display_device.h" // lib includes #include #include #include #include #include #include #include #include // local includes #include "audio.h" #include "platform/common.h" #include "rtsp.h" // platform-specific includes #ifdef _WIN32 #include #include #include #endif namespace display_device { namespace { constexpr std::chrono::milliseconds DEFAULT_RETRY_INTERVAL {5000}; /** * @brief A global for the settings manager interface and other settings whose lifetime is managed by `display_device::init(...)`. */ struct { std::mutex mutex {}; std::chrono::milliseconds config_revert_delay {0}; std::unique_ptr> sm_instance {nullptr}; } DD_DATA; /** * @brief Helper class for capturing audio context when the API demands it. * * The capture is needed to be done in case some of the displays are going * to be deactivated before the stream starts. In this case the audio context * will be captured for this display and can be restored once it is turned back. */ class sunshine_audio_context_t: public AudioContextInterface { public: [[nodiscard]] bool capture() override { return context_scheduler.execute([](auto &audio_context) { // Explicitly releasing the context first in case it was not release yet so that it can be potentially cleaned up. audio_context = boost::none; audio_context = audio_context_t {}; // Always say that we have captured it successfully as otherwise the settings change procedure will be aborted. return true; }); } [[nodiscard]] bool isCaptured() const override { return context_scheduler.execute([](const auto &audio_context) { if (audio_context) { // In case we still have context we need to check whether it was released or not. // If it was released we can pretend that we no longer have it as it will be immediately cleaned up in `capture` method before we acquire new context. return !audio_context->released; } return false; }); } void release() override { context_scheduler.schedule([](auto &audio_context, auto &stop_token) { if (audio_context) { audio_context->released = true; const auto *audio_ctx_ptr = audio_context->audio_ctx_ref.get(); if (audio_ctx_ptr && !audio::is_audio_ctx_sink_available(*audio_ctx_ptr) && audio_context->retry_counter > 0) { // It is possible that the audio sink is not immediately available after the display is turned on. // Therefore, we will hold on to the audio context a little longer, until it is either available // or we time out. --audio_context->retry_counter; return; } } audio_context = boost::none; stop_token.requestStop(); }, SchedulerOptions {.m_sleep_durations = {2s}}); } private: struct audio_context_t { /** * @brief A reference to the audio context that will automatically extend the audio session. * @note It is auto-initialized here for convenience. */ decltype(audio::get_audio_ctx_ref()) audio_ctx_ref {audio::get_audio_ctx_ref()}; /** * @brief Will be set to true if the capture was released, but we still have to keep the context around, because the device is not available. */ bool released {false}; /** * @brief How many times to check if the audio sink is available before giving up. */ int retry_counter {15}; }; RetryScheduler> context_scheduler {std::make_unique>(boost::none)}; }; /** * @brief Convert string to unsigned int. * @note For random reason there is std::stoi, but not std::stou... * @param value String to be converted * @return Parsed unsigned integer. */ unsigned int stou(const std::string &value) { unsigned long result {std::stoul(value)}; if (result > std::numeric_limits::max()) { throw std::out_of_range("stou"); } return result; } /** * @brief Parse resolution value from the string. * @param input String to be parsed. * @param output Reference to output variable to fill in. * @returns True on successful parsing (empty string allowed), false otherwise. * * @examples * std::optional resolution; * if (parse_resolution_string("1920x1080", resolution)) { * if (resolution) { * BOOST_LOG(info) << "Value was specified"; * } * else { * BOOST_LOG(info) << "Value was empty"; * } * } * @examples_end */ bool parse_resolution_string(const std::string &input, std::optional &output) { const std::string trimmed_input {boost::algorithm::trim_copy(input)}; const std::regex resolution_regex {R"(^(\d+)x(\d+)$)"}; if (std::smatch match; std::regex_match(trimmed_input, match, resolution_regex)) { try { output = Resolution { stou(match[1].str()), stou(match[2].str()) }; return true; } catch (const std::out_of_range &) { BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << " (number out of range)."; } catch (const std::exception &err) { BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << ":\n" << err.what(); } } else { if (trimmed_input.empty()) { output = std::nullopt; return true; } BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << R"(. It must match a "1920x1080" pattern!)"; } return false; } /** * @brief Parse refresh rate value from the string. * @param input String to be parsed. * @param output Reference to output variable to fill in. * @param allow_decimal_point Specify whether the decimal point is allowed or not. * @returns True on successful parsing (empty string allowed), false otherwise. * * @examples * std::optional refresh_rate; * if (parse_refresh_rate_string("59.95", refresh_rate)) { * if (refresh_rate) { * BOOST_LOG(info) << "Value was specified"; * } * else { * BOOST_LOG(info) << "Value was empty"; * } * } * @examples_end */ bool parse_refresh_rate_string(const std::string &input, std::optional &output, const bool allow_decimal_point = true) { static const auto is_zero {[](const auto &character) { return character == '0'; }}; const std::string trimmed_input {boost::algorithm::trim_copy(input)}; const std::regex refresh_rate_regex {allow_decimal_point ? R"(^(\d+)(?:\.(\d+))?$)" : R"(^(\d+)$)"}; if (std::smatch match; std::regex_match(trimmed_input, match, refresh_rate_regex)) { try { // Here we are trimming zeros from the string to possibly reduce out of bounds case std::string trimmed_match_1 {boost::algorithm::trim_left_copy_if(match[1].str(), is_zero)}; if (trimmed_match_1.empty()) { trimmed_match_1 = "0"s; // Just in case ALL the string is full of zeros, we want to leave one } std::string trimmed_match_2; if (allow_decimal_point && match[2].matched) { trimmed_match_2 = boost::algorithm::trim_right_copy_if(match[2].str(), is_zero); } if (!trimmed_match_2.empty()) { // We have a decimal point and will have to split it into numerator and denominator. // For example: // 59.995: // numerator = 59995 // denominator = 1000 // We are essentially removing the decimal point here: 59.995 -> 59995 const std::string numerator_str {trimmed_match_1 + trimmed_match_2}; const auto numerator {stou(numerator_str)}; // Here we are counting decimal places and calculating denominator: 10^decimal_places const auto denominator {static_cast(std::pow(10, trimmed_match_2.size()))}; output = Rational {numerator, denominator}; } else { // We do not have a decimal point, just a valid number. // For example: // 60: // numerator = 60 // denominator = 1 output = Rational {stou(trimmed_match_1), 1}; } return true; } catch (const std::out_of_range &) { BOOST_LOG(error) << "Failed to parse refresh rate string " << trimmed_input << " (number out of range)."; } catch (const std::exception &err) { BOOST_LOG(error) << "Failed to parse refresh rate string " << trimmed_input << ":\n" << err.what(); } } else { if (trimmed_input.empty()) { output = std::nullopt; return true; } BOOST_LOG(error) << "Failed to parse refresh rate string " << trimmed_input << ". Must have a pattern of " << (allow_decimal_point ? R"("123" or "123.456")" : R"("123")") << "!"; } return false; } /** * @brief Parse device preparation option from the user configuration and the session information. * @param video_config User's video related configuration. * @returns Parsed device preparation value we need to use. * Empty optional if no preparation nor configuration shall take place. * * @examples * const config::video_t &video_config { config::video }; * const auto device_prep_option = parse_device_prep_option(video_config); * @examples_end */ std::optional parse_device_prep_option(const config::video_t &video_config) { using enum config::video_t::dd_t::config_option_e; using enum SingleDisplayConfiguration::DevicePreparation; switch (video_config.dd.configuration_option) { case verify_only: return VerifyOnly; case ensure_active: return EnsureActive; case ensure_primary: return EnsurePrimary; case ensure_only_display: return EnsureOnlyDisplay; case disabled: break; } return std::nullopt; } /** * @brief Parse resolution option from the user configuration and the session information. * @param video_config User's video related configuration. * @param session Session information. * @param config A reference to a display config object that will be modified on success. * @returns True on successful parsing, false otherwise. * * @examples * const std::shared_ptr launch_session; * const config::video_t &video_config { config::video }; * * SingleDisplayConfiguration config; * const bool success = parse_resolution_option(video_config, *launch_session, config); * @examples_end */ bool parse_resolution_option(const config::video_t &video_config, const rtsp_stream::launch_session_t &session, SingleDisplayConfiguration &config) { using resolution_option_e = config::video_t::dd_t::resolution_option_e; switch (video_config.dd.resolution_option) { case resolution_option_e::automatic: { if (!session.enable_sops) { BOOST_LOG(warning) << R"(Sunshine is configured to change resolution automatically, but the "Optimize game settings" is not set in the client! Resolution will not be changed.)"; } else if (session.width >= 0 && session.height >= 0) { config.m_resolution = Resolution { static_cast(session.width), static_cast(session.height) }; } else { BOOST_LOG(error) << "Resolution provided by client session config is invalid: " << session.width << "x" << session.height; return false; } break; } case resolution_option_e::manual: { if (!session.enable_sops) { BOOST_LOG(warning) << R"(Sunshine is configured to change resolution manually, but the "Optimize game settings" is not set in the client! Resolution will not be changed.)"; } else { if (!parse_resolution_string(video_config.dd.manual_resolution, config.m_resolution)) { BOOST_LOG(error) << "Failed to parse manual resolution string!"; return false; } if (!config.m_resolution) { BOOST_LOG(error) << "Manual resolution must be specified!"; return false; } } break; } case resolution_option_e::disabled: break; } return true; } /** * @brief Parse refresh rate option from the user configuration and the session information. * @param video_config User's video related configuration. * @param session Session information. * @param config A reference to a config object that will be modified on success. * @returns True on successful parsing, false otherwise. * * @examples * const std::shared_ptr launch_session; * const config::video_t &video_config { config::video }; * * SingleDisplayConfiguration config; * const bool success = parse_refresh_rate_option(video_config, *launch_session, config); * @examples_end */ bool parse_refresh_rate_option(const config::video_t &video_config, const rtsp_stream::launch_session_t &session, SingleDisplayConfiguration &config) { using refresh_rate_option_e = config::video_t::dd_t::refresh_rate_option_e; switch (video_config.dd.refresh_rate_option) { case refresh_rate_option_e::automatic: { if (session.fps >= 0) { config.m_refresh_rate = Rational {static_cast(session.fps), 1000}; } else { BOOST_LOG(error) << "FPS value provided by client session config is invalid: " << session.fps; return false; } break; } case refresh_rate_option_e::manual: { if (!parse_refresh_rate_string(video_config.dd.manual_refresh_rate, config.m_refresh_rate)) { BOOST_LOG(error) << "Failed to parse manual refresh rate string!"; return false; } if (!config.m_refresh_rate) { BOOST_LOG(error) << "Manual refresh rate must be specified!"; return false; } break; } case refresh_rate_option_e::disabled: break; } return true; } /** * @brief Parse HDR option from the user configuration and the session information. * @param video_config User's video related configuration. * @param session Session information. * @returns Parsed HDR state value we need to switch to. * Empty optional if no action is required. * * @examples * const std::shared_ptr launch_session; * const config::video_t &video_config { config::video }; * const auto hdr_option = parse_hdr_option(video_config, *launch_session); * @examples_end */ std::optional parse_hdr_option(const config::video_t &video_config, const rtsp_stream::launch_session_t &session) { using hdr_option_e = config::video_t::dd_t::hdr_option_e; switch (video_config.dd.hdr_option) { case hdr_option_e::automatic: return session.enable_hdr ? HdrState::Enabled : HdrState::Disabled; case hdr_option_e::disabled: break; } return std::nullopt; } /** * @brief Indicates which remapping fields and config structure shall be used. */ enum class remapping_type_e { mixed, ///! Both reseolution and refresh rate may be remapped resolution_only, ///! Only resolution will be remapped refresh_rate_only ///! Only refresh rate will be remapped }; /** * @brief Determine the ramapping type from the user config. * @param video_config User's video related configuration. * @returns Enum value if remapping can be performed, null optional if remapping shall be skipped. */ std::optional determine_remapping_type(const config::video_t &video_config) { using dd_t = config::video_t::dd_t; const bool auto_resolution {video_config.dd.resolution_option == dd_t::resolution_option_e::automatic}; const bool auto_refresh_rate {video_config.dd.refresh_rate_option == dd_t::refresh_rate_option_e::automatic}; if (auto_resolution && auto_refresh_rate) { return remapping_type_e::mixed; } if (auto_resolution) { return remapping_type_e::resolution_only; } if (auto_refresh_rate) { return remapping_type_e::refresh_rate_only; } return std::nullopt; } /** * @brief Contains remapping data parsed from the string values. */ struct parsed_remapping_entry_t { std::optional requested_resolution; std::optional requested_fps; std::optional final_resolution; std::optional final_refresh_rate; }; /** * @brief Check if resolution is to be mapped based on remmaping type. * @param type Remapping type to check. * @returns True if resolution is to be mapped, false otherwise. */ bool is_resolution_mapped(const remapping_type_e type) { return type == remapping_type_e::resolution_only || type == remapping_type_e::mixed; } /** * @brief Check if FPS is to be mapped based on remmaping type. * @param type Remapping type to check. * @returns True if FPS is to be mapped, false otherwise. */ bool is_fps_mapped(const remapping_type_e type) { return type == remapping_type_e::refresh_rate_only || type == remapping_type_e::mixed; } /** * @brief Parse the remapping entry from the config into an internal structure. * @param entry Entry to parse. * @param type Specify which entry fields should be parsed. * @returns Parsed structure or null optional if a necessary field could not be parsed. */ std::optional parse_remapping_entry(const config::video_t::dd_t::mode_remapping_entry_t &entry, const remapping_type_e type) { parsed_remapping_entry_t result {}; if (is_resolution_mapped(type) && (!parse_resolution_string(entry.requested_resolution, result.requested_resolution) || !parse_resolution_string(entry.final_resolution, result.final_resolution))) { return std::nullopt; } if (is_fps_mapped(type) && (!parse_refresh_rate_string(entry.requested_fps, result.requested_fps, false) || !parse_refresh_rate_string(entry.final_refresh_rate, result.final_refresh_rate))) { return std::nullopt; } return result; } /** * @brief Remap the the requested display mode based on the config. * @param video_config User's video related configuration. * @param session Session information. * @param config A reference to a config object that will be modified on success. * @returns True if the remapping was performed or skipped, false if remapping has failed due to invalid config. * * @examples * const std::shared_ptr launch_session; * const config::video_t &video_config { config::video }; * * SingleDisplayConfiguration config; * const bool success = remap_display_mode_if_needed(video_config, *launch_session, config); * @examples_end */ bool remap_display_mode_if_needed(const config::video_t &video_config, const rtsp_stream::launch_session_t &session, SingleDisplayConfiguration &config) { const auto remapping_type {determine_remapping_type(video_config)}; if (!remapping_type) { return true; } const auto &remapping_list {[&]() { using enum remapping_type_e; switch (*remapping_type) { case resolution_only: return video_config.dd.mode_remapping.resolution_only; case refresh_rate_only: return video_config.dd.mode_remapping.refresh_rate_only; case mixed: default: return video_config.dd.mode_remapping.mixed; } }()}; if (remapping_list.empty()) { BOOST_LOG(debug) << "No values are available for display mode remapping."; return true; } BOOST_LOG(debug) << "Trying to remap display modes..."; const auto entry_to_string {[type = *remapping_type](const config::video_t::dd_t::mode_remapping_entry_t &entry) { const bool mapping_resolution {is_resolution_mapped(type)}; const bool mapping_fps {is_fps_mapped(type)}; // clang-format off return (mapping_resolution ? " - requested resolution: "s + entry.requested_resolution + "\n" : "") + (mapping_fps ? " - requested FPS: "s + entry.requested_fps + "\n" : "") + (mapping_resolution ? " - final resolution: "s + entry.final_resolution + "\n" : "") + (mapping_fps ? " - final refresh rate: "s + entry.final_refresh_rate : ""); // clang-format on }}; for (const auto &entry : remapping_list) { const auto parsed_entry {parse_remapping_entry(entry, *remapping_type)}; if (!parsed_entry) { BOOST_LOG(error) << "Failed to parse remapping entry from:\n" << entry_to_string(entry); return false; } if (!parsed_entry->final_resolution && !parsed_entry->final_refresh_rate) { BOOST_LOG(error) << "At least one final value must be set for remapping display modes! Entry:\n" << entry_to_string(entry); return false; } if (!session.enable_sops && (parsed_entry->requested_resolution || parsed_entry->final_resolution)) { BOOST_LOG(warning) << R"(Skipping remapping entry, because the "Optimize game settings" is not set in the client! Entry:\n)" << entry_to_string(entry); continue; } // Note: at this point config should already have parsed resolution set. if (parsed_entry->requested_resolution && parsed_entry->requested_resolution != config.m_resolution) { BOOST_LOG(verbose) << "Skipping remapping because requested resolutions do not match! Entry:\n" << entry_to_string(entry); continue; } // Note: at this point config should already have parsed refresh rate set. if (parsed_entry->requested_fps && parsed_entry->requested_fps != config.m_refresh_rate) { BOOST_LOG(verbose) << "Skipping remapping because requested FPS do not match! Entry:\n" << entry_to_string(entry); continue; } BOOST_LOG(info) << "Remapping requested display mode. Entry:\n" << entry_to_string(entry); if (parsed_entry->final_resolution) { config.m_resolution = parsed_entry->final_resolution; } if (parsed_entry->final_refresh_rate) { config.m_refresh_rate = parsed_entry->final_refresh_rate; } break; } return true; } /** * @brief Construct a settings manager interface to manage display device settings. * @param persistence_filepath File location for saving persistent state. * @param video_config User's video related configuration. * @return An interface or nullptr if the OS does not support the interface. */ std::unique_ptr make_settings_manager([[maybe_unused]] const std::filesystem::path &persistence_filepath, [[maybe_unused]] const config::video_t &video_config) { #ifdef _WIN32 return std::make_unique( std::make_shared(std::make_shared()), std::make_shared(), std::make_unique( std::make_shared(persistence_filepath) ), WinWorkarounds { .m_hdr_blank_delay = video_config.dd.wa.hdr_toggle_delay != std::chrono::milliseconds::zero() ? std::make_optional(video_config.dd.wa.hdr_toggle_delay) : std::nullopt } ); #else return nullptr; #endif } /** * @brief Defines the "revert config" algorithms. */ enum class revert_option_e { try_once, ///< Try reverting once and then abort. try_indefinitely, ///< Keep trying to revert indefinitely. try_indefinitely_with_delay ///< Keep trying to revert indefinitely, but delay the first try by some amount of time. }; /** * @brief Reverts the configuration based on the provided option. * @note This is function does not lock mutex. */ void revert_configuration_unlocked(const revert_option_e option) { if (!DD_DATA.sm_instance) { // Platform is not supported, nothing to do. return; } // Note: by default the executor function is immediately executed in the calling thread. With delay, we want to avoid that. SchedulerOptions scheduler_option {.m_sleep_durations = {DEFAULT_RETRY_INTERVAL}}; if (option == revert_option_e::try_indefinitely_with_delay && DD_DATA.config_revert_delay > std::chrono::milliseconds::zero()) { scheduler_option.m_sleep_durations = {DD_DATA.config_revert_delay, DEFAULT_RETRY_INTERVAL}; scheduler_option.m_execution = SchedulerOptions::Execution::ScheduledOnly; } DD_DATA.sm_instance->schedule([try_once = (option == revert_option_e::try_once), tried_out_devices = std::set {}](auto &settings_iface, auto &stop_token) mutable { if (try_once) { std::ignore = settings_iface.revertSettings(); stop_token.requestStop(); return; } auto available_devices {[&settings_iface]() { const auto devices {settings_iface.enumAvailableDevices()}; std::set parsed_devices; std::transform( std::begin(devices), std::end(devices), std::inserter(parsed_devices, std::end(parsed_devices)), [](const auto &device) { return device.m_device_id + " - " + device.m_friendly_name; } ); return parsed_devices; }()}; if (available_devices == tried_out_devices) { BOOST_LOG(debug) << "Skipping reverting configuration, because no newly added/removed devices were detected since last check. Currently available devices:\n" << toJson(available_devices); return; } using enum SettingsManagerInterface::RevertResult; if (const auto result {settings_iface.revertSettings()}; result == Ok) { stop_token.requestStop(); return; } else if (result == ApiTemporarilyUnavailable) { // Do nothing and retry next time return; } // If we have failed to revert settings then we will try to do it next time only if a device was added/removed BOOST_LOG(warning) << "Failed to revert display device configuration (will retry once devices are added or removed). Enabling all of the available devices:\n" << toJson(available_devices); tried_out_devices.swap(available_devices); }, scheduler_option); } } // namespace std::unique_ptr init(const std::filesystem::path &persistence_filepath, const config::video_t &video_config) { std::lock_guard lock {DD_DATA.mutex}; // We can support re-init without any issues, however we should make sure to clean up first! if (video_config.dd.configuration_option == config::video_t::dd_t::config_option_e::disabled) { if (!persistence_filepath.empty() && std::filesystem::exists(persistence_filepath)) { std::filesystem::remove(persistence_filepath); } } else { revert_configuration_unlocked(revert_option_e::try_once); } DD_DATA.config_revert_delay = video_config.dd.config_revert_delay; DD_DATA.sm_instance = nullptr; // If we fail to create settings manager, this means platform is not supported, and // we will need to provided error-free pass-trough in other methods if (auto settings_manager {make_settings_manager(persistence_filepath, video_config)}) { DD_DATA.sm_instance = std::make_unique>(std::move(settings_manager)); const auto available_devices {DD_DATA.sm_instance->execute([](auto &settings_iface) { return settings_iface.enumAvailableDevices(); })}; BOOST_LOG(info) << "Currently available display devices:\n" << toJson(available_devices); // In case we have failed to revert configuration before shutting down, we should // do it now. revert_configuration_unlocked(revert_option_e::try_indefinitely); } class deinit_t: public platf::deinit_t { public: ~deinit_t() override { std::lock_guard lock {DD_DATA.mutex}; try { // This may throw if used incorrectly. At the moment this will not happen, however // in case some unforeseen changes are made that could raise an exception, // we definitely don't want this to happen in destructor. Especially in the // deinit_t where the outcome does not really matter. revert_configuration_unlocked(revert_option_e::try_once); } catch (std::exception &err) { BOOST_LOG(fatal) << err.what(); } DD_DATA.sm_instance = nullptr; } }; return std::make_unique(); } std::string map_output_name(const std::string &output_name) { std::lock_guard lock {DD_DATA.mutex}; if (!DD_DATA.sm_instance) { // Fallback to giving back the output name if the platform is not supported. return output_name; } return DD_DATA.sm_instance->execute([&output_name](auto &settings_iface) { return settings_iface.getDisplayName(output_name); }); } std::string map_display_name(const std::string &display_name) { std::lock_guard lock { DD_DATA.mutex }; if (!DD_DATA.sm_instance) { return {}; } const auto available_devices { DD_DATA.sm_instance->execute([](auto &settings_iface) { return settings_iface.enumAvailableDevices(); }) }; for (auto &i : available_devices) { if (i.m_display_name == display_name) { return i.m_device_id; } } return {}; } void configure_display(const config::video_t &video_config, const rtsp_stream::launch_session_t &session) { const auto result { parse_configuration(video_config, session) }; if (const auto *parsed_config { std::get_if(&result) }; parsed_config) { configure_display(*parsed_config); return; } if (const auto *disabled {std::get_if(&result)}; disabled) { revert_configuration(); return; } // Error already logged for failed_to_parse_tag_t case, and we also don't // want to revert active configuration in case we have any } void configure_display(const SingleDisplayConfiguration &config) { std::lock_guard lock {DD_DATA.mutex}; if (!DD_DATA.sm_instance) { // Platform is not supported, nothing to do. return; } DD_DATA.sm_instance->schedule([config](auto &settings_iface, auto &stop_token) { // We only want to keep retrying in case of a transient errors. // In other cases, when we either fail or succeed we just want to stop... if (settings_iface.applySettings(config) != SettingsManagerInterface::ApplyResult::ApiTemporarilyUnavailable) { stop_token.requestStop(); } }, {.m_sleep_durations = {DEFAULT_RETRY_INTERVAL}}); } void revert_configuration() { std::lock_guard lock {DD_DATA.mutex}; revert_configuration_unlocked(revert_option_e::try_indefinitely_with_delay); } bool reset_persistence() { std::lock_guard lock {DD_DATA.mutex}; if (!DD_DATA.sm_instance) { // Platform is not supported, assume success. return true; } return DD_DATA.sm_instance->execute([](auto &settings_iface, auto &stop_token) { // Whatever the outcome is we want to stop interfering with the user, // so any schedulers need to be stopped. stop_token.requestStop(); return settings_iface.resetPersistence(); }); } EnumeratedDeviceList enumerate_devices() { std::lock_guard lock {DD_DATA.mutex}; if (!DD_DATA.sm_instance) { // Platform is not supported. return {}; } return DD_DATA.sm_instance->execute([](auto &settings_iface) { return settings_iface.enumAvailableDevices(); }); } std::variant parse_configuration(const config::video_t &video_config, const rtsp_stream::launch_session_t &session) { const auto device_prep {parse_device_prep_option(video_config)}; if (!device_prep) { return configuration_disabled_tag_t {}; } SingleDisplayConfiguration config; config.m_device_id = video_config.output_name; config.m_device_prep = *device_prep; config.m_hdr_state = parse_hdr_option(video_config, session); if (!parse_resolution_option(video_config, session, config)) { // Error already logged return failed_to_parse_tag_t {}; } if (!parse_refresh_rate_option(video_config, session, config)) { // Error already logged return failed_to_parse_tag_t {}; } if (!remap_display_mode_if_needed(video_config, session, config)) { // Error already logged return failed_to_parse_tag_t {}; } return config; } } // namespace display_device ================================================ FILE: src/display_device.h ================================================ /** * @file src/display_device.h * @brief Declarations for display device handling. */ #pragma once // standard includes #include #include // lib includes #include // forward declarations namespace platf { class deinit_t; } namespace config { struct video_t; } namespace rtsp_stream { struct launch_session_t; } namespace display_device { /** * @brief Initialize the implementation and perform the initial state recovery (if needed). * @param persistence_filepath File location for reading/saving persistent state. * @param video_config User's video related configuration. * @returns A deinit_t instance that performs cleanup when destroyed. * * @examples * const config::video_t &video_config { config::video }; * const auto init_guard { init("/my/persitence/file.state", video_config) }; * @examples_end */ [[nodiscard]] std::unique_ptr init(const std::filesystem::path &persistence_filepath, const config::video_t &video_config); /** * @brief Map the output name to a specific display. * @param output_name The user-configurable output name. * @returns Mapped display name or empty string if the output name could not be mapped. * * @examples * const auto mapped_name_config { map_output_name(config::video.output_name) }; * const auto mapped_name_custom { map_output_name("{some-device-id}") }; * @examples_end */ [[nodiscard]] std::string map_output_name(const std::string &output_name); [[nodiscard]] std::string map_display_name(const std::string &display_name); /** * @brief Configure the display device based on the user configuration and the session information. * @note This is a convenience method for calling similar method of a different signature. * * @param video_config User's video related configuration. * @param session Session information. * * @examples * const std::shared_ptr launch_session; * const config::video_t &video_config { config::video }; * * configure_display(video_config, *launch_session); * @examples_end */ void configure_display(const config::video_t &video_config, const rtsp_stream::launch_session_t &session); /** * @brief Configure the display device using the provided configuration. * * In some cases configuring display can fail due to transient issues and * we will keep trying every 5 seconds, even if the stream has already started as there was * no possibility to apply settings before the stream start. * * Therefore, there is no return value as we still want to continue with the stream, so that * the users can do something about it once they are connected. Otherwise, we might * prevent users from logging in at all if we keep failing to apply configuration. * * @param config Configuration for the display. * * @examples * const SingleDisplayConfiguration valid_config { }; * configure_display(valid_config); * @examples_end */ void configure_display(const SingleDisplayConfiguration &config); /** * @brief Revert the display configuration and restore the previous state. * * In case the state could not be restored, by default it will be retried again in 5 seconds * (repeating indefinitely until success or until persistence is reset). * * @examples * revert_configuration(); * @examples_end */ void revert_configuration(); /** * @brief Reset the persistence and currently held initial display state. * * This is normally used to get out of the "broken" state where the algorithm wants * to restore the initial display state, but it is no longer possible. * * This could happen if the display is no longer available or the hardware was changed * and the device ids no longer match. * * The user then accepts that Sunshine is not able to restore the state and "agrees" to * do it manually. * * @return True if persistence was reset, false otherwise. * @note Whether the function succeeds or fails, any of the scheduled "retries" from * other methods will be stopped to not interfere with the user actions. * * @examples * const auto result = reset_persistence(); * @examples_end */ bool reset_persistence(); /** * @brief Enumerate the available devices. * @return A list of devices. * * @examples * const auto devices = enumerate_devices(); * @examples_end */ [[nodiscard]] EnumeratedDeviceList enumerate_devices(); /** * @brief A tag structure indicating that configuration parsing has failed. */ struct failed_to_parse_tag_t {}; /** * @brief A tag structure indicating that configuration is disabled. */ struct configuration_disabled_tag_t {}; /** * @brief Parse the user configuration and the session information. * @param video_config User's video related configuration. * @param session Session information. * @return Parsed single display configuration or * a tag indicating that the parsing has failed or * a tag indicating that the user does not want to perform any configuration. * * @examples * const std::shared_ptr launch_session; * const config::video_t &video_config { config::video }; * * const auto config { parse_configuration(video_config, *launch_session) }; * if (const auto *parsed_config { std::get_if(&result) }; parsed_config) { * configure_display(*config); * } * @examples_end */ [[nodiscard]] std::variant parse_configuration(const config::video_t &video_config, const rtsp_stream::launch_session_t &session); } // namespace display_device ================================================ FILE: src/entry_handler.cpp ================================================ /** * @file entry_handler.cpp * @brief Definitions for entry handling functions. */ // standard includes #include #include #include #include // local includes #include "config.h" #include "confighttp.h" #include "entry_handler.h" #include "globals.h" #include "httpcommon.h" #include "logging.h" #include "network.h" #include "platform/common.h" extern "C" { #ifdef _WIN32 #include #endif } using namespace std::literals; void launch_ui(const std::optional &path) { std::string url = std::format("https://localhost:{}", static_cast(net::map_port(confighttp::PORT_HTTPS))); if (path) { url += *path; } platf::open_url(url); } namespace args { int creds(const char *name, int argc, char *argv[]) { if (argc < 2 || argv[0] == "help"sv || argv[1] == "help"sv) { help(name); } http::save_user_creds(config::sunshine.credentials_file, argv[0], argv[1]); return 0; } int help(const char *name) { logging::print_help(name); return 0; } int version() { // version was already logged at startup return 0; } #ifdef _WIN32 int restore_nvprefs_undo() { if (nvprefs_instance.load()) { nvprefs_instance.restore_from_and_delete_undo_file_if_exists(); nvprefs_instance.unload(); } return 0; } #endif } // namespace args namespace lifetime { char **argv; std::atomic_int desired_exit_code; void exit_sunshine(int exit_code, bool async) { // Store the exit code of the first exit_sunshine() call int zero = 0; desired_exit_code.compare_exchange_strong(zero, exit_code); // Raise SIGINT to start termination std::raise(SIGINT); // Termination will happen asynchronously, but the caller may // have wanted synchronous behavior. while (!async) { std::this_thread::sleep_for(1s); } } void debug_trap() { #ifdef _WIN32 DebugBreak(); #else std::raise(SIGTRAP); #endif // If debug trap still doesn't work, abort abort(); } char **get_argv() { return argv; } } // namespace lifetime void log_publisher_data() { BOOST_LOG(info) << "Package Publisher: "sv << SUNSHINE_PUBLISHER_NAME; BOOST_LOG(info) << "Publisher Website: "sv << SUNSHINE_PUBLISHER_WEBSITE; BOOST_LOG(info) << "Get support: "sv << SUNSHINE_PUBLISHER_ISSUE_URL; } #ifdef _WIN32 bool is_gamestream_enabled() { DWORD enabled; DWORD size = sizeof(enabled); return RegGetValueW( HKEY_LOCAL_MACHINE, L"SOFTWARE\\NVIDIA Corporation\\NvStream", L"EnableStreaming", RRF_RT_REG_DWORD, nullptr, &enabled, &size ) == ERROR_SUCCESS && enabled != 0; } namespace service_ctrl { class service_controller { public: /** * @brief Constructor for service_controller class. * @param service_desired_access SERVICE_* desired access flags. */ service_controller(DWORD service_desired_access) { scm_handle = OpenSCManagerA(nullptr, nullptr, SC_MANAGER_CONNECT); if (!scm_handle) { auto winerr = GetLastError(); BOOST_LOG(error) << "OpenSCManager() failed: "sv << winerr; return; } service_handle = OpenServiceA(scm_handle, "ApolloService", service_desired_access); if (!service_handle) { auto winerr = GetLastError(); BOOST_LOG(error) << "OpenService() failed: "sv << winerr; return; } } ~service_controller() { if (service_handle) { CloseServiceHandle(service_handle); } if (scm_handle) { CloseServiceHandle(scm_handle); } } /** * @brief Asynchronously starts the Sunshine service. */ bool start_service() { if (!service_handle) { return false; } if (!StartServiceA(service_handle, 0, nullptr)) { auto winerr = GetLastError(); if (winerr != ERROR_SERVICE_ALREADY_RUNNING) { BOOST_LOG(error) << "StartService() failed: "sv << winerr; return false; } } return true; } /** * @brief Query the service status. * @param status The SERVICE_STATUS struct to populate. */ bool query_service_status(SERVICE_STATUS &status) { if (!service_handle) { return false; } if (!QueryServiceStatus(service_handle, &status)) { auto winerr = GetLastError(); BOOST_LOG(error) << "QueryServiceStatus() failed: "sv << winerr; return false; } return true; } private: SC_HANDLE scm_handle = nullptr; SC_HANDLE service_handle = nullptr; }; bool is_service_running() { service_controller sc {SERVICE_QUERY_STATUS}; SERVICE_STATUS status; if (!sc.query_service_status(status)) { return false; } return status.dwCurrentState == SERVICE_RUNNING; } bool start_service() { service_controller sc {SERVICE_QUERY_STATUS | SERVICE_START}; std::cout << "Starting Sunshine..."sv; // This operation is asynchronous, so we must wait for it to complete if (!sc.start_service()) { return false; } SERVICE_STATUS status; do { Sleep(1000); std::cout << '.'; } while (sc.query_service_status(status) && status.dwCurrentState == SERVICE_START_PENDING); if (status.dwCurrentState != SERVICE_RUNNING) { BOOST_LOG(error) << SERVICE_NAME " failed to start: "sv << status.dwWin32ExitCode; return false; } std::cout << std::endl; return true; } bool wait_for_ui_ready() { std::cout << "Waiting for Web UI to be ready..."; // Wait up to 30 seconds for the web UI to start for (int i = 0; i < 30; i++) { PMIB_TCPTABLE tcp_table = nullptr; ULONG table_size = 0; ULONG err; auto fg = util::fail_guard([&tcp_table]() { free(tcp_table); }); do { // Query all open TCP sockets to look for our web UI port err = GetTcpTable(tcp_table, &table_size, false); if (err == ERROR_INSUFFICIENT_BUFFER) { free(tcp_table); tcp_table = (PMIB_TCPTABLE) malloc(table_size); } } while (err == ERROR_INSUFFICIENT_BUFFER); if (err != NO_ERROR) { BOOST_LOG(error) << "Failed to query TCP table: "sv << err; return false; } uint16_t port_nbo = htons(net::map_port(confighttp::PORT_HTTPS)); for (DWORD i = 0; i < tcp_table->dwNumEntries; i++) { auto &entry = tcp_table->table[i]; // Look for our port in the listening state if (entry.dwLocalPort == port_nbo && entry.dwState == MIB_TCP_STATE_LISTEN) { std::cout << std::endl; return true; } } Sleep(1000); std::cout << '.'; } std::cout << "timed out"sv << std::endl; return false; } } // namespace service_ctrl #endif ================================================ FILE: src/entry_handler.h ================================================ /** * @file entry_handler.h * @brief Declarations for entry handling functions. */ #pragma once // standard includes #include #include // local includes #include "thread_pool.h" #include "thread_safe.h" /** * @brief Launch the Web UI. * @param path Optional path to append to the base URL. * @examples * launch_ui(); * launch_ui("/pin"); * @examples_end */ void launch_ui(const std::optional &path = std::nullopt); /** * @brief Functions for handling command line arguments. */ namespace args { /** * @brief Reset the user credentials. * @param name The name of the program. * @param argc The number of arguments. * @param argv The arguments. * @examples * creds("sunshine", 2, {"new_username", "new_password"}); * @examples_end */ int creds(const char *name, int argc, char *argv[]); /** * @brief Print help to stdout, then exit. * @param name The name of the program. * @examples * help("sunshine"); * @examples_end */ int help(const char *name); /** * @brief Print the version to stdout, then exit. * @examples * version(); * @examples_end */ int version(); #ifdef _WIN32 /** * @brief Restore global NVIDIA control panel settings. * If Sunshine was improperly terminated, this function restores * the global NVIDIA control panel settings to the undo file left * by Sunshine. This function is typically called by the uninstaller. * @examples * restore_nvprefs_undo(); * @examples_end */ int restore_nvprefs_undo(); #endif } // namespace args /** * @brief Functions for handling the lifetime of Sunshine. */ namespace lifetime { extern char **argv; extern std::atomic_int desired_exit_code; /** * @brief Terminates Sunshine gracefully with the provided exit code. * @param exit_code The exit code to return from main(). * @param async Specifies whether our termination will be non-blocking. */ void exit_sunshine(int exit_code, bool async); /** * @brief Breaks into the debugger or terminates Sunshine if no debugger is attached. */ void debug_trap(); /** * @brief Get the argv array passed to main(). */ char **get_argv(); } // namespace lifetime /** * @brief Log the publisher metadata provided from CMake. */ void log_publisher_data(); #ifdef _WIN32 /** * @brief Check if NVIDIA's GameStream software is running. * @return `true` if GameStream is enabled, `false` otherwise. */ bool is_gamestream_enabled(); /** * @brief Namespace for controlling the Sunshine service model on Windows. */ namespace service_ctrl { /** * @brief Check if the service is running. * @examples * is_service_running(); * @examples_end */ bool is_service_running(); /** * @brief Start the service and wait for startup to complete. * @examples * start_service(); * @examples_end */ bool start_service(); /** * @brief Wait for the UI to be ready after Sunshine startup. * @examples * wait_for_ui_ready(); * @examples_end */ bool wait_for_ui_ready(); } // namespace service_ctrl #endif ================================================ FILE: src/file_handler.cpp ================================================ /** * @file file_handler.cpp * @brief Definitions for file handling functions. */ // standard includes #include #include // local includes #include "file_handler.h" #include "logging.h" namespace file_handler { std::string get_parent_directory(const std::string &path) { // remove any trailing path separators std::string trimmed_path = path; while (!trimmed_path.empty() && trimmed_path.back() == '/') { trimmed_path.pop_back(); } std::filesystem::path p(trimmed_path); return p.parent_path().string(); } bool make_directory(const std::string &path) { // first, check if the directory already exists if (std::filesystem::exists(path)) { return true; } return std::filesystem::create_directories(path); } std::string read_file(const char *path) { if (!std::filesystem::exists(path)) { BOOST_LOG(debug) << "Missing file: " << path; return {}; } std::ifstream in(path); return std::string {(std::istreambuf_iterator(in)), std::istreambuf_iterator()}; } int write_file(const char *path, const std::string_view &contents) { std::ofstream out(path); if (!out.is_open()) { return -1; } out << contents; return 0; } } // namespace file_handler ================================================ FILE: src/file_handler.h ================================================ /** * @file file_handler.h * @brief Declarations for file handling functions. */ #pragma once // standard includes #include /** * @brief Responsible for file handling functions. */ namespace file_handler { /** * @brief Get the parent directory of a file or directory. * @param path The path of the file or directory. * @return The parent directory. * @examples * std::string parent_dir = get_parent_directory("path/to/file"); * @examples_end */ std::string get_parent_directory(const std::string &path); /** * @brief Make a directory. * @param path The path of the directory. * @return `true` on success, `false` on failure. * @examples * bool dir_created = make_directory("path/to/directory"); * @examples_end */ bool make_directory(const std::string &path); /** * @brief Read a file to string. * @param path The path of the file. * @return The contents of the file. * @examples * std::string contents = read_file("path/to/file"); * @examples_end */ std::string read_file(const char *path); /** * @brief Writes a file. * @param path The path of the file. * @param contents The contents to write. * @return ``0`` on success, ``-1`` on failure. * @examples * int write_status = write_file("path/to/file", "file contents"); * @examples_end */ int write_file(const char *path, const std::string_view &contents); } // namespace file_handler ================================================ FILE: src/globals.cpp ================================================ /** * @file globals.cpp * @brief Definitions for globally accessible variables and functions. */ // local includes #include "globals.h" safe::mail_t mail::man; thread_pool_util::ThreadPool task_pool; bool display_cursor = true; #ifdef _WIN32 nvprefs::nvprefs_interface nvprefs_instance; #endif ================================================ FILE: src/globals.h ================================================ /** * @file globals.h * @brief Declarations for globally accessible variables and functions. */ #pragma once // local includes #include "entry_handler.h" #include "thread_pool.h" /** * @brief A thread pool for processing tasks. */ extern thread_pool_util::ThreadPool task_pool; /** * @brief A boolean flag to indicate whether the cursor should be displayed. */ extern bool display_cursor; #ifdef _WIN32 // Declare global singleton used for NVIDIA control panel modifications #include "platform/windows/nvprefs/nvprefs_interface.h" /** * @brief A global singleton used for NVIDIA control panel modifications. */ extern nvprefs::nvprefs_interface nvprefs_instance; #endif /** * @brief Handles process-wide communication. */ namespace mail { #define MAIL(x) \ constexpr auto x = std::string_view { \ #x \ } /** * @brief A process-wide communication mechanism. */ extern safe::mail_t man; // Global mail MAIL(shutdown); MAIL(broadcast_shutdown); MAIL(video_packets); MAIL(audio_packets); MAIL(switch_display); // Local mail MAIL(touch_port); MAIL(idr); MAIL(invalidate_ref_frames); MAIL(gamepad_feedback); MAIL(hdr); #undef MAIL } // namespace mail ================================================ FILE: src/httpcommon.cpp ================================================ /** * @file src/httpcommon.cpp * @brief Definitions for common HTTP. */ #define BOOST_BIND_GLOBAL_PLACEHOLDERS // standard includes #include #include #include // lib includes #include #include #include #include #include #include #include #include // local includes #include "config.h" #include "crypto.h" #include "file_handler.h" #include "httpcommon.h" #include "logging.h" #include "network.h" #include "nvhttp.h" #include "platform/common.h" #include "process.h" #include "rtsp.h" #include "utility.h" namespace http { using namespace std::literals; namespace fs = std::filesystem; namespace pt = boost::property_tree; int reload_user_creds(const std::string &file); bool user_creds_exist(const std::string &file); std::string unique_id; uuid_util::uuid_t uuid; net::net_e origin_web_ui_allowed; int init() { bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE]; origin_web_ui_allowed = net::from_enum_string(config::nvhttp.origin_web_ui_allowed); if (clean_slate) { uuid = uuid_util::uuid_t::generate(); unique_id = uuid.string(); auto dir = std::filesystem::temp_directory_path() / "Sunshine"sv; config::nvhttp.cert = (dir / ("cert-"s + unique_id)).string(); config::nvhttp.pkey = (dir / ("pkey-"s + unique_id)).string(); } if ((!fs::exists(config::nvhttp.pkey) || !fs::exists(config::nvhttp.cert)) && create_creds(config::nvhttp.pkey, config::nvhttp.cert)) { return -1; } if (!user_creds_exist(config::sunshine.credentials_file)) { BOOST_LOG(info) << "Open the Web UI to set your new username and password and getting started"; } else if (reload_user_creds(config::sunshine.credentials_file)) { return -1; } return 0; } int save_user_creds(const std::string &file, const std::string &username, const std::string &password, bool run_our_mouth) { nlohmann::json outputTree; if (fs::exists(file)) { try { std::ifstream in(file); in >> outputTree; } catch (std::exception &e) { BOOST_LOG(error) << "Couldn't read user credentials: "sv << e.what(); return -1; } } auto salt = crypto::rand_alphabet(16); outputTree["username"] = username; outputTree["salt"] = salt; outputTree["password"] = util::hex(crypto::hash(password + salt)).to_string(); try { std::ofstream out(file); out << outputTree.dump(4); // Pretty-print with an indent of 4 spaces. } catch (std::exception &e) { BOOST_LOG(error) << "error writing to the credentials file, perhaps try this again as an administrator? Details: "sv << e.what(); return -1; } BOOST_LOG(info) << "New credentials have been created"sv; return 0; } bool user_creds_exist(const std::string &file) { if (!fs::exists(file)) { return false; } pt::ptree inputTree; try { pt::read_json(file, inputTree); return inputTree.find("username") != inputTree.not_found() && inputTree.find("password") != inputTree.not_found() && inputTree.find("salt") != inputTree.not_found(); } catch (std::exception &e) { BOOST_LOG(error) << "validating user credentials: "sv << e.what(); } return false; } int reload_user_creds(const std::string &file) { pt::ptree inputTree; try { pt::read_json(file, inputTree); config::sunshine.username = inputTree.get("username"); config::sunshine.password = inputTree.get("password"); config::sunshine.salt = inputTree.get("salt"); } catch (std::exception &e) { BOOST_LOG(error) << "loading user credentials: "sv << e.what(); return -1; } return 0; } int create_creds(const std::string &pkey, const std::string &cert) { fs::path pkey_path = pkey; fs::path cert_path = cert; auto creds = crypto::gen_creds("Sunshine Gamestream Host"sv, 2048); auto pkey_dir = pkey_path; auto cert_dir = cert_path; pkey_dir.remove_filename(); cert_dir.remove_filename(); std::error_code err_code {}; fs::create_directories(pkey_dir, err_code); if (err_code) { BOOST_LOG(error) << "Couldn't create directory ["sv << pkey_dir << "] :"sv << err_code.message(); return -1; } fs::create_directories(cert_dir, err_code); if (err_code) { BOOST_LOG(error) << "Couldn't create directory ["sv << cert_dir << "] :"sv << err_code.message(); return -1; } if (file_handler::write_file(pkey.c_str(), creds.pkey)) { BOOST_LOG(error) << "Couldn't open ["sv << config::nvhttp.pkey << ']'; return -1; } if (file_handler::write_file(cert.c_str(), creds.x509)) { BOOST_LOG(error) << "Couldn't open ["sv << config::nvhttp.cert << ']'; return -1; } fs::permissions(pkey_path, fs::perms::owner_read | fs::perms::owner_write, fs::perm_options::replace, err_code); if (err_code) { BOOST_LOG(error) << "Couldn't change permissions of ["sv << config::nvhttp.pkey << "] :"sv << err_code.message(); return -1; } fs::permissions(cert_path, fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read | fs::perms::owner_write, fs::perm_options::replace, err_code); if (err_code) { BOOST_LOG(error) << "Couldn't change permissions of ["sv << config::nvhttp.cert << "] :"sv << err_code.message(); return -1; } return 0; } bool download_file(const std::string &url, const std::string &file, long ssl_version) { // sonar complains about weak ssl and tls versions; however sonar cannot detect the fix CURL *curl = curl_easy_init(); // NOSONAR if (!curl) { BOOST_LOG(error) << "Couldn't create CURL instance"; return false; } if (std::string file_dir = file_handler::get_parent_directory(file); !file_handler::make_directory(file_dir)) { BOOST_LOG(error) << "Couldn't create directory ["sv << file_dir << ']'; curl_easy_cleanup(curl); return false; } FILE *fp = fopen(file.c_str(), "wb"); if (!fp) { BOOST_LOG(error) << "Couldn't open ["sv << file << ']'; curl_easy_cleanup(curl); return false; } curl_easy_setopt(curl, CURLOPT_SSLVERSION, ssl_version); // NOSONAR curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); #ifdef _WIN32 curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); #endif CURLcode result = curl_easy_perform(curl); if (result != CURLE_OK) { BOOST_LOG(error) << "Couldn't download ["sv << url << ", code:" << result << ']'; } curl_easy_cleanup(curl); fclose(fp); return result == CURLE_OK; } std::string url_escape(const std::string &url) { char *string = curl_easy_escape(nullptr, url.c_str(), static_cast(url.length())); std::string result(string); curl_free(string); return result; } std::string url_get_host(const std::string &url) { CURLU *curlu = curl_url(); curl_url_set(curlu, CURLUPART_URL, url.c_str(), static_cast(url.length())); char *host; if (curl_url_get(curlu, CURLUPART_HOST, &host, 0) != CURLUE_OK) { curl_url_cleanup(curlu); return ""; } std::string result(host); curl_free(host); curl_url_cleanup(curlu); return result; } } // namespace http ================================================ FILE: src/httpcommon.h ================================================ /** * @file src/httpcommon.h * @brief Declarations for common HTTP. */ #pragma once // lib includes #include // local includes #include "network.h" #include "thread_safe.h" #include "uuid.h" namespace http { int init(); int create_creds(const std::string &pkey, const std::string &cert); int save_user_creds( const std::string &file, const std::string &username, const std::string &password, bool run_our_mouth = false ); int reload_user_creds(const std::string &file); bool download_file(const std::string &url, const std::string &file, long ssl_version = CURL_SSLVERSION_TLSv1_2); std::string url_escape(const std::string &url); std::string url_get_host(const std::string &url); extern std::string unique_id; extern uuid_util::uuid_t uuid; extern net::net_e origin_web_ui_allowed; } // namespace http ================================================ FILE: src/input.cpp ================================================ /** * @file src/input.cpp * @brief Definitions for gamepad, keyboard, and mouse input handling. */ #include extern "C" { #include #include } // standard includes #include #include #include #include #include #include // lib includes #include // local includes #include "config.h" #include "globals.h" #include "input.h" #include "logging.h" #include "platform/common.h" #include "thread_pool.h" #include "utility.h" // Win32 WHEEL_DELTA constant #ifndef WHEEL_DELTA #define WHEEL_DELTA 120 #endif using namespace std::literals; namespace input { constexpr auto MAX_GAMEPADS = std::min((std::size_t) platf::MAX_GAMEPADS, sizeof(std::int16_t) * 8); #define DISABLE_LEFT_BUTTON_DELAY ((thread_pool_util::ThreadPool::task_id_t) 0x01) #define ENABLE_LEFT_BUTTON_DELAY nullptr constexpr auto VKEY_SHIFT = 0x10; constexpr auto VKEY_LSHIFT = 0xA0; constexpr auto VKEY_RSHIFT = 0xA1; constexpr auto VKEY_CONTROL = 0x11; constexpr auto VKEY_LCONTROL = 0xA2; constexpr auto VKEY_RCONTROL = 0xA3; constexpr auto VKEY_MENU = 0x12; constexpr auto VKEY_LMENU = 0xA4; constexpr auto VKEY_RMENU = 0xA5; enum class button_state_e { NONE, ///< No button state DOWN, ///< Button is down UP ///< Button is up }; template int alloc_id(std::bitset &gamepad_mask) { for (int x = 0; x < gamepad_mask.size(); ++x) { if (!gamepad_mask[x]) { gamepad_mask[x] = true; return x; } } return -1; } template void free_id(std::bitset &gamepad_mask, int id) { gamepad_mask[id] = false; } typedef uint32_t key_press_id_t; key_press_id_t make_kpid(uint16_t vk, uint8_t flags) { return (key_press_id_t) vk << 8 | flags; } uint16_t vk_from_kpid(key_press_id_t kpid) { return kpid >> 8; } uint8_t flags_from_kpid(key_press_id_t kpid) { return kpid & 0xFF; } /** * @brief Convert a little-endian netfloat to a native endianness float. * @param f Netfloat value. * @return The native endianness float value. */ float from_netfloat(netfloat f) { return boost::endian::endian_load(f); } /** * @brief Convert a little-endian netfloat to a native endianness float and clamps it. * @param f Netfloat value. * @param min The minimium value for clamping. * @param max The maximum value for clamping. * @return Clamped native endianess float value. */ float from_clamped_netfloat(netfloat f, float min, float max) { return std::clamp(from_netfloat(f), min, max); } static task_pool_util::TaskPool::task_id_t key_press_repeat_id {}; static std::unordered_map key_press {}; static std::array mouse_press {}; static platf::input_t platf_input; static std::bitset gamepadMask {}; void free_gamepad(platf::input_t &platf_input, int id) { platf::gamepad_update(platf_input, id, platf::gamepad_state_t {}); platf::free_gamepad(platf_input, id); free_id(gamepadMask, id); } struct gamepad_t { gamepad_t(): gamepad_state {}, back_timeout_id {}, id {-1}, back_button_state {button_state_e::NONE} { } ~gamepad_t() { if (id >= 0) { task_pool.push([id = this->id]() { free_gamepad(platf_input, id); }); } } platf::gamepad_state_t gamepad_state; thread_pool_util::ThreadPool::task_id_t back_timeout_id; int id; // When emulating the HOME button, we may need to artificially release the back button. // Afterwards, the gamepad state on sunshine won't match the state on Moonlight. // To prevent Sunshine from sending erroneous input data to the active application, // Sunshine forces the button to be in a specific state until the gamepad state matches that of // Moonlight once more. button_state_e back_button_state; }; struct input_t { enum shortkey_e { CTRL = 0x1, ///< Control key ALT = 0x2, ///< Alt key SHIFT = 0x4, ///< Shift key SHORTCUT = CTRL | ALT | SHIFT ///< Shortcut combination }; input_t( safe::mail_raw_t::event_t touch_port_event, platf::feedback_queue_t feedback_queue ): shortcutFlags {}, gamepads(MAX_GAMEPADS), client_context {platf::allocate_client_input_context(platf_input)}, touch_port_event {std::move(touch_port_event)}, feedback_queue {std::move(feedback_queue)}, mouse_left_button_timeout {}, touch_port {{0, 0, 0, 0}, 0, 0, 1.0f}, accumulated_vscroll_delta {}, accumulated_hscroll_delta {} { } // Keep track of alt+ctrl+shift key combo int shortcutFlags; std::vector gamepads; std::unique_ptr client_context; safe::mail_raw_t::event_t touch_port_event; platf::feedback_queue_t feedback_queue; std::list> input_queue; std::mutex input_queue_lock; thread_pool_util::ThreadPool::task_id_t mouse_left_button_timeout; input::touch_port_t touch_port; int32_t accumulated_vscroll_delta; int32_t accumulated_hscroll_delta; }; /** * @brief Apply shortcut based on VKEY * @param keyCode The VKEY code * @return 0 if no shortcut applied, > 0 if shortcut applied. */ inline int apply_shortcut(short keyCode) { constexpr auto VK_F1 = 0x70; constexpr auto VK_F13 = 0x7C; BOOST_LOG(debug) << "Apply Shortcut: 0x"sv << util::hex((std::uint8_t) keyCode).to_string_view(); if (keyCode >= VK_F1 && keyCode <= VK_F13) { mail::man->event(mail::switch_display)->raise(keyCode - VK_F1); return 1; } switch (keyCode) { case 0x4E /* VKEY_N */: display_cursor = !display_cursor; return 1; } return 0; } void print(PNV_REL_MOUSE_MOVE_PACKET packet) { BOOST_LOG(debug) << "--begin relative mouse move packet--"sv << std::endl << "deltaX ["sv << util::endian::big(packet->deltaX) << ']' << std::endl << "deltaY ["sv << util::endian::big(packet->deltaY) << ']' << std::endl << "--end relative mouse move packet--"sv; } void print(PNV_ABS_MOUSE_MOVE_PACKET packet) { BOOST_LOG(debug) << "--begin absolute mouse move packet--"sv << std::endl << "x ["sv << util::endian::big(packet->x) << ']' << std::endl << "y ["sv << util::endian::big(packet->y) << ']' << std::endl << "width ["sv << util::endian::big(packet->width) << ']' << std::endl << "height ["sv << util::endian::big(packet->height) << ']' << std::endl << "--end absolute mouse move packet--"sv; } void print(PNV_MOUSE_BUTTON_PACKET packet) { BOOST_LOG(debug) << "--begin mouse button packet--"sv << std::endl << "action ["sv << util::hex(packet->header.magic).to_string_view() << ']' << std::endl << "button ["sv << util::hex(packet->button).to_string_view() << ']' << std::endl << "--end mouse button packet--"sv; } void print(PNV_SCROLL_PACKET packet) { BOOST_LOG(debug) << "--begin mouse scroll packet--"sv << std::endl << "scrollAmt1 ["sv << util::endian::big(packet->scrollAmt1) << ']' << std::endl << "--end mouse scroll packet--"sv; } void print(PSS_HSCROLL_PACKET packet) { BOOST_LOG(debug) << "--begin mouse hscroll packet--"sv << std::endl << "scrollAmount ["sv << util::endian::big(packet->scrollAmount) << ']' << std::endl << "--end mouse hscroll packet--"sv; } void print(PNV_KEYBOARD_PACKET packet) { BOOST_LOG(debug) << "--begin keyboard packet--"sv << std::endl << "keyAction ["sv << util::hex(packet->header.magic).to_string_view() << ']' << std::endl << "keyCode ["sv << util::hex(packet->keyCode).to_string_view() << ']' << std::endl << "modifiers ["sv << util::hex(packet->modifiers).to_string_view() << ']' << std::endl << "flags ["sv << util::hex(packet->flags).to_string_view() << ']' << std::endl << "--end keyboard packet--"sv; } void print(PNV_UNICODE_PACKET packet) { std::string text(packet->text, util::endian::big(packet->header.size) - sizeof(packet->header.magic)); BOOST_LOG(debug) << "--begin unicode packet--"sv << std::endl << "text ["sv << text << ']' << std::endl << "--end unicode packet--"sv; } void print(PNV_MULTI_CONTROLLER_PACKET packet) { // Moonlight spams controller packet even when not necessary BOOST_LOG(verbose) << "--begin controller packet--"sv << std::endl << "controllerNumber ["sv << packet->controllerNumber << ']' << std::endl << "activeGamepadMask ["sv << util::hex(packet->activeGamepadMask).to_string_view() << ']' << std::endl << "buttonFlags ["sv << util::hex((uint32_t) packet->buttonFlags | (packet->buttonFlags2 << 16)).to_string_view() << ']' << std::endl << "leftTrigger ["sv << util::hex(packet->leftTrigger).to_string_view() << ']' << std::endl << "rightTrigger ["sv << util::hex(packet->rightTrigger).to_string_view() << ']' << std::endl << "leftStickX ["sv << packet->leftStickX << ']' << std::endl << "leftStickY ["sv << packet->leftStickY << ']' << std::endl << "rightStickX ["sv << packet->rightStickX << ']' << std::endl << "rightStickY ["sv << packet->rightStickY << ']' << std::endl << "--end controller packet--"sv; } /** * @brief Prints a touch packet. * @param packet The touch packet. */ void print(PSS_TOUCH_PACKET packet) { BOOST_LOG(debug) << "--begin touch packet--"sv << std::endl << "eventType ["sv << util::hex(packet->eventType).to_string_view() << ']' << std::endl << "pointerId ["sv << util::hex(packet->pointerId).to_string_view() << ']' << std::endl << "x ["sv << from_netfloat(packet->x) << ']' << std::endl << "y ["sv << from_netfloat(packet->y) << ']' << std::endl << "pressureOrDistance ["sv << from_netfloat(packet->pressureOrDistance) << ']' << std::endl << "contactAreaMajor ["sv << from_netfloat(packet->contactAreaMajor) << ']' << std::endl << "contactAreaMinor ["sv << from_netfloat(packet->contactAreaMinor) << ']' << std::endl << "rotation ["sv << (uint32_t) packet->rotation << ']' << std::endl << "--end touch packet--"sv; } /** * @brief Prints a pen packet. * @param packet The pen packet. */ void print(PSS_PEN_PACKET packet) { BOOST_LOG(debug) << "--begin pen packet--"sv << std::endl << "eventType ["sv << util::hex(packet->eventType).to_string_view() << ']' << std::endl << "toolType ["sv << util::hex(packet->toolType).to_string_view() << ']' << std::endl << "penButtons ["sv << util::hex(packet->penButtons).to_string_view() << ']' << std::endl << "x ["sv << from_netfloat(packet->x) << ']' << std::endl << "y ["sv << from_netfloat(packet->y) << ']' << std::endl << "pressureOrDistance ["sv << from_netfloat(packet->pressureOrDistance) << ']' << std::endl << "contactAreaMajor ["sv << from_netfloat(packet->contactAreaMajor) << ']' << std::endl << "contactAreaMinor ["sv << from_netfloat(packet->contactAreaMinor) << ']' << std::endl << "rotation ["sv << (uint32_t) packet->rotation << ']' << std::endl << "tilt ["sv << (uint32_t) packet->tilt << ']' << std::endl << "--end pen packet--"sv; } /** * @brief Prints a controller arrival packet. * @param packet The controller arrival packet. */ void print(PSS_CONTROLLER_ARRIVAL_PACKET packet) { BOOST_LOG(debug) << "--begin controller arrival packet--"sv << std::endl << "controllerNumber ["sv << (uint32_t) packet->controllerNumber << ']' << std::endl << "type ["sv << util::hex(packet->type).to_string_view() << ']' << std::endl << "capabilities ["sv << util::hex(packet->capabilities).to_string_view() << ']' << std::endl << "supportedButtonFlags ["sv << util::hex(packet->supportedButtonFlags).to_string_view() << ']' << std::endl << "--end controller arrival packet--"sv; } /** * @brief Prints a controller touch packet. * @param packet The controller touch packet. */ void print(PSS_CONTROLLER_TOUCH_PACKET packet) { BOOST_LOG(debug) << "--begin controller touch packet--"sv << std::endl << "controllerNumber ["sv << (uint32_t) packet->controllerNumber << ']' << std::endl << "eventType ["sv << util::hex(packet->eventType).to_string_view() << ']' << std::endl << "pointerId ["sv << util::hex(packet->pointerId).to_string_view() << ']' << std::endl << "x ["sv << from_netfloat(packet->x) << ']' << std::endl << "y ["sv << from_netfloat(packet->y) << ']' << std::endl << "pressure ["sv << from_netfloat(packet->pressure) << ']' << std::endl << "--end controller touch packet--"sv; } /** * @brief Prints a controller motion packet. * @param packet The controller motion packet. */ void print(PSS_CONTROLLER_MOTION_PACKET packet) { BOOST_LOG(verbose) << "--begin controller motion packet--"sv << std::endl << "controllerNumber ["sv << util::hex(packet->controllerNumber).to_string_view() << ']' << std::endl << "motionType ["sv << util::hex(packet->motionType).to_string_view() << ']' << std::endl << "x ["sv << from_netfloat(packet->x) << ']' << std::endl << "y ["sv << from_netfloat(packet->y) << ']' << std::endl << "z ["sv << from_netfloat(packet->z) << ']' << std::endl << "--end controller motion packet--"sv; } /** * @brief Prints a controller battery packet. * @param packet The controller battery packet. */ void print(PSS_CONTROLLER_BATTERY_PACKET packet) { BOOST_LOG(verbose) << "--begin controller battery packet--"sv << std::endl << "controllerNumber ["sv << util::hex(packet->controllerNumber).to_string_view() << ']' << std::endl << "batteryState ["sv << util::hex(packet->batteryState).to_string_view() << ']' << std::endl << "batteryPercentage ["sv << util::hex(packet->batteryPercentage).to_string_view() << ']' << std::endl << "--end controller battery packet--"sv; } void print(void *payload) { auto header = (PNV_INPUT_HEADER) payload; switch (util::endian::little(header->magic)) { case MOUSE_MOVE_REL_MAGIC_GEN5: print((PNV_REL_MOUSE_MOVE_PACKET) payload); break; case MOUSE_MOVE_ABS_MAGIC: print((PNV_ABS_MOUSE_MOVE_PACKET) payload); break; case MOUSE_BUTTON_DOWN_EVENT_MAGIC_GEN5: case MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5: print((PNV_MOUSE_BUTTON_PACKET) payload); break; case SCROLL_MAGIC_GEN5: print((PNV_SCROLL_PACKET) payload); break; case SS_HSCROLL_MAGIC: print((PSS_HSCROLL_PACKET) payload); break; case KEY_DOWN_EVENT_MAGIC: case KEY_UP_EVENT_MAGIC: print((PNV_KEYBOARD_PACKET) payload); break; case UTF8_TEXT_EVENT_MAGIC: print((PNV_UNICODE_PACKET) payload); break; case MULTI_CONTROLLER_MAGIC_GEN5: print((PNV_MULTI_CONTROLLER_PACKET) payload); break; case SS_TOUCH_MAGIC: print((PSS_TOUCH_PACKET) payload); break; case SS_PEN_MAGIC: print((PSS_PEN_PACKET) payload); break; case SS_CONTROLLER_ARRIVAL_MAGIC: print((PSS_CONTROLLER_ARRIVAL_PACKET) payload); break; case SS_CONTROLLER_TOUCH_MAGIC: print((PSS_CONTROLLER_TOUCH_PACKET) payload); break; case SS_CONTROLLER_MOTION_MAGIC: print((PSS_CONTROLLER_MOTION_PACKET) payload); break; case SS_CONTROLLER_BATTERY_MAGIC: print((PSS_CONTROLLER_BATTERY_PACKET) payload); break; } } void passthrough(std::shared_ptr &input, PNV_REL_MOUSE_MOVE_PACKET packet) { if (!config::input.mouse) { return; } input->mouse_left_button_timeout = DISABLE_LEFT_BUTTON_DELAY; platf::move_mouse(platf_input, util::endian::big(packet->deltaX), util::endian::big(packet->deltaY)); } /** * @brief Converts client coordinates on the specified surface into screen coordinates. * @param input The input context. * @param val The cartesian coordinate pair to convert. * @param size The size of the client's surface containing the value. * @return The host-relative coordinate pair if a touchport is available. */ std::optional> client_to_touchport(std::shared_ptr &input, const std::pair &val, const std::pair &size) { auto &touch_port_event = input->touch_port_event; auto &touch_port = input->touch_port; if (touch_port_event->peek()) { touch_port = *touch_port_event->pop(); } if (!touch_port) { BOOST_LOG(verbose) << "Ignoring early absolute input without a touch port"sv; return std::nullopt; } auto scalarX = touch_port.width / size.first; auto scalarY = touch_port.height / size.second; float x = std::clamp(val.first, 0.0f, size.first) * scalarX; float y = std::clamp(val.second, 0.0f, size.second) * scalarY; auto offsetX = touch_port.client_offsetX; auto offsetY = touch_port.client_offsetY; x = std::clamp(x, offsetX, (size.first * scalarX) - offsetX); y = std::clamp(y, offsetY, (size.second * scalarY) - offsetY); return std::pair {(x - offsetX) * touch_port.scalar_inv, (y - offsetY) * touch_port.scalar_inv}; } /** * @brief Multiply a polar coordinate pair by a cartesian scaling factor. * @param r The radial coordinate. * @param angle The angular coordinate (radians). * @param scalar The scalar cartesian coordinate pair. * @return The scaled radial coordinate. */ float multiply_polar_by_cartesian_scalar(float r, float angle, const std::pair &scalar) { // Convert polar to cartesian coordinates float x = r * std::cos(angle); float y = r * std::sin(angle); // Scale the values x *= scalar.first; y *= scalar.second; // Convert the result back to a polar radial coordinate return std::sqrt(std::pow(x, 2) + std::pow(y, 2)); } std::pair scale_client_contact_area(const std::pair &val, uint16_t rotation, const std::pair &scalar) { // If the rotation is unknown, we'll just scale both axes equally by using // a 45-degree angle for our scaling calculations float angle = rotation == LI_ROT_UNKNOWN ? (M_PI / 4) : (rotation * (M_PI / 180)); // If we have a major but not a minor axis, treat the touch as circular float major = val.first; float minor = val.second != 0.0f ? val.second : val.first; // The minor axis is perpendicular to major axis so the angle must be rotated by 90 degrees return {multiply_polar_by_cartesian_scalar(major, angle, scalar), multiply_polar_by_cartesian_scalar(minor, angle + (M_PI / 2), scalar)}; } void passthrough(std::shared_ptr &input, PNV_ABS_MOUSE_MOVE_PACKET packet) { if (!config::input.mouse) { return; } if (input->mouse_left_button_timeout == DISABLE_LEFT_BUTTON_DELAY) { input->mouse_left_button_timeout = ENABLE_LEFT_BUTTON_DELAY; } float x = util::endian::big(packet->x); float y = util::endian::big(packet->y); // Prevent divide by zero // Don't expect it to happen, but just in case if (!packet->width || !packet->height) { BOOST_LOG(warning) << "Moonlight passed invalid dimensions"sv; return; } auto width = (float) util::endian::big(packet->width); auto height = (float) util::endian::big(packet->height); auto tpcoords = client_to_touchport(input, {x, y}, {width, height}); if (!tpcoords) { return; } auto &touch_port = input->touch_port; platf::touch_port_t abs_port { touch_port.offset_x, touch_port.offset_y, touch_port.env_width, touch_port.env_height }; platf::abs_mouse(platf_input, abs_port, tpcoords->first, tpcoords->second); } void passthrough(std::shared_ptr &input, PNV_MOUSE_BUTTON_PACKET packet) { if (!config::input.mouse) { return; } auto release = util::endian::little(packet->header.magic) == MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5; auto button = util::endian::big(packet->button); if (button > 0 && button < mouse_press.size()) { if (mouse_press[button] != release) { // button state is already what we want return; } mouse_press[button] = !release; } /** * When Moonlight sends mouse input through absolute coordinates, * it's possible that BUTTON_RIGHT is pressed down immediately after releasing BUTTON_LEFT. * As a result, Sunshine will left-click on hyperlinks in the browser before right-clicking * * This can be solved by delaying BUTTON_LEFT, however, any delay on input is undesirable during gaming * As a compromise, Sunshine will only put delays on BUTTON_LEFT when * absolute mouse coordinates have been sent. * * Try to make sure BUTTON_RIGHT gets called before BUTTON_LEFT is released. * * input->mouse_left_button_timeout can only be nullptr * when the last mouse coordinates were absolute */ if (button == BUTTON_LEFT && release && !input->mouse_left_button_timeout) { auto f = [=]() { auto left_released = mouse_press[BUTTON_LEFT]; if (left_released) { // Already released left button return; } platf::button_mouse(platf_input, BUTTON_LEFT, release); mouse_press[BUTTON_LEFT] = false; input->mouse_left_button_timeout = nullptr; }; input->mouse_left_button_timeout = task_pool.pushDelayed(std::move(f), 10ms).task_id; return; } if ( button == BUTTON_RIGHT && !release && input->mouse_left_button_timeout > DISABLE_LEFT_BUTTON_DELAY ) { platf::button_mouse(platf_input, BUTTON_RIGHT, false); platf::button_mouse(platf_input, BUTTON_RIGHT, true); mouse_press[BUTTON_RIGHT] = false; return; } platf::button_mouse(platf_input, button, release); } short map_keycode(short keycode) { auto it = config::input.keybindings.find(keycode); if (it != std::end(config::input.keybindings)) { return it->second; } return keycode; } /** * @brief Update flags for keyboard shortcut combo's */ inline void update_shortcutFlags(int *flags, short keyCode, bool release) { switch (keyCode) { case VKEY_SHIFT: case VKEY_LSHIFT: case VKEY_RSHIFT: if (release) { *flags &= ~input_t::SHIFT; } else { *flags |= input_t::SHIFT; } break; case VKEY_CONTROL: case VKEY_LCONTROL: case VKEY_RCONTROL: if (release) { *flags &= ~input_t::CTRL; } else { *flags |= input_t::CTRL; } break; case VKEY_MENU: case VKEY_LMENU: case VKEY_RMENU: if (release) { *flags &= ~input_t::ALT; } else { *flags |= input_t::ALT; } break; } } bool is_modifier(uint16_t keyCode) { switch (keyCode) { case VKEY_SHIFT: case VKEY_LSHIFT: case VKEY_RSHIFT: case VKEY_CONTROL: case VKEY_LCONTROL: case VKEY_RCONTROL: case VKEY_MENU: case VKEY_LMENU: case VKEY_RMENU: return true; default: return false; } } void send_key_and_modifiers(uint16_t key_code, bool release, uint8_t flags, uint8_t synthetic_modifiers) { if (!release) { // Press any synthetic modifiers required for this key if (synthetic_modifiers & MODIFIER_SHIFT) { platf::keyboard_update(platf_input, VKEY_SHIFT, false, flags); } if (synthetic_modifiers & MODIFIER_CTRL) { platf::keyboard_update(platf_input, VKEY_CONTROL, false, flags); } if (synthetic_modifiers & MODIFIER_ALT) { platf::keyboard_update(platf_input, VKEY_MENU, false, flags); } } platf::keyboard_update(platf_input, map_keycode(key_code), release, flags); if (!release) { // Raise any synthetic modifier keys we pressed if (synthetic_modifiers & MODIFIER_SHIFT) { platf::keyboard_update(platf_input, VKEY_SHIFT, true, flags); } if (synthetic_modifiers & MODIFIER_CTRL) { platf::keyboard_update(platf_input, VKEY_CONTROL, true, flags); } if (synthetic_modifiers & MODIFIER_ALT) { platf::keyboard_update(platf_input, VKEY_MENU, true, flags); } } } void repeat_key(uint16_t key_code, uint8_t flags, uint8_t synthetic_modifiers) { // If key no longer pressed, stop repeating if (!key_press[make_kpid(key_code, flags)]) { key_press_repeat_id = nullptr; return; } send_key_and_modifiers(key_code, false, flags, synthetic_modifiers); key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_period, key_code, flags, synthetic_modifiers).task_id; } void passthrough(std::shared_ptr &input, PNV_KEYBOARD_PACKET packet) { if (!config::input.keyboard) { return; } auto release = util::endian::little(packet->header.magic) == KEY_UP_EVENT_MAGIC; auto keyCode = packet->keyCode & 0x00FF; // Set synthetic modifier flags if the keyboard packet is requesting modifier // keys that are not current pressed. uint8_t synthetic_modifiers = 0; if (!release && !is_modifier(keyCode)) { if (!(input->shortcutFlags & input_t::SHIFT) && (packet->modifiers & MODIFIER_SHIFT)) { synthetic_modifiers |= MODIFIER_SHIFT; } if (!(input->shortcutFlags & input_t::CTRL) && (packet->modifiers & MODIFIER_CTRL)) { synthetic_modifiers |= MODIFIER_CTRL; } if (!(input->shortcutFlags & input_t::ALT) && (packet->modifiers & MODIFIER_ALT)) { synthetic_modifiers |= MODIFIER_ALT; } } auto &pressed = key_press[make_kpid(keyCode, packet->flags)]; if (!pressed) { if (!release) { // A new key has been pressed down, we need to check for key combo's // If a key-combo has been pressed down, don't pass it through if (input->shortcutFlags == input_t::SHORTCUT && apply_shortcut(keyCode) > 0) { return; } if (key_press_repeat_id) { task_pool.cancel(key_press_repeat_id); } if (config::input.key_repeat_delay.count() > 0) { key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_delay, keyCode, packet->flags, synthetic_modifiers).task_id; } } else { // Already released return; } } else if (!release) { // Already pressed down key return; } pressed = !release; send_key_and_modifiers(keyCode, release, packet->flags, synthetic_modifiers); update_shortcutFlags(&input->shortcutFlags, map_keycode(keyCode), release); } /** * @brief Called to pass a vertical scroll message the platform backend. * @param input The input context pointer. * @param packet The scroll packet. */ void passthrough(std::shared_ptr &input, PNV_SCROLL_PACKET packet) { if (!config::input.mouse) { return; } if (config::input.high_resolution_scrolling) { platf::scroll(platf_input, util::endian::big(packet->scrollAmt1)); } else { input->accumulated_vscroll_delta += util::endian::big(packet->scrollAmt1); auto full_ticks = input->accumulated_vscroll_delta / WHEEL_DELTA; if (full_ticks) { // Send any full ticks that have accumulated and store the rest platf::scroll(platf_input, full_ticks * WHEEL_DELTA); input->accumulated_vscroll_delta -= full_ticks * WHEEL_DELTA; } } } /** * @brief Called to pass a horizontal scroll message the platform backend. * @param input The input context pointer. * @param packet The scroll packet. */ void passthrough(std::shared_ptr &input, PSS_HSCROLL_PACKET packet) { if (!config::input.mouse) { return; } if (config::input.high_resolution_scrolling) { platf::hscroll(platf_input, util::endian::big(packet->scrollAmount)); } else { input->accumulated_hscroll_delta += util::endian::big(packet->scrollAmount); auto full_ticks = input->accumulated_hscroll_delta / WHEEL_DELTA; if (full_ticks) { // Send any full ticks that have accumulated and store the rest platf::hscroll(platf_input, full_ticks * WHEEL_DELTA); input->accumulated_hscroll_delta -= full_ticks * WHEEL_DELTA; } } } void passthrough(PNV_UNICODE_PACKET packet) { if (!config::input.keyboard) { return; } auto size = util::endian::big(packet->header.size) - sizeof(packet->header.magic); platf::unicode(platf_input, packet->text, size); } /** * @brief Called to pass a controller arrival message to the platform backend. * @param input The input context pointer. * @param packet The controller arrival packet. */ void passthrough(std::shared_ptr &input, PSS_CONTROLLER_ARRIVAL_PACKET packet) { if (!config::input.controller) { return; } if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) { BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']'; return; } if (input->gamepads[packet->controllerNumber].id >= 0) { BOOST_LOG(warning) << "ControllerNumber already allocated ["sv << packet->controllerNumber << ']'; return; } platf::gamepad_arrival_t arrival { packet->type, util::endian::little(packet->capabilities), util::endian::little(packet->supportedButtonFlags), }; auto id = alloc_id(gamepadMask); if (id < 0) { return; } // Allocate a new gamepad if (platf::alloc_gamepad(platf_input, {id, packet->controllerNumber}, arrival, input->feedback_queue)) { free_id(gamepadMask, id); return; } input->gamepads[packet->controllerNumber].id = id; } /** * @brief Called to pass a touch message to the platform backend. * @param input The input context pointer. * @param packet The touch packet. */ void passthrough(std::shared_ptr &input, PSS_TOUCH_PACKET packet) { if (!config::input.mouse) { return; } // Convert the client normalized coordinates to touchport coordinates auto coords = client_to_touchport(input, {from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f}, {65535.f, 65535.f}); if (!coords) { return; } auto &touch_port = input->touch_port; platf::touch_port_t abs_port { touch_port.offset_x, touch_port.offset_y, touch_port.env_width, touch_port.env_height }; // Renormalize the coordinates coords->first /= abs_port.width; coords->second /= abs_port.height; // Normalize rotation value to 0-359 degree range auto rotation = util::endian::little(packet->rotation); if (rotation != LI_ROT_UNKNOWN) { rotation %= 360; } // Normalize the contact area based on the touchport auto contact_area = scale_client_contact_area( {from_clamped_netfloat(packet->contactAreaMajor, 0.0f, 1.0f) * 65535.f, from_clamped_netfloat(packet->contactAreaMinor, 0.0f, 1.0f) * 65535.f}, rotation, {abs_port.width / 65535.f, abs_port.height / 65535.f} ); platf::touch_input_t touch { packet->eventType, rotation, util::endian::little(packet->pointerId), coords->first, coords->second, from_clamped_netfloat(packet->pressureOrDistance, 0.0f, 1.0f), contact_area.first, contact_area.second, }; platf::touch_update(input->client_context.get(), abs_port, touch); } /** * @brief Called to pass a pen message to the platform backend. * @param input The input context pointer. * @param packet The pen packet. */ void passthrough(std::shared_ptr &input, PSS_PEN_PACKET packet) { if (!config::input.mouse) { return; } // Convert the client normalized coordinates to touchport coordinates auto coords = client_to_touchport(input, {from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f}, {65535.f, 65535.f}); if (!coords) { return; } auto &touch_port = input->touch_port; platf::touch_port_t abs_port { touch_port.offset_x, touch_port.offset_y, touch_port.env_width, touch_port.env_height }; // Renormalize the coordinates coords->first /= abs_port.width; coords->second /= abs_port.height; // Normalize rotation value to 0-359 degree range auto rotation = util::endian::little(packet->rotation); if (rotation != LI_ROT_UNKNOWN) { rotation %= 360; } // Normalize the contact area based on the touchport auto contact_area = scale_client_contact_area( {from_clamped_netfloat(packet->contactAreaMajor, 0.0f, 1.0f) * 65535.f, from_clamped_netfloat(packet->contactAreaMinor, 0.0f, 1.0f) * 65535.f}, rotation, {abs_port.width / 65535.f, abs_port.height / 65535.f} ); platf::pen_input_t pen { packet->eventType, packet->toolType, packet->penButtons, packet->tilt, rotation, coords->first, coords->second, from_clamped_netfloat(packet->pressureOrDistance, 0.0f, 1.0f), contact_area.first, contact_area.second, }; platf::pen_update(input->client_context.get(), abs_port, pen); } /** * @brief Called to pass a controller touch message to the platform backend. * @param input The input context pointer. * @param packet The controller touch packet. */ void passthrough(std::shared_ptr &input, PSS_CONTROLLER_TOUCH_PACKET packet) { if (!config::input.controller) { return; } if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) { BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']'; return; } auto &gamepad = input->gamepads[packet->controllerNumber]; if (gamepad.id < 0) { BOOST_LOG(warning) << "ControllerNumber ["sv << packet->controllerNumber << "] not allocated"sv; return; } platf::gamepad_touch_t touch { {gamepad.id, packet->controllerNumber}, packet->eventType, util::endian::little(packet->pointerId), from_clamped_netfloat(packet->x, 0.0f, 1.0f), from_clamped_netfloat(packet->y, 0.0f, 1.0f), from_clamped_netfloat(packet->pressure, 0.0f, 1.0f), }; platf::gamepad_touch(platf_input, touch); } /** * @brief Called to pass a controller motion message to the platform backend. * @param input The input context pointer. * @param packet The controller motion packet. */ void passthrough(std::shared_ptr &input, PSS_CONTROLLER_MOTION_PACKET packet) { if (!config::input.controller) { return; } if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) { BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']'; return; } auto &gamepad = input->gamepads[packet->controllerNumber]; if (gamepad.id < 0) { BOOST_LOG(warning) << "ControllerNumber ["sv << packet->controllerNumber << "] not allocated"sv; return; } platf::gamepad_motion_t motion { {gamepad.id, packet->controllerNumber}, packet->motionType, from_netfloat(packet->x), from_netfloat(packet->y), from_netfloat(packet->z), }; platf::gamepad_motion(platf_input, motion); } /** * @brief Called to pass a controller battery message to the platform backend. * @param input The input context pointer. * @param packet The controller battery packet. */ void passthrough(std::shared_ptr &input, PSS_CONTROLLER_BATTERY_PACKET packet) { if (!config::input.controller) { return; } if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) { BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']'; return; } auto &gamepad = input->gamepads[packet->controllerNumber]; if (gamepad.id < 0) { BOOST_LOG(warning) << "ControllerNumber ["sv << packet->controllerNumber << "] not allocated"sv; return; } platf::gamepad_battery_t battery { {gamepad.id, packet->controllerNumber}, packet->batteryState, packet->batteryPercentage }; platf::gamepad_battery(platf_input, battery); } void passthrough(std::shared_ptr &input, PNV_MULTI_CONTROLLER_PACKET packet) { if (!config::input.controller) { return; } if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) { BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']'; return; } auto &gamepad = input->gamepads[packet->controllerNumber]; // If this is an event for a new gamepad, create the gamepad now. Ideally, the client would // send a controller arrival instead of this but it's still supported for legacy clients. if ((packet->activeGamepadMask & (1 << packet->controllerNumber)) && gamepad.id < 0) { auto id = alloc_id(gamepadMask); if (id < 0) { return; } if (platf::alloc_gamepad(platf_input, {id, (uint8_t) packet->controllerNumber}, {}, input->feedback_queue)) { free_id(gamepadMask, id); return; } gamepad.id = id; } else if (!(packet->activeGamepadMask & (1 << packet->controllerNumber)) && gamepad.id >= 0) { // If this is the final event for a gamepad being removed, free the gamepad and return. free_gamepad(platf_input, gamepad.id); gamepad.id = -1; return; } // If this gamepad has not been initialized, ignore it. // This could happen when platf::alloc_gamepad fails if (gamepad.id < 0) { BOOST_LOG(warning) << "ControllerNumber ["sv << packet->controllerNumber << "] not allocated"sv; return; } std::uint16_t bf = packet->buttonFlags; std::uint32_t bf2 = packet->buttonFlags2; platf::gamepad_state_t gamepad_state { bf | (bf2 << 16), packet->leftTrigger, packet->rightTrigger, packet->leftStickX, packet->leftStickY, packet->rightStickX, packet->rightStickY }; auto bf_new = gamepad_state.buttonFlags; switch (gamepad.back_button_state) { case button_state_e::UP: if (!(platf::BACK & bf_new)) { gamepad.back_button_state = button_state_e::NONE; } gamepad_state.buttonFlags &= ~platf::BACK; break; case button_state_e::DOWN: if (platf::BACK & bf_new) { gamepad.back_button_state = button_state_e::NONE; } gamepad_state.buttonFlags |= platf::BACK; break; case button_state_e::NONE: break; } bf = gamepad_state.buttonFlags ^ gamepad.gamepad_state.buttonFlags; bf_new = gamepad_state.buttonFlags; if (platf::BACK & bf) { if (platf::BACK & bf_new) { // Don't emulate home button if timeout < 0 if (config::input.back_button_timeout >= 0ms) { auto f = [input, controller = packet->controllerNumber]() { auto &gamepad = input->gamepads[controller]; auto &state = gamepad.gamepad_state; // Force the back button up gamepad.back_button_state = button_state_e::UP; state.buttonFlags &= ~platf::BACK; platf::gamepad_update(platf_input, gamepad.id, state); // Press Home button state.buttonFlags |= platf::HOME; platf::gamepad_update(platf_input, gamepad.id, state); // Sleep for a short time to allow the input to be detected std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Release Home button state.buttonFlags &= ~platf::HOME; platf::gamepad_update(platf_input, gamepad.id, state); gamepad.back_timeout_id = nullptr; }; gamepad.back_timeout_id = task_pool.pushDelayed(std::move(f), config::input.back_button_timeout).task_id; } } else if (gamepad.back_timeout_id) { task_pool.cancel(gamepad.back_timeout_id); gamepad.back_timeout_id = nullptr; } } platf::gamepad_update(platf_input, gamepad.id, gamepad_state); gamepad.gamepad_state = gamepad_state; } enum class batch_result_e { batched, ///< This entry was batched with the source entry not_batchable, ///< Not eligible to batch but continue attempts to batch terminate_batch, ///< Stop trying to batch with this entry }; /** * @brief Batch two relative mouse messages. * @param dest The original packet to batch into. * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ batch_result_e batch(PNV_REL_MOUSE_MOVE_PACKET dest, PNV_REL_MOUSE_MOVE_PACKET src) { short deltaX, deltaY; // Batching is safe as long as the result doesn't overflow a 16-bit integer if (!__builtin_add_overflow(util::endian::big(dest->deltaX), util::endian::big(src->deltaX), &deltaX)) { return batch_result_e::terminate_batch; } if (!__builtin_add_overflow(util::endian::big(dest->deltaY), util::endian::big(src->deltaY), &deltaY)) { return batch_result_e::terminate_batch; } // Take the sum of deltas dest->deltaX = util::endian::big(deltaX); dest->deltaY = util::endian::big(deltaY); return batch_result_e::batched; } /** * @brief Batch two absolute mouse messages. * @param dest The original packet to batch into. * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ batch_result_e batch(PNV_ABS_MOUSE_MOVE_PACKET dest, PNV_ABS_MOUSE_MOVE_PACKET src) { // Batching must only happen if the reference width and height don't change if (dest->width != src->width || dest->height != src->height) { return batch_result_e::terminate_batch; } // Take the latest absolute position *dest = *src; return batch_result_e::batched; } /** * @brief Batch two vertical scroll messages. * @param dest The original packet to batch into. * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ batch_result_e batch(PNV_SCROLL_PACKET dest, PNV_SCROLL_PACKET src) { short scrollAmt; // Batching is safe as long as the result doesn't overflow a 16-bit integer if (!__builtin_add_overflow(util::endian::big(dest->scrollAmt1), util::endian::big(src->scrollAmt1), &scrollAmt)) { return batch_result_e::terminate_batch; } // Take the sum of delta dest->scrollAmt1 = util::endian::big(scrollAmt); dest->scrollAmt2 = util::endian::big(scrollAmt); return batch_result_e::batched; } /** * @brief Batch two horizontal scroll messages. * @param dest The original packet to batch into. * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ batch_result_e batch(PSS_HSCROLL_PACKET dest, PSS_HSCROLL_PACKET src) { short scrollAmt; // Batching is safe as long as the result doesn't overflow a 16-bit integer if (!__builtin_add_overflow(util::endian::big(dest->scrollAmount), util::endian::big(src->scrollAmount), &scrollAmt)) { return batch_result_e::terminate_batch; } // Take the sum of delta dest->scrollAmount = util::endian::big(scrollAmt); return batch_result_e::batched; } /** * @brief Batch two controller state messages. * @param dest The original packet to batch into. * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ batch_result_e batch(PNV_MULTI_CONTROLLER_PACKET dest, PNV_MULTI_CONTROLLER_PACKET src) { // Do not allow batching if the active controllers change if (dest->activeGamepadMask != src->activeGamepadMask) { return batch_result_e::terminate_batch; } // We can only batch entries for the same controller, but allow batching attempts to continue // in case we have more packets for this controller later in the queue. if (dest->controllerNumber != src->controllerNumber) { return batch_result_e::not_batchable; } // Do not allow batching if the button state changes on this controller if (dest->buttonFlags != src->buttonFlags || dest->buttonFlags2 != src->buttonFlags2) { return batch_result_e::terminate_batch; } // Take the latest state *dest = *src; return batch_result_e::batched; } /** * @brief Batch two touch messages. * @param dest The original packet to batch into. * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ batch_result_e batch(PSS_TOUCH_PACKET dest, PSS_TOUCH_PACKET src) { // Only batch hover or move events if (dest->eventType != LI_TOUCH_EVENT_MOVE && dest->eventType != LI_TOUCH_EVENT_HOVER) { return batch_result_e::terminate_batch; } // Don't batch beyond state changing events if (src->eventType != LI_TOUCH_EVENT_MOVE && src->eventType != LI_TOUCH_EVENT_HOVER) { return batch_result_e::terminate_batch; } // Batched events must be the same pointer ID if (dest->pointerId != src->pointerId) { return batch_result_e::not_batchable; } // The pointer must be in the same state if (dest->eventType != src->eventType) { return batch_result_e::terminate_batch; } // Take the latest state *dest = *src; return batch_result_e::batched; } /** * @brief Batch two pen messages. * @param dest The original packet to batch into. * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ batch_result_e batch(PSS_PEN_PACKET dest, PSS_PEN_PACKET src) { // Only batch hover or move events if (dest->eventType != LI_TOUCH_EVENT_MOVE && dest->eventType != LI_TOUCH_EVENT_HOVER) { return batch_result_e::terminate_batch; } // Batched events must be the same type if (dest->eventType != src->eventType) { return batch_result_e::terminate_batch; } // Do not allow batching if the button state changes if (dest->penButtons != src->penButtons) { return batch_result_e::terminate_batch; } // Do not batch beyond tool changes if (dest->toolType != src->toolType) { return batch_result_e::terminate_batch; } // Take the latest state *dest = *src; return batch_result_e::batched; } /** * @brief Batch two controller touch messages. * @param dest The original packet to batch into. * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ batch_result_e batch(PSS_CONTROLLER_TOUCH_PACKET dest, PSS_CONTROLLER_TOUCH_PACKET src) { // Only batch hover or move events if (dest->eventType != LI_TOUCH_EVENT_MOVE && dest->eventType != LI_TOUCH_EVENT_HOVER) { return batch_result_e::terminate_batch; } // We can only batch entries for the same controller, but allow batching attempts to continue // in case we have more packets for this controller later in the queue. if (dest->controllerNumber != src->controllerNumber) { return batch_result_e::not_batchable; } // Don't batch beyond state changing events if (src->eventType != LI_TOUCH_EVENT_MOVE && src->eventType != LI_TOUCH_EVENT_HOVER) { return batch_result_e::terminate_batch; } // Batched events must be the same pointer ID if (dest->pointerId != src->pointerId) { return batch_result_e::not_batchable; } // The pointer must be in the same state if (dest->eventType != src->eventType) { return batch_result_e::terminate_batch; } // Take the latest state *dest = *src; return batch_result_e::batched; } /** * @brief Batch two controller motion messages. * @param dest The original packet to batch into. * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ batch_result_e batch(PSS_CONTROLLER_MOTION_PACKET dest, PSS_CONTROLLER_MOTION_PACKET src) { // We can only batch entries for the same controller, but allow batching attempts to continue // in case we have more packets for this controller later in the queue. if (dest->controllerNumber != src->controllerNumber) { return batch_result_e::not_batchable; } // Batched events must be the same sensor if (dest->motionType != src->motionType) { return batch_result_e::not_batchable; } // Take the latest state *dest = *src; return batch_result_e::batched; } /** * @brief Batch two input messages. * @param dest The original packet to batch into. * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ batch_result_e batch(PNV_INPUT_HEADER dest, PNV_INPUT_HEADER src) { // We can only batch if the packet types are the same if (dest->magic != src->magic) { return batch_result_e::terminate_batch; } // We can only batch certain message types switch (util::endian::little(dest->magic)) { case MOUSE_MOVE_REL_MAGIC_GEN5: return batch((PNV_REL_MOUSE_MOVE_PACKET) dest, (PNV_REL_MOUSE_MOVE_PACKET) src); case MOUSE_MOVE_ABS_MAGIC: return batch((PNV_ABS_MOUSE_MOVE_PACKET) dest, (PNV_ABS_MOUSE_MOVE_PACKET) src); case SCROLL_MAGIC_GEN5: return batch((PNV_SCROLL_PACKET) dest, (PNV_SCROLL_PACKET) src); case SS_HSCROLL_MAGIC: return batch((PSS_HSCROLL_PACKET) dest, (PSS_HSCROLL_PACKET) src); case MULTI_CONTROLLER_MAGIC_GEN5: return batch((PNV_MULTI_CONTROLLER_PACKET) dest, (PNV_MULTI_CONTROLLER_PACKET) src); case SS_TOUCH_MAGIC: return batch((PSS_TOUCH_PACKET) dest, (PSS_TOUCH_PACKET) src); case SS_PEN_MAGIC: return batch((PSS_PEN_PACKET) dest, (PSS_PEN_PACKET) src); case SS_CONTROLLER_TOUCH_MAGIC: return batch((PSS_CONTROLLER_TOUCH_PACKET) dest, (PSS_CONTROLLER_TOUCH_PACKET) src); case SS_CONTROLLER_MOTION_MAGIC: return batch((PSS_CONTROLLER_MOTION_PACKET) dest, (PSS_CONTROLLER_MOTION_PACKET) src); default: // Not a batchable message type return batch_result_e::terminate_batch; } } /** * @brief Called on a thread pool thread to process an input message. * @param input The input context pointer. */ void passthrough_next_message(std::shared_ptr input) { // 'entry' backs the 'payload' pointer, so they must remain in scope together std::vector entry; PNV_INPUT_HEADER payload; // Lock the input queue while batching, but release it before sending // the input to the OS. This avoids potentially lengthy lock contention // in the control stream thread while input is being processed by the OS. { std::lock_guard lg(input->input_queue_lock); // If all entries have already been processed, nothing to do if (input->input_queue.empty()) { return; } // Pop off the first entry, which we will send entry = input->input_queue.front(); payload = (PNV_INPUT_HEADER) entry.data(); input->input_queue.pop_front(); // Try to batch with remaining items on the queue auto i = input->input_queue.begin(); while (i != input->input_queue.end()) { auto batchable_entry = *i; auto batchable_payload = (PNV_INPUT_HEADER) batchable_entry.data(); auto batch_result = batch(payload, batchable_payload); if (batch_result == batch_result_e::terminate_batch) { // Stop batching break; } else if (batch_result == batch_result_e::batched) { // Erase this entry since it was batched i = input->input_queue.erase(i); } else { // We couldn't batch this entry, but try to batch later entries. i++; } } } // Print the final input packet input::print((void *) payload); // Send the batched input to the OS switch (util::endian::little(payload->magic)) { case MOUSE_MOVE_REL_MAGIC_GEN5: passthrough(input, (PNV_REL_MOUSE_MOVE_PACKET) payload); break; case MOUSE_MOVE_ABS_MAGIC: passthrough(input, (PNV_ABS_MOUSE_MOVE_PACKET) payload); break; case MOUSE_BUTTON_DOWN_EVENT_MAGIC_GEN5: case MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5: passthrough(input, (PNV_MOUSE_BUTTON_PACKET) payload); break; case SCROLL_MAGIC_GEN5: passthrough(input, (PNV_SCROLL_PACKET) payload); break; case SS_HSCROLL_MAGIC: passthrough(input, (PSS_HSCROLL_PACKET) payload); break; case KEY_DOWN_EVENT_MAGIC: case KEY_UP_EVENT_MAGIC: passthrough(input, (PNV_KEYBOARD_PACKET) payload); break; case UTF8_TEXT_EVENT_MAGIC: passthrough((PNV_UNICODE_PACKET) payload); break; case MULTI_CONTROLLER_MAGIC_GEN5: passthrough(input, (PNV_MULTI_CONTROLLER_PACKET) payload); break; case SS_TOUCH_MAGIC: passthrough(input, (PSS_TOUCH_PACKET) payload); break; case SS_PEN_MAGIC: passthrough(input, (PSS_PEN_PACKET) payload); break; case SS_CONTROLLER_ARRIVAL_MAGIC: passthrough(input, (PSS_CONTROLLER_ARRIVAL_PACKET) payload); break; case SS_CONTROLLER_TOUCH_MAGIC: passthrough(input, (PSS_CONTROLLER_TOUCH_PACKET) payload); break; case SS_CONTROLLER_MOTION_MAGIC: passthrough(input, (PSS_CONTROLLER_MOTION_PACKET) payload); break; case SS_CONTROLLER_BATTERY_MAGIC: passthrough(input, (PSS_CONTROLLER_BATTERY_PACKET) payload); break; } } /** * @brief Called on the control stream thread to queue an input message. * @param input The input context pointer. * @param input_data The input message. */ void passthrough(std::shared_ptr &input, std::vector &&input_data, const crypto::PERM& permission) { // No input permissions at all if (!(permission & crypto::PERM::_all_inputs)) { return; } // Have some input permission // Otherwise have all input permission if ((permission & crypto::PERM::_all_inputs) != crypto::PERM::_all_inputs) { PNV_INPUT_HEADER payload = (PNV_INPUT_HEADER)input_data.data(); // Check permission switch (util::endian::little(payload->magic)) { case MULTI_CONTROLLER_MAGIC_GEN5: case SS_CONTROLLER_ARRIVAL_MAGIC: case SS_CONTROLLER_TOUCH_MAGIC: case SS_CONTROLLER_MOTION_MAGIC: case SS_CONTROLLER_BATTERY_MAGIC: if (!(permission & crypto::PERM::input_controller)) { return; } else { break; } case MOUSE_MOVE_REL_MAGIC_GEN5: case MOUSE_MOVE_ABS_MAGIC: case MOUSE_BUTTON_DOWN_EVENT_MAGIC_GEN5: case MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5: case SCROLL_MAGIC_GEN5: case SS_HSCROLL_MAGIC: if (!(permission & crypto::PERM::input_mouse)) { return; } else { break; } case KEY_DOWN_EVENT_MAGIC: case KEY_UP_EVENT_MAGIC: case UTF8_TEXT_EVENT_MAGIC: if (!(permission & crypto::PERM::input_kbd)) { return; } else { break; } case SS_TOUCH_MAGIC: if (!(permission & crypto::PERM::input_touch)) { return; } else { break; } case SS_PEN_MAGIC: if (!(permission & crypto::PERM::input_pen)) { return; } else { break; } default: // Unknown input event return; } } { std::lock_guard lg(input->input_queue_lock); input->input_queue.push_back(std::move(input_data)); } task_pool.push(passthrough_next_message, input); } void reset(std::shared_ptr &input) { task_pool.cancel(key_press_repeat_id); task_pool.cancel(input->mouse_left_button_timeout); // Ensure input is synchronous, by using the task_pool task_pool.push([]() { for (int x = 0; x < mouse_press.size(); ++x) { if (mouse_press[x]) { platf::button_mouse(platf_input, x, true); mouse_press[x] = false; } } for (auto &kp : key_press) { if (!kp.second) { // already released continue; } platf::keyboard_update(platf_input, vk_from_kpid(kp.first) & 0x00FF, true, flags_from_kpid(kp.first)); key_press[kp.first] = false; } }); } class deinit_t: public platf::deinit_t { public: ~deinit_t() override { platf_input.reset(); } }; [[nodiscard]] std::unique_ptr init() { platf_input = platf::input(); return std::make_unique(); } bool probe_gamepads() { auto input = static_cast(platf_input.get()); const auto gamepads = platf::supported_gamepads(input); for (auto &gamepad : gamepads) { if (gamepad.is_enabled && gamepad.name != "auto") { return false; } } return true; } std::shared_ptr alloc(safe::mail_t mail) { auto input = std::make_shared( mail->event(mail::touch_port), mail->queue(mail::gamepad_feedback) ); // Workaround to ensure new frames will be captured when a client connects task_pool.pushDelayed([]() { platf::move_mouse(platf_input, 1, 1); platf::move_mouse(platf_input, -1, -1); }, 100ms); return input; } } // namespace input ================================================ FILE: src/input.h ================================================ /** * @file src/input.h * @brief Declarations for gamepad, keyboard, and mouse input handling. */ #pragma once // standard includes #include // local includes #include "platform/common.h" #include "thread_safe.h" #include "crypto.h" namespace input { struct input_t; void print(void *input); void reset(std::shared_ptr &input); void passthrough(std::shared_ptr &input, std::vector &&input_data, const crypto::PERM& permission); [[nodiscard]] std::unique_ptr init(); bool probe_gamepads(); std::shared_ptr alloc(safe::mail_t mail); struct touch_port_t: public platf::touch_port_t { int env_width, env_height; // Offset x and y coordinates of the client float client_offsetX, client_offsetY; float scalar_inv; explicit operator bool() const { return width != 0 && height != 0 && env_width != 0 && env_height != 0; } }; /** * @brief Scale the ellipse axes according to the provided size. * @param val The major and minor axis pair. * @param rotation The rotation value from the touch/pen event. * @param scalar The scalar cartesian coordinate pair. * @return The major and minor axis pair. */ std::pair scale_client_contact_area(const std::pair &val, uint16_t rotation, const std::pair &scalar); } // namespace input ================================================ FILE: src/logging.cpp ================================================ /** * @file src/logging.cpp * @brief Definitions for logging related functions. */ // standard includes #include #include #include #include // lib includes #include #include #include #include #include #include #include // local includes #include "logging.h" // conditional includes #ifdef __ANDROID__ #include #else #include #endif extern "C" { #include } using namespace std::literals; namespace bl = boost::log; boost::shared_ptr> sink; bl::sources::severity_logger verbose(0); // Dominating output bl::sources::severity_logger debug(1); // Follow what is happening bl::sources::severity_logger info(2); // Should be informed about bl::sources::severity_logger warning(3); // Strange events bl::sources::severity_logger error(4); // Recoverable errors bl::sources::severity_logger fatal(5); // Unrecoverable errors #ifdef SUNSHINE_TESTS bl::sources::severity_logger tests(10); // Automatic tests output #endif BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", int) namespace logging { deinit_t::~deinit_t() { deinit(); } void deinit() { log_flush(); bl::core::get()->remove_sink(sink); sink.reset(); } void formatter(const boost::log::record_view &view, boost::log::formatting_ostream &os) { constexpr const char *message = "Message"; constexpr const char *severity = "Severity"; auto log_level = view.attribute_values()[severity].extract().get(); std::string_view log_type; switch (log_level) { case 0: log_type = "Verbose: "sv; break; case 1: log_type = "Debug: "sv; break; case 2: log_type = "Info: "sv; break; case 3: log_type = "Warning: "sv; break; case 4: log_type = "Error: "sv; break; case 5: log_type = "Fatal: "sv; break; #ifdef SUNSHINE_TESTS case 10: log_type = "Tests: "sv; break; #endif }; auto now = std::chrono::system_clock::now(); auto ms = std::chrono::duration_cast( now - std::chrono::time_point_cast(now) ); auto t = std::chrono::system_clock::to_time_t(now); auto lt = *std::localtime(&t); os << "["sv << std::put_time(<, "%Y-%m-%d %H:%M:%S.") << boost::format("%03u") % ms.count() << "]: "sv << log_type << view.attribute_values()[message].extract(); } #ifdef __ANDROID__ namespace sinks = boost::log::sinks; namespace expr = boost::log::expressions; void android_log(const std::string &message, int severity) { android_LogPriority android_priority; switch (severity) { case 0: android_priority = ANDROID_LOG_VERBOSE; break; case 1: android_priority = ANDROID_LOG_DEBUG; break; case 2: android_priority = ANDROID_LOG_INFO; break; case 3: android_priority = ANDROID_LOG_WARN; break; case 4: android_priority = ANDROID_LOG_ERROR; break; case 5: android_priority = ANDROID_LOG_FATAL; break; default: android_priority = ANDROID_LOG_UNKNOWN; break; } __android_log_print(android_priority, "Sunshine", "%s", message.c_str()); } // custom sink backend for android struct android_sink_backend: public sinks::basic_sink_backend { void consume(const bl::record_view &rec) { int log_sev = rec[severity].get(); const std::string log_msg = rec[expr::smessage].get(); // log to android android_log(log_msg, log_sev); } }; #endif [[nodiscard]] std::unique_ptr init(int min_log_level, const std::string &log_file) { if (sink) { // Deinitialize the logging system before reinitializing it. This can probably only ever be hit in tests. deinit(); } // Check if the log file exists and handle backup std::string backup_log_file = log_file + ".backup"; if (std::filesystem::exists(log_file)) { try { // If the backup file exists, remove it if (std::filesystem::exists(backup_log_file)) { std::filesystem::remove(backup_log_file); } // Rename the current log file to the backup name std::filesystem::rename(log_file, backup_log_file); } catch (std::exception& e) { std::cout << "Failed to rotate log file: " << e.what() << std::endl; } } #ifndef __ANDROID__ setup_av_logging(min_log_level); setup_libdisplaydevice_logging(min_log_level); #endif sink = boost::make_shared(); #ifndef SUNSHINE_TESTS boost::shared_ptr stream {&std::cout, boost::null_deleter()}; sink->locked_backend()->add_stream(stream); #endif sink->locked_backend()->add_stream(boost::make_shared(log_file)); sink->set_filter(severity >= min_log_level); sink->set_formatter(&formatter); // Flush after each log record to ensure log file contents on disk isn't stale. // This is particularly important when running from a Windows service. sink->locked_backend()->auto_flush(true); bl::core::get()->add_sink(sink); #ifdef __ANDROID__ auto android_sink = boost::make_shared>(); bl::core::get()->add_sink(android_sink); #endif return std::make_unique(); } #ifndef __ANDROID__ void setup_av_logging(int min_log_level) { if (min_log_level >= 1) { av_log_set_level(AV_LOG_QUIET); } else { av_log_set_level(AV_LOG_DEBUG); } av_log_set_callback([](void *ptr, int level, const char *fmt, va_list vl) { static int print_prefix = 1; char buffer[1024]; av_log_format_line(ptr, level, fmt, vl, buffer, sizeof(buffer), &print_prefix); if (level <= AV_LOG_ERROR) { // We print AV_LOG_FATAL at the error level. FFmpeg prints things as fatal that // are expected in some cases, such as lack of codec support or similar things. BOOST_LOG(error) << buffer; } else if (level <= AV_LOG_WARNING) { BOOST_LOG(warning) << buffer; } else if (level <= AV_LOG_INFO) { BOOST_LOG(info) << buffer; } else if (level <= AV_LOG_VERBOSE) { // AV_LOG_VERBOSE is less verbose than AV_LOG_DEBUG BOOST_LOG(debug) << buffer; } else { BOOST_LOG(verbose) << buffer; } }); } void setup_libdisplaydevice_logging(int min_log_level) { constexpr int min_level {static_cast(display_device::Logger::LogLevel::verbose)}; constexpr int max_level {static_cast(display_device::Logger::LogLevel::fatal)}; const auto log_level {static_cast(std::min(std::max(min_level, min_log_level), max_level))}; display_device::Logger::get().setLogLevel(log_level); display_device::Logger::get().setCustomCallback([](const display_device::Logger::LogLevel level, const std::string &message) { switch (level) { case display_device::Logger::LogLevel::verbose: BOOST_LOG(verbose) << message; break; case display_device::Logger::LogLevel::debug: BOOST_LOG(debug) << message; break; case display_device::Logger::LogLevel::info: BOOST_LOG(info) << message; break; case display_device::Logger::LogLevel::warning: BOOST_LOG(warning) << message; break; case display_device::Logger::LogLevel::error: BOOST_LOG(error) << message; break; case display_device::Logger::LogLevel::fatal: BOOST_LOG(fatal) << message; break; } }); } #endif void log_flush() { if (sink) { sink->flush(); } } void print_help(const char *name) { std::cout << "Usage: "sv << name << " [options] [/path/to/configuration_file] [--cmd]"sv << std::endl << " Any configurable option can be overwritten with: \"name=value\""sv << std::endl << std::endl << " Note: The configuration will be created if it doesn't exist."sv << std::endl << std::endl << " --help | print help"sv << std::endl << " --creds username password | set user credentials for the Web manager"sv << std::endl << " --version | print the version of sunshine"sv << std::endl << std::endl << " flags"sv << std::endl << " -0 | Read PIN from stdin"sv << std::endl << " -1 | Do not load previously saved state and do retain any state after shutdown"sv << std::endl << " | Effectively starting as if for the first time without overwriting any pairings with your devices"sv << std::endl << " -2 | Force replacement of headers in video stream"sv << std::endl << " -p | Enable/Disable UPnP"sv << std::endl << std::endl; } std::string bracket(const std::string &input) { return "["s + input + "]"s; } std::wstring bracket(const std::wstring &input) { return L"["s + input + L"]"s; } } // namespace logging ================================================ FILE: src/logging.h ================================================ /** * @file src/logging.h * @brief Declarations for logging related functions. */ #pragma once // lib includes #include #include using text_sink = boost::log::sinks::asynchronous_sink; extern boost::log::sources::severity_logger verbose; extern boost::log::sources::severity_logger debug; extern boost::log::sources::severity_logger info; extern boost::log::sources::severity_logger warning; extern boost::log::sources::severity_logger error; extern boost::log::sources::severity_logger fatal; #ifdef SUNSHINE_TESTS extern boost::log::sources::severity_logger tests; #endif #include "config.h" #include "stat_trackers.h" /** * @brief Handles the initialization and deinitialization of the logging system. */ namespace logging { class deinit_t { public: /** * @brief A destructor that restores the initial state. */ ~deinit_t(); }; /** * @brief Deinitialize the logging system. * @examples * deinit(); * @examples_end */ void deinit(); void formatter(const boost::log::record_view &view, boost::log::formatting_ostream &os); /** * @brief Initialize the logging system. * @param min_log_level The minimum log level to output. * @param log_file The log file to write to. * @return An object that will deinitialize the logging system when it goes out of scope. * @examples * log_init(2, "sunshine.log"); * @examples_end */ [[nodiscard]] std::unique_ptr init(int min_log_level, const std::string &log_file); /** * @brief Setup AV logging. * @param min_log_level The log level. */ void setup_av_logging(int min_log_level); /** * @brief Setup logging for libdisplaydevice. * @param min_log_level The log level. */ void setup_libdisplaydevice_logging(int min_log_level); /** * @brief Flush the log. * @examples * log_flush(); * @examples_end */ void log_flush(); /** * @brief Print help to stdout. * @param name The name of the program. * @examples * print_help("sunshine"); * @examples_end */ void print_help(const char *name); /** * @brief A helper class for tracking and logging numerical values across a period of time * @examples * min_max_avg_periodic_logger logger(debug, "Test time value", "ms", 5s); * logger.collect_and_log(1); * // ... * logger.collect_and_log(2); * // after 5 seconds * logger.collect_and_log(3); * // In the log: * // [2024:01:01:12:00:00]: Debug: Test time value (min/max/avg): 1ms/3ms/2.00ms * @examples_end */ template class min_max_avg_periodic_logger { public: min_max_avg_periodic_logger(boost::log::sources::severity_logger &severity, std::string_view message, std::string_view units, std::chrono::seconds interval_in_seconds = std::chrono::seconds(20)): severity(severity), message(message), units(units), interval(interval_in_seconds), enabled(config::sunshine.min_log_level <= severity.default_severity()) { } void collect_and_log(const T &value) { if (enabled) { auto print_info = [&](const T &min_value, const T &max_value, double avg_value) { auto f = stat_trackers::two_digits_after_decimal(); if constexpr (std::is_floating_point_v) { BOOST_LOG(severity.get()) << message << " (min/max/avg): " << f % min_value << units << "/" << f % max_value << units << "/" << f % avg_value << units; } else { BOOST_LOG(severity.get()) << message << " (min/max/avg): " << min_value << units << "/" << max_value << units << "/" << f % avg_value << units; } }; tracker.collect_and_callback_on_interval(value, print_info, interval); } } void collect_and_log(std::function func) { if (enabled) { collect_and_log(func()); } } void reset() { if (enabled) { tracker.reset(); } } bool is_enabled() const { return enabled; } private: std::reference_wrapper> severity; std::string message; std::string units; std::chrono::seconds interval; bool enabled; stat_trackers::min_max_avg_tracker tracker; }; /** * @brief A helper class for tracking and logging short time intervals across a period of time * @examples * time_delta_periodic_logger logger(debug, "Test duration", 5s); * logger.first_point_now(); * // ... * logger.second_point_now_and_log(); * // after 5 seconds * logger.first_point_now(); * // ... * logger.second_point_now_and_log(); * // In the log: * // [2024:01:01:12:00:00]: Debug: Test duration (min/max/avg): 1.23ms/3.21ms/2.31ms * @examples_end */ class time_delta_periodic_logger { public: time_delta_periodic_logger(boost::log::sources::severity_logger &severity, std::string_view message, std::chrono::seconds interval_in_seconds = std::chrono::seconds(20)): logger(severity, message, "ms", interval_in_seconds) { } void first_point(const std::chrono::steady_clock::time_point &point) { if (logger.is_enabled()) { point1 = point; } } void first_point_now() { if (logger.is_enabled()) { first_point(std::chrono::steady_clock::now()); } } void second_point_and_log(const std::chrono::steady_clock::time_point &point) { if (logger.is_enabled()) { logger.collect_and_log(std::chrono::duration(point - point1).count()); } } void second_point_now_and_log() { if (logger.is_enabled()) { second_point_and_log(std::chrono::steady_clock::now()); } } void reset() { if (logger.is_enabled()) { logger.reset(); } } bool is_enabled() const { return logger.is_enabled(); } private: std::chrono::steady_clock::time_point point1 = std::chrono::steady_clock::now(); min_max_avg_periodic_logger logger; }; /** * @brief Enclose string in square brackets. * @param input Input string. * @return Enclosed string. */ std::string bracket(const std::string &input); /** * @brief Enclose string in square brackets. * @param input Input string. * @return Enclosed string. */ std::wstring bracket(const std::wstring &input); } // namespace logging ================================================ FILE: src/main.cpp ================================================ /** * @file src/main.cpp * @brief Definitions for the main entry point for Sunshine. */ // standard includes #include #include #include #include // local includes #include "confighttp.h" #include "display_device.h" #include "entry_handler.h" #include "globals.h" #include "httpcommon.h" #include "logging.h" #include "main.h" #include "nvhttp.h" #include "process.h" #include "system_tray.h" #include "upnp.h" #include "uuid.h" #include "video.h" #ifdef _WIN32 #include "platform/windows/misc.h" #include "platform/windows/virtual_display.h" #endif #define PROBE_DISPLAY_UUID "38F72B96-B00C-4F21-8B6C-E1BFF1602B0E" extern "C" { #include "rswrapper.h" } using namespace std::literals; std::map> signal_handlers; void on_signal_forwarder(int sig) { signal_handlers.at(sig)(); } template void on_signal(int sig, FN &&fn) { signal_handlers.emplace(sig, std::forward(fn)); std::signal(sig, on_signal_forwarder); } std::map> cmd_to_func { {"creds"sv, [](const char *name, int argc, char **argv) { return args::creds(name, argc, argv); }}, {"help"sv, [](const char *name, int argc, char **argv) { return args::help(name); }}, {"version"sv, [](const char *name, int argc, char **argv) { return args::version(); }}, #ifdef _WIN32 {"restore-nvprefs-undo"sv, [](const char *name, int argc, char **argv) { return args::restore_nvprefs_undo(); }}, #endif }; #ifdef _WIN32 LRESULT CALLBACK SessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_CLOSE: DestroyWindow(hwnd); return 0; case WM_DESTROY: PostQuitMessage(0); return 0; case WM_ENDSESSION: { // Terminate ourselves with a blocking exit call std::cout << "Received WM_ENDSESSION"sv << std::endl; lifetime::exit_sunshine(0, false); return 0; } default: return DefWindowProc(hwnd, uMsg, wParam, lParam); } } WINAPI BOOL ConsoleCtrlHandler(DWORD type) { if (type == CTRL_CLOSE_EVENT) { BOOST_LOG(info) << "Console closed handler called"; lifetime::exit_sunshine(0, false); } return FALSE; } #endif #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 constexpr bool tray_is_enabled = true; #else constexpr bool tray_is_enabled = false; #endif void mainThreadLoop(const std::shared_ptr> &shutdown_event) { bool run_loop = false; // Conditions that would require the main thread event loop #ifndef _WIN32 run_loop = tray_is_enabled; // On Windows, tray runs in separate thread, so no main loop needed for tray #endif if (!run_loop) { BOOST_LOG(info) << "No main thread features enabled, skipping event loop"sv; return; } // Main thread event loop BOOST_LOG(info) << "Starting main loop"sv; while (true) { if (shutdown_event->peek()) { BOOST_LOG(info) << "Shutdown event detected, breaking main loop"sv; if (tray_is_enabled && config::sunshine.system_tray) { system_tray::end_tray(); } break; } if (tray_is_enabled) { system_tray::process_tray_events(); } // Sleep to avoid busy waiting std::this_thread::sleep_for(std::chrono::milliseconds(50)); } } int main(int argc, char *argv[]) { lifetime::argv = argv; task_pool_util::TaskPool::task_id_t force_shutdown = nullptr; #ifdef _WIN32 // Avoid searching the PATH in case a user has configured their system insecurely // by placing a user-writable directory in the system-wide PATH variable. SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); setlocale(LC_ALL, "C"); #endif #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" // Use UTF-8 conversion for the default C++ locale (used by boost::log) std::locale utf8_locale(std::locale(), new std::codecvt_utf8); std::locale::global(utf8_locale); boost::filesystem::path::imbue(utf8_locale); #pragma GCC diagnostic pop mail::man = std::make_shared(); // parse config file if (config::parse(argc, argv)) { return 0; } auto log_deinit_guard = logging::init(config::sunshine.min_log_level, config::sunshine.log_file); if (!log_deinit_guard) { BOOST_LOG(error) << "Logging failed to initialize"sv; } // logging can begin at this point // if anything is logged prior to this point, it will appear in stdout, but not in the log viewer in the UI // the version should be printed to the log before anything else BOOST_LOG(info) << PROJECT_NAME << " version: " << PROJECT_VERSION << " commit: " << PROJECT_VERSION_COMMIT; // Log publisher metadata log_publisher_data(); // Log modified_config_settings for (auto &[name, val] : config::modified_config_settings) { BOOST_LOG(info) << "config: '"sv << name << "' = "sv << val; } config::modified_config_settings.clear(); if (!config::sunshine.cmd.name.empty()) { auto fn = cmd_to_func.find(config::sunshine.cmd.name); if (fn == std::end(cmd_to_func)) { BOOST_LOG(fatal) << "Unknown command: "sv << config::sunshine.cmd.name; BOOST_LOG(info) << "Possible commands:"sv; for (auto &[key, _] : cmd_to_func) { BOOST_LOG(info) << '\t' << key; } return 7; } return fn->second(argv[0], config::sunshine.cmd.argc, config::sunshine.cmd.argv); } // Adding guard here first as it also performs recovery after crash, // otherwise people could theoretically end up without display output. // It also should be destroyed before forced shutdown to expedite the cleanup. auto display_device_deinit_guard = display_device::init(platf::appdata() / "display_device.state", config::video); if (!display_device_deinit_guard) { BOOST_LOG(error) << "Display device session failed to initialize"sv; } #ifdef _WIN32 // Modify relevant NVIDIA control panel settings if the system has corresponding gpu if (nvprefs_instance.load()) { // Restore global settings to the undo file left by improper termination of sunshine.exe nvprefs_instance.restore_from_and_delete_undo_file_if_exists(); // Modify application settings for sunshine.exe nvprefs_instance.modify_application_profile(); // Modify global settings, undo file is produced in the process to restore after improper termination nvprefs_instance.modify_global_profile(); // Unload dynamic library to survive driver re-installation nvprefs_instance.unload(); } // Wait as long as possible to terminate Sunshine.exe during logoff/shutdown SetProcessShutdownParameters(0x100, SHUTDOWN_NORETRY); // We must create a hidden window to receive shutdown notifications since we load gdi32.dll std::promise session_monitor_hwnd_promise; auto session_monitor_hwnd_future = session_monitor_hwnd_promise.get_future(); std::promise session_monitor_join_thread_promise; auto session_monitor_join_thread_future = session_monitor_join_thread_promise.get_future(); std::thread session_monitor_thread([&]() { session_monitor_join_thread_promise.set_value_at_thread_exit(); WNDCLASSA wnd_class {}; wnd_class.lpszClassName = "SunshineSessionMonitorClass"; wnd_class.lpfnWndProc = SessionMonitorWindowProc; if (!RegisterClassA(&wnd_class)) { session_monitor_hwnd_promise.set_value(nullptr); BOOST_LOG(error) << "Failed to register session monitor window class"sv << std::endl; return; } auto wnd = CreateWindowExA( 0, wnd_class.lpszClassName, "Sunshine Session Monitor Window", 0, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, nullptr, nullptr ); session_monitor_hwnd_promise.set_value(wnd); if (!wnd) { BOOST_LOG(error) << "Failed to create session monitor window"sv << std::endl; return; } ShowWindow(wnd, SW_HIDE); // Run the message loop for our window MSG msg {}; while (GetMessage(&msg, nullptr, 0, 0) > 0) { TranslateMessage(&msg); DispatchMessage(&msg); } }); auto session_monitor_join_thread_guard = util::fail_guard([&]() { if (session_monitor_hwnd_future.wait_for(1s) == std::future_status::ready) { if (HWND session_monitor_hwnd = session_monitor_hwnd_future.get()) { PostMessage(session_monitor_hwnd, WM_CLOSE, 0, 0); } if (session_monitor_join_thread_future.wait_for(1s) == std::future_status::ready) { session_monitor_thread.join(); return; } else { BOOST_LOG(warning) << "session_monitor_join_thread_future reached timeout"; } } else { BOOST_LOG(warning) << "session_monitor_hwnd_future reached timeout"; } session_monitor_thread.detach(); }); #endif task_pool.start(1); // Create signal handler after logging has been initialized auto shutdown_event = mail::man->event(mail::shutdown); on_signal(SIGINT, [&force_shutdown, &display_device_deinit_guard, shutdown_event]() { BOOST_LOG(info) << "Interrupt handler called"sv; auto task = []() { BOOST_LOG(fatal) << "10 seconds passed, yet Sunshine's still running: Forcing shutdown"sv; logging::log_flush(); lifetime::debug_trap(); }; proc::proc.terminate(); force_shutdown = task_pool.pushDelayed(task, 10s).task_id; shutdown_event->raise(true); display_device_deinit_guard = nullptr; }); on_signal(SIGTERM, [&force_shutdown, &display_device_deinit_guard, shutdown_event]() { BOOST_LOG(info) << "Terminate handler called"sv; auto task = []() { BOOST_LOG(fatal) << "10 seconds passed, yet Sunshine's still running: Forcing shutdown"sv; logging::log_flush(); lifetime::debug_trap(); }; force_shutdown = task_pool.pushDelayed(task, 10s).task_id; shutdown_event->raise(true); display_device_deinit_guard = nullptr; }); #ifdef _WIN32 // Terminate gracefully on Windows when console window is closed SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE); #endif proc::refresh(config::stream.file_apps); // If any of the following fail, we log an error and continue event though sunshine will not function correctly. // This allows access to the UI to fix configuration problems or view the logs. auto platf_deinit_guard = platf::init(); if (!platf_deinit_guard) { BOOST_LOG(error) << "Platform failed to initialize"sv; } auto proc_deinit_guard = proc::init(); if (!proc_deinit_guard) { BOOST_LOG(error) << "Proc failed to initialize"sv; } reed_solomon_init(); auto input_deinit_guard = input::init(); if (input::probe_gamepads()) { BOOST_LOG(warning) << "No gamepad input is available"sv; } if (video::probe_encoders()) { #ifdef _WIN32 bool allow_probing = video::allow_encoder_probing(); // Create a temporary virtual display for encoder capability probing if (proc::vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK) { std::string probe_uuid_str = PROBE_DISPLAY_UUID; auto probe_uuid = uuid_util::uuid_t::parse(probe_uuid_str); auto* probe_guid = (GUID*)(void*)&probe_uuid; BOOST_LOG(info) << "Creating a temporary virtual display to probe for encoders..."sv; if (!config::video.adapter_name.empty()) { VDISPLAY::setRenderAdapterByName(platf::from_utf8(config::video.adapter_name)); } VDISPLAY::createVirtualDisplay( probe_uuid_str.c_str(), "Probe", 800, 600, 60, *probe_guid ); std::this_thread::sleep_for(500ms); // Probe again anyways if (video::probe_encoders()) { if (allow_probing) { BOOST_LOG(error) << "Video failed to find working encoder: allow probing but failed"sv; } else { BOOST_LOG(error) << "Video failed to find working encoder even after attempted with a virtual display"sv; } } VDISPLAY::removeVirtualDisplay(*probe_guid); } else if (!allow_probing) { BOOST_LOG(error) << "Video failed to find working encoder: probe failed and virtual display driver isn't initialized"sv; } #else BOOST_LOG(error) << "Video failed to find working encoder: probing failed."sv; #endif } if (http::init()) { BOOST_LOG(fatal) << "HTTP interface failed to initialize"sv; #ifdef _WIN32 BOOST_LOG(fatal) << "To relaunch Apollo successfully, use the shortcut in the Start Menu. Do not run sunshine.exe manually."sv; std::this_thread::sleep_for(10s); #endif return -1; } std::unique_ptr mDNS; auto sync_mDNS = std::async(std::launch::async, [&mDNS]() { if (config::sunshine.enable_discovery) { mDNS = platf::publish::start(); } }); std::unique_ptr upnp_unmap; auto sync_upnp = std::async(std::launch::async, [&upnp_unmap]() { upnp_unmap = upnp::start(); }); // FIXME: Temporary workaround: Simple-Web_server needs to be updated or replaced if (shutdown_event->peek()) { return lifetime::desired_exit_code; } std::thread httpThread {nvhttp::start}; std::thread configThread {confighttp::start}; std::thread rtspThread {rtsp_stream::start}; #ifdef _WIN32 // If we're using the default port and GameStream is enabled, warn the user if (config::sunshine.port == 47989 && is_gamestream_enabled()) { BOOST_LOG(fatal) << "GameStream is still enabled in GeForce Experience! This *will* cause streaming problems with Apollo!"sv; BOOST_LOG(fatal) << "Disable GameStream on the SHIELD tab in GeForce Experience or change the Port setting on the Advanced tab in the Apollo Web UI."sv; } #endif if (tray_is_enabled && config::sunshine.system_tray) { BOOST_LOG(info) << "Starting system tray"sv; #ifdef _WIN32 // TODO: Windows has a weird bug where when running as a service and on the first Windows boot, // he tray icon would not appear even though Sunshine is running correctly otherwise. // Restarting the service would allow the icon to appear normally. // For now we will keep the Windows tray icon on a separate thread. // Ideally, we would run the system tray on the main thread for all platforms. system_tray::init_tray_threaded(); #else system_tray::init_tray(); #endif } mainThreadLoop(shutdown_event); // Wait for shutdown, this is not necessary when we're using the main event loop shutdown_event->view(); httpThread.join(); configThread.join(); rtspThread.join(); task_pool.stop(); task_pool.join(); #ifdef _WIN32 // Restore global NVIDIA control panel settings if (nvprefs_instance.owning_undo_file() && nvprefs_instance.load()) { nvprefs_instance.restore_global_profile(); nvprefs_instance.unload(); } // Stop the threaded tray if it was started if (tray_is_enabled && config::sunshine.system_tray) { system_tray::end_tray_threaded(); } #endif return lifetime::desired_exit_code; } ================================================ FILE: src/main.h ================================================ /** * @file src/main.h * @brief Declarations for the main entry point for Sunshine. */ #pragma once /** * @brief Main application entry point. * @param argc The number of arguments. * @param argv The arguments. * @examples * main(1, const char* args[] = {"sunshine", nullptr}); * @examples_end */ int main(int argc, char *argv[]); ================================================ FILE: src/move_by_copy.h ================================================ /** * @file src/move_by_copy.h * @brief Declarations for the MoveByCopy utility class. */ #pragma once // standard includes #include /** * @brief Contains utilities for moving objects by copying them. */ namespace move_by_copy_util { /** * When a copy is made, it moves the object * This allows you to move an object when a move can't be done. */ template class MoveByCopy { public: typedef T move_type; private: move_type _to_move; public: explicit MoveByCopy(move_type &&to_move): _to_move(std::move(to_move)) { } MoveByCopy(MoveByCopy &&other) = default; MoveByCopy(const MoveByCopy &other) { *this = other; } MoveByCopy &operator=(MoveByCopy &&other) = default; MoveByCopy &operator=(const MoveByCopy &other) { this->_to_move = std::move(const_cast(other)._to_move); return *this; } operator move_type() { return std::move(_to_move); } }; template MoveByCopy cmove(T &movable) { return MoveByCopy(std::move(movable)); } // Do NOT use this unless you are absolutely certain the object to be moved is no longer used by the caller template MoveByCopy const_cmove(const T &movable) { return MoveByCopy(std::move(const_cast(movable))); } } // namespace move_by_copy_util ================================================ FILE: src/network.cpp ================================================ /** * @file src/network.cpp * @brief Definitions for networking related functions. */ // standard includes #include #include // local includes #include "config.h" #include "logging.h" #include "network.h" #include "utility.h" using namespace std::literals; namespace ip = boost::asio::ip; namespace net { std::vector pc_ips_v4 { ip::make_network_v4("127.0.0.0/8"sv), }; std::vector lan_ips_v4 { ip::make_network_v4("192.168.0.0/16"sv), ip::make_network_v4("172.16.0.0/12"sv), ip::make_network_v4("10.0.0.0/8"sv), ip::make_network_v4("100.64.0.0/10"sv), ip::make_network_v4("169.254.0.0/16"sv), }; std::vector pc_ips_v6 { ip::make_network_v6("::1/128"sv), }; std::vector lan_ips_v6 { ip::make_network_v6("fc00::/7"sv), ip::make_network_v6("fe80::/64"sv), }; net_e from_enum_string(const std::string_view &view) { if (view == "wan") { return WAN; } if (view == "lan") { return LAN; } return PC; } net_e from_address(const std::string_view &view) { auto addr = normalize_address(ip::make_address(view)); if (addr.is_v6()) { for (auto &range : pc_ips_v6) { if (range.hosts().find(addr.to_v6()) != range.hosts().end()) { return PC; } } for (auto &range : lan_ips_v6) { if (range.hosts().find(addr.to_v6()) != range.hosts().end()) { return LAN; } } } else { for (auto &range : pc_ips_v4) { if (range.hosts().find(addr.to_v4()) != range.hosts().end()) { return PC; } } for (auto &range : lan_ips_v4) { if (range.hosts().find(addr.to_v4()) != range.hosts().end()) { return LAN; } } } return WAN; } std::string_view to_enum_string(net_e net) { switch (net) { case PC: return "pc"sv; case LAN: return "lan"sv; case WAN: return "wan"sv; } // avoid warning return "wan"sv; } af_e af_from_enum_string(const std::string_view &view) { if (view == "ipv4") { return IPV4; } if (view == "both") { return BOTH; } // avoid warning return BOTH; } std::string_view af_to_any_address_string(af_e af) { switch (af) { case IPV4: return "0.0.0.0"sv; case BOTH: return "::"sv; } // avoid warning return "::"sv; } boost::asio::ip::address normalize_address(boost::asio::ip::address address) { // Convert IPv6-mapped IPv4 addresses into regular IPv4 addresses if (address.is_v6()) { auto v6 = address.to_v6(); if (v6.is_v4_mapped()) { return boost::asio::ip::make_address_v4(boost::asio::ip::v4_mapped, v6); } } return address; } std::string addr_to_normalized_string(boost::asio::ip::address address) { return normalize_address(address).to_string(); } std::string addr_to_url_escaped_string(boost::asio::ip::address address) { address = normalize_address(address); if (address.is_v6()) { std::stringstream ss; ss << '[' << address.to_string() << ']'; return ss.str(); } else { return address.to_string(); } } int encryption_mode_for_address(boost::asio::ip::address address) { auto nettype = net::from_address(address.to_string()); if (nettype == net::net_e::PC || nettype == net::net_e::LAN) { return config::stream.lan_encryption_mode; } else { return config::stream.wan_encryption_mode; } } host_t host_create(af_e af, ENetAddress &addr, std::uint16_t port) { static std::once_flag enet_init_flag; std::call_once(enet_init_flag, []() { enet_initialize(); }); auto any_addr = net::af_to_any_address_string(af); enet_address_set_host(&addr, any_addr.data()); enet_address_set_port(&addr, port); // Maximum of 128 clients, which should be enough for anyone auto host = host_t {enet_host_create(af == IPV4 ? AF_INET : AF_INET6, &addr, 128, 0, 0, 0)}; // Enable opportunistic QoS tagging (automatically disables if the network appears to drop tagged packets) enet_socket_set_option(host->socket, ENET_SOCKOPT_QOS, 1); return host; } void free_host(ENetHost *host) { std::for_each(host->peers, host->peers + host->peerCount, [](ENetPeer &peer_ref) { ENetPeer *peer = &peer_ref; if (peer) { enet_peer_disconnect_now(peer, 0); } }); enet_host_destroy(host); } std::uint16_t map_port(int port) { // calculate the port from the config port auto mapped_port = (std::uint16_t) ((int) config::sunshine.port + port); // Ensure port is in the range of 1024-65535 if (mapped_port < 1024 || mapped_port > 65535) { BOOST_LOG(warning) << "Port out of range: "sv << mapped_port; } return mapped_port; } /** * @brief Returns a string for use as the instance name for mDNS. * @param hostname The hostname to use for instance name generation. * @return Hostname-based instance name or "Sunshine" if hostname is invalid. */ std::string mdns_instance_name(const std::string_view &hostname) { // Start with the unmodified hostname std::string instancename {hostname.data(), hostname.size()}; // Truncate to 63 characters per RFC 6763 section 7.2. if (instancename.size() > 63) { instancename.resize(63); } for (auto i = 0; i < instancename.size(); i++) { // Replace any spaces with dashes if (instancename[i] == ' ') { instancename[i] = '-'; } else if (!std::isalnum(instancename[i]) && instancename[i] != '-') { // Stop at the first invalid character instancename.resize(i); break; } } return !instancename.empty() ? instancename : "Apollo"; } } // namespace net ================================================ FILE: src/network.h ================================================ /** * @file src/network.h * @brief Declarations for networking related functions. */ #pragma once // standard includes #include #include // lib includes #include #include // local includes #include "utility.h" namespace net { void free_host(ENetHost *host); /** * @brief Map a specified port based on the base port. * @param port The port to map as a difference from the base port. * @return The mapped port number. * @examples * std::uint16_t mapped_port = net::map_port(1); * @examples_end * @todo Ensure port is not already in use by another application. */ std::uint16_t map_port(int port); using host_t = util::safe_ptr; using peer_t = ENetPeer *; using packet_t = util::safe_ptr; enum net_e : int { PC, ///< PC LAN, ///< LAN WAN ///< WAN }; enum af_e : int { IPV4, ///< IPv4 only BOTH ///< IPv4 and IPv6 }; net_e from_enum_string(const std::string_view &view); std::string_view to_enum_string(net_e net); net_e from_address(const std::string_view &view); host_t host_create(af_e af, ENetAddress &addr, std::uint16_t port); /** * @brief Get the address family enum value from a string. * @param view The config option value. * @return The address family enum value. */ af_e af_from_enum_string(const std::string_view &view); /** * @brief Get the wildcard binding address for a given address family. * @param af Address family. * @return Normalized address. */ std::string_view af_to_any_address_string(af_e af); /** * @brief Convert an address to a normalized form. * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses. * @param address The address to normalize. * @return Normalized address. */ boost::asio::ip::address normalize_address(boost::asio::ip::address address); /** * @brief Get the given address in normalized string form. * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses. * @param address The address to normalize. * @return Normalized address in string form. */ std::string addr_to_normalized_string(boost::asio::ip::address address); /** * @brief Get the given address in a normalized form for the host portion of a URL. * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses. * @param address The address to normalize and escape. * @return Normalized address in URL-escaped string. */ std::string addr_to_url_escaped_string(boost::asio::ip::address address); /** * @brief Get the encryption mode for the given remote endpoint address. * @param address The address used to look up the desired encryption mode. * @return The WAN or LAN encryption mode, based on the provided address. */ int encryption_mode_for_address(boost::asio::ip::address address); /** * @brief Returns a string for use as the instance name for mDNS. * @param hostname The hostname to use for instance name generation. * @return Hostname-based instance name or "Sunshine" if hostname is invalid. */ std::string mdns_instance_name(const std::string_view &hostname); } // namespace net ================================================ FILE: src/nvenc/nvenc_base.cpp ================================================ /** * @file src/nvenc/nvenc_base.cpp * @brief Definitions for abstract platform-agnostic base of standalone NVENC encoder. */ // this include #include "nvenc_base.h" // standard includes #include // local includes #include "src/config.h" #include "src/logging.h" #include "src/utility.h" #define MAKE_NVENC_VER(major, minor) ((major) | ((minor) << 24)) // Make sure we check backwards compatibility when bumping the Video Codec SDK version // Things to look out for: // - NV_ENC_*_VER definitions where the value inside NVENCAPI_STRUCT_VERSION() was increased // - Incompatible struct changes in nvEncodeAPI.h (fields removed, semantics changed, etc.) // - Test both old and new drivers with all supported codecs #if NVENCAPI_VERSION != MAKE_NVENC_VER(12U, 0U) #error Check and update NVENC code for backwards compatibility! #endif namespace { GUID quality_preset_guid_from_number(unsigned number) { if (number > 7) { number = 7; } switch (number) { case 1: default: return NV_ENC_PRESET_P1_GUID; case 2: return NV_ENC_PRESET_P2_GUID; case 3: return NV_ENC_PRESET_P3_GUID; case 4: return NV_ENC_PRESET_P4_GUID; case 5: return NV_ENC_PRESET_P5_GUID; case 6: return NV_ENC_PRESET_P6_GUID; case 7: return NV_ENC_PRESET_P7_GUID; } }; bool equal_guids(const GUID &guid1, const GUID &guid2) { return std::memcmp(&guid1, &guid2, sizeof(GUID)) == 0; } auto quality_preset_string_from_guid(const GUID &guid) { if (equal_guids(guid, NV_ENC_PRESET_P1_GUID)) { return "P1"; } if (equal_guids(guid, NV_ENC_PRESET_P2_GUID)) { return "P2"; } if (equal_guids(guid, NV_ENC_PRESET_P3_GUID)) { return "P3"; } if (equal_guids(guid, NV_ENC_PRESET_P4_GUID)) { return "P4"; } if (equal_guids(guid, NV_ENC_PRESET_P5_GUID)) { return "P5"; } if (equal_guids(guid, NV_ENC_PRESET_P6_GUID)) { return "P6"; } if (equal_guids(guid, NV_ENC_PRESET_P7_GUID)) { return "P7"; } return "Unknown"; } } // namespace namespace nvenc { nvenc_base::nvenc_base(NV_ENC_DEVICE_TYPE device_type): device_type(device_type) { } nvenc_base::~nvenc_base() { // Use destroy_encoder() instead } bool nvenc_base::create_encoder(const nvenc_config &config, const video::config_t &client_config, const nvenc_colorspace_t &colorspace, NV_ENC_BUFFER_FORMAT buffer_format) { // Pick the minimum NvEncode API version required to support the specified codec // to maximize driver compatibility. AV1 was introduced in SDK v12.0. minimum_api_version = (client_config.videoFormat <= 1) ? MAKE_NVENC_VER(11U, 0U) : MAKE_NVENC_VER(12U, 0U); if (!nvenc && !init_library()) { return false; } if (encoder) { destroy_encoder(); } auto fail_guard = util::fail_guard([this] { destroy_encoder(); }); encoder_params.width = client_config.width; encoder_params.height = client_config.height; encoder_params.buffer_format = buffer_format; encoder_params.rfi = true; NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS session_params = {min_struct_version(NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER)}; session_params.device = device; session_params.deviceType = device_type; session_params.apiVersion = minimum_api_version; if (nvenc_failed(nvenc->nvEncOpenEncodeSessionEx(&session_params, &encoder))) { BOOST_LOG(error) << "NvEnc: NvEncOpenEncodeSessionEx() failed: " << last_nvenc_error_string; return false; } uint32_t encode_guid_count = 0; if (nvenc_failed(nvenc->nvEncGetEncodeGUIDCount(encoder, &encode_guid_count))) { BOOST_LOG(error) << "NvEnc: NvEncGetEncodeGUIDCount() failed: " << last_nvenc_error_string; return false; }; std::vector encode_guids(encode_guid_count); if (nvenc_failed(nvenc->nvEncGetEncodeGUIDs(encoder, encode_guids.data(), encode_guids.size(), &encode_guid_count))) { BOOST_LOG(error) << "NvEnc: NvEncGetEncodeGUIDs() failed: " << last_nvenc_error_string; return false; } NV_ENC_INITIALIZE_PARAMS init_params = {min_struct_version(NV_ENC_INITIALIZE_PARAMS_VER)}; switch (client_config.videoFormat) { case 0: // H.264 init_params.encodeGUID = NV_ENC_CODEC_H264_GUID; break; case 1: // HEVC init_params.encodeGUID = NV_ENC_CODEC_HEVC_GUID; break; case 2: // AV1 init_params.encodeGUID = NV_ENC_CODEC_AV1_GUID; break; default: BOOST_LOG(error) << "NvEnc: unknown video format " << client_config.videoFormat; return false; } { auto search_predicate = [&](const GUID &guid) { return equal_guids(init_params.encodeGUID, guid); }; if (std::find_if(encode_guids.begin(), encode_guids.end(), search_predicate) == encode_guids.end()) { BOOST_LOG(error) << "NvEnc: encoding format is not supported by the gpu"; return false; } } auto get_encoder_cap = [&](NV_ENC_CAPS cap) { NV_ENC_CAPS_PARAM param = {min_struct_version(NV_ENC_CAPS_PARAM_VER), cap}; int value = 0; nvenc->nvEncGetEncodeCaps(encoder, init_params.encodeGUID, ¶m, &value); return value; }; auto buffer_is_10bit = [&]() { return buffer_format == NV_ENC_BUFFER_FORMAT_YUV420_10BIT || buffer_format == NV_ENC_BUFFER_FORMAT_YUV444_10BIT; }; auto buffer_is_yuv444 = [&]() { return buffer_format == NV_ENC_BUFFER_FORMAT_AYUV || buffer_format == NV_ENC_BUFFER_FORMAT_YUV444_10BIT; }; { auto supported_width = get_encoder_cap(NV_ENC_CAPS_WIDTH_MAX); auto supported_height = get_encoder_cap(NV_ENC_CAPS_HEIGHT_MAX); if (encoder_params.width > supported_width || encoder_params.height > supported_height) { BOOST_LOG(error) << "NvEnc: gpu max encode resolution " << supported_width << "x" << supported_height << ", requested " << encoder_params.width << "x" << encoder_params.height; return false; } } if (buffer_is_10bit() && !get_encoder_cap(NV_ENC_CAPS_SUPPORT_10BIT_ENCODE)) { BOOST_LOG(error) << "NvEnc: gpu doesn't support 10-bit encode"; return false; } if (buffer_is_yuv444() && !get_encoder_cap(NV_ENC_CAPS_SUPPORT_YUV444_ENCODE)) { BOOST_LOG(error) << "NvEnc: gpu doesn't support YUV444 encode"; return false; } if (async_event_handle && !get_encoder_cap(NV_ENC_CAPS_ASYNC_ENCODE_SUPPORT)) { BOOST_LOG(warning) << "NvEnc: gpu doesn't support async encode"; async_event_handle = nullptr; } encoder_params.rfi = get_encoder_cap(NV_ENC_CAPS_SUPPORT_REF_PIC_INVALIDATION); init_params.presetGUID = quality_preset_guid_from_number(config.quality_preset); init_params.tuningInfo = NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY; init_params.enablePTD = 1; init_params.enableEncodeAsync = async_event_handle ? 1 : 0; init_params.enableWeightedPrediction = config.weighted_prediction && get_encoder_cap(NV_ENC_CAPS_SUPPORT_WEIGHTED_PREDICTION); init_params.encodeWidth = encoder_params.width; init_params.darWidth = encoder_params.width; init_params.encodeHeight = encoder_params.height; init_params.darHeight = encoder_params.height; init_params.frameRateNum = client_config.framerate; init_params.frameRateDen = 1; NV_ENC_PRESET_CONFIG preset_config = {min_struct_version(NV_ENC_PRESET_CONFIG_VER), {min_struct_version(NV_ENC_CONFIG_VER, 7, 8)}}; if (nvenc_failed(nvenc->nvEncGetEncodePresetConfigEx(encoder, init_params.encodeGUID, init_params.presetGUID, init_params.tuningInfo, &preset_config))) { BOOST_LOG(error) << "NvEnc: NvEncGetEncodePresetConfigEx() failed: " << last_nvenc_error_string; return false; } NV_ENC_CONFIG enc_config = preset_config.presetCfg; enc_config.profileGUID = NV_ENC_CODEC_PROFILE_AUTOSELECT_GUID; enc_config.gopLength = NVENC_INFINITE_GOPLENGTH; enc_config.frameIntervalP = 1; enc_config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_CBR; enc_config.rcParams.zeroReorderDelay = 1; enc_config.rcParams.enableLookahead = 0; enc_config.rcParams.lowDelayKeyFrameScale = 1; enc_config.rcParams.multiPass = config.two_pass == nvenc_two_pass::quarter_resolution ? NV_ENC_TWO_PASS_QUARTER_RESOLUTION : config.two_pass == nvenc_two_pass::full_resolution ? NV_ENC_TWO_PASS_FULL_RESOLUTION : NV_ENC_MULTI_PASS_DISABLED; enc_config.rcParams.enableAQ = config.adaptive_quantization; enc_config.rcParams.averageBitRate = client_config.bitrate * 1000; if (get_encoder_cap(NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE)) { enc_config.rcParams.vbvBufferSize = client_config.bitrate * 1000 / client_config.framerate; if (config.vbv_percentage_increase > 0) { enc_config.rcParams.vbvBufferSize += enc_config.rcParams.vbvBufferSize * config.vbv_percentage_increase / 100; } } auto set_h264_hevc_common_format_config = [&](auto &format_config) { format_config.repeatSPSPPS = 1; format_config.idrPeriod = NVENC_INFINITE_GOPLENGTH; format_config.sliceMode = 3; format_config.sliceModeData = client_config.slicesPerFrame; if (buffer_is_yuv444()) { format_config.chromaFormatIDC = 3; } format_config.enableFillerDataInsertion = config.insert_filler_data; }; auto set_ref_frames = [&](uint32_t &ref_frames_option, NV_ENC_NUM_REF_FRAMES &L0_option, uint32_t ref_frames_default) { if (client_config.numRefFrames > 0) { ref_frames_option = client_config.numRefFrames; } else { ref_frames_option = ref_frames_default; } if (ref_frames_option > 0 && !get_encoder_cap(NV_ENC_CAPS_SUPPORT_MULTIPLE_REF_FRAMES)) { ref_frames_option = 1; encoder_params.rfi = false; } encoder_params.ref_frames_in_dpb = ref_frames_option; // This limits ref frames any frame can use to 1, but allows larger buffer size for fallback if some frames are invalidated through rfi L0_option = NV_ENC_NUM_REF_FRAMES_1; }; auto set_minqp_if_enabled = [&](int value) { if (config.enable_min_qp) { enc_config.rcParams.enableMinQP = 1; enc_config.rcParams.minQP.qpInterP = value; enc_config.rcParams.minQP.qpIntra = value; } }; auto fill_h264_hevc_vui = [&](auto &vui_config) { vui_config.videoSignalTypePresentFlag = 1; vui_config.videoFormat = NV_ENC_VUI_VIDEO_FORMAT_UNSPECIFIED; vui_config.videoFullRangeFlag = colorspace.full_range; vui_config.colourDescriptionPresentFlag = 1; vui_config.colourPrimaries = colorspace.primaries; vui_config.transferCharacteristics = colorspace.tranfer_function; vui_config.colourMatrix = colorspace.matrix; vui_config.chromaSampleLocationFlag = buffer_is_yuv444() ? 0 : 1; vui_config.chromaSampleLocationTop = 0; vui_config.chromaSampleLocationBot = 0; }; switch (client_config.videoFormat) { case 0: { // H.264 enc_config.profileGUID = buffer_is_yuv444() ? NV_ENC_H264_PROFILE_HIGH_444_GUID : NV_ENC_H264_PROFILE_HIGH_GUID; auto &format_config = enc_config.encodeCodecConfig.h264Config; set_h264_hevc_common_format_config(format_config); if (config.h264_cavlc || !get_encoder_cap(NV_ENC_CAPS_SUPPORT_CABAC)) { format_config.entropyCodingMode = NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC; } else { format_config.entropyCodingMode = NV_ENC_H264_ENTROPY_CODING_MODE_CABAC; } set_ref_frames(format_config.maxNumRefFrames, format_config.numRefL0, 5); set_minqp_if_enabled(config.min_qp_h264); fill_h264_hevc_vui(format_config.h264VUIParameters); break; } case 1: { // HEVC auto &format_config = enc_config.encodeCodecConfig.hevcConfig; set_h264_hevc_common_format_config(format_config); if (buffer_is_10bit()) { format_config.pixelBitDepthMinus8 = 2; } set_ref_frames(format_config.maxNumRefFramesInDPB, format_config.numRefL0, 5); set_minqp_if_enabled(config.min_qp_hevc); fill_h264_hevc_vui(format_config.hevcVUIParameters); if (client_config.enableIntraRefresh == 1) { if (get_encoder_cap(NV_ENC_CAPS_SUPPORT_INTRA_REFRESH)) { format_config.enableIntraRefresh = 1; format_config.intraRefreshPeriod = 300; format_config.intraRefreshCnt = 299; if (get_encoder_cap(NV_ENC_CAPS_SINGLE_SLICE_INTRA_REFRESH)) { format_config.singleSliceIntraRefresh = 1; } else { BOOST_LOG(warning) << "NvEnc: Single Slice Intra Refresh not supported"; } } else { BOOST_LOG(error) << "NvEnc: Client asked for intra-refresh but the encoder does not support intra-refresh"; } } break; } case 2: { // AV1 auto &format_config = enc_config.encodeCodecConfig.av1Config; format_config.repeatSeqHdr = 1; format_config.idrPeriod = NVENC_INFINITE_GOPLENGTH; if (buffer_is_yuv444()) { format_config.chromaFormatIDC = 3; } format_config.enableBitstreamPadding = config.insert_filler_data; if (buffer_is_10bit()) { format_config.inputPixelBitDepthMinus8 = 2; format_config.pixelBitDepthMinus8 = 2; } format_config.colorPrimaries = colorspace.primaries; format_config.transferCharacteristics = colorspace.tranfer_function; format_config.matrixCoefficients = colorspace.matrix; format_config.colorRange = colorspace.full_range; format_config.chromaSamplePosition = buffer_is_yuv444() ? 0 : 1; set_ref_frames(format_config.maxNumRefFramesInDPB, format_config.numFwdRefs, 8); set_minqp_if_enabled(config.min_qp_av1); if (client_config.slicesPerFrame > 1) { // NVENC only supports slice counts that are powers of two, so we'll pick powers of two // with bias to rows due to hopefully more similar macroblocks with a row vs a column. format_config.numTileRows = std::pow(2, std::ceil(std::log2(client_config.slicesPerFrame) / 2)); format_config.numTileColumns = std::pow(2, std::floor(std::log2(client_config.slicesPerFrame) / 2)); } break; } } init_params.encodeConfig = &enc_config; if (nvenc_failed(nvenc->nvEncInitializeEncoder(encoder, &init_params))) { BOOST_LOG(error) << "NvEnc: NvEncInitializeEncoder() failed: " << last_nvenc_error_string; return false; } if (async_event_handle) { NV_ENC_EVENT_PARAMS event_params = {min_struct_version(NV_ENC_EVENT_PARAMS_VER)}; event_params.completionEvent = async_event_handle; if (nvenc_failed(nvenc->nvEncRegisterAsyncEvent(encoder, &event_params))) { BOOST_LOG(error) << "NvEnc: NvEncRegisterAsyncEvent() failed: " << last_nvenc_error_string; return false; } } NV_ENC_CREATE_BITSTREAM_BUFFER create_bitstream_buffer = {min_struct_version(NV_ENC_CREATE_BITSTREAM_BUFFER_VER)}; if (nvenc_failed(nvenc->nvEncCreateBitstreamBuffer(encoder, &create_bitstream_buffer))) { BOOST_LOG(error) << "NvEnc: NvEncCreateBitstreamBuffer() failed: " << last_nvenc_error_string; return false; } output_bitstream = create_bitstream_buffer.bitstreamBuffer; if (!create_and_register_input_buffer()) { return false; } { auto f = stat_trackers::two_digits_after_decimal(); BOOST_LOG(debug) << "NvEnc: requested encoded frame size " << f % (client_config.bitrate / 8. / client_config.framerate) << " kB"; } { auto video_format_string = client_config.videoFormat == 0 ? "H.264 " : client_config.videoFormat == 1 ? "HEVC " : client_config.videoFormat == 2 ? "AV1 " : " "; std::string extra; if (init_params.enableEncodeAsync) { extra += " async"; } if (buffer_is_yuv444()) { extra += " yuv444"; } if (buffer_is_10bit()) { extra += " 10-bit"; } if (enc_config.rcParams.multiPass != NV_ENC_MULTI_PASS_DISABLED) { extra += " two-pass"; } if (config.vbv_percentage_increase > 0 && get_encoder_cap(NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE)) { extra += std::format(" vbv+{}", config.vbv_percentage_increase); } if (encoder_params.rfi) { extra += " rfi"; } if (init_params.enableWeightedPrediction) { extra += " weighted-prediction"; } if (enc_config.rcParams.enableAQ) { extra += " spatial-aq"; } if (enc_config.rcParams.enableMinQP) { extra += std::format(" qpmin={}", enc_config.rcParams.minQP.qpInterP); } if (config.insert_filler_data) { extra += " filler-data"; } BOOST_LOG(info) << "NvEnc: created encoder " << video_format_string << quality_preset_string_from_guid(init_params.presetGUID) << extra; } encoder_state = {}; fail_guard.disable(); return true; } void nvenc_base::destroy_encoder() { if (output_bitstream) { if (nvenc_failed(nvenc->nvEncDestroyBitstreamBuffer(encoder, output_bitstream))) { BOOST_LOG(error) << "NvEnc: NvEncDestroyBitstreamBuffer() failed: " << last_nvenc_error_string; } output_bitstream = nullptr; } if (encoder && async_event_handle) { NV_ENC_EVENT_PARAMS event_params = {min_struct_version(NV_ENC_EVENT_PARAMS_VER)}; event_params.completionEvent = async_event_handle; if (nvenc_failed(nvenc->nvEncUnregisterAsyncEvent(encoder, &event_params))) { BOOST_LOG(error) << "NvEnc: NvEncUnregisterAsyncEvent() failed: " << last_nvenc_error_string; } } if (registered_input_buffer) { if (nvenc_failed(nvenc->nvEncUnregisterResource(encoder, registered_input_buffer))) { BOOST_LOG(error) << "NvEnc: NvEncUnregisterResource() failed: " << last_nvenc_error_string; } registered_input_buffer = nullptr; } if (encoder) { if (nvenc_failed(nvenc->nvEncDestroyEncoder(encoder))) { BOOST_LOG(error) << "NvEnc: NvEncDestroyEncoder() failed: " << last_nvenc_error_string; } encoder = nullptr; } encoder_state = {}; encoder_params = {}; } nvenc_encoded_frame nvenc_base::encode_frame(uint64_t frame_index, bool force_idr) { if (!encoder) { return {}; } assert(registered_input_buffer); assert(output_bitstream); if (!synchronize_input_buffer()) { BOOST_LOG(error) << "NvEnc: failed to synchronize input buffer"; return {}; } NV_ENC_MAP_INPUT_RESOURCE mapped_input_buffer = {min_struct_version(NV_ENC_MAP_INPUT_RESOURCE_VER)}; mapped_input_buffer.registeredResource = registered_input_buffer; if (nvenc_failed(nvenc->nvEncMapInputResource(encoder, &mapped_input_buffer))) { BOOST_LOG(error) << "NvEnc: NvEncMapInputResource() failed: " << last_nvenc_error_string; return {}; } auto unmap_guard = util::fail_guard([&] { if (nvenc_failed(nvenc->nvEncUnmapInputResource(encoder, mapped_input_buffer.mappedResource))) { BOOST_LOG(error) << "NvEnc: NvEncUnmapInputResource() failed: " << last_nvenc_error_string; } }); NV_ENC_PIC_PARAMS pic_params = {min_struct_version(NV_ENC_PIC_PARAMS_VER, 4, 6)}; pic_params.inputWidth = encoder_params.width; pic_params.inputHeight = encoder_params.height; pic_params.encodePicFlags = force_idr ? NV_ENC_PIC_FLAG_FORCEIDR : 0; pic_params.inputTimeStamp = frame_index; pic_params.pictureStruct = NV_ENC_PIC_STRUCT_FRAME; pic_params.inputBuffer = mapped_input_buffer.mappedResource; pic_params.bufferFmt = mapped_input_buffer.mappedBufferFmt; pic_params.outputBitstream = output_bitstream; pic_params.completionEvent = async_event_handle; if (nvenc_failed(nvenc->nvEncEncodePicture(encoder, &pic_params))) { BOOST_LOG(error) << "NvEnc: NvEncEncodePicture() failed: " << last_nvenc_error_string; return {}; } NV_ENC_LOCK_BITSTREAM lock_bitstream = {min_struct_version(NV_ENC_LOCK_BITSTREAM_VER, 1, 2)}; lock_bitstream.outputBitstream = output_bitstream; lock_bitstream.doNotWait = async_event_handle ? 1 : 0; if (async_event_handle && !wait_for_async_event(100)) { BOOST_LOG(error) << "NvEnc: frame " << frame_index << " encode wait timeout"; return {}; } if (nvenc_failed(nvenc->nvEncLockBitstream(encoder, &lock_bitstream))) { BOOST_LOG(error) << "NvEnc: NvEncLockBitstream() failed: " << last_nvenc_error_string; return {}; } auto data_pointer = (uint8_t *) lock_bitstream.bitstreamBufferPtr; nvenc_encoded_frame encoded_frame { {data_pointer, data_pointer + lock_bitstream.bitstreamSizeInBytes}, lock_bitstream.outputTimeStamp, lock_bitstream.pictureType == NV_ENC_PIC_TYPE_IDR, encoder_state.rfi_needs_confirmation, }; if (encoder_state.rfi_needs_confirmation) { // Invalidation request has been fulfilled, and video network packet will be marked as such encoder_state.rfi_needs_confirmation = false; } encoder_state.last_encoded_frame_index = frame_index; if (encoded_frame.idr) { BOOST_LOG(debug) << "NvEnc: idr frame " << encoded_frame.frame_index; } if (nvenc_failed(nvenc->nvEncUnlockBitstream(encoder, lock_bitstream.outputBitstream))) { BOOST_LOG(error) << "NvEnc: NvEncUnlockBitstream() failed: " << last_nvenc_error_string; } encoder_state.frame_size_logger.collect_and_log(encoded_frame.data.size() / 1000.); return encoded_frame; } bool nvenc_base::invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame) { if (!encoder || !encoder_params.rfi) { return false; } if (first_frame >= encoder_state.last_rfi_range.first && last_frame <= encoder_state.last_rfi_range.second) { BOOST_LOG(debug) << "NvEnc: rfi request " << first_frame << "-" << last_frame << " already done"; return true; } encoder_state.rfi_needs_confirmation = true; if (last_frame < first_frame) { BOOST_LOG(error) << "NvEnc: invaid rfi request " << first_frame << "-" << last_frame << ", generating IDR"; return false; } BOOST_LOG(debug) << "NvEnc: rfi request " << first_frame << "-" << last_frame << " expanding to last encoded frame " << encoder_state.last_encoded_frame_index; last_frame = encoder_state.last_encoded_frame_index; encoder_state.last_rfi_range = {first_frame, last_frame}; if (last_frame - first_frame + 1 >= encoder_params.ref_frames_in_dpb) { BOOST_LOG(debug) << "NvEnc: rfi request too large, generating IDR"; return false; } for (auto i = first_frame; i <= last_frame; i++) { if (nvenc_failed(nvenc->nvEncInvalidateRefFrames(encoder, i))) { BOOST_LOG(error) << "NvEnc: NvEncInvalidateRefFrames() " << i << " failed: " << last_nvenc_error_string; return false; } } return true; } bool nvenc_base::nvenc_failed(NVENCSTATUS status) { auto status_string = [](NVENCSTATUS status) -> std::string { switch (status) { #define nvenc_status_case(x) \ case x: \ return #x; nvenc_status_case(NV_ENC_SUCCESS); nvenc_status_case(NV_ENC_ERR_NO_ENCODE_DEVICE); nvenc_status_case(NV_ENC_ERR_UNSUPPORTED_DEVICE); nvenc_status_case(NV_ENC_ERR_INVALID_ENCODERDEVICE); nvenc_status_case(NV_ENC_ERR_INVALID_DEVICE); nvenc_status_case(NV_ENC_ERR_DEVICE_NOT_EXIST); nvenc_status_case(NV_ENC_ERR_INVALID_PTR); nvenc_status_case(NV_ENC_ERR_INVALID_EVENT); nvenc_status_case(NV_ENC_ERR_INVALID_PARAM); nvenc_status_case(NV_ENC_ERR_INVALID_CALL); nvenc_status_case(NV_ENC_ERR_OUT_OF_MEMORY); nvenc_status_case(NV_ENC_ERR_ENCODER_NOT_INITIALIZED); nvenc_status_case(NV_ENC_ERR_UNSUPPORTED_PARAM); nvenc_status_case(NV_ENC_ERR_LOCK_BUSY); nvenc_status_case(NV_ENC_ERR_NOT_ENOUGH_BUFFER); nvenc_status_case(NV_ENC_ERR_INVALID_VERSION); nvenc_status_case(NV_ENC_ERR_MAP_FAILED); nvenc_status_case(NV_ENC_ERR_NEED_MORE_INPUT); nvenc_status_case(NV_ENC_ERR_ENCODER_BUSY); nvenc_status_case(NV_ENC_ERR_EVENT_NOT_REGISTERD); nvenc_status_case(NV_ENC_ERR_GENERIC); nvenc_status_case(NV_ENC_ERR_INCOMPATIBLE_CLIENT_KEY); nvenc_status_case(NV_ENC_ERR_UNIMPLEMENTED); nvenc_status_case(NV_ENC_ERR_RESOURCE_REGISTER_FAILED); nvenc_status_case(NV_ENC_ERR_RESOURCE_NOT_REGISTERED); nvenc_status_case(NV_ENC_ERR_RESOURCE_NOT_MAPPED); // Newer versions of sdk may add more constants, look for them at the end of NVENCSTATUS enum #undef nvenc_status_case default: return std::to_string(status); } }; last_nvenc_error_string.clear(); if (status != NV_ENC_SUCCESS) { /* This API function gives broken strings more often than not if (nvenc && encoder) { last_nvenc_error_string = nvenc->nvEncGetLastErrorString(encoder); if (!last_nvenc_error_string.empty()) last_nvenc_error_string += " "; } */ last_nvenc_error_string += status_string(status); return true; } return false; } uint32_t nvenc_base::min_struct_version(uint32_t version, uint32_t v11_struct_version, uint32_t v12_struct_version) { assert(minimum_api_version); // Mask off and replace the original NVENCAPI_VERSION version &= ~NVENCAPI_VERSION; version |= minimum_api_version; // If there's a struct version override, apply that too if (v11_struct_version || v12_struct_version) { version &= ~(0xFFu << 16); version |= (((minimum_api_version & 0xFF) >= 12) ? v12_struct_version : v11_struct_version) << 16; } return version; } } // namespace nvenc ================================================ FILE: src/nvenc/nvenc_base.h ================================================ /** * @file src/nvenc/nvenc_base.h * @brief Declarations for abstract platform-agnostic base of standalone NVENC encoder. */ #pragma once // lib includes #include // local includes #include "nvenc_colorspace.h" #include "nvenc_config.h" #include "nvenc_encoded_frame.h" #include "src/logging.h" #include "src/video.h" /** * @brief Standalone NVENC encoder */ namespace nvenc { /** * @brief Abstract platform-agnostic base of standalone NVENC encoder. * Derived classes perform platform-specific operations. */ class nvenc_base { public: /** * @param device_type Underlying device type used by derived class. */ explicit nvenc_base(NV_ENC_DEVICE_TYPE device_type); virtual ~nvenc_base(); nvenc_base(const nvenc_base &) = delete; nvenc_base &operator=(const nvenc_base &) = delete; /** * @brief Create the encoder. * @param config NVENC encoder configuration. * @param client_config Stream configuration requested by the client. * @param colorspace YUV colorspace. * @param buffer_format Platform-agnostic input surface format. * @return `true` on success, `false` on error */ bool create_encoder(const nvenc_config &config, const video::config_t &client_config, const nvenc_colorspace_t &colorspace, NV_ENC_BUFFER_FORMAT buffer_format); /** * @brief Destroy the encoder. * Derived classes classes call it in the destructor. */ void destroy_encoder(); /** * @brief Encode the next frame using platform-specific input surface. * @param frame_index Frame index that uniquely identifies the frame. * Afterwards serves as parameter for `invalidate_ref_frames()`. * No restrictions on the first frame index, but later frame indexes must be subsequent. * @param force_idr Whether to encode frame as forced IDR. * @return Encoded frame. */ nvenc_encoded_frame encode_frame(uint64_t frame_index, bool force_idr); /** * @brief Perform reference frame invalidation (RFI) procedure. * @param first_frame First frame index of the invalidation range. * @param last_frame Last frame index of the invalidation range. * @return `true` on success, `false` on error. * After error next frame must be encoded with `force_idr = true`. */ bool invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame); protected: /** * @brief Required. Used for loading NvEnc library and setting `nvenc` variable with `NvEncodeAPICreateInstance()`. * Called during `create_encoder()` if `nvenc` variable is not initialized. * @return `true` on success, `false` on error */ virtual bool init_library() = 0; /** * @brief Required. Used for creating outside-facing input surface, * registering this surface with `nvenc->nvEncRegisterResource()` and setting `registered_input_buffer` variable. * Called during `create_encoder()`. * @return `true` on success, `false` on error */ virtual bool create_and_register_input_buffer() = 0; /** * @brief Optional. Override if you must perform additional operations on the registered input surface in the beginning of `encode_frame()`. * Typically used for interop copy. * @return `true` on success, `false` on error */ virtual bool synchronize_input_buffer() { return true; } /** * @brief Optional. Override if you want to create encoder in async mode. * In this case must also set `async_event_handle` variable. * @param timeout_ms Wait timeout in milliseconds * @return `true` on success, `false` on timeout or error */ virtual bool wait_for_async_event(uint32_t timeout_ms) { return false; } bool nvenc_failed(NVENCSTATUS status); /** * @brief This function returns the corresponding struct version for the minimum API required by the codec. * @details Reducing the struct versions maximizes driver compatibility by avoiding needless API breaks. * @param version The raw structure version from `NVENCAPI_STRUCT_VERSION()`. * @param v11_struct_version Optionally specifies the struct version to use with v11 SDK major versions. * @param v12_struct_version Optionally specifies the struct version to use with v12 SDK major versions. * @return A suitable struct version for the active codec. */ uint32_t min_struct_version(uint32_t version, uint32_t v11_struct_version = 0, uint32_t v12_struct_version = 0); const NV_ENC_DEVICE_TYPE device_type; void *encoder = nullptr; struct { uint32_t width = 0; uint32_t height = 0; NV_ENC_BUFFER_FORMAT buffer_format = NV_ENC_BUFFER_FORMAT_UNDEFINED; uint32_t ref_frames_in_dpb = 0; bool rfi = false; } encoder_params; std::string last_nvenc_error_string; // Derived classes set these variables void *device = nullptr; ///< Platform-specific handle of encoding device. ///< Should be set in constructor or `init_library()`. std::shared_ptr nvenc; ///< Function pointers list produced by `NvEncodeAPICreateInstance()`. ///< Should be set in `init_library()`. NV_ENC_REGISTERED_PTR registered_input_buffer = nullptr; ///< Platform-specific input surface registered with `NvEncRegisterResource()`. ///< Should be set in `create_and_register_input_buffer()`. void *async_event_handle = nullptr; ///< (optional) Platform-specific handle of event object event. ///< Can be set in constructor or `init_library()`, must override `wait_for_async_event()`. private: NV_ENC_OUTPUT_PTR output_bitstream = nullptr; uint32_t minimum_api_version = 0; struct { uint64_t last_encoded_frame_index = 0; bool rfi_needs_confirmation = false; std::pair last_rfi_range; logging::min_max_avg_periodic_logger frame_size_logger = {debug, "NvEnc: encoded frame sizes in kB", ""}; } encoder_state; }; } // namespace nvenc ================================================ FILE: src/nvenc/nvenc_colorspace.h ================================================ /** * @file src/nvenc/nvenc_colorspace.h * @brief Declarations for NVENC YUV colorspace. */ #pragma once // lib includes #include namespace nvenc { /** * @brief YUV colorspace and color range. */ struct nvenc_colorspace_t { NV_ENC_VUI_COLOR_PRIMARIES primaries; NV_ENC_VUI_TRANSFER_CHARACTERISTIC tranfer_function; NV_ENC_VUI_MATRIX_COEFFS matrix; bool full_range; }; } // namespace nvenc ================================================ FILE: src/nvenc/nvenc_config.h ================================================ /** * @file src/nvenc/nvenc_config.h * @brief Declarations for NVENC encoder configuration. */ #pragma once namespace nvenc { enum class nvenc_two_pass { disabled, ///< Single pass, the fastest and no extra vram quarter_resolution, ///< Larger motion vectors being caught, faster and uses less extra vram full_resolution, ///< Better overall statistics, slower and uses more extra vram }; /** * @brief NVENC encoder configuration. */ struct nvenc_config { // Quality preset from 1 to 7, higher is slower int quality_preset = 1; // Use optional preliminary pass for better motion vectors, bitrate distribution and stricter VBV(HRD), uses CUDA cores nvenc_two_pass two_pass = nvenc_two_pass::quarter_resolution; // Percentage increase of VBV/HRD from the default single frame, allows low-latency variable bitrate int vbv_percentage_increase = 0; // Improves fades compression, uses CUDA cores bool weighted_prediction = false; // Allocate more bitrate to flat regions since they're visually more perceptible, uses CUDA cores bool adaptive_quantization = false; // Don't use QP below certain value, limits peak image quality to save bitrate bool enable_min_qp = false; // Min QP value for H.264 when enable_min_qp is selected unsigned min_qp_h264 = 19; // Min QP value for HEVC when enable_min_qp is selected unsigned min_qp_hevc = 23; // Min QP value for AV1 when enable_min_qp is selected unsigned min_qp_av1 = 23; // Use CAVLC entropy coding in H.264 instead of CABAC, not relevant and here for historical reasons bool h264_cavlc = false; // Add filler data to encoded frames to stay at target bitrate, mainly for testing bool insert_filler_data = false; // Intra refresh for clients that doesn't request keyframe correctly bool intra_refresh = false; }; } // namespace nvenc ================================================ FILE: src/nvenc/nvenc_d3d11.cpp ================================================ /** * @file src/nvenc/nvenc_d3d11.cpp * @brief Definitions for abstract Direct3D11 NVENC encoder. */ // local includes #include "src/logging.h" #ifdef _WIN32 #include "nvenc_d3d11.h" namespace nvenc { nvenc_d3d11::nvenc_d3d11(NV_ENC_DEVICE_TYPE device_type): nvenc_base(device_type) { async_event_handle = CreateEvent(nullptr, FALSE, FALSE, nullptr); } nvenc_d3d11::~nvenc_d3d11() { if (dll) { FreeLibrary(dll); dll = nullptr; } if (async_event_handle) { CloseHandle(async_event_handle); } } bool nvenc_d3d11::init_library() { if (dll) { return true; } #ifdef _WIN64 constexpr auto dll_name = "nvEncodeAPI64.dll"; #else constexpr auto dll_name = "nvEncodeAPI.dll"; #endif if ((dll = LoadLibraryEx(dll_name, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32))) { if (auto create_instance = (decltype(NvEncodeAPICreateInstance) *) GetProcAddress(dll, "NvEncodeAPICreateInstance")) { auto new_nvenc = std::make_unique(); new_nvenc->version = min_struct_version(NV_ENCODE_API_FUNCTION_LIST_VER); if (nvenc_failed(create_instance(new_nvenc.get()))) { BOOST_LOG(error) << "NvEnc: NvEncodeAPICreateInstance() failed: " << last_nvenc_error_string; } else { nvenc = std::move(new_nvenc); return true; } } else { BOOST_LOG(error) << "NvEnc: No NvEncodeAPICreateInstance() in " << dll_name; } } else { BOOST_LOG(debug) << "NvEnc: Couldn't load NvEnc library " << dll_name; } if (dll) { FreeLibrary(dll); dll = nullptr; } return false; } bool nvenc_d3d11::wait_for_async_event(uint32_t timeout_ms) { return WaitForSingleObject(async_event_handle, timeout_ms) == WAIT_OBJECT_0; } } // namespace nvenc #endif ================================================ FILE: src/nvenc/nvenc_d3d11.h ================================================ /** * @file src/nvenc/nvenc_d3d11.h * @brief Declarations for abstract Direct3D11 NVENC encoder. */ #pragma once #ifdef _WIN32 // standard includes #include #include // local includes #include "nvenc_base.h" namespace nvenc { _COM_SMARTPTR_TYPEDEF(ID3D11Device, IID_ID3D11Device); _COM_SMARTPTR_TYPEDEF(ID3D11Texture2D, IID_ID3D11Texture2D); _COM_SMARTPTR_TYPEDEF(IDXGIDevice, IID_IDXGIDevice); _COM_SMARTPTR_TYPEDEF(IDXGIAdapter, IID_IDXGIAdapter); /** * @brief Abstract Direct3D11 NVENC encoder. * Encapsulates common code used by native and interop implementations. */ class nvenc_d3d11: public nvenc_base { public: explicit nvenc_d3d11(NV_ENC_DEVICE_TYPE device_type); ~nvenc_d3d11(); /** * @brief Get input surface texture. * @return Input surface texture. */ virtual ID3D11Texture2D *get_input_texture() = 0; protected: bool init_library() override; bool wait_for_async_event(uint32_t timeout_ms) override; private: HMODULE dll = nullptr; }; } // namespace nvenc #endif ================================================ FILE: src/nvenc/nvenc_d3d11_native.cpp ================================================ /** * @file src/nvenc/nvenc_d3d11_native.cpp * @brief Definitions for native Direct3D11 NVENC encoder. */ #ifdef _WIN32 // this include #include "nvenc_d3d11_native.h" // local includes #include "nvenc_utils.h" namespace nvenc { nvenc_d3d11_native::nvenc_d3d11_native(ID3D11Device *d3d_device): nvenc_d3d11(NV_ENC_DEVICE_TYPE_DIRECTX), d3d_device(d3d_device) { device = d3d_device; } nvenc_d3d11_native::~nvenc_d3d11_native() { if (encoder) { destroy_encoder(); } } ID3D11Texture2D * nvenc_d3d11_native::get_input_texture() { return d3d_input_texture.GetInterfacePtr(); } bool nvenc_d3d11_native::create_and_register_input_buffer() { if (encoder_params.buffer_format == NV_ENC_BUFFER_FORMAT_YUV444_10BIT) { BOOST_LOG(error) << "NvEnc: 10-bit 4:4:4 encoding is incompatible with D3D11 surface formats, use CUDA interop"; return false; } if (!d3d_input_texture) { D3D11_TEXTURE2D_DESC desc = {}; desc.Width = encoder_params.width; desc.Height = encoder_params.height; desc.MipLevels = 1; desc.ArraySize = 1; desc.Format = dxgi_format_from_nvenc_format(encoder_params.buffer_format); desc.SampleDesc.Count = 1; desc.Usage = D3D11_USAGE_DEFAULT; desc.BindFlags = D3D11_BIND_RENDER_TARGET; if (d3d_device->CreateTexture2D(&desc, nullptr, &d3d_input_texture) != S_OK) { BOOST_LOG(error) << "NvEnc: couldn't create input texture"; return false; } } if (!registered_input_buffer) { NV_ENC_REGISTER_RESOURCE register_resource = {min_struct_version(NV_ENC_REGISTER_RESOURCE_VER, 3, 4)}; register_resource.resourceType = NV_ENC_INPUT_RESOURCE_TYPE_DIRECTX; register_resource.width = encoder_params.width; register_resource.height = encoder_params.height; register_resource.resourceToRegister = d3d_input_texture.GetInterfacePtr(); register_resource.bufferFormat = encoder_params.buffer_format; register_resource.bufferUsage = NV_ENC_INPUT_IMAGE; if (nvenc_failed(nvenc->nvEncRegisterResource(encoder, ®ister_resource))) { BOOST_LOG(error) << "NvEnc: NvEncRegisterResource() failed: " << last_nvenc_error_string; return false; } registered_input_buffer = register_resource.registeredResource; } return true; } } // namespace nvenc #endif ================================================ FILE: src/nvenc/nvenc_d3d11_native.h ================================================ /** * @file src/nvenc/nvenc_d3d11_native.h * @brief Declarations for native Direct3D11 NVENC encoder. */ #pragma once #ifdef _WIN32 // standard includes #include #include // local includes #include "nvenc_d3d11.h" namespace nvenc { /** * @brief Native Direct3D11 NVENC encoder. */ class nvenc_d3d11_native final: public nvenc_d3d11 { public: /** * @param d3d_device Direct3D11 device used for encoding. */ explicit nvenc_d3d11_native(ID3D11Device *d3d_device); ~nvenc_d3d11_native(); ID3D11Texture2D *get_input_texture() override; private: bool create_and_register_input_buffer() override; const ID3D11DevicePtr d3d_device; ID3D11Texture2DPtr d3d_input_texture; }; } // namespace nvenc #endif ================================================ FILE: src/nvenc/nvenc_d3d11_on_cuda.cpp ================================================ /** * @file src/nvenc/nvenc_d3d11_on_cuda.cpp * @brief Definitions for CUDA NVENC encoder with Direct3D11 input surfaces. */ #ifdef _WIN32 // this include #include "nvenc_d3d11_on_cuda.h" // local includes #include "nvenc_utils.h" namespace nvenc { nvenc_d3d11_on_cuda::nvenc_d3d11_on_cuda(ID3D11Device *d3d_device): nvenc_d3d11(NV_ENC_DEVICE_TYPE_CUDA), d3d_device(d3d_device) { } nvenc_d3d11_on_cuda::~nvenc_d3d11_on_cuda() { if (encoder) { destroy_encoder(); } if (cuda_context) { { auto autopop_context = push_context(); if (cuda_d3d_input_texture) { if (cuda_failed(cuda_functions.cuGraphicsUnregisterResource(cuda_d3d_input_texture))) { BOOST_LOG(error) << "NvEnc: cuGraphicsUnregisterResource() failed: error " << last_cuda_error; } cuda_d3d_input_texture = nullptr; } if (cuda_surface) { if (cuda_failed(cuda_functions.cuMemFree(cuda_surface))) { BOOST_LOG(error) << "NvEnc: cuMemFree() failed: error " << last_cuda_error; } cuda_surface = 0; } } if (cuda_failed(cuda_functions.cuCtxDestroy(cuda_context))) { BOOST_LOG(error) << "NvEnc: cuCtxDestroy() failed: error " << last_cuda_error; } cuda_context = nullptr; } if (cuda_functions.dll) { FreeLibrary(cuda_functions.dll); cuda_functions = {}; } } ID3D11Texture2D *nvenc_d3d11_on_cuda::get_input_texture() { return d3d_input_texture.GetInterfacePtr(); } bool nvenc_d3d11_on_cuda::init_library() { if (!nvenc_d3d11::init_library()) { return false; } constexpr auto dll_name = "nvcuda.dll"; if ((cuda_functions.dll = LoadLibraryEx(dll_name, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32))) { auto load_function = [&](T &location, auto symbol) -> bool { location = (T) GetProcAddress(cuda_functions.dll, symbol); return location != nullptr; }; if (!load_function(cuda_functions.cuInit, "cuInit") || !load_function(cuda_functions.cuD3D11GetDevice, "cuD3D11GetDevice") || !load_function(cuda_functions.cuCtxCreate, "cuCtxCreate_v2") || !load_function(cuda_functions.cuCtxDestroy, "cuCtxDestroy_v2") || !load_function(cuda_functions.cuCtxPushCurrent, "cuCtxPushCurrent_v2") || !load_function(cuda_functions.cuCtxPopCurrent, "cuCtxPopCurrent_v2") || !load_function(cuda_functions.cuMemAllocPitch, "cuMemAllocPitch_v2") || !load_function(cuda_functions.cuMemFree, "cuMemFree_v2") || !load_function(cuda_functions.cuGraphicsD3D11RegisterResource, "cuGraphicsD3D11RegisterResource") || !load_function(cuda_functions.cuGraphicsUnregisterResource, "cuGraphicsUnregisterResource") || !load_function(cuda_functions.cuGraphicsMapResources, "cuGraphicsMapResources") || !load_function(cuda_functions.cuGraphicsUnmapResources, "cuGraphicsUnmapResources") || !load_function(cuda_functions.cuGraphicsSubResourceGetMappedArray, "cuGraphicsSubResourceGetMappedArray") || !load_function(cuda_functions.cuMemcpy2D, "cuMemcpy2D_v2")) { BOOST_LOG(error) << "NvEnc: missing CUDA functions in " << dll_name; FreeLibrary(cuda_functions.dll); cuda_functions = {}; } } else { BOOST_LOG(debug) << "NvEnc: couldn't load CUDA dynamic library " << dll_name; } if (cuda_functions.dll) { IDXGIDevicePtr dxgi_device; IDXGIAdapterPtr dxgi_adapter; if (d3d_device && SUCCEEDED(d3d_device->QueryInterface(IID_PPV_ARGS(&dxgi_device))) && SUCCEEDED(dxgi_device->GetAdapter(&dxgi_adapter))) { CUdevice cuda_device; if (cuda_succeeded(cuda_functions.cuInit(0)) && cuda_succeeded(cuda_functions.cuD3D11GetDevice(&cuda_device, dxgi_adapter)) && cuda_succeeded(cuda_functions.cuCtxCreate(&cuda_context, CU_CTX_SCHED_BLOCKING_SYNC, cuda_device)) && cuda_succeeded(cuda_functions.cuCtxPopCurrent(&cuda_context))) { device = cuda_context; } else { BOOST_LOG(error) << "NvEnc: couldn't create CUDA interop context: error " << last_cuda_error; } } else { BOOST_LOG(error) << "NvEnc: couldn't get DXGI adapter for CUDA interop"; } } return device != nullptr; } bool nvenc_d3d11_on_cuda::create_and_register_input_buffer() { if (encoder_params.buffer_format != NV_ENC_BUFFER_FORMAT_YUV444_10BIT) { BOOST_LOG(error) << "NvEnc: CUDA interop is expected to be used only for 10-bit 4:4:4 encoding"; return false; } if (!d3d_input_texture) { D3D11_TEXTURE2D_DESC desc = {}; desc.Width = encoder_params.width; desc.Height = encoder_params.height * 3; // Planar YUV desc.MipLevels = 1; desc.ArraySize = 1; desc.Format = dxgi_format_from_nvenc_format(encoder_params.buffer_format); desc.SampleDesc.Count = 1; desc.Usage = D3D11_USAGE_DEFAULT; desc.BindFlags = D3D11_BIND_RENDER_TARGET; if (d3d_device->CreateTexture2D(&desc, nullptr, &d3d_input_texture) != S_OK) { BOOST_LOG(error) << "NvEnc: couldn't create input texture"; return false; } } { auto autopop_context = push_context(); if (!autopop_context) { return false; } if (!cuda_d3d_input_texture) { if (cuda_failed(cuda_functions.cuGraphicsD3D11RegisterResource( &cuda_d3d_input_texture, d3d_input_texture, CU_GRAPHICS_REGISTER_FLAGS_NONE ))) { BOOST_LOG(error) << "NvEnc: cuGraphicsD3D11RegisterResource() failed: error " << last_cuda_error; return false; } } if (!cuda_surface) { if (cuda_failed(cuda_functions.cuMemAllocPitch( &cuda_surface, &cuda_surface_pitch, // Planar 16-bit YUV encoder_params.width * 2, encoder_params.height * 3, 16 ))) { BOOST_LOG(error) << "NvEnc: cuMemAllocPitch() failed: error " << last_cuda_error; return false; } } } if (!registered_input_buffer) { NV_ENC_REGISTER_RESOURCE register_resource = {min_struct_version(NV_ENC_REGISTER_RESOURCE_VER, 3, 4)}; register_resource.resourceType = NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR; register_resource.width = encoder_params.width; register_resource.height = encoder_params.height; register_resource.pitch = cuda_surface_pitch; register_resource.resourceToRegister = (void *) cuda_surface; register_resource.bufferFormat = encoder_params.buffer_format; register_resource.bufferUsage = NV_ENC_INPUT_IMAGE; if (nvenc_failed(nvenc->nvEncRegisterResource(encoder, ®ister_resource))) { BOOST_LOG(error) << "NvEnc: NvEncRegisterResource() failed: " << last_nvenc_error_string; return false; } registered_input_buffer = register_resource.registeredResource; } return true; } bool nvenc_d3d11_on_cuda::synchronize_input_buffer() { auto autopop_context = push_context(); if (!autopop_context) { return false; } if (cuda_failed(cuda_functions.cuGraphicsMapResources(1, &cuda_d3d_input_texture, 0))) { BOOST_LOG(error) << "NvEnc: cuGraphicsMapResources() failed: error " << last_cuda_error; return false; } auto unmap = [&]() -> bool { if (cuda_failed(cuda_functions.cuGraphicsUnmapResources(1, &cuda_d3d_input_texture, 0))) { BOOST_LOG(error) << "NvEnc: cuGraphicsUnmapResources() failed: error " << last_cuda_error; return false; } return true; }; auto unmap_guard = util::fail_guard(unmap); CUarray input_texture_array; if (cuda_failed(cuda_functions.cuGraphicsSubResourceGetMappedArray(&input_texture_array, cuda_d3d_input_texture, 0, 0))) { BOOST_LOG(error) << "NvEnc: cuGraphicsSubResourceGetMappedArray() failed: error " << last_cuda_error; return false; } { CUDA_MEMCPY2D copy_params = {}; copy_params.srcMemoryType = CU_MEMORYTYPE_ARRAY; copy_params.srcArray = input_texture_array; copy_params.dstMemoryType = CU_MEMORYTYPE_DEVICE; copy_params.dstDevice = cuda_surface; copy_params.dstPitch = cuda_surface_pitch; // Planar 16-bit YUV copy_params.WidthInBytes = encoder_params.width * 2; copy_params.Height = encoder_params.height * 3; if (cuda_failed(cuda_functions.cuMemcpy2D(©_params))) { BOOST_LOG(error) << "NvEnc: cuMemcpy2D() failed: error " << last_cuda_error; return false; } } unmap_guard.disable(); return unmap(); } bool nvenc_d3d11_on_cuda::cuda_succeeded(CUresult result) { last_cuda_error = result; return result == CUDA_SUCCESS; } bool nvenc_d3d11_on_cuda::cuda_failed(CUresult result) { last_cuda_error = result; return result != CUDA_SUCCESS; } nvenc_d3d11_on_cuda::autopop_context::~autopop_context() { if (pushed_context) { CUcontext popped_context; if (parent.cuda_failed(parent.cuda_functions.cuCtxPopCurrent(&popped_context))) { BOOST_LOG(error) << "NvEnc: cuCtxPopCurrent() failed: error " << parent.last_cuda_error; } } } nvenc_d3d11_on_cuda::autopop_context nvenc_d3d11_on_cuda::push_context() { if (cuda_context && cuda_succeeded(cuda_functions.cuCtxPushCurrent(cuda_context))) { return {*this, cuda_context}; } else { BOOST_LOG(error) << "NvEnc: cuCtxPushCurrent() failed: error " << last_cuda_error; return {*this, nullptr}; } } } // namespace nvenc #endif ================================================ FILE: src/nvenc/nvenc_d3d11_on_cuda.h ================================================ /** * @file src/nvenc/nvenc_d3d11_on_cuda.h * @brief Declarations for CUDA NVENC encoder with Direct3D11 input surfaces. */ #pragma once #ifdef _WIN32 // lib includes #include // local includes #include "nvenc_d3d11.h" namespace nvenc { /** * @brief Interop Direct3D11 on CUDA NVENC encoder. * Input surface is Direct3D11, encoding is performed by CUDA. */ class nvenc_d3d11_on_cuda final: public nvenc_d3d11 { public: /** * @param d3d_device Direct3D11 device that will create input surface texture. * CUDA encoding device will be derived from it. */ explicit nvenc_d3d11_on_cuda(ID3D11Device *d3d_device); ~nvenc_d3d11_on_cuda(); ID3D11Texture2D *get_input_texture() override; private: bool init_library() override; bool create_and_register_input_buffer() override; bool synchronize_input_buffer() override; bool cuda_succeeded(CUresult result); bool cuda_failed(CUresult result); struct autopop_context { autopop_context(nvenc_d3d11_on_cuda &parent, CUcontext pushed_context): parent(parent), pushed_context(pushed_context) { } ~autopop_context(); explicit operator bool() const { return pushed_context != nullptr; } nvenc_d3d11_on_cuda &parent; CUcontext pushed_context = nullptr; }; autopop_context push_context(); HMODULE dll = nullptr; const ID3D11DevicePtr d3d_device; ID3D11Texture2DPtr d3d_input_texture; struct { tcuInit *cuInit; tcuD3D11GetDevice *cuD3D11GetDevice; tcuCtxCreate_v2 *cuCtxCreate; tcuCtxDestroy_v2 *cuCtxDestroy; tcuCtxPushCurrent_v2 *cuCtxPushCurrent; tcuCtxPopCurrent_v2 *cuCtxPopCurrent; tcuMemAllocPitch_v2 *cuMemAllocPitch; tcuMemFree_v2 *cuMemFree; tcuGraphicsD3D11RegisterResource *cuGraphicsD3D11RegisterResource; tcuGraphicsUnregisterResource *cuGraphicsUnregisterResource; tcuGraphicsMapResources *cuGraphicsMapResources; tcuGraphicsUnmapResources *cuGraphicsUnmapResources; tcuGraphicsSubResourceGetMappedArray *cuGraphicsSubResourceGetMappedArray; tcuMemcpy2D_v2 *cuMemcpy2D; HMODULE dll; } cuda_functions = {}; CUresult last_cuda_error = CUDA_SUCCESS; CUcontext cuda_context = nullptr; CUgraphicsResource cuda_d3d_input_texture = nullptr; CUdeviceptr cuda_surface = 0; size_t cuda_surface_pitch = 0; }; } // namespace nvenc #endif ================================================ FILE: src/nvenc/nvenc_encoded_frame.h ================================================ /** * @file src/nvenc/nvenc_encoded_frame.h * @brief Declarations for NVENC encoded frame. */ #pragma once // standard includes #include #include namespace nvenc { /** * @brief Encoded frame. */ struct nvenc_encoded_frame { std::vector data; uint64_t frame_index = 0; bool idr = false; bool after_ref_frame_invalidation = false; }; } // namespace nvenc ================================================ FILE: src/nvenc/nvenc_utils.cpp ================================================ /** * @file src/nvenc/nvenc_utils.cpp * @brief Definitions for NVENC utilities. */ // standard includes #include // local includes #include "nvenc_utils.h" namespace nvenc { #ifdef _WIN32 DXGI_FORMAT dxgi_format_from_nvenc_format(NV_ENC_BUFFER_FORMAT format) { switch (format) { case NV_ENC_BUFFER_FORMAT_YUV420_10BIT: return DXGI_FORMAT_P010; case NV_ENC_BUFFER_FORMAT_NV12: return DXGI_FORMAT_NV12; case NV_ENC_BUFFER_FORMAT_AYUV: return DXGI_FORMAT_AYUV; case NV_ENC_BUFFER_FORMAT_YUV444_10BIT: return DXGI_FORMAT_R16_UINT; default: return DXGI_FORMAT_UNKNOWN; } } #endif NV_ENC_BUFFER_FORMAT nvenc_format_from_sunshine_format(platf::pix_fmt_e format) { switch (format) { case platf::pix_fmt_e::nv12: return NV_ENC_BUFFER_FORMAT_NV12; case platf::pix_fmt_e::p010: return NV_ENC_BUFFER_FORMAT_YUV420_10BIT; case platf::pix_fmt_e::ayuv: return NV_ENC_BUFFER_FORMAT_AYUV; case platf::pix_fmt_e::yuv444p16: return NV_ENC_BUFFER_FORMAT_YUV444_10BIT; default: return NV_ENC_BUFFER_FORMAT_UNDEFINED; } } nvenc_colorspace_t nvenc_colorspace_from_sunshine_colorspace(const video::sunshine_colorspace_t &sunshine_colorspace) { nvenc_colorspace_t colorspace; switch (sunshine_colorspace.colorspace) { case video::colorspace_e::rec601: // Rec. 601 colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_SMPTE170M; colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE170M; colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_SMPTE170M; break; case video::colorspace_e::rec709: // Rec. 709 colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_BT709; colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709; colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_BT709; break; case video::colorspace_e::bt2020sdr: // Rec. 2020 colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_BT2020; assert(sunshine_colorspace.bit_depth == 10); colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT2020_10; colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_BT2020_NCL; break; case video::colorspace_e::bt2020: // Rec. 2020 with ST 2084 perceptual quantizer colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_BT2020; assert(sunshine_colorspace.bit_depth == 10); colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE2084; colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_BT2020_NCL; break; } colorspace.full_range = sunshine_colorspace.full_range; return colorspace; } } // namespace nvenc ================================================ FILE: src/nvenc/nvenc_utils.h ================================================ /** * @file src/nvenc/nvenc_utils.h * @brief Declarations for NVENC utilities. */ #pragma once // plafform includes #ifdef _WIN32 #include #endif // lib includes #include // local includes #include "nvenc_colorspace.h" #include "src/platform/common.h" #include "src/video_colorspace.h" namespace nvenc { #ifdef _WIN32 DXGI_FORMAT dxgi_format_from_nvenc_format(NV_ENC_BUFFER_FORMAT format); #endif NV_ENC_BUFFER_FORMAT nvenc_format_from_sunshine_format(platf::pix_fmt_e format); nvenc_colorspace_t nvenc_colorspace_from_sunshine_colorspace(const video::sunshine_colorspace_t &sunshine_colorspace); } // namespace nvenc ================================================ FILE: src/nvhttp.cpp ================================================ /** * @file src/nvhttp.cpp * @brief Definitions for the nvhttp (GameStream) server. */ // macros #define BOOST_BIND_GLOBAL_PLACEHOLDERS // standard includes #include #include #include #include #include // lib includes #include #include #include #include #include #include // local includes #include "config.h" #include "display_device.h" #include "file_handler.h" #include "globals.h" #include "httpcommon.h" #include "logging.h" #include "network.h" #include "nvhttp.h" #include "platform/common.h" #include "process.h" #include "rtsp.h" #include "stream.h" #include "system_tray.h" #include "utility.h" #include "uuid.h" #include "video.h" #include "zwpad.h" #ifdef _WIN32 #include "platform/windows/virtual_display.h" #endif using namespace std::literals; namespace nvhttp { namespace fs = std::filesystem; namespace pt = boost::property_tree; using p_named_cert_t = crypto::p_named_cert_t; using PERM = crypto::PERM; struct client_t { std::vector named_devices; }; struct pair_session_t; crypto::cert_chain_t cert_chain; static std::string one_time_pin; static std::string otp_passphrase; static std::string otp_device_name; static std::chrono::time_point otp_creation_time; class SunshineHTTPSServer: public SimpleWeb::ServerBase { public: SunshineHTTPSServer(const std::string &certification_file, const std::string &private_key_file): ServerBase::ServerBase(443), context(boost::asio::ssl::context::tls_server) { // Disabling TLS 1.0 and 1.1 (see RFC 8996) context.set_options(boost::asio::ssl::context::no_tlsv1); context.set_options(boost::asio::ssl::context::no_tlsv1_1); context.use_certificate_chain_file(certification_file); context.use_private_key_file(private_key_file, boost::asio::ssl::context::pem); } std::function, SSL*)> verify; std::function, std::shared_ptr)> on_verify_failed; protected: boost::asio::ssl::context context; void after_bind() override { if (verify) { context.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once); context.set_verify_callback([](int verified, boost::asio::ssl::verify_context &ctx) { // To respond with an error message, a connection must be established return 1; }); } } // This is Server::accept() with SSL validation support added void accept() override { auto connection = create_connection(*io_service, context); acceptor->async_accept(connection->socket->lowest_layer(), [this, connection](const SimpleWeb::error_code &ec) { auto lock = connection->handler_runner->continue_lock(); if (!lock) { return; } if (ec != SimpleWeb::error::operation_aborted) { this->accept(); } auto session = std::make_shared(config.max_request_streambuf_size, connection); if (!ec) { boost::asio::ip::tcp::no_delay option(true); SimpleWeb::error_code ec; session->connection->socket->lowest_layer().set_option(option, ec); session->connection->set_timeout(config.timeout_request); session->connection->socket->async_handshake(boost::asio::ssl::stream_base::server, [this, session](const SimpleWeb::error_code &ec) { session->connection->cancel_timeout(); auto lock = session->connection->handler_runner->continue_lock(); if (!lock) { return; } if (!ec) { if (verify && !verify(session->request, session->connection->socket->native_handle())) { this->write(session, on_verify_failed); } else { this->read(session); } } else if (this->on_error) { this->on_error(session->request, ec); } }); } else if (this->on_error) { this->on_error(session->request, ec); } }); } }; using https_server_t = SunshineHTTPSServer; using http_server_t = SimpleWeb::Server; struct conf_intern_t { std::string servercert; std::string pkey; } conf_intern; // uniqueID, session std::unordered_map map_id_sess; client_t client_root; std::atomic session_id_counter; using resp_https_t = std::shared_ptr::Response>; using req_https_t = std::shared_ptr::Request>; using resp_http_t = std::shared_ptr::Response>; using req_http_t = std::shared_ptr::Request>; enum class op_e { ADD, ///< Add certificate REMOVE ///< Remove certificate }; std::string get_arg(const args_t &args, const char *name, const char *default_value) { auto it = args.find(name); if (it == std::end(args)) { if (default_value != nullptr) { return std::string(default_value); } throw std::out_of_range(name); } return it->second; } // Helper function to extract command entries from a JSON object. cmd_list_t extract_command_entries(const nlohmann::json& j, const std::string& key) { cmd_list_t commands; // Check if the key exists in the JSON. if (j.contains(key)) { // Ensure that the value for the key is an array. try { for (const auto& item : j.at(key)) { try { // Extract "cmd" and "elevated" fields from the JSON object. std::string cmd = item.at("cmd").get(); bool elevated = util::get_non_string_json_value(item, "elevated", false); // Add the command entry to the list. commands.push_back({cmd, elevated}); } catch (const std::exception& e) { BOOST_LOG(warning) << "Error parsing command entry: " << e.what(); } } } catch (const std::exception &e) { BOOST_LOG(warning) << "Error retrieving key \"" << key << "\": " << e.what(); } } else { BOOST_LOG(debug) << "Key \"" << key << "\" not found in the JSON."; } return commands; } void save_state() { nlohmann::json root = nlohmann::json::object(); // If the state file exists, try to read it. if (fs::exists(config::nvhttp.file_state)) { try { std::ifstream in(config::nvhttp.file_state); in >> root; } catch (std::exception &e) { BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what(); return; } } // Erase any previous "root" key. root.erase("root"); // Create a new "root" object and set the unique id. root["root"] = nlohmann::json::object(); root["root"]["uniqueid"] = http::unique_id; client_t &client = client_root; nlohmann::json named_cert_nodes = nlohmann::json::array(); std::unordered_set unique_certs; std::unordered_map name_counts; for (auto &named_cert_p : client.named_devices) { // Only add each unique certificate once. if (unique_certs.insert(named_cert_p->cert).second) { nlohmann::json named_cert_node = nlohmann::json::object(); std::string base_name = named_cert_p->name; // Remove any pending id suffix (e.g., " (2)") if present. size_t pos = base_name.find(" ("); if (pos != std::string::npos) { base_name = base_name.substr(0, pos); } int count = name_counts[base_name]++; std::string final_name = base_name; if (count > 0) { final_name += " (" + std::to_string(count + 1) + ")"; } named_cert_node["name"] = final_name; named_cert_node["cert"] = named_cert_p->cert; named_cert_node["uuid"] = named_cert_p->uuid; named_cert_node["display_mode"] = named_cert_p->display_mode; named_cert_node["perm"] = static_cast(named_cert_p->perm); named_cert_node["enable_legacy_ordering"] = named_cert_p->enable_legacy_ordering; named_cert_node["allow_client_commands"] = named_cert_p->allow_client_commands; named_cert_node["always_use_virtual_display"] = named_cert_p->always_use_virtual_display; // Add "do" commands if available. if (!named_cert_p->do_cmds.empty()) { nlohmann::json do_cmds_node = nlohmann::json::array(); for (const auto &cmd : named_cert_p->do_cmds) { do_cmds_node.push_back(crypto::command_entry_t::serialize(cmd)); } named_cert_node["do"] = do_cmds_node; } // Add "undo" commands if available. if (!named_cert_p->undo_cmds.empty()) { nlohmann::json undo_cmds_node = nlohmann::json::array(); for (const auto &cmd : named_cert_p->undo_cmds) { undo_cmds_node.push_back(crypto::command_entry_t::serialize(cmd)); } named_cert_node["undo"] = undo_cmds_node; } named_cert_nodes.push_back(named_cert_node); } } root["root"]["named_devices"] = named_cert_nodes; try { std::ofstream out(config::nvhttp.file_state); out << root.dump(4); // Pretty-print with an indent of 4 spaces. } catch (std::exception &e) { BOOST_LOG(error) << "Couldn't write "sv << config::nvhttp.file_state << ": "sv << e.what(); return; } } void load_state() { if (!fs::exists(config::nvhttp.file_state)) { BOOST_LOG(info) << "File "sv << config::nvhttp.file_state << " doesn't exist"sv; http::unique_id = uuid_util::uuid_t::generate().string(); return; } nlohmann::json tree; try { std::ifstream in(config::nvhttp.file_state); in >> tree; } catch (std::exception &e) { BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what(); return; } // Check that the file contains a "root.uniqueid" value. if (!tree.contains("root") || !tree["root"].contains("uniqueid")) { http::uuid = uuid_util::uuid_t::generate(); http::unique_id = http::uuid.string(); return; } std::string uid = tree["root"]["uniqueid"]; http::uuid = uuid_util::uuid_t::parse(uid); http::unique_id = uid; nlohmann::json root = tree["root"]; client_t client; // Local client to load into // Import from the old format if available. if (root.contains("devices")) { for (auto &device_node : root["devices"]) { // For each device, if there is a "certs" array, add a named certificate. if (device_node.contains("certs")) { for (auto &el : device_node["certs"]) { auto named_cert_p = std::make_shared(); named_cert_p->name = ""; named_cert_p->cert = el.get(); named_cert_p->uuid = uuid_util::uuid_t::generate().string(); named_cert_p->display_mode = ""; named_cert_p->perm = PERM::_all; named_cert_p->enable_legacy_ordering = true; named_cert_p->allow_client_commands = true; named_cert_p->always_use_virtual_display = false; client.named_devices.emplace_back(named_cert_p); } } } } // Import from the new format. if (root.contains("named_devices")) { for (auto &el : root["named_devices"]) { auto named_cert_p = std::make_shared(); named_cert_p->name = el.value("name", ""); named_cert_p->cert = el.value("cert", ""); named_cert_p->uuid = el.value("uuid", ""); named_cert_p->display_mode = el.value("display_mode", ""); named_cert_p->perm = (PERM)(util::get_non_string_json_value(el, "perm", (uint32_t)PERM::_all)) & PERM::_all; named_cert_p->enable_legacy_ordering = el.value("enable_legacy_ordering", true); named_cert_p->allow_client_commands = el.value("allow_client_commands", true); named_cert_p->always_use_virtual_display = el.value("always_use_virtual_display", false); // Load command entries for "do" and "undo" keys. named_cert_p->do_cmds = extract_command_entries(el, "do"); named_cert_p->undo_cmds = extract_command_entries(el, "undo"); client.named_devices.emplace_back(named_cert_p); } } // Clear any existing certificate chain and add the imported certificates. cert_chain.clear(); for (auto &named_cert : client.named_devices) { cert_chain.add(named_cert); } client_root = client; } void add_authorized_client(const p_named_cert_t& named_cert_p) { client_t &client = client_root; client.named_devices.push_back(named_cert_p); #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_paired(named_cert_p->name); #endif if (!config::sunshine.flags[config::flag::FRESH_STATE]) { save_state(); load_state(); } } std::shared_ptr make_launch_session(bool host_audio, bool input_only, const args_t &args, const crypto::named_cert_t* named_cert_p) { auto launch_session = std::make_shared(); launch_session->id = ++session_id_counter; // If launched from client if (named_cert_p->uuid != http::unique_id) { auto rikey = util::from_hex_vec(get_arg(args, "rikey"), true); std::copy(rikey.cbegin(), rikey.cend(), std::back_inserter(launch_session->gcm_key)); launch_session->host_audio = host_audio; // Encrypted RTSP is enabled with client reported corever >= 1 auto corever = util::from_view(get_arg(args, "corever", "0")); if (corever >= 1) { launch_session->rtsp_cipher = crypto::cipher::gcm_t { launch_session->gcm_key, false }; launch_session->rtsp_iv_counter = 0; } launch_session->rtsp_url_scheme = launch_session->rtsp_cipher ? "rtspenc://"s : "rtsp://"s; // Generate the unique identifiers for this connection that we will send later during RTSP handshake unsigned char raw_payload[8]; RAND_bytes(raw_payload, sizeof(raw_payload)); launch_session->av_ping_payload = util::hex_vec(raw_payload); RAND_bytes((unsigned char *) &launch_session->control_connect_data, sizeof(launch_session->control_connect_data)); launch_session->iv.resize(16); uint32_t prepend_iv = util::endian::big(util::from_view(get_arg(args, "rikeyid"))); auto prepend_iv_p = (uint8_t *) &prepend_iv; std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session->iv)); } std::stringstream mode; if (named_cert_p->display_mode.empty()) { auto mode_str = get_arg(args, "mode", config::video.fallback_mode.c_str()); mode = std::stringstream(mode_str); BOOST_LOG(info) << "Display mode for client ["sv << named_cert_p->name <<"] requested to ["sv << mode_str << ']'; } else { mode = std::stringstream(named_cert_p->display_mode); BOOST_LOG(info) << "Display mode for client ["sv << named_cert_p->name <<"] overriden to ["sv << named_cert_p->display_mode << ']'; } // Split mode by the char "x", to populate width/height/fps int x = 0; std::string segment; while (std::getline(mode, segment, 'x')) { if (x == 0) { launch_session->width = atoi(segment.c_str()); } if (x == 1) { launch_session->height = atoi(segment.c_str()); } if (x == 2) { auto fps = atof(segment.c_str()); if (fps < 1000) { fps *= 1000; }; launch_session->fps = (int)fps; break; } x++; } // Parsing have failed or missing components if (x != 2) { launch_session->width = 1920; launch_session->height = 1080; launch_session->fps = 60000; // 60fps * 1000 denominator } launch_session->device_name = named_cert_p->name.empty() ? "ApolloDisplay"s : named_cert_p->name; launch_session->unique_id = named_cert_p->uuid; launch_session->perm = named_cert_p->perm; launch_session->enable_sops = util::from_view(get_arg(args, "sops", "0")); launch_session->surround_info = util::from_view(get_arg(args, "surroundAudioInfo", "196610")); launch_session->surround_params = (get_arg(args, "surroundParams", "")); launch_session->gcmap = util::from_view(get_arg(args, "gcmap", "0")); launch_session->enable_hdr = util::from_view(get_arg(args, "hdrMode", "0")); launch_session->virtual_display = util::from_view(get_arg(args, "virtualDisplay", "0")) || named_cert_p->always_use_virtual_display; launch_session->scale_factor = util::from_view(get_arg(args, "scaleFactor", "100")); launch_session->client_do_cmds = named_cert_p->do_cmds; launch_session->client_undo_cmds = named_cert_p->undo_cmds; launch_session->input_only = input_only; return launch_session; } void remove_session(const pair_session_t &sess) { map_id_sess.erase(sess.client.uniqueID); } void fail_pair(pair_session_t &sess, pt::ptree &tree, const std::string status_msg) { tree.put("root.paired", 0); tree.put("root..status_code", 400); tree.put("root..status_message", status_msg); remove_session(sess); // Security measure, delete the session when something went wrong and force a re-pair BOOST_LOG(warning) << "Pair attempt failed due to " << status_msg; } void getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin) { if (sess.last_phase != PAIR_PHASE::NONE) { fail_pair(sess, tree, "Out of order call to getservercert"); return; } sess.last_phase = PAIR_PHASE::GETSERVERCERT; if (sess.async_insert_pin.salt.size() < 32) { fail_pair(sess, tree, "Salt too short"); return; } std::string_view salt_view {sess.async_insert_pin.salt.data(), 32}; auto salt = util::from_hex>(salt_view, true); auto key = crypto::gen_aes_key(salt, pin); sess.cipher_key = std::make_unique(key); tree.put("root.paired", 1); tree.put("root.plaincert", util::hex_vec(conf_intern.servercert, true)); tree.put("root..status_code", 200); } void clientchallenge(pair_session_t &sess, pt::ptree &tree, const std::string &challenge) { if (sess.last_phase != PAIR_PHASE::GETSERVERCERT) { fail_pair(sess, tree, "Out of order call to clientchallenge"); return; } sess.last_phase = PAIR_PHASE::CLIENTCHALLENGE; if (!sess.cipher_key) { fail_pair(sess, tree, "Cipher key not set"); return; } crypto::cipher::ecb_t cipher(*sess.cipher_key, false); std::vector decrypted; cipher.decrypt(challenge, decrypted); auto x509 = crypto::x509(conf_intern.servercert); auto sign = crypto::signature(x509); auto serversecret = crypto::rand(16); decrypted.insert(std::end(decrypted), std::begin(sign), std::end(sign)); decrypted.insert(std::end(decrypted), std::begin(serversecret), std::end(serversecret)); auto hash = crypto::hash({(char *) decrypted.data(), decrypted.size()}); auto serverchallenge = crypto::rand(16); std::string plaintext; plaintext.reserve(hash.size() + serverchallenge.size()); plaintext.insert(std::end(plaintext), std::begin(hash), std::end(hash)); plaintext.insert(std::end(plaintext), std::begin(serverchallenge), std::end(serverchallenge)); std::vector encrypted; cipher.encrypt(plaintext, encrypted); sess.serversecret = std::move(serversecret); sess.serverchallenge = std::move(serverchallenge); tree.put("root.paired", 1); tree.put("root.challengeresponse", util::hex_vec(encrypted, true)); tree.put("root..status_code", 200); } void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const std::string &encrypted_response) { if (sess.last_phase != PAIR_PHASE::CLIENTCHALLENGE) { fail_pair(sess, tree, "Out of order call to serverchallengeresp"); return; } sess.last_phase = PAIR_PHASE::SERVERCHALLENGERESP; if (!sess.cipher_key || sess.serversecret.empty()) { fail_pair(sess, tree, "Cipher key or serversecret not set"); return; } std::vector decrypted; crypto::cipher::ecb_t cipher(*sess.cipher_key, false); cipher.decrypt(encrypted_response, decrypted); sess.clienthash = std::move(decrypted); auto serversecret = sess.serversecret; auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret); serversecret.insert(std::end(serversecret), std::begin(sign), std::end(sign)); tree.put("root.pairingsecret", util::hex_vec(serversecret, true)); tree.put("root.paired", 1); tree.put("root..status_code", 200); } void clientpairingsecret(pair_session_t &sess, pt::ptree &tree, const std::string &client_pairing_secret) { if (sess.last_phase != PAIR_PHASE::SERVERCHALLENGERESP) { fail_pair(sess, tree, "Out of order call to clientpairingsecret"); return; } sess.last_phase = PAIR_PHASE::CLIENTPAIRINGSECRET; auto &client = sess.client; if (client_pairing_secret.size() <= 16) { fail_pair(sess, tree, "Client pairing secret too short"); return; } std::string_view secret {client_pairing_secret.data(), 16}; std::string_view sign {client_pairing_secret.data() + secret.size(), client_pairing_secret.size() - secret.size()}; auto x509 = crypto::x509(client.cert); if (!x509) { fail_pair(sess, tree, "Invalid client certificate"); return; } auto x509_sign = crypto::signature(x509); std::string data; data.reserve(sess.serverchallenge.size() + x509_sign.size() + secret.size()); data.insert(std::end(data), std::begin(sess.serverchallenge), std::end(sess.serverchallenge)); data.insert(std::end(data), std::begin(x509_sign), std::end(x509_sign)); data.insert(std::end(data), std::begin(secret), std::end(secret)); auto hash = crypto::hash(data); // if hash not correct, probably MITM bool same_hash = hash.size() == sess.clienthash.size() && std::equal(hash.begin(), hash.end(), sess.clienthash.begin()); auto verify = crypto::verify256(crypto::x509(client.cert), secret, sign); if (same_hash && verify) { tree.put("root.paired", 1); auto named_cert_p = std::make_shared(); named_cert_p->name = client.name; for (char& c : named_cert_p->name) { if (c == '(') c = '['; else if (c == ')') c = ']'; } named_cert_p->cert = std::move(client.cert); named_cert_p->uuid = uuid_util::uuid_t::generate().string(); // If the device is the first one paired with the server, assign full permission. if (client_root.named_devices.empty()) { named_cert_p->perm = PERM::_all; } else { named_cert_p->perm = PERM::_default; } named_cert_p->enable_legacy_ordering = true; named_cert_p->allow_client_commands = true; named_cert_p->always_use_virtual_display = false; auto it = map_id_sess.find(client.uniqueID); map_id_sess.erase(it); add_authorized_client(named_cert_p); } else { tree.put("root.paired", 0); BOOST_LOG(warning) << "Pair attempt failed due to same_hash: " << same_hash << ", verify: " << verify; } remove_session(sess); tree.put("root..status_code", 200); } template struct tunnel; template<> struct tunnel { static auto constexpr to_string = "HTTPS"sv; }; template<> struct tunnel { static auto constexpr to_string = "NONE"sv; }; inline crypto::named_cert_t* get_verified_cert(req_https_t request) { return (crypto::named_cert_t*)request->userp.get(); } template void print_req(std::shared_ptr::Request> request) { BOOST_LOG(debug) << "TUNNEL :: "sv << tunnel::to_string; BOOST_LOG(debug) << "METHOD :: "sv << request->method; BOOST_LOG(debug) << "DESTINATION :: "sv << request->path; for (auto &[name, val] : request->header) { BOOST_LOG(debug) << name << " -- " << val; } BOOST_LOG(debug) << " [--] "sv; for (auto &[name, val] : request->parse_query_string()) { BOOST_LOG(debug) << name << " -- " << val; } BOOST_LOG(debug) << " [--] "sv; } template void not_found(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { print_req(request); pt::ptree tree; tree.put("root..status_code", 404); std::ostringstream data; pt::write_xml(data, tree); response->write(SimpleWeb::StatusCode::client_error_not_found, data.str()); response->close_connection_after_response = true; } template void pair(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { print_req(request); pt::ptree tree; auto fg = util::fail_guard([&]() { std::ostringstream data; pt::write_xml(data, tree); response->write(data.str()); response->close_connection_after_response = true; }); if (!config::sunshine.enable_pairing) { tree.put("root..status_code", 403); tree.put("root..status_message", "Pairing is disabled for this instance"); return; } auto args = request->parse_query_string(); if (args.find("uniqueid"s) == std::end(args)) { tree.put("root..status_code", 400); tree.put("root..status_message", "Missing uniqueid parameter"); return; } auto uniqID {get_arg(args, "uniqueid")}; args_t::const_iterator it; if (it = args.find("phrase"); it != std::end(args)) { if (it->second == "getservercert"sv) { pair_session_t sess; auto deviceName { get_arg(args, "devicename") }; if (deviceName == "roth"sv) { deviceName = "Legacy Moonlight Client"; } sess.client.uniqueID = std::move(uniqID); sess.client.name = std::move(deviceName); sess.client.cert = util::from_hex_vec(get_arg(args, "clientcert"), true); BOOST_LOG(debug) << sess.client.cert; auto ptr = map_id_sess.emplace(sess.client.uniqueID, std::move(sess)).first; ptr->second.async_insert_pin.salt = std::move(get_arg(args, "salt")); auto it = args.find("otpauth"); if (it != std::end(args)) { if (one_time_pin.empty() || (std::chrono::steady_clock::now() - otp_creation_time > OTP_EXPIRE_DURATION)) { one_time_pin.clear(); otp_passphrase.clear(); otp_device_name.clear(); tree.put("root..status_code", 503); tree.put("root..status_message", "OTP auth not available."); } else { auto hash = util::hex(crypto::hash(one_time_pin + ptr->second.async_insert_pin.salt + otp_passphrase), true); if (hash.to_string_view() == it->second) { if (!otp_device_name.empty()) { ptr->second.client.name = std::move(otp_device_name); } getservercert(ptr->second, tree, one_time_pin); one_time_pin.clear(); otp_passphrase.clear(); otp_device_name.clear(); return; } } // Always return positive, attackers will fail in the next steps. getservercert(ptr->second, tree, crypto::rand(16)); return; } if (config::sunshine.flags[config::flag::PIN_STDIN]) { std::string pin; std::cout << "Please insert pin: "sv; std::getline(std::cin, pin); getservercert(ptr->second, tree, pin); } else { #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_require_pin(); #endif ptr->second.async_insert_pin.response = std::move(response); fg.disable(); return; } } else if (it->second == "pairchallenge"sv) { tree.put("root.paired", 1); tree.put("root..status_code", 200); return; } } auto sess_it = map_id_sess.find(uniqID); if (sess_it == std::end(map_id_sess)) { tree.put("root..status_code", 400); tree.put("root..status_message", "Invalid uniqueid"); return; } if (it = args.find("clientchallenge"); it != std::end(args)) { auto challenge = util::from_hex_vec(it->second, true); clientchallenge(sess_it->second, tree, challenge); } else if (it = args.find("serverchallengeresp"); it != std::end(args)) { auto encrypted_response = util::from_hex_vec(it->second, true); serverchallengeresp(sess_it->second, tree, encrypted_response); } else if (it = args.find("clientpairingsecret"); it != std::end(args)) { auto pairingsecret = util::from_hex_vec(it->second, true); clientpairingsecret(sess_it->second, tree, pairingsecret); } else { tree.put("root..status_code", 404); tree.put("root..status_message", "Invalid pairing request"); } } bool pin(std::string pin, std::string name) { pt::ptree tree; if (map_id_sess.empty()) { return false; } // ensure pin is 4 digits if (pin.size() != 4) { tree.put("root.paired", 0); tree.put("root..status_code", 400); tree.put( "root..status_message", std::format("Pin must be 4 digits, {} provided", pin.size()) ); return false; } // ensure all pin characters are numeric if (!std::all_of(pin.begin(), pin.end(), ::isdigit)) { tree.put("root.paired", 0); tree.put("root..status_code", 400); tree.put("root..status_message", "Pin must be numeric"); return false; } auto &sess = std::begin(map_id_sess)->second; getservercert(sess, tree, pin); if (!name.empty()) { sess.client.name = name; } // response to the request for pin std::ostringstream data; pt::write_xml(data, tree); auto &async_response = sess.async_insert_pin.response; if (async_response.has_left() && async_response.left()) { async_response.left()->write(data.str()); } else if (async_response.has_right() && async_response.right()) { async_response.right()->write(data.str()); } else { return false; } // reset async_response async_response = std::decay_t(); // response to the current request return true; } template void serverinfo(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { print_req(request); int pair_status = 0; if constexpr (std::is_same_v) { auto args = request->parse_query_string(); auto clientID = args.find("uniqueid"s); if (clientID != std::end(args)) { pair_status = 1; } } auto local_endpoint = request->local_endpoint(); pt::ptree tree; tree.put("root..status_code", 200); tree.put("root.hostname", config::nvhttp.sunshine_name); tree.put("root.appversion", VERSION); tree.put("root.GfeVersion", GFE_VERSION); tree.put("root.uniqueid", http::unique_id); tree.put("root.HttpsPort", net::map_port(PORT_HTTPS)); tree.put("root.ExternalPort", net::map_port(PORT_HTTP)); tree.put("root.MaxLumaPixelsHEVC", video::active_hevc_mode > 1 ? "1869449984" : "0"); // Only include the MAC address for requests sent from paired clients over HTTPS. // For HTTP requests, use a placeholder MAC address that Moonlight knows to ignore. if constexpr (std::is_same_v) { tree.put("root.mac", platf::get_mac_address(net::addr_to_normalized_string(local_endpoint.address()))); auto named_cert_p = get_verified_cert(request); if (!!(named_cert_p->perm & PERM::server_cmd)) { pt::ptree& root_node = tree.get_child("root"); if (config::sunshine.server_cmds.size() > 0) { // Broadcast server_cmds for (const auto& cmd : config::sunshine.server_cmds) { pt::ptree cmd_node; cmd_node.put_value(cmd.cmd_name); root_node.push_back(std::make_pair("ServerCommand", cmd_node)); } } } else { BOOST_LOG(debug) << "Permission Get ServerCommand denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; } tree.put("root.Permission", std::to_string((uint32_t)named_cert_p->perm)); #ifdef _WIN32 tree.put("root.VirtualDisplayCapable", true); if (!!(named_cert_p->perm & PERM::_all_actions)) { tree.put("root.VirtualDisplayDriverReady", proc::vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK); } else { tree.put("root.VirtualDisplayDriverReady", true); } #endif } else { tree.put("root.mac", "00:00:00:00:00:00"); tree.put("root.Permission", "0"); } // Moonlight clients track LAN IPv6 addresses separately from LocalIP which is expected to // always be an IPv4 address. If we return that same IPv6 address here, it will clobber the // stored LAN IPv4 address. To avoid this, we need to return an IPv4 address in this field // when we get a request over IPv6. // // HACK: We should return the IPv4 address of local interface here, but we don't currently // have that implemented. For now, we will emulate the behavior of GFE+GS-IPv6-Forwarder, // which returns 127.0.0.1 as LocalIP for IPv6 connections. Moonlight clients with IPv6 // support know to ignore this bogus address. if (local_endpoint.address().is_v6() && !local_endpoint.address().to_v6().is_v4_mapped()) { tree.put("root.LocalIP", "127.0.0.1"); } else { tree.put("root.LocalIP", net::addr_to_normalized_string(local_endpoint.address())); } uint32_t codec_mode_flags = SCM_H264; if (video::last_encoder_probe_supported_yuv444_for_codec[0]) { codec_mode_flags |= SCM_H264_HIGH8_444; } if (video::active_hevc_mode >= 2) { codec_mode_flags |= SCM_HEVC; if (video::last_encoder_probe_supported_yuv444_for_codec[1]) { codec_mode_flags |= SCM_HEVC_REXT8_444; } } if (video::active_hevc_mode >= 3) { codec_mode_flags |= SCM_HEVC_MAIN10; if (video::last_encoder_probe_supported_yuv444_for_codec[1]) { codec_mode_flags |= SCM_HEVC_REXT10_444; } } if (video::active_av1_mode >= 2) { codec_mode_flags |= SCM_AV1_MAIN8; if (video::last_encoder_probe_supported_yuv444_for_codec[2]) { codec_mode_flags |= SCM_AV1_HIGH8_444; } } if (video::active_av1_mode >= 3) { codec_mode_flags |= SCM_AV1_MAIN10; if (video::last_encoder_probe_supported_yuv444_for_codec[2]) { codec_mode_flags |= SCM_AV1_HIGH10_444; } } tree.put("root.ServerCodecModeSupport", codec_mode_flags); tree.put("root.PairStatus", pair_status); if constexpr (std::is_same_v) { int current_appid = proc::proc.running(); // When input only mode is enabled, the only resume method should be launching the same app again. if (config::input.enable_input_only_mode && current_appid != proc::input_only_app_id) { current_appid = 0; } tree.put("root.currentgame", current_appid); tree.put("root.currentgameuuid", proc::proc.get_running_app_uuid()); tree.put("root.state", current_appid > 0 ? "SUNSHINE_SERVER_BUSY" : "SUNSHINE_SERVER_FREE"); } else { tree.put("root.currentgame", 0); tree.put("root.currentgameuuid", ""); tree.put("root.state", "SUNSHINE_SERVER_FREE"); } std::ostringstream data; pt::write_xml(data, tree); response->write(data.str()); response->close_connection_after_response = true; } nlohmann::json get_all_clients() { nlohmann::json named_cert_nodes = nlohmann::json::array(); client_t &client = client_root; std::list connected_uuids = rtsp_stream::get_all_session_uuids(); for (auto &named_cert : client.named_devices) { nlohmann::json named_cert_node; named_cert_node["name"] = named_cert->name; named_cert_node["uuid"] = named_cert->uuid; named_cert_node["display_mode"] = named_cert->display_mode; named_cert_node["perm"] = static_cast(named_cert->perm); named_cert_node["enable_legacy_ordering"] = named_cert->enable_legacy_ordering; named_cert_node["allow_client_commands"] = named_cert->allow_client_commands; named_cert_node["always_use_virtual_display"] = named_cert->always_use_virtual_display; // Add "do" commands if available if (!named_cert->do_cmds.empty()) { nlohmann::json do_cmds_node = nlohmann::json::array(); for (const auto &cmd : named_cert->do_cmds) { do_cmds_node.push_back(crypto::command_entry_t::serialize(cmd)); } named_cert_node["do"] = do_cmds_node; } // Add "undo" commands if available if (!named_cert->undo_cmds.empty()) { nlohmann::json undo_cmds_node = nlohmann::json::array(); for (const auto &cmd : named_cert->undo_cmds) { undo_cmds_node.push_back(crypto::command_entry_t::serialize(cmd)); } named_cert_node["undo"] = undo_cmds_node; } // Determine connection status bool connected = false; if (connected_uuids.empty()) { connected = false; } else { for (auto it = connected_uuids.begin(); it != connected_uuids.end(); ++it) { if (*it == named_cert->uuid) { connected = true; connected_uuids.erase(it); break; } } } named_cert_node["connected"] = connected; named_cert_nodes.push_back(named_cert_node); } return named_cert_nodes; } void applist(resp_https_t response, req_https_t request) { print_req(request); pt::ptree tree; auto g = util::fail_guard([&]() { std::ostringstream data; pt::write_xml(data, tree); response->write(data.str()); response->close_connection_after_response = true; }); auto &apps = tree.add_child("root", pt::ptree {}); apps.put(".status_code", 200); auto named_cert_p = get_verified_cert(request); if (!!(named_cert_p->perm & PERM::_all_actions)) { auto current_appid = proc::proc.running(); auto should_hide_inactive_apps = config::input.enable_input_only_mode && current_appid > 0 && current_appid != proc::input_only_app_id; auto app_list = proc::proc.get_apps(); bool enable_legacy_ordering = config::sunshine.legacy_ordering && named_cert_p->enable_legacy_ordering; size_t bits; if (enable_legacy_ordering) { bits = zwpad::pad_width_for_count(app_list.size()); } for (size_t i = 0; i < app_list.size(); i++) { auto& app = app_list[i]; auto appid = util::from_view(app.id); if (should_hide_inactive_apps) { if ( appid != current_appid && appid != proc::input_only_app_id && appid != proc::terminate_app_id ) { continue; } } else { if (appid == proc::terminate_app_id) { continue; } } std::string app_name; if (enable_legacy_ordering) { app_name = zwpad::pad_for_ordering(app.name, bits, i); } else { app_name = app.name; } pt::ptree app_node; app_node.put("IsHdrSupported"s, video::active_hevc_mode == 3 ? 1 : 0); app_node.put("AppTitle"s, app_name); app_node.put("UUID", app.uuid); app_node.put("IDX", app.idx); app_node.put("ID", app.id); apps.push_back(std::make_pair("App", std::move(app_node))); } } else { BOOST_LOG(debug) << "Permission ListApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; pt::ptree app_node; app_node.put("IsHdrSupported"s, 0); app_node.put("AppTitle"s, "Permission Denied"); app_node.put("UUID", ""); app_node.put("IDX", "0"); app_node.put("ID", "114514"); apps.push_back(std::make_pair("App", std::move(app_node))); return; } } void launch(bool &host_audio, resp_https_t response, req_https_t request) { print_req(request); pt::ptree tree; auto g = util::fail_guard([&]() { std::ostringstream data; pt::write_xml(data, tree); response->write(data.str()); response->close_connection_after_response = true; }); auto args = request->parse_query_string(); auto appid_str = get_arg(args, "appid", "0"); auto appuuid_str = get_arg(args, "appuuid", ""); auto appid = util::from_view(appid_str); auto current_appid = proc::proc.running(); auto current_app_uuid = proc::proc.get_running_app_uuid(); bool is_input_only = config::input.enable_input_only_mode && (appid == proc::input_only_app_id || (appuuid_str == REMOTE_INPUT_UUID)); auto named_cert_p = get_verified_cert(request); auto perm = PERM::launch; BOOST_LOG(verbose) << "Launching app [" << appid_str << "] with UUID [" << appuuid_str << "]"; // BOOST_LOG(verbose) << "QS: " << request->query_string; // If we have already launched an app, we should allow clients with view permission to join the input only or current app's session. if ( current_appid > 0 && (appuuid_str != TERMINATE_APP_UUID || appid != proc::terminate_app_id) && (is_input_only || appid == current_appid || (!appuuid_str.empty() && appuuid_str == current_app_uuid)) ) { perm = PERM::_allow_view; } if (!(named_cert_p->perm & perm)) { BOOST_LOG(debug) << "Permission LaunchApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; tree.put("root.resume", 0); tree.put("root..status_code", 403); tree.put("root..status_message", "Permission denied"); return; } if ( args.find("rikey"s) == std::end(args) || args.find("rikeyid"s) == std::end(args) || args.find("localAudioPlayMode"s) == std::end(args) || (args.find("appid"s) == std::end(args) && args.find("appuuid"s) == std::end(args)) ) { tree.put("root.resume", 0); tree.put("root..status_code", 400); tree.put("root..status_message", "Missing a required launch parameter"); return; } if (!is_input_only) { // Special handling for the "terminate" app if ( (config::input.enable_input_only_mode && appid == proc::terminate_app_id) || appuuid_str == TERMINATE_APP_UUID ) { proc::proc.terminate(); tree.put("root.resume", 0); tree.put("root..status_code", 410); tree.put("root..status_message", "App terminated."); return; } if ( current_appid > 0 && current_appid != proc::input_only_app_id && ( (appid > 0 && appid != current_appid) || (!appuuid_str.empty() && appuuid_str != current_app_uuid) ) ) { tree.put("root.resume", 0); tree.put("root..status_code", 400); tree.put("root..status_message", "An app is already running on this host"); return; } } host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); auto launch_session = make_launch_session(host_audio, is_input_only, args, named_cert_p); auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv; tree.put("root..status_code", 403); tree.put("root..status_message", "Encryption is mandatory for this host but unsupported by the client"); tree.put("root.gamesession", 0); return; } bool no_active_sessions = rtsp_stream::session_count() == 0; if (is_input_only) { BOOST_LOG(info) << "Launching input only session..."sv; launch_session->client_do_cmds.clear(); launch_session->client_undo_cmds.clear(); // Still probe encoders once, if input only session is launched first // But we're ignoring if it's successful or not if (no_active_sessions && !proc::proc.virtual_display) { video::probe_encoders(); if (current_appid == 0) { proc::proc.launch_input_only(); } } } else if (appid > 0 || !appuuid_str.empty()) { if (appid == current_appid || (!appuuid_str.empty() && appuuid_str == current_app_uuid)) { // We're basically resuming the same app BOOST_LOG(debug) << "Resuming app [" << proc::proc.get_last_run_app_name() << "] from launch app path..."; if (!proc::proc.allow_client_commands || !named_cert_p->allow_client_commands) { launch_session->client_do_cmds.clear(); launch_session->client_undo_cmds.clear(); } if (current_appid == proc::input_only_app_id) { launch_session->input_only = true; } if (no_active_sessions && !proc::proc.virtual_display) { display_device::configure_display(config::video, *launch_session); if (video::probe_encoders()) { tree.put("root.resume", 0); tree.put("root..status_code", 503); tree.put("root..status_message", "Failed to initialize video capture/encoding. Is a display connected and turned on?"); return; } } } else { const auto& apps = proc::proc.get_apps(); auto app_iter = std::find_if(apps.begin(), apps.end(), [&appid_str, &appuuid_str](const auto _app) { return _app.id == appid_str || _app.uuid == appuuid_str; }); if (app_iter == apps.end()) { BOOST_LOG(error) << "Couldn't find app with ID ["sv << appid_str << "] or UUID ["sv << appuuid_str << ']'; tree.put("root..status_code", 404); tree.put("root..status_message", "Cannot find requested application"); tree.put("root.gamesession", 0); return; } if (!app_iter->allow_client_commands) { launch_session->client_do_cmds.clear(); launch_session->client_undo_cmds.clear(); } auto err = proc::proc.execute(*app_iter, launch_session); if (err) { tree.put("root..status_code", err); tree.put( "root..status_message", err == 503 ? "Failed to initialize video capture/encoding. Is a display connected and turned on?" : "Failed to start the specified application"); tree.put("root.gamesession", 0); return; } } } else { tree.put("root..status_code", 403); tree.put("root..status_message", "How did you get here?"); tree.put("root.gamesession", 0); } tree.put("root..status_code", 200); tree.put( "root.sessionUrl0", std::format( "{}{}:{}", launch_session->rtsp_url_scheme, net::addr_to_url_escaped_string(request->local_endpoint().address()), static_cast(net::map_port(rtsp_stream::RTSP_SETUP_PORT)) ) ); tree.put("root.gamesession", 1); rtsp_stream::launch_session_raise(launch_session); } void resume(bool &host_audio, resp_https_t response, req_https_t request) { print_req(request); pt::ptree tree; auto g = util::fail_guard([&]() { std::ostringstream data; pt::write_xml(data, tree); response->write(data.str()); response->close_connection_after_response = true; }); auto named_cert_p = get_verified_cert(request); if (!(named_cert_p->perm & PERM::_allow_view)) { BOOST_LOG(debug) << "Permission ViewApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; tree.put("root.resume", 0); tree.put("root..status_code", 403); tree.put("root..status_message", "Permission denied"); return; } auto current_appid = proc::proc.running(); if (current_appid == 0) { tree.put("root.resume", 0); tree.put("root..status_code", 503); tree.put("root..status_message", "No running app to resume"); return; } auto args = request->parse_query_string(); if ( args.find("rikey"s) == std::end(args) || args.find("rikeyid"s) == std::end(args) ) { tree.put("root.resume", 0); tree.put("root..status_code", 400); tree.put("root..status_message", "Missing a required resume parameter"); return; } // Newer Moonlight clients send localAudioPlayMode on /resume too, // so we should use it if it's present in the args and there are // no active sessions we could be interfering with. const bool no_active_sessions {rtsp_stream::session_count() == 0}; if (no_active_sessions && args.find("localAudioPlayMode"s) != std::end(args)) { host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); } auto launch_session = make_launch_session(host_audio, false, args, named_cert_p); if (!proc::proc.allow_client_commands || !named_cert_p->allow_client_commands) { launch_session->client_do_cmds.clear(); launch_session->client_undo_cmds.clear(); } if (config::input.enable_input_only_mode && current_appid == proc::input_only_app_id) { launch_session->input_only = true; } if (no_active_sessions && !proc::proc.virtual_display) { // We want to prepare display only if there are no active sessions // and the current session isn't virtual display at the moment. // This should be done before probing encoders as it could change the active displays. display_device::configure_display(config::video, *launch_session); // Probe encoders again before streaming to ensure our chosen // encoder matches the active GPU (which could have changed // due to hotplugging, driver crash, primary monitor change, // or any number of other factors). if (video::probe_encoders()) { tree.put("root.resume", 0); tree.put("root..status_code", 503); tree.put("root..status_message", "Failed to initialize video capture/encoding. Is a display connected and turned on?"); return; } } auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv; tree.put("root..status_code", 403); tree.put("root..status_message", "Encryption is mandatory for this host but unsupported by the client"); tree.put("root.gamesession", 0); return; } tree.put("root..status_code", 200); tree.put( "root.sessionUrl0", std::format( "{}{}:{}", launch_session->rtsp_url_scheme, net::addr_to_url_escaped_string(request->local_endpoint().address()), static_cast(net::map_port(rtsp_stream::RTSP_SETUP_PORT)) ) ); tree.put("root.resume", 1); rtsp_stream::launch_session_raise(launch_session); #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_client_connected(named_cert_p->name); #endif } void cancel(resp_https_t response, req_https_t request) { print_req(request); pt::ptree tree; auto g = util::fail_guard([&]() { std::ostringstream data; pt::write_xml(data, tree); response->write(data.str()); response->close_connection_after_response = true; }); auto named_cert_p = get_verified_cert(request); if (!(named_cert_p->perm & PERM::launch)) { BOOST_LOG(debug) << "Permission CancelApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; tree.put("root.resume", 0); tree.put("root..status_code", 403); tree.put("root..status_message", "Permission denied"); return; } tree.put("root.cancel", 1); tree.put("root..status_code", 200); rtsp_stream::terminate_sessions(); if (proc::proc.running() > 0) { proc::proc.terminate(); } // The config needs to be reverted regardless of whether "proc::proc.terminate()" was called or not. display_device::revert_configuration(); } void appasset(resp_https_t response, req_https_t request) { print_req(request); auto fg = util::fail_guard([&]() { response->write(SimpleWeb::StatusCode::server_error_internal_server_error); response->close_connection_after_response = true; }); auto named_cert_p = get_verified_cert(request); if (!(named_cert_p->perm & PERM::_all_actions)) { BOOST_LOG(debug) << "Permission Get AppAsset denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; fg.disable(); response->write(SimpleWeb::StatusCode::client_error_unauthorized); response->close_connection_after_response = true; return; } auto args = request->parse_query_string(); auto app_image = proc::proc.get_app_image(util::from_view(get_arg(args, "appid"))); fg.disable(); std::ifstream in(app_image, std::ios::binary); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "image/png"); response->write(SimpleWeb::StatusCode::success_ok, in, headers); response->close_connection_after_response = true; } void getClipboard(resp_https_t response, req_https_t request) { print_req(request); auto named_cert_p = get_verified_cert(request); if ( !(named_cert_p->perm & PERM::_allow_view) || !(named_cert_p->perm & PERM::clipboard_read) ) { BOOST_LOG(debug) << "Permission Read Clipboard denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; response->write(SimpleWeb::StatusCode::client_error_unauthorized); response->close_connection_after_response = true; return; } auto args = request->parse_query_string(); auto clipboard_type = get_arg(args, "type"); if (clipboard_type != "text"sv) { BOOST_LOG(debug) << "Clipboard type [" << clipboard_type << "] is not supported!"; response->write(SimpleWeb::StatusCode::client_error_bad_request); response->close_connection_after_response = true; return; } std::list connected_uuids = rtsp_stream::get_all_session_uuids(); bool found = !connected_uuids.empty(); if (found) { found = (std::find(connected_uuids.begin(), connected_uuids.end(), named_cert_p->uuid) != connected_uuids.end()); } if (!found) { BOOST_LOG(debug) << "Client ["<< named_cert_p->name << "] trying to get clipboard is not connected to a stream"; response->write(SimpleWeb::StatusCode::client_error_forbidden); response->close_connection_after_response = true; return; } std::string content = platf::get_clipboard(); response->write(content); return; } void setClipboard(resp_https_t response, req_https_t request) { print_req(request); auto named_cert_p = get_verified_cert(request); if ( !(named_cert_p->perm & PERM::_allow_view) || !(named_cert_p->perm & PERM::clipboard_set) ) { BOOST_LOG(debug) << "Permission Write Clipboard denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; response->write(SimpleWeb::StatusCode::client_error_unauthorized); response->close_connection_after_response = true; return; } auto args = request->parse_query_string(); auto clipboard_type = get_arg(args, "type"); if (clipboard_type != "text"sv) { BOOST_LOG(debug) << "Clipboard type [" << clipboard_type << "] is not supported!"; response->write(SimpleWeb::StatusCode::client_error_bad_request); response->close_connection_after_response = true; return; } std::list connected_uuids = rtsp_stream::get_all_session_uuids(); bool found = !connected_uuids.empty(); if (found) { found = (std::find(connected_uuids.begin(), connected_uuids.end(), named_cert_p->uuid) != connected_uuids.end()); } if (!found) { BOOST_LOG(debug) << "Client ["<< named_cert_p->name << "] trying to set clipboard is not connected to a stream"; response->write(SimpleWeb::StatusCode::client_error_forbidden); response->close_connection_after_response = true; return; } std::string content = request->content.string(); bool success = platf::set_clipboard(content); if (!success) { BOOST_LOG(debug) << "Setting clipboard failed!"; response->write(SimpleWeb::StatusCode::server_error_internal_server_error); response->close_connection_after_response = true; } response->write(); return; } void setup(const std::string &pkey, const std::string &cert) { conf_intern.pkey = pkey; conf_intern.servercert = cert; } void start() { auto shutdown_event = mail::man->event(mail::shutdown); auto port_http = net::map_port(PORT_HTTP); auto port_https = net::map_port(PORT_HTTPS); auto address_family = net::af_from_enum_string(config::sunshine.address_family); bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE]; if (!clean_slate) { load_state(); } auto pkey = file_handler::read_file(config::nvhttp.pkey.c_str()); auto cert = file_handler::read_file(config::nvhttp.cert.c_str()); setup(pkey, cert); // resume doesn't always get the parameter "localAudioPlayMode" // launch will store it in host_audio bool host_audio {}; https_server_t https_server {config::nvhttp.cert, config::nvhttp.pkey}; http_server_t http_server; // Verify certificates after establishing connection https_server.verify = [](req_https_t req, SSL *ssl) { crypto::x509_t x509 { #if OPENSSL_VERSION_MAJOR >= 3 SSL_get1_peer_certificate(ssl) #else SSL_get_peer_certificate(ssl) #endif }; if (!x509) { BOOST_LOG(info) << "unknown -- denied"sv; return false; } bool verified = false; p_named_cert_t named_cert_p; auto fg = util::fail_guard([&]() { char subject_name[256]; X509_NAME_oneline(X509_get_subject_name(x509.get()), subject_name, sizeof(subject_name)); if (verified) { BOOST_LOG(debug) << subject_name << " -- "sv << "verified, device name: "sv << named_cert_p->name; } else { BOOST_LOG(debug) << subject_name << " -- "sv << "denied"sv; } }); auto err_str = cert_chain.verify(x509.get(), named_cert_p); if (err_str) { BOOST_LOG(warning) << "SSL Verification error :: "sv << err_str; return verified; } verified = true; req->userp = named_cert_p; return true; }; https_server.on_verify_failed = [](resp_https_t resp, req_https_t req) { pt::ptree tree; auto g = util::fail_guard([&]() { std::ostringstream data; pt::write_xml(data, tree); resp->write(data.str()); resp->close_connection_after_response = true; }); tree.put("root..status_code"s, 401); tree.put("root..query"s, req->path); tree.put("root..status_message"s, "The client is not authorized. Certificate verification failed."s); }; https_server.default_resource["GET"] = not_found; https_server.resource["^/serverinfo$"]["GET"] = serverinfo; https_server.resource["^/pair$"]["GET"] = pair; https_server.resource["^/applist$"]["GET"] = applist; https_server.resource["^/appasset$"]["GET"] = appasset; https_server.resource["^/launch$"]["GET"] = [&host_audio](auto resp, auto req) { launch(host_audio, resp, req); }; https_server.resource["^/resume$"]["GET"] = [&host_audio](auto resp, auto req) { resume(host_audio, resp, req); }; https_server.resource["^/cancel$"]["GET"] = cancel; https_server.resource["^/actions/clipboard$"]["GET"] = getClipboard; https_server.resource["^/actions/clipboard$"]["POST"] = setClipboard; https_server.config.reuse_address = true; https_server.config.address = net::af_to_any_address_string(address_family); https_server.config.port = port_https; http_server.default_resource["GET"] = not_found; http_server.resource["^/serverinfo$"]["GET"] = serverinfo; http_server.resource["^/pair$"]["GET"] = pair; http_server.config.reuse_address = true; http_server.config.address = net::af_to_any_address_string(address_family); http_server.config.port = port_http; auto accept_and_run = [&](auto *http_server) { try { http_server->start(); } catch (boost::system::system_error &err) { // It's possible the exception gets thrown after calling http_server->stop() from a different thread if (shutdown_event->peek()) { return; } BOOST_LOG(fatal) << "Couldn't start http server on ports ["sv << port_https << ", "sv << port_https << "]: "sv << err.what(); shutdown_event->raise(true); return; } }; std::thread ssl {accept_and_run, &https_server}; std::thread tcp {accept_and_run, &http_server}; // Wait for any event shutdown_event->view(); map_id_sess.clear(); https_server.stop(); http_server.stop(); ssl.join(); tcp.join(); } std::string request_otp(const std::string& passphrase, const std::string& deviceName) { if (passphrase.size() < 4) { return ""; } one_time_pin = crypto::rand_alphabet(4, "0123456789"sv); otp_passphrase = passphrase; otp_device_name = deviceName; otp_creation_time = std::chrono::steady_clock::now(); return one_time_pin; } void erase_all_clients() { client_t client; client_root = client; cert_chain.clear(); save_state(); load_state(); } void stop_session(stream::session_t& session, bool graceful) { if (graceful) { stream::session::graceful_stop(session); } else { stream::session::stop(session); } } bool find_and_stop_session(const std::string& uuid, bool graceful) { auto session = rtsp_stream::find_session(uuid); if (session) { stop_session(*session, graceful); return true; } return false; } void update_session_info(stream::session_t& session, const std::string& name, const crypto::PERM newPerm) { stream::session::update_device_info(session, name, newPerm); } bool find_and_udpate_session_info(const std::string& uuid, const std::string& name, const crypto::PERM newPerm) { auto session = rtsp_stream::find_session(uuid); if (session) { update_session_info(*session, name, newPerm); return true; } return false; } bool update_device_info( const std::string& uuid, const std::string& name, const std::string& display_mode, const cmd_list_t& do_cmds, const cmd_list_t& undo_cmds, const crypto::PERM newPerm, const bool enable_legacy_ordering, const bool allow_client_commands, const bool always_use_virtual_display ) { find_and_udpate_session_info(uuid, name, newPerm); client_t &client = client_root; auto it = client.named_devices.begin(); for (; it != client.named_devices.end(); ++it) { auto named_cert_p = *it; if (named_cert_p->uuid == uuid) { named_cert_p->name = name; named_cert_p->display_mode = display_mode; named_cert_p->perm = newPerm; named_cert_p->do_cmds = do_cmds; named_cert_p->undo_cmds = undo_cmds; named_cert_p->enable_legacy_ordering = enable_legacy_ordering; named_cert_p->allow_client_commands = allow_client_commands; named_cert_p->always_use_virtual_display = always_use_virtual_display; save_state(); return true; } } return false; } bool unpair_client(const std::string_view uuid) { bool removed = false; client_t &client = client_root; for (auto it = client.named_devices.begin(); it != client.named_devices.end();) { if ((*it)->uuid == uuid) { it = client.named_devices.erase(it); removed = true; } else { ++it; } } save_state(); load_state(); if (removed) { auto session = rtsp_stream::find_session(uuid); if (session) { stop_session(*session, true); } if (client.named_devices.empty()) { proc::proc.terminate(); } } return removed; } } // namespace nvhttp ================================================ FILE: src/nvhttp.h ================================================ /** * @file src/nvhttp.h * @brief Declarations for the nvhttp (GameStream) server. */ // macros #pragma once // standard includes #include #include #include // lib includes #include #include #include // local includes #include "crypto.h" #include "rtsp.h" #include "thread_safe.h" using namespace std::chrono_literals; /** * @brief Contains all the functions and variables related to the nvhttp (GameStream) server. */ namespace nvhttp { using args_t = SimpleWeb::CaseInsensitiveMultimap; using cmd_list_t = std::list; /** * @brief The protocol version. * @details The version of the GameStream protocol we are mocking. * @note The negative 4th number indicates to Moonlight that this is Sunshine. */ constexpr auto VERSION = "7.1.431.-1"; /** * @brief The GFE version we are replicating. */ constexpr auto GFE_VERSION = "3.23.0.74"; /** * @brief The HTTP port, as a difference from the config port. */ constexpr auto PORT_HTTP = 0; /** * @brief The HTTPS port, as a difference from the config port. */ constexpr auto PORT_HTTPS = -5; constexpr auto OTP_EXPIRE_DURATION = 180s; /** * @brief Start the nvhttp server. * @examples * nvhttp::start(); * @examples_end */ void start(); std::string get_arg(const args_t &args, const char *name, const char *default_value = nullptr); // Helper function to extract command entries cmd_list_t extract_command_entries(const nlohmann::json& j, const std::string& key); std::shared_ptr make_launch_session(bool host_audio, bool input_only, const args_t &args, const crypto::named_cert_t* named_cert_p); /** * @brief Setup the nvhttp server. * @param pkey * @param cert */ void setup(const std::string &pkey, const std::string &cert); class SunshineHTTPS: public SimpleWeb::HTTPS { public: SunshineHTTPS(boost::asio::io_context &io_context, boost::asio::ssl::context &ctx): SimpleWeb::HTTPS(io_context, ctx) { } virtual ~SunshineHTTPS() { // Gracefully shutdown the TLS connection SimpleWeb::error_code ec; shutdown(ec); } }; enum class PAIR_PHASE { NONE, ///< Sunshine is not in a pairing phase GETSERVERCERT, ///< Sunshine is in the get server certificate phase CLIENTCHALLENGE, ///< Sunshine is in the client challenge phase SERVERCHALLENGERESP, ///< Sunshine is in the server challenge response phase CLIENTPAIRINGSECRET ///< Sunshine is in the client pairing secret phase }; struct pair_session_t { struct { std::string uniqueID = {}; std::string cert = {}; std::string name = {}; } client; std::unique_ptr cipher_key = {}; std::vector clienthash = {}; std::string serversecret = {}; std::string serverchallenge = {}; struct { util::Either< std::shared_ptr::Response>, std::shared_ptr::Response>> response; std::string salt = {}; } async_insert_pin; /** * @brief used as a security measure to prevent out of order calls */ PAIR_PHASE last_phase = PAIR_PHASE::NONE; }; /** * @brief removes the temporary pairing session * @param sess */ void remove_session(const pair_session_t &sess); /** * @brief Pair, phase 1 * * Moonlight will send a salt and client certificate, we'll also need the user provided pin. * * PIN and SALT will be used to derive a shared AES key that needs to be stored * in order to be used to decrypt_symmetric in the next phases. * * At this stage we only have to send back our public certificate. */ void getservercert(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &pin); /** * @brief Pair, phase 2 * * Using the AES key that we generated in phase 1 we have to decrypt the client challenge, * * We generate a SHA256 hash with the following: * - Decrypted challenge * - Server certificate signature * - Server secret: a randomly generated secret * * The hash + server_challenge will then be AES encrypted and sent as the `challengeresponse` in the returned XML */ void clientchallenge(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &challenge); /** * @brief Pair, phase 3 * * Moonlight will send back a `serverchallengeresp`: an AES encrypted client hash, * we have to send back the `pairingsecret`: * using our private key we have to sign the certificate_signature + server_secret (generated in phase 2) */ void serverchallengeresp(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &encrypted_response); /** * @brief Pair, phase 4 (final) * * We now have to use everything we exchanged before in order to verify and finally pair the clients * * We'll check the client_hash obtained at phase 3, it should contain the following: * - The original server_challenge * - The signature of the X509 client_cert * - The unencrypted client_pairing_secret * We'll check that SHA256(server_challenge + client_public_cert_signature + client_secret) == client_hash * * Then using the client certificate public key we should be able to verify that * the client secret has been signed by Moonlight */ void clientpairingsecret(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &client_pairing_secret); /** * @brief Compare the user supplied pin to the Moonlight pin. * @param pin The user supplied pin. * @param name The user supplied name. * @return `true` if the pin is correct, `false` otherwise. * @examples * bool pin_status = nvhttp::pin("1234", "laptop"); * @examples_end */ bool pin(std::string pin, std::string name); std::string request_otp(const std::string& passphrase, const std::string& deviceName); /** * @brief Remove single client. * @param uuid The UUID of the client to remove. * @examples * nvhttp::unpair_client("4D7BB2DD-5704-A405-B41C-891A022932E1"); * @examples_end */ bool unpair_client(std::string_view uuid); /** * @brief Get all paired clients. * @return The list of all paired clients. * @examples * nlohmann::json clients = nvhttp::get_all_clients(); * @examples_end */ nlohmann::json get_all_clients(); /** * @brief Remove all paired clients. * @examples * nvhttp::erase_all_clients(); * @examples_end */ void erase_all_clients(); /** * @brief Stops a session. * * @param session The session * @param[in] graceful Whether to stop gracefully */ void stop_session(stream::session_t& session, bool graceful); /** * @brief Finds and stop session. * * @param[in] uuid The uuid string * @param[in] graceful Whether to stop gracefully */ bool find_and_stop_session(const std::string& uuid, bool graceful); /** * @brief Update device info associated to the session * * @param session The session * @param[in] name New name * @param[in] newPerm New permission */ void update_session_info(stream::session_t& session, const std::string& name, const crypto::PERM newPerm); /** * @brief Finds and udpate session information. * * @param[in] uuid The uuid string * @param[in] name New name * @param[in] newPerm New permission */ bool find_and_udpate_session_info(const std::string& uuid, const std::string& name, const crypto::PERM newPerm); /** * @brief Update device info * * @param[in] uuid The uuid string * @param[in] name New name * @param[in] do_cmds The do commands * @param[in] undo_cmds The undo commands * @param[in] newPerm New permission * @param[in] enable_legacy_ordering Enable legacy ordering * @param[in] allow_client_commands Allow client commands * @param[in] always_use_virtual_display Always use virtual display * * @return Whether the update is successful */ bool update_device_info( const std::string& uuid, const std::string& name, const std::string& display_mode, const cmd_list_t& do_cmds, const cmd_list_t& undo_cmds, const crypto::PERM newPerm, const bool enable_legacy_ordering, const bool allow_client_commands, const bool always_use_virtual_display ); } // namespace nvhttp ================================================ FILE: src/platform/common.h ================================================ /** * @file src/platform/common.h * @brief Declarations for common platform specific utilities. */ #pragma once // standard includes #include #include #include #include #include // lib includes #include #ifndef _WIN32 #include #include #include #include #endif // local includes #include "src/config.h" #include "src/logging.h" #include "src/thread_safe.h" #include "src/utility.h" #include "src/video_colorspace.h" extern "C" { #include } using namespace std::literals; struct sockaddr; struct AVFrame; struct AVBufferRef; struct AVHWFramesContext; struct AVCodecContext; struct AVDictionary; #ifdef _WIN32 // Forward declarations of boost classes to avoid having to include boost headers // here, which results in issues with Windows.h and WinSock2.h include order. namespace boost { namespace asio { namespace ip { class address; } // namespace ip } // namespace asio namespace filesystem { class path; } namespace process::inline v1 { class child; class group; template class basic_environment; typedef basic_environment environment; } // namespace process::inline v1 } // namespace boost #endif namespace video { struct config_t; } // namespace video namespace nvenc { class nvenc_base; } namespace platf { // Limited by bits in activeGamepadMask constexpr auto MAX_GAMEPADS = 16; constexpr std::uint32_t DPAD_UP = 0x0001; constexpr std::uint32_t DPAD_DOWN = 0x0002; constexpr std::uint32_t DPAD_LEFT = 0x0004; constexpr std::uint32_t DPAD_RIGHT = 0x0008; constexpr std::uint32_t START = 0x0010; constexpr std::uint32_t BACK = 0x0020; constexpr std::uint32_t LEFT_STICK = 0x0040; constexpr std::uint32_t RIGHT_STICK = 0x0080; constexpr std::uint32_t LEFT_BUTTON = 0x0100; constexpr std::uint32_t RIGHT_BUTTON = 0x0200; constexpr std::uint32_t HOME = 0x0400; constexpr std::uint32_t A = 0x1000; constexpr std::uint32_t B = 0x2000; constexpr std::uint32_t X = 0x4000; constexpr std::uint32_t Y = 0x8000; constexpr std::uint32_t PADDLE1 = 0x010000; constexpr std::uint32_t PADDLE2 = 0x020000; constexpr std::uint32_t PADDLE3 = 0x040000; constexpr std::uint32_t PADDLE4 = 0x080000; constexpr std::uint32_t TOUCHPAD_BUTTON = 0x100000; constexpr std::uint32_t MISC_BUTTON = 0x200000; struct supported_gamepad_t { std::string name; bool is_enabled; std::string reason_disabled; }; enum class gamepad_feedback_e { rumble, ///< Rumble rumble_triggers, ///< Rumble triggers set_motion_event_state, ///< Set motion event state set_rgb_led, ///< Set RGB LED set_adaptive_triggers, ///< Set adaptive triggers }; struct gamepad_feedback_msg_t { static gamepad_feedback_msg_t make_rumble(std::uint16_t id, std::uint16_t lowfreq, std::uint16_t highfreq) { gamepad_feedback_msg_t msg; msg.type = gamepad_feedback_e::rumble; msg.id = id; msg.data.rumble = {lowfreq, highfreq}; return msg; } static gamepad_feedback_msg_t make_rumble_triggers(std::uint16_t id, std::uint16_t left, std::uint16_t right) { gamepad_feedback_msg_t msg; msg.type = gamepad_feedback_e::rumble_triggers; msg.id = id; msg.data.rumble_triggers = {left, right}; return msg; } static gamepad_feedback_msg_t make_motion_event_state(std::uint16_t id, std::uint8_t motion_type, std::uint16_t report_rate) { gamepad_feedback_msg_t msg; msg.type = gamepad_feedback_e::set_motion_event_state; msg.id = id; msg.data.motion_event_state.motion_type = motion_type; msg.data.motion_event_state.report_rate = report_rate; return msg; } static gamepad_feedback_msg_t make_rgb_led(std::uint16_t id, std::uint8_t r, std::uint8_t g, std::uint8_t b) { gamepad_feedback_msg_t msg; msg.type = gamepad_feedback_e::set_rgb_led; msg.id = id; msg.data.rgb_led = {r, g, b}; return msg; } static gamepad_feedback_msg_t make_adaptive_triggers(std::uint16_t id, uint8_t event_flags, uint8_t type_left, uint8_t type_right, const std::array &left, const std::array &right) { gamepad_feedback_msg_t msg; msg.type = gamepad_feedback_e::set_adaptive_triggers; msg.id = id; msg.data.adaptive_triggers = {.event_flags = event_flags, .type_left = type_left, .type_right = type_right, .left = left, .right = right}; return msg; } gamepad_feedback_e type; std::uint16_t id; union { struct { std::uint16_t lowfreq; std::uint16_t highfreq; } rumble; struct { std::uint16_t left_trigger; std::uint16_t right_trigger; } rumble_triggers; struct { std::uint16_t report_rate; std::uint8_t motion_type; } motion_event_state; struct { std::uint8_t r; std::uint8_t g; std::uint8_t b; } rgb_led; struct { uint16_t controllerNumber; uint8_t event_flags; uint8_t type_left; uint8_t type_right; std::array left; std::array right; } adaptive_triggers; } data; }; using feedback_queue_t = safe::mail_raw_t::queue_t; namespace speaker { enum speaker_e { FRONT_LEFT, ///< Front left FRONT_RIGHT, ///< Front right FRONT_CENTER, ///< Front center LOW_FREQUENCY, ///< Low frequency BACK_LEFT, ///< Back left BACK_RIGHT, ///< Back right SIDE_LEFT, ///< Side left SIDE_RIGHT, ///< Side right MAX_SPEAKERS, ///< Maximum number of speakers }; constexpr std::uint8_t map_stereo[] { FRONT_LEFT, FRONT_RIGHT }; constexpr std::uint8_t map_surround51[] { FRONT_LEFT, FRONT_RIGHT, FRONT_CENTER, LOW_FREQUENCY, BACK_LEFT, BACK_RIGHT, }; constexpr std::uint8_t map_surround71[] { FRONT_LEFT, FRONT_RIGHT, FRONT_CENTER, LOW_FREQUENCY, BACK_LEFT, BACK_RIGHT, SIDE_LEFT, SIDE_RIGHT, }; } // namespace speaker enum class mem_type_e { system, ///< System memory vaapi, ///< VAAPI dxgi, ///< DXGI cuda, ///< CUDA videotoolbox, ///< VideoToolbox unknown ///< Unknown }; enum class pix_fmt_e { yuv420p, ///< YUV 4:2:0 yuv420p10, ///< YUV 4:2:0 10-bit nv12, ///< NV12 p010, ///< P010 ayuv, ///< AYUV yuv444p16, ///< Planar 10-bit (shifted to 16-bit) YUV 4:4:4 y410, ///< Y410 unknown ///< Unknown }; inline std::string_view from_pix_fmt(pix_fmt_e pix_fmt) { using namespace std::literals; #define _CONVERT(x) \ case pix_fmt_e::x: \ return #x##sv switch (pix_fmt) { _CONVERT(yuv420p); _CONVERT(yuv420p10); _CONVERT(nv12); _CONVERT(p010); _CONVERT(ayuv); _CONVERT(yuv444p16); _CONVERT(y410); _CONVERT(unknown); } #undef _CONVERT return "unknown"sv; } // Dimensions for touchscreen input struct touch_port_t { int offset_x, offset_y; int width, height; }; // These values must match Limelight-internal.h's SS_FF_* constants! namespace platform_caps { typedef uint32_t caps_t; constexpr caps_t pen_touch = 0x01; // Pen and touch events constexpr caps_t controller_touch = 0x02; // Controller touch events }; // namespace platform_caps struct gamepad_state_t { std::uint32_t buttonFlags; std::uint8_t lt; std::uint8_t rt; std::int16_t lsX; std::int16_t lsY; std::int16_t rsX; std::int16_t rsY; }; struct gamepad_id_t { // The global index is used when looking up gamepads in the platform's // gamepad array. It identifies gamepads uniquely among all clients. int globalIndex; // The client-relative index is the controller number as reported by the // client. It must be used when communicating back to the client via // the input feedback queue. std::uint8_t clientRelativeIndex; }; struct gamepad_arrival_t { std::uint8_t type; std::uint16_t capabilities; std::uint32_t supportedButtons; }; struct gamepad_touch_t { gamepad_id_t id; std::uint8_t eventType; std::uint32_t pointerId; float x; float y; float pressure; }; struct gamepad_motion_t { gamepad_id_t id; std::uint8_t motionType; // Accel: m/s^2 // Gyro: deg/s float x; float y; float z; }; struct gamepad_battery_t { gamepad_id_t id; std::uint8_t state; std::uint8_t percentage; }; struct touch_input_t { std::uint8_t eventType; std::uint16_t rotation; // Degrees (0..360) or LI_ROT_UNKNOWN std::uint32_t pointerId; float x; float y; float pressureOrDistance; // Distance for hover and pressure for contact float contactAreaMajor; float contactAreaMinor; }; struct pen_input_t { std::uint8_t eventType; std::uint8_t toolType; std::uint8_t penButtons; std::uint8_t tilt; // Degrees (0..90) or LI_TILT_UNKNOWN std::uint16_t rotation; // Degrees (0..360) or LI_ROT_UNKNOWN float x; float y; float pressureOrDistance; // Distance for hover and pressure for contact float contactAreaMajor; float contactAreaMinor; }; class deinit_t { public: virtual ~deinit_t() = default; }; struct img_t: std::enable_shared_from_this { public: img_t() = default; img_t(img_t &&) = delete; img_t(const img_t &) = delete; img_t &operator=(img_t &&) = delete; img_t &operator=(const img_t &) = delete; std::uint8_t *data {}; std::int32_t width {}; std::int32_t height {}; std::int32_t pixel_pitch {}; std::int32_t row_pitch {}; std::optional frame_timestamp; virtual ~img_t() = default; }; struct sink_t { // Play on host PC std::string host; // On macOS and Windows, it is not possible to create a virtual sink // Therefore, it is optional struct null_t { std::string stereo; std::string surround51; std::string surround71; }; std::optional null; }; struct encode_device_t { virtual ~encode_device_t() = default; virtual int convert(platf::img_t &img) = 0; video::sunshine_colorspace_t colorspace; }; struct avcodec_encode_device_t: encode_device_t { void *data {}; AVFrame *frame {}; int convert(platf::img_t &img) override { return -1; } virtual void apply_colorspace() { } /** * @brief Set the frame to be encoded. * @note Implementations must take ownership of 'frame'. */ virtual int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { BOOST_LOG(error) << "Illegal call to hwdevice_t::set_frame(). Did you forget to override it?"; return -1; }; /** * @brief Initialize the hwframes context. * @note Implementations may set parameters during initialization of the hwframes context. */ virtual void init_hwframes(AVHWFramesContext *frames) {}; /** * @brief Provides a hook for allow platform-specific code to adjust codec options. * @note Implementations may set or modify codec options prior to codec initialization. */ virtual void init_codec_options(AVCodecContext *ctx, AVDictionary **options) {}; /** * @brief Prepare to derive a context. * @note Implementations may make modifications required before context derivation */ virtual int prepare_to_derive_context(int hw_device_type) { return 0; }; }; struct nvenc_encode_device_t: encode_device_t { virtual bool init_encoder(const video::config_t &client_config, const video::sunshine_colorspace_t &colorspace) = 0; nvenc::nvenc_base *nvenc = nullptr; }; enum class capture_e : int { ok, ///< Success reinit, ///< Need to reinitialize timeout, ///< Timeout interrupted, ///< Capture was interrupted error ///< Error }; class display_t { public: /** * @brief Callback for when a new image is ready. * When display has a new image ready or a timeout occurs, this callback will be called with the image. * If a frame was captured, frame_captured will be true. If a timeout occurred, it will be false. * @retval true On success * @retval false On break request */ using push_captured_image_cb_t = std::function &&img, bool frame_captured)>; /** * @brief Get free image from pool. * Calls must be synchronized. * Blocks until there is free image in the pool or capture is interrupted. * @retval true On success, img_out contains free image * @retval false When capture has been interrupted, img_out contains nullptr */ using pull_free_image_cb_t = std::function &img_out)>; display_t() noexcept: offset_x {0}, offset_y {0} { } /** * @brief Capture a frame. * @param push_captured_image_cb The callback that is called with captured image, * must be called from the same thread as capture() * @param pull_free_image_cb Capture backends call this callback to get empty image from the pool. * If backend uses multiple threads, calls to this callback must be synchronized. * Calls to this callback and push_captured_image_cb must be synchronized as well. * @param cursor A pointer to the flag that indicates whether the cursor should be captured as well. * @retval capture_e::ok When stopping * @retval capture_e::error On error * @retval capture_e::reinit When need of reinitialization */ virtual capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) = 0; virtual std::shared_ptr alloc_img() = 0; virtual int dummy_img(img_t *img) = 0; virtual std::unique_ptr make_avcodec_encode_device(pix_fmt_e pix_fmt) { return nullptr; } virtual std::unique_ptr make_nvenc_encode_device(pix_fmt_e pix_fmt) { return nullptr; } virtual bool is_hdr() { return false; } virtual bool get_hdr_metadata(SS_HDR_METADATA &metadata) { std::memset(&metadata, 0, sizeof(metadata)); return false; } /** * @brief Check that a given codec is supported by the display device. * @param name The FFmpeg codec name (or similar for non-FFmpeg codecs). * @param config The codec configuration. * @return `true` if supported, `false` otherwise. */ virtual bool is_codec_supported(std::string_view name, const ::video::config_t &config) { return true; } virtual ~display_t() = default; // Offsets for when streaming a specific monitor. By default, they are 0. int offset_x, offset_y; int env_width, env_height; int width, height; protected: // collect capture timing data (at loglevel debug) logging::time_delta_periodic_logger sleep_overshoot_logger = {debug, "Frame capture sleep overshoot"}; }; class mic_t { public: virtual capture_e sample(std::vector &frame_buffer) = 0; virtual ~mic_t() = default; }; class audio_control_t { public: virtual int set_sink(const std::string &sink) = 0; virtual std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) = 0; /** * @brief Check if the audio sink is available in the system. * @param sink Sink to be checked. * @returns True if available, false otherwise. */ virtual bool is_sink_available(const std::string &sink) = 0; virtual std::optional sink_info() = 0; virtual ~audio_control_t() = default; }; void freeInput(void *); using input_t = util::safe_ptr; std::filesystem::path appdata(); std::string get_mac_address(const std::string_view &address); std::string get_local_ip_for_gateway(); std::string from_sockaddr(const sockaddr *const); std::pair from_sockaddr_ex(const sockaddr *const); std::unique_ptr audio_control(); /** * @brief Get the display_t instance for the given hwdevice_type. * If display_name is empty, use the first monitor that's compatible you can find * If you require to use this parameter in a separate thread, make a copy of it. * @param display_name The name of the monitor that SHOULD be displayed * @param config Stream configuration * @return The display_t instance based on hwdevice_type. */ std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); // A list of names of displays accepted as display_name with the mem_type_e std::vector display_names(mem_type_e hwdevice_type); /** * @brief Check if GPUs/drivers have changed since the last call to this function. * @return `true` if a change has occurred or if it is unknown whether a change occurred. */ bool needs_encoder_reenumeration(); boost::process::v1::child run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const boost::process::v1::environment &env, FILE *file, std::error_code &ec, boost::process::v1::group *group); enum class thread_priority_e : int { low, ///< Low priority normal, ///< Normal priority high, ///< High priority critical ///< Critical priority }; void adjust_thread_priority(thread_priority_e priority); // Allow OS-specific actions to be taken to prepare for streaming void streaming_will_start(); void streaming_will_stop(); void restart(); /** * @brief Set an environment variable. * @param name The name of the environment variable. * @param value The value to set the environment variable to. * @return 0 on success, non-zero on failure. */ int set_env(const std::string &name, const std::string &value); /** * @brief Unset an environment variable. * @param name The name of the environment variable. * @return 0 on success, non-zero on failure. */ int unset_env(const std::string &name); struct buffer_descriptor_t { const char *buffer; size_t size; // Constructors required for emplace_back() prior to C++20 buffer_descriptor_t(const char *buffer, size_t size): buffer(buffer), size(size) { } buffer_descriptor_t(): buffer(nullptr), size(0) { } }; struct batched_send_info_t { // Optional headers to be prepended to each packet const char *headers; size_t header_size; // One or more data buffers to use for the payloads // // NB: Data buffers must be aligned to payload size! std::vector &payload_buffers; size_t payload_size; // The offset (in header+payload message blocks) in the header and payload // buffers to begin sending messages from size_t block_offset; // The number of header+payload message blocks to send size_t block_count; std::uintptr_t native_socket; boost::asio::ip::address &target_address; uint16_t target_port; boost::asio::ip::address &source_address; /** * @brief Returns a payload buffer descriptor for the given payload offset. * @param offset The offset in the total payload data (bytes). * @return Buffer descriptor describing the region at the given offset. */ buffer_descriptor_t buffer_for_payload_offset(ptrdiff_t offset) { for (const auto &desc : payload_buffers) { if (offset < desc.size) { return { desc.buffer + offset, desc.size - offset, }; } else { offset -= desc.size; } } return {}; } }; bool send_batch(batched_send_info_t &send_info); struct send_info_t { const char *header; size_t header_size; const char *payload; size_t payload_size; std::uintptr_t native_socket; boost::asio::ip::address &target_address; uint16_t target_port; boost::asio::ip::address &source_address; }; bool send(send_info_t &send_info); enum class qos_data_type_e : int { audio, ///< Audio video ///< Video }; /** * @brief Enable QoS on the given socket for traffic to the specified destination. * @param native_socket The native socket handle. * @param address The destination address for traffic sent on this socket. * @param port The destination port for traffic sent on this socket. * @param data_type The type of traffic sent on this socket. * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic. */ std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging); /** * @brief Open a url in the default web browser. * @param url The url to open. */ void open_url(const std::string &url); /** * @brief Attempt to gracefully terminate a process group. * @param native_handle The native handle of the process group. * @return `true` if termination was successfully requested. */ bool request_process_group_exit(std::uintptr_t native_handle); /** * @brief Check if a process group still has running children. * @param native_handle The native handle of the process group. * @return `true` if processes are still running. */ bool process_group_running(std::uintptr_t native_handle); input_t input(); /** * @brief Get the current mouse position on screen * @param input The input_t instance to use. * @return Screen coordinates of the mouse. * @examples * auto [x, y] = get_mouse_loc(input); * @examples_end */ util::point_t get_mouse_loc(input_t &input); void move_mouse(input_t &input, int deltaX, int deltaY); void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y); void button_mouse(input_t &input, int button, bool release); void scroll(input_t &input, int distance); void hscroll(input_t &input, int distance); void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags); void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state); void unicode(input_t &input, char *utf8, int size); typedef deinit_t client_input_t; /** * @brief Allocate a context to store per-client input data. * @param input The global input context. * @return A unique pointer to a per-client input data context. */ std::unique_ptr allocate_client_input_context(input_t &input); /** * @brief Send a touch event to the OS. * @param input The client-specific input context. * @param touch_port The current viewport for translating to screen coordinates. * @param touch The touch event. */ void touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch); /** * @brief Send a pen event to the OS. * @param input The client-specific input context. * @param touch_port The current viewport for translating to screen coordinates. * @param pen The pen event. */ void pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen); /** * @brief Send a gamepad touch event to the OS. * @param input The global input context. * @param touch The touch event. */ void gamepad_touch(input_t &input, const gamepad_touch_t &touch); /** * @brief Send a gamepad motion event to the OS. * @param input The global input context. * @param motion The motion event. */ void gamepad_motion(input_t &input, const gamepad_motion_t &motion); /** * @brief Send a gamepad battery event to the OS. * @param input The global input context. * @param battery The battery event. */ void gamepad_battery(input_t &input, const gamepad_battery_t &battery); /** * @brief Create a new virtual gamepad. * @param input The global input context. * @param id The gamepad ID. * @param metadata Controller metadata from client (empty if none provided). * @param feedback_queue The queue for posting messages back to the client. * @return 0 on success. */ int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue); void free_gamepad(input_t &input, int nr); /** * @brief Get the supported platform capabilities to advertise to the client. * @return Capability flags. */ platform_caps::caps_t get_capabilities(); #define SERVICE_NAME "Apollo" #define SERVICE_TYPE "_nvstream._tcp" namespace publish { [[nodiscard]] std::unique_ptr start(); } [[nodiscard]] std::unique_ptr init(); /** * @brief Returns the current computer name in UTF-8. * @return Computer name or a placeholder upon failure. */ std::string get_host_name(); /** * @brief Gets the supported gamepads for this platform backend. * @details This may be called prior to `platf::input()`! * @param input Pointer to the platform's `input_t` or `nullptr`. * @return Vector of gamepad options and status. */ std::vector &supported_gamepads(input_t *input); struct high_precision_timer: private boost::noncopyable { virtual ~high_precision_timer() = default; /** * @brief Sleep for the duration * @param duration Sleep duration */ virtual void sleep_for(const std::chrono::nanoseconds &duration) = 0; /** * @brief Check if platform-specific timer backend has been initialized successfully * @return `true` on success, `false` on error */ virtual operator bool() = 0; }; /** * @brief Create platform-specific timer capable of high-precision sleep * @return A unique pointer to timer */ std::unique_ptr create_high_precision_timer(); std::string get_clipboard(); bool set_clipboard(const std::string& content); } // namespace platf ================================================ FILE: src/platform/linux/audio.cpp ================================================ /** * @file src/platform/linux/audio.cpp * @brief Definitions for audio control on Linux. */ // standard includes #include #include #include // lib includes #include #include #include #include // local includes #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/thread_safe.h" namespace platf { using namespace std::literals; constexpr pa_channel_position_t position_mapping[] { PA_CHANNEL_POSITION_FRONT_LEFT, PA_CHANNEL_POSITION_FRONT_RIGHT, PA_CHANNEL_POSITION_FRONT_CENTER, PA_CHANNEL_POSITION_LFE, PA_CHANNEL_POSITION_REAR_LEFT, PA_CHANNEL_POSITION_REAR_RIGHT, PA_CHANNEL_POSITION_SIDE_LEFT, PA_CHANNEL_POSITION_SIDE_RIGHT, }; std::string to_string(const char *name, const std::uint8_t *mapping, int channels) { std::stringstream ss; ss << "rate=48000 sink_name="sv << name << " format=float channels="sv << channels << " channel_map="sv; std::for_each_n(mapping, channels - 1, [&ss](std::uint8_t pos) { ss << pa_channel_position_to_string(position_mapping[pos]) << ','; }); ss << pa_channel_position_to_string(position_mapping[mapping[channels - 1]]); ss << " sink_properties=device.description="sv << name; auto result = ss.str(); BOOST_LOG(debug) << "null-sink args: "sv << result; return result; } struct mic_attr_t: public mic_t { util::safe_ptr mic; capture_e sample(std::vector &sample_buf) override { auto sample_size = sample_buf.size(); auto buf = sample_buf.data(); int status; if (pa_simple_read(mic.get(), buf, sample_size * sizeof(float), &status)) { BOOST_LOG(error) << "pa_simple_read() failed: "sv << pa_strerror(status); return capture_e::error; } return capture_e::ok; } }; std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, std::string source_name) { auto mic = std::make_unique(); pa_sample_spec ss {PA_SAMPLE_FLOAT32, sample_rate, (std::uint8_t) channels}; pa_channel_map pa_map; pa_map.channels = channels; std::for_each_n(pa_map.map, pa_map.channels, [mapping](auto &channel) mutable { channel = position_mapping[*mapping++]; }); pa_buffer_attr pa_attr = { .maxlength = uint32_t(-1), .tlength = uint32_t(-1), .prebuf = uint32_t(-1), .minreq = uint32_t(-1), .fragsize = uint32_t(frame_size * channels * sizeof(float)) }; int status; mic->mic.reset( pa_simple_new(nullptr, "sunshine", pa_stream_direction_t::PA_STREAM_RECORD, source_name.c_str(), "sunshine-record", &ss, &pa_map, &pa_attr, &status) ); if (!mic->mic) { auto err_str = pa_strerror(status); BOOST_LOG(error) << "pa_simple_new() failed: "sv << err_str; return nullptr; } return mic; } namespace pa { template struct add_const_helper; template struct add_const_helper { using type = const std::remove_pointer_t *; }; template struct add_const_helper { using type = const T *; }; template using add_const_t = typename add_const_helper, T>::type; template void pa_free(T *p) { pa_xfree(p); } using ctx_t = util::safe_ptr; using loop_t = util::safe_ptr; using op_t = util::safe_ptr; using string_t = util::safe_ptr>; template using cb_simple_t = std::function i)>; template void cb(ctx_t::pointer ctx, add_const_t i, void *userdata) { auto &f = *(cb_simple_t *) userdata; // Cannot similarly filter on eol here. Unless reported otherwise assume // we have no need for special filtering like cb? f(ctx, i); } template using cb_t = std::function i, int eol)>; template void cb(ctx_t::pointer ctx, add_const_t i, int eol, void *userdata) { auto &f = *(cb_t *) userdata; // For some reason, pulseaudio calls this callback after disconnecting if (i && eol) { return; } f(ctx, i, eol); } void cb_i(ctx_t::pointer ctx, std::uint32_t i, void *userdata) { auto alarm = (safe::alarm_raw_t *) userdata; alarm->ring(i); } void ctx_state_cb(ctx_t::pointer ctx, void *userdata) { auto &f = *(std::function *) userdata; f(ctx); } void success_cb(ctx_t::pointer ctx, int status, void *userdata) { assert(userdata != nullptr); auto alarm = (safe::alarm_raw_t *) userdata; alarm->ring(status ? 0 : 1); } class server_t: public audio_control_t { enum ctx_event_e : int { ready, terminated, failed }; public: loop_t loop; ctx_t ctx; std::string requested_sink; struct { std::uint32_t stereo = PA_INVALID_INDEX; std::uint32_t surround51 = PA_INVALID_INDEX; std::uint32_t surround71 = PA_INVALID_INDEX; } index; std::unique_ptr> events; std::unique_ptr> events_cb; std::thread worker; int init() { events = std::make_unique>(); loop.reset(pa_mainloop_new()); ctx.reset(pa_context_new(pa_mainloop_get_api(loop.get()), "sunshine")); events_cb = std::make_unique>([this](ctx_t::pointer ctx) { switch (pa_context_get_state(ctx)) { case PA_CONTEXT_READY: events->raise(ready); break; case PA_CONTEXT_TERMINATED: BOOST_LOG(debug) << "Pulseadio context terminated"sv; events->raise(terminated); break; case PA_CONTEXT_FAILED: BOOST_LOG(debug) << "Pulseadio context failed"sv; events->raise(failed); break; case PA_CONTEXT_CONNECTING: BOOST_LOG(debug) << "Connecting to pulseaudio"sv; case PA_CONTEXT_UNCONNECTED: case PA_CONTEXT_AUTHORIZING: case PA_CONTEXT_SETTING_NAME: break; } }); pa_context_set_state_callback(ctx.get(), ctx_state_cb, events_cb.get()); auto status = pa_context_connect(ctx.get(), nullptr, PA_CONTEXT_NOFLAGS, nullptr); if (status) { BOOST_LOG(error) << "Couldn't connect to pulseaudio: "sv << pa_strerror(status); return -1; } worker = std::thread { [](loop_t::pointer loop) { int retval; auto status = pa_mainloop_run(loop, &retval); if (status < 0) { BOOST_LOG(error) << "Couldn't run pulseaudio main loop"sv; return; } }, loop.get() }; auto event = events->pop(); if (event == failed) { return -1; } return 0; } int load_null(const char *name, const std::uint8_t *channel_mapping, int channels) { auto alarm = safe::make_alarm(); op_t op { pa_context_load_module( ctx.get(), "module-null-sink", to_string(name, channel_mapping, channels).c_str(), cb_i, alarm.get() ), }; alarm->wait(); return *alarm->status(); } int unload_null(std::uint32_t i) { if (i == PA_INVALID_INDEX) { return 0; } auto alarm = safe::make_alarm(); op_t op { pa_context_unload_module(ctx.get(), i, success_cb, alarm.get()) }; alarm->wait(); if (*alarm->status()) { BOOST_LOG(error) << "Couldn't unload null-sink with index ["sv << i << "]: "sv << pa_strerror(pa_context_errno(ctx.get())); return -1; } return 0; } std::optional sink_info() override { constexpr auto stereo = "sink-sunshine-stereo"; constexpr auto surround51 = "sink-sunshine-surround51"; constexpr auto surround71 = "sink-sunshine-surround71"; auto alarm = safe::make_alarm(); sink_t sink; // Count of all virtual sinks that are created by us int nullcount = 0; cb_t f = [&](ctx_t::pointer ctx, const pa_sink_info *sink_info, int eol) { if (!sink_info) { if (!eol) { BOOST_LOG(error) << "Couldn't get pulseaudio sink info: "sv << pa_strerror(pa_context_errno(ctx)); alarm->ring(-1); } alarm->ring(0); return; } // Ensure Sunshine won't create a sink that already exists. if (!std::strcmp(sink_info->name, stereo)) { index.stereo = sink_info->owner_module; ++nullcount; } else if (!std::strcmp(sink_info->name, surround51)) { index.surround51 = sink_info->owner_module; ++nullcount; } else if (!std::strcmp(sink_info->name, surround71)) { index.surround71 = sink_info->owner_module; ++nullcount; } }; op_t op {pa_context_get_sink_info_list(ctx.get(), cb, &f)}; if (!op) { BOOST_LOG(error) << "Couldn't create card info operation: "sv << pa_strerror(pa_context_errno(ctx.get())); return std::nullopt; } alarm->wait(); if (*alarm->status()) { return std::nullopt; } auto sink_name = get_default_sink_name(); sink.host = sink_name; if (index.stereo == PA_INVALID_INDEX) { index.stereo = load_null(stereo, speaker::map_stereo, sizeof(speaker::map_stereo)); if (index.stereo == PA_INVALID_INDEX) { BOOST_LOG(warning) << "Couldn't create virtual sink for stereo: "sv << pa_strerror(pa_context_errno(ctx.get())); } else { ++nullcount; } } if (index.surround51 == PA_INVALID_INDEX) { index.surround51 = load_null(surround51, speaker::map_surround51, sizeof(speaker::map_surround51)); if (index.surround51 == PA_INVALID_INDEX) { BOOST_LOG(warning) << "Couldn't create virtual sink for surround-51: "sv << pa_strerror(pa_context_errno(ctx.get())); } else { ++nullcount; } } if (index.surround71 == PA_INVALID_INDEX) { index.surround71 = load_null(surround71, speaker::map_surround71, sizeof(speaker::map_surround71)); if (index.surround71 == PA_INVALID_INDEX) { BOOST_LOG(warning) << "Couldn't create virtual sink for surround-71: "sv << pa_strerror(pa_context_errno(ctx.get())); } else { ++nullcount; } } if (sink_name.empty()) { BOOST_LOG(warning) << "Couldn't find an active default sink. Continuing with virtual audio only."sv; } if (nullcount == 3) { sink.null = std::make_optional(sink_t::null_t {stereo, surround51, surround71}); } return std::make_optional(std::move(sink)); } std::string get_default_sink_name() { std::string sink_name; auto alarm = safe::make_alarm(); cb_simple_t server_f = [&](ctx_t::pointer ctx, const pa_server_info *server_info) { if (!server_info) { BOOST_LOG(error) << "Couldn't get pulseaudio server info: "sv << pa_strerror(pa_context_errno(ctx)); alarm->ring(-1); } if (server_info->default_sink_name) { sink_name = server_info->default_sink_name; } alarm->ring(0); }; op_t server_op {pa_context_get_server_info(ctx.get(), cb, &server_f)}; alarm->wait(); // No need to check status. If it failed just return default name. return sink_name; } std::string get_monitor_name(const std::string &sink_name) { std::string monitor_name; auto alarm = safe::make_alarm(); if (sink_name.empty()) { return monitor_name; } cb_t sink_f = [&](ctx_t::pointer ctx, const pa_sink_info *sink_info, int eol) { if (!sink_info) { if (!eol) { BOOST_LOG(error) << "Couldn't get pulseaudio sink info for ["sv << sink_name << "]: "sv << pa_strerror(pa_context_errno(ctx)); alarm->ring(-1); } alarm->ring(0); return; } monitor_name = sink_info->monitor_source_name; }; op_t sink_op {pa_context_get_sink_info_by_name(ctx.get(), sink_name.c_str(), cb, &sink_f)}; alarm->wait(); // No need to check status. If it failed just return default name. BOOST_LOG(info) << "Found default monitor by name: "sv << monitor_name; return monitor_name; } std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override { // Sink choice priority: // 1. Config sink // 2. Last sink swapped to (Usually virtual in this case) // 3. Default Sink // An attempt was made to always use default to match the switching mechanic, // but this happens right after the swap so the default returned by PA was not // the new one just set! auto sink_name = config::audio.sink; if (sink_name.empty()) { sink_name = requested_sink; } if (sink_name.empty()) { sink_name = get_default_sink_name(); } return ::platf::microphone(mapping, channels, sample_rate, frame_size, get_monitor_name(sink_name)); } bool is_sink_available(const std::string &sink) override { BOOST_LOG(warning) << "audio_control_t::is_sink_available() unimplemented: "sv << sink; return true; } int set_sink(const std::string &sink) override { auto alarm = safe::make_alarm(); BOOST_LOG(info) << "Setting default sink to: ["sv << sink << "]"sv; op_t op { pa_context_set_default_sink( ctx.get(), sink.c_str(), success_cb, alarm.get() ), }; if (!op) { BOOST_LOG(error) << "Couldn't create set default-sink operation: "sv << pa_strerror(pa_context_errno(ctx.get())); return -1; } alarm->wait(); if (*alarm->status()) { BOOST_LOG(error) << "Couldn't set default-sink ["sv << sink << "]: "sv << pa_strerror(pa_context_errno(ctx.get())); return -1; } requested_sink = sink; return 0; } ~server_t() override { unload_null(index.stereo); unload_null(index.surround51); unload_null(index.surround71); if (worker.joinable()) { pa_context_disconnect(ctx.get()); KITTY_WHILE_LOOP(auto event = events->pop(), event != terminated && event != failed, { event = events->pop(); }) pa_mainloop_quit(loop.get(), 0); worker.join(); } } }; } // namespace pa std::unique_ptr audio_control() { auto audio = std::make_unique(); if (audio->init()) { return nullptr; } return audio; } } // namespace platf ================================================ FILE: src/platform/linux/cuda.cpp ================================================ /** * @file src/platform/linux/cuda.cpp * @brief Definitions for CUDA encoding. */ // standard includes #include #include #include #include // lib includes #include #include extern "C" { #include #include #include } // local includes #include "cuda.h" #include "graphics.h" #include "src/logging.h" #include "src/utility.h" #include "src/video.h" #include "wayland.h" #define SUNSHINE_STRINGVIEW_HELPER(x) x##sv #define SUNSHINE_STRINGVIEW(x) SUNSHINE_STRINGVIEW_HELPER(x) #define CU_CHECK(x, y) \ if (check((x), SUNSHINE_STRINGVIEW(y ": "))) \ return -1 #define CU_CHECK_IGNORE(x, y) \ check((x), SUNSHINE_STRINGVIEW(y ": ")) namespace fs = std::filesystem; using namespace std::literals; namespace cuda { constexpr auto cudaDevAttrMaxThreadsPerBlock = (CUdevice_attribute) 1; constexpr auto cudaDevAttrMaxThreadsPerMultiProcessor = (CUdevice_attribute) 39; void pass_error(const std::string_view &sv, const char *name, const char *description) { BOOST_LOG(error) << sv << name << ':' << description; } void cff(CudaFunctions *cf) { cuda_free_functions(&cf); } using cdf_t = util::safe_ptr; static cdf_t cdf; inline static int check(CUresult result, const std::string_view &sv) { if (result != CUDA_SUCCESS) { const char *name; const char *description; cdf->cuGetErrorName(result, &name); cdf->cuGetErrorString(result, &description); BOOST_LOG(error) << sv << name << ':' << description; return -1; } return 0; } void freeStream(CUstream stream) { CU_CHECK_IGNORE(cdf->cuStreamDestroy(stream), "Couldn't destroy cuda stream"); } void unregisterResource(CUgraphicsResource resource) { CU_CHECK_IGNORE(cdf->cuGraphicsUnregisterResource(resource), "Couldn't unregister resource"); } using registered_resource_t = util::safe_ptr; class img_t: public platf::img_t { public: tex_t tex; }; int init() { auto status = cuda_load_functions(&cdf, nullptr); if (status) { BOOST_LOG(error) << "Couldn't load cuda: "sv << status; return -1; } CU_CHECK(cdf->cuInit(0), "Couldn't initialize cuda"); return 0; } class cuda_t: public platf::avcodec_encode_device_t { public: int init(int in_width, int in_height) { if (!cdf) { BOOST_LOG(warning) << "cuda not initialized"sv; return -1; } data = (void *) 0x1; width = in_width; height = in_height; return 0; } int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { this->hwframe.reset(frame); this->frame = frame; auto hwframe_ctx = (AVHWFramesContext *) hw_frames_ctx->data; if (hwframe_ctx->sw_format != AV_PIX_FMT_NV12) { BOOST_LOG(error) << "cuda::cuda_t doesn't support any format other than AV_PIX_FMT_NV12"sv; return -1; } if (!frame->buf[0]) { if (av_hwframe_get_buffer(hw_frames_ctx, frame, 0)) { BOOST_LOG(error) << "Couldn't get hwframe for NVENC"sv; return -1; } } auto cuda_ctx = (AVCUDADeviceContext *) hwframe_ctx->device_ctx->hwctx; stream = make_stream(); if (!stream) { return -1; } cuda_ctx->stream = stream.get(); auto sws_opt = sws_t::make(width, height, frame->width, frame->height, width * 4); if (!sws_opt) { return -1; } sws = std::move(*sws_opt); linear_interpolation = width != frame->width || height != frame->height; return 0; } void apply_colorspace() override { sws.apply_colorspace(colorspace); auto tex = tex_t::make(height, width * 4); if (!tex) { return; } // The default green color is ugly. // Update the background color platf::img_t img; img.width = width; img.height = height; img.pixel_pitch = 4; img.row_pitch = img.width * img.pixel_pitch; std::vector image_data; image_data.resize(img.row_pitch * img.height); img.data = image_data.data(); if (sws.load_ram(img, tex->array)) { return; } 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}); } cudaTextureObject_t tex_obj(const tex_t &tex) const { return linear_interpolation ? tex.texture.linear : tex.texture.point; } stream_t stream; frame_t hwframe; int width, height; // When height and width don't change, it's not necessary to use linear interpolation bool linear_interpolation; sws_t sws; }; class cuda_ram_t: public cuda_t { public: int convert(platf::img_t &img) override { 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()); } int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { if (cuda_t::set_frame(frame, hw_frames_ctx)) { return -1; } auto tex_opt = tex_t::make(height, width * 4); if (!tex_opt) { return -1; } tex = std::move(*tex_opt); return 0; } tex_t tex; }; class cuda_vram_t: public cuda_t { public: int convert(platf::img_t &img) override { return sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex_obj(((img_t *) &img)->tex), stream.get()); } }; /** * @brief Opens the DRM device associated with the CUDA device index. * @param index CUDA device index to open. * @return File descriptor or -1 on failure. */ file_t open_drm_fd_for_cuda_device(int index) { CUdevice device; CU_CHECK(cdf->cuDeviceGet(&device, index), "Couldn't get CUDA device"); // There's no way to directly go from CUDA to a DRM device, so we'll // use sysfs to look up the DRM device name from the PCI ID. std::array pci_bus_id; CU_CHECK(cdf->cuDeviceGetPCIBusId(pci_bus_id.data(), pci_bus_id.size(), device), "Couldn't get CUDA device PCI bus ID"); BOOST_LOG(debug) << "Found CUDA device with PCI bus ID: "sv << pci_bus_id.data(); // Linux uses lowercase hexadecimal while CUDA uses uppercase std::transform(pci_bus_id.begin(), pci_bus_id.end(), pci_bus_id.begin(), [](char c) { return std::tolower(c); }); // Look for the name of the primary node in sysfs try { char sysfs_path[PATH_MAX]; std::snprintf(sysfs_path, sizeof(sysfs_path), "/sys/bus/pci/devices/%s/drm", pci_bus_id.data()); fs::path sysfs_dir {sysfs_path}; for (auto &entry : fs::directory_iterator {sysfs_dir}) { auto file = entry.path().filename(); auto filestring = file.generic_string(); if (std::string_view {filestring}.substr(0, 4) != "card"sv) { continue; } BOOST_LOG(debug) << "Found DRM primary node: "sv << filestring; fs::path dri_path {"/dev/dri"sv}; auto device_path = dri_path / file; return open(device_path.c_str(), O_RDWR); } } catch (const std::filesystem::filesystem_error &err) { BOOST_LOG(error) << "Failed to read sysfs: "sv << err.what(); } BOOST_LOG(error) << "Unable to find DRM device with PCI bus ID: "sv << pci_bus_id.data(); return -1; } class gl_cuda_vram_t: public platf::avcodec_encode_device_t { public: /** * @brief Initialize the GL->CUDA encoding device. * @param in_width Width of captured frames. * @param in_height Height of captured frames. * @param offset_x Offset of content in captured frame. * @param offset_y Offset of content in captured frame. * @return 0 on success or -1 on failure. */ int init(int in_width, int in_height, int offset_x, int offset_y) { // This must be non-zero to tell the video core that it's a hardware encoding device. data = (void *) 0x1; // TODO: Support more than one CUDA device file = std::move(open_drm_fd_for_cuda_device(0)); if (file.el < 0) { char string[1024]; BOOST_LOG(error) << "Couldn't open DRM FD for CUDA device: "sv << strerror_r(errno, string, sizeof(string)); return -1; } gbm.reset(gbm::create_device(file.el)); if (!gbm) { BOOST_LOG(error) << "Couldn't create GBM device: ["sv << util::hex(eglGetError()).to_string_view() << ']'; return -1; } display = egl::make_display(gbm.get()); if (!display) { return -1; } auto ctx_opt = egl::make_ctx(display.get()); if (!ctx_opt) { return -1; } ctx = std::move(*ctx_opt); width = in_width; height = in_height; sequence = 0; this->offset_x = offset_x; this->offset_y = offset_y; return 0; } /** * @brief Initialize color conversion into target CUDA frame. * @param frame Destination CUDA frame to write into. * @param hw_frames_ctx_buf FFmpeg hardware frame context. * @return 0 on success or -1 on failure. */ int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx_buf) override { this->hwframe.reset(frame); this->frame = frame; if (!frame->buf[0]) { if (av_hwframe_get_buffer(hw_frames_ctx_buf, frame, 0)) { BOOST_LOG(error) << "Couldn't get hwframe for VAAPI"sv; return -1; } } auto hw_frames_ctx = (AVHWFramesContext *) hw_frames_ctx_buf->data; sw_format = hw_frames_ctx->sw_format; auto nv12_opt = egl::create_target(frame->width, frame->height, sw_format); if (!nv12_opt) { return -1; } auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height, sw_format); if (!sws_opt) { return -1; } this->sws = std::move(*sws_opt); this->nv12 = std::move(*nv12_opt); auto cuda_ctx = (AVCUDADeviceContext *) hw_frames_ctx->device_ctx->hwctx; stream = make_stream(); if (!stream) { return -1; } cuda_ctx->stream = stream.get(); CU_CHECK(cdf->cuGraphicsGLRegisterImage(&y_res, nv12->tex[0], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY), "Couldn't register Y plane texture"); CU_CHECK(cdf->cuGraphicsGLRegisterImage(&uv_res, nv12->tex[1], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY), "Couldn't register UV plane texture"); return 0; } /** * @brief Convert the captured image into the target CUDA frame. * @param img Captured screen image. * @return 0 on success or -1 on failure. */ int convert(platf::img_t &img) override { auto &descriptor = (egl::img_descriptor_t &) img; if (descriptor.sequence == 0) { // For dummy images, use a blank RGB texture instead of importing a DMA-BUF rgb = egl::create_blank(img); } else if (descriptor.sequence > sequence) { sequence = descriptor.sequence; rgb = egl::rgb_t {}; auto rgb_opt = egl::import_source(display.get(), descriptor.sd); if (!rgb_opt) { return -1; } rgb = std::move(*rgb_opt); } // Perform the color conversion and scaling in GL sws.load_vram(descriptor, offset_x, offset_y, rgb->tex[0]); sws.convert(nv12->buf); auto fmt_desc = av_pix_fmt_desc_get(sw_format); // Map the GL textures to read for CUDA CUgraphicsResource resources[2] = {y_res.get(), uv_res.get()}; CU_CHECK(cdf->cuGraphicsMapResources(2, resources, stream.get()), "Couldn't map GL textures in CUDA"); // Copy from the GL textures to the target CUDA frame for (int i = 0; i < 2; i++) { CUDA_MEMCPY2D cpy = {}; cpy.srcMemoryType = CU_MEMORYTYPE_ARRAY; CU_CHECK(cdf->cuGraphicsSubResourceGetMappedArray(&cpy.srcArray, resources[i], 0, 0), "Couldn't get mapped plane array"); cpy.dstMemoryType = CU_MEMORYTYPE_DEVICE; cpy.dstDevice = (CUdeviceptr) frame->data[i]; cpy.dstPitch = frame->linesize[i]; cpy.WidthInBytes = (frame->width * fmt_desc->comp[i].step) >> (i ? fmt_desc->log2_chroma_w : 0); cpy.Height = frame->height >> (i ? fmt_desc->log2_chroma_h : 0); CU_CHECK_IGNORE(cdf->cuMemcpy2DAsync(&cpy, stream.get()), "Couldn't copy texture to CUDA frame"); } // Unmap the textures to allow modification from GL again CU_CHECK(cdf->cuGraphicsUnmapResources(2, resources, stream.get()), "Couldn't unmap GL textures from CUDA"); return 0; } /** * @brief Configures shader parameters for the specified colorspace. */ void apply_colorspace() override { sws.apply_colorspace(colorspace); } file_t file; gbm::gbm_t gbm; egl::display_t display; egl::ctx_t ctx; // This must be destroyed before display_t stream_t stream; frame_t hwframe; egl::sws_t sws; egl::nv12_t nv12; AVPixelFormat sw_format; int width, height; std::uint64_t sequence; egl::rgb_t rgb; registered_resource_t y_res; registered_resource_t uv_res; int offset_x, offset_y; }; std::unique_ptr make_avcodec_encode_device(int width, int height, bool vram) { if (init()) { return nullptr; } std::unique_ptr cuda; if (vram) { cuda = std::make_unique(); } else { cuda = std::make_unique(); } if (cuda->init(width, height)) { return nullptr; } return cuda; } /** * @brief Create a GL->CUDA encoding device for consuming captured dmabufs. * @param width Width of captured frames. * @param height Height of captured frames. * @param offset_x Offset of content in captured frame. * @param offset_y Offset of content in captured frame. * @return FFmpeg encoding device context. */ std::unique_ptr make_avcodec_gl_encode_device(int width, int height, int offset_x, int offset_y) { if (init()) { return nullptr; } auto cuda = std::make_unique(); if (cuda->init(width, height, offset_x, offset_y)) { return nullptr; } return cuda; } namespace nvfbc { static PNVFBCCREATEINSTANCE createInstance {}; static NVFBC_API_FUNCTION_LIST func {NVFBC_VERSION}; static constexpr inline NVFBC_BOOL nv_bool(bool b) { return b ? NVFBC_TRUE : NVFBC_FALSE; } static void *handle {nullptr}; int init() { static bool funcs_loaded = false; if (funcs_loaded) { return 0; } if (!handle) { handle = dyn::handle({"libnvidia-fbc.so.1", "libnvidia-fbc.so"}); if (!handle) { return -1; } } std::vector> funcs { {(dyn::apiproc *) &createInstance, "NvFBCCreateInstance"}, }; if (dyn::load(handle, funcs)) { dlclose(handle); handle = nullptr; return -1; } auto status = cuda::nvfbc::createInstance(&cuda::nvfbc::func); if (status) { BOOST_LOG(error) << "Unable to create NvFBC instance"sv; dlclose(handle); handle = nullptr; return -1; } funcs_loaded = true; return 0; } class ctx_t { public: ctx_t(NVFBC_SESSION_HANDLE handle) { NVFBC_BIND_CONTEXT_PARAMS params {NVFBC_BIND_CONTEXT_PARAMS_VER}; if (func.nvFBCBindContext(handle, ¶ms)) { BOOST_LOG(error) << "Couldn't bind NvFBC context to current thread: " << func.nvFBCGetLastErrorStr(handle); } this->handle = handle; } ~ctx_t() { NVFBC_RELEASE_CONTEXT_PARAMS params {NVFBC_RELEASE_CONTEXT_PARAMS_VER}; if (func.nvFBCReleaseContext(handle, ¶ms)) { BOOST_LOG(error) << "Couldn't release NvFBC context from current thread: " << func.nvFBCGetLastErrorStr(handle); } } NVFBC_SESSION_HANDLE handle; }; class handle_t { enum flag_e { SESSION_HANDLE, SESSION_CAPTURE, MAX_FLAGS, }; public: handle_t() = default; handle_t(handle_t &&other): handle_flags {other.handle_flags}, handle {other.handle} { other.handle_flags.reset(); } handle_t &operator=(handle_t &&other) { std::swap(handle_flags, other.handle_flags); std::swap(handle, other.handle); return *this; } static std::optional make() { NVFBC_CREATE_HANDLE_PARAMS params {NVFBC_CREATE_HANDLE_PARAMS_VER}; // Set privateData to allow NvFBC on consumer NVIDIA GPUs. // Based on https://github.com/keylase/nvidia-patch/blob/3193b4b1cea91527bf09ea9b8db5aade6a3f3c0a/win/nvfbcwrp/nvfbcwrp_main.cpp#L23-L25 . const unsigned int MAGIC_PRIVATE_DATA[4] = {0xAEF57AC5, 0x401D1A39, 0x1B856BBE, 0x9ED0CEBA}; params.privateData = MAGIC_PRIVATE_DATA; params.privateDataSize = sizeof(MAGIC_PRIVATE_DATA); handle_t handle; auto status = func.nvFBCCreateHandle(&handle.handle, ¶ms); if (status) { BOOST_LOG(error) << "Failed to create session: "sv << handle.last_error(); return std::nullopt; } handle.handle_flags[SESSION_HANDLE] = true; return handle; } const char *last_error() { return func.nvFBCGetLastErrorStr(handle); } std::optional status() { NVFBC_GET_STATUS_PARAMS params {NVFBC_GET_STATUS_PARAMS_VER}; auto status = func.nvFBCGetStatus(handle, ¶ms); if (status) { BOOST_LOG(error) << "Failed to get NvFBC status: "sv << last_error(); return std::nullopt; } return params; } int capture(NVFBC_CREATE_CAPTURE_SESSION_PARAMS &capture_params) { if (func.nvFBCCreateCaptureSession(handle, &capture_params)) { BOOST_LOG(error) << "Failed to start capture session: "sv << last_error(); return -1; } handle_flags[SESSION_CAPTURE] = true; NVFBC_TOCUDA_SETUP_PARAMS setup_params { NVFBC_TOCUDA_SETUP_PARAMS_VER, NVFBC_BUFFER_FORMAT_BGRA, }; if (func.nvFBCToCudaSetUp(handle, &setup_params)) { BOOST_LOG(error) << "Failed to setup cuda interop with nvFBC: "sv << last_error(); return -1; } return 0; } int stop() { if (!handle_flags[SESSION_CAPTURE]) { return 0; } NVFBC_DESTROY_CAPTURE_SESSION_PARAMS params {NVFBC_DESTROY_CAPTURE_SESSION_PARAMS_VER}; if (func.nvFBCDestroyCaptureSession(handle, ¶ms)) { BOOST_LOG(error) << "Couldn't destroy capture session: "sv << last_error(); return -1; } handle_flags[SESSION_CAPTURE] = false; return 0; } int reset() { if (!handle_flags[SESSION_HANDLE]) { return 0; } stop(); NVFBC_DESTROY_HANDLE_PARAMS params {NVFBC_DESTROY_HANDLE_PARAMS_VER}; ctx_t ctx {handle}; if (func.nvFBCDestroyHandle(handle, ¶ms)) { BOOST_LOG(error) << "Couldn't destroy session handle: "sv << func.nvFBCGetLastErrorStr(handle); } handle_flags[SESSION_HANDLE] = false; return 0; } ~handle_t() { reset(); } std::bitset handle_flags; NVFBC_SESSION_HANDLE handle; }; class display_t: public platf::display_t { public: int init(const std::string_view &display_name, const ::video::config_t &config) { auto handle = handle_t::make(); if (!handle) { return -1; } ctx_t ctx {handle->handle}; auto status_params = handle->status(); if (!status_params) { return -1; } int streamedMonitor = -1; if (!display_name.empty()) { if (status_params->bXRandRAvailable) { auto monitor_nr = util::from_view(display_name); if (monitor_nr < 0 || monitor_nr >= status_params->dwOutputNum) { 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; } else { streamedMonitor = monitor_nr; } } else { BOOST_LOG(warning) << "XrandR not available, streaming entire virtual desktop"sv; } } delay = std::chrono::nanoseconds {1s} / config.framerate; capture_params = NVFBC_CREATE_CAPTURE_SESSION_PARAMS {NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER}; capture_params.eCaptureType = NVFBC_CAPTURE_SHARED_CUDA; capture_params.bDisableAutoModesetRecovery = nv_bool(true); capture_params.dwSamplingRateMs = 1000 /* ms */ / config.framerate; if (streamedMonitor != -1) { auto &output = status_params->outputs[streamedMonitor]; width = output.trackedBox.w; height = output.trackedBox.h; offset_x = output.trackedBox.x; offset_y = output.trackedBox.y; capture_params.eTrackingType = NVFBC_TRACKING_OUTPUT; capture_params.dwOutputId = output.dwId; } else { capture_params.eTrackingType = NVFBC_TRACKING_SCREEN; width = status_params->screenSize.w; height = status_params->screenSize.h; } env_width = status_params->screenSize.w; env_height = status_params->screenSize.h; this->handle = std::move(*handle); return 0; } platf::capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); { // We must create at least one texture on this thread before calling NvFBCToCudaSetUp() // Otherwise it fails with "Unable to register an OpenGL buffer to a CUDA resource (result: 201)" message std::shared_ptr img_dummy; pull_free_image_cb(img_dummy); } // Force display_t::capture to initialize handle_t::capture cursor_visible = !*cursor; ctx_t ctx {handle.handle}; auto fg = util::fail_guard([&]() { handle.reset(); }); sleep_overshoot_logger.reset(); while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { std::this_thread::sleep_for(next_frame - now); sleep_overshoot_logger.first_point(next_frame); sleep_overshoot_logger.second_point_now_and_log(); } next_frame += delay; if (next_frame < now) { // some major slowdown happened; we couldn't keep up next_frame = now + delay; } std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 150ms, *cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: case platf::capture_e::interrupted: return status; case platf::capture_e::timeout: if (!push_captured_image_cb(std::move(img_out), false)) { return platf::capture_e::ok; } break; case platf::capture_e::ok: if (!push_captured_image_cb(std::move(img_out), true)) { return platf::capture_e::ok; } break; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; return status; } } return platf::capture_e::ok; } // Reinitialize the capture session. platf::capture_e reinit(bool cursor) { if (handle.stop()) { return platf::capture_e::error; } cursor_visible = cursor; if (cursor) { capture_params.bPushModel = nv_bool(false); capture_params.bWithCursor = nv_bool(true); capture_params.bAllowDirectCapture = nv_bool(false); } else { capture_params.bPushModel = nv_bool(true); capture_params.bWithCursor = nv_bool(false); capture_params.bAllowDirectCapture = nv_bool(true); } if (handle.capture(capture_params)) { return platf::capture_e::error; } // If trying to capture directly, test if it actually does. if (capture_params.bAllowDirectCapture) { CUdeviceptr device_ptr; NVFBC_FRAME_GRAB_INFO info; NVFBC_TOCUDA_GRAB_FRAME_PARAMS grab { NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER, NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT, &device_ptr, &info, 0, }; // Direct Capture may fail the first few times, even if it's possible for (int x = 0; x < 3; ++x) { if (auto status = func.nvFBCToCudaGrabFrame(handle.handle, &grab)) { if (status == NVFBC_ERR_MUST_RECREATE) { return platf::capture_e::reinit; } BOOST_LOG(error) << "Couldn't capture nvFramebuffer: "sv << handle.last_error(); return platf::capture_e::error; } if (info.bDirectCapture) { break; } BOOST_LOG(debug) << "Direct capture failed attempt ["sv << x << ']'; } if (!info.bDirectCapture) { BOOST_LOG(debug) << "Direct capture failed, trying the extra copy method"sv; // Direct capture failed capture_params.bPushModel = nv_bool(false); capture_params.bWithCursor = nv_bool(false); capture_params.bAllowDirectCapture = nv_bool(false); if (handle.stop() || handle.capture(capture_params)) { return platf::capture_e::error; } } } return platf::capture_e::ok; } platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { if (cursor != cursor_visible) { auto status = reinit(cursor); if (status != platf::capture_e::ok) { return status; } } CUdeviceptr device_ptr; NVFBC_FRAME_GRAB_INFO info; NVFBC_TOCUDA_GRAB_FRAME_PARAMS grab { NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER, NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT, &device_ptr, &info, (std::uint32_t) timeout.count(), }; if (auto status = func.nvFBCToCudaGrabFrame(handle.handle, &grab)) { if (status == NVFBC_ERR_MUST_RECREATE) { return platf::capture_e::reinit; } BOOST_LOG(error) << "Couldn't capture nvFramebuffer: "sv << handle.last_error(); return platf::capture_e::error; } if (!pull_free_image_cb(img_out)) { return platf::capture_e::interrupted; } auto img = (img_t *) img_out.get(); if (img->tex.copy((std::uint8_t *) device_ptr, img->height, img->row_pitch)) { return platf::capture_e::error; } return platf::capture_e::ok; } std::unique_ptr make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override { return ::cuda::make_avcodec_encode_device(width, height, true); } std::shared_ptr alloc_img() override { auto img = std::make_shared(); img->data = nullptr; img->width = width; img->height = height; img->pixel_pitch = 4; img->row_pitch = img->width * img->pixel_pitch; auto tex_opt = tex_t::make(height, width * img->pixel_pitch); if (!tex_opt) { return nullptr; } img->tex = std::move(*tex_opt); return img; }; int dummy_img(platf::img_t *) override { return 0; } std::chrono::nanoseconds delay; bool cursor_visible; handle_t handle; NVFBC_CREATE_CAPTURE_SESSION_PARAMS capture_params; }; } // namespace nvfbc } // namespace cuda namespace platf { std::shared_ptr nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { if (hwdevice_type != mem_type_e::cuda) { BOOST_LOG(error) << "Could not initialize nvfbc display with the given hw device type"sv; return nullptr; } auto display = std::make_shared(); if (display->init(display_name, config)) { return nullptr; } return display; } std::vector nvfbc_display_names() { if (cuda::init() || cuda::nvfbc::init()) { return {}; } std::vector display_names; auto handle = cuda::nvfbc::handle_t::make(); if (!handle) { return {}; } auto status_params = handle->status(); if (!status_params) { return {}; } if (!status_params->bIsCapturePossible) { BOOST_LOG(error) << "NVidia driver doesn't support NvFBC screencasting"sv; } BOOST_LOG(info) << "Found ["sv << status_params->dwOutputNum << "] outputs"sv; BOOST_LOG(info) << "Virtual Desktop: "sv << status_params->screenSize.w << 'x' << status_params->screenSize.h; BOOST_LOG(info) << "XrandR: "sv << (status_params->bXRandRAvailable ? "available"sv : "unavailable"sv); for (auto x = 0; x < status_params->dwOutputNum; ++x) { auto &output = status_params->outputs[x]; BOOST_LOG(info) << "-- Output --"sv; BOOST_LOG(debug) << " ID: "sv << output.dwId; BOOST_LOG(debug) << " Name: "sv << output.name; BOOST_LOG(info) << " Resolution: "sv << output.trackedBox.w << 'x' << output.trackedBox.h; BOOST_LOG(info) << " Offset: "sv << output.trackedBox.x << 'x' << output.trackedBox.y; display_names.emplace_back(std::to_string(x)); } return display_names; } } // namespace platf ================================================ FILE: src/platform/linux/cuda.cu ================================================ /** * @file src/platform/linux/cuda.cu * @brief CUDA implementation for Linux. */ // standard includes #include #include #include #include #include // platform includes #include // local includes #include "cuda.h" using namespace std::literals; #define SUNSHINE_STRINGVIEW_HELPER(x) x##sv #define SUNSHINE_STRINGVIEW(x) SUNSHINE_STRINGVIEW_HELPER(x) #define CU_CHECK(x, y) \ if (check((x), SUNSHINE_STRINGVIEW(y ": "))) \ return -1 #define CU_CHECK_VOID(x, y) \ if (check((x), SUNSHINE_STRINGVIEW(y ": "))) \ return; #define CU_CHECK_PTR(x, y) \ if (check((x), SUNSHINE_STRINGVIEW(y ": "))) \ return nullptr; #define CU_CHECK_OPT(x, y) \ if (check((x), SUNSHINE_STRINGVIEW(y ": "))) \ return std::nullopt; #define CU_CHECK_IGNORE(x, y) \ check((x), SUNSHINE_STRINGVIEW(y ": ")) using namespace std::literals; // Special declarations /** * NVCC tends to have problems with standard headers. * Don't include common.h, instead use bare minimum * of standard headers and duplicate declarations of necessary classes. * Not pretty and extremely error-prone, fix at earliest convenience. */ namespace platf { struct img_t: std::enable_shared_from_this { public: std::uint8_t *data {}; std::int32_t width {}; std::int32_t height {}; std::int32_t pixel_pitch {}; std::int32_t row_pitch {}; std::optional frame_timestamp; virtual ~img_t() = default; }; } // namespace platf // End special declarations namespace cuda { struct alignas(16) cuda_color_t { float4 color_vec_y; float4 color_vec_u; float4 color_vec_v; float2 range_y; float2 range_uv; }; static_assert(sizeof(video::color_t) == sizeof(cuda::cuda_color_t), "color matrix struct mismatch"); auto constexpr INVALID_TEXTURE = std::numeric_limits::max(); template inline T div_align(T l, T r) { return (l + r - 1) / r; } void pass_error(const std::string_view &sv, const char *name, const char *description); inline static int check(cudaError_t result, const std::string_view &sv) { if (result) { auto name = cudaGetErrorName(result); auto description = cudaGetErrorString(result); pass_error(sv, name, description); return -1; } return 0; } template ptr_t make_ptr() { void *p; CU_CHECK_PTR(cudaMalloc(&p, sizeof(T)), "Couldn't allocate color matrix"); ptr_t ptr {p}; return ptr; } void freeCudaPtr_t::operator()(void *ptr) { CU_CHECK_IGNORE(cudaFree(ptr), "Couldn't free cuda device pointer"); } void freeCudaStream_t::operator()(cudaStream_t ptr) { CU_CHECK_IGNORE(cudaStreamDestroy(ptr), "Couldn't free cuda stream"); } stream_t make_stream(int flags) { cudaStream_t stream; if (!flags) { CU_CHECK_PTR(cudaStreamCreate(&stream), "Couldn't create cuda stream"); } else { CU_CHECK_PTR(cudaStreamCreateWithFlags(&stream, flags), "Couldn't create cuda stream with flags"); } return stream_t {stream}; } inline __device__ float3 bgra_to_rgb(uchar4 vec) { return make_float3((float) vec.z, (float) vec.y, (float) vec.x); } inline __device__ float3 bgra_to_rgb(float4 vec) { return make_float3(vec.z, vec.y, vec.x); } inline __device__ float2 calcUV(float3 pixel, const cuda_color_t *const color_matrix) { float4 vec_u = color_matrix->color_vec_u; float4 vec_v = color_matrix->color_vec_v; float u = dot(pixel, make_float3(vec_u)) + vec_u.w; float v = dot(pixel, make_float3(vec_v)) + vec_v.w; u = u * color_matrix->range_uv.x + color_matrix->range_uv.y; v = v * color_matrix->range_uv.x + color_matrix->range_uv.y; return make_float2(u, v); } inline __device__ float calcY(float3 pixel, const cuda_color_t *const color_matrix) { float4 vec_y = color_matrix->color_vec_y; return (dot(pixel, make_float3(vec_y)) + vec_y.w) * color_matrix->range_y.x + color_matrix->range_y.y; } __global__ void RGBA_to_NV12( cudaTextureObject_t srcImage, std::uint8_t *dstY, std::uint8_t *dstUV, std::uint32_t dstPitchY, std::uint32_t dstPitchUV, float scale, const viewport_t viewport, const cuda_color_t *const color_matrix ) { int idX = (threadIdx.x + blockDim.x * blockIdx.x) * 2; int idY = (threadIdx.y + blockDim.y * blockIdx.y) * 2; if (idX >= viewport.width) { return; } if (idY >= viewport.height) { return; } float x = idX * scale; float y = idY * scale; idX += viewport.offsetX; idY += viewport.offsetY; uint8_t *dstY0 = dstY + idX + idY * dstPitchY; uint8_t *dstY1 = dstY + idX + (idY + 1) * dstPitchY; dstUV = dstUV + idX + (idY / 2 * dstPitchUV); float3 rgb_lt = bgra_to_rgb(tex2D(srcImage, x, y)); float3 rgb_rt = bgra_to_rgb(tex2D(srcImage, x + scale, y)); float3 rgb_lb = bgra_to_rgb(tex2D(srcImage, x, y + scale)); float3 rgb_rb = bgra_to_rgb(tex2D(srcImage, x + scale, y + scale)); float2 uv_lt = calcUV(rgb_lt, color_matrix) * 256.0f; float2 uv_rt = calcUV(rgb_rt, color_matrix) * 256.0f; float2 uv_lb = calcUV(rgb_lb, color_matrix) * 256.0f; float2 uv_rb = calcUV(rgb_rb, color_matrix) * 256.0f; float2 uv = (uv_lt + uv_lb + uv_rt + uv_rb) * 0.25f; dstUV[0] = uv.x; dstUV[1] = uv.y; dstY0[0] = calcY(rgb_lt, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible dstY0[1] = calcY(rgb_rt, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible dstY1[0] = calcY(rgb_lb, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible dstY1[1] = calcY(rgb_rb, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible } int tex_t::copy(std::uint8_t *src, int height, int pitch) { CU_CHECK(cudaMemcpy2DToArray(array, 0, 0, src, pitch, pitch, height, cudaMemcpyDeviceToDevice), "Couldn't copy to cuda array from deviceptr"); return 0; } std::optional tex_t::make(int height, int pitch) { tex_t tex; auto format = cudaCreateChannelDesc(); CU_CHECK_OPT(cudaMallocArray(&tex.array, &format, pitch, height, cudaArrayDefault), "Couldn't allocate cuda array"); cudaResourceDesc res {}; res.resType = cudaResourceTypeArray; res.res.array.array = tex.array; cudaTextureDesc desc {}; desc.readMode = cudaReadModeNormalizedFloat; desc.filterMode = cudaFilterModePoint; desc.normalizedCoords = false; std::fill_n(std::begin(desc.addressMode), 2, cudaAddressModeClamp); CU_CHECK_OPT(cudaCreateTextureObject(&tex.texture.point, &res, &desc, nullptr), "Couldn't create cuda texture that uses point interpolation"); desc.filterMode = cudaFilterModeLinear; CU_CHECK_OPT(cudaCreateTextureObject(&tex.texture.linear, &res, &desc, nullptr), "Couldn't create cuda texture that uses linear interpolation"); return tex; } tex_t::tex_t(): array {}, texture {INVALID_TEXTURE, INVALID_TEXTURE} { } tex_t::tex_t(tex_t &&other): array {other.array}, texture {other.texture} { other.array = 0; other.texture.point = INVALID_TEXTURE; other.texture.linear = INVALID_TEXTURE; } tex_t &tex_t::operator=(tex_t &&other) { std::swap(array, other.array); std::swap(texture, other.texture); return *this; } tex_t::~tex_t() { if (texture.point != INVALID_TEXTURE) { CU_CHECK_IGNORE(cudaDestroyTextureObject(texture.point), "Couldn't deallocate cuda texture that uses point interpolation"); texture.point = INVALID_TEXTURE; } if (texture.linear != INVALID_TEXTURE) { CU_CHECK_IGNORE(cudaDestroyTextureObject(texture.linear), "Couldn't deallocate cuda texture that uses linear interpolation"); texture.linear = INVALID_TEXTURE; } if (array) { CU_CHECK_IGNORE(cudaFreeArray(array), "Couldn't deallocate cuda array"); array = cudaArray_t {}; } } sws_t::sws_t(int in_width, int in_height, int out_width, int out_height, int pitch, int threadsPerBlock, ptr_t &&color_matrix): threadsPerBlock {threadsPerBlock}, color_matrix {std::move(color_matrix)} { // Ensure aspect ratio is maintained auto scalar = std::fminf(out_width / (float) in_width, out_height / (float) in_height); auto out_width_f = in_width * scalar; auto out_height_f = in_height * scalar; // result is always positive auto offsetX_f = (out_width - out_width_f) / 2; auto offsetY_f = (out_height - out_height_f) / 2; viewport.width = out_width_f; viewport.height = out_height_f; viewport.offsetX = offsetX_f; viewport.offsetY = offsetY_f; scale = 1.0f / scalar; } std::optional sws_t::make(int in_width, int in_height, int out_width, int out_height, int pitch) { cudaDeviceProp props; int device; CU_CHECK_OPT(cudaGetDevice(&device), "Couldn't get cuda device"); CU_CHECK_OPT(cudaGetDeviceProperties(&props, device), "Couldn't get cuda device properties"); auto ptr = make_ptr(); if (!ptr) { return std::nullopt; } return std::make_optional(in_width, in_height, out_width, out_height, pitch, props.maxThreadsPerMultiProcessor / props.maxBlocksPerMultiProcessor, std::move(ptr)); } 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) { return convert(Y, UV, pitchY, pitchUV, texture, stream, viewport); } 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) { int threadsX = viewport.width / 2; int threadsY = viewport.height / 2; dim3 block(threadsPerBlock); dim3 grid(div_align(threadsX, threadsPerBlock), threadsY); RGBA_to_NV12<<>>(texture, Y, UV, pitchY, pitchUV, scale, viewport, (cuda_color_t *) color_matrix.get()); return CU_CHECK_IGNORE(cudaGetLastError(), "RGBA_to_NV12 failed"); } void sws_t::apply_colorspace(const video::sunshine_colorspace_t &colorspace) { auto color_p = video::color_vectors_from_colorspace(colorspace); CU_CHECK_IGNORE(cudaMemcpy(color_matrix.get(), color_p, sizeof(video::color_t), cudaMemcpyHostToDevice), "Couldn't copy color matrix to cuda"); } int sws_t::load_ram(platf::img_t &img, cudaArray_t array) { 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"); } } // namespace cuda ================================================ FILE: src/platform/linux/cuda.h ================================================ /** * @file src/platform/linux/cuda.h * @brief Definitions for CUDA implementation. */ #pragma once #if defined(SUNSHINE_BUILD_CUDA) // standard includes #include #include #include #include #include // local includes #include "src/video_colorspace.h" namespace platf { struct avcodec_encode_device_t; struct img_t; } // namespace platf namespace cuda { namespace nvfbc { std::vector display_names(); } std::unique_ptr make_avcodec_encode_device(int width, int height, bool vram); /** * @brief Create a GL->CUDA encoding device for consuming captured dmabufs. * @param in_width Width of captured frames. * @param in_height Height of captured frames. * @param offset_x Offset of content in captured frame. * @param offset_y Offset of content in captured frame. * @return FFmpeg encoding device context. */ std::unique_ptr make_avcodec_gl_encode_device(int width, int height, int offset_x, int offset_y); int init(); } // namespace cuda typedef struct cudaArray *cudaArray_t; #if !defined(__CUDACC__) typedef struct CUstream_st *cudaStream_t; typedef unsigned long long cudaTextureObject_t; #else /* defined(__CUDACC__) */ typedef __location__(device_builtin) struct CUstream_st *cudaStream_t; typedef __location__(device_builtin) unsigned long long cudaTextureObject_t; #endif /* !defined(__CUDACC__) */ namespace cuda { class freeCudaPtr_t { public: void operator()(void *ptr); }; class freeCudaStream_t { public: void operator()(cudaStream_t ptr); }; using ptr_t = std::unique_ptr; using stream_t = std::unique_ptr; stream_t make_stream(int flags = 0); struct viewport_t { int width, height; int offsetX, offsetY; }; class tex_t { public: static std::optional make(int height, int pitch); tex_t(); tex_t(tex_t &&); tex_t &operator=(tex_t &&other); ~tex_t(); int copy(std::uint8_t *src, int height, int pitch); cudaArray_t array; struct texture { cudaTextureObject_t point; cudaTextureObject_t linear; } texture; }; class sws_t { public: sws_t() = default; sws_t(int in_width, int in_height, int out_width, int out_height, int pitch, int threadsPerBlock, ptr_t &&color_matrix); /** * in_width, in_height -- The width and height of the captured image in pixels * out_width, out_height -- the width and height of the NV12 image in pixels * * pitch -- The size of a single row of pixels in bytes */ static std::optional make(int in_width, int in_height, int out_width, int out_height, int pitch); // Converts loaded image into a CUDevicePtr int convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream); int 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); void apply_colorspace(const video::sunshine_colorspace_t &colorspace); int load_ram(platf::img_t &img, cudaArray_t array); ptr_t color_matrix; int threadsPerBlock; viewport_t viewport; float scale; }; } // namespace cuda #endif ================================================ FILE: src/platform/linux/graphics.cpp ================================================ /** * @file src/platform/linux/graphics.cpp * @brief Definitions for graphics related functions. */ // standard includes #include // local includes #include "graphics.h" #include "src/file_handler.h" #include "src/logging.h" #include "src/video.h" extern "C" { #include } // I want to have as little build dependencies as possible // There aren't that many DRM_FORMAT I need to use, so define them here // // They aren't likely to change any time soon. #define fourcc_code(a, b, c, d) ((std::uint32_t) (a) | ((std::uint32_t) (b) << 8) | ((std::uint32_t) (c) << 16) | ((std::uint32_t) (d) << 24)) #define fourcc_mod_code(vendor, val) ((((uint64_t) vendor) << 56) | ((val) & 0x00ffffffffffffffULL)) #define DRM_FORMAT_MOD_INVALID fourcc_mod_code(0, ((1ULL << 56) - 1)) #if !defined(SUNSHINE_SHADERS_DIR) // for testing this needs to be defined in cmake as we don't do an install #define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/opengl" #endif using namespace std::literals; namespace gl { GladGLContext ctx; void drain_errors(const std::string_view &prefix) { GLenum err; while ((err = ctx.GetError()) != GL_NO_ERROR) { BOOST_LOG(error) << "GL: "sv << prefix << ": ["sv << util::hex(err).to_string_view() << ']'; } } tex_t::~tex_t() { if (size() != 0) { ctx.DeleteTextures(size(), begin()); } } tex_t tex_t::make(std::size_t count) { tex_t textures {count}; ctx.GenTextures(textures.size(), textures.begin()); float color[] = {0.0f, 0.0f, 0.0f, 1.0f}; for (auto tex : textures) { gl::ctx.BindTexture(GL_TEXTURE_2D, tex); gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // x gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // y gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); gl::ctx.TexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, color); } return textures; } frame_buf_t::~frame_buf_t() { if (begin()) { ctx.DeleteFramebuffers(size(), begin()); } } frame_buf_t frame_buf_t::make(std::size_t count) { frame_buf_t frame_buf {count}; ctx.GenFramebuffers(frame_buf.size(), frame_buf.begin()); return frame_buf; } void frame_buf_t::copy(int id, int texture, int offset_x, int offset_y, int width, int height) { gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, (*this)[id]); gl::ctx.ReadBuffer(GL_COLOR_ATTACHMENT0 + id); gl::ctx.BindTexture(GL_TEXTURE_2D, texture); gl::ctx.CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, offset_x, offset_y, width, height); } std::string shader_t::err_str() { int length; ctx.GetShaderiv(handle(), GL_INFO_LOG_LENGTH, &length); std::string string; string.resize(length); ctx.GetShaderInfoLog(handle(), length, &length, string.data()); string.resize(length - 1); return string; } util::Either shader_t::compile(const std::string_view &source, GLenum type) { shader_t shader; auto data = source.data(); GLint length = source.length(); shader._shader.el = ctx.CreateShader(type); ctx.ShaderSource(shader.handle(), 1, &data, &length); ctx.CompileShader(shader.handle()); int status = 0; ctx.GetShaderiv(shader.handle(), GL_COMPILE_STATUS, &status); if (!status) { return shader.err_str(); } return shader; } GLuint shader_t::handle() const { return _shader.el; } buffer_t buffer_t::make(util::buffer_t &&offsets, const char *block, const std::string_view &data) { buffer_t buffer; buffer._block = block; buffer._size = data.size(); buffer._offsets = std::move(offsets); ctx.GenBuffers(1, &buffer._buffer.el); ctx.BindBuffer(GL_UNIFORM_BUFFER, buffer.handle()); ctx.BufferData(GL_UNIFORM_BUFFER, data.size(), (const std::uint8_t *) data.data(), GL_DYNAMIC_DRAW); return buffer; } GLuint buffer_t::handle() const { return _buffer.el; } const char *buffer_t::block() const { return _block; } void buffer_t::update(const std::string_view &view, std::size_t offset) { ctx.BindBuffer(GL_UNIFORM_BUFFER, handle()); ctx.BufferSubData(GL_UNIFORM_BUFFER, offset, view.size(), (const void *) view.data()); } void buffer_t::update(std::string_view *members, std::size_t count, std::size_t offset) { util::buffer_t buffer {_size}; for (int x = 0; x < count; ++x) { auto val = members[x]; std::copy_n((const std::uint8_t *) val.data(), val.size(), &buffer[_offsets[x]]); } update(util::view(buffer.begin(), buffer.end()), offset); } std::string program_t::err_str() { int length; ctx.GetProgramiv(handle(), GL_INFO_LOG_LENGTH, &length); std::string string; string.resize(length); ctx.GetShaderInfoLog(handle(), length, &length, string.data()); string.resize(length - 1); return string; } util::Either program_t::link(const shader_t &vert, const shader_t &frag) { program_t program; program._program.el = ctx.CreateProgram(); ctx.AttachShader(program.handle(), vert.handle()); ctx.AttachShader(program.handle(), frag.handle()); // p_handle stores a copy of the program handle, since program will be moved before // the fail guard function is called. auto fg = util::fail_guard([p_handle = program.handle(), &vert, &frag]() { ctx.DetachShader(p_handle, vert.handle()); ctx.DetachShader(p_handle, frag.handle()); }); ctx.LinkProgram(program.handle()); int status = 0; ctx.GetProgramiv(program.handle(), GL_LINK_STATUS, &status); if (!status) { return program.err_str(); } return program; } void program_t::bind(const buffer_t &buffer) { ctx.UseProgram(handle()); auto i = ctx.GetUniformBlockIndex(handle(), buffer.block()); ctx.BindBufferBase(GL_UNIFORM_BUFFER, i, buffer.handle()); } std::optional program_t::uniform(const char *block, std::pair *members, std::size_t count) { auto i = ctx.GetUniformBlockIndex(handle(), block); if (i == GL_INVALID_INDEX) { BOOST_LOG(error) << "Couldn't find index of ["sv << block << ']'; return std::nullopt; } int size; ctx.GetActiveUniformBlockiv(handle(), i, GL_UNIFORM_BLOCK_DATA_SIZE, &size); bool error_flag = false; util::buffer_t offsets {count}; auto indices = (std::uint32_t *) alloca(count * sizeof(std::uint32_t)); auto names = (const char **) alloca(count * sizeof(const char *)); auto names_p = names; std::for_each_n(members, count, [names_p](auto &member) mutable { *names_p++ = std::get<0>(member); }); std::fill_n(indices, count, GL_INVALID_INDEX); ctx.GetUniformIndices(handle(), count, names, indices); for (int x = 0; x < count; ++x) { if (indices[x] == GL_INVALID_INDEX) { error_flag = true; BOOST_LOG(error) << "Couldn't find ["sv << block << '.' << members[x].first << ']'; } } if (error_flag) { return std::nullopt; } ctx.GetActiveUniformsiv(handle(), count, indices, GL_UNIFORM_OFFSET, offsets.begin()); util::buffer_t buffer {(std::size_t) size}; for (int x = 0; x < count; ++x) { auto val = std::get<1>(members[x]); std::copy_n((const std::uint8_t *) val.data(), val.size(), &buffer[offsets[x]]); } return buffer_t::make(std::move(offsets), block, std::string_view {(char *) buffer.begin(), buffer.size()}); } GLuint program_t::handle() const { return _program.el; } } // namespace gl namespace gbm { device_destroy_fn device_destroy; create_device_fn create_device; int init() { static void *handle {nullptr}; static bool funcs_loaded = false; if (funcs_loaded) { return 0; } if (!handle) { handle = dyn::handle({"libgbm.so.1", "libgbm.so"}); if (!handle) { return -1; } } std::vector> funcs { {(GLADapiproc *) &device_destroy, "gbm_device_destroy"}, {(GLADapiproc *) &create_device, "gbm_create_device"}, }; if (dyn::load(handle, funcs)) { return -1; } funcs_loaded = true; return 0; } } // namespace gbm namespace egl { constexpr auto EGL_LINUX_DMA_BUF_EXT = 0x3270; constexpr auto EGL_LINUX_DRM_FOURCC_EXT = 0x3271; constexpr auto EGL_DMA_BUF_PLANE0_FD_EXT = 0x3272; constexpr auto EGL_DMA_BUF_PLANE0_OFFSET_EXT = 0x3273; constexpr auto EGL_DMA_BUF_PLANE0_PITCH_EXT = 0x3274; constexpr auto EGL_DMA_BUF_PLANE1_FD_EXT = 0x3275; constexpr auto EGL_DMA_BUF_PLANE1_OFFSET_EXT = 0x3276; constexpr auto EGL_DMA_BUF_PLANE1_PITCH_EXT = 0x3277; constexpr auto EGL_DMA_BUF_PLANE2_FD_EXT = 0x3278; constexpr auto EGL_DMA_BUF_PLANE2_OFFSET_EXT = 0x3279; constexpr auto EGL_DMA_BUF_PLANE2_PITCH_EXT = 0x327A; constexpr auto EGL_DMA_BUF_PLANE3_FD_EXT = 0x3440; constexpr auto EGL_DMA_BUF_PLANE3_OFFSET_EXT = 0x3441; constexpr auto EGL_DMA_BUF_PLANE3_PITCH_EXT = 0x3442; constexpr auto EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT = 0x3443; constexpr auto EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT = 0x3444; constexpr auto EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT = 0x3445; constexpr auto EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT = 0x3446; constexpr auto EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT = 0x3447; constexpr auto EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT = 0x3448; constexpr auto EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT = 0x3449; constexpr auto EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT = 0x344A; bool fail() { return eglGetError() != EGL_SUCCESS; } /** * @memberof egl::display_t */ display_t make_display(std::variant native_display) { constexpr auto EGL_PLATFORM_GBM_MESA = 0x31D7; constexpr auto EGL_PLATFORM_WAYLAND_KHR = 0x31D8; constexpr auto EGL_PLATFORM_X11_KHR = 0x31D5; int egl_platform; void *native_display_p; switch (native_display.index()) { case 0: egl_platform = EGL_PLATFORM_GBM_MESA; native_display_p = std::get<0>(native_display); break; case 1: egl_platform = EGL_PLATFORM_WAYLAND_KHR; native_display_p = std::get<1>(native_display); break; case 2: egl_platform = EGL_PLATFORM_X11_KHR; native_display_p = std::get<2>(native_display); break; default: BOOST_LOG(error) << "egl::make_display(): Index ["sv << native_display.index() << "] not implemented"sv; return nullptr; } // native_display.left() equals native_display.right() display_t display = eglGetPlatformDisplay(egl_platform, native_display_p, nullptr); if (fail()) { BOOST_LOG(error) << "Couldn't open EGL display: ["sv << util::hex(eglGetError()).to_string_view() << ']'; return nullptr; } int major, minor; if (!eglInitialize(display.get(), &major, &minor)) { BOOST_LOG(error) << "Couldn't initialize EGL display: ["sv << util::hex(eglGetError()).to_string_view() << ']'; return nullptr; } const char *extension_st = eglQueryString(display.get(), EGL_EXTENSIONS); const char *version = eglQueryString(display.get(), EGL_VERSION); const char *vendor = eglQueryString(display.get(), EGL_VENDOR); const char *apis = eglQueryString(display.get(), EGL_CLIENT_APIS); BOOST_LOG(debug) << "EGL: ["sv << vendor << "]: version ["sv << version << ']'; BOOST_LOG(debug) << "API's supported: ["sv << apis << ']'; const char *extensions[] { "EGL_KHR_create_context", "EGL_KHR_surfaceless_context", "EGL_EXT_image_dma_buf_import", "EGL_EXT_image_dma_buf_import_modifiers", }; for (auto ext : extensions) { if (!std::strstr(extension_st, ext)) { BOOST_LOG(error) << "Missing extension: ["sv << ext << ']'; return nullptr; } } return display; } std::optional make_ctx(display_t::pointer display) { constexpr int conf_attr[] { EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE }; int count; EGLConfig conf; if (!eglChooseConfig(display, conf_attr, &conf, 1, &count)) { BOOST_LOG(error) << "Couldn't set config attributes: ["sv << util::hex(eglGetError()).to_string_view() << ']'; return std::nullopt; } if (!eglBindAPI(EGL_OPENGL_API)) { BOOST_LOG(error) << "Couldn't bind API: ["sv << util::hex(eglGetError()).to_string_view() << ']'; return std::nullopt; } constexpr int attr[] { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }; ctx_t ctx {display, eglCreateContext(display, conf, EGL_NO_CONTEXT, attr)}; if (fail()) { BOOST_LOG(error) << "Couldn't create EGL context: ["sv << util::hex(eglGetError()).to_string_view() << ']'; return std::nullopt; } TUPLE_EL_REF(ctx_p, 1, ctx.el); if (!eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx_p)) { BOOST_LOG(error) << "Couldn't make current display"sv; return std::nullopt; } if (!gladLoadGLContext(&gl::ctx, eglGetProcAddress)) { BOOST_LOG(error) << "Couldn't load OpenGL library"sv; return std::nullopt; } BOOST_LOG(debug) << "GL: vendor: "sv << gl::ctx.GetString(GL_VENDOR); BOOST_LOG(debug) << "GL: renderer: "sv << gl::ctx.GetString(GL_RENDERER); BOOST_LOG(debug) << "GL: version: "sv << gl::ctx.GetString(GL_VERSION); BOOST_LOG(debug) << "GL: shader: "sv << gl::ctx.GetString(GL_SHADING_LANGUAGE_VERSION); gl::ctx.PixelStorei(GL_UNPACK_ALIGNMENT, 1); return ctx; } struct plane_attr_t { EGLAttrib fd; EGLAttrib offset; EGLAttrib pitch; EGLAttrib lo; EGLAttrib hi; }; inline plane_attr_t get_plane(std::uint32_t plane_indice) { switch (plane_indice) { case 0: return { EGL_DMA_BUF_PLANE0_FD_EXT, EGL_DMA_BUF_PLANE0_OFFSET_EXT, EGL_DMA_BUF_PLANE0_PITCH_EXT, EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, }; case 1: return { EGL_DMA_BUF_PLANE1_FD_EXT, EGL_DMA_BUF_PLANE1_OFFSET_EXT, EGL_DMA_BUF_PLANE1_PITCH_EXT, EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT, }; case 2: return { EGL_DMA_BUF_PLANE2_FD_EXT, EGL_DMA_BUF_PLANE2_OFFSET_EXT, EGL_DMA_BUF_PLANE2_PITCH_EXT, EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT, }; case 3: return { EGL_DMA_BUF_PLANE3_FD_EXT, EGL_DMA_BUF_PLANE3_OFFSET_EXT, EGL_DMA_BUF_PLANE3_PITCH_EXT, EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT, }; } // Avoid warning return {}; } /** * @brief Get EGL attributes for eglCreateImage() to import the provided surface. * @param surface The surface descriptor. * @return Vector of EGL attributes. */ std::vector surface_descriptor_to_egl_attribs(const surface_descriptor_t &surface) { std::vector attribs; attribs.emplace_back(EGL_WIDTH); attribs.emplace_back(surface.width); attribs.emplace_back(EGL_HEIGHT); attribs.emplace_back(surface.height); attribs.emplace_back(EGL_LINUX_DRM_FOURCC_EXT); attribs.emplace_back(surface.fourcc); for (auto x = 0; x < 4; ++x) { auto fd = surface.fds[x]; if (fd < 0) { continue; } auto plane_attr = get_plane(x); attribs.emplace_back(plane_attr.fd); attribs.emplace_back(fd); attribs.emplace_back(plane_attr.offset); attribs.emplace_back(surface.offsets[x]); attribs.emplace_back(plane_attr.pitch); attribs.emplace_back(surface.pitches[x]); if (surface.modifier != DRM_FORMAT_MOD_INVALID) { attribs.emplace_back(plane_attr.lo); attribs.emplace_back(surface.modifier & 0xFFFFFFFF); attribs.emplace_back(plane_attr.hi); attribs.emplace_back(surface.modifier >> 32); } } attribs.emplace_back(EGL_NONE); return attribs; } std::optional import_source(display_t::pointer egl_display, const surface_descriptor_t &xrgb) { auto attribs = surface_descriptor_to_egl_attribs(xrgb); rgb_t rgb { egl_display, eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs.data()), gl::tex_t::make(1) }; if (!rgb->xrgb8) { BOOST_LOG(error) << "Couldn't import RGB Image: "sv << util::hex(eglGetError()).to_string_view(); return std::nullopt; } gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]); gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, rgb->xrgb8); gl::ctx.BindTexture(GL_TEXTURE_2D, 0); gl_drain_errors; return rgb; } /** * @brief Create a black RGB texture of the specified image size. * @param img The image to use for texture sizing. * @return The new RGB texture. */ rgb_t create_blank(platf::img_t &img) { rgb_t rgb { EGL_NO_DISPLAY, EGL_NO_IMAGE, gl::tex_t::make(1) }; gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]); gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, img.width, img.height); gl::ctx.BindTexture(GL_TEXTURE_2D, 0); auto framebuf = gl::frame_buf_t::make(1); framebuf.bind(&rgb->tex[0], &rgb->tex[0] + 1); GLenum attachment = GL_COLOR_ATTACHMENT0; gl::ctx.DrawBuffers(1, &attachment); const GLuint rgb_black[] = {0, 0, 0, 0}; gl::ctx.ClearBufferuiv(GL_COLOR, 0, rgb_black); gl_drain_errors; return rgb; } std::optional import_target(display_t::pointer egl_display, std::array &&fds, const surface_descriptor_t &y, const surface_descriptor_t &uv) { auto y_attribs = surface_descriptor_to_egl_attribs(y); auto uv_attribs = surface_descriptor_to_egl_attribs(uv); nv12_t nv12 { egl_display, eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, y_attribs.data()), eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, uv_attribs.data()), gl::tex_t::make(2), gl::frame_buf_t::make(2), std::move(fds) }; if (!nv12->r8 || !nv12->bg88) { BOOST_LOG(error) << "Couldn't import YUV target: "sv << util::hex(eglGetError()).to_string_view(); return std::nullopt; } gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[0]); gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, nv12->r8); gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[1]); gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, nv12->bg88); nv12->buf.bind(std::begin(nv12->tex), std::end(nv12->tex)); GLenum attachments[] { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 }; for (int x = 0; x < sizeof(attachments) / sizeof(decltype(attachments[0])); ++x) { gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, nv12->buf[x]); gl::ctx.DrawBuffers(1, &attachments[x]); const float y_black[] = {0.0f, 0.0f, 0.0f, 0.0f}; const float uv_black[] = {0.5f, 0.5f, 0.5f, 0.5f}; gl::ctx.ClearBufferfv(GL_COLOR, 0, x == 0 ? y_black : uv_black); } gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0); gl_drain_errors; return nv12; } /** * @brief Create biplanar YUV textures to render into. * @param width Width of the target frame. * @param height Height of the target frame. * @param format Format of the target frame. * @return The new RGB texture. */ std::optional create_target(int width, int height, AVPixelFormat format) { nv12_t nv12 { EGL_NO_DISPLAY, EGL_NO_IMAGE, EGL_NO_IMAGE, gl::tex_t::make(2), gl::frame_buf_t::make(2), }; GLint y_format; GLint uv_format; // Determine the size of each plane element auto fmt_desc = av_pix_fmt_desc_get(format); if (fmt_desc->comp[0].depth <= 8) { y_format = GL_R8; uv_format = GL_RG8; } else if (fmt_desc->comp[0].depth <= 16) { y_format = GL_R16; uv_format = GL_RG16; } else { BOOST_LOG(error) << "Unsupported target pixel format: "sv << format; return std::nullopt; } gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[0]); gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, y_format, width, height); gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[1]); gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, uv_format, width >> fmt_desc->log2_chroma_w, height >> fmt_desc->log2_chroma_h); nv12->buf.bind(std::begin(nv12->tex), std::end(nv12->tex)); GLenum attachments[] { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 }; for (int x = 0; x < sizeof(attachments) / sizeof(decltype(attachments[0])); ++x) { gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, nv12->buf[x]); gl::ctx.DrawBuffers(1, &attachments[x]); const float y_black[] = {0.0f, 0.0f, 0.0f, 0.0f}; const float uv_black[] = {0.5f, 0.5f, 0.5f, 0.5f}; gl::ctx.ClearBufferfv(GL_COLOR, 0, x == 0 ? y_black : uv_black); } gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0); gl_drain_errors; return nv12; } void sws_t::apply_colorspace(const video::sunshine_colorspace_t &colorspace) { auto color_p = video::color_vectors_from_colorspace(colorspace); std::string_view members[] { util::view(color_p->color_vec_y), util::view(color_p->color_vec_u), util::view(color_p->color_vec_v), util::view(color_p->range_y), util::view(color_p->range_uv), }; color_matrix.update(members, sizeof(members) / sizeof(decltype(members[0]))); program[0].bind(color_matrix); program[1].bind(color_matrix); } std::optional sws_t::make(int in_width, int in_height, int out_width, int out_height, gl::tex_t &&tex) { sws_t sws; sws.serial = std::numeric_limits::max(); // Ensure aspect ratio is maintained auto scalar = std::fminf(out_width / (float) in_width, out_height / (float) in_height); auto out_width_f = in_width * scalar; auto out_height_f = in_height * scalar; // result is always positive auto offsetX_f = (out_width - out_width_f) / 2; auto offsetY_f = (out_height - out_height_f) / 2; sws.out_width = out_width_f; sws.out_height = out_height_f; sws.in_width = in_width; sws.in_height = in_height; sws.offsetX = offsetX_f; sws.offsetY = offsetY_f; auto width_i = 1.0f / sws.out_width; { const char *sources[] { SUNSHINE_SHADERS_DIR "/ConvertUV.frag", SUNSHINE_SHADERS_DIR "/ConvertUV.vert", SUNSHINE_SHADERS_DIR "/ConvertY.frag", SUNSHINE_SHADERS_DIR "/Scene.vert", SUNSHINE_SHADERS_DIR "/Scene.frag", }; GLenum shader_type[2] { GL_FRAGMENT_SHADER, GL_VERTEX_SHADER, }; constexpr auto count = sizeof(sources) / sizeof(const char *); util::Either compiled_sources[count]; bool error_flag = false; for (int x = 0; x < count; ++x) { auto &compiled_source = compiled_sources[x]; compiled_source = gl::shader_t::compile(file_handler::read_file(sources[x]), shader_type[x % 2]); gl_drain_errors; if (compiled_source.has_right()) { BOOST_LOG(error) << sources[x] << ": "sv << compiled_source.right(); error_flag = true; } } if (error_flag) { return std::nullopt; } auto program = gl::program_t::link(compiled_sources[3].left(), compiled_sources[4].left()); if (program.has_right()) { BOOST_LOG(error) << "GL linker: "sv << program.right(); return std::nullopt; } // Cursor - shader sws.program[2] = std::move(program.left()); program = gl::program_t::link(compiled_sources[1].left(), compiled_sources[0].left()); if (program.has_right()) { BOOST_LOG(error) << "GL linker: "sv << program.right(); return std::nullopt; } // UV - shader sws.program[1] = std::move(program.left()); program = gl::program_t::link(compiled_sources[3].left(), compiled_sources[2].left()); if (program.has_right()) { BOOST_LOG(error) << "GL linker: "sv << program.right(); return std::nullopt; } // Y - shader sws.program[0] = std::move(program.left()); } auto loc_width_i = gl::ctx.GetUniformLocation(sws.program[1].handle(), "width_i"); if (loc_width_i < 0) { BOOST_LOG(error) << "Couldn't find uniform [width_i]"sv; return std::nullopt; } gl::ctx.UseProgram(sws.program[1].handle()); gl::ctx.Uniform1fv(loc_width_i, 1, &width_i); auto color_p = video::color_vectors_from_colorspace(video::colorspace_e::rec601, false); std::pair members[] { std::make_pair("color_vec_y", util::view(color_p->color_vec_y)), std::make_pair("color_vec_u", util::view(color_p->color_vec_u)), std::make_pair("color_vec_v", util::view(color_p->color_vec_v)), std::make_pair("range_y", util::view(color_p->range_y)), std::make_pair("range_uv", util::view(color_p->range_uv)), }; auto color_matrix = sws.program[0].uniform("ColorMatrix", members, sizeof(members) / sizeof(decltype(members[0]))); if (!color_matrix) { return std::nullopt; } sws.color_matrix = std::move(*color_matrix); sws.tex = std::move(tex); sws.cursor_framebuffer = gl::frame_buf_t::make(1); sws.cursor_framebuffer.bind(&sws.tex[0], &sws.tex[1]); sws.program[0].bind(sws.color_matrix); sws.program[1].bind(sws.color_matrix); gl::ctx.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); gl_drain_errors; return sws; } int sws_t::blank(gl::frame_buf_t &fb, int offsetX, int offsetY, int width, int height) { auto f = [&]() { std::swap(offsetX, this->offsetX); std::swap(offsetY, this->offsetY); std::swap(width, this->out_width); std::swap(height, this->out_height); }; f(); auto fg = util::fail_guard(f); return convert(fb); } std::optional sws_t::make(int in_width, int in_height, int out_width, int out_height, AVPixelFormat format) { GLint gl_format; // Decide the bit depth format of the backing texture based the target frame format auto fmt_desc = av_pix_fmt_desc_get(format); switch (fmt_desc->comp[0].depth) { case 8: gl_format = GL_RGBA8; break; case 10: gl_format = GL_RGB10_A2; break; case 12: gl_format = GL_RGBA12; break; case 16: gl_format = GL_RGBA16; break; default: BOOST_LOG(error) << "Unsupported pixel format for EGL frame: "sv << (int) format; return std::nullopt; } auto tex = gl::tex_t::make(2); gl::ctx.BindTexture(GL_TEXTURE_2D, tex[0]); gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, gl_format, in_width, in_height); return make(in_width, in_height, out_width, out_height, std::move(tex)); } void sws_t::load_ram(platf::img_t &img) { loaded_texture = tex[0]; gl::ctx.BindTexture(GL_TEXTURE_2D, loaded_texture); gl::ctx.TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, img.width, img.height, GL_BGRA, GL_UNSIGNED_BYTE, img.data); } void sws_t::load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture) { // When only a sub-part of the image must be encoded... const bool copy = offset_x || offset_y || img.sd.width != in_width || img.sd.height != in_height; if (copy) { auto framebuf = gl::frame_buf_t::make(1); framebuf.bind(&texture, &texture + 1); loaded_texture = tex[0]; framebuf.copy(0, loaded_texture, offset_x, offset_y, in_width, in_height); } else { loaded_texture = texture; } if (img.data) { GLenum attachment = GL_COLOR_ATTACHMENT0; gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, cursor_framebuffer[0]); gl::ctx.UseProgram(program[2].handle()); // When a copy has already been made... if (!copy) { gl::ctx.BindTexture(GL_TEXTURE_2D, texture); gl::ctx.DrawBuffers(1, &attachment); gl::ctx.Viewport(0, 0, in_width, in_height); gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3); loaded_texture = tex[0]; } gl::ctx.BindTexture(GL_TEXTURE_2D, tex[1]); if (serial != img.serial) { serial = img.serial; gl::ctx.TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, img.src_w, img.src_h, 0, GL_BGRA, GL_UNSIGNED_BYTE, img.data); } gl::ctx.Enable(GL_BLEND); gl::ctx.DrawBuffers(1, &attachment); #ifndef NDEBUG auto status = gl::ctx.CheckFramebufferStatus(GL_FRAMEBUFFER); if (status != GL_FRAMEBUFFER_COMPLETE) { BOOST_LOG(error) << "Pass Cursor: CheckFramebufferStatus() --> [0x"sv << util::hex(status).to_string_view() << ']'; return; } #endif gl::ctx.Viewport(img.x, img.y, img.width, img.height); gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3); gl::ctx.Disable(GL_BLEND); gl::ctx.BindTexture(GL_TEXTURE_2D, 0); gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0); } } int sws_t::convert(gl::frame_buf_t &fb) { gl::ctx.BindTexture(GL_TEXTURE_2D, loaded_texture); GLenum attachments[] { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 }; for (int x = 0; x < sizeof(attachments) / sizeof(decltype(attachments[0])); ++x) { gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, fb[x]); gl::ctx.DrawBuffers(1, &attachments[x]); #ifndef NDEBUG auto status = gl::ctx.CheckFramebufferStatus(GL_FRAMEBUFFER); if (status != GL_FRAMEBUFFER_COMPLETE) { BOOST_LOG(error) << "Pass "sv << x << ": CheckFramebufferStatus() --> [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } #endif gl::ctx.UseProgram(program[x].handle()); gl::ctx.Viewport(offsetX / (x + 1), offsetY / (x + 1), out_width / (x + 1), out_height / (x + 1)); gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3); } gl::ctx.BindTexture(GL_TEXTURE_2D, 0); gl::ctx.Flush(); return 0; } } // namespace egl void free_frame(AVFrame *frame) { av_frame_free(&frame); } ================================================ FILE: src/platform/linux/graphics.h ================================================ /** * @file src/platform/linux/graphics.h * @brief Declarations for graphics related functions. */ #pragma once // standard includes #include #include // lib includes #include #include // local includes #include "misc.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" #include "src/video_colorspace.h" #define SUNSHINE_STRINGIFY_HELPER(x) #x #define SUNSHINE_STRINGIFY(x) SUNSHINE_STRINGIFY_HELPER(x) #define gl_drain_errors_helper(x) gl::drain_errors(x) #define gl_drain_errors gl_drain_errors_helper(__FILE__ ":" SUNSHINE_STRINGIFY(__LINE__)) extern "C" int close(int __fd); // X11 Display extern "C" struct _XDisplay; struct AVFrame; void free_frame(AVFrame *frame); using frame_t = util::safe_ptr; namespace gl { extern GladGLContext ctx; void drain_errors(const std::string_view &prefix); class tex_t: public util::buffer_t { using util::buffer_t::buffer_t; public: tex_t(tex_t &&) = default; tex_t &operator=(tex_t &&) = default; ~tex_t(); static tex_t make(std::size_t count); }; class frame_buf_t: public util::buffer_t { using util::buffer_t::buffer_t; public: frame_buf_t(frame_buf_t &&) = default; frame_buf_t &operator=(frame_buf_t &&) = default; ~frame_buf_t(); static frame_buf_t make(std::size_t count); inline void bind(std::nullptr_t, std::nullptr_t) { int x = 0; for (auto fb : (*this)) { ctx.BindFramebuffer(GL_FRAMEBUFFER, fb); ctx.FramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + x, 0, 0); ++x; } return; } template void bind(It it_begin, It it_end) { using namespace std::literals; if (std::distance(it_begin, it_end) > size()) { BOOST_LOG(warning) << "To many elements to bind"sv; return; } int x = 0; std::for_each(it_begin, it_end, [&](auto tex) { ctx.BindFramebuffer(GL_FRAMEBUFFER, (*this)[x]); ctx.BindTexture(GL_TEXTURE_2D, tex); ctx.FramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + x, tex, 0); ++x; }); } /** * Copies a part of the framebuffer to texture */ void copy(int id, int texture, int offset_x, int offset_y, int width, int height); }; class shader_t { KITTY_USING_MOVE_T(shader_internal_t, GLuint, std::numeric_limits::max(), { if (el != std::numeric_limits::max()) { ctx.DeleteShader(el); } }); public: std::string err_str(); static util::Either compile(const std::string_view &source, GLenum type); GLuint handle() const; private: shader_internal_t _shader; }; class buffer_t { KITTY_USING_MOVE_T(buffer_internal_t, GLuint, std::numeric_limits::max(), { if (el != std::numeric_limits::max()) { ctx.DeleteBuffers(1, &el); } }); public: static buffer_t make(util::buffer_t &&offsets, const char *block, const std::string_view &data); GLuint handle() const; const char *block() const; void update(const std::string_view &view, std::size_t offset = 0); void update(std::string_view *members, std::size_t count, std::size_t offset = 0); private: const char *_block; std::size_t _size; util::buffer_t _offsets; buffer_internal_t _buffer; }; class program_t { KITTY_USING_MOVE_T(program_internal_t, GLuint, std::numeric_limits::max(), { if (el != std::numeric_limits::max()) { ctx.DeleteProgram(el); } }); public: std::string err_str(); static util::Either link(const shader_t &vert, const shader_t &frag); void bind(const buffer_t &buffer); std::optional uniform(const char *block, std::pair *members, std::size_t count); GLuint handle() const; private: program_internal_t _program; }; } // namespace gl namespace gbm { struct device; typedef void (*device_destroy_fn)(device *gbm); typedef device *(*create_device_fn)(int fd); extern device_destroy_fn device_destroy; extern create_device_fn create_device; using gbm_t = util::dyn_safe_ptr; int init(); } // namespace gbm namespace egl { using display_t = util::dyn_safe_ptr_v2; struct rgb_img_t { display_t::pointer display; EGLImage xrgb8; gl::tex_t tex; }; struct nv12_img_t { display_t::pointer display; EGLImage r8; EGLImage bg88; gl::tex_t tex; gl::frame_buf_t buf; // sizeof(va::DRMPRIMESurfaceDescriptor::objects) / sizeof(va::DRMPRIMESurfaceDescriptor::objects[0]); static constexpr std::size_t num_fds = 4; std::array fds; }; KITTY_USING_MOVE_T(rgb_t, rgb_img_t, , { if (el.xrgb8) { eglDestroyImage(el.display, el.xrgb8); } }); KITTY_USING_MOVE_T(nv12_t, nv12_img_t, , { if (el.r8) { eglDestroyImage(el.display, el.r8); } if (el.bg88) { eglDestroyImage(el.display, el.bg88); } }); KITTY_USING_MOVE_T(ctx_t, (std::tuple), , { TUPLE_2D_REF(disp, ctx, el); if (ctx) { eglMakeCurrent(disp, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); eglDestroyContext(disp, ctx); } }); struct surface_descriptor_t { int width; int height; int fds[4]; std::uint32_t fourcc; std::uint64_t modifier; std::uint32_t pitches[4]; std::uint32_t offsets[4]; }; display_t make_display(std::variant native_display); std::optional make_ctx(display_t::pointer display); std::optional import_source( display_t::pointer egl_display, const surface_descriptor_t &xrgb ); rgb_t create_blank(platf::img_t &img); std::optional import_target( display_t::pointer egl_display, std::array &&fds, const surface_descriptor_t &y, const surface_descriptor_t &uv ); /** * @brief Creates biplanar YUV textures to render into. * @param width Width of the target frame. * @param height Height of the target frame. * @param format Format of the target frame. * @return The new RGB texture. */ std::optional create_target(int width, int height, AVPixelFormat format); class cursor_t: public platf::img_t { public: int x, y; int src_w, src_h; unsigned long serial; std::vector buffer; }; // Allow cursor and the underlying image to be kept together class img_descriptor_t: public cursor_t { public: ~img_descriptor_t() { reset(); } void reset() { for (auto x = 0; x < 4; ++x) { if (sd.fds[x] >= 0) { close(sd.fds[x]); sd.fds[x] = -1; } } } surface_descriptor_t sd; // Increment sequence when new rgb_t needs to be created std::uint64_t sequence; }; class sws_t { public: static std::optional make(int in_width, int in_height, int out_width, int out_height, gl::tex_t &&tex); static std::optional make(int in_width, int in_height, int out_width, int out_height, AVPixelFormat format); // Convert the loaded image into the first two framebuffers int convert(gl::frame_buf_t &fb); // Make an area of the image black int blank(gl::frame_buf_t &fb, int offsetX, int offsetY, int width, int height); void load_ram(platf::img_t &img); void load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture); void apply_colorspace(const video::sunshine_colorspace_t &colorspace); // The first texture is the monitor image. // The second texture is the cursor image gl::tex_t tex; // The cursor image will be blended into this framebuffer gl::frame_buf_t cursor_framebuffer; gl::frame_buf_t copy_framebuffer; // Y - shader, UV - shader, Cursor - shader gl::program_t program[3]; gl::buffer_t color_matrix; int out_width, out_height; int in_width, in_height; int offsetX, offsetY; // Pointer to the texture to be converted to nv12 int loaded_texture; // Store latest cursor for load_vram std::uint64_t serial; }; bool fail(); } // namespace egl ================================================ FILE: src/platform/linux/input/inputtino.cpp ================================================ /** * @file src/platform/linux/input/inputtino.cpp * @brief Definitions for the inputtino Linux input handling. */ // lib includes #include #include // local includes #include "inputtino_common.h" #include "inputtino_gamepad.h" #include "inputtino_keyboard.h" #include "inputtino_mouse.h" #include "inputtino_pen.h" #include "inputtino_touch.h" #include "src/config.h" #include "src/platform/common.h" #include "src/utility.h" using namespace std::literals; namespace platf { input_t input() { return {new input_raw_t()}; } std::unique_ptr allocate_client_input_context(input_t &input) { return std::make_unique(input); } void freeInput(void *p) { auto *input = (input_raw_t *) p; delete input; } void move_mouse(input_t &input, int deltaX, int deltaY) { auto raw = (input_raw_t *) input.get(); platf::mouse::move(raw, deltaX, deltaY); } void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { auto raw = (input_raw_t *) input.get(); platf::mouse::move_abs(raw, touch_port, x, y); } void button_mouse(input_t &input, int button, bool release) { auto raw = (input_raw_t *) input.get(); platf::mouse::button(raw, button, release); } void scroll(input_t &input, int high_res_distance) { auto raw = (input_raw_t *) input.get(); platf::mouse::scroll(raw, high_res_distance); } void hscroll(input_t &input, int high_res_distance) { auto raw = (input_raw_t *) input.get(); platf::mouse::hscroll(raw, high_res_distance); } void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) { auto raw = (input_raw_t *) input.get(); platf::keyboard::update(raw, modcode, release, flags); } void unicode(input_t &input, char *utf8, int size) { auto raw = (input_raw_t *) input.get(); platf::keyboard::unicode(raw, utf8, size); } void touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { auto raw = (client_input_raw_t *) input; platf::touch::update(raw, touch_port, touch); } void pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { auto raw = (client_input_raw_t *) input; platf::pen::update(raw, touch_port, pen); } int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { auto raw = (input_raw_t *) input.get(); return platf::gamepad::alloc(raw, id, metadata, feedback_queue); } void free_gamepad(input_t &input, int nr) { auto raw = (input_raw_t *) input.get(); platf::gamepad::free(raw, nr); } void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { auto raw = (input_raw_t *) input.get(); platf::gamepad::update(raw, nr, gamepad_state); } void gamepad_touch(input_t &input, const gamepad_touch_t &touch) { auto raw = (input_raw_t *) input.get(); platf::gamepad::touch(raw, touch); } void gamepad_motion(input_t &input, const gamepad_motion_t &motion) { auto raw = (input_raw_t *) input.get(); platf::gamepad::motion(raw, motion); } void gamepad_battery(input_t &input, const gamepad_battery_t &battery) { auto raw = (input_raw_t *) input.get(); platf::gamepad::battery(raw, battery); } platform_caps::caps_t get_capabilities() { platform_caps::caps_t caps = 0; // TODO: if has_uinput caps |= platform_caps::pen_touch; // We support controller touchpad input only when emulating the PS5 controller if (config::input.gamepad == "ds5"sv || config::input.gamepad == "auto"sv) { caps |= platform_caps::controller_touch; } return caps; } util::point_t get_mouse_loc(input_t &input) { auto raw = (input_raw_t *) input.get(); return platf::mouse::get_location(raw); } std::vector &supported_gamepads(input_t *input) { return platf::gamepad::supported_gamepads(input); } } // namespace platf ================================================ FILE: src/platform/linux/input/inputtino_common.h ================================================ /** * @file src/platform/linux/input/inputtino_common.h * @brief Declarations for inputtino common input handling. */ #pragma once // lib includes #include #include #include // local includes #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" using namespace std::literals; namespace platf { using joypads_t = std::variant; struct joypad_state { std::unique_ptr joypad; gamepad_feedback_msg_t last_rumble; gamepad_feedback_msg_t last_rgb_led; }; struct input_raw_t { input_raw_t(): mouse(inputtino::Mouse::create({ .name = "Mouse passthrough", .vendor_id = 0xBEEF, .product_id = 0xDEAD, .version = 0x111, })), keyboard(inputtino::Keyboard::create({ .name = "Keyboard passthrough", .vendor_id = 0xBEEF, .product_id = 0xDEAD, .version = 0x111, })), gamepads(MAX_GAMEPADS) { if (!mouse) { BOOST_LOG(warning) << "Unable to create virtual mouse: " << mouse.getErrorMessage(); } if (!keyboard) { BOOST_LOG(warning) << "Unable to create virtual keyboard: " << keyboard.getErrorMessage(); } } ~input_raw_t() = default; // All devices are wrapped in Result because it might be that we aren't able to create them (ex: udev permission denied) inputtino::Result mouse; inputtino::Result keyboard; /** * A list of gamepads that are currently connected. * The pointer is shared because that state will be shared with background threads that deal with rumble and LED */ std::vector> gamepads; }; struct client_input_raw_t: public client_input_t { client_input_raw_t(input_t &input): touch(inputtino::TouchScreen::create({ .name = "Touch passthrough", .vendor_id = 0xBEEF, .product_id = 0xDEAD, .version = 0x111, })), pen(inputtino::PenTablet::create({ .name = "Pen passthrough", .vendor_id = 0xBEEF, .product_id = 0xDEAD, .version = 0x111, })) { global = (input_raw_t *) input.get(); if (!touch) { BOOST_LOG(warning) << "Unable to create virtual touch screen: " << touch.getErrorMessage(); } if (!pen) { BOOST_LOG(warning) << "Unable to create virtual pen tablet: " << pen.getErrorMessage(); } } input_raw_t *global; // Device state and handles for pen and touch input must be stored in the per-client // input context, because each connected client may be sending their own independent // pen/touch events. To maintain separation, we expose separate pen and touch devices // for each client. inputtino::Result touch; inputtino::Result pen; }; inline float deg2rad(float degree) { return degree * (M_PI / 180.f); } } // namespace platf ================================================ FILE: src/platform/linux/input/inputtino_gamepad.cpp ================================================ /** * @file src/platform/linux/input/inputtino_gamepad.cpp * @brief Definitions for inputtino gamepad input handling. */ // lib includes #include #include #include // local includes #include "inputtino_common.h" #include "inputtino_gamepad.h" #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" using namespace std::literals; namespace platf::gamepad { enum GamepadStatus { UHID_NOT_AVAILABLE = 0, ///< UHID is not available UINPUT_NOT_AVAILABLE, ///< UINPUT is not available XINPUT_NOT_AVAILABLE, ///< XINPUT is not available GAMEPAD_STATUS ///< Helper to indicate the number of status }; auto create_xbox_one() { return inputtino::XboxOneJoypad::create({.name = "Sunshine X-Box One (virtual) pad", // https://github.com/torvalds/linux/blob/master/drivers/input/joystick/xpad.c#L147 .vendor_id = 0x045E, .product_id = 0x02EA, .version = 0x0408}); } auto create_switch() { return inputtino::SwitchJoypad::create({.name = "Sunshine Nintendo (virtual) pad", // https://github.com/torvalds/linux/blob/master/drivers/hid/hid-ids.h#L981 .vendor_id = 0x057e, .product_id = 0x2009, .version = 0x8111}); } auto create_ds5(int globalIndex) { std::string device_mac = ""; // Inputtino checks empty() to generate a random MAC if (!config::input.ds5_inputtino_randomize_mac && globalIndex >= 0 && globalIndex <= 255) { // Generate private virtual device MAC based on gamepad globalIndex between 0 (00) and 255 (ff) device_mac = std::format("02:00:00:00:00:{:02x}", globalIndex); } return inputtino::PS5Joypad::create({.name = "Sunshine PS5 (virtual) pad", .vendor_id = 0x054C, .product_id = 0x0CE6, .version = 0x8111, .device_phys = device_mac, .device_uniq = device_mac}); } int alloc(input_raw_t *raw, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { ControllerType selectedGamepadType; if (config::input.gamepad == "xone"sv) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox One controller (manual selection)"sv; selectedGamepadType = XboxOneWired; } else if (config::input.gamepad == "ds5"sv) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualSense 5 controller (manual selection)"sv; selectedGamepadType = DualSenseWired; } else if (config::input.gamepad == "switch"sv) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Nintendo Pro controller (manual selection)"sv; selectedGamepadType = SwitchProWired; } else if (metadata.type == LI_CTYPE_XBOX) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox One controller (auto-selected by client-reported type)"sv; selectedGamepadType = XboxOneWired; } else if (metadata.type == LI_CTYPE_PS) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 5 controller (auto-selected by client-reported type)"sv; selectedGamepadType = DualSenseWired; } else if (metadata.type == LI_CTYPE_NINTENDO) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Nintendo Pro controller (auto-selected by client-reported type)"sv; selectedGamepadType = SwitchProWired; } else if (config::input.motion_as_ds4 && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 5 controller (auto-selected by motion sensor presence)"sv; selectedGamepadType = DualSenseWired; } else if (config::input.touchpad_as_ds4 && (metadata.capabilities & LI_CCAP_TOUCHPAD)) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 5 controller (auto-selected by touchpad presence)"sv; selectedGamepadType = DualSenseWired; } else { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox One controller (default)"sv; selectedGamepadType = XboxOneWired; } if (selectedGamepadType == XboxOneWired || selectedGamepadType == SwitchProWired) { if (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO)) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has motion sensors, but they are not usable when emulating a joypad different from DS5"sv; } if (metadata.capabilities & LI_CCAP_TOUCHPAD) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has a touchpad, but it is not usable when emulating a joypad different from DS5"sv; } if (metadata.capabilities & LI_CCAP_RGB_LED) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has an RGB LED, but it is not usable when emulating a joypad different from DS5"sv; } } else if (selectedGamepadType == DualSenseWired) { if (!(metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " is emulating a DualShock 5 controller, but the client gamepad doesn't have motion sensors active"sv; } if (!(metadata.capabilities & LI_CCAP_TOUCHPAD)) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " is emulating a DualShock 5 controller, but the client gamepad doesn't have a touchpad"sv; } } auto gamepad = std::make_shared(joypad_state {}); auto on_rumble_fn = [feedback_queue, idx = id.clientRelativeIndex, gamepad](int low_freq, int high_freq) { // Don't resend duplicate rumble data 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) { return; } gamepad_feedback_msg_t msg = gamepad_feedback_msg_t::make_rumble(idx, low_freq, high_freq); feedback_queue->raise(msg); gamepad->last_rumble = msg; }; switch (selectedGamepadType) { case XboxOneWired: { auto xOne = create_xbox_one(); if (xOne) { (*xOne).set_on_rumble(on_rumble_fn); gamepad->joypad = std::make_unique(std::move(*xOne)); raw->gamepads[id.globalIndex] = std::move(gamepad); return 0; } else { BOOST_LOG(warning) << "Unable to create virtual Xbox One controller: " << xOne.getErrorMessage(); return -1; } } case SwitchProWired: { auto switchPro = create_switch(); if (switchPro) { (*switchPro).set_on_rumble(on_rumble_fn); gamepad->joypad = std::make_unique(std::move(*switchPro)); raw->gamepads[id.globalIndex] = std::move(gamepad); return 0; } else { BOOST_LOG(warning) << "Unable to create virtual Switch Pro controller: " << switchPro.getErrorMessage(); return -1; } } case DualSenseWired: { auto ds5 = create_ds5(id.globalIndex); if (ds5) { (*ds5).set_on_rumble(on_rumble_fn); (*ds5).set_on_led([feedback_queue, idx = id.clientRelativeIndex, gamepad](int r, int g, int b) { // Don't resend duplicate LED data 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) { return; } auto msg = gamepad_feedback_msg_t::make_rgb_led(idx, r, g, b); feedback_queue->raise(msg); gamepad->last_rgb_led = msg; }); (*ds5).set_on_trigger_effect([feedback_queue, idx = id.clientRelativeIndex](const inputtino::PS5Joypad::TriggerEffect &trigger_effect) { 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)); }); // Activate the motion sensors feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_ACCEL, 100)); feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_GYRO, 100)); gamepad->joypad = std::make_unique(std::move(*ds5)); raw->gamepads[id.globalIndex] = std::move(gamepad); return 0; } else { BOOST_LOG(warning) << "Unable to create virtual DualShock 5 controller: " << ds5.getErrorMessage(); return -1; } } } return -1; } void free(input_raw_t *raw, int nr) { // This will call the destructor which in turn will stop the background threads for rumble and LED (and ultimately remove the joypad device) raw->gamepads[nr]->joypad.reset(); raw->gamepads[nr].reset(); } void update(input_raw_t *raw, int nr, const gamepad_state_t &gamepad_state) { auto gamepad = raw->gamepads[nr]; if (!gamepad) { return; } std::visit([gamepad_state](inputtino::Joypad &gc) { gc.set_pressed_buttons(gamepad_state.buttonFlags); gc.set_stick(inputtino::Joypad::LS, gamepad_state.lsX, gamepad_state.lsY); gc.set_stick(inputtino::Joypad::RS, gamepad_state.rsX, gamepad_state.rsY); gc.set_triggers(gamepad_state.lt, gamepad_state.rt); }, *gamepad->joypad); } void touch(input_raw_t *raw, const gamepad_touch_t &touch) { auto gamepad = raw->gamepads[touch.id.globalIndex]; if (!gamepad) { return; } // Only the PS5 controller supports touch input if (std::holds_alternative(*gamepad->joypad)) { if (touch.pressure > 0.5) { std::get(*gamepad->joypad).place_finger(touch.pointerId, touch.x * inputtino::PS5Joypad::touchpad_width, touch.y * inputtino::PS5Joypad::touchpad_height); } else { std::get(*gamepad->joypad).release_finger(touch.pointerId); } } } void motion(input_raw_t *raw, const gamepad_motion_t &motion) { auto gamepad = raw->gamepads[motion.id.globalIndex]; if (!gamepad) { return; } // Only the PS5 controller supports motion if (std::holds_alternative(*gamepad->joypad)) { switch (motion.motionType) { case LI_MOTION_TYPE_ACCEL: std::get(*gamepad->joypad).set_motion(inputtino::PS5Joypad::ACCELERATION, motion.x, motion.y, motion.z); break; case LI_MOTION_TYPE_GYRO: std::get(*gamepad->joypad).set_motion(inputtino::PS5Joypad::GYROSCOPE, deg2rad(motion.x), deg2rad(motion.y), deg2rad(motion.z)); break; } } } void battery(input_raw_t *raw, const gamepad_battery_t &battery) { auto gamepad = raw->gamepads[battery.id.globalIndex]; if (!gamepad) { return; } // Only the PS5 controller supports battery reports if (std::holds_alternative(*gamepad->joypad)) { inputtino::PS5Joypad::BATTERY_STATE state; switch (battery.state) { case LI_BATTERY_STATE_CHARGING: state = inputtino::PS5Joypad::BATTERY_CHARGHING; break; case LI_BATTERY_STATE_DISCHARGING: state = inputtino::PS5Joypad::BATTERY_DISCHARGING; break; case LI_BATTERY_STATE_FULL: state = inputtino::PS5Joypad::BATTERY_FULL; break; case LI_BATTERY_STATE_UNKNOWN: case LI_BATTERY_STATE_NOT_PRESENT: default: return; } if (battery.percentage != LI_BATTERY_PERCENTAGE_UNKNOWN) { std::get(*gamepad->joypad).set_battery(state, battery.percentage); } } } std::vector &supported_gamepads(input_t *input) { if (!input) { static std::vector gps { supported_gamepad_t {"auto", true, ""}, supported_gamepad_t {"xone", false, ""}, supported_gamepad_t {"ds5", false, ""}, supported_gamepad_t {"switch", false, ""}, }; return gps; } auto ds5 = create_ds5(-1); // Index -1 will result in a random MAC virtual device, which is fine for probing auto switchPro = create_switch(); auto xOne = create_xbox_one(); static std::vector gps { supported_gamepad_t {"auto", true, ""}, supported_gamepad_t {"xone", static_cast(xOne), !xOne ? xOne.getErrorMessage() : ""}, supported_gamepad_t {"ds5", static_cast(ds5), !ds5 ? ds5.getErrorMessage() : ""}, supported_gamepad_t {"switch", static_cast(switchPro), !switchPro ? switchPro.getErrorMessage() : ""}, }; for (auto &[name, is_enabled, reason_disabled] : gps) { if (!is_enabled) { BOOST_LOG(warning) << "Gamepad " << name << " is disabled due to " << reason_disabled; } } return gps; } } // namespace platf::gamepad ================================================ FILE: src/platform/linux/input/inputtino_gamepad.h ================================================ /** * @file src/platform/linux/input/inputtino_gamepad.h * @brief Declarations for inputtino gamepad input handling. */ #pragma once // lib includes #include #include #include // local includes #include "inputtino_common.h" #include "src/platform/common.h" using namespace std::literals; namespace platf::gamepad { enum ControllerType { XboxOneWired, ///< Xbox One Wired Controller DualSenseWired, ///< DualSense Wired Controller SwitchProWired ///< Switch Pro Wired Controller }; int alloc(input_raw_t *raw, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue); void free(input_raw_t *raw, int nr); void update(input_raw_t *raw, int nr, const gamepad_state_t &gamepad_state); void touch(input_raw_t *raw, const gamepad_touch_t &touch); void motion(input_raw_t *raw, const gamepad_motion_t &motion); void battery(input_raw_t *raw, const gamepad_battery_t &battery); std::vector &supported_gamepads(input_t *input); } // namespace platf::gamepad ================================================ FILE: src/platform/linux/input/inputtino_keyboard.cpp ================================================ /** * @file src/platform/linux/input/inputtino_keyboard.cpp * @brief Definitions for inputtino keyboard input handling. */ // lib includes #include #include #include // local includes #include "inputtino_common.h" #include "inputtino_keyboard.h" #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" using namespace std::literals; namespace platf::keyboard { /** * Takes an UTF-32 encoded string and returns a hex string representation of the bytes (uppercase) * * ex: ['👱'] = "1F471" // see UTF encoding at https://www.compart.com/en/unicode/U+1F471 * * adapted from: https://stackoverflow.com/a/7639754 */ std::string to_hex(const std::basic_string &str) { std::stringstream ss; ss << std::hex << std::setfill('0'); for (const auto &ch : str) { ss << static_cast(ch); } std::string hex_unicode(ss.str()); std::ranges::transform(hex_unicode, hex_unicode.begin(), ::toupper); return hex_unicode; } /** * A map of linux scan code -> Moonlight keyboard code */ static const std::map key_mappings = { {KEY_BACKSPACE, 0x08}, {KEY_TAB, 0x09}, {KEY_ENTER, 0x0D}, {KEY_LEFTSHIFT, 0x10}, {KEY_LEFTCTRL, 0x11}, {KEY_CAPSLOCK, 0x14}, {KEY_ESC, 0x1B}, {KEY_SPACE, 0x20}, {KEY_PAGEUP, 0x21}, {KEY_PAGEDOWN, 0x22}, {KEY_END, 0x23}, {KEY_HOME, 0x24}, {KEY_LEFT, 0x25}, {KEY_UP, 0x26}, {KEY_RIGHT, 0x27}, {KEY_DOWN, 0x28}, {KEY_SYSRQ, 0x2C}, {KEY_INSERT, 0x2D}, {KEY_DELETE, 0x2E}, {KEY_0, 0x30}, {KEY_1, 0x31}, {KEY_2, 0x32}, {KEY_3, 0x33}, {KEY_4, 0x34}, {KEY_5, 0x35}, {KEY_6, 0x36}, {KEY_7, 0x37}, {KEY_8, 0x38}, {KEY_9, 0x39}, {KEY_A, 0x41}, {KEY_B, 0x42}, {KEY_C, 0x43}, {KEY_D, 0x44}, {KEY_E, 0x45}, {KEY_F, 0x46}, {KEY_G, 0x47}, {KEY_H, 0x48}, {KEY_I, 0x49}, {KEY_J, 0x4A}, {KEY_K, 0x4B}, {KEY_L, 0x4C}, {KEY_M, 0x4D}, {KEY_N, 0x4E}, {KEY_O, 0x4F}, {KEY_P, 0x50}, {KEY_Q, 0x51}, {KEY_R, 0x52}, {KEY_S, 0x53}, {KEY_T, 0x54}, {KEY_U, 0x55}, {KEY_V, 0x56}, {KEY_W, 0x57}, {KEY_X, 0x58}, {KEY_Y, 0x59}, {KEY_Z, 0x5A}, {KEY_LEFTMETA, 0x5B}, {KEY_RIGHTMETA, 0x5C}, {KEY_KP0, 0x60}, {KEY_KP1, 0x61}, {KEY_KP2, 0x62}, {KEY_KP3, 0x63}, {KEY_KP4, 0x64}, {KEY_KP5, 0x65}, {KEY_KP6, 0x66}, {KEY_KP7, 0x67}, {KEY_KP8, 0x68}, {KEY_KP9, 0x69}, {KEY_KPASTERISK, 0x6A}, {KEY_KPPLUS, 0x6B}, {KEY_KPMINUS, 0x6D}, {KEY_KPDOT, 0x6E}, {KEY_KPSLASH, 0x6F}, {KEY_F1, 0x70}, {KEY_F2, 0x71}, {KEY_F3, 0x72}, {KEY_F4, 0x73}, {KEY_F5, 0x74}, {KEY_F6, 0x75}, {KEY_F7, 0x76}, {KEY_F8, 0x77}, {KEY_F9, 0x78}, {KEY_F10, 0x79}, {KEY_F11, 0x7A}, {KEY_F12, 0x7B}, {KEY_NUMLOCK, 0x90}, {KEY_SCROLLLOCK, 0x91}, {KEY_LEFTSHIFT, 0xA0}, {KEY_RIGHTSHIFT, 0xA1}, {KEY_LEFTCTRL, 0xA2}, {KEY_RIGHTCTRL, 0xA3}, {KEY_LEFTALT, 0xA4}, {KEY_RIGHTALT, 0xA5}, {KEY_SEMICOLON, 0xBA}, {KEY_EQUAL, 0xBB}, {KEY_COMMA, 0xBC}, {KEY_MINUS, 0xBD}, {KEY_DOT, 0xBE}, {KEY_SLASH, 0xBF}, {KEY_GRAVE, 0xC0}, {KEY_LEFTBRACE, 0xDB}, {KEY_BACKSLASH, 0xDC}, {KEY_RIGHTBRACE, 0xDD}, {KEY_APOSTROPHE, 0xDE}, {KEY_102ND, 0xE2} }; void update(input_raw_t *raw, uint16_t modcode, bool release, uint8_t flags) { if (raw->keyboard) { if (release) { (*raw->keyboard).release(modcode); } else { (*raw->keyboard).press(modcode); } } } void unicode(input_raw_t *raw, char *utf8, int size) { if (raw->keyboard) { /* Reading input text as UTF-8 */ auto utf8_str = boost::locale::conv::to_utf(utf8, utf8 + size, "UTF-8"); /* Converting to UTF-32 */ auto utf32_str = boost::locale::conv::utf_to_utf(utf8_str); /* To HEX string */ auto hex_unicode = to_hex(utf32_str); BOOST_LOG(debug) << "Unicode, typing U+"sv << hex_unicode; /* pressing + + U */ (*raw->keyboard).press(0xA2); // LEFTCTRL (*raw->keyboard).press(0xA0); // LEFTSHIFT (*raw->keyboard).press(0x55); // U (*raw->keyboard).release(0x55); // U /* input each HEX character */ for (auto &ch : hex_unicode) { auto key_str = "KEY_"s + ch; auto keycode = libevdev_event_code_from_name(EV_KEY, key_str.c_str()); auto wincode = key_mappings.find(keycode); if (keycode == -1 || wincode == key_mappings.end()) { BOOST_LOG(warning) << "Unicode, unable to find keycode for: "sv << ch; } else { (*raw->keyboard).press(wincode->second); (*raw->keyboard).release(wincode->second); } } /* releasing and */ (*raw->keyboard).release(0xA0); // LEFTSHIFT (*raw->keyboard).release(0xA2); // LEFTCTRL } } } // namespace platf::keyboard ================================================ FILE: src/platform/linux/input/inputtino_keyboard.h ================================================ /** * @file src/platform/linux/input/inputtino_keyboard.h * @brief Declarations for inputtino keyboard input handling. */ #pragma once // lib includes #include #include #include // local includes #include "inputtino_common.h" using namespace std::literals; namespace platf::keyboard { void update(input_raw_t *raw, uint16_t modcode, bool release, uint8_t flags); void unicode(input_raw_t *raw, char *utf8, int size); } // namespace platf::keyboard ================================================ FILE: src/platform/linux/input/inputtino_mouse.cpp ================================================ /** * @file src/platform/linux/input/inputtino_mouse.cpp * @brief Definitions for inputtino mouse input handling. */ // lib includes #include #include #include // local includes #include "inputtino_common.h" #include "inputtino_mouse.h" #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" using namespace std::literals; namespace platf::mouse { void move(input_raw_t *raw, int deltaX, int deltaY) { if (raw->mouse) { (*raw->mouse).move(deltaX, deltaY); } } void move_abs(input_raw_t *raw, const touch_port_t &touch_port, float x, float y) { if (raw->mouse) { (*raw->mouse).move_abs(x, y, touch_port.width, touch_port.height); } } void button(input_raw_t *raw, int button, bool release) { if (raw->mouse) { inputtino::Mouse::MOUSE_BUTTON btn_type; switch (button) { case BUTTON_LEFT: btn_type = inputtino::Mouse::LEFT; break; case BUTTON_MIDDLE: btn_type = inputtino::Mouse::MIDDLE; break; case BUTTON_RIGHT: btn_type = inputtino::Mouse::RIGHT; break; case BUTTON_X1: btn_type = inputtino::Mouse::SIDE; break; case BUTTON_X2: btn_type = inputtino::Mouse::EXTRA; break; default: BOOST_LOG(warning) << "Unknown mouse button: " << button; return; } if (release) { (*raw->mouse).release(btn_type); } else { (*raw->mouse).press(btn_type); } } } void scroll(input_raw_t *raw, int high_res_distance) { if (raw->mouse) { (*raw->mouse).vertical_scroll(high_res_distance); } } void hscroll(input_raw_t *raw, int high_res_distance) { if (raw->mouse) { (*raw->mouse).horizontal_scroll(high_res_distance); } } util::point_t get_location(input_raw_t *raw) { if (raw->mouse) { // TODO: decide what to do after https://github.com/games-on-whales/inputtino/issues/6 is resolved. // TODO: auto x = (*raw->mouse).get_absolute_x(); // TODO: auto y = (*raw->mouse).get_absolute_y(); return {0, 0}; } return {0, 0}; } } // namespace platf::mouse ================================================ FILE: src/platform/linux/input/inputtino_mouse.h ================================================ /** * @file src/platform/linux/input/inputtino_mouse.h * @brief Declarations for inputtino mouse input handling. */ #pragma once // lib includes #include #include #include // local includes #include "inputtino_common.h" #include "src/platform/common.h" using namespace std::literals; namespace platf::mouse { void move(input_raw_t *raw, int deltaX, int deltaY); void move_abs(input_raw_t *raw, const touch_port_t &touch_port, float x, float y); void button(input_raw_t *raw, int button, bool release); void scroll(input_raw_t *raw, int high_res_distance); void hscroll(input_raw_t *raw, int high_res_distance); util::point_t get_location(input_raw_t *raw); } // namespace platf::mouse ================================================ FILE: src/platform/linux/input/inputtino_pen.cpp ================================================ /** * @file src/platform/linux/input/inputtino_pen.cpp * @brief Definitions for inputtino pen input handling. */ // lib includes #include #include #include // local includes #include "inputtino_common.h" #include "inputtino_pen.h" #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" using namespace std::literals; namespace platf::pen { void update(client_input_raw_t *raw, const touch_port_t &touch_port, const pen_input_t &pen) { if (raw->pen) { // First set the buttons (*raw->pen).set_btn(inputtino::PenTablet::PRIMARY, pen.penButtons & LI_PEN_BUTTON_PRIMARY); (*raw->pen).set_btn(inputtino::PenTablet::SECONDARY, pen.penButtons & LI_PEN_BUTTON_SECONDARY); (*raw->pen).set_btn(inputtino::PenTablet::TERTIARY, pen.penButtons & LI_PEN_BUTTON_TERTIARY); // Set the tool inputtino::PenTablet::TOOL_TYPE tool; switch (pen.toolType) { case LI_TOOL_TYPE_PEN: tool = inputtino::PenTablet::PEN; break; case LI_TOOL_TYPE_ERASER: tool = inputtino::PenTablet::ERASER; break; default: tool = inputtino::PenTablet::SAME_AS_BEFORE; break; } // Normalize rotation value to 0-359 degree range auto rotation = pen.rotation; if (rotation != LI_ROT_UNKNOWN) { rotation %= 360; } // Here we receive: // - Rotation: degrees from vertical in Y dimension (parallel to screen, 0..360) // - Tilt: degrees from vertical in Z dimension (perpendicular to screen, 0..90) float tilt_x = 0; float tilt_y = 0; // Convert polar coordinates into Y tilt angles if (pen.tilt != LI_TILT_UNKNOWN && rotation != LI_ROT_UNKNOWN) { auto rotation_rads = deg2rad(rotation); auto tilt_rads = deg2rad(pen.tilt); auto r = std::sin(tilt_rads); auto z = std::cos(tilt_rads); tilt_x = std::atan2(std::sin(-rotation_rads) * r, z) * 180.f / M_PI; tilt_y = std::atan2(std::cos(-rotation_rads) * r, z) * 180.f / M_PI; } bool is_touching = pen.eventType == LI_TOUCH_EVENT_DOWN || pen.eventType == LI_TOUCH_EVENT_MOVE; (*raw->pen).place_tool(tool, pen.x, pen.y, is_touching ? pen.pressureOrDistance : -1, is_touching ? -1 : pen.pressureOrDistance, tilt_x, tilt_y); } } } // namespace platf::pen ================================================ FILE: src/platform/linux/input/inputtino_pen.h ================================================ /** * @file src/platform/linux/input/inputtino_pen.h * @brief Declarations for inputtino pen input handling. */ #pragma once // lib includes #include #include #include // local includes #include "inputtino_common.h" #include "src/platform/common.h" using namespace std::literals; namespace platf::pen { void update(client_input_raw_t *raw, const touch_port_t &touch_port, const pen_input_t &pen); } ================================================ FILE: src/platform/linux/input/inputtino_touch.cpp ================================================ /** * @file src/platform/linux/input/inputtino_touch.cpp * @brief Definitions for inputtino touch input handling. */ // lib includes #include #include #include // local includes #include "inputtino_common.h" #include "inputtino_touch.h" #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" using namespace std::literals; namespace platf::touch { void update(client_input_raw_t *raw, const touch_port_t &touch_port, const touch_input_t &touch) { if (raw->touch) { switch (touch.eventType) { case LI_TOUCH_EVENT_HOVER: case LI_TOUCH_EVENT_DOWN: case LI_TOUCH_EVENT_MOVE: { // Convert our 0..360 range to -90..90 relative to Y axis int adjusted_angle = touch.rotation; if (adjusted_angle > 90 && adjusted_angle < 270) { // Lower hemisphere adjusted_angle = 180 - adjusted_angle; } // Wrap the value if it's out of range if (adjusted_angle > 90) { adjusted_angle -= 360; } else if (adjusted_angle < -90) { adjusted_angle += 360; } (*raw->touch).place_finger(touch.pointerId, touch.x, touch.y, touch.pressureOrDistance, adjusted_angle); break; } case LI_TOUCH_EVENT_CANCEL: case LI_TOUCH_EVENT_UP: case LI_TOUCH_EVENT_HOVER_LEAVE: { (*raw->touch).release_finger(touch.pointerId); break; } // TODO: LI_TOUCH_EVENT_CANCEL_ALL } } } } // namespace platf::touch ================================================ FILE: src/platform/linux/input/inputtino_touch.h ================================================ /** * @file src/platform/linux/input/inputtino_touch.h * @brief Declarations for inputtino touch input handling. */ #pragma once // lib includes #include #include #include // local includes #include "inputtino_common.h" #include "src/platform/common.h" using namespace std::literals; namespace platf::touch { void update(client_input_raw_t *raw, const touch_port_t &touch_port, const touch_input_t &touch); } ================================================ FILE: src/platform/linux/kmsgrab.cpp ================================================ /** * @file src/platform/linux/kmsgrab.cpp * @brief Definitions for KMS screen capture. */ // standard includes #include #include #include #include #include // platform includes #include #include #include #include #include #include // local includes #include "cuda.h" #include "graphics.h" #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/round_robin.h" #include "src/utility.h" #include "src/video.h" #include "vaapi.h" #include "wayland.h" using namespace std::literals; namespace fs = std::filesystem; namespace platf { namespace kms { class cap_sys_admin { public: cap_sys_admin() { caps = cap_get_proc(); cap_value_t sys_admin = CAP_SYS_ADMIN; if (cap_set_flag(caps, CAP_EFFECTIVE, 1, &sys_admin, CAP_SET) || cap_set_proc(caps)) { BOOST_LOG(error) << "Failed to gain CAP_SYS_ADMIN"; } } ~cap_sys_admin() { cap_value_t sys_admin = CAP_SYS_ADMIN; if (cap_set_flag(caps, CAP_EFFECTIVE, 1, &sys_admin, CAP_CLEAR) || cap_set_proc(caps)) { BOOST_LOG(error) << "Failed to drop CAP_SYS_ADMIN"; } cap_free(caps); } cap_t caps; }; class wrapper_fb { public: wrapper_fb(drmModeFB *fb): fb {fb}, fb_id {fb->fb_id}, width {fb->width}, height {fb->height} { pixel_format = DRM_FORMAT_XRGB8888; modifier = DRM_FORMAT_MOD_INVALID; std::fill_n(handles, 4, 0); std::fill_n(pitches, 4, 0); std::fill_n(offsets, 4, 0); handles[0] = fb->handle; pitches[0] = fb->pitch; } wrapper_fb(drmModeFB2 *fb2): fb2 {fb2}, fb_id {fb2->fb_id}, width {fb2->width}, height {fb2->height} { pixel_format = fb2->pixel_format; modifier = (fb2->flags & DRM_MODE_FB_MODIFIERS) ? fb2->modifier : DRM_FORMAT_MOD_INVALID; memcpy(handles, fb2->handles, sizeof(handles)); memcpy(pitches, fb2->pitches, sizeof(pitches)); memcpy(offsets, fb2->offsets, sizeof(offsets)); } ~wrapper_fb() { if (fb) { drmModeFreeFB(fb); } else if (fb2) { drmModeFreeFB2(fb2); } } drmModeFB *fb = nullptr; drmModeFB2 *fb2 = nullptr; uint32_t fb_id; uint32_t width; uint32_t height; uint32_t pixel_format; uint64_t modifier; uint32_t handles[4]; uint32_t pitches[4]; uint32_t offsets[4]; }; using plane_res_t = util::safe_ptr; using encoder_t = util::safe_ptr; using res_t = util::safe_ptr; using plane_t = util::safe_ptr; using fb_t = std::unique_ptr; using crtc_t = util::safe_ptr; using obj_prop_t = util::safe_ptr; using prop_t = util::safe_ptr; using prop_blob_t = util::safe_ptr; using version_t = util::safe_ptr; using conn_type_count_t = std::map; static int env_width; static int env_height; std::string_view plane_type(std::uint64_t val) { switch (val) { case DRM_PLANE_TYPE_OVERLAY: return "DRM_PLANE_TYPE_OVERLAY"sv; case DRM_PLANE_TYPE_PRIMARY: return "DRM_PLANE_TYPE_PRIMARY"sv; case DRM_PLANE_TYPE_CURSOR: return "DRM_PLANE_TYPE_CURSOR"sv; } return "UNKNOWN"sv; } struct connector_t { // For example: HDMI-A or HDMI std::uint32_t type; // Equals zero if not applicable std::uint32_t crtc_id; // For example HDMI-A-{index} or HDMI-{index} std::uint32_t index; // ID of the connector std::uint32_t connector_id; bool connected; }; struct monitor_t { // Connector attributes std::uint32_t type; std::uint32_t index; // Monitor index in the global list std::uint32_t monitor_index; platf::touch_port_t viewport; }; struct card_descriptor_t { std::string path; std::map crtc_to_monitor; }; static std::vector card_descriptors; static std::uint32_t from_view(const std::string_view &string) { #define _CONVERT(x, y) \ if (string == x) \ return DRM_MODE_CONNECTOR_##y // This list was created from the following sources: // https://gitlab.freedesktop.org/mesa/drm/-/blob/main/xf86drmMode.c (drmModeGetConnectorTypeName) // https://gitlab.freedesktop.org/wayland/weston/-/blob/e74f2897b9408b6356a555a0ce59146836307ff5/libweston/backend-drm/drm.c#L1458-1477 // https://github.com/GNOME/mutter/blob/65d481594227ea7188c0416e8e00b57caeea214f/src/backends/meta-monitor-manager.c#L1618-L1639 _CONVERT("VGA"sv, VGA); _CONVERT("DVII"sv, DVII); _CONVERT("DVI-I"sv, DVII); _CONVERT("DVID"sv, DVID); _CONVERT("DVI-D"sv, DVID); _CONVERT("DVIA"sv, DVIA); _CONVERT("DVI-A"sv, DVIA); _CONVERT("Composite"sv, Composite); _CONVERT("SVIDEO"sv, SVIDEO); _CONVERT("S-Video"sv, SVIDEO); _CONVERT("LVDS"sv, LVDS); _CONVERT("Component"sv, Component); _CONVERT("9PinDIN"sv, 9PinDIN); _CONVERT("DIN"sv, 9PinDIN); _CONVERT("DisplayPort"sv, DisplayPort); _CONVERT("DP"sv, DisplayPort); _CONVERT("HDMIA"sv, HDMIA); _CONVERT("HDMI-A"sv, HDMIA); _CONVERT("HDMI"sv, HDMIA); _CONVERT("HDMIB"sv, HDMIB); _CONVERT("HDMI-B"sv, HDMIB); _CONVERT("TV"sv, TV); _CONVERT("eDP"sv, eDP); _CONVERT("VIRTUAL"sv, VIRTUAL); _CONVERT("Virtual"sv, VIRTUAL); _CONVERT("DSI"sv, DSI); _CONVERT("DPI"sv, DPI); _CONVERT("WRITEBACK"sv, WRITEBACK); _CONVERT("Writeback"sv, WRITEBACK); _CONVERT("SPI"sv, SPI); #ifdef DRM_MODE_CONNECTOR_USB _CONVERT("USB"sv, USB); #endif // If the string starts with "Unknown", it may have the raw type // value appended to the string. Let's try to read it. if (string.find("Unknown"sv) == 0) { std::uint32_t type; std::string null_terminated_string {string}; if (std::sscanf(null_terminated_string.c_str(), "Unknown%u", &type) == 1) { return type; } } BOOST_LOG(error) << "Unknown Monitor connector type ["sv << string << "]: Please report this to the GitHub issue tracker"sv; return DRM_MODE_CONNECTOR_Unknown; } class plane_it_t: public round_robin_util::it_wrap_t { public: plane_it_t(int fd, std::uint32_t *plane_p, std::uint32_t *end): fd {fd}, plane_p {plane_p}, end {end} { load_next_valid_plane(); } plane_it_t(int fd, std::uint32_t *end): fd {fd}, plane_p {end}, end {end} { } void load_next_valid_plane() { this->plane.reset(); for (; plane_p != end; ++plane_p) { plane_t plane = drmModeGetPlane(fd, *plane_p); if (!plane) { BOOST_LOG(error) << "Couldn't get drm plane ["sv << (end - plane_p) << "]: "sv << strerror(errno); continue; } this->plane = util::make_shared(plane.release()); break; } } void inc() { ++plane_p; load_next_valid_plane(); } bool eq(const plane_it_t &other) const { return plane_p == other.plane_p; } plane_t::pointer get() { return plane.get(); } int fd; std::uint32_t *plane_p; std::uint32_t *end; util::shared_t plane; }; struct cursor_t { // Public properties used during blending bool visible = false; std::int32_t x, y; std::uint32_t dst_w, dst_h; std::uint32_t src_w, src_h; std::vector pixels; unsigned long serial; // Private properties used for tracking cursor changes std::uint64_t prop_src_x, prop_src_y, prop_src_w, prop_src_h; std::uint32_t fb_id; }; class card_t { public: using connector_interal_t = util::safe_ptr; int init(const char *path) { cap_sys_admin admin; fd.el = open(path, O_RDWR); if (fd.el < 0) { BOOST_LOG(error) << "Couldn't open: "sv << path << ": "sv << strerror(errno); return -1; } version_t ver {drmGetVersion(fd.el)}; BOOST_LOG(info) << path << " -> "sv << ((ver && ver->name) ? ver->name : "UNKNOWN"); // Open the render node for this card to share with libva. // If it fails, we'll just share the primary node instead. char *rendernode_path = drmGetRenderDeviceNameFromFd(fd.el); if (rendernode_path) { BOOST_LOG(debug) << "Opening render node: "sv << rendernode_path; render_fd.el = open(rendernode_path, O_RDWR); if (render_fd.el < 0) { BOOST_LOG(warning) << "Couldn't open render node: "sv << rendernode_path << ": "sv << strerror(errno); render_fd.el = dup(fd.el); } free(rendernode_path); } else { BOOST_LOG(warning) << "No render device name for: "sv << path; render_fd.el = dup(fd.el); } if (drmSetClientCap(fd.el, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1)) { BOOST_LOG(error) << "GPU driver doesn't support universal planes: "sv << path; return -1; } if (drmSetClientCap(fd.el, DRM_CLIENT_CAP_ATOMIC, 1)) { BOOST_LOG(warning) << "GPU driver doesn't support atomic mode-setting: "sv << path; #if defined(SUNSHINE_BUILD_X11) // We won't be able to capture the mouse cursor with KMS on non-atomic drivers, // so fall back to X11 if it's available and the user didn't explicitly force KMS. if (window_system == window_system_e::X11 && config::video.capture != "kms") { BOOST_LOG(info) << "Avoiding KMS capture under X11 due to lack of atomic mode-setting"sv; return -1; } #endif BOOST_LOG(warning) << "Cursor capture may fail without atomic mode-setting support!"sv; } plane_res.reset(drmModeGetPlaneResources(fd.el)); if (!plane_res) { BOOST_LOG(error) << "Couldn't get drm plane resources"sv; return -1; } return 0; } fb_t fb(plane_t::pointer plane) { cap_sys_admin admin; auto fb2 = drmModeGetFB2(fd.el, plane->fb_id); if (fb2) { return std::make_unique(fb2); } auto fb = drmModeGetFB(fd.el, plane->fb_id); if (fb) { return std::make_unique(fb); } return nullptr; } crtc_t crtc(std::uint32_t id) { return drmModeGetCrtc(fd.el, id); } encoder_t encoder(std::uint32_t id) { return drmModeGetEncoder(fd.el, id); } res_t res() { return drmModeGetResources(fd.el); } bool is_nvidia() { version_t ver {drmGetVersion(fd.el)}; return ver && ver->name && strncmp(ver->name, "nvidia-drm", 10) == 0; } bool is_cursor(std::uint32_t plane_id) { auto props = plane_props(plane_id); for (auto &[prop, val] : props) { if (prop->name == "type"sv) { if (val == DRM_PLANE_TYPE_CURSOR) { return true; } else { return false; } } } return false; } std::optional prop_value_by_name(const std::vector> &props, std::string_view name) { for (auto &[prop, val] : props) { if (prop->name == name) { return val; } } return std::nullopt; } std::uint32_t get_panel_orientation(std::uint32_t plane_id) { auto props = plane_props(plane_id); auto value = prop_value_by_name(props, "rotation"sv); if (value) { return *value; } BOOST_LOG(error) << "Failed to determine panel orientation, defaulting to landscape."; return DRM_MODE_ROTATE_0; } int get_crtc_index_by_id(std::uint32_t crtc_id) { auto resources = res(); for (int i = 0; i < resources->count_crtcs; i++) { if (resources->crtcs[i] == crtc_id) { return i; } } return -1; } connector_interal_t connector(std::uint32_t id) { return drmModeGetConnector(fd.el, id); } std::vector monitors(conn_type_count_t &conn_type_count) { auto resources = res(); if (!resources) { BOOST_LOG(error) << "Couldn't get connector resources"sv; return {}; } std::vector monitors; std::for_each_n(resources->connectors, resources->count_connectors, [this, &conn_type_count, &monitors](std::uint32_t id) { auto conn = connector(id); std::uint32_t crtc_id = 0; if (conn->encoder_id) { auto enc = encoder(conn->encoder_id); if (enc) { crtc_id = enc->crtc_id; } } auto index = ++conn_type_count[conn->connector_type]; monitors.emplace_back(connector_t { conn->connector_type, crtc_id, index, conn->connector_id, conn->connection == DRM_MODE_CONNECTED, }); }); return monitors; } file_t handleFD(std::uint32_t handle) { file_t fb_fd; auto status = drmPrimeHandleToFD(fd.el, handle, 0 /* flags */, &fb_fd.el); if (status) { return {}; } return fb_fd; } std::vector> props(std::uint32_t id, std::uint32_t type) { obj_prop_t obj_prop = drmModeObjectGetProperties(fd.el, id, type); if (!obj_prop) { return {}; } std::vector> props; props.reserve(obj_prop->count_props); for (auto x = 0; x < obj_prop->count_props; ++x) { props.emplace_back(drmModeGetProperty(fd.el, obj_prop->props[x]), obj_prop->prop_values[x]); } return props; } std::vector> plane_props(std::uint32_t id) { return props(id, DRM_MODE_OBJECT_PLANE); } std::vector> crtc_props(std::uint32_t id) { return props(id, DRM_MODE_OBJECT_CRTC); } std::vector> connector_props(std::uint32_t id) { return props(id, DRM_MODE_OBJECT_CONNECTOR); } plane_t operator[](std::uint32_t index) { return drmModeGetPlane(fd.el, plane_res->planes[index]); } std::uint32_t count() { return plane_res->count_planes; } plane_it_t begin() const { return plane_it_t {fd.el, plane_res->planes, plane_res->planes + plane_res->count_planes}; } plane_it_t end() const { return plane_it_t {fd.el, plane_res->planes + plane_res->count_planes}; } file_t fd; file_t render_fd; plane_res_t plane_res; }; std::map map_crtc_to_monitor(const std::vector &connectors) { std::map result; for (auto &connector : connectors) { result.emplace(connector.crtc_id, monitor_t { connector.type, connector.index, }); } return result; } struct kms_img_t: public img_t { ~kms_img_t() override { delete[] data; data = nullptr; } }; void print(plane_t::pointer plane, fb_t::pointer fb, crtc_t::pointer crtc) { if (crtc) { BOOST_LOG(debug) << "crtc("sv << crtc->x << ", "sv << crtc->y << ')'; BOOST_LOG(debug) << "crtc("sv << crtc->width << ", "sv << crtc->height << ')'; BOOST_LOG(debug) << "plane->possible_crtcs == "sv << plane->possible_crtcs; } BOOST_LOG(debug) << "x("sv << plane->x << ") y("sv << plane->y << ") crtc_x("sv << plane->crtc_x << ") crtc_y("sv << plane->crtc_y << ") crtc_id("sv << plane->crtc_id << ')'; BOOST_LOG(debug) << "Resolution: "sv << fb->width << 'x' << fb->height << ": Pitch: "sv << fb->pitches[0] << ": Offset: "sv << fb->offsets[0]; std::stringstream ss; ss << "Format ["sv; std::for_each_n(plane->formats, plane->count_formats - 1, [&ss](auto format) { ss << util::view(format) << ", "sv; }); ss << util::view(plane->formats[plane->count_formats - 1]) << ']'; BOOST_LOG(debug) << ss.str(); } class display_t: public platf::display_t { public: display_t(mem_type_e mem_type): platf::display_t(), mem_type {mem_type} { } int init(const std::string &display_name, const ::video::config_t &config) { delay = std::chrono::nanoseconds {1s} / config.framerate; int monitor_index = util::from_view(display_name); int monitor = 0; fs::path card_dir {"/dev/dri"sv}; for (auto &entry : fs::directory_iterator {card_dir}) { auto file = entry.path().filename(); auto filestring = file.generic_string(); if (filestring.size() < 4 || std::string_view {filestring}.substr(0, 4) != "card"sv) { continue; } kms::card_t card; if (card.init(entry.path().c_str())) { continue; } // Skip non-Nvidia cards if we're looking for CUDA devices // unless NVENC is selected manually by the user if (mem_type == mem_type_e::cuda && !card.is_nvidia()) { BOOST_LOG(debug) << file << " is not a CUDA device"sv; if (config::video.encoder != "nvenc") { continue; } } auto end = std::end(card); for (auto plane = std::begin(card); plane != end; ++plane) { // Skip unused planes if (!plane->fb_id) { continue; } if (card.is_cursor(plane->plane_id)) { continue; } if (monitor != monitor_index) { ++monitor; continue; } auto fb = card.fb(plane.get()); if (!fb) { BOOST_LOG(error) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno); return -1; } if (!fb->handles[0]) { BOOST_LOG(error) << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Probably not permitted"sv; return -1; } for (int i = 0; i < 4; ++i) { if (!fb->handles[i]) { break; } auto fb_fd = card.handleFD(fb->handles[i]); if (fb_fd.el < 0) { BOOST_LOG(error) << "Couldn't get primary file descriptor for Framebuffer ["sv << fb->fb_id << "]: "sv << strerror(errno); continue; } } auto crtc = card.crtc(plane->crtc_id); if (!crtc) { BOOST_LOG(error) << "Couldn't get CRTC info: "sv << strerror(errno); continue; } BOOST_LOG(info) << "Found monitor for DRM screencasting"sv; // We need to find the correct /dev/dri/card{nr} to correlate the crtc_id with the monitor descriptor auto pos = std::find_if(std::begin(card_descriptors), std::end(card_descriptors), [&](card_descriptor_t &cd) { return cd.path == filestring; }); if (pos == std::end(card_descriptors)) { // This code path shouldn't happen, but it's there just in case. // card_descriptors is part of the guesswork after all. BOOST_LOG(error) << "Couldn't find ["sv << entry.path() << "]: This shouldn't have happened :/"sv; return -1; } // TODO: surf_sd = fb->to_sd(); kms::print(plane.get(), fb.get(), crtc.get()); img_width = fb->width; img_height = fb->height; img_offset_x = crtc->x; img_offset_y = crtc->y; this->env_width = ::platf::kms::env_width; this->env_height = ::platf::kms::env_height; auto monitor = pos->crtc_to_monitor.find(plane->crtc_id); if (monitor != std::end(pos->crtc_to_monitor)) { auto &viewport = monitor->second.viewport; width = viewport.width; height = viewport.height; switch (card.get_panel_orientation(plane->plane_id)) { case DRM_MODE_ROTATE_270: BOOST_LOG(debug) << "Detected panel orientation at 90, swapping width and height."; width = viewport.height; height = viewport.width; break; case DRM_MODE_ROTATE_90: case DRM_MODE_ROTATE_180: BOOST_LOG(warning) << "Panel orientation is unsupported, screen capture may not work correctly."; break; } offset_x = viewport.offset_x; offset_y = viewport.offset_y; } // This code path shouldn't happen, but it's there just in case. // crtc_to_monitor is part of the guesswork after all. else { BOOST_LOG(warning) << "Couldn't find crtc_id, this shouldn't have happened :\\"sv; width = crtc->width; height = crtc->height; offset_x = crtc->x; offset_y = crtc->y; } plane_id = plane->plane_id; crtc_id = plane->crtc_id; crtc_index = card.get_crtc_index_by_id(plane->crtc_id); // Find the connector for this CRTC kms::conn_type_count_t conn_type_count; for (auto &connector : card.monitors(conn_type_count)) { if (connector.crtc_id == crtc_id) { BOOST_LOG(info) << "Found connector ID ["sv << connector.connector_id << ']'; connector_id = connector.connector_id; auto connector_props = card.connector_props(*connector_id); hdr_metadata_blob_id = card.prop_value_by_name(connector_props, "HDR_OUTPUT_METADATA"sv); } } this->card = std::move(card); goto break_loop; } } BOOST_LOG(error) << "Couldn't find monitor ["sv << monitor_index << ']'; return -1; // Neatly break from nested for loop break_loop: // Look for the cursor plane for this CRTC cursor_plane_id = -1; auto end = std::end(card); for (auto plane = std::begin(card); plane != end; ++plane) { if (!card.is_cursor(plane->plane_id)) { continue; } // NB: We do not skip unused planes here because cursor planes // will look unused if the cursor is currently hidden. if (!(plane->possible_crtcs & (1 << crtc_index))) { // Skip cursor planes for other CRTCs continue; } else if (plane->possible_crtcs != (1 << crtc_index)) { // We assume a 1:1 mapping between cursor planes and CRTCs, which seems to // match the behavior of drivers in the real world. If it's violated, we'll // proceed anyway but print a warning in the log. BOOST_LOG(warning) << "Cursor plane spans multiple CRTCs!"sv; } BOOST_LOG(info) << "Found cursor plane ["sv << plane->plane_id << ']'; cursor_plane_id = plane->plane_id; break; } if (cursor_plane_id < 0) { BOOST_LOG(warning) << "No KMS cursor plane found. Cursor may not be displayed while streaming!"sv; } return 0; } bool is_hdr() { if (!hdr_metadata_blob_id || *hdr_metadata_blob_id == 0) { return false; } prop_blob_t hdr_metadata_blob = drmModeGetPropertyBlob(card.fd.el, *hdr_metadata_blob_id); if (hdr_metadata_blob == nullptr) { BOOST_LOG(error) << "Unable to get HDR metadata blob: "sv << strerror(errno); return false; } if (hdr_metadata_blob->length < sizeof(uint32_t) + sizeof(hdr_metadata_infoframe)) { BOOST_LOG(error) << "HDR metadata blob is too small: "sv << hdr_metadata_blob->length; return false; } auto raw_metadata = (hdr_output_metadata *) hdr_metadata_blob->data; if (raw_metadata->metadata_type != 0) { // HDMI_STATIC_METADATA_TYPE1 BOOST_LOG(error) << "Unknown HDMI_STATIC_METADATA_TYPE value: "sv << raw_metadata->metadata_type; return false; } if (raw_metadata->hdmi_metadata_type1.metadata_type != 0) { // Static Metadata Type 1 BOOST_LOG(error) << "Unknown secondary metadata type value: "sv << raw_metadata->hdmi_metadata_type1.metadata_type; return false; } // We only support Traditional Gamma SDR or SMPTE 2084 PQ HDR EOTFs. // Print a warning if we encounter any others. switch (raw_metadata->hdmi_metadata_type1.eotf) { case 0: // HDMI_EOTF_TRADITIONAL_GAMMA_SDR return false; case 1: // HDMI_EOTF_TRADITIONAL_GAMMA_HDR BOOST_LOG(warning) << "Unsupported HDR EOTF: Traditional Gamma"sv; return true; case 2: // HDMI_EOTF_SMPTE_ST2084 return true; case 3: // HDMI_EOTF_BT_2100_HLG BOOST_LOG(warning) << "Unsupported HDR EOTF: HLG"sv; return true; default: BOOST_LOG(warning) << "Unsupported HDR EOTF: "sv << raw_metadata->hdmi_metadata_type1.eotf; return true; } } bool get_hdr_metadata(SS_HDR_METADATA &metadata) { // This performs all the metadata validation if (!is_hdr()) { return false; } prop_blob_t hdr_metadata_blob = drmModeGetPropertyBlob(card.fd.el, *hdr_metadata_blob_id); if (hdr_metadata_blob == nullptr) { BOOST_LOG(error) << "Unable to get HDR metadata blob: "sv << strerror(errno); return false; } auto raw_metadata = (hdr_output_metadata *) hdr_metadata_blob->data; for (int i = 0; i < 3; i++) { metadata.displayPrimaries[i].x = raw_metadata->hdmi_metadata_type1.display_primaries[i].x; metadata.displayPrimaries[i].y = raw_metadata->hdmi_metadata_type1.display_primaries[i].y; } metadata.whitePoint.x = raw_metadata->hdmi_metadata_type1.white_point.x; metadata.whitePoint.y = raw_metadata->hdmi_metadata_type1.white_point.y; metadata.maxDisplayLuminance = raw_metadata->hdmi_metadata_type1.max_display_mastering_luminance; metadata.minDisplayLuminance = raw_metadata->hdmi_metadata_type1.min_display_mastering_luminance; metadata.maxContentLightLevel = raw_metadata->hdmi_metadata_type1.max_cll; metadata.maxFrameAverageLightLevel = raw_metadata->hdmi_metadata_type1.max_fall; return true; } void update_cursor() { if (cursor_plane_id < 0) { return; } plane_t plane = drmModeGetPlane(card.fd.el, cursor_plane_id); std::optional prop_crtc_x; std::optional prop_crtc_y; std::optional prop_crtc_w; std::optional prop_crtc_h; std::optional prop_src_x; std::optional prop_src_y; std::optional prop_src_w; std::optional prop_src_h; auto props = card.plane_props(cursor_plane_id); for (auto &[prop, val] : props) { if (prop->name == "CRTC_X"sv) { prop_crtc_x = val; } else if (prop->name == "CRTC_Y"sv) { prop_crtc_y = val; } else if (prop->name == "CRTC_W"sv) { prop_crtc_w = val; } else if (prop->name == "CRTC_H"sv) { prop_crtc_h = val; } else if (prop->name == "SRC_X"sv) { prop_src_x = val; } else if (prop->name == "SRC_Y"sv) { prop_src_y = val; } else if (prop->name == "SRC_W"sv) { prop_src_w = val; } else if (prop->name == "SRC_H"sv) { prop_src_h = val; } } if (!prop_crtc_w || !prop_crtc_h || !prop_crtc_x || !prop_crtc_y) { BOOST_LOG(error) << "Cursor plane is missing required plane CRTC properties!"sv; BOOST_LOG(error) << "Atomic mode-setting must be enabled to capture the cursor!"sv; cursor_plane_id = -1; captured_cursor.visible = false; return; } if (!prop_src_x || !prop_src_y || !prop_src_w || !prop_src_h) { BOOST_LOG(error) << "Cursor plane is missing required plane SRC properties!"sv; BOOST_LOG(error) << "Atomic mode-setting must be enabled to capture the cursor!"sv; cursor_plane_id = -1; captured_cursor.visible = false; return; } // Update the cursor position and size unconditionally captured_cursor.x = *prop_crtc_x; captured_cursor.y = *prop_crtc_y; captured_cursor.dst_w = *prop_crtc_w; captured_cursor.dst_h = *prop_crtc_h; // We're technically cheating a bit here by assuming that we can detect // changes to the cursor plane via property adjustments. If this isn't // true, we'll really have to mmap() the dmabuf and draw that every time. bool cursor_dirty = false; if (!plane->fb_id) { captured_cursor.visible = false; captured_cursor.fb_id = 0; } else if (plane->fb_id != captured_cursor.fb_id) { BOOST_LOG(debug) << "Refreshing cursor image after FB changed"sv; cursor_dirty = true; } else if (*prop_src_x != captured_cursor.prop_src_x || *prop_src_y != captured_cursor.prop_src_y || *prop_src_w != captured_cursor.prop_src_w || *prop_src_h != captured_cursor.prop_src_h) { BOOST_LOG(debug) << "Refreshing cursor image after source dimensions changed"sv; cursor_dirty = true; } // If the cursor is dirty, map it so we can download the new image if (cursor_dirty) { auto fb = card.fb(plane.get()); if (!fb || !fb->handles[0]) { // This means the cursor is not currently visible captured_cursor.visible = false; return; } // All known cursor planes in the wild are ARGB8888 if (fb->pixel_format != DRM_FORMAT_ARGB8888) { BOOST_LOG(error) << "Unsupported non-ARGB8888 cursor format: "sv << fb->pixel_format; captured_cursor.visible = false; cursor_plane_id = -1; return; } // All known cursor planes in the wild require linear buffers if (fb->modifier != DRM_FORMAT_MOD_LINEAR && fb->modifier != DRM_FORMAT_MOD_INVALID) { BOOST_LOG(error) << "Unsupported non-linear cursor modifier: "sv << fb->modifier; captured_cursor.visible = false; cursor_plane_id = -1; return; } // The SRC_* properties are in Q16.16 fixed point, so convert to integers auto src_x = *prop_src_x >> 16; auto src_y = *prop_src_y >> 16; auto src_w = *prop_src_w >> 16; auto src_h = *prop_src_h >> 16; // Check for a legal source rectangle if (src_x + src_w > fb->width || src_y + src_h > fb->height) { BOOST_LOG(error) << "Illegal source size: ["sv << src_x + src_w << ',' << src_y + src_h << "] > ["sv << fb->width << ',' << fb->height << ']'; captured_cursor.visible = false; return; } file_t plane_fd = card.handleFD(fb->handles[0]); if (plane_fd.el < 0) { captured_cursor.visible = false; return; } // We will map the entire region, but only copy what the source rectangle specifies size_t mapped_size = ((size_t) fb->pitches[0]) * fb->height; void *mapped_data = mmap(nullptr, mapped_size, PROT_READ, MAP_SHARED, plane_fd.el, fb->offsets[0]); // If we got ENOSYS back, let's try to map it as a dumb buffer instead (required for Nvidia GPUs) if (mapped_data == MAP_FAILED && errno == ENOSYS) { drm_mode_map_dumb map = {}; map.handle = fb->handles[0]; if (drmIoctl(card.fd.el, DRM_IOCTL_MODE_MAP_DUMB, &map) < 0) { BOOST_LOG(error) << "Failed to map cursor FB as dumb buffer: "sv << strerror(errno); captured_cursor.visible = false; return; } mapped_data = mmap(nullptr, mapped_size, PROT_READ, MAP_SHARED, card.fd.el, map.offset); } if (mapped_data == MAP_FAILED) { BOOST_LOG(error) << "Failed to mmap cursor FB: "sv << strerror(errno); captured_cursor.visible = false; return; } captured_cursor.pixels.resize(src_w * src_h * 4); // Prepare to read the dmabuf from the CPU struct dma_buf_sync sync; sync.flags = DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ; drmIoctl(plane_fd.el, DMA_BUF_IOCTL_SYNC, &sync); // If the image is tightly packed, copy it in one shot if (fb->pitches[0] == src_w * 4 && src_x == 0) { memcpy(captured_cursor.pixels.data(), &((std::uint8_t *) mapped_data)[src_y * fb->pitches[0]], src_h * fb->pitches[0]); } else { // Copy row by row to deal with mismatched pitch or an X offset auto pixel_dst = captured_cursor.pixels.data(); for (int y = 0; y < src_h; y++) { memcpy(&pixel_dst[y * (src_w * 4)], &((std::uint8_t *) mapped_data)[(y + src_y) * fb->pitches[0] + (src_x * 4)], src_w * 4); } } // End the CPU read and unmap the dmabuf sync.flags = DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ; drmIoctl(plane_fd.el, DMA_BUF_IOCTL_SYNC, &sync); munmap(mapped_data, mapped_size); captured_cursor.visible = true; captured_cursor.src_w = src_w; captured_cursor.src_h = src_h; captured_cursor.prop_src_x = *prop_src_x; captured_cursor.prop_src_y = *prop_src_y; captured_cursor.prop_src_w = *prop_src_w; captured_cursor.prop_src_h = *prop_src_h; captured_cursor.fb_id = plane->fb_id; ++captured_cursor.serial; } } inline capture_e refresh(file_t *file, egl::surface_descriptor_t *sd, std::optional &frame_timestamp) { // Check for a change in HDR metadata if (connector_id) { auto connector_props = card.connector_props(*connector_id); if (hdr_metadata_blob_id != card.prop_value_by_name(connector_props, "HDR_OUTPUT_METADATA"sv)) { BOOST_LOG(info) << "Reinitializing capture after HDR metadata change"sv; return capture_e::reinit; } } plane_t plane = drmModeGetPlane(card.fd.el, plane_id); frame_timestamp = std::chrono::steady_clock::now(); auto fb = card.fb(plane.get()); if (!fb) { // This can happen if the display is being reconfigured while streaming BOOST_LOG(warning) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno); return capture_e::timeout; } if (!fb->handles[0]) { BOOST_LOG(error) << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Probably not permitted"sv; return capture_e::error; } for (int y = 0; y < 4; ++y) { if (!fb->handles[y]) { // setting sd->fds[y] to a negative value indicates that sd->offsets[y] and sd->pitches[y] // are uninitialized and contain invalid values. sd->fds[y] = -1; // It's not clear whether there could still be valid handles left. // So, continue anyway. // TODO: Is this redundant? continue; } file[y] = card.handleFD(fb->handles[y]); if (file[y].el < 0) { BOOST_LOG(error) << "Couldn't get primary file descriptor for Framebuffer ["sv << fb->fb_id << "]: "sv << strerror(errno); return capture_e::error; } sd->fds[y] = file[y].el; sd->offsets[y] = fb->offsets[y]; sd->pitches[y] = fb->pitches[y]; } sd->width = fb->width; sd->height = fb->height; sd->modifier = fb->modifier; sd->fourcc = fb->pixel_format; if ( fb->width != img_width || fb->height != img_height ) { return capture_e::reinit; } update_cursor(); return capture_e::ok; } mem_type_e mem_type; std::chrono::nanoseconds delay; int img_width, img_height; int img_offset_x, img_offset_y; int plane_id; int crtc_id; int crtc_index; std::optional connector_id; std::optional hdr_metadata_blob_id; int cursor_plane_id; cursor_t captured_cursor {}; card_t card; }; class display_ram_t: public display_t { public: display_ram_t(mem_type_e mem_type): display_t(mem_type) { } int init(const std::string &display_name, const ::video::config_t &config) { if (!gbm::create_device) { BOOST_LOG(warning) << "libgbm not initialized"sv; return -1; } if (display_t::init(display_name, config)) { return -1; } gbm.reset(gbm::create_device(card.fd.el)); if (!gbm) { BOOST_LOG(error) << "Couldn't create GBM device: ["sv << util::hex(eglGetError()).to_string_view() << ']'; return -1; } display = egl::make_display(gbm.get()); if (!display) { return -1; } auto ctx_opt = egl::make_ctx(display.get()); if (!ctx_opt) { return -1; } ctx = std::move(*ctx_opt); return 0; } capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); sleep_overshoot_logger.reset(); while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { std::this_thread::sleep_for(next_frame - now); sleep_overshoot_logger.first_point(next_frame); sleep_overshoot_logger.second_point_now_and_log(); } next_frame += delay; if (next_frame < now) { // some major slowdown happened; we couldn't keep up next_frame = now + delay; } std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: case platf::capture_e::interrupted: return status; case platf::capture_e::timeout: if (!push_captured_image_cb(std::move(img_out), false)) { return platf::capture_e::ok; } break; case platf::capture_e::ok: if (!push_captured_image_cb(std::move(img_out), true)) { return platf::capture_e::ok; } break; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; return status; } } return capture_e::ok; } std::unique_ptr make_avcodec_encode_device(pix_fmt_e pix_fmt) override { #ifdef SUNSHINE_BUILD_VAAPI if (mem_type == mem_type_e::vaapi) { return va::make_avcodec_encode_device(width, height, false); } #endif #ifdef SUNSHINE_BUILD_CUDA if (mem_type == mem_type_e::cuda) { return cuda::make_avcodec_encode_device(width, height, false); } #endif return std::make_unique(); } void blend_cursor(img_t &img) { // TODO: Cursor scaling is not supported in this codepath. // We always draw the cursor at the source size. auto pixels = (int *) img.data; int32_t screen_height = img.height; int32_t screen_width = img.width; // This is the position in the target that we will start drawing the cursor auto cursor_x = std::max(0, captured_cursor.x - img_offset_x); auto cursor_y = std::max(0, captured_cursor.y - img_offset_y); // If the cursor is partially off screen, the coordinates may be negative // which means we will draw the top-right visible portion of the cursor only. auto cursor_delta_x = cursor_x - std::max(-captured_cursor.src_w, captured_cursor.x - img_offset_x); auto cursor_delta_y = cursor_y - std::max(-captured_cursor.src_h, captured_cursor.y - img_offset_y); auto delta_height = std::min(captured_cursor.src_h, std::max(0, screen_height - cursor_y)) - cursor_delta_y; auto delta_width = std::min(captured_cursor.src_w, std::max(0, screen_width - cursor_x)) - cursor_delta_x; for (auto y = 0; y < delta_height; ++y) { // Offset into the cursor image to skip drawing the parts of the cursor image that are off screen // // NB: We must access the elements via the data() function because cursor_end may point to the // the first element beyond the valid range of the vector. Using vector's [] operator in that // manner is undefined behavior (and triggers errors when using debug libc++), while doing the // same with an array is fine. auto cursor_begin = (uint32_t *) &captured_cursor.pixels.data()[((y + cursor_delta_y) * captured_cursor.src_w + cursor_delta_x) * 4]; auto cursor_end = (uint32_t *) &captured_cursor.pixels.data()[((y + cursor_delta_y) * captured_cursor.src_w + delta_width + cursor_delta_x) * 4]; auto pixels_begin = &pixels[(y + cursor_y) * (img.row_pitch / img.pixel_pitch) + cursor_x]; std::for_each(cursor_begin, cursor_end, [&](uint32_t cursor_pixel) { auto colors_in = (uint8_t *) pixels_begin; auto alpha = (*(uint *) &cursor_pixel) >> 24u; if (alpha == 255) { *pixels_begin = cursor_pixel; } else { auto colors_out = (uint8_t *) &cursor_pixel; colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255; colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255; colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255 / 2) / 255; } ++pixels_begin; }); } } capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { file_t fb_fd[4]; egl::surface_descriptor_t sd; std::optional frame_timestamp; auto status = refresh(fb_fd, &sd, frame_timestamp); if (status != capture_e::ok) { return status; } auto rgb_opt = egl::import_source(display.get(), sd); if (!rgb_opt) { return capture_e::error; } auto &rgb = *rgb_opt; gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]); // Don't remove these lines, see https://github.com/LizardByte/Sunshine/issues/453 int w, h; gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h); BOOST_LOG(debug) << "width and height: w "sv << w << " h "sv << h; if (!pull_free_image_cb(img_out)) { return platf::capture_e::interrupted; } 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); img_out->frame_timestamp = frame_timestamp; if (cursor && captured_cursor.visible) { blend_cursor(*img_out); } return capture_e::ok; } std::shared_ptr alloc_img() override { auto img = std::make_shared(); img->width = width; img->height = height; img->pixel_pitch = 4; img->row_pitch = img->pixel_pitch * width; img->data = new std::uint8_t[height * img->row_pitch]; return img; } int dummy_img(platf::img_t *img) override { return 0; } gbm::gbm_t gbm; egl::display_t display; egl::ctx_t ctx; }; class display_vram_t: public display_t { public: display_vram_t(mem_type_e mem_type): display_t(mem_type) { } std::unique_ptr make_avcodec_encode_device(pix_fmt_e pix_fmt) override { #ifdef SUNSHINE_BUILD_VAAPI if (mem_type == mem_type_e::vaapi) { return va::make_avcodec_encode_device(width, height, dup(card.render_fd.el), img_offset_x, img_offset_y, true); } #endif #ifdef SUNSHINE_BUILD_CUDA if (mem_type == mem_type_e::cuda) { return cuda::make_avcodec_gl_encode_device(width, height, img_offset_x, img_offset_y); } #endif BOOST_LOG(error) << "Unsupported pixel format for egl::display_vram_t: "sv << platf::from_pix_fmt(pix_fmt); return nullptr; } std::shared_ptr alloc_img() override { auto img = std::make_shared(); img->width = width; img->height = height; img->serial = std::numeric_limitsserial)>::max(); img->data = nullptr; img->pixel_pitch = 4; img->sequence = 0; std::fill_n(img->sd.fds, 4, -1); return img; } int dummy_img(platf::img_t *img) override { // Empty images are recognized as dummies by the zero sequence number return 0; } capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) { auto next_frame = std::chrono::steady_clock::now(); sleep_overshoot_logger.reset(); while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { std::this_thread::sleep_for(next_frame - now); sleep_overshoot_logger.first_point(next_frame); sleep_overshoot_logger.second_point_now_and_log(); } next_frame += delay; if (next_frame < now) { // some major slowdown happened; we couldn't keep up next_frame = now + delay; } std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: case platf::capture_e::interrupted: return status; case platf::capture_e::timeout: if (!push_captured_image_cb(std::move(img_out), false)) { return platf::capture_e::ok; } break; case platf::capture_e::ok: if (!push_captured_image_cb(std::move(img_out), true)) { return platf::capture_e::ok; } break; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; return status; } } return capture_e::ok; } capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds /* timeout */, bool cursor) { file_t fb_fd[4]; if (!pull_free_image_cb(img_out)) { return platf::capture_e::interrupted; } auto img = (egl::img_descriptor_t *) img_out.get(); img->reset(); auto status = refresh(fb_fd, &img->sd, img->frame_timestamp); if (status != capture_e::ok) { return status; } img->sequence = ++sequence; if (cursor && captured_cursor.visible) { // Copy new cursor pixel data if it's been updated if (img->serial != captured_cursor.serial) { img->buffer = captured_cursor.pixels; img->serial = captured_cursor.serial; } img->x = captured_cursor.x; img->y = captured_cursor.y; img->src_w = captured_cursor.src_w; img->src_h = captured_cursor.src_h; img->width = captured_cursor.dst_w; img->height = captured_cursor.dst_h; img->pixel_pitch = 4; img->row_pitch = img->pixel_pitch * img->width; img->data = img->buffer.data(); } else { img->data = nullptr; } for (auto x = 0; x < 4; ++x) { fb_fd[x].release(); } return capture_e::ok; } int init(const std::string &display_name, const ::video::config_t &config) { if (display_t::init(display_name, config)) { return -1; } #ifdef SUNSHINE_BUILD_VAAPI if (mem_type == mem_type_e::vaapi && !va::validate(card.render_fd.el)) { BOOST_LOG(warning) << "Monitor "sv << display_name << " doesn't support hardware encoding. Reverting back to GPU -> RAM -> GPU"sv; return -1; } #endif #ifndef SUNSHINE_BUILD_CUDA if (mem_type == mem_type_e::cuda) { BOOST_LOG(warning) << "Attempting to use NVENC without CUDA support. Reverting back to GPU -> RAM -> GPU"sv; return -1; } #endif return 0; } std::uint64_t sequence {}; }; } // namespace kms std::shared_ptr kms_display(mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { if (hwdevice_type == mem_type_e::vaapi || hwdevice_type == mem_type_e::cuda) { auto disp = std::make_shared(hwdevice_type); if (!disp->init(display_name, config)) { return disp; } // In the case of failure, attempt the old method for VAAPI } auto disp = std::make_shared(hwdevice_type); if (disp->init(display_name, config)) { return nullptr; } return disp; } /** * On Wayland, it's not possible to determine the position of the monitor on the desktop with KMS. * Wayland does allow applications to query attached monitors on the desktop, * however, the naming scheme is not standardized across implementations. * * As a result, correlating the KMS output to the wayland outputs is guess work at best. * But, it's necessary for absolute mouse coordinates to work. * * This is an ugly hack :( */ void correlate_to_wayland(std::vector &cds) { auto monitors = wl::monitors(); BOOST_LOG(info) << "-------- Start of KMS monitor list --------"sv; for (auto &monitor : monitors) { std::string_view name = monitor->name; // Try to convert names in the format: // {type}-{index} // {index} is n'th occurrence of {type} auto index_begin = name.find_last_of('-'); std::uint32_t index; if (index_begin == std::string_view::npos) { index = 1; } else { index = std::max(1, util::from_view(name.substr(index_begin + 1))); } auto type = kms::from_view(name.substr(0, index_begin)); for (auto &card_descriptor : cds) { for (auto &[_, monitor_descriptor] : card_descriptor.crtc_to_monitor) { if (monitor_descriptor.index == index && monitor_descriptor.type == type) { monitor_descriptor.viewport.offset_x = monitor->viewport.offset_x; monitor_descriptor.viewport.offset_y = monitor->viewport.offset_y; // A sanity check, it's guesswork after all. if ( monitor_descriptor.viewport.width != monitor->viewport.width || monitor_descriptor.viewport.height != monitor->viewport.height ) { BOOST_LOG(warning) << "Mismatch on expected Resolution compared to actual resolution: "sv << monitor_descriptor.viewport.width << 'x' << monitor_descriptor.viewport.height << " vs "sv << monitor->viewport.width << 'x' << monitor->viewport.height; } BOOST_LOG(info) << "Monitor " << monitor_descriptor.monitor_index << " is "sv << name << ": "sv << monitor->description; goto break_for_loop; } } } break_for_loop: BOOST_LOG(verbose) << "Reduced to name: "sv << name << ": "sv << index; } BOOST_LOG(info) << "--------- End of KMS monitor list ---------"sv; } // A list of names of displays accepted as display_name std::vector kms_display_names(mem_type_e hwdevice_type) { int count = 0; if (!fs::exists("/dev/dri")) { BOOST_LOG(warning) << "Couldn't find /dev/dri, kmsgrab won't be enabled"sv; return {}; } if (!gbm::create_device) { BOOST_LOG(warning) << "libgbm not initialized"sv; return {}; } kms::conn_type_count_t conn_type_count; std::vector cds; std::vector display_names; fs::path card_dir {"/dev/dri"sv}; for (auto &entry : fs::directory_iterator {card_dir}) { auto file = entry.path().filename(); auto filestring = file.generic_string(); if (std::string_view {filestring}.substr(0, 4) != "card"sv) { continue; } kms::card_t card; if (card.init(entry.path().c_str())) { continue; } // Skip non-Nvidia cards if we're looking for CUDA devices // unless NVENC is selected manually by the user if (hwdevice_type == mem_type_e::cuda && !card.is_nvidia()) { BOOST_LOG(debug) << file << " is not a CUDA device"sv; if (config::video.encoder == "nvenc") { BOOST_LOG(warning) << "Using NVENC with your display connected to a different GPU may not work properly!"sv; } else { continue; } } auto crtc_to_monitor = kms::map_crtc_to_monitor(card.monitors(conn_type_count)); auto end = std::end(card); for (auto plane = std::begin(card); plane != end; ++plane) { // Skip unused planes if (!plane->fb_id) { continue; } if (card.is_cursor(plane->plane_id)) { continue; } auto fb = card.fb(plane.get()); if (!fb) { BOOST_LOG(error) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno); continue; } if (!fb->handles[0]) { BOOST_LOG(error) << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Probably not permitted"sv; BOOST_LOG((window_system != window_system_e::X11 || config::video.capture == "kms") ? fatal : error) << "You must run [sudo setcap cap_sys_admin+p $(readlink -f $(which sunshine))] for KMS display capture to work!\n"sv << "If you installed from AppImage or Flatpak, please refer to the official documentation:\n"sv << "https://docs.lizardbyte.dev/projects/sunshine/latest/md_docs_2getting__started.html#linux"sv; break; } // This appears to return the offset of the monitor auto crtc = card.crtc(plane->crtc_id); if (!crtc) { BOOST_LOG(error) << "Couldn't get CRTC info: "sv << strerror(errno); continue; } auto it = crtc_to_monitor.find(plane->crtc_id); if (it != std::end(crtc_to_monitor)) { it->second.viewport = platf::touch_port_t { (int) crtc->x, (int) crtc->y, (int) crtc->width, (int) crtc->height, }; it->second.monitor_index = count; } kms::env_width = std::max(kms::env_width, (int) (crtc->x + crtc->width)); kms::env_height = std::max(kms::env_height, (int) (crtc->y + crtc->height)); kms::print(plane.get(), fb.get(), crtc.get()); display_names.emplace_back(std::to_string(count++)); } cds.emplace_back(kms::card_descriptor_t { std::move(file), std::move(crtc_to_monitor), }); } if (!wl::init()) { correlate_to_wayland(cds); } // Deduce the full virtual desktop size kms::env_width = 0; kms::env_height = 0; for (auto &card_descriptor : cds) { for (auto &[_, monitor_descriptor] : card_descriptor.crtc_to_monitor) { BOOST_LOG(debug) << "Monitor description"sv; BOOST_LOG(debug) << "Resolution: "sv << monitor_descriptor.viewport.width << 'x' << monitor_descriptor.viewport.height; BOOST_LOG(debug) << "Offset: "sv << monitor_descriptor.viewport.offset_x << 'x' << monitor_descriptor.viewport.offset_y; kms::env_width = std::max(kms::env_width, (int) (monitor_descriptor.viewport.offset_x + monitor_descriptor.viewport.width)); kms::env_height = std::max(kms::env_height, (int) (monitor_descriptor.viewport.offset_y + monitor_descriptor.viewport.height)); } } BOOST_LOG(debug) << "Desktop resolution: "sv << kms::env_width << 'x' << kms::env_height; kms::card_descriptors = std::move(cds); return display_names; } } // namespace platf ================================================ FILE: src/platform/linux/misc.cpp ================================================ /** * @file src/platform/linux/misc.cpp * @brief Miscellaneous definitions for Linux. */ // Required for in6_pktinfo with glibc headers #ifndef _GNU_SOURCE #define _GNU_SOURCE 1 #endif // standard includes #include #include // platform includes #include #include #include #include #include // lib includes #include #include #include #include #include #include #include #include #include #include #include #include // local includes #include "graphics.h" #include "misc.h" #include "src/config.h" #include "src/entry_handler.h" #include "src/logging.h" #include "src/platform/common.h" #include "vaapi.h" #include #ifdef __GNUC__ #define SUNSHINE_GNUC_EXTENSION __extension__ #else #define SUNSHINE_GNUC_EXTENSION #endif using namespace std::literals; namespace fs = std::filesystem; namespace bp = boost::process::v1; window_system_e window_system; namespace dyn { void *handle(const std::vector &libs) { void *handle; for (auto lib : libs) { handle = dlopen(lib, RTLD_LAZY | RTLD_LOCAL); if (handle) { return handle; } } std::stringstream ss; ss << "Couldn't find any of the following libraries: ["sv << libs.front(); std::for_each(std::begin(libs) + 1, std::end(libs), [&](auto lib) { ss << ", "sv << lib; }); ss << ']'; BOOST_LOG(error) << ss.str(); return nullptr; } int load(void *handle, const std::vector> &funcs, bool strict) { int err = 0; for (auto &func : funcs) { TUPLE_2D_REF(fn, name, func); *fn = SUNSHINE_GNUC_EXTENSION(apiproc) dlsym(handle, name); if (!*fn && strict) { BOOST_LOG(error) << "Couldn't find function: "sv << name; err = -1; } } return err; } } // namespace dyn namespace platf { using ifaddr_t = util::safe_ptr; ifaddr_t get_ifaddrs() { ifaddrs *p {nullptr}; getifaddrs(&p); return ifaddr_t {p}; } /** * @brief Performs migration if necessary, then returns the appdata directory. * @details This is used for the log directory, so it cannot invoke Boost logging! * @return The path of the appdata directory that should be used. */ fs::path appdata() { static std::once_flag migration_flag; static fs::path config_path; // Ensure migration is only attempted once std::call_once(migration_flag, []() { bool found = false; bool migrate_config = true; const char *dir; const char *homedir; const char *migrate_envvar; // Get the home directory if ((homedir = getenv("HOME")) == nullptr || strlen(homedir) == 0) { // If HOME is empty or not set, use the current user's home directory homedir = getpwuid(geteuid())->pw_dir; } // May be set if running under a systemd service with the ConfigurationDirectory= option set. if ((dir = getenv("CONFIGURATION_DIRECTORY")) != nullptr && strlen(dir) > 0) { found = true; config_path = fs::path(dir) / "sunshine"sv; } // Otherwise, follow the XDG base directory specification: // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if (!found && (dir = getenv("XDG_CONFIG_HOME")) != nullptr && strlen(dir) > 0) { found = true; config_path = fs::path(dir) / "sunshine"sv; } // As a last resort, use the home directory if (!found) { migrate_config = false; config_path = fs::path(homedir) / ".config/sunshine"sv; } // migrate from the old config location if necessary migrate_envvar = getenv("SUNSHINE_MIGRATE_CONFIG"); if (migrate_config && found && migrate_envvar && strcmp(migrate_envvar, "1") == 0) { std::error_code ec; fs::path old_config_path = fs::path(homedir) / ".config/sunshine"sv; if (old_config_path != config_path && fs::exists(old_config_path, ec)) { if (!fs::exists(config_path, ec)) { std::cout << "Migrating config from "sv << old_config_path << " to "sv << config_path << std::endl; if (!ec) { // Create the new directory tree if it doesn't already exist fs::create_directories(config_path, ec); } if (!ec) { // Copy the old directory into the new location // NB: We use a copy instead of a move so that cross-volume migrations work fs::copy(old_config_path, config_path, fs::copy_options::recursive | fs::copy_options::copy_symlinks, ec); } if (!ec) { // If the copy was successful, delete the original directory fs::remove_all(old_config_path, ec); if (ec) { std::cerr << "Failed to clean up old config directory: " << ec.message() << std::endl; // This is not fatal. Next time we start, we'll warn the user to delete the old one. ec.clear(); } } if (ec) { std::cerr << "Migration failed: " << ec.message() << std::endl; config_path = old_config_path; } } else { // We cannot use Boost logging because it hasn't been initialized yet! std::cerr << "Config exists in both "sv << old_config_path << " and "sv << config_path << ". Using "sv << config_path << " for config" << std::endl; std::cerr << "It is recommended to remove "sv << old_config_path << std::endl; } } } }); return config_path; } std::string from_sockaddr(const sockaddr *const ip_addr) { char data[INET6_ADDRSTRLEN] = {}; auto family = ip_addr->sa_family; if (family == AF_INET6) { inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); } else if (family == AF_INET) { inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN); } return std::string {data}; } std::pair from_sockaddr_ex(const sockaddr *const ip_addr) { char data[INET6_ADDRSTRLEN] = {}; auto family = ip_addr->sa_family; std::uint16_t port = 0; if (family == AF_INET6) { inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); port = ((sockaddr_in6 *) ip_addr)->sin6_port; } else if (family == AF_INET) { inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN); port = ((sockaddr_in *) ip_addr)->sin_port; } return {port, std::string {data}}; } std::string get_mac_address(const std::string_view &address) { auto ifaddrs = get_ifaddrs(); for (auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) { if (pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) { std::ifstream mac_file("/sys/class/net/"s + pos->ifa_name + "/address"); if (mac_file.good()) { std::string mac_address; std::getline(mac_file, mac_address); return mac_address; } } } BOOST_LOG(warning) << "Unable to find MAC address for "sv << address; return "00:00:00:00:00:00"s; } std::string get_local_ip_for_gateway() { int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); if (fd < 0) { BOOST_LOG(warning) << "Socket creation failed: " << strerror(errno); return ""; } char buffer[8192]; struct nlmsghdr *nlMsg = (struct nlmsghdr *)buffer; struct rtmsg *rtMsg = (struct rtmsg *)NLMSG_DATA(nlMsg); struct rtattr *rtAttr; int len = 0; memset(nlMsg, 0, sizeof(struct nlmsghdr)); nlMsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); nlMsg->nlmsg_type = RTM_GETROUTE; nlMsg->nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST; nlMsg->nlmsg_seq = 1; nlMsg->nlmsg_pid = getpid(); if (send(fd, nlMsg, nlMsg->nlmsg_len, 0) < 0) { BOOST_LOG(warning) << "Send message failed: " << strerror(errno); close(fd); return ""; } std::string local_ip; bool found = false; while ((len = recv(fd, nlMsg, sizeof(buffer), 0)) > 0) { for (; NLMSG_OK(nlMsg, len); nlMsg = NLMSG_NEXT(nlMsg, len)) { if (nlMsg->nlmsg_type == NLMSG_DONE) { found = true; break; } rtMsg = (struct rtmsg *)NLMSG_DATA(nlMsg); if (rtMsg->rtm_family != AF_INET || rtMsg->rtm_table != RT_TABLE_MAIN) continue; rtAttr = (struct rtattr *)RTM_RTA(rtMsg); int rtLen = RTM_PAYLOAD(nlMsg); in_addr gateway; in_addr local; memset(&gateway, 0, sizeof(gateway)); memset(&local, 0, sizeof(local)); for (; RTA_OK(rtAttr, rtLen); rtAttr = RTA_NEXT(rtAttr, rtLen)) { switch(rtAttr->rta_type) { case RTA_GATEWAY: gateway.s_addr = *reinterpret_cast(RTA_DATA(rtAttr)); break; case RTA_PREFSRC: local.s_addr = *reinterpret_cast(RTA_DATA(rtAttr)); break; default: break; } } if (gateway.s_addr != 0 && local.s_addr != 0) { local_ip = inet_ntoa(local); found = true; break; } } if (found) break; } close(fd); if (local_ip.empty()) { BOOST_LOG(warning) << "No associated IP address found for the default gateway"; } return local_ip; } bp::child 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) { // clang-format off if (!group) { if (!file) { 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); } else { 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); } } else { if (!file) { 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); } else { 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); } } // clang-format on } /** * @brief Open a url in the default web browser. * @param url The url to open. */ void open_url(const std::string &url) { // set working dir to user home directory auto working_dir = boost::filesystem::path(std::getenv("HOME")); std::string cmd = R"(xdg-open ")" + url + R"(")"; boost::process::v1::environment _env = boost::this_process::environment(); std::error_code ec; auto child = run_command(false, false, cmd, working_dir, _env, nullptr, ec, nullptr); if (ec) { BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); } else { BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; child.detach(); } } void adjust_thread_priority(thread_priority_e priority) { // Unimplemented } void streaming_will_start() { // Nothing to do } void streaming_will_stop() { // Nothing to do } void restart_on_exit() { char executable[PATH_MAX]; ssize_t len = readlink("/proc/self/exe", executable, PATH_MAX - 1); if (len == -1) { BOOST_LOG(fatal) << "readlink() failed: "sv << errno; return; } executable[len] = '\0'; // ASIO doesn't use O_CLOEXEC, so we have to close all fds ourselves int openmax = (int) sysconf(_SC_OPEN_MAX); for (int fd = STDERR_FILENO + 1; fd < openmax; fd++) { close(fd); } // Re-exec ourselves with the same arguments if (execv(executable, lifetime::get_argv()) < 0) { BOOST_LOG(fatal) << "execv() failed: "sv << errno; return; } } void restart() { // Gracefully clean up and restart ourselves instead of exiting atexit(restart_on_exit); lifetime::exit_sunshine(0, true); } int set_env(const std::string &name, const std::string &value) { return setenv(name.c_str(), value.c_str(), 1); } int unset_env(const std::string &name) { return unsetenv(name.c_str()); } bool request_process_group_exit(std::uintptr_t native_handle) { if (kill(-((pid_t) native_handle), SIGTERM) == 0 || errno == ESRCH) { BOOST_LOG(debug) << "Successfully sent SIGTERM to process group: "sv << native_handle; return true; } else { BOOST_LOG(warning) << "Unable to send SIGTERM to process group ["sv << native_handle << "]: "sv << errno; return false; } } bool process_group_running(std::uintptr_t native_handle) { return waitpid(-((pid_t) native_handle), nullptr, WNOHANG) >= 0; } struct sockaddr_in to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) { struct sockaddr_in saddr_v4 = {}; saddr_v4.sin_family = AF_INET; saddr_v4.sin_port = htons(port); auto addr_bytes = address.to_bytes(); memcpy(&saddr_v4.sin_addr, addr_bytes.data(), sizeof(saddr_v4.sin_addr)); return saddr_v4; } struct sockaddr_in6 to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) { struct sockaddr_in6 saddr_v6 = {}; saddr_v6.sin6_family = AF_INET6; saddr_v6.sin6_port = htons(port); saddr_v6.sin6_scope_id = address.scope_id(); auto addr_bytes = address.to_bytes(); memcpy(&saddr_v6.sin6_addr, addr_bytes.data(), sizeof(saddr_v6.sin6_addr)); return saddr_v6; } bool send_batch(batched_send_info_t &send_info) { auto sockfd = (int) send_info.native_socket; struct msghdr msg = {}; // Convert the target address into a sockaddr struct sockaddr_in taddr_v4 = {}; struct sockaddr_in6 taddr_v6 = {}; if (send_info.target_address.is_v6()) { taddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port); msg.msg_name = (struct sockaddr *) &taddr_v6; msg.msg_namelen = sizeof(taddr_v6); } else { taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); msg.msg_name = (struct sockaddr *) &taddr_v4; msg.msg_namelen = sizeof(taddr_v4); } union { char buf[CMSG_SPACE(sizeof(uint16_t)) + std::max(CMSG_SPACE(sizeof(struct in_pktinfo)), CMSG_SPACE(sizeof(struct in6_pktinfo)))]; struct cmsghdr alignment; } cmbuf = {}; // Must be zeroed for CMSG_NXTHDR() socklen_t cmbuflen = 0; msg.msg_control = cmbuf.buf; msg.msg_controllen = sizeof(cmbuf.buf); // The PKTINFO option will always be first, then we will conditionally // append the UDP_SEGMENT option next if applicable. auto pktinfo_cm = CMSG_FIRSTHDR(&msg); if (send_info.source_address.is_v6()) { struct in6_pktinfo pktInfo; struct sockaddr_in6 saddr_v6 = to_sockaddr(send_info.source_address.to_v6(), 0); pktInfo.ipi6_addr = saddr_v6.sin6_addr; pktInfo.ipi6_ifindex = 0; cmbuflen += CMSG_SPACE(sizeof(pktInfo)); pktinfo_cm->cmsg_level = IPPROTO_IPV6; pktinfo_cm->cmsg_type = IPV6_PKTINFO; pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); } else { struct in_pktinfo pktInfo; struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); pktInfo.ipi_spec_dst = saddr_v4.sin_addr; pktInfo.ipi_ifindex = 0; cmbuflen += CMSG_SPACE(sizeof(pktInfo)); pktinfo_cm->cmsg_level = IPPROTO_IP; pktinfo_cm->cmsg_type = IP_PKTINFO; pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); } auto const max_iovs_per_msg = send_info.payload_buffers.size() + (send_info.headers ? 1 : 0); #ifdef UDP_SEGMENT { // UDP GSO on Linux currently only supports sending 64K or 64 segments at a time size_t seg_index = 0; const size_t seg_max = 65536 / 1500; struct iovec iovs[(send_info.headers ? std::min(seg_max, send_info.block_count) : 1) * max_iovs_per_msg]; auto msg_size = send_info.header_size + send_info.payload_size; while (seg_index < send_info.block_count) { int iovlen = 0; auto segs_in_batch = std::min(send_info.block_count - seg_index, seg_max); if (send_info.headers) { // Interleave iovs for headers and payloads for (auto i = 0; i < segs_in_batch; i++) { iovs[iovlen].iov_base = (void *) &send_info.headers[(send_info.block_offset + seg_index + i) * send_info.header_size]; iovs[iovlen].iov_len = send_info.header_size; iovlen++; auto payload_desc = send_info.buffer_for_payload_offset((send_info.block_offset + seg_index + i) * send_info.payload_size); iovs[iovlen].iov_base = (void *) payload_desc.buffer; iovs[iovlen].iov_len = send_info.payload_size; iovlen++; } } else { // Translate buffer descriptors into iovs auto payload_offset = (send_info.block_offset + seg_index) * send_info.payload_size; auto payload_length = payload_offset + (segs_in_batch * send_info.payload_size); while (payload_offset < payload_length) { auto payload_desc = send_info.buffer_for_payload_offset(payload_offset); iovs[iovlen].iov_base = (void *) payload_desc.buffer; iovs[iovlen].iov_len = std::min(payload_desc.size, payload_length - payload_offset); payload_offset += iovs[iovlen].iov_len; iovlen++; } } msg.msg_iov = iovs; msg.msg_iovlen = iovlen; // We should not use GSO if the data is <= one full block size if (segs_in_batch > 1) { msg.msg_controllen = cmbuflen + CMSG_SPACE(sizeof(uint16_t)); // Enable GSO to perform segmentation of our buffer for us auto cm = CMSG_NXTHDR(&msg, pktinfo_cm); cm->cmsg_level = SOL_UDP; cm->cmsg_type = UDP_SEGMENT; cm->cmsg_len = CMSG_LEN(sizeof(uint16_t)); *((uint16_t *) CMSG_DATA(cm)) = msg_size; } else { msg.msg_controllen = cmbuflen; } // This will fail if GSO is not available, so we will fall back to non-GSO if // it's the first sendmsg() call. On subsequent calls, we will treat errors as // actual failures and return to the caller. auto bytes_sent = sendmsg(sockfd, &msg, 0); if (bytes_sent < 0) { // If there's no send buffer space, wait for some to be available if (errno == EAGAIN) { struct pollfd pfd; pfd.fd = sockfd; pfd.events = POLLOUT; if (poll(&pfd, 1, -1) != 1) { BOOST_LOG(warning) << "poll() failed: "sv << errno; break; } // Try to send again continue; } BOOST_LOG(verbose) << "sendmsg() failed: "sv << errno; break; } seg_index += bytes_sent / msg_size; } // If we sent something, return the status and don't fall back to the non-GSO path. if (seg_index != 0) { return seg_index >= send_info.block_count; } } #endif { // If GSO is not supported, use sendmmsg() instead. struct mmsghdr msgs[send_info.block_count]; struct iovec iovs[send_info.block_count * (send_info.headers ? 2 : 1)]; int iov_idx = 0; for (size_t i = 0; i < send_info.block_count; i++) { msgs[i].msg_len = 0; msgs[i].msg_hdr.msg_iov = &iovs[iov_idx]; msgs[i].msg_hdr.msg_iovlen = send_info.headers ? 2 : 1; if (send_info.headers) { iovs[iov_idx].iov_base = (void *) &send_info.headers[(send_info.block_offset + i) * send_info.header_size]; iovs[iov_idx].iov_len = send_info.header_size; iov_idx++; } auto payload_desc = send_info.buffer_for_payload_offset((send_info.block_offset + i) * send_info.payload_size); iovs[iov_idx].iov_base = (void *) payload_desc.buffer; iovs[iov_idx].iov_len = send_info.payload_size; iov_idx++; msgs[i].msg_hdr.msg_name = msg.msg_name; msgs[i].msg_hdr.msg_namelen = msg.msg_namelen; msgs[i].msg_hdr.msg_control = cmbuf.buf; msgs[i].msg_hdr.msg_controllen = cmbuflen; msgs[i].msg_hdr.msg_flags = 0; } // Call sendmmsg() until all messages are sent size_t blocks_sent = 0; while (blocks_sent < send_info.block_count) { int msgs_sent = sendmmsg(sockfd, &msgs[blocks_sent], send_info.block_count - blocks_sent, 0); if (msgs_sent < 0) { // If there's no send buffer space, wait for some to be available if (errno == EAGAIN) { struct pollfd pfd; pfd.fd = sockfd; pfd.events = POLLOUT; if (poll(&pfd, 1, -1) != 1) { BOOST_LOG(warning) << "poll() failed: "sv << errno; break; } // Try to send again continue; } BOOST_LOG(warning) << "sendmmsg() failed: "sv << errno; return false; } blocks_sent += msgs_sent; } return true; } } bool send(send_info_t &send_info) { auto sockfd = (int) send_info.native_socket; struct msghdr msg = {}; // Convert the target address into a sockaddr struct sockaddr_in taddr_v4 = {}; struct sockaddr_in6 taddr_v6 = {}; if (send_info.target_address.is_v6()) { taddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port); msg.msg_name = (struct sockaddr *) &taddr_v6; msg.msg_namelen = sizeof(taddr_v6); } else { taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); msg.msg_name = (struct sockaddr *) &taddr_v4; msg.msg_namelen = sizeof(taddr_v4); } union { char buf[std::max(CMSG_SPACE(sizeof(struct in_pktinfo)), CMSG_SPACE(sizeof(struct in6_pktinfo)))]; struct cmsghdr alignment; } cmbuf; socklen_t cmbuflen = 0; msg.msg_control = cmbuf.buf; msg.msg_controllen = sizeof(cmbuf.buf); auto pktinfo_cm = CMSG_FIRSTHDR(&msg); if (send_info.source_address.is_v6()) { struct in6_pktinfo pktInfo; struct sockaddr_in6 saddr_v6 = to_sockaddr(send_info.source_address.to_v6(), 0); pktInfo.ipi6_addr = saddr_v6.sin6_addr; pktInfo.ipi6_ifindex = 0; cmbuflen += CMSG_SPACE(sizeof(pktInfo)); pktinfo_cm->cmsg_level = IPPROTO_IPV6; pktinfo_cm->cmsg_type = IPV6_PKTINFO; pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); } else { struct in_pktinfo pktInfo; struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); pktInfo.ipi_spec_dst = saddr_v4.sin_addr; pktInfo.ipi_ifindex = 0; cmbuflen += CMSG_SPACE(sizeof(pktInfo)); pktinfo_cm->cmsg_level = IPPROTO_IP; pktinfo_cm->cmsg_type = IP_PKTINFO; pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); } struct iovec iovs[2]; int iovlen = 0; if (send_info.header) { iovs[iovlen].iov_base = (void *) send_info.header; iovs[iovlen].iov_len = send_info.header_size; iovlen++; } iovs[iovlen].iov_base = (void *) send_info.payload; iovs[iovlen].iov_len = send_info.payload_size; iovlen++; msg.msg_iov = iovs; msg.msg_iovlen = iovlen; msg.msg_controllen = cmbuflen; auto bytes_sent = sendmsg(sockfd, &msg, 0); // If there's no send buffer space, wait for some to be available while (bytes_sent < 0 && errno == EAGAIN) { struct pollfd pfd; pfd.fd = sockfd; pfd.events = POLLOUT; if (poll(&pfd, 1, -1) != 1) { BOOST_LOG(warning) << "poll() failed: "sv << errno; break; } // Try to send again bytes_sent = sendmsg(sockfd, &msg, 0); } if (bytes_sent < 0) { BOOST_LOG(warning) << "sendmsg() failed: "sv << errno; return false; } return true; } // We can't track QoS state separately for each destination on this OS, // so we keep a ref count to only disable QoS options when all clients // are disconnected. static std::atomic qos_ref_count = 0; class qos_t: public deinit_t { public: qos_t(int sockfd, std::vector> options): sockfd(sockfd), options(options) { qos_ref_count++; } virtual ~qos_t() { if (--qos_ref_count == 0) { for (const auto &tuple : options) { auto reset_val = std::get<2>(tuple); if (setsockopt(sockfd, std::get<0>(tuple), std::get<1>(tuple), &reset_val, sizeof(reset_val)) < 0) { BOOST_LOG(warning) << "Failed to reset option: "sv << errno; } } } } private: int sockfd; std::vector> options; }; /** * @brief Enables QoS on the given socket for traffic to the specified destination. * @param native_socket The native socket handle. * @param address The destination address for traffic sent on this socket. * @param port The destination port for traffic sent on this socket. * @param data_type The type of traffic sent on this socket. * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic. */ std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) { int sockfd = (int) native_socket; std::vector> reset_options; if (dscp_tagging) { int level; int option; // With dual-stack sockets, Linux uses IPV6_TCLASS for IPv6 traffic // and IP_TOS for IPv4 traffic. if (address.is_v6() && !address.to_v6().is_v4_mapped()) { level = SOL_IPV6; option = IPV6_TCLASS; } else { level = SOL_IP; option = IP_TOS; } // The specific DSCP values here are chosen to be consistent with Windows, // except that we use CS6 instead of CS7 for audio traffic. int dscp = 0; switch (data_type) { case qos_data_type_e::video: dscp = 40; break; case qos_data_type_e::audio: dscp = 48; break; default: BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type; break; } if (dscp) { // Shift to put the DSCP value in the correct position in the TOS field dscp <<= 2; if (setsockopt(sockfd, level, option, &dscp, sizeof(dscp)) == 0) { // Reset TOS to -1 when QoS is disabled reset_options.emplace_back(std::make_tuple(level, option, -1)); } else { BOOST_LOG(error) << "Failed to set TOS/TCLASS: "sv << errno; } } } // We can use SO_PRIORITY to set outgoing traffic priority without DSCP tagging. // // NB: We set this after IP_TOS/IPV6_TCLASS since setting TOS value seems to // reset SO_PRIORITY back to 0. // // 6 is the highest priority that can be used without SYS_CAP_ADMIN. int priority = data_type == qos_data_type_e::audio ? 6 : 5; if (setsockopt(sockfd, SOL_SOCKET, SO_PRIORITY, &priority, sizeof(priority)) == 0) { // Reset SO_PRIORITY to 0 when QoS is disabled reset_options.emplace_back(std::make_tuple(SOL_SOCKET, SO_PRIORITY, 0)); } else { BOOST_LOG(error) << "Failed to set SO_PRIORITY: "sv << errno; } return std::make_unique(sockfd, reset_options); } std::string get_host_name() { try { return boost::asio::ip::host_name(); } catch (boost::system::system_error &err) { BOOST_LOG(error) << "Failed to get hostname: "sv << err.what(); return "Sunshine"s; } } namespace source { enum source_e : std::size_t { #ifdef SUNSHINE_BUILD_CUDA NVFBC, ///< NvFBC #endif #ifdef SUNSHINE_BUILD_WAYLAND WAYLAND, ///< Wayland #endif #ifdef SUNSHINE_BUILD_DRM KMS, ///< KMS #endif #ifdef SUNSHINE_BUILD_X11 X11, ///< X11 #endif MAX_FLAGS ///< The maximum number of flags }; } // namespace source static std::bitset sources; #ifdef SUNSHINE_BUILD_CUDA std::vector nvfbc_display_names(); std::shared_ptr nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); bool verify_nvfbc() { return !nvfbc_display_names().empty(); } #endif #ifdef SUNSHINE_BUILD_WAYLAND std::vector wl_display_names(); std::shared_ptr wl_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); bool verify_wl() { return window_system == window_system_e::WAYLAND && !wl_display_names().empty(); } #endif #ifdef SUNSHINE_BUILD_DRM std::vector kms_display_names(mem_type_e hwdevice_type); std::shared_ptr kms_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); bool verify_kms() { return !kms_display_names(mem_type_e::unknown).empty(); } #endif #ifdef SUNSHINE_BUILD_X11 std::vector x11_display_names(); std::shared_ptr x11_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); bool verify_x11() { return window_system == window_system_e::X11 && !x11_display_names().empty(); } #endif std::vector display_names(mem_type_e hwdevice_type) { #ifdef SUNSHINE_BUILD_CUDA // display using NvFBC only supports mem_type_e::cuda if (sources[source::NVFBC] && hwdevice_type == mem_type_e::cuda) { return nvfbc_display_names(); } #endif #ifdef SUNSHINE_BUILD_WAYLAND if (sources[source::WAYLAND]) { return wl_display_names(); } #endif #ifdef SUNSHINE_BUILD_DRM if (sources[source::KMS]) { return kms_display_names(hwdevice_type); } #endif #ifdef SUNSHINE_BUILD_X11 if (sources[source::X11]) { return x11_display_names(); } #endif return {}; } /** * @brief Returns if GPUs/drivers have changed since the last call to this function. * @return `true` if a change has occurred or if it is unknown whether a change occurred. */ bool needs_encoder_reenumeration() { // We don't track GPU state, so we will always reenumerate. Fortunately, it is fast on Linux. return true; } std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { #ifdef SUNSHINE_BUILD_CUDA if (sources[source::NVFBC] && hwdevice_type == mem_type_e::cuda) { BOOST_LOG(info) << "Screencasting with NvFBC"sv; return nvfbc_display(hwdevice_type, display_name, config); } #endif #ifdef SUNSHINE_BUILD_WAYLAND if (sources[source::WAYLAND]) { BOOST_LOG(info) << "Screencasting with Wayland's protocol"sv; return wl_display(hwdevice_type, display_name, config); } #endif #ifdef SUNSHINE_BUILD_DRM if (sources[source::KMS]) { BOOST_LOG(info) << "Screencasting with KMS"sv; return kms_display(hwdevice_type, display_name, config); } #endif #ifdef SUNSHINE_BUILD_X11 if (sources[source::X11]) { BOOST_LOG(info) << "Screencasting with X11"sv; return x11_display(hwdevice_type, display_name, config); } #endif return nullptr; } std::unique_ptr init() { // enable low latency mode for AMD // https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/30039 set_env("AMD_DEBUG", "lowlatencyenc"); // These are allowed to fail. gbm::init(); window_system = window_system_e::NONE; #ifdef SUNSHINE_BUILD_WAYLAND if (std::getenv("WAYLAND_DISPLAY")) { window_system = window_system_e::WAYLAND; } #endif #if defined(SUNSHINE_BUILD_X11) || defined(SUNSHINE_BUILD_CUDA) if (std::getenv("DISPLAY") && window_system != window_system_e::WAYLAND) { if (std::getenv("WAYLAND_DISPLAY")) { BOOST_LOG(warning) << "Wayland detected, yet sunshine will use X11 for screencasting, screencasting will only work on XWayland applications"sv; } window_system = window_system_e::X11; } #endif #ifdef SUNSHINE_BUILD_CUDA if ((config::video.capture.empty() && sources.none()) || config::video.capture == "nvfbc") { if (verify_nvfbc()) { sources[source::NVFBC] = true; } } #endif #ifdef SUNSHINE_BUILD_WAYLAND if ((config::video.capture.empty() && sources.none()) || config::video.capture == "wlr") { if (verify_wl()) { sources[source::WAYLAND] = true; } } #endif #ifdef SUNSHINE_BUILD_DRM if ((config::video.capture.empty() && sources.none()) || config::video.capture == "kms") { if (verify_kms()) { sources[source::KMS] = true; } } #endif #ifdef SUNSHINE_BUILD_X11 // We enumerate this capture backend regardless of other suitable sources, // since it may be needed as a NvFBC fallback for software encoding on X11. if (config::video.capture.empty() || config::video.capture == "x11") { if (verify_x11()) { sources[source::X11] = true; } } #endif if (sources.none()) { BOOST_LOG(error) << "Unable to initialize capture method"sv; return nullptr; } if (!gladLoaderLoadEGL(EGL_NO_DISPLAY) || !eglGetPlatformDisplay) { BOOST_LOG(warning) << "Couldn't load EGL library"sv; } return std::make_unique(); } class linux_high_precision_timer: public high_precision_timer { public: void sleep_for(const std::chrono::nanoseconds &duration) override { std::this_thread::sleep_for(duration); } operator bool() override { return true; } }; std::unique_ptr create_high_precision_timer() { return std::make_unique(); } std::string get_clipboard() { // Placeholder return ""; } bool set_clipboard(const std::string& content) { // Placeholder return false; } } // namespace platf ================================================ FILE: src/platform/linux/misc.h ================================================ /** * @file src/platform/linux/misc.h * @brief Miscellaneous declarations for Linux. */ #pragma once // standard includes #include #include // local includes #include "src/utility.h" KITTY_USING_MOVE_T(file_t, int, -1, { if (el >= 0) { close(el); } }); enum class window_system_e { NONE, ///< No window system X11, ///< X11 WAYLAND, ///< Wayland }; extern window_system_e window_system; namespace dyn { typedef void (*apiproc)(void); int load(void *handle, const std::vector> &funcs, bool strict = true); void *handle(const std::vector &libs); } // namespace dyn ================================================ FILE: src/platform/linux/publish.cpp ================================================ /** * @file src/platform/linux/publish.cpp * @brief Definitions for publishing services on Linux. * @note Adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html */ // standard includes #include // local includes #include "misc.h" #include "src/logging.h" #include "src/network.h" #include "src/nvhttp.h" #include "src/platform/common.h" #include "src/utility.h" using namespace std::literals; namespace avahi { /** * @brief Error codes used by avahi. */ enum err_e { OK = 0, ///< OK ERR_FAILURE = -1, ///< Generic error code ERR_BAD_STATE = -2, ///< Object was in a bad state ERR_INVALID_HOST_NAME = -3, ///< Invalid host name ERR_INVALID_DOMAIN_NAME = -4, ///< Invalid domain name ERR_NO_NETWORK = -5, ///< No suitable network protocol available ERR_INVALID_TTL = -6, ///< Invalid DNS TTL ERR_IS_PATTERN = -7, ///< RR key is pattern ERR_COLLISION = -8, ///< Name collision ERR_INVALID_RECORD = -9, ///< Invalid RR ERR_INVALID_SERVICE_NAME = -10, ///< Invalid service name ERR_INVALID_SERVICE_TYPE = -11, ///< Invalid service type ERR_INVALID_PORT = -12, ///< Invalid port number ERR_INVALID_KEY = -13, ///< Invalid key ERR_INVALID_ADDRESS = -14, ///< Invalid address ERR_TIMEOUT = -15, ///< Timeout reached ERR_TOO_MANY_CLIENTS = -16, ///< Too many clients ERR_TOO_MANY_OBJECTS = -17, ///< Too many objects ERR_TOO_MANY_ENTRIES = -18, ///< Too many entries ERR_OS = -19, ///< OS error ERR_ACCESS_DENIED = -20, ///< Access denied ERR_INVALID_OPERATION = -21, ///< Invalid operation ERR_DBUS_ERROR = -22, ///< An unexpected D-Bus error occurred ERR_DISCONNECTED = -23, ///< Daemon connection failed ERR_NO_MEMORY = -24, ///< Memory exhausted ERR_INVALID_OBJECT = -25, ///< The object passed to this function was invalid ERR_NO_DAEMON = -26, ///< Daemon not running ERR_INVALID_INTERFACE = -27, ///< Invalid interface ERR_INVALID_PROTOCOL = -28, ///< Invalid protocol ERR_INVALID_FLAGS = -29, ///< Invalid flags ERR_NOT_FOUND = -30, ///< Not found ERR_INVALID_CONFIG = -31, ///< Configuration error ERR_VERSION_MISMATCH = -32, ///< Version mismatch ERR_INVALID_SERVICE_SUBTYPE = -33, ///< Invalid service subtype ERR_INVALID_PACKET = -34, ///< Invalid packet ERR_INVALID_DNS_ERROR = -35, ///< Invalid DNS return code ERR_DNS_FORMERR = -36, ///< DNS Error: Form error ERR_DNS_SERVFAIL = -37, ///< DNS Error: Server Failure ERR_DNS_NXDOMAIN = -38, ///< DNS Error: No such domain ERR_DNS_NOTIMP = -39, ///< DNS Error: Not implemented ERR_DNS_REFUSED = -40, ///< DNS Error: Operation refused ERR_DNS_YXDOMAIN = -41, ///< TODO ERR_DNS_YXRRSET = -42, ///< TODO ERR_DNS_NXRRSET = -43, ///< TODO ERR_DNS_NOTAUTH = -44, ///< DNS Error: Not authorized ERR_DNS_NOTZONE = -45, ///< TODO ERR_INVALID_RDATA = -46, ///< Invalid RDATA ERR_INVALID_DNS_CLASS = -47, ///< Invalid DNS class ERR_INVALID_DNS_TYPE = -48, ///< Invalid DNS type ERR_NOT_SUPPORTED = -49, ///< Not supported ERR_NOT_PERMITTED = -50, ///< Operation not permitted ERR_INVALID_ARGUMENT = -51, ///< Invalid argument ERR_IS_EMPTY = -52, ///< Is empty ERR_NO_CHANGE = -53, ///< The requested operation is invalid because it is redundant ERR_MAX = -54 ///< TODO }; constexpr auto IF_UNSPEC = -1; enum proto { PROTO_INET = 0, ///< IPv4 PROTO_INET6 = 1, ///< IPv6 PROTO_UNSPEC = -1 ///< Unspecified/all protocol(s) }; enum ServerState { SERVER_INVALID, ///< Invalid state (initial) SERVER_REGISTERING, ///< Host RRs are being registered SERVER_RUNNING, ///< All host RRs have been established 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() SERVER_FAILURE ///< Some fatal failure happened, the server is unable to proceed }; enum ClientState { CLIENT_S_REGISTERING = SERVER_REGISTERING, ///< Server state: REGISTERING CLIENT_S_RUNNING = SERVER_RUNNING, ///< Server state: RUNNING CLIENT_S_COLLISION = SERVER_COLLISION, ///< Server state: COLLISION CLIENT_FAILURE = 100, ///< Some kind of error happened on the client side 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. }; enum EntryGroupState { ENTRY_GROUP_UNCOMMITED, ///< The group has not yet been committed, the user must still call avahi_entry_group_commit() ENTRY_GROUP_REGISTERING, ///< The entries of the group are currently being registered ENTRY_GROUP_ESTABLISHED, ///< The entries have successfully been established ENTRY_GROUP_COLLISION, ///< A name collision for one of the entries in the group has been detected, the entries have been withdrawn ENTRY_GROUP_FAILURE ///< Some kind of failure happened, the entries have been withdrawn }; enum ClientFlags { CLIENT_IGNORE_USER_CONFIG = 1, ///< Don't read user configuration 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 }; /** * @brief Flags for publishing functions. */ enum PublishFlags { PUBLISH_UNIQUE = 1, ///< For raw records: The RRset is intended to be unique PUBLISH_NO_PROBE = 2, ///< For raw records: Though the RRset is intended to be unique no probes shall be sent PUBLISH_NO_ANNOUNCE = 4, ///< For raw records: Do not announce this RR to other hosts PUBLISH_ALLOW_MULTIPLE = 8, ///< For raw records: Allow multiple local records of this type, even if they are intended to be unique PUBLISH_NO_REVERSE = 16, ///< For address records: don't create a reverse (PTR) entry PUBLISH_NO_COOKIE = 32, ///< For service records: do not implicitly add the local service cookie to TXT data PUBLISH_UPDATE = 64, ///< Update existing records instead of adding new ones PUBLISH_USE_WIDE_AREA = 128, ///< Register the record using wide area DNS (i.e. unicast DNS update) PUBLISH_USE_MULTICAST = 256 ///< Register the record using multicast DNS }; using IfIndex = int; using Protocol = int; struct EntryGroup; struct Poll; struct SimplePoll; struct Client; typedef void (*ClientCallback)(Client *, ClientState, void *userdata); typedef void (*EntryGroupCallback)(EntryGroup *g, EntryGroupState state, void *userdata); typedef void (*free_fn)(void *); typedef Client *(*client_new_fn)(const Poll *poll_api, ClientFlags flags, ClientCallback callback, void *userdata, int *error); typedef void (*client_free_fn)(Client *); typedef char *(*alternative_service_name_fn)(char *); typedef Client *(*entry_group_get_client_fn)(EntryGroup *); typedef EntryGroup *(*entry_group_new_fn)(Client *, EntryGroupCallback, void *userdata); typedef int (*entry_group_add_service_fn)( EntryGroup *group, IfIndex interface, Protocol protocol, PublishFlags flags, const char *name, const char *type, const char *domain, const char *host, uint16_t port, ... ); typedef int (*entry_group_is_empty_fn)(EntryGroup *); typedef int (*entry_group_reset_fn)(EntryGroup *); typedef int (*entry_group_commit_fn)(EntryGroup *); typedef char *(*strdup_fn)(const char *); typedef char *(*strerror_fn)(int); typedef int (*client_errno_fn)(Client *); typedef Poll *(*simple_poll_get_fn)(SimplePoll *); typedef int (*simple_poll_loop_fn)(SimplePoll *); typedef void (*simple_poll_quit_fn)(SimplePoll *); typedef SimplePoll *(*simple_poll_new_fn)(); typedef void (*simple_poll_free_fn)(SimplePoll *); free_fn free; client_new_fn client_new; client_free_fn client_free; alternative_service_name_fn alternative_service_name; entry_group_get_client_fn entry_group_get_client; entry_group_new_fn entry_group_new; entry_group_add_service_fn entry_group_add_service; entry_group_is_empty_fn entry_group_is_empty; entry_group_reset_fn entry_group_reset; entry_group_commit_fn entry_group_commit; strdup_fn strdup; strerror_fn strerror; client_errno_fn client_errno; simple_poll_get_fn simple_poll_get; simple_poll_loop_fn simple_poll_loop; simple_poll_quit_fn simple_poll_quit; simple_poll_new_fn simple_poll_new; simple_poll_free_fn simple_poll_free; int init_common() { static void *handle {nullptr}; static bool funcs_loaded = false; if (funcs_loaded) { return 0; } if (!handle) { handle = dyn::handle({"libavahi-common.so.3", "libavahi-common.so"}); if (!handle) { return -1; } } std::vector> funcs { {(dyn::apiproc *) &alternative_service_name, "avahi_alternative_service_name"}, {(dyn::apiproc *) &free, "avahi_free"}, {(dyn::apiproc *) &strdup, "avahi_strdup"}, {(dyn::apiproc *) &strerror, "avahi_strerror"}, {(dyn::apiproc *) &simple_poll_get, "avahi_simple_poll_get"}, {(dyn::apiproc *) &simple_poll_loop, "avahi_simple_poll_loop"}, {(dyn::apiproc *) &simple_poll_quit, "avahi_simple_poll_quit"}, {(dyn::apiproc *) &simple_poll_new, "avahi_simple_poll_new"}, {(dyn::apiproc *) &simple_poll_free, "avahi_simple_poll_free"}, }; if (dyn::load(handle, funcs)) { return -1; } funcs_loaded = true; return 0; } int init_client() { if (init_common()) { return -1; } static void *handle {nullptr}; static bool funcs_loaded = false; if (funcs_loaded) { return 0; } if (!handle) { handle = dyn::handle({"libavahi-client.so.3", "libavahi-client.so"}); if (!handle) { return -1; } } std::vector> funcs { {(dyn::apiproc *) &client_new, "avahi_client_new"}, {(dyn::apiproc *) &client_free, "avahi_client_free"}, {(dyn::apiproc *) &entry_group_get_client, "avahi_entry_group_get_client"}, {(dyn::apiproc *) &entry_group_new, "avahi_entry_group_new"}, {(dyn::apiproc *) &entry_group_add_service, "avahi_entry_group_add_service"}, {(dyn::apiproc *) &entry_group_is_empty, "avahi_entry_group_is_empty"}, {(dyn::apiproc *) &entry_group_reset, "avahi_entry_group_reset"}, {(dyn::apiproc *) &entry_group_commit, "avahi_entry_group_commit"}, {(dyn::apiproc *) &client_errno, "avahi_client_errno"}, }; if (dyn::load(handle, funcs)) { return -1; } funcs_loaded = true; return 0; } } // namespace avahi namespace platf::publish { template void free(T *p) { avahi::free(p); } template using ptr_t = util::safe_ptr>; using client_t = util::dyn_safe_ptr; using poll_t = util::dyn_safe_ptr; avahi::EntryGroup *group = nullptr; poll_t poll; client_t client; ptr_t name; void create_services(avahi::Client *c); void entry_group_callback(avahi::EntryGroup *g, avahi::EntryGroupState state, void *) { group = g; switch (state) { case avahi::ENTRY_GROUP_ESTABLISHED: BOOST_LOG(info) << "Avahi service " << name.get() << " successfully established."; break; case avahi::ENTRY_GROUP_COLLISION: name.reset(avahi::alternative_service_name(name.get())); BOOST_LOG(info) << "Avahi service name collision, renaming service to " << name.get(); create_services(avahi::entry_group_get_client(g)); break; case avahi::ENTRY_GROUP_FAILURE: BOOST_LOG(error) << "Avahi entry group failure: " << avahi::strerror(avahi::client_errno(avahi::entry_group_get_client(g))); avahi::simple_poll_quit(poll.get()); break; case avahi::ENTRY_GROUP_UNCOMMITED: case avahi::ENTRY_GROUP_REGISTERING:; } } void create_services(avahi::Client *c) { int ret; auto fg = util::fail_guard([]() { avahi::simple_poll_quit(poll.get()); }); if (!group) { if (!(group = avahi::entry_group_new(c, entry_group_callback, nullptr))) { BOOST_LOG(error) << "avahi::entry_group_new() failed: "sv << avahi::strerror(avahi::client_errno(c)); return; } } if (avahi::entry_group_is_empty(group)) { BOOST_LOG(info) << "Adding avahi service "sv << name.get(); ret = avahi::entry_group_add_service( group, avahi::IF_UNSPEC, avahi::PROTO_UNSPEC, avahi::PublishFlags(0), name.get(), SERVICE_TYPE, nullptr, nullptr, net::map_port(nvhttp::PORT_HTTP), nullptr ); if (ret < 0) { if (ret == avahi::ERR_COLLISION) { // A service name collision with a local service happened. Let's pick a new name name.reset(avahi::alternative_service_name(name.get())); BOOST_LOG(info) << "Service name collision, renaming service to "sv << name.get(); avahi::entry_group_reset(group); create_services(c); fg.disable(); return; } BOOST_LOG(error) << "Failed to add "sv << SERVICE_TYPE << " service: "sv << avahi::strerror(ret); return; } ret = avahi::entry_group_commit(group); if (ret < 0) { BOOST_LOG(error) << "Failed to commit entry group: "sv << avahi::strerror(ret); return; } } fg.disable(); } void client_callback(avahi::Client *c, avahi::ClientState state, void *) { switch (state) { case avahi::CLIENT_S_RUNNING: create_services(c); break; case avahi::CLIENT_FAILURE: BOOST_LOG(error) << "Client failure: "sv << avahi::strerror(avahi::client_errno(c)); avahi::simple_poll_quit(poll.get()); break; case avahi::CLIENT_S_COLLISION: case avahi::CLIENT_S_REGISTERING: if (group) { avahi::entry_group_reset(group); } break; case avahi::CLIENT_CONNECTING:; } } class deinit_t: public ::platf::deinit_t { public: std::thread poll_thread; deinit_t(std::thread poll_thread): poll_thread {std::move(poll_thread)} { } ~deinit_t() override { if (avahi::simple_poll_quit && poll) { avahi::simple_poll_quit(poll.get()); } if (poll_thread.joinable()) { poll_thread.join(); } } }; [[nodiscard]] std::unique_ptr<::platf::deinit_t> start() { if (avahi::init_client()) { return nullptr; } int avhi_error; poll.reset(avahi::simple_poll_new()); if (!poll) { BOOST_LOG(error) << "Failed to create simple poll object."sv; return nullptr; } auto instance_name = net::mdns_instance_name(platf::get_host_name()); name.reset(avahi::strdup(instance_name.c_str())); client.reset( avahi::client_new(avahi::simple_poll_get(poll.get()), avahi::ClientFlags(0), client_callback, nullptr, &avhi_error) ); if (!client) { BOOST_LOG(error) << "Failed to create client: "sv << avahi::strerror(avhi_error); return nullptr; } return std::make_unique(std::thread {avahi::simple_poll_loop, poll.get()}); } } // namespace platf::publish ================================================ FILE: src/platform/linux/vaapi.cpp ================================================ /** * @file src/platform/linux/vaapi.cpp * @brief Definitions for VA-API hardware accelerated capture. */ // standard includes #include #include #include #include extern "C" { #include #include #include #include #if !VA_CHECK_VERSION(1, 9, 0) // vaSyncBuffer stub allows Sunshine built against libva <2.9.0 to link against ffmpeg on libva 2.9.0 or later VAStatus vaSyncBuffer( VADisplay dpy, VABufferID buf_id, uint64_t timeout_ns ) { return VA_STATUS_ERROR_UNIMPLEMENTED; } #endif } // local includes #include "graphics.h" #include "misc.h" #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" #include "src/video.h" using namespace std::literals; extern "C" struct AVBufferRef; namespace va { constexpr auto SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2 = 0x40000000; constexpr auto EXPORT_SURFACE_WRITE_ONLY = 0x0002; constexpr auto EXPORT_SURFACE_SEPARATE_LAYERS = 0x0004; using VADisplay = void *; using VAStatus = int; using VAGenericID = unsigned int; using VASurfaceID = VAGenericID; struct DRMPRIMESurfaceDescriptor { // VA Pixel format fourcc of the whole surface (VA_FOURCC_*). uint32_t fourcc; uint32_t width; uint32_t height; // Number of distinct DRM objects making up the surface. uint32_t num_objects; struct { // DRM PRIME file descriptor for this object. // Needs to be closed manually int fd; // Total size of this object (may include regions which are not part of the surface) uint32_t size; // Format modifier applied to this object, not sure what that means uint64_t drm_format_modifier; } objects[4]; // Number of layers making up the surface. uint32_t num_layers; struct { // DRM format fourcc of this layer (DRM_FOURCC_*). uint32_t drm_format; // Number of planes in this layer. uint32_t num_planes; // references objects --> DRMPRIMESurfaceDescriptor.objects[object_index[0]] uint32_t object_index[4]; // Offset within the object of each plane. uint32_t offset[4]; // Pitch of each plane. uint32_t pitch[4]; } layers[4]; }; using display_t = util::safe_ptr_v2; int vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device, AVBufferRef **hw_device_buf); class va_t: public platf::avcodec_encode_device_t { public: int init(int in_width, int in_height, file_t &&render_device) { file = std::move(render_device); if (!gbm::create_device) { BOOST_LOG(warning) << "libgbm not initialized"sv; return -1; } this->data = (void *) vaapi_init_avcodec_hardware_input_buffer; gbm.reset(gbm::create_device(file.el)); if (!gbm) { char string[1024]; BOOST_LOG(error) << "Couldn't create GBM device: ["sv << strerror_r(errno, string, sizeof(string)) << ']'; return -1; } display = egl::make_display(gbm.get()); if (!display) { return -1; } auto ctx_opt = egl::make_ctx(display.get()); if (!ctx_opt) { return -1; } ctx = std::move(*ctx_opt); width = in_width; height = in_height; return 0; } /** * @brief Finds a supported VA entrypoint for the given VA profile. * @param profile The profile to match. * @return A valid encoding entrypoint or 0 on failure. */ VAEntrypoint select_va_entrypoint(VAProfile profile) { std::vector entrypoints(vaMaxNumEntrypoints(va_display)); int num_eps; auto status = vaQueryConfigEntrypoints(va_display, profile, entrypoints.data(), &num_eps); if (status != VA_STATUS_SUCCESS) { BOOST_LOG(error) << "Failed to query VA entrypoints: "sv << vaErrorStr(status); return (VAEntrypoint) 0; } entrypoints.resize(num_eps); // Sorted in order of descending preference VAEntrypoint ep_preferences[] = { VAEntrypointEncSliceLP, VAEntrypointEncSlice, VAEntrypointEncPicture }; for (auto ep_pref : ep_preferences) { if (std::find(entrypoints.begin(), entrypoints.end(), ep_pref) != entrypoints.end()) { return ep_pref; } } return (VAEntrypoint) 0; } /** * @brief Determines if a given VA profile is supported. * @param profile The profile to match. * @return Boolean value indicating if the profile is supported. */ bool is_va_profile_supported(VAProfile profile) { std::vector profiles(vaMaxNumProfiles(va_display)); int num_profs; auto status = vaQueryConfigProfiles(va_display, profiles.data(), &num_profs); if (status != VA_STATUS_SUCCESS) { BOOST_LOG(error) << "Failed to query VA profiles: "sv << vaErrorStr(status); return false; } profiles.resize(num_profs); return std::find(profiles.begin(), profiles.end(), profile) != profiles.end(); } /** * @brief Determines the matching VA profile for the codec configuration. * @param ctx The FFmpeg codec context. * @return The matching VA profile or `VAProfileNone` on failure. */ VAProfile get_va_profile(AVCodecContext *ctx) { if (ctx->codec_id == AV_CODEC_ID_H264) { // There's no VAAPI profile for H.264 4:4:4 return VAProfileH264High; } else if (ctx->codec_id == AV_CODEC_ID_HEVC) { switch (ctx->profile) { case AV_PROFILE_HEVC_REXT: switch (av_pix_fmt_desc_get(ctx->sw_pix_fmt)->comp[0].depth) { case 10: return VAProfileHEVCMain444_10; case 8: return VAProfileHEVCMain444; } break; case AV_PROFILE_HEVC_MAIN_10: return VAProfileHEVCMain10; case AV_PROFILE_HEVC_MAIN: return VAProfileHEVCMain; } } else if (ctx->codec_id == AV_CODEC_ID_AV1) { switch (ctx->profile) { case AV_PROFILE_AV1_HIGH: return VAProfileAV1Profile1; case AV_PROFILE_AV1_MAIN: return VAProfileAV1Profile0; } } BOOST_LOG(error) << "Unknown encoder profile: "sv << ctx->profile; return VAProfileNone; } void init_codec_options(AVCodecContext *ctx, AVDictionary **options) override { auto va_profile = get_va_profile(ctx); if (va_profile == VAProfileNone || !is_va_profile_supported(va_profile)) { // Don't bother doing anything if the profile isn't supported return; } auto va_entrypoint = select_va_entrypoint(va_profile); if (va_entrypoint == 0) { // It's possible that only decoding is supported for this profile return; } auto vendor = vaQueryVendorString(va_display); if (va_entrypoint == VAEntrypointEncSliceLP) { BOOST_LOG(info) << "Using LP encoding mode"sv; av_dict_set_int(options, "low_power", 1, 0); } else { BOOST_LOG(info) << "Using normal encoding mode"sv; } VAConfigAttrib rc_attr = {VAConfigAttribRateControl}; auto status = vaGetConfigAttributes(va_display, va_profile, va_entrypoint, &rc_attr, 1); if (status != VA_STATUS_SUCCESS) { // Stick to the default rate control (CQP) rc_attr.value = 0; } VAConfigAttrib slice_attr = {VAConfigAttribEncMaxSlices}; status = vaGetConfigAttributes(va_display, va_profile, va_entrypoint, &slice_attr, 1); if (status != VA_STATUS_SUCCESS) { // Assume only a single slice is supported slice_attr.value = 1; } if (ctx->slices > slice_attr.value) { BOOST_LOG(info) << "Limiting slice count to encoder maximum: "sv << slice_attr.value; ctx->slices = slice_attr.value; } // Use VBR with a single frame VBV when the user forces it and for known good cases: // - Intel GPUs // - AV1 // // VBR ensures the bitstream isn't full of filler data for bitrate undershoots and // single frame VBV ensures that we don't have large bitrate overshoots (at least // as much as they can be avoided without pre-analysis). // // When we have to resort to the default 1 second VBV for encoding quality reasons, // we stick to CBR in order to avoid encoding huge frames after bitrate undershoots // leave headroom available in the RC window. if (config::video.vaapi.strict_rc_buffer || (vendor && strstr(vendor, "Intel")) || ctx->codec_id == AV_CODEC_ID_AV1) { ctx->rc_buffer_size = ctx->bit_rate * ctx->framerate.den / ctx->framerate.num; if (rc_attr.value & VA_RC_VBR) { BOOST_LOG(info) << "Using VBR with single frame VBV size"sv; av_dict_set(options, "rc_mode", "VBR", 0); } else if (rc_attr.value & VA_RC_CBR) { BOOST_LOG(info) << "Using CBR with single frame VBV size"sv; av_dict_set(options, "rc_mode", "CBR", 0); } else { BOOST_LOG(warning) << "Using CQP with single frame VBV size"sv; av_dict_set_int(options, "qp", config::video.qp, 0); } } else if (!(rc_attr.value & (VA_RC_CBR | VA_RC_VBR))) { BOOST_LOG(warning) << "Using CQP rate control"sv; av_dict_set_int(options, "qp", config::video.qp, 0); } else { BOOST_LOG(info) << "Using default rate control"sv; } } int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx_buf) override { this->hwframe.reset(frame); this->frame = frame; if (!frame->buf[0]) { if (av_hwframe_get_buffer(hw_frames_ctx_buf, frame, 0)) { BOOST_LOG(error) << "Couldn't get hwframe for VAAPI"sv; return -1; } } va::DRMPRIMESurfaceDescriptor prime; va::VASurfaceID surface = (std::uintptr_t) frame->data[3]; auto hw_frames_ctx = (AVHWFramesContext *) hw_frames_ctx_buf->data; auto status = vaExportSurfaceHandle( this->va_display, surface, va::SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, va::EXPORT_SURFACE_WRITE_ONLY | va::EXPORT_SURFACE_SEPARATE_LAYERS, &prime ); if (status) { BOOST_LOG(error) << "Couldn't export va surface handle: ["sv << (int) surface << "]: "sv << vaErrorStr(status); return -1; } // Keep track of file descriptors std::array fds; for (int x = 0; x < prime.num_objects; ++x) { fds[x] = prime.objects[x].fd; } if (prime.num_layers != 2) { BOOST_LOG(error) << "Invalid layer count for VA surface: expected 2, got "sv << prime.num_layers; return -1; } egl::surface_descriptor_t sds[2] = {}; for (int plane = 0; plane < 2; ++plane) { auto &sd = sds[plane]; auto &layer = prime.layers[plane]; sd.fourcc = layer.drm_format; // UV plane is subsampled sd.width = prime.width / (plane == 0 ? 1 : 2); sd.height = prime.height / (plane == 0 ? 1 : 2); // The modifier must be the same for all planes sd.modifier = prime.objects[layer.object_index[0]].drm_format_modifier; std::fill_n(sd.fds, 4, -1); for (int x = 0; x < layer.num_planes; ++x) { sd.fds[x] = prime.objects[layer.object_index[x]].fd; sd.pitches[x] = layer.pitch[x]; sd.offsets[x] = layer.offset[x]; } } auto nv12_opt = egl::import_target(display.get(), std::move(fds), sds[0], sds[1]); if (!nv12_opt) { return -1; } auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height, hw_frames_ctx->sw_format); if (!sws_opt) { return -1; } this->sws = std::move(*sws_opt); this->nv12 = std::move(*nv12_opt); return 0; } void apply_colorspace() override { sws.apply_colorspace(colorspace); } va::display_t::pointer va_display; file_t file; gbm::gbm_t gbm; egl::display_t display; egl::ctx_t ctx; // This must be destroyed before display_t to ensure the GPU // driver is still loaded when vaDestroySurfaces() is called. frame_t hwframe; egl::sws_t sws; egl::nv12_t nv12; int width, height; }; class va_ram_t: public va_t { public: int convert(platf::img_t &img) override { sws.load_ram(img); sws.convert(nv12->buf); return 0; } }; class va_vram_t: public va_t { public: int convert(platf::img_t &img) override { auto &descriptor = (egl::img_descriptor_t &) img; if (descriptor.sequence == 0) { // For dummy images, use a blank RGB texture instead of importing a DMA-BUF rgb = egl::create_blank(img); } else if (descriptor.sequence > sequence) { sequence = descriptor.sequence; rgb = egl::rgb_t {}; auto rgb_opt = egl::import_source(display.get(), descriptor.sd); if (!rgb_opt) { return -1; } rgb = std::move(*rgb_opt); } sws.load_vram(descriptor, offset_x, offset_y, rgb->tex[0]); sws.convert(nv12->buf); return 0; } int init(int in_width, int in_height, file_t &&render_device, int offset_x, int offset_y) { if (va_t::init(in_width, in_height, std::move(render_device))) { return -1; } sequence = 0; this->offset_x = offset_x; this->offset_y = offset_y; return 0; } std::uint64_t sequence; egl::rgb_t rgb; int offset_x, offset_y; }; /** * This is a private structure of FFmpeg, I need this to manually create * a VAAPI hardware context * * xdisplay will not be used internally by FFmpeg */ typedef struct VAAPIDevicePriv { union { void *xdisplay; int fd; } drm; int drm_fd; } VAAPIDevicePriv; /** * VAAPI connection details. * * Allocated as AVHWDeviceContext.hwctx */ typedef struct AVVAAPIDeviceContext { /** * The VADisplay handle, to be filled by the user. */ va::VADisplay display; /** * Driver quirks to apply - this is filled by av_hwdevice_ctx_init(), * with reference to a table of known drivers, unless the * AV_VAAPI_DRIVER_QUIRK_USER_SET bit is already present. The user * may need to refer to this field when performing any later * operations using VAAPI with the same VADisplay. */ unsigned int driver_quirks; } AVVAAPIDeviceContext; static void __log(void *level, const char *msg) { BOOST_LOG(*(boost::log::sources::severity_logger *) level) << msg; } static void vaapi_hwdevice_ctx_free(AVHWDeviceContext *ctx) { auto hwctx = (AVVAAPIDeviceContext *) ctx->hwctx; auto priv = (VAAPIDevicePriv *) ctx->user_opaque; vaTerminate(hwctx->display); close(priv->drm_fd); av_freep(&priv); } int vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *base, AVBufferRef **hw_device_buf) { auto va = (va::va_t *) base; auto fd = dup(va->file.el); auto *priv = (VAAPIDevicePriv *) av_mallocz(sizeof(VAAPIDevicePriv)); priv->drm_fd = fd; auto fg = util::fail_guard([fd, priv]() { close(fd); av_free(priv); }); va::display_t display {vaGetDisplayDRM(fd)}; if (!display) { auto render_device = config::video.adapter_name.empty() ? "/dev/dri/renderD128" : config::video.adapter_name.c_str(); BOOST_LOG(error) << "Couldn't open a va display from DRM with device: "sv << render_device; return -1; } va->va_display = display.get(); vaSetErrorCallback(display.get(), __log, &error); vaSetErrorCallback(display.get(), __log, &info); int major, minor; auto status = vaInitialize(display.get(), &major, &minor); if (status) { BOOST_LOG(error) << "Couldn't initialize va display: "sv << vaErrorStr(status); return -1; } BOOST_LOG(info) << "vaapi vendor: "sv << vaQueryVendorString(display.get()); *hw_device_buf = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VAAPI); auto ctx = (AVHWDeviceContext *) (*hw_device_buf)->data; auto hwctx = (AVVAAPIDeviceContext *) ctx->hwctx; // Ownership of the VADisplay and DRM fd is now ours to manage via the free() function hwctx->display = display.release(); ctx->user_opaque = priv; ctx->free = vaapi_hwdevice_ctx_free; fg.disable(); auto err = av_hwdevice_ctx_init(*hw_device_buf); if (err) { char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Failed to create FFMpeg hardware device context: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return err; } return 0; } static bool query(display_t::pointer display, VAProfile profile) { std::vector entrypoints; entrypoints.resize(vaMaxNumEntrypoints(display)); int count; auto status = vaQueryConfigEntrypoints(display, profile, entrypoints.data(), &count); if (status) { BOOST_LOG(error) << "Couldn't query entrypoints: "sv << vaErrorStr(status); return false; } entrypoints.resize(count); for (auto entrypoint : entrypoints) { if (entrypoint == VAEntrypointEncSlice || entrypoint == VAEntrypointEncSliceLP) { return true; } } return false; } bool validate(int fd) { va::display_t display {vaGetDisplayDRM(fd)}; if (!display) { char string[1024]; auto bytes = readlink(std::format("/proc/self/fd/{}", fd).c_str(), string, sizeof(string)); std::string_view render_device {string, (std::size_t) bytes}; BOOST_LOG(error) << "Couldn't open a va display from DRM with device: "sv << render_device; return false; } int major, minor; auto status = vaInitialize(display.get(), &major, &minor); if (status) { BOOST_LOG(error) << "Couldn't initialize va display: "sv << vaErrorStr(status); return false; } if (!query(display.get(), VAProfileH264Main)) { return false; } if (video::active_hevc_mode > 1 && !query(display.get(), VAProfileHEVCMain)) { return false; } if (video::active_hevc_mode > 2 && !query(display.get(), VAProfileHEVCMain10)) { return false; } return true; } std::unique_ptr make_avcodec_encode_device(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram) { if (vram) { auto egl = std::make_unique(); if (egl->init(width, height, std::move(card), offset_x, offset_y)) { return nullptr; } return egl; } else { auto egl = std::make_unique(); if (egl->init(width, height, std::move(card))) { return nullptr; } return egl; } } std::unique_ptr make_avcodec_encode_device(int width, int height, int offset_x, int offset_y, bool vram) { auto render_device = config::video.adapter_name.empty() ? "/dev/dri/renderD128" : config::video.adapter_name.c_str(); file_t file = open(render_device, O_RDWR); if (file.el < 0) { char string[1024]; BOOST_LOG(error) << "Couldn't open "sv << render_device << ": " << strerror_r(errno, string, sizeof(string)); return nullptr; } return make_avcodec_encode_device(width, height, std::move(file), offset_x, offset_y, vram); } std::unique_ptr make_avcodec_encode_device(int width, int height, bool vram) { return make_avcodec_encode_device(width, height, 0, 0, vram); } } // namespace va ================================================ FILE: src/platform/linux/vaapi.h ================================================ /** * @file src/platform/linux/vaapi.h * @brief Declarations for VA-API hardware accelerated capture. */ #pragma once // local includes #include "misc.h" #include "src/platform/common.h" namespace egl { struct surface_descriptor_t; } namespace va { /** * Width --> Width of the image * Height --> Height of the image * offset_x --> Horizontal offset of the image in the texture * offset_y --> Vertical offset of the image in the texture * file_t card --> The file descriptor of the render device used for encoding */ std::unique_ptr make_avcodec_encode_device(int width, int height, bool vram); std::unique_ptr make_avcodec_encode_device(int width, int height, int offset_x, int offset_y, bool vram); std::unique_ptr make_avcodec_encode_device(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram); // Ensure the render device pointed to by fd is capable of encoding h264 with the hevc_mode configured bool validate(int fd); } // namespace va ================================================ FILE: src/platform/linux/wayland.cpp ================================================ /** * @file src/platform/linux/wayland.cpp * @brief Definitions for Wayland capture. */ // standard includes #include // platform includes #include #include #include #include #include #include #include #include // local includes #include "graphics.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/round_robin.h" #include "src/utility.h" #include "wayland.h" extern const wl_interface wl_output_interface; using namespace std::literals; // Disable warning for converting incompatible functions #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpedantic" #pragma GCC diagnostic ignored "-Wpmf-conversions" namespace wl { // Helper to call C++ method from wayland C callback template static auto classCall(void *data, Params... params) -> decltype(((*reinterpret_cast(data)).*m)(params...)) { return ((*reinterpret_cast(data)).*m)(params...); } #define CLASS_CALL(c, m) classCall // Define buffer params listener static const struct zwp_linux_buffer_params_v1_listener params_listener = { .created = dmabuf_t::buffer_params_created, .failed = dmabuf_t::buffer_params_failed }; int display_t::init(const char *display_name) { if (!display_name) { display_name = std::getenv("WAYLAND_DISPLAY"); } if (!display_name) { BOOST_LOG(error) << "Environment variable WAYLAND_DISPLAY has not been defined"sv; return -1; } display_internal.reset(wl_display_connect(display_name)); if (!display_internal) { BOOST_LOG(error) << "Couldn't connect to Wayland display: "sv << display_name; return -1; } BOOST_LOG(info) << "Found display ["sv << display_name << ']'; return 0; } void display_t::roundtrip() { wl_display_roundtrip(display_internal.get()); } /** * @brief Waits up to the specified timeout to dispatch new events on the wl_display. * @param timeout The timeout in milliseconds. * @return `true` if new events were dispatched or `false` if the timeout expired. */ bool display_t::dispatch(std::chrono::milliseconds timeout) { // Check if any events are queued already. If not, flush // outgoing events, and prepare to wait for readability. if (wl_display_prepare_read(display_internal.get()) == 0) { wl_display_flush(display_internal.get()); // Wait for an event to come in struct pollfd pfd = {}; pfd.fd = wl_display_get_fd(display_internal.get()); pfd.events = POLLIN; if (poll(&pfd, 1, timeout.count()) == 1 && (pfd.revents & POLLIN)) { // Read the new event(s) wl_display_read_events(display_internal.get()); } else { // We timed out, so unlock the queue now wl_display_cancel_read(display_internal.get()); return false; } } // Dispatch any existing or new pending events wl_display_dispatch_pending(display_internal.get()); return true; } wl_registry *display_t::registry() { return wl_display_get_registry(display_internal.get()); } inline monitor_t::monitor_t(wl_output *output): output {output}, wl_listener { &CLASS_CALL(monitor_t, wl_geometry), &CLASS_CALL(monitor_t, wl_mode), &CLASS_CALL(monitor_t, wl_done), &CLASS_CALL(monitor_t, wl_scale), }, xdg_listener { &CLASS_CALL(monitor_t, xdg_position), &CLASS_CALL(monitor_t, xdg_size), &CLASS_CALL(monitor_t, xdg_done), &CLASS_CALL(monitor_t, xdg_name), &CLASS_CALL(monitor_t, xdg_description) } { } inline void monitor_t::xdg_name(zxdg_output_v1 *, const char *name) { this->name = name; BOOST_LOG(info) << "Name: "sv << this->name; } void monitor_t::xdg_description(zxdg_output_v1 *, const char *description) { this->description = description; BOOST_LOG(info) << "Found monitor: "sv << this->description; } void monitor_t::xdg_position(zxdg_output_v1 *, std::int32_t x, std::int32_t y) { viewport.offset_x = x; viewport.offset_y = y; BOOST_LOG(info) << "Offset: "sv << x << 'x' << y; } void monitor_t::xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height) { BOOST_LOG(info) << "Logical size: "sv << width << 'x' << height; } void monitor_t::wl_mode( wl_output *wl_output, std::uint32_t flags, std::int32_t width, std::int32_t height, std::int32_t refresh ) { viewport.width = width; viewport.height = height; BOOST_LOG(info) << "Resolution: "sv << width << 'x' << height; } void monitor_t::listen(zxdg_output_manager_v1 *output_manager) { auto xdg_output = zxdg_output_manager_v1_get_xdg_output(output_manager, output); zxdg_output_v1_add_listener(xdg_output, &xdg_listener, this); wl_output_add_listener(output, &wl_listener, this); } interface_t::interface_t() noexcept : screencopy_manager {nullptr}, dmabuf_interface {nullptr}, output_manager {nullptr}, listener { &CLASS_CALL(interface_t, add_interface), &CLASS_CALL(interface_t, del_interface) } { } void interface_t::listen(wl_registry *registry) { wl_registry_add_listener(registry, &listener, this); } void interface_t::add_interface( wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version ) { BOOST_LOG(debug) << "Available interface: "sv << interface << '(' << id << ") version "sv << version; if (!std::strcmp(interface, wl_output_interface.name)) { BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; monitors.emplace_back( std::make_unique( (wl_output *) wl_registry_bind(registry, id, &wl_output_interface, 2) ) ); } else if (!std::strcmp(interface, zxdg_output_manager_v1_interface.name)) { BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; output_manager = (zxdg_output_manager_v1 *) wl_registry_bind(registry, id, &zxdg_output_manager_v1_interface, version); this->interface[XDG_OUTPUT] = true; } else if (!std::strcmp(interface, zwlr_screencopy_manager_v1_interface.name)) { BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; screencopy_manager = (zwlr_screencopy_manager_v1 *) wl_registry_bind(registry, id, &zwlr_screencopy_manager_v1_interface, version); this->interface[WLR_EXPORT_DMABUF] = true; } else if (!std::strcmp(interface, zwp_linux_dmabuf_v1_interface.name)) { BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; dmabuf_interface = (zwp_linux_dmabuf_v1 *) wl_registry_bind(registry, id, &zwp_linux_dmabuf_v1_interface, version); this->interface[LINUX_DMABUF] = true; } } void interface_t::del_interface(wl_registry *registry, uint32_t id) { BOOST_LOG(info) << "Delete: "sv << id; } // Initialize GBM bool dmabuf_t::init_gbm() { if (gbm_device) { return true; } // Find render node drmDevice *devices[16]; int n = drmGetDevices2(0, devices, 16); if (n <= 0) { BOOST_LOG(error) << "No DRM devices found"sv; return false; } int drm_fd = -1; for (int i = 0; i < n; i++) { if (devices[i]->available_nodes & (1 << DRM_NODE_RENDER)) { drm_fd = open(devices[i]->nodes[DRM_NODE_RENDER], O_RDWR); if (drm_fd >= 0) { break; } } } drmFreeDevices(devices, n); if (drm_fd < 0) { BOOST_LOG(error) << "Failed to open DRM render node"sv; return false; } gbm_device = gbm_create_device(drm_fd); if (!gbm_device) { close(drm_fd); BOOST_LOG(error) << "Failed to create GBM device"sv; return false; } return true; } // Cleanup GBM void dmabuf_t::cleanup_gbm() { if (current_bo) { gbm_bo_destroy(current_bo); current_bo = nullptr; } if (current_wl_buffer) { wl_buffer_destroy(current_wl_buffer); current_wl_buffer = nullptr; } } dmabuf_t::dmabuf_t(): status {READY}, frames {}, current_frame {&frames[0]}, listener { &CLASS_CALL(dmabuf_t, buffer), &CLASS_CALL(dmabuf_t, flags), &CLASS_CALL(dmabuf_t, ready), &CLASS_CALL(dmabuf_t, failed), &CLASS_CALL(dmabuf_t, damage), &CLASS_CALL(dmabuf_t, linux_dmabuf), &CLASS_CALL(dmabuf_t, buffer_done), } { } // Start capture void dmabuf_t::listen( zwlr_screencopy_manager_v1 *screencopy_manager, zwp_linux_dmabuf_v1 *dmabuf_interface, wl_output *output, bool blend_cursor ) { this->dmabuf_interface = dmabuf_interface; // Reset state shm_info.supported = false; dmabuf_info.supported = false; // Create new frame auto frame = zwlr_screencopy_manager_v1_capture_output( screencopy_manager, blend_cursor ? 1 : 0, output ); // Store frame data pointer for callbacks zwlr_screencopy_frame_v1_set_user_data(frame, this); // Add listener zwlr_screencopy_frame_v1_add_listener(frame, &listener, this); status = WAITING; } dmabuf_t::~dmabuf_t() { cleanup_gbm(); for (auto &frame : frames) { frame.destroy(); } if (gbm_device) { // We should close the DRM FD, but it's owned by GBM gbm_device_destroy(gbm_device); gbm_device = nullptr; } } // Buffer format callback void dmabuf_t::buffer( zwlr_screencopy_frame_v1 *frame, uint32_t format, uint32_t width, uint32_t height, uint32_t stride ) { shm_info.supported = true; shm_info.format = format; shm_info.width = width; shm_info.height = height; shm_info.stride = stride; BOOST_LOG(debug) << "Screencopy supports SHM format: "sv << format; } // DMA-BUF format callback void dmabuf_t::linux_dmabuf( zwlr_screencopy_frame_v1 *frame, std::uint32_t format, std::uint32_t width, std::uint32_t height ) { dmabuf_info.supported = true; dmabuf_info.format = format; dmabuf_info.width = width; dmabuf_info.height = height; BOOST_LOG(debug) << "Screencopy supports DMA-BUF format: "sv << format; } // Flags callback void dmabuf_t::flags(zwlr_screencopy_frame_v1 *frame, std::uint32_t flags) { y_invert = flags & ZWLR_SCREENCOPY_FRAME_V1_FLAGS_Y_INVERT; BOOST_LOG(debug) << "Frame flags: "sv << flags << (y_invert ? " (y_invert)" : ""); } // DMA-BUF creation helper void dmabuf_t::create_and_copy_dmabuf(zwlr_screencopy_frame_v1 *frame) { if (!init_gbm()) { BOOST_LOG(error) << "Failed to initialize GBM"sv; zwlr_screencopy_frame_v1_destroy(frame); status = REINIT; return; } // Create GBM buffer current_bo = gbm_bo_create(gbm_device, dmabuf_info.width, dmabuf_info.height, dmabuf_info.format, GBM_BO_USE_RENDERING); if (!current_bo) { BOOST_LOG(error) << "Failed to create GBM buffer"sv; zwlr_screencopy_frame_v1_destroy(frame); status = REINIT; return; } // Get buffer info int fd = gbm_bo_get_fd(current_bo); if (fd < 0) { BOOST_LOG(error) << "Failed to get buffer FD"sv; gbm_bo_destroy(current_bo); current_bo = nullptr; zwlr_screencopy_frame_v1_destroy(frame); status = REINIT; return; } uint32_t stride = gbm_bo_get_stride(current_bo); uint64_t modifier = gbm_bo_get_modifier(current_bo); // Store in surface descriptor for later use auto next_frame = get_next_frame(); next_frame->sd.fds[0] = fd; next_frame->sd.pitches[0] = stride; next_frame->sd.offsets[0] = 0; next_frame->sd.modifier = modifier; // Create linux-dmabuf buffer auto params = zwp_linux_dmabuf_v1_create_params(dmabuf_interface); zwp_linux_buffer_params_v1_add(params, fd, 0, 0, stride, modifier >> 32, modifier & 0xffffffff); // Add listener for buffer creation zwp_linux_buffer_params_v1_add_listener(params, ¶ms_listener, frame); // Create Wayland buffer (async - callback will handle copy) zwp_linux_buffer_params_v1_create(params, dmabuf_info.width, dmabuf_info.height, dmabuf_info.format, 0); } // Buffer done callback - time to create buffer void dmabuf_t::buffer_done(zwlr_screencopy_frame_v1 *frame) { auto next_frame = get_next_frame(); // Prefer DMA-BUF if supported if (dmabuf_info.supported && dmabuf_interface) { // Store format info first next_frame->sd.fourcc = dmabuf_info.format; next_frame->sd.width = dmabuf_info.width; next_frame->sd.height = dmabuf_info.height; // Create and start copy create_and_copy_dmabuf(frame); } else if (shm_info.supported) { // SHM fallback would go here BOOST_LOG(warning) << "SHM capture not implemented"sv; zwlr_screencopy_frame_v1_destroy(frame); status = REINIT; } else { BOOST_LOG(error) << "No supported buffer types"sv; zwlr_screencopy_frame_v1_destroy(frame); status = REINIT; } } // Buffer params created callback void dmabuf_t::buffer_params_created( void *data, struct zwp_linux_buffer_params_v1 *params, struct wl_buffer *buffer ) { auto frame = static_cast(data); auto self = static_cast(zwlr_screencopy_frame_v1_get_user_data(frame)); // Store for cleanup self->current_wl_buffer = buffer; // Start the actual copy zwlr_screencopy_frame_v1_copy(frame, buffer); } // Buffer params failed callback void dmabuf_t::buffer_params_failed( void *data, struct zwp_linux_buffer_params_v1 *params ) { auto frame = static_cast(data); auto self = static_cast(zwlr_screencopy_frame_v1_get_user_data(frame)); BOOST_LOG(error) << "Failed to create buffer from params"sv; self->cleanup_gbm(); zwlr_screencopy_frame_v1_destroy(frame); self->status = REINIT; } // Ready callback void dmabuf_t::ready( zwlr_screencopy_frame_v1 *frame, std::uint32_t tv_sec_hi, std::uint32_t tv_sec_lo, std::uint32_t tv_nsec ) { BOOST_LOG(debug) << "Frame ready"sv; // Frame is ready for use, GBM buffer now contains screen content current_frame->destroy(); current_frame = get_next_frame(); // Keep the GBM buffer alive but destroy the Wayland objects if (current_wl_buffer) { wl_buffer_destroy(current_wl_buffer); current_wl_buffer = nullptr; } cleanup_gbm(); zwlr_screencopy_frame_v1_destroy(frame); status = READY; } // Failed callback void dmabuf_t::failed(zwlr_screencopy_frame_v1 *frame) { BOOST_LOG(error) << "Frame capture failed"sv; // Clean up resources cleanup_gbm(); auto next_frame = get_next_frame(); next_frame->destroy(); zwlr_screencopy_frame_v1_destroy(frame); status = REINIT; } void dmabuf_t::damage( zwlr_screencopy_frame_v1 *frame, std::uint32_t x, std::uint32_t y, std::uint32_t width, std::uint32_t height ) {}; void frame_t::destroy() { for (auto x = 0; x < 4; ++x) { if (sd.fds[x] >= 0) { close(sd.fds[x]); sd.fds[x] = -1; } } } frame_t::frame_t() { // File descriptors aren't open std::fill_n(sd.fds, 4, -1); }; std::vector> monitors(const char *display_name) { display_t display; if (display.init(display_name)) { return {}; } interface_t interface; interface.listen(display.registry()); display.roundtrip(); if (!interface[interface_t::XDG_OUTPUT]) { BOOST_LOG(error) << "Missing Wayland wire XDG_OUTPUT"sv; return {}; } for (auto &monitor : interface.monitors) { monitor->listen(interface.output_manager); } display.roundtrip(); return std::move(interface.monitors); } static bool validate() { display_t display; return display.init() == 0; } int init() { static bool validated = validate(); return !validated; } } // namespace wl #pragma GCC diagnostic pop ================================================ FILE: src/platform/linux/wayland.h ================================================ /** * @file src/platform/linux/wayland.h * @brief Declarations for Wayland capture. */ #pragma once // standard includes #include #ifdef SUNSHINE_BUILD_WAYLAND #include #include #include #endif // local includes #include "graphics.h" /** * The classes defined in this macro block should only be used by * cpp files whose compilation depends on SUNSHINE_BUILD_WAYLAND */ #ifdef SUNSHINE_BUILD_WAYLAND namespace wl { using display_internal_t = util::safe_ptr; class frame_t { public: frame_t(); void destroy(); egl::surface_descriptor_t sd; }; class dmabuf_t { public: enum status_e { WAITING, ///< Waiting for a frame READY, ///< Frame is ready REINIT, ///< Reinitialize the frame }; dmabuf_t(); ~dmabuf_t(); dmabuf_t(dmabuf_t &&) = delete; dmabuf_t(const dmabuf_t &) = delete; dmabuf_t &operator=(const dmabuf_t &) = delete; dmabuf_t &operator=(dmabuf_t &&) = delete; void listen(zwlr_screencopy_manager_v1 *screencopy_manager, zwp_linux_dmabuf_v1 *dmabuf_interface, wl_output *output, bool blend_cursor = false); static void buffer_params_created(void *data, struct zwp_linux_buffer_params_v1 *params, struct wl_buffer *wl_buffer); static void buffer_params_failed(void *data, struct zwp_linux_buffer_params_v1 *params); void buffer(zwlr_screencopy_frame_v1 *frame, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t stride); void linux_dmabuf(zwlr_screencopy_frame_v1 *frame, std::uint32_t format, std::uint32_t width, std::uint32_t height); void buffer_done(zwlr_screencopy_frame_v1 *frame); void flags(zwlr_screencopy_frame_v1 *frame, std::uint32_t flags); void damage(zwlr_screencopy_frame_v1 *frame, std::uint32_t x, std::uint32_t y, std::uint32_t width, std::uint32_t height); void ready(zwlr_screencopy_frame_v1 *frame, std::uint32_t tv_sec_hi, std::uint32_t tv_sec_lo, std::uint32_t tv_nsec); void failed(zwlr_screencopy_frame_v1 *frame); frame_t *get_next_frame() { return current_frame == &frames[0] ? &frames[1] : &frames[0]; } status_e status; std::array frames; frame_t *current_frame; zwlr_screencopy_frame_v1_listener listener; private: bool init_gbm(); void cleanup_gbm(); void create_and_copy_dmabuf(zwlr_screencopy_frame_v1 *frame); zwp_linux_dmabuf_v1 *dmabuf_interface {nullptr}; struct { bool supported {false}; std::uint32_t format; std::uint32_t width; std::uint32_t height; std::uint32_t stride; } shm_info; struct { bool supported {false}; std::uint32_t format; std::uint32_t width; std::uint32_t height; } dmabuf_info; struct gbm_device *gbm_device {nullptr}; struct gbm_bo *current_bo {nullptr}; struct wl_buffer *current_wl_buffer {nullptr}; bool y_invert {false}; }; class monitor_t { public: explicit monitor_t(wl_output *output); monitor_t(monitor_t &&) = delete; monitor_t(const monitor_t &) = delete; monitor_t &operator=(const monitor_t &) = delete; monitor_t &operator=(monitor_t &&) = delete; void listen(zxdg_output_manager_v1 *output_manager); void xdg_name(zxdg_output_v1 *, const char *name); void xdg_description(zxdg_output_v1 *, const char *description); void xdg_position(zxdg_output_v1 *, std::int32_t x, std::int32_t y); void xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height); void xdg_done(zxdg_output_v1 *) {} void wl_geometry(wl_output *wl_output, std::int32_t x, std::int32_t y, std::int32_t physical_width, std::int32_t physical_height, std::int32_t subpixel, const char *make, const char *model, std::int32_t transform) {} void wl_mode(wl_output *wl_output, std::uint32_t flags, std::int32_t width, std::int32_t height, std::int32_t refresh); void wl_done(wl_output *wl_output) {} void wl_scale(wl_output *wl_output, std::int32_t factor) {} wl_output *output; std::string name; std::string description; platf::touch_port_t viewport; wl_output_listener wl_listener; zxdg_output_v1_listener xdg_listener; }; class interface_t { struct bind_t { std::uint32_t id; std::uint32_t version; }; public: enum interface_e { XDG_OUTPUT, ///< xdg-output WLR_EXPORT_DMABUF, ///< screencopy manager LINUX_DMABUF, ///< linux-dmabuf protocol MAX_INTERFACES, ///< Maximum number of interfaces }; interface_t() noexcept; interface_t(interface_t &&) = delete; interface_t(const interface_t &) = delete; interface_t &operator=(const interface_t &) = delete; interface_t &operator=(interface_t &&) = delete; void listen(wl_registry *registry); bool operator[](interface_e bit) const { return interface[bit]; } std::vector> monitors; zwlr_screencopy_manager_v1 *screencopy_manager {nullptr}; zwp_linux_dmabuf_v1 *dmabuf_interface {nullptr}; zxdg_output_manager_v1 *output_manager {nullptr}; private: void add_interface(wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version); void del_interface(wl_registry *registry, uint32_t id); std::bitset interface; wl_registry_listener listener; }; class display_t { public: /** * @brief Initialize display. * If display_name == nullptr -> display_name = std::getenv("WAYLAND_DISPLAY") * @param display_name The name of the display. * @return 0 on success, -1 on failure. */ int init(const char *display_name = nullptr); // Roundtrip with Wayland connection void roundtrip(); // Wait up to the timeout to read and dispatch new events bool dispatch(std::chrono::milliseconds timeout); // Get the registry associated with the display // No need to manually free the registry wl_registry *registry(); inline display_internal_t::pointer get() { return display_internal.get(); } private: display_internal_t display_internal; }; std::vector> monitors(const char *display_name = nullptr); int init(); } // namespace wl #else struct wl_output; struct zxdg_output_manager_v1; namespace wl { class monitor_t { public: monitor_t(wl_output *output); monitor_t(monitor_t &&) = delete; monitor_t(const monitor_t &) = delete; monitor_t &operator=(const monitor_t &) = delete; monitor_t &operator=(monitor_t &&) = delete; void listen(zxdg_output_manager_v1 *output_manager); wl_output *output; std::string name; std::string description; platf::touch_port_t viewport; }; inline std::vector> monitors(const char *display_name = nullptr) { return {}; } inline int init() { return -1; } } // namespace wl #endif ================================================ FILE: src/platform/linux/wlgrab.cpp ================================================ /** * @file src/platform/linux/wlgrab.cpp * @brief Definitions for wlgrab capture. */ // standard includes #include // local includes #include "cuda.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/video.h" #include "vaapi.h" #include "wayland.h" using namespace std::literals; namespace wl { static int env_width; static int env_height; struct img_t: public platf::img_t { ~img_t() override { delete[] data; data = nullptr; } }; class wlr_t: public platf::display_t { public: int init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { delay = std::chrono::nanoseconds {1s} / config.framerate; mem_type = hwdevice_type; if (display.init()) { return -1; } interface.listen(display.registry()); display.roundtrip(); if (!interface[wl::interface_t::XDG_OUTPUT]) { BOOST_LOG(error) << "Missing Wayland wire for xdg_output"sv; return -1; } if (!interface[wl::interface_t::WLR_EXPORT_DMABUF]) { BOOST_LOG(error) << "Missing Wayland wire for wlr-export-dmabuf"sv; return -1; } auto monitor = interface.monitors[0].get(); if (!display_name.empty()) { auto streamedMonitor = util::from_view(display_name); if (streamedMonitor >= 0 && streamedMonitor < interface.monitors.size()) { monitor = interface.monitors[streamedMonitor].get(); } } monitor->listen(interface.output_manager); display.roundtrip(); output = monitor->output; offset_x = monitor->viewport.offset_x; offset_y = monitor->viewport.offset_y; width = monitor->viewport.width; height = monitor->viewport.height; this->env_width = ::wl::env_width; this->env_height = ::wl::env_height; BOOST_LOG(info) << "Selected monitor ["sv << monitor->description << "] for streaming"sv; BOOST_LOG(debug) << "Offset: "sv << offset_x << 'x' << offset_y; BOOST_LOG(debug) << "Resolution: "sv << width << 'x' << height; BOOST_LOG(debug) << "Desktop Resolution: "sv << env_width << 'x' << env_height; return 0; } int dummy_img(platf::img_t *img) override { return 0; } inline platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { auto to = std::chrono::steady_clock::now() + timeout; // Dispatch events until we get a new frame or the timeout expires dmabuf.listen(interface.screencopy_manager, interface.dmabuf_interface, output, cursor); do { auto remaining_time_ms = std::chrono::duration_cast(to - std::chrono::steady_clock::now()); if (remaining_time_ms.count() < 0 || !display.dispatch(remaining_time_ms)) { return platf::capture_e::timeout; } } while (dmabuf.status == dmabuf_t::WAITING); auto current_frame = dmabuf.current_frame; if ( dmabuf.status == dmabuf_t::REINIT || current_frame->sd.width != width || current_frame->sd.height != height ) { return platf::capture_e::reinit; } return platf::capture_e::ok; } platf::mem_type_e mem_type; std::chrono::nanoseconds delay; wl::display_t display; interface_t interface; dmabuf_t dmabuf; wl_output *output; }; class wlr_ram_t: public wlr_t { public: platf::capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); sleep_overshoot_logger.reset(); while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { std::this_thread::sleep_for(next_frame - now); sleep_overshoot_logger.first_point(next_frame); sleep_overshoot_logger.second_point_now_and_log(); } next_frame += delay; if (next_frame < now) { // some major slowdown happened; we couldn't keep up next_frame = now + delay; } std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: case platf::capture_e::interrupted: return status; case platf::capture_e::timeout: if (!push_captured_image_cb(std::move(img_out), false)) { return platf::capture_e::ok; } break; case platf::capture_e::ok: if (!push_captured_image_cb(std::move(img_out), true)) { return platf::capture_e::ok; } break; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; return status; } } return platf::capture_e::ok; } platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { auto status = wlr_t::snapshot(pull_free_image_cb, img_out, timeout, cursor); if (status != platf::capture_e::ok) { return status; } auto frame_timestamp = std::chrono::steady_clock::now(); auto current_frame = dmabuf.current_frame; auto rgb_opt = egl::import_source(egl_display.get(), current_frame->sd); if (!rgb_opt) { return platf::capture_e::reinit; } if (!pull_free_image_cb(img_out)) { return platf::capture_e::interrupted; } gl::ctx.BindTexture(GL_TEXTURE_2D, (*rgb_opt)->tex[0]); // Don't remove these lines, see https://github.com/LizardByte/Sunshine/issues/453 int w, h; gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h); BOOST_LOG(debug) << "width and height: w "sv << w << " h "sv << h; 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); gl::ctx.BindTexture(GL_TEXTURE_2D, 0); img_out->frame_timestamp = frame_timestamp; return platf::capture_e::ok; } int init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { if (wlr_t::init(hwdevice_type, display_name, config)) { return -1; } egl_display = egl::make_display(display.get()); if (!egl_display) { return -1; } auto ctx_opt = egl::make_ctx(egl_display.get()); if (!ctx_opt) { return -1; } ctx = std::move(*ctx_opt); return 0; } std::unique_ptr make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override { #ifdef SUNSHINE_BUILD_VAAPI if (mem_type == platf::mem_type_e::vaapi) { return va::make_avcodec_encode_device(width, height, false); } #endif #ifdef SUNSHINE_BUILD_CUDA if (mem_type == platf::mem_type_e::cuda) { return cuda::make_avcodec_encode_device(width, height, false); } #endif return std::make_unique(); } std::shared_ptr alloc_img() override { auto img = std::make_shared(); img->width = width; img->height = height; img->pixel_pitch = 4; img->row_pitch = img->pixel_pitch * width; img->data = new std::uint8_t[height * img->row_pitch]; return img; } egl::display_t egl_display; egl::ctx_t ctx; }; class wlr_vram_t: public wlr_t { public: platf::capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); sleep_overshoot_logger.reset(); while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { std::this_thread::sleep_for(next_frame - now); sleep_overshoot_logger.first_point(next_frame); sleep_overshoot_logger.second_point_now_and_log(); } next_frame += delay; if (next_frame < now) { // some major slowdown happened; we couldn't keep up next_frame = now + delay; } std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: case platf::capture_e::interrupted: return status; case platf::capture_e::timeout: if (!push_captured_image_cb(std::move(img_out), false)) { return platf::capture_e::ok; } break; case platf::capture_e::ok: if (!push_captured_image_cb(std::move(img_out), true)) { return platf::capture_e::ok; } break; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; return status; } } return platf::capture_e::ok; } platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { auto status = wlr_t::snapshot(pull_free_image_cb, img_out, timeout, cursor); if (status != platf::capture_e::ok) { return status; } auto frame_timestamp = std::chrono::steady_clock::now(); if (!pull_free_image_cb(img_out)) { return platf::capture_e::interrupted; } auto img = (egl::img_descriptor_t *) img_out.get(); img->reset(); auto current_frame = dmabuf.current_frame; ++sequence; img->sequence = sequence; img->sd = current_frame->sd; img->frame_timestamp = frame_timestamp; // Prevent dmabuf from closing the file descriptors. std::fill_n(current_frame->sd.fds, 4, -1); return platf::capture_e::ok; } std::shared_ptr alloc_img() override { auto img = std::make_shared(); img->width = width; img->height = height; img->sequence = 0; img->serial = std::numeric_limitsserial)>::max(); img->data = nullptr; // File descriptors aren't open std::fill_n(img->sd.fds, 4, -1); return img; } std::unique_ptr make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override { #ifdef SUNSHINE_BUILD_VAAPI if (mem_type == platf::mem_type_e::vaapi) { return va::make_avcodec_encode_device(width, height, 0, 0, true); } #endif #ifdef SUNSHINE_BUILD_CUDA if (mem_type == platf::mem_type_e::cuda) { return cuda::make_avcodec_gl_encode_device(width, height, 0, 0); } #endif return std::make_unique(); } int dummy_img(platf::img_t *img) override { // Empty images are recognized as dummies by the zero sequence number return 0; } std::uint64_t sequence {}; }; } // namespace wl namespace platf { std::shared_ptr wl_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) { BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv; return nullptr; } if (hwdevice_type == platf::mem_type_e::vaapi || hwdevice_type == platf::mem_type_e::cuda) { auto wlr = std::make_shared(); if (wlr->init(hwdevice_type, display_name, config)) { return nullptr; } return wlr; } auto wlr = std::make_shared(); if (wlr->init(hwdevice_type, display_name, config)) { return nullptr; } return wlr; } std::vector wl_display_names() { std::vector display_names; wl::display_t display; if (display.init()) { return {}; } wl::interface_t interface; interface.listen(display.registry()); display.roundtrip(); if (!interface[wl::interface_t::XDG_OUTPUT]) { BOOST_LOG(warning) << "Missing Wayland wire for xdg_output"sv; return {}; } if (!interface[wl::interface_t::WLR_EXPORT_DMABUF]) { BOOST_LOG(warning) << "Missing Wayland wire for wlr-export-dmabuf"sv; return {}; } wl::env_width = 0; wl::env_height = 0; for (auto &monitor : interface.monitors) { monitor->listen(interface.output_manager); } display.roundtrip(); BOOST_LOG(info) << "-------- Start of Wayland monitor list --------"sv; for (int x = 0; x < interface.monitors.size(); ++x) { auto monitor = interface.monitors[x].get(); wl::env_width = std::max(wl::env_width, (int) (monitor->viewport.offset_x + monitor->viewport.width)); wl::env_height = std::max(wl::env_height, (int) (monitor->viewport.offset_y + monitor->viewport.height)); BOOST_LOG(info) << "Monitor " << x << " is "sv << monitor->name << ": "sv << monitor->description; display_names.emplace_back(std::to_string(x)); } BOOST_LOG(info) << "--------- End of Wayland monitor list ---------"sv; return display_names; } } // namespace platf ================================================ FILE: src/platform/linux/x11grab.cpp ================================================ /** * @file src/platform/linux/x11grab.cpp * @brief Definitions for x11 capture. */ // standard includes #include #include // plaform includes #include #include #include #include #include #include #include #include #include // local includes #include "cuda.h" #include "graphics.h" #include "misc.h" #include "src/config.h" #include "src/globals.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/task_pool.h" #include "src/video.h" #include "vaapi.h" #include "x11grab.h" using namespace std::literals; namespace platf { int load_xcb(); int load_x11(); namespace x11 { #define _FN(x, ret, args) \ typedef ret(*x##_fn) args; \ static x##_fn x _FN(GetImage, XImage *, (Display * display, Drawable d, int x, int y, unsigned int width, unsigned int height, unsigned long plane_mask, int format)); _FN(OpenDisplay, Display *, (_Xconst char *display_name)); _FN(GetWindowAttributes, Status, (Display * display, Window w, XWindowAttributes *window_attributes_return)); _FN(CloseDisplay, int, (Display * display)); _FN(Free, int, (void *data)); _FN(InitThreads, Status, (void) ); namespace rr { _FN(GetScreenResources, XRRScreenResources *, (Display * dpy, Window window)); _FN(GetOutputInfo, XRROutputInfo *, (Display * dpy, XRRScreenResources *resources, RROutput output)); _FN(GetCrtcInfo, XRRCrtcInfo *, (Display * dpy, XRRScreenResources *resources, RRCrtc crtc)); _FN(FreeScreenResources, void, (XRRScreenResources * resources)); _FN(FreeOutputInfo, void, (XRROutputInfo * outputInfo)); _FN(FreeCrtcInfo, void, (XRRCrtcInfo * crtcInfo)); static int init() { static void *handle {nullptr}; static bool funcs_loaded = false; if (funcs_loaded) { return 0; } if (!handle) { handle = dyn::handle({"libXrandr.so.2", "libXrandr.so"}); if (!handle) { return -1; } } std::vector> funcs { {(dyn::apiproc *) &GetScreenResources, "XRRGetScreenResources"}, {(dyn::apiproc *) &GetOutputInfo, "XRRGetOutputInfo"}, {(dyn::apiproc *) &GetCrtcInfo, "XRRGetCrtcInfo"}, {(dyn::apiproc *) &FreeScreenResources, "XRRFreeScreenResources"}, {(dyn::apiproc *) &FreeOutputInfo, "XRRFreeOutputInfo"}, {(dyn::apiproc *) &FreeCrtcInfo, "XRRFreeCrtcInfo"}, }; if (dyn::load(handle, funcs)) { return -1; } funcs_loaded = true; return 0; } } // namespace rr namespace fix { _FN(GetCursorImage, XFixesCursorImage *, (Display * dpy)); static int init() { static void *handle {nullptr}; static bool funcs_loaded = false; if (funcs_loaded) { return 0; } if (!handle) { handle = dyn::handle({"libXfixes.so.3", "libXfixes.so"}); if (!handle) { return -1; } } std::vector> funcs { {(dyn::apiproc *) &GetCursorImage, "XFixesGetCursorImage"}, }; if (dyn::load(handle, funcs)) { return -1; } funcs_loaded = true; return 0; } } // namespace fix static int init() { static void *handle {nullptr}; static bool funcs_loaded = false; if (funcs_loaded) { return 0; } if (!handle) { handle = dyn::handle({"libX11.so.6", "libX11.so"}); if (!handle) { return -1; } } std::vector> funcs { {(dyn::apiproc *) &GetImage, "XGetImage"}, {(dyn::apiproc *) &OpenDisplay, "XOpenDisplay"}, {(dyn::apiproc *) &GetWindowAttributes, "XGetWindowAttributes"}, {(dyn::apiproc *) &Free, "XFree"}, {(dyn::apiproc *) &CloseDisplay, "XCloseDisplay"}, {(dyn::apiproc *) &InitThreads, "XInitThreads"}, }; if (dyn::load(handle, funcs)) { return -1; } funcs_loaded = true; return 0; } } // namespace x11 namespace xcb { static xcb_extension_t *shm_id; _FN(shm_get_image_reply, xcb_shm_get_image_reply_t *, (xcb_connection_t * c, xcb_shm_get_image_cookie_t cookie, xcb_generic_error_t **e)); _FN(shm_get_image_unchecked, xcb_shm_get_image_cookie_t, (xcb_connection_t * c, xcb_drawable_t drawable, int16_t x, int16_t y, uint16_t width, uint16_t height, uint32_t plane_mask, uint8_t format, xcb_shm_seg_t shmseg, uint32_t offset)); _FN(shm_attach, xcb_void_cookie_t, (xcb_connection_t * c, xcb_shm_seg_t shmseg, uint32_t shmid, uint8_t read_only)); _FN(get_extension_data, xcb_query_extension_reply_t *, (xcb_connection_t * c, xcb_extension_t *ext)); _FN(get_setup, xcb_setup_t *, (xcb_connection_t * c)); _FN(disconnect, void, (xcb_connection_t * c)); _FN(connection_has_error, int, (xcb_connection_t * c)); _FN(connect, xcb_connection_t *, (const char *displayname, int *screenp)); _FN(setup_roots_iterator, xcb_screen_iterator_t, (const xcb_setup_t *R)); _FN(generate_id, std::uint32_t, (xcb_connection_t * c)); int init_shm() { static void *handle {nullptr}; static bool funcs_loaded = false; if (funcs_loaded) { return 0; } if (!handle) { handle = dyn::handle({"libxcb-shm.so.0", "libxcb-shm.so"}); if (!handle) { return -1; } } std::vector> funcs { {(dyn::apiproc *) &shm_id, "xcb_shm_id"}, {(dyn::apiproc *) &shm_get_image_reply, "xcb_shm_get_image_reply"}, {(dyn::apiproc *) &shm_get_image_unchecked, "xcb_shm_get_image_unchecked"}, {(dyn::apiproc *) &shm_attach, "xcb_shm_attach"}, }; if (dyn::load(handle, funcs)) { return -1; } funcs_loaded = true; return 0; } int init() { static void *handle {nullptr}; static bool funcs_loaded = false; if (funcs_loaded) { return 0; } if (!handle) { handle = dyn::handle({"libxcb.so.1", "libxcb.so"}); if (!handle) { return -1; } } std::vector> funcs { {(dyn::apiproc *) &get_extension_data, "xcb_get_extension_data"}, {(dyn::apiproc *) &get_setup, "xcb_get_setup"}, {(dyn::apiproc *) &disconnect, "xcb_disconnect"}, {(dyn::apiproc *) &connection_has_error, "xcb_connection_has_error"}, {(dyn::apiproc *) &connect, "xcb_connect"}, {(dyn::apiproc *) &setup_roots_iterator, "xcb_setup_roots_iterator"}, {(dyn::apiproc *) &generate_id, "xcb_generate_id"}, }; if (dyn::load(handle, funcs)) { return -1; } funcs_loaded = true; return 0; } #undef _FN } // namespace xcb void freeImage(XImage *); void freeX(XFixesCursorImage *); using xcb_connect_t = util::dyn_safe_ptr; using xcb_img_t = util::c_ptr; using ximg_t = util::safe_ptr; using xcursor_t = util::safe_ptr; using crtc_info_t = util::dyn_safe_ptr<_XRRCrtcInfo, &x11::rr::FreeCrtcInfo>; using output_info_t = util::dyn_safe_ptr<_XRROutputInfo, &x11::rr::FreeOutputInfo>; using screen_res_t = util::dyn_safe_ptr<_XRRScreenResources, &x11::rr::FreeScreenResources>; class shm_id_t { public: shm_id_t(): id {-1} { } shm_id_t(int id): id {id} { } shm_id_t(shm_id_t &&other) noexcept: id(other.id) { other.id = -1; } ~shm_id_t() { if (id != -1) { shmctl(id, IPC_RMID, nullptr); id = -1; } } int id; }; class shm_data_t { public: shm_data_t(): data {(void *) -1} { } shm_data_t(void *data): data {data} { } shm_data_t(shm_data_t &&other) noexcept: data(other.data) { other.data = (void *) -1; } ~shm_data_t() { if ((std::uintptr_t) data != -1) { shmdt(data); } } void *data; }; struct x11_img_t: public img_t { ximg_t img; }; struct shm_img_t: public img_t { ~shm_img_t() override { delete[] data; data = nullptr; } }; static void blend_cursor(Display *display, img_t &img, int offsetX, int offsetY) { xcursor_t overlay {x11::fix::GetCursorImage(display)}; if (!overlay) { BOOST_LOG(error) << "Couldn't get cursor from XFixesGetCursorImage"sv; return; } overlay->x -= overlay->xhot; overlay->y -= overlay->yhot; overlay->x -= offsetX; overlay->y -= offsetY; overlay->x = std::max((short) 0, overlay->x); overlay->y = std::max((short) 0, overlay->y); auto pixels = (int *) img.data; auto screen_height = img.height; auto screen_width = img.width; auto delta_height = std::min(overlay->height, std::max(0, screen_height - overlay->y)); auto delta_width = std::min(overlay->width, std::max(0, screen_width - overlay->x)); for (auto y = 0; y < delta_height; ++y) { auto overlay_begin = &overlay->pixels[y * overlay->width]; auto overlay_end = &overlay->pixels[y * overlay->width + delta_width]; auto pixels_begin = &pixels[(y + overlay->y) * (img.row_pitch / img.pixel_pitch) + overlay->x]; std::for_each(overlay_begin, overlay_end, [&](long pixel) { int *pixel_p = (int *) &pixel; auto colors_in = (uint8_t *) pixels_begin; auto alpha = (*(uint *) pixel_p) >> 24u; if (alpha == 255) { *pixels_begin = *pixel_p; } else { auto colors_out = (uint8_t *) pixel_p; colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255; colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255; colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255 / 2) / 255; } ++pixels_begin; }); } } struct x11_attr_t: public display_t { std::chrono::nanoseconds delay; x11::xdisplay_t xdisplay; Window xwindow; XWindowAttributes xattr; mem_type_e mem_type; /** * Last X (NOT the streamed monitor!) size. * This way we can trigger reinitialization if the dimensions changed while streaming */ // int env_width, env_height; x11_attr_t(mem_type_e mem_type): xdisplay {x11::OpenDisplay(nullptr)}, xwindow {}, xattr {}, mem_type {mem_type} { x11::InitThreads(); } int init(const std::string &display_name, const ::video::config_t &config) { if (!xdisplay) { BOOST_LOG(error) << "Could not open X11 display"sv; return -1; } delay = std::chrono::nanoseconds {1s} / config.framerate; xwindow = DefaultRootWindow(xdisplay.get()); refresh(); int streamedMonitor = -1; if (!display_name.empty()) { streamedMonitor = (int) util::from_view(display_name); } if (streamedMonitor != -1) { BOOST_LOG(info) << "Configuring selected display ("sv << streamedMonitor << ") to stream"sv; screen_res_t screenr {x11::rr::GetScreenResources(xdisplay.get(), xwindow)}; int output = screenr->noutput; output_info_t result; int monitor = 0; for (int x = 0; x < output; ++x) { output_info_t out_info {x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x])}; if (out_info) { if (monitor++ == streamedMonitor) { result = std::move(out_info); break; } } } if (!result) { BOOST_LOG(error) << "Could not stream display number ["sv << streamedMonitor << "], there are only ["sv << monitor << "] displays."sv; return -1; } if (result->crtc) { crtc_info_t crt_info {x11::rr::GetCrtcInfo(xdisplay.get(), screenr.get(), result->crtc)}; BOOST_LOG(info) << "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; width = crt_info->width; height = crt_info->height; offset_x = crt_info->x; offset_y = crt_info->y; } else { BOOST_LOG(warning) << "Couldn't get requested display info, defaulting to recording entire virtual desktop"sv; width = xattr.width; height = xattr.height; } } else { width = xattr.width; height = xattr.height; } env_width = xattr.width; env_height = xattr.height; return 0; } /** * Called when the display attributes should change. */ void refresh() { x11::GetWindowAttributes(xdisplay.get(), xwindow, &xattr); // Update xattr's } capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); sleep_overshoot_logger.reset(); while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { std::this_thread::sleep_for(next_frame - now); sleep_overshoot_logger.first_point(next_frame); sleep_overshoot_logger.second_point_now_and_log(); } next_frame += delay; if (next_frame < now) { // some major slowdown happened; we couldn't keep up next_frame = now + delay; } std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: case platf::capture_e::interrupted: return status; case platf::capture_e::timeout: if (!push_captured_image_cb(std::move(img_out), false)) { return platf::capture_e::ok; } break; case platf::capture_e::ok: if (!push_captured_image_cb(std::move(img_out), true)) { return platf::capture_e::ok; } break; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; return status; } } return capture_e::ok; } capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { refresh(); // The whole X server changed, so we must reinit everything if (xattr.width != env_width || xattr.height != env_height) { BOOST_LOG(warning) << "X dimensions changed in non-SHM mode, request reinit"sv; return capture_e::reinit; } if (!pull_free_image_cb(img_out)) { return platf::capture_e::interrupted; } auto img = (x11_img_t *) img_out.get(); XImage *x_img {x11::GetImage(xdisplay.get(), xwindow, offset_x, offset_y, width, height, AllPlanes, ZPixmap)}; img->frame_timestamp = std::chrono::steady_clock::now(); img->width = x_img->width; img->height = x_img->height; img->data = (uint8_t *) x_img->data; img->row_pitch = x_img->bytes_per_line; img->pixel_pitch = x_img->bits_per_pixel / 8; img->img.reset(x_img); if (cursor) { blend_cursor(xdisplay.get(), *img, offset_x, offset_y); } return capture_e::ok; } std::shared_ptr alloc_img() override { return std::make_shared(); } std::unique_ptr make_avcodec_encode_device(pix_fmt_e pix_fmt) override { #ifdef SUNSHINE_BUILD_VAAPI if (mem_type == mem_type_e::vaapi) { return va::make_avcodec_encode_device(width, height, false); } #endif #ifdef SUNSHINE_BUILD_CUDA if (mem_type == mem_type_e::cuda) { return cuda::make_avcodec_encode_device(width, height, false); } #endif return std::make_unique(); } int dummy_img(img_t *img) override { // TODO: stop cheating and give black image if (!img) { return -1; }; auto pull_dummy_img_callback = [&img](std::shared_ptr &img_out) -> bool { img_out = img->shared_from_this(); return true; }; std::shared_ptr img_out; snapshot(pull_dummy_img_callback, img_out, 0s, true); return 0; } }; struct shm_attr_t: public x11_attr_t { x11::xdisplay_t shm_xdisplay; // Prevent race condition with x11_attr_t::xdisplay xcb_connect_t xcb; xcb_screen_t *display; std::uint32_t seg; shm_id_t shm_id; shm_data_t data; task_pool_util::TaskPool::task_id_t refresh_task_id; void delayed_refresh() { refresh(); refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id; } shm_attr_t(mem_type_e mem_type): x11_attr_t(mem_type), shm_xdisplay {x11::OpenDisplay(nullptr)} { refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id; } ~shm_attr_t() override { while (!task_pool.cancel(refresh_task_id)); } capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); sleep_overshoot_logger.reset(); while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { std::this_thread::sleep_for(next_frame - now); sleep_overshoot_logger.first_point(next_frame); sleep_overshoot_logger.second_point_now_and_log(); } next_frame += delay; if (next_frame < now) { // some major slowdown happened; we couldn't keep up next_frame = now + delay; } std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: case platf::capture_e::interrupted: return status; case platf::capture_e::timeout: if (!push_captured_image_cb(std::move(img_out), false)) { return platf::capture_e::ok; } break; case platf::capture_e::ok: if (!push_captured_image_cb(std::move(img_out), true)) { return platf::capture_e::ok; } break; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; return status; } } return capture_e::ok; } capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { // The whole X server changed, so we must reinit everything if (xattr.width != env_width || xattr.height != env_height) { BOOST_LOG(warning) << "X dimensions changed in SHM mode, request reinit"sv; return capture_e::reinit; } else { 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); auto frame_timestamp = std::chrono::steady_clock::now(); xcb_img_t img_reply {xcb::shm_get_image_reply(xcb.get(), img_cookie, nullptr)}; if (!img_reply) { BOOST_LOG(error) << "Could not get image reply"sv; return capture_e::reinit; } if (!pull_free_image_cb(img_out)) { return platf::capture_e::interrupted; } std::copy_n((std::uint8_t *) data.data, frame_size(), img_out->data); img_out->frame_timestamp = frame_timestamp; if (cursor) { blend_cursor(shm_xdisplay.get(), *img_out, offset_x, offset_y); } return capture_e::ok; } } std::shared_ptr alloc_img() override { auto img = std::make_shared(); img->width = width; img->height = height; img->pixel_pitch = 4; img->row_pitch = img->pixel_pitch * width; img->data = new std::uint8_t[height * img->row_pitch]; return img; } int dummy_img(platf::img_t *img) override { return 0; } int init(const std::string &display_name, const ::video::config_t &config) { if (x11_attr_t::init(display_name, config)) { return 1; } shm_xdisplay.reset(x11::OpenDisplay(nullptr)); xcb.reset(xcb::connect(nullptr, nullptr)); if (xcb::connection_has_error(xcb.get())) { return -1; } if (!xcb::get_extension_data(xcb.get(), xcb::shm_id)->present) { BOOST_LOG(error) << "Missing SHM extension"sv; return -1; } auto iter = xcb::setup_roots_iterator(xcb::get_setup(xcb.get())); display = iter.data; seg = xcb::generate_id(xcb.get()); shm_id.id = shmget(IPC_PRIVATE, frame_size(), IPC_CREAT | 0777); if (shm_id.id == -1) { BOOST_LOG(error) << "shmget failed"sv; return -1; } xcb::shm_attach(xcb.get(), seg, shm_id.id, false); data.data = shmat(shm_id.id, nullptr, 0); if ((uintptr_t) data.data == -1) { BOOST_LOG(error) << "shmat failed"sv; return -1; } return 0; } std::uint32_t frame_size() { return width * height * 4; } }; std::shared_ptr x11_display(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) { BOOST_LOG(error) << "Could not initialize x11 display with the given hw device type"sv; return nullptr; } if (xcb::init_shm() || xcb::init() || x11::init() || x11::rr::init() || x11::fix::init()) { BOOST_LOG(error) << "Couldn't init x11 libraries"sv; return nullptr; } // Attempt to use shared memory X11 to avoid copying the frame auto shm_disp = std::make_shared(hwdevice_type); auto status = shm_disp->init(display_name, config); if (status > 0) { // x11_attr_t::init() failed, don't bother trying again. return nullptr; } if (status == 0) { return shm_disp; } // Fallback auto x11_disp = std::make_shared(hwdevice_type); if (x11_disp->init(display_name, config)) { return nullptr; } return x11_disp; } std::vector x11_display_names() { if (load_x11() || load_xcb()) { BOOST_LOG(error) << "Couldn't init x11 libraries"sv; return {}; } BOOST_LOG(info) << "Detecting displays"sv; x11::xdisplay_t xdisplay {x11::OpenDisplay(nullptr)}; if (!xdisplay) { return {}; } auto xwindow = DefaultRootWindow(xdisplay.get()); screen_res_t screenr {x11::rr::GetScreenResources(xdisplay.get(), xwindow)}; int output = screenr->noutput; int monitor = 0; for (int x = 0; x < output; ++x) { output_info_t out_info {x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x])}; if (out_info) { BOOST_LOG(info) << "Detected display: "sv << out_info->name << " (id: "sv << monitor << ")"sv << out_info->name << " connected: "sv << (out_info->connection == RR_Connected); ++monitor; } } std::vector names; names.reserve(monitor); for (auto x = 0; x < monitor; ++x) { names.emplace_back(std::to_string(x)); } return names; } void freeImage(XImage *p) { XDestroyImage(p); } void freeX(XFixesCursorImage *p) { x11::Free(p); } int load_xcb() { // This will be called once only static int xcb_status = xcb::init_shm() || xcb::init(); return xcb_status; } int load_x11() { // This will be called once only static int x11_status = window_system == window_system_e::NONE || x11::init() || x11::rr::init() || x11::fix::init(); return x11_status; } namespace x11 { std::optional cursor_t::make() { if (load_x11()) { return std::nullopt; } cursor_t cursor; cursor.ctx.reset((cursor_ctx_t::pointer) x11::OpenDisplay(nullptr)); return cursor; } void cursor_t::capture(egl::cursor_t &img) { auto display = (xdisplay_t::pointer) ctx.get(); xcursor_t xcursor = fix::GetCursorImage(display); if (img.serial != xcursor->cursor_serial) { auto buf_size = xcursor->width * xcursor->height * sizeof(int); if (img.buffer.size() < buf_size) { img.buffer.resize(buf_size); } std::transform(xcursor->pixels, xcursor->pixels + buf_size / 4, (int *) img.buffer.data(), [](long pixel) -> int { return pixel; }); } img.data = img.buffer.data(); img.width = img.src_w = xcursor->width; img.height = img.src_h = xcursor->height; img.x = xcursor->x - xcursor->xhot; img.y = xcursor->y - xcursor->yhot; img.pixel_pitch = 4; img.row_pitch = img.pixel_pitch * img.width; img.serial = xcursor->cursor_serial; } void cursor_t::blend(img_t &img, int offsetX, int offsetY) { blend_cursor((xdisplay_t::pointer) ctx.get(), img, offsetX, offsetY); } xdisplay_t make_display() { return OpenDisplay(nullptr); } void freeDisplay(_XDisplay *xdisplay) { CloseDisplay(xdisplay); } void freeCursorCtx(cursor_ctx_t::pointer ctx) { CloseDisplay((xdisplay_t::pointer) ctx); } } // namespace x11 } // namespace platf ================================================ FILE: src/platform/linux/x11grab.h ================================================ /** * @file src/platform/linux/x11grab.h * @brief Declarations for x11 capture. */ #pragma once // standard includes #include // local includes #include "src/platform/common.h" #include "src/utility.h" // X11 Display extern "C" struct _XDisplay; namespace egl { class cursor_t; } namespace platf::x11 { struct cursor_ctx_raw_t; void freeCursorCtx(cursor_ctx_raw_t *ctx); void freeDisplay(_XDisplay *xdisplay); using cursor_ctx_t = util::safe_ptr; using xdisplay_t = util::safe_ptr<_XDisplay, freeDisplay>; class cursor_t { public: static std::optional make(); void capture(egl::cursor_t &img); /** * Capture and blend the cursor into the image * * img <-- destination image * offsetX, offsetY <--- Top left corner of the virtual screen */ void blend(img_t &img, int offsetX, int offsetY); cursor_ctx_t ctx; }; xdisplay_t make_display(); } // namespace platf::x11 ================================================ FILE: src/platform/macos/av_audio.h ================================================ /** * @file src/platform/macos/av_audio.h * @brief Declarations for audio capture on macOS. */ #pragma once // platform includes #import // lib includes #include "third-party/TPCircularBuffer/TPCircularBuffer.h" #define kBufferLength 4096 @interface AVAudio: NSObject { @public TPCircularBuffer audioSampleBuffer; } @property (nonatomic, assign) AVCaptureSession *audioCaptureSession; @property (nonatomic, assign) AVCaptureConnection *audioConnection; @property (nonatomic, assign) NSCondition *samplesArrivedSignal; + (NSArray *)microphoneNames; + (AVCaptureDevice *)findMicrophone:(NSString *)name; - (int)setupMicrophone:(AVCaptureDevice *)device sampleRate:(UInt32)sampleRate frameSize:(UInt32)frameSize channels:(UInt8)channels; @end ================================================ FILE: src/platform/macos/av_audio.m ================================================ /** * @file src/platform/macos/av_audio.m * @brief Definitions for audio capture on macOS. */ // local includes #import "av_audio.h" @implementation AVAudio + (NSArray *)microphones { if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:((NSOperatingSystemVersion) {10, 15, 0})]) { // This will generate a warning about AVCaptureDeviceDiscoverySession being // unavailable before macOS 10.15, but we have a guard to prevent it from // being called on those earlier systems. // Unfortunately the supported way to silence this warning, using @available, // produces linker errors for __isPlatformVersionAtLeast, so we have to use // a different method. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability-new" AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInMicrophone, AVCaptureDeviceTypeExternalUnknown] mediaType:AVMediaTypeAudio position:AVCaptureDevicePositionUnspecified]; return discoverySession.devices; #pragma clang diagnostic pop } else { // We're intentionally using a deprecated API here specifically for versions // of macOS where it's not deprecated, so we can ignore any deprecation // warnings: #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio]; #pragma clang diagnostic pop } } + (NSArray *)microphoneNames { NSMutableArray *result = [[NSMutableArray alloc] init]; for (AVCaptureDevice *device in [AVAudio microphones]) { [result addObject:[device localizedName]]; } return result; } + (AVCaptureDevice *)findMicrophone:(NSString *)name { for (AVCaptureDevice *device in [AVAudio microphones]) { if ([[device localizedName] isEqualToString:name]) { return device; } } return nil; } - (void)dealloc { // make sure we don't process any further samples self.audioConnection = nil; // make sure nothing gets stuck on this signal [self.samplesArrivedSignal signal]; [self.samplesArrivedSignal release]; TPCircularBufferCleanup(&audioSampleBuffer); [super dealloc]; } - (int)setupMicrophone:(AVCaptureDevice *)device sampleRate:(UInt32)sampleRate frameSize:(UInt32)frameSize channels:(UInt8)channels { self.audioCaptureSession = [[AVCaptureSession alloc] init]; NSError *error; AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error]; if (audioInput == nil) { return -1; } if ([self.audioCaptureSession canAddInput:audioInput]) { [self.audioCaptureSession addInput:audioInput]; } else { [audioInput dealloc]; return -1; } AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc] init]; [audioOutput setAudioSettings:@{ (NSString *) AVFormatIDKey: [NSNumber numberWithUnsignedInt:kAudioFormatLinearPCM], (NSString *) AVSampleRateKey: [NSNumber numberWithUnsignedInt:sampleRate], (NSString *) AVNumberOfChannelsKey: [NSNumber numberWithUnsignedInt:channels], (NSString *) AVLinearPCMBitDepthKey: [NSNumber numberWithUnsignedInt:32], (NSString *) AVLinearPCMIsFloatKey: @YES, (NSString *) AVLinearPCMIsNonInterleaved: @NO }]; dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, QOS_CLASS_USER_INITIATED, DISPATCH_QUEUE_PRIORITY_HIGH); dispatch_queue_t recordingQueue = dispatch_queue_create("audioSamplingQueue", qos); [audioOutput setSampleBufferDelegate:self queue:recordingQueue]; if ([self.audioCaptureSession canAddOutput:audioOutput]) { [self.audioCaptureSession addOutput:audioOutput]; } else { [audioInput release]; [audioOutput release]; return -1; } self.audioConnection = [audioOutput connectionWithMediaType:AVMediaTypeAudio]; [self.audioCaptureSession startRunning]; [audioInput release]; [audioOutput release]; self.samplesArrivedSignal = [[NSCondition alloc] init]; TPCircularBufferInit(&self->audioSampleBuffer, kBufferLength * channels); return 0; } - (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { if (connection == self.audioConnection) { AudioBufferList audioBufferList; CMBlockBufferRef blockBuffer; CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer, NULL, &audioBufferList, sizeof(audioBufferList), NULL, NULL, 0, &blockBuffer); // NSAssert(audioBufferList.mNumberBuffers == 1, @"Expected interleaved PCM format but buffer contained %u streams", audioBufferList.mNumberBuffers); // this is safe, because an interleaved PCM stream has exactly one buffer, // and we don't want to do sanity checks in a performance critical exec path AudioBuffer audioBuffer = audioBufferList.mBuffers[0]; TPCircularBufferProduceBytes(&self->audioSampleBuffer, audioBuffer.mData, audioBuffer.mDataByteSize); [self.samplesArrivedSignal signal]; } } @end ================================================ FILE: src/platform/macos/av_img_t.h ================================================ /** * @file src/platform/macos/av_img_t.h * @brief Declarations for AV image types on macOS. */ #pragma once // platform includes #include #include // local includes #include "src/platform/common.h" namespace platf { struct av_sample_buf_t { CMSampleBufferRef buf; explicit av_sample_buf_t(CMSampleBufferRef buf): buf((CMSampleBufferRef) CFRetain(buf)) { } ~av_sample_buf_t() { if (buf != nullptr) { CFRelease(buf); } } }; struct av_pixel_buf_t { CVPixelBufferRef buf; // Constructor explicit av_pixel_buf_t(CMSampleBufferRef sb): buf( CMSampleBufferGetImageBuffer(sb) ) { CVPixelBufferLockBaseAddress(buf, kCVPixelBufferLock_ReadOnly); } [[nodiscard]] uint8_t *data() const { return static_cast(CVPixelBufferGetBaseAddress(buf)); } // Destructor ~av_pixel_buf_t() { if (buf != nullptr) { CVPixelBufferUnlockBaseAddress(buf, kCVPixelBufferLock_ReadOnly); } } }; struct av_img_t: img_t { std::shared_ptr sample_buffer; std::shared_ptr pixel_buffer; }; struct temp_retain_av_img_t { std::shared_ptr sample_buffer; std::shared_ptr pixel_buffer; uint8_t *data; temp_retain_av_img_t( std::shared_ptr sb, std::shared_ptr pb, uint8_t *dt ): sample_buffer(std::move(sb)), pixel_buffer(std::move(pb)), data(dt) { } }; } // namespace platf ================================================ FILE: src/platform/macos/av_video.h ================================================ /** * @file src/platform/macos/av_video.h * @brief Declarations for video capture on macOS. */ #pragma once // platform includes #import #import struct CaptureSession { AVCaptureVideoDataOutput *output; NSCondition *captureStopped; }; @interface AVVideo: NSObject #define kMaxDisplays 32 @property (nonatomic, assign) CGDirectDisplayID displayID; @property (nonatomic, assign) CMTime minFrameDuration; @property (nonatomic, assign) OSType pixelFormat; @property (nonatomic, assign) int frameWidth; @property (nonatomic, assign) int frameHeight; typedef bool (^FrameCallbackBlock)(CMSampleBufferRef); @property (nonatomic, assign) AVCaptureSession *session; @property (nonatomic, assign) NSMapTable *videoOutputs; @property (nonatomic, assign) NSMapTable *captureCallbacks; @property (nonatomic, assign) NSMapTable *captureSignals; + (NSArray *)displayNames; + (NSString *)getDisplayName:(CGDirectDisplayID)displayID; - (id)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate; - (void)setFrameWidth:(int)frameWidth frameHeight:(int)frameHeight; - (dispatch_semaphore_t)capture:(FrameCallbackBlock)frameCallback; @end ================================================ FILE: src/platform/macos/av_video.m ================================================ /** * @file src/platform/macos/av_video.m * @brief Definitions for video capture on macOS. */ // local includes #import "av_video.h" @implementation AVVideo // XXX: Currently, this function only returns the screen IDs as names, // which is not very helpful to the user. The API to retrieve names // was deprecated with 10.9+. // However, there is a solution with little external code that can be used: // https://stackoverflow.com/questions/20025868/cgdisplayioserviceport-is-deprecated-in-os-x-10-9-how-to-replace + (NSArray *)displayNames { CGDirectDisplayID displays[kMaxDisplays]; uint32_t count; if (CGGetActiveDisplayList(kMaxDisplays, displays, &count) != kCGErrorSuccess) { return [NSArray array]; } NSMutableArray *result = [NSMutableArray array]; for (uint32_t i = 0; i < count; i++) { [result addObject:@{ @"id": [NSNumber numberWithUnsignedInt:displays[i]], @"name": [NSString stringWithFormat:@"%d", displays[i]], @"displayName": [self getDisplayName:displays[i]], }]; } return [NSArray arrayWithArray:result]; } + (NSString *)getDisplayName:(CGDirectDisplayID)displayID { for (NSScreen *screen in [NSScreen screens]) { if ([screen.deviceDescription[@"NSScreenNumber"] isEqualToNumber:[NSNumber numberWithUnsignedInt:displayID]]) { return screen.localizedName; } } return nil; } - (id)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate { self = [super init]; CGDisplayModeRef mode = CGDisplayCopyDisplayMode(displayID); self.displayID = displayID; self.pixelFormat = kCVPixelFormatType_32BGRA; self.frameWidth = (int) CGDisplayModeGetPixelWidth(mode); self.frameHeight = (int) CGDisplayModeGetPixelHeight(mode); self.minFrameDuration = CMTimeMake(1, frameRate); self.session = [[AVCaptureSession alloc] init]; self.videoOutputs = [[NSMapTable alloc] init]; self.captureCallbacks = [[NSMapTable alloc] init]; self.captureSignals = [[NSMapTable alloc] init]; CFRelease(mode); AVCaptureScreenInput *screenInput = [[AVCaptureScreenInput alloc] initWithDisplayID:self.displayID]; [screenInput setMinFrameDuration:self.minFrameDuration]; if ([self.session canAddInput:screenInput]) { [self.session addInput:screenInput]; } else { [screenInput release]; return nil; } [self.session startRunning]; return self; } - (void)dealloc { [self.videoOutputs release]; [self.captureCallbacks release]; [self.captureSignals release]; [self.session stopRunning]; [super dealloc]; } - (void)setFrameWidth:(int)frameWidth frameHeight:(int)frameHeight { self.frameWidth = frameWidth; self.frameHeight = frameHeight; } - (dispatch_semaphore_t)capture:(FrameCallbackBlock)frameCallback { @synchronized(self) { AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init]; [videoOutput setVideoSettings:@{ (NSString *) kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithUnsignedInt:self.pixelFormat], (NSString *) kCVPixelBufferWidthKey: [NSNumber numberWithInt:self.frameWidth], (NSString *) kCVPixelBufferHeightKey: [NSNumber numberWithInt:self.frameHeight], (NSString *) AVVideoScalingModeKey: AVVideoScalingModeResizeAspect, }]; dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, DISPATCH_QUEUE_PRIORITY_HIGH); dispatch_queue_t recordingQueue = dispatch_queue_create("videoCaptureQueue", qos); [videoOutput setSampleBufferDelegate:self queue:recordingQueue]; [self.session stopRunning]; if ([self.session canAddOutput:videoOutput]) { [self.session addOutput:videoOutput]; } else { [videoOutput release]; return nil; } AVCaptureConnection *videoConnection = [videoOutput connectionWithMediaType:AVMediaTypeVideo]; dispatch_semaphore_t signal = dispatch_semaphore_create(0); [self.videoOutputs setObject:videoOutput forKey:videoConnection]; [self.captureCallbacks setObject:frameCallback forKey:videoConnection]; [self.captureSignals setObject:signal forKey:videoConnection]; [self.session startRunning]; return signal; } } - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { FrameCallbackBlock callback = [self.captureCallbacks objectForKey:connection]; if (callback != nil) { if (!callback(sampleBuffer)) { @synchronized(self) { [self.session stopRunning]; [self.captureCallbacks removeObjectForKey:connection]; [self.session removeOutput:[self.videoOutputs objectForKey:connection]]; [self.videoOutputs removeObjectForKey:connection]; dispatch_semaphore_signal([self.captureSignals objectForKey:connection]); [self.captureSignals removeObjectForKey:connection]; [self.session startRunning]; } } } } @end ================================================ FILE: src/platform/macos/display.mm ================================================ /** * @file src/platform/macos/display.mm * @brief Definitions for display capture on macOS. */ // local includes #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/platform/macos/av_img_t.h" #include "src/platform/macos/av_video.h" #include "src/platform/macos/misc.h" #include "src/platform/macos/nv12_zero_device.h" // Avoid conflict between AVFoundation and libavutil both defining AVMediaType #define AVMediaType AVMediaType_FFmpeg #include "src/video.h" #undef AVMediaType namespace fs = std::filesystem; namespace platf { using namespace std::literals; struct av_display_t: public display_t { AVVideo *av_capture {}; CGDirectDisplayID display_id {}; ~av_display_t() override { [av_capture release]; } capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto signal = [av_capture capture:^(CMSampleBufferRef sampleBuffer) { auto new_sample_buffer = std::make_shared(sampleBuffer); auto new_pixel_buffer = std::make_shared(new_sample_buffer->buf); std::shared_ptr img_out; if (!pull_free_image_cb(img_out)) { // got interrupt signal // returning false here stops capture backend return false; } auto av_img = std::static_pointer_cast(img_out); auto old_data_retainer = std::make_shared( av_img->sample_buffer, av_img->pixel_buffer, img_out->data ); av_img->sample_buffer = new_sample_buffer; av_img->pixel_buffer = new_pixel_buffer; img_out->data = new_pixel_buffer->data(); img_out->width = (int) CVPixelBufferGetWidth(new_pixel_buffer->buf); img_out->height = (int) CVPixelBufferGetHeight(new_pixel_buffer->buf); img_out->row_pitch = (int) CVPixelBufferGetBytesPerRow(new_pixel_buffer->buf); img_out->pixel_pitch = img_out->row_pitch / img_out->width; old_data_retainer = nullptr; if (!push_captured_image_cb(std::move(img_out), true)) { // got interrupt signal // returning false here stops capture backend return false; } return true; }]; // FIXME: We should time out if an image isn't returned for a while dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER); return capture_e::ok; } std::shared_ptr alloc_img() override { return std::make_shared(); } std::unique_ptr make_avcodec_encode_device(pix_fmt_e pix_fmt) override { if (pix_fmt == pix_fmt_e::yuv420p) { av_capture.pixelFormat = kCVPixelFormatType_32BGRA; return std::make_unique(); } else if (pix_fmt == pix_fmt_e::nv12 || pix_fmt == pix_fmt_e::p010) { auto device = std::make_unique(); device->init(static_cast(av_capture), pix_fmt, setResolution, setPixelFormat); return device; } else { BOOST_LOG(error) << "Unsupported Pixel Format."sv; return nullptr; } } int dummy_img(img_t *img) override { if (!platf::is_screen_capture_allowed()) { // If we don't have the screen capture permission, this function will hang // indefinitely without doing anything useful. Exit instead to avoid this. // A non-zero return value indicates failure to the calling function. return 1; } auto signal = [av_capture capture:^(CMSampleBufferRef sampleBuffer) { auto new_sample_buffer = std::make_shared(sampleBuffer); auto new_pixel_buffer = std::make_shared(new_sample_buffer->buf); auto av_img = (av_img_t *) img; auto old_data_retainer = std::make_shared( av_img->sample_buffer, av_img->pixel_buffer, img->data ); av_img->sample_buffer = new_sample_buffer; av_img->pixel_buffer = new_pixel_buffer; img->data = new_pixel_buffer->data(); img->width = (int) CVPixelBufferGetWidth(new_pixel_buffer->buf); img->height = (int) CVPixelBufferGetHeight(new_pixel_buffer->buf); img->row_pitch = (int) CVPixelBufferGetBytesPerRow(new_pixel_buffer->buf); img->pixel_pitch = img->row_pitch / img->width; old_data_retainer = nullptr; // returning false here stops capture backend return false; }]; dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER); return 0; } /** * A bridge from the pure C++ code of the hwdevice_t class to the pure Objective C code. * * display --> an opaque pointer to an object of this class * width --> the intended capture width * height --> the intended capture height */ static void setResolution(void *display, int width, int height) { [static_cast(display) setFrameWidth:width frameHeight:height]; } static void setPixelFormat(void *display, OSType pixelFormat) { static_cast(display).pixelFormat = pixelFormat; } }; std::shared_ptr display(platf::mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::videotoolbox) { BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv; return nullptr; } auto display = std::make_shared(); // Default to main display display->display_id = CGMainDisplayID(); // Print all displays available with it's name and id auto display_array = [AVVideo displayNames]; BOOST_LOG(info) << "Detecting displays"sv; for (NSDictionary *item in display_array) { NSNumber *display_id = item[@"id"]; // We need show display's product name and corresponding display number given by user NSString *name = item[@"displayName"]; // We are using CGGetActiveDisplayList that only returns active displays so hardcoded connected value in log to true BOOST_LOG(info) << "Detected display: "sv << name.UTF8String << " (id: "sv << [NSString stringWithFormat:@"%@", display_id].UTF8String << ") connected: true"sv; if (!display_name.empty() && std::atoi(display_name.c_str()) == [display_id unsignedIntValue]) { display->display_id = [display_id unsignedIntValue]; } } BOOST_LOG(info) << "Configuring selected display ("sv << display->display_id << ") to stream"sv; display->av_capture = [[AVVideo alloc] initWithDisplay:display->display_id frameRate:config.framerate]; if (!display->av_capture) { BOOST_LOG(error) << "Video setup failed."sv; return nullptr; } display->width = display->av_capture.frameWidth; display->height = display->av_capture.frameHeight; // We also need set env_width and env_height for absolute mouse coordinates display->env_width = display->width; display->env_height = display->height; return display; } std::vector display_names(mem_type_e hwdevice_type) { __block std::vector display_names; auto display_array = [AVVideo displayNames]; display_names.reserve([display_array count]); [display_array enumerateObjectsUsingBlock:^(NSDictionary *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { NSString *name = obj[@"name"]; display_names.emplace_back(name.UTF8String); }]; return display_names; } /** * @brief Returns if GPUs/drivers have changed since the last call to this function. * @return `true` if a change has occurred or if it is unknown whether a change occurred. */ bool needs_encoder_reenumeration() { // We don't track GPU state, so we will always reenumerate. Fortunately, it is fast on macOS. return true; } } // namespace platf ================================================ FILE: src/platform/macos/input.cpp ================================================ /** * @file src/platform/macos/input.cpp * @brief Definitions for macOS input handling. */ // standard includes #include #include #include // platform includes #include #import #include #include // local includes #include "src/display_device.h" #include "src/input.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" /** * @brief Delay for a double click, in milliseconds. * @todo Make this configurable. */ constexpr std::chrono::milliseconds MULTICLICK_DELAY_MS(500); namespace platf { using namespace std::literals; struct macos_input_t { public: CGDirectDisplayID display {}; CGFloat displayScaling {}; CGEventSourceRef source {}; // keyboard related stuff CGEventRef kb_event {}; CGEventFlags kb_flags {}; // mouse related stuff CGEventRef mouse_event {}; // mouse event source bool mouse_down[3] {}; // mouse button status std::chrono::steady_clock::steady_clock::time_point last_mouse_event[3][2]; // timestamp of last mouse events }; // A struct to hold a Windows keycode to Mac virtual keycode mapping. struct KeyCodeMap { int win_keycode; int mac_keycode; }; // Customized less operator for using std::lower_bound() on a KeyCodeMap array. bool operator<(const KeyCodeMap &a, const KeyCodeMap &b) { return a.win_keycode < b.win_keycode; } // clang-format off const KeyCodeMap kKeyCodesMap[] = { { 0x08 /* VKEY_BACK */, kVK_Delete }, { 0x09 /* VKEY_TAB */, kVK_Tab }, { 0x0A /* VKEY_BACKTAB */, 0x21E4 }, { 0x0C /* VKEY_CLEAR */, kVK_ANSI_KeypadClear }, { 0x0D /* VKEY_RETURN */, kVK_Return }, { 0x10 /* VKEY_SHIFT */, kVK_Shift }, { 0x11 /* VKEY_CONTROL */, kVK_Control }, { 0x12 /* VKEY_MENU */, kVK_Option }, { 0x13 /* VKEY_PAUSE */, -1 }, { 0x14 /* VKEY_CAPITAL */, kVK_CapsLock }, { 0x15 /* VKEY_KANA */, kVK_JIS_Kana }, { 0x15 /* VKEY_HANGUL */, -1 }, { 0x17 /* VKEY_JUNJA */, -1 }, { 0x18 /* VKEY_FINAL */, -1 }, { 0x19 /* VKEY_HANJA */, -1 }, { 0x19 /* VKEY_KANJI */, -1 }, { 0x1B /* VKEY_ESCAPE */, kVK_Escape }, { 0x1C /* VKEY_CONVERT */, -1 }, { 0x1D /* VKEY_NONCONVERT */, -1 }, { 0x1E /* VKEY_ACCEPT */, -1 }, { 0x1F /* VKEY_MODECHANGE */, -1 }, { 0x20 /* VKEY_SPACE */, kVK_Space }, { 0x21 /* VKEY_PRIOR */, kVK_PageUp }, { 0x22 /* VKEY_NEXT */, kVK_PageDown }, { 0x23 /* VKEY_END */, kVK_End }, { 0x24 /* VKEY_HOME */, kVK_Home }, { 0x25 /* VKEY_LEFT */, kVK_LeftArrow }, { 0x26 /* VKEY_UP */, kVK_UpArrow }, { 0x27 /* VKEY_RIGHT */, kVK_RightArrow }, { 0x28 /* VKEY_DOWN */, kVK_DownArrow }, { 0x29 /* VKEY_SELECT */, -1 }, { 0x2A /* VKEY_PRINT */, -1 }, { 0x2B /* VKEY_EXECUTE */, -1 }, { 0x2C /* VKEY_SNAPSHOT */, -1 }, { 0x2D /* VKEY_INSERT */, kVK_Help }, { 0x2E /* VKEY_DELETE */, kVK_ForwardDelete }, { 0x2F /* VKEY_HELP */, kVK_Help }, { 0x30 /* VKEY_0 */, kVK_ANSI_0 }, { 0x31 /* VKEY_1 */, kVK_ANSI_1 }, { 0x32 /* VKEY_2 */, kVK_ANSI_2 }, { 0x33 /* VKEY_3 */, kVK_ANSI_3 }, { 0x34 /* VKEY_4 */, kVK_ANSI_4 }, { 0x35 /* VKEY_5 */, kVK_ANSI_5 }, { 0x36 /* VKEY_6 */, kVK_ANSI_6 }, { 0x37 /* VKEY_7 */, kVK_ANSI_7 }, { 0x38 /* VKEY_8 */, kVK_ANSI_8 }, { 0x39 /* VKEY_9 */, kVK_ANSI_9 }, { 0x41 /* VKEY_A */, kVK_ANSI_A }, { 0x42 /* VKEY_B */, kVK_ANSI_B }, { 0x43 /* VKEY_C */, kVK_ANSI_C }, { 0x44 /* VKEY_D */, kVK_ANSI_D }, { 0x45 /* VKEY_E */, kVK_ANSI_E }, { 0x46 /* VKEY_F */, kVK_ANSI_F }, { 0x47 /* VKEY_G */, kVK_ANSI_G }, { 0x48 /* VKEY_H */, kVK_ANSI_H }, { 0x49 /* VKEY_I */, kVK_ANSI_I }, { 0x4A /* VKEY_J */, kVK_ANSI_J }, { 0x4B /* VKEY_K */, kVK_ANSI_K }, { 0x4C /* VKEY_L */, kVK_ANSI_L }, { 0x4D /* VKEY_M */, kVK_ANSI_M }, { 0x4E /* VKEY_N */, kVK_ANSI_N }, { 0x4F /* VKEY_O */, kVK_ANSI_O }, { 0x50 /* VKEY_P */, kVK_ANSI_P }, { 0x51 /* VKEY_Q */, kVK_ANSI_Q }, { 0x52 /* VKEY_R */, kVK_ANSI_R }, { 0x53 /* VKEY_S */, kVK_ANSI_S }, { 0x54 /* VKEY_T */, kVK_ANSI_T }, { 0x55 /* VKEY_U */, kVK_ANSI_U }, { 0x56 /* VKEY_V */, kVK_ANSI_V }, { 0x57 /* VKEY_W */, kVK_ANSI_W }, { 0x58 /* VKEY_X */, kVK_ANSI_X }, { 0x59 /* VKEY_Y */, kVK_ANSI_Y }, { 0x5A /* VKEY_Z */, kVK_ANSI_Z }, { 0x5B /* VKEY_LWIN */, kVK_Command }, { 0x5C /* VKEY_RWIN */, kVK_RightCommand }, { 0x5D /* VKEY_APPS */, kVK_RightCommand }, { 0x5F /* VKEY_SLEEP */, -1 }, { 0x60 /* VKEY_NUMPAD0 */, kVK_ANSI_Keypad0 }, { 0x61 /* VKEY_NUMPAD1 */, kVK_ANSI_Keypad1 }, { 0x62 /* VKEY_NUMPAD2 */, kVK_ANSI_Keypad2 }, { 0x63 /* VKEY_NUMPAD3 */, kVK_ANSI_Keypad3 }, { 0x64 /* VKEY_NUMPAD4 */, kVK_ANSI_Keypad4 }, { 0x65 /* VKEY_NUMPAD5 */, kVK_ANSI_Keypad5 }, { 0x66 /* VKEY_NUMPAD6 */, kVK_ANSI_Keypad6 }, { 0x67 /* VKEY_NUMPAD7 */, kVK_ANSI_Keypad7 }, { 0x68 /* VKEY_NUMPAD8 */, kVK_ANSI_Keypad8 }, { 0x69 /* VKEY_NUMPAD9 */, kVK_ANSI_Keypad9 }, { 0x6A /* VKEY_MULTIPLY */, kVK_ANSI_KeypadMultiply }, { 0x6B /* VKEY_ADD */, kVK_ANSI_KeypadPlus }, { 0x6C /* VKEY_SEPARATOR */, -1 }, { 0x6D /* VKEY_SUBTRACT */, kVK_ANSI_KeypadMinus }, { 0x6E /* VKEY_DECIMAL */, kVK_ANSI_KeypadDecimal }, { 0x6F /* VKEY_DIVIDE */, kVK_ANSI_KeypadDivide }, { 0x70 /* VKEY_F1 */, kVK_F1 }, { 0x71 /* VKEY_F2 */, kVK_F2 }, { 0x72 /* VKEY_F3 */, kVK_F3 }, { 0x73 /* VKEY_F4 */, kVK_F4 }, { 0x74 /* VKEY_F5 */, kVK_F5 }, { 0x75 /* VKEY_F6 */, kVK_F6 }, { 0x76 /* VKEY_F7 */, kVK_F7 }, { 0x77 /* VKEY_F8 */, kVK_F8 }, { 0x78 /* VKEY_F9 */, kVK_F9 }, { 0x79 /* VKEY_F10 */, kVK_F10 }, { 0x7A /* VKEY_F11 */, kVK_F11 }, { 0x7B /* VKEY_F12 */, kVK_F12 }, { 0x7C /* VKEY_F13 */, kVK_F13 }, { 0x7D /* VKEY_F14 */, kVK_F14 }, { 0x7E /* VKEY_F15 */, kVK_F15 }, { 0x7F /* VKEY_F16 */, kVK_F16 }, { 0x80 /* VKEY_F17 */, kVK_F17 }, { 0x81 /* VKEY_F18 */, kVK_F18 }, { 0x82 /* VKEY_F19 */, kVK_F19 }, { 0x83 /* VKEY_F20 */, kVK_F20 }, { 0x84 /* VKEY_F21 */, -1 }, { 0x85 /* VKEY_F22 */, -1 }, { 0x86 /* VKEY_F23 */, -1 }, { 0x87 /* VKEY_F24 */, -1 }, { 0x90 /* VKEY_NUMLOCK */, -1 }, { 0x91 /* VKEY_SCROLL */, -1 }, { 0xA0 /* VKEY_LSHIFT */, kVK_Shift }, { 0xA1 /* VKEY_RSHIFT */, kVK_RightShift }, { 0xA2 /* VKEY_LCONTROL */, kVK_Control }, { 0xA3 /* VKEY_RCONTROL */, kVK_RightControl }, { 0xA4 /* VKEY_LMENU */, kVK_Option }, { 0xA5 /* VKEY_RMENU */, kVK_RightOption }, { 0xA6 /* VKEY_BROWSER_BACK */, -1 }, { 0xA7 /* VKEY_BROWSER_FORWARD */, -1 }, { 0xA8 /* VKEY_BROWSER_REFRESH */, -1 }, { 0xA9 /* VKEY_BROWSER_STOP */, -1 }, { 0xAA /* VKEY_BROWSER_SEARCH */, -1 }, { 0xAB /* VKEY_BROWSER_FAVORITES */, -1 }, { 0xAC /* VKEY_BROWSER_HOME */, -1 }, { 0xAD /* VKEY_VOLUME_MUTE */, -1 }, { 0xAE /* VKEY_VOLUME_DOWN */, -1 }, { 0xAF /* VKEY_VOLUME_UP */, -1 }, { 0xB0 /* VKEY_MEDIA_NEXT_TRACK */, -1 }, { 0xB1 /* VKEY_MEDIA_PREV_TRACK */, -1 }, { 0xB2 /* VKEY_MEDIA_STOP */, -1 }, { 0xB3 /* VKEY_MEDIA_PLAY_PAUSE */, -1 }, { 0xB4 /* VKEY_MEDIA_LAUNCH_MAIL */, -1 }, { 0xB5 /* VKEY_MEDIA_LAUNCH_MEDIA_SELECT */, -1 }, { 0xB6 /* VKEY_MEDIA_LAUNCH_APP1 */, -1 }, { 0xB7 /* VKEY_MEDIA_LAUNCH_APP2 */, -1 }, { 0xBA /* VKEY_OEM_1 */, kVK_ANSI_Semicolon }, { 0xBB /* VKEY_OEM_PLUS */, kVK_ANSI_Equal }, { 0xBC /* VKEY_OEM_COMMA */, kVK_ANSI_Comma }, { 0xBD /* VKEY_OEM_MINUS */, kVK_ANSI_Minus }, { 0xBE /* VKEY_OEM_PERIOD */, kVK_ANSI_Period }, { 0xBF /* VKEY_OEM_2 */, kVK_ANSI_Slash }, { 0xC0 /* VKEY_OEM_3 */, kVK_ANSI_Grave }, { 0xDB /* VKEY_OEM_4 */, kVK_ANSI_LeftBracket }, { 0xDC /* VKEY_OEM_5 */, kVK_ANSI_Backslash }, { 0xDD /* VKEY_OEM_6 */, kVK_ANSI_RightBracket }, { 0xDE /* VKEY_OEM_7 */, kVK_ANSI_Quote }, { 0xDF /* VKEY_OEM_8 */, -1 }, { 0xE2 /* VKEY_OEM_102 */, -1 }, { 0xE5 /* VKEY_PROCESSKEY */, -1 }, { 0xE7 /* VKEY_PACKET */, -1 }, { 0xF6 /* VKEY_ATTN */, -1 }, { 0xF7 /* VKEY_CRSEL */, -1 }, { 0xF8 /* VKEY_EXSEL */, -1 }, { 0xF9 /* VKEY_EREOF */, -1 }, { 0xFA /* VKEY_PLAY */, -1 }, { 0xFB /* VKEY_ZOOM */, -1 }, { 0xFC /* VKEY_NONAME */, -1 }, { 0xFD /* VKEY_PA1 */, -1 }, { 0xFE /* VKEY_OEM_CLEAR */, kVK_ANSI_KeypadClear } }; // clang-format on int keysym(int keycode) { KeyCodeMap key_map {}; key_map.win_keycode = keycode; const KeyCodeMap *temp_map = std::lower_bound( kKeyCodesMap, kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]), key_map ); if (temp_map >= kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]) || temp_map->win_keycode != keycode || temp_map->mac_keycode == -1) { return -1; } return temp_map->mac_keycode; } void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) { auto key = keysym(modcode); BOOST_LOG(debug) << "got keycode: 0x"sv << std::hex << modcode << ", translated to: 0x" << std::hex << key << ", release:" << release; if (key < 0) { return; } auto macos_input = ((macos_input_t *) input.get()); auto event = macos_input->kb_event; if (key == kVK_Shift || key == kVK_RightShift || key == kVK_Command || key == kVK_RightCommand || key == kVK_Option || key == kVK_RightOption || key == kVK_Control || key == kVK_RightControl) { CGEventFlags mask; switch (key) { case kVK_Shift: case kVK_RightShift: mask = kCGEventFlagMaskShift; break; case kVK_Command: case kVK_RightCommand: mask = kCGEventFlagMaskCommand; break; case kVK_Option: case kVK_RightOption: mask = kCGEventFlagMaskAlternate; break; case kVK_Control: case kVK_RightControl: mask = kCGEventFlagMaskControl; break; } macos_input->kb_flags = release ? macos_input->kb_flags & ~mask : macos_input->kb_flags | mask; CGEventSetType(event, kCGEventFlagsChanged); CGEventSetFlags(event, macos_input->kb_flags); } else { CGEventSetIntegerValueField(event, kCGKeyboardEventKeycode, key); CGEventSetType(event, release ? kCGEventKeyUp : kCGEventKeyDown); } CGEventPost(kCGHIDEventTap, event); } void unicode(input_t &input, char *utf8, int size) { BOOST_LOG(info) << "unicode: Unicode input not yet implemented for MacOS."sv; } int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { BOOST_LOG(info) << "alloc_gamepad: Gamepad not yet implemented for MacOS."sv; return -1; } void free_gamepad(input_t &input, int nr) { BOOST_LOG(info) << "free_gamepad: Gamepad not yet implemented for MacOS."sv; } void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { BOOST_LOG(info) << "gamepad: Gamepad not yet implemented for MacOS."sv; } // returns current mouse location: util::point_t get_mouse_loc(input_t &input) { // Creating a new event every time to avoid any reuse risk const auto macos_input = static_cast(input.get()); const auto snapshot_event = CGEventCreate(macos_input->source); const auto current = CGEventGetLocation(snapshot_event); CFRelease(snapshot_event); return util::point_t { current.x, current.y }; } void post_mouse( input_t &input, const CGMouseButton button, const CGEventType type, const util::point_t raw_location, const util::point_t previous_location, const int click_count ) { BOOST_LOG(debug) << "mouse_event: "sv << button << ", type: "sv << type << ", location:"sv << raw_location.x << ":"sv << raw_location.y << " click_count: "sv << click_count; const auto macos_input = static_cast(input.get()); const auto display = macos_input->display; const auto event = macos_input->mouse_event; // get display bounds for current display const CGRect display_bounds = CGDisplayBounds(display); // limit mouse to current display bounds const auto location = CGPoint { std::clamp(raw_location.x, display_bounds.origin.x, display_bounds.origin.x + display_bounds.size.width - 1), std::clamp(raw_location.y, display_bounds.origin.y, display_bounds.origin.y + display_bounds.size.height - 1) }; CGEventSetType(event, type); CGEventSetLocation(event, location); CGEventSetIntegerValueField(event, kCGMouseEventButtonNumber, button); CGEventSetIntegerValueField(event, kCGMouseEventClickState, click_count); // Include deltas so some 3D applications can consume changes (game cameras, etc) const double deltaX = raw_location.x - previous_location.x; const double deltaY = raw_location.y - previous_location.y; CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX); CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY); CGEventPost(kCGHIDEventTap, event); // For why this is here, see: // https://stackoverflow.com/questions/15194409/simulated-mouseevent-not-working-properly-osx CGWarpMouseCursorPosition(location); } inline CGEventType event_type_mouse(input_t &input) { const auto macos_input = static_cast(input.get()); if (macos_input->mouse_down[0]) { return kCGEventLeftMouseDragged; } if (macos_input->mouse_down[1]) { return kCGEventOtherMouseDragged; } if (macos_input->mouse_down[2]) { return kCGEventRightMouseDragged; } return kCGEventMouseMoved; } void move_mouse( input_t &input, const int deltaX, const int deltaY ) { const auto current = get_mouse_loc(input); const auto location = util::point_t {current.x + deltaX, current.y + deltaY}; post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, current, 0); } void abs_mouse( input_t &input, const touch_port_t &touch_port, const float x, const float y ) { const auto macos_input = static_cast(input.get()); const auto scaling = macos_input->displayScaling; const auto display = macos_input->display; auto location = util::point_t {x * scaling, y * scaling}; CGRect display_bounds = CGDisplayBounds(display); // in order to get the correct mouse location for capturing display , we need to add the display bounds to the location location.x += display_bounds.origin.x; location.y += display_bounds.origin.y; post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, get_mouse_loc(input), 0); } void button_mouse(input_t &input, const int button, const bool release) { CGMouseButton mac_button; CGEventType event; const auto macos_input = static_cast(input.get()); switch (button) { case 1: mac_button = kCGMouseButtonLeft; event = release ? kCGEventLeftMouseUp : kCGEventLeftMouseDown; break; case 2: mac_button = kCGMouseButtonCenter; event = release ? kCGEventOtherMouseUp : kCGEventOtherMouseDown; break; case 3: mac_button = kCGMouseButtonRight; event = release ? kCGEventRightMouseUp : kCGEventRightMouseDown; break; default: BOOST_LOG(warning) << "Unsupported mouse button for MacOS: "sv << button; return; } macos_input->mouse_down[mac_button] = !release; // if the last mouse down was less than MULTICLICK_DELAY_MS, we send a double click event const auto now = std::chrono::steady_clock::now(); const auto mouse_position = get_mouse_loc(input); if (now < macos_input->last_mouse_event[mac_button][release] + MULTICLICK_DELAY_MS) { post_mouse(input, mac_button, event, mouse_position, mouse_position, 2); } else { post_mouse(input, mac_button, event, mouse_position, mouse_position, 1); } macos_input->last_mouse_event[mac_button][release] = now; } void scroll(input_t &input, const int high_res_distance) { CGEventRef upEvent = CGEventCreateScrollWheelEvent( nullptr, kCGScrollEventUnitLine, 2, high_res_distance > 0 ? 1 : -1, high_res_distance ); CGEventPost(kCGHIDEventTap, upEvent); CFRelease(upEvent); } void hscroll(input_t &input, int high_res_distance) { // Unimplemented } /** * @brief Allocates a context to store per-client input data. * @param input The global input context. * @return A unique pointer to a per-client input data context. */ std::unique_ptr allocate_client_input_context(input_t &input) { // Unused return nullptr; } /** * @brief Sends a touch event to the OS. * @param input The client-specific input context. * @param touch_port The current viewport for translating to screen coordinates. * @param touch The touch event. */ void touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { // Unimplemented feature - platform_caps::pen_touch } /** * @brief Sends a pen event to the OS. * @param input The client-specific input context. * @param touch_port The current viewport for translating to screen coordinates. * @param pen The pen event. */ void pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { // Unimplemented feature - platform_caps::pen_touch } /** * @brief Sends a gamepad touch event to the OS. * @param input The global input context. * @param touch The touch event. */ void gamepad_touch(input_t &input, const gamepad_touch_t &touch) { // Unimplemented feature - platform_caps::controller_touch } /** * @brief Sends a gamepad motion event to the OS. * @param input The global input context. * @param motion The motion event. */ void gamepad_motion(input_t &input, const gamepad_motion_t &motion) { // Unimplemented } /** * @brief Sends a gamepad battery event to the OS. * @param input The global input context. * @param battery The battery event. */ void gamepad_battery(input_t &input, const gamepad_battery_t &battery) { // Unimplemented } input_t input() { input_t result {new macos_input_t()}; const auto macos_input = static_cast(result.get()); // Default to main display macos_input->display = CGMainDisplayID(); auto output_name = display_device::map_output_name(config::video.output_name); // If output_name is set, try to find the display with that display id if (!output_name.empty()) { uint32_t max_display = 32; uint32_t display_count; CGDirectDisplayID displays[max_display]; if (CGGetActiveDisplayList(max_display, displays, &display_count) != kCGErrorSuccess) { BOOST_LOG(error) << "Unable to get active display list , error: "sv << std::endl; } else { for (int i = 0; i < display_count; i++) { CGDirectDisplayID display_id = displays[i]; if (display_id == std::atoi(output_name.c_str())) { macos_input->display = display_id; } } } } // Input coordinates are based on the virtual resolution not the physical, so we need the scaling factor const CGDisplayModeRef mode = CGDisplayCopyDisplayMode(macos_input->display); macos_input->displayScaling = ((CGFloat) CGDisplayPixelsWide(macos_input->display)) / ((CGFloat) CGDisplayModeGetPixelWidth(mode)); CFRelease(mode); macos_input->source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); macos_input->kb_event = CGEventCreate(macos_input->source); macos_input->kb_flags = 0; macos_input->mouse_event = CGEventCreate(macos_input->source); macos_input->mouse_down[0] = false; macos_input->mouse_down[1] = false; macos_input->mouse_down[2] = false; BOOST_LOG(debug) << "Display "sv << macos_input->display << ", pixel dimension: " << CGDisplayPixelsWide(macos_input->display) << "x"sv << CGDisplayPixelsHigh(macos_input->display); return result; } void freeInput(void *p) { const auto *input = static_cast(p); CFRelease(input->source); CFRelease(input->kb_event); CFRelease(input->mouse_event); delete input; } std::vector &supported_gamepads(input_t *input) { static std::vector gamepads { supported_gamepad_t {"", false, "gamepads.macos_not_implemented"} }; return gamepads; } /** * @brief Returns the supported platform capabilities to advertise to the client. * @return Capability flags. */ platform_caps::caps_t get_capabilities() { return 0; } } // namespace platf ================================================ FILE: src/platform/macos/microphone.mm ================================================ /** * @file src/platform/macos/microphone.mm * @brief Definitions for microphone capture on macOS. */ // local includes #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/platform/macos/av_audio.h" namespace platf { using namespace std::literals; struct av_mic_t: public mic_t { AVAudio *av_audio_capture {}; ~av_mic_t() override { [av_audio_capture release]; } capture_e sample(std::vector &sample_in) override { auto sample_size = sample_in.size(); uint32_t length = 0; void *byteSampleBuffer = TPCircularBufferTail(&av_audio_capture->audioSampleBuffer, &length); while (length < sample_size * sizeof(float)) { [av_audio_capture.samplesArrivedSignal wait]; byteSampleBuffer = TPCircularBufferTail(&av_audio_capture->audioSampleBuffer, &length); } const float *sampleBuffer = (float *) byteSampleBuffer; std::vector vectorBuffer(sampleBuffer, sampleBuffer + sample_size); std::copy_n(std::begin(vectorBuffer), sample_size, std::begin(sample_in)); TPCircularBufferConsume(&av_audio_capture->audioSampleBuffer, sample_size * sizeof(float)); return capture_e::ok; } }; struct macos_audio_control_t: public audio_control_t { AVCaptureDevice *audio_capture_device {}; public: int set_sink(const std::string &sink) override { BOOST_LOG(warning) << "audio_control_t::set_sink() unimplemented: "sv << sink; return 0; } std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override { auto mic = std::make_unique(); const char *audio_sink = ""; if (!config::audio.sink.empty()) { audio_sink = config::audio.sink.c_str(); } if ((audio_capture_device = [AVAudio findMicrophone:[NSString stringWithUTF8String:audio_sink]]) == nullptr) { BOOST_LOG(error) << "opening microphone '"sv << audio_sink << "' failed. Please set a valid input source in the Sunshine config."sv; BOOST_LOG(error) << "Available inputs:"sv; for (NSString *name in [AVAudio microphoneNames]) { BOOST_LOG(error) << "\t"sv << [name UTF8String]; } return nullptr; } mic->av_audio_capture = [[AVAudio alloc] init]; if ([mic->av_audio_capture setupMicrophone:audio_capture_device sampleRate:sample_rate frameSize:frame_size channels:channels]) { BOOST_LOG(error) << "Failed to setup microphone."sv; return nullptr; } return mic; } bool is_sink_available(const std::string &sink) override { BOOST_LOG(warning) << "audio_control_t::is_sink_available() unimplemented: "sv << sink; return true; } std::optional sink_info() override { sink_t sink; return sink; } }; std::unique_ptr audio_control() { return std::make_unique(); } } // namespace platf ================================================ FILE: src/platform/macos/misc.h ================================================ /** * @file src/platform/macos/misc.h * @brief Miscellaneous declarations for macOS platform. */ #pragma once // standard includes #include // platform includes #include namespace platf { bool is_screen_capture_allowed(); } namespace dyn { typedef void (*apiproc)(); int load(void *handle, const std::vector> &funcs, bool strict = true); void *handle(const std::vector &libs); } // namespace dyn ================================================ FILE: src/platform/macos/misc.mm ================================================ /** * @file src/platform/macos/misc.mm * @brief Miscellaneous definitions for macOS platform. */ // Required for IPV6_PKTINFO with Darwin headers #ifndef __APPLE_USE_RFC_3542 // NOLINT(bugprone-reserved-identifier) #define __APPLE_USE_RFC_3542 1 #endif // standard includes #include #include // platform includes #include #include #include #include #include #include // lib includes #include #include #include // local includes #include "misc.h" #include "src/entry_handler.h" #include "src/logging.h" #include "src/platform/common.h" using namespace std::literals; namespace fs = std::filesystem; namespace bp = boost::process; namespace platf { // Even though the following two functions are available starting in macOS 10.15, they weren't // actually in the Mac SDK until Xcode 12.2, the first to include the SDK for macOS 11 #if __MAC_OS_X_VERSION_MAX_ALLOWED < 110000 // __MAC_11_0 // If they're not in the SDK then we can use our own function definitions. // Need to use weak import so that this will link in macOS 10.14 and earlier extern "C" bool CGPreflightScreenCaptureAccess(void) __attribute__((weak_import)); extern "C" bool CGRequestScreenCaptureAccess(void) __attribute__((weak_import)); #endif namespace { auto screen_capture_allowed = std::atomic {false}; } // namespace // Return whether screen capture is allowed for this process. bool is_screen_capture_allowed() { return screen_capture_allowed; } std::unique_ptr init() { // This will generate a warning about CGPreflightScreenCaptureAccess and // CGRequestScreenCaptureAccess being unavailable before macOS 10.15, but // we have a guard to prevent it from being called on those earlier systems. // Unfortunately the supported way to silence this warning, using @available, // produces linker errors for __isPlatformVersionAtLeast, so we have to use // a different method. // We also ignore "tautological-pointer-compare" because when compiling with // Xcode 12.2 and later, these functions are not weakly linked and will never // be null, and therefore generate this warning. Since we are weakly linking // when compiling with earlier Xcode versions, the check for null is // necessary, and so we ignore the warning. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability-new" #pragma clang diagnostic ignored "-Wtautological-pointer-compare" if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:((NSOperatingSystemVersion) {10, 15, 0})] && // Double check that these weakly-linked symbols have been loaded: CGPreflightScreenCaptureAccess != nullptr && CGRequestScreenCaptureAccess != nullptr && !CGPreflightScreenCaptureAccess()) { BOOST_LOG(error) << "No screen capture permission!"sv; BOOST_LOG(error) << "Please activate it in 'System Preferences' -> 'Privacy' -> 'Screen Recording'"sv; CGRequestScreenCaptureAccess(); return nullptr; } #pragma clang diagnostic pop // Record that we determined that we have the screen capture permission. screen_capture_allowed = true; return std::make_unique(); } fs::path appdata() { const char *homedir; if ((homedir = getenv("HOME")) == nullptr) { homedir = getpwuid(geteuid())->pw_dir; } return fs::path {homedir} / ".config/sunshine"sv; } using ifaddr_t = util::safe_ptr; ifaddr_t get_ifaddrs() { ifaddrs *p {nullptr}; getifaddrs(&p); return ifaddr_t {p}; } std::string from_sockaddr(const sockaddr *const ip_addr) { char data[INET6_ADDRSTRLEN] = {}; auto family = ip_addr->sa_family; if (family == AF_INET6) { inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); } else if (family == AF_INET) { inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN); } return std::string {data}; } std::pair from_sockaddr_ex(const sockaddr *const ip_addr) { char data[INET6_ADDRSTRLEN] = {}; auto family = ip_addr->sa_family; std::uint16_t port = 0; if (family == AF_INET6) { inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); port = ((sockaddr_in6 *) ip_addr)->sin6_port; } else if (family == AF_INET) { inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN); port = ((sockaddr_in *) ip_addr)->sin_port; } return {port, std::string {data}}; } std::string get_mac_address(const std::string_view &address) { auto ifaddrs = get_ifaddrs(); for (auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) { if (pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) { BOOST_LOG(verbose) << "Looking for MAC of "sv << pos->ifa_name; struct ifaddrs *ifap, *ifaptr; unsigned char *ptr; std::string mac_address; if (getifaddrs(&ifap) == 0) { for (ifaptr = ifap; ifaptr != nullptr; ifaptr = (ifaptr)->ifa_next) { if (!strcmp((ifaptr)->ifa_name, pos->ifa_name) && (((ifaptr)->ifa_addr)->sa_family == AF_LINK)) { ptr = (unsigned char *) LLADDR((struct sockaddr_dl *) (ifaptr)->ifa_addr); char buff[100]; snprintf(buff, sizeof(buff), "%02x:%02x:%02x:%02x:%02x:%02x", *ptr, *(ptr + 1), *(ptr + 2), *(ptr + 3), *(ptr + 4), *(ptr + 5)); mac_address = buff; break; } } freeifaddrs(ifap); if (ifaptr != nullptr) { BOOST_LOG(verbose) << "Found MAC of "sv << pos->ifa_name << ": "sv << mac_address; return mac_address; } } } } BOOST_LOG(warning) << "Unable to find MAC address for "sv << address; return "00:00:00:00:00:00"s; } // TODO: return actual IP std::string get_local_ip_for_gateway() { return ""; } bp::child 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) { // clang-format off if (!group) { if (!file) { 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); } else { 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); } } else { if (!file) { 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); } else { 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); } } // clang-format on } /** * @brief Open a url in the default web browser. * @param url The url to open. */ void open_url(const std::string &url) { boost::filesystem::path working_dir; std::string cmd = R"(open ")" + url + R"(")"; boost::process::v1::environment _env = boost::this_process::environment(); std::error_code ec; auto child = run_command(false, false, cmd, working_dir, _env, nullptr, ec, nullptr); if (ec) { BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); } else { BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; child.detach(); } } void adjust_thread_priority(thread_priority_e priority) { // Unimplemented } void streaming_will_start() { // Nothing to do } void streaming_will_stop() { // Nothing to do } void restart_on_exit() { char executable[2048]; uint32_t size = sizeof(executable); if (_NSGetExecutablePath(executable, &size) < 0) { BOOST_LOG(fatal) << "NSGetExecutablePath() failed: "sv << errno; return; } // ASIO doesn't use O_CLOEXEC, so we have to close all fds ourselves int openmax = (int) sysconf(_SC_OPEN_MAX); for (int fd = STDERR_FILENO + 1; fd < openmax; fd++) { close(fd); } // Re-exec ourselves with the same arguments if (execv(executable, lifetime::get_argv()) < 0) { BOOST_LOG(fatal) << "execv() failed: "sv << errno; return; } } void restart() { // Gracefully clean up and restart ourselves instead of exiting atexit(restart_on_exit); lifetime::exit_sunshine(0, true); } int set_env(const std::string &name, const std::string &value) { return setenv(name.c_str(), value.c_str(), 1); } int unset_env(const std::string &name) { return unsetenv(name.c_str()); } bool request_process_group_exit(std::uintptr_t native_handle) { if (killpg((pid_t) native_handle, SIGTERM) == 0 || errno == ESRCH) { BOOST_LOG(debug) << "Successfully sent SIGTERM to process group: "sv << native_handle; return true; } else { BOOST_LOG(warning) << "Unable to send SIGTERM to process group ["sv << native_handle << "]: "sv << errno; return false; } } bool process_group_running(std::uintptr_t native_handle) { return waitpid(-((pid_t) native_handle), nullptr, WNOHANG) >= 0; } struct sockaddr_in to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) { struct sockaddr_in saddr_v4 = {}; saddr_v4.sin_family = AF_INET; saddr_v4.sin_port = htons(port); auto addr_bytes = address.to_bytes(); memcpy(&saddr_v4.sin_addr, addr_bytes.data(), sizeof(saddr_v4.sin_addr)); return saddr_v4; } struct sockaddr_in6 to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) { struct sockaddr_in6 saddr_v6 = {}; saddr_v6.sin6_family = AF_INET6; saddr_v6.sin6_port = htons(port); saddr_v6.sin6_scope_id = address.scope_id(); auto addr_bytes = address.to_bytes(); memcpy(&saddr_v6.sin6_addr, addr_bytes.data(), sizeof(saddr_v6.sin6_addr)); return saddr_v6; } bool send_batch(batched_send_info_t &send_info) { // Fall back to unbatched send calls return false; } bool send(send_info_t &send_info) { auto sockfd = (int) send_info.native_socket; struct msghdr msg = {}; // Convert the target address into a sockaddr struct sockaddr_in taddr_v4 = {}; struct sockaddr_in6 taddr_v6 = {}; if (send_info.target_address.is_v6()) { taddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port); msg.msg_name = (struct sockaddr *) &taddr_v6; msg.msg_namelen = sizeof(taddr_v6); } else { taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); msg.msg_name = (struct sockaddr *) &taddr_v4; msg.msg_namelen = sizeof(taddr_v4); } union { char buf[std::max(CMSG_SPACE(sizeof(struct in_pktinfo)), CMSG_SPACE(sizeof(struct in6_pktinfo)))]; struct cmsghdr alignment; } cmbuf {}; socklen_t cmbuflen = 0; msg.msg_control = cmbuf.buf; msg.msg_controllen = sizeof(cmbuf.buf); auto pktinfo_cm = CMSG_FIRSTHDR(&msg); if (send_info.source_address.is_v6()) { struct in6_pktinfo pktInfo {}; struct sockaddr_in6 saddr_v6 = to_sockaddr(send_info.source_address.to_v6(), 0); pktInfo.ipi6_addr = saddr_v6.sin6_addr; pktInfo.ipi6_ifindex = 0; cmbuflen += CMSG_SPACE(sizeof(pktInfo)); pktinfo_cm->cmsg_level = IPPROTO_IPV6; pktinfo_cm->cmsg_type = IPV6_PKTINFO; pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); } else { struct in_pktinfo pktInfo {}; struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); pktInfo.ipi_spec_dst = saddr_v4.sin_addr; pktInfo.ipi_ifindex = 0; cmbuflen += CMSG_SPACE(sizeof(pktInfo)); pktinfo_cm->cmsg_level = IPPROTO_IP; pktinfo_cm->cmsg_type = IP_PKTINFO; pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); } struct iovec iovs[2] = {}; int iovlen = 0; if (send_info.header) { iovs[iovlen].iov_base = (void *) send_info.header; iovs[iovlen].iov_len = send_info.header_size; iovlen++; } iovs[iovlen].iov_base = (void *) send_info.payload; iovs[iovlen].iov_len = send_info.payload_size; iovlen++; msg.msg_iov = iovs; msg.msg_iovlen = iovlen; msg.msg_controllen = cmbuflen; auto bytes_sent = sendmsg(sockfd, &msg, 0); // If there's no send buffer space, wait for some to be available while (bytes_sent < 0 && errno == EAGAIN) { struct pollfd pfd; pfd.fd = sockfd; pfd.events = POLLOUT; if (poll(&pfd, 1, -1) != 1) { BOOST_LOG(warning) << "poll() failed: "sv << errno; break; } // Try to send again bytes_sent = sendmsg(sockfd, &msg, 0); } if (bytes_sent < 0) { BOOST_LOG(warning) << "sendmsg() failed: "sv << errno; return false; } return true; } // We can't track QoS state separately for each destination on this OS, // so we keep a ref count to only disable QoS options when all clients // are disconnected. static std::atomic qos_ref_count = 0; class qos_t: public deinit_t { public: qos_t(int sockfd, std::vector> options): sockfd(sockfd), options(options) { qos_ref_count++; } virtual ~qos_t() { if (--qos_ref_count == 0) { for (const auto &tuple : options) { auto reset_val = std::get<2>(tuple); if (setsockopt(sockfd, std::get<0>(tuple), std::get<1>(tuple), &reset_val, sizeof(reset_val)) < 0) { BOOST_LOG(warning) << "Failed to reset option: "sv << errno; } } } } private: int sockfd; std::vector> options; }; /** * @brief Enables QoS on the given socket for traffic to the specified destination. * @param native_socket The native socket handle. * @param address The destination address for traffic sent on this socket. * @param port The destination port for traffic sent on this socket. * @param data_type The type of traffic sent on this socket. * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic. */ std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) { int sockfd = (int) native_socket; std::vector> reset_options; // We can use SO_NET_SERVICE_TYPE to set link-layer prioritization without DSCP tagging int service_type = 0; switch (data_type) { case qos_data_type_e::video: service_type = NET_SERVICE_TYPE_VI; break; case qos_data_type_e::audio: service_type = NET_SERVICE_TYPE_VO; break; default: BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type; break; } if (service_type) { if (setsockopt(sockfd, SOL_SOCKET, SO_NET_SERVICE_TYPE, &service_type, sizeof(service_type)) == 0) { // Reset SO_NET_SERVICE_TYPE to best-effort when QoS is disabled reset_options.emplace_back(std::make_tuple(SOL_SOCKET, SO_NET_SERVICE_TYPE, NET_SERVICE_TYPE_BE)); } else { BOOST_LOG(error) << "Failed to set SO_NET_SERVICE_TYPE: "sv << errno; } } if (dscp_tagging) { int level; int option; if (address.is_v6()) { level = IPPROTO_IPV6; option = IPV6_TCLASS; } else { level = IPPROTO_IP; option = IP_TOS; } // The specific DSCP values here are chosen to be consistent with Windows, // except that we use CS6 instead of CS7 for audio traffic. int dscp = 0; switch (data_type) { case qos_data_type_e::video: dscp = 40; break; case qos_data_type_e::audio: dscp = 48; break; default: BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type; break; } if (dscp) { // Shift to put the DSCP value in the correct position in the TOS field dscp <<= 2; if (setsockopt(sockfd, level, option, &dscp, sizeof(dscp)) == 0) { // Reset TOS to -1 when QoS is disabled reset_options.emplace_back(std::make_tuple(level, option, -1)); } else { BOOST_LOG(error) << "Failed to set TOS/TCLASS: "sv << errno; } } } return std::make_unique(sockfd, reset_options); } std::string get_host_name() { try { return boost::asio::ip::host_name(); } catch (boost::system::system_error &err) { BOOST_LOG(error) << "Failed to get hostname: "sv << err.what(); return "Sunshine"s; } } class macos_high_precision_timer: public high_precision_timer { public: void sleep_for(const std::chrono::nanoseconds &duration) override { std::this_thread::sleep_for(duration); } operator bool() override { return true; } }; std::unique_ptr create_high_precision_timer() { return std::make_unique(); } std::string get_clipboard() { // Placeholder return ""; } bool set_clipboard(const std::string& content) { // Placeholder return false; } } // namespace platf namespace dyn { void *handle(const std::vector &libs) { void *handle; for (auto lib : libs) { handle = dlopen(lib, RTLD_LAZY | RTLD_LOCAL); if (handle) { return handle; } } std::stringstream ss; ss << "Couldn't find any of the following libraries: ["sv << libs.front(); std::for_each(std::begin(libs) + 1, std::end(libs), [&](auto lib) { ss << ", "sv << lib; }); ss << ']'; BOOST_LOG(error) << ss.str(); return nullptr; } int load(void *handle, const std::vector> &funcs, bool strict) { int err = 0; for (auto &func : funcs) { TUPLE_2D_REF(fn, name, func); *fn = (void (*)()) dlsym(handle, name); if (!*fn && strict) { BOOST_LOG(error) << "Couldn't find function: "sv << name; err = -1; } } return err; } } // namespace dyn ================================================ FILE: src/platform/macos/nv12_zero_device.cpp ================================================ /** * @file src/platform/macos/nv12_zero_device.cpp * @brief Definitions for NV12 zero copy device on macOS. */ // standard includes #include // local includes #include "src/platform/macos/av_img_t.h" #include "src/platform/macos/nv12_zero_device.h" #include "src/video.h" extern "C" { #include "libavutil/imgutils.h" } namespace platf { void free_frame(AVFrame *frame) { av_frame_free(&frame); } void free_buffer(void *opaque, uint8_t *data) { CVPixelBufferRelease((CVPixelBufferRef) data); } util::safe_ptr av_frame; int nv12_zero_device::convert(platf::img_t &img) { auto *av_img = (av_img_t *) &img; // Release any existing CVPixelBuffer previously retained for encoding av_buffer_unref(&av_frame->buf[0]); // Attach an AVBufferRef to this frame which will retain ownership of the CVPixelBuffer // until av_buffer_unref() is called (above) or the frame is freed with av_frame_free(). // // The presence of the AVBufferRef allows FFmpeg to simply add a reference to the buffer // rather than having to perform a deep copy of the data buffers in avcodec_send_frame(). av_frame->buf[0] = av_buffer_create((uint8_t *) CFRetain(av_img->pixel_buffer->buf), 0, free_buffer, nullptr, 0); // Place a CVPixelBufferRef at data[3] as required by AV_PIX_FMT_VIDEOTOOLBOX av_frame->data[3] = (uint8_t *) av_img->pixel_buffer->buf; return 0; } int nv12_zero_device::set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { this->frame = frame; av_frame.reset(frame); resolution_fn(this->display, frame->width, frame->height); return 0; } int nv12_zero_device::init(void *display, pix_fmt_e pix_fmt, resolution_fn_t resolution_fn, const pixel_format_fn_t &pixel_format_fn) { pixel_format_fn(display, pix_fmt == pix_fmt_e::nv12 ? kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange : kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange); this->display = display; this->resolution_fn = std::move(resolution_fn); // we never use this pointer, but its existence is checked/used // by the platform independent code data = this; return 0; } } // namespace platf ================================================ FILE: src/platform/macos/nv12_zero_device.h ================================================ /** * @file src/platform/macos/nv12_zero_device.h * @brief Declarations for NV12 zero copy device on macOS. */ #pragma once // local includes #include "src/platform/common.h" struct AVFrame; namespace platf { void free_frame(AVFrame *frame); class nv12_zero_device: public avcodec_encode_device_t { // display holds a pointer to an av_video object. Since the namespaces of AVFoundation // and FFMPEG collide, we need this opaque pointer and cannot use the definition void *display; public: // this function is used to set the resolution on an av_video object that we cannot // call directly because of namespace collisions between AVFoundation and FFMPEG using resolution_fn_t = std::function; resolution_fn_t resolution_fn; using pixel_format_fn_t = std::function; int init(void *display, pix_fmt_e pix_fmt, resolution_fn_t resolution_fn, const pixel_format_fn_t &pixel_format_fn); int convert(img_t &img) override; int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override; private: util::safe_ptr av_frame; }; } // namespace platf ================================================ FILE: src/platform/macos/publish.cpp ================================================ /** * @file src/platform/macos/publish.cpp * @brief Definitions for publishing services on macOS. */ // standard includes #include // platform includes #include // local includes #include "src/logging.h" #include "src/network.h" #include "src/nvhttp.h" #include "src/platform/common.h" using namespace std::literals; namespace platf::publish { namespace { /** @brief Custom deleter intended to be used for `std::unique_ptr`. */ struct ServiceRefDeleter { typedef DNSServiceRef pointer; ///< Type of object to be deleted. void operator()(pointer serviceRef) { DNSServiceRefDeallocate(serviceRef); BOOST_LOG(info) << "Deregistered DNS service."sv; } }; /** @brief This class encapsulates the polling and deinitialization of our connection with * the mDNS service. Implements the `::platf::deinit_t` interface. */ class deinit_t: public ::platf::deinit_t, std::unique_ptr { public: /** @brief Construct deinit_t object. * * Create a thread that will use `select(2)` to wait for a response from the mDNS service. * The thread will give up if an error is received or if `_stopRequested` becomes true. * * @param serviceRef An initialized reference to the mDNS service. */ deinit_t(DNSServiceRef serviceRef): unique_ptr(serviceRef) { _thread = std::thread {[serviceRef, &_stopRequested = std::as_const(_stopRequested)]() { const auto socket = DNSServiceRefSockFD(serviceRef); while (!_stopRequested) { auto fdset = fd_set {}; FD_ZERO(&fdset); FD_SET(socket, &fdset); auto timeout = timeval {.tv_sec = 3, .tv_usec = 0}; // 3 second timeout const auto ready = select(socket + 1, &fdset, nullptr, nullptr, &timeout); if (ready == -1) { BOOST_LOG(error) << "Failed to obtain response from DNS service."sv; break; } else if (ready != 0) { DNSServiceProcessResult(serviceRef); break; } } }}; } /** @brief Ensure that we gracefully finish polling the mDNS service before freeing our * connection to it. */ ~deinit_t() override { _stopRequested = true; _thread.join(); } deinit_t(const deinit_t &) = delete; deinit_t &operator=(const deinit_t &) = delete; private: std::thread _thread; ///< Thread for polling the mDNS service for a response. std::atomic _stopRequested = false; ///< Whether to stop polling the mDNS service. }; /** @brief Callback that will be invoked when the mDNS service finishes registering our service. * @param errorCode Describes whether the registration was successful. */ void registrationCallback(DNSServiceRef /*serviceRef*/, DNSServiceFlags /*flags*/, DNSServiceErrorType errorCode, const char * /*name*/, const char * /*regtype*/, const char * /*domain*/, void * /*context*/) { if (errorCode != kDNSServiceErr_NoError) { BOOST_LOG(error) << "Failed to register DNS service: Error "sv << errorCode; return; } BOOST_LOG(info) << "Successfully registered DNS service."sv; } } // anonymous namespace /** * @brief Main entry point for publication of our service on macOS. * * This function initiates a connection to the macOS mDNS service and requests to register * our Sunshine service. Registration will occur asynchronously (unless it fails immediately, * which is probably only possible if the host machine is misconfigured). * * @return Either `nullptr` (if the registration fails immediately) or a `uniqur_ptr`, * which will manage polling for a response from the mDNS service, and then, when * deconstructed, will deregister the service. */ [[nodiscard]] std::unique_ptr<::platf::deinit_t> start() { auto serviceRef = DNSServiceRef {}; const auto status = DNSServiceRegister( &serviceRef, 0, // flags 0, // interfaceIndex nullptr, // name SERVICE_TYPE, nullptr, // domain nullptr, // host htons(net::map_port(nvhttp::PORT_HTTP)), 0, // txtLen nullptr, // txtRecord registrationCallback, nullptr // context ); if (status != kDNSServiceErr_NoError) { BOOST_LOG(error) << "Failed immediately to register DNS service: Error "sv << status; return nullptr; } return std::make_unique(serviceRef); } } // namespace platf::publish ================================================ FILE: src/platform/windows/PolicyConfig.h ================================================ /** * @file src/platform/windows/PolicyConfig.h * @brief Undocumented COM-interface IPolicyConfig. * @details Use for setting default audio render endpoint. * @author EreTIk * @see https://kitere.github.io/ */ #pragma once // platform includes #include #ifdef __MINGW32__ #undef DEFINE_GUID #ifdef __cplusplus #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}} #else #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}} #endif DEFINE_GUID(IID_IPolicyConfig, 0xf8679f50, 0x850a, 0x41cf, 0x9c, 0x72, 0x43, 0x0f, 0x29, 0x02, 0x90, 0xc8); DEFINE_GUID(CLSID_CPolicyConfigClient, 0x870af99c, 0x171d, 0x4f9e, 0xaf, 0x0d, 0xe6, 0x3d, 0xf4, 0x0c, 0x2b, 0xc9); #endif interface DECLSPEC_UUID("f8679f50-850a-41cf-9c72-430f290290c8") IPolicyConfig; class DECLSPEC_UUID("870af99c-171d-4f9e-af0d-e63df40c2bc9") CPolicyConfigClient; // ---------------------------------------------------------------------------- // class CPolicyConfigClient // {870af99c-171d-4f9e-af0d-e63df40c2bc9} // // interface IPolicyConfig // {f8679f50-850a-41cf-9c72-430f290290c8} // // Query interface: // CComPtr PolicyConfig; // PolicyConfig.CoCreateInstance(__uuidof(CPolicyConfigClient)); // // @compatible: Windows 7 and Later // ---------------------------------------------------------------------------- interface IPolicyConfig: public IUnknown { public: virtual HRESULT GetMixFormat( PCWSTR, WAVEFORMATEX ** ); virtual HRESULT STDMETHODCALLTYPE GetDeviceFormat( PCWSTR, INT, WAVEFORMATEX ** ); virtual HRESULT STDMETHODCALLTYPE ResetDeviceFormat( PCWSTR ); virtual HRESULT STDMETHODCALLTYPE SetDeviceFormat( PCWSTR, WAVEFORMATEX *, WAVEFORMATEX * ); virtual HRESULT STDMETHODCALLTYPE GetProcessingPeriod( PCWSTR, INT, PINT64, PINT64 ); virtual HRESULT STDMETHODCALLTYPE SetProcessingPeriod( PCWSTR, PINT64 ); virtual HRESULT STDMETHODCALLTYPE GetShareMode( PCWSTR, struct DeviceShareMode * ); virtual HRESULT STDMETHODCALLTYPE SetShareMode( PCWSTR, struct DeviceShareMode * ); virtual HRESULT STDMETHODCALLTYPE GetPropertyValue( PCWSTR, const PROPERTYKEY &, PROPVARIANT * ); virtual HRESULT STDMETHODCALLTYPE SetPropertyValue( PCWSTR, const PROPERTYKEY &, PROPVARIANT * ); virtual HRESULT STDMETHODCALLTYPE SetDefaultEndpoint( PCWSTR wszDeviceId, ERole eRole ); virtual HRESULT STDMETHODCALLTYPE SetEndpointVisibility( PCWSTR, INT ); }; ================================================ FILE: src/platform/windows/audio.cpp ================================================ /** * @file src/platform/windows/audio.cpp * @brief Definitions for Windows audio capture. */ #define INITGUID // standard includes #include // platform includes #include #include #include #include #include #include // local includes #include "misc.h" #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" // Must be the last included file // clang-format off #include "PolicyConfig.h" // clang-format on DEFINE_PROPERTYKEY(PKEY_Device_DeviceDesc, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 2); // DEVPROP_TYPE_STRING DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14); // DEVPROP_TYPE_STRING DEFINE_PROPERTYKEY(PKEY_DeviceInterface_FriendlyName, 0x026e516e, 0xb814, 0x414b, 0x83, 0xcd, 0x85, 0x6d, 0x6f, 0xef, 0x48, 0x22, 2); #if defined(__x86_64) || defined(__x86_64__) || defined(__amd64) || defined(__amd64__) || defined(_M_AMD64) #define STEAM_DRIVER_SUBDIR L"x64" #else #warning No known Steam audio driver for this architecture #endif namespace { constexpr auto SAMPLE_RATE = 48000; constexpr auto STEAM_AUDIO_DRIVER_PATH = L"%CommonProgramFiles(x86)%\\Steam\\drivers\\Windows10\\" STEAM_DRIVER_SUBDIR L"\\SteamStreamingSpeakers.inf"; constexpr auto waveformat_mask_stereo = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT; constexpr auto waveformat_mask_surround51_with_backspeakers = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT; constexpr auto waveformat_mask_surround51_with_sidespeakers = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT; constexpr auto waveformat_mask_surround71 = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT; enum class sample_format_e { f32, s32, s24in32, s24, s16, _size, }; constexpr WAVEFORMATEXTENSIBLE create_waveformat(sample_format_e sample_format, WORD channel_count, DWORD channel_mask) { WAVEFORMATEXTENSIBLE waveformat = {}; switch (sample_format) { default: case sample_format_e::f32: waveformat.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; waveformat.Format.wBitsPerSample = 32; waveformat.Samples.wValidBitsPerSample = 32; break; case sample_format_e::s32: waveformat.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; waveformat.Format.wBitsPerSample = 32; waveformat.Samples.wValidBitsPerSample = 32; break; case sample_format_e::s24in32: waveformat.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; waveformat.Format.wBitsPerSample = 32; waveformat.Samples.wValidBitsPerSample = 24; break; case sample_format_e::s24: waveformat.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; waveformat.Format.wBitsPerSample = 24; waveformat.Samples.wValidBitsPerSample = 24; break; case sample_format_e::s16: waveformat.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; waveformat.Format.wBitsPerSample = 16; waveformat.Samples.wValidBitsPerSample = 16; break; } static_assert((int) sample_format_e::_size == 5, "Unrecognized sample_format_e"); waveformat.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; waveformat.Format.nChannels = channel_count; waveformat.Format.nSamplesPerSec = SAMPLE_RATE; waveformat.Format.nBlockAlign = waveformat.Format.nChannels * waveformat.Format.wBitsPerSample / 8; waveformat.Format.nAvgBytesPerSec = waveformat.Format.nSamplesPerSec * waveformat.Format.nBlockAlign; waveformat.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); waveformat.dwChannelMask = channel_mask; return waveformat; } using virtual_sink_waveformats_t = std::vector; /** * @brief List of supported waveformats for an N-channel virtual audio device * @tparam channel_count Number of virtual audio channels * @returns std::vector * @note The list of virtual formats returned are sorted in preference order and the first valid * format will be used. All bits-per-sample options are listed because we try to match * this to the default audio device. See also: set_format() below. */ template virtual_sink_waveformats_t create_virtual_sink_waveformats() { if constexpr (channel_count == 2) { auto channel_mask = waveformat_mask_stereo; // The 32-bit formats are a lower priority for stereo because using one will disable Dolby/DTS // spatial audio mode if the user enabled it on the Steam speaker. return { create_waveformat(sample_format_e::s24in32, channel_count, channel_mask), create_waveformat(sample_format_e::s24, channel_count, channel_mask), create_waveformat(sample_format_e::s16, channel_count, channel_mask), create_waveformat(sample_format_e::f32, channel_count, channel_mask), create_waveformat(sample_format_e::s32, channel_count, channel_mask), }; } else if (channel_count == 6) { auto channel_mask1 = waveformat_mask_surround51_with_backspeakers; auto channel_mask2 = waveformat_mask_surround51_with_sidespeakers; return { create_waveformat(sample_format_e::f32, channel_count, channel_mask1), create_waveformat(sample_format_e::f32, channel_count, channel_mask2), create_waveformat(sample_format_e::s32, channel_count, channel_mask1), create_waveformat(sample_format_e::s32, channel_count, channel_mask2), create_waveformat(sample_format_e::s24in32, channel_count, channel_mask1), create_waveformat(sample_format_e::s24in32, channel_count, channel_mask2), create_waveformat(sample_format_e::s24, channel_count, channel_mask1), create_waveformat(sample_format_e::s24, channel_count, channel_mask2), create_waveformat(sample_format_e::s16, channel_count, channel_mask1), create_waveformat(sample_format_e::s16, channel_count, channel_mask2), }; } else if (channel_count == 8) { auto channel_mask = waveformat_mask_surround71; return { create_waveformat(sample_format_e::f32, channel_count, channel_mask), create_waveformat(sample_format_e::s32, channel_count, channel_mask), create_waveformat(sample_format_e::s24in32, channel_count, channel_mask), create_waveformat(sample_format_e::s24, channel_count, channel_mask), create_waveformat(sample_format_e::s16, channel_count, channel_mask), }; } } std::string waveformat_to_pretty_string(const WAVEFORMATEXTENSIBLE &waveformat) { std::string result = waveformat.SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT ? "F" : waveformat.SubFormat == KSDATAFORMAT_SUBTYPE_PCM ? "S" : "UNKNOWN"; result += std::format("{} {} ", static_cast(waveformat.Samples.wValidBitsPerSample), static_cast(waveformat.Format.nSamplesPerSec)); switch (waveformat.dwChannelMask) { case waveformat_mask_stereo: result += "2.0"; break; case waveformat_mask_surround51_with_backspeakers: result += "5.1"; break; case waveformat_mask_surround51_with_sidespeakers: result += "5.1 (sidespeakers)"; break; case waveformat_mask_surround71: result += "7.1"; break; default: result += std::format("{} channels (unrecognized)", static_cast(waveformat.Format.nChannels)); break; } return result; } } // namespace using namespace std::literals; namespace platf::audio { template void Release(T *p) { p->Release(); } template void co_task_free(T *p) { CoTaskMemFree((LPVOID) p); } using device_enum_t = util::safe_ptr>; using device_t = util::safe_ptr>; using collection_t = util::safe_ptr>; using audio_client_t = util::safe_ptr>; using audio_capture_t = util::safe_ptr>; using wave_format_t = util::safe_ptr>; using wstring_t = util::safe_ptr>; using handle_t = util::safe_ptr_v2; using policy_t = util::safe_ptr>; using prop_t = util::safe_ptr>; class co_init_t: public deinit_t { public: co_init_t() { CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY); } ~co_init_t() override { CoUninitialize(); } }; class prop_var_t { public: prop_var_t() { PropVariantInit(&prop); } ~prop_var_t() { PropVariantClear(&prop); } PROPVARIANT prop; }; struct format_t { WORD channel_count; std::string name; int capture_waveformat_channel_mask; virtual_sink_waveformats_t virtual_sink_waveformats; }; const std::array formats = { format_t { 2, "Stereo", waveformat_mask_stereo, create_virtual_sink_waveformats<2>(), }, format_t { 6, "Surround 5.1", waveformat_mask_surround51_with_backspeakers, create_virtual_sink_waveformats<6>(), }, format_t { 8, "Surround 7.1", waveformat_mask_surround71, create_virtual_sink_waveformats<8>(), }, }; audio_client_t make_audio_client(device_t &device, const format_t &format) { audio_client_t audio_client; auto status = device->Activate( IID_IAudioClient, CLSCTX_ALL, nullptr, (void **) &audio_client ); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't activate Device: [0x"sv << util::hex(status).to_string_view() << ']'; return nullptr; } WAVEFORMATEXTENSIBLE capture_waveformat = create_waveformat(sample_format_e::f32, format.channel_count, format.capture_waveformat_channel_mask); { wave_format_t mixer_waveformat; status = audio_client->GetMixFormat(&mixer_waveformat); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't get mix format for audio device: [0x"sv << util::hex(status).to_string_view() << ']'; return nullptr; } // Prefer the native channel layout of captured audio device when channel counts match if (mixer_waveformat->nChannels == format.channel_count && mixer_waveformat->wFormatTag == WAVE_FORMAT_EXTENSIBLE && mixer_waveformat->cbSize >= 22) { auto waveformatext_pointer = reinterpret_cast(mixer_waveformat.get()); capture_waveformat.dwChannelMask = waveformatext_pointer->dwChannelMask; } BOOST_LOG(info) << "Audio mixer format is "sv << mixer_waveformat->wBitsPerSample << "-bit, "sv << mixer_waveformat->nSamplesPerSec << " Hz, "sv << ((mixer_waveformat->nSamplesPerSec != 48000) ? "will be resampled to 48000 by Windows"sv : "no resampling needed"sv); } status = audio_client->Initialize( AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY, // Enable automatic resampling to 48 KHz 0, 0, (LPWAVEFORMATEX) &capture_waveformat, nullptr ); if (status) { BOOST_LOG(error) << "Couldn't initialize audio client for ["sv << format.name << "]: [0x"sv << util::hex(status).to_string_view() << ']'; return nullptr; } BOOST_LOG(info) << "Audio capture format is "sv << logging::bracket(waveformat_to_pretty_string(capture_waveformat)); return audio_client; } device_t default_device(device_enum_t &device_enum) { device_t device; HRESULT status; status = device_enum->GetDefaultAudioEndpoint( eRender, eConsole, &device ); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't get default audio endpoint [0x"sv << util::hex(status).to_string_view() << ']'; return nullptr; } return device; } class audio_notification_t: public ::IMMNotificationClient { public: audio_notification_t() { } // IUnknown implementation (unused by IMMDeviceEnumerator) ULONG STDMETHODCALLTYPE AddRef() { return 1; } ULONG STDMETHODCALLTYPE Release() { return 1; } HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID **ppvInterface) { if (IID_IUnknown == riid) { AddRef(); *ppvInterface = (IUnknown *) this; return S_OK; } else if (__uuidof(IMMNotificationClient) == riid) { AddRef(); *ppvInterface = (IMMNotificationClient *) this; return S_OK; } else { *ppvInterface = nullptr; return E_NOINTERFACE; } } // IMMNotificationClient HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) { if (flow == eRender) { default_render_device_changed_flag.store(true); } return S_OK; } HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) { return S_OK; } HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) { return S_OK; } HRESULT STDMETHODCALLTYPE OnDeviceStateChanged( LPCWSTR pwstrDeviceId, DWORD dwNewState ) { return S_OK; } HRESULT STDMETHODCALLTYPE OnPropertyValueChanged( LPCWSTR pwstrDeviceId, const PROPERTYKEY key ) { return S_OK; } /** * @brief Checks if the default rendering device changed and resets the change flag * @return `true` if the device changed since last call */ bool check_default_render_device_changed() { return default_render_device_changed_flag.exchange(false); } private: std::atomic_bool default_render_device_changed_flag; }; class mic_wasapi_t: public mic_t { public: capture_e sample(std::vector &sample_out) override { auto sample_size = sample_out.size(); // Refill the sample buffer if needed while (sample_buf_pos - std::begin(sample_buf) < sample_size) { auto capture_result = _fill_buffer(); if (capture_result != capture_e::ok) { return capture_result; } } // Fill the output buffer with samples std::copy_n(std::begin(sample_buf), sample_size, std::begin(sample_out)); // Move any excess samples to the front of the buffer std::move(&sample_buf[sample_size], sample_buf_pos, std::begin(sample_buf)); sample_buf_pos -= sample_size; return capture_e::ok; } int init(std::uint32_t sample_rate, std::uint32_t frame_size, std::uint32_t channels_out) { audio_event.reset(CreateEventA(nullptr, FALSE, FALSE, nullptr)); if (!audio_event) { BOOST_LOG(error) << "Couldn't create Event handle"sv; return -1; } HRESULT status; status = CoCreateInstance( CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, IID_IMMDeviceEnumerator, (void **) &device_enum ); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't create Device Enumerator [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } status = device_enum->RegisterEndpointNotificationCallback(&endpt_notification); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't register endpoint notification [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } auto device = default_device(device_enum); if (!device) { return -1; } for (const auto &format : formats) { if (format.channel_count != channels_out) { BOOST_LOG(debug) << "Skipping audio format ["sv << format.name << "] with channel count ["sv << format.channel_count << " != "sv << channels_out << ']'; continue; } BOOST_LOG(debug) << "Trying audio format ["sv << format.name << ']'; audio_client = make_audio_client(device, format); if (audio_client) { BOOST_LOG(debug) << "Found audio format ["sv << format.name << ']'; channels = channels_out; break; } } if (!audio_client) { BOOST_LOG(error) << "Couldn't find supported format for audio"sv; return -1; } REFERENCE_TIME default_latency; audio_client->GetDevicePeriod(&default_latency, nullptr); default_latency_ms = default_latency / 1000; std::uint32_t frames; status = audio_client->GetBufferSize(&frames); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't acquire the number of audio frames [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } // *2 --> needs to fit double sample_buf = util::buffer_t {std::max(frames, frame_size) * 2 * channels_out}; sample_buf_pos = std::begin(sample_buf); status = audio_client->GetService(IID_IAudioCaptureClient, (void **) &audio_capture); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't initialize audio capture client [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } status = audio_client->SetEventHandle(audio_event.get()); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't set event handle [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } { DWORD task_index = 0; mmcss_task_handle = AvSetMmThreadCharacteristics("Pro Audio", &task_index); if (!mmcss_task_handle) { BOOST_LOG(error) << "Couldn't associate audio capture thread with Pro Audio MMCSS task [0x" << util::hex(GetLastError()).to_string_view() << ']'; } } status = audio_client->Start(); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't start recording [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } return 0; } ~mic_wasapi_t() override { if (device_enum) { device_enum->UnregisterEndpointNotificationCallback(&endpt_notification); } if (audio_client) { audio_client->Stop(); } if (mmcss_task_handle) { AvRevertMmThreadCharacteristics(mmcss_task_handle); } } private: capture_e _fill_buffer() { HRESULT status; // Total number of samples struct sample_aligned_t { std::uint32_t uninitialized; float *samples; } sample_aligned; // number of samples / number of channels struct block_aligned_t { std::uint32_t audio_sample_size; } block_aligned; // Check if the default audio device has changed if (endpt_notification.check_default_render_device_changed()) { // Invoke the audio_control_t's callback if it wants one if (default_endpt_changed_cb) { (*default_endpt_changed_cb)(); } // Reinitialize to pick up the new default device return capture_e::reinit; } status = WaitForSingleObjectEx(audio_event.get(), default_latency_ms, FALSE); switch (status) { case WAIT_OBJECT_0: break; case WAIT_TIMEOUT: return capture_e::timeout; default: BOOST_LOG(error) << "Couldn't wait for audio event: [0x"sv << util::hex(status).to_string_view() << ']'; return capture_e::error; } std::uint32_t packet_size {}; for ( status = audio_capture->GetNextPacketSize(&packet_size); SUCCEEDED(status) && packet_size > 0; status = audio_capture->GetNextPacketSize(&packet_size) ) { DWORD buffer_flags; status = audio_capture->GetBuffer( (BYTE **) &sample_aligned.samples, &block_aligned.audio_sample_size, &buffer_flags, nullptr, nullptr ); switch (status) { case S_OK: break; case AUDCLNT_E_DEVICE_INVALIDATED: return capture_e::reinit; default: BOOST_LOG(error) << "Couldn't capture audio [0x"sv << util::hex(status).to_string_view() << ']'; return capture_e::error; } if (buffer_flags & AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY) { BOOST_LOG(debug) << "Audio capture signaled buffer discontinuity"; } sample_aligned.uninitialized = std::end(sample_buf) - sample_buf_pos; auto n = std::min(sample_aligned.uninitialized, block_aligned.audio_sample_size * channels); if (n < block_aligned.audio_sample_size * channels) { BOOST_LOG(warning) << "Audio capture buffer overflow"; } if (buffer_flags & AUDCLNT_BUFFERFLAGS_SILENT) { std::fill_n(sample_buf_pos, n, 0); } else { std::copy_n(sample_aligned.samples, n, sample_buf_pos); } sample_buf_pos += n; audio_capture->ReleaseBuffer(block_aligned.audio_sample_size); } if (status == AUDCLNT_E_DEVICE_INVALIDATED) { return capture_e::reinit; } if (FAILED(status)) { return capture_e::error; } return capture_e::ok; } public: handle_t audio_event; device_enum_t device_enum; device_t device; audio_client_t audio_client; audio_capture_t audio_capture; audio_notification_t endpt_notification; std::optional> default_endpt_changed_cb; REFERENCE_TIME default_latency_ms; util::buffer_t sample_buf; float *sample_buf_pos; int channels; HANDLE mmcss_task_handle = nullptr; }; class audio_control_t: public ::platf::audio_control_t { public: std::optional sink_info() override { sink_t sink; // Fill host sink name with the device_id of the current default audio device. { auto device = default_device(device_enum); if (!device) { return std::nullopt; } audio::wstring_t id; device->GetId(&id); sink.host = to_utf8(id.get()); } // Prepare to search for the device_id of the virtual audio sink device, // this device can be either user-configured or // the Steam Streaming Speakers we use by default. match_fields_list_t match_list; if (config::audio.virtual_sink.empty()) { match_list = match_steam_speakers(); } else { match_list = match_all_fields(from_utf8(config::audio.virtual_sink)); } // Search for the virtual audio sink device currently present in the system. auto matched = find_device_id(match_list); if (matched) { // Prepare to fill virtual audio sink names with device_id. auto device_id = to_utf8(matched->second); // Also prepend format name (basically channel layout at the moment) // because we don't want to extend the platform interface. sink.null = std::make_optional(sink_t::null_t { "virtual-"s + formats[0].name + device_id, "virtual-"s + formats[1].name + device_id, "virtual-"s + formats[2].name + device_id, }); } else if (!config::audio.virtual_sink.empty()) { BOOST_LOG(warning) << "Couldn't find the specified virtual audio sink " << config::audio.virtual_sink; } return sink; } bool is_sink_available(const std::string &sink) override { const auto match_list = match_all_fields(from_utf8(sink)); const auto matched = find_device_id(match_list); return static_cast(matched); } /** * @brief Extract virtual audio sink information possibly encoded in the sink name. * @param sink The sink name * @return A pair of device_id and format reference if the sink name matches * our naming scheme for virtual audio sinks, `std::nullopt` otherwise. */ std::optional>> extract_virtual_sink_info(const std::string &sink) { // Encoding format: // [virtual-(format name)]device_id std::string current = sink; auto prefix = "virtual-"sv; if (current.find(prefix) == 0) { current = current.substr(prefix.size(), current.size() - prefix.size()); for (const auto &format : formats) { auto &name = format.name; if (current.find(name) == 0) { auto device_id = from_utf8(current.substr(name.size(), current.size() - name.size())); return std::make_pair(device_id, std::reference_wrapper(format)); } } } return std::nullopt; } std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override { auto mic = std::make_unique(); if (mic->init(sample_rate, frame_size, channels)) { return nullptr; } if (config::audio.keep_default) { // If this is a virtual sink, set a callback that will change the sink back if it's changed auto virtual_sink_info = extract_virtual_sink_info(assigned_sink); if (virtual_sink_info) { mic->default_endpt_changed_cb = [this] { BOOST_LOG(info) << "Resetting sink to ["sv << assigned_sink << "] after default changed"; set_sink(assigned_sink); }; } } return mic; } /** * If the requested sink is a virtual sink, meaning no speakers attached to * the host, then we can seamlessly set the format to stereo and surround sound. * * Any virtual sink detected will be prefixed by: * virtual-(format name) * If it doesn't contain that prefix, then the format will not be changed */ std::optional set_format(const std::string &sink) { if (sink.empty()) { return std::nullopt; } auto virtual_sink_info = extract_virtual_sink_info(sink); if (!virtual_sink_info) { // Sink name does not begin with virtual-(format name), hence it's not a virtual sink // and we don't want to change playback format of the corresponding device. // Also need to perform matching, sink name is not necessarily device_id in this case. auto matched = find_device_id(match_all_fields(from_utf8(sink))); if (matched) { return matched->second; } else { BOOST_LOG(error) << "Couldn't find audio sink " << sink; return std::nullopt; } } // When switching to a Steam virtual speaker device, try to retain the bit depth of the // default audio device. Switching from a 16-bit device to a 24-bit one has been known to // cause glitches for some users. int wanted_bits_per_sample = 32; auto current_default_dev = default_device(device_enum); if (current_default_dev) { audio::prop_t prop; prop_var_t current_device_format; if (SUCCEEDED(current_default_dev->OpenPropertyStore(STGM_READ, &prop)) && SUCCEEDED(prop->GetValue(PKEY_AudioEngine_DeviceFormat, ¤t_device_format.prop))) { auto *format = (WAVEFORMATEXTENSIBLE *) current_device_format.prop.blob.pBlobData; wanted_bits_per_sample = format->Samples.wValidBitsPerSample; BOOST_LOG(info) << "Virtual audio device will use "sv << wanted_bits_per_sample << "-bit to match default device"sv; } } auto &device_id = virtual_sink_info->first; auto &waveformats = virtual_sink_info->second.get().virtual_sink_waveformats; for (const auto &waveformat : waveformats) { // We're using completely undocumented and unlisted API, // better not pass objects without copying them first. auto device_id_copy = device_id; auto waveformat_copy = waveformat; auto waveformat_copy_pointer = reinterpret_cast(&waveformat_copy); if (wanted_bits_per_sample != waveformat.Samples.wValidBitsPerSample) { continue; } WAVEFORMATEXTENSIBLE p {}; if (SUCCEEDED(policy->SetDeviceFormat(device_id_copy.c_str(), waveformat_copy_pointer, (WAVEFORMATEX *) &p))) { BOOST_LOG(info) << "Changed virtual audio sink format to " << logging::bracket(waveformat_to_pretty_string(waveformat)); return device_id; } } BOOST_LOG(error) << "Couldn't set virtual audio sink waveformat"; return std::nullopt; } int set_sink(const std::string &sink) override { auto device_id = set_format(sink); if (!device_id) { return -1; } int failure {}; for (int x = 0; x < (int) ERole_enum_count; ++x) { auto status = policy->SetDefaultEndpoint(device_id->c_str(), (ERole) x); if (status) { // Depending on the format of the string, we could get either of these errors if (status == HRESULT_FROM_WIN32(ERROR_NOT_FOUND) || status == E_INVALIDARG) { BOOST_LOG(warning) << "Audio sink not found: "sv << sink; } else { BOOST_LOG(warning) << "Couldn't set ["sv << sink << "] to role ["sv << x << "]: 0x"sv << util::hex(status).to_string_view(); } ++failure; } } // Remember the assigned sink name, so we have it for later if we need to set it // back after another application changes it if (!failure) { assigned_sink = sink; } return failure; } enum class match_field_e { device_id, ///< Match device_id device_friendly_name, ///< Match endpoint friendly name adapter_friendly_name, ///< Match adapter friendly name device_description, ///< Match endpoint description }; using match_fields_list_t = std::vector>; using matched_field_t = std::pair; audio_control_t::match_fields_list_t match_steam_speakers() { return { {match_field_e::adapter_friendly_name, L"Steam Streaming Speakers"} }; } audio_control_t::match_fields_list_t match_all_fields(const std::wstring &name) { return { {match_field_e::device_id, name}, // {0.0.0.00000000}.{29dd7668-45b2-4846-882d-950f55bf7eb8} {match_field_e::device_friendly_name, name}, // Digital Audio (S/PDIF) (High Definition Audio Device) {match_field_e::device_description, name}, // Digital Audio (S/PDIF) {match_field_e::adapter_friendly_name, name}, // High Definition Audio Device }; } /** * @brief Search for currently present audio device_id using multiple match fields. * @param match_list Pairs of match fields and values * @return Optional pair of matched field and device_id */ std::optional find_device_id(const match_fields_list_t &match_list) { if (match_list.empty()) { return std::nullopt; } collection_t collection; auto status = device_enum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't enumerate: [0x"sv << util::hex(status).to_string_view() << ']'; return std::nullopt; } UINT count = 0; collection->GetCount(&count); std::vector matched(match_list.size()); for (auto x = 0; x < count; ++x) { audio::device_t device; collection->Item(x, &device); audio::wstring_t wstring_id; device->GetId(&wstring_id); std::wstring device_id = wstring_id.get(); audio::prop_t prop; device->OpenPropertyStore(STGM_READ, &prop); prop_var_t adapter_friendly_name; prop_var_t device_friendly_name; prop_var_t device_desc; prop->GetValue(PKEY_Device_FriendlyName, &device_friendly_name.prop); prop->GetValue(PKEY_DeviceInterface_FriendlyName, &adapter_friendly_name.prop); prop->GetValue(PKEY_Device_DeviceDesc, &device_desc.prop); for (size_t i = 0; i < match_list.size(); i++) { if (matched[i].empty()) { const wchar_t *match_value = nullptr; switch (match_list[i].first) { case match_field_e::device_id: match_value = device_id.c_str(); break; case match_field_e::device_friendly_name: match_value = device_friendly_name.prop.pwszVal; break; case match_field_e::adapter_friendly_name: match_value = adapter_friendly_name.prop.pwszVal; break; case match_field_e::device_description: match_value = device_desc.prop.pwszVal; break; } if (match_value && std::wcscmp(match_value, match_list[i].second.c_str()) == 0) { matched[i] = device_id; } } } } for (size_t i = 0; i < match_list.size(); i++) { if (!matched[i].empty()) { return matched_field_t(match_list[i].first, matched[i]); } } return std::nullopt; } /** * @brief Resets the default audio device from Steam Streaming Speakers. */ void reset_default_device() { auto matched_steam = find_device_id(match_steam_speakers()); if (!matched_steam) { return; } auto steam_device_id = matched_steam->second; { // Get the current default audio device (if present) auto current_default_dev = default_device(device_enum); if (!current_default_dev) { return; } audio::wstring_t current_default_id; current_default_dev->GetId(¤t_default_id); // If Steam Streaming Speakers are already not default, we're done. if (steam_device_id != current_default_id.get()) { return; } } // Disable the Steam Streaming Speakers temporarily to allow the OS to pick a new default. auto hr = policy->SetEndpointVisibility(steam_device_id.c_str(), FALSE); if (FAILED(hr)) { BOOST_LOG(warning) << "Failed to disable Steam audio device: "sv << util::hex(hr).to_string_view(); return; } // Get the newly selected default audio device auto new_default_dev = default_device(device_enum); // Enable the Steam Streaming Speakers again hr = policy->SetEndpointVisibility(steam_device_id.c_str(), TRUE); if (FAILED(hr)) { BOOST_LOG(warning) << "Failed to enable Steam audio device: "sv << util::hex(hr).to_string_view(); return; } // If there's now no audio device, the Steam Streaming Speakers were the only device available. // There's no other device to set as the default, so just return. if (!new_default_dev) { return; } audio::wstring_t new_default_id; new_default_dev->GetId(&new_default_id); // Set the new default audio device for (int x = 0; x < (int) ERole_enum_count; ++x) { policy->SetDefaultEndpoint(new_default_id.get(), (ERole) x); } BOOST_LOG(info) << "Successfully reset default audio device"sv; } /** * @brief Installs the Steam Streaming Speakers driver, if present. * @return `true` if installation was successful. */ bool install_steam_audio_drivers() { #ifdef STEAM_DRIVER_SUBDIR // MinGW's libnewdev.a is missing DiInstallDriverW() even though the headers have it, // so we have to load it at runtime. It's Vista or later, so it will always be available. auto newdev = LoadLibraryExW(L"newdev.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); if (!newdev) { BOOST_LOG(error) << "newdev.dll failed to load"sv; return false; } auto fg = util::fail_guard([newdev]() { FreeLibrary(newdev); }); auto fn_DiInstallDriverW = (decltype(DiInstallDriverW) *) GetProcAddress(newdev, "DiInstallDriverW"); if (!fn_DiInstallDriverW) { BOOST_LOG(error) << "DiInstallDriverW() is missing"sv; return false; } // Get the current default audio device (if present) auto old_default_dev = default_device(device_enum); // Install the Steam Streaming Speakers driver WCHAR driver_path[MAX_PATH] = {}; ExpandEnvironmentStringsW(STEAM_AUDIO_DRIVER_PATH, driver_path, ARRAYSIZE(driver_path)); if (fn_DiInstallDriverW(nullptr, driver_path, 0, nullptr)) { BOOST_LOG(info) << "Successfully installed Steam Streaming Speakers"sv; // Wait for 5 seconds to allow the audio subsystem to reconfigure things before // modifying the default audio device or enumerating devices again. Sleep(5000); // If there was a previous default device, restore that original device as the // default output device just in case installing the new one changed it. if (old_default_dev) { audio::wstring_t old_default_id; old_default_dev->GetId(&old_default_id); for (int x = 0; x < (int) ERole_enum_count; ++x) { policy->SetDefaultEndpoint(old_default_id.get(), (ERole) x); } } return true; } else { auto err = GetLastError(); switch (err) { case ERROR_ACCESS_DENIED: BOOST_LOG(warning) << "Administrator privileges are required to install Steam Streaming Speakers"sv; break; case ERROR_FILE_NOT_FOUND: case ERROR_PATH_NOT_FOUND: BOOST_LOG(info) << "Steam audio drivers not found. This is expected if you don't have Steam installed."sv; break; default: BOOST_LOG(warning) << "Failed to install Steam audio drivers: "sv << err; break; } return false; } #else BOOST_LOG(warning) << "Unable to install Steam Streaming Speakers on unknown architecture"sv; return false; #endif } int init() { auto status = CoCreateInstance( CLSID_CPolicyConfigClient, nullptr, CLSCTX_ALL, IID_IPolicyConfig, (void **) &policy ); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't create audio policy config: [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } status = CoCreateInstance( CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, IID_IMMDeviceEnumerator, (void **) &device_enum ); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't create Device Enumerator: [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } return 0; } ~audio_control_t() override { } policy_t policy; audio::device_enum_t device_enum; std::string assigned_sink; }; } // namespace platf::audio namespace platf { // It's not big enough to justify it's own source file :/ namespace dxgi { int init(); } std::unique_ptr audio_control() { auto control = std::make_unique(); if (control->init()) { return nullptr; } // Install Steam Streaming Speakers if needed. We do this during audio_control() to ensure // the sink information returned includes the new Steam Streaming Speakers device. if (config::audio.install_steam_drivers && !control->find_device_id(control->match_steam_speakers())) { // This is best effort. Don't fail if it doesn't work. control->install_steam_audio_drivers(); } return control; } std::unique_ptr init() { if (dxgi::init()) { return nullptr; } // Initialize COM auto co_init = std::make_unique(); // If Steam Streaming Speakers are currently the default audio device, // change the default to something else (if another device is available). audio::audio_control_t audio_ctrl; if (audio_ctrl.init() == 0) { audio_ctrl.reset_default_device(); } return co_init; } } // namespace platf ================================================ FILE: src/platform/windows/display.h ================================================ /** * @file src/platform/windows/display.h * @brief Declarations for the Windows display backend. */ #pragma once // platform includes #include #include #include #include #include #include #include #include // local includes #include "src/platform/common.h" #include "src/utility.h" #include "src/video.h" namespace platf::dxgi { extern const char *format_str[]; // Add D3D11_CREATE_DEVICE_DEBUG here to enable the D3D11 debug runtime. // You should have a debugger like WinDbg attached to receive debug messages. auto constexpr D3D11_CREATE_DEVICE_FLAGS = 0; template void Release(T *dxgi) { dxgi->Release(); } using factory1_t = util::safe_ptr>; using dxgi_t = util::safe_ptr>; using dxgi1_t = util::safe_ptr>; using device_t = util::safe_ptr>; using device1_t = util::safe_ptr>; using device_ctx_t = util::safe_ptr>; using adapter_t = util::safe_ptr>; using output_t = util::safe_ptr>; using output1_t = util::safe_ptr>; using output5_t = util::safe_ptr>; using output6_t = util::safe_ptr>; using dup_t = util::safe_ptr>; using texture2d_t = util::safe_ptr>; using texture1d_t = util::safe_ptr>; using resource_t = util::safe_ptr>; using resource1_t = util::safe_ptr>; using multithread_t = util::safe_ptr>; using vs_t = util::safe_ptr>; using ps_t = util::safe_ptr>; using blend_t = util::safe_ptr>; using input_layout_t = util::safe_ptr>; using render_target_t = util::safe_ptr>; using shader_res_t = util::safe_ptr>; using buf_t = util::safe_ptr>; using raster_state_t = util::safe_ptr>; using sampler_state_t = util::safe_ptr>; using blob_t = util::safe_ptr>; using depth_stencil_state_t = util::safe_ptr>; using depth_stencil_view_t = util::safe_ptr>; using keyed_mutex_t = util::safe_ptr>; namespace video { using device_t = util::safe_ptr>; using ctx_t = util::safe_ptr>; using processor_t = util::safe_ptr>; using processor_out_t = util::safe_ptr>; using processor_in_t = util::safe_ptr>; using processor_enum_t = util::safe_ptr>; } // namespace video class hwdevice_t; struct cursor_t { std::vector img_data; DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info; int x, y; bool visible; }; class gpu_cursor_t { public: gpu_cursor_t(): cursor_view {0, 0, 0, 0, 0.0f, 1.0f} {}; void set_pos(LONG topleft_x, LONG topleft_y, LONG display_width, LONG display_height, DXGI_MODE_ROTATION display_rotation, bool visible) { this->topleft_x = topleft_x; this->topleft_y = topleft_y; this->display_width = display_width; this->display_height = display_height; this->display_rotation = display_rotation; this->visible = visible; update_viewport(); } void set_texture(LONG texture_width, LONG texture_height, texture2d_t &&texture) { this->texture = std::move(texture); this->texture_width = texture_width; this->texture_height = texture_height; update_viewport(); } void update_viewport() { switch (display_rotation) { case DXGI_MODE_ROTATION_UNSPECIFIED: case DXGI_MODE_ROTATION_IDENTITY: cursor_view.TopLeftX = topleft_x; cursor_view.TopLeftY = topleft_y; cursor_view.Width = texture_width; cursor_view.Height = texture_height; break; case DXGI_MODE_ROTATION_ROTATE90: cursor_view.TopLeftX = topleft_y; cursor_view.TopLeftY = display_width - texture_width - topleft_x; cursor_view.Width = texture_height; cursor_view.Height = texture_width; break; case DXGI_MODE_ROTATION_ROTATE180: cursor_view.TopLeftX = display_width - texture_width - topleft_x; cursor_view.TopLeftY = display_height - texture_height - topleft_y; cursor_view.Width = texture_width; cursor_view.Height = texture_height; break; case DXGI_MODE_ROTATION_ROTATE270: cursor_view.TopLeftX = display_height - texture_height - topleft_y; cursor_view.TopLeftY = topleft_x; cursor_view.Width = texture_height; cursor_view.Height = texture_width; break; } } texture2d_t texture; LONG texture_width; LONG texture_height; LONG topleft_x; LONG topleft_y; LONG display_width; LONG display_height; DXGI_MODE_ROTATION display_rotation; shader_res_t input_res; D3D11_VIEWPORT cursor_view; bool visible; }; class display_base_t: public display_t { public: int init(const ::video::config_t &config, const std::string &display_name); capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override; factory1_t factory; adapter_t adapter; output_t output; device_t device; device_ctx_t device_ctx; DXGI_RATIONAL display_refresh_rate; int display_refresh_rate_rounded; DXGI_MODE_ROTATION display_rotation = DXGI_MODE_ROTATION_UNSPECIFIED; int width_before_rotation; int height_before_rotation; int client_frame_rate; DXGI_FORMAT capture_format; D3D_FEATURE_LEVEL feature_level; std::unique_ptr timer = create_high_precision_timer(); typedef enum _D3DKMT_SCHEDULINGPRIORITYCLASS { D3DKMT_SCHEDULINGPRIORITYCLASS_IDLE, ///< Idle priority class D3DKMT_SCHEDULINGPRIORITYCLASS_BELOW_NORMAL, ///< Below normal priority class D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL, ///< Normal priority class D3DKMT_SCHEDULINGPRIORITYCLASS_ABOVE_NORMAL, ///< Above normal priority class D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH, ///< High priority class D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME ///< Realtime priority class } D3DKMT_SCHEDULINGPRIORITYCLASS; typedef UINT D3DKMT_HANDLE; typedef struct _D3DKMT_OPENADAPTERFROMLUID { LUID AdapterLuid; D3DKMT_HANDLE hAdapter; } D3DKMT_OPENADAPTERFROMLUID; typedef struct _D3DKMT_WDDM_2_7_CAPS { union { struct { UINT HwSchSupported : 1; UINT HwSchEnabled : 1; UINT HwSchEnabledByDefault : 1; UINT IndependentVidPnVSyncControl : 1; UINT Reserved : 28; }; UINT Value; }; } D3DKMT_WDDM_2_7_CAPS; typedef struct _D3DKMT_QUERYADAPTERINFO { D3DKMT_HANDLE hAdapter; UINT Type; VOID *pPrivateDriverData; UINT PrivateDriverDataSize; } D3DKMT_QUERYADAPTERINFO; const UINT KMTQAITYPE_WDDM_2_7_CAPS = 70; typedef struct _D3DKMT_CLOSEADAPTER { D3DKMT_HANDLE hAdapter; } D3DKMT_CLOSEADAPTER; typedef NTSTATUS(WINAPI *PD3DKMTSetProcessSchedulingPriorityClass)(HANDLE, D3DKMT_SCHEDULINGPRIORITYCLASS); typedef NTSTATUS(WINAPI *PD3DKMTOpenAdapterFromLuid)(D3DKMT_OPENADAPTERFROMLUID *); typedef NTSTATUS(WINAPI *PD3DKMTQueryAdapterInfo)(D3DKMT_QUERYADAPTERINFO *); typedef NTSTATUS(WINAPI *PD3DKMTCloseAdapter)(D3DKMT_CLOSEADAPTER *); virtual bool is_hdr() override; virtual bool get_hdr_metadata(SS_HDR_METADATA &metadata) override; const char *dxgi_format_to_string(DXGI_FORMAT format); const char *colorspace_to_string(DXGI_COLOR_SPACE_TYPE type); virtual std::vector get_supported_capture_formats() = 0; protected: int get_pixel_pitch() { return (capture_format == DXGI_FORMAT_R16G16B16A16_FLOAT) ? 8 : 4; } virtual capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) = 0; virtual capture_e release_snapshot() = 0; virtual int complete_img(img_t *img, bool dummy) = 0; }; /** * Display component for devices that use software encoders. */ class display_ram_t: public display_base_t { public: std::shared_ptr alloc_img() override; int dummy_img(img_t *img) override; int complete_img(img_t *img, bool dummy) override; std::vector get_supported_capture_formats() override; std::unique_ptr make_avcodec_encode_device(pix_fmt_e pix_fmt) override; D3D11_MAPPED_SUBRESOURCE img_info; texture2d_t texture; }; /** * Display component for devices that use hardware encoders. */ class display_vram_t: public display_base_t, public std::enable_shared_from_this { public: std::shared_ptr alloc_img() override; int dummy_img(img_t *img_base) override; int complete_img(img_t *img_base, bool dummy) override; std::vector get_supported_capture_formats() override; bool is_codec_supported(std::string_view name, const ::video::config_t &config) override; std::unique_ptr make_avcodec_encode_device(pix_fmt_e pix_fmt) override; std::unique_ptr make_nvenc_encode_device(pix_fmt_e pix_fmt) override; std::atomic next_image_id; }; /** * Display duplicator that uses the DirectX Desktop Duplication API. */ class duplication_t { public: dup_t dup; bool has_frame {}; std::chrono::steady_clock::time_point last_protected_content_warning_time {}; int init(display_base_t *display, const ::video::config_t &config); capture_e next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p); capture_e reset(dup_t::pointer dup_p = dup_t::pointer()); capture_e release_frame(); ~duplication_t(); }; /** * Display backend that uses DDAPI with a software encoder. */ class display_ddup_ram_t: public display_ram_t { public: int init(const ::video::config_t &config, const std::string &display_name); capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; capture_e release_snapshot() override; duplication_t dup; cursor_t cursor; }; /** * Display backend that uses DDAPI with a hardware encoder. */ class display_ddup_vram_t: public display_vram_t { public: int init(const ::video::config_t &config, const std::string &display_name); capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; capture_e release_snapshot() override; duplication_t dup; sampler_state_t sampler_linear; blend_t blend_alpha; blend_t blend_invert; blend_t blend_disable; ps_t cursor_ps; vs_t cursor_vs; gpu_cursor_t cursor_alpha; gpu_cursor_t cursor_xor; texture2d_t old_surface_delayed_destruction; std::chrono::steady_clock::time_point old_surface_timestamp; std::variant> last_frame_variant; }; /** * Display duplicator that uses the Windows.Graphics.Capture API. */ class wgc_capture_t { winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice uwp_device {nullptr}; winrt::Windows::Graphics::Capture::GraphicsCaptureItem item {nullptr}; winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool frame_pool {nullptr}; winrt::Windows::Graphics::Capture::GraphicsCaptureSession capture_session {nullptr}; winrt::Windows::Graphics::Capture::Direct3D11CaptureFrame produced_frame {nullptr}, consumed_frame {nullptr}; SRWLOCK frame_lock = SRWLOCK_INIT; CONDITION_VARIABLE frame_present_cv; void on_frame_arrived(winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const &sender, winrt::Windows::Foundation::IInspectable const &); public: wgc_capture_t(); ~wgc_capture_t(); int init(display_base_t *display, const ::video::config_t &config); capture_e next_frame(std::chrono::milliseconds timeout, ID3D11Texture2D **out, uint64_t &out_time); capture_e release_frame(); int set_cursor_visible(bool); }; /** * Display backend that uses Windows.Graphics.Capture with a software encoder. */ class display_wgc_ram_t: public display_ram_t { wgc_capture_t dup; public: int init(const ::video::config_t &config, const std::string &display_name); capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; capture_e release_snapshot() override; }; /** * Display backend that uses Windows.Graphics.Capture with a hardware encoder. */ class display_wgc_vram_t: public display_vram_t { wgc_capture_t dup; public: int init(const ::video::config_t &config, const std::string &display_name); capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; capture_e release_snapshot() override; }; } // namespace platf::dxgi ================================================ FILE: src/platform/windows/display_base.cpp ================================================ /** * @file src/platform/windows/display_base.cpp * @brief Definitions for the Windows display base code. */ // standard includes #include #include // platform includes #include // lib includes #include #include // We have to include boost/process/v1.hpp before display.h due to WinSock.h, // but that prevents the definition of NTSTATUS so we must define it ourself. typedef long NTSTATUS; // Definition from the WDK's d3dkmthk.h typedef enum _D3DKMT_GPU_PREFERENCE_QUERY_STATE: DWORD { D3DKMT_GPU_PREFERENCE_STATE_UNINITIALIZED, ///< The GPU preference isn't initialized. D3DKMT_GPU_PREFERENCE_STATE_HIGH_PERFORMANCE, ///< The highest performing GPU is preferred. D3DKMT_GPU_PREFERENCE_STATE_MINIMUM_POWER, ///< The minimum-powered GPU is preferred. D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED, ///< A GPU preference isn't specified. D3DKMT_GPU_PREFERENCE_STATE_NOT_FOUND, ///< A GPU preference isn't found. D3DKMT_GPU_PREFERENCE_STATE_USER_SPECIFIED_GPU ///< A specific GPU is preferred. } D3DKMT_GPU_PREFERENCE_QUERY_STATE; #include "display.h" #include "misc.h" #include "src/config.h" #include "src/display_device.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/video.h" namespace platf { using namespace std::literals; } namespace platf::dxgi { /** * DDAPI-specific initialization goes here. */ int duplication_t::init(display_base_t *display, const ::video::config_t &config) { HRESULT status; // Capture format will be determined from the first call to AcquireNextFrame() display->capture_format = DXGI_FORMAT_UNKNOWN; // FIXME: Duplicate output on RX580 in combination with DOOM (2016) --> BSOD { // IDXGIOutput5 is optional, but can provide improved performance and wide color support dxgi::output5_t output5 {}; status = display->output->QueryInterface(IID_IDXGIOutput5, (void **) &output5); if (SUCCEEDED(status)) { // Ask the display implementation which formats it supports auto supported_formats = display->get_supported_capture_formats(); if (supported_formats.empty()) { BOOST_LOG(warning) << "No compatible capture formats for this encoder"sv; return -1; } // We try this twice, in case we still get an error on reinitialization for (int x = 0; x < 2; ++x) { // Ensure we can duplicate the current display syncThreadDesktop(); status = output5->DuplicateOutput1((IUnknown *) display->device.get(), 0, supported_formats.size(), supported_formats.data(), &dup); if (SUCCEEDED(status)) { break; } std::this_thread::sleep_for(200ms); } // We don't retry with DuplicateOutput() because we can hit this codepath when we're racing // with mode changes and we don't want to accidentally fall back to suboptimal capture if // we get unlucky and succeed below. if (FAILED(status)) { BOOST_LOG(warning) << "DuplicateOutput1 Failed [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } } else { BOOST_LOG(warning) << "IDXGIOutput5 is not supported by your OS. Capture performance may be reduced."sv; dxgi::output1_t output1 {}; status = display->output->QueryInterface(IID_IDXGIOutput1, (void **) &output1); if (FAILED(status)) { BOOST_LOG(error) << "Failed to query IDXGIOutput1 from the output"sv; return -1; } for (int x = 0; x < 2; ++x) { // Ensure we can duplicate the current display syncThreadDesktop(); status = output1->DuplicateOutput((IUnknown *) display->device.get(), &dup); if (SUCCEEDED(status)) { break; } std::this_thread::sleep_for(200ms); } if (FAILED(status)) { BOOST_LOG(error) << "DuplicateOutput Failed [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } } } DXGI_OUTDUPL_DESC dup_desc; dup->GetDesc(&dup_desc); BOOST_LOG(info) << "Desktop resolution ["sv << dup_desc.ModeDesc.Width << 'x' << dup_desc.ModeDesc.Height << ']'; BOOST_LOG(info) << "Desktop format ["sv << display->dxgi_format_to_string(dup_desc.ModeDesc.Format) << ']'; display->display_refresh_rate = dup_desc.ModeDesc.RefreshRate; double display_refresh_rate_decimal = (double) display->display_refresh_rate.Numerator / display->display_refresh_rate.Denominator; BOOST_LOG(info) << "Display refresh rate [" << display_refresh_rate_decimal << "Hz]"; BOOST_LOG(info) << "Requested frame rate [" << display->client_frame_rate << "fps]"; display->display_refresh_rate_rounded = lround(display_refresh_rate_decimal); return 0; } capture_e duplication_t::next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p) { auto capture_status = release_frame(); if (capture_status != capture_e::ok) { return capture_status; } auto status = dup->AcquireNextFrame(timeout.count(), &frame_info, res_p); switch (status) { case S_OK: // ProtectedContentMaskedOut seems to semi-randomly be TRUE or FALSE even when protected content // is on screen the whole time, so we can't just print when it changes. Instead we'll keep track // of the last time we printed the warning and print another if we haven't printed one recently. if (frame_info.ProtectedContentMaskedOut && std::chrono::steady_clock::now() > last_protected_content_warning_time + 10s) { BOOST_LOG(warning) << "Windows is currently blocking DRM-protected content from capture. You may see black regions where this content would be."sv; last_protected_content_warning_time = std::chrono::steady_clock::now(); } has_frame = true; return capture_e::ok; case DXGI_ERROR_WAIT_TIMEOUT: return capture_e::timeout; case WAIT_ABANDONED: case DXGI_ERROR_ACCESS_LOST: case DXGI_ERROR_ACCESS_DENIED: return capture_e::reinit; default: BOOST_LOG(error) << "Couldn't acquire next frame [0x"sv << util::hex(status).to_string_view(); return capture_e::error; } } capture_e duplication_t::reset(dup_t::pointer dup_p) { auto capture_status = release_frame(); dup.reset(dup_p); return capture_status; } capture_e duplication_t::release_frame() { if (!has_frame) { return capture_e::ok; } auto status = dup->ReleaseFrame(); has_frame = false; switch (status) { case S_OK: return capture_e::ok; case DXGI_ERROR_INVALID_CALL: BOOST_LOG(warning) << "Duplication frame already released"; return capture_e::ok; case DXGI_ERROR_ACCESS_LOST: return capture_e::reinit; default: BOOST_LOG(error) << "Error while releasing duplication frame [0x"sv << util::hex(status).to_string_view(); return capture_e::error; } } duplication_t::~duplication_t() { release_frame(); } capture_e 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) { auto adjust_client_frame_rate = [&]() -> DXGI_RATIONAL { // Adjust capture frame interval when display refresh rate is not integral but very close to requested fps. if (display_refresh_rate.Denominator > 1) { DXGI_RATIONAL candidate = display_refresh_rate; if (client_frame_rate % display_refresh_rate_rounded == 0) { candidate.Numerator *= client_frame_rate / display_refresh_rate_rounded; } else if (display_refresh_rate_rounded % client_frame_rate == 0) { candidate.Denominator *= display_refresh_rate_rounded / client_frame_rate; } double candidate_rate = (double) candidate.Numerator / candidate.Denominator; // Can only decrease requested fps, otherwise client may start accumulating frames and suffer increased latency. if (client_frame_rate > candidate_rate && candidate_rate / client_frame_rate > 0.99) { BOOST_LOG(info) << "Adjusted capture rate to " << candidate_rate << "fps to better match display"; return candidate; } } return {(uint32_t) client_frame_rate, 1}; }; DXGI_RATIONAL client_frame_rate_adjusted = adjust_client_frame_rate(); std::optional frame_pacing_group_start; uint32_t frame_pacing_group_frames = 0; // Keep the display awake during capture. If the display goes to sleep during // capture, best case is that capture stops until it powers back on. However, // worst case it will trigger us to reinit DD, waking the display back up in // a neverending cycle of waking and sleeping the display of an idle machine. SetThreadExecutionState(ES_CONTINUOUS | ES_DISPLAY_REQUIRED); auto clear_display_required = util::fail_guard([]() { SetThreadExecutionState(ES_CONTINUOUS); }); sleep_overshoot_logger.reset(); while (true) { // This will return false if the HDR state changes or for any number of other // display or GPU changes. We should reinit to examine the updated state of // the display subsystem. It is recommended to call this once per frame. if (!factory->IsCurrent()) { return platf::capture_e::reinit; } platf::capture_e status = capture_e::ok; std::shared_ptr img_out; // Try to continue frame pacing group, snapshot() is called with zero timeout after waiting for client frame interval if (frame_pacing_group_start) { const uint32_t seconds = (uint64_t) frame_pacing_group_frames * client_frame_rate_adjusted.Denominator / client_frame_rate_adjusted.Numerator; const uint32_t remainder = (uint64_t) frame_pacing_group_frames * client_frame_rate_adjusted.Denominator % client_frame_rate_adjusted.Numerator; const auto sleep_target = *frame_pacing_group_start + std::chrono::nanoseconds(1s) * seconds + std::chrono::nanoseconds(1s) * remainder / client_frame_rate_adjusted.Numerator; const auto sleep_period = sleep_target - std::chrono::steady_clock::now(); if (sleep_period <= 0ns) { // We missed next frame time, invalidating current frame pacing group frame_pacing_group_start = std::nullopt; frame_pacing_group_frames = 0; status = capture_e::timeout; } else { bool elastic = false; if (sleep_period >= 2ms) { elastic = true; timer->sleep_for(sleep_period - 2ms); sleep_overshoot_logger.first_point(sleep_target); sleep_overshoot_logger.second_point_now_and_log(); } status = snapshot(pull_free_image_cb, img_out, elastic ? 2ms : std::chrono::duration_cast(sleep_period), *cursor); if (status == capture_e::ok && img_out) { frame_pacing_group_frames += 1; } else { frame_pacing_group_start = std::nullopt; frame_pacing_group_frames = 0; } } } // Start new frame pacing group if necessary, snapshot() is called with non-zero timeout if (status == capture_e::timeout || (status == capture_e::ok && !frame_pacing_group_start)) { status = snapshot(pull_free_image_cb, img_out, 200ms, *cursor); if (status == capture_e::ok && img_out) { frame_pacing_group_start = img_out->frame_timestamp; if (!frame_pacing_group_start) { BOOST_LOG(warning) << "snapshot() provided image without timestamp"; frame_pacing_group_start = std::chrono::steady_clock::now(); } frame_pacing_group_frames = 1; } else if (status == platf::capture_e::timeout) { // The D3D11 device is protected by an unfair lock that is held the entire time that // IDXGIOutputDuplication::AcquireNextFrame() is running. This is normally harmless, // however sometimes the encoding thread needs to interact with our ID3D11Device to // create dummy images or initialize the shared state that is used to pass textures // between the capture and encoding ID3D11Devices. // // When we're in a state where we're not actively receiving frames regularly, we will // spend almost 100% of our time in AcquireNextFrame() holding that critical lock. // Worse still, since it's unfair, we can monopolize it while the encoding thread // is starved. The encoding thread may acquire it for a few moments across a few // ID3D11Device calls before losing it again to us for another long time waiting in // AcquireNextFrame(). The starvation caused by this lock contention causes encoder // reinitialization to take several seconds instead of a fraction of a second. // // To avoid starving the encoding thread, sleep without the lock held for a little // while each time we reach our max frame timeout. This will only happen when nothing // is updating the display, so no visible stutter should be introduced by the sleep. std::this_thread::sleep_for(10ms); } } switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: case platf::capture_e::interrupted: return status; case platf::capture_e::timeout: if (!push_captured_image_cb(std::move(img_out), false)) { return capture_e::ok; } break; case platf::capture_e::ok: if (!push_captured_image_cb(std::move(img_out), true)) { return capture_e::ok; } break; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; return status; } status = release_snapshot(); if (status != platf::capture_e::ok) { return status; } } return capture_e::ok; } /** * @brief Tests to determine if the Desktop Duplication API can capture the given output. * @details When testing for enumeration only, we avoid resyncing the thread desktop. * @param adapter The DXGI adapter to use for capture. * @param output The DXGI output to capture. * @param enumeration_only Specifies whether this test is occurring for display enumeration. */ bool test_dxgi_duplication(adapter_t &adapter, output_t &output, bool enumeration_only) { D3D_FEATURE_LEVEL featureLevels[] { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0, D3D_FEATURE_LEVEL_9_3, D3D_FEATURE_LEVEL_9_2, D3D_FEATURE_LEVEL_9_1 }; device_t device; auto status = D3D11CreateDevice( adapter.get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr, D3D11_CREATE_DEVICE_FLAGS, featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), D3D11_SDK_VERSION, &device, nullptr, nullptr ); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create D3D11 device for DD test [0x"sv << util::hex(status).to_string_view() << ']'; return false; } output1_t output1; status = output->QueryInterface(IID_IDXGIOutput1, (void **) &output1); if (FAILED(status)) { BOOST_LOG(error) << "Failed to query IDXGIOutput1 from the output"sv; return false; } // Check if we can use the Desktop Duplication API on this output for (int x = 0; x < 2; ++x) { dup_t dup; // Only resynchronize the thread desktop when not enumerating displays. // During enumeration, the caller will do this only once to ensure // a consistent view of available outputs. if (!enumeration_only) { syncThreadDesktop(); } status = output1->DuplicateOutput((IUnknown *) device.get(), &dup); if (SUCCEEDED(status)) { return true; } // If we're not resyncing the thread desktop and we don't have permission to // capture the current desktop, just bail immediately. Retrying won't help. if (enumeration_only && status == E_ACCESSDENIED) { break; } else { std::this_thread::sleep_for(200ms); } } BOOST_LOG(error) << "DuplicateOutput() test failed [0x"sv << util::hex(status).to_string_view() << ']'; return false; } /** * @brief Hook for NtGdiDdDDIGetCachedHybridQueryValue() from win32u.dll. * @param gpuPreference A pointer to the location where the preference will be written. * @return Always STATUS_SUCCESS if valid arguments are provided. */ NTSTATUS __stdcall NtGdiDdDDIGetCachedHybridQueryValueHook(D3DKMT_GPU_PREFERENCE_QUERY_STATE *gpuPreference) { // By faking a cached GPU preference state of D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED, this will // prevent DXGI from performing the normal GPU preference resolution that looks at the registry, // power settings, and the hybrid adapter DDI interface to pick a GPU. Instead, we will not be // bound to any specific GPU. This will prevent DXGI from performing output reparenting (moving // outputs from their true location to the render GPU), which breaks DDA. if (gpuPreference) { *gpuPreference = D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED; return 0; // STATUS_SUCCESS } else { return STATUS_INVALID_PARAMETER; } } int display_base_t::init(const ::video::config_t &config, const std::string &display_name) { std::once_flag windows_cpp_once_flag; std::call_once(windows_cpp_once_flag, []() { DECLARE_HANDLE(DPI_AWARENESS_CONTEXT); typedef BOOL (*User32_SetProcessDpiAwarenessContext)(DPI_AWARENESS_CONTEXT value); { auto user32 = LoadLibraryA("user32.dll"); auto f = (User32_SetProcessDpiAwarenessContext) GetProcAddress(user32, "SetProcessDpiAwarenessContext"); if (f) { f(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); } FreeLibrary(user32); } { // We aren't calling MH_Uninitialize(), but that's okay because this hook lasts for the life of the process MH_Initialize(); MH_CreateHookApi(L"win32u.dll", "NtGdiDdDDIGetCachedHybridQueryValue", (void *) NtGdiDdDDIGetCachedHybridQueryValueHook, nullptr); MH_EnableHook(MH_ALL_HOOKS); } }); // Get rectangle of full desktop for absolute mouse coordinates env_width = GetSystemMetrics(SM_CXVIRTUALSCREEN); env_height = GetSystemMetrics(SM_CYVIRTUALSCREEN); HRESULT status; status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } auto adapter_name = from_utf8(config::video.adapter_name); auto output_name = from_utf8(display_name); adapter_t::pointer adapter_p; for (int tries = 0; tries < 2; ++tries) { for (int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) { dxgi::adapter_t adapter_tmp {adapter_p}; DXGI_ADAPTER_DESC1 adapter_desc; adapter_tmp->GetDesc1(&adapter_desc); if (!adapter_name.empty() && adapter_desc.Description != adapter_name) { continue; } dxgi::output_t::pointer output_p; for (int y = 0; adapter_tmp->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) { dxgi::output_t output_tmp {output_p}; DXGI_OUTPUT_DESC desc; output_tmp->GetDesc(&desc); if (!output_name.empty() && desc.DeviceName != output_name) { continue; } if (desc.AttachedToDesktop && test_dxgi_duplication(adapter_tmp, output_tmp, false)) { output = std::move(output_tmp); offset_x = desc.DesktopCoordinates.left; offset_y = desc.DesktopCoordinates.top; width = desc.DesktopCoordinates.right - offset_x; height = desc.DesktopCoordinates.bottom - offset_y; display_rotation = desc.Rotation; if (display_rotation == DXGI_MODE_ROTATION_ROTATE90 || display_rotation == DXGI_MODE_ROTATION_ROTATE270) { width_before_rotation = height; height_before_rotation = width; } else { width_before_rotation = width; height_before_rotation = height; } // left and bottom may be negative, yet absolute mouse coordinates start at 0x0 // Ensure offset starts at 0x0 offset_x -= GetSystemMetrics(SM_XVIRTUALSCREEN); offset_y -= GetSystemMetrics(SM_YVIRTUALSCREEN); break; } } if (output) { adapter = std::move(adapter_tmp); break; } } if (output) { break; } // If we made it here without finding an output, try to power on the display and retry. if (tries == 0) { SetThreadExecutionState(ES_DISPLAY_REQUIRED); Sleep(500); } } if (!output) { BOOST_LOG(error) << "Failed to locate an output device"sv; return -1; } D3D_FEATURE_LEVEL featureLevels[] { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0, D3D_FEATURE_LEVEL_9_3, D3D_FEATURE_LEVEL_9_2, D3D_FEATURE_LEVEL_9_1 }; status = adapter->QueryInterface(IID_IDXGIAdapter, (void **) &adapter_p); if (FAILED(status)) { BOOST_LOG(error) << "Failed to query IDXGIAdapter interface"sv; return -1; } status = D3D11CreateDevice( adapter_p, D3D_DRIVER_TYPE_UNKNOWN, nullptr, D3D11_CREATE_DEVICE_FLAGS, featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), D3D11_SDK_VERSION, &device, &feature_level, &device_ctx ); adapter_p->Release(); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create D3D11 device [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } DXGI_ADAPTER_DESC adapter_desc; adapter->GetDesc(&adapter_desc); auto description = to_utf8(adapter_desc.Description); BOOST_LOG(info) << std::endl << "Device Description : " << description << std::endl << "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl << "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl << "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl << "Device Sys Mem : "sv << adapter_desc.DedicatedSystemMemory / 1048576 << " MiB"sv << std::endl << "Share Sys Mem : "sv << adapter_desc.SharedSystemMemory / 1048576 << " MiB"sv << std::endl << "Feature Level : 0x"sv << util::hex(feature_level).to_string_view() << std::endl << "Capture size : "sv << width << 'x' << height << std::endl << "Offset : "sv << offset_x << 'x' << offset_y << std::endl << "Virtual Desktop : "sv << env_width << 'x' << env_height; // Bump up thread priority { const DWORD flags = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY; TOKEN_PRIVILEGES tp; HANDLE token; LUID val; if (OpenProcessToken(GetCurrentProcess(), flags, &token) && !!LookupPrivilegeValue(nullptr, SE_INC_BASE_PRIORITY_NAME, &val)) { tp.PrivilegeCount = 1; tp.Privileges[0].Luid = val; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; if (!AdjustTokenPrivileges(token, false, &tp, sizeof(tp), nullptr, nullptr)) { BOOST_LOG(warning) << "Could not set privilege to increase GPU priority"; } } CloseHandle(token); HMODULE gdi32 = GetModuleHandleA("GDI32"); if (gdi32) { auto check_hags = [&](const LUID &adapter) -> bool { auto d3dkmt_open_adapter = (PD3DKMTOpenAdapterFromLuid) GetProcAddress(gdi32, "D3DKMTOpenAdapterFromLuid"); auto d3dkmt_query_adapter_info = (PD3DKMTQueryAdapterInfo) GetProcAddress(gdi32, "D3DKMTQueryAdapterInfo"); auto d3dkmt_close_adapter = (PD3DKMTCloseAdapter) GetProcAddress(gdi32, "D3DKMTCloseAdapter"); if (!d3dkmt_open_adapter || !d3dkmt_query_adapter_info || !d3dkmt_close_adapter) { BOOST_LOG(error) << "Couldn't load d3dkmt functions from gdi32.dll to determine GPU HAGS status"; return false; } D3DKMT_OPENADAPTERFROMLUID d3dkmt_adapter = {adapter}; if (FAILED(d3dkmt_open_adapter(&d3dkmt_adapter))) { BOOST_LOG(error) << "D3DKMTOpenAdapterFromLuid() failed while trying to determine GPU HAGS status"; return false; } bool result; D3DKMT_WDDM_2_7_CAPS d3dkmt_adapter_caps = {}; D3DKMT_QUERYADAPTERINFO d3dkmt_adapter_info = {}; d3dkmt_adapter_info.hAdapter = d3dkmt_adapter.hAdapter; d3dkmt_adapter_info.Type = KMTQAITYPE_WDDM_2_7_CAPS; d3dkmt_adapter_info.pPrivateDriverData = &d3dkmt_adapter_caps; d3dkmt_adapter_info.PrivateDriverDataSize = sizeof(d3dkmt_adapter_caps); if (SUCCEEDED(d3dkmt_query_adapter_info(&d3dkmt_adapter_info))) { result = d3dkmt_adapter_caps.HwSchEnabled; } else { BOOST_LOG(warning) << "D3DKMTQueryAdapterInfo() failed while trying to determine GPU HAGS status"; result = false; } D3DKMT_CLOSEADAPTER d3dkmt_close_adapter_wrap = {d3dkmt_adapter.hAdapter}; if (FAILED(d3dkmt_close_adapter(&d3dkmt_close_adapter_wrap))) { BOOST_LOG(error) << "D3DKMTCloseAdapter() failed while trying to determine GPU HAGS status"; } return result; }; auto d3dkmt_set_process_priority = (PD3DKMTSetProcessSchedulingPriorityClass) GetProcAddress(gdi32, "D3DKMTSetProcessSchedulingPriorityClass"); if (d3dkmt_set_process_priority) { auto priority = D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME; bool hags_enabled = check_hags(adapter_desc.AdapterLuid); if (adapter_desc.VendorId == 0x10DE) { // As of 2023.07, NVIDIA driver has unfixed bug(s) where "realtime" can cause unrecoverable encoding freeze or outright driver crash // This issue happens more frequently with HAGS, in DX12 games or when VRAM is filled close to max capacity // Track OBS to see if they find better workaround or NVIDIA fixes it on their end, they seem to be in communication if (hags_enabled && !config::video.nv_realtime_hags) { priority = D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH; } } BOOST_LOG(info) << "Active GPU has HAGS " << (hags_enabled ? "enabled" : "disabled"); BOOST_LOG(info) << "Using " << (priority == D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH ? "high" : "realtime") << " GPU priority"; if (FAILED(d3dkmt_set_process_priority(GetCurrentProcess(), priority))) { BOOST_LOG(warning) << "Failed to adjust GPU priority. Please run application as administrator for optimal performance."; } } else { BOOST_LOG(error) << "Couldn't load D3DKMTSetProcessSchedulingPriorityClass function from gdi32.dll to adjust GPU priority"; } } dxgi::dxgi_t dxgi; status = device->QueryInterface(IID_IDXGIDevice, (void **) &dxgi); if (FAILED(status)) { BOOST_LOG(warning) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } status = dxgi->SetGPUThreadPriority(0x4000001E); if (FAILED(status)) { BOOST_LOG(info) << "Failed to request absoloute capture GPU thread priority. Trying relative priority."; status = dxgi->SetGPUThreadPriority(7); if (FAILED(status)) { BOOST_LOG(warning) << "Failed to request relative capture GPU thread priority. Please run application as administrator for optimal performance."; } else { BOOST_LOG(info) << "Relative capture GPU thread priority request success."; } } } // Try to reduce latency { dxgi::dxgi1_t dxgi {}; status = device->QueryInterface(IID_IDXGIDevice, (void **) &dxgi); if (FAILED(status)) { BOOST_LOG(error) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } status = dxgi->SetMaximumFrameLatency(1); if (FAILED(status)) { BOOST_LOG(warning) << "Failed to set maximum frame latency [0x"sv << util::hex(status).to_string_view() << ']'; } } client_frame_rate = config.framerate; dxgi::output6_t output6 {}; status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6); if (SUCCEEDED(status)) { DXGI_OUTPUT_DESC1 desc1; output6->GetDesc1(&desc1); BOOST_LOG(info) << std::endl << "Colorspace : "sv << colorspace_to_string(desc1.ColorSpace) << std::endl << "Bits Per Color : "sv << desc1.BitsPerColor << std::endl << "Red Primary : ["sv << desc1.RedPrimary[0] << ',' << desc1.RedPrimary[1] << ']' << std::endl << "Green Primary : ["sv << desc1.GreenPrimary[0] << ',' << desc1.GreenPrimary[1] << ']' << std::endl << "Blue Primary : ["sv << desc1.BluePrimary[0] << ',' << desc1.BluePrimary[1] << ']' << std::endl << "White Point : ["sv << desc1.WhitePoint[0] << ',' << desc1.WhitePoint[1] << ']' << std::endl << "Min Luminance : "sv << desc1.MinLuminance << " nits"sv << std::endl << "Max Luminance : "sv << desc1.MaxLuminance << " nits"sv << std::endl << "Max Full Luminance : "sv << desc1.MaxFullFrameLuminance << " nits"sv; } if (!timer || !*timer) { BOOST_LOG(error) << "Uninitialized high precision timer"; return -1; } return 0; } bool display_base_t::is_hdr() { dxgi::output6_t output6 {}; auto status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6); if (FAILED(status)) { BOOST_LOG(warning) << "Failed to query IDXGIOutput6 from the output"sv; return false; } DXGI_OUTPUT_DESC1 desc1; output6->GetDesc1(&desc1); return desc1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020; } bool display_base_t::get_hdr_metadata(SS_HDR_METADATA &metadata) { dxgi::output6_t output6 {}; std::memset(&metadata, 0, sizeof(metadata)); auto status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6); if (FAILED(status)) { BOOST_LOG(warning) << "Failed to query IDXGIOutput6 from the output"sv; return false; } DXGI_OUTPUT_DESC1 desc1; output6->GetDesc1(&desc1); // The primaries reported here seem to correspond to scRGB (Rec. 709) // which we then convert to Rec 2020 in our scRGB FP16 -> PQ shader // prior to encoding. It's not clear to me if we're supposed to report // the primaries of the original colorspace or the one we've converted // it to, but let's just report Rec 2020 primaries and D65 white level // to avoid confusing clients by reporting Rec 709 primaries with a // Rec 2020 colorspace. It seems like most clients ignore the primaries // in the metadata anyway (luminance range is most important). desc1.RedPrimary[0] = 0.708f; desc1.RedPrimary[1] = 0.292f; desc1.GreenPrimary[0] = 0.170f; desc1.GreenPrimary[1] = 0.797f; desc1.BluePrimary[0] = 0.131f; desc1.BluePrimary[1] = 0.046f; desc1.WhitePoint[0] = 0.3127f; desc1.WhitePoint[1] = 0.3290f; metadata.displayPrimaries[0].x = desc1.RedPrimary[0] * 50000; metadata.displayPrimaries[0].y = desc1.RedPrimary[1] * 50000; metadata.displayPrimaries[1].x = desc1.GreenPrimary[0] * 50000; metadata.displayPrimaries[1].y = desc1.GreenPrimary[1] * 50000; metadata.displayPrimaries[2].x = desc1.BluePrimary[0] * 50000; metadata.displayPrimaries[2].y = desc1.BluePrimary[1] * 50000; metadata.whitePoint.x = desc1.WhitePoint[0] * 50000; metadata.whitePoint.y = desc1.WhitePoint[1] * 50000; metadata.maxDisplayLuminance = desc1.MaxLuminance; metadata.minDisplayLuminance = desc1.MinLuminance * 10000; // These are content-specific metadata parameters that this interface doesn't give us metadata.maxContentLightLevel = 0; metadata.maxFrameAverageLightLevel = 0; metadata.maxFullFrameLuminance = desc1.MaxFullFrameLuminance; return true; } const char *format_str[] = { "DXGI_FORMAT_UNKNOWN", "DXGI_FORMAT_R32G32B32A32_TYPELESS", "DXGI_FORMAT_R32G32B32A32_FLOAT", "DXGI_FORMAT_R32G32B32A32_UINT", "DXGI_FORMAT_R32G32B32A32_SINT", "DXGI_FORMAT_R32G32B32_TYPELESS", "DXGI_FORMAT_R32G32B32_FLOAT", "DXGI_FORMAT_R32G32B32_UINT", "DXGI_FORMAT_R32G32B32_SINT", "DXGI_FORMAT_R16G16B16A16_TYPELESS", "DXGI_FORMAT_R16G16B16A16_FLOAT", "DXGI_FORMAT_R16G16B16A16_UNORM", "DXGI_FORMAT_R16G16B16A16_UINT", "DXGI_FORMAT_R16G16B16A16_SNORM", "DXGI_FORMAT_R16G16B16A16_SINT", "DXGI_FORMAT_R32G32_TYPELESS", "DXGI_FORMAT_R32G32_FLOAT", "DXGI_FORMAT_R32G32_UINT", "DXGI_FORMAT_R32G32_SINT", "DXGI_FORMAT_R32G8X24_TYPELESS", "DXGI_FORMAT_D32_FLOAT_S8X24_UINT", "DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS", "DXGI_FORMAT_X32_TYPELESS_G8X24_UINT", "DXGI_FORMAT_R10G10B10A2_TYPELESS", "DXGI_FORMAT_R10G10B10A2_UNORM", "DXGI_FORMAT_R10G10B10A2_UINT", "DXGI_FORMAT_R11G11B10_FLOAT", "DXGI_FORMAT_R8G8B8A8_TYPELESS", "DXGI_FORMAT_R8G8B8A8_UNORM", "DXGI_FORMAT_R8G8B8A8_UNORM_SRGB", "DXGI_FORMAT_R8G8B8A8_UINT", "DXGI_FORMAT_R8G8B8A8_SNORM", "DXGI_FORMAT_R8G8B8A8_SINT", "DXGI_FORMAT_R16G16_TYPELESS", "DXGI_FORMAT_R16G16_FLOAT", "DXGI_FORMAT_R16G16_UNORM", "DXGI_FORMAT_R16G16_UINT", "DXGI_FORMAT_R16G16_SNORM", "DXGI_FORMAT_R16G16_SINT", "DXGI_FORMAT_R32_TYPELESS", "DXGI_FORMAT_D32_FLOAT", "DXGI_FORMAT_R32_FLOAT", "DXGI_FORMAT_R32_UINT", "DXGI_FORMAT_R32_SINT", "DXGI_FORMAT_R24G8_TYPELESS", "DXGI_FORMAT_D24_UNORM_S8_UINT", "DXGI_FORMAT_R24_UNORM_X8_TYPELESS", "DXGI_FORMAT_X24_TYPELESS_G8_UINT", "DXGI_FORMAT_R8G8_TYPELESS", "DXGI_FORMAT_R8G8_UNORM", "DXGI_FORMAT_R8G8_UINT", "DXGI_FORMAT_R8G8_SNORM", "DXGI_FORMAT_R8G8_SINT", "DXGI_FORMAT_R16_TYPELESS", "DXGI_FORMAT_R16_FLOAT", "DXGI_FORMAT_D16_UNORM", "DXGI_FORMAT_R16_UNORM", "DXGI_FORMAT_R16_UINT", "DXGI_FORMAT_R16_SNORM", "DXGI_FORMAT_R16_SINT", "DXGI_FORMAT_R8_TYPELESS", "DXGI_FORMAT_R8_UNORM", "DXGI_FORMAT_R8_UINT", "DXGI_FORMAT_R8_SNORM", "DXGI_FORMAT_R8_SINT", "DXGI_FORMAT_A8_UNORM", "DXGI_FORMAT_R1_UNORM", "DXGI_FORMAT_R9G9B9E5_SHAREDEXP", "DXGI_FORMAT_R8G8_B8G8_UNORM", "DXGI_FORMAT_G8R8_G8B8_UNORM", "DXGI_FORMAT_BC1_TYPELESS", "DXGI_FORMAT_BC1_UNORM", "DXGI_FORMAT_BC1_UNORM_SRGB", "DXGI_FORMAT_BC2_TYPELESS", "DXGI_FORMAT_BC2_UNORM", "DXGI_FORMAT_BC2_UNORM_SRGB", "DXGI_FORMAT_BC3_TYPELESS", "DXGI_FORMAT_BC3_UNORM", "DXGI_FORMAT_BC3_UNORM_SRGB", "DXGI_FORMAT_BC4_TYPELESS", "DXGI_FORMAT_BC4_UNORM", "DXGI_FORMAT_BC4_SNORM", "DXGI_FORMAT_BC5_TYPELESS", "DXGI_FORMAT_BC5_UNORM", "DXGI_FORMAT_BC5_SNORM", "DXGI_FORMAT_B5G6R5_UNORM", "DXGI_FORMAT_B5G5R5A1_UNORM", "DXGI_FORMAT_B8G8R8A8_UNORM", "DXGI_FORMAT_B8G8R8X8_UNORM", "DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM", "DXGI_FORMAT_B8G8R8A8_TYPELESS", "DXGI_FORMAT_B8G8R8A8_UNORM_SRGB", "DXGI_FORMAT_B8G8R8X8_TYPELESS", "DXGI_FORMAT_B8G8R8X8_UNORM_SRGB", "DXGI_FORMAT_BC6H_TYPELESS", "DXGI_FORMAT_BC6H_UF16", "DXGI_FORMAT_BC6H_SF16", "DXGI_FORMAT_BC7_TYPELESS", "DXGI_FORMAT_BC7_UNORM", "DXGI_FORMAT_BC7_UNORM_SRGB", "DXGI_FORMAT_AYUV", "DXGI_FORMAT_Y410", "DXGI_FORMAT_Y416", "DXGI_FORMAT_NV12", "DXGI_FORMAT_P010", "DXGI_FORMAT_P016", "DXGI_FORMAT_420_OPAQUE", "DXGI_FORMAT_YUY2", "DXGI_FORMAT_Y210", "DXGI_FORMAT_Y216", "DXGI_FORMAT_NV11", "DXGI_FORMAT_AI44", "DXGI_FORMAT_IA44", "DXGI_FORMAT_P8", "DXGI_FORMAT_A8P8", "DXGI_FORMAT_B4G4R4A4_UNORM", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, "DXGI_FORMAT_P208", "DXGI_FORMAT_V208", "DXGI_FORMAT_V408" }; const char *display_base_t::dxgi_format_to_string(DXGI_FORMAT format) { return format_str[format]; } const char *display_base_t::colorspace_to_string(DXGI_COLOR_SPACE_TYPE type) { const char *type_str[] = { "DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709", "DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709", "DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P709", "DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P2020", "DXGI_COLOR_SPACE_RESERVED", "DXGI_COLOR_SPACE_YCBCR_FULL_G22_NONE_P709_X601", "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P601", "DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P601", "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709", "DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P709", "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P2020", "DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P2020", "DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020", "DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_LEFT_P2020", "DXGI_COLOR_SPACE_RGB_STUDIO_G2084_NONE_P2020", "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_TOPLEFT_P2020", "DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_TOPLEFT_P2020", "DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P2020", "DXGI_COLOR_SPACE_YCBCR_STUDIO_GHLG_TOPLEFT_P2020", "DXGI_COLOR_SPACE_YCBCR_FULL_GHLG_TOPLEFT_P2020", "DXGI_COLOR_SPACE_RGB_STUDIO_G24_NONE_P709", "DXGI_COLOR_SPACE_RGB_STUDIO_G24_NONE_P2020", "DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_LEFT_P709", "DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_LEFT_P2020", "DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_TOPLEFT_P2020", }; if (type < ARRAYSIZE(type_str)) { return type_str[type]; } else { return "UNKNOWN"; } } } // namespace platf::dxgi namespace platf { /** * Pick a display adapter and capture method. * @param hwdevice_type enables possible use of hardware encoder */ std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { if (config::video.capture == "ddx" || config::video.capture.empty()) { if (hwdevice_type == mem_type_e::dxgi) { auto disp = std::make_shared(); if (!disp->init(config, display_name)) { return disp; } } else if (hwdevice_type == mem_type_e::system) { auto disp = std::make_shared(); if (!disp->init(config, display_name)) { return disp; } } } if (config::video.capture == "wgc" || config::video.capture.empty()) { if (hwdevice_type == mem_type_e::dxgi) { auto disp = std::make_shared(); if (!disp->init(config, display_name)) { return disp; } } else if (hwdevice_type == mem_type_e::system) { auto disp = std::make_shared(); if (!disp->init(config, display_name)) { return disp; } } } // ddx and wgc failed return nullptr; } std::vector display_names(mem_type_e) { std::vector display_names; HRESULT status; BOOST_LOG(debug) << "Detecting monitors..."sv; // We sync the thread desktop once before we start the enumeration process // to ensure test_dxgi_duplication() returns consistent results for all GPUs // even if the current desktop changes during our enumeration process. // It is critical that we either fully succeed in enumeration or fully fail, // otherwise it can lead to the capture code switching monitors unexpectedly. syncThreadDesktop(); dxgi::factory1_t factory; status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']'; return {}; } dxgi::adapter_t adapter; for (int x = 0; factory->EnumAdapters1(x, &adapter) != DXGI_ERROR_NOT_FOUND; ++x) { DXGI_ADAPTER_DESC1 adapter_desc; adapter->GetDesc1(&adapter_desc); BOOST_LOG(debug) << std::endl << "====== ADAPTER ====="sv << std::endl << "Device Name : "sv << to_utf8(adapter_desc.Description) << std::endl << "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl << "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl << "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl << "Device Sys Mem : "sv << adapter_desc.DedicatedSystemMemory / 1048576 << " MiB"sv << std::endl << "Share Sys Mem : "sv << adapter_desc.SharedSystemMemory / 1048576 << " MiB"sv << std::endl << std::endl << " ====== OUTPUT ======"sv << std::endl; dxgi::output_t::pointer output_p {}; for (int y = 0; adapter->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) { dxgi::output_t output {output_p}; DXGI_OUTPUT_DESC desc; output->GetDesc(&desc); auto device_name = to_utf8(desc.DeviceName); auto width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left; auto height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top; BOOST_LOG(debug) << " Output Name : "sv << device_name << std::endl << " AttachedToDesktop : "sv << (desc.AttachedToDesktop ? "yes"sv : "no"sv) << std::endl << " Resolution : "sv << width << 'x' << height << std::endl << std::endl; // Don't include the display in the list if we can't actually capture it if (desc.AttachedToDesktop && dxgi::test_dxgi_duplication(adapter, output, true)) { display_names.emplace_back(std::move(device_name)); } } } return display_names; } /** * @brief Returns if GPUs/drivers have changed since the last call to this function. * @return `true` if a change has occurred or if it is unknown whether a change occurred. */ bool needs_encoder_reenumeration() { // Serialize access to the static DXGI factory static std::mutex reenumeration_state_lock; auto lg = std::lock_guard(reenumeration_state_lock); // Keep a reference to the DXGI factory, which will keep track of changes internally. static dxgi::factory1_t factory; if (!factory || !factory->IsCurrent()) { factory.reset(); auto status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']'; factory.release(); } // Always request reenumeration on the first streaming session just to ensure we // can deal with any initialization races that may occur when the system is booting. BOOST_LOG(info) << "Encoder reenumeration is required"sv; return true; } else { // The DXGI factory from last time is still current, so no encoder changes have occurred. return false; } } } // namespace platf ================================================ FILE: src/platform/windows/display_ram.cpp ================================================ /** * @file src/platform/windows/display_ram.cpp * @brief Definitions for handling ram. */ // local includes #include "display.h" #include "misc.h" #include "src/logging.h" namespace platf { using namespace std::literals; } namespace platf::dxgi { struct img_t: public ::platf::img_t { ~img_t() override { delete[] data; data = nullptr; } }; void blend_cursor_monochrome(const cursor_t &cursor, img_t &img) { int height = cursor.shape_info.Height / 2; int width = cursor.shape_info.Width; int pitch = cursor.shape_info.Pitch; // img cursor.{x,y} < 0, skip parts of the cursor.img_data auto cursor_skip_y = -std::min(0, cursor.y); auto cursor_skip_x = -std::min(0, cursor.x); // img cursor.{x,y} > img.{x,y}, truncate parts of the cursor.img_data auto cursor_truncate_y = std::max(0, cursor.y - img.height); auto cursor_truncate_x = std::max(0, cursor.x - img.width); auto cursor_width = width - cursor_skip_x - cursor_truncate_x; auto cursor_height = height - cursor_skip_y - cursor_truncate_y; if (cursor_height > height || cursor_width > width) { return; } auto img_skip_y = std::max(0, cursor.y); auto img_skip_x = std::max(0, cursor.x); auto cursor_img_data = cursor.img_data.data() + cursor_skip_y * pitch; int delta_height = std::min(cursor_height - cursor_truncate_y, std::max(0, img.height - img_skip_y)); int delta_width = std::min(cursor_width - cursor_truncate_x, std::max(0, img.width - img_skip_x)); auto pixels_per_byte = width / pitch; auto bytes_per_row = delta_width / pixels_per_byte; auto img_data = (int *) img.data; for (int i = 0; i < delta_height; ++i) { auto and_mask = &cursor_img_data[i * pitch]; auto xor_mask = &cursor_img_data[(i + height) * pitch]; auto img_pixel_p = &img_data[(i + img_skip_y) * (img.row_pitch / img.pixel_pitch) + img_skip_x]; auto skip_x = cursor_skip_x; for (int x = 0; x < bytes_per_row; ++x) { for (auto bit = 0u; bit < 8; ++bit) { if (skip_x > 0) { --skip_x; continue; } int and_ = *and_mask & (1 << (7 - bit)) ? -1 : 0; int xor_ = *xor_mask & (1 << (7 - bit)) ? -1 : 0; *img_pixel_p &= and_; *img_pixel_p ^= xor_; ++img_pixel_p; } ++and_mask; ++xor_mask; } } } void apply_color_alpha(int *img_pixel_p, int cursor_pixel) { auto colors_out = (std::uint8_t *) &cursor_pixel; auto colors_in = (std::uint8_t *) img_pixel_p; // TODO: When use of IDXGIOutput5 is implemented, support different color formats auto alpha = colors_out[3]; if (alpha == 255) { *img_pixel_p = cursor_pixel; } else { colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255; colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255; colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255 / 2) / 255; } } void apply_color_masked(int *img_pixel_p, int cursor_pixel) { // TODO: When use of IDXGIOutput5 is implemented, support different color formats auto alpha = ((std::uint8_t *) &cursor_pixel)[3]; if (alpha == 0xFF) { *img_pixel_p ^= cursor_pixel; } else { *img_pixel_p = cursor_pixel; } } void blend_cursor_color(const cursor_t &cursor, img_t &img, const bool masked) { int height = cursor.shape_info.Height; int width = cursor.shape_info.Width; int pitch = cursor.shape_info.Pitch; // img cursor.y < 0, skip parts of the cursor.img_data auto cursor_skip_y = -std::min(0, cursor.y); auto cursor_skip_x = -std::min(0, cursor.x); // img cursor.{x,y} > img.{x,y}, truncate parts of the cursor.img_data auto cursor_truncate_y = std::max(0, cursor.y - img.height); auto cursor_truncate_x = std::max(0, cursor.x - img.width); auto img_skip_y = std::max(0, cursor.y); auto img_skip_x = std::max(0, cursor.x); auto cursor_width = width - cursor_skip_x - cursor_truncate_x; auto cursor_height = height - cursor_skip_y - cursor_truncate_y; if (cursor_height > height || cursor_width > width) { return; } auto cursor_img_data = (int *) &cursor.img_data[cursor_skip_y * pitch]; int delta_height = std::min(cursor_height - cursor_truncate_y, std::max(0, img.height - img_skip_y)); int delta_width = std::min(cursor_width - cursor_truncate_x, std::max(0, img.width - img_skip_x)); auto img_data = (int *) img.data; for (int i = 0; i < delta_height; ++i) { auto cursor_begin = &cursor_img_data[i * cursor.shape_info.Width + cursor_skip_x]; auto cursor_end = &cursor_begin[delta_width]; auto img_pixel_p = &img_data[(i + img_skip_y) * (img.row_pitch / img.pixel_pitch) + img_skip_x]; std::for_each(cursor_begin, cursor_end, [&](int cursor_pixel) { if (masked) { apply_color_masked(img_pixel_p, cursor_pixel); } else { apply_color_alpha(img_pixel_p, cursor_pixel); } ++img_pixel_p; }); } } void blend_cursor(const cursor_t &cursor, img_t &img) { switch (cursor.shape_info.Type) { case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR: blend_cursor_color(cursor, img, false); break; case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME: blend_cursor_monochrome(cursor, img); break; case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR: blend_cursor_color(cursor, img, true); break; default: BOOST_LOG(warning) << "Unsupported cursor format ["sv << cursor.shape_info.Type << ']'; } } capture_e display_ddup_ram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { HRESULT status; DXGI_OUTDUPL_FRAME_INFO frame_info; resource_t::pointer res_p {}; auto capture_status = dup.next_frame(frame_info, timeout, &res_p); resource_t res {res_p}; if (capture_status != capture_e::ok) { return capture_status; } const bool mouse_update_flag = frame_info.LastMouseUpdateTime.QuadPart != 0 || frame_info.PointerShapeBufferSize > 0; const bool frame_update_flag = frame_info.AccumulatedFrames != 0 || frame_info.LastPresentTime.QuadPart != 0; const bool update_flag = mouse_update_flag || frame_update_flag; if (!update_flag) { return capture_e::timeout; } std::optional frame_timestamp; if (auto qpc_displayed = std::max(frame_info.LastPresentTime.QuadPart, frame_info.LastMouseUpdateTime.QuadPart)) { // Translate QueryPerformanceCounter() value to steady_clock time point frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), qpc_displayed); } if (frame_info.PointerShapeBufferSize > 0) { auto &img_data = cursor.img_data; img_data.resize(frame_info.PointerShapeBufferSize); UINT dummy; status = dup.dup->GetFramePointerShape(img_data.size(), img_data.data(), &dummy, &cursor.shape_info); if (FAILED(status)) { BOOST_LOG(error) << "Failed to get new pointer shape [0x"sv << util::hex(status).to_string_view() << ']'; return capture_e::error; } } if (frame_info.LastMouseUpdateTime.QuadPart) { cursor.x = frame_info.PointerPosition.Position.x; cursor.y = frame_info.PointerPosition.Position.y; cursor.visible = frame_info.PointerPosition.Visible; } if (frame_update_flag) { { texture2d_t src {}; status = res->QueryInterface(IID_ID3D11Texture2D, (void **) &src); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't query interface [0x"sv << util::hex(status).to_string_view() << ']'; return capture_e::error; } D3D11_TEXTURE2D_DESC desc; src->GetDesc(&desc); // If we don't know the capture format yet, grab it from this texture and create the staging texture if (capture_format == DXGI_FORMAT_UNKNOWN) { capture_format = desc.Format; BOOST_LOG(info) << "Capture format ["sv << dxgi_format_to_string(capture_format) << ']'; D3D11_TEXTURE2D_DESC t {}; t.Width = width; t.Height = height; t.MipLevels = 1; t.ArraySize = 1; t.SampleDesc.Count = 1; t.Usage = D3D11_USAGE_STAGING; t.Format = capture_format; t.CPUAccessFlags = D3D11_CPU_ACCESS_READ; auto status = device->CreateTexture2D(&t, nullptr, &texture); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create staging texture [0x"sv << util::hex(status).to_string_view() << ']'; return capture_e::error; } } // It's possible for our display enumeration to race with mode changes and result in // mismatched image pool and desktop texture sizes. If this happens, just reinit again. if (desc.Width != width || desc.Height != height) { BOOST_LOG(info) << "Capture size changed ["sv << width << 'x' << height << " -> "sv << desc.Width << 'x' << desc.Height << ']'; return capture_e::reinit; } // It's also possible for the capture format to change on the fly. If that happens, // reinitialize capture to try format detection again and create new images. if (capture_format != desc.Format) { BOOST_LOG(info) << "Capture format changed ["sv << dxgi_format_to_string(capture_format) << " -> "sv << dxgi_format_to_string(desc.Format) << ']'; return capture_e::reinit; } // Copy from GPU to CPU device_ctx->CopyResource(texture.get(), src.get()); } } if (!pull_free_image_cb(img_out)) { return capture_e::interrupted; } auto img = (img_t *) img_out.get(); // If we don't know the final capture format yet, encode a dummy image if (capture_format == DXGI_FORMAT_UNKNOWN) { BOOST_LOG(debug) << "Capture format is still unknown. Encoding a blank image"sv; if (dummy_img(img)) { return capture_e::error; } } else { // Map the staging texture for CPU access (making it inaccessible for the GPU) status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &img_info); if (FAILED(status)) { BOOST_LOG(error) << "Failed to map texture [0x"sv << util::hex(status).to_string_view() << ']'; return capture_e::error; } // Now that we know the capture format, we can finish creating the image if (complete_img(img, false)) { device_ctx->Unmap(texture.get(), 0); img_info.pData = nullptr; return capture_e::error; } std::copy_n((std::uint8_t *) img_info.pData, height * img_info.RowPitch, (std::uint8_t *) img->data); // Unmap the staging texture to allow GPU access again device_ctx->Unmap(texture.get(), 0); img_info.pData = nullptr; } if (cursor_visible && cursor.visible) { blend_cursor(cursor, *img); } if (img) { img->frame_timestamp = frame_timestamp; } return capture_e::ok; } capture_e display_ddup_ram_t::release_snapshot() { return dup.release_frame(); } std::shared_ptr display_ram_t::alloc_img() { auto img = std::make_shared(); // Initialize fields that are format-independent img->width = width; img->height = height; return img; } int display_ram_t::complete_img(platf::img_t *img, bool dummy) { // If this is not a dummy image, we must know the format by now if (!dummy && capture_format == DXGI_FORMAT_UNKNOWN) { BOOST_LOG(error) << "display_ram_t::complete_img() called with unknown capture format!"; return -1; } img->pixel_pitch = get_pixel_pitch(); if (dummy && !img->row_pitch) { // Assume our dummy image will have no padding img->row_pitch = img->pixel_pitch * img->width; } // Reallocate the image buffer if the pitch changes if (!dummy && img->row_pitch != img_info.RowPitch) { img->row_pitch = img_info.RowPitch; delete img->data; img->data = nullptr; } if (!img->data) { img->data = new std::uint8_t[img->row_pitch * height]; } return 0; } /** * @memberof platf::dxgi::display_ram_t */ int display_ram_t::dummy_img(platf::img_t *img) { if (complete_img(img, true)) { return -1; } std::fill_n((std::uint8_t *) img->data, height * img->row_pitch, 0); return 0; } std::vector display_ram_t::get_supported_capture_formats() { return {DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8X8_UNORM}; } int display_ddup_ram_t::init(const ::video::config_t &config, const std::string &display_name) { if (display_base_t::init(config, display_name) || dup.init(this, config)) { return -1; } return 0; } std::unique_ptr display_ram_t::make_avcodec_encode_device(pix_fmt_e pix_fmt) { return std::make_unique(); } } // namespace platf::dxgi ================================================ FILE: src/platform/windows/display_vram.cpp ================================================ /** * @file src/platform/windows/display_vram.cpp * @brief Definitions for handling video ram. */ // standard includes #include // platform includes #include #include extern "C" { #include #include } // lib includes #include #include // local includes #include "display.h" #include "misc.h" #include "src/config.h" #include "src/logging.h" #include "src/nvenc/nvenc_config.h" #include "src/nvenc/nvenc_d3d11_native.h" #include "src/nvenc/nvenc_d3d11_on_cuda.h" #include "src/nvenc/nvenc_utils.h" #include "src/video.h" #if !defined(SUNSHINE_SHADERS_DIR) // for testing this needs to be defined in cmake as we don't do an install #define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/directx" #endif namespace platf { using namespace std::literals; } static void free_frame(AVFrame *frame) { av_frame_free(&frame); } using frame_t = util::safe_ptr; namespace platf::dxgi { template buf_t make_buffer(device_t::pointer device, const T &t) { static_assert(sizeof(T) % 16 == 0, "Buffer needs to be aligned on a 16-byte alignment"); D3D11_BUFFER_DESC buffer_desc { sizeof(T), D3D11_USAGE_IMMUTABLE, D3D11_BIND_CONSTANT_BUFFER }; D3D11_SUBRESOURCE_DATA init_data { &t }; buf_t::pointer buf_p; auto status = device->CreateBuffer(&buffer_desc, &init_data, &buf_p); if (status) { BOOST_LOG(error) << "Failed to create buffer: [0x"sv << util::hex(status).to_string_view() << ']'; return nullptr; } return buf_t {buf_p}; } blend_t make_blend(device_t::pointer device, bool enable, bool invert) { D3D11_BLEND_DESC bdesc {}; auto &rt = bdesc.RenderTarget[0]; rt.BlendEnable = enable; rt.RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; if (enable) { rt.BlendOp = D3D11_BLEND_OP_ADD; rt.BlendOpAlpha = D3D11_BLEND_OP_ADD; if (invert) { // Invert colors rt.SrcBlend = D3D11_BLEND_INV_DEST_COLOR; rt.DestBlend = D3D11_BLEND_INV_SRC_COLOR; } else { // Regular alpha blending rt.SrcBlend = D3D11_BLEND_SRC_ALPHA; rt.DestBlend = D3D11_BLEND_INV_SRC_ALPHA; } rt.SrcBlendAlpha = D3D11_BLEND_ZERO; rt.DestBlendAlpha = D3D11_BLEND_ZERO; } blend_t blend; auto status = device->CreateBlendState(&bdesc, &blend); if (status) { BOOST_LOG(error) << "Failed to create blend state: [0x"sv << util::hex(status).to_string_view() << ']'; return nullptr; } return blend; } blob_t convert_yuv420_packed_uv_type0_ps_hlsl; blob_t convert_yuv420_packed_uv_type0_ps_linear_hlsl; blob_t convert_yuv420_packed_uv_type0_ps_perceptual_quantizer_hlsl; blob_t convert_yuv420_packed_uv_type0_vs_hlsl; blob_t convert_yuv420_packed_uv_type0s_ps_hlsl; blob_t convert_yuv420_packed_uv_type0s_ps_linear_hlsl; blob_t convert_yuv420_packed_uv_type0s_ps_perceptual_quantizer_hlsl; blob_t convert_yuv420_packed_uv_type0s_vs_hlsl; blob_t convert_yuv420_planar_y_ps_hlsl; blob_t convert_yuv420_planar_y_ps_linear_hlsl; blob_t convert_yuv420_planar_y_ps_perceptual_quantizer_hlsl; blob_t convert_yuv420_planar_y_vs_hlsl; blob_t convert_yuv444_packed_ayuv_ps_hlsl; blob_t convert_yuv444_packed_ayuv_ps_linear_hlsl; blob_t convert_yuv444_packed_vs_hlsl; blob_t convert_yuv444_planar_ps_hlsl; blob_t convert_yuv444_planar_ps_linear_hlsl; blob_t convert_yuv444_planar_ps_perceptual_quantizer_hlsl; blob_t convert_yuv444_packed_y410_ps_hlsl; blob_t convert_yuv444_packed_y410_ps_linear_hlsl; blob_t convert_yuv444_packed_y410_ps_perceptual_quantizer_hlsl; blob_t convert_yuv444_planar_vs_hlsl; blob_t cursor_ps_hlsl; blob_t cursor_ps_normalize_white_hlsl; blob_t cursor_vs_hlsl; struct img_d3d_t: public platf::img_t { // These objects are owned by the display_t's ID3D11Device texture2d_t capture_texture; render_target_t capture_rt; keyed_mutex_t capture_mutex; // This is the shared handle used by hwdevice_t to open capture_texture HANDLE encoder_texture_handle = {}; // Set to true if the image corresponds to a dummy texture used prior to // the first successful capture of a desktop frame bool dummy = false; // Set to true if the image is blank (contains no content at all, including a cursor) bool blank = true; // Unique identifier for this image uint32_t id = 0; // DXGI format of this image texture DXGI_FORMAT format; virtual ~img_d3d_t() override { if (encoder_texture_handle) { CloseHandle(encoder_texture_handle); } }; }; struct texture_lock_helper { keyed_mutex_t _mutex; bool _locked = false; texture_lock_helper(const texture_lock_helper &) = delete; texture_lock_helper &operator=(const texture_lock_helper &) = delete; texture_lock_helper(texture_lock_helper &&other) { _mutex.reset(other._mutex.release()); _locked = other._locked; other._locked = false; } texture_lock_helper &operator=(texture_lock_helper &&other) { if (_locked) { _mutex->ReleaseSync(0); } _mutex.reset(other._mutex.release()); _locked = other._locked; other._locked = false; return *this; } texture_lock_helper(IDXGIKeyedMutex *mutex): _mutex(mutex) { if (_mutex) { _mutex->AddRef(); } } ~texture_lock_helper() { if (_locked) { _mutex->ReleaseSync(0); } } bool lock() { if (_locked) { return true; } HRESULT status = _mutex->AcquireSync(0, INFINITE); if (status == S_OK) { _locked = true; } else { BOOST_LOG(error) << "Failed to acquire texture mutex [0x"sv << util::hex(status).to_string_view() << ']'; } return _locked; } }; util::buffer_t make_cursor_xor_image(const util::buffer_t &img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info) { constexpr std::uint32_t inverted = 0xFFFFFFFF; constexpr std::uint32_t transparent = 0; switch (shape_info.Type) { case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR: // This type doesn't require any XOR-blending return {}; case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR: { util::buffer_t cursor_img = img_data; std::for_each((std::uint32_t *) std::begin(cursor_img), (std::uint32_t *) std::end(cursor_img), [](auto &pixel) { auto alpha = (std::uint8_t) ((pixel >> 24) & 0xFF); if (alpha == 0xFF) { // Pixels with 0xFF alpha will be XOR-blended as is. } else if (alpha == 0x00) { // Pixels with 0x00 alpha will be blended by make_cursor_alpha_image(). // We make them transparent for the XOR-blended cursor image. pixel = transparent; } else { // Other alpha values are illegal in masked color cursors BOOST_LOG(warning) << "Illegal alpha value in masked color cursor: " << alpha; } }); return cursor_img; } case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME: // Monochrome is handled below break; default: BOOST_LOG(error) << "Invalid cursor shape type: " << shape_info.Type; return {}; } shape_info.Height /= 2; util::buffer_t cursor_img {shape_info.Width * shape_info.Height * 4}; auto bytes = shape_info.Pitch * shape_info.Height; auto pixel_begin = (std::uint32_t *) std::begin(cursor_img); auto pixel_data = pixel_begin; auto and_mask = std::begin(img_data); auto xor_mask = std::begin(img_data) + bytes; for (auto x = 0; x < bytes; ++x) { for (auto c = 7; c >= 0 && ((std::uint8_t *) pixel_data) != std::end(cursor_img); --c) { auto bit = 1 << c; auto color_type = ((*and_mask & bit) ? 1 : 0) + ((*xor_mask & bit) ? 2 : 0); switch (color_type) { case 0: // Opaque black (handled by alpha-blending) case 2: // Opaque white (handled by alpha-blending) case 1: // Color of screen (transparent) *pixel_data = transparent; break; case 3: // Inverse of screen *pixel_data = inverted; break; } ++pixel_data; } ++and_mask; ++xor_mask; } return cursor_img; } util::buffer_t make_cursor_alpha_image(const util::buffer_t &img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info) { constexpr std::uint32_t black = 0xFF000000; constexpr std::uint32_t white = 0xFFFFFFFF; constexpr std::uint32_t transparent = 0; switch (shape_info.Type) { case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR: { util::buffer_t cursor_img = img_data; std::for_each((std::uint32_t *) std::begin(cursor_img), (std::uint32_t *) std::end(cursor_img), [](auto &pixel) { auto alpha = (std::uint8_t) ((pixel >> 24) & 0xFF); if (alpha == 0xFF) { // Pixels with 0xFF alpha will be XOR-blended by make_cursor_xor_image(). // We make them transparent for the alpha-blended cursor image. pixel = transparent; } else if (alpha == 0x00) { // Pixels with 0x00 alpha will be blended as opaque with the alpha-blended image. pixel |= 0xFF000000; } else { // Other alpha values are illegal in masked color cursors BOOST_LOG(warning) << "Illegal alpha value in masked color cursor: " << alpha; } }); return cursor_img; } case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR: // Color cursors are just an ARGB bitmap which requires no processing. return img_data; case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME: // Monochrome cursors are handled below. break; default: BOOST_LOG(error) << "Invalid cursor shape type: " << shape_info.Type; return {}; } shape_info.Height /= 2; util::buffer_t cursor_img {shape_info.Width * shape_info.Height * 4}; auto bytes = shape_info.Pitch * shape_info.Height; auto pixel_begin = (std::uint32_t *) std::begin(cursor_img); auto pixel_data = pixel_begin; auto and_mask = std::begin(img_data); auto xor_mask = std::begin(img_data) + bytes; for (auto x = 0; x < bytes; ++x) { for (auto c = 7; c >= 0 && ((std::uint8_t *) pixel_data) != std::end(cursor_img); --c) { auto bit = 1 << c; auto color_type = ((*and_mask & bit) ? 1 : 0) + ((*xor_mask & bit) ? 2 : 0); switch (color_type) { case 0: // Opaque black *pixel_data = black; break; case 2: // Opaque white *pixel_data = white; break; case 3: // Inverse of screen (handled by XOR blending) case 1: // Color of screen (transparent) *pixel_data = transparent; break; } ++pixel_data; } ++and_mask; ++xor_mask; } return cursor_img; } blob_t compile_shader(LPCSTR file, LPCSTR entrypoint, LPCSTR shader_model) { blob_t::pointer msg_p = nullptr; blob_t::pointer compiled_p; DWORD flags = D3DCOMPILE_ENABLE_STRICTNESS; #ifndef NDEBUG flags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; #endif auto wFile = from_utf8(file); auto status = D3DCompileFromFile(wFile.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, entrypoint, shader_model, flags, 0, &compiled_p, &msg_p); if (msg_p) { BOOST_LOG(warning) << std::string_view {(const char *) msg_p->GetBufferPointer(), msg_p->GetBufferSize() - 1}; msg_p->Release(); } if (status) { BOOST_LOG(error) << "Couldn't compile ["sv << file << "] [0x"sv << util::hex(status).to_string_view() << ']'; return nullptr; } return blob_t {compiled_p}; } blob_t compile_pixel_shader(LPCSTR file) { return compile_shader(file, "main_ps", "ps_5_0"); } blob_t compile_vertex_shader(LPCSTR file) { return compile_shader(file, "main_vs", "vs_5_0"); } class d3d_base_encode_device final { public: int convert(platf::img_t &img_base) { // Garbage collect mapped capture images whose weak references have expired for (auto it = img_ctx_map.begin(); it != img_ctx_map.end();) { if (it->second.img_weak.expired()) { it = img_ctx_map.erase(it); } else { it++; } } auto &img = (img_d3d_t &) img_base; if (!img.blank) { auto &img_ctx = img_ctx_map[img.id]; // Open the shared capture texture with our ID3D11Device if (initialize_image_context(img, img_ctx)) { return -1; } // Acquire encoder mutex to synchronize with capture code auto status = img_ctx.encoder_mutex->AcquireSync(0, INFINITE); if (status != S_OK) { BOOST_LOG(error) << "Failed to acquire encoder mutex [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } auto draw = [&](auto &input, auto &y_or_yuv_viewports, auto &uv_viewport) { device_ctx->PSSetShaderResources(0, 1, &input); // Draw Y/YUV device_ctx->OMSetRenderTargets(1, &out_Y_or_YUV_rtv, nullptr); device_ctx->VSSetShader(convert_Y_or_YUV_vs.get(), nullptr, 0); device_ctx->PSSetShader(img.format == DXGI_FORMAT_R16G16B16A16_FLOAT ? convert_Y_or_YUV_fp16_ps.get() : convert_Y_or_YUV_ps.get(), nullptr, 0); auto viewport_count = (format == DXGI_FORMAT_R16_UINT) ? 3 : 1; assert(viewport_count <= y_or_yuv_viewports.size()); device_ctx->RSSetViewports(viewport_count, y_or_yuv_viewports.data()); device_ctx->Draw(3 * viewport_count, 0); // vertex shader will spread vertices across viewports // Draw UV if needed if (out_UV_rtv) { assert(format == DXGI_FORMAT_NV12 || format == DXGI_FORMAT_P010); device_ctx->OMSetRenderTargets(1, &out_UV_rtv, nullptr); device_ctx->VSSetShader(convert_UV_vs.get(), nullptr, 0); device_ctx->PSSetShader(img.format == DXGI_FORMAT_R16G16B16A16_FLOAT ? convert_UV_fp16_ps.get() : convert_UV_ps.get(), nullptr, 0); device_ctx->RSSetViewports(1, &uv_viewport); device_ctx->Draw(3, 0); } }; // Clear render target view(s) once so that the aspect ratio mismatch "bars" appear black if (!rtvs_cleared) { auto black = create_black_texture_for_rtv_clear(); if (black) { draw(black, out_Y_or_YUV_viewports_for_clear, out_UV_viewport_for_clear); } rtvs_cleared = true; } // Draw captured frame draw(img_ctx.encoder_input_res, out_Y_or_YUV_viewports, out_UV_viewport); // Release encoder mutex to allow capture code to reuse this image img_ctx.encoder_mutex->ReleaseSync(0); ID3D11ShaderResourceView *emptyShaderResourceView = nullptr; device_ctx->PSSetShaderResources(0, 1, &emptyShaderResourceView); } return 0; } void apply_colorspace(const ::video::sunshine_colorspace_t &colorspace) { auto color_vectors = ::video::color_vectors_from_colorspace(colorspace); if (format == DXGI_FORMAT_AYUV || format == DXGI_FORMAT_R16_UINT || format == DXGI_FORMAT_Y410) { color_vectors = ::video::new_color_vectors_from_colorspace(colorspace); } if (!color_vectors) { BOOST_LOG(error) << "No vector data for colorspace"sv; return; } auto color_matrix = make_buffer(device.get(), *color_vectors); if (!color_matrix) { BOOST_LOG(warning) << "Failed to create color matrix"sv; return; } device_ctx->VSSetConstantBuffers(3, 1, &color_matrix); device_ctx->PSSetConstantBuffers(0, 1, &color_matrix); this->color_matrix = std::move(color_matrix); } int init_output(ID3D11Texture2D *frame_texture, int width, int height) { // The underlying frame pool owns the texture, so we must reference it for ourselves frame_texture->AddRef(); output_texture.reset(frame_texture); HRESULT status = S_OK; #define create_vertex_shader_helper(x, y) \ if (FAILED(status = device->CreateVertexShader(x->GetBufferPointer(), x->GetBufferSize(), nullptr, &y))) { \ BOOST_LOG(error) << "Failed to create vertex shader " << #x << ": " << util::log_hex(status); \ return -1; \ } #define create_pixel_shader_helper(x, y) \ if (FAILED(status = device->CreatePixelShader(x->GetBufferPointer(), x->GetBufferSize(), nullptr, &y))) { \ BOOST_LOG(error) << "Failed to create pixel shader " << #x << ": " << util::log_hex(status); \ return -1; \ } const bool downscaling = display->width > width || display->height > height; switch (format) { case DXGI_FORMAT_NV12: // Semi-planar 8-bit YUV 4:2:0 create_vertex_shader_helper(convert_yuv420_planar_y_vs_hlsl, convert_Y_or_YUV_vs); create_pixel_shader_helper(convert_yuv420_planar_y_ps_hlsl, convert_Y_or_YUV_ps); create_pixel_shader_helper(convert_yuv420_planar_y_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps); if (downscaling) { create_vertex_shader_helper(convert_yuv420_packed_uv_type0s_vs_hlsl, convert_UV_vs); create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_hlsl, convert_UV_ps); create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_linear_hlsl, convert_UV_fp16_ps); } else { create_vertex_shader_helper(convert_yuv420_packed_uv_type0_vs_hlsl, convert_UV_vs); create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_hlsl, convert_UV_ps); create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_linear_hlsl, convert_UV_fp16_ps); } break; case DXGI_FORMAT_P010: // Semi-planar 16-bit YUV 4:2:0, 10 most significant bits store the value create_vertex_shader_helper(convert_yuv420_planar_y_vs_hlsl, convert_Y_or_YUV_vs); create_pixel_shader_helper(convert_yuv420_planar_y_ps_hlsl, convert_Y_or_YUV_ps); if (display->is_hdr()) { create_pixel_shader_helper(convert_yuv420_planar_y_ps_perceptual_quantizer_hlsl, convert_Y_or_YUV_fp16_ps); } else { create_pixel_shader_helper(convert_yuv420_planar_y_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps); } if (downscaling) { create_vertex_shader_helper(convert_yuv420_packed_uv_type0s_vs_hlsl, convert_UV_vs); create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_hlsl, convert_UV_ps); if (display->is_hdr()) { create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_perceptual_quantizer_hlsl, convert_UV_fp16_ps); } else { create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_linear_hlsl, convert_UV_fp16_ps); } } else { create_vertex_shader_helper(convert_yuv420_packed_uv_type0_vs_hlsl, convert_UV_vs); create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_hlsl, convert_UV_ps); if (display->is_hdr()) { create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_perceptual_quantizer_hlsl, convert_UV_fp16_ps); } else { create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_linear_hlsl, convert_UV_fp16_ps); } } break; case DXGI_FORMAT_R16_UINT: // Planar 16-bit YUV 4:4:4, 10 most significant bits store the value create_vertex_shader_helper(convert_yuv444_planar_vs_hlsl, convert_Y_or_YUV_vs); create_pixel_shader_helper(convert_yuv444_planar_ps_hlsl, convert_Y_or_YUV_ps); if (display->is_hdr()) { create_pixel_shader_helper(convert_yuv444_planar_ps_perceptual_quantizer_hlsl, convert_Y_or_YUV_fp16_ps); } else { create_pixel_shader_helper(convert_yuv444_planar_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps); } break; case DXGI_FORMAT_AYUV: // Packed 8-bit YUV 4:4:4 create_vertex_shader_helper(convert_yuv444_packed_vs_hlsl, convert_Y_or_YUV_vs); create_pixel_shader_helper(convert_yuv444_packed_ayuv_ps_hlsl, convert_Y_or_YUV_ps); create_pixel_shader_helper(convert_yuv444_packed_ayuv_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps); break; case DXGI_FORMAT_Y410: // Packed 10-bit YUV 4:4:4 create_vertex_shader_helper(convert_yuv444_packed_vs_hlsl, convert_Y_or_YUV_vs); create_pixel_shader_helper(convert_yuv444_packed_y410_ps_hlsl, convert_Y_or_YUV_ps); if (display->is_hdr()) { create_pixel_shader_helper(convert_yuv444_packed_y410_ps_perceptual_quantizer_hlsl, convert_Y_or_YUV_fp16_ps); } else { create_pixel_shader_helper(convert_yuv444_packed_y410_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps); } break; default: BOOST_LOG(error) << "Unable to create shaders because of the unrecognized surface format"; return -1; } #undef create_vertex_shader_helper #undef create_pixel_shader_helper auto out_width = width; auto out_height = height; float in_width = display->width; float in_height = display->height; // Ensure aspect ratio is maintained auto scalar = std::fminf(out_width / in_width, out_height / in_height); auto out_width_f = in_width * scalar; auto out_height_f = in_height * scalar; // result is always positive auto offsetX = (out_width - out_width_f) / 2; auto offsetY = (out_height - out_height_f) / 2; out_Y_or_YUV_viewports[0] = {offsetX, offsetY, out_width_f, out_height_f, 0.0f, 1.0f}; // Y plane out_Y_or_YUV_viewports[1] = out_Y_or_YUV_viewports[0]; // U plane out_Y_or_YUV_viewports[1].TopLeftY += out_height; out_Y_or_YUV_viewports[2] = out_Y_or_YUV_viewports[1]; // V plane out_Y_or_YUV_viewports[2].TopLeftY += out_height; out_Y_or_YUV_viewports_for_clear[0] = {0, 0, (float) out_width, (float) out_height, 0.0f, 1.0f}; // Y plane out_Y_or_YUV_viewports_for_clear[1] = out_Y_or_YUV_viewports_for_clear[0]; // U plane out_Y_or_YUV_viewports_for_clear[1].TopLeftY += out_height; out_Y_or_YUV_viewports_for_clear[2] = out_Y_or_YUV_viewports_for_clear[1]; // V plane out_Y_or_YUV_viewports_for_clear[2].TopLeftY += out_height; out_UV_viewport = {offsetX / 2, offsetY / 2, out_width_f / 2, out_height_f / 2, 0.0f, 1.0f}; out_UV_viewport_for_clear = {0, 0, (float) out_width / 2, (float) out_height / 2, 0.0f, 1.0f}; float subsample_offset_in[16 / sizeof(float)] {1.0f / (float) out_width_f, 1.0f / (float) out_height_f}; // aligned to 16-byte subsample_offset = make_buffer(device.get(), subsample_offset_in); if (!subsample_offset) { BOOST_LOG(error) << "Failed to create subsample offset vertex constant buffer"; return -1; } device_ctx->VSSetConstantBuffers(0, 1, &subsample_offset); { int32_t rotation_modifier = display->display_rotation == DXGI_MODE_ROTATION_UNSPECIFIED ? 0 : display->display_rotation - 1; int32_t rotation_data[16 / sizeof(int32_t)] {-rotation_modifier}; // aligned to 16-byte auto rotation = make_buffer(device.get(), rotation_data); if (!rotation) { BOOST_LOG(error) << "Failed to create display rotation vertex constant buffer"; return -1; } device_ctx->VSSetConstantBuffers(1, 1, &rotation); } DXGI_FORMAT rtv_Y_or_YUV_format = DXGI_FORMAT_UNKNOWN; DXGI_FORMAT rtv_UV_format = DXGI_FORMAT_UNKNOWN; bool rtv_simple_clear = false; switch (format) { case DXGI_FORMAT_NV12: rtv_Y_or_YUV_format = DXGI_FORMAT_R8_UNORM; rtv_UV_format = DXGI_FORMAT_R8G8_UNORM; rtv_simple_clear = true; break; case DXGI_FORMAT_P010: rtv_Y_or_YUV_format = DXGI_FORMAT_R16_UNORM; rtv_UV_format = DXGI_FORMAT_R16G16_UNORM; rtv_simple_clear = true; break; case DXGI_FORMAT_AYUV: rtv_Y_or_YUV_format = DXGI_FORMAT_R8G8B8A8_UINT; break; case DXGI_FORMAT_R16_UINT: rtv_Y_or_YUV_format = DXGI_FORMAT_R16_UINT; break; case DXGI_FORMAT_Y410: rtv_Y_or_YUV_format = DXGI_FORMAT_R10G10B10A2_UINT; break; default: BOOST_LOG(error) << "Unable to create render target views because of the unrecognized surface format"; return -1; } auto create_rtv = [&](auto &rt, DXGI_FORMAT rt_format) -> bool { D3D11_RENDER_TARGET_VIEW_DESC rtv_desc = {}; rtv_desc.Format = rt_format; rtv_desc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; auto status = device->CreateRenderTargetView(output_texture.get(), &rtv_desc, &rt); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create render target view: " << util::log_hex(status); return false; } return true; }; // Create Y/YUV render target view if (!create_rtv(out_Y_or_YUV_rtv, rtv_Y_or_YUV_format)) { return -1; } // Create UV render target view if needed if (rtv_UV_format != DXGI_FORMAT_UNKNOWN && !create_rtv(out_UV_rtv, rtv_UV_format)) { return -1; } if (rtv_simple_clear) { // Clear the RTVs to ensure the aspect ratio padding is black const float y_black[] = {0.0f, 0.0f, 0.0f, 0.0f}; device_ctx->ClearRenderTargetView(out_Y_or_YUV_rtv.get(), y_black); if (out_UV_rtv) { const float uv_black[] = {0.5f, 0.5f, 0.5f, 0.5f}; device_ctx->ClearRenderTargetView(out_UV_rtv.get(), uv_black); } rtvs_cleared = true; } else { // Can't use ClearRenderTargetView(), will clear on first convert() rtvs_cleared = false; } return 0; } int init(std::shared_ptr display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) { switch (pix_fmt) { case pix_fmt_e::nv12: format = DXGI_FORMAT_NV12; break; case pix_fmt_e::p010: format = DXGI_FORMAT_P010; break; case pix_fmt_e::ayuv: format = DXGI_FORMAT_AYUV; break; case pix_fmt_e::yuv444p16: format = DXGI_FORMAT_R16_UINT; break; case pix_fmt_e::y410: format = DXGI_FORMAT_Y410; break; default: BOOST_LOG(error) << "D3D11 backend doesn't support pixel format: " << from_pix_fmt(pix_fmt); return -1; } D3D_FEATURE_LEVEL featureLevels[] { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0, D3D_FEATURE_LEVEL_9_3, D3D_FEATURE_LEVEL_9_2, D3D_FEATURE_LEVEL_9_1 }; HRESULT status = D3D11CreateDevice( adapter_p, D3D_DRIVER_TYPE_UNKNOWN, nullptr, D3D11_CREATE_DEVICE_FLAGS | D3D11_CREATE_DEVICE_VIDEO_SUPPORT, featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), D3D11_SDK_VERSION, &device, nullptr, &device_ctx ); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create encoder D3D11 device [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } dxgi::dxgi_t dxgi; status = device->QueryInterface(IID_IDXGIDevice, (void **) &dxgi); if (FAILED(status)) { BOOST_LOG(warning) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } status = dxgi->SetGPUThreadPriority(0x4000001E); if (FAILED(status)) { BOOST_LOG(info) << "Failed to request absoloute encoding GPU thread priority. Trying relative priority."; status = dxgi->SetGPUThreadPriority(7); if (FAILED(status)) { BOOST_LOG(warning) << "Failed to request relative encoding GPU thread priority. Please run application as administrator for optimal performance."; } else { BOOST_LOG(info) << "Relative encoding GPU thread priority request success."; } } auto default_color_vectors = ::video::color_vectors_from_colorspace(::video::colorspace_e::rec601, false); if (!default_color_vectors) { BOOST_LOG(error) << "Missing color vectors for Rec. 601"sv; return -1; } color_matrix = make_buffer(device.get(), *default_color_vectors); if (!color_matrix) { BOOST_LOG(error) << "Failed to create color matrix buffer"sv; return -1; } device_ctx->VSSetConstantBuffers(3, 1, &color_matrix); device_ctx->PSSetConstantBuffers(0, 1, &color_matrix); this->display = std::dynamic_pointer_cast(display); if (!this->display) { return -1; } display = nullptr; blend_disable = make_blend(device.get(), false, false); if (!blend_disable) { return -1; } D3D11_SAMPLER_DESC sampler_desc {}; sampler_desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; sampler_desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; sampler_desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; sampler_desc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP; sampler_desc.ComparisonFunc = D3D11_COMPARISON_NEVER; sampler_desc.MinLOD = 0; sampler_desc.MaxLOD = D3D11_FLOAT32_MAX; status = device->CreateSamplerState(&sampler_desc, &sampler_linear); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create point sampler state [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu); device_ctx->PSSetSamplers(0, 1, &sampler_linear); device_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); return 0; } struct encoder_img_ctx_t { // Used to determine if the underlying texture changes. // Not safe for actual use by the encoder! texture2d_t::const_pointer capture_texture_p; texture2d_t encoder_texture; shader_res_t encoder_input_res; keyed_mutex_t encoder_mutex; std::weak_ptr img_weak; void reset() { capture_texture_p = nullptr; encoder_texture.reset(); encoder_input_res.reset(); encoder_mutex.reset(); img_weak.reset(); } }; int initialize_image_context(const img_d3d_t &img, encoder_img_ctx_t &img_ctx) { // If we've already opened the shared texture, we're done if (img_ctx.encoder_texture && img.capture_texture.get() == img_ctx.capture_texture_p) { return 0; } // Reset this image context in case it was used before with a different texture. // Textures can change when transitioning from a dummy image to a real image. img_ctx.reset(); device1_t device1; auto status = device->QueryInterface(__uuidof(ID3D11Device1), (void **) &device1); if (FAILED(status)) { BOOST_LOG(error) << "Failed to query ID3D11Device1 [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } // Open a handle to the shared texture status = device1->OpenSharedResource1(img.encoder_texture_handle, __uuidof(ID3D11Texture2D), (void **) &img_ctx.encoder_texture); if (FAILED(status)) { BOOST_LOG(error) << "Failed to open shared image texture [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } // Get the keyed mutex to synchronize with the capture code status = img_ctx.encoder_texture->QueryInterface(__uuidof(IDXGIKeyedMutex), (void **) &img_ctx.encoder_mutex); if (FAILED(status)) { BOOST_LOG(error) << "Failed to query IDXGIKeyedMutex [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } // Create the SRV for the encoder texture status = device->CreateShaderResourceView(img_ctx.encoder_texture.get(), nullptr, &img_ctx.encoder_input_res); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create shader resource view for encoding [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } img_ctx.capture_texture_p = img.capture_texture.get(); img_ctx.img_weak = img.weak_from_this(); return 0; } shader_res_t create_black_texture_for_rtv_clear() { constexpr auto width = 32; constexpr auto height = 32; D3D11_TEXTURE2D_DESC texture_desc = {}; texture_desc.Width = width; texture_desc.Height = height; texture_desc.MipLevels = 1; texture_desc.ArraySize = 1; texture_desc.SampleDesc.Count = 1; texture_desc.Usage = D3D11_USAGE_IMMUTABLE; texture_desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; texture_desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; std::vector mem(4 * width * height, 0); D3D11_SUBRESOURCE_DATA texture_data = {mem.data(), 4 * width, 0}; texture2d_t texture; auto status = device->CreateTexture2D(&texture_desc, &texture_data, &texture); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create black texture: " << util::log_hex(status); return {}; } shader_res_t resource_view; status = device->CreateShaderResourceView(texture.get(), nullptr, &resource_view); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create black texture resource view: " << util::log_hex(status); return {}; } return resource_view; } ::video::color_t *color_p; buf_t subsample_offset; buf_t color_matrix; blend_t blend_disable; sampler_state_t sampler_linear; render_target_t out_Y_or_YUV_rtv; render_target_t out_UV_rtv; bool rtvs_cleared = false; // d3d_img_t::id -> encoder_img_ctx_t // These store the encoder textures for each img_t that passes through // convert(). We can't store them in the img_t itself because it is shared // amongst multiple hwdevice_t objects (and therefore multiple ID3D11Devices). std::map img_ctx_map; std::shared_ptr display; vs_t convert_Y_or_YUV_vs; ps_t convert_Y_or_YUV_ps; ps_t convert_Y_or_YUV_fp16_ps; vs_t convert_UV_vs; ps_t convert_UV_ps; ps_t convert_UV_fp16_ps; std::array out_Y_or_YUV_viewports, out_Y_or_YUV_viewports_for_clear; D3D11_VIEWPORT out_UV_viewport, out_UV_viewport_for_clear; DXGI_FORMAT format; device_t device; device_ctx_t device_ctx; texture2d_t output_texture; }; class d3d_avcodec_encode_device_t: public avcodec_encode_device_t { public: int init(std::shared_ptr display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) { int result = base.init(display, adapter_p, pix_fmt); data = base.device.get(); return result; } int convert(platf::img_t &img_base) override { return base.convert(img_base); } void apply_colorspace() override { base.apply_colorspace(colorspace); } void init_hwframes(AVHWFramesContext *frames) override { // We may be called with a QSV or D3D11VA context if (frames->device_ctx->type == AV_HWDEVICE_TYPE_D3D11VA) { auto d3d11_frames = (AVD3D11VAFramesContext *) frames->hwctx; // The encoder requires textures with D3D11_BIND_RENDER_TARGET set d3d11_frames->BindFlags = D3D11_BIND_RENDER_TARGET; d3d11_frames->MiscFlags = 0; } // We require a single texture frames->initial_pool_size = 1; } int prepare_to_derive_context(int hw_device_type) override { // QuickSync requires our device to be multithread-protected if (hw_device_type == AV_HWDEVICE_TYPE_QSV) { multithread_t mt; auto status = base.device->QueryInterface(IID_ID3D11Multithread, (void **) &mt); if (FAILED(status)) { BOOST_LOG(warning) << "Failed to query ID3D11Multithread interface from device [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } mt->SetMultithreadProtected(TRUE); } return 0; } int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { this->hwframe.reset(frame); this->frame = frame; // Populate this frame with a hardware buffer if one isn't there already if (!frame->buf[0]) { auto err = av_hwframe_get_buffer(hw_frames_ctx, frame, 0); if (err) { char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Failed to get hwframe buffer: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return -1; } } // If this is a frame from a derived context, we'll need to map it to D3D11 ID3D11Texture2D *frame_texture; if (frame->format != AV_PIX_FMT_D3D11) { frame_t d3d11_frame {av_frame_alloc()}; d3d11_frame->format = AV_PIX_FMT_D3D11; auto err = av_hwframe_map(d3d11_frame.get(), frame, AV_HWFRAME_MAP_WRITE | AV_HWFRAME_MAP_OVERWRITE); if (err) { char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Failed to map D3D11 frame: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return -1; } // Get the texture from the mapped frame frame_texture = (ID3D11Texture2D *) d3d11_frame->data[0]; } else { // Otherwise, we can just use the texture inside the original frame frame_texture = (ID3D11Texture2D *) frame->data[0]; } return base.init_output(frame_texture, frame->width, frame->height); } private: d3d_base_encode_device base; frame_t hwframe; }; class d3d_nvenc_encode_device_t: public nvenc_encode_device_t { public: bool init_device(std::shared_ptr display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) { buffer_format = nvenc::nvenc_format_from_sunshine_format(pix_fmt); if (buffer_format == NV_ENC_BUFFER_FORMAT_UNDEFINED) { BOOST_LOG(error) << "Unexpected pixel format for NvENC ["sv << from_pix_fmt(pix_fmt) << ']'; return false; } if (base.init(display, adapter_p, pix_fmt)) { return false; } if (pix_fmt == pix_fmt_e::yuv444p16) { nvenc_d3d = std::make_unique(base.device.get()); } else { nvenc_d3d = std::make_unique(base.device.get()); } nvenc = nvenc_d3d.get(); return true; } bool init_encoder(const ::video::config_t &client_config, const ::video::sunshine_colorspace_t &colorspace) override { if (!nvenc_d3d) { return false; } auto nvenc_colorspace = nvenc::nvenc_colorspace_from_sunshine_colorspace(colorspace); if (!nvenc_d3d->create_encoder(config::video.nv, client_config, nvenc_colorspace, buffer_format)) { return false; } base.apply_colorspace(colorspace); return base.init_output(nvenc_d3d->get_input_texture(), client_config.width, client_config.height) == 0; } int convert(platf::img_t &img_base) override { return base.convert(img_base); } private: d3d_base_encode_device base; std::unique_ptr nvenc_d3d; NV_ENC_BUFFER_FORMAT buffer_format = NV_ENC_BUFFER_FORMAT_UNDEFINED; }; bool set_cursor_texture(device_t::pointer device, gpu_cursor_t &cursor, util::buffer_t &&cursor_img, DXGI_OUTDUPL_POINTER_SHAPE_INFO &shape_info) { // This cursor image may not be used if (cursor_img.size() == 0) { cursor.input_res.reset(); cursor.set_texture(0, 0, nullptr); return true; } D3D11_SUBRESOURCE_DATA data { std::begin(cursor_img), 4 * shape_info.Width, 0 }; // Create texture for cursor D3D11_TEXTURE2D_DESC t {}; t.Width = shape_info.Width; t.Height = cursor_img.size() / data.SysMemPitch; t.MipLevels = 1; t.ArraySize = 1; t.SampleDesc.Count = 1; t.Usage = D3D11_USAGE_IMMUTABLE; t.Format = DXGI_FORMAT_B8G8R8A8_UNORM; t.BindFlags = D3D11_BIND_SHADER_RESOURCE; texture2d_t texture; auto status = device->CreateTexture2D(&t, &data, &texture); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create mouse texture [0x"sv << util::hex(status).to_string_view() << ']'; return false; } // Free resources before allocating on the next line. cursor.input_res.reset(); status = device->CreateShaderResourceView(texture.get(), nullptr, &cursor.input_res); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create cursor shader resource view [0x"sv << util::hex(status).to_string_view() << ']'; return false; } cursor.set_texture(t.Width, t.Height, std::move(texture)); return true; } capture_e display_ddup_vram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { HRESULT status; DXGI_OUTDUPL_FRAME_INFO frame_info; resource_t::pointer res_p {}; auto capture_status = dup.next_frame(frame_info, timeout, &res_p); resource_t res {res_p}; if (capture_status != capture_e::ok) { return capture_status; } const bool mouse_update_flag = frame_info.LastMouseUpdateTime.QuadPart != 0 || frame_info.PointerShapeBufferSize > 0; const bool frame_update_flag = frame_info.LastPresentTime.QuadPart != 0; const bool update_flag = mouse_update_flag || frame_update_flag; if (!update_flag) { return capture_e::timeout; } std::optional frame_timestamp; if (auto qpc_displayed = std::max(frame_info.LastPresentTime.QuadPart, frame_info.LastMouseUpdateTime.QuadPart)) { // Translate QueryPerformanceCounter() value to steady_clock time point frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), qpc_displayed); } if (frame_info.PointerShapeBufferSize > 0) { DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info {}; util::buffer_t img_data {frame_info.PointerShapeBufferSize}; UINT dummy; status = dup.dup->GetFramePointerShape(img_data.size(), std::begin(img_data), &dummy, &shape_info); if (FAILED(status)) { BOOST_LOG(error) << "Failed to get new pointer shape [0x"sv << util::hex(status).to_string_view() << ']'; return capture_e::error; } auto alpha_cursor_img = make_cursor_alpha_image(img_data, shape_info); auto xor_cursor_img = make_cursor_xor_image(img_data, shape_info); if (!set_cursor_texture(device.get(), cursor_alpha, std::move(alpha_cursor_img), shape_info) || !set_cursor_texture(device.get(), cursor_xor, std::move(xor_cursor_img), shape_info)) { return capture_e::error; } } if (frame_info.LastMouseUpdateTime.QuadPart) { cursor_alpha.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, width, height, display_rotation, frame_info.PointerPosition.Visible); cursor_xor.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, width, height, display_rotation, frame_info.PointerPosition.Visible); } const bool blend_mouse_cursor_flag = (cursor_alpha.visible || cursor_xor.visible) && cursor_visible; texture2d_t src {}; if (frame_update_flag) { // Get the texture object from this frame status = res->QueryInterface(IID_ID3D11Texture2D, (void **) &src); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't query interface [0x"sv << util::hex(status).to_string_view() << ']'; return capture_e::error; } D3D11_TEXTURE2D_DESC desc; src->GetDesc(&desc); // It's possible for our display enumeration to race with mode changes and result in // mismatched image pool and desktop texture sizes. If this happens, just reinit again. if (desc.Width != width_before_rotation || desc.Height != height_before_rotation) { BOOST_LOG(info) << "Capture size changed ["sv << width << 'x' << height << " -> "sv << desc.Width << 'x' << desc.Height << ']'; return capture_e::reinit; } // If we don't know the capture format yet, grab it from this texture if (capture_format == DXGI_FORMAT_UNKNOWN) { capture_format = desc.Format; BOOST_LOG(info) << "Capture format ["sv << dxgi_format_to_string(capture_format) << ']'; } // It's also possible for the capture format to change on the fly. If that happens, // reinitialize capture to try format detection again and create new images. if (capture_format != desc.Format) { BOOST_LOG(info) << "Capture format changed ["sv << dxgi_format_to_string(capture_format) << " -> "sv << dxgi_format_to_string(desc.Format) << ']'; return capture_e::reinit; } } enum class lfa { nothing, replace_surface_with_img, replace_img_with_surface, copy_src_to_img, copy_src_to_surface, }; enum class ofa { forward_last_img, copy_last_surface_and_blend_cursor, dummy_fallback, }; auto last_frame_action = lfa::nothing; auto out_frame_action = ofa::dummy_fallback; if (capture_format == DXGI_FORMAT_UNKNOWN) { // We don't know the final capture format yet, so we will encode a black dummy image last_frame_action = lfa::nothing; out_frame_action = ofa::dummy_fallback; } else { if (src) { // We got a new frame from DesktopDuplication... if (blend_mouse_cursor_flag) { // ...and we need to blend the mouse cursor onto it. // Copy the frame to intermediate surface so we can blend this and future mouse cursor updates // without new frames from DesktopDuplication. We use direct3d surface directly here and not // an image from pull_free_image_cb mainly because it's lighter (surface sharing between // direct3d devices produce significant memory overhead). last_frame_action = lfa::copy_src_to_surface; // Copy the intermediate surface to a new image from pull_free_image_cb and blend the mouse cursor onto it. out_frame_action = ofa::copy_last_surface_and_blend_cursor; } else { // ...and we don't need to blend the mouse cursor. // Copy the frame to a new image from pull_free_image_cb and save the shared pointer to the image // in case the mouse cursor appears without a new frame from DesktopDuplication. last_frame_action = lfa::copy_src_to_img; // Use saved last image shared pointer as output image evading copy. out_frame_action = ofa::forward_last_img; } } else if (!std::holds_alternative(last_frame_variant)) { // We didn't get a new frame from DesktopDuplication... if (blend_mouse_cursor_flag) { // ...but we need to blend the mouse cursor. if (std::holds_alternative>(last_frame_variant)) { // We have the shared pointer of the last image, replace it with intermediate surface // while copying contents so we can blend this and future mouse cursor updates. last_frame_action = lfa::replace_img_with_surface; } // Copy the intermediate surface which contains last DesktopDuplication frame // to a new image from pull_free_image_cb and blend the mouse cursor onto it. out_frame_action = ofa::copy_last_surface_and_blend_cursor; } else { // ...and we don't need to blend the mouse cursor. // This happens when the mouse cursor disappears from screen, // or there's mouse cursor on screen, but its drawing is disabled in sunshine. if (std::holds_alternative(last_frame_variant)) { // We have the intermediate surface that was used as the mouse cursor blending base. // Replace it with an image from pull_free_image_cb copying contents and freeing up the surface memory. // Save the shared pointer to the image in case the mouse cursor reappears. last_frame_action = lfa::replace_surface_with_img; } // Use saved last image shared pointer as output image evading copy. out_frame_action = ofa::forward_last_img; } } } auto create_surface = [&](texture2d_t &surface) -> bool { // Try to reuse the old surface if it hasn't been destroyed yet. if (old_surface_delayed_destruction) { surface.reset(old_surface_delayed_destruction.release()); return true; } // Otherwise create a new surface. D3D11_TEXTURE2D_DESC t {}; t.Width = width_before_rotation; t.Height = height_before_rotation; t.MipLevels = 1; t.ArraySize = 1; t.SampleDesc.Count = 1; t.Usage = D3D11_USAGE_DEFAULT; t.Format = capture_format; t.BindFlags = 0; status = device->CreateTexture2D(&t, nullptr, &surface); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create frame copy texture [0x"sv << util::hex(status).to_string_view() << ']'; return false; } return true; }; auto get_locked_d3d_img = [&](std::shared_ptr &img, bool dummy = false) -> std::tuple, texture_lock_helper> { auto d3d_img = std::static_pointer_cast(img); // Finish creating the image (if it hasn't happened already), // also creates synchronization primitives for shared access from multiple direct3d devices. if (complete_img(d3d_img.get(), dummy)) { return {nullptr, nullptr}; } // This image is shared between capture direct3d device and encoders direct3d devices, // we must acquire lock before doing anything to it. texture_lock_helper lock_helper(d3d_img->capture_mutex.get()); if (!lock_helper.lock()) { BOOST_LOG(error) << "Failed to lock capture texture"; return {nullptr, nullptr}; } // Clear the blank flag now that we're ready to capture into the image d3d_img->blank = false; return {std::move(d3d_img), std::move(lock_helper)}; }; switch (last_frame_action) { case lfa::nothing: { break; } case lfa::replace_surface_with_img: { auto p_surface = std::get_if(&last_frame_variant); if (!p_surface) { BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; return capture_e::error; } std::shared_ptr img; if (!pull_free_image_cb(img)) { return capture_e::interrupted; } auto [d3d_img, lock] = get_locked_d3d_img(img); if (!d3d_img) { return capture_e::error; } device_ctx->CopyResource(d3d_img->capture_texture.get(), p_surface->get()); // We delay the destruction of intermediate surface in case the mouse cursor reappears shortly. old_surface_delayed_destruction.reset(p_surface->release()); old_surface_timestamp = std::chrono::steady_clock::now(); last_frame_variant = img; break; } case lfa::replace_img_with_surface: { auto p_img = std::get_if>(&last_frame_variant); if (!p_img) { BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; return capture_e::error; } auto [d3d_img, lock] = get_locked_d3d_img(*p_img); if (!d3d_img) { return capture_e::error; } p_img = nullptr; last_frame_variant = texture2d_t {}; auto &surface = std::get(last_frame_variant); if (!create_surface(surface)) { return capture_e::error; } device_ctx->CopyResource(surface.get(), d3d_img->capture_texture.get()); break; } case lfa::copy_src_to_img: { last_frame_variant = {}; std::shared_ptr img; if (!pull_free_image_cb(img)) { return capture_e::interrupted; } auto [d3d_img, lock] = get_locked_d3d_img(img); if (!d3d_img) { return capture_e::error; } device_ctx->CopyResource(d3d_img->capture_texture.get(), src.get()); last_frame_variant = img; break; } case lfa::copy_src_to_surface: { auto p_surface = std::get_if(&last_frame_variant); if (!p_surface) { last_frame_variant = texture2d_t {}; p_surface = std::get_if(&last_frame_variant); if (!create_surface(*p_surface)) { return capture_e::error; } } device_ctx->CopyResource(p_surface->get(), src.get()); break; } } auto blend_cursor = [&](img_d3d_t &d3d_img) { device_ctx->VSSetShader(cursor_vs.get(), nullptr, 0); device_ctx->PSSetShader(cursor_ps.get(), nullptr, 0); device_ctx->OMSetRenderTargets(1, &d3d_img.capture_rt, nullptr); if (cursor_alpha.texture.get()) { // Perform an alpha blending operation device_ctx->OMSetBlendState(blend_alpha.get(), nullptr, 0xFFFFFFFFu); device_ctx->PSSetShaderResources(0, 1, &cursor_alpha.input_res); device_ctx->RSSetViewports(1, &cursor_alpha.cursor_view); device_ctx->Draw(3, 0); } if (cursor_xor.texture.get()) { // Perform an invert blending without touching alpha values device_ctx->OMSetBlendState(blend_invert.get(), nullptr, 0x00FFFFFFu); device_ctx->PSSetShaderResources(0, 1, &cursor_xor.input_res); device_ctx->RSSetViewports(1, &cursor_xor.cursor_view); device_ctx->Draw(3, 0); } device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu); ID3D11RenderTargetView *emptyRenderTarget = nullptr; device_ctx->OMSetRenderTargets(1, &emptyRenderTarget, nullptr); device_ctx->RSSetViewports(0, nullptr); ID3D11ShaderResourceView *emptyShaderResourceView = nullptr; device_ctx->PSSetShaderResources(0, 1, &emptyShaderResourceView); }; switch (out_frame_action) { case ofa::forward_last_img: { auto p_img = std::get_if>(&last_frame_variant); if (!p_img) { BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; return capture_e::error; } img_out = *p_img; break; } case ofa::copy_last_surface_and_blend_cursor: { auto p_surface = std::get_if(&last_frame_variant); if (!p_surface) { BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; return capture_e::error; } if (!blend_mouse_cursor_flag) { BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; return capture_e::error; } if (!pull_free_image_cb(img_out)) { return capture_e::interrupted; } auto [d3d_img, lock] = get_locked_d3d_img(img_out); if (!d3d_img) { return capture_e::error; } device_ctx->CopyResource(d3d_img->capture_texture.get(), p_surface->get()); blend_cursor(*d3d_img); break; } case ofa::dummy_fallback: { if (!pull_free_image_cb(img_out)) { return capture_e::interrupted; } // Clear the image if it has been used as a dummy. // It can have the mouse cursor blended onto it. auto old_d3d_img = (img_d3d_t *) img_out.get(); bool reclear_dummy = !old_d3d_img->blank && old_d3d_img->capture_texture; auto [d3d_img, lock] = get_locked_d3d_img(img_out, true); if (!d3d_img) { return capture_e::error; } if (reclear_dummy) { const float rgb_black[] = {0.0f, 0.0f, 0.0f, 0.0f}; device_ctx->ClearRenderTargetView(d3d_img->capture_rt.get(), rgb_black); } if (blend_mouse_cursor_flag) { blend_cursor(*d3d_img); } break; } } // Perform delayed destruction of the unused surface if the time is due. if (old_surface_delayed_destruction && old_surface_timestamp + 10s < std::chrono::steady_clock::now()) { old_surface_delayed_destruction.reset(); } if (img_out) { img_out->frame_timestamp = frame_timestamp; } return capture_e::ok; } capture_e display_ddup_vram_t::release_snapshot() { return dup.release_frame(); } int display_ddup_vram_t::init(const ::video::config_t &config, const std::string &display_name) { if (display_base_t::init(config, display_name) || dup.init(this, config)) { return -1; } D3D11_SAMPLER_DESC sampler_desc {}; sampler_desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; sampler_desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; sampler_desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; sampler_desc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP; sampler_desc.ComparisonFunc = D3D11_COMPARISON_NEVER; sampler_desc.MinLOD = 0; sampler_desc.MaxLOD = D3D11_FLOAT32_MAX; auto status = device->CreateSamplerState(&sampler_desc, &sampler_linear); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create point sampler state [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } status = device->CreateVertexShader(cursor_vs_hlsl->GetBufferPointer(), cursor_vs_hlsl->GetBufferSize(), nullptr, &cursor_vs); if (status) { BOOST_LOG(error) << "Failed to create scene vertex shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } { int32_t rotation_modifier = display_rotation == DXGI_MODE_ROTATION_UNSPECIFIED ? 0 : display_rotation - 1; int32_t rotation_data[16 / sizeof(int32_t)] {rotation_modifier}; // aligned to 16-byte auto rotation = make_buffer(device.get(), rotation_data); if (!rotation) { BOOST_LOG(error) << "Failed to create display rotation vertex constant buffer"; return -1; } device_ctx->VSSetConstantBuffers(2, 1, &rotation); } if (config.dynamicRange && is_hdr()) { // This shader will normalize scRGB white levels to a user-defined white level status = device->CreatePixelShader(cursor_ps_normalize_white_hlsl->GetBufferPointer(), cursor_ps_normalize_white_hlsl->GetBufferSize(), nullptr, &cursor_ps); if (status) { BOOST_LOG(error) << "Failed to create cursor blending (normalized white) pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } // Use a 300 nit target for the mouse cursor. We should really get // the user's SDR white level in nits, but there is no API that // provides that information to Win32 apps. float white_multiplier_data[16 / sizeof(float)] {300.0f / 80.f}; // aligned to 16-byte auto white_multiplier = make_buffer(device.get(), white_multiplier_data); if (!white_multiplier) { BOOST_LOG(warning) << "Failed to create cursor blending (normalized white) white multiplier constant buffer"; return -1; } device_ctx->PSSetConstantBuffers(1, 1, &white_multiplier); } else { status = device->CreatePixelShader(cursor_ps_hlsl->GetBufferPointer(), cursor_ps_hlsl->GetBufferSize(), nullptr, &cursor_ps); if (status) { BOOST_LOG(error) << "Failed to create cursor blending pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } } blend_alpha = make_blend(device.get(), true, false); blend_invert = make_blend(device.get(), true, true); blend_disable = make_blend(device.get(), false, false); if (!blend_disable || !blend_alpha || !blend_invert) { return -1; } device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu); device_ctx->PSSetSamplers(0, 1, &sampler_linear); device_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); return 0; } /** * Get the next frame from the Windows.Graphics.Capture API and copy it into a new snapshot texture. * @param pull_free_image_cb call this to get a new free image from the video subsystem. * @param img_out the captured frame is returned here * @param timeout how long to wait for the next frame * @param cursor_visible */ capture_e display_wgc_vram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { texture2d_t src; uint64_t frame_qpc; dup.set_cursor_visible(cursor_visible); auto capture_status = dup.next_frame(timeout, &src, frame_qpc); if (capture_status != capture_e::ok) { return capture_status; } auto frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), frame_qpc); D3D11_TEXTURE2D_DESC desc; src->GetDesc(&desc); // It's possible for our display enumeration to race with mode changes and result in // mismatched image pool and desktop texture sizes. If this happens, just reinit again. if (desc.Width != width_before_rotation || desc.Height != height_before_rotation) { BOOST_LOG(info) << "Capture size changed ["sv << width << 'x' << height << " -> "sv << desc.Width << 'x' << desc.Height << ']'; return capture_e::reinit; } // It's also possible for the capture format to change on the fly. If that happens, // reinitialize capture to try format detection again and create new images. if (capture_format != desc.Format) { BOOST_LOG(info) << "Capture format changed ["sv << dxgi_format_to_string(capture_format) << " -> "sv << dxgi_format_to_string(desc.Format) << ']'; return capture_e::reinit; } std::shared_ptr img; if (!pull_free_image_cb(img)) { return capture_e::interrupted; } auto d3d_img = std::static_pointer_cast(img); d3d_img->blank = false; // image is always ready for capture if (complete_img(d3d_img.get(), false) == 0) { texture_lock_helper lock_helper(d3d_img->capture_mutex.get()); if (lock_helper.lock()) { device_ctx->CopyResource(d3d_img->capture_texture.get(), src.get()); } else { BOOST_LOG(error) << "Failed to lock capture texture"; return capture_e::error; } } else { return capture_e::error; } img_out = img; if (img_out) { img_out->frame_timestamp = frame_timestamp; } return capture_e::ok; } capture_e display_wgc_vram_t::release_snapshot() { return dup.release_frame(); } int display_wgc_vram_t::init(const ::video::config_t &config, const std::string &display_name) { if (display_base_t::init(config, display_name) || dup.init(this, config)) { return -1; } return 0; } std::shared_ptr display_vram_t::alloc_img() { auto img = std::make_shared(); // Initialize format-independent fields img->width = width_before_rotation; img->height = height_before_rotation; img->id = next_image_id++; img->blank = true; return img; } // This cannot use ID3D11DeviceContext because it can be called concurrently by the encoding thread int display_vram_t::complete_img(platf::img_t *img_base, bool dummy) { auto img = (img_d3d_t *) img_base; // If this already has a capture texture and it's not switching dummy state, nothing to do if (img->capture_texture && img->dummy == dummy) { return 0; } // If this is not a dummy image, we must know the format by now if (!dummy && capture_format == DXGI_FORMAT_UNKNOWN) { BOOST_LOG(error) << "display_vram_t::complete_img() called with unknown capture format!"; return -1; } // Reset the image (in case this was previously a dummy) img->capture_texture.reset(); img->capture_rt.reset(); img->capture_mutex.reset(); img->data = nullptr; if (img->encoder_texture_handle) { CloseHandle(img->encoder_texture_handle); img->encoder_texture_handle = nullptr; } // Initialize format-dependent fields img->pixel_pitch = get_pixel_pitch(); img->row_pitch = img->pixel_pitch * img->width; img->dummy = dummy; img->format = (capture_format == DXGI_FORMAT_UNKNOWN) ? DXGI_FORMAT_B8G8R8A8_UNORM : capture_format; D3D11_TEXTURE2D_DESC t {}; t.Width = img->width; t.Height = img->height; t.MipLevels = 1; t.ArraySize = 1; t.SampleDesc.Count = 1; t.Usage = D3D11_USAGE_DEFAULT; t.Format = img->format; t.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; t.MiscFlags = D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX; auto status = device->CreateTexture2D(&t, nullptr, &img->capture_texture); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create img buf texture [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } status = device->CreateRenderTargetView(img->capture_texture.get(), nullptr, &img->capture_rt); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } // Get the keyed mutex to synchronize with the encoding code status = img->capture_texture->QueryInterface(__uuidof(IDXGIKeyedMutex), (void **) &img->capture_mutex); if (FAILED(status)) { BOOST_LOG(error) << "Failed to query IDXGIKeyedMutex [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } resource1_t resource; status = img->capture_texture->QueryInterface(__uuidof(IDXGIResource1), (void **) &resource); if (FAILED(status)) { BOOST_LOG(error) << "Failed to query IDXGIResource1 [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } // Create a handle for the encoder device to use to open this texture status = resource->CreateSharedHandle(nullptr, DXGI_SHARED_RESOURCE_READ, nullptr, &img->encoder_texture_handle); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create shared texture handle [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } img->data = (std::uint8_t *) img->capture_texture.get(); return 0; } // This cannot use ID3D11DeviceContext because it can be called concurrently by the encoding thread /** * @memberof platf::dxgi::display_vram_t */ int display_vram_t::dummy_img(platf::img_t *img_base) { return complete_img(img_base, true); } std::vector display_vram_t::get_supported_capture_formats() { return { // scRGB FP16 is the ideal format for Wide Color Gamut and Advanced Color // displays (both SDR and HDR). This format uses linear gamma, so we will // use a linear->PQ shader for HDR and a linear->sRGB shader for SDR. DXGI_FORMAT_R16G16B16A16_FLOAT, // DXGI_FORMAT_R10G10B10A2_UNORM seems like it might give us frames already // converted to SMPTE 2084 PQ, however it seems to actually just clamp the // scRGB FP16 values that DWM is using when the desktop format is scRGB FP16. // // If there is a case where the desktop format is really SMPTE 2084 PQ, it // might make sense to support capturing it without conversion to scRGB, // but we avoid it for now. // We include the 8-bit modes too for when the display is in SDR mode, // while the client stream is HDR-capable. These UNORM formats can // use our normal pixel shaders that expect sRGB input. DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8X8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM, }; } /** * @brief Check that a given codec is supported by the display device. * @param name The FFmpeg codec name (or similar for non-FFmpeg codecs). * @param config The codec configuration. * @return `true` if supported, `false` otherwise. */ bool display_vram_t::is_codec_supported(std::string_view name, const ::video::config_t &config) { DXGI_ADAPTER_DESC adapter_desc; adapter->GetDesc(&adapter_desc); if (adapter_desc.VendorId == 0x1002) { // AMD // If it's not an AMF encoder, it's not compatible with an AMD GPU if (!boost::algorithm::ends_with(name, "_amf")) { return false; } // Perform AMF version checks if we're using an AMD GPU. This check is placed in display_vram_t // to avoid hitting the display_ram_t path which uses software encoding and doesn't touch AMF. HMODULE amfrt = LoadLibraryW(AMF_DLL_NAME); if (amfrt) { auto unload_amfrt = util::fail_guard([amfrt]() { FreeLibrary(amfrt); }); auto fnAMFQueryVersion = (AMFQueryVersion_Fn) GetProcAddress(amfrt, AMF_QUERY_VERSION_FUNCTION_NAME); if (fnAMFQueryVersion) { amf_uint64 version; auto result = fnAMFQueryVersion(&version); if (result == AMF_OK) { if (config.videoFormat == 2 && version < AMF_MAKE_FULL_VERSION(1, 4, 30, 0)) { // AMF 1.4.30 adds ultra low latency mode for AV1. Don't use AV1 on earlier versions. // This corresponds to driver version 23.5.2 (23.10.01.45) or newer. BOOST_LOG(warning) << "AV1 encoding is disabled on AMF version "sv << AMF_GET_MAJOR_VERSION(version) << '.' << AMF_GET_MINOR_VERSION(version) << '.' << AMF_GET_SUBMINOR_VERSION(version) << '.' << AMF_GET_BUILD_VERSION(version); BOOST_LOG(warning) << "If your AMD GPU supports AV1 encoding, update your graphics drivers!"sv; return false; } else if (config.dynamicRange && version < AMF_MAKE_FULL_VERSION(1, 4, 23, 0)) { // Older versions of the AMD AMF runtime can crash when fed P010 surfaces. // Fail if AMF version is below 1.4.23 where HEVC Main10 encoding was introduced. // AMF 1.4.23 corresponds to driver version 21.12.1 (21.40.11.03) or newer. BOOST_LOG(warning) << "HDR encoding is disabled on AMF version "sv << AMF_GET_MAJOR_VERSION(version) << '.' << AMF_GET_MINOR_VERSION(version) << '.' << AMF_GET_SUBMINOR_VERSION(version) << '.' << AMF_GET_BUILD_VERSION(version); BOOST_LOG(warning) << "If your AMD GPU supports HEVC Main10 encoding, update your graphics drivers!"sv; return false; } } else { BOOST_LOG(warning) << "AMFQueryVersion() failed: "sv << result; } } else { BOOST_LOG(warning) << "AMF DLL missing export: "sv << AMF_QUERY_VERSION_FUNCTION_NAME; } } else { BOOST_LOG(warning) << "Detected AMD GPU but AMF failed to load"sv; } } else if (adapter_desc.VendorId == 0x8086) { // Intel // If it's not a QSV encoder, it's not compatible with an Intel GPU if (!boost::algorithm::ends_with(name, "_qsv")) { return false; } if (config.chromaSamplingType == 1) { if (config.videoFormat == 0 || config.videoFormat == 2) { // QSV doesn't support 4:4:4 in H.264 or AV1 return false; } // TODO: Blacklist HEVC 4:4:4 based on adapter model } } else if (adapter_desc.VendorId == 0x10de) { // Nvidia // If it's not an NVENC encoder, it's not compatible with an Nvidia GPU if (!boost::algorithm::ends_with(name, "_nvenc")) { return false; } } else { BOOST_LOG(warning) << "Unknown GPU vendor ID: " << util::hex(adapter_desc.VendorId).to_string_view(); } return true; } std::unique_ptr display_vram_t::make_avcodec_encode_device(pix_fmt_e pix_fmt) { auto device = std::make_unique(); if (device->init(shared_from_this(), adapter.get(), pix_fmt) != 0) { return nullptr; } return device; } std::unique_ptr display_vram_t::make_nvenc_encode_device(pix_fmt_e pix_fmt) { auto device = std::make_unique(); if (!device->init_device(shared_from_this(), adapter.get(), pix_fmt)) { return nullptr; } return device; } int init() { BOOST_LOG(info) << "Compiling shaders..."sv; #define compile_vertex_shader_helper(x) \ if (!(x##_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/" #x ".hlsl"))) \ return -1; #define compile_pixel_shader_helper(x) \ if (!(x##_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/" #x ".hlsl"))) \ return -1; compile_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps); compile_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_linear); compile_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_perceptual_quantizer); compile_vertex_shader_helper(convert_yuv420_packed_uv_type0_vs); compile_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps); compile_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_linear); compile_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_perceptual_quantizer); compile_vertex_shader_helper(convert_yuv420_packed_uv_type0s_vs); compile_pixel_shader_helper(convert_yuv420_planar_y_ps); compile_pixel_shader_helper(convert_yuv420_planar_y_ps_linear); compile_pixel_shader_helper(convert_yuv420_planar_y_ps_perceptual_quantizer); compile_vertex_shader_helper(convert_yuv420_planar_y_vs); compile_pixel_shader_helper(convert_yuv444_packed_ayuv_ps); compile_pixel_shader_helper(convert_yuv444_packed_ayuv_ps_linear); compile_vertex_shader_helper(convert_yuv444_packed_vs); compile_pixel_shader_helper(convert_yuv444_planar_ps); compile_pixel_shader_helper(convert_yuv444_planar_ps_linear); compile_pixel_shader_helper(convert_yuv444_planar_ps_perceptual_quantizer); compile_pixel_shader_helper(convert_yuv444_packed_y410_ps); compile_pixel_shader_helper(convert_yuv444_packed_y410_ps_linear); compile_pixel_shader_helper(convert_yuv444_packed_y410_ps_perceptual_quantizer); compile_vertex_shader_helper(convert_yuv444_planar_vs); compile_pixel_shader_helper(cursor_ps); compile_pixel_shader_helper(cursor_ps_normalize_white); compile_vertex_shader_helper(cursor_vs); BOOST_LOG(info) << "Compiled shaders"sv; #undef compile_vertex_shader_helper #undef compile_pixel_shader_helper return 0; } } // namespace platf::dxgi ================================================ FILE: src/platform/windows/display_wgc.cpp ================================================ /** * @file src/platform/windows/display_wgc.cpp * @brief Definitions for WinRT Windows.Graphics.Capture API */ // platform includes #include // Gross hack to work around MINGW-packages#22160 #define ____FIReference_1_boolean_INTERFACE_DEFINED__ #include #include #include #include // local includes #include "display.h" #include "misc.h" #include "src/logging.h" namespace platf { using namespace std::literals; } namespace winrt { using namespace Windows::Foundation; using namespace Windows::Foundation::Metadata; using namespace Windows::Graphics::Capture; using namespace Windows::Graphics::DirectX::Direct3D11; extern "C" { HRESULT __stdcall CreateDirect3D11DeviceFromDXGIDevice(::IDXGIDevice *dxgiDevice, ::IInspectable **graphicsDevice); } /** * Windows structures sometimes have compile-time GUIDs. GCC supports this, but in a roundabout way. * If WINRT_IMPL_HAS_DECLSPEC_UUID is true, then the compiler supports adding this attribute to a struct. For example, Visual Studio. * If not, then MinGW GCC has a workaround to assign a GUID to a structure. */ struct #if WINRT_IMPL_HAS_DECLSPEC_UUID __declspec(uuid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1")) #endif IDirect3DDxgiInterfaceAccess: ::IUnknown { virtual HRESULT __stdcall GetInterface(REFIID id, void **object) = 0; }; } // namespace winrt #if !WINRT_IMPL_HAS_DECLSPEC_UUID static constexpr GUID GUID__IDirect3DDxgiInterfaceAccess = { 0xA9B3D012, 0x3DF2, 0x4EE3, {0xB8, 0xD1, 0x86, 0x95, 0xF4, 0x57, 0xD3, 0xC1} // compare with __declspec(uuid(...)) for the struct above. }; template<> constexpr auto __mingw_uuidof() -> GUID const & { return GUID__IDirect3DDxgiInterfaceAccess; } #endif namespace platf::dxgi { wgc_capture_t::wgc_capture_t() { InitializeConditionVariable(&frame_present_cv); } wgc_capture_t::~wgc_capture_t() { if (capture_session) { capture_session.Close(); } if (frame_pool) { frame_pool.Close(); } item = nullptr; capture_session = nullptr; frame_pool = nullptr; } /** * @brief Initialize the Windows.Graphics.Capture backend. * @return 0 on success, -1 on failure. */ int wgc_capture_t::init(display_base_t *display, const ::video::config_t &config) { HRESULT status; dxgi::dxgi_t dxgi; winrt::com_ptr<::IInspectable> d3d_comhandle; try { if (!winrt::GraphicsCaptureSession::IsSupported()) { BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows!"sv; return -1; } if (FAILED(status = display->device->QueryInterface(IID_IDXGIDevice, (void **) &dxgi))) { BOOST_LOG(error) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } if (FAILED(status = winrt::CreateDirect3D11DeviceFromDXGIDevice(*&dxgi, d3d_comhandle.put()))) { BOOST_LOG(error) << "Failed to query WinRT DirectX interface from device [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } } catch (winrt::hresult_error &e) { 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() << ']'; return -1; } DXGI_OUTPUT_DESC output_desc; uwp_device = d3d_comhandle.as(); display->output->GetDesc(&output_desc); auto monitor_factory = winrt::get_activation_factory(); if (monitor_factory == nullptr || FAILED(status = monitor_factory->CreateForMonitor(output_desc.Monitor, winrt::guid_of(), winrt::put_abi(item)))) { 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() << ']'; return -1; } if (config.dynamicRange) { display->capture_format = DXGI_FORMAT_R16G16B16A16_FLOAT; } else { display->capture_format = DXGI_FORMAT_B8G8R8A8_UNORM; } try { frame_pool = winrt::Direct3D11CaptureFramePool::CreateFreeThreaded(uwp_device, static_cast(display->capture_format), 2, item.Size()); capture_session = frame_pool.CreateCaptureSession(item); frame_pool.FrameArrived({this, &wgc_capture_t::on_frame_arrived}); } catch (winrt::hresult_error &e) { 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() << ']'; return -1; } try { if (winrt::ApiInformation::IsPropertyPresent(L"Windows.Graphics.Capture.GraphicsCaptureSession", L"IsBorderRequired")) { capture_session.IsBorderRequired(false); } else { BOOST_LOG(warning) << "Can't disable colored border around capture area on this version of Windows"; } } catch (winrt::hresult_error &e) { 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() << ']'; } try { if (winrt::ApiInformation::IsPropertyPresent(L"Windows.Graphics.Capture.GraphicsCaptureSession", L"MinUpdateInterval")) { capture_session.MinUpdateInterval(winrt::TimeSpan{ 10000000 / (config.framerate * 2) }); } else { BOOST_LOG(warning) << "Can't set MinUpdateInterval"; } } catch (winrt::hresult_error &e) { BOOST_LOG(warning) << "Screen capture may not be fully supported on this device for this release of Windows: failed to set MinUpdateInterval: [0x"sv << util::hex(e.code()).to_string_view() << ']'; } try { capture_session.StartCapture(); } catch (winrt::hresult_error &e) { 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() << ']'; return -1; } return 0; } /** * This function runs in a separate thread spawned by the frame pool and is a producer of frames. * To maintain parity with the original display interface, this frame will be consumed by the capture thread. * Acquire a read-write lock, make the produced frame available to the capture thread, then wake the capture thread. */ void wgc_capture_t::on_frame_arrived(winrt::Direct3D11CaptureFramePool const &sender, winrt::IInspectable const &) { winrt::Windows::Graphics::Capture::Direct3D11CaptureFrame frame {nullptr}; try { frame = sender.TryGetNextFrame(); } catch (winrt::hresult_error &e) { BOOST_LOG(warning) << "Failed to capture frame: "sv << e.code(); return; } if (frame != nullptr) { AcquireSRWLockExclusive(&frame_lock); if (produced_frame) { produced_frame.Close(); } produced_frame = frame; ReleaseSRWLockExclusive(&frame_lock); WakeConditionVariable(&frame_present_cv); } } /** * @brief Get the next frame from the producer thread. * If not available, the capture thread blocks until one is, or the wait times out. * @param timeout how long to wait for the next frame * @param out a texture containing the frame just captured * @param out_time the timestamp of the frame just captured */ capture_e wgc_capture_t::next_frame(std::chrono::milliseconds timeout, ID3D11Texture2D **out, uint64_t &out_time) { // this CONSUMER runs in the capture thread release_frame(); AcquireSRWLockExclusive(&frame_lock); if (produced_frame == nullptr && SleepConditionVariableSRW(&frame_present_cv, &frame_lock, timeout.count(), 0) == 0) { ReleaseSRWLockExclusive(&frame_lock); if (GetLastError() == ERROR_TIMEOUT) { return capture_e::timeout; } else { return capture_e::error; } } if (produced_frame) { consumed_frame = produced_frame; produced_frame = nullptr; } ReleaseSRWLockExclusive(&frame_lock); if (consumed_frame == nullptr) { // spurious wakeup return capture_e::timeout; } auto capture_access = consumed_frame.Surface().as(); if (capture_access == nullptr) { return capture_e::error; } capture_access->GetInterface(IID_ID3D11Texture2D, (void **) out); out_time = consumed_frame.SystemRelativeTime().count(); // raw ticks from query performance counter return capture_e::ok; } capture_e wgc_capture_t::release_frame() { if (consumed_frame != nullptr) { consumed_frame.Close(); consumed_frame = nullptr; } return capture_e::ok; } int wgc_capture_t::set_cursor_visible(bool x) { try { if (capture_session.IsCursorCaptureEnabled() != x) { capture_session.IsCursorCaptureEnabled(x); } return 0; } catch (winrt::hresult_error &) { return -1; } } int display_wgc_ram_t::init(const ::video::config_t &config, const std::string &display_name) { if (display_base_t::init(config, display_name) || dup.init(this, config)) { return -1; } texture.reset(); return 0; } /** * @brief Get the next frame from the Windows.Graphics.Capture API and copy it into a new snapshot texture. * @param pull_free_image_cb call this to get a new free image from the video subsystem. * @param img_out the captured frame is returned here * @param timeout how long to wait for the next frame * @param cursor_visible whether to capture the cursor */ capture_e display_wgc_ram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { HRESULT status; texture2d_t src; uint64_t frame_qpc; dup.set_cursor_visible(cursor_visible); auto capture_status = dup.next_frame(timeout, &src, frame_qpc); if (capture_status != capture_e::ok) { return capture_status; } auto frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), frame_qpc); D3D11_TEXTURE2D_DESC desc; src->GetDesc(&desc); // Create the staging texture if it doesn't exist. It should match the source in size and format. if (texture == nullptr) { capture_format = desc.Format; BOOST_LOG(info) << "Capture format ["sv << dxgi_format_to_string(capture_format) << ']'; D3D11_TEXTURE2D_DESC t {}; t.Width = width; t.Height = height; t.MipLevels = 1; t.ArraySize = 1; t.SampleDesc.Count = 1; t.Usage = D3D11_USAGE_STAGING; t.Format = capture_format; t.CPUAccessFlags = D3D11_CPU_ACCESS_READ; auto status = device->CreateTexture2D(&t, nullptr, &texture); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create staging texture [0x"sv << util::hex(status).to_string_view() << ']'; return capture_e::error; } } // It's possible for our display enumeration to race with mode changes and result in // mismatched image pool and desktop texture sizes. If this happens, just reinit again. if (desc.Width != width || desc.Height != height) { BOOST_LOG(info) << "Capture size changed ["sv << width << 'x' << height << " -> "sv << desc.Width << 'x' << desc.Height << ']'; return capture_e::reinit; } // It's also possible for the capture format to change on the fly. If that happens, // reinitialize capture to try format detection again and create new images. if (capture_format != desc.Format) { BOOST_LOG(info) << "Capture format changed ["sv << dxgi_format_to_string(capture_format) << " -> "sv << dxgi_format_to_string(desc.Format) << ']'; return capture_e::reinit; } // Copy from GPU to CPU device_ctx->CopyResource(texture.get(), src.get()); if (!pull_free_image_cb(img_out)) { return capture_e::interrupted; } auto img = (img_t *) img_out.get(); // Map the staging texture for CPU access (making it inaccessible for the GPU) if (FAILED(status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &img_info))) { BOOST_LOG(error) << "Failed to map texture [0x"sv << util::hex(status).to_string_view() << ']'; return capture_e::error; } // Now that we know the capture format, we can finish creating the image if (complete_img(img, false)) { device_ctx->Unmap(texture.get(), 0); img_info.pData = nullptr; return capture_e::error; } std::copy_n((std::uint8_t *) img_info.pData, height * img_info.RowPitch, (std::uint8_t *) img->data); // Unmap the staging texture to allow GPU access again device_ctx->Unmap(texture.get(), 0); img_info.pData = nullptr; if (img) { img->frame_timestamp = frame_timestamp; } return capture_e::ok; } capture_e display_wgc_ram_t::release_snapshot() { return dup.release_frame(); } } // namespace platf::dxgi ================================================ FILE: src/platform/windows/input.cpp ================================================ /** * @file src/platform/windows/input.cpp * @brief Definitions for input handling on Windows. */ #define WINVER 0x0A00 // platform includes #include // standard includes #include #include // lib includes #include // local includes #include "keylayout.h" #include "misc.h" #include "src/config.h" #include "src/globals.h" #include "src/logging.h" #include "src/platform/common.h" #ifdef __MINGW32__ // DECLARE_HANDLE(HSYNTHETICPOINTERDEVICE); WINUSERAPI HSYNTHETICPOINTERDEVICE WINAPI CreateSyntheticPointerDevice(POINTER_INPUT_TYPE pointerType, ULONG maxCount, POINTER_FEEDBACK_MODE mode); WINUSERAPI BOOL WINAPI InjectSyntheticPointerInput(HSYNTHETICPOINTERDEVICE device, CONST POINTER_TYPE_INFO *pointerInfo, UINT32 count); WINUSERAPI VOID WINAPI DestroySyntheticPointerDevice(HSYNTHETICPOINTERDEVICE device); #endif namespace platf { using namespace std::literals; thread_local HDESK _lastKnownInputDesktop = nullptr; constexpr touch_port_t target_touch_port { 0, 0, 65535, 65535 }; using client_t = util::safe_ptr<_VIGEM_CLIENT_T, vigem_free>; using target_t = util::safe_ptr<_VIGEM_TARGET_T, vigem_target_free>; void CALLBACK x360_notify( client_t::pointer client, target_t::pointer target, std::uint8_t largeMotor, std::uint8_t smallMotor, std::uint8_t /* led_number */, void *userdata ); void CALLBACK ds4_notify( client_t::pointer client, target_t::pointer target, std::uint8_t largeMotor, std::uint8_t smallMotor, DS4_LIGHTBAR_COLOR /* led_color */, void *userdata ); struct gp_touch_context_t { uint8_t pointerIndex; uint16_t x; uint16_t y; }; struct gamepad_context_t { target_t gp; feedback_queue_t feedback_queue; union { XUSB_REPORT x360; DS4_REPORT_EX ds4; } report; // Map from pointer ID to pointer index std::map pointer_id_map; uint8_t available_pointers; uint8_t client_relative_index; thread_pool_util::ThreadPool::task_id_t repeat_task {}; std::chrono::steady_clock::time_point last_report_ts; gamepad_feedback_msg_t last_rumble; gamepad_feedback_msg_t last_rgb_led; }; constexpr float EARTH_G = 9.80665f; #define MPS2_TO_DS4_ACCEL(x) (int32_t) (((x) / EARTH_G) * 8192) #define DPS_TO_DS4_GYRO(x) (int32_t) ((x) * (1024 / 64)) #define APPLY_CALIBRATION(val, bias, scale) (int32_t) (((float) (val) + (bias)) / (scale)) constexpr DS4_TOUCH ds4_touch_unused = { .bPacketCounter = 0, .bIsUpTrackingNum1 = 0x80, .bTouchData1 = {0x00, 0x00, 0x00}, .bIsUpTrackingNum2 = 0x80, .bTouchData2 = {0x00, 0x00, 0x00}, }; // See https://github.com/ViGEm/ViGEmBus/blob/22835473d17fbf0c4d4bb2f2d42fd692b6e44df4/sys/Ds4Pdo.cpp#L153-L164 constexpr DS4_REPORT_EX ds4_report_init_ex = { {{.bThumbLX = 0x80, .bThumbLY = 0x80, .bThumbRX = 0x80, .bThumbRY = 0x80, .wButtons = DS4_BUTTON_DPAD_NONE, .bSpecial = 0, .bTriggerL = 0, .bTriggerR = 0, .wTimestamp = 0, .bBatteryLvl = 0xFF, .wGyroX = 0, .wGyroY = 0, .wGyroZ = 0, .wAccelX = 0, .wAccelY = 0, .wAccelZ = 0, ._bUnknown1 = {0x00, 0x00, 0x00, 0x00, 0x00}, .bBatteryLvlSpecial = 0x1A, // Wired - Full battery ._bUnknown2 = {0x00, 0x00}, .bTouchPacketsN = 1, .sCurrentTouch = ds4_touch_unused, .sPreviousTouch = {ds4_touch_unused, ds4_touch_unused}}} }; /** * @brief Updates the DS4 input report with the provided motion data. * @details Acceleration is in m/s^2 and gyro is in deg/s. * @param gamepad The gamepad to update. * @param motion_type The type of motion data. * @param x X component of motion. * @param y Y component of motion. * @param z Z component of motion. */ static void ds4_update_motion(gamepad_context_t &gamepad, uint8_t motion_type, float x, float y, float z) { auto &report = gamepad.report.ds4.Report; // Use int32 to process this data, so we can clamp if needed. int32_t intX, intY, intZ; switch (motion_type) { case LI_MOTION_TYPE_ACCEL: // Convert to the DS4's accelerometer scale intX = MPS2_TO_DS4_ACCEL(x); intY = MPS2_TO_DS4_ACCEL(y); intZ = MPS2_TO_DS4_ACCEL(z); // Apply the inverse of ViGEmBus's calibration data intX = APPLY_CALIBRATION(intX, -297, 1.010796f); intY = APPLY_CALIBRATION(intY, -42, 1.014614f); intZ = APPLY_CALIBRATION(intZ, -512, 1.024768f); break; case LI_MOTION_TYPE_GYRO: // Convert to the DS4's gyro scale intX = DPS_TO_DS4_GYRO(x); intY = DPS_TO_DS4_GYRO(y); intZ = DPS_TO_DS4_GYRO(z); // Apply the inverse of ViGEmBus's calibration data intX = APPLY_CALIBRATION(intX, 1, 0.977596f); intY = APPLY_CALIBRATION(intY, 0, 0.972370f); intZ = APPLY_CALIBRATION(intZ, 0, 0.971550f); break; default: return; } // Clamp the values to the range of the data type intX = std::clamp(intX, INT16_MIN, INT16_MAX); intY = std::clamp(intY, INT16_MIN, INT16_MAX); intZ = std::clamp(intZ, INT16_MIN, INT16_MAX); // Populate the report switch (motion_type) { case LI_MOTION_TYPE_ACCEL: report.wAccelX = (int16_t) intX; report.wAccelY = (int16_t) intY; report.wAccelZ = (int16_t) intZ; break; case LI_MOTION_TYPE_GYRO: report.wGyroX = (int16_t) intX; report.wGyroY = (int16_t) intY; report.wGyroZ = (int16_t) intZ; break; default: return; } } class vigem_t { public: int init() { // Probe ViGEm during startup to see if we can successfully attach gamepads. This will allow us to // immediately display the error message in the web UI even before the user tries to stream. client_t client {vigem_alloc()}; VIGEM_ERROR status = vigem_connect(client.get()); if (!VIGEM_SUCCESS(status)) { // Log a special fatal message for this case to show the error in the web UI BOOST_LOG(fatal) << "ViGEmBus is not installed or running. You must install ViGEmBus for gamepad support!"sv; } else { vigem_disconnect(client.get()); } gamepads.resize(MAX_GAMEPADS); return 0; } /** * @brief Attaches a new gamepad. * @param id The gamepad ID. * @param feedback_queue The queue for posting messages back to the client. * @param gp_type The type of gamepad. * @return 0 on success. */ int alloc_gamepad_internal(const gamepad_id_t &id, feedback_queue_t &feedback_queue, VIGEM_TARGET_TYPE gp_type) { auto &gamepad = gamepads[id.globalIndex]; assert(!gamepad.gp); gamepad.client_relative_index = id.clientRelativeIndex; gamepad.last_report_ts = std::chrono::steady_clock::now(); // Establish a connect to the ViGEm driver if we don't have one yet if (!client) { BOOST_LOG(debug) << "Connecting to ViGEmBus driver"sv; client.reset(vigem_alloc()); auto status = vigem_connect(client.get()); if (!VIGEM_SUCCESS(status)) { BOOST_LOG(warning) << "Couldn't setup connection to ViGEm for gamepad support ["sv << util::hex(status).to_string_view() << ']'; client.reset(); return -1; } } if (gp_type == Xbox360Wired) { gamepad.gp.reset(vigem_target_x360_alloc()); XUSB_REPORT_INIT(&gamepad.report.x360); } else { gamepad.gp.reset(vigem_target_ds4_alloc()); // There is no equivalent DS4_REPORT_EX_INIT() gamepad.report.ds4 = ds4_report_init_ex; // Set initial accelerometer and gyro state ds4_update_motion(gamepad, LI_MOTION_TYPE_ACCEL, 0.0f, EARTH_G, 0.0f); ds4_update_motion(gamepad, LI_MOTION_TYPE_GYRO, 0.0f, 0.0f, 0.0f); // Request motion events from the client at 100 Hz feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(gamepad.client_relative_index, LI_MOTION_TYPE_ACCEL, 100)); feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(gamepad.client_relative_index, LI_MOTION_TYPE_GYRO, 100)); // We support pointer index 0 and 1 gamepad.available_pointers = 0x3; } auto status = vigem_target_add(client.get(), gamepad.gp.get()); if (!VIGEM_SUCCESS(status)) { BOOST_LOG(error) << "Couldn't add Gamepad to ViGEm connection ["sv << util::hex(status).to_string_view() << ']'; return -1; } gamepad.feedback_queue = std::move(feedback_queue); if (gp_type == Xbox360Wired) { status = vigem_target_x360_register_notification(client.get(), gamepad.gp.get(), x360_notify, this); } else { status = vigem_target_ds4_register_notification(client.get(), gamepad.gp.get(), ds4_notify, this); } if (!VIGEM_SUCCESS(status)) { BOOST_LOG(warning) << "Couldn't register notifications for rumble support ["sv << util::hex(status).to_string_view() << ']'; } return 0; } /** * @brief Detaches the specified gamepad * @param nr The gamepad. */ void free_target(int nr) { auto &gamepad = gamepads[nr]; if (gamepad.repeat_task) { task_pool.cancel(gamepad.repeat_task); gamepad.repeat_task = nullptr; } if (gamepad.gp && vigem_target_is_attached(gamepad.gp.get())) { auto status = vigem_target_remove(client.get(), gamepad.gp.get()); if (!VIGEM_SUCCESS(status)) { BOOST_LOG(warning) << "Couldn't detach gamepad from ViGEm ["sv << util::hex(status).to_string_view() << ']'; } } gamepad.gp.reset(); // Disconnect from ViGEm if we just removed the last gamepad bool disconnect = true; for (auto &gamepad : gamepads) { if (gamepad.gp && vigem_target_is_attached(gamepad.gp.get())) { disconnect = false; break; } } if (disconnect) { BOOST_LOG(debug) << "Disconnecting from ViGEmBus driver"sv; vigem_disconnect(client.get()); client.reset(); } } /** * @brief Pass rumble data back to the client. * @param target The gamepad. * @param largeMotor The large motor. * @param smallMotor The small motor. */ void rumble(target_t::pointer target, std::uint8_t largeMotor, std::uint8_t smallMotor) { // config::input.forward_rumble - Default is true so ignore rumble messages when false if( config::input.forward_rumble == false ) { // Do nothing; just return return; } for (int x = 0; x < gamepads.size(); ++x) { auto &gamepad = gamepads[x]; if (gamepad.gp.get() == target) { // Convert from 8-bit to 16-bit values uint16_t normalizedLargeMotor = largeMotor << 8; uint16_t normalizedSmallMotor = smallMotor << 8; // Don't resend duplicate rumble data if (normalizedSmallMotor != gamepad.last_rumble.data.rumble.highfreq || normalizedLargeMotor != gamepad.last_rumble.data.rumble.lowfreq) { // We have to use the client-relative index when communicating back to the client gamepad_feedback_msg_t msg = gamepad_feedback_msg_t::make_rumble( gamepad.client_relative_index, normalizedLargeMotor, normalizedSmallMotor ); gamepad.feedback_queue->raise(msg); gamepad.last_rumble = msg; } return; } } } /** * @brief Pass RGB LED data back to the client. * @param target The gamepad. * @param r The red channel. * @param g The red channel. * @param b The red channel. */ void set_rgb_led(target_t::pointer target, std::uint8_t r, std::uint8_t g, std::uint8_t b) { for (int x = 0; x < gamepads.size(); ++x) { auto &gamepad = gamepads[x]; if (gamepad.gp.get() == target) { // Don't resend duplicate RGB data if (r != gamepad.last_rgb_led.data.rgb_led.r || g != gamepad.last_rgb_led.data.rgb_led.g || b != gamepad.last_rgb_led.data.rgb_led.b) { // We have to use the client-relative index when communicating back to the client gamepad_feedback_msg_t msg = gamepad_feedback_msg_t::make_rgb_led(gamepad.client_relative_index, r, g, b); gamepad.feedback_queue->raise(msg); gamepad.last_rgb_led = msg; } return; } } } /** * @brief vigem_t destructor. */ ~vigem_t() { if (client) { for (auto &gamepad : gamepads) { if (gamepad.gp && vigem_target_is_attached(gamepad.gp.get())) { auto status = vigem_target_remove(client.get(), gamepad.gp.get()); if (!VIGEM_SUCCESS(status)) { BOOST_LOG(warning) << "Couldn't detach gamepad from ViGEm ["sv << util::hex(status).to_string_view() << ']'; } } } vigem_disconnect(client.get()); } } std::vector gamepads; client_t client; }; void CALLBACK x360_notify( client_t::pointer client, target_t::pointer target, std::uint8_t largeMotor, std::uint8_t smallMotor, std::uint8_t /* led_number */, void *userdata ) { BOOST_LOG(debug) << "largeMotor: "sv << (int) largeMotor << std::endl << "smallMotor: "sv << (int) smallMotor; task_pool.push(&vigem_t::rumble, (vigem_t *) userdata, target, largeMotor, smallMotor); } void CALLBACK ds4_notify( client_t::pointer client, target_t::pointer target, std::uint8_t largeMotor, std::uint8_t smallMotor, DS4_LIGHTBAR_COLOR led_color, void *userdata ) { BOOST_LOG(debug) << "largeMotor: "sv << (int) largeMotor << std::endl << "smallMotor: "sv << (int) smallMotor << std::endl << "LED: "sv << util::hex(led_color.Red).to_string_view() << ' ' << util::hex(led_color.Green).to_string_view() << ' ' << util::hex(led_color.Blue).to_string_view() << std::endl; task_pool.push(&vigem_t::rumble, (vigem_t *) userdata, target, largeMotor, smallMotor); task_pool.push(&vigem_t::set_rgb_led, (vigem_t *) userdata, target, led_color.Red, led_color.Green, led_color.Blue); } struct input_raw_t { ~input_raw_t() { delete vigem; } vigem_t *vigem; decltype(CreateSyntheticPointerDevice) *fnCreateSyntheticPointerDevice; decltype(InjectSyntheticPointerInput) *fnInjectSyntheticPointerInput; decltype(DestroySyntheticPointerDevice) *fnDestroySyntheticPointerDevice; }; input_t input() { input_t result {new input_raw_t {}}; auto &raw = *(input_raw_t *) result.get(); raw.vigem = new vigem_t {}; if (raw.vigem->init()) { delete raw.vigem; raw.vigem = nullptr; } // Get pointers to virtual touch/pen input functions (Win10 1809+) raw.fnCreateSyntheticPointerDevice = (decltype(CreateSyntheticPointerDevice) *) GetProcAddress(GetModuleHandleA("user32.dll"), "CreateSyntheticPointerDevice"); raw.fnInjectSyntheticPointerInput = (decltype(InjectSyntheticPointerInput) *) GetProcAddress(GetModuleHandleA("user32.dll"), "InjectSyntheticPointerInput"); raw.fnDestroySyntheticPointerDevice = (decltype(DestroySyntheticPointerDevice) *) GetProcAddress(GetModuleHandleA("user32.dll"), "DestroySyntheticPointerDevice"); return result; } /** * @brief Calls SendInput() and switches input desktops if required. * @param i The `INPUT` struct to send. */ void send_input(INPUT &i) { retry: auto send = SendInput(1, &i, sizeof(INPUT)); if (send != 1) { auto hDesk = syncThreadDesktop(); if (_lastKnownInputDesktop != hDesk) { _lastKnownInputDesktop = hDesk; goto retry; } BOOST_LOG(error) << "Couldn't send input"sv; } } /** * @brief Calls InjectSyntheticPointerInput() and switches input desktops if required. * @details Must only be called if InjectSyntheticPointerInput() is available. * @param input The global input context. * @param device The synthetic pointer device handle. * @param pointerInfo An array of `POINTER_TYPE_INFO` structs. * @param count The number of elements in `pointerInfo`. * @return true if input was successfully injected. */ bool inject_synthetic_pointer_input(input_raw_t *input, HSYNTHETICPOINTERDEVICE device, const POINTER_TYPE_INFO *pointerInfo, UINT32 count) { retry: if (!input->fnInjectSyntheticPointerInput(device, pointerInfo, count)) { auto hDesk = syncThreadDesktop(); if (_lastKnownInputDesktop != hDesk) { _lastKnownInputDesktop = hDesk; goto retry; } return false; } return true; } void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { INPUT i {}; i.type = INPUT_MOUSE; auto &mi = i.mi; mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | // MOUSEEVENTF_VIRTUALDESK maps to the entirety of the desktop rather than the primary desktop MOUSEEVENTF_VIRTUALDESK; auto scaled_x = std::lround((x + touch_port.offset_x) * ((float) target_touch_port.width / (float) touch_port.width)); auto scaled_y = std::lround((y + touch_port.offset_y) * ((float) target_touch_port.height / (float) touch_port.height)); mi.dx = scaled_x; mi.dy = scaled_y; send_input(i); } void move_mouse(input_t &input, int deltaX, int deltaY) { INPUT i {}; i.type = INPUT_MOUSE; auto &mi = i.mi; mi.dwFlags = MOUSEEVENTF_MOVE; mi.dx = deltaX; mi.dy = deltaY; send_input(i); } util::point_t get_mouse_loc(input_t &input) { throw std::runtime_error("not implemented yet, has to pass tests"); // TODO: Tests are failing, something wrong here? POINT p; if (!GetCursorPos(&p)) { return util::point_t {0.0, 0.0}; } return util::point_t { (double) p.x, (double) p.y }; } void button_mouse(input_t &input, int button, bool release) { INPUT i {}; i.type = INPUT_MOUSE; auto &mi = i.mi; if (button == 1) { mi.dwFlags = release ? MOUSEEVENTF_LEFTUP : MOUSEEVENTF_LEFTDOWN; } else if (button == 2) { mi.dwFlags = release ? MOUSEEVENTF_MIDDLEUP : MOUSEEVENTF_MIDDLEDOWN; } else if (button == 3) { mi.dwFlags = release ? MOUSEEVENTF_RIGHTUP : MOUSEEVENTF_RIGHTDOWN; } else if (button == 4) { mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN; mi.mouseData = XBUTTON1; } else { mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN; mi.mouseData = XBUTTON2; } send_input(i); } void scroll(input_t &input, int distance) { INPUT i {}; i.type = INPUT_MOUSE; auto &mi = i.mi; mi.dwFlags = MOUSEEVENTF_WHEEL; mi.mouseData = distance; send_input(i); } void hscroll(input_t &input, int distance) { INPUT i {}; i.type = INPUT_MOUSE; auto &mi = i.mi; mi.dwFlags = MOUSEEVENTF_HWHEEL; mi.mouseData = distance; send_input(i); } void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) { INPUT i {}; i.type = INPUT_KEYBOARD; auto &ki = i.ki; // If the client did not normalize this VK code to a US English layout, we can't accurately convert it to a scancode. // If we're set to always send scancodes, we will use the current keyboard layout to convert to a scancode. This will // assume the client and host have the same keyboard layout, but it's probably better than always using US English. if (!(flags & SS_KBE_FLAG_NON_NORMALIZED)) { // Mask off the extended key byte ki.wScan = VK_TO_SCANCODE_MAP[modcode & 0xFF]; } else if (config::input.always_send_scancodes && modcode != VK_LWIN && modcode != VK_RWIN && modcode != VK_PAUSE) { // For some reason, MapVirtualKey(VK_LWIN, MAPVK_VK_TO_VSC) doesn't seem to work :/ ki.wScan = MapVirtualKey(modcode, MAPVK_VK_TO_VSC); } // If we can map this to a scancode, send it as a scancode for maximum game compatibility. if (ki.wScan) { ki.dwFlags = KEYEVENTF_SCANCODE; } else { // If there is no scancode mapping or it's non-normalized, send it as a regular VK event. ki.wVk = modcode; } // https://docs.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#keystroke-message-flags switch (modcode) { case VK_LWIN: case VK_RWIN: case VK_RMENU: case VK_RCONTROL: case VK_INSERT: case VK_DELETE: case VK_HOME: case VK_END: case VK_PRIOR: case VK_NEXT: case VK_UP: case VK_DOWN: case VK_LEFT: case VK_RIGHT: case VK_DIVIDE: case VK_APPS: ki.dwFlags |= KEYEVENTF_EXTENDEDKEY; break; default: break; } if (release) { ki.dwFlags |= KEYEVENTF_KEYUP; } send_input(i); } struct client_input_raw_t: public client_input_t { client_input_raw_t(input_t &input) { global = (input_raw_t *) input.get(); } ~client_input_raw_t() override { if (penRepeatTask) { task_pool.cancel(penRepeatTask); } if (touchRepeatTask) { task_pool.cancel(touchRepeatTask); } if (pen) { global->fnDestroySyntheticPointerDevice(pen); } if (touch) { global->fnDestroySyntheticPointerDevice(touch); } } input_raw_t *global; // Device state and handles for pen and touch input must be stored in the per-client // input context, because each connected client may be sending their own independent // pen/touch events. To maintain separation, we expose separate pen and touch devices // for each client. HSYNTHETICPOINTERDEVICE pen {}; POINTER_TYPE_INFO penInfo {}; thread_pool_util::ThreadPool::task_id_t penRepeatTask {}; HSYNTHETICPOINTERDEVICE touch {}; POINTER_TYPE_INFO touchInfo[10] {}; UINT32 activeTouchSlots {}; thread_pool_util::ThreadPool::task_id_t touchRepeatTask {}; }; /** * @brief Allocates a context to store per-client input data. * @param input The global input context. * @return A unique pointer to a per-client input data context. */ std::unique_ptr allocate_client_input_context(input_t &input) { return std::make_unique(input); } /** * @brief Compacts the touch slots into a contiguous block and updates the active count. * @details Since this swaps entries around, all slot pointers/references are invalid after compaction. * @param raw The client-specific input context. */ void perform_touch_compaction(client_input_raw_t *raw) { // Windows requires all active touches be contiguous when fed into InjectSyntheticPointerInput(). UINT32 i; for (i = 0; i < ARRAYSIZE(raw->touchInfo); i++) { if (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags == POINTER_FLAG_NONE) { // This is an empty slot. Look for a later entry to move into this slot. for (UINT32 j = i + 1; j < ARRAYSIZE(raw->touchInfo); j++) { if (raw->touchInfo[j].touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) { std::swap(raw->touchInfo[i], raw->touchInfo[j]); break; } } // If we didn't find anything, we've reached the end of active slots. if (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags == POINTER_FLAG_NONE) { break; } } } // Update the number of active touch slots raw->activeTouchSlots = i; } /** * @brief Gets a pointer slot by client-relative pointer ID, claiming a new one if necessary. * @param raw The raw client-specific input context. * @param pointerId The client's pointer ID. * @param eventType The LI_TOUCH_EVENT value from the client. * @return A pointer to the slot entry. */ POINTER_TYPE_INFO *pointer_by_id(client_input_raw_t *raw, uint32_t pointerId, uint8_t eventType) { // Compact active touches into a single contiguous block perform_touch_compaction(raw); // Try to find a matching pointer ID for (UINT32 i = 0; i < ARRAYSIZE(raw->touchInfo); i++) { if (raw->touchInfo[i].touchInfo.pointerInfo.pointerId == pointerId && raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) { if (eventType == LI_TOUCH_EVENT_DOWN && (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT)) { BOOST_LOG(warning) << "Pointer "sv << pointerId << " already down. Did the client drop an up/cancel event?"sv; } return &raw->touchInfo[i]; } } if (eventType != LI_TOUCH_EVENT_HOVER && eventType != LI_TOUCH_EVENT_DOWN) { BOOST_LOG(warning) << "Unexpected new pointer "sv << pointerId << " for event "sv << (uint32_t) eventType << ". Did the client drop a down/hover event?"sv; } // If there was none, grab an unused entry and increment the active slot count for (UINT32 i = 0; i < ARRAYSIZE(raw->touchInfo); i++) { if (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags == POINTER_FLAG_NONE) { raw->touchInfo[i].touchInfo.pointerInfo.pointerId = pointerId; raw->activeTouchSlots = i + 1; return &raw->touchInfo[i]; } } return nullptr; } /** * @brief Populate common `POINTER_INFO` members shared between pen and touch events. * @param pointerInfo The pointer info to populate. * @param touchPort The current viewport for translating to screen coordinates. * @param eventType The type of touch/pen event. * @param x The normalized 0.0-1.0 X coordinate. * @param y The normalized 0.0-1.0 Y coordinate. */ void populate_common_pointer_info(POINTER_INFO &pointerInfo, const touch_port_t &touchPort, uint8_t eventType, float x, float y) { switch (eventType) { case LI_TOUCH_EVENT_HOVER: pointerInfo.pointerFlags &= ~POINTER_FLAG_INCONTACT; pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_UPDATE; pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x; pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y; break; case LI_TOUCH_EVENT_DOWN: pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_DOWN; pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x; pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y; break; case LI_TOUCH_EVENT_UP: // We expect to get another LI_TOUCH_EVENT_HOVER if the pointer remains in range pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE); pointerInfo.pointerFlags |= POINTER_FLAG_UP; break; case LI_TOUCH_EVENT_MOVE: pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_UPDATE; pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x; pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y; break; case LI_TOUCH_EVENT_CANCEL: case LI_TOUCH_EVENT_CANCEL_ALL: // If we were in contact with the touch surface at the time of the cancellation, // we'll set POINTER_FLAG_UP, otherwise set POINTER_FLAG_UPDATE. if (pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) { pointerInfo.pointerFlags |= POINTER_FLAG_UP; } else { pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE; } pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE); pointerInfo.pointerFlags |= POINTER_FLAG_CANCELED; break; case LI_TOUCH_EVENT_HOVER_LEAVE: pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE); pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE; break; case LI_TOUCH_EVENT_BUTTON_ONLY: // On Windows, we can only pass buttons if we have an active pointer if (pointerInfo.pointerFlags != POINTER_FLAG_NONE) { pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE; } break; default: BOOST_LOG(warning) << "Unknown touch event: "sv << (uint32_t) eventType; break; } } // Active pointer interactions sent via InjectSyntheticPointerInput() seem to be automatically // cancelled by Windows if not repeated/updated within about a second. To avoid this, refresh // the injected input periodically. constexpr auto ISPI_REPEAT_INTERVAL = 50ms; /** * @brief Repeats the current touch state to avoid the interactions timing out. * @param raw The raw client-specific input context. */ void repeat_touch(client_input_raw_t *raw) { if (!inject_synthetic_pointer_input(raw->global, raw->touch, raw->touchInfo, raw->activeTouchSlots)) { auto err = GetLastError(); BOOST_LOG(warning) << "Failed to refresh virtual touch input: "sv << err; } raw->touchRepeatTask = task_pool.pushDelayed(repeat_touch, ISPI_REPEAT_INTERVAL, raw).task_id; } /** * @brief Repeats the current pen state to avoid the interactions timing out. * @param raw The raw client-specific input context. */ void repeat_pen(client_input_raw_t *raw) { if (!inject_synthetic_pointer_input(raw->global, raw->pen, &raw->penInfo, 1)) { auto err = GetLastError(); BOOST_LOG(warning) << "Failed to refresh virtual pen input: "sv << err; } raw->penRepeatTask = task_pool.pushDelayed(repeat_pen, ISPI_REPEAT_INTERVAL, raw).task_id; } /** * @brief Cancels all active touches. * @param raw The raw client-specific input context. */ void cancel_all_active_touches(client_input_raw_t *raw) { // Cancel touch repeat callbacks if (raw->touchRepeatTask) { task_pool.cancel(raw->touchRepeatTask); raw->touchRepeatTask = nullptr; } // Compact touches to update activeTouchSlots perform_touch_compaction(raw); // If we have active slots, cancel them all if (raw->activeTouchSlots > 0) { for (UINT32 i = 0; i < raw->activeTouchSlots; i++) { populate_common_pointer_info(raw->touchInfo[i].touchInfo.pointerInfo, {}, LI_TOUCH_EVENT_CANCEL_ALL, 0.0f, 0.0f); raw->touchInfo[i].touchInfo.touchMask = TOUCH_MASK_NONE; } if (!inject_synthetic_pointer_input(raw->global, raw->touch, raw->touchInfo, raw->activeTouchSlots)) { auto err = GetLastError(); BOOST_LOG(warning) << "Failed to cancel all virtual touch input: "sv << err; } } // Zero all touch state std::memset(raw->touchInfo, 0, sizeof(raw->touchInfo)); raw->activeTouchSlots = 0; } // These are edge-triggered pointer state flags that should always be cleared next frame constexpr auto EDGE_TRIGGERED_POINTER_FLAGS = POINTER_FLAG_DOWN | POINTER_FLAG_UP | POINTER_FLAG_CANCELED | POINTER_FLAG_UPDATE; /** * @brief Sends a touch event to the OS. * @param input The client-specific input context. * @param touch_port The current viewport for translating to screen coordinates. * @param touch The touch event. */ void touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { auto raw = (client_input_raw_t *) input; // Bail if we're not running on an OS that supports virtual touch input if (!raw->global->fnCreateSyntheticPointerDevice || !raw->global->fnInjectSyntheticPointerInput || !raw->global->fnDestroySyntheticPointerDevice) { BOOST_LOG(warning) << "Touch input requires Windows 10 1809 or later"sv; return; } // If there's not already a virtual touch device, create one now if (!raw->touch) { if (touch.eventType != LI_TOUCH_EVENT_CANCEL_ALL) { BOOST_LOG(info) << "Creating virtual touch input device"sv; raw->touch = raw->global->fnCreateSyntheticPointerDevice(PT_TOUCH, ARRAYSIZE(raw->touchInfo), POINTER_FEEDBACK_DEFAULT); if (!raw->touch) { auto err = GetLastError(); BOOST_LOG(warning) << "Failed to create virtual touch device: "sv << err; return; } } else { // No need to cancel anything if we had no touch input device return; } } // Cancel touch repeat callbacks if (raw->touchRepeatTask) { task_pool.cancel(raw->touchRepeatTask); raw->touchRepeatTask = nullptr; } // If this is a special request to cancel all touches, do that and return if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) { cancel_all_active_touches(raw); return; } // Find or allocate an entry for this touch pointer ID auto pointer = pointer_by_id(raw, touch.pointerId, touch.eventType); if (!pointer) { BOOST_LOG(error) << "No unused pointer entries! Cancelling all active touches!"sv; cancel_all_active_touches(raw); pointer = pointer_by_id(raw, touch.pointerId, touch.eventType); } pointer->type = PT_TOUCH; auto &touchInfo = pointer->touchInfo; touchInfo.pointerInfo.pointerType = PT_TOUCH; // Populate shared pointer info fields populate_common_pointer_info(touchInfo.pointerInfo, touch_port, touch.eventType, touch.x, touch.y); touchInfo.touchMask = TOUCH_MASK_NONE; // Pressure and contact area only apply to in-contact pointers. // // The clients also pass distance and tool size for hovers, but Windows doesn't // provide APIs to receive that data. if (touchInfo.pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) { if (touch.pressureOrDistance != 0.0f) { touchInfo.touchMask |= TOUCH_MASK_PRESSURE; // Convert the 0.0f..1.0f float to the 0..1024 range that Windows uses touchInfo.pressure = (UINT32) (touch.pressureOrDistance * 1024); } else { // The default touch pressure is 512 touchInfo.pressure = 512; } if (touch.contactAreaMajor != 0.0f && touch.contactAreaMinor != 0.0f) { // For the purposes of contact area calculation, we will assume the touches // are at a 45 degree angle if rotation is unknown. This will scale the major // axis value by width and height equally. float rotationAngleDegs = touch.rotation == LI_ROT_UNKNOWN ? 45 : touch.rotation; float majorAxisAngle = rotationAngleDegs * (M_PI / 180); float minorAxisAngle = majorAxisAngle + (M_PI / 2); // Estimate the contact rectangle float contactWidth = (std::cos(majorAxisAngle) * touch.contactAreaMajor) + (std::cos(minorAxisAngle) * touch.contactAreaMinor); float contactHeight = (std::sin(majorAxisAngle) * touch.contactAreaMajor) + (std::sin(minorAxisAngle) * touch.contactAreaMinor); // Convert into screen coordinates centered at the touch location and constrained by screen dimensions touchInfo.rcContact.left = std::max(touch_port.offset_x, touchInfo.pointerInfo.ptPixelLocation.x - std::floor(contactWidth / 2)); touchInfo.rcContact.right = std::min(touch_port.offset_x + touch_port.width, touchInfo.pointerInfo.ptPixelLocation.x + std::ceil(contactWidth / 2)); touchInfo.rcContact.top = std::max(touch_port.offset_y, touchInfo.pointerInfo.ptPixelLocation.y - std::floor(contactHeight / 2)); touchInfo.rcContact.bottom = std::min(touch_port.offset_y + touch_port.height, touchInfo.pointerInfo.ptPixelLocation.y + std::ceil(contactHeight / 2)); touchInfo.touchMask |= TOUCH_MASK_CONTACTAREA; } } else { touchInfo.pressure = 0; touchInfo.rcContact = {}; } if (touch.rotation != LI_ROT_UNKNOWN) { touchInfo.touchMask |= TOUCH_MASK_ORIENTATION; touchInfo.orientation = touch.rotation; } else { touchInfo.orientation = 0; } if (!inject_synthetic_pointer_input(raw->global, raw->touch, raw->touchInfo, raw->activeTouchSlots)) { auto err = GetLastError(); BOOST_LOG(warning) << "Failed to inject virtual touch input: "sv << err; return; } // Clear pointer flags that should only remain set for one frame touchInfo.pointerInfo.pointerFlags &= ~EDGE_TRIGGERED_POINTER_FLAGS; // If we still have an active touch, refresh the touch state periodically if (raw->activeTouchSlots > 1 || touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) { raw->touchRepeatTask = task_pool.pushDelayed(repeat_touch, ISPI_REPEAT_INTERVAL, raw).task_id; } } /** * @brief Sends a pen event to the OS. * @param input The client-specific input context. * @param touch_port The current viewport for translating to screen coordinates. * @param pen The pen event. */ void pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { auto raw = (client_input_raw_t *) input; // Bail if we're not running on an OS that supports virtual pen input if (!raw->global->fnCreateSyntheticPointerDevice || !raw->global->fnInjectSyntheticPointerInput || !raw->global->fnDestroySyntheticPointerDevice) { BOOST_LOG(warning) << "Pen input requires Windows 10 1809 or later"sv; return; } // If there's not already a virtual pen device, create one now if (!raw->pen) { if (pen.eventType != LI_TOUCH_EVENT_CANCEL_ALL) { BOOST_LOG(info) << "Creating virtual pen input device"sv; raw->pen = raw->global->fnCreateSyntheticPointerDevice(PT_PEN, 1, POINTER_FEEDBACK_DEFAULT); if (!raw->pen) { auto err = GetLastError(); BOOST_LOG(warning) << "Failed to create virtual pen device: "sv << err; return; } } else { // No need to cancel anything if we had no pen input device return; } } // Cancel pen repeat callbacks if (raw->penRepeatTask) { task_pool.cancel(raw->penRepeatTask); raw->penRepeatTask = nullptr; } raw->penInfo.type = PT_PEN; auto &penInfo = raw->penInfo.penInfo; penInfo.pointerInfo.pointerType = PT_PEN; penInfo.pointerInfo.pointerId = 0; // Populate shared pointer info fields populate_common_pointer_info(penInfo.pointerInfo, touch_port, pen.eventType, pen.x, pen.y); // Windows only supports a single pen button, so send all buttons as the barrel button if (pen.penButtons) { penInfo.penFlags |= PEN_FLAG_BARREL; } else { penInfo.penFlags &= ~PEN_FLAG_BARREL; } switch (pen.toolType) { default: case LI_TOOL_TYPE_PEN: penInfo.penFlags &= ~PEN_FLAG_ERASER; break; case LI_TOOL_TYPE_ERASER: penInfo.penFlags |= PEN_FLAG_ERASER; break; case LI_TOOL_TYPE_UNKNOWN: // Leave tool flags alone break; } penInfo.penMask = PEN_MASK_NONE; // Windows doesn't support hover distance, so only pass pressure/distance when the pointer is in contact if ((penInfo.pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) && pen.pressureOrDistance != 0.0f) { penInfo.penMask |= PEN_MASK_PRESSURE; // Convert the 0.0f..1.0f float to the 0..1024 range that Windows uses penInfo.pressure = (UINT32) (pen.pressureOrDistance * 1024); } else { // The default pen pressure is 0 penInfo.pressure = 0; } if (pen.rotation != LI_ROT_UNKNOWN) { penInfo.penMask |= PEN_MASK_ROTATION; penInfo.rotation = pen.rotation; } else { penInfo.rotation = 0; } // We require rotation and tilt to perform the conversion to X and Y tilt angles if (pen.tilt != LI_TILT_UNKNOWN && pen.rotation != LI_ROT_UNKNOWN) { auto rotationRads = pen.rotation * (M_PI / 180.f); auto tiltRads = pen.tilt * (M_PI / 180.f); auto r = std::sin(tiltRads); auto z = std::cos(tiltRads); // Convert polar coordinates into X and Y tilt angles penInfo.penMask |= PEN_MASK_TILT_X | PEN_MASK_TILT_Y; penInfo.tiltX = (INT32) (std::atan2(std::sin(-rotationRads) * r, z) * 180.f / M_PI); penInfo.tiltY = (INT32) (std::atan2(std::cos(-rotationRads) * r, z) * 180.f / M_PI); } else { penInfo.tiltX = 0; penInfo.tiltY = 0; } if (!inject_synthetic_pointer_input(raw->global, raw->pen, &raw->penInfo, 1)) { auto err = GetLastError(); BOOST_LOG(warning) << "Failed to inject virtual pen input: "sv << err; return; } // Clear pointer flags that should only remain set for one frame penInfo.pointerInfo.pointerFlags &= ~EDGE_TRIGGERED_POINTER_FLAGS; // If we still have an active pen interaction, refresh the pen state periodically if (penInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) { raw->penRepeatTask = task_pool.pushDelayed(repeat_pen, ISPI_REPEAT_INTERVAL, raw).task_id; } } void unicode(input_t &input, char *utf8, int size) { // We can do no worse than one UTF-16 character per byte of UTF-8 WCHAR wide[size]; int chars = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, size, wide, size); if (chars <= 0) { return; } // Send all key down events for (int i = 0; i < chars; i++) { INPUT input {}; input.type = INPUT_KEYBOARD; input.ki.wScan = wide[i]; input.ki.dwFlags = KEYEVENTF_UNICODE; send_input(input); } // Send all key up events for (int i = 0; i < chars; i++) { INPUT input {}; input.type = INPUT_KEYBOARD; input.ki.wScan = wide[i]; input.ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP; send_input(input); } } int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { auto raw = (input_raw_t *) input.get(); if (!raw->vigem) { return 0; } VIGEM_TARGET_TYPE selectedGamepadType; if (config::input.gamepad == "x360"sv) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox 360 controller (manual selection)"sv; selectedGamepadType = Xbox360Wired; } else if (config::input.gamepad == "ds4"sv) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 4 controller (manual selection)"sv; selectedGamepadType = DualShock4Wired; } else if (metadata.type == LI_CTYPE_PS) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 4 controller (auto-selected by client-reported type)"sv; selectedGamepadType = DualShock4Wired; } else if (metadata.type == LI_CTYPE_XBOX) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox 360 controller (auto-selected by client-reported type)"sv; selectedGamepadType = Xbox360Wired; } else if (config::input.motion_as_ds4 && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 4 controller (auto-selected by motion sensor presence)"sv; selectedGamepadType = DualShock4Wired; } else if (config::input.touchpad_as_ds4 && (metadata.capabilities & LI_CCAP_TOUCHPAD)) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 4 controller (auto-selected by touchpad presence)"sv; selectedGamepadType = DualShock4Wired; } else { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox 360 controller (default)"sv; selectedGamepadType = Xbox360Wired; } if (selectedGamepadType == Xbox360Wired) { if (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO)) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has motion sensors, but they are not usable when emulating an Xbox 360 controller"sv; } if (metadata.capabilities & LI_CCAP_TOUCHPAD) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has a touchpad, but it is not usable when emulating an Xbox 360 controller"sv; } if (metadata.capabilities & LI_CCAP_RGB_LED) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has an RGB LED, but it is not usable when emulating an Xbox 360 controller"sv; } } else if (selectedGamepadType == DualShock4Wired) { if (!(metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " is emulating a DualShock 4 controller, but the client gamepad doesn't have motion sensors active"sv; } if (!(metadata.capabilities & LI_CCAP_TOUCHPAD)) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " is emulating a DualShock 4 controller, but the client gamepad doesn't have a touchpad"sv; } } return raw->vigem->alloc_gamepad_internal(id, feedback_queue, selectedGamepadType); } void free_gamepad(input_t &input, int nr) { auto raw = (input_raw_t *) input.get(); if (!raw->vigem) { return; } raw->vigem->free_target(nr); } /** * @brief Converts the standard button flags into X360 format. * @param gamepad_state The gamepad button/axis state sent from the client. * @return XUSB_BUTTON flags. */ static XUSB_BUTTON x360_buttons(const gamepad_state_t &gamepad_state) { int buttons {}; auto flags = gamepad_state.buttonFlags; if (flags & DPAD_UP) { buttons |= XUSB_GAMEPAD_DPAD_UP; } if (flags & DPAD_DOWN) { buttons |= XUSB_GAMEPAD_DPAD_DOWN; } if (flags & DPAD_LEFT) { buttons |= XUSB_GAMEPAD_DPAD_LEFT; } if (flags & DPAD_RIGHT) { buttons |= XUSB_GAMEPAD_DPAD_RIGHT; } if (flags & START) { buttons |= XUSB_GAMEPAD_START; } if (flags & BACK) { buttons |= XUSB_GAMEPAD_BACK; } if (flags & LEFT_STICK) { buttons |= XUSB_GAMEPAD_LEFT_THUMB; } if (flags & RIGHT_STICK) { buttons |= XUSB_GAMEPAD_RIGHT_THUMB; } if (flags & LEFT_BUTTON) { buttons |= XUSB_GAMEPAD_LEFT_SHOULDER; } if (flags & RIGHT_BUTTON) { buttons |= XUSB_GAMEPAD_RIGHT_SHOULDER; } if (flags & (HOME | MISC_BUTTON)) { buttons |= XUSB_GAMEPAD_GUIDE; } if (flags & A) { buttons |= XUSB_GAMEPAD_A; } if (flags & B) { buttons |= XUSB_GAMEPAD_B; } if (flags & X) { buttons |= XUSB_GAMEPAD_X; } if (flags & Y) { buttons |= XUSB_GAMEPAD_Y; } return (XUSB_BUTTON) buttons; } /** * @brief Updates the X360 input report with the provided gamepad state. * @param gamepad The gamepad to update. * @param gamepad_state The gamepad button/axis state sent from the client. */ static void x360_update_state(gamepad_context_t &gamepad, const gamepad_state_t &gamepad_state) { auto &report = gamepad.report.x360; report.wButtons = x360_buttons(gamepad_state); report.bLeftTrigger = gamepad_state.lt; report.bRightTrigger = gamepad_state.rt; report.sThumbLX = gamepad_state.lsX; report.sThumbLY = gamepad_state.lsY; report.sThumbRX = gamepad_state.rsX; report.sThumbRY = gamepad_state.rsY; } static DS4_DPAD_DIRECTIONS ds4_dpad(const gamepad_state_t &gamepad_state) { auto flags = gamepad_state.buttonFlags; if (flags & DPAD_UP) { if (flags & DPAD_RIGHT) { return DS4_BUTTON_DPAD_NORTHEAST; } else if (flags & DPAD_LEFT) { return DS4_BUTTON_DPAD_NORTHWEST; } else { return DS4_BUTTON_DPAD_NORTH; } } else if (flags & DPAD_DOWN) { if (flags & DPAD_RIGHT) { return DS4_BUTTON_DPAD_SOUTHEAST; } else if (flags & DPAD_LEFT) { return DS4_BUTTON_DPAD_SOUTHWEST; } else { return DS4_BUTTON_DPAD_SOUTH; } } else if (flags & DPAD_RIGHT) { return DS4_BUTTON_DPAD_EAST; } else if (flags & DPAD_LEFT) { return DS4_BUTTON_DPAD_WEST; } return DS4_BUTTON_DPAD_NONE; } /** * @brief Converts the standard button flags into DS4 format. * @param gamepad_state The gamepad button/axis state sent from the client. * @return DS4_BUTTONS flags. */ static DS4_BUTTONS ds4_buttons(const gamepad_state_t &gamepad_state) { int buttons {}; auto flags = gamepad_state.buttonFlags; if (flags & LEFT_STICK) { buttons |= DS4_BUTTON_THUMB_LEFT; } if (flags & RIGHT_STICK) { buttons |= DS4_BUTTON_THUMB_RIGHT; } if (flags & LEFT_BUTTON) { buttons |= DS4_BUTTON_SHOULDER_LEFT; } if (flags & RIGHT_BUTTON) { buttons |= DS4_BUTTON_SHOULDER_RIGHT; } if (flags & START) { buttons |= DS4_BUTTON_OPTIONS; } if (flags & BACK) { buttons |= DS4_BUTTON_SHARE; } if (flags & A) { buttons |= DS4_BUTTON_CROSS; } if (flags & B) { buttons |= DS4_BUTTON_CIRCLE; } if (flags & X) { buttons |= DS4_BUTTON_SQUARE; } if (flags & Y) { buttons |= DS4_BUTTON_TRIANGLE; } if (gamepad_state.lt > 0) { buttons |= DS4_BUTTON_TRIGGER_LEFT; } if (gamepad_state.rt > 0) { buttons |= DS4_BUTTON_TRIGGER_RIGHT; } return (DS4_BUTTONS) buttons; } static DS4_SPECIAL_BUTTONS ds4_special_buttons(const gamepad_state_t &gamepad_state) { int buttons {}; if (gamepad_state.buttonFlags & HOME) { buttons |= DS4_SPECIAL_BUTTON_PS; } // Allow either PS4/PS5 clickpad button or Xbox Series X share button to activate DS4 clickpad if (gamepad_state.buttonFlags & (TOUCHPAD_BUTTON | MISC_BUTTON)) { buttons |= DS4_SPECIAL_BUTTON_TOUCHPAD; } // Manual DS4 emulation: check if BACK button should also trigger DS4 touchpad click if (config::input.gamepad == "ds4"sv && config::input.ds4_back_as_touchpad_click && (gamepad_state.buttonFlags & BACK)) { buttons |= DS4_SPECIAL_BUTTON_TOUCHPAD; } return (DS4_SPECIAL_BUTTONS) buttons; } static std::uint8_t to_ds4_triggerX(std::int16_t v) { return (v + std::numeric_limits::max() / 2 + 1) / 257; } static std::uint8_t to_ds4_triggerY(std::int16_t v) { auto new_v = -((std::numeric_limits::max() / 2 + v - 1)) / 257; return new_v == 0 ? 0xFF : (std::uint8_t) new_v; } /** * @brief Updates the DS4 input report with the provided gamepad state. * @param gamepad The gamepad to update. * @param gamepad_state The gamepad button/axis state sent from the client. */ static void ds4_update_state(gamepad_context_t &gamepad, const gamepad_state_t &gamepad_state) { auto &report = gamepad.report.ds4.Report; report.wButtons = static_cast(ds4_buttons(gamepad_state)) | static_cast(ds4_dpad(gamepad_state)); report.bSpecial = ds4_special_buttons(gamepad_state); report.bTriggerL = gamepad_state.lt; report.bTriggerR = gamepad_state.rt; report.bThumbLX = to_ds4_triggerX(gamepad_state.lsX); report.bThumbLY = to_ds4_triggerY(gamepad_state.lsY); report.bThumbRX = to_ds4_triggerX(gamepad_state.rsX); report.bThumbRY = to_ds4_triggerY(gamepad_state.rsY); } /** * @brief Sends DS4 input with updated timestamps and repeats to keep timestamp updated. * @details Some applications require updated timestamps values to register DS4 input. * @param vigem The global ViGEm context object. * @param nr The global gamepad index. */ void ds4_update_ts_and_send(vigem_t *vigem, int nr) { auto &gamepad = vigem->gamepads[nr]; // Cancel any pending updates. We will requeue one here when we're finished. if (gamepad.repeat_task) { task_pool.cancel(gamepad.repeat_task); gamepad.repeat_task = nullptr; } if (gamepad.gp && vigem_target_is_attached(gamepad.gp.get())) { auto now = std::chrono::steady_clock::now(); auto delta_ns = std::chrono::duration_cast(now - gamepad.last_report_ts); // Timestamp is reported in 5.333us units gamepad.report.ds4.Report.wTimestamp += (uint16_t) (delta_ns.count() / 5333); // Send the report to the virtual device auto status = vigem_target_ds4_update_ex(vigem->client.get(), gamepad.gp.get(), gamepad.report.ds4); if (!VIGEM_SUCCESS(status)) { BOOST_LOG(warning) << "Couldn't send gamepad input to ViGEm ["sv << util::hex(status).to_string_view() << ']'; return; } // Repeat at least every 100ms to keep the 16-bit timestamp field from overflowing gamepad.last_report_ts = now; gamepad.repeat_task = task_pool.pushDelayed(ds4_update_ts_and_send, 100ms, vigem, nr).task_id; } } /** * @brief Updates virtual gamepad with the provided gamepad state. * @param input The input context. * @param nr The gamepad index to update. * @param gamepad_state The gamepad button/axis state sent from the client. */ void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { auto vigem = ((input_raw_t *) input.get())->vigem; // If there is no gamepad support if (!vigem) { return; } auto &gamepad = vigem->gamepads[nr]; if (!gamepad.gp) { return; } VIGEM_ERROR status; if (vigem_target_get_type(gamepad.gp.get()) == Xbox360Wired) { x360_update_state(gamepad, gamepad_state); status = vigem_target_x360_update(vigem->client.get(), gamepad.gp.get(), gamepad.report.x360); if (!VIGEM_SUCCESS(status)) { BOOST_LOG(warning) << "Couldn't send gamepad input to ViGEm ["sv << util::hex(status).to_string_view() << ']'; } } else { ds4_update_state(gamepad, gamepad_state); ds4_update_ts_and_send(vigem, nr); } } /** * @brief Sends a gamepad touch event to the OS. * @param input The global input context. * @param touch The touch event. */ void gamepad_touch(input_t &input, const gamepad_touch_t &touch) { auto vigem = ((input_raw_t *) input.get())->vigem; // If there is no gamepad support if (!vigem) { return; } auto &gamepad = vigem->gamepads[touch.id.globalIndex]; if (!gamepad.gp) { return; } // Touch is only supported on DualShock 4 controllers if (vigem_target_get_type(gamepad.gp.get()) != DualShock4Wired) { return; } auto &report = gamepad.report.ds4.Report; uint8_t pointerIndex; if (touch.eventType == LI_TOUCH_EVENT_DOWN) { if (gamepad.available_pointers & 0x1) { // Reserve pointer index 0 for this touch gamepad.pointer_id_map[touch.pointerId] = pointerIndex = 0; gamepad.available_pointers &= ~(1 << pointerIndex); // Set pointer 0 down report.sCurrentTouch.bIsUpTrackingNum1 &= ~0x80; report.sCurrentTouch.bIsUpTrackingNum1++; } else if (gamepad.available_pointers & 0x2) { // Reserve pointer index 1 for this touch gamepad.pointer_id_map[touch.pointerId] = pointerIndex = 1; gamepad.available_pointers &= ~(1 << pointerIndex); // Set pointer 1 down report.sCurrentTouch.bIsUpTrackingNum2 &= ~0x80; report.sCurrentTouch.bIsUpTrackingNum2++; } else { BOOST_LOG(warning) << "No more free pointer indices! Did the client miss an touch up event?"sv; return; } } else if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) { // Raise both pointers report.sCurrentTouch.bIsUpTrackingNum1 |= 0x80; report.sCurrentTouch.bIsUpTrackingNum2 |= 0x80; // Remove all pointer index mappings gamepad.pointer_id_map.clear(); // All pointers are now available gamepad.available_pointers = 0x3; } else { auto i = gamepad.pointer_id_map.find(touch.pointerId); if (i == gamepad.pointer_id_map.end()) { BOOST_LOG(warning) << "Pointer ID not found! Did the client miss a touch down event?"sv; return; } pointerIndex = (*i).second; if (touch.eventType == LI_TOUCH_EVENT_UP || touch.eventType == LI_TOUCH_EVENT_CANCEL) { // Remove the pointer index mapping gamepad.pointer_id_map.erase(i); // Set pointer up if (pointerIndex == 0) { report.sCurrentTouch.bIsUpTrackingNum1 |= 0x80; } else { report.sCurrentTouch.bIsUpTrackingNum2 |= 0x80; } // Free the pointer index gamepad.available_pointers |= (1 << pointerIndex); } else if (touch.eventType != LI_TOUCH_EVENT_MOVE) { BOOST_LOG(warning) << "Unsupported touch event for gamepad: "sv << (uint32_t) touch.eventType; return; } } // Touchpad is 1920x943 according to ViGEm uint16_t x = touch.x * 1920; uint16_t y = touch.y * 943; uint8_t touchData[] = { (uint8_t) (x & 0xFF), // Low 8 bits of X (uint8_t) ((x >> 8 & 0x0F) | (y & 0x0F) << 4), // High 4 bits of X and low 4 bits of Y (uint8_t) (y >> 4 & 0xFF) // High 8 bits of Y }; report.sCurrentTouch.bPacketCounter++; if (touch.eventType != LI_TOUCH_EVENT_CANCEL_ALL) { if (pointerIndex == 0) { memcpy(report.sCurrentTouch.bTouchData1, touchData, sizeof(touchData)); } else { memcpy(report.sCurrentTouch.bTouchData2, touchData, sizeof(touchData)); } } ds4_update_ts_and_send(vigem, touch.id.globalIndex); } /** * @brief Sends a gamepad motion event to the OS. * @param input The global input context. * @param motion The motion event. */ void gamepad_motion(input_t &input, const gamepad_motion_t &motion) { auto vigem = ((input_raw_t *) input.get())->vigem; // If there is no gamepad support if (!vigem) { return; } auto &gamepad = vigem->gamepads[motion.id.globalIndex]; if (!gamepad.gp) { return; } // Motion is only supported on DualShock 4 controllers if (vigem_target_get_type(gamepad.gp.get()) != DualShock4Wired) { return; } ds4_update_motion(gamepad, motion.motionType, motion.x, motion.y, motion.z); ds4_update_ts_and_send(vigem, motion.id.globalIndex); } /** * @brief Sends a gamepad battery event to the OS. * @param input The global input context. * @param battery The battery event. */ void gamepad_battery(input_t &input, const gamepad_battery_t &battery) { auto vigem = ((input_raw_t *) input.get())->vigem; // If there is no gamepad support if (!vigem) { return; } auto &gamepad = vigem->gamepads[battery.id.globalIndex]; if (!gamepad.gp) { return; } // Battery is only supported on DualShock 4 controllers if (vigem_target_get_type(gamepad.gp.get()) != DualShock4Wired) { return; } // For details on the report format of these battery level fields, see: // https://github.com/torvalds/linux/blob/946c6b59c56dc6e7d8364a8959cb36bf6d10bc37/drivers/hid/hid-playstation.c#L2305-L2314 auto &report = gamepad.report.ds4.Report; // Update the battery state if it is known switch (battery.state) { case LI_BATTERY_STATE_CHARGING: case LI_BATTERY_STATE_DISCHARGING: if (battery.state == LI_BATTERY_STATE_CHARGING) { report.bBatteryLvlSpecial |= 0x10; // Connected via USB } else { report.bBatteryLvlSpecial &= ~0x10; // Not connected via USB } // If there was a special battery status set before, clear that and // initialize the battery level to 50%. It will be overwritten below // if the actual percentage is known. if ((report.bBatteryLvlSpecial & 0xF) > 0xA) { report.bBatteryLvlSpecial = (report.bBatteryLvlSpecial & ~0xF) | 0x5; } break; case LI_BATTERY_STATE_FULL: report.bBatteryLvlSpecial = 0x1B; // USB + Battery Full report.bBatteryLvl = 0xFF; break; case LI_BATTERY_STATE_NOT_PRESENT: case LI_BATTERY_STATE_NOT_CHARGING: report.bBatteryLvlSpecial = 0x1F; // USB + Charging Error break; default: break; } // Update the battery level if it is known if (battery.percentage != LI_BATTERY_PERCENTAGE_UNKNOWN) { report.bBatteryLvl = battery.percentage * 255 / 100; // Don't overwrite low nibble if there's a special status there (see above) if ((report.bBatteryLvlSpecial & 0x10) && (report.bBatteryLvlSpecial & 0xF) <= 0xA) { report.bBatteryLvlSpecial = (report.bBatteryLvlSpecial & ~0xF) | ((battery.percentage + 5) / 10); } } ds4_update_ts_and_send(vigem, battery.id.globalIndex); } void freeInput(void *p) { auto input = (input_raw_t *) p; delete input; } std::vector &supported_gamepads(input_t *input) { if (!input) { static std::vector gps { supported_gamepad_t {"auto", true, ""}, supported_gamepad_t {"x360", false, ""}, supported_gamepad_t {"ds4", false, ""}, }; return gps; } auto vigem = ((input_raw_t *) input)->vigem; auto enabled = vigem != nullptr; auto reason = enabled ? "" : "gamepads.vigem-not-available"; // ds4 == ps4 static std::vector gps { supported_gamepad_t {"auto", true, reason}, supported_gamepad_t {"x360", enabled, reason}, supported_gamepad_t {"ds4", enabled, reason} }; for (auto &[name, is_enabled, reason_disabled] : gps) { if (!is_enabled) { BOOST_LOG(warning) << "Gamepad " << name << " is disabled due to " << reason_disabled; } } return gps; } /** * @brief Returns the supported platform capabilities to advertise to the client. * @return Capability flags. */ platform_caps::caps_t get_capabilities() { platform_caps::caps_t caps = 0; // We support controller touchpad input as long as we're not emulating X360 if (config::input.gamepad != "x360"sv) { caps |= platform_caps::controller_touch; } // We support pen and touch input on Win10 1809+ if (GetProcAddress(GetModuleHandleA("user32.dll"), "CreateSyntheticPointerDevice") != nullptr) { if (config::input.native_pen_touch) { caps |= platform_caps::pen_touch; } } else { BOOST_LOG(warning) << "Touch input requires Windows 10 1809 or later"sv; } return caps; } } // namespace platf ================================================ FILE: src/platform/windows/keylayout.h ================================================ /** * @file src/platform/windows/keylayout.h * @brief Keyboard layout mapping for scancode translation */ #pragma once // standard includes #include #include namespace platf { // Virtual Key to Scan Code mapping for the US English layout (00000409). // GameStream uses this as the canonical key layout for scancode conversion. constexpr std::array::max() + 1> VK_TO_SCANCODE_MAP { 0, /* 0x00 */ 0, /* 0x01 */ 0, /* 0x02 */ 70, /* 0x03 */ 0, /* 0x04 */ 0, /* 0x05 */ 0, /* 0x06 */ 0, /* 0x07 */ 14, /* 0x08 */ 15, /* 0x09 */ 0, /* 0x0a */ 0, /* 0x0b */ 76, /* 0x0c */ 28, /* 0x0d */ 0, /* 0x0e */ 0, /* 0x0f */ 42, /* 0x10 */ 29, /* 0x11 */ 56, /* 0x12 */ 0, /* 0x13 */ 58, /* 0x14 */ 0, /* 0x15 */ 0, /* 0x16 */ 0, /* 0x17 */ 0, /* 0x18 */ 0, /* 0x19 */ 0, /* 0x1a */ 1, /* 0x1b */ 0, /* 0x1c */ 0, /* 0x1d */ 0, /* 0x1e */ 0, /* 0x1f */ 57, /* 0x20 */ 73, /* 0x21 */ 81, /* 0x22 */ 79, /* 0x23 */ 71, /* 0x24 */ 75, /* 0x25 */ 72, /* 0x26 */ 77, /* 0x27 */ 80, /* 0x28 */ 0, /* 0x29 */ 0, /* 0x2a */ 0, /* 0x2b */ 84, /* 0x2c */ 82, /* 0x2d */ 83, /* 0x2e */ 99, /* 0x2f */ 11, /* 0x30 */ 2, /* 0x31 */ 3, /* 0x32 */ 4, /* 0x33 */ 5, /* 0x34 */ 6, /* 0x35 */ 7, /* 0x36 */ 8, /* 0x37 */ 9, /* 0x38 */ 10, /* 0x39 */ 0, /* 0x3a */ 0, /* 0x3b */ 0, /* 0x3c */ 0, /* 0x3d */ 0, /* 0x3e */ 0, /* 0x3f */ 0, /* 0x40 */ 30, /* 0x41 */ 48, /* 0x42 */ 46, /* 0x43 */ 32, /* 0x44 */ 18, /* 0x45 */ 33, /* 0x46 */ 34, /* 0x47 */ 35, /* 0x48 */ 23, /* 0x49 */ 36, /* 0x4a */ 37, /* 0x4b */ 38, /* 0x4c */ 50, /* 0x4d */ 49, /* 0x4e */ 24, /* 0x4f */ 25, /* 0x50 */ 16, /* 0x51 */ 19, /* 0x52 */ 31, /* 0x53 */ 20, /* 0x54 */ 22, /* 0x55 */ 47, /* 0x56 */ 17, /* 0x57 */ 45, /* 0x58 */ 21, /* 0x59 */ 44, /* 0x5a */ 91, /* 0x5b */ 92, /* 0x5c */ 93, /* 0x5d */ 0, /* 0x5e */ 95, /* 0x5f */ 82, /* 0x60 */ 79, /* 0x61 */ 80, /* 0x62 */ 81, /* 0x63 */ 75, /* 0x64 */ 76, /* 0x65 */ 77, /* 0x66 */ 71, /* 0x67 */ 72, /* 0x68 */ 73, /* 0x69 */ 55, /* 0x6a */ 78, /* 0x6b */ 0, /* 0x6c */ 74, /* 0x6d */ 83, /* 0x6e */ 53, /* 0x6f */ 59, /* 0x70 */ 60, /* 0x71 */ 61, /* 0x72 */ 62, /* 0x73 */ 63, /* 0x74 */ 64, /* 0x75 */ 65, /* 0x76 */ 66, /* 0x77 */ 67, /* 0x78 */ 68, /* 0x79 */ 87, /* 0x7a */ 88, /* 0x7b */ 100, /* 0x7c */ 101, /* 0x7d */ 102, /* 0x7e */ 103, /* 0x7f */ 104, /* 0x80 */ 105, /* 0x81 */ 106, /* 0x82 */ 107, /* 0x83 */ 108, /* 0x84 */ 109, /* 0x85 */ 110, /* 0x86 */ 118, /* 0x87 */ 0, /* 0x88 */ 0, /* 0x89 */ 0, /* 0x8a */ 0, /* 0x8b */ 0, /* 0x8c */ 0, /* 0x8d */ 0, /* 0x8e */ 0, /* 0x8f */ 69, /* 0x90 */ 70, /* 0x91 */ 0, /* 0x92 */ 0, /* 0x93 */ 0, /* 0x94 */ 0, /* 0x95 */ 0, /* 0x96 */ 0, /* 0x97 */ 0, /* 0x98 */ 0, /* 0x99 */ 0, /* 0x9a */ 0, /* 0x9b */ 0, /* 0x9c */ 0, /* 0x9d */ 0, /* 0x9e */ 0, /* 0x9f */ 42, /* 0xa0 */ 54, /* 0xa1 */ 29, /* 0xa2 */ 29, /* 0xa3 */ 56, /* 0xa4 */ 56, /* 0xa5 */ 106, /* 0xa6 */ 105, /* 0xa7 */ 103, /* 0xa8 */ 104, /* 0xa9 */ 101, /* 0xaa */ 102, /* 0xab */ 50, /* 0xac */ 32, /* 0xad */ 46, /* 0xae */ 48, /* 0xaf */ 25, /* 0xb0 */ 16, /* 0xb1 */ 36, /* 0xb2 */ 34, /* 0xb3 */ 108, /* 0xb4 */ 109, /* 0xb5 */ 107, /* 0xb6 */ 33, /* 0xb7 */ 0, /* 0xb8 */ 0, /* 0xb9 */ 39, /* 0xba */ 13, /* 0xbb */ 51, /* 0xbc */ 12, /* 0xbd */ 52, /* 0xbe */ 53, /* 0xbf */ 41, /* 0xc0 */ 115, /* 0xc1 */ 126, /* 0xc2 */ 0, /* 0xc3 */ 0, /* 0xc4 */ 0, /* 0xc5 */ 0, /* 0xc6 */ 0, /* 0xc7 */ 0, /* 0xc8 */ 0, /* 0xc9 */ 0, /* 0xca */ 0, /* 0xcb */ 0, /* 0xcc */ 0, /* 0xcd */ 0, /* 0xce */ 0, /* 0xcf */ 0, /* 0xd0 */ 0, /* 0xd1 */ 0, /* 0xd2 */ 0, /* 0xd3 */ 0, /* 0xd4 */ 0, /* 0xd5 */ 0, /* 0xd6 */ 0, /* 0xd7 */ 0, /* 0xd8 */ 0, /* 0xd9 */ 0, /* 0xda */ 26, /* 0xdb */ 43, /* 0xdc */ 27, /* 0xdd */ 40, /* 0xde */ 0, /* 0xdf */ 0, /* 0xe0 */ 0, /* 0xe1 */ 86, /* 0xe2 */ 0, /* 0xe3 */ 0, /* 0xe4 */ 0, /* 0xe5 */ 0, /* 0xe6 */ 0, /* 0xe7 */ 0, /* 0xe8 */ 113, /* 0xe9 */ 92, /* 0xea */ 123, /* 0xeb */ 0, /* 0xec */ 111, /* 0xed */ 90, /* 0xee */ 0, /* 0xef */ 0, /* 0xf0 */ 91, /* 0xf1 */ 0, /* 0xf2 */ 95, /* 0xf3 */ 0, /* 0xf4 */ 94, /* 0xf5 */ 0, /* 0xf6 */ 0, /* 0xf7 */ 0, /* 0xf8 */ 93, /* 0xf9 */ 0, /* 0xfa */ 98, /* 0xfb */ 0, /* 0xfc */ 0, /* 0xfd */ 0, /* 0xfe */ 0, /* 0xff */ }; } // namespace platf ================================================ FILE: src/platform/windows/misc.cpp ================================================ /** * @file src/platform/windows/misc.cpp * @brief Miscellaneous definitions for Windows. */ // standard includes #include #include #include #include #include #include #ifndef BOOST_PROCESS_VERSION #define BOOST_PROCESS_VERSION 1 #endif // lib includes #include #include #include #include #include #include // prevent clang format from "optimizing" the header include order // clang-format off #include #include #include #include #include #include #include #include #include #include #include #include // clang-format on // Boost overrides NTDDI_VERSION, so we re-override it here #undef NTDDI_VERSION #define NTDDI_VERSION NTDDI_WIN10 #include // local includes #include "misc.h" #include "utils.h" #include "nvprefs/nvprefs_interface.h" #include "src/entry_handler.h" #include "src/globals.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" // UDP_SEND_MSG_SIZE was added in the Windows 10 20H1 SDK #ifndef UDP_SEND_MSG_SIZE #define UDP_SEND_MSG_SIZE 2 #endif // PROC_THREAD_ATTRIBUTE_JOB_LIST is currently missing from MinGW headers #ifndef PROC_THREAD_ATTRIBUTE_JOB_LIST #define PROC_THREAD_ATTRIBUTE_JOB_LIST ProcThreadAttributeValue(13, FALSE, TRUE, FALSE) #endif #include #ifndef WLAN_API_MAKE_VERSION #define WLAN_API_MAKE_VERSION(_major, _minor) (((DWORD) (_minor)) << 16 | (_major)) #endif #include extern "C" { NTSTATUS NTAPI NtSetTimerResolution(ULONG DesiredResolution, BOOLEAN SetResolution, PULONG CurrentResolution); } namespace { std::atomic used_nt_set_timer_resolution = false; bool nt_set_timer_resolution_max() { ULONG minimum, maximum, current; if (!NT_SUCCESS(NtQueryTimerResolution(&minimum, &maximum, ¤t)) || !NT_SUCCESS(NtSetTimerResolution(maximum, TRUE, ¤t))) { return false; } return true; } bool nt_set_timer_resolution_min() { ULONG minimum, maximum, current; if (!NT_SUCCESS(NtQueryTimerResolution(&minimum, &maximum, ¤t)) || !NT_SUCCESS(NtSetTimerResolution(minimum, TRUE, ¤t))) { return false; } return true; } } // namespace namespace bp = boost::process; static std::string ensureCrLf(const std::string& utf8Str); static std::wstring getClipboardData(); static int setClipboardData(const std::wstring& utf16Str); using namespace std::literals; namespace platf { using adapteraddrs_t = util::c_ptr; bool enabled_mouse_keys = false; MOUSEKEYS previous_mouse_keys_state; HANDLE qos_handle = nullptr; decltype(QOSCreateHandle) *fn_QOSCreateHandle = nullptr; decltype(QOSAddSocketToFlow) *fn_QOSAddSocketToFlow = nullptr; decltype(QOSRemoveSocketFromFlow) *fn_QOSRemoveSocketFromFlow = nullptr; HANDLE wlan_handle = nullptr; decltype(WlanOpenHandle) *fn_WlanOpenHandle = nullptr; decltype(WlanCloseHandle) *fn_WlanCloseHandle = nullptr; decltype(WlanFreeMemory) *fn_WlanFreeMemory = nullptr; decltype(WlanEnumInterfaces) *fn_WlanEnumInterfaces = nullptr; decltype(WlanSetInterface) *fn_WlanSetInterface = nullptr; std::filesystem::path appdata() { WCHAR sunshine_path[MAX_PATH]; GetModuleFileNameW(nullptr, sunshine_path, _countof(sunshine_path)); return std::filesystem::path {sunshine_path}.remove_filename() / L"config"sv; } std::string from_sockaddr(const sockaddr *const socket_address) { char data[INET6_ADDRSTRLEN] = {}; auto family = socket_address->sa_family; if (family == AF_INET6) { inet_ntop(AF_INET6, &((sockaddr_in6 *) socket_address)->sin6_addr, data, INET6_ADDRSTRLEN); } else if (family == AF_INET) { inet_ntop(AF_INET, &((sockaddr_in *) socket_address)->sin_addr, data, INET_ADDRSTRLEN); } return std::string {data}; } std::pair from_sockaddr_ex(const sockaddr *const ip_addr) { char data[INET6_ADDRSTRLEN] = {}; auto family = ip_addr->sa_family; std::uint16_t port = 0; if (family == AF_INET6) { inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); port = ((sockaddr_in6 *) ip_addr)->sin6_port; } else if (family == AF_INET) { inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN); port = ((sockaddr_in *) ip_addr)->sin_port; } return {port, std::string {data}}; } adapteraddrs_t get_adapteraddrs() { adapteraddrs_t info {nullptr}; ULONG size = 0; while (GetAdaptersAddresses(AF_UNSPEC, 0, nullptr, info.get(), &size) == ERROR_BUFFER_OVERFLOW) { info.reset((PIP_ADAPTER_ADDRESSES) malloc(size)); } return info; } std::string get_mac_address(const std::string_view &address) { adapteraddrs_t info = get_adapteraddrs(); for (auto adapter_pos = info.get(); adapter_pos != nullptr; adapter_pos = adapter_pos->Next) { for (auto addr_pos = adapter_pos->FirstUnicastAddress; addr_pos != nullptr; addr_pos = addr_pos->Next) { if (adapter_pos->PhysicalAddressLength != 0 && address == from_sockaddr(addr_pos->Address.lpSockaddr)) { std::stringstream mac_addr; mac_addr << std::hex; for (int i = 0; i < adapter_pos->PhysicalAddressLength; i++) { if (i > 0) { mac_addr << ':'; } mac_addr << std::setw(2) << std::setfill('0') << (int) adapter_pos->PhysicalAddress[i]; } return mac_addr.str(); } } } BOOST_LOG(debug) << "Unable to find MAC address for "sv << address << ", is this a virtual network adapter?"; return "00:00:00:00:00:00"s; } std::string get_local_ip_for_gateway() { PIP_ADAPTER_INFO pAdapterInfo; PIP_ADAPTER_INFO pAdapter = nullptr; DWORD dwRetVal = 0; ULONG ulOutBufLen = sizeof(IP_ADAPTER_INFO); pAdapterInfo = (IP_ADAPTER_INFO *)malloc(sizeof(IP_ADAPTER_INFO)); if (pAdapterInfo == nullptr) { BOOST_LOG(warning) << "Error allocating memory needed to call GetAdaptersInfo"; return ""; } // Make an initial call to GetAdaptersInfo to get the necessary size into the ulOutBufLen variable if (GetAdaptersInfo(pAdapterInfo, &ulOutBufLen) == ERROR_BUFFER_OVERFLOW) { free(pAdapterInfo); pAdapterInfo = (IP_ADAPTER_INFO *)malloc(ulOutBufLen); if (pAdapterInfo == nullptr) { BOOST_LOG(warning) << "Error allocating memory needed to call GetAdaptersInfo"; return ""; } } if ((dwRetVal = GetAdaptersInfo(pAdapterInfo, &ulOutBufLen)) != NO_ERROR) { if (pAdapterInfo) { free(pAdapterInfo); } BOOST_LOG(warning) << "GetAdaptersInfo failed with error: " + std::to_string(dwRetVal); return ""; } pAdapter = pAdapterInfo; std::string local_ip; // Iterate through the list of adapters while (pAdapter) { IP_ADDR_STRING* pGateway = &pAdapter->GatewayList; if (pGateway && pGateway->IpAddress.String[0] != '\0') { // This adapter has a default gateway, use its IP address local_ip = pAdapter->IpAddressList.IpAddress.String; break; } pAdapter = pAdapter->Next; } if (pAdapterInfo) { free(pAdapterInfo); } if (local_ip.empty()) { BOOST_LOG(warning) << "No associated IP address found for the default gateway"; } return local_ip; } HDESK syncThreadDesktop() { auto hDesk = OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, FALSE, GENERIC_ALL); if (!hDesk) { auto err = GetLastError(); BOOST_LOG(error) << "Failed to Open Input Desktop [0x"sv << util::hex(err).to_string_view() << ']'; return nullptr; } if (!SetThreadDesktop(hDesk)) { auto err = GetLastError(); BOOST_LOG(error) << "Failed to sync desktop to thread [0x"sv << util::hex(err).to_string_view() << ']'; } CloseDesktop(hDesk); return hDesk; } void print_status(const std::string_view &prefix, HRESULT status) { char err_string[1024]; DWORD bytes = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, status, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), err_string, sizeof(err_string), nullptr); BOOST_LOG(error) << prefix << ": "sv << std::string_view {err_string, bytes}; } bool IsUserAdmin(HANDLE user_token) { WINBOOL ret; SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY; PSID AdministratorsGroup; ret = AllocateAndInitializeSid( &NtAuthority, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &AdministratorsGroup ); if (ret) { if (!CheckTokenMembership(user_token, AdministratorsGroup, &ret)) { ret = false; BOOST_LOG(error) << "Failed to verify token membership for administrative access: " << GetLastError(); } FreeSid(AdministratorsGroup); } else { BOOST_LOG(error) << "Unable to allocate SID to check administrative access: " << GetLastError(); } return ret; } /** * @brief Obtain the current sessions user's primary token with elevated privileges. * @return The user's token. If user has admin capability it will be elevated, otherwise it will be a limited token. On error, `nullptr`. */ HANDLE retrieve_users_token(bool elevated) { DWORD consoleSessionId; HANDLE userToken; TOKEN_ELEVATION_TYPE elevationType; DWORD dwSize; // Get the session ID of the active console session consoleSessionId = WTSGetActiveConsoleSessionId(); if (0xFFFFFFFF == consoleSessionId) { // If there is no active console session, log a warning and return null BOOST_LOG(warning) << "There isn't an active user session, therefore it is not possible to execute commands under the users profile."; return nullptr; } // Get the user token for the active console session if (!WTSQueryUserToken(consoleSessionId, &userToken)) { BOOST_LOG(debug) << "QueryUserToken failed, this would prevent commands from launching under the users profile."; return nullptr; } // We need to know if this is an elevated token or not. // Get the elevation type of the user token // Elevation - Default: User is not an admin, UAC enabled/disabled does not matter. // Elevation - Limited: User is an admin, has UAC enabled. // Elevation - Full: User is an admin, has UAC disabled. if (!GetTokenInformation(userToken, TokenElevationType, &elevationType, sizeof(TOKEN_ELEVATION_TYPE), &dwSize)) { BOOST_LOG(debug) << "Retrieving token information failed: " << GetLastError(); CloseHandle(userToken); return nullptr; } // User is currently not an administrator // The documentation for this scenario is conflicting, so we'll double check to see if user is actually an admin. if (elevated && (elevationType == TokenElevationTypeDefault && !IsUserAdmin(userToken))) { // 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. BOOST_LOG(warning) << "This command requires elevation and the current user account logged in does not have administrator rights. " << "For security reasons Sunshine will retain the same access level as the current user and will not elevate it."; } // User has a limited token, this means they have UAC enabled and is an Administrator if (elevated && elevationType == TokenElevationTypeLimited) { TOKEN_LINKED_TOKEN linkedToken; // Retrieve the administrator token that is linked to the limited token if (!GetTokenInformation(userToken, TokenLinkedToken, reinterpret_cast(&linkedToken), sizeof(TOKEN_LINKED_TOKEN), &dwSize)) { // If the retrieval failed, log an error message and return null BOOST_LOG(error) << "Retrieving linked token information failed: " << GetLastError(); CloseHandle(userToken); // There is no scenario where this should be hit, except for an actual error. return nullptr; } // Since we need the elevated token, we'll replace it with their administrative token. CloseHandle(userToken); userToken = linkedToken.LinkedToken; } // We don't need to do anything for TokenElevationTypeFull users here, because they're already elevated. return userToken; } bool merge_user_environment_block(bp::environment &env, HANDLE shell_token) { // Get the target user's environment block PVOID env_block; if (!CreateEnvironmentBlock(&env_block, shell_token, FALSE)) { return false; } // Parse the environment block and populate env for (auto c = (PWCHAR) env_block; *c != UNICODE_NULL; c += wcslen(c) + 1) { // Environment variable entries end with a null-terminator, so std::wstring() will get an entire entry. std::string env_tuple = to_utf8(std::wstring {c}); std::string env_name = env_tuple.substr(0, env_tuple.find('=')); std::string env_val = env_tuple.substr(env_tuple.find('=') + 1); // Perform a case-insensitive search to see if this variable name already exists auto itr = std::find_if(env.cbegin(), env.cend(), [&](const auto &e) { return boost::iequals(e.get_name(), env_name); }); if (itr != env.cend()) { // Use this existing name if it is already present to ensure we merge properly env_name = itr->get_name(); } // For the PATH variable, we will merge the values together if (boost::iequals(env_name, "PATH")) { env[env_name] = env_val + ";" + env[env_name].to_string(); } else { // Other variables will be superseded by those in the user's environment block env[env_name] = env_val; } } DestroyEnvironmentBlock(env_block); return true; } /** * @brief Check if the current process is running with system-level privileges. * @return `true` if the current process has system-level privileges, `false` otherwise. */ bool is_running_as_system() { BOOL ret; PSID SystemSid; DWORD dwSize = SECURITY_MAX_SID_SIZE; // Allocate memory for the SID structure SystemSid = LocalAlloc(LMEM_FIXED, dwSize); if (SystemSid == nullptr) { BOOST_LOG(error) << "Failed to allocate memory for the SID structure: " << GetLastError(); return false; } // Create a SID for the local system account ret = CreateWellKnownSid(WinLocalSystemSid, nullptr, SystemSid, &dwSize); if (ret) { // Check if the current process token contains this SID if (!CheckTokenMembership(nullptr, SystemSid, &ret)) { BOOST_LOG(error) << "Failed to check token membership: " << GetLastError(); ret = false; } } else { 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(); } // Free the memory allocated for the SID structure LocalFree(SystemSid); return ret; } // Note: This does NOT append a null terminator void append_string_to_environment_block(wchar_t *env_block, int &offset, const std::wstring &wstr) { std::memcpy(&env_block[offset], wstr.data(), wstr.length() * sizeof(wchar_t)); offset += wstr.length(); } std::wstring create_environment_block(bp::environment &env) { int size = 0; for (const auto &entry : env) { auto name = entry.get_name(); auto value = entry.to_string(); size += from_utf8(name).length() + 1 /* L'=' */ + from_utf8(value).length() + 1 /* L'\0' */; } size += 1 /* L'\0' */; wchar_t env_block[size]; int offset = 0; for (const auto &entry : env) { auto name = entry.get_name(); auto value = entry.to_string(); // Construct the NAME=VAL\0 string append_string_to_environment_block(env_block, offset, from_utf8(name)); env_block[offset++] = L'='; append_string_to_environment_block(env_block, offset, from_utf8(value)); env_block[offset++] = L'\0'; } // Append a final null terminator env_block[offset++] = L'\0'; return std::wstring(env_block, offset); } LPPROC_THREAD_ATTRIBUTE_LIST allocate_proc_thread_attr_list(DWORD attribute_count) { SIZE_T size; InitializeProcThreadAttributeList(nullptr, attribute_count, 0, &size); auto list = (LPPROC_THREAD_ATTRIBUTE_LIST) HeapAlloc(GetProcessHeap(), 0, size); if (list == nullptr) { return nullptr; } if (!InitializeProcThreadAttributeList(list, attribute_count, 0, &size)) { HeapFree(GetProcessHeap(), 0, list); return nullptr; } return list; } void free_proc_thread_attr_list(LPPROC_THREAD_ATTRIBUTE_LIST list) { DeleteProcThreadAttributeList(list); HeapFree(GetProcessHeap(), 0, list); } /** * @brief Create a `bp::child` object from the results of launching a process. * @param process_launched A boolean indicating if the launch was successful. * @param cmd The command that was used to launch the process. * @param ec A reference to an `std::error_code` object that will store any error that occurred during the launch. * @param process_info A reference to a `PROCESS_INFORMATION` structure that contains information about the new process. * @return A `bp::child` object representing the new process, or an empty `bp::child` object if the launch failed. */ bp::child create_boost_child_from_results(bool process_launched, const std::string &cmd, std::error_code &ec, PROCESS_INFORMATION &process_info) { // Use RAII to ensure the process is closed when we're done with it, even if there was an error. auto close_process_handles = util::fail_guard([process_launched, process_info]() { if (process_launched) { CloseHandle(process_info.hThread); CloseHandle(process_info.hProcess); } }); if (ec) { // If there was an error, return an empty bp::child object return bp::child(); } if (process_launched) { // If the launch was successful, create a new bp::child object representing the new process auto child = bp::child((bp::pid_t) process_info.dwProcessId); BOOST_LOG(info) << cmd << " running with PID "sv << child.id(); return child; } else { auto winerror = GetLastError(); BOOST_LOG(error) << "Failed to launch process: "sv << winerror; ec = std::make_error_code(std::errc::invalid_argument); // We must NOT attach the failed process here, since this case can potentially be induced by ACL // manipulation (denying yourself execute permission) to cause an escalation of privilege. // So to protect ourselves against that, we'll return an empty child process instead. return bp::child(); } } /** * @brief Impersonate the current user and invoke the callback function. * @param user_token A handle to the user's token that was obtained from the shell. * @param callback A function that will be executed while impersonating the user. * @return Object that will store any error that occurred during the impersonation */ std::error_code impersonate_current_user(HANDLE user_token, std::function callback) { std::error_code ec; // Impersonate the user when launching the process. This will ensure that appropriate access // checks are done against the user token, not our SYSTEM token. It will also allow network // shares and mapped network drives to be used as launch targets, since those credentials // are stored per-user. if (!ImpersonateLoggedOnUser(user_token)) { auto winerror = GetLastError(); // Log the failure of impersonating the user and its error code BOOST_LOG(error) << "Failed to impersonate user: "sv << winerror; ec = std::make_error_code(std::errc::permission_denied); return ec; } // Execute the callback function while impersonating the user callback(); // End impersonation of the logged on user. If this fails (which is extremely unlikely), // we will be running with an unknown user token. The only safe thing to do in that case // is terminate ourselves. if (!RevertToSelf()) { auto winerror = GetLastError(); // Log the failure of reverting to self and its error code BOOST_LOG(fatal) << "Failed to revert to self after impersonation: "sv << winerror; DebugBreak(); } return ec; } /** * @brief Create a `STARTUPINFOEXW` structure for launching a process. * @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. * @param job A job object handle to insert the new process into. This pointer must remain valid for the life of this startup info! * @param ec A reference to a `std::error_code` object that will store any error that occurred during the creation of the structure. * @return A structure that contains information about how to launch the new process. */ STARTUPINFOEXW create_startup_info(FILE *file, HANDLE *job, std::error_code &ec) { // Initialize a zeroed-out STARTUPINFOEXW structure and set its size STARTUPINFOEXW startup_info = {}; startup_info.StartupInfo.cb = sizeof(startup_info); // Allocate a process attribute list with space for 2 elements startup_info.lpAttributeList = allocate_proc_thread_attr_list(2); if (startup_info.lpAttributeList == nullptr) { // If the allocation failed, set ec to an appropriate error code and return the structure ec = std::make_error_code(std::errc::not_enough_memory); return startup_info; } if (file) { // If a file was provided, get its handle and use it as the standard output and error for the new process HANDLE log_file_handle = (HANDLE) _get_osfhandle(_fileno(file)); // Populate std handles if the caller gave us a log file to use startup_info.StartupInfo.dwFlags |= STARTF_USESTDHANDLES; startup_info.StartupInfo.hStdInput = nullptr; startup_info.StartupInfo.hStdOutput = log_file_handle; startup_info.StartupInfo.hStdError = log_file_handle; // Allow the log file handle to be inherited by the child process (without inheriting all of // our inheritable handles, such as our own log file handle created by SunshineSvc). // // Note: The value we point to here must be valid for the lifetime of the attribute list, // so we need to point into the STARTUPINFO instead of our log_file_variable on the stack. UpdateProcThreadAttribute(startup_info.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &startup_info.StartupInfo.hStdOutput, sizeof(startup_info.StartupInfo.hStdOutput), nullptr, nullptr); } if (job) { // Atomically insert the new process into the specified job. // // Note: The value we point to here must be valid for the lifetime of the attribute list, // so we take a HANDLE* instead of just a HANDLE to use the caller's stack storage. UpdateProcThreadAttribute(startup_info.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_JOB_LIST, job, sizeof(*job), nullptr, nullptr); } return startup_info; } /** * @brief This function overrides HKEY_CURRENT_USER and HKEY_CLASSES_ROOT using the provided token. * @param token The primary token identifying the user to use, or `nullptr` to restore original keys. * @return `true` if the override or restore operation was successful. */ bool override_per_user_predefined_keys(HANDLE token) { HKEY user_classes_root = nullptr; if (token) { auto err = RegOpenUserClassesRoot(token, 0, GENERIC_ALL, &user_classes_root); if (err != ERROR_SUCCESS) { BOOST_LOG(error) << "Failed to open classes root for target user: "sv << err; return false; } } auto close_classes_root = util::fail_guard([user_classes_root]() { if (user_classes_root) { RegCloseKey(user_classes_root); } }); HKEY user_key = nullptr; if (token) { impersonate_current_user(token, [&]() { // RegOpenCurrentUser() doesn't take a token. It assumes we're impersonating the desired user. auto err = RegOpenCurrentUser(GENERIC_ALL, &user_key); if (err != ERROR_SUCCESS) { BOOST_LOG(error) << "Failed to open user key for target user: "sv << err; user_key = nullptr; } }); if (!user_key) { return false; } } auto close_user = util::fail_guard([user_key]() { if (user_key) { RegCloseKey(user_key); } }); auto err = RegOverridePredefKey(HKEY_CLASSES_ROOT, user_classes_root); if (err != ERROR_SUCCESS) { BOOST_LOG(error) << "Failed to override HKEY_CLASSES_ROOT: "sv << err; return false; } err = RegOverridePredefKey(HKEY_CURRENT_USER, user_key); if (err != ERROR_SUCCESS) { BOOST_LOG(error) << "Failed to override HKEY_CURRENT_USER: "sv << err; RegOverridePredefKey(HKEY_CLASSES_ROOT, nullptr); return false; } return true; } /** * @brief Quote/escape an argument according to the Windows parsing convention. * @param argument The raw argument to process. * @return An argument string suitable for use by CreateProcess(). */ std::wstring escape_argument(const std::wstring &argument) { // If there are no characters requiring quoting/escaping, we're done if (argument.find_first_of(L" \t\n\v\"") == argument.npos) { return argument; } // The algorithm implemented here comes from a MSDN blog post: // https://web.archive.org/web/20120201194949/http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx std::wstring escaped_arg; escaped_arg.push_back(L'"'); for (auto it = argument.begin();; it++) { auto backslash_count = 0U; while (it != argument.end() && *it == L'\\') { it++; backslash_count++; } if (it == argument.end()) { escaped_arg.append(backslash_count * 2, L'\\'); break; } else if (*it == L'"') { escaped_arg.append(backslash_count * 2 + 1, L'\\'); } else { escaped_arg.append(backslash_count, L'\\'); } escaped_arg.push_back(*it); } escaped_arg.push_back(L'"'); return escaped_arg; } /** * @brief Escape an argument according to cmd's parsing convention. * @param argument An argument already escaped by `escape_argument()`. * @return An argument string suitable for use by cmd.exe. */ std::wstring escape_argument_for_cmd(const std::wstring &argument) { // Start with the original string and modify from there std::wstring escaped_arg = argument; // Look for the next cmd metacharacter size_t match_pos = 0; while ((match_pos = escaped_arg.find_first_of(L"()%!^\"<>&|", match_pos)) != std::wstring::npos) { // Insert an escape character and skip past the match escaped_arg.insert(match_pos, 1, L'^'); match_pos += 2; } return escaped_arg; } /** * @brief Resolve the given raw command into a proper command string for CreateProcess(). * @details This converts URLs and non-executable file paths into a runnable command like ShellExecute(). * @param raw_cmd The raw command provided by the user. * @param working_dir The working directory for the new process. * @param token The user token currently being impersonated or `nullptr` if running as ourselves. * @param creation_flags The creation flags for CreateProcess(), which may be modified by this function. * @return A command string suitable for use by CreateProcess(). */ std::wstring resolve_command_string(const std::string &raw_cmd, const std::wstring &working_dir, HANDLE token, DWORD &creation_flags) { std::wstring raw_cmd_w = from_utf8(raw_cmd); // First, convert the given command into parts so we can get the executable/file/URL without parameters auto raw_cmd_parts = boost::program_options::split_winmain(raw_cmd_w); if (raw_cmd_parts.empty()) { // This is highly unexpected, but we'll just return the raw string and hope for the best. BOOST_LOG(warning) << "Failed to split command string: "sv << raw_cmd; return from_utf8(raw_cmd); } auto raw_target = raw_cmd_parts.at(0); if (PathIsURLW(raw_target.c_str())) { // If the target is a URL, handle it directly with rundll32.exe std::wstring cmd = L"rundll32.exe url.dll,FileProtocolHandler " + raw_target; return cmd; } // If the target is not a URL, assume it's a regular file path auto extension = PathFindExtensionW(raw_target.c_str()); if (extension == nullptr || *extension == 0) { // If the file has no extension, assume it's a command and allow CreateProcess() // to try to find it via PATH return from_utf8(raw_cmd); } else if (boost::iequals(extension, L".exe")) { // If the file has an .exe extension, we will bypass the resolution here and // directly pass the unmodified command string to CreateProcess(). The argument // escaping rules are subtly different between CreateProcess() and ShellExecute(), // and we want to preserve backwards compatibility with older configs. return from_utf8(raw_cmd); } // For regular files, the class is found using the file extension (including the dot) std::wstring lookup_string = extension; HRESULT res; std::array shell_command_string; bool needs_cmd_escaping = false; { // Overriding these predefined keys affects process-wide state, so serialize all calls // to ensure the handle state is consistent while we perform the command query. static std::mutex per_user_key_mutex; auto lg = std::lock_guard(per_user_key_mutex); // Override HKEY_CLASSES_ROOT and HKEY_CURRENT_USER to ensure we query the correct class info if (!override_per_user_predefined_keys(token)) { return from_utf8(raw_cmd); } // Find the command string for the specified class DWORD out_len = shell_command_string.size(); res = AssocQueryStringW(ASSOCF_NOTRUNCATE, ASSOCSTR_COMMAND, lookup_string.c_str(), L"open", shell_command_string.data(), &out_len); // In some cases (UWP apps), we might not have a command for this target. If that happens, // we'll have to launch via cmd.exe. This prevents proper job tracking, but that was already // broken for UWP apps anyway due to how they are started by Windows. Even 'start /wait' // doesn't work properly for UWP, so really no termination tracking seems to work at all. // // FIXME: Maybe we can improve this in the future. if (res == HRESULT_FROM_WIN32(ERROR_NO_ASSOCIATION)) { BOOST_LOG(warning) << "Using trampoline to handle target: "sv << raw_cmd; std::wcscpy(shell_command_string.data(), L"cmd.exe /c start \"\" /wait \"%1\" %*"); needs_cmd_escaping = true; // We must suppress the console window that would otherwise appear when starting cmd.exe. creation_flags &= ~CREATE_NEW_CONSOLE; creation_flags |= CREATE_NO_WINDOW; res = S_OK; } // Reset per-user keys back to the original value override_per_user_predefined_keys(nullptr); } if (res != S_OK) { BOOST_LOG(warning) << "Failed to query command string for raw command: "sv << raw_cmd << " ["sv << util::hex(res).to_string_view() << ']'; return from_utf8(raw_cmd); } // Finally, construct the real command string that will be passed into CreateProcess(). // We support common substitutions (%*, %1, %2, %L, %W, %V, etc), but there are other // uncommon ones that are unsupported here. // // https://web.archive.org/web/20111002101214/http://msdn.microsoft.com/en-us/library/windows/desktop/cc144101(v=vs.85).aspx std::wstring cmd_string {shell_command_string.data()}; size_t match_pos = 0; while ((match_pos = cmd_string.find_first_of(L'%', match_pos)) != std::wstring::npos) { std::wstring match_replacement; // If no additional character exists after the match, the dangling '%' is stripped if (match_pos + 1 == cmd_string.size()) { cmd_string.erase(match_pos, 1); break; } // Shell command replacements are strictly '%' followed by a single non-'%' character auto next_char = std::tolower(cmd_string.at(match_pos + 1)); switch (next_char) { // Escape character case L'%': match_replacement = L'%'; break; // Argument replacements case L'0': case L'1': case L'2': case L'3': case L'4': case L'5': case L'6': case L'7': case L'8': case L'9': { // Arguments numbers are 1-based, except for %0 which is equivalent to %1 int index = next_char - L'0'; if (next_char != L'0') { index--; } // Replace with the matching argument, or nothing if the index is invalid if (index < raw_cmd_parts.size()) { match_replacement = raw_cmd_parts.at(index); } break; } // All arguments following the target case L'*': for (int i = 1; i < raw_cmd_parts.size(); i++) { // Insert a space before arguments after the first one if (i > 1) { match_replacement += L' '; } // Argument escaping applies only to %*, not the single substitutions like %2 auto escaped_argument = escape_argument(raw_cmd_parts.at(i)); if (needs_cmd_escaping) { // If we're using the cmd.exe trampoline, we'll need to add additional escaping escaped_argument = escape_argument_for_cmd(escaped_argument); } match_replacement += escaped_argument; } break; // Long file path of target case L'l': case L'd': case L'v': { std::array path; std::array other_dirs {working_dir.c_str(), nullptr}; // PathFindOnPath() is a little gross because it uses the same // buffer for input and output, so we need to copy our input // into the path array. std::wcsncpy(path.data(), raw_target.c_str(), path.size()); if (path[path.size() - 1] != 0) { // The path was so long it was truncated by this copy. We'll // assume it was an absolute path (likely) and use it unmodified. match_replacement = raw_target; } // See if we can find the path on our search path or working directory else if (PathFindOnPathW(path.data(), other_dirs.data())) { match_replacement = std::wstring {path.data()}; } else { // We couldn't find the target, so we'll just hope for the best match_replacement = raw_target; } break; } // Working directory case L'w': match_replacement = working_dir; break; default: BOOST_LOG(warning) << "Unsupported argument replacement: %%" << next_char; break; } // Replace the % and following character with the match replacement cmd_string.replace(match_pos, 2, match_replacement); // Skip beyond the match replacement itself to prevent recursive replacement match_pos += match_replacement.size(); } BOOST_LOG(info) << "Resolved user-provided command '"sv << raw_cmd << "' to '"sv << cmd_string << '\''; return cmd_string; } /** * @brief Run a command on the users profile. * * Launches a child process as the user, using the current user's environment and a specific working directory. * * @param elevated Specify whether to elevate the process. * @param interactive Specify whether this will run in a window or hidden. * @param cmd The command to run. * @param working_dir The working directory for the new process. * @param env The environment variables to use for the new process. * @param file A file object to redirect the child process's output to (may be `nullptr`). * @param ec An error code, set to indicate any errors that occur during the launch process. * @param group A pointer to a `bp::group` object to which the new process should belong (may be `nullptr`). * @return A `bp::child` object representing the new process, or an empty `bp::child` object if the launch fails. */ bp::child 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) { std::wstring start_dir = from_utf8(working_dir.string()); HANDLE job = group ? group->native_handle() : nullptr; STARTUPINFOEXW startup_info = create_startup_info(file, job ? &job : nullptr, ec); PROCESS_INFORMATION process_info; // Clone the environment to create a local copy. Boost.Process (bp) shares the environment with all spawned processes. // Since we're going to modify the 'env' variable by merging user-specific environment variables into it, // we make a clone to prevent side effects to the shared environment. bp::environment cloned_env = env; if (ec) { // In the event that startup_info failed, return a blank child process. return bp::child(); } // Use RAII to ensure the attribute list is freed when we're done with it auto attr_list_free = util::fail_guard([list = startup_info.lpAttributeList]() { free_proc_thread_attr_list(list); }); DWORD creation_flags = EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_BREAKAWAY_FROM_JOB; // Create a new console for interactive processes and use no console for non-interactive processes creation_flags |= interactive ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW; // Find the PATH variable in our environment block using a case-insensitive search auto sunshine_wenv = boost::this_process::wenvironment(); std::wstring path_var_name {L"PATH"}; std::wstring old_path_val; auto itr = std::find_if(sunshine_wenv.cbegin(), sunshine_wenv.cend(), [&](const auto &e) { return boost::iequals(e.get_name(), path_var_name); }); if (itr != sunshine_wenv.cend()) { // Use the existing variable if it exists, since Boost treats these as case-sensitive. path_var_name = itr->get_name(); old_path_val = sunshine_wenv[path_var_name].to_string(); } // Temporarily prepend the specified working directory to PATH to ensure CreateProcess() // will (preferentially) find binaries that reside in the working directory. sunshine_wenv[path_var_name].assign(start_dir + L";" + old_path_val); // Restore the old PATH value for our process when we're done here auto restore_path = util::fail_guard([&]() { if (old_path_val.empty()) { sunshine_wenv[path_var_name].clear(); } else { sunshine_wenv[path_var_name].assign(old_path_val); } }); BOOL ret; if (is_running_as_system()) { // Duplicate the current user's token HANDLE user_token = retrieve_users_token(elevated); if (!user_token) { // Fail the launch rather than risking launching with Sunshine's permissions unmodified. ec = std::make_error_code(std::errc::permission_denied); return bp::child(); } // Use RAII to ensure the shell token is closed when we're done with it auto token_close = util::fail_guard([user_token]() { CloseHandle(user_token); }); // Populate env with user-specific environment variables if (!merge_user_environment_block(cloned_env, user_token)) { ec = std::make_error_code(std::errc::not_enough_memory); return bp::child(); } // Open the process as the current user account, elevation is handled in the token itself. ec = impersonate_current_user(user_token, [&]() { std::wstring env_block = create_environment_block(cloned_env); std::wstring wcmd = resolve_command_string(cmd, start_dir, user_token, creation_flags); ret = CreateProcessAsUserW(user_token, nullptr, (LPWSTR) wcmd.c_str(), nullptr, nullptr, !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), creation_flags, env_block.data(), start_dir.empty() ? nullptr : start_dir.c_str(), (LPSTARTUPINFOW) &startup_info, &process_info); }); } // Otherwise, launch the process using CreateProcessW() // This will inherit the elevation of whatever the user launched Sunshine with. else { // Open our current token to resolve environment variables HANDLE process_token; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE, &process_token)) { ec = std::make_error_code(std::errc::permission_denied); return bp::child(); } auto token_close = util::fail_guard([process_token]() { CloseHandle(process_token); }); // Populate env with user-specific environment variables if (!merge_user_environment_block(cloned_env, process_token)) { ec = std::make_error_code(std::errc::not_enough_memory); return bp::child(); } std::wstring env_block = create_environment_block(cloned_env); std::wstring wcmd = resolve_command_string(cmd, start_dir, nullptr, creation_flags); ret = CreateProcessW(nullptr, (LPWSTR) wcmd.c_str(), nullptr, nullptr, !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), creation_flags, env_block.data(), start_dir.empty() ? nullptr : start_dir.c_str(), (LPSTARTUPINFOW) &startup_info, &process_info); } // Use the results of the launch to create a bp::child object return create_boost_child_from_results(ret, cmd, ec, process_info); } /** * @brief Open a url in the default web browser. * @param url The url to open. */ void open_url(const std::string &url) { boost::process::v1::environment _env = boost::this_process::environment(); auto working_dir = boost::filesystem::path(); std::error_code ec; auto child = run_command(false, false, url, working_dir, _env, nullptr, ec, nullptr); if (ec) { BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); } else { BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; child.detach(); } } void adjust_thread_priority(thread_priority_e priority) { int win32_priority; switch (priority) { case thread_priority_e::low: win32_priority = THREAD_PRIORITY_BELOW_NORMAL; break; case thread_priority_e::normal: win32_priority = THREAD_PRIORITY_NORMAL; break; case thread_priority_e::high: win32_priority = THREAD_PRIORITY_ABOVE_NORMAL; break; case thread_priority_e::critical: win32_priority = THREAD_PRIORITY_HIGHEST; break; default: BOOST_LOG(error) << "Unknown thread priority: "sv << (int) priority; return; } if (!SetThreadPriority(GetCurrentThread(), win32_priority)) { auto winerr = GetLastError(); BOOST_LOG(warning) << "Unable to set thread priority to "sv << win32_priority << ": "sv << winerr; } } void streaming_will_start() { static std::once_flag load_wlanapi_once_flag; std::call_once(load_wlanapi_once_flag, []() { // wlanapi.dll is not installed by default on Windows Server, so we load it dynamically HMODULE wlanapi = LoadLibraryExA("wlanapi.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); if (!wlanapi) { BOOST_LOG(debug) << "wlanapi.dll is not available on this OS"sv; return; } fn_WlanOpenHandle = (decltype(fn_WlanOpenHandle)) GetProcAddress(wlanapi, "WlanOpenHandle"); fn_WlanCloseHandle = (decltype(fn_WlanCloseHandle)) GetProcAddress(wlanapi, "WlanCloseHandle"); fn_WlanFreeMemory = (decltype(fn_WlanFreeMemory)) GetProcAddress(wlanapi, "WlanFreeMemory"); fn_WlanEnumInterfaces = (decltype(fn_WlanEnumInterfaces)) GetProcAddress(wlanapi, "WlanEnumInterfaces"); fn_WlanSetInterface = (decltype(fn_WlanSetInterface)) GetProcAddress(wlanapi, "WlanSetInterface"); if (!fn_WlanOpenHandle || !fn_WlanCloseHandle || !fn_WlanFreeMemory || !fn_WlanEnumInterfaces || !fn_WlanSetInterface) { BOOST_LOG(error) << "wlanapi.dll is missing exports?"sv; fn_WlanOpenHandle = nullptr; fn_WlanCloseHandle = nullptr; fn_WlanFreeMemory = nullptr; fn_WlanEnumInterfaces = nullptr; fn_WlanSetInterface = nullptr; FreeLibrary(wlanapi); return; } }); // Enable MMCSS scheduling for DWM DwmEnableMMCSS(true); // Reduce timer period to 0.5ms if (nt_set_timer_resolution_max()) { used_nt_set_timer_resolution = true; } else { BOOST_LOG(error) << "NtSetTimerResolution() failed, falling back to timeBeginPeriod()"; timeBeginPeriod(1); used_nt_set_timer_resolution = false; } // Promote ourselves to high priority class SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS); // Modify NVIDIA control panel settings again, in case they have been changed externally since sunshine launch if (nvprefs_instance.load()) { if (!nvprefs_instance.owning_undo_file()) { nvprefs_instance.restore_from_and_delete_undo_file_if_exists(); } nvprefs_instance.modify_application_profile(); nvprefs_instance.modify_global_profile(); nvprefs_instance.unload(); } // Enable low latency mode on all connected WLAN NICs if wlanapi.dll is available if (fn_WlanOpenHandle) { DWORD negotiated_version; if (fn_WlanOpenHandle(WLAN_API_MAKE_VERSION(2, 0), nullptr, &negotiated_version, &wlan_handle) == ERROR_SUCCESS) { PWLAN_INTERFACE_INFO_LIST wlan_interface_list; if (fn_WlanEnumInterfaces(wlan_handle, nullptr, &wlan_interface_list) == ERROR_SUCCESS) { for (DWORD i = 0; i < wlan_interface_list->dwNumberOfItems; i++) { if (wlan_interface_list->InterfaceInfo[i].isState == wlan_interface_state_connected) { // Enable media streaming mode for 802.11 wireless interfaces to reduce latency and // unnecessary background scanning operations that cause packet loss and jitter. // // https://docs.microsoft.com/en-us/windows-hardware/drivers/network/oid-wdi-set-connection-quality // https://docs.microsoft.com/en-us/previous-versions/windows/hardware/wireless/native-802-11-media-streaming BOOL value = TRUE; auto error = fn_WlanSetInterface(wlan_handle, &wlan_interface_list->InterfaceInfo[i].InterfaceGuid, wlan_intf_opcode_media_streaming_mode, sizeof(value), &value, nullptr); if (error == ERROR_SUCCESS) { BOOST_LOG(info) << "WLAN interface "sv << i << " is now in low latency mode"sv; } } } fn_WlanFreeMemory(wlan_interface_list); } else { fn_WlanCloseHandle(wlan_handle, nullptr); wlan_handle = nullptr; } } } // If there is no mouse connected, enable Mouse Keys to force the cursor to appear if (!GetSystemMetrics(SM_MOUSEPRESENT)) { BOOST_LOG(info) << "A mouse was not detected. Sunshine will enable Mouse Keys while streaming to force the mouse cursor to appear."; // Get the current state of Mouse Keys so we can restore it when streaming is over previous_mouse_keys_state.cbSize = sizeof(previous_mouse_keys_state); if (SystemParametersInfoW(SPI_GETMOUSEKEYS, 0, &previous_mouse_keys_state, 0)) { MOUSEKEYS new_mouse_keys_state = {}; // Enable Mouse Keys new_mouse_keys_state.cbSize = sizeof(new_mouse_keys_state); new_mouse_keys_state.dwFlags = MKF_MOUSEKEYSON | MKF_AVAILABLE; new_mouse_keys_state.iMaxSpeed = 10; new_mouse_keys_state.iTimeToMaxSpeed = 1000; if (SystemParametersInfoW(SPI_SETMOUSEKEYS, 0, &new_mouse_keys_state, 0)) { // Remember to restore the previous settings when we stop streaming enabled_mouse_keys = true; } else { auto winerr = GetLastError(); BOOST_LOG(warning) << "Unable to enable Mouse Keys: "sv << winerr; } } else { auto winerr = GetLastError(); BOOST_LOG(warning) << "Unable to get current state of Mouse Keys: "sv << winerr; } } } void streaming_will_stop() { // Demote ourselves back to normal priority class SetPriorityClass(GetCurrentProcess(), NORMAL_PRIORITY_CLASS); // End our 0.5ms timer request if (used_nt_set_timer_resolution) { used_nt_set_timer_resolution = false; if (!nt_set_timer_resolution_min()) { BOOST_LOG(error) << "nt_set_timer_resolution_min() failed even though nt_set_timer_resolution_max() succeeded"; } } else { timeEndPeriod(1); } // Disable MMCSS scheduling for DWM DwmEnableMMCSS(false); // Closing our WLAN client handle will undo our optimizations if (wlan_handle != nullptr) { fn_WlanCloseHandle(wlan_handle, nullptr); wlan_handle = nullptr; } // Restore Mouse Keys back to the previous settings if we turned it on if (enabled_mouse_keys) { enabled_mouse_keys = false; if (!SystemParametersInfoW(SPI_SETMOUSEKEYS, 0, &previous_mouse_keys_state, 0)) { auto winerr = GetLastError(); BOOST_LOG(warning) << "Unable to restore original state of Mouse Keys: "sv << winerr; } } } void restart_on_exit() { STARTUPINFOEXW startup_info {}; startup_info.StartupInfo.cb = sizeof(startup_info); WCHAR executable[MAX_PATH]; if (GetModuleFileNameW(nullptr, executable, ARRAYSIZE(executable)) == 0) { auto winerr = GetLastError(); BOOST_LOG(fatal) << "Failed to get Sunshine path: "sv << winerr; return; } PROCESS_INFORMATION process_info; if (!CreateProcessW(executable, GetCommandLineW(), nullptr, nullptr, false, CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr, (LPSTARTUPINFOW) &startup_info, &process_info)) { auto winerr = GetLastError(); BOOST_LOG(fatal) << "Unable to restart Sunshine: "sv << winerr; return; } CloseHandle(process_info.hProcess); CloseHandle(process_info.hThread); } void restart() { // If we're running standalone, we have to respawn ourselves via CreateProcess(). // If we're running from the service, we should just exit and let it respawn us. if (GetConsoleWindow() != nullptr) { // Avoid racing with the new process by waiting until we're exiting to start it. atexit(restart_on_exit); } // We use an async exit call here because we can't block the HTTP thread or we'll hang shutdown. lifetime::exit_sunshine(0, true); } int set_env(const std::string &name, const std::string &value) { return _putenv_s(name.c_str(), value.c_str()); } int unset_env(const std::string &name) { return _putenv_s(name.c_str(), ""); } struct enum_wnd_context_t { std::set process_ids; bool requested_exit; }; static BOOL CALLBACK prgrp_enum_windows(HWND hwnd, LPARAM lParam) { auto enum_ctx = (enum_wnd_context_t *) lParam; // Find the owner PID of this window DWORD wnd_process_id; if (!GetWindowThreadProcessId(hwnd, &wnd_process_id)) { // Continue enumeration return TRUE; } // Check if this window is owned by a process we want to terminate if (enum_ctx->process_ids.find(wnd_process_id) != enum_ctx->process_ids.end()) { // Send an async WM_CLOSE message to this window if (SendNotifyMessageW(hwnd, WM_CLOSE, 0, 0)) { BOOST_LOG(debug) << "Sent WM_CLOSE to PID: "sv << wnd_process_id; enum_ctx->requested_exit = true; } else { auto error = GetLastError(); BOOST_LOG(warning) << "Failed to send WM_CLOSE to PID ["sv << wnd_process_id << "]: " << error; } } // Continue enumeration return TRUE; } bool request_process_group_exit(std::uintptr_t native_handle) { auto job_handle = (HANDLE) native_handle; // Get list of all processes in our job object bool success; DWORD required_length = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST); auto process_id_list = (PJOBOBJECT_BASIC_PROCESS_ID_LIST) calloc(1, required_length); auto fg = util::fail_guard([&process_id_list]() { free(process_id_list); }); while (!(success = QueryInformationJobObject(job_handle, JobObjectBasicProcessIdList, process_id_list, required_length, &required_length)) && GetLastError() == ERROR_MORE_DATA) { free(process_id_list); process_id_list = (PJOBOBJECT_BASIC_PROCESS_ID_LIST) calloc(1, required_length); if (!process_id_list) { return false; } } if (!success) { auto err = GetLastError(); BOOST_LOG(warning) << "Failed to enumerate processes in group: "sv << err; return false; } else if (process_id_list->NumberOfProcessIdsInList == 0) { // If all processes are already dead, treat it as a success return true; } enum_wnd_context_t enum_ctx = {}; enum_ctx.requested_exit = false; for (DWORD i = 0; i < process_id_list->NumberOfProcessIdsInList; i++) { enum_ctx.process_ids.emplace(process_id_list->ProcessIdList[i]); } // Enumerate all windows belonging to processes in the list EnumWindows(prgrp_enum_windows, (LPARAM) &enum_ctx); // Return success if we told at least one window to close return enum_ctx.requested_exit; } bool process_group_running(std::uintptr_t native_handle) { JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accounting_info; if (!QueryInformationJobObject((HANDLE) native_handle, JobObjectBasicAccountingInformation, &accounting_info, sizeof(accounting_info), nullptr)) { auto err = GetLastError(); BOOST_LOG(error) << "Failed to get job accounting info: "sv << err; return false; } return accounting_info.ActiveProcesses != 0; } SOCKADDR_IN to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) { SOCKADDR_IN saddr_v4 = {}; saddr_v4.sin_family = AF_INET; saddr_v4.sin_port = htons(port); auto addr_bytes = address.to_bytes(); memcpy(&saddr_v4.sin_addr, addr_bytes.data(), sizeof(saddr_v4.sin_addr)); return saddr_v4; } SOCKADDR_IN6 to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) { SOCKADDR_IN6 saddr_v6 = {}; saddr_v6.sin6_family = AF_INET6; saddr_v6.sin6_port = htons(port); saddr_v6.sin6_scope_id = address.scope_id(); auto addr_bytes = address.to_bytes(); memcpy(&saddr_v6.sin6_addr, addr_bytes.data(), sizeof(saddr_v6.sin6_addr)); return saddr_v6; } // Use UDP segmentation offload if it is supported by the OS. If the NIC is capable, this will use // hardware acceleration to reduce CPU usage. Support for USO was introduced in Windows 10 20H1. bool send_batch(batched_send_info_t &send_info) { WSAMSG msg; // Convert the target address into a SOCKADDR SOCKADDR_IN taddr_v4; SOCKADDR_IN6 taddr_v6; if (send_info.target_address.is_v6()) { taddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port); msg.name = (PSOCKADDR) &taddr_v6; msg.namelen = sizeof(taddr_v6); } else { taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); msg.name = (PSOCKADDR) &taddr_v4; msg.namelen = sizeof(taddr_v4); } auto const max_bufs_per_msg = send_info.payload_buffers.size() + (send_info.headers ? 1 : 0); WSABUF bufs[(send_info.headers ? send_info.block_count : 1) * max_bufs_per_msg]; DWORD bufcount = 0; if (send_info.headers) { // Interleave buffers for headers and payloads for (auto i = 0; i < send_info.block_count; i++) { bufs[bufcount].buf = (char *) &send_info.headers[(send_info.block_offset + i) * send_info.header_size]; bufs[bufcount].len = send_info.header_size; bufcount++; auto payload_desc = send_info.buffer_for_payload_offset((send_info.block_offset + i) * send_info.payload_size); bufs[bufcount].buf = (char *) payload_desc.buffer; bufs[bufcount].len = send_info.payload_size; bufcount++; } } else { // Translate buffer descriptors into WSABUFs auto payload_offset = send_info.block_offset * send_info.payload_size; auto payload_length = payload_offset + (send_info.block_count * send_info.payload_size); while (payload_offset < payload_length) { auto payload_desc = send_info.buffer_for_payload_offset(payload_offset); bufs[bufcount].buf = (char *) payload_desc.buffer; bufs[bufcount].len = std::min(payload_desc.size, payload_length - payload_offset); payload_offset += bufs[bufcount].len; bufcount++; } } msg.lpBuffers = bufs; msg.dwBufferCount = bufcount; msg.dwFlags = 0; // At most, one DWORD option and one PKTINFO option char cmbuf[WSA_CMSG_SPACE(sizeof(DWORD)) + std::max(WSA_CMSG_SPACE(sizeof(IN6_PKTINFO)), WSA_CMSG_SPACE(sizeof(IN_PKTINFO)))] = {}; ULONG cmbuflen = 0; msg.Control.buf = cmbuf; msg.Control.len = sizeof(cmbuf); auto cm = WSA_CMSG_FIRSTHDR(&msg); if (send_info.source_address.is_v6()) { IN6_PKTINFO pktInfo; SOCKADDR_IN6 saddr_v6 = to_sockaddr(send_info.source_address.to_v6(), 0); pktInfo.ipi6_addr = saddr_v6.sin6_addr; pktInfo.ipi6_ifindex = 0; cmbuflen += WSA_CMSG_SPACE(sizeof(pktInfo)); cm->cmsg_level = IPPROTO_IPV6; cm->cmsg_type = IPV6_PKTINFO; cm->cmsg_len = WSA_CMSG_LEN(sizeof(pktInfo)); memcpy(WSA_CMSG_DATA(cm), &pktInfo, sizeof(pktInfo)); } else { IN_PKTINFO pktInfo; SOCKADDR_IN saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); pktInfo.ipi_addr = saddr_v4.sin_addr; pktInfo.ipi_ifindex = 0; cmbuflen += WSA_CMSG_SPACE(sizeof(pktInfo)); cm->cmsg_level = IPPROTO_IP; cm->cmsg_type = IP_PKTINFO; cm->cmsg_len = WSA_CMSG_LEN(sizeof(pktInfo)); memcpy(WSA_CMSG_DATA(cm), &pktInfo, sizeof(pktInfo)); } if (send_info.block_count > 1) { cmbuflen += WSA_CMSG_SPACE(sizeof(DWORD)); cm = WSA_CMSG_NXTHDR(&msg, cm); cm->cmsg_level = IPPROTO_UDP; cm->cmsg_type = UDP_SEND_MSG_SIZE; cm->cmsg_len = WSA_CMSG_LEN(sizeof(DWORD)); *((DWORD *) WSA_CMSG_DATA(cm)) = send_info.header_size + send_info.payload_size; } msg.Control.len = cmbuflen; // If USO is not supported, this will fail and the caller will fall back to unbatched sends. DWORD bytes_sent; return WSASendMsg((SOCKET) send_info.native_socket, &msg, 0, &bytes_sent, nullptr, nullptr) != SOCKET_ERROR; } bool send(send_info_t &send_info) { WSAMSG msg; // Convert the target address into a SOCKADDR SOCKADDR_IN taddr_v4; SOCKADDR_IN6 taddr_v6; if (send_info.target_address.is_v6()) { taddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port); msg.name = (PSOCKADDR) &taddr_v6; msg.namelen = sizeof(taddr_v6); } else { taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); msg.name = (PSOCKADDR) &taddr_v4; msg.namelen = sizeof(taddr_v4); } WSABUF bufs[2]; DWORD bufcount = 0; if (send_info.header) { bufs[bufcount].buf = (char *) send_info.header; bufs[bufcount].len = send_info.header_size; bufcount++; } bufs[bufcount].buf = (char *) send_info.payload; bufs[bufcount].len = send_info.payload_size; bufcount++; msg.lpBuffers = bufs; msg.dwBufferCount = bufcount; msg.dwFlags = 0; char cmbuf[std::max(WSA_CMSG_SPACE(sizeof(IN6_PKTINFO)), WSA_CMSG_SPACE(sizeof(IN_PKTINFO)))] = {}; ULONG cmbuflen = 0; msg.Control.buf = cmbuf; msg.Control.len = sizeof(cmbuf); auto cm = WSA_CMSG_FIRSTHDR(&msg); if (send_info.source_address.is_v6()) { IN6_PKTINFO pktInfo; SOCKADDR_IN6 saddr_v6 = to_sockaddr(send_info.source_address.to_v6(), 0); pktInfo.ipi6_addr = saddr_v6.sin6_addr; pktInfo.ipi6_ifindex = 0; cmbuflen += WSA_CMSG_SPACE(sizeof(pktInfo)); cm->cmsg_level = IPPROTO_IPV6; cm->cmsg_type = IPV6_PKTINFO; cm->cmsg_len = WSA_CMSG_LEN(sizeof(pktInfo)); memcpy(WSA_CMSG_DATA(cm), &pktInfo, sizeof(pktInfo)); } else { IN_PKTINFO pktInfo; SOCKADDR_IN saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); pktInfo.ipi_addr = saddr_v4.sin_addr; pktInfo.ipi_ifindex = 0; cmbuflen += WSA_CMSG_SPACE(sizeof(pktInfo)); cm->cmsg_level = IPPROTO_IP; cm->cmsg_type = IP_PKTINFO; cm->cmsg_len = WSA_CMSG_LEN(sizeof(pktInfo)); memcpy(WSA_CMSG_DATA(cm), &pktInfo, sizeof(pktInfo)); } msg.Control.len = cmbuflen; DWORD bytes_sent; if (WSASendMsg((SOCKET) send_info.native_socket, &msg, 0, &bytes_sent, nullptr, nullptr) == SOCKET_ERROR) { auto winerr = WSAGetLastError(); BOOST_LOG(warning) << "WSASendMsg() failed: "sv << winerr; return false; } return true; } class qos_t: public deinit_t { public: qos_t(QOS_FLOWID flow_id): flow_id(flow_id) { } virtual ~qos_t() { if (!fn_QOSRemoveSocketFromFlow(qos_handle, (SOCKET) nullptr, flow_id, 0)) { auto winerr = GetLastError(); BOOST_LOG(warning) << "QOSRemoveSocketFromFlow() failed: "sv << winerr; } } private: QOS_FLOWID flow_id; }; /** * @brief Enables QoS on the given socket for traffic to the specified destination. * @param native_socket The native socket handle. * @param address The destination address for traffic sent on this socket. * @param port The destination port for traffic sent on this socket. * @param data_type The type of traffic sent on this socket. * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic. */ std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) { SOCKADDR_IN saddr_v4; SOCKADDR_IN6 saddr_v6; PSOCKADDR dest_addr; bool using_connect_hack = false; // Windows doesn't support any concept of traffic priority without DSCP tagging if (!dscp_tagging) { return nullptr; } static std::once_flag load_qwave_once_flag; std::call_once(load_qwave_once_flag, []() { // qWAVE is not installed by default on Windows Server, so we load it dynamically HMODULE qwave = LoadLibraryExA("qwave.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); if (!qwave) { BOOST_LOG(debug) << "qwave.dll is not available on this OS"sv; return; } fn_QOSCreateHandle = (decltype(fn_QOSCreateHandle)) GetProcAddress(qwave, "QOSCreateHandle"); fn_QOSAddSocketToFlow = (decltype(fn_QOSAddSocketToFlow)) GetProcAddress(qwave, "QOSAddSocketToFlow"); fn_QOSRemoveSocketFromFlow = (decltype(fn_QOSRemoveSocketFromFlow)) GetProcAddress(qwave, "QOSRemoveSocketFromFlow"); if (!fn_QOSCreateHandle || !fn_QOSAddSocketToFlow || !fn_QOSRemoveSocketFromFlow) { BOOST_LOG(error) << "qwave.dll is missing exports?"sv; fn_QOSCreateHandle = nullptr; fn_QOSAddSocketToFlow = nullptr; fn_QOSRemoveSocketFromFlow = nullptr; FreeLibrary(qwave); return; } QOS_VERSION qos_version {1, 0}; if (!fn_QOSCreateHandle(&qos_version, &qos_handle)) { auto winerr = GetLastError(); BOOST_LOG(warning) << "QOSCreateHandle() failed: "sv << winerr; return; } }); // If qWAVE is unavailable, just return if (!fn_QOSAddSocketToFlow || !qos_handle) { return nullptr; } auto disconnect_fg = util::fail_guard([&]() { if (using_connect_hack) { SOCKADDR_IN6 empty = {}; empty.sin6_family = AF_INET6; if (connect((SOCKET) native_socket, (PSOCKADDR) &empty, sizeof(empty)) < 0) { auto wsaerr = WSAGetLastError(); BOOST_LOG(error) << "qWAVE dual-stack workaround failed: "sv << wsaerr; } } }); if (address.is_v6()) { auto address_v6 = address.to_v6(); saddr_v6 = to_sockaddr(address_v6, port); dest_addr = (PSOCKADDR) &saddr_v6; // qWAVE doesn't properly support IPv4-mapped IPv6 addresses, nor does it // correctly support IPv4 addresses on a dual-stack socket (despite MSDN's // claims to the contrary). To get proper QoS tagging when hosting in dual // stack mode, we will temporarily connect() the socket to allow qWAVE to // successfully initialize a flow, then disconnect it again so WSASendMsg() // works later on. if (address_v6.is_v4_mapped()) { if (connect((SOCKET) native_socket, (PSOCKADDR) &saddr_v6, sizeof(saddr_v6)) < 0) { auto wsaerr = WSAGetLastError(); BOOST_LOG(error) << "qWAVE dual-stack workaround failed: "sv << wsaerr; } else { BOOST_LOG(debug) << "Using qWAVE connect() workaround for QoS tagging"sv; using_connect_hack = true; dest_addr = nullptr; } } } else { saddr_v4 = to_sockaddr(address.to_v4(), port); dest_addr = (PSOCKADDR) &saddr_v4; } QOS_TRAFFIC_TYPE traffic_type; switch (data_type) { case qos_data_type_e::audio: traffic_type = QOSTrafficTypeVoice; break; case qos_data_type_e::video: traffic_type = QOSTrafficTypeAudioVideo; break; default: BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type; return nullptr; } QOS_FLOWID flow_id = 0; if (!fn_QOSAddSocketToFlow(qos_handle, (SOCKET) native_socket, dest_addr, traffic_type, QOS_NON_ADAPTIVE_FLOW, &flow_id)) { auto winerr = GetLastError(); BOOST_LOG(warning) << "QOSAddSocketToFlow() failed: "sv << winerr; return nullptr; } return std::make_unique(flow_id); } int64_t qpc_counter() { LARGE_INTEGER performance_counter; if (QueryPerformanceCounter(&performance_counter)) { return performance_counter.QuadPart; } return 0; } std::chrono::nanoseconds qpc_time_difference(int64_t performance_counter1, int64_t performance_counter2) { auto get_frequency = []() { LARGE_INTEGER frequency; frequency.QuadPart = 0; QueryPerformanceFrequency(&frequency); return frequency.QuadPart; }; static const double frequency = get_frequency(); if (frequency) { return std::chrono::nanoseconds((int64_t) ((performance_counter1 - performance_counter2) * frequency / std::nano::den)); } return {}; } std::wstring from_utf8(const std::string_view &string) { // No conversion needed if the string is empty if (string.empty()) { return {}; } // Get the output size required to store the string auto output_size = MultiByteToWideChar(CP_UTF8, 0, string.data(), string.size(), nullptr, 0); if (output_size == 0) { auto winerr = GetLastError(); BOOST_LOG(error) << "Failed to get UTF-16 buffer size: "sv << winerr; return {}; } // Perform the conversion std::wstring output(output_size, L'\0'); output_size = MultiByteToWideChar(CP_UTF8, 0, string.data(), string.size(), output.data(), output.size()); if (output_size == 0) { auto winerr = GetLastError(); BOOST_LOG(error) << "Failed to convert string to UTF-16: "sv << winerr; return {}; } return output; } std::string to_utf8(const std::wstring_view &string) { // No conversion needed if the string is empty if (string.empty()) { return {}; } // Get the output size required to store the string auto output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), nullptr, 0, nullptr, nullptr); if (output_size == 0) { auto winerr = GetLastError(); BOOST_LOG(error) << "Failed to get UTF-8 buffer size: "sv << winerr; return {}; } // Perform the conversion std::string output(output_size, '\0'); output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), output.data(), output.size(), nullptr, nullptr); if (output_size == 0) { auto winerr = GetLastError(); BOOST_LOG(error) << "Failed to convert string to UTF-8: "sv << winerr; return {}; } return output; } std::string get_host_name() { WCHAR hostname[256]; if (GetHostNameW(hostname, ARRAYSIZE(hostname)) == SOCKET_ERROR) { BOOST_LOG(error) << "GetHostNameW() failed: "sv << WSAGetLastError(); return "Sunshine"s; } return to_utf8(hostname); } class win32_high_precision_timer: public high_precision_timer { public: win32_high_precision_timer() { // Use CREATE_WAITABLE_TIMER_HIGH_RESOLUTION if supported (Windows 10 1809+) timer = CreateWaitableTimerEx(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); if (!timer) { timer = CreateWaitableTimerEx(nullptr, nullptr, 0, TIMER_ALL_ACCESS); if (!timer) { BOOST_LOG(error) << "Unable to create high_precision_timer, CreateWaitableTimerEx() failed: " << GetLastError(); } } } ~win32_high_precision_timer() { if (timer) { CloseHandle(timer); } } void sleep_for(const std::chrono::nanoseconds &duration) override { if (!timer) { BOOST_LOG(error) << "Attempting high_precision_timer::sleep_for() with uninitialized timer"; return; } if (duration < 0s) { BOOST_LOG(error) << "Attempting high_precision_timer::sleep_for() with negative duration"; return; } if (duration > 5s) { BOOST_LOG(error) << "Attempting high_precision_timer::sleep_for() with unexpectedly large duration (>5s)"; return; } LARGE_INTEGER due_time; due_time.QuadPart = duration.count() / -100; SetWaitableTimer(timer, &due_time, 0, nullptr, nullptr, false); WaitForSingleObject(timer, INFINITE); } operator bool() override { return timer != nullptr; } private: HANDLE timer = nullptr; }; std::unique_ptr create_high_precision_timer() { return std::make_unique(); } std::string get_clipboard() { std::string currentClipboard = to_utf8(getClipboardData()); return currentClipboard; } bool set_clipboard(const std::string& content) { std::wstring cpContent = from_utf8(ensureCrLf(content)); return !setClipboardData(cpContent); } } // namespace platf static std::string ensureCrLf(const std::string& utf8Str) { std::string result; result.reserve(utf8Str.size() + utf8Str.size() / 2); // Reserve extra space for (size_t i = 0; i < utf8Str.size(); ++i) { if (utf8Str[i] == '\n' && (i == 0 || utf8Str[i - 1] != '\r')) { result += '\r'; // Add \r before \n if not present } result += utf8Str[i]; // Always add the current character } return result; } static std::wstring getClipboardData() { if (!OpenClipboard(nullptr)) { BOOST_LOG(warning) << "Failed to open clipboard."; return L""; } HANDLE hData = GetClipboardData(CF_UNICODETEXT); if (hData == nullptr) { BOOST_LOG(warning) << "No text data in clipboard or failed to get data."; CloseClipboard(); return L""; } wchar_t* pszText = static_cast(GlobalLock(hData)); if (pszText == nullptr) { BOOST_LOG(warning) << "Failed to lock clipboard data."; CloseClipboard(); return L""; } std::wstring ret = pszText; GlobalUnlock(hData); CloseClipboard(); return ret; } static int setClipboardData(const std::wstring& utf16Str) { if (!OpenClipboard(nullptr)) { BOOST_LOG(warning) << "Failed to open clipboard."; return 1; } if (!EmptyClipboard()) { BOOST_LOG(warning) << "Failed to empty clipboard."; CloseClipboard(); return 1; } // Allocate global memory for the clipboard text HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, utf16Str.size() * 2 + 2); if (hGlobal == nullptr) { BOOST_LOG(warning) << "Failed to allocate global memory."; CloseClipboard(); return 1; } // Lock the global memory and copy the text char* pGlobal = static_cast(GlobalLock(hGlobal)); if (pGlobal == nullptr) { BOOST_LOG(warning) << "Failed to lock global memory."; GlobalFree(hGlobal); CloseClipboard(); return 1; } memcpy(pGlobal, utf16Str.c_str(), utf16Str.size() * 2 + 2); GlobalUnlock(hGlobal); // Set the clipboard data if (SetClipboardData(CF_UNICODETEXT, hGlobal) == nullptr) { BOOST_LOG(warning) << "Failed to set clipboard data."; GlobalFree(hGlobal); CloseClipboard(); return 1; } CloseClipboard(); return 0; } #ifdef BOOST_PROCESS_VERSION #undef BOOST_PROCESS_VERSION #endif ================================================ FILE: src/platform/windows/misc.h ================================================ /** * @file src/platform/windows/misc.h * @brief Miscellaneous declarations for Windows. */ #pragma once // standard includes #include #include // platform includes #include #include namespace platf { void print_status(const std::string_view &prefix, HRESULT status); HDESK syncThreadDesktop(); int64_t qpc_counter(); std::chrono::nanoseconds qpc_time_difference(int64_t performance_counter1, int64_t performance_counter2); /** * @brief Convert a UTF-8 string into a UTF-16 wide string. * @param string The UTF-8 string. * @return The converted UTF-16 wide string. */ std::wstring from_utf8(const std::string_view &string); /** * @brief Convert a UTF-16 wide string into a UTF-8 string. * @param string The UTF-16 wide string. * @return The converted UTF-8 string. */ std::string to_utf8(const std::wstring_view &string); } // namespace platf ================================================ FILE: src/platform/windows/nvprefs/driver_settings.cpp ================================================ /** * @file src/platform/windows/nvprefs/driver_settings.cpp * @brief Definitions for nvidia driver settings. */ // this include #include "driver_settings.h" // local includes #include "nvprefs_common.h" namespace { const auto sunshine_application_profile_name = L"SunshineStream"; const auto sunshine_application_path = L"sunshine.exe"; void nvapi_error_message(NvAPI_Status status) { NvAPI_ShortString message = {}; NvAPI_GetErrorMessage(status, message); nvprefs::error_message(std::string("NvAPI error: ") + message); } void fill_nvapi_string(NvAPI_UnicodeString &dest, const wchar_t *src) { static_assert(sizeof(NvU16) == sizeof(wchar_t)); memcpy_s(dest, NVAPI_UNICODE_STRING_MAX * sizeof(NvU16), src, (wcslen(src) + 1) * sizeof(wchar_t)); } } // namespace namespace nvprefs { driver_settings_t::~driver_settings_t() { if (session_handle) { NvAPI_DRS_DestroySession(session_handle); } } bool driver_settings_t::init() { if (session_handle) { return true; } NvAPI_Status status; status = NvAPI_Initialize(); if (status != NVAPI_OK) { info_message("NvAPI_Initialize() failed, ignore if you don't have NVIDIA video card"); return false; } status = NvAPI_DRS_CreateSession(&session_handle); if (status != NVAPI_OK) { nvapi_error_message(status); error_message("NvAPI_DRS_CreateSession() failed"); return false; } return load_settings(); } void driver_settings_t::destroy() { if (session_handle) { NvAPI_DRS_DestroySession(session_handle); session_handle = nullptr; } NvAPI_Unload(); } bool driver_settings_t::load_settings() { if (!session_handle) { return false; } NvAPI_Status status = NvAPI_DRS_LoadSettings(session_handle); if (status != NVAPI_OK) { nvapi_error_message(status); error_message("NvAPI_DRS_LoadSettings() failed"); destroy(); return false; } return true; } bool driver_settings_t::save_settings() { if (!session_handle) { return false; } NvAPI_Status status = NvAPI_DRS_SaveSettings(session_handle); if (status != NVAPI_OK) { nvapi_error_message(status); error_message("NvAPI_DRS_SaveSettings() failed"); return false; } return true; } bool driver_settings_t::restore_global_profile_to_undo(const undo_data_t &undo_data) { if (!session_handle) { return false; } const auto &swapchain_data = undo_data.get_opengl_swapchain(); if (swapchain_data) { NvAPI_Status status; NvDRSProfileHandle profile_handle = nullptr; status = NvAPI_DRS_GetBaseProfile(session_handle, &profile_handle); if (status != NVAPI_OK) { nvapi_error_message(status); error_message("NvAPI_DRS_GetBaseProfile() failed"); return false; } NVDRS_SETTING setting = {}; setting.version = NVDRS_SETTING_VER; status = NvAPI_DRS_GetSetting(session_handle, profile_handle, OGL_CPL_PREFER_DXPRESENT_ID, &setting); if (status == NVAPI_OK && setting.settingLocation == NVDRS_CURRENT_PROFILE_LOCATION && setting.u32CurrentValue == swapchain_data->our_value) { if (swapchain_data->undo_value) { setting = {}; setting.version = NVDRS_SETTING_VER1; setting.settingId = OGL_CPL_PREFER_DXPRESENT_ID; setting.settingType = NVDRS_DWORD_TYPE; setting.settingLocation = NVDRS_CURRENT_PROFILE_LOCATION; setting.u32CurrentValue = *swapchain_data->undo_value; status = NvAPI_DRS_SetSetting(session_handle, profile_handle, &setting); if (status != NVAPI_OK) { nvapi_error_message(status); error_message("NvAPI_DRS_SetSetting() OGL_CPL_PREFER_DXPRESENT failed"); return false; } } else { status = NvAPI_DRS_DeleteProfileSetting(session_handle, profile_handle, OGL_CPL_PREFER_DXPRESENT_ID); if (status != NVAPI_OK && status != NVAPI_SETTING_NOT_FOUND) { nvapi_error_message(status); error_message("NvAPI_DRS_DeleteProfileSetting() OGL_CPL_PREFER_DXPRESENT failed"); return false; } } info_message("Restored OGL_CPL_PREFER_DXPRESENT for base profile"); } else if (status == NVAPI_OK || status == NVAPI_SETTING_NOT_FOUND) { info_message("OGL_CPL_PREFER_DXPRESENT has been changed from our value in base profile, not restoring"); } else { error_message("NvAPI_DRS_GetSetting() OGL_CPL_PREFER_DXPRESENT failed"); return false; } } return true; } bool driver_settings_t::check_and_modify_global_profile(std::optional &undo_data) { if (!session_handle) { return false; } undo_data.reset(); NvAPI_Status status; if (!get_nvprefs_options().opengl_vulkan_on_dxgi) { // User requested to leave OpenGL/Vulkan DXGI swapchain setting alone return true; } NvDRSProfileHandle profile_handle = nullptr; status = NvAPI_DRS_GetBaseProfile(session_handle, &profile_handle); if (status != NVAPI_OK) { nvapi_error_message(status); error_message("NvAPI_DRS_GetBaseProfile() failed"); return false; } NVDRS_SETTING setting = {}; setting.version = NVDRS_SETTING_VER; status = NvAPI_DRS_GetSetting(session_handle, profile_handle, OGL_CPL_PREFER_DXPRESENT_ID, &setting); // Remember current OpenGL/Vulkan DXGI swapchain setting and change it if needed if (status == NVAPI_SETTING_NOT_FOUND || (status == NVAPI_OK && setting.u32CurrentValue != OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED)) { undo_data = undo_data_t(); if (status == NVAPI_OK) { undo_data->set_opengl_swapchain(OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED, setting.u32CurrentValue); } else { undo_data->set_opengl_swapchain(OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED, std::nullopt); } setting = {}; setting.version = NVDRS_SETTING_VER1; setting.settingId = OGL_CPL_PREFER_DXPRESENT_ID; setting.settingType = NVDRS_DWORD_TYPE; setting.settingLocation = NVDRS_CURRENT_PROFILE_LOCATION; setting.u32CurrentValue = OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED; status = NvAPI_DRS_SetSetting(session_handle, profile_handle, &setting); if (status != NVAPI_OK) { nvapi_error_message(status); error_message("NvAPI_DRS_SetSetting() OGL_CPL_PREFER_DXPRESENT failed"); return false; } info_message("Changed OGL_CPL_PREFER_DXPRESENT to OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED for base profile"); } else if (status != NVAPI_OK) { nvapi_error_message(status); error_message("NvAPI_DRS_GetSetting() OGL_CPL_PREFER_DXPRESENT failed"); return false; } return true; } bool driver_settings_t::check_and_modify_application_profile(bool &modified) { if (!session_handle) { return false; } modified = false; NvAPI_Status status; NvAPI_UnicodeString profile_name = {}; fill_nvapi_string(profile_name, sunshine_application_profile_name); NvDRSProfileHandle profile_handle = nullptr; status = NvAPI_DRS_FindProfileByName(session_handle, profile_name, &profile_handle); if (status != NVAPI_OK) { // Create application profile if missing NVDRS_PROFILE profile = {}; profile.version = NVDRS_PROFILE_VER1; fill_nvapi_string(profile.profileName, sunshine_application_profile_name); status = NvAPI_DRS_CreateProfile(session_handle, &profile, &profile_handle); if (status != NVAPI_OK) { nvapi_error_message(status); error_message("NvAPI_DRS_CreateProfile() failed"); return false; } modified = true; } NvAPI_UnicodeString sunshine_path = {}; fill_nvapi_string(sunshine_path, sunshine_application_path); NVDRS_APPLICATION application = {}; application.version = NVDRS_APPLICATION_VER_V1; status = NvAPI_DRS_GetApplicationInfo(session_handle, profile_handle, sunshine_path, &application); if (status != NVAPI_OK) { // Add application to application profile if missing application.version = NVDRS_APPLICATION_VER_V1; application.isPredefined = 0; fill_nvapi_string(application.appName, sunshine_application_path); fill_nvapi_string(application.userFriendlyName, sunshine_application_path); fill_nvapi_string(application.launcher, L""); status = NvAPI_DRS_CreateApplication(session_handle, profile_handle, &application); if (status != NVAPI_OK) { nvapi_error_message(status); error_message("NvAPI_DRS_CreateApplication() failed"); return false; } modified = true; } NVDRS_SETTING setting = {}; setting.version = NVDRS_SETTING_VER1; status = NvAPI_DRS_GetSetting(session_handle, profile_handle, PREFERRED_PSTATE_ID, &setting); if (!get_nvprefs_options().sunshine_high_power_mode) { if (status == NVAPI_OK && setting.settingLocation == NVDRS_CURRENT_PROFILE_LOCATION) { // User requested to not use high power mode for sunshine.exe, // remove the setting from application profile if it's been set previously status = NvAPI_DRS_DeleteProfileSetting(session_handle, profile_handle, PREFERRED_PSTATE_ID); if (status != NVAPI_OK && status != NVAPI_SETTING_NOT_FOUND) { nvapi_error_message(status); error_message("NvAPI_DRS_DeleteProfileSetting() PREFERRED_PSTATE failed"); return false; } modified = true; info_message(std::wstring(L"Removed PREFERRED_PSTATE for ") + sunshine_application_path); } } else if (status != NVAPI_OK || setting.settingLocation != NVDRS_CURRENT_PROFILE_LOCATION || setting.u32CurrentValue != PREFERRED_PSTATE_PREFER_MAX) { // Set power setting if needed setting = {}; setting.version = NVDRS_SETTING_VER1; setting.settingId = PREFERRED_PSTATE_ID; setting.settingType = NVDRS_DWORD_TYPE; setting.settingLocation = NVDRS_CURRENT_PROFILE_LOCATION; setting.u32CurrentValue = PREFERRED_PSTATE_PREFER_MAX; status = NvAPI_DRS_SetSetting(session_handle, profile_handle, &setting); if (status != NVAPI_OK) { nvapi_error_message(status); error_message("NvAPI_DRS_SetSetting() PREFERRED_PSTATE failed"); return false; } modified = true; info_message(std::wstring(L"Changed PREFERRED_PSTATE to PREFERRED_PSTATE_PREFER_MAX for ") + sunshine_application_path); } return true; } } // namespace nvprefs ================================================ FILE: src/platform/windows/nvprefs/driver_settings.h ================================================ /** * @file src/platform/windows/nvprefs/driver_settings.h * @brief Declarations for nvidia driver settings. */ #pragma once // nvapi headers // disable clang-format header reordering // as needs types from // clang-format off #include #include // clang-format on // local includes #include "undo_data.h" namespace nvprefs { class driver_settings_t { public: ~driver_settings_t(); bool init(); void destroy(); bool load_settings(); bool save_settings(); bool restore_global_profile_to_undo(const undo_data_t &undo_data); bool check_and_modify_global_profile(std::optional &undo_data); bool check_and_modify_application_profile(bool &modified); private: NvDRSSessionHandle session_handle = nullptr; }; } // namespace nvprefs ================================================ FILE: src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp ================================================ /** * @file src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp * @brief Definitions for the NVAPI wrapper. */ // standard includes #include // local includes #include "driver_settings.h" #include "nvprefs_common.h" // special nvapi header that should be the last include #include namespace { std::map interfaces; HMODULE dll = nullptr; template NvAPI_Status call_interface(const char *name, Args... args) { auto func = (Func *) interfaces[name]; if (!func) { return interfaces.empty() ? NVAPI_API_NOT_INITIALIZED : NVAPI_NOT_SUPPORTED; } return func(args...); } } // namespace #undef NVAPI_INTERFACE #define NVAPI_INTERFACE NvAPI_Status __cdecl extern void *__cdecl nvapi_QueryInterface(NvU32 id); NVAPI_INTERFACE NvAPI_Initialize() { if (dll) { return NVAPI_OK; } #ifdef _WIN64 auto dll_name = "nvapi64.dll"; #else auto dll_name = "nvapi.dll"; #endif if ((dll = LoadLibraryEx(dll_name, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32))) { if (auto query_interface = (decltype(nvapi_QueryInterface) *) GetProcAddress(dll, "nvapi_QueryInterface")) { for (const auto &item : nvapi_interface_table) { interfaces[item.func] = query_interface(item.id); } return NVAPI_OK; } } NvAPI_Unload(); return NVAPI_LIBRARY_NOT_FOUND; } NVAPI_INTERFACE NvAPI_Unload() { if (dll) { interfaces.clear(); FreeLibrary(dll); dll = nullptr; } return NVAPI_OK; } NVAPI_INTERFACE NvAPI_GetErrorMessage(NvAPI_Status nr, NvAPI_ShortString szDesc) { return call_interface("NvAPI_GetErrorMessage", nr, szDesc); } // This is only a subset of NvAPI_DRS_* functions, more can be added if needed NVAPI_INTERFACE NvAPI_DRS_CreateSession(NvDRSSessionHandle *phSession) { return call_interface("NvAPI_DRS_CreateSession", phSession); } NVAPI_INTERFACE NvAPI_DRS_DestroySession(NvDRSSessionHandle hSession) { return call_interface("NvAPI_DRS_DestroySession", hSession); } NVAPI_INTERFACE NvAPI_DRS_LoadSettings(NvDRSSessionHandle hSession) { return call_interface("NvAPI_DRS_LoadSettings", hSession); } NVAPI_INTERFACE NvAPI_DRS_SaveSettings(NvDRSSessionHandle hSession) { return call_interface("NvAPI_DRS_SaveSettings", hSession); } NVAPI_INTERFACE NvAPI_DRS_CreateProfile(NvDRSSessionHandle hSession, NVDRS_PROFILE *pProfileInfo, NvDRSProfileHandle *phProfile) { return call_interface("NvAPI_DRS_CreateProfile", hSession, pProfileInfo, phProfile); } NVAPI_INTERFACE NvAPI_DRS_FindProfileByName(NvDRSSessionHandle hSession, NvAPI_UnicodeString profileName, NvDRSProfileHandle *phProfile) { return call_interface("NvAPI_DRS_FindProfileByName", hSession, profileName, phProfile); } NVAPI_INTERFACE NvAPI_DRS_CreateApplication(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_APPLICATION *pApplication) { return call_interface("NvAPI_DRS_CreateApplication", hSession, hProfile, pApplication); } NVAPI_INTERFACE NvAPI_DRS_GetApplicationInfo(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvAPI_UnicodeString appName, NVDRS_APPLICATION *pApplication) { return call_interface("NvAPI_DRS_GetApplicationInfo", hSession, hProfile, appName, pApplication); } NVAPI_INTERFACE NvAPI_DRS_SetSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_SETTING *pSetting) { return call_interface("NvAPI_DRS_SetSetting", hSession, hProfile, pSetting); } NVAPI_INTERFACE NvAPI_DRS_GetSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvU32 settingId, NVDRS_SETTING *pSetting) { return call_interface("NvAPI_DRS_GetSetting", hSession, hProfile, settingId, pSetting); } NVAPI_INTERFACE NvAPI_DRS_DeleteProfileSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvU32 settingId) { return call_interface("NvAPI_DRS_DeleteProfileSetting", hSession, hProfile, settingId); } NVAPI_INTERFACE NvAPI_DRS_GetBaseProfile(NvDRSSessionHandle hSession, NvDRSProfileHandle *phProfile) { return call_interface("NvAPI_DRS_GetBaseProfile", hSession, phProfile); } ================================================ FILE: src/platform/windows/nvprefs/nvprefs_common.cpp ================================================ /** * @file src/platform/windows/nvprefs/nvprefs_common.cpp * @brief Definitions for common nvidia preferences. */ // this include #include "nvprefs_common.h" // local includes #include "src/config.h" #include "src/logging.h" namespace nvprefs { void info_message(const std::wstring &message) { BOOST_LOG(info) << "nvprefs: " << message; } void info_message(const std::string &message) { BOOST_LOG(info) << "nvprefs: " << message; } void error_message(const std::wstring &message) { BOOST_LOG(error) << "nvprefs: " << message; } void error_message(const std::string &message) { BOOST_LOG(error) << "nvprefs: " << message; } nvprefs_options get_nvprefs_options() { nvprefs_options options; options.opengl_vulkan_on_dxgi = config::video.nv_opengl_vulkan_on_dxgi; options.sunshine_high_power_mode = config::video.nv_sunshine_high_power_mode; return options; } } // namespace nvprefs ================================================ FILE: src/platform/windows/nvprefs/nvprefs_common.h ================================================ /** * @file src/platform/windows/nvprefs/nvprefs_common.h * @brief Declarations for common nvidia preferences. */ #pragma once // platform includes // disable clang-format header reordering // clang-format off #include #include // clang-format on // local includes #include "src/utility.h" namespace nvprefs { struct safe_handle: public util::safe_ptr_v2 { using util::safe_ptr_v2::safe_ptr_v2; explicit operator bool() const { auto handle = get(); return handle != nullptr && handle != INVALID_HANDLE_VALUE; } }; struct safe_hlocal_deleter { void operator()(void *p) { LocalFree(p); } }; template using safe_hlocal = util::uniq_ptr, safe_hlocal_deleter>; using safe_sid = util::safe_ptr_v2; void info_message(const std::wstring &message); void info_message(const std::string &message); void error_message(const std::wstring &message); void error_message(const std::string &message); struct nvprefs_options { bool opengl_vulkan_on_dxgi = true; bool sunshine_high_power_mode = true; }; nvprefs_options get_nvprefs_options(); } // namespace nvprefs ================================================ FILE: src/platform/windows/nvprefs/nvprefs_interface.cpp ================================================ /** * @file src/platform/windows/nvprefs/nvprefs_interface.cpp * @brief Definitions for nvidia preferences interface. */ // standard includes #include // local includes #include "driver_settings.h" #include "nvprefs_interface.h" #include "undo_file.h" namespace { const auto sunshine_program_data_folder = "Sunshine"; const auto nvprefs_undo_file_name = "nvprefs_undo.json"; } // namespace namespace nvprefs { struct nvprefs_interface::impl { bool loaded = false; driver_settings_t driver_settings; std::filesystem::path undo_folder_path; std::filesystem::path undo_file_path; std::optional undo_data; std::optional undo_file; }; nvprefs_interface::nvprefs_interface(): pimpl(new impl()) { } nvprefs_interface::~nvprefs_interface() { if (owning_undo_file() && load()) { restore_global_profile(); } unload(); } bool nvprefs_interface::load() { if (!pimpl->loaded) { // Check %ProgramData% variable, need it for storing undo file wchar_t program_data_env[MAX_PATH]; auto get_env_result = GetEnvironmentVariableW(L"ProgramData", program_data_env, MAX_PATH); if (get_env_result == 0 || get_env_result >= MAX_PATH || !std::filesystem::is_directory(program_data_env)) { error_message("Missing or malformed %ProgramData% environment variable"); return false; } // Prepare undo file path variables pimpl->undo_folder_path = std::filesystem::path(program_data_env) / sunshine_program_data_folder; pimpl->undo_file_path = pimpl->undo_folder_path / nvprefs_undo_file_name; // Dynamically load nvapi library and load driver settings pimpl->loaded = pimpl->driver_settings.init(); } return pimpl->loaded; } void nvprefs_interface::unload() { if (pimpl->loaded) { // Unload dynamically loaded nvapi library pimpl->driver_settings.destroy(); pimpl->loaded = false; } } bool nvprefs_interface::restore_from_and_delete_undo_file_if_exists() { if (!pimpl->loaded) { return false; } // Check for undo file from previous improper termination bool access_denied = false; if (auto undo_file = undo_file_t::open_existing_file(pimpl->undo_file_path, access_denied)) { // Try to restore from the undo file info_message("Opened undo file from previous improper termination"); if (auto undo_data = undo_file->read_undo_data()) { if (pimpl->driver_settings.restore_global_profile_to_undo(*undo_data) && pimpl->driver_settings.save_settings()) { info_message("Restored global profile settings from undo file - deleting the file"); } else { error_message("Failed to restore global profile settings from undo file, deleting the file anyway"); } } else { error_message("Coulnd't read undo file, deleting the file anyway"); } if (!undo_file->delete_file()) { error_message("Couldn't delete undo file"); return false; } } else if (access_denied) { error_message("Couldn't open undo file from previous improper termination, or confirm that there's no such file"); return false; } return true; } bool nvprefs_interface::modify_application_profile() { if (!pimpl->loaded) { return false; } // Modify and save sunshine.exe application profile settings, if needed bool modified = false; if (!pimpl->driver_settings.check_and_modify_application_profile(modified)) { error_message("Failed to modify application profile settings"); return false; } else if (modified) { if (pimpl->driver_settings.save_settings()) { info_message("Modified application profile settings"); } else { error_message("Couldn't save application profile settings"); return false; } } else { info_message("No need to modify application profile settings"); } return true; } bool nvprefs_interface::modify_global_profile() { if (!pimpl->loaded) { return false; } // Modify but not save global profile settings, if needed std::optional undo_data; if (!pimpl->driver_settings.check_and_modify_global_profile(undo_data)) { error_message("Couldn't modify global profile settings"); return false; } else if (!undo_data) { info_message("No need to modify global profile settings"); return true; } auto make_undo_and_commit = [&]() -> bool { // Create and lock undo file if it hasn't been done yet if (!pimpl->undo_file) { // Prepare Sunshine folder in ProgramData if it doesn't exist if (!CreateDirectoryW(pimpl->undo_folder_path.c_str(), nullptr) && GetLastError() != ERROR_ALREADY_EXISTS) { error_message("Couldn't create undo folder"); return false; } // Create undo file to handle improper termination of nvprefs.exe pimpl->undo_file = undo_file_t::create_new_file(pimpl->undo_file_path); if (!pimpl->undo_file) { error_message("Couldn't create undo file"); return false; } } assert(undo_data); if (pimpl->undo_data) { // Merge undo data if settings has been modified externally since our last modification pimpl->undo_data->merge(*undo_data); } else { pimpl->undo_data = undo_data; } // Write undo data to undo file if (!pimpl->undo_file->write_undo_data(*pimpl->undo_data)) { error_message("Couldn't write to undo file - deleting the file"); if (!pimpl->undo_file->delete_file()) { error_message("Couldn't delete undo file"); } return false; } // Save global profile settings if (!pimpl->driver_settings.save_settings()) { error_message("Couldn't save global profile settings"); return false; } return true; }; if (!make_undo_and_commit()) { // Revert settings modifications pimpl->driver_settings.load_settings(); return false; } return true; } bool nvprefs_interface::owning_undo_file() { return pimpl->undo_file.has_value(); } bool nvprefs_interface::restore_global_profile() { if (!pimpl->loaded || !pimpl->undo_data || !pimpl->undo_file) { return false; } // Restore global profile settings with undo data if (pimpl->driver_settings.restore_global_profile_to_undo(*pimpl->undo_data) && pimpl->driver_settings.save_settings()) { // Global profile settings sucessfully restored, can delete undo file if (!pimpl->undo_file->delete_file()) { error_message("Couldn't delete undo file"); return false; } pimpl->undo_data = std::nullopt; pimpl->undo_file = std::nullopt; } else { error_message("Couldn't restore global profile settings"); return false; } return true; } } // namespace nvprefs ================================================ FILE: src/platform/windows/nvprefs/nvprefs_interface.h ================================================ /** * @file src/platform/windows/nvprefs/nvprefs_interface.h * @brief Declarations for nvidia preferences interface. */ #pragma once // standard includes #include namespace nvprefs { class nvprefs_interface { public: nvprefs_interface(); ~nvprefs_interface(); bool load(); void unload(); bool restore_from_and_delete_undo_file_if_exists(); bool modify_application_profile(); bool modify_global_profile(); bool owning_undo_file(); bool restore_global_profile(); private: struct impl; std::unique_ptr pimpl; }; } // namespace nvprefs ================================================ FILE: src/platform/windows/nvprefs/undo_data.cpp ================================================ /** * @file src/platform/windows/nvprefs/undo_data.cpp * @brief Definitions for undoing changes to nvidia preferences. */ // lib includes #include // local includes #include "nvprefs_common.h" #include "undo_data.h" using json = nlohmann::json; // Separate namespace for ADL, otherwise we need to define json // functions in the same namespace as our types namespace nlohmann { using data_t = nvprefs::undo_data_t::data_t; using opengl_swapchain_t = data_t::opengl_swapchain_t; template struct adl_serializer> { static void to_json(json &j, const std::optional &opt) { if (opt == std::nullopt) { j = nullptr; } else { j = *opt; } } static void from_json(const json &j, std::optional &opt) { if (j.is_null()) { opt = std::nullopt; } else { opt = j.template get(); } } }; template<> struct adl_serializer { static void to_json(json &j, const data_t &data) { j = json {{"opengl_swapchain", data.opengl_swapchain}}; } static void from_json(const json &j, data_t &data) { j.at("opengl_swapchain").get_to(data.opengl_swapchain); } }; template<> struct adl_serializer { static void to_json(json &j, const opengl_swapchain_t &opengl_swapchain) { j = json { {"our_value", opengl_swapchain.our_value}, {"undo_value", opengl_swapchain.undo_value} }; } static void from_json(const json &j, opengl_swapchain_t &opengl_swapchain) { j.at("our_value").get_to(opengl_swapchain.our_value); j.at("undo_value").get_to(opengl_swapchain.undo_value); } }; } // namespace nlohmann namespace nvprefs { void undo_data_t::set_opengl_swapchain(uint32_t our_value, std::optional undo_value) { data.opengl_swapchain = data_t::opengl_swapchain_t { our_value, undo_value }; } std::optional undo_data_t::get_opengl_swapchain() const { return data.opengl_swapchain; } std::string undo_data_t::write() const { try { // Keep this assignment otherwise data will be treated as an array due to // initializer list shenanigangs. const json json_data = data; return json_data.dump(); } catch (const std::exception &err) { error_message(std::string {"failed to serialize json data"}); return {}; } } void undo_data_t::read(const std::vector &buffer) { try { data = json::parse(std::begin(buffer), std::end(buffer)); } catch (const std::exception &err) { error_message(std::string {"failed to parse json data: "} + err.what()); data = {}; } } void undo_data_t::merge(const undo_data_t &newer_data) { const auto &swapchain_data = newer_data.get_opengl_swapchain(); if (swapchain_data) { set_opengl_swapchain(swapchain_data->our_value, swapchain_data->undo_value); } } } // namespace nvprefs ================================================ FILE: src/platform/windows/nvprefs/undo_data.h ================================================ /** * @file src/platform/windows/nvprefs/undo_data.h * @brief Declarations for undoing changes to nvidia preferences. */ #pragma once // standard includes #include #include #include #include namespace nvprefs { class undo_data_t { public: struct data_t { struct opengl_swapchain_t { uint32_t our_value; std::optional undo_value; }; std::optional opengl_swapchain; }; void set_opengl_swapchain(uint32_t our_value, std::optional undo_value); std::optional get_opengl_swapchain() const; std::string write() const; void read(const std::vector &buffer); void merge(const undo_data_t &newer_data); private: data_t data; }; } // namespace nvprefs ================================================ FILE: src/platform/windows/nvprefs/undo_file.cpp ================================================ /** * @file src/platform/windows/nvprefs/undo_file.cpp * @brief Definitions for the nvidia undo file. */ // local includes #include "undo_file.h" namespace { using namespace nvprefs; DWORD relax_permissions(HANDLE file_handle) { PACL old_dacl = nullptr; safe_hlocal sd; DWORD status = GetSecurityInfo(file_handle, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, &old_dacl, nullptr, &sd); if (status != ERROR_SUCCESS) { return status; } safe_sid users_sid; SID_IDENTIFIER_AUTHORITY nt_authorithy = SECURITY_NT_AUTHORITY; if (!AllocateAndInitializeSid(&nt_authorithy, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_USERS, 0, 0, 0, 0, 0, 0, &users_sid)) { return GetLastError(); } EXPLICIT_ACCESS ea = {}; ea.grfAccessPermissions = GENERIC_READ | GENERIC_WRITE | DELETE; ea.grfAccessMode = GRANT_ACCESS; ea.grfInheritance = NO_INHERITANCE; ea.Trustee.TrusteeForm = TRUSTEE_IS_SID; ea.Trustee.ptstrName = (LPTSTR) users_sid.get(); safe_hlocal new_dacl; status = SetEntriesInAcl(1, &ea, old_dacl, &new_dacl); if (status != ERROR_SUCCESS) { return status; } status = SetSecurityInfo(file_handle, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, new_dacl.get(), nullptr); if (status != ERROR_SUCCESS) { return status; } return 0; } } // namespace namespace nvprefs { std::optional undo_file_t::open_existing_file(std::filesystem::path file_path, bool &access_denied) { undo_file_t file; file.file_handle.reset(CreateFileW(file_path.c_str(), GENERIC_READ | DELETE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); if (file.file_handle) { access_denied = false; return file; } else { auto last_error = GetLastError(); access_denied = (last_error != ERROR_FILE_NOT_FOUND && last_error != ERROR_PATH_NOT_FOUND); return std::nullopt; } } std::optional undo_file_t::create_new_file(std::filesystem::path file_path) { undo_file_t file; file.file_handle.reset(CreateFileW(file_path.c_str(), GENERIC_WRITE | STANDARD_RIGHTS_ALL, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr)); if (file.file_handle) { // give GENERIC_READ, GENERIC_WRITE and DELETE permissions to Users group if (relax_permissions(file.file_handle.get()) != 0) { error_message("Failed to relax permissions on undo file"); } return file; } else { return std::nullopt; } } bool undo_file_t::delete_file() { if (!file_handle) { return false; } FILE_DISPOSITION_INFO delete_file_info = {TRUE}; if (SetFileInformationByHandle(file_handle.get(), FileDispositionInfo, &delete_file_info, sizeof(delete_file_info))) { file_handle.reset(); return true; } else { return false; } } bool undo_file_t::write_undo_data(const undo_data_t &undo_data) { if (!file_handle) { return false; } std::string buffer; try { buffer = undo_data.write(); } catch (...) { error_message("Couldn't serialize undo data"); return false; } if (!SetFilePointerEx(file_handle.get(), {}, nullptr, FILE_BEGIN) || !SetEndOfFile(file_handle.get())) { error_message("Couldn't clear undo file"); return false; } DWORD bytes_written = 0; if (!WriteFile(file_handle.get(), buffer.data(), buffer.size(), &bytes_written, nullptr) || bytes_written != buffer.size()) { error_message("Couldn't write undo file"); return false; } if (!FlushFileBuffers(file_handle.get())) { error_message("Failed to flush undo file"); } return true; } std::optional undo_file_t::read_undo_data() { if (!file_handle) { return std::nullopt; } LARGE_INTEGER file_size; if (!GetFileSizeEx(file_handle.get(), &file_size)) { error_message("Couldn't get undo file size"); return std::nullopt; } if ((size_t) file_size.QuadPart > 1024) { error_message("Undo file size is unexpectedly large, aborting"); return std::nullopt; } std::vector buffer(file_size.QuadPart); DWORD bytes_read = 0; if (!ReadFile(file_handle.get(), buffer.data(), buffer.size(), &bytes_read, nullptr) || bytes_read != buffer.size()) { error_message("Couldn't read undo file"); return std::nullopt; } undo_data_t undo_data; try { undo_data.read(buffer); } catch (...) { error_message("Couldn't parse undo file"); return std::nullopt; } return undo_data; } } // namespace nvprefs ================================================ FILE: src/platform/windows/nvprefs/undo_file.h ================================================ /** * @file src/platform/windows/nvprefs/undo_file.h * @brief Declarations for the nvidia undo file. */ #pragma once // standard includes #include // local includes #include "nvprefs_common.h" #include "undo_data.h" namespace nvprefs { class undo_file_t { public: static std::optional open_existing_file(std::filesystem::path file_path, bool &access_denied); static std::optional create_new_file(std::filesystem::path file_path); bool delete_file(); bool write_undo_data(const undo_data_t &undo_data); std::optional read_undo_data(); private: undo_file_t() = default; safe_handle file_handle; }; } // namespace nvprefs ================================================ FILE: src/platform/windows/publish.cpp ================================================ /** * @file src/platform/windows/publish.cpp * @brief Definitions for Windows mDNS service registration. */ // platform includes // WinSock2.h must be included before Windows.h // clang-format off #include #include // clang-format on #include #include // local includes #include "misc.h" #include "src/config.h" #include "src/logging.h" #include "src/network.h" #include "src/nvhttp.h" #include "src/platform/common.h" #include "src/thread_safe.h" #define _FN(x, ret, args) \ typedef ret(*x##_fn) args; \ static x##_fn x using namespace std::literals; #define __SV(quote) L##quote##sv #define SV(quote) __SV(quote) extern "C" { #ifndef __MINGW32__ constexpr auto DNS_REQUEST_PENDING = 9506L; constexpr auto DNS_QUERY_REQUEST_VERSION1 = 0x1; constexpr auto DNS_QUERY_RESULTS_VERSION1 = 0x1; #endif #define SERVICE_DOMAIN "local" constexpr auto SERVICE_TYPE_DOMAIN = SV(SERVICE_TYPE "." SERVICE_DOMAIN); #ifndef __MINGW32__ typedef struct _DNS_SERVICE_INSTANCE { LPWSTR pszInstanceName; LPWSTR pszHostName; IP4_ADDRESS *ip4Address; IP6_ADDRESS *ip6Address; WORD wPort; WORD wPriority; WORD wWeight; // Property list DWORD dwPropertyCount; PWSTR *keys; PWSTR *values; DWORD dwInterfaceIndex; } DNS_SERVICE_INSTANCE, *PDNS_SERVICE_INSTANCE; #endif typedef VOID WINAPI DNS_SERVICE_REGISTER_COMPLETE( _In_ DWORD Status, _In_ PVOID pQueryContext, _In_ PDNS_SERVICE_INSTANCE pInstance ); typedef DNS_SERVICE_REGISTER_COMPLETE *PDNS_SERVICE_REGISTER_COMPLETE; #ifndef __MINGW32__ typedef struct _DNS_SERVICE_CANCEL { PVOID reserved; } DNS_SERVICE_CANCEL, *PDNS_SERVICE_CANCEL; typedef struct _DNS_SERVICE_REGISTER_REQUEST { ULONG Version; ULONG InterfaceIndex; PDNS_SERVICE_INSTANCE pServiceInstance; PDNS_SERVICE_REGISTER_COMPLETE pRegisterCompletionCallback; PVOID pQueryContext; HANDLE hCredentials; BOOL unicastEnabled; } DNS_SERVICE_REGISTER_REQUEST, *PDNS_SERVICE_REGISTER_REQUEST; #endif _FN(_DnsServiceFreeInstance, VOID, (_In_ PDNS_SERVICE_INSTANCE pInstance)); _FN(_DnsServiceDeRegister, DWORD, (_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, _Inout_opt_ PDNS_SERVICE_CANCEL pCancel)); _FN(_DnsServiceRegister, DWORD, (_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, _Inout_opt_ PDNS_SERVICE_CANCEL pCancel)); } /* extern "C" */ namespace platf::publish { VOID WINAPI register_cb(DWORD status, PVOID pQueryContext, PDNS_SERVICE_INSTANCE pInstance) { auto alarm = (safe::alarm_t::element_type *) pQueryContext; if (status) { print_status("register_cb()"sv, status); } alarm->ring(pInstance); } static int service(bool enable, PDNS_SERVICE_INSTANCE &existing_instance) { auto alarm = safe::make_alarm(); std::wstring domain {SERVICE_TYPE_DOMAIN.data(), SERVICE_TYPE_DOMAIN.size()}; auto hostname = platf::get_host_name(); auto name = from_utf8(net::mdns_instance_name(hostname) + '.') + domain; auto host = from_utf8(hostname + ".local"); DNS_SERVICE_INSTANCE instance {}; instance.pszInstanceName = name.data(); instance.wPort = net::map_port(nvhttp::PORT_HTTP); instance.pszHostName = host.data(); // Setting these values ensures Windows mDNS answers comply with RFC 1035. // If these are unset, Windows will send a TXT record that has zero strings, // which is illegal. Setting them to a single empty value causes Windows to // send a single empty string for the TXT record, which is the correct thing // to do when advertising a service without any TXT strings. // // Most clients aren't strictly checking TXT record compliance with RFC 1035, // but Apple's mDNS resolver does and rejects the entire answer if an invalid // TXT record is present. PWCHAR keys[] = {nullptr}; PWCHAR values[] = {nullptr}; instance.dwPropertyCount = 1; instance.keys = keys; instance.values = values; DNS_SERVICE_REGISTER_REQUEST req {}; req.Version = DNS_QUERY_REQUEST_VERSION1; req.pQueryContext = alarm.get(); req.pServiceInstance = enable ? &instance : existing_instance; req.pRegisterCompletionCallback = register_cb; DNS_STATUS status {}; if (enable) { status = _DnsServiceRegister(&req, nullptr); if (status != DNS_REQUEST_PENDING) { print_status("DnsServiceRegister()"sv, status); return -1; } } else { status = _DnsServiceDeRegister(&req, nullptr); if (status != DNS_REQUEST_PENDING) { print_status("DnsServiceDeRegister()"sv, status); return -1; } } alarm->wait(); auto registered_instance = alarm->status(); if (enable) { // Store this instance for later deregistration existing_instance = registered_instance; } else if (registered_instance) { // Deregistration was successful _DnsServiceFreeInstance(registered_instance); existing_instance = nullptr; } return registered_instance ? 0 : -1; } class mdns_registration_t: public ::platf::deinit_t { public: mdns_registration_t(): existing_instance(nullptr) { if (service(true, existing_instance)) { BOOST_LOG(error) << "Unable to register Apollo mDNS service"sv; return; } BOOST_LOG(info) << "Registered Apollo mDNS service"sv; } ~mdns_registration_t() override { if (existing_instance) { if (service(false, existing_instance)) { BOOST_LOG(error) << "Unable to unregister Apollo mDNS service"sv; return; } BOOST_LOG(info) << "Unregistered Apollo mDNS service"sv; } } private: PDNS_SERVICE_INSTANCE existing_instance; }; int load_funcs(HMODULE handle) { auto fg = util::fail_guard([handle]() { FreeLibrary(handle); }); _DnsServiceFreeInstance = (_DnsServiceFreeInstance_fn) GetProcAddress(handle, "DnsServiceFreeInstance"); _DnsServiceDeRegister = (_DnsServiceDeRegister_fn) GetProcAddress(handle, "DnsServiceDeRegister"); _DnsServiceRegister = (_DnsServiceRegister_fn) GetProcAddress(handle, "DnsServiceRegister"); if (!(_DnsServiceFreeInstance && _DnsServiceDeRegister && _DnsServiceRegister)) { BOOST_LOG(error) << "mDNS service not available in dnsapi.dll"sv; return -1; } fg.disable(); return 0; } std::unique_ptr<::platf::deinit_t> start() { HMODULE handle = LoadLibrary("dnsapi.dll"); if (!handle || load_funcs(handle)) { BOOST_LOG(error) << "Couldn't load dnsapi.dll, You'll need to add PC manually from Moonlight"sv; return nullptr; } return std::make_unique(); } } // namespace platf::publish ================================================ FILE: src/platform/windows/utils.cpp ================================================ #include "utils.h" #include #include #include "src/utility.h" #include "src/logging.h" std::wstring acpToUtf16(const std::string& origStr) { auto acp = GetACP(); int utf16Len = MultiByteToWideChar(acp, 0, origStr.c_str(), origStr.size(), NULL, 0); if (utf16Len == 0) { return L""; } std::wstring utf16Str(utf16Len, L'\0'); MultiByteToWideChar(acp, 0, origStr.c_str(), origStr.size(), &utf16Str[0], utf16Len); return utf16Str; } std::string utf16ToAcp(const std::wstring& utf16Str) { auto acp = GetACP(); int codepageLen = WideCharToMultiByte(acp, 0, utf16Str.c_str(), utf16Str.size(), NULL, 0, NULL, NULL); if (codepageLen == 0) { return ""; } std::string codepageStr(codepageLen, '\0'); WideCharToMultiByte(acp, 0, utf16Str.c_str(), utf16Str.size(), &codepageStr[0], codepageLen, NULL, NULL); return codepageStr; } std::string utf8ToAcp(const std::string& utf8Str) { if (GetACP() == CP_UTF8) { return std::string(utf8Str); } int utf16Len = MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), utf8Str.size(), NULL, 0); if (utf16Len == 0) { return std::string(utf8Str); } std::wstring utf16Str(utf16Len, L'\0'); MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), utf8Str.size(), &utf16Str[0], utf16Len); return utf16ToAcp(utf16Str); } std::string acpToUtf8(const std::string& origStr) { if (GetACP() == CP_UTF8) { return std::string(origStr); } auto utf16Str = acpToUtf16(origStr); int utf8Len = WideCharToMultiByte(CP_UTF8, 0, utf16Str.c_str(), utf16Str.size(), NULL, 0, NULL, NULL); if (utf8Len == 0) { return std::string(origStr); } std::string utf8Str(utf8Len, '\0'); WideCharToMultiByte(CP_UTF8, 0, utf16Str.c_str(), utf16Str.size(), &utf8Str[0], utf8Len, NULL, NULL); return utf8Str; } std::string currentCodePageToCharset() { // Map of Windows code pages to their corresponding charset strings static const std::unordered_map codePageCharsetMap = { {65001, "UTF-8"}, {1200, "UTF-16LE"}, {1201, "UTF-16BE"}, {1250, "windows-1250"}, {1251, "windows-1251"}, {1252, "windows-1252"}, {1253, "windows-1253"}, {1254, "windows-1254"}, {1255, "windows-1255"}, {1256, "windows-1256"}, {1257, "windows-1257"}, {1258, "windows-1258"}, {936, "GBK"}, // Simplified Chinese {949, "EUC-KR"}, // Korean {950, "Big5"}, // Traditional Chinese {932, "Shift_JIS"} // Japanese }; static std::string charsetStr; if (charsetStr.empty()) { // Get the active Windows code page int codePage = GetACP(); // Find the charset in the map auto it = codePageCharsetMap.find(codePage); if (it != codePageCharsetMap.end()) { charsetStr = it->second; } else { // Fallback charset if code page is not found in the map charsetStr = "ISO-8859-1"; } } return charsetStr; } // Modified from https://github.com/FrogTheFrog/Sunshine/blob/b6f8573d35eff7c55da6965dfa317dc9722bd4ef/src/platform/windows/display_device/windows_utils.cpp std::string get_error_string(LONG error_code) { std::stringstream error; error << "[code: "; switch (error_code) { case ERROR_INVALID_PARAMETER: error << "ERROR_INVALID_PARAMETER"; break; case ERROR_NOT_SUPPORTED: error << "ERROR_NOT_SUPPORTED"; break; case ERROR_ACCESS_DENIED: error << "ERROR_ACCESS_DENIED"; break; case ERROR_INSUFFICIENT_BUFFER: error << "ERROR_INSUFFICIENT_BUFFER"; break; case ERROR_GEN_FAILURE: error << "ERROR_GEN_FAILURE"; break; case ERROR_SUCCESS: error << "ERROR_SUCCESS"; break; default: error << error_code; break; } error << ", message: " << std::system_category().message(static_cast(error_code)) << "]"; return error.str(); } bool query_display_config(std::vector& paths, std::vector& modes, bool active_only) { LONG result = ERROR_SUCCESS; // When we want to enable/disable displays, we need to get all paths as they will not be active. // This will require some additional filtering of duplicate and otherwise useless paths. UINT32 flags = active_only ? QDC_ONLY_ACTIVE_PATHS : QDC_ALL_PATHS; flags |= QDC_VIRTUAL_MODE_AWARE; // supported from W10 onwards do { UINT32 path_count { 0 }; UINT32 mode_count { 0 }; result = GetDisplayConfigBufferSizes(flags, &path_count, &mode_count); if (result != ERROR_SUCCESS) { BOOST_LOG(error) << get_error_string(result) << " failed to get display paths and modes!"; return false; } paths.resize(path_count); modes.resize(mode_count); result = QueryDisplayConfig(flags, &path_count, paths.data(), &mode_count, modes.data(), nullptr); // The function may have returned fewer paths/modes than estimated paths.resize(path_count); modes.resize(mode_count); // It's possible that between the call to GetDisplayConfigBufferSizes and QueryDisplayConfig // that the display state changed, so loop on the case of ERROR_INSUFFICIENT_BUFFER. } while (result == ERROR_INSUFFICIENT_BUFFER); if (result != ERROR_SUCCESS) { BOOST_LOG(error) << get_error_string(result) << " failed to query display paths and modes!"; return false; } return true; } bool is_user_session_locked() { LPWSTR buffer { nullptr }; const auto cleanup_guard { util::fail_guard([&buffer]() { if (buffer) { WTSFreeMemory(buffer); } }) }; DWORD buffer_size_in_bytes { 0 }; if (WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, WTSGetActiveConsoleSessionId(), WTSSessionInfoEx, &buffer, &buffer_size_in_bytes)) { if (buffer_size_in_bytes > 0) { const auto wts_info { reinterpret_cast(buffer) }; if (wts_info && wts_info->Level == 1) { const bool is_locked { wts_info->Data.WTSInfoExLevel1.SessionFlags == WTS_SESSIONSTATE_LOCK }; BOOST_LOG(debug) << "is_user_session_locked: " << is_locked; return is_locked; } } BOOST_LOG(warning) << "Failed to get session info in is_user_session_locked."; } else { BOOST_LOG(error) << get_error_string(GetLastError()) << " failed while calling WTSQuerySessionInformationW!"; } return false; } bool test_no_access_to_ccd_api() { std::vector paths; std::vector modes; if (!query_display_config(paths, modes, true)) { BOOST_LOG(debug) << "test_no_access_to_ccd_api failed in query_display_config."; return true; } // Here we are supplying the retrieved display data back to SetDisplayConfig (with VALIDATE flag only, so that we make no actual changes). // Unless something is really broken on Windows, this call should never fail under normal circumstances - the configuration is 100% correct, since it was // provided by Windows. const UINT32 flags { SDC_VALIDATE | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_VIRTUAL_MODE_AWARE }; const LONG result { SetDisplayConfig(paths.size(), paths.data(), modes.size(), modes.data(), flags) }; BOOST_LOG(debug) << "test_no_access_to_ccd_api result: " << get_error_string(result); return result == ERROR_ACCESS_DENIED; } bool is_changing_settings_going_to_fail() { return is_user_session_locked() || test_no_access_to_ccd_api(); } ================================================ FILE: src/platform/windows/utils.h ================================================ #pragma once #include #include #include #include std::wstring acpToUtf16(const std::string& origStr); std::string utf16ToAcp(const std::wstring& utf16Str); std::string utf8ToAcp(const std::string& utf8Str); std::string acpToUtf8(const std::string& currentStr); std::string currentCodePageToCharset(); std::string get_error_string(LONG error_code); bool query_display_config(std::vector& paths, std::vector& modes, bool active_only); bool is_user_session_locked(); bool test_no_access_to_ccd_api(); bool is_changing_settings_going_to_fail(); ================================================ FILE: src/platform/windows/virtual_display.cpp ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include "virtual_display.h" using namespace SUDOVDA; namespace VDISPLAY { // {dff7fd29-5b75-41d1-9731-b32a17a17104} // static const GUID DEFAULT_DISPLAY_GUID = { 0xdff7fd29, 0x5b75, 0x41d1, { 0x97, 0x31, 0xb3, 0x2a, 0x17, 0xa1, 0x71, 0x04 } }; HANDLE SUDOVDA_DRIVER_HANDLE = INVALID_HANDLE_VALUE; // START ISOLATED DISPLAY DECLARATIONS struct positionwidthheight; struct coordinates; struct coordinatesdifferences; struct coordinates { int x; int y; }; struct positionwidthheight { struct coordinates position; int width; int height; int modeindex; }; struct coordinatesdifferences { struct coordinates left; struct coordinates right; struct coordinates Difference; struct coordinates AbsDifference; }; std::vector matchDisplay(std::wstring sMatch); std::vector< struct positionwidthheight*>rearrangeVirtualDisplayForLowerRight(std::vector< struct positionwidthheight*> displays); std::string printAllDisplays(std::vector< struct positionwidthheight*> displays); std::vector < struct coordinates > moveToBeConnected(std::vector < struct coordinates > unknown, std::vector< struct coordinates> connected); // END ISOLATED DISPLAY DECLARATIONS LONG getDeviceSettings(const wchar_t* deviceName, DEVMODEW& devMode) { devMode.dmSize = sizeof(DEVMODEW); return EnumDisplaySettingsW(deviceName, ENUM_CURRENT_SETTINGS, &devMode); } LONG changeDisplaySettings2(const wchar_t* deviceName, int width, int height, int refresh_rate, bool bApplyIsolated) { UINT32 pathCount = 0; UINT32 modeCount = 0; if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &pathCount, &modeCount)) { wprintf(L"[SUDOVDA] Failed to query display configuration size.\n"); return ERROR_INVALID_PARAMETER; } std::vector pathArray(pathCount); std::vector modeArray(modeCount); std::vector displayArray; struct positionwidthheight *pCurrentElement; if (QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &pathCount, pathArray.data(), &modeCount, modeArray.data(), nullptr) != ERROR_SUCCESS) { wprintf(L"[SUDOVDA] Failed to query display configuration.\n"); return ERROR_INVALID_PARAMETER; } bool bAtVirtualDisplay; bool bVirtualDisplayAlreadyAdded = false; std::string sDisplayOutput; if (bApplyIsolated == true) { for (UINT32 i = 0; i < pathCount; i++) { DISPLAYCONFIG_SOURCE_DEVICE_NAME sourceName = {}; sourceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; sourceName.header.size = sizeof(sourceName); sourceName.header.adapterId = pathArray[i].sourceInfo.adapterId; sourceName.header.id = pathArray[i].sourceInfo.id; bAtVirtualDisplay = false; if (DisplayConfigGetDeviceInfo(&sourceName.header) != ERROR_SUCCESS) { continue; } auto* sourceInfo = &pathArray[i].sourceInfo; auto* targetInfo = &pathArray[i].targetInfo; if (std::wstring_view(sourceName.viewGdiDeviceName) == std::wstring_view(deviceName)) { bAtVirtualDisplay = true; } if ( true ) { for (UINT32 j = 0; j < modeCount; j++) { if ( modeArray[j].infoType == DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE && modeArray[j].adapterId.HighPart == sourceInfo->adapterId.HighPart && modeArray[j].adapterId.LowPart == sourceInfo->adapterId.LowPart && modeArray[j].id == sourceInfo->id ) { auto* sourceMode = &modeArray[j].sourceMode; wprintf(L"[SUDOVDA] Current mode found: [%dx%dx%d]\n", sourceMode->width, sourceMode->height, targetInfo->refreshRate); pCurrentElement = new (struct positionwidthheight); pCurrentElement->position.x = modeArray[j].sourceMode.position.x; pCurrentElement->position.y = modeArray[j].sourceMode.position.y; pCurrentElement->height = modeArray[j].sourceMode.height; pCurrentElement->width = modeArray[j].sourceMode.width; pCurrentElement->modeindex = j; // This is the virtual display - insert at the front of the vector if (bAtVirtualDisplay == true && bVirtualDisplayAlreadyAdded == false) { displayArray.insert( displayArray.begin()+0, pCurrentElement); bVirtualDisplayAlreadyAdded = true; } else { displayArray.push_back(pCurrentElement); } } } } } sDisplayOutput = ""; sDisplayOutput += "Before: \n"; sDisplayOutput += printAllDisplays(displayArray); displayArray = rearrangeVirtualDisplayForLowerRight(displayArray); sDisplayOutput += ""; sDisplayOutput += "After: \n"; sDisplayOutput += printAllDisplays(displayArray); int iIndex; int xdifference, ydifference = 0; for (iIndex = 0; iIndex < displayArray.size(); iIndex += 1) { // Find the primary display and get the offset to apply to all of the displays to keep the same primary if( modeArray[(displayArray[iIndex]->modeindex)].sourceMode.position.x == 0 && modeArray[(displayArray[iIndex]->modeindex)].sourceMode.position.y == 0 ) { xdifference = (displayArray[iIndex]->position.x) * -1; ydifference = (displayArray[iIndex]->position.y) * -1; break; } } // Set all of the OS Displays to their new locations; Do not change the primary // Update the real vector for the system call for (iIndex = 0; iIndex < displayArray.size(); iIndex += 1) { modeArray[(displayArray[iIndex]->modeindex)].sourceMode.position.x = displayArray[iIndex]->position.x + xdifference; modeArray[(displayArray[iIndex]->modeindex)].sourceMode.position.y = displayArray[iIndex]->position.y + ydifference; modeArray[(displayArray[iIndex]->modeindex)].sourceMode.height = displayArray[iIndex]->height; modeArray[(displayArray[iIndex]->modeindex)].sourceMode.width = displayArray[iIndex]->width; } // Apply the changes only if the virtual display was found if( bVirtualDisplayAlreadyAdded == true ) { LONG status = SetDisplayConfig( pathCount, pathArray.data(), modeCount, modeArray.data(), SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE ); if (status != ERROR_SUCCESS) { wprintf(L"[SUDOVDA] Failed to apply display settings.\n"); } else { wprintf(L"[SUDOVDA] Display settings updated successfully.\n"); } } for (iIndex = 0; iIndex < displayArray.size(); iIndex += 1) { if (displayArray[iIndex] != nullptr) { delete displayArray[iIndex]; } displayArray.clear(); } } // After performing the isolated display movements, do the regular movements for (UINT32 i = 0; i < pathCount; i++) { DISPLAYCONFIG_SOURCE_DEVICE_NAME sourceName = {}; sourceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; sourceName.header.size = sizeof(sourceName); sourceName.header.adapterId = pathArray[i].sourceInfo.adapterId; sourceName.header.id = pathArray[i].sourceInfo.id; if (DisplayConfigGetDeviceInfo(&sourceName.header) != ERROR_SUCCESS) { continue; } auto* sourceInfo = &pathArray[i].sourceInfo; auto* targetInfo = &pathArray[i].targetInfo; if (std::wstring_view(sourceName.viewGdiDeviceName) == std::wstring_view(deviceName)) { wprintf(L"[SUDOVDA] Display found: %ls\n", deviceName); for (UINT32 j = 0; j < modeCount; j++) { if ( modeArray[j].infoType == DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE && modeArray[j].adapterId.HighPart == sourceInfo->adapterId.HighPart && modeArray[j].adapterId.LowPart == sourceInfo->adapterId.LowPart && modeArray[j].id == sourceInfo->id ) { auto* sourceMode = &modeArray[j].sourceMode; wprintf(L"[SUDOVDA] Current mode found: [%dx%dx%d]\n", sourceMode->width, sourceMode->height, targetInfo->refreshRate); sourceMode->width = width; sourceMode->height = height; targetInfo->refreshRate = {(UINT32)refresh_rate, 1000}; // Apply the changes LONG status = SetDisplayConfig( pathCount, pathArray.data(), modeCount, modeArray.data(), SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE ); if (status != ERROR_SUCCESS) { wprintf(L"[SUDOVDA] Failed to apply display settings.\n"); } else { wprintf(L"[SUDOVDA] Display settings updated successfully.\n"); } return status; } } wprintf(L"[SUDOVDA] Mode [%dx%dx%d] not found for display: %ls\n", width, height, refresh_rate, deviceName); return ERROR_INVALID_PARAMETER; } } wprintf(L"[SUDOVDA] Display not found: %ls\n", deviceName); return ERROR_DEVICE_NOT_CONNECTED; } LONG changeDisplaySettings(const wchar_t* deviceName, int width, int height, int refresh_rate) { DEVMODEW devMode = {}; devMode.dmSize = sizeof(devMode); // Old method to set at least baseline refresh rate if (EnumDisplaySettingsW(deviceName, ENUM_CURRENT_SETTINGS, &devMode)) { DWORD targetRefreshRate = refresh_rate / 1000; DWORD altRefreshRate = targetRefreshRate; if (refresh_rate % 1000) { if (refresh_rate % 1000 >= 900) { targetRefreshRate += 1; } else { altRefreshRate += 1; } } else { altRefreshRate -= 1; } wprintf(L"[SUDOVDA] Applying baseline display mode [%dx%dx%d] for %ls.\n", width, height, targetRefreshRate, deviceName); devMode.dmPelsWidth = width; devMode.dmPelsHeight = height; devMode.dmDisplayFrequency = targetRefreshRate; devMode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY; auto res = ChangeDisplaySettingsExW(deviceName, &devMode, NULL, CDS_UPDATEREGISTRY, NULL); if (res != ERROR_SUCCESS) { wprintf(L"[SUDOVDA] Failed to apply baseline display mode, trying alt mode: [%dx%dx%d].\n", width, height, altRefreshRate); devMode.dmDisplayFrequency = altRefreshRate; res = ChangeDisplaySettingsExW(deviceName, &devMode, NULL, CDS_UPDATEREGISTRY, NULL); if (res != ERROR_SUCCESS) { wprintf(L"[SUDOVDA] Failed to apply alt baseline display mode.\n"); } } if (res == ERROR_SUCCESS) { wprintf(L"[SUDOVDA] Baseline display mode applied successfully."); } } // Use new method to set refresh rate if fine tuned return changeDisplaySettings2(deviceName, width, height, refresh_rate); } std::wstring getPrimaryDisplay() { DISPLAY_DEVICEW displayDevice; displayDevice.cb = sizeof(DISPLAY_DEVICE); std::wstring primaryDeviceName; int deviceIndex = 0; while (EnumDisplayDevicesW(NULL, deviceIndex, &displayDevice, 0)) { if (displayDevice.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE) { primaryDeviceName = displayDevice.DeviceName; break; } deviceIndex++; } return primaryDeviceName; } bool setPrimaryDisplay(const wchar_t* primaryDeviceName) { DEVMODEW primaryDevMode{}; if (!getDeviceSettings(primaryDeviceName, primaryDevMode)) { return false; }; int offset_x = primaryDevMode.dmPosition.x; int offset_y = primaryDevMode.dmPosition.y; LONG result; DISPLAY_DEVICEW displayDevice; displayDevice.cb = sizeof(DISPLAY_DEVICEA); int device_index = 0; while (EnumDisplayDevicesW(NULL, device_index, &displayDevice, 0)) { device_index++; if (!(displayDevice.StateFlags & DISPLAY_DEVICE_ACTIVE)) { continue; } DEVMODEW devMode{}; if (getDeviceSettings(displayDevice.DeviceName, devMode)) { devMode.dmPosition.x -= offset_x; devMode.dmPosition.y -= offset_y; devMode.dmFields = DM_POSITION; result = ChangeDisplaySettingsExW(displayDevice.DeviceName, &devMode, NULL, CDS_UPDATEREGISTRY | CDS_NORESET, NULL); if (result != DISP_CHANGE_SUCCESSFUL) { wprintf(L"[SUDOVDA] Changing config for display %ls failed!\n\n", displayDevice.DeviceName); return false; } } } // Update primary device's config to ensure it's primary primaryDevMode.dmPosition.x = 0; primaryDevMode.dmPosition.y = 0; primaryDevMode.dmFields = DM_POSITION; result = ChangeDisplaySettingsExW(primaryDeviceName, &primaryDevMode, NULL, CDS_UPDATEREGISTRY | CDS_NORESET | CDS_SET_PRIMARY, NULL); if (result != DISP_CHANGE_SUCCESSFUL) { wprintf(L"[SUDOVDA] Changing config for primary display %ls failed!\n\n", primaryDeviceName); return false; } wprintf(L"[SUDOVDA] Applying primary display %ls ...\n\n", primaryDeviceName); result = ChangeDisplaySettingsExW(NULL, NULL, NULL, 0, NULL); if (result != DISP_CHANGE_SUCCESSFUL) { wprintf(L"[SUDOVDA] Applying display coinfig failed!\n\n"); return false; } return true; } bool findDisplayIds(const wchar_t* displayName, LUID& adapterId, uint32_t& targetId) { UINT32 pathCount; UINT32 modeCount; if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &pathCount, &modeCount)) { return false; } std::vector paths(pathCount); std::vector modes(modeCount); if (QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &pathCount, paths.data(), &modeCount, modes.data(), nullptr)) { return false; } auto path = std::find_if(paths.begin(), paths.end(), [&displayName](DISPLAYCONFIG_PATH_INFO _path) { DISPLAYCONFIG_PATH_SOURCE_INFO sourceInfo = _path.sourceInfo; DISPLAYCONFIG_SOURCE_DEVICE_NAME sourceName = {}; sourceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; sourceName.header.size = sizeof(sourceName); sourceName.header.adapterId = sourceInfo.adapterId; sourceName.header.id = sourceInfo.id; if (DisplayConfigGetDeviceInfo(&sourceName.header) != ERROR_SUCCESS) { return false; } return std::wstring_view(displayName) == sourceName.viewGdiDeviceName; }); if (path == paths.end()) { return false; } adapterId = path->sourceInfo.adapterId; targetId = path->targetInfo.id; return true; } bool getDisplayHDR(const LUID& adapterLuid, const wchar_t* displayName) { Microsoft::WRL::ComPtr dxgiFactory; HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(&dxgiFactory)); if (FAILED(hr)) { wprintf(L"[SUDOVDA] CreateDXGIFactory1 failed in getDisplayHDR! hr=0x%lx\n", hr); return false; } for (UINT adapterIdx = 0; ; ++adapterIdx) { Microsoft::WRL::ComPtr currentAdapter; hr = dxgiFactory->EnumAdapters1(adapterIdx, currentAdapter.ReleaseAndGetAddressOf()); if (hr == DXGI_ERROR_NOT_FOUND) { break; // No more adapters } if (FAILED(hr)) { wprintf(L"[SUDOVDA] EnumAdapters1 failed for index %u in getDisplayHDR! hr=0x%lx\n", adapterIdx, hr); break; } DXGI_ADAPTER_DESC1 adapterDesc; hr = currentAdapter->GetDesc1(&adapterDesc); if (FAILED(hr)) { wprintf(L"[SUDOVDA] GetDesc1 (Adapter) failed for index %u in getDisplayHDR! hr=0x%lx\n", adapterIdx, hr); continue; } if (adapterDesc.AdapterLuid.LowPart == adapterLuid.LowPart && adapterDesc.AdapterLuid.HighPart == adapterLuid.HighPart) { std::wstring_view displayName_view{displayName}; // Adapter found. Now iterate its outputs and match against targetGdiDeviceName. for (UINT outputIdx = 0; ; ++outputIdx) { Microsoft::WRL::ComPtr dxgiOutput; hr = currentAdapter->EnumOutputs(outputIdx, dxgiOutput.ReleaseAndGetAddressOf()); if (hr == DXGI_ERROR_NOT_FOUND) { wprintf(L"[SUDOVDA] No more DXGI outputs on matched adapter for GDI name %ls.\n", displayName); break; // No more outputs on this adapter } if (FAILED(hr) || !dxgiOutput) { continue; // Error, try next output } DXGI_OUTPUT_DESC dxgiOutputDesc; hr = dxgiOutput->GetDesc(&dxgiOutputDesc); if (FAILED(hr)) { continue; } MONITORINFOEXW monitorInfoEx = {}; monitorInfoEx.cbSize = sizeof(MONITORINFOEXW); if (GetMonitorInfoW(dxgiOutputDesc.Monitor, &monitorInfoEx)) { if (displayName_view == monitorInfoEx.szDevice) { // This is the correct output! wprintf(L"[SUDOVDA] Matched DXGI output GDI name: %ls\n", monitorInfoEx.szDevice); Microsoft::WRL::ComPtr dxgiOutput6; hr = dxgiOutput.As(&dxgiOutput6); if (SUCCEEDED(hr) && dxgiOutput6) { DXGI_OUTPUT_DESC1 outputDesc1; hr = dxgiOutput6->GetDesc1(&outputDesc1); if (SUCCEEDED(hr)) { if (outputDesc1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020) { return true; // HDR Active } } else { wprintf(L"[SUDOVDA] GetDesc1 (Output) failed for %ls. hr=0x%lx\n", monitorInfoEx.szDevice, hr); } } else { wprintf(L"[SUDOVDA] QueryInterface for IDXGIOutput6 failed for %ls. hr=0x%lx. HDR check method not available or output not capable.\n", monitorInfoEx.szDevice, hr); } // Matched the output, checked HDR (it was false or error). This is the only output we care about for this adapter. return false; // Return false as HDR not active or error for this specific display } } else { DWORD lastError = GetLastError(); wprintf(L"[SUDOVDA] GetMonitorInfoW failed for HMONITOR 0x%p from DXGI output %ls. Error: %lu\n", dxgiOutputDesc.Monitor, dxgiOutputDesc.DeviceName, lastError); } } // end output enumeration loop for the matched adapter // If output loop completes, the targetGdiDeviceName was not found among this adapter's DXGI outputs. wprintf(L"[SUDOVDA] Target GDI name %ls not found among DXGI outputs of the matched adapter.\n", displayName); return false; } } // end adapter enumeration loop // If adapter loop completes without finding the adapterLuidFromCaller wprintf(L"[SUDOVDA] Target adapter LUID {%lx-%lx} not found via DXGI.\n", adapterLuid.HighPart, adapterLuid.LowPart); return false; } bool setDisplayHDR(const LUID& adapterId, const uint32_t& targetId, bool enableAdvancedColor) { DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE setHdrInfo = {}; setHdrInfo.header.type = DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE; setHdrInfo.header.size = sizeof(setHdrInfo); setHdrInfo.header.adapterId = adapterId; setHdrInfo.header.id = targetId; setHdrInfo.enableAdvancedColor = enableAdvancedColor; return DisplayConfigSetDeviceInfo(&setHdrInfo.header) == ERROR_SUCCESS; } bool getDisplayHDRByName(const wchar_t* displayName) { LUID adapterId; uint32_t targetId; if (!findDisplayIds(displayName, adapterId, targetId)) { wprintf(L"[SUDOVDA] Failed to find display IDs for %ls!\n", displayName); return false; } return getDisplayHDR(adapterId, displayName); } bool setDisplayHDRByName(const wchar_t* displayName, bool enableAdvancedColor) { LUID adapterId; uint32_t targetId; if (!findDisplayIds(displayName, adapterId, targetId)) { return false; } return setDisplayHDR(adapterId, targetId, enableAdvancedColor); } void closeVDisplayDevice() { if (SUDOVDA_DRIVER_HANDLE == INVALID_HANDLE_VALUE) { return; } CloseHandle(SUDOVDA_DRIVER_HANDLE); SUDOVDA_DRIVER_HANDLE = INVALID_HANDLE_VALUE; } DRIVER_STATUS openVDisplayDevice() { uint32_t retryInterval = 20; while (true) { SUDOVDA_DRIVER_HANDLE = OpenDevice(&SUVDA_INTERFACE_GUID); if (SUDOVDA_DRIVER_HANDLE == INVALID_HANDLE_VALUE) { if (retryInterval > 320) { printf("[SUDOVDA] Open device failed!\n"); return DRIVER_STATUS::FAILED; } retryInterval *= 2; Sleep(retryInterval); continue; } break; } if (!CheckProtocolCompatible(SUDOVDA_DRIVER_HANDLE)) { printf("[SUDOVDA] SUDOVDA protocol not compatible with driver!\n"); closeVDisplayDevice(); return DRIVER_STATUS::VERSION_INCOMPATIBLE; } return DRIVER_STATUS::OK; } bool startPingThread(std::function failCb) { if (SUDOVDA_DRIVER_HANDLE == INVALID_HANDLE_VALUE) { return false; } VIRTUAL_DISPLAY_GET_WATCHDOG_OUT watchdogOut; if (GetWatchdogTimeout(SUDOVDA_DRIVER_HANDLE, watchdogOut)) { printf("[SUDOVDA] Watchdog: Timeout %d, Countdown %d\n", watchdogOut.Timeout, watchdogOut.Countdown); } else { printf("[SUDOVDA] Watchdog fetch failed!\n"); return false; } if (watchdogOut.Timeout) { auto sleepInterval = watchdogOut.Timeout * 1000 / 3; std::thread ping_thread([sleepInterval, failCb = std::move(failCb)]{ uint8_t fail_count = 0; for (;;) { if (!sleepInterval) return; if (!PingDriver(SUDOVDA_DRIVER_HANDLE)) { fail_count += 1; if (fail_count > 3) { failCb(); return; } }; Sleep(sleepInterval); } }); ping_thread.detach(); } return true; } bool setRenderAdapterByName(const std::wstring& adapterName) { if (SUDOVDA_DRIVER_HANDLE == INVALID_HANDLE_VALUE) { return false; } Microsoft::WRL::ComPtr factory; if (!SUCCEEDED(CreateDXGIFactory1(IID_PPV_ARGS(&factory)))) { return false; } Microsoft::WRL::ComPtr adapter; DXGI_ADAPTER_DESC desc; int i = 0; while (SUCCEEDED(factory->EnumAdapters(i, &adapter))) { i += 1; if (!SUCCEEDED(adapter->GetDesc(&desc))) { continue; } if (std::wstring_view(desc.Description) != adapterName) { continue; } if (SetRenderAdapter(SUDOVDA_DRIVER_HANDLE, desc.AdapterLuid)) { return true; } } return false; } std::wstring createVirtualDisplay( const char* s_client_uid, const char* s_client_name, uint32_t width, uint32_t height, uint32_t fps, const GUID& guid ) { if (SUDOVDA_DRIVER_HANDLE == INVALID_HANDLE_VALUE) { return std::wstring(); } VIRTUAL_DISPLAY_ADD_OUT output; if (!AddVirtualDisplay(SUDOVDA_DRIVER_HANDLE, width, height, fps, guid, s_client_name, s_client_uid, output)) { printf("[SUDOVDA] Failed to add virtual display.\n"); return std::wstring(); } uint32_t retryInterval = 20; wchar_t deviceName[CCHDEVICENAME]{}; while (!GetAddedDisplayName(output, deviceName)) { Sleep(retryInterval); if (retryInterval > 320) { printf("[SUDOVDA] Cannot get name for newly added virtual display!\n"); return std::wstring(); } retryInterval *= 2; } wprintf(L"[SUDOVDA] Virtual display added successfully: %ls\n", deviceName); printf("[SUDOVDA] Configuration: W: %d, H: %d, FPS: %d\n", width, height, fps); return std::wstring(deviceName); } bool removeVirtualDisplay(const GUID& guid) { if (SUDOVDA_DRIVER_HANDLE == INVALID_HANDLE_VALUE) { return false; } if (RemoveVirtualDisplay(SUDOVDA_DRIVER_HANDLE, guid)) { printf("[SUDOVDA] Virtual display removed successfully.\n"); return true; } else { return false; } } // START ISOLATED DISPLAY METHODS // Shows the coordinates/height/width for the displays in the vector structure std::string printAllDisplays(std::vector< struct positionwidthheight*> displays) { int iIndex; std::string sOutput; for (iIndex = 0; iIndex < displays.size(); iIndex++) { sOutput += "Index: "; sOutput += std::to_string(iIndex); sOutput += ", X : "; sOutput += std::to_string(displays[iIndex]->position.x); sOutput += ", Y : "; sOutput += std::to_string(displays[iIndex]->position.y); sOutput += ", width : "; sOutput += std::to_string(displays[iIndex]->width); sOutput += ", height : "; sOutput += std::to_string(displays[iIndex]->height); sOutput += "\n"; } return sOutput; } // Helper method for the rearrangeVirtualDisplayForLowerRight() method to move the unknown unconnected display to be connected to the // second display which is assumed to be already connected // // It will return the move that the unknown display would need to perform std::vector < struct coordinates > moveToBeConnected(std::vector < struct coordinates > unknown, std::vector< struct coordinates> connected) { // Figure out if the boxes are connected // Assume that there are 4 points int iIndex, iIndex2; std::vector< struct coordinatesdifferences > differences; std::vector< struct coordinatesdifferences > vertical; std::vector< struct coordinatesdifferences > horizontal; std::vector < struct coordinates >moveResult; std::vector < struct coordinates > unknown2; struct coordinatesdifferences sTemp1; struct coordinates sNoMove; sNoMove.x = 0; sNoMove.y = 0; struct coordinates sDoMove; sDoMove.x = 0; sDoMove.y = 0; bool bCornerConnect = false; bool bVerticalConnect = false; bool bHorizontalConnect = false; int iCountLess; int iCountGreater; // Subtract all of the points for (iIndex = 0; iIndex < connected.size(); iIndex += 1) { for (iIndex2 = 0; iIndex2 < unknown.size(); iIndex2 += 1) { sTemp1.left.x = connected[iIndex].x; sTemp1.left.y = connected[iIndex].y; sTemp1.right.x = unknown[iIndex2].x; sTemp1.right.y = unknown[iIndex2].y; sTemp1.Difference.x = sTemp1.left.x - sTemp1.right.x; sTemp1.Difference.y = sTemp1.left.y - sTemp1.right.y; sTemp1.AbsDifference.x = abs(sTemp1.Difference.x); sTemp1.AbsDifference.y = abs(sTemp1.Difference.y); differences.push_back(sTemp1); } } for (iIndex = 0; iIndex < differences.size(); iIndex += 1) { // See if they are any corner connects sTemp1 = differences[iIndex]; if (sTemp1.AbsDifference.x <= 1 && sTemp1.AbsDifference.y <= 1) { bCornerConnect = true; break; } // See if there are any vertical connects if (sTemp1.AbsDifference.x <= 1) { vertical.push_back(sTemp1); } // See if there are any horizontal connects if (sTemp1.AbsDifference.y <= 1) { horizontal.push_back(sTemp1); } } // Check the vertical connects iCountLess = 0; iCountGreater = 0; for (iIndex = 0; iIndex < vertical.size(); iIndex += 1) { if (vertical[iIndex].left.y <= vertical[iIndex].right.y) { iCountLess += 1; } if (vertical[iIndex].left.y >= vertical[iIndex].right.y) { iCountGreater += 1; } } // Check the sum off all of the counts if (((iCountLess > 0) && (iCountGreater == 0)) || ((iCountGreater > 0) && (iCountLess == 0)) || (iCountLess == 0 && iCountGreater == 0)) { // Boxes are on the same vertical but above or below each other bVerticalConnect = false; } else { bVerticalConnect = true; } // Check the horizontal connects iCountLess = 0; iCountGreater = 0; for (iIndex = 0; iIndex < horizontal.size(); iIndex += 1) { if (horizontal[iIndex].left.x <= horizontal[iIndex].right.x) { iCountLess += 1; } if (horizontal[iIndex].left.x >= horizontal[iIndex].right.x) { iCountGreater += 1; } } // Check the sum off all of the counts if (((iCountLess > 0) && (iCountGreater == 0)) || ((iCountGreater > 0) && (iCountLess == 0)) || (iCountLess == 0 && iCountGreater == 0)) { // Boxes are on the same horizontal but to the left or right of each other bHorizontalConnect = false; } else { bHorizontalConnect = true; } // End the logic if there is no move required if (bHorizontalConnect == true || bVerticalConnect == true || bCornerConnect == true) { moveResult.push_back(sNoMove); return moveResult; } // Otherwise, show the move required int iShortestX = INT_MAX; int iShortestXIndex = -1; // Try the horizontal (x) move first for (iIndex = 0; iIndex < differences.size(); iIndex += 1) { if (differences[iIndex].AbsDifference.x < iShortestX) { iShortestXIndex = iIndex; iShortestX = differences[iIndex].AbsDifference.x; } } if (iShortestX <= 1) { // X move is not required } else { // This is the X to move sDoMove.x = differences[iShortestXIndex].Difference.x; // Perform the x move on the left so that we can check the y unknown2 = unknown; for (iIndex = 0; iIndex < unknown2.size(); iIndex += 1) { unknown2[iIndex].x += sDoMove.x; } // Call oneself recursively only once so that we can see if there is Y to do. std::vector < struct coordinates >moveResult2; moveResult2 = moveToBeConnected(unknown2, connected); // Format the answer for a return sDoMove.y = moveResult2[0].y; moveResult.push_back(sDoMove); return moveResult; } // Figure out the y move required // Otherwise, show the move required int iShortestY = INT_MAX; int iShortestYIndex = -1; // Try the horizontal (x) move first for (iIndex = 0; iIndex < differences.size(); iIndex += 1) { if (differences[iIndex].AbsDifference.y < iShortestY) { iShortestYIndex = iIndex; iShortestY = differences[iIndex].AbsDifference.y; } } if (iShortestY <= 1) { // Y move is not required } else { // This is the Y to move sDoMove.y = differences[iShortestYIndex].Difference.y; moveResult.push_back(sDoMove); return moveResult; } moveResult.push_back(sNoMove); return moveResult; } // Main method to rearrange the displays to have one isolated display in the lower right and // move the other displays as necessary especially if there are holes std::vector< struct positionwidthheight*>rearrangeVirtualDisplayForLowerRight(std::vector< struct positionwidthheight*> displays) { // Make a temporary connected List based on the current Displays // Here connected means that the displays are "touching" by either the // vertical axis or a horizontal axis or a corner. int count = displays.size(); std::vector< int > vConnected(count, 0); // Need the index of the virtual display to put into the lower right corner as primary int changeIndex = 0; // Find the Maxx and Maxy for the current displays int imaxx = INT_MIN; int imaxy = INT_MIN; int imaxindex = -1; int itempx; int itempy; int itempvalid = 0; // Figure out the maxx and maxy, and the index for that rectangle for (int index = 0; index < count; index++) { itempx = displays[index]->position.x + displays[index]->width; itempy = displays[index]->position.y + displays[index]->height; itempvalid = 1; if (changeIndex == index) { itempvalid = 0; } if (itempvalid > 0) { if (imaxx < itempx) { imaxx = itempx; imaxy = itempy; imaxindex = index; } else if (imaxx == itempx) { if (imaxy < itempy) { imaxy = itempy; imaxindex = index; } } } } // Adjust all of the other windows based on the offset for the display that will be 0,0 in the lower right corner. if (imaxindex > -1) { // Adjusting other displays based on the offset for the display that will be 0,0 in the lower right corner for (int index = 0; index < count; index++) { itempvalid = 1; if (changeIndex == index) { itempvalid = 0; } if (itempvalid > 0) { displays[index]->position.x -= imaxx; displays[index]->position.y -= imaxy; } } } // Get the location, width and height of the window that is moving // Make sure the correct display is set to 0,0. for (int index = 0; index < count; index++) { if (index == changeIndex) { displays[index]->position.x = 0; displays[index]->position.y = 0; vConnected[index] = 1; } } bool bAddedConnected; int connectedboxx, connectedboxy, connectedboxwidth, connectedboxheight; int secondboxx, secondboxy, secondboxwidth, secondboxheight, secondboxindex; bool bFirstTime = true; int xmin; int ymin; int minindexconnected; int minindexnonconnected; std::vector< struct coordinates> connectedboxpoints; std::vector< struct coordinates> secondboxpoints; struct coordinates sTempCoordinates; // MAIN LOOP to rearrange displays to be connected to each other. // This is either corner to corner or vertical side or horizontal side do { xmin = INT_MAX; ymin = INT_MAX; minindexconnected = -1; minindexnonconnected = -1; do { bAddedConnected = false; for (int index = 0; index < count; index++) { if (vConnected[index] == 1) { // Skip the virtual window if this is not the first time because we do not want an displays connected to it if (bFirstTime == false && index == changeIndex) { continue; } connectedboxx = displays[index]->position.x; connectedboxy = displays[index]->position.y; connectedboxwidth = displays[index]->width; connectedboxheight = displays[index]->height; connectedboxpoints.clear(); sTempCoordinates.x = connectedboxx; sTempCoordinates.y = connectedboxy; connectedboxpoints.push_back(sTempCoordinates); sTempCoordinates.x = connectedboxx + connectedboxwidth; sTempCoordinates.y = connectedboxy; connectedboxpoints.push_back(sTempCoordinates); sTempCoordinates.x = connectedboxx; sTempCoordinates.y = connectedboxy + connectedboxheight; connectedboxpoints.push_back(sTempCoordinates); sTempCoordinates.x = connectedboxx + connectedboxwidth; sTempCoordinates.y = connectedboxy + connectedboxheight; connectedboxpoints.push_back(sTempCoordinates); // Go through all other boxes and see if there is a connected box to this one for (int index2 = 0; index2 < count; index2++) { if (index2 == index || vConnected[index2] == 1 || index2 == changeIndex) { // Skip oneself and the skip boxes already connected and skip over changeIndex continue; } secondboxx = displays[index2]->position.x; secondboxy = displays[index2]->position.y; secondboxwidth = displays[index2]->width; secondboxheight = displays[index2]->height; secondboxindex = index2; secondboxpoints.clear(); sTempCoordinates.x = secondboxx; sTempCoordinates.y = secondboxy; secondboxpoints.push_back(sTempCoordinates); sTempCoordinates.x = secondboxx + secondboxwidth; sTempCoordinates.y = secondboxy; secondboxpoints.push_back(sTempCoordinates); sTempCoordinates.x = secondboxx; sTempCoordinates.y = secondboxy + secondboxheight; secondboxpoints.push_back(sTempCoordinates); sTempCoordinates.x = secondboxx + secondboxwidth; sTempCoordinates.y = secondboxy + secondboxheight; secondboxpoints.push_back(sTempCoordinates); // What would it take to MOVE the display to be connected to another connected display // The result of this may not be used as there may be a closer display when we go through the list std::vector < struct coordinates > sToMove = moveToBeConnected(secondboxpoints, connectedboxpoints); // No movement necessary if (sToMove[0].x == 0 && sToMove[0].y == 0) { vConnected[secondboxindex] = 1; // NEWLY ADDED bFirstTime = false; bAddedConnected = true; xmin = INT_MAX; ymin = INT_MAX; // Need to restart the whole loop sequence to not connect more than one at the same time. break; } else { if (index != changeIndex) { // Want to see if this display would be the closest one to move via the x coordinates if (abs(sToMove[0].x) < xmin) { xmin = abs(sToMove[0].x); ymin = abs(sToMove[0].y); minindexconnected = index; minindexnonconnected = index2; } } } } } // Need to restart the whole loop sequence to not connect more than one at the same time. if (bAddedConnected == true) { break; } } } while (bAddedConnected == true); // We are finish adding the connected box during the initial pass throguh // We should also have the minimal display to move bFirstTime = false; if (xmin != INT_MAX || ymin != INT_MAX) { connectedboxx = displays[minindexconnected]->position.x; connectedboxy = displays[minindexconnected]->position.y; connectedboxwidth = displays[minindexconnected]->width; connectedboxheight = displays[minindexconnected]->height; connectedboxpoints.clear(); sTempCoordinates.x = connectedboxx; sTempCoordinates.y = connectedboxy; connectedboxpoints.push_back(sTempCoordinates); sTempCoordinates.x = connectedboxx + connectedboxwidth; sTempCoordinates.y = connectedboxy; connectedboxpoints.push_back(sTempCoordinates); sTempCoordinates.x = connectedboxx; sTempCoordinates.y = connectedboxy + connectedboxheight; connectedboxpoints.push_back(sTempCoordinates); sTempCoordinates.x = connectedboxx + connectedboxwidth; sTempCoordinates.y = connectedboxy + connectedboxheight; connectedboxpoints.push_back(sTempCoordinates); secondboxx = displays[minindexnonconnected]->position.x; secondboxy = displays[minindexnonconnected]->position.y; secondboxwidth = displays[minindexnonconnected]->width; secondboxheight = displays[minindexnonconnected]->height; secondboxindex = minindexnonconnected; secondboxpoints.clear(); sTempCoordinates.x = secondboxx; sTempCoordinates.y = secondboxy; secondboxpoints.push_back(sTempCoordinates); sTempCoordinates.x = secondboxx + secondboxwidth; sTempCoordinates.y = secondboxy; secondboxpoints.push_back(sTempCoordinates); sTempCoordinates.x = secondboxx; sTempCoordinates.y = secondboxy + secondboxheight; secondboxpoints.push_back(sTempCoordinates); sTempCoordinates.x = secondboxx + secondboxwidth; sTempCoordinates.y = secondboxy + secondboxheight; secondboxpoints.push_back(sTempCoordinates); // Perform the actual move std::vector < struct coordinates > sToMove = moveToBeConnected(secondboxpoints, connectedboxpoints); // Apply the move to the array of displays displays[minindexnonconnected]->position.x += sToMove[0].x; displays[minindexnonconnected]->position.y += sToMove[0].y; } } while (xmin != INT_MAX); return displays; } // Utility function to match the DeviceString to the Display Names // Typical DeviceStrings are the driver names // // Example: matchDisplay(L"SudoMaker Virtual Display Adapter") // Result: L"\\\\.\\Display2" std::vector matchDisplay(std::wstring sMatch) { DISPLAY_DEVICEW displayDevice; displayDevice.cb = sizeof(DISPLAY_DEVICE); std::wstring matchDeviceName; std::vector vMatches; int deviceIndex = 0; while (EnumDisplayDevicesW(NULL, deviceIndex, &displayDevice, 0)) { if (std::wstring(displayDevice.DeviceString) == sMatch && displayDevice.StateFlags > 0) { matchDeviceName = displayDevice.DeviceName; vMatches.push_back(matchDeviceName); } deviceIndex++; } return vMatches; } // END ISOLATED DISPLAY METHODS } ================================================ FILE: src/platform/windows/virtual_display.h ================================================ #pragma once #include #include #ifndef FILE_DEVICE_UNKNOWN #define FILE_DEVICE_UNKNOWN 0x00000022 #endif #include #include #include namespace VDISPLAY { enum class DRIVER_STATUS { UNKNOWN = 1, OK = 0, FAILED = -1, VERSION_INCOMPATIBLE = -2, WATCHDOG_FAILED = -3 }; extern HANDLE SUDOVDA_DRIVER_HANDLE; LONG getDeviceSettings(const wchar_t* deviceName, DEVMODEW& devMode); LONG changeDisplaySettings(const wchar_t* deviceName, int width, int height, int refresh_rate); LONG changeDisplaySettings2(const wchar_t* deviceName, int width, int height, int refresh_rate, bool bApplyIsolated=false); std::wstring getPrimaryDisplay(); bool setPrimaryDisplay(const wchar_t* primaryDeviceName); bool getDisplayHDRByName(const wchar_t* displayName); bool setDisplayHDRByName(const wchar_t* displayName, bool enableAdvancedColor); void closeVDisplayDevice(); DRIVER_STATUS openVDisplayDevice(); bool startPingThread(std::function failCb); bool setRenderAdapterByName(const std::wstring& adapterName); std::wstring createVirtualDisplay( const char* s_client_uid, const char* s_client_name, uint32_t width, uint32_t height, uint32_t fps, const GUID& guid ); bool removeVirtualDisplay(const GUID& guid); std::vector matchDisplay(std::wstring sMatch); } ================================================ FILE: src/platform/windows/windows.rc ================================================ /** * @file src/platform/windows/windows.rc * @brief Windows resource file. */ #include "winver.h" #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) VS_VERSION_INFO VERSIONINFO FILEVERSION PROJECT_VERSION_MAJOR,PROJECT_VERSION_MINOR,PROJECT_VERSION_PATCH,0 PRODUCTVERSION PROJECT_VERSION_MAJOR,PROJECT_VERSION_MINOR,PROJECT_VERSION_PATCH,0 FILEOS VOS__WINDOWS32 FILETYPE VFT_APP FILESUBTYPE VFT2_UNKNOWN BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904E4" BEGIN VALUE "CompanyName", TOSTRING(PROJECT_VENDOR) VALUE "FileDescription", TOSTRING(PROJECT_NAME) VALUE "FileVersion", TOSTRING(PROJECT_VERSION) VALUE "InternalName", TOSTRING(PROJECT_NAME) VALUE "ProductName", TOSTRING(PROJECT_NAME) VALUE "ProductVersion", TOSTRING(PROJECT_VERSION) VALUE "LegalCopyright", "https://raw.githubusercontent.com/ClassicOldSong/Apollo/master/LICENSE" END END BLOCK "VarFileInfo" BEGIN /* The following line should only be modified for localized versions. */ /* It consists of any number of WORD,WORD pairs, with each pair */ /* describing a language,codepage combination supported by the file. */ /* */ /* For example, a file might have values "0x409,1252" indicating that it */ /* supports English language (0x409) in the Windows ANSI codepage (1252). */ VALUE "Translation", 0x409, 1252 END END SuperDuperAmazing ICON DISCARDABLE PROJECT_ICON_PATH ================================================ FILE: src/process.cpp ================================================ /** * @file src/process.cpp * @brief Definitions for the startup and shutdown of the apps started by a streaming Session. */ #define BOOST_BIND_GLOBAL_PLACEHOLDERS #ifndef BOOST_PROCESS_VERSION #define BOOST_PROCESS_VERSION 1 #endif // standard includes #include #include #include #include // lib includes #include #include #include #include #include #include #include #include // local includes #include "config.h" #include "crypto.h" #include "display_device.h" #include "file_handler.h" #include "logging.h" #include "platform/common.h" #include "process.h" #include "httpcommon.h" #include "system_tray.h" #include "utility.h" #include "video.h" #include "uuid.h" #ifdef _WIN32 // from_utf8() string conversion function #include "platform/windows/misc.h" #include "platform/windows/utils.h" // _SH constants for _wfsopen() #include #endif #define DEFAULT_APP_IMAGE_PATH SUNSHINE_ASSETS_DIR "/box.png" namespace proc { using namespace std::literals; namespace pt = boost::property_tree; proc_t proc; int input_only_app_id = -1; std::string input_only_app_id_str; int terminate_app_id = -1; std::string terminate_app_id_str; #ifdef _WIN32 VDISPLAY::DRIVER_STATUS vDisplayDriverStatus = VDISPLAY::DRIVER_STATUS::UNKNOWN; void onVDisplayWatchdogFailed() { vDisplayDriverStatus = VDISPLAY::DRIVER_STATUS::WATCHDOG_FAILED; VDISPLAY::closeVDisplayDevice(); } void initVDisplayDriver() { vDisplayDriverStatus = VDISPLAY::openVDisplayDevice(); if (vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK) { if (!VDISPLAY::startPingThread(onVDisplayWatchdogFailed)) { onVDisplayWatchdogFailed(); return; } } } #endif class deinit_t: public platf::deinit_t { public: ~deinit_t() { proc.terminate(); } }; std::unique_ptr init() { return std::make_unique(); } void terminate_process_group(boost::process::v1::child &proc, boost::process::v1::group &group, std::chrono::seconds exit_timeout) { if (group.valid() && platf::process_group_running((std::uintptr_t) group.native_handle())) { if (exit_timeout.count() > 0) { // Request processes in the group to exit gracefully if (platf::request_process_group_exit((std::uintptr_t) group.native_handle())) { // If the request was successful, wait for a little while for them to exit. BOOST_LOG(info) << "Successfully requested the app to exit. Waiting up to "sv << exit_timeout.count() << " seconds for it to close."sv; // group::wait_for() and similar functions are broken and deprecated, so we use a simple polling loop while (platf::process_group_running((std::uintptr_t) group.native_handle()) && (--exit_timeout).count() >= 0) { std::this_thread::sleep_for(1s); } if (exit_timeout.count() < 0) { BOOST_LOG(warning) << "App did not fully exit within the timeout. Terminating the app's remaining processes."sv; } else { BOOST_LOG(info) << "All app processes have successfully exited."sv; } } else { BOOST_LOG(info) << "App did not respond to a graceful termination request. Forcefully terminating the app's processes."sv; } } else { BOOST_LOG(info) << "No graceful exit timeout was specified for this app. Forcefully terminating the app's processes."sv; } // We always call terminate() even if we waited successfully for all processes above. // This ensures the process group state is consistent with the OS in boost. std::error_code ec; group.terminate(ec); group.detach(); } if (proc.valid()) { // avoid zombie process proc.detach(); } } boost::filesystem::path find_working_directory(const std::string &cmd, const boost::process::v1::environment &env) { // Parse the raw command string into parts to get the actual command portion #ifdef _WIN32 auto parts = boost::program_options::split_winmain(cmd); #else auto parts = boost::program_options::split_unix(cmd); #endif if (parts.empty()) { BOOST_LOG(error) << "Unable to parse command: "sv << cmd; return boost::filesystem::path(); } BOOST_LOG(debug) << "Parsed target ["sv << parts.at(0) << "] from command ["sv << cmd << ']'; // If the target is a URL, don't parse any further here if (parts.at(0).find("://") != std::string::npos) { return boost::filesystem::path(); } // If the cmd path is not an absolute path, resolve it using our PATH variable boost::filesystem::path cmd_path(parts.at(0)); if (!cmd_path.is_absolute()) { cmd_path = boost::process::v1::search_path(parts.at(0)); if (cmd_path.empty()) { BOOST_LOG(error) << "Unable to find executable ["sv << parts.at(0) << "]. Is it in your PATH?"sv; return boost::filesystem::path(); } } BOOST_LOG(debug) << "Resolved target ["sv << parts.at(0) << "] to path ["sv << cmd_path << ']'; // Now that we have a complete path, we can just use parent_path() return cmd_path.parent_path(); } void proc_t::launch_input_only() { _app_id = input_only_app_id; _app_name = "Remote Input"; _app.uuid = REMOTE_INPUT_UUID; _app.terminate_on_pause = true; allow_client_commands = false; placebo = true; #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_playing(_app_name); #endif } int proc_t::execute(const ctx_t& app, std::shared_ptr launch_session) { if (_app_id == input_only_app_id) { terminate(false, false); std::this_thread::sleep_for(1s); } else { // Ensure starting from a clean slate terminate(false, false); } _app = app; _app_id = util::from_view(app.id); _app_name = app.name; _launch_session = launch_session; allow_client_commands = app.allow_client_commands; uint32_t client_width = launch_session->width ? launch_session->width : 1920; uint32_t client_height = launch_session->height ? launch_session->height : 1080; uint32_t render_width = client_width; uint32_t render_height = client_height; int scale_factor = launch_session->scale_factor; if (_app.scale_factor != 100) { scale_factor = _app.scale_factor; } if (scale_factor != 100) { render_width *= ((float)scale_factor / 100); render_height *= ((float)scale_factor / 100); // Chop the last bit to ensure the scaled resolution is even numbered // Most odd resolutions won't work well render_width &= ~1; render_height &= ~1; } launch_session->width = render_width; launch_session->height = render_height; this->initial_display = config::video.output_name; // Executed when returning from function auto fg = util::fail_guard([&]() { // Restore to user defined output name config::video.output_name = this->initial_display; terminate(); display_device::revert_configuration(); }); if (!app.gamepad.empty()) { _saved_input_config = std::make_shared(config::input); if (app.gamepad == "disabled") { config::input.controller = false; } else { config::input.controller = true; config::input.gamepad = app.gamepad; } } #ifdef _WIN32 if ( config::video.headless_mode // Headless mode || launch_session->virtual_display // User requested virtual display || _app.virtual_display // App is configured to use virtual display || !video::allow_encoder_probing() // No active display presents ) { if (vDisplayDriverStatus != VDISPLAY::DRIVER_STATUS::OK) { // Try init driver again initVDisplayDriver(); } if (vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK) { // Try set the render adapter matching the capture adapter if user has specified one if (!config::video.adapter_name.empty()) { VDISPLAY::setRenderAdapterByName(platf::from_utf8(config::video.adapter_name)); } std::string device_name; std::string device_uuid_str; uuid_util::uuid_t device_uuid; if (_app.use_app_identity) { device_name = _app.name; if (_app.per_client_app_identity) { device_uuid = uuid_util::uuid_t::parse(launch_session->unique_id); auto app_uuid = uuid_util::uuid_t::parse(_app.uuid); // Use XOR to mix the two UUIDs device_uuid.b64[0] ^= app_uuid.b64[0]; device_uuid.b64[1] ^= app_uuid.b64[1]; device_uuid_str = device_uuid.string(); } else { device_uuid_str = _app.uuid; device_uuid = uuid_util::uuid_t::parse(_app.uuid); } } else { device_name = launch_session->device_name; device_uuid_str = launch_session->unique_id; device_uuid = uuid_util::uuid_t::parse(launch_session->unique_id); } memcpy(&launch_session->display_guid, &device_uuid, sizeof(GUID)); int target_fps = launch_session->fps ? launch_session->fps : 60000; if (target_fps < 1000) { target_fps *= 1000; } if (config::video.double_refreshrate) { target_fps *= 2; } std::wstring vdisplayName = VDISPLAY::createVirtualDisplay( device_uuid_str.c_str(), device_name.c_str(), render_width, render_height, target_fps, launch_session->display_guid ); // No matter we get the display name or not, the virtual display might still be created. // We need to track it properly to remove the display when the session terminates. launch_session->virtual_display = true; if (!vdisplayName.empty()) { BOOST_LOG(info) << "Virtual Display created at " << vdisplayName; // Don't change display settings when no params are given if (launch_session->width && launch_session->height && launch_session->fps) { // Apply display settings VDISPLAY::changeDisplaySettings(vdisplayName.c_str(), render_width, render_height, target_fps); } // Check the ISOLATED DISPLAY configuration setting and rearrange the displays if (config::video.isolated_virtual_display_option == true) { // Apply the isolated display settings VDISPLAY::changeDisplaySettings2(vdisplayName.c_str(), render_width, render_height, target_fps, true); } // Set virtual_display to true when everything went fine this->virtual_display = true; this->display_name = platf::to_utf8(vdisplayName); // When using virtual display, we don't care which display user configured to use. // So we always set output_name to the newly created virtual display as a workaround for // empty name when probing graphics cards. config::video.output_name = display_device::map_display_name(this->display_name); } else { BOOST_LOG(warning) << "Virtual Display creation failed, or cannot get created display name in time!"; } } else { // Driver isn't working so we don't need to track virtual display. launch_session->virtual_display = false; } } display_device::configure_display(config::video, *launch_session); // We should not preserve display state when using virtual display. // It is already handled by Windows properly. if (this->virtual_display) { display_device::reset_persistence(); } #else display_device::configure_display(config::video, *launch_session); #endif // Probe encoders again before streaming to ensure our chosen // encoder matches the active GPU (which could have changed // due to hotplugging, driver crash, primary monitor change, // or any number of other factors). if (rtsp_stream::session_count() == 0 && video::probe_encoders()) { if (config::video.ignore_encoder_probe_failure) { BOOST_LOG(warning) << "Encoder probe failed, but continuing due to user configuration."; } else { return 503; } } std::string fps_str; char fps_buf[8]; snprintf(fps_buf, sizeof(fps_buf), "%.3f", (float)launch_session->fps / 1000.0f); fps_str = fps_buf; // Add Stream-specific environment variables // Sunshine Compatibility _env["SUNSHINE_APP_ID"] = _app.id; _env["SUNSHINE_APP_NAME"] = _app.name; _env["SUNSHINE_CLIENT_WIDTH"] = std::to_string(render_width); _env["SUNSHINE_CLIENT_HEIGHT"] = std::to_string(render_height); _env["SUNSHINE_CLIENT_FPS"] = config::sunshine.envvar_compatibility_mode ? std::to_string(std::round((float)launch_session->fps / 1000.0f)) : fps_str; _env["SUNSHINE_CLIENT_HDR"] = launch_session->enable_hdr ? "true" : "false"; _env["SUNSHINE_CLIENT_GCMAP"] = std::to_string(launch_session->gcmap); _env["SUNSHINE_CLIENT_HOST_AUDIO"] = launch_session->host_audio ? "true" : "false"; _env["SUNSHINE_CLIENT_ENABLE_SOPS"] = launch_session->enable_sops ? "true" : "false"; _env["APOLLO_APP_ID"] = _app.id; _env["APOLLO_APP_NAME"] = _app.name; _env["APOLLO_APP_UUID"] = _app.uuid; _env["APOLLO_APP_STATUS"] = "STARTING"; _env["APOLLO_CLIENT_UUID"] = launch_session->unique_id; _env["APOLLO_CLIENT_NAME"] = launch_session->device_name; _env["APOLLO_CLIENT_WIDTH"] = std::to_string(render_width); _env["APOLLO_CLIENT_HEIGHT"] = std::to_string(render_height); _env["APOLLO_CLIENT_RENDER_WIDTH"] = std::to_string(launch_session->width); _env["APOLLO_CLIENT_RENDER_HEIGHT"] = std::to_string(launch_session->height); _env["APOLLO_CLIENT_SCALE_FACTOR"] = std::to_string(scale_factor); _env["APOLLO_CLIENT_FPS"] = fps_str; _env["APOLLO_CLIENT_HDR"] = launch_session->enable_hdr ? "true" : "false"; _env["APOLLO_CLIENT_GCMAP"] = std::to_string(launch_session->gcmap); _env["APOLLO_CLIENT_HOST_AUDIO"] = launch_session->host_audio ? "true" : "false"; _env["APOLLO_CLIENT_ENABLE_SOPS"] = launch_session->enable_sops ? "true" : "false"; int channelCount = launch_session->surround_info & 65535; switch (channelCount) { case 2: _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "2.0"; _env["APOLLO_CLIENT_AUDIO_CONFIGURATION"] = "2.0"; break; case 6: _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "5.1"; _env["APOLLO_CLIENT_AUDIO_CONFIGURATION"] = "5.1"; break; case 8: _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "7.1"; _env["APOLLO_CLIENT_AUDIO_CONFIGURATION"] = "7.1"; break; } _env["SUNSHINE_CLIENT_AUDIO_SURROUND_PARAMS"] = launch_session->surround_params; _env["APOLLO_CLIENT_AUDIO_SURROUND_PARAMS"] = launch_session->surround_params; if (!_app.output.empty() && _app.output != "null"sv) { #ifdef _WIN32 // fopen() interprets the filename as an ANSI string on Windows, so we must convert it // to UTF-16 and use the wchar_t variants for proper Unicode log file path support. auto woutput = platf::from_utf8(_app.output); // Use _SH_DENYNO to allow us to open this log file again for writing even if it is // still open from a previous execution. This is required to handle the case of a // detached process executing again while the previous process is still running. _pipe.reset(_wfsopen(woutput.c_str(), L"a", _SH_DENYNO)); #else _pipe.reset(fopen(_app.output.c_str(), "a")); #endif } std::error_code ec; _app_prep_begin = std::begin(_app.prep_cmds); _app_prep_it = _app_prep_begin; for (; _app_prep_it != std::end(_app.prep_cmds); ++_app_prep_it) { auto &cmd = *_app_prep_it; // Skip empty commands if (cmd.do_cmd.empty()) { continue; } boost::filesystem::path working_dir = _app.working_dir.empty() ? find_working_directory(cmd.do_cmd, _env) : boost::filesystem::path(_app.working_dir); BOOST_LOG(info) << "Executing Do Cmd: ["sv << cmd.do_cmd << "] elevated: " << cmd.elevated; auto child = platf::run_command(cmd.elevated, true, cmd.do_cmd, working_dir, _env, _pipe.get(), ec, nullptr); if (ec) { BOOST_LOG(error) << "Couldn't run ["sv << cmd.do_cmd << "]: System: "sv << ec.message(); // We don't want any prep commands failing launch of the desktop. // This is to prevent the issue where users reboot their PC and need to log in with Sunshine. // permission_denied is typically returned when the user impersonation fails, which can happen when user is not signed in yet. if (!(_app.cmd.empty() && ec == std::errc::permission_denied)) { return -1; } } child.wait(); auto ret = child.exit_code(); if (ret != 0 && ec != std::errc::permission_denied) { BOOST_LOG(error) << '[' << cmd.do_cmd << "] failed with code ["sv << ret << ']'; return -1; } } _env["APOLLO_APP_STATUS"] = "RUNNING"; for (auto &cmd : _app.detached) { boost::filesystem::path working_dir = _app.working_dir.empty() ? find_working_directory(cmd, _env) : boost::filesystem::path(_app.working_dir); BOOST_LOG(info) << "Spawning ["sv << cmd << "] in ["sv << working_dir << ']'; auto child = platf::run_command(_app.elevated, true, cmd, working_dir, _env, _pipe.get(), ec, nullptr); if (ec) { BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd << "]: System: "sv << ec.message(); } else { child.detach(); } } if (_app.cmd.empty()) { BOOST_LOG(info) << "No commands configured, showing desktop..."sv; placebo = true; } else { boost::filesystem::path working_dir = _app.working_dir.empty() ? find_working_directory(_app.cmd, _env) : boost::filesystem::path(_app.working_dir); BOOST_LOG(info) << "Executing: ["sv << _app.cmd << "] in ["sv << working_dir << ']'; _process = platf::run_command(_app.elevated, true, _app.cmd, working_dir, _env, _pipe.get(), ec, &_process_group); if (ec) { BOOST_LOG(warning) << "Couldn't run ["sv << _app.cmd << "]: System: "sv << ec.message(); return -1; } } _app_launch_time = std::chrono::steady_clock::now(); #ifdef _WIN32 auto resetHDRThread = std::thread([this, enable_hdr = launch_session->enable_hdr]{ // Windows doesn't seem to be able to set HDR correctly when a display is just connected / changed resolution, // so we have tooggle HDR for the virtual display manually after a delay. auto retryInterval = 200ms; while (is_changing_settings_going_to_fail()) { if (retryInterval > 2s) { BOOST_LOG(warning) << "Restoring HDR settings failed due to retry timeout!"; return; } std::this_thread::sleep_for(retryInterval); retryInterval *= 2; } retryInterval = 200ms; while (this->display_name.empty()) { if (retryInterval > 2s) { BOOST_LOG(warning) << "Not getting current display in time! HDR will not be toggled."; return; } std::this_thread::sleep_for(retryInterval); retryInterval *= 2; } // We should have got the actual streaming display by now std::string currentDisplay = this->display_name; auto currentDisplayW = platf::from_utf8(currentDisplay); initial_hdr = VDISPLAY::getDisplayHDRByName(currentDisplayW.c_str()); if (config::video.dd.hdr_option == config::video_t::dd_t::hdr_option_e::automatic) { mode_changed_display = currentDisplay; // Try turn off HDR whatever // As we always have to apply the workaround by turining off HDR first VDISPLAY::setDisplayHDRByName(currentDisplayW.c_str(), false); if (enable_hdr) { if (VDISPLAY::setDisplayHDRByName(currentDisplayW.c_str(), true)) { BOOST_LOG(info) << "HDR enabled for display " << currentDisplay; } else { BOOST_LOG(info) << "HDR enable failed for display " << currentDisplay; } } } else if (initial_hdr) { if (VDISPLAY::setDisplayHDRByName(currentDisplayW.c_str(), false) && VDISPLAY::setDisplayHDRByName(currentDisplayW.c_str(), true)) { BOOST_LOG(info) << "HDR toggled successfully for display " << currentDisplay; } else { BOOST_LOG(info) << "HDR toggle failed for display " << currentDisplay; } } }); resetHDRThread.detach(); #endif fg.disable(); #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_playing(_app.name); #endif return 0; } int proc_t::running() { #ifndef _WIN32 // On POSIX OSes, we must periodically wait for our children to avoid // them becoming zombies. This must be synchronized carefully with // calls to bp::wait() and platf::process_group_running() which both // invoke waitpid() under the hood. auto reaper = util::fail_guard([]() { while (waitpid(-1, nullptr, WNOHANG) > 0); }); #endif if (placebo) { return _app_id; } else if (_app.wait_all && _process_group && platf::process_group_running((std::uintptr_t) _process_group.native_handle())) { // The app is still running if any process in the group is still running return _app_id; } else if (_process.running()) { // The app is still running only if the initial process launched is still running return _app_id; } else if (_app.auto_detach && std::chrono::steady_clock::now() - _app_launch_time < 5s) { BOOST_LOG(info) << "App exited with code ["sv << _process.native_exit_code() << "] within 5 seconds of launch. Treating the app as a detached command."sv; BOOST_LOG(info) << "Adjust this behavior in the Applications tab or apps.json if this is not what you want."sv; placebo = true; #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 if (_process.native_exit_code() != 0) { system_tray::update_tray_launch_error(proc::proc.get_last_run_app_name(), _process.native_exit_code()); } #endif return _app_id; } // Perform cleanup actions now if needed if (_process) { terminate(); } return 0; } void proc_t::resume() { BOOST_LOG(info) << "Session resuming for app [" << _app_name << "]."; if (!_app.state_cmds.empty()) { auto exec_thread = std::thread([cmd_list = _app.state_cmds, app_working_dir = _app.working_dir, _env = _env]() mutable { _env["APOLLO_APP_STATUS"] = "RESUMING"; std::error_code ec; auto _state_resume_it = std::begin(cmd_list); for (; _state_resume_it != std::end(cmd_list); ++_state_resume_it) { auto &cmd = *_state_resume_it; // Skip empty commands if (cmd.do_cmd.empty()) { continue; } boost::filesystem::path working_dir = app_working_dir.empty() ? find_working_directory(cmd.do_cmd, _env) : boost::filesystem::path(app_working_dir); BOOST_LOG(info) << "Executing Resume Cmd: ["sv << cmd.do_cmd << "] elevated: " << cmd.elevated; auto child = platf::run_command(cmd.elevated, true, cmd.do_cmd, working_dir, _env, nullptr, ec, nullptr); if (ec) { BOOST_LOG(error) << "Couldn't run ["sv << cmd.do_cmd << "]: System: "sv << ec.message(); break; } child.wait(); auto ret = child.exit_code(); if (ret != 0 && ec != std::errc::permission_denied) { BOOST_LOG(error) << '[' << cmd.do_cmd << "] failed with code ["sv << ret << ']'; break; } } }); exec_thread.detach(); } } void proc_t::pause() { if (!running()) { BOOST_LOG(info) << "Session already stopped, do not run pause commands."; return; } if (_app.terminate_on_pause) { BOOST_LOG(info) << "Terminating app [" << _app_name << "] when all clients are disconnected. Pause commands are skipped."; terminate(); return; } BOOST_LOG(info) << "Session pausing for app [" << _app_name << "]."; if (!_app.state_cmds.empty()) { auto exec_thread = std::thread([cmd_list = _app.state_cmds, app_working_dir = _app.working_dir, _env = _env]() mutable { _env["APOLLO_APP_STATUS"] = "PAUSING"; std::error_code ec; auto _state_pause_it = std::begin(cmd_list); for (; _state_pause_it != std::end(cmd_list); ++_state_pause_it) { auto &cmd = *_state_pause_it; // Skip empty commands if (cmd.undo_cmd.empty()) { continue; } boost::filesystem::path working_dir = app_working_dir.empty() ? find_working_directory(cmd.undo_cmd, _env) : boost::filesystem::path(app_working_dir); BOOST_LOG(info) << "Executing Pause Cmd: ["sv << cmd.undo_cmd << "] elevated: " << cmd.elevated; auto child = platf::run_command(cmd.elevated, true, cmd.undo_cmd, working_dir, _env, nullptr, ec, nullptr); if (ec) { BOOST_LOG(error) << "Couldn't run ["sv << cmd.undo_cmd << "]: System: "sv << ec.message(); break; } child.wait(); auto ret = child.exit_code(); if (ret != 0 && ec != std::errc::permission_denied) { BOOST_LOG(error) << '[' << cmd.undo_cmd << "] failed with code ["sv << ret << ']'; break; } } }); exec_thread.detach(); } #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_pausing(proc::proc.get_last_run_app_name()); #endif } void proc_t::terminate(bool immediate, bool needs_refresh) { std::error_code ec; placebo = false; if (!immediate) { terminate_process_group(_process, _process_group, _app.exit_timeout); } _process = boost::process::v1::child(); _process_group = boost::process::v1::group(); _env["APOLLO_APP_STATUS"] = "TERMINATING"; for (; _app_prep_it != _app_prep_begin; --_app_prep_it) { auto &cmd = *(_app_prep_it - 1); if (cmd.undo_cmd.empty()) { continue; } boost::filesystem::path working_dir = _app.working_dir.empty() ? find_working_directory(cmd.undo_cmd, _env) : boost::filesystem::path(_app.working_dir); BOOST_LOG(info) << "Executing Undo Cmd: ["sv << cmd.undo_cmd << ']'; auto child = platf::run_command(cmd.elevated, true, cmd.undo_cmd, working_dir, _env, _pipe.get(), ec, nullptr); if (ec) { BOOST_LOG(warning) << "System: "sv << ec.message(); } child.wait(); auto ret = child.exit_code(); if (ret != 0) { BOOST_LOG(warning) << "Return code ["sv << ret << ']'; } } _pipe.reset(); bool has_run = _app_id > 0; #ifdef _WIN32 // Revert HDR state if (has_run && !mode_changed_display.empty()) { auto displayNameW = platf::from_utf8(mode_changed_display); if (VDISPLAY::setDisplayHDRByName(displayNameW.c_str(), initial_hdr)) { BOOST_LOG(info) << "HDR reverted for display " << mode_changed_display; } else { BOOST_LOG(info) << "HDR revert failed for display " << mode_changed_display; } } bool used_virtual_display = vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK && _launch_session && _launch_session->virtual_display; if (used_virtual_display) { if (VDISPLAY::removeVirtualDisplay(_launch_session->display_guid)) { BOOST_LOG(info) << "Virtual Display removed successfully"; } else if (this->virtual_display) { BOOST_LOG(warning) << "Virtual Display remove failed"; } else { BOOST_LOG(warning) << "Virtual Display remove failed, but it seems it was not created correctly either."; } } // Only show the Stopped notification if we actually have an app to stop // Since terminate() is always run when a new app has started if (proc::proc.get_last_run_app_name().length() > 0 && has_run) { if (used_virtual_display) { display_device::reset_persistence(); } else { display_device::revert_configuration(); } #else if (proc::proc.get_last_run_app_name().length() > 0 && has_run) { display_device::revert_configuration(); #endif #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_stopped(proc::proc.get_last_run_app_name()); #endif } // Load the configured output_name first // to prevent the value being write to empty when the initial terminate happens if (!has_run && initial_display.empty()) { initial_display = config::video.output_name; } else { // Restore output name to its original value config::video.output_name = initial_display; } _app_id = -1; _app_name.clear(); _app = {}; display_name.clear(); initial_display.clear(); mode_changed_display.clear(); _launch_session.reset(); virtual_display = false; allow_client_commands = false; if (_saved_input_config) { config::input = *_saved_input_config; _saved_input_config.reset(); } if (needs_refresh) { refresh(config::stream.file_apps, false); } } const std::vector &proc_t::get_apps() const { return _apps; } std::vector &proc_t::get_apps() { return _apps; } // Gets application image from application list. // Returns image from assets directory if found there. // Returns default image if image configuration is not set. // Returns http content-type header compatible image type. std::string proc_t::get_app_image(int app_id) { auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto app) { return app.id == std::to_string(app_id); }); auto app_image_path = iter == _apps.end() ? std::string() : iter->image_path; return validate_app_image_path(app_image_path); } std::string proc_t::get_last_run_app_name() { return _app_name; } std::string proc_t::get_running_app_uuid() { return _app.uuid; } boost::process::environment proc_t::get_env() { return _env; } proc_t::~proc_t() { // It's not safe to call terminate() here because our proc_t is a static variable // that may be destroyed after the Boost loggers have been destroyed. Instead, // we return a deinit_t to main() to handle termination when we're exiting. // Once we reach this point here, termination must have already happened. assert(!placebo); assert(!_process.running()); } std::string_view::iterator find_match(std::string_view::iterator begin, std::string_view::iterator end) { int stack = 0; --begin; do { ++begin; switch (*begin) { case '(': ++stack; break; case ')': --stack; } } while (begin != end && stack != 0); if (begin == end) { throw std::out_of_range("Missing closing bracket \')\'"); } return begin; } std::string parse_env_val(boost::process::v1::native_environment &env, const std::string_view &val_raw) { auto pos = std::begin(val_raw); auto dollar = std::find(pos, std::end(val_raw), '$'); std::stringstream ss; while (dollar != std::end(val_raw)) { auto next = dollar + 1; if (next != std::end(val_raw)) { switch (*next) { case '(': { ss.write(pos, (dollar - pos)); auto var_begin = next + 1; auto var_end = find_match(next, std::end(val_raw)); auto var_name = std::string {var_begin, var_end}; #ifdef _WIN32 // Windows treats environment variable names in a case-insensitive manner, // so we look for a case-insensitive match here. This is critical for // correctly appending to PATH on Windows. auto itr = std::find_if(env.cbegin(), env.cend(), [&](const auto &e) { return boost::iequals(e.get_name(), var_name); }); if (itr != env.cend()) { // Use an existing case-insensitive match var_name = itr->get_name(); } #endif ss << env[var_name].to_string(); pos = var_end + 1; next = var_end; break; } case '$': ss.write(pos, (next - pos)); pos = next + 1; ++next; break; } dollar = std::find(next, std::end(val_raw), '$'); } else { dollar = next; } } ss.write(pos, (dollar - pos)); return ss.str(); } std::string validate_app_image_path(std::string app_image_path) { if (app_image_path.empty()) { return DEFAULT_APP_IMAGE_PATH; } // get the image extension and convert it to lowercase auto image_extension = std::filesystem::path(app_image_path).extension().string(); boost::to_lower(image_extension); // return the default box image if extension is not "png" if (image_extension != ".png") { return DEFAULT_APP_IMAGE_PATH; } // check if image is in assets directory auto full_image_path = std::filesystem::path(SUNSHINE_ASSETS_DIR) / app_image_path; if (std::filesystem::exists(full_image_path)) { return full_image_path.string(); } else if (app_image_path == "./assets/steam.png") { // handle old default steam image definition return SUNSHINE_ASSETS_DIR "/steam.png"; } // check if specified image exists std::error_code code; if (!std::filesystem::exists(app_image_path, code)) { // return default box image if image does not exist BOOST_LOG(warning) << "Couldn't find app image at path ["sv << app_image_path << ']'; return DEFAULT_APP_IMAGE_PATH; } // image is a png, and not in assets directory // return only "content-type" http header compatible image type return app_image_path; } std::optional calculate_sha256(const std::string &filename) { crypto::md_ctx_t ctx {EVP_MD_CTX_create()}; if (!ctx) { return std::nullopt; } if (!EVP_DigestInit_ex(ctx.get(), EVP_sha256(), nullptr)) { return std::nullopt; } // Read file and update calculated SHA char buf[1024 * 16]; std::ifstream file(filename, std::ifstream::binary); while (file.good()) { file.read(buf, sizeof(buf)); if (!EVP_DigestUpdate(ctx.get(), buf, file.gcount())) { return std::nullopt; } } file.close(); unsigned char result[SHA256_DIGEST_LENGTH]; if (!EVP_DigestFinal_ex(ctx.get(), result, nullptr)) { return std::nullopt; } // Transform byte-array to string std::stringstream ss; ss << std::hex << std::setfill('0'); for (const auto &byte : result) { ss << std::setw(2) << (int) byte; } return ss.str(); } uint32_t calculate_crc32(const std::string &input) { boost::crc_32_type result; result.process_bytes(input.data(), input.length()); return result.checksum(); } std::tuple calculate_app_id(const std::string &app_name, std::string app_image_path, int index) { // Generate id by hashing name with image data if present std::vector to_hash; to_hash.push_back(app_name); auto file_path = validate_app_image_path(app_image_path); if (file_path != DEFAULT_APP_IMAGE_PATH) { auto file_hash = calculate_sha256(file_path); if (file_hash) { to_hash.push_back(file_hash.value()); } else { // Fallback to just hashing image path to_hash.push_back(file_path); } } // Create combined strings for hash std::stringstream ss; for_each(to_hash.begin(), to_hash.end(), [&ss](const std::string &s) { ss << s; }); auto input_no_index = ss.str(); ss << index; auto input_with_index = ss.str(); // CRC32 then truncate to signed 32-bit range due to client limitations auto id_no_index = std::to_string(abs((int32_t) calculate_crc32(input_no_index))); auto id_with_index = std::to_string(abs((int32_t) calculate_crc32(input_with_index))); return std::make_tuple(id_no_index, id_with_index); } /** * @brief Migrate the applications stored in the file tree by merging in a new app. * * This function updates the application entries in *fileTree_p* using the data in *inputTree_p*. * If an app in the file tree does not have a UUID, one is generated and inserted. * If an app with the same UUID as the new app is found, it is replaced. * Additionally, empty keys (such as "prep-cmd" or "detached") and keys no longer needed ("launching", "index") * are removed from the input. * * Legacy versions of Sunshine/Apollo stored boolean and integer values as strings. * The following keys are converted: * - Boolean keys: "exclude-global-prep-cmd", "elevated", "auto-detach", "wait-all", * "use-app-identity", "per-client-app-identity", "virtual-display" * - Integer keys: "exit-timeout" * * A migration version is stored in the file tree (under "version") so that future changes can be applied. * * @param fileTree_p Pointer to the JSON object representing the file tree. * @param inputTree_p Pointer to the JSON object representing the new app. */ void migrate_apps(nlohmann::json* fileTree_p, nlohmann::json* inputTree_p) { std::string new_app_uuid; if (inputTree_p) { // If the input contains a non-empty "uuid", use it; otherwise generate one. if (inputTree_p->contains("uuid") && !(*inputTree_p)["uuid"].get().empty()) { new_app_uuid = (*inputTree_p)["uuid"].get(); } else { new_app_uuid = uuid_util::uuid_t::generate().string(); (*inputTree_p)["uuid"] = new_app_uuid; } // Remove "prep-cmd" if empty. if (inputTree_p->contains("prep-cmd") && (*inputTree_p)["prep-cmd"].empty()) { inputTree_p->erase("prep-cmd"); } // Remove "detached" if empty. if (inputTree_p->contains("detached") && (*inputTree_p)["detached"].empty()) { inputTree_p->erase("detached"); } // Remove keys that are no longer needed. inputTree_p->erase("launching"); inputTree_p->erase("index"); } // Get the current apps array; if it doesn't exist, create one. nlohmann::json newApps = nlohmann::json::array(); if (fileTree_p->contains("apps") && (*fileTree_p)["apps"].is_array()) { for (auto &app : (*fileTree_p)["apps"]) { // For apps without a UUID, generate one and remove "launching". if (!app.contains("uuid") || app["uuid"].get().empty()) { app["uuid"] = uuid_util::uuid_t::generate().string(); app.erase("launching"); newApps.push_back(std::move(app)); } else { // If an app with the same UUID as the new app is found, replace it. if (!new_app_uuid.empty() && app["uuid"].get() == new_app_uuid) { newApps.push_back(*inputTree_p); new_app_uuid.clear(); } else { newApps.push_back(std::move(app)); } } } } // If the new app's UUID has not been merged yet, add it. if (!new_app_uuid.empty() && inputTree_p) { newApps.push_back(*inputTree_p); } (*fileTree_p)["apps"] = newApps; } void migration_v2(nlohmann::json& fileTree) { static const int this_version = 2; // Determine the current migration version (default to 1 if not present). int file_version = 1; if (fileTree.contains("version")) { try { file_version = fileTree["version"].get(); } catch (const std::exception& e) { BOOST_LOG(info) << "Cannot parse apps.json version, treating as v1: " << e.what(); } } // If the version is less than this_version, perform legacy conversion. if (file_version < this_version) { BOOST_LOG(info) << "Migrating app list from v1 to v2..."; migrate_apps(&fileTree, nullptr); // List of keys to convert to booleans. std::vector boolean_keys = { "allow-client-commands", "exclude-global-prep-cmd", "elevated", "auto-detach", "wait-all", "use-app-identity", "per-client-app-identity", "virtual-display" }; // List of keys to convert to integers. std::vector integer_keys = { "exit-timeout", "scale-factor" }; // Walk through each app and convert legacy string values. for (auto &app : fileTree["apps"]) { for (const auto &key : boolean_keys) { if (app.contains(key)) { auto& _key = app[key]; if (_key.is_string()) { std::string s = _key.get(); std::transform(s.begin(), s.end(), s.begin(), ::tolower); // Normalize to lowercase for comparison _key = (s == "true" || s == "on" || s == "yes"); } else if (_key.is_array()) { // Check if the array contains at least one item and interpret the first element if (!_key.empty() && _key[0].is_string()) { std::string first = _key[0].get(); std::transform(first.begin(), first.end(), first.begin(), ::tolower); // Normalize if (first == "on" || first == "true" || first == "yes") { _key = true; } else if (first == "off" || first == "false" || first == "no") { _key = false; } else { _key = false; // Default for unknown values } } else { _key = false; // Treat empty arrays or non-string first elements as false } } else { // Fallback: Treat truthy/falsey cases if (_key.is_boolean()) { // Leave booleans as they are } else if (_key.is_number()) { _key = (_key.get() != 0); // Non-zero numbers are truthy } else if (_key.is_null()) { _key = false; // Null is false } else { _key = !_key.empty(); // Non-empty objects/arrays are truthy, empty ones are falsey } } } } for (const auto &key : integer_keys) { if (app.contains(key) && app[key].is_string()) { std::string s = app[key].get(); app[key] = std::stoi(s); } } // For each entry in the "prep-cmd" array, convert "elevated" if necessary. if (app.contains("prep-cmd") && app["prep-cmd"].is_array()) { for (auto &prep : app["prep-cmd"]) { if (prep.contains("elevated") && prep["elevated"].is_string()) { std::string s = prep["elevated"].get(); prep["elevated"] = (s == "true"); } } } } // Update migration version to this_version. fileTree["version"] = this_version; BOOST_LOG(info) << "Migrated app list from v1 to v2."; } } void migrate(nlohmann::json& fileTree, const std::string& fileName) { int last_version = 2; int file_version = 0; if (fileTree.contains("version")) { file_version = fileTree["version"].get(); } if (file_version < last_version) { migration_v2(fileTree); file_handler::write_file(fileName.c_str(), fileTree.dump(4)); } } std::optional parse(const std::string &file_name) { // Prepare environment variables. auto this_env = boost::this_process::environment(); std::set ids; std::vector apps; int i = 0; size_t fail_count = 0; do { // Read the JSON file into a tree. nlohmann::json tree; try { std::string content = file_handler::read_file(file_name.c_str()); tree = nlohmann::json::parse(content); } catch (const std::exception& e) { BOOST_LOG(warning) << "Couldn't read apps.json properly! Apps will not be loaded."sv; break; } try { migrate(tree, file_name); if (tree.contains("env") && tree["env"].is_object()) { for (auto &item : tree["env"].items()) { this_env[item.key()] = parse_env_val(this_env, item.value().get()); } } // Ensure the "apps" array exists. if (!tree.contains("apps") || !tree["apps"].is_array()) { BOOST_LOG(warning) << "No apps were defined in apps.json!!!"sv; break; } // Iterate over each application in the "apps" array. for (auto &app_node : tree["apps"]) { proc::ctx_t ctx; ctx.idx = std::to_string(i); ctx.uuid = app_node.at("uuid"); // Build the list of preparation commands. std::vector prep_cmds; bool exclude_global_prep = app_node.value("exclude-global-prep-cmd", false); if (!exclude_global_prep) { prep_cmds.reserve(config::sunshine.prep_cmds.size()); for (auto &prep_cmd : config::sunshine.prep_cmds) { auto do_cmd = parse_env_val(this_env, prep_cmd.do_cmd); auto undo_cmd = parse_env_val(this_env, prep_cmd.undo_cmd); prep_cmds.emplace_back( std::move(do_cmd), std::move(undo_cmd), std::move(prep_cmd.elevated) ); } } if (app_node.contains("prep-cmd") && app_node["prep-cmd"].is_array()) { for (auto &prep_node : app_node["prep-cmd"]) { std::string do_cmd = parse_env_val(this_env, prep_node.value("do", "")); std::string undo_cmd = parse_env_val(this_env, prep_node.value("undo", "")); bool elevated = prep_node.value("elevated", false); prep_cmds.emplace_back( std::move(do_cmd), std::move(undo_cmd), std::move(elevated) ); } } // Build the list of pause/resume commands. std::vector state_cmds; bool exclude_global_state_cmds = app_node.value("exclude-global-state-cmd", false); if (!exclude_global_state_cmds) { state_cmds.reserve(config::sunshine.state_cmds.size()); for (auto &state_cmd : config::sunshine.state_cmds) { auto do_cmd = parse_env_val(this_env, state_cmd.do_cmd); auto undo_cmd = parse_env_val(this_env, state_cmd.undo_cmd); state_cmds.emplace_back( std::move(do_cmd), std::move(undo_cmd), std::move(state_cmd.elevated) ); } } if (app_node.contains("state-cmd") && app_node["state-cmd"].is_array()) { for (auto &prep_node : app_node["state-cmd"]) { std::string do_cmd = parse_env_val(this_env, prep_node.value("do", "")); std::string undo_cmd = parse_env_val(this_env, prep_node.value("undo", "")); bool elevated = prep_node.value("elevated", false); state_cmds.emplace_back( std::move(do_cmd), std::move(undo_cmd), std::move(elevated) ); } } // Build the list of detached commands. std::vector detached; if (app_node.contains("detached") && app_node["detached"].is_array()) { for (auto &detached_val : app_node["detached"]) { detached.emplace_back(parse_env_val(this_env, detached_val.get())); } } // Process other fields. if (app_node.contains("output")) ctx.output = parse_env_val(this_env, app_node.value("output", "")); std::string name = parse_env_val(this_env, app_node.value("name", "")); if (app_node.contains("cmd")) ctx.cmd = parse_env_val(this_env, app_node.value("cmd", "")); if (app_node.contains("working-dir")) { ctx.working_dir = parse_env_val(this_env, app_node.value("working-dir", "")); #ifdef _WIN32 // The working directory, unlike the command itself, should not be quoted. boost::erase_all(ctx.working_dir, "\""); ctx.working_dir += '\\'; #endif } if (app_node.contains("image-path")) ctx.image_path = parse_env_val(this_env, app_node.value("image-path", "")); ctx.elevated = app_node.value("elevated", false); ctx.auto_detach = app_node.value("auto-detach", true); ctx.wait_all = app_node.value("wait-all", true); ctx.exit_timeout = std::chrono::seconds { app_node.value("exit-timeout", 5) }; ctx.virtual_display = app_node.value("virtual-display", false); ctx.scale_factor = app_node.value("scale-factor", 100); ctx.use_app_identity = app_node.value("use-app-identity", false); ctx.per_client_app_identity = app_node.value("per-client-app-identity", false); ctx.allow_client_commands = app_node.value("allow-client-commands", true); ctx.terminate_on_pause = app_node.value("terminate-on-pause", false); ctx.gamepad = app_node.value("gamepad", ""); // Calculate a unique application id. auto possible_ids = calculate_app_id(name, ctx.image_path, i++); if (ids.count(std::get<0>(possible_ids)) == 0) { ctx.id = std::get<0>(possible_ids); } else { ctx.id = std::get<1>(possible_ids); } ids.insert(ctx.id); ctx.name = std::move(name); ctx.prep_cmds = std::move(prep_cmds); ctx.state_cmds = std::move(state_cmds); ctx.detached = std::move(detached); apps.emplace_back(std::move(ctx)); } fail_count = 0; } catch (std::exception &e) { BOOST_LOG(error) << "Error happened during app loading: "sv << e.what(); fail_count += 1; if (fail_count >= 3) { // No hope for recovering BOOST_LOG(warning) << "Couldn't parse/migrate apps.json properly! Apps will not be loaded."sv; break; } BOOST_LOG(warning) << "App format is still invalid! Trying to re-migrate the app list..."sv; // Always try migrating from scratch when error happened tree["version"] = 0; try { migrate(tree, file_name); } catch (std::exception &e) { BOOST_LOG(error) << "Error happened during migration: "sv << e.what(); break; } this_env = boost::this_process::environment(); ids.clear(); apps.clear(); i = 0; continue; } break; } while (fail_count < 3); if (fail_count > 0) { BOOST_LOG(warning) << "No applications configured, adding fallback Desktop entry."; proc::ctx_t ctx; ctx.idx = std::to_string(i); ctx.uuid = FALLBACK_DESKTOP_UUID; // Placeholder UUID ctx.name = "Desktop (fallback)"; ctx.image_path = parse_env_val(this_env, "desktop-alt.png"); ctx.virtual_display = false; ctx.scale_factor = 100; ctx.use_app_identity = false; ctx.per_client_app_identity = false; ctx.allow_client_commands = false; ctx.terminate_on_pause = false; ctx.elevated = false; ctx.auto_detach = true; ctx.wait_all = false; // Desktop doesn't have a specific command to wait for ctx.exit_timeout = 5s; // Calculate unique ID auto possible_ids = calculate_app_id(ctx.name, ctx.image_path, i++); if (ids.count(std::get<0>(possible_ids)) == 0) { // Avoid using index to generate id if possible ctx.id = std::get<0>(possible_ids); } else { // Fallback to include index on collision ctx.id = std::get<1>(possible_ids); } ids.insert(ctx.id); apps.emplace_back(std::move(ctx)); } // Virtual Display entry #ifdef _WIN32 if (vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK) { proc::ctx_t ctx; ctx.idx = std::to_string(i); ctx.uuid = VIRTUAL_DISPLAY_UUID; ctx.name = "Virtual Display"; ctx.image_path = parse_env_val(this_env, "virtual_desktop.png"); ctx.virtual_display = true; ctx.scale_factor = 100; ctx.use_app_identity = false; ctx.per_client_app_identity = false; ctx.allow_client_commands = false; ctx.terminate_on_pause = false; ctx.elevated = false; ctx.auto_detach = true; ctx.wait_all = false; ctx.exit_timeout = 5s; auto possible_ids = calculate_app_id(ctx.name, ctx.image_path, i++); if (ids.count(std::get<0>(possible_ids)) == 0) { // Avoid using index to generate id if possible ctx.id = std::get<0>(possible_ids); } else { // Fallback to include index on collision ctx.id = std::get<1>(possible_ids); } ids.insert(ctx.id); apps.emplace_back(std::move(ctx)); } #endif if (config::input.enable_input_only_mode) { // Input Only entry { proc::ctx_t ctx; ctx.idx = std::to_string(i); ctx.uuid = REMOTE_INPUT_UUID; ctx.name = "Remote Input"; ctx.image_path = parse_env_val(this_env, "input_only.png"); ctx.virtual_display = false; ctx.scale_factor = 100; ctx.use_app_identity = false; ctx.per_client_app_identity = false; ctx.allow_client_commands = false; ctx.terminate_on_pause = true; // There's no need to keep an active input only session ongoing ctx.elevated = false; ctx.auto_detach = true; ctx.wait_all = true; ctx.exit_timeout = 5s; auto possible_ids = calculate_app_id(ctx.name, ctx.image_path, i++); if (ids.count(std::get<0>(possible_ids)) == 0) { // Avoid using index to generate id if possible ctx.id = std::get<0>(possible_ids); } else { // Fallback to include index on collision ctx.id = std::get<1>(possible_ids); } ids.insert(ctx.id); input_only_app_id_str = ctx.id; input_only_app_id = util::from_view(ctx.id); apps.emplace_back(std::move(ctx)); } // Terminate entry { proc::ctx_t ctx; ctx.idx = std::to_string(i); ctx.uuid = TERMINATE_APP_UUID; ctx.name = "Terminate"; ctx.image_path = parse_env_val(this_env, "terminate.png"); ctx.virtual_display = false; ctx.scale_factor = 100; ctx.use_app_identity = false; ctx.per_client_app_identity = false; ctx.allow_client_commands = false; ctx.terminate_on_pause = false; ctx.elevated = false; ctx.auto_detach = true; ctx.wait_all = true; ctx.exit_timeout = 5s; auto possible_ids = calculate_app_id(ctx.name, ctx.image_path, i++); if (ids.count(std::get<0>(possible_ids)) == 0) { // Avoid using index to generate id if possible ctx.id = std::get<0>(possible_ids); } else { // Fallback to include index on collision ctx.id = std::get<1>(possible_ids); } // ids.insert(ctx.id); terminate_app_id_str = ctx.id; terminate_app_id = util::from_view(ctx.id); apps.emplace_back(std::move(ctx)); } } return proc::proc_t { std::move(this_env), std::move(apps) }; } void refresh(const std::string &file_name, bool needs_terminate) { if (needs_terminate) { proc.terminate(false, false); } #ifdef _WIN32 size_t fail_count = 0; while (fail_count < 5 && vDisplayDriverStatus != VDISPLAY::DRIVER_STATUS::OK) { initVDisplayDriver(); if (vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK) { break; } fail_count += 1; std::this_thread::sleep_for(1s); } #endif auto proc_opt = proc::parse(file_name); if (proc_opt) { proc = std::move(*proc_opt); } } } // namespace proc ================================================ FILE: src/process.h ================================================ /** * @file src/process.h * @brief Declarations for the startup and shutdown of the apps started by a streaming Session. */ #pragma once #ifndef __kernel_entry #define __kernel_entry #endif #ifndef BOOST_PROCESS_VERSION #define BOOST_PROCESS_VERSION 1 #endif // standard includes #include #include // lib includes #include #include #include #include #include #include // local includes #include "config.h" #include "platform/common.h" #include "rtsp.h" #include "utility.h" #ifdef _WIN32 #include "platform/windows/virtual_display.h" #endif #define VIRTUAL_DISPLAY_UUID "8902CB19-674A-403D-A587-41B092E900BA" #define FALLBACK_DESKTOP_UUID "EAAC6159-089A-46A9-9E24-6436885F6610" #define REMOTE_INPUT_UUID "8CB5C136-DA67-4F99-B4A1-F9CD35005CF4" #define TERMINATE_APP_UUID "E16CBE1B-295D-4632-9A76-EC4180C857D3" namespace proc { using file_t = util::safe_ptr_v2; #ifdef _WIN32 extern VDISPLAY::DRIVER_STATUS vDisplayDriverStatus; #endif typedef config::prep_cmd_t cmd_t; /** * pre_cmds -- guaranteed to be executed unless any of the commands fail. * detached -- commands detached from Sunshine * cmd -- Runs indefinitely until: * No session is running and a different set of commands it to be executed * Command exits * working_dir -- the process working directory. This is required for some games to run properly. * cmd_output -- * empty -- The output of the commands are appended to the output of sunshine * "null" -- The output of the commands are discarded * filename -- The output of the commands are appended to filename */ struct ctx_t { std::vector prep_cmds; std::vector state_cmds; /** * Some applications, such as Steam, either exit quickly, or keep running indefinitely. * * Apps that launch normal child processes and terminate will be handled by the process * grouping logic (wait_all). However, apps that launch child processes indirectly or * into another process group (such as UWP apps) can only be handled by the auto-detach * heuristic which catches processes that exit 0 very quickly, but we won't have proper * process tracking for those. * * For cases where users just want to kick off a background process and never manage the * lifetime of that process, they can use detached commands for that. */ std::vector detached; std::string idx; std::string uuid; std::string name; std::string cmd; std::string working_dir; std::string output; std::string image_path; std::string id; std::string gamepad; bool elevated; bool auto_detach; bool wait_all; bool virtual_display; bool virtual_display_primary; bool use_app_identity; bool per_client_app_identity; bool allow_client_commands; bool terminate_on_pause; int scale_factor; std::chrono::seconds exit_timeout; }; class proc_t { public: KITTY_DEFAULT_CONSTR_MOVE_THROW(proc_t) std::string display_name; std::string initial_display; std::string mode_changed_display; bool initial_hdr = false; bool virtual_display = false; bool allow_client_commands = false; proc_t( boost::process::v1::environment &&env, std::vector &&apps ): _env(std::move(env)), _apps(std::move(apps)) { } void launch_input_only(); int execute(const ctx_t& _app, std::shared_ptr launch_session); /** * @return `_app_id` if a process is running, otherwise returns `0` */ int running(); ~proc_t(); const std::vector &get_apps() const; std::vector &get_apps(); std::string get_app_image(int app_id); std::string get_last_run_app_name(); std::string get_running_app_uuid(); boost::process::v1::environment get_env(); void resume(); void pause(); void terminate(bool immediate = false, bool needs_refresh = true); private: int _app_id = 0; std::string _app_name; boost::process::v1::environment _env; std::shared_ptr _launch_session; std::shared_ptr _saved_input_config; std::vector _apps; ctx_t _app; std::chrono::steady_clock::time_point _app_launch_time; // If no command associated with _app_id, yet it's still running bool placebo {}; boost::process::v1::child _process; boost::process::v1::group _process_group; file_t _pipe; std::vector::const_iterator _app_prep_it; std::vector::const_iterator _app_prep_begin; }; boost::filesystem::path find_working_directory(const std::string &cmd, const boost::process::v1::environment &env); /** * @brief Calculate a stable id based on name and image data * @return Tuple of id calculated without index (for use if no collision) and one with. */ std::tuple calculate_app_id(const std::string &app_name, std::string app_image_path, int index); std::string validate_app_image_path(std::string app_image_path); void refresh(const std::string &file_name, bool needs_terminate = true); void migrate_apps(nlohmann::json* fileTree_p, nlohmann::json* inputTree_p); std::optional parse(const std::string &file_name); /** * @brief Initialize proc functions * @return Unique pointer to `deinit_t` to manage cleanup */ std::unique_ptr init(); /** * @brief Terminates all child processes in a process group. * @param proc The child process itself. * @param group The group of all children in the process tree. * @param exit_timeout The timeout to wait for the process group to gracefully exit. */ void terminate_process_group(boost::process::v1::child &proc, boost::process::v1::group &group, std::chrono::seconds exit_timeout); extern proc_t proc; extern int input_only_app_id; extern std::string input_only_app_id_str; extern int terminate_app_id; extern std::string terminate_app_id_str; } // namespace proc #ifdef BOOST_PROCESS_VERSION #undef BOOST_PROCESS_VERSION #endif ================================================ FILE: src/round_robin.h ================================================ /** * @file src/round_robin.h * @brief Declarations for a round-robin iterator. */ #pragma once // standard includes #include /** * @brief A round-robin iterator utility. * @tparam V The value type. * @tparam T The iterator type. */ namespace round_robin_util { template class it_wrap_t { public: using iterator_category = std::random_access_iterator_tag; using value_type = V; using difference_type = V; using pointer = V *; using const_pointer = V const *; using reference = V &; using const_reference = V const &; typedef T iterator; typedef std::ptrdiff_t diff_t; iterator operator+=(diff_t step) { while (step-- > 0) { ++_this(); } return _this(); } iterator operator-=(diff_t step) { while (step-- > 0) { --_this(); } return _this(); } iterator operator+(diff_t step) { iterator new_ = _this(); return new_ += step; } iterator operator-(diff_t step) { iterator new_ = _this(); return new_ -= step; } diff_t operator-(iterator first) { diff_t step = 0; while (first != _this()) { ++step; ++first; } return step; } iterator operator++() { _this().inc(); return _this(); } iterator operator--() { _this().dec(); return _this(); } iterator operator++(int) { iterator new_ = _this(); ++_this(); return new_; } iterator operator--(int) { iterator new_ = _this(); --_this(); return new_; } reference operator*() { return *_this().get(); } const_reference operator*() const { return *_this().get(); } pointer operator->() { return &*_this(); } const_pointer operator->() const { return &*_this(); } bool operator!=(const iterator &other) const { return !(_this() == other); } bool operator<(const iterator &other) const { return !(_this() >= other); } bool operator>=(const iterator &other) const { return _this() == other || _this() > other; } bool operator<=(const iterator &other) const { return _this() == other || _this() < other; } bool operator==(const iterator &other) const { return _this().eq(other); }; bool operator>(const iterator &other) const { return _this().gt(other); } private: iterator &_this() { return *static_cast(this); } const iterator &_this() const { return *static_cast(this); } }; template class round_robin_t: public it_wrap_t> { public: using iterator = It; using pointer = V *; round_robin_t(iterator begin, iterator end): _begin(begin), _end(end), _pos(begin) { } void inc() { ++_pos; if (_pos == _end) { _pos = _begin; } } void dec() { if (_pos == _begin) { _pos = _end; } --_pos; } bool eq(const round_robin_t &other) const { return *_pos == *other._pos; } pointer get() const { return &*_pos; } private: It _begin; It _end; It _pos; }; template round_robin_t make_round_robin(It begin, It end) { return round_robin_t(begin, end); } } // namespace round_robin_util ================================================ FILE: src/rswrapper.c ================================================ /** * @file src/rswrapper.c * @brief Wrappers for nanors vectorization with different ISA options */ // _FORTIY_SOURCE can cause some versions of GCC to try to inline // memset() with incompatible target options when compiling rs.c #ifdef _FORTIFY_SOURCE #undef _FORTIFY_SOURCE #endif // The assert() function is decorated with __cold on macOS which // is incompatible with Clang's target multiversioning feature #ifndef NDEBUG #define NDEBUG #endif #define DECORATE_FUNC_I(a, b) a##b #define DECORATE_FUNC(a, b) DECORATE_FUNC_I(a, b) // Append an ISA suffix to the public RS API #define reed_solomon_init DECORATE_FUNC(reed_solomon_init, ISA_SUFFIX) #define reed_solomon_new DECORATE_FUNC(reed_solomon_new, ISA_SUFFIX) #define reed_solomon_new_static DECORATE_FUNC(reed_solomon_new_static, ISA_SUFFIX) #define reed_solomon_release DECORATE_FUNC(reed_solomon_release, ISA_SUFFIX) #define reed_solomon_decode DECORATE_FUNC(reed_solomon_decode, ISA_SUFFIX) #define reed_solomon_encode DECORATE_FUNC(reed_solomon_encode, ISA_SUFFIX) // Append an ISA suffix to internal functions to prevent multiple definition errors #define obl_axpy_ref DECORATE_FUNC(obl_axpy_ref, ISA_SUFFIX) #define obl_scal_ref DECORATE_FUNC(obl_scal_ref, ISA_SUFFIX) #define obl_axpyb32_ref DECORATE_FUNC(obl_axpyb32_ref, ISA_SUFFIX) #define obl_axpy DECORATE_FUNC(obl_axpy, ISA_SUFFIX) #define obl_scal DECORATE_FUNC(obl_scal, ISA_SUFFIX) #define obl_swap DECORATE_FUNC(obl_swap, ISA_SUFFIX) #define obl_axpyb32 DECORATE_FUNC(obl_axpyb32, ISA_SUFFIX) #define axpy DECORATE_FUNC(axpy, ISA_SUFFIX) #define scal DECORATE_FUNC(scal, ISA_SUFFIX) #define gemm DECORATE_FUNC(gemm, ISA_SUFFIX) #define invert_mat DECORATE_FUNC(invert_mat, ISA_SUFFIX) #if defined(__x86_64) || defined(__x86_64__) || defined(__amd64) || defined(__amd64__) || defined(_M_AMD64) // Compile a variant for SSSE3 #if defined(__clang__) #pragma clang attribute push(__attribute__((target("ssse3"))), apply_to = function) #else #pragma GCC push_options #pragma GCC target("ssse3") #endif #define ISA_SUFFIX _ssse3 #define OBLAS_SSE3 #include "../third-party/nanors/rs.c" #undef OBLAS_SSE3 #undef ISA_SUFFIX #if defined(__clang__) #pragma clang attribute pop #else #pragma GCC pop_options #endif // Compile a variant for AVX2 #if defined(__clang__) #pragma clang attribute push(__attribute__((target("avx2"))), apply_to = function) #else #pragma GCC push_options #pragma GCC target("avx2") #endif #define ISA_SUFFIX _avx2 #define OBLAS_AVX2 #include "../third-party/nanors/rs.c" #undef OBLAS_AVX2 #undef ISA_SUFFIX #if defined(__clang__) #pragma clang attribute pop #else #pragma GCC pop_options #endif // Compile a variant for AVX512BW #if defined(__clang__) #pragma clang attribute push(__attribute__((target("avx512f,avx512bw"))), apply_to = function) #else #pragma GCC push_options #pragma GCC target("avx512f,avx512bw") #endif #define ISA_SUFFIX _avx512 #define OBLAS_AVX512 #include "../third-party/nanors/rs.c" #undef OBLAS_AVX512 #undef ISA_SUFFIX #if defined(__clang__) #pragma clang attribute pop #else #pragma GCC pop_options #endif #endif // Compile a default variant #define ISA_SUFFIX _def #include "../third-party/nanors/deps/obl/autoshim.h" #include "../third-party/nanors/rs.c" #undef ISA_SUFFIX #undef reed_solomon_init #undef reed_solomon_new #undef reed_solomon_new_static #undef reed_solomon_release #undef reed_solomon_decode #undef reed_solomon_encode #include "rswrapper.h" reed_solomon_new_t reed_solomon_new_fn; reed_solomon_release_t reed_solomon_release_fn; reed_solomon_encode_t reed_solomon_encode_fn; reed_solomon_decode_t reed_solomon_decode_fn; /** * @brief This initializes the RS function pointers to the best vectorized version available. * @details The streaming code will directly invoke these function pointers during encoding. */ void reed_solomon_init(void) { #if defined(__x86_64) || defined(__x86_64__) || defined(__amd64) || defined(__amd64__) || defined(_M_AMD64) if (__builtin_cpu_supports("avx512f") && __builtin_cpu_supports("avx512bw")) { reed_solomon_new_fn = reed_solomon_new_avx512; reed_solomon_release_fn = reed_solomon_release_avx512; reed_solomon_encode_fn = reed_solomon_encode_avx512; reed_solomon_decode_fn = reed_solomon_decode_avx512; reed_solomon_init_avx512(); } else if (__builtin_cpu_supports("avx2")) { reed_solomon_new_fn = reed_solomon_new_avx2; reed_solomon_release_fn = reed_solomon_release_avx2; reed_solomon_encode_fn = reed_solomon_encode_avx2; reed_solomon_decode_fn = reed_solomon_decode_avx2; reed_solomon_init_avx2(); } else if (__builtin_cpu_supports("ssse3")) { reed_solomon_new_fn = reed_solomon_new_ssse3; reed_solomon_release_fn = reed_solomon_release_ssse3; reed_solomon_encode_fn = reed_solomon_encode_ssse3; reed_solomon_decode_fn = reed_solomon_decode_ssse3; reed_solomon_init_ssse3(); } else #endif { reed_solomon_new_fn = reed_solomon_new_def; reed_solomon_release_fn = reed_solomon_release_def; reed_solomon_encode_fn = reed_solomon_encode_def; reed_solomon_decode_fn = reed_solomon_decode_def; reed_solomon_init_def(); } } ================================================ FILE: src/rswrapper.h ================================================ /** * @file src/rswrapper.h * @brief Wrappers for nanors vectorization * @details This is a drop-in replacement for nanors rs.h */ #pragma once // standard includes #include typedef struct _reed_solomon reed_solomon; typedef reed_solomon *(*reed_solomon_new_t)(int data_shards, int parity_shards); typedef void (*reed_solomon_release_t)(reed_solomon *rs); typedef int (*reed_solomon_encode_t)(reed_solomon *rs, uint8_t **shards, int nr_shards, int bs); typedef int (*reed_solomon_decode_t)(reed_solomon *rs, uint8_t **shards, uint8_t *marks, int nr_shards, int bs); extern reed_solomon_new_t reed_solomon_new_fn; extern reed_solomon_release_t reed_solomon_release_fn; extern reed_solomon_encode_t reed_solomon_encode_fn; extern reed_solomon_decode_t reed_solomon_decode_fn; #define reed_solomon_new reed_solomon_new_fn #define reed_solomon_release reed_solomon_release_fn #define reed_solomon_encode reed_solomon_encode_fn #define reed_solomon_decode reed_solomon_decode_fn /** * @brief This initializes the RS function pointers to the best vectorized version available. * @details The streaming code will directly invoke these function pointers during encoding. */ void reed_solomon_init(void); ================================================ FILE: src/rtsp.cpp ================================================ /** * @file src/rtsp.cpp * @brief Definitions for RTSP streaming. */ #define BOOST_BIND_GLOBAL_PLACEHOLDERS extern "C" { #include #include } // standard includes #include #include #include #include #include #include // lib includes #include #include // local includes #include "config.h" #include "globals.h" #include "input.h" #include "logging.h" #include "network.h" #include "rtsp.h" #include "stream.h" #include "sync.h" #include "video.h" namespace asio = boost::asio; using asio::ip::tcp; using asio::ip::udp; using namespace std::literals; namespace rtsp_stream { void free_msg(PRTSP_MESSAGE msg) { freeMessage(msg); delete msg; } #pragma pack(push, 1) struct encrypted_rtsp_header_t { // We set the MSB in encrypted RTSP messages to allow format-agnostic // parsing code to be able to tell encrypted from plaintext messages. static constexpr std::uint32_t ENCRYPTED_MESSAGE_TYPE_BIT = 0x80000000; uint8_t *payload() { return (uint8_t *) (this + 1); } std::uint32_t payload_length() { return util::endian::big(typeAndLength) & ~ENCRYPTED_MESSAGE_TYPE_BIT; } bool is_encrypted() { return !!(util::endian::big(typeAndLength) & ENCRYPTED_MESSAGE_TYPE_BIT); } // This field is the length of the payload + ENCRYPTED_MESSAGE_TYPE_BIT in big-endian std::uint32_t typeAndLength; // This field is the number used to initialize the bottom 4 bytes of the AES IV in big-endian std::uint32_t sequenceNumber; // This field is the AES GCM authentication tag std::uint8_t tag[16]; }; #pragma pack(pop) class rtsp_server_t; using msg_t = util::safe_ptr; using cmd_func_t = std::function; void print_msg(PRTSP_MESSAGE msg); void cmd_not_found(tcp::socket &sock, launch_session_t &, msg_t &&req); void respond(tcp::socket &sock, launch_session_t &session, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload); class socket_t: public std::enable_shared_from_this { public: socket_t(boost::asio::io_context &io_context, std::function &&handle_data_fn): handle_data_fn {std::move(handle_data_fn)}, sock {io_context} { } /** * @brief Queue an asynchronous read to begin the next message. */ void read() { if (begin == std::end(msg_buf) || (session->rtsp_cipher && begin + sizeof(encrypted_rtsp_header_t) >= std::end(msg_buf))) { BOOST_LOG(error) << "RTSP: read(): Exceeded maximum rtsp packet size: "sv << msg_buf.size(); respond(sock, *session, nullptr, 400, "BAD REQUEST", 0, {}); boost::system::error_code ec; sock.close(ec); return; } if (session->rtsp_cipher) { // For encrypted RTSP, we will read the the entire header first 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)); } else { sock.async_read_some( boost::asio::buffer(begin, (std::size_t) (std::end(msg_buf) - begin)), boost::bind( &socket_t::handle_read_plaintext, shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred ) ); } } /** * @brief Handle the initial read of the header of an encrypted message. * @param socket The socket the message was received on. * @param ec The error code of the read operation. * @param bytes The number of bytes read. */ static void handle_read_encrypted_header(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { BOOST_LOG(debug) << "handle_read_encrypted_header(): Handle read of size: "sv << bytes << " bytes"sv; auto sock_close = util::fail_guard([&socket]() { boost::system::error_code ec; socket->sock.close(ec); if (ec) { BOOST_LOG(error) << "RTSP: handle_read_encrypted_header(): Couldn't close tcp socket: "sv << ec.message(); } }); if (ec || bytes < sizeof(encrypted_rtsp_header_t)) { BOOST_LOG(error) << "RTSP: handle_read_encrypted_header(): Couldn't read from tcp socket: "sv << ec.message(); respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); return; } auto header = (encrypted_rtsp_header_t *) socket->begin; if (!header->is_encrypted()) { BOOST_LOG(error) << "RTSP: handle_read_encrypted_header(): Rejecting unencrypted RTSP message"sv; respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); return; } auto payload_length = header->payload_length(); // Check if we have enough space to read this message if (socket->begin + sizeof(*header) + payload_length >= std::end(socket->msg_buf)) { BOOST_LOG(error) << "RTSP: handle_read_encrypted_header(): Exceeded maximum rtsp packet size: "sv << socket->msg_buf.size(); respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); return; } sock_close.disable(); // Read the remainder of the header and full encrypted payload 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)); } /** * @brief Handle the final read of the content of an encrypted message. * @param socket The socket the message was received on. * @param ec The error code of the read operation. * @param bytes The number of bytes read. */ static void handle_read_encrypted_message(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { BOOST_LOG(debug) << "handle_read_encrypted(): Handle read of size: "sv << bytes << " bytes"sv; auto sock_close = util::fail_guard([&socket]() { boost::system::error_code ec; socket->sock.close(ec); if (ec) { BOOST_LOG(error) << "RTSP: handle_read_encrypted_message(): Couldn't close tcp socket: "sv << ec.message(); } }); auto header = (encrypted_rtsp_header_t *) socket->begin; auto payload_length = header->payload_length(); auto seq = util::endian::big(header->sequenceNumber); if (ec || bytes < payload_length) { BOOST_LOG(error) << "RTSP: handle_read_encrypted(): Couldn't read from tcp socket: "sv << ec.message(); respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); return; } // We use the deterministic IV construction algorithm specified in NIST SP 800-38D // Section 8.2.1. The sequence number is our "invocation" field and the 'RC' in the // high bytes is the "fixed" field. Because each client provides their own unique // key, our values in the fixed field need only uniquely identify each independent // use of the client's key with AES-GCM in our code. // // The sequence number is 32 bits long which allows for 2^32 RTSP messages to be // received from each client before the IV repeats. crypto::aes_t iv(12); std::copy_n((uint8_t *) &seq, sizeof(seq), std::begin(iv)); iv[10] = 'C'; // Client originated iv[11] = 'R'; // RTSP std::vector plaintext; if (socket->session->rtsp_cipher->decrypt(std::string_view {(const char *) header->tag, sizeof(header->tag) + bytes}, plaintext, &iv)) { BOOST_LOG(error) << "Failed to verify RTSP message tag"sv; respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); return; } msg_t req {new msg_t::element_type {}}; if (auto status = parseRtspMessage(req.get(), (char *) plaintext.data(), plaintext.size())) { BOOST_LOG(error) << "Malformed RTSP message: ["sv << status << ']'; respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); return; } sock_close.disable(); print_msg(req.get()); socket->handle_data(std::move(req)); } /** * @brief Queue an asynchronous read of the payload portion of a plaintext message. */ void read_plaintext_payload() { if (begin == std::end(msg_buf)) { BOOST_LOG(error) << "RTSP: read_plaintext_payload(): Exceeded maximum rtsp packet size: "sv << msg_buf.size(); respond(sock, *session, nullptr, 400, "BAD REQUEST", 0, {}); boost::system::error_code ec; sock.close(ec); return; } sock.async_read_some( boost::asio::buffer(begin, (std::size_t) (std::end(msg_buf) - begin)), boost::bind( &socket_t::handle_plaintext_payload, shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred ) ); } /** * @brief Handle the read of the payload portion of a plaintext message. * @param socket The socket the message was received on. * @param ec The error code of the read operation. * @param bytes The number of bytes read. */ static void handle_plaintext_payload(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { BOOST_LOG(debug) << "handle_plaintext_payload(): Handle read of size: "sv << bytes << " bytes"sv; auto sock_close = util::fail_guard([&socket]() { boost::system::error_code ec; socket->sock.close(ec); if (ec) { BOOST_LOG(error) << "RTSP: handle_plaintext_payload(): Couldn't close tcp socket: "sv << ec.message(); } }); if (ec) { BOOST_LOG(error) << "RTSP: handle_plaintext_payload(): Couldn't read from tcp socket: "sv << ec.message(); return; } auto end = socket->begin + bytes; msg_t req {new msg_t::element_type {}}; if (auto status = parseRtspMessage(req.get(), socket->msg_buf.data(), (std::size_t) (end - socket->msg_buf.data()))) { BOOST_LOG(error) << "Malformed RTSP message: ["sv << status << ']'; respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); return; } sock_close.disable(); auto fg = util::fail_guard([&socket]() { socket->read_plaintext_payload(); }); auto content_length = 0; for (auto option = req->options; option != nullptr; option = option->next) { if ("Content-length"sv == option->option) { BOOST_LOG(debug) << "Found Content-Length: "sv << option->content << " bytes"sv; // If content_length > bytes read, then we need to store current data read, // to be appended by the next read. std::string_view content {option->content}; auto begin = std::find_if(std::begin(content), std::end(content), [](auto ch) { return (bool) std::isdigit(ch); }); content_length = util::from_chars(begin, std::end(content)); break; } } if (end - socket->crlf >= content_length) { if (end - socket->crlf > content_length) { BOOST_LOG(warning) << "(end - socket->crlf) > content_length -- "sv << (std::size_t) (end - socket->crlf) << " > "sv << content_length; } fg.disable(); print_msg(req.get()); socket->handle_data(std::move(req)); } socket->begin = end; } /** * @brief Handle the read of the header portion of a plaintext message. * @param socket The socket the message was received on. * @param ec The error code of the read operation. * @param bytes The number of bytes read. */ static void handle_read_plaintext(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { BOOST_LOG(debug) << "handle_read_plaintext(): Handle read of size: "sv << bytes << " bytes"sv; if (ec) { BOOST_LOG(error) << "RTSP: handle_read_plaintext(): Couldn't read from tcp socket: "sv << ec.message(); boost::system::error_code ec; socket->sock.close(ec); if (ec) { BOOST_LOG(error) << "RTSP: handle_read_plaintext(): Couldn't close tcp socket: "sv << ec.message(); } return; } auto fg = util::fail_guard([&socket]() { socket->read(); }); auto begin = std::max(socket->begin - 4, socket->begin); auto buf_size = bytes + (begin - socket->begin); auto end = begin + buf_size; constexpr auto needle = "\r\n\r\n"sv; auto it = std::search(begin, begin + buf_size, std::begin(needle), std::end(needle)); if (it == end) { socket->begin = end; return; } // Emulate read completion for payload data socket->begin = it + needle.size(); socket->crlf = socket->begin; buf_size = end - socket->begin; fg.disable(); handle_plaintext_payload(socket, ec, buf_size); } void handle_data(msg_t &&req) { handle_data_fn(sock, *session, std::move(req)); } std::function handle_data_fn; tcp::socket sock; std::array msg_buf; char *crlf; char *begin = msg_buf.data(); std::shared_ptr session; }; class rtsp_server_t { public: ~rtsp_server_t() { clear(); } int bind(net::af_e af, std::uint16_t port, boost::system::error_code &ec) { acceptor.open(af == net::IPV4 ? tcp::v4() : tcp::v6(), ec); if (ec) { return -1; } acceptor.set_option(boost::asio::socket_base::reuse_address {true}); acceptor.bind(tcp::endpoint(af == net::IPV4 ? tcp::v4() : tcp::v6(), port), ec); if (ec) { return -1; } acceptor.listen(4096, ec); if (ec) { return -1; } next_socket = std::make_shared(io_context, [this](tcp::socket &sock, launch_session_t &session, msg_t &&msg) { handle_msg(sock, session, std::move(msg)); }); acceptor.async_accept(next_socket->sock, [this](const auto &ec) { handle_accept(ec); }); return 0; } void handle_msg(tcp::socket &sock, launch_session_t &session, msg_t &&req) { auto func = _map_cmd_cb.find(req->message.request.command); if (func != std::end(_map_cmd_cb)) { func->second(this, sock, session, std::move(req)); } else { cmd_not_found(sock, session, std::move(req)); } boost::system::error_code ec; sock.shutdown(boost::asio::socket_base::shutdown_type::shutdown_both, ec); } void handle_accept(const boost::system::error_code &ec) { if (ec) { BOOST_LOG(error) << "Couldn't accept incoming connections: "sv << ec.message(); // Stop server clear(); return; } auto socket = std::move(next_socket); auto launch_session {launch_event.view(0s)}; if (launch_session) { // Associate the current RTSP session with this socket and start reading socket->session = launch_session; socket->read(); } else { // This can happen due to normal things like port scanning, so let's not make these visible by default BOOST_LOG(debug) << "No pending session for incoming RTSP connection"sv; // If there is no session pending, close the connection immediately boost::system::error_code ec; socket->sock.close(ec); } // Queue another asynchronous accept for the next incoming connection next_socket = std::make_shared(io_context, [this](tcp::socket &sock, launch_session_t &session, msg_t &&msg) { handle_msg(sock, session, std::move(msg)); }); acceptor.async_accept(next_socket->sock, [this](const auto &ec) { handle_accept(ec); }); } void map(const std::string_view &type, cmd_func_t cb) { _map_cmd_cb.emplace(type, std::move(cb)); } /** * @brief Launch a new streaming session. * @note If the client does not begin streaming within the ping_timeout, * the session will be discarded. * @param launch_session Streaming session information. */ void session_raise(std::shared_ptr launch_session) { // If a launch event is still pending, don't overwrite it. if (launch_event.view(0s)) { return; } // Raise the new launch session to prepare for the RTSP handshake launch_event.raise(std::move(launch_session)); // Arm the timer to expire this launch session if the client times out raised_timer.expires_after(config::stream.ping_timeout); raised_timer.async_wait([this](const boost::system::error_code &ec) { if (!ec) { auto discarded = launch_event.pop(0s); if (discarded) { BOOST_LOG(debug) << "Event timeout: "sv << discarded->unique_id; } } }); } /** * @brief Clear state for the oldest launch session. * @param launch_session_id The ID of the session to clear. */ void session_clear(uint32_t launch_session_id) { // We currently only support a single pending RTSP session, // so the ID should always match the one for that session. auto launch_session = launch_event.view(0s); if (launch_session) { if (launch_session->id != launch_session_id) { BOOST_LOG(error) << "Attempted to clear unexpected session: "sv << launch_session_id << " vs "sv << launch_session->id; } else { raised_timer.cancel(); launch_event.pop(); } } } /** * @brief Get the number of active sessions. * @return Count of active sessions. */ int session_count() { auto lg = _session_slots.lock(); return _session_slots->size(); } safe::event_t> launch_event; /** * @brief Clear launch sessions. * @param all If true, clear all sessions. Otherwise, only clear timed out and stopped sessions. * @examples * clear(false); * @examples_end */ void clear(bool all = true) { auto lg = _session_slots.lock(); for (auto i = _session_slots->begin(); i != _session_slots->end();) { auto &slot = *(*i); if (all || stream::session::state(slot) == stream::session::state_e::STOPPING) { stream::session::stop(slot); stream::session::join(slot); i = _session_slots->erase(i); } else { i++; } } } /** * @brief Removes the provided session from the set of sessions. * @param session The session to remove. */ void remove(const std::shared_ptr &session) { auto lg = _session_slots.lock(); _session_slots->erase(session); } /** * @brief Inserts the provided session into the set of sessions. * @param session The session to insert. */ void insert(const std::shared_ptr &session) { auto lg = _session_slots.lock(); _session_slots->emplace(session); BOOST_LOG(info) << "New streaming session started [active sessions: "sv << _session_slots->size() << ']'; } /** * @brief Runs an iteration of the RTSP server loop */ void iterate() { // If we have a session, we will return to the server loop every // 500ms to allow session cleanup to happen. if (session_count() > 0) { io_context.run_one_for(500ms); } else { io_context.run_one(); } } /** * @brief Stop the RTSP server. */ void stop() { acceptor.close(); io_context.stop(); clear(); } std::shared_ptr find_session(const std::string_view& uuid) { auto lg = _session_slots.lock(); for (auto &slot : *_session_slots) { if (slot && stream::session::uuid_match(*slot, uuid)) { return slot; } } return nullptr; } std::list get_all_session_uuids() { std::list uuids; auto lg = _session_slots.lock(); for (auto &slot : *_session_slots) { if (slot) { uuids.push_back(stream::session::uuid(*slot)); } } return uuids; } private: std::unordered_map _map_cmd_cb; sync_util::sync_t>> _session_slots; boost::asio::io_context io_context; tcp::acceptor acceptor {io_context}; boost::asio::steady_timer raised_timer {io_context}; std::shared_ptr next_socket; }; rtsp_server_t server {}; void launch_session_raise(std::shared_ptr launch_session) { server.session_raise(std::move(launch_session)); } void launch_session_clear(uint32_t launch_session_id) { server.session_clear(launch_session_id); } int session_count() { // Ensure session_count is up-to-date server.clear(false); return server.session_count(); } std::shared_ptr find_session(const std::string_view& uuid) { return server.find_session(uuid); } std::list get_all_session_uuids() { return server.get_all_session_uuids(); } void terminate_sessions() { server.clear(true); } int send(tcp::socket &sock, const std::string_view &sv) { std::size_t bytes_send = 0; while (bytes_send != sv.size()) { boost::system::error_code ec; bytes_send += sock.send(boost::asio::buffer(sv.substr(bytes_send)), 0, ec); if (ec) { BOOST_LOG(error) << "RTSP: Couldn't send data over tcp socket: "sv << ec.message(); return -1; } } return 0; } void respond(tcp::socket &sock, launch_session_t &session, msg_t &resp) { auto payload = std::make_pair(resp->payload, resp->payloadLength); // Restore response message for proper destruction auto lg = util::fail_guard([&]() { resp->payload = payload.first; resp->payloadLength = payload.second; }); resp->payload = nullptr; resp->payloadLength = 0; int serialized_len; util::c_ptr raw_resp {serializeRtspMessage(resp.get(), &serialized_len)}; BOOST_LOG(debug) << "---Begin Response---"sv << std::endl << std::string_view {raw_resp.get(), (std::size_t) serialized_len} << std::endl << std::string_view {payload.first, (std::size_t) payload.second} << std::endl << "---End Response---"sv << std::endl; // Encrypt the RTSP message if encryption is enabled if (session.rtsp_cipher) { // We use the deterministic IV construction algorithm specified in NIST SP 800-38D // Section 8.2.1. The sequence number is our "invocation" field and the 'RH' in the // high bytes is the "fixed" field. Because each client provides their own unique // key, our values in the fixed field need only uniquely identify each independent // use of the client's key with AES-GCM in our code. // // The sequence number is 32 bits long which allows for 2^32 RTSP messages to be // sent to each client before the IV repeats. crypto::aes_t iv(12); session.rtsp_iv_counter++; std::copy_n((uint8_t *) &session.rtsp_iv_counter, sizeof(session.rtsp_iv_counter), std::begin(iv)); iv[10] = 'H'; // Host originated iv[11] = 'R'; // RTSP // Allocate the message with an empty header and reserved space for the payload auto payload_length = serialized_len + payload.second; std::vector message(sizeof(encrypted_rtsp_header_t)); message.reserve(message.size() + payload_length); // Copy the complete plaintext into the message std::copy_n(raw_resp.get(), serialized_len, std::back_inserter(message)); std::copy_n(payload.first, payload.second, std::back_inserter(message)); // Initialize the message header auto header = (encrypted_rtsp_header_t *) message.data(); header->typeAndLength = util::endian::big(encrypted_rtsp_header_t::ENCRYPTED_MESSAGE_TYPE_BIT + payload_length); header->sequenceNumber = util::endian::big(session.rtsp_iv_counter); // Encrypt the RTSP message in place session.rtsp_cipher->encrypt(std::string_view {(const char *) header->payload(), (std::size_t) payload_length}, header->tag, &iv); // Send the full encrypted message send(sock, std::string_view {(char *) message.data(), message.size()}); } else { std::string_view tmp_resp {raw_resp.get(), (size_t) serialized_len}; // Send the plaintext RTSP message header if (send(sock, tmp_resp)) { return; } // Send the plaintext RTSP message payload (if present) send(sock, std::string_view {payload.first, (std::size_t) payload.second}); } } void respond(tcp::socket &sock, launch_session_t &session, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload) { msg_t resp {new msg_t::element_type}; createRtspResponse(resp.get(), nullptr, 0, const_cast("RTSP/1.0"), statuscode, const_cast(status_msg), seqn, options, const_cast(payload.data()), (int) payload.size()); respond(sock, session, resp); } void cmd_not_found(tcp::socket &sock, launch_session_t &session, msg_t &&req) { respond(sock, session, nullptr, 404, "NOT FOUND", req->sequenceNumber, {}); } void cmd_option(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified option.option = const_cast("CSeq"); auto seqn_str = std::to_string(req->sequenceNumber); option.content = const_cast(seqn_str.c_str()); respond(sock, session, &option, 200, "OK", req->sequenceNumber, {}); } void cmd_describe(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified option.option = const_cast("CSeq"); auto seqn_str = std::to_string(req->sequenceNumber); option.content = const_cast(seqn_str.c_str()); std::stringstream ss; // Tell the client about our supported features ss << "a=x-ss-general.featureFlags:" << (uint32_t) platf::get_capabilities() << std::endl; // Always request new control stream encryption if the client supports it uint32_t encryption_flags_supported = SS_ENC_CONTROL_V2 | SS_ENC_AUDIO; uint32_t encryption_flags_requested = SS_ENC_CONTROL_V2; // Determine the encryption desired for this remote endpoint auto encryption_mode = net::encryption_mode_for_address(sock.remote_endpoint().address()); if (encryption_mode != config::ENCRYPTION_MODE_NEVER) { // Advertise support for video encryption if it's not disabled encryption_flags_supported |= SS_ENC_VIDEO; // If it's mandatory, also request it to enable use if the client // didn't explicitly opt in, but it otherwise has support. if (encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { encryption_flags_requested |= SS_ENC_VIDEO | SS_ENC_AUDIO; } } // Report supported and required encryption flags ss << "a=x-ss-general.encryptionSupported:" << encryption_flags_supported << std::endl; ss << "a=x-ss-general.encryptionRequested:" << encryption_flags_requested << std::endl; if (video::last_encoder_probe_supported_ref_frames_invalidation) { ss << "a=x-nv-video[0].refPicInvalidation:1"sv << std::endl; } if (video::active_hevc_mode != 1) { ss << "sprop-parameter-sets=AAAAAU"sv << std::endl; } if (video::active_av1_mode != 1) { ss << "a=rtpmap:98 AV1/90000"sv << std::endl; } if (!session.surround_params.empty()) { // If we have our own surround parameters, advertise them twice first ss << "a=fmtp:97 surround-params="sv << session.surround_params << std::endl; ss << "a=fmtp:97 surround-params="sv << session.surround_params << std::endl; } for (int x = 0; x < audio::MAX_STREAM_CONFIG; ++x) { auto &stream_config = audio::stream_configs[x]; std::uint8_t mapping[platf::speaker::MAX_SPEAKERS]; auto mapping_p = stream_config.mapping; /** * GFE advertises incorrect mapping for normal quality configurations, * as a result, Moonlight rotates all channels from index '3' to the right * To work around this, rotate channels to the left from index '3' */ if (x == audio::SURROUND51 || x == audio::SURROUND71) { std::copy_n(mapping_p, stream_config.channelCount, mapping); std::rotate(mapping + 3, mapping + 4, mapping + audio::MAX_STREAM_CONFIG); mapping_p = mapping; } ss << "a=fmtp:97 surround-params="sv << stream_config.channelCount << stream_config.streams << stream_config.coupledStreams; std::for_each_n(mapping_p, stream_config.channelCount, [&ss](std::uint8_t digit) { ss << (char) (digit + '0'); }); ss << std::endl; } respond(sock, session, &option, 200, "OK", req->sequenceNumber, ss.str()); } void cmd_setup(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM options[4] {}; auto &seqn = options[0]; auto &session_option = options[1]; auto &port_option = options[2]; auto &payload_option = options[3]; seqn.option = const_cast("CSeq"); auto seqn_str = std::to_string(req->sequenceNumber); seqn.content = const_cast(seqn_str.c_str()); std::string_view target {req->message.request.target}; auto begin = std::find(std::begin(target), std::end(target), '=') + 1; auto end = std::find(begin, std::end(target), '/'); std::string_view type {begin, (size_t) std::distance(begin, end)}; std::uint16_t port; if (type == "audio"sv) { port = net::map_port(stream::AUDIO_STREAM_PORT); } else if (type == "video"sv) { port = net::map_port(stream::VIDEO_STREAM_PORT); } else if (type == "control"sv) { port = net::map_port(stream::CONTROL_PORT); } else { cmd_not_found(sock, session, std::move(req)); return; } seqn.next = &session_option; session_option.option = const_cast("Session"); session_option.content = const_cast("DEADBEEFCAFE;timeout = 90"); session_option.next = &port_option; // Moonlight merely requires 'server_port=' auto port_value = std::format("server_port={}", static_cast(port)); port_option.option = const_cast("Transport"); port_option.content = port_value.data(); // Send identifiers that will be echoed in the other connections auto connect_data = std::to_string(session.control_connect_data); if (type == "control"sv) { payload_option.option = const_cast("X-SS-Connect-Data"); payload_option.content = connect_data.data(); } else { payload_option.option = const_cast("X-SS-Ping-Payload"); payload_option.content = session.av_ping_payload.data(); } port_option.next = &payload_option; respond(sock, session, &seqn, 200, "OK", req->sequenceNumber, {}); } void cmd_announce(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified option.option = const_cast("CSeq"); auto seqn_str = std::to_string(req->sequenceNumber); option.content = const_cast(seqn_str.c_str()); std::string_view payload {req->payload, (size_t) req->payloadLength}; std::vector lines; auto whitespace = [](char ch) { return ch == '\n' || ch == '\r'; }; { auto pos = std::begin(payload); auto begin = pos; while (pos != std::end(payload)) { if (whitespace(*pos++)) { lines.emplace_back(begin, pos - begin - 1); while (pos != std::end(payload) && whitespace(*pos)) { ++pos; } begin = pos; } } } std::string_view client; std::unordered_map args; for (auto line : lines) { auto type = line.substr(0, 2); if (type == "s="sv) { client = line.substr(2); } else if (type == "a=") { auto pos = line.find(':'); auto name = line.substr(2, pos - 2); auto val = line.substr(pos + 1); if (val[val.size() - 1] == ' ') { val = val.substr(0, val.size() - 1); } args.emplace(name, val); } } // Initialize any omitted parameters to defaults args.try_emplace("x-nv-video[0].encoderCscMode"sv, "0"sv); args.try_emplace("x-nv-vqos[0].bitStreamFormat"sv, "0"sv); args.try_emplace("x-nv-video[0].dynamicRangeMode"sv, "0"sv); args.try_emplace("x-nv-aqos.packetDuration"sv, "5"sv); args.try_emplace("x-nv-general.useReliableUdp"sv, "1"sv); args.try_emplace("x-nv-vqos[0].fec.minRequiredFecPackets"sv, "0"sv); args.try_emplace("x-nv-general.featureFlags"sv, "135"sv); args.try_emplace("x-ml-general.featureFlags"sv, "0"sv); args.try_emplace("x-nv-vqos[0].qosTrafficType"sv, "5"sv); args.try_emplace("x-nv-aqos.qosTrafficType"sv, "4"sv); args.try_emplace("x-ml-video.configuredBitrateKbps"sv, "0"sv); args.try_emplace("x-ss-general.encryptionEnabled"sv, "0"sv); args.try_emplace("x-ss-video[0].chromaSamplingType"sv, "0"sv); args.try_emplace("x-ss-video[0].intraRefresh"sv, "0"sv); stream::config_t config; std::int64_t configuredBitrateKbps; config.audio.flags[audio::config_t::HOST_AUDIO] = session.host_audio; try { config.audio.channels = util::from_view(args.at("x-nv-audio.surround.numChannels"sv)); config.audio.mask = util::from_view(args.at("x-nv-audio.surround.channelMask"sv)); config.audio.packetDuration = util::from_view(args.at("x-nv-aqos.packetDuration"sv)); config.audio.flags[audio::config_t::HIGH_QUALITY] = util::from_view(args.at("x-nv-audio.surround.AudioQuality"sv)); config.controlProtocolType = util::from_view(args.at("x-nv-general.useReliableUdp"sv)); config.packetsize = util::from_view(args.at("x-nv-video[0].packetSize"sv)); config.minRequiredFecPackets = util::from_view(args.at("x-nv-vqos[0].fec.minRequiredFecPackets"sv)); config.mlFeatureFlags = util::from_view(args.at("x-ml-general.featureFlags"sv)); config.audioQosType = util::from_view(args.at("x-nv-aqos.qosTrafficType"sv)); config.videoQosType = util::from_view(args.at("x-nv-vqos[0].qosTrafficType"sv)); config.encryptionFlagsEnabled = util::from_view(args.at("x-ss-general.encryptionEnabled"sv)); // Legacy clients use nvFeatureFlags to indicate support for audio encryption if (util::from_view(args.at("x-nv-general.featureFlags"sv)) & 0x20) { config.encryptionFlagsEnabled |= SS_ENC_AUDIO; } config.monitor.height = util::from_view(args.at("x-nv-video[0].clientViewportHt"sv)); config.monitor.width = util::from_view(args.at("x-nv-video[0].clientViewportWd"sv)); config.monitor.framerate = util::from_view(args.at("x-nv-video[0].maxFPS"sv)); config.monitor.bitrate = util::from_view(args.at("x-nv-vqos[0].bw.maximumBitrateKbps"sv)); config.monitor.slicesPerFrame = util::from_view(args.at("x-nv-video[0].videoEncoderSlicesPerFrame"sv)); config.monitor.numRefFrames = util::from_view(args.at("x-nv-video[0].maxNumReferenceFrames"sv)); config.monitor.encoderCscMode = util::from_view(args.at("x-nv-video[0].encoderCscMode"sv)); config.monitor.videoFormat = util::from_view(args.at("x-nv-vqos[0].bitStreamFormat"sv)); config.monitor.dynamicRange = util::from_view(args.at("x-nv-video[0].dynamicRangeMode"sv)); config.monitor.chromaSamplingType = util::from_view(args.at("x-ss-video[0].chromaSamplingType"sv)); config.monitor.enableIntraRefresh = util::from_view(args.at("x-ss-video[0].intraRefresh"sv)); if (config::video.limit_framerate) { config.monitor.encodingFramerate = session.fps; } else { if (config.monitor.framerate > 1000) { config.monitor.encodingFramerate = config.monitor.framerate; } else { config.monitor.encodingFramerate = config.monitor.framerate * 1000; } } // When fractional refresh rate requested from client side, it should be well above 1000fps // 4000fps is when Warp2 Mode is enabled on the client, requested framerate can be actual * 4 if (config.monitor.framerate > 4000) { config.monitor.framerate = std::round((float)config.monitor.framerate / 1000); } config.monitor.input_only = session.input_only; configuredBitrateKbps = util::from_view(args.at("x-ml-video.configuredBitrateKbps"sv)); if (!configuredBitrateKbps) { configuredBitrateKbps = config.monitor.bitrate; } BOOST_LOG(info) << "Client Requested bitrate is [" << configuredBitrateKbps << "kbps]"; if (config::video.max_bitrate > 0) { if (config::video.max_bitrate < configuredBitrateKbps) { configuredBitrateKbps = config::video.max_bitrate; } } BOOST_LOG(info) << "Host Streaming bitrate is [" << configuredBitrateKbps << "kbps]"; // Hack: Restore bitrate for warp mode size_t warp_factor = std::round((float)config.monitor.framerate * 1000 / session.fps); if (config::video.limit_framerate && warp_factor >= 2) { configuredBitrateKbps *= warp_factor; BOOST_LOG(info) << "Warp factor [" << warp_factor << "] engaged"; } } catch (std::out_of_range &) { respond(sock, session, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); return; } // When using stereo audio, the audio quality is (strangely) indicated by whether the Host field // in the RTSP message matches a local interface's IP address. Fortunately, Moonlight always sends // 0.0.0.0 when it wants low quality, so it is easy to check without enumerating interfaces. if (config.audio.channels == 2) { for (auto option = req->options; option != nullptr; option = option->next) { if ("Host"sv == option->option) { std::string_view content {option->content}; BOOST_LOG(debug) << "Found Host: "sv << content; config.audio.flags[audio::config_t::HIGH_QUALITY] = (content.find("0.0.0.0"sv) == std::string::npos); } } } else if (session.surround_params.length() > 3) { // Channels std::uint8_t c = session.surround_params[0] - '0'; // Streams std::uint8_t n = session.surround_params[1] - '0'; // Coupled streams std::uint8_t m = session.surround_params[2] - '0'; auto valid = false; if ((c == 6 || c == 8) && c == config.audio.channels && n + m == c && session.surround_params.length() == c + 3) { config.audio.customStreamParams.channelCount = c; config.audio.customStreamParams.streams = n; config.audio.customStreamParams.coupledStreams = m; valid = true; for (std::uint8_t i = 0; i < c; i++) { config.audio.customStreamParams.mapping[i] = session.surround_params[i + 3] - '0'; if (config.audio.customStreamParams.mapping[i] >= c) { valid = false; break; } } } config.audio.flags[audio::config_t::CUSTOM_SURROUND_PARAMS] = valid; } config.audio.input_only = session.input_only; // If the client sent a configured bitrate, we will choose the actual bitrate ourselves // by using FEC percentage and audio quality settings. If the calculated bitrate ends up // too low, we'll allow it to exceed the limits rather than reducing the encoding bitrate // down to nearly nothing. if (configuredBitrateKbps) { BOOST_LOG(debug) << "Client configured bitrate is "sv << configuredBitrateKbps << " Kbps"sv; // If the FEC percentage isn't too high, adjust the configured bitrate to ensure video // traffic doesn't exceed the user's selected bitrate when the FEC shards are included. if (config::stream.fec_percentage <= 80) { configuredBitrateKbps /= 100.f / (100 - config::stream.fec_percentage); } // Adjust the bitrate to account for audio traffic bandwidth usage (capped at 20% reduction). // The bitrate per channel is 256 Kbps for high quality mode and 96 Kbps for normal quality. auto audioBitrateAdjustment = (config.audio.flags[audio::config_t::HIGH_QUALITY] ? 256 : 96) * config.audio.channels; configuredBitrateKbps -= std::min((std::int64_t) audioBitrateAdjustment, configuredBitrateKbps / 5); // Reduce it by another 500Kbps to account for A/V packet overhead and control data // traffic (capped at 10% reduction). configuredBitrateKbps -= std::min((std::int64_t) 500, configuredBitrateKbps / 10); BOOST_LOG(debug) << "Final adjusted video encoding bitrate is "sv << configuredBitrateKbps << " Kbps"sv; config.monitor.bitrate = configuredBitrateKbps; } if (config.monitor.videoFormat == 1 && video::active_hevc_mode == 1) { BOOST_LOG(warning) << "HEVC is disabled, yet the client requested HEVC"sv; respond(sock, session, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); return; } if (config.monitor.videoFormat == 2 && video::active_av1_mode == 1) { BOOST_LOG(warning) << "AV1 is disabled, yet the client requested AV1"sv; respond(sock, session, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); return; } // Check that any required encryption is enabled auto encryption_mode = net::encryption_mode_for_address(sock.remote_endpoint().address()); if (encryption_mode == config::ENCRYPTION_MODE_MANDATORY && (config.encryptionFlagsEnabled & (SS_ENC_VIDEO | SS_ENC_AUDIO)) != (SS_ENC_VIDEO | SS_ENC_AUDIO)) { BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv; respond(sock, session, &option, 403, "Forbidden", req->sequenceNumber, {}); return; } auto stream_session = stream::session::alloc(config, session); server->insert(stream_session); if (stream::session::start(*stream_session, sock.remote_endpoint().address().to_string())) { BOOST_LOG(error) << "Failed to start a streaming session"sv; server->remove(stream_session); respond(sock, session, &option, 500, "Internal Server Error", req->sequenceNumber, {}); return; } respond(sock, session, &option, 200, "OK", req->sequenceNumber, {}); } void cmd_play(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified option.option = const_cast("CSeq"); auto seqn_str = std::to_string(req->sequenceNumber); option.content = const_cast(seqn_str.c_str()); respond(sock, session, &option, 200, "OK", req->sequenceNumber, {}); } void start() { auto shutdown_event = mail::man->event(mail::shutdown); server.map("OPTIONS"sv, &cmd_option); server.map("DESCRIBE"sv, &cmd_describe); server.map("SETUP"sv, &cmd_setup); server.map("ANNOUNCE"sv, &cmd_announce); server.map("PLAY"sv, &cmd_play); boost::system::error_code ec; if (server.bind(net::af_from_enum_string(config::sunshine.address_family), net::map_port(rtsp_stream::RTSP_SETUP_PORT), ec)) { BOOST_LOG(fatal) << "Couldn't bind RTSP server to port ["sv << net::map_port(rtsp_stream::RTSP_SETUP_PORT) << "], " << ec.message(); shutdown_event->raise(true); return; } std::thread rtsp_thread {[&shutdown_event] { auto broadcast_shutdown_event = mail::man->event(mail::broadcast_shutdown); while (!shutdown_event->peek()) { server.iterate(); if (broadcast_shutdown_event->peek()) { server.clear(); } else { // cleanup all stopped sessions server.clear(false); } } server.clear(); }}; // Wait for shutdown shutdown_event->view(); // Stop the server and join the server thread server.stop(); rtsp_thread.join(); } void print_msg(PRTSP_MESSAGE msg) { std::string_view type = msg->type == TYPE_RESPONSE ? "RESPONSE"sv : "REQUEST"sv; std::string_view payload {msg->payload, (size_t) msg->payloadLength}; std::string_view protocol {msg->protocol}; auto seqnm = msg->sequenceNumber; std::string_view messageBuffer {msg->messageBuffer}; BOOST_LOG(debug) << "type ["sv << type << ']'; BOOST_LOG(debug) << "sequence number ["sv << seqnm << ']'; BOOST_LOG(debug) << "protocol :: "sv << protocol; BOOST_LOG(debug) << "payload :: "sv << payload; if (msg->type == TYPE_RESPONSE) { auto &resp = msg->message.response; auto statuscode = resp.statusCode; std::string_view status {resp.statusString}; BOOST_LOG(debug) << "statuscode :: "sv << statuscode; BOOST_LOG(debug) << "status :: "sv << status; } else { auto &req = msg->message.request; std::string_view command {req.command}; std::string_view target {req.target}; BOOST_LOG(debug) << "command :: "sv << command; BOOST_LOG(debug) << "target :: "sv << target; } for (auto option = msg->options; option != nullptr; option = option->next) { std::string_view content {option->content}; std::string_view name {option->option}; BOOST_LOG(debug) << name << " :: "sv << content; } BOOST_LOG(debug) << "---Begin MessageBuffer---"sv << std::endl << messageBuffer << std::endl << "---End MessageBuffer---"sv << std::endl; } } // namespace rtsp_stream ================================================ FILE: src/rtsp.h ================================================ /** * @file src/rtsp.h * @brief Declarations for RTSP streaming. */ #pragma once // standard includes #include #include #include // local includes #include "crypto.h" #include "thread_safe.h" #ifdef _WIN32 #include #endif // Resolve circular dependencies namespace stream { struct session_t; } namespace rtsp_stream { constexpr auto RTSP_SETUP_PORT = 21; struct launch_session_t { uint32_t id; crypto::aes_t gcm_key; crypto::aes_t iv; std::string av_ping_payload; uint32_t control_connect_data; std::string device_name; std::string unique_id; crypto::PERM perm; bool input_only; bool host_audio; int width; int height; int fps; int gcmap; int surround_info; std::string surround_params; bool enable_hdr; bool enable_sops; bool virtual_display; uint32_t scale_factor; std::optional rtsp_cipher; std::string rtsp_url_scheme; uint32_t rtsp_iv_counter; std::list client_do_cmds; std::list client_undo_cmds; #ifdef _WIN32 GUID display_guid{}; #endif }; void launch_session_raise(std::shared_ptr launch_session); /** * @brief Clear state for the specified launch session. * @param launch_session_id The ID of the session to clear. */ void launch_session_clear(uint32_t launch_session_id); /** * @brief Get the number of active sessions. * @return Count of active sessions. */ int session_count(); std::shared_ptr find_session(const std::string_view& uuid); std::list get_all_session_uuids(); /** * @brief Terminates all running streaming sessions. */ void terminate_sessions(); /** * @brief Runs the RTSP server loop. */ void start(); } // namespace rtsp_stream ================================================ FILE: src/stat_trackers.cpp ================================================ /** * @file src/stat_trackers.cpp * @brief Definitions for streaming statistic tracking. */ // local includes #include "stat_trackers.h" namespace stat_trackers { boost::format one_digit_after_decimal() { return boost::format("%1$.1f"); } boost::format two_digits_after_decimal() { return boost::format("%1$.2f"); } } // namespace stat_trackers ================================================ FILE: src/stat_trackers.h ================================================ /** * @file src/stat_trackers.h * @brief Declarations for streaming statistic tracking. */ #pragma once // standard includes #include #include #include // lib includes #include namespace stat_trackers { boost::format one_digit_after_decimal(); boost::format two_digits_after_decimal(); template class min_max_avg_tracker { public: using callback_function = std::function; void collect_and_callback_on_interval(T stat, const callback_function &callback, std::chrono::seconds interval_in_seconds) { if (data.calls == 0) { data.last_callback_time = std::chrono::steady_clock::now(); } else if (std::chrono::steady_clock::now() > data.last_callback_time + interval_in_seconds) { callback(data.stat_min, data.stat_max, data.stat_total / data.calls); data = {}; } data.stat_min = std::min(data.stat_min, stat); data.stat_max = std::max(data.stat_max, stat); data.stat_total += stat; data.calls += 1; } void reset() { data = {}; } private: struct { std::chrono::steady_clock::time_point last_callback_time = std::chrono::steady_clock::now(); T stat_min = std::numeric_limits::max(); T stat_max = std::numeric_limits::min(); double stat_total = 0; uint32_t calls = 0; } data; }; } // namespace stat_trackers ================================================ FILE: src/stream.cpp ================================================ /** * @file src/stream.cpp * @brief Definitions for the streaming protocols. */ // standard includes #include #include #include // lib includes #include #include extern "C" { // clang-format off #include #include "rswrapper.h" // clang-format on } // local includes #include "config.h" #include "crypto.h" #include "display_device.h" #include "globals.h" #include "input.h" #include "logging.h" #include "network.h" #include "platform/common.h" #include "process.h" #include "stream.h" #include "sync.h" #include "system_tray.h" #include "thread_safe.h" #include "utility.h" #define IDX_START_A 0 #define IDX_START_B 1 #define IDX_INVALIDATE_REF_FRAMES 2 #define IDX_LOSS_STATS 3 #define IDX_INPUT_DATA 5 #define IDX_RUMBLE_DATA 6 #define IDX_TERMINATION 7 #define IDX_PERIODIC_PING 8 #define IDX_REQUEST_IDR_FRAME 9 #define IDX_ENCRYPTED 10 #define IDX_HDR_MODE 11 #define IDX_RUMBLE_TRIGGER_DATA 12 #define IDX_SET_MOTION_EVENT 13 #define IDX_SET_RGB_LED 14 #define IDX_EXEC_SERVER_CMD 15 #define IDX_SET_CLIPBOARD 16 #define IDX_FILE_TRANSFER_NONCE_REQUEST 17 #define IDX_SET_ADAPTIVE_TRIGGERS 18 static const short packetTypes[] = { 0x0305, // Start A 0x0307, // Start B 0x0301, // Invalidate reference frames 0x0201, // Loss Stats 0x0204, // Frame Stats (unused) 0x0206, // Input data 0x010b, // Rumble data 0x0109, // Termination 0x0200, // Periodic Ping 0x0302, // IDR frame 0x0001, // fully encrypted 0x010e, // HDR mode 0x5500, // Rumble triggers (Sunshine protocol extension) 0x5501, // Set motion event (Sunshine protocol extension) 0x5502, // Set RGB LED (Sunshine protocol extension) 0x3000, // Execute Server Command (Apollo protocol extension) 0x3001, // Set Clipboard (Apollo protocol extension) 0x3002, // File transfer nonce request (Apollo protocol extension) 0x5503, // Set Adaptive triggers (Sunshine protocol extension) }; namespace asio = boost::asio; namespace sys = boost::system; using asio::ip::tcp; using asio::ip::udp; using namespace std::literals; namespace stream { enum class socket_e : int { video, ///< Video audio ///< Audio }; #pragma pack(push, 1) struct video_short_frame_header_t { uint8_t *payload() { return (uint8_t *) (this + 1); } std::uint8_t headerType; // Always 0x01 for short headers // Sunshine extension // Frame processing latency, in 1/10 ms units // zero when the frame is repeated or there is no backend implementation boost::endian::little_uint16_at frame_processing_latency; // Currently known values: // 1 = Normal P-frame // 2 = IDR-frame // 4 = P-frame with intra-refresh blocks // 5 = P-frame after reference frame invalidation std::uint8_t frameType; // Length of the final packet payload for codecs that cannot handle // zero padding, such as AV1 (Sunshine extension). boost::endian::little_uint16_at lastPayloadLen; std::uint8_t unknown[2]; }; static_assert( sizeof(video_short_frame_header_t) == 8, "Short frame header must be 8 bytes" ); struct video_packet_raw_t { uint8_t *payload() { return (uint8_t *) (this + 1); } RTP_PACKET rtp; char reserved[4]; NV_VIDEO_PACKET packet; }; struct video_packet_enc_prefix_t { std::uint8_t iv[12]; // 12-byte IV is ideal for AES-GCM std::uint32_t frameNumber; std::uint8_t tag[16]; }; struct audio_packet_t { RTP_PACKET rtp; }; struct control_header_v2 { std::uint16_t type; std::uint16_t payloadLength; uint8_t *payload() { return (uint8_t *) (this + 1); } }; struct control_terminate_t { control_header_v2 header; std::uint32_t ec; }; struct control_rumble_t { control_header_v2 header; std::uint32_t useless; std::uint16_t id; std::uint16_t lowfreq; std::uint16_t highfreq; }; struct control_rumble_triggers_t { control_header_v2 header; std::uint16_t id; std::uint16_t left; std::uint16_t right; }; struct control_set_motion_event_t { control_header_v2 header; std::uint16_t id; std::uint16_t reportrate; std::uint8_t type; }; struct control_set_rgb_led_t { control_header_v2 header; std::uint16_t id; std::uint8_t r; std::uint8_t g; std::uint8_t b; }; struct control_adaptive_triggers_t { control_header_v2 header; std::uint16_t id; /** * 0x04 - Right trigger * 0x08 - Left trigger */ std::uint8_t event_flags; std::uint8_t type_left; std::uint8_t type_right; std::uint8_t left[DS_EFFECT_PAYLOAD_SIZE]; std::uint8_t right[DS_EFFECT_PAYLOAD_SIZE]; }; struct control_hdr_mode_t { control_header_v2 header; std::uint8_t enabled; // Sunshine protocol extension SS_HDR_METADATA metadata; }; typedef struct control_encrypted_t { std::uint16_t encryptedHeaderType; // Always LE 0x0001 std::uint16_t length; // sizeof(seq) + 16 byte tag + secondary header and data // seq is accepted as an arbitrary value in Moonlight std::uint32_t seq; // Monotonically increasing sequence number (used as IV for AES-GCM) uint8_t *payload() { return (uint8_t *) (this + 1); } // encrypted control_header_v2 and payload data follow } *control_encrypted_p; struct audio_fec_packet_t { RTP_PACKET rtp; AUDIO_FEC_HEADER fecHeader; }; #pragma pack(pop) constexpr std::size_t round_to_pkcs7_padded(std::size_t size) { return ((size + 15) / 16) * 16; } constexpr std::size_t MAX_AUDIO_PACKET_SIZE = 1400; using audio_aes_t = std::array; using av_session_id_t = std::variant; // IP address or SS-Ping-Payload from RTSP handshake using message_queue_t = std::shared_ptr>>; using message_queue_queue_t = std::shared_ptr>>; // return bytes written on success // return -1 on error static inline int encode_audio(bool encrypted, const audio::buffer_t &plaintext, uint8_t *destination, crypto::aes_t &iv, crypto::cipher::cbc_t &cbc) { // If encryption isn't enabled if (!encrypted) { std::copy(std::begin(plaintext), std::end(plaintext), destination); return plaintext.size(); } return cbc.encrypt(std::string_view {(char *) std::begin(plaintext), plaintext.size()}, destination, &iv); } static inline void while_starting_do_nothing(std::atomic &state) { while (state.load(std::memory_order_acquire) == session::state_e::STARTING) { std::this_thread::sleep_for(1ms); } } class control_server_t { public: int bind(net::af_e address_family, std::uint16_t port) { _host = net::host_create(address_family, _addr, port); return !(bool) _host; } // Get session associated with address. // If none are found, try to find a session not yet claimed. (It will be marked by a port of value 0 // If none of those are found, return nullptr session_t *get_session(const net::peer_t peer, uint32_t connect_data); // Circular dependency: // iterate refers to session // session refers to broadcast_ctx_t // broadcast_ctx_t refers to control_server_t // Therefore, iterate is implemented further down the source file void iterate(std::chrono::milliseconds timeout); /** * @brief Call the handler for a given control stream message. * @param type The message type. * @param session The session the message was received on. * @param payload The payload of the message. * @param reinjected `true` if this message is being reprocessed after decryption. */ void call(std::uint16_t type, session_t *session, const std::string_view &payload, bool reinjected); void map(uint16_t type, std::function cb) { _map_type_cb.emplace(type, std::move(cb)); } int send(const std::string_view &payload, net::peer_t peer) { auto packet = enet_packet_create(payload.data(), payload.size(), ENET_PACKET_FLAG_RELIABLE); if (enet_peer_send(peer, 0, packet)) { enet_packet_destroy(packet); return -1; } return 0; } void flush() { enet_host_flush(_host.get()); } // Callbacks std::unordered_map> _map_type_cb; // All active sessions (including those still waiting for a peer to connect) sync_util::sync_t> _sessions; // ENet peer to session mapping for sessions with a peer connected sync_util::sync_t> _peer_to_session; ENetAddress _addr; net::host_t _host; }; struct broadcast_ctx_t { message_queue_queue_t message_queue_queue; std::thread recv_thread; std::thread video_thread; std::thread audio_thread; std::thread control_thread; asio::io_context io_context; udp::socket video_sock {io_context}; udp::socket audio_sock {io_context}; control_server_t control_server; }; struct session_t { config_t config; safe::mail_t mail; std::shared_ptr input; std::thread audioThread; std::thread videoThread; std::chrono::steady_clock::time_point pingTimeout; safe::shared_t::ptr_t broadcast_ref; boost::asio::ip::address localAddress; struct { std::string ping_payload; int lowseq; udp::endpoint peer; std::optional cipher; std::uint64_t gcm_iv_counter; safe::mail_raw_t::event_t idr_events; safe::mail_raw_t::event_t> invalidate_ref_frames_events; std::unique_ptr qos; } video; struct { crypto::cipher::cbc_t cipher; std::string ping_payload; std::uint16_t sequenceNumber; // avRiKeyId == util::endian::big(First (sizeof(avRiKeyId)) bytes of launch_session->iv) std::uint32_t avRiKeyId; std::uint32_t timestamp; udp::endpoint peer; util::buffer_t shards; util::buffer_t shards_p; audio_fec_packet_t fec_packet; std::unique_ptr qos; } audio; struct { crypto::cipher::gcm_t cipher; crypto::aes_t legacy_input_enc_iv; // Only used when the client doesn't support full control stream encryption crypto::aes_t incoming_iv; crypto::aes_t outgoing_iv; std::uint32_t connect_data; // Used for new clients with ML_FF_SESSION_ID_V1 std::string expected_peer_address; // Only used for legacy clients without ML_FF_SESSION_ID_V1 net::peer_t peer; std::uint32_t seq; platf::feedback_queue_t feedback_queue; safe::mail_raw_t::event_t hdr_queue; } control; std::uint32_t launch_session_id; std::string device_name; std::string device_uuid; crypto::PERM permission; std::list do_cmds; std::list undo_cmds; safe::mail_raw_t::event_t shutdown_event; safe::signal_t controlEnd; std::atomic state; }; /** * First part of cipher must be struct of type control_encrypted_t * * returns empty string_view on failure * returns string_view pointing to payload data */ template static inline std::string_view encode_control(session_t *session, const std::string_view &plaintext, std::array &tagged_cipher) { static_assert( max_payload_size >= sizeof(control_encrypted_t) + sizeof(crypto::cipher::tag_size), "max_payload_size >= sizeof(control_encrypted_t) + sizeof(crypto::cipher::tag_size)" ); if (session->config.controlProtocolType != 13) { return plaintext; } auto seq = session->control.seq++; auto &iv = session->control.outgoing_iv; if (session->config.encryptionFlagsEnabled & SS_ENC_CONTROL_V2) { // We use the deterministic IV construction algorithm specified in NIST SP 800-38D // Section 8.2.1. The sequence number is our "invocation" field and the 'CH' in the // high bytes is the "fixed" field. Because each client provides their own unique // key, our values in the fixed field need only uniquely identify each independent // use of the client's key with AES-GCM in our code. // // The sequence number is 32 bits long which allows for 2^32 control stream messages // to be sent to each client before the IV repeats. iv.resize(12); std::copy_n((uint8_t *) &seq, sizeof(seq), std::begin(iv)); iv[10] = 'H'; // Host originated iv[11] = 'C'; // Control stream } else { // Nvidia's old style encryption uses a 16-byte IV iv.resize(16); iv[0] = (std::uint8_t) seq; } auto packet = (control_encrypted_p) tagged_cipher.data(); auto bytes = session->control.cipher.encrypt(plaintext, packet->payload(), &iv); if (bytes <= 0) { BOOST_LOG(error) << "Couldn't encrypt control data"sv; return {}; } std::uint16_t packet_length = bytes + crypto::cipher::tag_size + sizeof(control_encrypted_t::seq); packet->encryptedHeaderType = util::endian::little(0x0001); packet->length = util::endian::little(packet_length); packet->seq = util::endian::little(seq); return std::string_view {(char *) tagged_cipher.data(), packet_length + sizeof(control_encrypted_t) - sizeof(control_encrypted_t::seq)}; } int start_broadcast(broadcast_ctx_t &ctx); void end_broadcast(broadcast_ctx_t &ctx); static auto broadcast = safe::make_shared(start_broadcast, end_broadcast); session_t *control_server_t::get_session(const net::peer_t peer, uint32_t connect_data) { { // Fast path - look up existing session by peer auto lg = _peer_to_session.lock(); auto it = _peer_to_session->find(peer); if (it != _peer_to_session->end()) { return it->second; } } // Slow path - process new session TUPLE_2D(peer_port, peer_addr, platf::from_sockaddr_ex((sockaddr *) &peer->address.address)); auto lg = _sessions.lock(); for (auto pos = std::begin(*_sessions); pos != std::end(*_sessions); ++pos) { auto session_p = *pos; // Skip sessions that are already established if (session_p->control.peer) { continue; } // Identify the connection by the unique connect data if the client supports it. // Only fall back to IP address matching for clients without session ID support. if (session_p->config.mlFeatureFlags & ML_FF_SESSION_ID_V1) { if (session_p->control.connect_data != connect_data) { continue; } else { BOOST_LOG(debug) << "Initialized new control stream session by connect data match [v2]"sv; } } else { if (session_p->control.expected_peer_address != peer_addr) { continue; } else { BOOST_LOG(debug) << "Initialized new control stream session by IP address match [v1]"sv; } } // Once the control stream connection is established, RTSP session state can be torn down rtsp_stream::launch_session_clear(session_p->launch_session_id); session_p->control.peer = peer; // Use the local address from the control connection as the source address // for other communications to the client. This is necessary to ensure // proper routing on multi-homed hosts. auto local_address = platf::from_sockaddr((sockaddr *) &peer->localAddress.address); session_p->localAddress = boost::asio::ip::make_address(local_address); BOOST_LOG(debug) << "Control local address ["sv << local_address << ']'; BOOST_LOG(debug) << "Control peer address ["sv << peer_addr << ':' << peer_port << ']'; // Insert this into the map for O(1) lookups in the future auto ptslg = _peer_to_session.lock(); _peer_to_session->emplace(peer, session_p); return session_p; } return nullptr; } /** * @brief Call the handler for a given control stream message. * @param type The message type. * @param session The session the message was received on. * @param payload The payload of the message. * @param reinjected `true` if this message is being reprocessed after decryption. */ void control_server_t::call(std::uint16_t type, session_t *session, const std::string_view &payload, bool reinjected) { // If we are using the encrypted control stream protocol, drop any messages that come off the wire unencrypted if (session->config.controlProtocolType == 13 && !reinjected && type != packetTypes[IDX_ENCRYPTED]) { BOOST_LOG(error) << "Dropping unencrypted message on encrypted control stream: "sv << util::hex(type).to_string_view(); return; } auto cb = _map_type_cb.find(type); if (cb == std::end(_map_type_cb)) { BOOST_LOG(debug) << "type [Unknown] { "sv << util::hex(type).to_string_view() << " }"sv << std::endl << "---data---"sv << std::endl << util::hex_vec(payload) << std::endl << "---end data---"sv; } else { cb->second(session, payload); } } void control_server_t::iterate(std::chrono::milliseconds timeout) { ENetEvent event; auto res = enet_host_service(_host.get(), &event, timeout.count()); if (res > 0) { auto session = get_session(event.peer, event.data); if (!session) { BOOST_LOG(warning) << "Rejected connection from ["sv << platf::from_sockaddr((sockaddr *) &event.peer->address.address) << "]: it's not properly set up"sv; enet_peer_disconnect_now(event.peer, 0); return; } session->pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout; switch (event.type) { case ENET_EVENT_TYPE_RECEIVE: { net::packet_t packet {event.packet}; auto type = *(std::uint16_t *) packet->data; std::string_view payload {(char *) packet->data + sizeof(type), packet->dataLength - sizeof(type)}; call(type, session, payload, false); } break; case ENET_EVENT_TYPE_CONNECT: BOOST_LOG(info) << "CLIENT CONNECTED"sv; break; case ENET_EVENT_TYPE_DISCONNECT: BOOST_LOG(info) << "CLIENT DISCONNECTED"sv; // No more clients to send video data to ^_^ if (session->state == session::state_e::RUNNING) { session::stop(*session); } break; case ENET_EVENT_TYPE_NONE: break; } } } namespace fec { using rs_t = util::safe_ptr; struct fec_t { size_t data_shards; size_t nr_shards; size_t percentage; size_t blocksize; size_t prefixsize; util::buffer_t shards; util::buffer_t headers; util::buffer_t shards_p; std::vector payload_buffers; char *data(size_t el) { return (char *) shards_p[el]; } char *prefix(size_t el) { return prefixsize ? &headers[el * prefixsize] : nullptr; } size_t size() const { return nr_shards; } }; static fec_t encode(const std::string_view &payload, size_t blocksize, size_t fecpercentage, size_t minparityshards, size_t prefixsize) { auto payload_size = payload.size(); auto pad = payload_size % blocksize != 0; auto aligned_data_shards = payload_size / blocksize; auto data_shards = aligned_data_shards + (pad ? 1 : 0); auto parity_shards = (data_shards * fecpercentage + 99) / 100; // increase the FEC percentage for this frame if the parity shard minimum is not met if (parity_shards < minparityshards && fecpercentage != 0) { parity_shards = minparityshards; fecpercentage = (100 * parity_shards) / data_shards; BOOST_LOG(verbose) << "Increasing FEC percentage to "sv << fecpercentage << " to meet parity shard minimum"sv << std::endl; } auto nr_shards = data_shards + parity_shards; // If we need to store a zero-padded data shard, allocate that first to // to keep the shards in order and reduce buffer fragmentation auto parity_shard_offset = pad ? 1 : 0; util::buffer_t shards {(parity_shard_offset + parity_shards) * blocksize}; util::buffer_t shards_p {nr_shards}; std::vector payload_buffers; payload_buffers.reserve(2); // Point into the payload buffer for all except the final padded data shard auto next = std::begin(payload); for (auto x = 0; x < aligned_data_shards; ++x) { shards_p[x] = (uint8_t *) next; next += blocksize; } payload_buffers.emplace_back(std::begin(payload), aligned_data_shards * blocksize); // If the last data shard needs to be zero-padded, we must use the shards buffer if (pad) { shards_p[aligned_data_shards] = (uint8_t *) &shards[0]; // GCC doesn't figure out that std::copy_n() can be replaced with memcpy() here // and ends up compiling a horribly slow element-by-element copy loop, so we // help it by using memcpy()/memset() directly. auto copy_len = std::min(blocksize, std::end(payload) - next); std::memcpy(shards_p[aligned_data_shards], next, copy_len); if (copy_len < blocksize) { // Zero any additional space after the end of the payload std::memset(shards_p[aligned_data_shards] + copy_len, 0, blocksize - copy_len); } } // Add a payload buffer describing the shard buffer payload_buffers.emplace_back(std::begin(shards), shards.size()); if (fecpercentage != 0) { // Point into our allocated buffer for the parity shards for (auto x = 0; x < parity_shards; ++x) { shards_p[data_shards + x] = (uint8_t *) &shards[(parity_shard_offset + x) * blocksize]; } // packets = parity_shards + data_shards rs_t rs {reed_solomon_new(data_shards, parity_shards)}; reed_solomon_encode(rs.get(), shards_p.begin(), nr_shards, blocksize); } return { data_shards, nr_shards, fecpercentage, blocksize, prefixsize, std::move(shards), util::buffer_t {nr_shards * prefixsize}, std::move(shards_p), std::move(payload_buffers), }; } } // namespace fec /** * @brief Combines two buffers and inserts new buffers at each slice boundary of the result. * @param insert_size The number of bytes to insert. * @param slice_size The number of bytes between insertions. * @param data1 The first data buffer. * @param data2 The second data buffer. */ std::vector concat_and_insert(uint64_t insert_size, uint64_t slice_size, const std::string_view &data1, const std::string_view &data2) { auto data_size = data1.size() + data2.size(); auto pad = data_size % slice_size != 0; auto elements = data_size / slice_size + (pad ? 1 : 0); std::vector result; result.resize(elements * insert_size + data_size); auto next = std::begin(data1); auto end = std::end(data1); for (auto x = 0; x < elements; ++x) { void *p = &result[x * (insert_size + slice_size)]; // For the last iteration, only copy to the end of the data if (x == elements - 1) { slice_size = data_size - (x * slice_size); } // Test if this slice will extend into the next buffer if (next + slice_size > end) { // Copy the first portion from the first buffer auto copy_len = end - next; std::copy(next, end, (char *) p + insert_size); // Copy the remaining portion from the second buffer next = std::begin(data2); end = std::end(data2); std::copy(next, next + (slice_size - copy_len), (char *) p + copy_len + insert_size); next += slice_size - copy_len; } else { std::copy(next, next + slice_size, (char *) p + insert_size); next += slice_size; } } return result; } std::vector replace(const std::string_view &original, const std::string_view &old, const std::string_view &_new) { std::vector replaced; replaced.reserve(original.size() + _new.size() - old.size()); auto begin = std::begin(original); auto end = std::end(original); auto next = std::search(begin, end, std::begin(old), std::end(old)); std::copy(begin, next, std::back_inserter(replaced)); if (next != end) { std::copy(std::begin(_new), std::end(_new), std::back_inserter(replaced)); std::copy(next + old.size(), end, std::back_inserter(replaced)); } return replaced; } /** * @brief Pass gamepad feedback data back to the client. * @param session The session object. * @param msg The message to pass. * @return 0 on success. */ int send_feedback_msg(session_t *session, platf::gamepad_feedback_msg_t &msg) { if (!session->control.peer) { BOOST_LOG(warning) << "Couldn't send gamepad feedback data, still waiting for PING from Moonlight"sv; // Still waiting for PING from Moonlight return -1; } std::string payload; if (msg.type == platf::gamepad_feedback_e::rumble) { control_rumble_t plaintext; plaintext.header.type = packetTypes[IDX_RUMBLE_DATA]; plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2); auto &data = msg.data.rumble; plaintext.useless = 0xC0FFEE; plaintext.id = util::endian::little(msg.id); plaintext.lowfreq = util::endian::little(data.lowfreq); plaintext.highfreq = util::endian::little(data.highfreq); BOOST_LOG(verbose) << "Rumble: "sv << msg.id << " :: "sv << util::hex(data.lowfreq).to_string_view() << " :: "sv << util::hex(data.highfreq).to_string_view(); std::array encrypted_payload; payload = encode_control(session, util::view(plaintext), encrypted_payload); } else if (msg.type == platf::gamepad_feedback_e::rumble_triggers) { control_rumble_triggers_t plaintext; plaintext.header.type = packetTypes[IDX_RUMBLE_TRIGGER_DATA]; plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2); auto &data = msg.data.rumble_triggers; plaintext.id = util::endian::little(msg.id); plaintext.left = util::endian::little(data.left_trigger); plaintext.right = util::endian::little(data.right_trigger); 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(); std::array encrypted_payload; payload = encode_control(session, util::view(plaintext), encrypted_payload); } else if (msg.type == platf::gamepad_feedback_e::set_motion_event_state) { control_set_motion_event_t plaintext; plaintext.header.type = packetTypes[IDX_SET_MOTION_EVENT]; plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2); auto &data = msg.data.motion_event_state; plaintext.id = util::endian::little(msg.id); plaintext.reportrate = util::endian::little(data.report_rate); plaintext.type = data.motion_type; 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(); std::array encrypted_payload; payload = encode_control(session, util::view(plaintext), encrypted_payload); } else if (msg.type == platf::gamepad_feedback_e::set_rgb_led) { control_set_rgb_led_t plaintext; plaintext.header.type = packetTypes[IDX_SET_RGB_LED]; plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2); auto &data = msg.data.rgb_led; plaintext.id = util::endian::little(msg.id); plaintext.r = data.r; plaintext.g = data.g; plaintext.b = data.b; 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(); std::array encrypted_payload; payload = encode_control(session, util::view(plaintext), encrypted_payload); } else if (msg.type == platf::gamepad_feedback_e::set_adaptive_triggers) { control_adaptive_triggers_t plaintext; plaintext.header.type = packetTypes[IDX_SET_ADAPTIVE_TRIGGERS]; plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2); plaintext.id = util::endian::little(msg.id); plaintext.event_flags = msg.data.adaptive_triggers.event_flags; plaintext.type_left = msg.data.adaptive_triggers.type_left; std::ranges::copy(msg.data.adaptive_triggers.left, plaintext.left); plaintext.type_right = msg.data.adaptive_triggers.type_right; std::ranges::copy(msg.data.adaptive_triggers.right, plaintext.right); std::array encrypted_payload; payload = encode_control(session, util::view(plaintext), encrypted_payload); } else { BOOST_LOG(error) << "Unknown gamepad feedback message type"sv; return -1; } if (session->broadcast_ref->control_server.send(payload, session->control.peer)) { TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *) &session->control.peer->address.address)); BOOST_LOG(warning) << "Couldn't send gamepad feedback to ["sv << addr << ':' << port << ']'; return -1; } return 0; } int send_hdr_mode(session_t *session, video::hdr_info_t hdr_info) { if (!session->control.peer) { BOOST_LOG(warning) << "Couldn't send HDR mode, still waiting for PING from Moonlight"sv; // Still waiting for PING from Moonlight return -1; } control_hdr_mode_t plaintext {}; plaintext.header.type = packetTypes[IDX_HDR_MODE]; plaintext.header.payloadLength = sizeof(control_hdr_mode_t) - sizeof(control_header_v2); plaintext.enabled = hdr_info->enabled; plaintext.metadata = hdr_info->metadata; std::array encrypted_payload; auto payload = encode_control(session, util::view(plaintext), encrypted_payload); if (session->broadcast_ref->control_server.send(payload, session->control.peer)) { TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *) &session->control.peer->address.address)); BOOST_LOG(warning) << "Couldn't send HDR mode to ["sv << addr << ':' << port << ']'; return -1; } BOOST_LOG(debug) << "Sent HDR mode: " << hdr_info->enabled; return 0; } void controlBroadcastThread(control_server_t *server) { server->map(packetTypes[IDX_PERIODIC_PING], [](session_t *session, const std::string_view &payload) { BOOST_LOG(verbose) << "type [IDX_PERIODIC_PING]"sv; }); server->map(packetTypes[IDX_START_A], [&](session_t *session, const std::string_view &payload) { BOOST_LOG(debug) << "type [IDX_START_A]"sv; }); server->map(packetTypes[IDX_START_B], [&](session_t *session, const std::string_view &payload) { BOOST_LOG(debug) << "type [IDX_START_B]"sv; }); server->map(packetTypes[IDX_LOSS_STATS], [&](session_t *session, const std::string_view &payload) { int32_t *stats = (int32_t *) payload.data(); auto count = stats[0]; std::chrono::milliseconds t {stats[1]}; auto lastGoodFrame = stats[3]; BOOST_LOG(verbose) << "type [IDX_LOSS_STATS]"sv << std::endl << "---begin stats---" << std::endl << "loss count since last report [" << count << ']' << std::endl << "time in milli since last report [" << t.count() << ']' << std::endl << "last good frame [" << lastGoodFrame << ']' << std::endl << "---end stats---"; }); server->map(packetTypes[IDX_REQUEST_IDR_FRAME], [&](session_t *session, const std::string_view &payload) { BOOST_LOG(debug) << "type [IDX_REQUEST_IDR_FRAME]"sv; session->video.idr_events->raise(true); }); server->map(packetTypes[IDX_INVALIDATE_REF_FRAMES], [&](session_t *session, const std::string_view &payload) { auto frames = (std::int64_t *) payload.data(); auto firstFrame = frames[0]; auto lastFrame = frames[1]; BOOST_LOG(debug) << "type [IDX_INVALIDATE_REF_FRAMES]"sv << std::endl << "firstFrame [" << firstFrame << ']' << std::endl << "lastFrame [" << lastFrame << ']'; session->video.invalidate_ref_frames_events->raise(std::make_pair(firstFrame, lastFrame)); }); server->map(packetTypes[IDX_INPUT_DATA], [&](session_t *session, const std::string_view &payload) { BOOST_LOG(debug) << "type [IDX_INPUT_DATA]"sv; auto tagged_cipher_length = util::endian::big(*(int32_t *) payload.data()); std::string_view tagged_cipher {payload.data() + sizeof(tagged_cipher_length), (size_t) tagged_cipher_length}; std::vector plaintext; auto &cipher = session->control.cipher; auto &iv = session->control.legacy_input_enc_iv; if (cipher.decrypt(tagged_cipher, plaintext, &iv)) { // something went wrong :( BOOST_LOG(error) << "Failed to verify tag"sv; session::stop(*session); return; } if (tagged_cipher_length >= 16 + iv.size()) { std::copy(payload.end() - 16, payload.end(), std::begin(iv)); } input::passthrough(session->input, std::move(plaintext), session->permission); }); server->map(packetTypes[IDX_EXEC_SERVER_CMD], [server](session_t *session, const std::string_view &payload) { BOOST_LOG(debug) << "type [IDX_EXEC_SERVER_CMD]"sv; if (!(session->permission & crypto::PERM::server_cmd)) { BOOST_LOG(debug) << "Permission Exec Server Cmd deined for [" << session->device_name << "]"; return; } uint8_t cmdIndex = *(uint8_t*)payload.data(); if (cmdIndex < config::sunshine.server_cmds.size()) { const auto& cmd = config::sunshine.server_cmds[cmdIndex]; BOOST_LOG(info) << "Executing server command: " << cmd.cmd_name; auto exec_thread = std::thread([&cmd]{ std::error_code ec; auto env = proc::proc.get_env(); boost::filesystem::path working_dir = proc::find_working_directory(cmd.cmd_val, env); auto child = platf::run_command(cmd.elevated, true, cmd.cmd_val, working_dir, env, nullptr, ec, nullptr); if (ec) { BOOST_LOG(error) << "Failed to execute server command: " << ec.message(); } else { child.detach(); } }); exec_thread.detach(); } else { BOOST_LOG(error) << "Invalid server command index: " << (int)cmdIndex; } }); server->map(packetTypes[IDX_SET_CLIPBOARD], [server](session_t *session, const std::string_view &payload) { BOOST_LOG(info) << "type [IDX_SET_CLIPBOARD]: "sv << payload << " size: " << payload.size(); if (!(session->permission & crypto::PERM::clipboard_set)) { BOOST_LOG(debug) << "Permission Clipboard Set deined for [" << session->device_name << "]"; return; } }); server->map(packetTypes[IDX_FILE_TRANSFER_NONCE_REQUEST], [server](session_t *session, const std::string_view &payload) { BOOST_LOG(info) << "type [IDX_FILE_TRANSFER_NONCE_REQUEST]: "sv << payload << " size: " << payload.size(); if (!(session->permission & crypto::PERM::file_upload)) { BOOST_LOG(debug) << "Permission File Upload deined for [" << session->device_name << "]"; return; } }); server->map(packetTypes[IDX_ENCRYPTED], [server](session_t *session, const std::string_view &payload) { BOOST_LOG(verbose) << "type [IDX_ENCRYPTED]"sv; auto header = (control_encrypted_p) (payload.data() - 2); auto length = util::endian::little(header->length); auto seq = util::endian::little(header->seq); if (length < (16 + 4 + 4)) { BOOST_LOG(warning) << "Control: Runt packet"sv; return; } auto tagged_cipher_length = length - 4; std::string_view tagged_cipher {(char *) header->payload(), (size_t) tagged_cipher_length}; auto &cipher = session->control.cipher; auto &iv = session->control.incoming_iv; if (session->config.encryptionFlagsEnabled & SS_ENC_CONTROL_V2) { // We use the deterministic IV construction algorithm specified in NIST SP 800-38D // Section 8.2.1. The sequence number is our "invocation" field and the 'CC' in the // high bytes is the "fixed" field. Because each client provides their own unique // key, our values in the fixed field need only uniquely identify each independent // use of the client's key with AES-GCM in our code. // // The sequence number is 32 bits long which allows for 2^32 control stream messages // to be received from each client before the IV repeats. iv.resize(12); std::copy_n((uint8_t *) &seq, sizeof(seq), std::begin(iv)); iv[10] = 'C'; // Client originated iv[11] = 'C'; // Control stream } else { // Nvidia's old style encryption uses a 16-byte IV iv.resize(16); iv[0] = (std::uint8_t) seq; } std::vector plaintext; if (cipher.decrypt(tagged_cipher, plaintext, &iv)) { // something went wrong :( BOOST_LOG(error) << "Failed to verify tag"sv; session::stop(*session); return; } auto type = *(std::uint16_t *) plaintext.data(); std::string_view next_payload {(char *) plaintext.data() + 4, plaintext.size() - 4}; if (type == packetTypes[IDX_ENCRYPTED]) { BOOST_LOG(error) << "Bad packet type [IDX_ENCRYPTED] found"sv; session::stop(*session); return; } // IDX_INPUT_DATA callback will attempt to decrypt unencrypted data, therefore we need pass it directly if (type == packetTypes[IDX_INPUT_DATA]) { plaintext.erase(std::begin(plaintext), std::begin(plaintext) + 4); input::passthrough(session->input, std::move(plaintext), session->permission); } else { server->call(type, session, next_payload, true); } }); // This thread handles latency-sensitive control messages platf::adjust_thread_priority(platf::thread_priority_e::critical); // Check for both the full shutdown event and the shutdown event for this // broadcast to ensure we can inform connected clients of our graceful // termination when we shut down. auto shutdown_event = mail::man->event(mail::shutdown); auto broadcast_shutdown_event = mail::man->event(mail::broadcast_shutdown); while (!shutdown_event->peek() && !broadcast_shutdown_event->peek()) { bool has_session_awaiting_peer = false; { auto lg = server->_sessions.lock(); auto now = std::chrono::steady_clock::now(); KITTY_WHILE_LOOP(auto pos = std::begin(*server->_sessions), pos != std::end(*server->_sessions), { // Don't perform additional session processing if we're shutting down if (shutdown_event->peek() || broadcast_shutdown_event->peek()) { break; } auto session = *pos; if (now > session->pingTimeout) { auto address = session->control.peer ? platf::from_sockaddr((sockaddr *) &session->control.peer->address.address) : session->control.expected_peer_address; BOOST_LOG(info) << address << ": Ping Timeout"sv; session::stop(*session); } if (session->state.load(std::memory_order_acquire) == session::state_e::STOPPING) { pos = server->_sessions->erase(pos); if (session->control.peer) { { auto ptslg = server->_peer_to_session.lock(); server->_peer_to_session->erase(session->control.peer); } enet_peer_disconnect_now(session->control.peer, 0); } session->controlEnd.raise(true); continue; } // Remember if we have a session that's waiting for a peer to connect to the // control stream. This ensures the clients are properly notified even when // the app terminates before they finish connecting. if (!session->control.peer) { has_session_awaiting_peer = true; } else { auto &feedback_queue = session->control.feedback_queue; while (feedback_queue->peek()) { auto feedback_msg = feedback_queue->pop(); send_feedback_msg(session, *feedback_msg); } auto &hdr_queue = session->control.hdr_queue; while (session->control.peer && hdr_queue->peek()) { auto hdr_info = hdr_queue->pop(); send_hdr_mode(session, std::move(hdr_info)); } } ++pos; }) } // Don't break until any pending sessions either expire or connect if (proc::proc.running() == 0 && !has_session_awaiting_peer) { BOOST_LOG(info) << "Process terminated"sv; break; } server->iterate(150ms); } // Let all remaining connections know the server is shutting down // reason: graceful termination std::uint32_t reason = 0x80030023; control_terminate_t plaintext; plaintext.header.type = packetTypes[IDX_TERMINATION]; plaintext.header.payloadLength = sizeof(plaintext.ec); plaintext.ec = util::endian::big(reason); std::array encrypted_payload; auto lg = server->_sessions.lock(); for (auto pos = std::begin(*server->_sessions); pos != std::end(*server->_sessions); ++pos) { auto session = *pos; // We may not have gotten far enough to have an ENet connection yet if (session->control.peer) { auto payload = encode_control(session, util::view(plaintext), encrypted_payload); if (server->send(payload, session->control.peer)) { TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *) &session->control.peer->address.address)); BOOST_LOG(warning) << "Couldn't send termination code to ["sv << addr << ':' << port << ']'; } } session->shutdown_event->raise(true); session->controlEnd.raise(true); } server->flush(); } void recvThread(broadcast_ctx_t &ctx) { std::map peer_to_video_session; std::map peer_to_audio_session; auto &video_sock = ctx.video_sock; auto &audio_sock = ctx.audio_sock; auto &message_queue_queue = ctx.message_queue_queue; auto broadcast_shutdown_event = mail::man->event(mail::broadcast_shutdown); auto &io = ctx.io_context; udp::endpoint peer; std::array buf[2]; std::function recv_func[2]; auto populate_peer_to_session = [&]() { while (message_queue_queue->peek()) { auto message_queue_opt = message_queue_queue->pop(); TUPLE_3D_REF(socket_type, session_id, message_queue, *message_queue_opt); switch (socket_type) { case socket_e::video: if (message_queue) { peer_to_video_session.emplace(session_id, message_queue); } else { peer_to_video_session.erase(session_id); } break; case socket_e::audio: if (message_queue) { peer_to_audio_session.emplace(session_id, message_queue); } else { peer_to_audio_session.erase(session_id); } break; } } }; auto recv_func_init = [&](udp::socket &sock, int buf_elem, std::map &peer_to_session) { recv_func[buf_elem] = [&, buf_elem](const boost::system::error_code &ec, size_t bytes) { auto fg = util::fail_guard([&]() { sock.async_receive_from(asio::buffer(buf[buf_elem]), peer, 0, recv_func[buf_elem]); }); auto type_str = buf_elem ? "AUDIO"sv : "VIDEO"sv; BOOST_LOG(verbose) << "Recv: "sv << peer.address().to_string() << ':' << peer.port() << " :: " << type_str; populate_peer_to_session(); // No data, yet no error if (ec == boost::system::errc::connection_refused || ec == boost::system::errc::connection_reset) { return; } if (ec || !bytes) { BOOST_LOG(error) << "Couldn't receive data from udp socket: "sv << ec.message(); return; } if (bytes == 4) { // For legacy PING packets, find the matching session by address. auto it = peer_to_session.find(peer.address()); if (it != std::end(peer_to_session)) { BOOST_LOG(debug) << "RAISE: "sv << peer.address().to_string() << ':' << peer.port() << " :: " << type_str; it->second->raise(peer, std::string {buf[buf_elem].data(), bytes}); } } else if (bytes >= sizeof(SS_PING)) { auto ping = (PSS_PING) buf[buf_elem].data(); // For new PING packets that include a client identifier, search by payload. auto it = peer_to_session.find(std::string {ping->payload, sizeof(ping->payload)}); if (it != std::end(peer_to_session)) { BOOST_LOG(debug) << "RAISE: "sv << peer.address().to_string() << ':' << peer.port() << " :: " << type_str; it->second->raise(peer, std::string {buf[buf_elem].data(), bytes}); } } }; }; recv_func_init(video_sock, 0, peer_to_video_session); recv_func_init(audio_sock, 1, peer_to_audio_session); video_sock.async_receive_from(asio::buffer(buf[0]), peer, 0, recv_func[0]); audio_sock.async_receive_from(asio::buffer(buf[1]), peer, 0, recv_func[1]); while (!broadcast_shutdown_event->peek()) { io.run(); } } void videoBroadcastThread(udp::socket &sock) { auto shutdown_event = mail::man->event(mail::broadcast_shutdown); auto packets = mail::man->queue(mail::video_packets); auto video_epoch = std::chrono::steady_clock::now(); // Video traffic is sent on this thread platf::adjust_thread_priority(platf::thread_priority_e::high); logging::min_max_avg_periodic_logger frame_processing_latency_logger(debug, "Frame processing latency", "ms"); logging::time_delta_periodic_logger frame_send_batch_latency_logger(debug, "Network: each send_batch() latency"); logging::time_delta_periodic_logger frame_fec_latency_logger(debug, "Network: each FEC block latency"); logging::time_delta_periodic_logger frame_network_latency_logger(debug, "Network: frame's overall network latency"); crypto::aes_t iv(12); auto timer = platf::create_high_precision_timer(); if (!timer || !*timer) { BOOST_LOG(error) << "Failed to create timer, aborting video broadcast thread"; return; } auto ratecontrol_next_frame_start = std::chrono::steady_clock::now(); while (auto packet = packets->pop()) { if (shutdown_event->peek()) { break; } frame_network_latency_logger.first_point_now(); auto session = (session_t *) packet->channel_data; auto lowseq = session->video.lowseq; std::string_view payload {(char *) packet->data(), packet->data_size()}; std::vector payload_with_replacements; // Apply replacements on the packet payload before performing any other operations. // We need to know the final frame size to calculate the last packet size, and we // must avoid matching replacements against the frame header or any other non-video // part of the payload. if (packet->is_idr() && packet->replacements) { for (auto &replacement : *packet->replacements) { auto frame_old = replacement.old; auto frame_new = replacement._new; payload_with_replacements = replace(payload, frame_old, frame_new); payload = {(char *) payload_with_replacements.data(), payload_with_replacements.size()}; } } video_short_frame_header_t frame_header = {}; frame_header.headerType = 0x01; // Short header type frame_header.frameType = packet->is_idr() ? 2 : packet->after_ref_frame_invalidation ? 5 : 1; frame_header.lastPayloadLen = (payload.size() + sizeof(frame_header)) % (session->config.packetsize - sizeof(NV_VIDEO_PACKET)); if (frame_header.lastPayloadLen == 0) { frame_header.lastPayloadLen = session->config.packetsize - sizeof(NV_VIDEO_PACKET); } if (packet->frame_timestamp) { auto duration_to_latency = [](const std::chrono::steady_clock::duration &duration) { const auto duration_us = std::chrono::duration_cast(duration).count(); return (uint16_t) std::clamp((duration_us + 50) / 100, 0, std::numeric_limits::max()); }; uint16_t latency = duration_to_latency(std::chrono::steady_clock::now() - *packet->frame_timestamp); frame_header.frame_processing_latency = latency; frame_processing_latency_logger.collect_and_log(latency / 10.); } else { frame_header.frame_processing_latency = 0; } auto fecPercentage = config::stream.fec_percentage; // Insert space for packet headers auto blocksize = session->config.packetsize + MAX_RTP_HEADER_SIZE; auto payload_blocksize = blocksize - sizeof(video_packet_raw_t); auto payload_new = concat_and_insert(sizeof(video_packet_raw_t), payload_blocksize, std::string_view {(char *) &frame_header, sizeof(frame_header)}, payload); payload = std::string_view {(char *) payload_new.data(), payload_new.size()}; // There are 2 bits for FEC block count for a maximum of 4 FEC blocks constexpr auto MAX_FEC_BLOCKS = 4; // The max number of data shards per block is found by solving this system of equations for D: // D = 255 - P // P = D * F // which results in the solution: // D = 255 / (1 + F) // multiplied by 100 since F is the percentage as an integer: // D = (255 * 100) / (100 + F) auto max_data_shards_per_fec_block = (DATA_SHARDS_MAX * 100) / (100 + fecPercentage); // Compute the number of FEC blocks needed for this frame using the block size and max shards auto max_data_per_fec_block = max_data_shards_per_fec_block * blocksize; auto fec_blocks_needed = (payload.size() + (max_data_per_fec_block - 1)) / max_data_per_fec_block; // If the number of FEC blocks needed exceeds the protocol limit, turn off FEC for this frame. // For normal FEC percentages, this should only happen for enormous frames (over 800 packets at 20%). if (fec_blocks_needed > MAX_FEC_BLOCKS) { BOOST_LOG(warning) << "Skipping FEC for abnormally large encoded frame (needed "sv << fec_blocks_needed << " FEC blocks)"sv; fecPercentage = 0; fec_blocks_needed = MAX_FEC_BLOCKS; } std::array fec_blocks; decltype(fec_blocks)::iterator fec_blocks_begin = std::begin(fec_blocks), fec_blocks_end = std::begin(fec_blocks) + fec_blocks_needed; BOOST_LOG(verbose) << "Generating "sv << fec_blocks_needed << " FEC blocks"sv; // Align individual FEC blocks to blocksize auto unaligned_size = payload.size() / fec_blocks_needed; auto aligned_size = ((unaligned_size + (blocksize - 1)) / blocksize) * blocksize; // If we exceed the 10-bit FEC packet index (which means our frame exceeded 4096 packets), // the frame will be unrecoverable. Log an error for this case. if (aligned_size / blocksize >= 1024) { BOOST_LOG(error) << "Encoder produced a frame too large to send! Is the encoder broken? (needed "sv << (aligned_size / blocksize) << " packets)"sv; } // Split the data into aligned FEC blocks for (int x = 0; x < fec_blocks_needed; ++x) { if (x == fec_blocks_needed - 1) { // The last block must extend to the end of the payload fec_blocks[x] = payload.substr(x * aligned_size); } else { // Earlier blocks just extend to the next block offset fec_blocks[x] = payload.substr(x * aligned_size, aligned_size); } } try { // Use around 80% of 1Gbps 1Gbps percent ms packet byte size_t ratecontrol_packets_in_1ms = std::giga::num * 80 / 100 / 1000 / blocksize / 8; // Send less than 64K in a single batch. // On Windows, batches above 64K seem to bypass SO_SNDBUF regardless of its size, // appear in "Other I/O" and begin waiting for interrupts. // This gives inconsistent performance so we'd rather avoid it. size_t send_batch_size = 64 * 1024 / blocksize; // Also don't exceed 64 packets, which can happen when Moonlight requests // unusually small packet size. // Generic Segmentation Offload on Linux can't do more than 64. send_batch_size = std::min(64, send_batch_size); // Don't ignore the last ratecontrol group of the previous frame auto ratecontrol_frame_start = std::max(ratecontrol_next_frame_start, std::chrono::steady_clock::now()); size_t ratecontrol_frame_packets_sent = 0; size_t ratecontrol_group_packets_sent = 0; auto blockIndex = 0; std::for_each(fec_blocks_begin, fec_blocks_end, [&](std::string_view ¤t_payload) { auto packets = (current_payload.size() + (blocksize - 1)) / blocksize; for (int x = 0; x < packets; ++x) { auto *inspect = (video_packet_raw_t *) ¤t_payload[x * blocksize]; inspect->packet.frameIndex = packet->frame_index(); inspect->packet.streamPacketIndex = ((uint32_t) lowseq + x) << 8; // Match multiFecFlags with Moonlight inspect->packet.multiFecFlags = 0x10; inspect->packet.multiFecBlocks = (blockIndex << 4) | ((fec_blocks_needed - 1) << 6); inspect->packet.flags = FLAG_CONTAINS_PIC_DATA; if (x == 0) { inspect->packet.flags |= FLAG_SOF; } if (x == packets - 1) { inspect->packet.flags |= FLAG_EOF; } } frame_fec_latency_logger.first_point_now(); // If video encryption is enabled, we allocate space for the encryption header before each shard auto shards = fec::encode(current_payload, blocksize, fecPercentage, session->config.minRequiredFecPackets, session->video.cipher ? sizeof(video_packet_enc_prefix_t) : 0); frame_fec_latency_logger.second_point_now_and_log(); auto peer_address = session->video.peer.address(); auto batch_info = platf::batched_send_info_t { shards.headers.begin(), shards.prefixsize, shards.payload_buffers, shards.blocksize, 0, 0, (uintptr_t) sock.native_handle(), peer_address, session->video.peer.port(), session->localAddress, }; size_t next_shard_to_send = 0; // RTP video timestamps use a 90 KHz clock and the frame_timestamp from when the frame was captured // When a timestamp isn't available (duplicate frames), the timestamp from rate control is used instead. bool frame_is_dupe = false; if (!packet->frame_timestamp) { packet->frame_timestamp = ratecontrol_next_frame_start; frame_is_dupe = true; } using rtp_tick = std::chrono::duration>; uint32_t timestamp = std::chrono::round(*packet->frame_timestamp - video_epoch).count(); // set FEC info now that we know for sure what our percentage will be for this frame for (auto x = 0; x < shards.size(); ++x) { auto *inspect = (video_packet_raw_t *) shards.data(x); inspect->packet.fecInfo = (x << 12 | shards.data_shards << 22 | shards.percentage << 4); inspect->rtp.header = 0x80 | FLAG_EXTENSION; inspect->rtp.sequenceNumber = util::endian::big(lowseq + x); inspect->rtp.timestamp = util::endian::big(timestamp); inspect->packet.multiFecBlocks = (blockIndex << 4) | ((fec_blocks_needed - 1) << 6); inspect->packet.frameIndex = packet->frame_index(); // Encrypt this shard if video encryption is enabled if (session->video.cipher) { // We use the deterministic IV construction algorithm specified in NIST SP 800-38D // Section 8.2.1. The sequence number is our "invocation" field and the 'V' in the // high bytes is the "fixed" field. Because each client provides their own unique // key, our values in the fixed field need only uniquely identify each independent // use of the client's key with AES-GCM in our code. // // The IV counter is 64 bits long which allows for 2^64 encrypted video packets // to be sent to each client before the IV repeats. std::copy_n((uint8_t *) &session->video.gcm_iv_counter, sizeof(session->video.gcm_iv_counter), std::begin(iv)); iv[11] = 'V'; // Video stream session->video.gcm_iv_counter++; // Encrypt the target buffer in place auto *prefix = (video_packet_enc_prefix_t *) shards.prefix(x); prefix->frameNumber = packet->frame_index(); std::copy(std::begin(iv), std::end(iv), prefix->iv); session->video.cipher->encrypt(std::string_view {(char *) inspect, (size_t) blocksize}, prefix->tag, (uint8_t *) inspect, &iv); } if (x - next_shard_to_send + 1 >= send_batch_size || x + 1 == shards.size()) { // Do pacing within the frame. // Also trigger pacing before the first send_batch() of the frame // to account for the last send_batch() of the previous frame. if (ratecontrol_group_packets_sent >= ratecontrol_packets_in_1ms || ratecontrol_frame_packets_sent == 0) { auto due = ratecontrol_frame_start + std::chrono::duration_cast(1ms) * ratecontrol_frame_packets_sent / ratecontrol_packets_in_1ms; auto now = std::chrono::steady_clock::now(); if (now < due) { timer->sleep_for(due - now); } ratecontrol_group_packets_sent = 0; } size_t current_batch_size = x - next_shard_to_send + 1; batch_info.block_offset = next_shard_to_send; batch_info.block_count = current_batch_size; frame_send_batch_latency_logger.first_point_now(); // Use a batched send if it's supported on this platform if (!platf::send_batch(batch_info)) { // Batched send is not available, so send each packet individually BOOST_LOG(verbose) << "Falling back to unbatched send"sv; for (auto y = 0; y < current_batch_size; y++) { auto send_info = platf::send_info_t { shards.prefix(next_shard_to_send + y), shards.prefixsize, shards.data(next_shard_to_send + y), shards.blocksize, (uintptr_t) sock.native_handle(), peer_address, session->video.peer.port(), session->localAddress, }; platf::send(send_info); } } frame_send_batch_latency_logger.second_point_now_and_log(); ratecontrol_group_packets_sent += current_batch_size; ratecontrol_frame_packets_sent += current_batch_size; next_shard_to_send = x + 1; } } // remember this in case the next frame comes immediately ratecontrol_next_frame_start = ratecontrol_frame_start + std::chrono::duration_cast(1ms) * ratecontrol_frame_packets_sent / ratecontrol_packets_in_1ms; frame_network_latency_logger.second_point_now_and_log(); BOOST_LOG(verbose) << "Sent Frame seq ["sv << packet->frame_index() << "] pts ["sv << timestamp << "] shards ["sv << shards.size() << "/"sv << shards.percentage << "%]"sv << (frame_is_dupe ? " Dupe" : "") << (packet->is_idr() ? " Key" : "") << (packet->after_ref_frame_invalidation ? " RFI" : ""); ++blockIndex; lowseq += shards.size(); }); session->video.lowseq = lowseq; } catch (const std::exception &e) { BOOST_LOG(error) << "Broadcast video failed "sv << e.what(); std::this_thread::sleep_for(100ms); } } shutdown_event->raise(true); } void audioBroadcastThread(udp::socket &sock) { auto shutdown_event = mail::man->event(mail::broadcast_shutdown); auto packets = mail::man->queue(mail::audio_packets); audio_packet_t audio_packet; fec::rs_t rs {reed_solomon_new(RTPA_DATA_SHARDS, RTPA_FEC_SHARDS)}; crypto::aes_t iv(16); // For unknown reasons, the RS parity matrix computed by our RS implementation // doesn't match the one Nvidia uses for audio data. I'm not exactly sure why, // but we can simply replace it with the matrix generated by OpenFEC which // works correctly. This is possible because the data and FEC shard count is // constant and known in advance. const unsigned char parity[] = {0x77, 0x40, 0x38, 0x0e, 0xc7, 0xa7, 0x0d, 0x6c}; memcpy(rs.get()->p, parity, sizeof(parity)); audio_packet.rtp.header = 0x80; audio_packet.rtp.packetType = 97; audio_packet.rtp.ssrc = 0; // Audio traffic is sent on this thread platf::adjust_thread_priority(platf::thread_priority_e::high); while (auto packet = packets->pop()) { if (shutdown_event->peek()) { break; } TUPLE_2D_REF(channel_data, packet_data, *packet); auto session = (session_t *) channel_data; auto sequenceNumber = session->audio.sequenceNumber; auto timestamp = session->audio.timestamp; *(std::uint32_t *) iv.data() = util::endian::big(session->audio.avRiKeyId + sequenceNumber); auto &shards_p = session->audio.shards_p; auto bytes = encode_audio(session->config.encryptionFlagsEnabled & SS_ENC_AUDIO, packet_data, shards_p[sequenceNumber % RTPA_DATA_SHARDS], iv, session->audio.cipher); if (bytes < 0) { BOOST_LOG(error) << "Couldn't encode audio packet"sv; break; } BOOST_LOG(verbose) << "Audio [seq "sv << sequenceNumber << ", pts "sv << timestamp << "] :: send..."sv; audio_packet.rtp.sequenceNumber = util::endian::big(sequenceNumber); audio_packet.rtp.timestamp = util::endian::big(timestamp); session->audio.sequenceNumber++; session->audio.timestamp += session->config.audio.packetDuration; auto peer_address = session->audio.peer.address(); try { auto send_info = platf::send_info_t { (const char *) &audio_packet, sizeof(audio_packet), (const char *) shards_p[sequenceNumber % RTPA_DATA_SHARDS], (size_t) bytes, (uintptr_t) sock.native_handle(), peer_address, session->audio.peer.port(), session->localAddress, }; platf::send(send_info); auto &fec_packet = session->audio.fec_packet; // initialize the FEC header at the beginning of the FEC block if (sequenceNumber % RTPA_DATA_SHARDS == 0) { fec_packet.fecHeader.baseSequenceNumber = util::endian::big(sequenceNumber); fec_packet.fecHeader.baseTimestamp = util::endian::big(timestamp); } // generate parity shards at the end of the FEC block if ((sequenceNumber + 1) % RTPA_DATA_SHARDS == 0) { reed_solomon_encode(rs.get(), shards_p.begin(), RTPA_TOTAL_SHARDS, bytes); for (auto x = 0; x < RTPA_FEC_SHARDS; ++x) { fec_packet.rtp.sequenceNumber = util::endian::big(sequenceNumber + x + 1); fec_packet.fecHeader.fecShardIndex = x; auto send_info = platf::send_info_t { (const char *) &fec_packet, sizeof(fec_packet), (const char *) shards_p[RTPA_DATA_SHARDS + x], (size_t) bytes, (uintptr_t) sock.native_handle(), peer_address, session->audio.peer.port(), session->localAddress, }; platf::send(send_info); BOOST_LOG(verbose) << "Audio FEC ["sv << (sequenceNumber & ~(RTPA_DATA_SHARDS - 1)) << ' ' << x << "] :: send..."sv; } } } catch (const std::exception &e) { BOOST_LOG(error) << "Broadcast audio failed "sv << e.what(); std::this_thread::sleep_for(100ms); } } shutdown_event->raise(true); } int start_broadcast(broadcast_ctx_t &ctx) { auto address_family = net::af_from_enum_string(config::sunshine.address_family); auto protocol = address_family == net::IPV4 ? udp::v4() : udp::v6(); auto control_port = net::map_port(CONTROL_PORT); auto video_port = net::map_port(VIDEO_STREAM_PORT); auto audio_port = net::map_port(AUDIO_STREAM_PORT); if (ctx.control_server.bind(address_family, control_port)) { BOOST_LOG(error) << "Couldn't bind Control server to port ["sv << control_port << "], likely another process already bound to the port"sv; return -1; } boost::system::error_code ec; ctx.video_sock.open(protocol, ec); if (ec) { BOOST_LOG(fatal) << "Couldn't open socket for Video server: "sv << ec.message(); return -1; } // Set video socket send buffer size (SO_SENDBUF) to 1MB try { ctx.video_sock.set_option(boost::asio::socket_base::send_buffer_size(1024 * 1024)); } catch (...) { BOOST_LOG(error) << "Failed to set video socket send buffer size (SO_SENDBUF)"; } ctx.video_sock.bind(udp::endpoint(protocol, video_port), ec); if (ec) { BOOST_LOG(fatal) << "Couldn't bind Video server to port ["sv << video_port << "]: "sv << ec.message(); return -1; } ctx.audio_sock.open(protocol, ec); if (ec) { BOOST_LOG(fatal) << "Couldn't open socket for Audio server: "sv << ec.message(); return -1; } ctx.audio_sock.bind(udp::endpoint(protocol, audio_port), ec); if (ec) { BOOST_LOG(fatal) << "Couldn't bind Audio server to port ["sv << audio_port << "]: "sv << ec.message(); return -1; } ctx.message_queue_queue = std::make_shared(30); ctx.video_thread = std::thread {videoBroadcastThread, std::ref(ctx.video_sock)}; ctx.audio_thread = std::thread {audioBroadcastThread, std::ref(ctx.audio_sock)}; ctx.control_thread = std::thread {controlBroadcastThread, &ctx.control_server}; ctx.recv_thread = std::thread {recvThread, std::ref(ctx)}; return 0; } void end_broadcast(broadcast_ctx_t &ctx) { auto broadcast_shutdown_event = mail::man->event(mail::broadcast_shutdown); broadcast_shutdown_event->raise(true); auto video_packets = mail::man->queue(mail::video_packets); auto audio_packets = mail::man->queue(mail::audio_packets); // Minimize delay stopping video/audio threads video_packets->stop(); audio_packets->stop(); ctx.message_queue_queue->stop(); ctx.io_context.stop(); ctx.video_sock.close(); ctx.audio_sock.close(); video_packets.reset(); audio_packets.reset(); BOOST_LOG(debug) << "Waiting for main listening thread to end..."sv; ctx.recv_thread.join(); BOOST_LOG(debug) << "Waiting for main video thread to end..."sv; ctx.video_thread.join(); BOOST_LOG(debug) << "Waiting for main audio thread to end..."sv; ctx.audio_thread.join(); BOOST_LOG(debug) << "Waiting for main control thread to end..."sv; ctx.control_thread.join(); BOOST_LOG(debug) << "All broadcasting threads ended"sv; broadcast_shutdown_event->reset(); } int recv_ping(session_t *session, decltype(broadcast)::ptr_t ref, socket_e type, std::string_view expected_payload, udp::endpoint &peer, std::chrono::milliseconds timeout) { auto messages = std::make_shared(30); av_session_id_t session_id = std::string {expected_payload}; // Only allow matches on the peer address for legacy clients if (!(session->config.mlFeatureFlags & ML_FF_SESSION_ID_V1)) { ref->message_queue_queue->raise(type, peer.address(), messages); } ref->message_queue_queue->raise(type, session_id, messages); auto fg = util::fail_guard([&]() { messages->stop(); // remove message queue from session if (!(session->config.mlFeatureFlags & ML_FF_SESSION_ID_V1)) { ref->message_queue_queue->raise(type, peer.address(), nullptr); } ref->message_queue_queue->raise(type, session_id, nullptr); }); auto start_time = std::chrono::steady_clock::now(); auto current_time = start_time; while (current_time - start_time < config::stream.ping_timeout) { auto delta_time = current_time - start_time; auto msg_opt = messages->pop(config::stream.ping_timeout - delta_time); if (!msg_opt) { break; } TUPLE_2D_REF(recv_peer, msg, *msg_opt); if (msg.find(expected_payload) != std::string::npos) { // Match the new PING payload format BOOST_LOG(debug) << "Received ping [v2] from "sv << recv_peer.address() << ':' << recv_peer.port() << " ["sv << util::hex_vec(msg) << ']'; } else if (!(session->config.mlFeatureFlags & ML_FF_SESSION_ID_V1) && msg == "PING"sv) { // Match the legacy fixed PING payload only if the new type is not supported BOOST_LOG(debug) << "Received ping [v1] from "sv << recv_peer.address() << ':' << recv_peer.port() << " ["sv << util::hex_vec(msg) << ']'; } else { BOOST_LOG(debug) << "Received non-ping from "sv << recv_peer.address() << ':' << recv_peer.port() << " ["sv << util::hex_vec(msg) << ']'; current_time = std::chrono::steady_clock::now(); continue; } // Update connection details. peer = recv_peer; return 0; } BOOST_LOG(error) << "Initial Ping Timeout"sv; return -1; } void videoThread(session_t *session) { auto fg = util::fail_guard([&]() { session::stop(*session); }); while_starting_do_nothing(session->state); auto ref = broadcast.ref(); auto error = recv_ping(session, ref, socket_e::video, session->video.ping_payload, session->video.peer, config::stream.ping_timeout); if (error < 0) { return; } // Enable local prioritization and QoS tagging on video traffic if requested by the client auto address = session->video.peer.address(); session->video.qos = platf::enable_socket_qos(ref->video_sock.native_handle(), address, session->video.peer.port(), platf::qos_data_type_e::video, session->config.videoQosType != 0); BOOST_LOG(debug) << "Start capturing Video"sv; video::capture(session->mail, session->config.monitor, session); } void audioThread(session_t *session) { auto fg = util::fail_guard([&]() { session::stop(*session); }); while_starting_do_nothing(session->state); auto ref = broadcast.ref(); auto error = recv_ping(session, ref, socket_e::audio, session->audio.ping_payload, session->audio.peer, config::stream.ping_timeout); if (error < 0) { return; } // Enable local prioritization and QoS tagging on audio traffic if requested by the client auto address = session->audio.peer.address(); session->audio.qos = platf::enable_socket_qos(ref->audio_sock.native_handle(), address, session->audio.peer.port(), platf::qos_data_type_e::audio, session->config.audioQosType != 0); BOOST_LOG(debug) << "Start capturing Audio"sv; audio::capture(session->mail, session->config.audio, session); } namespace session { std::atomic_uint running_sessions; state_e state(session_t &session) { return session.state.load(std::memory_order_relaxed); } inline bool send(session_t& session, const std::string_view &payload) { return session.broadcast_ref->control_server.send(payload, session.control.peer); } std::string uuid(const session_t& session) { return session.device_uuid; } bool uuid_match(const session_t &session, const std::string_view& uuid) { return session.device_uuid == uuid; } bool update_device_info(session_t& session, const std::string& name, const crypto::PERM& newPerm) { session.permission = newPerm; if (!(newPerm & crypto::PERM::_allow_view)) { BOOST_LOG(debug) << "Session: View permission revoked for [" << session.device_name << "], disconnecting..."; graceful_stop(session); return true; } BOOST_LOG(debug) << "Session: Permission updated for [" << session.device_name << "]"; if (session.device_name != name) { BOOST_LOG(debug) << "Session: Device name changed from [" << session.device_name << "] to [" << name << "]"; session.device_name = name; } return false; } void stop(session_t &session) { while_starting_do_nothing(session.state); auto expected = state_e::RUNNING; auto already_stopping = !session.state.compare_exchange_strong(expected, state_e::STOPPING); if (already_stopping) { return; } session.shutdown_event->raise(true); } void graceful_stop(session_t& session) { while_starting_do_nothing(session.state); auto expected = state_e::RUNNING; auto already_stopping = !session.state.compare_exchange_strong(expected, state_e::STOPPING); if (already_stopping) { return; } // reason: graceful termination std::uint32_t reason = 0x80030023; control_terminate_t plaintext; plaintext.header.type = packetTypes[IDX_TERMINATION]; plaintext.header.payloadLength = sizeof(plaintext.ec); plaintext.ec = util::endian::big(reason); // We may not have gotten far enough to have an ENet connection yet if (session.control.peer) { std::array encrypted_payload; auto payload = stream::encode_control(&session, util::view(plaintext), encrypted_payload); if (send(session, payload)) { TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *) &session.control.peer->address.address)); BOOST_LOG(warning) << "Couldn't send termination code to ["sv << addr << ':' << port << ']'; } } session.shutdown_event->raise(true); session.controlEnd.raise(true); } void join(session_t &session) { // Current Nvidia drivers have a bug where NVENC can deadlock the encoder thread with hardware-accelerated // GPU scheduling enabled. If this happens, we will terminate ourselves and the service can restart. // The alternative is that Sunshine can never start another session until it's manually restarted. auto task = []() { BOOST_LOG(fatal) << "Hang detected! Session failed to terminate in 10 seconds."sv; logging::log_flush(); lifetime::debug_trap(); }; auto force_kill = task_pool.pushDelayed(task, 10s).task_id; auto fg = util::fail_guard([&force_kill]() { // Cancel the kill task if we manage to return from this function task_pool.cancel(force_kill); }); BOOST_LOG(debug) << "Waiting for video to end..."sv; session.videoThread.join(); BOOST_LOG(debug) << "Waiting for audio to end..."sv; session.audioThread.join(); BOOST_LOG(debug) << "Waiting for control to end..."sv; session.controlEnd.view(); // Reset input on session stop to avoid stuck repeated keys BOOST_LOG(debug) << "Resetting Input..."sv; input::reset(session.input); if (!session.undo_cmds.empty()) { auto exec_thread = std::thread([cmd_list = session.undo_cmds]{ for (auto &cmd : cmd_list) { std::error_code ec; auto env = proc::proc.get_env(); boost::filesystem::path working_dir = proc::find_working_directory(cmd.cmd, env); auto child = platf::run_command(cmd.elevated, true, cmd.cmd, working_dir, env, nullptr, ec, nullptr); BOOST_LOG(info) << "Spawning client undo command ["sv << cmd.cmd << "] in ["sv << working_dir << ']'; if (ec) { BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd.cmd << "]: System: "sv << ec.message(); } else { child.detach(); } } }); exec_thread.detach(); } // If this is the last session, invoke the platform callbacks if (--running_sessions == 0) { bool revert_display_config {config::video.dd.config_revert_on_disconnect}; if (proc::proc.running()) { proc::proc.pause(); } else { // We have no app running and also no clients anymore. revert_display_config = true; } if (revert_display_config) { display_device::revert_configuration(); } platf::streaming_will_stop(); } BOOST_LOG(debug) << "Session ended"sv; } int start(session_t &session, const std::string &addr_string) { session.input = input::alloc(session.mail); session.broadcast_ref = broadcast.ref(); if (!session.broadcast_ref) { return -1; } session.control.expected_peer_address = addr_string; BOOST_LOG(debug) << "Expecting incoming session connections from "sv << addr_string; // Insert this session into the session list { auto lg = session.broadcast_ref->control_server._sessions.lock(); session.broadcast_ref->control_server._sessions->push_back(&session); } auto addr = boost::asio::ip::make_address(addr_string); session.video.peer.address(addr); session.video.peer.port(0); session.audio.peer.address(addr); session.audio.peer.port(0); session.pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout; session.audioThread = std::thread {audioThread, &session}; session.videoThread = std::thread {videoThread, &session}; session.state.store(state_e::RUNNING, std::memory_order_relaxed); // If this is the first session, invoke the platform callbacks if (++running_sessions == 1) { platf::streaming_will_start(); proc::proc.resume(); } if (!session.do_cmds.empty()) { auto exec_thread = std::thread([cmd_list = session.do_cmds]{ for (auto &cmd : cmd_list) { std::error_code ec; auto env = proc::proc.get_env(); boost::filesystem::path working_dir = proc::find_working_directory(cmd.cmd, env); auto child = platf::run_command(cmd.elevated, true, cmd.cmd, working_dir, env, nullptr, ec, nullptr); BOOST_LOG(info) << "Spawning client do command ["sv << cmd.cmd << "] in ["sv << working_dir << ']'; if (ec) { BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd.cmd << "]: System: "sv << ec.message(); } else { child.detach(); } } }); exec_thread.detach(); } return 0; } std::shared_ptr alloc(config_t &config, rtsp_stream::launch_session_t &launch_session) { auto session = std::make_shared(); auto mail = std::make_shared(); session->shutdown_event = mail->event(mail::shutdown); session->launch_session_id = launch_session.id; session->device_name = launch_session.device_name; session->device_uuid = launch_session.unique_id; session->permission = launch_session.perm; session->do_cmds = std::move(launch_session.client_do_cmds); session->undo_cmds = std::move(launch_session.client_undo_cmds); session->config = config; session->control.connect_data = launch_session.control_connect_data; session->control.feedback_queue = mail->queue(mail::gamepad_feedback); session->control.hdr_queue = mail->event(mail::hdr); session->control.legacy_input_enc_iv = launch_session.iv; session->control.cipher = crypto::cipher::gcm_t { launch_session.gcm_key, false }; session->video.idr_events = mail->event(mail::idr); session->video.invalidate_ref_frames_events = mail->event>(mail::invalidate_ref_frames); session->video.lowseq = 0; session->video.ping_payload = launch_session.av_ping_payload; if (config.encryptionFlagsEnabled & SS_ENC_VIDEO) { BOOST_LOG(info) << "Video encryption enabled"sv; session->video.cipher = crypto::cipher::gcm_t { launch_session.gcm_key, false }; session->video.gcm_iv_counter = 0; } constexpr auto max_block_size = crypto::cipher::round_to_pkcs7_padded(2048); util::buffer_t shards {RTPA_TOTAL_SHARDS * max_block_size}; util::buffer_t shards_p {RTPA_TOTAL_SHARDS}; for (auto x = 0; x < RTPA_TOTAL_SHARDS; ++x) { shards_p[x] = (uint8_t *) &shards[x * max_block_size]; } // Audio FEC spans multiple audio packets, // therefore its session specific session->audio.shards = std::move(shards); session->audio.shards_p = std::move(shards_p); session->audio.fec_packet.rtp.header = 0x80; session->audio.fec_packet.rtp.packetType = 127; session->audio.fec_packet.rtp.timestamp = 0; session->audio.fec_packet.rtp.ssrc = 0; session->audio.fec_packet.fecHeader.payloadType = 97; session->audio.fec_packet.fecHeader.ssrc = 0; session->audio.cipher = crypto::cipher::cbc_t { launch_session.gcm_key, true }; session->audio.ping_payload = launch_session.av_ping_payload; session->audio.avRiKeyId = util::endian::big(*(std::uint32_t *) launch_session.iv.data()); session->audio.sequenceNumber = 0; session->audio.timestamp = 0; session->control.peer = nullptr; session->state.store(state_e::STOPPED, std::memory_order_relaxed); session->mail = std::move(mail); return session; } } // namespace session } // namespace stream ================================================ FILE: src/stream.h ================================================ /** * @file src/stream.h * @brief Declarations for the streaming protocols. */ #pragma once // standard includes #include // lib includes #include // local includes #include "audio.h" #include "crypto.h" #include "video.h" namespace stream { constexpr auto VIDEO_STREAM_PORT = 9; constexpr auto CONTROL_PORT = 10; constexpr auto AUDIO_STREAM_PORT = 11; struct session_t; struct config_t { audio::config_t audio; video::config_t monitor; int packetsize; int minRequiredFecPackets; int mlFeatureFlags; int controlProtocolType; int audioQosType; int videoQosType; uint32_t encryptionFlagsEnabled; std::optional gcmap; }; namespace session { enum class state_e : int { STOPPED, ///< The session is stopped STOPPING, ///< The session is stopping STARTING, ///< The session is starting RUNNING, ///< The session is running }; std::shared_ptr alloc(config_t &config, rtsp_stream::launch_session_t &launch_session); std::string uuid(const session_t& session); bool uuid_match(const session_t& session, const std::string_view& uuid); bool update_device_info(session_t& session, const std::string& name, const crypto::PERM& newPerm); int start(session_t &session, const std::string &addr_string); void stop(session_t &session); void graceful_stop(session_t& session); void join(session_t &session); state_e state(session_t &session); inline bool send(session_t& session, const std::string_view &payload); } // namespace session } // namespace stream ================================================ FILE: src/sync.h ================================================ /** * @file src/sync.h * @brief Declarations for synchronization utilities. */ #pragma once // standard includes #include #include #include namespace sync_util { template class sync_t { public: using value_t = T; using mutex_t = M; std::lock_guard lock() { return std::lock_guard {_lock}; } template sync_t(Args &&...args): raw {std::forward(args)...} { } sync_t &operator=(sync_t &&other) noexcept { std::lock(_lock, other._lock); raw = std::move(other.raw); _lock.unlock(); other._lock.unlock(); return *this; } sync_t &operator=(sync_t &other) noexcept { std::lock(_lock, other._lock); raw = other.raw; _lock.unlock(); other._lock.unlock(); return *this; } template sync_t &operator=(V &&val) { auto lg = lock(); raw = val; return *this; } sync_t &operator=(const value_t &val) noexcept { auto lg = lock(); raw = val; return *this; } sync_t &operator=(value_t &&val) noexcept { auto lg = lock(); raw = std::move(val); return *this; } value_t *operator->() { return &raw; } value_t &operator*() { return raw; } const value_t &operator*() const { return raw; } value_t raw; private: mutex_t _lock; }; } // namespace sync_util ================================================ FILE: src/system_tray.cpp ================================================ /** * @file src/system_tray.cpp * @brief Definitions for the system tray icon and notification system. */ // macros #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN #include #include #include "platform/windows/utils.h" #define TRAY_ICON WEB_DIR "images/apollo.ico" #define TRAY_ICON_PLAYING WEB_DIR "images/apollo-playing.ico" #define TRAY_ICON_PAUSING WEB_DIR "images/apollo-pausing.ico" #define TRAY_ICON_LOCKED WEB_DIR "images/apollo-locked.ico" #elif defined(__linux__) || defined(linux) || defined(__linux) #define TRAY_ICON SUNSHINE_TRAY_PREFIX "-tray" #define TRAY_ICON_PLAYING SUNSHINE_TRAY_PREFIX "-playing" #define TRAY_ICON_PAUSING SUNSHINE_TRAY_PREFIX "-pausing" #define TRAY_ICON_LOCKED SUNSHINE_TRAY_PREFIX "-locked" #elif defined(__APPLE__) || defined(__MACH__) #define TRAY_ICON WEB_DIR "images/logo-apollo-16.png" #define TRAY_ICON_PLAYING WEB_DIR "images/apollo-playing-16.png" #define TRAY_ICON_PAUSING WEB_DIR "images/apollo-pausing-16.png" #define TRAY_ICON_LOCKED WEB_DIR "images/apollo-locked-16.png" #include #endif #define TRAY_MSG_NO_APP_RUNNING "Reload Apps" #ifndef BOOST_PROCESS_VERSION #define BOOST_PROCESS_VERSION 1 #endif // standard includes #include #include #include #include #include #include // lib includes #include #include #include // local includes #include "config.h" #include "confighttp.h" #include "display_device.h" #include "logging.h" #include "platform/common.h" #include "process.h" #include "network.h" #include "src/entry_handler.h" using namespace std::literals; // system_tray namespace namespace system_tray { static std::atomic tray_initialized = false; // Threading variables for all platforms static std::thread tray_thread; static std::atomic tray_thread_running = false; static std::atomic tray_thread_should_exit = false; void tray_open_ui_cb([[maybe_unused]] struct tray_menu *item) { BOOST_LOG(info) << "Opening UI from system tray"sv; launch_ui(); } void tray_force_stop_cb(struct tray_menu *item) { BOOST_LOG(info) << "Force stop from system tray"sv; proc::proc.terminate(); } void tray_reset_display_device_config_cb([[maybe_unused]] struct tray_menu *item) { BOOST_LOG(info) << "Resetting display device config from system tray"sv; std::ignore = display_device::reset_persistence(); } void tray_restart_cb([[maybe_unused]] struct tray_menu *item) { BOOST_LOG(info) << "Restarting from system tray"sv; proc::proc.terminate(); platf::restart(); } void tray_quit_cb([[maybe_unused]] struct tray_menu *item) { BOOST_LOG(info) << "Quitting from system tray"sv; proc::proc.terminate(); #ifdef _WIN32 // If we're running in a service, return a special status to // tell it to terminate too, otherwise it will just respawn us. if (GetConsoleWindow() == nullptr) { lifetime::exit_sunshine(ERROR_SHUTDOWN_IN_PROGRESS, true); return; } #endif lifetime::exit_sunshine(0, true); } // Tray menu static struct tray tray = { .icon = TRAY_ICON, .tooltip = PROJECT_NAME, .menu = (struct tray_menu[]) { // todo - use boost/locale to translate menu strings { .text = "Open Apollo", .cb = tray_open_ui_cb }, { .text = "-" }, // { .text = "-" }, // { .text = "Donate", // .submenu = // (struct tray_menu[]) { // { .text = "GitHub Sponsors", .cb = tray_donate_github_cb }, // { .text = "MEE6", .cb = tray_donate_mee6_cb }, // { .text = "Patreon", .cb = tray_donate_patreon_cb }, // { .text = "PayPal", .cb = tray_donate_paypal_cb }, // { .text = nullptr } } }, // { .text = "-" }, { .text = TRAY_MSG_NO_APP_RUNNING, .cb = tray_force_stop_cb }, // Currently display device settings are only supported on Windows #ifdef _WIN32 {.text = "Reset Display Device Config", .cb = tray_reset_display_device_config_cb}, #endif {.text = "Restart", .cb = tray_restart_cb}, {.text = "Quit", .cb = tray_quit_cb}, {.text = nullptr} }, .iconPathCount = 4, .allIconPaths = {TRAY_ICON, TRAY_ICON_LOCKED, TRAY_ICON_PLAYING, TRAY_ICON_PAUSING}, }; int init_tray() { #ifdef _WIN32 // If we're running as SYSTEM, Explorer.exe will not have permission to open our thread handle // to monitor for thread termination. If Explorer fails to open our thread, our tray icon // will persist forever if we terminate unexpectedly. To avoid this, we will modify our thread // DACL to add an ACE that allows SYNCHRONIZE access to Everyone. { PACL old_dacl; PSECURITY_DESCRIPTOR sd; auto error = GetSecurityInfo(GetCurrentThread(), SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, &old_dacl, nullptr, &sd); if (error != ERROR_SUCCESS) { BOOST_LOG(warning) << "GetSecurityInfo() failed: "sv << error; return 1; } auto free_sd = util::fail_guard([sd]() { LocalFree(sd); }); SID_IDENTIFIER_AUTHORITY sid_authority = SECURITY_WORLD_SID_AUTHORITY; PSID world_sid; if (!AllocateAndInitializeSid(&sid_authority, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &world_sid)) { error = GetLastError(); BOOST_LOG(warning) << "AllocateAndInitializeSid() failed: "sv << error; return 1; } auto free_sid = util::fail_guard([world_sid]() { FreeSid(world_sid); }); EXPLICIT_ACCESS ea {}; ea.grfAccessPermissions = SYNCHRONIZE; ea.grfAccessMode = GRANT_ACCESS; ea.grfInheritance = NO_INHERITANCE; ea.Trustee.TrusteeForm = TRUSTEE_IS_SID; ea.Trustee.ptstrName = (LPSTR) world_sid; PACL new_dacl; error = SetEntriesInAcl(1, &ea, old_dacl, &new_dacl); if (error != ERROR_SUCCESS) { BOOST_LOG(warning) << "SetEntriesInAcl() failed: "sv << error; return 1; } auto free_new_dacl = util::fail_guard([new_dacl]() { LocalFree(new_dacl); }); error = SetSecurityInfo(GetCurrentThread(), SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, new_dacl, nullptr); if (error != ERROR_SUCCESS) { BOOST_LOG(warning) << "SetSecurityInfo() failed: "sv << error; return 1; } } // Wait for the shell to be initialized before registering the tray icon. // This ensures the tray icon works reliably after a logoff/logon cycle. while (GetShellWindow() == nullptr) { Sleep(1000); } #endif if (tray_init(&tray) < 0) { BOOST_LOG(warning) << "Failed to create system tray"sv; return 1; } BOOST_LOG(info) << "System tray created"sv; tray_initialized = true; return 0; } int process_tray_events() { if (!tray_initialized) { return 1; } // Process one iteration of the tray loop with non-blocking mode (0) if (const int result = tray_loop(0); result != 0) { BOOST_LOG(warning) << "System tray loop failed"sv; return result; } return 0; } int end_tray() { if (tray_initialized) { tray_initialized = false; tray_exit(); } return 0; } void update_tray_playing(std::string app_name) { if (!tray_initialized) { return; } tray.notification_title = nullptr; tray.notification_text = nullptr; tray.notification_cb = nullptr; tray.notification_icon = nullptr; tray.icon = TRAY_ICON_PLAYING; tray_update(&tray); tray.icon = TRAY_ICON_PLAYING; tray.notification_title = "App launched"; char msg[256]; static char force_close_msg[256]; snprintf(msg, std::size(msg), "%s launched.", app_name.c_str()); snprintf(force_close_msg, std::size(force_close_msg), "Force close [%s]", app_name.c_str()); #ifdef _WIN32 strncpy(msg, utf8ToAcp(msg).c_str(), std::size(msg) - 1); strncpy(force_close_msg, utf8ToAcp(force_close_msg).c_str(), std::size(force_close_msg) - 1); #endif tray.notification_text = msg; tray.notification_icon = TRAY_ICON_PLAYING; tray.tooltip = PROJECT_NAME; tray.menu[2].text = force_close_msg; tray_update(&tray); } void update_tray_pausing(std::string app_name) { if (!tray_initialized) { return; } tray.notification_title = nullptr; tray.notification_text = nullptr; tray.notification_cb = nullptr; tray.notification_icon = nullptr; tray.icon = TRAY_ICON_PAUSING; tray_update(&tray); char msg[256]; snprintf(msg, std::size(msg), "Streaming paused for %s", app_name.c_str()); #ifdef _WIN32 strncpy(msg, utf8ToAcp(msg).c_str(), std::size(msg) - 1); #endif tray.icon = TRAY_ICON_PAUSING; tray.notification_title = "Stream Paused"; tray.notification_text = msg; tray.notification_icon = TRAY_ICON_PAUSING; tray.tooltip = PROJECT_NAME; tray_update(&tray); } void update_tray_stopped(std::string app_name) { if (!tray_initialized) { return; } tray.notification_title = nullptr; tray.notification_text = nullptr; tray.notification_cb = nullptr; tray.notification_icon = nullptr; tray.icon = TRAY_ICON; tray_update(&tray); char msg[256]; snprintf(msg, std::size(msg), "Streaming stopped for %s", app_name.c_str()); #ifdef _WIN32 strncpy(msg, utf8ToAcp(msg).c_str(), std::size(msg) - 1); #endif tray.icon = TRAY_ICON; tray.notification_icon = TRAY_ICON; tray.notification_title = "Application Stopped"; tray.notification_text = msg; tray.tooltip = PROJECT_NAME; tray.menu[2].text = TRAY_MSG_NO_APP_RUNNING; tray_update(&tray); } void update_tray_launch_error(std::string app_name, int exit_code) { if (!tray_initialized) { return; } tray.notification_title = NULL; tray.notification_text = NULL; tray.notification_cb = NULL; tray.notification_icon = NULL; tray.icon = TRAY_ICON; tray_update(&tray); char msg[256]; snprintf(msg, std::size(msg), "Application %s exited too fast with code %d. Click here to terminate the stream.", app_name.c_str(), exit_code); #ifdef _WIN32 strncpy(msg, utf8ToAcp(msg).c_str(), std::size(msg) - 1); #endif tray.icon = TRAY_ICON; tray.notification_icon = TRAY_ICON; tray.notification_title = "Launch Error"; tray.notification_text = msg; tray.notification_cb = []() { BOOST_LOG(info) << "Force stop from notification"sv; proc::proc.terminate(); }; tray.tooltip = PROJECT_NAME; tray_update(&tray); } void update_tray_require_pin() { if (!tray_initialized) { return; } tray.notification_title = nullptr; tray.notification_text = nullptr; tray.notification_cb = nullptr; tray.notification_icon = nullptr; tray.icon = TRAY_ICON; tray_update(&tray); tray.icon = TRAY_ICON; tray.notification_title = "Incoming Pairing Request"; tray.notification_text = "Click here to complete the pairing process"; tray.notification_icon = TRAY_ICON_LOCKED; tray.tooltip = PROJECT_NAME; tray.notification_cb = []() { launch_ui("/pin#PIN"); }; tray_update(&tray); } void update_tray_paired(std::string device_name) { if (!tray_initialized) { return; } tray.notification_title = NULL; tray.notification_text = NULL; tray.notification_cb = NULL; tray.notification_icon = NULL; tray_update(&tray); char msg[256]; snprintf(msg, std::size(msg), "Device %s paired Succesfully. Please make sure you have access to the device.", device_name.c_str()); #ifdef _WIN32 strncpy(msg, utf8ToAcp(msg).c_str(), std::size(msg) - 1); #endif tray.notification_title = "Device Paired Succesfully"; tray.notification_text = msg; tray.notification_icon = TRAY_ICON; tray.tooltip = PROJECT_NAME; tray_update(&tray); } void update_tray_client_connected(std::string client_name) { if (!tray_initialized) { return; } tray.notification_title = NULL; tray.notification_text = NULL; tray.notification_cb = NULL; tray.notification_icon = NULL; tray.icon = TRAY_ICON; tray_update(&tray); char msg[256]; snprintf(msg, std::size(msg), "%s has connected to the session.", client_name.c_str()); #ifdef _WIN32 strncpy(msg, utf8ToAcp(msg).c_str(), std::size(msg) - 1); #endif tray.notification_title = "Client Connected"; tray.notification_text = msg; tray.notification_icon = TRAY_ICON; tray.tooltip = PROJECT_NAME; tray_update(&tray); } // Threading functions available on all platforms static void tray_thread_worker() { BOOST_LOG(info) << "System tray thread started"sv; // Initialize the tray in this thread if (init_tray() != 0) { BOOST_LOG(error) << "Failed to initialize tray in thread"sv; tray_thread_running = false; return; } tray_thread_running = true; // Main tray event loop while (!tray_thread_should_exit) { if (process_tray_events() != 0) { BOOST_LOG(warning) << "Tray event processing failed in thread"sv; break; } // Sleep to avoid busy waiting std::this_thread::sleep_for(std::chrono::milliseconds(50)); } // Clean up the tray end_tray(); tray_thread_running = false; BOOST_LOG(info) << "System tray thread ended"sv; } int init_tray_threaded() { if (tray_thread_running) { BOOST_LOG(warning) << "Tray thread is already running"sv; return 1; } #ifdef _WIN32 std::string tmp_str = "Open Apollo (" + config::nvhttp.sunshine_name + ":" + std::to_string(net::map_port(confighttp::PORT_HTTPS)) + ")"; static const std::string title_str = utf8ToAcp(tmp_str); #else static const std::string title_str = "Open Apollo (" + config::nvhttp.sunshine_name + ":" + std::to_string(net::map_port(confighttp::PORT_HTTPS)) + ")"; #endif tray.menu[0].text = title_str.c_str(); if (config::sunshine.hide_tray_controls) { tray.menu[1].text = nullptr; } tray_thread_should_exit = false; try { tray_thread = std::thread(tray_thread_worker); // Wait for the thread to start and initialize const auto start_time = std::chrono::steady_clock::now(); while (!tray_thread_running && !tray_thread_should_exit) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); // Timeout after 10 seconds if (std::chrono::steady_clock::now() - start_time > std::chrono::seconds(10)) { BOOST_LOG(error) << "Tray thread initialization timeout"sv; tray_thread_should_exit = true; if (tray_thread.joinable()) { tray_thread.join(); } return 1; } } if (!tray_thread_running) { BOOST_LOG(error) << "Tray thread failed to start"sv; if (tray_thread.joinable()) { tray_thread.join(); } return 1; } BOOST_LOG(info) << "System tray thread initialized successfully"sv; return 0; } catch (const std::exception &e) { BOOST_LOG(error) << "Failed to create tray thread: " << e.what(); return 1; } } int end_tray_threaded() { if (!tray_thread_running) { return 0; } BOOST_LOG(info) << "Stopping system tray thread"sv; tray_thread_should_exit = true; if (tray_thread.joinable()) { tray_thread.join(); } BOOST_LOG(info) << "System tray thread stopped"sv; return 0; } } // namespace system_tray #ifdef BOOST_PROCESS_VERSION #undef BOOST_PROCESS_VERSION 1 #endif #endif ================================================ FILE: src/system_tray.h ================================================ /** * @file src/system_tray.h * @brief Declarations for the system tray icon and notification system. */ #pragma once /** * @brief Handles the system tray icon and notification system. */ namespace system_tray { /** * @brief Callback for opening the UI from the system tray. * @param item The tray menu item. */ void tray_open_ui_cb([[maybe_unused]] struct tray_menu *item); void tray_force_stop_cb(struct tray_menu *item); /** * @brief Callback for resetting display device configuration. * @param item The tray menu item. */ void tray_reset_display_device_config_cb([[maybe_unused]] struct tray_menu *item); /** * @brief Callback for restarting Sunshine from the system tray. * @param item The tray menu item. */ void tray_restart_cb([[maybe_unused]] struct tray_menu *item); /** * @brief Callback for exiting Sunshine from the system tray. * @param item The tray menu item. */ void tray_quit_cb([[maybe_unused]] struct tray_menu *item); /** * @brief Initializes the system tray without starting a loop. * @return 0 if initialization was successful, non-zero otherwise. */ int init_tray(); /** * @brief Processes a single tray event iteration. * @return 0 if processing was successful, non-zero otherwise. */ int process_tray_events(); /** * @brief Exit the system tray. * @return 0 after exiting the system tray. */ int end_tray(); /** * @brief Sets the tray icon in playing mode and spawns the appropriate notification * @param app_name The started application name */ void update_tray_playing(std::string app_name); /** * @brief Sets the tray icon in pausing mode (stream stopped but app running) and spawns the appropriate notification * @param app_name The paused application name */ void update_tray_pausing(std::string app_name); /** * @brief Sets the tray icon in stopped mode (app and stream stopped) and spawns the appropriate notification * @param app_name The started application name */ void update_tray_stopped(std::string app_name); void update_tray_launch_error(std::string app_name, int exit_code); /** * @brief Spawns a notification for PIN Pairing. Clicking it opens the PIN Web UI Page */ void update_tray_require_pin(); void update_tray_paired(std::string device_name); void update_tray_client_connected(std::string client_name); /** * @brief Initializes and runs the system tray in a separate thread. * @return 0 if initialization was successful, non-zero otherwise. */ int init_tray_threaded(); /** * @brief Stops the threaded system tray and waits for the thread to finish. * @return 0 after stopping the threaded tray. */ int end_tray_threaded(); } // namespace system_tray ================================================ FILE: src/task_pool.h ================================================ /** * @file src/task_pool.h * @brief Declarations for the task pool system. */ #pragma once // standard includes #include #include #include #include #include #include #include #include #include // local includes #include "move_by_copy.h" #include "utility.h" namespace task_pool_util { class _ImplBase { public: // _unique_base_type _this_ptr; inline virtual ~_ImplBase() = default; virtual void run() = 0; }; template class _Impl: public _ImplBase { Function _func; public: _Impl(Function &&f): _func(std::forward(f)) { } void run() override { _func(); } }; class TaskPool { public: typedef std::unique_ptr<_ImplBase> __task; typedef _ImplBase *task_id_t; typedef std::chrono::steady_clock::time_point __time_point; template class timer_task_t { public: task_id_t task_id; std::future future; timer_task_t(task_id_t task_id, std::future &future): task_id {task_id}, future {std::move(future)} { } }; protected: std::deque<__task> _tasks; std::vector> _timer_tasks; std::mutex _task_mutex; public: TaskPool() = default; TaskPool(TaskPool &&other) noexcept: _tasks {std::move(other._tasks)}, _timer_tasks {std::move(other._timer_tasks)} { } TaskPool &operator=(TaskPool &&other) noexcept { std::swap(_tasks, other._tasks); std::swap(_timer_tasks, other._timer_tasks); return *this; } template auto push(Function &&newTask, Args &&...args) { static_assert(std::is_invocable_v, "arguments don't match the function"); using __return = std::invoke_result_t; using task_t = std::packaged_task<__return()>; auto bind = [task = std::forward(newTask), tuple_args = std::make_tuple(std::forward(args)...)]() mutable { return std::apply(task, std::move(tuple_args)); }; task_t task(std::move(bind)); auto future = task.get_future(); std::lock_guard lg(_task_mutex); _tasks.emplace_back(toRunnable(std::move(task))); return future; } void pushDelayed(std::pair<__time_point, __task> &&task) { std::lock_guard lg(_task_mutex); auto it = _timer_tasks.cbegin(); for (; it < _timer_tasks.cend(); ++it) { if (std::get<0>(*it) < task.first) { break; } } _timer_tasks.emplace(it, task.first, std::move(task.second)); } /** * @return An id to potentially delay the task. */ template auto pushDelayed(Function &&newTask, std::chrono::duration duration, Args &&...args) { static_assert(std::is_invocable_v, "arguments don't match the function"); using __return = std::invoke_result_t; using task_t = std::packaged_task<__return()>; __time_point time_point; if constexpr (std::is_floating_point_v) { time_point = std::chrono::steady_clock::now() + std::chrono::duration_cast(duration); } else { time_point = std::chrono::steady_clock::now() + duration; } auto bind = [task = std::forward(newTask), tuple_args = std::make_tuple(std::forward(args)...)]() mutable { return std::apply(task, std::move(tuple_args)); }; task_t task(std::move(bind)); auto future = task.get_future(); auto runnable = toRunnable(std::move(task)); task_id_t task_id = &*runnable; pushDelayed(std::pair {time_point, std::move(runnable)}); return timer_task_t<__return> {task_id, future}; } /** * @param task_id The id of the task to delay. * @param duration The delay before executing the task. */ template void delay(task_id_t task_id, std::chrono::duration duration) { std::lock_guard lg(_task_mutex); auto it = _timer_tasks.begin(); for (; it < _timer_tasks.cend(); ++it) { const __task &task = std::get<1>(*it); if (&*task == task_id) { std::get<0>(*it) = std::chrono::steady_clock::now() + duration; break; } } if (it == _timer_tasks.cend()) { return; } // smaller time goes to the back auto prev = it - 1; while (it > _timer_tasks.cbegin()) { if (std::get<0>(*it) > std::get<0>(*prev)) { std::swap(*it, *prev); } --prev; --it; } } bool cancel(task_id_t task_id) { std::lock_guard lg(_task_mutex); auto it = _timer_tasks.begin(); for (; it < _timer_tasks.cend(); ++it) { const __task &task = std::get<1>(*it); if (&*task == task_id) { _timer_tasks.erase(it); return true; } } return false; } std::optional> pop(task_id_t task_id) { std::lock_guard lg(_task_mutex); auto pos = std::find_if(std::begin(_timer_tasks), std::end(_timer_tasks), [&task_id](const auto &t) { return t.second.get() == task_id; }); if (pos == std::end(_timer_tasks)) { return std::nullopt; } return std::move(*pos); } std::optional<__task> pop() { std::lock_guard lg(_task_mutex); if (!_tasks.empty()) { __task task = std::move(_tasks.front()); _tasks.pop_front(); return task; } if (!_timer_tasks.empty() && std::get<0>(_timer_tasks.back()) <= std::chrono::steady_clock::now()) { __task task = std::move(std::get<1>(_timer_tasks.back())); _timer_tasks.pop_back(); return task; } return std::nullopt; } bool ready() { std::lock_guard lg(_task_mutex); return !_tasks.empty() || (!_timer_tasks.empty() && std::get<0>(_timer_tasks.back()) <= std::chrono::steady_clock::now()); } std::optional<__time_point> next() { std::lock_guard lg(_task_mutex); if (_timer_tasks.empty()) { return std::nullopt; } return std::get<0>(_timer_tasks.back()); } private: template std::unique_ptr<_ImplBase> toRunnable(Function &&f) { return std::make_unique<_Impl>(std::forward(f)); } }; } // namespace task_pool_util ================================================ FILE: src/thread_pool.h ================================================ /** * @file src/thread_pool.h * @brief Declarations for the thread pool system. */ #pragma once // standard includes #include // local includes #include "task_pool.h" namespace thread_pool_util { /** * Allow threads to execute unhindered while keeping full control over the threads. */ class ThreadPool: public task_pool_util::TaskPool { public: typedef TaskPool::__task __task; private: std::vector _thread; std::condition_variable _cv; std::mutex _lock; bool _continue; public: ThreadPool(): _continue {false} { } explicit ThreadPool(int threads): _thread(threads), _continue {true} { for (auto &t : _thread) { t = std::thread(&ThreadPool::_main, this); } } ~ThreadPool() noexcept { if (!_continue) { return; } stop(); join(); } template auto push(Function &&newTask, Args &&...args) { std::lock_guard lg(_lock); auto future = TaskPool::push(std::forward(newTask), std::forward(args)...); _cv.notify_one(); return future; } void pushDelayed(std::pair<__time_point, __task> &&task) { std::lock_guard lg(_lock); TaskPool::pushDelayed(std::move(task)); } template auto pushDelayed(Function &&newTask, std::chrono::duration duration, Args &&...args) { std::lock_guard lg(_lock); auto future = TaskPool::pushDelayed(std::forward(newTask), duration, std::forward(args)...); // Update all timers for wait_until _cv.notify_all(); return future; } void start(int threads) { _continue = true; _thread.resize(threads); for (auto &t : _thread) { t = std::thread(&ThreadPool::_main, this); } } void stop() { std::lock_guard lg(_lock); _continue = false; _cv.notify_all(); } void join() { for (auto &t : _thread) { t.join(); } } public: void _main() { while (_continue) { if (auto task = this->pop()) { (*task)->run(); } else { std::unique_lock uniq_lock(_lock); if (ready()) { continue; } if (!_continue) { break; } if (auto tp = next()) { _cv.wait_until(uniq_lock, *tp); } else { _cv.wait(uniq_lock); } } } // Execute remaining tasks while (auto task = this->pop()) { (*task)->run(); } } }; } // namespace thread_pool_util ================================================ FILE: src/thread_safe.h ================================================ /** * @file src/thread_safe.h * @brief Declarations for thread-safe data structures. */ #pragma once // standard includes #include #include #include #include #include #include #include // local includes #include "utility.h" namespace safe { template class event_t { public: using status_t = util::optional_t; template void raise(Args &&...args) { std::lock_guard lg {_lock}; if (!_continue) { return; } if constexpr (std::is_same_v, status_t>) { _status = std::make_optional(std::forward(args)...); } else { _status = status_t {std::forward(args)...}; } _cv.notify_all(); } // pop and view should not be used interchangeably status_t pop() { std::unique_lock ul {_lock}; if (!_continue) { return util::false_v; } while (!_status) { _cv.wait(ul); if (!_continue) { return util::false_v; } } auto val = std::move(_status); _status = util::false_v; return val; } // pop and view should not be used interchangeably template status_t pop(std::chrono::duration delay) { std::unique_lock ul {_lock}; if (!_continue) { return util::false_v; } while (!_status) { if (!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) { return util::false_v; } } auto val = std::move(_status); _status = util::false_v; return val; } // pop and view should not be used interchangeably status_t view() { std::unique_lock ul {_lock}; if (!_continue) { return util::false_v; } while (!_status) { _cv.wait(ul); if (!_continue) { return util::false_v; } } return _status; } // pop and view should not be used interchangeably template status_t view(std::chrono::duration delay) { std::unique_lock ul {_lock}; if (!_continue) { return util::false_v; } while (!_status) { if (!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) { return util::false_v; } } return _status; } bool peek() { return _continue && (bool) _status; } void stop() { std::lock_guard lg {_lock}; _continue = false; _cv.notify_all(); } void reset() { std::lock_guard lg {_lock}; _continue = true; _status = util::false_v; } [[nodiscard]] bool running() const { return _continue; } private: bool _continue {true}; status_t _status {util::false_v}; std::condition_variable _cv; std::mutex _lock; }; template class alarm_raw_t { public: using status_t = util::optional_t; void ring(const status_t &status) { std::lock_guard lg(_lock); _status = status; _rang = true; _cv.notify_one(); } void ring(status_t &&status) { std::lock_guard lg(_lock); _status = std::move(status); _rang = true; _cv.notify_one(); } template auto wait_for(const std::chrono::duration &rel_time) { std::unique_lock ul(_lock); return _cv.wait_for(ul, rel_time, [this]() { return _rang; }); } template auto wait_for(const std::chrono::duration &rel_time, Pred &&pred) { std::unique_lock ul(_lock); return _cv.wait_for(ul, rel_time, [this, &pred]() { return _rang || pred(); }); } template auto wait_until(const std::chrono::duration &rel_time) { std::unique_lock ul(_lock); return _cv.wait_until(ul, rel_time, [this]() { return _rang; }); } template auto wait_until(const std::chrono::duration &rel_time, Pred &&pred) { std::unique_lock ul(_lock); return _cv.wait_until(ul, rel_time, [this, &pred]() { return _rang || pred(); }); } auto wait() { std::unique_lock ul(_lock); _cv.wait(ul, [this]() { return _rang; }); } template auto wait(Pred &&pred) { std::unique_lock ul(_lock); _cv.wait(ul, [this, &pred]() { return _rang || pred(); }); } const status_t &status() const { return _status; } status_t &status() { return _status; } void reset() { _status = status_t {}; _rang = false; } private: std::mutex _lock; std::condition_variable _cv; status_t _status {util::false_v}; bool _rang {false}; }; template using alarm_t = std::shared_ptr>; template alarm_t make_alarm() { return std::make_shared>(); } template class queue_t { public: using status_t = util::optional_t; queue_t(std::uint32_t max_elements = 32): _max_elements {max_elements} { } template void raise(Args &&...args) { std::lock_guard ul {_lock}; if (!_continue) { return; } if (_queue.size() == _max_elements) { _queue.clear(); } _queue.emplace_back(std::forward(args)...); _cv.notify_all(); } bool peek() { return _continue && !_queue.empty(); } template status_t pop(std::chrono::duration delay) { std::unique_lock ul {_lock}; if (!_continue) { return util::false_v; } while (_queue.empty()) { if (!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) { return util::false_v; } } auto val = std::move(_queue.front()); _queue.erase(std::begin(_queue)); return val; } status_t pop() { std::unique_lock ul {_lock}; if (!_continue) { return util::false_v; } while (_queue.empty()) { _cv.wait(ul); if (!_continue) { return util::false_v; } } auto val = std::move(_queue.front()); _queue.erase(std::begin(_queue)); return val; } std::vector &unsafe() { return _queue; } void stop() { std::lock_guard lg {_lock}; _continue = false; _cv.notify_all(); } [[nodiscard]] bool running() const { return _continue; } private: bool _continue {true}; std::uint32_t _max_elements; std::mutex _lock; std::condition_variable _cv; std::vector _queue; }; template class shared_t { public: using element_type = T; using construct_f = std::function; using destruct_f = std::function; struct ptr_t { shared_t *owner; ptr_t(): owner {nullptr} { } explicit ptr_t(shared_t *owner): owner {owner} { } ptr_t(ptr_t &&ptr) noexcept: owner {ptr.owner} { ptr.owner = nullptr; } ptr_t(const ptr_t &ptr) noexcept: owner {ptr.owner} { if (!owner) { return; } auto tmp = ptr.owner->ref(); tmp.owner = nullptr; } ptr_t &operator=(const ptr_t &ptr) noexcept { if (!ptr.owner) { release(); return *this; } return *this = std::move(*ptr.owner->ref()); } ptr_t &operator=(ptr_t &&ptr) noexcept { if (owner) { release(); } std::swap(owner, ptr.owner); return *this; } ~ptr_t() { if (owner) { release(); } } operator bool() const { return owner != nullptr; } void release() { std::lock_guard lg {owner->_lock}; if (!--owner->_count) { owner->_destruct(*get()); (*this)->~element_type(); } owner = nullptr; } element_type *get() const { return reinterpret_cast(owner->_object_buf.data()); } element_type *operator->() { return reinterpret_cast(owner->_object_buf.data()); } }; template shared_t(FC &&fc, FD &&fd): _construct {std::forward(fc)}, _destruct {std::forward(fd)} { } [[nodiscard]] ptr_t ref() { std::lock_guard lg {_lock}; if (!_count) { new (_object_buf.data()) element_type; if (_construct(*reinterpret_cast(_object_buf.data()))) { return ptr_t {nullptr}; } } ++_count; return ptr_t {this}; } private: construct_f _construct; destruct_f _destruct; std::array _object_buf; std::uint32_t _count; std::mutex _lock; }; template auto make_shared(F_Construct &&fc, F_Destruct &&fd) { return shared_t { std::forward(fc), std::forward(fd) }; } using signal_t = event_t; class mail_raw_t; using mail_t = std::shared_ptr; void cleanup(mail_raw_t *); template class post_t: public T { public: template post_t(mail_t mail, Args &&...args): T(std::forward(args)...), mail {std::move(mail)} { } mail_t mail; ~post_t() { cleanup(mail.get()); } }; template inline auto lock(const std::weak_ptr &wp) { return std::reinterpret_pointer_cast(wp.lock()); } class mail_raw_t: public std::enable_shared_from_this { public: template using event_t = std::shared_ptr>>; template using queue_t = std::shared_ptr>>; template event_t event(const std::string_view &id) { std::lock_guard lg {mutex}; auto it = id_to_post.find(id); if (it != std::end(id_to_post)) { return lock>(it->second); } auto post = std::make_shared::element_type>(shared_from_this()); id_to_post.emplace(std::pair> {std::string {id}, post}); return post; } template queue_t queue(const std::string_view &id) { std::lock_guard lg {mutex}; auto it = id_to_post.find(id); if (it != std::end(id_to_post)) { return lock>(it->second); } auto post = std::make_shared::element_type>(shared_from_this(), 32); id_to_post.emplace(std::pair> {std::string {id}, post}); return post; } void cleanup() { std::lock_guard lg {mutex}; for (auto it = std::begin(id_to_post); it != std::end(id_to_post); ++it) { auto &weak = it->second; if (weak.expired()) { id_to_post.erase(it); return; } } } std::mutex mutex; std::map, std::less<>> id_to_post; }; inline void cleanup(mail_raw_t *mail) { mail->cleanup(); } } // namespace safe ================================================ FILE: src/upnp.cpp ================================================ /** * @file src/upnp.cpp * @brief Definitions for UPnP port mapping. */ // standard includes #include // workaround for type_t error in miniupnpc 2.3.3, see https://github.com/miniupnp/miniupnp/commit/e263ab6f56c382e10fed31347ec68095d691a0e8 // lib includes #include // Needed to compile size_t in Windows #include #include // local includes #include "config.h" #include "confighttp.h" #include "globals.h" #include "logging.h" #include "network.h" #include "nvhttp.h" #include "rtsp.h" #include "stream.h" #include "upnp.h" #include "utility.h" using namespace std::literals; namespace upnp { struct mapping_t { struct { std::string wan; std::string lan; std::string proto; } port; std::string description; }; static std::string_view status_string(int status) { switch (status) { case 0: return "No IGD device found"sv; case 1: return "Valid IGD device found"sv; case 2: return "Valid IGD device found, but it isn't connected"sv; case 3: return "A UPnP device has been found, but it wasn't recognized as an IGD"sv; } return "Unknown status"sv; } /** * This function is a wrapper around UPNP_GetValidIGD() that returns the status code. There is a pre-processor * check to determine which version of the function to call based on the version of the MiniUPnPc library. */ int UPNP_GetValidIGDStatus(device_t &device, urls_t *urls, IGDdatas *data, std::array &lan_addr) { #if (MINIUPNPC_API_VERSION >= 18) return UPNP_GetValidIGD(device.get(), &urls->el, data, lan_addr.data(), lan_addr.size(), nullptr, 0); #else return UPNP_GetValidIGD(device.get(), &urls->el, data, lan_addr.data(), lan_addr.size()); #endif } class deinit_t: public platf::deinit_t { public: deinit_t() { auto rtsp = std::to_string(net::map_port(rtsp_stream::RTSP_SETUP_PORT)); auto video = std::to_string(net::map_port(stream::VIDEO_STREAM_PORT)); auto audio = std::to_string(net::map_port(stream::AUDIO_STREAM_PORT)); auto control = std::to_string(net::map_port(stream::CONTROL_PORT)); auto gs_http = std::to_string(net::map_port(nvhttp::PORT_HTTP)); auto gs_https = std::to_string(net::map_port(nvhttp::PORT_HTTPS)); auto wm_http = std::to_string(net::map_port(confighttp::PORT_HTTPS)); mappings.assign({ {{rtsp, rtsp, "TCP"s}, "Sunshine - RTSP"s}, {{video, video, "UDP"s}, "Sunshine - Video"s}, {{audio, audio, "UDP"s}, "Sunshine - Audio"s}, {{control, control, "UDP"s}, "Sunshine - Control"s}, {{gs_http, gs_http, "TCP"s}, "Sunshine - Client HTTP"s}, {{gs_https, gs_https, "TCP"s}, "Sunshine - Client HTTPS"s}, }); // Only map port for the Web Manager if it is configured to accept connection from WAN if (net::from_enum_string(config::nvhttp.origin_web_ui_allowed) > net::LAN) { mappings.emplace_back(mapping_t {{wm_http, wm_http, "TCP"s}, "Sunshine - Web UI"s}); } // Start the mapping thread upnp_thread = std::thread {&deinit_t::upnp_thread_proc, this}; } ~deinit_t() { upnp_thread.join(); } /** * @brief Opens pinholes for IPv6 traffic if the IGD is capable. * @details Not many IGDs support this feature, so we perform error logging with debug level. * @return `true` if the pinholes were opened successfully. */ bool create_ipv6_pinholes() { int err; device_t device {upnpDiscover(2000, nullptr, nullptr, 0, IPv6, 2, &err)}; if (!device || err) { BOOST_LOG(debug) << "Couldn't discover any IPv6 UPNP devices"sv; return false; } IGDdatas data; urls_t urls; std::array lan_addr; auto status = upnp::UPNP_GetValidIGDStatus(device, &urls, &data, lan_addr); if (status != 1 && status != 2) { BOOST_LOG(debug) << "No valid IPv6 IGD: "sv << status_string(status); return false; } if (data.IPv6FC.controlurl[0] != 0) { int firewallEnabled, pinholeAllowed; // Check if this firewall supports IPv6 pinholes err = UPNP_GetFirewallStatus(urls->controlURL_6FC, data.IPv6FC.servicetype, &firewallEnabled, &pinholeAllowed); if (err == UPNPCOMMAND_SUCCESS) { BOOST_LOG(debug) << "UPnP IPv6 firewall control available. Firewall is "sv << (firewallEnabled ? "enabled"sv : "disabled"sv) << ", pinhole is "sv << (pinholeAllowed ? "allowed"sv : "disallowed"sv); if (pinholeAllowed) { // Create pinholes for each port auto mapping_period = std::to_string(PORT_MAPPING_LIFETIME.count()); auto shutdown_event = mail::man->event(mail::shutdown); for (auto it = std::begin(mappings); it != std::end(mappings) && !shutdown_event->peek(); ++it) { auto mapping = *it; char uniqueId[8]; // Open a pinhole for the LAN port, since there will be no WAN->LAN port mapping on IPv6 err = UPNP_AddPinhole(urls->controlURL_6FC, data.IPv6FC.servicetype, "", "0", lan_addr.data(), mapping.port.lan.c_str(), mapping.port.proto.c_str(), mapping_period.c_str(), uniqueId); if (err == UPNPCOMMAND_SUCCESS) { BOOST_LOG(debug) << "Successfully created pinhole for "sv << mapping.port.proto << ' ' << mapping.port.lan; } else { BOOST_LOG(debug) << "Failed to create pinhole for "sv << mapping.port.proto << ' ' << mapping.port.lan << ": "sv << err; } } return err == 0; } else { BOOST_LOG(debug) << "IPv6 pinholes are not allowed by the IGD"sv; return false; } } else { BOOST_LOG(debug) << "Failed to get IPv6 firewall status: "sv << err; return false; } } else { BOOST_LOG(debug) << "IPv6 Firewall Control is not supported by the IGD"sv; return false; } } /** * @brief Maps a port via UPnP. * @param data IGDdatas from UPNP_GetValidIGD() * @param urls urls_t from UPNP_GetValidIGD() * @param lan_addr Local IP address to map to * @param mapping Information about port to map * @return `true` on success. */ bool map_upnp_port(const IGDdatas &data, const urls_t &urls, const std::string &lan_addr, const mapping_t &mapping) { char intClient[16]; char intPort[6]; char desc[80]; char enabled[4]; char leaseDuration[16]; bool indefinite = false; // First check if this port is already mapped successfully BOOST_LOG(debug) << "Checking for existing UPnP port mapping for "sv << mapping.port.wan; auto err = UPNP_GetSpecificPortMappingEntry( urls->controlURL, data.first.servicetype, // In params mapping.port.wan.c_str(), mapping.port.proto.c_str(), nullptr, // Out params intClient, intPort, desc, enabled, leaseDuration ); if (err == 714) { // NoSuchEntryInArray BOOST_LOG(debug) << "Mapping entry not found for "sv << mapping.port.wan; } else if (err == UPNPCOMMAND_SUCCESS) { // Some routers change the description, so we can't check that here if (!std::strcmp(intClient, lan_addr.c_str())) { if (std::atoi(leaseDuration) == 0) { BOOST_LOG(debug) << "Static mapping entry found for "sv << mapping.port.wan; // It's a static mapping, so we're done here return true; } else { BOOST_LOG(debug) << "Mapping entry found for "sv << mapping.port.wan << " ("sv << leaseDuration << " seconds remaining)"sv; } } else { BOOST_LOG(warning) << "UPnP conflict detected with: "sv << intClient; // Some UPnP IGDs won't let unauthenticated clients delete other conflicting port mappings // for security reasons, but we will give it a try anyway. err = UPNP_DeletePortMapping( urls->controlURL, data.first.servicetype, mapping.port.wan.c_str(), mapping.port.proto.c_str(), nullptr ); if (err) { BOOST_LOG(error) << "Unable to delete conflicting UPnP port mapping: "sv << err; return false; } } } else { BOOST_LOG(error) << "UPNP_GetSpecificPortMappingEntry() failed: "sv << err; // If we get a strange error from the router, we'll assume it's some old broken IGDv1 // device and only use indefinite lease durations to hopefully avoid confusing it. if (err != 606) { // Unauthorized indefinite = true; } } // Add/update the port mapping auto mapping_period = std::to_string(indefinite ? 0 : PORT_MAPPING_LIFETIME.count()); err = UPNP_AddPortMapping( urls->controlURL, data.first.servicetype, mapping.port.wan.c_str(), mapping.port.lan.c_str(), lan_addr.data(), mapping.description.c_str(), mapping.port.proto.c_str(), nullptr, mapping_period.c_str() ); if (err != UPNPCOMMAND_SUCCESS && !indefinite) { // This may be an old/broken IGD that doesn't like non-static mappings. BOOST_LOG(debug) << "Trying static mapping after failure: "sv << err; err = UPNP_AddPortMapping( urls->controlURL, data.first.servicetype, mapping.port.wan.c_str(), mapping.port.lan.c_str(), lan_addr.data(), mapping.description.c_str(), mapping.port.proto.c_str(), nullptr, "0" ); } if (err) { BOOST_LOG(error) << "Failed to map "sv << mapping.port.proto << ' ' << mapping.port.lan << ": "sv << err; return false; } BOOST_LOG(debug) << "Successfully mapped "sv << mapping.port.proto << ' ' << mapping.port.lan; return true; } /** * @brief Unmaps all ports. * @param urls urls_t from UPNP_GetValidIGD() * @param data IGDdatas from UPNP_GetValidIGD() */ void unmap_all_upnp_ports(const urls_t &urls, const IGDdatas &data) { for (auto it = std::begin(mappings); it != std::end(mappings); ++it) { auto status = UPNP_DeletePortMapping( urls->controlURL, data.first.servicetype, it->port.wan.c_str(), it->port.proto.c_str(), nullptr ); if (status && status != 714) { // NoSuchEntryInArray BOOST_LOG(warning) << "Failed to unmap "sv << it->port.proto << ' ' << it->port.lan << ": "sv << status; } else { BOOST_LOG(debug) << "Successfully unmapped "sv << it->port.proto << ' ' << it->port.lan; } } } /** * @brief Maintains UPnP port forwarding rules */ void upnp_thread_proc() { auto shutdown_event = mail::man->event(mail::shutdown); bool mapped = false; IGDdatas data; urls_t mapped_urls; auto address_family = net::af_from_enum_string(config::sunshine.address_family); // Refresh UPnP rules every few minutes. They can be lost if the router reboots, // WAN IP address changes, or various other conditions. do { int err = 0; device_t device {upnpDiscover(2000, nullptr, nullptr, 0, IPv4, 2, &err)}; if (!device || err) { BOOST_LOG(warning) << "Couldn't discover any IPv4 UPNP devices"sv; mapped = false; continue; } for (auto dev = device.get(); dev != nullptr; dev = dev->pNext) { BOOST_LOG(debug) << "Found device: "sv << dev->descURL; } std::array lan_addr; urls_t urls; auto status = upnp::UPNP_GetValidIGDStatus(device, &urls, &data, lan_addr); if (status != 1 && status != 2) { BOOST_LOG(error) << status_string(status); mapped = false; continue; } std::string lan_addr_str {lan_addr.data()}; BOOST_LOG(debug) << "Found valid IGD device: "sv << urls->rootdescURL; for (auto it = std::begin(mappings); it != std::end(mappings) && !shutdown_event->peek(); ++it) { map_upnp_port(data, urls, lan_addr_str, *it); } if (!mapped) { BOOST_LOG(info) << "Completed UPnP port mappings to "sv << lan_addr_str << " via "sv << urls->rootdescURL; } // If we are listening on IPv6 and the IGD has an IPv6 firewall enabled, try to create IPv6 firewall pinholes if (address_family == net::af_e::BOTH) { if (create_ipv6_pinholes() && !mapped) { // Only log the first time through BOOST_LOG(info) << "Successfully opened IPv6 pinholes on the IGD"sv; } } mapped = true; mapped_urls = std::move(urls); } while (!shutdown_event->view(REFRESH_INTERVAL)); if (mapped) { // Unmap ports upon termination BOOST_LOG(info) << "Unmapping UPNP ports..."sv; unmap_all_upnp_ports(mapped_urls, data); } } std::vector mappings; std::thread upnp_thread; }; std::unique_ptr start() { if (!config::sunshine.flags[config::flag::UPNP]) { return nullptr; } return std::make_unique(); } } // namespace upnp ================================================ FILE: src/upnp.h ================================================ /** * @file src/upnp.h * @brief Declarations for UPnP port mapping. */ #pragma once // lib includes #include // local includes #include "platform/common.h" /** * @brief UPnP port mapping. */ namespace upnp { constexpr auto INET6_ADDRESS_STRLEN = 46; constexpr auto IPv4 = 0; constexpr auto IPv6 = 1; constexpr auto PORT_MAPPING_LIFETIME = 3600s; constexpr auto REFRESH_INTERVAL = 120s; using device_t = util::safe_ptr; KITTY_USING_MOVE_T(urls_t, UPNPUrls, , { FreeUPNPUrls(&el); }); /** * @brief Get the valid IGD status. * @param device The device. * @param urls The URLs. * @param data The IGD data. * @param lan_addr The LAN address. * @return The UPnP Status. * @retval 0 No IGD found. * @retval 1 A valid connected IGD has been found. * @retval 2 A valid IGD has been found but it reported as not connected. * @retval 3 An UPnP device has been found but was not recognized as an IGD. */ int UPNP_GetValidIGDStatus(device_t &device, urls_t *urls, IGDdatas *data, std::array &lan_addr); [[nodiscard]] std::unique_ptr start(); } // namespace upnp ================================================ FILE: src/utility.h ================================================ /** * @file src/utility.h * @brief Declarations for utility functions. */ #pragma once // standard includes #include #include #include #include #include #include #include #include #include #include #include #include #define KITTY_WHILE_LOOP(x, y, z) \ { \ x; \ while (y) z \ } template struct argument_type; template struct argument_type { typedef U type; }; #define KITTY_USING_MOVE_T(move_t, t, init_val, z) \ class move_t { \ public: \ using element_type = typename argument_type::type; \ \ move_t(): \ el {init_val} { \ } \ template \ move_t(Args &&...args): \ el {std::forward(args)...} { \ } \ move_t(const move_t &) = delete; \ \ move_t(move_t &&other) noexcept: \ el {std::move(other.el)} { \ other.el = element_type {init_val}; \ } \ \ move_t &operator=(const move_t &) = delete; \ \ move_t &operator=(move_t &&other) { \ std::swap(el, other.el); \ return *this; \ } \ element_type *operator->() { \ return ⪙ \ } \ const element_type *operator->() const { \ return ⪙ \ } \ \ inline element_type release() { \ element_type val = std::move(el); \ el = element_type {init_val}; \ return val; \ } \ \ ~move_t() z \ \ element_type el; \ } #define KITTY_DECL_CONSTR(x) \ x(x &&) noexcept = default; \ x &operator=(x &&) noexcept = default; \ x(); #define KITTY_DEFAULT_CONSTR_MOVE(x) \ x(x &&) noexcept = default; \ x &operator=(x &&) noexcept = default; #define KITTY_DEFAULT_CONSTR_MOVE_THROW(x) \ x(x &&) = default; \ x &operator=(x &&) = default; \ x() = default; #define KITTY_DEFAULT_CONSTR(x) \ KITTY_DEFAULT_CONSTR_MOVE(x) \ x(const x &) noexcept = default; \ x &operator=(const x &) = default; #define TUPLE_2D(a, b, expr) \ decltype(expr) a##_##b = expr; \ auto &a = std::get<0>(a##_##b); \ auto &b = std::get<1>(a##_##b) #define TUPLE_2D_REF(a, b, expr) \ auto &a##_##b = expr; \ auto &a = std::get<0>(a##_##b); \ auto &b = std::get<1>(a##_##b) #define TUPLE_3D(a, b, c, expr) \ decltype(expr) a##_##b##_##c = expr; \ auto &a = std::get<0>(a##_##b##_##c); \ auto &b = std::get<1>(a##_##b##_##c); \ auto &c = std::get<2>(a##_##b##_##c) #define TUPLE_3D_REF(a, b, c, expr) \ auto &a##_##b##_##c = expr; \ auto &a = std::get<0>(a##_##b##_##c); \ auto &b = std::get<1>(a##_##b##_##c); \ auto &c = std::get<2>(a##_##b##_##c) #define TUPLE_EL(a, b, expr) \ decltype(expr) a##_ = expr; \ auto &a = std::get(a##_) #define TUPLE_EL_REF(a, b, expr) \ auto &a = std::get(expr) namespace util { template class X, class... Y> struct __instantiation_of: public std::false_type {}; template class X, class... Y> struct __instantiation_of>: public std::true_type {}; template class X, class T, class... Y> static constexpr auto instantiation_of_v = __instantiation_of::value; template struct __either; template struct __either { using type = X; }; template struct __either { using type = Y; }; template using either_t = typename __either::type; template struct overloaded: Ts... { using Ts::operator()...; }; template overloaded(Ts...) -> overloaded; template class FailGuard { public: FailGuard() = delete; FailGuard(T &&f) noexcept: _func {std::forward(f)} { } FailGuard(FailGuard &&other) noexcept: _func {std::move(other._func)} { this->failure = other.failure; other.failure = false; } FailGuard(const FailGuard &) = delete; FailGuard &operator=(const FailGuard &) = delete; FailGuard &operator=(FailGuard &&other) = delete; ~FailGuard() noexcept { if (failure) { _func(); } } void disable() { failure = false; } bool failure {true}; private: T _func; }; template [[nodiscard]] auto fail_guard(T &&f) { return FailGuard {std::forward(f)}; } template void append_struct(std::vector &buf, const T &_struct) { constexpr size_t data_len = sizeof(_struct); buf.reserve(data_len); auto *data = (uint8_t *) &_struct; for (size_t x = 0; x < data_len; ++x) { buf.push_back(data[x]); } } template class Hex { public: typedef T elem_type; private: const char _bits[16] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; char _hex[sizeof(elem_type) * 2]; public: Hex(const elem_type &elem, bool rev) { if (!rev) { const uint8_t *data = reinterpret_cast(&elem) + sizeof(elem_type) - 1; for (auto it = begin(); it < cend();) { *it++ = _bits[*data / 16]; *it++ = _bits[*data-- % 16]; } } else { const uint8_t *data = reinterpret_cast(&elem); for (auto it = begin(); it < cend();) { *it++ = _bits[*data / 16]; *it++ = _bits[*data++ % 16]; } } } char *begin() { return _hex; } char *end() { return _hex + sizeof(elem_type) * 2; } const char *begin() const { return _hex; } const char *end() const { return _hex + sizeof(elem_type) * 2; } const char *cbegin() const { return _hex; } const char *cend() const { return _hex + sizeof(elem_type) * 2; } std::string to_string() const { return {begin(), end()}; } std::string_view to_string_view() const { return {begin(), sizeof(elem_type) * 2}; } }; template Hex hex(const T &elem, bool rev = false) { return Hex(elem, rev); } template std::string log_hex(const T &value) { return "0x" + Hex(value, false).to_string(); } template std::string hex_vec(It begin, It end, bool rev = false) { auto str_size = 2 * std::distance(begin, end); std::string hex; hex.resize(str_size); const char _bits[16] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; if (rev) { for (auto it = std::begin(hex); it < std::end(hex);) { *it++ = _bits[((uint8_t) *begin) / 16]; *it++ = _bits[((uint8_t) *begin++) % 16]; } } else { --end; for (auto it = std::begin(hex); it < std::end(hex);) { *it++ = _bits[((uint8_t) *end) / 16]; *it++ = _bits[((uint8_t) *end--) % 16]; } } return hex; } template std::string hex_vec(C &&c, bool rev = false) { return hex_vec(std::begin(c), std::end(c), rev); } template T from_hex(const std::string_view &hex, bool rev = false) { std::uint8_t buf[sizeof(T)]; static char constexpr shift_bit = 'a' - 'A'; auto is_convertable = [](char ch) -> bool { if (isdigit(ch)) { return true; } ch |= shift_bit; if ('a' > ch || ch > 'z') { return false; } return true; }; auto buf_size = std::count_if(std::begin(hex), std::end(hex), is_convertable) / 2; auto padding = sizeof(T) - buf_size; const char *data = hex.data() + hex.size() - 1; auto convert = [](char ch) -> std::uint8_t { if (ch >= '0' && ch <= '9') { return (std::uint8_t) ch - '0'; } return (std::uint8_t) (ch | (char) 32) - 'a' + (char) 10; }; std::fill_n(buf + buf_size, padding, 0); std::for_each_n(buf, buf_size, [&](auto &el) { while (!is_convertable(*data)) { --data; } std::uint8_t ch_r = convert(*data--); while (!is_convertable(*data)) { --data; } std::uint8_t ch_l = convert(*data--); el = (ch_l << 4) | ch_r; }); if (rev) { std::reverse(std::begin(buf), std::end(buf)); } return *reinterpret_cast(buf); } inline std::string from_hex_vec(const std::string &hex, bool rev = false) { std::string buf; static char constexpr shift_bit = 'a' - 'A'; auto is_convertable = [](char ch) -> bool { if (isdigit(ch)) { return true; } ch |= shift_bit; if ('a' > ch || ch > 'z') { return false; } return true; }; auto buf_size = std::count_if(std::begin(hex), std::end(hex), is_convertable) / 2; buf.resize(buf_size); const char *data = hex.data() + hex.size() - 1; auto convert = [](char ch) -> std::uint8_t { if (ch >= '0' && ch <= '9') { return (std::uint8_t) ch - '0'; } return (std::uint8_t) (ch | (char) 32) - 'a' + (char) 10; }; for (auto &el : buf) { while (!is_convertable(*data)) { --data; } std::uint8_t ch_r = convert(*data--); while (!is_convertable(*data)) { --data; } std::uint8_t ch_l = convert(*data--); el = (ch_l << 4) | ch_r; } if (rev) { std::reverse(std::begin(buf), std::end(buf)); } return buf; } template T get_non_string_json_value(const nlohmann::json& j, const std::string& key, const T& default_value = T{}) { if (!j.contains(key)) return default_value; const auto& val = j.at(key); if (val.is_number() || val.is_boolean()) return val.get(); if (val.is_string()) { std::string s = val.get(); if constexpr (std::is_same_v) { return std::stoi(s); } else if constexpr (std::is_same_v) { return std::stod(s); } else if constexpr (std::is_same_v) { return s == "true"; } else if constexpr (std::is_same_v) { return s; } else { return default_value; } } return default_value; } template class hash { public: using value_type = T; std::size_t operator()(const value_type &value) const { const auto *p = reinterpret_cast(&value); return std::hash {}(std::string_view {p, sizeof(value_type)}); } }; template auto enm(const T &val) -> const std::underlying_type_t & { return *reinterpret_cast *>(&val); } template auto enm(T &val) -> std::underlying_type_t & { return *reinterpret_cast *>(&val); } inline std::int64_t from_chars(const char *begin, const char *end) { if (begin == end) { return 0; } std::int64_t res {}; std::int64_t mul = 1; while (begin != --end) { res += (std::int64_t) (*end - '0') * mul; mul *= 10; } return *begin != '-' ? res + (std::int64_t) (*begin - '0') * mul : -res; } inline std::int64_t from_view(const std::string_view &number) { return from_chars(std::begin(number), std::end(number)); } template class Either: public std::variant { public: using std::variant::variant; constexpr bool has_left() const { return std::holds_alternative(*this); } constexpr bool has_right() const { return std::holds_alternative(*this); } X &left() { return std::get(*this); } Y &right() { return std::get(*this); } const X &left() const { return std::get(*this); } const Y &right() const { return std::get(*this); } }; // Compared to std::unique_ptr, it adds the ability to get the address of the pointer itself template> class uniq_ptr { public: using element_type = T; using pointer = element_type *; using const_pointer = element_type const *; using deleter_type = D; constexpr uniq_ptr() noexcept: _p {nullptr} { } constexpr uniq_ptr(std::nullptr_t) noexcept: _p {nullptr} { } uniq_ptr(const uniq_ptr &other) noexcept = delete; uniq_ptr &operator=(const uniq_ptr &other) noexcept = delete; template uniq_ptr(V *p) noexcept: _p {p} { static_assert(std::is_same_v || std::is_same_v || std::is_base_of_v, "element_type must be base class of V"); } template uniq_ptr(std::unique_ptr &&uniq) noexcept: _p {uniq.release()} { static_assert(std::is_same_v || std::is_same_v || std::is_base_of_v, "element_type must be base class of V"); } template uniq_ptr(uniq_ptr &&other) noexcept: _p {other.release()} { static_assert(std::is_same_v || std::is_same_v || std::is_base_of_v, "element_type must be base class of V"); } template uniq_ptr &operator=(uniq_ptr &&other) noexcept { static_assert(std::is_same_v || std::is_same_v || std::is_base_of_v, "element_type must be base class of V"); reset(other.release()); return *this; } template uniq_ptr &operator=(std::unique_ptr &&uniq) noexcept { static_assert(std::is_same_v || std::is_same_v || std::is_base_of_v, "element_type must be base class of V"); reset(uniq.release()); return *this; } ~uniq_ptr() { reset(); } void reset(pointer p = pointer()) { if (_p) { _deleter(_p); } _p = p; } pointer release() { auto tmp = _p; _p = nullptr; return tmp; } pointer get() { return _p; } const_pointer get() const { return _p; } std::add_lvalue_reference_t operator*() const { return *_p; } std::add_lvalue_reference_t operator*() { return *_p; } const_pointer operator->() const { return _p; } pointer operator->() { return _p; } pointer *operator&() const { return &_p; } pointer *operator&() { return &_p; } deleter_type &get_deleter() { return _deleter; } const deleter_type &get_deleter() const { return _deleter; } explicit operator bool() const { return _p != nullptr; } protected: pointer _p; deleter_type _deleter; }; template bool operator==(const uniq_ptr &x, const uniq_ptr &y) { return x.get() == y.get(); } template bool operator!=(const uniq_ptr &x, const uniq_ptr &y) { return x.get() != y.get(); } template bool operator==(const std::unique_ptr &x, const uniq_ptr &y) { return x.get() == y.get(); } template bool operator!=(const std::unique_ptr &x, const uniq_ptr &y) { return x.get() != y.get(); } template bool operator==(const uniq_ptr &x, const std::unique_ptr &y) { return x.get() == y.get(); } template bool operator!=(const uniq_ptr &x, const std::unique_ptr &y) { return x.get() != y.get(); } template bool operator==(const uniq_ptr &x, std::nullptr_t) { return !(bool) x; } template bool operator!=(const uniq_ptr &x, std::nullptr_t) { return (bool) x; } template bool operator==(std::nullptr_t, const uniq_ptr &y) { return !(bool) y; } template bool operator!=(std::nullptr_t, const uniq_ptr &y) { return (bool) y; } template using shared_t = std::shared_ptr; template shared_t

make_shared(T *pointer) { return shared_t

(reinterpret_cast(pointer), typename P::deleter_type()); } template class wrap_ptr { public: using element_type = T; using pointer = element_type *; using const_pointer = element_type const *; using reference = element_type &; using const_reference = element_type const &; wrap_ptr(): _own_ptr {false}, _p {nullptr} { } wrap_ptr(pointer p): _own_ptr {false}, _p {p} { } wrap_ptr(std::unique_ptr &&uniq_p): _own_ptr {true}, _p {uniq_p.release()} { } wrap_ptr(wrap_ptr &&other): _own_ptr {other._own_ptr}, _p {other._p} { other._own_ptr = false; } wrap_ptr &operator=(wrap_ptr &&other) noexcept { if (_own_ptr) { delete _p; } _p = other._p; _own_ptr = other._own_ptr; other._own_ptr = false; return *this; } template wrap_ptr &operator=(std::unique_ptr &&uniq_ptr) { static_assert(std::is_base_of_v, "element_type must be base class of V"); _own_ptr = true; _p = uniq_ptr.release(); return *this; } wrap_ptr &operator=(pointer p) { if (_own_ptr) { delete _p; } _p = p; _own_ptr = false; return *this; } ~wrap_ptr() { if (_own_ptr) { delete _p; } _own_ptr = false; } const_reference operator*() const { return *_p; } reference operator*() { return *_p; } const_pointer operator->() const { return _p; } pointer operator->() { return _p; } private: bool _own_ptr; pointer _p; }; template constexpr bool is_pointer_v = instantiation_of_v || instantiation_of_v || instantiation_of_v || std::is_pointer_v; template struct __false_v; template struct __false_v>> { static constexpr std::nullopt_t value = std::nullopt; }; template struct __false_v>> { static constexpr std::nullptr_t value = nullptr; }; template struct __false_v>> { static constexpr bool value = false; }; template static constexpr auto false_v = __false_v::value; template using optional_t = either_t< (std::is_same_v || is_pointer_v), T, std::optional>; template class buffer_t { public: buffer_t(): _els {0} {}; buffer_t(buffer_t &&o) noexcept: _els {o._els}, _buf {std::move(o._buf)} { o._els = 0; } buffer_t(const buffer_t &o): _els {o._els}, _buf {std::make_unique(_els)} { std::copy(o.begin(), o.end(), begin()); } buffer_t &operator=(buffer_t &&o) noexcept { std::swap(_els, o._els); std::swap(_buf, o._buf); return *this; }; explicit buffer_t(size_t elements): _els {elements}, _buf {std::make_unique(elements)} { } explicit buffer_t(size_t elements, const T &t): _els {elements}, _buf {std::make_unique(elements)} { std::fill_n(_buf.get(), elements, t); } T &operator[](size_t el) { return _buf[el]; } const T &operator[](size_t el) const { return _buf[el]; } size_t size() const { return _els; } void fake_resize(std::size_t els) { _els = els; } T *begin() { return _buf.get(); } const T *begin() const { return _buf.get(); } T *end() { return _buf.get() + _els; } const T *end() const { return _buf.get() + _els; } private: size_t _els; std::unique_ptr _buf; }; template T either(std::optional &&l, T &&r) { if (l) { return std::move(*l); } return std::forward(r); } template struct Function { typedef ReturnType (*type)(Args...); }; template::type function> struct Destroy { typedef T pointer; void operator()(pointer p) { function(p); } }; template::type function> using safe_ptr = uniq_ptr>; // You cannot specialize an alias template::type function> using safe_ptr_v2 = uniq_ptr>; template void c_free(T *p) { free(p); } template void dynamic(T *p) { (*function)(p); } template using dyn_safe_ptr = safe_ptr>; template using dyn_safe_ptr_v2 = safe_ptr>; template using c_ptr = safe_ptr>; template std::string_view view(It begin, It end) { return std::string_view {(const char *) begin, (std::size_t) (end - begin)}; } template std::string_view view(const T &data) { return std::string_view((const char *) &data, sizeof(T)); } struct point_t { double x; double y; friend std::ostream &operator<<(std::ostream &os, const point_t &p) { return (os << "Point(x: " << p.x << ", y: " << p.y << ")"); } }; namespace endian { template struct endianness { enum : bool { #if defined(__BYTE_ORDER) && __BYTE_ORDER == __BIG_ENDIAN || \ defined(__BIG_ENDIAN__) || \ defined(__ARMEB__) || \ defined(__THUMBEB__) || \ defined(__AARCH64EB__) || \ defined(_MIBSEB) || defined(__MIBSEB) || defined(__MIBSEB__) // It's a big-endian target architecture little = false, #elif defined(__BYTE_ORDER) && __BYTE_ORDER == __LITTLE_ENDIAN || \ defined(__LITTLE_ENDIAN__) || \ defined(__ARMEL__) || \ defined(__THUMBEL__) || \ defined(__AARCH64EL__) || \ defined(_MIPSEL) || defined(__MIPSEL) || defined(__MIPSEL__) || \ defined(_WIN32) little = true, ///< little-endian target architecture #else #error "Unknown Endianness" #endif big = !little ///< big-endian target architecture }; }; template struct endian_helper {}; template struct endian_helper)>> { static inline T big(T x) { if constexpr (endianness::little) { uint8_t *data = reinterpret_cast(&x); std::reverse(data, data + sizeof(x)); } return x; } static inline T little(T x) { if constexpr (endianness::big) { uint8_t *data = reinterpret_cast(&x); std::reverse(data, data + sizeof(x)); } return x; } }; template struct endian_helper>> { static inline T little(T x) { if (!x) { return x; } if constexpr (endianness::big) { auto *data = reinterpret_cast(&*x); std::reverse(data, data + sizeof(*x)); } return x; } static inline T big(T x) { if (!x) { return x; } if constexpr (endianness::little) { auto *data = reinterpret_cast(&*x); std::reverse(data, data + sizeof(*x)); } return x; } }; template inline auto little(T x) { return endian_helper::little(x); } template inline auto big(T x) { return endian_helper::big(x); } } // namespace endian } // namespace util ================================================ FILE: src/uuid.h ================================================ /** * @file src/uuid.h * @brief Declarations for UUID generation. */ #pragma once // standard includes #include #include /** * @brief UUID utilities. */ namespace uuid_util { union uuid_t { std::uint8_t b8[16]; std::uint16_t b16[8]; std::uint32_t b32[4]; std::uint64_t b64[2]; static uuid_t generate(std::default_random_engine &engine) { std::uniform_int_distribution dist(0, std::numeric_limits::max()); uuid_t buf; for (auto &el : buf.b8) { el = dist(engine); } buf.b8[7] &= (std::uint8_t) 0b00101111; buf.b8[9] &= (std::uint8_t) 0b10011111; return buf; } static uuid_t generate() { std::random_device r; std::default_random_engine engine {r()}; return generate(engine); } static uuid_t parse(std::string& uuid_str) { if (uuid_str.length() != 36) { throw std::invalid_argument("Invalid UUID string length"); } uuid_t uuid; unsigned int temp16_1; unsigned int temp32_1, temp32_2; // Parse UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx unsigned int data1, data2; std::sscanf( uuid_str.c_str(), "%8x-%4x-%4x-%4x-%8x%4x", &uuid.b32[0], &data1, &data2, &temp16_1, &temp32_1, &temp32_2 ); // Assign parsed values into uuid_t structure uuid.b16[2] = static_cast(data1); uuid.b16[3] = static_cast(data2); // Manually splitting the last segments into bytes uuid.b8[8] = (temp16_1 >> 8) & 0xFF; uuid.b8[9] = temp16_1 & 0xFF; uuid.b8[10] = (temp32_1 >> 24) & 0xFF; uuid.b8[11] = (temp32_1 >> 16) & 0xFF; uuid.b8[12] = (temp32_1 >> 8) & 0xFF; uuid.b8[13] = temp32_1 & 0xFF; uuid.b8[14] = (temp32_2 >> 8) & 0xFF; uuid.b8[15] = temp32_2 & 0xFF; return uuid; } [[nodiscard]] std::string string() const { std::string result; result.reserve(sizeof(uuid_t) * 2 + 4); auto hex = util::hex(*this, true); auto hex_view = hex.to_string_view(); std::string_view slices[] = { hex_view.substr(0, 8), hex_view.substr(8, 4), hex_view.substr(12, 4), hex_view.substr(16, 4) }; auto last_slice = hex_view.substr(20, 12); for (auto &slice : slices) { std::copy(std::begin(slice), std::end(slice), std::back_inserter(result)); result.push_back('-'); } std::copy(std::begin(last_slice), std::end(last_slice), std::back_inserter(result)); return result; } constexpr bool operator==(const uuid_t &other) const { return b64[0] == other.b64[0] && b64[1] == other.b64[1]; } constexpr bool operator<(const uuid_t &other) const { return (b64[0] < other.b64[0] || (b64[0] == other.b64[0] && b64[1] < other.b64[1])); } constexpr bool operator>(const uuid_t &other) const { return (b64[0] > other.b64[0] || (b64[0] == other.b64[0] && b64[1] > other.b64[1])); } }; } // namespace uuid_util ================================================ FILE: src/video.cpp ================================================ /** * @file src/video.cpp * @brief Definitions for video. */ // standard includes #include #include #include #include // lib includes #include extern "C" { #include #include #include #include } // local includes #include "process.h" #include "cbs.h" #include "config.h" #include "display_device.h" #include "globals.h" #include "input.h" #include "logging.h" #include "nvenc/nvenc_base.h" #include "platform/common.h" #include "sync.h" #include "video.h" #ifdef _WIN32 #include "platform/windows/virtual_display.h" extern "C" { #include } #endif using namespace std::literals; namespace video { /** * @brief Check if we can allow probing for the encoders. * @return True if there should be no issues with the probing, false if we should prevent it. */ bool allow_encoder_probing() { const auto devices {display_device::enumerate_devices()}; // // If there are no devices, then either the API is not working correctly or OS does not support the lib. // // Either way we should not block the probing in this case as we can't tell what's wrong. // if (devices.empty()) { // return true; // } if (devices.empty()) { #ifdef _WIN32 // We'll create a temporary virtual display for probing anyways. if (proc::vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK) { return false; } #endif return true; } // Since Windows 11 24H2, it is possible that there will be no active devices present // for some reason (probably a bug). Trying to probe encoders in such a state locks/breaks the DXGI // and also the display device for Windows. So we must have at least 1 active device. const bool at_least_one_device_is_active = std::any_of(std::begin(devices), std::end(devices), [](const auto &device) { // If device has additional info, it is active. return static_cast(device.m_info); }); if (at_least_one_device_is_active) { return true; } BOOST_LOG(error) << "No display devices are active at the moment! Cannot probe the encoders."; return false; } void free_ctx(AVCodecContext *ctx) { avcodec_free_context(&ctx); } void free_frame(AVFrame *frame) { av_frame_free(&frame); } void free_buffer(AVBufferRef *ref) { av_buffer_unref(&ref); } namespace nv { enum class profile_h264_e : int { high = 2, ///< High profile high_444p = 3, ///< High 4:4:4 Predictive profile }; enum class profile_hevc_e : int { main = 0, ///< Main profile main_10 = 1, ///< Main 10 profile rext = 2, ///< Rext profile }; } // namespace nv namespace qsv { enum class profile_h264_e : int { high = 100, ///< High profile high_444p = 244, ///< High 4:4:4 Predictive profile }; enum class profile_hevc_e : int { main = 1, ///< Main profile main_10 = 2, ///< Main 10 profile rext = 4, ///< RExt profile }; enum class profile_av1_e : int { main = 1, ///< Main profile high = 2, ///< High profile }; } // namespace qsv util::Either dxgi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *); util::Either vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *); util::Either cuda_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *); util::Either vt_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *); class avcodec_software_encode_device_t: public platf::avcodec_encode_device_t { public: int convert(platf::img_t &img) override { // If we need to add aspect ratio padding, we need to scale into an intermediate output buffer bool requires_padding = (sw_frame->width != sws_output_frame->width || sw_frame->height != sws_output_frame->height); // Setup the input frame using the caller's img_t sws_input_frame->data[0] = img.data; sws_input_frame->linesize[0] = img.row_pitch; // Perform color conversion and scaling to the final size auto status = sws_scale_frame(sws.get(), requires_padding ? sws_output_frame.get() : sw_frame.get(), sws_input_frame.get()); if (status < 0) { char string[AV_ERROR_MAX_STRING_SIZE]; BOOST_LOG(error) << "Couldn't scale frame: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); return -1; } // If we require aspect ratio padding, copy the output frame into the final padded frame if (requires_padding) { auto fmt_desc = av_pix_fmt_desc_get((AVPixelFormat) sws_output_frame->format); auto planes = av_pix_fmt_count_planes((AVPixelFormat) sws_output_frame->format); for (int plane = 0; plane < planes; plane++) { auto shift_h = plane == 0 ? 0 : fmt_desc->log2_chroma_h; auto shift_w = plane == 0 ? 0 : fmt_desc->log2_chroma_w; auto offset = ((offsetW >> shift_w) * fmt_desc->comp[plane].step) + (offsetH >> shift_h) * sw_frame->linesize[plane]; // Copy line-by-line to preserve leading padding for each row for (int line = 0; line < sws_output_frame->height >> shift_h; line++) { memcpy(sw_frame->data[plane] + offset + (line * sw_frame->linesize[plane]), sws_output_frame->data[plane] + (line * sws_output_frame->linesize[plane]), (size_t) (sws_output_frame->width >> shift_w) * fmt_desc->comp[plane].step); } } } // If frame is not a software frame, it means we still need to transfer from main memory // to vram memory if (frame->hw_frames_ctx) { auto status = av_hwframe_transfer_data(frame, sw_frame.get(), 0); if (status < 0) { char string[AV_ERROR_MAX_STRING_SIZE]; BOOST_LOG(error) << "Failed to transfer image data to hardware frame: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); return -1; } } return 0; } int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { this->frame = frame; // If it's a hwframe, allocate buffers for hardware if (hw_frames_ctx) { hw_frame.reset(frame); if (av_hwframe_get_buffer(hw_frames_ctx, frame, 0)) { return -1; } } else { sw_frame.reset(frame); } return 0; } void apply_colorspace() override { auto avcodec_colorspace = avcodec_colorspace_from_sunshine_colorspace(colorspace); sws_setColorspaceDetails(sws.get(), sws_getCoefficients(SWS_CS_DEFAULT), 0, sws_getCoefficients(avcodec_colorspace.software_format), avcodec_colorspace.range - 1, 0, 1 << 16, 1 << 16); } /** * When preserving aspect ratio, ensure that padding is black */ void prefill() { auto frame = sw_frame ? sw_frame.get() : this->frame; av_frame_get_buffer(frame, 0); av_frame_make_writable(frame); ptrdiff_t linesize[4] = {frame->linesize[0], frame->linesize[1], frame->linesize[2], frame->linesize[3]}; av_image_fill_black(frame->data, linesize, (AVPixelFormat) frame->format, frame->color_range, frame->width, frame->height); } int init(int in_width, int in_height, AVFrame *frame, AVPixelFormat format, bool hardware) { // If the device used is hardware, yet the image resides on main memory if (hardware) { sw_frame.reset(av_frame_alloc()); sw_frame->width = frame->width; sw_frame->height = frame->height; sw_frame->format = format; } else { this->frame = frame; } // Fill aspect ratio padding in the destination frame prefill(); auto out_width = frame->width; auto out_height = frame->height; // Ensure aspect ratio is maintained auto scalar = std::fminf((float) out_width / in_width, (float) out_height / in_height); out_width = in_width * scalar; out_height = in_height * scalar; sws_input_frame.reset(av_frame_alloc()); sws_input_frame->width = in_width; sws_input_frame->height = in_height; sws_input_frame->format = AV_PIX_FMT_BGR0; sws_output_frame.reset(av_frame_alloc()); sws_output_frame->width = out_width; sws_output_frame->height = out_height; sws_output_frame->format = format; // Result is always positive offsetW = (frame->width - out_width) / 2; offsetH = (frame->height - out_height) / 2; sws.reset(sws_alloc_context()); if (!sws) { return -1; } AVDictionary *options {nullptr}; av_dict_set_int(&options, "srcw", sws_input_frame->width, 0); av_dict_set_int(&options, "srch", sws_input_frame->height, 0); av_dict_set_int(&options, "src_format", sws_input_frame->format, 0); av_dict_set_int(&options, "dstw", sws_output_frame->width, 0); av_dict_set_int(&options, "dsth", sws_output_frame->height, 0); av_dict_set_int(&options, "dst_format", sws_output_frame->format, 0); av_dict_set_int(&options, "sws_flags", SWS_LANCZOS | SWS_ACCURATE_RND, 0); av_dict_set_int(&options, "threads", config::video.min_threads, 0); auto status = av_opt_set_dict(sws.get(), &options); av_dict_free(&options); if (status < 0) { char string[AV_ERROR_MAX_STRING_SIZE]; BOOST_LOG(error) << "Failed to set SWS options: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); return -1; } status = sws_init_context(sws.get(), nullptr, nullptr); if (status < 0) { char string[AV_ERROR_MAX_STRING_SIZE]; BOOST_LOG(error) << "Failed to initialize SWS: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); return -1; } return 0; } // Store ownership when frame is hw_frame avcodec_frame_t hw_frame; avcodec_frame_t sw_frame; avcodec_frame_t sws_input_frame; avcodec_frame_t sws_output_frame; sws_t sws; // Offset of input image to output frame in pixels int offsetW; int offsetH; }; enum flag_e : uint32_t { DEFAULT = 0, ///< Default flags PARALLEL_ENCODING = 1 << 1, ///< Capture and encoding can run concurrently on separate threads H264_ONLY = 1 << 2, ///< When HEVC is too heavy LIMITED_GOP_SIZE = 1 << 3, ///< Some encoders don't like it when you have an infinite GOP_SIZE. e.g. VAAPI SINGLE_SLICE_ONLY = 1 << 4, ///< Never use multiple slices. Older intel iGPU's ruin it for everyone else CBR_WITH_VBR = 1 << 5, ///< Use a VBR rate control mode to simulate CBR RELAXED_COMPLIANCE = 1 << 6, ///< Use FF_COMPLIANCE_UNOFFICIAL compliance mode NO_RC_BUF_LIMIT = 1 << 7, ///< Don't set rc_buffer_size REF_FRAMES_INVALIDATION = 1 << 8, ///< Support reference frames invalidation ALWAYS_REPROBE = 1 << 9, ///< This is an encoder of last resort and we want to aggressively probe for a better one YUV444_SUPPORT = 1 << 10, ///< Encoder may support 4:4:4 chroma sampling depending on hardware ASYNC_TEARDOWN = 1 << 11, ///< Encoder supports async teardown on a different thread }; class avcodec_encode_session_t: public encode_session_t { public: avcodec_encode_session_t() = default; avcodec_encode_session_t(avcodec_ctx_t &&avcodec_ctx, std::unique_ptr encode_device, int inject): avcodec_ctx {std::move(avcodec_ctx)}, device {std::move(encode_device)}, inject {inject} { } avcodec_encode_session_t(avcodec_encode_session_t &&other) noexcept = default; ~avcodec_encode_session_t() { // Flush any remaining frames in the encoder if (avcodec_send_frame(avcodec_ctx.get(), nullptr) == 0) { packet_raw_avcodec pkt; while (avcodec_receive_packet(avcodec_ctx.get(), pkt.av_packet) == 0); } // Order matters here because the context relies on the hwdevice still being valid avcodec_ctx.reset(); device.reset(); } // Ensure objects are destroyed in the correct order avcodec_encode_session_t &operator=(avcodec_encode_session_t &&other) { device = std::move(other.device); avcodec_ctx = std::move(other.avcodec_ctx); replacements = std::move(other.replacements); sps = std::move(other.sps); vps = std::move(other.vps); inject = other.inject; return *this; } int convert(platf::img_t &img) override { if (!device) { return -1; } return device->convert(img); } void request_idr_frame() override { if (device && device->frame) { auto &frame = device->frame; frame->pict_type = AV_PICTURE_TYPE_I; frame->flags |= AV_FRAME_FLAG_KEY; } } void request_normal_frame() override { if (device && device->frame) { auto &frame = device->frame; frame->pict_type = AV_PICTURE_TYPE_NONE; frame->flags &= ~AV_FRAME_FLAG_KEY; } } void invalidate_ref_frames(int64_t first_frame, int64_t last_frame) override { BOOST_LOG(error) << "Encoder doesn't support reference frame invalidation"; request_idr_frame(); } avcodec_ctx_t avcodec_ctx; std::unique_ptr device; std::vector replacements; cbs::nal_t sps; cbs::nal_t vps; // inject sps/vps data into idr pictures int inject; }; class nvenc_encode_session_t: public encode_session_t { public: nvenc_encode_session_t(std::unique_ptr encode_device): device(std::move(encode_device)) { } int convert(platf::img_t &img) override { if (!device) { return -1; } return device->convert(img); } void request_idr_frame() override { force_idr = true; } void request_normal_frame() override { force_idr = false; } void invalidate_ref_frames(int64_t first_frame, int64_t last_frame) override { if (!device || !device->nvenc) { return; } if (!device->nvenc->invalidate_ref_frames(first_frame, last_frame)) { force_idr = true; } } nvenc::nvenc_encoded_frame encode_frame(uint64_t frame_index) { if (!device || !device->nvenc) { return {}; } auto result = device->nvenc->encode_frame(frame_index, force_idr); force_idr = false; return result; } private: std::unique_ptr device; bool force_idr = false; }; struct sync_session_ctx_t { safe::signal_t *join_event; safe::mail_raw_t::event_t shutdown_event; safe::mail_raw_t::queue_t packets; safe::mail_raw_t::event_t idr_events; safe::mail_raw_t::event_t hdr_events; safe::mail_raw_t::event_t touch_port_events; config_t config; int frame_nr; void *channel_data; }; struct sync_session_t { sync_session_ctx_t *ctx; std::unique_ptr session; }; using encode_session_ctx_queue_t = safe::queue_t; using encode_e = platf::capture_e; struct capture_ctx_t { img_event_t images; config_t config; }; struct capture_thread_async_ctx_t { std::shared_ptr> capture_ctx_queue; std::thread capture_thread; safe::signal_t reinit_event; const encoder_t *encoder_p; sync_util::sync_t> display_wp; }; struct capture_thread_sync_ctx_t { encode_session_ctx_queue_t encode_session_ctx_queue {30}; }; int start_capture_sync(capture_thread_sync_ctx_t &ctx); void end_capture_sync(capture_thread_sync_ctx_t &ctx); int start_capture_async(capture_thread_async_ctx_t &ctx); void end_capture_async(capture_thread_async_ctx_t &ctx); // Keep a reference counter to ensure the capture thread only runs when other threads have a reference to the capture thread auto capture_thread_async = safe::make_shared(start_capture_async, end_capture_async); auto capture_thread_sync = safe::make_shared(start_capture_sync, end_capture_sync); #ifdef _WIN32 encoder_t nvenc { "nvenc"sv, std::make_unique( platf::mem_type_e::dxgi, platf::pix_fmt_e::nv12, platf::pix_fmt_e::p010, platf::pix_fmt_e::ayuv, platf::pix_fmt_e::yuv444p16 ), { {}, // Common options {}, // SDR-specific options {}, // HDR-specific options {}, // YUV444 SDR-specific options {}, // YUV444 HDR-specific options {}, // Fallback options "av1_nvenc"s, }, { {}, // Common options {}, // SDR-specific options {}, // HDR-specific options {}, // YUV444 SDR-specific options {}, // YUV444 HDR-specific options {}, // Fallback options "hevc_nvenc"s, }, { {}, // Common options {}, // SDR-specific options {}, // HDR-specific options {}, // YUV444 SDR-specific options {}, // YUV444 HDR-specific options {}, // Fallback options "h264_nvenc"s, }, PARALLEL_ENCODING | REF_FRAMES_INVALIDATION | YUV444_SUPPORT | ASYNC_TEARDOWN // flags }; #elif !defined(__APPLE__) encoder_t nvenc { "nvenc"sv, std::make_unique( #ifdef _WIN32 AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_NONE, AV_PIX_FMT_D3D11, #else AV_HWDEVICE_TYPE_CUDA, AV_HWDEVICE_TYPE_NONE, AV_PIX_FMT_CUDA, #endif AV_PIX_FMT_NV12, AV_PIX_FMT_P010, AV_PIX_FMT_NONE, AV_PIX_FMT_NONE, #ifdef _WIN32 dxgi_init_avcodec_hardware_input_buffer #else cuda_init_avcodec_hardware_input_buffer #endif ), { // Common options { {"delay"s, 0}, {"forced-idr"s, 1}, {"zerolatency"s, 1}, {"surfaces"s, 1}, {"cbr_padding"s, false}, {"preset"s, &config::video.nv_legacy.preset}, {"tune"s, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY}, {"rc"s, NV_ENC_PARAMS_RC_CBR}, {"multipass"s, &config::video.nv_legacy.multipass}, {"aq"s, &config::video.nv_legacy.aq}, }, {}, // SDR-specific options {}, // HDR-specific options {}, // YUV444 SDR-specific options {}, // YUV444 HDR-specific options {}, // Fallback options "av1_nvenc"s, }, { // Common options { {"delay"s, 0}, {"forced-idr"s, 1}, {"zerolatency"s, 1}, {"surfaces"s, 1}, {"cbr_padding"s, false}, {"preset"s, &config::video.nv_legacy.preset}, {"tune"s, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY}, {"rc"s, NV_ENC_PARAMS_RC_CBR}, {"multipass"s, &config::video.nv_legacy.multipass}, {"aq"s, &config::video.nv_legacy.aq}, }, { // SDR-specific options {"profile"s, (int) nv::profile_hevc_e::main}, }, { // HDR-specific options {"profile"s, (int) nv::profile_hevc_e::main_10}, }, {}, // YUV444 SDR-specific options {}, // YUV444 HDR-specific options {}, // Fallback options "hevc_nvenc"s, }, { { {"delay"s, 0}, {"forced-idr"s, 1}, {"zerolatency"s, 1}, {"surfaces"s, 1}, {"cbr_padding"s, false}, {"preset"s, &config::video.nv_legacy.preset}, {"tune"s, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY}, {"rc"s, NV_ENC_PARAMS_RC_CBR}, {"coder"s, &config::video.nv_legacy.h264_coder}, {"multipass"s, &config::video.nv_legacy.multipass}, {"aq"s, &config::video.nv_legacy.aq}, }, { // SDR-specific options {"profile"s, (int) nv::profile_h264_e::high}, }, {}, // HDR-specific options {}, // YUV444 SDR-specific options {}, // YUV444 HDR-specific options {}, // Fallback options "h264_nvenc"s, }, PARALLEL_ENCODING }; #endif #ifdef _WIN32 encoder_t quicksync { "quicksync"sv, std::make_unique( AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_QSV, AV_PIX_FMT_QSV, AV_PIX_FMT_NV12, AV_PIX_FMT_P010, AV_PIX_FMT_VUYX, AV_PIX_FMT_XV30, dxgi_init_avcodec_hardware_input_buffer ), { // Common options { {"preset"s, &config::video.qsv.qsv_preset}, {"forced_idr"s, 1}, {"async_depth"s, 1}, {"low_delay_brc"s, 1}, {"low_power"s, 1}, }, { // SDR-specific options {"profile"s, (int) qsv::profile_av1_e::main}, }, { // HDR-specific options {"profile"s, (int) qsv::profile_av1_e::main}, }, { // YUV444 SDR-specific options {"profile"s, (int) qsv::profile_av1_e::high}, }, { // YUV444 HDR-specific options {"profile"s, (int) qsv::profile_av1_e::high}, }, {}, // Fallback options "av1_qsv"s, }, { // Common options { {"preset"s, &config::video.qsv.qsv_preset}, {"forced_idr"s, 1}, {"async_depth"s, 1}, {"low_delay_brc"s, 1}, {"low_power"s, 1}, {"recovery_point_sei"s, 0}, {"pic_timing_sei"s, 0}, }, { // SDR-specific options {"profile"s, (int) qsv::profile_hevc_e::main}, }, { // HDR-specific options {"profile"s, (int) qsv::profile_hevc_e::main_10}, }, { // YUV444 SDR-specific options {"profile"s, (int) qsv::profile_hevc_e::rext}, }, { // YUV444 HDR-specific options {"profile"s, (int) qsv::profile_hevc_e::rext}, }, { // Fallback options {"low_power"s, []() { return config::video.qsv.qsv_slow_hevc ? 0 : 1; }}, }, "hevc_qsv"s, }, { // Common options { {"preset"s, &config::video.qsv.qsv_preset}, {"cavlc"s, &config::video.qsv.qsv_cavlc}, {"forced_idr"s, 1}, {"async_depth"s, 1}, {"low_delay_brc"s, 1}, {"low_power"s, 1}, {"recovery_point_sei"s, 0}, {"vcm"s, 1}, {"pic_timing_sei"s, 0}, {"max_dec_frame_buffering"s, 1}, }, { // SDR-specific options {"profile"s, (int) qsv::profile_h264_e::high}, }, {}, // HDR-specific options { // YUV444 SDR-specific options {"profile"s, (int) qsv::profile_h264_e::high_444p}, }, {}, // YUV444 HDR-specific options { // Fallback options {"low_power"s, 0}, // Some old/low-end Intel GPUs don't support low power encoding }, "h264_qsv"s, }, PARALLEL_ENCODING | CBR_WITH_VBR | RELAXED_COMPLIANCE | NO_RC_BUF_LIMIT | YUV444_SUPPORT }; encoder_t amdvce { "amdvce"sv, std::make_unique( AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_NONE, AV_PIX_FMT_D3D11, AV_PIX_FMT_NV12, AV_PIX_FMT_P010, AV_PIX_FMT_NONE, AV_PIX_FMT_NONE, dxgi_init_avcodec_hardware_input_buffer ), { // Common options { {"filler_data"s, false}, {"forced_idr"s, 1}, {"latency"s, "lowest_latency"s}, {"async_depth"s, 1}, {"skip_frame"s, 0}, {"log_to_dbg"s, []() { return config::sunshine.min_log_level < 2 ? 1 : 0; }}, {"preencode"s, &config::video.amd.amd_preanalysis}, {"quality"s, &config::video.amd.amd_quality_av1}, {"rc"s, &config::video.amd.amd_rc_av1}, {"usage"s, &config::video.amd.amd_usage_av1}, {"enforce_hrd"s, &config::video.amd.amd_enforce_hrd}, }, {}, // SDR-specific options {}, // HDR-specific options {}, // YUV444 SDR-specific options {}, // YUV444 HDR-specific options {}, // Fallback options "av1_amf"s, }, { // Common options { {"filler_data"s, false}, {"forced_idr"s, 1}, {"latency"s, 1}, {"async_depth"s, 1}, {"skip_frame"s, 0}, {"log_to_dbg"s, []() { return config::sunshine.min_log_level < 2 ? 1 : 0; }}, {"gops_per_idr"s, 1}, {"header_insertion_mode"s, "idr"s}, {"preencode"s, &config::video.amd.amd_preanalysis}, {"quality"s, &config::video.amd.amd_quality_hevc}, {"rc"s, &config::video.amd.amd_rc_hevc}, {"usage"s, &config::video.amd.amd_usage_hevc}, {"vbaq"s, &config::video.amd.amd_vbaq}, {"enforce_hrd"s, &config::video.amd.amd_enforce_hrd}, {"level"s, [](const config_t &cfg) { auto size = cfg.width * cfg.height; // For 4K and below, try to use level 5.1 or 5.2 if possible if (size <= 8912896) { if (size * cfg.framerate <= 534773760) { return "5.1"s; } else if (size * cfg.framerate <= 1069547520) { return "5.2"s; } } return "auto"s; }}, }, {}, // SDR-specific options {}, // HDR-specific options {}, // YUV444 SDR-specific options {}, // YUV444 HDR-specific options {}, // Fallback options "hevc_amf"s, }, { // Common options { {"filler_data"s, false}, {"forced_idr"s, 1}, {"latency"s, 1}, {"async_depth"s, 1}, {"frame_skipping"s, 0}, {"log_to_dbg"s, []() { return config::sunshine.min_log_level < 2 ? 1 : 0; }}, {"preencode"s, &config::video.amd.amd_preanalysis}, {"quality"s, &config::video.amd.amd_quality_h264}, {"rc"s, &config::video.amd.amd_rc_h264}, {"usage"s, &config::video.amd.amd_usage_h264}, {"vbaq"s, &config::video.amd.amd_vbaq}, {"enforce_hrd"s, &config::video.amd.amd_enforce_hrd}, }, {}, // SDR-specific options {}, // HDR-specific options {}, // YUV444 SDR-specific options {}, // YUV444 HDR-specific options { // Fallback options {"usage"s, 2 /* AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY */}, // Workaround for https://github.com/GPUOpen-LibrariesAndSDKs/AMF/issues/410 }, "h264_amf"s, }, PARALLEL_ENCODING }; #endif encoder_t software { "software"sv, std::make_unique( AV_HWDEVICE_TYPE_NONE, AV_HWDEVICE_TYPE_NONE, AV_PIX_FMT_NONE, AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P10, AV_PIX_FMT_YUV444P, AV_PIX_FMT_YUV444P10, nullptr ), { // libsvtav1 takes different presets than libx264/libx265. // We set an infinite GOP length, use a low delay prediction structure, // force I frames to be key frames, and set max bitrate to default to work // around a FFmpeg bug with CBR mode. { {"svtav1-params"s, "keyint=-1:pred-struct=1:force-key-frames=1:mbr=0"s}, {"preset"s, &config::video.sw.svtav1_preset}, }, {}, // SDR-specific options {}, // HDR-specific options {}, // YUV444 SDR-specific options {}, // YUV444 HDR-specific options {}, // Fallback options #ifdef ENABLE_BROKEN_AV1_ENCODER // Due to bugs preventing on-demand IDR frames from working and very poor // real-time encoding performance, we do not enable libsvtav1 by default. // It is only suitable for testing AV1 until the IDR frame issue is fixed. "libsvtav1"s, #else {}, #endif }, { // x265's Info SEI is so long that it causes the IDR picture data to be // kicked to the 2nd packet in the frame, breaking Moonlight's parsing logic. // It also looks like gop_size isn't passed on to x265, so we have to set // 'keyint=-1' in the parameters ourselves. { {"forced-idr"s, 1}, {"x265-params"s, "info=0:keyint=-1"s}, {"preset"s, &config::video.sw.sw_preset}, {"tune"s, &config::video.sw.sw_tune}, }, {}, // SDR-specific options {}, // HDR-specific options {}, // YUV444 SDR-specific options {}, // YUV444 HDR-specific options {}, // Fallback options "libx265"s, }, { // Common options { {"preset"s, &config::video.sw.sw_preset}, {"tune"s, &config::video.sw.sw_tune}, }, {}, // SDR-specific options {}, // HDR-specific options {}, // YUV444 SDR-specific options {}, // YUV444 HDR-specific options {}, // Fallback options "libx264"s, }, H264_ONLY | PARALLEL_ENCODING | ALWAYS_REPROBE | YUV444_SUPPORT }; #ifdef __linux__ encoder_t vaapi { "vaapi"sv, std::make_unique( AV_HWDEVICE_TYPE_VAAPI, AV_HWDEVICE_TYPE_NONE, AV_PIX_FMT_VAAPI, AV_PIX_FMT_NV12, AV_PIX_FMT_P010, AV_PIX_FMT_NONE, AV_PIX_FMT_NONE, vaapi_init_avcodec_hardware_input_buffer ), { // Common options { {"async_depth"s, 1}, {"idr_interval"s, std::numeric_limits::max()}, }, {}, // SDR-specific options {}, // HDR-specific options {}, // YUV444 SDR-specific options {}, // YUV444 HDR-specific options {}, // Fallback options "av1_vaapi"s, }, { // Common options { {"async_depth"s, 1}, {"sei"s, 0}, {"idr_interval"s, std::numeric_limits::max()}, }, {}, // SDR-specific options {}, // HDR-specific options {}, // YUV444 SDR-specific options {}, // YUV444 HDR-specific options {}, // Fallback options "hevc_vaapi"s, }, { // Common options { {"async_depth"s, 1}, {"sei"s, 0}, {"idr_interval"s, std::numeric_limits::max()}, }, {}, // SDR-specific options {}, // HDR-specific options {}, // YUV444 SDR-specific options {}, // YUV444 HDR-specific options {}, // Fallback options "h264_vaapi"s, }, // RC buffer size will be set in platform code if supported LIMITED_GOP_SIZE | PARALLEL_ENCODING | NO_RC_BUF_LIMIT }; #endif #ifdef __APPLE__ encoder_t videotoolbox { "videotoolbox"sv, std::make_unique( AV_HWDEVICE_TYPE_VIDEOTOOLBOX, AV_HWDEVICE_TYPE_NONE, AV_PIX_FMT_VIDEOTOOLBOX, AV_PIX_FMT_NV12, AV_PIX_FMT_P010, AV_PIX_FMT_NONE, AV_PIX_FMT_NONE, vt_init_avcodec_hardware_input_buffer ), { // Common options { {"allow_sw"s, &config::video.vt.vt_allow_sw}, {"require_sw"s, &config::video.vt.vt_require_sw}, {"realtime"s, &config::video.vt.vt_realtime}, {"prio_speed"s, 1}, {"max_ref_frames"s, 1}, }, {}, // SDR-specific options {}, // HDR-specific options {}, // YUV444 SDR-specific options {}, // YUV444 HDR-specific options {}, // Fallback options "av1_videotoolbox"s, }, { // Common options { {"allow_sw"s, &config::video.vt.vt_allow_sw}, {"require_sw"s, &config::video.vt.vt_require_sw}, {"realtime"s, &config::video.vt.vt_realtime}, {"prio_speed"s, 1}, {"max_ref_frames"s, 1}, }, {}, // SDR-specific options {}, // HDR-specific options {}, // YUV444 SDR-specific options {}, // YUV444 HDR-specific options {}, // Fallback options "hevc_videotoolbox"s, }, { // Common options { {"allow_sw"s, &config::video.vt.vt_allow_sw}, {"require_sw"s, &config::video.vt.vt_require_sw}, {"realtime"s, &config::video.vt.vt_realtime}, {"prio_speed"s, 1}, {"max_ref_frames"s, 1}, }, {}, // SDR-specific options {}, // HDR-specific options {}, // YUV444 SDR-specific options {}, // YUV444 HDR-specific options { // Fallback options {"flags"s, "-low_delay"}, }, "h264_videotoolbox"s, }, DEFAULT }; #endif static const std::vector encoders { #ifndef __APPLE__ &nvenc, #endif #ifdef _WIN32 &quicksync, &amdvce, #endif #ifdef __linux__ &vaapi, #endif #ifdef __APPLE__ &videotoolbox, #endif &software }; static encoder_t *chosen_encoder; int active_hevc_mode; int active_av1_mode; bool last_encoder_probe_supported_ref_frames_invalidation = false; std::array last_encoder_probe_supported_yuv444_for_codec = { true, true, true }; void reset_display(std::shared_ptr &disp, const platf::mem_type_e &type, const std::string &display_name, const config_t &config) { // We try this twice, in case we still get an error on reinitialization for (int x = 0; x < 2; ++x) { disp.reset(); disp = platf::display(type, display_name, config); if (disp) { break; } // The capture code depends on us to sleep between failures std::this_thread::sleep_for(200ms); } } /** * @brief Update the list of display names before or during a stream. * @details This will attempt to keep `current_display_index` pointing at the same display. * @param dev_type The encoder device type used for display lookup. * @param display_names The list of display names to repopulate. * @param current_display_index The current display index or -1 if not yet known. */ void refresh_displays(platf::mem_type_e dev_type, std::vector &display_names, int ¤t_display_index, std::string &preferred_display_name) { // It is possible that the output name may be empty even if it wasn't before (device disconnected) or vice-versa const auto output_name { display_device::map_output_name(config::video.output_name) }; std::string current_display_name = preferred_display_name; // If we have a current display index, let's start with that if (current_display_name.empty() && current_display_index >= 0 && current_display_index < display_names.size()) { current_display_name = display_names.at(current_display_index); } // Refresh the display names auto old_display_names = std::move(display_names); display_names = platf::display_names(dev_type); // If we now have no displays, let's put the old display array back and fail if (display_names.empty() && !old_display_names.empty()) { BOOST_LOG(error) << "No displays were found after reenumeration!"sv; display_names = std::move(old_display_names); return; } else if (display_names.empty()) { display_names.emplace_back(output_name); } // We now have a new display name list, so reset the index back to 0 current_display_index = 0; if (current_display_name.empty()) { current_display_name = display_device::map_output_name(config::video.output_name); } // If we had a name previously, let's try to find it in the new list if (!current_display_name.empty()) { for (int x = 0; x < display_names.size(); ++x) { if (display_names[x] == current_display_name) { current_display_index = x; return; } } // The old display was removed, so we'll start back at the first display again BOOST_LOG(warning) << "Previous active display ["sv << current_display_name << "] is no longer present"sv; } else { for (int x = 0; x < display_names.size(); ++x) { if (display_names[x] == output_name) { current_display_index = x; return; } } } } void refresh_displays(platf::mem_type_e dev_type, std::vector &display_names, int ¤t_display_index) { static std::string empty_str = ""; refresh_displays(dev_type, display_names, current_display_index, empty_str); } void captureThread( std::shared_ptr> capture_ctx_queue, sync_util::sync_t> &display_wp, safe::signal_t &reinit_event, const encoder_t &encoder ) { std::vector capture_ctxs; auto fg = util::fail_guard([&]() { capture_ctx_queue->stop(); // Stop all sessions listening to this thread for (auto &capture_ctx : capture_ctxs) { capture_ctx.images->stop(); } for (auto &capture_ctx : capture_ctx_queue->unsafe()) { capture_ctx.images->stop(); } }); auto switch_display_event = mail::man->event(mail::switch_display); // Wait for the initial capture context or a request to stop the queue auto initial_capture_ctx = capture_ctx_queue->pop(); if (!initial_capture_ctx) { return; } capture_ctxs.emplace_back(std::move(*initial_capture_ctx)); std::vector display_names; int display_p = -1; std::shared_ptr disp; if (!proc::proc.display_name.empty()) { disp = platf::display(encoder.platform_formats->dev_type, proc::proc.display_name, capture_ctxs.front().config); } if (!disp) { // Get all the monitor names now, rather than at boot, to // get the most up-to-date list available monitors refresh_displays(encoder.platform_formats->dev_type, display_names, display_p); disp = platf::display(encoder.platform_formats->dev_type, display_names[display_p], capture_ctxs.front().config); if (disp) { proc::proc.display_name = display_names[display_p]; } else { return; } } display_wp = disp; constexpr auto capture_buffer_size = 12; std::list> imgs(capture_buffer_size); std::vector> imgs_used_timestamps; const std::chrono::seconds trim_timeot = 3s; auto trim_imgs = [&]() { // count allocated and used within current pool size_t allocated_count = 0; size_t used_count = 0; for (const auto &img : imgs) { if (img) { allocated_count += 1; if (img.use_count() > 1) { used_count += 1; } } } // remember the timestamp of currently used count const auto now = std::chrono::steady_clock::now(); if (imgs_used_timestamps.size() <= used_count) { imgs_used_timestamps.resize(used_count + 1); } imgs_used_timestamps[used_count] = now; // decide whether to trim allocated unused above the currently used count // based on last used timestamp and universal timeout size_t trim_target = used_count; for (size_t i = used_count; i < imgs_used_timestamps.size(); i++) { if (imgs_used_timestamps[i] && now - *imgs_used_timestamps[i] < trim_timeot) { trim_target = i; } } // trim allocated unused above the newly decided trim target if (allocated_count > trim_target) { size_t to_trim = allocated_count - trim_target; // prioritize trimming least recently used for (auto it = imgs.rbegin(); it != imgs.rend(); it++) { auto &img = *it; if (img && img.use_count() == 1) { img.reset(); to_trim -= 1; if (to_trim == 0) { break; } } } // forget timestamps that no longer relevant imgs_used_timestamps.resize(trim_target + 1); } }; auto pull_free_image_callback = [&](std::shared_ptr &img_out) -> bool { img_out.reset(); while (capture_ctx_queue->running()) { // pick first allocated but unused for (auto it = imgs.begin(); it != imgs.end(); it++) { if (*it && it->use_count() == 1) { img_out = *it; if (it != imgs.begin()) { // move image to the front of the list to prioritize its reusal imgs.erase(it); imgs.push_front(img_out); } break; } } // otherwise pick first unallocated if (!img_out) { for (auto it = imgs.begin(); it != imgs.end(); it++) { if (!*it) { // allocate image *it = disp->alloc_img(); img_out = *it; if (it != imgs.begin()) { // move image to the front of the list to prioritize its reusal imgs.erase(it); imgs.push_front(img_out); } break; } } } if (img_out) { // trim allocated but unused portion of the pool based on timeouts trim_imgs(); img_out->frame_timestamp.reset(); return true; } else { // sleep and retry if image pool is full std::this_thread::sleep_for(1ms); } } return false; }; // Capture takes place on this thread platf::adjust_thread_priority(platf::thread_priority_e::critical); while (capture_ctx_queue->running()) { bool artificial_reinit = false; auto push_captured_image_callback = [&](std::shared_ptr &&img, bool frame_captured) -> bool { KITTY_WHILE_LOOP(auto capture_ctx = std::begin(capture_ctxs), capture_ctx != std::end(capture_ctxs), { if (!capture_ctx->images->running()) { capture_ctx = capture_ctxs.erase(capture_ctx); continue; } if (frame_captured) { capture_ctx->images->raise(img); } ++capture_ctx; }) if (!capture_ctx_queue->running()) { return false; } while (capture_ctx_queue->peek()) { capture_ctxs.emplace_back(std::move(*capture_ctx_queue->pop())); } if (switch_display_event->peek()) { artificial_reinit = true; return false; } return true; }; auto status = disp->capture(push_captured_image_callback, pull_free_image_callback, &display_cursor); if (artificial_reinit && status != platf::capture_e::error) { status = platf::capture_e::reinit; artificial_reinit = false; } switch (status) { case platf::capture_e::reinit: { reinit_event.raise(true); // Some classes of images contain references to the display --> display won't delete unless img is deleted for (auto &img : imgs) { img.reset(); } // display_wp is modified in this thread only // Wait for the other shared_ptr's of display to be destroyed. // New displays will only be created in this thread. while (display_wp->use_count() != 1) { // Free images that weren't consumed by the encoders. These can reference the display and prevent // the ref count from reaching 1. We do this here rather than on the encoder thread to avoid race // conditions where the encoding loop might free a good frame after reinitializing if we capture // a new frame here before the encoder has finished reinitializing. KITTY_WHILE_LOOP(auto capture_ctx = std::begin(capture_ctxs), capture_ctx != std::end(capture_ctxs), { if (!capture_ctx->images->running()) { capture_ctx = capture_ctxs.erase(capture_ctx); continue; } while (capture_ctx->images->peek()) { capture_ctx->images->pop(); } ++capture_ctx; }); std::this_thread::sleep_for(20ms); } while (capture_ctx_queue->running()) { // Release the display before reenumerating displays, since some capture backends // only support a single display session per device/application. disp.reset(); // Refresh display names since a display removal might have caused the reinitialization refresh_displays(encoder.platform_formats->dev_type, display_names, display_p, proc::proc.display_name); // Process any pending display switch with the new list of displays if (switch_display_event->peek()) { display_p = std::clamp(*switch_display_event->pop(), 0, (int) display_names.size() - 1); } // reset_display() will sleep between retries reset_display(disp, encoder.platform_formats->dev_type, display_names[display_p], capture_ctxs.front().config); if (disp) { proc::proc.display_name = display_names[display_p]; break; } } if (!disp) { return; } display_wp = disp; reinit_event.reset(); continue; } case platf::capture_e::error: case platf::capture_e::ok: case platf::capture_e::timeout: case platf::capture_e::interrupted: return; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; return; } } } int encode_avcodec(int64_t frame_nr, avcodec_encode_session_t &session, safe::mail_raw_t::queue_t &packets, void *channel_data, std::optional frame_timestamp) { auto &frame = session.device->frame; frame->pts = frame_nr; auto &ctx = session.avcodec_ctx; auto &sps = session.sps; auto &vps = session.vps; // send the frame to the encoder auto ret = avcodec_send_frame(ctx.get(), frame); if (ret < 0) { char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Could not send a frame for encoding: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, ret); return -1; } while (ret >= 0) { auto packet = std::make_unique(); auto av_packet = packet.get()->av_packet; ret = avcodec_receive_packet(ctx.get(), av_packet); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { return 0; } else if (ret < 0) { return ret; } if (av_packet->flags & AV_PKT_FLAG_KEY) { BOOST_LOG(debug) << "Frame "sv << frame_nr << ": IDR Keyframe (AV_FRAME_FLAG_KEY)"sv; } if ((frame->flags & AV_FRAME_FLAG_KEY) && !(av_packet->flags & AV_PKT_FLAG_KEY)) { BOOST_LOG(error) << "Encoder did not produce IDR frame when requested!"sv; } if (session.inject) { if (session.inject == 1) { auto h264 = cbs::make_sps_h264(ctx.get(), av_packet); sps = std::move(h264.sps); } else { auto hevc = cbs::make_sps_hevc(ctx.get(), av_packet); sps = std::move(hevc.sps); vps = std::move(hevc.vps); session.replacements.emplace_back( std::string_view((char *) std::begin(vps.old), vps.old.size()), std::string_view((char *) std::begin(vps._new), vps._new.size()) ); } session.inject = 0; session.replacements.emplace_back( std::string_view((char *) std::begin(sps.old), sps.old.size()), std::string_view((char *) std::begin(sps._new), sps._new.size()) ); } if (av_packet && av_packet->pts == frame_nr) { packet->frame_timestamp = frame_timestamp; } packet->replacements = &session.replacements; packet->channel_data = channel_data; packets->raise(std::move(packet)); } return 0; } int encode_nvenc(int64_t frame_nr, nvenc_encode_session_t &session, safe::mail_raw_t::queue_t &packets, void *channel_data, std::optional frame_timestamp) { auto encoded_frame = session.encode_frame(frame_nr); if (encoded_frame.data.empty()) { BOOST_LOG(error) << "NvENC returned empty packet"; return -1; } if (frame_nr != encoded_frame.frame_index) { BOOST_LOG(error) << "NvENC frame index mismatch " << frame_nr << " " << encoded_frame.frame_index; } auto packet = std::make_unique(std::move(encoded_frame.data), encoded_frame.frame_index, encoded_frame.idr); packet->channel_data = channel_data; packet->after_ref_frame_invalidation = encoded_frame.after_ref_frame_invalidation; packet->frame_timestamp = frame_timestamp; packets->raise(std::move(packet)); return 0; } int encode(int64_t frame_nr, encode_session_t &session, safe::mail_raw_t::queue_t &packets, void *channel_data, std::optional frame_timestamp) { if (auto avcodec_session = dynamic_cast(&session)) { return encode_avcodec(frame_nr, *avcodec_session, packets, channel_data, frame_timestamp); } else if (auto nvenc_session = dynamic_cast(&session)) { return encode_nvenc(frame_nr, *nvenc_session, packets, channel_data, frame_timestamp); } return -1; } std::unique_ptr make_avcodec_encode_session( platf::display_t *disp, const encoder_t &encoder, const config_t &config, int width, int height, std::unique_ptr encode_device ) { auto platform_formats = dynamic_cast(encoder.platform_formats.get()); if (!platform_formats) { return nullptr; } bool hardware = platform_formats->avcodec_base_dev_type != AV_HWDEVICE_TYPE_NONE; auto &video_format = encoder.codec_from_config(config); if (!video_format[encoder_t::PASSED] || !disp->is_codec_supported(video_format.name, config)) { BOOST_LOG(error) << encoder.name << ": "sv << video_format.name << " mode not supported"sv; return nullptr; } if (config.dynamicRange && !video_format[encoder_t::DYNAMIC_RANGE]) { BOOST_LOG(error) << video_format.name << ": dynamic range not supported"sv; return nullptr; } if (config.chromaSamplingType == 1 && !video_format[encoder_t::YUV444]) { BOOST_LOG(error) << video_format.name << ": YUV 4:4:4 not supported"sv; return nullptr; } auto codec = avcodec_find_encoder_by_name(video_format.name.c_str()); if (!codec) { BOOST_LOG(error) << "Couldn't open ["sv << video_format.name << ']'; return nullptr; } auto colorspace = encode_device->colorspace; auto sw_fmt = (colorspace.bit_depth == 8 && config.chromaSamplingType == 0) ? platform_formats->avcodec_pix_fmt_8bit : (colorspace.bit_depth == 8 && config.chromaSamplingType == 1) ? platform_formats->avcodec_pix_fmt_yuv444_8bit : (colorspace.bit_depth == 10 && config.chromaSamplingType == 0) ? platform_formats->avcodec_pix_fmt_10bit : (colorspace.bit_depth == 10 && config.chromaSamplingType == 1) ? platform_formats->avcodec_pix_fmt_yuv444_10bit : AV_PIX_FMT_NONE; // Allow up to 1 retry to apply the set of fallback options. // // Note: If we later end up needing multiple sets of // fallback options, we may need to allow more retries // to try applying each set. avcodec_ctx_t ctx; for (int retries = 0; retries < 2; retries++) { ctx.reset(avcodec_alloc_context3(codec)); ctx->width = config.width; ctx->height = config.height; ctx->time_base = AVRational {1, config.framerate}; ctx->framerate = AVRational {config.framerate, 1}; switch (config.videoFormat) { case 0: // 10-bit h264 encoding is not supported by our streaming protocol assert(!config.dynamicRange); ctx->profile = (config.chromaSamplingType == 1) ? AV_PROFILE_H264_HIGH_444_PREDICTIVE : AV_PROFILE_H264_HIGH; break; case 1: if (config.chromaSamplingType == 1) { // HEVC uses the same RExt profile for both 8 and 10 bit YUV 4:4:4 encoding ctx->profile = AV_PROFILE_HEVC_REXT; } else { ctx->profile = config.dynamicRange ? AV_PROFILE_HEVC_MAIN_10 : AV_PROFILE_HEVC_MAIN; } break; case 2: // AV1 supports both 8 and 10 bit encoding with the same Main profile // but YUV 4:4:4 sampling requires High profile ctx->profile = (config.chromaSamplingType == 1) ? AV_PROFILE_AV1_HIGH : AV_PROFILE_AV1_MAIN; break; } // B-frames delay decoder output, so never use them ctx->max_b_frames = 0; // Use an infinite GOP length since I-frames are generated on demand ctx->gop_size = encoder.flags & LIMITED_GOP_SIZE ? std::numeric_limits::max() : std::numeric_limits::max(); ctx->keyint_min = std::numeric_limits::max(); // Some client decoders have limits on the number of reference frames if (config.numRefFrames) { if (video_format[encoder_t::REF_FRAMES_RESTRICT]) { ctx->refs = config.numRefFrames; } else { BOOST_LOG(warning) << "Client requested reference frame limit, but encoder doesn't support it!"sv; } } // We forcefully reset the flags to avoid clash on reuse of AVCodecContext ctx->flags = 0; ctx->flags |= AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY; ctx->flags2 |= AV_CODEC_FLAG2_FAST; auto avcodec_colorspace = avcodec_colorspace_from_sunshine_colorspace(colorspace); ctx->color_range = avcodec_colorspace.range; ctx->color_primaries = avcodec_colorspace.primaries; ctx->color_trc = avcodec_colorspace.transfer_function; ctx->colorspace = avcodec_colorspace.matrix; // Used by cbs::make_sps_hevc ctx->sw_pix_fmt = sw_fmt; if (hardware) { avcodec_buffer_t encoding_stream_context; ctx->pix_fmt = platform_formats->avcodec_dev_pix_fmt; // Create the base hwdevice context auto buf_or_error = platform_formats->init_avcodec_hardware_input_buffer(encode_device.get()); if (buf_or_error.has_right()) { return nullptr; } encoding_stream_context = std::move(buf_or_error.left()); // If this encoder requires derivation from the base, derive the desired type if (platform_formats->avcodec_derived_dev_type != AV_HWDEVICE_TYPE_NONE) { avcodec_buffer_t derived_context; // Allow the hwdevice to prepare for this type of context to be derived if (encode_device->prepare_to_derive_context(platform_formats->avcodec_derived_dev_type)) { return nullptr; } auto err = av_hwdevice_ctx_create_derived(&derived_context, platform_formats->avcodec_derived_dev_type, encoding_stream_context.get(), 0); if (err) { char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Failed to derive device context: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return nullptr; } encoding_stream_context = std::move(derived_context); } // Initialize avcodec hardware frames { avcodec_buffer_t frame_ref {av_hwframe_ctx_alloc(encoding_stream_context.get())}; auto frame_ctx = (AVHWFramesContext *) frame_ref->data; frame_ctx->format = ctx->pix_fmt; frame_ctx->sw_format = sw_fmt; frame_ctx->height = ctx->height; frame_ctx->width = ctx->width; frame_ctx->initial_pool_size = 0; // Allow the hwdevice to modify hwframe context parameters encode_device->init_hwframes(frame_ctx); if (auto err = av_hwframe_ctx_init(frame_ref.get()); err < 0) { return nullptr; } ctx->hw_frames_ctx = av_buffer_ref(frame_ref.get()); } ctx->slices = config.slicesPerFrame; } else /* software */ { ctx->pix_fmt = sw_fmt; // Clients will request for the fewest slices per frame to get the // most efficient encode, but we may want to provide more slices than // requested to ensure we have enough parallelism for good performance. ctx->slices = std::max(config.slicesPerFrame, config::video.min_threads); } if (encoder.flags & SINGLE_SLICE_ONLY) { ctx->slices = 1; } ctx->thread_type = FF_THREAD_SLICE; ctx->thread_count = ctx->slices; AVDictionary *options {nullptr}; auto handle_option = [&options, &config](const encoder_t::option_t &option) { std::visit( util::overloaded { [&](int v) { av_dict_set_int(&options, option.name.c_str(), v, 0); }, [&](int *v) { av_dict_set_int(&options, option.name.c_str(), *v, 0); }, [&](std::optional *v) { if (*v) { av_dict_set_int(&options, option.name.c_str(), **v, 0); } }, [&](const std::function &v) { av_dict_set_int(&options, option.name.c_str(), v(), 0); }, [&](const std::string &v) { av_dict_set(&options, option.name.c_str(), v.c_str(), 0); }, [&](std::string *v) { if (!v->empty()) { av_dict_set(&options, option.name.c_str(), v->c_str(), 0); } }, [&](const std::function &v) { av_dict_set(&options, option.name.c_str(), v(config).c_str(), 0); } }, option.value ); }; // Apply common options, then format-specific overrides for (auto &option : video_format.common_options) { handle_option(option); } for (auto &option : (config.dynamicRange ? video_format.hdr_options : video_format.sdr_options)) { handle_option(option); } if (config.chromaSamplingType == 1) { for (auto &option : (config.dynamicRange ? video_format.hdr444_options : video_format.sdr444_options)) { handle_option(option); } } if (retries > 0) { for (auto &option : video_format.fallback_options) { handle_option(option); } } auto bitrate = config.bitrate * 1000; ctx->rc_max_rate = bitrate; ctx->bit_rate = bitrate; if (encoder.flags & CBR_WITH_VBR) { // Ensure rc_max_bitrate != bit_rate to force VBR mode ctx->bit_rate--; } else { ctx->rc_min_rate = bitrate; } if (encoder.flags & RELAXED_COMPLIANCE) { ctx->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL; } if (!(encoder.flags & NO_RC_BUF_LIMIT)) { if (!hardware && (ctx->slices > 1 || config.videoFormat == 1)) { // Use a larger rc_buffer_size for software encoding when slices are enabled, // because libx264 can severely degrade quality if the buffer is too small. // libx265 encounters this issue more frequently, so always scale the // buffer by 1.5x for software HEVC encoding. ctx->rc_buffer_size = bitrate / ((config.framerate * 10) / 15); } else { ctx->rc_buffer_size = bitrate / config.framerate; #ifndef __APPLE__ if (encoder.name == "nvenc" && config::video.nv_legacy.vbv_percentage_increase > 0) { ctx->rc_buffer_size += ctx->rc_buffer_size * config::video.nv_legacy.vbv_percentage_increase / 100; } #endif } } // Allow the encoding device a final opportunity to set/unset or override any options encode_device->init_codec_options(ctx.get(), &options); if (auto status = avcodec_open2(ctx.get(), codec, &options)) { char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; if (!video_format.fallback_options.empty() && retries == 0) { BOOST_LOG(info) << "Retrying with fallback configuration options for ["sv << video_format.name << "] after error: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, status); continue; } else { BOOST_LOG(error) << "Could not open codec ["sv << video_format.name << "]: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, status); return nullptr; } } // Successfully opened the codec break; } avcodec_frame_t frame {av_frame_alloc()}; frame->format = ctx->pix_fmt; frame->width = ctx->width; frame->height = ctx->height; frame->color_range = ctx->color_range; frame->color_primaries = ctx->color_primaries; frame->color_trc = ctx->color_trc; frame->colorspace = ctx->colorspace; frame->chroma_location = ctx->chroma_sample_location; // Attach HDR metadata to the AVFrame if (colorspace_is_hdr(colorspace)) { SS_HDR_METADATA hdr_metadata; if (disp->get_hdr_metadata(hdr_metadata)) { auto mdm = av_mastering_display_metadata_create_side_data(frame.get()); mdm->display_primaries[0][0] = av_make_q(hdr_metadata.displayPrimaries[0].x, 50000); mdm->display_primaries[0][1] = av_make_q(hdr_metadata.displayPrimaries[0].y, 50000); mdm->display_primaries[1][0] = av_make_q(hdr_metadata.displayPrimaries[1].x, 50000); mdm->display_primaries[1][1] = av_make_q(hdr_metadata.displayPrimaries[1].y, 50000); mdm->display_primaries[2][0] = av_make_q(hdr_metadata.displayPrimaries[2].x, 50000); mdm->display_primaries[2][1] = av_make_q(hdr_metadata.displayPrimaries[2].y, 50000); mdm->white_point[0] = av_make_q(hdr_metadata.whitePoint.x, 50000); mdm->white_point[1] = av_make_q(hdr_metadata.whitePoint.y, 50000); mdm->min_luminance = av_make_q(hdr_metadata.minDisplayLuminance, 10000); mdm->max_luminance = av_make_q(hdr_metadata.maxDisplayLuminance, 1); mdm->has_luminance = hdr_metadata.maxDisplayLuminance != 0 ? 1 : 0; mdm->has_primaries = hdr_metadata.displayPrimaries[0].x != 0 ? 1 : 0; if (hdr_metadata.maxContentLightLevel != 0 || hdr_metadata.maxFrameAverageLightLevel != 0) { auto clm = av_content_light_metadata_create_side_data(frame.get()); clm->MaxCLL = hdr_metadata.maxContentLightLevel; clm->MaxFALL = hdr_metadata.maxFrameAverageLightLevel; } } else { BOOST_LOG(error) << "Couldn't get display hdr metadata when colorspace selection indicates it should have one"; } } std::unique_ptr encode_device_final; if (!encode_device->data) { auto software_encode_device = std::make_unique(); if (software_encode_device->init(width, height, frame.get(), sw_fmt, hardware)) { return nullptr; } software_encode_device->colorspace = colorspace; encode_device_final = std::move(software_encode_device); } else { encode_device_final = std::move(encode_device); } if (encode_device_final->set_frame(frame.release(), ctx->hw_frames_ctx)) { return nullptr; } encode_device_final->apply_colorspace(); auto session = std::make_unique( std::move(ctx), std::move(encode_device_final), // 0 ==> don't inject, 1 ==> inject for h264, 2 ==> inject for hevc config.videoFormat <= 1 ? (1 - (int) video_format[encoder_t::VUI_PARAMETERS]) * (1 + config.videoFormat) : 0 ); return session; } std::unique_ptr make_nvenc_encode_session(const config_t &client_config, std::unique_ptr encode_device) { if (!encode_device->init_encoder(client_config, encode_device->colorspace)) { return nullptr; } return std::make_unique(std::move(encode_device)); } std::unique_ptr make_encode_session(platf::display_t *disp, const encoder_t &encoder, const config_t &config, int width, int height, std::unique_ptr encode_device) { if (dynamic_cast(encode_device.get())) { auto avcodec_encode_device = boost::dynamic_pointer_cast(std::move(encode_device)); return make_avcodec_encode_session(disp, encoder, config, width, height, std::move(avcodec_encode_device)); } else if (dynamic_cast(encode_device.get())) { auto nvenc_encode_device = boost::dynamic_pointer_cast(std::move(encode_device)); return make_nvenc_encode_session(config, std::move(nvenc_encode_device)); } return nullptr; } void encode_run( int &frame_nr, // Store progress of the frame number safe::mail_t mail, img_event_t images, config_t config, std::shared_ptr disp, std::unique_ptr encode_device, safe::signal_t &reinit_event, const encoder_t &encoder, void *channel_data ) { auto session = make_encode_session(disp.get(), encoder, config, disp->width, disp->height, std::move(encode_device)); if (!session) { return; } // As a workaround for NVENC hangs and to generally speed up encoder reinit, // we will complete the encoder teardown in a separate thread if supported. // This will move expensive processing off the encoder thread to allow us // to restart encoding as soon as possible. For cases where the NVENC driver // hang occurs, this thread may probably never exit, but it will allow // streaming to continue without requiring a full restart of Sunshine. auto fail_guard = util::fail_guard([&encoder, &session] { if (encoder.flags & ASYNC_TEARDOWN) { std::thread encoder_teardown_thread {[session = std::move(session)]() mutable { BOOST_LOG(info) << "Starting async encoder teardown"; session.reset(); BOOST_LOG(info) << "Async encoder teardown complete"; }}; encoder_teardown_thread.detach(); } }); // set max frame time based on client-requested target framerate. double minimum_fps_target = (config::video.minimum_fps_target > 0.0) ? config::video.minimum_fps_target * 1000 : std::max(config.encodingFramerate / 5, 10000); auto max_frametime = std::chrono::nanoseconds(1000ms) * 1000 / minimum_fps_target; auto encode_frame_threshold = std::chrono::nanoseconds(1000ms) * 1000 / config.encodingFramerate; auto frame_variation_threshold = encode_frame_threshold / 4; auto min_frame_diff = encode_frame_threshold - frame_variation_threshold; BOOST_LOG(info) << "Minimum FPS target set to ~"sv << (minimum_fps_target / 2000) << "fps ("sv << max_frametime * 2 << ")"sv; BOOST_LOG(info) << "Encoding Frame threshold: "sv << encode_frame_threshold; auto shutdown_event = mail->event(mail::shutdown); auto packets = mail::man->queue(mail::video_packets); auto idr_events = mail->event(mail::idr); auto invalidate_ref_frames_events = mail->event>(mail::invalidate_ref_frames); { // Load a dummy image into the AVFrame to ensure we have something to encode // even if we timeout waiting on the first frame. This is a relatively large // allocation which can be freed immediately after convert(), so we do this // in a separate scope. auto dummy_img = disp->alloc_img(); if (!dummy_img || disp->dummy_img(dummy_img.get()) || session->convert(*dummy_img)) { return; } } if (config.input_only) { BOOST_LOG(info) << "Input only session, video will not be captured."sv; // Encode the dummy img only once if (encode(frame_nr++, *session, packets, channel_data, std::chrono::steady_clock::now())) { BOOST_LOG(error) << "Could not encode dummy video packet"sv; return; } while (true) { if (shutdown_event->peek() || !images->running() || (reinit_event.peek())) { return; } else { std::this_thread::sleep_for(300ms); } } } std::chrono::steady_clock::time_point encode_frame_timestamp; bool missing_frame_timestamp_warning_logged = false; while (true) { // Break out of the encoding loop if any of the following are true: // a) The stream is ending // b) Sunshine is quitting // c) The capture side is waiting to reinit and we've encoded at least one frame // // If we have to reinit before we have received any captured frames, we will encode // the blank dummy frame just to let Moonlight know that we're alive. if (shutdown_event->peek() || !images->running() || (reinit_event.peek() && frame_nr > 1)) { break; } bool requested_idr_frame = false; while (invalidate_ref_frames_events->peek()) { if (auto frames = invalidate_ref_frames_events->pop(0ms)) { session->invalidate_ref_frames(frames->first, frames->second); } } if (idr_events->peek()) { requested_idr_frame = true; idr_events->pop(); } if (requested_idr_frame) { session->request_idr_frame(); } std::optional frame_timestamp; // Encode at a minimum FPS to avoid image quality issues with static content if (!requested_idr_frame || images->peek()) { if (auto img = images->pop(max_frametime)) { frame_timestamp = img->frame_timestamp; if (!frame_timestamp) { if (!missing_frame_timestamp_warning_logged) { BOOST_LOG(warning) << "Encoder received image without frame timestamp; substituting steady_clock::now()"sv; missing_frame_timestamp_warning_logged = true; } frame_timestamp = std::chrono::steady_clock::now(); } auto current_timestamp = *frame_timestamp; auto time_diff = current_timestamp - encode_frame_timestamp; // If new frame comes in way too fast, just drop if (time_diff < -frame_variation_threshold) { continue; } if (session->convert(*img)) { BOOST_LOG(error) << "Could not convert image"sv; break; } if (time_diff < frame_variation_threshold) { *frame_timestamp = encode_frame_timestamp; } else { encode_frame_timestamp = current_timestamp; } encode_frame_timestamp += encode_frame_threshold; } else if (!images->running()) { break; } } if (encode(frame_nr++, *session, packets, channel_data, frame_timestamp)) { BOOST_LOG(error) << "Could not encode video packet"sv; break; } session->request_normal_frame(); } } input::touch_port_t make_port(platf::display_t *display, const config_t &config) { float wd = display->width; float hd = display->height; float wt = config.width; float ht = config.height; auto scalar = std::fminf(wt / wd, ht / hd); auto w2 = scalar * wd; auto h2 = scalar * hd; auto offsetX = (config.width - w2) * 0.5f; auto offsetY = (config.height - h2) * 0.5f; return input::touch_port_t { { display->offset_x, display->offset_y, config.width, config.height, }, display->env_width, display->env_height, offsetX, offsetY, 1.0f / scalar, }; } std::unique_ptr make_encode_device(platf::display_t &disp, const encoder_t &encoder, const config_t &config) { std::unique_ptr result; auto colorspace = colorspace_from_client_config(config, disp.is_hdr()); platf::pix_fmt_e pix_fmt; if (config.chromaSamplingType == 1) { // YUV 4:4:4 if (!(encoder.flags & YUV444_SUPPORT)) { // Encoder can't support YUV 4:4:4 regardless of hardware capabilities return {}; } pix_fmt = (colorspace.bit_depth == 10) ? encoder.platform_formats->pix_fmt_yuv444_10bit : encoder.platform_formats->pix_fmt_yuv444_8bit; } else { // YUV 4:2:0 pix_fmt = (colorspace.bit_depth == 10) ? encoder.platform_formats->pix_fmt_10bit : encoder.platform_formats->pix_fmt_8bit; } { auto encoder_name = encoder.codec_from_config(config).name; BOOST_LOG(info) << "Creating encoder " << logging::bracket(encoder_name); auto color_coding = colorspace.colorspace == colorspace_e::bt2020 ? "HDR (Rec. 2020 + SMPTE 2084 PQ)" : colorspace.colorspace == colorspace_e::rec601 ? "SDR (Rec. 601)" : colorspace.colorspace == colorspace_e::rec709 ? "SDR (Rec. 709)" : colorspace.colorspace == colorspace_e::bt2020sdr ? "SDR (Rec. 2020)" : "unknown"; BOOST_LOG(info) << "Color coding: " << color_coding; BOOST_LOG(info) << "Color depth: " << colorspace.bit_depth << "-bit"; BOOST_LOG(info) << "Color range: " << (colorspace.full_range ? "JPEG" : "MPEG"); } if (dynamic_cast(encoder.platform_formats.get())) { result = disp.make_avcodec_encode_device(pix_fmt); } else if (dynamic_cast(encoder.platform_formats.get())) { result = disp.make_nvenc_encode_device(pix_fmt); } if (result) { result->colorspace = colorspace; } return result; } std::optional make_synced_session(platf::display_t *disp, const encoder_t &encoder, platf::img_t &img, sync_session_ctx_t &ctx) { sync_session_t encode_session; encode_session.ctx = &ctx; auto encode_device = make_encode_device(*disp, encoder, ctx.config); if (!encode_device) { return std::nullopt; } // absolute mouse coordinates require that the dimensions of the screen are known ctx.touch_port_events->raise(make_port(disp, ctx.config)); // Update client with our current HDR display state hdr_info_t hdr_info = std::make_unique(false); if (colorspace_is_hdr(encode_device->colorspace)) { if (disp->get_hdr_metadata(hdr_info->metadata)) { hdr_info->enabled = true; } else { BOOST_LOG(error) << "Couldn't get display hdr metadata when colorspace selection indicates it should have one"; } } ctx.hdr_events->raise(std::move(hdr_info)); auto session = make_encode_session(disp, encoder, ctx.config, img.width, img.height, std::move(encode_device)); if (!session) { return std::nullopt; } // Load the initial image to prepare for encoding if (session->convert(img)) { BOOST_LOG(error) << "Could not convert initial image"sv; return std::nullopt; } encode_session.session = std::move(session); return encode_session; } encode_e encode_run_sync( std::vector> &synced_session_ctxs, encode_session_ctx_queue_t &encode_session_ctx_queue, std::vector &display_names, int &display_p ) { const auto &encoder = *chosen_encoder; std::shared_ptr disp; auto switch_display_event = mail::man->event(mail::switch_display); if (synced_session_ctxs.empty()) { auto ctx = encode_session_ctx_queue.pop(); if (!ctx) { return encode_e::ok; } synced_session_ctxs.emplace_back(std::make_unique(std::move(*ctx))); } while (encode_session_ctx_queue.running()) { // Refresh display names since a display removal might have caused the reinitialization refresh_displays(encoder.platform_formats->dev_type, display_names, display_p); // Process any pending display switch with the new list of displays if (switch_display_event->peek()) { display_p = std::clamp(*switch_display_event->pop(), 0, (int) display_names.size() - 1); } // reset_display() will sleep between retries reset_display(disp, encoder.platform_formats->dev_type, display_names[display_p], synced_session_ctxs.front()->config); if (disp) { break; } } if (!disp) { return encode_e::error; } auto img = disp->alloc_img(); if (!img || disp->dummy_img(img.get())) { return encode_e::error; } std::vector synced_sessions; for (auto &ctx : synced_session_ctxs) { auto synced_session = make_synced_session(disp.get(), encoder, *img, *ctx); if (!synced_session) { return encode_e::error; } synced_sessions.emplace_back(std::move(*synced_session)); } auto ec = platf::capture_e::ok; while (encode_session_ctx_queue.running()) { auto push_captured_image_callback = [&](std::shared_ptr &&img, bool frame_captured) -> bool { while (encode_session_ctx_queue.peek()) { auto encode_session_ctx = encode_session_ctx_queue.pop(); if (!encode_session_ctx) { return false; } synced_session_ctxs.emplace_back(std::make_unique(std::move(*encode_session_ctx))); auto encode_session = make_synced_session(disp.get(), encoder, *img, *synced_session_ctxs.back()); if (!encode_session) { ec = platf::capture_e::error; return false; } synced_sessions.emplace_back(std::move(*encode_session)); } KITTY_WHILE_LOOP(auto pos = std::begin(synced_sessions), pos != std::end(synced_sessions), { auto ctx = pos->ctx; if (ctx->shutdown_event->peek()) { // Let waiting thread know it can delete shutdown_event ctx->join_event->raise(true); pos = synced_sessions.erase(pos); synced_session_ctxs.erase(std::find_if(std::begin(synced_session_ctxs), std::end(synced_session_ctxs), [&ctx_p = ctx](auto &ctx) { return ctx.get() == ctx_p; })); if (synced_sessions.empty()) { return false; } continue; } if (ctx->idr_events->peek()) { pos->session->request_idr_frame(); ctx->idr_events->pop(); } if (frame_captured && pos->session->convert(*img)) { BOOST_LOG(error) << "Could not convert image"sv; ctx->shutdown_event->raise(true); continue; } std::optional frame_timestamp; if (img) { frame_timestamp = img->frame_timestamp; } if (encode(ctx->frame_nr++, *pos->session, ctx->packets, ctx->channel_data, frame_timestamp)) { BOOST_LOG(error) << "Could not encode video packet"sv; ctx->shutdown_event->raise(true); continue; } pos->session->request_normal_frame(); ++pos; }) if (switch_display_event->peek()) { ec = platf::capture_e::reinit; return false; } return true; }; auto pull_free_image_callback = [&img](std::shared_ptr &img_out) -> bool { img_out = img; img_out->frame_timestamp.reset(); return true; }; auto status = disp->capture(push_captured_image_callback, pull_free_image_callback, &display_cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: case platf::capture_e::ok: case platf::capture_e::timeout: case platf::capture_e::interrupted: return ec != platf::capture_e::ok ? ec : status; } } return encode_e::ok; } void captureThreadSync() { auto ref = capture_thread_sync.ref(); std::vector> synced_session_ctxs; auto &ctx = ref->encode_session_ctx_queue; auto lg = util::fail_guard([&]() { ctx.stop(); for (auto &ctx : synced_session_ctxs) { ctx->shutdown_event->raise(true); ctx->join_event->raise(true); } for (auto &ctx : ctx.unsafe()) { ctx.shutdown_event->raise(true); ctx.join_event->raise(true); } }); // Encoding and capture takes place on this thread platf::adjust_thread_priority(platf::thread_priority_e::high); std::vector display_names; int display_p = -1; while (encode_run_sync(synced_session_ctxs, ctx, display_names, display_p) == encode_e::reinit) {} } void capture_async( safe::mail_t mail, config_t &config, void *channel_data ) { auto shutdown_event = mail->event(mail::shutdown); auto images = std::make_shared(); auto lg = util::fail_guard([&]() { images->stop(); shutdown_event->raise(true); }); auto ref = capture_thread_async.ref(); if (!ref) { return; } ref->capture_ctx_queue->raise(capture_ctx_t {images, config}); if (!ref->capture_ctx_queue->running()) { return; } int frame_nr = 1; auto touch_port_event = mail->event(mail::touch_port); auto hdr_event = mail->event(mail::hdr); // Encoding takes place on this thread platf::adjust_thread_priority(platf::thread_priority_e::high); while (!shutdown_event->peek() && images->running()) { // Wait for the main capture event when the display is being reinitialized if (ref->reinit_event.peek()) { std::this_thread::sleep_for(20ms); continue; } // Wait for the display to be ready std::shared_ptr display; { auto lg = ref->display_wp.lock(); if (ref->display_wp->expired()) { continue; } display = ref->display_wp->lock(); } auto &encoder = *chosen_encoder; auto encode_device = make_encode_device(*display, encoder, config); if (!encode_device) { return; } // absolute mouse coordinates require that the dimensions of the screen are known touch_port_event->raise(make_port(display.get(), config)); // Update client with our current HDR display state hdr_info_t hdr_info = std::make_unique(false); if (colorspace_is_hdr(encode_device->colorspace)) { if (display->get_hdr_metadata(hdr_info->metadata)) { hdr_info->enabled = true; } else { BOOST_LOG(error) << "Couldn't get display hdr metadata when colorspace selection indicates it should have one"; } } hdr_event->raise(std::move(hdr_info)); encode_run( frame_nr, mail, images, config, display, std::move(encode_device), ref->reinit_event, *ref->encoder_p, channel_data ); } } void capture( safe::mail_t mail, config_t config, void *channel_data ) { auto idr_events = mail->event(mail::idr); idr_events->raise(true); if (chosen_encoder->flags & PARALLEL_ENCODING) { capture_async(std::move(mail), config, channel_data); } else { safe::signal_t join_event; auto ref = capture_thread_sync.ref(); ref->encode_session_ctx_queue.raise(sync_session_ctx_t { &join_event, mail->event(mail::shutdown), mail::man->queue(mail::video_packets), std::move(idr_events), mail->event(mail::hdr), mail->event(mail::touch_port), config, 1, channel_data, }); // Wait for join signal join_event.view(); } } enum validate_flag_e { VUI_PARAMS = 0x01, ///< VUI parameters }; int validate_config(std::shared_ptr disp, const encoder_t &encoder, const config_t &config) { auto encode_device = make_encode_device(*disp, encoder, config); if (!encode_device) { return -1; } auto session = make_encode_session(disp.get(), encoder, config, disp->width, disp->height, std::move(encode_device)); if (!session) { return -1; } { // Image buffers are large, so we use a separate scope to free it immediately after convert() auto img = disp->alloc_img(); if (!img || disp->dummy_img(img.get()) || session->convert(*img)) { return -1; } } session->request_idr_frame(); auto packets = mail::man->queue(mail::video_packets); while (!packets->peek()) { if (encode(1, *session, packets, nullptr, {})) { return -1; } } auto packet = packets->pop(); if (!packet->is_idr()) { BOOST_LOG(error) << "First packet type is not an IDR frame"sv; return -1; } int flag = 0; // This check only applies for H.264 and HEVC if (config.videoFormat <= 1) { if (auto packet_avcodec = dynamic_cast(packet.get())) { if (cbs::validate_sps(packet_avcodec->av_packet, config.videoFormat ? AV_CODEC_ID_H265 : AV_CODEC_ID_H264)) { flag |= VUI_PARAMS; } } else { // Don't check it for non-avcodec encoders. flag |= VUI_PARAMS; } } return flag; } bool validate_encoder(encoder_t &encoder, bool expect_failure) { const auto output_name {display_device::map_output_name(config::video.output_name)}; std::shared_ptr disp; BOOST_LOG(info) << "Trying encoder ["sv << encoder.name << ']'; auto fg = util::fail_guard([&]() { BOOST_LOG(info) << "Encoder ["sv << encoder.name << "] failed"sv; }); auto test_hevc = active_hevc_mode >= 2 || (active_hevc_mode == 0 && !(encoder.flags & H264_ONLY)); auto test_av1 = active_av1_mode >= 2 || (active_av1_mode == 0 && !(encoder.flags & H264_ONLY)); encoder.h264.capabilities.set(); encoder.hevc.capabilities.set(); encoder.av1.capabilities.set(); // First, test encoder viability config_t config_max_ref_frames {1920, 1080, 60, 1000, 1, 1, 1, 0, 0, 0}; config_t config_autoselect {1920, 1080, 60, 1000, 1, 0, 1, 0, 0, 0}; // If the encoder isn't supported at all (not even H.264), bail early reset_display(disp, encoder.platform_formats->dev_type, output_name, config_autoselect); if (!disp) { return false; } if (!disp->is_codec_supported(encoder.h264.name, config_autoselect)) { fg.disable(); BOOST_LOG(info) << "Encoder ["sv << encoder.name << "] is not supported on this GPU"sv; return false; } // If we're expecting failure, use the autoselect ref config first since that will always succeed // if the encoder is available. auto max_ref_frames_h264 = expect_failure ? -1 : validate_config(disp, encoder, config_max_ref_frames); auto autoselect_h264 = max_ref_frames_h264 >= 0 ? max_ref_frames_h264 : validate_config(disp, encoder, config_autoselect); if (autoselect_h264 < 0) { return false; } else if (expect_failure) { // We expected failure, but actually succeeded. Do the max_ref_frames probe we skipped. max_ref_frames_h264 = validate_config(disp, encoder, config_max_ref_frames); } std::vector> packet_deficiencies { {VUI_PARAMS, encoder_t::VUI_PARAMETERS}, }; for (auto [validate_flag, encoder_flag] : packet_deficiencies) { encoder.h264[encoder_flag] = (max_ref_frames_h264 & validate_flag && autoselect_h264 & validate_flag); } encoder.h264[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_h264 >= 0; encoder.h264[encoder_t::PASSED] = true; if (test_hevc) { config_max_ref_frames.videoFormat = 1; config_autoselect.videoFormat = 1; if (disp->is_codec_supported(encoder.hevc.name, config_autoselect)) { auto max_ref_frames_hevc = validate_config(disp, encoder, config_max_ref_frames); // If H.264 succeeded with max ref frames specified, assume that we can count on // HEVC to also succeed with max ref frames specified if HEVC is supported. auto autoselect_hevc = (max_ref_frames_hevc >= 0 || max_ref_frames_h264 >= 0) ? max_ref_frames_hevc : validate_config(disp, encoder, config_autoselect); for (auto [validate_flag, encoder_flag] : packet_deficiencies) { encoder.hevc[encoder_flag] = (max_ref_frames_hevc & validate_flag && autoselect_hevc & validate_flag); } encoder.hevc[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_hevc >= 0; encoder.hevc[encoder_t::PASSED] = max_ref_frames_hevc >= 0 || autoselect_hevc >= 0; } else { BOOST_LOG(info) << "Encoder ["sv << encoder.hevc.name << "] is not supported on this GPU"sv; encoder.hevc.capabilities.reset(); } } else { // Clear all cap bits for HEVC if we didn't probe it encoder.hevc.capabilities.reset(); } if (test_av1) { config_max_ref_frames.videoFormat = 2; config_autoselect.videoFormat = 2; if (disp->is_codec_supported(encoder.av1.name, config_autoselect)) { auto max_ref_frames_av1 = validate_config(disp, encoder, config_max_ref_frames); // If H.264 succeeded with max ref frames specified, assume that we can count on // AV1 to also succeed with max ref frames specified if AV1 is supported. auto autoselect_av1 = (max_ref_frames_av1 >= 0 || max_ref_frames_h264 >= 0) ? max_ref_frames_av1 : validate_config(disp, encoder, config_autoselect); for (auto [validate_flag, encoder_flag] : packet_deficiencies) { encoder.av1[encoder_flag] = (max_ref_frames_av1 & validate_flag && autoselect_av1 & validate_flag); } encoder.av1[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_av1 >= 0; encoder.av1[encoder_t::PASSED] = max_ref_frames_av1 >= 0 || autoselect_av1 >= 0; } else { BOOST_LOG(info) << "Encoder ["sv << encoder.av1.name << "] is not supported on this GPU"sv; encoder.av1.capabilities.reset(); } } else { // Clear all cap bits for AV1 if we didn't probe it encoder.av1.capabilities.reset(); } // Test HDR and YUV444 support { // H.264 is special because encoders may support YUV 4:4:4 without supporting 10-bit color depth if (encoder.flags & YUV444_SUPPORT) { config_t config_h264_yuv444 {1920, 1080, 60, 1000, 1, 0, 1, 0, 0, 1}; encoder.h264[encoder_t::YUV444] = disp->is_codec_supported(encoder.h264.name, config_h264_yuv444) && validate_config(disp, encoder, config_h264_yuv444) >= 0; } else { encoder.h264[encoder_t::YUV444] = false; } const config_t generic_hdr_config = {1920, 1080, 60, 1000, 1, 0, 3, 1, 1, 0}; // Reset the display since we're switching from SDR to HDR reset_display(disp, encoder.platform_formats->dev_type, output_name, generic_hdr_config); if (!disp) { return false; } auto test_hdr_and_yuv444 = [&](auto &flag_map, auto video_format) { auto config = generic_hdr_config; config.videoFormat = video_format; if (!flag_map[encoder_t::PASSED]) { return; } auto encoder_codec_name = encoder.codec_from_config(config).name; // Test 4:4:4 HDR first. If 4:4:4 is supported, 4:2:0 should also be supported. config.chromaSamplingType = 1; if ((encoder.flags & YUV444_SUPPORT) && disp->is_codec_supported(encoder_codec_name, config) && validate_config(disp, encoder, config) >= 0) { flag_map[encoder_t::DYNAMIC_RANGE] = true; flag_map[encoder_t::YUV444] = true; return; } else { flag_map[encoder_t::YUV444] = false; } // Test 4:2:0 HDR config.chromaSamplingType = 0; if (disp->is_codec_supported(encoder_codec_name, config) && validate_config(disp, encoder, config) >= 0) { flag_map[encoder_t::DYNAMIC_RANGE] = true; } else { flag_map[encoder_t::DYNAMIC_RANGE] = false; } }; // HDR is not supported with H.264. Don't bother even trying it. encoder.h264[encoder_t::DYNAMIC_RANGE] = false; test_hdr_and_yuv444(encoder.hevc, 1); test_hdr_and_yuv444(encoder.av1, 2); } encoder.h264[encoder_t::VUI_PARAMETERS] = encoder.h264[encoder_t::VUI_PARAMETERS] && !config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE]; encoder.hevc[encoder_t::VUI_PARAMETERS] = encoder.hevc[encoder_t::VUI_PARAMETERS] && !config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE]; if (!encoder.h264[encoder_t::VUI_PARAMETERS]) { BOOST_LOG(warning) << encoder.name << ": h264 missing sps->vui parameters"sv; } if (encoder.hevc[encoder_t::PASSED] && !encoder.hevc[encoder_t::VUI_PARAMETERS]) { BOOST_LOG(warning) << encoder.name << ": hevc missing sps->vui parameters"sv; } fg.disable(); return true; } int probe_encoders() { if (!allow_encoder_probing()) { // Error already logged return -1; } auto encoder_list = encoders; // If we already have a good encoder, check to see if another probe is required if (chosen_encoder && !(chosen_encoder->flags & ALWAYS_REPROBE) && !platf::needs_encoder_reenumeration()) { return 0; } // Restart encoder selection auto previous_encoder = chosen_encoder; chosen_encoder = nullptr; active_hevc_mode = config::video.hevc_mode; active_av1_mode = config::video.av1_mode; last_encoder_probe_supported_ref_frames_invalidation = false; auto adjust_encoder_constraints = [&](encoder_t *encoder) { // If we can't satisfy both the encoder and codec requirement, prefer the encoder over codec support if (active_hevc_mode == 3 && !encoder->hevc[encoder_t::DYNAMIC_RANGE]) { BOOST_LOG(warning) << "Encoder ["sv << encoder->name << "] does not support HEVC Main10 on this system"sv; active_hevc_mode = 0; } else if (active_hevc_mode == 2 && !encoder->hevc[encoder_t::PASSED]) { BOOST_LOG(warning) << "Encoder ["sv << encoder->name << "] does not support HEVC on this system"sv; active_hevc_mode = 0; } if (active_av1_mode == 3 && !encoder->av1[encoder_t::DYNAMIC_RANGE]) { BOOST_LOG(warning) << "Encoder ["sv << encoder->name << "] does not support AV1 Main10 on this system"sv; active_av1_mode = 0; } else if (active_av1_mode == 2 && !encoder->av1[encoder_t::PASSED]) { BOOST_LOG(warning) << "Encoder ["sv << encoder->name << "] does not support AV1 on this system"sv; active_av1_mode = 0; } }; if (!config::video.encoder.empty()) { // If there is a specific encoder specified, use it if it passes validation KITTY_WHILE_LOOP(auto pos = std::begin(encoder_list), pos != std::end(encoder_list), { auto encoder = *pos; if (encoder->name == config::video.encoder) { // Remove the encoder from the list entirely if it fails validation if (!validate_encoder(*encoder, previous_encoder && previous_encoder != encoder)) { pos = encoder_list.erase(pos); break; } // We will return an encoder here even if it fails one of the codec requirements specified by the user adjust_encoder_constraints(encoder); chosen_encoder = encoder; break; } pos++; }); if (chosen_encoder == nullptr) { BOOST_LOG(error) << "Couldn't find any working encoder matching ["sv << config::video.encoder << ']'; } } BOOST_LOG(info) << "// Testing for available encoders, this may generate errors. You can safely ignore those errors. //"sv; // If we haven't found an encoder yet, but we want one with specific codec support, search for that now. if (chosen_encoder == nullptr && (active_hevc_mode >= 2 || active_av1_mode >= 2)) { KITTY_WHILE_LOOP(auto pos = std::begin(encoder_list), pos != std::end(encoder_list), { auto encoder = *pos; // Remove the encoder from the list entirely if it fails validation if (!validate_encoder(*encoder, previous_encoder && previous_encoder != encoder)) { pos = encoder_list.erase(pos); continue; } // Skip it if it doesn't support the specified codec at all if ((active_hevc_mode >= 2 && !encoder->hevc[encoder_t::PASSED]) || (active_av1_mode >= 2 && !encoder->av1[encoder_t::PASSED])) { pos++; continue; } // Skip it if it doesn't support HDR on the specified codec if ((active_hevc_mode == 3 && !encoder->hevc[encoder_t::DYNAMIC_RANGE]) || (active_av1_mode == 3 && !encoder->av1[encoder_t::DYNAMIC_RANGE])) { pos++; continue; } chosen_encoder = encoder; break; }); if (chosen_encoder == nullptr) { BOOST_LOG(error) << "Couldn't find any working encoder that meets HEVC/AV1 requirements"sv; } } // If no encoder was specified or the specified encoder was unusable, keep trying // the remaining encoders until we find one that passes validation. if (chosen_encoder == nullptr) { KITTY_WHILE_LOOP(auto pos = std::begin(encoder_list), pos != std::end(encoder_list), { auto encoder = *pos; // If we've used a previous encoder and it's not this one, we expect this encoder to // fail to validate. It will use a slightly different order of checks to more quickly // eliminate failing encoders. if (!validate_encoder(*encoder, previous_encoder && previous_encoder != encoder)) { pos = encoder_list.erase(pos); continue; } // We will return an encoder here even if it fails one of the codec requirements specified by the user adjust_encoder_constraints(encoder); chosen_encoder = encoder; break; }); } if (chosen_encoder == nullptr) { const auto output_name {display_device::map_output_name(config::video.output_name)}; BOOST_LOG(fatal) << "Unable to find display or encoder during startup."sv; if (!config::video.adapter_name.empty() || !output_name.empty()) { BOOST_LOG(fatal) << "Please ensure your manually chosen GPU and monitor are connected and powered on."sv; } else { BOOST_LOG(fatal) << "Please check that a display is connected and powered on."sv; } return -1; } BOOST_LOG(info); BOOST_LOG(info) << "// Ignore any errors mentioned above, they are not relevant. //"sv; BOOST_LOG(info); auto &encoder = *chosen_encoder; last_encoder_probe_supported_ref_frames_invalidation = (encoder.flags & REF_FRAMES_INVALIDATION); last_encoder_probe_supported_yuv444_for_codec[0] = encoder.h264[encoder_t::PASSED] && encoder.h264[encoder_t::YUV444]; last_encoder_probe_supported_yuv444_for_codec[1] = encoder.hevc[encoder_t::PASSED] && encoder.hevc[encoder_t::YUV444]; last_encoder_probe_supported_yuv444_for_codec[2] = encoder.av1[encoder_t::PASSED] && encoder.av1[encoder_t::YUV444]; BOOST_LOG(debug) << "------ h264 ------"sv; for (int x = 0; x < encoder_t::MAX_FLAGS; ++x) { auto flag = (encoder_t::flag_e) x; BOOST_LOG(debug) << encoder_t::from_flag(flag) << (encoder.h264[flag] ? ": supported"sv : ": unsupported"sv); } BOOST_LOG(debug) << "-------------------"sv; BOOST_LOG(info) << "Found H.264 encoder: "sv << encoder.h264.name << " ["sv << encoder.name << ']'; if (encoder.hevc[encoder_t::PASSED]) { BOOST_LOG(debug) << "------ hevc ------"sv; for (int x = 0; x < encoder_t::MAX_FLAGS; ++x) { auto flag = (encoder_t::flag_e) x; BOOST_LOG(debug) << encoder_t::from_flag(flag) << (encoder.hevc[flag] ? ": supported"sv : ": unsupported"sv); } BOOST_LOG(debug) << "-------------------"sv; BOOST_LOG(info) << "Found HEVC encoder: "sv << encoder.hevc.name << " ["sv << encoder.name << ']'; } if (encoder.av1[encoder_t::PASSED]) { BOOST_LOG(debug) << "------ av1 ------"sv; for (int x = 0; x < encoder_t::MAX_FLAGS; ++x) { auto flag = (encoder_t::flag_e) x; BOOST_LOG(debug) << encoder_t::from_flag(flag) << (encoder.av1[flag] ? ": supported"sv : ": unsupported"sv); } BOOST_LOG(debug) << "-------------------"sv; BOOST_LOG(info) << "Found AV1 encoder: "sv << encoder.av1.name << " ["sv << encoder.name << ']'; } if (active_hevc_mode == 0) { active_hevc_mode = encoder.hevc[encoder_t::PASSED] ? (encoder.hevc[encoder_t::DYNAMIC_RANGE] ? 3 : 2) : 1; } if (active_av1_mode == 0) { active_av1_mode = encoder.av1[encoder_t::PASSED] ? (encoder.av1[encoder_t::DYNAMIC_RANGE] ? 3 : 2) : 1; } return 0; } // Linux only declaration typedef int (*vaapi_init_avcodec_hardware_input_buffer_fn)(platf::avcodec_encode_device_t *encode_device, AVBufferRef **hw_device_buf); util::Either vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) { avcodec_buffer_t hw_device_buf; // If an egl hwdevice if (encode_device->data) { if (((vaapi_init_avcodec_hardware_input_buffer_fn) encode_device->data)(encode_device, &hw_device_buf)) { return -1; } return hw_device_buf; } auto render_device = config::video.adapter_name.empty() ? nullptr : config::video.adapter_name.c_str(); auto status = av_hwdevice_ctx_create(&hw_device_buf, AV_HWDEVICE_TYPE_VAAPI, render_device, nullptr, 0); if (status < 0) { char string[AV_ERROR_MAX_STRING_SIZE]; BOOST_LOG(error) << "Failed to create a VAAPI device: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); return -1; } return hw_device_buf; } util::Either cuda_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) { avcodec_buffer_t hw_device_buf; auto status = av_hwdevice_ctx_create(&hw_device_buf, AV_HWDEVICE_TYPE_CUDA, nullptr, nullptr, 1 /* AV_CUDA_USE_PRIMARY_CONTEXT */); if (status < 0) { char string[AV_ERROR_MAX_STRING_SIZE]; BOOST_LOG(error) << "Failed to create a CUDA device: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); return -1; } return hw_device_buf; } util::Either vt_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) { avcodec_buffer_t hw_device_buf; auto status = av_hwdevice_ctx_create(&hw_device_buf, AV_HWDEVICE_TYPE_VIDEOTOOLBOX, nullptr, nullptr, 0); if (status < 0) { char string[AV_ERROR_MAX_STRING_SIZE]; BOOST_LOG(error) << "Failed to create a VideoToolbox device: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); return -1; } return hw_device_buf; } #ifdef _WIN32 } void do_nothing(void *) { } namespace video { util::Either dxgi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) { avcodec_buffer_t ctx_buf {av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_D3D11VA)}; auto ctx = (AVD3D11VADeviceContext *) ((AVHWDeviceContext *) ctx_buf->data)->hwctx; std::fill_n((std::uint8_t *) ctx, sizeof(AVD3D11VADeviceContext), 0); auto device = (ID3D11Device *) encode_device->data; device->AddRef(); ctx->device = device; ctx->lock_ctx = (void *) 1; ctx->lock = do_nothing; ctx->unlock = do_nothing; auto err = av_hwdevice_ctx_init(ctx_buf.get()); if (err) { char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Failed to create FFMpeg hardware device context: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return err; } return ctx_buf; } #endif int start_capture_async(capture_thread_async_ctx_t &capture_thread_ctx) { capture_thread_ctx.encoder_p = chosen_encoder; capture_thread_ctx.reinit_event.reset(); capture_thread_ctx.capture_ctx_queue = std::make_shared>(30); capture_thread_ctx.capture_thread = std::thread { captureThread, capture_thread_ctx.capture_ctx_queue, std::ref(capture_thread_ctx.display_wp), std::ref(capture_thread_ctx.reinit_event), std::ref(*capture_thread_ctx.encoder_p) }; return 0; } void end_capture_async(capture_thread_async_ctx_t &capture_thread_ctx) { capture_thread_ctx.capture_ctx_queue->stop(); capture_thread_ctx.capture_thread.join(); } int start_capture_sync(capture_thread_sync_ctx_t &ctx) { std::thread {&captureThreadSync}.detach(); return 0; } void end_capture_sync(capture_thread_sync_ctx_t &ctx) { } platf::mem_type_e map_base_dev_type(AVHWDeviceType type) { switch (type) { case AV_HWDEVICE_TYPE_D3D11VA: return platf::mem_type_e::dxgi; case AV_HWDEVICE_TYPE_VAAPI: return platf::mem_type_e::vaapi; case AV_HWDEVICE_TYPE_CUDA: return platf::mem_type_e::cuda; case AV_HWDEVICE_TYPE_NONE: return platf::mem_type_e::system; case AV_HWDEVICE_TYPE_VIDEOTOOLBOX: return platf::mem_type_e::videotoolbox; default: return platf::mem_type_e::unknown; } return platf::mem_type_e::unknown; } platf::pix_fmt_e map_pix_fmt(AVPixelFormat fmt) { switch (fmt) { case AV_PIX_FMT_VUYX: return platf::pix_fmt_e::ayuv; case AV_PIX_FMT_XV30: return platf::pix_fmt_e::y410; case AV_PIX_FMT_YUV420P10: return platf::pix_fmt_e::yuv420p10; case AV_PIX_FMT_YUV420P: return platf::pix_fmt_e::yuv420p; case AV_PIX_FMT_NV12: return platf::pix_fmt_e::nv12; case AV_PIX_FMT_P010: return platf::pix_fmt_e::p010; default: return platf::pix_fmt_e::unknown; } return platf::pix_fmt_e::unknown; } } // namespace video ================================================ FILE: src/video.h ================================================ /** * @file src/video.h * @brief Declarations for video. */ #pragma once // local includes #include "input.h" #include "platform/common.h" #include "thread_safe.h" #include "video_colorspace.h" extern "C" { #include #include } struct AVPacket; namespace video { /* Encoding configuration requested by remote client */ struct config_t { // DO NOT CHANGE ORDER OR ADD FIELDS IN THE MIDDLE!!!!! // ONLY APPEND NEW FIELD AFTERWARDS!!!!!!!!! // BIG F WORD to Sunshine!!!!!!!!! int width; // Video width in pixels int height; // Video height in pixels int framerate; // Requested framerate, used in individual frame bitrate budget calculation int bitrate; // Video bitrate in kilobits (1000 bits) for requested framerate int slicesPerFrame; // Number of slices per frame int numRefFrames; // Max number of reference frames /* Requested color range and SDR encoding colorspace, HDR encoding colorspace is always BT.2020+ST2084 Color range (encoderCscMode & 0x1) : 0 - limited, 1 - full SDR encoding colorspace (encoderCscMode >> 1) : 0 - BT.601, 1 - BT.709, 2 - BT.2020 */ int encoderCscMode; int videoFormat; // 0 - H.264, 1 - HEVC, 2 - AV1 /* Encoding color depth (bit depth): 0 - 8-bit, 1 - 10-bit HDR encoding activates when color depth is higher than 8-bit and the display which is being captured is operating in HDR mode */ int dynamicRange; int chromaSamplingType; // 0 - 4:2:0, 1 - 4:4:4 int enableIntraRefresh; // 0 - disabled, 1 - enabled int encodingFramerate; // Requested display framerate bool input_only; }; platf::mem_type_e map_base_dev_type(AVHWDeviceType type); platf::pix_fmt_e map_pix_fmt(AVPixelFormat fmt); void free_ctx(AVCodecContext *ctx); void free_frame(AVFrame *frame); void free_buffer(AVBufferRef *ref); using avcodec_ctx_t = util::safe_ptr; using avcodec_frame_t = util::safe_ptr; using avcodec_buffer_t = util::safe_ptr; using sws_t = util::safe_ptr; using img_event_t = std::shared_ptr>>; struct encoder_platform_formats_t { virtual ~encoder_platform_formats_t() = default; platf::mem_type_e dev_type; platf::pix_fmt_e pix_fmt_8bit, pix_fmt_10bit; platf::pix_fmt_e pix_fmt_yuv444_8bit, pix_fmt_yuv444_10bit; }; struct encoder_platform_formats_avcodec: encoder_platform_formats_t { using init_buffer_function_t = std::function(platf::avcodec_encode_device_t *)>; encoder_platform_formats_avcodec( const AVHWDeviceType &avcodec_base_dev_type, const AVHWDeviceType &avcodec_derived_dev_type, const AVPixelFormat &avcodec_dev_pix_fmt, const AVPixelFormat &avcodec_pix_fmt_8bit, const AVPixelFormat &avcodec_pix_fmt_10bit, const AVPixelFormat &avcodec_pix_fmt_yuv444_8bit, const AVPixelFormat &avcodec_pix_fmt_yuv444_10bit, const init_buffer_function_t &init_avcodec_hardware_input_buffer_function ): avcodec_base_dev_type {avcodec_base_dev_type}, avcodec_derived_dev_type {avcodec_derived_dev_type}, avcodec_dev_pix_fmt {avcodec_dev_pix_fmt}, avcodec_pix_fmt_8bit {avcodec_pix_fmt_8bit}, avcodec_pix_fmt_10bit {avcodec_pix_fmt_10bit}, avcodec_pix_fmt_yuv444_8bit {avcodec_pix_fmt_yuv444_8bit}, avcodec_pix_fmt_yuv444_10bit {avcodec_pix_fmt_yuv444_10bit}, init_avcodec_hardware_input_buffer {init_avcodec_hardware_input_buffer_function} { dev_type = map_base_dev_type(avcodec_base_dev_type); pix_fmt_8bit = map_pix_fmt(avcodec_pix_fmt_8bit); pix_fmt_10bit = map_pix_fmt(avcodec_pix_fmt_10bit); pix_fmt_yuv444_8bit = map_pix_fmt(avcodec_pix_fmt_yuv444_8bit); pix_fmt_yuv444_10bit = map_pix_fmt(avcodec_pix_fmt_yuv444_10bit); } AVHWDeviceType avcodec_base_dev_type, avcodec_derived_dev_type; AVPixelFormat avcodec_dev_pix_fmt; AVPixelFormat avcodec_pix_fmt_8bit, avcodec_pix_fmt_10bit; AVPixelFormat avcodec_pix_fmt_yuv444_8bit, avcodec_pix_fmt_yuv444_10bit; init_buffer_function_t init_avcodec_hardware_input_buffer; }; struct encoder_platform_formats_nvenc: encoder_platform_formats_t { encoder_platform_formats_nvenc( const platf::mem_type_e &dev_type, const platf::pix_fmt_e &pix_fmt_8bit, const platf::pix_fmt_e &pix_fmt_10bit, const platf::pix_fmt_e &pix_fmt_yuv444_8bit, const platf::pix_fmt_e &pix_fmt_yuv444_10bit ) { encoder_platform_formats_t::dev_type = dev_type; encoder_platform_formats_t::pix_fmt_8bit = pix_fmt_8bit; encoder_platform_formats_t::pix_fmt_10bit = pix_fmt_10bit; encoder_platform_formats_t::pix_fmt_yuv444_8bit = pix_fmt_yuv444_8bit; encoder_platform_formats_t::pix_fmt_yuv444_10bit = pix_fmt_yuv444_10bit; } }; struct encoder_t { std::string_view name; enum flag_e { PASSED, ///< Indicates the encoder is supported. REF_FRAMES_RESTRICT, ///< Set maximum reference frames. DYNAMIC_RANGE, ///< HDR support. YUV444, ///< YUV 4:4:4 support. VUI_PARAMETERS, ///< AMD encoder with VAAPI doesn't add VUI parameters to SPS. MAX_FLAGS ///< Maximum number of flags. }; static std::string_view from_flag(flag_e flag) { #define _CONVERT(x) \ case flag_e::x: \ return std::string_view(#x) switch (flag) { _CONVERT(PASSED); _CONVERT(REF_FRAMES_RESTRICT); _CONVERT(DYNAMIC_RANGE); _CONVERT(YUV444); _CONVERT(VUI_PARAMETERS); _CONVERT(MAX_FLAGS); } #undef _CONVERT return {"unknown"}; } struct option_t { KITTY_DEFAULT_CONSTR_MOVE(option_t) option_t(const option_t &) = default; std::string name; std::variant *, std::function, std::string, std::string *, std::function> value; option_t(std::string &&name, decltype(value) &&value): name {std::move(name)}, value {std::move(value)} { } }; const std::unique_ptr platform_formats; struct codec_t { std::vector common_options; std::vector sdr_options; std::vector hdr_options; std::vector sdr444_options; std::vector hdr444_options; std::vector fallback_options; std::string name; std::bitset capabilities; bool operator[](flag_e flag) const { return capabilities[(std::size_t) flag]; } std::bitset::reference operator[](flag_e flag) { return capabilities[(std::size_t) flag]; } } av1, hevc, h264; const codec_t &codec_from_config(const config_t &config) const { switch (config.videoFormat) { default: BOOST_LOG(error) << "Unknown video format " << config.videoFormat << ", falling back to H.264"; // fallthrough case 0: return h264; case 1: return hevc; case 2: return av1; } } uint32_t flags; }; struct encode_session_t { virtual ~encode_session_t() = default; virtual int convert(platf::img_t &img) = 0; virtual void request_idr_frame() = 0; virtual void request_normal_frame() = 0; virtual void invalidate_ref_frames(int64_t first_frame, int64_t last_frame) = 0; }; // encoders extern encoder_t software; #if !defined(__APPLE__) extern encoder_t nvenc; // available for windows and linux #endif #ifdef _WIN32 extern encoder_t amdvce; extern encoder_t quicksync; #endif #ifdef __linux__ extern encoder_t vaapi; #endif #ifdef __APPLE__ extern encoder_t videotoolbox; #endif struct packet_raw_t { virtual ~packet_raw_t() = default; virtual bool is_idr() = 0; virtual int64_t frame_index() = 0; virtual uint8_t *data() = 0; virtual size_t data_size() = 0; struct replace_t { std::string_view old; std::string_view _new; KITTY_DEFAULT_CONSTR_MOVE(replace_t) replace_t(std::string_view old, std::string_view _new) noexcept: old {std::move(old)}, _new {std::move(_new)} { } }; std::vector *replacements = nullptr; void *channel_data = nullptr; bool after_ref_frame_invalidation = false; std::optional frame_timestamp; }; struct packet_raw_avcodec: packet_raw_t { packet_raw_avcodec() { av_packet = av_packet_alloc(); } ~packet_raw_avcodec() { av_packet_free(&this->av_packet); } bool is_idr() override { return av_packet->flags & AV_PKT_FLAG_KEY; } int64_t frame_index() override { return av_packet->pts; } uint8_t *data() override { return av_packet->data; } size_t data_size() override { return av_packet->size; } AVPacket *av_packet; }; struct packet_raw_generic: packet_raw_t { packet_raw_generic(std::vector &&frame_data, int64_t frame_index, bool idr): frame_data {std::move(frame_data)}, index {frame_index}, idr {idr} { } bool is_idr() override { return idr; } int64_t frame_index() override { return index; } uint8_t *data() override { return frame_data.data(); } size_t data_size() override { return frame_data.size(); } std::vector frame_data; int64_t index; bool idr; }; using packet_t = std::unique_ptr; struct hdr_info_raw_t { explicit hdr_info_raw_t(bool enabled): enabled {enabled}, metadata {} {}; explicit hdr_info_raw_t(bool enabled, const SS_HDR_METADATA &metadata): enabled {enabled}, metadata {metadata} {}; bool enabled; SS_HDR_METADATA metadata; }; using hdr_info_t = std::unique_ptr; extern int active_hevc_mode; extern int active_av1_mode; extern bool last_encoder_probe_supported_ref_frames_invalidation; extern std::array last_encoder_probe_supported_yuv444_for_codec; // 0 - H.264, 1 - HEVC, 2 - AV1 void capture( safe::mail_t mail, config_t config, void *channel_data ); bool validate_encoder(encoder_t &encoder, bool expect_failure); /** * @brief Check if we can allow probing for the encoders. * @return True if there should be no issues with the probing, false if we should prevent it. */ bool allow_encoder_probing(); /** * @brief Probe encoders and select the preferred encoder. * This is called once at startup and each time a stream is launched to * ensure the best encoder is selected. Encoder availability can change * at runtime due to all sorts of things from driver updates to eGPUs. * * @warning This is only safe to call when there is no client actively streaming. */ int probe_encoders(); } // namespace video ================================================ FILE: src/video_colorspace.cpp ================================================ /** * @file src/video_colorspace.cpp * @brief Definitions for colorspace functions. */ // this include #include "video_colorspace.h" // local includes #include "logging.h" #include "video.h" extern "C" { #include } namespace video { bool colorspace_is_hdr(const sunshine_colorspace_t &colorspace) { return colorspace.colorspace == colorspace_e::bt2020; } sunshine_colorspace_t colorspace_from_client_config(const config_t &config, bool hdr_display) { sunshine_colorspace_t colorspace; /* See video::config_t declaration for details */ BOOST_LOG(info) << "Client dynamicRange: " << config.dynamicRange << ", Display is HDR: " << hdr_display; if (config.dynamicRange > 0 && hdr_display) { // Rec. 2020 with ST 2084 perceptual quantizer colorspace.colorspace = colorspace_e::bt2020; } else { switch (config.encoderCscMode >> 1) { case 0: // Rec. 601 colorspace.colorspace = colorspace_e::rec601; break; case 1: // Rec. 709 colorspace.colorspace = colorspace_e::rec709; break; case 2: // Rec. 2020 colorspace.colorspace = colorspace_e::bt2020sdr; break; default: BOOST_LOG(error) << "Unknown video colorspace in csc, falling back to Rec. 709"; colorspace.colorspace = colorspace_e::rec709; break; } } colorspace.full_range = (config.encoderCscMode & 0x1); switch (config.dynamicRange) { case 0: colorspace.bit_depth = 8; break; case 1: colorspace.bit_depth = 10; break; default: BOOST_LOG(error) << "Unknown dynamicRange value, falling back to 10-bit color depth"; colorspace.bit_depth = 10; break; } if (colorspace.colorspace == colorspace_e::bt2020sdr && colorspace.bit_depth != 10) { BOOST_LOG(error) << "BT.2020 SDR colorspace expects 10-bit color depth, falling back to Rec. 709"; colorspace.colorspace = colorspace_e::rec709; } return colorspace; } avcodec_colorspace_t avcodec_colorspace_from_sunshine_colorspace(const sunshine_colorspace_t &sunshine_colorspace) { avcodec_colorspace_t avcodec_colorspace; switch (sunshine_colorspace.colorspace) { case colorspace_e::rec601: // Rec. 601 avcodec_colorspace.primaries = AVCOL_PRI_SMPTE170M; avcodec_colorspace.transfer_function = AVCOL_TRC_SMPTE170M; avcodec_colorspace.matrix = AVCOL_SPC_SMPTE170M; avcodec_colorspace.software_format = SWS_CS_SMPTE170M; break; case colorspace_e::rec709: // Rec. 709 avcodec_colorspace.primaries = AVCOL_PRI_BT709; avcodec_colorspace.transfer_function = AVCOL_TRC_BT709; avcodec_colorspace.matrix = AVCOL_SPC_BT709; avcodec_colorspace.software_format = SWS_CS_ITU709; break; case colorspace_e::bt2020sdr: // Rec. 2020 avcodec_colorspace.primaries = AVCOL_PRI_BT2020; assert(sunshine_colorspace.bit_depth == 10); avcodec_colorspace.transfer_function = AVCOL_TRC_BT2020_10; avcodec_colorspace.matrix = AVCOL_SPC_BT2020_NCL; avcodec_colorspace.software_format = SWS_CS_BT2020; break; case colorspace_e::bt2020: // Rec. 2020 with ST 2084 perceptual quantizer avcodec_colorspace.primaries = AVCOL_PRI_BT2020; assert(sunshine_colorspace.bit_depth == 10); avcodec_colorspace.transfer_function = AVCOL_TRC_SMPTE2084; avcodec_colorspace.matrix = AVCOL_SPC_BT2020_NCL; avcodec_colorspace.software_format = SWS_CS_BT2020; break; } avcodec_colorspace.range = sunshine_colorspace.full_range ? AVCOL_RANGE_JPEG : AVCOL_RANGE_MPEG; return avcodec_colorspace; } const color_t *color_vectors_from_colorspace(const sunshine_colorspace_t &colorspace) { return color_vectors_from_colorspace(colorspace.colorspace, colorspace.full_range); } const color_t *color_vectors_from_colorspace(colorspace_e colorspace, bool full_range) { using float2 = float[2]; auto make_color_matrix = [](float Cr, float Cb, const float2 &range_Y, const float2 &range_UV) -> color_t { float Cg = 1.0f - Cr - Cb; float Cr_i = 1.0f - Cr; float Cb_i = 1.0f - Cb; float shift_y = range_Y[0] / 255.0f; float shift_uv = range_UV[0] / 255.0f; float scale_y = (range_Y[1] - range_Y[0]) / 255.0f; float scale_uv = (range_UV[1] - range_UV[0]) / 255.0f; return { {Cr, Cg, Cb, 0.0f}, {-(Cr * 0.5f / Cb_i), -(Cg * 0.5f / Cb_i), 0.5f, 0.5f}, {0.5f, -(Cg * 0.5f / Cr_i), -(Cb * 0.5f / Cr_i), 0.5f}, {scale_y, shift_y}, {scale_uv, shift_uv}, }; }; static const color_t colors[] { make_color_matrix(0.299f, 0.114f, {16.0f, 235.0f}, {16.0f, 240.0f}), // BT601 MPEG make_color_matrix(0.299f, 0.114f, {0.0f, 255.0f}, {0.0f, 255.0f}), // BT601 JPEG make_color_matrix(0.2126f, 0.0722f, {16.0f, 235.0f}, {16.0f, 240.0f}), // BT709 MPEG make_color_matrix(0.2126f, 0.0722f, {0.0f, 255.0f}, {0.0f, 255.0f}), // BT709 JPEG make_color_matrix(0.2627f, 0.0593f, {16.0f, 235.0f}, {16.0f, 240.0f}), // BT2020 MPEG make_color_matrix(0.2627f, 0.0593f, {0.0f, 255.0f}, {0.0f, 255.0f}), // BT2020 JPEG }; const color_t *result = nullptr; switch (colorspace) { case colorspace_e::rec601: default: result = &colors[0]; break; case colorspace_e::rec709: result = &colors[2]; break; case colorspace_e::bt2020: case colorspace_e::bt2020sdr: result = &colors[4]; break; }; if (full_range) { result++; } return result; } const color_t *new_color_vectors_from_colorspace(const sunshine_colorspace_t &colorspace) { constexpr auto generate_color_vectors = [](const sunshine_colorspace_t &colorspace) -> color_t { double Kr, Kb; switch (colorspace.colorspace) { case colorspace_e::rec601: Kr = 0.299; Kb = 0.114; break; case colorspace_e::rec709: default: Kr = 0.2126; Kb = 0.0722; break; case colorspace_e::bt2020: case colorspace_e::bt2020sdr: Kr = 0.2627; Kb = 0.0593; break; } double Kg = 1.0 - Kr - Kb; double y_mult, y_add; double uv_mult, uv_add; // "Matrix coefficients" section of ITU-T H.273 if (colorspace.full_range) { y_mult = (1 << colorspace.bit_depth) - 1; y_add = 0; uv_mult = (1 << colorspace.bit_depth) - 1; uv_add = (1 << (colorspace.bit_depth - 1)); } else { y_mult = (1 << (colorspace.bit_depth - 8)) * 219; y_add = (1 << (colorspace.bit_depth - 8)) * 16; uv_mult = (1 << (colorspace.bit_depth - 8)) * 224; uv_add = (1 << (colorspace.bit_depth - 8)) * 128; } // For rounding y_add += 0.5; uv_add += 0.5; color_t color_vectors; color_vectors.color_vec_y[0] = Kr * y_mult; color_vectors.color_vec_y[1] = Kg * y_mult; color_vectors.color_vec_y[2] = Kb * y_mult; color_vectors.color_vec_y[3] = y_add; color_vectors.color_vec_u[0] = -0.5 * Kr / (1.0 - Kb) * uv_mult; color_vectors.color_vec_u[1] = -0.5 * Kg / (1.0 - Kb) * uv_mult; color_vectors.color_vec_u[2] = 0.5 * uv_mult; color_vectors.color_vec_u[3] = uv_add; color_vectors.color_vec_v[0] = 0.5 * uv_mult; color_vectors.color_vec_v[1] = -0.5 * Kg / (1.0 - Kr) * uv_mult; color_vectors.color_vec_v[2] = -0.5 * Kb / (1.0 - Kr) * uv_mult; color_vectors.color_vec_v[3] = uv_add; // Unused color_vectors.range_y[0] = 1; color_vectors.range_y[1] = 0; color_vectors.range_uv[0] = 1; color_vectors.range_uv[1] = 0; return color_vectors; }; static constexpr color_t colors[] = { generate_color_vectors({colorspace_e::rec601, false, 8}), generate_color_vectors({colorspace_e::rec601, true, 8}), generate_color_vectors({colorspace_e::rec601, false, 10}), generate_color_vectors({colorspace_e::rec601, true, 10}), generate_color_vectors({colorspace_e::rec709, false, 8}), generate_color_vectors({colorspace_e::rec709, true, 8}), generate_color_vectors({colorspace_e::rec709, false, 10}), generate_color_vectors({colorspace_e::rec709, true, 10}), generate_color_vectors({colorspace_e::bt2020, false, 8}), generate_color_vectors({colorspace_e::bt2020, true, 8}), generate_color_vectors({colorspace_e::bt2020, false, 10}), generate_color_vectors({colorspace_e::bt2020, true, 10}), }; const color_t *result = nullptr; switch (colorspace.colorspace) { case colorspace_e::rec601: result = &colors[0]; break; case colorspace_e::rec709: default: result = &colors[4]; break; case colorspace_e::bt2020: case colorspace_e::bt2020sdr: result = &colors[8]; break; } if (colorspace.bit_depth == 10) { result += 2; } if (colorspace.full_range) { result += 1; } return result; } } // namespace video ================================================ FILE: src/video_colorspace.h ================================================ /** * @file src/video_colorspace.h * @brief Declarations for colorspace functions. */ #pragma once extern "C" { #include } namespace video { enum class colorspace_e { rec601, ///< Rec. 601 rec709, ///< Rec. 709 bt2020sdr, ///< Rec. 2020 SDR bt2020, ///< Rec. 2020 HDR }; struct sunshine_colorspace_t { colorspace_e colorspace; bool full_range; unsigned bit_depth; }; bool colorspace_is_hdr(const sunshine_colorspace_t &colorspace); // Declared in video.h struct config_t; sunshine_colorspace_t colorspace_from_client_config(const config_t &config, bool hdr_display); struct avcodec_colorspace_t { AVColorPrimaries primaries; AVColorTransferCharacteristic transfer_function; AVColorSpace matrix; AVColorRange range; int software_format; }; avcodec_colorspace_t avcodec_colorspace_from_sunshine_colorspace(const sunshine_colorspace_t &sunshine_colorspace); struct alignas(16) color_t { float color_vec_y[4]; float color_vec_u[4]; float color_vec_v[4]; float range_y[2]; float range_uv[2]; }; const color_t *color_vectors_from_colorspace(const sunshine_colorspace_t &colorspace); const color_t *color_vectors_from_colorspace(colorspace_e colorspace, bool full_range); /** * @brief New version of `color_vectors_from_colorspace()` function that better adheres to the standards. * Returned vectors are used to perform RGB->YUV conversion. * Unlike its predecessor, color vectors will produce output in `UINT` range, not `UNORM` range. * Input is still in `UNORM` range. Returned vectors won't modify color primaries and color * transfer function. * @param colorspace Targeted YUV colorspace. * @return `const color_t*` that contains RGB->YUV transformation vectors. * Components `range_y` and `range_uv` are there for backwards compatibility * and can be ignored in the computation. */ const color_t *new_color_vectors_from_colorspace(const sunshine_colorspace_t &colorspace); } // namespace video ================================================ FILE: src/zwpad.h ================================================ // zero_width_pad.hpp #pragma once #include #include // std::bit_width – C++20 #include namespace zwpad { // Two distinct zero-width characters. // U+200B ZERO WIDTH SPACE – “0” // U+200C ZERO WIDTH NON-JOINER – “1” inline constexpr char8_t ZW0[] = u8"\u200B"; inline constexpr char8_t ZW1[] = u8"\u200C"; /// \brief Encode \p index with a fixed-width binary prefix made of /// zero-width code-points and append the original text. /// /// \param text The payload you actually want to keep visible. /// \param padBits How many zero-width *digits* to prepend. /// (Usually: std::bit_width(count-1).) /// \param index Position in the ordered set, **0-based**. /// /// \return A UTF-8 std::string whose first \p padBits characters are /// either U+200B or U+200C, followed by \p text. /// /// The lexical order of the resulting strings corresponds to the /// numerical order of *index* because U+200B < U+200C. /// inline std::string pad_for_ordering(std::string_view text, std::size_t padBits, std::size_t index) { if (padBits == 0) throw std::invalid_argument("padBits must be > 0"); if (index >= (std::size_t{1} << padBits)) throw std::out_of_range("index does not fit into padBits"); std::string out; out.reserve(padBits * 3 + text.size()); // each ZW char is 3-byte UTF-8 for (std::size_t bit = 0; bit < padBits; ++bit) { // Emit the *most* significant bit first. const bool one = (index >> (padBits - 1 - bit)) & 1; out += one ? reinterpret_cast(ZW1) : reinterpret_cast(ZW0); } out.append(text); return out; } /// Convenience: compute the minimal pad width from the total count. [[nodiscard]] inline std::size_t pad_width_for_count(std::size_t count) { if (count == 0) throw std::invalid_argument("count must be > 0"); return std::bit_width(count - 1); // e.g. count==8 → 3 bits } } // namespace zwpad ================================================ FILE: src_assets/common/assets/web/Checkbox.vue ================================================ ================================================ FILE: src_assets/common/assets/web/ClientCard.vue ================================================ ================================================ FILE: src_assets/common/assets/web/Navbar.vue ================================================ ================================================ FILE: src_assets/common/assets/web/PlatformLayout.vue ================================================ ================================================ FILE: src_assets/common/assets/web/ResourceCard.vue ================================================ ================================================ FILE: src_assets/common/assets/web/ThemeToggle.vue ================================================ ================================================ FILE: src_assets/common/assets/web/apollo_version.js ================================================ class ApolloVersion { constructor(release = null, version = null) { if (release) { this.release = release; this.version = release.tag_name; this.versionName = release.name; this.versionTag = release.tag_tag; } else if (version) { this.release = null; this.version = version; this.versionName = null; this.versionTag = null; } else { throw new Error('Either release or version must be provided'); } this.versionParts = this.parseVersion(this.version); this.versionMajor = this.versionParts ? this.versionParts[0] : null; this.versionMinor = this.versionParts ? this.versionParts[1] : null; this.versionPatch = this.versionParts ? this.versionParts[2] : null; this.versionIncremental = this.versionParts ? this.versionParts[3] : null; } parseVersion(version) { if (!version) { return null; } let v = version; if (v.indexOf("v") === 0) { v = v.substring(1); } const [mainVer, incrementalVer] = v.split('-'); const versionParts = mainVer.split('.').map(i => parseInt(i, 10)); if (incrementalVer) { const [prefix, verStr] = incrementalVer.split('.'); let incremental = parseInt(verStr, 10); if (prefix === 'beta') { // we couldn't have 2^16 alpha versions? incremental <<= 16; } versionParts.push(incremental); } return versionParts; } isGreater(otherVersion, checkIncremental) { let otherVersionParts; if (otherVersion instanceof ApolloVersion) { otherVersionParts = otherVersion.versionParts; } else if (typeof otherVersion === 'string') { otherVersionParts = this.parseVersion(otherVersion); } else { throw new Error('Invalid argument: otherVersion must be a ApolloVersion object or a version string'); } if (!this.versionParts || !otherVersionParts) { return false; } for (let i = 0; i < Math.min(checkIncremental && 4 || 3, this.versionParts.length, otherVersionParts.length); i++) { if (this.versionParts[i] > otherVersionParts[i]) { return true; } else if (this.versionParts[i] < otherVersionParts[i]) { return false; } } return false; } } export default ApolloVersion; ================================================ FILE: src_assets/common/assets/web/apps.html ================================================ <%- header %>

{{ $t('apps.applications_title') }}

{{ $t('apps.applications_desc') }}
{{ $t('apps.applications_reorder_desc') }}
{{ $t('apps.applications_tips') }}

{{ editForm.name || "< NO NAME >"}}

{{ $t('apps.name') }} {{ $t('apps.actions') }}
{{app.name || ' '}}
{{ $t('apps.app_name_desc') }}
{{ $t('apps.image_desc') }}
{{ $t('config.gamepad_desc') }}
{{ $t('apps.cmd_desc') }}
{{ $t('_common.note') }} {{ $t('apps.cmd_note') }}
{{ $t('apps.detached_cmds_desc') }}
{{ $t('_common.note') }} {{ $t('apps.detached_cmds_note') }}
{{ $t('apps.working_dir_desc') }}
{{ $t('apps.output_desc') }}
{{ $t('apps.exit_timeout_desc') }}
{{ $t('apps.resolution_scale_factor_desc') }}

{{ $t('apps.env_vars_about') }}

{{ $t('apps.env_vars_desc') }}
{{ $t('apps.env_var_name') }}
APOLLO_APP_ID {{ $t('apps.env_app_id') }}
APOLLO_APP_NAME {{ $t('apps.env_app_name') }}
APOLLO_APP_UUID {{ $t('apps.env_app_uuid') }}
APOLLO_APP_STATUS {{ $t('apps.env_app_status') }}
APOLLO_CLIENT_UUID {{ $t('apps.env_client_uuid') }}
APOLLO_CLIENT_NAME {{ $t('apps.env_client_name') }}
APOLLO_CLIENT_WIDTH {{ $t('apps.env_client_width') }}
APOLLO_CLIENT_HEIGHT {{ $t('apps.env_client_height') }}
APOLLO_CLIENT_FPS {{ $t('apps.env_client_fps') }}
APOLLO_CLIENT_HDR {{ $t('apps.env_client_hdr') }}
APOLLO_CLIENT_GCMAP {{ $t('apps.env_client_gcmap') }}
APOLLO_CLIENT_HOST_AUDIO {{ $t('apps.env_client_host_audio') }}
APOLLO_CLIENT_ENABLE_SOPS {{ $t('apps.env_client_enable_sops') }}
APOLLO_CLIENT_AUDIO_CONFIGURATION {{ $t('apps.env_client_audio_config') }}
{{ $t('apps.env_sunshine_compatibility') }}
================================================ FILE: src_assets/common/assets/web/config.html ================================================ <%- header %>

{{ $t('config.configuration') }}

{{ $t('_common.success') }} {{ $t('config.apply_note') }}
{{ $t('_common.success') }} {{ $t('config.restart_note') }}
================================================ FILE: src_assets/common/assets/web/configs/tabs/Advanced.vue ================================================ ================================================ FILE: src_assets/common/assets/web/configs/tabs/AudioVideo.vue ================================================ ================================================ FILE: src_assets/common/assets/web/configs/tabs/ContainerEncoders.vue ================================================ ================================================ FILE: src_assets/common/assets/web/configs/tabs/Files.vue ================================================ ================================================ FILE: src_assets/common/assets/web/configs/tabs/General.vue ================================================ ================================================ FILE: src_assets/common/assets/web/configs/tabs/Inputs.vue ================================================ ================================================ FILE: src_assets/common/assets/web/configs/tabs/Network.vue ================================================ ================================================ FILE: src_assets/common/assets/web/configs/tabs/audiovideo/AdapterNameSelector.vue ================================================ ================================================ FILE: src_assets/common/assets/web/configs/tabs/audiovideo/DisplayDeviceOptions.vue ================================================ ================================================ FILE: src_assets/common/assets/web/configs/tabs/audiovideo/DisplayModesSettings.vue ================================================ ================================================ FILE: src_assets/common/assets/web/configs/tabs/audiovideo/DisplayOutputSelector.vue ================================================ ================================================ FILE: src_assets/common/assets/web/configs/tabs/encoders/AmdAmfEncoder.vue ================================================ ================================================ FILE: src_assets/common/assets/web/configs/tabs/encoders/IntelQuickSyncEncoder.vue ================================================ ================================================ FILE: src_assets/common/assets/web/configs/tabs/encoders/NvidiaNvencEncoder.vue ================================================ ================================================ FILE: src_assets/common/assets/web/configs/tabs/encoders/SoftwareEncoder.vue ================================================ ================================================ FILE: src_assets/common/assets/web/configs/tabs/encoders/VAAPIEncoder.vue ================================================ ================================================ FILE: src_assets/common/assets/web/configs/tabs/encoders/VideotoolboxEncoder.vue ================================================ ================================================ FILE: src_assets/common/assets/web/index.html ================================================ <%- header %>

{{ $t('index.welcome') }}

{{ $t('index.description') }}


  • {{v.value}}
View Logs

Version {{version.version}}


{{ $t('index.loading_latest') }}
{{ $t('index.version_dirty') }} 🌇
{{ $t('index.installed_version_not_stable') }}
{{ $t('index.version_latest') }}

{{ $t('index.new_pre_release') }}

{{ $t('index.download') }}
{{preReleaseVersion.release.name}}
{{preReleaseVersion.release.body}}

{{ $t('index.new_stable') }}

{{ $t('index.download') }}

{{githubVersion.release.name}}

{{githubVersion.release.body}}
================================================ FILE: src_assets/common/assets/web/init.js ================================================ import i18n from './locale' // must import even if not implicitly using here // https://github.com/aurelia/skeleton-navigation/issues/894 // https://discourse.aurelia.io/t/bootstrap-import-bootstrap-breaks-dropdown-menu-in-navbar/641/9 import 'bootstrap/dist/js/bootstrap' export function initApp(app, config) { //Wait for locale initialization, then render i18n().then(i18n => { app.use(i18n); app.provide('i18n', i18n.global) app.mount('#app'); if (config) { config(app) } }); } ================================================ FILE: src_assets/common/assets/web/locale.js ================================================ import {createI18n} from "vue-i18n"; // Import only the fallback language files import en from './public/assets/locale/en.json' export default async function() { let r = await (await fetch("./api/configLocale", { credentials: 'include' })).json(); let locale = r.locale ?? "en"; document.querySelector('html').setAttribute('lang', locale); let messages = { en }; try { if (locale !== 'en') { let r = await (await fetch(`./assets/locale/${locale}.json`, { credentials: 'include' })).json(); messages[locale] = r; } } catch (e) { console.error("Failed to download translations", e); } const i18n = createI18n({ locale: locale, // set locale fallbackLocale: 'en', // set fallback locale messages: messages }) return i18n; } ================================================ FILE: src_assets/common/assets/web/login.html ================================================ <%- header %>

{{ $t('welcome.greeting') }}

{{ $t('_common.error') }} {{error}}
{{ $t('_common.success') }} {{ $t('welcome.login_success') }}
================================================ FILE: src_assets/common/assets/web/password.html ================================================ <%- header %>

{{ $t('password.password_change') }}

{{ $t('password.current_creds') }}

 

{{ $t('password.new_creds') }}

{{ $t('password.new_username_desc') }}
Error: {{error}}
{{ $t('_common.success') }} {{ $t('password.success_msg') }}
================================================ FILE: src_assets/common/assets/web/pin.html ================================================ <%- header %>

art://{{ hostAddr }}:{{ hostPort }}

{{ otp && otp || '????' }}

{{ otpMessage }}
{{ $t('pin.otp_msg') }}
{{ $t('_common.warning') }} {{ $t('pin.warning_msg') }}

{{ $t('pin.device_management') }}


{{ $t('pin.device_management_desc') }}

{{ $t('pin.device_management_warning') }} {{ $t('_common.learn_more') }}

{{ $t('_common.success') }} {{ $t('pin.unpair_single_success') }}
{{ $t('pin.unpair_all_success') }}
{{ $t('pin.unpair_all_error') }}
    {{ $t('pin.unpair_single_no_devices') }}
================================================ FILE: src_assets/common/assets/web/platform-i18n.js ================================================ import {inject} from 'vue' class PlatformMessageI18n { /** * @param {string} platform */ constructor(platform) { this.platform = platform } /** * @param {string} key * @param {string} platform identifier * @return {string} key with platform identifier */ getPlatformKey(key, platform) { return key + '_' + platform } /** * @param {string} key * @param {string?} defaultMsg * @return {string} translated message or defaultMsg if provided */ getMessageUsingPlatform(key, defaultMsg) { const realKey = this.getPlatformKey(key, this.platform) const i18n = inject('i18n') let message = i18n.t(realKey) if (message !== realKey) { // We got a message back, return early return message } // If on Windows, we don't fallback to unix, so return early if (this.platform === 'windows') { return defaultMsg ? defaultMsg : message } // there's no message for key, check for unix version const unixKey = this.getPlatformKey(key, 'unix') message = i18n.t(unixKey) if (message === unixKey && defaultMsg) { // there's no message for unix key, return defaultMsg return defaultMsg } return message } } /** * @param {string?} platform * @return {PlatformMessageI18n} instance */ export function usePlatformI18n(platform) { if (!platform) { platform = inject('platform').value } if (!platform) { throw 'platform argument missing' } return inject( 'platformMessage', () => new PlatformMessageI18n(platform), true ) } /** * @param {string} key * @param {string?} defaultMsg * @return {string} translated message or defaultMsg if provided */ export function $tp(key, defaultMsg) { const pm = usePlatformI18n() return pm.getMessageUsingPlatform(key, defaultMsg) } ================================================ FILE: src_assets/common/assets/web/public/assets/css/apollo.css ================================================ /* Hide pages while localization is loading */ [v-cloak] { display: none; } [data-bs-theme=dark] .element { color: var(--bs-primary-text-emphasis); background-color: var(--bs-primary-bg-subtle); } @media (prefers-color-scheme: dark) { .element { color: var(--bs-primary-text-emphasis); background-color: var(--bs-primary-bg-subtle); } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/bg.json ================================================ { "_common": { "apply": "Прилагане", "auto": "Автоматично", "autodetect": "Автоматично откриване (препоръчително)", "beta": "(бета)", "cancel": "Отказ", "disabled": "Изключено", "disabled_def": "Изключено (по подразбиране)", "disabled_def_cbox": "По подразбиране: отметнато", "dismiss": "Отхвърляне", "do_cmd": "Команда преди стартиране", "elevated": "Изпълнение като администратор", "enabled": "Включено", "enabled_def": "Включено (по подразбиране)", "enabled_def_cbox": "По подразбиране: проверено", "error": "Грешка!", "note": "Забележка:", "password": "Парола", "run_as": "Стартиране като администратор", "save": "Запазване", "see_more": "Вижте повече", "success": "Успешно!", "undo_cmd": "Команда след приключване", "username": "Потребителско име", "warning": "Внимание!" }, "apps": { "actions": "Действия", "add_cmds": "Добавяне на команди", "add_new": "Добавяне на ново", "app_name": "Име на приложението", "app_name_desc": "Име на приложението, както се показва в Moonlight", "applications_desc": "Приложенията се опресняват при рестартиране на клиента", "applications_title": "Приложения", "auto_detach": "Продължаване на предаването, ако приложението се затвори бързо", "auto_detach_desc": "По този начин ще се направи опит за автоматично разпознаване на приложения от тип „стартираща програма“, които се затварят бързо след като стартират друга програма или на друго свое копие. Когато се засече такова, то се третира като разкачено приложение.", "cmd": "Команда", "cmd_desc": "Основното приложение, което да се стартира. Ако е празно, няма да се стартира никакво приложение.", "cmd_note": "Ако пътят до изпълнимия файл на командата съдържа интервали, трябва да го заградите с кавички.", "cmd_prep_desc": "Списък с команди, които да се изпълняват преди/след това приложение. Ако някоя от подготвителните команди се провали, стартирането на приложението се прекъсва.", "cmd_prep_name": "Подготвителни команди", "covers_found": "Намерени обложки", "delete": "Изтриване", "detached_cmds": "Разкачени команди", "detached_cmds_add": "Добавяне на разкачена команда", "detached_cmds_desc": "Списък с команди, които да се изпълняват във фонов режим.", "detached_cmds_note": "Ако пътят до изпълнимия файл на командата съдържа интервали, трябва да го заградите с кавички.", "edit": "Редактиране", "env_app_id": "Идентификатор на приложението", "env_app_name": "Име на приложението", "env_client_audio_config": "Конфигурацията на звука, поискана от клиента (2.0/5.1/7.1)", "env_client_enable_sops": "Клиентът е заявил опцията за оптимизиране на играта за оптимално поточно предаване (true/false)", "env_client_fps": "Заявените от клиента кадри/сек (целочислена стойност)", "env_client_gcmap": "Заявената маска за контролера, във формат на битова маска (целочислена стойност)", "env_client_hdr": "HDR е включен от клиента (true/false)", "env_client_height": "Височината, заявена от клиента (целочислена стойност)", "env_client_host_audio": "Клиентът е поискал звука да се изпълнява на отдалечения компютър (true/false)", "env_client_width": "Ширината, заявена от клиента (целочислена стойност)", "env_displayplacer_example": "Пример за автоматизиране на резолюцията чрез displayplacer:", "env_qres_example": "Пример за автоматизиране на резолюцията чрез QRes:", "env_qres_path": "Път до qres", "env_var_name": "Име на променливата", "env_vars_about": "Относно променливите на средата", "env_vars_desc": "Всички команди получават тези променливи на средата по подразбиране:", "env_xrandr_example": "Пример за автоматизиране на резолюцията чрез Xrandr:", "exit_timeout": "Време за изчакване при затваряне", "exit_timeout_desc": "Брой секунди за изчакване на всички процеси на приложението да завършат самостоятелно, когато бъде изпратена заявка за затваряне. Ако не е зададено, по подразбиране се изчаква до 5 секунди. Ако е зададено 0 или отрицателна стойност, приложението ще бъде прекратено незабавно.", "find_cover": "Търсене на обложка", "global_prep_desc": "Включване/изключване на изпълнението на глобалните подготвителни команди за това приложение.", "global_prep_name": "Глобални команди за подготовка", "image": "Изображение", "image_desc": "Пътят до иконката/картинката/изображението на приложението, което ще бъде изпратено на клиента. Изображението трябва да е файл във формата PNG. Ако не е зададено, Apollo ще изпрати стандартно изображение.", "loading": "Зареждане…", "name": "Име", "output_desc": "Файлът, в който се запазва изходът (текстовия поток) от командата. Ако не е посочен, изходът се игнорира.", "output_name": "Изход", "run_as_desc": "Това може да е необходимо за някои приложения, които изискват администраторски права, за да работят правилно.", "wait_all": "Поточното предаване да продължава, докато всички процеси на приложението не се затворят", "wait_all_desc": "По този начин поточното предаване ще продължи, докато всички процеси, стартирани от приложението, не завършат изпълнението си. Ако няма отметка, предаването ще спре, когато първоначалният процес на приложението завърши, дори и да има други процеси от приложението, които все още работят.", "working_dir": "Работна директория", "working_dir_desc": "Работната директория, която да се подаде на процеса. Някои приложения, например, използват работната директория, за да търсят конфигурационни файлове. Ако не е зададена, по подразбиране ще се ползва директорията, в която се намира командата." }, "config": { "adapter_name": "Име на устройството", "adapter_name_desc_linux_1": "Ръчно задаване на графичния процесор, който да се използва за прихващане на екрана.", "adapter_name_desc_linux_2": "за намиране на всички устройства, които могат да използват VAAPI", "adapter_name_desc_linux_3": "Заменете ``renderD129`` с устройството върнато от по-горната команда, за да видите името и възможностите на устройството. За да бъде поддържано от Apollo, то трябва задължително да има поне:", "adapter_name_desc_windows": "Ръчно задаване на графичния процесор, който да се използва за прихващане на екрана. Ако не е зададено, графичният процесор се избира автоматично. Силно препоръчваме да оставите това поле празно, за да се направи автоматичен избор! Забележка: към този графичен процесор трябва да има свързан и включен екран. Съответните стойности могат да бъдат намерени чрез следната команда:", "adapter_name_placeholder_windows": "Radeon RX 580 Series", "add": "Добавяне", "address_family": "Вид адреси", "address_family_both": "IPv4+IPv6", "address_family_desc": "Задайте вида адреси, използвани от Apollo", "address_family_ipv4": "Само IPv4", "always_send_scancodes": "Винаги да се пращат скан-кодове", "always_send_scancodes_desc": "Изпращането на скан-кодове подобрява съвместимостта с игри и приложения, но може да доведе до неправилно разчетени входни сигнали от клавиатурата при някои клиенти, ако не се ползва клавиатурна подредба съвместима с английски (САЩ). Включете това, ако въвеждането от клавиатурата изобщо не работи в някои приложения. Изключете го, ако клавишите на клиента пращат грешни входни сигнали към отдалечения компютър.", "amd_coder": "Кодиране чрез AMF (H264)", "amd_coder_desc": "Позволява избирането на ентропия при кодирането, така че да се даде приоритет на качеството или скростта на кодиране. Само за H.264.", "amd_enforce_hrd": "Принудителна настройка на декодера с хипотетични справки (HRD) на AMF", "amd_enforce_hrd_desc": "Увеличава ограниченията за контрол на скоростта, така че да се спазват изискванията на модела на HRD. Това значително намалява превишаването на идеалната побитова скорост, но може да предизвика дефекти при кодирането или влошаване на качеството при определени видео карти.", "amd_preanalysis": "Предварителен анализ на AMF", "amd_preanalysis_desc": "Дава възможност за предварителен анализ на контрола на скоростта, който може да повиши качеството за сметка на увеличено забавяне на кодирането.", "amd_quality": "Качество на AMF", "amd_quality_balanced": "балансирано – балансирано (по подразбиране)", "amd_quality_desc": "По този начин се контролира балансът между скоростта и качеството на кодиране.", "amd_quality_group": "Настройки за качеството на AMF", "amd_quality_quality": "качество – предпочитане на качеството", "amd_quality_speed": "скорост – предпочитане на скоростта", "amd_rc": "Управление на скоростта на AMF", "amd_rc_cbr": "cbr – постоянна побитова скорост (препоръчва се, ако HRD е включено)", "amd_rc_cqp": "cqp – режим на постоянно qp", "amd_rc_desc": "Това задава метода за управление на скоростта, така че да се гарантира, че няма да се превишава целевата побитова скорост на клиента. „cqp“ не е подходящ метод за подсигуряване на определената побитова скорост, а другите възможности (с изключение на „vbr_latency“) зависят от Принудителната настройка на HRD, която да помогне с ограничаването на превишаванията на побитовата скорост.", "amd_rc_group": "Настройки за управление на скоростта на AMF", "amd_rc_vbr_latency": "vbr_latency – променлива побитова скорост, ограничена от забавянето (препоръчва се, ако HRD е изключен; по подразбиране)", "amd_rc_vbr_peak": "vbr_peak – променлива побитова скорост, ограничена от максимумите", "amd_usage": "Използване на AMF", "amd_usage_desc": "С тази настройка се задава основният профил на кодиране. Всички настройки по-долу нагласят подмножество от профила на използване, но има зададени и допълнителни скрити настройки, които не могат да бъдат променени другаде.", "amd_usage_lowlatency": "lowlatency ниско забавяне (най-бързо)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality – ниско забавяне, високо качество (бързо)", "amd_usage_transcoding": "transcoding – транскодиране (най-бавно)", "amd_usage_ultralowlatency": "ultralowlatency – ултра ниско забавяне (най-бързо; по подразбиране)", "amd_usage_webcam": "webcam – уеб камера (бавно)", "amd_vbaq": "Базирано на вариации адаптивно квантуване на AMF (VBAQ)", "amd_vbaq_desc": "Човешкото зрение обикновено е по-малко чувствително към дефекти в места с много текстури. В режима VBAQ дисперсията на пикселите се използва за разпознаване на сложността на пространствените текстури, което позволява на кодирането да заделя повече битове за по-гладките области. Включването на тази функция води до подобряване на субективното визуално качество при някои видове съдържание.", "apply_note": "Натиснете „Прилагане“, за да рестартирате Apollo и да приложите промените. Това ще прекрати всички текущи сесии.", "audio_sink": "Звуков изход", "audio_sink_desc_linux": "Името на звуковия изход, използван за връщане на звука. Ако не е зададено, pulseaudio ще избере мониторното устройство по подразбиране. Можете да намерите името на звуковия изход като използвате някоя от тези команди:", "audio_sink_desc_macos": "Името на звуковия изход, използван за връщане на звука. Apollo може да получи достъп само до микрофоните в macOS, поради системни ограничения. За поточно предаване на системен звук ще Ви трябва помощта на Soundflower или BlackHole.", "audio_sink_desc_windows": "Ръчно задаване на конкретно звуково устройство за прихващане. Ако не е зададено, устройството се избира автоматично. Силно препоръчително е да оставите това поле празно, за да използвате автоматичния избор на устройство! Ако имате няколко звукови устройства с еднакви имена, може да научите идентификаторите им чрез следната команда:", "audio_sink_placeholder_macos": "BlackHole 2 канала", "audio_sink_placeholder_windows": "Високоговорители (звуково устройство с високо качество)", "av1_mode": "Поддръжка на AV1", "av1_mode_0": "Apollo ще обявява поддръжката на AV1 въз основа на възможностите за кодиране (препоръчително)", "av1_mode_1": "Apollo няма да обявява поддръжката на AV1", "av1_mode_2": "Apollo ще обявява поддръжката на AV1 Main 8-битов профил", "av1_mode_3": "Apollo ще обявява поддръжката на AV1 Main 8-битов и 10-битов (HDR) профили", "av1_mode_desc": "Позволява на клиента да поиска видео поток с кодиране AV1 Main 8-битов или 10-битов. Кодирането на AV1 натоварва повече процесора, така че включването на тази настройка може да намали производителността при използване на софтуерно кодиране.", "back_button_timeout": "Време на изчакване за симулиране на бутона Home/Guide", "back_button_timeout_desc": "Ако бутонът Back/Select се задържи натиснат за определения брой милисекунди, се симулира натискане на бутона Home/Guide. Ако е зададена стойност < 0 (по подразбиране), задържането на бутона Back/Select няма да симулира натискането на бутона Home/Guide.", "capture": "Принудително използване на определен метод на прихващане", "capture_desc": "В автоматичния режим Apollo ще използва първия, който работи. NvFBC изисква коригирани драйвери на nvidia.", "cert": "Сертификат", "cert_desc": "Сертификатът, който да се ползва за сдвояване на уеб интерфейса и клиента Moonlight. За по-добра съвместимост той трябва да има публичен ключ от вида RSA-2048.", "channels": "Максимален брой свързани клиенти", "channels_desc_1": "Apollo може да позволи една сесия за поточно предаване да бъде споделена с множество клиенти едновременно.", "channels_desc_2": "Някои методи за хардуерно кодиране могат да имат ограничения, които намаляват производителността при излъчване на повече от един поток.", "coder_cabac": "cabac – контекстно-адаптивно двоично аритметично кодиране – по-високо качество", "coder_cavlc": "cavlc – контекстно адаптивно кодиране с променлива дължина – по-бързо декодиране", "configuration": "Настройки", "controller": "Управление чрез контролер", "controller_desc": "Позволява на клиентите да управляват отдалечения компютър с контролер", "credentials_file": "Файл с удостоверителни данни", "credentials_file_desc": "Съхраняване на потребителското име/парола отделно от файла за състоянието на Apollo.", "dd_config_ensure_active": "Автоматично активиране на дисплея", "dd_config_ensure_only_display": "Деактивиране на други дисплеи и активиране само на посочения дисплей", "dd_config_ensure_primary": "Автоматично активиране на дисплея и превръщането му в основен дисплей", "dd_config_label": "Конфигурация на устройството", "dd_config_revert_delay": "Забавяне на връщането на конфигурацията", "dd_config_revert_delay_desc": "Допълнително забавяне в милисекунди, което да се изчака, преди да се върне конфигурацията, когато приложението е затворено или последната сесия е прекратена. Основната цел е да се осигури по-плавен преход при бързо превключване между приложенията.", "dd_config_verify_only": "Проверете дали дисплеят е включен", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "Включване/изключване на режима HDR по заявка на клиента (по подразбиране)", "dd_hdr_option_disabled": "Не променяйте настройките на HDR", "dd_mode_remapping": "Пренареждане на режима на дисплея", "dd_mode_remapping_add": "Добавяне на запис за пренасочване", "dd_mode_remapping_desc_1": "Посочете записи за пренасочване, за да промените изискваната разделителна способност и/или честота на опресняване на други стойности.", "dd_mode_remapping_desc_2": "Списъкът се итерира отгоре надолу и се използва първото съвпадение.", "dd_mode_remapping_desc_3": "Полетата \"Requested\" могат да бъдат оставени празни, за да съответстват на всяка поискана стойност.", "dd_mode_remapping_desc_4_final_values_mixed": "Трябва да се посочи поне едно поле \"Final\". Непосочената разделителна способност или честота на опресняване няма да бъдат променени.", "dd_mode_remapping_desc_4_final_values_non_mixed": "Полето \"Final\" трябва да бъде посочено и не може да бъде празно.", "dd_mode_remapping_desc_5_sops_mixed_only": "Опцията \"Оптимизиране на настройките на играта\" трябва да е разрешена в клиента Moonlight, в противен случай се пропускат записи с посочени полета за разделителна способност.", "dd_mode_remapping_desc_5_sops_resolution_only": "Опцията \"Оптимизиране на настройките на играта\" трябва да е разрешена в клиента Moonlight, в противен случай мапингът се пропуска.", "dd_mode_remapping_final_refresh_rate": "Окончателна честота на опресняване", "dd_mode_remapping_final_resolution": "Окончателна резолюция", "dd_mode_remapping_requested_fps": "Заявена FPS", "dd_mode_remapping_requested_resolution": "Искана резолюция", "dd_options_header": "Разширени опции на устройството за показване", "dd_refresh_rate_option": "Честота на опресняване", "dd_refresh_rate_option_auto": "Използвайте стойността на FPS, предоставена от клиента (по подразбиране)", "dd_refresh_rate_option_disabled": "Не променяйте честотата на опресняване", "dd_refresh_rate_option_manual": "Използване на ръчно въведена честота на опресняване", "dd_refresh_rate_option_manual_desc": "Въведете честотата на опресняване, която ще се използва", "dd_resolution_option": "Резолюция", "dd_resolution_option_auto": "Използвайте резолюция, предоставена от клиента (по подразбиране)", "dd_resolution_option_disabled": "Не променяйте резолюцията", "dd_resolution_option_manual": "Използване на ръчно въведена резолюция", "dd_resolution_option_manual_desc": "Въведете разделителната способност, която ще се използва", "dd_resolution_option_ogs_desc": "Опцията \"Оптимизиране на настройките на играта\" трябва да бъде активирана в клиента на Moonlight, за да работи това.", "dd_wa_hdr_toggle_desc": "Когато използвате виртуално дисплейно устройство за стрийминг, то може да покаже неправилен HDR цвят. С активирането на тази опция Apollo ще се опита да смекчи този проблем.", "dd_wa_hdr_toggle": "Активиране на заобикаляне на висококонтрастния режим за HDR", "ds4_back_as_touchpad_click": "Симулиране на бутона Back/Select чрез натискане на сензорния панел", "ds4_back_as_touchpad_click_desc": "При принудително симулиране на контролер DualShock 4, да се симулира натискането на бутона Back/Select чрез натискане на сензорния панел", "encoder": "Принудително използване на определен метод на кодиране", "encoder_desc": "Принудително използване на определен метод на кодиране. Ако не е зададено, Apollo ще избере най-добрия наличен вариант. Забележка: ако използвате Уиндоус и изберете хардуерно кодиране, то трябва да се поддържа от видео картата, към която е свързан екранът.", "encoder_software": "Софтуерно", "external_ip": "Външен IP адрес", "external_ip_desc": "Ако не е зададен външен IP адрес, Apollo автоматично ще открие какъв е той", "fec_percentage": "Процент на FEC", "fec_percentage_desc": "Процент на пакетите за коригиране на грешки от всеки пакет данни за всеки видео кадър. По-високите стойности могат да коригират по-голяма загуба на мрежови пакети, но за сметка на увеличаване на данните предавани по мрежата.", "ffmpeg_auto": "auto – нека ffmpeg реши (по подразбиране)", "file_apps": "Файл с приложения", "file_apps_desc": "Файлът, в който се съхраняват настройките на приложенията в Apollo.", "file_state": "Файл за състоянието", "file_state_desc": "Файлът, в който се съхранява текущото състояние на Apollo", "gamepad": "Симулиран вид контролер", "gamepad_auto": "Опции за автоматичен избор", "gamepad_desc": "Изберете какъв вид контролер да бъде симулиран на отдалечения компютър", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "Опции за избор на DS4", "gamepad_ds5": "DS5 (PS5)", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Ръчни настройки за DS4", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Подготвителни команди", "global_prep_cmd_desc": "Настройване на списък с команди, които да се изпълняват преди или след дадено приложение. Ако някоя от посочените подготвителни команди се провали, процесът на стартиране на приложението ще бъде прекъснат.", "hevc_mode": "Поддръжка на HEVC", "hevc_mode_0": "Apollo ще обявява поддръжката на HEVC въз основа на възможностите за кодиране (препоръчително)", "hevc_mode_1": "Apollo няма да обявява поддръжката на HEVC", "hevc_mode_2": "Apollo ще обявява поддръжката на HEVC, профил Main", "hevc_mode_3": "Apollo ще обявява поддръжката на HEVC, профили Main и Main10 (HDR)", "hevc_mode_desc": "Позволява на клиента да поиска видео поток с кодиране HEVC Main или HEVC Main10. Кодирането на HEVC натоварва повече процесора, така че включването на тази настройка може да намали производителността при използване на софтуерно кодиране.", "high_resolution_scrolling": "Поддръжка на превъртане с висока резолюция", "high_resolution_scrolling_desc": "Когато това е включено, Apollo просто ще препредава командите за превъртане на колелцето на мишката с висока резолюция, идващи от клиенти използващи Moonlight. Може да е по-добре това да бъде изключено за по-старите приложения, в които превъртането може да мести съдържанието твърде бързо, ако събитията за превъртане са с висока резолюция.", "install_steam_audio_drivers": "Инсталиране на аудио драйверите на Steam", "install_steam_audio_drivers_desc": "Ако Steam е инсталиран, това автоматично ще инсталира и драйвера за поточно предаване на звук на Steam, чрез който може да се поддържа 5.1/7.1 обемен звук, както и да се заглушава звука на отдалечения компютър.", "key_repeat_delay": "Забавяне на повторението на клавишите", "key_repeat_delay_desc": "Колко бързо да започва повтарянето на симулираното натискане на клавишите, при задържането им в натиснато положение. Това е първоначалното закъснение в милисекунди преди да за почне повторението на клавишите.", "key_repeat_frequency": "Честота на повтаряне на клавишите", "key_repeat_frequency_desc": "Колко често да се извършват симулирани натискания на клавишите в секунда, при задържане на клавишите в натиснато положение. Стойността тук може да бъде и нецяло число.", "key_rightalt_to_key_win": "Карта на клавиш Alt вдясно към клавиш Windows", "key_rightalt_to_key_win_desc": "Възможно е натискането на клавиша Windows да не може да бъде изпратено към сървъра от Moonlight. В тези случаи може да настроите Apollo да мисли, че десният Alt е клавишът Windows.", "keyboard": "Управление чрез клавиатура", "keyboard_desc": "Позволява на клиентите да управляват отдалечения компютър с клавиатура", "lan_encryption_mode": "Режим на шифроване в LAN", "lan_encryption_mode_1": "Включено за поддържаните клиенти", "lan_encryption_mode_2": "Задължително за всички клиенти", "lan_encryption_mode_desc": "Това определя дали да се използва шифроване при излъчване в локалната мрежа. Шифроването може да намали производителността на излъчването, особено при не особено мощни сървъри и клиенти.", "locale": "Език", "locale_desc": "Език на потребителския интерфейс на Apollo.", "log_level": "Ниво на съобщенията в журнала", "log_level_0": "Verbose", "log_level_1": "Debug", "log_level_2": "Info", "log_level_3": "Warning", "log_level_4": "Error", "log_level_5": "Fatal", "log_level_6": "Нищо", "log_level_desc": "Минималното ниво на съобщения в журнала, извеждан на стандартния изход", "log_path": "Път до журналния файл", "log_path_desc": "Файлът, в който се запазва текущия журнал на Apollo.", "min_fps_factor": "Коефициент на минимален брой кадри/сек", "min_fps_factor_desc": "Apollo ще използва този коефициент за изчисляване на минималното време между кадрите. Лекото увеличаване на тази стойност може да помогне при излъчване на предимно статично съдържание. По-високите стойности ще създадат по-голям мрежов трафик.", "min_threads": "Минимален брой нишки на процесора", "min_threads_desc": "Увеличаването на стойността леко намалява ефективността на кодирането, но компромисът обикновено си заслужава, тъй като ще се използват повече процесорни ядра за кодиране. Идеалната стойност е най-ниската възможна стойност, при която кодирането може да се извършва надеждно при желаните от настройки за излъчване и използваният хардуер.", "misc": "Други настройки", "motion_as_ds4": "Симулиране на контролер DS4, ако контролерът на клиента разполага с поддръжка на сензори за движение", "motion_as_ds4_desc": "Ако е изключено, сензорите за движение няма да се вземат предвид при избора на вида контролер.", "mouse": "Управление чрез мишка", "mouse_desc": "Позволява на клиентите да управляват отдалечения компютър с мишка", "native_pen_touch": "Собствена поддръжка на писалка/докосване", "native_pen_touch_desc": "Когато е включено, Apollo просто ще препредава командите идващи от писалка/докосване както са получени от клиентите използващи Moonlight. Може да е по-добре това да бъде изключено за по-старите приложения, които няма собствена поддръжка на писалка/докосване.", "notify_pre_releases": "Известия за предварителни версии", "notify_pre_releases_desc": "Дали да бъдете уведомявани за нови предварителни версии на Apollo, преди превръщането им в официални", "nvenc_h264_cavlc": "Предпочитане на CAVLC пред CABAC за H.264", "nvenc_h264_cavlc_desc": "Опростен вариант за ентропия при кодирането. CAVLC се нуждае от около 10% повече побитова скорост за същото качество. Има смисъл само за много стари декодиращи устройства.", "nvenc_latency_over_power": "Предпочитане на по-малкото забавяне пред икономията на енергия", "nvenc_latency_over_power_desc": "Apollo изисква от графичния процесор да работи на максималната си тактова честота по време на излъчване, за да намали забавянето при кодирането. Изключването на тази настройка не се препоръчва, тъй като това може да доведе до значително увеличаване на закъснението при кодиране.", "nvenc_opengl_vulkan_on_dxgi": "Изчертаване на OpenGL/Vulkan върху DXGI", "nvenc_opengl_vulkan_on_dxgi_desc": "Apollo не може да прихваща с пълна честота на кадрите програми, реализирани с OpenGL или Vulkan, работещи на цял екран, освен ако не се изчертават върху DXGI. Това е генерална системна настройка, която ще бъде върната в първоначалното си състояние при затваряне на процеса на Apollo.", "nvenc_preset": "Настройка за производителност", "nvenc_preset_1": "(най-бързо, по подразбиране)", "nvenc_preset_7": "(най-бавно)", "nvenc_preset_desc": "По-големите числа подобряват компресията (качеството при дадена побитова скорост) за сметка на увеличено забавяне при кодирането. Препоръчително е това да се променя само ако има ограничение от мрежата или декодера. В противен случай подобен ефект може да се постигне чрез увеличаване на побитовата скорост.", "nvenc_realtime_hags": "Използване на реално-времеви приоритет при хардуерно-ускореното планиране на задачите на графичния процесор (HAGS)", "nvenc_realtime_hags_desc": "В момента драйверите на NVIDIA могат да засичат при кодиране, ако HAGS е включено, използва се реално-времеви приоритет и използването на видео паметта е близо до максимума. Изключването на тази опция понижава приоритета до „висок“, заобикаляйки засичането за сметка на намалена производителност на прихващане на екрана, когато графичният процесор е силно натоварен.", "nvenc_spatial_aq": "Пространствено AQ", "nvenc_spatial_aq_desc": "Използване на по-високи стойности на QP за по-простите области във видеото. Препоръчително е това да бъде включено, когато се излъчва с по-ниска побитова скорост.", "nvenc_spatial_aq_disabled": "Изключено (по-бързо, по подразбиране)", "nvenc_spatial_aq_enabled": "Включено (по-бавно)", "nvenc_twopass": "Режим на две преминавания", "nvenc_twopass_desc": "Добавя предварителна стъпка на кодиране. Това позволява да се открият повече вектори на движение, да се разпредели по-добре побитовата скорост в рамките на кадъра, както и да се спазват по-стриктно ограниченията на побитовата скорост. Изключването не се препоръчва, тъй като това може да доведе до периодично превишаване на зададената побитова скорост и последваща загуба на пакети.", "nvenc_twopass_disabled": "Изключено (най-бързо, не се препоръчва)", "nvenc_twopass_full_res": "Пълна резолюция (по-бавно)", "nvenc_twopass_quarter_res": "Четвърт резолюция (по-бързо, по подразбиране)", "nvenc_vbv_increase": "Процентно увеличение на VBV/HRD в един кадър", "nvenc_vbv_increase_desc": "По подразбиране Apollo използва VBV/HRD в един кадър, което означава, че размерът на всеки кодиран видео кадър не се очаква да превишава желаната побитова скорост, разделена на желаната честота на кадрите. Отслабването на това ограничение може да бъде от полза и да действа като променлива побитова скорост с ниско забавяне, но същевременно може да доведе до загуба на пакети, ако мрежата не разполага с достатъчен буфер, за да се справи с пиковете на побитова скорост. Максимално допустимата стойност е 400, което съответства на 5 пъти увеличен максимален размер на кодирания видео кадър.", "origin_web_ui_allowed": "Разрешение за достъп до уеб интерфейса", "origin_web_ui_allowed_desc": "Определя от къде може да се ползва уеб интерфейсът. Това не отменя нуждата от въвеждане на потребителско име и парола.", "origin_web_ui_allowed_lan": "Само устройства в локалната мрежа имат достъп до уеб интерфейса", "origin_web_ui_allowed_pc": "Само компютърът, на който работи Apollo, има достъп до уеб интерфейса", "origin_web_ui_allowed_wan": "Всеки има достъп до уеб интерфейса", "output_name_desc_unix": "По време на стартирането на Apollo би трябвало да видите списък с откритите екрани. Забележка: трябва да използвате стойността на идентификатора в скобите. По-долу е даден пример – действителният екран може да бъде намерен в раздела за Отстраняване на проблеми.", "output_name_desc_windows": "Ръчно задаване на идентификатор на екран, който да се ползва за прихващане на картината. Ако не е зададено, ще се използва основният екран. Забележка: ако сте посочили конкретна видео карта по-горе, този екран трябва да е свързан към същата. По време на стартирането на Apollo би трябвало да видите списък с откритите екрани. По-долу е даден пример – действителният екран може да бъде намерен в раздела за Отстраняване на проблеми.", "output_name_unix": "Номер на екрана", "output_name_windows": "Идентификатора на екрана", "ping_timeout": "Време за изчакване на отговор", "ping_timeout_desc": "Продължителност от време в милисекунди, през което да се изчаква за получаване на данни от Moonlight, преди да се прекрати потокът", "pkey": "Частен ключ", "pkey_desc": "Частният ключ, използван за уеб интерфейса и при сдвояване с клиента на Moonlight. За най-добра съвместимост е добре това да бъде частен ключ от вида RSA-2048.", "port": "Порт", "port_alert_1": "Apollo не може да използва портове с номера по-малки 1024!", "port_alert_2": "Не могат да се ползват портове с номера по-големи от 65535!", "port_desc": "Задаване на групата от портове, които да се използват от Apollo", "port_http_port_note": "Използвайте този порт, за да се свържете с Moonlight.", "port_note": "Бележка", "port_port": "Порт", "port_protocol": "Протокол", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "Даването на достъп до уеб интерфейса от Интернет представлява риск за сигурността! Действате на своя отговорност!", "port_web_ui": "Уеб интерфейс", "qp": "Параметър на квантуване (QP)", "qp_desc": "Някои устройства може да не поддържат постоянна побитова скорост на предаване. За тях вместо това се използва параметъра на кватуване. По-високите стойности означават по-голяма компресия, но и по-ниско качество.", "qsv_coder": "Кодиране чрез QuickSync (H264)", "qsv_preset": "Настройка на QuickSync", "qsv_preset_fast": "бързо (ниско качество)", "qsv_preset_faster": "по-бързо (по-ниско качество)", "qsv_preset_medium": "средно (по подразбиране)", "qsv_preset_slow": "бавно (добро качество)", "qsv_preset_slower": "по-бавно (по-добро качество)", "qsv_preset_slowest": "най-бавно (най-добро качество)", "qsv_preset_veryfast": "най-бързо (най-ниско качество)", "qsv_slow_hevc": "Разрешаване на бавното кодиране чрез HEVC", "qsv_slow_hevc_desc": "Това може да даде възможност за кодиране чрез HEVC при ползване на по-стари графични процесори на Intel, за сметка на по-голямо използване на графичния процесор и по-ниска производителност.", "restart_note": "Apollo се рестартира, за да се приложат промените.", "sunshine_name": "Име на Apollo", "sunshine_name_desc": "Името, показвано в Moonlight за този сървър. Ако не е посочено, се използва името на компютъра.", "sw_preset": "Настройка на софтуерното кодиране", "sw_preset_desc": "Оптимизиране на баланса между скоростта на кодиране (брой кодирани кадри в секунда) и ефективността на компресиране (качество за бит в битовия поток). Стандартната стойност е „супер бързо“.", "sw_preset_fast": "бързо", "sw_preset_faster": "по-бързо", "sw_preset_medium": "средно", "sw_preset_slow": "бавно", "sw_preset_slower": "по-бавно", "sw_preset_superfast": "супер бързо (по подразбиране)", "sw_preset_ultrafast": "ултра бързо", "sw_preset_veryfast": "много бързо", "sw_preset_veryslow": "много бавно", "sw_tune": "Фина настройка на софтуерното кодиране", "sw_tune_animation": "animation – подходящо за анимационни филми; използва по-висока степен на деблокиране и повече референтни кадри", "sw_tune_desc": "Опции за фина настройка, които се прилагат след зададената по-горе настройка. Стандартната стойност е „zerolatency“.", "sw_tune_fastdecode": "fastdecode – позволява по-бързо декодиране чрез изключване на определени филтри", "sw_tune_film": "film – подходящо за висококачествено филмово съдържание; намалява деблокирането", "sw_tune_grain": "grain – запазва зърнестата структура типична за по-стари филмови материали", "sw_tune_stillimage": "stillimage – подходящо за съдържание с малко движение, подобно на презентация", "sw_tune_zerolatency": "zerolatency – подходящо за бързо кодиране и предаване с ниско забавяне (по подразбиране)", "touchpad_as_ds4": "Симулиране на контролер DS4, ако контролерът на клиента разполага със сензорен панел", "touchpad_as_ds4_desc": "Ако е изключено, сензорният панел няма да се взема предвид при избора на вида контролер.", "upnp": "UPnP", "upnp_desc": "Автоматично настройване на пренасочването на портове за излъчване през Интернет", "vaapi_strict_rc_buffer": "Налагане на строги ограничения за побитовата скорост на кадрите за H.264/HEVC при видео карти на AMD", "vaapi_strict_rc_buffer_desc": "Включването на това може да предотврати пропускането на кадри по мрежата по време на промени в сцената, но качеството на видеото по време на движение може да бъде намалено.", "virtual_sink": "Виртуален изход", "virtual_sink_desc": "Ръчно задаване на виртуално звуково устройство, което да се ползва. Ако не е зададено, устройството се избира автоматично. Силно препоръчително е да оставите това поле празно, за да използвате автоматичния избор на устройство!", "virtual_sink_placeholder": "Виртуални високоговорители на Steam", "vt_coder": "Кодиране на VideoToolbox", "vt_realtime": "Кодиране в реално време на VideoToolbox", "vt_software": "Софтуерно кодиране на VideoToolbox", "vt_software_allowed": "Разрешено", "vt_software_forced": "Принудително", "wan_encryption_mode": "Режим на шифроване в WAN", "wan_encryption_mode_1": "Включено за поддържаните клиенти (по подразбиране)", "wan_encryption_mode_2": "Задължително за всички клиенти", "wan_encryption_mode_desc": "Това определя дали да се използва шифроване при излъчване през Интернет. Шифроването може да намали производителността на излъчването, особено при не особено мощни сървъри и клиенти." }, "index": { "description": "Apollo е сървър за собствено поточно предаване на игри, предназначен за ползване с Moonlight.", "download": "Сваляне", "installed_version_not_stable": "Използвате предварителна версия на Apollo. Възможно е да се сблъскате с различни видове проблеми. Моля, съобщавайте за всички проблеми, които срещате. Благодарим, че помагате да направим Apollo по-добър софтуер!", "loading_latest": "Зареждане на последната версия…", "new_pre_release": "Има нова версия предварителна версия!", "new_stable": "Има нова стабилна версия!", "startup_errors": "Внимание! Apollo засече тези грешки по време на стартиране. НАИСТИНА Е ПРЕПОРЪЧИТЕЛНО да ги отстраните, преди да започнете излъчването.", "version_dirty": "Благодарим, че помогнахте да направим Apollo по-добър софтуер!", "version_latest": "Използвате най-новата версия на Apollo", "welcome": "Здравейте от Apollo!" }, "navbar": { "applications": "Приложения", "configuration": "Настройки", "home": "Начало", "password": "Промяна на паролата", "pin": "ПИН", "theme_auto": "Автоматично", "theme_dark": "Тъмна", "theme_light": "Светла", "toggle_theme": "Цветова схема", "troubleshoot": "Отстраняване на проблеми" }, "password": { "confirm_password": "Потвърждаване на паролата", "current_creds": "Текущи данни за идентификация", "new_creds": "Нови данни за идентификация", "new_username_desc": "Ако не бъде посочено, потребителското име няма да бъде променено", "password_change": "Промяна на паролата", "success_msg": "Паролата е променена успешно! Тази страница скоро ще се презареди и браузърът ще поиска да въведете новите данни за идентификация." }, "pin": { "device_name": "Име на устройството", "pair_failure": "Неуспешно сдвояване. Проверете дали ПИН кодът е въведен правилно.", "pair_success": "Успешно сдвояване! Проверете Moonlight, за да продължите.", "pin_pairing": "Сдвояване чрез ПИН код", "send": "Изпращане", "warning_msg": "Уверете се, че имате достъп до клиента, с който сдвоявате сървъра. Този софтуер може да предостави пълен контрол върху компютъра, затова внимавайте!" }, "resource_card": { "github_discussions": "Дискусии в GitHub", "legal": "Правни въпроси", "legal_desc": "Използвайки този софтуер, Вие се съгласявате с правилата и условията в следните документи.", "license": "Лиценз", "lizardbyte_website": "Уеб сайт на LizardByte", "resources": "Ресурси", "resources_desc": "Ресурси за Apollo!", "third_party_notice": "Забележка относно ползването на имената на трети страни" }, "troubleshooting": { "dd_reset": "Нулиране на настройките на постоянното устройство за показване", "dd_reset_desc": "Ако Apollo заседне при опит да възстанови променените настройки на устройството за показване, можете да нулирате настройките и да продължите да възстановявате състоянието на дисплея ръчно.", "dd_reset_error": "Грешка при нулиране на постоянството!", "dd_reset_success": "Успешно нулиране на постоянството!", "force_close": "Принудително затваряне", "force_close_desc": "Ако Moonlight се оплаква, че дадено приложение работи в момента, принудителното затваряне на това приложение би трябвало да реши проблема.", "force_close_error": "Грешка при затваряне на приложението", "force_close_success": "Приложението е затворено успешно!", "logs": "Журнал", "logs_desc": "Разгледайте съобщенията в журнала на Apollo", "logs_find": "Търсене…", "restart_Apollo": "Рестартиране на Apollo", "restart_Apollo_desc": "Ако Apollo не работи правилно, можете да опитате да го рестартирате. Това ще прекрати всички текущи сесии.", "restart_Apollo_success": "Apollo се рестартира", "troubleshooting": "Отстраняване на проблеми", "unpair_all": "Премахване на сдвояването с всички клиенти", "unpair_all_error": "Грешка при премахването на сдвояванията", "unpair_all_success": "Премахнато е сдвояването с всички устройства.", "unpair_desc": "Премахване на всички сдвоени устройства. Устройствата, чието сдвояване бъде премахнато, докато имат активна сесия, ще останат свързани, но няма да могат да започнат нови сесии или да възобновят вече започнати такива.", "unpair_single_no_devices": "Няма сдвоени устройства.", "unpair_single_success": "Въпреки това едно или повече устройства може все още да са в активна сесия. Използвайте бутона „Принудително затваряне“ по-горе, за да прекратите всички активни сесии.", "unpair_single_unknown": "Неизвестен клиент", "unpair_title": "Премахване на сдвояването с устройствата" }, "welcome": { "confirm_password": "Потвърждаване на паролата", "create_creds": "Преди да започнете, трябва да си създадете ново потребителско име и парола за достъп до уеб интерфейса.", "create_creds_alert": "Данните по-долу ще са необходими за достъп до уеб интерфейса на Apollo. Пазете ги, тъй като няма да ги видите отново!", "greeting": "Добре дошли в Apollo!", "login": "Вписване", "welcome_success": "Тази страница скоро ще се презареди и браузърът ще поиска да въведете новите данни за идентификация" } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/cs.json ================================================ { "_common": { "apply": "Použít", "auto": "Automaticky", "autodetect": "Automatická detekce (doporučeno)", "beta": "(beta)", "cancel": "Zrušit", "disabled": "Zakázáno", "disabled_def": "Zakázáno (výchozí)", "disabled_def_cbox": "Výchozí: nezaškrtnuto", "dismiss": "Odmítnout", "do_cmd": "Provést příkaz", "elevated": "Zvýšené", "enabled": "Povoleno", "enabled_def": "Povoleno (výchozí)", "enabled_def_cbox": "Výchozí: zaškrtnuto", "error": "Chyba!", "note": "Pozn.:", "password": "Heslo", "run_as": "Spustit jako správce", "save": "Uložit", "see_more": "Zobrazit více", "success": "Úspěch!", "undo_cmd": "Vrátit příkaz", "username": "Uživatelské jméno", "warning": "Varování!" }, "apps": { "actions": "Akce", "add_cmds": "Přidat příkazy", "add_new": "Přidat nový", "app_name": "Název aplikace", "app_name_desc": "Název aplikace, jak je uveden v aplikaci Moonlight", "applications_desc": "Aplikace se obnovují pouze při restartování klienta", "applications_title": "Aplikace", "auto_detach": "Pokračovat ve streamování, pokud se aplikace rychle ukončí", "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.", "cmd": "Příkaz", "cmd_desc": "Hlavní aplikace ke spuštění. Pokud je prázdná, nebude spuštěna žádná aplikace.", "cmd_note": "Pokud cesta ke spustitelnému příkazu obsahuje mezery, musíte ji uvést v uvozovkách.", "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ší.", "cmd_prep_name": "Příprava příkazů", "covers_found": "Nalezené obaly", "delete": "Vymazat", "detached_cmds": "Oddělené příkazy", "detached_cmds_add": "Přidat oddělený příkaz", "detached_cmds_desc": "Seznam příkazů, které mají být spuštěny na pozadí.", "detached_cmds_note": "Pokud cesta k spustitelnému příkazu obsahuje mezery, musíte ji vložit do uvozovek.", "edit": "Upravit", "env_app_id": "ID aplikace", "env_app_name": "Název aplikace", "env_client_audio_config": "Nastavení zvuku požadované klientem (2.0/5.1/7.1)", "env_client_enable_sops": "Klient požádal o možnost optimalizovat hru pro optimální streamování (true/false)", "env_client_fps": "FPS požadované klientem (int)", "env_client_gcmap": "Požadovaná maska gamepadu ve formátu bitset/bitfield (int)", "env_client_hdr": "HDR je povoleno klientem (true/false)", "env_client_height": "Výška požadovaná klientem (int)", "env_client_host_audio": "Klient si vyžádal hostitelský zvuk (true/false)", "env_client_width": "Šířka požadovaná klientem (int)", "env_displayplacer_example": "Příklad - displayplacer pro automatizaci rozlišení:", "env_qres_example": "Příklad – QR pro automatizaci rozlišení:", "env_qres_path": "qres cesta", "env_var_name": "Název Var", "env_vars_about": "O proměnných prostředí", "env_vars_desc": "Ve výchozím nastavení získávají tyto proměnné prostředí všechny příkazy:", "env_xrandr_example": "Příklad - Xrandr pro automatizaci rozlišení:", "exit_timeout": "Časový limit pro ukončení", "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ě.", "find_cover": "Najít obal", "global_prep_desc": "Povolit/zakázat provádění globálních předběžných příkazů pro tuto aplikaci.", "global_prep_name": "Globální předběžné příkazy", "image": "Obrázek", "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.", "loading": "Načítám...", "name": "Název", "output_desc": "Soubor, kde je uložen výstup příkazu, pokud není zadán, výstup je ignorován", "output_name": "Výstup", "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.", "wait_all": "Pokračujte ve streamování, dokud se neukončí všechny procesy aplikace", "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ěží.", "working_dir": "Pracovní adresář", "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" }, "config": { "adapter_name": "Název adaptéru", "adapter_name_desc_linux_1": "Ruční zadání GPU, která se má použít pro snímání.", "adapter_name_desc_linux_2": "najít všechna zařízení schopná VAAPI", "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ě:", "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:", "adapter_name_placeholder_windows": "Řada Radeon RX 580", "add": "Přidat", "address_family": "Rodina adres", "address_family_both": "IPv4+IPv6", "address_family_desc": "Nastavení rodiny adres, kterou používá Sunshine", "address_family_ipv4": "Pouze IPv4", "always_send_scancodes": "Vždy odesílat Scancody", "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.", "amd_coder": "AMF kodér (H264)", "amd_coder_desc": "Umožňuje vybrat entropie kódování pro upřednostnění kvality nebo rychlosti kódování. Pouze H.264.", "amd_enforce_hrd": "Vymáhání hypotetického referenčního dekodéru (HRD) AMF", "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.", "amd_preanalysis": "Předběžná analýza AMF", "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í.", "amd_quality": "Kvalita AMF", "amd_quality_balanced": "vyrovnané -- vyvážené (výchozí)", "amd_quality_desc": "Tím se řídí rovnováha mezi rychlostí kódování a kvalitou.", "amd_quality_group": "Nastavení kvality AMF", "amd_quality_quality": "kvalita -- preferovat kvalitu", "amd_quality_speed": "Rychlost -- preferovat rychlost", "amd_rc": "Ovládání AMF", "amd_rc_cbr": "cbr -- konstantní bitrate (doporučujeme pokud je HRD zapnuto)", "amd_rc_cqp": "cqp -- konstantní qp režim", "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.", "amd_rc_group": "Nastavení řízení AMF", "amd_rc_vbr_latency": "vbr_latency -- proměnný datový tok s omezenou latencí (doporučeno, pokud je HRD vypnuta; výchozí)", "amd_rc_vbr_peak": "vbr_peak -- maximální nastavená proměnná bitrate", "amd_usage": "Využití AMF", "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.", "amd_usage_lowlatency": "lowlatency - nízká latence (nejrychlejší)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - nízká latence, vysoká kvalita (rychlá)", "amd_usage_transcoding": "překódování -- překódování (nejpomalejší)", "amd_usage_ultralowlatency": "ultralowlatence - ultra nízká latence (nejrychlejší; výchozí)", "amd_usage_webcam": "webová kamera -- webová kamera (pomalá)", "amd_vbaq": "AMF adaptivní kvantifikace založená na rozptylu (VBAQ)", "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.", "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.", "audio_sink": "Zvukový výřez", "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:", "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.", "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:", "audio_sink_placeholder_macos": "BlackHole 2ch", "audio_sink_placeholder_windows": "Reproduktory (High Definition Audio Device)", "av1_mode": "AV1 podpora", "av1_mode_0": "Sunshine bude inzerovat podporu AV1 na základě schopností kodéru (doporučeno)", "av1_mode_1": "Sunshine nebude inzerovat podporu AV1", "av1_mode_2": "Sunshine bude inzerovat podporu hlavního 8bitového profilu AV1", "av1_mode_3": "Sunshine bude inzerovat podporu hlavních 8bitových a 10bitových profilů AV1 (HDR)", "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.", "back_button_timeout": "Časový limit emulace tlačítka Domů/Návod", "back_button_timeout_desc": "Pokud je tlačítko Zpět/Vybrat podrženo po zadaný počet milisekund, je emulováno stisknutí tlačítka Domů/Průvodce. Pokud je nastavena hodnota < 0 (výchozí), podržením tlačítka Zpět/Vybrat se tlačítko Domů/Průvodce neemuluje.", "capture": "Vynutit specifickou metodu snímání", "capture_desc": "V automatickém režimu Sunshine použije první, který funguje. NvFBC vyžaduje opravené ovladače nvidia.", "cert": "Certifikát", "cert_desc": "Certifikát použitý pro párování webového uživatelského rozhraní a klienta Moonlight. Kvůli nejlepší kompatibilitě by měl mít veřejný klíč RSA-2048.", "channels": "Maximální počet připojených klientů", "channels_desc_1": "Sunshine může umožnit sdílení jedné relace streamování s více klienty současně.", "channels_desc_2": "Některé hardwarové enkodéry mohou mít omezení, která snižují výkon s více streamy.", "coder_cabac": "cabac -- kontextové binární aritmetické kódování – vyšší kvalita", "coder_cavlc": "cavlc -- kontextové adaptivní kódování variabilní délky - rychlejší dekódování", "configuration": "Konfigurace", "controller": "Povolení vstupu z gamepadu", "controller_desc": "Umožňuje hostům ovládat hostitelský systém pomocí gamepadu/ovladače", "credentials_file": "Soubor pověření", "credentials_file_desc": "Uživatelské jméno/heslo ukládejte odděleně od souboru stavu Sunshine.", "dd_config_ensure_active": "Automaticky aktivovat displej", "dd_config_ensure_only_display": "Deaktivovat další displeje a aktivovat pouze zadaný displej", "dd_config_ensure_primary": "Automaticky aktivovat displej a učinit jej primárním displejem", "dd_configuration_option": "Konfigurace zařízení", "dd_config_revert_delay": "Zpoždění vrácení konfigurace", "dd_config_revert_delay_desc": "Dodatečná prodleva v milisekundách, která má být vyčkána před vrácením konfigurace, pokud byla aplikace zavřena nebo poslední relace ukončena. Hlavním účelem je zajistit plynulejší přechod při rychlém přepínání mezi aplikacemi.", "dd_config_revert_on_disconnect": "Vrácení konfigurace při odpojení", "dd_config_revert_on_disconnect_desc": "Vrácení konfigurace při odpojení všech klientů místo ukončení aplikace nebo poslední relace.", "dd_config_verify_only": "Ověřte, zda je displej povolen", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "Zapnout/vypnout HDR režim podle požadavku klienta (výchozí)", "dd_hdr_option_disabled": "Neměnit nastavení HDR", "dd_manual_refresh_rate": "Manuální obnovovací frekvence", "dd_manual_resolution": "Manuální rozlišení", "dd_mode_remapping": "Přemapování režimu zobrazení", "dd_mode_remapping_add": "Přidat položku pro nové mapování", "dd_mode_remapping_desc_1": "Určete položky pro nové mapování pro změnu požadovaného rozlišení a/nebo obnovení frekvence na jiné hodnoty.", "dd_mode_remapping_desc_2": "Seznam se iteruje shora dolů a použije se první shoda.", "dd_mode_remapping_desc_3": "Pole \"Požadováno\" mohou být ponechána prázdná, aby odpovídala libovolné požadované hodnotě.", "dd_mode_remapping_desc_4_final_values_mixed": "Musí být zadáno alespoň jedno pole \"Final\". Nezadané rozlišení nebo obnovovací frekvence se nezmění.", "dd_mode_remapping_desc_4_final_values_non_mixed": "Pole \"Final\" musí být zadáno a nesmí být prázdné.", "dd_mode_remapping_desc_5_sops_mixed_only": "V klientovi Moonlight musí být povolena možnost \"Optimalizovat nastavení hry\", jinak budou položky se zadanými poli rozlišení přeskočeny.", "dd_mode_remapping_desc_5_sops_resolution_only": "V klientovi Moonlight musí být povolena možnost \"Optimalizovat nastavení hry\", jinak se mapování přeskočí.", "dd_mode_remapping_final_refresh_rate": "Konečná obnovovací frekvence", "dd_mode_remapping_final_resolution": "Konečné rozlišení", "dd_mode_remapping_requested_fps": "Požadované FPS", "dd_mode_remapping_requested_resolution": "Požadované rozlišení", "dd_options_header": "Rozšířené možnosti displeje", "dd_refresh_rate_option": "Obnovovací frekvence", "dd_refresh_rate_option_auto": "Použít hodnotu FPS zadanou klientem (výchozí)", "dd_refresh_rate_option_disabled": "Neměnit obnovovací frekvenci", "dd_refresh_rate_option_manual": "Použít ručně zadanou obnovovací frekvenci", "dd_resolution_option": "Rozlišení", "dd_resolution_option_auto": "Použít rozlišení poskytované klientem (výchozí)", "dd_resolution_option_disabled": "Neměnit rozlišení", "dd_resolution_option_manual": "Použít ručně zadané rozlišení", "dd_resolution_option_ogs_desc": "Aby tato funkce fungovala, musí být v klientovi Moonlight povolena možnost \"Optimalizovat nastavení hry\".", "dd_wa_hdr_toggle_delay_desc_1": "Při použití virtuálního zobrazovacího zařízení (VDD) pro streamování může dojít k nesprávnému zobrazení barev HDR. Sunshine se může pokusit tento problém zmírnit vypnutím a opětovným zapnutím HDR.", "dd_wa_hdr_toggle_delay_desc_2": "Pokud je hodnota nastavena na 0, je obcházení zakázáno (výchozí nastavení). Pokud je hodnota v rozmezí 0 až 3000 milisekund, Sunshine vypne HDR, počká zadanou dobu a poté HDR opět zapne. Doporučená doba zpoždění je ve většině případů přibližně 500 milisekund.", "dd_wa_hdr_toggle_delay_desc_3": "NEPOUŽÍVEJTE toto řešení, pokud skutečně nemáte problémy s HDR, protože přímo ovlivňuje čas spuštění streamu!", "dd_wa_hdr_toggle_delay": "Řešení pro HDR s vysokým kontrastem", "ds4_back_as_touchpad_click": "Namapovat Zpět/Vybrat na klepnutí touchpadu", "ds4_back_as_touchpad_click_desc": "Při vynucení emulace DS4 namapujte funkci Zpět/Vybrat na klepnutí touchpadu", "ds5_inputtino_randomize_mac": "Náhodné nastavení virtuálního ovladače MAC", "ds5_inputtino_randomize_mac_desc": "Při registraci řadiče použijte náhodný MAC místo MAC založeného na interním indexu řadiče, aby nedošlo k promíchání konfiguračních nastavení různých řadičů při jejich výměně na straně klienta.", "encoder": "Vynutit specifický enkodér", "encoder_desc": "Vynutit konkrétní kodér, jinak Sunshine vybere nejlepší dostupnou možnost. Poznámka: Pokud v systému Windows zadáte hardwarový kodér, musí odpovídat grafickému procesoru, ke kterému je displej připojen.", "encoder_software": "Software", "external_ip": "Externí IP", "external_ip_desc": "Pokud není zadána žádná externí IP adresa, Sunshine automaticky zjistí externí IP adresu", "fec_percentage": "Procento FEC", "fec_percentage_desc": "Procento paketů pro opravu chyb v každém datovém paketu v každém videosnímku. Vyšší hodnoty mohou korigovat větší ztráty síťových paketů, ale za cenu zvýšení využití šířky pásma.", "ffmpeg_auto": "auto -- nechat ffmpeg rozhodnout (výchozí)", "file_apps": "Soubor aplikací", "file_apps_desc": "Soubor, ve kterém jsou uloženy aktuální aplikace Sunshine.", "file_state": "Stavový soubor", "file_state_desc": "Soubor, ve kterém je uložen aktuální stav Sunshine", "gamepad": "Typ emulovaného gamepadu", "gamepad_auto": "Možnosti automatického výběru", "gamepad_desc": "Výběr typu gamepadu pro emulaci v hostitelském počítači", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "Možnosti výběru DS4", "gamepad_ds5": "DS5 (PS5)", "gamepad_ds5_manual": "Možnosti výběru DS5", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Možnosti manuálního ovládání DS4", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Příprava příkazů", "global_prep_cmd_desc": "Konfigurace seznamu příkazů, které se mají spustit před nebo po spuštění libovolné aplikace. Pokud některý ze zadaných přípravných příkazů selže, proces spuštění aplikace se přeruší.", "hevc_mode": "Podpora HEVC", "hevc_mode_0": "Sunshine bude propagovat podporu pro HEVC na základě možností enkodéru (doporučeno)", "hevc_mode_1": "Sunshine nebude inzerovat podporu HEVC", "hevc_mode_2": "Sunshine bude inzerovat podporu hlavního profilu HEVC", "hevc_mode_3": "Sunshine bude inzerovat podporu profilů HEVC Main a Main10 (HDR)", "hevc_mode_desc": "Umožňuje klientovi vyžádat videostreamy HEVC Main nebo HEVC Main10. Kódování HEVC je náročnější na procesor, takže zapnutí této funkce může snížit výkon při použití softwarového kódování.", "high_resolution_scrolling": "Podpora rolování s vysokým rozlišením", "high_resolution_scrolling_desc": "Je-li tato funkce povolena, bude Sunshine předávat události posouvání ve vysokém rozlišení od klientů Moonlight. To může být užitečné zakázat u starších aplikací, které se při událostech posouvání ve vysokém rozlišení posouvají příliš rychle.", "install_steam_audio_drivers": "Instalace ovladačů zvuku služby Steam", "install_steam_audio_drivers_desc": "Pokud je nainstalována služba Steam, automaticky se nainstaluje ovladač Steam Streaming Speakers pro podporu prostorového zvuku 5.1/7.1 a ztlumení zvuku hostitele.", "key_repeat_delay": "Zpoždění opakování kláves", "key_repeat_delay_desc": "Ovládání rychlosti opakování kláves. Počáteční prodleva v milisekundách před opakováním kláves.", "key_repeat_frequency": "Frekvence opakování kláves", "key_repeat_frequency_desc": "Jak často se klávesy opakují každou sekundu. Tato nastavitelná možnost podporuje desetinná čísla.", "key_rightalt_to_key_win": "Mapování pravé klávesy Alt na klávesu Windows", "key_rightalt_to_key_win_desc": "Je možné, že klávesa Windows nelze odeslat přímo z aplikace Moonlight. V takových případech může být užitečné přimět Sunshine, aby si myslel, že pravý Alt je klávesa Windows", "keybindings": "Klávesové zkratky", "keyboard": "Povolit vstupu z klávesnice", "keyboard_desc": "Umožňuje hostům ovládat hostitelský systém pomocí klávesnice", "lan_encryption_mode": "Režim šifrování LAN", "lan_encryption_mode_1": "Povoleno pro podporované klienty", "lan_encryption_mode_2": "Vyžadováno pro všechny klienty", "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ů.", "locale": "Místní prostředí", "locale_desc": "Místní jazyk používaný pro uživatelské rozhraní Sunshine.", "log_path": "Cesta k logu", "log_path_desc": "Soubor, ve kterém jsou uloženy aktuální logy Sunshine.", "max_bitrate": "Maximální bitrate", "max_bitrate_desc": "Maximální bitrate (v Kb/s), kterým bude Sunshine kódovat datový tok. Pokud je nastaven na 0, bude vždy použit bitrate požadovaný aplikací Moonlight.", "minimum_fps_target": "Minimální cílová hodnota FPS", "minimum_fps_target_desc": "Nejnižší efektivní FPS, kterého může stream dosáhnout. Hodnota 0 je považována za zhruba polovinu FPS streamu. Pokud streamujete obsah s 24 nebo 30 snímky za sekundu, doporučuje se nastavení 20.", "min_log_level": "Úroveň logu", "min_log_level_0": "Verbose", "min_log_level_1": "Ladit", "min_log_level_2": "Info", "min_log_level_3": "Varování", "min_log_level_4": "Chyba", "min_log_level_5": "Kritická chyba", "min_log_level_6": "Nic", "min_log_level_desc": "Minimální úroveň protokolu vypisovaná do standardního výstupu", "min_threads": "Minimální počet vláken CPU", "min_threads_desc": "Zvýšení této hodnoty mírně snižuje efektivitu kódování, ale tento kompromis se obvykle vyplatí, protože získáte více jader procesoru pro kódování. Ideální hodnota je nejnižší hodnota, která dokáže spolehlivě enkódovat při požadovaném nastavení streamování na vašem hardwaru.", "misc": "Různé možnosti", "motion_as_ds4": "Emulovat gamepad DS4, pokud klientský gamepad hlásí přítomnost pohybových senzorů", "motion_as_ds4_desc": "Pokud je vypnuto, nebudou při výběru typu gamepadu brány v úvahu snímače pohybu.", "mouse": "Povolit vstup myši", "mouse_desc": "Umožňuje hostům ovládat systém pomocí myši", "native_pen_touch": "Nativní podpora pera/dotyku", "native_pen_touch_desc": "Je-li tato funkce povolena, bude Sunshine předávat nativní události pera/dotyku z klientů Moonlight. To může být užitečné vypnout pro starší aplikace bez nativní podpory pera/dotyku.", "notify_pre_releases": "Oznámení před vydáním", "notify_pre_releases_desc": "Zda chcete být informováni o nových předběžných verzích Sunshine", "nvenc_h264_cavlc": "Preferovat CAVLC před CABAC v H.264", "nvenc_h264_cavlc_desc": "Jednodušší forma entropického kódování. CAVLC potřebuje pro stejnou kvalitu přibližně o 10 % vyšší datový tok. Má význam pouze pro opravdu stará dekódovací zařízení.", "nvenc_latency_over_power": "Preferovat nižší latenci kódování před úsporami energie", "nvenc_latency_over_power_desc": "Sunshine požaduje maximální taktovací frekvenci GPU při streamování, aby se snížila latence kódování. Jeho vypnutí se nedoporučuje, protože může vést k výraznému zvýšení latence enkódování.", "nvenc_opengl_vulkan_on_dxgi": "Prezentovat OpenGL/Vulkan na vrchu DXGI", "nvenc_opengl_vulkan_on_dxgi_desc": "Sunshine nedokáže zachytit programy OpenGL a Vulkan na celou obrazovku s plnou snímkovou frekvencí, pokud nejsou prezentovány nad DXGI. Jedná se o celosystémové nastavení, které se při ukončení programu Sunshine vrátí zpět.", "nvenc_preset": "Předvolba výkonu", "nvenc_preset_1": "(nejrychlejší, výchozí)", "nvenc_preset_7": "(nejpomalejší)", "nvenc_preset_desc": "Vyšší čísla zlepšují kompresi (kvalitu při daném datovém toku) za cenu zvýšené latence kódování. Doporučuje se měnit pouze v případě omezení ze strany sítě nebo dekodéru, jinak lze podobného efektu dosáhnout zvýšením datového toku.", "nvenc_realtime_hags": "Použít prioritu reálného času v hardwarově akcelerovaném plánování gpu", "nvenc_realtime_hags_desc": "V současné době mohou ovladače NVIDIA v enkodéru zamrznout, pokud je povolena funkce HAGS, je použita priorita reálného času a využití VRAM se blíží maximu. Zakázáním této možnosti se priorita sníží na vysokou, čímž se zamrznutí obejde za cenu snížení výkonu snímání při velkém zatížení GPU.", "nvenc_spatial_aq": "Prostorové AQ", "nvenc_spatial_aq_desc": "Přiřadit vyšší hodnoty QP plochým oblastem videa. Doporučuje se povolit při streamování s nižšími datovými toky.", "nvenc_twopass": "Dvouprůchodový režim", "nvenc_twopass_desc": "Přidá předběžný průchod kódování. To umožňuje detekovat více vektorů pohybu, lépe rozložit datový tok napříč snímkem a přísněji dodržovat limity datového toku. Vypnutí se nedoporučuje, protože to může vést k občasnému překročení datového toku a následné ztrátě paketů.", "nvenc_twopass_disabled": "Zakázáno (nejrychlejší, nedoporučeno)", "nvenc_twopass_full_res": "Úplné rozlišení (pomalejší)", "nvenc_twopass_quarter_res": "Čtvrtinové rozlišení (rychlejší, výchozí)", "nvenc_vbv_increase": "Procentuální nárůst VBV/HRD v jednom snímku", "nvenc_vbv_increase_desc": "Ve výchozím nastavení používá Sunshine jednosnímkové VBV/HRD, což znamená, že velikost kódovaného videosnímku nesmí překročit požadovaný datový tok dělený požadovanou snímkovou frekvencí. Uvolnění tohoto omezení může být přínosné a fungovat jako proměnný datový tok s nízkou latencí, ale může také vést ke ztrátě paketů, pokud síť nemá vyrovnávací paměť, která by zvládla skokové nárůsty datového toku. Maximální přijatelná hodnota je 400, což odpovídá 5x zvýšenému hornímu limitu velikosti kódovaného videosnímku.", "origin_web_ui_allowed": "Povolené webové rozhraní Origin", "origin_web_ui_allowed_desc": "Původ adresy vzdáleného koncového bodu, kterému není odepřen přístup k webovému uživatelskému rozhraní", "origin_web_ui_allowed_lan": "K webovému uživatelskému rozhraní mohou přistupovat pouze osoby v síti LAN", "origin_web_ui_allowed_pc": "K webovému uživatelskému rozhraní může přistupovat pouze localhost", "origin_web_ui_allowed_wan": "K webovému uživatelskému rozhraní může přistupovat kdokoli", "output_name": "Zobrazit ID", "output_name_desc_unix": "Při spuštění Sunshine by se měl zobrazit seznam zjištěných displejů. Poznámka: Musíte použít hodnotu id uvnitř závorky. Níže je uveden příklad; skutečný výstup naleznete na kartě Odstraňování problémů.", "output_name_desc_windows": "Ruční zadání id zobrazovacího zařízení, které se má použít pro zachycení. Pokud není nastaveno, zachytí se primární displej. Poznámka: Pokud jste výše zadali grafický procesor, musí být tento displej připojen k tomuto grafickému procesoru. Během spuštění Sunshine by se měl zobrazit seznam zjištěných displejů. Níže je uveden příklad; skutečný výstup naleznete na kartě Odstraňování problémů.", "ping_timeout": "Časový limit příchozího pingu", "ping_timeout_desc": "Jak dlouho v milisekundách čekat na data z moonlight před vypnutím datového toku", "pkey": "Soukromý klíč", "pkey_desc": "Soukromý klíč používaný pro párování webového uživatelského rozhraní a klienta Moonlight. Pro zajištění nejlepší kompatibility by to měl být soukromý klíč RSA-2048.", "port": "Port", "port_alert_1": "Sunshine nemůže používat porty nižší než 1024!", "port_alert_2": "Porty nad 65535 nejsou k dispozici!", "port_desc": "Nastavení rodiny portů, které používá Sunshine", "port_http_port_note": "Tento port slouží k připojení k Moonlight.", "port_note": "Poznámka", "port_port": "Port", "port_protocol": "Protokol", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "Vystavení webového uživatelského rozhraní na internet je bezpečnostní riziko! Pokračujte na vlastní nebezpečí!", "port_web_ui": "Web UI", "qp": "Kvantizační parametr", "qp_desc": "Některá zařízení nemusí podporovat konstantní přenosovou rychlost. U těchto zařízení se místo toho používá QP. Vyšší hodnota znamená větší kompresi, ale nižší kvalitu.", "qsv_coder": "Kodér QuickSync (H264)", "qsv_preset": "Předvolba QuickSync", "qsv_preset_fast": "rychle (nízká kvalita)", "qsv_preset_faster": "rychlejší (nižší kvalita)", "qsv_preset_medium": "střední (výchozí)", "qsv_preset_slow": "pomalý (dobrá kvalita)", "qsv_preset_slower": "pomalejší (lepší kvalita)", "qsv_preset_slowest": "nejpomalejší (nejlepší kvalita)", "qsv_preset_veryfast": "nejrychlejší (nejnižší kvalita)", "qsv_slow_hevc": "Povolit pomalé kódování HEVC", "qsv_slow_hevc_desc": "To může umožnit kódování HEVC na starších grafických procesorech Intel za cenu vyššího využití grafického procesoru a nižšího výkonu.", "restart_note": "Sunshine se restartuje a aplikuje změny.", "stream_audio": "Streamování zvuku", "stream_audio_desc": "Zda se má zvuk streamovat, nebo ne. Vypnutí této funkce může být užitečné pro streamování bezhlavých displejů jako druhých monitorů.", "sunshine_name": "Jméno Sunshine", "sunshine_name_desc": "Název zobrazený u Moonlight. Pokud není zadán, použije se název hostitele počítače", "sw_preset": "Předvolby SW", "sw_preset_desc": "Optimalizace kompromisu mezi rychlostí kódování (zakódované snímky za sekundu) a účinností komprese (kvalita na bit v datovém toku). Výchozí hodnota je superrychlá.", "sw_preset_fast": "rychlá", "sw_preset_faster": "rychleji", "sw_preset_medium": "střední", "sw_preset_slow": "pomalu", "sw_preset_slower": "pomalejší", "sw_preset_superfast": "superrychlá (výchozí)", "sw_preset_ultrafast": "ultrarychlá", "sw_preset_veryfast": "velmirychlá", "sw_preset_veryslow": "velmipomalá", "sw_tune": "SW ladění", "sw_tune_animation": "animace -- vhodné pro kreslené filmy; používá vyšší deblokaci a více referenčních snímků", "sw_tune_desc": "Možnosti ladění, které se použijí po předvolbě. Výchozí hodnota je nulová latence.", "sw_tune_fastdecode": "fastdecode -- umožňuje rychlejší dekódování vypnutím určitých filtrů", "sw_tune_film": "film -- použití pro vysoce kvalitní filmový obsah; snižuje deblokaci", "sw_tune_grain": "zrno - zachovává strukturu zrna ve starém zrnitém filmovém materiálu", "sw_tune_stillimage": "stillimage -- dobré pro prezentační obsah", "sw_tune_zerolatency": "zerolatency -- vhodné pro rychlé kódování a streamování s nízkou latencí (výchozí)", "system_tray": "Povolit systémovou lištu", "system_tray_desc": "Zobrazit ikonu v systémové liště a zobrazit oznámení na ploše", "touchpad_as_ds4": "Emulovat gamepad DS4, pokud klientský gamepad hlásí přítomnost touchpadu", "touchpad_as_ds4_desc": "Pokud je vypnuto, nebude při výběru typu gamepadu zohledněna přítomnost touchpadu.", "upnp": "UPnP", "upnp_desc": "Automatická konfigurace přesměrování portů pro streamování přes Internet", "vaapi_strict_rc_buffer": "Přísné vynucení limitů datového toku snímků pro H.264/HEVC na grafických procesorech AMD", "vaapi_strict_rc_buffer_desc": "Povolením této možnosti lze zabránit vypadávání snímků po síti při změnách scény, ale kvalita videa může být při pohybu snížena.", "virtual_sink": "Virtuální zvuk", "virtual_sink_desc": "Ručně zadejte virtuální zvukové zařízení, které chcete použít. Pokud není nastaveno, je zařízení vybráno automaticky. Důrazně doporučujeme ponechat toto pole prázdné, chcete-li použít automatický výběr zařízení!", "virtual_sink_placeholder": "Streamovací reproduktory služby Steam", "vt_coder": "VideoToolbox Coder", "vt_realtime": "VideoToolbox Kódování v reálném čase", "vt_software": "Kódování softwaru VideoToolbox", "vt_software_allowed": "Povoleno", "vt_software_forced": "Vynucené", "wan_encryption_mode": "Režim šifrování WAN", "wan_encryption_mode_1": "Povoleno pro podporované klienty (výchozí)", "wan_encryption_mode_2": "Vyžadováno pro všechny klienty", "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ů." }, "index": { "description": "Sunshine je samostatný hostitel herního streamu pro Moonlight.", "download": "Stáhnout", "installed_version_not_stable": "Používáte předběžnou verzi Sunshine. Mohou se vyskytnout chyby nebo jiné problémy. Nahlaste prosím všechny problémy, se kterými se setkáte. Děkujeme, že pomáháte vytvořit lepší software Sunshine!", "loading_latest": "Načítání nejnovější verze...", "new_pre_release": "K dispozici je nová verze před vydáním!", "new_stable": "K dispozici je nová stabilní verze!", "startup_errors": "Pozor! Sunshine zjistil tyto chyby při spuštění. DŮRAZNĚ DOPORUČUJEME je před streamováním opravit.", "version_dirty": "Děkujeme, že pomáháte vylepšovat software Sunshine!", "version_latest": "Používáte nejnovější verzi Sunshine", "welcome": "Ahoj, Sunshine!" }, "navbar": { "applications": "Aplikace", "configuration": "Konfigurace", "home": "Domů", "password": "Změnit heslo", "pin": "PIN", "theme_auto": "Automaticky", "theme_dark": "Tmavý", "theme_light": "Světlý", "toggle_theme": "Téma", "troubleshoot": "Řešení problémů" }, "password": { "confirm_password": "Potvrzení hesla", "current_creds": "Aktuální přihlašovací údaje", "new_creds": "Nové přihlašovací údaje", "new_username_desc": "Pokud není zadáno, uživatelské jméno se nezmění", "password_change": "Změna hesla", "success_msg": "Heslo bylo úspěšně změněno! Tato stránka se brzy znovu načte a prohlížeč vás požádá o zadání nových přihlašovacích údajů." }, "pin": { "device_name": "Název zařízení", "pair_failure": "Spárování se nezdařilo: Zkontrolujte, zda je PIN správně zadán", "pair_success": "Úspěch! Prosím, zkontrolujte Moonlight pro pokračování", "pin_pairing": "Párování PIN", "send": "Odeslat", "warning_msg": "Ujistěte se, že máte přístup ke klientovi, se kterým se párujete. Tento software může dát počítači úplnou kontrolu, proto buďte opatrní!" }, "resource_card": { "github_discussions": "Diskuse na GitHubu", "legal": "Právní předpisy", "legal_desc": "Dalším používáním tohoto softwaru souhlasíte s podmínkami uvedenými v následujících dokumentech.", "license": "Licence", "lizardbyte_website": "Webové stránky LizardByte", "resources": "Zdroje", "resources_desc": "Zdroje pro Sunshine!", "third_party_notice": "Oznámení třetí strany" }, "troubleshooting": { "dd_reset": "Obnovení nastavení zařízení s trvalým displejem", "dd_reset_desc": "Pokud se Sunshine zasekne při pokusu o obnovení změněných nastavení zobrazovacího zařízení, můžete obnovit nastavení a pokračovat v obnovení stavu displeje ručně.", "dd_reset_error": "Chyba při obnovení perzistence!", "dd_reset_success": "Úspěch resetování perzistence!", "force_close": "Vynutit zavření", "force_close_desc": "Pokud si aplikace Moonlight stěžuje na aktuálně spuštěnou aplikaci, mělo by její násilné zavření problém vyřešit.", "force_close_error": "Chyba při zavírání aplikace", "force_close_success": "Aplikace úspěšně uzavřena!", "logs": "Logy", "logs_desc": "Podívejte se na logy nahrané Sunshine", "logs_find": "Hledat...", "restart_sunshine": "Restartovat Sunshine", "restart_sunshine_desc": "Pokud Sunshine nefunguje správně, můžete jej zkusit restartovat. Tím se ukončí všechny spuštěné relace.", "restart_sunshine_success": "Sunshine se restartuje", "troubleshooting": "Řešení problémů", "unpair_all": "Zrušit párování všech", "unpair_all_error": "Chyba při odpárování", "unpair_all_success": "Všechna zařízení jsou nespárovaná.", "unpair_desc": "Odeberte spárovaná zařízení. Jednotlivá nespárovaná zařízení s aktivní relací zůstanou připojena, ale nemohou zahájit nebo obnovit relaci.", "unpair_single_no_devices": "Neexistují žádná spárovaná zařízení.", "unpair_single_success": "Zařízení však mohou být stále v aktivní relaci. Pomocí výše uvedeného tlačítka \"Vynutit ukončení\" ukončete všechny otevřené relace.", "unpair_single_unknown": "Neznámý klient", "unpair_title": "Zrušit párování" }, "welcome": { "confirm_password": "Potvrdit heslo", "create_creds": "Před zahájením je třeba vytvořit nové uživatelské jméno a heslo pro přístup k webovému uživatelskému rozhraní.", "create_creds_alert": "Pro přístup k webovému uživatelskému rozhraní Sunshine je třeba zadat níže uvedené přihlašovací údaje. Uchovávejte je v bezpečí, protože už je nikdy neuvidíte!", "greeting": "Vítejte v Sunshine!", "login": "Přihlásit se", "welcome_success": "Tato stránka se brzy znovu načte a prohlížeč vás požádá o nové přihlašovací údaje" } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/de.json ================================================ { "_common": { "apply": "Übernehmen", "auto": "Automatisch", "autodetect": "AutoDetection (empfohlen)", "beta": "(Beta)", "cancel": "Abbrechen", "disabled": "Deaktiviert", "disabled_def": "Deaktiviert (Standard)", "disabled_def_cbox": "Standard: nicht markiert", "dismiss": "Verwerfen", "do_cmd": "Befehl ausführen", "elevated": "Erhöhte", "enabled": "Aktiviert", "enabled_def": "Aktiviert (Standard)", "enabled_def_cbox": "Standard: ausgewählt", "error": "Fehler!", "note": "Hinweis:", "password": "Passwort", "run_as": "Als Admin ausführen", "save": "Speichern", "see_more": "Mehr ansehen", "success": "Erfolgreich!", "undo_cmd": "Befehl rückgängig machen", "username": "Benutzername", "warning": "Warnung!" }, "apps": { "actions": "Aktionen", "add_cmds": "Befehle hinzufügen", "add_new": "Neu hinzufügen", "app_name": "Anwendungsname", "app_name_desc": "Name der App, wie in Moonlight angezeigt", "applications_desc": "Anwendungen werden nur beim Neustart des Clients aktualisiert", "applications_title": "Anwendungen", "auto_detach": "Streaming fortsetzen, wenn die Anwendung schnell beendet wird", "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.", "cmd": "Befehl", "cmd_desc": "Die zu startende Hauptanwendung. Wenn leer wird keine Anwendung gestartet.", "cmd_note": "Wenn der Pfad zum ausführbaren Kommando Leerzeichen enthält, müssen Sie ihn in Anführungszeichen einfügen.", "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.", "cmd_prep_name": "Befehlsvorbereitungen", "covers_found": "Cover gefunden", "delete": "Löschen", "detached_cmds": "Getrennte Befehle", "detached_cmds_add": "Separiertes Kommando hinzufügen", "detached_cmds_desc": "Eine Liste von Befehlen, die im Hintergrund ausgeführt werden sollen.", "detached_cmds_note": "Wenn der Pfad zum ausführbaren Kommando Leerzeichen enthält, müssen Sie ihn in Anführungszeichen einfügen.", "edit": "Bearbeiten", "env_app_id": "App-ID", "env_app_name": "App-Name", "env_client_audio_config": "Die vom Client angeforderte Audio-Konfiguration (2.0/5.1/7.1)", "env_client_enable_sops": "Der Client hat die Option angefordert, das Spiel für ein optimales Streaming zu optimieren (true/false)", "env_client_fps": "Das vom Client angeforderte FPS (float)", "env_client_gcmap": "Die angeforderte Gamepadmaske im Bitset/Bitfield Format (int)", "env_client_hdr": "HDR ist vom Client aktiviert (true/false)", "env_client_height": "Die vom Client angeforderte Höhe (int)", "env_client_host_audio": "Der Client hat Host-Audio angefordert (true/falsch)", "env_client_width": "Die vom Client angeforderte Breite (int)", "env_displayplacer_example": "Beispiel - displayplacer für die Automatisierung der Auflösung:", "env_qres_example": "Beispiel - QRes für die Automatisierung der Auflösung:", "env_qres_path": "qres Pfad", "env_var_name": "Var Name", "env_vars_about": "Über Umgebungsvariablen", "env_vars_desc": "Alle Befehle erhalten diese Umgebungsvariablen standardmäßig:", "env_xrandr_example": "Beispiel - Xrandr für die Auflösungsautomatisierung:", "exit_timeout": "Timeout beenden", "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.", "find_cover": "Cover finden", "global_prep_desc": "Aktiviere/Deaktiviere die Ausführung von Global Prep Commands für diese Anwendung.", "global_prep_name": "Globale Vorbereitungsbefehle", "image": "Bild", "image_desc": "Symbol/Bild/Bild/Bildpfad, der an den Client gesendet wird. Das Bild muss eine PNG-Datei sein. Falls nicht gesetzt, sendet Apollo ein Standardbild-Bild.", "loading": "Wird geladen...", "name": "Name", "output_desc": "Die Datei, in der die Ausgabe des Befehls gespeichert wird, wenn sie nicht angegeben ist, wird die Ausgabe ignoriert", "output_name": "Ausgang", "run_as_desc": "Dies kann für einige Anwendungen notwendig sein, die Administratorrechte benötigen, um ordnungsgemäß zu funktionieren.", "wait_all": "Streaming fortsetzen bis alle App-Prozesse beendet sind", "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.", "working_dir": "Arbeitsverzeichnis", "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 Apollo standardmäßig das übergeordnete Verzeichnis des Befehls verwenden" }, "config": { "adapter_name": "Adaptername", "adapter_name_desc_linux_1": "Geben Sie eine GPU für die Aufnahme manuell an.", "adapter_name_desc_linux_2": "um alle Geräte zu finden, die VAAPI nutzen können", "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 Apollo unterstützt zu werden, muss es zumindest über folgende Punkte verfügen:", "adapter_name_desc_windows": "Legen Sie eine GPU für die Aufnahme manuell fest. Falls nicht festgelegt, wird die GPU automatisch ausgewählt. Wir empfehlen dringend, dieses Feld leer zu lassen, um die automatische GPU-Auswahl zu verwenden! Hinweis: Diese GPU muss ein Display angeschlossen und eingeschaltet haben. Die passenden Werte finden Sie mit dem folgenden Befehl:", "adapter_name_placeholder_windows": "Radeon RX 580-Serie", "add": "Neu", "address_family": "Adressfamilie", "address_family_both": "IPv4+IPv6", "address_family_desc": "Adressfamilie einstellen, die von Apollo verwendet wird", "address_family_ipv4": "Nur IPv4", "always_send_scancodes": "Scancodes immer senden", "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.", "amd_coder": "AMF Coder (H264)", "amd_coder_desc": "Erlaubt es Ihnen, die Entropy-Kodierung auszuwählen, um die Qualität oder die Kodierungsgeschwindigkeit zu priorisieren. H.264 nur.", "amd_enforce_hrd": "Hypothetische Referenz-Decodierer (HRD) durchsetzen", "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.", "amd_preanalysis": "AMF-Voranalyse", "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.", "amd_quality": "AMF-Qualität", "amd_quality_balanced": "ausgewogen -- Ausgewogen (Standard)", "amd_quality_desc": "Dies steuert die Balance zwischen Kodierungsgeschwindigkeit und Qualität.", "amd_quality_group": "AMF Qualitätseinstellungen", "amd_quality_quality": "Qualität -- Qualität bevorzugen", "amd_quality_speed": "speed -- bevorzuge Geschwindigkeit", "amd_rc": "AMF-Ratensteuerung", "amd_rc_cbr": "cbr – Konstante Bitrate", "amd_rc_cqp": "cqp -- Konstanter qp-Modus", "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.", "amd_rc_group": "AMF Rate Control Einstellungen", "amd_rc_vbr_latency": "vbr_latency -- latenzeingeschränkte Bitrate (Standard)", "amd_rc_vbr_peak": "vbr_peak – eingeschränkte Variablen-Bitrate spitzen", "amd_usage": "AMF-Nutzung", "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.", "amd_usage_lowlatency": "niedrige Latenz - niedrige Latenz (schnell)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - niedrige Latenz, hohe Qualität (schnell)", "amd_usage_transcoding": "transcoding -- Umkodierung (langsamste)", "amd_usage_ultralowlatency": "ultralowlatenz - extrem niedrige Latenz (schnellste)", "amd_usage_webcam": "webcam -- Webcam (langsam)", "amd_vbaq": "AMF-Varianz-basierte Adaptive Quantisierung (VBAQ)", "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.", "apply_note": "Klicken Sie auf 'Anwenden', um Apollo neu zu starten und Änderungen anzuwenden. Dies wird alle laufenden Sitzungen beenden.", "audio_sink": "Audio Sink", "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:", "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.", "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:", "audio_sink_placeholder_macos": "BlackHole 2ch", "audio_sink_placeholder_windows": "Lautsprecher (High Definition Audio Device)", "av1_mode": "AV1 Support", "av1_mode_0": "Apollo werbt Unterstützung für AV1 basierend auf Encoder Fähigkeiten (empfohlen)", "av1_mode_1": "Apollo werbt keinen Support für AV1", "av1_mode_2": "Apollo werbt Unterstützung für AV1 Hauptprofil mit 8-Bit", "av1_mode_3": "Apollo werbt Unterstützung für AV1 Hauptprofile mit 8-Bit und 10-Bit (HDR)", "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.", "back_button_timeout": "Timeout für Home/Guide Button Emulation", "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.", "capture": "Erzwinge eine bestimmte Aufnahmemethode", "capture_desc": "Im automatischen Modus wird Apollo den ersten verwenden, der funktioniert. NvFBC benötigt gepatchte Nvidia-Treiber.", "cert": "Zertifikat", "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.", "channels": "Maximal verbundene Clients", "channels_desc_1": "Apollo kann eine einzelne Streaming-Sitzung gleichzeitig mit mehreren Clients teilen.", "channels_desc_2": "Einige Hardware-Encoder haben möglicherweise Einschränkungen, die die Leistung bei mehreren Streams verringern.", "coder_cabac": "cabac -- kontextadaptive binäre arithmetische Kodierung - höhere Qualität", "coder_cavlc": "cavlc -- kontextadaptive Kodierung variabler Länge - schnellere Dekodierung", "configuration": "Konfiguration", "controller": "Enable Gamepad Input", "controller_desc": "Erlaubt Gästen das Host-System mit einem Gamepad/Controller zu steuern", "credentials_file": "Anmeldedaten Datei", "credentials_file_desc": "Speichere Benutzername/Passwort getrennt von Apollo's Status-Datei.", "dd_config_ensure_active": "Bildschirm automatisch aktivieren", "dd_config_ensure_only_display": "Andere Displays deaktivieren und nur die angegebene Anzeige aktivieren", "dd_config_ensure_primary": "Bildschirm automatisch aktivieren und es zur primären Anzeige machen", "dd_config_label": "Gerätekonfiguration", "dd_config_revert_delay": "Zurücksetzungsverzögerung konfigurieren", "dd_config_revert_delay_desc": "Zusätzliche Verzögerung in Millisekunden, um zu warten, bevor die Konfiguration rückgängig gemacht wird, wenn die App geschlossen oder die letzte Sitzung beendet wurde. Hauptziel ist es, einen reibungsloseren Übergang beim schnellen Wechsel zwischen Apps zu ermöglichen.", "dd_config_verify_only": "Überprüfen Sie, ob das Display aktiviert ist", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "Ein-/Ausschalten des HDR-Modus, wie vom Client gewünscht (Standard)", "dd_hdr_option_disabled": "HDR-Einstellungen nicht ändern", "dd_mode_remapping": "Anzeige Modus neu zuordnen", "dd_mode_remapping_add": "Remapping Eintrag hinzufügen", "dd_mode_remapping_desc_1": "Legen Sie die Remapping-Einträge fest, um die angeforderte Auflösung und/oder die Aktualisierungsrate auf andere Werte zu ändern.", "dd_mode_remapping_desc_2": "Die Liste wird von oben nach unten iteriert und die erste Übereinstimmung verwendet.", "dd_mode_remapping_desc_3": "\"Angeforderte\" Felder können leer gelassen werden, um dem gewünschten Wert zu entsprechen.", "dd_mode_remapping_desc_4_final_values_mixed": "Mindestens ein \"End\"-Feld muss angegeben werden. Die nicht angegebene Auflösung oder die Aktualisierungsrate wird nicht geändert.", "dd_mode_remapping_desc_4_final_values_non_mixed": "\"Endlich\"-Feld muss angegeben werden und darf nicht leer sein.", "dd_mode_remapping_desc_5_sops_mixed_only": "Die Option \"Spieleinstellungen optimieren\" muss im Moonlight-Client aktiviert sein, andernfalls werden Einträge mit bestimmten Auflösungsfeldern übersprungen.", "dd_mode_remapping_desc_5_sops_resolution_only": "Option \"Spieleinstellungen optimieren\" muss im Moonlight-Client aktiviert sein, andernfalls wird das Mapping übersprungen.", "dd_mode_remapping_final_refresh_rate": "Endgültige Aktualisierungsrate", "dd_mode_remapping_final_resolution": "Endgültige Entschließung", "dd_mode_remapping_requested_fps": "Angeforderte FPS", "dd_mode_remapping_requested_resolution": "Angeforderte Auflösung", "dd_options_header": "Erweiterte Anzeigeoptionen", "dd_refresh_rate_option": "Aktualisierungsrate", "dd_refresh_rate_option_auto": "FPS Wert des Clients verwenden (Standard)", "dd_refresh_rate_option_disabled": "Aktualisierungsrate nicht ändern", "dd_refresh_rate_option_manual": "Manuell eingegebene Aktualisierungsrate verwenden", "dd_refresh_rate_option_manual_desc": "Geben Sie die zu verwendende Aktualisierungsrate ein", "dd_resolution_option": "Auflösung", "dd_resolution_option_auto": "Auflösung des Clients verwenden (Standard)", "dd_resolution_option_disabled": "Auflösung nicht ändern", "dd_resolution_option_manual": "Manuell eingegebene Auflösung verwenden", "dd_resolution_option_manual_desc": "Die zu verwendende Auflösung eingeben", "dd_resolution_option_ogs_desc": "Die Option \"Spieleinstellungen optimieren\" muss auf dem Moonlight-Client aktiviert sein, damit dies funktioniert.", "dd_wa_hdr_toggle_desc": "Wenn das virtuelle Display-Gerät als Streaming verwendet wird, könnte es eine falsche HDR-Farbe anzeigen. Wenn diese Option aktiviert ist, wird Apollo versuchen, dieses Problem zu lindern.", "dd_wa_hdr_toggle": "Hochkontrast-Workaround für HDR aktivieren", "ds4_back_as_touchpad_click": "Zum Touchpad-Klick zurück/auswählen", "ds4_back_as_touchpad_click_desc": "Beim Erzwingen der DS4-Emulation zum Touchpad-Klick zurück/auswählen", "encoder": "Erzwinge einen bestimmten Encoder", "encoder_desc": "Erzwinge einen bestimmten Encoder, sonst wählt Apollo 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.", "encoder_software": "Software", "external_ip": "Externe IP", "external_ip_desc": "Wenn keine externe IP-Adresse angegeben ist, erkennt Apollo automatisch externe IP", "fec_percentage": "Prozentsatz FEC", "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.", "ffmpeg_auto": "auto -- ffmpeg entscheiden lassen (Standard)", "file_apps": "App-Datei", "file_apps_desc": "Die Datei, in der die aktuellen Apps von Apollo gespeichert werden.", "file_state": "Zustandsdatei", "file_state_desc": "Die Datei, in der der aktuelle Zustand von Apollo gespeichert ist", "fps": "Angekündigte FPS", "gamepad": "Emulierter Gamepad-Typ", "gamepad_auto": "Automatische Auswahloptionen", "gamepad_desc": "Wähle welche Art von Gamepad emuliert werden soll", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "DS4 Auswahloptionen", "gamepad_ds5": "DS5 (PS5)", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Manuelle DS4 Optionen", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Befehlsvorbereitungen", "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.", "hevc_mode": "HEVC Unterstützung", "hevc_mode_0": "Apollo werbt Unterstützung für HEVC basierend auf Encoderfähigkeiten (empfohlen)", "hevc_mode_1": "Apollo werbt keine Unterstützung für HEVC", "hevc_mode_2": "Apollo werbt Unterstützung für das HEVC Hauptprofil", "hevc_mode_3": "Apollo werbt Unterstützung für HEVC Haupt- und Main10-Profile (HDR)", "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.", "high_resolution_scrolling": "Unterstützung für hochauflösende Scrolling", "high_resolution_scrolling_desc": "Wenn aktiviert, durchläuft Apollo 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.", "install_steam_audio_drivers": "Steam Audio Treiber installieren", "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.", "key_repeat_delay": "Schlüssel-Wiederholung Verzögerung", "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.", "key_repeat_frequency": "Tastendruck-Frequenz", "key_repeat_frequency_desc": "Wie oft Tasten jede Sekunde wiederholen. Diese konfigurierbare Option unterstützt Dezimalstellen.", "key_rightalt_to_key_win": "Rechter Alt-Taste auf Windows-Taste zuweisen", "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, Apollo glauben zu lassen, dass die rechte Alt-Taste die Windows-Taste ist", "keyboard": "Tastatureingabe aktivieren", "keyboard_desc": "Erlaubt Gästen das Host-System mit der Tastatur zu steuern", "lan_encryption_mode": "LAN-Verschlüsselungsmodus", "lan_encryption_mode_1": "Für unterstützte Clients aktiviert", "lan_encryption_mode_2": "Benötigt für alle Kunden", "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.", "locale": "Lokal", "locale_desc": "Die Locale, die für die Benutzeroberfläche von Apollo verwendet wird.", "log_level": "Log-Level", "log_level_0": "Verbose", "log_level_1": "Debug", "log_level_2": "Info", "log_level_3": "Warnung", "log_level_4": "Fehler", "log_level_5": "Fatal", "log_level_6": "Keine", "log_level_desc": "Der minimale Log-Level wird auf Standard gedruckt", "log_path": "Logdateipfad", "log_path_desc": "Die Datei, in der die aktuellen Logs von Apollo gespeichert werden.", "min_fps_factor": "Minimaler FPS Faktor", "min_fps_factor_desc": "Sonnenschein verwendet diesen Faktor, um die minimale Zeit zwischen den Frames zu berechnen. Die Erhöhung dieses Wertes kann helfen, wenn überwiegend statische Inhalte gestreamt werden. Höhere Werte verbrauchen mehr Bandbreite.", "min_threads": "Minimale CPU-Thread-Anzahl", "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.", "misc": "Verschiedene Optionen", "motion_as_ds4": "Ein DS4 Gamepad emulieren, wenn der Client Gamepad Bewegungsmelder meldet", "motion_as_ds4_desc": "Wenn deaktiviert, werden Bewegungssensor bei der Auswahl des Gamepad-Typs nicht berücksichtigt.", "mouse": "Maus-Eingabe aktivieren", "mouse_desc": "Erlaubt Gästen das Host-System mit der Maus zu steuern", "native_pen_touch": "Native Pen/Touch Unterstützung", "native_pen_touch_desc": "Wenn aktiviert, durchläuft Apollo natives Pen / Berühren von Moonlight-Clients. Dies kann nützlich sein, um ältere Anwendungen ohne nativen Stift-/Berührungs-Support zu deaktivieren.", "notify_pre_releases": "Pre-Release-Benachrichtigungen", "notify_pre_releases_desc": "Ob über neue Versionen von Apollo benachrichtigt werden soll", "nvenc_h264_cavlc": "CAVLC gegenüber CABAC in H.264 bevorzugen", "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.", "nvenc_latency_over_power": "Reduzierte Encoding-Latenz gegenüber Energieeinsparungen bevorzugen", "nvenc_latency_over_power_desc": "Apollo 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.", "nvenc_opengl_vulkan_on_dxgi": "OpenGL/Vulkan auf DXGI zeigen", "nvenc_opengl_vulkan_on_dxgi_desc": "Apollo 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.", "nvenc_preset": "Leistungsvorgabe", "nvenc_preset_1": "(schnellste, Standard)", "nvenc_preset_7": "(langsamste)", "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.", "nvenc_realtime_hags": "Echtzeit-Priorität in der Hardware-beschleunigten gpu Planung verwenden", "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.", "nvenc_spatial_aq": "Spatial AQ", "nvenc_spatial_aq_desc": "Zuweisen von höheren QP-Werten zu flachen Regionen des Videos. Wird empfohlen zu aktivieren, wenn Streaming mit niedrigeren Bitraten.", "nvenc_spatial_aq_disabled": "Deaktiviert (schneller, Standard)", "nvenc_spatial_aq_enabled": "Aktiviert (langsamer)", "nvenc_twopass": "Zwei-Pass-Modus", "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.", "nvenc_twopass_disabled": "Deaktiviert (schnellste, nicht empfohlen)", "nvenc_twopass_full_res": "Vollständige Auflösung (langsamer)", "nvenc_twopass_quarter_res": "Viertelauflösung (schneller, Standard)", "nvenc_vbv_increase": "Prozentsatz Erhöhung des Einzelbild-VBV/HRD", "nvenc_vbv_increase_desc": "Standardmäßig verwendet Apollo 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.", "origin_web_ui_allowed": "Ursprungsweb-UI erlaubt", "origin_web_ui_allowed_desc": "Der Ursprung der Remote-Endpunkt-Adresse, der der Zugriff auf das Web-Interface nicht verweigert wird", "origin_web_ui_allowed_lan": "Nur LAN-Nutzer können auf Web-UI zugreifen", "origin_web_ui_allowed_pc": "Nur localhost kann auf Web-UI zugreifen", "origin_web_ui_allowed_wan": "Jeder kann auf Web-UI zugreifen", "output_name_desc_unix": "Während des Starts von Apollo sollten Sie die Liste der erkannten Anzeigen sehen. Hinweis: Sie müssen den Id-Wert innerhalb der Klammer verwenden.", "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:", "output_name_unix": "Anzeigenummer", "output_name_windows": "Ausgabename", "ping_timeout": "Ping-Timeout", "ping_timeout_desc": "Verzögerung in Millisekunden beim Warten auf Daten von Moonlight bevor der Stream beendet wird", "pkey": "Privater Schlüssel", "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.", "port": "Port", "port_alert_1": "Apollo kann keine Ports unter 1024 benutzen!", "port_alert_2": "Ports über 65535 sind nicht verfügbar!", "port_desc": "Legen Sie die Familie der von Apollo verwendeten Ports fest", "port_http_port_note": "Benutzen Sie diesen Port, um sich mit Moonlight zu verbinden.", "port_note": "Notiz", "port_port": "Port", "port_protocol": "Protocol", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "Das Web-Interface dem Internet zu übergeben, ist ein Sicherheitsrisiko! Fahren Sie auf eigene Gefahr!", "port_web_ui": "Web UI", "qp": "Quantifizierungsparameter", "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.", "qsv_coder": "QuickSync Coder (H264)", "qsv_preset": "QuickSync Preset", "qsv_preset_fast": "schneller (niedrigere Qualität)", "qsv_preset_faster": "schnellste (niedrigste Qualität)", "qsv_preset_medium": "medium (Standard)", "qsv_preset_slow": "langsam (gute Qualität)", "qsv_preset_slower": "langsamer (bessere Qualität)", "qsv_preset_slowest": "langsamste (beste Qualität)", "qsv_preset_veryfast": "schnellste (niedrigste Qualität)", "qsv_slow_hevc": "Langsame HEVC Encodierung erlauben", "qsv_slow_hevc_desc": "Dies kann HEVC-Kodierung auf älteren Intel GPUs ermöglichen, auf Kosten einer höheren GPU-Nutzung und schlechteren Performance.", "res_fps_desc": "Die von Apollo beworbenen Anzeigenmodi 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 die Art und Weise, wie der Bildschirm an Mondlight gesendet wird.", "resolutions": "Angemeldete Auflösungen", "restart_note": "Apollo wird neu gestartet, um Änderungen anzuwenden.", "sunshine_name": "Apollo Name", "sunshine_name_desc": "Der von Mononlight angezeigte Name, falls nicht angegeben, wird der Hostname des PCs verwendet", "sw_preset": "SW-Voreinstellungen", "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.", "sw_preset_fast": "schnell", "sw_preset_faster": "schneller", "sw_preset_medium": "mittel", "sw_preset_slow": "langsam", "sw_preset_slower": "langsamer", "sw_preset_superfast": "superschnell (Standard)", "sw_preset_ultrafast": "extrem schnell", "sw_preset_veryfast": "veryfast", "sw_preset_veryslow": "veryslow", "sw_tune": "SW Tune", "sw_tune_animation": "animation -- gut für Cartoons; verwendet höhere Deblocking und mehr Referenzrahmen", "sw_tune_desc": "Einstellmöglichkeiten, die nach der Voreinstellung angewendet werden. Standard ist Null.", "sw_tune_fastdecode": "fastdecode -- ermöglicht eine schnellere Dekodierung durch Deaktivieren bestimmter Filter", "sw_tune_film": "film -- verwenden für qualitativ hochwertige Filminhalte; senkt Deblocking", "sw_tune_grain": "korn -- bewahrt die Kornstruktur im alten, körnigen Filmmaterial", "sw_tune_stillimage": "stillimage -- gut für slideshow-ähnliche Inhalte", "sw_tune_zerolatency": "Zerolatency -- gut für schnelle Kodierung und Low-Latency Streaming (Standard)", "touchpad_as_ds4": "Ein DS4 Gamepad emulieren, wenn der Client Gamepad meldet, dass ein Touchpad vorhanden ist", "touchpad_as_ds4_desc": "Wenn deaktiviert, wird das Touchpad-Vorhandensein bei der Auswahl des Gamepad-Typs nicht berücksichtigt.", "upnp": "UPnP", "upnp_desc": "Portweiterleitung für Streaming über das Internet automatisch konfigurieren", "vaapi_strict_rc_buffer": "Bitratenlimit für H.264/HEVC auf AMD GPUs strikt durchsetzen", "vaapi_strict_rc_buffer_desc": "Wenn Sie diese Option aktivieren, können während der Szenenänderung gelöschte Frames über das Netzwerk vermieden werden, aber die Videoqualität kann während der Bewegung reduziert werden.", "virtual_sink": "Virtueller Sink", "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!", "virtual_sink_placeholder": "Steam Streaming Lautsprecher", "vt_coder": "VideoToolbox Coder", "vt_realtime": "VideoToolbox Echtzeit-Codierung", "vt_software": "VideoToolbox Software Encoding", "vt_software_allowed": "Zulässig", "vt_software_forced": "Erzwungen", "wan_encryption_mode": "WAN-Verschlüsselungsmodus", "wan_encryption_mode_1": "Aktiviert für unterstützte Clients (Standard)", "wan_encryption_mode_2": "Benötigt für alle Kunden", "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." }, "index": { "description": "Apollo ist ein selbst gehosteter Game-Stream-Host für Moonlight.", "download": "Download", "installed_version_not_stable": "Sie verwenden eine Vor-Release-Version von Apollo. Sie können Fehler oder andere Probleme haben. Bitte melde alle Probleme, auf die du triffst. Danke, dass du dabei geholfen hast, Apollo zu einer besseren Software zu machen!", "loading_latest": "Lade neueste Version...", "new_pre_release": "Eine neue Pre-Release Version ist verfügbar!", "new_stable": "Eine neue Stable Version ist verfügbar!", "startup_errors": "Achtung! Apollo erkannte diese Fehler während des Starts. Wir STRONGLY EMPFOHLEN beheben sie vor dem Streaming.", "version_dirty": "Vielen Dank, dass Sie dazu beigetragen haben, Apollo zu einer besseren Software zu machen!", "version_latest": "Du verwendest die neueste Version von Apollo", "welcome": "Hallo, Apollo!" }, "navbar": { "applications": "Anwendungen", "configuration": "Konfiguration", "home": "Zuhause", "password": "Passwort ändern", "pin": "Pin", "theme_auto": "Auto", "theme_dark": "Dunkel", "theme_light": "Hell", "toggle_theme": "Thema", "troubleshoot": "Fehlerbehebung" }, "password": { "confirm_password": "Passwort wiederholen", "current_creds": "Aktuelle Zugangsdaten", "new_creds": "Neue Zugangsdaten", "new_username_desc": "Wenn nicht angegeben, wird der Benutzername nicht geändert", "password_change": "Passwortänderung", "success_msg": "Passwort wurde erfolgreich geändert! Diese Seite wird bald neu geladen, Ihr Browser wird Sie nach den neuen Zugangsdaten fragen." }, "pin": { "device_name": "Gerätename", "pair_failure": "Paarung fehlgeschlagen: Prüfen Sie, ob die PIN korrekt eingegeben wurde", "pair_success": "Erfolg! Weiter geht es in Moonlight", "pin_pairing": "PIN Pairing", "send": "Senden", "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!" }, "resource_card": { "github_discussions": "GitHub Discussions", "legal": "Rechtlich", "legal_desc": "Durch die Weiterverwendung dieser Software erklären Sie sich mit den Nutzungsbedingungen in den folgenden Dokumenten einverstanden.", "license": "Lizenz", "lizardbyte_website": "LizardByte Webseite", "resources": "Ressourcen", "resources_desc": "Ressourcen für Apollo!", "third_party_notice": "Drittanbieter-Mitteilung" }, "troubleshooting": { "dd_reset": "Persistente Anzeigeeinstellungen zurücksetzen", "dd_reset_desc": "Wenn Apollo versucht, die geänderten Geräteeinstellungen wiederherzustellen, können Sie die Einstellungen zurücksetzen und den Anzeigestatus manuell wiederherstellen.", "dd_reset_error": "Fehler beim Zurücksetzen der Persistenz!", "dd_reset_success": "Erfolgreich zurücksetzen!", "force_close": "Schließen erzwingen", "force_close_desc": "Wenn sich Moonlight über eine aktuell laufende App beschwert, sollte das Schließen der App das Problem beheben.", "force_close_error": "Fehler beim Schließen der Anwendung", "force_close_success": "Anwendung erfolgreich geschlossen!", "logs": "Logs", "logs_desc": "Siehe die Logs hochgeladen von Apollo", "logs_find": "Suchen...", "restart_apollo": "Apollo neu starten", "restart_apollo_desc": "Wenn Apollo nicht richtig funktioniert, können Sie versuchen, es neu zu starten. Dies wird alle laufenden Sitzungen beenden.", "restart_apollo_success": "Apollo wird neu gestartet", "troubleshooting": "Fehlerbehebung", "unpair_all": "Alle trennen", "unpair_all_error": "Fehler beim Entkoppeln", "unpair_all_success": "Erfolgreich getrennt!", "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.", "unpair_single_no_devices": "Es gibt keine gekoppelten Geräte.", "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.", "unpair_single_unknown": "Unbekannter Client", "unpair_title": "Geräte trennen" }, "welcome": { "confirm_password": "Passwort bestätigen", "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.", "create_creds_alert": "Die unten angegebenen Anmeldedaten werden benötigt, um auf das Webinterface von Apollo zuzugreifen. Halten Sie sie sicher, da Sie sie nie wieder sehen werden!", "greeting": "Willkommen bei Apollo!", "login": "Anmelden", "welcome_success": "Diese Seite wird bald neu geladen, Ihr Browser wird Sie nach den neuen Anmeldeinformationen fragen" } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/en.json ================================================ { "_common": { "apply": "Apply", "auto": "Automatic", "autodetect": "Autodetect (recommended)", "beta": "(beta)", "cancel": "Cancel", "cmd_name": "Command Name", "cmd_val": "Command Value", "default": "Default", "default_global": "Default (Global)", "disabled": "Disabled", "disabled_def": "Disabled (default)", "disabled_def_cbox": "Default: unchecked", "dismiss": "Dismiss", "do_cmd": "Do Command", "elevated": "Elevated", "enabled": "Enabled", "enabled_def": "Enabled (default)", "enabled_def_cbox": "Default: checked", "error": "Error!", "learn_more": "Learn More", "note": "Note:", "password": "Password", "run_as": "Run as Admin", "save": "Save", "see_more": "See More", "success": "Success!", "undo_cmd": "Undo Command", "username": "Username", "warning": "Warning!" }, "apps": { "actions": "Actions", "add_cmds": "Add Commands", "add_new": "Add New", "allow_client_commands": "Allow client prepare commands", "allow_client_commands_desc": "Whether to execute client prepare commands when running this app.", "alphabetize": "Alphabetize", "already_ordered": "Apps are already alphabetized.", "app_name": "Application Name", "app_name_desc": "Application Name, as shown on Moonlight", "applications_desc": "Applications are refreshed when a session is terminated.", "applications_reorder_desc": "Drag and drop apps to reorder the apps. Any changes made will terminate the current running app.", "applications_tips": "Tip: You can also right click the launch app button to copy the launch link, and send it to your friends. They need to use Artemis and have already paired with you.\nYou can also double click the app name to export the app entry to a file, to add them to your favorite emulator frontends.", "applications_title": "Applications", "auto_detach": "Continue streaming if the application exits quickly", "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.", "close": "Terminate", "close_warning": "Are you sure to terminate the current running app?", "close_failed": "App termination failed.", "cmd": "Command", "cmd_desc": "The main application to start. If blank, no application will be started.", "cmd_note": "If the path to the command executable contains spaces, you must enclose it in quotes.", "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.", "cmd_prep_name": "Command Preparations", "cmd_state_desc": "A list of commands to be run when resuming(first client connects when no clients are connected) or pausing(all clients disconnect) this application.\nDo commands for resume and Undo command for pause.\nPlease make sure to clean up any side effects of the commands in the preparation undo commands.\nPlease note that pause command will not be executed when the session terminates.", "cmd_state_name": "Resume/Pause Commands", "covers_found": "Covers Found", "delete": "Delete", "delete_failed": "App delete failed: ", "detached_cmds": "Detached Commands", "detached_cmds_add": "Add Detached Command", "detached_cmds_desc": "A list of commands to be run in the background.", "detached_cmds_note": "If the path to the command executable contains spaces, you must enclose it in quotes.", "edit": "Edit", "env_app_id": "App ID (legacy)", "env_app_name": "App Name", "env_app_uuid": "App UUID", "env_app_status": "App Status: One of 'STARTING', 'RUNNING', 'PAUSING', 'RESUMING', 'TERMINATING' (string)", "env_client_audio_config": "The Audio Configuration requested by the client (2.0/5.1/7.1)", "env_client_enable_sops": "The client has requested the option to optimize the game for optimal streaming (true/false)", "env_client_fps": "The FPS requested by the client (float)", "env_client_gcmap": "The requested gamepad mask, in a bitset/bitfield format (int)", "env_client_hdr": "HDR is enabled by the client (true/false)", "env_client_height": "The Height requested by the client (int)", "env_client_host_audio": "The client has requested host audio (true/false)", "env_client_width": "The Width requested by the client (int)", "env_client_uuid": "UUID of the client starting the stream (string)", "env_client_name": "Name of the client starting the stream (string)", "env_displayplacer_example": "Example - displayplacer for Resolution Automation:", "env_qres_example": "Example - QRes for Resolution Automation:", "env_qres_path": "qres path", "env_rtss_cli_example": "Example - rtss-cli for FPS Limitation:", "env_sunshine_compatibility": "Environment variables starting with \"SUNSHINE_\" are deprecated but still kept for compatibility with Sunshine related tools. The SUNSHINE_CLIENT_FPS variable is modified to FLOAT for fractional refresh rate support(especially for Special-K), if your script/tool can't take floating number input, go to Advanced tab to enable \"ENVVAR compatibility mode\". APOLLO_CLIENT_FPS is always FLOAT type.", "env_var_name": "Var Name", "env_vars_about": "About Environment Variables", "env_vars_desc": "All commands get these environment variables by default:", "env_xrandr_example": "Example - Xrandr for Resolution Automation:", "export_launcher_file": "Export Launcher File", "exit_timeout": "Exit Timeout", "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.", "find_cover": "Find Cover", "global_prep_desc": "Enable/Disable the execution of Global Prep Commands for this application.", "global_prep_name": "Global Prep Commands", "global_state_desc": "Enable/Disable the execution of Global Resume/Pause Commands for this application.", "global_state_name": "Global Resume/Pause Commands", "image": "Image", "image_desc": "Application icon/picture/image path that will be sent to client. Image must be a PNG file. If not set, Apollo will send default box image.", "launch": "Launch", "launch_local_client": "Do you want to launch the app from local client on this device?", "launch_warning": "Are you sure you want to launch this app? This will terminate the currently running app.", "launch_failed": "App launch failed: ", "loading": "Loading...", "name": "Name", "output_desc": "The file where the output of the command is stored, if it is not specified, the output is ignored", "output_name": "Output", "per_client_app_identity": "Per Client App Identity", "per_client_app_identity_desc": "Separate the app's identity per-client. Useful when you want different virtual display configurations on this specific app for different clients", "reorder_failed": "Failed to reorder apps: ", "resolution_scale_factor": "Resolution Scale Factor", "resolution_scale_factor_desc": "Scale the client requested resolution based on this factor. e.g. 2000x1000 with a factor of 120% will become 2400x1200. Overrides client requested factor when the number isn't 100%. This option won't affect client requested streaming resolution.", "run_as_desc": "This can be necessary for some applications that require administrator permissions to run properly. Might cause URL schemes to fail.", "save_failed": "Failed to save app: ", "save_order": "Save Order", "terminate_on_pause": "Terminate on Pause", "terminate_on_pause_desc": "Terminate the app when all clients are disconnected.", "use_app_identity": "Use App Identity", "use_app_identity_desc": "Use the app's own identity while creating virtual displays instead of client's. This is useful when you want display configuration for each APP separately.", "virtual_display": "Always create Virtual Display", "virtual_display_desc": "Always create a virtual display when starting this app, overriding client request. Please make sure the SudoVDA driver is installed and enabled.", "virtual_display_primary": "Enforce Virtual Display Primary", "virtual_display_primary_desc": "Automatically set the virtual display as primary display when the app starts. Virtual display will always be set to primary when client requests to use virtual display. (Recommended to keep on) [Broken on Windows 11 24H2]", "wait_all": "Continue streaming until all app processes exit", "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.", "working_dir": "Working Directory", "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, Apollo will default to the parent directory of the command" }, "client_card": { "clients": "Clients", "clients_desc": "Clients that are specifically tuned to work the best with Apollo", "generic_moonlight_clients_desc": "Generic Moonlight clients are still usable with Apollo." }, "config": { "adapter_name": "Adapter Name", "adapter_name_desc_linux_1": "Manually specify a GPU to use for capture.", "adapter_name_desc_linux_2": "to find all devices capable of VAAPI", "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 Apollo, it needs to have at the very minimum:", "adapter_name_desc_windows": "Manually specify a GPU to use for capture. If unset, the GPU is chosen automatically. We strongly recommend leaving this field blank to use automatic GPU selection! Note: This GPU must have a display connected and powered on. The appropriate values can be found using the following command:", "adapter_name_placeholder_windows": "Radeon RX 580 Series", "add": "Add", "address_family": "Address Family", "address_family_both": "IPv4+IPv6", "address_family_desc": "Set the address family used by Apollo", "address_family_ipv4": "IPv4 only", "always_send_scancodes": "Always Send Scancodes", "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.", "amd_coder": "AMF Coder (H264)", "amd_coder_desc": "Allows you to select the entropy encoding to prioritize quality or encoding speed. H.264 only.", "amd_enforce_hrd": "AMF Hypothetical Reference Decoder (HRD) Enforcement", "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.", "amd_preanalysis": "AMF Preanalysis", "amd_preanalysis_desc": "This enables rate-control preanalysis, which may increase quality at the expense of increased encoding latency.", "amd_quality": "AMF Quality", "amd_quality_balanced": "balanced -- balanced (default)", "amd_quality_desc": "This controls the balance between encoding speed and quality.", "amd_quality_group": "AMF Quality Settings", "amd_quality_quality": "quality -- prefer quality", "amd_quality_speed": "speed -- prefer speed", "amd_rc": "AMF Rate Control", "amd_rc_cbr": "cbr -- constant bitrate (recommended if HRD is enabled)", "amd_rc_cqp": "cqp -- constant qp mode", "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.", "amd_rc_group": "AMF Rate Control Settings", "amd_rc_vbr_latency": "vbr_latency -- latency constrained variable bitrate (recommended if HRD is disabled; default)", "amd_rc_vbr_peak": "vbr_peak -- peak constrained variable bitrate", "amd_usage": "AMF Usage", "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.", "amd_usage_lowlatency": "lowlatency - low latency (fastest)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - low latency, high quality (fast)", "amd_usage_transcoding": "transcoding -- transcoding (slowest)", "amd_usage_ultralowlatency": "ultralowlatency - ultra low latency (fastest; default)", "amd_usage_webcam": "webcam -- webcam (slow)", "amd_vbaq": "AMF Variance Based Adaptive Quantization (VBAQ)", "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.", "apply_note": "Click 'Apply' to restart Apollo and apply changes. This will terminate any running sessions.", "audio_sink": "Audio Sink", "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:", "audio_sink_desc_macos": "The name of the audio sink used for Audio Loopback. Apollo can only access microphones on macOS due to system limitations. To stream system audio using Soundflower or BlackHole.", "audio_sink_desc_windows": "The audio device to be used when audio output is allowed on host by the client.\nIf unset, the device is chosen automatically.\nYou can get the Device ID using the following command:", "audio_sink_placeholder_macos": "BlackHole 2ch", "audio_sink_placeholder_windows": "Speakers (High Definition Audio Device)", "auto_capture_sink": "Auto capture current sink", "auto_capture_sink_desc": "Auto capture current sink after default audio sink changed.", "av1_mode": "AV1 Support", "av1_mode_0": "Apollo will advertise support for AV1 based on encoder capabilities (recommended)", "av1_mode_1": "Apollo will not advertise support for AV1", "av1_mode_2": "Apollo will advertise support for AV1 Main 8-bit profile", "av1_mode_3": "Apollo will advertise support for AV1 Main 8-bit and 10-bit (HDR) profiles", "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.", "back_button_timeout": "Home/Guide Button Emulation Timeout", "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.", "capture": "Force a Specific Capture Method", "capture_desc": "On automatic mode Apollo will use the first one that works. NvFBC requires patched nvidia drivers.", "cert": "Certificate", "cert_desc": "The certificate used for the web UI and Moonlight client pairing. For best compatibility, this should have an RSA-2048 public key.", "channels": "Maximum Connected Clients", "channels_desc_1": "Apollo can allow a single streaming session to be shared with multiple clients simultaneously.", "channels_desc_2": "Some hardware encoders may have limitations that reduce performance with multiple streams.", "coder_cabac": "cabac -- context adaptive binary arithmetic coding - higher quality", "coder_cavlc": "cavlc -- context adaptive variable-length coding - faster decode", "configuration": "Configuration", "controller": "Enable Gamepad Input", "controller_desc": "Allows guests to control the host system with a gamepad / controller", "credentials_file": "Credentials File", "credentials_file_desc": "Store Username/Password separately from Apollo's state file.", "dd_configuration_option": "Device configuration", "dd_config_ensure_active": "Activate the display automatically", "dd_config_ensure_only_display": "Deactivate other displays and activate only the specified display", "dd_config_ensure_primary": "Activate the display automatically and make it a primary display", "dd_config_revert_delay": "Config revert delay", "dd_config_revert_delay_desc": "Additional delay in milliseconds to wait before reverting configuration when the app has been closed or the last session terminated. Main purpose is to provide a smoother transition when quickly switching between apps.", "dd_config_revert_on_disconnect": "Config revert on disconnect", "dd_config_revert_on_disconnect_desc": "Revert configuration upon disconnect of all clients instead of app close or last session termination.", "dd_config_verify_only": "Verify that the display is enabled", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "Switch on/off the HDR mode as requested by the client (default)", "dd_hdr_option_disabled": "Do not change HDR settings", "dd_mode_remapping": "Display mode remapping", "dd_mode_remapping_add": "Add remapping entry", "dd_mode_remapping_desc_1": "Specify remapping entries to change the requested resolution and/or refresh rate to other values.", "dd_mode_remapping_desc_2": "The list is iterated from top to bottom and the first match is used.", "dd_mode_remapping_desc_3": "\"Requested\" fields can be left empty to match any requested value.", "dd_mode_remapping_desc_4_final_values_mixed": "At least one \"Final\" field must be specified. The unspecified resolution or refresh rate will not be changed.", "dd_mode_remapping_desc_4_final_values_non_mixed": "\"Final\" field must be specified and cannot be empty.", "dd_mode_remapping_desc_5_sops_mixed_only": "\"Optimize game settings\" option must be enabled in the Moonlight client, otherwise entries with any resolution fields specified are skipped.", "dd_mode_remapping_desc_5_sops_resolution_only": "\"Optimize game settings\" option must be enabled in the Moonlight client, otherwise the mapping is skipped.", "dd_mode_remapping_final_refresh_rate": "Final refresh rate", "dd_mode_remapping_final_resolution": "Final resolution", "dd_mode_remapping_requested_fps": "Requested FPS", "dd_mode_remapping_requested_resolution": "Requested resolution", "dd_options_header": "Advanced display device options", "dd_refresh_rate_option": "Refresh rate", "dd_refresh_rate_option_auto": "Use FPS value provided by the client (default)", "dd_refresh_rate_option_disabled": "Do not change refresh rate", "dd_refresh_rate_option_manual": "Use manually entered refresh rate", "dd_refresh_rate_option_manual_desc": "Enter the refresh rate to be used", "dd_resolution_option": "Resolution", "dd_resolution_option_auto": "Use resolution provided by the client (default)", "dd_resolution_option_disabled": "Do not change resolution", "dd_resolution_option_manual": "Use manually entered resolution", "dd_resolution_option_manual_desc": "Enter the resolution to be used", "dd_resolution_option_ogs_desc": "\"Optimize game settings\" option must be enabled on the Moonlight client for this to work.", "dd_resolution_option_vdisplay_desc": "Not recommended to use this option on usual circumstances. Please use native Windows settings to change display configurations. When using virtual display, only built-in resolution and client requested resolution/refresh rate will work.", "dd_resolution_option_multi_instance_desc": "When using multiple instance of Apollo, make sure you have disabled this option in all instances, or it'll end up messing things up. It's still recommended to disable this part if you don't want your physical monitor's resolution to change or you only stream with virtual display.", "dd_wa_hdr_toggle_delay_desc_1": "When using virtual display device (VDD) for streaming, it might incorrectly display HDR color. Apollo can try to mitigate this issue, by turning HDR off and then on again.", "dd_wa_hdr_toggle_delay_desc_2": "If the value is set to 0, the workaround is disabled (default). If the value is between 0 and 3000 milliseconds, Apollo 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.", "dd_wa_hdr_toggle_delay_desc_3": "DO NOT use this workaround unless you actually have issues with HDR as it directly impacts stream start time!", "dd_wa_hdr_toggle_delay": "High-contrast workaround for HDR", "double_refreshrate": "Double refresh rate for Virtual Display", "double_refreshrate_desc": "Double the requested refresh rate when creating virtual displays, streamed refresh rate still remain the same. Can potentially improve stutter problem on some systems.", "ds4_back_as_touchpad_click": "Map Back/Select to Touchpad Click", "ds4_back_as_touchpad_click_desc": "When forcing DS4 emulation, map Back/Select to Touchpad Click", "enable_discovery": "Enable Auto Discovery", "enable_discovery_desc": "When disabled, you'll need to manually enter host IP on the client to pair.", "enable_input_only_mode": "Enable Input Only Mode", "enable_input_only_mode_desc": "Add an Input Only app entry. When enabled, the app list will only show the current running app and the Input Only entry when streaming. The Input Only entry will not receive any image or audio. Useful for operating the desktop on TV or connecting peripherals which the TV doesn't support with a phone.", "enable_pairing": "Enable Pairing", "enable_pairing_desc": "Enable pairing for the Moonlight client. This allows the client to authenticate with the host and establish a secure connection.", "encoder": "Force a Specific Encoder", "encoder_desc": "Force a specific encoder, otherwise Apollo 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.", "encoder_software": "Software", "envvar_compatibility_mode": "ENVVAR compatibility mode", "envvar_compatibility_mode_desc": "Enable compatibility mode for environment variables. This will modify the behavior of certain environment variables to be more compatible with older tools.", "external_ip": "External IP", "external_ip_desc": "If no external IP address is given, Apollo will automatically detect external IP", "fallback_mode": "Fallback Display Mode", "fallback_mode_desc": "Apollo will use this mode when the client does not provide a mode or when the app is launched through the web UI. Format: [Width]x[Height]x[FPS]", "fallback_mode_error": "Invalid fallback mode. Format: [Width]x[Height]x[FPS]", "fec_percentage": "FEC Percentage", "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.", "ffmpeg_auto": "auto -- let ffmpeg decide (default)", "file_apps": "Apps File", "file_apps_desc": "The file where current apps of Apollo are stored.", "file_state": "State File", "file_state_desc": "The file where current state of Apollo is stored", "forward_rumble": "Forward Rumble Messages", "forward_rumble_desc": "Forward Rumble Messages to clients", "gamepad": "Emulated Gamepad Type", "gamepad_auto": "Automatic selection options", "gamepad_desc": "Choose which type of gamepad to emulate on the host", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "DS4 selection options", "gamepad_ds5": "DS5 (PS5)", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Manual DS4 options", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Command Preparations", "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.", "global_state_cmd": "Resume/Pause Commands", "global_state_cmd_desc": "Configure a list of commands to be executed when resuming(first client connects when no clients are connected) or pausing(all clients disconnect) any application.\nDo commands for resume and Undo command for pause.\nPlease make sure to clean up any side effects of the commands in the preparation undo commands.\nPlease note that pause command will not be executed when the session terminates.", "headless_mode": "Headless Mode", "headless_mode_desc": "Start Apollo in headless mode. When enabled, all apps will start in virtual display.", "hevc_mode": "HEVC Support", "hevc_mode_0": "Apollo will advertise support for HEVC based on encoder capabilities (recommended)", "hevc_mode_1": "Apollo will not advertise support for HEVC", "hevc_mode_2": "Apollo will advertise support for HEVC Main profile", "hevc_mode_3": "Apollo will advertise support for HEVC Main and Main10 (HDR) profiles", "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.", "hide_tray_controls": "Hide tray control options", "hide_tray_controls_desc": "Do not show \"Force Stop\", \"Restart\" and \"Quit\" in tray menu.", "high_resolution_scrolling": "High Resolution Scrolling Support", "high_resolution_scrolling_desc": "When enabled, Apollo 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.", "ignore_encoder_probe_failure": "Ignore Encoder Probe Failure", "ignore_encoder_probe_failure_desc": "Allow streaming to continue even if probing for encoders fails. This may result in streaming failure if no encoder is available.", "install_steam_audio_drivers": "Install Steam Audio Drivers", "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.", "isolated_virtual_display_option": "Move the Virtual Display to the bottom right-most corner of the display layout", "isolated_virtual_display_option_desc": "This makes the display isolated from all other display and contains mouse movements to the virtual screen. This reorganizes the displays such that the all other displays are to the left of the virtual display.", "keep_sink_default": "Keep virtual sink as default", "keep_sink_default_desc": "Whether to force selected virtual sink as default (effective when host audio output is disabled).", "key_repeat_delay": "Key Repeat Delay", "key_repeat_delay_desc": "Control how fast keys will repeat themselves. The initial delay in milliseconds before repeating keys.", "key_repeat_frequency": "Key Repeat Frequency", "key_repeat_frequency_desc": "How often keys repeat every second. This configurable option supports decimals.", "key_rightalt_to_key_win": "Map Right Alt key to Windows key", "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 Apollo think the Right Alt key is the Windows key", "keyboard": "Enable Keyboard Input", "keyboard_desc": "Allows guests to control the host system with the keyboard", "lan_encryption_mode": "LAN Encryption Mode", "lan_encryption_mode_1": "Enabled for supported clients", "lan_encryption_mode_2": "Required for all clients", "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.", "legacy_ordering": "App ordering for legacy clients", "legacy_ordering_desc": "Enable ordering support workaround for legacy clients. Can cause issues with clients or scripts that can't handle UTF8 correctly. Artemis clients support this by default.", "limit_framerate": "Limit capture framerate", "limit_framerate_desc": "Limit the framerate being captured to client requested framerate. May not run at full framerate if vsync is enabled and display refreshrate does not match requested framerate. Could cause lag on some clients if disabled.", "locale": "Locale", "locale_desc": "The locale used for Apollo's user interface.", "min_log_level": "Log Level", "min_log_level_1": "Debug", "min_log_level_0": "Verbose", "min_log_level_2": "Info", "min_log_level_3": "Warning", "min_log_level_4": "Error", "min_log_level_5": "Fatal", "min_log_level_6": "None", "min_log_level_desc": "The minimum log level printed to standard out", "log_path": "Logfile Path", "log_path_desc": "The file where the current logs of Apollo are stored.", "max_bitrate": "Maximum Bitrate", "max_bitrate_desc": "The maximum bitrate (in Kbps) that Apollo will encode the stream at. If set to 0, it will always use the bitrate requested by the client.", "minimum_fps_target": "Minimum FPS Target", "minimum_fps_target_desc": "The lowest effective FPS a stream can reach. Set 0 for automatic.", "min_threads": "Minimum CPU Thread Count", "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.", "misc": "Miscellaneous options", "motion_as_ds4": "Emulate a DS4 gamepad if the client gamepad reports motion sensors are present", "motion_as_ds4_desc": "If disabled, motion sensors will not be taken into account during gamepad type selection.", "mouse": "Enable Mouse Input", "mouse_desc": "Allows guests to control the host system with the mouse", "native_pen_touch": "Native Pen/Touch Support", "native_pen_touch_desc": "When enabled, Apollo will pass through native pen/touch events from Moonlight clients. This can be useful to disable for older applications without native pen/touch support.", "notify_pre_releases": "PreRelease Notifications", "notify_pre_releases_desc": "Whether to be notified of new pre-release versions of Apollo", "nvenc_h264_cavlc": "Prefer CAVLC over CABAC in H.264", "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.", "nvenc_intra_refresh": "Intra Refresh", "nvenc_intra_refresh_desc": "Enable Intra Refresh for some clients to render correctly continuously (e.g. Xbox Client)", "nvenc_latency_over_power": "Prefer lower encoding latency over power savings", "nvenc_latency_over_power_desc": "Apollo 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.", "nvenc_opengl_vulkan_on_dxgi": "Present OpenGL/Vulkan on top of DXGI", "nvenc_opengl_vulkan_on_dxgi_desc": "Apollo 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 Apollo program exit.", "nvenc_preset": "Performance preset", "nvenc_preset_1": "(fastest, default)", "nvenc_preset_7": "(slowest)", "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.", "nvenc_realtime_hags": "Use realtime priority in hardware accelerated gpu scheduling", "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.", "nvenc_spatial_aq": "Spatial AQ", "nvenc_spatial_aq_desc": "Assign higher QP values to flat regions of the video. Recommended to enable when streaming at lower bitrates.", "nvenc_spatial_aq_disabled": "Disabled (faster, default)", "nvenc_spatial_aq_enabled": "Enabled (slower)", "nvenc_twopass": "Two-pass mode", "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.", "nvenc_twopass_disabled": "Disabled (fastest, not recommended)", "nvenc_twopass_full_res": "Full resolution (slower)", "nvenc_twopass_quarter_res": "Quarter resolution (faster, default)", "nvenc_vbv_increase": "Single-frame VBV/HRD percentage increase", "nvenc_vbv_increase_desc": "By default Apollo 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.", "origin_web_ui_allowed": "Origin Web UI Allowed", "origin_web_ui_allowed_desc": "The origin of the remote endpoint address that is not denied access to Web UI", "origin_web_ui_allowed_lan": "Only those in LAN may access Web UI", "origin_web_ui_allowed_pc": "Only localhost may access Web UI", "origin_web_ui_allowed_wan": "Anyone may access Web UI", "output_name_desc_unix": "During Apollo 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.", "output_name_desc_windows": "Manually specify a display device id 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. During Apollo startup, you should see the list of detected displays. Below is an example; the actual output can be found in the Troubleshooting tab.", "output_name_unix": "Display number", "output_name_windows": "Display Device Id", "ping_timeout": "Ping Timeout", "ping_timeout_desc": "How long to wait in milliseconds for data from moonlight before shutting down the stream", "pkey": "Private Key", "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.", "port": "Port", "port_alert_1": "Apollo cannot use ports below 1024!", "port_alert_2": "Ports above 65535 are not available!", "port_desc": "Set the family of ports used by Apollo", "port_http_port_note": "Use this port to connect with Moonlight.", "port_note": "Note", "port_port": "Port", "port_protocol": "Protocol", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "Exposing the Web UI to the internet is a security risk! Proceed at your own risk!", "port_web_ui": "Web UI", "qp": "Quantization Parameter", "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.", "qsv_coder": "QuickSync Coder (H264)", "qsv_preset": "QuickSync Preset", "qsv_preset_fast": "fast (low quality)", "qsv_preset_faster": "faster (lower quality)", "qsv_preset_medium": "medium (default)", "qsv_preset_slow": "slow (good quality)", "qsv_preset_slower": "slower (better quality)", "qsv_preset_slowest": "slowest (best quality)", "qsv_preset_veryfast": "fastest (lowest quality)", "qsv_slow_hevc": "Allow Slow HEVC Encoding", "qsv_slow_hevc_desc": "This can enable HEVC encoding on older Intel GPUs, at the cost of higher GPU usage and worse performance.", "restart_note": "Apollo is restarting to apply changes.", "server_cmd": "Server Commands", "server_cmd_desc": "Configure a list of commands to be executed when called from client during streaming.", "stream_audio": "Stream Audio", "stream_audio_desc": "Whether to stream audio or not. Disabling this can be useful for streaming headless displays as second monitors.", "sunshine_name": "Server Name", "sunshine_name_desc": "The name displayed by Moonlight. If not specified, the PC's hostname is used", "sw_preset": "SW Presets", "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.", "sw_preset_fast": "fast", "sw_preset_faster": "faster", "sw_preset_medium": "medium", "sw_preset_slow": "slow", "sw_preset_slower": "slower", "sw_preset_superfast": "superfast (default)", "sw_preset_ultrafast": "ultrafast", "sw_preset_veryfast": "veryfast", "sw_preset_veryslow": "veryslow", "sw_tune": "SW Tune", "sw_tune_animation": "animation -- good for cartoons; uses higher deblocking and more reference frames", "sw_tune_desc": "Tuning options, which are applied after the preset. Defaults to zerolatency.", "sw_tune_fastdecode": "fastdecode -- allows faster decoding by disabling certain filters", "sw_tune_film": "film -- use for high quality movie content; lowers deblocking", "sw_tune_grain": "grain -- preserves the grain structure in old, grainy film material", "sw_tune_stillimage": "stillimage -- good for slideshow-like content", "sw_tune_zerolatency": "zerolatency -- good for fast encoding and low-latency streaming (default)", "system_tray": "Enable System Tray", "system_tray_desc": "Whether to show Apollo icon in the system tray", "touchpad_as_ds4": "Emulate a DS4 gamepad if the client gamepad reports a touchpad is present", "touchpad_as_ds4_desc": "If disabled, touchpad presence will not be taken into account during gamepad type selection.", "upnp": "UPnP", "upnp_desc": "Automatically configure port forwarding for streaming over the Internet", "vaapi_strict_rc_buffer": "Strictly enforce frame bitrate limits for H.264/HEVC on AMD GPUs", "vaapi_strict_rc_buffer_desc": "Enabling this option can avoid dropped frames over the network during scene changes, but video quality may be reduced during motion.", "virtual_sink": "Virtual Sink", "virtual_sink_desc": "The audio device to be used when audio output isn't allowed on host by the client.\nIf unset, the device is chosen automatically.\nWe strongly recommend leaving this field blank to use automatic device selection!", "virtual_sink_placeholder": "Steam Streaming Speakers", "vt_coder": "VideoToolbox Coder", "vt_realtime": "VideoToolbox Realtime Encoding", "vt_software": "VideoToolbox Software Encoding", "vt_software_allowed": "Allowed", "vt_software_forced": "Forced", "wan_encryption_mode": "WAN Encryption Mode", "wan_encryption_mode_1": "Enabled for supported clients (default)", "wan_encryption_mode_2": "Required for all clients", "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." }, "login": { "save_password": "Remember Password" }, "index": { "description": "Apollo is a self-hosted game stream host for Moonlight.", "download": "Download", "installed_version_not_stable": "You are running a pre-release version of Apollo. You may experience bugs or other issues. Please report any issues you encounter. Thank you for helping to make Apollo a better software!", "loading_latest": "Loading latest release...", "new_pre_release": "A new Pre-Release Version is Available!", "new_stable": "A new Stable Version is Available!", "startup_errors": "Attention! Apollo detected these errors during startup. We STRONGLY RECOMMEND fixing them before streaming.", "version_dirty": "Thank you for helping to make Apollo a better software!", "version_latest": "You are running the latest version of Apollo", "welcome": "Hello, Apollo!" }, "navbar": { "applications": "Applications", "configuration": "Configuration", "home": "Home", "password": "Change Password", "pin": "PIN", "theme_auto": "Auto", "theme_dark": "Dark", "theme_light": "Light", "toggle_theme": "Theme", "troubleshoot": "Troubleshooting" }, "password": { "confirm_password": "Confirm Password", "current_creds": "Current Credentials", "new_creds": "New Credentials", "new_username_desc": "If not specified, the username will not change", "password_change": "Password Change", "success_msg": "Password has been changed successfully! This page will reload soon, your browser will ask you for the new credentials." }, "permissions": { "input_controller": "Controller Input", "input_touch": "Touch Input", "input_pen": "Pen Input", "input_mouse": "Mouse Input", "input_kbd": "Keyboard Input", "clipboard_set": "Clipboard Set", "clipboard_read": "Clipboard Read", "file_upload": "File Upload", "file_dwnload": "File Download", "server_cmd": "Server Command", "list": "List Apps", "view": "View Streams", "launch": "Launch Apps" }, "pin": { "allow_client_commands": "Allow client commands", "allow_client_commands_desc": "Allow client commands to be executed when connecting to this device.", "always_use_virtual_display": "Always create virtual display", "always_use_virtual_display_desc": "Always create a virtual display when connecting from this device.", "client_do_cmd": "Client connect commands", "client_do_cmd_desc": "Commands to be executed when client connects. All of the commands are executed detached.", "client_undo_cmd": "Client disconnect commands", "client_undo_cmd_desc": "Commands to be executed when client disconnects. All of the commands are executed detached.", "device_name": "Optional: Device Name", "display_mode_override": "Display Mode Override", "display_mode_override_desc": "Apollo will ignore client requested display mode and use this value to configure (virtual) displays. Leave blank for auto matching. Format: [Width]x[Height]x[FPS]", "display_mode_override_error": "Invalid mode override. Format: [Width]x[Height]x[FPS]", "enable_legacy_ordering": "Enable legacy ordering", "enable_legacy_ordering_desc": "Enable app ordering for legacy clients. Disable if this client can't handle UTF8 correctly. Needs to enable \"App ordering for legacy clients\" in the Advanced settings.", "pair_failure": "Pairing Failed: Check if the PIN is typed correctly", "pair_success": "Success! Please check Moonlight to continue", "pair_success_check_perm": "Pair success! Please grant necessary permissions to the client manually below.", "pin_pairing": "PIN Pairing", "send": "Send", "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!", "otp_pairing": "OTP Pairing", "generate_pin": "Generate PIN", "otp_passphrase": "One Time Passphrase", "otp_expired": "EXPIRED", "otp_expired_msg": "OTP expired. Please request a new one.", "otp_success": "PIN request success, the PIN is available within 3 minutes.", "otp_msg": "OTP pairing is only available for the latest Artemis clients. Please use legacy pairing method for other clients.", "otp_pair_now": "PIN generated successfully, do you want to pair now?", "device_management": "Device Management", "device_management_desc": "Manage your paired devices and their permissions.", "device_management_warning": "The first paired device will have full permissions by default. Other newly paired devices will have limited permissions set by default.", "save_client_error": "Error while saving client: ", "unpair_all": "Unpair All", "unpair_all_success": "All devices unpaired.", "unpair_all_error": "Error while unpairing", "unpair_single_no_devices": "There are no paired devices.", "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.", "unpair_single_unknown": "Unknown Client" }, "resource_card": { "github_discussions": "GitHub Discussions", "github_wiki": "GitHub WiKi", "github_stuttering_clinic": "Stuttering Clinic", "legal": "Legal", "legal_desc": "By continuing to use this software you agree to the terms and conditions in the following documents.", "license": "License", "resources": "Resources", "resources_desc": "Resources for Apollo!", "third_party_notice": "Third Party Notice" }, "troubleshooting": { "dd_reset": "Reset Persistent Display Device Settings", "dd_reset_desc": "If Apollo is stuck trying to restore the changed display device settings, you can reset the settings and proceed to restore the display state manually.", "dd_reset_error": "Error while resetting persistence!", "dd_reset_success": "Success resetting persistence!", "force_close": "Force Close", "force_close_desc": "If Moonlight complains about an app currently running, force closing the app should fix the issue. This will also reload the App list.", "force_close_error": "Error while closing Application", "force_close_success": "Application Closed Successfully!", "logs": "Logs", "logs_desc": "See the logs uploaded by Apollo", "logs_find": "Find...", "restart_apollo": "Restart Apollo", "restart_apollo_desc": "If Apollo isn't working properly, you can try restarting it. This will terminate any running sessions.", "restart_apollo_success": "Apollo is restarting", "quit_apollo": "Quit Apollo", "quit_apollo_desc": "Exit Apollo. This will terminate any running sessions.", "quit_apollo_success": "Apollo has exited.", "quit_apollo_success_ongoing": "Apollo is quitting...", "quit_apollo_confirm": "Do you really want to quit Apollo? You'll not be able to start Apollo again if you have no other methods to operate your computer.", "troubleshooting": "Troubleshooting" }, "welcome": { "confirm_password": "Confirm password", "create_creds": "Before Getting Started, we need you to make a new username and password for accessing the Web UI.", "create_creds_alert": "The credentials below are needed to access Apollo's Web UI. Keep them safe, since you will never see them again!", "greeting": "Welcome to Apollo!", "login": "Login", "welcome_success": "This page will reload soon, your browser will ask you for the new credentials", "login_success": "This page will reload soon." } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/en_GB.json ================================================ { "_common": { "apply": "Apply", "auto": "Automatic", "autodetect": "Autodetect (recommended)", "beta": "(beta)", "cancel": "Cancel", "disabled": "Disabled", "disabled_def": "Disabled (default)", "disabled_def_cbox": "Default: unchecked", "dismiss": "Dismiss", "do_cmd": "Do Command", "elevated": "Elevated", "enabled": "Enabled", "enabled_def": "Enabled (default)", "enabled_def_cbox": "Default: checked", "error": "Error!", "note": "Note:", "password": "Password", "run_as": "Run as Admin", "save": "Save", "see_more": "See More", "success": "Success!", "undo_cmd": "Undo Command", "username": "Username", "warning": "Warning!" }, "apps": { "actions": "Actions", "add_cmds": "Add Commands", "add_new": "Add New", "app_name": "Application Name", "app_name_desc": "Application Name, as shown on Moonlight", "applications_desc": "Applications are refreshed only when Client is restarted", "applications_title": "Applications", "auto_detach": "Continue streaming if the application exits quickly", "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.", "cmd": "Command", "cmd_desc": "The main application to start. If blank, no application will be started.", "cmd_note": "If the path to the command executable contains spaces, you must enclose it in quotes.", "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.", "cmd_prep_name": "Command Preparations", "covers_found": "Covers Found", "delete": "Delete", "detached_cmds": "Detached Commands", "detached_cmds_add": "Add Detached Command", "detached_cmds_desc": "A list of commands to be run in the background.", "detached_cmds_note": "If the path to the command executable contains spaces, you must enclose it in quotes.", "edit": "Edit", "env_app_id": "App ID", "env_app_name": "App Name", "env_client_audio_config": "The Audio Configuration requested by the client (2.0/5.1/7.1)", "env_client_enable_sops": "The client has requested the option to optimize the game for optimal streaming (true/false)", "env_client_fps": "The FPS requested by the client (float)", "env_client_gcmap": "The requested gamepad mask, in a bitset/bitfield format (int)", "env_client_hdr": "HDR is enabled by the client (true/false)", "env_client_height": "The Height requested by the client (int)", "env_client_host_audio": "The client has requested host audio (true/false)", "env_client_width": "The Width requested by the client (int)", "env_displayplacer_example": "Example - displayplacer for Resolution Automation:", "env_qres_example": "Example - QRes for Resolution Automation:", "env_qres_path": "qres path", "env_var_name": "Var Name", "env_vars_about": "About Environment Variables", "env_vars_desc": "All commands get these environment variables by default:", "env_xrandr_example": "Example - Xrandr for Resolution Automation:", "exit_timeout": "Exit Timeout", "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.", "find_cover": "Find Cover", "global_prep_desc": "Enable/Disable the execution of Global Prep Commands for this application.", "global_prep_name": "Global Prep Commands", "image": "Image", "image_desc": "Application icon/picture/image path that will be sent to client. Image must be a PNG file. If not set, Apollo will send default box image.", "loading": "Loading...", "name": "Name", "output_desc": "The file where the output of the command is stored, if it is not specified, the output is ignored", "output_name": "Output", "run_as_desc": "This can be necessary for some applications that require administrator permissions to run properly.", "wait_all": "Continue streaming until all app processes exit", "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.", "working_dir": "Working Directory", "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, Apollo will default to the parent directory of the command" }, "config": { "adapter_name": "Adapter Name", "adapter_name_desc_linux_1": "Manually specify a GPU to use for capture.", "adapter_name_desc_linux_2": "to find all devices capable of VAAPI", "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 Apollo, it needs to have at the very minimum:", "adapter_name_desc_windows": "Manually specify a GPU to use for capture. If unset, the GPU is chosen automatically. We strongly recommend leaving this field blank to use automatic GPU selection! Note: This GPU must have a display connected and powered on. The appropriate values can be found using the following command:", "adapter_name_placeholder_windows": "Radeon RX 580 Series", "add": "Add", "address_family": "Address Family", "address_family_both": "IPv4+IPv6", "address_family_desc": "Set the address family used by Apollo", "address_family_ipv4": "IPv4 only", "always_send_scancodes": "Always Send Scancodes", "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.", "amd_coder": "AMF Coder (H264)", "amd_coder_desc": "Allows you to select the entropy encoding to prioritize quality or encoding speed. H.264 only.", "amd_enforce_hrd": "AMF Hypothetical Reference Decoder (HRD) Enforcement", "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.", "amd_preanalysis": "AMF Preanalysis", "amd_preanalysis_desc": "This enables rate-control preanalysis, which may increase quality at the expense of increased encoding latency.", "amd_quality": "AMF Quality", "amd_quality_balanced": "balanced -- balanced (default)", "amd_quality_desc": "This controls the balance between encoding speed and quality.", "amd_quality_group": "AMF Quality Settings", "amd_quality_quality": "quality -- prefer quality", "amd_quality_speed": "speed -- prefer speed", "amd_rc": "AMF Rate Control", "amd_rc_cbr": "cbr -- constant bitrate", "amd_rc_cqp": "cqp -- constant qp mode", "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.", "amd_rc_group": "AMF Rate Control Settings", "amd_rc_vbr_latency": "vbr_latency -- latency constrained variable bitrate (default)", "amd_rc_vbr_peak": "vbr_peak -- peak constrained variable bitrate", "amd_usage": "AMF Usage", "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.", "amd_usage_lowlatency": "lowlatency - low latency (fast)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - low latency, high quality (fast)", "amd_usage_transcoding": "transcoding -- transcoding (slowest)", "amd_usage_ultralowlatency": "ultralowlatency - ultra low latency (fastest)", "amd_usage_webcam": "webcam -- webcam (slow)", "amd_vbaq": "AMF Variance Based Adaptive Quantization (VBAQ)", "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.", "apply_note": "Click 'Apply' to restart Apollo and apply changes. This will terminate any running sessions.", "audio_sink": "Audio Sink", "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:", "audio_sink_desc_macos": "The name of the audio sink used for Audio Loopback. Apollo can only access microphones on macOS due to system limitations. To stream system audio using Soundflower or BlackHole.", "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:", "audio_sink_placeholder_macos": "BlackHole 2ch", "audio_sink_placeholder_windows": "Speakers (High Definition Audio Device)", "av1_mode": "AV1 Support", "av1_mode_0": "Apollo will advertise support for AV1 based on encoder capabilities (recommended)", "av1_mode_1": "Apollo will not advertise support for AV1", "av1_mode_2": "Apollo will advertise support for AV1 Main 8-bit profile", "av1_mode_3": "Apollo will advertise support for AV1 Main 8-bit and 10-bit (HDR) profiles", "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.", "back_button_timeout": "Home/Guide Button Emulation Timeout", "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.", "capture": "Force a Specific Capture Method", "capture_desc": "On automatic mode Apollo will use the first one that works. NvFBC requires patched nvidia drivers.", "cert": "Certificate", "cert_desc": "The certificate used for the web UI and Moonlight client pairing. For best compatibility, this should have an RSA-2048 public key.", "channels": "Maximum Connected Clients", "channels_desc_1": "Apollo can allow a single streaming session to be shared with multiple clients simultaneously.", "channels_desc_2": "Some hardware encoders may have limitations that reduce performance with multiple streams.", "coder_cabac": "cabac -- context adaptive binary arithmetic coding - higher quality", "coder_cavlc": "cavlc -- context adaptive variable-length coding - faster decode", "configuration": "Configuration", "controller": "Enable Gamepad Input", "controller_desc": "Allows guests to control the host system with a gamepad / controller", "credentials_file": "Credentials File", "credentials_file_desc": "Store Username/Password separately from Apollo's state file.", "dd_config_ensure_active": "Activate the display automatically", "dd_config_ensure_only_display": "Deactivate other displays and activate only the specified display", "dd_config_ensure_primary": "Activate the display automatically and make it a primary display", "dd_config_label": "Device configuration", "dd_config_revert_delay": "Config revert delay", "dd_config_revert_delay_desc": "Additional delay in milliseconds to wait before reverting configuration when the app has been closed or the last session terminated. Main purpose is to provide a smoother transition when quickly switching between apps.", "dd_config_verify_only": "Verify that the display is enabled", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "Switch on/off the HDR mode as requested by the client (default)", "dd_hdr_option_disabled": "Do not change HDR settings", "dd_mode_remapping": "Display mode remapping", "dd_mode_remapping_add": "Add remapping entry", "dd_mode_remapping_desc_1": "Specify remapping entries to change the requested resolution and/or refresh rate to other values.", "dd_mode_remapping_desc_2": "The list is iterated from top to bottom and the first match is used.", "dd_mode_remapping_desc_3": "\"Requested\" fields can be left empty to match any requested value.", "dd_mode_remapping_desc_4_final_values_mixed": "At least one \"Final\" field must be specified. The unspecified resolution or refresh rate will not be changed.", "dd_mode_remapping_desc_4_final_values_non_mixed": "\"Final\" field must be specified and cannot be empty.", "dd_mode_remapping_desc_5_sops_mixed_only": "\"Optimize game settings\" option must be enabled in the Moonlight client, otherwise entries with any resolution fields specified are skipped.", "dd_mode_remapping_desc_5_sops_resolution_only": "\"Optimize game settings\" option must be enabled in the Moonlight client, otherwise the mapping is skipped.", "dd_mode_remapping_final_refresh_rate": "Final refresh rate", "dd_mode_remapping_final_resolution": "Final resolution", "dd_mode_remapping_requested_fps": "Requested FPS", "dd_mode_remapping_requested_resolution": "Requested resolution", "dd_options_header": "Advanced display device options", "dd_refresh_rate_option": "Refresh rate", "dd_refresh_rate_option_auto": "Use FPS value provided by the client (default)", "dd_refresh_rate_option_disabled": "Do not change refresh rate", "dd_refresh_rate_option_manual": "Use manually entered refresh rate", "dd_refresh_rate_option_manual_desc": "Enter the refresh rate to be used", "dd_resolution_option": "Resolution", "dd_resolution_option_auto": "Use resolution provided by the client (default)", "dd_resolution_option_disabled": "Do not change resolution", "dd_resolution_option_manual": "Use manually entered resolution", "dd_resolution_option_manual_desc": "Enter the resolution to be used", "dd_resolution_option_ogs_desc": "\"Optimize game settings\" option must be enabled on the Moonlight client for this to work.", "dd_wa_hdr_toggle_desc": "When using virtual display device as for streaming, it might display incorrect HDR color. With this option enabled, Apollo will try to mitigate this issue.", "dd_wa_hdr_toggle": "Enable high-contrast workaround for HDR", "ds4_back_as_touchpad_click": "Map Back/Select to Touchpad Click", "ds4_back_as_touchpad_click_desc": "When forcing DS4 emulation, map Back/Select to Touchpad Click", "encoder": "Force a Specific Encoder", "encoder_desc": "Force a specific encoder, otherwise Apollo 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.", "encoder_software": "Software", "external_ip": "External IP", "external_ip_desc": "If no external IP address is given, Apollo will automatically detect external IP", "fec_percentage": "FEC Percentage", "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.", "ffmpeg_auto": "auto -- let ffmpeg decide (default)", "file_apps": "Apps File", "file_apps_desc": "The file where current apps of Apollo are stored.", "file_state": "State File", "file_state_desc": "The file where current state of Apollo is stored", "fps": "Advertised FPS", "gamepad": "Emulated Gamepad Type", "gamepad_auto": "Automatic selection options", "gamepad_desc": "Choose which type of gamepad to emulate on the host", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "DS4 selection options", "gamepad_ds5": "DS5 (PS5)", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Manual DS4 options", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Command Preparations", "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.", "hevc_mode": "HEVC Support", "hevc_mode_0": "Apollo will advertise support for HEVC based on encoder capabilities (recommended)", "hevc_mode_1": "Apollo will not advertise support for HEVC", "hevc_mode_2": "Apollo will advertise support for HEVC Main profile", "hevc_mode_3": "Apollo will advertise support for HEVC Main and Main10 (HDR) profiles", "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.", "high_resolution_scrolling": "High Resolution Scrolling Support", "high_resolution_scrolling_desc": "When enabled, Apollo 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.", "install_steam_audio_drivers": "Install Steam Audio Drivers", "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.", "isolated_virtual_display_option": "Move the Virtual Display to the bottom right-most corner of the display layout", "isolated_virtual_display_option_desc": "This makes the display isolated from all other display and contains mouse movements to the virtual screen. This reorganizes the displays such that the all other displays are to the left of the virtual display.", "key_repeat_delay": "Key Repeat Delay", "key_repeat_delay_desc": "Control how fast keys will repeat themselves. The initial delay in milliseconds before repeating keys.", "key_repeat_frequency": "Key Repeat Frequency", "key_repeat_frequency_desc": "How often keys repeat every second. This configurable option supports decimals.", "key_rightalt_to_key_win": "Map Right Alt key to Windows key", "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 Apollo think the Right Alt key is the Windows key", "keyboard": "Enable Keyboard Input", "keyboard_desc": "Allows guests to control the host system with the keyboard", "lan_encryption_mode": "LAN Encryption Mode", "lan_encryption_mode_1": "Enabled for supported clients", "lan_encryption_mode_2": "Required for all clients", "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.", "locale": "Locale", "locale_desc": "The locale used for Apollo's user interface.", "log_level": "Log Level", "log_level_0": "Verbose", "log_level_1": "Debug", "log_level_2": "Info", "log_level_3": "Warning", "log_level_4": "Error", "log_level_5": "Fatal", "log_level_6": "None", "log_level_desc": "The minimum log level printed to standard out", "log_path": "Logfile Path", "log_path_desc": "The file where the current logs of Apollo are stored.", "min_fps_factor": "Minimum FPS Factor", "min_fps_factor_desc": "Apollo will use this factor to calculate the minimum time between frames. Increasing this value slightly may help when streaming mostly static content. Higher values will consume more bandwidth.", "min_threads": "Minimum CPU Thread Count", "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.", "misc": "Miscellaneous options", "motion_as_ds4": "Emulate a DS4 gamepad if the client gamepad reports motion sensors are present", "motion_as_ds4_desc": "If disabled, motion sensors will not be taken into account during gamepad type selection.", "mouse": "Enable Mouse Input", "mouse_desc": "Allows guests to control the host system with the mouse", "native_pen_touch": "Native Pen/Touch Support", "native_pen_touch_desc": "When enabled, Apollo will pass through native pen/touch events from Moonlight clients. This can be useful to disable for older applications without native pen/touch support.", "notify_pre_releases": "PreRelease Notifications", "notify_pre_releases_desc": "Whether to be notified of new pre-release versions of Apollo", "nvenc_h264_cavlc": "Prefer CAVLC over CABAC in H.264", "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.", "nvenc_latency_over_power": "Prefer lower encoding latency over power savings", "nvenc_latency_over_power_desc": "Apollo 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.", "nvenc_opengl_vulkan_on_dxgi": "Present OpenGL/Vulkan on top of DXGI", "nvenc_opengl_vulkan_on_dxgi_desc": "Apollo 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 Apollo program exit.", "nvenc_preset": "Performance preset", "nvenc_preset_1": "(fastest, default)", "nvenc_preset_7": "(slowest)", "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.", "nvenc_realtime_hags": "Use realtime priority in hardware accelerated gpu scheduling", "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.", "nvenc_spatial_aq": "Spatial AQ", "nvenc_spatial_aq_desc": "Assign higher QP values to flat regions of the video. Recommended to enable when streaming at lower bitrates.", "nvenc_spatial_aq_disabled": "Disabled (faster, default)", "nvenc_spatial_aq_enabled": "Enabled (slower)", "nvenc_twopass": "Two-pass mode", "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.", "nvenc_twopass_disabled": "Disabled (fastest, not recommended)", "nvenc_twopass_full_res": "Full resolution (slower)", "nvenc_twopass_quarter_res": "Quarter resolution (faster, default)", "nvenc_vbv_increase": "Single-frame VBV/HRD percentage increase", "nvenc_vbv_increase_desc": "By default Apollo 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.", "origin_web_ui_allowed": "Origin Web UI Allowed", "origin_web_ui_allowed_desc": "The origin of the remote endpoint address that is not denied access to Web UI", "origin_web_ui_allowed_lan": "Only those in LAN may access Web UI", "origin_web_ui_allowed_pc": "Only localhost may access Web UI", "origin_web_ui_allowed_wan": "Anyone may access Web UI", "output_name_desc_unix": "During Apollo startup, you should see the list of detected displays. Note: You need to use the id value inside the parenthesis.", "output_name_desc_windows": "Manually specify a device id 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:", "output_name_unix": "Display number", "output_name_windows": "Display Device Id", "ping_timeout": "Ping Timeout", "ping_timeout_desc": "How long to wait in milliseconds for data from moonlight before shutting down the stream", "pkey": "Private Key", "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.", "port": "Port", "port_alert_1": "Apollo cannot use ports below 1024!", "port_alert_2": "Ports above 65535 are not available!", "port_desc": "Set the family of ports used by Apollo", "port_http_port_note": "Use this port to connect with Moonlight.", "port_note": "Note", "port_port": "Port", "port_protocol": "Protocol", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "Exposing the Web UI to the internet is a security risk! Proceed at your own risk!", "port_web_ui": "Web UI", "qp": "Quantization Parameter", "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.", "qsv_coder": "QuickSync Coder (H264)", "qsv_preset": "QuickSync Preset", "qsv_preset_fast": "faster (lower quality)", "qsv_preset_faster": "fastest (lowest quality)", "qsv_preset_medium": "medium (default)", "qsv_preset_slow": "slow (good quality)", "qsv_preset_slower": "slower (better quality)", "qsv_preset_slowest": "slowest (best quality)", "qsv_preset_veryfast": "fastest (lowest quality)", "qsv_slow_hevc": "Allow Slow HEVC Encoding", "qsv_slow_hevc_desc": "This can enable HEVC encoding on older Intel GPUs, at the cost of higher GPU usage and worse performance.", "res_fps_desc": "The display modes advertised by Apollo. 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.", "resolutions": "Advertised Resolutions", "restart_note": "Apollo is restarting to apply changes.", "sunshine_name": "Apollo Name", "sunshine_name_desc": "The name displayed by Moonlight. If not specified, the PC's hostname is used", "sw_preset": "SW Presets", "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.", "sw_preset_fast": "fast", "sw_preset_faster": "faster", "sw_preset_medium": "medium", "sw_preset_slow": "slow", "sw_preset_slower": "slower", "sw_preset_superfast": "superfast (default)", "sw_preset_ultrafast": "ultrafast", "sw_preset_veryfast": "veryfast", "sw_preset_veryslow": "veryslow", "sw_tune": "SW Tune", "sw_tune_animation": "animation -- good for cartoons; uses higher deblocking and more reference frames", "sw_tune_desc": "Tuning options, which are applied after the preset. Defaults to zerolatency.", "sw_tune_fastdecode": "fastdecode -- allows faster decoding by disabling certain filters", "sw_tune_film": "film -- use for high quality movie content; lowers deblocking", "sw_tune_grain": "grain -- preserves the grain structure in old, grainy film material", "sw_tune_stillimage": "stillimage -- good for slideshow-like content", "sw_tune_zerolatency": "zerolatency -- good for fast encoding and low-latency streaming (default)", "touchpad_as_ds4": "Emulate a DS4 gamepad if the client gamepad reports a touchpad is present", "touchpad_as_ds4_desc": "If disabled, touchpad presence will not be taken into account during gamepad type selection.", "upnp": "UPnP", "upnp_desc": "Automatically configure port forwarding for streaming over the Internet", "vaapi_strict_rc_buffer": "Strictly enforce frame bitrate limits for H.264/HEVC on AMD GPUs", "vaapi_strict_rc_buffer_desc": "Enabling this option can avoid dropped frames over the network during scene changes, but video quality may be reduced during motion.", "virtual_sink": "Virtual Sink", "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!", "virtual_sink_placeholder": "Steam Streaming Speakers", "vt_coder": "VideoToolbox Coder", "vt_realtime": "VideoToolbox Realtime Encoding", "vt_software": "VideoToolbox Software Encoding", "vt_software_allowed": "Allowed", "vt_software_forced": "Forced", "wan_encryption_mode": "WAN Encryption Mode", "wan_encryption_mode_1": "Enabled for supported clients (default)", "wan_encryption_mode_2": "Required for all clients", "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." }, "index": { "description": "Apollo is a self-hosted game stream host for Moonlight.", "download": "Download", "installed_version_not_stable": "You are running a pre-release version of Apollo. You may experience bugs or other issues. Please report any issues you encounter. Thank you for helping to make Apollo a better software!", "loading_latest": "Loading latest release...", "new_pre_release": "A new Pre-Release Version is Available!", "new_stable": "A new Stable Version is Available!", "startup_errors": "Attention! Apollo detected these errors during startup. We STRONGLY RECOMMEND fixing them before streaming.", "version_dirty": "Thank you for helping to make Apollo a better software!", "version_latest": "You are running the latest version of Apollo", "welcome": "Hello, Apollo!" }, "navbar": { "applications": "Applications", "configuration": "Configuration", "home": "Home", "password": "Change Password", "pin": "Pin", "theme_auto": "Auto", "theme_dark": "Dark", "theme_light": "Light", "toggle_theme": "Theme", "troubleshoot": "Troubleshooting" }, "password": { "confirm_password": "Confirm Password", "current_creds": "Current Credentials", "new_creds": "New Credentials", "new_username_desc": "If not specified, the username will not change", "password_change": "Password Change", "success_msg": "Password has been changed successfully! This page will reload soon, your browser will ask you for the new credentials." }, "pin": { "device_name": "Device Name", "pair_failure": "Pairing Failed: Check if the PIN is typed correctly", "pair_success": "Success! Please check Moonlight to continue", "pin_pairing": "PIN Pairing", "send": "Send", "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!" }, "resource_card": { "github_discussions": "GitHub Discussions", "legal": "Legal", "legal_desc": "By continuing to use this software you agree to the terms and conditions in the following documents.", "license": "License", "lizardbyte_website": "LizardByte Website", "resources": "Resources", "resources_desc": "Resources for Apollo!", "third_party_notice": "Third Party Notice" }, "troubleshooting": { "dd_reset": "Reset Persistent Display Device Settings", "dd_reset_desc": "If Apollo is stuck trying to restore the changed display device settings, you can reset the settings and proceed to restore the display state manually.", "dd_reset_error": "Error while resetting persistence!", "dd_reset_success": "Success resetting persistence!", "force_close": "Force Close", "force_close_desc": "If Moonlight complains about an app currently running, force closing the app should fix the issue.", "force_close_error": "Error while closing Application", "force_close_success": "Application Closed Successfully!", "logs": "Logs", "logs_desc": "See the logs uploaded by Apollo", "logs_find": "Find...", "restart_apollo": "Restart Apollo", "restart_apollo_desc": "If Apollo isn't working properly, you can try restarting it. This will terminate any running sessions.", "restart_apollo_success": "Apollo is restarting", "troubleshooting": "Troubleshooting", "unpair_all": "Unpair All", "unpair_all_error": "Error while unpairing", "unpair_all_success": "All devices unpaired.", "unpair_desc": "Remove your paired devices. Individually unpaired devices with an active session will remain connected, but cannot start or resume a session.", "unpair_single_no_devices": "There are no paired devices.", "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.", "unpair_single_unknown": "Unknown Client", "unpair_title": "Unpair Devices" }, "welcome": { "confirm_password": "Confirm password", "create_creds": "Before Getting Started, we need you to make a new username and password for accessing the Web UI.", "create_creds_alert": "The credentials below are needed to access Apollo's Web UI. Keep them safe, since you will never see them again!", "greeting": "Welcome to Apollo!", "login": "Login", "welcome_success": "This page will reload soon, your browser will ask you for the new credentials" } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/en_US.json ================================================ { "_common": { "apply": "Apply", "auto": "Automatic", "autodetect": "Autodetect (recommended)", "beta": "(beta)", "cancel": "Cancel", "disabled": "Disabled", "disabled_def": "Disabled (default)", "disabled_def_cbox": "Default: unchecked", "dismiss": "Dismiss", "do_cmd": "Do Command", "elevated": "Elevated", "enabled": "Enabled", "enabled_def": "Enabled (default)", "enabled_def_cbox": "Default: checked", "error": "Error!", "note": "Note:", "password": "Password", "run_as": "Run as Admin", "save": "Save", "see_more": "See More", "success": "Success!", "undo_cmd": "Undo Command", "username": "Username", "warning": "Warning!" }, "apps": { "actions": "Actions", "add_cmds": "Add Commands", "add_new": "Add New", "app_name": "Application Name", "app_name_desc": "Application Name, as shown on Moonlight", "applications_desc": "Applications are refreshed only when Client is restarted", "applications_title": "Applications", "auto_detach": "Continue streaming if the application exits quickly", "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.", "cmd": "Command", "cmd_desc": "The main application to start. If blank, no application will be started.", "cmd_note": "If the path to the command executable contains spaces, you must enclose it in quotes.", "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.", "cmd_prep_name": "Command Preparations", "covers_found": "Covers Found", "delete": "Delete", "detached_cmds": "Detached Commands", "detached_cmds_add": "Add Detached Command", "detached_cmds_desc": "A list of commands to be run in the background.", "detached_cmds_note": "If the path to the command executable contains spaces, you must enclose it in quotes.", "edit": "Edit", "env_app_id": "App ID", "env_app_name": "App Name", "env_client_audio_config": "The Audio Configuration requested by the client (2.0/5.1/7.1)", "env_client_enable_sops": "The client has requested the option to optimize the game for optimal streaming (true/false)", "env_client_fps": "The FPS requested by the client (float)", "env_client_gcmap": "The requested gamepad mask, in a bitset/bitfield format (int)", "env_client_hdr": "HDR is enabled by the client (true/false)", "env_client_height": "The Height requested by the client (int)", "env_client_host_audio": "The client has requested host audio (true/false)", "env_client_width": "The Width requested by the client (int)", "env_displayplacer_example": "Example - displayplacer for Resolution Automation:", "env_qres_example": "Example - QRes for Resolution Automation:", "env_qres_path": "qres path", "env_var_name": "Var Name", "env_vars_about": "About Environment Variables", "env_vars_desc": "All commands get these environment variables by default:", "env_xrandr_example": "Example - Xrandr for Resolution Automation:", "exit_timeout": "Exit Timeout", "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.", "find_cover": "Find Cover", "global_prep_desc": "Enable/Disable the execution of Global Prep Commands for this application.", "global_prep_name": "Global Prep Commands", "image": "Image", "image_desc": "Application icon/picture/image path that will be sent to client. Image must be a PNG file. If not set, Apollo will send default box image.", "loading": "Loading...", "name": "Name", "output_desc": "The file where the output of the command is stored, if it is not specified, the output is ignored", "output_name": "Output", "run_as_desc": "This can be necessary for some applications that require administrator permissions to run properly.", "wait_all": "Continue streaming until all app processes exit", "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.", "working_dir": "Working Directory", "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, Apollo will default to the parent directory of the command" }, "config": { "adapter_name": "Adapter Name", "adapter_name_desc_linux_1": "Manually specify a GPU to use for capture.", "adapter_name_desc_linux_2": "to find all devices capable of VAAPI", "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 Apollo, it needs to have at the very minimum:", "adapter_name_desc_windows": "Manually specify a GPU to use for capture. If unset, the GPU is chosen automatically. We strongly recommend leaving this field blank to use automatic GPU selection! Note: This GPU must have a display connected and powered on. The appropriate values can be found using the following command:", "adapter_name_placeholder_windows": "Radeon RX 580 Series", "add": "Add", "address_family": "Address Family", "address_family_both": "IPv4+IPv6", "address_family_desc": "Set the address family used by Apollo", "address_family_ipv4": "IPv4 only", "always_send_scancodes": "Always Send Scancodes", "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.", "amd_coder": "AMF Coder (H264)", "amd_coder_desc": "Allows you to select the entropy encoding to prioritize quality or encoding speed. H.264 only.", "amd_enforce_hrd": "AMF Hypothetical Reference Decoder (HRD) Enforcement", "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.", "amd_preanalysis": "AMF Preanalysis", "amd_preanalysis_desc": "This enables rate-control preanalysis, which may increase quality at the expense of increased encoding latency.", "amd_quality": "AMF Quality", "amd_quality_balanced": "balanced -- balanced (default)", "amd_quality_desc": "This controls the balance between encoding speed and quality.", "amd_quality_group": "AMF Quality Settings", "amd_quality_quality": "quality -- prefer quality", "amd_quality_speed": "speed -- prefer speed", "amd_rc": "AMF Rate Control", "amd_rc_cbr": "cbr -- constant bitrate (recommended if HRD is enabled)", "amd_rc_cqp": "cqp -- constant qp mode", "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.", "amd_rc_group": "AMF Rate Control Settings", "amd_rc_vbr_latency": "vbr_latency -- latency constrained variable bitrate (recommended if HRD is disabled; default)", "amd_rc_vbr_peak": "vbr_peak -- peak constrained variable bitrate", "amd_usage": "AMF Usage", "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.", "amd_usage_lowlatency": "lowlatency - low latency (fastest)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - low latency, high quality (fast)", "amd_usage_transcoding": "transcoding -- transcoding (slowest)", "amd_usage_ultralowlatency": "ultralowlatency - ultra low latency (fastest; default)", "amd_usage_webcam": "webcam -- webcam (slow)", "amd_vbaq": "AMF Variance Based Adaptive Quantization (VBAQ)", "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.", "apply_note": "Click 'Apply' to restart Apollo and apply changes. This will terminate any running sessions.", "audio_sink": "Audio Sink", "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:", "audio_sink_desc_macos": "The name of the audio sink used for Audio Loopback. Apollo can only access microphones on macOS due to system limitations. To stream system audio using Soundflower or BlackHole.", "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:", "audio_sink_placeholder_macos": "BlackHole 2ch", "audio_sink_placeholder_windows": "Speakers (High Definition Audio Device)", "av1_mode": "AV1 Support", "av1_mode_0": "Apollo will advertise support for AV1 based on encoder capabilities (recommended)", "av1_mode_1": "Apollo will not advertise support for AV1", "av1_mode_2": "Apollo will advertise support for AV1 Main 8-bit profile", "av1_mode_3": "Apollo will advertise support for AV1 Main 8-bit and 10-bit (HDR) profiles", "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.", "back_button_timeout": "Home/Guide Button Emulation Timeout", "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.", "capture": "Force a Specific Capture Method", "capture_desc": "On automatic mode Apollo will use the first one that works. NvFBC requires patched nvidia drivers.", "cert": "Certificate", "cert_desc": "The certificate used for the web UI and Moonlight client pairing. For best compatibility, this should have an RSA-2048 public key.", "channels": "Maximum Connected Clients", "channels_desc_1": "Apollo can allow a single streaming session to be shared with multiple clients simultaneously.", "channels_desc_2": "Some hardware encoders may have limitations that reduce performance with multiple streams.", "coder_cabac": "cabac -- context adaptive binary arithmetic coding - higher quality", "coder_cavlc": "cavlc -- context adaptive variable-length coding - faster decode", "configuration": "Configuration", "controller": "Enable Gamepad Input", "controller_desc": "Allows guests to control the host system with a gamepad / controller", "credentials_file": "Credentials File", "credentials_file_desc": "Store Username/Password separately from Apollo's state file.", "dd_config_ensure_active": "Activate the display automatically", "dd_config_ensure_only_display": "Deactivate other displays and activate only the specified display", "dd_config_ensure_primary": "Activate the display automatically and make it a primary display", "dd_config_label": "Device configuration", "dd_config_revert_delay": "Config revert delay", "dd_config_revert_delay_desc": "Additional delay in milliseconds to wait before reverting configuration when the app has been closed or the last session terminated. Main purpose is to provide a smoother transition when quickly switching between apps.", "dd_config_verify_only": "Verify that the display is enabled", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "Switch on/off the HDR mode as requested by the client (default)", "dd_hdr_option_disabled": "Do not change HDR settings", "dd_mode_remapping": "Display mode remapping", "dd_mode_remapping_add": "Add remapping entry", "dd_mode_remapping_desc_1": "Specify remapping entries to change the requested resolution and/or refresh rate to other values.", "dd_mode_remapping_desc_2": "The list is iterated from top to bottom and the first match is used.", "dd_mode_remapping_desc_3": "\"Requested\" fields can be left empty to match any requested value.", "dd_mode_remapping_desc_4_final_values_mixed": "At least one \"Final\" field must be specified. The unspecified resolution or refresh rate will not be changed.", "dd_mode_remapping_desc_4_final_values_non_mixed": "\"Final\" field must be specified and cannot be empty.", "dd_mode_remapping_desc_5_sops_mixed_only": "\"Optimize game settings\" option must be enabled in the Moonlight client, otherwise entries with any resolution fields specified are skipped.", "dd_mode_remapping_desc_5_sops_resolution_only": "\"Optimize game settings\" option must be enabled in the Moonlight client, otherwise the mapping is skipped.", "dd_mode_remapping_final_refresh_rate": "Final refresh rate", "dd_mode_remapping_final_resolution": "Final resolution", "dd_mode_remapping_requested_fps": "Requested FPS", "dd_mode_remapping_requested_resolution": "Requested resolution", "dd_options_header": "Advanced display device options", "dd_refresh_rate_option": "Refresh rate", "dd_refresh_rate_option_auto": "Use FPS value provided by the client (default)", "dd_refresh_rate_option_disabled": "Do not change refresh rate", "dd_refresh_rate_option_manual": "Use manually entered refresh rate", "dd_refresh_rate_option_manual_desc": "Enter the refresh rate to be used", "dd_resolution_option": "Resolution", "dd_resolution_option_auto": "Use resolution provided by the client (default)", "dd_resolution_option_disabled": "Do not change resolution", "dd_resolution_option_manual": "Use manually entered resolution", "dd_resolution_option_manual_desc": "Enter the resolution to be used", "dd_resolution_option_ogs_desc": "\"Optimize game settings\" option must be enabled on the Moonlight client for this to work.", "dd_wa_hdr_toggle_desc": "When using virtual display device as for streaming, it might display incorrect HDR color. With this option enabled, Apollo will try to mitigate this issue.", "dd_wa_hdr_toggle": "Enable high-contrast workaround for HDR", "ds4_back_as_touchpad_click": "Map Back/Select to Touchpad Click", "ds4_back_as_touchpad_click_desc": "When forcing DS4 emulation, map Back/Select to Touchpad Click", "encoder": "Force a Specific Encoder", "encoder_desc": "Force a specific encoder, otherwise Apollo 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.", "encoder_software": "Software", "external_ip": "External IP", "external_ip_desc": "If no external IP address is given, Apollo will automatically detect external IP", "fec_percentage": "FEC Percentage", "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.", "ffmpeg_auto": "auto -- let ffmpeg decide (default)", "file_apps": "Apps File", "file_apps_desc": "The file where current apps of Apollo are stored.", "file_state": "State File", "file_state_desc": "The file where current state of Apollo is stored", "gamepad": "Emulated Gamepad Type", "gamepad_auto": "Automatic selection options", "gamepad_desc": "Choose which type of gamepad to emulate on the host", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "DS4 selection options", "gamepad_ds5": "DS5 (PS5)", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Manual DS4 options", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Command Preparations", "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.", "hevc_mode": "HEVC Support", "hevc_mode_0": "Apollo will advertise support for HEVC based on encoder capabilities (recommended)", "hevc_mode_1": "Apollo will not advertise support for HEVC", "hevc_mode_2": "Apollo will advertise support for HEVC Main profile", "hevc_mode_3": "Apollo will advertise support for HEVC Main and Main10 (HDR) profiles", "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.", "high_resolution_scrolling": "High Resolution Scrolling Support", "high_resolution_scrolling_desc": "When enabled, Apollo 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.", "install_steam_audio_drivers": "Install Steam Audio Drivers", "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.", "isolated_virtual_display_option": "Move the Virtual Display to the bottom right-most corner of the display layout", "isolated_virtual_display_option_desc": "This makes the display isolated from all other display and contains mouse movements to the virtual screen. This reorganizes the displays such that the all other displays are to the left of the virtual display.", "key_repeat_delay": "Key Repeat Delay", "key_repeat_delay_desc": "Control how fast keys will repeat themselves. The initial delay in milliseconds before repeating keys.", "key_repeat_frequency": "Key Repeat Frequency", "key_repeat_frequency_desc": "How often keys repeat every second. This configurable option supports decimals.", "key_rightalt_to_key_win": "Map Right Alt key to Windows key", "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 Apollo think the Right Alt key is the Windows key", "keyboard": "Enable Keyboard Input", "keyboard_desc": "Allows guests to control the host system with the keyboard", "lan_encryption_mode": "LAN Encryption Mode", "lan_encryption_mode_1": "Enabled for supported clients", "lan_encryption_mode_2": "Required for all clients", "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.", "locale": "Locale", "locale_desc": "The locale used for Apollo's user interface.", "log_level": "Log Level", "log_level_0": "Verbose", "log_level_1": "Debug", "log_level_2": "Info", "log_level_3": "Warning", "log_level_4": "Error", "log_level_5": "Fatal", "log_level_6": "None", "log_level_desc": "The minimum log level printed to standard out", "log_path": "Logfile Path", "log_path_desc": "The file where the current logs of Apollo are stored.", "min_fps_factor": "Minimum FPS Factor", "min_fps_factor_desc": "Apollo will use this factor to calculate the minimum time between frames. Increasing this value slightly may help when streaming mostly static content. Higher values will consume more bandwidth.", "min_threads": "Minimum CPU Thread Count", "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.", "misc": "Miscellaneous options", "motion_as_ds4": "Emulate a DS4 gamepad if the client gamepad reports motion sensors are present", "motion_as_ds4_desc": "If disabled, motion sensors will not be taken into account during gamepad type selection.", "mouse": "Enable Mouse Input", "mouse_desc": "Allows guests to control the host system with the mouse", "native_pen_touch": "Native Pen/Touch Support", "native_pen_touch_desc": "When enabled, Apollo will pass through native pen/touch events from Moonlight clients. This can be useful to disable for older applications without native pen/touch support.", "notify_pre_releases": "PreRelease Notifications", "notify_pre_releases_desc": "Whether to be notified of new pre-release versions of Apollo", "nvenc_h264_cavlc": "Prefer CAVLC over CABAC in H.264", "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.", "nvenc_latency_over_power": "Prefer lower encoding latency over power savings", "nvenc_latency_over_power_desc": "Apollo 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.", "nvenc_opengl_vulkan_on_dxgi": "Present OpenGL/Vulkan on top of DXGI", "nvenc_opengl_vulkan_on_dxgi_desc": "Apollo 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 Apollo program exit.", "nvenc_preset": "Performance preset", "nvenc_preset_1": "(fastest, default)", "nvenc_preset_7": "(slowest)", "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.", "nvenc_realtime_hags": "Use realtime priority in hardware accelerated gpu scheduling", "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.", "nvenc_spatial_aq": "Spatial AQ", "nvenc_spatial_aq_desc": "Assign higher QP values to flat regions of the video. Recommended to enable when streaming at lower bitrates.", "nvenc_spatial_aq_disabled": "Disabled (faster, default)", "nvenc_spatial_aq_enabled": "Enabled (slower)", "nvenc_twopass": "Two-pass mode", "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.", "nvenc_twopass_disabled": "Disabled (fastest, not recommended)", "nvenc_twopass_full_res": "Full resolution (slower)", "nvenc_twopass_quarter_res": "Quarter resolution (faster, default)", "nvenc_vbv_increase": "Single-frame VBV/HRD percentage increase", "nvenc_vbv_increase_desc": "By default Apollo 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.", "origin_web_ui_allowed": "Origin Web UI Allowed", "origin_web_ui_allowed_desc": "The origin of the remote endpoint address that is not denied access to Web UI", "origin_web_ui_allowed_lan": "Only those in LAN may access Web UI", "origin_web_ui_allowed_pc": "Only localhost may access Web UI", "origin_web_ui_allowed_wan": "Anyone may access Web UI", "output_name_desc_unix": "During Apollo startup, you should see the list of detected displays when Headless Mode isn't enabled. 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.", "output_name_desc_windows": "Manually specify a device id 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:", "output_name_unix": "Display number", "output_name_windows": "Display Device Id", "ping_timeout": "Ping Timeout", "ping_timeout_desc": "How long to wait in milliseconds for data from moonlight before shutting down the stream", "pkey": "Private Key", "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.", "port": "Port", "port_alert_1": "Apollo cannot use ports below 1024!", "port_alert_2": "Ports above 65535 are not available!", "port_desc": "Set the family of ports used by Apollo", "port_http_port_note": "Use this port to connect with Moonlight.", "port_note": "Note", "port_port": "Port", "port_protocol": "Protocol", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "Exposing the Web UI to the internet is a security risk! Proceed at your own risk!", "port_web_ui": "Web UI", "qp": "Quantization Parameter", "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.", "qsv_coder": "QuickSync Coder (H264)", "qsv_preset": "QuickSync Preset", "qsv_preset_fast": "fast (low quality)", "qsv_preset_faster": "faster (lower quality)", "qsv_preset_medium": "medium (default)", "qsv_preset_slow": "slow (good quality)", "qsv_preset_slower": "slower (better quality)", "qsv_preset_slowest": "slowest (best quality)", "qsv_preset_veryfast": "fastest (lowest quality)", "qsv_slow_hevc": "Allow Slow HEVC Encoding", "qsv_slow_hevc_desc": "This can enable HEVC encoding on older Intel GPUs, at the cost of higher GPU usage and worse performance.", "restart_note": "Apollo is restarting to apply changes.", "sunshine_name": "Apollo Name", "sunshine_name_desc": "The name displayed by Moonlight. If not specified, the PC's hostname is used", "sw_preset": "SW Presets", "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.", "sw_preset_fast": "fast", "sw_preset_faster": "faster", "sw_preset_medium": "medium", "sw_preset_slow": "slow", "sw_preset_slower": "slower", "sw_preset_superfast": "superfast (default)", "sw_preset_ultrafast": "ultrafast", "sw_preset_veryfast": "veryfast", "sw_preset_veryslow": "veryslow", "sw_tune": "SW Tune", "sw_tune_animation": "animation -- good for cartoons; uses higher deblocking and more reference frames", "sw_tune_desc": "Tuning options, which are applied after the preset. Defaults to zerolatency.", "sw_tune_fastdecode": "fastdecode -- allows faster decoding by disabling certain filters", "sw_tune_film": "film -- use for high quality movie content; lowers deblocking", "sw_tune_grain": "grain -- preserves the grain structure in old, grainy film material", "sw_tune_stillimage": "stillimage -- good for slideshow-like content", "sw_tune_zerolatency": "zerolatency -- good for fast encoding and low-latency streaming (default)", "touchpad_as_ds4": "Emulate a DS4 gamepad if the client gamepad reports a touchpad is present", "touchpad_as_ds4_desc": "If disabled, touchpad presence will not be taken into account during gamepad type selection.", "upnp": "UPnP", "upnp_desc": "Automatically configure port forwarding for streaming over the Internet", "vaapi_strict_rc_buffer": "Strictly enforce frame bitrate limits for H.264/HEVC on AMD GPUs", "vaapi_strict_rc_buffer_desc": "Enabling this option can avoid dropped frames over the network during scene changes, but video quality may be reduced during motion.", "virtual_sink": "Virtual Sink", "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!", "virtual_sink_placeholder": "Steam Streaming Speakers", "vt_coder": "VideoToolbox Coder", "vt_realtime": "VideoToolbox Realtime Encoding", "vt_software": "VideoToolbox Software Encoding", "vt_software_allowed": "Allowed", "vt_software_forced": "Forced", "wan_encryption_mode": "WAN Encryption Mode", "wan_encryption_mode_1": "Enabled for supported clients (default)", "wan_encryption_mode_2": "Required for all clients", "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." }, "index": { "description": "Apollo is a self-hosted game stream host for Moonlight.", "download": "Download", "installed_version_not_stable": "You are running a pre-release version of Apollo. You may experience bugs or other issues. Please report any issues you encounter. Thank you for helping to make Apollo a better software!", "loading_latest": "Loading latest release...", "new_pre_release": "A new Pre-Release Version is Available!", "new_stable": "A new Stable Version is Available!", "startup_errors": "Attention! Apollo detected these errors during startup. We STRONGLY RECOMMEND fixing them before streaming.", "version_dirty": "Thank you for helping to make Apollo a better software!", "version_latest": "You are running the latest version of Apollo", "welcome": "Hello, Apollo!" }, "navbar": { "applications": "Applications", "configuration": "Configuration", "home": "Home", "password": "Change Password", "pin": "Pin", "theme_auto": "Auto", "theme_dark": "Dark", "theme_light": "Light", "toggle_theme": "Theme", "troubleshoot": "Troubleshooting" }, "password": { "confirm_password": "Confirm Password", "current_creds": "Current Credentials", "new_creds": "New Credentials", "new_username_desc": "If not specified, the username will not change", "password_change": "Password Change", "success_msg": "Password has been changed successfully! This page will reload soon, your browser will ask you for the new credentials." }, "pin": { "device_name": "Device Name", "pair_failure": "Pairing Failed: Check if the PIN is typed correctly", "pair_success": "Success! Please check Moonlight to continue", "pin_pairing": "PIN Pairing", "send": "Send", "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!" }, "resource_card": { "github_discussions": "GitHub Discussions", "legal": "Legal", "legal_desc": "By continuing to use this software you agree to the terms and conditions in the following documents.", "license": "License", "lizardbyte_website": "LizardByte Website", "resources": "Resources", "resources_desc": "Resources for Apollo!", "third_party_notice": "Third Party Notice" }, "troubleshooting": { "dd_reset": "Reset Persistent Display Device Settings", "dd_reset_desc": "If Apollo is stuck trying to restore the changed display device settings, you can reset the settings and proceed to restore the display state manually.", "dd_reset_error": "Error while resetting persistence!", "dd_reset_success": "Success resetting persistence!", "force_close": "Force Close", "force_close_desc": "If Moonlight complains about an app currently running, force closing the app should fix the issue.", "force_close_error": "Error while closing Application", "force_close_success": "Application Closed Successfully!", "logs": "Logs", "logs_desc": "See the logs uploaded by Apollo", "logs_find": "Find...", "restart_apollo": "Restart Apollo", "restart_apollo_desc": "If Apollo isn't working properly, you can try restarting it. This will terminate any running sessions.", "restart_apollo_success": "Apollo is restarting", "troubleshooting": "Troubleshooting", "unpair_all": "Unpair All", "unpair_all_error": "Error while unpairing", "unpair_all_success": "All devices unpaired.", "unpair_desc": "Remove your paired devices. Individually unpaired devices with an active session will remain connected, but cannot start or resume a session.", "unpair_single_no_devices": "There are no paired devices.", "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.", "unpair_single_unknown": "Unknown Client", "unpair_title": "Unpair Devices" }, "welcome": { "confirm_password": "Confirm password", "create_creds": "Before Getting Started, we need you to make a new username and password for accessing the Web UI.", "create_creds_alert": "The credentials below are needed to access Apollo's Web UI. Keep them safe, since you will never see them again!", "greeting": "Welcome to Apollo!", "login": "Login", "welcome_success": "This page will reload soon, your browser will ask you for the new credentials" } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/es.json ================================================ { "_common": { "apply": "Aplicar", "auto": "Automático", "autodetect": "Autodetectar (recomendado)", "beta": "(beta)", "cancel": "Cancelar", "cmd_name": "Nombre del comando", "cmd_val": "Valor del comando", "default": "Por defecto", "default_global": "Por defecto (Global)", "disabled": "Deshabilitado", "disabled_def": "Desactivado (por defecto)", "disabled_def_cbox": "Por defecto: sin marcar", "dismiss": "Descartar", "do_cmd": "Hacer comando", "elevated": "Elevado", "enabled": "Habilitado", "enabled_def": "Habilitado (por defecto)", "enabled_def_cbox": "Por defecto: habilitado", "error": "¡Error!", "learn_more": "Aprender más", "note": "Nota:", "password": "Contraseña", "run_as": "Ejecutar como administrador", "save": "Guardar", "see_more": "Ver más", "success": "¡Éxito!", "undo_cmd": "Deshacer comando", "username": "Nombre de usuario", "warning": "¡Advertencia!" }, "apps": { "actions": "Acciones", "add_cmds": "Añadir comandos", "add_new": "Añadir nuevo", "app_name": "Nombre de la aplicación", "app_name_desc": "Nombre de la aplicación, como se muestra en Moonlight", "applications_desc": "Las aplicaciones se actualizan sólo cuando se reinicia Client", "applications_title": "Aplicaciones", "auto_detach": "Continuar la transmisión si la aplicación cierra rápidamente", "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.", "cmd": "Comando", "cmd_desc": "La aplicación principal a iniciar. Si está en blanco, no se iniciará ninguna aplicación.", "cmd_note": "Si la ruta al comando ejecutable contiene espacios, debe encerrarla entre comillas.", "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.", "cmd_prep_name": "Preparaciones de Comando", "cmd_state_desc": "Una lista de comandos que ejecutar cuando se restaure (primer cliente se conecta sin haber ninguno más) o se pause(todos los clientes se desconectan) esta aplicaión.\nHacer comando para restaurar y Deshacer comando para pausar.\nAsegúrate de limpiar cualquier efecto de los comandos de preparación en Deshacer comando.\nTen en cuenta que los comandos de pausa no se ejecutarán al terminar una sesión.", "cmd_state_name": "Comandos de Pausa/Restaurar", "covers_found": "Portadas encontradas", "delete": "Eliminar", "detached_cmds": "Comandos separados", "detached_cmds_add": "Añadir comando separado", "detached_cmds_desc": "Una lista de comandos a ejecutar en segundo plano.", "detached_cmds_note": "Si la ruta al comando ejecutable contiene espacios, debe encerrarla entre comillas.", "edit": "Editar", "env_app_id": "ID de la aplicación", "env_app_name": "Nombre de la aplicación", "env_client_audio_config": "La configuración de audio solicitada por el cliente (2.0/5.1/7.1)", "env_client_enable_sops": "El cliente ha solicitado la opción de optimizar el juego para una transmisión óptima (verdadero/falso)", "env_client_fps": "El FPS solicitado por el cliente (float)", "env_client_gcmap": "La máscara de gamepad solicitada, en formato bitset/bitfield (int)", "env_client_hdr": "HDR está activado por el cliente (verdadero/falso)", "env_client_height": "La altura solicitada por el cliente (int)", "env_client_host_audio": "El cliente ha solicitado audio del host (verdadero/falso)", "env_client_width": "Anchura solicitada por el cliente (int)", "env_displayplacer_example": "Ejemplo - displayplacer para Automatización de Resoluciones:", "env_qres_example": "Ejemplo - QRes para Automatización de Resoluciones:", "env_qres_path": "ruta de qres", "env_var_name": "Nombre Var", "env_vars_about": "Acerca de variables de entorno", "env_vars_desc": "Todos los comandos obtienen estas variables de entorno de forma predeterminada:", "env_xrandr_example": "Ejemplo - Xrandr para Automatización de Resolución:", "exit_timeout": "Tiempo de espera de salida", "exit_timeout_desc": "Número de segundos a esperar a que todos los procesos de la aplicación salgan con gracia cuando se les solicite salir. Si no se activa, el valor predeterminado es esperar hasta 5 segundos. Si se establece a cero o un valor negativo, la aplicación se cerrará inmediatamente.", "find_cover": "Encontrar portada", "global_prep_desc": "Activar/Desactivar la ejecución de Comandos de Preparación Global para esta aplicación.", "global_prep_name": "Comandos de preparación global", "image": "Imagen", "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, Apollo enviará la imagen predeterminada de la caja.", "loading": "Cargando...", "name": "Nombre", "output_desc": "El archivo donde se almacena la salida del comando, si no se especifica, se ignora la salida", "output_name": "Salida", "run_as_desc": "Esto puede ser necesario para que algunas aplicaciones que requieren permisos de administrador, funcionen correctamente.", "wait_all": "Continuar la transmisión hasta que todos los procesos de la aplicación salgan", "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.", "working_dir": "Directorio de trabajo", "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, Apollo se establecerá por defecto en el directorio padre del comando" }, "config": { "adapter_name": "Nombre del adaptador", "adapter_name_desc_linux_1": "Especifique manualmente un GPU a usar para capturar.", "adapter_name_desc_linux_2": "para encontrar todos los dispositivos capaces de VAAPI", "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 Apollo, necesita tener como mínimo:", "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:", "adapter_name_placeholder_windows": "Serie RX 580 de Radeon", "add": "Añadir", "address_family": "Familia de dirección", "address_family_both": "IPv4+IPv6", "address_family_desc": "Establecer la familia de direcciones usada por Apollo", "address_family_ipv4": "Sólo IPv4", "always_send_scancodes": "Enviar siempre códigos de escaneo", "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 teclas del cliente están generando una entrada incorrecta en el host.", "amd_coder": "Codificador AMF (H264)", "amd_coder_desc": "Le permite seleccionar la codificación entropía para priorizar la calidad o la velocidad de codificación. H.264 solamente.", "amd_enforce_hrd": "Aplicación del decodificador de referencia hipotético (HRD) AMF", "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.", "amd_preanalysis": "Análisis previo de AMF", "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.", "amd_quality": "Calidad AMF", "amd_quality_balanced": "balanceada -- balanceado (por defecto)", "amd_quality_desc": "Controla el equilibrio entre la velocidad de codificación y la calidad.", "amd_quality_group": "Ajustes de calidad AMF", "amd_quality_quality": "calidad -- Preferir calidad", "amd_quality_speed": "velocidad -- preferir velocidad", "amd_rc": "Control de tasa AMF", "amd_rc_cbr": "cbr -- velocidad de bits constante (recomendada si HRD está habilitado)", "amd_rc_cqp": "cqp -- modo qp constante", "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.", "amd_rc_group": "Ajustes de control de tasa AMF", "amd_rc_vbr_latency": "vbr_latency -- tasa de bits variable restringida por latencia (por defecto)", "amd_rc_vbr_peak": "vbr_peak -- tasa de bits variable restringida máxima", "amd_usage": "Uso de AMF", "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.", "amd_usage_lowlatency": "baja latencia - baja latencia (la más rápida)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - baja latencia, alta calidad (rápido)", "amd_usage_transcoding": "transcodificación -- transcodificación (más lenta)", "amd_usage_ultralowlatency": "latencia ultra baja - latencia ultra baja (más rápida)", "amd_usage_webcam": "cámara web -- cámara web (lento)", "amd_vbaq": "Cuantización adaptativa basada en la varianza AMF (VBAQ)", "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.", "apply_note": "Haga clic en 'Aplicar' para reiniciar Apollo y aplicar los cambios. Esto terminará cualquier sesión en ejecución.", "audio_sink": "Salida de audio", "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:", "audio_sink_desc_macos": "El nombre de la salida de audio usado para Audio Loopback. Apollo sólo puede acceder a micrófonos en macOS debido a limitaciones del sistema. Para transmitir audio del sistema usando Soundflower o BlackHole.", "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:", "audio_sink_placeholder_macos": "BlackHole 2ch", "audio_sink_placeholder_windows": "Altavoces (Dispositivo de audio de alta definición)", "av1_mode": "Soporte AV1", "av1_mode_0": "Apollo anunciará soporte para AV1 basado en las capacidades del codificador (recomendado)", "av1_mode_1": "Apollo no anunciará soporte para AV1", "av1_mode_2": "Apollo anunciará soporte para el perfil principal de 8 bits AV1", "av1_mode_3": "Apollo anunciará soporte para perfiles AV1 de 8-bit y 10-bit (HDR)", "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.", "back_button_timeout": "Tiempo de Emulación de Botón de Inicio/Guía", "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.", "capture": "Forzar un método de captura específico", "capture_desc": "En modo automático Apollo usará el primero que funcione.", "cert": "Certificado", "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.", "channels": "Máximo de clientes conectados", "channels_desc_1": "Apollo puede permitir que una sola sesión de transmisión sea compartida con varios clientes simultáneamente.", "channels_desc_2": "Algunos codificadores de hardware pueden tener limitaciones que reducen el rendimiento con múltiples secuencias.", "coder_cabac": "cabac -- codificación aritmética binaria adaptativa contextual - mayor calidad", "coder_cavlc": "cavlc -- codificación de longitud variable adaptativa de contexto - decodificación más rápida", "configuration": "Configuración", "controller": "Activar entrada de Gamepad", "controller_desc": "Permite a los huéspedes controlar el sistema de host con un gamepad / controlador", "credentials_file": "Archivo de credenciales", "credentials_file_desc": "Guardar nombre de usuario/contraseña por separado del archivo de estado de Apollo.", "dd_config_ensure_active": "Activar la pantalla automáticamente", "dd_config_ensure_only_display": "Desactivar otras pantallas y activar sólo la pantalla especificada", "dd_config_ensure_primary": "Activar la pantalla automáticamente y convertirla en una pantalla principal", "dd_config_label": "Configuración del dispositivo", "dd_config_revert_delay": "Retraso de configuración", "dd_config_revert_delay_desc": "Retraso adicional en milisegundos a esperar antes de revertir la configuración cuando la aplicación ha sido cerrada o la última sesión ha terminado. El propósito principal es proporcionar una transición más suave cuando cambia rápidamente entre aplicaciones.", "dd_config_verify_only": "Verificar que la pantalla está habilitada", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "Encender/apagar el modo HDR tal como lo solicitó el cliente (por defecto)", "dd_hdr_option_disabled": "No cambiar la configuración de HDR", "dd_mode_remapping": "Modo de visualización de mapeo", "dd_mode_remapping_add": "Añadir entrada de remplazamiento", "dd_mode_remapping_desc_1": "Especifique las entradas de remplazamiento para cambiar la resolución solicitada y/o la tasa de actualización a otros valores.", "dd_mode_remapping_desc_2": "La lista está iterada de arriba a abajo y se utiliza la primera partida.", "dd_mode_remapping_desc_3": "Los campos \"solicitados\" se pueden dejar vacíos para que coincidan con cualquier valor solicitado.", "dd_mode_remapping_desc_4_final_values_mixed": "Al menos un campo \"Final\" debe ser especificado. La resolución no especificada o la tasa de actualización no se cambiará.", "dd_mode_remapping_desc_4_final_values_non_mixed": "El campo \"Final\" debe ser especificado y no puede estar vacío.", "dd_mode_remapping_desc_5_sops_mixed_only": "La opción \"Optimizar ajustes del juego\" debe estar habilitada en el cliente de luz lunar, de lo contrario se omiten las entradas con cualquier campo de resolución especificado.", "dd_mode_remapping_desc_5_sops_resolution_only": "La opción \"Optimizar ajustes del juego\" debe estar habilitada en el cliente de luz lunar, de lo contrario el mapeo se omitirá.", "dd_mode_remapping_final_refresh_rate": "Tasa de actualización final", "dd_mode_remapping_final_resolution": "Resolución final", "dd_mode_remapping_requested_fps": "FPS solicitado", "dd_mode_remapping_requested_resolution": "Resolución solicitada", "dd_options_header": "Opciones avanzadas de dispositivo de pantalla", "dd_refresh_rate_option": "Tasa de actualización", "dd_refresh_rate_option_auto": "Usar valor FPS proporcionado por el cliente (por defecto)", "dd_refresh_rate_option_disabled": "No cambiar la tasa de actualización", "dd_refresh_rate_option_manual": "Usar manualmente la tasa de actualización introducida", "dd_refresh_rate_option_manual_desc": "Introduzca la tasa de actualización a utilizar", "dd_resolution_option": "Resolución", "dd_resolution_option_auto": "Usar resolución proporcionada por el cliente (por defecto)", "dd_resolution_option_disabled": "No cambiar resolución", "dd_resolution_option_manual": "Usar resolución introducida manualmente", "dd_resolution_option_manual_desc": "Introduzca la resolución a usar", "dd_resolution_option_ogs_desc": "La opción \"Optimizar ajustes del juego\" debe estar habilitada en el cliente de luz lunar para que esto funcione.", "dd_wa_hdr_toggle_desc": "Cuando se utiliza el dispositivo de visualización virtual como para el streaming, puede mostrar un color HDR incorrecto. Con esta opción activada, Apollo intentará mitigar este problema.", "dd_wa_hdr_toggle": "Habilitar solución de alto contraste para HDR", "ds4_back_as_touchpad_click": "Mapa Atrás/Seleccionar a Touchpad Clic", "ds4_back_as_touchpad_click_desc": "Al forzar la emulación DS4, mapar Atrás/Seleccionar a Touchpad Clic", "enable_discovery": "Habilitar descubrimiento automático", "enable_discovery_desc": "Si se deshabilita tendrás que introducir la IP del host para emparejar.", "enable_input_only_mode": "Habilitar modo Sólo entrada", "enable_input_only_mode_desc": "Añade una app de Sólo entrada. Si se habilita la lista de apps solo mostrará la aplicación en ejecución y el modo Sólo entrada mientras se retransmite. El modo Sólo entrada no recibirá ni video ni audio. Útil para usar el escritorio en la TV o conectar periféricos que el televisor no soporta sin un teléfono", "enable_pairing": "Habilitar emparejamiento", "enable_pairing_desc": "Habilita emparejarse con el cliente de Moonlight. Esto permite al cliente autenticarse con el host y crear una conexión segura.", "encoder": "Forzar un codificador específico", "encoder_desc": "Forzar un codificador específico, de lo contrario Apollo 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.", "encoder_software": "Software", "external_ip": "IP externa", "external_ip_desc": "Si no se da ninguna dirección IP externa, Apollo detectará automáticamente IP externa", "fec_percentage": "Porcentaje FEC", "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.", "ffmpeg_auto": "auto -- dejar que ffmpeg decida (por defecto)", "file_apps": "Archivo de aplicaciones", "file_apps_desc": "El archivo donde se almacenan las aplicaciones actuales de Apollo.", "file_state": "Archivo de estado", "file_state_desc": "El archivo donde se almacena el estado actual de Apollo", "fps": "FPS anunciada", "gamepad": "Tipo de Gamepad emulado", "gamepad_auto": "Opciones de selección automática", "gamepad_desc": "Elegir qué tipo de gamepad emular en el host", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "Opciones de selección DS4", "gamepad_ds5": "DS5 (PS5)", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Opciones manuales del DS4", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Preparaciones de Comando", "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.", "hevc_mode": "Soporte de HEVC", "hevc_mode_0": "Apollo anunciará el soporte para HEVC basado en las capacidades del codificador (recomendado)", "hevc_mode_1": "Apollo no anunciará soporte para HEVC", "hevc_mode_2": "Apollo anunciará soporte para el perfil principal de HEVC", "hevc_mode_3": "Apollo anunciará soporte para perfiles HEVC Main y Main10 (HDR)", "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.", "hide_tray_controls": "Oculta los controles en la bandeja del sistema", "hide_tray_controls_desc": "No mostrará \"Forzar parada\", \"Reiniciar\" y \"Salir\" en la bandeja del sistema.", "high_resolution_scrolling": "Soporte de desplazamiento de alta resolución", "high_resolution_scrolling_desc": "Cuando está activado, Apollo 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.", "install_steam_audio_drivers": "Instalar los controladores de audio de Steam", "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.", "key_repeat_delay": "Retardo de repetición de teclas", "key_repeat_delay_desc": "Controla cómo se repetirán las teclas rápidas. El retardo inicial en milisegundos antes de repetir las teclas.", "key_repeat_frequency": "Frecuencia de repetición de la tecla", "key_repeat_frequency_desc": "Con qué frecuencia las teclas se repiten cada segundo. Esta opción configurable soporta decimales.", "key_rightalt_to_key_win": "Mapear tecla Alt Derecho a la tecla Windows", "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 Apollo piense que la tecla Alt correcta es la tecla de Windows", "keyboard": "Activar entrada de teclado", "keyboard_desc": "Permite a los invitados controlar el sistema de host con el teclado", "lan_encryption_mode": "Modo de cifrado LAN", "lan_encryption_mode_1": "Habilitar para clientes compatibles", "lan_encryption_mode_2": "Requerir para todos los clientes", "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.", "legacy_ordering": "Reordenar lista de apps legado", "legacy_ordering_desc": "Habilita un apaño en cliente legados para permitir reordenar. Puede causar errores con clientes y scripts que no soporten UTF8 correctamente. Los clientes Artemis lo soportan por defecto.", "limit_framerate": "Limitar velocidad de fotogramas capturados", "limit_framerate_desc": "Limita los fotogramas capturados a la velocidad del cliente. Puede no ejecutarse a velocidad completa si se habilita VSync y la valocidad de la pantalla no coincide con la pedida por el cliente. Puede causar peor rendimiento en algunos clientes si se deshabilita.", "locale": "Localización", "locale_desc": "La configuración regional utilizada para la interfaz de usuario de Apollo.", "log_level": "Nivel de registro", "log_level_0": "Detallado", "log_level_1": "Depurar", "log_level_2": "Información", "log_level_3": "Advertencia", "log_level_4": "Error", "log_level_5": "Fatal", "log_level_6": "Ninguna", "log_level_desc": "El nivel mínimo de registro impreso a nivel estándar", "log_path": "Ruta del archivo de registro", "log_path_desc": "El archivo donde se almacenan los registros actuales de Apollo.", "min_fps_factor": "Factor FPS Mínimo", "min_fps_factor_desc": "Apollo usará este factor para calcular el tiempo mínimo entre fotogramas. Incrementar este valor ligeramente puede ayudar a la transmisión en su mayoría de contenido estático. Valores más altos consumirán más banda ancha.", "min_threads": "Recuento mínimo de hilos de CPU", "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.", "misc": "Miscelánea", "motion_as_ds4": "Emular un gamepad DS4 si el gamepad del cliente informa que los sensores de movimiento están presentes", "motion_as_ds4_desc": "Si está desactivado, los sensores de movimiento no se tendrán en cuenta durante la selección del tipo gamepad.", "mouse": "Activar entrada del ratón", "mouse_desc": "Permite a los huéspedes controlar el sistema de host con el ratón", "native_pen_touch": "Soporte de lápiz/táctil nativo", "native_pen_touch_desc": "Cuando está activado, Apollo pasará a través de eventos nativos de pluma/táctil de clientes de Apollo. Esto puede ser útil para deshabilitar para aplicaciones antiguas sin soporte nativo de pluma/táctil.", "notify_pre_releases": "Notificaciones de pre-lanzamiento", "notify_pre_releases_desc": "Si desea ser notificado de las nuevas versiones de Apollo", "nvenc_h264_cavlc": "Preferir CAVLC sobre CABAC en H.264", "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.", "nvenc_latency_over_power": "Preferir menor latencia de codificación sobre ahorro de energía", "nvenc_latency_over_power_desc": "Apollo 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.", "nvenc_opengl_vulkan_on_dxgi": "Presentar OpenGL/Vulkan por encima de DXGI", "nvenc_opengl_vulkan_on_dxgi_desc": "Apollo 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 Apollo.", "nvenc_preset": "Rendimiento predefinido", "nvenc_preset_1": "(más rápido, por defecto)", "nvenc_preset_7": "(más lento)", "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.", "nvenc_realtime_hags": "Usar prioridad en tiempo real en la programación de gpu acelerada por hardware", "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.", "nvenc_spatial_aq": "Spatial AQ", "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.", "nvenc_spatial_aq_disabled": "Desactivado (más rápido, por defecto)", "nvenc_spatial_aq_enabled": "Habilitado (más lento)", "nvenc_twopass": "Modo dos pases", "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.", "nvenc_twopass_disabled": "Desactivado (lo más rápido, no recomendado)", "nvenc_twopass_full_res": "Resolución completa (más lento)", "nvenc_twopass_quarter_res": "Resolución de un cuarto (más rápido, por defecto)", "nvenc_vbv_increase": "Incremento porcentual de VBV/HRD de un solo fotograma", "nvenc_vbv_increase_desc": "Por defecto, Apollo 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.", "origin_web_ui_allowed": "Origin Web UI Permitido", "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", "origin_web_ui_allowed_lan": "Sólo aquellos en LAN pueden acceder a la Web UI", "origin_web_ui_allowed_pc": "Sólo localhost puede acceder a la Web UI", "origin_web_ui_allowed_wan": "Cualquiera puede acceder a Web UI", "output_name_desc_unix": "Durante el arranque de Apollo, debería ver la lista de pantallas detectadas. Nota: Necesita usar el valor id dentro del paréntesis.", "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:", "output_name_unix": "Mostrar número", "output_name_windows": "Mostrar Id de dispositivo", "ping_timeout": "Tiempo de espera", "ping_timeout_desc": "Cuánto tiempo en milisegundos esperar a los datos de Moonlight antes de apagar la retransmisión", "pkey": "Clave Privada", "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.", "port": "Puerto", "port_alert_1": "¡Apollo no puede usar puertos por debajo de 1024!", "port_alert_2": "¡Los puertos superiores a 65535 no están disponibles!", "port_desc": "Establecer la familia de puertos utilizados por Apollo", "port_http_port_note": "Use este puerto para conectar con Moonlgiht", "port_note": "Nota", "port_port": "Puerto", "port_protocol": "Protocolo", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "¡Exponer la UI web a Internet es un riesgo para la seguridad! ¡Proceda bajo su propia responsabilidad!", "port_web_ui": "Web UI", "qp": "Parámetro de Cuantizació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.", "qsv_coder": "Codificador QuickSync (H264)", "qsv_preset": "Preajuste QuickSync", "qsv_preset_fast": "rápido (baja calidad)", "qsv_preset_faster": "más rápido (menor calidad)", "qsv_preset_medium": "medio (por defecto)", "qsv_preset_slow": "lento (buena calidad)", "qsv_preset_slower": "lento (mejor calidad)", "qsv_preset_slowest": "Lo más lento (la mejor calidad)", "qsv_preset_veryfast": "más rápido (menor calidad)", "qsv_slow_hevc": "Permitir codificación HEVC lenta", "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.", "res_fps_desc": "Los modos de visualización anunciados por Apollo. Algunas versiones de Moonlight, como Moonlight-nx (Switch), se basan en estas listas para asegurar que las resoluciones y fps solicitadas sean soportadas. Esta configuración no cambia la forma en que la secuencia de pantalla se envía a Moonlight.", "resolutions": "Resoluciones anunciadas", "restart_note": "Apollo se está reiniciando para aplicar cambios.", "server_cmd": "Comandos de servidor", "server_cmd_desc": "Configura una lista de comandos que se puedan ejecutar a petición del cliente durante la retransmisión.", "stream_audio": "Transmitir audio", "stream_audio_desc": "Si se retransmite o no el audio. Deshabilitar esto puede ser útil para retransmitir monitores headless como segundos monitores.", "sunshine_name": "Nombre de Apollo", "sunshine_name_desc": "El nombre mostrado por Moonlight/Artemis. Si no se especifica, se utiliza el nombre de host del PC", "sw_preset": "Preajustes SW", "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.", "sw_preset_fast": "rápido", "sw_preset_faster": "más rápido", "sw_preset_medium": "medio", "sw_preset_slow": "lento", "sw_preset_slower": "más lento", "sw_preset_superfast": "superrápido (por defecto)", "sw_preset_ultrafast": "Super Veloz", "sw_preset_veryfast": "muy rápido", "sw_preset_veryslow": "muy lento", "sw_tune": "Sintonía SW", "sw_tune_animation": "animación -- buena para dibujos animados; utiliza un mayor desbloqueo y más fotogramas de referencia", "sw_tune_desc": "Opciones de ajuste, que se aplican después de la predeterminada. Por defecto es cero.", "sw_tune_fastdecode": "decodificación rápida -- permite una decodificación más rápida deshabilitando ciertos filtros", "sw_tune_film": "Película: se utiliza para películas de alta calidad; reduce el desbloqueo.", "sw_tune_grain": "grano -- conserva la estructura del grano en el viejo material de película de grano", "sw_tune_stillimage": "imagen fija -- bueno para contenido de diapositivas", "sw_tune_zerolatency": "latencia cero -- bueno para codificación rápida y transmisión de baja latencia (por defecto)", "touchpad_as_ds4": "Emular un gamepad DS4 si el gamepad del cliente reporta que un touchpad está presente", "touchpad_as_ds4_desc": "Si está desactivado, la presencia del touchpad no se tendrá en cuenta durante la selección del tipo gamepad.", "upnp": "UPnP", "upnp_desc": "Configurar automáticamente el reenvío de puertos para transmitir a través de Internet", "vaapi_strict_rc_buffer": "Aplicar estrictamente límites de bitrate de fotogramas para H.264/HEVC en AMD GPUs", "vaapi_strict_rc_buffer_desc": "Activar esta opción puede evitar fotogramas sueltos/perdidos en la red durante los cambios de escena, pero la calidad del vídeo puede reducirse durante el movimiento.", "virtual_sink": "Enlace virtual", "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!", "virtual_sink_placeholder": "Altavoces de Steam Streaming", "vt_coder": "Codificador de VideoToolbox", "vt_realtime": "Codificación de tiempo real de VideoToolbox", "vt_software": "Codificación de software VideoToolbox", "vt_software_allowed": "Permitido", "vt_software_forced": "Forzado", "wan_encryption_mode": "Modo de cifrado WAN", "wan_encryption_mode_1": "Activado para clientes compatibles (por defecto)", "wan_encryption_mode_2": "Requerido para todos los clientes", "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." }, "index": { "description": "Apollo es un servidor de transmisión de juego autoalojado para Moonlight.", "download": "Descargar", "installed_version_not_stable": "Está ejecutando una versión pre-lanzamiento de Apollo. Puede que experimente fallos u otros problemas. Por favor, informe de cualquier problema que encuentre. ¡Gracias por ayudar a hacer de Apollo un mejor software!", "loading_latest": "Cargando la última versión...", "new_pre_release": "¡Una nueva versión de pre-lanzamiento está disponible!", "new_stable": "¡Una nueva versión estable está disponible!", "startup_errors": "Atención. Apollo ha detectado estos errores durante el arranque. RECOMENDAMOS ENCARECIDAMENTE solucionarlos antes de transmitir.", "version_dirty": "¡Gracias por ayudar a hacer de Apollo un mejor software!", "version_latest": "Está ejecutando la última versión de Apollo", "welcome": "¡Hola, Apollo!" }, "navbar": { "applications": "Aplicaciones", "configuration": "Configuración", "home": "Inicio", "password": "Cambiar contraseña", "pin": "Pin", "theme_auto": "Auto", "theme_dark": "Oscuro", "theme_light": "Claro", "toggle_theme": "Tema", "troubleshoot": "Resolución de problemas" }, "password": { "confirm_password": "Confirmar contraseña", "current_creds": "Credenciales actuales", "new_creds": "Nuevas credenciales", "new_username_desc": "Si no se especifica, el nombre de usuario no cambiará", "password_change": "Cambio de contraseña", "success_msg": "¡La contraseña se ha cambiado con éxito! Esta página se recargará pronto, su navegador le pedirá las nuevas credenciales." }, "pin": { "device_name": "Nombre del dispositivo", "pair_failure": "Falló el emparejamiento: Compruebe si el PIN está escrito correctamente", "pair_success": "¡Éxito! Por favor revise Moonlight para continuar", "pin_pairing": "Emparejamiento de PIN", "send": "Enviar", "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!" }, "resource_card": { "github_discussions": "Discusiones GitHub", "legal": "Legal", "legal_desc": "Al continuar utilizando este software usted acepta los términos y condiciones de los siguientes documentos.", "license": "Licencia", "lizardbyte_website": "Sitio web de LizardByte", "resources": "Recursos", "resources_desc": "¡Recursos para Apollo!", "third_party_notice": "Aviso de Terceros" }, "troubleshooting": { "dd_reset": "Restablecer la configuración persistente del dispositivo", "dd_reset_desc": "Si Apollo está atascado tratando de restaurar los ajustes cambiados del dispositivo de visualización, puede restablecer los ajustes y proceder a restaurar el estado de visualización manualmente.", "dd_reset_error": "¡Error al restablecer la persistencia!", "dd_reset_success": "¡Se ha restablecido la persistencia!", "force_close": "Forzar cierre", "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.", "force_close_error": "Error al cerrar la aplicación", "force_close_success": "¡Aplicación cerrada con éxito!", "logs": "Registros", "logs_desc": "Ver los registros cargados por Apollo", "logs_find": "Encontrar...", "restart_apollo": "Reiniciar Apollo", "restart_apollo_desc": "Si Apollo no funciona correctamente, puede intentar reiniciarlo. Esto terminará cualquier sesión en ejecución.", "restart_apollo_success": "Apollo se está reiniciando", "troubleshooting": "Resolución de problemas", "unpair_all": "Desemparejar todo", "unpair_all_error": "Error al desvincular", "unpair_all_success": "Todos los dispositivos están desvínculados.", "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.", "unpair_single_no_devices": "No hay dispositivos vinculados.", "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.", "unpair_single_unknown": "Cliente desconocido", "unpair_title": "Desvincular dispositivos" }, "welcome": { "confirm_password": "Confirmar contraseña", "create_creds": "Antes de empezar, necesitamos que crees un nuevo nombre de usuario y contraseña para acceder a la Web UI.", "create_creds_alert": "Las credenciales a continuación son necesarias para acceder a la interfaz web de Apollo. Manténgalas seguras, ¡ya que nunca volverá a verlas!", "greeting": "¡Bienvenido a Apollo!", "login": "Iniciar sesión", "welcome_success": "Esta página se recargará pronto, su navegador le pedirá las nuevas credenciales" } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/fr.json ================================================ { "_common": { "apply": "Appliquer", "auto": "Automatique", "autodetect": "Détection automatique (recommandé)", "beta": "(bêta)", "cancel": "Annuler", "disabled": "Désactivé", "disabled_def": "Désactivé (par défaut)", "disabled_def_cbox": "Par défaut: décoché", "dismiss": "Refuser", "do_cmd": "Commande de début", "elevated": "Élevée", "enabled": "Activé", "enabled_def": "Activé (par défaut)", "enabled_def_cbox": "Par défaut: vérifié", "error": "Erreur !", "note": "Note :", "password": "Mot de passe", "run_as": "Exécuter en tant qu'Administrateur", "save": "Sauvegarder", "see_more": "Voir plus", "success": "Succès !", "undo_cmd": "Commande de fin", "username": "Nom d’utilisateur", "warning": "Attention !" }, "apps": { "actions": "Actions", "add_cmds": "Ajouter des commandes", "add_new": "Ajouter une application", "app_name": "Nom de l'application", "app_name_desc": "Nom de l'application, affiché dans Moonlight", "applications_desc": "Les applications ne sont actualisées qu'au redémarrage du client", "applications_title": "Applications", "auto_detach": "Continuer le streaming si l'application quitte rapidement", "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.", "cmd": "Commande", "cmd_desc": "L'application principale à démarrer. Si vide, aucune application ne sera démarrée.", "cmd_note": "Si le chemin vers l'exécutable de la commande contient des espaces, vous devez l'entourer de guillemets.", "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.", "cmd_prep_name": "Commandes de préparation", "covers_found": "Jaquettes trouvées", "delete": "Supprimer", "detached_cmds": "Commandes détachées", "detached_cmds_add": "Ajouter une commande détachée", "detached_cmds_desc": "Une liste de commandes à exécuter en arrière-plan.", "detached_cmds_note": "Si le chemin vers l'exécutable de la commande contient des espaces, vous devez l'entourer de guillemets.", "edit": "Modifier", "env_app_id": "ID de l'application", "env_app_name": "Nom de l'application", "env_client_audio_config": "La configuration audio demandée par le client (2.0/5.1/7.1)", "env_client_enable_sops": "Le client a activé l'option pour optimiser le jeu pour une diffusion optimale (true/false)", "env_client_fps": "FPS demandé par le client (float)", "env_client_gcmap": "Le masque de manette demandé, au format bitset/bitfield (entier)", "env_client_hdr": "Le HDR est activé par le client (true/false)", "env_client_height": "La hauteur demandée par le client (entier)", "env_client_host_audio": "Le client a activé l'audio côté audio (true/false)", "env_client_width": "La largeur demandée par le client (entier)", "env_displayplacer_example": "Exemple - displayplacer pour l'automatisation de la résolution :", "env_qres_example": "Exemple - QRes pour l'automatisation de la résolution :", "env_qres_path": "chemin de qres", "env_var_name": "Nom de la variable", "env_vars_about": "À propos des variables d'environnement", "env_vars_desc": "Toutes les commandes récupèrent ces variables d'environnement par défaut :", "env_xrandr_example": "Exemple - Xrandr pour l'automatisation de la résolution :", "exit_timeout": "Délai de fermeture", "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.", "find_cover": "Trouver une jaquette", "global_prep_desc": "Activer/désactiver l'exécution des commandes globales de préparation pour cette application.", "global_prep_name": "Commandes globales de préparation", "image": "Image", "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, Apollo enverra une jaquette par défaut.", "loading": "Chargement...", "name": "Nom", "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", "output_name": "Sortie", "run_as_desc": "Cela peut être nécessaire pour certaines applications qui nécessitent des autorisations d'administrateur pour fonctionner correctement.", "wait_all": "Continuer le streaming jusqu'à ce que tous les processus de l'application quittent", "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.", "working_dir": "Démarrer dans", "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, Apollo utilisera par défaut le répertoire parent de la commande" }, "config": { "adapter_name": "Nom de l'adaptateur", "adapter_name_desc_linux_1": "Spécifiez manuellement un GPU à utiliser pour la capture.", "adapter_name_desc_linux_2": "pour trouver tous les appareils capables d'utiliser l'interface VAAPI", "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 Apollo, il doit avoir au minimum les caractéristiques suivantes :", "adapter_name_desc_windows": "Spécifiez manuellement un GPU à utiliser pour la capture. Si non défini, le GPU est choisi automatiquement. Nous vous recommandons fortement de laisser ce champ vide pour utiliser la sélection automatique du GPU ! Note: Ce GPU doit avoir un écran connecté et allumé. Les valeurs appropriées peuvent être trouvées en utilisant la commande suivante :", "adapter_name_placeholder_windows": "Séries Radeon RX 580", "add": "Ajouter", "address_family": "Famille d'adresses", "address_family_both": "IPv4 et IPv6", "address_family_desc": "Définir la famille d'adresses utilisée par Apollo", "address_family_ipv4": "IPv4 uniquement", "always_send_scancodes": "Toujours envoyer les codes de balayage", "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.", "amd_coder": "Codeur AMF (H264)", "amd_coder_desc": "Permet de sélectionner l'encodage de l'entropie pour prioriser la qualité ou la vitesse d'encodage. H.264 seulement.", "amd_enforce_hrd": "Enforcement du Décodeur Hypothetical Reference Decoder (HRD) de l'AMF", "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.", "amd_preanalysis": "Pré-analyse AMF", "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.", "amd_quality": "Qualité AMF", "amd_quality_balanced": "équilibré -- équilibré (par défaut)", "amd_quality_desc": "Ceci contrôle l'équilibre entre la vitesse d'encodage et la qualité.", "amd_quality_group": "Paramètres de qualité AMF", "amd_quality_quality": "qualité -- préférer la qualité", "amd_quality_speed": "vitesse -- préférez la vitesse", "amd_rc": "Contrôle du débit AMF", "amd_rc_cbr": "cbr -- débit constant", "amd_rc_cqp": "cqp -- mode constant qp", "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.", "amd_rc_group": "Réglages de contrôle du débit AMF", "amd_rc_vbr_latency": "vbr_latency -- débit variable limité de latence (par défaut)", "amd_rc_vbr_peak": "vbr_peak -- débit variable contraint par le pic", "amd_usage": "Utilisation de l'AMF", "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.", "amd_usage_lowlatency": "lowlatency - faible latence (rapide)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - faible latence, haute qualité (rapide)", "amd_usage_transcoding": "transcoding -- transcodage (plus lent)", "amd_usage_ultralowlatency": "ultralowlatency - latence ultra basse (plus rapide)", "amd_usage_webcam": "webcam -- webcam (lent)", "amd_vbaq": "Quantification adaptative basée sur la variance AMF (VBAQ)", "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.", "apply_note": "Cliquez sur \"Appliquer\" pour redémarrer Apollo et appliquer les modifications. Cela mettra fin à toutes les sessions en cours.", "audio_sink": "Sortie audio", "audio_sink_desc_linux": "Le nom de la sortie audio utilisée pour le retour 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 de la sortie audio en utilisant l'une des commandes suivantes :", "audio_sink_desc_macos": "Le nom de la sortie audio utilisée pour le retour audio. Apollo ne peut accéder qu'aux micros sur macOS en raison de limitations du système. Pour diffuser l'audio du système, utilisez Soundflower ou BlackHole.", "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 :", "audio_sink_placeholder_macos": "BlackHole 2ch", "audio_sink_placeholder_windows": "Haut-parleurs (High Definition Audio Device)", "av1_mode": "Support de l'AV1", "av1_mode_0": "Apollo annoncera la prise en charge de l'AV1 en fonction des capacités de l'encodeur (recommandé)", "av1_mode_1": "Apollo n'annoncera pas la prise en charge de l'AV1", "av1_mode_2": "Apollo annoncera la prise en charge de l'AV1 Main 8-bit profile", "av1_mode_3": "Apollo annoncera la prise en charge de l'AV1 Main 8-bit et 10-bit (HDR) profiles", "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.", "back_button_timeout": "Délai d'émulation du bouton Home/Guide", "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.", "capture": "Forcer une méthode de capture spécifique", "capture_desc": "En mode automatique, Apollo utilisera le premier qui fonctionne. NvFBC nécessite des pilotes nvidia corrigés.", "cert": "Certificat", "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.", "channels": "Nombre maximum de clients connectés", "channels_desc_1": "Apollo peut permettre à une seule session de streaming d'être partagée simultanément avec plusieurs clients.", "channels_desc_2": "Certains encodeurs matériels peuvent avoir des limitations qui réduisent les performances avec plusieurs flux.", "coder_cabac": "cabac -- contexte de codage arithmétique binaire adaptatif - qualité supérieure", "coder_cavlc": "cavlc -- codage de la durée adaptative du contexte - décodage plus rapide", "configuration": "Configuration", "controller": "Activer l'entrée manette", "controller_desc": "Permet aux invités de contrôler le système hôte avec une manette", "credentials_file": "Fichier des identifiants", "credentials_file_desc": "Stocker le nom d'utilisateur/mot de passe séparément du fichier de données de Apollo.", "dd_config_ensure_active": "Activer l'affichage automatiquement", "dd_config_ensure_only_display": "Désactiver les autres affichages et n'activer que l'affichage spécifié", "dd_config_ensure_primary": "Activer l'affichage automatiquement et en faire un affichage principal", "dd_config_label": "Configuration de l'appareil", "dd_config_revert_delay": "Délai d'annulation de la configuration", "dd_config_revert_delay_desc": "Délai supplémentaire en millisecondes à attendre avant de revenir à la configuration lorsque l'application a été fermée ou que la dernière session s'est terminée. L'objectif principal est de fournir une transition plus souple lors d'un basculement rapide entre les applications.", "dd_config_verify_only": "Vérifiez que l'affichage est activé", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "Activer/désactiver le mode HDR tel que demandé par le client (par défaut)", "dd_hdr_option_disabled": "Ne pas modifier les paramètres HDR", "dd_mode_remapping": "Remplacer le mode d'affichage", "dd_mode_remapping_add": "Ajouter une entrée de remappage", "dd_mode_remapping_desc_1": "Spécifiez les entrées de redimensionnement pour modifier la résolution demandée et/ou le taux de rafraîchissement vers d'autres valeurs.", "dd_mode_remapping_desc_2": "La liste est parcourue de haut en bas, et la première correspondance est utilisée.", "dd_mode_remapping_desc_3": "Les champs \"Demandés\" peuvent être laissés vides pour correspondre à n'importe quelle valeur demandée.", "dd_mode_remapping_desc_4_final_values_mixed": "Au moins un champ \"Final\" doit être spécifié. La résolution non spécifiée ou le taux de rafraîchissement ne sera pas modifié.", "dd_mode_remapping_desc_4_final_values_non_mixed": "Le champ \"Final\" doit être spécifié et ne peut pas être vide.", "dd_mode_remapping_desc_5_sops_mixed_only": "L'option \"Optimiser les paramètres du jeu\" doit être activée dans le client Moonlight, sinon les entrées avec les champs de résolution spécifiés sont ignorées.", "dd_mode_remapping_desc_5_sops_resolution_only": "L'option \"Optimiser les paramètres du jeu\" doit être activée dans le client Moonlight, sinon le mapping est ignoré.", "dd_mode_remapping_final_refresh_rate": "Taux de rafraîchissement final", "dd_mode_remapping_final_resolution": "Résolution finale", "dd_mode_remapping_requested_fps": "FPS demandés", "dd_mode_remapping_requested_resolution": "Résolution demandée", "dd_options_header": "Options avancées pour les périphériques d'affichage.", "dd_refresh_rate_option": "Taux de rafraîchissement", "dd_refresh_rate_option_auto": "Utiliser la valeur FPS fournie par le client (par défaut)", "dd_refresh_rate_option_disabled": "Ne pas modifier le taux de rafraîchissement", "dd_refresh_rate_option_manual": "Utiliser la fréquence de rafraîchissement saisie manuellement", "dd_refresh_rate_option_manual_desc": "Entrez le taux de rafraîchissement à utiliser", "dd_resolution_option": "Résolution", "dd_resolution_option_auto": "Utiliser la résolution fournie par le client (par défaut)", "dd_resolution_option_disabled": "Ne pas modifier la résolution", "dd_resolution_option_manual": "Utiliser la résolution saisie manuellement", "dd_resolution_option_manual_desc": "Entrez la résolution à utiliser", "dd_resolution_option_ogs_desc": "L'option \"Optimiser les paramètres du jeu\" doit être activée sur le client Moonlight pour que cela fonctionne.", "dd_wa_hdr_toggle_desc": "Lorsque vous utilisez un périphérique d'affichage virtuel comme pour le streaming, il peut afficher une couleur HDR incorrecte. Avec cette option activée, Apollo essaiera d'atténuer ce problème.", "dd_wa_hdr_toggle": "Activer la solution de contournement à haut contraste pour HDR", "ds4_back_as_touchpad_click": "Mapper Retour/Sélection au clic du pavé tactile", "ds4_back_as_touchpad_click_desc": "Lorsque vous forcez l'émulation DS4, mappez le bouton Retour/Sélection sur le clic du pavé tactile.", "encoder": "Forcer un encodeur spécifique", "encoder_desc": "Forcer un encodeur spécifique, sinon Apollo 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é.", "encoder_software": "Logiciel", "external_ip": "Adresse IP externe", "external_ip_desc": "Si aucune adresse IP externe n'est fournie, Apollo la détectera automatiquement", "fec_percentage": "Pourcentage de FEC", "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.", "ffmpeg_auto": "auto -- laisser ffmpeg décider (par défaut)", "file_apps": "Fichier des applications", "file_apps_desc": "Le fichier où sont stockées les applications de Apollo.", "file_state": "Fichier des données", "file_state_desc": "Le fichier où l'état actuel de Apollo est stocké", "fps": "FPS annoncés", "gamepad": "Type de manette émulée", "gamepad_auto": "Options de sélection automatique", "gamepad_desc": "Choisissez le type de manette à émuler sur l'hôte", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "Options de sélection DS4", "gamepad_ds5": "DS5 (PS5)", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Options manuelles pour DS4", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Commandes de préparation", "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.", "hevc_mode": "Support du HEVC", "hevc_mode_0": "Apollo annoncera la prise en charge de HEVC en fonction des capacités de l'encodeur (recommandé)", "hevc_mode_1": "Apollo n'annoncera pas la prise en charge du HEVC", "hevc_mode_2": "Apollo annoncera la prise en charge du HEVC Main profile", "hevc_mode_3": "Apollo annoncera la prise en charge du HEVC Main et Main10 (HDR)", "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.", "high_resolution_scrolling": "Prise en charge du défilement haute résolution", "high_resolution_scrolling_desc": "Lorsque cette option est activée, Apollo passera par les événements de défilement haute résolution des clients Moonlight. 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.", "install_steam_audio_drivers": "Installer les pilotes audio Steam", "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.", "key_repeat_delay": "Délai de répétition de la clé", "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.", "key_repeat_frequency": "Fréquence de répétition des touches", "key_repeat_frequency_desc": "Fréquence de répétition des touches chaque seconde. Cette option configurable prend en charge les décimaux.", "key_rightalt_to_key_win": "Mapper la touche Alt droite à la touche Windows", "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 à Apollo que la touche Alt droite est la touche Windows", "keyboard": "Activer l'entrée clavier", "keyboard_desc": "Permet aux invités de contrôler le système hôte avec le clavier", "lan_encryption_mode": "Mode de chiffrement LAN", "lan_encryption_mode_1": "Activé pour les clients pris en charge", "lan_encryption_mode_2": "Obligatoire pour tous les clients", "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.", "locale": "Langue", "locale_desc": "La langue utilisée pour l'interface utilisateur de Apollo.", "log_level": "Niveau de journalisation", "log_level_0": "Verbeux", "log_level_1": "Débug", "log_level_2": "Info", "log_level_3": "Avertissements", "log_level_4": "Erreurs", "log_level_5": "Erreurs fatales", "log_level_6": "Aucun", "log_level_desc": "Le niveau minimum des journaux affiché sur la sortie standard.", "log_path": "Chemin du fichier journal", "log_path_desc": "Le fichier où sont stockés les logs actuels de Apollo.", "min_fps_factor": "Facteur FPS minimum", "min_fps_factor_desc": "Apollo utilisera ce facteur pour calculer le temps minimum entre les images. Augmenter légèrement cette valeur peut aider lorsque vous streamez du contenu statique. Des valeurs plus élevées consommeront plus de bande passante.", "min_threads": "Nombre minimum de threads CPU", "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.", "misc": "Options diverses", "motion_as_ds4": "Émuler une manette DS4 si la manette client signale qu'elle dispose de capteurs de mouvement", "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.", "mouse": "Activer l'entrée de la souris", "mouse_desc": "Permet aux invités de contrôler le système hôte avec la souris", "native_pen_touch": "Prise en charge stylo/écran tactile native", "native_pen_touch_desc": "Lorsque cette option est activée, Apollo 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.", "notify_pre_releases": "Notifications de pré-publication", "notify_pre_releases_desc": "Si vous voulez être informé des nouvelles versions de la pré-version de Apollo", "nvenc_h264_cavlc": "Préférer CAVLC sur CABAC en H.264", "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.", "nvenc_latency_over_power": "Préférer une latence d'encodage plus faible aux économies d'énergie", "nvenc_latency_over_power_desc": "Apollo 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.", "nvenc_opengl_vulkan_on_dxgi": "Présenter OpenGL/Vulkan sur DXGI", "nvenc_opengl_vulkan_on_dxgi_desc": "Apollo 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.", "nvenc_preset": "Préréglage des performances", "nvenc_preset_1": "(plus rapide, par défaut)", "nvenc_preset_7": "(plus lent)", "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.", "nvenc_realtime_hags": "Utiliser la priorité en temps réel dans la planification matérielle du gpu accéléré", "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é.", "nvenc_spatial_aq": "Spatial AQ", "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.", "nvenc_spatial_aq_disabled": "Désactivé (plus rapide, par défaut)", "nvenc_spatial_aq_enabled": "Activé (plus lent)", "nvenc_twopass": "Mode bi-passe", "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.", "nvenc_twopass_disabled": "Désactivé (plus rapide, non recommandé)", "nvenc_twopass_full_res": "Résolution complète (plus lente)", "nvenc_twopass_quarter_res": "Quart de résolution (plus rapide, par défaut)", "nvenc_vbv_increase": "Augmentation du pourcentage de VBV/HRD à une seule image", "nvenc_vbv_increase_desc": "Par défaut, Apollo utilise un VBV/HRD à une seule image, ce qui signifie que la taille de chaque image vidéo encodée ne doit pas dépasser le débit binaire demandé divisé par le taux de trame demandé. Assouplir cette restriction peut être bénéfique et agir comme un débit variable à faible latence, mais cela peut également entraîner une perte de paquets si le réseau n'a pas de marge tampon suffisante pour gérer les pics de débit binaire. La valeur maximale acceptée est 400, ce qui correspond à une limite supérieure de taille d'image vidéo encodée augmentée de 5x.", "origin_web_ui_allowed": "Origines autorisées à accéder à l'interface web", "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é", "origin_web_ui_allowed_lan": "Seuls ceux qui sont en LAN peuvent accéder à l'interface Web", "origin_web_ui_allowed_pc": "Seul localhost peut accéder à l'interface Web", "origin_web_ui_allowed_wan": "N'importe qui peut accéder à l'interface Web", "output_name_desc_unix": "Lors du démarrage de Apollo, vous devriez voir la liste des affichages détectés. Note : Vous devez utiliser la valeur de l'id entre parenthèses.", "output_name_desc_windows": "Spécifiez manuellement un identifiant de périphérique d'affichage à utiliser pour la capture. Si ce champ est vide, l'affichage principal sera capturé. Remarque : Si vous avez spécifié un GPU ci-dessus, cet affichage doit être connecté à ce GPU. Lors du démarrage de Apollo, vous devriez voir la liste des affichages détectés. Ci-dessous un exemple ; la sortie réelle peut être consultée dans l'onglet Dépannage.", "output_name_unix": "Numéro d'affichage", "output_name_windows": "Identifiant du périphérique d'affichage", "ping_timeout": "Timeout du ping", "ping_timeout_desc": "Combien de temps attendre en millisecondes pour des données de Moonlight avant de couper le stream", "pkey": "Clé privée", "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.", "port": "Port", "port_alert_1": "Apollo ne peut pas utiliser les ports inférieurs à 1024 !", "port_alert_2": "Les ports supérieurs à 65535 ne sont pas disponibles !", "port_desc": "Définir la famille de ports utilisés par Apollo", "port_http_port_note": "Utilisez ce port pour vous connecter avec Moonlight.", "port_note": "Note ", "port_port": "Port", "port_protocol": "Protocole", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "Exposer l'interface Web à Internet est un risque de sécurité ! Procédez à vos propres risques !", "port_web_ui": "Interface Web", "qp": "Paramètre de quantification", "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é.", "qsv_coder": "Codeur QuickSync (H264)", "qsv_preset": "Préréglage QuickSync", "qsv_preset_fast": "plus rapide (qualité inférieure)", "qsv_preset_faster": "le plus rapide (qualité la plus basse)", "qsv_preset_medium": "moyen (par défaut)", "qsv_preset_slow": "lent (bonne qualité)", "qsv_preset_slower": "plus lent (meilleure qualité)", "qsv_preset_slowest": "plus lent (meilleure qualité)", "qsv_preset_veryfast": "le plus rapide (qualité la plus basse)", "qsv_slow_hevc": "Autoriser l'encodage Slow HEVC", "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.", "res_fps_desc": "Les modes d'affichage annoncés par Apollo. Certaines versions de Lune, telles que Moonlight-nx (Switch), s'appuient sur ces listes pour s'assurer que les résolutions et fps demandées sont prises en charge. Ce paramètre ne change pas comment le flux d'écran est envoyé à Lune.", "resolutions": "Résolutions annoncées", "restart_note": "Apollo redémarre pour appliquer les changements.", "sunshine_name": "Nom de Apollo", "sunshine_name_desc": "Le nom affiché par Moonlight. S'il n'est pas spécifié, le nom d'hôte du PC est utilisé.", "sw_preset": "Préréglages SW", "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.", "sw_preset_fast": "rapide", "sw_preset_faster": "plus rapide", "sw_preset_medium": "moyen", "sw_preset_slow": "lent", "sw_preset_slower": "plus lent", "sw_preset_superfast": "superfast (par défaut)", "sw_preset_ultrafast": "ultrafast", "sw_preset_veryfast": "veryfast", "sw_preset_veryslow": "veryslow", "sw_tune": "Réglage SW", "sw_tune_animation": "animation -- bonne pour les dessins animés ; utilise un déblocage plus élevé et plus d'images de référence", "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.", "sw_tune_fastdecode": "fastdecode -- permet un décodage plus rapide en désactivant certains filtres", "sw_tune_film": "film -- utilisé pour le contenu de film de haute qualité; baisse le déblocage", "sw_tune_grain": "grain -- conserve la structure du grain dans le vieux matériel de film graineux", "sw_tune_stillimage": "stillimage -- bon pour le contenu du diaporama", "sw_tune_zerolatency": "zerolatency -- bon pour un encodage rapide et un streaming à faible latence (par défaut)", "touchpad_as_ds4": "Émuler une manette DS4 si la manette client signale qu'elle dispose d'un pavé tactile", "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.", "upnp": "UPnP", "upnp_desc": "Configurer automatiquement la redirection de port pour le streaming sur Internet", "vaapi_strict_rc_buffer": "Appliquer strictement les limites de débit d'images pour H.264/HEVC sur les GPU AMD", "vaapi_strict_rc_buffer_desc": "L'activation de cette option peut éviter de laisser tomber des images sur le réseau pendant les changements de scène, mais la qualité de la vidéo peut être réduite pendant le mouvement.", "virtual_sink": "Sortie virtuelle", "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 !", "virtual_sink_placeholder": "Steam Streaming Speakers", "vt_coder": "VideoToolbox Coder", "vt_realtime": "Encodage en temps réel de VideoToolbox", "vt_software": "Encodage du logiciel VideoToolbox", "vt_software_allowed": "Autorisé", "vt_software_forced": "Forcé", "wan_encryption_mode": "Mode de chiffrement WAN", "wan_encryption_mode_1": "Activé pour les clients pris en charge (par défaut)", "wan_encryption_mode_2": "Obligatoire pour tous les clients", "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." }, "index": { "description": "Apollo est un serveur de streaming auto-hébergé pour Moonlight.", "download": "Télécharger", "installed_version_not_stable": "Vous utilisez une version pré-publiée de Apollo. Vous pouvez rencontrer des bugs ou d'autres problèmes. Veuillez signaler tout problème que vous rencontrez. Merci de nous aider à faire de Apollo un meilleur logiciel!", "loading_latest": "Chargement de la dernière version...", "new_pre_release": "Une nouvelle version de pré-version est disponible!", "new_stable": "Une nouvelle version stable est disponible !", "startup_errors": "Attention ! Apollo a détecté ces erreurs lors du démarrage. Nous recommandons vivement de les corriger avant de commencer à streamer.", "version_dirty": "Merci de contribuer à faire de Apollo un meilleur logiciel !", "version_latest": "Vous utilisez la dernière version de Apollo", "welcome": "Bonjour, Apollo!" }, "navbar": { "applications": "Applications", "configuration": "Configuration", "home": "Accueil", "password": "Changer le mot de passe", "pin": "Pin", "theme_auto": "Automatique", "theme_dark": "Sombre", "theme_light": "Clair", "toggle_theme": "Thème", "troubleshoot": "Dépannage" }, "password": { "confirm_password": "Confirmer le mot de passe", "current_creds": "Identifiants actuels", "new_creds": "Nouveaux identifiants", "new_username_desc": "S'il n'est pas spécifié, le nom d'utilisateur ne changera pas", "password_change": "Changement du mot de passe", "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." }, "pin": { "device_name": "Nom de l'appareil", "pair_failure": "Échec de l'appairage : Vérifiez si le code PIN est correctement saisi", "pair_success": "Succès ! Veuillez vérifier Moonlight pour continuer", "pin_pairing": "Appairage par code PIN", "send": "Envoyer", "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 !" }, "resource_card": { "github_discussions": "Discussions GitHub", "legal": "Légal", "legal_desc": "En continuant à utiliser ce logiciel, vous acceptez les termes et conditions des documents suivants.", "license": "Licence", "lizardbyte_website": "Site web de LizardByte", "resources": "Ressources", "resources_desc": "Ressources pour Apollo !", "third_party_notice": "Avis aux tiers" }, "troubleshooting": { "dd_reset": "Réinitialiser les paramètres d'affichage persistant", "dd_reset_desc": "Si Apollo est bloqué en essayant de restaurer les paramètres de l'appareil d'affichage modifiés, vous pouvez réinitialiser les paramètres et procéder à la restauration manuelle de l'état.", "dd_reset_error": "Erreur lors de la réinitialisation de la persistance !", "dd_reset_success": "Réinitialisation de la persistance réussie !", "force_close": "Fermer de force", "force_close_desc": "Si Moonlight signale qu'une application est actuellement en cours d'exécution, forcer la fermeture de l'application devrait résoudre le problème.", "force_close_error": "Erreur lors de la fermeture de l'application", "force_close_success": "L'application à bien été fermée !", "logs": "Journaux", "logs_desc": "Voir les journaux envoyés par Apollo", "logs_find": "Rechercher...", "restart_apollo": "Redémarrer Apollo", "restart_apollo_desc": "Si Apollo ne fonctionne pas correctement, vous pouvez essayer de le redémarrer. Cela mettra fin à toutes les sessions en cours.", "restart_apollo_success": "Apollo redémarre", "troubleshooting": "Dépannage", "unpair_all": "Dissocier tous les périphériques", "unpair_all_error": "Erreur lors de la dissociation", "unpair_all_success": "Désappairage réussi.", "unpair_desc": "Supprimez vos périphériques appairés. Les périphériques dissociés individuellement avec une session active resteront connectés, mais ne pourront pas démarrer ou reprendre une session.", "unpair_single_no_devices": "Il n'y a aucun appareil associé.", "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.", "unpair_single_unknown": "Client inconnu", "unpair_title": "Dissocier les périphériques" }, "welcome": { "confirm_password": "Confirmation du mot de passe", "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.", "create_creds_alert": "Les identifiants ci-dessous sont nécessaires pour accéder à l'interface Web de Apollo. Gardez-les en sécurité, car vous ne les reverrez plus jamais !", "greeting": "Bienvenue dans Apollo !", "login": "Connexion", "welcome_success": "Cette page se rechargera bientôt, votre navigateur vous demandera les nouveaux identifiants" } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/hu.json ================================================ { "_common": { "apply": "Alkalmazás", "auto": "Automatikus", "autodetect": "Automatikus felismerés (ajánlott)", "beta": "(béta)", "cancel": "Mégse", "disabled": "Kikapcsolva", "disabled_def": "Kikapcsolva (alapértelmezett)", "disabled_def_cbox": "Alapértelmezett: nincs bejelölve", "dismiss": "Eltüntetés", "do_cmd": "Do parancs", "elevated": "Megemelt", "enabled": "Bekapcsolva", "enabled_def": "Bekapcsolva (alapértelmezett)", "enabled_def_cbox": "Alapértelmezett: bekapcsolva", "error": "Hiba!", "note": "Megjegyzés:", "password": "Jelszó", "run_as": "Futtatás rendszergazdaként", "save": "Mentés", "see_more": "Lásd még", "success": "Siker!", "undo_cmd": "Parancs visszavonása", "username": "Felhasználónév", "warning": "Figyelem!" }, "apps": { "actions": "Tevékenységek", "add_cmds": "Parancsok hozzáadása", "add_new": "Új hozzáadása", "app_name": "Alkalmazás neve", "app_name_desc": "Alkalmazás neve, a Moonlight-on feltüntetett módon", "applications_desc": "Az alkalmazások csak a kliensgép újraindításakor frissülnek", "applications_title": "Alkalmazások", "auto_detach": "Folytassa a streaminget, ha az alkalmazás gyorsan kilép", "auto_detach_desc": "Ez megpróbálja automatikusan felismerni az olyan indító típusú alkalmazásokat, amelyek gyorsan bezáródnak egy másik program vagy saját példányuk elindítása után. Ha egy indító típusú alkalmazást észlel, azt leválasztott alkalmazásként kezeli.", "cmd": "Parancs", "cmd_desc": "A fő alkalmazás elindítása. Ha üres, akkor nem indul alkalmazás.", "cmd_note": "Ha a parancs futtatható fájljának elérési útvonala szóközöket tartalmaz, akkor idézőjelek közé kell zárnia.", "cmd_prep_desc": "Az alkalmazás előtt/után futtatandó parancsok listája. Ha bármelyik előkészítő parancs sikertelen, az alkalmazás elindítása megszakad.", "cmd_prep_name": "Parancsnoki előkészületek", "covers_found": "Fedelek találtak", "delete": "Törlés", "detached_cmds": "Leválasztott parancsok", "detached_cmds_add": "Önálló parancs hozzáadása", "detached_cmds_desc": "A háttérben futtatandó parancsok listája.", "detached_cmds_note": "Ha a parancs futtatható fájljának elérési útvonala szóközöket tartalmaz, akkor idézőjelek közé kell zárnia.", "edit": "Szerkesztés", "env_app_id": "Alkalmazás azonosítója", "env_app_name": "Alkalmazás neve", "env_client_audio_config": "A kliensgép által kért hangkonfiguráció (2.0/5.1/7.1)", "env_client_enable_sops": "Az ügyfél kérte a játék optimalizálásának lehetőségét az optimális streaminghez (igaz/hamis).", "env_client_fps": "A kliensgép által kért FPS (egész szám)", "env_client_gcmap": "A kért gamepad maszk, bset/bitfield formátumban (int)", "env_client_hdr": "A kliensgép engedélyezi a HDR-t (igaz/hamis)", "env_client_height": "A kliensgép által kért magasság (egész szám)", "env_client_host_audio": "A kliensgép a gazdagép hangját kérte (igaz/hamis)", "env_client_width": "A kliensgép által kért szélesség (egész szám)", "env_displayplacer_example": "Példa - displayplacer a Resolution Automation számára:", "env_qres_example": "Példa - QRes a felbontás automatizálásához:", "env_qres_path": "qres útvonal", "env_var_name": "Var neve", "env_vars_about": "A környezeti változókról", "env_vars_desc": "Alapértelmezés szerint minden parancs megkapja ezeket a környezeti változókat:", "env_xrandr_example": "Példa - Xrandr a felbontás automatizálásához:", "exit_timeout": "Kilépési időkorlát", "exit_timeout_desc": "A kilépésre vonatkozó kérés esetén az alkalmazás összes folyamatának méltóságteljes kilépésére várandó másodpercek száma. Ha nincs megadva, az alapértelmezett érték 5 másodpercig várakozik. Ha 0-ra van állítva, az alkalmazás azonnal befejeződik.", "find_cover": "Fedezet keresése", "global_prep_desc": "A globális előkészítő parancsok végrehajtásának engedélyezése/letiltása az alkalmazás számára.", "global_prep_name": "Globális előkészítő parancsok", "image": "Kép", "image_desc": "Az ügyfélnek küldött alkalmazás ikon/kép/kép elérési útvonala. A képnek PNG fájlnak kell lennie. Ha nincs megadva, a Sunshine alapértelmezett dobozképet küld.", "loading": "Betöltés...", "name": "Név", "output_desc": "Az a fájl, ahol a parancs kimenete tárolódik, ha nincs megadva, a kimenetet figyelmen kívül hagyjuk.", "output_name": "Kimenet", "run_as_desc": "Erre olyan alkalmazásoknál lehet szükség, amelyek megfelelő futtatásához rendszergazdai engedélyek szükségesek.", "wait_all": "Folytassa a streaminget, amíg az összes alkalmazásfolyamat ki nem lép", "wait_all_desc": "A streaming addig folytatódik, amíg az alkalmazás által indított összes folyamat le nem zárul. Ha nincs bejelölve, a streaming leáll, amikor az alkalmazás kezdeti folyamata kilép, még akkor is, ha más alkalmazásfolyamatok még futnak.", "working_dir": "Munkakönyvtár", "working_dir_desc": "A folyamatnak átadandó munkakönyvtár. Egyes alkalmazások például a munkakönyvtárat használják a konfigurációs fájlok keresésére. Ha nincs megadva, a Sunshine alapértelmezés szerint a parancs szülő könyvtárát fogja használni." }, "config": { "adapter_name": "Adapter neve", "adapter_name_desc_linux_1": "Kézzel adja meg a rögzítéshez használt GPU-t.", "adapter_name_desc_linux_2": "az összes VAAPI-képes eszköz megtalálása", "adapter_name_desc_linux_3": "A ``renderD129``-t helyettesítsük a fenti eszközzel, hogy felsoroljuk az eszköz nevét és képességeit. Ahhoz, hogy a Sunshine támogassa, legalább a következőkkel kell rendelkeznie:", "adapter_name_desc_windows": "Kézzel adja meg a rögzítéshez használt GPU-t. Ha nincs megadva, a GPU automatikusan kiválasztásra kerül. A GPU automatikus kiválasztásához javasoljuk, hogy hagyja üresen ezt a mezőt! Megjegyzés: Ehhez a GPU-hoz csatlakoztatott és bekapcsolt kijelzőnek kell lennie. A megfelelő értékeket a következő paranccsal találhatja meg:", "adapter_name_placeholder_windows": "Radeon RX 580 sorozat", "add": "Hozzáadás", "address_family": "Címcsalád", "address_family_both": "IPv4+IPv6", "address_family_desc": "A Sunshine által használt címcsalád beállítása", "address_family_ipv4": "Csak IPv4", "always_send_scancodes": "Mindig küldjön kódokat", "always_send_scancodes_desc": "A scan-kódok küldése javítja a kompatibilitást a játékokkal és alkalmazásokkal, de bizonyos, nem amerikai angol billentyűzetkiosztást használó ügyfeleknél helytelen billentyűzetbevitelt eredményezhet. Engedélyezze, ha a billentyűzetbevitel egyáltalán nem működik bizonyos alkalmazásokban. Tiltja le, ha az ügyfél billentyűi rossz bevitelt generálnak a hoszton.", "amd_coder": "AMF kódoló (H264)", "amd_coder_desc": "Lehetővé teszi az entrópia kódolás kiválasztását a minőség vagy a kódolási sebesség előtérbe helyezéséhez. Csak H.264.", "amd_enforce_hrd": "AMF hipotetikus referencia dekóder (HRD) végrehajtása", "amd_enforce_hrd_desc": "Növeli az árfolyam-szabályozásra vonatkozó korlátozásokat, hogy megfeleljen a HRD-modell követelményeinek. Ez nagymértékben csökkenti a bitráta-túlcsordulást, de bizonyos kártyákon kódolási leleteket vagy csökkent minőséget okozhat.", "amd_preanalysis": "AMF előelemzés", "amd_preanalysis_desc": "Ez lehetővé teszi a sebesség-szabályozás előzetes elemzését, ami növelheti a minőséget a megnövekedett kódolási késleltetés rovására.", "amd_quality": "AMF minőség", "amd_quality_balanced": "balanced -- kiegyensúlyozott (alapértelmezett)", "amd_quality_desc": "Ez szabályozza a kódolási sebesség és a minőség közötti egyensúlyt.", "amd_quality_group": "AMF minőségi beállítások", "amd_quality_quality": "minőség -- a minőség előnyben részesítése", "amd_quality_speed": "sebesség -- a sebesség előnyben részesítése", "amd_rc": "AMF sebesség vezérlés", "amd_rc_cbr": "cbr -- állandó bitráta (ajánlott, ha a HRD engedélyezve van)", "amd_rc_cqp": "cqp -- állandó qp üzemmód", "amd_rc_desc": "Ez vezérli a sebességszabályozási módszert, hogy biztosítsa, hogy nem lépjük túl az ügyfél bitrátájának célértékét. A 'cqp' nem alkalmas bitráta-célzásra, és a 'vbr_latency'-n kívül más opciók is a HRD Enforcementtől függenek, hogy segítsenek a bitráta-túllépések korlátozásában.", "amd_rc_group": "AMF Rate Control beállítások", "amd_rc_vbr_latency": "vbr_latency -- késleltetéskorlátozott változó bitráta (ajánlott, ha a HRD le van tiltva; alapértelmezett)", "amd_rc_vbr_peak": "vbr_peak -- csúcsértékkel korlátozott változó bitráta", "amd_usage": "AMF használat", "amd_usage_desc": "Ez állítja be az alap kódolási profilt. Az alább bemutatott összes opció felülírja a használati profil egy részhalmazát, de vannak további rejtett beállítások, amelyek máshol nem konfigurálhatók.", "amd_usage_lowlatency": "lowlatency - alacsony késleltetés (leggyorsabb)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - alacsony késleltetés, magas minőség (gyors)", "amd_usage_transcoding": "transzkódolás -- transzkódolás (leglassabb)", "amd_usage_ultralowlatency": "ultralowlatency - ultra alacsony késleltetés (leggyorsabb; alapértelmezett)", "amd_usage_webcam": "webcam -- webkamera (lassú)", "amd_vbaq": "AMF Variancia alapú adaptív kvantálás (VBAQ)", "amd_vbaq_desc": "Az emberi vizuális rendszer jellemzően kevésbé érzékeny az erősen texturált területeken megjelenő műalkotásokra. A VBAQ módban a pixelvariancia a térbeli textúrák összetettségének jelzésére szolgál, lehetővé téve a kódoló számára, hogy több bitet rendeljen a simább területekhez. E funkció engedélyezése bizonyos tartalmak esetében javítja a szubjektív vizuális minőséget.", "apply_note": "Kattintson az 'Alkalmaz' gombra a Sunshine újraindításához és a módosítások alkalmazásához. Ezzel minden futó munkamenet megszűnik.", "audio_sink": "Audio Sink", "audio_sink_desc_linux": "Az Audio Loopbackhez használt hangkimenet neve. Ha nem adja meg ezt a változót, a pulseaudio az alapértelmezett monitor eszközt fogja kiválasztani. A hangelnyelő nevét bármelyik parancs segítségével megtudhatja:", "audio_sink_desc_macos": "Az Audio Loopbackhez használt hangkimenet neve. A Sunshine a rendszer korlátai miatt csak a macOS rendszerben érheti el a mikrofonokat. Rendszerhang streameléséhez Soundflower vagy BlackHole használatával.", "audio_sink_desc_windows": "Kézzel adja meg a rögzítendő hangeszközöket. Ha nincs megadva, az eszköz automatikusan kiválasztásra kerül. Az automatikus eszközkiválasztás használatához erősen ajánlott üresen hagyni ezt a mezőt! Ha több azonos nevű audioeszközzel rendelkezik, az eszközazonosítót a következő paranccsal kaphatja meg:", "audio_sink_placeholder_macos": "BlackHole 2ch", "audio_sink_placeholder_windows": "Hangszórók (nagy felbontású hangeszköz)", "av1_mode": "AV1 támogatás", "av1_mode_0": "A Sunshine az AV1 támogatását a kódoló képességei alapján hirdeti (ajánlott)", "av1_mode_1": "A Sunshine nem fogja hirdetni az AV1 támogatását", "av1_mode_2": "A Sunshine az AV1 Main 8-bites profil támogatását fogja hirdetni", "av1_mode_3": "A Sunshine az AV1 Main 8-bites és 10-bites (HDR) profilok támogatását hirdeti meg", "av1_mode_desc": "Lehetővé teszi az ügyfél számára, hogy AV1 Main 8 bites vagy 10 bites videófolyamokat kérjen. Az AV1 kódolása CPU-igényesebb, ezért ennek engedélyezése csökkentheti a teljesítményt szoftveres kódolás esetén.", "back_button_timeout": "Home/Guide gomb Emulációs időkorlát", "back_button_timeout_desc": "Ha a Back/Select gombot a megadott számú milliszekundumig lenyomva tartja, a Home/Guide gomb megnyomása utánozódik. Ha < 0 értékre van beállítva (alapértelmezett), a Back/Select gomb nyomva tartása nem emulálja a Home/Guide gombot.", "capture": "Speciális rögzítési módszer kikényszerítése", "capture_desc": "Automatikus üzemmódban a Sunshine az elsőt használja, amelyik működik. Az NvFBC javított nvidia illesztőprogramokat igényel.", "cert": "Tanúsítvány", "cert_desc": "A webes felhasználói felület és a Moonlight-ügyfél párosításához használt tanúsítvány. A legjobb kompatibilitás érdekében ennek RSA-2048-as nyilvános kulccsal kell rendelkeznie.", "channels": "Maximális csatlakoztatott ügyfelek", "channels_desc_1": "A Sunshine lehetővé teszi, hogy egyetlen streaming munkamenetet egyszerre több ügyfél is használhasson.", "channels_desc_2": "Néhány hardveres kódolónak lehetnek olyan korlátai, amelyek több adatfolyam esetén csökkentik a teljesítményt.", "coder_cabac": "cabac -- kontextus adaptív bináris aritmetikai kódolás - magasabb minőség", "coder_cavlc": "cavlc -- kontextus adaptív változó hosszúságú kódolás - gyorsabb dekódolás", "configuration": "Konfiguráció", "controller": "Gamepad bemenet engedélyezése", "controller_desc": "Lehetővé teszi a vendégek számára, hogy gamepaddal / kontrollerrel irányítsák a gazdarendszert.", "credentials_file": "Hitelesítési fájl", "credentials_file_desc": "Tárolja a felhasználónevet/jelszót a Sunshine állapotfájljától elkülönítve.", "dd_config_ensure_active": "A kijelző automatikus aktiválása", "dd_config_ensure_only_display": "Más kijelzők kikapcsolása és csak a megadott kijelző aktiválása", "dd_config_ensure_primary": "A kijelző automatikus aktiválása és elsődleges kijelzővé tétele", "dd_configuration_option": "Eszköz konfigurációja", "dd_config_revert_delay": "Config revert késleltetés", "dd_config_revert_delay_desc": "Kiegészítő késleltetés milliszekundumban, amelyet a konfiguráció visszaállítása előtt várni kell, ha az alkalmazás bezárásra került vagy az utolsó munkamenet befejeződött. Fő célja, hogy simább átmenetet biztosítson az alkalmazások közötti gyors váltáskor.", "dd_config_revert_on_disconnect": "Konfiguráció visszaállítása a kapcsolat megszakításakor", "dd_config_revert_on_disconnect_desc": "A konfiguráció visszaállítása az összes ügyfél kapcsolatának megszakításakor az alkalmazás bezárása vagy az utolsó munkamenet befejezése helyett.", "dd_config_verify_only": "Ellenőrizze, hogy a kijelző engedélyezve van-e", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "A HDR üzemmód be-/kikapcsolása az ügyfél kérésének megfelelően (alapértelmezett).", "dd_hdr_option_disabled": "Ne módosítsa a HDR beállításokat", "dd_manual_refresh_rate": "Kézi frissítési gyakoriság", "dd_manual_resolution": "Kézi felbontás", "dd_mode_remapping": "Megjelenítési mód átképzése", "dd_mode_remapping_add": "Remapping bejegyzés hozzáadása", "dd_mode_remapping_desc_1": "Adjon meg remapping bejegyzéseket a kért felbontás és/vagy frissítési sebesség más értékekre történő módosításához.", "dd_mode_remapping_desc_2": "A lista felülről lefelé halad, és az első találatot használja fel.", "dd_mode_remapping_desc_3": "A \"Requested\" mezők üresen hagyhatók, hogy megfeleljenek bármely kért értéknek.", "dd_mode_remapping_desc_4_final_values_mixed": "Legalább egy \"Végleges\" mezőt meg kell adni. A meg nem adott felbontás vagy frissítési sebesség nem fog megváltozni.", "dd_mode_remapping_desc_4_final_values_non_mixed": "A \"Final\" mezőt meg kell adni, és nem lehet üres.", "dd_mode_remapping_desc_5_sops_mixed_only": "A \"Játékbeállítások optimalizálása\" opciónak engedélyezve kell lennie a Moonlight kliensben, különben a felbontási mezőkkel rendelkező bejegyzések kihagyásra kerülnek.", "dd_mode_remapping_desc_5_sops_resolution_only": "A \"Játékbeállítások optimalizálása\" opciónak engedélyezve kell lennie a Moonlight kliensben, különben a leképezés kihagyásra kerül.", "dd_mode_remapping_final_refresh_rate": "Végső frissítési gyakoriság", "dd_mode_remapping_final_resolution": "Végső felbontás", "dd_mode_remapping_requested_fps": "Kért FPS", "dd_mode_remapping_requested_resolution": "Kért felbontás", "dd_options_header": "Speciális kijelzőeszköz beállítások", "dd_refresh_rate_option": "Frissítési gyakoriság", "dd_refresh_rate_option_auto": "Az ügyfél által megadott FPS-érték használata (alapértelmezett)", "dd_refresh_rate_option_disabled": "Ne változtassa meg a frissítési gyakoriságot", "dd_refresh_rate_option_manual": "Kézzel megadott frissítési sebesség használata", "dd_resolution_option": "Felbontás", "dd_resolution_option_auto": "A kliensgép által megadott felbontás használata (alapértelmezett)", "dd_resolution_option_disabled": "Ne változtassa meg a felbontást", "dd_resolution_option_manual": "Kézzel megadott felbontás használata", "dd_resolution_option_ogs_desc": "A \"Játékbeállítások optimalizálása\" opciónak engedélyezve kell lennie a Moonlight kliensben, hogy ez működjön.", "dd_wa_hdr_toggle_delay_desc_1": "Ha virtuális megjelenítő eszközt (VDD) használ a streaminghez, előfordulhat, hogy a HDR színt helytelenül jeleníti meg. A Sunshine megpróbálhatja enyhíteni ezt a problémát a HDR kikapcsolásával, majd újra bekapcsolásával.", "dd_wa_hdr_toggle_delay_desc_2": "Ha az érték 0-ra van állítva, a megoldás le van tiltva (alapértelmezett). Ha az érték 0 és 3000 milliszekundum között van, a Sunshine kikapcsolja a HDR-t, vár a megadott ideig, majd újra bekapcsolja a HDR-t. Az ajánlott késleltetési idő a legtöbb esetben 500 milliszekundum körül van.", "dd_wa_hdr_toggle_delay_desc_3": "NE használja ezt a megoldást, kivéve, ha valóban problémái vannak a HDR-rel, mivel ez közvetlenül befolyásolja a stream indulási idejét!", "dd_wa_hdr_toggle_delay": "Nagy kontrasztú megoldás HDR esetén", "ds4_back_as_touchpad_click": "Vissza/kiválasztás az érintőpadra kattintás", "ds4_back_as_touchpad_click_desc": "A DS4 emuláció kényszerítésekor a Vissza/Választás gombot a Touchpad kattintáshoz kell rendelni.", "ds5_inputtino_randomize_mac": "Virtuális vezérlő MAC véletlenszerűvé tétele", "ds5_inputtino_randomize_mac_desc": "A vezérlő regisztrációjakor a vezérlők belső indexén alapuló MAC helyett véletlenszerű MAC-et használ, hogy elkerülje a különböző vezérlők konfigurációs beállításainak keveredését, amikor a vezérlők ügyféloldalon cserélődnek.", "encoder": "Egy adott kódoló kényszerítése", "encoder_desc": "Kényszeríts egy adott kódolót, különben a Sunshine a legjobb elérhető opciót fogja kiválasztani. Megjegyzés: Ha Windows alatt hardveres kódolót adsz meg, annak meg kell egyeznie azzal a GPU-val, amelyhez a kijelző csatlakoztatva van.", "encoder_software": "Szoftveres", "external_ip": "Külső IP", "external_ip_desc": "Ha nincs megadva külső IP-cím, a Sunshine automatikusan felismeri a külső IP-címet.", "fec_percentage": "FEC százalékos aránya", "fec_percentage_desc": "A hibajavító csomagok százalékos aránya adatcsomagonként az egyes videóképkockákban. A magasabb értékek több hálózati csomagveszteséget korrigálhatnak, de ennek ára a sávszélesség-használat növekedése.", "ffmpeg_auto": "auto -- hagyja, hogy az ffmpeg döntsön (alapértelmezett)", "file_apps": "Alkalmazások fájl", "file_apps_desc": "Az a fájl, amelyben a Sunshine aktuális alkalmazásai tárolódnak.", "file_state": "State fájl", "file_state_desc": "A fájl, ahol a Sunshine aktuális állapota tárolódik", "gamepad": "Emulált gamepad típus", "gamepad_auto": "Automatikus kiválasztási lehetőségek", "gamepad_desc": "Válassza ki, hogy milyen típusú gamepadot szeretne emulálni a gazdán.", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "DS4 kiválasztási lehetőségek", "gamepad_ds5": "DS5 (PS5)", "gamepad_ds5_manual": "DS5 kiválasztási lehetőségek", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Kézi DS4 opciók", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Parancsnoki előkészületek", "global_prep_cmd_desc": "Konfigurálja a bármely alkalmazás futtatása előtt vagy után végrehajtandó parancsok listáját. Ha a megadott előkészítő parancsok bármelyike sikertelen, az alkalmazás indítási folyamata megszakad.", "hevc_mode": "HEVC támogatás", "hevc_mode_0": "A Sunshine a HEVC támogatását a kódoló képességei alapján fogja hirdetni (ajánlott)", "hevc_mode_1": "A Sunshine nem fogja hirdetni a HEVC támogatását", "hevc_mode_2": "A Sunshine a HEVC fő profil támogatását fogja hirdetni", "hevc_mode_3": "A Sunshine a HEVC Main és Main10 (HDR) profilok támogatását fogja hirdetni", "hevc_mode_desc": "Lehetővé teszi az ügyfél számára, hogy HEVC Main vagy HEVC Main10 videófolyamokat kérjen. A HEVC kódolása CPU-igényesebb, ezért ennek engedélyezése csökkentheti a teljesítményt szoftveres kódolás esetén.", "high_resolution_scrolling": "Nagy felbontású görgetés támogatása", "high_resolution_scrolling_desc": "Ha engedélyezve van, a Sunshine átadja a Moonlight-ügyfelek nagy felbontású görgetési eseményeit. Ezt hasznos lehet letiltani olyan régebbi alkalmazások esetében, amelyek túl gyorsan görgetnek a nagy felbontású görgetési eseményekkel.", "install_steam_audio_drivers": "Steam audió illesztőprogramok telepítése", "install_steam_audio_drivers_desc": "Ha a Steam telepítve van, ez automatikusan telepíti a Steam Streaming Speakers illesztőprogramot az 5.1/7.1 surround hangzás és a host hang elnémításának támogatásához.", "key_repeat_delay": "Kulcsismétlés késleltetése", "key_repeat_delay_desc": "Szabályozza, hogy a billentyűk milyen gyorsan ismétlődjenek. A kezdeti késleltetés milliszekundumban a billentyűk ismétlése előtt.", "key_repeat_frequency": "Kulcs Ismétlési gyakoriság", "key_repeat_frequency_desc": "Milyen gyakran ismétlődnek a billentyűk másodpercenként. Ez a konfigurálható opció támogatja a tizedesjegyeket.", "key_rightalt_to_key_win": "A jobb Alt billentyű Windows billentyűhöz való hozzárendelése", "key_rightalt_to_key_win_desc": "Előfordulhat, hogy a Windows-kulcsot nem tudja közvetlenül a Moonlightból elküldeni. Ezekben az esetekben hasznos lehet, ha a Sunshine úgy gondolja, hogy a jobb Alt billentyű a Windows billentyű.", "keybindings": "Billentyűzetkötések", "keyboard": "Billentyűzetbemenet engedélyezése", "keyboard_desc": "Lehetővé teszi a vendégek számára, hogy a billentyűzettel irányítsák a gazdarendszert.", "lan_encryption_mode": "LAN titkosítási mód", "lan_encryption_mode_1": "Engedélyezve a támogatott kliensgépeknél", "lan_encryption_mode_2": "Minden kliensgépnek kötelező", "lan_encryption_mode_desc": "Ez határozza meg, hogy a helyi hálózaton keresztüli streaming során mikor kerül sor titkosításra. A titkosítás csökkentheti a streaming teljesítményét, különösen a kisebb teljesítményű hosztokon és klienseken.", "locale": "Területi beállítás", "locale_desc": "A Sunshine felhasználói felületén használt területi beállítás.", "log_path": "Naplófájl elérési útvonal", "log_path_desc": "Az a fájl, amelyben a Sunshine aktuális naplói tárolódnak.", "max_bitrate": "Maximális bitráta", "max_bitrate_desc": "A maximális bitráta (Kbps-ban), amellyel a Sunshine kódolni fogja a streamet. Ha 0-ra van állítva, mindig a Moonlight által kért bitrátát fogja használni.", "minimum_fps_target": "Minimális FPS cél", "minimum_fps_target_desc": "A legalacsonyabb effektív FPS, amelyet egy stream elérhet. A 0 értéket a folyam FPS-értékének nagyjából felének tekintjük. A 20-as beállítás ajánlott, ha 24 vagy 30fps sebességű tartalmat streamel.", "min_log_level": "Napló szint", "min_log_level_0": "Bővebben", "min_log_level_1": "Debug", "min_log_level_2": "Info", "min_log_level_3": "Figyelmeztetés", "min_log_level_4": "Hiba", "min_log_level_5": "Végzetes", "min_log_level_6": "Nincs", "min_log_level_desc": "A minimális naplózási szint, amely a szabványos kimenetre kerül kiírásra", "min_threads": "Minimális CPU szálszám", "min_threads_desc": "Az érték növelése kissé csökkenti a kódolás hatékonyságát, de a kompromisszum általában megéri, ha több CPU-magot használhatunk a kódoláshoz. Az ideális érték az a legalacsonyabb érték, amely megbízhatóan kódol a kívánt streaming-beállítások mellett a hardveren.", "misc": "Egyéb lehetőségek", "motion_as_ds4": "DS4 gamepad emulálása, ha a kliens gamepad mozgásérzékelők jelenlétét jelzi.", "motion_as_ds4_desc": "Ha letiltja, a mozgásérzékelőket nem veszi figyelembe a játékvezérlő típusának kiválasztásakor.", "mouse": "Egérbemenet engedélyezése", "mouse_desc": "Lehetővé teszi a vendégek számára, hogy az egérrel irányítsák a gazdarendszert.", "native_pen_touch": "Natív toll/érintés támogatás", "native_pen_touch_desc": "Ha engedélyezve van, a Sunshine átadja a Moonlight-ügyfelek natív toll/érintés eseményeit. Ezt hasznos lehet letiltani a régebbi, natív toll/érintés támogatással nem rendelkező alkalmazások esetében.", "notify_pre_releases": "Kiadás előtti értesítések", "notify_pre_releases_desc": "Akar-e értesítést kapni a Sunshine új, kiadás előtti verzióiról?", "nvenc_h264_cavlc": "A CAVLC előnyben részesítése a CABAC-kal szemben a H.264-ben", "nvenc_h264_cavlc_desc": "Az entrópia kódolás egyszerűbb formája. A CAVLC-nek körülbelül 10%-kal több bitrátára van szüksége ugyanahhoz a minőséghez. Csak nagyon régi dekódoló eszközök esetében releváns.", "nvenc_latency_over_power": "Az alacsonyabb kódolási késleltetés előnyben részesítése az energiatakarékossággal szemben", "nvenc_latency_over_power_desc": "A Sunshine a maximális GPU-órajelsebességet kéri streamelés közben, hogy csökkentse a kódolási késleltetést. Ennek kikapcsolása nem ajánlott, mivel ez jelentősen megnövekedett kódolási késleltetéshez vezethet.", "nvenc_opengl_vulkan_on_dxgi": "OpenGL/Vulkan bemutatása a DXGI tetején", "nvenc_opengl_vulkan_on_dxgi_desc": "A Sunshine nem képes teljes képernyős OpenGL és Vulkan programokat teljes képkocka sebességgel rögzíteni, hacsak nem a DXGI tetején vannak jelen. Ez egy rendszerszintű beállítás, amely a Sunshine programból való kilépéskor visszaáll.", "nvenc_preset": "Előre beállított teljesítmény", "nvenc_preset_1": "(leggyorsabb, alapértelmezett)", "nvenc_preset_7": "(leglassabb)", "nvenc_preset_desc": "A nagyobb számok javítják a tömörítést (minőséget adott bitráta mellett) a megnövekedett kódolási késleltetés árán. Csak akkor ajánlott változtatni, ha a hálózat vagy a dekóder korlátozza, egyébként hasonló hatás érhető el a bitráta növelésével.", "nvenc_realtime_hags": "Valós idejű prioritás használata hardveres gyorsított gpu ütemezésben", "nvenc_realtime_hags_desc": "Jelenleg az NVIDIA illesztőprogramok lefagyhatnak a kódolóban, ha a HAGS engedélyezve van, valós idejű prioritást használnak, és a VRAM kihasználtsága közel van a maximumhoz. Ennek az opciónak a letiltása a prioritást magasra csökkenti, így elkerülhető a lefagyás, ami a GPU nagy terhelésénél a rögzítési teljesítmény csökkenésének árán történik.", "nvenc_spatial_aq": "Térbeli AQ", "nvenc_spatial_aq_desc": "A videó lapos régióihoz magasabb QP-értékeket rendelhet. Ajánlott engedélyezni alacsonyabb bitráta mellett történő streamelés esetén.", "nvenc_twopass": "Kétszeri átjárási mód", "nvenc_twopass_desc": "Előzetes kódolási lépés hozzáadása. Ez lehetővé teszi több mozgásvektor felismerését, a bitráta jobb elosztását a képkockán belül, és a bitráta-határértékek szigorúbb betartását. A kikapcsolása nem ajánlott, mivel ez esetenként bitráta-túllépéshez és későbbi csomagvesztéshez vezethet.", "nvenc_twopass_disabled": "Kikapcsolva (leggyorsabb, nem ajánlott)", "nvenc_twopass_full_res": "Teljes felbontás (lassabb)", "nvenc_twopass_quarter_res": "Negyedes felbontás (gyorsabb, alapértelmezett)", "nvenc_vbv_increase": "Egyetlen képkocka VBV/HRD százalékos növekedése", "nvenc_vbv_increase_desc": "Alapértelmezés szerint a sunshine egykockás VBV/HRD-t használ, ami azt jelenti, hogy a kódolt videóképkocka mérete várhatóan nem haladja meg a kért bitrátát osztva a kért képkocka sebességgel. Ennek a korlátozásnak az enyhítése előnyös lehet, és alacsony késleltetésű változó bitrátaként működhet, de csomagvesztéshez is vezethet, ha a hálózatnak nincs pufferterülete a bitráta-csúcsok kezeléséhez. A maximálisan elfogadott érték 400, ami megfelel az 5x megnövelt kódolt videóképkocka felső mérethatárának.", "origin_web_ui_allowed": "Origin webes felhasználói felület Engedélyezve", "origin_web_ui_allowed_desc": "A webes felhasználói felülethez való hozzáférést nem tiltó távoli végpontcím eredete", "origin_web_ui_allowed_lan": "Csak a LAN-ban lévők férhetnek hozzá a webes felhasználói felülethez", "origin_web_ui_allowed_pc": "Csak a localhost férhet hozzá a webes felhasználói felülethez", "origin_web_ui_allowed_wan": "Bárki hozzáférhet a webes felhasználói felülethez", "output_name": "Kijelző azonosító", "output_name_desc_unix": "A Sunshine indítása során meg kell jelenítenie az észlelt kijelzők listáját. Megjegyzés: A zárójelben lévő id értéket kell használnia. Az alábbiakban egy példa látható; a tényleges kimenet a Hibaelhárítás lapon található.", "output_name_desc_windows": "A rögzítéshez használt kijelzőeszköz azonosítójának manuális megadása. Ha nincs megadva, az elsődleges kijelzőt rögzíti a rendszer. Megjegyzés: Ha fentebb GPU-t adott meg, akkor ennek a kijelzőnek ahhoz a GPU-hoz kell csatlakoznia. A Sunshine indítása során meg kell jelenítenie az észlelt kijelzők listáját. Az alábbiakban egy példa látható; a tényleges kimenet a Hibaelhárítás lapon található.", "ping_timeout": "Ping időkorlát", "ping_timeout_desc": "Mennyi ideig kell várni milliszekundumban a holdfénytől érkező adatokra a folyam leállítása előtt.", "pkey": "Privát kulcs", "pkey_desc": "A webes felhasználói felület és a Moonlight-ügyfél párosításához használt titkos kulcs. A legjobb kompatibilitás érdekében ez egy RSA-2048-as magánkulcs kell, hogy legyen.", "port": "Port", "port_alert_1": "A Sunshine nem használhat 1024 alatti portokat!", "port_alert_2": "A 65535 feletti portok nem elérhetőek!", "port_desc": "A Sunshine által használt portok családjának beállítása", "port_http_port_note": "Ezt a portot használja a Moonlighthoz való csatlakozáshoz.", "port_note": "Megjegyzés", "port_port": "Port", "port_protocol": "Protokoll", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "A webes felhasználói felület internetre való kitettsége biztonsági kockázatot jelent! Csak saját felelősségre!", "port_web_ui": "Webes felhasználói felület", "qp": "Kvantálási paraméter", "qp_desc": "Előfordulhat, hogy egyes eszközök nem támogatják a konstans bitsebességet. Ezeknél az eszközöknél a QP-t használják helyette. A magasabb érték nagyobb tömörítést, de kevesebb minőséget jelent.", "qsv_coder": "QuickSync kódoló (H264)", "qsv_preset": "QuickSync Előbeállítás", "qsv_preset_fast": "fast (alacsony minőség)", "qsv_preset_faster": "faster (alacsonyabb minőség)", "qsv_preset_medium": "medium (alapértelmezett)", "qsv_preset_slow": "slow (jó minőség)", "qsv_preset_slower": "slower (jobb minőség)", "qsv_preset_slowest": "slowest (legjobb minőség)", "qsv_preset_veryfast": "fastest (legalacsonyabb minőség)", "qsv_slow_hevc": "Lassú HEVC kódolás engedélyezése", "qsv_slow_hevc_desc": "Ez lehetővé teheti a HEVC kódolást a régebbi Intel GPU-kon, ami a GPU nagyobb kihasználtsága és rosszabb teljesítménye árán érhető el.", "restart_note": "A Sunshine újraindul, hogy alkalmazza a változtatásokat.", "stream_audio": "Stream Audio", "stream_audio_desc": "A hang streamelés vagy sem. Ennek kikapcsolása hasznos lehet fej nélküli kijelzők második monitorként történő streameléséhez.", "sunshine_name": "Sunshine-név", "sunshine_name_desc": "A Holdfény által megjelenített név. Ha nincs megadva, a számítógép hostnevét használja a rendszer", "sw_preset": "SW előbeállítások", "sw_preset_desc": "A kódolási sebesség (kódolt képkockák másodpercenként) és a tömörítési hatékonyság (minőség a bitfolyamban lévő bitenként) közötti kompromisszum optimalizálása. Alapértelmezés szerint szupergyors.", "sw_preset_fast": "fast", "sw_preset_faster": "faster", "sw_preset_medium": "medium", "sw_preset_slow": "slow", "sw_preset_slower": "slower", "sw_preset_superfast": "superfast (alapértelmezett)", "sw_preset_ultrafast": "ultrafast", "sw_preset_veryfast": "veryfast", "sw_preset_veryslow": "veryslow", "sw_tune": "SW Tune", "sw_tune_animation": "animáció -- jó rajzfilmekhez; nagyobb deblockingot és több referencia képkockát használ", "sw_tune_desc": "Hangolási lehetőségek, amelyek az előbeállítás után kerülnek alkalmazásra. Alapértelmezett beállítása nulla lappangási idő.", "sw_tune_fastdecode": "fastdecode -- gyorsabb dekódolást tesz lehetővé bizonyos szűrők kikapcsolásával", "sw_tune_film": "film -- kiváló minőségű filmtartalom esetén; csökkenti a deblockingot", "sw_tune_grain": "grain -- megőrzi a szemcseszerkezetet a régi, szemcsés filmanyagban", "sw_tune_stillimage": "stillimage -- jó diavetítésszerű tartalomhoz", "sw_tune_zerolatency": "zerolatency -- gyors kódoláshoz és alacsony késleltetésű streaminghez jó (alapértelmezett)", "system_tray": "Rendszertálca engedélyezése", "system_tray_desc": "ikon megjelenítése a tálcán és asztali értesítések megjelenítése", "touchpad_as_ds4": "DS4 gamepad emulálása, ha a kliensgép gamepad érintőtábla jelenlétét jelzi", "touchpad_as_ds4_desc": "Ha letiltja, az érintőpad jelenlétét nem veszi figyelembe a rendszer a gamepad típusának kiválasztásakor.", "upnp": "UPnP", "upnp_desc": "Porttovábbítás automatikus konfigurálása az interneten keresztüli streaminghez", "vaapi_strict_rc_buffer": "A H.264/HEVC képkocka-bitráta korlátok szigorú betartása AMD GPU-kon", "vaapi_strict_rc_buffer_desc": "Ha engedélyezi ezt a beállítást, elkerülheti a hálózaton keresztül történő képkocka kiesést a jelenetváltások során, de a videó minősége csökkenhet mozgás közben.", "virtual_sink": "Virtuális mosogató", "virtual_sink_desc": "Kézzel adja meg a használni kívánt virtuális audioeszközt. Ha nincs megadva, az eszköz automatikusan kiválasztásra kerül. Az automatikus eszközkiválasztás használatához erősen ajánlott üresen hagyni ezt a mezőt!", "virtual_sink_placeholder": "Steam streaming hangszórók", "vt_coder": "VideoToolbox kódoló", "vt_realtime": "VideoToolbox valós idejű kódolás", "vt_software": "VideoToolbox szoftveres kódolás", "vt_software_allowed": "Engedélyezett", "vt_software_forced": "Kényszerített", "wan_encryption_mode": "WAN titkosítási mód", "wan_encryption_mode_1": "Engedélyezve a támogatott kliensgépeken (alapértelmezett)", "wan_encryption_mode_2": "Minden kliensgép számára kötelező", "wan_encryption_mode_desc": "Ez határozza meg, hogy az interneten keresztüli streaming során mikor kerül sor titkosításra. A titkosítás csökkentheti a streaming teljesítményét, különösen a kisebb teljesítményű hosztokon és klienseken." }, "index": { "description": "A Sunshine a Moonlight saját szervezésű játékstream hostja.", "download": "Letöltés", "installed_version_not_stable": "Ön a Sunshine kiadás előtti verzióját futtatja. Előfordulhatnak hibák vagy egyéb problémák. Kérjük, jelentse a felmerülő problémákat. Köszönjük, hogy segít a Sunshine jobbá tételében!", "loading_latest": "Legújabb kiadás betöltése...", "new_pre_release": "Elérhető egy új előzetes verzió!", "new_stable": "Elérhető egy új stabil verzió!", "startup_errors": "Figyelem! A Sunshine ezeket a hibákat észlelte az indítás során. KIFEJEZETTEN AJÁNLJUK ezek kijavítását a streamelés előtt.", "version_dirty": "Köszönjük, hogy segítesz a Sunshine jobb szoftverré tételében!", "version_latest": "A Sunshine legújabb verzióját futtatod", "welcome": "Helló, Sunshine!" }, "navbar": { "applications": "Alkalmazások", "configuration": "Konfiguráció", "home": "Home", "password": "Jelszó módosítása", "pin": "PIN", "theme_auto": "Auto", "theme_dark": "Sötét", "theme_light": "Világos", "toggle_theme": "Téma", "troubleshoot": "Hibaelhárítás" }, "password": { "confirm_password": "Jelszó megerősítése", "current_creds": "Jelenlegi megbízólevelek", "new_creds": "Új megbízólevelek", "new_username_desc": "Ha nincs megadva, a felhasználónév nem fog változni.", "password_change": "Jelszó módosítása", "success_msg": "A jelszó módosítása sikeresen megtörtént! Ez az oldal hamarosan újratöltődik, a böngésző kérni fogja az új hitelesítő adatokat." }, "pin": { "device_name": "Eszköz neve", "pair_failure": "A párosítás sikertelen: Ellenőrizze, hogy a PIN kód helyesen lett-e beírva", "pair_success": "Siker! Kérjük, ellenőrizze a Holdfényt a folytatáshoz", "pin_pairing": "PIN párosítás", "send": "Küldés", "warning_msg": "Győződjön meg róla, hogy hozzáférése van a párosított ügyfélhez. Ez a szoftver teljes irányítást adhat a számítógépének, ezért legyen óvatos!" }, "resource_card": { "github_discussions": "GitHub Discussions", "legal": "Jogi", "legal_desc": "A szoftver további használatával Ön elfogadja a következő dokumentumokban foglalt feltételeket.", "license": "Licenc", "lizardbyte_website": "LizardByte honlapja", "resources": "Források", "resources_desc": "Források a Sunshine-hoz!", "third_party_notice": "Harmadik fél közleménye" }, "troubleshooting": { "dd_reset": "Tartós kijelzőkészülék beállításainak visszaállítása", "dd_reset_desc": "Ha a Sunshine elakad a módosított kijelzőeszköz-beállítások visszaállítása során, akkor visszaállíthatja a beállításokat, és manuálisan folytathatja a kijelző állapotának visszaállítását.", "dd_reset_error": "Hiba a perzisztencia visszaállítása közben!", "dd_reset_success": "Sikeres visszaállítása kitartás!", "force_close": "Bezárás erőltetése", "force_close_desc": "Ha a Moonlight panaszt tesz egy jelenleg futó alkalmazás miatt, az alkalmazás kényszerített bezárása megoldja a problémát.", "force_close_error": "Hiba az alkalmazás bezárásakor", "force_close_success": "Az alkalmazás bezárása sikeres!", "logs": "Naplók", "logs_desc": "A Sunshine által feltöltött naplók megtekintése", "logs_find": "Keresés...", "restart_sunshine": "Sunshine újraindítása", "restart_sunshine_desc": "Ha a Sunshine nem működik megfelelően, próbálja meg újraindítani. Ez megszakítja a futó munkameneteket.", "restart_sunshine_success": "A Sunshine újraindul", "troubleshooting": "Hibaelhárítás", "unpair_all": "Unpair All", "unpair_all_error": "Hiba a párosítás feloldásakor", "unpair_all_success": "Minden eszköz nincs párosítva.", "unpair_desc": "Távolítsa el a párosított eszközöket. Az aktív munkamenettel rendelkező, különállóan nem párosított eszközök továbbra is kapcsolatban maradnak, de nem tudnak munkamenetet indítani vagy folytatni.", "unpair_single_no_devices": "Nincsenek párosított eszközök.", "unpair_single_success": "Előfordulhat azonban, hogy az eszköz(ök) még mindig aktív munkamenetben van(nak). A fenti \"Force Close\" (Bezárás kikényszerítése) gomb segítségével fejezze be a nyitott munkameneteket.", "unpair_single_unknown": "Ismeretlen kliens", "unpair_title": "Eszközök párosításának feloldása" }, "welcome": { "confirm_password": "Jelszó megerősítése", "create_creds": "Mielőtt elkezdené, új felhasználónevet és jelszót kell létrehoznia a webes felhasználói felülethez való hozzáféréshez.", "create_creds_alert": "A Sunshine webes felhasználói felületének eléréséhez az alábbi hitelesítő adatokra van szükség. Tartsa őket biztonságban, mivel soha többé nem fogja látni őket!", "greeting": "Üdvözlünk a Sunshine-ban!", "login": "Bejelentkezés", "welcome_success": "Ez az oldal hamarosan újratöltődik, és a böngésző kérni fogja az új hitelesítő adatokat." } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/it.json ================================================ { "_common": { "apply": "Applica", "auto": "Automatico", "autodetect": "Rilevamento automatico (consigliato)", "beta": "(beta)", "cancel": "Annulla", "disabled": "Disattivato", "disabled_def": "Disabilitato (predefinito)", "disabled_def_cbox": "Predefinito: deselezionato", "dismiss": "Ignora", "do_cmd": "Esegui Comando", "elevated": "Come Admin", "enabled": "Abilitato", "enabled_def": "Abilitato (predefinito)", "enabled_def_cbox": "Predefinito: controllato", "error": "Errore!", "note": "Nota:", "password": "Password", "run_as": "Esegui come amministratore", "save": "Salva", "see_more": "Vedi Altro", "success": "Operazione riuscita!", "undo_cmd": "Comando di Annullamento", "username": "Nome utente", "warning": "Attenzione!" }, "apps": { "actions": "Azioni", "add_cmds": "Aggiungi comandi", "add_new": "Aggiungi nuovo", "app_name": "Nome applicazione", "app_name_desc": "Il Nome dell'applicazione, come mostrato su Moonlight", "applications_desc": "Le applicazioni vengono aggiornate solo al riavvio del client", "applications_title": "Applicazioni", "auto_detach": "Continua lo streaming se l'applicazione chiude improvvisamente", "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.", "cmd": "Comando", "cmd_desc": "L'applicazione principale da avviare. Se vuoto, non verrà avviata alcuna applicazione.", "cmd_note": "Se il percorso del comando eseguibile contiene spazi, è necessario racchiuderlo tra doppie virgolette.", "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.", "cmd_prep_name": "Comandi Preliminari", "covers_found": "Copertine trovate", "delete": "Cancella", "detached_cmds": "Comandi Separati", "detached_cmds_add": "Aggiungi comando separato", "detached_cmds_desc": "Un elenco di comandi da eseguire in background.", "detached_cmds_note": "Se il percorso del comando eseguibile contiene spazi, è necessario racchiuderlo tra doppie virgolette.", "edit": "Modifica", "env_app_id": "ID dell'applicazione", "env_app_name": "Nome app", "env_client_audio_config": "La configurazione audio richiesta dal client (2.0/5.1/7.1)", "env_client_enable_sops": "Se il client ha richiesto l'opzione \"Ottimizza le impostazioni del gioco per lo streaming\" (TRUE o FALSE)", "env_client_fps": "FPS richiesti dal client (float)", "env_client_gcmap": "La maschera del gamepad richiesta, in formato bitset/bitfield (int)", "env_client_hdr": "Se L'HDR è abilitato dal client (TRUE o FALSE)", "env_client_height": "L'altezza richiesta dal client (int)", "env_client_host_audio": "Se Il client ha richiesto l'audio dell'host (TRUE o FALSE)", "env_client_width": "La larghezza richiesta dal client (int)", "env_displayplacer_example": "Esempio - displayplacer per l'automazione della risoluzione:", "env_qres_example": "Esempio - QRes per l'automazione della risoluzione:", "env_qres_path": "percorso di QRes", "env_var_name": "Nome Variable", "env_vars_about": "Informazioni sulle variabili d'ambiente", "env_vars_desc": "Tutti i comandi ottengono queste variabili d'ambiente per impostazione predefinita:", "env_xrandr_example": "Esempio - Xrandr per l'automazione della risoluzione:", "exit_timeout": "Timeout Uscita", "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.", "find_cover": "Trova Copertina", "global_prep_desc": "Abilita/Disabilita l'esecuzione dei Comandi di Preparazione Globali per questa applicazione.", "global_prep_name": "Comandi di Preparazione Globali", "image": "Immagine", "image_desc": "Percorso dell'immagine/icona/foto che verrà inviata al client. L'immagine deve essere un file PNG. Se non impostata, Apollo invierà l'immagine predefinita.", "loading": "Caricamento...", "name": "Nome", "output_desc": "Il file dove viene memorizzato l'output del comando, se non specificato, l'output viene ignorato", "output_name": "Output", "run_as_desc": "Questo può essere necessario per alcune applicazioni che richiedono permessi di amministratore per funzionare correttamente.", "wait_all": "Continua lo streaming fino all'uscita di tutti i processi dell'app", "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.", "working_dir": "Directory di Lavoro", "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, Apollo userà come predefinita la directory padre del comando" }, "config": { "adapter_name": "Nome Adattatore", "adapter_name_desc_linux_1": "Specifica manualmente una GPU da usare per la cattura.", "adapter_name_desc_linux_2": "per trovare tutti i dispositivi con capacità VAAPI", "adapter_name_desc_linux_3": "Sostituisci ``renderD129`` con il dispositivo sopra per elencare il nome e le funzionalità del dispositivo. Per essere supportato da Apollo, ha bisogno di avere minimo:", "adapter_name_desc_windows": "Specifica manualmente una GPU da usare per la cattura. Se lascato vuoto, la GPU viene scelta automaticamente. Raccomandiamo vivamente di lasciare vuoto questo campo per utilizzare la selezione automatica della GPU! Nota: questa GPU deve avere un display connesso e acceso. I valori appropriati possono essere trovati usando il seguente comando:", "adapter_name_placeholder_windows": "Radeon RX 580 Series", "add": "Aggiungi", "address_family": "Famiglia di indirizzi", "address_family_both": "IPv4+IPv6", "address_family_desc": "Imposta la famiglia di indirizzi utilizzata da Apollo", "address_family_ipv4": "Solo IPv4", "always_send_scancodes": "Invia sempre Scancode", "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.", "amd_coder": "Coder AMF (H264)", "amd_coder_desc": "Consente di selezionare la codifica dell'entropia per dare la priorità alla qualità o alla velocità di codifica. Solo H.264.", "amd_enforce_hrd": "Applica Decoder di Riferimento Ipotetico (HRD) per AMF", "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.", "amd_preanalysis": "Preanalisi AMF", "amd_preanalysis_desc": "Ciò consente la preanalisi nel controllo della velocità, che può aumentare la qualità a scapito di una maggiore latenza di codifica.", "amd_quality": "Qualità AMF", "amd_quality_balanced": "bilanciato -- bilanciato (predefinito)", "amd_quality_desc": "Questo controlla l'equilibrio tra velocità di codifica e qualità.", "amd_quality_group": "Impostazioni Qualità AMF", "amd_quality_quality": "qualità -- preferisce la qualità", "amd_quality_speed": "velocità -- preferisce la velocità", "amd_rc": "Controllo della Velocità AMF", "amd_rc_cbr": "cbr -- bitrate costante", "amd_rc_cqp": "cqp -- modalità qp costante", "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.", "amd_rc_group": "Impostazioni Controllo di Velocità AMF", "amd_rc_vbr_latency": "vbr_latency -- bitrate variabile con vincoli di latenza", "amd_rc_vbr_peak": "vbr_peak -- bitrate variabile con vincoli di picco", "amd_usage": "Utilizzo AMF", "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.", "amd_usage_lowlatency": "lowlatency - bassa latenza (più veloce)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - bassa latenza, alta qualità (veloce)", "amd_usage_transcoding": "transcoding -- transcodifica (più lenta)", "amd_usage_ultralowlatency": "ultralowlatency - latenza ultra bassa (più veloce)", "amd_usage_webcam": "webcam -- webcam (lento)", "amd_vbaq": "Quantizzazione Adattiva Basata Sulla Varianza AMF (VBAQ)", "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.", "apply_note": "Fare clic su 'Applica' per applicare le modifiche e riavviare Apollo. Questo terminerà qualsiasi sessione in esecuzione.", "audio_sink": "Uscita Audio", "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:", "audio_sink_desc_macos": "Il nome dell'uscita audio utilizzata per Audio Loopback. Apollo può accedere solo ai microfoni su macOS a causa delle limitazioni di sistema. Puoi usare Soundflower o BlackHole per trasmettere l'audio di sistema.", "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:", "audio_sink_placeholder_macos": "BlackHole 2ch", "audio_sink_placeholder_windows": "Altoparlanti (High Definition Audio Device)", "av1_mode": "Supporto AV1", "av1_mode_0": "Apollo fornirà il supporto per AV1 basandosi sulle funzionalità dell'encoder (raccomandato)", "av1_mode_1": "Apollo non fornirà il supporto per AV1", "av1_mode_2": "Apollo fornirà il supporto per il profilo AV1 Main a 8 bit", "av1_mode_3": "Apollo fornirà il supporto per i profili AV1 Main a 8 bit e 10 bit (HDR)", "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.", "back_button_timeout": "Timeout Emulazione Home/Tasto Guida", "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.", "capture": "Forza un metodo di acquisizione specifico", "capture_desc": "In modalità automatica Apollo userà il primo che funziona. NvFBC richiede driver nvidia patchati.", "cert": "Certificato", "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.", "channels": "Massimi Client Connessi", "channels_desc_1": "Apollo può consentire che una singola sessione di streaming sia condivisa con più client contemporaneamente.", "channels_desc_2": "Alcuni encoder hardware possono avere limitazioni che riducono le prestazioni con più flussi.", "coder_cabac": "cabac -- codifica aritmetica binaria adattiva contestuale - qualità superiore", "coder_cavlc": "cavlc -- codifica contestuale adattativa a lunghezza variabile - decodifica più veloce", "configuration": "Configurazione", "controller": "Abilita l'input del Gamepad", "controller_desc": "Permette ai guest di controllare il sistema host con un gamepad / controller", "credentials_file": "File Credenziali", "credentials_file_desc": "Memorizza il nome utente/password separatamente dal file di stato di Apollo.", "dd_config_ensure_active": "Attiva automaticamente il display", "dd_config_ensure_only_display": "Disattiva altri display e attiva solo il display specificato", "dd_config_ensure_primary": "Attivare automaticamente il display e renderlo uno schermo primario", "dd_config_label": "Configurazione dispositivo", "dd_config_revert_delay": "Ritardo ripristino configurazione", "dd_config_revert_delay_desc": "Ulteriori ritardi in millisecondi per attendere prima di ripristinare la configurazione quando l'app è stata chiusa o l'ultima sessione è terminata. Lo scopo principale è quello di fornire una transizione più fluida quando si passa rapidamente tra le applicazioni.", "dd_config_verify_only": "Verifica che il display sia abilitato", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "Attiva/disattiva la modalità HDR come richiesto dal client (predefinito)", "dd_hdr_option_disabled": "Non modificare le impostazioni HDR", "dd_mode_remapping": "Modalità di visualizzazione remapping", "dd_mode_remapping_add": "Aggiungi voce di remapping", "dd_mode_remapping_desc_1": "Specificare le voci di remapping per modificare la risoluzione richiesta e/o la frequenza di aggiornamento ad altri valori.", "dd_mode_remapping_desc_2": "L'elenco è iterato dall'alto verso il basso e viene usata la prima partita.", "dd_mode_remapping_desc_3": "I campi \"Richiesti\" possono essere lasciati vuoti per corrispondere a qualsiasi valore richiesto.", "dd_mode_remapping_desc_4_final_values_mixed": "Almeno un campo \"Finale\" deve essere specificato. La risoluzione o la frequenza di aggiornamento non specificata non saranno modificate.", "dd_mode_remapping_desc_4_final_values_non_mixed": "Il campo \"Finale\" deve essere specificato e non può essere vuoto.", "dd_mode_remapping_desc_5_sops_mixed_only": "L'opzione \"Ottimizza le impostazioni di gioco\" deve essere abilitata nel client Moonlight, altrimenti le voci con tutti i campi di risoluzione specificati vengono saltate.", "dd_mode_remapping_desc_5_sops_resolution_only": "L'opzione \"Ottimizza le impostazioni di gioco\" deve essere abilitata nel client Moonlight, altrimenti la mappatura viene saltata.", "dd_mode_remapping_final_refresh_rate": "Frequenza di aggiornamento finale", "dd_mode_remapping_final_resolution": "Risoluzione finale", "dd_mode_remapping_requested_fps": "FPS Richiesto", "dd_mode_remapping_requested_resolution": "Risoluzione richiesta", "dd_options_header": "Opzioni avanzate del dispositivo di visualizzazione", "dd_refresh_rate_option": "Velocità di aggiornamento", "dd_refresh_rate_option_auto": "Usa il valore FPS fornito dal client (predefinito)", "dd_refresh_rate_option_disabled": "Non modificare la frequenza di aggiornamento", "dd_refresh_rate_option_manual": "Usa la frequenza di aggiornamento inserita manualmente", "dd_refresh_rate_option_manual_desc": "Inserisci la frequenza di aggiornamento da usare", "dd_resolution_option": "Risoluzione", "dd_resolution_option_auto": "Usa la risoluzione fornita dal client (predefinito)", "dd_resolution_option_disabled": "Non modificare la risoluzione", "dd_resolution_option_manual": "Usa la risoluzione inserita manualmente", "dd_resolution_option_manual_desc": "Inserisci la risoluzione da usare", "dd_resolution_option_ogs_desc": "L'opzione \"Ottimizza le impostazioni di gioco\" deve essere abilitata sul client Moonlight perché questo funzioni.", "dd_wa_hdr_toggle_desc": "Quando si utilizza il dispositivo di visualizzazione virtuale come per lo streaming, potrebbe mostrare un colore HDR errato. Con questa opzione abilitata, Apollo cercherà di mitigare questo problema.", "dd_wa_hdr_toggle": "Abilita workaround ad alto contrasto per HDR", "ds4_back_as_touchpad_click": "Mappa Indietro/Select come Clic Touchpad", "ds4_back_as_touchpad_click_desc": "Quando si forza l'emulazione DS4, mappa Indietro/Select come Clic Touchpad", "encoder": "Forza un encoder specifico", "encoder_desc": "Forza un encoder specifico, altrimenti Apollo selezionerà l'opzione migliore disponibile. Nota: Se si specifica un codificatore hardware su Windows, deve corrispondere alla GPU a cui è collegato il display.", "encoder_software": "Software", "external_ip": "IP Esterno", "external_ip_desc": "Se non viene fornito alcun indirizzo IP esterno, Apollo lo rileverà automaticamente", "fec_percentage": "Percentuale FEC", "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.", "ffmpeg_auto": "auto -- lascia che decida ffmpeg (predefinito)", "file_apps": "File Applicazioni", "file_apps_desc": "Il file in cui vengono memorizzate le attuali applicazioni di Apollo.", "file_state": "File Stato", "file_state_desc": "Il file in cui viene memorizzato lo stato attuale di Apollo", "gamepad": "Tipo di Gamepad Emulato", "gamepad_auto": "Opzioni di selezione automatica", "gamepad_desc": "Scegli quale tipo di gamepad emulare sull'host", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "Opzioni di selezione DS4", "gamepad_ds5": "DS5 (PS5)", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Opzioni manuali DS4", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Comandi di preparazione", "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.", "hevc_mode": "Supporto HEVC", "hevc_mode_0": "Apollo fornirà il supporto per HEVC basandosi sulle funzionalità dell'encoder (raccomandato)", "hevc_mode_1": "Apollo non fornirà il supporto per HEVC", "hevc_mode_2": "Apollo fornirà il supporto per il profilo HEVC Main", "hevc_mode_3": "Apollo fornirà il supporto per i profili HEVC Main e Main10 (HDR)", "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.", "high_resolution_scrolling": "Supporto Scorrimento Mouse ad Alta Risoluzione", "high_resolution_scrolling_desc": "Quando abilitato, Apollo 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.", "install_steam_audio_drivers": "Installa i Driver Audio di Steam", "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.", "key_repeat_delay": "Ritardo Ripetizione Tasti", "key_repeat_delay_desc": "Controlla quanto velocemente i tasti si ripeteranno. È Il ritardo iniziale in millisecondi prima di ripetere i tasti.", "key_repeat_frequency": "Frequenza Di Ripetizione Tasti", "key_repeat_frequency_desc": "Quante volte i tasti si ripetono ogni secondo. Questa opzione supporta i decimali.", "key_rightalt_to_key_win": "Mappare il tasto Alt destro sul tasto Windows", "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 Apollo che il tasto Alt Destro è il Tasto Windows", "keyboard": "Abilita Input da Tastiera", "keyboard_desc": "Consente ai guest di controllare il sistema host con la tastiera", "lan_encryption_mode": "Modalità Crittografia LAN", "lan_encryption_mode_1": "Abilitato per i client supportati", "lan_encryption_mode_2": "Obbligatorio per tutti i client", "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.", "locale": "Lingua", "locale_desc": "La lingua utilizzata per l'interfaccia utente di Apollo.", "log_level": "Livello di Log", "log_level_0": "Dettagliato", "log_level_1": "Debug", "log_level_2": "Informazioni", "log_level_3": "Avviso", "log_level_4": "Errore", "log_level_5": "Critico", "log_level_6": "Nessuno", "log_level_desc": "Il livello minimo di log sullo standard output", "log_path": "Percorso File Di Log", "log_path_desc": "Il file in cui vengono memorizzati i log attuali di Apollo.", "min_fps_factor": "Fattore FPS Minimo", "min_fps_factor_desc": "Apollo userà questo fattore per calcolare il tempo minimo tra i frame. Aumentare leggermente questo valore può aiutare durante lo streaming per lo più di contenuti statici. Valori più alti consumeranno più larghezza di banda.", "min_threads": "Conteggio Minimo Thread CPU", "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.", "misc": "Opzioni varie", "motion_as_ds4": "Emula un gamepad DS4 se quello del client segnala che ci sono sensori di movimento", "motion_as_ds4_desc": "Se disabilitato, i sensori di movimento non saranno presi in considerazione durante la selezione del tipo gamepad.", "mouse": "Abilita l'Input del Mouse", "mouse_desc": "Permette ai guest di controllare il sistema host con il mouse", "native_pen_touch": "Supporto Nativo Della Penna/Touch", "native_pen_touch_desc": "Se abilitato, Apollo 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.", "notify_pre_releases": "Notifiche Pre-Rilascio", "notify_pre_releases_desc": "Indica se notificare o meno le nuove versioni pre-rilascio di Apollo", "nvenc_h264_cavlc": "Preferisci CAVLC a CABAC in H.264", "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.", "nvenc_latency_over_power": "Prioritizza una latenza di codifica più bassa rispetto al risparmio energetico", "nvenc_latency_over_power_desc": "Apollo 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.", "nvenc_opengl_vulkan_on_dxgi": "Presenta OpenGL/Vulkan sopra DXGI", "nvenc_opengl_vulkan_on_dxgi_desc": "Apollo 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 Apollo.", "nvenc_preset": "Preimpostazione prestazioni", "nvenc_preset_1": "(più veloce, predefinito)", "nvenc_preset_7": "(più lento)", "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.", "nvenc_realtime_hags": "Usa la priorità in tempo reale nello scheduling hardware dell'accelerazione GPU", "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.", "nvenc_spatial_aq": "AQ spaziale", "nvenc_spatial_aq_desc": "Assegna valori QP più alti alle parti piatte del video. È consigliato abilitarlo per lo streaming a bitrate più bassi.", "nvenc_spatial_aq_disabled": "Disabilitato (più veloce, predefinito)", "nvenc_spatial_aq_enabled": "Abilitato (più lento)", "nvenc_twopass": "Modalità a due passaggi", "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.", "nvenc_twopass_disabled": "Disabilitato (più veloce, non consigliato)", "nvenc_twopass_full_res": "Risoluzione completa (lenta)", "nvenc_twopass_quarter_res": "Un quarto di risoluzione (più veloce, predefinito)", "nvenc_vbv_increase": "Incremento percentuale VBV/HRD singolo frame", "nvenc_vbv_increase_desc": "Per impostazione predefinita, Apollo 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.", "origin_web_ui_allowed": "Origine Web UI Consentita", "origin_web_ui_allowed_desc": "L'origine dell'indirizzo di endpoint remoto a cui viene consentito l'accesso all'interfaccia utente Web", "origin_web_ui_allowed_lan": "Solo quelli in LAN possono accedere all'interfaccia utente Web", "origin_web_ui_allowed_pc": "Solo localhost può accedere all'interfaccia Web", "origin_web_ui_allowed_wan": "Chiunque può accedere all'interfaccia Web", "output_name_desc_unix": "Durante l'avvio di Apollo, 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\".", "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:", "output_name_unix": "Numero di Display", "output_name_windows": "Nome Output", "ping_timeout": "Timeout Ping", "ping_timeout_desc": "Per quanti millisecondi attendere dati da Moonlight prima di chiudere lo streaming", "pkey": "Chiave Privata", "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.", "port": "Porta", "port_alert_1": "Apollo non può utilizzare porte sotto 1024!", "port_alert_2": "I porti sopra 65535 non sono disponibili!", "port_desc": "Imposta la famiglia di porte utilizzati da Apollo", "port_http_port_note": "Usa questa porta per connetterti con Moonlight.", "port_note": "Nota", "port_port": "Porta", "port_protocol": "Protocollo", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "Esporre l'interfaccia Web su Internet è un rischio per la sicurezza! Procedi a tuo rischio!", "port_web_ui": "Web UI", "qp": "Parametro Di Quantizzazione", "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à.", "qsv_coder": "Coder QuickSync (H264)", "qsv_preset": "Preimpostazione QuickSync", "qsv_preset_fast": "più veloce (qualità inferiore)", "qsv_preset_faster": "più veloce (qualità minima)", "qsv_preset_medium": "medio (predefinito)", "qsv_preset_slow": "lento (buona qualità)", "qsv_preset_slower": "più lento (migliore qualità)", "qsv_preset_slowest": "più lento (migliore qualità)", "qsv_preset_veryfast": "ancora più veloce (qualità minima)", "qsv_slow_hevc": "Permetti la codifica lenta in HEVC", "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.", "restart_note": "Apollo sta riavviando per applicare le modifiche.", "sunshine_name": "Nome Apollo", "sunshine_name_desc": "Il nome visualizzato da Moonlight. Se non specificato, viene utilizzato il nome host del PC", "sw_preset": "Preset SW", "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.", "sw_preset_fast": "veloce", "sw_preset_faster": "più veloce", "sw_preset_medium": "medio", "sw_preset_slow": "lento", "sw_preset_slower": "più lento", "sw_preset_superfast": "superveloce (predefinito)", "sw_preset_ultrafast": "ultra veloce", "sw_preset_veryfast": "molto veloce", "sw_preset_veryslow": "molto lento", "sw_tune": "Rifinimento SW", "sw_tune_animation": "animazione -- buona per i cartoni animati; utilizza un maggiore de-blocking e più frame di riferimento", "sw_tune_desc": "Opzioni di rifinimento, che vengono applicate dopo la preimpostazione. Predefinite a zerolatency.", "sw_tune_fastdecode": "fastdecode -- permette una decodifica più veloce disabilitando alcuni filtri", "sw_tune_film": "film -- uso per contenuti cinematografici di alta qualità; riduce il deblocking", "sw_tune_grain": "grain -- conserva la struttura della grana nel vecchio materiale di film", "sw_tune_stillimage": "stillimage -- buono per contenuti simili alle presentazioni", "sw_tune_zerolatency": "zerolatency -- buono per la codifica veloce e lo streaming a bassa latenza (predefinito)", "touchpad_as_ds4": "Emula un gamepad DS4 se il gamepad client segnala che un touchpad è presente", "touchpad_as_ds4_desc": "Se disabilitata, la presenza del touchpad non sarà presa in considerazione durante la selezione del tipo del gamepad.", "upnp": "UPnP", "upnp_desc": "Configura automaticamente l'inoltro delle porte per lo streaming su Internet", "vaapi_strict_rc_buffer": "Applicare rigorosamente i limiti di bitrate frame per H.264/HEVC su GPU AMD", "vaapi_strict_rc_buffer_desc": "Abilitare questa opzione può evitare di cadere quadri sulla rete durante i cambiamenti di scena, ma la qualità video può essere ridotta durante il movimento.", "virtual_sink": "Uscita Audio Virtuale", "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!", "virtual_sink_placeholder": "Altoparlanti Streaming Di Steam", "vt_coder": "Coder VideoToolbox", "vt_realtime": "Codifica VideoToolbox In Tempo Reale", "vt_software": "Codifica Software VideoToolbox", "vt_software_allowed": "Consentita", "vt_software_forced": "Forzata", "wan_encryption_mode": "Modalità Crittografia WAN", "wan_encryption_mode_1": "Abilitato per i client supportati (predefinito)", "wan_encryption_mode_2": "Obbligatorio per tutti i client", "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." }, "index": { "description": "Apollo è una piattaforma autonoma di game stream per Moonlight.", "download": "Download", "installed_version_not_stable": "Stai eseguendo una versione in anteprima di Apollo. Potresti riscontrare bug o altri problemi. Si prega di segnalare eventuali problemi incontrati. Grazie per aver contribuito a rendere Apollo un software migliore!", "loading_latest": "Caricamento dell'ultima versione...", "new_pre_release": "Una nuova versione pre-rilascio è disponibile!", "new_stable": "Una nuova versione Stabile è disponibile!", "startup_errors": "Attenzione! Apollo ha rilevato questi errori durante l'avvio. Ti raccomandamo vivamente di risolverli prima dello streaming.", "version_dirty": "Grazie per aver contribuito a rendere Apollo un software migliore!", "version_latest": "Stai eseguendo l'ultima versione di Apollo", "welcome": "Ciao, Apollo!" }, "navbar": { "applications": "Applicazioni", "configuration": "Configurazione", "home": "Home", "password": "Modifica Password", "pin": "Pin", "theme_auto": "Automatico", "theme_dark": "Scuro", "theme_light": "Chiaro", "toggle_theme": "Tema", "troubleshoot": "Risoluzione Dei Problemi" }, "password": { "confirm_password": "Conferma Password", "current_creds": "Credenziali Attuali", "new_creds": "Nuove credenziali", "new_username_desc": "Se non specificato, il nome utente non cambierà", "password_change": "Cambio Password", "success_msg": "La password è stata modificata con successo! Questa pagina verrà ricaricata presto, il tuo browser ti chiederà le nuove credenziali." }, "pin": { "device_name": "Nome del Dispositivo", "pair_failure": "Accoppiamento non riuscito: verificare se il PIN è digitato correttamente", "pair_success": "Fatto! Controlla Moonlight per proseguire", "pin_pairing": "Accoppiamento con PIN", "send": "Invia", "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!" }, "resource_card": { "github_discussions": "Discussioni di GitHub", "legal": "Info legali", "legal_desc": "Continuando a utilizzare questo software si accettano i termini e le condizioni riportati nei seguenti documenti.", "license": "Licenza", "lizardbyte_website": "Sito web di LizardByte", "resources": "Risorse", "resources_desc": "Risorse per Apollo!", "third_party_notice": "Avvisi di terze parti" }, "troubleshooting": { "dd_reset": "Ripristina Impostazioni Del Dispositivo Di Visualizzazione Persistente", "dd_reset_desc": "Se Apollo è bloccato cercando di ripristinare le impostazioni del dispositivo di visualizzazione modificate, è possibile ripristinare le impostazioni e procedere a ripristinare lo stato di visualizzazione manualmente.", "dd_reset_error": "Errore durante il ripristino della persistenza!", "dd_reset_success": "Ripristino persistenza riuscito!", "force_close": "Chiusura forzata", "force_close_desc": "Se Moonlight si lamenta di un'app attualmente in esecuzione, la chiusura forzata dell'app dovrebbe risolvere il problema.", "force_close_error": "Errore durante la chiusura dell'applicazione", "force_close_success": "Applicazione chiusa con successo!", "logs": "Log", "logs_desc": "Vedi i log caricati da Apollo", "logs_find": "Trova...", "restart_Apollo": "Riavvia Apollo", "restart_Apollo_desc": "Se Apollo non funziona correttamente, puoi provare a riavviarlo. Questo terminerà qualsiasi sessione in esecuzione.", "restart_Apollo_success": "Apollo sta riavviando", "troubleshooting": "Risoluzione dei Problemi", "unpair_all": "Rimuovi Tutto", "unpair_all_error": "Errore durante la rimozione", "unpair_all_success": "Rimozione Riuscita.", "unpair_desc": "Rimuove i dispositivi accoppiati. I dispositivi separati con una sessione attiva rimarranno collegati, ma non potranno avviare o riprendere una sessione.", "unpair_single_no_devices": "Non ci sono dispositivi accoppiati.", "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.", "unpair_single_unknown": "Client Sconosciuto", "unpair_title": "Rimuovi Dispositivi" }, "welcome": { "confirm_password": "Conferma password", "create_creds": "Prima di iniziare, è necessario creare un nuovo nome utente e una nuova password per accedere all'interfaccia web.", "create_creds_alert": "Le credenziali in basso sono necessarie per accedere all'interfaccia web di Apollo. Tienile al sicuro, poichè non potrai più visualizzarle!", "greeting": "Benvenuto in Apollo!", "login": "Login", "welcome_success": "Questa pagina verrà ricaricata, il tuo browser ti richiederà le nuove credenziali" } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/ja.json ================================================ { "_common": { "apply": "適用", "auto": "自動", "autodetect": "自動検出 (推奨)", "beta": "(ベータ版)", "cancel": "キャンセル", "disabled": "無効", "disabled_def": "無効 (デフォルト)", "disabled_def_cbox": "デフォルト: 未チェック", "dismiss": "却下する", "do_cmd": "コマンド実行", "elevated": "管理者として実行", "enabled": "有効", "enabled_def": "有効 (デフォルト)", "enabled_def_cbox": "デフォルト: checked", "error": "エラー!", "note": "メモ:", "password": "パスワード", "run_as": "管理者として実行", "save": "保存", "see_more": "もっと見る", "success": "成功!", "undo_cmd": "元に戻す", "username": "ユーザー名", "warning": "警告!" }, "apps": { "actions": "アクション", "add_cmds": "コマンドを追加", "add_new": "新規追加", "app_name": "アプリケーション名", "app_name_desc": "アプリケーション名(Moonlight に表示させるもの)", "applications_desc": "アプリケーション一覧はクライアントの再起動時にのみ更新されます", "applications_title": "アプリケーション一覧", "auto_detach": "アプリケーションが一瞬終了してもストリーミングを続行します", "auto_detach_desc": "これにより、別のプログラムまたは別インスタンスを起動してすぐ終了する、ランチャー型アプリを自動的に検出しようとします。 ランチャータイプのアプリが検出されると、切断されたアプリとして扱われます。", "cmd": "コマンド", "cmd_desc": "起動したいメインアプリケーション。空白の場合はアプリケーションは起動しません。", "cmd_note": "コマンド実行ファイルへのパスにスペースが含まれている場合は、引用符で囲む必要があります。", "cmd_prep_desc": "このアプリケーションの前/後に実行するコマンドのリストです。prep-commandsのいずれかに失敗した場合、アプリケーションの起動は中止されます。", "cmd_prep_name": "コマンドの準備", "covers_found": "カバー画像が見つかりました", "delete": "削除", "detached_cmds": "切り離されたコマンド", "detached_cmds_add": "別のコマンドを追加", "detached_cmds_desc": "バックグラウンドで実行するコマンドのリスト。", "detached_cmds_note": "コマンド実行ファイルへのパスにスペースが含まれている場合は、引用符で囲む必要があります。", "edit": "編集", "env_app_id": "アプリ ID", "env_app_name": "アプリ名", "env_client_audio_config": "クライアントから要求されたオーディオ設定 (2.0/5.1/7.1)", "env_client_enable_sops": "クライアントは最適なストリーミングのためにゲームを最適化するオプションを要求しています (true/false)", "env_client_fps": "クライアントから要求された FPS (float)", "env_client_gcmap": "要求されたゲームパッドマスク、ビットセット/ビットフィールド形式にて指定 (int)", "env_client_hdr": "HDR はクライアントによって有効になっています (true/false)", "env_client_height": "クライアントから要求された高さ (整数)", "env_client_host_audio": "クライアントはホストオーディオを要求しています (true/false)", "env_client_width": "クライアントから要求された幅 (int)", "env_displayplacer_example": "例 - 解像度自動化のためのディスプレイ プレーサ:", "env_qres_example": "例 - 自動解像度用のQRes:", "env_qres_path": "qresのパス", "env_var_name": "変数名", "env_vars_about": "環境変数について", "env_vars_desc": "すべてのコマンドはデフォルトでこれらの環境変数を取得します:", "env_xrandr_example": "例 - 解像度自動化のための Xrandr:", "exit_timeout": "終了タイムアウト", "exit_timeout_desc": "終了要求時にすべてのアプリプロセスが正常に終了するまで待機する秒数。 設定されていない場合、デフォルトでは5秒まで待機します。ゼロまたはマイナス値に設定されている場合、アプリは直ちに終了します。", "find_cover": "カバーを見つける", "global_prep_desc": "このアプリケーションのグローバル準備コマンドの実行を有効/無効にする。", "global_prep_name": "グローバル準備コマンド", "image": "画像", "image_desc": "クライアントに送信されるアプリケーションアイコン/画像/画像パス。画像はPNGファイルである必要があります。設定されていない場合、Apolloはデフォルトのボックス画像を送信します。", "loading": "読み込み中...", "name": "名前", "output_desc": "コマンドの出力を保存するファイル。指定されない場合、出力は無視されます。", "output_name": "出力", "run_as_desc": "これは、管理者権限を必要とするアプリケーションが正常に動作するために必要な場合があります。", "wait_all": "すべてのアプリプロセスが終了するまでストリーミングを続ける", "wait_all_desc": "これは、アプリによって開始されたすべてのプロセスが終了するまで、ストリーミングを続けます。 チェックを外すと、他のアプリプロセスがまだ実行中であっても、最初のアプリプロセスが終了するとストリーミングは停止します。", "working_dir": "作業ディレクトリ", "working_dir_desc": "プロセスに渡される作業ディレクトリ。たとえば、アプリケーションによっては、作業ディレクトリを使用して設定ファイルを検索します。 設定されていない場合、Apolloはデフォルトでコマンドの親ディレクトリを使用します。" }, "config": { "adapter_name": "アダプター名", "adapter_name_desc_linux_1": "キャプチャに使用する GPU を手動で指定します。", "adapter_name_desc_linux_2": "VAAPIが可能なすべてのデバイスを検索する", "adapter_name_desc_linux_3": "``renderD129`` を上記のデバイスに置き換えて、デバイスの名前と機能を一覧表示します。 Apolloでサポートされるには、最小限にする必要があります。", "adapter_name_desc_windows": "キャプチャに使用する GPU を手動で指定します。未設定の場合は、GPU が自動的に選択されます。 自動GPU選択を使用するには、このフィールドを空白のままにすることを強くお勧めします! 注:このGPUはディスプレイを接続して電源を入れている必要があります。 次のコマンドを使用して、適切な値を見つけることができます。", "adapter_name_placeholder_windows": "Radeon RX 580シリーズ", "add": "追加", "address_family": "アドレスファミリー", "address_family_both": "IPv4+IPv6", "address_family_desc": "Apolloが使用するアドレスファミリーを設定する", "address_family_ipv4": "IPv4 のみ", "always_send_scancodes": "常にスキャンコードを送信する", "always_send_scancodes_desc": "スキャンコードを送信すると、ゲームやアプリとの互換性が向上しますが、米国英語のキーボードレイアウトを使用していない特定のクライアントからのキーボード入力が誤っている可能性があります。 特定のアプリケーションでキーボード入力がまったく動作しない場合に有効にします。 クライアントのキーがホストに間違った入力を生成している場合は無効にします。", "amd_coder": "AMFコーダー(H264)", "amd_coder_desc": "エントロピーエンコーディングを選択して品質やエンコーディング速度を優先することができます。H.264 のみ。", "amd_enforce_hrd": "AMF仮説リファレンスデコーダ(HRD) Enforcement", "amd_enforce_hrd_desc": "HRDモデル要件を満たすためのレート制御の制約を増やします。 これによりビットレートのオーバーフローが大幅に減少しますが、エンコードアーティファクトや特定のカードの品質が低下する可能性があります。", "amd_preanalysis": "AMF事前解析", "amd_preanalysis_desc": "これにより、レート制御の事前分析が可能になり、エンコード待ち時間の増加を犠牲にして品質が向上する可能性があります。", "amd_quality": "AMF品質", "amd_quality_balanced": "balance-- balance(デフォルト)", "amd_quality_desc": "これにより、エンコード速度と品質のバランスを制御します。", "amd_quality_group": "AMF品質設定", "amd_quality_quality": "品質 -- 品質を優先", "amd_quality_speed": "スピード -- 速度を優先", "amd_rc": "AMFレート制御", "amd_rc_cbr": "cbr -- 固定ビットレート(デフォルト)", "amd_rc_cqp": "cqp -- 固定qp モード", "amd_rc_desc": "これは、クライアントのビットレート目標を超えないようにするレート制御方法を制御します。 'cqp'はビットレートターゲティングには適していません。'vbr_latency'以外のオプションはHRDエンフォースメントに依存してビットレートオーバーフローを制限します。", "amd_rc_group": "AMFレートコントロール設定", "amd_rc_vbr_latency": "vbr_latency -- レイテンシ制約付き可変ビットレート", "amd_rc_vbr_peak": "vbr_peak -- ピーク制約可変ビットレート", "amd_usage": "AMF使用率", "amd_usage_desc": "これにより、基本エンコーディングプロファイルが設定されます。 以下に表示されるすべてのオプションは、使用状況プロファイルのサブセットを上書きしますが、他の場所では設定できない追加の非表示設定が適用されます。", "amd_usage_lowlatency": "lowlaterity - 低レイテンシ(最速)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - 低レイテンシ、高品質 (高速)", "amd_usage_transcoding": "トランスコード-- トランスコード(最も遅い)", "amd_usage_ultralowlatency": "超低レイテンシー - 超低レイテンシ(最速)", "amd_usage_webcam": "ウェブカメラ -- ウェブカメラ (スロー)", "amd_vbaq": "AMF分散ベース適応型量子化(VBAQ)", "amd_vbaq_desc": "人間の視覚システムは、高度なテクスチャ領域の人工物には通常、あまり敏感です。 VBAQモードでは、ピクセル分散を使用して空間テクスチャの複雑さを示し、エンコーダがより多くのビットを割り当て、領域をスムーズにすることができます。 この機能を有効にすると、一部のコンテンツで主観的なビジュアル品質が向上します。", "apply_note": "'適用' をクリックして Apollo を再起動し、変更を適用します。これにより、実行中のセッションはすべて終了します。", "audio_sink": "音声シンク", "audio_sink_desc_linux": "オーディオループバックに使用されるオーディオシンクの名前。この変数を指定しない場合、pulseaudio はデフォルトのモニターデバイスを選択します。 いずれかのコマンドを使用して、オーディオシンクの名前を見つけることができます。", "audio_sink_desc_macos": "Audio Loopback に使用されるオーディオシンクの名前。Apolloはシステムの制限により、macOSのマイクにのみアクセスできます。 Soundflower または BlackHole を使用してシステムのオーディオをストリーミングする。", "audio_sink_desc_windows": "キャプチャする特定のオーディオデバイスを手動で指定します。未設定の場合、デバイスは自動的に選択されます。 自動デバイス選択を使用するには、このフィールドを空白のままにすることを強くお勧めします! 同じ名前の複数のオーディオデバイスをお持ちの場合は、次のコマンドを使用してデバイス ID を取得できます。", "audio_sink_placeholder_macos": "BlackHole 2ch", "audio_sink_placeholder_windows": "スピーカー (高品位オーディオデバイス)", "av1_mode": "AV1 サポート", "av1_mode_0": "サンシャインはエンコーダ機能に基づいてAV1のサポートを宣伝します(推奨)", "av1_mode_1": "サンシャインはAV1のサポートを宣伝しません", "av1_mode_2": "ApolloはAV1メイン8ビットプロファイルのサポートを宣伝します", "av1_mode_3": "ApolloはAV1メイン8ビットと10ビット(HDR)プロファイルのサポートを宣伝します。", "av1_mode_desc": "クライアントがAV1 Main 8ビットまたは10ビットのビデオストリームを要求できるようにします。 AV1はエンコードにCPU負荷がかかるため、ソフトウェアエンコーディングを使用する際のパフォーマンスが低下する可能性があります。", "back_button_timeout": "ホーム/ガイドボタンエミュレーションのタイムアウト", "back_button_timeout_desc": "Back/Selectボタンが指定されたミリ秒の間押し続けられると、Home/Guideボタン押下がエミュレートされる。値<0(デフォルト)に設定すると、Back/Selectボタンを押し続けてもHome/Guideボタンはエミュレートされない。", "capture": "特定のキャプチャ方法を強制する", "capture_desc": "自動モードでApolloは動作する最初のものを使用します. NvFBCはパッチ済み nvidiaドライバが必要です.", "cert": "証明書", "cert_desc": "Web UIとMoonlightクライアントのペアリングに使用される証明書。互換性を確保するためには、RSA-2048 公開鍵が必要です。", "channels": "最大接続クライアント数", "channels_desc_1": "Apolloは、単一のストリーミングセッションを複数のクライアントと同時に共有することができます。", "channels_desc_2": "一部のハードウェアエンコーダには、複数のストリームでパフォーマンスを低下させる制限がある場合があります。", "coder_cabac": "cabac -- コンテキスト適応二進数演算符号化 - 高品質", "coder_cavlc": "cavlc -- コンテキスト適応型可変長符号化 - 高速デコード", "configuration": "設定", "controller": "ゲームパッド入力を有効にする", "controller_desc": "ゲストがゲームパッド/コントローラーでホストシステムを制御できるようにします", "credentials_file": "資格情報ファイル", "credentials_file_desc": "ユーザー名/パスワードは、サンシャインのステートファイルとは別に保管してください。", "dd_config_ensure_active": "ディスプレイを自動的に有効にする", "dd_config_ensure_only_display": "他のディスプレイを無効にして指定したディスプレイのみ有効にする", "dd_config_ensure_primary": "自動的にディスプレイを有効にし、プライマリディスプレイにする", "dd_config_label": "デバイス設定", "dd_config_revert_delay": "設定の戻す遅延時間", "dd_config_revert_delay_desc": "アプリが閉じられたか、最後のセッションが終了したときに設定を元に戻すまで待機するミリ秒単位の追加の遅延が発生します。 主な目的は、アプリ間の迅速な切り替え時のスムーズな移行を提供することです。", "dd_config_verify_only": "ディスプレイが有効になっていることを確認します(デフォルト)", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "クライアントが要求するHDRモードのオン/オフを切り替えます (デフォルト)", "dd_hdr_option_disabled": "HDR設定を変更しない", "dd_mode_remapping": "ディスプレイモードの再マッピング", "dd_mode_remapping_add": "再マッピングエントリを追加", "dd_mode_remapping_desc_1": "要求された解像度を変更するために、再マッピングエントリを指定します。または、リフレッシュレートを他の値に変更します。", "dd_mode_remapping_desc_2": "リストは上から下に反復され、最初の一致が使用されます。", "dd_mode_remapping_desc_3": "\"Requested\" フィールドは、要求された値に一致する空のままにすることができます。", "dd_mode_remapping_desc_4_final_values_mixed": "少なくとも 1 つの \"Final\" フィールドを指定する必要があります。解像度またはリフレッシュレートは変更されません。", "dd_mode_remapping_desc_4_final_values_non_mixed": "\"Final\" フィールドを指定しなければならず、空にすることはできません。", "dd_mode_remapping_desc_5_sops_mixed_only": "Moonlight クライアントでは、「ゲーム設定の最適化」オプションが有効になっていなければなりません。そうでなければ、解像度フィールドが指定されたエントリがスキップされます。", "dd_mode_remapping_desc_5_sops_resolution_only": "Moonlight クライアントで「ゲーム設定の最適化」オプションが有効になっていなければなりません。そうでなければマッピングがスキップされます。", "dd_mode_remapping_final_refresh_rate": "最終更新レート", "dd_mode_remapping_final_resolution": "最終解像度", "dd_mode_remapping_requested_fps": "要求されたFPS", "dd_mode_remapping_requested_resolution": "要求された解像度", "dd_options_header": "高度なディスプレイデバイスオプション", "dd_refresh_rate_option": "リフレッシュ率", "dd_refresh_rate_option_auto": "クライアントから提供されたFPS値を使用 (デフォルト)", "dd_refresh_rate_option_disabled": "更新レートを変更しない", "dd_refresh_rate_option_manual": "手動で更新レートを使用する", "dd_refresh_rate_option_manual_desc": "使用するリフレッシュレートを入力してください", "dd_resolution_option": "解像度", "dd_resolution_option_auto": "クライアントから提供された解像度を使用します (デフォルト)", "dd_resolution_option_disabled": "解像度を変更しない", "dd_resolution_option_manual": "手動で入力した解像度を使用", "dd_resolution_option_manual_desc": "使用する解像度を入力してください", "dd_resolution_option_ogs_desc": "これを行うには、Moonlightクライアントで「ゲーム設定の最適化」オプションを有効にする必要があります。", "dd_wa_hdr_toggle_desc": "ストリーミングとして仮想ディスプレイデバイスを使用すると、HDR色が正しく表示されない場合があります。このオプションを有効にすると、Apolloはこの問題を軽減しようとします。", "dd_wa_hdr_toggle": "HDRの高コントラスト回避を有効にする", "ds4_back_as_touchpad_click": "戻る/選択をタッチパッドにマップする", "ds4_back_as_touchpad_click_desc": "DS4エミュレーションを強制するときは、戻る/選択をタッチパッドにマップする", "encoder": "特定のエンコーダーを強制する", "encoder_desc": "特定のエンコーダを強制します。そうでなければ、Apolloは最良の選択肢を選択します。 注:Windowsでハードウェアエンコーダを指定する場合は、ディスプレイが接続されているGPUと一致する必要があります。", "encoder_software": "ソフトウェア", "external_ip": "外部 IP", "external_ip_desc": "外部IPアドレスが指定されていない場合、Apolloは自動的に外部IPを検出します。", "fec_percentage": "FECの割合", "fec_percentage_desc": "各ビデオフレーム内のデータ パケットあたりのパケットを修正するエラー率。 より高い値は、ネットワークパケットの損失を増やすことができますが、帯域幅の使用量を増加させることができます。", "ffmpeg_auto": "auto -- ffmpegで判断する (デフォルト)", "file_apps": "アプリファイル", "file_apps_desc": "Apolloの現在のアプリが保存されているファイル。", "file_state": "状態ファイル", "file_state_desc": "サンシャインの現在の状態が保存されているファイル", "fps": "通知されたFPS", "gamepad": "エミュレートしたゲームパッドのタイプ", "gamepad_auto": "自動選択オプション", "gamepad_desc": "ホスト上でエミュレートするゲームパッドの種類を選択します", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "DS4選択オプション", "gamepad_ds5": "DS5 (PS5)", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "DS4マニュアルオプション", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "コマンドの準備", "global_prep_cmd_desc": "アプリケーションの実行前または実行後に実行されるコマンドのリストを構成します。 指定された preparation コマンドのいずれかに失敗すると、アプリケーションの起動プロセスは中断されます。", "hevc_mode": "HEVC サポート", "hevc_mode_0": "サンシャインはエンコーダ機能に基づいてHEVCのサポートを宣伝します(推奨)", "hevc_mode_1": "サンシャインはHEVCのサポートを宣伝しません", "hevc_mode_2": "サンシャインはHEVCメインプロファイルのサポートを宣伝します", "hevc_mode_3": "サンシャインはHEVC MainおよびMain10(HDR)プロファイルのサポートを宣伝します", "hevc_mode_desc": "HEVC MainまたはHEVC Main10ビデオストリームのリクエストをクライアントに許可します。 HEVCはエンコードにCPU負荷がかかるため、ソフトウェアエンコーディングを使用する際のパフォーマンスが低下する可能性があります。", "high_resolution_scrolling": "高解像度スクロールサポート", "high_resolution_scrolling_desc": "有効にすると、ApolloはMoonlightのクライアントから高解像度スクロールイベントを通過します。 これは、高解像度スクロールイベントで高速にスクロールする古いアプリケーションでは無効にすることができます。", "install_steam_audio_drivers": "Steam オーディオドライバをインストール", "install_steam_audio_drivers_desc": "Steamがインストールされている場合、Steam Streaming Speakersドライバが自動的にインストールされ、5.1/7.1 サラウンドサウンドとホストオーディオのミュートがサポートされます。", "key_repeat_delay": "Key Repeat Delay", "key_repeat_delay_desc": "キーを繰り返す速度を制御します。キーを繰り返すまでの時間をミリ秒単位で設定します。", "key_repeat_frequency": "キーの繰り返し周波数", "key_repeat_frequency_desc": "キーが毎秒繰り返される頻度。この設定可能なオプションは10進数をサポートします。", "key_rightalt_to_key_win": "右AltキーをWindowsキーにマップする", "key_rightalt_to_key_win_desc": "Moonlight から Windows キーを直接送信できない可能性があります。 これらの場合、ApolloにRight AltキーがWindowsキーであると考えさせると便利かもしれません。", "keyboard": "キーボード入力を有効にする", "keyboard_desc": "ゲストがキーボードでホストシステムを制御できるようにします", "lan_encryption_mode": "LAN 暗号化モード", "lan_encryption_mode_1": "サポートされているクライアントで有効", "lan_encryption_mode_2": "すべてのクライアントに必要です", "lan_encryption_mode_desc": "これは、ローカルネットワーク経由でストリーミングする際に暗号化がいつ使用されるかを決定します。暗号化は、特に強力なホストやクライアントでは、ストリーミングのパフォーマンスを低下させることができます。", "locale": "ロケール", "locale_desc": "Apolloのユーザーインターフェースに使用されるロケール。", "log_level": "ログレベル", "log_level_0": "Verbose", "log_level_1": "Debug", "log_level_2": "情報", "log_level_3": "警告", "log_level_4": "エラー", "log_level_5": "Fatal", "log_level_6": "なし", "log_level_desc": "標準出力に印刷された最小ログレベル", "log_path": "ログファイルのパス", "log_path_desc": "Apolloの現在のログが保存されているファイル。", "min_fps_factor": "FPSの最小係数", "min_fps_factor_desc": "サンシャインは、フレーム間の最小時間を計算するためにこの係数を使用します。 この値を少し増やすと、ほとんどの静的コンテンツをストリーミングするのに役立ちます。値を大きくすると、帯域幅が増えます。", "min_threads": "最小CPUスレッド数", "min_threads_desc": "値を大きくするとエンコーディングの効率はわずかに低下しますが、通常はエンコーディングにCPUコアをより多く使用する価値があります。 理想的な値は、ハードウェア上の希望のストリーミング設定で確実にエンコードできる最小値です。", "misc": "その他のオプション", "motion_as_ds4": "クライアントのゲームパッドがモーションセンサーが存在することを報告する場合、DS4ゲームパッドをエミュレートします", "motion_as_ds4_desc": "無効にすると、モーションセンサーはゲームパッドの種類選択中に考慮されません。", "mouse": "マウス入力を有効にする", "mouse_desc": "ゲストがマウスでホストシステムを制御できるようにします", "native_pen_touch": "Native Pen/Touch サポート", "native_pen_touch_desc": "有効にすると、ApolloはMoonlightクライアントからネイティブのペン/タッチイベントを通過します。これはネイティブのペン/タッチサポートがない古いアプリケーションでは無効にするのに便利です。", "notify_pre_releases": "プレリリース通知", "notify_pre_releases_desc": "Apolloの新しいプレリリースバージョンを通知するかどうか", "nvenc_h264_cavlc": "H.264よりCAVLCを優先する", "nvenc_h264_cavlc_desc": "単純なエントロピーコーディング形式。CAVLCは同じ品質のために約10%のビットレートを必要とします。本当に古いデコードデバイスにのみ関係します。", "nvenc_latency_over_power": "省電力よりもエンコーディングのレイテンシを低減したい場合", "nvenc_latency_over_power_desc": "Apollo は、エンコーディングのレイテンシを低減するためにストリーミング中に最大GPU クロック速度を要求します。 これを無効にするとエンコード待ち時間が大幅に増加する可能性があるため、推奨されません。", "nvenc_opengl_vulkan_on_dxgi": "DXGI上に現在のOpenGL/Vulkan", "nvenc_opengl_vulkan_on_dxgi_desc": "Apolloは、DXGIの上に存在しない限り、フルフレームレートでフルスクリーンOpenGLとVulkanプログラムをキャプチャすることはできません。 これはシステム全体の設定であり、サンシャインプログラムの出口に戻ります。", "nvenc_preset": "パフォーマンスプリセット", "nvenc_preset_1": "(高速、デフォルト)", "nvenc_preset_7": "(最も遅い)", "nvenc_preset_desc": "数値が高いほど、符号化遅延の増加を犠牲にして圧縮(一定のビットレートでの品質)が向上します。 ネットワークまたはデコーダによって制限されている場合にのみ変更することをお勧めします, そうでなければ、ビットレートを増やすことによって、同様の効果を達成することができます.", "nvenc_realtime_hags": "ハードウェアアクセラレーションGPUスケジューリングでリアルタイム優先度を使用する", "nvenc_realtime_hags_desc": "現在、NVIDIAドライバは、HAGSが有効で、リアルタイムプライオリティが使用され、VRAM使用率が最大に近い場合、エンコーダでフリーズすることがあります。このオプションを無効にすると、優先順位が高に下がり、GPUに大きな負荷がかかったときのキャプチャパフォーマンスの低下と引き換えに、フリーズを回避できます。", "nvenc_spatial_aq": "Spatial AQ", "nvenc_spatial_aq_desc": "より高いQP値をビデオのフラットリージョンに割り当てます。低ビットレートでストリーミングする際に有効にすることをお勧めします。", "nvenc_spatial_aq_disabled": "無効 (高速、デフォルト)", "nvenc_spatial_aq_enabled": "有効 (低速)", "nvenc_twopass": "Two-passモード", "nvenc_twopass_desc": "予備的なエンコードパスを追加します。これは、より多くのモーションベクトルを検出することができます。より良いフレーム全体でビットレートを分配し、より厳密にビットレート制限に従います。 これは時折ビットレートオーバーシュートやその後のパケット損失につながる可能性があるため、無効にすることは推奨されません。", "nvenc_twopass_disabled": "無効 (高速、推奨されません)", "nvenc_twopass_full_res": "フル解像度(低速)", "nvenc_twopass_quarter_res": "クォーター解像度(速く、デフォルト)", "nvenc_vbv_increase": "シングルフレーム VBV/HRD パーセンテージ増加", "nvenc_vbv_increase_desc": "デフォルトでは、単一フレームVBV/HRDを使用しています。つまり、エンコードされたビデオフレームサイズは要求されたビットレートを要求されたフレームレートで割った値を超えないことが予想されます。 この制限を緩和することは有益であり、低レイテンシの可変ビットレートとして機能することができます。 ネットワークにビットレートのスパイクを処理するバッファヘッドルームがない場合、パケットロスを引き起こす可能性があります。 許容可能な最大値は400で、エンコードされたビデオフレームの上限サイズ制限の5倍に相当します。", "origin_web_ui_allowed": "許可されたオリジンウェブUI", "origin_web_ui_allowed_desc": "Web UIへのアクセスが拒否されていないリモートエンドポイントアドレスのオリジンです", "origin_web_ui_allowed_lan": "LAN 内のユーザだけが Web UI にアクセスできます", "origin_web_ui_allowed_pc": "ローカルホストのみがWebUIにアクセスできます", "origin_web_ui_allowed_wan": "誰でもWeb UIにアクセスできます", "output_name_desc_unix": "Apolloの起動時には、検出されたディスプレイのリストが表示されます。注:括弧内のid値を使用する必要があります。", "output_name_desc_windows": "キャプチャに使用するディスプレイを手動で指定します。未設定の場合、プライマリディスプレイをキャプチャします。 注意: 上記の GPU を指定した場合、この表示は GPU に接続する必要があります。次のコマンドを使用して適切な値を見つけることができます。", "output_name_unix": "番号を表示", "output_name_windows": "出力名", "ping_timeout": "Pingのタイムアウト", "ping_timeout_desc": "Moonlightがデータが止まってからストリームをシャットダウンするまで待機時間をミリ秒で指定", "pkey": "プライベートキー", "pkey_desc": "ウェブ UI とMoonlight クライアントのペアリングに使用される秘密鍵。互換性を確保するためには、RSA-2048 秘密鍵を使用する必要があります。", "port": "ポート", "port_alert_1": "1024以下のポートを使用することはできません!", "port_alert_2": "65535以上のポートは利用できません!", "port_desc": "Apolloが使用するポートのファミリーを設定する", "port_http_port_note": "Moonlight に接続するには、このポートを使用してください。", "port_note": "メモ", "port_port": "ポート", "port_protocol": "Protocol", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "Web UIをインターネットに公開することはセキュリティ上のリスクです! ご自身の責任で進めてください!", "port_web_ui": "Web UI", "qp": "量子化パラメータ", "qp_desc": "デバイスによっては、Constant Bit Rateをサポートしていない可能性があります。これらのデバイスでは、QPが代わりに使用されます。値が高いほど圧縮が多くなりますが、品質が低下します。", "qsv_coder": "QuickSync Coder (H264)", "qsv_preset": "QuickSync Preset", "qsv_preset_fast": "より速く (低品質)", "qsv_preset_faster": "最速(低品質)", "qsv_preset_medium": "ミディアム(デフォルト)", "qsv_preset_slow": "遅い (良質)", "qsv_preset_slower": "遅い (より良い品質)", "qsv_preset_slowest": "最も遅い (最高品質)", "qsv_preset_veryfast": "最速(低品質)", "qsv_slow_hevc": "低速HEVCエンコーディングを許可する", "qsv_slow_hevc_desc": "これにより、GPU 使用率の向上とパフォーマンスの低下を犠牲にして、古い Intel GPU での HEVC エンコーディングを有効にできます。", "res_fps_desc": "Apolloによって宣伝された表示モード。 Moonlight-nx(Switch)のようないくつかのバージョンのMoonlightは、要求された解像度とfpsがサポートされていることを確認するために、これらのリストに依存しています。 この設定では、画面ストリームがMoonlightに送信される方法は変更されません。", "resolutions": "広告解像度の設定", "restart_note": "サンシャインは変更を適用するために再起動しています。", "sunshine_name": "サンシャイン名", "sunshine_name_desc": "Moonlight によって表示される名前。指定されていない場合は、PC のホスト名が使用されます", "sw_preset": "SWプリセット", "sw_preset_desc": "エンコード速度(エンコードフレーム/秒)と圧縮効率(ビットストリームのビット毎の品質)のトレードオフを最適化します。デフォルトは超高速です。", "sw_preset_fast": "速い", "sw_preset_faster": "より速く", "sw_preset_medium": "medium", "sw_preset_slow": "遅い", "sw_preset_slower": "遅いです", "sw_preset_superfast": "スーパーファスト(デフォルト)", "sw_preset_ultrafast": "超高速", "sw_preset_veryfast": "veryfast", "sw_preset_veryslow": "veryslow", "sw_tune": "SWチューン", "sw_tune_animation": "アニメーションは漫画に適していますより高いデブロッキングや参照フレームを使っています", "sw_tune_desc": "チューニングオプション。プリセットの後に適用されます。デフォルトはゼロになります。", "sw_tune_fastdecode": "fastdecode -- 特定のフィルタを無効にすることでより高速なデコードが可能です", "sw_tune_film": "フィルム-- 高品質の映画コンテンツに使用します。", "sw_tune_grain": "穀物は、古くて粒状のフィルム素材に保存されています", "sw_tune_stillimage": "スタイルはスライドショーのようなコンテンツに適しています", "sw_tune_zerolatency": "zerolatency -- 高速なエンコーディングと低遅延ストリーミングに適しています (デフォルト)", "touchpad_as_ds4": "クライアントゲームパッドがタッチパッドが存在することを報告する場合、DS4ゲームパッドをエミュレートします", "touchpad_as_ds4_desc": "無効にすると、ゲームパッドの種類選択中にタッチパッドの存在が考慮されません。", "upnp": "UPnP", "upnp_desc": "インターネット経由でストリーミングするポート転送を自動的に設定します", "vaapi_strict_rc_buffer": "AMD GPUでH.264/HEVCのフレームビットレート制限を厳密に強制する", "vaapi_strict_rc_buffer_desc": "このオプションを有効にすると、シーンの変更中にネットワーク上でフレームがドロップされるのを避けることができますが、移動中にビデオの品質が低下する可能性があります。", "virtual_sink": "Virtual Sink", "virtual_sink_desc": "使用する仮想オーディオデバイスを手動で指定します。未設定の場合は、デバイスが自動的に選択されます。 自動デバイス選択を使用するには、このフィールドを空白のままにすることを強くお勧めします!", "virtual_sink_placeholder": "Steamストリーミングスピーカー", "vt_coder": "VideoToolbox Coder", "vt_realtime": "VideoToolbox リアルタイムエンコーディング", "vt_software": "VideoToolbox ソフトウェアエンコーディング", "vt_software_allowed": "許可", "vt_software_forced": "強制的に", "wan_encryption_mode": "WAN暗号化モード", "wan_encryption_mode_1": "サポートされているクライアントで有効になっています(デフォルト)", "wan_encryption_mode_2": "すべてのクライアントに必要です", "wan_encryption_mode_desc": "これは、インターネット経由でストリーミングする際に暗号化がいつ使用されるかを決定します。特に強力なホストやクライアントでは、暗号化によりストリーミングパフォーマンスが低下します。" }, "index": { "description": "サンシャインはムーンライトのための自己ホストゲームストリームホストです。", "download": "ダウンロード", "installed_version_not_stable": "Apolloのプレリリース版を実行しています。バグやその他の問題が発生する可能性があります。 問題が発生した場合は報告してください。Apolloをより良いソフトウェアにしていただきありがとうございます!", "loading_latest": "最新のリリースを読み込んでいます...", "new_pre_release": "新しいプレリリースバージョンが利用可能です!", "new_stable": "新しい安定版が利用可能です!", "startup_errors": "注意Apolloは起動時にこれらのエラーを検出しました。ストリーミングの前にこれらのエラーを修正することを強くお勧めします。", "version_dirty": "Apolloをより良いソフトウェアにしてくれてありがとうございます!", "version_latest": "サンシャインの最新バージョンを実行しています", "welcome": "こんにちは、サンシャイン!" }, "navbar": { "applications": "アプリケーション", "configuration": "設定", "home": "ホーム", "password": "パスワードの変更", "pin": "Pin", "theme_auto": "自動", "theme_dark": "ダーク", "theme_light": "ライト", "toggle_theme": "テーマ", "troubleshoot": "トラブルシューティング" }, "password": { "confirm_password": "パスワードの確認", "current_creds": "現在の資格情報", "new_creds": "新しい資格情報", "new_username_desc": "指定しない場合、ユーザー名は変更されません", "password_change": "パスワードの変更", "success_msg": "パスワードが正常に変更されました!このページはまもなくリロードされます。ブラウザーは新しい資格情報を要求します。" }, "pin": { "device_name": "端末名", "pair_failure": "ペアリングに失敗しました:PINが正しく入力されたかどうかを確認します", "pair_success": "成功!Moonlight を確認して続行してください", "pin_pairing": "PINペアリング", "send": "送信", "warning_msg": "ペアリングするクライアントにアクセスできることを確認してください。このソフトウェアは、あなたのコンピュータを完全にコントロールすることができますので、注意してください!" }, "resource_card": { "github_discussions": "GitHub Discussions", "legal": "Legal", "legal_desc": "このソフトウェアの使用を継続することにより、以下のドキュメントの利用規約に同意したことになります。", "license": "ライセンス", "lizardbyte_website": "LizardByte ウェブサイト", "resources": "リソース", "resources_desc": "Apolloのための資源!", "third_party_notice": "第三者通知" }, "troubleshooting": { "dd_reset": "永続的なディスプレイデバイス設定をリセット", "dd_reset_desc": "Apolloが変更されたディスプレイデバイス設定を復元しようとし続けている場合は、設定をリセットして手動で表示状態を復元することができます。", "dd_reset_error": "永続化をリセット中にエラーが発生しました!", "dd_reset_success": "持続性のリセットに成功しました!", "force_close": "強制閉じる", "force_close_desc": "Moonlight が現在実行中のアプリについて不満がある場合、強制終了すると問題が修正されます。", "force_close_error": "アプリケーションを終了中にエラー", "force_close_success": "申請は正常に終了しました!", "logs": "ログ", "logs_desc": "Apolloによってアップロードされたログを参照してください", "logs_find": "検索...", "restart_apollo": "サンシャインを再起動", "restart_apollo_desc": "Apolloが正常に動作していない場合は、再起動を試みることができます。実行中のセッションはすべて終了します。", "restart_apollo_success": "サンシャインが再起動しています", "troubleshooting": "トラブルシューティング", "unpair_all": "すべてのペアリングを解除", "unpair_all_error": "ペアリング解除中のエラー", "unpair_all_success": "ペアを解除しました!", "unpair_desc": "ペアリングされたデバイスを削除します。アクティブなセッションを持つペアリングされていないデバイスは、接続されたままですが、セッションを開始または再開することはできません。", "unpair_single_no_devices": "ペアリングされたデバイスがありません。", "unpair_single_success": "ただし、デバイスはまだアクティブなセッションにいる可能性があります。上の「強制終了」ボタンを使用して、開いているセッションを終了します。", "unpair_single_unknown": "不明なクライアント", "unpair_title": "デバイスのペアリングを解除" }, "welcome": { "confirm_password": "パスワードの確認", "create_creds": "始める前に、Web UI にアクセスするための新しいユーザー名とパスワードを作成する必要があります。", "create_creds_alert": "以下の資格情報は、ApolloのWeb UIにアクセスするために必要です。あなたが二度と見ることはありませんので、安全に保管してください!", "greeting": "Apolloへようこそ!", "login": "ログイン", "welcome_success": "このページはまもなく再読み込みされます。ブラウザーは新しい資格情報を要求します。" } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/ko.json ================================================ { "_common": { "apply": "적용", "auto": "자동 설정", "autodetect": "자동 감지 (권장)", "beta": "(베타)", "cancel": "취소", "disabled": "비활성화", "disabled_def": "사용 안 함(기본값)", "disabled_def_cbox": "기본값: 선택 안 함", "dismiss": "해제", "do_cmd": "명령 수행", "elevated": "관리자 권한으로 실행", "enabled": "활성화됨", "enabled_def": "활성화됨 (기본값)", "enabled_def_cbox": "기본값: 확인됨", "error": "오류!", "note": "참고:", "password": "비밀번호", "run_as": "관리자 권한으로 실행", "save": "저장", "see_more": "자세히 보기", "success": "성공!", "undo_cmd": "명령 실행 취소", "username": "사용자 이름", "warning": "경고!" }, "apps": { "actions": "작업", "add_cmds": "명령어 추가", "add_new": "신규 추가", "app_name": "애플리케이션 이름", "app_name_desc": "Moonlight에 표시될 애플리케이션 이름", "applications_desc": "클라이언트를 다시 시작할 때만 애플리케이션이 새로 고침 됩니다.", "applications_title": "애플리케이션", "auto_detach": "애플리케이션이 빠르게 종료되는 경우에도 스트리밍 계속하기", "auto_detach_desc": "다른 프로그램이나 인스턴스를 실행한 후 빠르게 종료되는 런처형 앱을 자동으로 감지하려고 시도합니다. 런처형 앱이 감지되면 해당 앱은 분리된 앱으로 처리됩니다.", "cmd": "명령", "cmd_desc": "시작할 기본 애플리케이션입니다. 비어 있으면 애플리케이션이 시작되지 않습니다.", "cmd_note": "명령 실행 파일의 경로에 공백이 포함되어 있으면 따옴표로 묶어야 합니다.", "cmd_prep_desc": "이 애플리케이션 전/후에 실행할 명령 목록입니다. 준비 명령 중 하나라도 실패하면 애플리케이션 시작이 중단됩니다.", "cmd_prep_name": "명령 준비", "covers_found": "커버 발견", "delete": "삭제", "detached_cmds": "분리된 명령", "detached_cmds_add": "분리된 명령 추가", "detached_cmds_desc": "백그라운드에서 실행할 명령 목록입니다.", "detached_cmds_note": "명령 실행 파일의 경로에 공백이 포함되어 있으면 따옴표로 묶어야 합니다.", "edit": "편집", "env_app_id": "앱 ID", "env_app_name": "앱 이름", "env_client_audio_config": "클라이언트가 요청한 오디오 구성(2.0/5.1/7.1)", "env_client_enable_sops": "클라이언트가 최적의 스트리밍을 위해 게임 최적화 옵션을 요청했습니다(참/거짓).", "env_client_fps": "클라이언트가 요청한 FPS (float)", "env_client_gcmap": "요청된 게임패드 마스크, 비트셋/비트필드 형식(int)", "env_client_hdr": "클라이언트가 HDR을 활성화합니다(참/거짓).", "env_client_height": "클라이언트가 요청한 높이(int)", "env_client_host_audio": "클라이언트가 호스트 오디오를 요청했습니다(참/거짓).", "env_client_width": "클라이언트가 요청한 너비(int)", "env_displayplacer_example": "예시 - 해상도 자동화를 위한 디스플레이플레이스:", "env_qres_example": "예 - 해결 자동화를 위한 QR:", "env_qres_path": "Qres 경로", "env_var_name": "변수 이름", "env_vars_about": "환경 변수 정보", "env_vars_desc": "모든 명령은 기본적으로 이러한 환경 변수를 가져옵니다:", "env_xrandr_example": "예시 - 해상도 자동화를 위한 Xrandr:", "exit_timeout": "종료 시간 초과", "exit_timeout_desc": "종료 요청 시 모든 앱 프로세스가 정상적으로 종료될 때까지 기다릴 시간(초)입니다. 설정하지 않으면 기본값은 최대 5초까지 대기하는 것입니다. 0 또는 음수 값으로 설정하면 앱이 즉시 종료됩니다.", "find_cover": "표지 찾기", "global_prep_desc": "이 애플리케이션에 대한 글로벌 준비 명령 실행을 활성화/비활성화합니다.", "global_prep_name": "글로벌 준비 명령", "image": "이미지", "image_desc": "클라이언트로 전송할 애플리케이션 아이콘/사진/이미지 경로입니다. 이미지는 PNG 파일이어야 합니다. 설정하지 않으면 선샤인은 기본 상자 이미지를 전송합니다.", "loading": "로드 중...", "name": "이름", "output_desc": "명령의 출력이 저장되는 파일로, 지정하지 않으면 출력이 무시됩니다.", "output_name": "출력", "run_as_desc": "이는 제대로 실행하려면 관리자 권한이 필요한 일부 애플리케이션에 필요할 수 있습니다.", "wait_all": "모든 앱 프로세스가 종료될 때까지 스트리밍을 계속합니다.", "wait_all_desc": "앱에서 시작한 모든 프로세스가 종료될 때까지 스트리밍이 계속됩니다. 이 옵션을 선택하지 않으면 다른 앱 프로세스가 계속 실행 중이더라도 초기 앱 프로세스가 종료되면 스트리밍이 중지됩니다.", "working_dir": "작업 디렉토리", "working_dir_desc": "프로세스에 전달할 작업 디렉터리입니다. 예를 들어 일부 애플리케이션은 작업 디렉터리를 사용하여 구성 파일을 검색합니다. 이 옵션을 설정하지 않으면 기본적으로 Apollo은 다음 명령의 상위 디렉터리로 설정됩니다." }, "config": { "adapter_name": "어댑터 이름", "adapter_name_desc_linux_1": "캡처에 사용할 GPU를 수동으로 지정합니다.", "adapter_name_desc_linux_2": "를 검색하여 VAAPI를 지원하는 모든 장치를 찾습니다.", "adapter_name_desc_linux_3": "'renderD129'를 위의 장치로 바꾸면 장치의 이름과 기능이 나열됩니다. 선샤인에서 지원하려면 최소한 이 기능이 있어야 합니다:", "adapter_name_desc_windows": "캡처에 사용할 GPU를 수동으로 지정합니다. 설정하지 않으면 GPU가 자동으로 선택됩니다. 자동 GPU 선택을 사용하려면 이 필드를 비워 두는 것이 좋습니다! 참고: 이 GPU는 디스플레이가 연결되어 있고 전원이 켜져 있어야 합니다. 적절한 값은 다음 명령을 사용하여 찾을 수 있습니다:", "adapter_name_placeholder_windows": "Radeon RX 580 시리즈", "add": "추가", "address_family": "주소 가족", "address_family_both": "IPv4+IPv6", "address_family_desc": "선샤인이 사용하는 주소 계열 설정", "address_family_ipv4": "IPv4 전용", "always_send_scancodes": "항상 스캔 코드 보내기", "always_send_scancodes_desc": "스캔코드를 전송하면 게임 및 앱과의 호환성이 향상되지만 미국식 영어 키보드 레이아웃을 사용하지 않는 특정 클라이언트에서 키보드 입력이 잘못될 수 있습니다. 특정 애플리케이션에서 키보드 입력이 전혀 작동하지 않는 경우 활성화합니다. 클라이언트의 키가 호스트에서 잘못된 입력을 생성하는 경우 비활성화합니다.", "amd_coder": "AMF 코더(H264)", "amd_coder_desc": "엔트로피 인코딩을 선택하여 품질 또는 인코딩 속도의 우선순위를 정할 수 있습니다. H.264만 해당.", "amd_enforce_hrd": "AMF 가상 참조 디코더(HRD) 시행", "amd_enforce_hrd_desc": "HRD 모델 요구 사항을 충족하기 위해 속도 제어에 대한 제약 조건을 높입니다. 이렇게 하면 비트레이트 오버플로가 크게 줄어들지만 특정 카드에서 인코딩 아티팩트 또는 품질 저하가 발생할 수 있습니다.", "amd_preanalysis": "AMF 사전 분석", "amd_preanalysis_desc": "이렇게 하면 속도 제어 사전 분석이 가능하므로 인코딩 지연 시간이 증가하는 대신 품질이 향상될 수 있습니다.", "amd_quality": "AMF 품질", "amd_quality_balanced": "균형 잡힌 -- 균형 잡힌(기본값)", "amd_quality_desc": "인코딩 속도와 품질 간의 균형을 제어합니다.", "amd_quality_group": "AMF 품질 설정", "amd_quality_quality": "품질 - 품질 선호", "amd_quality_speed": "속도 - 속도 선호", "amd_rc": "AMF 요금 제어", "amd_rc_cbr": "cbr - 일정한 비트레이트(HRD가 활성화된 경우 권장)", "amd_rc_cqp": "CQP -- 상수 QP 모드", "amd_rc_desc": "이는 클라이언트 비트레이트 목표를 초과하지 않도록 비트레이트 제어 방법을 제어합니다. 'cqp'는 비트레이트 타겟팅에 적합하지 않으며, 'vbr_latency' 이외의 다른 옵션은 비트레이트 오버플로를 제한하는 데 도움이 되는 HRD 적용에 의존합니다.", "amd_rc_group": "AMF 속도 제어 설정", "amd_rc_vbr_latency": "vbr_latency - 지연 시간 제한 가변 비트 전송률(HRD가 비활성화된 경우 권장, 기본값)", "amd_rc_vbr_peak": "vbr_peak - 피크 제한 가변 비트 전송률", "amd_usage": "AMF 사용법", "amd_usage_desc": "기본 인코딩 프로필을 설정합니다. 아래에 제시된 모든 옵션은 사용 프로필의 하위 집합을 재정의하지만 다른 곳에서는 구성할 수 없는 숨겨진 설정이 추가로 적용됩니다.", "amd_usage_lowlatency": "낮은 지연 시간 - 낮은 지연 시간(가장 빠름)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - 낮은 지연 시간, 높은 품질(빠른)", "amd_usage_transcoding": "트랜스코딩 -- 트랜스코딩(가장 느림)", "amd_usage_ultralowlatency": "초저지연 - 초저지연(가장 빠름, 기본값)", "amd_usage_webcam": "웹캠 -- 웹캠(느림)", "amd_vbaq": "AMF 분산 기반 적응형 양자화(VBAQ)", "amd_vbaq_desc": "인간의 시각 시스템은 일반적으로 텍스처가 심한 영역의 아티팩트에 덜 민감합니다. VBAQ 모드에서는 픽셀 분산이 공간 텍스처의 복잡도를 나타내는 데 사용되므로 인코더가 더 부드러운 영역에 더 많은 비트를 할당할 수 있습니다. 이 기능을 활성화하면 일부 콘텐츠에서 주관적인 화질이 개선됩니다.", "apply_note": "'적용'을 클릭하여 Apollo을 다시 시작하고 변경 사항을 적용합니다. 그러면 실행 중인 모든 세션이 종료됩니다.", "audio_sink": "오디오 싱크", "audio_sink_desc_linux": "오디오 루프백에 사용되는 오디오 싱크의 이름입니다. 이 변수를 지정하지 않으면 pulseaudio가 기본 모니터 장치를 선택합니다. 오디오 싱크의 이름은 다음 명령을 사용하여 찾을 수 있습니다:", "audio_sink_desc_macos": "오디오 루프백에 사용되는 오디오 싱크의 이름입니다. Apollo은 시스템 제한으로 인해 macOS에서만 마이크에 액세스할 수 있습니다. 사운드플라워 또는 블랙홀을 사용하여 시스템 오디오를 스트리밍하려면.", "audio_sink_desc_windows": "캡처할 특정 오디오 장치를 수동으로 지정합니다. 설정하지 않으면 장치가 자동으로 선택됩니다. 자동 장치 선택을 사용하려면 이 필드를 비워 두는 것이 좋습니다! 동일한 이름의 오디오 장치가 여러 개 있는 경우 다음 명령을 사용하여 장치 ID를 얻을 수 있습니다:", "audio_sink_placeholder_macos": "블랙홀 2채널", "audio_sink_placeholder_windows": "스피커(고화질 오디오 장치)", "av1_mode": "AV1 지원", "av1_mode_0": "선샤인은 인코더 기능에 따라 AV1 지원을 광고합니다(권장).", "av1_mode_1": "Apollo은 AV1에 대한 지원을 광고하지 않습니다.", "av1_mode_2": "선샤인은 AV1 메인 8비트 프로파일 지원을 광고합니다.", "av1_mode_3": "선샤인은 AV1 메인 8비트 및 10비트(HDR) 프로파일 지원을 광고할 예정입니다.", "av1_mode_desc": "클라이언트가 AV1 메인 8비트 또는 10비트 비디오 스트림을 요청할 수 있습니다. AV1은 인코딩 시 CPU를 더 많이 사용하므로 이 옵션을 활성화하면 소프트웨어 인코딩을 사용할 때 성능이 저하될 수 있습니다.", "back_button_timeout": "홈/가이드 버튼 에뮬레이션 시간 초과", "back_button_timeout_desc": "뒤로/선택 버튼을 지정된 밀리초 동안 누르고 있으면 홈/가이드 버튼 누름이 에뮬레이션됩니다. 값을 0 미만(기본값)으로 설정하면 뒤로/선택 버튼을 길게 눌러도 홈/가이드 버튼이 에뮬레이션되지 않습니다.", "capture": "특정 캡처 방법 강제 적용", "capture_desc": "자동 모드에서 Apollo은 가장 먼저 작동하는 것을 사용합니다. NvFBC에는 패치된 엔비디아 드라이버가 필요합니다.", "cert": "인증서", "cert_desc": "웹 UI와 Moonlight 클라이언트 페어링에 사용되는 인증서입니다. 최상의 호환성을 위해 RSA-2048 공개 키가 있어야 합니다.", "channels": "최대 연결 클라이언트 수", "channels_desc_1": "Apollo을 사용하면 하나의 스트리밍 세션을 여러 클라이언트와 동시에 공유할 수 있습니다.", "channels_desc_2": "일부 하드웨어 인코더에는 다중 스트림에서 성능을 저하시키는 제한이 있을 수 있습니다.", "coder_cabac": "카박 - 컨텍스트 적응형 이진 산술 코딩 - 더 높은 품질", "coder_cavlc": "cavlc - 컨텍스트 적응형 가변 길이 코딩 - 더 빠른 디코딩", "configuration": "구성", "controller": "게임패드 입력 활성화", "controller_desc": "게스트가 게임패드/컨트롤러로 호스트 시스템을 제어할 수 있습니다.", "credentials_file": "자격 증명 파일", "credentials_file_desc": "사용자 이름/비밀번호를 Apollo의 상태 파일과 별도로 저장하세요.", "dd_config_ensure_active": "디스플레이 자동 활성화", "dd_config_ensure_only_display": "다른 디스플레이를 비활성화하고 지정된 디스플레이만 활성화하기", "dd_config_ensure_primary": "디스플레이를 자동으로 활성화하고 기본 디스플레이로 설정하기", "dd_config_label": "장치 구성", "dd_config_revert_delay": "구성 되돌리기 지연", "dd_config_revert_delay_desc": "앱이 닫히거나 마지막 세션이 종료된 경우 구성을 되돌리기 전에 대기하는 추가 지연 시간(밀리초)입니다. 주요 목적은 앱 간에 빠르게 전환할 때 보다 원활한 전환을 제공하기 위한 것입니다.", "dd_config_verify_only": "디스플레이가 활성화되어 있는지 확인합니다(기본값).", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "클라이언트의 요청에 따라 HDR 모드 켜기/끄기(기본값)", "dd_hdr_option_disabled": "HDR 설정 변경하지 않기", "dd_mode_remapping": "디스플레이 모드 재매핑", "dd_mode_remapping_add": "리매핑 항목 추가", "dd_mode_remapping_desc_1": "요청된 해상도 및/또는 새로 고침 빈도를 다른 값으로 변경하려면 리매핑 항목을 지정합니다.", "dd_mode_remapping_desc_2": "목록은 위에서 아래로 반복되며 첫 번째 일치 항목이 사용됩니다.", "dd_mode_remapping_desc_3": "'요청됨' 필드는 요청된 값과 일치하도록 비워 둘 수 있습니다.", "dd_mode_remapping_desc_4_final_values_mixed": "'최종' 필드를 하나 이상 지정해야 합니다. 지정하지 않은 해상도 또는 새로 고침 빈도는 변경되지 않습니다.", "dd_mode_remapping_desc_4_final_values_non_mixed": "\"최종\" 필드는 반드시 지정해야 하며 비워 둘 수 없습니다.", "dd_mode_remapping_desc_5_sops_mixed_only": "문라이트 클라이언트에서 \"게임 설정 최적화\" 옵션을 활성화해야 하며, 그렇지 않으면 지정된 해상도 필드가 있는 항목은 건너뜁니다.", "dd_mode_remapping_desc_5_sops_resolution_only": "문라이트 클라이언트에서 \"게임 설정 최적화\" 옵션을 활성화해야 하며, 그렇지 않으면 매핑을 건너뜁니다.", "dd_mode_remapping_final_refresh_rate": "최종 새로고침 빈도", "dd_mode_remapping_final_resolution": "최종 해결", "dd_mode_remapping_requested_fps": "요청된 FPS", "dd_mode_remapping_requested_resolution": "요청된 해결 방법", "dd_options_header": "고급 디스플레이 장치 옵션", "dd_refresh_rate_option": "새로 고침 빈도", "dd_refresh_rate_option_auto": "클라이언트에서 제공한 FPS 값 사용(기본값)", "dd_refresh_rate_option_disabled": "새로 고침 빈도 변경하지 않기", "dd_refresh_rate_option_manual": "수동으로 입력한 새로고침 빈도 사용", "dd_refresh_rate_option_manual_desc": "사용할 새로 고침 빈도를 입력합니다.", "dd_resolution_option": "해상도", "dd_resolution_option_auto": "클라이언트에서 제공한 해상도 사용(기본값)", "dd_resolution_option_disabled": "해상도 변경 금지", "dd_resolution_option_manual": "수동으로 입력한 해상도 사용", "dd_resolution_option_manual_desc": "사용할 해상도를 입력합니다.", "dd_resolution_option_ogs_desc": "이 기능을 사용하려면 Moonlight 클라이언트에서 \"게임 설정 최적화\" 옵션이 활성화되어 있어야 합니다.", "dd_wa_hdr_toggle_desc": "가상 디스플레이 장치를 스트리밍에 사용할 때 잘못된 HDR 색상이 표시될 수 있습니다. 이 옵션을 활성화하면 선샤인은 이 문제를 완화하기 위해 노력합니다.", "dd_wa_hdr_toggle": "HDR을 위한 고대비 해결 방법 활성화", "ds4_back_as_touchpad_click": "지도 뒤로 가기/터치패드 클릭으로 선택", "ds4_back_as_touchpad_click_desc": "DS4 에뮬레이션을 강제할 때 뒤로/선택을 터치패드 클릭에 매핑합니다.", "encoder": "특정 인코더 강제 적용", "encoder_desc": "특정 인코더를 강제로 지정하지 않으면 Apollo에서 사용 가능한 최상의 옵션을 선택합니다. 참고: Windows에서 하드웨어 인코더를 지정하는 경우 디스플레이가 연결된 GPU와 일치해야 합니다.", "encoder_software": "소프트웨어", "external_ip": "외부 IP", "external_ip_desc": "외부 IP 주소가 지정되지 않은 경우 Apollo은 자동으로 외부 IP를 감지합니다.", "fec_percentage": "FEC 백분율", "fec_percentage_desc": "각 비디오 프레임의 데이터 패킷당 오류를 수정하는 패킷의 백분율입니다. 값이 높을수록 더 많은 네트워크 패킷 손실을 보정할 수 있지만 대역폭 사용량이 증가합니다.", "ffmpeg_auto": "자동 -- FFMPEG 결정에 맡김(기본값)", "file_apps": "앱 파일", "file_apps_desc": "현재 선샤인의 앱이 저장되어 있는 파일입니다.", "file_state": "상태 파일", "file_state_desc": "선샤인의 현재 상태가 저장된 파일입니다.", "gamepad": "에뮬레이트된 게임패드 유형", "gamepad_auto": "자동 선택 옵션", "gamepad_desc": "호스트에서 에뮬레이트할 게임패드 유형을 선택합니다.", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "DS4 선택 옵션", "gamepad_ds5": "DS5 (PS5)", "gamepad_switch": "닌텐도 프로 (스위치 컨트롤러)", "gamepad_manual": "DS4 수동 옵션", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "명령 준비", "global_prep_cmd_desc": "애플리케이션 실행 전 또는 실행 후에 실행할 명령 목록을 구성합니다. 지정된 준비 명령 중 하나라도 실패하면 애플리케이션 실행 프로세스가 중단됩니다.", "hevc_mode": "HEVC 지원", "hevc_mode_0": "선샤인은 인코더 기능에 따라 HEVC 지원을 광고합니다(권장).", "hevc_mode_1": "Apollo은 HEVC 지원을 광고하지 않습니다.", "hevc_mode_2": "선샤인은 HEVC 메인 프로필에 대한 지원을 광고합니다.", "hevc_mode_3": "선샤인은 HEVC 메인 및 메인10(HDR) 프로파일 지원을 광고할 예정입니다.", "hevc_mode_desc": "클라이언트가 HEVC 메인 또는 HEVC 메인10 비디오 스트림을 요청할 수 있도록 허용합니다. HEVC는 인코딩에 CPU를 더 많이 사용하므로 이 옵션을 활성화하면 소프트웨어 인코딩을 사용할 때 성능이 저하될 수 있습니다.", "high_resolution_scrolling": "고해상도 스크롤 지원", "high_resolution_scrolling_desc": "이 옵션을 활성화하면 Apollo은 Moonlight 클라이언트의 고해상도 스크롤 이벤트를 통과합니다. 고해상도 스크롤 이벤트로 너무 빠르게 스크롤하는 구형 애플리케이션의 경우 이 옵션을 비활성화하면 유용할 수 있습니다.", "install_steam_audio_drivers": "Steam 오디오 드라이버 설치", "install_steam_audio_drivers_desc": "Steam이 설치되어 있으면 5.1/7.1 서라운드 사운드와 호스트 오디오 음소거를 지원하는 Steam Streaming Speakers 드라이버가 자동으로 설치됩니다.", "key_repeat_delay": "키 반복 지연", "key_repeat_delay_desc": "키가 반복되는 속도를 제어합니다. 키가 반복되기 전 초기 지연 시간 (ms) 입니다.", "key_repeat_frequency": "키 반복 빈도", "key_repeat_frequency_desc": "매 초마다 키가 반복되는 빈도입니다. (이 옵션은 소수점 입력이 가능합니다.)", "key_rightalt_to_key_win": "오른쪽 Alt 키를 Windows 키로 매핑", "key_rightalt_to_key_win_desc": "달빛에서 Windows 키를 직접 보낼 수 없는 경우가 있을 수 있습니다. 이러한 경우 Apollo이 오른쪽 Alt 키를 Windows 키로 인식하도록 하는 것이 유용할 수 있습니다.", "keyboard": "키보드 입력 활성화", "keyboard_desc": "게스트가 키보드로 호스트 시스템을 제어할 수 있습니다.", "lan_encryption_mode": "LAN 암호화 모드", "lan_encryption_mode_1": "지원되는 클라이언트에서 사용 가능", "lan_encryption_mode_2": "모든 사용자에게 필수", "lan_encryption_mode_desc": "로컬 네트워크를 통해 스트리밍할 때 암호화를 사용할 시기를 결정합니다. 암호화는 특히 성능이 낮은 호스트와 클라이언트에서 스트리밍 성능을 저하시킬 수 있습니다.", "locale": "로캘", "locale_desc": "Apollo의 사용자 인터페이스에 사용되는 로캘입니다.", "log_level": "로그 레벨", "log_level_0": "Verbose", "log_level_1": "Debug", "log_level_2": "정보", "log_level_3": "경고", "log_level_4": "오류", "log_level_5": "치명적", "log_level_6": "없음", "log_level_desc": "표준 출력으로 인쇄되는 최소 로그 수준", "log_path": "로그 파일 경로", "log_path_desc": "선샤인의 현재 로그가 저장된 파일입니다.", "min_fps_factor": "최소 FPS 계수", "min_fps_factor_desc": "선샤인은 이 계수를 사용하여 프레임 사이의 최소 시간을 계산합니다. 이 값을 약간 높이면 정적인 콘텐츠를 주로 스트리밍할 때 도움이 될 수 있습니다. 값이 높을수록 더 많은 대역폭을 소비합니다.", "min_threads": "최소 CPU 스레드 수", "min_threads_desc": "이 값을 높이면 인코딩 효율이 약간 떨어지지만, 일반적으로 인코딩에 더 많은 CPU 코어를 사용할 수 있다는 점에서 그만한 가치가 있습니다. 이상적인 값은 하드웨어에서 원하는 스트리밍 설정으로 안정적으로 인코딩할 수 있는 가장 낮은 값입니다.", "misc": "기타 옵션", "motion_as_ds4": "클라이언트의 게임패드가 모션 기능을 보유중인 경우 DS4 게임패드로 에뮬레이트", "motion_as_ds4_desc": "비활성화시, 게임패드를 선택할 때 모션 센서 유무를 확인하지 않습니다.", "mouse": "마우스 입력 활성화", "mouse_desc": "게스트가 마우스로 호스트 시스템을 제어할 수 있습니다.", "native_pen_touch": "기본 펜/터치 지원", "native_pen_touch_desc": "활성화하면 선샤인은 문라이트 클라이언트의 기본 펜/터치 이벤트를 통과합니다. 이 기능은 기본 펜/터치를 지원하지 않는 구형 애플리케이션에서 비활성화하면 유용할 수 있습니다.", "notify_pre_releases": "사전 릴리스 알림", "notify_pre_releases_desc": "선샤인의 새 사전 출시 버전에 대한 알림 여부", "nvenc_h264_cavlc": "H.264에서 CABAC보다 CAVLC 선호", "nvenc_h264_cavlc_desc": "더 간단한 형태의 엔트로피 코딩. CAVLC는 동일한 화질을 위해 약 10% 더 많은 비트 전송률이 필요합니다. 아주 오래된 디코딩 장치에만 해당됩니다.", "nvenc_latency_over_power": "전력 절감보다 낮은 인코딩 지연 시간 선호", "nvenc_latency_over_power_desc": "선샤인은 인코딩 지연 시간을 줄이기 위해 스트리밍 중에 최대 GPU 클럭 속도를 요청합니다. 이 기능을 비활성화하면 인코딩 지연 시간이 크게 늘어날 수 있으므로 비활성화하는 것은 권장하지 않습니다.", "nvenc_opengl_vulkan_on_dxgi": "DXGI 위에 OpenGL/Vulkan 제공", "nvenc_opengl_vulkan_on_dxgi_desc": "Apollo은 DXGI 위에 표시되지 않는 한 전체 화면 OpenGL 및 Vulkan 프로그램을 풀 프레임 속도로 캡처할 수 없습니다. 이는 시스템 전체 설정으로, 선샤인 프로그램 종료 시 되돌려집니다.", "nvenc_preset": "성능 프리셋", "nvenc_preset_1": "(가장 빠름, 기본값)", "nvenc_preset_7": "(가장 느림)", "nvenc_preset_desc": "수치가 높을수록 인코딩 지연 시간이 증가하는 대신 압축(주어진 비트 전송률에서의 품질)이 향상됩니다. 네트워크 또는 디코더에 의해 제한되는 경우에만 변경하는 것이 좋으며, 그렇지 않은 경우 비트 전송률을 높여도 비슷한 효과를 얻을 수 있습니다.", "nvenc_realtime_hags": "하드웨어 가속 GPU 스케줄링에서 실시간 우선순위 사용", "nvenc_realtime_hags_desc": "현재 NVIDIA 드라이버는 HAGS가 활성화되어 있고 실시간 우선순위가 사용되며 VRAM 사용률이 최대에 가까울 때 인코더에서 멈출 수 있습니다. 이 옵션을 비활성화하면 우선순위가 높음으로 낮아져 GPU가 과부하 상태일 때 캡처 성능이 저하되는 대신 프리즈를 방지할 수 있습니다.", "nvenc_spatial_aq": "공간 AQ", "nvenc_spatial_aq_desc": "동영상의 평평한 영역에 더 높은 QP 값을 할당합니다. 낮은 비트레이트에서 스트리밍할 때 활성화하는 것이 좋습니다.", "nvenc_spatial_aq_disabled": "사용 안 함(더 빠름, 기본값)", "nvenc_spatial_aq_enabled": "사용(느림)", "nvenc_twopass": "투패스 모드", "nvenc_twopass_desc": "예비 인코딩 패스를 추가합니다. 이를 통해 더 많은 모션 벡터를 감지하고 프레임 전체에 비트 전송률을 더 잘 분배하며 비트 전송률 제한을 더 엄격하게 준수할 수 있습니다. 이 기능을 비활성화하면 가끔 비트레이트 오버슈팅과 그에 따른 패킷 손실이 발생할 수 있으므로 비활성화하는 것은 권장하지 않습니다.", "nvenc_twopass_disabled": "사용 안 함(가장 빠름, 권장하지 않음)", "nvenc_twopass_full_res": "전체 해상도(느림)", "nvenc_twopass_quarter_res": "분기 해상도(더 빠른, 기본값)", "nvenc_vbv_increase": "싱글 프레임 VBV/HRD 비율 증가", "nvenc_vbv_increase_desc": "기본적으로 선샤인은 단일 프레임 VBV/HRD를 사용하므로 인코딩된 동영상 프레임 크기가 요청된 비트레이트를 요청된 프레임 전송률로 나눈 값을 초과하지 않아야 합니다. 이 제한을 완화하면 지연 시간이 짧은 가변 비트레이트의 이점이 있지만 네트워크에 비트레이트 급증을 처리할 수 있는 버퍼 헤드룸이 없는 경우 패킷 손실이 발생할 수도 있습니다. 허용되는 최대 값은 400이며, 이는 인코딩된 동영상 프레임 상한 크기를 5배 늘린 것에 해당합니다.", "origin_web_ui_allowed": "Origin 웹 UI 허용", "origin_web_ui_allowed_desc": "웹 UI에 대한 액세스가 거부되지 않은 원격 엔드포인트 주소의 원본입니다.", "origin_web_ui_allowed_lan": "LAN에 있는 사용자만 웹 UI에 액세스할 수 있습니다.", "origin_web_ui_allowed_pc": "로컬호스트만 웹 UI에 액세스할 수 있습니다.", "origin_web_ui_allowed_wan": "누구나 웹 UI에 액세스할 수 있습니다.", "output_name_desc_unix": "선샤인이 시작되면 감지된 디스플레이 목록이 표시됩니다. 참고: 괄호 안의 ID 값을 사용해야 합니다. 아래는 예시이며, 실제 출력은 문제 해결 탭에서 확인할 수 있습니다.", "output_name_desc_windows": "캡처에 사용할 디스플레이 디바이스 ID를 수동으로 지정합니다. 설정하지 않으면 기본 디스플레이가 캡처됩니다. 참고: 위에서 GPU를 지정한 경우 이 디스플레이는 해당 GPU에 연결되어 있어야 합니다. 선샤인이 시작되면 감지된 디스플레이 목록이 표시됩니다. 아래는 예시이며, 실제 출력은 문제 해결 탭에서 확인할 수 있습니다.", "output_name_unix": "표시 번호", "output_name_windows": "장치 ID 표시", "ping_timeout": "핑 시간 초과", "ping_timeout_desc": "스트림을 종료하기 전에 달빛의 데이터를 대기하는 시간(밀리초)", "pkey": "개인 키", "pkey_desc": "웹 UI와 Moonlight 클라이언트 페어링에 사용되는 개인 키입니다. 최상의 호환성을 위해 RSA-2048 개인키를 사용해야 합니다.", "port": "포트", "port_alert_1": "Apollo은 1024 이하의 포트를 사용할 수 없습니다, 다시 확인하세요.", "port_alert_2": "65535 이상의 포트는 사용할 수 없습니다, 다시 확인하세요.", "port_desc": "Apollo에서 사용하는 포트 제품군 설정", "port_http_port_note": "이 포트를 사용하여 Moonlight에 연결하세요.", "port_note": "참고", "port_port": "포트", "port_protocol": "프로토콜", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "웹 UI를 인터넷에 노출하는 것은 보안상 위험합니다! 자신의 책임하에 진행하세요!", "port_web_ui": "웹 UI", "qp": "양자화 파라미터", "qp_desc": "일부 디바이스는 고정 비트 전송률을 지원하지 않을 수 있습니다. 이러한 디바이스에서는 대신 QP가 사용됩니다. 값이 높을수록 압축률이 높아지지만 품질은 떨어집니다.", "qsv_coder": "퀵싱크 코더(H264)", "qsv_preset": "퀵싱크 프리셋", "qsv_preset_fast": "빠름 (낮은 품질)", "qsv_preset_faster": "더 빠름 (더 낮은 품질)", "qsv_preset_medium": "중간 (기본값)", "qsv_preset_slow": "느림 (좋은 품질)", "qsv_preset_slower": "더 느림 (더 좋은 품질)", "qsv_preset_slowest": "가장 느림 (최고 품질)", "qsv_preset_veryfast": "가장 빠름 (최저 품질)", "qsv_slow_hevc": "느린 HEVC 인코딩 허용", "qsv_slow_hevc_desc": "이렇게 하면 구형 인텔 GPU에서 HEVC 인코딩이 가능하지만, GPU 사용량이 증가하고 성능이 저하될 수 있습니다.", "restart_note": "변경 사항을 적용하기 위해 Apollo이 다시 시작됩니다.", "sunshine_name": "Apollo 이름", "sunshine_name_desc": "Moonlight에서 표시되는 이름입니다. 지정하지 않으면 PC의 이름이 사용됩니다.", "sw_preset": "SW 프리셋", "sw_preset_desc": "인코딩 속도(초당 인코딩 프레임 수)와 압축 효율성(비트스트림의 비트당 품질) 간의 절충점을 최적화합니다. 기본값은 초고속입니다.", "sw_preset_fast": "빠름", "sw_preset_faster": "더 빠름", "sw_preset_medium": "중간", "sw_preset_slow": "느림", "sw_preset_slower": "더 느림", "sw_preset_superfast": "엄청 빠름 (기본값)", "sw_preset_ultrafast": "엄청 빠름", "sw_preset_veryfast": "매우 빠름", "sw_preset_veryslow": "매우 느림", "sw_tune": "SW Tune", "sw_tune_animation": "애니메이션 - 만화에 적합하며 더 높은 디블럭킹과 더 많은 기준 프레임을 사용합니다.", "sw_tune_desc": "사전 설정 후에 적용되는 튜닝 옵션입니다. 기본값은 제로 레이턴시입니다.", "sw_tune_fastdecode": "fastdecode - 특정 필터를 비활성화하여 더 빠르게 디코딩할 수 있습니다.", "sw_tune_film": "영화 - 고품질 영화 콘텐츠에 사용, 차단 해제 감소", "sw_tune_grain": "그레인 - 오래되고 거친 필름 소재의 그레인 구조를 보존합니다.", "sw_tune_stillimage": "스틸 이미지 - 슬라이드쇼와 같은 콘텐츠에 적합", "sw_tune_zerolatency": "제로 레이턴시 - 빠른 인코딩 및 저지연 스트리밍에 적합(기본값)", "touchpad_as_ds4": "클라이언트 게임패드가 터치패드가 있다고 보고하는 경우 DS4 게임패드 에뮬레이션", "touchpad_as_ds4_desc": "비활성화하면 게임패드 유형을 선택할 때 터치패드의 존재 여부가 고려되지 않습니다.", "upnp": "UPnP", "upnp_desc": "인터넷을 통한 스트리밍을 위한 포트 포워딩 자동 구성", "vaapi_strict_rc_buffer": "AMD GPU에서 H.264/HEVC에 대한 프레임 비트레이트 제한을 엄격하게 적용합니다.", "vaapi_strict_rc_buffer_desc": "이 옵션을 활성화하면 장면이 변경되는 동안 네트워크를 통해 프레임이 끊기는 것을 방지할 수 있지만 움직이는 동안 동영상 품질이 저하될 수 있습니다.", "virtual_sink": "가상 싱크", "virtual_sink_desc": "사용할 가상 오디오 장치를 수동으로 지정합니다. 설정하지 않으면 자동으로 장치가 선택됩니다. 자동으로 장치를 선택할려면 이 칸을 비우는 것을 권장드립니다.", "virtual_sink_placeholder": "Steam Streaming Speakers", "vt_coder": "비디오 툴박스 코더", "vt_realtime": "비디오툴박스 실시간 인코딩", "vt_software": "비디오툴박스 소프트웨어 인코딩", "vt_software_allowed": "허용됨", "vt_software_forced": "강제", "wan_encryption_mode": "WAN 암호화 모드", "wan_encryption_mode_1": "지원되는 클라이언트에 사용(기본값)", "wan_encryption_mode_2": "모든 사용자에게 필수", "wan_encryption_mode_desc": "인터넷을 통해 스트리밍할 때 암호화를 사용할 시기를 결정합니다. 암호화는 특히 성능이 낮은 호스트와 클라이언트에서 스트리밍 성능을 저하시킬 수 있습니다." }, "index": { "description": "Apollo은 Moonlight의 게임 스트리밍 프로그램 입니다.", "download": "다운로드", "installed_version_not_stable": "Apollo의 정식 출시 이전 버전을 실행 중입니다. 버그나 기타 문제가 발생할 수 있습니다. 문제가 발생하면 신고해 주세요. Apollo을 더 나은 소프트웨어로 만드는 데 도움을 주셔서 감사합니다!", "loading_latest": "새로운 업데이트를 확인하는 중...", "new_pre_release": "새로운 사전 출시 버전이 출시되었습니다!", "new_stable": "새로운 안정 버전이 출시되었습니다!", "startup_errors": "주의! 시작하는 중에 오류가 감지되었습니다. 스트리밍하기 전에 이 오류를 수정할 것을 강력히 권장합니다.", "version_dirty": "Apollo이 더 나은 소프트웨어가 될 수 있도록 도와주셔서 감사합니다!", "version_latest": "최신 버전의 Apollo을 실행 중입니다.", "welcome": "반가워요, Apollo!" }, "navbar": { "applications": "애플리케이션", "configuration": "설정", "home": "홈", "password": "비밀번호 변경", "pin": "핀", "theme_auto": "자동", "theme_dark": "다크 테마", "theme_light": "라이트 테마", "toggle_theme": "테마", "troubleshoot": "문제 해결" }, "password": { "confirm_password": "비밀번호 확인", "current_creds": "현재 자격 증명", "new_creds": "새 자격 증명", "new_username_desc": "지정하지 않으면 사용자 아이디가 변경되지 않습니다.", "password_change": "비밀번호 변경", "success_msg": "비밀번호가 성공적으로 변경되었습니다! 이 페이지가 곧 다시 로드되며 브라우저에서 새 자격 증명을 입력하라는 메시지가 표시됩니다." }, "pin": { "device_name": "장치 이름", "pair_failure": "페어링에 실패했습니다: PIN이 올바르게 입력되었는지 확인하세요.", "pair_success": "성공! 계속하려면 달빛을 확인해 주세요.", "pin_pairing": "PIN 페어링", "send": "보내기", "warning_msg": "페어링하려는 클라이언트에 대한 액세스 권한이 있는지 확인하세요. 이 소프트웨어는 컴퓨터를 완전히 제어할 수 있으므로 주의하세요!" }, "resource_card": { "github_discussions": "GitHub 토론", "legal": "법률", "legal_desc": "이 소프트웨어를 계속 사용함으로써 귀하는 다음 문서의 이용 약관에 동의하게 됩니다.", "license": "라이선스", "lizardbyte_website": "리자드바이트 웹사이트", "resources": "리소스", "resources_desc": "선샤인을 위한 리소스!", "third_party_notice": "제3자 고지" }, "troubleshooting": { "dd_reset": "영구 디스플레이 장치 설정 재설정", "dd_reset_desc": "변경된 디스플레이 장치 설정을 복원하는 데 문제가 있는 경우 설정을 재설정하고 수동으로 디스플레이 상태 복원을 진행할 수 있습니다.", "dd_reset_error": "지속성을 재설정하는 동안 오류가 발생했습니다!", "dd_reset_success": "지속성 재설정에 성공했습니다!", "force_close": "강제 종료", "force_close_desc": "현재 실행 중인 앱에 대해 문라이트가 불만을 제기하는 경우 앱을 강제 종료하면 문제가 해결됩니다.", "force_close_error": "애플리케이션을 닫는 동안 오류가 발생했습니다.", "force_close_success": "신청이 성공적으로 마감되었습니다!", "logs": "로그", "logs_desc": "Apollo이 업로드한 로그 보기", "logs_find": "찾기...", "restart_sunshine": "선샤인 다시 시작", "restart_sunshine_desc": "Apollo이 제대로 작동하지 않는다면 다시 시작해 보세요. 그러면 실행 중인 모든 세션이 종료됩니다.", "restart_sunshine_success": "선샤인이 다시 시작됩니다.", "troubleshooting": "문제 해결", "unpair_all": "모두 페어링 해제", "unpair_all_error": "페어링 해제 중 오류", "unpair_all_success": "페어링되지 않은 모든 장치.", "unpair_desc": "페어링된 장치를 제거합니다. 활성 세션이 있는 개별적으로 페어링되지 않은 장치는 연결된 상태로 유지되지만 세션을 시작하거나 다시 시작할 수는 없습니다.", "unpair_single_no_devices": "페어링된 장치가 없습니다.", "unpair_single_success": "그러나 디바이스가 여전히 활성 세션에 있을 수 있습니다. 열려 있는 세션을 종료하려면 위의 '강제 종료' 버튼을 사용하세요.", "unpair_single_unknown": "알 수 없는 클라이언트", "unpair_title": "장치 페어링 해제" }, "welcome": { "confirm_password": "비밀번호 확인", "create_creds": "시작하기 전에 웹 UI에 액세스하기 위한 새 사용자 아이디와 비밀번호를 만들어야 합니다.", "create_creds_alert": "선샤인의 웹 UI에 액세스하려면 아래 자격 증명이 필요합니다. 다시는 볼 수 없으니 안전하게 보관하세요!", "greeting": "Apollo에 오신 것을 환영합니다!", "login": "로그인", "welcome_success": "이 페이지는 곧 새로고침 될 것이며, 브라우저에서 다시 로그인을 해야 합니다." } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/pl.json ================================================ { "_common": { "apply": "Zastosuj", "auto": "Automatyczny", "autodetect": "Autowykrywanie (zalecane)", "beta": "(beta)", "cancel": "Anuluj", "disabled": "Wyłączony", "disabled_def": "Wyłączone (domyślnie)", "disabled_def_cbox": "Domyślnie: odznaczone", "dismiss": "Odrzuć", "do_cmd": "Polecenie Do", "elevated": "Podwyższone", "enabled": "Włączone", "enabled_def": "Włączone (domyślnie)", "enabled_def_cbox": "Domyślnie: zaznaczone", "error": "Błąd!", "note": "Uwaga:", "password": "Hasło", "run_as": "Uruchom jako administrator", "save": "Zapisz", "see_more": "Zobacz więcej", "success": "Sukces!", "undo_cmd": "Polecenie Undo", "username": "Nazwa użytkownika", "warning": "Ostrzeżenie!" }, "apps": { "actions": "Działania", "add_cmds": "Dodaj polecenia", "add_new": "Dodaj nowy", "app_name": "Nazwa aplikacji", "app_name_desc": "Nazwa aplikacji wyświetlana w aplikacji Moonlight", "applications_desc": "Aplikacje są odświeżane tylko po ponownym uruchomieniu klienta", "applications_title": "Aplikacje", "auto_detach": "Kontynuuj przesyłanie strumieniowe, jeśli aplikacja szybko się wyłączy", "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.", "cmd": "Polecenie", "cmd_desc": "Główna aplikacja do uruchomienia. Jeśli puste, żadna aplikacja nie zostanie uruchomiona.", "cmd_note": "Jeśli ścieżka do pliku wykonywalnego zawiera spacje, należy ująć ją w cudzysłów.", "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.", "cmd_prep_name": "Polecenia przygotowujące", "covers_found": "Znalezione okładki", "delete": "Usuń", "detached_cmds": "Polecenia odłączone", "detached_cmds_add": "Dodaj odłączone polecenie", "detached_cmds_desc": "Lista poleceń uruchamianych w tle.", "detached_cmds_note": "Jeśli ścieżka do pliku wykonywalnego zawiera spacje, należy ująć ją w cudzysłów.", "edit": "Edytuj", "env_app_id": "Identyfikator aplikacji", "env_app_name": "Nazwa aplikacji", "env_client_audio_config": "Konfiguracja audio wymagana przez klienta (2.0/5.1/7.1)", "env_client_enable_sops": "Klient zażądał opcji optymalizacji gry pod kątem optymalnego strumienia (prawda/fałsz)", "env_client_fps": "FPS żądane przez klienta (float)", "env_client_gcmap": "Żądana maska kontrolera w formacie zestawu bitów/pola bitów (liczba całkowita)", "env_client_hdr": "HDR jest włączony przez klienta (prawda/fałsz)", "env_client_height": "Wysokość żądana przez klienta (liczba całkowita)", "env_client_host_audio": "Klient zażądał dźwięku hosta (prawda/fałsz)", "env_client_width": "Szerokość żądana przez klienta (liczba całkowita)", "env_displayplacer_example": "Przykład - displayplacer dla automatycznej rozdzielczości:", "env_qres_example": "Przykład - QRes dla automatycznej rozdzielczości:", "env_qres_path": "ścieżka qres", "env_var_name": "Nazwa Var", "env_vars_about": "Informacje o zmiennych środowiskowych", "env_vars_desc": "Wszystkie polecenia domyślnie pobierają te zmienne środowiskowe:", "env_xrandr_example": "Przykład - Xrandr dla automatycznej rozdzielczości:", "exit_timeout": "Limit czasu wyjścia", "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 zero lub wartość ujemną, aplikacja zostanie natychmiast zakończona.", "find_cover": "Znajdź okładkę", "global_prep_desc": "Włączenie/wyłączenie wykonywania globalnych poleceń przygotowawczych dla tej aplikacji.", "global_prep_name": "Globalne polecenia przygotowawcze", "image": "Obraz", "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, Apollo wyśle domyślny obraz pudełka.", "loading": "Ładowanie...", "name": "Nazwa", "output_desc": "Plik, w którym przechowywane są dane wyjściowe polecenia, jeśli nie zostanie określony, dane wyjściowe zostaną zignorowane", "output_name": "Wyjście", "run_as_desc": "Może to być konieczne w przypadku niektórych aplikacji, które wymagają uprawnień administratora do prawidłowego działania.", "wait_all": "Kontynuuj przesyłanie strumieniowe, aż wszystkie procesy aplikacji zakończą działanie", "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.", "working_dir": "Katalog roboczy", "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, Apollo domyślnie wybierze katalog nadrzędny polecenia" }, "config": { "adapter_name": "Nazwa adaptera", "adapter_name_desc_linux_1": "Ręczne określenie procesora graficznego używanego do przechwytywania.", "adapter_name_desc_linux_2": "aby znaleźć wszystkie urządzenia obsługujące VAAPI", "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 Apollo, musi mieć co najmniej:", "adapter_name_desc_windows": "Ręczne określenie procesora graficznego używanego do przechwytywania. Jeśli nie zostanie ustawione, procesor graficzny zostanie wybrany automatycznie. Zdecydowanie zalecamy pozostawienie tego pola pustego, aby korzystać z automatycznego wyboru GPU! Uwaga: Ten procesor graficzny musi mieć podłączony i włączony wyświetlacz. Odpowiednie wartości można znaleźć za pomocą następującego polecenia:", "adapter_name_placeholder_windows": "Seria Radeon RX 580", "add": "Dodaj", "address_family": "Rodzina adresów", "address_family_both": "IPv4+IPv6", "address_family_desc": "Ustaw rodzinę adresów używaną przez Apollo", "address_family_ipv4": "Tylko IPv4", "always_send_scancodes": "Zawsze wysyłaj kody skanowania", "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.", "amd_coder": "AMF Coder (H264)", "amd_coder_desc": "Umożliwia wybór kodowania entropijnego w celu nadania priorytetu jakości lub szybkości kodowania. Tylko H.264.", "amd_enforce_hrd": "Wymuszanie AMF Hypothetical Reference Decoder (HRD)", "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.", "amd_preanalysis": "Wstępna analiza AMF", "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.", "amd_quality": "Jakość AMF", "amd_quality_balanced": "balanced -- zrównoważony (domyślnie)", "amd_quality_desc": "Kontroluje to równowagę między szybkością kodowania a jakością.", "amd_quality_group": "Ustawienia jakości AMF", "amd_quality_quality": "quality - preferuj jakość", "amd_quality_speed": "speed - preferuj szybkość", "amd_rc": "Kontrola prędkości AMF", "amd_rc_cbr": "cbr -- stały bitrate (zalecane, jeśli włączona jest funkcja HRD)", "amd_rc_cqp": "cqp -- stały tryb qp", "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.", "amd_rc_group": "Ustawienia kontroli prędkości AMF", "amd_rc_vbr_latency": "vbr_latency -- zmienna szybkość bitrate z ograniczonym opóźnieniem (zalecane, jeśli HRD jest wyłączone; domyślne)", "amd_rc_vbr_peak": "vbr_peak -- zmienna przepływność ograniczona wartością szczytową", "amd_usage": "Użycie AMF", "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.", "amd_usage_lowlatency": "lowlatency - niskie opóźnienie (najszybsze)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - niskie opóźnienie, wysoka jakość (szybko)", "amd_usage_transcoding": "transcoding -- transkodowanie (najwolniejsze)", "amd_usage_ultralowlatency": "ultralowlatency - ultra niskie opóźnienie (najszybsze; domyślne)", "amd_usage_webcam": "webcam -- kamera internetowa (wolno)", "amd_vbaq": "AMF Adaptacyjna kwantyzacja oparta na wariancji (VBAQ)", "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.", "apply_note": "Kliknij \"Zastosuj\", aby ponownie uruchomić Apollo i zastosować zmiany. Spowoduje to zakończenie wszystkich uruchomionych sesji.", "audio_sink": "Wejście audio", "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:", "audio_sink_desc_macos": "Nazwa odbiornika audio używanego dla Audio Loopback. Apollo 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.", "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:", "audio_sink_placeholder_macos": "BlackHole 2ch", "audio_sink_placeholder_windows": "Głośniki (High Definition Audio Device)", "av1_mode": "Wsparcie AV1", "av1_mode_0": "Apollo będzie reklamować obsługę AV1 w oparciu o możliwości enkodera (zalecane)", "av1_mode_1": "Apollo nie będzie reklamować wsparcia dla AV1", "av1_mode_2": "Apollo będzie zalecać wsparcie dla 8-bitowego profilu AV1 Main", "av1_mode_3": "Apollo będzie zalecać obsługę profili AV1 Main 8-bit i 10-bit (HDR)", "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.", "back_button_timeout": "Limit czasu emulacji przycisku Home/Guide", "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.", "capture": "Wymuś określoną metodę przechwytywania", "capture_desc": "W trybie automatycznym Apollo użyje pierwszego działającego sterownika. NvFBC wymaga poprawionych sterowników NVIDIA.", "cert": "Certyfikat", "cert_desc": "Certyfikat używany do parowania interfejsu użytkownika i klienta Moonlight. Aby uzyskać najlepszą kompatybilność, powinien on mieć klucz publiczny RSA-2048.", "channels": "Maksymalna liczba połączonych klientów", "channels_desc_1": "Apollo pozwala na udostępnianie pojedynczej sesji streamingowej wielu klientom jednocześnie.", "channels_desc_2": "Niektóre kodery sprzętowe mogą mieć ograniczenia, które zmniejszają wydajność przy wielu strumieniach.", "coder_cabac": "cabac -- adaptacyjne binarne kodowanie arytmetyczne - wyższa jakość", "coder_cavlc": "cavlc - adaptacyjne kodowanie kontekstowe o zmiennej długości - szybsze dekodowanie", "configuration": "Konfiguracja", "controller": "Włącz wejście kontrolera", "controller_desc": "Umożliwia gościom kontrolowanie systemu hosta za pomocą gamepada / kontrolera", "credentials_file": "Plik poświadczeń", "credentials_file_desc": "Przechowuj nazwę użytkownika/hasło oddzielnie od pliku stanu Apollo.", "dd_config_ensure_active": "Automatycznie aktywuj wyświetlanie", "dd_config_ensure_only_display": "Dezaktywuj inne wyświetlacze i aktywuj tylko określony wyświetlacz", "dd_config_ensure_primary": "Aktywuj ekran automatycznie i spraw, by był głównym wyświetlaczem", "dd_config_label": "Konfiguracja urządzenia", "dd_config_revert_delay": "Opóźnienie przywrócenia konfiguracji", "dd_config_revert_delay_desc": "Dodatkowe opóźnienie w milisekundach, aby poczekać przed przywróceniem konfiguracji po zamknięciu aplikacji lub zakończeniu ostatniej sesji. Głównym celem jest zapewnienie płynniejszego przejścia przy szybkiej zmianie pomiędzy aplikacjami.", "dd_config_verify_only": "Sprawdź, czy wyświetlacz jest włączony", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "Włącz/wyłącz tryb HDR zgodnie z żądaniem klienta (domyślnie)", "dd_hdr_option_disabled": "Nie zmieniaj ustawień HDR", "dd_mode_remapping": "Ponowne wyświetlanie trybu wyświetlania", "dd_mode_remapping_add": "Dodaj wpis reappingu", "dd_mode_remapping_desc_1": "Określ powtarzanie wpisów aby zmienić żądaną rozdzielczość i/lub częstotliwość odświeżania na inne wartości.", "dd_mode_remapping_desc_2": "Lista jest powtarzana od góry do dołu i użyto pierwszego meczu.", "dd_mode_remapping_desc_3": "Pola \"Wymagane\" mogą pozostać puste, aby dopasować dowolną żądaną wartość.", "dd_mode_remapping_desc_4_final_values_mixed": "Co najmniej jedno pole \"Final\" musi być określone. Nieokreślona rozdzielczość lub częstotliwość odświeżania nie zostaną zmienione.", "dd_mode_remapping_desc_4_final_values_non_mixed": "Pole \"Final\" musi być określone i nie może być puste.", "dd_mode_remapping_desc_5_sops_mixed_only": "Opcja \"Optymalizacja ustawień gry\" musi być włączona w Kliencie Luny Księżyca, w przeciwnym razie wpisy z określonymi polami rozdzielczości są pominięte.", "dd_mode_remapping_desc_5_sops_resolution_only": "Opcja \"Optymalizacja ustawień gry\" musi być włączona w Kliencie Lundicy, w przeciwnym razie mapowanie zostanie pominięte.", "dd_mode_remapping_final_refresh_rate": "Częstotliwość ostatecznego odświeżania", "dd_mode_remapping_final_resolution": "Ostateczna rozdzielczość", "dd_mode_remapping_requested_fps": "Żądany FPS", "dd_mode_remapping_requested_resolution": "Żądana rozdzielczość", "dd_options_header": "Zaawansowane opcje wyświetlania urządzenia", "dd_refresh_rate_option": "Częstotliwość odświeżania", "dd_refresh_rate_option_auto": "Użyj wartości FPS dostarczonej przez klienta (domyślnie)", "dd_refresh_rate_option_disabled": "Nie zmieniaj szybkości odświeżania", "dd_refresh_rate_option_manual": "Użyj ręcznie wprowadzonej prędkości odświeżania", "dd_refresh_rate_option_manual_desc": "Wprowadź szybkość odświeżania do użycia", "dd_resolution_option": "Rozdzielczość", "dd_resolution_option_auto": "Użyj rozdzielczości dostarczonej przez klienta (domyślnie)", "dd_resolution_option_disabled": "Nie zmieniaj rozdzielczości", "dd_resolution_option_manual": "Użyj ręcznie wprowadzonej rozdzielczości", "dd_resolution_option_manual_desc": "Wprowadź rozdzielczość do użycia", "dd_resolution_option_ogs_desc": "Opcja \"Optymalizacja ustawień gry\" musi być włączona w kliencie Księżyca, aby to działało.", "dd_wa_hdr_toggle_desc": "Gdy używasz wirtualnego urządzenia wyświetlającego jako streamingu, może wyświetlać niepoprawny kolor HDR. W tej opcji Apollo spróbuje złagodzić ten problem.", "dd_wa_hdr_toggle": "Włącz obsługę wysokiego kontrastu dla HDR", "ds4_back_as_touchpad_click": "Mapuj przycisk Wstecz/Wybierz na kliknięcie panelu dotykowego", "ds4_back_as_touchpad_click_desc": "Podczas wymuszania emulacji DS4, mapuj Back/Select na kliknięcie panelu dotykowego", "encoder": "Wymuś określony koder", "encoder_desc": "Wymuś określony koder, w przeciwnym razie Apollo 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.", "encoder_software": "Oprogramowanie", "external_ip": "Zewnętrzny adres IP", "external_ip_desc": "Jeśli nie podano zewnętrznego adresu IP, Apollo automatycznie wykryje zewnętrzny adres IP", "fec_percentage": "Procent FEC", "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.", "ffmpeg_auto": "auto -- niech ffmpeg zdecyduje (domyślnie)", "file_apps": "Plik aplikacji", "file_apps_desc": "Plik, w którym przechowywane są bieżące aplikacje Apollo.", "file_state": "Plik stanu", "file_state_desc": "Plik, w którym przechowywany jest aktualny stan Apollo", "gamepad": "Emulowany typ kontrolera", "gamepad_auto": "Opcje automatycznego wyboru", "gamepad_desc": "Wybierz typ kontrolera, który ma być emulowany na hoście", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "Opcje wyboru DS4", "gamepad_ds5": "DS5 (PS5)", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Ustawienia DS4", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Polecenia przygotowujące", "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.", "hevc_mode": "Obsługa HEVC", "hevc_mode_0": "Apollo będzie zalecać obsługę HEVC w oparciu o możliwości kodera (zalecane)", "hevc_mode_1": "Apollo nie będzie zalecać wsparcia dla HEVC", "hevc_mode_2": "Apollo będzie zalecać wsparcie dla głównego profilu HEVC", "hevc_mode_3": "Apollo będzie zalecać obsługę profili HEVC Main i Main10 (HDR)", "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.", "high_resolution_scrolling": "Obsługa przewijania w wysokiej rozdzielczości", "high_resolution_scrolling_desc": "Po włączeniu Apollo 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.", "install_steam_audio_drivers": "Zainstaluj sterowniki audio Steam", "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.", "key_repeat_delay": "Opóźnienie powtarzania klawiszy", "key_repeat_delay_desc": "Kontroluje szybkość powtarzania klawiszy. Początkowe opóźnienie w milisekundach przed powtarzaniem klawiszy.", "key_repeat_frequency": "Częstotliwość powtarzania klawiszy", "key_repeat_frequency_desc": "Jak często klawisze powtarzają się co sekundę. Ta konfigurowalna opcja obsługuje wartości dziesiętne.", "key_rightalt_to_key_win": "Przycisk Mapy Prawy Alt do klucza Windows", "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 Apollo myślał, że prawy Alt jest klawiszem Windows", "keyboard": "Włącz wejście klawiatury", "keyboard_desc": "Umożliwia gościom kontrolowanie systemu hosta za pomocą klawiatury", "lan_encryption_mode": "Tryb szyfrowania LAN", "lan_encryption_mode_1": "Włączone dla obsługiwanych klientów", "lan_encryption_mode_2": "Wymagane dla wszystkich klientów", "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.", "locale": "Język", "locale_desc": "Ustawienia językowe używane w interfejsie użytkownika Apollo.", "log_level": "Poziom raportowania", "log_level_0": "Rozszerzony", "log_level_1": "Debugowanie", "log_level_2": "Informacyjne", "log_level_3": "Ostrzeżenia", "log_level_4": "Błąd", "log_level_5": "Krytyczny", "log_level_6": "Brak", "log_level_desc": "Minimalny poziom logów wyświetlany w konsoli", "log_path": "Ścieżka pliku dziennika", "log_path_desc": "Plik, w którym przechowywane są bieżące dzienniki Apollo.", "min_fps_factor": "Minimalny współczynnik FPS", "min_fps_factor_desc": "Apollo użyje tego współczynnika do obliczenia minimalnego czasu między klatkami. Nieznaczne zwiększenie tej wartości może pomóc w przypadku strumieniowania głównie treści statycznych. Wyższe wartości będą zużywać więcej przepustowości.", "min_threads": "Minimalna liczba wątków procesora", "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.", "misc": "Różne opcje", "motion_as_ds4": "Emulacja kontrolera DS4, jeśli kliencki kontroler zgłasza obecność czujników ruchu", "motion_as_ds4_desc": "Jeśli opcja ta jest wyłączona, czujniki ruchu nie będą brane pod uwagę podczas wyboru typu kontrolera.", "mouse": "Włącz wejście myszy", "mouse_desc": "Umożliwia gościom kontrolowanie systemu hosta za pomocą myszy", "native_pen_touch": "Natywna obsługa pióra/dotyku", "native_pen_touch_desc": "Po włączeniu Apollo 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.", "notify_pre_releases": "Powiadomienia o wydaniu wstępnym", "notify_pre_releases_desc": "Czy otrzymywać powiadomienia o nowych przedpremierowych wersjach Apollo", "nvenc_h264_cavlc": "Preferowanie CAVLC nad CABAC w H.264", "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.", "nvenc_latency_over_power": "Niższe opóźnienie kodowania przedkładane nad oszczędność energii", "nvenc_latency_over_power_desc": "Apollo żą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.", "nvenc_opengl_vulkan_on_dxgi": "Prezentacja OpenGL/Vulkan na DXGI", "nvenc_opengl_vulkan_on_dxgi_desc": "Apollo 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 Apollo.", "nvenc_preset": "Wstępne ustawienia wydajności", "nvenc_preset_1": "(najszybszy, domyślny)", "nvenc_preset_7": "(najwolniejszy)", "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.", "nvenc_realtime_hags": "Użycie priorytetu czasu rzeczywistego w harmonogramie sprzętowej akceleracji procesora graficznego", "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.", "nvenc_spatial_aq": "Spatial AQ", "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.", "nvenc_spatial_aq_disabled": "Wyłączone (szybciej, domyślnie)", "nvenc_spatial_aq_enabled": "Włączone (wolniej)", "nvenc_twopass": "Tryb dwuprzebiegowy", "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.", "nvenc_twopass_disabled": "Wyłączony (najszybszy, niezalecany)", "nvenc_twopass_full_res": "Pełna rozdzielczość (wolniej)", "nvenc_twopass_quarter_res": "Rozdzielczość ćwiartki (szybsza, domyślna)", "nvenc_vbv_increase": "Procentowy wzrost VBV/HRD w pojedynczej ramce", "nvenc_vbv_increase_desc": "Domyślnie Apollo 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.", "origin_web_ui_allowed": "Interfejs Origin Web UI dozwolony", "origin_web_ui_allowed_desc": "Pochodzenie adresu zdalnego punktu końcowego, któremu nie odmówiono dostępu do interfejsu Web UI", "origin_web_ui_allowed_lan": "Tylko osoby w sieci LAN mogą uzyskać dostęp do interfejsu użytkownika", "origin_web_ui_allowed_pc": "Tylko localhost może uzyskać dostęp do Web UI", "origin_web_ui_allowed_wan": "Każdy może uzyskać dostęp do Web UI", "output_name_desc_unix": "Podczas uruchamiania Apollo 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.", "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 Apollo 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.", "output_name_unix": "Wyświetlany numer", "output_name_windows": "Wyświetl identyfikator urządzenia", "ping_timeout": "Limit czasu ping", "ping_timeout_desc": "Jak długo czekać w milisekundach na dane z Moonlight przed zamknięciem strumienia", "pkey": "Klucz prywatny", "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.", "port": "Port", "port_alert_1": "Apollo nie może używać portów poniżej 1024!", "port_alert_2": "Porty powyżej 65535 nie są dostępne!", "port_desc": "Ustaw rodzinę portów używanych przez Apollo", "port_http_port_note": "Ten port służy do łączenia się z Moonlight.", "port_note": "Uwaga", "port_port": "Port", "port_protocol": "Protokół", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "Wystawienie interfejsu użytkownika na Internet stanowi zagrożenie dla bezpieczeństwa! Postępuj na własne ryzyko!", "port_web_ui": "Web UI", "qp": "Parametr kwantyzacji", "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ść.", "qsv_coder": "Koder QuickSync (H264)", "qsv_preset": "Ustawienie wstępne QuickSync", "qsv_preset_fast": "szybki (niska jakość)", "qsv_preset_faster": "szybciej (niższa jakość)", "qsv_preset_medium": "średni (domyślnie)", "qsv_preset_slow": "wolny (dobra jakość)", "qsv_preset_slower": "wolniej (lepsza jakość)", "qsv_preset_slowest": "najwolniejszy (najlepsza jakość)", "qsv_preset_veryfast": "najszybszy (najniższa jakość)", "qsv_slow_hevc": "Zezwalaj na wolne kodowanie HEVC", "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.", "restart_note": "Apollo uruchamia się ponownie, aby zastosować zmiany.", "sunshine_name": "Nazwa Apollo", "sunshine_name_desc": "Nazwa wyświetlana przez Moonlight. Jeśli nie zostanie określona, używana jest nazwa hosta komputera", "sw_preset": "Ustawienia wstępne SW", "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.", "sw_preset_fast": "szybki", "sw_preset_faster": "szybciej", "sw_preset_medium": "średni", "sw_preset_slow": "wolny", "sw_preset_slower": "wolniejszy", "sw_preset_superfast": "superszybki (domyślnie)", "sw_preset_ultrafast": "ultraszybki", "sw_preset_veryfast": "bardzo szybki", "sw_preset_veryslow": "bardzo wolny", "sw_tune": "Dostrajanie SW", "sw_tune_animation": "animacja - dobra do kreskówek; wykorzystuje wyższe odblokowanie i więcej klatek referencyjnych", "sw_tune_desc": "Opcje strojenia, które są stosowane po ustawieniu wstępnym. Domyślne ustawienie to zerolatency.", "sw_tune_fastdecode": "fastdecode -- umożliwia szybsze dekodowanie poprzez wyłączenie niektórych filtrów", "sw_tune_film": "Film - użycie dla wysokiej jakości treści filmowych; obniża deblocking", "sw_tune_grain": "grain - zachowuje strukturę ziarna w starym, ziarnistym materiale filmowym", "sw_tune_stillimage": "stillimage - dobre dla zawartości podobnej do pokazu slajdów", "sw_tune_zerolatency": "zerolatency -- dobre dla szybkiego kodowania i strumieniowania z niskim opóźnieniem (domyślnie)", "touchpad_as_ds4": "Emulacja kontrolera DS4, jeśli kliencki kontroler zgłasza obecność touchpada", "touchpad_as_ds4_desc": "Jeśli opcja ta jest wyłączona, obecność touchpada nie będzie brana pod uwagę podczas wyboru typu kontrolera.", "upnp": "UPnP", "upnp_desc": "Automatycznie skonfiguruj przekierowanie portów do przesyłania strumieniowego przez Internet", "vaapi_strict_rc_buffer": "Ścisłe egzekwowanie limitów przepływności klatek dla H.264/HEVC na układach GPU AMD", "vaapi_strict_rc_buffer_desc": "Włączenie tej opcji pozwala uniknąć porzucania klatek przez sieć podczas zmiany sceny, ale jakość wideo może zostać obniżona podczas ruchu.", "virtual_sink": "Wirtualny odbiornik", "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!", "virtual_sink_placeholder": "Głośniki do strumieniowania Steam", "vt_coder": "Koder VideoToolbox", "vt_realtime": "Kodowanie w czasie rzeczywistym VideoToolbox", "vt_software": "Kodowanie oprogramowania VideoToolbox", "vt_software_allowed": "Dozwolone", "vt_software_forced": "Wymuszone", "wan_encryption_mode": "Tryb szyfrowania WAN", "wan_encryption_mode_1": "Włączone dla obsługiwanych klientów (domyślnie)", "wan_encryption_mode_2": "Wymagane dla wszystkich klientów", "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." }, "index": { "description": "Apollo jest samodzielnym hostem strumienia gry dla Moonlight.", "download": "Pobierz", "installed_version_not_stable": "Korzystasz z przedpremierowej wersji Apollo. Mogą wystąpić błędy lub inne problemy. Prosimy o zgłaszanie wszelkich napotkanych problemów. Dziękujemy za pomoc w ulepszaniu oprogramowania Apollo!", "loading_latest": "Ładowanie najnowszej wersji...", "new_pre_release": "Dostępna jest nowa wersja przedpremierowa!", "new_stable": "Nowa stabilna wersja jest już dostępna!", "startup_errors": "Uwaga! Apollo wykrył te błędy podczas uruchamiania. ZDECYDOWANIE ZALECAMY ich naprawienie przed rozpoczęciem streamowania.", "version_dirty": "Dziękujemy za pomoc w ulepszaniu oprogramowania Apollo!", "version_latest": "Korzystasz z najnowszej wersji Apollo", "welcome": "Witaj, Apollo!" }, "navbar": { "applications": "Aplikacje", "configuration": "Konfiguracja", "home": "Strona główna", "password": "Zmień hasło", "pin": "Pin", "theme_auto": "Auto", "theme_dark": "Ciemny", "theme_light": "Jasny", "toggle_theme": "Wygląd", "troubleshoot": "Rozwiązywanie problemów" }, "password": { "confirm_password": "Potwierdź hasło", "current_creds": "Aktualne dane logowania", "new_creds": "Nowe dane logowania", "new_username_desc": "Jeśli nie zostanie podane, nazwa użytkownika nie ulegnie zmianie", "password_change": "Zmiana hasła", "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." }, "pin": { "device_name": "Nazwa urządzenia", "pair_failure": "Parowanie nie powiodło się: Sprawdź, czy kod PIN został wpisany poprawnie", "pair_success": "Sukces! Sprawdź Moonlight, aby kontynuować", "pin_pairing": "Parowanie PIN", "send": "Wyślij", "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!" }, "resource_card": { "github_discussions": "Dyskusje GitHub", "legal": "Legal", "legal_desc": "Kontynuując korzystanie z tego oprogramowania, użytkownik wyraża zgodę na warunki określone w poniższych dokumentach.", "license": "Licencja", "lizardbyte_website": "Strona internetowa LizardByte", "resources": "Zasoby", "resources_desc": "Zasoby dla Apollo!", "third_party_notice": "Powiadomienia strony trzeciej" }, "troubleshooting": { "dd_reset": "Resetuj stałe ustawienia wyświetlacza", "dd_reset_desc": "Jeśli Apollo utknie próbując przywrócić zmienione ustawienia wyświetlacza, możesz zresetować ustawienia i ręcznie przywrócić stan wyświetlacza.", "dd_reset_error": "Błąd podczas resetowania trwałości!", "dd_reset_success": "Pomyślnie resetowano trwałość!", "force_close": "Wymuś zamknięcie", "force_close_desc": "Jeśli Moonlight skarży się na aktualnie uruchomioną aplikację, wymuszenie jej zamknięcia powinno rozwiązać problem.", "force_close_error": "Błąd podczas zamykania aplikacji", "force_close_success": "Aplikacja zamknięta pomyślnie!", "logs": "Dzienniki", "logs_desc": "Zobacz dzienniki przesłane przez Apollo", "logs_find": "Znajdź...", "restart_Apollo": "Restart Apollo", "restart_Apollo_desc": "Jeśli Apollo nie działa poprawnie, możesz spróbować uruchomić go ponownie. Spowoduje to zakończenie wszystkich uruchomionych sesji.", "restart_Apollo_success": "Apollo uruchamia się ponownie", "troubleshooting": "Rozwiązywanie problemów", "unpair_all": "Rozparuj wszystko", "unpair_all_error": "Błąd podczas rozłączania pary", "unpair_all_success": "Wszystkie urządzenia rozłączone.", "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.", "unpair_single_no_devices": "Nie ma sparowanych urządzeń.", "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.", "unpair_single_unknown": "Nieznany klient", "unpair_title": "Odparuj urządzenia" }, "welcome": { "confirm_password": "Potwierdź hasło", "create_creds": "Przed rozpoczęciem pracy należy utworzyć nową nazwę użytkownika i hasło dostępu do interfejsu użytkownika.", "create_creds_alert": "Poniższe dane uwierzytelniające są potrzebne do uzyskania dostępu do interfejsu użytkownika Apollo. Zachowaj je w bezpiecznym miejscu, ponieważ nigdy więcej ich nie zobaczysz!", "greeting": "Witamy w Apollo!", "login": "Login", "welcome_success": "Strona zostanie wkrótce przeładowana, a przeglądarka poprosi o podanie nowych danych uwierzytelniających" } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/pt.json ================================================ { "_common": { "apply": "Aplicar", "auto": "Automático", "autodetect": "Detetar automaticamente (recomendado)", "beta": "(beta)", "cancel": "Cancelar", "disabled": "Desabilitado", "disabled_def": "Desativado (padrão)", "disabled_def_cbox": "Padrão: desmarcado", "dismiss": "Descartar", "do_cmd": "Faça o Comando", "elevated": "Elevado", "enabled": "Ativado", "enabled_def": "Ativado (padrão)", "enabled_def_cbox": "Padrão: marcado", "error": "Erro!", "note": "Nota:", "password": "Palavra-passe", "run_as": "Executar como Administrador", "save": "Guardar", "see_more": "Ver mais", "success": "Sucesso!", "undo_cmd": "Desfazer Comando", "username": "Usuário:", "warning": "Aviso!" }, "apps": { "actions": "Ações.", "add_cmds": "Adicionar Comandos", "add_new": "Adicionar novo", "app_name": "Nome da aplicação", "app_name_desc": "Nome do aplicativo, como mostrado no Moonlight", "applications_desc": "Aplicações só são atualizadas quando o Cliente for reiniciado", "applications_title": "Aplicações", "auto_detach": "Continue transmitindo se o aplicativo fechar rapidamente", "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.", "cmd": "Comando", "cmd_desc": "O aplicativo principal a ser iniciado. Se em branco, nenhum aplicativo será iniciado.", "cmd_note": "Se o caminho para o comando conter espaços, você deve colocá-lo entre aspas.", "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.", "cmd_prep_name": "Preparações do Comando", "covers_found": "Capas encontradas", "delete": "excluir", "detached_cmds": "Comandos desanexados", "detached_cmds_add": "Adicionar Comando Desanexado", "detached_cmds_desc": "Uma lista de comandos a serem executados em segundo plano.", "detached_cmds_note": "Se o caminho para o comando conter espaços, você deve colocá-lo entre aspas.", "edit": "Alterar", "env_app_id": "ID do aplicativo", "env_app_name": "Nome do aplicativo", "env_client_audio_config": "A configuração de áudio solicitada pelo cliente (2.0/5.1/7.1)", "env_client_enable_sops": "O cliente solicitou a opção de otimizar o jogo para uma transmissão ideal (verdadeiro/falso)", "env_client_fps": "O FPS solicitado pelo cliente (float)", "env_client_gcmap": "A máscara de gamepad solicitada, em formato bitset/bitfield (int)", "env_client_hdr": "O HDR está ativado pelo cliente (verdadeiro/falso)", "env_client_height": "A altura solicitada pelo cliente (int)", "env_client_host_audio": "O cliente solicitou áudio de host (verdadeiro/falso)", "env_client_width": "A largura solicitada pelo cliente (int)", "env_displayplacer_example": "Exemplo - displayplacer para Automação de Resolução:", "env_qres_example": "Exemplo - QRes para Automação de Resolução:", "env_qres_path": "Caminho das configurações rápidas", "env_var_name": "Nome da Var", "env_vars_about": "Sobre Variáveis de Ambiente", "env_vars_desc": "Todos os comandos obtêm essas variáveis de ambiente por padrão:", "env_xrandr_example": "Exemplo - Xrandr para Automação de Resolução:", "exit_timeout": "Tempo Esgotado", "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.", "find_cover": "Encontrar capa", "global_prep_desc": "Ativar/desativar a execução de comandos de preparação global para este aplicativo.", "global_prep_name": "Comandos de Preparação Global", "image": "Imagem:", "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, Apollo irá enviar a imagem da caixa padrão.", "loading": "Carregandochar@@0", "name": "Nome", "output_desc": "O arquivo onde a saída do comando é armazenada, se não for especificado, a saída é ignorada", "output_name": "Saída", "run_as_desc": "Isto pode ser necessário para que alguns aplicativos que requerem permissões de administrador sejam executados corretamente.", "wait_all": "Continue transmitindo até que todos os processos de app saiam", "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.", "working_dir": "Diretório de trabalho", "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 Apollo será o padrão para o diretório pai do comando" }, "config": { "adapter_name": "Nome do adaptador", "adapter_name_desc_linux_1": "Especifique manualmente uma GPU para usar na captura.", "adapter_name_desc_linux_2": "para encontrar todos os dispositivos capazes do VAAPI", "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:", "adapter_name_desc_windows": "Especifique manualmente uma GPU para usar na captura. Se não definido, a GPU é escolhida automaticamente. É altamente recomendável deixar este campo em branco para usar a seleção GPU automática! Nota: Esta GPU deve ter um display conectado e ligado. Os valores apropriados podem ser encontrados usando o seguinte comando:", "adapter_name_placeholder_windows": "Radeon série RX 580", "add": "Adicionar", "address_family": "Família de endereços", "address_family_both": "IPv6 + IPv6", "address_family_desc": "Definir a família de endereços usada pelo Apollo", "address_family_ipv4": "Apenas IPv4", "always_send_scancodes": "Sempre enviar Scancodes", "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.", "amd_coder": "Codificador AMF (H264)", "amd_coder_desc": "Permite que você selecione a codificação entropia para priorizar a qualidade ou a velocidade de codificação. Somente H.264.", "amd_enforce_hrd": "Aplicação de Decodificador de Referência Hipotetica (HRD) AMF", "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.", "amd_preanalysis": "Pré-análise AMF", "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.", "amd_quality": "Qualidade AMF", "amd_quality_balanced": "Balanceado - balanceado (padrão)", "amd_quality_desc": "Isto controla o equilíbrio entre a velocidade de codificação e a qualidade.", "amd_quality_group": "Configurações de qualidade AMF", "amd_quality_quality": "qualidade -- preferir qualidade", "amd_quality_speed": "velocidade -- preferir velocidade", "amd_rc": "Controle de Taxa AMF", "amd_rc_cbr": "cbr -- taxa de bits constante (padrão)", "amd_rc_cqp": "cqp -- modo qp constante", "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.", "amd_rc_group": "Configurações de controle de taxa AMF", "amd_rc_vbr_latency": "vbr_latency -- bitrate variável limitado pela latência (recomendado se o HDR estiver desabilitado; padrão)", "amd_rc_vbr_peak": "vbr_pico -- pico de taxa de bits variável restrita", "amd_usage": "Uso do AMF", "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.", "amd_usage_lowlatency": "baixa latência - baixa latência (mais rápido)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - baixa latência, alta qualidade (rápido)", "amd_usage_transcoding": "transcodificação -- transcodificando (mais lento)", "amd_usage_ultralowlatency": "ultralowlatência - latência ultra baixa (mais rápida)", "amd_usage_webcam": "webcam -- câmera (lenta)", "amd_vbaq": "Variação da Variação Baseada na Quantização Adaptativa (VBAQ)", "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.", "apply_note": "Clique em 'Aplicar' para reiniciar o Apollo e aplicar as alterações. Isto encerrará todas as sessões em execução.", "audio_sink": "Pia de Áudio", "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:", "audio_sink_desc_macos": "O nome do sumidouro de áudio usado para o loop de áudio. O Apollo só pode acessar microfones no macOS devido a limitações do sistema. Para fazer streaming de áudio do sistema usando Soundflower ou BlackHole.", "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:", "audio_sink_placeholder_macos": "BlackHole 2ch", "audio_sink_placeholder_windows": "Alto-falantes (Dispositivo de Áudio de Alta Definição)", "av1_mode": "Suporte AV1", "av1_mode_0": "O Apollo anunciará o suporte para a AV1 com base nos recursos do codificador (recomendado)", "av1_mode_1": "O sol não anunciará o suporte para a AV1", "av1_mode_2": "O Apollo anunciará o suporte para o perfil AV1 de 8 bits", "av1_mode_3": "A luz do sol anunciará o suporte para os perfis AV1 (8-bit principal) e de 10 bits (HDR)", "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.", "back_button_timeout": "Tempo de Emulação do Botão Home/Guia", "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.", "capture": "Forçar um Método de Captura Específica", "capture_desc": "Modo automático Apollo usará o primeiro que funciona. NvFBC requer drivers nvidia corrigidos.", "cert": "Certificado", "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.", "channels": "Máximo de Clientes Conectados", "channels_desc_1": "Apollo pode permitir que uma única sessão de streaming seja compartilhada com vários clientes simultaneamente.", "channels_desc_2": "Alguns codificadores de hardware podem ter limitações que reduzem o desempenho com vários fluxos.", "coder_cabac": "cabac -- contexto adaptável de programação aritmética binária - qualidade superior", "coder_cavlc": "cavlc -- código adaptável de comprimento de variável de contexto - decodificação mais rápida", "configuration": "Configuração", "controller": "Enable Gamepad Input", "controller_desc": "Permite que os convidados controlem o sistema de host com controle / controle do gamepad", "credentials_file": "Arquivo de credenciais", "credentials_file_desc": "Armazenar Usuário/Senha separadamente do arquivo de estado da Apollo.", "dd_config_ensure_active": "Ativar a tela automaticamente", "dd_config_ensure_only_display": "Desativar outras exibições e ativar somente a exibição especificada", "dd_config_ensure_primary": "Ativar a tela automaticamente e torná-la uma tela primária", "dd_config_label": "Configuração do dispositivo", "dd_config_revert_delay": "Configurar atraso de reverter", "dd_config_revert_delay_desc": "Atraso adicional em milissegundos para esperar antes de reverter a configuração quando o aplicativo for fechado ou a última sessão for encerrada. O principal é proporcionar uma transição mais suave ao alternar rapidamente entre aplicativos.", "dd_config_verify_only": "Verifique se a tela está ativada", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "Ligar/desligar o modo HDR conforme solicitado pelo cliente (padrão)", "dd_hdr_option_disabled": "Não alterar as configurações do HDR", "dd_mode_remapping": "Exibir modo recondicionamento", "dd_mode_remapping_add": "Adicionar entrada de retração", "dd_mode_remapping_desc_1": "Especifique os registros de remessa para alterar a resolução solicitada e/ou a taxa de atualização para outros valores.", "dd_mode_remapping_desc_2": "A lista é iterada de cima para baixo e a primeira correspondência é usada.", "dd_mode_remapping_desc_3": "Os campos \"Solicitado\" podem ser vazios para corresponder a qualquer valor solicitado.", "dd_mode_remapping_desc_4_final_values_mixed": "Pelo menos um campo \"Final\" deve ser especificado. A resolução não especificada ou taxa de atualização não serão alteradas.", "dd_mode_remapping_desc_4_final_values_non_mixed": "O campo \"Final\" precisa ser especificado e não pode estar vazio.", "dd_mode_remapping_desc_5_sops_mixed_only": "A opção \"Otimizar configurações do jogo\" deve ser ativada no cliente de luar, caso contrário, as entradas com qualquer resolução especificada serão ignoradas.", "dd_mode_remapping_desc_5_sops_resolution_only": "Opção \"Otimizar configurações do jogo\" deve ser ativada no cliente do Luar, caso contrário o mapeamento será ignorado.", "dd_mode_remapping_final_refresh_rate": "Taxa de atualização final", "dd_mode_remapping_final_resolution": "Resolução final", "dd_mode_remapping_requested_fps": "FPS solicitado", "dd_mode_remapping_requested_resolution": "Resolução solicitada", "dd_options_header": "Opções avançadas do dispositivo", "dd_refresh_rate_option": "Taxa de atualização", "dd_refresh_rate_option_auto": "Usar valor de FPS fornecido pelo cliente (padrão)", "dd_refresh_rate_option_disabled": "Não alterar a taxa de atualização", "dd_refresh_rate_option_manual": "Usar taxa de atualização digitada manualmente", "dd_refresh_rate_option_manual_desc": "Digite a taxa de atualização a ser utilizada", "dd_resolution_option": "Resolução:", "dd_resolution_option_auto": "Resolução de uso fornecida pelo cliente (padrão)", "dd_resolution_option_disabled": "Não alterar a resolução", "dd_resolution_option_manual": "Usar resolução inserida manualmente", "dd_resolution_option_manual_desc": "Digite a resolução a ser usada", "dd_resolution_option_ogs_desc": "A opção \"Otimizar configurações do jogo\" deve estar ativada no cliente do Luar para que isto funcione.", "dd_wa_hdr_toggle_desc": "Ao usar o dispositivo de exibição virtual para streaming, ele pode exibir uma cor HDR incorreta. Com esta opção ativada, o Apollo tentará mitigar esse problema.", "dd_wa_hdr_toggle": "Ativar solução alternativa de alto contraste para HDR", "ds4_back_as_touchpad_click": "Mapear Voltar/Selecionar para o Touchpad Clique", "ds4_back_as_touchpad_click_desc": "Ao forçar a emulação do DS4, selecione um Voltar/Selecione para o Touchpad Clique", "encoder": "Forçar um Codificador Específico", "encoder_desc": "Força um codificador específico, caso contrário, Apollo 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.", "encoder_software": "Software", "external_ip": "IP externo", "external_ip_desc": "Se nenhum endereço IP externo for dado, Apollo detectará automaticamente IP externo", "fec_percentage": "Porcentagem FEC", "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.", "ffmpeg_auto": "auto -- let ffmpeg decide (padrão)", "file_apps": "Arquivo de apps", "file_apps_desc": "O arquivo onde os aplicativos atuais de Apollo são armazenados.", "file_state": "Arquivo de estado", "file_state_desc": "O arquivo onde o estado atual de Apollo é armazenado", "fps": "FPS anunciado", "gamepad": "Tipo de controle emulado", "gamepad_auto": "Opções de seleção automáticas", "gamepad_desc": "Escolha qual tipo de controle será emulado no host", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "Opções de seleção DS4", "gamepad_ds5": "DS5 (PS5)", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Opções de DS4 manual", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Preparações do Comando", "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.", "hevc_mode": "Suporte ao HEVC", "hevc_mode_0": "Apollo anunciará suporte para o HEVC com base em recursos de codificador (recomendado)", "hevc_mode_1": "O sol não anunciará o suporte ao HEVC", "hevc_mode_2": "O sol anunciará o suporte para o perfil principal do HEVC", "hevc_mode_3": "A luz do sol anunciará o suporte para os perfis HEVC Main e Main10 (HDR)", "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.", "high_resolution_scrolling": "Suporte a Alta Resolução", "high_resolution_scrolling_desc": "Quando habilitado, o Apollo 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.", "install_steam_audio_drivers": "Instalar drivers de áudio Steam", "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.", "key_repeat_delay": "Atraso da repetição da chave", "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.", "key_repeat_frequency": "Frequência de repetição de chave", "key_repeat_frequency_desc": "Com que frequência as chaves se repetem a cada segundo. Esta opção configurável suporta decimais.", "key_rightalt_to_key_win": "Tecla Alt Right Map para a tecla Windows", "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 Apollo pensar que a tecla Alt direita é a tecla Windows", "keyboard": "Habilitar Entrada de Teclado", "keyboard_desc": "Permite aos convidados controlar o sistema de host com o teclado", "lan_encryption_mode": "Modo de Criptografia LAN", "lan_encryption_mode_1": "Habilitado para clientes suportados", "lan_encryption_mode_2": "Obrigatório para todos os clientes", "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.", "locale": "Localidade", "locale_desc": "A localidade usada para a interface de usuário do Apollo.", "log_level": "Nível do Registro", "log_level_0": "Verbose", "log_level_1": "Debug", "log_level_2": "Informações", "log_level_3": "ATENÇÃO", "log_level_4": "ERRO", "log_level_5": "Fatal", "log_level_6": "Nenhuma", "log_level_desc": "O nível mínimo de log impresso no padrão", "log_path": "Caminho do Logfile", "log_path_desc": "O arquivo onde os logs atuais de Apollo são armazenados.", "min_fps_factor": "Fator mínimo de FPS", "min_fps_factor_desc": "O Apollo utilizará este fator para calcular o tempo mínimo entre quadros. Aumentar este valor ligeiramente pode ajudar ao fazer streaming de conteúdo principalmente estático. Valores mais altos consumirão mais largura de banda.", "min_threads": "Contagem mínima de tópicos da CPU", "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.", "misc": "Opções diversas", "motion_as_ds4": "Emular um gamepad DS4 se o cliente reportar sensores de movimento estiverem presentes", "motion_as_ds4_desc": "Se desativado, os sensores de movimento não serão tidos em conta durante a seleção de tipo gamepad", "mouse": "Habilitar Entrada do Mouse", "mouse_desc": "Permite aos convidados controlar o sistema de host com o mouse", "native_pen_touch": "Suporte nativo para Pen/Toque", "native_pen_touch_desc": "Quando ativado, o Apollo 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.", "notify_pre_releases": "Pré-Lançar notificações", "notify_pre_releases_desc": "Se deve ser notificado de novas versões de lançamento do Apollo", "nvenc_h264_cavlc": "Preferir CAVLC ao CABAC no H.264", "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.", "nvenc_latency_over_power": "Prefere latência de codificação inferior sobre economia de energia", "nvenc_latency_over_power_desc": "O Apollo 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.", "nvenc_opengl_vulkan_on_dxgi": "Apresentar OpenGL/Vulkan em cima de DXGI", "nvenc_opengl_vulkan_on_dxgi_desc": "O Apollo 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 Apollo do programa.", "nvenc_preset": "Predefinição de desempenho", "nvenc_preset_1": "(mais rápido, padrão)", "nvenc_preset_7": "(mais lento)", "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.", "nvenc_realtime_hags": "Use prioridade em tempo real em agendamento de gpu acelerado por hardware", "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.", "nvenc_spatial_aq": "Spatial AQ", "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.", "nvenc_spatial_aq_disabled": "Desativado (mais rápido, padrão)", "nvenc_spatial_aq_enabled": "Ativado (mais lento)", "nvenc_twopass": "Modo de duas passagens", "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.", "nvenc_twopass_disabled": "Desativado (mais rápido, não recomendado)", "nvenc_twopass_full_res": "Resolução completa (mais lento)", "nvenc_twopass_quarter_res": "Resolução de trimestre (mais rápido, padrão)", "nvenc_vbv_increase": "Porcentagem de VBV/HRD de Um-frame", "nvenc_vbv_increase_desc": "Por padrão, o Apollo 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.", "origin_web_ui_allowed": "Interface de Origem Web Permitida", "origin_web_ui_allowed_desc": "A origem do endereço do endpoint remoto que não é negado o acesso à Web UI", "origin_web_ui_allowed_lan": "Somente aqueles em LAN podem acessar a interface Web", "origin_web_ui_allowed_pc": "Somente localhost pode acessar a Web UI", "origin_web_ui_allowed_wan": "Alguém pode acessar a interface web", "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.", "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:", "output_name_unix": "Mostrar número", "output_name_windows": "Nome da saída", "ping_timeout": "Tempo limite", "ping_timeout_desc": "Quanto tempo esperar em milissegundos por dados do luar antes de desligar o fluxo", "pkey": "Chave Privada", "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.", "port": "Porta", "port_alert_1": "O sol não pode usar portas abaixo de 1024!", "port_alert_2": "Portas acima de 65535 não estão disponíveis!", "port_desc": "Definir a família dos portos usados pelo Apollo", "port_http_port_note": "Use esta porta para conectar com o Luar.", "port_note": "Observação", "port_port": "Porta", "port_protocol": "Protocol", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "Expor a interface da web à internet é um risco de segurança! Proceda por sua própria conta e risco!", "port_web_ui": "Web UI", "qp": "Parâmetro de Quantização", "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.", "qsv_coder": "Programador QuickSync (H264)", "qsv_preset": "QuickSync Preset", "qsv_preset_fast": "rápido (baixa qualidade)", "qsv_preset_faster": "mais rápido (menor qualidade)", "qsv_preset_medium": "médio (padrão)", "qsv_preset_slow": "lento (boa qualidade)", "qsv_preset_slower": "mais lento (melhor qualidade)", "qsv_preset_slowest": "mais lento (melhor qualidade)", "qsv_preset_veryfast": "mais rápido (menor qualidade)", "qsv_slow_hevc": "Permitir codificação lenta do HEVC", "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.", "res_fps_desc": "Os modos de exibição anunciados pelo Sol. Algumas versões do Moonlight, como Moonlight-nx (Switch), dependem destas listas para garantir que as resoluções e fugas solicitadas sejam apoiadas. Esta configuração não altera a forma como o fluxo de tela é enviado para o Moonlight.", "resolutions": "Resoluções anunciadas", "restart_note": "O sol está reiniciando para aplicar mudanças.", "sunshine_name": "Nome do Sol", "sunshine_name_desc": "O nome exibido pela luz da lua. Se não for especificado, o nome do host do PC é usado", "sw_preset": "Predefinições SW", "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.", "sw_preset_fast": "rápido", "sw_preset_faster": "mais rápido", "sw_preset_medium": "Médio", "sw_preset_slow": "devagar", "sw_preset_slower": "lento", "sw_preset_superfast": "super rápido (padrão)", "sw_preset_ultrafast": "anular", "sw_preset_veryfast": "veryfast", "sw_preset_veryslow": "veryslow", "sw_tune": "Ajuste SW", "sw_tune_animation": "animação -- boa para desenhos; usa maior debargamento e mais quadros de referência", "sw_tune_desc": "Ajuste as opções que são aplicadas após a predefinição. O padrão é zero.", "sw_tune_fastdecode": "fastdecode -- permite uma decodificação mais rápida desabilitando certos filtros", "sw_tune_film": "filme - usado para conteúdo de filmes de alta qualidade; reduz o deblocking", "sw_tune_grain": "grãos - preserva a estrutura de grãos em material cinematográfico antigo e cinzento", "sw_tune_stillimage": "ainda - bom para conteúdo parecido com a apresentação de slides", "sw_tune_zerolatency": "zerolatência -- bom para codificação rápida e streaming de baixa latência (padrão)", "touchpad_as_ds4": "Emule um gamepad DS4 se o cliente controla um touchpad estiver presente", "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.", "upnp": "UPNP", "upnp_desc": "Configurar automaticamente o encaminhamento de portas para transmissão na Internet", "vaapi_strict_rc_buffer": "Impedir com rigor limites de taxa de bits para H.264/HEVC nas GPUs AMD", "vaapi_strict_rc_buffer_desc": "Habilitar esta opção pode evitar frames lançados pela rede durante as mudanças de cena, mas a qualidade de vídeo pode ser reduzida durante o movimento.", "virtual_sink": "Pia Virtual", "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!", "virtual_sink_placeholder": "Alto-falantes de streaming Steam", "vt_coder": "VideoToolbox Coder", "vt_realtime": "Codificação em Tempo Real VideoToolbox", "vt_software": "Codificação VideoToolbox Software", "vt_software_allowed": "Permitido", "vt_software_forced": "Forçado", "wan_encryption_mode": "Modo de Criptografia WAN", "wan_encryption_mode_1": "Habilitado para clientes suportados (padrão)", "wan_encryption_mode_2": "Obrigatório para todos os clientes", "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." }, "index": { "description": "O sol é um anfitrião de jogos auto-hospedado para o Moonlight.", "download": "BAIXAR", "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!", "loading_latest": "Carregando a última versão...", "new_pre_release": "Uma nova versão de pré-lançamento está disponível!", "new_stable": "Uma nova versão Stable está disponível!", "startup_errors": "Atenção! A Apollo detectou estes erros durante o arranque. Recomendamos vivamente que os corrija antes de transmitir.", "version_dirty": "Obrigado por ajudar a fazer do sol um software melhor!", "version_latest": "Você está executando a última versão do Apollo", "welcome": "Olá, Apollo!" }, "navbar": { "applications": "Aplicações", "configuration": "Configuração", "home": "Residencial", "password": "Mudar a senha", "pin": "PIN", "theme_auto": "Automático", "theme_dark": "Escuro", "theme_light": "Fino", "toggle_theme": "Tema", "troubleshoot": "Solução de problemas" }, "password": { "confirm_password": "Confirmar senha", "current_creds": "Credenciais atuais", "new_creds": "Novas Credenciais", "new_username_desc": "Se não for especificado, o nome de usuário não irá mudar", "password_change": "Alteração de senha", "success_msg": "A senha foi alterada com sucesso! Essa página será recarregada em breve, seu navegador irá pedir as novas credenciais." }, "pin": { "device_name": "Nome do dispositivo", "pair_failure": "Pareamento Falhou: Verifique se o PIN foi digitado corretamente", "pair_success": "Sucesso! Por favor, verifique a Lua Lunar para continuar", "pin_pairing": "PIN Pairing", "send": "Mandar", "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!" }, "resource_card": { "github_discussions": "GitHub Discussions", "legal": "Informações", "legal_desc": "Ao continuar a usar este software, você concorda com os termos e condições nos seguintes documentos.", "license": "Tipo:", "lizardbyte_website": "Portal LizardByte", "resources": "Recursos", "resources_desc": "Recursos para luz solar!", "third_party_notice": "Aviso de terceiros" }, "troubleshooting": { "dd_reset": "Redefinir Configurações do Dispositivo de Exibição Persistente", "dd_reset_desc": "Se o Apollo estiver preso tentando restaurar as configurações alteradas do dispositivo de exibição, você pode redefinir as configurações e prosseguir para restaurar o estado da exibição manualmente.", "dd_reset_error": "Erro ao redefinir a persistência!", "dd_reset_success": "Sucesso ao redefinir a persistência!", "force_close": "Forçar fechamento", "force_close_desc": "Se o Moonlight reclamar de um aplicativo em execução, forçar o fechamento do aplicativo deve resolver o problema.", "force_close_error": "Erro ao fechar o aplicativo", "force_close_success": "Aplicativo fechado com sucesso!", "logs": "Registros", "logs_desc": "Veja os logs carregados por Apollo", "logs_find": "Localizar...", "restart_apollo": "Reiniciar o Apollo", "restart_apollo_desc": "Se o sol não estiver funcionando corretamente, você pode tentar reiniciá-lo. Isso encerrará todas as sessões em execução.", "restart_apollo_success": "A luz do sol está reiniciando", "troubleshooting": "Solução de problemas", "unpair_all": "Desconectar todos", "unpair_all_error": "Erro ao desemparelhar", "unpair_all_success": "Desemparelhado com sucesso!", "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.", "unpair_single_no_devices": "Não há dispositivos emparelhados.", "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.", "unpair_single_unknown": "Cliente Desconhecido", "unpair_title": "Desconectar dispositivos" }, "welcome": { "confirm_password": "Confirmar a senha", "create_creds": "Antes de começar, precisamos que você crie um novo nome de usuário e senha para acessar a interface da web.", "create_creds_alert": "As credenciais abaixo são necessárias para acessar a interface da Web do Apollo. Mantenha-as seguras, já que você nunca vai vê-las novamente!", "greeting": "Bem-vindo ao Sol!", "login": "Conectar", "welcome_success": "Esta página será recarregada em breve, seu navegador irá pedir novas credenciais" } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/pt_BR.json ================================================ { "_common": { "apply": "Aplicar", "auto": "Automático", "autodetect": "Autodetecção (recomendado)", "beta": "(beta)", "cancel": "Cancelar", "cmd_name": "Nome do Comando", "cmd_val": "Valor do Comando", "disabled": "Desativado", "disabled_def": "Desativado (padrão)", "disabled_def_cbox": "Padrão: desmarcado", "dismiss": "Dispensar", "do_cmd": "Executar Comando", "elevated": "Elevado", "enabled": "Ativado", "enabled_def": "Ativado (padrão)", "enabled_def_cbox": "Padrão: marcado", "error": "Erro!", "learn_more": "Aprender Mais", "note": "Observação:", "password": "Senha", "run_as": "Executar como Administrador", "save": "Salvar", "see_more": "Ver Mais", "success": "Sucesso!", "undo_cmd": "Desfazer Comando", "username": "Nome de Usuário", "warning": "Atenção!" }, "apps": { "actions": "Ações", "add_cmds": "Adicionar Comandos", "add_new": "Adicionar Novo", "allow_client_commands": "Permitir Comandos Preparativos do Cliente", "allow_client_commands_desc": "Se os Comandos Preparativos do Cliente devem ser executados ao abrir esse aplicativo.", "app_name": "Nome do Aplicativo", "app_name_desc": "Nome do aplicativo, conforme mostrado no Moonlight", "applications_desc": "Os aplicativos são atualizados somente quando o cliente é reiniciado", "applications_title": "Aplicativos", "auto_detach": "Continuar a transmissão se o aplicativo for encerrado rapidamente", "auto_detach_desc": "Isso tentará detectar automaticamente os Aplicativos-Lançadores que fecham rapidamente após iniciar outro programa ou instância deles mesmos. Quando um Aplicativo-Lançador é detectado, ele é tratado como um Aplicativo Desvinculado.", "close": "Encerrar", "close_warning": "Tem certeza que deseja encerrar o aplicativo em execução?", "close_failed": "O encerramento do aplicativo falhou.", "cmd": "Comando", "cmd_desc": "O aplicativo principal a ser iniciado. Se estiver em branco, nenhum aplicativo será iniciado.", "cmd_note": "Se o caminho para o executável do comando contiver espaços, você deverá colocá-lo entre aspas.", "cmd_prep_desc": "Uma lista de comandos a serem executados antes/depois desse aplicativo. Se algum dos Comandos Preparativos falhar, a inicialização do aplicativo será abortada.", "cmd_prep_name": "Comando Preparativos", "covers_found": "Capas encontradas", "delete": "Excluir", "delete_failed": "A remoção do aplicativo falhou: ", "detached_cmds": "Comandos Desvinculados", "detached_cmds_add": "Adicionar Comando Desvinculado", "detached_cmds_desc": "Uma lista de comandos a serem executados em segundo plano.", "detached_cmds_note": "Se o caminho para o executável do comando contiver espaços, você deverá colocá-lo entre aspas.", "edit": "Editar", "env_app_id": "ID do Aplicativo", "env_app_name": "Nome do Aplicativo", "env_client_audio_config": "A Configuração de Áudio solicitada pelo cliente (2.0/5.1/7.1)", "env_client_enable_sops": "O cliente solicitou a opção de otimizar o jogo para otimizar a transmissão (verdadeiro/falso)", "env_client_fps": "O FPS solicitado pelo cliente (float)", "env_client_gcmap": "A máscara de gamepad solicitada, em um formato de campo de bits (inteiro)", "env_client_hdr": "O cliente solicita a ativação do HDR (verdadeiro/falso)", "env_client_height": "A Altura da tela solicitada pelo cliente (inteiro)", "env_client_host_audio": "O cliente solicitou o áudio do servidor (verdadeiro/falso)", "env_client_width": "A Largura da tela solicitada pelo cliente (inteiro)", "env_client_uuid": "UID do cliente iniciando a transmissão (texto)", "env_client_name": "Nome do cliente iniciando a transmissão (texto)", "env_displayplacer_example": "Exemplo - displayplacer para Automação de Resolução:", "env_qres_example": "Exemplo - QRes para Automação de Resolução:", "env_qres_path": "Caminho do QRes", "env_var_name": "Nome da variável", "env_vars_about": "Sobre as variáveis de ambiente", "env_vars_desc": "Todos os comandos possuem essas variáveis de ambiente por padrão:", "env_xrandr_example": "Exemplo - Xrandr para Automação de Resolução:", "exit_timeout": "Tempo limite de saída", "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.", "find_cover": "Encontrar capa", "global_prep_desc": "Ativar/desativar a execução de Comandos Preparativos Globais para esse aplicativo.", "global_prep_name": "Comandos Preparativos Globais", "image": "Imagem", "image_desc": "Caminho do ícone/imagem do aplicativo que será enviado ao cliente. A imagem deve ser um arquivo PNG. Se não for definida, o Apollo enviará a imagem padrão.", "launch": "Abrir", "launch_warning": "Tem certeza que deseja abrir esse aplicativo? Isso vai encerrar o aplicativo em execução atual.", "launch_failed": "A abertura do aplicativo falhou: ", "loading": "Carregando...", "name": "Nome", "output_desc": "O arquivo em que a saída do comando é armazenada; se não for especificado, a saída será ignorada", "output_name": "Saída", "per_client_app_identity": "Identidade do aplicativo por cliente", "per_client_app_identity_desc": "Separa a identidade do aplicativo por cliente. Útil quando você quer usar esse aplicativo com configurações de monitores virtuais diferentes para cada cliente", "run_as_desc": "Isso pode ser necessário para alguns aplicativos que exigem permissões de administrador para serem executados corretamente.", "save_failed": "Falha ao salvar o aplicativo: ", "wait_all": "Continuar a transmissão até que todos os processos do aplicativo sejam encerrados", "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.", "working_dir": "Diretório de Trabalho", "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 Apollo será o diretório pai do comando", "virtual_display": "Sempre usar Monitor Virtual", "virtual_display_desc": "Sempre usar esse aplicativo com o monitor virtual, ignorando solicitações do cliente. Por favor, garanta que o driver SudoVDA está instalado e ativado.", "virtual_display_primary": "Forçar Monitor Virtual como primário", "virtual_display_primary_desc": "Automaticamente marca o monitor virtual como o monitor principal quando esse aplicativo iniciar. Monitores Virtuais sempre serão marcados como primérios quando o cliente solicitar usar o monitor virtual. (Recomendando manter marcado) [Não funciona no Windows 11 24H2]", "resolution_scale_factor": "Fator de Escala da Resolução", "resolution_scale_factor_desc": "Escala a resolução solicitada pelo cliente baseada nesse fator. Por ex., 2000x1000 com um fator de 120% se tornará 2400x1200. Quando o valor é diferente de 100%, ignora o Fator de Escala de Resolução solicitado pelo cliente. Não afeta a resolução de transmissão solicitada pelo cliente.", "use_app_identity": "Usar a identidade do aplicativo", "use_app_identity_desc": "Usa a própria identidade do aplicativo quando criando monitores virtuais ao invés da identidade do cliente. Isso é útil se você quer uma configuração de monitores para cada aplicativo individualmente." }, "client_card": { "clients": "Clientes", "clients_desc": "Clientes que foram especificamente aprimorados para funcionar bem com o Apollo.", "generic_moonlight_clients_desc": "Clientes genéricos Moonlight ainda funcionam com o Apollo." }, "config": { "adapter_name": "Nome da GPU", "adapter_name_desc_linux_1": "Especificar manualmente uma GPU a ser usada para captura.", "adapter_name_desc_linux_2": "para encontrar todos os dispositivos compatíveis com VAAPI", "adapter_name_desc_linux_3": "Substitua ``renderD129`` pelo dispositivo acima para listar o nome e os recursos do dispositivo. Para ser suportado pelo Apollo, ele precisa ter, no mínimo:", "adapter_name_desc_windows": "Especificar manualmente uma GPU a ser usada para captura. Se não for definido, a GPU será escolhida automaticamente. É altamente recomendável deixar esse campo em branco para usar a seleção automática de GPU! Observação: essa GPU deve ter uma tela conectada e ligada. Os valores apropriados podem ser encontrados usando o seguinte comando:", "adapter_name_placeholder_windows": "Série Radeon RX 580", "add": "Adicionar", "address_family": "Tipo de Endereço IP", "address_family_both": "IPv4+IPv6", "address_family_desc": "Definir os tipos de endereços IP usados pelo Apollo", "address_family_ipv4": "Somente IPv4", "always_send_scancodes": "Sempre enviar scancodes do teclado", "always_send_scancodes_desc": "O envio dos scancodes de teclado 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 servidor.", "amd_coder": "Codificador AMF (H264)", "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.", "amd_enforce_hrd": "Execução do Decodificador de Referência Hipotético (HRD) da AMF", "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.", "amd_preanalysis": "Pré-análise da AMF", "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.", "amd_quality": "Qualidade AMF", "amd_quality_balanced": "balanced -- balanceado (padrão)", "amd_quality_desc": "Isso controla o equilíbrio entre a velocidade e a qualidade da codificação.", "amd_quality_group": "Configurações de qualidade do AMF", "amd_quality_quality": "qualidade -- preferir qualidade", "amd_quality_speed": "velocidade -- preferir velocidade", "amd_rc": "Controle de taxa AMF", "amd_rc_cbr": "cbr -- taxa de bits constante (recomendado se o HRD estiver ativado)", "amd_rc_cqp": "cqp -- modo qp constante", "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.", "amd_rc_group": "Configurações do controle de taxa AMF", "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)", "amd_rc_vbr_peak": "vbr_peak -- taxa de bits variável com restrição de pico", "amd_usage": "Uso do AMF", "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.", "amd_usage_lowlatency": "lowlatency - baixa latência (mais rápido)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - baixa latência, alta qualidade (rápido)", "amd_usage_transcoding": "transcoding -- transcodificação (mais lento)", "amd_usage_ultralowlatency": "ultralowlatency - latência ultra baixa (mais rápido; padrão)", "amd_usage_webcam": "webcam -- webcam (lento)", "amd_vbaq": "Quantização adaptativa baseada em variância AMF (VBAQ)", "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.", "apply_note": "Clique em Aplicar para reiniciar o Apollo e aplicar as alterações. Isso encerrará todas as sessões em execução.", "audio_sink": "Saída de Áudio", "audio_sink_desc_linux": "O nome da saída de áudio usado para Loopback de áudio. Se você não especificar essa variável, o pulseaudio selecionará o dispositivo monitor padrão. Você pode encontrar o nome da saída de áudio usando qualquer um dos comandos:", "audio_sink_desc_macos": "O nome da saída de áudio usado para Loopback de áudio. O Apollo só pode acessar microfones no macOS devido a limitações do sistema. Para transmitir o áudio do sistema usando o Soundflower ou o BlackHole.", "audio_sink_desc_windows": "Especificar manualmente um dispositivo de áudio específico para saída. 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:", "audio_sink_placeholder_macos": "BlackHole 2ch", "audio_sink_placeholder_windows": "Alto-falantes (dispositivo de áudio de alta definição)", "auto_capture_sink": "Capturar automaticamente saída de áudio em uso", "auto_capture_sink_desc": "Captura automaticamente a saída de áudio em uso quando a saída padrão é alterada.", "av1_mode": "Suporte AV1", "av1_mode_0": "O Apollo anunciará o suporte para AV1 com base nos recursos do codificador (recomendado)", "av1_mode_1": "O Apollo não anunciará o suporte ao AV1", "av1_mode_2": "O Apollo anunciará o suporte ao perfil AV1 Main de 8 bits", "av1_mode_3": "O Apollo anunciará o suporte aos perfis AV1 Main de 8 e 10 bits (HDR)", "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.", "back_button_timeout": "Tempo limite de emulação do botão Início/Guia", "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.", "capture": "Forçar um método de captura específico", "capture_desc": "No modo automático, o Apollo usará o primeiro método que funcionar. O NvFBC requer drivers nvidia corrigidos.", "cert": "Certificado", "cert_desc": "O certificado usado para a interface Web e o emparelhamento do cliente Moonlight. Para melhor compatibilidade, ele deve ter uma chave pública RSA-2048.", "channels": "Máximo de clientes conectados", "channels_desc_1": "O Apollo pode permitir que uma única sessão de transmissão seja compartilhada com vários clientes simultaneamente.", "channels_desc_2": "Alguns codificadores de hardware podem ter limitações que reduzem o desempenho com vários transmissões.", "coder_cabac": "cabac -- codificação aritmética binária adaptável ao contexto - qualidade superior", "coder_cavlc": "cavlc -- codificação de comprimento variável adaptável ao contexto - decodificação mais rápida", "configuration": "Configuração", "controller": "Ativar entrada do controle de jogo", "controller_desc": "Permite que os convidados controlem o servidor com um gamepad/controle", "credentials_file": "Arquivo de credenciais", "credentials_file_desc": "Armazenar o nome de usuário/senha separadamente do arquivo de estado do Apollo.", "dd_config_ensure_active": "Ativar a tela automaticamente", "dd_config_ensure_only_display": "Desativar outras telas e ativar somente a tela especificada", "dd_config_ensure_primary": "Ativar a tela automaticamente e torná-la a tela primária", "dd_config_label": "Configuração do dispositivo", "dd_config_revert_delay": "Configurar atraso de reversão", "dd_config_revert_delay_desc": "Atraso adicional em milissegundos para esperar antes de reverter a configuração quando o aplicativo for fechado ou a última sessão for encerrada. O principal é proporcionar uma transição mais suave ao alternar rapidamente entre aplicativos.", "dd_config_revert_on_disconnect": "Reverter configurações ao desconectar", "dd_config_revert_on_disconnect_desc": "Reverter configurações ao se desconectar de todos os clientes, ao invés do encerramento do aplicativo ou da última sessão.", "dd_config_verify_only": "Verifique se a tela está ativada", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "Ligar/desligar o modo HDR conforme solicitado pelo cliente (padrão)", "dd_hdr_option_disabled": "Não alterar as configurações do HDR", "dd_mode_remapping": "Modo de Remapeamento da Tela", "dd_mode_remapping_add": "Adicionar entrada de remapeamento", "dd_mode_remapping_desc_1": "Especifique os registros de remapeamento para alterar a resolução solicitada e/ou a taxa de atualização para outros valores.", "dd_mode_remapping_desc_2": "A lista é iterada de cima para baixo e a primeira correspondência é usada.", "dd_mode_remapping_desc_3": "Os campos \"Solicitado\" podem ser vazios para corresponder a qualquer valor solicitado.", "dd_mode_remapping_desc_4_final_values_mixed": "Pelo menos um campo \"Final\" deve ser especificado. A resolução não especificada ou taxa de atualização não serão alteradas.", "dd_mode_remapping_desc_4_final_values_non_mixed": "O campo \"Final\" precisa ser especificado e não pode estar vazio.", "dd_mode_remapping_desc_5_sops_mixed_only": "A opção \"Otimizar configurações do jogo\" deve ser ativada no cliente Moonlight, caso contrário, as entradas com qualquer resolução especificada serão ignoradas.", "dd_mode_remapping_desc_5_sops_resolution_only": "Opção \"Otimizar configurações do jogo\" deve ser ativada no cliente Moonlight, caso contrário o mapeamento será ignorado.", "dd_mode_remapping_final_refresh_rate": "Taxa de atualização final", "dd_mode_remapping_final_resolution": "Resolução final", "dd_mode_remapping_requested_fps": "FPS solicitado", "dd_mode_remapping_requested_resolution": "Resolução solicitada", "dd_options_header": "Opções avançadas da tela", "dd_refresh_rate_option": "Taxa de atualização", "dd_refresh_rate_option_auto": "Usar valor de FPS fornecido pelo cliente (padrão)", "dd_refresh_rate_option_disabled": "Não alterar a taxa de atualização", "dd_refresh_rate_option_manual": "Usar taxa de atualização digitada manualmente", "dd_refresh_rate_option_manual_desc": "Digite a taxa de atualização a ser utilizada", "dd_resolution_option": "Resolução:", "dd_resolution_option_auto": "Usar resolução fornecida pelo cliente (padrão)", "dd_resolution_option_disabled": "Não alterar a resolução", "dd_resolution_option_manual": "Usar resolução inserida manualmente", "dd_resolution_option_manual_desc": "Digite a resolução a ser usada", "dd_resolution_option_ogs_desc": "A opção \"Otimizar configurações do jogo\" deve estar ativada no cliente Moonlight para que isto funcione.", "dd_wa_hdr_toggle_delay_desc_1": "Ao usar uma tela virtual para transmissão, ele pode exibir uma cor HDR incorreta. Com esta opção ativada, o Apollo tentará mitigar esse problema desligando e religando o HDR.", "dd_wa_hdr_toggle_delay_desc_2": "Se o valor for 0, essa solução é desativada (padrão). Se o valor for entre 0 e 3000 milissegundos, Apollo vai desativar o HDR, esperar pelo tempo especificado e então ligar o HDR novamente. O atraso recomendado é de aproximadamente 500 milissegundos para a maioria dos casos.", "dd_wa_hdr_toggle_delay_desc_3": "NÃO UTILIZE essa solução a não ser que você tenha problemas com o HDR, visto que isso afeta diretamente o tempo de iniciação da transmissão!", "dd_wa_hdr_toggle_delay": "Solução alternativa de alto contraste para HDR", "double_refreshrate": "Taxa de atualização dobrada para Monitores Virtuais", "double_refreshrate_desc": "Dobra a taxa de atualização quando criando monitores virtuais, mantendo a taxa de atualização da transmissão inalterada. Pode potencialmente melhorar problemas com engasgos de quadros em alguns sistemas.", "ds4_back_as_touchpad_click": "Mapear Voltar/Selecionar para o clique no touchpad", "ds4_back_as_touchpad_click_desc": "Ao forçar a emulação DS4, mapeia o Voltar/Selecionar para o clique no touchpad", "enable_input_only_mode": "Ativar Modo de Somente Controle", "enable_input_only_mode_desc": "Adiciona uma entrada de Aplicativo de Somente Controle. Quando ativado, a lista de aplicativos vai mostrar somente o aplicativo em execução e a entrada de Somente Controle quando estiver transmitindo. A entrada Somente Controle não receberá imagem nem áudio. Isso é útil quando controlando o computador pela TV ou conectando periféricos que a TV não suporta.", "enable_pairing": "Ativar Pareamento", "enable_pairing_desc": "Ativa pareamento com um cliente Moonlight. Isso permite o cliente autenticar com o servidor e estabelecer uma conexão segura.", "encoder": "Forçar um codificador específico", "encoder_desc": "Force um codificador específico; caso contrário, o Apollo 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.", "encoder_software": "Software", "external_ip": "IP externo", "external_ip_desc": "Se nenhum endereço IP externo for fornecido, o Apollo detectará automaticamente o IP externo", "fallback_mode": "Modo Monitor de Reserva", "fallback_mode_desc": "Apollo vai usar esse modo quando o cliente não providenciar um modo ou quando o aplicativo é iniciado através da UI da web. Formato: [Largura]x[Altura]x[FPS]", "fallback_mode_error": "Modo de reserva inválido. Formato: [Largura]x[Altura]x[FPS]", "fec_percentage": "Porcentagem de FEC", "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.", "ffmpeg_auto": "auto -- deixa o ffmpeg decidir (padrão)", "file_apps": "Arquivo de aplicativos", "file_apps_desc": "O arquivo em que são armazenados os aplicativos atuais do Apollo.", "file_state": "Arquivo de estado", "file_state_desc": "O arquivo em que são armazenados o estado atual do Apollo", "gamepad": "Tipo de gamepad emulado", "gamepad_auto": "Opções de seleção automática", "gamepad_desc": "Escolha o tipo de gamepad a ser emulado no servidor", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "Opções de seleção DS4", "gamepad_ds5": "DS5 (PS5)", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Opções manuais do DS4", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Comando Preparativo", "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 preparativos especificados falhar, o processo de inicialização do aplicativo será abortado.", "headless_mode": "Modo Discreto", "headless_mode_desc": "Inicia Apollo no modo discreto. Quando ativado, todos os aplicativos vão iniciar no monitor virtual.", "hevc_mode": "Suporte a HEVC", "hevc_mode_0": "O Apollo anunciará o suporte para HEVC com base nos recursos do codificador (recomendado)", "hevc_mode_1": "O Apollo não anunciará suporte para HEVC", "hevc_mode_2": "O Apollo anunciará o suporte ao perfil principal HEVC", "hevc_mode_3": "O Apollo anunciará o suporte aos perfis HEVC Main e Main10 (HDR)", "hevc_mode_desc": "Permite que o cliente solicite transmissões 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.", "hide_tray_controls": "Esconder controles no ícone de bandeja", "hide_tray_controls_desc": "Não exibir \"Forçar fechamento\", \"Reiniciar\" e \"Sair\" do menu do ícone de bandeja.", "high_resolution_scrolling": "Suporte à rolagem de alta resolução", "high_resolution_scrolling_desc": "Quando ativado, o Apollo transmitirá os eventos de rolagem de alta resolução dos clientes Moonlight. Isso pode ser útil desativar para aplicativos mais antigos que rolam muito rápido com eventos de rolagem de alta resolução.", "install_steam_audio_drivers": "Instalar os drivers de áudio do Steam", "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 servidor.", "keep_sink_default": "Manter saída de áudio virtual como padrão", "keep_sink_default_desc": "Se o uso da saída de áudio virtual será forçada como padrão (útil quando a saída de áudio do servidor está desabilitado)", "key_repeat_delay": "Atraso de repetição de tecla", "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.", "key_repeat_frequency": "Frequência de repetição da tecla", "key_repeat_frequency_desc": "A frequência com que as teclas se repetem a cada segundo. Essa configuração aceita decimais.", "key_rightalt_to_key_win": "Tecla Alt direito como tecla Windows", "key_rightalt_to_key_win_desc": "Pode ser que você não consiga enviar a tecla Windows diretamente do cliente Moonlight. Nesses casos, pode ser útil fazer com que o Apollo interprete a tecla Alt direito como a tecla Windows", "keyboard": "Ativar entrada de teclado", "keyboard_desc": "Permite que os convidados controlem o teclado do servidor", "lan_encryption_mode": "Modo de criptografia da LAN", "lan_encryption_mode_1": "Ativado para clientes compatíveis", "lan_encryption_mode_2": "Necessário para todos os clientes", "lan_encryption_mode_desc": "Isso determina quando a criptografia será usada durante a transmissão pela rede local. A criptografia pode reduzir o desempenho da transmissão, principalmente em servidores e clientes menos potentes.", "limit_framerate": "Limitar taxa de quadros da captura", "limit_framerate_desc": "Limita a taxa de quadros da captura pela taxa de quadros solicitado pelo cliente. Pode não executar com taxa de quadros máxima se VSync estiver ativado e a taxa de atualização do monitor não for igual ao solicitado. Pode causar lag em algums clientes se desabilitado.", "locale": "Linguagem", "locale_desc": "A linguagem usada na interface de usuário do Apollo.", "log_level": "Nível do log", "log_level_0": "Verboso", "log_level_1": "Depuração", "log_level_2": "Informação", "log_level_3": "Advertência", "log_level_4": "Erro", "log_level_5": "Fatal", "log_level_6": "Nenhum", "log_level_desc": "O nível mínimo do log impresso na saída padrão", "log_path": "Caminho do arquivo de log", "log_path_desc": "O arquivo em que os logs atuais do Apollo são armazenados.", "max_bitrate": "Taxa de bits máximo", "max_bitrate_desc": "A taxa máxima de bits (em Kbps) que Apollo usará para condificar a transmissão. Se for 0, sempre usará a taxa de bits solicitada pelo cliente Artemis/Moonlight.", "min_fps_factor": "Fator mínimo de FPS", "min_fps_factor_desc": "O Apollo usará esse fator para calcular o tempo mínimo entre os quadros. Aumentar um pouco esse valor pode ajudar na transmissão de conteúdo predominantemente estático. Valores mais altos consumirão mais largura de banda.", "min_threads": "Quantidade mínima de threads da CPU", "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 transmissão desejadas em seu hardware.", "misc": "Opções diversas", "motion_as_ds4": "Emular um gamepad DS4 se o gamepad do cliente informar que há sensores de movimento presentes", "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.", "mouse": "Ativar entrada do mouse", "mouse_desc": "Permite que os convidados controlem o mouse do servidor", "native_pen_touch": "Suporte nativo a caneta/toque", "native_pen_touch_desc": "Quando ativado, o Apollo transmitirá os eventos nativos de caneta/toque dos clientes Moonlight. Isso pode ser útil desativar para aplicativos mais antigos sem suporte nativo a caneta/toque.", "notify_pre_releases": "Notificações de pré-lançamento", "notify_pre_releases_desc": "Se deseja ser notificado sobre novas versões de pré-lançamento do Apollo", "nvenc_h264_cavlc": "Prefira o CAVLC ao CABAC em H.264", "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.", "nvenc_intra_refresh": "Intra Refresh", "nvenc_intra_refresh_desc": "Ativa Intra Refresh para que alguns clientes consigam continuamente renderizar corretamente (por ex. Clientes Xbox)", "nvenc_latency_over_power": "Preferir uma latência de codificação menor do que a economia de energia", "nvenc_latency_over_power_desc": "O Apollo solicita a velocidade máxima do clock da GPU durante a transmissão para reduzir a latência da codificação. Não é recomendável desativar, pois isso pode levar a um aumento significativo da latência de codificação.", "nvenc_opengl_vulkan_on_dxgi": "Apresentar OpenGL/Vulkan por cima do DXGI", "nvenc_opengl_vulkan_on_dxgi_desc": "O Apollo não consegue capturar programas OpenGL e Vulkan em tela cheia com taxa de quadros total, a menos que eles sejam apresentados em cima do DXGI. Essa é uma configuração de todo o sistema e que é revertida ao sair do programa Apollo.", "nvenc_preset": "Predefinição de desempenho", "nvenc_preset_1": "(mais rápido, padrão)", "nvenc_preset_7": "(mais lento)", "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, prefira aumentar a taxa de bits para ter maior qualidade.", "nvenc_realtime_hags": "Usar prioridade Tempo Real no escalonador GPU acelerado por hardware", "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 de Tempo Real para Alta, evitando o congelamento ao custo de um desempenho de captura reduzido quando a GPU está muito carregada.", "nvenc_spatial_aq": "AQ espacial", "nvenc_spatial_aq_desc": "Atribui valores de QP (parâmetros de quantização) mais altos a regiões planas do vídeo. Recomenda-se ativá-lo ao fazer transmissão com taxas de bits mais baixas.", "nvenc_spatial_aq_disabled": "Desativado (mais rápido, padrão)", "nvenc_spatial_aq_enabled": "Ativado (mais lento)", "nvenc_twopass": "Modo de duas passagens", "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.", "nvenc_twopass_disabled": "Desativado (mais rápido, não recomendado)", "nvenc_twopass_full_res": "Resolução total (mais lenta)", "nvenc_twopass_quarter_res": "Resolução de um quarto (mais rápida, padrão)", "nvenc_vbv_increase": "Aumento percentual de VBV/HRD em um único quadro", "nvenc_vbv_increase_desc": "Por padrão, o Apollo 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.", "origin_web_ui_allowed": "Origens permitidas para acesso da UI da Web", "origin_web_ui_allowed_desc": "Onde na rede o usuário é deve estar para possuir acesso à interface de usuário da Web", "origin_web_ui_allowed_lan": "Somente as pessoas na LAN podem acessar a interface do usuário da Web", "origin_web_ui_allowed_pc": "Somente o localhost pode acessar a interface do usuário da Web", "origin_web_ui_allowed_wan": "Qualquer pessoa pode acessar interface de usuário da Web", "output_name_desc_unix": "Durante a inicialização do Apollo, você deverá ver uma 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.", "output_name_desc_windows": "Especifique manualmente o ID do monitor a ser usado para captura. Se não for definido, o monitor principal será capturado. Observação: se você especificou uma GPU acima, esse monitor deverá estar conectado a essa GPU. Durante a inicialização do Apollo, 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.", "output_name_unix": "Número do monitor", "output_name_windows": "ID do monitor", "ping_timeout": "Tempo limite de ping", "ping_timeout_desc": "Quanto tempo esperar, em milissegundos, pelos dados do Moonlight antes de encerrar a transmissão", "pkey": "Chave privada", "pkey_desc": "A chave privada usada para a interface do usuário da Web e o pareamento do cliente Moonlight. Para melhor compatibilidade, essa deve ser uma chave privada RSA-2048.", "port": "Porta", "port_alert_1": "O Apollo não pode usar portas abaixo de 1024!", "port_alert_2": "As portas acima de 65535 não estão disponíveis!", "port_desc": "Definir a família de portas usadas pelo Apollo", "port_http_port_note": "Use essa porta para se conectar ao Moonlight.", "port_note": "Observação", "port_port": "Porta", "port_protocol": "Protocolo", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "Expor a interface do usuário da Web à Internet é um risco à segurança! Prossiga por sua própria conta e risco!", "port_web_ui": "UI da Web", "qp": "Parâmetro de quantização", "qp_desc": "Alguns dispositivos podem não suportar Constant Bit Rate. Para esses dispositivos, o QP (parâmetro de quantização) é usado em seu lugar. Um valor mais alto significa mais compactação, mas menos qualidade.", "qsv_coder": "Codificador QuickSync (H264)", "qsv_preset": "Predefinição de QuickSync", "qsv_preset_fast": "rápido (baixa qualidade)", "qsv_preset_faster": "mais rápido (qualidade inferior)", "qsv_preset_medium": "médio (padrão)", "qsv_preset_slow": "lento (boa qualidade)", "qsv_preset_slower": "mais lento (melhor qualidade)", "qsv_preset_slowest": "mais lento (melhor qualidade)", "qsv_preset_veryfast": "mais rápido (qualidade mais baixa)", "qsv_slow_hevc": "Permitir codificação lenta de HEVC", "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.", "restart_note": "O Apollo está sendo reiniciado para aplicar as alterações.", "server_cmd": "Comandos de Servidor", "server_cmd_desc": "Configura uma lista de comandos a serem executados no servidor quando solicitados durante uma transmissão pelo cliente.", "sunshine_name": "Nome Apollo", "sunshine_name_desc": "O nome exibido pelo Moonlight. Se não for especificado, será usado o nome do PC servidor", "sw_preset": "Predefinições de SW", "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.", "sw_preset_fast": "rápido", "sw_preset_faster": "mais rápido", "sw_preset_medium": "médio", "sw_preset_slow": "lento", "sw_preset_slower": "mais lento", "sw_preset_superfast": "super rápido (padrão)", "sw_preset_ultrafast": "ultra rápido", "sw_preset_veryfast": "muito rápido", "sw_preset_veryslow": "muito lento", "sw_tune": "Ajustes finos do SW", "sw_tune_animation": "animação -- bom para desenhos animados; usa desblocagem mais alta e mais quadros de referência", "sw_tune_desc": "Opções de ajuste, que são aplicadas após a predefinição. O padrão é zerolatency.", "sw_tune_fastdecode": "fastdecode -- permite uma decodificação mais rápida ao desativar determinados filtros", "sw_tune_film": "filme - use para conteúdo de filme de alta qualidade; reduz a desblocagem", "sw_tune_grain": "granulação -- preserva a estrutura de granulação em material de filme antigo e granulado", "sw_tune_stillimage": "stillimage -- bom para conteúdo do tipo apresentação de slides", "sw_tune_zerolatency": "zerolatency -- bom para codificação rápida e transmissão de baixa latência (padrão)", "touchpad_as_ds4": "Emular um gamepad DS4 se o gamepad do cliente informar que há um touchpad presente", "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.", "upnp": "UPnP", "upnp_desc": "Configurar automaticamente o redirecionamento de portas para transmissão pela Internet", "vaapi_strict_rc_buffer": "Impor limites estritos de taxa de bits de quadros para H.264/HEVC em GPUs AMD", "vaapi_strict_rc_buffer_desc": "A ativação dessa opção pode evitar a queda de quadros na rede durante as mudanças de cena, mas a qualidade do vídeo pode ser reduzida durante o movimento.", "virtual_sink": "Saída de Áudio Virtual", "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!", "virtual_sink_placeholder": "Steam Streaming Speakers", "vt_coder": "Codificador do VideoToolbox", "vt_realtime": "Codificação em tempo real do VideoToolbox", "vt_software": "Codificação de software VideoToolbox", "vt_software_allowed": "Permitido", "vt_software_forced": "Forçado", "wan_encryption_mode": "Modo de criptografia WAN", "wan_encryption_mode_1": "Ativado para clientes compatíveis (padrão)", "wan_encryption_mode_2": "Necessário para todos os clientes", "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 servidores e clientes menos potentes." }, "login": { "save_password": "Lembrar senha" }, "index": { "description": "O Apollo é um servidor de transmissão de jogos auto-hospedado para o Moonlight.", "download": "Baixar", "installed_version_not_stable": "Você está executando uma versão de pré-lançamento do Apollo. É possível que você encontre bugs ou outros problemas. Informe todos os problemas que encontrar. Obrigado por ajudar a tornar o Apollo um software melhor!", "loading_latest": "Carregando a versão mais recente...", "new_pre_release": "Uma nova versão de pré-lançamento está disponível!", "new_stable": "Uma nova versão estável está disponível!", "startup_errors": "Atenção! A Apollo detectou esses erros durante a inicialização. RECOMENDAMOS FORTEMENTE corrigi-los antes da transmissão.", "version_dirty": "Obrigado por ajudar a tornar o Apollo um software melhor!", "version_latest": "Você está executando a versão mais recente do Apollo", "welcome": "Olá, Apollo!" }, "navbar": { "applications": "Aplicativos", "configuration": "Configuração", "home": "Início", "password": "Alterar senha", "pin": "PIN", "theme_auto": "Automático", "theme_dark": "Escuro", "theme_light": "Claro", "toggle_theme": "Tema", "troubleshoot": "Solução de Problemas" }, "password": { "confirm_password": "Confirmar senha", "current_creds": "Credenciais atuais", "new_creds": "Novas credenciais", "new_username_desc": "Se não for especificado, o nome de usuário não será alterado", "password_change": "Alteração de senha", "success_msg": "A senha foi alterada com sucesso! Esta página será recarregada em breve e seu navegador solicitará as novas credenciais." }, "permissions": { "input_controller": "Entrada de controle", "input_touch": "Entrada de toque", "input_pen": "Entrada de caneta", "input_mouse": "Entrada de mouse", "input_kbd": "Entrada de teclado", "clipboard_set": "Escrever na área de transferência", "clipboard_read": "Ler da área de transferência", "file_upload": "Upload de arquivos", "file_dwnload": "Download de arquivos", "server_cmd": "Comandos de servidor", "list": "Lista de aplicativos", "view": "Ver transmissões", "launch": "Iniciar aplicativos" }, "pin": { "client_do_cmd": "Comandos de conexão do cliente", "client_do_cmd_desc": "Comandos a serem executados quando o cliente conecta. Todos os comandos são executados como desvinculados.", "client_undo_cmd": "Comandos de desconexão do cliente", "client_undo_cmd_desc": "Comandos a serem executados quando o cliente desconecta. Todos os comandos são executados como desvinculados.", "device_name": "Nome do dispositivo", "display_mode_override": "Substituição do modo de monitor", "display_mode_override_desc": "Apollo vai ignorar o modo de monitor solicitado pelo cliente e usar esse valor para configurar os monitores (virtuais). Deixe em branco para correspondência automática. Formato: [Largura]x[Altura]x[FPS]", "display_mode_override_error": "Substituição de modo inválido. Formato: [Largura]x[Altura]x[FPS]", "pair_failure": "Falha no pareamento: Verifique se o PIN foi digitado corretamente", "pair_success": "Sucesso! Por favor, verifique o Moonlight para continuar", "pair_success_check_perm": "Pareamento bem-sucedido! Por favor conceda manualmente abaixo as permissões necessárias para o cliente.", "pin_pairing": "Pareamento por PIN", "send": "Enviar", "warning_msg": "Certifique-se de ter acesso ao cliente com o qual está fazendo o pareamento. Esse software pode dar controle total ao seu computador, portanto, tenha cuidado!", "otp_pairing": "Pareamento por OTP", "generate_pin": "Gerar PIN", "otp_passphrase": "Frase-senha de Uso Único (OTP)", "otp_expired": "EXPIRADO", "otp_expired_msg": "OTP expirado. Por favor, solicite um novo.", "otp_success": "Solicitação de PIN bem sucedido, o PIN estará disponível em até 3 minutos.", "otp_msg": "Pareamento por OTP só está disponível para os clientes Artemis mais recentes. Por favor, use o método de pareamento legado para outros clientes.", "otp_pair_now": "PIN gerado com sucesso, você quer parear agora?", "device_management": "Gerenciamento de Dispositivos", "device_management_desc": "Gerencie os seus dispositivos pareados e suas permissões.", "device_management_warning": "O primeiro dispositivo pareado terá todas as permissões ativadas por padrão. Todos dispositivos pareados depois terão por padrão o mínimo de permissões.", "save_client_error": "Erro salvando o cliente: ", "unpair_all": "Desparear todos", "unpair_all_success": "Todos os dispositivos foram despareados.", "unpair_all_error": "Erro ao desparear", "unpair_single_no_devices": "Não há dispositivos pareados.", "unpair_single_success": "Despareamento bem-sucedido. Entretanto, 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.", "unpair_single_unknown": "Cliente desconhecido" }, "resource_card": { "github_discussions": "Discussões no GitHub", "legal": "Legal", "legal_desc": "Ao continuar a usar este software, você concorda com os termos e condições dos documentos a seguir.", "license": "Licença", "lizardbyte_website": "Site da LizardByte", "resources": "Recursos", "resources_desc": "Recursos para o Apollo!", "third_party_notice": "Aviso de terceiros" }, "troubleshooting": { "dd_reset": "Redefinir Configurações do Monitor Persistente", "dd_reset_desc": "Se o Apollo estiver preso tentando restaurar as configurações alteradas do monitor, você pode redefinir as configurações e prosseguir para restaurar o estado do monitor manualmente.", "dd_reset_error": "Erro ao redefinir a persistência!", "dd_reset_success": "Sucesso ao redefinir a persistência!", "force_close": "Forçar fechamento", "force_close_desc": "Se o Moonlight reclamar de um aplicativo em execução, forçar o fechamento do aplicativo deve corrigir o problema.", "force_close_error": "Erro ao fechar o aplicativo", "force_close_success": "Aplicativo encerrado com sucesso!", "logs": "Logs", "logs_desc": "Veja os logs carregados pelo Apollo", "logs_find": "Procurar...", "restart_apollo": "Reiniciar o Apollo", "restart_apollo_desc": "Se o Apollo não estiver funcionando corretamente, você pode tentar reiniciá-lo. Isso encerrará todas as sessões em execução.", "restart_apollo_success": "O Apollo está reiniciando", "quit_apollo": "Encerrar Apollo", "quit_apollo_desc": "Encerrar Apollo. Isso vai encerrar todas as sessões ativas.", "quit_apollo_success": "Apollo encerrou.", "quit_apollo_success_ongoing": "Apollo está encerrando...", "quit_apollo_confirm": "Você tem certeza que deseja encerrar Apollo? Você não poderá iniciar Apollo novamente se você não tiver outras formas de controlar o seu computador.", "troubleshooting": "Solução de Problemas" }, "welcome": { "confirm_password": "Confirmar senha", "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.", "create_creds_alert": "As credenciais abaixo são necessárias para acessar a interface de usuário da Web do Apollo. Mantenha-as em segurança, pois você nunca mais as verá!", "greeting": "Bem-vindo ao Apollo!", "login": "Login", "welcome_success": "Esta página será recarregada em breve e seu navegador solicitará as novas credenciais", "login_success": "Essa página vai recarregar automaticamente em breve." } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/ru.json ================================================ { "_common": { "apply": "Применить", "auto": "Автоматически", "autodetect": "Автоопределение (рекомендуется)", "beta": "(бета)", "cancel": "Отмена", "disabled": "Отключено", "disabled_def": "Отключено (по умолчанию)", "disabled_def_cbox": "По умолчанию: снято", "dismiss": "Отклонить", "do_cmd": "Команда запуска", "elevated": "Требуются", "enabled": "Включено", "enabled_def": "Включено (по умолчанию)", "enabled_def_cbox": "По умолчанию: проверено", "error": "Ошибка!", "note": "Примечание:", "password": "Пароль", "run_as": "Права администратора", "save": "Сохранить", "see_more": "Подробнее", "success": "Успех!", "undo_cmd": "Команда закрытия", "username": "Имя пользователя", "warning": "Предупреждение!" }, "apps": { "actions": "Действия", "add_cmds": "Добавить команды", "add_new": "Добавить новое", "app_name": "Название приложения", "app_name_desc": "Имя приложения, для показа в Moonlight", "applications_desc": "Приложения обновятся только после перезапуска клиента", "applications_title": "Приложения", "auto_detach": "Продолжить трансляцию, если приложение завершит работу быстро", "auto_detach_desc": "Пытаться автоматически обнаружить приложения-лаунчеры, которые быстро закрываются после запуска другой программы или собственной копии. Когда такое приложение обнаружено, оно рассматривается как независимое.", "cmd": "Команда", "cmd_desc": "Основное приложение для запуска. Если пустое, то не будет запущено приложение.", "cmd_note": "Если путь к исполняемому файлу содержит пробелы, вы должны заключить его в кавычки.", "cmd_prep_desc": "Список команд, которые должны быть выполнены до/после этого приложения. Если одна из команд не выполнена, запуск приложения прерывается.", "cmd_prep_name": "Команды подготовки", "covers_found": "Найденные обложки", "delete": "Удалить", "detached_cmds": "Независимые команды", "detached_cmds_add": "Добавить независимую команду", "detached_cmds_desc": "Список команд, работающих в фоновом режиме.", "detached_cmds_note": "Если путь к исполняемому файлу содержит пробелы, следует заключить его в кавычки.", "edit": "Изменить", "env_app_id": "ID приложения", "env_app_name": "Название приложения", "env_client_audio_config": "Запрошенная клиентом конфигурация аудио (2.0/5.1/7.1)", "env_client_enable_sops": "Клиент запросил оптимизацию настроек игры для потокового вещания (истина/ложь)", "env_client_fps": "Частота кадров, запрошенная клиентом (float)", "env_client_gcmap": "Запрашиваемая маска геймпада, в формате bitset/bitfield (целое)", "env_client_hdr": "HDR включен клиентом (истина/ложь)", "env_client_height": "Высота, запрошенная клиентом (целое)", "env_client_host_audio": "Клиент запросил звук с сервера (истина/ложь)", "env_client_width": "Ширина, запрошенная клиентом (целое)", "env_displayplacer_example": "Пример - displayplacer для автоматизации решения:", "env_qres_example": "Пример: автопереключение разрешения через QRes:", "env_qres_path": "путь qres", "env_var_name": "Переменная окружения", "env_vars_about": "О переменных среды", "env_vars_desc": "Всем командам по умолчанию передаются эти переменные окружения:", "env_xrandr_example": "Пример - Xrandr для автоматизации решения:", "exit_timeout": "Ожидание завершения", "exit_timeout_desc": "Сколько секунд ожидать корректного завершения всех процессов приложения при закрытии. Если не указано, то по умолчанию, ожидание длится 5 секунд. Если указан нуль или отрицательное значение, приложение будет прекращено незамедлительно.", "find_cover": "Найти обложку", "global_prep_desc": "Включить/отключить исполнение глобальных команд подготовки для этого приложения.", "global_prep_name": "Глобальные команды", "image": "Изображение", "image_desc": "Путь к иконке/обложке/изображению, который будет отправлен клиенту. Изображение должно быть PNG файлом. Если не указано, Apollo пошлёт обложку по умолчанию.", "loading": "Загрузка...", "name": "Название", "output_desc": "Файл, в котором сохраняется вывод команды, если он не указан, вывод игнорируется", "output_name": "Вывод", "run_as_desc": "Это может потребоваться для некоторых приложений, которым требуются права администратора для правильного запуска.", "wait_all": "Продолжать вещание, пока не завершатся все процессы приложения", "wait_all_desc": "Продолжать вещание, пока все процессы, запущенные приложением не будут завершены. Если не выбрано, вещание прекратится, по завершении начального процесса приложения, даже если запущены другие подпроцессы.", "working_dir": "Рабочая папка", "working_dir_desc": "Рабочий каталог, передаваемый процессу. К примеру, некоторые приложения используют рабочий каталог для поиска конфигурационных файлов. Если не указан, Apollo по умолчанию использует вышестоящий каталог команды" }, "config": { "adapter_name": "Имя адаптера", "adapter_name_desc_linux_1": "Вручную укажите GPU для захвата.", "adapter_name_desc_linux_2": "найти все устройства, поддерживающие VAAPI", "adapter_name_desc_linux_3": "Замените ``renderD129`` устройством сверху, чтобы перечислить имя и возможности устройства. Чтобы быть поддержанным Apollo, он должен иметь как минимум свое:", "adapter_name_desc_windows": "Укажите GPU для захвата. Если флажок установлен, GPU выбирается автоматически. Мы настоятельно рекомендуем оставить это поле пустым, чтобы использовать автоматический выбор GPU! Примечание: этот GPU должен иметь дисплей подключён и включен. Соответствующие значения могут быть найдены с помощью следующей команды:", "adapter_name_placeholder_windows": "Radeon RX 580 серия", "add": "Добавить", "address_family": "Семейство адресов", "address_family_both": "IPv4 + IPv6", "address_family_desc": "Установить семейство адресов, используемое Apollo", "address_family_ipv4": "Только IPv4", "always_send_scancodes": "Всегда посылать коды клавиш", "always_send_scancodes_desc": "Передача кодов клавиш улучшает совместимость с играми и приложениями, но может привести к неправильному вводу с клавиатуры, если клиент используют раскладку, отличную от английской США. Включите, если в каких-то приложениях ввод с клавиатуры не работает вовсе. Отключите, если клавиши клиента передают на ввод не те клавиши сервер.", "amd_coder": "AMF Coder (H264)", "amd_coder_desc": "Позволяет выбрать энтропическую кодировку для приоритизации скорости или качества кодирования. H.264 только.", "amd_enforce_hrd": "AMF Hypothetical Reference Decoder (HRD) Enforcement", "amd_enforce_hrd_desc": "Увеличивает ограничения на контроль за скоростью для удовлетворения требований модели HRD. Это значительно снижает переполнение битрейта, но может вызвать кодировку артефактов или уменьшить качество на некоторых картах.", "amd_preanalysis": "Предварительный анализ AMF", "amd_preanalysis_desc": "Это позволяет проводить предварительный анализ скорости управления, который может повысить качество за счет увеличения задержки кодирования.", "amd_quality": "Качество AMF", "amd_quality_balanced": "сбалансированный -- сбалансированный (по умолчанию)", "amd_quality_desc": "Задаёт соотношение между скоростью и качеством кодирования.", "amd_quality_group": "Настройки качества AMF", "amd_quality_quality": "качество -- упор на качество", "amd_quality_speed": "скорость -- упор на скорость", "amd_rc": "Контроль скорости AMF", "amd_rc_cbr": "cbr -- постоянный битрейт", "amd_rc_cqp": "cqp -- постоянный режим qp", "amd_rc_desc": "Это контролирует способ контроля тарифов, чтобы убедиться, что мы не превысили цели битрейта клиента. 'cqp' не подходит для таргетинга битрейта, а другие параметры помимо 'vbr_latency' зависят от соблюдения HRD Enforcement для ограничения переполнения битрейта.", "amd_rc_group": "Настройки контроля скорости AMF", "amd_rc_vbr_latency": "vbr_latency -- Задержка ограничивает битрейт (по умолчанию)", "amd_rc_vbr_peak": "vbr_peak -- пиковый ограниченный битрейт", "amd_usage": "Использование AMF", "amd_usage_desc": "Определяет базовую кодировку профиля. Все параметры, представленные ниже, переопределят поднабор пользовательского профиля, но есть дополнительные скрытые настройки, которые не могут быть настроены в другом месте.", "amd_usage_lowlatency": "низкая задержка - низкая задержка (быстро)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - низкая задержка, высокое качество (быстро)", "amd_usage_transcoding": "перекодирование -- перекодирование (медленно)", "amd_usage_ultralowlatency": "ультразвуковая задержка - ультра низкая задержка (быстрый)", "amd_usage_webcam": "веб-камера -- веб-камера (медленно)", "amd_vbaq": "Адаптивное квантование на основе отклонений AMF (VBAQ)", "amd_vbaq_desc": "Как правило, визуальная система человека менее чувствительна к артефактам в особо текстурированных районах. В режиме VBAQ отклонение пикселей используется для обозначения сложности пространственной текстуры, что позволяет кодировщику выделять больше битов для более плавности зон. Включение этой функции приводит к улучшению субъективного качества зрения с некоторым содержимым.", "apply_note": "Нажмите 'Применить', чтобы перезапустить Apollo и применить изменения. Все запущенные сессии будут завершены.", "audio_sink": "Снимок звука", "audio_sink_desc_linux": "Название звукового приёмника, используемого для обратной ретрансляции. Если эта переменная не указана, pulseaudio выберет устройство по умолчанию. Определить название звукового приёмника можно либо командой:", "audio_sink_desc_macos": "Название звуковой раковины, используемой для аудиоциклов. Apollo может получить доступ только к микрофонам в macOS из-за ограничений системы. Для трансляции системного аудио с помощью Soundflower или BlackHole.", "audio_sink_desc_windows": "Укажите вручную определённое аудиоустройство для записи. Если устройство выключено, оно выбирается автоматически. Рекомендуем оставить это поле пустым, чтобы использовать автоматический выбор устройств! Если у вас несколько аудио устройств с одинаковыми именами, вы можете получить ID устройства, используя следующую команду:", "audio_sink_placeholder_macos": "BlackHole 2ch", "audio_sink_placeholder_windows": "Динамики (High Definition Audio Device)", "av1_mode": "Поддержка AV1", "av1_mode_0": "Apollo будет рекламировать поддержку AV1 на основе возможностей кодировщика (рекомендуется)", "av1_mode_1": "Apollo не будет рекламировать поддержку AV1", "av1_mode_2": "Apollo рекламирует поддержку AV1 Main 8-bit профиля", "av1_mode_3": "Apollo будет рекламировать поддержку профилей AV1 Main 8-bit и 10-bit (HDR)", "av1_mode_desc": "Позволяет клиенту запрашивать AV1 Main 8-bit или 10-битные видео потоки. AV1 является более интенсивным процессором для кодирования, поэтому включение этой опции может снизить производительность при использовании программного обеспечения.", "back_button_timeout": "Таймаут эмуляции кнопки Домой", "back_button_timeout_desc": "Если кнопка Назад/Выбор удерживается вниз для заданного количества миллисекунд, то кнопка Home/Guide будет эмулирована. Если установлено значение < 0 (по умолчанию), удерживая кнопку Назад/Выделение не будет эмулировать кнопку Домашний/Гид.", "capture": "Принудительный метод захвата", "capture_desc": "В автоматическом режиме Apollo будет использовать первый работающий драйвер NvFBC.", "cert": "Сертификат", "cert_desc": "Сертификат, используемый для веб-интерфейса и привязки клиентов Moonlight. Для совместимости должен иметь открытый ключ RSA-2048.", "channels": "Максимальное число подключенных клиентов", "channels_desc_1": "Apollo позволяет одновременное совместное использование одного сеанса потокового вещания.", "channels_desc_2": "Некоторые аппаратные кодировщики могут иметь ограничения, уменьшающие производительность с несколькими потоками.", "coder_cabac": "cabac -- контекстная адаптивная арифметическая кодировка - более высокое качество", "coder_cavlc": "cavlc -- контекстное адаптивное кодирование переменной длины - ускорение декодирования", "configuration": "Конфигурация", "controller": "Enable Gamepad Input", "controller_desc": "Позволяет гостям контролировать хост-систему с помощью геймпада / контроллера", "credentials_file": "Файл учётных данных", "credentials_file_desc": "Храните имя пользователя/пароль отдельно от файла состояния Apollo.", "dd_config_ensure_active": "Активировать экран автоматически", "dd_config_ensure_only_display": "Отключить другие дисплеи и активировать только указанный дисплей", "dd_config_ensure_primary": "Активировать экран автоматически и сделать его основным дисплеем", "dd_config_label": "Конфигурация устройства", "dd_config_revert_delay": "Задержка отката конфигурации", "dd_config_revert_delay_desc": "Дополнительная задержка в миллисекундах перед откатом конфигурации приложения или последней сессии прервана. Главная цель - обеспечить более плавный переход при быстром переключении между приложениями.", "dd_config_verify_only": "Проверьте, включен ли дисплей", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "Включение/выключение режима HDR по требованию клиента (по умолчанию)", "dd_hdr_option_disabled": "Не изменять настройки HDR", "dd_mode_remapping": "Режим отображения", "dd_mode_remapping_add": "Добавить пересопоставление записи", "dd_mode_remapping_desc_1": "Укажите переотображение записей для изменения требуемого разрешения и/или частоты обновления на другие значения.", "dd_mode_remapping_desc_2": "Список итераций сверху вниз, и используется первое совпадение.", "dd_mode_remapping_desc_3": "Поля \"Запрошенные\" могут быть пустыми для соответствия любому запрошенному значению.", "dd_mode_remapping_desc_4_final_values_mixed": "По крайней мере одно поле \"Final\" должно быть указано. Неуказанное разрешение или частота обновления не будет изменена.", "dd_mode_remapping_desc_4_final_values_non_mixed": "Поле \"Final\" должно быть заполнено и не может быть пустым.", "dd_mode_remapping_desc_5_sops_mixed_only": "Опция \"Оптимизировать настройки игры\" должна быть включена в клиенте Moonlight, в противном случае записи с полями разрешения пропущены.", "dd_mode_remapping_desc_5_sops_resolution_only": "Опция \"Оптимизировать настройки игры\" должна быть включена в клиенте Moonlight, иначе сопоставление пропущено.", "dd_mode_remapping_final_refresh_rate": "Окончательная частота обновления", "dd_mode_remapping_final_resolution": "Окончательное решение", "dd_mode_remapping_requested_fps": "Запрошенные FPS", "dd_mode_remapping_requested_resolution": "Запрошенное разрешение", "dd_options_header": "Расширенные настройки устройства", "dd_refresh_rate_option": "Частота обновления", "dd_refresh_rate_option_auto": "Использовать значение FPS (по умолчанию)", "dd_refresh_rate_option_disabled": "Не изменять частоту обновления", "dd_refresh_rate_option_manual": "Использовать вручную введенную частоту обновления", "dd_refresh_rate_option_manual_desc": "Введите частоту обновления для использования", "dd_resolution_option": "Разрешение", "dd_resolution_option_auto": "Использовать разрешение, предоставляемое клиентом (по умолчанию)", "dd_resolution_option_disabled": "Не изменять разрешение", "dd_resolution_option_manual": "Использовать вручную введенное разрешение", "dd_resolution_option_manual_desc": "Введите разрешение, которое будет использовано", "dd_resolution_option_ogs_desc": "Для этого необходимо включить опцию \"Оптимизация настроек игры\" на клиенте Moonlight.", "dd_wa_hdr_toggle_desc": "При использовании виртуального устройства отображения для потокового воспроизведения может отображаться неправильный цвет HDR. При включенной опции Apollo попытается уменьшить эту проблему.", "dd_wa_hdr_toggle": "Включить высококонтрастный обход для HDR", "ds4_back_as_touchpad_click": "Назад/Выберете для нажатия сенсорной панели", "ds4_back_as_touchpad_click_desc": "При принудительной эмуляции DS4, нажмите на карточку Назад/Выделение для сенсорной панели", "encoder": "Принудительный кодировщик", "encoder_desc": "Принудительно использовать конкретный кодировщик, иначе Apollo выберет наилучший доступный вариант. Примечание: Если указать аппаратный кодировщик в Windows, он должен соответствовать GPU, к которому подключён экран.", "encoder_software": "Программный", "external_ip": "Внешний IP", "external_ip_desc": "Если внешний IP адрес не указан, Apollo будет автоматически определять внешний IP", "fec_percentage": "Процент FEC", "fec_percentage_desc": "Процент погрешности исправления пакетов по каждому пакету данных в каждом видеокадре. Более высокие значения могут корректно повлиять на потерю сетевых пакетов, но за счет увеличения пропускной способности.", "ffmpeg_auto": "auto -- пусть ffmpeg решить (по умолчанию)", "file_apps": "Файл приложений", "file_apps_desc": "Файл, в котором хранятся текущие приложения Apollo.", "file_state": "Файл состояния", "file_state_desc": "Файл, в котором хранится текущее состояние Apollo", "gamepad": "Тип эмулируемого геймпада", "gamepad_auto": "Настройка автоматического выбора", "gamepad_desc": "Выберите тип геймпада для эмулирования на хосте", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "Параметры выбора DS4", "gamepad_ds5": "DS5 (PS5)", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Ручные настройки DS4", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Команды подготовки", "global_prep_cmd_desc": "Настроить список команд, которые будут выполнены до или после запуска любого приложения. Если какая-либо из указанных команд подготовки не удается, процесс запуска приложения будет прерван.", "hevc_mode": "Поддержка HEVC", "hevc_mode_0": "Apollo будет рекламировать поддержку HEVC на основе возможностей кодировщика (рекомендуется)", "hevc_mode_1": "Apollo не будет рекламировать поддержку HEVC", "hevc_mode_2": "Apollo будет рекламировать поддержку HEVC Main", "hevc_mode_3": "Apollo будет рекламировать поддержку профилей HEVC Main и Main10 (HDR)", "hevc_mode_desc": "Позволяет клиенту запрашивать HEVC Main или HEVC Main10 видео потоков. HEVC является более процессорным в кодировке, поэтому включение этой опции может снизить производительность при использовании программного обеспечения.", "high_resolution_scrolling": "Поддержка прокрутки высокого разрешения", "high_resolution_scrolling_desc": "Когда включено, Apollo будет прокручивать события с высоким разрешением от клиентов лунного света. Это может быть полезно для отключения для старых приложений, которые слишком быстро прокручивать при прокрутке событий высокого разрешения.", "install_steam_audio_drivers": "Установить Steam Audio Drivers", "install_steam_audio_drivers_desc": "Если Steam установлен, он автоматически установит драйвер Steam Streaming Speakers для поддержки объёмного звука 5.1/7.1 и заглушения звука на сервере.", "key_repeat_delay": "Задержка повтора нажатий", "key_repeat_delay_desc": "Контролируйте как быстрые клавиши будут повторяться. Начальная задержка в миллисекундах до повторных нажатий.", "key_repeat_frequency": "Частота повторения нажатий", "key_repeat_frequency_desc": "Как часто нажатия повторяются за секунду. Эта настройка поддерживает десятичные дроби.", "key_rightalt_to_key_win": "Карта клавиши Alt справа для клавиши Windows", "key_rightalt_to_key_win_desc": "Возможно, вы не можете послать нажатие кнопки Windows непосредственно из Moonlight. В таком случае, полезно чтобы Apollo думал, что клавиша правый Alt является клавишей Windows", "keyboard": "Включить ввод с клавиатуры", "keyboard_desc": "Позволяет гостям управлять системой хоста с помощью клавиатуры", "lan_encryption_mode": "Режим шифрования LAN", "lan_encryption_mode_1": "Включено для поддерживаемых клиентов", "lan_encryption_mode_2": "Требуется для всех клиентов", "lan_encryption_mode_desc": "Определяет, когда шифрование будет использоваться при вещании в локальной сети. Шифрование может снизить качество вещания, особенно на более слабых серверах и клиентах.", "locale": "Язык", "locale_desc": "Локализация, используемая для пользовательского интерфейса Apollo.", "log_level": "Уровень журналирования", "log_level_0": "Подробные", "log_level_1": "Отладочные", "log_level_2": "Информация", "log_level_3": "Предупреждения", "log_level_4": "Ошибки", "log_level_5": "Критические", "log_level_6": "Нет", "log_level_desc": "Минимальный log level выведен в stdout", "log_path": "Путь к файлу журнала", "log_path_desc": "Файл, в котором хранятся текущие журналы Apollo.", "min_fps_factor": "Минимальный коэффициент FPS", "min_fps_factor_desc": "Солнечный свет будет использовать этот фактор для расчета минимального времени между рамками. Увеличение этого значения может помочь при потоке в основном статического контента. Более высокие значения будут потреблять больше пропускной способности.", "min_threads": "Минимальное количество потоков ЦП", "min_threads_desc": "Увеличение значения немного снижает эффективность кодирования, но tradeoff обычно стоит увеличить использование большего количества ядер процессора для кодирования. Идеальное значение это наименьшее значение, которое может надежно закодировать при желаемых потоковых настройках на вашем оборудовании.", "misc": "Прочие параметры", "motion_as_ds4": "Эмулируйте геймпад DS4, если клиент сообщает датчики движения", "motion_as_ds4_desc": "Если отключено, датчики движения не будут учитываться при выборе типа геймпада.", "mouse": "Включить ввод мыши", "mouse_desc": "Позволяет гостям контролировать хост-систему мышкой", "native_pen_touch": "Родная Pencil/Touch поддержка", "native_pen_touch_desc": "Если включено, Apollo будет проходить через родные события pen/touch от клиентов Moonlight. Это может быть полезно для более старых приложений без поддержки pen/touch .", "notify_pre_releases": "Уведомления о предварительном выпуске", "notify_pre_releases_desc": "Уведомлять ли о новых предварительных версиях Apollo", "nvenc_h264_cavlc": "Предпочитайте CAVLC поверх CABAC в H.264", "nvenc_h264_cavlc_desc": "Форма кодирования энтропии. CAVLC требует на 10% больше битрейта для того же качества. Только для очень старых декодирующих устройств.", "nvenc_latency_over_power": "Предпочитайте более низкую задержку кодирования по сравнению с экономией энергии", "nvenc_latency_over_power_desc": "Apollo requests maximum GPU clock speed while streaming to reduce encoding latency. Отключение этого значения не рекомендуется, так как это может привести к значительному увеличению задержки кодирования.", "nvenc_opengl_vulkan_on_dxgi": "Настоящий OpenGL/Vulkan на вершине DXGI", "nvenc_opengl_vulkan_on_dxgi_desc": "Apollo не может захватывать полноэкранные программы OpenGL и Vulkan с полной скоростью кадра, если они не присутствуют поверх DXGI. Это общесистемная настройка, которая возвращается при выходе из программы.", "nvenc_preset": "Преднастройка производительности", "nvenc_preset_1": "(быстрый, по умолчанию)", "nvenc_preset_7": "(медленно)", "nvenc_preset_desc": "Более высокие значения улучшают сжатие (качество на заданном битрейте) за счет увеличения задержки кодирования. Рекомендуется изменять только когда ограничено сетью или декодером, в противном случае подобный эффект может быть достигнут путем увеличения битрейта.", "nvenc_realtime_hags": "Использовать приоритет реального времени в аппаратном ускоренном планировании gpu", "nvenc_realtime_hags_desc": "В настоящее время драйвера NVIDIA могут заморозиться в кодировщике при включенном HAGS, используется приоритет реального времени и использование VRAM близко к максимуму. Отключение этой опции снижает приоритет на высокий, блокирование по цене пониженной производительности захвата при высокой нагрузке GPU.", "nvenc_spatial_aq": "Spatial AQ", "nvenc_spatial_aq_desc": "Назначить более высокие значения QP для плоских регионов видео. Рекомендуется включить при потоке на более низких битрейтах.", "nvenc_spatial_aq_disabled": "Отключено (быстрее, по умолчанию)", "nvenc_spatial_aq_enabled": "Включено (медленнее)", "nvenc_twopass": "Режим двухпрохождения", "nvenc_twopass_desc": "Добавляет предварительную кодировку. Это позволяет обнаружить больше векторов движений, лучше распределять битрейт по фрейму и строго придерживаться лимитов битрейта. Отключение не рекомендуется, так как это может привести к перегрузке битрейта и последующей потере пакетов.", "nvenc_twopass_disabled": "Отключено (быстрый, не рекомендуется)", "nvenc_twopass_full_res": "Полное разрешение (медленнее)", "nvenc_twopass_quarter_res": "Квартальное разрешение (по умолчанию)", "nvenc_vbv_increase": "Однокадровое увеличение VBV/HRD", "nvenc_vbv_increase_desc": "По умолчанию солнечный свет использует однокадровый VBV/HRD, что означает, что любой кодируемый размер видеокадра не должен превышать запрашиваемый битрейт, поделенный на заданную скорость кадра. Расслабляя это ограничение может быть полезным и выступать в качестве битрейта с низкой задержкой, но может также привести к потере пакетов, если в сети нет заголовка буфера для обработки битрейтов. Максимально допустимое значение - 400, что соответствует 5-кратному увеличенному пределу видеокадра в кодировке.", "origin_web_ui_allowed": "Веб-интерфейс Origin разрешён", "origin_web_ui_allowed_desc": "Источник адреса удаленной конечной точки, которой не запрещён доступ к веб-интерфейсу", "origin_web_ui_allowed_lan": "Только из LAN могут получить доступ к веб-интерфейсу", "origin_web_ui_allowed_pc": "Только локальный ПК имеет доступ к веб-интерфейсу", "origin_web_ui_allowed_wan": "Любой имеет доступ к веб-интерфейсу", "output_name_desc_unix": "Во время запуска Apollo вы увидите список обнаруженных экранов. Примечание: используйте значение id в скобках. Пример ниже; нужный экран можно обнаружить на вкладке Устранение проблем.", "output_name_desc_windows": "Вручную укажите экран для захвата. Если не указано, то будет произведён захват основного экрана. Примечание: Если вы ранее указали GPU, этот экран должен быть подключен к тому GPU. Подходящие значения определяются с помощью следующей команды:", "output_name_unix": "Номер экрана", "output_name_windows": "Имя вывода", "ping_timeout": "Таймаут пинга", "ping_timeout_desc": "Время ожидания данных от Moonlight до завершения вещания, в миллисекундах", "pkey": "Закрытый ключ", "pkey_desc": "Закрытый ключ, используемый для веб-интерфейса и привязки клиентов Moonlight. Для совместимости должен быть закрытым ключом RSA-2048.", "port": "Порт", "port_alert_1": "Apollo не может использовать порты ниже 1024!", "port_alert_2": "Порты выше 65535 недоступны!", "port_desc": "Установить семейство портов, используемых Apollo", "port_http_port_note": "Используйте этот порт для соединения с Moonlight.", "port_note": "Примечание", "port_port": "Порт", "port_protocol": "Протокол", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "Доступность веб-интерфейса из Интернета не безопасна! Продолжайте на свой страх и риск!", "port_web_ui": "Веб-интерфейс", "qp": "Параметр квантования", "qp_desc": "Некоторые устройства могут не поддерживать постоянную ширину поток. Для таких устройств используется параметр квантования. Более высокое значение означает большее сжатие, но меньшее качество.", "qsv_coder": "Кодировщик QuickSync (H264)", "qsv_preset": "Предустановка QuickSync", "qsv_preset_fast": "fast (низкое качество)", "qsv_preset_faster": "faster (худшее качество)", "qsv_preset_medium": "medium (по умолчанию)", "qsv_preset_slow": "slow (хорошее качество)", "qsv_preset_slower": "slower (отличное качество)", "qsv_preset_slowest": "slowest (лучшее качество)", "qsv_preset_veryfast": "самый быстрый (низкое качество)", "qsv_slow_hevc": "Разрешить Slow HEVC кодирование", "qsv_slow_hevc_desc": "Это позволяет включить HEVC кодирование на старых процессорах Intel за счет более высокого использования GPU и более низкой производительности.", "restart_note": "Apollo перезапускается, чтобы применить изменения.", "sunshine_name": "Название сервера Apollo", "sunshine_name_desc": "Имя сервера, отображаемое в Moonlight. Если не указано, используется имя ПК", "sw_preset": "Предустановки программного кодирования", "sw_preset_desc": "Оптимизировать компромисс между скоростью кодирования (кодированные кадры в секунду) и эффективностью сжатия (качество за бит в bitstream). По умолчанию супербыстро.", "sw_preset_fast": "быстро", "sw_preset_faster": "быстрее", "sw_preset_medium": "средняя", "sw_preset_slow": "медленно", "sw_preset_slower": "медленнее", "sw_preset_superfast": "сверхбыстро (по умолчанию)", "sw_preset_ultrafast": "ультрабыстро", "sw_preset_veryfast": "veryfast", "sw_preset_veryslow": "veryslow", "sw_tune": "Подстройки Программного кодирования", "sw_tune_animation": "animation -- подходит для мультфильмов, использует агрессивное подавление блочности и больше опорных кадров", "sw_tune_desc": "Подстроечные параметры, применяемые после предустановки. По умолчанию zerolatency.", "sw_tune_fastdecode": "fastdecode -- позволяет ускорить декодирование отключением некоторых фильтров", "sw_tune_film": "фильм -- используется для высококачественного кино; меньше подавляет блочность", "sw_tune_grain": "grain -- предаёт зернистость старой фотоплёнки", "sw_tune_stillimage": "stillimage -- хорош для малоподвижных изображений", "sw_tune_zerolatency": "zerolatency -- хорош для быстрого кодирования и вещания с низкой задержкой (по умолчанию)", "touchpad_as_ds4": "Эмулировать геймпад DS4, если клиент сообщает, о наличии сенсорной панели", "touchpad_as_ds4_desc": "Если отключено, присутствие сенсорной панели не будет учитываться при выборе типа геймпада.", "upnp": "UPnP", "upnp_desc": "Автоматически настраивать переадресацию портов для вещания через Интернет", "vaapi_strict_rc_buffer": "Строго применять ограничения скорости битрейта для H.264/HEVC на AMD GPU", "vaapi_strict_rc_buffer_desc": "Включение этой опции позволит избежать сброса кадров по сети во время изменения сцен, но во время движения качество видео может быть снижено.", "virtual_sink": "Виртуальный приёмник", "virtual_sink_desc": "Вручную укажите виртуальное аудио устройство. Если не указано, устройство будет выбрано автоматически. Рекомендуем оставить это поле пустым, для автоматического выбора устройств!", "virtual_sink_placeholder": "Динамики Steam", "vt_coder": "Кодировщик VideoToolbox", "vt_realtime": "Кодирование в реальном времени через VideoToolbox", "vt_software": "Программное кодирование через VideoToolbox", "vt_software_allowed": "Разрешено", "vt_software_forced": "Принудительно", "wan_encryption_mode": "Режим шифрования WAN", "wan_encryption_mode_1": "Включено для поддерживаемых клиентов (по умолчанию)", "wan_encryption_mode_2": "Требуется для всех клиентов", "wan_encryption_mode_desc": "Определяет, когда будет использоваться шифрование при вещании через Интернет. Шифрование может снизить качество вещания, особенно на более слабых серверах и клиентах." }, "index": { "description": "Apollo - это ваш собственный сервер вещания игр для Moonlight.", "download": "Скачать", "installed_version_not_stable": "Вы используете предварительную версию Apollo. Вы можете столкнуться с ошибками или другими проблемами. Пожалуйста, сообщайте о проблемах, с которыми вы столкнётесь. Благодарим за помощь по улучшению Apollo!", "loading_latest": "Загрузка свежей версии...", "new_pre_release": "Доступна новая предварительная версия!", "new_stable": "Доступна новая стабильная версия!", "startup_errors": "Внимание! Apollo обнаружил эти ошибки во время запуска. Мы НАСТОЯТЕЛЬНО РЕКОМЕНДУЕМ исправить их перед запуском вещания.", "version_dirty": "Спасибо за помощь в создании программного обеспечения Apollo!", "version_latest": "Вы используете свежую версию Apollo", "welcome": "Привет, Apollo!" }, "navbar": { "applications": "Приложения", "configuration": "Настройки", "home": "Главная", "password": "Изменить пароль", "pin": "Pin", "theme_auto": "Автоматически", "theme_dark": "Тёмное", "theme_light": "Светлое", "toggle_theme": "Оформление", "troubleshoot": "Устранение проблем" }, "password": { "confirm_password": "Подтвердите пароль", "current_creds": "Текущие учетные данные", "new_creds": "Новые учетные данные", "new_username_desc": "Если не указано, имя пользователя не изменится", "password_change": "Смена пароля", "success_msg": "Пароль успешно изменен! Эта страница скоро перезагрузится, ваш браузер запросит новые учетные данные." }, "pin": { "device_name": "Имя устройства", "pair_failure": "Не удалось привязать: проверьте, правильность PIN-кода", "pair_success": "Успешно! Перейдите в Moonlight для продолжения", "pin_pairing": "PIN привязки", "send": "Отправить", "warning_msg": "Убедитесь, что клиент, который вы привязываете доступен. Данное ПО может передать полный контроль над вашим компьютером, так что будьте осторожны!" }, "resource_card": { "github_discussions": "GitHub Discussions", "legal": "Юридическая информация", "legal_desc": "Используя данное ПО, вы соглашаетесь с условиями, изложенными в следующих документах.", "license": "Лицензия", "lizardbyte_website": "Сайт LizardByte", "resources": "Полезные источники", "resources_desc": "Полезные ресурсы, посвящённые Apollo!", "third_party_notice": "Уведомление о третьих сторонах" }, "troubleshooting": { "dd_reset": "Сбросить настройки постоянного дисплея устройства", "dd_reset_desc": "Если Apollo пытается восстановить измененные настройки дисплея, вы можете сбросить настройки и продолжить восстановление состояния дисплея вручную.", "dd_reset_error": "Ошибка при сбросе настойчивости!", "dd_reset_success": "Успех восстановления настойчивости!", "force_close": "Принудительное закрытие", "force_close_desc": "Если Moonlight жалуется на запущенное приложение, принудительное закрытие приложения должно помочь.", "force_close_error": "Ошибка при закрытии приложения", "force_close_success": "Приложение закрыто успешно!", "logs": "Журналы", "logs_desc": "Смотреть журналы, выгруженные Apollo", "logs_find": "Найти...", "restart_Apollo": "Перезапустить Apollo", "restart_Apollo_desc": "Если Apollo работает некорректно, вы можете попробовать перезапустить его. Это прекратит работу всех запущенных сеансов.", "restart_Apollo_success": "Apollo перезапускается", "troubleshooting": "Устранение проблем", "unpair_all": "Отвязать все", "unpair_all_error": "Ошибка при отвязывании", "unpair_all_success": "Все устройства отвязаны.", "unpair_desc": "Удалите свои привязанные устройства. Устройства с активным сеансом, отвязанные по одному, останутся подключенными, но не смогут начать или возобновить сеанс.", "unpair_single_no_devices": "Нет привязанных устройств.", "unpair_single_success": "Однако, устройство (устройства) может находиться в имеющемся сеансе. Воспользуйтесь кнопкой «Принудительное закрытие» выше для завершения всех сеансов.", "unpair_single_unknown": "Неизвестный клиент", "unpair_title": "Отвязать устройства" }, "welcome": { "confirm_password": "Подтвердите пароль", "create_creds": "Перед началом работы нам нужно создать новые логин и пароль для доступа к веб-интерфейсу.", "create_creds_alert": "Учетные данные, указанные ниже, необходимы для доступа к веб-интерфейсу Apollo. Сохраните их в надёжном месте, так как больше вы их не увидите!", "greeting": "Добро пожаловать в Apollo!", "login": "Вход", "welcome_success": "Эта страница скоро перезагрузится, ваш браузер запросит новые учетные данные" } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/sv.json ================================================ { "_common": { "apply": "Tillämpa", "auto": "Automatisk", "autodetect": "Automatisk detektion (rekommenderas)", "beta": "(beta)", "cancel": "Avbryt", "cmd_name": "Kommandonamn", "cmd_val": "Kommandovärde", "disabled": "Inaktiverad", "disabled_def": "Inaktiverad (standard)", "disabled_def_cbox": "Standard: avmarkerad", "dismiss": "Avfärda", "do_cmd": "Kör kommando", "elevated": "Förhöjd", "enabled": "Aktiverad", "enabled_def": "Aktiverad (standard)", "enabled_def_cbox": "Standard: markerad", "error": "Fel!", "learn_more": "Läs mer", "note": "Notera:", "password": "Lösenord", "run_as": "Kör som administratör", "save": "Spara", "see_more": "Se mer", "success": "Klart!", "undo_cmd": "Ångra kommando", "username": "Användarnamn", "warning": "Varning!" }, "apps": { "actions": "Händelser", "add_cmds": "Lägg till kommandon", "add_new": "Lägg till ny", "allow_client_commands": "Tillåt klientkommandon", "allow_client_commands_desc": "Tillåt klienten att skicka kommandon till Apollo för att köra program eller utföra andra åtgärder.", "app_name": "Applikationsnamn", "app_name_desc": "Applikationsnamn, som visas i Moonlight", "applications_desc": "Applikationer uppdateras endast när klienten startas om", "applications_title": "Applikationer", "auto_detach": "Fortsätt strömma om programmet avslutas snabbt", "auto_detach_desc": "Detta kommer att försöka att automatiskt upptäcka \"launcher\"-appar som stänger snabbt efter att ha startat ett annat program eller instans av sig själva. När en app med \"launcher\"-app upptäcks behandlas den som en fristående app.", "close": "Stäng", "close_warning": "Är du säker på att du vill stänga appen som körs?", "close_failed": "Kunde inte stänga appen", "cmd": "Kommando", "cmd_desc": "Huvudprogrammet som startas. Om den är tom, kommer inget program att startas.", "cmd_note": "Om sökvägen till körbara kommandot innehåller mellanslag, måste du bifoga det i citattecken.", "cmd_prep_desc": "En lista över kommandon som ska köras före/efter detta program. Om något av för-kommandona misslyckas, avbryts program starten.", "cmd_prep_name": "Kommando förberedelser", "covers_found": "Hittade omslag", "delete": "Radera", "delete_failed": "Kunde inte radera appen: ", "detached_cmds": "Fristående kommandon", "detached_cmds_add": "Lägg till fristående kommando", "detached_cmds_desc": "En lista över kommandon som ska köras i bakgrunden.", "detached_cmds_note": "Om sökvägen till körbara kommandot innehåller mellanslag, måste du inkludera det i citattecken.", "edit": "Redigera", "env_app_id": "App ID", "env_app_name": "Appens namn", "env_client_audio_config": "Ljudkonfigurationen begärd av klienten (2.0/5.1/7.1)", "env_client_enable_sops": "Klienten har begärt möjligheten att optimera spelet för optimal steaming\n(sant/falskt)", "env_client_fps": "FPS begärd av klienten (float)", "env_client_gcmap": "Den begärda gamepad masken, i bitset/bitfield format (int)", "env_client_hdr": "HDR är aktiverat av klienten (true/false)", "env_client_height": "Höjd som begärts av klienten (int)", "env_client_host_audio": "Klienten har begärt värdljud (true/false)", "env_client_width": "Bredden begärd av klienten (int)", "env_client_uuid": "UID för klienten som startar strömmen (int)", "env_client_name": "Namn på klienten som startar strömmen (string)", "env_displayplacer_example": "Exempel - en displayplacer för automatiserad upplösning:", "env_qres_example": "Exempel - QRes för upplösningsautomatisering:", "env_qres_path": "qres sökväg", "env_var_name": "Variabel namn", "env_vars_about": "Om miljövariabler", "env_vars_desc": "Alla kommandon får dessa miljövariabler som standard:", "env_xrandr_example": "Exempel - Xrandr för upplösningsautomatisering:", "exit_timeout": "Avbryt Timeout", "exit_timeout_desc": "Antalet sekunder att vänta på att alla app-processer ska avslutas graciöst/korrekt 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.", "find_cover": "Hitta omslag", "global_prep_desc": "Aktivera/Inaktivera exekvering av globala prep kommandon för denna applikation.", "global_prep_name": "Globala prep kommandon", "image": "Bild", "image_desc": "Applikationens 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 Apollo att skicka en standardbild.", "launch": "Launch", "launch_warning": "Är du säker på att du vill starta appen? Detta kommer att stänga den aktuella appen.", "launch_failed": "Kunde inte starta appen: ", "loading": "Laddar...", "name": "Namn", "output_desc": "Filen där kommandots utdata lagras, om den inte är angiven, så ignoreras utdata", "output_name": "Utdata", "per_client_app_identity": "Per klient app identitet", "per_client_app_identity_desc": "Separera appens identitet per klient. Användbart när du vill ha olika inställningar för virtuella skärmar för denna specifika app för olika klienter.", "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.", "save_failed": "Kunde inte spara appen: ", "wait_all": "Fortsätt strömma tills alla app-processer avslutas", "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 app-processen avslutas, även om andra app-processer fortfarande är igång.", "working_dir": "Arbetskatalog", "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 inget anges, kommer Apollo som standard använda den överordnade katalogen från vart kommandot körs.", "virtual_display": "Använd alltid virtuell skärm", "virtual_display_desc": "Använd alltid en virtuell skärm för denna app, även om klienten inte begär det. Säkerställ att SudoVDA drivrutinen är installerad och aktiverad.", "virtual_display_primary": "Tvinga Virtuell Skärm som Primär Skärm", "virtual_display_primary_desc": "Tvinga den virtuella skärmen som den primära skärmen för denna app. Virtiell skärm sätts alltid som primär skärm när klienten begär att använda en virtuell skärm. (Rekommenderas att ha aktiverad) [Trasig på Windows 11 24H2]", "resolution_scale_factor": "Skalningsfaktor för upplösning", "resolution_scale_factor_desc": "Skala klientens begärda upplösning baserat på denna faktor. T.ex. 2000x1000 med en faktor på 120% blir 2400x1200. Åsidosätter klientens begärda faktor när siffran inte är 100%. Detta alternativ påverkar inte klientens begärda streamingupplösning.", "use_app_identity": "Använd Appens Identitet", "use_app_identity_desc": "Använd appens egen identitet istället för klientens när du skapar virtuella skärmar. Detta är användbart när du vill ha separat skärmkonfiguration för varje APP." }, "client_card": { "clients": "Klienter", "clients_desc": "Klienter specifikt optimerade för att fungera bäst med Apollo", "generic_moonlight_clients_desc": "Generiska Moonlight-klienter som kan användas med Apollo" }, "config": { "adapter_name": "Adapternamn", "adapter_name_desc_linux_1": "Ange manuellt vilket grafikkort (GPU) som ska användas för skärminspelning.", "adapter_name_desc_linux_2": "att hitta alla enheter kapabla till VAAPI", "adapter_name_desc_linux_3": "Ersätt ``renderD129`` med enheten ovanifrån för att lista enhetens namn och egenskaper. För att stödjas av Apollo, måste det som minst ha:", "adapter_name_desc_windows": "Ange manuellt en GPU som ska användas för skärminspelning. Specificeras ingen, 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 skärm ansluten och påslagen. Du hittar lämpliga värden med hjälp av följande kommando:", "adapter_name_placeholder_windows": "Radeon RX 580-serien", "add": "Lägg till", "address_family": "Adressfamilj", "address_family_both": "IPv4+IPv6", "address_family_desc": "Ställ in adressfamiljen som används av Apollo", "address_family_ipv4": "Endast IPv4", "always_send_scancodes": "Skicka alltid scancodes", "always_send_scancodes_desc": "Att skicka scancodes 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 tangenter på klienten genererar fel indata på värden.", "amd_coder": "AMF-omkodare (H264)", "amd_coder_desc": "Låter dig välja entropikodning för att prioritera kvalitet eller kodningshastighet. H.264 endast.", "amd_enforce_hrd": "AMF Hypotetisk referensavomkodare (HRD) tvingas", "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 kodnings-artefakter eller minskad kvalitet på vissa kort.", "amd_preanalysis": "AMF Föranalys", "amd_preanalysis_desc": "Detta möjliggör föranalys av hastighetskontroll, vilket kan öka kvaliteten på bekostnad av ökad kodningstid.", "amd_quality": "AMF Kvalitet", "amd_quality_balanced": "balanced -- balanserad (standard)", "amd_quality_desc": "Detta styr balansen mellan kodningshastighet och kvalitet.", "amd_quality_group": "AMF Kvalitetsinställningar", "amd_quality_quality": "kvalitet – föredra kvalitet", "amd_quality_speed": "speed -- föredra hastighet", "amd_rc": "AMF Rate kontroll", "amd_rc_cbr": "cbr – konstant bithastighet", "amd_rc_cqp": "cqp – konstant qp-läge", "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.", "amd_rc_group": "Inställningar för AMF Rate", "amd_rc_vbr_latency": "vbr_latency – fördröjningsbegränsad variabelbithastighet (standard)", "amd_rc_vbr_peak": "vbr_peak – peak constrained variabelbithastighet", "amd_usage": "AMF användning", "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.", "amd_usage_lowlatency": "låg latens - låg latens (snabb)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - låg latens, hög kvalitet (snabb)", "amd_usage_transcoding": "transcoding – Omkodning (långsammare)", "amd_usage_ultralowlatency": "ultralowlatency - extremt låg latens (snabbast)", "amd_usage_webcam": "webcam – webbkamera (långsam)", "amd_vbaq": "AMF Variansbaserad adaptiv kvantisering (VBAQ)", "amd_vbaq_desc": "Det mänskliga visuella systemet är vanligtvis mindre känsligt för artefakter i områden med mycket textur. I VBAQ-läge används pixelvarians för att indikera komplexiteten i rumsliga texturer, vilket gör att omkodaren kan tilldela fler bitar till jämnare områden. Aktivering av denna funktion leder till förbättringar i subjektiv visuell kvalitet med visst innehåll.", "apply_note": "Klicka på \"Tillämpa\" för att starta om Apollo och tillämpa ändringar. Detta kommer att avsluta alla pågående sessioner.", "audio_sink": "Ljudmottagare", "audio_sink_desc_linux": "Namnet på ljudmottagaren som används för Audio Loopback. Om du inte anger denna variabel, kommer pulseaudio att välja standardenheten. Du kan hitta namnet på ljudmottagaren med hjälp av antingen kommandot:", "audio_sink_desc_macos": "Namnet på ljudmottagaren som används för Audio Loopback. Apollo kan bara komma åt mikrofoner på macOS på grund av systembegränsningar. För att strömma systemljud med Soundflower eller BlackHole.", "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:", "audio_sink_placeholder_macos": "BlackHole 2ch", "audio_sink_placeholder_windows": "Högtalare (High Definition Audio Device)", "auto_capture_sink": "Automatisk inspelning av aktuell ljudmottagare", "auto_capture_sink_desc": "Automatisk inspelning av aktuell ljudmottagare efter att standardljudmottagaren har ändrats.", "av1_mode": "AV1 Support", "av1_mode_0": "Apollo kommer att annonsera stöd för AV1 baserat på kodarfunktioner (rekommenderas)", "av1_mode_1": "Apollo kommer inte att annonsera stöd för AV1", "av1_mode_2": "Apollo kommer att annonsera stöd för AV1 Main 8-bitars profil", "av1_mode_3": "Apollo kommer att annonsera stöd för AV1 Main 8-bitars och 10-bitars (HDR) profiler", "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.", "back_button_timeout": "Hem/Guide Knapp Emulations Timeout", "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.", "capture": "Tvinga en specifik fångstmetod", "capture_desc": "På automatiskt läge Apollo kommer att använda den första som fungerar. NvFBC kräver patchade nvidia-drivrutiner.", "cert": "Certifikat", "cert_desc": "Certifikatet används för webb UI och Moonlight-klient parkoppling. För bästa kompatibilitet bör detta ha en RSA-2048 publik nyckel.", "channels": "Maximalt antal anslutna klienter", "channels_desc_1": "Apollo kan tillåta en enda streaming-session att delas med flera klienter samtidigt.", "channels_desc_2": "Vissa hårdvaruomkodare kan ha begränsningar som minskar prestandan med flera strömmar.", "coder_cabac": "cabac – kontext adaptiv binär aritmetisk kodning - högre kvalitet", "coder_cavlc": "cavlc – sammanhangsberoende kodning med variabellängd - snabbare avkodning", "configuration": "Konfiguration", "controller": "Aktivera Spelkontrollinmatning", "controller_desc": "Tillåter gäster att styra värdsystemet med en spelkontroll / kontroller", "credentials_file": "Filen för inloggningsuppgifter", "credentials_file_desc": "Lagra användarnamn/lösenord separat från Apollos statusfil.", "dd_config_ensure_active": "Aktivera skärmen automatiskt", "dd_config_ensure_only_display": "Inaktivera andra skärmar och aktivera endast den angivna skärmen", "dd_config_ensure_primary": "Aktivera skärmen automatiskt och gör den till en primär skärm", "dd_config_label": "Enhetskonfiguration", "dd_config_revert_delay": "Konfigurera återställ fördröjning", "dd_config_revert_delay_desc": "Ytterligare fördröjning i millisekunder för att vänta innan konfiguration återställs när appen har stängts eller den senaste sessionen avslutats. Huvudsyftet är att ge en smidigare övergång när du snabbt växlar mellan appar.", "dd_config_revert_on_disconnect": "Återställ konfiguration vid frånkoppling", "dd_config_revert_on_disconnect_desc": "Återställ konfigurationen när alla klienter kopplas från istället för vid appstängning eller när sista sessionen avslutas.", "dd_config_verify_only": "Kontrollera att skärmen är aktiverad", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "Slå på/av HDR-läget som begärts av klienten (standard)", "dd_hdr_option_disabled": "Ändra inte HDR-inställningar", "dd_mode_remapping": "Visningsläge ommappning", "dd_mode_remapping_add": "Lägg till omappningspost", "dd_mode_remapping_desc_1": "Ange för att ändra den begärda upplösningen och/eller uppdatera hastigheten till andra värden.", "dd_mode_remapping_desc_2": "Listan är itererad från topp till botten och den första matchen används.", "dd_mode_remapping_desc_3": "\"Begärda\" fält kan lämnas tomma för att matcha alla begärda värden.", "dd_mode_remapping_desc_4_final_values_mixed": "Minst ett \"Final\"-fält måste anges. Ospecificerad upplösning eller uppdateringsfrekvens kommer inte att ändras.", "dd_mode_remapping_desc_4_final_values_non_mixed": "Fältet \"Final\" måste anges och kan inte vara tomt.", "dd_mode_remapping_desc_5_sops_mixed_only": "Alternativet \"Optimera spelinställningar\" måste vara aktiverat i Moonlight-klienten, annars hoppas poster med existerande upplösningsfält över.", "dd_mode_remapping_desc_5_sops_resolution_only": "Alternativet \"Optimera spelinställningar\" måste vara aktiverat i Moonlight klienten, annars hoppas mappningen över.", "dd_mode_remapping_final_refresh_rate": "Slutlig uppdateringsfrekvens", "dd_mode_remapping_final_resolution": "Slutlig upplösning", "dd_mode_remapping_requested_fps": "Begärd FPS", "dd_mode_remapping_requested_resolution": "Begärd upplösning", "dd_options_header": "Avancerade alternativ för skärm", "dd_refresh_rate_option": "Uppdateringsfrekvens", "dd_refresh_rate_option_auto": "Använd FPS-värde begärd av klienten (standard)", "dd_refresh_rate_option_disabled": "Ändra inte uppdateringsfrekvens", "dd_refresh_rate_option_manual": "Använd manuellt inmatad uppdateringsfrekvens", "dd_refresh_rate_option_manual_desc": "Ange uppdateringsfrekvens som ska användas", "dd_resolution_option": "Upplösning", "dd_resolution_option_auto": "Använd upplösning begärd av klienten (standard)", "dd_resolution_option_disabled": "Ändra inte upplösning", "dd_resolution_option_manual": "Använd manuellt inmatad upplösning", "dd_resolution_option_manual_desc": "Ange upplösning som ska användas", "dd_resolution_option_ogs_desc": "Alternativet \"Optimera spelinställningar\" måste vara aktiverat på Moonlight-klienten för att detta ska fungera.", "dd_wa_hdr_toggle_delay_desc_1": "När du använder virtuell skärmenhet (VDD) för streaming kan den visa HDR-färg felaktigt. Apollo kan försöka mildra detta problem genom att stänga av HDR och sedan slå på den igen.", "dd_wa_hdr_toggle_delay_desc_2": "Om värdet är inställt på 0 är lösningen inaktiverad (standard). Om värdet är mellan 0 och 3000 millisekunder kommer Apollo att stänga av HDR, vänta den angivna tiden och sedan slå på HDR igen. Den rekommenderade fördröjningstiden är runt 500 millisekunder i de flesta fall.", "dd_wa_hdr_toggle_delay_desc_3": "ANVÄND INTE denna lösning om du inte verkligen har problem med HDR eftersom den direkt påverkar streamens starttid!", "dd_wa_hdr_toggle_delay": "Högkontrastlösning för HDR", "double_refreshrate": "Dubbel uppdateringsfrekvens för Virtuell Skärm", "double_refreshrate_desc": "Fördubbla den begärda uppdateringsfrekvensen när virtuella skärmar skapas, streamad uppdateringsfrekvens förblir densamma. Kan potentiellt förbättra hackproblem på vissa system.", "ds4_back_as_touchpad_click": "Mappa Bakåt/Välj till Pekplatta-klick", "ds4_back_as_touchpad_click_desc": "När du tvingar DS4-emulering, mappa Bakåt/Välj till Pekplatta-klick", "enable_input_only_mode": "Aktivera endast inmatningsläge", "enable_input_only_mode_desc": "Lägg till en post för Endast Inmatning. När aktiverat kommer applistan endast visa den aktuella applikationen och posten för Endast inmatning vid streaming. Posten för Endast inmatning kommer inte ta emot någon bild eller ljud. Användbart för att styra skrivbordet på TV eller ansluta kringutrustning som TV:n inte stödjer med en telefon.", "enable_pairing": "Aktivera parkoppling", "enable_pairing_desc": "Aktivera parkoppling för Moonlight-klienten. Detta tillåter klienten att autentisera med värden och upprätta en säker anslutning.", "encoder": "Tvinga en specifik omkodare", "encoder_desc": "Tvinga en specifik omkodare, annars kommer Apollo att välja det bästa tillgängliga alternativet. Obs: Om du anger en hårdvaruomkodare i Windows, måste den matcha GPU där skärmen är ansluten.", "encoder_software": "Mjukvaruomkodare", "external_ip": "Extern IP", "external_ip_desc": "Om ingen extern IP-adress anges, kommer Apollo automatiskt upptäcka extern IP", "fallback_mode": "Backupläge för skärm", "fallback_mode_desc": "Apollo kommer att använda detta läge när klienten inte begärt ett läge eller när appen startas via webbgränssnittet. Format: [Bredd]x[Höjd]x[FPS]", "fallback_mode_error": "Ogiltigt backupläge. Format: [Bredd]x[Höjd]x[FPS]", "fec_percentage": "FEC Procent", "fec_percentage_desc": "Procentandel av felkorrigerande paket per datapaket i varje videoruta. Högre värden kan korrigera för mer nätverkspaketförlust, men på bekostnad av ökad bandbreddsanvändning.", "ffmpeg_auto": "auto -- låt ffmpeg bestämma (standard)", "file_apps": "App-fil", "file_apps_desc": "Filen där aktuella appar från Apollo lagras.", "file_state": "Statusfil", "file_state_desc": "Filen där nuvarande status av Apollo lagras", "gamepad": "Emulerad spelkontroll", "gamepad_auto": "Automatiska val", "gamepad_desc": "Välj vilken typ av gamepad som ska emuleras på värden", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "DS4 alternativ", "gamepad_ds5": "DS5 (PS5)", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Manuella DS4-alternativ", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Kommandoförberedelser", "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.", "headless_mode": "Headless Mode", "headless_mode_desc": "Starta Apollo i headless mode. När det är aktiverat kommer alla appar att starta i virtuell skärm.", "hevc_mode": "Stöd för HEVC", "hevc_mode_0": "Apollo kommer att annonsera stöd för HEVC baserat på kodarfunktioner (rekommenderas)", "hevc_mode_1": "Apollo kommer inte att annonsera stöd för HEVC", "hevc_mode_2": "Apollo kommer att annonsera stöd för HEVC Main profil", "hevc_mode_3": "Apollo kommer att annonsera stöd för HEVC Main och Main10 (HDR) profiler", "hevc_mode_desc": "Tillåter klienten att begära HEVC Main eller HEVC Main10 videoströmmar. HEVC är mer CPU-intensiv att omkoda, så att detta kan minska prestandan vid användning av mjukvaruomkodning.", "hide_tray_controls": "Dölj aktivitetsfältets kontrollalternativ", "hide_tray_controls_desc": "Visa inte \"Tvinga stopp\", \"Starta om\" och \"Avsluta\" i aktivitetsfältets meny.", "high_resolution_scrolling": "Stöd för högupplöst rullning", "high_resolution_scrolling_desc": "När aktiverad kommer Apollo att hantera högupplösta scroll-händelser från Moonlight klienter. Detta kan vara användbart att inaktivera för äldre program som bläddrar för snabbt med högupplösta scroll-händelser.", "install_steam_audio_drivers": "Installera Steam ljud-drivrutiner", "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 avstängning av värdljud.", "keep_sink_default": "Behåll virtuell ljudmottagare som standard", "keep_sink_default_desc": "Huruvida vald virtuell ljudmottagare ska tvingas vara standard (gäller när värdljudutgång är inaktiverad).", "key_repeat_delay": "Tangentupprepningsfördröjning", "key_repeat_delay_desc": "Kontrollera hur snabbt tangenter upprepar sig. Den initiala fördröjningen i millisekunder innan du upprepar tangentbordsinmatning.", "key_repeat_frequency": "Tangentupprepningsfrekvens", "key_repeat_frequency_desc": "Hur ofta tangenter upprepas per sekund. Detta konfigurerbara alternativ stöder decimaler.", "key_rightalt_to_key_win": "Mappa Höger Alt nyckel till Windows-tangenten", "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 Apollo tolkar Höger Alt som Windows-tangenten", "keyboard": "Aktivera tangentbordsinmatning", "keyboard_desc": "Tillåter gäster att styra värdsystemet med tangentbordet", "lan_encryption_mode": "LAN-krypteringsläge", "lan_encryption_mode_1": "Aktiverat för stödda klienter", "lan_encryption_mode_2": "Krävs för alla klienter", "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.", "limit_framerate": "Begränsa inspelningens bildfrekvens", "limit_framerate_desc": "Begränsa bildfrekvensen som spelas in till den bildfrekvens som klienten begär. Kanske inte körs med full bildfrekvens om vsync är aktiverat och skärmens uppdateringsfrekvens inte matchar begärd bildfrekvens. Kan orsaka fördröjning på vissa klienter om inaktiverad.", "locale": "Språk", "locale_desc": "Språket som används för Apollos användargränssnitt.", "log_level": "Loggnivå", "log_level_0": "Verbose", "log_level_1": "Debug", "log_level_2": "Info", "log_level_3": "Warning", "log_level_4": "Error", "log_level_5": "Fatal", "log_level_6": "None", "log_level_desc": "Minsta loggnivå utskriven till loggfilen.", "log_path": "Sökväg till loggfil", "log_path_desc": "Filen där de aktuella loggarna av Apollo lagras.", "max_bitrate": "Maximal bithastighet", "max_bitrate_desc": "Den maximala bithastigheten (i Kbps) som Apollo kommer att koda strömmen med. Om inställd på 0 kommer den alltid att använda den bithastighet som begärs av Artemis/Moonlight.", "min_fps_factor": "Minsta FPS faktor", "min_fps_factor_desc": "Apollo kommer att använda denna faktor för att beräkna minimitiden mellan bildrutor. Att öka detta värde något kan hjälpa vid streaming av mestadels statiskt innehåll. Högre värden kommer att förbruka mer bandbredd.", "min_threads": "Minsta antal CPU-trådar", "min_threads_desc": "Att öka värdet något minskar kodningseffektiviteten, men kompromissen är vanligtvis värd det för att kunna använda fler CPU-kärnor för kodning. Det ideala värdet är det lägsta värde som tillförlitligt kan koda med dina önskade streaminginställningar på din hårdvara.", "misc": "Diverse alternativ", "motion_as_ds4": "Emulera en DS4 spelkontroller om klientens spelkontroller rapporterar att rörelsesensorer finns", "motion_as_ds4_desc": "Om inaktiverad kommer rörelsesensorer inte att beaktas under val av speltyp.", "mouse": "Aktivera musinmatning", "mouse_desc": "Tillåter gäster att styra värdsystemet med musen", "native_pen_touch": "Inbyggt stöd för penna/tryck", "native_pen_touch_desc": "När aktiverad kommer Apollo att skicka vidare inbyggda penna/touch-händelser från Moonlight-klienter. Detta kan vara användbart att inaktivera för äldre applikationer utan inbyggt stöd för penna/touch.", "notify_pre_releases": "Notiser för förhandsversioner", "notify_pre_releases_desc": "Huruvida du meddelas om nya förhandsversioner av Apollo", "nvenc_h264_cavlc": "Föredra CAVLC över CABAC i H.264", "nvenc_h264_cavlc_desc": "Enklare form av entropi-omkodning. CAVLC behöver cirka 10% mer bithastighet för samma kvalitet. Endast relevant för riktigt gamla avkodningsenheter.", "nvenc_intra_refresh": "Intra-uppdatering", "nvenc_intra_refresh_desc": "Aktivera Intra-uppdatering för att vissa klienter ska renderas korrekt kontinuerligt (t.ex. Xbox-klient)", "nvenc_latency_over_power": "Föredra lägre omkodningsfördröjning över energibesparing", "nvenc_latency_over_power_desc": "Apollo begär maximal GPU klockfrekvens under strömning för att minskad omkodningslatens. Rekommenderas att inte inaktiveras eftersom detta kan leda till avsevärt ökad omkodningslatens.", "nvenc_opengl_vulkan_on_dxgi": "Presentera OpenGL/Vulkan ovanpå DXGI", "nvenc_opengl_vulkan_on_dxgi_desc": "Apollo 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.", "nvenc_preset": "Prestandaförinställning", "nvenc_preset_1": "(snabbast, standard)", "nvenc_preset_7": "(långsammast)", "nvenc_preset_desc": "Högre tal förbättrar kompression (kvalitet vid given bitrate) på bekostnad av ökad omkodningslatens. Rekommenderas att ändras endast när du är begränsad av nätverk eller avomkodare, annars kan liknande effekt åstadkommas genom att öka bithastigheten.", "nvenc_realtime_hags": "Använd realtidsprioritet i hårdvaruaccelererad gpu-schemaläggning", "nvenc_realtime_hags_desc": "För närvarande kan NVIDIA-drivrutiner frysa i kodaren när HAGS är aktiverat, realtidsprioritet används och VRAM-användningen är nära maximum. Genom att inaktivera detta alternativ sänks prioriteten till hög, vilket kringgår frysningen på bekostnad av reducerad inspelningsprestanda när grafikkortet är hårt belastat.", "nvenc_spatial_aq": "Spatial AQ", "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.", "nvenc_spatial_aq_disabled": "Inaktiverad (snabbare, standard)", "nvenc_spatial_aq_enabled": "Aktiverad (långsammare)", "nvenc_twopass": "Tvåpass-läge", "nvenc_twopass_desc": "Lägger till ett preliminärt omkodningspass. Detta möjliggör att fler röringsvektorer kan upptäckas, bättre fördelning av bithastighet över bildrutan och striktare följsamhet till bithastighetsgrägaser. Det rekommenderas inte att inaktivera detta eftersom det kan leda till tillfälliga överskridningar av bithastigheten och efterföljande paketförlust.", "nvenc_twopass_disabled": "Inaktiverad (snabbast rekommenderas inte)", "nvenc_twopass_full_res": "Full upplösning (långsammare)", "nvenc_twopass_quarter_res": "Kvartalsupplösning (snabbare, standard)", "nvenc_vbv_increase": "Procentuell ökning av VBV/HRD för enstaka bildruta", "nvenc_vbv_increase_desc": "Som standard använder Apollo VBV/HRD för enstaka bildrutor, vilket betyder att storleken på kodade videobildrutor inte förväntas överstiga begärd bithastighet delat med begärd bildfrekvens. Att lätta på denna begränsning kan vara fördelaktigt och fungera som låg-latens variabel bithastighet, men kan också leda till paketförlust om nätverket inte har tillräcklig buffert för att hantera toppar i bithastigheten. Det maximala accepterade värdet är 400, vilket motsvarar en 5x ökad övre gräns för kodad videobildrutestorlek.", "origin_web_ui_allowed": "Tillgång till webbgränssnitt från källa", "origin_web_ui_allowed_desc": "Från vilken källa som webbgränssnittet kan nås", "origin_web_ui_allowed_lan": "Endast de i LAN kan komma åt Webb UI", "origin_web_ui_allowed_pc": "Endast localhost kan komma åt webbgränssnittet", "origin_web_ui_allowed_wan": "Vem som helst kan komma åt webbgränssnittet", "output_name_desc_unix": "Vid uppstart av Apollo bör du se listan över upptäckta skärmar. Observera: Du behöver använda id-värdet inom parenteserna. Nedan finns ett exempel; den faktiska utdata kan hittas i fliken Felsökning.", "output_name_desc_windows": "Ange manuellt ett skärmenhet-id som ska användas för inspelning. Om inte inställt kommer den primära skärmen att spelas in. Observera: Om du angett en GPU ovan måste denna skärm vara ansluten till den GPU. Vid uppstart av Apollo bör du se listan över upptäckta skärmar. Nedan finns ett exempel; den faktiska utdata kan hittas i fliken Felsökning.", "output_name_unix": "Skärmnummer", "output_name_windows": "Skärmenhet-ID", "ping_timeout": "Ping Timeout", "ping_timeout_desc": "Hur lång tid att vänta i millisekunder för data från Apollo innan du stänger av strömmen", "pkey": "Privat nyckel", "pkey_desc": "Den privata nyckeln som används för webb UI och Moonlight parkoppling. För bästa kompatibilitet bör detta vara en RSA-2048 privat nyckel.", "port": "Port", "port_alert_1": "Apollo kan inte använda portar under 1024!", "port_alert_2": "Portar över 65535 är inte tillgängliga!", "port_desc": "Ställ in familjen av portar som används av Apollo", "port_http_port_note": "Använd denna port för att ansluta med Moonlight.", "port_note": "Anteckning", "port_port": "Port", "port_protocol": "Protocol", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "Att exponera Web UI till internet är en säkerhetsrisk! Fortsätt på egen risk!", "port_web_ui": "Webb UI", "qp": "Kvantifieringsparameter", "qp_desc": "Vissa enheter kanske inte stöder konstant Bit Rate. För dessa enheter används istället QP. Högre värde innebär mer komprimering och lägre kvalitet.", "qsv_coder": "QuickSync Omkodare (H264)", "qsv_preset": "QuickSync Preset", "qsv_preset_fast": "fast (lägre kvalitet)", "qsv_preset_faster": "faster (lägsta kvalitet)", "qsv_preset_medium": "medium (standard)", "qsv_preset_slow": "slow (god kvalitet)", "qsv_preset_slower": "slower (bättre kvalitet)", "qsv_preset_slowest": "slowest (bästa kvalitet)", "qsv_preset_veryfast": "fastest (lägsta kvalitet)", "qsv_slow_hevc": "Tillåt långsam HEVC-omkodning", "qsv_slow_hevc_desc": "Detta kan aktivera HEVC-omkodning på äldre Intel GPU:er, på bekostnad av högre GPU-användning och sämre prestanda.", "restart_note": "Apollo startar om för att tillämpa ändringar.", "server_cmd": "Serverkommandon", "server_cmd_desc": "Konfigurera en lista med kommandon som ska köras när de anropas från klienten under streaming.", "sunshine_name": "Apollo Namn", "sunshine_name_desc": "Namnet som visas av Moonlight. Om det inte anges används datorns värdnamn", "sw_preset": "SW förinställningar", "sw_preset_desc": "Optimera avvägningen mellan omkodningshastighet (kodade bildrutor per sekund) och komprimeringseffektivitet (kvalitet per bit i bitström). Standard är superfast.", "sw_preset_fast": "fast", "sw_preset_faster": "faster", "sw_preset_medium": "medium", "sw_preset_slow": "slow", "sw_preset_slower": "slower", "sw_preset_superfast": "superfast (standard)", "sw_preset_ultrafast": "ultrafast", "sw_preset_veryfast": "veryfast", "sw_preset_veryslow": "veryslow", "sw_tune": "SW Tune", "sw_tune_animation": "animation -- bra för tecknade filmer; använder högre avblockering och fler referensbilder", "sw_tune_desc": "Finjusteringsalternativ som tillämpas efter förinställningen. Standard är zerolatency.", "sw_tune_fastdecode": "fastdecode -- möjliggör snabbare avkodning genom att inaktivera vissa filter", "sw_tune_film": "film -- använd för filminnehåll av hög kvalitet; sänker avblockering", "sw_tune_grain": "grain -- bevarar kornstrukturen i gammalt, kornigt filmmaterial", "sw_tune_stillimage": "stillimage -- bra för bildspelsliknande innehåll", "sw_tune_zerolatency": "zerolatency -- bra för snabb kodning och streaming med låg latens (standard)", "touchpad_as_ds4": "Emulera en DS4-spelkontroll om klientens spelkontroll rapporterar att en pekplatta finns", "touchpad_as_ds4_desc": "Om inaktiverad kommer pekplattans närvaro inte att beaktas vid val av spelkontrollstyp.", "upnp": "UPnP", "upnp_desc": "Konfigurera portvidarebefordran automatiskt för streaming över Internet", "vaapi_strict_rc_buffer": "Strikt tillämpa gränser för bildrutans bithastighet för H.264/HEVC på AMD GPU", "vaapi_strict_rc_buffer_desc": "Aktivering av detta alternativ kan undvika tappade bildrutor över nätverket vid scenförändringar, men videokvaliteten kan minska vid rörelse.", "virtual_sink": "Virtuell ljudmottagare", "virtual_sink_desc": "Ljudenheten som ska användas när ljudutmatning inte tillåts på värden av klienten.\nOm ej inställd väljs enheten automatiskt.\nVi rekommenderar starkt att lämna detta fält tomt för att använda automatiskt enhetsval!", "virtual_sink_placeholder": "Steam Streaming Högtalare", "vt_coder": "VideoToolbox-kodare", "vt_realtime": "VideoToolbox realtidskodning", "vt_software": "VideoToolbox mjukvarukodning", "vt_software_allowed": "Tillåten", "vt_software_forced": "Tvingad", "wan_encryption_mode": "WAN-krypteringsläge", "wan_encryption_mode_1": "Aktiverad för klienter med stöd (standard)", "wan_encryption_mode_2": "Krävs för alla klienter", "wan_encryption_mode_desc": "Detta avgör när kryptering kommer att användas vid streaming över Internet. Kryptering kan minska streamingprestandan, särskilt på mindre kraftfulla värdar och klienter." }, "login": { "save_password": "Kom ihåg lösenord" }, "index": { "description": "Apollo är en självhostad spelstreaming-värd för Moonlight.", "download": "Ladda ner", "installed_version_not_stable": "Du kör en förhandsversion av Apollo. Du kan uppleva buggar eller andra problem. Vänligen rapportera eventuella problem du stöter på. Tack för att du hjälper till att göra Apollo till en bättre programvara!", "loading_latest": "Laddar senaste versionen...", "new_pre_release": "En ny förhandsversion är tillgänglig!", "new_stable": "En ny stabil version är tillgänglig!", "startup_errors": "Observera! Apollo upptäckte dessa fel vid uppstart. Vi REKOMMENDERAR STARKT att du åtgärdar dem innan streaming.", "version_dirty": "Tack för att du hjälper till att göra Apollo till en bättre programvara!", "version_latest": "Du kör den senaste versionen av Apollo", "welcome": "Hej, Apollo!" }, "navbar": { "applications": "Applikationer", "configuration": "Konfiguration", "home": "Hem", "password": "Ändra lösenord", "pin": "Pin", "theme_auto": "Automatiskt", "theme_dark": "Mörk", "theme_light": "Ljus", "toggle_theme": "Tema", "troubleshoot": "Felsökning" }, "password": { "confirm_password": "Bekräfta lösenord", "current_creds": "Nuvarande uppgifter", "new_creds": "Nya inloggningsuppgifter", "new_username_desc": "Om det inte anges kommer användarnamnet inte att ändras", "password_change": "Ändra lösenord", "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." }, "permissions": { "input_controller": "Kontrollerinmatning", "input_touch": "Touchinmatning", "input_pen": "Pennainmatning", "input_mouse": "Musinmatning", "input_kbd": "Tangentbordsinmatning", "clipboard_set": "Ställ in urklipp", "clipboard_read": "Läs urklipp", "file_upload": "Filuppladdning", "file_dwnload": "Filnedladdning", "server_cmd": "Serverkommando", "list": "Lista appar", "view": "Visa strömmar", "launch": "Starta appar" }, "pin": { "client_do_cmd": "Klientanslutningskommandon", "client_do_cmd_desc": "Kommandon som ska köras när klienten ansluter. Alla kommandon körs frikopplade.", "client_undo_cmd": "Klientfrånkopplingskommandon", "client_undo_cmd_desc": "Kommandon som ska köras när klienten kopplar från. Alla kommandon körs frikopplade.", "device_name": "Valfritt: Enhetsnamn", "display_mode_override": "Åsidosättning av visningsläge", "display_mode_override_desc": "Apollo kommer att ignorera klientens begärda visningsläge och använda detta värde för att konfigurera (virtuella) skärmar. Lämna tomt för automatisk matchning. Format: [Bredd]x[Höjd]x[FPS]", "display_mode_override_error": "Ogiltigt åsidosättningsläge. Format: [Bredd]x[Höjd]x[FPS]", "pair_failure": "Parkoppling misslyckades: Kontrollera om PIN-koden är korrekt inskriven", "pair_success": "Lyckades! Kontrollera Moonlight för att fortsätta", "pair_success_check_perm": "Parkoppling lyckades! Vänligen ge nödvändiga behörigheter till klienten manuellt nedan.", "pin_pairing": "PIN-parkoppling", "send": "Skicka", "warning_msg": "Försäkra dig om att du har tillgång till klienten du parkopplar med. Denna programvara kan ge total kontroll över din dator, så var försiktig!", "otp_pairing": "OTP-parkoppling", "generate_pin": "Generera PIN", "otp_passphrase": "Engångslösenord", "otp_expired": "UTGÅNGEN", "otp_expired_msg": "OTP har gått ut. Vänligen begär en ny.", "otp_success": "PIN-begäran lyckades, PIN-koden är tillgänglig inom 3 minuter.", "otp_msg": "OTP-parkoppling är endast tillgänglig för de senaste Artemis-klienterna. Använd äldre parkopplingsmetod för andra klienter.", "otp_pair_now": "PIN genererades framgångsrikt, vill du parkoppla nu?", "device_management": "Enhetshantering", "device_management_desc": "Hantera dina parkopplade enheter och deras behörigheter.", "device_management_warning": "Den första parkopplade enheten kommer att ha fulla behörigheter som standard. Andra nyligen parkopplade enheter kommer att ha begränsade behörigheter som standard.", "save_client_error": "Fel vid sparande av klient: ", "unpair_all": "Koppla från alla", "unpair_all_success": "Alla enheter frånkopplade.", "unpair_all_error": "Fel vid frånkoppling", "unpair_single_no_devices": "Det finns inga parkopplade enheter.", "unpair_single_success": "Enheten/enheterna kan dock fortfarande vara i en aktiv session. Använd knappen 'Tvinga stängning' ovan för att avsluta eventuella öppna sessioner.", "unpair_single_unknown": "Okänd klient" }, "resource_card": { "github_discussions": "GitHub Discussions", "legal": "Juridiskt", "legal_desc": "Genom att fortsätta använda denna programvara godkänner du villkoren i följande dokument.", "license": "Licens", "lizardbyte_website": "LizardByte Hemsida", "resources": "Resurser", "resources_desc": "Resurser för Apollo!", "third_party_notice": "Meddelande från tredje part" }, "troubleshooting": { "dd_reset": "Återställ permanenta inställningar för skärmenhet", "dd_reset_desc": "Om Apollo har fastnat i försök att återställa de ändrade inställningarna för skärmenheten kan du återställa inställningarna och fortsätta att återställa skärmläget manuellt.", "dd_reset_error": "Fel vid återställning av permanenta inställningar!", "dd_reset_success": "Lyckades återställa permanenta inställningar!", "force_close": "Tvinga stängning", "force_close_desc": "Om Moonlight klagar på att en app körs för närvarande bör tvingad stängning av appen åtgärda problemet.", "force_close_error": "Fel vid stängning av applikation", "force_close_success": "Applikation stängdes framgångsrikt!", "logs": "Loggar", "logs_desc": "Se loggarna som laddats upp av Apollo", "logs_find": "Sök...", "restart_apollo": "Starta om Apollo", "restart_apollo_desc": "Om Apollo inte fungerar korrekt kan du försöka starta om det. Detta kommer att avsluta alla pågående sessioner.", "restart_apollo_success": "Apollo startar om", "quit_apollo": "Avsluta Apollo", "quit_apollo_desc": "Avsluta Apollo. Detta kommer att avsluta alla pågående sessioner.", "quit_apollo_success": "Apollo har avslutats.", "quit_apollo_success_ongoing": "Apollo avslutas...", "quit_apollo_confirm": "Vill du verkligen avsluta Apollo? Du kommer inte att kunna starta Apollo igen om du inte har andra metoder för att använda din dator.", "troubleshooting": "Felsökning" }, "welcome": { "confirm_password": "Bekräfta lösenord", "create_creds": "Innan vi börjar behöver du skapa ett nytt användarnamn och lösenord för att komma åt webbgränssnittet.", "create_creds_alert": "Inloggningsuppgifterna nedan behövs för att komma åt Apollos webbgränssnitt. Förvara dem säkert, eftersom du aldrig kommer att se dem igen!", "greeting": "Välkommen till Apollo!", "login": "Logga in", "welcome_success": "Denna sida kommer att laddas om snart, din webbläsare kommer att be om de nya inloggningsuppgifterna", "login_success": "Denna sida kommer att laddas om snart." } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/tr.json ================================================ { "_common": { "apply": "Uygula", "auto": "Otomatik", "autodetect": "Otomatik Algıla (önerilir)", "beta": "(beta)", "cancel": "İptal", "disabled": "Devre Dışı", "disabled_def": "Devre Dışı (varsayılan)", "disabled_def_cbox": "Varsayılan: işaretlenmemiş", "dismiss": "Yoksay", "do_cmd": "Komut Yap", "elevated": "Yükseltilmiş", "enabled": "Etkin", "enabled_def": "Etkin (varsayılan)", "enabled_def_cbox": "Varsayılan: işaretli", "error": "Hata!", "note": "Not:", "password": "Şifre", "run_as": "Yönetici olarak çalıştır", "save": "Kaydet", "see_more": "Daha Fazla Gör", "success": "Başarılı!", "undo_cmd": "Geri Al Komutu", "username": "Kullanıcı Adı", "warning": "Uyarı!" }, "apps": { "actions": "Eylemler", "add_cmds": "Komutlar Ekle", "add_new": "Yeni Ekle", "app_name": "Uygulama Adı", "app_name_desc": "Uygulama Adı, Moonlight'ta gösterildiği gibi", "applications_desc": "Uygulamalar yalnızca İstemci yeniden başlatıldığında yenilenir", "applications_title": "Uygulamalar", "auto_detach": "Uygulama hızlı bir şekilde çıkarsa akışa devam edin", "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.", "cmd": "Komut", "cmd_desc": "Başlatılacak ana uygulama. Boşsa, hiçbir uygulama başlatılmayacaktır.", "cmd_note": "Komut çalıştırılabilir dosyasının yolu boşluk içeriyorsa, tırnak içine almanız gerekir.", "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.", "cmd_prep_name": "Komut Hazırlıkları", "covers_found": "Kapaklar Bulundu", "delete": "Sil", "detached_cmds": "Müstakil Komutlar", "detached_cmds_add": "Müstakil Komut Ekleme", "detached_cmds_desc": "Arka planda çalıştırılacak komutların bir listesi.", "detached_cmds_note": "Komut çalıştırılabilir dosyasının yolu boşluk içeriyorsa, tırnak içine almanız gerekir.", "edit": "Düzenle", "env_app_id": "Uygulama Kimliği", "env_app_name": "Uygulama Adı", "env_client_audio_config": "İstemci tarafından talep edilen Ses Yapılandırması (2.0/5.1/7.1)", "env_client_enable_sops": "İstemci, oyunu optimum akış için optimize etme seçeneğini talep etti (doğru/yanlış)", "env_client_fps": "İstemci tarafından talep edilen FPS (float)", "env_client_gcmap": "İstenen kontrolcü maskesi, bit kümesi/bit alanı biçiminde (int)", "env_client_hdr": "HDR istemci tarafından etkinleştirildi (true/false)", "env_client_height": "İstemci tarafından talep edilen Yükseklik (int)", "env_client_host_audio": "İstemci ana bilgisayar sesi talep etti (true/false)", "env_client_width": "İstemci tarafından talep edilen Genişlik (int)", "env_displayplacer_example": "Örnek - Resolution Automation için displayplacer:", "env_qres_example": "Örnek - Çözünürlük Otomasyonu için QRes:", "env_qres_path": "qres yolu", "env_var_name": "Var Adı", "env_vars_about": "Ortam Değişkenleri Hakkında", "env_vars_desc": "Tüm komutlar varsayılan olarak bu ortam değişkenlerini alır:", "env_xrandr_example": "Örnek - Çözüm Otomasyonu için Xrandr:", "exit_timeout": "Çıkış Zaman Aşımı", "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.", "find_cover": "Kapak Resmi Bul", "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.", "global_prep_name": "Global Hazırlık Komutları", "image": "Resim", "image_desc": "İstemciye gönderilecek uygulama simgesi/resmi/görüntü yolu. Görüntü bir PNG dosyası olmalıdır. Ayarlanmazsa, Apollo varsayılan kutu görüntüsünü gönderir.", "loading": "Yükleniyor...", "name": "Ad", "output_desc": "Komutun çıktısının saklanacağı dosya, belirtilmezse çıktı yok sayılır", "output_name": "Çıktı", "run_as_desc": "Bu, düzgün çalışması için yönetici izinleri gerektiren bazı uygulamalar için gerekli olabilir.", "wait_all": "Tüm uygulama süreçleri çıkana kadar akışa devam edin", "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.", "working_dir": "Çalışma Dizini", "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, Apollo varsayılan olarak komutun üst dizinini kullanır" }, "config": { "adapter_name": "Adaptör Adı", "adapter_name_desc_linux_1": "Yakalama için kullanılacak GPU'yu manuel olarak belirleyin.", "adapter_name_desc_linux_2": "VAAPI kullanabilen tüm cihazları bulmak için", "adapter_name_desc_linux_3": "Cihazın adını ve özelliklerini listelemek için ``renderD129`` yerine yukarıdaki cihazı yazın. Apollo tarafından desteklenebilmesi için en azından şu özelliklere sahip olması gerekir:", "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:", "adapter_name_placeholder_windows": "Radeon RX 580 Serisi", "add": "Ekle", "address_family": "Adres Aile", "address_family_both": "IPv4+IPv6", "address_family_desc": "Apollo tarafından kullanılan adres ailesini ayarlama", "address_family_ipv4": "Yalnızca IPv4", "always_send_scancodes": "Her Zaman Scancode Gönderin", "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.", "amd_coder": "AMF Kodlayıcı (H264)", "amd_coder_desc": "Kaliteye veya kodlama hızına öncelik vermek için entropi kodlamasını seçmenizi sağlar. Yalnızca H.264.", "amd_enforce_hrd": "AMF Varsayımsal Referans Kod Çözücü (HRD) Uygulaması", "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.", "amd_preanalysis": "AMF Ön Analizi", "amd_preanalysis_desc": "Bu, artan kodlama gecikmesi pahasına kaliteyi artırabilecek hız kontrolü ön analizini mümkün kılar.", "amd_quality": "AMF Kalitesi", "amd_quality_balanced": "dengeli -- dengeli (varsayılan)", "amd_quality_desc": "Bu, kodlama hızı ve kalitesi arasındaki dengeyi kontrol eder.", "amd_quality_group": "AMF Kalite Ayarları", "amd_quality_quality": "kalite -- kaliteyi tercih edin", "amd_quality_speed": "hız -- hızı tercih et", "amd_rc": "AMF Oran Kontrolü", "amd_rc_cbr": "cbr -- sabit bit hızı (HRD etkinleştirilmişse önerilir)", "amd_rc_cqp": "cqp -- sabit qp modu", "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.", "amd_rc_group": "AMF Hız Kontrol Ayarları", "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)", "amd_rc_vbr_peak": "vbr_peak -- tepe noktası kısıtlı değişken bit hızı", "amd_usage": "AMF Kullanımı", "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.", "amd_usage_lowlatency": "lowlatency - düşük gecikme süresi (en hızlı)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - düşük gecikme süresi, yüksek kalite (hızlı)", "amd_usage_transcoding": "kod dönüştürme -- kod dönüştürme (en yavaş)", "amd_usage_ultralowlatency": "ultralowlatency - ultra düşük gecikme süresi (en hızlı; varsayılan)", "amd_usage_webcam": "web kamerası -- web kamerası (yavaş)", "amd_vbaq": "AMF Varyans Tabanlı Uyarlanabilir Niceleme (VBAQ)", "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.", "apply_note": "Apollo'ı yeniden başlatmak ve değişiklikleri uygulamak için 'Uygula'ya tıklayın. Bu, çalışan tüm oturumları sonlandıracaktır.", "audio_sink": "Ses Hedefi", "audio_sink_desc_linux": "Ses Geri Döngüsü için kullanılan ses emicinin adı. Bu değişkeni belirtmezseniz pulseaudio varsayılan monitör cihazını seçecektir. Herhangi bir komutu kullanarak ses emicinin adını bulabilirsiniz:", "audio_sink_desc_macos": "Ses Geri Döngüsü için kullanılan ses emicinin adı. Apollo, sistem sınırlamaları nedeniyle yalnızca macOS üzerindeki mikrofonlara erişebilir. Soundflower veya BlackHole kullanarak sistem sesi akışı sağlamak için.", "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:", "audio_sink_placeholder_macos": "BlackHole 2ch", "audio_sink_placeholder_windows": "Hoparlörler (Yüksek Çözünürlüklü Ses Cihazı)", "av1_mode": "AV1 Desteği", "av1_mode_0": "Apollo, kodlayıcı özelliklerine göre AV1 desteğini duyuracak (önerilir)", "av1_mode_1": "Apollo AV1 için desteği duyurmayacak", "av1_mode_2": "Apollo AV1 Ana 8-bit profil desteğinin reklamını yapacak", "av1_mode_3": "Apollo, AV1 Ana 8-bit ve 10-bit (HDR) profilleri desteğini duyuracak", "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.", "back_button_timeout": "Ana Sayfa/Yönlendirme Düğmesi Emülasyon Zaman Aşımı", "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.", "capture": "Belirli Bir Yakalama Yöntemini Zorlama", "capture_desc": "Otomatik modda Apollo çalışan ilk sürücüyü kullanacaktır. NvFBC yamalanmış nvidia sürücüleri gerektirir.", "cert": "Sertifika", "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.", "channels": "Maksimum Bağlı İstemci", "channels_desc_1": "Apollo, tek bir akış oturumunun aynı anda birden fazla istemci ile paylaşılmasına izin verebilir.", "channels_desc_2": "Bazı donanım kodlayıcıları, birden fazla akışla performansı düşüren sınırlamalara sahip olabilir.", "coder_cabac": "cabac -- bağlama uyarlanabi̇li̇r i̇ki̇li̇ ari̇tmeti̇k kodlama - daha yüksek kali̇te", "coder_cavlc": "cavlc -- bağlam uyarlamalı değişken uzunluklu kodlama - daha hızlı kod çözme", "configuration": "Konfigürasyon", "controller": "Kontrolcü Girişini Etkinleştir", "controller_desc": "Konukların ana sistemi bir gamepad / kontrol cihazı ile kontrol etmesine olanak tanır", "credentials_file": "Kimlik Bilgileri Dosyası", "credentials_file_desc": "Kullanıcı Adı/Şifre'yi Apollo'ın durum dosyasından ayrı olarak saklayın.", "dd_config_ensure_active": "Ekranı otomatik olarak etkinleştirin", "dd_config_ensure_only_display": "Diğer ekranları devre dışı bırakma ve yalnızca belirtilen ekranı etkinleştirme", "dd_config_ensure_primary": "Ekranı otomatik olarak etkinleştirin ve birincil ekran haline getirin", "dd_config_label": "Cihaz yapılandırması", "dd_config_revert_delay": "Yapılandırma geri dönüş gecikmesi", "dd_config_revert_delay_desc": "Uygulama kapatıldığında veya son oturum sonlandırıldığında yapılandırmaya geri dönmeden önce beklemek için milisaniye cinsinden ek gecikme. Ana amaç, uygulamalar arasında hızlı geçiş yaparken daha yumuşak bir geçiş sağlamaktır.", "dd_config_verify_only": "Ekranın etkin olduğunu doğrulayın", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "İstemci tarafından talep edildiği şekilde HDR modunu açma/kapatma (varsayılan)", "dd_hdr_option_disabled": "HDR ayarlarını değiştirmeyin", "dd_mode_remapping": "Ekran modu yeniden eşleme", "dd_mode_remapping_add": "Yeniden eşleme girişi ekle", "dd_mode_remapping_desc_1": "İstenen çözünürlüğü ve/veya yenileme hızını başka değerlere değiştirmek için yeniden eşleme girişlerini belirtin.", "dd_mode_remapping_desc_2": "Liste yukarıdan aşağıya doğru yinelenir ve ilk eşleşme kullanılır.", "dd_mode_remapping_desc_3": "\"Talep edilen\" alanlar, talep edilen herhangi bir değerle eşleşecek şekilde boş bırakılabilir.", "dd_mode_remapping_desc_4_final_values_mixed": "En az bir \"Final\" alanı belirtilmelidir. Belirtilmeyen çözünürlük veya yenileme hızı değiştirilmeyecektir.", "dd_mode_remapping_desc_4_final_values_non_mixed": "\"Final\" alanı belirtilmelidir ve boş olamaz.", "dd_mode_remapping_desc_5_sops_mixed_only": "Moonlight istemcisinde \"Oyun ayarlarını optimize et\" seçeneği etkinleştirilmelidir, aksi takdirde herhangi bir çözünürlük alanı belirtilen girişler atlanır.", "dd_mode_remapping_desc_5_sops_resolution_only": "Moonlight istemcisinde \"Oyun ayarlarını optimize et\" seçeneği etkinleştirilmelidir, aksi takdirde eşleme atlanır.", "dd_mode_remapping_final_refresh_rate": "Son yenileme hızı", "dd_mode_remapping_final_resolution": "Nihai çözüm", "dd_mode_remapping_requested_fps": "Talep Edilen FPS", "dd_mode_remapping_requested_resolution": "Talep edilen çözüm", "dd_options_header": "Gelişmiş görüntüleme cihazı seçenekleri", "dd_refresh_rate_option": "Yenileme hızı", "dd_refresh_rate_option_auto": "İstemci tarafından sağlanan FPS değerini kullan (varsayılan)", "dd_refresh_rate_option_disabled": "Yenileme hızını değiştirmeyin", "dd_refresh_rate_option_manual": "Manuel olarak girilen yenileme hızını kullanın", "dd_refresh_rate_option_manual_desc": "Kullanılacak yenileme hızını girin", "dd_resolution_option": "Çözünürlük", "dd_resolution_option_auto": "İstemci tarafından sağlanan çözünürlüğü kullan (varsayılan)", "dd_resolution_option_disabled": "Çözünürlüğü değiştirmeyin", "dd_resolution_option_manual": "Manuel olarak girilen çözünürlüğü kullanın", "dd_resolution_option_manual_desc": "Kullanılacak çözünürlüğü girin", "dd_resolution_option_ogs_desc": "Bunun çalışması için Moonlight istemcisinde \"Oyun ayarlarını optimize et\" seçeneği etkinleştirilmelidir.", "dd_wa_hdr_toggle_desc": "Akış için sanal görüntüleme cihazı kullanırken, yanlış HDR rengi görüntüleyebilir. Bu seçenek etkinleştirildiğinde, Apollo bu sorunu hafifletmeye çalışacaktır.", "dd_wa_hdr_toggle": "HDR için yüksek kontrastlı geçici çözümü etkinleştirin", "ds4_back_as_touchpad_click": "Geri/Seçimi Dokunmatik Yüzeye Eşle Tıklama", "ds4_back_as_touchpad_click_desc": "DS4 emülasyonunu zorlarken, Geri/Seç'i Dokunmatik Yüzey Tıklaması ile eşleyin", "encoder": "Belirli Bir Kodlayıcıyı Zorla", "encoder_desc": "Belirli bir kodlayıcıyı zorlayın, aksi takdirde Apollo 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.", "encoder_software": "Yazılım", "external_ip": "Harici IP", "external_ip_desc": "Harici IP adresi verilmezse, Apollo harici IP'yi otomatik olarak algılar", "fec_percentage": "FEC Yüzdesi", "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.", "ffmpeg_auto": "auto -- ffmpeg'in karar vermesine izin ver (varsayılan)", "file_apps": "Uygulamalar Dosyası", "file_apps_desc": "Apollo'ın mevcut uygulamalarının depolandığı dosya.", "file_state": "Durum Dosyası", "file_state_desc": "Apollo'ın mevcut durumunun depolandığı dosya", "fps": "İlan Edilen FPS", "gamepad": "Emüle Edilmiş Oyun Kumandası Türü", "gamepad_auto": "Otomatik seçim seçenekleri", "gamepad_desc": "Ana bilgisayarda hangi tür gamepad'in taklit edileceğini seçin", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "DS4 seçim seçenekleri", "gamepad_ds5": "DS5 (PS5)", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Manuel DS4 seçenekleri", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Komut Hazırlıkları", "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.", "hevc_mode": "HEVC Desteği", "hevc_mode_0": "Apollo, kodlayıcı yeteneklerine göre HEVC desteğini yayınlayacak (önerilir)", "hevc_mode_1": "Apollo HEVC desteğinin reklamını yapmayacak", "hevc_mode_2": "Apollo, HEVC Ana profil desteğinin reklamını yapacak", "hevc_mode_3": "Apollo, HEVC Main ve Main10 (HDR) profillerine yönelik desteğin reklamını yapacak", "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.", "high_resolution_scrolling": "Yüksek Çözünürlüklü Kaydırma Desteği", "high_resolution_scrolling_desc": "Etkinleştirildiğinde Apollo, 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.", "install_steam_audio_drivers": "Steam Ses Sürücülerini Yükleyin", "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.", "key_repeat_delay": "Tuş Tekrarlama Gecikmesi", "key_repeat_delay_desc": "Tuşların kendilerini ne kadar hızlı tekrarlayacaklarını kontrol edin. Tuşları tekrarlamadan önce milisaniye cinsinden ilk gecikme.", "key_repeat_frequency": "Anahtar Tekrarlama Sıklığı", "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.", "key_rightalt_to_key_win": "Sağ Alt tuşunu Windows tuşuyla eşleştirme", "key_rightalt_to_key_win_desc": "Windows Tuşunu Moonlight'tan doğrudan gönderemiyor olabilirsiniz. Bu gibi durumlarda Apollo'ın Sağ Alt tuşunun Windows tuşu olduğunu düşünmesini sağlamak yararlı olabilir", "keyboard": "Klavye Girişini Etkinleştir", "keyboard_desc": "Konukların ana sistemi klavye ile kontrol etmesini sağlar", "lan_encryption_mode": "LAN Şifreleme Modu", "lan_encryption_mode_1": "Desteklenen istemciler için etkinleştirildi", "lan_encryption_mode_2": "Tüm müşteriler için gereklidir", "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.", "locale": "Yerel", "locale_desc": "Apollo'ın kullanıcı arayüzü için kullanılan yerel ayar.", "log_level": "Günlük Seviyesi", "log_level_0": "Verbose", "log_level_1": "Hata Ayıklama", "log_level_2": "Bilgi", "log_level_3": "Uyarı", "log_level_4": "Hata", "log_level_5": "Kritik", "log_level_6": "Hiçbiri", "log_level_desc": "Standart çıkışa yazdırılan minimum günlük düzeyi", "log_path": "Günlük Dosyası Yolu", "log_path_desc": "Apollo'ın geçerli günlüklerinin depolandığı dosya.", "min_fps_factor": "Minimum FPS Faktörü", "min_fps_factor_desc": "Apollo, kareler arasındaki minimum süreyi hesaplamak için bu faktörü kullanacaktır. Bu değeri biraz artırmak, çoğunlukla statik içerik akışı yaparken yardımcı olabilir. Daha yüksek değerler daha fazla bant genişliği tüketecektir.", "min_threads": "Minimum CPU İplik Sayısı", "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.", "misc": "Çeşitli seçenekler", "motion_as_ds4": "İstemci gamepad hareket sensörlerinin mevcut olduğunu bildirirse bir DS4 gamepad taklit edin", "motion_as_ds4_desc": "Devre dışı bırakılırsa, gamepad tipi seçimi sırasında hareket sensörleri dikkate alınmaz.", "mouse": "Fare Girişini Etkinleştir", "mouse_desc": "Konukların ana sistemi fare ile kontrol etmesini sağlar", "native_pen_touch": "Yerel Kalem/Dokunmatik Desteği", "native_pen_touch_desc": "Etkinleştirildiğinde Apollo, 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.", "notify_pre_releases": "Yayın Öncesi Bildirimler", "notify_pre_releases_desc": "Apollo'ın yeni yayın öncesi sürümlerinden haberdar edilip edilmeme", "nvenc_h264_cavlc": "H.264'te CAVLC'yi CABAC'a tercih edin", "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.", "nvenc_latency_over_power": "Güç tasarrufu yerine daha düşük kodlama gecikmesini tercih edin", "nvenc_latency_over_power_desc": "Apollo, 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.", "nvenc_opengl_vulkan_on_dxgi": "OpenGL/Vulkan'ı DXGI üzerinde sunma", "nvenc_opengl_vulkan_on_dxgi_desc": "Apollo, DXGI'nin üstünde bulunmadıkları sürece tam ekran OpenGL ve Vulkan programlarını tam kare hızında yakalayamaz. Bu, Apollo programından çıkıldığında geri döndürülen sistem genelinde bir ayardır.", "nvenc_preset": "Performans ön ayarı", "nvenc_preset_1": "(en hızlı, varsayılan)", "nvenc_preset_7": "(en yavaş)", "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.", "nvenc_realtime_hags": "Donanım hızlandırmalı gpu zamanlamasında gerçek zamanlı önceliği kullanma", "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.", "nvenc_spatial_aq": "Uzamsal AQ", "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.", "nvenc_spatial_aq_disabled": "Devre dışı (daha hızlı, varsayılan)", "nvenc_spatial_aq_enabled": "Etkin (daha yavaş)", "nvenc_twopass": "İki geçişli mod", "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.", "nvenc_twopass_disabled": "Devre dışı (en hızlı, önerilmez)", "nvenc_twopass_full_res": "Tam çözünürlük (daha yavaş)", "nvenc_twopass_quarter_res": "Çeyrek çözünürlük (daha hızlı, varsayılan)", "nvenc_vbv_increase": "Tek kare VBV/HRD yüzde artışı", "nvenc_vbv_increase_desc": "Varsayılan olarak Apollo 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.", "origin_web_ui_allowed": "Origin Web Kullanıcı Arayüzüne İzin Verildi", "origin_web_ui_allowed_desc": "Web UI'ye erişimi reddedilmeyen uzak uç nokta adresinin kaynağı", "origin_web_ui_allowed_lan": "Yalnızca LAN'da bulunanlar Web UI'ye erişebilir", "origin_web_ui_allowed_pc": "Web kullanıcı arayüzüne yalnızca localhost erişebilir", "origin_web_ui_allowed_wan": "Web UI'ye herkes erişebilir", "output_name_desc_unix": "Apollo başlangıcı sırasında, algılanan ekranların listesini görmelisiniz. Not: Parantez içindeki id değerini kullanmanız gerekir.", "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:", "output_name_unix": "Ekran numarası", "output_name_windows": "Çıktı Adı", "ping_timeout": "Ping Zaman Aşımı", "ping_timeout_desc": "Akışı kapatmadan önce ay ışığından gelen veriler için milisaniye cinsinden ne kadar süre bekleneceği", "pkey": "Özel Anahtar", "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.", "port": "Port", "port_alert_1": "Apollo 1024'ün altındaki bağlantı noktalarını kullanamaz!", "port_alert_2": "65535'in üzerindeki bağlantı noktaları kullanılamaz!", "port_desc": "Apollo tarafından kullanılan bağlantı noktaları ailesini ayarlayın", "port_http_port_note": "Moonlight ile bağlantı kurmak için bu bağlantı noktasını kullanın.", "port_note": "Not", "port_port": "Port", "port_protocol": "Protokol", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "Web kullanıcı arayüzünü internete açmak bir güvenlik riskidir! Kendi sorumluluğunuzda devam edin!", "port_web_ui": "Web Kullanıcı Arayüzü", "qp": "Kuantizasyon Parametresi", "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.", "qsv_coder": "QuickSync Kodlayıcı (H264)", "qsv_preset": "QuickSync Ön Ayarı", "qsv_preset_fast": "hızlı (düşük kalite)", "qsv_preset_faster": "daha hızlı (daha düşük kalite)", "qsv_preset_medium": "orta (varsayılan)", "qsv_preset_slow": "yavaş (iyi kalite)", "qsv_preset_slower": "daha yavaş (daha iyi kalite)", "qsv_preset_slowest": "en yavaş (en iyi kalite)", "qsv_preset_veryfast": "en hızlı (en düşük kalite)", "qsv_slow_hevc": "Yavaş HEVC Kodlamasına İzin Ver", "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.", "res_fps_desc": "Apollo tarafından ilan edilen 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.", "resolutions": "İlan Edilen Kararlar", "restart_note": "Apollo değişiklikleri uygulamak için yeniden başlatılıyor.", "sunshine_name": "Günışığı Adı", "sunshine_name_desc": "Moonlight tarafından görüntülenen ad. Belirtilmezse, bilgisayarın ana bilgisayar adı kullanılır", "sw_preset": "SW Ön Ayarları", "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.", "sw_preset_fast": "hızlı", "sw_preset_faster": "daha hızlı", "sw_preset_medium": "orta", "sw_preset_slow": "yavaş", "sw_preset_slower": "daha yavaş", "sw_preset_superfast": "süper hızlı (varsayılan)", "sw_preset_ultrafast": "ultra hızlı", "sw_preset_veryfast": "çok hızlı", "sw_preset_veryslow": "çok yavaş", "sw_tune": "SW Tune", "sw_tune_animation": "animasyon -- çizgi filmler için iyi; daha yüksek deblocking ve daha fazla referans karesi kullanır", "sw_tune_desc": "Ön ayardan sonra uygulanan ayarlama seçenekleri. Varsayılan değer sıfır gecikmedir.", "sw_tune_fastdecode": "fastdecode -- belirli filtreleri devre dışı bırakarak daha hızlı kod çözme sağlar", "sw_tune_film": "film -- yüksek kaliteli film içeriği için kullanın; deblocking'i azaltır", "sw_tune_grain": "gren -- eski, grenli film malzemesinde gren yapısını korur", "sw_tune_stillimage": "stillimage -- slayt gösterisi benzeri içerik için iyi", "sw_tune_zerolatency": "zerolatency -- hızlı kodlama ve düşük gecikmeli akış için iyidir (varsayılan)", "touchpad_as_ds4": "İstemci oyun kumandası bir dokunmatik yüzey olduğunu bildirirse bir DS4 oyun kumandasını taklit edin", "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.", "upnp": "UPnP", "upnp_desc": "İnternet üzerinden akış için port yönlendirmeyi otomatik olarak yapılandırın", "vaapi_strict_rc_buffer": "AMD GPU'larda H.264/HEVC için kare bit hızı sınırlarını kesin olarak uygulayın", "vaapi_strict_rc_buffer_desc": "Bu seçeneğin etkinleştirilmesi, sahne değişiklikleri sırasında ağ üzerinden karelerin düşmesini önleyebilir, ancak hareket sırasında video kalitesi düşebilir.", "virtual_sink": "Sanal Lavabo", "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!", "virtual_sink_placeholder": "Steam Akış Hoparlörleri", "vt_coder": "VideoToolbox Kodlayıcı", "vt_realtime": "VideoToolbox Gerçek Zamanlı Kodlama", "vt_software": "VideoToolbox Yazılım Kodlaması", "vt_software_allowed": "İzin verildi", "vt_software_forced": "Zorla", "wan_encryption_mode": "WAN Şifreleme Modu", "wan_encryption_mode_1": "Desteklenen istemciler için etkin (varsayılan)", "wan_encryption_mode_2": "Tüm müşteriler için gereklidir", "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." }, "index": { "description": "Apollo, Moonlight için kendi kendine barındırılan bir oyun akışı sunucusudur.", "download": "İndir", "installed_version_not_stable": "Apollo'ı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. Apollo'ın daha iyi bir yazılım olmasına yardımcı olduğunuz için teşekkür ederiz!", "loading_latest": "Son sürüm yükleniyor...", "new_pre_release": "Yeni Bir Yayın Öncesi Sürüm Mevcut!", "new_stable": "Yeni bir Kararlı Sürüm Kullanılabilir!", "startup_errors": "Dikkat! Apollo başlatma sırasında bu hataları tespit etti. Yayınlamadan önce bunları düzeltmenizi ŞİDDETLE TAVSİYE EDERİZ.", "version_dirty": "Apollo'ı daha iyi bir yazılım haline getirmeye yardımcı olduğunuz için teşekkür ederiz!", "version_latest": "Apollo'ın en son sürümünü çalıştırıyorsunuz", "welcome": "Merhaba, Günışığı!" }, "navbar": { "applications": "Uygulamalar", "configuration": "Konfigürasyon", "home": "Ana Sayfa", "password": "Şifre Değiştir", "pin": "Pin", "theme_auto": "Otomatik", "theme_dark": "Karanlık", "theme_light": "Açık", "toggle_theme": "Tema", "troubleshoot": "Sorun Giderme" }, "password": { "confirm_password": "Şifreyi Onayla", "current_creds": "Güncel Kimlik Bilgileri", "new_creds": "Yeni Kimlik Bilgileri", "new_username_desc": "Belirtilmezse, kullanıcı adı değişmez", "password_change": "Şifre Değişikliği", "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." }, "pin": { "device_name": "Cihaz Adı", "pair_failure": "Eşleştirme Başarısız: PIN'in doğru yazılıp yazılmadığını kontrol edin", "pair_success": "Başarılı! Devam etmek için lütfen Moonlight'ı kontrol edin", "pin_pairing": "PIN Eşleştirme", "send": "Gönder", "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!" }, "resource_card": { "github_discussions": "GitHub Tartışmaları", "legal": "Yasal", "legal_desc": "Bu yazılımı kullanmaya devam ederek aşağıdaki belgelerde yer alan hüküm ve koşulları kabul etmiş olursunuz.", "license": "Lisans", "lizardbyte_website": "LizardByte Web Sitesi", "resources": "Kaynaklar", "resources_desc": "Günışığı için Kaynaklar!", "third_party_notice": "Üçüncü Taraf Bildirimi" }, "troubleshooting": { "dd_reset": "Kalıcı Ekran Aygıtı Ayarlarını Sıfırla", "dd_reset_desc": "Apollo değiştirilen görüntüleme cihazı ayarlarını geri yüklemeye çalışırken takılırsa, ayarları sıfırlayabilir ve ekran durumunu manuel olarak geri yüklemeye devam edebilirsiniz.", "dd_reset_error": "Kalıcılık sıfırlanırken hata oluştu!", "dd_reset_success": "Kalıcılığı sıfırlayan başarı!", "force_close": "Kapatmaya Zorla", "force_close_desc": "Moonlight çalışmakta olan bir uygulama hakkında şikayet ederse, uygulamayı kapatmaya zorlamak sorunu çözecektir.", "force_close_error": "Uygulama kapatılırken hata oluştu", "force_close_success": "Başvuru Başarıyla Sonuçlandı!", "logs": "Günlükler", "logs_desc": "Apollo tarafından yüklenen günlüklere bakın", "logs_find": "Bul...", "restart_apollo": "Günışığını Yeniden Başlat", "restart_apollo_desc": "Apollo düzgün çalışmıyorsa, yeniden başlatmayı deneyebilirsiniz. Bu, çalışan tüm oturumları sonlandıracaktır.", "restart_apollo_success": "Günışığı yeniden başlıyor", "troubleshooting": "Sorun Giderme", "unpair_all": "Tümünü Eşleşmeleri Kaldır", "unpair_all_error": "Eşleştirme kaldırılırken hata oluştu", "unpair_all_success": "Tüm cihazlar eşleştirilmemiş.", "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.", "unpair_single_no_devices": "Eşleştirilmiş cihaz yok.", "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.", "unpair_single_unknown": "Bilinmeyen Müşteri", "unpair_title": "Cihazların Eşleşmesini Kaldırma" }, "welcome": { "confirm_password": "Şifreyi onayla", "create_creds": "Başlamadan önce, Web UI'ye erişmek için yeni bir kullanıcı adı ve şifre oluşturmanız gerekmektedir.", "create_creds_alert": "Apollo'ı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!", "greeting": "Apollo'a hoş geldiniz!", "login": "Giriş", "welcome_success": "Bu sayfa yakında yeniden yüklenecek, tarayıcınız sizden yeni kimlik bilgilerinizi isteyecek" } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/uk.json ================================================ { "_common": { "apply": "Застосувати", "auto": "Автоматично", "autodetect": "Автовизначення (рекомендовано)", "beta": "(бета-версія)", "cancel": "Скасувати", "disabled": "Вимкнено", "disabled_def": "Вимкнено (за замовчуванням)", "disabled_def_cbox": "За замовчуванням: вимкнено", "dismiss": "Відхилити", "do_cmd": "Виконати команду", "elevated": "Потребуються", "enabled": "Увімкнено", "enabled_def": "Увімкнено (за замовчуванням)", "enabled_def_cbox": "За замовчуванням: вибрано", "error": "Помилка!", "note": "Примітка:", "password": "Пароль", "run_as": "Запустити від імені адміністратора", "save": "Зберегти", "see_more": "Дивитися більше", "success": "Успішно!", "undo_cmd": "Скасувати команду", "username": "Ім'я користувача", "warning": "Попередження!" }, "apps": { "actions": "Дії", "add_cmds": "Додати команди", "add_new": "Додати новий", "app_name": "Назва програми", "app_name_desc": "Назва програми, як показано в Moonlight", "applications_desc": "Програми оновлюються лише після перезапуску Клієнта", "applications_title": "Програми", "auto_detach": "Продовжити стримінг, якщо програма швидко завершується", "auto_detach_desc": "Ц зробить спробу автоматично виявити програми типу launcher, які швидко закриваються після запуску або запуску іншої програми. Якщо буде виявлено програму типу launcher, її буде розпізнано як окрему програму.", "cmd": "Команда", "cmd_desc": "Основна програма для запуску. Якщо поле порожнє, жодна програма не буде запущена.", "cmd_note": "Якщо шлях до виконуваного файлу команди містить пробіли, ви повинні взяти його в лапки.", "cmd_prep_desc": "Список команд, які потрібно запустити до/після цього додатка. Якщо будь-яка з підготовчих команд не спрацює, запуск програми буде перервано.", "cmd_prep_name": "Підготовчі Команди", "covers_found": "Обкладинки знайдено", "delete": "Видалити", "detached_cmds": "Відокремлені команди", "detached_cmds_add": "Додати окрему команду", "detached_cmds_desc": "Список команд для запуску у фоновому режимі.", "detached_cmds_note": "Якщо шлях до виконуваного файлу команди містить пробіли, ви повинні взяти його в лапки.", "edit": "Редагувати", "env_app_id": "ID застосунку", "env_app_name": "Назва програми", "env_client_audio_config": "Конфігурація аудіо, яку запитує клієнт (2.0/5.1/7.1)", "env_client_enable_sops": "Клієнт запросив опцію для оптимізації гри та оптимальної якості стримінгу (так/ні)", "env_client_fps": "FPS, який запитує клієнт (float)", "env_client_gcmap": "Запитувана маска геймпада у форматі бітового набору/бітового поля (ціле число)", "env_client_hdr": "HDR увімкнено клієнтом (так/ні)", "env_client_height": "Висота, яку запитує клієнт (ціле число)", "env_client_host_audio": "Клієнт запросив аудіо хоста (так/ні)", "env_client_width": "Ширина, яку запитує клієнт (int)", "env_displayplacer_example": "Приклад - displayplacer для Автоматизації Роздільної Здатності:", "env_qres_example": "Приклад - QR-коди для Автоматизації Роздільної Здатності:", "env_qres_path": "qres шлях", "env_var_name": "Назва Змінної Середовища", "env_vars_about": "Про Змінні Середовища", "env_vars_desc": "Всі команди отримують ці Змінні Середовища за замовчуванням:", "env_xrandr_example": "Приклад - Xrandr для Автоматизації Роздільної Здатності:", "exit_timeout": "Тайм-аут виходу", "exit_timeout_desc": "Кількість секунд, протягом яких всі процеси програми будуть примусово завершені після запиту на вихід. Якщо значення не встановлено, за замовчуванням програма буде чекати до 5 секунд. Якщо встановлено на нуль або від'ємне значення, програму буде негайно завершено.", "find_cover": "Знайти обкладинку", "global_prep_desc": "Ввімкнути/Вимкнути виконання глобальних команд Prep для цього застосунку.", "global_prep_name": "Глобальні команди підготовки", "image": "Зображення", "image_desc": "Іконка програми/зображення/шлях до зображення, яке буде надіслано клієнту. Зображення має бути у форматі PNG. Якщо не вказано, Apollo надішле зображення за замовчуванням.", "loading": "Завантаження...", "name": "Ім'я", "output_desc": "Файл, в якому зберігається вивід команди, якщо його не вказано, то вивід ігнорується", "output_name": "Виведення", "run_as_desc": "Це може знадобитися для деяких програм, які потребують дозволів адміністратора для нормального функціонування.", "wait_all": "Продовжуйте стримінг доти, доки всі процеси програми не завершаться", "wait_all_desc": "Це продовжить стримінг доти, доки не завершаться всі процеси, запущені програмою. Якщо цей прапорець не ввімкнений, стримінг припиниться після закриття початкової програми, навіть якщо інші процеси програми все ще запущено.", "working_dir": "Робочий каталог", "working_dir_desc": "Робочий каталог, який переданий процесу. Наприклад, деякі програми використовують робочий каталог для пошуку файлів конфігурації. Якщо цей параметр не встановлено, Apollo за замовчуванням буде використовувати батьківський каталог команди" }, "config": { "adapter_name": "Назва Адаптера", "adapter_name_desc_linux_1": "Вкажіть вручну GPU для захоплення.", "adapter_name_desc_linux_2": "знайти всі пристрої з підтримкою VAAPI", "adapter_name_desc_linux_3": "Замініть ``renderD129`` на пристрій зверху, щоб вивести назву та можливості пристрою. Для підтримки Apollo пристрій повинен мати як мінімум такі параметри:", "adapter_name_desc_windows": "Вручну вкажіть GPU для захоплення. Якщо GPU не встановлений вручну, то його буде обрано автоматично. Ми наполегливо рекомендуємо залишити це поле порожнім, щоб використовувати автоматичний вибір GPU! Зауважимо, що цей GPU повинен бути ввімкнутим та під'\nєднаним. Допустимі значення можна знайти за допомогою наступної команди:", "adapter_name_placeholder_windows": "Radeon RX 580 Серії", "add": "Додати", "address_family": "Сімейство Адрес", "address_family_both": "IPv4+IPv6", "address_family_desc": "Встановити сімейство адрес, що використовується в Apollo", "address_family_ipv4": "Тільки IPv4", "always_send_scancodes": "Завжди Надсилати Скан-коди", "always_send_scancodes_desc": "Надсилання скан-кодів покращує сумісність з іграми та програмами, але може призвести до некоректного введення з клавіатури деякими клієнтами, які не використовують розкладку клавіатури США. Увімкніть, якщо введення з клавіатури взагалі не працює у певних програмах. Вимкніть, якщо клавіші на клієнті генерують неправильні клавіші на хості.", "amd_coder": "AMF Coder (H264)", "amd_coder_desc": "Дозволяє вибрати ентропійне кодування, щоб надати пріоритет якості або швидкості кодування. Тільки H.264.", "amd_enforce_hrd": "Примусове застосування AMF Hypothetical Reference Decoder (HRD)", "amd_enforce_hrd_desc": "Збільшує обмеження на керування швидкістю, щоб відповідати вимогам моделі HRD. Це значно зменшує переповнення бітрейту, але може спричинити артефакти кодування або зниження якості на певних GPU.", "amd_preanalysis": "Попередній аналіз AMF", "amd_preanalysis_desc": "Це вмикає попередній аналіз контролю швидкості, що може підвищити якість шляхом збільшення затримки кодування.", "amd_quality": "Якість AMF", "amd_quality_balanced": "balanced -- збалансований (за замовчуванням)", "amd_quality_desc": "Це дозволяє контролювати баланс між швидкістю та якістю кодування.", "amd_quality_group": "Налаштування якості AMF", "amd_quality_quality": "якість - пріоритизувати якість", "amd_quality_speed": "швидкість - пріоритизувати швидкість", "amd_rc": "Контроль Швидкості AMF", "amd_rc_cbr": "cbr -- постійний бітрейт (рекомендується, якщо увімкнено HRD)", "amd_rc_cqp": "cqp -- постійний режим qp", "amd_rc_desc": "Цей параметр контролює метод керування швидкістю, щоб переконатися, що ми не перевищуємо цільовий бітрейт клієнта. 'cqp' не підходить для визначення бітрейту, а інші параметри, окрім 'vbr_latency', залежать від Примусового HRD, щоб допомогти уникнути перезаповнення бітрейту.", "amd_rc_group": "Налаштування контролю швидкості AMF", "amd_rc_vbr_latency": "vbr_latency -- обмеженням затримки змінного бітрейту (рекомендується, якщо HRD вимкнено; за замовчуванням)", "amd_rc_vbr_peak": "vbr_peak -- пікове обмеження змінного бітрейту", "amd_usage": "Використання AMF", "amd_usage_desc": "Тут встановлюється базовий профіль кодування. Усі параметри, наведені нижче, перевизначають підмножину профілю використання, але також застосовуються додаткові приховані налаштування, які не можна налаштувати деінде, окрім як тут.", "amd_usage_lowlatency": "lowlatency - з низькою затримкою (найшвидший)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - низька затримка, висока якість (швидкий)", "amd_usage_transcoding": "transcoding -- перекодування (найповільніше)", "amd_usage_ultralowlatency": "ultralowlatency - наднизька затримка (найшвидша; за замовчуванням)", "amd_usage_webcam": "веб-камера -- веб-камера (повільно)", "amd_vbaq": "Адаптивне квантування на основі дисперсії AMF (VBAQ)", "amd_vbaq_desc": "Людський зір зазвичай менш чутливий до артефактів у високотекстурованих областях. У режимі VBAQ дисперсія пікселів використовується для позначення складності просторових текстур, що дозволяє кодеру виділяти більше бітів для більш гладких ділянок. Увімкнення цієї функції призводить до покращення суб'єктивної візуальної якості певного контенту.", "apply_note": "Натисніть \"Застосувати\", щоб перезапустити Apollo і застосувати зміни. Це призведе до завершення всіх запущених сеансів.", "audio_sink": "Пристрій виведення аудіо", "audio_sink_desc_linux": "Назва пристрою аудіовиводу, що використовується для зациклення звуку. Якщо ви не вкажете цю змінну, pulseaudio вибере пристрій за замовчуванням. Ви можете дізнатися назву пристрою аудіовиводу за допомогою будь-якої з команд:", "audio_sink_desc_macos": "Назва пристрою аудіовиводу, що використовується для зациклення звуку (Loopback). Apollo може отримати доступ до мікрофонів лише на macOS через системні обмеження. Для трансляції системного аудіо за допомогою Soundflower або BlackHole.", "audio_sink_desc_windows": "Вручну вкажіть конкретний аудіопристрій для захоплення. Якщо не вказано, пристрій буде обрано автоматично. Ми наполегливо рекомендуємо залишити це поле порожнім, щоб використовувати автоматичний вибір пристрою! Якщо у вас є кілька аудіопристроїв з однаковими іменами, ви можете отримати ідентифікатор пристрою за допомогою наступної команди:", "audio_sink_placeholder_macos": "BlackHole 2ch", "audio_sink_placeholder_windows": "Динаміки (High Definition аудіопристрої)", "av1_mode": "Підтримка AV1", "av1_mode_0": "Apollo пропонуватиме підтримку AV1 на основі можливостей кодерів (рекомендовано)", "av1_mode_1": "Apollo не буде пропонувати підтримку AV1", "av1_mode_2": "Apollo пропонуватиме підтримку 8-бітового профілю AV1 Main", "av1_mode_3": "Apollo пропонуватиме підтримку профілів AV1 Main 8-біт і 10-біт (HDR)", "av1_mode_desc": "Дозволяє клієнту запитувати основні 8-бітні або 10-бітні відеопотоки AV1. Кодування AV1 вимагає більше ресурсів CPU, тому увімкнення цієї опції може знизити продуктивність при використанні програмного кодування.", "back_button_timeout": "Тайм-аут емуляції Home/Guide кнопок керування", "back_button_timeout_desc": "Якщо утримувати кнопки Back/Select протягом вказаної кількості мілісекунд, імітується натискання кнопки Home/Guide. Якщо встановлено значення < 0 (за замовчуванням), утримання кнопки Back/Select не імітуватиме натискання кнопок Home/Guide.", "capture": "Примусове застосування конкретного методу захоплення (Capture)", "capture_desc": "У автоматичному режимі Apollo використовуватиме перший-ліпший драйвер. Для роботи NvFBC потрібні пропатчені драйвери nVidia.", "cert": "Сертифікат", "cert_desc": "Сертифікат, який використовується для створення пари між веб UI й клієнтом Moonlight. Для найкращої сумісності він повинен мати відкритий ключ RSA-2048.", "channels": "Максимальна кількість підключених клієнтів", "channels_desc_1": "Apollo може дозволити спільний доступ до однієї стримінгової сесії кільком клієнтам одночасно.", "channels_desc_2": "Деякі апаратні кодери можуть мати обмеження, які знижують продуктивність при роботі з декількома потоками.", "coder_cabac": "cabac -- контекстно-адаптивне двійкове арифметичне кодування - вища якість", "coder_cavlc": "cavlc -- контекстно-адаптивне кодування змінної довжини - швидке декодування", "configuration": "Налаштування", "controller": "Увімкнути введення з геймпада", "controller_desc": "Дозволяє гостям керувати хост-системою за допомогою геймпада / контролера", "credentials_file": "Файл облікових даних", "credentials_file_desc": "Зберігайте ім'я користувача/пароль окремо від файлу стану Apollo.", "dd_config_ensure_active": "Активувати автовідтворення дисплея", "dd_config_ensure_only_display": "Вимкнути інші дисплеї та активувати тільки зазначений дисплей", "dd_config_ensure_primary": "Автоматично активувати дисплей та зробити його основним екраном", "dd_config_label": "Конфігурація пристрою", "dd_config_revert_delay": "Затримка відновлення конфігурації", "dd_config_revert_delay_desc": "Додаткова затримка в мілісекундах до очікування перед тим, як буде скасовано конфігурацію при закритті програми або припиненні останньої сесії. Головна мета - забезпечити більш плавне перемикання при швидкому перемиканні між додатками.", "dd_config_verify_only": "Переконайтеся, що дисплей увімкнений", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "Увімкнути / вимкнути режим HDR як запит клієнтом (за замовчуванням)", "dd_hdr_option_disabled": "Не змінювати налаштування HDR", "dd_mode_remapping": "Переказ режиму екрану", "dd_mode_remapping_add": "Додати пункт перерахування", "dd_mode_remapping_desc_1": "Вкажіть записи, щоб змінити потрібну роздільну здатність і/або оновити ставку до інших значень.", "dd_mode_remapping_desc_2": "Список повторюється з верху до низу і використовується перший матч.", "dd_mode_remapping_desc_3": "Поля \"Запитовано\" можуть бути порожніми, щоб відповідати жодному запитуванню.", "dd_mode_remapping_desc_4_final_values_mixed": "Необхідно вказати принаймні одне поле \"Final\". Невизначена роздільна здатність або оновлення не змінюється.", "dd_mode_remapping_desc_4_final_values_non_mixed": "Потрібно вказати поле \"Final\" і не може бути порожнім.", "dd_mode_remapping_desc_5_sops_mixed_only": "Опція \"Оптимізація налаштувань гри\" повинна бути включена в клієнті Moonlight, в іншому випадку записи з іншими полями з будь-якою роздільною здатністю пропущені.", "dd_mode_remapping_desc_5_sops_resolution_only": "Опція \"Оптимізація налаштувань гри\" повинна бути увімкнена в клієнті місячного світла, інакше натискання пропущене.", "dd_mode_remapping_final_refresh_rate": "Фінальна ставка оновлення", "dd_mode_remapping_final_resolution": "Остаточна роздільна здатність", "dd_mode_remapping_requested_fps": "Запитаний FPS", "dd_mode_remapping_requested_resolution": "Запитана роздільна здатність", "dd_options_header": "Додаткові налаштування пристрою", "dd_refresh_rate_option": "Оновити курс", "dd_refresh_rate_option_auto": "Використовувати значення FPS наданих клієнтом (за замовчуванням)", "dd_refresh_rate_option_disabled": "Не змінюйте частоту оновлення", "dd_refresh_rate_option_manual": "Використовувати введений вручну курс оновлення", "dd_refresh_rate_option_manual_desc": "Введіть швидкість оновлення, яку слід використовувати", "dd_resolution_option": "Роздільна здатність", "dd_resolution_option_auto": "Використовувати роздільну здатність, що надається клієнтом (за замовчуванням)", "dd_resolution_option_disabled": "Не змінювати роздільну здатність", "dd_resolution_option_manual": "Використовувати введену вручну", "dd_resolution_option_manual_desc": "Введіть роздільну здатність для використання", "dd_resolution_option_ogs_desc": "Опція \"Оптимізація налаштувань гри\" повинна бути увімкнена на віддаленому клієнті, щоб це спрацювало.", "dd_wa_hdr_toggle_desc": "При використанні віртуального відображення пристрою для стрімінгу він може відображати неправильний HDR колір. Якщо цей параметр увімкнуто, сонячний екран намагатиметься пом'якшити цю проблему.", "dd_wa_hdr_toggle": "Увімкнути висококонтрастний режим для HDR", "ds4_back_as_touchpad_click": "Призначити клавіші Back/Select на сенсорну клавіатуру", "ds4_back_as_touchpad_click_desc": "При включеній примусовій емуляції DS4, налаштуйте Back/Select на клацання touchpad'а", "encoder": "Примусове використання певного кодера", "encoder_desc": "Примусово використовуйте конкретний кодер, інакше Apollo обере найкращий з доступних варіантів. Примітка: Якщо ви вказуєте апаратний кодер у Windows, він має відповідати графічному процесору, до якого під'єднано монітор.", "encoder_software": "Програмне забезпечення", "external_ip": "Зовнішня IP-адреса", "external_ip_desc": "Якщо зовнішню IP-адресу не вказано, Apollo автоматично визначить зовнішню IP-адресу", "fec_percentage": "Відсоток FEC", "fec_percentage_desc": "Відсоток пакетів виправлення помилок на кожен пакет даних у кожному відеокадрі. Вищі значення можуть викликати більшу втрату мережевих пакетів, але використовувати збільшену пропускну здатність.", "ffmpeg_auto": "auto -- дозволити ffmpeg вирішувати (за замовчуванням)", "file_apps": "Файли програми", "file_apps_desc": "Файл, у якому зберігаються поточні програми Apollo.", "file_state": "Файл стану", "file_state_desc": "Файл, у якому зберігається поточний стан Apollo", "gamepad": "Тип емульованого геймпаду", "gamepad_auto": "Параметри автоматичного вибору", "gamepad_desc": "Виберіть тип геймпаду для емуляції на хості", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "Опції DS4", "gamepad_ds5": "DS5 (PS5)", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Налаштування DS4 вручну", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Підготовчі Команди", "global_prep_cmd_desc": "Налаштуйте список команд, які потрібно виконати до або після запуску будь-якої програми. Якщо будь-яка із зазначених команд підготовки не спрацює, процес запуску програми буде перервано.", "hevc_mode": "Підтримка HEVC", "hevc_mode_0": "Apollo буде рекламувати підтримку HEVC на основі можливостей кодера (рекомендовано)", "hevc_mode_1": "Apollo не буде пропонувати підтримку HEVC", "hevc_mode_2": "Apollo пропонуватиме підтримку для Основного HEVC профілю", "hevc_mode_3": "Apollo пропонуватиме підтримку профілів HEVC Main та Main10 (HDR)", "hevc_mode_desc": "Дозволяє клієнту запитувати відеопотоки HEVC Main або HEVC Main10. Кодування HEVC вимагає більше ресурсів CPU, тому увімкнення цієї опції може знизити продуктивність при використанні програмного кодування.", "high_resolution_scrolling": "Підтримка прокрутки з високою роздільною здатністю", "high_resolution_scrolling_desc": "Якщо увімкнено, Apollo пропускатиме події прокрутки з високою роздільною здатністю від клієнтів Moonlight. Вимкнення цього може бути корисним для старих програм, які прокручують вміст занадто швидко за допомогою подій прокрутки у високій роздільній здатності.", "install_steam_audio_drivers": "Встановити звукові Steam драйвери", "install_steam_audio_drivers_desc": "Якщо Steam інстальовано, це автоматично інсталює драйвер Steam Streaming Speakers для підтримки об'ємного звуку 5.1/7.1 і вимкнення звуку хост-комп'ютера.", "key_repeat_delay": "Затримка повтору клавіш", "key_repeat_delay_desc": "Керування швидкістю повторення клавіш. Початкова затримка у мілісекундах перед повторенням натискання.", "key_repeat_frequency": "Частота повторення клавіш", "key_repeat_frequency_desc": "Як часто клавіші повторюються щосекунди. Цей параметр підтримує десяткові числа.", "key_rightalt_to_key_win": "Клавіша Alt Map праворуч від ключа Windows", "key_rightalt_to_key_win_desc": "Може статися так, що ви не зможете надіслати команду клавіші Windows з Moonshine напряму. У таких випадках може бути корисним змусити Apollo вважати клавішу Alt праворуч клавішею Windows", "keyboard": "Увімкнути введення з клавіатури", "keyboard_desc": "Дозволити гостям керувати хост-системою за допомогою клавіатури", "lan_encryption_mode": "Режим шифрування LAN мережі", "lan_encryption_mode_1": "Увімкнено для підтримуваних клієнтів", "lan_encryption_mode_2": "Обов'язкове для всіх клієнтів", "lan_encryption_mode_desc": "Цей параметр визначає, коли буде використовуватися шифрування під час потокового передавання через локальну мережу. Шифрування може знизити продуктивність потокового передавання, особливо на менш потужних хостах і клієнтах.", "locale": "Мова", "locale_desc": "Мова, що використовується для Apollo UI.", "log_level": "Рівень Логування", "log_level_0": "Детально (Verbose)", "log_level_1": "Режим налагодження (Debug)", "log_level_2": "Інфо", "log_level_3": "Попередження", "log_level_4": "Помилка", "log_level_5": "Критична помилка", "log_level_6": "Нічого", "log_level_desc": "Мінімальний стандартний рівень логування, що виводиться", "log_path": "Шлях до лог-файлу", "log_path_desc": "Файл, у якому зберігаються поточні логи Apollo.", "min_fps_factor": "Мінімальний FPS Фактор", "min_fps_factor_desc": "Apollo використовуватиме цей фактор для обчислення мінімального часу між кадрами. Невелике збільшення цього значення може допомогти при стримінгу переважно статичного контенту. Вищі значення будуть більш негативно впливати на пропускну здатність.", "min_threads": "Мінімальна кількість потоків CPU", "min_threads_desc": "Збільшення значення дещо знижує ефективність кодування, але цей компроміс зазвичай вартий того, щоб отримати можливість використовувати більше ядер CPU для кодування. Ідеальне значення - це найменше значення, яке може надійно кодувати за бажаних налаштувань стримінгу на вашому обладнанні.", "misc": "Інші параметри", "motion_as_ds4": "Емулювати геймпад DS4, якщо клієнтський геймпад повідомляє про наявність motion датчиків", "motion_as_ds4_desc": "Якщо вимкнено, motion датчики не враховуватимуться під час вибору типу геймпада.", "mouse": "Увімкнути введення за допомогою миші", "mouse_desc": "Дозволяє гостям керувати хост-системою за допомогою миші", "native_pen_touch": "Вбудована підтримка пера/сенсорного вводу", "native_pen_touch_desc": "Якщо увімкнено, Apollo передаватиме події нативного пера/дотику від клієнтів Moonlight. Для старих програм без підтримки нативного пера/дотику може бути корисним вимкнення цього налаштування.", "notify_pre_releases": "PreRelease Сповіщення", "notify_pre_releases_desc": "Чи отримувати сповіщення про нові pre-release версії Apollo", "nvenc_h264_cavlc": "Надайте перевагу CAVLC над CABAC в H.264", "nvenc_h264_cavlc_desc": "Простіша форма ентропійного кодування. CAVLC потребує приблизно на 10% більшого бітрейту для такої ж якості. Актуально лише для дуже старих декодерів.", "nvenc_latency_over_power": "Надайте перевагу меншій затримці кодування над економією енергії", "nvenc_latency_over_power_desc": "Apollo запитує максимальну тактову частоту CPU під час стримінгу, щоб зменшити затримку кодування. Вимкнення цього параметра не рекомендується, оскільки це може призвести до значного збільшення затримки кодування.", "nvenc_opengl_vulkan_on_dxgi": "Відображати OpenGL/Vulkan поверх DXGI", "nvenc_opengl_vulkan_on_dxgi_desc": "Apollo не може захоплювати повноекранні програми OpenGL та Vulkan з повною частотою кадрів, якщо вони не присутні поверх DXGI. Це загальносистемне налаштування, яке повертається до початкового стану після завершення роботи програми.", "nvenc_preset": "Пресет продуктивності", "nvenc_preset_1": "(найшвидший, за замовчуванням)", "nvenc_preset_7": "(найповільніше)", "nvenc_preset_desc": "Більші значення покращують стиснення (якість при заданому бітрейті) ціною збільшення затримки кодування. Рекомендується змінювати тільки тоді, коли це обмежено мережею або декодером, інакше аналогічного ефекту можна досягти збільшенням бітрейту.", "nvenc_realtime_hags": "Використання пріоритету реального часу у плануванні GPU з апаратним прискоренням", "nvenc_realtime_hags_desc": "Наразі драйвери NVIDIA можуть зависати в кодері, коли ввімкнено HAGS, використовується пріоритет реального часу та завантаження VRAM близьке до максимального. Вимкнення цієї опції знижує пріоритет до високого, що дозволяє уникнути зависання ціною зниження продуктивності захоплення при високому навантаженні GPU.", "nvenc_spatial_aq": "Просторове AQ", "nvenc_spatial_aq_desc": "Призначає вищі значення QP пласким ділянкам відео. Рекомендується вмикати під час стримінгу з низьким бітрейтом.", "nvenc_spatial_aq_disabled": "Вимкнено (швидше, за замовчуванням)", "nvenc_spatial_aq_enabled": "Увімкнено (повільніше)", "nvenc_twopass": "Режим двоетапної перевірки", "nvenc_twopass_desc": "Додає попередній прохід кодування. Це дозволяє виявити більше векторів руху, краще розподілити бітрейт у кадрі та суворіше дотримуватися лімітів бітрейту. Вимикати його не рекомендується, оскільки це може призвести до випадкового перевищення бітрейту і подальшої втрати пакетів.", "nvenc_twopass_disabled": "Вимкнено (найшвидше, не рекомендується)", "nvenc_twopass_full_res": "Повна роздільна здатність (повільніше)", "nvenc_twopass_quarter_res": "Чверть роздільної здатності (швидше, за замовчуванням)", "nvenc_vbv_increase": "Збільшення однокадрового VBV/HRD у відсотках", "nvenc_vbv_increase_desc": "За замовчуванням Apollo використовує однокадровий VBV/HRD, що означає, що розмір будь-якого закодованого відеокадру не повинен перевищувати запитуваного бітрейту, поділеного на запитувану частоту кадрів. Послаблення цього обмеження може бути корисним і діяти як змінний бітрейт з низькою затримкою, але також може призвести до втрати пакетів, якщо мережа не має достатньої ємності буфера, щоб впоратися зі стрибками бітрейту. Максимально допустиме значення 400, що відповідає 5-кратному збільшенню верхньої межі розміру кодованого відеокадру.", "origin_web_ui_allowed": "Origin Web UI Дозволено", "origin_web_ui_allowed_desc": "Походження адреси endpoint'а, якій не заборонено доступ до Web UI", "origin_web_ui_allowed_lan": "Доступ до Web UI мають лише ті, хто перебуває в LAN мережі", "origin_web_ui_allowed_pc": "Доступ до Web UI може мати лише localhost", "origin_web_ui_allowed_wan": "Будь-хто може отримати доступ до Web UI", "output_name_desc_unix": "Під час запуску Apollo ви повинні побачити список виявлених дисплеїв. Примітка: Ви маєте використовувати значення ідентифікатора у дужках. Нижче наведено приклад; фактичний вивід можна знайти на вкладці Виправлення неполадок.", "output_name_desc_windows": "Вручну вкажіть ідентифікатор пристрою для захоплення. Якщо не вказано, буде захоплено основний екран. Примітка: Якщо вище ви вказали GPU, цей дисплей повинен бути підключений до цього ж GPU. Під час запуску Apollo, ви повинні побачити список виявлених дисплеїв. Нижче наведено приклад, фактичний вивід зображення можна знайти на вкладці Виправлення неполадок.", "output_name_unix": "Номер дисплея", "output_name_windows": "Показувати ID пристрою", "ping_timeout": "Тайм-аут пінгу", "ping_timeout_desc": "Скільки часу в мілісекундах чекати на дані від Moonlight, перш ніж вимкнути стримінг", "pkey": "Приватний ключ", "pkey_desc": "Приватний ключ, який використовується для створення пари між Web UI й клієнтом Moonlight. Для найкращої сумісності це має бути приватний ключ формату RSA-2048.", "port": "Порт", "port_alert_1": "Apollo не може використовувати порти нижче 1024!", "port_alert_2": "Порти вище 65535 недоступні!", "port_desc": "Встановити сімейство портів, що використовуються Apollo", "port_http_port_note": "Використовуйте цей порт для підключення до Moonlight.", "port_note": "Нотатка", "port_port": "Порт", "port_protocol": "Протокол", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "Оприлюднення Web UI в Інтернеті є ризикованим щодо безпеки! Дійте на власний страх і ризик!", "port_web_ui": "Web UI", "qp": "Параметр квантування", "qp_desc": "Деякі пристрої можуть не підтримувати постійну швидкість передачі даних. На таких пристроях замість неї використовується QP. Вище значення означає більше стиснення, але нижчу якість.", "qsv_coder": "QuickSync кодер (H264)", "qsv_preset": "QuickSync Пресет", "qsv_preset_fast": "швидко (низька якість)", "qsv_preset_faster": "швидше (нижча якість)", "qsv_preset_medium": "середнє (за замовчуванням)", "qsv_preset_slow": "повільно (хороша якість)", "qsv_preset_slower": "повільніше (краща якість)", "qsv_preset_slowest": "найповільніше (найкраща якість)", "qsv_preset_veryfast": "найшвидше (найнижча якість)", "qsv_slow_hevc": "Дозволити повільне HEVC кодування", "qsv_slow_hevc_desc": "Це може дозволити кодування HEVC на старих Intel GPU, але внаслідок більшого використання GPU та гіршої продуктивності.", "restart_note": "Apollo перезапускається, щоб застосувати зміни.", "sunshine_name": "Apollo ім'я", "sunshine_name_desc": "Ім'я, яке відображається Moonlight. Якщо не вказано, використовується ім'я хоста комп'ютера", "sw_preset": "Пресети SW", "sw_preset_desc": "Оптимізація компромісу між швидкістю кодування (кількість закодованих кадрів за секунду) та ефективністю стиснення (якість на біт у бітовому потоці). За замовчуванням - супершвидко.", "sw_preset_fast": "швидко", "sw_preset_faster": "швидше", "sw_preset_medium": "середнє", "sw_preset_slow": "повільно", "sw_preset_slower": "повільніше", "sw_preset_superfast": "супершвидкий (за замовчуванням)", "sw_preset_ultrafast": "ультрашвидкий", "sw_preset_veryfast": "дуже швидкий", "sw_preset_veryslow": "дуже повільний", "sw_tune": "ПЗ налаштування", "sw_tune_animation": "анімація - добре підходить для мультфільмів; використовує вищий рівень деблокування та більше кадрів порівняння", "sw_tune_desc": "Параметри налаштування, які застосовуються після пресету. За замовчуванням - нульова латентність.", "sw_tune_fastdecode": "fastdecode -- дозволяє пришвидшити декодування, вимкнувши певні фільтри", "sw_tune_film": "фільм - використовується для високоякісного кіноконтенту; зменшує деблокування", "sw_tune_grain": "зернистість - зберігає зернисту структуру в старих, зернистих плівкових матеріалах", "sw_tune_stillimage": "стоп-кадр - добре підходить для контенту, схожого на слайд-шоу", "sw_tune_zerolatency": "zerolatency - добре підходить для швидкого кодування та стримінгу з низькою затримкою (за замовчуванням)", "touchpad_as_ds4": "Емулювати геймпад DS4, якщо клієнтський геймпад повідомляє про наявність touchpad'а", "touchpad_as_ds4_desc": "Якщо вимкнено, наявність touchpad не враховуватиметься під час вибору типу геймпада.", "upnp": "UPnP", "upnp_desc": "Автоматичне налаштування переадресації портів для стримінгу через Інтернет", "vaapi_strict_rc_buffer": "Суворе дотримання обмежень на бітрейт кадру для H.264/HEVC на GPU від AMD", "vaapi_strict_rc_buffer_desc": "Увімкнення цієї опції дозволяє уникнути втрати кадрів у мережі під час зміни сцени, але під час руху якість відео може погіршитися.", "virtual_sink": "Віртуальний пристрій виведення аудіо", "virtual_sink_desc": "Вручну вкажіть віртуальний аудіопристрій для використання. Якщо не вказано, пристрій буде обрано автоматично. Ми наполегливо рекомендуємо залишити це поле порожнім, щоб використовувати автоматичний вибір пристрою!", "virtual_sink_placeholder": "Динаміки стримінгу Steam", "vt_coder": "VideoToolbox Кодер", "vt_realtime": "Кодування VideoToolbox у реальному часі", "vt_software": "Кодування ПЗ VideoToolbox", "vt_software_allowed": "Дозволено", "vt_software_forced": "Примусово", "wan_encryption_mode": "Режим шифрування WAN", "wan_encryption_mode_1": "Увімкнено для підтримуваних клієнтів (за замовчуванням)", "wan_encryption_mode_2": "Обов'язково для всіх клієнтів", "wan_encryption_mode_desc": "Цей параметр визначає, коли буде використовуватися шифрування під час потокового передавання через Інтернет. Шифрування може знизити продуктивність потокового стримінгу, особливо на менш потужних хостах і клієнтах." }, "index": { "description": "Apollo - це самостійний ігровий стримінговий хостинг для Moonlight.", "download": "Завантажити", "installed_version_not_stable": "Ви використовуєте попередню версію Apollo. Ви можете зіткнутися з помилками або іншими проблемами. Будь ласка, повідомляйте про будь-які проблеми, з якими ви зіткнулися. Дякуємо, що допомагаєте зробити Apollo кращою програмою!", "loading_latest": "Завантаження останньої версії...", "new_pre_release": "Доступна нова Pre-Release версія!", "new_stable": "Доступна нова стабільна версія!", "startup_errors": "Увага! Apollo виявив ці помилки під час запуску. Ми НАПОЛЕГЛИВО РЕКОМЕНДУЄМО виправити їх перед стримінгом.", "version_dirty": "Дякуємо, що допомагаєте зробити Apollo кращою програмою!", "version_latest": "Ви використовуєте останню версію Apollo", "welcome": "Привіт, Apollo!" }, "navbar": { "applications": "Застосунки", "configuration": "Конфігурація", "home": "Головна", "password": "Змінити Пароль", "pin": "Закріпити", "theme_auto": "Авто", "theme_dark": "Темна", "theme_light": "Світла", "toggle_theme": "Тема", "troubleshoot": "Усунення неполадок" }, "password": { "confirm_password": "Підтвердити пароль", "current_creds": "Поточні облікові дані", "new_creds": "Нові облікові дані", "new_username_desc": "Якщо не вказано, ім'я користувача не зміниться", "password_change": "Зміна пароля", "success_msg": "Пароль успішно змінено! Ця сторінка незабаром перезавантажиться, ваш браузер запитає вас про нові облікові дані." }, "pin": { "device_name": "Назва пристрою", "pair_failure": "Не вдалося створити пару: Перевірте правильність введення PIN-коду", "pair_success": "Успішно! Будь ласка, перевірте Moonlight, щоб продовжити", "pin_pairing": "Сполучення PIN-коду", "send": "Надіслати", "warning_msg": "Переконайтеся, що у вас є доступ до клієнта, з яким ви створюєте пару. Це програмне забезпечення може повністю контролювати ваш комп'ютер, тому будьте обережні!" }, "resource_card": { "github_discussions": "Обговорення на GitHub", "legal": "Юридична інформація", "legal_desc": "Продовжуючи використовувати це програмне забезпечення, ви погоджуєтеся з умовами та положеннями, викладеними в наступних документах.", "license": "Ліцензія", "lizardbyte_website": "Вебсайт LizardByte", "resources": "Ресурси", "resources_desc": "Ресурси для Apollo!", "third_party_notice": "Сповіщення третім особам" }, "troubleshooting": { "dd_reset": "Скинути налаштування постійного відображення пристроїв", "dd_reset_desc": "Якщо сонячне світло застрягло, намагається відновити змінені налаштування пристроїв для дисплея, ви можете скинути налаштування і перейти до відновлення стану екрана вручну.", "dd_reset_error": "Помилка під час відновлення наполегливості!", "dd_reset_success": "Успіх відновлення збереження!", "force_close": "Закрити примусово", "force_close_desc": "Якщо Moonlight скаржиться на запущену програму, примусове закриття програми має вирішити проблему.", "force_close_error": "Помилка під час закриття програми", "force_close_success": "Застосунок успішно закрито!", "logs": "Логи", "logs_desc": "Перегляньте логи, завантажені Apollo", "logs_find": "Пошук...", "restart_Apollo": "Перезапустити Apollo", "restart_Apollo_desc": "Якщо Apollo не працює належним чином, ви можете спробувати перезапустити його. Це призведе до завершення усіх запущених сеансів.", "restart_Apollo_success": "Apollo перезапускається", "troubleshooting": "Усунення неполадок", "unpair_all": "Відв'язати всі пари", "unpair_all_error": "Помилка під час від'єднання пари", "unpair_all_success": "Усі пристрої не під'єднані.", "unpair_desc": "Видаліть пов’язані пристрої. Вбудовані пристрої з активною сесією залишаться, але не зможуть почати або відновити сеанс.", "unpair_single_no_devices": "Немає пов'язаних пристроїв.", "unpair_single_success": "Однак пристрій(ої) все ще можуть бути в активному сеансі. Використовуйте кнопку \"Примусове закриття\" вище, щоб завершити будь-які відкриті сеанси.", "unpair_single_unknown": "Невідомий клієнт", "unpair_title": "Відв'язати пристрої" }, "welcome": { "confirm_password": "Підтвердити пароль", "create_creds": "Перед початком роботи нам потрібно, щоб ви створили нове ім'я користувача та пароль для доступу до Web UI.", "create_creds_alert": "Наведені нижче облікові дані необхідні для доступу до Apollo's Web UI. Зберігайте їх у безпеці, оскільки ви більше ніколи їх не побачите!", "greeting": "Ласкаво просимо до Apollo!", "login": "Авторизація", "welcome_success": "Ця сторінка незабаром перезавантажиться, ваш браузер попросить вас ввести нові облікові дані" } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/vi.json ================================================ { "_common": { "apply": "Áp dụng", "auto": "Tự động", "autodetect": "Phát hiện tự động (đề xuất)", "beta": "(phiên bản thử nghiệm)", "cancel": "Hủy", "disabled": "Tắt", "disabled_def": "Tắt (mặc định)", "disabled_def_cbox": "Mặc định: không bật", "dismiss": "Bỏ qua", "do_cmd": "Thực hiện lệnh", "elevated": "Nâng cao", "enabled": "Đã bật", "enabled_def": "Bật (mặc định)", "enabled_def_cbox": "Mặc định: đã bật", "error": "Lỗi!", "note": "Lưu ý:", "password": "Mật khẩu", "run_as": "Chạy với quyền Admin", "save": "Lưu", "see_more": "Xem thêm", "success": "Thành công!", "undo_cmd": "Hủy lệnh", "username": "Tên đăng nhập", "warning": "Cảnh báo!" }, "apps": { "actions": "Hành động", "add_cmds": "Thêm lệnh", "add_new": "Thêm mới", "app_name": "Tên ứng dụng", "app_name_desc": "Tên ứng dụng, hiển thị trên Moonlight", "applications_desc": "Ứng dụng chỉ được làm mới khi Client được khởi động lại.", "applications_title": "Ứng dụng", "auto_detach": "Không ngắt stream nếu ứng dụng thoát trong thời gian ngắn", "auto_detach_desc": "Tùy chọn này sẽ cố gắng tự động nhận diện các ứng dụng dạng launcher, thường thoát ngay sau khi mở một chương trình khác hoặc một phiên bản khác của chính nó.\nKhi phát hiện launcher như vậy, hệ thống sẽ xử lý nó như một ứng dụng tách biệt để tránh ngắt kết nối stream.", "cmd": "Lệnh", "cmd_desc": "Ứng dụng chính để khởi động. Nếu để trống, sẽ không khởi động ứng dụng nào.", "cmd_note": "Nếu đường dẫn đến tệp thực thi lệnh chứa khoảng trắng (space), bạn phải đặt nó trong dấu ngoặc kép.", "cmd_prep_desc": "Danh sách các lệnh sẽ được chạy trước hoặc sau ứng dụng này. Nếu bất kỳ lệnh chuẩn bị nào bị lỗi, quá trình khởi chạy ứng dụng sẽ bị hủy.", "cmd_prep_name": "Chuẩn bị lệnh", "covers_found": "Bìa đã tìm thấy", "delete": "Xóa", "detached_cmds": "Lệnh độc lập", "detached_cmds_add": "Thêm lệnh tách rời", "detached_cmds_desc": "Danh sách các lệnh cần chạy ở chế độ nền.", "detached_cmds_note": "Nếu đường dẫn đến tệp thực thi lệnh chứa khoảng trắng (space), bạn phải đặt nó trong dấu ngoặc kép.", "edit": "Chỉnh sửa", "env_app_id": "ID ứng dụng", "env_app_name": "Tên ứng dụng", "env_client_audio_config": "Cấu hình âm thanh được yêu cầu bởi client (2.0/5.1/7.1)", "env_client_enable_sops": "Client yêu cầu tùy chọn tối ưu hóa trò chơi cho việc streaming tối ưu (có/không)", "env_client_fps": "Tốc độ khung hình mỗi giây (FPS) mà client yêu cầu (số nguyên)", "env_client_gcmap": "The requested gamepad mask, in a bitset/bitfield format (int)", "env_client_hdr": "HDR được kích hoạt bởi client (true/false)", "env_client_height": "Chiều cao do client yêu cầu (số nguyên)", "env_client_host_audio": "Client yêu cầu âm thanh từ host (có/không)", "env_client_width": "Chiều rộng được yêu cầu bởi client (số nguyên)", "env_displayplacer_example": "Ví dụ - Công cụ hiển thị cho độ phân giải động:", "env_qres_example": "Ví dụ - QRes cho độ phân giải động:", "env_qres_path": "Đường dẫn qres", "env_var_name": "Tên biến", "env_vars_about": "Về biến môi trường", "env_vars_desc": "Tất cả các lệnh đều được gán các biến môi trường sau theo mặc định:", "env_xrandr_example": "Ví dụ - Xrandr cho độ phân giải động:", "exit_timeout": "Thời gian chờ thoát", "exit_timeout_desc": "Số giây chờ đợi cho tất cả các tiến trình của ứng dụng thoát ra một cách trơn tru khi được yêu cầu thoát. Nếu không được thiết lập, giá trị mặc định là chờ tối đa 5 giây. Nếu được thiết lập thành 0, ứng dụng sẽ bị kết thúc ngay lập tức.", "find_cover": "Tìm chỗ trú ẩn", "global_prep_desc": "Bật/Tắt việc thực thi các lệnh chuẩn bị toàn cầu cho ứng dụng này.", "global_prep_name": "Lệnh chuẩn bị toàn cầu", "image": "Hình ảnh", "image_desc": "Đường dẫn đến biểu tượng/hình ảnh sẽ được gửi đến client. Hình ảnh phải là file PNG. Nếu không được thiết lập, Sunshine sẽ gửi hình ảnh mặc định.", "loading": "Đang tải...", "name": "Tên", "output_desc": "Tệp chứa kết quả đầu ra của lệnh. Nếu không được chỉ định, kết quả đầu ra sẽ bị bỏ qua.", "output_name": "Đầu ra", "run_as_desc": "Điều này có thể cần thiết cho một số ứng dụng yêu cầu quyền quản trị viên (Administrator) để hoạt động đúng cách.", "wait_all": "Tiếp tục streaming cho đến khi tất cả các tiến trình của ứng dụng kết thúc", "wait_all_desc": "Quá trình streaming này sẽ tiếp tục cho đến khi tất cả các tiến trình được ứng dụng khởi chạy đã kết thúc. Khi tùy chọn này không được chọn, stream sẽ dừng lại khi tiến trình chính của ứng dụng kết thúc, ngay cả khi các tiến trình khác của ứng dụng vẫn đang chạy.", "working_dir": "Thư mục làm việc", "working_dir_desc": "Thư mục làm việc cần được truyền vào quá trình. Ví dụ, một số ứng dụng sử dụng thư mục làm việc để tìm kiếm các tệp cấu hình. Nếu không được thiết lập, Sunshine sẽ mặc định sử dụng thư mục cha của lệnh." }, "config": { "adapter_name": "Adapter Name", "adapter_name_desc_linux_1": "Chọn GPU cụ thể để sử dụng cho quá trình capture.", "adapter_name_desc_linux_2": "Tìm tất cả các thiết bị hỗ trợ VAAPI", "adapter_name_desc_linux_3": "Thay thế ``renderD129`` bằng thiết bị từ trên để liệt kê tên và khả năng của thiết bị. Để được hỗ trợ bởi Sunshine, thiết bị cần phải có ít nhất:", "adapter_name_desc_windows": "Chỉ định GPU cụ thể để sử dụng cho quá trình capture Nếu không được thiết lập, GPU sẽ được chọn tự động. Chúng tôi khuyến nghị để để trống trường này để sử dụng tính năng chọn GPU tự động! Lưu ý: GPU này phải có màn hình kết nối và đang bật nguồn. Các giá trị phù hợp có thể được tìm thấy bằng cách sử dụng lệnh sau:", "adapter_name_placeholder_windows": "Radeon RX 580 Series", "add": "Thêm", "address_family": "Kiểu địa chỉ mạng", "address_family_both": "IPv4 và IPv6", "address_family_desc": "Đặt loại địa chỉ mạng được sử dụng bởi Sunshine", "address_family_ipv4": "Chỉ hỗ trợ IPv4", "always_send_scancodes": "Luôn gửi mã quét", "always_send_scancodes_desc": "Gửi mã quét (scancodes) giúp tăng tính tương thích với các trò chơi và ứng dụng, nhưng có thể dẫn đến nhập liệu bàn phím không chính xác từ một số client không sử dụng bố cục bàn phím tiếng Anh Mỹ. Bật tùy chọn này nếu nhập liệu bàn phím không hoạt động trong một số ứng dụng. Tắt tùy chọn này nếu các phím trên client tạo ra nhập liệu sai trên host.", "amd_coder": "Mã hóa AMF (H264)", "amd_coder_desc": "Cho phép bạn chọn mã hóa entropy để ưu tiên chất lượng hoặc tốc độ mã hóa. Chỉ hỗ trợ H.264.", "amd_enforce_hrd": "Thiết bị giải mã tham chiếu giả định AMF (HRD)", "amd_enforce_hrd_desc": "Tăng cường các giới hạn kiểm soát tốc độ để đáp ứng yêu cầu của mô hình HRD. Điều này giúp giảm đáng kể hiện tượng tràn bitrate, nhưng có thể gây ra các lỗi mã hóa hoặc giảm chất lượng trên một số thẻ.", "amd_preanalysis": "Phân tích tiền xử lý AMF", "amd_preanalysis_desc": "Điều này cho phép thực hiện phân tích trước để kiểm soát tốc độ, có thể cải thiện chất lượng nhưng đồng thời làm tăng độ trễ mã hóa.", "amd_quality": "Chất lượng AMF", "amd_quality_balanced": "cân bằng -- cân bằng (mặc định)", "amd_quality_desc": "Điều này điều chỉnh sự cân bằng giữa tốc độ encode và chất lượng.", "amd_quality_group": "Cài đặt chất lượng AMF", "amd_quality_quality": "chất lượng -- ưu tiên chất lượng", "amd_quality_speed": "Tốc độ -- Ưu tiên tốc độ", "amd_rc": "Kiểm soát tốc độ AMF", "amd_rc_cbr": "cbr -- Tốc độ bit cố định (được khuyến nghị nếu HRD được bật)", "amd_rc_cqp": "cqp -- Chế độ QP cố định", "amd_rc_desc": "Điều này kiểm soát phương pháp điều chỉnh tốc độ để đảm bảo chúng ta không vượt quá mục tiêu bitrate của khách hàng. 'cqp' không phù hợp cho việc điều chỉnh bitrate, và các tùy chọn khác ngoài 'vbr_latency' phụ thuộc vào HRD Enforcement để giúp hạn chế việc vượt quá bitrate.", "amd_rc_group": "Cài đặt kiểm soát tốc độ AMF", "amd_rc_vbr_latency": "vbr_latency -- Tốc độ bit biến đổi có giới hạn độ trễ (được khuyến nghị nếu HRD bị vô hiệu hóa; mặc định)", "amd_rc_vbr_peak": "vbr_peak -- Tốc độ bit biến đổi có giới hạn đỉnh", "amd_usage": "Sử dụng AMF", "amd_usage_desc": "Điều này thiết lập cấu hình encode cơ bản. Tất cả các tùy chọn được trình bày bên dưới sẽ ghi đè lên một phần của cấu hình sử dụng, nhưng có thêm các thiết lập ẩn được áp dụng mà không thể cấu hình ở nơi khác.", "amd_usage_lowlatency": "lowlatency - độ trễ thấp (nhanh nhất)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - độ trễ thấp, chất lượng cao (nhanh)", "amd_usage_transcoding": "Chuyển mã -- Chuyển mã (chậm nhất)", "amd_usage_ultralowlatency": "ultralowlatency - độ trễ cực thấp (nhanh nhất; mặc định)", "amd_usage_webcam": "webcam -- webcam (chậm)", "amd_vbaq": "Cơ chế nén thích nghi theo mức độ thay đổi hình ảnh (VBAQ) của AMF", "amd_vbaq_desc": "Hệ thống thị giác của con người thường ít nhạy cảm hơn với các hiện tượng nhiễu trong các vùng có kết cấu phức tạp. Trong chế độ VBAQ, độ biến thiên của pixel được sử dụng để chỉ ra độ phức tạp của kết cấu không gian, cho phép bộ mã hóa phân bổ nhiều bit hơn cho các vùng mịn hơn. Kích hoạt tính năng này mang lại cải thiện về chất lượng hình ảnh chủ quan với một số nội dung.", "apply_note": "Nhấp vào 'Áp dụng' để khởi động lại Sunshine và áp dụng các thay đổi. Điều này sẽ kết thúc tất cả các phiên đang chạy.", "audio_sink": "Bộ thu âm thanh", "audio_sink_desc_linux": "Tên của thiết bị âm thanh được sử dụng cho vòng lặp âm thanh (Audio Loopback). Nếu bạn không chỉ định biến này, pulseaudio sẽ chọn thiết bị monitor mặc định. Bạn có thể tìm tên của thiết bị âm thanh bằng một trong hai lệnh sau:", "audio_sink_desc_macos": "Tên của thiết bị đầu ra âm thanh được sử dụng cho Audio Loopback. Sunshine chỉ có thể truy cập micro trên macOS do hạn chế của hệ thống. Để phát âm thanh hệ thống thông qua Soundflower hoặc BlackHole.", "audio_sink_desc_windows": "Chọn thủ công thiết bị âm thanh cụ thể để ghi âm. Nếu không được thiết lập, thiết bị sẽ được chọn tự động. Chúng tôi khuyến nghị mạnh mẽ để để trống trường này để sử dụng tính năng chọn thiết bị tự động! Nếu bạn có nhiều thiết bị âm thanh có tên giống nhau, bạn có thể lấy ID thiết bị bằng cách sử dụng lệnh sau:", "audio_sink_placeholder_macos": "Lỗ đen 2ch", "audio_sink_placeholder_windows": "Loa (Thiết bị âm thanh độ nét cao)", "av1_mode": "Hỗ trợ AV1", "av1_mode_0": "Sunshine sẽ quảng cáo hỗ trợ cho AV1 dựa trên khả năng của bộ mã hóa (được khuyến nghị)", "av1_mode_1": "Sunshine sẽ không quảng cáo hỗ trợ cho AV1.", "av1_mode_2": "Sunshine sẽ quảng cáo hỗ trợ cho AV1 Main 8-bit profile.", "av1_mode_3": "Sunshine sẽ quảng cáo hỗ trợ cho các cấu hình AV1 Main 8-bit và 10-bit (HDR).", "av1_mode_desc": "Cho phép khách hàng yêu cầu luồng video AV1 Main 8-bit hoặc 10-bit. AV1 đòi hỏi nhiều tài nguyên CPU hơn để mã hóa, do đó việc kích hoạt tính năng này có thể làm giảm hiệu suất khi sử dụng mã hóa phần mềm.", "back_button_timeout": "Thời gian chờ cho nút Home/Hướng dẫn", "back_button_timeout_desc": "Nếu nút Back/Select được giữ nhấn trong số mili giây đã chỉ định, thao tác nhấn nút Home/Guide sẽ được mô phỏng. Nếu giá trị được đặt nhỏ hơn 0 (mặc định), việc giữ nút Back/Select sẽ không mô phỏng thao tác nhấn nút Home/Guide.", "capture": "Buộc sử dụng phương pháp capture cụ thể", "capture_desc": "Ở chế độ tự động, Sunshine sẽ sử dụng trình điều khiển đầu tiên hoạt động. NvFBC yêu cầu trình điều khiển NVIDIA đã được vá.", "cert": "Chứng chỉ", "cert_desc": "Chứng chỉ được sử dụng cho việc ghép nối giao diện người dùng web (web UI) và ứng dụng Moonlight. Để đảm bảo tương thích tốt nhất, chứng chỉ này nên sử dụng khóa công khai RSA-2048.", "channels": "Số lượng khách hàng kết nối tối đa", "channels_desc_1": "Ánh sáng mặt trời cho phép một phiên phát trực tuyến duy nhất được chia sẻ đồng thời với nhiều khách hàng.", "channels_desc_2": "Một số bộ mã hóa phần cứng có thể có các hạn chế làm giảm hiệu suất khi xử lý nhiều luồng.", "coder_cabac": "cabac -- Mã hóa nhị phân thích ứng theo ngữ cảnh - Chất lượng cao hơn", "coder_cavlc": "cavlc -- Mã hóa độ dài biến đổi thích ứng với ngữ cảnh - Giải mã nhanh hơn", "configuration": "Cấu hình", "controller": "Bật điều khiển bằng gamepad", "controller_desc": "Cho phép khách điều khiển hệ thống chủ bằng gamepad / bộ điều khiển.", "credentials_file": "Tệp thông tin xác thực", "credentials_file_desc": "Lưu tên người dùng/mật khẩu riêng biệt với tệp trạng thái của Sunshine.", "dd_config_ensure_active": "Bật màn hình tự động", "dd_config_ensure_only_display": "Tắt các màn hình khác và chỉ kích hoạt màn hình đã chỉ định.", "dd_config_ensure_primary": "Kích hoạt màn hình tự động và thiết lập nó làm màn hình chính.", "dd_configuration_option": "Cấu hình thiết bị", "dd_config_revert_delay": "Thời gian trễ khôi phục cấu hình", "dd_config_revert_delay_desc": "Thời gian trễ bổ sung (tính bằng mili giây) để chờ trước khi khôi phục cấu hình khi ứng dụng đã bị đóng hoặc phiên làm việc cuối cùng đã kết thúc. Mục đích chính là cung cấp quá trình chuyển đổi mượt mà hơn khi chuyển đổi nhanh giữa các ứng dụng.", "dd_config_revert_on_disconnect": "Khôi phục cài đặt gốc khi ngắt kết nối", "dd_config_revert_on_disconnect_desc": "Khôi phục cấu hình khi tất cả các client ngắt kết nối thay vì khi ứng dụng đóng hoặc phiên làm việc cuối cùng kết thúc.", "dd_config_verify_only": "Kiểm tra xem màn hình đã được bật chưa.", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "Bật/tắt chế độ HDR theo yêu cầu của khách hàng (mặc định)", "dd_hdr_option_disabled": "Không thay đổi cài đặt HDR.", "dd_manual_refresh_rate": "Tốc độ làm mới thủ công", "dd_manual_resolution": "Giải quyết thủ công", "dd_mode_remapping": "Chuyển đổi chế độ hiển thị", "dd_mode_remapping_add": "Thêm mục remapping", "dd_mode_remapping_desc_1": "Chỉ định các mục remapping để thay đổi độ phân giải và/hoặc tần số làm mới yêu cầu sang các giá trị khác.", "dd_mode_remapping_desc_2": "Danh sách được duyệt từ trên xuống dưới và kết quả khớp đầu tiên được sử dụng.", "dd_mode_remapping_desc_3": "Các trường \"Yêu cầu\" có thể để trống để phù hợp với bất kỳ giá trị nào được yêu cầu.", "dd_mode_remapping_desc_4_final_values_mixed": "Phải chỉ định ít nhất một trường \"Final\". Độ phân giải hoặc tần số làm mới không được chỉ định sẽ không được thay đổi.", "dd_mode_remapping_desc_4_final_values_non_mixed": "Trường \"Final\" phải được chỉ định và không được để trống.", "dd_mode_remapping_desc_5_sops_mixed_only": "Tùy chọn \"Tối ưu hóa cài đặt trò chơi\" phải được bật trong ứng dụng Moonlight, nếu không các mục có trường độ phân giải được chỉ định sẽ bị bỏ qua.", "dd_mode_remapping_desc_5_sops_resolution_only": "Tùy chọn \"Tối ưu hóa cài đặt trò chơi\" phải được bật trong ứng dụng Moonlight, nếu không quá trình ánh xạ sẽ bị bỏ qua.", "dd_mode_remapping_final_refresh_rate": "Tần số làm mới cuối cùng", "dd_mode_remapping_final_resolution": "Quyết định cuối cùng", "dd_mode_remapping_requested_fps": "Tốc độ khung hình yêu cầu (FPS)", "dd_mode_remapping_requested_resolution": "Giải pháp được yêu cầu", "dd_options_header": "Các tùy chọn hiển thị nâng cao", "dd_refresh_rate_option": "Tần số làm mới", "dd_refresh_rate_option_auto": "Sử dụng giá trị FPS do khách hàng cung cấp (mặc định)", "dd_refresh_rate_option_disabled": "Không thay đổi tần số làm mới.", "dd_refresh_rate_option_manual": "Sử dụng tần số làm mới được nhập thủ công", "dd_resolution_option": "Quyết định", "dd_resolution_option_auto": "Sử dụng độ phân giải do khách hàng cung cấp (mặc định)", "dd_resolution_option_disabled": "Không thay đổi độ phân giải", "dd_resolution_option_manual": "Sử dụng độ phân giải được nhập thủ công", "dd_resolution_option_ogs_desc": "Tùy chọn \"Tối ưu hóa cài đặt trò chơi\" phải được bật trên ứng dụng Moonlight để tính năng này hoạt động.", "dd_wa_hdr_toggle_delay_desc_1": "Khi sử dụng thiết bị hiển thị ảo (VDD) cho việc phát trực tuyến, màu HDR có thể hiển thị không chính xác. Sunshine có thể thử khắc phục vấn đề này bằng cách tắt HDR và sau đó bật lại.", "dd_wa_hdr_toggle_delay_desc_2": "Nếu giá trị được đặt là 0, tính năng khắc phục sự cố sẽ bị vô hiệu hóa (mặc định). Nếu giá trị nằm trong khoảng từ 0 đến 3000 mili giây, Sunshine sẽ tắt HDR, chờ trong khoảng thời gian đã chỉ định và sau đó bật HDR lại. Thời gian chờ khuyến nghị là khoảng 500 mili giây trong hầu hết các trường hợp.", "dd_wa_hdr_toggle_delay_desc_3": "KHÔNG sử dụng giải pháp tạm thời này trừ khi bạn thực sự gặp vấn đề với HDR, vì nó ảnh hưởng trực tiếp đến thời gian bắt đầu phát trực tiếp!", "dd_wa_hdr_toggle_delay": "Giải pháp thay thế cho HDR có độ tương phản cao", "ds4_back_as_touchpad_click": "Quay lại bản đồ/Chọn bằng cách nhấp chuột vào bàn di chuột", "ds4_back_as_touchpad_click_desc": "Khi ép buộc mô phỏng DS4, gán nút Back/Select cho thao tác nhấp chuột trên bàn di chuột.", "ds5_inputtino_randomize_mac": "Ngẫu nhiên hóa địa chỉ MAC của bộ điều khiển ảo", "ds5_inputtino_randomize_mac_desc": "Khi đăng ký bộ điều khiển, hãy sử dụng một địa chỉ MAC ngẫu nhiên thay vì địa chỉ dựa trên chỉ số nội bộ của bộ điều khiển để tránh trộn lẫn các thiết lập cấu hình của các bộ điều khiển khác nhau khi chúng được hoán đổi trên phía client.", "encoder": "Bắt buộc sử dụng bộ mã hóa cụ thể", "encoder_desc": "Buộc sử dụng bộ mã hóa cụ thể, nếu không Sunshine sẽ tự động chọn tùy chọn tốt nhất có sẵn. Lưu ý: Nếu bạn chỉ định bộ mã hóa phần cứng trên Windows, nó phải trùng khớp với GPU mà màn hình được kết nối.", "encoder_software": "Phần mềm", "external_ip": "Địa chỉ IP bên ngoài", "external_ip_desc": "Nếu không được cung cấp địa chỉ IP bên ngoài, Sunshine sẽ tự động phát hiện địa chỉ IP bên ngoài.", "fec_percentage": "Tỷ lệ phần trăm FEC", "fec_percentage_desc": "Tỷ lệ gói tin sửa lỗi trên mỗi gói tin dữ liệu trong mỗi khung hình video. Giá trị cao hơn có thể bù đắp cho việc mất gói tin mạng nhiều hơn, nhưng đổi lại sẽ làm tăng sử dụng băng thông.", "ffmpeg_auto": "Tự động -- để ffmpeg quyết định (mặc định)", "file_apps": "Tệp ứng dụng", "file_apps_desc": "Thư mục chứa các ứng dụng hiện tại của Sunshine.", "file_state": "Tệp của Nhà nước", "file_state_desc": "Tệp chứa trạng thái hiện tại của Sunshine", "gamepad": "Loại bộ điều khiển trò chơi mô phỏng", "gamepad_auto": "Tùy chọn chọn tự động", "gamepad_desc": "Chọn loại gamepad muốn mô phỏng trên máy chủ.", "gamepad_ds4": "DS4 (PlayStation 4)", "gamepad_ds4_manual": "Các tùy chọn lựa chọn cho DS4", "gamepad_ds5": "DS5 (PS5)", "gamepad_ds5_manual": "Các tùy chọn lựa chọn cho DS5", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Các tùy chọn DS4 thủ công", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Chuẩn bị lệnh", "global_prep_cmd_desc": "Cấu hình danh sách các lệnh cần thực thi trước hoặc sau khi chạy bất kỳ ứng dụng nào. Nếu bất kỳ lệnh chuẩn bị nào trong danh sách bị thất bại, quá trình khởi chạy ứng dụng sẽ bị hủy bỏ.", "hevc_mode": "Hỗ trợ HEVC", "hevc_mode_0": "Sunshine sẽ quảng cáo hỗ trợ cho HEVC dựa trên khả năng của bộ mã hóa (được khuyến nghị)", "hevc_mode_1": "Sunshine sẽ không quảng cáo hỗ trợ cho HEVC.", "hevc_mode_2": "Sunshine sẽ quảng cáo hỗ trợ cho HEVC Main profile.", "hevc_mode_3": "Sunshine sẽ quảng cáo hỗ trợ cho các cấu hình HEVC Main và Main10 (HDR).", "hevc_mode_desc": "Cho phép khách hàng yêu cầu luồng video HEVC Main hoặc HEVC Main10. HEVC đòi hỏi nhiều tài nguyên CPU hơn khi mã hóa, do đó việc kích hoạt tính năng này có thể làm giảm hiệu suất khi sử dụng mã hóa phần mềm.", "high_resolution_scrolling": "Hỗ trợ cuộn với độ phân giải cao", "high_resolution_scrolling_desc": "Khi được bật, Sunshine sẽ truyền các sự kiện cuộn có độ phân giải cao từ các ứng dụng Moonlight. Tính năng này có thể hữu ích để tắt cho các ứng dụng cũ có tốc độ cuộn quá nhanh khi sử dụng sự kiện cuộn có độ phân giải cao.", "install_steam_audio_drivers": "Cài đặt trình điều khiển âm thanh Steam", "install_steam_audio_drivers_desc": "Nếu Steam đã được cài đặt, trình điều khiển loa phát trực tuyến Steam sẽ được cài đặt tự động để hỗ trợ âm thanh vòm 5.1/7.1 và tắt âm thanh của ứng dụng chủ.", "key_repeat_delay": "Thời gian trễ lặp lại phím", "key_repeat_delay_desc": "Điều chỉnh tốc độ lặp lại của các phím. Thời gian trễ ban đầu (tính bằng mili giây) trước khi các phím bắt đầu lặp lại.", "key_repeat_frequency": "Tần suất lặp lại phím", "key_repeat_frequency_desc": "Tần suất lặp lại của các phím mỗi giây. Tùy chọn này có thể điều chỉnh và hỗ trợ số thập phân.", "key_rightalt_to_key_win": "Gán phím Alt bên phải cho phím Windows", "key_rightalt_to_key_win_desc": "Có thể bạn không thể gửi phím Windows từ Moonlight trực tiếp. Trong trường hợp đó, có thể hữu ích khi làm cho Sunshine nghĩ rằng phím Alt bên phải là phím Windows.", "keybindings": "Phím tắt", "keyboard": "Bật nhập liệu bằng bàn phím", "keyboard_desc": "Cho phép khách truy cập điều khiển hệ thống chủ thông qua bàn phím.", "lan_encryption_mode": "Chế độ mã hóa mạng LAN", "lan_encryption_mode_1": "Đã kích hoạt cho các khách hàng được hỗ trợ", "lan_encryption_mode_2": "Yêu cầu đối với tất cả khách hàng", "lan_encryption_mode_desc": "Điều này xác định thời điểm mã hóa sẽ được sử dụng khi truyền phát qua mạng nội bộ của bạn. Mã hóa có thể làm giảm hiệu suất truyền phát, đặc biệt là trên các máy chủ và thiết bị khách có cấu hình yếu.", "locale": "Vùng", "locale_desc": "Ngôn ngữ giao diện người dùng được sử dụng cho Sunshine.", "log_path": "Đường dẫn tệp nhật ký", "log_path_desc": "Tệp chứa các bản ghi hiện tại của Sunshine.", "max_bitrate": "Tốc độ bit tối đa", "max_bitrate_desc": "Tốc độ bit tối đa (đơn vị Kbps) mà Sunshine sẽ mã hóa luồng. Nếu đặt thành 0, nó sẽ luôn sử dụng tốc độ bit được yêu cầu bởi Moonlight.", "minimum_fps_target": "Mục tiêu FPS tối thiểu", "minimum_fps_target_desc": "Tốc độ khung hình hiệu quả thấp nhất mà luồng có thể đạt được. Giá trị 0 được coi là khoảng một nửa tốc độ khung hình của luồng. Nên thiết lập giá trị 20 nếu bạn phát nội dung có tốc độ khung hình 24 hoặc 30fps.", "min_log_level": "Mức độ ghi nhật ký", "min_log_level_0": "Chi tiết", "min_log_level_1": "Gỡ lỗi", "min_log_level_2": "Thông tin", "min_log_level_3": "Cảnh báo", "min_log_level_4": "Lỗi", "min_log_level_5": "Chết người", "min_log_level_6": "Không có", "min_log_level_desc": "Mức ghi nhật ký tối thiểu được in ra tiêu chuẩn đầu ra.", "min_threads": "Số luồng CPU tối thiểu", "min_threads_desc": "Tăng giá trị một chút sẽ làm giảm hiệu suất mã hóa, nhưng sự đánh đổi này thường đáng giá để tận dụng thêm các lõi CPU cho quá trình mã hóa. Giá trị lý tưởng là giá trị thấp nhất có thể mã hóa một cách đáng tin cậy ở cài đặt phát trực tuyến mong muốn trên phần cứng của bạn.", "misc": "Các tùy chọn khác", "motion_as_ds4": "Mô phỏng tay cầm DS4 nếu tay cầm của client báo cáo có cảm biến chuyển động.", "motion_as_ds4_desc": "Nếu bị vô hiệu hóa, cảm biến chuyển động sẽ không được tính đến trong quá trình chọn loại gamepad.", "mouse": "Bật nhập liệu chuột", "mouse_desc": "Cho phép khách truy cập điều khiển hệ thống chủ bằng chuột.", "native_pen_touch": "Hỗ trợ bút cảm ứng và chạm gốc", "native_pen_touch_desc": "Khi được bật, Sunshine sẽ truyền các sự kiện bút/chạm gốc từ các ứng dụng Moonlight. Tính năng này có thể hữu ích để tắt cho các ứng dụng cũ không hỗ trợ bút/chạm gốc.", "notify_pre_releases": "Thông báo trước khi phát hành", "notify_pre_releases_desc": "Có muốn nhận thông báo về các phiên bản thử nghiệm mới của Sunshine không?", "nvenc_h264_cavlc": "Ưu tiên CAVLC hơn CABAC trong H.264", "nvenc_h264_cavlc_desc": "Hình thức đơn giản hơn của mã hóa entropy. CAVLC cần khoảng 10% băng thông bit cao hơn để đạt được chất lượng tương đương. Chỉ áp dụng cho các thiết bị giải mã rất cũ.", "nvenc_latency_over_power": "Ưu tiên độ trễ mã hóa thấp hơn so với tiết kiệm năng lượng.", "nvenc_latency_over_power_desc": "Sunshine yêu cầu tốc độ đồng hồ GPU tối đa khi phát trực tiếp để giảm độ trễ mã hóa. Việc tắt tính năng này không được khuyến nghị vì có thể dẫn đến độ trễ mã hóa tăng đáng kể.", "nvenc_opengl_vulkan_on_dxgi": "Hiển thị OpenGL/Vulkan trên nền DXGI", "nvenc_opengl_vulkan_on_dxgi_desc": "Ánh sáng mặt trời không thể ghi lại các chương trình OpenGL và Vulkan toàn màn hình ở tốc độ khung hình đầy đủ trừ khi chúng được hiển thị trên DXGI. Đây là cài đặt hệ thống và sẽ được khôi phục lại sau khi chương trình Ánh sáng mặt trời kết thúc.", "nvenc_preset": "Cài đặt sẵn hiệu suất", "nvenc_preset_1": "(nhanh nhất, mặc định)", "nvenc_preset_7": "(chậm nhất)", "nvenc_preset_desc": "Các giá trị cao hơn cải thiện tỷ lệ nén (chất lượng ở cùng bitrate) nhưng làm tăng độ trễ mã hóa. Nên thay đổi chỉ khi bị giới hạn bởi mạng hoặc bộ giải mã, nếu không, hiệu quả tương tự có thể đạt được bằng cách tăng bitrate.", "nvenc_realtime_hags": "Sử dụng ưu tiên thời gian thực trong lịch trình GPU được tăng tốc phần cứng.", "nvenc_realtime_hags_desc": "Hiện tại, trình điều khiển NVIDIA có thể bị treo trong trình mã hóa khi tùy chọn HAGS được bật, ưu tiên thời gian thực được sử dụng và sử dụng VRAM gần đạt mức tối đa. Tắt tùy chọn này sẽ hạ ưu tiên xuống mức cao, tránh tình trạng treo nhưng làm giảm hiệu suất ghi hình khi GPU đang hoạt động nặng.", "nvenc_spatial_aq": "Chất lượng không khí theo không gian", "nvenc_spatial_aq_desc": "Gán giá trị QP cao hơn cho các vùng phẳng trong video. Được khuyến nghị bật khi phát trực tuyến ở tốc độ bit thấp.", "nvenc_twopass": "Chế độ hai lần quét", "nvenc_twopass_desc": "Thêm bước mã hóa sơ bộ. Điều này cho phép phát hiện nhiều vector chuyển động hơn, phân phối bitrate đều hơn trong khung hình và tuân thủ nghiêm ngặt hơn các giới hạn bitrate. Không nên tắt tính năng này vì có thể dẫn đến việc vượt quá bitrate tạm thời và mất gói dữ liệu sau đó.", "nvenc_twopass_disabled": "Tắt (nhanh nhất, không được khuyến nghị)", "nvenc_twopass_full_res": "Độ phân giải cao (chậm hơn)", "nvenc_twopass_quarter_res": "Độ phân giải theo quý (nhanh hơn, mặc định)", "nvenc_vbv_increase": "Tỷ lệ phần trăm tăng của VBV/HRD trong một khung hình", "nvenc_vbv_increase_desc": "Theo mặc định, Sunshine sử dụng VBV/HRD một khung hình, có nghĩa là kích thước khung hình video đã mã hóa không được vượt quá tỷ lệ bit yêu cầu chia cho tần số khung hình yêu cầu. Nới lỏng hạn chế này có thể mang lại lợi ích và hoạt động như bitrate biến đổi độ trễ thấp, nhưng cũng có thể dẫn đến mất gói nếu mạng không có dung lượng đệm đủ để xử lý các đỉnh bitrate. Giá trị tối đa được chấp nhận là 400, tương ứng với giới hạn kích thước khung hình video đã mã hóa tăng gấp 5 lần.", "origin_web_ui_allowed": "Giao diện người dùng web gốc được phép", "origin_web_ui_allowed_desc": "Nguồn gốc của địa chỉ điểm cuối từ xa không bị từ chối truy cập vào giao diện người dùng web (Web UI).", "origin_web_ui_allowed_lan": "Chỉ những người trong mạng LAN mới có thể truy cập giao diện người dùng web.", "origin_web_ui_allowed_pc": "Chỉ máy chủ cục bộ (localhost) mới có thể truy cập giao diện người dùng web (Web UI).", "origin_web_ui_allowed_wan": "Bất kỳ ai cũng có thể truy cập giao diện người dùng web (Web UI).", "output_name": "ID hiển thị", "output_name_desc_unix": "Trong quá trình khởi động Sunshine, bạn sẽ thấy danh sách các màn hình được phát hiện. Lưu ý: Bạn cần sử dụng giá trị ID bên trong dấu ngoặc đơn. Dưới đây là một ví dụ; kết quả thực tế có thể được tìm thấy trong tab Khắc phục sự cố.", "output_name_desc_windows": "Chỉ định thủ công ID thiết bị hiển thị để sử dụng cho việc ghi hình. Nếu không được thiết lập, thiết bị hiển thị chính sẽ được ghi hình. Lưu ý: Nếu bạn đã chỉ định GPU ở trên, thiết bị hiển thị này phải được kết nối với GPU đó. Trong quá trình khởi động Sunshine, bạn sẽ thấy danh sách các thiết bị hiển thị được phát hiện. Dưới đây là một ví dụ; kết quả thực tế có thể được tìm thấy trong tab Khắc phục sự cố.", "ping_timeout": "Thời gian chờ ping", "ping_timeout_desc": "Thời gian chờ (tính bằng mili giây) trước khi ngừng truyền dữ liệu từ Moonlight.", "pkey": "Khóa riêng", "pkey_desc": "Khóa riêng tư được sử dụng cho việc ghép nối giữa giao diện web và ứng dụng Moonlight. Để đảm bảo tương thích tốt nhất, khóa riêng tư này nên là khóa RSA-2048.", "port": "Cảng", "port_alert_1": "Sunshine không thể sử dụng các cổng dưới 1024!", "port_alert_2": "Các cổng trên 65535 không khả dụng!", "port_desc": "Đặt nhóm cổng được sử dụng bởi Sunshine", "port_http_port_note": "Sử dụng cổng này để kết nối với Moonlight.", "port_note": "Lưu ý", "port_port": "Cảng", "port_protocol": "Quy trình", "port_tcp": "Giao thức truyền tải liên kết (TCP)", "port_udp": "UDP (Giao thức dữ liệu không định hướng)", "port_warning": "Việc phơi bày giao diện người dùng web (Web UI) ra internet là một rủi ro bảo mật! Hãy tiếp tục với rủi ro của riêng bạn!", "port_web_ui": "Giao diện người dùng web", "qp": "Tham số lượng tử hóa", "qp_desc": "Một số thiết bị có thể không hỗ trợ Tốc độ bit cố định (Constant Bit Rate). Đối với những thiết bị này, QP (Quality Profile) sẽ được sử dụng thay thế. Giá trị cao hơn có nghĩa là nén nhiều hơn, nhưng chất lượng sẽ thấp hơn.", "qsv_coder": "QuickSync Coder (H.264)", "qsv_preset": "Cài đặt nhanh QuickSync", "qsv_preset_fast": "Nhanh (chất lượng thấp)", "qsv_preset_faster": "nhanh hơn (chất lượng thấp hơn)", "qsv_preset_medium": "Trung bình (mặc định)", "qsv_preset_slow": "chậm (chất lượng tốt)", "qsv_preset_slower": "chậm hơn (chất lượng tốt hơn)", "qsv_preset_slowest": "chậm nhất (chất lượng tốt nhất)", "qsv_preset_veryfast": "nhanh nhất (chất lượng thấp nhất)", "qsv_slow_hevc": "Cho phép mã hóa HEVC chậm", "qsv_slow_hevc_desc": "Điều này có thể cho phép mã hóa HEVC trên các GPU Intel cũ hơn, nhưng sẽ làm tăng sử dụng GPU và giảm hiệu suất.", "restart_note": "Sunshine đang khởi động lại để áp dụng các thay đổi.", "stream_audio": "Phát trực tiếp âm thanh", "stream_audio_desc": "Có nên phát âm thanh hay không. Tắt tính năng này có thể hữu ích khi phát video trên các màn hình không có giao diện người dùng (headless displays) như màn hình phụ.", "sunshine_name": "Tên Ánh Dương", "sunshine_name_desc": "Tên hiển thị bởi Moonlight. Nếu không được chỉ định, tên máy chủ của PC sẽ được sử dụng.", "sw_preset": "Cài đặt sẵn cho SW", "sw_preset_desc": "Tối ưu hóa sự cân bằng giữa tốc độ mã hóa (số khung hình được mã hóa mỗi giây) và hiệu quả nén (chất lượng trên mỗi bit trong luồng bit). Mặc định là siêu nhanh.", "sw_preset_fast": "nhanh", "sw_preset_faster": "nhanh hơn", "sw_preset_medium": "trung bình", "sw_preset_slow": "chậm", "sw_preset_slower": "chậm hơn", "sw_preset_superfast": "siêu nhanh (mặc định)", "sw_preset_ultrafast": "siêu nhanh", "sw_preset_veryfast": "rất nhanh", "sw_preset_veryslow": "rất chậm", "sw_tune": "Điều chỉnh phần mềm", "sw_tune_animation": "Hoạt hình -- phù hợp cho phim hoạt hình; sử dụng thuật toán giảm nhiễu cao hơn và nhiều khung tham chiếu hơn.", "sw_tune_desc": "Các tùy chọn điều chỉnh, được áp dụng sau khi thiết lập trước. Mặc định là zerolatency.", "sw_tune_fastdecode": "fastdecode -- cho phép giải mã nhanh hơn bằng cách vô hiệu hóa một số bộ lọc.", "sw_tune_film": "Phim -- dùng cho nội dung phim chất lượng cao; giảm hiện tượng vỡ khối.", "sw_tune_grain": "hạt -- giữ nguyên cấu trúc hạt trong vật liệu phim cũ, có hạt.", "sw_tune_stillimage": "Hình ảnh tĩnh -- Phù hợp cho nội dung dạng trình chiếu.", "sw_tune_zerolatency": "zerolatency -- phù hợp cho mã hóa nhanh và phát trực tuyến có độ trễ thấp (mặc định)", "system_tray": "Bật khay hệ thống", "system_tray_desc": "Hiển thị biểu tượng trong khay hệ thống và hiển thị thông báo trên màn hình desktop.", "touchpad_as_ds4": "Mô phỏng tay cầm DS4 nếu tay cầm của client báo có bàn di chuột.", "touchpad_as_ds4_desc": "Nếu tính năng này bị tắt, sự hiện diện của bàn di chuột sẽ không được xem xét trong quá trình chọn loại gamepad.", "upnp": "UPnP (Tự động phát hiện và chia sẻ thiết bị)", "upnp_desc": "Tự động cấu hình chuyển tiếp cổng để phát trực tuyến qua Internet.", "vaapi_strict_rc_buffer": "Thực thi nghiêm ngặt giới hạn tốc độ khung hình cho H.264/HEVC trên GPU AMD.", "vaapi_strict_rc_buffer_desc": "Bật tùy chọn này có thể tránh tình trạng mất khung hình trên mạng trong quá trình chuyển cảnh, nhưng chất lượng video có thể bị giảm trong quá trình chuyển động.", "virtual_sink": "Bồn rửa ảo", "virtual_sink_desc": "Chỉ định thủ công thiết bị âm thanh ảo để sử dụng. Nếu không được thiết lập, thiết bị sẽ được chọn tự động. Chúng tôi khuyến nghị mạnh mẽ để để trống trường này để sử dụng tính năng chọn thiết bị tự động!", "virtual_sink_placeholder": "Loa phát trực tuyến Steam", "vt_coder": "VideoToolbox Coder", "vt_realtime": "VideoToolbox Mã hóa thời gian thực", "vt_software": "Phần mềm VideoToolbox cho mã hóa video", "vt_software_allowed": "Được phép", "vt_software_forced": "Bắt buộc", "wan_encryption_mode": "Chế độ mã hóa WAN", "wan_encryption_mode_1": "Được kích hoạt cho các khách hàng được hỗ trợ (mặc định)", "wan_encryption_mode_2": "Yêu cầu đối với tất cả khách hàng", "wan_encryption_mode_desc": "Điều này xác định thời điểm mã hóa sẽ được sử dụng khi truyền phát qua Internet. Mã hóa có thể làm giảm hiệu suất truyền phát, đặc biệt là trên các máy chủ và thiết bị khách có cấu hình yếu." }, "index": { "description": "Sunshine là một nền tảng phát trực tiếp game tự chủ cho Moonlight.", "download": "Tải xuống", "installed_version_not_stable": "Bạn đang sử dụng phiên bản thử nghiệm của Sunshine. Bạn có thể gặp phải lỗi hoặc các vấn đề khác. Vui lòng báo cáo bất kỳ vấn đề nào bạn gặp phải. Cảm ơn bạn đã giúp Sunshine trở thành phần mềm tốt hơn!", "loading_latest": "Đang tải phiên bản mới nhất...", "new_pre_release": "Phiên bản thử nghiệm mới đã có sẵn!", "new_stable": "Phiên bản ổn định mới đã có sẵn!", "startup_errors": "Lưu ý! Sunshine đã phát hiện các lỗi sau đây trong quá trình khởi động. Chúng tôi KHUYẾN NGHỊ MẠNH MẼ bạn khắc phục các lỗi này trước khi bắt đầu phát trực tuyến.", "version_dirty": "Cảm ơn bạn đã giúp Sunshine trở thành phần mềm tốt hơn!", "version_latest": "Bạn đang sử dụng phiên bản mới nhất của Sunshine.", "welcome": "Chào nắng!" }, "navbar": { "applications": "Ứng dụng", "configuration": "Cấu hình", "home": "Trang chủ", "password": "Thay đổi mật khẩu", "pin": "Mã PIN", "theme_auto": "Tự động", "theme_dark": "Tối", "theme_light": "Ánh sáng", "toggle_theme": "Chủ đề", "troubleshoot": "Khắc phục sự cố" }, "password": { "confirm_password": "Xác nhận mật khẩu", "current_creds": "Chứng chỉ hiện tại", "new_creds": "Chứng chỉ mới", "new_username_desc": "Nếu không được chỉ định, tên người dùng sẽ không thay đổi.", "password_change": "Thay đổi mật khẩu", "success_msg": "Mật khẩu đã được thay đổi thành công! Trang này sẽ được tải lại trong giây lát, trình duyệt của bạn sẽ yêu cầu bạn nhập thông tin đăng nhập mới." }, "pin": { "device_name": "Tên thiết bị", "pair_failure": "Kết nối không thành công: Vui lòng kiểm tra xem mã PIN đã được nhập chính xác chưa.", "pair_success": "Thành công! Vui lòng kiểm tra Moonlight để tiếp tục.", "pin_pairing": "Kết nối PIN", "send": "Gửi", "warning_msg": "Đảm bảo bạn có quyền truy cập vào máy tính mà bạn đang kết nối. Phần mềm này có thể cho phép kiểm soát hoàn toàn máy tính của bạn, vì vậy hãy cẩn thận!" }, "resource_card": { "github_discussions": "Thảo luận trên GitHub", "legal": "Pháp lý", "legal_desc": "Bằng cách tiếp tục sử dụng phần mềm này, bạn đồng ý với các điều khoản và điều kiện được quy định trong các tài liệu sau đây.", "license": "Giấy phép", "lizardbyte_website": "Trang web LizardByte", "resources": "Tài nguyên", "resources_desc": "Tài nguyên cho Ánh nắng!", "third_party_notice": "Thông báo từ bên thứ ba" }, "troubleshooting": { "dd_reset": "Đặt lại cài đặt thiết bị hiển thị cố định", "dd_reset_desc": "Nếu Sunshine gặp sự cố khi cố gắng khôi phục cài đặt thiết bị hiển thị đã thay đổi, bạn có thể đặt lại cài đặt và tiếp tục khôi phục trạng thái hiển thị thủ công.", "dd_reset_error": "Lỗi xảy ra trong quá trình khôi phục trạng thái lưu trữ!", "dd_reset_success": "Thành công trong việc thiết lập lại sự kiên trì!", "force_close": "Buộc đóng ứng dụng", "force_close_desc": "Nếu Moonlight báo lỗi về một ứng dụng đang chạy, việc buộc đóng ứng dụng đó sẽ khắc phục sự cố.", "force_close_error": "Lỗi khi đóng ứng dụng", "force_close_success": "Đơn đăng ký đã được đóng thành công!", "logs": "Nhật ký", "logs_desc": "Xem các bản ghi được tải lên bởi Sunshine", "logs_find": "Tìm...", "restart_sunshine": "Khởi động lại Sunshine", "restart_sunshine_desc": "Nếu Sunshine không hoạt động đúng cách, bạn có thể thử khởi động lại ứng dụng. Điều này sẽ kết thúc tất cả các phiên đang chạy.", "restart_sunshine_success": "Sunshine đang khởi động lại.", "troubleshooting": "Khắc phục sự cố", "unpair_all": "Bỏ ghép tất cả", "unpair_all_error": "Lỗi khi hủy ghép nối", "unpair_all_success": "Tất cả các thiết bị đã ngắt kết nối.", "unpair_desc": "Hãy ngắt kết nối các thiết bị đã ghép nối. Các thiết bị đã ghép nối nhưng đang có phiên hoạt động sẽ vẫn kết nối, nhưng không thể bắt đầu hoặc tiếp tục phiên.", "unpair_single_no_devices": "Không có thiết bị nào được ghép đôi.", "unpair_single_success": "Tuy nhiên, thiết bị (các thiết bị) có thể vẫn đang trong phiên hoạt động. Nhấn nút 'Buộc đóng' ở trên để kết thúc tất cả các phiên đang mở.", "unpair_single_unknown": "Khách hàng không xác định", "unpair_title": "Ngắt kết nối thiết bị" }, "welcome": { "confirm_password": "Xác nhận mật khẩu", "create_creds": "Trước khi bắt đầu, chúng tôi cần bạn tạo một tên người dùng và mật khẩu mới để truy cập vào giao diện người dùng web (Web UI).", "create_creds_alert": "Các thông tin đăng nhập sau đây là cần thiết để truy cập giao diện người dùng web của Sunshine. Hãy giữ chúng an toàn, vì bạn sẽ không bao giờ thấy chúng nữa!", "greeting": "Chào mừng đến với Sunshine!", "login": "Đăng nhập", "welcome_success": "Trang này sẽ được tải lại trong giây lát, trình duyệt của bạn sẽ yêu cầu bạn nhập lại thông tin đăng nhập." } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/zh.json ================================================ { "_common": { "apply": "应用", "auto": "自动", "autodetect": "自动检测 (推荐)", "beta": "(测试版)", "cancel": "取消", "cmd_name": "命令名称", "cmd_val": "命令值", "default": "默认", "default_global": "默认(全局)", "disabled": "禁用", "disabled_def": "禁用(默认)", "disabled_def_cbox": "默认:禁用", "dismiss": "关闭", "do_cmd": "前置命令", "elevated": "提权运行", "enabled": "启用", "enabled_def": "启用(默认)", "enabled_def_cbox": "默认:启用", "error": "错误!", "learn_more": "了解更多", "note": "注:", "password": "密码", "run_as": "以管理员身份运行", "save": "保存", "see_more": "查看更多", "success": "成功!", "undo_cmd": "后置命令", "username": "用户名", "warning": "警告!" }, "apps": { "actions": "操作", "add_cmds": "添加命令", "add_new": "添加新应用", "allow_client_commands": "允许客户端准备命令", "allow_client_commands_desc": "在此APP运行时是否允许执行客户端准备命令", "alphabetize": "按字母排序", "already_ordered": "应用已按字母排序。", "app_name": "应用名称", "app_name_desc": "在 Moonlight 显示的应用名称", "applications_desc": "应用列表在会话终止时刷新", "applications_reorder_desc": "拖动应用以重新排序。任何更改都会终止当前正在运行的APP。", "applications_tips": "提示:您也可以右键点击启动应用按钮来复制启动链接,并将其发送给朋友。他们需要使用 Artemis 并已经与您配对。\n您也可以双击应用名称来导出应用启动文件,添加到您最喜欢的模拟器前端。", "applications_title": "应用", "auto_detach": "启动串流后应用突然关闭时不退出串流", "auto_detach_desc": "这将尝试自动检测在启动另一个程序或自身实例后很快关闭的启动类应用。 检测到启动型应用程序时,它会被视为一个分离的应用程序。", "close": "终止", "close_warning": "确定要终止当前正在运行的APP吗?", "close_failed": "APP终止失败。", "cmd": "命令", "cmd_desc": "要启动的主要应用程序。如果为空,将不会启动任何应用程序。", "cmd_note": "如果命令中可执行文件的路径包含空格,则必须用引号括起来。", "cmd_prep_desc": "应用运行前/后要执行的命令列表。如果任何前置命令失败,应用的启动过程将被中止。", "cmd_prep_name": "准备命令", "cmd_state_desc": "应用暂停(所有客户端断开连接)或恢复(第一个客户端连接)时执行的命令列表。\n前置命令在恢复时执行,后置命令在暂停时执行。\n请确保在命令准备工作的后置命令中清理任何命令产生的副作用,当会话终止时,暂停命令将不会被执行。", "cmd_state_name": "暂停/恢复命令", "covers_found": "找到的封面", "delete": "删除", "delete_failed": "APP删除失败:", "detached_cmds": "独立命令", "detached_cmds_add": "添加独立命令", "detached_cmds_desc": "要在后台运行的命令列表。", "detached_cmds_note": "如果命令可执行文件的路径包含空格,您必须在引号里将其贴出。", "edit": "编辑", "env_app_id": "应用 ID (已弃用)", "env_app_name": "应用名称", "env_app_uuid": "应用 UUID", "env_app_status": "应用状态: 值为 'STARTING', 'RUNNING', 'PAUSING', 'RESUMING', 'TERMINATING' 的任意一个 (string)", "env_client_audio_config": "客户端请求的音频配置 (2.0/5.1/7.1)", "env_client_enable_sops": "客户端请求自动更改游戏设置以实现最佳串流效果 (true/false)", "env_client_fps": "客户端请求的帧率 (float)", "env_client_gcmap": "客户端请求的游戏手柄掩码,采用 bitset/bitfield 格式 (int)", "env_client_hdr": "客户端请求的 HDR 状态 (true/false)", "env_client_height": "客户端请求的分辨率的高度 (int)", "env_client_host_audio": "客户端请求在主机播放声音 (true/false)", "env_client_width": "客户端请求的分辨率的宽度 (int)", "env_client_uuid": "客户端 UUID", "env_client_name": "客户端名称", "env_displayplacer_example": "示例 - 使用 displayplacer 自动更改分辨率:", "env_qres_example": "示例 - 使用 QRes 自动更改分辨率:", "env_qres_path": "QRes 路径", "env_rtss_cli_example": "示例 - 使用 rtss-cli 限制 FPS:", "env_sunshine_compatibility": "以 \"SUNSHINE_\" 开头的环境变量已弃用,但仍保留它们以确保对 Sunshine 相关工具的兼容。SUNSHINE_CLIENT_FPS 变量已修改为 FLOAT 以支持小数刷新率(特别适用于 Special-K),如果您的脚本/工具无法接受浮点数输入,请前往 Advanced 选项卡启用 \"环境变量兼容模式\"。APOLLO_CLIENT_FPS 则始终为 FLOAT 类型。", "env_var_name": "变量名称", "env_vars_about": "关于环境变量", "env_vars_desc": "默认情况下,所有命令都会得到这些环境变量:", "env_xrandr_example": "示例 - Xrandr 用于分辨率自动化:", "export_launcher_file": "导出启动器文件", "exit_timeout": "退出超时", "exit_timeout_desc": "请求退出时,等待所有应用进程正常关闭的秒数。 如果未设置,默认等待5秒钟。如果设置为零或负值,应用程序将立即终止。", "find_cover": "查找封面", "global_prep_desc": "启用/禁用此应用程序的全局准备命令。", "global_prep_name": "全局准备命令", "global_state_desc": "启用/禁用此应用程序的全局暂停/恢复命令。", "global_state_name": "全局暂停/恢复命令", "image": "图片", "image_desc": "发送到客户端的应用程序图标/图片/图像的路径。图片必须是 PNG 文件。如果未设置,Apollo 将发送默认图片。", "launch": "启动", "launch_local_client": "要通过此设备上的客户端启动应用吗?", "launch_warning": "确定要启动此应用吗?这将会终止当前已启动的应用。", "launch_success": "应用启动成功!", "launch_failed": "应用启动失败:", "loading": "加载中...", "name": "名称", "output_desc": "存储命令输出的文件,如果未指定,输出将被忽略", "output_name": "输出", "per_client_app_identity": "按客户端区分 App 身份", "per_client_app_identity_desc": "当你希望在使用此 App 时每个客户端都有不同的虚拟显示器组合配置时有用。", "reorder_failed": "重新排序应用失败:", "resolution_scale_factor": "分辨率缩放比例", "resolution_scale_factor_desc": "基于此比例缩放客户端请求的分辨率。例如 2000x1000 缩放 120% 将变成 2400x1200。当此项为非 100% 时覆盖客户端请求的缩放比例。此选项不会影响客户端请求的串流分辨率。", "run_as_desc": "这可能是某些需要管理员权限才能正常运行的应用程序所必需的。可能会导致 URL schemes 无法正常启动。", "save_failed": "保存APP失败:", "save_order": "保存排序", "terminate_on_pause": "暂停时终止", "terminate_on_pause_desc": "当所有客户端断开连接时终止此 APP。", "use_app_identity": "使用 App 身份", "use_app_identity_desc": "在创建虚拟显示器时使用 App 自身的身份,而非客户端的。这样可以针对 APP 进行单独的显示器组合配置。", "virtual_display": "总是创建虚拟显示器", "virtual_display_desc": "在启动这个 App 的时候总是创建虚拟显示器,覆盖客户端请求。请确保 SudoVDA 虚拟显示器驱动已安装并启用。", "wait_all": "继续串流直到所有应用进程退出", "wait_all_desc": "这将继续串流直到应用程序启动的所有进程终止。 当未选中时,串流将在初始应用进程终止时停止,即使其他应用进程仍在运行。", "working_dir": "工作目录", "working_dir_desc": "应传递给进程的工作目录。例如,某些应用程序使用工作目录搜索配置文件。如果不设置,Apollo 将默认使用命令的父目录" }, "client_card": { "clients": "客户端", "clients_desc": "为 Apollo 精心特调的客户端", "generic_moonlight_clients_desc": "普通 Moonlight 客户端也可与 Apollo 共同使用。" }, "config": { "adapter_name": "适配器名称", "adapter_name_desc_linux_1": "手动指定用于捕获的 GPU。", "adapter_name_desc_linux_2": "找到所有能够使用 VAAPI 的设备", "adapter_name_desc_linux_3": "用上面的设备替换``renderD129``,列出设备的名称和功能。要获得 Apollo 的支持,设备至少需要具备以下功能:", "adapter_name_desc_windows": "手动指定用于捕获的 GPU 。如果未设置,GPU 将被自动选择。 我们强烈建议将此字段留空以使用自动的 GPU 选择!注意:此GPU 必须连接并开启显示器。 可以使用以下命令找到适当的值:", "adapter_name_placeholder_windows": "Radeon RX 580 Series", "add": "添加", "address_family": "IP 地址族", "address_family_both": "IPv4+IPv6", "address_family_desc": "设置 Apollo 使用的 IP 地址族", "address_family_ipv4": "仅 IPv4", "always_send_scancodes": "总是发送扫描码", "always_send_scancodes_desc": "发送扫描码增强了与游戏和应用的兼容性,但可能会导致某些客户端输入不正确的键盘,而这些客户端不使用美国的英文键盘布局。 如果键盘输入在某些应用程序中根本无法工作,请启用。 如果客户端上的密钥在主机上生成错误的输入,请禁用。", "amd_coder": "AMF 编码器 (H264)", "amd_coder_desc": "允许您选择用于优先质量或编码速度的缠绕编码。 H.264。", "amd_enforce_hrd": "AMF 推测参考解码器 (HRD)", "amd_enforce_hrd_desc": "提高对费率控制的限制,以满足人力资源开发模式的要求。 这大大减少了比特率过量流量,但可能导致编码伪影或降低某些卡片的质量。", "amd_preanalysis": "AMF 预分析", "amd_preanalysis_desc": "启用码率控制预分析,可能会以增加编码延迟为代价提高质量。", "amd_quality": "AMF 质量", "amd_quality_balanced": "balanced -- 平衡(默认)", "amd_quality_desc": "这将控制编码速度和质量之间的平衡。", "amd_quality_group": "AMF 质量设置", "amd_quality_quality": "quality -- 偏好质量", "amd_quality_speed": "speed -- 偏好速度", "amd_rc": "AMF 码率控制", "amd_rc_cbr": "cbr -- 恒定比特率(在启用HDR时推荐)", "amd_rc_cqp": "cqp -- 恒定 QP 模式", "amd_rc_desc": "这将控制码率控制方法,确保我们不超过客户端比特率限制。 “cqp”不适合于比特率限制,除“vbr_latency”之外的其他选项依赖于HRD(假想参考解码器)限制来防止比特率超过限制。", "amd_rc_group": "AMF 码率控制设置", "amd_rc_vbr_latency": "vbr_latency -- 考虑延迟的可变比特率(在禁用HDR时推荐使用;默认)", "amd_rc_vbr_peak": "vbr_peak -- 受峰值限制的可变比特率", "amd_usage": "AMF 工作模式", "amd_usage_desc": "设置基本编码配置文件。 以下列出的所有选项将覆盖使用情况简介的子集,但是应用到了其他不可配置的隐藏设置。", "amd_usage_lowlatency": "lowlatency -- 低延迟(最快)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality -- 低延迟、高质量(快速)", "amd_usage_transcoding": "transcoding -- 转码(最慢)", "amd_usage_ultralowlatency": "ultralowlatency -- 超低延迟(最快;默认)", "amd_usage_webcam": "webcam -- 网络摄像头(慢)", "amd_vbaq": "AMF 基于方差的自适应量化 (VBAQ)", "amd_vbaq_desc": "人类的视觉系统通常对高度纹理化区域中的瑕疵不太敏感。在VBAQ模式下,像素方差被用来指示空间纹理的复杂性,这使得编码器可以将更多的比特分配给更平滑的区域。启用这个特性可以在某些内容上提升主观视觉质量。", "apply_note": "点击“应用”重启 Apollo 并应用更改。这将终止任何正在运行的会话。", "audio_sink": "音频输出设备", "audio_sink_desc_linux": "手动指定需要抓取的音频输出设备。如果您没有指定此变量,PulseAudio 将选择默认监测设备。 您可以使用以下任何命令找到音频输出设备的名称:", "audio_sink_desc_macos": "手动指定需要抓取的音频输出设备。由于系统限制,Apollo 在 macOS 上只能访问麦克风。 使用 Soundflow 或 BlackHole 来串流系统音频。", "audio_sink_desc_windows": "当客户端允许在PC上播放音频时使用的音频设备。\n如未指定,则将自动选择。 \n如果您有多个具有相同名称的音频设备,您可以使用以下命令获取设备ID:", "audio_sink_placeholder_macos": "BlackHole 2ch", "audio_sink_placeholder_windows": "扬声器(High Definition Audio Device)", "auto_capture_sink": "自动捕获当前音频输出设备", "auto_capture_sink_desc": "在默认音频输出设备变更后自动捕获新的默认设备。", "av1_mode": "AV1 支持", "av1_mode_0": "Apollo 将基于编码器能力通告对 AV1 的支持(推荐)", "av1_mode_1": "Apollo 将不会通告对 AV1 的支持", "av1_mode_2": "Apollo 将通告 AV1 Main 8-bit 配置支持", "av1_mode_3": "Apollo 将通告 AV1 Main 8-bit 和 10-bit (HDR) 配置支持", "av1_mode_desc": "允许客户端请求 AV1 Main 8-bit 或 10-bit 视频流。AV1 的编码对 CPU 的要求较高,因此在使用软件编码时,启用此功能可能会降低性能。", "back_button_timeout": "主页/导航按钮模拟超时", "back_button_timeout_desc": "如果按住“返回/选择”按钮达指定的毫秒数,将模拟按下“主页/导航”按钮。如果设置值小于 0(默认值),则按住“返回/选择”按钮不会模拟按下“主页/导航”按钮。", "capture": "强制特定捕获方法", "capture_desc": "在自动模式下,Apollo 将使用第一个能正常工作的模式。NvFBC 需要已打补丁的 Nvidia 驱动程序。", "cert": "证书", "cert_desc": "用于 Web UI 和 Moonlilght 客户端配对的证书。为了最佳兼容性,这应该是一个 RSA-2048 公钥。", "channels": "最多同时连接客户端数", "channels_desc_1": "Apollo 允许多个客户端同时共享一个串流会话。", "channels_desc_2": "某些硬件编码器可能存在限制,在编码多条流时会降低性能。", "coder_cabac": "cabac -- 上下文自适应二进制算术编码- 较高质量", "coder_cavlc": "cavlc -- 上下文适应变量编码 - 更快解码", "configuration": "配置", "controller": "启用游戏手柄输入", "controller_desc": "允许客户端使用游戏手柄控制主机系统", "credentials_file": "凭据文件", "credentials_file_desc": "将用户名/密码与 Apollo 的状态文件分开保存。", "dd_configuration_label": "设备配置", "dd_config_ensure_active": "自动激活显示", "dd_config_ensure_only_display": "停用其他显示器并仅激活指定的显示", "dd_config_ensure_primary": "自动激活显示并将其作为主要显示", "dd_config_revert_delay": "配置恢复延迟", "dd_config_revert_delay_desc": "在恢复配置前等待更多的以毫秒为单位的延迟,当应用程序已关闭或上次会话终止。 主要目的是在应用程序之间快速切换时提供更顺利的转换。", "dd_config_verify_only": "验证显示是否已启用", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "按客户端请求开启/关闭HDR 模式 (默认)", "dd_hdr_option_disabled": "不要更改 HDR 设置", "dd_mode_remapping": "显示模式重映射模式", "dd_mode_remapping_add": "添加重新映射条目", "dd_mode_remapping_desc_1": "指定重映射条目以更改请求的分辨率和/或刷新率到其他值。", "dd_mode_remapping_desc_2": "列表从上到下反转并使用第一次匹配。", "dd_mode_remapping_desc_3": "“请求”字段可以留空以匹配任何请求的值。", "dd_mode_remapping_desc_4_final_values_mixed": "必须指定至少一个\"最终\"字段。未指定的分辨率或刷新率不会更改。", "dd_mode_remapping_desc_4_final_values_non_mixed": "“最终”字段必须指定并且不能为空。", "dd_mode_remapping_desc_5_sops_mixed_only": "\"优化游戏设置\"选项必须在 Moonlight 客户端启用,否则将跳过指定任何分辨率字段的条目。", "dd_mode_remapping_desc_5_sops_resolution_only": "\"优化游戏设置\"选项必须在 Moonlight 客户端启用,否则将跳过映射。", "dd_mode_remapping_final_refresh_rate": "最终刷新率", "dd_mode_remapping_final_resolution": "最终分辨率", "dd_mode_remapping_requested_fps": "请求FPS", "dd_mode_remapping_requested_resolution": "请求的分辨率", "dd_options_header": "高级显示设备选项", "dd_refresh_rate_option": "刷新率", "dd_refresh_rate_option_auto": "使用客户端提供的 FPS 值 (默认)", "dd_refresh_rate_option_disabled": "不要改变刷新率", "dd_refresh_rate_option_manual": "使用手动输入的刷新率", "dd_refresh_rate_option_manual_desc": "输入要使用的刷新率", "dd_resolution_option": "分辨率", "dd_resolution_option_auto": "使用客户端提供的分辨率(默认)", "dd_resolution_option_disabled": "不改变分辨率", "dd_resolution_option_manual": "使用手动输入的分辨率", "dd_resolution_option_manual_desc": "输入要使用的分辨率", "dd_resolution_option_ogs_desc": "“优化游戏设置”选项必须在 Moonlight 客户端启用才能正常工作。", "dd_resolution_option_vdisplay_desc": "一般情况下不推荐启用此选项,请使用 Windows 原生设置配置显示器组合。当使用虚拟显示器时,只有内置或客户端请求的分辨率/帧率组合可用。", "dd_resolution_option_multi_instance_desc": "当同时启动多个 Apollo 实例时,请确保每个实例都已禁用此选项,否则显示器组合可能会被搞乱。当你不希望物理显示器的分辨率被改变或只需要串流虚拟显示器时,依旧推荐完全禁用此选项。", "dd_wa_hdr_toggle_desc": "当使用虚拟显示设备作为串流时,它可能会显示不正确的 HDR 颜色。启用此选项,Apollo 将尝试缓解这个问题。", "dd_wa_hdr_toggle": "为 HDR 启用高对比度临时修复", "double_refreshrate": "虚拟显示器刷新率翻倍", "double_refreshrate_desc": "创建虚拟显示器时刷新率翻倍,串流的视频刷新率不受影响。可能可以缓解某些电脑上出现的画面卡顿现象。", "ds4_back_as_touchpad_click": "映射回/选择触摸板点击", "ds4_back_as_touchpad_click_desc": "强制使用 DS4 模拟时,将“返回”/“选择”映射到触摸板点击", "enable_discovery": "启用自动发现", "enable_discovery_desc": "禁用后,你将只能通过输入服务端 IP 进行配对。", "enable_input_only_mode": "启用仅输入模式", "enable_input_only_mode_desc": "新增一个仅输入APP。启用时,应用列表在串流时将只显示当前正在运行的APP和仅输入APP。仅输入APP不会接收到任何画面/音频。可以用于用手机更方便地操作电视大屏串流或连接电视不支持的外设。", "enable_pairing": "启用配对", "enable_pairing_desc": "启用 Moonlight 客户端的配对。这允许客户端与主机进行身份验证并建立安全连接。", "encoder": "强制指定编码器", "encoder_desc": "强制指定一个特定编码器,否则 Apollo 将选择最佳可用选项。注意:如果您在 Windows 上指定了硬件编码器,它必须匹配连接显示器的 GPU。", "encoder_software": "软件", "envvar_compatibility_mode": "环境变量兼容模式", "envvar_compatibility_mode_desc": "启用环境变量兼容模式。这将修改某些环境变量的行为,以更好地与旧工具兼容。", "external_ip": "外部 IP", "external_ip_desc": "如果没有指定外部 IP 地址,Apollo 将自动检测外部 IP", "fallback_mode": "备用显示参数", "fallback_mode_desc": "当客户端未提供显示参数或通过 Web UI 启动应用时使用。格式:[宽度]x[高度]x[帧率]", "fallback_mode_error": "无效的备用显示参数。格式:[宽度]x[高度]x[帧率]", "fec_percentage": "FEC (前向纠错) 参数", "fec_percentage_desc": "每个视频帧中的错误纠正数据包百分比。较高的值可纠正更多的网络数据包丢失,但代价是增加带宽使用量。", "ffmpeg_auto": "auto -- 由 ffmpeg 决定(默认)", "file_apps": "应用程序配置文件", "file_apps_desc": "Apollo 保存应用程序配置的文件。", "file_state": "实时状态文件", "file_state_desc": "Apollo 保存当前状态的文件", "forward_rumble": "转发手柄震动", "forward_rumble_desc": "向客户端转发手柄震动信息", "gamepad": "模拟游戏手柄类型", "gamepad_auto": "自动选择选项", "gamepad_desc": "选择要在主机上模拟的游戏手柄类型", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "DS4选择选项", "gamepad_ds5": "DS5 (PS5)", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "DS4 手柄手动配置选项", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "命令准备工作", "global_prep_cmd_desc": "任何应用运行前/后要运行的命令列表。如果任何前置命令失败,应用的启动过程将被中止。", "global_state_cmd": "暂停/恢复命令", "global_state_cmd_desc": "任何应用暂停(所有客户端断开连接)或恢复(第一个客户端连接)时执行的命令列表。\n前置命令在恢复时执行,后置命令在暂停时执行。\n请确保在命令准备工作的后置命令中清理任何命令产生的副作用,当会话终止时,暂停命令将不会被执行。", "headless_mode": "无头模式", "headless_mode_desc": "启用后Apollo将支持无显示器模式,所有App都将在虚拟显示器中启动。", "hevc_mode": "HEVC 支持", "hevc_mode_0": "Apollo 将根据编码器能力通告对 HEVC 的支持(推荐)", "hevc_mode_1": "Apollo 将不会通告对 HEVC 的支持", "hevc_mode_2": "Apollo 将通告 HEVC Main 配置支持", "hevc_mode_3": "Apollo 将通告 HEVC Main 和 Main10 (HDR) 配置支持", "hevc_mode_desc": "允许客户端请求HEVC 主流或 HEVC Main10 视频流。 HEVC更需要编码,因此在使用软件编码时可能降低性能。", "hide_tray_controls": "隐藏托盘图标控制选项", "hide_tray_controls_desc": "不在托盘图标菜单内显示 \"Force Close\", \"Restart\" 和 \"Quit\"。", "high_resolution_scrolling": "高分辨率鼠标滚动支持", "high_resolution_scrolling_desc": "启用后,Apollo 将透传来自 Moonlight 客户端的高分辨率滚动事件。对于那些使用高分辨率滚动事件时滚动速度过快的旧版应用程序来说,禁用此功能非常有用。", "ignore_encoder_probe_failure": "忽略编码器探测失败", "ignore_encoder_probe_failure_desc": "即使探测编码器失败也允许继续推流。如果无可用编码器,这可能导致推流失败。", "install_steam_audio_drivers": "安装 Steam 音频驱动程序", "install_steam_audio_drivers_desc": "如果安装了 Steam,则会自动安装 Steam Streaming Speakers 驱动程序,以支持 5.1/7.1 环绕声和主机音频静音。", "isolated_virtual_display_option": "将虚拟显示器移动到显示布局的右下角", "isolated_virtual_display_option_desc": "将使虚拟显示器与其他显示器隔离,并将鼠标限制在虚拟屏幕中。这将重新排列显示器,使所有其他显示器位于虚拟显示器的左侧。", "keep_sink_default": "保持虚拟音频输出设备为默认设备", "keep_sink_default_desc": "是否强制保持虚拟音频输出设备为默认设备(当客户端设置禁止在PC上播放声音时有效)", "key_repeat_delay": "按键重复延迟", "key_repeat_delay_desc": "控制按键重复的速度。重复按键前的初始延迟(毫秒)。", "key_repeat_frequency": "按键重复频率", "key_repeat_frequency_desc": "按键每秒重复的次数。此配置选项支持小数。", "key_rightalt_to_key_win": "将右Alt 键映射到 Windows 键", "key_rightalt_to_key_win_desc": "您可能无法直接从 Moonlight 发送 Windows 键。在这种情况下,让 Apollo 认为右 Alt 键是 Windows 键可能会很有用。", "keyboard": "启用键盘输入", "keyboard_desc": "允许客户端使用键盘控制主机系统", "lan_encryption_mode": "局域网加密模式", "lan_encryption_mode_1": "为支持的客户端启用", "lan_encryption_mode_2": "强制所有客户端使用", "lan_encryption_mode_desc": "这将决定在本地网络上进行流媒体传输时何时使用加密。加密会降低流媒体性能,尤其是在功能较弱的主机和客户端上。", "legacy_ordering": "过时客户端 APP 排序支持", "legacy_ordering_desc": "启用对过时客户端的 APP 排序支持。可能在某些无法正确处理 UTF8 编码的客户端/脚本上导致问题。Artemis 客户端默认支持此功能。", "limit_framerate": "限制捕获帧率", "limit_framerate_desc": "将捕获帧率限制到客户端请求的帧率。当启用垂直同步且显示器刷新率与客户端刷新率不匹配时可能会跑不满请求的帧率。若禁用,可能会在某些客户端上导致延迟。", "locale": "本地化", "locale_desc": "用于 Apollo 用户界面的本地化设置。", "min_log_level": "日志级别", "min_log_level_0": "详细 (Verbose)", "min_log_level_1": "调试 (Debug)", "min_log_level_2": "信息 (Info)", "min_log_level_3": "警告 (Warning)", "min_log_level_4": "错误", "min_log_level_5": "致命 (Fatal)", "min_log_level_6": "无 (None)", "min_log_level_desc": "打印到标准输出的最小日志级别", "log_path": "日志文件路径", "log_path_desc": "Apollo 当前日志存储的文件。", "max_bitrate": "最大比特率", "max_bitrate_desc": "限制 Apollo 所编码的最大比特率 (Kbps)。 设 0 则总是使用客户端请求的值。", "minimum_fps_target": "最低 FPS 目标", "minimum_fps_target_desc": "设置最低的有效编码帧率。设 0 为自动。", "min_threads": "最低 CPU 线程数", "min_threads_desc": "提高该值会略微降低编码效率,但为了获得更多的 CPU 内核用于编码,通常是值得的。理想值是在您的硬件配置上以所需的串流设置进行可靠编码的最低值。", "misc": "杂项选项", "motion_as_ds4": "如果客户端报告游戏手柄存在陀螺仪,则模拟一个 DS4 游戏手柄", "motion_as_ds4_desc": "如果禁用,则在选择游戏手柄类型时不会考虑陀螺仪的存在。", "mouse": "启用鼠标输入", "mouse_desc": "允许客户端使用鼠标控制主机系统", "native_pen_touch": "原生笔/触摸支持", "native_pen_touch_desc": "启用后,Apollo 将透传来自 Moonlight 客户端的原生笔/触控事件。对于不支持原生笔/触控的旧版应用程序来说,禁用此功能非常有用。", "notify_pre_releases": "预发布通知", "notify_pre_releases_desc": "是否接收 Apollo 新预发布版本的通知", "nvenc_h264_cavlc": "在 H.264 中,偏向 CAVLC 而不是 CABAC", "nvenc_h264_cavlc_desc": "一种更简单的熵编码形式。相同质量的情况下,CAVLC 需要增加 10% 的比特率。只适用于非常老旧的解码设备。", "nvenc_intra_refresh": "帧内刷新", "nvenc_intra_refresh_desc": "启用帧内刷新以使部分客户端可以持续正常渲染 (e.g. Xbox Client)", "nvenc_latency_over_power": "倾向于较低的编码延迟而不是省电", "nvenc_latency_over_power_desc": "Apollo 在串流时请求最高的 GPU 核心频率,以降低编码延迟。 不建议禁用它,因为这会大大增加编码延迟。", "nvenc_opengl_vulkan_on_dxgi": "在 DXGI 基础上呈现 OpenGL/Vulkan", "nvenc_opengl_vulkan_on_dxgi_desc": "Apollo 无法以满帧速率捕获不处于DXGI顶部的全屏 OpenGL 和 Vulkan 程序。这是系统范围的设置,会在 Apollo 程序退出时恢复。", "nvenc_preset": "性能预设", "nvenc_preset_1": "(最快,默认)", "nvenc_preset_7": "(最慢)", "nvenc_preset_desc": "数字越大,压缩效果(给定比特率下的质量)越好,但代价是编码延迟增加。建议仅在受网络或解码器限制时更改,否则可通过提高比特率达到类似效果。", "nvenc_realtime_hags": "在硬件加速 GPU 调度中使用实时优先级", "nvenc_realtime_hags_desc": "目前,当启用 HAGS、使用实时优先级且 VRAM 利用率接近最大值时,NVIDIA 驱动程序可能会冻结编码器。禁用该选项可将优先级降至高,从而避免冻结,但代价是在 GPU 负载较高时捕捉性能会降低。", "nvenc_spatial_aq": "Spatial AQ - 空间自适应量化", "nvenc_spatial_aq_desc": "将较高的 QP 值分配给视频的平场区域。建议在以较低的比特率进行串流时启用。", "nvenc_spatial_aq_disabled": "禁用(更快,默认)", "nvenc_spatial_aq_enabled": "启用(较慢)", "nvenc_twopass": "双通过模式", "nvenc_twopass_desc": "添加二次编码。这样可以检测到更多的运动矢量,更好地分配整个帧的比特率,并更能防止比特率超过限制。不建议禁用它,因为这会导致偶尔的比特率超限和随后的丢包。", "nvenc_twopass_disabled": "禁用(最快,不推荐)", "nvenc_twopass_full_res": "全分辨率(较慢)", "nvenc_twopass_quarter_res": "1/4分辨率(更快,默认)", "nvenc_vbv_increase": "单帧 VBV/HRD 百分比增加", "nvenc_vbv_increase_desc": "默认情况下,Apollo 使用单帧 VBV/HRD,这意味着任何编码的视频帧大小都不会超过所请求的码率除以所请求的帧速率。放宽这一限制可能会带来好处,起到低延迟可变码率的作用,但如果网络没有缓冲空间来处理码率峰值,也可能导致数据包丢失。可接受的最大值为 400,相当于编码视频帧的大小上限增加到 5 倍。", "origin_web_ui_allowed": "允许的 Web UI 访问来源", "origin_web_ui_allowed_desc": "未被拒绝访问 Web UI 的远端地址来源", "origin_web_ui_allowed_lan": "仅局域网中的设备可以访问 Web UI", "origin_web_ui_allowed_pc": "只有本地主机才能访问Web UI", "origin_web_ui_allowed_wan": "任何人都可以访问 Web UI", "output_name_desc_unix": "在 Apollo 启动过程中,您将看到检测到的显示器列表。注意:您需要使用括号内的 ID 值。", "output_name_desc_windows": "手动指定用于抓取的显示。如果未设置,则捕获主显示。 注意:如果您在上面指定了GPU,则此显示必须连接到该GPU。可以使用以下命令找到相应的值:", "output_name_unix": "显示器编号", "output_name_windows": "输出名称", "ping_timeout": "Ping 超时", "ping_timeout_desc": "关闭串流前等待 Moonlight 数据的时间(以毫秒计)", "pkey": "私人密钥", "pkey_desc": "用于 Web UI 和 Moonlilght 客户端配对的私钥。为了最佳兼容性,这应该是一个 RSA-2048 私钥。", "port": "端口", "port_alert_1": "Apollo 不能使用低于1024 的端口! ", "port_alert_2": "超过 65535 的端口不可用!", "port_desc": "设置 Apollo 使用的端口族", "port_http_port_note": "使用此端口连接 Moonlight。", "port_note": "说明", "port_port": "端口", "port_protocol": "协议", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "暴露 Web UI 到公网存在安全风险!请自行承担风险!", "port_web_ui": "Web UI", "qp": "量化参数 (QP)", "qp_desc": "某些设备可能不支持恒定码率。对于这些设备,则使用 QP 代替。数值越大,压缩率越高,但质量越差。", "qsv_coder": "QSV 编码器 (H264)", "qsv_preset": "QSV 编码器预设", "qsv_preset_fast": "fast - 较快(较低质量)", "qsv_preset_faster": "faster - 更快(更低质量)", "qsv_preset_medium": "medium - 中等(默认)", "qsv_preset_slow": "slow - 较慢(较高质量)", "qsv_preset_slower": "slower - 更慢(更高质量)", "qsv_preset_slowest": "slowest - 最慢(最高质量)", "qsv_preset_veryfast": "veryfast - 最快 (最低质量)", "qsv_slow_hevc": "允许慢速 HEVC 编码", "qsv_slow_hevc_desc": "这可以让英特尔旧的 GPU 上的 HEVC 编码,代价是GPU 使用率更高和性能更差。", "res_fps_desc": "由 Apollo 通告的显示模式。 某些版本的 Moonlight,如 Moonlight-nx (Switch),依靠这些清单来确保支持所请求的分辨率和 fps。 此设置不会改变屏幕串流送至 Moonlight 的方式。", "resolutions": "通告分辨率", "restart_note": "正在重启 Apollo 以应用更改。", "server_cmd": "服务端命令", "server_cmd_desc": "配置命令列表,当串流时从客户端调用。", "stream_audio": "串流音频", "stream_audio_desc": "是否向客户端传送音频。在将客户端作为扩展显示器时比较有用。", "sunshine_name": "Apollo 主机名称", "sunshine_name_desc": "在 Moonlight 中显示的名称。如果未指定,则使用 PC 的主机名", "sw_preset": "软件编码预设", "sw_preset_desc": "在编码速度和压缩效率之间权衡。默认为 superfast - 超快。", "sw_preset_fast": "fast - 快速", "sw_preset_faster": "faster - 更快", "sw_preset_medium": "medium - 中等", "sw_preset_slow": "slow - 慢", "sw_preset_slower": "slower - 更慢", "sw_preset_superfast": "superfast - 超快(默认)", "sw_preset_ultrafast": "ultrafast - 极快", "sw_preset_veryfast": "veryfast - 非常快", "sw_preset_veryslow": "veryslow - 非常慢", "sw_tune": "软件编码调校", "sw_tune_animation": "animation -- 适合动画片;使用更高的去块和更多的参考帧", "sw_tune_desc": "调校选项,在预设后应用。默认值为 zerolatency。", "sw_tune_fastdecode": "fastdecode -- 通过禁用某些过滤器来加快解码速度", "sw_tune_film": "film -- 用于高质量的电影内容;降低去块", "sw_tune_grain": "grain -- 在处理旧的、有颗粒感的电影胶片材料时,保持其原有的颗粒结构。", "sw_tune_stillimage": "stillimage -- 适用于类似幻灯片的内容", "sw_tune_zerolatency": "zerolatency -- 适用于快速编码和低延迟串流(默认值)", "system_tray": "启用系统托盘", "system_tray_desc": "是否在系统托盘区域显示 Apollo 图标", "touchpad_as_ds4": "如果客户端报告游戏手柄存在触摸板,则模拟一个 DS4 游戏手柄", "touchpad_as_ds4_desc": "如果禁用,则在选择游戏手柄类型时不会考虑触摸板的存在。", "upnp": "UPnP", "upnp_desc": "为公网串流自动配置端口转发", "vaapi_strict_rc_buffer": "在AMD GPU上严格强制执行H.264/HEVC帧比特率限制", "vaapi_strict_rc_buffer_desc": "启用此选项可以在场景更改时避免在网络上放置帧,但在移动时可能会降低视频质量。", "virtual_sink": "虚拟音频输出设备", "virtual_sink_desc": "当客户端禁止PC播放音频时使用的音频输出设备。\n如未指定,则将自动选择。\n我们强烈建议将此字段留空,以便使用自动设备选择!", "virtual_sink_placeholder": "Steam Streaming Speakers", "vt_coder": "VideoToolbox 编码器", "vt_realtime": "VideoToolbox 实时编码", "vt_software": "VideoToolbox 软件编码", "vt_software_allowed": "允许", "vt_software_forced": "强制", "wan_encryption_mode": "公网加密模式", "wan_encryption_mode_1": "为支持的客户端启用(默认)", "wan_encryption_mode_2": "强制所有客户端使用", "wan_encryption_mode_desc": "这决定了在公网串流时是否加密。加密会降低串流性能,尤其是在性能较弱的主机和客户端上。" }, "login": { "save_password": "记住密码" }, "index": { "description": "Apollo 是供 Moonlight 使用的自建游戏串流服务。", "download": "下载", "installed_version_not_stable": "您正在运行一个 Apollo 的预发布版本。您可能会遇到 Bug 或其他问题。 请报告您遇到的任何问题。感谢您帮助将 Apollo 变得更好!", "loading_latest": "正在检测最新版本...", "new_pre_release": "有新的预发布版本可用!", "new_stable": "新的稳定版本已发布!", "startup_errors": "请注意!Apollo 在启动过程中检测到这些错误。我们强烈建议您在串流之前修复这些错误。", "version_dirty": "感谢您的帮助,让 Apollo 变得更好!", "version_latest": "您正在运行最新版本的 Apollo", "welcome": "你好,Apollo!" }, "navbar": { "applications": "应用程序", "configuration": "配置", "home": "首页", "password": "更改密码", "pin": "PIN 码", "theme_auto": "自动操作", "theme_dark": "深色", "theme_light": "浅色", "toggle_theme": "主题", "troubleshoot": "故障排除" }, "password": { "confirm_password": "确认密码", "current_creds": "当前账户信息", "new_creds": "新的账户信息", "new_username_desc": "如果不输入新的用户名, 用户名将保持不变", "password_change": "更改密码", "success_msg": "密码已成功更改!此页面即将重新加载,您的浏览器将要求您输入新的账户信息。" }, "permissions": { "input_controller": "手柄输入", "input_touch": "触摸输入", "input_pen": "笔输入", "input_mouse": "鼠标输入", "input_kbd": "键盘输入", "clipboard_set": "上传剪贴板", "clipboard_read": "获取剪贴板", "file_upload": "上传文件", "file_dwnload": "下载文件", "server_cmd": "服务端命令", "list": "列出APP", "view": "查看串流", "launch": "启动APP" }, "pin": { "allow_client_commands": "允许客户端命令", "allow_client_commands_desc": "允许客户端命令在运行此应用时执行。", "always_use_virtual_display": "总是创建虚拟显示器", "always_use_virtual_display_desc": "此设备连接时总是创建虚拟显示器。", "client_do_cmd": "客户端连入命令", "client_do_cmd_desc": "当此客户端连接时执行的命令。所有命令都以后台模式允许。", "client_undo_cmd": "客户端断开命令", "client_undo_cmd_desc": "当此客户端断开连接时执行的命令。所有命令都以后台模式允许。", "device_name": "设备名称", "display_mode_override": "显示模式覆盖", "display_mode_override_desc": "Apollo 将无视客户端请求的显示参数而使用此参数来配置(虚拟)显示器。留空则自动匹配。格式: [Width]x[Height]x[FPS]", "display_mode_override_error": "无效的显示模式。格式: [Width]x[Height]x[FPS]", "enable_legacy_ordering": "启用过时客户端 APP 排序", "enable_legacy_ordering_desc": "启用对过时客户端的 APP 排序支持。如果此客户端无法正确处理 UTF8 编码,请禁用。需要在高级设置中启用 \"过时客户端 APP 排序支持\" 选项。", "pair_failure": "配对失败:请检查 PIN 码是否正确输入", "pair_success": "成功!请检查 Moonlight 以继续", "pair_success_check_perm": "配对成功!请在下方手动授予客户端必要的权限。", "pin_pairing": "PIN 码配对", "send": "发送", "warning_msg": "请确保您可以掌控您正在配对的客户端。该软件可以完全控制您的计算机,请务必小心!", "otp_pairing": "OTP 配对", "generate_pin": "生成 PIN", "otp_passphrase": "一次性口令", "otp_expired": "已过期", "otp_expired_msg": "口令已过期,请重新请求。", "otp_success": "一次性 PIN 请求成功,3分钟内有效。", "otp_msg": "一次性口令目前仅支持最新的 Artemis 客户端使用。其他客户端请使用传统配对方式。", "otp_pair_now": "PIN 请求成功,是否一键配对?", "device_management": "设备管理", "device_management_desc": "管理已配对的设备及其权限。", "device_management_warning": "第一个配对的设备将拥有全部权限,其他新配对的设备将默认拥有受限的权限。", "save_client_error": "保存客户端时出错:", "unpair_all": "全部取消配对", "unpair_all_success": "全部取消配对成功!", "unpair_all_error": "取消配对时出错", "unpair_single_no_devices": "没有配对的设备。", "unpair_single_success": "然而,设备可能仍然处于活动会话中,使用上面的“强制关闭”按钮结束任何打开的会话。", "unpair_single_unknown": "未知客户端" }, "resource_card": { "github_discussions": "Github 讨论区", "github_stuttering_clinic": "卡顿诊所", "legal": "法律声明", "legal_desc": "继续使用本软件即表示您同意以下文档中的条款和条件。", "license": "许可协议", "lizardbyte_website": "LizardByte 网站", "resources": "参考资源", "resources_desc": "Apollo 相关资源!", "third_party_notice": "第三方声明" }, "troubleshooting": { "dd_reset": "重置持久显示设备设置", "dd_reset_desc": "如果Apollo 被卡住试图恢复更改的显示设备设置,您可以重置设置并手动恢复显示状态。", "dd_reset_error": "重置持久性时发生错误!", "dd_reset_success": "成功重置持久性!", "force_close": "强制结束运行", "force_close_desc": "如果 Moonlight 抱怨某个应用正在运行,强制关闭该应用应该可以解决问题。这也会刷新应用列表。", "force_close_error": "关闭应用时出错", "force_close_success": "应用关闭成功!", "logs": "日志", "logs_desc": "查看 Apollo 上传的日志", "logs_find": "查找...", "restart_apollo": "重启 Apollo", "restart_apollo_desc": "如果 Apollo 无法正常工作,可以尝试重新启动。这将终止任何正在进行的会话。", "restart_apollo_success": "Apollo 正在重启", "quit_apollo": "退出 Apollo", "quit_apollo_desc": "停止运行 Apollo。这将会终止任何正在进行的会话。", "quit_apollo_success": "Apollo 已成功退出。", "quit_apollo_success_ongoing": "Apollo 正在退出...", "quit_apollo_confirm": "确定要退出 Apollo 吗?如果没有其他操作方式,你将无法再次启动 Apollo。", "troubleshooting": "故障排除" }, "welcome": { "confirm_password": "确认密码", "create_creds": "在开始之前,我们需要您为访问 Web UI 设置一个新的用户名和密码。", "create_creds_alert": "需要下面的账户信息才能访问 Apollo 的 Web UI 。请妥善保存,因为你再也不会见到它们!", "greeting": "欢迎使用 Apollo!", "login": "登录", "welcome_success": "此页面将很快重新加载,您的浏览器将询问您新的账户信息", "login_success": "此页面将重新加载。" } } ================================================ FILE: src_assets/common/assets/web/public/assets/locale/zh_TW.json ================================================ { "_common": { "apply": "套用", "auto": "自動", "autodetect": "自動偵測(建議)", "beta": "(測試版)", "cancel": "取消", "disabled": "已停用", "disabled_def": "已停用(預設)", "disabled_def_cbox": "預設值:未勾選", "dismiss": "關閉", "do_cmd": "執行指令", "elevated": "提高權限", "enabled": "已啟用", "enabled_def": "已啟用(預設值)", "enabled_def_cbox": "預設值:已勾選", "error": "錯誤!", "note": "請注意:", "password": "密碼", "run_as": "以系統管理員身份執行", "save": "儲存", "see_more": "查看更多資訊", "success": "成功!", "undo_cmd": "復原指令", "username": "使用者名稱", "warning": "警告!" }, "apps": { "actions": "行動", "add_cmds": "新增指令", "add_new": "新增", "app_name": "應用程式名稱", "app_name_desc": "應用程式名稱(在 Moonlight 上顯示的名稱)", "applications_desc": "應用程式清單僅在用戶端重新啟動時更新", "applications_title": "應用程式一覽", "auto_detach": "即使應用程式瞬間關閉,仍會繼續串流。", "auto_detach_desc": "此功能會自動偵測那些在啟動其他程式或自身的另一個執行個體後,立即關閉的啟動器類應用程式。一旦偵測到這類應用程式,系統會將其視為獨立運行的應用程式。", "cmd": "指令", "cmd_desc": "這是要啟動的主要應用程式。如果留空,則不會啟動任何程式。", "cmd_note": "如果指令執行檔的路徑有空格,請將路徑放在引號中。", "cmd_prep_desc": "這是啟動應用程式前後要執行的指令清單。如果任何準備指令執行失敗,應用程式將無法啟動。", "cmd_prep_name": "指令準備", "covers_found": "找到封面圖片", "delete": "刪除", "detached_cmds": "獨立指令", "detached_cmds_add": "新增獨立指令", "detached_cmds_desc": "在背景執行的指令清單。", "detached_cmds_note": "如果指令執行檔的路徑有空格,請將路徑放在引號中。", "edit": "編輯", "env_app_id": "應用程式 ID", "env_app_name": "應用程式名稱", "env_client_audio_config": "用戶端要求的音訊設定 (2.0/5.1/7.1)", "env_client_enable_sops": "用戶端要求將遊戲進行最佳化,以達到最佳串流效果 (true/false)", "env_client_fps": "用戶端要求的 FPS(整數)", "env_client_gcmap": "要求的遊戲手把遮罩,使用位元集合/位元欄位格式 (整數)", "env_client_hdr": "HDR 由用戶端啟用 (true/false)", "env_client_height": "用戶端要求的高度(整數)", "env_client_host_audio": "用戶端要求的主機音訊 (true/false)", "env_client_width": "用戶端要求的寬度(整數)", "env_displayplacer_example": "範例 - 用於解析度自動化的 displayplacer:", "env_qres_example": "範例 - 用於解析度自動化的 QRes:", "env_qres_path": "qres 路徑", "env_var_name": "變數名稱", "env_vars_about": "關於環境變數", "env_vars_desc": "所有指令預設會獲得這些環境變數:", "env_xrandr_example": "範例 - 用於解析度自動化的 Xrandr:", "exit_timeout": "結束逾時設定", "exit_timeout_desc": "當要求結束時,等待所有應用程式處理程序正常結束的秒數。如果未設定,預設會等待最多 5 秒。如果設為 0,應用程式將立即終止。", "find_cover": "尋找封面圖片", "global_prep_desc": "啟用/停用此應用程式的全域準備指令執行。", "global_prep_name": "全域準備指令", "image": "圖片", "image_desc": "將傳送至用戶端的應用程式圖示/圖片/圖片路徑。圖片必須是 PNG 格式。如果未設定,Sunshine 將傳送預設的盒狀圖示。", "loading": "載入中…", "name": "名稱", "output_desc": "指令輸出的儲存檔案。如果未指定,輸出將被忽略", "output_name": "輸出", "run_as_desc": "某些需要管理員權限才能正常運行的應用程式,可能需要這個設定。", "wait_all": "繼續串流,直到所有應用程式處理程序結束", "wait_all_desc": "這會繼續串流,直到應用程式啟動的所有處理程序都結束。如果未勾選,當最初的應用程式處理程序結束時,串流就會停止,即使還有其他處理程序在運行。", "working_dir": "工作目錄", "working_dir_desc": "這是要傳遞給處理程序的工作目錄。例如,有些應用程式會使用這個目錄來搜尋設定檔。如果沒有設定,Sunshine 會自動使用指令所在的目錄" }, "config": { "adapter_name": "顯示卡名稱", "adapter_name_desc_linux_1": "手動指定用於擷取的 GPU。", "adapter_name_desc_linux_2": "找出所有支援 VAAPI 的裝置", "adapter_name_desc_linux_3": "將 ``renderD129`` 替換為上面提到的裝置,以列出裝置的名稱和功能。要被 Sunshine 支援,裝置至少需要具備以下條件:", "adapter_name_desc_windows": "手動指定用於擷取的 GPU。如果未設定,系統會自動選擇 GPU。我們強烈建議保持此欄位為空,以使用自動 GPU 選擇!注意:此 GPU 必須已連接顯示器並開啟電源。可以使用以下指令來查找適當的值:", "adapter_name_placeholder_windows": "Radeon RX 580 系列", "add": "新增", "address_family": "位址族群", "address_family_both": "IPv4+IPv6", "address_family_desc": "設定 Sunshine 使用的位址族群", "address_family_ipv4": "僅 IPv4", "always_send_scancodes": "永遠傳送掃描碼", "always_send_scancodes_desc": "傳送掃描碼可以增強與遊戲和應用程式的相容性,但可能會導致某些未使用美式英語鍵盤佈局的用戶端輸入錯誤。若某些應用程式的鍵盤輸入完全無效,請啟用此選項。若用戶端的鍵盤輸入在主機端產生錯誤輸入,則請停用此選項。", "amd_coder": "AMF 編碼器 (H264)", "amd_coder_desc": "允許您選擇熵編碼,以優先品質或編碼速度。僅限 H.264。", "amd_enforce_hrd": "AMF 假想參考解碼器 (HRD) 強制執行", "amd_enforce_hrd_desc": "增加速率控制的限制,以符合 HRD 模型的要求。這可大幅減少比特率溢出,但可能會在某些卡上造成編碼假象或降低品質。", "amd_preanalysis": "AMF 預分析", "amd_preanalysis_desc": "這可進行速率控制預分析,以增加編碼延遲為代價來提高品質。", "amd_quality": "AMF 品質", "amd_quality_balanced": "balanced—平衡(預設)", "amd_quality_desc": "這可以控制編碼速度和品質之間的平衡。", "amd_quality_group": "AMF 品質設定", "amd_quality_quality": "quality—偏好品質", "amd_quality_speed": "speed—偏好速度", "amd_rc": "AMF 速率控制", "amd_rc_cbr": "cbr—固定位元率(如果啟用 HRD,建議使用)", "amd_rc_cqp": "cqp—常數 qp 模式", "amd_rc_desc": "這個選項控制了速率控制方法,確保不超過用戶端的位元率目標。'cqp' 不適用於位元率目標設定,除了 'vbr_latency' 外,其他選項依賴 HRD 強制執行來幫助限制位元率溢出。", "amd_rc_group": "AMF 速率控制設定", "amd_rc_vbr_latency": "vbr_latency—受延遲限制的可變位元率(如果停用 HRD,建議使用此選項;預設)", "amd_rc_vbr_peak": "vbr_peak—峰值受限的可變位元率", "amd_usage": "AMF 使用情況", "amd_usage_desc": "這會設定基本的編碼設定檔。以下顯示的所有選項都會覆蓋部分設定檔的設定,但設定檔還包含其他無法在其他地方調整的隱藏設定。", "amd_usage_lowlatency": "lowlatency—低延遲(最快)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality—低延遲、高品質(快速)", "amd_usage_transcoding": "transcoding—轉碼(最慢)", "amd_usage_ultralowlatency": "ultralowlatency—超低延遲(最快;預設值)", "amd_usage_webcam": "webcam—網路攝影機(慢速)", "amd_vbaq": "AMF 基於方差的自適應量化 (VBAQ)", "amd_vbaq_desc": "人類的視覺系統通常對高度紋理區域中的壓縮失真較不敏感。在 VBAQ 模式下,像素變異用於指示空間紋理的複雜度,使編碼器能夠將更多位元分配給較平滑的區域。啟用此功能可在某些內容上提升主觀視覺品質。", "apply_note": "點選「套用」以重新啟動 Sunshine 並應用變更。這將終止所有正在進行的工作階段。", "audio_sink": "音訊水槽", "audio_sink_desc_linux": "用於 Audio Loopback 的音訊輸出的名稱。如果您沒有指定這個變數,pulseaudio 將會選擇預設的監視裝置。您可以使用以下任一指令找出音訊輸出裝置的名稱:", "audio_sink_desc_macos": "用於音訊環回的音訊槽名稱。由於系統限制,Sunshine 只能在 macOS 上存取麥克風。要使用 Soundflower 或 BlackHole 串流系統音訊。", "audio_sink_desc_windows": "手動指定要擷取的特定音訊裝置。若未設定,系統會自動選擇裝置。我們強烈建議將此欄位留空以使用自動裝置選擇!若您有多個名稱相同的音訊裝置,您可以使用以下指令來取得裝置 ID:", "audio_sink_placeholder_macos": "黑洞 2ch", "audio_sink_placeholder_windows": "揚聲器(高解析度音訊裝置)", "av1_mode": "AV1 支援", "av1_mode_0": "Sunshine 將根據編碼器功能來宣告是否支援 AV1(建議)", "av1_mode_1": "Sunshine 不會宣告支援 AV1", "av1_mode_2": "Sunshine 將宣告支援 AV1 Main 8 位元設定檔", "av1_mode_3": "Sunshine 將宣告支援 AV1 Main 8 位元和 10 位元 (HDR) 設定檔", "av1_mode_desc": "允許用戶端要求 AV1 Main 8 位元或 10 位元視訊串流。AV1 的編碼較耗費 CPU,因此使用軟體編碼時,啟用此功能可能會降低效能。", "back_button_timeout": "主畫面/導覽按鈕模擬超時", "back_button_timeout_desc": "如果按住 Back/Select 按鈕達到指定的毫秒數,系統會模擬 Home/Guide 按鈕的按下動作。若設定為小於 0(預設值),則按住 Back/Select 按鈕不會模擬 Home/Guide 按鈕。", "capture": "強制使用特定的擷取方式", "capture_desc": "在自動模式下,Sunshine 會使用第一個有效的驅動程式。NvFBC 需要已修補的 nvidia 驅動程式。", "cert": "憑證", "cert_desc": "用於 Web UI 和 Moonlight 用戶端配對的憑證。為了確保最佳相容性,建議使用 RSA-2048 公鑰。", "channels": "最大連線用戶端數量", "channels_desc_1": "Sunshine 可讓單一串流工作階段同時與多個裝置共享。", "channels_desc_2": "某些硬體編碼器可能會因多重串流而受到性能限制。", "coder_cabac": "cabac—上下文自適應二元算術編碼,更高的品質", "coder_cavlc": "cavlc—上下文自適應可變長度編碼 - 解碼速度較快", "configuration": "組態", "controller": "啟用遊戲手把輸入", "controller_desc": "允許訪客使用遊戲手把或遊戲控制器操作主機系統。", "credentials_file": "憑證檔案", "credentials_file_desc": "將用戶名稱/密碼儲存在與 Sunshine 狀態檔案不同的位置。", "dd_config_ensure_active": "自動啟用顯示器", "dd_config_ensure_only_display": "停用其他顯示器,並只啟用指定的顯示器", "dd_config_ensure_primary": "自動啟用顯示器並設定為主要顯示器", "dd_configuration_option": "裝置組態", "dd_config_revert_delay": "設定回復延遲", "dd_config_revert_delay_desc": "當應用程式關閉或最後一個工作階段結束時,將會額外等待的延遲時間再恢復設定,以毫秒為單位。這樣做的主要目的是讓在快速切換應用程式時能夠更順暢。", "dd_config_revert_on_disconnect": "斷線時恢復設定", "dd_config_revert_on_disconnect_desc": "在所有用戶端斷線時恢復設定,而不是應用程式關閉或最後一個工作階段結束時。", "dd_config_verify_only": "確認顯示器已啟用", "dd_hdr_option": "HDR", "dd_hdr_option_auto": "根據用戶端的要求開啟/關閉 HDR 模式(預設值)", "dd_hdr_option_disabled": "不更改 HDR 設定", "dd_manual_refresh_rate": "手動更新率", "dd_manual_resolution": "手動解析度", "dd_mode_remapping": "顯示模式重新映射", "dd_mode_remapping_add": "新增重新映射項目", "dd_mode_remapping_desc_1": "指定重新映射項目以將請求的解析度和/或更新率更改為其他值。", "dd_mode_remapping_desc_2": "清單會從上到下迭代,並使用第一個匹配項目。", "dd_mode_remapping_desc_3": "「要求的」欄位可以留空,以匹配任何要求的值。", "dd_mode_remapping_desc_4_final_values_mixed": "必須指定至少一個「最終」欄位。未指定的解析度或更新率將不會被更改。", "dd_mode_remapping_desc_4_final_values_non_mixed": "必須指定「最終」欄位,且不能為空。", "dd_mode_remapping_desc_5_sops_mixed_only": "必須在 Moonlight 用戶端啟用「最佳化遊戲設定」選項,否則指定了解析度欄位的項目將被跳過。", "dd_mode_remapping_desc_5_sops_resolution_only": "必須在 Moonlight 用戶端啟用「最佳化遊戲設定」選項,否則映射將被跳過。", "dd_mode_remapping_final_refresh_rate": "最終更新率", "dd_mode_remapping_final_resolution": "最終解析度", "dd_mode_remapping_requested_fps": "要求的 FPS", "dd_mode_remapping_requested_resolution": "要求的解析度", "dd_options_header": "進階顯示裝置選項", "dd_refresh_rate_option": "更新率", "dd_refresh_rate_option_auto": "使用用戶端提供的 FPS 值(預設)", "dd_refresh_rate_option_disabled": "不變更更新率", "dd_refresh_rate_option_manual": "使用手動輸入的更新率", "dd_resolution_option": "解析度", "dd_resolution_option_auto": "使用用戶端提供的解析度(預設)", "dd_resolution_option_disabled": "不更改解析度", "dd_resolution_option_manual": "使用手動輸入的解析度", "dd_resolution_option_ogs_desc": "必須在 Moonlight 用戶端啟用「最佳化遊戲設定」選項,才能讓這個功能正常運作。", "dd_wa_hdr_toggle_delay_desc_1": "使用虛擬顯示裝置 (VDD) 進行串流時,可能會不正確顯示 HDR 顏色。陽光可以嘗試關閉 HDR,然後再開啟,以減少此問題。", "dd_wa_hdr_toggle_delay_desc_2": "如果該值設為 0,則會停用變通(預設)。如果值介於 0 和 3000 毫秒之間,Sunshine 會關閉 HDR,等待指定的時間,然後再開啟 HDR。在大多數情況下,建議的延遲時間約為 500 毫秒。", "dd_wa_hdr_toggle_delay_desc_3": "除非您真的有 HDR 問題,否則請勿使用此變通技術,因為它會直接影響串流的啟動時間!", "dd_wa_hdr_toggle_delay": "HDR 的高對比度解決方案", "ds4_back_as_touchpad_click": "地圖 Back/Select 至觸控板點選", "ds4_back_as_touchpad_click_desc": "當強制啟用 DS4 模擬時,將返回/選擇按鈕映射為觸控板點擊", "ds5_inputtino_randomize_mac": "隨機化虛擬控制器 MAC", "ds5_inputtino_randomize_mac_desc": "控制器註冊時,使用隨機 MAC 而非控制器內部索引,以避免在用戶端交換控制器時混淆不同控制器的組態設定。", "encoder": "強制指定編碼器", "encoder_desc": "強制指定特定的編碼器,否則 Sunshine 將選擇最佳的可用選項。注意:如果您在 Windows 上指定硬體編碼器,則必須與顯示器連接的 GPU 符合。", "encoder_software": "軟體", "external_ip": "外部 IP", "external_ip_desc": "如果未提供外部 IP 位址,Sunshine 會自動偵測外部 IP 位址。", "fec_percentage": "FEC 比例", "fec_percentage_desc": "每個視訊影格中每個資料封包的錯誤修正封包百分比。較高的值可以修正更多的網路封包遺失,但代價是增加頻寬使用量。", "ffmpeg_auto": "auto—由 ffmpeg 決定(預設)", "file_apps": "應用程式檔案", "file_apps_desc": "Sunshine 目前的應用程式所儲存的檔案。", "file_state": "狀態檔案", "file_state_desc": "儲存目前 Sunshine 狀態的檔案", "gamepad": "模擬遊戲手把類型", "gamepad_auto": "自動選擇選項", "gamepad_desc": "選擇要在主機上模擬的遊戲手把類型", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "DS4 選擇選項", "gamepad_ds5": "DS5 (PS5)", "gamepad_ds5_manual": "DS5 選擇選項", "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "手動 DS4 選項", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "指令準備", "global_prep_cmd_desc": "設定在執行任何應用程式之前或之後執行的指令清單。如果任何指定的準備指令失敗,應用程式的啟動程序將會中止。", "hevc_mode": "HEVC 支援", "hevc_mode_0": "Sunshine 將根據編碼器的能力宣告是否支援 HEVC(建議)", "hevc_mode_1": "Sunshine 不會宣告支援 HEVC", "hevc_mode_2": "Sunshine 將宣告支援 HEVC Main 設定檔", "hevc_mode_3": "Sunshine 會宣告支援 HEVC Main 和 Main10 (HDR) 設定檔", "hevc_mode_desc": "允許用戶端要求 HEVC Main 或 HEVC Main10 視訊串流。HEVC 的編碼較耗費 CPU,因此使用軟體編碼時,啟用此功能可能會降低效能。", "high_resolution_scrolling": "支援高解析度捲動", "high_resolution_scrolling_desc": "啟用後,Sunshine 會傳遞來自 Moonlight 用戶端的高解析度捲動事件。對於某些在高解析度捲動時捲動速度過快的舊應用程式,建議將此選項停用。", "install_steam_audio_drivers": "安裝 Steam 音訊驅動程式", "install_steam_audio_drivers_desc": "如果已安裝 Steam,這將會自動安裝 Steam Streaming Speakers 驅動程式,以支援 5.1/7.1 環繞音效和主機音訊靜音。", "key_repeat_delay": "按鍵重複延遲", "key_repeat_delay_desc": "控制按鍵重複的速度。設定按鍵重複前的初始延遲時間,以毫秒為單位。", "key_repeat_frequency": "按鍵重複頻率", "key_repeat_frequency_desc": "每秒按鍵重複的頻率。此選項支援小數點。", "key_rightalt_to_key_win": "將右 Alt 鍵映射為 Windows 鍵", "key_rightalt_to_key_win_desc": "Moonlight 可能無法直接發送 Windows 鍵。在這種情況下,讓 Sunshine 認為右 Alt 鍵是 Windows 鍵可能會很有用", "keybindings": "鍵盤綁定", "keyboard": "啟用鍵盤輸入", "keyboard_desc": "允許訪客使用鍵盤控制主機系統", "lan_encryption_mode": "區域網路加密模式", "lan_encryption_mode_1": "當用戶端支援時啟用", "lan_encryption_mode_2": "所有用戶端都需要", "lan_encryption_mode_desc": "這會決定在本地網路上進行串流時何時使用加密。加密可能會降低串流效能,特別是在較不強大的主機和用戶端上。", "locale": "語系", "locale_desc": "Sunshine 使用的使用者介面語言設定。", "log_path": "記錄檔路徑", "log_path_desc": "儲存目前 Sunshine 記錄的檔案。", "max_bitrate": "最大位元率", "max_bitrate_desc": "Sunshine 會以最大位元率(單位為 Kbps)來編碼串流。如果設為0,則會使用Moonlight所要求的位元率。", "minimum_fps_target": "最低 FPS 目標", "minimum_fps_target_desc": "串流可達到的最低有效 FPS。0 的值會被視為串流 FPS 的一半左右。如果您串流 24 或 30fps 的內容,建議設定為 20。", "min_log_level": "日誌層級", "min_log_level_0": "繁體", "min_log_level_1": "除錯", "min_log_level_2": "資訊", "min_log_level_3": "警告", "min_log_level_4": "錯誤", "min_log_level_5": "致命", "min_log_level_6": "無", "min_log_level_desc": "列印到標準輸出的最小記錄層級", "min_threads": "最低 CPU 執行緒數", "min_threads_desc": "增加該值會稍微降低編碼效率,但為了能使用更多 CPU 核心進行編碼,這樣的折衷通常是值得的。理想的值是在您的硬體上,能以您所需的串流設定進行可靠編碼的最低值。", "misc": "其他選項", "motion_as_ds4": "如果用戶端的遊戲手把報告有運動感應器,則會模擬 DS4 遊戲手把", "motion_as_ds4_desc": "如果禁用,則在選擇遊戲手把類型時不會考慮運動感應器的存在。", "mouse": "啟用滑鼠輸入", "mouse_desc": "允許訪客使用滑鼠控制主機系統", "native_pen_touch": "原生筆/觸控支援", "native_pen_touch_desc": "啟用後,Sunshine 將從 Moonlight 用戶端傳遞本機筆觸事件。對於沒有原生筆/觸控支援的舊版應用程式來說,停用此功能可能很有用。", "notify_pre_releases": "發佈前通知", "notify_pre_releases_desc": "是否接收 Sunshine 的新預發佈版本通知", "nvenc_h264_cavlc": "在 H.264 中選擇 CAVLC 而非 CABAC", "nvenc_h264_cavlc_desc": "較簡單的熵編碼形式。CAVLC需要約多10%的位元率才能達到相同的畫質。這僅對非常舊的解碼裝置有影響。", "nvenc_latency_over_power": "相較於省電,更傾向於較低的編碼延遲", "nvenc_latency_over_power_desc": "Sunshine會在進行串流時請求最大 GPU 時脈速度,以減少編碼延遲。建議不要停用此選項,因為這樣可能會顯著增加編碼延遲。", "nvenc_opengl_vulkan_on_dxgi": "在 DXGI 之上呈現 OpenGL/Vulkan", "nvenc_opengl_vulkan_on_dxgi_desc": "Sunshine 無法以完整幀率捕捉全螢幕的 OpenGL 和 Vulkan 程式,除非它們顯示在 DXGI 之上。這是系統範圍的設定,會在Sunshine程式退出時還原。", "nvenc_preset": "性能預設參數", "nvenc_preset_1": "(最快,預設值)", "nvenc_preset_7": "(最慢)", "nvenc_preset_desc": "數值越高,在增加編碼延遲的情況下,可提高壓縮率(在指定位元率下的品質)。建議僅在受限於網路或解碼器時才進行調整,否則可以透過增加位元率來達成類似的效果。", "nvenc_realtime_hags": "在硬體加速 GPU 排程中使用即時優先順序", "nvenc_realtime_hags_desc": "目前NVIDIA 驅動程式在啟用 HAGS、使用即時優先權且 VRAM 使用率接近最大值時,可能會在編碼器中凍結。停用此選項會將優先權降低至高,藉此避開凍結,但在 GPU 重度負載時擷取效能會降低。", "nvenc_spatial_aq": "Spatial AQ", "nvenc_spatial_aq_desc": "為視訊的平坦區域指定較高的 QP 值。建議在以較低位元率串流時啟用。", "nvenc_twopass": "兩次編碼模式", "nvenc_twopass_desc": "新增初步編碼過程,能夠檢測更多的運動向量,將位元率更均勻地分佈於畫面,並更精確地遵守位元率限制。建議不要停用這個功能,因為停用後可能會偶爾出現位元率超過限制,並導致資料包丟失。", "nvenc_twopass_disabled": "停用(最快,不建議使用)", "nvenc_twopass_full_res": "全解析度(較慢)", "nvenc_twopass_quarter_res": "四分之一解析度(更快,預設值)", "nvenc_vbv_increase": "單幅 VBV/HRD 百分比增加", "nvenc_vbv_increase_desc": "預設 sunshine 使用單幀 VBV/HRD,這表示任何編碼視訊幀大小都不會超過要求的位元率除以要求的幀速率。放寬此限制可能有益並可作為低延遲的可變位元率,但如果網路沒有緩衝空間處理比特率峰值,也可能導致封包遺失。可接受的最大值是 400,相當於 5 倍增加的編碼視訊畫格上限。", "origin_web_ui_allowed": "允許存取 Web UI 的來源", "origin_web_ui_allowed_desc": "未被拒絕存取 Web UI 的遠端端點位址來源", "origin_web_ui_allowed_lan": "只有區域網路中的人可以存取 Web UI", "origin_web_ui_allowed_pc": "只有 localhost 可以存取 Web UI", "origin_web_ui_allowed_wan": "任何人都可以存取 Web UI", "output_name": "顯示 ID", "output_name_desc_unix": "在 Sunshine 啟動時,您應該會看到檢測到的顯示器清單。請注意:需要使用括弧內的 ID 值。以下是範例,實際輸出可以在「故障排除」分頁中找到。", "output_name_desc_windows": "手動指定要用於擷取的顯示器設備 ID。如果未設定,則擷取主要顯示器。注意:如果您在上方指定了 GPU,則此顯示器必須連接到該 GPU。在 Sunshine 啟動時,您應該會看到檢測到的顯示器清單。以下是範例,實際輸出可以在「故障排除」分頁中找到。", "ping_timeout": "Ping 逾時", "ping_timeout_desc": "在關閉串流前,等待來自 Moonlight 的資料,以毫秒為單位", "pkey": "私人金鑰", "pkey_desc": "用於 Web UI 和 Moonlight 用戶端配對的私鑰。為了確保最佳相容性,建議使用 RSA-2048 私鑰。", "port": "連接埠", "port_alert_1": "Sunshine 不能使用低於 1024 的連接埠!", "port_alert_2": "65535 以上的連接埠無法使用!", "port_desc": "設定 Sunshine 使用的連接埠範圍", "port_http_port_note": "使用此連接埠與 Moonlight 連線。", "port_note": "注意事項", "port_port": "連接埠", "port_protocol": "通訊協定", "port_tcp": "TCP", "port_udp": "UDP", "port_warning": "將 Web UI 暴露於網際網路存在安全風險!請自行承擔風險!", "port_web_ui": "Web UI", "qp": "量化參數", "qp_desc": "某些裝置可能不支援 Constant Bit Rate。對於這些裝置,會使用 QP 來取代。值越高表示壓縮越多,但品質越低。", "qsv_coder": "QuickSync 編碼器(H264)", "qsv_preset": "QuickSync 預設值", "qsv_preset_fast": "快(低畫質)", "qsv_preset_faster": "更快(品質較低)", "qsv_preset_medium": "中(預設值)", "qsv_preset_slow": "慢(高畫質)", "qsv_preset_slower": "速度較慢(品質較佳)", "qsv_preset_slowest": "最慢(最高畫質)", "qsv_preset_veryfast": "最快(最低畫質)", "qsv_slow_hevc": "允許慢速 HEVC 編碼", "qsv_slow_hevc_desc": "這樣可以在較舊的 Intel GPU 上進行 HEVC 編碼,但代價是 GPU 使用量較高,效能較差。", "restart_note": "Sunshine 正在重新啟動以套用變更。", "stream_audio": "串流音訊", "stream_audio_desc": "是否串流音訊。停用此功能對於串流無頭顯示器作為第二台顯示器很有用。", "sunshine_name": "Sunshine 名稱", "sunshine_name_desc": "Moonlight 顯示的名稱。如果未指定,則使用 PC 的主機名稱。", "sw_preset": "SW 預設值", "sw_preset_desc": "最佳化編碼速度(每秒編碼幀數)與壓縮效率(位元流中每位元的品質)之間的權衡。預設為 superfast。", "sw_preset_fast": "快速", "sw_preset_faster": "更快", "sw_preset_medium": "中等", "sw_preset_slow": "慢速", "sw_preset_slower": "更慢", "sw_preset_superfast": "超快(預設)", "sw_preset_ultrafast": "超快", "sw_preset_veryfast": "非常快", "sw_preset_veryslow": "非常慢速", "sw_tune": "SW 調音", "sw_tune_animation": "animation—適用於卡通,使用較多的去區塊效應處理和更多的參考影格。", "sw_tune_desc": "調整選項,在預設值之後套用。預設為 zerolatency。", "sw_tune_fastdecode": "fastdecode—藉由停用某些過濾器來加快解碼速度", "sw_tune_film": "film—適用於高品質的電影內容,降低去區塊效應處理。", "sw_tune_grain": "grain—保留老電影畫面的顆粒結構", "sw_tune_stillimage": "stillimage—適用於類似投影片的內容。", "sw_tune_zerolatency": "zerolatency—適合快速編碼和低延遲串流(預設值)", "system_tray": "啟用系統匣", "system_tray_desc": "在系統匣中顯示圖示,並顯示桌面通知", "touchpad_as_ds4": "當用戶端的遊戲手把報告存在觸控板時,模擬 DS4 遊戲手把", "touchpad_as_ds4_desc": "若停用,選擇遊戲手把類型時將不會考慮觸控板的存在。", "upnp": "UPnP", "upnp_desc": "自動設定透過網際網路串流的連接埠轉發", "vaapi_strict_rc_buffer": "在 AMD GPU 上嚴格執行 H.264/HEVC 的幀位元率限制", "vaapi_strict_rc_buffer_desc": "啟用此選項可以避免場景變換時在網路上發生畫面掉幀,但可能會降低畫面移動時的影像品質。", "virtual_sink": "虛擬音訊輸出", "virtual_sink_desc": "手動指定要使用的虛擬音訊裝置。如果未設定,則會自動選擇裝置。我們強烈建議將此欄位留空,以使用自動裝置選擇!", "virtual_sink_placeholder": "Steam 串流喇叭", "vt_coder": "VideoToolbox 編碼器", "vt_realtime": "VideoToolbox 即時編碼", "vt_software": "VideoToolbox 軟體編碼", "vt_software_allowed": "允許", "vt_software_forced": "強制", "wan_encryption_mode": "WAN 加密模式", "wan_encryption_mode_1": "對支援的用戶端啟用(預設)", "wan_encryption_mode_2": "所有用戶端都需要", "wan_encryption_mode_desc": "這會決定在網際網路上串流時,何時會使用加密。加密可能會降低串流效能,尤其是在效能較低的主機和用戶端上。" }, "index": { "description": "Sunshine 是 Moonlight 的自架遊戲串流主機。", "download": "下載", "installed_version_not_stable": "您正在運行的是 Sunshine 的預發行版本。您可能會遇到錯誤或其他問題。請報告您遇到的任何問題。感謝您的協助,讓 Sunshine 成為更好的軟體!", "loading_latest": "正在載入最新版本…", "new_pre_release": "提供新的預發行版本!", "new_stable": "有新的穩定版本可用!", "startup_errors": "注意! Sunshine 在啟動時檢測到這些錯誤。我們強烈建議在開始串流之前修復它們。", "version_dirty": "感謝您的協助,讓 Sunshine 成為更好的軟體!", "version_latest": "您正在執行最新版本的 Sunshine", "welcome": "你好,Sunshine!" }, "navbar": { "applications": "應用程式", "configuration": "組態", "home": "首頁", "password": "變更密碼", "pin": "Pin 碼", "theme_auto": "自動", "theme_dark": "深色主題", "theme_light": "淺色主題", "toggle_theme": "主題", "troubleshoot": "疑難排解" }, "password": { "confirm_password": "確認密碼", "current_creds": "目前的憑證", "new_creds": "新憑證", "new_username_desc": "如果未指定,使用者名稱不會變更", "password_change": "密碼變更", "success_msg": "密碼已成功變更!此頁面將很快重新載入,您的瀏覽器將要求輸入新的憑證。" }, "pin": { "device_name": "裝置名稱", "pair_failure": "配對失敗:請確認 PIN 碼是否輸入正確", "pair_success": "成功!請檢查 Moonlight 來繼續", "pin_pairing": "PIN 碼配對", "send": "發送", "warning_msg": "請確保您可以存取要配對的用戶端。此軟體可讓對方完全控制您的電腦,請小心使用!" }, "resource_card": { "github_discussions": "GitHub 討論區", "legal": "法律資訊", "legal_desc": "繼續使用本軟體即表示您同意下列文件中的條款和條件。", "license": "許可證", "lizardbyte_website": "LizardByte 網站", "resources": "資源", "resources_desc": "Sunshine 相關資源!", "third_party_notice": "第三方通知" }, "troubleshooting": { "dd_reset": "重置為既存的顯示裝置設定", "dd_reset_desc": "如果 Sunshine 在嘗試恢復更改過的顯示裝置設定時卡住,您可以重置設定並手動恢復顯示狀態。", "dd_reset_error": "重置為既存設定時發生錯誤!", "dd_reset_success": "成功重置為既存設定!", "force_close": "強制關閉", "force_close_desc": "如果 Moonlight 抱怨目前的應用程式已經在執行,強制關閉該應用程式應該可以解決問題。", "force_close_error": "關閉應用程式時發生錯誤", "force_close_success": "應用程式已成功結束!", "logs": "日誌", "logs_desc": "查看 Sunshine 上傳的日誌", "logs_find": "尋找…", "restart_sunshine": "重新啟動 Sunshine", "restart_sunshine_desc": "如果 Sunshine 沒有正常運作,您可以嘗試重新啟動。這會終止所有正在執行的工作階段。", "restart_sunshine_success": "Sunshine 正在重新啟動", "troubleshooting": "疑難排解", "unpair_all": "全部解除配對", "unpair_all_error": "解除配對時發生錯誤", "unpair_all_success": "已取消配對所有裝置。", "unpair_desc": "刪除已配對的裝置。未配對的裝置若有使用中的工作階段,將保持連線,但無法啟動或恢復工作階段。", "unpair_single_no_devices": "沒有已配對的裝置。", "unpair_single_success": "然而,該裝置仍可能處於使用中的工作階段。請使用上方的「強制關閉」按鈕結束任何使用中的工作階段。", "unpair_single_unknown": "未知的用戶端", "unpair_title": "解除配對裝置" }, "welcome": { "confirm_password": "確認密碼", "create_creds": "在開始之前,我們需要您建立新的使用者名稱和密碼,以便存取 Web UI。", "create_creds_alert": "存取 Sunshine 的 Web UI 需要以下憑證。請妥善保管,因為您將無法再查看這些憑證!", "greeting": "歡迎來到 Sunshine!", "login": "登入", "welcome_success": "此頁面將很快重新載入,您的瀏覽器會要求您提供新的憑證" } } ================================================ FILE: src_assets/common/assets/web/template_header.html ================================================ Apollo ================================================ FILE: src_assets/common/assets/web/theme.js ================================================ const getStoredTheme = () => localStorage.getItem('theme') const setStoredTheme = theme => localStorage.setItem('theme', theme) export const getPreferredTheme = () => { const storedTheme = getStoredTheme() if (storedTheme) { return storedTheme } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' } const setTheme = theme => { if (theme === 'auto') { document.documentElement.setAttribute( 'data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') ) } else { document.documentElement.setAttribute('data-bs-theme', theme) } } export const showActiveTheme = (theme, focus = false) => { const themeSwitcher = document.querySelector('#bd-theme') if (!themeSwitcher) { return } const themeSwitcherText = document.querySelector('#bd-theme-text') const activeThemeIcon = document.querySelector('.theme-icon-active i') const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`) const classListOfActiveBtn = btnToActive.querySelector('i').classList document.querySelectorAll('[data-bs-theme-value]').forEach(element => { element.classList.remove('active') element.setAttribute('aria-pressed', 'false') }) btnToActive.classList.add('active') btnToActive.setAttribute('aria-pressed', 'true') activeThemeIcon.classList.remove(...activeThemeIcon.classList.values()) activeThemeIcon.classList.add(...classListOfActiveBtn) const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.textContent.trim()})` themeSwitcher.setAttribute('aria-label', themeSwitcherLabel) if (focus) { themeSwitcher.focus() } } export function setupThemeToggleListener() { document.querySelectorAll('[data-bs-theme-value]') .forEach(toggle => { toggle.addEventListener('click', () => { const theme = toggle.getAttribute('data-bs-theme-value') setStoredTheme(theme) setTheme(theme) showActiveTheme(theme, true) }) }) showActiveTheme(getPreferredTheme(), false) } export function loadAutoTheme() { setTheme(getPreferredTheme()) window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { const storedTheme = getStoredTheme() if (storedTheme !== 'light' && storedTheme !== 'dark') { setTheme(getPreferredTheme()) } }) window.addEventListener('DOMContentLoaded', () => { showActiveTheme(getPreferredTheme()) }) } ================================================ FILE: src_assets/common/assets/web/troubleshooting.html ================================================ <%- header %>

{{ $t('troubleshooting.troubleshooting') }}

{{ $t('troubleshooting.force_close') }}


{{ $t('troubleshooting.force_close_desc') }}

{{ $t('troubleshooting.force_close_success') }}
{{ $t('troubleshooting.force_close_error') }}

{{ $t('troubleshooting.restart_apollo') }}


{{ $t('troubleshooting.restart_apollo_desc') }}

{{ $t('troubleshooting.restart_apollo_success') }}

{{ $t('troubleshooting.quit_apollo') }}


{{ $t('troubleshooting.quit_apollo_desc') }}

{{ $t('troubleshooting.quit_apollo_success') }}
{{ $t('troubleshooting.quit_apollo_success_ongoing') }}

{{ $t('troubleshooting.dd_reset') }}


{{ $t('troubleshooting.dd_reset_desc') }}

{{ $t('troubleshooting.dd_reset_success') }}
{{ $t('troubleshooting.dd_reset_error') }}

{{ $t('troubleshooting.logs') }}


{{ $t('troubleshooting.logs_desc') }}

{{actualLogs}}
================================================ FILE: src_assets/common/assets/web/welcome.html ================================================ <%- header %>

{{ $t('welcome.greeting') }}

{{ $t('welcome.create_creds') }}

{{ $t('welcome.create_creds_alert') }}
{{ $t('_common.error') }} {{error}}
{{ $t('_common.success') }} {{ $t('welcome.welcome_success') }}
================================================ FILE: src_assets/linux/assets/apps.json ================================================ { "env": { "PATH": "$(PATH):$(HOME)/.local/bin" }, "apps": [ { "name": "Desktop", "image-path": "desktop.png", "allow-client-commands": false }, { "name": "Low Res Desktop", "image-path": "desktop.png", "allow-client-commands": false, "prep-cmd": [ { "do": "xrandr --output HDMI-1 --mode 1920x1080", "undo": "xrandr --output HDMI-1 --mode 1920x1200" } ] }, { "name": "Steam Big Picture", "prep-cmd": [ { "do": "", "undo": "setsid steam steam://close/bigpicture" } ], "detached": [ "setsid steam steam://open/bigpicture" ], "image-path": "steam.png" } ] } ================================================ FILE: src_assets/linux/assets/shaders/opengl/ConvertUV.frag ================================================ #version 300 es #ifdef GL_ES precision lowp float; #endif uniform sampler2D image; layout(shared) uniform ColorMatrix { vec4 color_vec_y; vec4 color_vec_u; vec4 color_vec_v; vec2 range_y; vec2 range_uv; }; in vec3 uuv; layout(location = 0) out vec2 color; //-------------------------------------------------------------------------------------- // Pixel Shader //-------------------------------------------------------------------------------------- void main() { vec3 rgb_left = texture(image, uuv.xz).rgb; vec3 rgb_right = texture(image, uuv.yz).rgb; vec3 rgb = (rgb_left + rgb_right) * 0.5; float u = dot(color_vec_u.xyz, rgb) + color_vec_u.w; float v = dot(color_vec_v.xyz, rgb) + color_vec_v.w; u = u * range_uv.x + range_uv.y; v = v * range_uv.x + range_uv.y; color = vec2(u, v); } ================================================ FILE: src_assets/linux/assets/shaders/opengl/ConvertUV.vert ================================================ #version 300 es #ifdef GL_ES precision mediump float; #endif uniform float width_i; out vec3 uuv; //-------------------------------------------------------------------------------------- // Vertex Shader //-------------------------------------------------------------------------------------- void main() { float idHigh = float(gl_VertexID >> 1); float idLow = float(gl_VertexID & int(1)); float x = idHigh * 4.0 - 1.0; float y = idLow * 4.0 - 1.0; float u_right = idHigh * 2.0; float u_left = u_right - width_i; float v = idLow * 2.0; uuv = vec3(u_left, u_right, v); gl_Position = vec4(x, y, 0.0, 1.0); } ================================================ FILE: src_assets/linux/assets/shaders/opengl/ConvertY.frag ================================================ #version 300 es #ifdef GL_ES precision lowp float; #endif uniform sampler2D image; layout(shared) uniform ColorMatrix { vec4 color_vec_y; vec4 color_vec_u; vec4 color_vec_v; vec2 range_y; vec2 range_uv; }; in vec2 tex; layout(location = 0) out float color; void main() { vec3 rgb = texture(image, tex).rgb; float y = dot(color_vec_y.xyz, rgb); color = y * range_y.x + range_y.y; } ================================================ FILE: src_assets/linux/assets/shaders/opengl/Scene.frag ================================================ #version 300 es #ifdef GL_ES precision lowp float; #endif uniform sampler2D image; in vec2 tex; layout(location = 0) out vec4 color; void main() { color = texture(image, tex); } ================================================ FILE: src_assets/linux/assets/shaders/opengl/Scene.vert ================================================ #version 300 es #ifdef GL_ES precision mediump float; #endif out vec2 tex; void main() { float idHigh = float(gl_VertexID >> 1); float idLow = float(gl_VertexID & int(1)); float x = idHigh * 4.0 - 1.0; float y = idLow * 4.0 - 1.0; float u = idHigh * 2.0; float v = idLow * 2.0; gl_Position = vec4(x, y, 0.0, 1.0); tex = vec2(u, v); } ================================================ FILE: src_assets/linux/misc/60-sunshine.conf ================================================ # Sunshine needs uhid for DS5 emulation uhid ================================================ FILE: src_assets/linux/misc/60-sunshine.rules ================================================ # Allows Sunshine to acces /dev/uinput KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", GROUP="input", MODE="0660", TAG+="uaccess" # Allows Sunshine to access /dev/uhid KERNEL=="uhid", GROUP="input", MODE="0660", TAG+="uaccess" # Joypads KERNEL=="hidraw*", ATTRS{name}=="Sunshine PS5 (virtual) pad", GROUP="input", MODE="0660", TAG+="uaccess" SUBSYSTEMS=="input", ATTRS{name}=="Sunshine X-Box One (virtual) pad", GROUP="input", MODE="0660", TAG+="uaccess" SUBSYSTEMS=="input", ATTRS{name}=="Sunshine gamepad (virtual) motion sensors", GROUP="input", MODE="0660", TAG+="uaccess" SUBSYSTEMS=="input", ATTRS{name}=="Sunshine Nintendo (virtual) pad", GROUP="input", MODE="0660", TAG+="uaccess" ================================================ FILE: src_assets/linux/misc/postinst ================================================ #!/bin/sh # Load uhid (DS5 emulation) echo "Loading uhid kernel module for DS5 emulation." modprobe uhid # Check if we're in an rpm-ostree environment if [ ! -x "$(command -v rpm-ostree)" ]; then echo "Not in an rpm-ostree environment, proceeding with post install steps." # Ensure Sunshine can grab images from KMS path_to_setcap=$(which setcap) path_to_sunshine=$(readlink -f "$(which sunshine)") if [ -x "$path_to_setcap" ] ; then echo "Setting CAP_SYS_ADMIN capability on Sunshine binary." echo "$path_to_setcap cap_sys_admin+p $path_to_sunshine" $path_to_setcap cap_sys_admin+p $path_to_sunshine echo "CAP_SYS_ADMIN capability set on Sunshine binary." else echo "error: setcap not found or not executable." fi # Trigger udev rule reload for /dev/uinput and /dev/uhid path_to_udevadm=$(which udevadm) if [ -x "$path_to_udevadm" ] ; then echo "Reloading udev rules." $path_to_udevadm control --reload-rules $path_to_udevadm trigger --property-match=DEVNAME=/dev/uinput $path_to_udevadm trigger --property-match=DEVNAME=/dev/uhid echo "Udev rules reloaded successfully." else echo "error: udevadm not found or not executable." fi else echo "rpm-ostree environment detected, skipping post install steps. Restart to apply the changes." fi ================================================ FILE: src_assets/macos/assets/Info.plist ================================================ CFBundleIdentifier dev.lizardbyte.sunshine CFBundleName Sunshine NSMicrophoneUsageDescription This app requires access to your microphone to stream audio. ================================================ FILE: src_assets/macos/assets/apps.json ================================================ { "env": { "PATH": "$(PATH):$(HOME)/.local/bin" }, "apps": [ { "name": "Desktop", "image-path": "desktop.png", "allow-client-commands": false }, { "name": "Steam Big Picture", "prep-cmd": [ { "do": "", "undo": "steam://close/bigpicture" } ], "detached": [ "open steam://open/bigpicture" ], "image-path": "steam.png" } ] } ================================================ FILE: src_assets/macos/misc/uninstall_pkg.sh ================================================ #!/bin/bash -e # note: this file was used to remove files when using the pkg/dmg, it is no longer used, but left for reference set -e package_name=org.macports.Sunshine echo "Removing files now..." FILES=$(pkgutil --files $package_name --only-files) for file in ${FILES}; do file="/$file" echo "removing: $file" rm -f "$file" done echo "Removing directories now..." DIRECTORIES=$(pkgutil --files org.macports.Sunshine --only-dirs) for dir in ${DIRECTORIES}; do dir="/$dir" echo "Checking if empty directory: $dir" # check if directory is empty... could just use ${DIRECTORIES} here if pkgutils added the `/` prefix empty_dir=$(find "$dir" -depth 0 -type d -empty) # remove the directory if it is empty if [[ $empty_dir != "" ]]; then # prevent the loop from running and failing if no directories found # shellcheck disable=SC2066 # don't split words as we already know this will be a single directory for i in "${empty_dir}"; do echo "Removing empty directory: ${i}" rmdir "${i}" done fi done echo "Forgetting Sunshine..." pkgutil --forget $package_name echo "Sunshine has been uninstalled..." ================================================ FILE: src_assets/windows/assets/apps.json ================================================ { "env": {}, "apps": [ { "name": "Desktop", "image-path": "desktop.png", "allow-client-commands": false }, { "name": "Steam Big Picture", "prep-cmd": [ { "do": "", "undo": "steam://close/bigpicture", "elevated": false } ], "detached": [ "steam://open/bigpicture" ], "image-path": "steam.png" } ] } ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_type0_ps.hlsl ================================================ #include "include/convert_base.hlsl" #define LEFT_SUBSAMPLING #include "include/convert_yuv420_packed_uv_ps_base.hlsl" ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_type0_ps_linear.hlsl ================================================ #include "include/convert_linear_base.hlsl" #define LEFT_SUBSAMPLING #include "include/convert_yuv420_packed_uv_ps_base.hlsl" ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_type0_ps_perceptual_quantizer.hlsl ================================================ #include "include/convert_perceptual_quantizer_base.hlsl" #define LEFT_SUBSAMPLING #include "include/convert_yuv420_packed_uv_ps_base.hlsl" ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_type0_vs.hlsl ================================================ cbuffer subsample_offset_cbuffer : register(b0) { float2 subsample_offset; }; cbuffer rotate_texture_steps_cbuffer : register(b1) { int rotate_texture_steps; }; #define LEFT_SUBSAMPLING #include "include/base_vs.hlsl" vertex_t main_vs(uint vertex_id : SV_VertexID) { return generate_fullscreen_triangle_vertex(vertex_id, subsample_offset, rotate_texture_steps); } ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_type0s_ps.hlsl ================================================ #include "include/convert_base.hlsl" #define LEFT_SUBSAMPLING_SCALE #include "include/convert_yuv420_packed_uv_ps_base.hlsl" ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_type0s_ps_linear.hlsl ================================================ #include "include/convert_linear_base.hlsl" #define LEFT_SUBSAMPLING_SCALE #include "include/convert_yuv420_packed_uv_ps_base.hlsl" ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_type0s_ps_perceptual_quantizer.hlsl ================================================ #include "include/convert_perceptual_quantizer_base.hlsl" #define LEFT_SUBSAMPLING_SCALE #include "include/convert_yuv420_packed_uv_ps_base.hlsl" ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_type0s_vs.hlsl ================================================ cbuffer subsample_offset_cbuffer : register(b0) { float2 subsample_offset; }; cbuffer rotate_texture_steps_cbuffer : register(b1) { int rotate_texture_steps; }; #define LEFT_SUBSAMPLING_SCALE #include "include/base_vs.hlsl" vertex_t main_vs(uint vertex_id : SV_VertexID) { return generate_fullscreen_triangle_vertex(vertex_id, subsample_offset, rotate_texture_steps); } ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv420_planar_y_ps.hlsl ================================================ #include "include/convert_base.hlsl" #include "include/convert_yuv420_planar_y_ps_base.hlsl" ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv420_planar_y_ps_linear.hlsl ================================================ #include "include/convert_linear_base.hlsl" #include "include/convert_yuv420_planar_y_ps_base.hlsl" ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv420_planar_y_ps_perceptual_quantizer.hlsl ================================================ #include "include/convert_perceptual_quantizer_base.hlsl" #include "include/convert_yuv420_planar_y_ps_base.hlsl" ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv420_planar_y_vs.hlsl ================================================ cbuffer rotate_texture_steps_cbuffer : register(b1) { int rotate_texture_steps; }; #include "include/base_vs.hlsl" vertex_t main_vs(uint vertex_id : SV_VertexID) { return generate_fullscreen_triangle_vertex(vertex_id, float2(0, 0), rotate_texture_steps); } ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv444_packed_ayuv_ps.hlsl ================================================ #include "include/convert_base.hlsl" #include "include/convert_yuv444_ps_base.hlsl" ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv444_packed_ayuv_ps_linear.hlsl ================================================ #include "include/convert_linear_base.hlsl" #include "include/convert_yuv444_ps_base.hlsl" ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv444_packed_vs.hlsl ================================================ cbuffer rotate_texture_steps_cbuffer : register(b1) { int rotate_texture_steps; }; #include "include/base_vs.hlsl" vertex_t main_vs(uint vertex_id : SV_VertexID) { return generate_fullscreen_triangle_vertex(vertex_id, float2(0, 0), rotate_texture_steps); } ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv444_packed_y410_ps.hlsl ================================================ #include "include/convert_base.hlsl" #define Y410 #include "include/convert_yuv444_ps_base.hlsl" ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv444_packed_y410_ps_linear.hlsl ================================================ #include "include/convert_linear_base.hlsl" #define Y410 #include "include/convert_yuv444_ps_base.hlsl" ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv444_packed_y410_ps_perceptual_quantizer.hlsl ================================================ #include "include/convert_perceptual_quantizer_base.hlsl" #define Y410 #include "include/convert_yuv444_ps_base.hlsl" ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv444_planar_ps.hlsl ================================================ #include "include/convert_base.hlsl" #define PLANAR_VIEWPORTS #include "include/convert_yuv444_ps_base.hlsl" ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv444_planar_ps_linear.hlsl ================================================ #include "include/convert_linear_base.hlsl" #define PLANAR_VIEWPORTS #include "include/convert_yuv444_ps_base.hlsl" ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv444_planar_ps_perceptual_quantizer.hlsl ================================================ #include "include/convert_perceptual_quantizer_base.hlsl" #define PLANAR_VIEWPORTS #include "include/convert_yuv444_ps_base.hlsl" ================================================ FILE: src_assets/windows/assets/shaders/directx/convert_yuv444_planar_vs.hlsl ================================================ cbuffer rotate_texture_steps_cbuffer : register(b1) { int rotate_texture_steps; }; cbuffer color_matrix_cbuffer : register(b3) { float4 color_vec_y; float4 color_vec_u; float4 color_vec_v; float2 range_y; float2 range_uv; }; #define PLANAR_VIEWPORTS #include "include/base_vs.hlsl" vertex_t main_vs(uint vertex_id : SV_VertexID) { vertex_t output = generate_fullscreen_triangle_vertex(vertex_id % 3, float2(0, 0), rotate_texture_steps); output.viewport = vertex_id / 3; if (output.viewport == 0) { output.color_vec = color_vec_y; } else if (output.viewport == 1) { output.color_vec = color_vec_u; } else { output.color_vec = color_vec_v; } return output; } ================================================ FILE: src_assets/windows/assets/shaders/directx/cursor_ps.hlsl ================================================ Texture2D cursor : register(t0); SamplerState def_sampler : register(s0); #include "include/base_vs_types.hlsl" float4 main_ps(vertex_t input) : SV_Target { return cursor.Sample(def_sampler, input.tex_coord, 0); } ================================================ FILE: src_assets/windows/assets/shaders/directx/cursor_ps_normalize_white.hlsl ================================================ Texture2D cursor : register(t0); SamplerState def_sampler : register(s0); cbuffer normalize_white_cbuffer : register(b1) { float white_multiplier; }; #include "include/base_vs_types.hlsl" float4 main_ps(vertex_t input) : SV_Target { float4 output = cursor.Sample(def_sampler, input.tex_coord, 0); output.rgb = output.rgb * white_multiplier; return output; } ================================================ FILE: src_assets/windows/assets/shaders/directx/cursor_vs.hlsl ================================================ cbuffer rotate_texture_steps_cbuffer : register(b2) { int rotate_texture_steps; }; #include "include/base_vs.hlsl" vertex_t main_vs(uint vertex_id : SV_VertexID) { return generate_fullscreen_triangle_vertex(vertex_id, float2(0, 0), rotate_texture_steps); } ================================================ FILE: src_assets/windows/assets/shaders/directx/include/base_vs.hlsl ================================================ #include "include/base_vs_types.hlsl" vertex_t generate_fullscreen_triangle_vertex(uint vertex_id, float2 subsample_offset, int rotate_texture_steps) { vertex_t output; float2 tex_coord; if (vertex_id == 0) { output.viewpoint_pos = float4(-1, -1, 0, 1); tex_coord = float2(0, 1); } else if (vertex_id == 1) { output.viewpoint_pos = float4(-1, 3, 0, 1); tex_coord = float2(0, -1); } else { output.viewpoint_pos = float4(3, -1, 0, 1); tex_coord = float2(2, 1); } if (rotate_texture_steps != 0) { float rotation_radians = radians(90 * rotate_texture_steps); float2x2 rotation_matrix = { cos(rotation_radians), -sin(rotation_radians), sin(rotation_radians), cos(rotation_radians) }; float2 rotation_center = { 0.5, 0.5 }; tex_coord = round(rotation_center + mul(rotation_matrix, tex_coord - rotation_center)); // Swap the xy offset coordinates if the texture is rotated an odd number of times. if (rotate_texture_steps & 1) { subsample_offset.xy = subsample_offset.yx; } } #if defined(LEFT_SUBSAMPLING) output.tex_right_left_center = float3(tex_coord.x, tex_coord.x - subsample_offset.x, tex_coord.y); #elif defined(LEFT_SUBSAMPLING_SCALE) float2 halfsample_offset = subsample_offset / 2; float3 right_center_left = float3(tex_coord.x + halfsample_offset.x, tex_coord.x - halfsample_offset.x, tex_coord.x - 3 * halfsample_offset.x); float2 top_bottom = float2(tex_coord.y - halfsample_offset.y, tex_coord.y + halfsample_offset.y); output.tex_right_center_left_top = float4(right_center_left, top_bottom.x); output.tex_right_center_left_bottom = float4(right_center_left, top_bottom.y); #elif defined(TOPLEFT_SUBSAMPLING) output.tex_right_left_top = float3(tex_coord.x, tex_coord.x - subsample_offset.x, tex_coord.y - subsample_offset.y); output.tex_right_left_bottom = float3(tex_coord.x, tex_coord.x - subsample_offset.x, tex_coord.y); #else output.tex_coord = tex_coord; #endif return output; } ================================================ FILE: src_assets/windows/assets/shaders/directx/include/base_vs_types.hlsl ================================================ struct vertex_t { float4 viewpoint_pos : SV_Position; #if defined(LEFT_SUBSAMPLING) float3 tex_right_left_center : TEXCOORD; #elif defined(LEFT_SUBSAMPLING_SCALE) float4 tex_right_center_left_top : TEXCOORD0; float4 tex_right_center_left_bottom : TEXCOORD1; #elif defined(TOPLEFT_SUBSAMPLING) float3 tex_right_left_top : TEXCOORD0; float3 tex_right_left_bottom : TEXCOORD1; #else float2 tex_coord : TEXCOORD; #endif #ifdef PLANAR_VIEWPORTS uint viewport : SV_ViewportArrayIndex; nointerpolation float4 color_vec : COLOR0; #endif }; ================================================ FILE: src_assets/windows/assets/shaders/directx/include/common.hlsl ================================================ // This is a fast sRGB approximation from Microsoft's ColorSpaceUtility.hlsli float3 ApplySRGBCurve(float3 x) { return x < 0.0031308 ? 12.92 * x : 1.13005 * sqrt(x - 0.00228) - 0.13448 * x + 0.005719; } float3 NitsToPQ(float3 L) { // Constants from SMPTE 2084 PQ static const float m1 = 2610.0 / 4096.0 / 4; static const float m2 = 2523.0 / 4096.0 * 128; static const float c1 = 3424.0 / 4096.0; static const float c2 = 2413.0 / 4096.0 * 32; static const float c3 = 2392.0 / 4096.0 * 32; float3 Lp = pow(saturate(L / 10000.0), m1); return pow((c1 + c2 * Lp) / (1 + c3 * Lp), m2); } float3 Rec709toRec2020(float3 rec709) { static const float3x3 ConvMat = { 0.627402, 0.329292, 0.043306, 0.069095, 0.919544, 0.011360, 0.016394, 0.088028, 0.895578 }; return mul(ConvMat, rec709); } float3 scRGBTo2100PQ(float3 rgb) { // Convert from Rec 709 primaries (used by scRGB) to Rec 2020 primaries (used by Rec 2100) rgb = Rec709toRec2020(rgb); // 1.0f is defined as 80 nits in the scRGB colorspace rgb *= 80; // Apply the PQ transfer function on the raw color values in nits return NitsToPQ(rgb); } ================================================ FILE: src_assets/windows/assets/shaders/directx/include/convert_base.hlsl ================================================ #define CONVERT_FUNCTION saturate ================================================ FILE: src_assets/windows/assets/shaders/directx/include/convert_linear_base.hlsl ================================================ #include "include/common.hlsl" float3 CONVERT_FUNCTION(float3 input) { return ApplySRGBCurve(saturate(input)); } ================================================ FILE: src_assets/windows/assets/shaders/directx/include/convert_perceptual_quantizer_base.hlsl ================================================ #include "include/common.hlsl" #define CONVERT_FUNCTION scRGBTo2100PQ ================================================ FILE: src_assets/windows/assets/shaders/directx/include/convert_yuv420_packed_uv_ps_base.hlsl ================================================ Texture2D image : register(t0); SamplerState def_sampler : register(s0); cbuffer color_matrix_cbuffer : register(b0) { float4 color_vec_y; float4 color_vec_u; float4 color_vec_v; float2 range_y; float2 range_uv; }; #include "include/base_vs_types.hlsl" float2 main_ps(vertex_t input) : SV_Target { #if defined(LEFT_SUBSAMPLING) float3 rgb_left = image.Sample(def_sampler, input.tex_right_left_center.xz).rgb; float3 rgb_right = image.Sample(def_sampler, input.tex_right_left_center.yz).rgb; float3 rgb = CONVERT_FUNCTION((rgb_left + rgb_right) * 0.5); #elif defined(LEFT_SUBSAMPLING_SCALE) float3 rgb = image.Sample(def_sampler, input.tex_right_center_left_top.yw).rgb; // top-center rgb += image.Sample(def_sampler, input.tex_right_center_left_bottom.yw).rgb; // bottom-center rgb *= 2; rgb += image.Sample(def_sampler, input.tex_right_center_left_top.xw).rgb; // top-right rgb += image.Sample(def_sampler, input.tex_right_center_left_top.zw).rgb; // top-left rgb += image.Sample(def_sampler, input.tex_right_center_left_bottom.xw).rgb; // bottom-right rgb += image.Sample(def_sampler, input.tex_right_center_left_bottom.zw).rgb; // bottom-left rgb = CONVERT_FUNCTION(rgb * (1./8)); #elif defined(TOPLEFT_SUBSAMPLING) float3 rgb_top_left = image.Sample(def_sampler, input.tex_right_left_top.xz).rgb; float3 rgb_top_right = image.Sample(def_sampler, input.tex_right_left_top.yz).rgb; float3 rgb_bottom_left = image.Sample(def_sampler, input.tex_right_left_bottom.xz).rgb; float3 rgb_bottom_right = image.Sample(def_sampler, input.tex_right_left_bottom.yz).rgb; float3 rgb = CONVERT_FUNCTION((rgb_top_left + rgb_top_right + rgb_bottom_left + rgb_bottom_right) * 0.25); #endif float u = dot(color_vec_u.xyz, rgb) + color_vec_u.w; float v = dot(color_vec_v.xyz, rgb) + color_vec_v.w; u = u * range_uv.x + range_uv.y; v = v * range_uv.x + range_uv.y; return float2(u, v); } ================================================ FILE: src_assets/windows/assets/shaders/directx/include/convert_yuv420_planar_y_ps_base.hlsl ================================================ Texture2D image : register(t0); SamplerState def_sampler : register(s0); cbuffer color_matrix_cbuffer : register(b0) { float4 color_vec_y; float4 color_vec_u; float4 color_vec_v; float2 range_y; float2 range_uv; }; #include "include/base_vs_types.hlsl" float main_ps(vertex_t input) : SV_Target { float3 rgb = CONVERT_FUNCTION(image.Sample(def_sampler, input.tex_coord, 0).rgb); float y = dot(color_vec_y.xyz, rgb) + color_vec_y.w; return y * range_y.x + range_y.y; } ================================================ FILE: src_assets/windows/assets/shaders/directx/include/convert_yuv444_ps_base.hlsl ================================================ Texture2D image : register(t0); SamplerState def_sampler : register(s0); #ifndef PLANAR_VIEWPORTS cbuffer color_matrix_cbuffer : register(b0) { float4 color_vec_y; float4 color_vec_u; float4 color_vec_v; float2 range_y; float2 range_uv; }; #endif #include "include/base_vs_types.hlsl" #ifdef PLANAR_VIEWPORTS uint main_ps(vertex_t input) : SV_Target #else uint4 main_ps(vertex_t input) : SV_Target #endif { float3 rgb = CONVERT_FUNCTION(image.Sample(def_sampler, input.tex_coord, 0).rgb); #ifdef PLANAR_VIEWPORTS // Planar R16, 10 most significant bits store the value return uint(dot(input.color_vec.xyz, rgb) + input.color_vec.w) << 6; #else float y = dot(color_vec_y.xyz, rgb) + color_vec_y.w; float u = dot(color_vec_u.xyz, rgb) + color_vec_u.w; float v = dot(color_vec_v.xyz, rgb) + color_vec_v.w; #ifdef Y410 return uint4(u, y, v, 0); #else // AYUV return uint4(v, u, y, 0); #endif #endif } ================================================ FILE: src_assets/windows/drivers/sudovda/install.bat ================================================ @echo off pushd %~dp0 set "CERTUTIL=certutil" where certutil >nul 2>&1 || set "CERTUTIL=%SystemRoot%\System32\certutil.exe" echo ================ echo Installing cert for the SudoVDA driver... %CERTUTIL% -addstore -f root "sudovda.cer" %CERTUTIL% -addstore -f TrustedPublisher "sudovda.cer" echo ================ echo Removing the old driver... It's OK to show an error if you're installing the driver for the first time. nefconc.exe --remove-device-node --hardware-id root\sudomaker\sudovda --class-guid "4D36E968-E325-11CE-BFC1-08002BE10318" echo ================ echo Installing the new driver... nefconc.exe --create-device-node --class-name Display --class-guid "4D36E968-E325-11CE-BFC1-08002BE10318" --hardware-id root\sudomaker\sudovda nefconc.exe --install-driver --inf-path "SudoVDA.inf" echo ================ echo Done! popd ================================================ FILE: src_assets/windows/drivers/sudovda/uninstall.bat ================================================ @echo off pushd %~dp0 nefconc.exe --remove-device-node --hardware-id root\sudomaker\sudovda --class-guid "4D36E968-E325-11CE-BFC1-08002BE10318" popd pause ================================================ FILE: src_assets/windows/misc/autostart/autostart-service.bat ================================================ @echo off rem Set the service to auto-start sc config ApolloService start= auto ================================================ FILE: src_assets/windows/misc/firewall/add-firewall-rule.bat ================================================ @echo off rem Get sunshine root directory for %%I in ("%~dp0\..") do set "ROOT_DIR=%%~fI" set RULE_NAME=Apollo set PROGRAM_BIN="%ROOT_DIR%\sunshine.exe" rem Add the rule netsh advfirewall firewall add rule name=%RULE_NAME% dir=in action=allow protocol=tcp program=%PROGRAM_BIN% enable=yes netsh advfirewall firewall add rule name=%RULE_NAME% dir=in action=allow protocol=udp program=%PROGRAM_BIN% enable=yes ================================================ FILE: src_assets/windows/misc/firewall/delete-firewall-rule.bat ================================================ @echo off set RULE_NAME=Apollo rem Delete the rule netsh advfirewall firewall delete rule name=%RULE_NAME% ================================================ FILE: src_assets/windows/misc/gamepad/install-gamepad.ps1 ================================================ # Check if a compatible version of ViGEmBus is already installed (1.17 or later) try { $vigemBusPath = "$env:SystemRoot\System32\drivers\ViGEmBus.sys" $fileVersion = (Get-Item $vigemBusPath).VersionInfo.FileVersion if ($fileVersion -ge [System.Version]"1.17") { Write-Information "The installed version is 1.17 or later, no update needed. Exiting." exit 0 } } catch { Write-Information "ViGEmBus driver not found or inaccessible, proceeding with installation." } # Install Virtual Gamepad $scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path $installerPath = Join-Path $scriptPath "vigembus_installer.exe" Start-Process ` -FilePath $installerPath ` -ArgumentList "/passive", "/promptrestart" ================================================ FILE: src_assets/windows/misc/gamepad/uninstall-gamepad.ps1 ================================================ # Use Get-CimInstance to find and uninstall Virtual Gamepad $product = Get-CimInstance -ClassName Win32_Product -Filter "Name='ViGEm Bus Driver'" if ($product) { Invoke-CimMethod -InputObject $product -MethodName Uninstall Write-Information "ViGEm Bus Driver uninstalled successfully" } else { Write-Warning "ViGEm Bus Driver not found" } ================================================ FILE: src_assets/windows/misc/migration/migrate-config.bat ================================================ @echo off rem Get sunshine root directory for %%I in ("%~dp0\..") do set "OLD_DIR=%%~fI" rem Create the config directory if it didn't already exist set "NEW_DIR=%OLD_DIR%\config" if not exist "%NEW_DIR%\" mkdir "%NEW_DIR%" icacls "%NEW_DIR%" /reset rem Migrate all files that aren't already present in the config dir if exist "%OLD_DIR%\apps.json" ( if not exist "%NEW_DIR%\apps.json" ( move "%OLD_DIR%\apps.json" "%NEW_DIR%\apps.json" icacls "%NEW_DIR%\apps.json" /reset ) ) if exist "%OLD_DIR%\sunshine.conf" ( if not exist "%NEW_DIR%\sunshine.conf" ( move "%OLD_DIR%\sunshine.conf" "%NEW_DIR%\sunshine.conf" icacls "%NEW_DIR%\sunshine.conf" /reset ) ) if exist "%OLD_DIR%\sunshine_state.json" ( if not exist "%NEW_DIR%\sunshine_state.json" ( move "%OLD_DIR%\sunshine_state.json" "%NEW_DIR%\sunshine_state.json" icacls "%NEW_DIR%\sunshine_state.json" /reset ) ) rem Migrate the credentials directory if exist "%OLD_DIR%\credentials\" ( if not exist "%NEW_DIR%\credentials\" ( move "%OLD_DIR%\credentials" "%NEW_DIR%\" ) ) rem Create the credentials directory if it wasn't migrated or already existing if not exist "%NEW_DIR%\credentials\" mkdir "%NEW_DIR%\credentials" rem Disallow read access to the credentials directory contents for normal users rem Note: We must use the SIDs directly because "Users" and "Administrators" are localized icacls "%NEW_DIR%\credentials" /inheritance:r icacls "%NEW_DIR%\credentials" /grant:r *S-1-5-32-544:(OI)(CI)(F) icacls "%NEW_DIR%\credentials" /grant:r *S-1-5-32-545:(R) rem Migrate the covers directory if exist "%OLD_DIR%\covers\" ( if not exist "%NEW_DIR%\covers\" ( move "%OLD_DIR%\covers" "%NEW_DIR%\" rem Fix apps.json image path values that point at the old covers directory powershell -NoProfile -c "(Get-Content '%NEW_DIR%\apps.json').replace('.\/covers\/', '.\/config\/covers\/') | Set-Content '%NEW_DIR%\apps.json'" ) ) rem Remove log files del "%OLD_DIR%\*.txt" del "%OLD_DIR%\*.log" ================================================ FILE: src_assets/windows/misc/path/update-path.bat ================================================ @echo off setlocal EnableDelayedExpansion rem Check if parameter is provided if "%~1"=="" ( echo Usage: %0 [add^|remove] echo add - Adds Apollo directories to system PATH echo remove - Removes Apollo directories from system PATH exit /b 1 ) rem Get Apollo root directory for %%I in ("%~dp0\..") do set "ROOT_DIR=%%~fI" echo Apollo root directory: !ROOT_DIR! rem Define directories to add to path set "PATHS_TO_MANAGE[0]=!ROOT_DIR!" set "PATHS_TO_MANAGE[1]=!ROOT_DIR!\tools" rem System path registry location set "KEY_NAME=HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" set "VALUE_NAME=Path" rem Get the current path for /f "tokens=2*" %%A in ('reg query "%KEY_NAME%" /v "%VALUE_NAME%"') do set "CURRENT_PATH=%%B" echo Current path: !CURRENT_PATH! rem Check if adding to path if /i "%~1"=="add" ( set "NEW_PATH=!CURRENT_PATH!" rem Process each directory to add for /L %%i in (0,1,1) do ( set "DIR_TO_ADD=!PATHS_TO_MANAGE[%%i]!" rem Check if path already contains this directory echo "!CURRENT_PATH!" | findstr /i /c:"!DIR_TO_ADD!" > nul if !ERRORLEVEL!==0 ( echo !DIR_TO_ADD! already in path ) else ( echo Adding to path: !DIR_TO_ADD! set "NEW_PATH=!NEW_PATH!;!DIR_TO_ADD!" ) ) rem Only update if path was changed if "!NEW_PATH!" neq "!CURRENT_PATH!" ( rem Set the new path in the registry reg add "%KEY_NAME%" /v "%VALUE_NAME%" /t REG_EXPAND_SZ /d "!NEW_PATH!" /f if !ERRORLEVEL!==0 ( echo Successfully added Apollo directories to PATH ) else ( echo Failed to add Apollo directories to PATH ) ) else ( echo No changes needed to PATH ) exit /b !ERRORLEVEL! ) rem Check if removing from path if /i "%~1"=="remove" ( set "CHANGES_MADE=0" rem Process each directory to remove for /L %%i in (0,1,1) do ( set "DIR_TO_REMOVE=!PATHS_TO_MANAGE[%%i]!" rem Check if path contains this directory echo "!CURRENT_PATH!" | findstr /i /c:"!DIR_TO_REMOVE!" > nul if !ERRORLEVEL!==0 ( echo Removing from path: !DIR_TO_REMOVE! rem Build a new path by parsing and filtering the current path set "NEW_PATH=" for %%p in ("!CURRENT_PATH:;=" "!") do ( set "PART=%%~p" if /i "!PART!" NEQ "!DIR_TO_REMOVE!" ( if defined NEW_PATH ( set "NEW_PATH=!NEW_PATH!;!PART!" ) else ( set "NEW_PATH=!PART!" ) ) ) set "CURRENT_PATH=!NEW_PATH!" set "CHANGES_MADE=1" ) else ( echo !DIR_TO_REMOVE! not found in path ) ) rem Only update if path was changed if "!CHANGES_MADE!"=="1" ( rem Set the new path in the registry reg add "%KEY_NAME%" /v "%VALUE_NAME%" /t REG_EXPAND_SZ /d "!CURRENT_PATH!" /f if !ERRORLEVEL!==0 ( echo Successfully removed Apollo directories from PATH ) else ( echo Failed to remove Apollo directories from PATH ) ) else ( echo No changes needed to PATH ) exit /b !ERRORLEVEL! ) echo Unknown parameter: %~1 echo Usage: %0 [add^|remove] exit /b 1 ================================================ FILE: src_assets/windows/misc/service/install-service.bat ================================================ @echo off setlocal enabledelayedexpansion rem Get sunshine root directory for %%I in ("%~dp0\..") do set "ROOT_DIR=%%~fI" set SERVICE_NAME=ApolloService set "SERVICE_BIN=%ROOT_DIR%\tools\sunshinesvc.exe" set "SERVICE_CONFIG_DIR=%LOCALAPPDATA%\SudoMaker\Apollo" set "SERVICE_CONFIG_FILE=%SERVICE_CONFIG_DIR%\service_start_type.txt" rem Set service to demand start. It will be changed to auto later if the user selected that option. set SERVICE_START_TYPE=demand rem Remove the legacy SunshineSvc service net stop sunshinesvc sc delete sunshinesvc rem Check if ApolloService already exists sc qc %SERVICE_NAME% > nul 2>&1 if %ERRORLEVEL%==0 ( rem Stop the existing service if running net stop %SERVICE_NAME% rem Reconfigure the existing service set SC_CMD=config ) else ( rem Create a new service set SC_CMD=create ) rem Check if we have a saved start type from previous installation if exist "%SERVICE_CONFIG_FILE%" ( rem Debug output file content type "%SERVICE_CONFIG_FILE%" rem Read the saved start type for /f "usebackq delims=" %%a in ("%SERVICE_CONFIG_FILE%") do ( set "SAVED_START_TYPE=%%a" ) echo Raw saved start type: [!SAVED_START_TYPE!] rem Check start type if "!SAVED_START_TYPE!"=="2-delayed" ( set SERVICE_START_TYPE=delayed-auto ) else if "!SAVED_START_TYPE!"=="2" ( set SERVICE_START_TYPE=auto ) else if "!SAVED_START_TYPE!"=="3" ( set SERVICE_START_TYPE=demand ) else if "!SAVED_START_TYPE!"=="4" ( set SERVICE_START_TYPE=disabled ) del "%SERVICE_CONFIG_FILE%" ) echo Setting service start type set to: [!SERVICE_START_TYPE!] rem Run the sc command to create/reconfigure the service sc %SC_CMD% %SERVICE_NAME% binPath= "\"%SERVICE_BIN%\"" start= %SERVICE_START_TYPE% DisplayName= "Apollo Service" rem Set the description of the service sc description %SERVICE_NAME% "Apollo is a self-hosted game stream host for Moonlight." rem Start the new service net start %SERVICE_NAME% ================================================ FILE: src_assets/windows/misc/service/uninstall-service.bat ================================================ @echo off setlocal enabledelayedexpansion set "SERVICE_CONFIG_DIR=%LOCALAPPDATA%\SudoMaker\Apollo" set "SERVICE_CONFIG_FILE=%SERVICE_CONFIG_DIR%\service_start_type.txt" rem Save the current service start type to a file if the service exists sc qc ApolloService >nul 2>&1 if %ERRORLEVEL%==0 ( if not exist "%SERVICE_CONFIG_DIR%\" mkdir "%SERVICE_CONFIG_DIR%\" rem Get the start type for /f "tokens=3" %%i in ('sc qc ApolloService ^| findstr /C:"START_TYPE"') do ( set "CURRENT_START_TYPE=%%i" ) rem Set the content to write if "!CURRENT_START_TYPE!"=="2" ( sc qc ApolloService | findstr /C:"(DELAYED)" >nul if !ERRORLEVEL!==0 ( set "CONTENT=2-delayed" ) else ( set "CONTENT=2" ) ) else if "!CURRENT_START_TYPE!" NEQ "" ( set "CONTENT=!CURRENT_START_TYPE!" ) else ( set "CONTENT=unknown" ) rem Write content to file echo !CONTENT!> "%SERVICE_CONFIG_FILE%" ) rem Stop and delete the legacy SunshineSvc service net stop sunshinesvc sc delete sunshinesvc rem Stop and delete the new ApolloService service net stop ApolloService sc delete ApolloService ================================================ FILE: tests/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.13) # https://github.com/google/oss-policies-info/blob/main/foundational-cxx-support-matrix.md#foundational-c-support project(test_sunshine) include_directories("${CMAKE_SOURCE_DIR}") enable_testing() # Add GoogleTest directory to the project set(GTEST_SOURCE_DIR "${CMAKE_SOURCE_DIR}/third-party/googletest") set(INSTALL_GTEST OFF) set(INSTALL_GMOCK OFF) add_subdirectory("${GTEST_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/googletest") include_directories("${GTEST_SOURCE_DIR}/googletest/include" "${GTEST_SOURCE_DIR}") # coverage # https://gcovr.com/en/stable/guide/compiling.html#compiler-options set(CMAKE_CXX_FLAGS "-fprofile-arcs -ftest-coverage -ggdb -O0") set(CMAKE_C_FLAGS "-fprofile-arcs -ftest-coverage -ggdb -O0") # if windows if (WIN32) # For Windows: Prevent overriding the parent project's compiler/linker settings set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # cmake-lint: disable=C0103 endif () # modify SUNSHINE_DEFINITIONS if (WIN32) list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_SHADERS_DIR="${CMAKE_SOURCE_DIR}/src_assets/windows/assets/shaders/directx") elseif (NOT APPLE) list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_SHADERS_DIR="${CMAKE_SOURCE_DIR}/src_assets/linux/assets/shaders/opengl") endif () set(TEST_DEFINITIONS) # list will be appended as needed # this indicates we're building tests in case sunshine needs to adjust some code or add private tests list(APPEND TEST_DEFINITIONS SUNSHINE_TESTS) list(APPEND TEST_DEFINITIONS SUNSHINE_SOURCE_DIR="${CMAKE_SOURCE_DIR}") list(APPEND TEST_DEFINITIONS SUNSHINE_TEST_BIN_DIR="${CMAKE_CURRENT_BINARY_DIR}") if(NOT WIN32) find_package(Udev 255) # we need 255+ for udevadm verify message(STATUS "UDEV_FOUND: ${UDEV_FOUND}") if(UDEV_FOUND) list(APPEND TEST_DEFINITIONS UDEVADM_EXECUTABLE="${UDEVADM_EXECUTABLE}") endif() endif() file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/tests/*.h ${CMAKE_SOURCE_DIR}/tests/*.cpp) set(SUNSHINE_SOURCES ${SUNSHINE_TARGET_FILES}) # remove main.cpp from the list of sources list(REMOVE_ITEM SUNSHINE_SOURCES ${CMAKE_SOURCE_DIR}/src/main.cpp) add_executable(${PROJECT_NAME} ${TEST_SOURCES} ${SUNSHINE_SOURCES}) # Copy files needed for config consistency tests to build directory # This ensures both CLI and CLion can access the same files relative to the test executable # Using configure_file ensures files are copied when they change between builds set(INTEGRATION_TEST_FILES "src/config.cpp" "src_assets/common/assets/web/config.html" "docs/configuration.md" "src_assets/common/assets/web/public/assets/locale/en.json" "src_assets/common/assets/web/configs/tabs/General.vue" "src_assets/linux/misc/60-sunshine.rules" ) foreach(file ${INTEGRATION_TEST_FILES}) configure_file( "${CMAKE_SOURCE_DIR}/${file}" "${CMAKE_CURRENT_BINARY_DIR}/${file}" COPYONLY ) endforeach() # Copy all locale files for locale consistency tests # Use a custom command to properly handle both adding and removing files set(LOCALE_SRC_DIR "${CMAKE_SOURCE_DIR}/src_assets/common/assets/web/public/assets/locale") set(LOCALE_DST_DIR "${CMAKE_CURRENT_BINARY_DIR}/src_assets/common/assets/web/public/assets/locale") add_custom_target(sync_locale_files ALL COMMAND ${CMAKE_COMMAND} -E rm -rf "${LOCALE_DST_DIR}" COMMAND ${CMAKE_COMMAND} -E make_directory "${LOCALE_DST_DIR}" COMMAND ${CMAKE_COMMAND} -E copy_directory "${LOCALE_SRC_DIR}" "${LOCALE_DST_DIR}" COMMENT "Synchronizing locale files for tests" VERBATIM ) foreach(dep ${SUNSHINE_TARGET_DEPENDENCIES}) add_dependencies(${PROJECT_NAME} ${dep}) # compile these before sunshine endforeach() # Ensure locale files are synchronized before building the test executable add_dependencies(${PROJECT_NAME} sync_locale_files) set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 23) target_link_libraries(${PROJECT_NAME} ${SUNSHINE_EXTERNAL_LIBRARIES} gtest ${PLATFORM_LIBRARIES}) target_compile_definitions(${PROJECT_NAME} PUBLIC ${SUNSHINE_DEFINITIONS} ${TEST_DEFINITIONS}) target_compile_options(${PROJECT_NAME} PRIVATE $<$:${SUNSHINE_COMPILE_OPTIONS}>;$<$:${SUNSHINE_COMPILE_OPTIONS_CUDA};-std=c++17>) # cmake-lint: disable=C0301 target_link_options(${PROJECT_NAME} PRIVATE) if (WIN32) # prefer static libraries since we're linking statically # this fixes libcurl linking errors when using non MSYS2 version of CMake set_target_properties(${PROJECT_NAME} PROPERTIES LINK_SEARCH_START_STATIC 1) endif () ================================================ FILE: tests/fixtures/http/hello-redirect.txt ================================================ hello-redirect.txt ================================================ FILE: tests/fixtures/http/hello.txt ================================================ hello.txt ================================================ FILE: tests/integration/test_config_consistency.cpp ================================================ /** * @file tests/integration/test_config_consistency.cpp * @brief Test configuration consistency across all configuration files */ #include "../tests_common.h" // standard includes #include #include #include #include #include #include #include #include #include #include #include // local includes #include "src/file_handler.h" class ConfigConsistencyTest: public ::testing::Test { protected: void SetUp() override { // Define the expected mapping between documentation sections and UI tabs expectedDocToTabMapping = { {"General", "general"}, {"Input", "input"}, {"Audio/Video", "av"}, {"Network", "network"}, {"Config Files", "files"}, {"Advanced", "advanced"}, {"NVIDIA NVENC Encoder", "nv"}, {"Intel QuickSync Encoder", "qsv"}, {"AMD AMF Encoder", "amd"}, {"VideoToolbox Encoder", "vt"}, {"VA-API Encoder", "vaapi"}, {"Software Encoder", "sw"} }; } // Extract config options from config.cpp - the authoritative source static std::set> extractConfigCppOptions() { std::set> options; std::string content = file_handler::read_file("src/config.cpp"); // Regex patterns to match different config option types in config.cpp const std::vector patterns = { std::regex(R"DELIM((?:string_f|path_f|string_restricted_f)\s*\(\s*vars\s*,\s*"([^"]+)")DELIM"), std::regex(R"DELIM((?:int_f|int_between_f)\s*\(\s*vars\s*,\s*"([^"]+)")DELIM"), std::regex(R"DELIM(bool_f\s*\(\s*vars\s*,\s*"([^"]+)")DELIM"), std::regex(R"DELIM((?:double_f|double_between_f)\s*\(\s*vars\s*,\s*"([^"]+)")DELIM"), std::regex(R"DELIM(generic_f\s*\(\s*vars\s*,\s*"([^"]+)")DELIM"), std::regex(R"DELIM(list_prep_cmd_f\s*\(\s*vars\s*,\s*"([^"]+)")DELIM"), std::regex(R"DELIM(map_int_int_f\s*\(\s*vars\s*,\s*"([^"]+)")DELIM") }; for (const auto &pattern : patterns) { std::sregex_iterator iter(content.begin(), content.end(), pattern); for (std::sregex_iterator end; iter != end; ++iter) { std::string optionName = (*iter)[1].str(); options.insert(optionName); } } return options; } // Helper function to find brace boundaries static size_t findClosingBrace(const std::string &content, const size_t start) { size_t pos = start + 1; int braceLevel = 1; while (pos < content.length() && braceLevel > 0) { if (content[pos] == '{') { braceLevel++; } else if (content[pos] == '}') { braceLevel--; } pos++; } return pos - 1; } // Helper function to extract tab ID from a tab object static std::string extractTabId(const std::string &tabObject) { const std::regex idPattern(R"DELIM(id:\s*"([^"]+)")DELIM"); if (std::smatch idMatch; std::regex_search(tabObject, idMatch, idPattern)) { return idMatch[1].str(); } return ""; } // Helper function to find and extract tabs content from HTML static std::string extractTabsContent(const std::string &content) { const size_t tabsStart = content.find("tabs: ["); if (tabsStart == std::string::npos) { return ""; } // Find the end of the tab array size_t pos = tabsStart + 7; // Skip "tabs: [" int bracketLevel = 1; size_t tabsEnd = pos; while (pos < content.length() && bracketLevel > 0) { if (content[pos] == '[') { bracketLevel++; } else if (content[pos] == ']') { bracketLevel--; } tabsEnd = pos; pos++; } return content.substr(tabsStart + 7, tabsEnd - tabsStart - 7); } // Helper function to extract options from a tab object (generic version) template static void extractOptionsFromTabGeneric(const std::string &tabObject, Container &container) { const std::string tabId = extractTabId(tabObject); if (tabId.empty()) { return; } const size_t optionsStart = tabObject.find("options:"); if (optionsStart == std::string::npos) { return; } const size_t optStart = tabObject.find('{', optionsStart); if (optStart == std::string::npos) { return; } const size_t optEnd = findClosingBrace(tabObject, optStart); std::string optionsSection = tabObject.substr(optStart + 1, optEnd - optStart - 1); // Extract option names const std::regex optionPattern(R"DELIM("([^"]+)":\s*)DELIM"); std::sregex_iterator optionIter(optionsSection.begin(), optionsSection.end(), optionPattern); for (const std::sregex_iterator optionEnd; optionIter != optionEnd; ++optionIter) { std::string optionName = (*optionIter)[1].str(); // Use if constexpr to handle different container types if constexpr (std::is_same_v>>) { container[optionName] = tabId; } else if constexpr (std::is_same_v, std::less<>>>) { container[tabId].push_back(optionName); } } } // Helper function to process tab objects from tabs content template static void processTabObjects(const std::string &tabsContent, Container &container) { size_t tabPos = 0; while (tabPos < tabsContent.length()) { const size_t objStart = tabsContent.find('{', tabPos); if (objStart == std::string::npos) { break; } const size_t objEnd = findClosingBrace(tabsContent, objStart); std::string tabObject = tabsContent.substr(objStart, objEnd - objStart + 1); extractOptionsFromTabGeneric(tabObject, container); tabPos = objEnd + 1; } } // Helper function to trim whitespace from string static void trimWhitespace(std::string &str) { str.erase(str.find_last_not_of(" \t\r\n") + 1); } // Helper function to extract option name from the Markdown line static std::string extractOptionFromMarkdownLine(const std::string &line) { const std::regex optionPattern(R"(^### ([^#\r\n]+))"); if (std::smatch optionMatch; std::regex_search(line, optionMatch, optionPattern)) { std::string optionName = optionMatch[1].str(); trimWhitespace(optionName); return optionName; } return ""; } // Extract config options from config.html static std::map> extractConfigHtmlOptions() { std::map> options; const std::string content = file_handler::read_file("src_assets/common/assets/web/config.html"); const std::string tabsContent = extractTabsContent(content); if (tabsContent.empty()) { return options; } processTabObjects(tabsContent, options); return options; } // Helper function to extract options from a single tab object (now using generic function) static void extractOptionsFromTab(const std::string &tabObject, std::map, std::less<>> &optionsByTab) { extractOptionsFromTabGeneric(tabObject, optionsByTab); } // Extract config options from config.html with order preserved static std::map, std::less<>> extractConfigHtmlOptionsWithOrder() { std::map, std::less<>> optionsByTab; const std::string content = file_handler::read_file("src_assets/common/assets/web/config.html"); const std::string tabsContent = extractTabsContent(content); if (tabsContent.empty()) { return optionsByTab; } processTabObjects(tabsContent, optionsByTab); return optionsByTab; } // Helper function to process markdown line for section headers static bool processSectionHeader(const std::string &line, std::string ¤tSection) { const std::regex sectionPattern(R"(^## ([^#\r\n]+))"); if (std::smatch sectionMatch; std::regex_search(line, sectionMatch, sectionPattern)) { currentSection = sectionMatch[1].str(); trimWhitespace(currentSection); return true; } return false; } // Helper function to process markdown line for option headers static bool processOptionHeader(const std::string &line, const std::string_view currentSection, std::map> &options) { if (currentSection.empty()) { return false; } if (const std::string optionName = extractOptionFromMarkdownLine(line); !optionName.empty()) { options[optionName] = currentSection; return true; } return false; } // Extract config options from configuration.md static std::map> extractConfigMdOptions() { std::map> options; const std::string content = file_handler::read_file("docs/configuration.md"); std::istringstream stream(content); std::string line; std::string currentSection; while (std::getline(stream, line)) { if (processSectionHeader(line, currentSection)) { continue; } processOptionHeader(line, currentSection, options); } return options; } // Helper function to process markdown option line for order-preserved extraction static void processMarkdownOptionLine(const std::string &line, const std::string ¤tSection, std::map, std::less<>> &optionsBySection) { if (currentSection.empty()) { return; } if (const std::string optionName = extractOptionFromMarkdownLine(line); !optionName.empty()) { optionsBySection[currentSection].push_back(optionName); } } // Extract config options from configuration.md with order preserved static std::map, std::less<>> extractConfigMdOptionsWithOrder() { std::map, std::less<>> optionsBySection; const std::string content = file_handler::read_file("docs/configuration.md"); std::istringstream stream(content); std::string line; std::string currentSection; while (std::getline(stream, line)) { if (processSectionHeader(line, currentSection)) { continue; } processMarkdownOptionLine(line, currentSection, optionsBySection); } return optionsBySection; } // Helper function to find the config section end static size_t findConfigSectionEnd(const std::string &content, size_t configStart) { size_t braceCount = 1; size_t configEnd = configStart; while (configStart < content.length() && braceCount > 0) { if (content[configStart] == '{') { braceCount++; } else if (content[configStart] == '}') { braceCount--; } configEnd = configStart; configStart++; } return configEnd; } // Helper function to extract keys from a config section static void extractKeysFromConfigSection(const std::string_view configSection, std::set> &options) { const std::regex keyPattern(R"DELIM("([^"]+)":\s*)DELIM"); std::string configStr(configSection); std::sregex_iterator iter(configStr.begin(), configStr.end(), keyPattern); for (const std::sregex_iterator end; iter != end; ++iter) { options.insert((*iter)[1].str()); } } // Extract config options from en.json static std::set> extractEnJsonConfigOptions() { std::set> options; const std::string content = file_handler::read_file("src_assets/common/assets/web/public/assets/locale/en.json"); // Look for the config section const std::regex configSectionPattern(R"DELIM("config":\s*\{)DELIM"); std::smatch match; if (!std::regex_search(content, match, configSectionPattern)) { return options; } // Find the config section and extract keys const size_t configStart = match.position() + match.length(); const size_t configEnd = findConfigSectionEnd(content, configStart); const std::string configSection = content.substr(configStart, configEnd - configStart); extractKeysFromConfigSection(configSection, options); return options; } std::map> expectedDocToTabMapping; // Helper function to check if an option exists in HTML options static bool isOptionInHtml(const std::string &option, const std::map> &htmlOptions) { return htmlOptions.contains(option); } // Helper function to check if an option exists in MD options static bool isOptionInMd(const std::string &option, const std::map> &mdOptions) { return mdOptions.contains(option); } // Helper function to validate option existence across files static void validateOptionExistence(const std::string &option, const std::map> &htmlOptions, const std::map> &mdOptions, const std::set> &jsonOptions, std::vector &missingFromFiles) { if (!isOptionInHtml(option, htmlOptions)) { missingFromFiles.push_back(std::format("config.html missing: {}", option)); } if (!isOptionInMd(option, mdOptions)) { missingFromFiles.push_back(std::format("configuration.md missing: {}", option)); } if (!jsonOptions.contains(option)) { missingFromFiles.push_back(std::format("en.json missing: {}", option)); } } // Helper function to check tab correspondence with documentation sections static void checkTabCorrespondence(const std::string &tab, const std::map> &expectedDocToTabMapping, const std::set> &mdSections, std::vector &inconsistencies) { bool found = false; for (const auto &[docSection, expectedTab] : expectedDocToTabMapping) { if (expectedTab != tab) { continue; } if (!mdSections.contains(docSection)) { inconsistencies.push_back(std::format("Tab '{}' maps to doc section '{}' but section not found", tab, docSection)); } found = true; break; } if (!found) { inconsistencies.push_back(std::format("Tab '{}' has no corresponding documentation section", tab)); } } // Helper function to check if a test fake option is found in missing files static void checkTestDummyDetection(const std::vector &missingFromFiles, const std::string &testDummyOption, bool &foundMissingDummyInHtml, bool &foundMissingDummyInMd, bool &foundMissingDummyInJson) { for (const auto &missing : missingFromFiles) { if (!missing.contains(testDummyOption)) { continue; } if (missing.contains("config.html")) { foundMissingDummyInHtml = true; } if (missing.contains("configuration.md")) { foundMissingDummyInMd = true; } if (missing.contains("en.json")) { foundMissingDummyInJson = true; } } } // Helper function to create comma-separated string from vector static std::string buildCommaSeparatedString(const std::vector &options) { std::string result; for (size_t i = 0; i < options.size(); ++i) { if (i > 0) { result += ", "; } result += options[i]; } return result; } }; TEST_F(ConfigConsistencyTest, AllConfigOptionsExistInAllFiles) { const auto cppOptions = extractConfigCppOptions(); const auto htmlOptions = extractConfigHtmlOptions(); const auto mdOptions = extractConfigMdOptions(); const auto jsonOptions = extractEnJsonConfigOptions(); // Options that are internal/special and shouldn't be in UI/docs const std::set> internalOptions = { "flags" // Internal config flags, not user-configurable }; std::vector missingFromFiles; // Check that all config.cpp options exist in other files (except internal ones) for (const auto &option : cppOptions) { if (internalOptions.contains(option)) { continue; // Skip internal options } validateOptionExistence(option, htmlOptions, mdOptions, jsonOptions, missingFromFiles); } if (!missingFromFiles.empty()) { std::string errorMsg = "Config options missing from files:\n"; for (const auto &missing : missingFromFiles) { errorMsg += std::format(" {}\n", missing); } FAIL() << errorMsg; } } TEST_F(ConfigConsistencyTest, ConfigTabsMatchDocumentationSections) { auto htmlOptions = extractConfigHtmlOptions(); auto mdOptions = extractConfigMdOptions(); // Get unique tabs and sections std::set> htmlTabs; std::set> mdSections; for (const auto &tab : htmlOptions | std::views::values) { htmlTabs.insert(tab); } for (const auto §ion : mdOptions | std::views::values) { mdSections.insert(section); } std::vector inconsistencies; // Check that each HTML tab has a corresponding documentation section for (const auto &tab : htmlTabs) { checkTabCorrespondence(tab, expectedDocToTabMapping, mdSections, inconsistencies); } // Check that each documentation section has a corresponding HTML tab for (const auto §ion : mdSections) { if (!expectedDocToTabMapping.contains(section)) { inconsistencies.push_back(std::format("Documentation section '{}' has no corresponding UI tab", section)); } } if (!inconsistencies.empty()) { std::string errorMsg = "Tab/Section mapping inconsistencies:\n"; for (const auto &inconsistency : inconsistencies) { errorMsg += std::format(" {}\n", inconsistency); } FAIL() << errorMsg; } } TEST_F(ConfigConsistencyTest, ConfigOptionsInSameOrderWithinSections) { // Extract options with order preserved auto htmlOptionsByTab = extractConfigHtmlOptionsWithOrder(); auto mdOptionsBySection = extractConfigMdOptionsWithOrder(); std::vector orderInconsistencies; // Compare order for each tab/section pair for (const auto &[docSection, tabId] : expectedDocToTabMapping) { if (!htmlOptionsByTab.contains(tabId) || !mdOptionsBySection.contains(docSection)) { continue; // Skip if either tab or section doesn't exist } const auto &htmlOrder = htmlOptionsByTab.at(tabId); const auto &mdOrder = mdOptionsBySection.at(docSection); // Find options that exist in both HTML and MD for this section std::vector commonOptions; for (const auto &option : htmlOrder) { if (std::ranges::find(mdOrder, option) != mdOrder.end()) { commonOptions.push_back(option); } } // Filter MD order to only include common options in the same order they appear in MD std::vector mdOrderFiltered; for (const auto &option : mdOrder) { if (std::ranges::find(commonOptions, option) != commonOptions.end()) { mdOrderFiltered.push_back(option); } } // Compare the order of common options if (commonOptions != mdOrderFiltered && !commonOptions.empty() && !mdOrderFiltered.empty()) { // Create readable string representations of the option lists std::string htmlOrderStr = buildCommaSeparatedString(commonOptions); std::string mdOrderStr = buildCommaSeparatedString(mdOrderFiltered); std::string detailMsg = std::format( "Section '{}' (tab '{}') has different option order:\n" " HTML order: [{}]\n" " MD order: [{}]", docSection, tabId, htmlOrderStr, mdOrderStr ); orderInconsistencies.push_back(detailMsg); } } if (!orderInconsistencies.empty()) { std::string errorMsg = "Config option order inconsistencies:\n"; for (const auto &inconsistency : orderInconsistencies) { errorMsg += std::format(" {}\n", inconsistency); } FAIL() << errorMsg; } } TEST_F(ConfigConsistencyTest, DummyConfigOptionsDoNotExist) { const auto cppOptions = extractConfigCppOptions(); const auto htmlOptions = extractConfigHtmlOptions(); const auto mdOptions = extractConfigMdOptions(); const auto jsonOptions = extractEnJsonConfigOptions(); // List of fake config options that should NOT exist in any files const std::vector dummyOptions = { "dummy_config_option", "nonexistent_setting", "fake_config_parameter", "test_dummy_option", "invalid_config_key" }; std::vector unexpectedlyFound; // Check that none of the fake options exist in any of the config files for (const auto &dummyOption : dummyOptions) { if (cppOptions.contains(dummyOption)) { unexpectedlyFound.push_back(std::format("config.cpp contains dummy option: {}", dummyOption)); } if (htmlOptions.contains(dummyOption)) { unexpectedlyFound.push_back(std::format("config.html contains dummy option: {}", dummyOption)); } if (mdOptions.contains(dummyOption)) { unexpectedlyFound.push_back(std::format("configuration.md contains dummy option: {}", dummyOption)); } if (jsonOptions.contains(dummyOption)) { unexpectedlyFound.push_back(std::format("en.json contains dummy option: {}", dummyOption)); } } // This test should pass (i.e., no fake options should be found) // If any fake options are found, it indicates a problem with the test data if (!unexpectedlyFound.empty()) { std::string errorMsg = "Dummy config options unexpectedly found in files:\n"; for (const auto &found : unexpectedlyFound) { errorMsg += std::format(" {}\n", found); } FAIL() << errorMsg; } } TEST_F(ConfigConsistencyTest, TestFrameworkDetectsMissingOptions) { const auto cppOptions = extractConfigCppOptions(); const auto htmlOptions = extractConfigHtmlOptions(); const auto mdOptions = extractConfigMdOptions(); const auto jsonOptions = extractEnJsonConfigOptions(); // Add a fake option to the cpp options to simulate a missing option scenario std::set> modifiedCppOptions = cppOptions; const std::string testDummyOption = "test_framework_validation_option"; modifiedCppOptions.insert(testDummyOption); // Options that are internal/special and shouldn't be in UI/docs std::set> internalOptions = { "flags" // Internal config flags, not user-configurable }; std::vector missingFromFiles; // Check that the fake option is detected as missing from other files for (const auto &option : modifiedCppOptions) { if (internalOptions.contains(option)) { continue; // Skip internal options } if (!htmlOptions.contains(option)) { missingFromFiles.push_back(std::format("config.html missing: {}", option)); } if (!mdOptions.contains(option)) { missingFromFiles.push_back(std::format("configuration.md missing: {}", option)); } if (!jsonOptions.contains(option)) { missingFromFiles.push_back(std::format("en.json missing: {}", option)); } } // Verify that the test framework detected the missing fake option bool foundMissingDummyInHtml = false; bool foundMissingDummyInMd = false; bool foundMissingDummyInJson = false; checkTestDummyDetection(missingFromFiles, testDummyOption, foundMissingDummyInHtml, foundMissingDummyInMd, foundMissingDummyInJson); // The test framework should have detected the fake option as missing from all files EXPECT_TRUE(foundMissingDummyInHtml) << "Test framework failed to detect missing option in config.html"; EXPECT_TRUE(foundMissingDummyInMd) << "Test framework failed to detect missing option in configuration.md"; EXPECT_TRUE(foundMissingDummyInJson) << "Test framework failed to detect missing option in en.json"; // Verify we have at least 3 missing entries (one for each file type) EXPECT_GE(missingFromFiles.size(), 3) << "Test framework should detect missing dummy option in all three file types"; } ================================================ FILE: tests/integration/test_external_commands.cpp ================================================ /** * @file tests/integration/test_external_commands.cpp * @brief Integration tests for running external commands with platform-specific validation */ #include "../tests_common.h" // standard includes #include #include #include #include // lib includes #include // local includes #include "src/platform/common.h" // Test data structure for parameterized testing struct ExternalCommandTestData { std::string command; std::string platform; // "windows", "linux", "macos", or "all" bool should_succeed; std::string description; std::string working_directory; // Optional: if empty, uses SUNSHINE_SOURCE_DIR bool xfail_condition = false; // Optional: condition for expected failure std::string xfail_reason = ""; // Optional: reason for expected failure // Constructor with xfail parameters ExternalCommandTestData(std::string cmd, std::string plat, const bool succeed, std::string desc, std::string work_dir = "", const bool xfail_cond = false, std::string xfail_rsn = ""): command(std::move(cmd)), platform(std::move(plat)), should_succeed(succeed), description(std::move(desc)), working_directory(std::move(work_dir)), xfail_condition(xfail_cond), xfail_reason(std::move(xfail_rsn)) {} }; class ExternalCommandTest: public ::testing::TestWithParam { protected: void SetUp() override { if constexpr (IS_WINDOWS) { current_platform = "windows"; } else if constexpr (IS_MACOS) { current_platform = "macos"; } else if constexpr (IS_LINUX) { current_platform = "linux"; } } [[nodiscard]] bool shouldRunOnCurrentPlatform(const std::string_view &test_platform) const { return test_platform == "all" || test_platform == current_platform; } // Helper function to run a command using the existing process infrastructure static std::pair runCommand(const std::string &cmd, const std::string_view &working_dir) { const auto env = boost::this_process::environment(); // Determine the working directory: use the provided working_dir or fall back to SUNSHINE_SOURCE_DIR boost::filesystem::path effective_working_dir; if (!working_dir.empty()) { effective_working_dir = working_dir; } else { // Use SUNSHINE_SOURCE_DIR CMake definition as the default working directory effective_working_dir = SUNSHINE_SOURCE_DIR; } std::error_code ec; // Create a temporary file to capture output const auto temp_file = std::tmpfile(); if (!temp_file) { return {-1, "Failed to create temporary file for output"}; } // Run the command using the existing platf::run_command function auto child = platf::run_command( false, // not elevated false, // not interactive cmd, effective_working_dir, env, temp_file, ec, nullptr // no process group ); if (ec) { std::fclose(temp_file); return {-1, std::format("Failed to start command: {}", ec.message())}; } // Wait for the command to complete child.wait(); int exit_code = child.exit_code(); // Read the output from the temporary file std::rewind(temp_file); std::string output; std::array buffer {}; while (std::fgets(buffer.data(), static_cast(buffer.size()), temp_file)) { // std::string constructor automatically handles null-terminated strings output += std::string(buffer.data()); } std::fclose(temp_file); return {exit_code, output}; } public: std::string current_platform; }; // Test case implementation TEST_P(ExternalCommandTest, RunExternalCommand) { const auto &[command, platform, should_succeed, description, working_directory, xfail_condition, xfail_reason] = GetParam(); // Skip test if not for the current platform if (!shouldRunOnCurrentPlatform(platform)) { GTEST_SKIP() << "Test not applicable for platform: " << current_platform; } // Use the xfail condition and reason from test data XFAIL_IF(xfail_condition, xfail_reason); BOOST_LOG(info) << "Running external command test: " << description; BOOST_LOG(debug) << "Command: " << command; auto [exit_code, output] = runCommand(command, working_directory); BOOST_LOG(debug) << "Command exit code: " << exit_code; if (!output.empty()) { BOOST_LOG(debug) << "Command output: " << output; } if (should_succeed) { HANDLE_XFAIL_ASSERT_EQ(exit_code, 0, std::format("Command should have succeeded but failed with exit code {}\nOutput: {}", std::to_string(exit_code), output)); } else { HANDLE_XFAIL_ASSERT_NE(exit_code, 0, std::format("Command should have failed but succeeded\nOutput: {}", output)); } } // Platform-specific command strings constexpr auto SIMPLE_COMMAND = IS_WINDOWS ? "where cmd" : "which sh"; #ifdef UDEVADM_EXECUTABLE #define UDEV_TESTS \ ExternalCommandTestData { \ std::format("{} verify {}/src_assets/linux/misc/60-sunshine.rules", UDEVADM_EXECUTABLE, SUNSHINE_TEST_BIN_DIR), \ "linux", \ true, \ "Test udev rules file" \ }, #else #define UDEV_TESTS #endif // Test data INSTANTIATE_TEST_SUITE_P( ExternalCommands, ExternalCommandTest, ::testing::Values( UDEV_TESTS // Cross-platform tests with xfail on Windows CI ExternalCommandTestData { SIMPLE_COMMAND, "all", true, "Simple command test", "", // working_directory IS_WINDOWS, // xfail_condition "Simple command test fails on Windows CI environment" // xfail_reason }, // Cross-platform failing test ExternalCommandTestData { "non_existent_command_12345", "all", false, "Test command that should fail" } ), [](const ::testing::TestParamInfo &info) { // Generate test names from a description std::string name = info.param.description; // Replace spaces and special characters with underscores for valid test names std::replace_if(name.begin(), name.end(), [](char c) { return !std::isalnum(c); }, '_'); return name; } ); ================================================ FILE: tests/integration/test_locale_consistency.cpp ================================================ /** * @file tests/integration/test_locale_consistency.cpp * @brief Test locale consistency across configuration files and locale JSON files */ #include "../tests_common.h" // standard includes #include #include #include #include #include #include #include #include #include // lib includes #include // local includes #include "src/file_handler.h" namespace fs = std::filesystem; class LocaleConsistencyTest: public ::testing::Test { protected: // Extract locale options from config.cpp static std::set> extractConfigCppLocales() { std::set> locales; const std::string content = file_handler::read_file("src/config.cpp"); // Find the string_restricted_f call for locale const std::regex localeSection(R"(string_restricted_f\s*\(\s*vars\s*,\s*"locale"[^}]*\{([^}]*)\})"); if (std::smatch match; std::regex_search(content, match, localeSection)) { const std::string localeList = match[1].str(); // Extract individual locale codes const std::regex localePattern(R"delimiter("([^"]+)"sv)delimiter"); std::sregex_iterator iter(localeList.begin(), localeList.end(), localePattern); for (const std::sregex_iterator end; iter != end; ++iter) { locales.insert((*iter)[1].str()); } } return locales; } // Extract locale options from General.vue static std::map> extractGeneralVueLocales() { std::map> locales; const std::string content = file_handler::read_file("src_assets/common/assets/web/configs/tabs/General.vue"); // Find the locale select section specifically const std::regex localeSelectPattern("id=\"locale\"[^>]*>([^<]*(?:]*>[^<]*[^<]*)*)"); if (std::smatch selectMatch; std::regex_search(content, selectMatch, localeSelectPattern)) { const std::string localeSection = selectMatch[1].str(); // Extract option elements with locale codes and display names from the locale section const std::regex optionPattern(R"delimiter(([^<]+))delimiter"); std::sregex_iterator iter(localeSection.begin(), localeSection.end(), optionPattern); for (const std::sregex_iterator end; iter != end; ++iter) { const std::string localeCode = (*iter)[1].str(); const std::string displayName = (*iter)[2].str(); locales[localeCode] = displayName; } } return locales; } // Get available locale JSON files static std::set> getAvailableLocaleFiles() { std::set> locales; const std::filesystem::path localeDir = "src_assets/common/assets/web/public/assets/locale"; if (!fs::exists(localeDir)) { return locales; } for (const auto &entry : fs::directory_iterator(localeDir)) { if (entry.is_regular_file() && entry.path().extension() == ".json") { const std::string filename = entry.path().stem().string(); locales.insert(filename); } } return locales; } // Helper function to check if a locale JSON file is valid using nlohmann/json static bool isValidLocaleFile(const std::string &localeCode) { const std::string filePath = std::format("src_assets/common/assets/web/public/assets/locale/{}.json", localeCode); if (!fs::exists(filePath)) { return false; } try { const std::string content = file_handler::read_file(filePath.c_str()); // Parse JSON using nlohmann/json to validate it's properly formatted const nlohmann::json localeJson = nlohmann::json::parse(content); // Basic validation - should be a JSON object with some content return localeJson.is_object() && !localeJson.empty(); } catch (const nlohmann::json::parse_error &) { return false; } } }; TEST_F(LocaleConsistencyTest, AllLocaleFilesHaveConfigCppEntries) { const auto configLocales = extractConfigCppLocales(); const auto localeFiles = getAvailableLocaleFiles(); std::vector missingFromConfig; // Check that every locale file has a corresponding entry in config.cpp for (const auto &localeFile : localeFiles) { if (!configLocales.contains(localeFile)) { missingFromConfig.push_back(localeFile); } } if (!missingFromConfig.empty()) { std::string errorMsg = "Locale files missing from config.cpp:\n"; for (const auto &missing : missingFromConfig) { errorMsg += std::format(" {}.json\n", missing); } FAIL() << errorMsg; } } TEST_F(LocaleConsistencyTest, AllLocaleFilesHaveGeneralVueEntries) { const auto vueLocales = extractGeneralVueLocales(); const auto localeFiles = getAvailableLocaleFiles(); std::vector missingFromVue; // Check that every locale file has a corresponding entry in General.vue for (const auto &localeFile : localeFiles) { if (!vueLocales.contains(localeFile)) { missingFromVue.push_back(localeFile); } } if (!missingFromVue.empty()) { std::string errorMsg = "Locale files missing from General.vue:\n"; for (const auto &missing : missingFromVue) { errorMsg += std::format(" {}.json\n", missing); } FAIL() << errorMsg; } } TEST_F(LocaleConsistencyTest, AllConfigCppLocalesHaveFiles) { const auto configLocales = extractConfigCppLocales(); const auto localeFiles = getAvailableLocaleFiles(); std::vector missingFiles; // Check that every config.cpp locale has a corresponding JSON file for (const auto &configLocale : configLocales) { if (!localeFiles.contains(configLocale)) { missingFiles.push_back(configLocale); } } if (!missingFiles.empty()) { std::string errorMsg = "config.cpp locales missing JSON files:\n"; for (const auto &missing : missingFiles) { errorMsg += std::format(" {}.json\n", missing); } FAIL() << errorMsg; } } TEST_F(LocaleConsistencyTest, AllGeneralVueLocalesHaveFiles) { const auto vueLocales = extractGeneralVueLocales(); const auto localeFiles = getAvailableLocaleFiles(); std::vector missingFiles; // Check that every General.vue locale has a corresponding JSON file for (const auto &vueLocale : vueLocales | std::views::keys) { if (!localeFiles.contains(vueLocale)) { missingFiles.push_back(vueLocale); } } if (!missingFiles.empty()) { std::string errorMsg = "General.vue locales missing JSON files:\n"; for (const auto &missing : missingFiles) { errorMsg += std::format(" {}.json\n", missing); } FAIL() << errorMsg; } } TEST_F(LocaleConsistencyTest, ConfigCppAndGeneralVueLocalesMatch) { const auto configLocales = extractConfigCppLocales(); const auto vueLocales = extractGeneralVueLocales(); std::vector configOnlyLocales; std::vector vueOnlyLocales; // Find locales in config.cpp but not in General.vue for (const auto &configLocale : configLocales) { if (!vueLocales.contains(configLocale)) { configOnlyLocales.push_back(configLocale); } } // Find locales in General.vue but not in config.cpp for (const auto &vueLocale : vueLocales | std::views::keys) { if (!configLocales.contains(vueLocale)) { vueOnlyLocales.push_back(vueLocale); } } std::string errorMsg; if (!configOnlyLocales.empty()) { errorMsg += "Locales in config.cpp but not in General.vue:\n"; for (const auto &locale : configOnlyLocales) { errorMsg += std::format(" {}\n", locale); } } if (!vueOnlyLocales.empty()) { errorMsg += "Locales in General.vue but not in config.cpp:\n"; for (const auto &locale : vueOnlyLocales) { errorMsg += std::format(" {}\n", locale); } } if (!errorMsg.empty()) { FAIL() << errorMsg; } } TEST_F(LocaleConsistencyTest, AllLocaleFilesAreValid) { const auto localeFiles = getAvailableLocaleFiles(); std::vector invalidFiles; // Check that all locale files are valid JSON for (const auto &localeFile : localeFiles) { if (!isValidLocaleFile(localeFile)) { invalidFiles.push_back(localeFile); } } if (!invalidFiles.empty()) { std::string errorMsg = "Invalid locale files found:\n"; for (const auto &invalid : invalidFiles) { errorMsg += std::format(" {}.json\n", invalid); } FAIL() << errorMsg; } } TEST_F(LocaleConsistencyTest, LocaleDisplayNamesAreConsistent) { const auto vueLocales = extractGeneralVueLocales(); const auto localeFiles = getAvailableLocaleFiles(); std::vector inconsistentDisplayNames; // Check that all locales in General.vue have corresponding JSON files for (const auto &[localeCode, displayName] : vueLocales) { if (!localeFiles.contains(localeCode)) { inconsistentDisplayNames.push_back( std::format("{}: has display name '{}' but no corresponding JSON file exists", localeCode, displayName) ); } } // Also check that locale files that exist have entries in General.vue for (const auto &localeFile : localeFiles) { if (!vueLocales.contains(localeFile)) { inconsistentDisplayNames.push_back( std::format("{}: has JSON file but no display name in General.vue", localeFile) ); } } if (!inconsistentDisplayNames.empty()) { std::string errorMsg = "Locale display name inconsistencies found:\n"; for (const auto &inconsistent : inconsistentDisplayNames) { errorMsg += std::format(" {}\n", inconsistent); } FAIL() << errorMsg; } } TEST_F(LocaleConsistencyTest, NoOrphanedLocaleReferences) { const auto configLocales = extractConfigCppLocales(); const auto vueLocales = extractGeneralVueLocales(); const auto localeFiles = getAvailableLocaleFiles(); std::vector orphanedReferences; // Check for locale references that don't have corresponding files for (const auto &configLocale : configLocales) { if (!localeFiles.contains(configLocale)) { orphanedReferences.push_back(std::format("config.cpp references missing file: {}.json", configLocale)); } } for (const auto &vueLocale : vueLocales | std::views::keys) { if (!localeFiles.contains(vueLocale)) { orphanedReferences.push_back(std::format("General.vue references missing file: {}.json", vueLocale)); } } if (!orphanedReferences.empty()) { std::string errorMsg = "Orphaned locale references found:\n"; for (const auto &orphaned : orphanedReferences) { errorMsg += std::format(" {}\n", orphaned); } FAIL() << errorMsg; } } TEST_F(LocaleConsistencyTest, TestFrameworkDetectsLocaleInconsistencies) { // Test the framework by simulating a missing locale scenario const std::string testLocale = "test_framework_validation_locale"; auto configLocales = extractConfigCppLocales(); auto vueLocales = extractGeneralVueLocales(); const auto localeFiles = getAvailableLocaleFiles(); // Add a fake locale to config to simulate a missing file configLocales.insert(testLocale); std::vector missingFiles; for (const auto &configLocale : configLocales) { if (!localeFiles.contains(configLocale)) { missingFiles.push_back(configLocale); } } // Verify the test framework detects the missing fake locale bool foundMissingTestLocale = false; for (const auto &missing : missingFiles) { if (missing == testLocale) { foundMissingTestLocale = true; break; } } EXPECT_TRUE(foundMissingTestLocale) << "Test framework failed to detect missing locale file"; EXPECT_GE(missingFiles.size(), 1) << "Test framework should detect at least the fake missing locale"; } ================================================ FILE: tests/tests_common.h ================================================ /** * @file tests/tests_common.h * @brief Common declarations. */ #pragma once #include #include #include #include // XFail/XPass pattern implementation (similar to pytest) namespace test_utils { /** * @brief Marks a test as expected to fail * @param condition The condition under which the test is expected to fail * @param reason The reason why the test is expected to fail */ struct XFailMarker { bool should_xfail; std::string reason; XFailMarker(bool condition, std::string reason): should_xfail(condition), reason(std::move(reason)) {} }; /** * @brief Helper function to handle xfail logic * @param marker The XFailMarker containing condition and reason * @param test_passed Whether the test actually passed */ inline void handleXFail(const XFailMarker &marker, bool test_passed) { if (marker.should_xfail) { if (test_passed) { // XPass: Test was expected to fail but passed const std::string message = "XPASS: Test unexpectedly passed (expected to fail: " + marker.reason + ")"; BOOST_LOG(warning) << message; GTEST_SKIP() << "XPASS: Test unexpectedly passed (expected to fail: " << marker.reason << ")"; } else { // XFail: Test failed as expected const std::string message = "XFAIL: Test failed as expected (" + marker.reason + ")"; BOOST_LOG(info) << message; GTEST_SKIP() << "XFAIL: " << marker.reason; } } // If not marked as xfail, let the test result stand as normal } /** * @brief Check if two values are equal without failing the test * @param actual The actual value * @param expected The expected value * @param message Optional message to include * @return true if values are equal, false otherwise */ template inline bool checkEqual(const T1 &actual, const T2 &expected, const std::string &message = "") { bool result = (actual == expected); if (!message.empty()) { BOOST_LOG(debug) << "Assertion check: " << message << " - " << (result ? "PASSED" : "FAILED"); } return result; } /** * @brief Check if two values are not equal without failing the test * @param actual The actual value * @param expected The expected value * @param message Optional message to include * @return true if values are not equal, false otherwise */ template inline bool checkNotEqual(const T1 &actual, const T2 &expected, const std::string &message = "") { const bool result = (actual != expected); if (!message.empty()) { BOOST_LOG(debug) << "Assertion check: " << message << " - " << (result ? "PASSED" : "FAILED"); } return result; } } // namespace test_utils // Convenience macros for xfail testing #define XFAIL_IF(condition, reason) \ test_utils::XFailMarker xfail_marker((condition), (reason)) #define HANDLE_XFAIL_ASSERT_EQ(actual, expected, message) \ do { \ if (xfail_marker.should_xfail) { \ /* For xfail tests, check the assertion without failing */ \ bool test_passed = test_utils::checkEqual((actual), (expected), (message)); \ test_utils::handleXFail(xfail_marker, test_passed); \ } else { \ /* Run the normal GTest assertion if not marked as xfail */ \ EXPECT_EQ((actual), (expected)) << (message); \ } \ } while (0) #define HANDLE_XFAIL_ASSERT_NE(actual, expected, message) \ do { \ if (xfail_marker.should_xfail) { \ /* For xfail tests, check the assertion without failing */ \ bool test_passed = test_utils::checkNotEqual((actual), (expected), (message)); \ test_utils::handleXFail(xfail_marker, test_passed); \ } else { \ /* Run the normal GTest assertion if not marked as xfail */ \ EXPECT_NE((actual), (expected)) << (message); \ } \ } while (0) // Platform detection macros for convenience #ifdef _WIN32 #define IS_WINDOWS true #else #define IS_WINDOWS false #endif #ifdef __linux__ #define IS_LINUX true #else #define IS_LINUX false #endif #ifdef __APPLE__ #define IS_MACOS true #else #define IS_MACOS false #endif struct PlatformTestSuite: testing::Test { static void SetUpTestSuite() { ASSERT_FALSE(platf_deinit); BOOST_LOG(tests) << "Setting up platform test suite"; platf_deinit = platf::init(); ASSERT_TRUE(platf_deinit); } static void TearDownTestSuite() { ASSERT_TRUE(platf_deinit); platf_deinit = {}; BOOST_LOG(tests) << "Tore down platform test suite"; } private: inline static std::unique_ptr platf_deinit; }; ================================================ FILE: tests/tests_environment.h ================================================ /** * @file tests/tests_environment.h * @brief Declarations for SunshineEnvironment. */ #pragma once #include "tests_common.h" struct SunshineEnvironment: testing::Environment { void SetUp() override { mail::man = std::make_shared(); deinit_log = logging::init(0, "test_sunshine.log"); } void TearDown() override { deinit_log = {}; mail::man = {}; } std::unique_ptr deinit_log; }; ================================================ FILE: tests/tests_events.h ================================================ /** * @file tests/tests_events.h * @brief Declarations for SunshineEventListener. */ #pragma once #include "tests_common.h" struct SunshineEventListener: testing::EmptyTestEventListener { SunshineEventListener() { sink = boost::make_shared(); sink_buffer = boost::make_shared(); sink->locked_backend()->add_stream(sink_buffer); sink->set_formatter(&logging::formatter); } void OnTestProgramStart(const testing::UnitTest &unit_test) override { boost::log::core::get()->add_sink(sink); } void OnTestProgramEnd(const testing::UnitTest &unit_test) override { boost::log::core::get()->remove_sink(sink); } void OnTestStart(const testing::TestInfo &test_info) override { BOOST_LOG(tests) << "From " << test_info.file() << ":" << test_info.line(); BOOST_LOG(tests) << " " << test_info.test_suite_name() << "/" << test_info.name() << " started"; } void OnTestPartResult(const testing::TestPartResult &test_part_result) override { std::string file = test_part_result.file_name(); BOOST_LOG(tests) << "At " << file << ":" << test_part_result.line_number(); auto result_text = test_part_result.passed() ? "Success" : test_part_result.nonfatally_failed() ? "Non-fatal failure" : test_part_result.fatally_failed() ? "Failure" : "Skip"; std::string summary = test_part_result.summary(); std::string message = test_part_result.message(); BOOST_LOG(tests) << " " << result_text << ": " << summary; if (message != summary) { BOOST_LOG(tests) << " " << message; } } void OnTestEnd(const testing::TestInfo &test_info) override { auto &result = *test_info.result(); auto result_text = result.Passed() ? "passed" : result.Skipped() ? "skipped" : "failed"; BOOST_LOG(tests) << test_info.test_suite_name() << "/" << test_info.name() << " " << result_text; if (result.Failed()) { std::cout << sink_buffer->str(); } sink_buffer->str(""); sink_buffer->clear(); } using sink_t = boost::log::sinks::synchronous_sink; boost::shared_ptr sink; boost::shared_ptr sink_buffer; }; ================================================ FILE: tests/tests_log_checker.h ================================================ /** * @file tests/tests_log_checker.h * @brief Utility functions to check log file contents. */ #pragma once #include #include #include #include #include namespace log_checker { /** * @brief Remove the timestamp prefix from a log line. * @param line The log line. * @return The log line without the timestamp prefix. */ inline std::string remove_timestamp_prefix(const std::string &line) { static const std::regex timestamp_regex(R"(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\]: )"); return std::regex_replace(line, timestamp_regex, ""); } /** * @brief Check if a log file contains a line that starts with the given string. * @param log_file Path to the log file. * @param start_str The string that the line should start with. * @return True if such a line is found, false otherwise. */ inline bool line_starts_with(const std::string &log_file, const std::string_view &start_str) { logging::log_flush(); std::ifstream input(log_file); if (!input.is_open()) { return false; } for (std::string line; std::getline(input, line);) { line = remove_timestamp_prefix(line); if (line.rfind(start_str, 0) == 0) { return true; } } return false; } /** * @brief Check if a log file contains a line that ends with the given string. * @param log_file Path to the log file. * @param end_str The string that the line should end with. * @return True if such a line is found, false otherwise. */ inline bool line_ends_with(const std::string &log_file, const std::string_view &end_str) { logging::log_flush(); std::ifstream input(log_file); if (!input.is_open()) { return false; } for (std::string line; std::getline(input, line);) { line = remove_timestamp_prefix(line); if (line.size() >= end_str.size() && line.compare(line.size() - end_str.size(), end_str.size(), end_str) == 0) { return true; } } return false; } /** * @brief Check if a log file contains a line that equals the given string. * @param log_file Path to the log file. * @param str The string that the line should equal. * @return True if such a line is found, false otherwise. */ inline bool line_equals(const std::string &log_file, const std::string_view &str) { logging::log_flush(); std::ifstream input(log_file); if (!input.is_open()) { return false; } for (std::string line; std::getline(input, line);) { line = remove_timestamp_prefix(line); if (line == str) { return true; } } return false; } /** * @brief Check if a log file contains a line that contains the given substring. * @param log_file Path to the log file. * @param substr The substring to search for. * @param case_insensitive Whether the search should be case-insensitive. * @return True if such a line is found, false otherwise. */ inline bool line_contains(const std::string &log_file, const std::string_view &substr, bool case_insensitive = false) { logging::log_flush(); std::ifstream input(log_file); if (!input.is_open()) { return false; } std::string search_str(substr); if (case_insensitive) { // sonarcloud complains about this, but the solution doesn't work for macOS-12 std::transform(search_str.begin(), search_str.end(), search_str.begin(), ::tolower); } for (std::string line; std::getline(input, line);) { line = remove_timestamp_prefix(line); if (case_insensitive) { // sonarcloud complains about this, but the solution doesn't work for macOS-12 std::transform(line.begin(), line.end(), line.begin(), ::tolower); } if (line.find(search_str) != std::string::npos) { return true; } } return false; } } // namespace log_checker ================================================ FILE: tests/tests_main.cpp ================================================ /** * @file tests/tests_main.cpp * @brief Entry point definition. */ #include "tests_common.h" #include "tests_environment.h" #include "tests_events.h" int main(int argc, char **argv) { testing::InitGoogleTest(&argc, argv); testing::AddGlobalTestEnvironment(new SunshineEnvironment); testing::UnitTest::GetInstance()->listeners().Append(new SunshineEventListener); return RUN_ALL_TESTS(); } ================================================ FILE: tests/unit/platform/test_common.cpp ================================================ /** * @file tests/unit/platform/test_common.cpp * @brief Test src/platform/common.*. */ #include "../../tests_common.h" #include #include struct SetEnvTest: ::testing::TestWithParam> { protected: void TearDown() override { // Clean up environment variable after each test const auto &[name, value, expected] = GetParam(); platf::unset_env(name); } }; TEST_P(SetEnvTest, SetEnvironmentVariableTests) { const auto &[name, value, expected] = GetParam(); platf::set_env(name, value); const char *env_value = std::getenv(name.c_str()); if (expected == 0 && !value.empty()) { ASSERT_NE(env_value, nullptr); ASSERT_EQ(std::string(env_value), value); } else { ASSERT_EQ(env_value, nullptr); } } TEST_P(SetEnvTest, UnsetEnvironmentVariableTests) { const auto &[name, value, expected] = GetParam(); platf::unset_env(name); const char *env_value = std::getenv(name.c_str()); if (expected == 0) { ASSERT_EQ(env_value, nullptr); } } INSTANTIATE_TEST_SUITE_P( SetEnvTests, SetEnvTest, ::testing::Values( std::make_tuple("SUNSHINE_UNIT_TEST_ENV_VAR", "test_value_0", 0), std::make_tuple("SUNSHINE_UNIT_TEST_ENV_VAR", "test_value_1", 0), std::make_tuple("", "test_value", -1) ) ); TEST(HostnameTests, TestAsioEquality) { // These should be equivalent on all platforms for ASCII hostnames ASSERT_EQ(platf::get_host_name(), boost::asio::ip::host_name()); } ================================================ FILE: tests/unit/test_audio.cpp ================================================ /** * @file tests/unit/test_audio.cpp * @brief Test src/audio.*. */ #include "../tests_common.h" #include using namespace audio; struct AudioTest: PlatformTestSuite, testing::WithParamInterface, config_t>> { void SetUp() override { m_config = std::get<1>(GetParam()); m_mail = std::make_shared(); } config_t m_config; safe::mail_t m_mail; }; constexpr std::bitset config_flags(const int flag = -1) { std::bitset<3> result = std::bitset(); if (flag >= 0) { result.set(flag); } return result; } INSTANTIATE_TEST_SUITE_P( Configurations, AudioTest, testing::Values( std::make_tuple("HIGH_STEREO", config_t {5, 2, 0x3, {0}, config_flags(config_t::HIGH_QUALITY)}), std::make_tuple("SURROUND51", config_t {5, 6, 0x3F, {0}, config_flags()}), std::make_tuple("SURROUND71", config_t {5, 8, 0x63F, {0}, config_flags()}), 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)}) ), [](const auto &info) { return std::string(std::get<0>(info.param)); } ); TEST_P(AudioTest, TestEncode) { std::thread timer([&] { // Terminate the audio capture after 100 ms std::this_thread::sleep_for(100ms); const auto shutdown_event = m_mail->event(mail::shutdown); const auto audio_packets = m_mail->queue(mail::audio_packets); shutdown_event->raise(true); audio_packets->stop(); }); std::thread capture([&] { const auto packets = m_mail->queue(mail::audio_packets); const auto shutdown_event = m_mail->event(mail::shutdown); while (const auto packet = packets->pop()) { if (shutdown_event->peek()) { break; } if (auto packet_data = packet->second; packet_data.size() == 0) { FAIL() << "Empty packet data"; } } }); audio::capture(m_mail, m_config, nullptr); timer.join(); capture.join(); } ================================================ FILE: tests/unit/test_display_device.cpp ================================================ /** * @file tests/unit/test_display_device.cpp * @brief Test src/display_device.*. */ #include "../tests_common.h" #include #include #include #include namespace { using config_option_e = config::video_t::dd_t::config_option_e; using device_prep_t = display_device::SingleDisplayConfiguration::DevicePreparation; using hdr_option_e = config::video_t::dd_t::hdr_option_e; using hdr_state_e = display_device::HdrState; using resolution_option_e = config::video_t::dd_t::resolution_option_e; using resolution_t = display_device::Resolution; using refresh_rate_option_e = config::video_t::dd_t::refresh_rate_option_e; using rational_t = display_device::Rational; struct failed_to_parse_resolution_tag_t {}; struct failed_to_parse_refresh_rate_tag_t {}; struct no_refresh_rate_tag_t {}; struct no_resolution_tag_t {}; struct client_resolution_t { int width; int height; }; using client_fps_t = int; using sops_enabled_t = bool; using client_wants_hdr_t = bool; constexpr unsigned int max_uint {std::numeric_limits::max()}; const std::string max_uint_string {std::to_string(std::numeric_limits::max())}; template struct DisplayDeviceConfigTest: testing::TestWithParam {}; } // namespace using ParseDeviceId = DisplayDeviceConfigTest>; INSTANTIATE_TEST_SUITE_P( DisplayDeviceConfigTest, ParseDeviceId, testing::Values( std::make_pair(""s, ""s), std::make_pair("SomeId"s, "SomeId"s), std::make_pair("{daeac860-f4db-5208-b1f5-cf59444fb768}"s, "{daeac860-f4db-5208-b1f5-cf59444fb768}"s) ) ); TEST_P(ParseDeviceId, IntegrationTest) { const auto &[input_value, expected_value] = GetParam(); config::video_t video_config {}; video_config.dd.configuration_option = config_option_e::verify_only; video_config.output_name = input_value; const auto result {display_device::parse_configuration(video_config, {})}; EXPECT_EQ(std::get(result).m_device_id, expected_value); } using ParseConfigOption = DisplayDeviceConfigTest>>; INSTANTIATE_TEST_SUITE_P( DisplayDeviceConfigTest, ParseConfigOption, testing::Values( std::make_pair(config_option_e::disabled, std::nullopt), std::make_pair(config_option_e::verify_only, device_prep_t::VerifyOnly), std::make_pair(config_option_e::ensure_active, device_prep_t::EnsureActive), std::make_pair(config_option_e::ensure_primary, device_prep_t::EnsurePrimary), std::make_pair(config_option_e::ensure_only_display, device_prep_t::EnsureOnlyDisplay) ) ); TEST_P(ParseConfigOption, IntegrationTest) { const auto &[input_value, expected_value] = GetParam(); config::video_t video_config {}; video_config.dd.configuration_option = input_value; const auto result {display_device::parse_configuration(video_config, {})}; if (const auto *parsed_config {std::get_if(&result)}; parsed_config) { ASSERT_EQ(parsed_config->m_device_prep, expected_value); } else { ASSERT_EQ(std::get_if(&result) != nullptr, !expected_value); } } using ParseHdrOption = DisplayDeviceConfigTest, std::optional>>; INSTANTIATE_TEST_SUITE_P( DisplayDeviceConfigTest, ParseHdrOption, testing::Values( std::make_pair(std::make_pair(hdr_option_e::disabled, client_wants_hdr_t {true}), std::nullopt), std::make_pair(std::make_pair(hdr_option_e::disabled, client_wants_hdr_t {false}), std::nullopt), std::make_pair(std::make_pair(hdr_option_e::automatic, client_wants_hdr_t {true}), hdr_state_e::Enabled), std::make_pair(std::make_pair(hdr_option_e::automatic, client_wants_hdr_t {false}), hdr_state_e::Disabled) ) ); TEST_P(ParseHdrOption, IntegrationTest) { const auto &[input_value, expected_value] = GetParam(); const auto &[input_hdr_option, input_enable_hdr] = input_value; config::video_t video_config {}; video_config.dd.configuration_option = config_option_e::verify_only; video_config.dd.hdr_option = input_hdr_option; rtsp_stream::launch_session_t session {}; session.enable_hdr = input_enable_hdr; const auto result {display_device::parse_configuration(video_config, session)}; EXPECT_EQ(std::get(result).m_hdr_state, expected_value); } using ParseResolutionOption = DisplayDeviceConfigTest>, std::variant>>; INSTANTIATE_TEST_SUITE_P( DisplayDeviceConfigTest, ParseResolutionOption, testing::Values( //---- Disabled cases ---- std::make_pair(std::make_tuple(resolution_option_e::disabled, sops_enabled_t {true}, client_resolution_t {1920, 1080}), no_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::disabled, sops_enabled_t {true}, "1920x1080"s), no_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::disabled, sops_enabled_t {true}, client_resolution_t {-1, -1}), no_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::disabled, sops_enabled_t {true}, "invalid_res"s), no_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::disabled, sops_enabled_t {false}, client_resolution_t {1920, 1080}), no_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::disabled, sops_enabled_t {false}, "1920x1080"s), no_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::disabled, sops_enabled_t {false}, client_resolution_t {-1, -1}), no_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::disabled, sops_enabled_t {false}, "invalid_res"s), no_resolution_tag_t {}), //---- Automatic cases ---- std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t {true}, client_resolution_t {1920, 1080}), resolution_t {1920, 1080}), std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t {true}, "1920x1080"s), resolution_t {}), std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t {true}, client_resolution_t {-1, -1}), failed_to_parse_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t {true}, "invalid_res"s), resolution_t {}), std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t {false}, client_resolution_t {1920, 1080}), no_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t {false}, "1920x1080"s), no_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t {false}, client_resolution_t {-1, -1}), no_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t {false}, "invalid_res"s), no_resolution_tag_t {}), //---- Manual cases ---- std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t {true}, client_resolution_t {1920, 1080}), failed_to_parse_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t {true}, "1920x1080"s), resolution_t {1920, 1080}), std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t {true}, client_resolution_t {-1, -1}), failed_to_parse_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t {true}, "invalid_res"s), failed_to_parse_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t {false}, client_resolution_t {1920, 1080}), no_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t {false}, "1920x1080"s), no_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t {false}, client_resolution_t {-1, -1}), no_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t {false}, "invalid_res"s), no_resolution_tag_t {}), //---- Both negative values from client are checked ---- std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t {true}, client_resolution_t {0, 0}), resolution_t {0, 0}), std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t {true}, client_resolution_t {-1, 0}), failed_to_parse_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t {true}, client_resolution_t {0, -1}), failed_to_parse_resolution_tag_t {}), //---- Resolution string format validation ---- std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t {true}, "0x0"s), resolution_t {0, 0}), std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t {true}, "0x"s), failed_to_parse_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t {true}, "x0"s), failed_to_parse_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t {true}, "-1x1"s), failed_to_parse_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t {true}, "1x-1"s), failed_to_parse_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t {true}, "x0x0"s), failed_to_parse_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t {true}, "0x0x"s), failed_to_parse_resolution_tag_t {}), //---- String number is out of bounds ---- std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t {true}, max_uint_string + "x"s + max_uint_string), resolution_t {max_uint, max_uint}), std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t {true}, max_uint_string + "0"s + "x"s + max_uint_string), failed_to_parse_resolution_tag_t {}), std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t {true}, max_uint_string + "x"s + max_uint_string + "0"s), failed_to_parse_resolution_tag_t {}) ) ); TEST_P(ParseResolutionOption, IntegrationTest) { const auto &[input_value, expected_value] = GetParam(); const auto &[input_resolution_option, input_enable_sops, input_resolution] = input_value; config::video_t video_config {}; video_config.dd.configuration_option = config_option_e::verify_only; video_config.dd.resolution_option = input_resolution_option; rtsp_stream::launch_session_t session {}; session.enable_sops = input_enable_sops; if (const auto *client_res {std::get_if(&input_resolution)}; client_res) { video_config.dd.manual_resolution = {}; session.width = client_res->width; session.height = client_res->height; } else { video_config.dd.manual_resolution = std::get(input_resolution); session.width = {}; session.height = {}; } const auto result {display_device::parse_configuration(video_config, session)}; if (const auto *failed_option {std::get_if(&expected_value)}; failed_option) { EXPECT_NO_THROW(std::get(result)); } else { std::optional expected_resolution; if (const auto *valid_resolution_option {std::get_if(&expected_value)}; valid_resolution_option) { expected_resolution = *valid_resolution_option; } EXPECT_EQ(std::get(result).m_resolution, expected_resolution); } } using ParseRefreshRateOption = DisplayDeviceConfigTest>, std::variant>>; INSTANTIATE_TEST_SUITE_P( DisplayDeviceConfigTest, ParseRefreshRateOption, testing::Values( //---- Disabled cases ---- std::make_pair(std::make_tuple(refresh_rate_option_e::disabled, client_fps_t {60}), no_refresh_rate_tag_t {}), std::make_pair(std::make_tuple(refresh_rate_option_e::disabled, "60"s), no_refresh_rate_tag_t {}), std::make_pair(std::make_tuple(refresh_rate_option_e::disabled, "59.9885"s), no_refresh_rate_tag_t {}), std::make_pair(std::make_tuple(refresh_rate_option_e::disabled, client_fps_t {-1}), no_refresh_rate_tag_t {}), std::make_pair(std::make_tuple(refresh_rate_option_e::disabled, "invalid_refresh_rate"s), no_refresh_rate_tag_t {}), //---- Automatic cases ---- std::make_pair(std::make_tuple(refresh_rate_option_e::automatic, client_fps_t {60}), rational_t {60, 1}), std::make_pair(std::make_tuple(refresh_rate_option_e::automatic, "60"s), rational_t {0, 1}), std::make_pair(std::make_tuple(refresh_rate_option_e::automatic, "59.9885"s), rational_t {0, 1}), std::make_pair(std::make_tuple(refresh_rate_option_e::automatic, client_fps_t {-1}), failed_to_parse_refresh_rate_tag_t {}), std::make_pair(std::make_tuple(refresh_rate_option_e::automatic, "invalid_refresh_rate"s), rational_t {0, 1}), //---- Manual cases ---- std::make_pair(std::make_tuple(refresh_rate_option_e::manual, client_fps_t {60}), failed_to_parse_refresh_rate_tag_t {}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "60"s), rational_t {60, 1}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "59.9885"s), rational_t {599885, 10000}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, client_fps_t {-1}), failed_to_parse_refresh_rate_tag_t {}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "invalid_refresh_rate"s), failed_to_parse_refresh_rate_tag_t {}), //---- Refresh rate string format validation ---- std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "0000000000000"s), rational_t {0, 1}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "0"s), rational_t {0, 1}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "00000000.0000000"s), rational_t {0, 1}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "0.0"s), rational_t {0, 1}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "000000000000010"s), rational_t {10, 1}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "00000010.0000000"s), rational_t {10, 1}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "00000010.1000000"s), rational_t {101, 10}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "00000010.0100000"s), rational_t {1001, 100}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "00000000.1000000"s), rational_t {1, 10}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "60,0"s), failed_to_parse_refresh_rate_tag_t {}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "-60.0"s), failed_to_parse_refresh_rate_tag_t {}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "60.-0"s), failed_to_parse_refresh_rate_tag_t {}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "a60.0"s), failed_to_parse_refresh_rate_tag_t {}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "60.0b"s), failed_to_parse_refresh_rate_tag_t {}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "a60"s), failed_to_parse_refresh_rate_tag_t {}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "60b"s), failed_to_parse_refresh_rate_tag_t {}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "-60"s), failed_to_parse_refresh_rate_tag_t {}), //---- String number is out of bounds ---- std::make_pair(std::make_tuple(refresh_rate_option_e::manual, max_uint_string), rational_t {max_uint, 1}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, max_uint_string + "0"s), failed_to_parse_refresh_rate_tag_t {}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, max_uint_string.substr(0, 1) + "."s + max_uint_string.substr(1)), rational_t {max_uint, static_cast(std::pow(10, max_uint_string.size() - 1))}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, max_uint_string.substr(0, 1) + "0"s + "."s + max_uint_string.substr(1)), failed_to_parse_refresh_rate_tag_t {}), std::make_pair(std::make_tuple(refresh_rate_option_e::manual, max_uint_string.substr(0, 1) + "."s + "0"s + max_uint_string.substr(1)), failed_to_parse_refresh_rate_tag_t {}) ) ); TEST_P(ParseRefreshRateOption, IntegrationTest) { const auto &[input_value, expected_value] = GetParam(); const auto &[input_refresh_rate_option, input_refresh_rate] = input_value; config::video_t video_config {}; video_config.dd.configuration_option = config_option_e::verify_only; video_config.dd.refresh_rate_option = input_refresh_rate_option; rtsp_stream::launch_session_t session {}; if (const auto *client_refresh_rate {std::get_if(&input_refresh_rate)}; client_refresh_rate) { video_config.dd.manual_refresh_rate = {}; session.fps = *client_refresh_rate; } else { video_config.dd.manual_refresh_rate = std::get(input_refresh_rate); session.fps = {}; } const auto result {display_device::parse_configuration(video_config, session)}; if (const auto *failed_option {std::get_if(&expected_value)}; failed_option) { EXPECT_NO_THROW(std::get(result)); } else { std::optional expected_refresh_rate; if (const auto *valid_refresh_rate_option {std::get_if(&expected_value)}; valid_refresh_rate_option) { expected_refresh_rate = *valid_refresh_rate_option; } EXPECT_EQ(std::get(result).m_refresh_rate, expected_refresh_rate); } } namespace { using res_t = resolution_t; using fps_t = client_fps_t; using remap_entries_t = config::video_t::dd_t::mode_remapping_t; struct no_value_t {}; template struct auto_value_t { T value; }; template struct manual_value_t { T value; }; using resolution_variant_t = std::variant, manual_value_t>; using rational_variant_t = std::variant, manual_value_t>; struct failed_to_remap_t {}; struct final_values_t { std::optional resolution; std::optional refresh_rate; }; const std::string INVALID_RES {"INVALID"}; const std::string INVALID_FPS {"1.23"}; const std::string INVALID_REFRESH_RATE {"INVALID"}; const remap_entries_t VALID_ENTRIES { .mixed = { {"1920x1080", "11", "1024x720", "1.11"}, {"1920x1080", "", "1024x720", "2"}, {"", "33", "1024x720", "3"}, {"1920x720", "44", "1024x720", ""}, {"1920x720", "55", "", "5"}, {"1920x720", "", "1024x720", ""}, {"", "11", "", "7.77"} }, .resolution_only = {{"1920x1080", "", "720x720", ""}, {"1024x720", "", "1920x1920", ""}}, .refresh_rate_only = {{"", "11", "", "1.23"}, {"", "22", "", "2.34"}} }; const remap_entries_t INVALID_REQ_RES { .mixed = {{INVALID_RES, "11", "1024x720", "1.11"}}, .resolution_only = {{INVALID_RES, "", "720x720", ""}}, .refresh_rate_only = {{INVALID_RES, "11", "", "1.23"}} }; const remap_entries_t INVALID_REQ_FPS { .mixed = {{"1920x1080", INVALID_FPS, "1024x720", "1.11"}}, .resolution_only = {{"1920x1080", INVALID_FPS, "720x720", ""}}, .refresh_rate_only = {{"", INVALID_FPS, "", "1.23"}} }; const remap_entries_t INVALID_FINAL_RES { .mixed = {{"1920x1080", "11", INVALID_RES, "1.11"}}, .resolution_only = {{"1920x1080", "", INVALID_RES, ""}}, .refresh_rate_only = {{"", "11", INVALID_RES, "1.23"}} }; const remap_entries_t INVALID_FINAL_REFRESH_RATE { .mixed = {{"1920x1080", "11", "1024x720", INVALID_REFRESH_RATE}}, .resolution_only = {{"1920x1080", "", "720x720", INVALID_REFRESH_RATE}}, .refresh_rate_only = {{"", "11", "", INVALID_REFRESH_RATE}} }; const remap_entries_t EMPTY_REQ_ENTRIES { .mixed = {{"", "", "1024x720", "1.11"}}, .resolution_only = {{"", "", "720x720", ""}}, .refresh_rate_only = {{"", "", "", "1.23"}} }; const remap_entries_t EMPTY_FINAL_ENTRIES { .mixed = {{"1920x1080", "11", "", ""}}, .resolution_only = {{"1920x1080", "", "", ""}}, .refresh_rate_only = {{"", "11", "", ""}} }; using DisplayModeRemapping = DisplayDeviceConfigTest, std::variant>>; INSTANTIATE_TEST_SUITE_P( DisplayDeviceConfigTest, DisplayModeRemapping, testing::Values( //---- Mixed (valid), SOPS enabled ---- std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {true}, VALID_ENTRIES), final_values_t {{{1024, 720}}, {{111, 100}}}), std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, auto_value_t {120}, sops_enabled_t {true}, VALID_ENTRIES), final_values_t {{{1024, 720}}, {{2, 1}}}), std::make_pair(std::make_tuple(auto_value_t {1, 1}, auto_value_t {33}, sops_enabled_t {true}, VALID_ENTRIES), final_values_t {{{1024, 720}}, {{3, 1}}}), std::make_pair(std::make_tuple(auto_value_t {1920, 720}, auto_value_t {44}, sops_enabled_t {true}, VALID_ENTRIES), final_values_t {{{1024, 720}}, {{44, 1}}}), std::make_pair(std::make_tuple(auto_value_t {1920, 720}, auto_value_t {55}, sops_enabled_t {true}, VALID_ENTRIES), final_values_t {{{1920, 720}}, {{5, 1}}}), std::make_pair(std::make_tuple(auto_value_t {1920, 720}, auto_value_t {60}, sops_enabled_t {true}, VALID_ENTRIES), final_values_t {{{1024, 720}}, {{60, 1}}}), std::make_pair(std::make_tuple(auto_value_t {1, 1}, auto_value_t {123}, sops_enabled_t {true}, VALID_ENTRIES), final_values_t {{{1, 1}}, {{123, 1}}}), //---- Mixed (valid), SOPS disabled ---- std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {false}, VALID_ENTRIES), final_values_t {std::nullopt, {{777, 100}}}), std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, auto_value_t {120}, sops_enabled_t {false}, VALID_ENTRIES), final_values_t {std::nullopt, {{120, 1}}}), std::make_pair(std::make_tuple(auto_value_t {1, 1}, auto_value_t {33}, sops_enabled_t {false}, VALID_ENTRIES), final_values_t {std::nullopt, {{33, 1}}}), std::make_pair(std::make_tuple(auto_value_t {1920, 720}, auto_value_t {44}, sops_enabled_t {false}, VALID_ENTRIES), final_values_t {std::nullopt, {{44, 1}}}), std::make_pair(std::make_tuple(auto_value_t {1920, 720}, auto_value_t {55}, sops_enabled_t {false}, VALID_ENTRIES), final_values_t {std::nullopt, {{55, 1}}}), std::make_pair(std::make_tuple(auto_value_t {1920, 720}, auto_value_t {60}, sops_enabled_t {false}, VALID_ENTRIES), final_values_t {std::nullopt, {{60, 1}}}), std::make_pair(std::make_tuple(auto_value_t {1, 1}, auto_value_t {123}, sops_enabled_t {false}, VALID_ENTRIES), final_values_t {std::nullopt, {{123, 1}}}), //---- Resolution only (valid), SOPS enabled ---- std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, manual_value_t {11}, sops_enabled_t {true}, VALID_ENTRIES), final_values_t {{{720, 720}}, {{11, 1}}}), std::make_pair(std::make_tuple(auto_value_t {1024, 720}, no_value_t {}, sops_enabled_t {true}, VALID_ENTRIES), final_values_t {{{1920, 1920}}, std::nullopt}), std::make_pair(std::make_tuple(auto_value_t {11, 11}, manual_value_t {33}, sops_enabled_t {true}, VALID_ENTRIES), final_values_t {{{11, 11}}, {{33, 1}}}), //---- Resolution only (valid), SOPS disabled ---- std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, manual_value_t {11}, sops_enabled_t {false}, VALID_ENTRIES), final_values_t {std::nullopt, {{11, 1}}}), std::make_pair(std::make_tuple(auto_value_t {1024, 720}, no_value_t {}, sops_enabled_t {false}, VALID_ENTRIES), final_values_t {std::nullopt, std::nullopt}), std::make_pair(std::make_tuple(auto_value_t {11, 11}, manual_value_t {33}, sops_enabled_t {false}, VALID_ENTRIES), final_values_t {std::nullopt, {{33, 1}}}), //---- Refresh rate only (valid), SOPS enabled ---- std::make_pair(std::make_tuple(manual_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {true}, VALID_ENTRIES), final_values_t {{{1920, 1080}}, {{123, 100}}}), std::make_pair(std::make_tuple(no_value_t {}, auto_value_t {22}, sops_enabled_t {true}, VALID_ENTRIES), final_values_t {std::nullopt, {{234, 100}}}), std::make_pair(std::make_tuple(manual_value_t {11, 11}, auto_value_t {33}, sops_enabled_t {true}, VALID_ENTRIES), final_values_t {{{11, 11}}, {{33, 1}}}), //---- Refresh rate only (valid), SOPS disabled ---- std::make_pair(std::make_tuple(manual_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {false}, VALID_ENTRIES), final_values_t {std::nullopt, {{123, 100}}}), std::make_pair(std::make_tuple(no_value_t {}, auto_value_t {22}, sops_enabled_t {false}, VALID_ENTRIES), final_values_t {std::nullopt, {{234, 100}}}), std::make_pair(std::make_tuple(manual_value_t {11, 11}, auto_value_t {33}, sops_enabled_t {false}, VALID_ENTRIES), final_values_t {std::nullopt, {{33, 1}}}), //---- No mapping (valid), SOPS enabled ---- std::make_pair(std::make_tuple(manual_value_t {1920, 1080}, manual_value_t {11}, sops_enabled_t {true}, VALID_ENTRIES), final_values_t {{{1920, 1080}}, {{11, 1}}}), std::make_pair(std::make_tuple(no_value_t {}, no_value_t {}, sops_enabled_t {true}, VALID_ENTRIES), final_values_t {std::nullopt, std::nullopt}), //---- No mapping (valid), SOPS disabled ---- std::make_pair(std::make_tuple(manual_value_t {1920, 1080}, manual_value_t {11}, sops_enabled_t {false}, VALID_ENTRIES), final_values_t {std::nullopt, {{11, 1}}}), std::make_pair(std::make_tuple(no_value_t {}, no_value_t {}, sops_enabled_t {false}, VALID_ENTRIES), final_values_t {std::nullopt, std::nullopt}), // ---- Invalid requested resolution, SOPS enabled ---- std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {true}, INVALID_REQ_RES), failed_to_remap_t {}), std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, manual_value_t {11}, sops_enabled_t {true}, INVALID_REQ_RES), failed_to_remap_t {}), std::make_pair(std::make_tuple(manual_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {true}, INVALID_REQ_RES), final_values_t {{{1920, 1080}}, {{123, 100}}}), // ---- Invalid requested resolution, SOPS disabled ---- std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {false}, INVALID_REQ_RES), failed_to_remap_t {}), std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, manual_value_t {11}, sops_enabled_t {false}, INVALID_REQ_RES), failed_to_remap_t {}), std::make_pair(std::make_tuple(manual_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {false}, INVALID_REQ_RES), final_values_t {std::nullopt, {{123, 100}}}), // ---- Invalid requested FPS, SOPS enabled ---- std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {true}, INVALID_REQ_FPS), failed_to_remap_t {}), std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, manual_value_t {11}, sops_enabled_t {true}, INVALID_REQ_FPS), final_values_t {{{720, 720}}, {{11, 1}}}), std::make_pair(std::make_tuple(manual_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {true}, INVALID_REQ_FPS), failed_to_remap_t {}), // ---- Invalid requested FPS, SOPS disabled ---- std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {false}, INVALID_REQ_FPS), failed_to_remap_t {}), std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, manual_value_t {11}, sops_enabled_t {false}, INVALID_REQ_FPS), final_values_t {std::nullopt, {{11, 1}}}), std::make_pair(std::make_tuple(manual_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {false}, INVALID_REQ_FPS), failed_to_remap_t {}), // ---- Invalid final resolution, SOPS enabled ---- std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {true}, INVALID_FINAL_RES), failed_to_remap_t {}), std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, manual_value_t {11}, sops_enabled_t {true}, INVALID_FINAL_RES), failed_to_remap_t {}), std::make_pair(std::make_tuple(manual_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {true}, INVALID_FINAL_RES), final_values_t {{{1920, 1080}}, {{123, 100}}}), // ---- Invalid final resolution, SOPS disabled ---- std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {false}, INVALID_FINAL_RES), failed_to_remap_t {}), std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, manual_value_t {11}, sops_enabled_t {false}, INVALID_FINAL_RES), failed_to_remap_t {}), std::make_pair(std::make_tuple(manual_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {false}, INVALID_FINAL_RES), final_values_t {std::nullopt, {{123, 100}}}), // ---- Invalid final refresh rate, SOPS enabled ---- std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {true}, INVALID_FINAL_REFRESH_RATE), failed_to_remap_t {}), std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, manual_value_t {11}, sops_enabled_t {true}, INVALID_FINAL_REFRESH_RATE), final_values_t {{{720, 720}}, {{11, 1}}}), std::make_pair(std::make_tuple(manual_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {true}, INVALID_FINAL_REFRESH_RATE), failed_to_remap_t {}), // ---- Invalid final refresh rate, SOPS disabled ---- std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {false}, INVALID_FINAL_REFRESH_RATE), failed_to_remap_t {}), std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, manual_value_t {11}, sops_enabled_t {false}, INVALID_FINAL_REFRESH_RATE), final_values_t {std::nullopt, {{11, 1}}}), std::make_pair(std::make_tuple(manual_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {false}, INVALID_FINAL_REFRESH_RATE), failed_to_remap_t {}), // ---- Empty req entries, SOPS enabled ---- std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {true}, EMPTY_REQ_ENTRIES), final_values_t {{{1024, 720}}, {{111, 100}}}), std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, manual_value_t {11}, sops_enabled_t {true}, EMPTY_REQ_ENTRIES), final_values_t {{{720, 720}}, {{11, 1}}}), std::make_pair(std::make_tuple(manual_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {true}, EMPTY_REQ_ENTRIES), final_values_t {{{1920, 1080}}, {{123, 100}}}), // ---- Empty req entries, SOPS disabled ---- std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {false}, EMPTY_REQ_ENTRIES), final_values_t {std::nullopt, {{11, 1}}}), std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, manual_value_t {11}, sops_enabled_t {false}, EMPTY_REQ_ENTRIES), final_values_t {std::nullopt, {{11, 1}}}), std::make_pair(std::make_tuple(manual_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {false}, EMPTY_REQ_ENTRIES), final_values_t {std::nullopt, {{123, 100}}}), // ---- Empty final entries, SOPS enabled ---- std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {true}, EMPTY_FINAL_ENTRIES), failed_to_remap_t {}), std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, manual_value_t {11}, sops_enabled_t {true}, EMPTY_FINAL_ENTRIES), failed_to_remap_t {}), std::make_pair(std::make_tuple(manual_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {true}, EMPTY_FINAL_ENTRIES), failed_to_remap_t {}), // ---- Empty final entries, SOPS disabled ---- std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {false}, EMPTY_FINAL_ENTRIES), failed_to_remap_t {}), std::make_pair(std::make_tuple(auto_value_t {1920, 1080}, manual_value_t {11}, sops_enabled_t {false}, EMPTY_FINAL_ENTRIES), failed_to_remap_t {}), std::make_pair(std::make_tuple(manual_value_t {1920, 1080}, auto_value_t {11}, sops_enabled_t {false}, EMPTY_FINAL_ENTRIES), failed_to_remap_t {}) ) ); TEST_P(DisplayModeRemapping, IntegrationTest) { const auto &[input_value, expected_value] = GetParam(); const auto &[input_res, input_fps, input_enable_sops, input_entries] = input_value; config::video_t video_config {}; rtsp_stream::launch_session_t session {}; { // resolution using enum resolution_option_e; if (const auto *no_res {std::get_if(&input_res)}; no_res) { video_config.dd.resolution_option = disabled; } else if (const auto *auto_res {std::get_if>(&input_res)}; auto_res) { video_config.dd.resolution_option = automatic; session.width = static_cast(auto_res->value.m_width); session.height = static_cast(auto_res->value.m_height); } else { const auto [manual_res] = std::get>(input_res); video_config.dd.resolution_option = manual; video_config.dd.manual_resolution = std::format("{}x{}", static_cast(manual_res.m_width), static_cast(manual_res.m_height)); } } { // fps using enum refresh_rate_option_e; if (const auto *no_fps {std::get_if(&input_fps)}; no_fps) { video_config.dd.refresh_rate_option = disabled; } else if (const auto *auto_fps {std::get_if>(&input_fps)}; auto_fps) { video_config.dd.refresh_rate_option = automatic; session.fps = auto_fps->value; } else { const auto [manual_fps] = std::get>(input_fps); video_config.dd.refresh_rate_option = manual; video_config.dd.manual_refresh_rate = std::to_string(manual_fps); } } video_config.dd.configuration_option = config_option_e::verify_only; video_config.dd.mode_remapping = input_entries; session.enable_sops = input_enable_sops; const auto result {display_device::parse_configuration(video_config, session)}; if (const auto *failed_option {std::get_if(&expected_value)}; failed_option) { EXPECT_NO_THROW(std::get(result)); } else { const auto &[expected_resolution, expected_refresh_rate] = std::get(expected_value); const auto &parsed_config = std::get(result); EXPECT_EQ(parsed_config.m_resolution, expected_resolution); EXPECT_EQ(parsed_config.m_refresh_rate, expected_refresh_rate ? std::make_optional(display_device::FloatingPoint {*expected_refresh_rate}) : std::nullopt); } } } // namespace ================================================ FILE: tests/unit/test_entry_handler.cpp ================================================ /** * @file tests/unit/test_entry_handler.cpp * @brief Test src/entry_handler.*. */ #include "../tests_common.h" #include "../tests_log_checker.h" #include TEST(EntryHandlerTests, LogPublisherDataTest) { // call log_publisher_data log_publisher_data(); // check if specific log messages exist ASSERT_TRUE(log_checker::line_starts_with("test_sunshine.log", "Info: Package Publisher: ")); ASSERT_TRUE(log_checker::line_starts_with("test_sunshine.log", "Info: Publisher Website: ")); ASSERT_TRUE(log_checker::line_starts_with("test_sunshine.log", "Info: Get support: ")); } ================================================ FILE: tests/unit/test_file_handler.cpp ================================================ /** * @file tests/unit/test_file_handler.cpp * @brief Test src/file_handler.*. */ #include "../tests_common.h" #include #include struct FileHandlerParentDirectoryTest: testing::TestWithParam> {}; TEST_P(FileHandlerParentDirectoryTest, Run) { auto [input, expected] = GetParam(); EXPECT_EQ(file_handler::get_parent_directory(input), expected); } INSTANTIATE_TEST_SUITE_P( FileHandlerTests, FileHandlerParentDirectoryTest, testing::Values( std::make_tuple("/path/to/file.txt", "/path/to"), std::make_tuple("/path/to/directory", "/path/to"), std::make_tuple("/path/to/directory/", "/path/to") ) ); struct FileHandlerMakeDirectoryTest: testing::TestWithParam> {}; TEST_P(FileHandlerMakeDirectoryTest, Run) { auto [input, expected, remove] = GetParam(); const std::string test_dir = platf::appdata().string() + "/tests/path/"; input = test_dir + input; EXPECT_EQ(file_handler::make_directory(input), expected); EXPECT_TRUE(std::filesystem::exists(input)); // remove test directory if (remove) { std::filesystem::remove_all(test_dir); EXPECT_FALSE(std::filesystem::exists(test_dir)); } } INSTANTIATE_TEST_SUITE_P( FileHandlerTests, FileHandlerMakeDirectoryTest, testing::Values( std::make_tuple("dir_123", true, false), std::make_tuple("dir_123", true, true), std::make_tuple("dir_123/abc", true, false), std::make_tuple("dir_123/abc", true, true) ) ); struct FileHandlerTests: testing::TestWithParam> {}; INSTANTIATE_TEST_SUITE_P( TestFiles, FileHandlerTests, testing::Values( std::make_tuple(0, ""), // empty file std::make_tuple(1, "a"), // single character std::make_tuple(2, "Mr. Blue Sky - Electric Light Orchestra"), // single line std::make_tuple(3, R"( Morning! Today's forecast calls for blue skies The sun is shining in the sky There ain't a cloud in sight It's stopped raining Everybody's in the play And don't you know, it's a beautiful new day Hey, hey, hey! Running down the avenue See how the sun shines brightly in the city All the streets where once was pity Mr. Blue Sky is living here today! Hey, hey, hey! )") // multi-line ) ); TEST_P(FileHandlerTests, WriteFileTest) { auto [fileNum, content] = GetParam(); const std::string fileName = std::format("write_file_test_{}.txt", fileNum); EXPECT_EQ(file_handler::write_file(fileName.c_str(), content), 0); } TEST_P(FileHandlerTests, ReadFileTest) { auto [fileNum, content] = GetParam(); const std::string fileName = std::format("write_file_test_{}.txt", fileNum); EXPECT_EQ(file_handler::read_file(fileName.c_str()), content); } TEST(FileHandlerTests, ReadMissingFileTest) { // read missing file EXPECT_EQ(file_handler::read_file("non-existing-file.txt"), ""); } ================================================ FILE: tests/unit/test_http_pairing.cpp ================================================ /** * @file tests/unit/test_http_pairing.cpp * @brief Test src/nvhttp.cpp HTTP pairing process */ #include "../tests_common.h" #include using namespace nvhttp; struct pairing_input { std::shared_ptr session; /** * Normally server challenge is generated by the server, but for testing purposes * we can override it with a custom value. This way the process is deterministic. */ std::string override_server_challenge; std::string pin; std::string client_challenge; std::string server_challenge_resp; std::string client_pairing_secret; }; struct pairing_output { bool phase_1_success; bool phase_2_success; bool phase_3_success; bool phase_4_success; }; const std::string PRIVATE_KEY = R"(-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDLePNlWN06FLlM ujWzIX8UICO7SWfH5DXlafVjpxwi/WCkdO6FxixqRNGu71wMvJXFbDlNR8fqX2xo +eq17J3uFKn+qdjmP3L38bkqxhoJ/nCrXkeGyCTQ+Daug63ZYSJeW2Mmf+LAR5/i /fWYfXpSlbcf5XJQPEWvENpLqWu+NOU50dJXIEVYpUXRx2+x4ZbwkH7tVJm94L+C OUyiJKQPyWgU2aFsyJGwHFfePfSUpfYHqbHZV/ILpY59VJairBwE99bx/mBvMI7a hBmJTSDuDffJcPDhFF5kZa0UkQPrPvhXcQaSRti7v0VonEQj8pTSnGYr9ktWKk92 wxDyn9S3AgMBAAECggEAbEhQ14WELg2rUz7hpxPTaiV0fo4hEcrMN+u8sKzVF3Xa QYsNCNoe9urq3/r39LtDxU3D7PGfXYYszmz50Jk8ruAGW8WN7XKkv3i/fxjv8JOc 6EYDMKJAnYkKqLLhCQddX/Oof2udg5BacVWPpvhX6a1NSEc2H6cDupfwZEWkVhMi bCC3JcNmjFa8N7ow1/5VQiYVTjpxfV7GY1GRe7vMvBucdQKH3tUG5PYXKXytXw/j KDLaECiYVT89KbApkI0zhy7I5g3LRq0Rs5fmYLCjVebbuAL1W5CJHFJeFOgMKvnO QSl7MfHkTnzTzUqwkwXjgNMGsTosV4UloL9gXVF6GQKBgQD5fI771WETkpaKjWBe 6XUVSS98IOAPbTGpb8CIhSjzCuztNAJ+0ey1zklQHonMFbdmcWTkTJoF3ECqAos9 vxB4ROg+TdqGDcRrXa7Twtmhv66QvYxttkaK3CqoLX8CCTnjgXBCijo6sCpo6H1T +y55bBDpxZjNFT5BV3+YPBfWQwKBgQDQyNt+saTqJqxGYV7zWQtOqKORRHAjaJpy m5035pky5wORsaxQY8HxbsTIQp9jBSw3SQHLHN/NAXDl2k7VAw/axMc+lj9eW+3z 2Hv5LVgj37jnJYEpYwehvtR0B4jZnXLyLwShoBdRPkGlC5fs9+oWjQZoDwMLZfTg eZVOJm6SfQKBgQDfxYcB/kuKIKsCLvhHaSJpKzF6JoqRi6FFlkScrsMh66TCxSmP 0n58O0Cqqhlyge/z5LVXyBVGOF2Pn6SAh4UgOr4MVAwyvNp2aprKuTQ2zhSnIjx4 k0sGdZ+VJOmMS/YuRwUHya+cwDHp0s3Gq77tja5F38PD/s/OD8sUIqJGvQKBgBfI 6ghy4GC0ayfRa+m5GSqq14dzDntaLU4lIDIAGS/NVYDBhunZk3yXq99Mh6/WJQVf Uc77yRsnsN7ekeB+as33YONmZm2vd1oyLV1jpwjfMcdTZHV8jKAGh1l4ikSQRUoF xTdMb5uXxg6xVWtvisFq63HrU+N2iAESmMnAYxRZAoGAVEFJRRjPrSIUTCCKRiTE br+cHqy6S5iYRxGl9riKySBKeU16fqUACIvUqmqlx4Secj3/Hn/VzYEzkxcSPwGi qMgdS0R+tacca7NopUYaaluneKYdS++DNlT/m+KVHqLynQr54z1qBlThg9KGrpmM LGZkXtQpx6sX7v3Kq56PkNk= -----END PRIVATE KEY-----)"; const std::string PUBLIC_CERT = R"(-----BEGIN CERTIFICATE----- MIIC6zCCAdOgAwIBAgIBATANBgkqhkiG9w0BAQsFADA5MQswCQYDVQQGEwJJVDEW MBQGA1UECgwNR2FtZXNPbldoYWxlczESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIy MDQwOTA5MTYwNVoXDTQyMDQwNDA5MTYwNVowOTELMAkGA1UEBhMCSVQxFjAUBgNV BAoMDUdhbWVzT25XaGFsZXMxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMt482VY3ToUuUy6NbMhfxQgI7tJZ8fkNeVp 9WOnHCL9YKR07oXGLGpE0a7vXAy8lcVsOU1Hx+pfbGj56rXsne4Uqf6p2OY/cvfx uSrGGgn+cKteR4bIJND4Nq6DrdlhIl5bYyZ/4sBHn+L99Zh9elKVtx/lclA8Ra8Q 2kupa7405TnR0lcgRVilRdHHb7HhlvCQfu1Umb3gv4I5TKIkpA/JaBTZoWzIkbAc V9499JSl9gepsdlX8guljn1UlqKsHAT31vH+YG8wjtqEGYlNIO4N98lw8OEUXmRl rRSRA+s++FdxBpJG2Lu/RWicRCPylNKcZiv2S1YqT3bDEPKf1LcCAwEAATANBgkq hkiG9w0BAQsFAAOCAQEAqPBqzvDjl89pZMll3Ge8RS7HeDuzgocrhOcT2jnk4ag7 /TROZuISjDp6+SnL3gPEt7E2OcFAczTg3l/wbT5PFb6vM96saLm4EP0zmLfK1FnM JDRahKutP9rx6RO5OHqsUB+b4jA4W0L9UnXUoLKbjig501AUix0p52FBxu+HJ90r HlLs3Vo6nj4Z/PZXrzaz8dtQ/KJMpd/g/9xlo6BKAnRk5SI8KLhO4hW6zG0QA56j X4wnh1bwdiidqpcgyuKossLOPxbS786WmsesaAWPnpoY6M8aija+ALwNNuWWmyMg 9SVDV76xJzM36Uq7Kg3QJYTlY04WmPIdJHkCtXWf9g== -----END CERTIFICATE-----)"; struct PairingTest: testing::TestWithParam> {}; TEST_P(PairingTest, Run) { auto [input, expected] = GetParam(); boost::property_tree::ptree tree; setup(PRIVATE_KEY, PUBLIC_CERT); // phase 1 getservercert(*input.session, tree, input.pin); ASSERT_EQ(tree.get("root.paired") == 1, expected.phase_1_success); if (!expected.phase_1_success) { return; } // phase 2 clientchallenge(*input.session, tree, input.client_challenge); ASSERT_EQ(tree.get("root.paired") == 1, expected.phase_2_success); if (!expected.phase_2_success) { return; } // phase 3 serverchallengeresp(*input.session, tree, input.server_challenge_resp); ASSERT_EQ(tree.get("root.paired") == 1, expected.phase_3_success); if (!expected.phase_3_success) { return; } input.session->serverchallenge = input.override_server_challenge; // phase 4 auto input_client_cert = input.session->client.cert; // Will be moved auto add_cert = std::make_shared>(30); clientpairingsecret(*input.session, add_cert, tree, input.client_pairing_secret); ASSERT_EQ(tree.get("root.paired") == 1, expected.phase_4_success); // Check that we actually added the input client certificate to `add_cert` if (expected.phase_4_success) { ASSERT_EQ(add_cert->peek(), true); auto cert = add_cert->pop(); char added_subject_name[256]; X509_NAME_oneline(X509_get_subject_name(cert.get()), added_subject_name, sizeof(added_subject_name)); auto input_cert = crypto::x509(input_client_cert); char original_suject_name[256]; X509_NAME_oneline(X509_get_subject_name(input_cert.get()), original_suject_name, sizeof(original_suject_name)); ASSERT_EQ(std::string(added_subject_name), std::string(original_suject_name)); } } INSTANTIATE_TEST_SUITE_P( TestWorkingPairing, PairingTest, testing::Values( std::make_tuple( pairing_input { .session = std::make_shared( pair_session_t { .client = { .uniqueID = "1234", .cert = PUBLIC_CERT, .name = "test" }, .async_insert_pin = {.salt = "ff5dc6eda99339a8a0793e216c4257c4"} } ), .override_server_challenge = util::from_hex_vec("AAAAAAAAAAAAAAAA", true), .pin = "5338", /* AES("CLIENT CHALLENGE") */ .client_challenge = util::from_hex_vec("741CD3D6890C16DA39D53BCA0893AAF0", true), /* SHA = SHA265(server_challenge + public cert signature + "SECRET ") = "6493DAE49C913E1AEAF37C1072F71D664B72B2C4DA1FFB4720BECE0D929E008A" * AES( SHA ) */ .server_challenge_resp = util::from_hex_vec("920BABAE9F7599AA1CA8EC87FB3454C91872A7D8D5127DDC176C2FDAE635CF7A", true), /* secret + x509 signature */ .client_pairing_secret = util::from_hex_vec("000102030405060708090A0B0C0D0EFF" // secret "9BB74D8DE2FF006C3F47FC45EFDAA97D433783AFAB3ACD85CA7ED2330BB2A7BD18A5B044AF8CAC177116FAE8A6E8E44653A8944A0F8EA138B2E013756D847D2C4FC52F736E2E7E9B4154712B18F8307B2A161E010F0587744163E42ECA9EA548FC435756EDCF1FEB94037631ABB72B29DDAC0EA5E61F2DBFCC3B20AA021473CC85AC98D88052CA6618ED1701EFBF142C18D5E779A3155B84DF65057D4823EC194E6DF14006793E8D7A3DCCE20A911636C4E01ECA8B54B9DE9F256F15DE9A980EA024B30D77579140D45EC220C738164BDEEEBF7364AE94A5FF9B784B40F2E640CE8603017DEEAC7B2AD77B807C643B7B349C110FE15F94C7B3D37FF15FDFBE26", true) }, pairing_output {true, true, true, true} ), // Testing that when passing some empty values we aren't triggering any exception std::make_tuple(pairing_input { .session = std::make_shared(pair_session_t {.client = {}, .async_insert_pin = {.salt = "ff5dc6eda99339a8a0793e216c4257c4"}}), .override_server_challenge = {}, .pin = {}, .client_challenge = {}, .server_challenge_resp = {}, .client_pairing_secret = util::from_hex_vec("000102030405060708090A0B0C0D0EFFxDEADBEEF", true), }, // Only phase 4 will fail, when we check what has been exchanged pairing_output {true, true, true, false}), // Testing that when passing some empty values we aren't triggering any exception std::make_tuple(pairing_input { .session = std::make_shared(pair_session_t {.client = {.cert = PUBLIC_CERT}, .async_insert_pin = {.salt = "ff5dc6eda99339a8a0793e216c4257c4"}}), .override_server_challenge = {}, .pin = {}, .client_challenge = {}, .server_challenge_resp = {}, .client_pairing_secret = util::from_hex_vec("000102030405060708090A0B0C0D0EFFxDEADBEEF", true), }, // Only phase 4 will fail, when we check what has been exchanged pairing_output {true, true, true, false}) ) ); INSTANTIATE_TEST_SUITE_P( TestFailingPairing, PairingTest, testing::Values( /** * Wrong PIN */ std::make_tuple( pairing_input { .session = std::make_shared( pair_session_t { .client = { .uniqueID = "1234", .cert = PUBLIC_CERT, .name = "test" }, .async_insert_pin = {.salt = "ff5dc6eda99339a8a0793e216c4257c4"} } ), .override_server_challenge = util::from_hex_vec("AAAAAAAAAAAAAAAA", true), .pin = "0000", .client_challenge = util::from_hex_vec("741CD3D6890C16DA39D53BCA0893AAF0", true), .server_challenge_resp = util::from_hex_vec("920BABAE9F7599AA1CA8EC87FB3454C91872A7D8D5127DDC176C2FDAE635CF7A", true), .client_pairing_secret = util::from_hex_vec("000102030405060708090A0B0C0D0EFF" // secret "9BB74D8DE2FF006C3F47FC45EFDAA97D433783AFAB3ACD85CA7ED2330BB2A7BD18A5B044AF8CAC177116FAE8A6E8E44653A8944A0F8EA138B2E013756D847D2C4FC52F736E2E7E9B4154712B18F8307B2A161E010F0587744163E42ECA9EA548FC435756EDCF1FEB94037631ABB72B29DDAC0EA5E61F2DBFCC3B20AA021473CC85AC98D88052CA6618ED1701EFBF142C18D5E779A3155B84DF65057D4823EC194E6DF14006793E8D7A3DCCE20A911636C4E01ECA8B54B9DE9F256F15DE9A980EA024B30D77579140D45EC220C738164BDEEEBF7364AE94A5FF9B784B40F2E640CE8603017DEEAC7B2AD77B807C643B7B349C110FE15F94C7B3D37FF15FDFBE26", true) }, pairing_output {true, true, true, false} ), /** * Wrong client challenge */ std::make_tuple(pairing_input {.session = std::make_shared(pair_session_t {.client = {.uniqueID = "1234", .cert = PUBLIC_CERT, .name = "test"}, .async_insert_pin = {.salt = "ff5dc6eda99339a8a0793e216c4257c4"}}), .override_server_challenge = util::from_hex_vec("AAAAAAAAAAAAAAAA", true), .pin = "5338", .client_challenge = util::from_hex_vec("741CD3D6890C16DA39D53BCA0893AAF0", true), .server_challenge_resp = util::from_hex_vec("WRONG", true), .client_pairing_secret = util::from_hex_vec("000102030405060708090A0B0C0D0EFF" // secret "9BB74D8DE2FF006C3F47FC45EFDAA97D433783AFAB3ACD85CA7ED2330BB2A7BD18A5B044AF8CAC177116FAE8A6E8E44653A8944A0F8EA138B2E013756D847D2C4FC52F736E2E7E9B4154712B18F8307B2A161E010F0587744163E42ECA9EA548FC435756EDCF1FEB94037631ABB72B29DDAC0EA5E61F2DBFCC3B20AA021473CC85AC98D88052CA6618ED1701EFBF142C18D5E779A3155B84DF65057D4823EC194E6DF14006793E8D7A3DCCE20A911636C4E01ECA8B54B9DE9F256F15DE9A980EA024B30D77579140D45EC220C738164BDEEEBF7364AE94A5FF9B784B40F2E640CE8603017DEEAC7B2AD77B807C643B7B349C110FE15F94C7B3D37FF15FDFBE26", true)}, pairing_output {true, true, true, false}), /** * Wrong signature */ std::make_tuple(pairing_input {.session = std::make_shared(pair_session_t {.client = {.uniqueID = "1234", .cert = PUBLIC_CERT, .name = "test"}, .async_insert_pin = {.salt = "ff5dc6eda99339a8a0793e216c4257c4"}}), .override_server_challenge = util::from_hex_vec("AAAAAAAAAAAAAAAA", true), .pin = "5338", .client_challenge = util::from_hex_vec("741CD3D6890C16DA39D53BCA0893AAF0", true), .server_challenge_resp = util::from_hex_vec("920BABAE9F7599AA1CA8EC87FB3454C91872A7D8D5127DDC176C2FDAE635CF7A", true), .client_pairing_secret = util::from_hex_vec("000102030405060708090A0B0C0D0EFF" // secret "NOSIGNATURE", // Wrong signature true)}, pairing_output {true, true, true, false}), /** * null values (phase 1) */ std::make_tuple(pairing_input {.session = std::make_shared()}, pairing_output {false}), /** * null values (phase 4, phase 2 and 3 have no reason to fail since we are running them in order) */ std::make_tuple(pairing_input {.session = std::make_shared(pair_session_t {.async_insert_pin = {.salt = "ff5dc6eda99339a8a0793e216c4257c4"}})}, pairing_output {true, true, true, false}) ) ); TEST(PairingTest, OutOfOrderCalls) { boost::property_tree::ptree tree; setup(PRIVATE_KEY, PUBLIC_CERT); pair_session_t sess {}; clientchallenge(sess, tree, "test"); ASSERT_FALSE(tree.get("root.paired") == 1); serverchallengeresp(sess, tree, "test"); ASSERT_FALSE(tree.get("root.paired") == 1); auto add_cert = std::make_shared>(30); clientpairingsecret(sess, add_cert, tree, "test"); ASSERT_FALSE(tree.get("root.paired") == 1); // This should work, it's the first time we call it sess.async_insert_pin.salt = "ff5dc6eda99339a8a0793e216c4257c4"; getservercert(sess, tree, "test"); ASSERT_TRUE(tree.get("root.paired") == 1); // Calling it again should fail getservercert(sess, tree, "test"); ASSERT_FALSE(tree.get("root.paired") == 1); } ================================================ FILE: tests/unit/test_httpcommon.cpp ================================================ /** * @file tests/unit/test_httpcommon.cpp * @brief Test src/httpcommon.*. */ // test imports #include "../tests_common.h" // lib imports #include // local imports #include struct UrlEscapeTest: testing::TestWithParam> {}; TEST_P(UrlEscapeTest, Run) { const auto &[input, expected] = GetParam(); ASSERT_EQ(http::url_escape(input), expected); } INSTANTIATE_TEST_SUITE_P( UrlEscapeTests, UrlEscapeTest, testing::Values( std::make_tuple("igdb_0123456789", "igdb_0123456789"), std::make_tuple("../../../", "..%2F..%2F..%2F"), std::make_tuple("..*\\", "..%2A%5C") ) ); struct UrlGetHostTest: testing::TestWithParam> {}; TEST_P(UrlGetHostTest, Run) { const auto &[input, expected] = GetParam(); ASSERT_EQ(http::url_get_host(input), expected); } INSTANTIATE_TEST_SUITE_P( UrlGetHostTests, UrlGetHostTest, testing::Values( std::make_tuple("https://images.igdb.com/example.txt", "images.igdb.com"), std::make_tuple("http://localhost:8080", "localhost"), std::make_tuple("nonsense!!}{::", "") ) ); struct DownloadFileTest: testing::TestWithParam> {}; TEST_P(DownloadFileTest, Run) { const auto &[url, filename] = GetParam(); const std::string test_dir = platf::appdata().string() + "/tests/"; std::string path = test_dir + filename; ASSERT_TRUE(http::download_file(url, path, CURL_SSLVERSION_TLSv1_0)); } #ifdef SUNSHINE_BUILD_FLATPAK // requires running `npm run serve` prior to running the tests constexpr const char *URL_1 = "http://0.0.0.0:3000/hello.txt"; constexpr const char *URL_2 = "http://0.0.0.0:3000/hello-redirect.txt"; #else constexpr const char *URL_1 = "https://httpbin.org/base64/aGVsbG8h"; constexpr const char *URL_2 = "https://httpbin.org/redirect-to?url=/base64/aGVsbG8h"; #endif INSTANTIATE_TEST_SUITE_P( DownloadFileTests, DownloadFileTest, testing::Values( std::make_tuple(URL_1, "hello.txt"), std::make_tuple(URL_2, "hello-redirect.txt") ) ); ================================================ FILE: tests/unit/test_logging.cpp ================================================ /** * @file tests/unit/test_logging.cpp * @brief Test src/logging.*. */ #include "../tests_common.h" #include "../tests_log_checker.h" #include #include #include namespace { std::array log_levels = { std::tuple("verbose", &verbose), std::tuple("debug", &debug), std::tuple("info", &info), std::tuple("warning", &warning), std::tuple("error", &error), std::tuple("fatal", &fatal), }; constexpr auto log_file = "test_sunshine.log"; } // namespace struct LogLevelsTest: testing::TestWithParam {}; INSTANTIATE_TEST_SUITE_P( Logging, LogLevelsTest, testing::ValuesIn(log_levels), [](const auto &info) { return std::string(std::get<0>(info.param)); } ); TEST_P(LogLevelsTest, PutMessage) { auto [label, plogger] = GetParam(); ASSERT_TRUE(plogger); auto &logger = *plogger; std::random_device rand_dev; std::mt19937_64 rand_gen(rand_dev()); auto test_message = std::format("{}{}", rand_gen(), rand_gen()); BOOST_LOG(logger) << test_message; ASSERT_TRUE(log_checker::line_contains(log_file, test_message)); } ================================================ FILE: tests/unit/test_mouse.cpp ================================================ /** * @file tests/unit/test_mouse.cpp * @brief Test src/input.*. */ #include "../tests_common.h" #include struct MouseHIDTest: PlatformTestSuite, testing::WithParamInterface { void SetUp() override { #ifdef _WIN32 // TODO: Windows tests are failing, `get_mouse_loc` seems broken and `platf::abs_mouse` too // the alternative `platf::abs_mouse` method seem to work better during tests, // but I'm not sure about real work GTEST_SKIP() << "TODO Windows"; #elif __linux__ // TODO: Inputtino waiting https://github.com/games-on-whales/inputtino/issues/6 is resolved. GTEST_SKIP() << "TODO Inputtino"; #endif } void TearDown() override { std::this_thread::sleep_for(std::chrono::milliseconds(200)); } }; INSTANTIATE_TEST_SUITE_P( MouseInputs, MouseHIDTest, testing::Values( util::point_t {40, 40}, util::point_t {70, 150} ) ); // todo: add tests for hitting screen edges TEST_P(MouseHIDTest, MoveInputTest) { util::point_t mouse_delta = GetParam(); BOOST_LOG(tests) << "MoveInputTest:: got param: " << mouse_delta; platf::input_t input = platf::input(); BOOST_LOG(tests) << "MoveInputTest:: init input"; BOOST_LOG(tests) << "MoveInputTest:: get current mouse loc"; auto old_loc = platf::get_mouse_loc(input); BOOST_LOG(tests) << "MoveInputTest:: got current mouse loc: " << old_loc; BOOST_LOG(tests) << "MoveInputTest:: move: " << mouse_delta; platf::move_mouse(input, mouse_delta.x, mouse_delta.y); std::this_thread::sleep_for(std::chrono::milliseconds(200)); BOOST_LOG(tests) << "MoveInputTest:: moved: " << mouse_delta; BOOST_LOG(tests) << "MoveInputTest:: get updated mouse loc"; auto new_loc = platf::get_mouse_loc(input); BOOST_LOG(tests) << "MoveInputTest:: got updated mouse loc: " << new_loc; bool has_input_moved = old_loc.x != new_loc.x && old_loc.y != new_loc.y; if (!has_input_moved) { BOOST_LOG(tests) << "MoveInputTest:: haven't moved"; } else { BOOST_LOG(tests) << "MoveInputTest:: moved"; } EXPECT_TRUE(has_input_moved); // Verify we moved as much as we requested EXPECT_EQ(new_loc.x - old_loc.x, mouse_delta.x); EXPECT_EQ(new_loc.y - old_loc.y, mouse_delta.y); } TEST_P(MouseHIDTest, AbsMoveInputTest) { util::point_t mouse_pos = GetParam(); BOOST_LOG(tests) << "AbsMoveInputTest:: got param: " << mouse_pos; platf::input_t input = platf::input(); BOOST_LOG(tests) << "AbsMoveInputTest:: init input"; BOOST_LOG(tests) << "AbsMoveInputTest:: get current mouse loc"; auto old_loc = platf::get_mouse_loc(input); BOOST_LOG(tests) << "AbsMoveInputTest:: got current mouse loc: " << old_loc; #ifdef _WIN32 platf::touch_port_t abs_port { 0, 0, 65535, 65535 }; #elif __linux__ platf::touch_port_t abs_port { 0, 0, 19200, 12000 }; #else platf::touch_port_t abs_port {}; #endif BOOST_LOG(tests) << "AbsMoveInputTest:: move: " << mouse_pos; platf::abs_mouse(input, abs_port, mouse_pos.x, mouse_pos.y); std::this_thread::sleep_for(std::chrono::milliseconds(200)); BOOST_LOG(tests) << "AbsMoveInputTest:: moved: " << mouse_pos; BOOST_LOG(tests) << "AbsMoveInputTest:: get updated mouse loc"; auto new_loc = platf::get_mouse_loc(input); BOOST_LOG(tests) << "AbsMoveInputTest:: got updated mouse loc: " << new_loc; bool has_input_moved = old_loc.x != new_loc.x || old_loc.y != new_loc.y; if (!has_input_moved) { BOOST_LOG(tests) << "AbsMoveInputTest:: haven't moved"; } else { BOOST_LOG(tests) << "AbsMoveInputTest:: moved"; } EXPECT_TRUE(has_input_moved); // Verify we moved to the absolute coordinate EXPECT_EQ(new_loc.x, mouse_pos.x); EXPECT_EQ(new_loc.y, mouse_pos.y); } ================================================ FILE: tests/unit/test_network.cpp ================================================ /** * @file tests/unit/test_network.cpp * @brief Test src/network.* */ #include "../tests_common.h" #include struct MdnsInstanceNameTest: testing::TestWithParam> {}; TEST_P(MdnsInstanceNameTest, Run) { auto [input, expected] = GetParam(); ASSERT_EQ(net::mdns_instance_name(input), expected); } INSTANTIATE_TEST_SUITE_P( MdnsInstanceNameTests, MdnsInstanceNameTest, testing::Values( std::make_tuple("shortname-123", "shortname-123"), std::make_tuple("space 123", "space-123"), std::make_tuple("hostname.domain.test", "hostname"), std::make_tuple("&", "Sunshine"), std::make_tuple("", "Sunshine"), std::make_tuple("😁", "Sunshine"), std::make_tuple(std::string(128, 'a'), std::string(63, 'a')) ) ); ================================================ FILE: tests/unit/test_rswrapper.cpp ================================================ /** * @file tests/unit/test_rswrapper.cpp * @brief Test src/rswrapper.* */ extern "C" { #include } #include "../tests_common.h" TEST(ReedSolomonWrapperTests, InitTest) { reed_solomon_init(); // Ensure all function pointers were populated ASSERT_NE(reed_solomon_new, nullptr); ASSERT_NE(reed_solomon_release, nullptr); ASSERT_NE(reed_solomon_encode, nullptr); ASSERT_NE(reed_solomon_decode, nullptr); } TEST(ReedSolomonWrapperTests, EncodeTest) { reed_solomon_init(); auto rs = reed_solomon_new(1, 1); ASSERT_NE(rs, nullptr); uint8_t dataShard[16] = {}; uint8_t fecShard[16] = {}; // If we picked the incorrect ISA in our wrapper, we should crash here uint8_t *shardPtrs[2] = {dataShard, fecShard}; auto ret = reed_solomon_encode(rs, shardPtrs, 2, sizeof(dataShard)); ASSERT_EQ(ret, 0); reed_solomon_release(rs); } ================================================ FILE: tests/unit/test_stream.cpp ================================================ /** * @file tests/unit/test_stream.cpp * @brief Test src/stream.* */ #include #include #include #include namespace stream { std::vector concat_and_insert(uint64_t insert_size, uint64_t slice_size, const std::string_view &data1, const std::string_view &data2); } #include "../tests_common.h" TEST(ConcatAndInsertTests, ConcatNoInsertionTest) { char b1[] = {'a', 'b'}; char b2[] = {'c', 'd', 'e'}; auto res = stream::concat_and_insert(0, 2, std::string_view {b1, sizeof(b1)}, std::string_view {b2, sizeof(b2)}); auto expected = std::vector {'a', 'b', 'c', 'd', 'e'}; ASSERT_EQ(res, expected); } TEST(ConcatAndInsertTests, ConcatLargeStrideTest) { char b1[] = {'a', 'b'}; char b2[] = {'c', 'd', 'e'}; auto res = stream::concat_and_insert(1, sizeof(b1) + sizeof(b2) + 1, std::string_view {b1, sizeof(b1)}, std::string_view {b2, sizeof(b2)}); auto expected = std::vector {0, 'a', 'b', 'c', 'd', 'e'}; ASSERT_EQ(res, expected); } TEST(ConcatAndInsertTests, ConcatSmallStrideTest) { char b1[] = {'a', 'b'}; char b2[] = {'c', 'd', 'e'}; auto res = stream::concat_and_insert(1, 1, std::string_view {b1, sizeof(b1)}, std::string_view {b2, sizeof(b2)}); auto expected = std::vector {0, 'a', 0, 'b', 0, 'c', 0, 'd', 0, 'e'}; ASSERT_EQ(res, expected); } ================================================ FILE: tests/unit/test_video.cpp ================================================ /** * @file tests/unit/test_video.cpp * @brief Test src/video.*. */ #include "../tests_common.h" #include struct EncoderTest: PlatformTestSuite, testing::WithParamInterface { void SetUp() override { auto &encoder = *GetParam(); if (!video::validate_encoder(encoder, false)) { // Encoder failed validation, // if it's software - fail, otherwise skip if (encoder.name == "software") { FAIL() << "Software encoder not available"; } else { GTEST_SKIP() << "Encoder not available"; } } } }; INSTANTIATE_TEST_SUITE_P( EncoderVariants, EncoderTest, testing::Values( #if !defined(__APPLE__) &video::nvenc, #endif #ifdef _WIN32 &video::amdvce, &video::quicksync, #endif #ifdef __linux__ &video::vaapi, #endif #ifdef __APPLE__ &video::videotoolbox, #endif &video::software ), [](const auto &info) { return std::string(info.param->name); } ); TEST_P(EncoderTest, ValidateEncoder) { // todo:: test something besides fixture setup } ================================================ FILE: third-party/.clang-format-ignore ================================================ ================================================ FILE: third-party/glad/include/EGL/eglplatform.h ================================================ #ifndef __eglplatform_h_ #define __eglplatform_h_ /* ** Copyright 2007-2020 The Khronos Group Inc. ** SPDX-License-Identifier: Apache-2.0 */ /* Platform-specific types and definitions for egl.h * * Adopters may modify khrplatform.h and this file to suit their platform. * You are encouraged to submit all modifications to the Khronos group so that * they can be included in future versions of this file. Please submit changes * by filing an issue or pull request on the public Khronos EGL Registry, at * https://www.github.com/KhronosGroup/EGL-Registry/ */ #include /* Macros used in EGL function prototype declarations. * * EGL functions should be prototyped as: * * EGLAPI return-type EGLAPIENTRY eglFunction(arguments); * typedef return-type (EXPAPIENTRYP PFNEGLFUNCTIONPROC) (arguments); * * KHRONOS_APICALL and KHRONOS_APIENTRY are defined in KHR/khrplatform.h */ #ifndef EGLAPI #define EGLAPI KHRONOS_APICALL #endif #ifndef EGLAPIENTRY #define EGLAPIENTRY KHRONOS_APIENTRY #endif #define EGLAPIENTRYP EGLAPIENTRY * /* The types NativeDisplayType, NativeWindowType, and NativePixmapType * are aliases of window-system-dependent types, such as X Display * or * Windows Device Context. They must be defined in platform-specific * code below. The EGL-prefixed versions of Native*Type are the same * types, renamed in EGL 1.3 so all types in the API start with "EGL". * * Khronos STRONGLY RECOMMENDS that you use the default definitions * provided below, since these changes affect both binary and source * portability of applications using EGL running on different EGL * implementations. */ #if defined(EGL_NO_PLATFORM_SPECIFIC_TYPES) typedef void *EGLNativeDisplayType; typedef void *EGLNativePixmapType; typedef void *EGLNativeWindowType; #elif defined(_WIN32) || defined(__VC32__) && !defined(__CYGWIN__) && !defined(__SCITECH_SNAP__) /* Win32 and WinCE */ #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN 1 #endif #include typedef HDC EGLNativeDisplayType; typedef HBITMAP EGLNativePixmapType; typedef HWND EGLNativeWindowType; #elif defined(__EMSCRIPTEN__) typedef int EGLNativeDisplayType; typedef int EGLNativePixmapType; typedef int EGLNativeWindowType; #elif defined(__WINSCW__) || defined(__SYMBIAN32__) /* Symbian */ typedef int EGLNativeDisplayType; typedef void *EGLNativePixmapType; typedef void *EGLNativeWindowType; #elif defined(WL_EGL_PLATFORM) typedef struct wl_display *EGLNativeDisplayType; typedef struct wl_egl_pixmap *EGLNativePixmapType; typedef struct wl_egl_window *EGLNativeWindowType; #elif defined(__GBM__) typedef struct gbm_device *EGLNativeDisplayType; typedef struct gbm_bo *EGLNativePixmapType; typedef void *EGLNativeWindowType; #elif defined(__ANDROID__) || defined(ANDROID) struct ANativeWindow; struct egl_native_pixmap_t; typedef void *EGLNativeDisplayType; typedef struct egl_native_pixmap_t *EGLNativePixmapType; typedef struct ANativeWindow *EGLNativeWindowType; #elif defined(USE_OZONE) typedef intptr_t EGLNativeDisplayType; typedef intptr_t EGLNativePixmapType; typedef intptr_t EGLNativeWindowType; #elif defined(__unix__) && defined(EGL_NO_X11) typedef void *EGLNativeDisplayType; typedef khronos_uintptr_t EGLNativePixmapType; typedef khronos_uintptr_t EGLNativeWindowType; #elif defined(__unix__) || defined(USE_X11) /* X11 (tentative) */ #include #include typedef Display *EGLNativeDisplayType; typedef Pixmap EGLNativePixmapType; typedef Window EGLNativeWindowType; #elif defined(__APPLE__) typedef int EGLNativeDisplayType; typedef void *EGLNativePixmapType; typedef void *EGLNativeWindowType; #elif defined(__HAIKU__) #include typedef void *EGLNativeDisplayType; typedef khronos_uintptr_t EGLNativePixmapType; typedef khronos_uintptr_t EGLNativeWindowType; #elif defined(__Fuchsia__) typedef void *EGLNativeDisplayType; typedef khronos_uintptr_t EGLNativePixmapType; typedef khronos_uintptr_t EGLNativeWindowType; #else #error "Platform not recognized" #endif /* EGL 1.2 types, renamed for consistency in EGL 1.3 */ typedef EGLNativeDisplayType NativeDisplayType; typedef EGLNativePixmapType NativePixmapType; typedef EGLNativeWindowType NativeWindowType; /* Define EGLint. This must be a signed integral type large enough to contain * all legal attribute names and values passed into and out of EGL, whether * their type is boolean, bitmask, enumerant (symbolic constant), integer, * handle, or other. While in general a 32-bit integer will suffice, if * handles are 64 bit types, then EGLint should be defined as a signed 64-bit * integer type. */ typedef khronos_int32_t EGLint; /* C++ / C typecast macros for special EGL handle values */ #if defined(__cplusplus) #define EGL_CAST(type, value) (static_cast(value)) #else #define EGL_CAST(type, value) ((type) (value)) #endif #endif /* __eglplatform_h */ ================================================ FILE: third-party/glad/include/KHR/khrplatform.h ================================================ #ifndef __khrplatform_h_ #define __khrplatform_h_ /* ** Copyright (c) 2008-2018 The Khronos Group Inc. ** ** Permission is hereby granted, free of charge, to any person obtaining a ** copy of this software and/or associated documentation files (the ** "Materials"), to deal in the Materials without restriction, including ** without limitation the rights to use, copy, modify, merge, publish, ** distribute, sublicense, and/or sell copies of the Materials, and to ** permit persons to whom the Materials are furnished to do so, subject to ** the following conditions: ** ** The above copyright notice and this permission notice shall be included ** in all copies or substantial portions of the Materials. ** ** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE ** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. */ /* Khronos platform-specific types and definitions. * * The master copy of khrplatform.h is maintained in the Khronos EGL * Registry repository at https://github.com/KhronosGroup/EGL-Registry * The last semantic modification to khrplatform.h was at commit ID: * 67a3e0864c2d75ea5287b9f3d2eb74a745936692 * * Adopters may modify this file to suit their platform. Adopters are * encouraged to submit platform specific modifications to the Khronos * group so that they can be included in future versions of this file. * Please submit changes by filing pull requests or issues on * the EGL Registry repository linked above. * * * See the Implementer's Guidelines for information about where this file * should be located on your system and for more details of its use: * http://www.khronos.org/registry/implementers_guide.pdf * * This file should be included as * #include * by Khronos client API header files that use its types and defines. * * The types in khrplatform.h should only be used to define API-specific types. * * Types defined in khrplatform.h: * khronos_int8_t signed 8 bit * khronos_uint8_t unsigned 8 bit * khronos_int16_t signed 16 bit * khronos_uint16_t unsigned 16 bit * khronos_int32_t signed 32 bit * khronos_uint32_t unsigned 32 bit * khronos_int64_t signed 64 bit * khronos_uint64_t unsigned 64 bit * khronos_intptr_t signed same number of bits as a pointer * khronos_uintptr_t unsigned same number of bits as a pointer * khronos_ssize_t signed size * khronos_usize_t unsigned size * khronos_float_t signed 32 bit floating point * khronos_time_ns_t unsigned 64 bit time in nanoseconds * khronos_utime_nanoseconds_t unsigned time interval or absolute time in * nanoseconds * khronos_stime_nanoseconds_t signed time interval in nanoseconds * khronos_boolean_enum_t enumerated boolean type. This should * only be used as a base type when a client API's boolean type is * an enum. Client APIs which use an integer or other type for * booleans cannot use this as the base type for their boolean. * * Tokens defined in khrplatform.h: * * KHRONOS_FALSE, KHRONOS_TRUE Enumerated boolean false/true values. * * KHRONOS_SUPPORT_INT64 is 1 if 64 bit integers are supported; otherwise 0. * KHRONOS_SUPPORT_FLOAT is 1 if floats are supported; otherwise 0. * * Calling convention macros defined in this file: * KHRONOS_APICALL * KHRONOS_APIENTRY * KHRONOS_APIATTRIBUTES * * These may be used in function prototypes as: * * KHRONOS_APICALL void KHRONOS_APIENTRY funcname( * int arg1, * int arg2) KHRONOS_APIATTRIBUTES; */ #if defined(__SCITECH_SNAP__) && !defined(KHRONOS_STATIC) #define KHRONOS_STATIC 1 #endif /*------------------------------------------------------------------------- * Definition of KHRONOS_APICALL *------------------------------------------------------------------------- * This precedes the return type of the function in the function prototype. */ #if defined(KHRONOS_STATIC) /* If the preprocessor constant KHRONOS_STATIC is defined, make the * header compatible with static linking. */ #define KHRONOS_APICALL #elif defined(_WIN32) #define KHRONOS_APICALL __declspec(dllimport) #elif defined(__SYMBIAN32__) #define KHRONOS_APICALL IMPORT_C #elif defined(__ANDROID__) #define KHRONOS_APICALL __attribute__((visibility("default"))) #else #define KHRONOS_APICALL #endif /*------------------------------------------------------------------------- * Definition of KHRONOS_APIENTRY *------------------------------------------------------------------------- * This follows the return type of the function and precedes the function * name in the function prototype. */ #if defined(_WIN32) && !defined(_WIN32_WCE) && !defined(__SCITECH_SNAP__) /* Win32 but not WinCE */ #define KHRONOS_APIENTRY __stdcall #else #define KHRONOS_APIENTRY #endif /*------------------------------------------------------------------------- * Definition of KHRONOS_APIATTRIBUTES *------------------------------------------------------------------------- * This follows the closing parenthesis of the function prototype arguments. */ #if defined(__ARMCC_2__) #define KHRONOS_APIATTRIBUTES __softfp #else #define KHRONOS_APIATTRIBUTES #endif /*------------------------------------------------------------------------- * basic type definitions *-----------------------------------------------------------------------*/ #if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__GNUC__) || defined(__SCO__) || defined(__USLC__) /* * Using */ #include typedef int32_t khronos_int32_t; typedef uint32_t khronos_uint32_t; typedef int64_t khronos_int64_t; typedef uint64_t khronos_uint64_t; #define KHRONOS_SUPPORT_INT64 1 #define KHRONOS_SUPPORT_FLOAT 1 #elif defined(__VMS) || defined(__sgi) /* * Using */ #include typedef int32_t khronos_int32_t; typedef uint32_t khronos_uint32_t; typedef int64_t khronos_int64_t; typedef uint64_t khronos_uint64_t; #define KHRONOS_SUPPORT_INT64 1 #define KHRONOS_SUPPORT_FLOAT 1 #elif defined(_WIN32) && !defined(__SCITECH_SNAP__) /* * Win32 */ typedef __int32 khronos_int32_t; typedef unsigned __int32 khronos_uint32_t; typedef __int64 khronos_int64_t; typedef unsigned __int64 khronos_uint64_t; #define KHRONOS_SUPPORT_INT64 1 #define KHRONOS_SUPPORT_FLOAT 1 #elif defined(__sun__) || defined(__digital__) /* * Sun or Digital */ typedef int khronos_int32_t; typedef unsigned int khronos_uint32_t; #if defined(__arch64__) || defined(_LP64) typedef long int khronos_int64_t; typedef unsigned long int khronos_uint64_t; #else typedef long long int khronos_int64_t; typedef unsigned long long int khronos_uint64_t; #endif /* __arch64__ */ #define KHRONOS_SUPPORT_INT64 1 #define KHRONOS_SUPPORT_FLOAT 1 #elif 0 /* * Hypothetical platform with no float or int64 support */ typedef int khronos_int32_t; typedef unsigned int khronos_uint32_t; #define KHRONOS_SUPPORT_INT64 0 #define KHRONOS_SUPPORT_FLOAT 0 #else /* * Generic fallback */ #include typedef int32_t khronos_int32_t; typedef uint32_t khronos_uint32_t; typedef int64_t khronos_int64_t; typedef uint64_t khronos_uint64_t; #define KHRONOS_SUPPORT_INT64 1 #define KHRONOS_SUPPORT_FLOAT 1 #endif /* * Types that are (so far) the same on all platforms */ typedef signed char khronos_int8_t; typedef unsigned char khronos_uint8_t; typedef signed short int khronos_int16_t; typedef unsigned short int khronos_uint16_t; /* * Types that differ between LLP64 and LP64 architectures - in LLP64, * pointers are 64 bits, but 'long' is still 32 bits. Win64 appears * to be the only LLP64 architecture in current use. */ #ifdef _WIN64 typedef signed long long int khronos_intptr_t; typedef unsigned long long int khronos_uintptr_t; typedef signed long long int khronos_ssize_t; typedef unsigned long long int khronos_usize_t; #else typedef signed long int khronos_intptr_t; typedef unsigned long int khronos_uintptr_t; typedef signed long int khronos_ssize_t; typedef unsigned long int khronos_usize_t; #endif #if KHRONOS_SUPPORT_FLOAT /* * Float type */ typedef float khronos_float_t; #endif #if KHRONOS_SUPPORT_INT64 /* Time types * * These types can be used to represent a time interval in nanoseconds or * an absolute Unadjusted System Time. Unadjusted System Time is the number * of nanoseconds since some arbitrary system event (e.g. since the last * time the system booted). The Unadjusted System Time is an unsigned * 64 bit value that wraps back to 0 every 584 years. Time intervals * may be either signed or unsigned. */ typedef khronos_uint64_t khronos_utime_nanoseconds_t; typedef khronos_int64_t khronos_stime_nanoseconds_t; #endif /* * Dummy value used to pad enum types to 32 bits. */ #ifndef KHRONOS_MAX_ENUM #define KHRONOS_MAX_ENUM 0x7FFFFFFF #endif /* * Enumerated boolean type * * Values other than zero should be considered to be true. Therefore * comparisons should not be made against KHRONOS_TRUE. */ typedef enum { KHRONOS_FALSE = 0, KHRONOS_TRUE = 1, KHRONOS_BOOLEAN_ENUM_FORCE_SIZE = KHRONOS_MAX_ENUM } khronos_boolean_enum_t; #endif /* __khrplatform_h_ */ ================================================ FILE: third-party/glad/include/glad/egl.h ================================================ /** * Loader generated by glad 2.0.0-beta on Tue Jun 1 10:22:05 2021 * * Generator: C/C++ * Specification: egl * Extensions: 0 * * APIs: * - egl=1.5 * * Options: * - ALIAS = False * - DEBUG = False * - HEADER_ONLY = False * - LOADER = True * - MX = True * - MX_GLOBAL = False * - ON_DEMAND = False * * Commandline: * --api='egl=1.5' --extensions='' c --loader --mx * * Online: * http://glad.sh/#api=egl%3D1.5&extensions=&generator=c&options=LOADER%2CMX * */ #ifndef GLAD_EGL_H_ #define GLAD_EGL_H_ #define GLAD_EGL #define GLAD_OPTION_EGL_LOADER #ifdef __cplusplus extern "C" { #endif #ifndef GLAD_PLATFORM_H_ #define GLAD_PLATFORM_H_ #ifndef GLAD_PLATFORM_WIN32 #if defined(_WIN32) || defined(__WIN32__) || defined(WIN32) || defined(__MINGW32__) #define GLAD_PLATFORM_WIN32 1 #else #define GLAD_PLATFORM_WIN32 0 #endif #endif #ifndef GLAD_PLATFORM_APPLE #ifdef __APPLE__ #define GLAD_PLATFORM_APPLE 1 #else #define GLAD_PLATFORM_APPLE 0 #endif #endif #ifndef GLAD_PLATFORM_EMSCRIPTEN #ifdef __EMSCRIPTEN__ #define GLAD_PLATFORM_EMSCRIPTEN 1 #else #define GLAD_PLATFORM_EMSCRIPTEN 0 #endif #endif #ifndef GLAD_PLATFORM_UWP #if defined(_MSC_VER) && !defined(GLAD_INTERNAL_HAVE_WINAPIFAMILY) #ifdef __has_include #if __has_include() #define GLAD_INTERNAL_HAVE_WINAPIFAMILY 1 #endif #elif _MSC_VER >= 1700 && !_USING_V110_SDK71_ #define GLAD_INTERNAL_HAVE_WINAPIFAMILY 1 #endif #endif #ifdef GLAD_INTERNAL_HAVE_WINAPIFAMILY #include #if !WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) && WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_APP) #define GLAD_PLATFORM_UWP 1 #endif #endif #ifndef GLAD_PLATFORM_UWP #define GLAD_PLATFORM_UWP 0 #endif #endif #ifdef __GNUC__ #define GLAD_GNUC_EXTENSION __extension__ #else #define GLAD_GNUC_EXTENSION #endif #ifndef GLAD_API_CALL #if defined(GLAD_API_CALL_EXPORT) #if GLAD_PLATFORM_WIN32 || defined(__CYGWIN__) #if defined(GLAD_API_CALL_EXPORT_BUILD) #if defined(__GNUC__) #define GLAD_API_CALL __attribute__((dllexport)) extern #else #define GLAD_API_CALL __declspec(dllexport) extern #endif #else #if defined(__GNUC__) #define GLAD_API_CALL __attribute__((dllimport)) extern #else #define GLAD_API_CALL __declspec(dllimport) extern #endif #endif #elif defined(__GNUC__) && defined(GLAD_API_CALL_EXPORT_BUILD) #define GLAD_API_CALL __attribute__((visibility("default"))) extern #else #define GLAD_API_CALL extern #endif #else #define GLAD_API_CALL extern #endif #endif #ifdef APIENTRY #define GLAD_API_PTR APIENTRY #elif GLAD_PLATFORM_WIN32 #define GLAD_API_PTR __stdcall #else #define GLAD_API_PTR #endif #ifndef GLAPI #define GLAPI GLAD_API_CALL #endif #ifndef GLAPIENTRY #define GLAPIENTRY GLAD_API_PTR #endif #define GLAD_MAKE_VERSION(major, minor) (major * 10000 + minor) #define GLAD_VERSION_MAJOR(version) (version / 10000) #define GLAD_VERSION_MINOR(version) (version % 10000) #define GLAD_GENERATOR_VERSION "2.0.0-beta" typedef void (*GLADapiproc)(void); typedef GLADapiproc (*GLADloadfunc)(const char *name); typedef GLADapiproc (*GLADuserptrloadfunc)(void *userptr, const char *name); typedef void (*GLADprecallback)(const char *name, GLADapiproc apiproc, int len_args, ...); typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apiproc, int len_args, ...); #endif /* GLAD_PLATFORM_H_ */ #define EGL_ALPHA_FORMAT 0x3088 #define EGL_ALPHA_FORMAT_NONPRE 0x308B #define EGL_ALPHA_FORMAT_PRE 0x308C #define EGL_ALPHA_MASK_SIZE 0x303E #define EGL_ALPHA_SIZE 0x3021 #define EGL_BACK_BUFFER 0x3084 #define EGL_BAD_ACCESS 0x3002 #define EGL_BAD_ALLOC 0x3003 #define EGL_BAD_ATTRIBUTE 0x3004 #define EGL_BAD_CONFIG 0x3005 #define EGL_BAD_CONTEXT 0x3006 #define EGL_BAD_CURRENT_SURFACE 0x3007 #define EGL_BAD_DISPLAY 0x3008 #define EGL_BAD_MATCH 0x3009 #define EGL_BAD_NATIVE_PIXMAP 0x300A #define EGL_BAD_NATIVE_WINDOW 0x300B #define EGL_BAD_PARAMETER 0x300C #define EGL_BAD_SURFACE 0x300D #define EGL_BIND_TO_TEXTURE_RGB 0x3039 #define EGL_BIND_TO_TEXTURE_RGBA 0x303A #define EGL_BLUE_SIZE 0x3022 #define EGL_BUFFER_DESTROYED 0x3095 #define EGL_BUFFER_PRESERVED 0x3094 #define EGL_BUFFER_SIZE 0x3020 #define EGL_CLIENT_APIS 0x308D #define EGL_CL_EVENT_HANDLE 0x309C #define EGL_COLORSPACE 0x3087 #define EGL_COLORSPACE_LINEAR 0x308A #define EGL_COLORSPACE_sRGB 0x3089 #define EGL_COLOR_BUFFER_TYPE 0x303F #define EGL_CONDITION_SATISFIED 0x30F6 #define EGL_CONFIG_CAVEAT 0x3027 #define EGL_CONFIG_ID 0x3028 #define EGL_CONFORMANT 0x3042 #define EGL_CONTEXT_CLIENT_TYPE 0x3097 #define EGL_CONTEXT_CLIENT_VERSION 0x3098 #define EGL_CONTEXT_LOST 0x300E #define EGL_CONTEXT_MAJOR_VERSION 0x3098 #define EGL_CONTEXT_MINOR_VERSION 0x30FB #define EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT 0x00000002 #define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001 #define EGL_CONTEXT_OPENGL_DEBUG 0x31B0 #define EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE 0x31B1 #define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD #define EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY 0x31BD #define EGL_CONTEXT_OPENGL_ROBUST_ACCESS 0x31B2 #define EGL_CORE_NATIVE_ENGINE 0x305B #define EGL_DEFAULT_DISPLAY EGL_CAST(EGLNativeDisplayType, 0) #define EGL_DEPTH_SIZE 0x3025 #define EGL_DISPLAY_SCALING 10000 #define EGL_DONT_CARE EGL_CAST(EGLint, -1) #define EGL_DRAW 0x3059 #define EGL_EXTENSIONS 0x3055 #define EGL_FALSE 0 #define EGL_FOREVER 0xFFFFFFFFFFFFFFFF #define EGL_GL_COLORSPACE 0x309D #define EGL_GL_COLORSPACE_LINEAR 0x308A #define EGL_GL_COLORSPACE_SRGB 0x3089 #define EGL_GL_RENDERBUFFER 0x30B9 #define EGL_GL_TEXTURE_2D 0x30B1 #define EGL_GL_TEXTURE_3D 0x30B2 #define EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_X 0x30B4 #define EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 0x30B6 #define EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 0x30B8 #define EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_X 0x30B3 #define EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_Y 0x30B5 #define EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_Z 0x30B7 #define EGL_GL_TEXTURE_LEVEL 0x30BC #define EGL_GL_TEXTURE_ZOFFSET 0x30BD #define EGL_GREEN_SIZE 0x3023 #define EGL_HEIGHT 0x3056 #define EGL_HORIZONTAL_RESOLUTION 0x3090 #define EGL_IMAGE_PRESERVED 0x30D2 #define EGL_LARGEST_PBUFFER 0x3058 #define EGL_LEVEL 0x3029 #define EGL_LOSE_CONTEXT_ON_RESET 0x31BF #define EGL_LUMINANCE_BUFFER 0x308F #define EGL_LUMINANCE_SIZE 0x303D #define EGL_MATCH_NATIVE_PIXMAP 0x3041 #define EGL_MAX_PBUFFER_HEIGHT 0x302A #define EGL_MAX_PBUFFER_PIXELS 0x302B #define EGL_MAX_PBUFFER_WIDTH 0x302C #define EGL_MAX_SWAP_INTERVAL 0x303C #define EGL_MIN_SWAP_INTERVAL 0x303B #define EGL_MIPMAP_LEVEL 0x3083 #define EGL_MIPMAP_TEXTURE 0x3082 #define EGL_MULTISAMPLE_RESOLVE 0x3099 #define EGL_MULTISAMPLE_RESOLVE_BOX 0x309B #define EGL_MULTISAMPLE_RESOLVE_BOX_BIT 0x0200 #define EGL_MULTISAMPLE_RESOLVE_DEFAULT 0x309A #define EGL_NATIVE_RENDERABLE 0x302D #define EGL_NATIVE_VISUAL_ID 0x302E #define EGL_NATIVE_VISUAL_TYPE 0x302F #define EGL_NONE 0x3038 #define EGL_NON_CONFORMANT_CONFIG 0x3051 #define EGL_NOT_INITIALIZED 0x3001 #define EGL_NO_CONTEXT EGL_CAST(EGLContext, 0) #define EGL_NO_DISPLAY EGL_CAST(EGLDisplay, 0) #define EGL_NO_IMAGE EGL_CAST(EGLImage, 0) #define EGL_NO_RESET_NOTIFICATION 0x31BE #define EGL_NO_SURFACE EGL_CAST(EGLSurface, 0) #define EGL_NO_SYNC EGL_CAST(EGLSync, 0) #define EGL_NO_TEXTURE 0x305C #define EGL_OPENGL_API 0x30A2 #define EGL_OPENGL_BIT 0x0008 #define EGL_OPENGL_ES2_BIT 0x0004 #define EGL_OPENGL_ES3_BIT 0x00000040 #define EGL_OPENGL_ES_API 0x30A0 #define EGL_OPENGL_ES_BIT 0x0001 #define EGL_OPENVG_API 0x30A1 #define EGL_OPENVG_BIT 0x0002 #define EGL_OPENVG_IMAGE 0x3096 #define EGL_PBUFFER_BIT 0x0001 #define EGL_PIXEL_ASPECT_RATIO 0x3092 #define EGL_PIXMAP_BIT 0x0002 #define EGL_READ 0x305A #define EGL_RED_SIZE 0x3024 #define EGL_RENDERABLE_TYPE 0x3040 #define EGL_RENDER_BUFFER 0x3086 #define EGL_RGB_BUFFER 0x308E #define EGL_SAMPLES 0x3031 #define EGL_SAMPLE_BUFFERS 0x3032 #define EGL_SIGNALED 0x30F2 #define EGL_SINGLE_BUFFER 0x3085 #define EGL_SLOW_CONFIG 0x3050 #define EGL_STENCIL_SIZE 0x3026 #define EGL_SUCCESS 0x3000 #define EGL_SURFACE_TYPE 0x3033 #define EGL_SWAP_BEHAVIOR 0x3093 #define EGL_SWAP_BEHAVIOR_PRESERVED_BIT 0x0400 #define EGL_SYNC_CL_EVENT 0x30FE #define EGL_SYNC_CL_EVENT_COMPLETE 0x30FF #define EGL_SYNC_CONDITION 0x30F8 #define EGL_SYNC_FENCE 0x30F9 #define EGL_SYNC_FLUSH_COMMANDS_BIT 0x0001 #define EGL_SYNC_PRIOR_COMMANDS_COMPLETE 0x30F0 #define EGL_SYNC_STATUS 0x30F1 #define EGL_SYNC_TYPE 0x30F7 #define EGL_TEXTURE_2D 0x305F #define EGL_TEXTURE_FORMAT 0x3080 #define EGL_TEXTURE_RGB 0x305D #define EGL_TEXTURE_RGBA 0x305E #define EGL_TEXTURE_TARGET 0x3081 #define EGL_TIMEOUT_EXPIRED 0x30F5 #define EGL_TRANSPARENT_BLUE_VALUE 0x3035 #define EGL_TRANSPARENT_GREEN_VALUE 0x3036 #define EGL_TRANSPARENT_RED_VALUE 0x3037 #define EGL_TRANSPARENT_RGB 0x3052 #define EGL_TRANSPARENT_TYPE 0x3034 #define EGL_TRUE 1 #define EGL_UNKNOWN EGL_CAST(EGLint, -1) #define EGL_UNSIGNALED 0x30F3 #define EGL_VENDOR 0x3053 #define EGL_VERSION 0x3054 #define EGL_VERTICAL_RESOLUTION 0x3091 #define EGL_VG_ALPHA_FORMAT 0x3088 #define EGL_VG_ALPHA_FORMAT_NONPRE 0x308B #define EGL_VG_ALPHA_FORMAT_PRE 0x308C #define EGL_VG_ALPHA_FORMAT_PRE_BIT 0x0040 #define EGL_VG_COLORSPACE 0x3087 #define EGL_VG_COLORSPACE_LINEAR 0x308A #define EGL_VG_COLORSPACE_LINEAR_BIT 0x0020 #define EGL_VG_COLORSPACE_sRGB 0x3089 #define EGL_WIDTH 0x3057 #define EGL_WINDOW_BIT 0x0004 #include #include struct AHardwareBuffer; struct wl_buffer; struct wl_display; struct wl_resource; typedef unsigned int EGLBoolean; typedef unsigned int EGLenum; typedef intptr_t EGLAttribKHR; typedef intptr_t EGLAttrib; typedef void *EGLClientBuffer; typedef void *EGLConfig; typedef void *EGLContext; typedef void *EGLDeviceEXT; typedef void *EGLDisplay; typedef void *EGLImage; typedef void *EGLImageKHR; typedef void *EGLLabelKHR; typedef void *EGLObjectKHR; typedef void *EGLOutputLayerEXT; typedef void *EGLOutputPortEXT; typedef void *EGLStreamKHR; typedef void *EGLSurface; typedef void *EGLSync; typedef void *EGLSyncKHR; typedef void *EGLSyncNV; typedef void (*__eglMustCastToProperFunctionPointerType)(void); typedef khronos_utime_nanoseconds_t EGLTimeKHR; typedef khronos_utime_nanoseconds_t EGLTime; typedef khronos_utime_nanoseconds_t EGLTimeNV; typedef khronos_utime_nanoseconds_t EGLuint64NV; typedef khronos_uint64_t EGLuint64KHR; typedef khronos_stime_nanoseconds_t EGLnsecsANDROID; typedef int EGLNativeFileDescriptorKHR; typedef khronos_ssize_t EGLsizeiANDROID; typedef void (*EGLSetBlobFuncANDROID)(const void *key, EGLsizeiANDROID keySize, const void *value, EGLsizeiANDROID valueSize); typedef EGLsizeiANDROID (*EGLGetBlobFuncANDROID)(const void *key, EGLsizeiANDROID keySize, void *value, EGLsizeiANDROID valueSize); struct EGLClientPixmapHI { void *pData; EGLint iWidth; EGLint iHeight; EGLint iStride; }; typedef void(GLAD_API_PTR *EGLDEBUGPROCKHR)(EGLenum error, const char *command, EGLint messageType, EGLLabelKHR threadLabel, EGLLabelKHR objectLabel, const char *message); #define PFNEGLBINDWAYLANDDISPLAYWL PFNEGLBINDWAYLANDDISPLAYWLPROC #define PFNEGLUNBINDWAYLANDDISPLAYWL PFNEGLUNBINDWAYLANDDISPLAYWLPROC #define PFNEGLQUERYWAYLANDBUFFERWL PFNEGLQUERYWAYLANDBUFFERWLPROC #define PFNEGLCREATEWAYLANDBUFFERFROMIMAGEWL PFNEGLCREATEWAYLANDBUFFERFROMIMAGEWLPROC #define EGL_VERSION_1_0 1 GLAD_API_CALL int GLAD_EGL_VERSION_1_0; #define EGL_VERSION_1_1 1 GLAD_API_CALL int GLAD_EGL_VERSION_1_1; #define EGL_VERSION_1_2 1 GLAD_API_CALL int GLAD_EGL_VERSION_1_2; #define EGL_VERSION_1_3 1 GLAD_API_CALL int GLAD_EGL_VERSION_1_3; #define EGL_VERSION_1_4 1 GLAD_API_CALL int GLAD_EGL_VERSION_1_4; #define EGL_VERSION_1_5 1 GLAD_API_CALL int GLAD_EGL_VERSION_1_5; typedef EGLBoolean(GLAD_API_PTR *PFNEGLBINDAPIPROC)(EGLenum api); typedef EGLBoolean(GLAD_API_PTR *PFNEGLBINDTEXIMAGEPROC)(EGLDisplay dpy, EGLSurface surface, EGLint buffer); typedef EGLBoolean(GLAD_API_PTR *PFNEGLCHOOSECONFIGPROC)(EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config); typedef EGLint(GLAD_API_PTR *PFNEGLCLIENTWAITSYNCPROC)(EGLDisplay dpy, EGLSync sync, EGLint flags, EGLTime timeout); typedef EGLBoolean(GLAD_API_PTR *PFNEGLCOPYBUFFERSPROC)(EGLDisplay dpy, EGLSurface surface, EGLNativePixmapType target); typedef EGLContext(GLAD_API_PTR *PFNEGLCREATECONTEXTPROC)(EGLDisplay dpy, EGLConfig config, EGLContext share_context, const EGLint *attrib_list); typedef EGLImage(GLAD_API_PTR *PFNEGLCREATEIMAGEPROC)(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLAttrib *attrib_list); typedef EGLSurface(GLAD_API_PTR *PFNEGLCREATEPBUFFERFROMCLIENTBUFFERPROC)(EGLDisplay dpy, EGLenum buftype, EGLClientBuffer buffer, EGLConfig config, const EGLint *attrib_list); typedef EGLSurface(GLAD_API_PTR *PFNEGLCREATEPBUFFERSURFACEPROC)(EGLDisplay dpy, EGLConfig config, const EGLint *attrib_list); typedef EGLSurface(GLAD_API_PTR *PFNEGLCREATEPIXMAPSURFACEPROC)(EGLDisplay dpy, EGLConfig config, EGLNativePixmapType pixmap, const EGLint *attrib_list); typedef EGLSurface(GLAD_API_PTR *PFNEGLCREATEPLATFORMPIXMAPSURFACEPROC)(EGLDisplay dpy, EGLConfig config, void *native_pixmap, const EGLAttrib *attrib_list); typedef EGLSurface(GLAD_API_PTR *PFNEGLCREATEPLATFORMWINDOWSURFACEPROC)(EGLDisplay dpy, EGLConfig config, void *native_window, const EGLAttrib *attrib_list); typedef EGLSync(GLAD_API_PTR *PFNEGLCREATESYNCPROC)(EGLDisplay dpy, EGLenum type, const EGLAttrib *attrib_list); typedef EGLSurface(GLAD_API_PTR *PFNEGLCREATEWINDOWSURFACEPROC)(EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const EGLint *attrib_list); typedef EGLBoolean(GLAD_API_PTR *PFNEGLDESTROYCONTEXTPROC)(EGLDisplay dpy, EGLContext ctx); typedef EGLBoolean(GLAD_API_PTR *PFNEGLDESTROYIMAGEPROC)(EGLDisplay dpy, EGLImage image); typedef EGLBoolean(GLAD_API_PTR *PFNEGLDESTROYSURFACEPROC)(EGLDisplay dpy, EGLSurface surface); typedef EGLBoolean(GLAD_API_PTR *PFNEGLDESTROYSYNCPROC)(EGLDisplay dpy, EGLSync sync); typedef EGLBoolean(GLAD_API_PTR *PFNEGLGETCONFIGATTRIBPROC)(EGLDisplay dpy, EGLConfig config, EGLint attribute, EGLint *value); typedef EGLBoolean(GLAD_API_PTR *PFNEGLGETCONFIGSPROC)(EGLDisplay dpy, EGLConfig *configs, EGLint config_size, EGLint *num_config); typedef EGLContext(GLAD_API_PTR *PFNEGLGETCURRENTCONTEXTPROC)(void); typedef EGLDisplay(GLAD_API_PTR *PFNEGLGETCURRENTDISPLAYPROC)(void); typedef EGLSurface(GLAD_API_PTR *PFNEGLGETCURRENTSURFACEPROC)(EGLint readdraw); typedef EGLDisplay(GLAD_API_PTR *PFNEGLGETDISPLAYPROC)(EGLNativeDisplayType display_id); typedef EGLint(GLAD_API_PTR *PFNEGLGETERRORPROC)(void); typedef EGLDisplay(GLAD_API_PTR *PFNEGLGETPLATFORMDISPLAYPROC)(EGLenum platform, void *native_display, const EGLAttrib *attrib_list); typedef __eglMustCastToProperFunctionPointerType(GLAD_API_PTR *PFNEGLGETPROCADDRESSPROC)(const char *procname); typedef EGLBoolean(GLAD_API_PTR *PFNEGLGETSYNCATTRIBPROC)(EGLDisplay dpy, EGLSync sync, EGLint attribute, EGLAttrib *value); typedef EGLBoolean(GLAD_API_PTR *PFNEGLINITIALIZEPROC)(EGLDisplay dpy, EGLint *major, EGLint *minor); typedef EGLBoolean(GLAD_API_PTR *PFNEGLMAKECURRENTPROC)(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx); typedef EGLenum(GLAD_API_PTR *PFNEGLQUERYAPIPROC)(void); typedef EGLBoolean(GLAD_API_PTR *PFNEGLQUERYCONTEXTPROC)(EGLDisplay dpy, EGLContext ctx, EGLint attribute, EGLint *value); typedef const char *(GLAD_API_PTR *PFNEGLQUERYSTRINGPROC)(EGLDisplay dpy, EGLint name); typedef EGLBoolean(GLAD_API_PTR *PFNEGLQUERYSURFACEPROC)(EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLint *value); typedef EGLBoolean(GLAD_API_PTR *PFNEGLRELEASETEXIMAGEPROC)(EGLDisplay dpy, EGLSurface surface, EGLint buffer); typedef EGLBoolean(GLAD_API_PTR *PFNEGLRELEASETHREADPROC)(void); typedef EGLBoolean(GLAD_API_PTR *PFNEGLSURFACEATTRIBPROC)(EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLint value); typedef EGLBoolean(GLAD_API_PTR *PFNEGLSWAPBUFFERSPROC)(EGLDisplay dpy, EGLSurface surface); typedef EGLBoolean(GLAD_API_PTR *PFNEGLSWAPINTERVALPROC)(EGLDisplay dpy, EGLint interval); typedef EGLBoolean(GLAD_API_PTR *PFNEGLTERMINATEPROC)(EGLDisplay dpy); typedef EGLBoolean(GLAD_API_PTR *PFNEGLWAITCLIENTPROC)(void); typedef EGLBoolean(GLAD_API_PTR *PFNEGLWAITGLPROC)(void); typedef EGLBoolean(GLAD_API_PTR *PFNEGLWAITNATIVEPROC)(EGLint engine); typedef EGLBoolean(GLAD_API_PTR *PFNEGLWAITSYNCPROC)(EGLDisplay dpy, EGLSync sync, EGLint flags); typedef EGLImageKHR(EGLAPIENTRYP PFNEGLCREATEIMAGEKHRPROC)(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLint *attrib_list); typedef EGLBoolean(EGLAPIENTRYP PFNEGLDESTROYIMAGEKHRPROC)(EGLDisplay dpy, EGLImageKHR image); GLAD_API_CALL PFNEGLCREATEIMAGEKHRPROC glad_eglCreateImageKHR; #define eglCreateImageKHR glad_eglCreateImageKHR GLAD_API_CALL PFNEGLDESTROYIMAGEKHRPROC glad_eglDestroyImageKHR; #define eglDestroyImageKHR glad_eglDestroyImageKHR GLAD_API_CALL PFNEGLBINDAPIPROC glad_eglBindAPI; #define eglBindAPI glad_eglBindAPI GLAD_API_CALL PFNEGLBINDTEXIMAGEPROC glad_eglBindTexImage; #define eglBindTexImage glad_eglBindTexImage GLAD_API_CALL PFNEGLCHOOSECONFIGPROC glad_eglChooseConfig; #define eglChooseConfig glad_eglChooseConfig GLAD_API_CALL PFNEGLCLIENTWAITSYNCPROC glad_eglClientWaitSync; #define eglClientWaitSync glad_eglClientWaitSync GLAD_API_CALL PFNEGLCOPYBUFFERSPROC glad_eglCopyBuffers; #define eglCopyBuffers glad_eglCopyBuffers GLAD_API_CALL PFNEGLCREATECONTEXTPROC glad_eglCreateContext; #define eglCreateContext glad_eglCreateContext GLAD_API_CALL PFNEGLCREATEIMAGEPROC glad_eglCreateImage; #define eglCreateImage glad_eglCreateImage GLAD_API_CALL PFNEGLCREATEPBUFFERFROMCLIENTBUFFERPROC glad_eglCreatePbufferFromClientBuffer; #define eglCreatePbufferFromClientBuffer glad_eglCreatePbufferFromClientBuffer GLAD_API_CALL PFNEGLCREATEPBUFFERSURFACEPROC glad_eglCreatePbufferSurface; #define eglCreatePbufferSurface glad_eglCreatePbufferSurface GLAD_API_CALL PFNEGLCREATEPIXMAPSURFACEPROC glad_eglCreatePixmapSurface; #define eglCreatePixmapSurface glad_eglCreatePixmapSurface GLAD_API_CALL PFNEGLCREATEPLATFORMPIXMAPSURFACEPROC glad_eglCreatePlatformPixmapSurface; #define eglCreatePlatformPixmapSurface glad_eglCreatePlatformPixmapSurface GLAD_API_CALL PFNEGLCREATEPLATFORMWINDOWSURFACEPROC glad_eglCreatePlatformWindowSurface; #define eglCreatePlatformWindowSurface glad_eglCreatePlatformWindowSurface GLAD_API_CALL PFNEGLCREATESYNCPROC glad_eglCreateSync; #define eglCreateSync glad_eglCreateSync GLAD_API_CALL PFNEGLCREATEWINDOWSURFACEPROC glad_eglCreateWindowSurface; #define eglCreateWindowSurface glad_eglCreateWindowSurface GLAD_API_CALL PFNEGLDESTROYCONTEXTPROC glad_eglDestroyContext; #define eglDestroyContext glad_eglDestroyContext GLAD_API_CALL PFNEGLDESTROYIMAGEPROC glad_eglDestroyImage; #define eglDestroyImage glad_eglDestroyImage GLAD_API_CALL PFNEGLDESTROYSURFACEPROC glad_eglDestroySurface; #define eglDestroySurface glad_eglDestroySurface GLAD_API_CALL PFNEGLDESTROYSYNCPROC glad_eglDestroySync; #define eglDestroySync glad_eglDestroySync GLAD_API_CALL PFNEGLGETCONFIGATTRIBPROC glad_eglGetConfigAttrib; #define eglGetConfigAttrib glad_eglGetConfigAttrib GLAD_API_CALL PFNEGLGETCONFIGSPROC glad_eglGetConfigs; #define eglGetConfigs glad_eglGetConfigs GLAD_API_CALL PFNEGLGETCURRENTCONTEXTPROC glad_eglGetCurrentContext; #define eglGetCurrentContext glad_eglGetCurrentContext GLAD_API_CALL PFNEGLGETCURRENTDISPLAYPROC glad_eglGetCurrentDisplay; #define eglGetCurrentDisplay glad_eglGetCurrentDisplay GLAD_API_CALL PFNEGLGETCURRENTSURFACEPROC glad_eglGetCurrentSurface; #define eglGetCurrentSurface glad_eglGetCurrentSurface GLAD_API_CALL PFNEGLGETDISPLAYPROC glad_eglGetDisplay; #define eglGetDisplay glad_eglGetDisplay GLAD_API_CALL PFNEGLGETERRORPROC glad_eglGetError; #define eglGetError glad_eglGetError GLAD_API_CALL PFNEGLGETPLATFORMDISPLAYPROC glad_eglGetPlatformDisplay; #define eglGetPlatformDisplay glad_eglGetPlatformDisplay GLAD_API_CALL PFNEGLGETPROCADDRESSPROC glad_eglGetProcAddress; #define eglGetProcAddress glad_eglGetProcAddress GLAD_API_CALL PFNEGLGETSYNCATTRIBPROC glad_eglGetSyncAttrib; #define eglGetSyncAttrib glad_eglGetSyncAttrib GLAD_API_CALL PFNEGLINITIALIZEPROC glad_eglInitialize; #define eglInitialize glad_eglInitialize GLAD_API_CALL PFNEGLMAKECURRENTPROC glad_eglMakeCurrent; #define eglMakeCurrent glad_eglMakeCurrent GLAD_API_CALL PFNEGLQUERYAPIPROC glad_eglQueryAPI; #define eglQueryAPI glad_eglQueryAPI GLAD_API_CALL PFNEGLQUERYCONTEXTPROC glad_eglQueryContext; #define eglQueryContext glad_eglQueryContext GLAD_API_CALL PFNEGLQUERYSTRINGPROC glad_eglQueryString; #define eglQueryString glad_eglQueryString GLAD_API_CALL PFNEGLQUERYSURFACEPROC glad_eglQuerySurface; #define eglQuerySurface glad_eglQuerySurface GLAD_API_CALL PFNEGLRELEASETEXIMAGEPROC glad_eglReleaseTexImage; #define eglReleaseTexImage glad_eglReleaseTexImage GLAD_API_CALL PFNEGLRELEASETHREADPROC glad_eglReleaseThread; #define eglReleaseThread glad_eglReleaseThread GLAD_API_CALL PFNEGLSURFACEATTRIBPROC glad_eglSurfaceAttrib; #define eglSurfaceAttrib glad_eglSurfaceAttrib GLAD_API_CALL PFNEGLSWAPBUFFERSPROC glad_eglSwapBuffers; #define eglSwapBuffers glad_eglSwapBuffers GLAD_API_CALL PFNEGLSWAPINTERVALPROC glad_eglSwapInterval; #define eglSwapInterval glad_eglSwapInterval GLAD_API_CALL PFNEGLTERMINATEPROC glad_eglTerminate; #define eglTerminate glad_eglTerminate GLAD_API_CALL PFNEGLWAITCLIENTPROC glad_eglWaitClient; #define eglWaitClient glad_eglWaitClient GLAD_API_CALL PFNEGLWAITGLPROC glad_eglWaitGL; #define eglWaitGL glad_eglWaitGL GLAD_API_CALL PFNEGLWAITNATIVEPROC glad_eglWaitNative; #define eglWaitNative glad_eglWaitNative GLAD_API_CALL PFNEGLWAITSYNCPROC glad_eglWaitSync; #define eglWaitSync glad_eglWaitSync GLAD_API_CALL int gladLoadEGLUserPtr(EGLDisplay display, GLADuserptrloadfunc load, void *userptr); GLAD_API_CALL int gladLoadEGL(EGLDisplay display, GLADloadfunc load); #ifdef GLAD_EGL GLAD_API_CALL int gladLoaderLoadEGL(EGLDisplay display); GLAD_API_CALL void gladLoaderUnloadEGL(void); #endif #ifdef __cplusplus } #endif #endif ================================================ FILE: third-party/glad/include/glad/gl.h ================================================ /** * Loader generated by glad 2.0.0-beta on Tue Jun 1 10:22:06 2021 * * Generator: C/C++ * Specification: gl * Extensions: 0 * * APIs: * - gl:compatibility=4.6 * * Options: * - ALIAS = False * - DEBUG = False * - HEADER_ONLY = False * - LOADER = True * - MX = True * - MX_GLOBAL = False * - ON_DEMAND = False * * Commandline: * --api='gl:compatibility=4.6' --extensions='' c --loader --mx * * Online: * http://glad.sh/#api=gl%3Acompatibility%3D4.6&extensions=&generator=c&options=LOADER%2CMX * */ #ifndef GLAD_GL_H_ #define GLAD_GL_H_ #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wreserved-id-macro" #endif #ifdef __gl_h_ #error OpenGL (gl.h) header already included (API: gl), remove previous include! #endif #define __gl_h_ 1 #ifdef __gl3_h_ #error OpenGL (gl3.h) header already included (API: gl), remove previous include! #endif #define __gl3_h_ 1 #ifdef __glext_h_ #error OpenGL (glext.h) header already included (API: gl), remove previous include! #endif #define __glext_h_ 1 #ifdef __gl3ext_h_ #error OpenGL (gl3ext.h) header already included (API: gl), remove previous include! #endif #define __gl3ext_h_ 1 #ifdef __clang__ #pragma clang diagnostic pop #endif #define GLAD_GL #define GLAD_OPTION_GL_LOADER #define GLAD_OPTION_GL_MX #ifdef __cplusplus extern "C" { #endif #ifndef GLAD_PLATFORM_H_ #define GLAD_PLATFORM_H_ #ifndef GLAD_PLATFORM_WIN32 #if defined(_WIN32) || defined(__WIN32__) || defined(WIN32) || defined(__MINGW32__) #define GLAD_PLATFORM_WIN32 1 #else #define GLAD_PLATFORM_WIN32 0 #endif #endif #ifndef GLAD_PLATFORM_APPLE #ifdef __APPLE__ #define GLAD_PLATFORM_APPLE 1 #else #define GLAD_PLATFORM_APPLE 0 #endif #endif #ifndef GLAD_PLATFORM_EMSCRIPTEN #ifdef __EMSCRIPTEN__ #define GLAD_PLATFORM_EMSCRIPTEN 1 #else #define GLAD_PLATFORM_EMSCRIPTEN 0 #endif #endif #ifndef GLAD_PLATFORM_UWP #if defined(_MSC_VER) && !defined(GLAD_INTERNAL_HAVE_WINAPIFAMILY) #ifdef __has_include #if __has_include() #define GLAD_INTERNAL_HAVE_WINAPIFAMILY 1 #endif #elif _MSC_VER >= 1700 && !_USING_V110_SDK71_ #define GLAD_INTERNAL_HAVE_WINAPIFAMILY 1 #endif #endif #ifdef GLAD_INTERNAL_HAVE_WINAPIFAMILY #include #if !WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) && WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_APP) #define GLAD_PLATFORM_UWP 1 #endif #endif #ifndef GLAD_PLATFORM_UWP #define GLAD_PLATFORM_UWP 0 #endif #endif #ifdef __GNUC__ #define GLAD_GNUC_EXTENSION __extension__ #else #define GLAD_GNUC_EXTENSION #endif #ifndef GLAD_API_CALL #if defined(GLAD_API_CALL_EXPORT) #if GLAD_PLATFORM_WIN32 || defined(__CYGWIN__) #if defined(GLAD_API_CALL_EXPORT_BUILD) #if defined(__GNUC__) #define GLAD_API_CALL __attribute__((dllexport)) extern #else #define GLAD_API_CALL __declspec(dllexport) extern #endif #else #if defined(__GNUC__) #define GLAD_API_CALL __attribute__((dllimport)) extern #else #define GLAD_API_CALL __declspec(dllimport) extern #endif #endif #elif defined(__GNUC__) && defined(GLAD_API_CALL_EXPORT_BUILD) #define GLAD_API_CALL __attribute__((visibility("default"))) extern #else #define GLAD_API_CALL extern #endif #else #define GLAD_API_CALL extern #endif #endif #ifdef APIENTRY #define GLAD_API_PTR APIENTRY #elif GLAD_PLATFORM_WIN32 #define GLAD_API_PTR __stdcall #else #define GLAD_API_PTR #endif #ifndef GLAPI #define GLAPI GLAD_API_CALL #endif #ifndef GLAPIENTRY #define GLAPIENTRY GLAD_API_PTR #endif #define GLAD_MAKE_VERSION(major, minor) (major * 10000 + minor) #define GLAD_VERSION_MAJOR(version) (version / 10000) #define GLAD_VERSION_MINOR(version) (version % 10000) #define GLAD_GENERATOR_VERSION "2.0.0-beta" typedef void (*GLADapiproc)(void); typedef GLADapiproc (*GLADloadfunc)(const char *name); typedef GLADapiproc (*GLADuserptrloadfunc)(void *userptr, const char *name); typedef void (*GLADprecallback)(const char *name, GLADapiproc apiproc, int len_args, ...); typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apiproc, int len_args, ...); #endif /* GLAD_PLATFORM_H_ */ #define GL_2D 0x0600 #define GL_2_BYTES 0x1407 #define GL_3D 0x0601 #define GL_3D_COLOR 0x0602 #define GL_3D_COLOR_TEXTURE 0x0603 #define GL_3_BYTES 0x1408 #define GL_4D_COLOR_TEXTURE 0x0604 #define GL_4_BYTES 0x1409 #define GL_ACCUM 0x0100 #define GL_ACCUM_ALPHA_BITS 0x0D5B #define GL_ACCUM_BLUE_BITS 0x0D5A #define GL_ACCUM_BUFFER_BIT 0x00000200 #define GL_ACCUM_CLEAR_VALUE 0x0B80 #define GL_ACCUM_GREEN_BITS 0x0D59 #define GL_ACCUM_RED_BITS 0x0D58 #define GL_ACTIVE_ATOMIC_COUNTER_BUFFERS 0x92D9 #define GL_ACTIVE_ATTRIBUTES 0x8B89 #define GL_ACTIVE_ATTRIBUTE_MAX_LENGTH 0x8B8A #define GL_ACTIVE_PROGRAM 0x8259 #define GL_ACTIVE_RESOURCES 0x92F5 #define GL_ACTIVE_SUBROUTINES 0x8DE5 #define GL_ACTIVE_SUBROUTINE_MAX_LENGTH 0x8E48 #define GL_ACTIVE_SUBROUTINE_UNIFORMS 0x8DE6 #define GL_ACTIVE_SUBROUTINE_UNIFORM_LOCATIONS 0x8E47 #define GL_ACTIVE_SUBROUTINE_UNIFORM_MAX_LENGTH 0x8E49 #define GL_ACTIVE_TEXTURE 0x84E0 #define GL_ACTIVE_UNIFORMS 0x8B86 #define GL_ACTIVE_UNIFORM_BLOCKS 0x8A36 #define GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH 0x8A35 #define GL_ACTIVE_UNIFORM_MAX_LENGTH 0x8B87 #define GL_ACTIVE_VARIABLES 0x9305 #define GL_ADD 0x0104 #define GL_ADD_SIGNED 0x8574 #define GL_ALIASED_LINE_WIDTH_RANGE 0x846E #define GL_ALIASED_POINT_SIZE_RANGE 0x846D #define GL_ALL_ATTRIB_BITS 0xFFFFFFFF #define GL_ALL_BARRIER_BITS 0xFFFFFFFF #define GL_ALL_SHADER_BITS 0xFFFFFFFF #define GL_ALPHA 0x1906 #define GL_ALPHA12 0x803D #define GL_ALPHA16 0x803E #define GL_ALPHA4 0x803B #define GL_ALPHA8 0x803C #define GL_ALPHA_BIAS 0x0D1D #define GL_ALPHA_BITS 0x0D55 #define GL_ALPHA_INTEGER 0x8D97 #define GL_ALPHA_SCALE 0x0D1C #define GL_ALPHA_TEST 0x0BC0 #define GL_ALPHA_TEST_FUNC 0x0BC1 #define GL_ALPHA_TEST_REF 0x0BC2 #define GL_ALREADY_SIGNALED 0x911A #define GL_ALWAYS 0x0207 #define GL_AMBIENT 0x1200 #define GL_AMBIENT_AND_DIFFUSE 0x1602 #define GL_AND 0x1501 #define GL_AND_INVERTED 0x1504 #define GL_AND_REVERSE 0x1502 #define GL_ANY_SAMPLES_PASSED 0x8C2F #define GL_ANY_SAMPLES_PASSED_CONSERVATIVE 0x8D6A #define GL_ARRAY_BUFFER 0x8892 #define GL_ARRAY_BUFFER_BINDING 0x8894 #define GL_ARRAY_SIZE 0x92FB #define GL_ARRAY_STRIDE 0x92FE #define GL_ATOMIC_COUNTER_BARRIER_BIT 0x00001000 #define GL_ATOMIC_COUNTER_BUFFER 0x92C0 #define GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTERS 0x92C5 #define GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES 0x92C6 #define GL_ATOMIC_COUNTER_BUFFER_BINDING 0x92C1 #define GL_ATOMIC_COUNTER_BUFFER_DATA_SIZE 0x92C4 #define GL_ATOMIC_COUNTER_BUFFER_INDEX 0x9301 #define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_COMPUTE_SHADER 0x90ED #define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_FRAGMENT_SHADER 0x92CB #define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_GEOMETRY_SHADER 0x92CA #define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_CONTROL_SHADER 0x92C8 #define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_EVALUATION_SHADER 0x92C9 #define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_VERTEX_SHADER 0x92C7 #define GL_ATOMIC_COUNTER_BUFFER_SIZE 0x92C3 #define GL_ATOMIC_COUNTER_BUFFER_START 0x92C2 #define GL_ATTACHED_SHADERS 0x8B85 #define GL_ATTRIB_STACK_DEPTH 0x0BB0 #define GL_AUTO_GENERATE_MIPMAP 0x8295 #define GL_AUTO_NORMAL 0x0D80 #define GL_AUX0 0x0409 #define GL_AUX1 0x040A #define GL_AUX2 0x040B #define GL_AUX3 0x040C #define GL_AUX_BUFFERS 0x0C00 #define GL_BACK 0x0405 #define GL_BACK_LEFT 0x0402 #define GL_BACK_RIGHT 0x0403 #define GL_BGR 0x80E0 #define GL_BGRA 0x80E1 #define GL_BGRA_INTEGER 0x8D9B #define GL_BGR_INTEGER 0x8D9A #define GL_BITMAP 0x1A00 #define GL_BITMAP_TOKEN 0x0704 #define GL_BLEND 0x0BE2 #define GL_BLEND_COLOR 0x8005 #define GL_BLEND_DST 0x0BE0 #define GL_BLEND_DST_ALPHA 0x80CA #define GL_BLEND_DST_RGB 0x80C8 #define GL_BLEND_EQUATION 0x8009 #define GL_BLEND_EQUATION_ALPHA 0x883D #define GL_BLEND_EQUATION_RGB 0x8009 #define GL_BLEND_SRC 0x0BE1 #define GL_BLEND_SRC_ALPHA 0x80CB #define GL_BLEND_SRC_RGB 0x80C9 #define GL_BLOCK_INDEX 0x92FD #define GL_BLUE 0x1905 #define GL_BLUE_BIAS 0x0D1B #define GL_BLUE_BITS 0x0D54 #define GL_BLUE_INTEGER 0x8D96 #define GL_BLUE_SCALE 0x0D1A #define GL_BOOL 0x8B56 #define GL_BOOL_VEC2 0x8B57 #define GL_BOOL_VEC3 0x8B58 #define GL_BOOL_VEC4 0x8B59 #define GL_BUFFER 0x82E0 #define GL_BUFFER_ACCESS 0x88BB #define GL_BUFFER_ACCESS_FLAGS 0x911F #define GL_BUFFER_BINDING 0x9302 #define GL_BUFFER_DATA_SIZE 0x9303 #define GL_BUFFER_IMMUTABLE_STORAGE 0x821F #define GL_BUFFER_MAPPED 0x88BC #define GL_BUFFER_MAP_LENGTH 0x9120 #define GL_BUFFER_MAP_OFFSET 0x9121 #define GL_BUFFER_MAP_POINTER 0x88BD #define GL_BUFFER_SIZE 0x8764 #define GL_BUFFER_STORAGE_FLAGS 0x8220 #define GL_BUFFER_UPDATE_BARRIER_BIT 0x00000200 #define GL_BUFFER_USAGE 0x8765 #define GL_BUFFER_VARIABLE 0x92E5 #define GL_BYTE 0x1400 #define GL_C3F_V3F 0x2A24 #define GL_C4F_N3F_V3F 0x2A26 #define GL_C4UB_V2F 0x2A22 #define GL_C4UB_V3F 0x2A23 #define GL_CAVEAT_SUPPORT 0x82B8 #define GL_CCW 0x0901 #define GL_CLAMP 0x2900 #define GL_CLAMP_FRAGMENT_COLOR 0x891B #define GL_CLAMP_READ_COLOR 0x891C #define GL_CLAMP_TO_BORDER 0x812D #define GL_CLAMP_TO_EDGE 0x812F #define GL_CLAMP_VERTEX_COLOR 0x891A #define GL_CLEAR 0x1500 #define GL_CLEAR_BUFFER 0x82B4 #define GL_CLEAR_TEXTURE 0x9365 #define GL_CLIENT_ACTIVE_TEXTURE 0x84E1 #define GL_CLIENT_ALL_ATTRIB_BITS 0xFFFFFFFF #define GL_CLIENT_ATTRIB_STACK_DEPTH 0x0BB1 #define GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT 0x00004000 #define GL_CLIENT_PIXEL_STORE_BIT 0x00000001 #define GL_CLIENT_STORAGE_BIT 0x0200 #define GL_CLIENT_VERTEX_ARRAY_BIT 0x00000002 #define GL_CLIPPING_INPUT_PRIMITIVES 0x82F6 #define GL_CLIPPING_OUTPUT_PRIMITIVES 0x82F7 #define GL_CLIP_DEPTH_MODE 0x935D #define GL_CLIP_DISTANCE0 0x3000 #define GL_CLIP_DISTANCE1 0x3001 #define GL_CLIP_DISTANCE2 0x3002 #define GL_CLIP_DISTANCE3 0x3003 #define GL_CLIP_DISTANCE4 0x3004 #define GL_CLIP_DISTANCE5 0x3005 #define GL_CLIP_DISTANCE6 0x3006 #define GL_CLIP_DISTANCE7 0x3007 #define GL_CLIP_ORIGIN 0x935C #define GL_CLIP_PLANE0 0x3000 #define GL_CLIP_PLANE1 0x3001 #define GL_CLIP_PLANE2 0x3002 #define GL_CLIP_PLANE3 0x3003 #define GL_CLIP_PLANE4 0x3004 #define GL_CLIP_PLANE5 0x3005 #define GL_COEFF 0x0A00 #define GL_COLOR 0x1800 #define GL_COLOR_ARRAY 0x8076 #define GL_COLOR_ARRAY_BUFFER_BINDING 0x8898 #define GL_COLOR_ARRAY_POINTER 0x8090 #define GL_COLOR_ARRAY_SIZE 0x8081 #define GL_COLOR_ARRAY_STRIDE 0x8083 #define GL_COLOR_ARRAY_TYPE 0x8082 #define GL_COLOR_ATTACHMENT0 0x8CE0 #define GL_COLOR_ATTACHMENT1 0x8CE1 #define GL_COLOR_ATTACHMENT10 0x8CEA #define GL_COLOR_ATTACHMENT11 0x8CEB #define GL_COLOR_ATTACHMENT12 0x8CEC #define GL_COLOR_ATTACHMENT13 0x8CED #define GL_COLOR_ATTACHMENT14 0x8CEE #define GL_COLOR_ATTACHMENT15 0x8CEF #define GL_COLOR_ATTACHMENT16 0x8CF0 #define GL_COLOR_ATTACHMENT17 0x8CF1 #define GL_COLOR_ATTACHMENT18 0x8CF2 #define GL_COLOR_ATTACHMENT19 0x8CF3 #define GL_COLOR_ATTACHMENT2 0x8CE2 #define GL_COLOR_ATTACHMENT20 0x8CF4 #define GL_COLOR_ATTACHMENT21 0x8CF5 #define GL_COLOR_ATTACHMENT22 0x8CF6 #define GL_COLOR_ATTACHMENT23 0x8CF7 #define GL_COLOR_ATTACHMENT24 0x8CF8 #define GL_COLOR_ATTACHMENT25 0x8CF9 #define GL_COLOR_ATTACHMENT26 0x8CFA #define GL_COLOR_ATTACHMENT27 0x8CFB #define GL_COLOR_ATTACHMENT28 0x8CFC #define GL_COLOR_ATTACHMENT29 0x8CFD #define GL_COLOR_ATTACHMENT3 0x8CE3 #define GL_COLOR_ATTACHMENT30 0x8CFE #define GL_COLOR_ATTACHMENT31 0x8CFF #define GL_COLOR_ATTACHMENT4 0x8CE4 #define GL_COLOR_ATTACHMENT5 0x8CE5 #define GL_COLOR_ATTACHMENT6 0x8CE6 #define GL_COLOR_ATTACHMENT7 0x8CE7 #define GL_COLOR_ATTACHMENT8 0x8CE8 #define GL_COLOR_ATTACHMENT9 0x8CE9 #define GL_COLOR_BUFFER_BIT 0x00004000 #define GL_COLOR_CLEAR_VALUE 0x0C22 #define GL_COLOR_COMPONENTS 0x8283 #define GL_COLOR_ENCODING 0x8296 #define GL_COLOR_INDEX 0x1900 #define GL_COLOR_INDEXES 0x1603 #define GL_COLOR_LOGIC_OP 0x0BF2 #define GL_COLOR_MATERIAL 0x0B57 #define GL_COLOR_MATERIAL_FACE 0x0B55 #define GL_COLOR_MATERIAL_PARAMETER 0x0B56 #define GL_COLOR_RENDERABLE 0x8286 #define GL_COLOR_SUM 0x8458 #define GL_COLOR_TABLE 0x80D0 #define GL_COLOR_WRITEMASK 0x0C23 #define GL_COMBINE 0x8570 #define GL_COMBINE_ALPHA 0x8572 #define GL_COMBINE_RGB 0x8571 #define GL_COMMAND_BARRIER_BIT 0x00000040 #define GL_COMPARE_REF_TO_TEXTURE 0x884E #define GL_COMPARE_R_TO_TEXTURE 0x884E #define GL_COMPATIBLE_SUBROUTINES 0x8E4B #define GL_COMPILE 0x1300 #define GL_COMPILE_AND_EXECUTE 0x1301 #define GL_COMPILE_STATUS 0x8B81 #define GL_COMPRESSED_ALPHA 0x84E9 #define GL_COMPRESSED_INTENSITY 0x84EC #define GL_COMPRESSED_LUMINANCE 0x84EA #define GL_COMPRESSED_LUMINANCE_ALPHA 0x84EB #define GL_COMPRESSED_R11_EAC 0x9270 #define GL_COMPRESSED_RED 0x8225 #define GL_COMPRESSED_RED_RGTC1 0x8DBB #define GL_COMPRESSED_RG 0x8226 #define GL_COMPRESSED_RG11_EAC 0x9272 #define GL_COMPRESSED_RGB 0x84ED #define GL_COMPRESSED_RGB8_ETC2 0x9274 #define GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 0x9276 #define GL_COMPRESSED_RGBA 0x84EE #define GL_COMPRESSED_RGBA8_ETC2_EAC 0x9278 #define GL_COMPRESSED_RGBA_BPTC_UNORM 0x8E8C #define GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT 0x8E8E #define GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT 0x8E8F #define GL_COMPRESSED_RG_RGTC2 0x8DBD #define GL_COMPRESSED_SIGNED_R11_EAC 0x9271 #define GL_COMPRESSED_SIGNED_RED_RGTC1 0x8DBC #define GL_COMPRESSED_SIGNED_RG11_EAC 0x9273 #define GL_COMPRESSED_SIGNED_RG_RGTC2 0x8DBE #define GL_COMPRESSED_SLUMINANCE 0x8C4A #define GL_COMPRESSED_SLUMINANCE_ALPHA 0x8C4B #define GL_COMPRESSED_SRGB 0x8C48 #define GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC 0x9279 #define GL_COMPRESSED_SRGB8_ETC2 0x9275 #define GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 0x9277 #define GL_COMPRESSED_SRGB_ALPHA 0x8C49 #define GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM 0x8E8D #define GL_COMPRESSED_TEXTURE_FORMATS 0x86A3 #define GL_COMPUTE_SHADER 0x91B9 #define GL_COMPUTE_SHADER_BIT 0x00000020 #define GL_COMPUTE_SHADER_INVOCATIONS 0x82F5 #define GL_COMPUTE_SUBROUTINE 0x92ED #define GL_COMPUTE_SUBROUTINE_UNIFORM 0x92F3 #define GL_COMPUTE_TEXTURE 0x82A0 #define GL_COMPUTE_WORK_GROUP_SIZE 0x8267 #define GL_CONDITION_SATISFIED 0x911C #define GL_CONSTANT 0x8576 #define GL_CONSTANT_ALPHA 0x8003 #define GL_CONSTANT_ATTENUATION 0x1207 #define GL_CONSTANT_COLOR 0x8001 #define GL_CONTEXT_COMPATIBILITY_PROFILE_BIT 0x00000002 #define GL_CONTEXT_CORE_PROFILE_BIT 0x00000001 #define GL_CONTEXT_FLAGS 0x821E #define GL_CONTEXT_FLAG_DEBUG_BIT 0x00000002 #define GL_CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT 0x00000001 #define GL_CONTEXT_FLAG_NO_ERROR_BIT 0x00000008 #define GL_CONTEXT_FLAG_ROBUST_ACCESS_BIT 0x00000004 #define GL_CONTEXT_LOST 0x0507 #define GL_CONTEXT_PROFILE_MASK 0x9126 #define GL_CONTEXT_RELEASE_BEHAVIOR 0x82FB #define GL_CONTEXT_RELEASE_BEHAVIOR_FLUSH 0x82FC #define GL_CONVOLUTION_1D 0x8010 #define GL_CONVOLUTION_2D 0x8011 #define GL_COORD_REPLACE 0x8862 #define GL_COPY 0x1503 #define GL_COPY_INVERTED 0x150C #define GL_COPY_PIXEL_TOKEN 0x0706 #define GL_COPY_READ_BUFFER 0x8F36 #define GL_COPY_READ_BUFFER_BINDING 0x8F36 #define GL_COPY_WRITE_BUFFER 0x8F37 #define GL_COPY_WRITE_BUFFER_BINDING 0x8F37 #define GL_CULL_FACE 0x0B44 #define GL_CULL_FACE_MODE 0x0B45 #define GL_CURRENT_BIT 0x00000001 #define GL_CURRENT_COLOR 0x0B00 #define GL_CURRENT_FOG_COORD 0x8453 #define GL_CURRENT_FOG_COORDINATE 0x8453 #define GL_CURRENT_INDEX 0x0B01 #define GL_CURRENT_NORMAL 0x0B02 #define GL_CURRENT_PROGRAM 0x8B8D #define GL_CURRENT_QUERY 0x8865 #define GL_CURRENT_RASTER_COLOR 0x0B04 #define GL_CURRENT_RASTER_DISTANCE 0x0B09 #define GL_CURRENT_RASTER_INDEX 0x0B05 #define GL_CURRENT_RASTER_POSITION 0x0B07 #define GL_CURRENT_RASTER_POSITION_VALID 0x0B08 #define GL_CURRENT_RASTER_SECONDARY_COLOR 0x845F #define GL_CURRENT_RASTER_TEXTURE_COORDS 0x0B06 #define GL_CURRENT_SECONDARY_COLOR 0x8459 #define GL_CURRENT_TEXTURE_COORDS 0x0B03 #define GL_CURRENT_VERTEX_ATTRIB 0x8626 #define GL_CW 0x0900 #define GL_DEBUG_CALLBACK_FUNCTION 0x8244 #define GL_DEBUG_CALLBACK_USER_PARAM 0x8245 #define GL_DEBUG_GROUP_STACK_DEPTH 0x826D #define GL_DEBUG_LOGGED_MESSAGES 0x9145 #define GL_DEBUG_NEXT_LOGGED_MESSAGE_LENGTH 0x8243 #define GL_DEBUG_OUTPUT 0x92E0 #define GL_DEBUG_OUTPUT_SYNCHRONOUS 0x8242 #define GL_DEBUG_SEVERITY_HIGH 0x9146 #define GL_DEBUG_SEVERITY_LOW 0x9148 #define GL_DEBUG_SEVERITY_MEDIUM 0x9147 #define GL_DEBUG_SEVERITY_NOTIFICATION 0x826B #define GL_DEBUG_SOURCE_API 0x8246 #define GL_DEBUG_SOURCE_APPLICATION 0x824A #define GL_DEBUG_SOURCE_OTHER 0x824B #define GL_DEBUG_SOURCE_SHADER_COMPILER 0x8248 #define GL_DEBUG_SOURCE_THIRD_PARTY 0x8249 #define GL_DEBUG_SOURCE_WINDOW_SYSTEM 0x8247 #define GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR 0x824D #define GL_DEBUG_TYPE_ERROR 0x824C #define GL_DEBUG_TYPE_MARKER 0x8268 #define GL_DEBUG_TYPE_OTHER 0x8251 #define GL_DEBUG_TYPE_PERFORMANCE 0x8250 #define GL_DEBUG_TYPE_POP_GROUP 0x826A #define GL_DEBUG_TYPE_PORTABILITY 0x824F #define GL_DEBUG_TYPE_PUSH_GROUP 0x8269 #define GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR 0x824E #define GL_DECAL 0x2101 #define GL_DECR 0x1E03 #define GL_DECR_WRAP 0x8508 #define GL_DELETE_STATUS 0x8B80 #define GL_DEPTH 0x1801 #define GL_DEPTH24_STENCIL8 0x88F0 #define GL_DEPTH32F_STENCIL8 0x8CAD #define GL_DEPTH_ATTACHMENT 0x8D00 #define GL_DEPTH_BIAS 0x0D1F #define GL_DEPTH_BITS 0x0D56 #define GL_DEPTH_BUFFER_BIT 0x00000100 #define GL_DEPTH_CLAMP 0x864F #define GL_DEPTH_CLEAR_VALUE 0x0B73 #define GL_DEPTH_COMPONENT 0x1902 #define GL_DEPTH_COMPONENT16 0x81A5 #define GL_DEPTH_COMPONENT24 0x81A6 #define GL_DEPTH_COMPONENT32 0x81A7 #define GL_DEPTH_COMPONENT32F 0x8CAC #define GL_DEPTH_COMPONENTS 0x8284 #define GL_DEPTH_FUNC 0x0B74 #define GL_DEPTH_RANGE 0x0B70 #define GL_DEPTH_RENDERABLE 0x8287 #define GL_DEPTH_SCALE 0x0D1E #define GL_DEPTH_STENCIL 0x84F9 #define GL_DEPTH_STENCIL_ATTACHMENT 0x821A #define GL_DEPTH_STENCIL_TEXTURE_MODE 0x90EA #define GL_DEPTH_TEST 0x0B71 #define GL_DEPTH_TEXTURE_MODE 0x884B #define GL_DEPTH_WRITEMASK 0x0B72 #define GL_DIFFUSE 0x1201 #define GL_DISPATCH_INDIRECT_BUFFER 0x90EE #define GL_DISPATCH_INDIRECT_BUFFER_BINDING 0x90EF #define GL_DISPLAY_LIST 0x82E7 #define GL_DITHER 0x0BD0 #define GL_DOMAIN 0x0A02 #define GL_DONT_CARE 0x1100 #define GL_DOT3_RGB 0x86AE #define GL_DOT3_RGBA 0x86AF #define GL_DOUBLE 0x140A #define GL_DOUBLEBUFFER 0x0C32 #define GL_DOUBLE_MAT2 0x8F46 #define GL_DOUBLE_MAT2x3 0x8F49 #define GL_DOUBLE_MAT2x4 0x8F4A #define GL_DOUBLE_MAT3 0x8F47 #define GL_DOUBLE_MAT3x2 0x8F4B #define GL_DOUBLE_MAT3x4 0x8F4C #define GL_DOUBLE_MAT4 0x8F48 #define GL_DOUBLE_MAT4x2 0x8F4D #define GL_DOUBLE_MAT4x3 0x8F4E #define GL_DOUBLE_VEC2 0x8FFC #define GL_DOUBLE_VEC3 0x8FFD #define GL_DOUBLE_VEC4 0x8FFE #define GL_DRAW_BUFFER 0x0C01 #define GL_DRAW_BUFFER0 0x8825 #define GL_DRAW_BUFFER1 0x8826 #define GL_DRAW_BUFFER10 0x882F #define GL_DRAW_BUFFER11 0x8830 #define GL_DRAW_BUFFER12 0x8831 #define GL_DRAW_BUFFER13 0x8832 #define GL_DRAW_BUFFER14 0x8833 #define GL_DRAW_BUFFER15 0x8834 #define GL_DRAW_BUFFER2 0x8827 #define GL_DRAW_BUFFER3 0x8828 #define GL_DRAW_BUFFER4 0x8829 #define GL_DRAW_BUFFER5 0x882A #define GL_DRAW_BUFFER6 0x882B #define GL_DRAW_BUFFER7 0x882C #define GL_DRAW_BUFFER8 0x882D #define GL_DRAW_BUFFER9 0x882E #define GL_DRAW_FRAMEBUFFER 0x8CA9 #define GL_DRAW_FRAMEBUFFER_BINDING 0x8CA6 #define GL_DRAW_INDIRECT_BUFFER 0x8F3F #define GL_DRAW_INDIRECT_BUFFER_BINDING 0x8F43 #define GL_DRAW_PIXEL_TOKEN 0x0705 #define GL_DST_ALPHA 0x0304 #define GL_DST_COLOR 0x0306 #define GL_DYNAMIC_COPY 0x88EA #define GL_DYNAMIC_DRAW 0x88E8 #define GL_DYNAMIC_READ 0x88E9 #define GL_DYNAMIC_STORAGE_BIT 0x0100 #define GL_EDGE_FLAG 0x0B43 #define GL_EDGE_FLAG_ARRAY 0x8079 #define GL_EDGE_FLAG_ARRAY_BUFFER_BINDING 0x889B #define GL_EDGE_FLAG_ARRAY_POINTER 0x8093 #define GL_EDGE_FLAG_ARRAY_STRIDE 0x808C #define GL_ELEMENT_ARRAY_BARRIER_BIT 0x00000002 #define GL_ELEMENT_ARRAY_BUFFER 0x8893 #define GL_ELEMENT_ARRAY_BUFFER_BINDING 0x8895 #define GL_EMISSION 0x1600 #define GL_ENABLE_BIT 0x00002000 #define GL_EQUAL 0x0202 #define GL_EQUIV 0x1509 #define GL_EVAL_BIT 0x00010000 #define GL_EXP 0x0800 #define GL_EXP2 0x0801 #define GL_EXTENSIONS 0x1F03 #define GL_EYE_LINEAR 0x2400 #define GL_EYE_PLANE 0x2502 #define GL_FALSE 0 #define GL_FASTEST 0x1101 #define GL_FEEDBACK 0x1C01 #define GL_FEEDBACK_BUFFER_POINTER 0x0DF0 #define GL_FEEDBACK_BUFFER_SIZE 0x0DF1 #define GL_FEEDBACK_BUFFER_TYPE 0x0DF2 #define GL_FILL 0x1B02 #define GL_FILTER 0x829A #define GL_FIRST_VERTEX_CONVENTION 0x8E4D #define GL_FIXED 0x140C #define GL_FIXED_ONLY 0x891D #define GL_FLAT 0x1D00 #define GL_FLOAT 0x1406 #define GL_FLOAT_32_UNSIGNED_INT_24_8_REV 0x8DAD #define GL_FLOAT_MAT2 0x8B5A #define GL_FLOAT_MAT2x3 0x8B65 #define GL_FLOAT_MAT2x4 0x8B66 #define GL_FLOAT_MAT3 0x8B5B #define GL_FLOAT_MAT3x2 0x8B67 #define GL_FLOAT_MAT3x4 0x8B68 #define GL_FLOAT_MAT4 0x8B5C #define GL_FLOAT_MAT4x2 0x8B69 #define GL_FLOAT_MAT4x3 0x8B6A #define GL_FLOAT_VEC2 0x8B50 #define GL_FLOAT_VEC3 0x8B51 #define GL_FLOAT_VEC4 0x8B52 #define GL_FOG 0x0B60 #define GL_FOG_BIT 0x00000080 #define GL_FOG_COLOR 0x0B66 #define GL_FOG_COORD 0x8451 #define GL_FOG_COORDINATE 0x8451 #define GL_FOG_COORDINATE_ARRAY 0x8457 #define GL_FOG_COORDINATE_ARRAY_BUFFER_BINDING 0x889D #define GL_FOG_COORDINATE_ARRAY_POINTER 0x8456 #define GL_FOG_COORDINATE_ARRAY_STRIDE 0x8455 #define GL_FOG_COORDINATE_ARRAY_TYPE 0x8454 #define GL_FOG_COORDINATE_SOURCE 0x8450 #define GL_FOG_COORD_ARRAY 0x8457 #define GL_FOG_COORD_ARRAY_BUFFER_BINDING 0x889D #define GL_FOG_COORD_ARRAY_POINTER 0x8456 #define GL_FOG_COORD_ARRAY_STRIDE 0x8455 #define GL_FOG_COORD_ARRAY_TYPE 0x8454 #define GL_FOG_COORD_SRC 0x8450 #define GL_FOG_DENSITY 0x0B62 #define GL_FOG_END 0x0B64 #define GL_FOG_HINT 0x0C54 #define GL_FOG_INDEX 0x0B61 #define GL_FOG_MODE 0x0B65 #define GL_FOG_START 0x0B63 #define GL_FRACTIONAL_EVEN 0x8E7C #define GL_FRACTIONAL_ODD 0x8E7B #define GL_FRAGMENT_DEPTH 0x8452 #define GL_FRAGMENT_INTERPOLATION_OFFSET_BITS 0x8E5D #define GL_FRAGMENT_SHADER 0x8B30 #define GL_FRAGMENT_SHADER_BIT 0x00000002 #define GL_FRAGMENT_SHADER_DERIVATIVE_HINT 0x8B8B #define GL_FRAGMENT_SHADER_INVOCATIONS 0x82F4 #define GL_FRAGMENT_SUBROUTINE 0x92EC #define GL_FRAGMENT_SUBROUTINE_UNIFORM 0x92F2 #define GL_FRAGMENT_TEXTURE 0x829F #define GL_FRAMEBUFFER 0x8D40 #define GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE 0x8215 #define GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE 0x8214 #define GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING 0x8210 #define GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE 0x8211 #define GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE 0x8216 #define GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE 0x8213 #define GL_FRAMEBUFFER_ATTACHMENT_LAYERED 0x8DA7 #define GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME 0x8CD1 #define GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE 0x8CD0 #define GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE 0x8212 #define GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE 0x8217 #define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE 0x8CD3 #define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER 0x8CD4 #define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL 0x8CD2 #define GL_FRAMEBUFFER_BARRIER_BIT 0x00000400 #define GL_FRAMEBUFFER_BINDING 0x8CA6 #define GL_FRAMEBUFFER_BLEND 0x828B #define GL_FRAMEBUFFER_COMPLETE 0x8CD5 #define GL_FRAMEBUFFER_DEFAULT 0x8218 #define GL_FRAMEBUFFER_DEFAULT_FIXED_SAMPLE_LOCATIONS 0x9314 #define GL_FRAMEBUFFER_DEFAULT_HEIGHT 0x9311 #define GL_FRAMEBUFFER_DEFAULT_LAYERS 0x9312 #define GL_FRAMEBUFFER_DEFAULT_SAMPLES 0x9313 #define GL_FRAMEBUFFER_DEFAULT_WIDTH 0x9310 #define GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT 0x8CD6 #define GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER 0x8CDB #define GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS 0x8DA8 #define GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT 0x8CD7 #define GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE 0x8D56 #define GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER 0x8CDC #define GL_FRAMEBUFFER_RENDERABLE 0x8289 #define GL_FRAMEBUFFER_RENDERABLE_LAYERED 0x828A #define GL_FRAMEBUFFER_SRGB 0x8DB9 #define GL_FRAMEBUFFER_UNDEFINED 0x8219 #define GL_FRAMEBUFFER_UNSUPPORTED 0x8CDD #define GL_FRONT 0x0404 #define GL_FRONT_AND_BACK 0x0408 #define GL_FRONT_FACE 0x0B46 #define GL_FRONT_LEFT 0x0400 #define GL_FRONT_RIGHT 0x0401 #define GL_FULL_SUPPORT 0x82B7 #define GL_FUNC_ADD 0x8006 #define GL_FUNC_REVERSE_SUBTRACT 0x800B #define GL_FUNC_SUBTRACT 0x800A #define GL_GENERATE_MIPMAP 0x8191 #define GL_GENERATE_MIPMAP_HINT 0x8192 #define GL_GEOMETRY_INPUT_TYPE 0x8917 #define GL_GEOMETRY_OUTPUT_TYPE 0x8918 #define GL_GEOMETRY_SHADER 0x8DD9 #define GL_GEOMETRY_SHADER_BIT 0x00000004 #define GL_GEOMETRY_SHADER_INVOCATIONS 0x887F #define GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED 0x82F3 #define GL_GEOMETRY_SUBROUTINE 0x92EB #define GL_GEOMETRY_SUBROUTINE_UNIFORM 0x92F1 #define GL_GEOMETRY_TEXTURE 0x829E #define GL_GEOMETRY_VERTICES_OUT 0x8916 #define GL_GEQUAL 0x0206 #define GL_GET_TEXTURE_IMAGE_FORMAT 0x8291 #define GL_GET_TEXTURE_IMAGE_TYPE 0x8292 #define GL_GREATER 0x0204 #define GL_GREEN 0x1904 #define GL_GREEN_BIAS 0x0D19 #define GL_GREEN_BITS 0x0D53 #define GL_GREEN_INTEGER 0x8D95 #define GL_GREEN_SCALE 0x0D18 #define GL_GUILTY_CONTEXT_RESET 0x8253 #define GL_HALF_FLOAT 0x140B #define GL_HIGH_FLOAT 0x8DF2 #define GL_HIGH_INT 0x8DF5 #define GL_HINT_BIT 0x00008000 #define GL_HISTOGRAM 0x8024 #define GL_IMAGE_1D 0x904C #define GL_IMAGE_1D_ARRAY 0x9052 #define GL_IMAGE_2D 0x904D #define GL_IMAGE_2D_ARRAY 0x9053 #define GL_IMAGE_2D_MULTISAMPLE 0x9055 #define GL_IMAGE_2D_MULTISAMPLE_ARRAY 0x9056 #define GL_IMAGE_2D_RECT 0x904F #define GL_IMAGE_3D 0x904E #define GL_IMAGE_BINDING_ACCESS 0x8F3E #define GL_IMAGE_BINDING_FORMAT 0x906E #define GL_IMAGE_BINDING_LAYER 0x8F3D #define GL_IMAGE_BINDING_LAYERED 0x8F3C #define GL_IMAGE_BINDING_LEVEL 0x8F3B #define GL_IMAGE_BINDING_NAME 0x8F3A #define GL_IMAGE_BUFFER 0x9051 #define GL_IMAGE_CLASS_10_10_10_2 0x82C3 #define GL_IMAGE_CLASS_11_11_10 0x82C2 #define GL_IMAGE_CLASS_1_X_16 0x82BE #define GL_IMAGE_CLASS_1_X_32 0x82BB #define GL_IMAGE_CLASS_1_X_8 0x82C1 #define GL_IMAGE_CLASS_2_X_16 0x82BD #define GL_IMAGE_CLASS_2_X_32 0x82BA #define GL_IMAGE_CLASS_2_X_8 0x82C0 #define GL_IMAGE_CLASS_4_X_16 0x82BC #define GL_IMAGE_CLASS_4_X_32 0x82B9 #define GL_IMAGE_CLASS_4_X_8 0x82BF #define GL_IMAGE_COMPATIBILITY_CLASS 0x82A8 #define GL_IMAGE_CUBE 0x9050 #define GL_IMAGE_CUBE_MAP_ARRAY 0x9054 #define GL_IMAGE_FORMAT_COMPATIBILITY_BY_CLASS 0x90C9 #define GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE 0x90C8 #define GL_IMAGE_FORMAT_COMPATIBILITY_TYPE 0x90C7 #define GL_IMAGE_PIXEL_FORMAT 0x82A9 #define GL_IMAGE_PIXEL_TYPE 0x82AA #define GL_IMAGE_TEXEL_SIZE 0x82A7 #define GL_IMPLEMENTATION_COLOR_READ_FORMAT 0x8B9B #define GL_IMPLEMENTATION_COLOR_READ_TYPE 0x8B9A #define GL_INCR 0x1E02 #define GL_INCR_WRAP 0x8507 #define GL_INDEX 0x8222 #define GL_INDEX_ARRAY 0x8077 #define GL_INDEX_ARRAY_BUFFER_BINDING 0x8899 #define GL_INDEX_ARRAY_POINTER 0x8091 #define GL_INDEX_ARRAY_STRIDE 0x8086 #define GL_INDEX_ARRAY_TYPE 0x8085 #define GL_INDEX_BITS 0x0D51 #define GL_INDEX_CLEAR_VALUE 0x0C20 #define GL_INDEX_LOGIC_OP 0x0BF1 #define GL_INDEX_MODE 0x0C30 #define GL_INDEX_OFFSET 0x0D13 #define GL_INDEX_SHIFT 0x0D12 #define GL_INDEX_WRITEMASK 0x0C21 #define GL_INFO_LOG_LENGTH 0x8B84 #define GL_INNOCENT_CONTEXT_RESET 0x8254 #define GL_INT 0x1404 #define GL_INTENSITY 0x8049 #define GL_INTENSITY12 0x804C #define GL_INTENSITY16 0x804D #define GL_INTENSITY4 0x804A #define GL_INTENSITY8 0x804B #define GL_INTERLEAVED_ATTRIBS 0x8C8C #define GL_INTERNALFORMAT_ALPHA_SIZE 0x8274 #define GL_INTERNALFORMAT_ALPHA_TYPE 0x827B #define GL_INTERNALFORMAT_BLUE_SIZE 0x8273 #define GL_INTERNALFORMAT_BLUE_TYPE 0x827A #define GL_INTERNALFORMAT_DEPTH_SIZE 0x8275 #define GL_INTERNALFORMAT_DEPTH_TYPE 0x827C #define GL_INTERNALFORMAT_GREEN_SIZE 0x8272 #define GL_INTERNALFORMAT_GREEN_TYPE 0x8279 #define GL_INTERNALFORMAT_PREFERRED 0x8270 #define GL_INTERNALFORMAT_RED_SIZE 0x8271 #define GL_INTERNALFORMAT_RED_TYPE 0x8278 #define GL_INTERNALFORMAT_SHARED_SIZE 0x8277 #define GL_INTERNALFORMAT_STENCIL_SIZE 0x8276 #define GL_INTERNALFORMAT_STENCIL_TYPE 0x827D #define GL_INTERNALFORMAT_SUPPORTED 0x826F #define GL_INTERPOLATE 0x8575 #define GL_INT_2_10_10_10_REV 0x8D9F #define GL_INT_IMAGE_1D 0x9057 #define GL_INT_IMAGE_1D_ARRAY 0x905D #define GL_INT_IMAGE_2D 0x9058 #define GL_INT_IMAGE_2D_ARRAY 0x905E #define GL_INT_IMAGE_2D_MULTISAMPLE 0x9060 #define GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY 0x9061 #define GL_INT_IMAGE_2D_RECT 0x905A #define GL_INT_IMAGE_3D 0x9059 #define GL_INT_IMAGE_BUFFER 0x905C #define GL_INT_IMAGE_CUBE 0x905B #define GL_INT_IMAGE_CUBE_MAP_ARRAY 0x905F #define GL_INT_SAMPLER_1D 0x8DC9 #define GL_INT_SAMPLER_1D_ARRAY 0x8DCE #define GL_INT_SAMPLER_2D 0x8DCA #define GL_INT_SAMPLER_2D_ARRAY 0x8DCF #define GL_INT_SAMPLER_2D_MULTISAMPLE 0x9109 #define GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY 0x910C #define GL_INT_SAMPLER_2D_RECT 0x8DCD #define GL_INT_SAMPLER_3D 0x8DCB #define GL_INT_SAMPLER_BUFFER 0x8DD0 #define GL_INT_SAMPLER_CUBE 0x8DCC #define GL_INT_SAMPLER_CUBE_MAP_ARRAY 0x900E #define GL_INT_VEC2 0x8B53 #define GL_INT_VEC3 0x8B54 #define GL_INT_VEC4 0x8B55 #define GL_INVALID_ENUM 0x0500 #define GL_INVALID_FRAMEBUFFER_OPERATION 0x0506 #define GL_INVALID_INDEX 0xFFFFFFFF #define GL_INVALID_OPERATION 0x0502 #define GL_INVALID_VALUE 0x0501 #define GL_INVERT 0x150A #define GL_ISOLINES 0x8E7A #define GL_IS_PER_PATCH 0x92E7 #define GL_IS_ROW_MAJOR 0x9300 #define GL_KEEP 0x1E00 #define GL_LAST_VERTEX_CONVENTION 0x8E4E #define GL_LAYER_PROVOKING_VERTEX 0x825E #define GL_LEFT 0x0406 #define GL_LEQUAL 0x0203 #define GL_LESS 0x0201 #define GL_LIGHT0 0x4000 #define GL_LIGHT1 0x4001 #define GL_LIGHT2 0x4002 #define GL_LIGHT3 0x4003 #define GL_LIGHT4 0x4004 #define GL_LIGHT5 0x4005 #define GL_LIGHT6 0x4006 #define GL_LIGHT7 0x4007 #define GL_LIGHTING 0x0B50 #define GL_LIGHTING_BIT 0x00000040 #define GL_LIGHT_MODEL_AMBIENT 0x0B53 #define GL_LIGHT_MODEL_COLOR_CONTROL 0x81F8 #define GL_LIGHT_MODEL_LOCAL_VIEWER 0x0B51 #define GL_LIGHT_MODEL_TWO_SIDE 0x0B52 #define GL_LINE 0x1B01 #define GL_LINEAR 0x2601 #define GL_LINEAR_ATTENUATION 0x1208 #define GL_LINEAR_MIPMAP_LINEAR 0x2703 #define GL_LINEAR_MIPMAP_NEAREST 0x2701 #define GL_LINES 0x0001 #define GL_LINES_ADJACENCY 0x000A #define GL_LINE_BIT 0x00000004 #define GL_LINE_LOOP 0x0002 #define GL_LINE_RESET_TOKEN 0x0707 #define GL_LINE_SMOOTH 0x0B20 #define GL_LINE_SMOOTH_HINT 0x0C52 #define GL_LINE_STIPPLE 0x0B24 #define GL_LINE_STIPPLE_PATTERN 0x0B25 #define GL_LINE_STIPPLE_REPEAT 0x0B26 #define GL_LINE_STRIP 0x0003 #define GL_LINE_STRIP_ADJACENCY 0x000B #define GL_LINE_TOKEN 0x0702 #define GL_LINE_WIDTH 0x0B21 #define GL_LINE_WIDTH_GRANULARITY 0x0B23 #define GL_LINE_WIDTH_RANGE 0x0B22 #define GL_LINK_STATUS 0x8B82 #define GL_LIST_BASE 0x0B32 #define GL_LIST_BIT 0x00020000 #define GL_LIST_INDEX 0x0B33 #define GL_LIST_MODE 0x0B30 #define GL_LOAD 0x0101 #define GL_LOCATION 0x930E #define GL_LOCATION_COMPONENT 0x934A #define GL_LOCATION_INDEX 0x930F #define GL_LOGIC_OP 0x0BF1 #define GL_LOGIC_OP_MODE 0x0BF0 #define GL_LOSE_CONTEXT_ON_RESET 0x8252 #define GL_LOWER_LEFT 0x8CA1 #define GL_LOW_FLOAT 0x8DF0 #define GL_LOW_INT 0x8DF3 #define GL_LUMINANCE 0x1909 #define GL_LUMINANCE12 0x8041 #define GL_LUMINANCE12_ALPHA12 0x8047 #define GL_LUMINANCE12_ALPHA4 0x8046 #define GL_LUMINANCE16 0x8042 #define GL_LUMINANCE16_ALPHA16 0x8048 #define GL_LUMINANCE4 0x803F #define GL_LUMINANCE4_ALPHA4 0x8043 #define GL_LUMINANCE6_ALPHA2 0x8044 #define GL_LUMINANCE8 0x8040 #define GL_LUMINANCE8_ALPHA8 0x8045 #define GL_LUMINANCE_ALPHA 0x190A #define GL_MAJOR_VERSION 0x821B #define GL_MANUAL_GENERATE_MIPMAP 0x8294 #define GL_MAP1_COLOR_4 0x0D90 #define GL_MAP1_GRID_DOMAIN 0x0DD0 #define GL_MAP1_GRID_SEGMENTS 0x0DD1 #define GL_MAP1_INDEX 0x0D91 #define GL_MAP1_NORMAL 0x0D92 #define GL_MAP1_TEXTURE_COORD_1 0x0D93 #define GL_MAP1_TEXTURE_COORD_2 0x0D94 #define GL_MAP1_TEXTURE_COORD_3 0x0D95 #define GL_MAP1_TEXTURE_COORD_4 0x0D96 #define GL_MAP1_VERTEX_3 0x0D97 #define GL_MAP1_VERTEX_4 0x0D98 #define GL_MAP2_COLOR_4 0x0DB0 #define GL_MAP2_GRID_DOMAIN 0x0DD2 #define GL_MAP2_GRID_SEGMENTS 0x0DD3 #define GL_MAP2_INDEX 0x0DB1 #define GL_MAP2_NORMAL 0x0DB2 #define GL_MAP2_TEXTURE_COORD_1 0x0DB3 #define GL_MAP2_TEXTURE_COORD_2 0x0DB4 #define GL_MAP2_TEXTURE_COORD_3 0x0DB5 #define GL_MAP2_TEXTURE_COORD_4 0x0DB6 #define GL_MAP2_VERTEX_3 0x0DB7 #define GL_MAP2_VERTEX_4 0x0DB8 #define GL_MAP_COHERENT_BIT 0x0080 #define GL_MAP_COLOR 0x0D10 #define GL_MAP_FLUSH_EXPLICIT_BIT 0x0010 #define GL_MAP_INVALIDATE_BUFFER_BIT 0x0008 #define GL_MAP_INVALIDATE_RANGE_BIT 0x0004 #define GL_MAP_PERSISTENT_BIT 0x0040 #define GL_MAP_READ_BIT 0x0001 #define GL_MAP_STENCIL 0x0D11 #define GL_MAP_UNSYNCHRONIZED_BIT 0x0020 #define GL_MAP_WRITE_BIT 0x0002 #define GL_MATRIX_MODE 0x0BA0 #define GL_MATRIX_STRIDE 0x92FF #define GL_MAX 0x8008 #define GL_MAX_3D_TEXTURE_SIZE 0x8073 #define GL_MAX_ARRAY_TEXTURE_LAYERS 0x88FF #define GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS 0x92DC #define GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE 0x92D8 #define GL_MAX_ATTRIB_STACK_DEPTH 0x0D35 #define GL_MAX_CLIENT_ATTRIB_STACK_DEPTH 0x0D3B #define GL_MAX_CLIP_DISTANCES 0x0D32 #define GL_MAX_CLIP_PLANES 0x0D32 #define GL_MAX_COLOR_ATTACHMENTS 0x8CDF #define GL_MAX_COLOR_TEXTURE_SAMPLES 0x910E #define GL_MAX_COMBINED_ATOMIC_COUNTERS 0x92D7 #define GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS 0x92D1 #define GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES 0x82FA #define GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS 0x8266 #define GL_MAX_COMBINED_DIMENSIONS 0x8282 #define GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS 0x8A33 #define GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS 0x8A32 #define GL_MAX_COMBINED_IMAGE_UNIFORMS 0x90CF #define GL_MAX_COMBINED_IMAGE_UNITS_AND_FRAGMENT_OUTPUTS 0x8F39 #define GL_MAX_COMBINED_SHADER_OUTPUT_RESOURCES 0x8F39 #define GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS 0x90DC #define GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS 0x8E1E #define GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS 0x8E1F #define GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS 0x8B4D #define GL_MAX_COMBINED_UNIFORM_BLOCKS 0x8A2E #define GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS 0x8A31 #define GL_MAX_COMPUTE_ATOMIC_COUNTERS 0x8265 #define GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS 0x8264 #define GL_MAX_COMPUTE_IMAGE_UNIFORMS 0x91BD #define GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS 0x90DB #define GL_MAX_COMPUTE_SHARED_MEMORY_SIZE 0x8262 #define GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS 0x91BC #define GL_MAX_COMPUTE_UNIFORM_BLOCKS 0x91BB #define GL_MAX_COMPUTE_UNIFORM_COMPONENTS 0x8263 #define GL_MAX_COMPUTE_WORK_GROUP_COUNT 0x91BE #define GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS 0x90EB #define GL_MAX_COMPUTE_WORK_GROUP_SIZE 0x91BF #define GL_MAX_CUBE_MAP_TEXTURE_SIZE 0x851C #define GL_MAX_CULL_DISTANCES 0x82F9 #define GL_MAX_DEBUG_GROUP_STACK_DEPTH 0x826C #define GL_MAX_DEBUG_LOGGED_MESSAGES 0x9144 #define GL_MAX_DEBUG_MESSAGE_LENGTH 0x9143 #define GL_MAX_DEPTH 0x8280 #define GL_MAX_DEPTH_TEXTURE_SAMPLES 0x910F #define GL_MAX_DRAW_BUFFERS 0x8824 #define GL_MAX_DUAL_SOURCE_DRAW_BUFFERS 0x88FC #define GL_MAX_ELEMENTS_INDICES 0x80E9 #define GL_MAX_ELEMENTS_VERTICES 0x80E8 #define GL_MAX_ELEMENT_INDEX 0x8D6B #define GL_MAX_EVAL_ORDER 0x0D30 #define GL_MAX_FRAGMENT_ATOMIC_COUNTERS 0x92D6 #define GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS 0x92D0 #define GL_MAX_FRAGMENT_IMAGE_UNIFORMS 0x90CE #define GL_MAX_FRAGMENT_INPUT_COMPONENTS 0x9125 #define GL_MAX_FRAGMENT_INTERPOLATION_OFFSET 0x8E5C #define GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS 0x90DA #define GL_MAX_FRAGMENT_UNIFORM_BLOCKS 0x8A2D #define GL_MAX_FRAGMENT_UNIFORM_COMPONENTS 0x8B49 #define GL_MAX_FRAGMENT_UNIFORM_VECTORS 0x8DFD #define GL_MAX_FRAMEBUFFER_HEIGHT 0x9316 #define GL_MAX_FRAMEBUFFER_LAYERS 0x9317 #define GL_MAX_FRAMEBUFFER_SAMPLES 0x9318 #define GL_MAX_FRAMEBUFFER_WIDTH 0x9315 #define GL_MAX_GEOMETRY_ATOMIC_COUNTERS 0x92D5 #define GL_MAX_GEOMETRY_ATOMIC_COUNTER_BUFFERS 0x92CF #define GL_MAX_GEOMETRY_IMAGE_UNIFORMS 0x90CD #define GL_MAX_GEOMETRY_INPUT_COMPONENTS 0x9123 #define GL_MAX_GEOMETRY_OUTPUT_COMPONENTS 0x9124 #define GL_MAX_GEOMETRY_OUTPUT_VERTICES 0x8DE0 #define GL_MAX_GEOMETRY_SHADER_INVOCATIONS 0x8E5A #define GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS 0x90D7 #define GL_MAX_GEOMETRY_TEXTURE_IMAGE_UNITS 0x8C29 #define GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS 0x8DE1 #define GL_MAX_GEOMETRY_UNIFORM_BLOCKS 0x8A2C #define GL_MAX_GEOMETRY_UNIFORM_COMPONENTS 0x8DDF #define GL_MAX_HEIGHT 0x827F #define GL_MAX_IMAGE_SAMPLES 0x906D #define GL_MAX_IMAGE_UNITS 0x8F38 #define GL_MAX_INTEGER_SAMPLES 0x9110 #define GL_MAX_LABEL_LENGTH 0x82E8 #define GL_MAX_LAYERS 0x8281 #define GL_MAX_LIGHTS 0x0D31 #define GL_MAX_LIST_NESTING 0x0B31 #define GL_MAX_MODELVIEW_STACK_DEPTH 0x0D36 #define GL_MAX_NAME_LENGTH 0x92F6 #define GL_MAX_NAME_STACK_DEPTH 0x0D37 #define GL_MAX_NUM_ACTIVE_VARIABLES 0x92F7 #define GL_MAX_NUM_COMPATIBLE_SUBROUTINES 0x92F8 #define GL_MAX_PATCH_VERTICES 0x8E7D #define GL_MAX_PIXEL_MAP_TABLE 0x0D34 #define GL_MAX_PROGRAM_TEXEL_OFFSET 0x8905 #define GL_MAX_PROGRAM_TEXTURE_GATHER_OFFSET 0x8E5F #define GL_MAX_PROJECTION_STACK_DEPTH 0x0D38 #define GL_MAX_RECTANGLE_TEXTURE_SIZE 0x84F8 #define GL_MAX_RENDERBUFFER_SIZE 0x84E8 #define GL_MAX_SAMPLES 0x8D57 #define GL_MAX_SAMPLE_MASK_WORDS 0x8E59 #define GL_MAX_SERVER_WAIT_TIMEOUT 0x9111 #define GL_MAX_SHADER_STORAGE_BLOCK_SIZE 0x90DE #define GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS 0x90DD #define GL_MAX_SUBROUTINES 0x8DE7 #define GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS 0x8DE8 #define GL_MAX_TESS_CONTROL_ATOMIC_COUNTERS 0x92D3 #define GL_MAX_TESS_CONTROL_ATOMIC_COUNTER_BUFFERS 0x92CD #define GL_MAX_TESS_CONTROL_IMAGE_UNIFORMS 0x90CB #define GL_MAX_TESS_CONTROL_INPUT_COMPONENTS 0x886C #define GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS 0x8E83 #define GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS 0x90D8 #define GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS 0x8E81 #define GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS 0x8E85 #define GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS 0x8E89 #define GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS 0x8E7F #define GL_MAX_TESS_EVALUATION_ATOMIC_COUNTERS 0x92D4 #define GL_MAX_TESS_EVALUATION_ATOMIC_COUNTER_BUFFERS 0x92CE #define GL_MAX_TESS_EVALUATION_IMAGE_UNIFORMS 0x90CC #define GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS 0x886D #define GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS 0x8E86 #define GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS 0x90D9 #define GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS 0x8E82 #define GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS 0x8E8A #define GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS 0x8E80 #define GL_MAX_TESS_GEN_LEVEL 0x8E7E #define GL_MAX_TESS_PATCH_COMPONENTS 0x8E84 #define GL_MAX_TEXTURE_BUFFER_SIZE 0x8C2B #define GL_MAX_TEXTURE_COORDS 0x8871 #define GL_MAX_TEXTURE_IMAGE_UNITS 0x8872 #define GL_MAX_TEXTURE_LOD_BIAS 0x84FD #define GL_MAX_TEXTURE_MAX_ANISOTROPY 0x84FF #define GL_MAX_TEXTURE_SIZE 0x0D33 #define GL_MAX_TEXTURE_STACK_DEPTH 0x0D39 #define GL_MAX_TEXTURE_UNITS 0x84E2 #define GL_MAX_TRANSFORM_FEEDBACK_BUFFERS 0x8E70 #define GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS 0x8C8A #define GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS 0x8C8B #define GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS 0x8C80 #define GL_MAX_UNIFORM_BLOCK_SIZE 0x8A30 #define GL_MAX_UNIFORM_BUFFER_BINDINGS 0x8A2F #define GL_MAX_UNIFORM_LOCATIONS 0x826E #define GL_MAX_VARYING_COMPONENTS 0x8B4B #define GL_MAX_VARYING_FLOATS 0x8B4B #define GL_MAX_VARYING_VECTORS 0x8DFC #define GL_MAX_VERTEX_ATOMIC_COUNTERS 0x92D2 #define GL_MAX_VERTEX_ATOMIC_COUNTER_BUFFERS 0x92CC #define GL_MAX_VERTEX_ATTRIBS 0x8869 #define GL_MAX_VERTEX_ATTRIB_BINDINGS 0x82DA #define GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET 0x82D9 #define GL_MAX_VERTEX_ATTRIB_STRIDE 0x82E5 #define GL_MAX_VERTEX_IMAGE_UNIFORMS 0x90CA #define GL_MAX_VERTEX_OUTPUT_COMPONENTS 0x9122 #define GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS 0x90D6 #define GL_MAX_VERTEX_STREAMS 0x8E71 #define GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS 0x8B4C #define GL_MAX_VERTEX_UNIFORM_BLOCKS 0x8A2B #define GL_MAX_VERTEX_UNIFORM_COMPONENTS 0x8B4A #define GL_MAX_VERTEX_UNIFORM_VECTORS 0x8DFB #define GL_MAX_VIEWPORTS 0x825B #define GL_MAX_VIEWPORT_DIMS 0x0D3A #define GL_MAX_WIDTH 0x827E #define GL_MEDIUM_FLOAT 0x8DF1 #define GL_MEDIUM_INT 0x8DF4 #define GL_MIN 0x8007 #define GL_MINMAX 0x802E #define GL_MINOR_VERSION 0x821C #define GL_MIN_FRAGMENT_INTERPOLATION_OFFSET 0x8E5B #define GL_MIN_MAP_BUFFER_ALIGNMENT 0x90BC #define GL_MIN_PROGRAM_TEXEL_OFFSET 0x8904 #define GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET 0x8E5E #define GL_MIN_SAMPLE_SHADING_VALUE 0x8C37 #define GL_MIPMAP 0x8293 #define GL_MIRRORED_REPEAT 0x8370 #define GL_MIRROR_CLAMP_TO_EDGE 0x8743 #define GL_MODELVIEW 0x1700 #define GL_MODELVIEW_MATRIX 0x0BA6 #define GL_MODELVIEW_STACK_DEPTH 0x0BA3 #define GL_MODULATE 0x2100 #define GL_MULT 0x0103 #define GL_MULTISAMPLE 0x809D #define GL_MULTISAMPLE_BIT 0x20000000 #define GL_N3F_V3F 0x2A25 #define GL_NAME_LENGTH 0x92F9 #define GL_NAME_STACK_DEPTH 0x0D70 #define GL_NAND 0x150E #define GL_NEAREST 0x2600 #define GL_NEAREST_MIPMAP_LINEAR 0x2702 #define GL_NEAREST_MIPMAP_NEAREST 0x2700 #define GL_NEGATIVE_ONE_TO_ONE 0x935E #define GL_NEVER 0x0200 #define GL_NICEST 0x1102 #define GL_NONE 0 #define GL_NOOP 0x1505 #define GL_NOR 0x1508 #define GL_NORMALIZE 0x0BA1 #define GL_NORMAL_ARRAY 0x8075 #define GL_NORMAL_ARRAY_BUFFER_BINDING 0x8897 #define GL_NORMAL_ARRAY_POINTER 0x808F #define GL_NORMAL_ARRAY_STRIDE 0x807F #define GL_NORMAL_ARRAY_TYPE 0x807E #define GL_NORMAL_MAP 0x8511 #define GL_NOTEQUAL 0x0205 #define GL_NO_ERROR 0 #define GL_NO_RESET_NOTIFICATION 0x8261 #define GL_NUM_ACTIVE_VARIABLES 0x9304 #define GL_NUM_COMPATIBLE_SUBROUTINES 0x8E4A #define GL_NUM_COMPRESSED_TEXTURE_FORMATS 0x86A2 #define GL_NUM_EXTENSIONS 0x821D #define GL_NUM_PROGRAM_BINARY_FORMATS 0x87FE #define GL_NUM_SAMPLE_COUNTS 0x9380 #define GL_NUM_SHADER_BINARY_FORMATS 0x8DF9 #define GL_NUM_SHADING_LANGUAGE_VERSIONS 0x82E9 #define GL_NUM_SPIR_V_EXTENSIONS 0x9554 #define GL_OBJECT_LINEAR 0x2401 #define GL_OBJECT_PLANE 0x2501 #define GL_OBJECT_TYPE 0x9112 #define GL_OFFSET 0x92FC #define GL_ONE 1 #define GL_ONE_MINUS_CONSTANT_ALPHA 0x8004 #define GL_ONE_MINUS_CONSTANT_COLOR 0x8002 #define GL_ONE_MINUS_DST_ALPHA 0x0305 #define GL_ONE_MINUS_DST_COLOR 0x0307 #define GL_ONE_MINUS_SRC1_ALPHA 0x88FB #define GL_ONE_MINUS_SRC1_COLOR 0x88FA #define GL_ONE_MINUS_SRC_ALPHA 0x0303 #define GL_ONE_MINUS_SRC_COLOR 0x0301 #define GL_OPERAND0_ALPHA 0x8598 #define GL_OPERAND0_RGB 0x8590 #define GL_OPERAND1_ALPHA 0x8599 #define GL_OPERAND1_RGB 0x8591 #define GL_OPERAND2_ALPHA 0x859A #define GL_OPERAND2_RGB 0x8592 #define GL_OR 0x1507 #define GL_ORDER 0x0A01 #define GL_OR_INVERTED 0x150D #define GL_OR_REVERSE 0x150B #define GL_OUT_OF_MEMORY 0x0505 #define GL_PACK_ALIGNMENT 0x0D05 #define GL_PACK_COMPRESSED_BLOCK_DEPTH 0x912D #define GL_PACK_COMPRESSED_BLOCK_HEIGHT 0x912C #define GL_PACK_COMPRESSED_BLOCK_SIZE 0x912E #define GL_PACK_COMPRESSED_BLOCK_WIDTH 0x912B #define GL_PACK_IMAGE_HEIGHT 0x806C #define GL_PACK_LSB_FIRST 0x0D01 #define GL_PACK_ROW_LENGTH 0x0D02 #define GL_PACK_SKIP_IMAGES 0x806B #define GL_PACK_SKIP_PIXELS 0x0D04 #define GL_PACK_SKIP_ROWS 0x0D03 #define GL_PACK_SWAP_BYTES 0x0D00 #define GL_PARAMETER_BUFFER 0x80EE #define GL_PARAMETER_BUFFER_BINDING 0x80EF #define GL_PASS_THROUGH_TOKEN 0x0700 #define GL_PATCHES 0x000E #define GL_PATCH_DEFAULT_INNER_LEVEL 0x8E73 #define GL_PATCH_DEFAULT_OUTER_LEVEL 0x8E74 #define GL_PATCH_VERTICES 0x8E72 #define GL_PERSPECTIVE_CORRECTION_HINT 0x0C50 #define GL_PIXEL_BUFFER_BARRIER_BIT 0x00000080 #define GL_PIXEL_MAP_A_TO_A 0x0C79 #define GL_PIXEL_MAP_A_TO_A_SIZE 0x0CB9 #define GL_PIXEL_MAP_B_TO_B 0x0C78 #define GL_PIXEL_MAP_B_TO_B_SIZE 0x0CB8 #define GL_PIXEL_MAP_G_TO_G 0x0C77 #define GL_PIXEL_MAP_G_TO_G_SIZE 0x0CB7 #define GL_PIXEL_MAP_I_TO_A 0x0C75 #define GL_PIXEL_MAP_I_TO_A_SIZE 0x0CB5 #define GL_PIXEL_MAP_I_TO_B 0x0C74 #define GL_PIXEL_MAP_I_TO_B_SIZE 0x0CB4 #define GL_PIXEL_MAP_I_TO_G 0x0C73 #define GL_PIXEL_MAP_I_TO_G_SIZE 0x0CB3 #define GL_PIXEL_MAP_I_TO_I 0x0C70 #define GL_PIXEL_MAP_I_TO_I_SIZE 0x0CB0 #define GL_PIXEL_MAP_I_TO_R 0x0C72 #define GL_PIXEL_MAP_I_TO_R_SIZE 0x0CB2 #define GL_PIXEL_MAP_R_TO_R 0x0C76 #define GL_PIXEL_MAP_R_TO_R_SIZE 0x0CB6 #define GL_PIXEL_MAP_S_TO_S 0x0C71 #define GL_PIXEL_MAP_S_TO_S_SIZE 0x0CB1 #define GL_PIXEL_MODE_BIT 0x00000020 #define GL_PIXEL_PACK_BUFFER 0x88EB #define GL_PIXEL_PACK_BUFFER_BINDING 0x88ED #define GL_PIXEL_UNPACK_BUFFER 0x88EC #define GL_PIXEL_UNPACK_BUFFER_BINDING 0x88EF #define GL_POINT 0x1B00 #define GL_POINTS 0x0000 #define GL_POINT_BIT 0x00000002 #define GL_POINT_DISTANCE_ATTENUATION 0x8129 #define GL_POINT_FADE_THRESHOLD_SIZE 0x8128 #define GL_POINT_SIZE 0x0B11 #define GL_POINT_SIZE_GRANULARITY 0x0B13 #define GL_POINT_SIZE_MAX 0x8127 #define GL_POINT_SIZE_MIN 0x8126 #define GL_POINT_SIZE_RANGE 0x0B12 #define GL_POINT_SMOOTH 0x0B10 #define GL_POINT_SMOOTH_HINT 0x0C51 #define GL_POINT_SPRITE 0x8861 #define GL_POINT_SPRITE_COORD_ORIGIN 0x8CA0 #define GL_POINT_TOKEN 0x0701 #define GL_POLYGON 0x0009 #define GL_POLYGON_BIT 0x00000008 #define GL_POLYGON_MODE 0x0B40 #define GL_POLYGON_OFFSET_CLAMP 0x8E1B #define GL_POLYGON_OFFSET_FACTOR 0x8038 #define GL_POLYGON_OFFSET_FILL 0x8037 #define GL_POLYGON_OFFSET_LINE 0x2A02 #define GL_POLYGON_OFFSET_POINT 0x2A01 #define GL_POLYGON_OFFSET_UNITS 0x2A00 #define GL_POLYGON_SMOOTH 0x0B41 #define GL_POLYGON_SMOOTH_HINT 0x0C53 #define GL_POLYGON_STIPPLE 0x0B42 #define GL_POLYGON_STIPPLE_BIT 0x00000010 #define GL_POLYGON_TOKEN 0x0703 #define GL_POSITION 0x1203 #define GL_POST_COLOR_MATRIX_COLOR_TABLE 0x80D2 #define GL_POST_CONVOLUTION_COLOR_TABLE 0x80D1 #define GL_PREVIOUS 0x8578 #define GL_PRIMARY_COLOR 0x8577 #define GL_PRIMITIVES_GENERATED 0x8C87 #define GL_PRIMITIVES_SUBMITTED 0x82EF #define GL_PRIMITIVE_RESTART 0x8F9D #define GL_PRIMITIVE_RESTART_FIXED_INDEX 0x8D69 #define GL_PRIMITIVE_RESTART_FOR_PATCHES_SUPPORTED 0x8221 #define GL_PRIMITIVE_RESTART_INDEX 0x8F9E #define GL_PROGRAM 0x82E2 #define GL_PROGRAM_BINARY_FORMATS 0x87FF #define GL_PROGRAM_BINARY_LENGTH 0x8741 #define GL_PROGRAM_BINARY_RETRIEVABLE_HINT 0x8257 #define GL_PROGRAM_INPUT 0x92E3 #define GL_PROGRAM_OUTPUT 0x92E4 #define GL_PROGRAM_PIPELINE 0x82E4 #define GL_PROGRAM_PIPELINE_BINDING 0x825A #define GL_PROGRAM_POINT_SIZE 0x8642 #define GL_PROGRAM_SEPARABLE 0x8258 #define GL_PROJECTION 0x1701 #define GL_PROJECTION_MATRIX 0x0BA7 #define GL_PROJECTION_STACK_DEPTH 0x0BA4 #define GL_PROVOKING_VERTEX 0x8E4F #define GL_PROXY_COLOR_TABLE 0x80D3 #define GL_PROXY_HISTOGRAM 0x8025 #define GL_PROXY_POST_COLOR_MATRIX_COLOR_TABLE 0x80D5 #define GL_PROXY_POST_CONVOLUTION_COLOR_TABLE 0x80D4 #define GL_PROXY_TEXTURE_1D 0x8063 #define GL_PROXY_TEXTURE_1D_ARRAY 0x8C19 #define GL_PROXY_TEXTURE_2D 0x8064 #define GL_PROXY_TEXTURE_2D_ARRAY 0x8C1B #define GL_PROXY_TEXTURE_2D_MULTISAMPLE 0x9101 #define GL_PROXY_TEXTURE_2D_MULTISAMPLE_ARRAY 0x9103 #define GL_PROXY_TEXTURE_3D 0x8070 #define GL_PROXY_TEXTURE_CUBE_MAP 0x851B #define GL_PROXY_TEXTURE_CUBE_MAP_ARRAY 0x900B #define GL_PROXY_TEXTURE_RECTANGLE 0x84F7 #define GL_Q 0x2003 #define GL_QUADRATIC_ATTENUATION 0x1209 #define GL_QUADS 0x0007 #define GL_QUADS_FOLLOW_PROVOKING_VERTEX_CONVENTION 0x8E4C #define GL_QUAD_STRIP 0x0008 #define GL_QUERY 0x82E3 #define GL_QUERY_BUFFER 0x9192 #define GL_QUERY_BUFFER_BARRIER_BIT 0x00008000 #define GL_QUERY_BUFFER_BINDING 0x9193 #define GL_QUERY_BY_REGION_NO_WAIT 0x8E16 #define GL_QUERY_BY_REGION_NO_WAIT_INVERTED 0x8E1A #define GL_QUERY_BY_REGION_WAIT 0x8E15 #define GL_QUERY_BY_REGION_WAIT_INVERTED 0x8E19 #define GL_QUERY_COUNTER_BITS 0x8864 #define GL_QUERY_NO_WAIT 0x8E14 #define GL_QUERY_NO_WAIT_INVERTED 0x8E18 #define GL_QUERY_RESULT 0x8866 #define GL_QUERY_RESULT_AVAILABLE 0x8867 #define GL_QUERY_RESULT_NO_WAIT 0x9194 #define GL_QUERY_TARGET 0x82EA #define GL_QUERY_WAIT 0x8E13 #define GL_QUERY_WAIT_INVERTED 0x8E17 #define GL_R 0x2002 #define GL_R11F_G11F_B10F 0x8C3A #define GL_R16 0x822A #define GL_R16F 0x822D #define GL_R16I 0x8233 #define GL_R16UI 0x8234 #define GL_R16_SNORM 0x8F98 #define GL_R32F 0x822E #define GL_R32I 0x8235 #define GL_R32UI 0x8236 #define GL_R3_G3_B2 0x2A10 #define GL_R8 0x8229 #define GL_R8I 0x8231 #define GL_R8UI 0x8232 #define GL_R8_SNORM 0x8F94 #define GL_RASTERIZER_DISCARD 0x8C89 #define GL_READ_BUFFER 0x0C02 #define GL_READ_FRAMEBUFFER 0x8CA8 #define GL_READ_FRAMEBUFFER_BINDING 0x8CAA #define GL_READ_ONLY 0x88B8 #define GL_READ_PIXELS 0x828C #define GL_READ_PIXELS_FORMAT 0x828D #define GL_READ_PIXELS_TYPE 0x828E #define GL_READ_WRITE 0x88BA #define GL_RED 0x1903 #define GL_RED_BIAS 0x0D15 #define GL_RED_BITS 0x0D52 #define GL_RED_INTEGER 0x8D94 #define GL_RED_SCALE 0x0D14 #define GL_REFERENCED_BY_COMPUTE_SHADER 0x930B #define GL_REFERENCED_BY_FRAGMENT_SHADER 0x930A #define GL_REFERENCED_BY_GEOMETRY_SHADER 0x9309 #define GL_REFERENCED_BY_TESS_CONTROL_SHADER 0x9307 #define GL_REFERENCED_BY_TESS_EVALUATION_SHADER 0x9308 #define GL_REFERENCED_BY_VERTEX_SHADER 0x9306 #define GL_REFLECTION_MAP 0x8512 #define GL_RENDER 0x1C00 #define GL_RENDERBUFFER 0x8D41 #define GL_RENDERBUFFER_ALPHA_SIZE 0x8D53 #define GL_RENDERBUFFER_BINDING 0x8CA7 #define GL_RENDERBUFFER_BLUE_SIZE 0x8D52 #define GL_RENDERBUFFER_DEPTH_SIZE 0x8D54 #define GL_RENDERBUFFER_GREEN_SIZE 0x8D51 #define GL_RENDERBUFFER_HEIGHT 0x8D43 #define GL_RENDERBUFFER_INTERNAL_FORMAT 0x8D44 #define GL_RENDERBUFFER_RED_SIZE 0x8D50 #define GL_RENDERBUFFER_SAMPLES 0x8CAB #define GL_RENDERBUFFER_STENCIL_SIZE 0x8D55 #define GL_RENDERBUFFER_WIDTH 0x8D42 #define GL_RENDERER 0x1F01 #define GL_RENDER_MODE 0x0C40 #define GL_REPEAT 0x2901 #define GL_REPLACE 0x1E01 #define GL_RESCALE_NORMAL 0x803A #define GL_RESET_NOTIFICATION_STRATEGY 0x8256 #define GL_RETURN 0x0102 #define GL_RG 0x8227 #define GL_RG16 0x822C #define GL_RG16F 0x822F #define GL_RG16I 0x8239 #define GL_RG16UI 0x823A #define GL_RG16_SNORM 0x8F99 #define GL_RG32F 0x8230 #define GL_RG32I 0x823B #define GL_RG32UI 0x823C #define GL_RG8 0x822B #define GL_RG8I 0x8237 #define GL_RG8UI 0x8238 #define GL_RG8_SNORM 0x8F95 #define GL_RGB 0x1907 #define GL_RGB10 0x8052 #define GL_RGB10_A2 0x8059 #define GL_RGB10_A2UI 0x906F #define GL_RGB12 0x8053 #define GL_RGB16 0x8054 #define GL_RGB16F 0x881B #define GL_RGB16I 0x8D89 #define GL_RGB16UI 0x8D77 #define GL_RGB16_SNORM 0x8F9A #define GL_RGB32F 0x8815 #define GL_RGB32I 0x8D83 #define GL_RGB32UI 0x8D71 #define GL_RGB4 0x804F #define GL_RGB5 0x8050 #define GL_RGB565 0x8D62 #define GL_RGB5_A1 0x8057 #define GL_RGB8 0x8051 #define GL_RGB8I 0x8D8F #define GL_RGB8UI 0x8D7D #define GL_RGB8_SNORM 0x8F96 #define GL_RGB9_E5 0x8C3D #define GL_RGBA 0x1908 #define GL_RGBA12 0x805A #define GL_RGBA16 0x805B #define GL_RGBA16F 0x881A #define GL_RGBA16I 0x8D88 #define GL_RGBA16UI 0x8D76 #define GL_RGBA16_SNORM 0x8F9B #define GL_RGBA2 0x8055 #define GL_RGBA32F 0x8814 #define GL_RGBA32I 0x8D82 #define GL_RGBA32UI 0x8D70 #define GL_RGBA4 0x8056 #define GL_RGBA8 0x8058 #define GL_RGBA8I 0x8D8E #define GL_RGBA8UI 0x8D7C #define GL_RGBA8_SNORM 0x8F97 #define GL_RGBA_INTEGER 0x8D99 #define GL_RGBA_MODE 0x0C31 #define GL_RGB_INTEGER 0x8D98 #define GL_RGB_SCALE 0x8573 #define GL_RG_INTEGER 0x8228 #define GL_RIGHT 0x0407 #define GL_S 0x2000 #define GL_SAMPLER 0x82E6 #define GL_SAMPLER_1D 0x8B5D #define GL_SAMPLER_1D_ARRAY 0x8DC0 #define GL_SAMPLER_1D_ARRAY_SHADOW 0x8DC3 #define GL_SAMPLER_1D_SHADOW 0x8B61 #define GL_SAMPLER_2D 0x8B5E #define GL_SAMPLER_2D_ARRAY 0x8DC1 #define GL_SAMPLER_2D_ARRAY_SHADOW 0x8DC4 #define GL_SAMPLER_2D_MULTISAMPLE 0x9108 #define GL_SAMPLER_2D_MULTISAMPLE_ARRAY 0x910B #define GL_SAMPLER_2D_RECT 0x8B63 #define GL_SAMPLER_2D_RECT_SHADOW 0x8B64 #define GL_SAMPLER_2D_SHADOW 0x8B62 #define GL_SAMPLER_3D 0x8B5F #define GL_SAMPLER_BINDING 0x8919 #define GL_SAMPLER_BUFFER 0x8DC2 #define GL_SAMPLER_CUBE 0x8B60 #define GL_SAMPLER_CUBE_MAP_ARRAY 0x900C #define GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW 0x900D #define GL_SAMPLER_CUBE_SHADOW 0x8DC5 #define GL_SAMPLES 0x80A9 #define GL_SAMPLES_PASSED 0x8914 #define GL_SAMPLE_ALPHA_TO_COVERAGE 0x809E #define GL_SAMPLE_ALPHA_TO_ONE 0x809F #define GL_SAMPLE_BUFFERS 0x80A8 #define GL_SAMPLE_COVERAGE 0x80A0 #define GL_SAMPLE_COVERAGE_INVERT 0x80AB #define GL_SAMPLE_COVERAGE_VALUE 0x80AA #define GL_SAMPLE_MASK 0x8E51 #define GL_SAMPLE_MASK_VALUE 0x8E52 #define GL_SAMPLE_POSITION 0x8E50 #define GL_SAMPLE_SHADING 0x8C36 #define GL_SCISSOR_BIT 0x00080000 #define GL_SCISSOR_BOX 0x0C10 #define GL_SCISSOR_TEST 0x0C11 #define GL_SECONDARY_COLOR_ARRAY 0x845E #define GL_SECONDARY_COLOR_ARRAY_BUFFER_BINDING 0x889C #define GL_SECONDARY_COLOR_ARRAY_POINTER 0x845D #define GL_SECONDARY_COLOR_ARRAY_SIZE 0x845A #define GL_SECONDARY_COLOR_ARRAY_STRIDE 0x845C #define GL_SECONDARY_COLOR_ARRAY_TYPE 0x845B #define GL_SELECT 0x1C02 #define GL_SELECTION_BUFFER_POINTER 0x0DF3 #define GL_SELECTION_BUFFER_SIZE 0x0DF4 #define GL_SEPARABLE_2D 0x8012 #define GL_SEPARATE_ATTRIBS 0x8C8D #define GL_SEPARATE_SPECULAR_COLOR 0x81FA #define GL_SET 0x150F #define GL_SHADER 0x82E1 #define GL_SHADER_BINARY_FORMATS 0x8DF8 #define GL_SHADER_BINARY_FORMAT_SPIR_V 0x9551 #define GL_SHADER_COMPILER 0x8DFA #define GL_SHADER_IMAGE_ACCESS_BARRIER_BIT 0x00000020 #define GL_SHADER_IMAGE_ATOMIC 0x82A6 #define GL_SHADER_IMAGE_LOAD 0x82A4 #define GL_SHADER_IMAGE_STORE 0x82A5 #define GL_SHADER_SOURCE_LENGTH 0x8B88 #define GL_SHADER_STORAGE_BARRIER_BIT 0x00002000 #define GL_SHADER_STORAGE_BLOCK 0x92E6 #define GL_SHADER_STORAGE_BUFFER 0x90D2 #define GL_SHADER_STORAGE_BUFFER_BINDING 0x90D3 #define GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT 0x90DF #define GL_SHADER_STORAGE_BUFFER_SIZE 0x90D5 #define GL_SHADER_STORAGE_BUFFER_START 0x90D4 #define GL_SHADER_TYPE 0x8B4F #define GL_SHADE_MODEL 0x0B54 #define GL_SHADING_LANGUAGE_VERSION 0x8B8C #define GL_SHININESS 0x1601 #define GL_SHORT 0x1402 #define GL_SIGNALED 0x9119 #define GL_SIGNED_NORMALIZED 0x8F9C #define GL_SIMULTANEOUS_TEXTURE_AND_DEPTH_TEST 0x82AC #define GL_SIMULTANEOUS_TEXTURE_AND_DEPTH_WRITE 0x82AE #define GL_SIMULTANEOUS_TEXTURE_AND_STENCIL_TEST 0x82AD #define GL_SIMULTANEOUS_TEXTURE_AND_STENCIL_WRITE 0x82AF #define GL_SINGLE_COLOR 0x81F9 #define GL_SLUMINANCE 0x8C46 #define GL_SLUMINANCE8 0x8C47 #define GL_SLUMINANCE8_ALPHA8 0x8C45 #define GL_SLUMINANCE_ALPHA 0x8C44 #define GL_SMOOTH 0x1D01 #define GL_SMOOTH_LINE_WIDTH_GRANULARITY 0x0B23 #define GL_SMOOTH_LINE_WIDTH_RANGE 0x0B22 #define GL_SMOOTH_POINT_SIZE_GRANULARITY 0x0B13 #define GL_SMOOTH_POINT_SIZE_RANGE 0x0B12 #define GL_SOURCE0_ALPHA 0x8588 #define GL_SOURCE0_RGB 0x8580 #define GL_SOURCE1_ALPHA 0x8589 #define GL_SOURCE1_RGB 0x8581 #define GL_SOURCE2_ALPHA 0x858A #define GL_SOURCE2_RGB 0x8582 #define GL_SPECULAR 0x1202 #define GL_SPHERE_MAP 0x2402 #define GL_SPIR_V_BINARY 0x9552 #define GL_SPIR_V_EXTENSIONS 0x9553 #define GL_SPOT_CUTOFF 0x1206 #define GL_SPOT_DIRECTION 0x1204 #define GL_SPOT_EXPONENT 0x1205 #define GL_SRC0_ALPHA 0x8588 #define GL_SRC0_RGB 0x8580 #define GL_SRC1_ALPHA 0x8589 #define GL_SRC1_COLOR 0x88F9 #define GL_SRC1_RGB 0x8581 #define GL_SRC2_ALPHA 0x858A #define GL_SRC2_RGB 0x8582 #define GL_SRC_ALPHA 0x0302 #define GL_SRC_ALPHA_SATURATE 0x0308 #define GL_SRC_COLOR 0x0300 #define GL_SRGB 0x8C40 #define GL_SRGB8 0x8C41 #define GL_SRGB8_ALPHA8 0x8C43 #define GL_SRGB_ALPHA 0x8C42 #define GL_SRGB_READ 0x8297 #define GL_SRGB_WRITE 0x8298 #define GL_STACK_OVERFLOW 0x0503 #define GL_STACK_UNDERFLOW 0x0504 #define GL_STATIC_COPY 0x88E6 #define GL_STATIC_DRAW 0x88E4 #define GL_STATIC_READ 0x88E5 #define GL_STENCIL 0x1802 #define GL_STENCIL_ATTACHMENT 0x8D20 #define GL_STENCIL_BACK_FAIL 0x8801 #define GL_STENCIL_BACK_FUNC 0x8800 #define GL_STENCIL_BACK_PASS_DEPTH_FAIL 0x8802 #define GL_STENCIL_BACK_PASS_DEPTH_PASS 0x8803 #define GL_STENCIL_BACK_REF 0x8CA3 #define GL_STENCIL_BACK_VALUE_MASK 0x8CA4 #define GL_STENCIL_BACK_WRITEMASK 0x8CA5 #define GL_STENCIL_BITS 0x0D57 #define GL_STENCIL_BUFFER_BIT 0x00000400 #define GL_STENCIL_CLEAR_VALUE 0x0B91 #define GL_STENCIL_COMPONENTS 0x8285 #define GL_STENCIL_FAIL 0x0B94 #define GL_STENCIL_FUNC 0x0B92 #define GL_STENCIL_INDEX 0x1901 #define GL_STENCIL_INDEX1 0x8D46 #define GL_STENCIL_INDEX16 0x8D49 #define GL_STENCIL_INDEX4 0x8D47 #define GL_STENCIL_INDEX8 0x8D48 #define GL_STENCIL_PASS_DEPTH_FAIL 0x0B95 #define GL_STENCIL_PASS_DEPTH_PASS 0x0B96 #define GL_STENCIL_REF 0x0B97 #define GL_STENCIL_RENDERABLE 0x8288 #define GL_STENCIL_TEST 0x0B90 #define GL_STENCIL_VALUE_MASK 0x0B93 #define GL_STENCIL_WRITEMASK 0x0B98 #define GL_STEREO 0x0C33 #define GL_STREAM_COPY 0x88E2 #define GL_STREAM_DRAW 0x88E0 #define GL_STREAM_READ 0x88E1 #define GL_SUBPIXEL_BITS 0x0D50 #define GL_SUBTRACT 0x84E7 #define GL_SYNC_CONDITION 0x9113 #define GL_SYNC_FENCE 0x9116 #define GL_SYNC_FLAGS 0x9115 #define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001 #define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117 #define GL_SYNC_STATUS 0x9114 #define GL_T 0x2001 #define GL_T2F_C3F_V3F 0x2A2A #define GL_T2F_C4F_N3F_V3F 0x2A2C #define GL_T2F_C4UB_V3F 0x2A29 #define GL_T2F_N3F_V3F 0x2A2B #define GL_T2F_V3F 0x2A27 #define GL_T4F_C4F_N3F_V4F 0x2A2D #define GL_T4F_V4F 0x2A28 #define GL_TESS_CONTROL_OUTPUT_VERTICES 0x8E75 #define GL_TESS_CONTROL_SHADER 0x8E88 #define GL_TESS_CONTROL_SHADER_BIT 0x00000008 #define GL_TESS_CONTROL_SHADER_PATCHES 0x82F1 #define GL_TESS_CONTROL_SUBROUTINE 0x92E9 #define GL_TESS_CONTROL_SUBROUTINE_UNIFORM 0x92EF #define GL_TESS_CONTROL_TEXTURE 0x829C #define GL_TESS_EVALUATION_SHADER 0x8E87 #define GL_TESS_EVALUATION_SHADER_BIT 0x00000010 #define GL_TESS_EVALUATION_SHADER_INVOCATIONS 0x82F2 #define GL_TESS_EVALUATION_SUBROUTINE 0x92EA #define GL_TESS_EVALUATION_SUBROUTINE_UNIFORM 0x92F0 #define GL_TESS_EVALUATION_TEXTURE 0x829D #define GL_TESS_GEN_MODE 0x8E76 #define GL_TESS_GEN_POINT_MODE 0x8E79 #define GL_TESS_GEN_SPACING 0x8E77 #define GL_TESS_GEN_VERTEX_ORDER 0x8E78 #define GL_TEXTURE 0x1702 #define GL_TEXTURE0 0x84C0 #define GL_TEXTURE1 0x84C1 #define GL_TEXTURE10 0x84CA #define GL_TEXTURE11 0x84CB #define GL_TEXTURE12 0x84CC #define GL_TEXTURE13 0x84CD #define GL_TEXTURE14 0x84CE #define GL_TEXTURE15 0x84CF #define GL_TEXTURE16 0x84D0 #define GL_TEXTURE17 0x84D1 #define GL_TEXTURE18 0x84D2 #define GL_TEXTURE19 0x84D3 #define GL_TEXTURE2 0x84C2 #define GL_TEXTURE20 0x84D4 #define GL_TEXTURE21 0x84D5 #define GL_TEXTURE22 0x84D6 #define GL_TEXTURE23 0x84D7 #define GL_TEXTURE24 0x84D8 #define GL_TEXTURE25 0x84D9 #define GL_TEXTURE26 0x84DA #define GL_TEXTURE27 0x84DB #define GL_TEXTURE28 0x84DC #define GL_TEXTURE29 0x84DD #define GL_TEXTURE3 0x84C3 #define GL_TEXTURE30 0x84DE #define GL_TEXTURE31 0x84DF #define GL_TEXTURE4 0x84C4 #define GL_TEXTURE5 0x84C5 #define GL_TEXTURE6 0x84C6 #define GL_TEXTURE7 0x84C7 #define GL_TEXTURE8 0x84C8 #define GL_TEXTURE9 0x84C9 #define GL_TEXTURE_1D 0x0DE0 #define GL_TEXTURE_1D_ARRAY 0x8C18 #define GL_TEXTURE_2D 0x0DE1 #define GL_TEXTURE_2D_ARRAY 0x8C1A #define GL_TEXTURE_2D_MULTISAMPLE 0x9100 #define GL_TEXTURE_2D_MULTISAMPLE_ARRAY 0x9102 #define GL_TEXTURE_3D 0x806F #define GL_TEXTURE_ALPHA_SIZE 0x805F #define GL_TEXTURE_ALPHA_TYPE 0x8C13 #define GL_TEXTURE_BASE_LEVEL 0x813C #define GL_TEXTURE_BINDING_1D 0x8068 #define GL_TEXTURE_BINDING_1D_ARRAY 0x8C1C #define GL_TEXTURE_BINDING_2D 0x8069 #define GL_TEXTURE_BINDING_2D_ARRAY 0x8C1D #define GL_TEXTURE_BINDING_2D_MULTISAMPLE 0x9104 #define GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY 0x9105 #define GL_TEXTURE_BINDING_3D 0x806A #define GL_TEXTURE_BINDING_BUFFER 0x8C2C #define GL_TEXTURE_BINDING_CUBE_MAP 0x8514 #define GL_TEXTURE_BINDING_CUBE_MAP_ARRAY 0x900A #define GL_TEXTURE_BINDING_RECTANGLE 0x84F6 #define GL_TEXTURE_BIT 0x00040000 #define GL_TEXTURE_BLUE_SIZE 0x805E #define GL_TEXTURE_BLUE_TYPE 0x8C12 #define GL_TEXTURE_BORDER 0x1005 #define GL_TEXTURE_BORDER_COLOR 0x1004 #define GL_TEXTURE_BUFFER 0x8C2A #define GL_TEXTURE_BUFFER_BINDING 0x8C2A #define GL_TEXTURE_BUFFER_DATA_STORE_BINDING 0x8C2D #define GL_TEXTURE_BUFFER_OFFSET 0x919D #define GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT 0x919F #define GL_TEXTURE_BUFFER_SIZE 0x919E #define GL_TEXTURE_COMPARE_FUNC 0x884D #define GL_TEXTURE_COMPARE_MODE 0x884C #define GL_TEXTURE_COMPONENTS 0x1003 #define GL_TEXTURE_COMPRESSED 0x86A1 #define GL_TEXTURE_COMPRESSED_BLOCK_HEIGHT 0x82B2 #define GL_TEXTURE_COMPRESSED_BLOCK_SIZE 0x82B3 #define GL_TEXTURE_COMPRESSED_BLOCK_WIDTH 0x82B1 #define GL_TEXTURE_COMPRESSED_IMAGE_SIZE 0x86A0 #define GL_TEXTURE_COMPRESSION_HINT 0x84EF #define GL_TEXTURE_COORD_ARRAY 0x8078 #define GL_TEXTURE_COORD_ARRAY_BUFFER_BINDING 0x889A #define GL_TEXTURE_COORD_ARRAY_POINTER 0x8092 #define GL_TEXTURE_COORD_ARRAY_SIZE 0x8088 #define GL_TEXTURE_COORD_ARRAY_STRIDE 0x808A #define GL_TEXTURE_COORD_ARRAY_TYPE 0x8089 #define GL_TEXTURE_CUBE_MAP 0x8513 #define GL_TEXTURE_CUBE_MAP_ARRAY 0x9009 #define GL_TEXTURE_CUBE_MAP_NEGATIVE_X 0x8516 #define GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 0x8518 #define GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 0x851A #define GL_TEXTURE_CUBE_MAP_POSITIVE_X 0x8515 #define GL_TEXTURE_CUBE_MAP_POSITIVE_Y 0x8517 #define GL_TEXTURE_CUBE_MAP_POSITIVE_Z 0x8519 #define GL_TEXTURE_CUBE_MAP_SEAMLESS 0x884F #define GL_TEXTURE_DEPTH 0x8071 #define GL_TEXTURE_DEPTH_SIZE 0x884A #define GL_TEXTURE_DEPTH_TYPE 0x8C16 #define GL_TEXTURE_ENV 0x2300 #define GL_TEXTURE_ENV_COLOR 0x2201 #define GL_TEXTURE_ENV_MODE 0x2200 #define GL_TEXTURE_FETCH_BARRIER_BIT 0x00000008 #define GL_TEXTURE_FILTER_CONTROL 0x8500 #define GL_TEXTURE_FIXED_SAMPLE_LOCATIONS 0x9107 #define GL_TEXTURE_GATHER 0x82A2 #define GL_TEXTURE_GATHER_SHADOW 0x82A3 #define GL_TEXTURE_GEN_MODE 0x2500 #define GL_TEXTURE_GEN_Q 0x0C63 #define GL_TEXTURE_GEN_R 0x0C62 #define GL_TEXTURE_GEN_S 0x0C60 #define GL_TEXTURE_GEN_T 0x0C61 #define GL_TEXTURE_GREEN_SIZE 0x805D #define GL_TEXTURE_GREEN_TYPE 0x8C11 #define GL_TEXTURE_HEIGHT 0x1001 #define GL_TEXTURE_IMAGE_FORMAT 0x828F #define GL_TEXTURE_IMAGE_TYPE 0x8290 #define GL_TEXTURE_IMMUTABLE_FORMAT 0x912F #define GL_TEXTURE_IMMUTABLE_LEVELS 0x82DF #define GL_TEXTURE_INTENSITY_SIZE 0x8061 #define GL_TEXTURE_INTENSITY_TYPE 0x8C15 #define GL_TEXTURE_INTERNAL_FORMAT 0x1003 #define GL_TEXTURE_LOD_BIAS 0x8501 #define GL_TEXTURE_LUMINANCE_SIZE 0x8060 #define GL_TEXTURE_LUMINANCE_TYPE 0x8C14 #define GL_TEXTURE_MAG_FILTER 0x2800 #define GL_TEXTURE_MATRIX 0x0BA8 #define GL_TEXTURE_MAX_ANISOTROPY 0x84FE #define GL_TEXTURE_MAX_LEVEL 0x813D #define GL_TEXTURE_MAX_LOD 0x813B #define GL_TEXTURE_MIN_FILTER 0x2801 #define GL_TEXTURE_MIN_LOD 0x813A #define GL_TEXTURE_PRIORITY 0x8066 #define GL_TEXTURE_RECTANGLE 0x84F5 #define GL_TEXTURE_RED_SIZE 0x805C #define GL_TEXTURE_RED_TYPE 0x8C10 #define GL_TEXTURE_RESIDENT 0x8067 #define GL_TEXTURE_SAMPLES 0x9106 #define GL_TEXTURE_SHADOW 0x82A1 #define GL_TEXTURE_SHARED_SIZE 0x8C3F #define GL_TEXTURE_STACK_DEPTH 0x0BA5 #define GL_TEXTURE_STENCIL_SIZE 0x88F1 #define GL_TEXTURE_SWIZZLE_A 0x8E45 #define GL_TEXTURE_SWIZZLE_B 0x8E44 #define GL_TEXTURE_SWIZZLE_G 0x8E43 #define GL_TEXTURE_SWIZZLE_R 0x8E42 #define GL_TEXTURE_SWIZZLE_RGBA 0x8E46 #define GL_TEXTURE_TARGET 0x1006 #define GL_TEXTURE_UPDATE_BARRIER_BIT 0x00000100 #define GL_TEXTURE_VIEW 0x82B5 #define GL_TEXTURE_VIEW_MIN_LAYER 0x82DD #define GL_TEXTURE_VIEW_MIN_LEVEL 0x82DB #define GL_TEXTURE_VIEW_NUM_LAYERS 0x82DE #define GL_TEXTURE_VIEW_NUM_LEVELS 0x82DC #define GL_TEXTURE_WIDTH 0x1000 #define GL_TEXTURE_WRAP_R 0x8072 #define GL_TEXTURE_WRAP_S 0x2802 #define GL_TEXTURE_WRAP_T 0x2803 #define GL_TIMEOUT_EXPIRED 0x911B #define GL_TIMEOUT_IGNORED 0xFFFFFFFFFFFFFFFF #define GL_TIMESTAMP 0x8E28 #define GL_TIME_ELAPSED 0x88BF #define GL_TOP_LEVEL_ARRAY_SIZE 0x930C #define GL_TOP_LEVEL_ARRAY_STRIDE 0x930D #define GL_TRANSFORM_BIT 0x00001000 #define GL_TRANSFORM_FEEDBACK 0x8E22 #define GL_TRANSFORM_FEEDBACK_ACTIVE 0x8E24 #define GL_TRANSFORM_FEEDBACK_BARRIER_BIT 0x00000800 #define GL_TRANSFORM_FEEDBACK_BINDING 0x8E25 #define GL_TRANSFORM_FEEDBACK_BUFFER 0x8C8E #define GL_TRANSFORM_FEEDBACK_BUFFER_ACTIVE 0x8E24 #define GL_TRANSFORM_FEEDBACK_BUFFER_BINDING 0x8C8F #define GL_TRANSFORM_FEEDBACK_BUFFER_INDEX 0x934B #define GL_TRANSFORM_FEEDBACK_BUFFER_MODE 0x8C7F #define GL_TRANSFORM_FEEDBACK_BUFFER_PAUSED 0x8E23 #define GL_TRANSFORM_FEEDBACK_BUFFER_SIZE 0x8C85 #define GL_TRANSFORM_FEEDBACK_BUFFER_START 0x8C84 #define GL_TRANSFORM_FEEDBACK_BUFFER_STRIDE 0x934C #define GL_TRANSFORM_FEEDBACK_OVERFLOW 0x82EC #define GL_TRANSFORM_FEEDBACK_PAUSED 0x8E23 #define GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN 0x8C88 #define GL_TRANSFORM_FEEDBACK_STREAM_OVERFLOW 0x82ED #define GL_TRANSFORM_FEEDBACK_VARYING 0x92F4 #define GL_TRANSFORM_FEEDBACK_VARYINGS 0x8C83 #define GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH 0x8C76 #define GL_TRANSPOSE_COLOR_MATRIX 0x84E6 #define GL_TRANSPOSE_MODELVIEW_MATRIX 0x84E3 #define GL_TRANSPOSE_PROJECTION_MATRIX 0x84E4 #define GL_TRANSPOSE_TEXTURE_MATRIX 0x84E5 #define GL_TRIANGLES 0x0004 #define GL_TRIANGLES_ADJACENCY 0x000C #define GL_TRIANGLE_FAN 0x0006 #define GL_TRIANGLE_STRIP 0x0005 #define GL_TRIANGLE_STRIP_ADJACENCY 0x000D #define GL_TRUE 1 #define GL_TYPE 0x92FA #define GL_UNDEFINED_VERTEX 0x8260 #define GL_UNIFORM 0x92E1 #define GL_UNIFORM_ARRAY_STRIDE 0x8A3C #define GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX 0x92DA #define GL_UNIFORM_BARRIER_BIT 0x00000004 #define GL_UNIFORM_BLOCK 0x92E2 #define GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS 0x8A42 #define GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES 0x8A43 #define GL_UNIFORM_BLOCK_BINDING 0x8A3F #define GL_UNIFORM_BLOCK_DATA_SIZE 0x8A40 #define GL_UNIFORM_BLOCK_INDEX 0x8A3A #define GL_UNIFORM_BLOCK_NAME_LENGTH 0x8A41 #define GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER 0x90EC #define GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER 0x8A46 #define GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER 0x8A45 #define GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER 0x84F0 #define GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER 0x84F1 #define GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER 0x8A44 #define GL_UNIFORM_BUFFER 0x8A11 #define GL_UNIFORM_BUFFER_BINDING 0x8A28 #define GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT 0x8A34 #define GL_UNIFORM_BUFFER_SIZE 0x8A2A #define GL_UNIFORM_BUFFER_START 0x8A29 #define GL_UNIFORM_IS_ROW_MAJOR 0x8A3E #define GL_UNIFORM_MATRIX_STRIDE 0x8A3D #define GL_UNIFORM_NAME_LENGTH 0x8A39 #define GL_UNIFORM_OFFSET 0x8A3B #define GL_UNIFORM_SIZE 0x8A38 #define GL_UNIFORM_TYPE 0x8A37 #define GL_UNKNOWN_CONTEXT_RESET 0x8255 #define GL_UNPACK_ALIGNMENT 0x0CF5 #define GL_UNPACK_COMPRESSED_BLOCK_DEPTH 0x9129 #define GL_UNPACK_COMPRESSED_BLOCK_HEIGHT 0x9128 #define GL_UNPACK_COMPRESSED_BLOCK_SIZE 0x912A #define GL_UNPACK_COMPRESSED_BLOCK_WIDTH 0x9127 #define GL_UNPACK_IMAGE_HEIGHT 0x806E #define GL_UNPACK_LSB_FIRST 0x0CF1 #define GL_UNPACK_ROW_LENGTH 0x0CF2 #define GL_UNPACK_SKIP_IMAGES 0x806D #define GL_UNPACK_SKIP_PIXELS 0x0CF4 #define GL_UNPACK_SKIP_ROWS 0x0CF3 #define GL_UNPACK_SWAP_BYTES 0x0CF0 #define GL_UNSIGNALED 0x9118 #define GL_UNSIGNED_BYTE 0x1401 #define GL_UNSIGNED_BYTE_2_3_3_REV 0x8362 #define GL_UNSIGNED_BYTE_3_3_2 0x8032 #define GL_UNSIGNED_INT 0x1405 #define GL_UNSIGNED_INT_10F_11F_11F_REV 0x8C3B #define GL_UNSIGNED_INT_10_10_10_2 0x8036 #define GL_UNSIGNED_INT_24_8 0x84FA #define GL_UNSIGNED_INT_2_10_10_10_REV 0x8368 #define GL_UNSIGNED_INT_5_9_9_9_REV 0x8C3E #define GL_UNSIGNED_INT_8_8_8_8 0x8035 #define GL_UNSIGNED_INT_8_8_8_8_REV 0x8367 #define GL_UNSIGNED_INT_ATOMIC_COUNTER 0x92DB #define GL_UNSIGNED_INT_IMAGE_1D 0x9062 #define GL_UNSIGNED_INT_IMAGE_1D_ARRAY 0x9068 #define GL_UNSIGNED_INT_IMAGE_2D 0x9063 #define GL_UNSIGNED_INT_IMAGE_2D_ARRAY 0x9069 #define GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE 0x906B #define GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY 0x906C #define GL_UNSIGNED_INT_IMAGE_2D_RECT 0x9065 #define GL_UNSIGNED_INT_IMAGE_3D 0x9064 #define GL_UNSIGNED_INT_IMAGE_BUFFER 0x9067 #define GL_UNSIGNED_INT_IMAGE_CUBE 0x9066 #define GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY 0x906A #define GL_UNSIGNED_INT_SAMPLER_1D 0x8DD1 #define GL_UNSIGNED_INT_SAMPLER_1D_ARRAY 0x8DD6 #define GL_UNSIGNED_INT_SAMPLER_2D 0x8DD2 #define GL_UNSIGNED_INT_SAMPLER_2D_ARRAY 0x8DD7 #define GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE 0x910A #define GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY 0x910D #define GL_UNSIGNED_INT_SAMPLER_2D_RECT 0x8DD5 #define GL_UNSIGNED_INT_SAMPLER_3D 0x8DD3 #define GL_UNSIGNED_INT_SAMPLER_BUFFER 0x8DD8 #define GL_UNSIGNED_INT_SAMPLER_CUBE 0x8DD4 #define GL_UNSIGNED_INT_SAMPLER_CUBE_MAP_ARRAY 0x900F #define GL_UNSIGNED_INT_VEC2 0x8DC6 #define GL_UNSIGNED_INT_VEC3 0x8DC7 #define GL_UNSIGNED_INT_VEC4 0x8DC8 #define GL_UNSIGNED_NORMALIZED 0x8C17 #define GL_UNSIGNED_SHORT 0x1403 #define GL_UNSIGNED_SHORT_1_5_5_5_REV 0x8366 #define GL_UNSIGNED_SHORT_4_4_4_4 0x8033 #define GL_UNSIGNED_SHORT_4_4_4_4_REV 0x8365 #define GL_UNSIGNED_SHORT_5_5_5_1 0x8034 #define GL_UNSIGNED_SHORT_5_6_5 0x8363 #define GL_UNSIGNED_SHORT_5_6_5_REV 0x8364 #define GL_UPPER_LEFT 0x8CA2 #define GL_V2F 0x2A20 #define GL_V3F 0x2A21 #define GL_VALIDATE_STATUS 0x8B83 #define GL_VENDOR 0x1F00 #define GL_VERSION 0x1F02 #define GL_VERTEX_ARRAY 0x8074 #define GL_VERTEX_ARRAY_BINDING 0x85B5 #define GL_VERTEX_ARRAY_BUFFER_BINDING 0x8896 #define GL_VERTEX_ARRAY_POINTER 0x808E #define GL_VERTEX_ARRAY_SIZE 0x807A #define GL_VERTEX_ARRAY_STRIDE 0x807C #define GL_VERTEX_ARRAY_TYPE 0x807B #define GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT 0x00000001 #define GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING 0x889F #define GL_VERTEX_ATTRIB_ARRAY_DIVISOR 0x88FE #define GL_VERTEX_ATTRIB_ARRAY_ENABLED 0x8622 #define GL_VERTEX_ATTRIB_ARRAY_INTEGER 0x88FD #define GL_VERTEX_ATTRIB_ARRAY_LONG 0x874E #define GL_VERTEX_ATTRIB_ARRAY_NORMALIZED 0x886A #define GL_VERTEX_ATTRIB_ARRAY_POINTER 0x8645 #define GL_VERTEX_ATTRIB_ARRAY_SIZE 0x8623 #define GL_VERTEX_ATTRIB_ARRAY_STRIDE 0x8624 #define GL_VERTEX_ATTRIB_ARRAY_TYPE 0x8625 #define GL_VERTEX_ATTRIB_BINDING 0x82D4 #define GL_VERTEX_ATTRIB_RELATIVE_OFFSET 0x82D5 #define GL_VERTEX_BINDING_BUFFER 0x8F4F #define GL_VERTEX_BINDING_DIVISOR 0x82D6 #define GL_VERTEX_BINDING_OFFSET 0x82D7 #define GL_VERTEX_BINDING_STRIDE 0x82D8 #define GL_VERTEX_PROGRAM_POINT_SIZE 0x8642 #define GL_VERTEX_PROGRAM_TWO_SIDE 0x8643 #define GL_VERTEX_SHADER 0x8B31 #define GL_VERTEX_SHADER_BIT 0x00000001 #define GL_VERTEX_SHADER_INVOCATIONS 0x82F0 #define GL_VERTEX_SUBROUTINE 0x92E8 #define GL_VERTEX_SUBROUTINE_UNIFORM 0x92EE #define GL_VERTEX_TEXTURE 0x829B #define GL_VERTICES_SUBMITTED 0x82EE #define GL_VIEWPORT 0x0BA2 #define GL_VIEWPORT_BIT 0x00000800 #define GL_VIEWPORT_BOUNDS_RANGE 0x825D #define GL_VIEWPORT_INDEX_PROVOKING_VERTEX 0x825F #define GL_VIEWPORT_SUBPIXEL_BITS 0x825C #define GL_VIEW_CLASS_128_BITS 0x82C4 #define GL_VIEW_CLASS_16_BITS 0x82CA #define GL_VIEW_CLASS_24_BITS 0x82C9 #define GL_VIEW_CLASS_32_BITS 0x82C8 #define GL_VIEW_CLASS_48_BITS 0x82C7 #define GL_VIEW_CLASS_64_BITS 0x82C6 #define GL_VIEW_CLASS_8_BITS 0x82CB #define GL_VIEW_CLASS_96_BITS 0x82C5 #define GL_VIEW_CLASS_BPTC_FLOAT 0x82D3 #define GL_VIEW_CLASS_BPTC_UNORM 0x82D2 #define GL_VIEW_CLASS_RGTC1_RED 0x82D0 #define GL_VIEW_CLASS_RGTC2_RG 0x82D1 #define GL_VIEW_CLASS_S3TC_DXT1_RGB 0x82CC #define GL_VIEW_CLASS_S3TC_DXT1_RGBA 0x82CD #define GL_VIEW_CLASS_S3TC_DXT3_RGBA 0x82CE #define GL_VIEW_CLASS_S3TC_DXT5_RGBA 0x82CF #define GL_VIEW_COMPATIBILITY_CLASS 0x82B6 #define GL_WAIT_FAILED 0x911D #define GL_WEIGHT_ARRAY_BUFFER_BINDING 0x889E #define GL_WRITE_ONLY 0x88B9 #define GL_XOR 0x1506 #define GL_ZERO 0 #define GL_ZERO_TO_ONE 0x935F #define GL_ZOOM_X 0x0D16 #define GL_ZOOM_Y 0x0D17 #include typedef unsigned int GLenum; typedef unsigned char GLboolean; typedef unsigned int GLbitfield; typedef void GLvoid; typedef khronos_int8_t GLbyte; typedef khronos_uint8_t GLubyte; typedef khronos_int16_t GLshort; typedef khronos_uint16_t GLushort; typedef int GLint; typedef unsigned int GLuint; typedef khronos_int32_t GLclampx; typedef int GLsizei; typedef khronos_float_t GLfloat; typedef khronos_float_t GLclampf; typedef double GLdouble; typedef double GLclampd; typedef void *GLeglClientBufferEXT; typedef void *GLeglImageOES; typedef char GLchar; typedef char GLcharARB; #ifdef __APPLE__ typedef void *GLhandleARB; #else typedef unsigned int GLhandleARB; #endif typedef khronos_uint16_t GLhalf; typedef khronos_uint16_t GLhalfARB; typedef khronos_int32_t GLfixed; #if defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && (__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > 1060) typedef khronos_intptr_t GLintptr; #else typedef khronos_intptr_t GLintptr; #endif #if defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && (__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > 1060) typedef khronos_intptr_t GLintptrARB; #else typedef khronos_intptr_t GLintptrARB; #endif #if defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && (__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > 1060) typedef khronos_ssize_t GLsizeiptr; #else typedef khronos_ssize_t GLsizeiptr; #endif #if defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && (__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > 1060) typedef khronos_ssize_t GLsizeiptrARB; #else typedef khronos_ssize_t GLsizeiptrARB; #endif typedef khronos_int64_t GLint64; typedef khronos_int64_t GLint64EXT; typedef khronos_uint64_t GLuint64; typedef khronos_uint64_t GLuint64EXT; typedef struct __GLsync *GLsync; struct _cl_context; struct _cl_event; typedef void(GLAD_API_PTR *GLDEBUGPROC)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam); typedef void(GLAD_API_PTR *GLDEBUGPROCARB)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam); typedef void(GLAD_API_PTR *GLDEBUGPROCKHR)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam); typedef void(GLAD_API_PTR *GLDEBUGPROCAMD)(GLuint id, GLenum category, GLenum severity, GLsizei length, const GLchar *message, void *userParam); typedef unsigned short GLhalfNV; typedef GLintptr GLvdpauSurfaceNV; typedef void(GLAD_API_PTR *GLVULKANPROCNV)(void); #define GL_VERSION_1_0 1 #define GL_VERSION_1_1 1 #define GL_VERSION_1_2 1 #define GL_VERSION_1_3 1 #define GL_VERSION_1_4 1 #define GL_VERSION_1_5 1 #define GL_VERSION_2_0 1 #define GL_VERSION_2_1 1 #define GL_VERSION_3_0 1 #define GL_VERSION_3_1 1 #define GL_VERSION_3_2 1 #define GL_VERSION_3_3 1 #define GL_VERSION_4_0 1 #define GL_VERSION_4_1 1 #define GL_VERSION_4_2 1 #define GL_VERSION_4_3 1 #define GL_VERSION_4_4 1 #define GL_VERSION_4_5 1 #define GL_VERSION_4_6 1 typedef void(GLAD_API_PTR *PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)(GLenum target, GLeglImageOES image); typedef void(GLAD_API_PTR *PFNGLACCUMPROC)(GLenum op, GLfloat value); typedef void(GLAD_API_PTR *PFNGLACTIVESHADERPROGRAMPROC)(GLuint pipeline, GLuint program); typedef void(GLAD_API_PTR *PFNGLACTIVETEXTUREPROC)(GLenum texture); typedef void(GLAD_API_PTR *PFNGLALPHAFUNCPROC)(GLenum func, GLfloat ref); typedef GLboolean(GLAD_API_PTR *PFNGLARETEXTURESRESIDENTPROC)(GLsizei n, const GLuint *textures, GLboolean *residences); typedef void(GLAD_API_PTR *PFNGLARRAYELEMENTPROC)(GLint i); typedef void(GLAD_API_PTR *PFNGLATTACHSHADERPROC)(GLuint program, GLuint shader); typedef void(GLAD_API_PTR *PFNGLBEGINPROC)(GLenum mode); typedef void(GLAD_API_PTR *PFNGLBEGINCONDITIONALRENDERPROC)(GLuint id, GLenum mode); typedef void(GLAD_API_PTR *PFNGLBEGINQUERYPROC)(GLenum target, GLuint id); typedef void(GLAD_API_PTR *PFNGLBEGINQUERYINDEXEDPROC)(GLenum target, GLuint index, GLuint id); typedef void(GLAD_API_PTR *PFNGLBEGINTRANSFORMFEEDBACKPROC)(GLenum primitiveMode); typedef void(GLAD_API_PTR *PFNGLBINDATTRIBLOCATIONPROC)(GLuint program, GLuint index, const GLchar *name); typedef void(GLAD_API_PTR *PFNGLBINDBUFFERPROC)(GLenum target, GLuint buffer); typedef void(GLAD_API_PTR *PFNGLBINDBUFFERBASEPROC)(GLenum target, GLuint index, GLuint buffer); typedef void(GLAD_API_PTR *PFNGLBINDBUFFERRANGEPROC)(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size); typedef void(GLAD_API_PTR *PFNGLBINDBUFFERSBASEPROC)(GLenum target, GLuint first, GLsizei count, const GLuint *buffers); typedef void(GLAD_API_PTR *PFNGLBINDBUFFERSRANGEPROC)(GLenum target, GLuint first, GLsizei count, const GLuint *buffers, const GLintptr *offsets, const GLsizeiptr *sizes); typedef void(GLAD_API_PTR *PFNGLBINDFRAGDATALOCATIONPROC)(GLuint program, GLuint color, const GLchar *name); typedef void(GLAD_API_PTR *PFNGLBINDFRAGDATALOCATIONINDEXEDPROC)(GLuint program, GLuint colorNumber, GLuint index, const GLchar *name); typedef void(GLAD_API_PTR *PFNGLBINDFRAMEBUFFERPROC)(GLenum target, GLuint framebuffer); typedef void(GLAD_API_PTR *PFNGLBINDIMAGETEXTUREPROC)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format); typedef void(GLAD_API_PTR *PFNGLBINDIMAGETEXTURESPROC)(GLuint first, GLsizei count, const GLuint *textures); typedef void(GLAD_API_PTR *PFNGLBINDPROGRAMPIPELINEPROC)(GLuint pipeline); typedef void(GLAD_API_PTR *PFNGLBINDRENDERBUFFERPROC)(GLenum target, GLuint renderbuffer); typedef void(GLAD_API_PTR *PFNGLBINDSAMPLERPROC)(GLuint unit, GLuint sampler); typedef void(GLAD_API_PTR *PFNGLBINDSAMPLERSPROC)(GLuint first, GLsizei count, const GLuint *samplers); typedef void(GLAD_API_PTR *PFNGLBINDTEXTUREPROC)(GLenum target, GLuint texture); typedef void(GLAD_API_PTR *PFNGLBINDTEXTUREUNITPROC)(GLuint unit, GLuint texture); typedef void(GLAD_API_PTR *PFNGLBINDTEXTURESPROC)(GLuint first, GLsizei count, const GLuint *textures); typedef void(GLAD_API_PTR *PFNGLBINDTRANSFORMFEEDBACKPROC)(GLenum target, GLuint id); typedef void(GLAD_API_PTR *PFNGLBINDVERTEXARRAYPROC)(GLuint array); typedef void(GLAD_API_PTR *PFNGLBINDVERTEXBUFFERPROC)(GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride); typedef void(GLAD_API_PTR *PFNGLBINDVERTEXBUFFERSPROC)(GLuint first, GLsizei count, const GLuint *buffers, const GLintptr *offsets, const GLsizei *strides); typedef void(GLAD_API_PTR *PFNGLBITMAPPROC)(GLsizei width, GLsizei height, GLfloat xorig, GLfloat yorig, GLfloat xmove, GLfloat ymove, const GLubyte *bitmap); typedef void(GLAD_API_PTR *PFNGLBLENDCOLORPROC)(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha); typedef void(GLAD_API_PTR *PFNGLBLENDEQUATIONPROC)(GLenum mode); typedef void(GLAD_API_PTR *PFNGLBLENDEQUATIONSEPARATEPROC)(GLenum modeRGB, GLenum modeAlpha); typedef void(GLAD_API_PTR *PFNGLBLENDEQUATIONSEPARATEIPROC)(GLuint buf, GLenum modeRGB, GLenum modeAlpha); typedef void(GLAD_API_PTR *PFNGLBLENDEQUATIONIPROC)(GLuint buf, GLenum mode); typedef void(GLAD_API_PTR *PFNGLBLENDFUNCPROC)(GLenum sfactor, GLenum dfactor); typedef void(GLAD_API_PTR *PFNGLBLENDFUNCSEPARATEPROC)(GLenum sfactorRGB, GLenum dfactorRGB, GLenum sfactorAlpha, GLenum dfactorAlpha); typedef void(GLAD_API_PTR *PFNGLBLENDFUNCSEPARATEIPROC)(GLuint buf, GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha); typedef void(GLAD_API_PTR *PFNGLBLENDFUNCIPROC)(GLuint buf, GLenum src, GLenum dst); typedef 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); typedef 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); typedef void(GLAD_API_PTR *PFNGLBUFFERDATAPROC)(GLenum target, GLsizeiptr size, const void *data, GLenum usage); typedef void(GLAD_API_PTR *PFNGLBUFFERSTORAGEPROC)(GLenum target, GLsizeiptr size, const void *data, GLbitfield flags); typedef void(GLAD_API_PTR *PFNGLBUFFERSUBDATAPROC)(GLenum target, GLintptr offset, GLsizeiptr size, const void *data); typedef void(GLAD_API_PTR *PFNGLCALLLISTPROC)(GLuint list); typedef void(GLAD_API_PTR *PFNGLCALLLISTSPROC)(GLsizei n, GLenum type, const void *lists); typedef GLenum(GLAD_API_PTR *PFNGLCHECKFRAMEBUFFERSTATUSPROC)(GLenum target); typedef GLenum(GLAD_API_PTR *PFNGLCHECKNAMEDFRAMEBUFFERSTATUSPROC)(GLuint framebuffer, GLenum target); typedef void(GLAD_API_PTR *PFNGLCLAMPCOLORPROC)(GLenum target, GLenum clamp); typedef void(GLAD_API_PTR *PFNGLCLEARPROC)(GLbitfield mask); typedef void(GLAD_API_PTR *PFNGLCLEARACCUMPROC)(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha); typedef void(GLAD_API_PTR *PFNGLCLEARBUFFERDATAPROC)(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void *data); typedef void(GLAD_API_PTR *PFNGLCLEARBUFFERSUBDATAPROC)(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void *data); typedef void(GLAD_API_PTR *PFNGLCLEARBUFFERFIPROC)(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil); typedef void(GLAD_API_PTR *PFNGLCLEARBUFFERFVPROC)(GLenum buffer, GLint drawbuffer, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLCLEARBUFFERIVPROC)(GLenum buffer, GLint drawbuffer, const GLint *value); typedef void(GLAD_API_PTR *PFNGLCLEARBUFFERUIVPROC)(GLenum buffer, GLint drawbuffer, const GLuint *value); typedef void(GLAD_API_PTR *PFNGLCLEARCOLORPROC)(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha); typedef void(GLAD_API_PTR *PFNGLCLEARDEPTHPROC)(GLdouble depth); typedef void(GLAD_API_PTR *PFNGLCLEARDEPTHFPROC)(GLfloat d); typedef void(GLAD_API_PTR *PFNGLCLEARINDEXPROC)(GLfloat c); typedef void(GLAD_API_PTR *PFNGLCLEARNAMEDBUFFERDATAPROC)(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void *data); typedef void(GLAD_API_PTR *PFNGLCLEARNAMEDBUFFERSUBDATAPROC)(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void *data); typedef void(GLAD_API_PTR *PFNGLCLEARNAMEDFRAMEBUFFERFIPROC)(GLuint framebuffer, GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil); typedef void(GLAD_API_PTR *PFNGLCLEARNAMEDFRAMEBUFFERFVPROC)(GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLCLEARNAMEDFRAMEBUFFERIVPROC)(GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLint *value); typedef void(GLAD_API_PTR *PFNGLCLEARNAMEDFRAMEBUFFERUIVPROC)(GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLuint *value); typedef void(GLAD_API_PTR *PFNGLCLEARSTENCILPROC)(GLint s); typedef void(GLAD_API_PTR *PFNGLCLEARTEXIMAGEPROC)(GLuint texture, GLint level, GLenum format, GLenum type, const void *data); typedef 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); typedef void(GLAD_API_PTR *PFNGLCLIENTACTIVETEXTUREPROC)(GLenum texture); typedef GLenum(GLAD_API_PTR *PFNGLCLIENTWAITSYNCPROC)(GLsync sync, GLbitfield flags, GLuint64 timeout); typedef void(GLAD_API_PTR *PFNGLCLIPCONTROLPROC)(GLenum origin, GLenum depth); typedef void(GLAD_API_PTR *PFNGLCLIPPLANEPROC)(GLenum plane, const GLdouble *equation); typedef void(GLAD_API_PTR *PFNGLCOLOR3BPROC)(GLbyte red, GLbyte green, GLbyte blue); typedef void(GLAD_API_PTR *PFNGLCOLOR3BVPROC)(const GLbyte *v); typedef void(GLAD_API_PTR *PFNGLCOLOR3DPROC)(GLdouble red, GLdouble green, GLdouble blue); typedef void(GLAD_API_PTR *PFNGLCOLOR3DVPROC)(const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLCOLOR3FPROC)(GLfloat red, GLfloat green, GLfloat blue); typedef void(GLAD_API_PTR *PFNGLCOLOR3FVPROC)(const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLCOLOR3IPROC)(GLint red, GLint green, GLint blue); typedef void(GLAD_API_PTR *PFNGLCOLOR3IVPROC)(const GLint *v); typedef void(GLAD_API_PTR *PFNGLCOLOR3SPROC)(GLshort red, GLshort green, GLshort blue); typedef void(GLAD_API_PTR *PFNGLCOLOR3SVPROC)(const GLshort *v); typedef void(GLAD_API_PTR *PFNGLCOLOR3UBPROC)(GLubyte red, GLubyte green, GLubyte blue); typedef void(GLAD_API_PTR *PFNGLCOLOR3UBVPROC)(const GLubyte *v); typedef void(GLAD_API_PTR *PFNGLCOLOR3UIPROC)(GLuint red, GLuint green, GLuint blue); typedef void(GLAD_API_PTR *PFNGLCOLOR3UIVPROC)(const GLuint *v); typedef void(GLAD_API_PTR *PFNGLCOLOR3USPROC)(GLushort red, GLushort green, GLushort blue); typedef void(GLAD_API_PTR *PFNGLCOLOR3USVPROC)(const GLushort *v); typedef void(GLAD_API_PTR *PFNGLCOLOR4BPROC)(GLbyte red, GLbyte green, GLbyte blue, GLbyte alpha); typedef void(GLAD_API_PTR *PFNGLCOLOR4BVPROC)(const GLbyte *v); typedef void(GLAD_API_PTR *PFNGLCOLOR4DPROC)(GLdouble red, GLdouble green, GLdouble blue, GLdouble alpha); typedef void(GLAD_API_PTR *PFNGLCOLOR4DVPROC)(const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLCOLOR4FPROC)(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha); typedef void(GLAD_API_PTR *PFNGLCOLOR4FVPROC)(const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLCOLOR4IPROC)(GLint red, GLint green, GLint blue, GLint alpha); typedef void(GLAD_API_PTR *PFNGLCOLOR4IVPROC)(const GLint *v); typedef void(GLAD_API_PTR *PFNGLCOLOR4SPROC)(GLshort red, GLshort green, GLshort blue, GLshort alpha); typedef void(GLAD_API_PTR *PFNGLCOLOR4SVPROC)(const GLshort *v); typedef void(GLAD_API_PTR *PFNGLCOLOR4UBPROC)(GLubyte red, GLubyte green, GLubyte blue, GLubyte alpha); typedef void(GLAD_API_PTR *PFNGLCOLOR4UBVPROC)(const GLubyte *v); typedef void(GLAD_API_PTR *PFNGLCOLOR4UIPROC)(GLuint red, GLuint green, GLuint blue, GLuint alpha); typedef void(GLAD_API_PTR *PFNGLCOLOR4UIVPROC)(const GLuint *v); typedef void(GLAD_API_PTR *PFNGLCOLOR4USPROC)(GLushort red, GLushort green, GLushort blue, GLushort alpha); typedef void(GLAD_API_PTR *PFNGLCOLOR4USVPROC)(const GLushort *v); typedef void(GLAD_API_PTR *PFNGLCOLORMASKPROC)(GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha); typedef void(GLAD_API_PTR *PFNGLCOLORMASKIPROC)(GLuint index, GLboolean r, GLboolean g, GLboolean b, GLboolean a); typedef void(GLAD_API_PTR *PFNGLCOLORMATERIALPROC)(GLenum face, GLenum mode); typedef void(GLAD_API_PTR *PFNGLCOLORP3UIPROC)(GLenum type, GLuint color); typedef void(GLAD_API_PTR *PFNGLCOLORP3UIVPROC)(GLenum type, const GLuint *color); typedef void(GLAD_API_PTR *PFNGLCOLORP4UIPROC)(GLenum type, GLuint color); typedef void(GLAD_API_PTR *PFNGLCOLORP4UIVPROC)(GLenum type, const GLuint *color); typedef void(GLAD_API_PTR *PFNGLCOLORPOINTERPROC)(GLint size, GLenum type, GLsizei stride, const void *pointer); typedef void(GLAD_API_PTR *PFNGLCOMPILESHADERPROC)(GLuint shader); typedef void(GLAD_API_PTR *PFNGLCOMPRESSEDTEXIMAGE1DPROC)(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, const void *data); typedef void(GLAD_API_PTR *PFNGLCOMPRESSEDTEXIMAGE2DPROC)(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void *data); typedef void(GLAD_API_PTR *PFNGLCOMPRESSEDTEXIMAGE3DPROC)(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void *data); typedef void(GLAD_API_PTR *PFNGLCOMPRESSEDTEXSUBIMAGE1DPROC)(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void *data); typedef void(GLAD_API_PTR *PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void *data); typedef 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); typedef void(GLAD_API_PTR *PFNGLCOMPRESSEDTEXTURESUBIMAGE1DPROC)(GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void *data); typedef void(GLAD_API_PTR *PFNGLCOMPRESSEDTEXTURESUBIMAGE2DPROC)(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void *data); typedef 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); typedef void(GLAD_API_PTR *PFNGLCOPYBUFFERSUBDATAPROC)(GLenum readTarget, GLenum writeTarget, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size); typedef 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); typedef void(GLAD_API_PTR *PFNGLCOPYNAMEDBUFFERSUBDATAPROC)(GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size); typedef void(GLAD_API_PTR *PFNGLCOPYPIXELSPROC)(GLint x, GLint y, GLsizei width, GLsizei height, GLenum type); typedef void(GLAD_API_PTR *PFNGLCOPYTEXIMAGE1DPROC)(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLint border); typedef void(GLAD_API_PTR *PFNGLCOPYTEXIMAGE2DPROC)(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLsizei height, GLint border); typedef void(GLAD_API_PTR *PFNGLCOPYTEXSUBIMAGE1DPROC)(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width); typedef void(GLAD_API_PTR *PFNGLCOPYTEXSUBIMAGE2DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height); typedef void(GLAD_API_PTR *PFNGLCOPYTEXSUBIMAGE3DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height); typedef void(GLAD_API_PTR *PFNGLCOPYTEXTURESUBIMAGE1DPROC)(GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width); typedef void(GLAD_API_PTR *PFNGLCOPYTEXTURESUBIMAGE2DPROC)(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height); typedef void(GLAD_API_PTR *PFNGLCOPYTEXTURESUBIMAGE3DPROC)(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height); typedef void(GLAD_API_PTR *PFNGLCREATEBUFFERSPROC)(GLsizei n, GLuint *buffers); typedef void(GLAD_API_PTR *PFNGLCREATEFRAMEBUFFERSPROC)(GLsizei n, GLuint *framebuffers); typedef GLuint(GLAD_API_PTR *PFNGLCREATEPROGRAMPROC)(void); typedef void(GLAD_API_PTR *PFNGLCREATEPROGRAMPIPELINESPROC)(GLsizei n, GLuint *pipelines); typedef void(GLAD_API_PTR *PFNGLCREATEQUERIESPROC)(GLenum target, GLsizei n, GLuint *ids); typedef void(GLAD_API_PTR *PFNGLCREATERENDERBUFFERSPROC)(GLsizei n, GLuint *renderbuffers); typedef void(GLAD_API_PTR *PFNGLCREATESAMPLERSPROC)(GLsizei n, GLuint *samplers); typedef GLuint(GLAD_API_PTR *PFNGLCREATESHADERPROC)(GLenum type); typedef GLuint(GLAD_API_PTR *PFNGLCREATESHADERPROGRAMVPROC)(GLenum type, GLsizei count, const GLchar *const *strings); typedef void(GLAD_API_PTR *PFNGLCREATETEXTURESPROC)(GLenum target, GLsizei n, GLuint *textures); typedef void(GLAD_API_PTR *PFNGLCREATETRANSFORMFEEDBACKSPROC)(GLsizei n, GLuint *ids); typedef void(GLAD_API_PTR *PFNGLCREATEVERTEXARRAYSPROC)(GLsizei n, GLuint *arrays); typedef void(GLAD_API_PTR *PFNGLCULLFACEPROC)(GLenum mode); typedef void(GLAD_API_PTR *PFNGLDEBUGMESSAGECALLBACKPROC)(GLDEBUGPROC callback, const void *userParam); typedef void(GLAD_API_PTR *PFNGLDEBUGMESSAGECONTROLPROC)(GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint *ids, GLboolean enabled); typedef void(GLAD_API_PTR *PFNGLDEBUGMESSAGEINSERTPROC)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *buf); typedef void(GLAD_API_PTR *PFNGLDELETEBUFFERSPROC)(GLsizei n, const GLuint *buffers); typedef void(GLAD_API_PTR *PFNGLDELETEFRAMEBUFFERSPROC)(GLsizei n, const GLuint *framebuffers); typedef void(GLAD_API_PTR *PFNGLDELETELISTSPROC)(GLuint list, GLsizei range); typedef void(GLAD_API_PTR *PFNGLDELETEPROGRAMPROC)(GLuint program); typedef void(GLAD_API_PTR *PFNGLDELETEPROGRAMPIPELINESPROC)(GLsizei n, const GLuint *pipelines); typedef void(GLAD_API_PTR *PFNGLDELETEQUERIESPROC)(GLsizei n, const GLuint *ids); typedef void(GLAD_API_PTR *PFNGLDELETERENDERBUFFERSPROC)(GLsizei n, const GLuint *renderbuffers); typedef void(GLAD_API_PTR *PFNGLDELETESAMPLERSPROC)(GLsizei count, const GLuint *samplers); typedef void(GLAD_API_PTR *PFNGLDELETESHADERPROC)(GLuint shader); typedef void(GLAD_API_PTR *PFNGLDELETESYNCPROC)(GLsync sync); typedef void(GLAD_API_PTR *PFNGLDELETETEXTURESPROC)(GLsizei n, const GLuint *textures); typedef void(GLAD_API_PTR *PFNGLDELETETRANSFORMFEEDBACKSPROC)(GLsizei n, const GLuint *ids); typedef void(GLAD_API_PTR *PFNGLDELETEVERTEXARRAYSPROC)(GLsizei n, const GLuint *arrays); typedef void(GLAD_API_PTR *PFNGLDEPTHFUNCPROC)(GLenum func); typedef void(GLAD_API_PTR *PFNGLDEPTHMASKPROC)(GLboolean flag); typedef void(GLAD_API_PTR *PFNGLDEPTHRANGEPROC)(GLdouble n, GLdouble f); typedef void(GLAD_API_PTR *PFNGLDEPTHRANGEARRAYVPROC)(GLuint first, GLsizei count, const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLDEPTHRANGEINDEXEDPROC)(GLuint index, GLdouble n, GLdouble f); typedef void(GLAD_API_PTR *PFNGLDEPTHRANGEFPROC)(GLfloat n, GLfloat f); typedef void(GLAD_API_PTR *PFNGLDETACHSHADERPROC)(GLuint program, GLuint shader); typedef void(GLAD_API_PTR *PFNGLDISABLEPROC)(GLenum cap); typedef void(GLAD_API_PTR *PFNGLDISABLECLIENTSTATEPROC)(GLenum array); typedef void(GLAD_API_PTR *PFNGLDISABLEVERTEXARRAYATTRIBPROC)(GLuint vaobj, GLuint index); typedef void(GLAD_API_PTR *PFNGLDISABLEVERTEXATTRIBARRAYPROC)(GLuint index); typedef void(GLAD_API_PTR *PFNGLDISABLEIPROC)(GLenum target, GLuint index); typedef void(GLAD_API_PTR *PFNGLDISPATCHCOMPUTEPROC)(GLuint num_groups_x, GLuint num_groups_y, GLuint num_groups_z); typedef void(GLAD_API_PTR *PFNGLDISPATCHCOMPUTEINDIRECTPROC)(GLintptr indirect); typedef void(GLAD_API_PTR *PFNGLDRAWARRAYSPROC)(GLenum mode, GLint first, GLsizei count); typedef void(GLAD_API_PTR *PFNGLDRAWARRAYSINDIRECTPROC)(GLenum mode, const void *indirect); typedef void(GLAD_API_PTR *PFNGLDRAWARRAYSINSTANCEDPROC)(GLenum mode, GLint first, GLsizei count, GLsizei instancecount); typedef void(GLAD_API_PTR *PFNGLDRAWARRAYSINSTANCEDBASEINSTANCEPROC)(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance); typedef void(GLAD_API_PTR *PFNGLDRAWBUFFERPROC)(GLenum buf); typedef void(GLAD_API_PTR *PFNGLDRAWBUFFERSPROC)(GLsizei n, const GLenum *bufs); typedef void(GLAD_API_PTR *PFNGLDRAWELEMENTSPROC)(GLenum mode, GLsizei count, GLenum type, const void *indices); typedef void(GLAD_API_PTR *PFNGLDRAWELEMENTSBASEVERTEXPROC)(GLenum mode, GLsizei count, GLenum type, const void *indices, GLint basevertex); typedef void(GLAD_API_PTR *PFNGLDRAWELEMENTSINDIRECTPROC)(GLenum mode, GLenum type, const void *indirect); typedef void(GLAD_API_PTR *PFNGLDRAWELEMENTSINSTANCEDPROC)(GLenum mode, GLsizei count, GLenum type, const void *indices, GLsizei instancecount); typedef void(GLAD_API_PTR *PFNGLDRAWELEMENTSINSTANCEDBASEINSTANCEPROC)(GLenum mode, GLsizei count, GLenum type, const void *indices, GLsizei instancecount, GLuint baseinstance); typedef void(GLAD_API_PTR *PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXPROC)(GLenum mode, GLsizei count, GLenum type, const void *indices, GLsizei instancecount, GLint basevertex); typedef void(GLAD_API_PTR *PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXBASEINSTANCEPROC)(GLenum mode, GLsizei count, GLenum type, const void *indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance); typedef void(GLAD_API_PTR *PFNGLDRAWPIXELSPROC)(GLsizei width, GLsizei height, GLenum format, GLenum type, const void *pixels); typedef void(GLAD_API_PTR *PFNGLDRAWRANGEELEMENTSPROC)(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void *indices); typedef void(GLAD_API_PTR *PFNGLDRAWRANGEELEMENTSBASEVERTEXPROC)(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void *indices, GLint basevertex); typedef void(GLAD_API_PTR *PFNGLDRAWTRANSFORMFEEDBACKPROC)(GLenum mode, GLuint id); typedef void(GLAD_API_PTR *PFNGLDRAWTRANSFORMFEEDBACKINSTANCEDPROC)(GLenum mode, GLuint id, GLsizei instancecount); typedef void(GLAD_API_PTR *PFNGLDRAWTRANSFORMFEEDBACKSTREAMPROC)(GLenum mode, GLuint id, GLuint stream); typedef void(GLAD_API_PTR *PFNGLDRAWTRANSFORMFEEDBACKSTREAMINSTANCEDPROC)(GLenum mode, GLuint id, GLuint stream, GLsizei instancecount); typedef void(GLAD_API_PTR *PFNGLEDGEFLAGPROC)(GLboolean flag); typedef void(GLAD_API_PTR *PFNGLEDGEFLAGPOINTERPROC)(GLsizei stride, const void *pointer); typedef void(GLAD_API_PTR *PFNGLEDGEFLAGVPROC)(const GLboolean *flag); typedef void(GLAD_API_PTR *PFNGLENABLEPROC)(GLenum cap); typedef void(GLAD_API_PTR *PFNGLENABLECLIENTSTATEPROC)(GLenum array); typedef void(GLAD_API_PTR *PFNGLENABLEVERTEXARRAYATTRIBPROC)(GLuint vaobj, GLuint index); typedef void(GLAD_API_PTR *PFNGLENABLEVERTEXATTRIBARRAYPROC)(GLuint index); typedef void(GLAD_API_PTR *PFNGLENABLEIPROC)(GLenum target, GLuint index); typedef void(GLAD_API_PTR *PFNGLENDPROC)(void); typedef void(GLAD_API_PTR *PFNGLENDCONDITIONALRENDERPROC)(void); typedef void(GLAD_API_PTR *PFNGLENDLISTPROC)(void); typedef void(GLAD_API_PTR *PFNGLENDQUERYPROC)(GLenum target); typedef void(GLAD_API_PTR *PFNGLENDQUERYINDEXEDPROC)(GLenum target, GLuint index); typedef void(GLAD_API_PTR *PFNGLENDTRANSFORMFEEDBACKPROC)(void); typedef void(GLAD_API_PTR *PFNGLEVALCOORD1DPROC)(GLdouble u); typedef void(GLAD_API_PTR *PFNGLEVALCOORD1DVPROC)(const GLdouble *u); typedef void(GLAD_API_PTR *PFNGLEVALCOORD1FPROC)(GLfloat u); typedef void(GLAD_API_PTR *PFNGLEVALCOORD1FVPROC)(const GLfloat *u); typedef void(GLAD_API_PTR *PFNGLEVALCOORD2DPROC)(GLdouble u, GLdouble v); typedef void(GLAD_API_PTR *PFNGLEVALCOORD2DVPROC)(const GLdouble *u); typedef void(GLAD_API_PTR *PFNGLEVALCOORD2FPROC)(GLfloat u, GLfloat v); typedef void(GLAD_API_PTR *PFNGLEVALCOORD2FVPROC)(const GLfloat *u); typedef void(GLAD_API_PTR *PFNGLEVALMESH1PROC)(GLenum mode, GLint i1, GLint i2); typedef void(GLAD_API_PTR *PFNGLEVALMESH2PROC)(GLenum mode, GLint i1, GLint i2, GLint j1, GLint j2); typedef void(GLAD_API_PTR *PFNGLEVALPOINT1PROC)(GLint i); typedef void(GLAD_API_PTR *PFNGLEVALPOINT2PROC)(GLint i, GLint j); typedef void(GLAD_API_PTR *PFNGLFEEDBACKBUFFERPROC)(GLsizei size, GLenum type, GLfloat *buffer); typedef GLsync(GLAD_API_PTR *PFNGLFENCESYNCPROC)(GLenum condition, GLbitfield flags); typedef void(GLAD_API_PTR *PFNGLFINISHPROC)(void); typedef void(GLAD_API_PTR *PFNGLFLUSHPROC)(void); typedef void(GLAD_API_PTR *PFNGLFLUSHMAPPEDBUFFERRANGEPROC)(GLenum target, GLintptr offset, GLsizeiptr length); typedef void(GLAD_API_PTR *PFNGLFLUSHMAPPEDNAMEDBUFFERRANGEPROC)(GLuint buffer, GLintptr offset, GLsizeiptr length); typedef void(GLAD_API_PTR *PFNGLFOGCOORDPOINTERPROC)(GLenum type, GLsizei stride, const void *pointer); typedef void(GLAD_API_PTR *PFNGLFOGCOORDDPROC)(GLdouble coord); typedef void(GLAD_API_PTR *PFNGLFOGCOORDDVPROC)(const GLdouble *coord); typedef void(GLAD_API_PTR *PFNGLFOGCOORDFPROC)(GLfloat coord); typedef void(GLAD_API_PTR *PFNGLFOGCOORDFVPROC)(const GLfloat *coord); typedef void(GLAD_API_PTR *PFNGLFOGFPROC)(GLenum pname, GLfloat param); typedef void(GLAD_API_PTR *PFNGLFOGFVPROC)(GLenum pname, const GLfloat *params); typedef void(GLAD_API_PTR *PFNGLFOGIPROC)(GLenum pname, GLint param); typedef void(GLAD_API_PTR *PFNGLFOGIVPROC)(GLenum pname, const GLint *params); typedef void(GLAD_API_PTR *PFNGLFRAMEBUFFERPARAMETERIPROC)(GLenum target, GLenum pname, GLint param); typedef void(GLAD_API_PTR *PFNGLFRAMEBUFFERRENDERBUFFERPROC)(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer); typedef void(GLAD_API_PTR *PFNGLFRAMEBUFFERTEXTUREPROC)(GLenum target, GLenum attachment, GLuint texture, GLint level); typedef void(GLAD_API_PTR *PFNGLFRAMEBUFFERTEXTURE1DPROC)(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level); typedef void(GLAD_API_PTR *PFNGLFRAMEBUFFERTEXTURE2DPROC)(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level); typedef void(GLAD_API_PTR *PFNGLFRAMEBUFFERTEXTURE3DPROC)(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level, GLint zoffset); typedef void(GLAD_API_PTR *PFNGLFRAMEBUFFERTEXTURELAYERPROC)(GLenum target, GLenum attachment, GLuint texture, GLint level, GLint layer); typedef void(GLAD_API_PTR *PFNGLFRONTFACEPROC)(GLenum mode); typedef void(GLAD_API_PTR *PFNGLFRUSTUMPROC)(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble zNear, GLdouble zFar); typedef void(GLAD_API_PTR *PFNGLGENBUFFERSPROC)(GLsizei n, GLuint *buffers); typedef void(GLAD_API_PTR *PFNGLGENFRAMEBUFFERSPROC)(GLsizei n, GLuint *framebuffers); typedef GLuint(GLAD_API_PTR *PFNGLGENLISTSPROC)(GLsizei range); typedef void(GLAD_API_PTR *PFNGLGENPROGRAMPIPELINESPROC)(GLsizei n, GLuint *pipelines); typedef void(GLAD_API_PTR *PFNGLGENQUERIESPROC)(GLsizei n, GLuint *ids); typedef void(GLAD_API_PTR *PFNGLGENRENDERBUFFERSPROC)(GLsizei n, GLuint *renderbuffers); typedef void(GLAD_API_PTR *PFNGLGENSAMPLERSPROC)(GLsizei count, GLuint *samplers); typedef void(GLAD_API_PTR *PFNGLGENTEXTURESPROC)(GLsizei n, GLuint *textures); typedef void(GLAD_API_PTR *PFNGLGENTRANSFORMFEEDBACKSPROC)(GLsizei n, GLuint *ids); typedef void(GLAD_API_PTR *PFNGLGENVERTEXARRAYSPROC)(GLsizei n, GLuint *arrays); typedef void(GLAD_API_PTR *PFNGLGENERATEMIPMAPPROC)(GLenum target); typedef void(GLAD_API_PTR *PFNGLGENERATETEXTUREMIPMAPPROC)(GLuint texture); typedef void(GLAD_API_PTR *PFNGLGETACTIVEATOMICCOUNTERBUFFERIVPROC)(GLuint program, GLuint bufferIndex, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETACTIVEATTRIBPROC)(GLuint program, GLuint index, GLsizei bufSize, GLsizei *length, GLint *size, GLenum *type, GLchar *name); typedef void(GLAD_API_PTR *PFNGLGETACTIVESUBROUTINENAMEPROC)(GLuint program, GLenum shadertype, GLuint index, GLsizei bufSize, GLsizei *length, GLchar *name); typedef void(GLAD_API_PTR *PFNGLGETACTIVESUBROUTINEUNIFORMNAMEPROC)(GLuint program, GLenum shadertype, GLuint index, GLsizei bufSize, GLsizei *length, GLchar *name); typedef void(GLAD_API_PTR *PFNGLGETACTIVESUBROUTINEUNIFORMIVPROC)(GLuint program, GLenum shadertype, GLuint index, GLenum pname, GLint *values); typedef void(GLAD_API_PTR *PFNGLGETACTIVEUNIFORMPROC)(GLuint program, GLuint index, GLsizei bufSize, GLsizei *length, GLint *size, GLenum *type, GLchar *name); typedef void(GLAD_API_PTR *PFNGLGETACTIVEUNIFORMBLOCKNAMEPROC)(GLuint program, GLuint uniformBlockIndex, GLsizei bufSize, GLsizei *length, GLchar *uniformBlockName); typedef void(GLAD_API_PTR *PFNGLGETACTIVEUNIFORMBLOCKIVPROC)(GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETACTIVEUNIFORMNAMEPROC)(GLuint program, GLuint uniformIndex, GLsizei bufSize, GLsizei *length, GLchar *uniformName); typedef void(GLAD_API_PTR *PFNGLGETACTIVEUNIFORMSIVPROC)(GLuint program, GLsizei uniformCount, const GLuint *uniformIndices, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETATTACHEDSHADERSPROC)(GLuint program, GLsizei maxCount, GLsizei *count, GLuint *shaders); typedef GLint(GLAD_API_PTR *PFNGLGETATTRIBLOCATIONPROC)(GLuint program, const GLchar *name); typedef void(GLAD_API_PTR *PFNGLGETBOOLEANI_VPROC)(GLenum target, GLuint index, GLboolean *data); typedef void(GLAD_API_PTR *PFNGLGETBOOLEANVPROC)(GLenum pname, GLboolean *data); typedef void(GLAD_API_PTR *PFNGLGETBUFFERPARAMETERI64VPROC)(GLenum target, GLenum pname, GLint64 *params); typedef void(GLAD_API_PTR *PFNGLGETBUFFERPARAMETERIVPROC)(GLenum target, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETBUFFERPOINTERVPROC)(GLenum target, GLenum pname, void **params); typedef void(GLAD_API_PTR *PFNGLGETBUFFERSUBDATAPROC)(GLenum target, GLintptr offset, GLsizeiptr size, void *data); typedef void(GLAD_API_PTR *PFNGLGETCLIPPLANEPROC)(GLenum plane, GLdouble *equation); typedef void(GLAD_API_PTR *PFNGLGETCOMPRESSEDTEXIMAGEPROC)(GLenum target, GLint level, void *img); typedef void(GLAD_API_PTR *PFNGLGETCOMPRESSEDTEXTUREIMAGEPROC)(GLuint texture, GLint level, GLsizei bufSize, void *pixels); typedef 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); typedef GLuint(GLAD_API_PTR *PFNGLGETDEBUGMESSAGELOGPROC)(GLuint count, GLsizei bufSize, GLenum *sources, GLenum *types, GLuint *ids, GLenum *severities, GLsizei *lengths, GLchar *messageLog); typedef void(GLAD_API_PTR *PFNGLGETDOUBLEI_VPROC)(GLenum target, GLuint index, GLdouble *data); typedef void(GLAD_API_PTR *PFNGLGETDOUBLEVPROC)(GLenum pname, GLdouble *data); typedef GLenum(GLAD_API_PTR *PFNGLGETERRORPROC)(void); typedef void(GLAD_API_PTR *PFNGLGETFLOATI_VPROC)(GLenum target, GLuint index, GLfloat *data); typedef void(GLAD_API_PTR *PFNGLGETFLOATVPROC)(GLenum pname, GLfloat *data); typedef GLint(GLAD_API_PTR *PFNGLGETFRAGDATAINDEXPROC)(GLuint program, const GLchar *name); typedef GLint(GLAD_API_PTR *PFNGLGETFRAGDATALOCATIONPROC)(GLuint program, const GLchar *name); typedef void(GLAD_API_PTR *PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC)(GLenum target, GLenum attachment, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETFRAMEBUFFERPARAMETERIVPROC)(GLenum target, GLenum pname, GLint *params); typedef GLenum(GLAD_API_PTR *PFNGLGETGRAPHICSRESETSTATUSPROC)(void); typedef void(GLAD_API_PTR *PFNGLGETINTEGER64I_VPROC)(GLenum target, GLuint index, GLint64 *data); typedef void(GLAD_API_PTR *PFNGLGETINTEGER64VPROC)(GLenum pname, GLint64 *data); typedef void(GLAD_API_PTR *PFNGLGETINTEGERI_VPROC)(GLenum target, GLuint index, GLint *data); typedef void(GLAD_API_PTR *PFNGLGETINTEGERVPROC)(GLenum pname, GLint *data); typedef void(GLAD_API_PTR *PFNGLGETINTERNALFORMATI64VPROC)(GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64 *params); typedef void(GLAD_API_PTR *PFNGLGETINTERNALFORMATIVPROC)(GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETLIGHTFVPROC)(GLenum light, GLenum pname, GLfloat *params); typedef void(GLAD_API_PTR *PFNGLGETLIGHTIVPROC)(GLenum light, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETMAPDVPROC)(GLenum target, GLenum query, GLdouble *v); typedef void(GLAD_API_PTR *PFNGLGETMAPFVPROC)(GLenum target, GLenum query, GLfloat *v); typedef void(GLAD_API_PTR *PFNGLGETMAPIVPROC)(GLenum target, GLenum query, GLint *v); typedef void(GLAD_API_PTR *PFNGLGETMATERIALFVPROC)(GLenum face, GLenum pname, GLfloat *params); typedef void(GLAD_API_PTR *PFNGLGETMATERIALIVPROC)(GLenum face, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETMULTISAMPLEFVPROC)(GLenum pname, GLuint index, GLfloat *val); typedef void(GLAD_API_PTR *PFNGLGETNAMEDBUFFERPARAMETERI64VPROC)(GLuint buffer, GLenum pname, GLint64 *params); typedef void(GLAD_API_PTR *PFNGLGETNAMEDBUFFERPARAMETERIVPROC)(GLuint buffer, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETNAMEDBUFFERPOINTERVPROC)(GLuint buffer, GLenum pname, void **params); typedef void(GLAD_API_PTR *PFNGLGETNAMEDBUFFERSUBDATAPROC)(GLuint buffer, GLintptr offset, GLsizeiptr size, void *data); typedef void(GLAD_API_PTR *PFNGLGETNAMEDFRAMEBUFFERATTACHMENTPARAMETERIVPROC)(GLuint framebuffer, GLenum attachment, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETNAMEDFRAMEBUFFERPARAMETERIVPROC)(GLuint framebuffer, GLenum pname, GLint *param); typedef void(GLAD_API_PTR *PFNGLGETNAMEDRENDERBUFFERPARAMETERIVPROC)(GLuint renderbuffer, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETOBJECTLABELPROC)(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei *length, GLchar *label); typedef void(GLAD_API_PTR *PFNGLGETOBJECTPTRLABELPROC)(const void *ptr, GLsizei bufSize, GLsizei *length, GLchar *label); typedef void(GLAD_API_PTR *PFNGLGETPIXELMAPFVPROC)(GLenum map, GLfloat *values); typedef void(GLAD_API_PTR *PFNGLGETPIXELMAPUIVPROC)(GLenum map, GLuint *values); typedef void(GLAD_API_PTR *PFNGLGETPIXELMAPUSVPROC)(GLenum map, GLushort *values); typedef void(GLAD_API_PTR *PFNGLGETPOINTERVPROC)(GLenum pname, void **params); typedef void(GLAD_API_PTR *PFNGLGETPOLYGONSTIPPLEPROC)(GLubyte *mask); typedef void(GLAD_API_PTR *PFNGLGETPROGRAMBINARYPROC)(GLuint program, GLsizei bufSize, GLsizei *length, GLenum *binaryFormat, void *binary); typedef void(GLAD_API_PTR *PFNGLGETPROGRAMINFOLOGPROC)(GLuint program, GLsizei bufSize, GLsizei *length, GLchar *infoLog); typedef void(GLAD_API_PTR *PFNGLGETPROGRAMINTERFACEIVPROC)(GLuint program, GLenum programInterface, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETPROGRAMPIPELINEINFOLOGPROC)(GLuint pipeline, GLsizei bufSize, GLsizei *length, GLchar *infoLog); typedef void(GLAD_API_PTR *PFNGLGETPROGRAMPIPELINEIVPROC)(GLuint pipeline, GLenum pname, GLint *params); typedef GLuint(GLAD_API_PTR *PFNGLGETPROGRAMRESOURCEINDEXPROC)(GLuint program, GLenum programInterface, const GLchar *name); typedef GLint(GLAD_API_PTR *PFNGLGETPROGRAMRESOURCELOCATIONPROC)(GLuint program, GLenum programInterface, const GLchar *name); typedef GLint(GLAD_API_PTR *PFNGLGETPROGRAMRESOURCELOCATIONINDEXPROC)(GLuint program, GLenum programInterface, const GLchar *name); typedef void(GLAD_API_PTR *PFNGLGETPROGRAMRESOURCENAMEPROC)(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei *length, GLchar *name); typedef void(GLAD_API_PTR *PFNGLGETPROGRAMRESOURCEIVPROC)(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount, const GLenum *props, GLsizei count, GLsizei *length, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETPROGRAMSTAGEIVPROC)(GLuint program, GLenum shadertype, GLenum pname, GLint *values); typedef void(GLAD_API_PTR *PFNGLGETPROGRAMIVPROC)(GLuint program, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETQUERYBUFFEROBJECTI64VPROC)(GLuint id, GLuint buffer, GLenum pname, GLintptr offset); typedef void(GLAD_API_PTR *PFNGLGETQUERYBUFFEROBJECTIVPROC)(GLuint id, GLuint buffer, GLenum pname, GLintptr offset); typedef void(GLAD_API_PTR *PFNGLGETQUERYBUFFEROBJECTUI64VPROC)(GLuint id, GLuint buffer, GLenum pname, GLintptr offset); typedef void(GLAD_API_PTR *PFNGLGETQUERYBUFFEROBJECTUIVPROC)(GLuint id, GLuint buffer, GLenum pname, GLintptr offset); typedef void(GLAD_API_PTR *PFNGLGETQUERYINDEXEDIVPROC)(GLenum target, GLuint index, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETQUERYOBJECTI64VPROC)(GLuint id, GLenum pname, GLint64 *params); typedef void(GLAD_API_PTR *PFNGLGETQUERYOBJECTIVPROC)(GLuint id, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETQUERYOBJECTUI64VPROC)(GLuint id, GLenum pname, GLuint64 *params); typedef void(GLAD_API_PTR *PFNGLGETQUERYOBJECTUIVPROC)(GLuint id, GLenum pname, GLuint *params); typedef void(GLAD_API_PTR *PFNGLGETQUERYIVPROC)(GLenum target, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETRENDERBUFFERPARAMETERIVPROC)(GLenum target, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETSAMPLERPARAMETERIIVPROC)(GLuint sampler, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETSAMPLERPARAMETERIUIVPROC)(GLuint sampler, GLenum pname, GLuint *params); typedef void(GLAD_API_PTR *PFNGLGETSAMPLERPARAMETERFVPROC)(GLuint sampler, GLenum pname, GLfloat *params); typedef void(GLAD_API_PTR *PFNGLGETSAMPLERPARAMETERIVPROC)(GLuint sampler, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETSHADERINFOLOGPROC)(GLuint shader, GLsizei bufSize, GLsizei *length, GLchar *infoLog); typedef void(GLAD_API_PTR *PFNGLGETSHADERPRECISIONFORMATPROC)(GLenum shadertype, GLenum precisiontype, GLint *range, GLint *precision); typedef void(GLAD_API_PTR *PFNGLGETSHADERSOURCEPROC)(GLuint shader, GLsizei bufSize, GLsizei *length, GLchar *source); typedef void(GLAD_API_PTR *PFNGLGETSHADERIVPROC)(GLuint shader, GLenum pname, GLint *params); typedef const GLubyte *(GLAD_API_PTR *PFNGLGETSTRINGPROC)(GLenum name); typedef const GLubyte *(GLAD_API_PTR *PFNGLGETSTRINGIPROC)(GLenum name, GLuint index); typedef GLuint(GLAD_API_PTR *PFNGLGETSUBROUTINEINDEXPROC)(GLuint program, GLenum shadertype, const GLchar *name); typedef GLint(GLAD_API_PTR *PFNGLGETSUBROUTINEUNIFORMLOCATIONPROC)(GLuint program, GLenum shadertype, const GLchar *name); typedef void(GLAD_API_PTR *PFNGLGETSYNCIVPROC)(GLsync sync, GLenum pname, GLsizei count, GLsizei *length, GLint *values); typedef void(GLAD_API_PTR *PFNGLGETTEXENVFVPROC)(GLenum target, GLenum pname, GLfloat *params); typedef void(GLAD_API_PTR *PFNGLGETTEXENVIVPROC)(GLenum target, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETTEXGENDVPROC)(GLenum coord, GLenum pname, GLdouble *params); typedef void(GLAD_API_PTR *PFNGLGETTEXGENFVPROC)(GLenum coord, GLenum pname, GLfloat *params); typedef void(GLAD_API_PTR *PFNGLGETTEXGENIVPROC)(GLenum coord, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETTEXIMAGEPROC)(GLenum target, GLint level, GLenum format, GLenum type, void *pixels); typedef void(GLAD_API_PTR *PFNGLGETTEXLEVELPARAMETERFVPROC)(GLenum target, GLint level, GLenum pname, GLfloat *params); typedef void(GLAD_API_PTR *PFNGLGETTEXLEVELPARAMETERIVPROC)(GLenum target, GLint level, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETTEXPARAMETERIIVPROC)(GLenum target, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETTEXPARAMETERIUIVPROC)(GLenum target, GLenum pname, GLuint *params); typedef void(GLAD_API_PTR *PFNGLGETTEXPARAMETERFVPROC)(GLenum target, GLenum pname, GLfloat *params); typedef void(GLAD_API_PTR *PFNGLGETTEXPARAMETERIVPROC)(GLenum target, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETTEXTUREIMAGEPROC)(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void *pixels); typedef void(GLAD_API_PTR *PFNGLGETTEXTURELEVELPARAMETERFVPROC)(GLuint texture, GLint level, GLenum pname, GLfloat *params); typedef void(GLAD_API_PTR *PFNGLGETTEXTURELEVELPARAMETERIVPROC)(GLuint texture, GLint level, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETTEXTUREPARAMETERIIVPROC)(GLuint texture, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETTEXTUREPARAMETERIUIVPROC)(GLuint texture, GLenum pname, GLuint *params); typedef void(GLAD_API_PTR *PFNGLGETTEXTUREPARAMETERFVPROC)(GLuint texture, GLenum pname, GLfloat *params); typedef void(GLAD_API_PTR *PFNGLGETTEXTUREPARAMETERIVPROC)(GLuint texture, GLenum pname, GLint *params); typedef 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); typedef void(GLAD_API_PTR *PFNGLGETTRANSFORMFEEDBACKVARYINGPROC)(GLuint program, GLuint index, GLsizei bufSize, GLsizei *length, GLsizei *size, GLenum *type, GLchar *name); typedef void(GLAD_API_PTR *PFNGLGETTRANSFORMFEEDBACKI64_VPROC)(GLuint xfb, GLenum pname, GLuint index, GLint64 *param); typedef void(GLAD_API_PTR *PFNGLGETTRANSFORMFEEDBACKI_VPROC)(GLuint xfb, GLenum pname, GLuint index, GLint *param); typedef void(GLAD_API_PTR *PFNGLGETTRANSFORMFEEDBACKIVPROC)(GLuint xfb, GLenum pname, GLint *param); typedef GLuint(GLAD_API_PTR *PFNGLGETUNIFORMBLOCKINDEXPROC)(GLuint program, const GLchar *uniformBlockName); typedef void(GLAD_API_PTR *PFNGLGETUNIFORMINDICESPROC)(GLuint program, GLsizei uniformCount, const GLchar *const *uniformNames, GLuint *uniformIndices); typedef GLint(GLAD_API_PTR *PFNGLGETUNIFORMLOCATIONPROC)(GLuint program, const GLchar *name); typedef void(GLAD_API_PTR *PFNGLGETUNIFORMSUBROUTINEUIVPROC)(GLenum shadertype, GLint location, GLuint *params); typedef void(GLAD_API_PTR *PFNGLGETUNIFORMDVPROC)(GLuint program, GLint location, GLdouble *params); typedef void(GLAD_API_PTR *PFNGLGETUNIFORMFVPROC)(GLuint program, GLint location, GLfloat *params); typedef void(GLAD_API_PTR *PFNGLGETUNIFORMIVPROC)(GLuint program, GLint location, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETUNIFORMUIVPROC)(GLuint program, GLint location, GLuint *params); typedef void(GLAD_API_PTR *PFNGLGETVERTEXARRAYINDEXED64IVPROC)(GLuint vaobj, GLuint index, GLenum pname, GLint64 *param); typedef void(GLAD_API_PTR *PFNGLGETVERTEXARRAYINDEXEDIVPROC)(GLuint vaobj, GLuint index, GLenum pname, GLint *param); typedef void(GLAD_API_PTR *PFNGLGETVERTEXARRAYIVPROC)(GLuint vaobj, GLenum pname, GLint *param); typedef void(GLAD_API_PTR *PFNGLGETVERTEXATTRIBIIVPROC)(GLuint index, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETVERTEXATTRIBIUIVPROC)(GLuint index, GLenum pname, GLuint *params); typedef void(GLAD_API_PTR *PFNGLGETVERTEXATTRIBLDVPROC)(GLuint index, GLenum pname, GLdouble *params); typedef void(GLAD_API_PTR *PFNGLGETVERTEXATTRIBPOINTERVPROC)(GLuint index, GLenum pname, void **pointer); typedef void(GLAD_API_PTR *PFNGLGETVERTEXATTRIBDVPROC)(GLuint index, GLenum pname, GLdouble *params); typedef void(GLAD_API_PTR *PFNGLGETVERTEXATTRIBFVPROC)(GLuint index, GLenum pname, GLfloat *params); typedef void(GLAD_API_PTR *PFNGLGETVERTEXATTRIBIVPROC)(GLuint index, GLenum pname, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETNCOLORTABLEPROC)(GLenum target, GLenum format, GLenum type, GLsizei bufSize, void *table); typedef void(GLAD_API_PTR *PFNGLGETNCOMPRESSEDTEXIMAGEPROC)(GLenum target, GLint lod, GLsizei bufSize, void *pixels); typedef void(GLAD_API_PTR *PFNGLGETNCONVOLUTIONFILTERPROC)(GLenum target, GLenum format, GLenum type, GLsizei bufSize, void *image); typedef void(GLAD_API_PTR *PFNGLGETNHISTOGRAMPROC)(GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void *values); typedef void(GLAD_API_PTR *PFNGLGETNMAPDVPROC)(GLenum target, GLenum query, GLsizei bufSize, GLdouble *v); typedef void(GLAD_API_PTR *PFNGLGETNMAPFVPROC)(GLenum target, GLenum query, GLsizei bufSize, GLfloat *v); typedef void(GLAD_API_PTR *PFNGLGETNMAPIVPROC)(GLenum target, GLenum query, GLsizei bufSize, GLint *v); typedef void(GLAD_API_PTR *PFNGLGETNMINMAXPROC)(GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void *values); typedef void(GLAD_API_PTR *PFNGLGETNPIXELMAPFVPROC)(GLenum map, GLsizei bufSize, GLfloat *values); typedef void(GLAD_API_PTR *PFNGLGETNPIXELMAPUIVPROC)(GLenum map, GLsizei bufSize, GLuint *values); typedef void(GLAD_API_PTR *PFNGLGETNPIXELMAPUSVPROC)(GLenum map, GLsizei bufSize, GLushort *values); typedef void(GLAD_API_PTR *PFNGLGETNPOLYGONSTIPPLEPROC)(GLsizei bufSize, GLubyte *pattern); typedef void(GLAD_API_PTR *PFNGLGETNSEPARABLEFILTERPROC)(GLenum target, GLenum format, GLenum type, GLsizei rowBufSize, void *row, GLsizei columnBufSize, void *column, void *span); typedef void(GLAD_API_PTR *PFNGLGETNTEXIMAGEPROC)(GLenum target, GLint level, GLenum format, GLenum type, GLsizei bufSize, void *pixels); typedef void(GLAD_API_PTR *PFNGLGETNUNIFORMDVPROC)(GLuint program, GLint location, GLsizei bufSize, GLdouble *params); typedef void(GLAD_API_PTR *PFNGLGETNUNIFORMFVPROC)(GLuint program, GLint location, GLsizei bufSize, GLfloat *params); typedef void(GLAD_API_PTR *PFNGLGETNUNIFORMIVPROC)(GLuint program, GLint location, GLsizei bufSize, GLint *params); typedef void(GLAD_API_PTR *PFNGLGETNUNIFORMUIVPROC)(GLuint program, GLint location, GLsizei bufSize, GLuint *params); typedef void(GLAD_API_PTR *PFNGLHINTPROC)(GLenum target, GLenum mode); typedef void(GLAD_API_PTR *PFNGLINDEXMASKPROC)(GLuint mask); typedef void(GLAD_API_PTR *PFNGLINDEXPOINTERPROC)(GLenum type, GLsizei stride, const void *pointer); typedef void(GLAD_API_PTR *PFNGLINDEXDPROC)(GLdouble c); typedef void(GLAD_API_PTR *PFNGLINDEXDVPROC)(const GLdouble *c); typedef void(GLAD_API_PTR *PFNGLINDEXFPROC)(GLfloat c); typedef void(GLAD_API_PTR *PFNGLINDEXFVPROC)(const GLfloat *c); typedef void(GLAD_API_PTR *PFNGLINDEXIPROC)(GLint c); typedef void(GLAD_API_PTR *PFNGLINDEXIVPROC)(const GLint *c); typedef void(GLAD_API_PTR *PFNGLINDEXSPROC)(GLshort c); typedef void(GLAD_API_PTR *PFNGLINDEXSVPROC)(const GLshort *c); typedef void(GLAD_API_PTR *PFNGLINDEXUBPROC)(GLubyte c); typedef void(GLAD_API_PTR *PFNGLINDEXUBVPROC)(const GLubyte *c); typedef void(GLAD_API_PTR *PFNGLINITNAMESPROC)(void); typedef void(GLAD_API_PTR *PFNGLINTERLEAVEDARRAYSPROC)(GLenum format, GLsizei stride, const void *pointer); typedef void(GLAD_API_PTR *PFNGLINVALIDATEBUFFERDATAPROC)(GLuint buffer); typedef void(GLAD_API_PTR *PFNGLINVALIDATEBUFFERSUBDATAPROC)(GLuint buffer, GLintptr offset, GLsizeiptr length); typedef void(GLAD_API_PTR *PFNGLINVALIDATEFRAMEBUFFERPROC)(GLenum target, GLsizei numAttachments, const GLenum *attachments); typedef void(GLAD_API_PTR *PFNGLINVALIDATENAMEDFRAMEBUFFERDATAPROC)(GLuint framebuffer, GLsizei numAttachments, const GLenum *attachments); typedef void(GLAD_API_PTR *PFNGLINVALIDATENAMEDFRAMEBUFFERSUBDATAPROC)(GLuint framebuffer, GLsizei numAttachments, const GLenum *attachments, GLint x, GLint y, GLsizei width, GLsizei height); typedef void(GLAD_API_PTR *PFNGLINVALIDATESUBFRAMEBUFFERPROC)(GLenum target, GLsizei numAttachments, const GLenum *attachments, GLint x, GLint y, GLsizei width, GLsizei height); typedef void(GLAD_API_PTR *PFNGLINVALIDATETEXIMAGEPROC)(GLuint texture, GLint level); typedef void(GLAD_API_PTR *PFNGLINVALIDATETEXSUBIMAGEPROC)(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth); typedef GLboolean(GLAD_API_PTR *PFNGLISBUFFERPROC)(GLuint buffer); typedef GLboolean(GLAD_API_PTR *PFNGLISENABLEDPROC)(GLenum cap); typedef GLboolean(GLAD_API_PTR *PFNGLISENABLEDIPROC)(GLenum target, GLuint index); typedef GLboolean(GLAD_API_PTR *PFNGLISFRAMEBUFFERPROC)(GLuint framebuffer); typedef GLboolean(GLAD_API_PTR *PFNGLISLISTPROC)(GLuint list); typedef GLboolean(GLAD_API_PTR *PFNGLISPROGRAMPROC)(GLuint program); typedef GLboolean(GLAD_API_PTR *PFNGLISPROGRAMPIPELINEPROC)(GLuint pipeline); typedef GLboolean(GLAD_API_PTR *PFNGLISQUERYPROC)(GLuint id); typedef GLboolean(GLAD_API_PTR *PFNGLISRENDERBUFFERPROC)(GLuint renderbuffer); typedef GLboolean(GLAD_API_PTR *PFNGLISSAMPLERPROC)(GLuint sampler); typedef GLboolean(GLAD_API_PTR *PFNGLISSHADERPROC)(GLuint shader); typedef GLboolean(GLAD_API_PTR *PFNGLISSYNCPROC)(GLsync sync); typedef GLboolean(GLAD_API_PTR *PFNGLISTEXTUREPROC)(GLuint texture); typedef GLboolean(GLAD_API_PTR *PFNGLISTRANSFORMFEEDBACKPROC)(GLuint id); typedef GLboolean(GLAD_API_PTR *PFNGLISVERTEXARRAYPROC)(GLuint array); typedef void(GLAD_API_PTR *PFNGLLIGHTMODELFPROC)(GLenum pname, GLfloat param); typedef void(GLAD_API_PTR *PFNGLLIGHTMODELFVPROC)(GLenum pname, const GLfloat *params); typedef void(GLAD_API_PTR *PFNGLLIGHTMODELIPROC)(GLenum pname, GLint param); typedef void(GLAD_API_PTR *PFNGLLIGHTMODELIVPROC)(GLenum pname, const GLint *params); typedef void(GLAD_API_PTR *PFNGLLIGHTFPROC)(GLenum light, GLenum pname, GLfloat param); typedef void(GLAD_API_PTR *PFNGLLIGHTFVPROC)(GLenum light, GLenum pname, const GLfloat *params); typedef void(GLAD_API_PTR *PFNGLLIGHTIPROC)(GLenum light, GLenum pname, GLint param); typedef void(GLAD_API_PTR *PFNGLLIGHTIVPROC)(GLenum light, GLenum pname, const GLint *params); typedef void(GLAD_API_PTR *PFNGLLINESTIPPLEPROC)(GLint factor, GLushort pattern); typedef void(GLAD_API_PTR *PFNGLLINEWIDTHPROC)(GLfloat width); typedef void(GLAD_API_PTR *PFNGLLINKPROGRAMPROC)(GLuint program); typedef void(GLAD_API_PTR *PFNGLLISTBASEPROC)(GLuint base); typedef void(GLAD_API_PTR *PFNGLLOADIDENTITYPROC)(void); typedef void(GLAD_API_PTR *PFNGLLOADMATRIXDPROC)(const GLdouble *m); typedef void(GLAD_API_PTR *PFNGLLOADMATRIXFPROC)(const GLfloat *m); typedef void(GLAD_API_PTR *PFNGLLOADNAMEPROC)(GLuint name); typedef void(GLAD_API_PTR *PFNGLLOADTRANSPOSEMATRIXDPROC)(const GLdouble *m); typedef void(GLAD_API_PTR *PFNGLLOADTRANSPOSEMATRIXFPROC)(const GLfloat *m); typedef void(GLAD_API_PTR *PFNGLLOGICOPPROC)(GLenum opcode); typedef void(GLAD_API_PTR *PFNGLMAP1DPROC)(GLenum target, GLdouble u1, GLdouble u2, GLint stride, GLint order, const GLdouble *points); typedef void(GLAD_API_PTR *PFNGLMAP1FPROC)(GLenum target, GLfloat u1, GLfloat u2, GLint stride, GLint order, const GLfloat *points); typedef 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); typedef 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); typedef void *(GLAD_API_PTR *PFNGLMAPBUFFERPROC)(GLenum target, GLenum access); typedef void *(GLAD_API_PTR *PFNGLMAPBUFFERRANGEPROC)(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access); typedef void(GLAD_API_PTR *PFNGLMAPGRID1DPROC)(GLint un, GLdouble u1, GLdouble u2); typedef void(GLAD_API_PTR *PFNGLMAPGRID1FPROC)(GLint un, GLfloat u1, GLfloat u2); typedef void(GLAD_API_PTR *PFNGLMAPGRID2DPROC)(GLint un, GLdouble u1, GLdouble u2, GLint vn, GLdouble v1, GLdouble v2); typedef void(GLAD_API_PTR *PFNGLMAPGRID2FPROC)(GLint un, GLfloat u1, GLfloat u2, GLint vn, GLfloat v1, GLfloat v2); typedef void *(GLAD_API_PTR *PFNGLMAPNAMEDBUFFERPROC)(GLuint buffer, GLenum access); typedef void *(GLAD_API_PTR *PFNGLMAPNAMEDBUFFERRANGEPROC)(GLuint buffer, GLintptr offset, GLsizeiptr length, GLbitfield access); typedef void(GLAD_API_PTR *PFNGLMATERIALFPROC)(GLenum face, GLenum pname, GLfloat param); typedef void(GLAD_API_PTR *PFNGLMATERIALFVPROC)(GLenum face, GLenum pname, const GLfloat *params); typedef void(GLAD_API_PTR *PFNGLMATERIALIPROC)(GLenum face, GLenum pname, GLint param); typedef void(GLAD_API_PTR *PFNGLMATERIALIVPROC)(GLenum face, GLenum pname, const GLint *params); typedef void(GLAD_API_PTR *PFNGLMATRIXMODEPROC)(GLenum mode); typedef void(GLAD_API_PTR *PFNGLMEMORYBARRIERPROC)(GLbitfield barriers); typedef void(GLAD_API_PTR *PFNGLMEMORYBARRIERBYREGIONPROC)(GLbitfield barriers); typedef void(GLAD_API_PTR *PFNGLMINSAMPLESHADINGPROC)(GLfloat value); typedef void(GLAD_API_PTR *PFNGLMULTMATRIXDPROC)(const GLdouble *m); typedef void(GLAD_API_PTR *PFNGLMULTMATRIXFPROC)(const GLfloat *m); typedef void(GLAD_API_PTR *PFNGLMULTTRANSPOSEMATRIXDPROC)(const GLdouble *m); typedef void(GLAD_API_PTR *PFNGLMULTTRANSPOSEMATRIXFPROC)(const GLfloat *m); typedef void(GLAD_API_PTR *PFNGLMULTIDRAWARRAYSPROC)(GLenum mode, const GLint *first, const GLsizei *count, GLsizei drawcount); typedef void(GLAD_API_PTR *PFNGLMULTIDRAWARRAYSINDIRECTPROC)(GLenum mode, const void *indirect, GLsizei drawcount, GLsizei stride); typedef void(GLAD_API_PTR *PFNGLMULTIDRAWARRAYSINDIRECTCOUNTPROC)(GLenum mode, const void *indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride); typedef void(GLAD_API_PTR *PFNGLMULTIDRAWELEMENTSPROC)(GLenum mode, const GLsizei *count, GLenum type, const void *const *indices, GLsizei drawcount); typedef void(GLAD_API_PTR *PFNGLMULTIDRAWELEMENTSBASEVERTEXPROC)(GLenum mode, const GLsizei *count, GLenum type, const void *const *indices, GLsizei drawcount, const GLint *basevertex); typedef void(GLAD_API_PTR *PFNGLMULTIDRAWELEMENTSINDIRECTPROC)(GLenum mode, GLenum type, const void *indirect, GLsizei drawcount, GLsizei stride); typedef void(GLAD_API_PTR *PFNGLMULTIDRAWELEMENTSINDIRECTCOUNTPROC)(GLenum mode, GLenum type, const void *indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD1DPROC)(GLenum target, GLdouble s); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD1DVPROC)(GLenum target, const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD1FPROC)(GLenum target, GLfloat s); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD1FVPROC)(GLenum target, const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD1IPROC)(GLenum target, GLint s); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD1IVPROC)(GLenum target, const GLint *v); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD1SPROC)(GLenum target, GLshort s); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD1SVPROC)(GLenum target, const GLshort *v); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD2DPROC)(GLenum target, GLdouble s, GLdouble t); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD2DVPROC)(GLenum target, const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD2FPROC)(GLenum target, GLfloat s, GLfloat t); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD2FVPROC)(GLenum target, const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD2IPROC)(GLenum target, GLint s, GLint t); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD2IVPROC)(GLenum target, const GLint *v); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD2SPROC)(GLenum target, GLshort s, GLshort t); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD2SVPROC)(GLenum target, const GLshort *v); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD3DPROC)(GLenum target, GLdouble s, GLdouble t, GLdouble r); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD3DVPROC)(GLenum target, const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD3FPROC)(GLenum target, GLfloat s, GLfloat t, GLfloat r); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD3FVPROC)(GLenum target, const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD3IPROC)(GLenum target, GLint s, GLint t, GLint r); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD3IVPROC)(GLenum target, const GLint *v); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD3SPROC)(GLenum target, GLshort s, GLshort t, GLshort r); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD3SVPROC)(GLenum target, const GLshort *v); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD4DPROC)(GLenum target, GLdouble s, GLdouble t, GLdouble r, GLdouble q); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD4DVPROC)(GLenum target, const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD4FPROC)(GLenum target, GLfloat s, GLfloat t, GLfloat r, GLfloat q); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD4FVPROC)(GLenum target, const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD4IPROC)(GLenum target, GLint s, GLint t, GLint r, GLint q); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD4IVPROC)(GLenum target, const GLint *v); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD4SPROC)(GLenum target, GLshort s, GLshort t, GLshort r, GLshort q); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD4SVPROC)(GLenum target, const GLshort *v); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORDP1UIPROC)(GLenum texture, GLenum type, GLuint coords); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORDP1UIVPROC)(GLenum texture, GLenum type, const GLuint *coords); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORDP2UIPROC)(GLenum texture, GLenum type, GLuint coords); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORDP2UIVPROC)(GLenum texture, GLenum type, const GLuint *coords); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORDP3UIPROC)(GLenum texture, GLenum type, GLuint coords); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORDP3UIVPROC)(GLenum texture, GLenum type, const GLuint *coords); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORDP4UIPROC)(GLenum texture, GLenum type, GLuint coords); typedef void(GLAD_API_PTR *PFNGLMULTITEXCOORDP4UIVPROC)(GLenum texture, GLenum type, const GLuint *coords); typedef void(GLAD_API_PTR *PFNGLNAMEDBUFFERDATAPROC)(GLuint buffer, GLsizeiptr size, const void *data, GLenum usage); typedef void(GLAD_API_PTR *PFNGLNAMEDBUFFERSTORAGEPROC)(GLuint buffer, GLsizeiptr size, const void *data, GLbitfield flags); typedef void(GLAD_API_PTR *PFNGLNAMEDBUFFERSUBDATAPROC)(GLuint buffer, GLintptr offset, GLsizeiptr size, const void *data); typedef void(GLAD_API_PTR *PFNGLNAMEDFRAMEBUFFERDRAWBUFFERPROC)(GLuint framebuffer, GLenum buf); typedef void(GLAD_API_PTR *PFNGLNAMEDFRAMEBUFFERDRAWBUFFERSPROC)(GLuint framebuffer, GLsizei n, const GLenum *bufs); typedef void(GLAD_API_PTR *PFNGLNAMEDFRAMEBUFFERPARAMETERIPROC)(GLuint framebuffer, GLenum pname, GLint param); typedef void(GLAD_API_PTR *PFNGLNAMEDFRAMEBUFFERREADBUFFERPROC)(GLuint framebuffer, GLenum src); typedef void(GLAD_API_PTR *PFNGLNAMEDFRAMEBUFFERRENDERBUFFERPROC)(GLuint framebuffer, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer); typedef void(GLAD_API_PTR *PFNGLNAMEDFRAMEBUFFERTEXTUREPROC)(GLuint framebuffer, GLenum attachment, GLuint texture, GLint level); typedef void(GLAD_API_PTR *PFNGLNAMEDFRAMEBUFFERTEXTURELAYERPROC)(GLuint framebuffer, GLenum attachment, GLuint texture, GLint level, GLint layer); typedef void(GLAD_API_PTR *PFNGLNAMEDRENDERBUFFERSTORAGEPROC)(GLuint renderbuffer, GLenum internalformat, GLsizei width, GLsizei height); typedef void(GLAD_API_PTR *PFNGLNAMEDRENDERBUFFERSTORAGEMULTISAMPLEPROC)(GLuint renderbuffer, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height); typedef void(GLAD_API_PTR *PFNGLNEWLISTPROC)(GLuint list, GLenum mode); typedef void(GLAD_API_PTR *PFNGLNORMAL3BPROC)(GLbyte nx, GLbyte ny, GLbyte nz); typedef void(GLAD_API_PTR *PFNGLNORMAL3BVPROC)(const GLbyte *v); typedef void(GLAD_API_PTR *PFNGLNORMAL3DPROC)(GLdouble nx, GLdouble ny, GLdouble nz); typedef void(GLAD_API_PTR *PFNGLNORMAL3DVPROC)(const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLNORMAL3FPROC)(GLfloat nx, GLfloat ny, GLfloat nz); typedef void(GLAD_API_PTR *PFNGLNORMAL3FVPROC)(const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLNORMAL3IPROC)(GLint nx, GLint ny, GLint nz); typedef void(GLAD_API_PTR *PFNGLNORMAL3IVPROC)(const GLint *v); typedef void(GLAD_API_PTR *PFNGLNORMAL3SPROC)(GLshort nx, GLshort ny, GLshort nz); typedef void(GLAD_API_PTR *PFNGLNORMAL3SVPROC)(const GLshort *v); typedef void(GLAD_API_PTR *PFNGLNORMALP3UIPROC)(GLenum type, GLuint coords); typedef void(GLAD_API_PTR *PFNGLNORMALP3UIVPROC)(GLenum type, const GLuint *coords); typedef void(GLAD_API_PTR *PFNGLNORMALPOINTERPROC)(GLenum type, GLsizei stride, const void *pointer); typedef void(GLAD_API_PTR *PFNGLOBJECTLABELPROC)(GLenum identifier, GLuint name, GLsizei length, const GLchar *label); typedef void(GLAD_API_PTR *PFNGLOBJECTPTRLABELPROC)(const void *ptr, GLsizei length, const GLchar *label); typedef void(GLAD_API_PTR *PFNGLORTHOPROC)(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble zNear, GLdouble zFar); typedef void(GLAD_API_PTR *PFNGLPASSTHROUGHPROC)(GLfloat token); typedef void(GLAD_API_PTR *PFNGLPATCHPARAMETERFVPROC)(GLenum pname, const GLfloat *values); typedef void(GLAD_API_PTR *PFNGLPATCHPARAMETERIPROC)(GLenum pname, GLint value); typedef void(GLAD_API_PTR *PFNGLPAUSETRANSFORMFEEDBACKPROC)(void); typedef void(GLAD_API_PTR *PFNGLPIXELMAPFVPROC)(GLenum map, GLsizei mapsize, const GLfloat *values); typedef void(GLAD_API_PTR *PFNGLPIXELMAPUIVPROC)(GLenum map, GLsizei mapsize, const GLuint *values); typedef void(GLAD_API_PTR *PFNGLPIXELMAPUSVPROC)(GLenum map, GLsizei mapsize, const GLushort *values); typedef void(GLAD_API_PTR *PFNGLPIXELSTOREFPROC)(GLenum pname, GLfloat param); typedef void(GLAD_API_PTR *PFNGLPIXELSTOREIPROC)(GLenum pname, GLint param); typedef void(GLAD_API_PTR *PFNGLPIXELTRANSFERFPROC)(GLenum pname, GLfloat param); typedef void(GLAD_API_PTR *PFNGLPIXELTRANSFERIPROC)(GLenum pname, GLint param); typedef void(GLAD_API_PTR *PFNGLPIXELZOOMPROC)(GLfloat xfactor, GLfloat yfactor); typedef void(GLAD_API_PTR *PFNGLPOINTPARAMETERFPROC)(GLenum pname, GLfloat param); typedef void(GLAD_API_PTR *PFNGLPOINTPARAMETERFVPROC)(GLenum pname, const GLfloat *params); typedef void(GLAD_API_PTR *PFNGLPOINTPARAMETERIPROC)(GLenum pname, GLint param); typedef void(GLAD_API_PTR *PFNGLPOINTPARAMETERIVPROC)(GLenum pname, const GLint *params); typedef void(GLAD_API_PTR *PFNGLPOINTSIZEPROC)(GLfloat size); typedef void(GLAD_API_PTR *PFNGLPOLYGONMODEPROC)(GLenum face, GLenum mode); typedef void(GLAD_API_PTR *PFNGLPOLYGONOFFSETPROC)(GLfloat factor, GLfloat units); typedef void(GLAD_API_PTR *PFNGLPOLYGONOFFSETCLAMPPROC)(GLfloat factor, GLfloat units, GLfloat clamp); typedef void(GLAD_API_PTR *PFNGLPOLYGONSTIPPLEPROC)(const GLubyte *mask); typedef void(GLAD_API_PTR *PFNGLPOPATTRIBPROC)(void); typedef void(GLAD_API_PTR *PFNGLPOPCLIENTATTRIBPROC)(void); typedef void(GLAD_API_PTR *PFNGLPOPDEBUGGROUPPROC)(void); typedef void(GLAD_API_PTR *PFNGLPOPMATRIXPROC)(void); typedef void(GLAD_API_PTR *PFNGLPOPNAMEPROC)(void); typedef void(GLAD_API_PTR *PFNGLPRIMITIVERESTARTINDEXPROC)(GLuint index); typedef void(GLAD_API_PTR *PFNGLPRIORITIZETEXTURESPROC)(GLsizei n, const GLuint *textures, const GLfloat *priorities); typedef void(GLAD_API_PTR *PFNGLPROGRAMBINARYPROC)(GLuint program, GLenum binaryFormat, const void *binary, GLsizei length); typedef void(GLAD_API_PTR *PFNGLPROGRAMPARAMETERIPROC)(GLuint program, GLenum pname, GLint value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM1DPROC)(GLuint program, GLint location, GLdouble v0); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM1DVPROC)(GLuint program, GLint location, GLsizei count, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM1FPROC)(GLuint program, GLint location, GLfloat v0); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM1FVPROC)(GLuint program, GLint location, GLsizei count, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM1IPROC)(GLuint program, GLint location, GLint v0); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM1IVPROC)(GLuint program, GLint location, GLsizei count, const GLint *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM1UIPROC)(GLuint program, GLint location, GLuint v0); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM1UIVPROC)(GLuint program, GLint location, GLsizei count, const GLuint *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM2DPROC)(GLuint program, GLint location, GLdouble v0, GLdouble v1); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM2DVPROC)(GLuint program, GLint location, GLsizei count, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM2FPROC)(GLuint program, GLint location, GLfloat v0, GLfloat v1); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM2FVPROC)(GLuint program, GLint location, GLsizei count, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM2IPROC)(GLuint program, GLint location, GLint v0, GLint v1); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM2IVPROC)(GLuint program, GLint location, GLsizei count, const GLint *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM2UIPROC)(GLuint program, GLint location, GLuint v0, GLuint v1); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM2UIVPROC)(GLuint program, GLint location, GLsizei count, const GLuint *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM3DPROC)(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM3DVPROC)(GLuint program, GLint location, GLsizei count, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM3FPROC)(GLuint program, GLint location, GLfloat v0, GLfloat v1, GLfloat v2); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM3FVPROC)(GLuint program, GLint location, GLsizei count, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM3IPROC)(GLuint program, GLint location, GLint v0, GLint v1, GLint v2); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM3IVPROC)(GLuint program, GLint location, GLsizei count, const GLint *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM3UIPROC)(GLuint program, GLint location, GLuint v0, GLuint v1, GLuint v2); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM3UIVPROC)(GLuint program, GLint location, GLsizei count, const GLuint *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM4DPROC)(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM4DVPROC)(GLuint program, GLint location, GLsizei count, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM4FPROC)(GLuint program, GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM4FVPROC)(GLuint program, GLint location, GLsizei count, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM4IPROC)(GLuint program, GLint location, GLint v0, GLint v1, GLint v2, GLint v3); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM4IVPROC)(GLuint program, GLint location, GLsizei count, const GLint *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM4UIPROC)(GLuint program, GLint location, GLuint v0, GLuint v1, GLuint v2, GLuint v3); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM4UIVPROC)(GLuint program, GLint location, GLsizei count, const GLuint *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2X3DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2X3FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2X4DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2X4FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3X2DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3X2FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3X4DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3X4FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4X2DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4X2FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4X3DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4X3FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLPROVOKINGVERTEXPROC)(GLenum mode); typedef void(GLAD_API_PTR *PFNGLPUSHATTRIBPROC)(GLbitfield mask); typedef void(GLAD_API_PTR *PFNGLPUSHCLIENTATTRIBPROC)(GLbitfield mask); typedef void(GLAD_API_PTR *PFNGLPUSHDEBUGGROUPPROC)(GLenum source, GLuint id, GLsizei length, const GLchar *message); typedef void(GLAD_API_PTR *PFNGLPUSHMATRIXPROC)(void); typedef void(GLAD_API_PTR *PFNGLPUSHNAMEPROC)(GLuint name); typedef void(GLAD_API_PTR *PFNGLQUERYCOUNTERPROC)(GLuint id, GLenum target); typedef void(GLAD_API_PTR *PFNGLRASTERPOS2DPROC)(GLdouble x, GLdouble y); typedef void(GLAD_API_PTR *PFNGLRASTERPOS2DVPROC)(const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLRASTERPOS2FPROC)(GLfloat x, GLfloat y); typedef void(GLAD_API_PTR *PFNGLRASTERPOS2FVPROC)(const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLRASTERPOS2IPROC)(GLint x, GLint y); typedef void(GLAD_API_PTR *PFNGLRASTERPOS2IVPROC)(const GLint *v); typedef void(GLAD_API_PTR *PFNGLRASTERPOS2SPROC)(GLshort x, GLshort y); typedef void(GLAD_API_PTR *PFNGLRASTERPOS2SVPROC)(const GLshort *v); typedef void(GLAD_API_PTR *PFNGLRASTERPOS3DPROC)(GLdouble x, GLdouble y, GLdouble z); typedef void(GLAD_API_PTR *PFNGLRASTERPOS3DVPROC)(const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLRASTERPOS3FPROC)(GLfloat x, GLfloat y, GLfloat z); typedef void(GLAD_API_PTR *PFNGLRASTERPOS3FVPROC)(const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLRASTERPOS3IPROC)(GLint x, GLint y, GLint z); typedef void(GLAD_API_PTR *PFNGLRASTERPOS3IVPROC)(const GLint *v); typedef void(GLAD_API_PTR *PFNGLRASTERPOS3SPROC)(GLshort x, GLshort y, GLshort z); typedef void(GLAD_API_PTR *PFNGLRASTERPOS3SVPROC)(const GLshort *v); typedef void(GLAD_API_PTR *PFNGLRASTERPOS4DPROC)(GLdouble x, GLdouble y, GLdouble z, GLdouble w); typedef void(GLAD_API_PTR *PFNGLRASTERPOS4DVPROC)(const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLRASTERPOS4FPROC)(GLfloat x, GLfloat y, GLfloat z, GLfloat w); typedef void(GLAD_API_PTR *PFNGLRASTERPOS4FVPROC)(const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLRASTERPOS4IPROC)(GLint x, GLint y, GLint z, GLint w); typedef void(GLAD_API_PTR *PFNGLRASTERPOS4IVPROC)(const GLint *v); typedef void(GLAD_API_PTR *PFNGLRASTERPOS4SPROC)(GLshort x, GLshort y, GLshort z, GLshort w); typedef void(GLAD_API_PTR *PFNGLRASTERPOS4SVPROC)(const GLshort *v); typedef void(GLAD_API_PTR *PFNGLREADBUFFERPROC)(GLenum src); typedef void(GLAD_API_PTR *PFNGLREADPIXELSPROC)(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void *pixels); typedef void(GLAD_API_PTR *PFNGLREADNPIXELSPROC)(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void *data); typedef void(GLAD_API_PTR *PFNGLRECTDPROC)(GLdouble x1, GLdouble y1, GLdouble x2, GLdouble y2); typedef void(GLAD_API_PTR *PFNGLRECTDVPROC)(const GLdouble *v1, const GLdouble *v2); typedef void(GLAD_API_PTR *PFNGLRECTFPROC)(GLfloat x1, GLfloat y1, GLfloat x2, GLfloat y2); typedef void(GLAD_API_PTR *PFNGLRECTFVPROC)(const GLfloat *v1, const GLfloat *v2); typedef void(GLAD_API_PTR *PFNGLRECTIPROC)(GLint x1, GLint y1, GLint x2, GLint y2); typedef void(GLAD_API_PTR *PFNGLRECTIVPROC)(const GLint *v1, const GLint *v2); typedef void(GLAD_API_PTR *PFNGLRECTSPROC)(GLshort x1, GLshort y1, GLshort x2, GLshort y2); typedef void(GLAD_API_PTR *PFNGLRECTSVPROC)(const GLshort *v1, const GLshort *v2); typedef void(GLAD_API_PTR *PFNGLRELEASESHADERCOMPILERPROC)(void); typedef GLint(GLAD_API_PTR *PFNGLRENDERMODEPROC)(GLenum mode); typedef void(GLAD_API_PTR *PFNGLRENDERBUFFERSTORAGEPROC)(GLenum target, GLenum internalformat, GLsizei width, GLsizei height); typedef void(GLAD_API_PTR *PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height); typedef void(GLAD_API_PTR *PFNGLRESUMETRANSFORMFEEDBACKPROC)(void); typedef void(GLAD_API_PTR *PFNGLROTATEDPROC)(GLdouble angle, GLdouble x, GLdouble y, GLdouble z); typedef void(GLAD_API_PTR *PFNGLROTATEFPROC)(GLfloat angle, GLfloat x, GLfloat y, GLfloat z); typedef void(GLAD_API_PTR *PFNGLSAMPLECOVERAGEPROC)(GLfloat value, GLboolean invert); typedef void(GLAD_API_PTR *PFNGLSAMPLEMASKIPROC)(GLuint maskNumber, GLbitfield mask); typedef void(GLAD_API_PTR *PFNGLSAMPLERPARAMETERIIVPROC)(GLuint sampler, GLenum pname, const GLint *param); typedef void(GLAD_API_PTR *PFNGLSAMPLERPARAMETERIUIVPROC)(GLuint sampler, GLenum pname, const GLuint *param); typedef void(GLAD_API_PTR *PFNGLSAMPLERPARAMETERFPROC)(GLuint sampler, GLenum pname, GLfloat param); typedef void(GLAD_API_PTR *PFNGLSAMPLERPARAMETERFVPROC)(GLuint sampler, GLenum pname, const GLfloat *param); typedef void(GLAD_API_PTR *PFNGLSAMPLERPARAMETERIPROC)(GLuint sampler, GLenum pname, GLint param); typedef void(GLAD_API_PTR *PFNGLSAMPLERPARAMETERIVPROC)(GLuint sampler, GLenum pname, const GLint *param); typedef void(GLAD_API_PTR *PFNGLSCALEDPROC)(GLdouble x, GLdouble y, GLdouble z); typedef void(GLAD_API_PTR *PFNGLSCALEFPROC)(GLfloat x, GLfloat y, GLfloat z); typedef void(GLAD_API_PTR *PFNGLSCISSORPROC)(GLint x, GLint y, GLsizei width, GLsizei height); typedef void(GLAD_API_PTR *PFNGLSCISSORARRAYVPROC)(GLuint first, GLsizei count, const GLint *v); typedef void(GLAD_API_PTR *PFNGLSCISSORINDEXEDPROC)(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height); typedef void(GLAD_API_PTR *PFNGLSCISSORINDEXEDVPROC)(GLuint index, const GLint *v); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3BPROC)(GLbyte red, GLbyte green, GLbyte blue); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3BVPROC)(const GLbyte *v); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3DPROC)(GLdouble red, GLdouble green, GLdouble blue); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3DVPROC)(const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3FPROC)(GLfloat red, GLfloat green, GLfloat blue); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3FVPROC)(const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3IPROC)(GLint red, GLint green, GLint blue); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3IVPROC)(const GLint *v); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3SPROC)(GLshort red, GLshort green, GLshort blue); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3SVPROC)(const GLshort *v); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3UBPROC)(GLubyte red, GLubyte green, GLubyte blue); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3UBVPROC)(const GLubyte *v); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3UIPROC)(GLuint red, GLuint green, GLuint blue); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3UIVPROC)(const GLuint *v); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3USPROC)(GLushort red, GLushort green, GLushort blue); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3USVPROC)(const GLushort *v); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLORP3UIPROC)(GLenum type, GLuint color); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLORP3UIVPROC)(GLenum type, const GLuint *color); typedef void(GLAD_API_PTR *PFNGLSECONDARYCOLORPOINTERPROC)(GLint size, GLenum type, GLsizei stride, const void *pointer); typedef void(GLAD_API_PTR *PFNGLSELECTBUFFERPROC)(GLsizei size, GLuint *buffer); typedef void(GLAD_API_PTR *PFNGLSHADEMODELPROC)(GLenum mode); typedef void(GLAD_API_PTR *PFNGLSHADERBINARYPROC)(GLsizei count, const GLuint *shaders, GLenum binaryFormat, const void *binary, GLsizei length); typedef void(GLAD_API_PTR *PFNGLSHADERSOURCEPROC)(GLuint shader, GLsizei count, const GLchar *const *string, const GLint *length); typedef void(GLAD_API_PTR *PFNGLSHADERSTORAGEBLOCKBINDINGPROC)(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding); typedef void(GLAD_API_PTR *PFNGLSPECIALIZESHADERPROC)(GLuint shader, const GLchar *pEntryPoint, GLuint numSpecializationConstants, const GLuint *pConstantIndex, const GLuint *pConstantValue); typedef void(GLAD_API_PTR *PFNGLSTENCILFUNCPROC)(GLenum func, GLint ref, GLuint mask); typedef void(GLAD_API_PTR *PFNGLSTENCILFUNCSEPARATEPROC)(GLenum face, GLenum func, GLint ref, GLuint mask); typedef void(GLAD_API_PTR *PFNGLSTENCILMASKPROC)(GLuint mask); typedef void(GLAD_API_PTR *PFNGLSTENCILMASKSEPARATEPROC)(GLenum face, GLuint mask); typedef void(GLAD_API_PTR *PFNGLSTENCILOPPROC)(GLenum fail, GLenum zfail, GLenum zpass); typedef void(GLAD_API_PTR *PFNGLSTENCILOPSEPARATEPROC)(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass); typedef void(GLAD_API_PTR *PFNGLTEXBUFFERPROC)(GLenum target, GLenum internalformat, GLuint buffer); typedef void(GLAD_API_PTR *PFNGLTEXBUFFERRANGEPROC)(GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size); typedef void(GLAD_API_PTR *PFNGLTEXCOORD1DPROC)(GLdouble s); typedef void(GLAD_API_PTR *PFNGLTEXCOORD1DVPROC)(const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLTEXCOORD1FPROC)(GLfloat s); typedef void(GLAD_API_PTR *PFNGLTEXCOORD1FVPROC)(const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLTEXCOORD1IPROC)(GLint s); typedef void(GLAD_API_PTR *PFNGLTEXCOORD1IVPROC)(const GLint *v); typedef void(GLAD_API_PTR *PFNGLTEXCOORD1SPROC)(GLshort s); typedef void(GLAD_API_PTR *PFNGLTEXCOORD1SVPROC)(const GLshort *v); typedef void(GLAD_API_PTR *PFNGLTEXCOORD2DPROC)(GLdouble s, GLdouble t); typedef void(GLAD_API_PTR *PFNGLTEXCOORD2DVPROC)(const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLTEXCOORD2FPROC)(GLfloat s, GLfloat t); typedef void(GLAD_API_PTR *PFNGLTEXCOORD2FVPROC)(const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLTEXCOORD2IPROC)(GLint s, GLint t); typedef void(GLAD_API_PTR *PFNGLTEXCOORD2IVPROC)(const GLint *v); typedef void(GLAD_API_PTR *PFNGLTEXCOORD2SPROC)(GLshort s, GLshort t); typedef void(GLAD_API_PTR *PFNGLTEXCOORD2SVPROC)(const GLshort *v); typedef void(GLAD_API_PTR *PFNGLTEXCOORD3DPROC)(GLdouble s, GLdouble t, GLdouble r); typedef void(GLAD_API_PTR *PFNGLTEXCOORD3DVPROC)(const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLTEXCOORD3FPROC)(GLfloat s, GLfloat t, GLfloat r); typedef void(GLAD_API_PTR *PFNGLTEXCOORD3FVPROC)(const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLTEXCOORD3IPROC)(GLint s, GLint t, GLint r); typedef void(GLAD_API_PTR *PFNGLTEXCOORD3IVPROC)(const GLint *v); typedef void(GLAD_API_PTR *PFNGLTEXCOORD3SPROC)(GLshort s, GLshort t, GLshort r); typedef void(GLAD_API_PTR *PFNGLTEXCOORD3SVPROC)(const GLshort *v); typedef void(GLAD_API_PTR *PFNGLTEXCOORD4DPROC)(GLdouble s, GLdouble t, GLdouble r, GLdouble q); typedef void(GLAD_API_PTR *PFNGLTEXCOORD4DVPROC)(const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLTEXCOORD4FPROC)(GLfloat s, GLfloat t, GLfloat r, GLfloat q); typedef void(GLAD_API_PTR *PFNGLTEXCOORD4FVPROC)(const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLTEXCOORD4IPROC)(GLint s, GLint t, GLint r, GLint q); typedef void(GLAD_API_PTR *PFNGLTEXCOORD4IVPROC)(const GLint *v); typedef void(GLAD_API_PTR *PFNGLTEXCOORD4SPROC)(GLshort s, GLshort t, GLshort r, GLshort q); typedef void(GLAD_API_PTR *PFNGLTEXCOORD4SVPROC)(const GLshort *v); typedef void(GLAD_API_PTR *PFNGLTEXCOORDP1UIPROC)(GLenum type, GLuint coords); typedef void(GLAD_API_PTR *PFNGLTEXCOORDP1UIVPROC)(GLenum type, const GLuint *coords); typedef void(GLAD_API_PTR *PFNGLTEXCOORDP2UIPROC)(GLenum type, GLuint coords); typedef void(GLAD_API_PTR *PFNGLTEXCOORDP2UIVPROC)(GLenum type, const GLuint *coords); typedef void(GLAD_API_PTR *PFNGLTEXCOORDP3UIPROC)(GLenum type, GLuint coords); typedef void(GLAD_API_PTR *PFNGLTEXCOORDP3UIVPROC)(GLenum type, const GLuint *coords); typedef void(GLAD_API_PTR *PFNGLTEXCOORDP4UIPROC)(GLenum type, GLuint coords); typedef void(GLAD_API_PTR *PFNGLTEXCOORDP4UIVPROC)(GLenum type, const GLuint *coords); typedef void(GLAD_API_PTR *PFNGLTEXCOORDPOINTERPROC)(GLint size, GLenum type, GLsizei stride, const void *pointer); typedef void(GLAD_API_PTR *PFNGLTEXENVFPROC)(GLenum target, GLenum pname, GLfloat param); typedef void(GLAD_API_PTR *PFNGLTEXENVFVPROC)(GLenum target, GLenum pname, const GLfloat *params); typedef void(GLAD_API_PTR *PFNGLTEXENVIPROC)(GLenum target, GLenum pname, GLint param); typedef void(GLAD_API_PTR *PFNGLTEXENVIVPROC)(GLenum target, GLenum pname, const GLint *params); typedef void(GLAD_API_PTR *PFNGLTEXGENDPROC)(GLenum coord, GLenum pname, GLdouble param); typedef void(GLAD_API_PTR *PFNGLTEXGENDVPROC)(GLenum coord, GLenum pname, const GLdouble *params); typedef void(GLAD_API_PTR *PFNGLTEXGENFPROC)(GLenum coord, GLenum pname, GLfloat param); typedef void(GLAD_API_PTR *PFNGLTEXGENFVPROC)(GLenum coord, GLenum pname, const GLfloat *params); typedef void(GLAD_API_PTR *PFNGLTEXGENIPROC)(GLenum coord, GLenum pname, GLint param); typedef void(GLAD_API_PTR *PFNGLTEXGENIVPROC)(GLenum coord, GLenum pname, const GLint *params); typedef void(GLAD_API_PTR *PFNGLTEXIMAGE1DPROC)(GLenum target, GLint level, GLint internalformat, GLsizei width, GLint border, GLenum format, GLenum type, const void *pixels); typedef void(GLAD_API_PTR *PFNGLTEXIMAGE2DPROC)(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void *pixels); typedef void(GLAD_API_PTR *PFNGLTEXIMAGE2DMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLboolean fixedsamplelocations); typedef 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); typedef void(GLAD_API_PTR *PFNGLTEXIMAGE3DMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations); typedef void(GLAD_API_PTR *PFNGLTEXPARAMETERIIVPROC)(GLenum target, GLenum pname, const GLint *params); typedef void(GLAD_API_PTR *PFNGLTEXPARAMETERIUIVPROC)(GLenum target, GLenum pname, const GLuint *params); typedef void(GLAD_API_PTR *PFNGLTEXPARAMETERFPROC)(GLenum target, GLenum pname, GLfloat param); typedef void(GLAD_API_PTR *PFNGLTEXPARAMETERFVPROC)(GLenum target, GLenum pname, const GLfloat *params); typedef void(GLAD_API_PTR *PFNGLTEXPARAMETERIPROC)(GLenum target, GLenum pname, GLint param); typedef void(GLAD_API_PTR *PFNGLTEXPARAMETERIVPROC)(GLenum target, GLenum pname, const GLint *params); typedef void(GLAD_API_PTR *PFNGLTEXSTORAGE1DPROC)(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width); typedef void(GLAD_API_PTR *PFNGLTEXSTORAGE2DPROC)(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height); typedef void(GLAD_API_PTR *PFNGLTEXSTORAGE2DMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLboolean fixedsamplelocations); typedef void(GLAD_API_PTR *PFNGLTEXSTORAGE3DPROC)(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth); typedef void(GLAD_API_PTR *PFNGLTEXSTORAGE3DMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations); typedef void(GLAD_API_PTR *PFNGLTEXSUBIMAGE1DPROC)(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type, const void *pixels); typedef void(GLAD_API_PTR *PFNGLTEXSUBIMAGE2DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void *pixels); typedef 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); typedef void(GLAD_API_PTR *PFNGLTEXTUREBARRIERPROC)(void); typedef void(GLAD_API_PTR *PFNGLTEXTUREBUFFERPROC)(GLuint texture, GLenum internalformat, GLuint buffer); typedef void(GLAD_API_PTR *PFNGLTEXTUREBUFFERRANGEPROC)(GLuint texture, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size); typedef void(GLAD_API_PTR *PFNGLTEXTUREPARAMETERIIVPROC)(GLuint texture, GLenum pname, const GLint *params); typedef void(GLAD_API_PTR *PFNGLTEXTUREPARAMETERIUIVPROC)(GLuint texture, GLenum pname, const GLuint *params); typedef void(GLAD_API_PTR *PFNGLTEXTUREPARAMETERFPROC)(GLuint texture, GLenum pname, GLfloat param); typedef void(GLAD_API_PTR *PFNGLTEXTUREPARAMETERFVPROC)(GLuint texture, GLenum pname, const GLfloat *param); typedef void(GLAD_API_PTR *PFNGLTEXTUREPARAMETERIPROC)(GLuint texture, GLenum pname, GLint param); typedef void(GLAD_API_PTR *PFNGLTEXTUREPARAMETERIVPROC)(GLuint texture, GLenum pname, const GLint *param); typedef void(GLAD_API_PTR *PFNGLTEXTURESTORAGE1DPROC)(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width); typedef void(GLAD_API_PTR *PFNGLTEXTURESTORAGE2DPROC)(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height); typedef void(GLAD_API_PTR *PFNGLTEXTURESTORAGE2DMULTISAMPLEPROC)(GLuint texture, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLboolean fixedsamplelocations); typedef void(GLAD_API_PTR *PFNGLTEXTURESTORAGE3DPROC)(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth); typedef void(GLAD_API_PTR *PFNGLTEXTURESTORAGE3DMULTISAMPLEPROC)(GLuint texture, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations); typedef void(GLAD_API_PTR *PFNGLTEXTURESUBIMAGE1DPROC)(GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type, const void *pixels); typedef void(GLAD_API_PTR *PFNGLTEXTURESUBIMAGE2DPROC)(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void *pixels); typedef 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); typedef void(GLAD_API_PTR *PFNGLTEXTUREVIEWPROC)(GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers); typedef void(GLAD_API_PTR *PFNGLTRANSFORMFEEDBACKBUFFERBASEPROC)(GLuint xfb, GLuint index, GLuint buffer); typedef void(GLAD_API_PTR *PFNGLTRANSFORMFEEDBACKBUFFERRANGEPROC)(GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size); typedef void(GLAD_API_PTR *PFNGLTRANSFORMFEEDBACKVARYINGSPROC)(GLuint program, GLsizei count, const GLchar *const *varyings, GLenum bufferMode); typedef void(GLAD_API_PTR *PFNGLTRANSLATEDPROC)(GLdouble x, GLdouble y, GLdouble z); typedef void(GLAD_API_PTR *PFNGLTRANSLATEFPROC)(GLfloat x, GLfloat y, GLfloat z); typedef void(GLAD_API_PTR *PFNGLUNIFORM1DPROC)(GLint location, GLdouble x); typedef void(GLAD_API_PTR *PFNGLUNIFORM1DVPROC)(GLint location, GLsizei count, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLUNIFORM1FPROC)(GLint location, GLfloat v0); typedef void(GLAD_API_PTR *PFNGLUNIFORM1FVPROC)(GLint location, GLsizei count, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLUNIFORM1IPROC)(GLint location, GLint v0); typedef void(GLAD_API_PTR *PFNGLUNIFORM1IVPROC)(GLint location, GLsizei count, const GLint *value); typedef void(GLAD_API_PTR *PFNGLUNIFORM1UIPROC)(GLint location, GLuint v0); typedef void(GLAD_API_PTR *PFNGLUNIFORM1UIVPROC)(GLint location, GLsizei count, const GLuint *value); typedef void(GLAD_API_PTR *PFNGLUNIFORM2DPROC)(GLint location, GLdouble x, GLdouble y); typedef void(GLAD_API_PTR *PFNGLUNIFORM2DVPROC)(GLint location, GLsizei count, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLUNIFORM2FPROC)(GLint location, GLfloat v0, GLfloat v1); typedef void(GLAD_API_PTR *PFNGLUNIFORM2FVPROC)(GLint location, GLsizei count, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLUNIFORM2IPROC)(GLint location, GLint v0, GLint v1); typedef void(GLAD_API_PTR *PFNGLUNIFORM2IVPROC)(GLint location, GLsizei count, const GLint *value); typedef void(GLAD_API_PTR *PFNGLUNIFORM2UIPROC)(GLint location, GLuint v0, GLuint v1); typedef void(GLAD_API_PTR *PFNGLUNIFORM2UIVPROC)(GLint location, GLsizei count, const GLuint *value); typedef void(GLAD_API_PTR *PFNGLUNIFORM3DPROC)(GLint location, GLdouble x, GLdouble y, GLdouble z); typedef void(GLAD_API_PTR *PFNGLUNIFORM3DVPROC)(GLint location, GLsizei count, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLUNIFORM3FPROC)(GLint location, GLfloat v0, GLfloat v1, GLfloat v2); typedef void(GLAD_API_PTR *PFNGLUNIFORM3FVPROC)(GLint location, GLsizei count, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLUNIFORM3IPROC)(GLint location, GLint v0, GLint v1, GLint v2); typedef void(GLAD_API_PTR *PFNGLUNIFORM3IVPROC)(GLint location, GLsizei count, const GLint *value); typedef void(GLAD_API_PTR *PFNGLUNIFORM3UIPROC)(GLint location, GLuint v0, GLuint v1, GLuint v2); typedef void(GLAD_API_PTR *PFNGLUNIFORM3UIVPROC)(GLint location, GLsizei count, const GLuint *value); typedef void(GLAD_API_PTR *PFNGLUNIFORM4DPROC)(GLint location, GLdouble x, GLdouble y, GLdouble z, GLdouble w); typedef void(GLAD_API_PTR *PFNGLUNIFORM4DVPROC)(GLint location, GLsizei count, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLUNIFORM4FPROC)(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3); typedef void(GLAD_API_PTR *PFNGLUNIFORM4FVPROC)(GLint location, GLsizei count, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLUNIFORM4IPROC)(GLint location, GLint v0, GLint v1, GLint v2, GLint v3); typedef void(GLAD_API_PTR *PFNGLUNIFORM4IVPROC)(GLint location, GLsizei count, const GLint *value); typedef void(GLAD_API_PTR *PFNGLUNIFORM4UIPROC)(GLint location, GLuint v0, GLuint v1, GLuint v2, GLuint v3); typedef void(GLAD_API_PTR *PFNGLUNIFORM4UIVPROC)(GLint location, GLsizei count, const GLuint *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMBLOCKBINDINGPROC)(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding); typedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX2DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX2FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX2X3DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX2X3FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX2X4DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX2X4FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX3DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX3FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX3X2DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX3X2FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX3X4DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX3X4FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX4DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX4FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX4X2DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX4X2FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX4X3DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX4X3FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); typedef void(GLAD_API_PTR *PFNGLUNIFORMSUBROUTINESUIVPROC)(GLenum shadertype, GLsizei count, const GLuint *indices); typedef GLboolean(GLAD_API_PTR *PFNGLUNMAPBUFFERPROC)(GLenum target); typedef GLboolean(GLAD_API_PTR *PFNGLUNMAPNAMEDBUFFERPROC)(GLuint buffer); typedef void(GLAD_API_PTR *PFNGLUSEPROGRAMPROC)(GLuint program); typedef void(GLAD_API_PTR *PFNGLUSEPROGRAMSTAGESPROC)(GLuint pipeline, GLbitfield stages, GLuint program); typedef void(GLAD_API_PTR *PFNGLVALIDATEPROGRAMPROC)(GLuint program); typedef void(GLAD_API_PTR *PFNGLVALIDATEPROGRAMPIPELINEPROC)(GLuint pipeline); typedef void(GLAD_API_PTR *PFNGLVERTEX2DPROC)(GLdouble x, GLdouble y); typedef void(GLAD_API_PTR *PFNGLVERTEX2DVPROC)(const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLVERTEX2FPROC)(GLfloat x, GLfloat y); typedef void(GLAD_API_PTR *PFNGLVERTEX2FVPROC)(const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLVERTEX2IPROC)(GLint x, GLint y); typedef void(GLAD_API_PTR *PFNGLVERTEX2IVPROC)(const GLint *v); typedef void(GLAD_API_PTR *PFNGLVERTEX2SPROC)(GLshort x, GLshort y); typedef void(GLAD_API_PTR *PFNGLVERTEX2SVPROC)(const GLshort *v); typedef void(GLAD_API_PTR *PFNGLVERTEX3DPROC)(GLdouble x, GLdouble y, GLdouble z); typedef void(GLAD_API_PTR *PFNGLVERTEX3DVPROC)(const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLVERTEX3FPROC)(GLfloat x, GLfloat y, GLfloat z); typedef void(GLAD_API_PTR *PFNGLVERTEX3FVPROC)(const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLVERTEX3IPROC)(GLint x, GLint y, GLint z); typedef void(GLAD_API_PTR *PFNGLVERTEX3IVPROC)(const GLint *v); typedef void(GLAD_API_PTR *PFNGLVERTEX3SPROC)(GLshort x, GLshort y, GLshort z); typedef void(GLAD_API_PTR *PFNGLVERTEX3SVPROC)(const GLshort *v); typedef void(GLAD_API_PTR *PFNGLVERTEX4DPROC)(GLdouble x, GLdouble y, GLdouble z, GLdouble w); typedef void(GLAD_API_PTR *PFNGLVERTEX4DVPROC)(const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLVERTEX4FPROC)(GLfloat x, GLfloat y, GLfloat z, GLfloat w); typedef void(GLAD_API_PTR *PFNGLVERTEX4FVPROC)(const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLVERTEX4IPROC)(GLint x, GLint y, GLint z, GLint w); typedef void(GLAD_API_PTR *PFNGLVERTEX4IVPROC)(const GLint *v); typedef void(GLAD_API_PTR *PFNGLVERTEX4SPROC)(GLshort x, GLshort y, GLshort z, GLshort w); typedef void(GLAD_API_PTR *PFNGLVERTEX4SVPROC)(const GLshort *v); typedef void(GLAD_API_PTR *PFNGLVERTEXARRAYATTRIBBINDINGPROC)(GLuint vaobj, GLuint attribindex, GLuint bindingindex); typedef void(GLAD_API_PTR *PFNGLVERTEXARRAYATTRIBFORMATPROC)(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset); typedef void(GLAD_API_PTR *PFNGLVERTEXARRAYATTRIBIFORMATPROC)(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset); typedef void(GLAD_API_PTR *PFNGLVERTEXARRAYATTRIBLFORMATPROC)(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset); typedef void(GLAD_API_PTR *PFNGLVERTEXARRAYBINDINGDIVISORPROC)(GLuint vaobj, GLuint bindingindex, GLuint divisor); typedef void(GLAD_API_PTR *PFNGLVERTEXARRAYELEMENTBUFFERPROC)(GLuint vaobj, GLuint buffer); typedef void(GLAD_API_PTR *PFNGLVERTEXARRAYVERTEXBUFFERPROC)(GLuint vaobj, GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride); typedef void(GLAD_API_PTR *PFNGLVERTEXARRAYVERTEXBUFFERSPROC)(GLuint vaobj, GLuint first, GLsizei count, const GLuint *buffers, const GLintptr *offsets, const GLsizei *strides); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB1DPROC)(GLuint index, GLdouble x); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB1DVPROC)(GLuint index, const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB1FPROC)(GLuint index, GLfloat x); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB1FVPROC)(GLuint index, const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB1SPROC)(GLuint index, GLshort x); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB1SVPROC)(GLuint index, const GLshort *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB2DPROC)(GLuint index, GLdouble x, GLdouble y); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB2DVPROC)(GLuint index, const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB2FPROC)(GLuint index, GLfloat x, GLfloat y); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB2FVPROC)(GLuint index, const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB2SPROC)(GLuint index, GLshort x, GLshort y); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB2SVPROC)(GLuint index, const GLshort *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB3DPROC)(GLuint index, GLdouble x, GLdouble y, GLdouble z); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB3DVPROC)(GLuint index, const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB3FPROC)(GLuint index, GLfloat x, GLfloat y, GLfloat z); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB3FVPROC)(GLuint index, const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB3SPROC)(GLuint index, GLshort x, GLshort y, GLshort z); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB3SVPROC)(GLuint index, const GLshort *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4NBVPROC)(GLuint index, const GLbyte *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4NIVPROC)(GLuint index, const GLint *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4NSVPROC)(GLuint index, const GLshort *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4NUBPROC)(GLuint index, GLubyte x, GLubyte y, GLubyte z, GLubyte w); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4NUBVPROC)(GLuint index, const GLubyte *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4NUIVPROC)(GLuint index, const GLuint *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4NUSVPROC)(GLuint index, const GLushort *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4BVPROC)(GLuint index, const GLbyte *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4DPROC)(GLuint index, GLdouble x, GLdouble y, GLdouble z, GLdouble w); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4DVPROC)(GLuint index, const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4FPROC)(GLuint index, GLfloat x, GLfloat y, GLfloat z, GLfloat w); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4FVPROC)(GLuint index, const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4IVPROC)(GLuint index, const GLint *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4SPROC)(GLuint index, GLshort x, GLshort y, GLshort z, GLshort w); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4SVPROC)(GLuint index, const GLshort *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4UBVPROC)(GLuint index, const GLubyte *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4UIVPROC)(GLuint index, const GLuint *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4USVPROC)(GLuint index, const GLushort *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBBINDINGPROC)(GLuint attribindex, GLuint bindingindex); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBDIVISORPROC)(GLuint index, GLuint divisor); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBFORMATPROC)(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI1IPROC)(GLuint index, GLint x); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI1IVPROC)(GLuint index, const GLint *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI1UIPROC)(GLuint index, GLuint x); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI1UIVPROC)(GLuint index, const GLuint *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI2IPROC)(GLuint index, GLint x, GLint y); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI2IVPROC)(GLuint index, const GLint *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI2UIPROC)(GLuint index, GLuint x, GLuint y); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI2UIVPROC)(GLuint index, const GLuint *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI3IPROC)(GLuint index, GLint x, GLint y, GLint z); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI3IVPROC)(GLuint index, const GLint *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI3UIPROC)(GLuint index, GLuint x, GLuint y, GLuint z); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI3UIVPROC)(GLuint index, const GLuint *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI4BVPROC)(GLuint index, const GLbyte *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI4IPROC)(GLuint index, GLint x, GLint y, GLint z, GLint w); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI4IVPROC)(GLuint index, const GLint *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI4SVPROC)(GLuint index, const GLshort *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI4UBVPROC)(GLuint index, const GLubyte *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI4UIPROC)(GLuint index, GLuint x, GLuint y, GLuint z, GLuint w); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI4UIVPROC)(GLuint index, const GLuint *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI4USVPROC)(GLuint index, const GLushort *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBIFORMATPROC)(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBIPOINTERPROC)(GLuint index, GLint size, GLenum type, GLsizei stride, const void *pointer); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBL1DPROC)(GLuint index, GLdouble x); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBL1DVPROC)(GLuint index, const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBL2DPROC)(GLuint index, GLdouble x, GLdouble y); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBL2DVPROC)(GLuint index, const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBL3DPROC)(GLuint index, GLdouble x, GLdouble y, GLdouble z); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBL3DVPROC)(GLuint index, const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBL4DPROC)(GLuint index, GLdouble x, GLdouble y, GLdouble z, GLdouble w); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBL4DVPROC)(GLuint index, const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBLFORMATPROC)(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBLPOINTERPROC)(GLuint index, GLint size, GLenum type, GLsizei stride, const void *pointer); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBP1UIPROC)(GLuint index, GLenum type, GLboolean normalized, GLuint value); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBP1UIVPROC)(GLuint index, GLenum type, GLboolean normalized, const GLuint *value); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBP2UIPROC)(GLuint index, GLenum type, GLboolean normalized, GLuint value); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBP2UIVPROC)(GLuint index, GLenum type, GLboolean normalized, const GLuint *value); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBP3UIPROC)(GLuint index, GLenum type, GLboolean normalized, GLuint value); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBP3UIVPROC)(GLuint index, GLenum type, GLboolean normalized, const GLuint *value); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBP4UIPROC)(GLuint index, GLenum type, GLboolean normalized, GLuint value); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBP4UIVPROC)(GLuint index, GLenum type, GLboolean normalized, const GLuint *value); typedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBPOINTERPROC)(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *pointer); typedef void(GLAD_API_PTR *PFNGLVERTEXBINDINGDIVISORPROC)(GLuint bindingindex, GLuint divisor); typedef void(GLAD_API_PTR *PFNGLVERTEXP2UIPROC)(GLenum type, GLuint value); typedef void(GLAD_API_PTR *PFNGLVERTEXP2UIVPROC)(GLenum type, const GLuint *value); typedef void(GLAD_API_PTR *PFNGLVERTEXP3UIPROC)(GLenum type, GLuint value); typedef void(GLAD_API_PTR *PFNGLVERTEXP3UIVPROC)(GLenum type, const GLuint *value); typedef void(GLAD_API_PTR *PFNGLVERTEXP4UIPROC)(GLenum type, GLuint value); typedef void(GLAD_API_PTR *PFNGLVERTEXP4UIVPROC)(GLenum type, const GLuint *value); typedef void(GLAD_API_PTR *PFNGLVERTEXPOINTERPROC)(GLint size, GLenum type, GLsizei stride, const void *pointer); typedef void(GLAD_API_PTR *PFNGLVIEWPORTPROC)(GLint x, GLint y, GLsizei width, GLsizei height); typedef void(GLAD_API_PTR *PFNGLVIEWPORTARRAYVPROC)(GLuint first, GLsizei count, const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLVIEWPORTINDEXEDFPROC)(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h); typedef void(GLAD_API_PTR *PFNGLVIEWPORTINDEXEDFVPROC)(GLuint index, const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLWAITSYNCPROC)(GLsync sync, GLbitfield flags, GLuint64 timeout); typedef void(GLAD_API_PTR *PFNGLWINDOWPOS2DPROC)(GLdouble x, GLdouble y); typedef void(GLAD_API_PTR *PFNGLWINDOWPOS2DVPROC)(const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLWINDOWPOS2FPROC)(GLfloat x, GLfloat y); typedef void(GLAD_API_PTR *PFNGLWINDOWPOS2FVPROC)(const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLWINDOWPOS2IPROC)(GLint x, GLint y); typedef void(GLAD_API_PTR *PFNGLWINDOWPOS2IVPROC)(const GLint *v); typedef void(GLAD_API_PTR *PFNGLWINDOWPOS2SPROC)(GLshort x, GLshort y); typedef void(GLAD_API_PTR *PFNGLWINDOWPOS2SVPROC)(const GLshort *v); typedef void(GLAD_API_PTR *PFNGLWINDOWPOS3DPROC)(GLdouble x, GLdouble y, GLdouble z); typedef void(GLAD_API_PTR *PFNGLWINDOWPOS3DVPROC)(const GLdouble *v); typedef void(GLAD_API_PTR *PFNGLWINDOWPOS3FPROC)(GLfloat x, GLfloat y, GLfloat z); typedef void(GLAD_API_PTR *PFNGLWINDOWPOS3FVPROC)(const GLfloat *v); typedef void(GLAD_API_PTR *PFNGLWINDOWPOS3IPROC)(GLint x, GLint y, GLint z); typedef void(GLAD_API_PTR *PFNGLWINDOWPOS3IVPROC)(const GLint *v); typedef void(GLAD_API_PTR *PFNGLWINDOWPOS3SPROC)(GLshort x, GLshort y, GLshort z); typedef void(GLAD_API_PTR *PFNGLWINDOWPOS3SVPROC)(const GLshort *v); typedef struct GladGLContext { void *userptr; int VERSION_1_0; int VERSION_1_1; int VERSION_1_2; int VERSION_1_3; int VERSION_1_4; int VERSION_1_5; int VERSION_2_0; int VERSION_2_1; int VERSION_3_0; int VERSION_3_1; int VERSION_3_2; int VERSION_3_3; int VERSION_4_0; int VERSION_4_1; int VERSION_4_2; int VERSION_4_3; int VERSION_4_4; int VERSION_4_5; int VERSION_4_6; PFNGLEGLIMAGETARGETTEXTURE2DOESPROC EGLImageTargetTexture2DOES; PFNGLACCUMPROC Accum; PFNGLACTIVESHADERPROGRAMPROC ActiveShaderProgram; PFNGLACTIVETEXTUREPROC ActiveTexture; PFNGLALPHAFUNCPROC AlphaFunc; PFNGLARETEXTURESRESIDENTPROC AreTexturesResident; PFNGLARRAYELEMENTPROC ArrayElement; PFNGLATTACHSHADERPROC AttachShader; PFNGLBEGINPROC Begin; PFNGLBEGINCONDITIONALRENDERPROC BeginConditionalRender; PFNGLBEGINQUERYPROC BeginQuery; PFNGLBEGINQUERYINDEXEDPROC BeginQueryIndexed; PFNGLBEGINTRANSFORMFEEDBACKPROC BeginTransformFeedback; PFNGLBINDATTRIBLOCATIONPROC BindAttribLocation; PFNGLBINDBUFFERPROC BindBuffer; PFNGLBINDBUFFERBASEPROC BindBufferBase; PFNGLBINDBUFFERRANGEPROC BindBufferRange; PFNGLBINDBUFFERSBASEPROC BindBuffersBase; PFNGLBINDBUFFERSRANGEPROC BindBuffersRange; PFNGLBINDFRAGDATALOCATIONPROC BindFragDataLocation; PFNGLBINDFRAGDATALOCATIONINDEXEDPROC BindFragDataLocationIndexed; PFNGLBINDFRAMEBUFFERPROC BindFramebuffer; PFNGLBINDIMAGETEXTUREPROC BindImageTexture; PFNGLBINDIMAGETEXTURESPROC BindImageTextures; PFNGLBINDPROGRAMPIPELINEPROC BindProgramPipeline; PFNGLBINDRENDERBUFFERPROC BindRenderbuffer; PFNGLBINDSAMPLERPROC BindSampler; PFNGLBINDSAMPLERSPROC BindSamplers; PFNGLBINDTEXTUREPROC BindTexture; PFNGLBINDTEXTUREUNITPROC BindTextureUnit; PFNGLBINDTEXTURESPROC BindTextures; PFNGLBINDTRANSFORMFEEDBACKPROC BindTransformFeedback; PFNGLBINDVERTEXARRAYPROC BindVertexArray; PFNGLBINDVERTEXBUFFERPROC BindVertexBuffer; PFNGLBINDVERTEXBUFFERSPROC BindVertexBuffers; PFNGLBITMAPPROC Bitmap; PFNGLBLENDCOLORPROC BlendColor; PFNGLBLENDEQUATIONPROC BlendEquation; PFNGLBLENDEQUATIONSEPARATEPROC BlendEquationSeparate; PFNGLBLENDEQUATIONSEPARATEIPROC BlendEquationSeparatei; PFNGLBLENDEQUATIONIPROC BlendEquationi; PFNGLBLENDFUNCPROC BlendFunc; PFNGLBLENDFUNCSEPARATEPROC BlendFuncSeparate; PFNGLBLENDFUNCSEPARATEIPROC BlendFuncSeparatei; PFNGLBLENDFUNCIPROC BlendFunci; PFNGLBLITFRAMEBUFFERPROC BlitFramebuffer; PFNGLBLITNAMEDFRAMEBUFFERPROC BlitNamedFramebuffer; PFNGLBUFFERDATAPROC BufferData; PFNGLBUFFERSTORAGEPROC BufferStorage; PFNGLBUFFERSUBDATAPROC BufferSubData; PFNGLCALLLISTPROC CallList; PFNGLCALLLISTSPROC CallLists; PFNGLCHECKFRAMEBUFFERSTATUSPROC CheckFramebufferStatus; PFNGLCHECKNAMEDFRAMEBUFFERSTATUSPROC CheckNamedFramebufferStatus; PFNGLCLAMPCOLORPROC ClampColor; PFNGLCLEARPROC Clear; PFNGLCLEARACCUMPROC ClearAccum; PFNGLCLEARBUFFERDATAPROC ClearBufferData; PFNGLCLEARBUFFERSUBDATAPROC ClearBufferSubData; PFNGLCLEARBUFFERFIPROC ClearBufferfi; PFNGLCLEARBUFFERFVPROC ClearBufferfv; PFNGLCLEARBUFFERIVPROC ClearBufferiv; PFNGLCLEARBUFFERUIVPROC ClearBufferuiv; PFNGLCLEARCOLORPROC ClearColor; PFNGLCLEARDEPTHPROC ClearDepth; PFNGLCLEARDEPTHFPROC ClearDepthf; PFNGLCLEARINDEXPROC ClearIndex; PFNGLCLEARNAMEDBUFFERDATAPROC ClearNamedBufferData; PFNGLCLEARNAMEDBUFFERSUBDATAPROC ClearNamedBufferSubData; PFNGLCLEARNAMEDFRAMEBUFFERFIPROC ClearNamedFramebufferfi; PFNGLCLEARNAMEDFRAMEBUFFERFVPROC ClearNamedFramebufferfv; PFNGLCLEARNAMEDFRAMEBUFFERIVPROC ClearNamedFramebufferiv; PFNGLCLEARNAMEDFRAMEBUFFERUIVPROC ClearNamedFramebufferuiv; PFNGLCLEARSTENCILPROC ClearStencil; PFNGLCLEARTEXIMAGEPROC ClearTexImage; PFNGLCLEARTEXSUBIMAGEPROC ClearTexSubImage; PFNGLCLIENTACTIVETEXTUREPROC ClientActiveTexture; PFNGLCLIENTWAITSYNCPROC ClientWaitSync; PFNGLCLIPCONTROLPROC ClipControl; PFNGLCLIPPLANEPROC ClipPlane; PFNGLCOLOR3BPROC Color3b; PFNGLCOLOR3BVPROC Color3bv; PFNGLCOLOR3DPROC Color3d; PFNGLCOLOR3DVPROC Color3dv; PFNGLCOLOR3FPROC Color3f; PFNGLCOLOR3FVPROC Color3fv; PFNGLCOLOR3IPROC Color3i; PFNGLCOLOR3IVPROC Color3iv; PFNGLCOLOR3SPROC Color3s; PFNGLCOLOR3SVPROC Color3sv; PFNGLCOLOR3UBPROC Color3ub; PFNGLCOLOR3UBVPROC Color3ubv; PFNGLCOLOR3UIPROC Color3ui; PFNGLCOLOR3UIVPROC Color3uiv; PFNGLCOLOR3USPROC Color3us; PFNGLCOLOR3USVPROC Color3usv; PFNGLCOLOR4BPROC Color4b; PFNGLCOLOR4BVPROC Color4bv; PFNGLCOLOR4DPROC Color4d; PFNGLCOLOR4DVPROC Color4dv; PFNGLCOLOR4FPROC Color4f; PFNGLCOLOR4FVPROC Color4fv; PFNGLCOLOR4IPROC Color4i; PFNGLCOLOR4IVPROC Color4iv; PFNGLCOLOR4SPROC Color4s; PFNGLCOLOR4SVPROC Color4sv; PFNGLCOLOR4UBPROC Color4ub; PFNGLCOLOR4UBVPROC Color4ubv; PFNGLCOLOR4UIPROC Color4ui; PFNGLCOLOR4UIVPROC Color4uiv; PFNGLCOLOR4USPROC Color4us; PFNGLCOLOR4USVPROC Color4usv; PFNGLCOLORMASKPROC ColorMask; PFNGLCOLORMASKIPROC ColorMaski; PFNGLCOLORMATERIALPROC ColorMaterial; PFNGLCOLORP3UIPROC ColorP3ui; PFNGLCOLORP3UIVPROC ColorP3uiv; PFNGLCOLORP4UIPROC ColorP4ui; PFNGLCOLORP4UIVPROC ColorP4uiv; PFNGLCOLORPOINTERPROC ColorPointer; PFNGLCOMPILESHADERPROC CompileShader; PFNGLCOMPRESSEDTEXIMAGE1DPROC CompressedTexImage1D; PFNGLCOMPRESSEDTEXIMAGE2DPROC CompressedTexImage2D; PFNGLCOMPRESSEDTEXIMAGE3DPROC CompressedTexImage3D; PFNGLCOMPRESSEDTEXSUBIMAGE1DPROC CompressedTexSubImage1D; PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC CompressedTexSubImage2D; PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC CompressedTexSubImage3D; PFNGLCOMPRESSEDTEXTURESUBIMAGE1DPROC CompressedTextureSubImage1D; PFNGLCOMPRESSEDTEXTURESUBIMAGE2DPROC CompressedTextureSubImage2D; PFNGLCOMPRESSEDTEXTURESUBIMAGE3DPROC CompressedTextureSubImage3D; PFNGLCOPYBUFFERSUBDATAPROC CopyBufferSubData; PFNGLCOPYIMAGESUBDATAPROC CopyImageSubData; PFNGLCOPYNAMEDBUFFERSUBDATAPROC CopyNamedBufferSubData; PFNGLCOPYPIXELSPROC CopyPixels; PFNGLCOPYTEXIMAGE1DPROC CopyTexImage1D; PFNGLCOPYTEXIMAGE2DPROC CopyTexImage2D; PFNGLCOPYTEXSUBIMAGE1DPROC CopyTexSubImage1D; PFNGLCOPYTEXSUBIMAGE2DPROC CopyTexSubImage2D; PFNGLCOPYTEXSUBIMAGE3DPROC CopyTexSubImage3D; PFNGLCOPYTEXTURESUBIMAGE1DPROC CopyTextureSubImage1D; PFNGLCOPYTEXTURESUBIMAGE2DPROC CopyTextureSubImage2D; PFNGLCOPYTEXTURESUBIMAGE3DPROC CopyTextureSubImage3D; PFNGLCREATEBUFFERSPROC CreateBuffers; PFNGLCREATEFRAMEBUFFERSPROC CreateFramebuffers; PFNGLCREATEPROGRAMPROC CreateProgram; PFNGLCREATEPROGRAMPIPELINESPROC CreateProgramPipelines; PFNGLCREATEQUERIESPROC CreateQueries; PFNGLCREATERENDERBUFFERSPROC CreateRenderbuffers; PFNGLCREATESAMPLERSPROC CreateSamplers; PFNGLCREATESHADERPROC CreateShader; PFNGLCREATESHADERPROGRAMVPROC CreateShaderProgramv; PFNGLCREATETEXTURESPROC CreateTextures; PFNGLCREATETRANSFORMFEEDBACKSPROC CreateTransformFeedbacks; PFNGLCREATEVERTEXARRAYSPROC CreateVertexArrays; PFNGLCULLFACEPROC CullFace; PFNGLDEBUGMESSAGECALLBACKPROC DebugMessageCallback; PFNGLDEBUGMESSAGECONTROLPROC DebugMessageControl; PFNGLDEBUGMESSAGEINSERTPROC DebugMessageInsert; PFNGLDELETEBUFFERSPROC DeleteBuffers; PFNGLDELETEFRAMEBUFFERSPROC DeleteFramebuffers; PFNGLDELETELISTSPROC DeleteLists; PFNGLDELETEPROGRAMPROC DeleteProgram; PFNGLDELETEPROGRAMPIPELINESPROC DeleteProgramPipelines; PFNGLDELETEQUERIESPROC DeleteQueries; PFNGLDELETERENDERBUFFERSPROC DeleteRenderbuffers; PFNGLDELETESAMPLERSPROC DeleteSamplers; PFNGLDELETESHADERPROC DeleteShader; PFNGLDELETESYNCPROC DeleteSync; PFNGLDELETETEXTURESPROC DeleteTextures; PFNGLDELETETRANSFORMFEEDBACKSPROC DeleteTransformFeedbacks; PFNGLDELETEVERTEXARRAYSPROC DeleteVertexArrays; PFNGLDEPTHFUNCPROC DepthFunc; PFNGLDEPTHMASKPROC DepthMask; PFNGLDEPTHRANGEPROC DepthRange; PFNGLDEPTHRANGEARRAYVPROC DepthRangeArrayv; PFNGLDEPTHRANGEINDEXEDPROC DepthRangeIndexed; PFNGLDEPTHRANGEFPROC DepthRangef; PFNGLDETACHSHADERPROC DetachShader; PFNGLDISABLEPROC Disable; PFNGLDISABLECLIENTSTATEPROC DisableClientState; PFNGLDISABLEVERTEXARRAYATTRIBPROC DisableVertexArrayAttrib; PFNGLDISABLEVERTEXATTRIBARRAYPROC DisableVertexAttribArray; PFNGLDISABLEIPROC Disablei; PFNGLDISPATCHCOMPUTEPROC DispatchCompute; PFNGLDISPATCHCOMPUTEINDIRECTPROC DispatchComputeIndirect; PFNGLDRAWARRAYSPROC DrawArrays; PFNGLDRAWARRAYSINDIRECTPROC DrawArraysIndirect; PFNGLDRAWARRAYSINSTANCEDPROC DrawArraysInstanced; PFNGLDRAWARRAYSINSTANCEDBASEINSTANCEPROC DrawArraysInstancedBaseInstance; PFNGLDRAWBUFFERPROC DrawBuffer; PFNGLDRAWBUFFERSPROC DrawBuffers; PFNGLDRAWELEMENTSPROC DrawElements; PFNGLDRAWELEMENTSBASEVERTEXPROC DrawElementsBaseVertex; PFNGLDRAWELEMENTSINDIRECTPROC DrawElementsIndirect; PFNGLDRAWELEMENTSINSTANCEDPROC DrawElementsInstanced; PFNGLDRAWELEMENTSINSTANCEDBASEINSTANCEPROC DrawElementsInstancedBaseInstance; PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXPROC DrawElementsInstancedBaseVertex; PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXBASEINSTANCEPROC DrawElementsInstancedBaseVertexBaseInstance; PFNGLDRAWPIXELSPROC DrawPixels; PFNGLDRAWRANGEELEMENTSPROC DrawRangeElements; PFNGLDRAWRANGEELEMENTSBASEVERTEXPROC DrawRangeElementsBaseVertex; PFNGLDRAWTRANSFORMFEEDBACKPROC DrawTransformFeedback; PFNGLDRAWTRANSFORMFEEDBACKINSTANCEDPROC DrawTransformFeedbackInstanced; PFNGLDRAWTRANSFORMFEEDBACKSTREAMPROC DrawTransformFeedbackStream; PFNGLDRAWTRANSFORMFEEDBACKSTREAMINSTANCEDPROC DrawTransformFeedbackStreamInstanced; PFNGLEDGEFLAGPROC EdgeFlag; PFNGLEDGEFLAGPOINTERPROC EdgeFlagPointer; PFNGLEDGEFLAGVPROC EdgeFlagv; PFNGLENABLEPROC Enable; PFNGLENABLECLIENTSTATEPROC EnableClientState; PFNGLENABLEVERTEXARRAYATTRIBPROC EnableVertexArrayAttrib; PFNGLENABLEVERTEXATTRIBARRAYPROC EnableVertexAttribArray; PFNGLENABLEIPROC Enablei; PFNGLENDPROC End; PFNGLENDCONDITIONALRENDERPROC EndConditionalRender; PFNGLENDLISTPROC EndList; PFNGLENDQUERYPROC EndQuery; PFNGLENDQUERYINDEXEDPROC EndQueryIndexed; PFNGLENDTRANSFORMFEEDBACKPROC EndTransformFeedback; PFNGLEVALCOORD1DPROC EvalCoord1d; PFNGLEVALCOORD1DVPROC EvalCoord1dv; PFNGLEVALCOORD1FPROC EvalCoord1f; PFNGLEVALCOORD1FVPROC EvalCoord1fv; PFNGLEVALCOORD2DPROC EvalCoord2d; PFNGLEVALCOORD2DVPROC EvalCoord2dv; PFNGLEVALCOORD2FPROC EvalCoord2f; PFNGLEVALCOORD2FVPROC EvalCoord2fv; PFNGLEVALMESH1PROC EvalMesh1; PFNGLEVALMESH2PROC EvalMesh2; PFNGLEVALPOINT1PROC EvalPoint1; PFNGLEVALPOINT2PROC EvalPoint2; PFNGLFEEDBACKBUFFERPROC FeedbackBuffer; PFNGLFENCESYNCPROC FenceSync; PFNGLFINISHPROC Finish; PFNGLFLUSHPROC Flush; PFNGLFLUSHMAPPEDBUFFERRANGEPROC FlushMappedBufferRange; PFNGLFLUSHMAPPEDNAMEDBUFFERRANGEPROC FlushMappedNamedBufferRange; PFNGLFOGCOORDPOINTERPROC FogCoordPointer; PFNGLFOGCOORDDPROC FogCoordd; PFNGLFOGCOORDDVPROC FogCoorddv; PFNGLFOGCOORDFPROC FogCoordf; PFNGLFOGCOORDFVPROC FogCoordfv; PFNGLFOGFPROC Fogf; PFNGLFOGFVPROC Fogfv; PFNGLFOGIPROC Fogi; PFNGLFOGIVPROC Fogiv; PFNGLFRAMEBUFFERPARAMETERIPROC FramebufferParameteri; PFNGLFRAMEBUFFERRENDERBUFFERPROC FramebufferRenderbuffer; PFNGLFRAMEBUFFERTEXTUREPROC FramebufferTexture; PFNGLFRAMEBUFFERTEXTURE1DPROC FramebufferTexture1D; PFNGLFRAMEBUFFERTEXTURE2DPROC FramebufferTexture2D; PFNGLFRAMEBUFFERTEXTURE3DPROC FramebufferTexture3D; PFNGLFRAMEBUFFERTEXTURELAYERPROC FramebufferTextureLayer; PFNGLFRONTFACEPROC FrontFace; PFNGLFRUSTUMPROC Frustum; PFNGLGENBUFFERSPROC GenBuffers; PFNGLGENFRAMEBUFFERSPROC GenFramebuffers; PFNGLGENLISTSPROC GenLists; PFNGLGENPROGRAMPIPELINESPROC GenProgramPipelines; PFNGLGENQUERIESPROC GenQueries; PFNGLGENRENDERBUFFERSPROC GenRenderbuffers; PFNGLGENSAMPLERSPROC GenSamplers; PFNGLGENTEXTURESPROC GenTextures; PFNGLGENTRANSFORMFEEDBACKSPROC GenTransformFeedbacks; PFNGLGENVERTEXARRAYSPROC GenVertexArrays; PFNGLGENERATEMIPMAPPROC GenerateMipmap; PFNGLGENERATETEXTUREMIPMAPPROC GenerateTextureMipmap; PFNGLGETACTIVEATOMICCOUNTERBUFFERIVPROC GetActiveAtomicCounterBufferiv; PFNGLGETACTIVEATTRIBPROC GetActiveAttrib; PFNGLGETACTIVESUBROUTINENAMEPROC GetActiveSubroutineName; PFNGLGETACTIVESUBROUTINEUNIFORMNAMEPROC GetActiveSubroutineUniformName; PFNGLGETACTIVESUBROUTINEUNIFORMIVPROC GetActiveSubroutineUniformiv; PFNGLGETACTIVEUNIFORMPROC GetActiveUniform; PFNGLGETACTIVEUNIFORMBLOCKNAMEPROC GetActiveUniformBlockName; PFNGLGETACTIVEUNIFORMBLOCKIVPROC GetActiveUniformBlockiv; PFNGLGETACTIVEUNIFORMNAMEPROC GetActiveUniformName; PFNGLGETACTIVEUNIFORMSIVPROC GetActiveUniformsiv; PFNGLGETATTACHEDSHADERSPROC GetAttachedShaders; PFNGLGETATTRIBLOCATIONPROC GetAttribLocation; PFNGLGETBOOLEANI_VPROC GetBooleani_v; PFNGLGETBOOLEANVPROC GetBooleanv; PFNGLGETBUFFERPARAMETERI64VPROC GetBufferParameteri64v; PFNGLGETBUFFERPARAMETERIVPROC GetBufferParameteriv; PFNGLGETBUFFERPOINTERVPROC GetBufferPointerv; PFNGLGETBUFFERSUBDATAPROC GetBufferSubData; PFNGLGETCLIPPLANEPROC GetClipPlane; PFNGLGETCOMPRESSEDTEXIMAGEPROC GetCompressedTexImage; PFNGLGETCOMPRESSEDTEXTUREIMAGEPROC GetCompressedTextureImage; PFNGLGETCOMPRESSEDTEXTURESUBIMAGEPROC GetCompressedTextureSubImage; PFNGLGETDEBUGMESSAGELOGPROC GetDebugMessageLog; PFNGLGETDOUBLEI_VPROC GetDoublei_v; PFNGLGETDOUBLEVPROC GetDoublev; PFNGLGETERRORPROC GetError; PFNGLGETFLOATI_VPROC GetFloati_v; PFNGLGETFLOATVPROC GetFloatv; PFNGLGETFRAGDATAINDEXPROC GetFragDataIndex; PFNGLGETFRAGDATALOCATIONPROC GetFragDataLocation; PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC GetFramebufferAttachmentParameteriv; PFNGLGETFRAMEBUFFERPARAMETERIVPROC GetFramebufferParameteriv; PFNGLGETGRAPHICSRESETSTATUSPROC GetGraphicsResetStatus; PFNGLGETINTEGER64I_VPROC GetInteger64i_v; PFNGLGETINTEGER64VPROC GetInteger64v; PFNGLGETINTEGERI_VPROC GetIntegeri_v; PFNGLGETINTEGERVPROC GetIntegerv; PFNGLGETINTERNALFORMATI64VPROC GetInternalformati64v; PFNGLGETINTERNALFORMATIVPROC GetInternalformativ; PFNGLGETLIGHTFVPROC GetLightfv; PFNGLGETLIGHTIVPROC GetLightiv; PFNGLGETMAPDVPROC GetMapdv; PFNGLGETMAPFVPROC GetMapfv; PFNGLGETMAPIVPROC GetMapiv; PFNGLGETMATERIALFVPROC GetMaterialfv; PFNGLGETMATERIALIVPROC GetMaterialiv; PFNGLGETMULTISAMPLEFVPROC GetMultisamplefv; PFNGLGETNAMEDBUFFERPARAMETERI64VPROC GetNamedBufferParameteri64v; PFNGLGETNAMEDBUFFERPARAMETERIVPROC GetNamedBufferParameteriv; PFNGLGETNAMEDBUFFERPOINTERVPROC GetNamedBufferPointerv; PFNGLGETNAMEDBUFFERSUBDATAPROC GetNamedBufferSubData; PFNGLGETNAMEDFRAMEBUFFERATTACHMENTPARAMETERIVPROC GetNamedFramebufferAttachmentParameteriv; PFNGLGETNAMEDFRAMEBUFFERPARAMETERIVPROC GetNamedFramebufferParameteriv; PFNGLGETNAMEDRENDERBUFFERPARAMETERIVPROC GetNamedRenderbufferParameteriv; PFNGLGETOBJECTLABELPROC GetObjectLabel; PFNGLGETOBJECTPTRLABELPROC GetObjectPtrLabel; PFNGLGETPIXELMAPFVPROC GetPixelMapfv; PFNGLGETPIXELMAPUIVPROC GetPixelMapuiv; PFNGLGETPIXELMAPUSVPROC GetPixelMapusv; PFNGLGETPOINTERVPROC GetPointerv; PFNGLGETPOLYGONSTIPPLEPROC GetPolygonStipple; PFNGLGETPROGRAMBINARYPROC GetProgramBinary; PFNGLGETPROGRAMINFOLOGPROC GetProgramInfoLog; PFNGLGETPROGRAMINTERFACEIVPROC GetProgramInterfaceiv; PFNGLGETPROGRAMPIPELINEINFOLOGPROC GetProgramPipelineInfoLog; PFNGLGETPROGRAMPIPELINEIVPROC GetProgramPipelineiv; PFNGLGETPROGRAMRESOURCEINDEXPROC GetProgramResourceIndex; PFNGLGETPROGRAMRESOURCELOCATIONPROC GetProgramResourceLocation; PFNGLGETPROGRAMRESOURCELOCATIONINDEXPROC GetProgramResourceLocationIndex; PFNGLGETPROGRAMRESOURCENAMEPROC GetProgramResourceName; PFNGLGETPROGRAMRESOURCEIVPROC GetProgramResourceiv; PFNGLGETPROGRAMSTAGEIVPROC GetProgramStageiv; PFNGLGETPROGRAMIVPROC GetProgramiv; PFNGLGETQUERYBUFFEROBJECTI64VPROC GetQueryBufferObjecti64v; PFNGLGETQUERYBUFFEROBJECTIVPROC GetQueryBufferObjectiv; PFNGLGETQUERYBUFFEROBJECTUI64VPROC GetQueryBufferObjectui64v; PFNGLGETQUERYBUFFEROBJECTUIVPROC GetQueryBufferObjectuiv; PFNGLGETQUERYINDEXEDIVPROC GetQueryIndexediv; PFNGLGETQUERYOBJECTI64VPROC GetQueryObjecti64v; PFNGLGETQUERYOBJECTIVPROC GetQueryObjectiv; PFNGLGETQUERYOBJECTUI64VPROC GetQueryObjectui64v; PFNGLGETQUERYOBJECTUIVPROC GetQueryObjectuiv; PFNGLGETQUERYIVPROC GetQueryiv; PFNGLGETRENDERBUFFERPARAMETERIVPROC GetRenderbufferParameteriv; PFNGLGETSAMPLERPARAMETERIIVPROC GetSamplerParameterIiv; PFNGLGETSAMPLERPARAMETERIUIVPROC GetSamplerParameterIuiv; PFNGLGETSAMPLERPARAMETERFVPROC GetSamplerParameterfv; PFNGLGETSAMPLERPARAMETERIVPROC GetSamplerParameteriv; PFNGLGETSHADERINFOLOGPROC GetShaderInfoLog; PFNGLGETSHADERPRECISIONFORMATPROC GetShaderPrecisionFormat; PFNGLGETSHADERSOURCEPROC GetShaderSource; PFNGLGETSHADERIVPROC GetShaderiv; PFNGLGETSTRINGPROC GetString; PFNGLGETSTRINGIPROC GetStringi; PFNGLGETSUBROUTINEINDEXPROC GetSubroutineIndex; PFNGLGETSUBROUTINEUNIFORMLOCATIONPROC GetSubroutineUniformLocation; PFNGLGETSYNCIVPROC GetSynciv; PFNGLGETTEXENVFVPROC GetTexEnvfv; PFNGLGETTEXENVIVPROC GetTexEnviv; PFNGLGETTEXGENDVPROC GetTexGendv; PFNGLGETTEXGENFVPROC GetTexGenfv; PFNGLGETTEXGENIVPROC GetTexGeniv; PFNGLGETTEXIMAGEPROC GetTexImage; PFNGLGETTEXLEVELPARAMETERFVPROC GetTexLevelParameterfv; PFNGLGETTEXLEVELPARAMETERIVPROC GetTexLevelParameteriv; PFNGLGETTEXPARAMETERIIVPROC GetTexParameterIiv; PFNGLGETTEXPARAMETERIUIVPROC GetTexParameterIuiv; PFNGLGETTEXPARAMETERFVPROC GetTexParameterfv; PFNGLGETTEXPARAMETERIVPROC GetTexParameteriv; PFNGLGETTEXTUREIMAGEPROC GetTextureImage; PFNGLGETTEXTURELEVELPARAMETERFVPROC GetTextureLevelParameterfv; PFNGLGETTEXTURELEVELPARAMETERIVPROC GetTextureLevelParameteriv; PFNGLGETTEXTUREPARAMETERIIVPROC GetTextureParameterIiv; PFNGLGETTEXTUREPARAMETERIUIVPROC GetTextureParameterIuiv; PFNGLGETTEXTUREPARAMETERFVPROC GetTextureParameterfv; PFNGLGETTEXTUREPARAMETERIVPROC GetTextureParameteriv; PFNGLGETTEXTURESUBIMAGEPROC GetTextureSubImage; PFNGLGETTRANSFORMFEEDBACKVARYINGPROC GetTransformFeedbackVarying; PFNGLGETTRANSFORMFEEDBACKI64_VPROC GetTransformFeedbacki64_v; PFNGLGETTRANSFORMFEEDBACKI_VPROC GetTransformFeedbacki_v; PFNGLGETTRANSFORMFEEDBACKIVPROC GetTransformFeedbackiv; PFNGLGETUNIFORMBLOCKINDEXPROC GetUniformBlockIndex; PFNGLGETUNIFORMINDICESPROC GetUniformIndices; PFNGLGETUNIFORMLOCATIONPROC GetUniformLocation; PFNGLGETUNIFORMSUBROUTINEUIVPROC GetUniformSubroutineuiv; PFNGLGETUNIFORMDVPROC GetUniformdv; PFNGLGETUNIFORMFVPROC GetUniformfv; PFNGLGETUNIFORMIVPROC GetUniformiv; PFNGLGETUNIFORMUIVPROC GetUniformuiv; PFNGLGETVERTEXARRAYINDEXED64IVPROC GetVertexArrayIndexed64iv; PFNGLGETVERTEXARRAYINDEXEDIVPROC GetVertexArrayIndexediv; PFNGLGETVERTEXARRAYIVPROC GetVertexArrayiv; PFNGLGETVERTEXATTRIBIIVPROC GetVertexAttribIiv; PFNGLGETVERTEXATTRIBIUIVPROC GetVertexAttribIuiv; PFNGLGETVERTEXATTRIBLDVPROC GetVertexAttribLdv; PFNGLGETVERTEXATTRIBPOINTERVPROC GetVertexAttribPointerv; PFNGLGETVERTEXATTRIBDVPROC GetVertexAttribdv; PFNGLGETVERTEXATTRIBFVPROC GetVertexAttribfv; PFNGLGETVERTEXATTRIBIVPROC GetVertexAttribiv; PFNGLGETNCOLORTABLEPROC GetnColorTable; PFNGLGETNCOMPRESSEDTEXIMAGEPROC GetnCompressedTexImage; PFNGLGETNCONVOLUTIONFILTERPROC GetnConvolutionFilter; PFNGLGETNHISTOGRAMPROC GetnHistogram; PFNGLGETNMAPDVPROC GetnMapdv; PFNGLGETNMAPFVPROC GetnMapfv; PFNGLGETNMAPIVPROC GetnMapiv; PFNGLGETNMINMAXPROC GetnMinmax; PFNGLGETNPIXELMAPFVPROC GetnPixelMapfv; PFNGLGETNPIXELMAPUIVPROC GetnPixelMapuiv; PFNGLGETNPIXELMAPUSVPROC GetnPixelMapusv; PFNGLGETNPOLYGONSTIPPLEPROC GetnPolygonStipple; PFNGLGETNSEPARABLEFILTERPROC GetnSeparableFilter; PFNGLGETNTEXIMAGEPROC GetnTexImage; PFNGLGETNUNIFORMDVPROC GetnUniformdv; PFNGLGETNUNIFORMFVPROC GetnUniformfv; PFNGLGETNUNIFORMIVPROC GetnUniformiv; PFNGLGETNUNIFORMUIVPROC GetnUniformuiv; PFNGLHINTPROC Hint; PFNGLINDEXMASKPROC IndexMask; PFNGLINDEXPOINTERPROC IndexPointer; PFNGLINDEXDPROC Indexd; PFNGLINDEXDVPROC Indexdv; PFNGLINDEXFPROC Indexf; PFNGLINDEXFVPROC Indexfv; PFNGLINDEXIPROC Indexi; PFNGLINDEXIVPROC Indexiv; PFNGLINDEXSPROC Indexs; PFNGLINDEXSVPROC Indexsv; PFNGLINDEXUBPROC Indexub; PFNGLINDEXUBVPROC Indexubv; PFNGLINITNAMESPROC InitNames; PFNGLINTERLEAVEDARRAYSPROC InterleavedArrays; PFNGLINVALIDATEBUFFERDATAPROC InvalidateBufferData; PFNGLINVALIDATEBUFFERSUBDATAPROC InvalidateBufferSubData; PFNGLINVALIDATEFRAMEBUFFERPROC InvalidateFramebuffer; PFNGLINVALIDATENAMEDFRAMEBUFFERDATAPROC InvalidateNamedFramebufferData; PFNGLINVALIDATENAMEDFRAMEBUFFERSUBDATAPROC InvalidateNamedFramebufferSubData; PFNGLINVALIDATESUBFRAMEBUFFERPROC InvalidateSubFramebuffer; PFNGLINVALIDATETEXIMAGEPROC InvalidateTexImage; PFNGLINVALIDATETEXSUBIMAGEPROC InvalidateTexSubImage; PFNGLISBUFFERPROC IsBuffer; PFNGLISENABLEDPROC IsEnabled; PFNGLISENABLEDIPROC IsEnabledi; PFNGLISFRAMEBUFFERPROC IsFramebuffer; PFNGLISLISTPROC IsList; PFNGLISPROGRAMPROC IsProgram; PFNGLISPROGRAMPIPELINEPROC IsProgramPipeline; PFNGLISQUERYPROC IsQuery; PFNGLISRENDERBUFFERPROC IsRenderbuffer; PFNGLISSAMPLERPROC IsSampler; PFNGLISSHADERPROC IsShader; PFNGLISSYNCPROC IsSync; PFNGLISTEXTUREPROC IsTexture; PFNGLISTRANSFORMFEEDBACKPROC IsTransformFeedback; PFNGLISVERTEXARRAYPROC IsVertexArray; PFNGLLIGHTMODELFPROC LightModelf; PFNGLLIGHTMODELFVPROC LightModelfv; PFNGLLIGHTMODELIPROC LightModeli; PFNGLLIGHTMODELIVPROC LightModeliv; PFNGLLIGHTFPROC Lightf; PFNGLLIGHTFVPROC Lightfv; PFNGLLIGHTIPROC Lighti; PFNGLLIGHTIVPROC Lightiv; PFNGLLINESTIPPLEPROC LineStipple; PFNGLLINEWIDTHPROC LineWidth; PFNGLLINKPROGRAMPROC LinkProgram; PFNGLLISTBASEPROC ListBase; PFNGLLOADIDENTITYPROC LoadIdentity; PFNGLLOADMATRIXDPROC LoadMatrixd; PFNGLLOADMATRIXFPROC LoadMatrixf; PFNGLLOADNAMEPROC LoadName; PFNGLLOADTRANSPOSEMATRIXDPROC LoadTransposeMatrixd; PFNGLLOADTRANSPOSEMATRIXFPROC LoadTransposeMatrixf; PFNGLLOGICOPPROC LogicOp; PFNGLMAP1DPROC Map1d; PFNGLMAP1FPROC Map1f; PFNGLMAP2DPROC Map2d; PFNGLMAP2FPROC Map2f; PFNGLMAPBUFFERPROC MapBuffer; PFNGLMAPBUFFERRANGEPROC MapBufferRange; PFNGLMAPGRID1DPROC MapGrid1d; PFNGLMAPGRID1FPROC MapGrid1f; PFNGLMAPGRID2DPROC MapGrid2d; PFNGLMAPGRID2FPROC MapGrid2f; PFNGLMAPNAMEDBUFFERPROC MapNamedBuffer; PFNGLMAPNAMEDBUFFERRANGEPROC MapNamedBufferRange; PFNGLMATERIALFPROC Materialf; PFNGLMATERIALFVPROC Materialfv; PFNGLMATERIALIPROC Materiali; PFNGLMATERIALIVPROC Materialiv; PFNGLMATRIXMODEPROC MatrixMode; PFNGLMEMORYBARRIERPROC MemoryBarrier; PFNGLMEMORYBARRIERBYREGIONPROC MemoryBarrierByRegion; PFNGLMINSAMPLESHADINGPROC MinSampleShading; PFNGLMULTMATRIXDPROC MultMatrixd; PFNGLMULTMATRIXFPROC MultMatrixf; PFNGLMULTTRANSPOSEMATRIXDPROC MultTransposeMatrixd; PFNGLMULTTRANSPOSEMATRIXFPROC MultTransposeMatrixf; PFNGLMULTIDRAWARRAYSPROC MultiDrawArrays; PFNGLMULTIDRAWARRAYSINDIRECTPROC MultiDrawArraysIndirect; PFNGLMULTIDRAWARRAYSINDIRECTCOUNTPROC MultiDrawArraysIndirectCount; PFNGLMULTIDRAWELEMENTSPROC MultiDrawElements; PFNGLMULTIDRAWELEMENTSBASEVERTEXPROC MultiDrawElementsBaseVertex; PFNGLMULTIDRAWELEMENTSINDIRECTPROC MultiDrawElementsIndirect; PFNGLMULTIDRAWELEMENTSINDIRECTCOUNTPROC MultiDrawElementsIndirectCount; PFNGLMULTITEXCOORD1DPROC MultiTexCoord1d; PFNGLMULTITEXCOORD1DVPROC MultiTexCoord1dv; PFNGLMULTITEXCOORD1FPROC MultiTexCoord1f; PFNGLMULTITEXCOORD1FVPROC MultiTexCoord1fv; PFNGLMULTITEXCOORD1IPROC MultiTexCoord1i; PFNGLMULTITEXCOORD1IVPROC MultiTexCoord1iv; PFNGLMULTITEXCOORD1SPROC MultiTexCoord1s; PFNGLMULTITEXCOORD1SVPROC MultiTexCoord1sv; PFNGLMULTITEXCOORD2DPROC MultiTexCoord2d; PFNGLMULTITEXCOORD2DVPROC MultiTexCoord2dv; PFNGLMULTITEXCOORD2FPROC MultiTexCoord2f; PFNGLMULTITEXCOORD2FVPROC MultiTexCoord2fv; PFNGLMULTITEXCOORD2IPROC MultiTexCoord2i; PFNGLMULTITEXCOORD2IVPROC MultiTexCoord2iv; PFNGLMULTITEXCOORD2SPROC MultiTexCoord2s; PFNGLMULTITEXCOORD2SVPROC MultiTexCoord2sv; PFNGLMULTITEXCOORD3DPROC MultiTexCoord3d; PFNGLMULTITEXCOORD3DVPROC MultiTexCoord3dv; PFNGLMULTITEXCOORD3FPROC MultiTexCoord3f; PFNGLMULTITEXCOORD3FVPROC MultiTexCoord3fv; PFNGLMULTITEXCOORD3IPROC MultiTexCoord3i; PFNGLMULTITEXCOORD3IVPROC MultiTexCoord3iv; PFNGLMULTITEXCOORD3SPROC MultiTexCoord3s; PFNGLMULTITEXCOORD3SVPROC MultiTexCoord3sv; PFNGLMULTITEXCOORD4DPROC MultiTexCoord4d; PFNGLMULTITEXCOORD4DVPROC MultiTexCoord4dv; PFNGLMULTITEXCOORD4FPROC MultiTexCoord4f; PFNGLMULTITEXCOORD4FVPROC MultiTexCoord4fv; PFNGLMULTITEXCOORD4IPROC MultiTexCoord4i; PFNGLMULTITEXCOORD4IVPROC MultiTexCoord4iv; PFNGLMULTITEXCOORD4SPROC MultiTexCoord4s; PFNGLMULTITEXCOORD4SVPROC MultiTexCoord4sv; PFNGLMULTITEXCOORDP1UIPROC MultiTexCoordP1ui; PFNGLMULTITEXCOORDP1UIVPROC MultiTexCoordP1uiv; PFNGLMULTITEXCOORDP2UIPROC MultiTexCoordP2ui; PFNGLMULTITEXCOORDP2UIVPROC MultiTexCoordP2uiv; PFNGLMULTITEXCOORDP3UIPROC MultiTexCoordP3ui; PFNGLMULTITEXCOORDP3UIVPROC MultiTexCoordP3uiv; PFNGLMULTITEXCOORDP4UIPROC MultiTexCoordP4ui; PFNGLMULTITEXCOORDP4UIVPROC MultiTexCoordP4uiv; PFNGLNAMEDBUFFERDATAPROC NamedBufferData; PFNGLNAMEDBUFFERSTORAGEPROC NamedBufferStorage; PFNGLNAMEDBUFFERSUBDATAPROC NamedBufferSubData; PFNGLNAMEDFRAMEBUFFERDRAWBUFFERPROC NamedFramebufferDrawBuffer; PFNGLNAMEDFRAMEBUFFERDRAWBUFFERSPROC NamedFramebufferDrawBuffers; PFNGLNAMEDFRAMEBUFFERPARAMETERIPROC NamedFramebufferParameteri; PFNGLNAMEDFRAMEBUFFERREADBUFFERPROC NamedFramebufferReadBuffer; PFNGLNAMEDFRAMEBUFFERRENDERBUFFERPROC NamedFramebufferRenderbuffer; PFNGLNAMEDFRAMEBUFFERTEXTUREPROC NamedFramebufferTexture; PFNGLNAMEDFRAMEBUFFERTEXTURELAYERPROC NamedFramebufferTextureLayer; PFNGLNAMEDRENDERBUFFERSTORAGEPROC NamedRenderbufferStorage; PFNGLNAMEDRENDERBUFFERSTORAGEMULTISAMPLEPROC NamedRenderbufferStorageMultisample; PFNGLNEWLISTPROC NewList; PFNGLNORMAL3BPROC Normal3b; PFNGLNORMAL3BVPROC Normal3bv; PFNGLNORMAL3DPROC Normal3d; PFNGLNORMAL3DVPROC Normal3dv; PFNGLNORMAL3FPROC Normal3f; PFNGLNORMAL3FVPROC Normal3fv; PFNGLNORMAL3IPROC Normal3i; PFNGLNORMAL3IVPROC Normal3iv; PFNGLNORMAL3SPROC Normal3s; PFNGLNORMAL3SVPROC Normal3sv; PFNGLNORMALP3UIPROC NormalP3ui; PFNGLNORMALP3UIVPROC NormalP3uiv; PFNGLNORMALPOINTERPROC NormalPointer; PFNGLOBJECTLABELPROC ObjectLabel; PFNGLOBJECTPTRLABELPROC ObjectPtrLabel; PFNGLORTHOPROC Ortho; PFNGLPASSTHROUGHPROC PassThrough; PFNGLPATCHPARAMETERFVPROC PatchParameterfv; PFNGLPATCHPARAMETERIPROC PatchParameteri; PFNGLPAUSETRANSFORMFEEDBACKPROC PauseTransformFeedback; PFNGLPIXELMAPFVPROC PixelMapfv; PFNGLPIXELMAPUIVPROC PixelMapuiv; PFNGLPIXELMAPUSVPROC PixelMapusv; PFNGLPIXELSTOREFPROC PixelStoref; PFNGLPIXELSTOREIPROC PixelStorei; PFNGLPIXELTRANSFERFPROC PixelTransferf; PFNGLPIXELTRANSFERIPROC PixelTransferi; PFNGLPIXELZOOMPROC PixelZoom; PFNGLPOINTPARAMETERFPROC PointParameterf; PFNGLPOINTPARAMETERFVPROC PointParameterfv; PFNGLPOINTPARAMETERIPROC PointParameteri; PFNGLPOINTPARAMETERIVPROC PointParameteriv; PFNGLPOINTSIZEPROC PointSize; PFNGLPOLYGONMODEPROC PolygonMode; PFNGLPOLYGONOFFSETPROC PolygonOffset; PFNGLPOLYGONOFFSETCLAMPPROC PolygonOffsetClamp; PFNGLPOLYGONSTIPPLEPROC PolygonStipple; PFNGLPOPATTRIBPROC PopAttrib; PFNGLPOPCLIENTATTRIBPROC PopClientAttrib; PFNGLPOPDEBUGGROUPPROC PopDebugGroup; PFNGLPOPMATRIXPROC PopMatrix; PFNGLPOPNAMEPROC PopName; PFNGLPRIMITIVERESTARTINDEXPROC PrimitiveRestartIndex; PFNGLPRIORITIZETEXTURESPROC PrioritizeTextures; PFNGLPROGRAMBINARYPROC ProgramBinary; PFNGLPROGRAMPARAMETERIPROC ProgramParameteri; PFNGLPROGRAMUNIFORM1DPROC ProgramUniform1d; PFNGLPROGRAMUNIFORM1DVPROC ProgramUniform1dv; PFNGLPROGRAMUNIFORM1FPROC ProgramUniform1f; PFNGLPROGRAMUNIFORM1FVPROC ProgramUniform1fv; PFNGLPROGRAMUNIFORM1IPROC ProgramUniform1i; PFNGLPROGRAMUNIFORM1IVPROC ProgramUniform1iv; PFNGLPROGRAMUNIFORM1UIPROC ProgramUniform1ui; PFNGLPROGRAMUNIFORM1UIVPROC ProgramUniform1uiv; PFNGLPROGRAMUNIFORM2DPROC ProgramUniform2d; PFNGLPROGRAMUNIFORM2DVPROC ProgramUniform2dv; PFNGLPROGRAMUNIFORM2FPROC ProgramUniform2f; PFNGLPROGRAMUNIFORM2FVPROC ProgramUniform2fv; PFNGLPROGRAMUNIFORM2IPROC ProgramUniform2i; PFNGLPROGRAMUNIFORM2IVPROC ProgramUniform2iv; PFNGLPROGRAMUNIFORM2UIPROC ProgramUniform2ui; PFNGLPROGRAMUNIFORM2UIVPROC ProgramUniform2uiv; PFNGLPROGRAMUNIFORM3DPROC ProgramUniform3d; PFNGLPROGRAMUNIFORM3DVPROC ProgramUniform3dv; PFNGLPROGRAMUNIFORM3FPROC ProgramUniform3f; PFNGLPROGRAMUNIFORM3FVPROC ProgramUniform3fv; PFNGLPROGRAMUNIFORM3IPROC ProgramUniform3i; PFNGLPROGRAMUNIFORM3IVPROC ProgramUniform3iv; PFNGLPROGRAMUNIFORM3UIPROC ProgramUniform3ui; PFNGLPROGRAMUNIFORM3UIVPROC ProgramUniform3uiv; PFNGLPROGRAMUNIFORM4DPROC ProgramUniform4d; PFNGLPROGRAMUNIFORM4DVPROC ProgramUniform4dv; PFNGLPROGRAMUNIFORM4FPROC ProgramUniform4f; PFNGLPROGRAMUNIFORM4FVPROC ProgramUniform4fv; PFNGLPROGRAMUNIFORM4IPROC ProgramUniform4i; PFNGLPROGRAMUNIFORM4IVPROC ProgramUniform4iv; PFNGLPROGRAMUNIFORM4UIPROC ProgramUniform4ui; PFNGLPROGRAMUNIFORM4UIVPROC ProgramUniform4uiv; PFNGLPROGRAMUNIFORMMATRIX2DVPROC ProgramUniformMatrix2dv; PFNGLPROGRAMUNIFORMMATRIX2FVPROC ProgramUniformMatrix2fv; PFNGLPROGRAMUNIFORMMATRIX2X3DVPROC ProgramUniformMatrix2x3dv; PFNGLPROGRAMUNIFORMMATRIX2X3FVPROC ProgramUniformMatrix2x3fv; PFNGLPROGRAMUNIFORMMATRIX2X4DVPROC ProgramUniformMatrix2x4dv; PFNGLPROGRAMUNIFORMMATRIX2X4FVPROC ProgramUniformMatrix2x4fv; PFNGLPROGRAMUNIFORMMATRIX3DVPROC ProgramUniformMatrix3dv; PFNGLPROGRAMUNIFORMMATRIX3FVPROC ProgramUniformMatrix3fv; PFNGLPROGRAMUNIFORMMATRIX3X2DVPROC ProgramUniformMatrix3x2dv; PFNGLPROGRAMUNIFORMMATRIX3X2FVPROC ProgramUniformMatrix3x2fv; PFNGLPROGRAMUNIFORMMATRIX3X4DVPROC ProgramUniformMatrix3x4dv; PFNGLPROGRAMUNIFORMMATRIX3X4FVPROC ProgramUniformMatrix3x4fv; PFNGLPROGRAMUNIFORMMATRIX4DVPROC ProgramUniformMatrix4dv; PFNGLPROGRAMUNIFORMMATRIX4FVPROC ProgramUniformMatrix4fv; PFNGLPROGRAMUNIFORMMATRIX4X2DVPROC ProgramUniformMatrix4x2dv; PFNGLPROGRAMUNIFORMMATRIX4X2FVPROC ProgramUniformMatrix4x2fv; PFNGLPROGRAMUNIFORMMATRIX4X3DVPROC ProgramUniformMatrix4x3dv; PFNGLPROGRAMUNIFORMMATRIX4X3FVPROC ProgramUniformMatrix4x3fv; PFNGLPROVOKINGVERTEXPROC ProvokingVertex; PFNGLPUSHATTRIBPROC PushAttrib; PFNGLPUSHCLIENTATTRIBPROC PushClientAttrib; PFNGLPUSHDEBUGGROUPPROC PushDebugGroup; PFNGLPUSHMATRIXPROC PushMatrix; PFNGLPUSHNAMEPROC PushName; PFNGLQUERYCOUNTERPROC QueryCounter; PFNGLRASTERPOS2DPROC RasterPos2d; PFNGLRASTERPOS2DVPROC RasterPos2dv; PFNGLRASTERPOS2FPROC RasterPos2f; PFNGLRASTERPOS2FVPROC RasterPos2fv; PFNGLRASTERPOS2IPROC RasterPos2i; PFNGLRASTERPOS2IVPROC RasterPos2iv; PFNGLRASTERPOS2SPROC RasterPos2s; PFNGLRASTERPOS2SVPROC RasterPos2sv; PFNGLRASTERPOS3DPROC RasterPos3d; PFNGLRASTERPOS3DVPROC RasterPos3dv; PFNGLRASTERPOS3FPROC RasterPos3f; PFNGLRASTERPOS3FVPROC RasterPos3fv; PFNGLRASTERPOS3IPROC RasterPos3i; PFNGLRASTERPOS3IVPROC RasterPos3iv; PFNGLRASTERPOS3SPROC RasterPos3s; PFNGLRASTERPOS3SVPROC RasterPos3sv; PFNGLRASTERPOS4DPROC RasterPos4d; PFNGLRASTERPOS4DVPROC RasterPos4dv; PFNGLRASTERPOS4FPROC RasterPos4f; PFNGLRASTERPOS4FVPROC RasterPos4fv; PFNGLRASTERPOS4IPROC RasterPos4i; PFNGLRASTERPOS4IVPROC RasterPos4iv; PFNGLRASTERPOS4SPROC RasterPos4s; PFNGLRASTERPOS4SVPROC RasterPos4sv; PFNGLREADBUFFERPROC ReadBuffer; PFNGLREADPIXELSPROC ReadPixels; PFNGLREADNPIXELSPROC ReadnPixels; PFNGLRECTDPROC Rectd; PFNGLRECTDVPROC Rectdv; PFNGLRECTFPROC Rectf; PFNGLRECTFVPROC Rectfv; PFNGLRECTIPROC Recti; PFNGLRECTIVPROC Rectiv; PFNGLRECTSPROC Rects; PFNGLRECTSVPROC Rectsv; PFNGLRELEASESHADERCOMPILERPROC ReleaseShaderCompiler; PFNGLRENDERMODEPROC RenderMode; PFNGLRENDERBUFFERSTORAGEPROC RenderbufferStorage; PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC RenderbufferStorageMultisample; PFNGLRESUMETRANSFORMFEEDBACKPROC ResumeTransformFeedback; PFNGLROTATEDPROC Rotated; PFNGLROTATEFPROC Rotatef; PFNGLSAMPLECOVERAGEPROC SampleCoverage; PFNGLSAMPLEMASKIPROC SampleMaski; PFNGLSAMPLERPARAMETERIIVPROC SamplerParameterIiv; PFNGLSAMPLERPARAMETERIUIVPROC SamplerParameterIuiv; PFNGLSAMPLERPARAMETERFPROC SamplerParameterf; PFNGLSAMPLERPARAMETERFVPROC SamplerParameterfv; PFNGLSAMPLERPARAMETERIPROC SamplerParameteri; PFNGLSAMPLERPARAMETERIVPROC SamplerParameteriv; PFNGLSCALEDPROC Scaled; PFNGLSCALEFPROC Scalef; PFNGLSCISSORPROC Scissor; PFNGLSCISSORARRAYVPROC ScissorArrayv; PFNGLSCISSORINDEXEDPROC ScissorIndexed; PFNGLSCISSORINDEXEDVPROC ScissorIndexedv; PFNGLSECONDARYCOLOR3BPROC SecondaryColor3b; PFNGLSECONDARYCOLOR3BVPROC SecondaryColor3bv; PFNGLSECONDARYCOLOR3DPROC SecondaryColor3d; PFNGLSECONDARYCOLOR3DVPROC SecondaryColor3dv; PFNGLSECONDARYCOLOR3FPROC SecondaryColor3f; PFNGLSECONDARYCOLOR3FVPROC SecondaryColor3fv; PFNGLSECONDARYCOLOR3IPROC SecondaryColor3i; PFNGLSECONDARYCOLOR3IVPROC SecondaryColor3iv; PFNGLSECONDARYCOLOR3SPROC SecondaryColor3s; PFNGLSECONDARYCOLOR3SVPROC SecondaryColor3sv; PFNGLSECONDARYCOLOR3UBPROC SecondaryColor3ub; PFNGLSECONDARYCOLOR3UBVPROC SecondaryColor3ubv; PFNGLSECONDARYCOLOR3UIPROC SecondaryColor3ui; PFNGLSECONDARYCOLOR3UIVPROC SecondaryColor3uiv; PFNGLSECONDARYCOLOR3USPROC SecondaryColor3us; PFNGLSECONDARYCOLOR3USVPROC SecondaryColor3usv; PFNGLSECONDARYCOLORP3UIPROC SecondaryColorP3ui; PFNGLSECONDARYCOLORP3UIVPROC SecondaryColorP3uiv; PFNGLSECONDARYCOLORPOINTERPROC SecondaryColorPointer; PFNGLSELECTBUFFERPROC SelectBuffer; PFNGLSHADEMODELPROC ShadeModel; PFNGLSHADERBINARYPROC ShaderBinary; PFNGLSHADERSOURCEPROC ShaderSource; PFNGLSHADERSTORAGEBLOCKBINDINGPROC ShaderStorageBlockBinding; PFNGLSPECIALIZESHADERPROC SpecializeShader; PFNGLSTENCILFUNCPROC StencilFunc; PFNGLSTENCILFUNCSEPARATEPROC StencilFuncSeparate; PFNGLSTENCILMASKPROC StencilMask; PFNGLSTENCILMASKSEPARATEPROC StencilMaskSeparate; PFNGLSTENCILOPPROC StencilOp; PFNGLSTENCILOPSEPARATEPROC StencilOpSeparate; PFNGLTEXBUFFERPROC TexBuffer; PFNGLTEXBUFFERRANGEPROC TexBufferRange; PFNGLTEXCOORD1DPROC TexCoord1d; PFNGLTEXCOORD1DVPROC TexCoord1dv; PFNGLTEXCOORD1FPROC TexCoord1f; PFNGLTEXCOORD1FVPROC TexCoord1fv; PFNGLTEXCOORD1IPROC TexCoord1i; PFNGLTEXCOORD1IVPROC TexCoord1iv; PFNGLTEXCOORD1SPROC TexCoord1s; PFNGLTEXCOORD1SVPROC TexCoord1sv; PFNGLTEXCOORD2DPROC TexCoord2d; PFNGLTEXCOORD2DVPROC TexCoord2dv; PFNGLTEXCOORD2FPROC TexCoord2f; PFNGLTEXCOORD2FVPROC TexCoord2fv; PFNGLTEXCOORD2IPROC TexCoord2i; PFNGLTEXCOORD2IVPROC TexCoord2iv; PFNGLTEXCOORD2SPROC TexCoord2s; PFNGLTEXCOORD2SVPROC TexCoord2sv; PFNGLTEXCOORD3DPROC TexCoord3d; PFNGLTEXCOORD3DVPROC TexCoord3dv; PFNGLTEXCOORD3FPROC TexCoord3f; PFNGLTEXCOORD3FVPROC TexCoord3fv; PFNGLTEXCOORD3IPROC TexCoord3i; PFNGLTEXCOORD3IVPROC TexCoord3iv; PFNGLTEXCOORD3SPROC TexCoord3s; PFNGLTEXCOORD3SVPROC TexCoord3sv; PFNGLTEXCOORD4DPROC TexCoord4d; PFNGLTEXCOORD4DVPROC TexCoord4dv; PFNGLTEXCOORD4FPROC TexCoord4f; PFNGLTEXCOORD4FVPROC TexCoord4fv; PFNGLTEXCOORD4IPROC TexCoord4i; PFNGLTEXCOORD4IVPROC TexCoord4iv; PFNGLTEXCOORD4SPROC TexCoord4s; PFNGLTEXCOORD4SVPROC TexCoord4sv; PFNGLTEXCOORDP1UIPROC TexCoordP1ui; PFNGLTEXCOORDP1UIVPROC TexCoordP1uiv; PFNGLTEXCOORDP2UIPROC TexCoordP2ui; PFNGLTEXCOORDP2UIVPROC TexCoordP2uiv; PFNGLTEXCOORDP3UIPROC TexCoordP3ui; PFNGLTEXCOORDP3UIVPROC TexCoordP3uiv; PFNGLTEXCOORDP4UIPROC TexCoordP4ui; PFNGLTEXCOORDP4UIVPROC TexCoordP4uiv; PFNGLTEXCOORDPOINTERPROC TexCoordPointer; PFNGLTEXENVFPROC TexEnvf; PFNGLTEXENVFVPROC TexEnvfv; PFNGLTEXENVIPROC TexEnvi; PFNGLTEXENVIVPROC TexEnviv; PFNGLTEXGENDPROC TexGend; PFNGLTEXGENDVPROC TexGendv; PFNGLTEXGENFPROC TexGenf; PFNGLTEXGENFVPROC TexGenfv; PFNGLTEXGENIPROC TexGeni; PFNGLTEXGENIVPROC TexGeniv; PFNGLTEXIMAGE1DPROC TexImage1D; PFNGLTEXIMAGE2DPROC TexImage2D; PFNGLTEXIMAGE2DMULTISAMPLEPROC TexImage2DMultisample; PFNGLTEXIMAGE3DPROC TexImage3D; PFNGLTEXIMAGE3DMULTISAMPLEPROC TexImage3DMultisample; PFNGLTEXPARAMETERIIVPROC TexParameterIiv; PFNGLTEXPARAMETERIUIVPROC TexParameterIuiv; PFNGLTEXPARAMETERFPROC TexParameterf; PFNGLTEXPARAMETERFVPROC TexParameterfv; PFNGLTEXPARAMETERIPROC TexParameteri; PFNGLTEXPARAMETERIVPROC TexParameteriv; PFNGLTEXSTORAGE1DPROC TexStorage1D; PFNGLTEXSTORAGE2DPROC TexStorage2D; PFNGLTEXSTORAGE2DMULTISAMPLEPROC TexStorage2DMultisample; PFNGLTEXSTORAGE3DPROC TexStorage3D; PFNGLTEXSTORAGE3DMULTISAMPLEPROC TexStorage3DMultisample; PFNGLTEXSUBIMAGE1DPROC TexSubImage1D; PFNGLTEXSUBIMAGE2DPROC TexSubImage2D; PFNGLTEXSUBIMAGE3DPROC TexSubImage3D; PFNGLTEXTUREBARRIERPROC TextureBarrier; PFNGLTEXTUREBUFFERPROC TextureBuffer; PFNGLTEXTUREBUFFERRANGEPROC TextureBufferRange; PFNGLTEXTUREPARAMETERIIVPROC TextureParameterIiv; PFNGLTEXTUREPARAMETERIUIVPROC TextureParameterIuiv; PFNGLTEXTUREPARAMETERFPROC TextureParameterf; PFNGLTEXTUREPARAMETERFVPROC TextureParameterfv; PFNGLTEXTUREPARAMETERIPROC TextureParameteri; PFNGLTEXTUREPARAMETERIVPROC TextureParameteriv; PFNGLTEXTURESTORAGE1DPROC TextureStorage1D; PFNGLTEXTURESTORAGE2DPROC TextureStorage2D; PFNGLTEXTURESTORAGE2DMULTISAMPLEPROC TextureStorage2DMultisample; PFNGLTEXTURESTORAGE3DPROC TextureStorage3D; PFNGLTEXTURESTORAGE3DMULTISAMPLEPROC TextureStorage3DMultisample; PFNGLTEXTURESUBIMAGE1DPROC TextureSubImage1D; PFNGLTEXTURESUBIMAGE2DPROC TextureSubImage2D; PFNGLTEXTURESUBIMAGE3DPROC TextureSubImage3D; PFNGLTEXTUREVIEWPROC TextureView; PFNGLTRANSFORMFEEDBACKBUFFERBASEPROC TransformFeedbackBufferBase; PFNGLTRANSFORMFEEDBACKBUFFERRANGEPROC TransformFeedbackBufferRange; PFNGLTRANSFORMFEEDBACKVARYINGSPROC TransformFeedbackVaryings; PFNGLTRANSLATEDPROC Translated; PFNGLTRANSLATEFPROC Translatef; PFNGLUNIFORM1DPROC Uniform1d; PFNGLUNIFORM1DVPROC Uniform1dv; PFNGLUNIFORM1FPROC Uniform1f; PFNGLUNIFORM1FVPROC Uniform1fv; PFNGLUNIFORM1IPROC Uniform1i; PFNGLUNIFORM1IVPROC Uniform1iv; PFNGLUNIFORM1UIPROC Uniform1ui; PFNGLUNIFORM1UIVPROC Uniform1uiv; PFNGLUNIFORM2DPROC Uniform2d; PFNGLUNIFORM2DVPROC Uniform2dv; PFNGLUNIFORM2FPROC Uniform2f; PFNGLUNIFORM2FVPROC Uniform2fv; PFNGLUNIFORM2IPROC Uniform2i; PFNGLUNIFORM2IVPROC Uniform2iv; PFNGLUNIFORM2UIPROC Uniform2ui; PFNGLUNIFORM2UIVPROC Uniform2uiv; PFNGLUNIFORM3DPROC Uniform3d; PFNGLUNIFORM3DVPROC Uniform3dv; PFNGLUNIFORM3FPROC Uniform3f; PFNGLUNIFORM3FVPROC Uniform3fv; PFNGLUNIFORM3IPROC Uniform3i; PFNGLUNIFORM3IVPROC Uniform3iv; PFNGLUNIFORM3UIPROC Uniform3ui; PFNGLUNIFORM3UIVPROC Uniform3uiv; PFNGLUNIFORM4DPROC Uniform4d; PFNGLUNIFORM4DVPROC Uniform4dv; PFNGLUNIFORM4FPROC Uniform4f; PFNGLUNIFORM4FVPROC Uniform4fv; PFNGLUNIFORM4IPROC Uniform4i; PFNGLUNIFORM4IVPROC Uniform4iv; PFNGLUNIFORM4UIPROC Uniform4ui; PFNGLUNIFORM4UIVPROC Uniform4uiv; PFNGLUNIFORMBLOCKBINDINGPROC UniformBlockBinding; PFNGLUNIFORMMATRIX2DVPROC UniformMatrix2dv; PFNGLUNIFORMMATRIX2FVPROC UniformMatrix2fv; PFNGLUNIFORMMATRIX2X3DVPROC UniformMatrix2x3dv; PFNGLUNIFORMMATRIX2X3FVPROC UniformMatrix2x3fv; PFNGLUNIFORMMATRIX2X4DVPROC UniformMatrix2x4dv; PFNGLUNIFORMMATRIX2X4FVPROC UniformMatrix2x4fv; PFNGLUNIFORMMATRIX3DVPROC UniformMatrix3dv; PFNGLUNIFORMMATRIX3FVPROC UniformMatrix3fv; PFNGLUNIFORMMATRIX3X2DVPROC UniformMatrix3x2dv; PFNGLUNIFORMMATRIX3X2FVPROC UniformMatrix3x2fv; PFNGLUNIFORMMATRIX3X4DVPROC UniformMatrix3x4dv; PFNGLUNIFORMMATRIX3X4FVPROC UniformMatrix3x4fv; PFNGLUNIFORMMATRIX4DVPROC UniformMatrix4dv; PFNGLUNIFORMMATRIX4FVPROC UniformMatrix4fv; PFNGLUNIFORMMATRIX4X2DVPROC UniformMatrix4x2dv; PFNGLUNIFORMMATRIX4X2FVPROC UniformMatrix4x2fv; PFNGLUNIFORMMATRIX4X3DVPROC UniformMatrix4x3dv; PFNGLUNIFORMMATRIX4X3FVPROC UniformMatrix4x3fv; PFNGLUNIFORMSUBROUTINESUIVPROC UniformSubroutinesuiv; PFNGLUNMAPBUFFERPROC UnmapBuffer; PFNGLUNMAPNAMEDBUFFERPROC UnmapNamedBuffer; PFNGLUSEPROGRAMPROC UseProgram; PFNGLUSEPROGRAMSTAGESPROC UseProgramStages; PFNGLVALIDATEPROGRAMPROC ValidateProgram; PFNGLVALIDATEPROGRAMPIPELINEPROC ValidateProgramPipeline; PFNGLVERTEX2DPROC Vertex2d; PFNGLVERTEX2DVPROC Vertex2dv; PFNGLVERTEX2FPROC Vertex2f; PFNGLVERTEX2FVPROC Vertex2fv; PFNGLVERTEX2IPROC Vertex2i; PFNGLVERTEX2IVPROC Vertex2iv; PFNGLVERTEX2SPROC Vertex2s; PFNGLVERTEX2SVPROC Vertex2sv; PFNGLVERTEX3DPROC Vertex3d; PFNGLVERTEX3DVPROC Vertex3dv; PFNGLVERTEX3FPROC Vertex3f; PFNGLVERTEX3FVPROC Vertex3fv; PFNGLVERTEX3IPROC Vertex3i; PFNGLVERTEX3IVPROC Vertex3iv; PFNGLVERTEX3SPROC Vertex3s; PFNGLVERTEX3SVPROC Vertex3sv; PFNGLVERTEX4DPROC Vertex4d; PFNGLVERTEX4DVPROC Vertex4dv; PFNGLVERTEX4FPROC Vertex4f; PFNGLVERTEX4FVPROC Vertex4fv; PFNGLVERTEX4IPROC Vertex4i; PFNGLVERTEX4IVPROC Vertex4iv; PFNGLVERTEX4SPROC Vertex4s; PFNGLVERTEX4SVPROC Vertex4sv; PFNGLVERTEXARRAYATTRIBBINDINGPROC VertexArrayAttribBinding; PFNGLVERTEXARRAYATTRIBFORMATPROC VertexArrayAttribFormat; PFNGLVERTEXARRAYATTRIBIFORMATPROC VertexArrayAttribIFormat; PFNGLVERTEXARRAYATTRIBLFORMATPROC VertexArrayAttribLFormat; PFNGLVERTEXARRAYBINDINGDIVISORPROC VertexArrayBindingDivisor; PFNGLVERTEXARRAYELEMENTBUFFERPROC VertexArrayElementBuffer; PFNGLVERTEXARRAYVERTEXBUFFERPROC VertexArrayVertexBuffer; PFNGLVERTEXARRAYVERTEXBUFFERSPROC VertexArrayVertexBuffers; PFNGLVERTEXATTRIB1DPROC VertexAttrib1d; PFNGLVERTEXATTRIB1DVPROC VertexAttrib1dv; PFNGLVERTEXATTRIB1FPROC VertexAttrib1f; PFNGLVERTEXATTRIB1FVPROC VertexAttrib1fv; PFNGLVERTEXATTRIB1SPROC VertexAttrib1s; PFNGLVERTEXATTRIB1SVPROC VertexAttrib1sv; PFNGLVERTEXATTRIB2DPROC VertexAttrib2d; PFNGLVERTEXATTRIB2DVPROC VertexAttrib2dv; PFNGLVERTEXATTRIB2FPROC VertexAttrib2f; PFNGLVERTEXATTRIB2FVPROC VertexAttrib2fv; PFNGLVERTEXATTRIB2SPROC VertexAttrib2s; PFNGLVERTEXATTRIB2SVPROC VertexAttrib2sv; PFNGLVERTEXATTRIB3DPROC VertexAttrib3d; PFNGLVERTEXATTRIB3DVPROC VertexAttrib3dv; PFNGLVERTEXATTRIB3FPROC VertexAttrib3f; PFNGLVERTEXATTRIB3FVPROC VertexAttrib3fv; PFNGLVERTEXATTRIB3SPROC VertexAttrib3s; PFNGLVERTEXATTRIB3SVPROC VertexAttrib3sv; PFNGLVERTEXATTRIB4NBVPROC VertexAttrib4Nbv; PFNGLVERTEXATTRIB4NIVPROC VertexAttrib4Niv; PFNGLVERTEXATTRIB4NSVPROC VertexAttrib4Nsv; PFNGLVERTEXATTRIB4NUBPROC VertexAttrib4Nub; PFNGLVERTEXATTRIB4NUBVPROC VertexAttrib4Nubv; PFNGLVERTEXATTRIB4NUIVPROC VertexAttrib4Nuiv; PFNGLVERTEXATTRIB4NUSVPROC VertexAttrib4Nusv; PFNGLVERTEXATTRIB4BVPROC VertexAttrib4bv; PFNGLVERTEXATTRIB4DPROC VertexAttrib4d; PFNGLVERTEXATTRIB4DVPROC VertexAttrib4dv; PFNGLVERTEXATTRIB4FPROC VertexAttrib4f; PFNGLVERTEXATTRIB4FVPROC VertexAttrib4fv; PFNGLVERTEXATTRIB4IVPROC VertexAttrib4iv; PFNGLVERTEXATTRIB4SPROC VertexAttrib4s; PFNGLVERTEXATTRIB4SVPROC VertexAttrib4sv; PFNGLVERTEXATTRIB4UBVPROC VertexAttrib4ubv; PFNGLVERTEXATTRIB4UIVPROC VertexAttrib4uiv; PFNGLVERTEXATTRIB4USVPROC VertexAttrib4usv; PFNGLVERTEXATTRIBBINDINGPROC VertexAttribBinding; PFNGLVERTEXATTRIBDIVISORPROC VertexAttribDivisor; PFNGLVERTEXATTRIBFORMATPROC VertexAttribFormat; PFNGLVERTEXATTRIBI1IPROC VertexAttribI1i; PFNGLVERTEXATTRIBI1IVPROC VertexAttribI1iv; PFNGLVERTEXATTRIBI1UIPROC VertexAttribI1ui; PFNGLVERTEXATTRIBI1UIVPROC VertexAttribI1uiv; PFNGLVERTEXATTRIBI2IPROC VertexAttribI2i; PFNGLVERTEXATTRIBI2IVPROC VertexAttribI2iv; PFNGLVERTEXATTRIBI2UIPROC VertexAttribI2ui; PFNGLVERTEXATTRIBI2UIVPROC VertexAttribI2uiv; PFNGLVERTEXATTRIBI3IPROC VertexAttribI3i; PFNGLVERTEXATTRIBI3IVPROC VertexAttribI3iv; PFNGLVERTEXATTRIBI3UIPROC VertexAttribI3ui; PFNGLVERTEXATTRIBI3UIVPROC VertexAttribI3uiv; PFNGLVERTEXATTRIBI4BVPROC VertexAttribI4bv; PFNGLVERTEXATTRIBI4IPROC VertexAttribI4i; PFNGLVERTEXATTRIBI4IVPROC VertexAttribI4iv; PFNGLVERTEXATTRIBI4SVPROC VertexAttribI4sv; PFNGLVERTEXATTRIBI4UBVPROC VertexAttribI4ubv; PFNGLVERTEXATTRIBI4UIPROC VertexAttribI4ui; PFNGLVERTEXATTRIBI4UIVPROC VertexAttribI4uiv; PFNGLVERTEXATTRIBI4USVPROC VertexAttribI4usv; PFNGLVERTEXATTRIBIFORMATPROC VertexAttribIFormat; PFNGLVERTEXATTRIBIPOINTERPROC VertexAttribIPointer; PFNGLVERTEXATTRIBL1DPROC VertexAttribL1d; PFNGLVERTEXATTRIBL1DVPROC VertexAttribL1dv; PFNGLVERTEXATTRIBL2DPROC VertexAttribL2d; PFNGLVERTEXATTRIBL2DVPROC VertexAttribL2dv; PFNGLVERTEXATTRIBL3DPROC VertexAttribL3d; PFNGLVERTEXATTRIBL3DVPROC VertexAttribL3dv; PFNGLVERTEXATTRIBL4DPROC VertexAttribL4d; PFNGLVERTEXATTRIBL4DVPROC VertexAttribL4dv; PFNGLVERTEXATTRIBLFORMATPROC VertexAttribLFormat; PFNGLVERTEXATTRIBLPOINTERPROC VertexAttribLPointer; PFNGLVERTEXATTRIBP1UIPROC VertexAttribP1ui; PFNGLVERTEXATTRIBP1UIVPROC VertexAttribP1uiv; PFNGLVERTEXATTRIBP2UIPROC VertexAttribP2ui; PFNGLVERTEXATTRIBP2UIVPROC VertexAttribP2uiv; PFNGLVERTEXATTRIBP3UIPROC VertexAttribP3ui; PFNGLVERTEXATTRIBP3UIVPROC VertexAttribP3uiv; PFNGLVERTEXATTRIBP4UIPROC VertexAttribP4ui; PFNGLVERTEXATTRIBP4UIVPROC VertexAttribP4uiv; PFNGLVERTEXATTRIBPOINTERPROC VertexAttribPointer; PFNGLVERTEXBINDINGDIVISORPROC VertexBindingDivisor; PFNGLVERTEXP2UIPROC VertexP2ui; PFNGLVERTEXP2UIVPROC VertexP2uiv; PFNGLVERTEXP3UIPROC VertexP3ui; PFNGLVERTEXP3UIVPROC VertexP3uiv; PFNGLVERTEXP4UIPROC VertexP4ui; PFNGLVERTEXP4UIVPROC VertexP4uiv; PFNGLVERTEXPOINTERPROC VertexPointer; PFNGLVIEWPORTPROC Viewport; PFNGLVIEWPORTARRAYVPROC ViewportArrayv; PFNGLVIEWPORTINDEXEDFPROC ViewportIndexedf; PFNGLVIEWPORTINDEXEDFVPROC ViewportIndexedfv; PFNGLWAITSYNCPROC WaitSync; PFNGLWINDOWPOS2DPROC WindowPos2d; PFNGLWINDOWPOS2DVPROC WindowPos2dv; PFNGLWINDOWPOS2FPROC WindowPos2f; PFNGLWINDOWPOS2FVPROC WindowPos2fv; PFNGLWINDOWPOS2IPROC WindowPos2i; PFNGLWINDOWPOS2IVPROC WindowPos2iv; PFNGLWINDOWPOS2SPROC WindowPos2s; PFNGLWINDOWPOS2SVPROC WindowPos2sv; PFNGLWINDOWPOS3DPROC WindowPos3d; PFNGLWINDOWPOS3DVPROC WindowPos3dv; PFNGLWINDOWPOS3FPROC WindowPos3f; PFNGLWINDOWPOS3FVPROC WindowPos3fv; PFNGLWINDOWPOS3IPROC WindowPos3i; PFNGLWINDOWPOS3IVPROC WindowPos3iv; PFNGLWINDOWPOS3SPROC WindowPos3s; PFNGLWINDOWPOS3SVPROC WindowPos3sv; } GladGLContext; GLAD_API_CALL int gladLoadGLContextUserPtr(GladGLContext *context, GLADuserptrloadfunc load, void *userptr); GLAD_API_CALL int gladLoadGLContext(GladGLContext *context, GLADloadfunc load); #ifdef GLAD_GL GLAD_API_CALL int gladLoaderLoadGLContext(GladGLContext *context); GLAD_API_CALL void gladLoaderUnloadGL(void); #endif #ifdef __cplusplus } #endif #endif ================================================ FILE: third-party/glad/src/egl.c ================================================ #include #include #include #include #ifndef GLAD_IMPL_UTIL_C_ #define GLAD_IMPL_UTIL_C_ #ifdef _MSC_VER #define GLAD_IMPL_UTIL_SSCANF sscanf_s #else #define GLAD_IMPL_UTIL_SSCANF sscanf #endif #endif /* GLAD_IMPL_UTIL_C_ */ #ifdef __cplusplus extern "C" { #endif int GLAD_EGL_VERSION_1_0 = 0; int GLAD_EGL_VERSION_1_1 = 0; int GLAD_EGL_VERSION_1_2 = 0; int GLAD_EGL_VERSION_1_3 = 0; int GLAD_EGL_VERSION_1_4 = 0; int GLAD_EGL_VERSION_1_5 = 0; PFNEGLBINDAPIPROC glad_eglBindAPI = NULL; PFNEGLBINDTEXIMAGEPROC glad_eglBindTexImage = NULL; PFNEGLCHOOSECONFIGPROC glad_eglChooseConfig = NULL; PFNEGLCLIENTWAITSYNCPROC glad_eglClientWaitSync = NULL; PFNEGLCOPYBUFFERSPROC glad_eglCopyBuffers = NULL; PFNEGLCREATECONTEXTPROC glad_eglCreateContext = NULL; PFNEGLCREATEIMAGEPROC glad_eglCreateImage = NULL; PFNEGLCREATEPBUFFERFROMCLIENTBUFFERPROC glad_eglCreatePbufferFromClientBuffer = NULL; PFNEGLCREATEPBUFFERSURFACEPROC glad_eglCreatePbufferSurface = NULL; PFNEGLCREATEPIXMAPSURFACEPROC glad_eglCreatePixmapSurface = NULL; PFNEGLCREATEPLATFORMPIXMAPSURFACEPROC glad_eglCreatePlatformPixmapSurface = NULL; PFNEGLCREATEPLATFORMWINDOWSURFACEPROC glad_eglCreatePlatformWindowSurface = NULL; PFNEGLCREATESYNCPROC glad_eglCreateSync = NULL; PFNEGLCREATEWINDOWSURFACEPROC glad_eglCreateWindowSurface = NULL; PFNEGLDESTROYCONTEXTPROC glad_eglDestroyContext = NULL; PFNEGLDESTROYIMAGEPROC glad_eglDestroyImage = NULL; PFNEGLDESTROYSURFACEPROC glad_eglDestroySurface = NULL; PFNEGLDESTROYSYNCPROC glad_eglDestroySync = NULL; PFNEGLGETCONFIGATTRIBPROC glad_eglGetConfigAttrib = NULL; PFNEGLGETCONFIGSPROC glad_eglGetConfigs = NULL; PFNEGLGETCURRENTCONTEXTPROC glad_eglGetCurrentContext = NULL; PFNEGLGETCURRENTDISPLAYPROC glad_eglGetCurrentDisplay = NULL; PFNEGLGETCURRENTSURFACEPROC glad_eglGetCurrentSurface = NULL; PFNEGLGETDISPLAYPROC glad_eglGetDisplay = NULL; PFNEGLGETERRORPROC glad_eglGetError = NULL; PFNEGLGETPLATFORMDISPLAYPROC glad_eglGetPlatformDisplay = NULL; PFNEGLGETPROCADDRESSPROC glad_eglGetProcAddress = NULL; PFNEGLGETSYNCATTRIBPROC glad_eglGetSyncAttrib = NULL; PFNEGLINITIALIZEPROC glad_eglInitialize = NULL; PFNEGLMAKECURRENTPROC glad_eglMakeCurrent = NULL; PFNEGLQUERYAPIPROC glad_eglQueryAPI = NULL; PFNEGLQUERYCONTEXTPROC glad_eglQueryContext = NULL; PFNEGLQUERYSTRINGPROC glad_eglQueryString = NULL; PFNEGLQUERYSURFACEPROC glad_eglQuerySurface = NULL; PFNEGLRELEASETEXIMAGEPROC glad_eglReleaseTexImage = NULL; PFNEGLRELEASETHREADPROC glad_eglReleaseThread = NULL; PFNEGLSURFACEATTRIBPROC glad_eglSurfaceAttrib = NULL; PFNEGLSWAPBUFFERSPROC glad_eglSwapBuffers = NULL; PFNEGLSWAPINTERVALPROC glad_eglSwapInterval = NULL; PFNEGLTERMINATEPROC glad_eglTerminate = NULL; PFNEGLWAITCLIENTPROC glad_eglWaitClient = NULL; PFNEGLWAITGLPROC glad_eglWaitGL = NULL; PFNEGLWAITNATIVEPROC glad_eglWaitNative = NULL; PFNEGLWAITSYNCPROC glad_eglWaitSync = NULL; PFNEGLCREATEIMAGEKHRPROC glad_eglCreateImageKHR = NULL; PFNEGLDESTROYIMAGEKHRPROC glad_eglDestroyImageKHR = NULL; static void glad_egl_load_EGL_VERSION_1_0( GLADuserptrloadfunc load, void* userptr) { if(!GLAD_EGL_VERSION_1_0) return; glad_eglChooseConfig = (PFNEGLCHOOSECONFIGPROC) load(userptr, "eglChooseConfig"); glad_eglCopyBuffers = (PFNEGLCOPYBUFFERSPROC) load(userptr, "eglCopyBuffers"); glad_eglCreateContext = (PFNEGLCREATECONTEXTPROC) load(userptr, "eglCreateContext"); glad_eglCreatePbufferSurface = (PFNEGLCREATEPBUFFERSURFACEPROC) load(userptr, "eglCreatePbufferSurface"); glad_eglCreatePixmapSurface = (PFNEGLCREATEPIXMAPSURFACEPROC) load(userptr, "eglCreatePixmapSurface"); glad_eglCreateWindowSurface = (PFNEGLCREATEWINDOWSURFACEPROC) load(userptr, "eglCreateWindowSurface"); glad_eglDestroyContext = (PFNEGLDESTROYCONTEXTPROC) load(userptr, "eglDestroyContext"); glad_eglDestroySurface = (PFNEGLDESTROYSURFACEPROC) load(userptr, "eglDestroySurface"); glad_eglGetConfigAttrib = (PFNEGLGETCONFIGATTRIBPROC) load(userptr, "eglGetConfigAttrib"); glad_eglGetConfigs = (PFNEGLGETCONFIGSPROC) load(userptr, "eglGetConfigs"); glad_eglGetCurrentDisplay = (PFNEGLGETCURRENTDISPLAYPROC) load(userptr, "eglGetCurrentDisplay"); glad_eglGetCurrentSurface = (PFNEGLGETCURRENTSURFACEPROC) load(userptr, "eglGetCurrentSurface"); glad_eglGetDisplay = (PFNEGLGETDISPLAYPROC) load(userptr, "eglGetDisplay"); glad_eglGetError = (PFNEGLGETERRORPROC) load(userptr, "eglGetError"); glad_eglGetProcAddress = (PFNEGLGETPROCADDRESSPROC) load(userptr, "eglGetProcAddress"); glad_eglInitialize = (PFNEGLINITIALIZEPROC) load(userptr, "eglInitialize"); glad_eglMakeCurrent = (PFNEGLMAKECURRENTPROC) load(userptr, "eglMakeCurrent"); glad_eglQueryContext = (PFNEGLQUERYCONTEXTPROC) load(userptr, "eglQueryContext"); glad_eglQueryString = (PFNEGLQUERYSTRINGPROC) load(userptr, "eglQueryString"); glad_eglQuerySurface = (PFNEGLQUERYSURFACEPROC) load(userptr, "eglQuerySurface"); glad_eglSwapBuffers = (PFNEGLSWAPBUFFERSPROC) load(userptr, "eglSwapBuffers"); glad_eglTerminate = (PFNEGLTERMINATEPROC) load(userptr, "eglTerminate"); glad_eglWaitGL = (PFNEGLWAITGLPROC) load(userptr, "eglWaitGL"); glad_eglWaitNative = (PFNEGLWAITNATIVEPROC) load(userptr, "eglWaitNative"); } static void glad_egl_load_EGL_VERSION_1_1( GLADuserptrloadfunc load, void* userptr) { if(!GLAD_EGL_VERSION_1_1) return; glad_eglBindTexImage = (PFNEGLBINDTEXIMAGEPROC) load(userptr, "eglBindTexImage"); glad_eglReleaseTexImage = (PFNEGLRELEASETEXIMAGEPROC) load(userptr, "eglReleaseTexImage"); glad_eglSurfaceAttrib = (PFNEGLSURFACEATTRIBPROC) load(userptr, "eglSurfaceAttrib"); glad_eglSwapInterval = (PFNEGLSWAPINTERVALPROC) load(userptr, "eglSwapInterval"); } static void glad_egl_load_EGL_VERSION_1_2( GLADuserptrloadfunc load, void* userptr) { if(!GLAD_EGL_VERSION_1_2) return; glad_eglBindAPI = (PFNEGLBINDAPIPROC) load(userptr, "eglBindAPI"); glad_eglCreatePbufferFromClientBuffer = (PFNEGLCREATEPBUFFERFROMCLIENTBUFFERPROC) load(userptr, "eglCreatePbufferFromClientBuffer"); glad_eglQueryAPI = (PFNEGLQUERYAPIPROC) load(userptr, "eglQueryAPI"); glad_eglReleaseThread = (PFNEGLRELEASETHREADPROC) load(userptr, "eglReleaseThread"); glad_eglWaitClient = (PFNEGLWAITCLIENTPROC) load(userptr, "eglWaitClient"); glad_eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC) load(userptr, "eglCreateImageKHR"); glad_eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC) load(userptr, "eglDestroyImageKHR"); } static void glad_egl_load_EGL_VERSION_1_4( GLADuserptrloadfunc load, void* userptr) { if(!GLAD_EGL_VERSION_1_4) return; glad_eglGetCurrentContext = (PFNEGLGETCURRENTCONTEXTPROC) load(userptr, "eglGetCurrentContext"); } static void glad_egl_load_EGL_VERSION_1_5( GLADuserptrloadfunc load, void* userptr) { if(!GLAD_EGL_VERSION_1_5) return; glad_eglClientWaitSync = (PFNEGLCLIENTWAITSYNCPROC) load(userptr, "eglClientWaitSync"); glad_eglCreateImage = (PFNEGLCREATEIMAGEPROC) load(userptr, "eglCreateImage"); glad_eglCreatePlatformPixmapSurface = (PFNEGLCREATEPLATFORMPIXMAPSURFACEPROC) load(userptr, "eglCreatePlatformPixmapSurface"); glad_eglCreatePlatformWindowSurface = (PFNEGLCREATEPLATFORMWINDOWSURFACEPROC) load(userptr, "eglCreatePlatformWindowSurface"); glad_eglCreateSync = (PFNEGLCREATESYNCPROC) load(userptr, "eglCreateSync"); glad_eglDestroyImage = (PFNEGLDESTROYIMAGEPROC) load(userptr, "eglDestroyImage"); glad_eglDestroySync = (PFNEGLDESTROYSYNCPROC) load(userptr, "eglDestroySync"); glad_eglGetPlatformDisplay = (PFNEGLGETPLATFORMDISPLAYPROC) load(userptr, "eglGetPlatformDisplay"); glad_eglGetSyncAttrib = (PFNEGLGETSYNCATTRIBPROC) load(userptr, "eglGetSyncAttrib"); glad_eglWaitSync = (PFNEGLWAITSYNCPROC) load(userptr, "eglWaitSync"); } static int glad_egl_get_extensions(EGLDisplay display, const char **extensions) { *extensions = eglQueryString(display, EGL_EXTENSIONS); return extensions != NULL; } static int glad_egl_has_extension(const char *extensions, const char *ext) { const char *loc; const char *terminator; if(extensions == NULL) { return 0; } while(1) { loc = strstr(extensions, ext); if(loc == NULL) { return 0; } terminator = loc + strlen(ext); if((loc == extensions || *(loc - 1) == ' ') && (*terminator == ' ' || *terminator == '\0')) { return 1; } extensions = terminator; } } static GLADapiproc glad_egl_get_proc_from_userptr(void *userptr, const char *name) { return (GLAD_GNUC_EXTENSION (GLADapiproc (*)(const char *name)) userptr)(name); } static int glad_egl_find_extensions_egl(EGLDisplay display) { const char *extensions; if (!glad_egl_get_extensions(display, &extensions)) return 0; (void) glad_egl_has_extension; return 1; } static int glad_egl_find_core_egl(EGLDisplay display) { int major, minor; const char *version; if (display == NULL) { display = EGL_NO_DISPLAY; /* this is usually NULL, better safe than sorry */ } if (display == EGL_NO_DISPLAY) { display = eglGetCurrentDisplay(); } #ifdef EGL_VERSION_1_4 if (display == EGL_NO_DISPLAY) { display = eglGetDisplay(EGL_DEFAULT_DISPLAY); } #endif #ifndef EGL_VERSION_1_5 if (display == EGL_NO_DISPLAY) { return 0; } #endif version = eglQueryString(display, EGL_VERSION); (void) eglGetError(); if (version == NULL) { major = 1; minor = 5; // We need version 1.5 anyway } else { GLAD_IMPL_UTIL_SSCANF(version, "%d.%d", &major, &minor); } GLAD_EGL_VERSION_1_0 = (major == 1 && minor >= 0) || major > 1; GLAD_EGL_VERSION_1_1 = (major == 1 && minor >= 1) || major > 1; GLAD_EGL_VERSION_1_2 = (major == 1 && minor >= 2) || major > 1; GLAD_EGL_VERSION_1_3 = (major == 1 && minor >= 3) || major > 1; GLAD_EGL_VERSION_1_4 = (major == 1 && minor >= 4) || major > 1; GLAD_EGL_VERSION_1_5 = (major == 1 && minor >= 5) || major > 1; return GLAD_MAKE_VERSION(major, minor); } int gladLoadEGLUserPtr(EGLDisplay display, GLADuserptrloadfunc load, void* userptr) { int version; eglGetDisplay = (PFNEGLGETDISPLAYPROC) load(userptr, "eglGetDisplay"); eglGetCurrentDisplay = (PFNEGLGETCURRENTDISPLAYPROC) load(userptr, "eglGetCurrentDisplay"); eglQueryString = (PFNEGLQUERYSTRINGPROC) load(userptr, "eglQueryString"); eglGetError = (PFNEGLGETERRORPROC) load(userptr, "eglGetError"); if (eglGetDisplay == NULL || eglGetCurrentDisplay == NULL || eglQueryString == NULL || eglGetError == NULL) return 0; version = glad_egl_find_core_egl(display); if (!version) return 0; glad_egl_load_EGL_VERSION_1_0(load, userptr); glad_egl_load_EGL_VERSION_1_1(load, userptr); glad_egl_load_EGL_VERSION_1_2(load, userptr); glad_egl_load_EGL_VERSION_1_4(load, userptr); glad_egl_load_EGL_VERSION_1_5(load, userptr); if (!glad_egl_find_extensions_egl(display)) return 0; return version; } int gladLoadEGL(EGLDisplay display, GLADloadfunc load) { return gladLoadEGLUserPtr(display, glad_egl_get_proc_from_userptr, GLAD_GNUC_EXTENSION (void*) load); } #ifdef GLAD_EGL #ifndef GLAD_LOADER_LIBRARY_C_ #define GLAD_LOADER_LIBRARY_C_ #include #include #if GLAD_PLATFORM_WIN32 #include #else #include #endif static void* glad_get_dlopen_handle(const char *lib_names[], int length) { void *handle = NULL; int i; for (i = 0; i < length; ++i) { #if GLAD_PLATFORM_WIN32 #if GLAD_PLATFORM_UWP size_t buffer_size = (strlen(lib_names[i]) + 1) * sizeof(WCHAR); LPWSTR buffer = (LPWSTR) malloc(buffer_size); if (buffer != NULL) { int ret = MultiByteToWideChar(CP_ACP, 0, lib_names[i], -1, buffer, buffer_size); if (ret != 0) { handle = (void*) LoadPackagedLibrary(buffer, 0); } free((void*) buffer); } #else handle = (void*) LoadLibraryA(lib_names[i]); #endif #else handle = dlopen(lib_names[i], RTLD_LAZY | RTLD_LOCAL); #endif if (handle != NULL) { return handle; } } return NULL; } static void glad_close_dlopen_handle(void* handle) { if (handle != NULL) { #if GLAD_PLATFORM_WIN32 FreeLibrary((HMODULE) handle); #else dlclose(handle); #endif } } static GLADapiproc glad_dlsym_handle(void* handle, const char *name) { if (handle == NULL) { return NULL; } #if GLAD_PLATFORM_WIN32 return (GLADapiproc) GetProcAddress((HMODULE) handle, name); #else return GLAD_GNUC_EXTENSION (GLADapiproc) dlsym(handle, name); #endif } #endif /* GLAD_LOADER_LIBRARY_C_ */ struct _glad_egl_userptr { void *handle; PFNEGLGETPROCADDRESSPROC get_proc_address_ptr; }; static GLADapiproc glad_egl_get_proc(void *vuserptr, const char* name) { struct _glad_egl_userptr userptr = *(struct _glad_egl_userptr*) vuserptr; GLADapiproc result = NULL; result = glad_dlsym_handle(userptr.handle, name); if (result == NULL) { result = GLAD_GNUC_EXTENSION (GLADapiproc) userptr.get_proc_address_ptr(name); } return result; } static void* _egl_handle = NULL; static void* glad_egl_dlopen_handle(void) { #if GLAD_PLATFORM_APPLE static const char *NAMES[] = {"libEGL.dylib"}; #elif GLAD_PLATFORM_WIN32 static const char *NAMES[] = {"libEGL.dll", "EGL.dll"}; #else static const char *NAMES[] = {"libEGL.so.1", "libEGL.so"}; #endif if (_egl_handle == NULL) { _egl_handle = glad_get_dlopen_handle(NAMES, sizeof(NAMES) / sizeof(NAMES[0])); } return _egl_handle; } static struct _glad_egl_userptr glad_egl_build_userptr(void *handle) { struct _glad_egl_userptr userptr; userptr.handle = handle; userptr.get_proc_address_ptr = (PFNEGLGETPROCADDRESSPROC) glad_dlsym_handle(handle, "eglGetProcAddress"); return userptr; } int gladLoaderLoadEGL(EGLDisplay display) { int version = 0; void *handle = NULL; int did_load = 0; struct _glad_egl_userptr userptr; did_load = _egl_handle == NULL; handle = glad_egl_dlopen_handle(); if (handle != NULL) { userptr = glad_egl_build_userptr(handle); if (userptr.get_proc_address_ptr != NULL) { version = gladLoadEGLUserPtr(display, glad_egl_get_proc, &userptr); } if (!version && did_load) { gladLoaderUnloadEGL(); } } return version; } void gladLoaderUnloadEGL() { if (_egl_handle != NULL) { glad_close_dlopen_handle(_egl_handle); _egl_handle = NULL; } } #endif /* GLAD_EGL */ #ifdef __cplusplus } #endif ================================================ FILE: third-party/glad/src/gl.c ================================================ #include #include #include #include #ifndef GLAD_IMPL_UTIL_C_ #define GLAD_IMPL_UTIL_C_ #ifdef _MSC_VER #define GLAD_IMPL_UTIL_SSCANF sscanf_s #else #define GLAD_IMPL_UTIL_SSCANF sscanf #endif #endif /* GLAD_IMPL_UTIL_C_ */ #ifdef __cplusplus extern "C" { #endif static void glad_gl_load_GL_VERSION_1_0(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_1_0) return; context->Accum = (PFNGLACCUMPROC) load(userptr, "glAccum"); context->AlphaFunc = (PFNGLALPHAFUNCPROC) load(userptr, "glAlphaFunc"); context->Begin = (PFNGLBEGINPROC) load(userptr, "glBegin"); context->Bitmap = (PFNGLBITMAPPROC) load(userptr, "glBitmap"); context->BlendFunc = (PFNGLBLENDFUNCPROC) load(userptr, "glBlendFunc"); context->CallList = (PFNGLCALLLISTPROC) load(userptr, "glCallList"); context->CallLists = (PFNGLCALLLISTSPROC) load(userptr, "glCallLists"); context->Clear = (PFNGLCLEARPROC) load(userptr, "glClear"); context->ClearAccum = (PFNGLCLEARACCUMPROC) load(userptr, "glClearAccum"); context->ClearColor = (PFNGLCLEARCOLORPROC) load(userptr, "glClearColor"); context->ClearDepth = (PFNGLCLEARDEPTHPROC) load(userptr, "glClearDepth"); context->ClearIndex = (PFNGLCLEARINDEXPROC) load(userptr, "glClearIndex"); context->ClearStencil = (PFNGLCLEARSTENCILPROC) load(userptr, "glClearStencil"); context->ClipPlane = (PFNGLCLIPPLANEPROC) load(userptr, "glClipPlane"); context->Color3b = (PFNGLCOLOR3BPROC) load(userptr, "glColor3b"); context->Color3bv = (PFNGLCOLOR3BVPROC) load(userptr, "glColor3bv"); context->Color3d = (PFNGLCOLOR3DPROC) load(userptr, "glColor3d"); context->Color3dv = (PFNGLCOLOR3DVPROC) load(userptr, "glColor3dv"); context->Color3f = (PFNGLCOLOR3FPROC) load(userptr, "glColor3f"); context->Color3fv = (PFNGLCOLOR3FVPROC) load(userptr, "glColor3fv"); context->Color3i = (PFNGLCOLOR3IPROC) load(userptr, "glColor3i"); context->Color3iv = (PFNGLCOLOR3IVPROC) load(userptr, "glColor3iv"); context->Color3s = (PFNGLCOLOR3SPROC) load(userptr, "glColor3s"); context->Color3sv = (PFNGLCOLOR3SVPROC) load(userptr, "glColor3sv"); context->Color3ub = (PFNGLCOLOR3UBPROC) load(userptr, "glColor3ub"); context->Color3ubv = (PFNGLCOLOR3UBVPROC) load(userptr, "glColor3ubv"); context->Color3ui = (PFNGLCOLOR3UIPROC) load(userptr, "glColor3ui"); context->Color3uiv = (PFNGLCOLOR3UIVPROC) load(userptr, "glColor3uiv"); context->Color3us = (PFNGLCOLOR3USPROC) load(userptr, "glColor3us"); context->Color3usv = (PFNGLCOLOR3USVPROC) load(userptr, "glColor3usv"); context->Color4b = (PFNGLCOLOR4BPROC) load(userptr, "glColor4b"); context->Color4bv = (PFNGLCOLOR4BVPROC) load(userptr, "glColor4bv"); context->Color4d = (PFNGLCOLOR4DPROC) load(userptr, "glColor4d"); context->Color4dv = (PFNGLCOLOR4DVPROC) load(userptr, "glColor4dv"); context->Color4f = (PFNGLCOLOR4FPROC) load(userptr, "glColor4f"); context->Color4fv = (PFNGLCOLOR4FVPROC) load(userptr, "glColor4fv"); context->Color4i = (PFNGLCOLOR4IPROC) load(userptr, "glColor4i"); context->Color4iv = (PFNGLCOLOR4IVPROC) load(userptr, "glColor4iv"); context->Color4s = (PFNGLCOLOR4SPROC) load(userptr, "glColor4s"); context->Color4sv = (PFNGLCOLOR4SVPROC) load(userptr, "glColor4sv"); context->Color4ub = (PFNGLCOLOR4UBPROC) load(userptr, "glColor4ub"); context->Color4ubv = (PFNGLCOLOR4UBVPROC) load(userptr, "glColor4ubv"); context->Color4ui = (PFNGLCOLOR4UIPROC) load(userptr, "glColor4ui"); context->Color4uiv = (PFNGLCOLOR4UIVPROC) load(userptr, "glColor4uiv"); context->Color4us = (PFNGLCOLOR4USPROC) load(userptr, "glColor4us"); context->Color4usv = (PFNGLCOLOR4USVPROC) load(userptr, "glColor4usv"); context->ColorMask = (PFNGLCOLORMASKPROC) load(userptr, "glColorMask"); context->ColorMaterial = (PFNGLCOLORMATERIALPROC) load(userptr, "glColorMaterial"); context->CopyPixels = (PFNGLCOPYPIXELSPROC) load(userptr, "glCopyPixels"); context->CullFace = (PFNGLCULLFACEPROC) load(userptr, "glCullFace"); context->DeleteLists = (PFNGLDELETELISTSPROC) load(userptr, "glDeleteLists"); context->DepthFunc = (PFNGLDEPTHFUNCPROC) load(userptr, "glDepthFunc"); context->DepthMask = (PFNGLDEPTHMASKPROC) load(userptr, "glDepthMask"); context->DepthRange = (PFNGLDEPTHRANGEPROC) load(userptr, "glDepthRange"); context->Disable = (PFNGLDISABLEPROC) load(userptr, "glDisable"); context->DrawBuffer = (PFNGLDRAWBUFFERPROC) load(userptr, "glDrawBuffer"); context->DrawPixels = (PFNGLDRAWPIXELSPROC) load(userptr, "glDrawPixels"); context->EdgeFlag = (PFNGLEDGEFLAGPROC) load(userptr, "glEdgeFlag"); context->EdgeFlagv = (PFNGLEDGEFLAGVPROC) load(userptr, "glEdgeFlagv"); context->Enable = (PFNGLENABLEPROC) load(userptr, "glEnable"); context->End = (PFNGLENDPROC) load(userptr, "glEnd"); context->EndList = (PFNGLENDLISTPROC) load(userptr, "glEndList"); context->EvalCoord1d = (PFNGLEVALCOORD1DPROC) load(userptr, "glEvalCoord1d"); context->EvalCoord1dv = (PFNGLEVALCOORD1DVPROC) load(userptr, "glEvalCoord1dv"); context->EvalCoord1f = (PFNGLEVALCOORD1FPROC) load(userptr, "glEvalCoord1f"); context->EvalCoord1fv = (PFNGLEVALCOORD1FVPROC) load(userptr, "glEvalCoord1fv"); context->EvalCoord2d = (PFNGLEVALCOORD2DPROC) load(userptr, "glEvalCoord2d"); context->EvalCoord2dv = (PFNGLEVALCOORD2DVPROC) load(userptr, "glEvalCoord2dv"); context->EvalCoord2f = (PFNGLEVALCOORD2FPROC) load(userptr, "glEvalCoord2f"); context->EvalCoord2fv = (PFNGLEVALCOORD2FVPROC) load(userptr, "glEvalCoord2fv"); context->EvalMesh1 = (PFNGLEVALMESH1PROC) load(userptr, "glEvalMesh1"); context->EvalMesh2 = (PFNGLEVALMESH2PROC) load(userptr, "glEvalMesh2"); context->EvalPoint1 = (PFNGLEVALPOINT1PROC) load(userptr, "glEvalPoint1"); context->EvalPoint2 = (PFNGLEVALPOINT2PROC) load(userptr, "glEvalPoint2"); context->FeedbackBuffer = (PFNGLFEEDBACKBUFFERPROC) load(userptr, "glFeedbackBuffer"); context->Finish = (PFNGLFINISHPROC) load(userptr, "glFinish"); context->Flush = (PFNGLFLUSHPROC) load(userptr, "glFlush"); context->Fogf = (PFNGLFOGFPROC) load(userptr, "glFogf"); context->Fogfv = (PFNGLFOGFVPROC) load(userptr, "glFogfv"); context->Fogi = (PFNGLFOGIPROC) load(userptr, "glFogi"); context->Fogiv = (PFNGLFOGIVPROC) load(userptr, "glFogiv"); context->FrontFace = (PFNGLFRONTFACEPROC) load(userptr, "glFrontFace"); context->Frustum = (PFNGLFRUSTUMPROC) load(userptr, "glFrustum"); context->GenLists = (PFNGLGENLISTSPROC) load(userptr, "glGenLists"); context->GetBooleanv = (PFNGLGETBOOLEANVPROC) load(userptr, "glGetBooleanv"); context->GetClipPlane = (PFNGLGETCLIPPLANEPROC) load(userptr, "glGetClipPlane"); context->GetDoublev = (PFNGLGETDOUBLEVPROC) load(userptr, "glGetDoublev"); context->GetError = (PFNGLGETERRORPROC) load(userptr, "glGetError"); context->GetFloatv = (PFNGLGETFLOATVPROC) load(userptr, "glGetFloatv"); context->GetIntegerv = (PFNGLGETINTEGERVPROC) load(userptr, "glGetIntegerv"); context->GetLightfv = (PFNGLGETLIGHTFVPROC) load(userptr, "glGetLightfv"); context->GetLightiv = (PFNGLGETLIGHTIVPROC) load(userptr, "glGetLightiv"); context->GetMapdv = (PFNGLGETMAPDVPROC) load(userptr, "glGetMapdv"); context->GetMapfv = (PFNGLGETMAPFVPROC) load(userptr, "glGetMapfv"); context->GetMapiv = (PFNGLGETMAPIVPROC) load(userptr, "glGetMapiv"); context->GetMaterialfv = (PFNGLGETMATERIALFVPROC) load(userptr, "glGetMaterialfv"); context->GetMaterialiv = (PFNGLGETMATERIALIVPROC) load(userptr, "glGetMaterialiv"); context->GetPixelMapfv = (PFNGLGETPIXELMAPFVPROC) load(userptr, "glGetPixelMapfv"); context->GetPixelMapuiv = (PFNGLGETPIXELMAPUIVPROC) load(userptr, "glGetPixelMapuiv"); context->GetPixelMapusv = (PFNGLGETPIXELMAPUSVPROC) load(userptr, "glGetPixelMapusv"); context->GetPolygonStipple = (PFNGLGETPOLYGONSTIPPLEPROC) load(userptr, "glGetPolygonStipple"); context->GetString = (PFNGLGETSTRINGPROC) load(userptr, "glGetString"); context->GetTexEnvfv = (PFNGLGETTEXENVFVPROC) load(userptr, "glGetTexEnvfv"); context->GetTexEnviv = (PFNGLGETTEXENVIVPROC) load(userptr, "glGetTexEnviv"); context->GetTexGendv = (PFNGLGETTEXGENDVPROC) load(userptr, "glGetTexGendv"); context->GetTexGenfv = (PFNGLGETTEXGENFVPROC) load(userptr, "glGetTexGenfv"); context->GetTexGeniv = (PFNGLGETTEXGENIVPROC) load(userptr, "glGetTexGeniv"); context->GetTexImage = (PFNGLGETTEXIMAGEPROC) load(userptr, "glGetTexImage"); context->GetTexLevelParameterfv = (PFNGLGETTEXLEVELPARAMETERFVPROC) load(userptr, "glGetTexLevelParameterfv"); context->GetTexLevelParameteriv = (PFNGLGETTEXLEVELPARAMETERIVPROC) load(userptr, "glGetTexLevelParameteriv"); context->GetTexParameterfv = (PFNGLGETTEXPARAMETERFVPROC) load(userptr, "glGetTexParameterfv"); context->GetTexParameteriv = (PFNGLGETTEXPARAMETERIVPROC) load(userptr, "glGetTexParameteriv"); context->Hint = (PFNGLHINTPROC) load(userptr, "glHint"); context->IndexMask = (PFNGLINDEXMASKPROC) load(userptr, "glIndexMask"); context->Indexd = (PFNGLINDEXDPROC) load(userptr, "glIndexd"); context->Indexdv = (PFNGLINDEXDVPROC) load(userptr, "glIndexdv"); context->Indexf = (PFNGLINDEXFPROC) load(userptr, "glIndexf"); context->Indexfv = (PFNGLINDEXFVPROC) load(userptr, "glIndexfv"); context->Indexi = (PFNGLINDEXIPROC) load(userptr, "glIndexi"); context->Indexiv = (PFNGLINDEXIVPROC) load(userptr, "glIndexiv"); context->Indexs = (PFNGLINDEXSPROC) load(userptr, "glIndexs"); context->Indexsv = (PFNGLINDEXSVPROC) load(userptr, "glIndexsv"); context->InitNames = (PFNGLINITNAMESPROC) load(userptr, "glInitNames"); context->IsEnabled = (PFNGLISENABLEDPROC) load(userptr, "glIsEnabled"); context->IsList = (PFNGLISLISTPROC) load(userptr, "glIsList"); context->LightModelf = (PFNGLLIGHTMODELFPROC) load(userptr, "glLightModelf"); context->LightModelfv = (PFNGLLIGHTMODELFVPROC) load(userptr, "glLightModelfv"); context->LightModeli = (PFNGLLIGHTMODELIPROC) load(userptr, "glLightModeli"); context->LightModeliv = (PFNGLLIGHTMODELIVPROC) load(userptr, "glLightModeliv"); context->Lightf = (PFNGLLIGHTFPROC) load(userptr, "glLightf"); context->Lightfv = (PFNGLLIGHTFVPROC) load(userptr, "glLightfv"); context->Lighti = (PFNGLLIGHTIPROC) load(userptr, "glLighti"); context->Lightiv = (PFNGLLIGHTIVPROC) load(userptr, "glLightiv"); context->LineStipple = (PFNGLLINESTIPPLEPROC) load(userptr, "glLineStipple"); context->LineWidth = (PFNGLLINEWIDTHPROC) load(userptr, "glLineWidth"); context->ListBase = (PFNGLLISTBASEPROC) load(userptr, "glListBase"); context->LoadIdentity = (PFNGLLOADIDENTITYPROC) load(userptr, "glLoadIdentity"); context->LoadMatrixd = (PFNGLLOADMATRIXDPROC) load(userptr, "glLoadMatrixd"); context->LoadMatrixf = (PFNGLLOADMATRIXFPROC) load(userptr, "glLoadMatrixf"); context->LoadName = (PFNGLLOADNAMEPROC) load(userptr, "glLoadName"); context->LogicOp = (PFNGLLOGICOPPROC) load(userptr, "glLogicOp"); context->Map1d = (PFNGLMAP1DPROC) load(userptr, "glMap1d"); context->Map1f = (PFNGLMAP1FPROC) load(userptr, "glMap1f"); context->Map2d = (PFNGLMAP2DPROC) load(userptr, "glMap2d"); context->Map2f = (PFNGLMAP2FPROC) load(userptr, "glMap2f"); context->MapGrid1d = (PFNGLMAPGRID1DPROC) load(userptr, "glMapGrid1d"); context->MapGrid1f = (PFNGLMAPGRID1FPROC) load(userptr, "glMapGrid1f"); context->MapGrid2d = (PFNGLMAPGRID2DPROC) load(userptr, "glMapGrid2d"); context->MapGrid2f = (PFNGLMAPGRID2FPROC) load(userptr, "glMapGrid2f"); context->Materialf = (PFNGLMATERIALFPROC) load(userptr, "glMaterialf"); context->Materialfv = (PFNGLMATERIALFVPROC) load(userptr, "glMaterialfv"); context->Materiali = (PFNGLMATERIALIPROC) load(userptr, "glMateriali"); context->Materialiv = (PFNGLMATERIALIVPROC) load(userptr, "glMaterialiv"); context->MatrixMode = (PFNGLMATRIXMODEPROC) load(userptr, "glMatrixMode"); context->MultMatrixd = (PFNGLMULTMATRIXDPROC) load(userptr, "glMultMatrixd"); context->MultMatrixf = (PFNGLMULTMATRIXFPROC) load(userptr, "glMultMatrixf"); context->NewList = (PFNGLNEWLISTPROC) load(userptr, "glNewList"); context->Normal3b = (PFNGLNORMAL3BPROC) load(userptr, "glNormal3b"); context->Normal3bv = (PFNGLNORMAL3BVPROC) load(userptr, "glNormal3bv"); context->Normal3d = (PFNGLNORMAL3DPROC) load(userptr, "glNormal3d"); context->Normal3dv = (PFNGLNORMAL3DVPROC) load(userptr, "glNormal3dv"); context->Normal3f = (PFNGLNORMAL3FPROC) load(userptr, "glNormal3f"); context->Normal3fv = (PFNGLNORMAL3FVPROC) load(userptr, "glNormal3fv"); context->Normal3i = (PFNGLNORMAL3IPROC) load(userptr, "glNormal3i"); context->Normal3iv = (PFNGLNORMAL3IVPROC) load(userptr, "glNormal3iv"); context->Normal3s = (PFNGLNORMAL3SPROC) load(userptr, "glNormal3s"); context->Normal3sv = (PFNGLNORMAL3SVPROC) load(userptr, "glNormal3sv"); context->Ortho = (PFNGLORTHOPROC) load(userptr, "glOrtho"); context->PassThrough = (PFNGLPASSTHROUGHPROC) load(userptr, "glPassThrough"); context->PixelMapfv = (PFNGLPIXELMAPFVPROC) load(userptr, "glPixelMapfv"); context->PixelMapuiv = (PFNGLPIXELMAPUIVPROC) load(userptr, "glPixelMapuiv"); context->PixelMapusv = (PFNGLPIXELMAPUSVPROC) load(userptr, "glPixelMapusv"); context->PixelStoref = (PFNGLPIXELSTOREFPROC) load(userptr, "glPixelStoref"); context->PixelStorei = (PFNGLPIXELSTOREIPROC) load(userptr, "glPixelStorei"); context->PixelTransferf = (PFNGLPIXELTRANSFERFPROC) load(userptr, "glPixelTransferf"); context->PixelTransferi = (PFNGLPIXELTRANSFERIPROC) load(userptr, "glPixelTransferi"); context->PixelZoom = (PFNGLPIXELZOOMPROC) load(userptr, "glPixelZoom"); context->PointSize = (PFNGLPOINTSIZEPROC) load(userptr, "glPointSize"); context->PolygonMode = (PFNGLPOLYGONMODEPROC) load(userptr, "glPolygonMode"); context->PolygonStipple = (PFNGLPOLYGONSTIPPLEPROC) load(userptr, "glPolygonStipple"); context->PopAttrib = (PFNGLPOPATTRIBPROC) load(userptr, "glPopAttrib"); context->PopMatrix = (PFNGLPOPMATRIXPROC) load(userptr, "glPopMatrix"); context->PopName = (PFNGLPOPNAMEPROC) load(userptr, "glPopName"); context->PushAttrib = (PFNGLPUSHATTRIBPROC) load(userptr, "glPushAttrib"); context->PushMatrix = (PFNGLPUSHMATRIXPROC) load(userptr, "glPushMatrix"); context->PushName = (PFNGLPUSHNAMEPROC) load(userptr, "glPushName"); context->RasterPos2d = (PFNGLRASTERPOS2DPROC) load(userptr, "glRasterPos2d"); context->RasterPos2dv = (PFNGLRASTERPOS2DVPROC) load(userptr, "glRasterPos2dv"); context->RasterPos2f = (PFNGLRASTERPOS2FPROC) load(userptr, "glRasterPos2f"); context->RasterPos2fv = (PFNGLRASTERPOS2FVPROC) load(userptr, "glRasterPos2fv"); context->RasterPos2i = (PFNGLRASTERPOS2IPROC) load(userptr, "glRasterPos2i"); context->RasterPos2iv = (PFNGLRASTERPOS2IVPROC) load(userptr, "glRasterPos2iv"); context->RasterPos2s = (PFNGLRASTERPOS2SPROC) load(userptr, "glRasterPos2s"); context->RasterPos2sv = (PFNGLRASTERPOS2SVPROC) load(userptr, "glRasterPos2sv"); context->RasterPos3d = (PFNGLRASTERPOS3DPROC) load(userptr, "glRasterPos3d"); context->RasterPos3dv = (PFNGLRASTERPOS3DVPROC) load(userptr, "glRasterPos3dv"); context->RasterPos3f = (PFNGLRASTERPOS3FPROC) load(userptr, "glRasterPos3f"); context->RasterPos3fv = (PFNGLRASTERPOS3FVPROC) load(userptr, "glRasterPos3fv"); context->RasterPos3i = (PFNGLRASTERPOS3IPROC) load(userptr, "glRasterPos3i"); context->RasterPos3iv = (PFNGLRASTERPOS3IVPROC) load(userptr, "glRasterPos3iv"); context->RasterPos3s = (PFNGLRASTERPOS3SPROC) load(userptr, "glRasterPos3s"); context->RasterPos3sv = (PFNGLRASTERPOS3SVPROC) load(userptr, "glRasterPos3sv"); context->RasterPos4d = (PFNGLRASTERPOS4DPROC) load(userptr, "glRasterPos4d"); context->RasterPos4dv = (PFNGLRASTERPOS4DVPROC) load(userptr, "glRasterPos4dv"); context->RasterPos4f = (PFNGLRASTERPOS4FPROC) load(userptr, "glRasterPos4f"); context->RasterPos4fv = (PFNGLRASTERPOS4FVPROC) load(userptr, "glRasterPos4fv"); context->RasterPos4i = (PFNGLRASTERPOS4IPROC) load(userptr, "glRasterPos4i"); context->RasterPos4iv = (PFNGLRASTERPOS4IVPROC) load(userptr, "glRasterPos4iv"); context->RasterPos4s = (PFNGLRASTERPOS4SPROC) load(userptr, "glRasterPos4s"); context->RasterPos4sv = (PFNGLRASTERPOS4SVPROC) load(userptr, "glRasterPos4sv"); context->ReadBuffer = (PFNGLREADBUFFERPROC) load(userptr, "glReadBuffer"); context->ReadPixels = (PFNGLREADPIXELSPROC) load(userptr, "glReadPixels"); context->Rectd = (PFNGLRECTDPROC) load(userptr, "glRectd"); context->Rectdv = (PFNGLRECTDVPROC) load(userptr, "glRectdv"); context->Rectf = (PFNGLRECTFPROC) load(userptr, "glRectf"); context->Rectfv = (PFNGLRECTFVPROC) load(userptr, "glRectfv"); context->Recti = (PFNGLRECTIPROC) load(userptr, "glRecti"); context->Rectiv = (PFNGLRECTIVPROC) load(userptr, "glRectiv"); context->Rects = (PFNGLRECTSPROC) load(userptr, "glRects"); context->Rectsv = (PFNGLRECTSVPROC) load(userptr, "glRectsv"); context->RenderMode = (PFNGLRENDERMODEPROC) load(userptr, "glRenderMode"); context->Rotated = (PFNGLROTATEDPROC) load(userptr, "glRotated"); context->Rotatef = (PFNGLROTATEFPROC) load(userptr, "glRotatef"); context->Scaled = (PFNGLSCALEDPROC) load(userptr, "glScaled"); context->Scalef = (PFNGLSCALEFPROC) load(userptr, "glScalef"); context->Scissor = (PFNGLSCISSORPROC) load(userptr, "glScissor"); context->SelectBuffer = (PFNGLSELECTBUFFERPROC) load(userptr, "glSelectBuffer"); context->ShadeModel = (PFNGLSHADEMODELPROC) load(userptr, "glShadeModel"); context->StencilFunc = (PFNGLSTENCILFUNCPROC) load(userptr, "glStencilFunc"); context->StencilMask = (PFNGLSTENCILMASKPROC) load(userptr, "glStencilMask"); context->StencilOp = (PFNGLSTENCILOPPROC) load(userptr, "glStencilOp"); context->TexCoord1d = (PFNGLTEXCOORD1DPROC) load(userptr, "glTexCoord1d"); context->TexCoord1dv = (PFNGLTEXCOORD1DVPROC) load(userptr, "glTexCoord1dv"); context->TexCoord1f = (PFNGLTEXCOORD1FPROC) load(userptr, "glTexCoord1f"); context->TexCoord1fv = (PFNGLTEXCOORD1FVPROC) load(userptr, "glTexCoord1fv"); context->TexCoord1i = (PFNGLTEXCOORD1IPROC) load(userptr, "glTexCoord1i"); context->TexCoord1iv = (PFNGLTEXCOORD1IVPROC) load(userptr, "glTexCoord1iv"); context->TexCoord1s = (PFNGLTEXCOORD1SPROC) load(userptr, "glTexCoord1s"); context->TexCoord1sv = (PFNGLTEXCOORD1SVPROC) load(userptr, "glTexCoord1sv"); context->TexCoord2d = (PFNGLTEXCOORD2DPROC) load(userptr, "glTexCoord2d"); context->TexCoord2dv = (PFNGLTEXCOORD2DVPROC) load(userptr, "glTexCoord2dv"); context->TexCoord2f = (PFNGLTEXCOORD2FPROC) load(userptr, "glTexCoord2f"); context->TexCoord2fv = (PFNGLTEXCOORD2FVPROC) load(userptr, "glTexCoord2fv"); context->TexCoord2i = (PFNGLTEXCOORD2IPROC) load(userptr, "glTexCoord2i"); context->TexCoord2iv = (PFNGLTEXCOORD2IVPROC) load(userptr, "glTexCoord2iv"); context->TexCoord2s = (PFNGLTEXCOORD2SPROC) load(userptr, "glTexCoord2s"); context->TexCoord2sv = (PFNGLTEXCOORD2SVPROC) load(userptr, "glTexCoord2sv"); context->TexCoord3d = (PFNGLTEXCOORD3DPROC) load(userptr, "glTexCoord3d"); context->TexCoord3dv = (PFNGLTEXCOORD3DVPROC) load(userptr, "glTexCoord3dv"); context->TexCoord3f = (PFNGLTEXCOORD3FPROC) load(userptr, "glTexCoord3f"); context->TexCoord3fv = (PFNGLTEXCOORD3FVPROC) load(userptr, "glTexCoord3fv"); context->TexCoord3i = (PFNGLTEXCOORD3IPROC) load(userptr, "glTexCoord3i"); context->TexCoord3iv = (PFNGLTEXCOORD3IVPROC) load(userptr, "glTexCoord3iv"); context->TexCoord3s = (PFNGLTEXCOORD3SPROC) load(userptr, "glTexCoord3s"); context->TexCoord3sv = (PFNGLTEXCOORD3SVPROC) load(userptr, "glTexCoord3sv"); context->TexCoord4d = (PFNGLTEXCOORD4DPROC) load(userptr, "glTexCoord4d"); context->TexCoord4dv = (PFNGLTEXCOORD4DVPROC) load(userptr, "glTexCoord4dv"); context->TexCoord4f = (PFNGLTEXCOORD4FPROC) load(userptr, "glTexCoord4f"); context->TexCoord4fv = (PFNGLTEXCOORD4FVPROC) load(userptr, "glTexCoord4fv"); context->TexCoord4i = (PFNGLTEXCOORD4IPROC) load(userptr, "glTexCoord4i"); context->TexCoord4iv = (PFNGLTEXCOORD4IVPROC) load(userptr, "glTexCoord4iv"); context->TexCoord4s = (PFNGLTEXCOORD4SPROC) load(userptr, "glTexCoord4s"); context->TexCoord4sv = (PFNGLTEXCOORD4SVPROC) load(userptr, "glTexCoord4sv"); context->TexEnvf = (PFNGLTEXENVFPROC) load(userptr, "glTexEnvf"); context->TexEnvfv = (PFNGLTEXENVFVPROC) load(userptr, "glTexEnvfv"); context->TexEnvi = (PFNGLTEXENVIPROC) load(userptr, "glTexEnvi"); context->TexEnviv = (PFNGLTEXENVIVPROC) load(userptr, "glTexEnviv"); context->TexGend = (PFNGLTEXGENDPROC) load(userptr, "glTexGend"); context->TexGendv = (PFNGLTEXGENDVPROC) load(userptr, "glTexGendv"); context->TexGenf = (PFNGLTEXGENFPROC) load(userptr, "glTexGenf"); context->TexGenfv = (PFNGLTEXGENFVPROC) load(userptr, "glTexGenfv"); context->TexGeni = (PFNGLTEXGENIPROC) load(userptr, "glTexGeni"); context->TexGeniv = (PFNGLTEXGENIVPROC) load(userptr, "glTexGeniv"); context->TexImage1D = (PFNGLTEXIMAGE1DPROC) load(userptr, "glTexImage1D"); context->TexImage2D = (PFNGLTEXIMAGE2DPROC) load(userptr, "glTexImage2D"); context->TexParameterf = (PFNGLTEXPARAMETERFPROC) load(userptr, "glTexParameterf"); context->TexParameterfv = (PFNGLTEXPARAMETERFVPROC) load(userptr, "glTexParameterfv"); context->TexParameteri = (PFNGLTEXPARAMETERIPROC) load(userptr, "glTexParameteri"); context->TexParameteriv = (PFNGLTEXPARAMETERIVPROC) load(userptr, "glTexParameteriv"); context->Translated = (PFNGLTRANSLATEDPROC) load(userptr, "glTranslated"); context->Translatef = (PFNGLTRANSLATEFPROC) load(userptr, "glTranslatef"); context->Vertex2d = (PFNGLVERTEX2DPROC) load(userptr, "glVertex2d"); context->Vertex2dv = (PFNGLVERTEX2DVPROC) load(userptr, "glVertex2dv"); context->Vertex2f = (PFNGLVERTEX2FPROC) load(userptr, "glVertex2f"); context->Vertex2fv = (PFNGLVERTEX2FVPROC) load(userptr, "glVertex2fv"); context->Vertex2i = (PFNGLVERTEX2IPROC) load(userptr, "glVertex2i"); context->Vertex2iv = (PFNGLVERTEX2IVPROC) load(userptr, "glVertex2iv"); context->Vertex2s = (PFNGLVERTEX2SPROC) load(userptr, "glVertex2s"); context->Vertex2sv = (PFNGLVERTEX2SVPROC) load(userptr, "glVertex2sv"); context->Vertex3d = (PFNGLVERTEX3DPROC) load(userptr, "glVertex3d"); context->Vertex3dv = (PFNGLVERTEX3DVPROC) load(userptr, "glVertex3dv"); context->Vertex3f = (PFNGLVERTEX3FPROC) load(userptr, "glVertex3f"); context->Vertex3fv = (PFNGLVERTEX3FVPROC) load(userptr, "glVertex3fv"); context->Vertex3i = (PFNGLVERTEX3IPROC) load(userptr, "glVertex3i"); context->Vertex3iv = (PFNGLVERTEX3IVPROC) load(userptr, "glVertex3iv"); context->Vertex3s = (PFNGLVERTEX3SPROC) load(userptr, "glVertex3s"); context->Vertex3sv = (PFNGLVERTEX3SVPROC) load(userptr, "glVertex3sv"); context->Vertex4d = (PFNGLVERTEX4DPROC) load(userptr, "glVertex4d"); context->Vertex4dv = (PFNGLVERTEX4DVPROC) load(userptr, "glVertex4dv"); context->Vertex4f = (PFNGLVERTEX4FPROC) load(userptr, "glVertex4f"); context->Vertex4fv = (PFNGLVERTEX4FVPROC) load(userptr, "glVertex4fv"); context->Vertex4i = (PFNGLVERTEX4IPROC) load(userptr, "glVertex4i"); context->Vertex4iv = (PFNGLVERTEX4IVPROC) load(userptr, "glVertex4iv"); context->Vertex4s = (PFNGLVERTEX4SPROC) load(userptr, "glVertex4s"); context->Vertex4sv = (PFNGLVERTEX4SVPROC) load(userptr, "glVertex4sv"); context->Viewport = (PFNGLVIEWPORTPROC) load(userptr, "glViewport"); } static void glad_gl_load_GL_VERSION_1_1(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_1_1) return; context->AreTexturesResident = (PFNGLARETEXTURESRESIDENTPROC) load(userptr, "glAreTexturesResident"); context->ArrayElement = (PFNGLARRAYELEMENTPROC) load(userptr, "glArrayElement"); context->BindTexture = (PFNGLBINDTEXTUREPROC) load(userptr, "glBindTexture"); context->ColorPointer = (PFNGLCOLORPOINTERPROC) load(userptr, "glColorPointer"); context->CopyTexImage1D = (PFNGLCOPYTEXIMAGE1DPROC) load(userptr, "glCopyTexImage1D"); context->CopyTexImage2D = (PFNGLCOPYTEXIMAGE2DPROC) load(userptr, "glCopyTexImage2D"); context->CopyTexSubImage1D = (PFNGLCOPYTEXSUBIMAGE1DPROC) load(userptr, "glCopyTexSubImage1D"); context->CopyTexSubImage2D = (PFNGLCOPYTEXSUBIMAGE2DPROC) load(userptr, "glCopyTexSubImage2D"); context->DeleteTextures = (PFNGLDELETETEXTURESPROC) load(userptr, "glDeleteTextures"); context->DisableClientState = (PFNGLDISABLECLIENTSTATEPROC) load(userptr, "glDisableClientState"); context->DrawArrays = (PFNGLDRAWARRAYSPROC) load(userptr, "glDrawArrays"); context->DrawElements = (PFNGLDRAWELEMENTSPROC) load(userptr, "glDrawElements"); context->EdgeFlagPointer = (PFNGLEDGEFLAGPOINTERPROC) load(userptr, "glEdgeFlagPointer"); context->EnableClientState = (PFNGLENABLECLIENTSTATEPROC) load(userptr, "glEnableClientState"); context->GenTextures = (PFNGLGENTEXTURESPROC) load(userptr, "glGenTextures"); context->GetPointerv = (PFNGLGETPOINTERVPROC) load(userptr, "glGetPointerv"); context->IndexPointer = (PFNGLINDEXPOINTERPROC) load(userptr, "glIndexPointer"); context->Indexub = (PFNGLINDEXUBPROC) load(userptr, "glIndexub"); context->Indexubv = (PFNGLINDEXUBVPROC) load(userptr, "glIndexubv"); context->InterleavedArrays = (PFNGLINTERLEAVEDARRAYSPROC) load(userptr, "glInterleavedArrays"); context->IsTexture = (PFNGLISTEXTUREPROC) load(userptr, "glIsTexture"); context->NormalPointer = (PFNGLNORMALPOINTERPROC) load(userptr, "glNormalPointer"); context->PolygonOffset = (PFNGLPOLYGONOFFSETPROC) load(userptr, "glPolygonOffset"); context->PopClientAttrib = (PFNGLPOPCLIENTATTRIBPROC) load(userptr, "glPopClientAttrib"); context->PrioritizeTextures = (PFNGLPRIORITIZETEXTURESPROC) load(userptr, "glPrioritizeTextures"); context->PushClientAttrib = (PFNGLPUSHCLIENTATTRIBPROC) load(userptr, "glPushClientAttrib"); context->TexCoordPointer = (PFNGLTEXCOORDPOINTERPROC) load(userptr, "glTexCoordPointer"); context->TexSubImage1D = (PFNGLTEXSUBIMAGE1DPROC) load(userptr, "glTexSubImage1D"); context->TexSubImage2D = (PFNGLTEXSUBIMAGE2DPROC) load(userptr, "glTexSubImage2D"); context->VertexPointer = (PFNGLVERTEXPOINTERPROC) load(userptr, "glVertexPointer"); } static void glad_gl_load_GL_VERSION_1_2(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_1_2) return; context->CopyTexSubImage3D = (PFNGLCOPYTEXSUBIMAGE3DPROC) load(userptr, "glCopyTexSubImage3D"); context->DrawRangeElements = (PFNGLDRAWRANGEELEMENTSPROC) load(userptr, "glDrawRangeElements"); context->TexImage3D = (PFNGLTEXIMAGE3DPROC) load(userptr, "glTexImage3D"); context->TexSubImage3D = (PFNGLTEXSUBIMAGE3DPROC) load(userptr, "glTexSubImage3D"); } static void glad_gl_load_GL_VERSION_1_3(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_1_3) return; context->ActiveTexture = (PFNGLACTIVETEXTUREPROC) load(userptr, "glActiveTexture"); context->ClientActiveTexture = (PFNGLCLIENTACTIVETEXTUREPROC) load(userptr, "glClientActiveTexture"); context->CompressedTexImage1D = (PFNGLCOMPRESSEDTEXIMAGE1DPROC) load(userptr, "glCompressedTexImage1D"); context->CompressedTexImage2D = (PFNGLCOMPRESSEDTEXIMAGE2DPROC) load(userptr, "glCompressedTexImage2D"); context->CompressedTexImage3D = (PFNGLCOMPRESSEDTEXIMAGE3DPROC) load(userptr, "glCompressedTexImage3D"); context->CompressedTexSubImage1D = (PFNGLCOMPRESSEDTEXSUBIMAGE1DPROC) load(userptr, "glCompressedTexSubImage1D"); context->CompressedTexSubImage2D = (PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC) load(userptr, "glCompressedTexSubImage2D"); context->CompressedTexSubImage3D = (PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC) load(userptr, "glCompressedTexSubImage3D"); context->GetCompressedTexImage = (PFNGLGETCOMPRESSEDTEXIMAGEPROC) load(userptr, "glGetCompressedTexImage"); context->LoadTransposeMatrixd = (PFNGLLOADTRANSPOSEMATRIXDPROC) load(userptr, "glLoadTransposeMatrixd"); context->LoadTransposeMatrixf = (PFNGLLOADTRANSPOSEMATRIXFPROC) load(userptr, "glLoadTransposeMatrixf"); context->MultTransposeMatrixd = (PFNGLMULTTRANSPOSEMATRIXDPROC) load(userptr, "glMultTransposeMatrixd"); context->MultTransposeMatrixf = (PFNGLMULTTRANSPOSEMATRIXFPROC) load(userptr, "glMultTransposeMatrixf"); context->MultiTexCoord1d = (PFNGLMULTITEXCOORD1DPROC) load(userptr, "glMultiTexCoord1d"); context->MultiTexCoord1dv = (PFNGLMULTITEXCOORD1DVPROC) load(userptr, "glMultiTexCoord1dv"); context->MultiTexCoord1f = (PFNGLMULTITEXCOORD1FPROC) load(userptr, "glMultiTexCoord1f"); context->MultiTexCoord1fv = (PFNGLMULTITEXCOORD1FVPROC) load(userptr, "glMultiTexCoord1fv"); context->MultiTexCoord1i = (PFNGLMULTITEXCOORD1IPROC) load(userptr, "glMultiTexCoord1i"); context->MultiTexCoord1iv = (PFNGLMULTITEXCOORD1IVPROC) load(userptr, "glMultiTexCoord1iv"); context->MultiTexCoord1s = (PFNGLMULTITEXCOORD1SPROC) load(userptr, "glMultiTexCoord1s"); context->MultiTexCoord1sv = (PFNGLMULTITEXCOORD1SVPROC) load(userptr, "glMultiTexCoord1sv"); context->MultiTexCoord2d = (PFNGLMULTITEXCOORD2DPROC) load(userptr, "glMultiTexCoord2d"); context->MultiTexCoord2dv = (PFNGLMULTITEXCOORD2DVPROC) load(userptr, "glMultiTexCoord2dv"); context->MultiTexCoord2f = (PFNGLMULTITEXCOORD2FPROC) load(userptr, "glMultiTexCoord2f"); context->MultiTexCoord2fv = (PFNGLMULTITEXCOORD2FVPROC) load(userptr, "glMultiTexCoord2fv"); context->MultiTexCoord2i = (PFNGLMULTITEXCOORD2IPROC) load(userptr, "glMultiTexCoord2i"); context->MultiTexCoord2iv = (PFNGLMULTITEXCOORD2IVPROC) load(userptr, "glMultiTexCoord2iv"); context->MultiTexCoord2s = (PFNGLMULTITEXCOORD2SPROC) load(userptr, "glMultiTexCoord2s"); context->MultiTexCoord2sv = (PFNGLMULTITEXCOORD2SVPROC) load(userptr, "glMultiTexCoord2sv"); context->MultiTexCoord3d = (PFNGLMULTITEXCOORD3DPROC) load(userptr, "glMultiTexCoord3d"); context->MultiTexCoord3dv = (PFNGLMULTITEXCOORD3DVPROC) load(userptr, "glMultiTexCoord3dv"); context->MultiTexCoord3f = (PFNGLMULTITEXCOORD3FPROC) load(userptr, "glMultiTexCoord3f"); context->MultiTexCoord3fv = (PFNGLMULTITEXCOORD3FVPROC) load(userptr, "glMultiTexCoord3fv"); context->MultiTexCoord3i = (PFNGLMULTITEXCOORD3IPROC) load(userptr, "glMultiTexCoord3i"); context->MultiTexCoord3iv = (PFNGLMULTITEXCOORD3IVPROC) load(userptr, "glMultiTexCoord3iv"); context->MultiTexCoord3s = (PFNGLMULTITEXCOORD3SPROC) load(userptr, "glMultiTexCoord3s"); context->MultiTexCoord3sv = (PFNGLMULTITEXCOORD3SVPROC) load(userptr, "glMultiTexCoord3sv"); context->MultiTexCoord4d = (PFNGLMULTITEXCOORD4DPROC) load(userptr, "glMultiTexCoord4d"); context->MultiTexCoord4dv = (PFNGLMULTITEXCOORD4DVPROC) load(userptr, "glMultiTexCoord4dv"); context->MultiTexCoord4f = (PFNGLMULTITEXCOORD4FPROC) load(userptr, "glMultiTexCoord4f"); context->MultiTexCoord4fv = (PFNGLMULTITEXCOORD4FVPROC) load(userptr, "glMultiTexCoord4fv"); context->MultiTexCoord4i = (PFNGLMULTITEXCOORD4IPROC) load(userptr, "glMultiTexCoord4i"); context->MultiTexCoord4iv = (PFNGLMULTITEXCOORD4IVPROC) load(userptr, "glMultiTexCoord4iv"); context->MultiTexCoord4s = (PFNGLMULTITEXCOORD4SPROC) load(userptr, "glMultiTexCoord4s"); context->MultiTexCoord4sv = (PFNGLMULTITEXCOORD4SVPROC) load(userptr, "glMultiTexCoord4sv"); context->SampleCoverage = (PFNGLSAMPLECOVERAGEPROC) load(userptr, "glSampleCoverage"); } static void glad_gl_load_GL_VERSION_1_4(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_1_4) return; context->BlendColor = (PFNGLBLENDCOLORPROC) load(userptr, "glBlendColor"); context->BlendEquation = (PFNGLBLENDEQUATIONPROC) load(userptr, "glBlendEquation"); context->BlendFuncSeparate = (PFNGLBLENDFUNCSEPARATEPROC) load(userptr, "glBlendFuncSeparate"); context->FogCoordPointer = (PFNGLFOGCOORDPOINTERPROC) load(userptr, "glFogCoordPointer"); context->FogCoordd = (PFNGLFOGCOORDDPROC) load(userptr, "glFogCoordd"); context->FogCoorddv = (PFNGLFOGCOORDDVPROC) load(userptr, "glFogCoorddv"); context->FogCoordf = (PFNGLFOGCOORDFPROC) load(userptr, "glFogCoordf"); context->FogCoordfv = (PFNGLFOGCOORDFVPROC) load(userptr, "glFogCoordfv"); context->MultiDrawArrays = (PFNGLMULTIDRAWARRAYSPROC) load(userptr, "glMultiDrawArrays"); context->MultiDrawElements = (PFNGLMULTIDRAWELEMENTSPROC) load(userptr, "glMultiDrawElements"); context->PointParameterf = (PFNGLPOINTPARAMETERFPROC) load(userptr, "glPointParameterf"); context->PointParameterfv = (PFNGLPOINTPARAMETERFVPROC) load(userptr, "glPointParameterfv"); context->PointParameteri = (PFNGLPOINTPARAMETERIPROC) load(userptr, "glPointParameteri"); context->PointParameteriv = (PFNGLPOINTPARAMETERIVPROC) load(userptr, "glPointParameteriv"); context->SecondaryColor3b = (PFNGLSECONDARYCOLOR3BPROC) load(userptr, "glSecondaryColor3b"); context->SecondaryColor3bv = (PFNGLSECONDARYCOLOR3BVPROC) load(userptr, "glSecondaryColor3bv"); context->SecondaryColor3d = (PFNGLSECONDARYCOLOR3DPROC) load(userptr, "glSecondaryColor3d"); context->SecondaryColor3dv = (PFNGLSECONDARYCOLOR3DVPROC) load(userptr, "glSecondaryColor3dv"); context->SecondaryColor3f = (PFNGLSECONDARYCOLOR3FPROC) load(userptr, "glSecondaryColor3f"); context->SecondaryColor3fv = (PFNGLSECONDARYCOLOR3FVPROC) load(userptr, "glSecondaryColor3fv"); context->SecondaryColor3i = (PFNGLSECONDARYCOLOR3IPROC) load(userptr, "glSecondaryColor3i"); context->SecondaryColor3iv = (PFNGLSECONDARYCOLOR3IVPROC) load(userptr, "glSecondaryColor3iv"); context->SecondaryColor3s = (PFNGLSECONDARYCOLOR3SPROC) load(userptr, "glSecondaryColor3s"); context->SecondaryColor3sv = (PFNGLSECONDARYCOLOR3SVPROC) load(userptr, "glSecondaryColor3sv"); context->SecondaryColor3ub = (PFNGLSECONDARYCOLOR3UBPROC) load(userptr, "glSecondaryColor3ub"); context->SecondaryColor3ubv = (PFNGLSECONDARYCOLOR3UBVPROC) load(userptr, "glSecondaryColor3ubv"); context->SecondaryColor3ui = (PFNGLSECONDARYCOLOR3UIPROC) load(userptr, "glSecondaryColor3ui"); context->SecondaryColor3uiv = (PFNGLSECONDARYCOLOR3UIVPROC) load(userptr, "glSecondaryColor3uiv"); context->SecondaryColor3us = (PFNGLSECONDARYCOLOR3USPROC) load(userptr, "glSecondaryColor3us"); context->SecondaryColor3usv = (PFNGLSECONDARYCOLOR3USVPROC) load(userptr, "glSecondaryColor3usv"); context->SecondaryColorPointer = (PFNGLSECONDARYCOLORPOINTERPROC) load(userptr, "glSecondaryColorPointer"); context->WindowPos2d = (PFNGLWINDOWPOS2DPROC) load(userptr, "glWindowPos2d"); context->WindowPos2dv = (PFNGLWINDOWPOS2DVPROC) load(userptr, "glWindowPos2dv"); context->WindowPos2f = (PFNGLWINDOWPOS2FPROC) load(userptr, "glWindowPos2f"); context->WindowPos2fv = (PFNGLWINDOWPOS2FVPROC) load(userptr, "glWindowPos2fv"); context->WindowPos2i = (PFNGLWINDOWPOS2IPROC) load(userptr, "glWindowPos2i"); context->WindowPos2iv = (PFNGLWINDOWPOS2IVPROC) load(userptr, "glWindowPos2iv"); context->WindowPos2s = (PFNGLWINDOWPOS2SPROC) load(userptr, "glWindowPos2s"); context->WindowPos2sv = (PFNGLWINDOWPOS2SVPROC) load(userptr, "glWindowPos2sv"); context->WindowPos3d = (PFNGLWINDOWPOS3DPROC) load(userptr, "glWindowPos3d"); context->WindowPos3dv = (PFNGLWINDOWPOS3DVPROC) load(userptr, "glWindowPos3dv"); context->WindowPos3f = (PFNGLWINDOWPOS3FPROC) load(userptr, "glWindowPos3f"); context->WindowPos3fv = (PFNGLWINDOWPOS3FVPROC) load(userptr, "glWindowPos3fv"); context->WindowPos3i = (PFNGLWINDOWPOS3IPROC) load(userptr, "glWindowPos3i"); context->WindowPos3iv = (PFNGLWINDOWPOS3IVPROC) load(userptr, "glWindowPos3iv"); context->WindowPos3s = (PFNGLWINDOWPOS3SPROC) load(userptr, "glWindowPos3s"); context->WindowPos3sv = (PFNGLWINDOWPOS3SVPROC) load(userptr, "glWindowPos3sv"); } static void glad_gl_load_GL_VERSION_1_5(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_1_5) return; context->BeginQuery = (PFNGLBEGINQUERYPROC) load(userptr, "glBeginQuery"); context->BindBuffer = (PFNGLBINDBUFFERPROC) load(userptr, "glBindBuffer"); context->BufferData = (PFNGLBUFFERDATAPROC) load(userptr, "glBufferData"); context->BufferSubData = (PFNGLBUFFERSUBDATAPROC) load(userptr, "glBufferSubData"); context->DeleteBuffers = (PFNGLDELETEBUFFERSPROC) load(userptr, "glDeleteBuffers"); context->DeleteQueries = (PFNGLDELETEQUERIESPROC) load(userptr, "glDeleteQueries"); context->EndQuery = (PFNGLENDQUERYPROC) load(userptr, "glEndQuery"); context->GenBuffers = (PFNGLGENBUFFERSPROC) load(userptr, "glGenBuffers"); context->GenQueries = (PFNGLGENQUERIESPROC) load(userptr, "glGenQueries"); context->GetBufferParameteriv = (PFNGLGETBUFFERPARAMETERIVPROC) load(userptr, "glGetBufferParameteriv"); context->GetBufferPointerv = (PFNGLGETBUFFERPOINTERVPROC) load(userptr, "glGetBufferPointerv"); context->GetBufferSubData = (PFNGLGETBUFFERSUBDATAPROC) load(userptr, "glGetBufferSubData"); context->GetQueryObjectiv = (PFNGLGETQUERYOBJECTIVPROC) load(userptr, "glGetQueryObjectiv"); context->GetQueryObjectuiv = (PFNGLGETQUERYOBJECTUIVPROC) load(userptr, "glGetQueryObjectuiv"); context->GetQueryiv = (PFNGLGETQUERYIVPROC) load(userptr, "glGetQueryiv"); context->IsBuffer = (PFNGLISBUFFERPROC) load(userptr, "glIsBuffer"); context->IsQuery = (PFNGLISQUERYPROC) load(userptr, "glIsQuery"); context->MapBuffer = (PFNGLMAPBUFFERPROC) load(userptr, "glMapBuffer"); context->UnmapBuffer = (PFNGLUNMAPBUFFERPROC) load(userptr, "glUnmapBuffer"); } static void glad_gl_load_GL_VERSION_2_0(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_2_0) return; context->EGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC) load(userptr, "glEGLImageTargetTexture2DOES"); context->AttachShader = (PFNGLATTACHSHADERPROC) load(userptr, "glAttachShader"); context->BindAttribLocation = (PFNGLBINDATTRIBLOCATIONPROC) load(userptr, "glBindAttribLocation"); context->BlendEquationSeparate = (PFNGLBLENDEQUATIONSEPARATEPROC) load(userptr, "glBlendEquationSeparate"); context->CompileShader = (PFNGLCOMPILESHADERPROC) load(userptr, "glCompileShader"); context->CreateProgram = (PFNGLCREATEPROGRAMPROC) load(userptr, "glCreateProgram"); context->CreateShader = (PFNGLCREATESHADERPROC) load(userptr, "glCreateShader"); context->DeleteProgram = (PFNGLDELETEPROGRAMPROC) load(userptr, "glDeleteProgram"); context->DeleteShader = (PFNGLDELETESHADERPROC) load(userptr, "glDeleteShader"); context->DetachShader = (PFNGLDETACHSHADERPROC) load(userptr, "glDetachShader"); context->DisableVertexAttribArray = (PFNGLDISABLEVERTEXATTRIBARRAYPROC) load(userptr, "glDisableVertexAttribArray"); context->DrawBuffers = (PFNGLDRAWBUFFERSPROC) load(userptr, "glDrawBuffers"); context->EnableVertexAttribArray = (PFNGLENABLEVERTEXATTRIBARRAYPROC) load(userptr, "glEnableVertexAttribArray"); context->GetActiveAttrib = (PFNGLGETACTIVEATTRIBPROC) load(userptr, "glGetActiveAttrib"); context->GetActiveUniform = (PFNGLGETACTIVEUNIFORMPROC) load(userptr, "glGetActiveUniform"); context->GetAttachedShaders = (PFNGLGETATTACHEDSHADERSPROC) load(userptr, "glGetAttachedShaders"); context->GetAttribLocation = (PFNGLGETATTRIBLOCATIONPROC) load(userptr, "glGetAttribLocation"); context->GetProgramInfoLog = (PFNGLGETPROGRAMINFOLOGPROC) load(userptr, "glGetProgramInfoLog"); context->GetProgramiv = (PFNGLGETPROGRAMIVPROC) load(userptr, "glGetProgramiv"); context->GetShaderInfoLog = (PFNGLGETSHADERINFOLOGPROC) load(userptr, "glGetShaderInfoLog"); context->GetShaderSource = (PFNGLGETSHADERSOURCEPROC) load(userptr, "glGetShaderSource"); context->GetShaderiv = (PFNGLGETSHADERIVPROC) load(userptr, "glGetShaderiv"); context->GetUniformLocation = (PFNGLGETUNIFORMLOCATIONPROC) load(userptr, "glGetUniformLocation"); context->GetUniformfv = (PFNGLGETUNIFORMFVPROC) load(userptr, "glGetUniformfv"); context->GetUniformiv = (PFNGLGETUNIFORMIVPROC) load(userptr, "glGetUniformiv"); context->GetVertexAttribPointerv = (PFNGLGETVERTEXATTRIBPOINTERVPROC) load(userptr, "glGetVertexAttribPointerv"); context->GetVertexAttribdv = (PFNGLGETVERTEXATTRIBDVPROC) load(userptr, "glGetVertexAttribdv"); context->GetVertexAttribfv = (PFNGLGETVERTEXATTRIBFVPROC) load(userptr, "glGetVertexAttribfv"); context->GetVertexAttribiv = (PFNGLGETVERTEXATTRIBIVPROC) load(userptr, "glGetVertexAttribiv"); context->IsProgram = (PFNGLISPROGRAMPROC) load(userptr, "glIsProgram"); context->IsShader = (PFNGLISSHADERPROC) load(userptr, "glIsShader"); context->LinkProgram = (PFNGLLINKPROGRAMPROC) load(userptr, "glLinkProgram"); context->ShaderSource = (PFNGLSHADERSOURCEPROC) load(userptr, "glShaderSource"); context->StencilFuncSeparate = (PFNGLSTENCILFUNCSEPARATEPROC) load(userptr, "glStencilFuncSeparate"); context->StencilMaskSeparate = (PFNGLSTENCILMASKSEPARATEPROC) load(userptr, "glStencilMaskSeparate"); context->StencilOpSeparate = (PFNGLSTENCILOPSEPARATEPROC) load(userptr, "glStencilOpSeparate"); context->Uniform1f = (PFNGLUNIFORM1FPROC) load(userptr, "glUniform1f"); context->Uniform1fv = (PFNGLUNIFORM1FVPROC) load(userptr, "glUniform1fv"); context->Uniform1i = (PFNGLUNIFORM1IPROC) load(userptr, "glUniform1i"); context->Uniform1iv = (PFNGLUNIFORM1IVPROC) load(userptr, "glUniform1iv"); context->Uniform2f = (PFNGLUNIFORM2FPROC) load(userptr, "glUniform2f"); context->Uniform2fv = (PFNGLUNIFORM2FVPROC) load(userptr, "glUniform2fv"); context->Uniform2i = (PFNGLUNIFORM2IPROC) load(userptr, "glUniform2i"); context->Uniform2iv = (PFNGLUNIFORM2IVPROC) load(userptr, "glUniform2iv"); context->Uniform3f = (PFNGLUNIFORM3FPROC) load(userptr, "glUniform3f"); context->Uniform3fv = (PFNGLUNIFORM3FVPROC) load(userptr, "glUniform3fv"); context->Uniform3i = (PFNGLUNIFORM3IPROC) load(userptr, "glUniform3i"); context->Uniform3iv = (PFNGLUNIFORM3IVPROC) load(userptr, "glUniform3iv"); context->Uniform4f = (PFNGLUNIFORM4FPROC) load(userptr, "glUniform4f"); context->Uniform4fv = (PFNGLUNIFORM4FVPROC) load(userptr, "glUniform4fv"); context->Uniform4i = (PFNGLUNIFORM4IPROC) load(userptr, "glUniform4i"); context->Uniform4iv = (PFNGLUNIFORM4IVPROC) load(userptr, "glUniform4iv"); context->UniformMatrix2fv = (PFNGLUNIFORMMATRIX2FVPROC) load(userptr, "glUniformMatrix2fv"); context->UniformMatrix3fv = (PFNGLUNIFORMMATRIX3FVPROC) load(userptr, "glUniformMatrix3fv"); context->UniformMatrix4fv = (PFNGLUNIFORMMATRIX4FVPROC) load(userptr, "glUniformMatrix4fv"); context->UseProgram = (PFNGLUSEPROGRAMPROC) load(userptr, "glUseProgram"); context->ValidateProgram = (PFNGLVALIDATEPROGRAMPROC) load(userptr, "glValidateProgram"); context->VertexAttrib1d = (PFNGLVERTEXATTRIB1DPROC) load(userptr, "glVertexAttrib1d"); context->VertexAttrib1dv = (PFNGLVERTEXATTRIB1DVPROC) load(userptr, "glVertexAttrib1dv"); context->VertexAttrib1f = (PFNGLVERTEXATTRIB1FPROC) load(userptr, "glVertexAttrib1f"); context->VertexAttrib1fv = (PFNGLVERTEXATTRIB1FVPROC) load(userptr, "glVertexAttrib1fv"); context->VertexAttrib1s = (PFNGLVERTEXATTRIB1SPROC) load(userptr, "glVertexAttrib1s"); context->VertexAttrib1sv = (PFNGLVERTEXATTRIB1SVPROC) load(userptr, "glVertexAttrib1sv"); context->VertexAttrib2d = (PFNGLVERTEXATTRIB2DPROC) load(userptr, "glVertexAttrib2d"); context->VertexAttrib2dv = (PFNGLVERTEXATTRIB2DVPROC) load(userptr, "glVertexAttrib2dv"); context->VertexAttrib2f = (PFNGLVERTEXATTRIB2FPROC) load(userptr, "glVertexAttrib2f"); context->VertexAttrib2fv = (PFNGLVERTEXATTRIB2FVPROC) load(userptr, "glVertexAttrib2fv"); context->VertexAttrib2s = (PFNGLVERTEXATTRIB2SPROC) load(userptr, "glVertexAttrib2s"); context->VertexAttrib2sv = (PFNGLVERTEXATTRIB2SVPROC) load(userptr, "glVertexAttrib2sv"); context->VertexAttrib3d = (PFNGLVERTEXATTRIB3DPROC) load(userptr, "glVertexAttrib3d"); context->VertexAttrib3dv = (PFNGLVERTEXATTRIB3DVPROC) load(userptr, "glVertexAttrib3dv"); context->VertexAttrib3f = (PFNGLVERTEXATTRIB3FPROC) load(userptr, "glVertexAttrib3f"); context->VertexAttrib3fv = (PFNGLVERTEXATTRIB3FVPROC) load(userptr, "glVertexAttrib3fv"); context->VertexAttrib3s = (PFNGLVERTEXATTRIB3SPROC) load(userptr, "glVertexAttrib3s"); context->VertexAttrib3sv = (PFNGLVERTEXATTRIB3SVPROC) load(userptr, "glVertexAttrib3sv"); context->VertexAttrib4Nbv = (PFNGLVERTEXATTRIB4NBVPROC) load(userptr, "glVertexAttrib4Nbv"); context->VertexAttrib4Niv = (PFNGLVERTEXATTRIB4NIVPROC) load(userptr, "glVertexAttrib4Niv"); context->VertexAttrib4Nsv = (PFNGLVERTEXATTRIB4NSVPROC) load(userptr, "glVertexAttrib4Nsv"); context->VertexAttrib4Nub = (PFNGLVERTEXATTRIB4NUBPROC) load(userptr, "glVertexAttrib4Nub"); context->VertexAttrib4Nubv = (PFNGLVERTEXATTRIB4NUBVPROC) load(userptr, "glVertexAttrib4Nubv"); context->VertexAttrib4Nuiv = (PFNGLVERTEXATTRIB4NUIVPROC) load(userptr, "glVertexAttrib4Nuiv"); context->VertexAttrib4Nusv = (PFNGLVERTEXATTRIB4NUSVPROC) load(userptr, "glVertexAttrib4Nusv"); context->VertexAttrib4bv = (PFNGLVERTEXATTRIB4BVPROC) load(userptr, "glVertexAttrib4bv"); context->VertexAttrib4d = (PFNGLVERTEXATTRIB4DPROC) load(userptr, "glVertexAttrib4d"); context->VertexAttrib4dv = (PFNGLVERTEXATTRIB4DVPROC) load(userptr, "glVertexAttrib4dv"); context->VertexAttrib4f = (PFNGLVERTEXATTRIB4FPROC) load(userptr, "glVertexAttrib4f"); context->VertexAttrib4fv = (PFNGLVERTEXATTRIB4FVPROC) load(userptr, "glVertexAttrib4fv"); context->VertexAttrib4iv = (PFNGLVERTEXATTRIB4IVPROC) load(userptr, "glVertexAttrib4iv"); context->VertexAttrib4s = (PFNGLVERTEXATTRIB4SPROC) load(userptr, "glVertexAttrib4s"); context->VertexAttrib4sv = (PFNGLVERTEXATTRIB4SVPROC) load(userptr, "glVertexAttrib4sv"); context->VertexAttrib4ubv = (PFNGLVERTEXATTRIB4UBVPROC) load(userptr, "glVertexAttrib4ubv"); context->VertexAttrib4uiv = (PFNGLVERTEXATTRIB4UIVPROC) load(userptr, "glVertexAttrib4uiv"); context->VertexAttrib4usv = (PFNGLVERTEXATTRIB4USVPROC) load(userptr, "glVertexAttrib4usv"); context->VertexAttribPointer = (PFNGLVERTEXATTRIBPOINTERPROC) load(userptr, "glVertexAttribPointer"); } static void glad_gl_load_GL_VERSION_2_1(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_2_1) return; context->UniformMatrix2x3fv = (PFNGLUNIFORMMATRIX2X3FVPROC) load(userptr, "glUniformMatrix2x3fv"); context->UniformMatrix2x4fv = (PFNGLUNIFORMMATRIX2X4FVPROC) load(userptr, "glUniformMatrix2x4fv"); context->UniformMatrix3x2fv = (PFNGLUNIFORMMATRIX3X2FVPROC) load(userptr, "glUniformMatrix3x2fv"); context->UniformMatrix3x4fv = (PFNGLUNIFORMMATRIX3X4FVPROC) load(userptr, "glUniformMatrix3x4fv"); context->UniformMatrix4x2fv = (PFNGLUNIFORMMATRIX4X2FVPROC) load(userptr, "glUniformMatrix4x2fv"); context->UniformMatrix4x3fv = (PFNGLUNIFORMMATRIX4X3FVPROC) load(userptr, "glUniformMatrix4x3fv"); } static void glad_gl_load_GL_VERSION_3_0(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_3_0) return; context->BeginConditionalRender = (PFNGLBEGINCONDITIONALRENDERPROC) load(userptr, "glBeginConditionalRender"); context->BeginTransformFeedback = (PFNGLBEGINTRANSFORMFEEDBACKPROC) load(userptr, "glBeginTransformFeedback"); context->BindBufferBase = (PFNGLBINDBUFFERBASEPROC) load(userptr, "glBindBufferBase"); context->BindBufferRange = (PFNGLBINDBUFFERRANGEPROC) load(userptr, "glBindBufferRange"); context->BindFragDataLocation = (PFNGLBINDFRAGDATALOCATIONPROC) load(userptr, "glBindFragDataLocation"); context->BindFramebuffer = (PFNGLBINDFRAMEBUFFERPROC) load(userptr, "glBindFramebuffer"); context->BindRenderbuffer = (PFNGLBINDRENDERBUFFERPROC) load(userptr, "glBindRenderbuffer"); context->BindVertexArray = (PFNGLBINDVERTEXARRAYPROC) load(userptr, "glBindVertexArray"); context->BlitFramebuffer = (PFNGLBLITFRAMEBUFFERPROC) load(userptr, "glBlitFramebuffer"); context->CheckFramebufferStatus = (PFNGLCHECKFRAMEBUFFERSTATUSPROC) load(userptr, "glCheckFramebufferStatus"); context->ClampColor = (PFNGLCLAMPCOLORPROC) load(userptr, "glClampColor"); context->ClearBufferfi = (PFNGLCLEARBUFFERFIPROC) load(userptr, "glClearBufferfi"); context->ClearBufferfv = (PFNGLCLEARBUFFERFVPROC) load(userptr, "glClearBufferfv"); context->ClearBufferiv = (PFNGLCLEARBUFFERIVPROC) load(userptr, "glClearBufferiv"); context->ClearBufferuiv = (PFNGLCLEARBUFFERUIVPROC) load(userptr, "glClearBufferuiv"); context->ColorMaski = (PFNGLCOLORMASKIPROC) load(userptr, "glColorMaski"); context->DeleteFramebuffers = (PFNGLDELETEFRAMEBUFFERSPROC) load(userptr, "glDeleteFramebuffers"); context->DeleteRenderbuffers = (PFNGLDELETERENDERBUFFERSPROC) load(userptr, "glDeleteRenderbuffers"); context->DeleteVertexArrays = (PFNGLDELETEVERTEXARRAYSPROC) load(userptr, "glDeleteVertexArrays"); context->Disablei = (PFNGLDISABLEIPROC) load(userptr, "glDisablei"); context->Enablei = (PFNGLENABLEIPROC) load(userptr, "glEnablei"); context->EndConditionalRender = (PFNGLENDCONDITIONALRENDERPROC) load(userptr, "glEndConditionalRender"); context->EndTransformFeedback = (PFNGLENDTRANSFORMFEEDBACKPROC) load(userptr, "glEndTransformFeedback"); context->FlushMappedBufferRange = (PFNGLFLUSHMAPPEDBUFFERRANGEPROC) load(userptr, "glFlushMappedBufferRange"); context->FramebufferRenderbuffer = (PFNGLFRAMEBUFFERRENDERBUFFERPROC) load(userptr, "glFramebufferRenderbuffer"); context->FramebufferTexture1D = (PFNGLFRAMEBUFFERTEXTURE1DPROC) load(userptr, "glFramebufferTexture1D"); context->FramebufferTexture2D = (PFNGLFRAMEBUFFERTEXTURE2DPROC) load(userptr, "glFramebufferTexture2D"); context->FramebufferTexture3D = (PFNGLFRAMEBUFFERTEXTURE3DPROC) load(userptr, "glFramebufferTexture3D"); context->FramebufferTextureLayer = (PFNGLFRAMEBUFFERTEXTURELAYERPROC) load(userptr, "glFramebufferTextureLayer"); context->GenFramebuffers = (PFNGLGENFRAMEBUFFERSPROC) load(userptr, "glGenFramebuffers"); context->GenRenderbuffers = (PFNGLGENRENDERBUFFERSPROC) load(userptr, "glGenRenderbuffers"); context->GenVertexArrays = (PFNGLGENVERTEXARRAYSPROC) load(userptr, "glGenVertexArrays"); context->GenerateMipmap = (PFNGLGENERATEMIPMAPPROC) load(userptr, "glGenerateMipmap"); context->GetBooleani_v = (PFNGLGETBOOLEANI_VPROC) load(userptr, "glGetBooleani_v"); context->GetFragDataLocation = (PFNGLGETFRAGDATALOCATIONPROC) load(userptr, "glGetFragDataLocation"); context->GetFramebufferAttachmentParameteriv = (PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC) load(userptr, "glGetFramebufferAttachmentParameteriv"); context->GetIntegeri_v = (PFNGLGETINTEGERI_VPROC) load(userptr, "glGetIntegeri_v"); context->GetRenderbufferParameteriv = (PFNGLGETRENDERBUFFERPARAMETERIVPROC) load(userptr, "glGetRenderbufferParameteriv"); context->GetStringi = (PFNGLGETSTRINGIPROC) load(userptr, "glGetStringi"); context->GetTexParameterIiv = (PFNGLGETTEXPARAMETERIIVPROC) load(userptr, "glGetTexParameterIiv"); context->GetTexParameterIuiv = (PFNGLGETTEXPARAMETERIUIVPROC) load(userptr, "glGetTexParameterIuiv"); context->GetTransformFeedbackVarying = (PFNGLGETTRANSFORMFEEDBACKVARYINGPROC) load(userptr, "glGetTransformFeedbackVarying"); context->GetUniformuiv = (PFNGLGETUNIFORMUIVPROC) load(userptr, "glGetUniformuiv"); context->GetVertexAttribIiv = (PFNGLGETVERTEXATTRIBIIVPROC) load(userptr, "glGetVertexAttribIiv"); context->GetVertexAttribIuiv = (PFNGLGETVERTEXATTRIBIUIVPROC) load(userptr, "glGetVertexAttribIuiv"); context->IsEnabledi = (PFNGLISENABLEDIPROC) load(userptr, "glIsEnabledi"); context->IsFramebuffer = (PFNGLISFRAMEBUFFERPROC) load(userptr, "glIsFramebuffer"); context->IsRenderbuffer = (PFNGLISRENDERBUFFERPROC) load(userptr, "glIsRenderbuffer"); context->IsVertexArray = (PFNGLISVERTEXARRAYPROC) load(userptr, "glIsVertexArray"); context->MapBufferRange = (PFNGLMAPBUFFERRANGEPROC) load(userptr, "glMapBufferRange"); context->RenderbufferStorage = (PFNGLRENDERBUFFERSTORAGEPROC) load(userptr, "glRenderbufferStorage"); context->RenderbufferStorageMultisample = (PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC) load(userptr, "glRenderbufferStorageMultisample"); context->TexParameterIiv = (PFNGLTEXPARAMETERIIVPROC) load(userptr, "glTexParameterIiv"); context->TexParameterIuiv = (PFNGLTEXPARAMETERIUIVPROC) load(userptr, "glTexParameterIuiv"); context->TransformFeedbackVaryings = (PFNGLTRANSFORMFEEDBACKVARYINGSPROC) load(userptr, "glTransformFeedbackVaryings"); context->Uniform1ui = (PFNGLUNIFORM1UIPROC) load(userptr, "glUniform1ui"); context->Uniform1uiv = (PFNGLUNIFORM1UIVPROC) load(userptr, "glUniform1uiv"); context->Uniform2ui = (PFNGLUNIFORM2UIPROC) load(userptr, "glUniform2ui"); context->Uniform2uiv = (PFNGLUNIFORM2UIVPROC) load(userptr, "glUniform2uiv"); context->Uniform3ui = (PFNGLUNIFORM3UIPROC) load(userptr, "glUniform3ui"); context->Uniform3uiv = (PFNGLUNIFORM3UIVPROC) load(userptr, "glUniform3uiv"); context->Uniform4ui = (PFNGLUNIFORM4UIPROC) load(userptr, "glUniform4ui"); context->Uniform4uiv = (PFNGLUNIFORM4UIVPROC) load(userptr, "glUniform4uiv"); context->VertexAttribI1i = (PFNGLVERTEXATTRIBI1IPROC) load(userptr, "glVertexAttribI1i"); context->VertexAttribI1iv = (PFNGLVERTEXATTRIBI1IVPROC) load(userptr, "glVertexAttribI1iv"); context->VertexAttribI1ui = (PFNGLVERTEXATTRIBI1UIPROC) load(userptr, "glVertexAttribI1ui"); context->VertexAttribI1uiv = (PFNGLVERTEXATTRIBI1UIVPROC) load(userptr, "glVertexAttribI1uiv"); context->VertexAttribI2i = (PFNGLVERTEXATTRIBI2IPROC) load(userptr, "glVertexAttribI2i"); context->VertexAttribI2iv = (PFNGLVERTEXATTRIBI2IVPROC) load(userptr, "glVertexAttribI2iv"); context->VertexAttribI2ui = (PFNGLVERTEXATTRIBI2UIPROC) load(userptr, "glVertexAttribI2ui"); context->VertexAttribI2uiv = (PFNGLVERTEXATTRIBI2UIVPROC) load(userptr, "glVertexAttribI2uiv"); context->VertexAttribI3i = (PFNGLVERTEXATTRIBI3IPROC) load(userptr, "glVertexAttribI3i"); context->VertexAttribI3iv = (PFNGLVERTEXATTRIBI3IVPROC) load(userptr, "glVertexAttribI3iv"); context->VertexAttribI3ui = (PFNGLVERTEXATTRIBI3UIPROC) load(userptr, "glVertexAttribI3ui"); context->VertexAttribI3uiv = (PFNGLVERTEXATTRIBI3UIVPROC) load(userptr, "glVertexAttribI3uiv"); context->VertexAttribI4bv = (PFNGLVERTEXATTRIBI4BVPROC) load(userptr, "glVertexAttribI4bv"); context->VertexAttribI4i = (PFNGLVERTEXATTRIBI4IPROC) load(userptr, "glVertexAttribI4i"); context->VertexAttribI4iv = (PFNGLVERTEXATTRIBI4IVPROC) load(userptr, "glVertexAttribI4iv"); context->VertexAttribI4sv = (PFNGLVERTEXATTRIBI4SVPROC) load(userptr, "glVertexAttribI4sv"); context->VertexAttribI4ubv = (PFNGLVERTEXATTRIBI4UBVPROC) load(userptr, "glVertexAttribI4ubv"); context->VertexAttribI4ui = (PFNGLVERTEXATTRIBI4UIPROC) load(userptr, "glVertexAttribI4ui"); context->VertexAttribI4uiv = (PFNGLVERTEXATTRIBI4UIVPROC) load(userptr, "glVertexAttribI4uiv"); context->VertexAttribI4usv = (PFNGLVERTEXATTRIBI4USVPROC) load(userptr, "glVertexAttribI4usv"); context->VertexAttribIPointer = (PFNGLVERTEXATTRIBIPOINTERPROC) load(userptr, "glVertexAttribIPointer"); } static void glad_gl_load_GL_VERSION_3_1(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_3_1) return; context->BindBufferBase = (PFNGLBINDBUFFERBASEPROC) load(userptr, "glBindBufferBase"); context->BindBufferRange = (PFNGLBINDBUFFERRANGEPROC) load(userptr, "glBindBufferRange"); context->CopyBufferSubData = (PFNGLCOPYBUFFERSUBDATAPROC) load(userptr, "glCopyBufferSubData"); context->DrawArraysInstanced = (PFNGLDRAWARRAYSINSTANCEDPROC) load(userptr, "glDrawArraysInstanced"); context->DrawElementsInstanced = (PFNGLDRAWELEMENTSINSTANCEDPROC) load(userptr, "glDrawElementsInstanced"); context->GetActiveUniformBlockName = (PFNGLGETACTIVEUNIFORMBLOCKNAMEPROC) load(userptr, "glGetActiveUniformBlockName"); context->GetActiveUniformBlockiv = (PFNGLGETACTIVEUNIFORMBLOCKIVPROC) load(userptr, "glGetActiveUniformBlockiv"); context->GetActiveUniformName = (PFNGLGETACTIVEUNIFORMNAMEPROC) load(userptr, "glGetActiveUniformName"); context->GetActiveUniformsiv = (PFNGLGETACTIVEUNIFORMSIVPROC) load(userptr, "glGetActiveUniformsiv"); context->GetIntegeri_v = (PFNGLGETINTEGERI_VPROC) load(userptr, "glGetIntegeri_v"); context->GetUniformBlockIndex = (PFNGLGETUNIFORMBLOCKINDEXPROC) load(userptr, "glGetUniformBlockIndex"); context->GetUniformIndices = (PFNGLGETUNIFORMINDICESPROC) load(userptr, "glGetUniformIndices"); context->PrimitiveRestartIndex = (PFNGLPRIMITIVERESTARTINDEXPROC) load(userptr, "glPrimitiveRestartIndex"); context->TexBuffer = (PFNGLTEXBUFFERPROC) load(userptr, "glTexBuffer"); context->UniformBlockBinding = (PFNGLUNIFORMBLOCKBINDINGPROC) load(userptr, "glUniformBlockBinding"); } static void glad_gl_load_GL_VERSION_3_2(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_3_2) return; context->ClientWaitSync = (PFNGLCLIENTWAITSYNCPROC) load(userptr, "glClientWaitSync"); context->DeleteSync = (PFNGLDELETESYNCPROC) load(userptr, "glDeleteSync"); context->DrawElementsBaseVertex = (PFNGLDRAWELEMENTSBASEVERTEXPROC) load(userptr, "glDrawElementsBaseVertex"); context->DrawElementsInstancedBaseVertex = (PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXPROC) load(userptr, "glDrawElementsInstancedBaseVertex"); context->DrawRangeElementsBaseVertex = (PFNGLDRAWRANGEELEMENTSBASEVERTEXPROC) load(userptr, "glDrawRangeElementsBaseVertex"); context->FenceSync = (PFNGLFENCESYNCPROC) load(userptr, "glFenceSync"); context->FramebufferTexture = (PFNGLFRAMEBUFFERTEXTUREPROC) load(userptr, "glFramebufferTexture"); context->GetBufferParameteri64v = (PFNGLGETBUFFERPARAMETERI64VPROC) load(userptr, "glGetBufferParameteri64v"); context->GetInteger64i_v = (PFNGLGETINTEGER64I_VPROC) load(userptr, "glGetInteger64i_v"); context->GetInteger64v = (PFNGLGETINTEGER64VPROC) load(userptr, "glGetInteger64v"); context->GetMultisamplefv = (PFNGLGETMULTISAMPLEFVPROC) load(userptr, "glGetMultisamplefv"); context->GetSynciv = (PFNGLGETSYNCIVPROC) load(userptr, "glGetSynciv"); context->IsSync = (PFNGLISSYNCPROC) load(userptr, "glIsSync"); context->MultiDrawElementsBaseVertex = (PFNGLMULTIDRAWELEMENTSBASEVERTEXPROC) load(userptr, "glMultiDrawElementsBaseVertex"); context->ProvokingVertex = (PFNGLPROVOKINGVERTEXPROC) load(userptr, "glProvokingVertex"); context->SampleMaski = (PFNGLSAMPLEMASKIPROC) load(userptr, "glSampleMaski"); context->TexImage2DMultisample = (PFNGLTEXIMAGE2DMULTISAMPLEPROC) load(userptr, "glTexImage2DMultisample"); context->TexImage3DMultisample = (PFNGLTEXIMAGE3DMULTISAMPLEPROC) load(userptr, "glTexImage3DMultisample"); context->WaitSync = (PFNGLWAITSYNCPROC) load(userptr, "glWaitSync"); } static void glad_gl_load_GL_VERSION_3_3(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_3_3) return; context->BindFragDataLocationIndexed = (PFNGLBINDFRAGDATALOCATIONINDEXEDPROC) load(userptr, "glBindFragDataLocationIndexed"); context->BindSampler = (PFNGLBINDSAMPLERPROC) load(userptr, "glBindSampler"); context->ColorP3ui = (PFNGLCOLORP3UIPROC) load(userptr, "glColorP3ui"); context->ColorP3uiv = (PFNGLCOLORP3UIVPROC) load(userptr, "glColorP3uiv"); context->ColorP4ui = (PFNGLCOLORP4UIPROC) load(userptr, "glColorP4ui"); context->ColorP4uiv = (PFNGLCOLORP4UIVPROC) load(userptr, "glColorP4uiv"); context->DeleteSamplers = (PFNGLDELETESAMPLERSPROC) load(userptr, "glDeleteSamplers"); context->GenSamplers = (PFNGLGENSAMPLERSPROC) load(userptr, "glGenSamplers"); context->GetFragDataIndex = (PFNGLGETFRAGDATAINDEXPROC) load(userptr, "glGetFragDataIndex"); context->GetQueryObjecti64v = (PFNGLGETQUERYOBJECTI64VPROC) load(userptr, "glGetQueryObjecti64v"); context->GetQueryObjectui64v = (PFNGLGETQUERYOBJECTUI64VPROC) load(userptr, "glGetQueryObjectui64v"); context->GetSamplerParameterIiv = (PFNGLGETSAMPLERPARAMETERIIVPROC) load(userptr, "glGetSamplerParameterIiv"); context->GetSamplerParameterIuiv = (PFNGLGETSAMPLERPARAMETERIUIVPROC) load(userptr, "glGetSamplerParameterIuiv"); context->GetSamplerParameterfv = (PFNGLGETSAMPLERPARAMETERFVPROC) load(userptr, "glGetSamplerParameterfv"); context->GetSamplerParameteriv = (PFNGLGETSAMPLERPARAMETERIVPROC) load(userptr, "glGetSamplerParameteriv"); context->IsSampler = (PFNGLISSAMPLERPROC) load(userptr, "glIsSampler"); context->MultiTexCoordP1ui = (PFNGLMULTITEXCOORDP1UIPROC) load(userptr, "glMultiTexCoordP1ui"); context->MultiTexCoordP1uiv = (PFNGLMULTITEXCOORDP1UIVPROC) load(userptr, "glMultiTexCoordP1uiv"); context->MultiTexCoordP2ui = (PFNGLMULTITEXCOORDP2UIPROC) load(userptr, "glMultiTexCoordP2ui"); context->MultiTexCoordP2uiv = (PFNGLMULTITEXCOORDP2UIVPROC) load(userptr, "glMultiTexCoordP2uiv"); context->MultiTexCoordP3ui = (PFNGLMULTITEXCOORDP3UIPROC) load(userptr, "glMultiTexCoordP3ui"); context->MultiTexCoordP3uiv = (PFNGLMULTITEXCOORDP3UIVPROC) load(userptr, "glMultiTexCoordP3uiv"); context->MultiTexCoordP4ui = (PFNGLMULTITEXCOORDP4UIPROC) load(userptr, "glMultiTexCoordP4ui"); context->MultiTexCoordP4uiv = (PFNGLMULTITEXCOORDP4UIVPROC) load(userptr, "glMultiTexCoordP4uiv"); context->NormalP3ui = (PFNGLNORMALP3UIPROC) load(userptr, "glNormalP3ui"); context->NormalP3uiv = (PFNGLNORMALP3UIVPROC) load(userptr, "glNormalP3uiv"); context->QueryCounter = (PFNGLQUERYCOUNTERPROC) load(userptr, "glQueryCounter"); context->SamplerParameterIiv = (PFNGLSAMPLERPARAMETERIIVPROC) load(userptr, "glSamplerParameterIiv"); context->SamplerParameterIuiv = (PFNGLSAMPLERPARAMETERIUIVPROC) load(userptr, "glSamplerParameterIuiv"); context->SamplerParameterf = (PFNGLSAMPLERPARAMETERFPROC) load(userptr, "glSamplerParameterf"); context->SamplerParameterfv = (PFNGLSAMPLERPARAMETERFVPROC) load(userptr, "glSamplerParameterfv"); context->SamplerParameteri = (PFNGLSAMPLERPARAMETERIPROC) load(userptr, "glSamplerParameteri"); context->SamplerParameteriv = (PFNGLSAMPLERPARAMETERIVPROC) load(userptr, "glSamplerParameteriv"); context->SecondaryColorP3ui = (PFNGLSECONDARYCOLORP3UIPROC) load(userptr, "glSecondaryColorP3ui"); context->SecondaryColorP3uiv = (PFNGLSECONDARYCOLORP3UIVPROC) load(userptr, "glSecondaryColorP3uiv"); context->TexCoordP1ui = (PFNGLTEXCOORDP1UIPROC) load(userptr, "glTexCoordP1ui"); context->TexCoordP1uiv = (PFNGLTEXCOORDP1UIVPROC) load(userptr, "glTexCoordP1uiv"); context->TexCoordP2ui = (PFNGLTEXCOORDP2UIPROC) load(userptr, "glTexCoordP2ui"); context->TexCoordP2uiv = (PFNGLTEXCOORDP2UIVPROC) load(userptr, "glTexCoordP2uiv"); context->TexCoordP3ui = (PFNGLTEXCOORDP3UIPROC) load(userptr, "glTexCoordP3ui"); context->TexCoordP3uiv = (PFNGLTEXCOORDP3UIVPROC) load(userptr, "glTexCoordP3uiv"); context->TexCoordP4ui = (PFNGLTEXCOORDP4UIPROC) load(userptr, "glTexCoordP4ui"); context->TexCoordP4uiv = (PFNGLTEXCOORDP4UIVPROC) load(userptr, "glTexCoordP4uiv"); context->VertexAttribDivisor = (PFNGLVERTEXATTRIBDIVISORPROC) load(userptr, "glVertexAttribDivisor"); context->VertexAttribP1ui = (PFNGLVERTEXATTRIBP1UIPROC) load(userptr, "glVertexAttribP1ui"); context->VertexAttribP1uiv = (PFNGLVERTEXATTRIBP1UIVPROC) load(userptr, "glVertexAttribP1uiv"); context->VertexAttribP2ui = (PFNGLVERTEXATTRIBP2UIPROC) load(userptr, "glVertexAttribP2ui"); context->VertexAttribP2uiv = (PFNGLVERTEXATTRIBP2UIVPROC) load(userptr, "glVertexAttribP2uiv"); context->VertexAttribP3ui = (PFNGLVERTEXATTRIBP3UIPROC) load(userptr, "glVertexAttribP3ui"); context->VertexAttribP3uiv = (PFNGLVERTEXATTRIBP3UIVPROC) load(userptr, "glVertexAttribP3uiv"); context->VertexAttribP4ui = (PFNGLVERTEXATTRIBP4UIPROC) load(userptr, "glVertexAttribP4ui"); context->VertexAttribP4uiv = (PFNGLVERTEXATTRIBP4UIVPROC) load(userptr, "glVertexAttribP4uiv"); context->VertexP2ui = (PFNGLVERTEXP2UIPROC) load(userptr, "glVertexP2ui"); context->VertexP2uiv = (PFNGLVERTEXP2UIVPROC) load(userptr, "glVertexP2uiv"); context->VertexP3ui = (PFNGLVERTEXP3UIPROC) load(userptr, "glVertexP3ui"); context->VertexP3uiv = (PFNGLVERTEXP3UIVPROC) load(userptr, "glVertexP3uiv"); context->VertexP4ui = (PFNGLVERTEXP4UIPROC) load(userptr, "glVertexP4ui"); context->VertexP4uiv = (PFNGLVERTEXP4UIVPROC) load(userptr, "glVertexP4uiv"); } static void glad_gl_load_GL_VERSION_4_0(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_4_0) return; context->BeginQueryIndexed = (PFNGLBEGINQUERYINDEXEDPROC) load(userptr, "glBeginQueryIndexed"); context->BindTransformFeedback = (PFNGLBINDTRANSFORMFEEDBACKPROC) load(userptr, "glBindTransformFeedback"); context->BlendEquationSeparatei = (PFNGLBLENDEQUATIONSEPARATEIPROC) load(userptr, "glBlendEquationSeparatei"); context->BlendEquationi = (PFNGLBLENDEQUATIONIPROC) load(userptr, "glBlendEquationi"); context->BlendFuncSeparatei = (PFNGLBLENDFUNCSEPARATEIPROC) load(userptr, "glBlendFuncSeparatei"); context->BlendFunci = (PFNGLBLENDFUNCIPROC) load(userptr, "glBlendFunci"); context->DeleteTransformFeedbacks = (PFNGLDELETETRANSFORMFEEDBACKSPROC) load(userptr, "glDeleteTransformFeedbacks"); context->DrawArraysIndirect = (PFNGLDRAWARRAYSINDIRECTPROC) load(userptr, "glDrawArraysIndirect"); context->DrawElementsIndirect = (PFNGLDRAWELEMENTSINDIRECTPROC) load(userptr, "glDrawElementsIndirect"); context->DrawTransformFeedback = (PFNGLDRAWTRANSFORMFEEDBACKPROC) load(userptr, "glDrawTransformFeedback"); context->DrawTransformFeedbackStream = (PFNGLDRAWTRANSFORMFEEDBACKSTREAMPROC) load(userptr, "glDrawTransformFeedbackStream"); context->EndQueryIndexed = (PFNGLENDQUERYINDEXEDPROC) load(userptr, "glEndQueryIndexed"); context->GenTransformFeedbacks = (PFNGLGENTRANSFORMFEEDBACKSPROC) load(userptr, "glGenTransformFeedbacks"); context->GetActiveSubroutineName = (PFNGLGETACTIVESUBROUTINENAMEPROC) load(userptr, "glGetActiveSubroutineName"); context->GetActiveSubroutineUniformName = (PFNGLGETACTIVESUBROUTINEUNIFORMNAMEPROC) load(userptr, "glGetActiveSubroutineUniformName"); context->GetActiveSubroutineUniformiv = (PFNGLGETACTIVESUBROUTINEUNIFORMIVPROC) load(userptr, "glGetActiveSubroutineUniformiv"); context->GetProgramStageiv = (PFNGLGETPROGRAMSTAGEIVPROC) load(userptr, "glGetProgramStageiv"); context->GetQueryIndexediv = (PFNGLGETQUERYINDEXEDIVPROC) load(userptr, "glGetQueryIndexediv"); context->GetSubroutineIndex = (PFNGLGETSUBROUTINEINDEXPROC) load(userptr, "glGetSubroutineIndex"); context->GetSubroutineUniformLocation = (PFNGLGETSUBROUTINEUNIFORMLOCATIONPROC) load(userptr, "glGetSubroutineUniformLocation"); context->GetUniformSubroutineuiv = (PFNGLGETUNIFORMSUBROUTINEUIVPROC) load(userptr, "glGetUniformSubroutineuiv"); context->GetUniformdv = (PFNGLGETUNIFORMDVPROC) load(userptr, "glGetUniformdv"); context->IsTransformFeedback = (PFNGLISTRANSFORMFEEDBACKPROC) load(userptr, "glIsTransformFeedback"); context->MinSampleShading = (PFNGLMINSAMPLESHADINGPROC) load(userptr, "glMinSampleShading"); context->PatchParameterfv = (PFNGLPATCHPARAMETERFVPROC) load(userptr, "glPatchParameterfv"); context->PatchParameteri = (PFNGLPATCHPARAMETERIPROC) load(userptr, "glPatchParameteri"); context->PauseTransformFeedback = (PFNGLPAUSETRANSFORMFEEDBACKPROC) load(userptr, "glPauseTransformFeedback"); context->ResumeTransformFeedback = (PFNGLRESUMETRANSFORMFEEDBACKPROC) load(userptr, "glResumeTransformFeedback"); context->Uniform1d = (PFNGLUNIFORM1DPROC) load(userptr, "glUniform1d"); context->Uniform1dv = (PFNGLUNIFORM1DVPROC) load(userptr, "glUniform1dv"); context->Uniform2d = (PFNGLUNIFORM2DPROC) load(userptr, "glUniform2d"); context->Uniform2dv = (PFNGLUNIFORM2DVPROC) load(userptr, "glUniform2dv"); context->Uniform3d = (PFNGLUNIFORM3DPROC) load(userptr, "glUniform3d"); context->Uniform3dv = (PFNGLUNIFORM3DVPROC) load(userptr, "glUniform3dv"); context->Uniform4d = (PFNGLUNIFORM4DPROC) load(userptr, "glUniform4d"); context->Uniform4dv = (PFNGLUNIFORM4DVPROC) load(userptr, "glUniform4dv"); context->UniformMatrix2dv = (PFNGLUNIFORMMATRIX2DVPROC) load(userptr, "glUniformMatrix2dv"); context->UniformMatrix2x3dv = (PFNGLUNIFORMMATRIX2X3DVPROC) load(userptr, "glUniformMatrix2x3dv"); context->UniformMatrix2x4dv = (PFNGLUNIFORMMATRIX2X4DVPROC) load(userptr, "glUniformMatrix2x4dv"); context->UniformMatrix3dv = (PFNGLUNIFORMMATRIX3DVPROC) load(userptr, "glUniformMatrix3dv"); context->UniformMatrix3x2dv = (PFNGLUNIFORMMATRIX3X2DVPROC) load(userptr, "glUniformMatrix3x2dv"); context->UniformMatrix3x4dv = (PFNGLUNIFORMMATRIX3X4DVPROC) load(userptr, "glUniformMatrix3x4dv"); context->UniformMatrix4dv = (PFNGLUNIFORMMATRIX4DVPROC) load(userptr, "glUniformMatrix4dv"); context->UniformMatrix4x2dv = (PFNGLUNIFORMMATRIX4X2DVPROC) load(userptr, "glUniformMatrix4x2dv"); context->UniformMatrix4x3dv = (PFNGLUNIFORMMATRIX4X3DVPROC) load(userptr, "glUniformMatrix4x3dv"); context->UniformSubroutinesuiv = (PFNGLUNIFORMSUBROUTINESUIVPROC) load(userptr, "glUniformSubroutinesuiv"); } static void glad_gl_load_GL_VERSION_4_1(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_4_1) return; context->ActiveShaderProgram = (PFNGLACTIVESHADERPROGRAMPROC) load(userptr, "glActiveShaderProgram"); context->BindProgramPipeline = (PFNGLBINDPROGRAMPIPELINEPROC) load(userptr, "glBindProgramPipeline"); context->ClearDepthf = (PFNGLCLEARDEPTHFPROC) load(userptr, "glClearDepthf"); context->CreateShaderProgramv = (PFNGLCREATESHADERPROGRAMVPROC) load(userptr, "glCreateShaderProgramv"); context->DeleteProgramPipelines = (PFNGLDELETEPROGRAMPIPELINESPROC) load(userptr, "glDeleteProgramPipelines"); context->DepthRangeArrayv = (PFNGLDEPTHRANGEARRAYVPROC) load(userptr, "glDepthRangeArrayv"); context->DepthRangeIndexed = (PFNGLDEPTHRANGEINDEXEDPROC) load(userptr, "glDepthRangeIndexed"); context->DepthRangef = (PFNGLDEPTHRANGEFPROC) load(userptr, "glDepthRangef"); context->GenProgramPipelines = (PFNGLGENPROGRAMPIPELINESPROC) load(userptr, "glGenProgramPipelines"); context->GetDoublei_v = (PFNGLGETDOUBLEI_VPROC) load(userptr, "glGetDoublei_v"); context->GetFloati_v = (PFNGLGETFLOATI_VPROC) load(userptr, "glGetFloati_v"); context->GetProgramBinary = (PFNGLGETPROGRAMBINARYPROC) load(userptr, "glGetProgramBinary"); context->GetProgramPipelineInfoLog = (PFNGLGETPROGRAMPIPELINEINFOLOGPROC) load(userptr, "glGetProgramPipelineInfoLog"); context->GetProgramPipelineiv = (PFNGLGETPROGRAMPIPELINEIVPROC) load(userptr, "glGetProgramPipelineiv"); context->GetShaderPrecisionFormat = (PFNGLGETSHADERPRECISIONFORMATPROC) load(userptr, "glGetShaderPrecisionFormat"); context->GetVertexAttribLdv = (PFNGLGETVERTEXATTRIBLDVPROC) load(userptr, "glGetVertexAttribLdv"); context->IsProgramPipeline = (PFNGLISPROGRAMPIPELINEPROC) load(userptr, "glIsProgramPipeline"); context->ProgramBinary = (PFNGLPROGRAMBINARYPROC) load(userptr, "glProgramBinary"); context->ProgramParameteri = (PFNGLPROGRAMPARAMETERIPROC) load(userptr, "glProgramParameteri"); context->ProgramUniform1d = (PFNGLPROGRAMUNIFORM1DPROC) load(userptr, "glProgramUniform1d"); context->ProgramUniform1dv = (PFNGLPROGRAMUNIFORM1DVPROC) load(userptr, "glProgramUniform1dv"); context->ProgramUniform1f = (PFNGLPROGRAMUNIFORM1FPROC) load(userptr, "glProgramUniform1f"); context->ProgramUniform1fv = (PFNGLPROGRAMUNIFORM1FVPROC) load(userptr, "glProgramUniform1fv"); context->ProgramUniform1i = (PFNGLPROGRAMUNIFORM1IPROC) load(userptr, "glProgramUniform1i"); context->ProgramUniform1iv = (PFNGLPROGRAMUNIFORM1IVPROC) load(userptr, "glProgramUniform1iv"); context->ProgramUniform1ui = (PFNGLPROGRAMUNIFORM1UIPROC) load(userptr, "glProgramUniform1ui"); context->ProgramUniform1uiv = (PFNGLPROGRAMUNIFORM1UIVPROC) load(userptr, "glProgramUniform1uiv"); context->ProgramUniform2d = (PFNGLPROGRAMUNIFORM2DPROC) load(userptr, "glProgramUniform2d"); context->ProgramUniform2dv = (PFNGLPROGRAMUNIFORM2DVPROC) load(userptr, "glProgramUniform2dv"); context->ProgramUniform2f = (PFNGLPROGRAMUNIFORM2FPROC) load(userptr, "glProgramUniform2f"); context->ProgramUniform2fv = (PFNGLPROGRAMUNIFORM2FVPROC) load(userptr, "glProgramUniform2fv"); context->ProgramUniform2i = (PFNGLPROGRAMUNIFORM2IPROC) load(userptr, "glProgramUniform2i"); context->ProgramUniform2iv = (PFNGLPROGRAMUNIFORM2IVPROC) load(userptr, "glProgramUniform2iv"); context->ProgramUniform2ui = (PFNGLPROGRAMUNIFORM2UIPROC) load(userptr, "glProgramUniform2ui"); context->ProgramUniform2uiv = (PFNGLPROGRAMUNIFORM2UIVPROC) load(userptr, "glProgramUniform2uiv"); context->ProgramUniform3d = (PFNGLPROGRAMUNIFORM3DPROC) load(userptr, "glProgramUniform3d"); context->ProgramUniform3dv = (PFNGLPROGRAMUNIFORM3DVPROC) load(userptr, "glProgramUniform3dv"); context->ProgramUniform3f = (PFNGLPROGRAMUNIFORM3FPROC) load(userptr, "glProgramUniform3f"); context->ProgramUniform3fv = (PFNGLPROGRAMUNIFORM3FVPROC) load(userptr, "glProgramUniform3fv"); context->ProgramUniform3i = (PFNGLPROGRAMUNIFORM3IPROC) load(userptr, "glProgramUniform3i"); context->ProgramUniform3iv = (PFNGLPROGRAMUNIFORM3IVPROC) load(userptr, "glProgramUniform3iv"); context->ProgramUniform3ui = (PFNGLPROGRAMUNIFORM3UIPROC) load(userptr, "glProgramUniform3ui"); context->ProgramUniform3uiv = (PFNGLPROGRAMUNIFORM3UIVPROC) load(userptr, "glProgramUniform3uiv"); context->ProgramUniform4d = (PFNGLPROGRAMUNIFORM4DPROC) load(userptr, "glProgramUniform4d"); context->ProgramUniform4dv = (PFNGLPROGRAMUNIFORM4DVPROC) load(userptr, "glProgramUniform4dv"); context->ProgramUniform4f = (PFNGLPROGRAMUNIFORM4FPROC) load(userptr, "glProgramUniform4f"); context->ProgramUniform4fv = (PFNGLPROGRAMUNIFORM4FVPROC) load(userptr, "glProgramUniform4fv"); context->ProgramUniform4i = (PFNGLPROGRAMUNIFORM4IPROC) load(userptr, "glProgramUniform4i"); context->ProgramUniform4iv = (PFNGLPROGRAMUNIFORM4IVPROC) load(userptr, "glProgramUniform4iv"); context->ProgramUniform4ui = (PFNGLPROGRAMUNIFORM4UIPROC) load(userptr, "glProgramUniform4ui"); context->ProgramUniform4uiv = (PFNGLPROGRAMUNIFORM4UIVPROC) load(userptr, "glProgramUniform4uiv"); context->ProgramUniformMatrix2dv = (PFNGLPROGRAMUNIFORMMATRIX2DVPROC) load(userptr, "glProgramUniformMatrix2dv"); context->ProgramUniformMatrix2fv = (PFNGLPROGRAMUNIFORMMATRIX2FVPROC) load(userptr, "glProgramUniformMatrix2fv"); context->ProgramUniformMatrix2x3dv = (PFNGLPROGRAMUNIFORMMATRIX2X3DVPROC) load(userptr, "glProgramUniformMatrix2x3dv"); context->ProgramUniformMatrix2x3fv = (PFNGLPROGRAMUNIFORMMATRIX2X3FVPROC) load(userptr, "glProgramUniformMatrix2x3fv"); context->ProgramUniformMatrix2x4dv = (PFNGLPROGRAMUNIFORMMATRIX2X4DVPROC) load(userptr, "glProgramUniformMatrix2x4dv"); context->ProgramUniformMatrix2x4fv = (PFNGLPROGRAMUNIFORMMATRIX2X4FVPROC) load(userptr, "glProgramUniformMatrix2x4fv"); context->ProgramUniformMatrix3dv = (PFNGLPROGRAMUNIFORMMATRIX3DVPROC) load(userptr, "glProgramUniformMatrix3dv"); context->ProgramUniformMatrix3fv = (PFNGLPROGRAMUNIFORMMATRIX3FVPROC) load(userptr, "glProgramUniformMatrix3fv"); context->ProgramUniformMatrix3x2dv = (PFNGLPROGRAMUNIFORMMATRIX3X2DVPROC) load(userptr, "glProgramUniformMatrix3x2dv"); context->ProgramUniformMatrix3x2fv = (PFNGLPROGRAMUNIFORMMATRIX3X2FVPROC) load(userptr, "glProgramUniformMatrix3x2fv"); context->ProgramUniformMatrix3x4dv = (PFNGLPROGRAMUNIFORMMATRIX3X4DVPROC) load(userptr, "glProgramUniformMatrix3x4dv"); context->ProgramUniformMatrix3x4fv = (PFNGLPROGRAMUNIFORMMATRIX3X4FVPROC) load(userptr, "glProgramUniformMatrix3x4fv"); context->ProgramUniformMatrix4dv = (PFNGLPROGRAMUNIFORMMATRIX4DVPROC) load(userptr, "glProgramUniformMatrix4dv"); context->ProgramUniformMatrix4fv = (PFNGLPROGRAMUNIFORMMATRIX4FVPROC) load(userptr, "glProgramUniformMatrix4fv"); context->ProgramUniformMatrix4x2dv = (PFNGLPROGRAMUNIFORMMATRIX4X2DVPROC) load(userptr, "glProgramUniformMatrix4x2dv"); context->ProgramUniformMatrix4x2fv = (PFNGLPROGRAMUNIFORMMATRIX4X2FVPROC) load(userptr, "glProgramUniformMatrix4x2fv"); context->ProgramUniformMatrix4x3dv = (PFNGLPROGRAMUNIFORMMATRIX4X3DVPROC) load(userptr, "glProgramUniformMatrix4x3dv"); context->ProgramUniformMatrix4x3fv = (PFNGLPROGRAMUNIFORMMATRIX4X3FVPROC) load(userptr, "glProgramUniformMatrix4x3fv"); context->ReleaseShaderCompiler = (PFNGLRELEASESHADERCOMPILERPROC) load(userptr, "glReleaseShaderCompiler"); context->ScissorArrayv = (PFNGLSCISSORARRAYVPROC) load(userptr, "glScissorArrayv"); context->ScissorIndexed = (PFNGLSCISSORINDEXEDPROC) load(userptr, "glScissorIndexed"); context->ScissorIndexedv = (PFNGLSCISSORINDEXEDVPROC) load(userptr, "glScissorIndexedv"); context->ShaderBinary = (PFNGLSHADERBINARYPROC) load(userptr, "glShaderBinary"); context->UseProgramStages = (PFNGLUSEPROGRAMSTAGESPROC) load(userptr, "glUseProgramStages"); context->ValidateProgramPipeline = (PFNGLVALIDATEPROGRAMPIPELINEPROC) load(userptr, "glValidateProgramPipeline"); context->VertexAttribL1d = (PFNGLVERTEXATTRIBL1DPROC) load(userptr, "glVertexAttribL1d"); context->VertexAttribL1dv = (PFNGLVERTEXATTRIBL1DVPROC) load(userptr, "glVertexAttribL1dv"); context->VertexAttribL2d = (PFNGLVERTEXATTRIBL2DPROC) load(userptr, "glVertexAttribL2d"); context->VertexAttribL2dv = (PFNGLVERTEXATTRIBL2DVPROC) load(userptr, "glVertexAttribL2dv"); context->VertexAttribL3d = (PFNGLVERTEXATTRIBL3DPROC) load(userptr, "glVertexAttribL3d"); context->VertexAttribL3dv = (PFNGLVERTEXATTRIBL3DVPROC) load(userptr, "glVertexAttribL3dv"); context->VertexAttribL4d = (PFNGLVERTEXATTRIBL4DPROC) load(userptr, "glVertexAttribL4d"); context->VertexAttribL4dv = (PFNGLVERTEXATTRIBL4DVPROC) load(userptr, "glVertexAttribL4dv"); context->VertexAttribLPointer = (PFNGLVERTEXATTRIBLPOINTERPROC) load(userptr, "glVertexAttribLPointer"); context->ViewportArrayv = (PFNGLVIEWPORTARRAYVPROC) load(userptr, "glViewportArrayv"); context->ViewportIndexedf = (PFNGLVIEWPORTINDEXEDFPROC) load(userptr, "glViewportIndexedf"); context->ViewportIndexedfv = (PFNGLVIEWPORTINDEXEDFVPROC) load(userptr, "glViewportIndexedfv"); } static void glad_gl_load_GL_VERSION_4_2(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_4_2) return; context->BindImageTexture = (PFNGLBINDIMAGETEXTUREPROC) load(userptr, "glBindImageTexture"); context->DrawArraysInstancedBaseInstance = (PFNGLDRAWARRAYSINSTANCEDBASEINSTANCEPROC) load(userptr, "glDrawArraysInstancedBaseInstance"); context->DrawElementsInstancedBaseInstance = (PFNGLDRAWELEMENTSINSTANCEDBASEINSTANCEPROC) load(userptr, "glDrawElementsInstancedBaseInstance"); context->DrawElementsInstancedBaseVertexBaseInstance = (PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXBASEINSTANCEPROC) load(userptr, "glDrawElementsInstancedBaseVertexBaseInstance"); context->DrawTransformFeedbackInstanced = (PFNGLDRAWTRANSFORMFEEDBACKINSTANCEDPROC) load(userptr, "glDrawTransformFeedbackInstanced"); context->DrawTransformFeedbackStreamInstanced = (PFNGLDRAWTRANSFORMFEEDBACKSTREAMINSTANCEDPROC) load(userptr, "glDrawTransformFeedbackStreamInstanced"); context->GetActiveAtomicCounterBufferiv = (PFNGLGETACTIVEATOMICCOUNTERBUFFERIVPROC) load(userptr, "glGetActiveAtomicCounterBufferiv"); context->GetInternalformativ = (PFNGLGETINTERNALFORMATIVPROC) load(userptr, "glGetInternalformativ"); context->MemoryBarrier = (PFNGLMEMORYBARRIERPROC) load(userptr, "glMemoryBarrier"); context->TexStorage1D = (PFNGLTEXSTORAGE1DPROC) load(userptr, "glTexStorage1D"); context->TexStorage2D = (PFNGLTEXSTORAGE2DPROC) load(userptr, "glTexStorage2D"); context->TexStorage3D = (PFNGLTEXSTORAGE3DPROC) load(userptr, "glTexStorage3D"); } static void glad_gl_load_GL_VERSION_4_3(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_4_3) return; context->BindVertexBuffer = (PFNGLBINDVERTEXBUFFERPROC) load(userptr, "glBindVertexBuffer"); context->ClearBufferData = (PFNGLCLEARBUFFERDATAPROC) load(userptr, "glClearBufferData"); context->ClearBufferSubData = (PFNGLCLEARBUFFERSUBDATAPROC) load(userptr, "glClearBufferSubData"); context->CopyImageSubData = (PFNGLCOPYIMAGESUBDATAPROC) load(userptr, "glCopyImageSubData"); context->DebugMessageCallback = (PFNGLDEBUGMESSAGECALLBACKPROC) load(userptr, "glDebugMessageCallback"); context->DebugMessageControl = (PFNGLDEBUGMESSAGECONTROLPROC) load(userptr, "glDebugMessageControl"); context->DebugMessageInsert = (PFNGLDEBUGMESSAGEINSERTPROC) load(userptr, "glDebugMessageInsert"); context->DispatchCompute = (PFNGLDISPATCHCOMPUTEPROC) load(userptr, "glDispatchCompute"); context->DispatchComputeIndirect = (PFNGLDISPATCHCOMPUTEINDIRECTPROC) load(userptr, "glDispatchComputeIndirect"); context->FramebufferParameteri = (PFNGLFRAMEBUFFERPARAMETERIPROC) load(userptr, "glFramebufferParameteri"); context->GetDebugMessageLog = (PFNGLGETDEBUGMESSAGELOGPROC) load(userptr, "glGetDebugMessageLog"); context->GetFramebufferParameteriv = (PFNGLGETFRAMEBUFFERPARAMETERIVPROC) load(userptr, "glGetFramebufferParameteriv"); context->GetInternalformati64v = (PFNGLGETINTERNALFORMATI64VPROC) load(userptr, "glGetInternalformati64v"); context->GetObjectLabel = (PFNGLGETOBJECTLABELPROC) load(userptr, "glGetObjectLabel"); context->GetObjectPtrLabel = (PFNGLGETOBJECTPTRLABELPROC) load(userptr, "glGetObjectPtrLabel"); context->GetPointerv = (PFNGLGETPOINTERVPROC) load(userptr, "glGetPointerv"); context->GetProgramInterfaceiv = (PFNGLGETPROGRAMINTERFACEIVPROC) load(userptr, "glGetProgramInterfaceiv"); context->GetProgramResourceIndex = (PFNGLGETPROGRAMRESOURCEINDEXPROC) load(userptr, "glGetProgramResourceIndex"); context->GetProgramResourceLocation = (PFNGLGETPROGRAMRESOURCELOCATIONPROC) load(userptr, "glGetProgramResourceLocation"); context->GetProgramResourceLocationIndex = (PFNGLGETPROGRAMRESOURCELOCATIONINDEXPROC) load(userptr, "glGetProgramResourceLocationIndex"); context->GetProgramResourceName = (PFNGLGETPROGRAMRESOURCENAMEPROC) load(userptr, "glGetProgramResourceName"); context->GetProgramResourceiv = (PFNGLGETPROGRAMRESOURCEIVPROC) load(userptr, "glGetProgramResourceiv"); context->InvalidateBufferData = (PFNGLINVALIDATEBUFFERDATAPROC) load(userptr, "glInvalidateBufferData"); context->InvalidateBufferSubData = (PFNGLINVALIDATEBUFFERSUBDATAPROC) load(userptr, "glInvalidateBufferSubData"); context->InvalidateFramebuffer = (PFNGLINVALIDATEFRAMEBUFFERPROC) load(userptr, "glInvalidateFramebuffer"); context->InvalidateSubFramebuffer = (PFNGLINVALIDATESUBFRAMEBUFFERPROC) load(userptr, "glInvalidateSubFramebuffer"); context->InvalidateTexImage = (PFNGLINVALIDATETEXIMAGEPROC) load(userptr, "glInvalidateTexImage"); context->InvalidateTexSubImage = (PFNGLINVALIDATETEXSUBIMAGEPROC) load(userptr, "glInvalidateTexSubImage"); context->MultiDrawArraysIndirect = (PFNGLMULTIDRAWARRAYSINDIRECTPROC) load(userptr, "glMultiDrawArraysIndirect"); context->MultiDrawElementsIndirect = (PFNGLMULTIDRAWELEMENTSINDIRECTPROC) load(userptr, "glMultiDrawElementsIndirect"); context->ObjectLabel = (PFNGLOBJECTLABELPROC) load(userptr, "glObjectLabel"); context->ObjectPtrLabel = (PFNGLOBJECTPTRLABELPROC) load(userptr, "glObjectPtrLabel"); context->PopDebugGroup = (PFNGLPOPDEBUGGROUPPROC) load(userptr, "glPopDebugGroup"); context->PushDebugGroup = (PFNGLPUSHDEBUGGROUPPROC) load(userptr, "glPushDebugGroup"); context->ShaderStorageBlockBinding = (PFNGLSHADERSTORAGEBLOCKBINDINGPROC) load(userptr, "glShaderStorageBlockBinding"); context->TexBufferRange = (PFNGLTEXBUFFERRANGEPROC) load(userptr, "glTexBufferRange"); context->TexStorage2DMultisample = (PFNGLTEXSTORAGE2DMULTISAMPLEPROC) load(userptr, "glTexStorage2DMultisample"); context->TexStorage3DMultisample = (PFNGLTEXSTORAGE3DMULTISAMPLEPROC) load(userptr, "glTexStorage3DMultisample"); context->TextureView = (PFNGLTEXTUREVIEWPROC) load(userptr, "glTextureView"); context->VertexAttribBinding = (PFNGLVERTEXATTRIBBINDINGPROC) load(userptr, "glVertexAttribBinding"); context->VertexAttribFormat = (PFNGLVERTEXATTRIBFORMATPROC) load(userptr, "glVertexAttribFormat"); context->VertexAttribIFormat = (PFNGLVERTEXATTRIBIFORMATPROC) load(userptr, "glVertexAttribIFormat"); context->VertexAttribLFormat = (PFNGLVERTEXATTRIBLFORMATPROC) load(userptr, "glVertexAttribLFormat"); context->VertexBindingDivisor = (PFNGLVERTEXBINDINGDIVISORPROC) load(userptr, "glVertexBindingDivisor"); } static void glad_gl_load_GL_VERSION_4_4(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_4_4) return; context->BindBuffersBase = (PFNGLBINDBUFFERSBASEPROC) load(userptr, "glBindBuffersBase"); context->BindBuffersRange = (PFNGLBINDBUFFERSRANGEPROC) load(userptr, "glBindBuffersRange"); context->BindImageTextures = (PFNGLBINDIMAGETEXTURESPROC) load(userptr, "glBindImageTextures"); context->BindSamplers = (PFNGLBINDSAMPLERSPROC) load(userptr, "glBindSamplers"); context->BindTextures = (PFNGLBINDTEXTURESPROC) load(userptr, "glBindTextures"); context->BindVertexBuffers = (PFNGLBINDVERTEXBUFFERSPROC) load(userptr, "glBindVertexBuffers"); context->BufferStorage = (PFNGLBUFFERSTORAGEPROC) load(userptr, "glBufferStorage"); context->ClearTexImage = (PFNGLCLEARTEXIMAGEPROC) load(userptr, "glClearTexImage"); context->ClearTexSubImage = (PFNGLCLEARTEXSUBIMAGEPROC) load(userptr, "glClearTexSubImage"); } static void glad_gl_load_GL_VERSION_4_5(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_4_5) return; context->BindTextureUnit = (PFNGLBINDTEXTUREUNITPROC) load(userptr, "glBindTextureUnit"); context->BlitNamedFramebuffer = (PFNGLBLITNAMEDFRAMEBUFFERPROC) load(userptr, "glBlitNamedFramebuffer"); context->CheckNamedFramebufferStatus = (PFNGLCHECKNAMEDFRAMEBUFFERSTATUSPROC) load(userptr, "glCheckNamedFramebufferStatus"); context->ClearNamedBufferData = (PFNGLCLEARNAMEDBUFFERDATAPROC) load(userptr, "glClearNamedBufferData"); context->ClearNamedBufferSubData = (PFNGLCLEARNAMEDBUFFERSUBDATAPROC) load(userptr, "glClearNamedBufferSubData"); context->ClearNamedFramebufferfi = (PFNGLCLEARNAMEDFRAMEBUFFERFIPROC) load(userptr, "glClearNamedFramebufferfi"); context->ClearNamedFramebufferfv = (PFNGLCLEARNAMEDFRAMEBUFFERFVPROC) load(userptr, "glClearNamedFramebufferfv"); context->ClearNamedFramebufferiv = (PFNGLCLEARNAMEDFRAMEBUFFERIVPROC) load(userptr, "glClearNamedFramebufferiv"); context->ClearNamedFramebufferuiv = (PFNGLCLEARNAMEDFRAMEBUFFERUIVPROC) load(userptr, "glClearNamedFramebufferuiv"); context->ClipControl = (PFNGLCLIPCONTROLPROC) load(userptr, "glClipControl"); context->CompressedTextureSubImage1D = (PFNGLCOMPRESSEDTEXTURESUBIMAGE1DPROC) load(userptr, "glCompressedTextureSubImage1D"); context->CompressedTextureSubImage2D = (PFNGLCOMPRESSEDTEXTURESUBIMAGE2DPROC) load(userptr, "glCompressedTextureSubImage2D"); context->CompressedTextureSubImage3D = (PFNGLCOMPRESSEDTEXTURESUBIMAGE3DPROC) load(userptr, "glCompressedTextureSubImage3D"); context->CopyNamedBufferSubData = (PFNGLCOPYNAMEDBUFFERSUBDATAPROC) load(userptr, "glCopyNamedBufferSubData"); context->CopyTextureSubImage1D = (PFNGLCOPYTEXTURESUBIMAGE1DPROC) load(userptr, "glCopyTextureSubImage1D"); context->CopyTextureSubImage2D = (PFNGLCOPYTEXTURESUBIMAGE2DPROC) load(userptr, "glCopyTextureSubImage2D"); context->CopyTextureSubImage3D = (PFNGLCOPYTEXTURESUBIMAGE3DPROC) load(userptr, "glCopyTextureSubImage3D"); context->CreateBuffers = (PFNGLCREATEBUFFERSPROC) load(userptr, "glCreateBuffers"); context->CreateFramebuffers = (PFNGLCREATEFRAMEBUFFERSPROC) load(userptr, "glCreateFramebuffers"); context->CreateProgramPipelines = (PFNGLCREATEPROGRAMPIPELINESPROC) load(userptr, "glCreateProgramPipelines"); context->CreateQueries = (PFNGLCREATEQUERIESPROC) load(userptr, "glCreateQueries"); context->CreateRenderbuffers = (PFNGLCREATERENDERBUFFERSPROC) load(userptr, "glCreateRenderbuffers"); context->CreateSamplers = (PFNGLCREATESAMPLERSPROC) load(userptr, "glCreateSamplers"); context->CreateTextures = (PFNGLCREATETEXTURESPROC) load(userptr, "glCreateTextures"); context->CreateTransformFeedbacks = (PFNGLCREATETRANSFORMFEEDBACKSPROC) load(userptr, "glCreateTransformFeedbacks"); context->CreateVertexArrays = (PFNGLCREATEVERTEXARRAYSPROC) load(userptr, "glCreateVertexArrays"); context->DisableVertexArrayAttrib = (PFNGLDISABLEVERTEXARRAYATTRIBPROC) load(userptr, "glDisableVertexArrayAttrib"); context->EnableVertexArrayAttrib = (PFNGLENABLEVERTEXARRAYATTRIBPROC) load(userptr, "glEnableVertexArrayAttrib"); context->FlushMappedNamedBufferRange = (PFNGLFLUSHMAPPEDNAMEDBUFFERRANGEPROC) load(userptr, "glFlushMappedNamedBufferRange"); context->GenerateTextureMipmap = (PFNGLGENERATETEXTUREMIPMAPPROC) load(userptr, "glGenerateTextureMipmap"); context->GetCompressedTextureImage = (PFNGLGETCOMPRESSEDTEXTUREIMAGEPROC) load(userptr, "glGetCompressedTextureImage"); context->GetCompressedTextureSubImage = (PFNGLGETCOMPRESSEDTEXTURESUBIMAGEPROC) load(userptr, "glGetCompressedTextureSubImage"); context->GetGraphicsResetStatus = (PFNGLGETGRAPHICSRESETSTATUSPROC) load(userptr, "glGetGraphicsResetStatus"); context->GetNamedBufferParameteri64v = (PFNGLGETNAMEDBUFFERPARAMETERI64VPROC) load(userptr, "glGetNamedBufferParameteri64v"); context->GetNamedBufferParameteriv = (PFNGLGETNAMEDBUFFERPARAMETERIVPROC) load(userptr, "glGetNamedBufferParameteriv"); context->GetNamedBufferPointerv = (PFNGLGETNAMEDBUFFERPOINTERVPROC) load(userptr, "glGetNamedBufferPointerv"); context->GetNamedBufferSubData = (PFNGLGETNAMEDBUFFERSUBDATAPROC) load(userptr, "glGetNamedBufferSubData"); context->GetNamedFramebufferAttachmentParameteriv = (PFNGLGETNAMEDFRAMEBUFFERATTACHMENTPARAMETERIVPROC) load(userptr, "glGetNamedFramebufferAttachmentParameteriv"); context->GetNamedFramebufferParameteriv = (PFNGLGETNAMEDFRAMEBUFFERPARAMETERIVPROC) load(userptr, "glGetNamedFramebufferParameteriv"); context->GetNamedRenderbufferParameteriv = (PFNGLGETNAMEDRENDERBUFFERPARAMETERIVPROC) load(userptr, "glGetNamedRenderbufferParameteriv"); context->GetQueryBufferObjecti64v = (PFNGLGETQUERYBUFFEROBJECTI64VPROC) load(userptr, "glGetQueryBufferObjecti64v"); context->GetQueryBufferObjectiv = (PFNGLGETQUERYBUFFEROBJECTIVPROC) load(userptr, "glGetQueryBufferObjectiv"); context->GetQueryBufferObjectui64v = (PFNGLGETQUERYBUFFEROBJECTUI64VPROC) load(userptr, "glGetQueryBufferObjectui64v"); context->GetQueryBufferObjectuiv = (PFNGLGETQUERYBUFFEROBJECTUIVPROC) load(userptr, "glGetQueryBufferObjectuiv"); context->GetTextureImage = (PFNGLGETTEXTUREIMAGEPROC) load(userptr, "glGetTextureImage"); context->GetTextureLevelParameterfv = (PFNGLGETTEXTURELEVELPARAMETERFVPROC) load(userptr, "glGetTextureLevelParameterfv"); context->GetTextureLevelParameteriv = (PFNGLGETTEXTURELEVELPARAMETERIVPROC) load(userptr, "glGetTextureLevelParameteriv"); context->GetTextureParameterIiv = (PFNGLGETTEXTUREPARAMETERIIVPROC) load(userptr, "glGetTextureParameterIiv"); context->GetTextureParameterIuiv = (PFNGLGETTEXTUREPARAMETERIUIVPROC) load(userptr, "glGetTextureParameterIuiv"); context->GetTextureParameterfv = (PFNGLGETTEXTUREPARAMETERFVPROC) load(userptr, "glGetTextureParameterfv"); context->GetTextureParameteriv = (PFNGLGETTEXTUREPARAMETERIVPROC) load(userptr, "glGetTextureParameteriv"); context->GetTextureSubImage = (PFNGLGETTEXTURESUBIMAGEPROC) load(userptr, "glGetTextureSubImage"); context->GetTransformFeedbacki64_v = (PFNGLGETTRANSFORMFEEDBACKI64_VPROC) load(userptr, "glGetTransformFeedbacki64_v"); context->GetTransformFeedbacki_v = (PFNGLGETTRANSFORMFEEDBACKI_VPROC) load(userptr, "glGetTransformFeedbacki_v"); context->GetTransformFeedbackiv = (PFNGLGETTRANSFORMFEEDBACKIVPROC) load(userptr, "glGetTransformFeedbackiv"); context->GetVertexArrayIndexed64iv = (PFNGLGETVERTEXARRAYINDEXED64IVPROC) load(userptr, "glGetVertexArrayIndexed64iv"); context->GetVertexArrayIndexediv = (PFNGLGETVERTEXARRAYINDEXEDIVPROC) load(userptr, "glGetVertexArrayIndexediv"); context->GetVertexArrayiv = (PFNGLGETVERTEXARRAYIVPROC) load(userptr, "glGetVertexArrayiv"); context->GetnColorTable = (PFNGLGETNCOLORTABLEPROC) load(userptr, "glGetnColorTable"); context->GetnCompressedTexImage = (PFNGLGETNCOMPRESSEDTEXIMAGEPROC) load(userptr, "glGetnCompressedTexImage"); context->GetnConvolutionFilter = (PFNGLGETNCONVOLUTIONFILTERPROC) load(userptr, "glGetnConvolutionFilter"); context->GetnHistogram = (PFNGLGETNHISTOGRAMPROC) load(userptr, "glGetnHistogram"); context->GetnMapdv = (PFNGLGETNMAPDVPROC) load(userptr, "glGetnMapdv"); context->GetnMapfv = (PFNGLGETNMAPFVPROC) load(userptr, "glGetnMapfv"); context->GetnMapiv = (PFNGLGETNMAPIVPROC) load(userptr, "glGetnMapiv"); context->GetnMinmax = (PFNGLGETNMINMAXPROC) load(userptr, "glGetnMinmax"); context->GetnPixelMapfv = (PFNGLGETNPIXELMAPFVPROC) load(userptr, "glGetnPixelMapfv"); context->GetnPixelMapuiv = (PFNGLGETNPIXELMAPUIVPROC) load(userptr, "glGetnPixelMapuiv"); context->GetnPixelMapusv = (PFNGLGETNPIXELMAPUSVPROC) load(userptr, "glGetnPixelMapusv"); context->GetnPolygonStipple = (PFNGLGETNPOLYGONSTIPPLEPROC) load(userptr, "glGetnPolygonStipple"); context->GetnSeparableFilter = (PFNGLGETNSEPARABLEFILTERPROC) load(userptr, "glGetnSeparableFilter"); context->GetnTexImage = (PFNGLGETNTEXIMAGEPROC) load(userptr, "glGetnTexImage"); context->GetnUniformdv = (PFNGLGETNUNIFORMDVPROC) load(userptr, "glGetnUniformdv"); context->GetnUniformfv = (PFNGLGETNUNIFORMFVPROC) load(userptr, "glGetnUniformfv"); context->GetnUniformiv = (PFNGLGETNUNIFORMIVPROC) load(userptr, "glGetnUniformiv"); context->GetnUniformuiv = (PFNGLGETNUNIFORMUIVPROC) load(userptr, "glGetnUniformuiv"); context->InvalidateNamedFramebufferData = (PFNGLINVALIDATENAMEDFRAMEBUFFERDATAPROC) load(userptr, "glInvalidateNamedFramebufferData"); context->InvalidateNamedFramebufferSubData = (PFNGLINVALIDATENAMEDFRAMEBUFFERSUBDATAPROC) load(userptr, "glInvalidateNamedFramebufferSubData"); context->MapNamedBuffer = (PFNGLMAPNAMEDBUFFERPROC) load(userptr, "glMapNamedBuffer"); context->MapNamedBufferRange = (PFNGLMAPNAMEDBUFFERRANGEPROC) load(userptr, "glMapNamedBufferRange"); context->MemoryBarrierByRegion = (PFNGLMEMORYBARRIERBYREGIONPROC) load(userptr, "glMemoryBarrierByRegion"); context->NamedBufferData = (PFNGLNAMEDBUFFERDATAPROC) load(userptr, "glNamedBufferData"); context->NamedBufferStorage = (PFNGLNAMEDBUFFERSTORAGEPROC) load(userptr, "glNamedBufferStorage"); context->NamedBufferSubData = (PFNGLNAMEDBUFFERSUBDATAPROC) load(userptr, "glNamedBufferSubData"); context->NamedFramebufferDrawBuffer = (PFNGLNAMEDFRAMEBUFFERDRAWBUFFERPROC) load(userptr, "glNamedFramebufferDrawBuffer"); context->NamedFramebufferDrawBuffers = (PFNGLNAMEDFRAMEBUFFERDRAWBUFFERSPROC) load(userptr, "glNamedFramebufferDrawBuffers"); context->NamedFramebufferParameteri = (PFNGLNAMEDFRAMEBUFFERPARAMETERIPROC) load(userptr, "glNamedFramebufferParameteri"); context->NamedFramebufferReadBuffer = (PFNGLNAMEDFRAMEBUFFERREADBUFFERPROC) load(userptr, "glNamedFramebufferReadBuffer"); context->NamedFramebufferRenderbuffer = (PFNGLNAMEDFRAMEBUFFERRENDERBUFFERPROC) load(userptr, "glNamedFramebufferRenderbuffer"); context->NamedFramebufferTexture = (PFNGLNAMEDFRAMEBUFFERTEXTUREPROC) load(userptr, "glNamedFramebufferTexture"); context->NamedFramebufferTextureLayer = (PFNGLNAMEDFRAMEBUFFERTEXTURELAYERPROC) load(userptr, "glNamedFramebufferTextureLayer"); context->NamedRenderbufferStorage = (PFNGLNAMEDRENDERBUFFERSTORAGEPROC) load(userptr, "glNamedRenderbufferStorage"); context->NamedRenderbufferStorageMultisample = (PFNGLNAMEDRENDERBUFFERSTORAGEMULTISAMPLEPROC) load(userptr, "glNamedRenderbufferStorageMultisample"); context->ReadnPixels = (PFNGLREADNPIXELSPROC) load(userptr, "glReadnPixels"); context->TextureBarrier = (PFNGLTEXTUREBARRIERPROC) load(userptr, "glTextureBarrier"); context->TextureBuffer = (PFNGLTEXTUREBUFFERPROC) load(userptr, "glTextureBuffer"); context->TextureBufferRange = (PFNGLTEXTUREBUFFERRANGEPROC) load(userptr, "glTextureBufferRange"); context->TextureParameterIiv = (PFNGLTEXTUREPARAMETERIIVPROC) load(userptr, "glTextureParameterIiv"); context->TextureParameterIuiv = (PFNGLTEXTUREPARAMETERIUIVPROC) load(userptr, "glTextureParameterIuiv"); context->TextureParameterf = (PFNGLTEXTUREPARAMETERFPROC) load(userptr, "glTextureParameterf"); context->TextureParameterfv = (PFNGLTEXTUREPARAMETERFVPROC) load(userptr, "glTextureParameterfv"); context->TextureParameteri = (PFNGLTEXTUREPARAMETERIPROC) load(userptr, "glTextureParameteri"); context->TextureParameteriv = (PFNGLTEXTUREPARAMETERIVPROC) load(userptr, "glTextureParameteriv"); context->TextureStorage1D = (PFNGLTEXTURESTORAGE1DPROC) load(userptr, "glTextureStorage1D"); context->TextureStorage2D = (PFNGLTEXTURESTORAGE2DPROC) load(userptr, "glTextureStorage2D"); context->TextureStorage2DMultisample = (PFNGLTEXTURESTORAGE2DMULTISAMPLEPROC) load(userptr, "glTextureStorage2DMultisample"); context->TextureStorage3D = (PFNGLTEXTURESTORAGE3DPROC) load(userptr, "glTextureStorage3D"); context->TextureStorage3DMultisample = (PFNGLTEXTURESTORAGE3DMULTISAMPLEPROC) load(userptr, "glTextureStorage3DMultisample"); context->TextureSubImage1D = (PFNGLTEXTURESUBIMAGE1DPROC) load(userptr, "glTextureSubImage1D"); context->TextureSubImage2D = (PFNGLTEXTURESUBIMAGE2DPROC) load(userptr, "glTextureSubImage2D"); context->TextureSubImage3D = (PFNGLTEXTURESUBIMAGE3DPROC) load(userptr, "glTextureSubImage3D"); context->TransformFeedbackBufferBase = (PFNGLTRANSFORMFEEDBACKBUFFERBASEPROC) load(userptr, "glTransformFeedbackBufferBase"); context->TransformFeedbackBufferRange = (PFNGLTRANSFORMFEEDBACKBUFFERRANGEPROC) load(userptr, "glTransformFeedbackBufferRange"); context->UnmapNamedBuffer = (PFNGLUNMAPNAMEDBUFFERPROC) load(userptr, "glUnmapNamedBuffer"); context->VertexArrayAttribBinding = (PFNGLVERTEXARRAYATTRIBBINDINGPROC) load(userptr, "glVertexArrayAttribBinding"); context->VertexArrayAttribFormat = (PFNGLVERTEXARRAYATTRIBFORMATPROC) load(userptr, "glVertexArrayAttribFormat"); context->VertexArrayAttribIFormat = (PFNGLVERTEXARRAYATTRIBIFORMATPROC) load(userptr, "glVertexArrayAttribIFormat"); context->VertexArrayAttribLFormat = (PFNGLVERTEXARRAYATTRIBLFORMATPROC) load(userptr, "glVertexArrayAttribLFormat"); context->VertexArrayBindingDivisor = (PFNGLVERTEXARRAYBINDINGDIVISORPROC) load(userptr, "glVertexArrayBindingDivisor"); context->VertexArrayElementBuffer = (PFNGLVERTEXARRAYELEMENTBUFFERPROC) load(userptr, "glVertexArrayElementBuffer"); context->VertexArrayVertexBuffer = (PFNGLVERTEXARRAYVERTEXBUFFERPROC) load(userptr, "glVertexArrayVertexBuffer"); context->VertexArrayVertexBuffers = (PFNGLVERTEXARRAYVERTEXBUFFERSPROC) load(userptr, "glVertexArrayVertexBuffers"); } static void glad_gl_load_GL_VERSION_4_6(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { if(!context->VERSION_4_6) return; context->MultiDrawArraysIndirectCount = (PFNGLMULTIDRAWARRAYSINDIRECTCOUNTPROC) load(userptr, "glMultiDrawArraysIndirectCount"); context->MultiDrawElementsIndirectCount = (PFNGLMULTIDRAWELEMENTSINDIRECTCOUNTPROC) load(userptr, "glMultiDrawElementsIndirectCount"); context->PolygonOffsetClamp = (PFNGLPOLYGONOFFSETCLAMPPROC) load(userptr, "glPolygonOffsetClamp"); context->SpecializeShader = (PFNGLSPECIALIZESHADERPROC) load(userptr, "glSpecializeShader"); } #if defined(GL_ES_VERSION_3_0) || defined(GL_VERSION_3_0) #define GLAD_GL_IS_SOME_NEW_VERSION 1 #else #define GLAD_GL_IS_SOME_NEW_VERSION 0 #endif static int glad_gl_get_extensions(GladGLContext *context, int version, const char **out_exts, unsigned int *out_num_exts_i, char ***out_exts_i) { #if GLAD_GL_IS_SOME_NEW_VERSION if(GLAD_VERSION_MAJOR(version) < 3) { #else (void) version; (void) out_num_exts_i; (void) out_exts_i; #endif if (context->GetString == NULL) { return 0; } *out_exts = (const char *)context->GetString(GL_EXTENSIONS); #if GLAD_GL_IS_SOME_NEW_VERSION } else { unsigned int index = 0; unsigned int num_exts_i = 0; char **exts_i = NULL; if (context->GetStringi == NULL || context->GetIntegerv == NULL) { return 0; } context->GetIntegerv(GL_NUM_EXTENSIONS, (int*) &num_exts_i); if (num_exts_i > 0) { exts_i = (char **) malloc(num_exts_i * (sizeof *exts_i)); } if (exts_i == NULL) { return 0; } for(index = 0; index < num_exts_i; index++) { const char *gl_str_tmp = (const char*) context->GetStringi(GL_EXTENSIONS, index); size_t len = strlen(gl_str_tmp) + 1; char *local_str = (char*) malloc(len * sizeof(char)); if(local_str != NULL) { memcpy(local_str, gl_str_tmp, len * sizeof(char)); } exts_i[index] = local_str; } *out_num_exts_i = num_exts_i; *out_exts_i = exts_i; } #endif return 1; } static void glad_gl_free_extensions(char **exts_i, unsigned int num_exts_i) { if (exts_i != NULL) { unsigned int index; for(index = 0; index < num_exts_i; index++) { free((void *) (exts_i[index])); } free((void *)exts_i); exts_i = NULL; } } static int glad_gl_has_extension(int version, const char *exts, unsigned int num_exts_i, char **exts_i, const char *ext) { if(GLAD_VERSION_MAJOR(version) < 3 || !GLAD_GL_IS_SOME_NEW_VERSION) { const char *extensions; const char *loc; const char *terminator; extensions = exts; if(extensions == NULL || ext == NULL) { return 0; } while(1) { loc = strstr(extensions, ext); if(loc == NULL) { return 0; } terminator = loc + strlen(ext); if((loc == extensions || *(loc - 1) == ' ') && (*terminator == ' ' || *terminator == '\0')) { return 1; } extensions = terminator; } } else { unsigned int index; for(index = 0; index < num_exts_i; index++) { const char *e = exts_i[index]; if(strcmp(e, ext) == 0) { return 1; } } } return 0; } static GLADapiproc glad_gl_get_proc_from_userptr(void *userptr, const char* name) { return (GLAD_GNUC_EXTENSION (GLADapiproc (*)(const char *name)) userptr)(name); } static int glad_gl_find_extensions_gl(GladGLContext *context, int version) { const char *exts = NULL; unsigned int num_exts_i = 0; char **exts_i = NULL; if (!glad_gl_get_extensions(context, version, &exts, &num_exts_i, &exts_i)) return 0; (void) glad_gl_has_extension; glad_gl_free_extensions(exts_i, num_exts_i); return 1; } static int glad_gl_find_core_gl(GladGLContext *context) { int i, major, minor; const char* version; const char* prefixes[] = { "OpenGL ES-CM ", "OpenGL ES-CL ", "OpenGL ES ", NULL }; version = (const char*) context->GetString(GL_VERSION); if (!version) return 0; for (i = 0; prefixes[i]; i++) { const size_t length = strlen(prefixes[i]); if (strncmp(version, prefixes[i], length) == 0) { version += length; break; } } GLAD_IMPL_UTIL_SSCANF(version, "%d.%d", &major, &minor); // attempt to grab whatever we can int temp = major; major = 5; context->VERSION_1_0 = (major == 1 && minor >= 0) || major > 1; context->VERSION_1_1 = (major == 1 && minor >= 1) || major > 1; context->VERSION_1_2 = (major == 1 && minor >= 2) || major > 1; context->VERSION_1_3 = (major == 1 && minor >= 3) || major > 1; context->VERSION_1_4 = (major == 1 && minor >= 4) || major > 1; context->VERSION_1_5 = (major == 1 && minor >= 5) || major > 1; context->VERSION_2_0 = (major == 2 && minor >= 0) || major > 2; context->VERSION_2_1 = (major == 2 && minor >= 1) || major > 2; context->VERSION_3_0 = (major == 3 && minor >= 0) || major > 3; context->VERSION_3_1 = (major == 3 && minor >= 1) || major > 3; context->VERSION_3_2 = (major == 3 && minor >= 2) || major > 3; context->VERSION_3_3 = (major == 3 && minor >= 3) || major > 3; context->VERSION_4_0 = (major == 4 && minor >= 0) || major > 4; context->VERSION_4_1 = (major == 4 && minor >= 1) || major > 4; context->VERSION_4_2 = (major == 4 && minor >= 2) || major > 4; context->VERSION_4_3 = (major == 4 && minor >= 3) || major > 4; context->VERSION_4_4 = (major == 4 && minor >= 4) || major > 4; context->VERSION_4_5 = (major == 4 && minor >= 5) || major > 4; context->VERSION_4_6 = (major == 4 && minor >= 6) || major > 4; major = temp; return GLAD_MAKE_VERSION(major, minor); } int gladLoadGLContextUserPtr(GladGLContext *context, GLADuserptrloadfunc load, void *userptr) { int version; context->GetString = (PFNGLGETSTRINGPROC) load(userptr, "glGetString"); if(context->GetString == NULL) return 0; if(context->GetString(GL_VERSION) == NULL) return 0; version = glad_gl_find_core_gl(context); glad_gl_load_GL_VERSION_1_0(context, load, userptr); glad_gl_load_GL_VERSION_1_1(context, load, userptr); glad_gl_load_GL_VERSION_1_2(context, load, userptr); glad_gl_load_GL_VERSION_1_3(context, load, userptr); glad_gl_load_GL_VERSION_1_4(context, load, userptr); glad_gl_load_GL_VERSION_1_5(context, load, userptr); glad_gl_load_GL_VERSION_2_0(context, load, userptr); glad_gl_load_GL_VERSION_2_1(context, load, userptr); glad_gl_load_GL_VERSION_3_0(context, load, userptr); glad_gl_load_GL_VERSION_3_1(context, load, userptr); glad_gl_load_GL_VERSION_3_2(context, load, userptr); glad_gl_load_GL_VERSION_3_3(context, load, userptr); glad_gl_load_GL_VERSION_4_0(context, load, userptr); glad_gl_load_GL_VERSION_4_1(context, load, userptr); glad_gl_load_GL_VERSION_4_2(context, load, userptr); glad_gl_load_GL_VERSION_4_3(context, load, userptr); glad_gl_load_GL_VERSION_4_4(context, load, userptr); glad_gl_load_GL_VERSION_4_5(context, load, userptr); glad_gl_load_GL_VERSION_4_6(context, load, userptr); if (!glad_gl_find_extensions_gl(context, version)) return 0; return version; } int gladLoadGLContext(GladGLContext *context, GLADloadfunc load) { return gladLoadGLContextUserPtr(context, glad_gl_get_proc_from_userptr, GLAD_GNUC_EXTENSION (void*) load); } #ifdef GLAD_GL #ifndef GLAD_LOADER_LIBRARY_C_ #define GLAD_LOADER_LIBRARY_C_ #include #include #if GLAD_PLATFORM_WIN32 #include #else #include #endif static void* glad_get_dlopen_handle(const char *lib_names[], int length) { void *handle = NULL; int i; for (i = 0; i < length; ++i) { #if GLAD_PLATFORM_WIN32 #if GLAD_PLATFORM_UWP size_t buffer_size = (strlen(lib_names[i]) + 1) * sizeof(WCHAR); LPWSTR buffer = (LPWSTR) malloc(buffer_size); if (buffer != NULL) { int ret = MultiByteToWideChar(CP_ACP, 0, lib_names[i], -1, buffer, buffer_size); if (ret != 0) { handle = (void*) LoadPackagedLibrary(buffer, 0); } free((void*) buffer); } #else handle = (void*) LoadLibraryA(lib_names[i]); #endif #else handle = dlopen(lib_names[i], RTLD_LAZY | RTLD_LOCAL); #endif if (handle != NULL) { return handle; } } return NULL; } static void glad_close_dlopen_handle(void* handle) { if (handle != NULL) { #if GLAD_PLATFORM_WIN32 FreeLibrary((HMODULE) handle); #else dlclose(handle); #endif } } static GLADapiproc glad_dlsym_handle(void* handle, const char *name) { if (handle == NULL) { return NULL; } #if GLAD_PLATFORM_WIN32 return (GLADapiproc) GetProcAddress((HMODULE) handle, name); #else return GLAD_GNUC_EXTENSION (GLADapiproc) dlsym(handle, name); #endif } #endif /* GLAD_LOADER_LIBRARY_C_ */ typedef void* (GLAD_API_PTR *GLADglprocaddrfunc)(const char*); struct _glad_gl_userptr { void *handle; GLADglprocaddrfunc gl_get_proc_address_ptr; }; static GLADapiproc glad_gl_get_proc(void *vuserptr, const char *name) { struct _glad_gl_userptr userptr = *(struct _glad_gl_userptr*) vuserptr; GLADapiproc result = NULL; if(userptr.gl_get_proc_address_ptr != NULL) { result = GLAD_GNUC_EXTENSION (GLADapiproc) userptr.gl_get_proc_address_ptr(name); } if(result == NULL) { result = glad_dlsym_handle(userptr.handle, name); } return result; } static void* _gl_handle = NULL; static void* glad_gl_dlopen_handle(void) { #if GLAD_PLATFORM_APPLE static const char *NAMES[] = { "../Frameworks/OpenGL.framework/OpenGL", "/Library/Frameworks/OpenGL.framework/OpenGL", "/System/Library/Frameworks/OpenGL.framework/OpenGL", "/System/Library/Frameworks/OpenGL.framework/Versions/Current/OpenGL" }; #elif GLAD_PLATFORM_WIN32 static const char *NAMES[] = {"opengl32.dll"}; #else static const char *NAMES[] = { #if defined(__CYGWIN__) "libGL-1.so", #endif "libGL.so.1", "libGL.so" }; #endif if (_gl_handle == NULL) { _gl_handle = glad_get_dlopen_handle(NAMES, sizeof(NAMES) / sizeof(NAMES[0])); } return _gl_handle; } static struct _glad_gl_userptr glad_gl_build_userptr(void *handle) { struct _glad_gl_userptr userptr; userptr.handle = handle; #if GLAD_PLATFORM_APPLE || defined(__HAIKU__) userptr.gl_get_proc_address_ptr = NULL; #elif GLAD_PLATFORM_WIN32 userptr.gl_get_proc_address_ptr = (GLADglprocaddrfunc) glad_dlsym_handle(handle, "wglGetProcAddress"); #else userptr.gl_get_proc_address_ptr = (GLADglprocaddrfunc) glad_dlsym_handle(handle, "glXGetProcAddressARB"); #endif return userptr; } int gladLoaderLoadGLContext(GladGLContext *context) { int version = 0; void *handle; int did_load = 0; struct _glad_gl_userptr userptr; did_load = _gl_handle == NULL; handle = glad_gl_dlopen_handle(); if (handle) { userptr = glad_gl_build_userptr(handle); version = gladLoadGLContextUserPtr(context,glad_gl_get_proc, &userptr); if (did_load) { gladLoaderUnloadGL(); } } return version; } void gladLoaderUnloadGL(void) { if (_gl_handle != NULL) { glad_close_dlopen_handle(_gl_handle); _gl_handle = NULL; } } #endif /* GLAD_GL */ #ifdef __cplusplus } #endif ================================================ FILE: third-party/nvfbc/NvFBC.h ================================================ /*! * \file * * This file contains the interface constants, structure definitions and * function prototypes defining the NvFBC API for Linux. * * Copyright (c) 2013-2020, NVIDIA CORPORATION. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #ifndef _NVFBC_H_ #define _NVFBC_H_ #include /*! * \mainpage NVIDIA Framebuffer Capture (NvFBC) for Linux. * * NvFBC is a high performance, low latency API to capture the framebuffer of * an X server screen. * * The output from NvFBC captures everything that would be visible if we were * directly looking at the monitor. This includes window manager decoration, * mouse cursor, overlay, etc. * * It is ideally suited to desktop or fullscreen application capture and * remoting. */ /*! * \defgroup FBC_REQ Requirements * * The following requirements are provided by the regular NVIDIA Display Driver * package: * * - OpenGL core >= 4.2: * Required. NvFBC relies on OpenGL to perform frame capture and * post-processing. * * - Vulkan 1.1: * Required. * * - libcuda.so.1 >= 5.5: * Optional. Used for capture to video memory with CUDA interop. * * The following requirements must be installed separately depending on the * Linux distribution being used: * * - XRandR extension >= 1.2: * Optional. Used for RandR output tracking. * * - libX11-xcb.so.1 >= 1.2: * Required. NvFBC uses a mix of Xlib and XCB. Xlib is needed to use GLX, * XCB is needed to make NvFBC more resilient against X server terminations * while a capture session is active. * * - libxcb.so.1 >= 1.3: * Required. See above. * * - xorg-server >= 1.3: * Optional. Required for push model to work properly. * * Note that all optional dependencies are dlopen()'d at runtime. Failure to * load an optional library is not fatal. */ /*! * \defgroup FBC_CHANGES ChangeLog * * NvFBC Linux API version 0.1 * - Initial BETA release. * * NvFBC Linux API version 0.2 * - Added 'bEnableMSE' field to NVFBC_H264_HW_ENC_CONFIG. * - Added 'dwMSE' field to NVFBC_TOH264_GRAB_FRAME_PARAMS. * - Added 'bEnableAQ' field to NVFBC_H264_HW_ENC_CONFIG. * - Added 'NVFBC_H264_PRESET_LOSSLESS_HP' enum to NVFBC_H264_PRESET. * - Added 'NVFBC_BUFFER_FORMAT_YUV444P' enum to NVFBC_BUFFER_FORMAT. * - Added 'eInputBufferFormat' field to NVFBC_H264_HW_ENC_CONFIG. * - Added '0' and '244' values for NVFBC_H264_HW_ENC_CONFIG::dwProfile. * * NvFBC Linux API version 0.3 * - Improved multi-threaded support by implementing an API locking mechanism. * - Added 'nvFBCBindContext' API entry point. * - Added 'nvFBCReleaseContext' API entry point. * * NvFBC Linux API version 1.0 * - Added codec agnostic interface for HW encoding. * - Deprecated H.264 interface. * - Added support for H.265/HEVC HW encoding. * * NvFBC Linux API version 1.1 * - Added 'nvFBCToHwGetCaps' API entry point. * - Added 'dwDiffMapScalingFactor' field to NVFBC_TOSYS_SETUP_PARAMS. * * NvFBC Linux API version 1.2 * - Deprecated ToHwEnc interface. * - Added ToGL interface that captures frames to an OpenGL texture in video * memory. * - Added 'bDisableAutoModesetRecovery' field to * NVFBC_CREATE_CAPTURE_SESSION_PARAMS. * - Added 'bExternallyManagedContext' field to NVFBC_CREATE_HANDLE_PARAMS. * * NvFBC Linux API version 1.3 * - Added NVFBC_BUFFER_FORMAT_RGBA * - Added 'dwTimeoutMs' field to NVFBC_TOSYS_GRAB_FRAME_PARAMS, * NVFBC_TOCUDA_GRAB_FRAME_PARAMS, and NVFBC_TOGL_GRAB_FRAME_PARAMS. * * NvFBC Linux API version 1.4 * - Clarified that NVFBC_BUFFER_FORMAT_{ARGB,RGB,RGBA} are byte-order formats. * - Renamed NVFBC_BUFFER_FORMAT_YUV420P to NVFBC_BUFFER_FORMAT_NV12. * - Added new requirements. * - Made NvFBC more resilient against the X server terminating during an active * capture session. See new comments for ::NVFBC_ERR_X. * - Relaxed requirement that 'frameSize' must have a width being a multiple of * 4 and a height being a multiple of 2. * - Added 'bRoundFrameSize' field to NVFBC_CREATE_CAPTURE_SESSION_PARAMS. * - Relaxed requirement that the scaling factor for differential maps must be * a multiple of the size of the frame. * - Added 'diffMapSize' field to NVFBC_TOSYS_SETUP_PARAMS and * NVFBC_TOGL_SETUP_PARAMS. * * NvFBC Linux API version 1.5 * - Added NVFBC_BUFFER_FORMAT_BGRA * * NvFBC Linux API version 1.6 * - Added the 'NVFBC_TOSYS_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY', * 'NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY', and * 'NVFBC_TOGL_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY' capture flags. * - Exposed debug and performance logs through the NVFBC_LOG_LEVEL environment * variable. Setting it to "1" enables performance logs, setting it to "2" * enables debugging logs, setting it to "3" enables both. * - Logs are printed to stdout or to the file pointed by the NVFBC_LOG_FILE * environment variable. * - Added 'ulTimestampUs' to NVFBC_FRAME_GRAB_INFO. * - Added 'dwSamplingRateMs' to NVFBC_CREATE_CAPTURE_SESSION_PARAMS. * - Added 'bPushModel' to NVFBC_CREATE_CAPTURE_SESSION_PARAMS. * * NvFBC Linux API version 1.7 * - Retired the NVFBC_CAPTURE_TO_HW_ENCODER interface. * This interface has been deprecated since NvFBC 1.2 and has received no * updates or new features since. We recommend using the NVIDIA Video Codec * SDK to encode NvFBC frames. * See: https://developer.nvidia.com/nvidia-video-codec-sdk * - Added a 'Capture Modes' section to those headers. * - Added a 'Post Processing' section to those headers. * - Added an 'Environment Variables' section to those headers. * - Added 'bInModeset' to NVFBC_GET_STATUS_PARAMS. * - Added 'bAllowDirectCapture' to NVFBC_CREATE_CAPTURE_SESSION_PARAMS. * - Added 'bDirectCaptured' to NVFBC_FRAME_GRAB_INFO. * - Added 'bRequiredPostProcessing' to NVFBC_FRAME_GRAB_INFO. */ /*! * \defgroup FBC_MODES Capture Modes * * When creating a capture session, NvFBC instantiates a capture subsystem * living in the NVIDIA X driver. * * This subsystem listens for damage events coming from applications then * generates (composites) frames for NvFBC when new content is available. * * This capture server can operate on a timer where it periodically checks if * there are any pending damage events, or it can generate frames as soon as it * receives a new damage event. * See NVFBC_CREATE_CAPTURE_SESSION_PARAMS::dwSamplingRateMs, * and NVFBC_CREATE_CAPTURE_SESSION_PARAMS::bPushModel. * * NvFBC can also attach itself to a fullscreen unoccluded application and have * it copy its frames directly into a buffer owned by NvFBC upon present. This * mode bypasses the X server. * See NVFBC_CREATE_CAPTURE_SESSION_PARAMS::bAllowDirectCapture. * * NvFBC is designed to capture frames with as few copies as possible. The * NVIDIA X driver composites frames directly into the NvFBC buffers, and * direct capture copies frames directly into these buffers as well. * * Depending on the configuration of a capture session, an extra copy (rendering * pass) may be needed. See the 'Post Processing' section. */ /*! * \defgroup FBC_PP Post Processing * * Depending on the configuration of a capture session, NvFBC might require to * do post processing on frames. * * Post processing is required for the following reasons: * - NvFBC needs to do a pixel format conversion. * - Diffmaps are requested. * - Capture to system memory is requested. * * NvFBC needs to do a conversion if the requested pixel format does not match * the native format. The native format is NVFBC_BUFFER_FORMAT_BGRA. * * Note: post processing is *not* required for frame scaling and frame cropping. * * Skipping post processing can reduce capture latency. An application can know * whether post processing was required by checking * NVFBC_FRAME_GRAB_INFO::bRequiredPostProcessing. */ /*! * \defgroup FBC_ENVVAR Environment Variables * * Below are the environment variables supported by NvFBC: * * - NVFBC_LOG_LEVEL * Bitfield where the first bit enables debug logs and the second bit enables * performance logs. Both can be enabled by setting this envvar to 3. * * - NVFBC_LOG_FILE * Write all NvFBC logs to the given file. * * - NVFBC_FORCE_ALLOW_DIRECT_CAPTURE * Used to override NVFBC_CREATE_CAPTURE_SESSION_PARAMS::bAllowDirectCapture. * * - NVFBC_FORCE_POST_PROCESSING * Used to force the post processing step, even if it could be skipped. * See the 'Post Processing' section. */ /*! * \defgroup FBC_STRUCT Structure Definition * * @{ */ #ifdef __cplusplus extern "C" { #endif /*! * Calling convention. */ #define NVFBCAPI /*! * NvFBC API major version. */ #define NVFBC_VERSION_MAJOR 1 /*! * NvFBC API minor version. */ #define NVFBC_VERSION_MINOR 7 /*! * NvFBC API version. */ #define NVFBC_VERSION (uint32_t)(NVFBC_VERSION_MINOR | (NVFBC_VERSION_MAJOR << 8)) /*! * Creates a version number for structure parameters. */ #define NVFBC_STRUCT_VERSION(typeName, ver) \ (uint32_t)(sizeof(typeName) | ((ver) << 16) | (NVFBC_VERSION << 24)) /*! * Defines error codes. * * \see NvFBCGetLastErrorStr */ typedef enum _NVFBCSTATUS { /*! * This indicates that the API call returned with no errors. */ NVFBC_SUCCESS = 0, /*! * This indicates that the API version between the client and the library * is not compatible. */ NVFBC_ERR_API_VERSION = 1, /*! * An internal error occurred. */ NVFBC_ERR_INTERNAL = 2, /*! * This indicates that one or more of the parameter passed to the API call * is invalid. */ NVFBC_ERR_INVALID_PARAM = 3, /*! * This indicates that one or more of the pointers passed to the API call * is invalid. */ NVFBC_ERR_INVALID_PTR = 4, /*! * This indicates that the handle passed to the API call to identify the * client is invalid. */ NVFBC_ERR_INVALID_HANDLE = 5, /*! * This indicates that the maximum number of threaded clients of the same * process has been reached. The limit is 10 threads per process. * There is no limit on the number of process. */ NVFBC_ERR_MAX_CLIENTS = 6, /*! * This indicates that the requested feature is not currently supported * by the library. */ NVFBC_ERR_UNSUPPORTED = 7, /*! * This indicates that the API call failed because it was unable to allocate * enough memory to perform the requested operation. */ NVFBC_ERR_OUT_OF_MEMORY = 8, /*! * This indicates that the API call was not expected. This happens when * API calls are performed in a wrong order, such as trying to capture * a frame prior to creating a new capture session; or trying to set up * a capture to video memory although a capture session to system memory * was created. */ NVFBC_ERR_BAD_REQUEST = 9, /*! * This indicates an X error, most likely meaning that the X server has * been terminated. When this error is returned, the only resort is to * create another FBC handle using NvFBCCreateHandle(). * * The previous handle should still be freed with NvFBCDestroyHandle(), but * it might leak resources, in particular X, GLX, and GL resources since * it is no longer possible to communicate with an X server to free them * through the driver. * * The best course of action to eliminate this potential leak is to close * the OpenGL driver, close the forked process running the capture, or * restart the application. */ NVFBC_ERR_X = 10, /*! * This indicates a GLX error. */ NVFBC_ERR_GLX = 11, /*! * This indicates an OpenGL error. */ NVFBC_ERR_GL = 12, /*! * This indicates a CUDA error. */ NVFBC_ERR_CUDA = 13, /*! * This indicates a HW encoder error. */ NVFBC_ERR_ENCODER = 14, /*! * This indicates an NvFBC context error. */ NVFBC_ERR_CONTEXT = 15, /*! * This indicates that the application must recreate the capture session. * * This error can be returned if a modeset event occurred while capturing * frames, and NVFBC_CREATE_HANDLE_PARAMS::bDisableAutoModesetRecovery * was set to NVFBC_TRUE. */ NVFBC_ERR_MUST_RECREATE = 16, /*! * This indicates a Vulkan error. */ NVFBC_ERR_VULKAN = 17, } NVFBCSTATUS; /*! * Defines boolean values. */ typedef enum _NVFBC_BOOL { /*! * False value. */ NVFBC_FALSE = 0, /*! * True value. */ NVFBC_TRUE, } NVFBC_BOOL; /*! * Maximum size in bytes of an error string. */ #define NVFBC_ERR_STR_LEN 512 /*! * Capture type. */ typedef enum _NVFBC_CAPTURE_TYPE { /*! * Capture frames to a buffer in system memory. */ NVFBC_CAPTURE_TO_SYS = 0, /*! * Capture frames to a CUDA device in video memory. * * Specifying this will dlopen() libcuda.so.1 and fail if not available. */ NVFBC_CAPTURE_SHARED_CUDA, /*! * Retired. Do not use. */ /* NVFBC_CAPTURE_TO_HW_ENCODER, */ /*! * Capture frames to an OpenGL buffer in video memory. */ NVFBC_CAPTURE_TO_GL = 3, } NVFBC_CAPTURE_TYPE; /*! * Tracking type. * * NvFBC can track a specific region of the framebuffer to capture. * * An X screen corresponds to the entire framebuffer. * * An RandR CRTC is a component of the GPU that reads pixels from a region of * the X screen and sends them through a pipeline to an RandR output. * A physical monitor can be connected to an RandR output. Tracking an RandR * output captures the region of the X screen that the RandR CRTC is sending to * the RandR output. */ typedef enum { /*! * By default, NvFBC tries to track a connected primary output. If none is * found, then it tries to track the first connected output. If none is * found then it tracks the entire X screen. * * If the XRandR extension is not available, this option has the same effect * as ::NVFBC_TRACKING_SCREEN. * * This default behavior might be subject to changes in the future. */ NVFBC_TRACKING_DEFAULT = 0, /*! * Track an RandR output specified by its ID in the appropriate field. * * The list of connected outputs can be queried via NvFBCGetStatus(). * This list can also be obtained using e.g., xrandr(1). * * If the XRandR extension is not available, setting this option returns an * error. */ NVFBC_TRACKING_OUTPUT, /*! * Track the entire X screen. */ NVFBC_TRACKING_SCREEN, } NVFBC_TRACKING_TYPE; /*! * Buffer format. */ typedef enum _NVFBC_BUFFER_FORMAT { /*! * Data will be converted to ARGB8888 byte-order format. 32 bpp. */ NVFBC_BUFFER_FORMAT_ARGB = 0, /*! * Data will be converted to RGB888 byte-order format. 24 bpp. */ NVFBC_BUFFER_FORMAT_RGB, /*! * Data will be converted to NV12 format using HDTV weights * according to ITU-R BT.709. 12 bpp. */ NVFBC_BUFFER_FORMAT_NV12, /*! * Data will be converted to YUV 444 planar format using HDTV weights * according to ITU-R BT.709. 24 bpp */ NVFBC_BUFFER_FORMAT_YUV444P, /*! * Data will be converted to RGBA8888 byte-order format. 32 bpp. */ NVFBC_BUFFER_FORMAT_RGBA, /*! * Native format. No pixel conversion needed. * BGRA8888 byte-order format. 32 bpp. */ NVFBC_BUFFER_FORMAT_BGRA, } NVFBC_BUFFER_FORMAT; #define NVFBC_BUFFER_FORMAT_YUV420P NVFBC_BUFFER_FORMAT_NV12 /*! * Handle used to identify an NvFBC session. */ typedef uint64_t NVFBC_SESSION_HANDLE; /*! * Box used to describe an area of the tracked region to capture. * * The coordinates are relative to the tracked region. * * E.g., if the size of the X screen is 3520x1200 and the tracked RandR output * scans a region of 1600x1200+1920+0, then setting a capture box of * 800x600+100+50 effectively captures a region of 800x600+2020+50 relative to * the X screen. */ typedef struct _NVFBC_BOX { /*! * [in] X offset of the box. */ uint32_t x; /*! * [in] Y offset of the box. */ uint32_t y; /*! * [in] Width of the box. */ uint32_t w; /*! * [in] Height of the box. */ uint32_t h; } NVFBC_BOX; /*! * Size used to describe the size of a frame. */ typedef struct _NVFBC_SIZE { /*! * [in] Width. */ uint32_t w; /*! * [in] Height. */ uint32_t h; } NVFBC_SIZE; /*! * Describes information about a captured frame. */ typedef struct _NVFBC_FRAME_GRAB_INFO { /*! * [out] Width of the captured frame. */ uint32_t dwWidth; /*! * [out] Height of the captured frame. */ uint32_t dwHeight; /*! * [out] Size of the frame in bytes. */ uint32_t dwByteSize; /*! * [out] Incremental ID of the current frame. * * This can be used to identify a frame. */ uint32_t dwCurrentFrame; /*! * [out] Whether the captured frame is a new frame. * * When using non blocking calls it is possible to capture a frame * that was already captured before if the display server did not * render a new frame in the meantime. In that case, this flag * will be set to NVFBC_FALSE. * * When using blocking calls each captured frame will have * this flag set to NVFBC_TRUE since the blocking mechanism waits for * the display server to render a new frame. * * Note that this flag does not guarantee that the content of * the frame will be different compared to the previous captured frame. * * In particular, some compositing managers report the entire * framebuffer as damaged when an application refreshes its content. * * Consider a single X screen spanned across physical displays A and B * and an NvFBC application tracking display A. Depending on the * compositing manager, it is possible that an application refreshing * itself on display B will trigger a frame capture on display A. * * Workarounds include: * - Using separate X screens * - Disabling the composite extension * - Using a compositing manager that properly reports what regions * are damaged * - Using NvFBC's diffmaps to find out if the frame changed */ NVFBC_BOOL bIsNewFrame; /*! * [out] Frame timestamp * * Time in micro seconds when the display server started rendering the * frame. * * This does not account for when the frame was captured. If capturing an * old frame (e.g., bIsNewFrame is NVFBC_FALSE) the reported timestamp * will reflect the time when the old frame was rendered by the display * server. */ uint64_t ulTimestampUs; /* * [out] Number of frames generated since the last capture. * * This can help applications tell whether they missed frames or there * were no frames generated by the server since the last capture. */ uint32_t dwMissedFrames; /* * [out] Whether the captured frame required post processing. * * See the 'Post Processing' section. */ NVFBC_BOOL bRequiredPostProcessing; /* * [out] Whether this frame was obtained via direct capture. * * See NVFBC_CREATE_CAPTURE_SESSION_PARAMS::bAllowDirectCapture. */ NVFBC_BOOL bDirectCapture; } NVFBC_FRAME_GRAB_INFO; /*! * Defines parameters for the CreateHandle() API call. */ typedef struct _NVFBC_CREATE_HANDLE_PARAMS { /*! * [in] Must be set to NVFBC_CREATE_HANDLE_PARAMS_VER */ uint32_t dwVersion; /*! * [in] Application specific private information passed to the NvFBC * session. */ const void *privateData; /*! * [in] Size of the application specific private information passed to the * NvFBC session. */ uint32_t privateDataSize; /*! * [in] Whether NvFBC should not create and manage its own graphics context * * NvFBC internally uses OpenGL to perform graphics operations on the * captured frames. By default, NvFBC will create and manage (e.g., make * current, detect new threads, etc.) its own OpenGL context. * * If set to NVFBC_TRUE, NvFBC will use the application's context. It will * be the application's responsibility to make sure that a context is * current on the thread calling into the NvFBC API. */ NVFBC_BOOL bExternallyManagedContext; /*! * [in] GLX context * * GLX context that NvFBC should use internally to create pixmaps and * make them current when creating a new capture session. * * Note: NvFBC expects a context created against a GLX_RGBA_TYPE render * type. */ void *glxCtx; /*! * [in] GLX framebuffer configuration * * Framebuffer configuration that was used to create the GLX context, and * that will be used to create pixmaps internally. * * Note: NvFBC expects a configuration having at least the following * attributes: * GLX_DRAWABLE_TYPE, GLX_PIXMAP_BIT * GLX_BIND_TO_TEXTURE_RGBA_EXT, 1 * GLX_BIND_TO_TEXTURE_TARGETS_EXT, GLX_TEXTURE_2D_BIT_EXT */ void *glxFBConfig; } NVFBC_CREATE_HANDLE_PARAMS; /*! * NVFBC_CREATE_HANDLE_PARAMS structure version. */ #define NVFBC_CREATE_HANDLE_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_CREATE_HANDLE_PARAMS, 2) /*! * Defines parameters for the ::NvFBCDestroyHandle() API call. */ typedef struct _NVFBC_DESTROY_HANDLE_PARAMS { /*! * [in] Must be set to NVFBC_DESTROY_HANDLE_PARAMS_VER */ uint32_t dwVersion; } NVFBC_DESTROY_HANDLE_PARAMS; /*! * NVFBC_DESTROY_HANDLE_PARAMS structure version. */ #define NVFBC_DESTROY_HANDLE_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_DESTROY_HANDLE_PARAMS, 1) /*! * Maximum number of connected RandR outputs to an X screen. */ #define NVFBC_OUTPUT_MAX 5 /*! * Maximum size in bytes of an RandR output name. */ #define NVFBC_OUTPUT_NAME_LEN 128 /*! * Describes an RandR output. * * Filling this structure relies on the XRandR extension. This feature cannot * be used if the extension is missing or its version is below the requirements. * * \see Requirements */ typedef struct _NVFBC_OUTPUT { /*! * Identifier of the RandR output. */ uint32_t dwId; /*! * Name of the RandR output, as reported by tools such as xrandr(1). * * Example: "DVI-I-0" */ char name[NVFBC_OUTPUT_NAME_LEN]; /*! * Region of the X screen tracked by the RandR CRTC driving this RandR * output. */ NVFBC_BOX trackedBox; } NVFBC_RANDR_OUTPUT_INFO; /*! * Defines parameters for the ::NvFBCGetStatus() API call. */ typedef struct _NVFBC_GET_STATUS_PARAMS { /*! * [in] Must be set to NVFBC_GET_STATUS_PARAMS_VER */ uint32_t dwVersion; /*! * [out] Whether or not framebuffer capture is supported by the graphics * driver. */ NVFBC_BOOL bIsCapturePossible; /*! * [out] Whether or not there is already a capture session on this system. */ NVFBC_BOOL bCurrentlyCapturing; /*! * [out] Whether or not it is possible to create a capture session on this * system. */ NVFBC_BOOL bCanCreateNow; /*! * [out] Size of the X screen (framebuffer). */ NVFBC_SIZE screenSize; /*! * [out] Whether the XRandR extension is available. * * If this extension is not available then it is not possible to have * information about RandR outputs. */ NVFBC_BOOL bXRandRAvailable; /*! * [out] Array of outputs connected to the X screen. * * An application can track a specific output by specifying its ID when * creating a capture session. * * Only if XRandR is available. */ NVFBC_RANDR_OUTPUT_INFO outputs[NVFBC_OUTPUT_MAX]; /*! * [out] Number of outputs connected to the X screen. * * This must be used to parse the array of connected outputs. * * Only if XRandR is available. */ uint32_t dwOutputNum; /*! * [out] Version of the NvFBC library running on this system. */ uint32_t dwNvFBCVersion; /*! * [out] Whether the X server is currently in modeset. * * When the X server is in modeset, it must give up all its video * memory allocations. It is not possible to create a capture * session until the modeset is over. * * Note that VT-switches are considered modesets. */ NVFBC_BOOL bInModeset; } NVFBC_GET_STATUS_PARAMS; /*! * NVFBC_GET_STATUS_PARAMS structure version. */ #define NVFBC_GET_STATUS_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_GET_STATUS_PARAMS, 2) /*! * Defines parameters for the ::NvFBCCreateCaptureSession() API call. */ typedef struct _NVFBC_CREATE_CAPTURE_SESSION_PARAMS { /*! * [in] Must be set to NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER */ uint32_t dwVersion; /*! * [in] Desired capture type. * * Note that when specyfing ::NVFBC_CAPTURE_SHARED_CUDA NvFBC will try to * dlopen() the corresponding libraries. This means that NvFBC can run on * a system without the CUDA library since it does not link against them. */ NVFBC_CAPTURE_TYPE eCaptureType; /*! * [in] What region of the framebuffer should be tracked. */ NVFBC_TRACKING_TYPE eTrackingType; /*! * [in] ID of the output to track if eTrackingType is set to * ::NVFBC_TRACKING_OUTPUT. */ uint32_t dwOutputId; /*! * [in] Crop the tracked region. * * The coordinates are relative to the tracked region. * * It can be set to 0 to capture the entire tracked region. */ NVFBC_BOX captureBox; /*! * [in] Desired size of the captured frame. * * This parameter allow to scale the captured frame. * * It can be set to 0 to disable frame resizing. */ NVFBC_SIZE frameSize; /*! * [in] Whether the mouse cursor should be composited to the frame. * * Disabling the cursor will not generate new frames when only the cursor * is moved. */ NVFBC_BOOL bWithCursor; /*! * [in] Whether NvFBC should not attempt to recover from modesets. * * NvFBC is able to detect when a modeset event occurred and can automatically * re-create a capture session with the same settings as before, then resume * its frame capture session transparently. * * This option allows to disable this behavior. NVFBC_ERR_MUST_RECREATE * will be returned in that case. * * It can be useful in the cases when an application needs to do some work * between setting up a capture and grabbing the first frame. * * For example: an application using the ToGL interface needs to register * resources with EncodeAPI prior to encoding frames. * * Note that during modeset recovery, NvFBC will try to re-create the * capture session every second until it succeeds. */ NVFBC_BOOL bDisableAutoModesetRecovery; /*! * [in] Whether NvFBC should round the requested frameSize. * * When disabled, NvFBC will not attempt to round the requested resolution. * * However, some pixel formats have resolution requirements. E.g., YUV/NV * formats must have a width being a multiple of 4, and a height being a * multiple of 2. RGB formats don't have such requirements. * * If the resolution doesn't meet the requirements of the format, then NvFBC * will fail at setup time. * * When enabled, NvFBC will round the requested width to the next multiple * of 4 and the requested height to the next multiple of 2. * * In this case, requesting any resolution will always work with every * format. However, an NvFBC client must be prepared to handle the case * where the requested resolution is different than the captured resolution. * * NVFBC_FRAME_GRAB_INFO::dwWidth and NVFBC_FRAME_GRAB_INFO::dwHeight should * always be used for getting information about captured frames. */ NVFBC_BOOL bRoundFrameSize; /*! * [in] Rate in ms at which the display server generates new frames * * This controls the frequency at which the display server will generate * new frames if new content is available. This effectively controls the * capture rate when using blocking calls. * * Note that lower values will increase the CPU and GPU loads. * * The default value is 16ms (~ 60 Hz). */ uint32_t dwSamplingRateMs; /*! * [in] Enable push model for frame capture * * When set to NVFBC_TRUE, the display server will generate frames whenever * it receives a damage event from applications. * * Setting this to NVFBC_TRUE will ignore ::dwSamplingRateMs. * * Using push model with the NVFBC_*_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY * capture flag should guarantee the shortest amount of time between an * application rendering a frame and an NvFBC client capturing it, provided * that the NvFBC client is able to process the frames quickly enough. * * Note that applications running at high frame rates will increase CPU and * GPU loads. */ NVFBC_BOOL bPushModel; /*! * [in] Allow direct capture * * Direct capture allows NvFBC to attach itself to a fullscreen graphics * application. Whenever that application presents a frame, it makes a copy * of it directly into a buffer owned by NvFBC thus bypassing the X server. * * When direct capture is *not* enabled, the NVIDIA X driver generates a * frame for NvFBC when it receives a damage event from an application if push * model is enabled, or periodically checks if there are any pending damage * events otherwise (see NVFBC_CREATE_CAPTURE_SESSION_PARAMS::dwSamplingRateMs). * * Direct capture is possible under the following conditions: * - Direct capture is allowed * - Push model is enabled (see NVFBC_CREATE_CAPTURE_SESSION_PARAMS::bPushModel) * - The mouse cursor is not composited (see NVFBC_CREATE_CAPTURE_SESSION_PARAMS::bWithCursor) * - No viewport transformation is required. This happens when the remote * desktop is e.g. rotated. * * When direct capture is possible, NvFBC will automatically attach itself * to a fullscreen unoccluded application, if such exists. * * Notes: * - This includes compositing desktops such as GNOME (e.g., gnome-shell * is the fullscreen unoccluded application). * - There can be only one fullscreen unoccluded application at a time. * - The NVIDIA X driver monitors which application qualifies or no * longer qualifies. * * For example, if a fullscreen application is launched in GNOME, NvFBC will * detach from gnome-shell and attach to that application. * * Attaching and detaching happens automatically from the perspective of an * NvFBC client. When detaching from an application, the X driver will * transparently resume generating frames for NvFBC. * * An application can know whether a given frame was obtained through * direct capture by checking NVFBC_FRAME_GRAB_INFO::bDirectCapture. */ NVFBC_BOOL bAllowDirectCapture; } NVFBC_CREATE_CAPTURE_SESSION_PARAMS; /*! * NVFBC_CREATE_CAPTURE_SESSION_PARAMS structure version. */ #define NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_CREATE_CAPTURE_SESSION_PARAMS, 6) /*! * Defines parameters for the ::NvFBCDestroyCaptureSession() API call. */ typedef struct _NVFBC_DESTROY_CAPTURE_SESSION_PARAMS { /*! * [in] Must be set to NVFBC_DESTROY_CAPTURE_SESSION_PARAMS_VER */ uint32_t dwVersion; } NVFBC_DESTROY_CAPTURE_SESSION_PARAMS; /*! * NVFBC_DESTROY_CAPTURE_SESSION_PARAMS structure version. */ #define NVFBC_DESTROY_CAPTURE_SESSION_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_DESTROY_CAPTURE_SESSION_PARAMS, 1) /*! * Defines parameters for the ::NvFBCBindContext() API call. */ typedef struct _NVFBC_BIND_CONTEXT_PARAMS { /*! * [in] Must be set to NVFBC_BIND_CONTEXT_PARAMS_VER */ uint32_t dwVersion; } NVFBC_BIND_CONTEXT_PARAMS; /*! * NVFBC_BIND_CONTEXT_PARAMS structure version. */ #define NVFBC_BIND_CONTEXT_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_BIND_CONTEXT_PARAMS, 1) /*! * Defines parameters for the ::NvFBCReleaseContext() API call. */ typedef struct _NVFBC_RELEASE_CONTEXT_PARAMS { /*! * [in] Must be set to NVFBC_RELEASE_CONTEXT_PARAMS_VER */ uint32_t dwVersion; } NVFBC_RELEASE_CONTEXT_PARAMS; /*! * NVFBC_RELEASE_CONTEXT_PARAMS structure version. */ #define NVFBC_RELEASE_CONTEXT_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_RELEASE_CONTEXT_PARAMS, 1) /*! * Defines flags that can be used when capturing to system memory. */ typedef enum { /*! * Default, capturing waits for a new frame or mouse move. * * The default behavior of blocking grabs is to wait for a new frame until * after the call was made. But it's possible that there is a frame already * ready that the client hasn't seen. * \see NVFBC_TOSYS_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY */ NVFBC_TOSYS_GRAB_FLAGS_NOFLAGS = 0, /*! * Capturing does not wait for a new frame nor a mouse move. * * It is therefore possible to capture the same frame multiple times. * When this occurs, the dwCurrentFrame parameter of the * NVFBC_FRAME_GRAB_INFO structure is not incremented. */ NVFBC_TOSYS_GRAB_FLAGS_NOWAIT = (1 << 0), /*! * Forces the destination buffer to be refreshed even if the frame has not * changed since previous capture. * * By default, if the captured frame is identical to the previous one, NvFBC * will omit one copy and not update the destination buffer. * * Setting that flag will prevent this behavior. This can be useful e.g., * if the application has modified the buffer in the meantime. */ NVFBC_TOSYS_GRAB_FLAGS_FORCE_REFRESH = (1 << 1), /*! * Similar to NVFBC_TOSYS_GRAB_FLAGS_NOFLAGS, except that the capture will * not wait if there is already a frame available that the client has * never seen yet. */ NVFBC_TOSYS_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY = (1 << 2), } NVFBC_TOSYS_GRAB_FLAGS; /*! * Defines parameters for the ::NvFBCToSysSetUp() API call. */ typedef struct _NVFBC_TOSYS_SETUP_PARAMS { /*! * [in] Must be set to NVFBC_TOSYS_SETUP_PARAMS_VER */ uint32_t dwVersion; /*! * [in] Desired buffer format. */ NVFBC_BUFFER_FORMAT eBufferFormat; /*! * [out] Pointer to a pointer to a buffer in system memory. * * This buffer contains the pixel value of the requested format. Refer to * the description of the buffer formats to understand the memory layout. * * The application does not need to allocate memory for this buffer. It * should not free this buffer either. This buffer is automatically * re-allocated when needed (e.g., when the resolution changes). * * This buffer is allocated by the NvFBC library to the proper size. This * size is returned in the dwByteSize field of the * ::NVFBC_FRAME_GRAB_INFO structure. */ void **ppBuffer; /*! * [in] Whether differential maps should be generated. */ NVFBC_BOOL bWithDiffMap; /*! * [out] Pointer to a pointer to a buffer in system memory. * * This buffer contains the differential map of two frames. It must be read * as an array of unsigned char. Each unsigned char is either 0 or * non-zero. 0 means that the pixel value at the given location has not * changed since the previous captured frame. Non-zero means that the pixel * value has changed. * * The application does not need to allocate memory for this buffer. It * should not free this buffer either. This buffer is automatically * re-allocated when needed (e.g., when the resolution changes). * * This buffer is allocated by the NvFBC library to the proper size. The * size of the differential map is returned in ::diffMapSize. * * This option is not compatible with the ::NVFBC_BUFFER_FORMAT_YUV420P and * ::NVFBC_BUFFER_FORMAT_YUV444P buffer formats. */ void **ppDiffMap; /*! * [in] Scaling factor of the differential maps. * * For example, a scaling factor of 16 means that one pixel of the diffmap * will represent 16x16 pixels of the original frames. * * If any of these 16x16 pixels is different between the current and the * previous frame, then the corresponding pixel in the diffmap will be set * to non-zero. * * The default scaling factor is 1. A dwDiffMapScalingFactor of 0 will be * set to 1. */ uint32_t dwDiffMapScalingFactor; /*! * [out] Size of the differential map. * * Only set if bWithDiffMap is set to NVFBC_TRUE. */ NVFBC_SIZE diffMapSize; } NVFBC_TOSYS_SETUP_PARAMS; /*! * NVFBC_TOSYS_SETUP_PARAMS structure version. */ #define NVFBC_TOSYS_SETUP_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_TOSYS_SETUP_PARAMS, 3) /*! * Defines parameters for the ::NvFBCToSysGrabFrame() API call. */ typedef struct _NVFBC_TOSYS_GRAB_FRAME_PARAMS { /*! * [in] Must be set to NVFBC_TOSYS_GRAB_FRAME_PARAMS_VER */ uint32_t dwVersion; /*! * [in] Flags defining the behavior of this frame capture. */ uint32_t dwFlags; /*! * [out] Information about the captured frame. * * Can be NULL. */ NVFBC_FRAME_GRAB_INFO *pFrameGrabInfo; /*! * [in] Wait timeout in milliseconds. * * When capturing frames with the NVFBC_TOSYS_GRAB_FLAGS_NOFLAGS or * NVFBC_TOSYS_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY flags, * NvFBC will wait for a new frame or mouse move until the below timer * expires. * * When timing out, the last captured frame will be returned. Note that as * long as the NVFBC_TOSYS_GRAB_FLAGS_FORCE_REFRESH flag is not set, * returning an old frame will incur no performance penalty. * * NvFBC clients can use the return value of the grab frame operation to * find out whether a new frame was captured, or the timer expired. * * Note that the behavior of blocking calls is to wait for a new frame * *after* the call has been made. When using timeouts, it is possible * that NvFBC will return a new frame (e.g., it has never been captured * before) even though no new frame was generated after the grab call. * * For the precise definition of what constitutes a new frame, see * ::bIsNewFrame. * * Set to 0 to disable timeouts. */ uint32_t dwTimeoutMs; } NVFBC_TOSYS_GRAB_FRAME_PARAMS; /*! * NVFBC_TOSYS_GRAB_FRAME_PARAMS structure version. */ #define NVFBC_TOSYS_GRAB_FRAME_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_TOSYS_GRAB_FRAME_PARAMS, 2) /*! * Defines flags that can be used when capturing to a CUDA buffer in video memory. */ typedef enum { /*! * Default, capturing waits for a new frame or mouse move. * * The default behavior of blocking grabs is to wait for a new frame until * after the call was made. But it's possible that there is a frame already * ready that the client hasn't seen. * \see NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY */ NVFBC_TOCUDA_GRAB_FLAGS_NOFLAGS = 0, /*! * Capturing does not wait for a new frame nor a mouse move. * * It is therefore possible to capture the same frame multiple times. * When this occurs, the dwCurrentFrame parameter of the * NVFBC_FRAME_GRAB_INFO structure is not incremented. */ NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT = (1 << 0), /*! * [in] Forces the destination buffer to be refreshed even if the frame * has not changed since previous capture. * * By default, if the captured frame is identical to the previous one, NvFBC * will omit one copy and not update the destination buffer. * * Setting that flag will prevent this behavior. This can be useful e.g., * if the application has modified the buffer in the meantime. */ NVFBC_TOCUDA_GRAB_FLAGS_FORCE_REFRESH = (1 << 1), /*! * Similar to NVFBC_TOCUDA_GRAB_FLAGS_NOFLAGS, except that the capture will * not wait if there is already a frame available that the client has * never seen yet. */ NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY = (1 << 2), } NVFBC_TOCUDA_FLAGS; /*! * Defines parameters for the ::NvFBCToCudaSetUp() API call. */ typedef struct _NVFBC_TOCUDA_SETUP_PARAMS { /*! * [in] Must be set to NVFBC_TOCUDA_SETUP_PARAMS_VER */ uint32_t dwVersion; /*! * [in] Desired buffer format. */ NVFBC_BUFFER_FORMAT eBufferFormat; } NVFBC_TOCUDA_SETUP_PARAMS; /*! * NVFBC_TOCUDA_SETUP_PARAMS structure version. */ #define NVFBC_TOCUDA_SETUP_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_TOCUDA_SETUP_PARAMS, 1) /*! * Defines parameters for the ::NvFBCToCudaGrabFrame() API call. */ typedef struct _NVFBC_TOCUDA_GRAB_FRAME_PARAMS { /*! * [in] Must be set to NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER. */ uint32_t dwVersion; /*! * [in] Flags defining the behavior of this frame capture. */ uint32_t dwFlags; /*! * [out] Pointer to a ::CUdeviceptr * * The application does not need to allocate memory for this CUDA device. * * The application does need to create its own CUDA context to use this * CUDA device. * * This ::CUdeviceptr will be mapped to a segment in video memory containing * the frame. It is not possible to process a CUDA device while capturing * a new frame. If the application wants to do so, it must copy the CUDA * device using ::cuMemcpyDtoD or ::cuMemcpyDtoH beforehand. */ void *pCUDADeviceBuffer; /*! * [out] Information about the captured frame. * * Can be NULL. */ NVFBC_FRAME_GRAB_INFO *pFrameGrabInfo; /*! * [in] Wait timeout in milliseconds. * * When capturing frames with the NVFBC_TOCUDA_GRAB_FLAGS_NOFLAGS or * NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY flags, * NvFBC will wait for a new frame or mouse move until the below timer * expires. * * When timing out, the last captured frame will be returned. Note that as * long as the NVFBC_TOCUDA_GRAB_FLAGS_FORCE_REFRESH flag is not set, * returning an old frame will incur no performance penalty. * * NvFBC clients can use the return value of the grab frame operation to * find out whether a new frame was captured, or the timer expired. * * Note that the behavior of blocking calls is to wait for a new frame * *after* the call has been made. When using timeouts, it is possible * that NvFBC will return a new frame (e.g., it has never been captured * before) even though no new frame was generated after the grab call. * * For the precise definition of what constitutes a new frame, see * ::bIsNewFrame. * * Set to 0 to disable timeouts. */ uint32_t dwTimeoutMs; } NVFBC_TOCUDA_GRAB_FRAME_PARAMS; /*! * NVFBC_TOCUDA_GRAB_FRAME_PARAMS structure version. */ #define NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_TOCUDA_GRAB_FRAME_PARAMS, 2) /*! * Defines flags that can be used when capturing to an OpenGL buffer in video memory. */ typedef enum { /*! * Default, capturing waits for a new frame or mouse move. * * The default behavior of blocking grabs is to wait for a new frame until * after the call was made. But it's possible that there is a frame already * ready that the client hasn't seen. * \see NVFBC_TOGL_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY */ NVFBC_TOGL_GRAB_FLAGS_NOFLAGS = 0, /*! * Capturing does not wait for a new frame nor a mouse move. * * It is therefore possible to capture the same frame multiple times. * When this occurs, the dwCurrentFrame parameter of the * NVFBC_FRAME_GRAB_INFO structure is not incremented. */ NVFBC_TOGL_GRAB_FLAGS_NOWAIT = (1 << 0), /*! * [in] Forces the destination buffer to be refreshed even if the frame * has not changed since previous capture. * * By default, if the captured frame is identical to the previous one, NvFBC * will omit one copy and not update the destination buffer. * * Setting that flag will prevent this behavior. This can be useful e.g., * if the application has modified the buffer in the meantime. */ NVFBC_TOGL_GRAB_FLAGS_FORCE_REFRESH = (1 << 1), /*! * Similar to NVFBC_TOGL_GRAB_FLAGS_NOFLAGS, except that the capture will * not wait if there is already a frame available that the client has * never seen yet. */ NVFBC_TOGL_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY = (1 << 2), } NVFBC_TOGL_FLAGS; /*! * Maximum number of GL textures that can be used to store frames. */ #define NVFBC_TOGL_TEXTURES_MAX 2 /*! * Defines parameters for the ::NvFBCToGLSetUp() API call. */ typedef struct _NVFBC_TOGL_SETUP_PARAMS { /*! * [in] Must be set to NVFBC_TOGL_SETUP_PARAMS_VER */ uint32_t dwVersion; /*! * [in] Desired buffer format. */ NVFBC_BUFFER_FORMAT eBufferFormat; /*! * [in] Whether differential maps should be generated. */ NVFBC_BOOL bWithDiffMap; /*! * [out] Pointer to a pointer to a buffer in system memory. * * \see NVFBC_TOSYS_SETUP_PARAMS::ppDiffMap */ void **ppDiffMap; /*! * [in] Scaling factor of the differential maps. * * \see NVFBC_TOSYS_SETUP_PARAMS::dwDiffMapScalingFactor */ uint32_t dwDiffMapScalingFactor; /*! * [out] List of GL textures that will store the captured frames. * * This array is 0 terminated. The number of textures varies depending on * the capture settings (such as whether diffmaps are enabled). * * An application wishing to interop with, for example, EncodeAPI will need * to register these textures prior to start encoding frames. * * After each frame capture, the texture holding the current frame will be * returned in NVFBC_TOGL_GRAB_FRAME_PARAMS::dwTexture. */ uint32_t dwTextures[NVFBC_TOGL_TEXTURES_MAX]; /*! * [out] GL target to which the texture should be bound. */ uint32_t dwTexTarget; /*! * [out] GL format of the textures. */ uint32_t dwTexFormat; /*! * [out] GL type of the textures. */ uint32_t dwTexType; /*! * [out] Size of the differential map. * * Only set if bWithDiffMap is set to NVFBC_TRUE. */ NVFBC_SIZE diffMapSize; } NVFBC_TOGL_SETUP_PARAMS; /*! * NVFBC_TOGL_SETUP_PARAMS structure version. */ #define NVFBC_TOGL_SETUP_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_TOGL_SETUP_PARAMS, 2) /*! * Defines parameters for the ::NvFBCToGLGrabFrame() API call. */ typedef struct _NVFBC_TOGL_GRAB_FRAME_PARAMS { /*! * [in] Must be set to NVFBC_TOGL_GRAB_FRAME_PARAMS_VER. */ uint32_t dwVersion; /*! * [in] Flags defining the behavior of this frame capture. */ uint32_t dwFlags; /*! * [out] Index of the texture storing the current frame. * * This is an index in the NVFBC_TOGL_SETUP_PARAMS::dwTextures array. */ uint32_t dwTextureIndex; /*! * [out] Information about the captured frame. * * Can be NULL. */ NVFBC_FRAME_GRAB_INFO *pFrameGrabInfo; /*! * [in] Wait timeout in milliseconds. * * When capturing frames with the NVFBC_TOGL_GRAB_FLAGS_NOFLAGS or * NVFBC_TOGL_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY flags, * NvFBC will wait for a new frame or mouse move until the below timer * expires. * * When timing out, the last captured frame will be returned. Note that as * long as the NVFBC_TOGL_GRAB_FLAGS_FORCE_REFRESH flag is not set, * returning an old frame will incur no performance penalty. * * NvFBC clients can use the return value of the grab frame operation to * find out whether a new frame was captured, or the timer expired. * * Note that the behavior of blocking calls is to wait for a new frame * *after* the call has been made. When using timeouts, it is possible * that NvFBC will return a new frame (e.g., it has never been captured * before) even though no new frame was generated after the grab call. * * For the precise definition of what constitutes a new frame, see * ::bIsNewFrame. * * Set to 0 to disable timeouts. */ uint32_t dwTimeoutMs; } NVFBC_TOGL_GRAB_FRAME_PARAMS; /*! * NVFBC_TOGL_GRAB_FRAME_PARAMS structure version. */ #define NVFBC_TOGL_GRAB_FRAME_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_TOGL_GRAB_FRAME_PARAMS, 2) /*! @} FBC_STRUCT */ /*! * \defgroup FBC_FUNC API Entry Points * * Entry points are thread-safe and can be called concurrently. * * The locking model includes a global lock that protects session handle * management (\see NvFBCCreateHandle, \see NvFBCDestroyHandle). * * Each NvFBC session uses a local lock to protect other entry points. Note * that in certain cases, a thread can hold the local lock for an undefined * amount of time, such as grabbing a frame using a blocking call. * * Note that a context is associated with each session. NvFBC clients wishing * to share a session between different threads are expected to release and * bind the context appropriately (\see NvFBCBindContext, * \see NvFBCReleaseContext). This is not required when each thread uses its * own NvFBC session. * * @{ */ /*! * Gets the last error message that got recorded for a client. * * When NvFBC returns an error, it will save an error message that can be * queried through this API call. Only the last message is saved. * The message and the return code should give enough information about * what went wrong. * * \param [in] sessionHandle * Handle to the NvFBC client. * \return * A NULL terminated error message, or an empty string. Its maximum length * is NVFBC_ERROR_STR_LEN. */ const char *NVFBCAPI NvFBCGetLastErrorStr(const NVFBC_SESSION_HANDLE sessionHandle); /*! * \brief Allocates a new handle for an NvFBC client. * * This function allocates a session handle used to identify an FBC client. * * This function implicitly calls NvFBCBindContext(). * * \param [out] pSessionHandle * Pointer that will hold the allocated session handle. * \param [in] pParams * ::NVFBC_CREATE_HANDLE_PARAMS * * \return * ::NVFBC_SUCCESS \n * ::NVFBC_ERR_INVALID_PTR \n * ::NVFBC_ERR_API_VERSION \n * ::NVFBC_ERR_INTERNAL \n * ::NVFBC_ERR_OUT_OF_MEMORY \n * ::NVFBC_ERR_MAX_CLIENTS \n * ::NVFBC_ERR_X \n * ::NVFBC_ERR_GLX \n * ::NVFBC_ERR_GL * */ NVFBCSTATUS NVFBCAPI NvFBCCreateHandle(NVFBC_SESSION_HANDLE *pSessionHandle, NVFBC_CREATE_HANDLE_PARAMS *pParams); /*! * \brief Destroys the handle of an NvFBC client. * * This function uninitializes an FBC client. * * This function implicitly calls NvFBCReleaseContext(). * * After this function returns, it is not possible to use this session handle * for any further API call. * * \param [in] sessionHandle * FBC session handle. * \param [in] pParams * ::NVFBC_DESTROY_HANDLE_PARAMS * * \return * ::NVFBC_SUCCESS \n * ::NVFBC_ERR_INVALID_HANDLE \n * ::NVFBC_ERR_API_VERSION \n * ::NVFBC_ERR_BAD_REQUEST \n * ::NVFBC_ERR_INTERNAL \n * ::NVFBC_ERR_CONTEXT \n * ::NVFBC_ERR_X */ NVFBCSTATUS NVFBCAPI NvFBCDestroyHandle(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_DESTROY_HANDLE_PARAMS *pParams); /*! * \brief Gets the current status of the display driver. * * This function queries the display driver for various information. * * \param [in] sessionHandle * FBC session handle. * \param [in] pParams * ::NVFBC_GET_STATUS_PARAMS * * \return * ::NVFBC_SUCCESS \n * ::NVFBC_ERR_INVALID_HANDLE \n * ::NVFBC_ERR_API_VERSION \n * ::NVFBC_ERR_INTERNAL \n * ::NVFBC_ERR_X */ NVFBCSTATUS NVFBCAPI NvFBCGetStatus(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_GET_STATUS_PARAMS *pParams); /*! * \brief Binds the FBC context to the calling thread. * * The NvFBC library internally relies on objects that must be bound to a * thread. Such objects are OpenGL contexts and CUDA contexts. * * This function binds these objects to the calling thread. * * The FBC context must be bound to the calling thread for most NvFBC entry * points, otherwise ::NVFBC_ERR_CONTEXT is returned. * * If the FBC context is already bound to a different thread, * ::NVFBC_ERR_CONTEXT is returned. The other thread must release the context * first by calling the ReleaseContext() entry point. * * If the FBC context is already bound to the current thread, this function has * no effects. * * \param [in] sessionHandle * FBC session handle. * \param [in] pParams * ::NVFBC_DESTROY_CAPTURE_SESSION_PARAMS * * \return * ::NVFBC_SUCCESS \n * ::NVFBC_ERR_INVALID_HANDLE \n * ::NVFBC_ERR_API_VERSION \n * ::NVFBC_ERR_BAD_REQUEST \n * ::NVFBC_ERR_CONTEXT \n * ::NVFBC_ERR_INTERNAL \n * ::NVFBC_ERR_X */ NVFBCSTATUS NVFBCAPI NvFBCBindContext(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_BIND_CONTEXT_PARAMS *pParams); /*! * \brief Releases the FBC context from the calling thread. * * If the FBC context is bound to a different thread, ::NVFBC_ERR_CONTEXT is * returned. * * If the FBC context is already released, this function has no effects. * * \param [in] sessionHandle * FBC session handle. * \param [in] pParams * ::NVFBC_SUCCESS \n * ::NVFBC_ERR_INVALID_HANDLE \n * ::NVFBC_ERR_API_VERSION \n * ::NVFBC_ERR_BAD_REQUEST \n * ::NVFBC_ERR_CONTEXT \n * ::NVFBC_ERR_INTERNAL \n * ::NVFBC_ERR_X */ NVFBCSTATUS NVFBCAPI NvFBCReleaseContext(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_RELEASE_CONTEXT_PARAMS *pParams); /*! * \brief Creates a capture session for an FBC client. * * This function starts a capture session of the desired type (system memory, * video memory with CUDA interop, or H.264 compressed frames in system memory). * * Not all types are supported on all systems. Also, it is possible to use * NvFBC without having the CUDA library. In this case, requesting a capture * session of the concerned type will return an error. * * After this function returns, the display driver will start generating frames * that can be captured using the corresponding API call. * * \param [in] sessionHandle * FBC session handle. * \param [in] pParams * ::NVFBC_CREATE_CAPTURE_SESSION_PARAMS * * \return * ::NVFBC_SUCCESS \n * ::NVFBC_ERR_INVALID_HANDLE \n * ::NVFBC_ERR_API_VERSION \n * ::NVFBC_ERR_BAD_REQUEST \n * ::NVFBC_ERR_CONTEXT \n * ::NVFBC_ERR_INVALID_PARAM \n * ::NVFBC_ERR_OUT_OF_MEMORY \n * ::NVFBC_ERR_X \n * ::NVFBC_ERR_GLX \n * ::NVFBC_ERR_GL \n * ::NVFBC_ERR_CUDA \n * ::NVFBC_ERR_MUST_RECREATE \n * ::NVFBC_ERR_INTERNAL */ NVFBCSTATUS NVFBCAPI NvFBCCreateCaptureSession(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_CREATE_CAPTURE_SESSION_PARAMS *pParams); /*! * \brief Destroys a capture session for an FBC client. * * This function stops a capture session and frees allocated objects. * * After this function returns, it is possible to create another capture * session using the corresponding API call. * * \param [in] sessionHandle * FBC session handle. * \param [in] pParams * ::NVFBC_DESTROY_CAPTURE_SESSION_PARAMS * * \return * ::NVFBC_SUCCESS \n * ::NVFBC_ERR_INVALID_HANDLE \n * ::NVFBC_ERR_API_VERSION \n * ::NVFBC_ERR_BAD_REQUEST \n * ::NVFBC_ERR_CONTEXT \n * ::NVFBC_ERR_INTERNAL \n * ::NVFBC_ERR_X */ NVFBCSTATUS NVFBCAPI NvFBCDestroyCaptureSession(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_DESTROY_CAPTURE_SESSION_PARAMS *pParams); /*! * \brief Sets up a capture to system memory session. * * This function configures how the capture to system memory should behave. It * can be called anytime and several times after the capture session has been * created. However, it must be called at least once prior to start capturing * frames. * * This function allocates the buffer that will contain the captured frame. * The application does not need to free this buffer. The size of this buffer * is returned in the ::NVFBC_FRAME_GRAB_INFO structure. * * \param [in] sessionHandle * FBC session handle. * \param [in] pParams * ::NVFBC_TOSYS_SETUP_PARAMS * * \return * ::NVFBC_SUCCESS \n * ::NVFBC_ERR_INVALID_HANDLE \n * ::NVFBC_ERR_API_VERSION \n * ::NVFBC_ERR_BAD_REQUEST \n * ::NVFBC_ERR_INTERNAL \n * ::NVFBC_ERR_CONTEXT \n * ::NVFBC_ERR_UNSUPPORTED \n * ::NVFBC_ERR_INVALID_PTR \n * ::NVFBC_ERR_INVALID_PARAM \n * ::NVFBC_ERR_OUT_OF_MEMORY \n * ::NVFBC_ERR_X */ NVFBCSTATUS NVFBCAPI NvFBCToSysSetUp(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOSYS_SETUP_PARAMS *pParams); /*! * \brief Captures a frame to a buffer in system memory. * * This function triggers a frame capture to a buffer in system memory that was * registered with the ToSysSetUp() API call. * * Note that it is possible that the resolution of the desktop changes while * capturing frames. This should be transparent for the application. * * When the resolution changes, the capture session is recreated using the same * parameters, and necessary buffers are re-allocated. The frame counter is not * reset. * * An application can detect that the resolution changed by comparing the * dwByteSize member of the ::NVFBC_FRAME_GRAB_INFO against a previous * frame and/or dwWidth and dwHeight. * * During a change of resolution the capture is paused even in asynchronous * mode. * * \param [in] sessionHandle * FBC session handle. * \param [in] pParams * ::NVFBC_TOSYS_GRAB_FRAME_PARAMS * * \return * ::NVFBC_SUCCESS \n * ::NVFBC_ERR_INVALID_HANDLE \n * ::NVFBC_ERR_API_VERSION \n * ::NVFBC_ERR_BAD_REQUEST \n * ::NVFBC_ERR_CONTEXT \n * ::NVFBC_ERR_INVALID_PTR \n * ::NVFBC_ERR_INTERNAL \n * ::NVFBC_ERR_X \n * ::NVFBC_ERR_MUST_RECREATE \n * \see NvFBCCreateCaptureSession \n * \see NvFBCToSysSetUp */ NVFBCSTATUS NVFBCAPI NvFBCToSysGrabFrame(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOSYS_GRAB_FRAME_PARAMS *pParams); /*! * \brief Sets up a capture to video memory session. * * This function configures how the capture to video memory with CUDA interop * should behave. It can be called anytime and several times after the capture * session has been created. However, it must be called at least once prior * to start capturing frames. * * \param [in] sessionHandle * FBC session handle. * * \param [in] pParams * ::NVFBC_TOCUDA_SETUP_PARAMS * * \return * ::NVFBC_SUCCESS \n * ::NVFBC_ERR_INVALID_HANDLE \n * ::NVFBC_ERR_API_VERSION \n * ::NVFBC_ERR_BAD_REQUEST \n * ::NVFBC_ERR_INTERNAL \n * ::NVFBC_ERR_CONTEXT \n * ::NVFBC_ERR_UNSUPPORTED \n * ::NVFBC_ERR_GL \n * ::NVFBC_ERR_X */ NVFBCSTATUS NVFBCAPI NvFBCToCudaSetUp(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOCUDA_SETUP_PARAMS *pParams); /*! * \brief Captures a frame to a CUDA device in video memory. * * This function triggers a frame capture to a CUDA device in video memory. * * Note about changes of resolution: \see NvFBCToSysGrabFrame * * \param [in] sessionHandle * FBC session handle. * * \param [in] pParams * ::NVFBC_TOCUDA_GRAB_FRAME_PARAMS * * \return * ::NVFBC_SUCCESS \n * ::NVFBC_ERR_INVALID_HANDLE \n * ::NVFBC_ERR_API_VERSION \n * ::NVFBC_ERR_BAD_REQUEST \n * ::NVFBC_ERR_CONTEXT \n * ::NVFBC_ERR_INVALID_PTR \n * ::NVFBC_ERR_CUDA \n * ::NVFBC_ERR_INTERNAL \n * ::NVFBC_ERR_X \n * ::NVFBC_ERR_MUST_RECREATE \n * \see NvFBCCreateCaptureSession \n * \see NvFBCToCudaSetUp */ NVFBCSTATUS NVFBCAPI NvFBCToCudaGrabFrame(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOCUDA_GRAB_FRAME_PARAMS *pParams); /*! * \brief Sets up a capture to OpenGL buffer in video memory session. * * This function configures how the capture to video memory should behave. * It can be called anytime and several times after the capture session has been * created. However, it must be called at least once prior to start capturing * frames. * * \param [in] sessionHandle * FBC session handle. * * \param [in] pParams * ::NVFBC_TOGL_SETUP_PARAMS * * \return * ::NVFBC_SUCCESS \n * ::NVFBC_ERR_INVALID_HANDLE \n * ::NVFBC_ERR_API_VERSION \n * ::NVFBC_ERR_BAD_REQUEST \n * ::NVFBC_ERR_INTERNAL \n * ::NVFBC_ERR_CONTEXT \n * ::NVFBC_ERR_UNSUPPORTED \n * ::NVFBC_ERR_GL \n * ::NVFBC_ERR_X */ NVFBCSTATUS NVFBCAPI NvFBCToGLSetUp(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOGL_SETUP_PARAMS *pParams); /*! * \brief Captures a frame to an OpenGL buffer in video memory. * * This function triggers a frame capture to a selected resource in video memory. * * Note about changes of resolution: \see NvFBCToSysGrabFrame * * \param [in] sessionHandle * FBC session handle. * * \param [in] pParams * ::NVFBC_TOGL_GRAB_FRAME_PARAMS * * \return * ::NVFBC_SUCCESS \n * ::NVFBC_ERR_INVALID_HANDLE \n * ::NVFBC_ERR_API_VERSION \n * ::NVFBC_ERR_BAD_REQUEST \n * ::NVFBC_ERR_CONTEXT \n * ::NVFBC_ERR_INVALID_PTR \n * ::NVFBC_ERR_INTERNAL \n * ::NVFBC_ERR_X \n * ::NVFBC_ERR_MUST_RECREATE \n * \see NvFBCCreateCaptureSession \n * \see NvFBCToCudaSetUp */ NVFBCSTATUS NVFBCAPI NvFBCToGLGrabFrame(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOGL_GRAB_FRAME_PARAMS *pParams); /*! * \cond FBC_PFN * * Defines API function pointers */ typedef const char *(NVFBCAPI *PNVFBCGETLASTERRORSTR)(const NVFBC_SESSION_HANDLE sessionHandle); typedef NVFBCSTATUS(NVFBCAPI *PNVFBCCREATEHANDLE)(NVFBC_SESSION_HANDLE *pSessionHandle, NVFBC_CREATE_HANDLE_PARAMS *pParams); typedef NVFBCSTATUS(NVFBCAPI *PNVFBCDESTROYHANDLE)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_DESTROY_HANDLE_PARAMS *pParams); typedef NVFBCSTATUS(NVFBCAPI *PNVFBCBINDCONTEXT)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_BIND_CONTEXT_PARAMS *pParams); typedef NVFBCSTATUS(NVFBCAPI *PNVFBCRELEASECONTEXT)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_RELEASE_CONTEXT_PARAMS *pParams); typedef NVFBCSTATUS(NVFBCAPI *PNVFBCGETSTATUS)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_GET_STATUS_PARAMS *pParams); typedef NVFBCSTATUS(NVFBCAPI *PNVFBCCREATECAPTURESESSION)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_CREATE_CAPTURE_SESSION_PARAMS *pParams); typedef NVFBCSTATUS(NVFBCAPI *PNVFBCDESTROYCAPTURESESSION)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_DESTROY_CAPTURE_SESSION_PARAMS *pParams); typedef NVFBCSTATUS(NVFBCAPI *PNVFBCTOSYSSETUP)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOSYS_SETUP_PARAMS *pParams); typedef NVFBCSTATUS(NVFBCAPI *PNVFBCTOSYSGRABFRAME)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOSYS_GRAB_FRAME_PARAMS *pParams); typedef NVFBCSTATUS(NVFBCAPI *PNVFBCTOCUDASETUP)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOCUDA_SETUP_PARAMS *pParams); typedef NVFBCSTATUS(NVFBCAPI *PNVFBCTOCUDAGRABFRAME)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOCUDA_GRAB_FRAME_PARAMS *pParams); typedef NVFBCSTATUS(NVFBCAPI *PNVFBCTOGLSETUP)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOGL_SETUP_PARAMS *pParams); typedef NVFBCSTATUS(NVFBCAPI *PNVFBCTOGLGRABFRAME)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOGL_GRAB_FRAME_PARAMS *pParams); /// \endcond /*! @} FBC_FUNC */ /*! * \ingroup FBC_STRUCT * * Structure populated with API function pointers. */ typedef struct { uint32_t dwVersion; //!< [in] Must be set to NVFBC_VERSION. PNVFBCGETLASTERRORSTR nvFBCGetLastErrorStr; //!< [out] Pointer to ::NvFBCGetLastErrorStr(). PNVFBCCREATEHANDLE nvFBCCreateHandle; //!< [out] Pointer to ::NvFBCCreateHandle(). PNVFBCDESTROYHANDLE nvFBCDestroyHandle; //!< [out] Pointer to ::NvFBCDestroyHandle(). PNVFBCGETSTATUS nvFBCGetStatus; //!< [out] Pointer to ::NvFBCGetStatus(). PNVFBCCREATECAPTURESESSION nvFBCCreateCaptureSession; //!< [out] Pointer to ::NvFBCCreateCaptureSession(). PNVFBCDESTROYCAPTURESESSION nvFBCDestroyCaptureSession; //!< [out] Pointer to ::NvFBCDestroyCaptureSession(). PNVFBCTOSYSSETUP nvFBCToSysSetUp; //!< [out] Pointer to ::NvFBCToSysSetUp(). PNVFBCTOSYSGRABFRAME nvFBCToSysGrabFrame; //!< [out] Pointer to ::NvFBCToSysGrabFrame(). PNVFBCTOCUDASETUP nvFBCToCudaSetUp; //!< [out] Pointer to ::NvFBCToCudaSetUp(). PNVFBCTOCUDAGRABFRAME nvFBCToCudaGrabFrame; //!< [out] Pointer to ::NvFBCToCudaGrabFrame(). void *pad1; //!< [out] Retired. Do not use. void *pad2; //!< [out] Retired. Do not use. void *pad3; //!< [out] Retired. Do not use. PNVFBCBINDCONTEXT nvFBCBindContext; //!< [out] Pointer to ::NvFBCBindContext(). PNVFBCRELEASECONTEXT nvFBCReleaseContext; //!< [out] Pointer to ::NvFBCReleaseContext(). void *pad4; //!< [out] Retired. Do not use. void *pad5; //!< [out] Retired. Do not use. void *pad6; //!< [out] Retired. Do not use. void *pad7; //!< [out] Retired. Do not use. PNVFBCTOGLSETUP nvFBCToGLSetUp; //!< [out] Pointer to ::nvFBCToGLSetup(). PNVFBCTOGLGRABFRAME nvFBCToGLGrabFrame; //!< [out] Pointer to ::nvFBCToGLGrabFrame(). } NVFBC_API_FUNCTION_LIST; /*! * \ingroup FBC_FUNC * * \brief Entry Points to the NvFBC interface. * * Creates an instance of the NvFBC interface, and populates the * pFunctionList with function pointers to the API routines implemented by * the NvFBC interface. * * \param [out] pFunctionList * * \return * ::NVFBC_SUCCESS \n * ::NVFBC_ERR_INVALID_PTR \n * ::NVFBC_ERR_API_VERSION */ NVFBCSTATUS NVFBCAPI NvFBCCreateInstance(NVFBC_API_FUNCTION_LIST *pFunctionList); /*! * \ingroup FBC_FUNC * * Defines function pointer for the ::NvFBCCreateInstance() API call. */ typedef NVFBCSTATUS(NVFBCAPI *PNVFBCCREATEINSTANCE)(NVFBC_API_FUNCTION_LIST *pFunctionList); #ifdef __cplusplus } #endif #endif // _NVFBC_H_ ================================================ FILE: third-party/nvfbc/helper_math.h ================================================ /* Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of NVIDIA CORPORATION nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * This file implements common mathematical operations on vector types * (float3, float4 etc.) since these are not provided as standard by CUDA. * * The syntax is modeled on the Cg standard library. * * This is part of the Helper library includes * * Thanks to Linh Hah for additions and fixes. */ #ifndef HELPER_MATH_H #define HELPER_MATH_H #include "cuda_runtime.h" typedef unsigned int uint; typedef unsigned short ushort; #ifndef EXIT_WAIVED #define EXIT_WAIVED 2 #endif #ifndef __CUDACC__ #include //////////////////////////////////////////////////////////////////////////////// // host implementations of CUDA functions //////////////////////////////////////////////////////////////////////////////// inline float fminf(float a, float b) { return a < b ? a : b; } inline float fmaxf(float a, float b) { return a > b ? a : b; } inline int max(int a, int b) { return a > b ? a : b; } inline int min(int a, int b) { return a < b ? a : b; } inline float rsqrtf(float x) { return 1.0f / sqrtf(x); } #endif //////////////////////////////////////////////////////////////////////////////// // constructors //////////////////////////////////////////////////////////////////////////////// inline __host__ __device__ float2 make_float2(float s) { return make_float2(s, s); } inline __host__ __device__ float2 make_float2(float3 a) { return make_float2(a.x, a.y); } inline __host__ __device__ float2 make_float2(int2 a) { return make_float2(float(a.x), float(a.y)); } inline __host__ __device__ float2 make_float2(uint2 a) { return make_float2(float(a.x), float(a.y)); } inline __host__ __device__ int2 make_int2(int s) { return make_int2(s, s); } inline __host__ __device__ int2 make_int2(int3 a) { return make_int2(a.x, a.y); } inline __host__ __device__ int2 make_int2(uint2 a) { return make_int2(int(a.x), int(a.y)); } inline __host__ __device__ int2 make_int2(float2 a) { return make_int2(int(a.x), int(a.y)); } inline __host__ __device__ uint2 make_uint2(uint s) { return make_uint2(s, s); } inline __host__ __device__ uint2 make_uint2(uint3 a) { return make_uint2(a.x, a.y); } inline __host__ __device__ uint2 make_uint2(int2 a) { return make_uint2(uint(a.x), uint(a.y)); } inline __host__ __device__ float3 make_float3(float s) { return make_float3(s, s, s); } inline __host__ __device__ float3 make_float3(float2 a) { return make_float3(a.x, a.y, 0.0f); } inline __host__ __device__ float3 make_float3(float2 a, float s) { return make_float3(a.x, a.y, s); } inline __host__ __device__ float3 make_float3(float4 a) { return make_float3(a.x, a.y, a.z); } inline __host__ __device__ float3 make_float3(int3 a) { return make_float3(float(a.x), float(a.y), float(a.z)); } inline __host__ __device__ float3 make_float3(uint3 a) { return make_float3(float(a.x), float(a.y), float(a.z)); } inline __host__ __device__ int3 make_int3(int s) { return make_int3(s, s, s); } inline __host__ __device__ int3 make_int3(int2 a) { return make_int3(a.x, a.y, 0); } inline __host__ __device__ int3 make_int3(int2 a, int s) { return make_int3(a.x, a.y, s); } inline __host__ __device__ int3 make_int3(uint3 a) { return make_int3(int(a.x), int(a.y), int(a.z)); } inline __host__ __device__ int3 make_int3(float3 a) { return make_int3(int(a.x), int(a.y), int(a.z)); } inline __host__ __device__ uint3 make_uint3(uint s) { return make_uint3(s, s, s); } inline __host__ __device__ uint3 make_uint3(uint2 a) { return make_uint3(a.x, a.y, 0); } inline __host__ __device__ uint3 make_uint3(uint2 a, uint s) { return make_uint3(a.x, a.y, s); } inline __host__ __device__ uint3 make_uint3(uint4 a) { return make_uint3(a.x, a.y, a.z); } inline __host__ __device__ uint3 make_uint3(int3 a) { return make_uint3(uint(a.x), uint(a.y), uint(a.z)); } inline __host__ __device__ float4 make_float4(float s) { return make_float4(s, s, s, s); } inline __host__ __device__ float4 make_float4(float3 a) { return make_float4(a.x, a.y, a.z, 0.0f); } inline __host__ __device__ float4 make_float4(float3 a, float w) { return make_float4(a.x, a.y, a.z, w); } inline __host__ __device__ float4 make_float4(int4 a) { return make_float4(float(a.x), float(a.y), float(a.z), float(a.w)); } inline __host__ __device__ float4 make_float4(uint4 a) { return make_float4(float(a.x), float(a.y), float(a.z), float(a.w)); } inline __host__ __device__ int4 make_int4(int s) { return make_int4(s, s, s, s); } inline __host__ __device__ int4 make_int4(int3 a) { return make_int4(a.x, a.y, a.z, 0); } inline __host__ __device__ int4 make_int4(int3 a, int w) { return make_int4(a.x, a.y, a.z, w); } inline __host__ __device__ int4 make_int4(uint4 a) { return make_int4(int(a.x), int(a.y), int(a.z), int(a.w)); } inline __host__ __device__ int4 make_int4(float4 a) { return make_int4(int(a.x), int(a.y), int(a.z), int(a.w)); } inline __host__ __device__ uint4 make_uint4(uint s) { return make_uint4(s, s, s, s); } inline __host__ __device__ uint4 make_uint4(uint3 a) { return make_uint4(a.x, a.y, a.z, 0); } inline __host__ __device__ uint4 make_uint4(uint3 a, uint w) { return make_uint4(a.x, a.y, a.z, w); } inline __host__ __device__ uint4 make_uint4(int4 a) { return make_uint4(uint(a.x), uint(a.y), uint(a.z), uint(a.w)); } //////////////////////////////////////////////////////////////////////////////// // negate //////////////////////////////////////////////////////////////////////////////// inline __host__ __device__ float2 operator-(float2 &a) { return make_float2(-a.x, -a.y); } inline __host__ __device__ int2 operator-(int2 &a) { return make_int2(-a.x, -a.y); } inline __host__ __device__ float3 operator-(float3 &a) { return make_float3(-a.x, -a.y, -a.z); } inline __host__ __device__ int3 operator-(int3 &a) { return make_int3(-a.x, -a.y, -a.z); } inline __host__ __device__ float4 operator-(float4 &a) { return make_float4(-a.x, -a.y, -a.z, -a.w); } inline __host__ __device__ int4 operator-(int4 &a) { return make_int4(-a.x, -a.y, -a.z, -a.w); } //////////////////////////////////////////////////////////////////////////////// // addition //////////////////////////////////////////////////////////////////////////////// inline __host__ __device__ float2 operator+(float2 a, float2 b) { return make_float2(a.x + b.x, a.y + b.y); } inline __host__ __device__ void operator+=(float2 &a, float2 b) { a.x += b.x; a.y += b.y; } inline __host__ __device__ float2 operator+(float2 a, float b) { return make_float2(a.x + b, a.y + b); } inline __host__ __device__ float2 operator+(float b, float2 a) { return make_float2(a.x + b, a.y + b); } inline __host__ __device__ void operator+=(float2 &a, float b) { a.x += b; a.y += b; } inline __host__ __device__ int2 operator+(int2 a, int2 b) { return make_int2(a.x + b.x, a.y + b.y); } inline __host__ __device__ void operator+=(int2 &a, int2 b) { a.x += b.x; a.y += b.y; } inline __host__ __device__ int2 operator+(int2 a, int b) { return make_int2(a.x + b, a.y + b); } inline __host__ __device__ int2 operator+(int b, int2 a) { return make_int2(a.x + b, a.y + b); } inline __host__ __device__ void operator+=(int2 &a, int b) { a.x += b; a.y += b; } inline __host__ __device__ uint2 operator+(uint2 a, uint2 b) { return make_uint2(a.x + b.x, a.y + b.y); } inline __host__ __device__ void operator+=(uint2 &a, uint2 b) { a.x += b.x; a.y += b.y; } inline __host__ __device__ uint2 operator+(uint2 a, uint b) { return make_uint2(a.x + b, a.y + b); } inline __host__ __device__ uint2 operator+(uint b, uint2 a) { return make_uint2(a.x + b, a.y + b); } inline __host__ __device__ void operator+=(uint2 &a, uint b) { a.x += b; a.y += b; } inline __host__ __device__ float3 operator+(float3 a, float3 b) { return make_float3(a.x + b.x, a.y + b.y, a.z + b.z); } inline __host__ __device__ void operator+=(float3 &a, float3 b) { a.x += b.x; a.y += b.y; a.z += b.z; } inline __host__ __device__ float3 operator+(float3 a, float b) { return make_float3(a.x + b, a.y + b, a.z + b); } inline __host__ __device__ void operator+=(float3 &a, float b) { a.x += b; a.y += b; a.z += b; } inline __host__ __device__ int3 operator+(int3 a, int3 b) { return make_int3(a.x + b.x, a.y + b.y, a.z + b.z); } inline __host__ __device__ void operator+=(int3 &a, int3 b) { a.x += b.x; a.y += b.y; a.z += b.z; } inline __host__ __device__ int3 operator+(int3 a, int b) { return make_int3(a.x + b, a.y + b, a.z + b); } inline __host__ __device__ void operator+=(int3 &a, int b) { a.x += b; a.y += b; a.z += b; } inline __host__ __device__ uint3 operator+(uint3 a, uint3 b) { return make_uint3(a.x + b.x, a.y + b.y, a.z + b.z); } inline __host__ __device__ void operator+=(uint3 &a, uint3 b) { a.x += b.x; a.y += b.y; a.z += b.z; } inline __host__ __device__ uint3 operator+(uint3 a, uint b) { return make_uint3(a.x + b, a.y + b, a.z + b); } inline __host__ __device__ void operator+=(uint3 &a, uint b) { a.x += b; a.y += b; a.z += b; } inline __host__ __device__ int3 operator+(int b, int3 a) { return make_int3(a.x + b, a.y + b, a.z + b); } inline __host__ __device__ uint3 operator+(uint b, uint3 a) { return make_uint3(a.x + b, a.y + b, a.z + b); } inline __host__ __device__ float3 operator+(float b, float3 a) { return make_float3(a.x + b, a.y + b, a.z + b); } inline __host__ __device__ float4 operator+(float4 a, float4 b) { return make_float4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); } inline __host__ __device__ void operator+=(float4 &a, float4 b) { a.x += b.x; a.y += b.y; a.z += b.z; a.w += b.w; } inline __host__ __device__ float4 operator+(float4 a, float b) { return make_float4(a.x + b, a.y + b, a.z + b, a.w + b); } inline __host__ __device__ float4 operator+(float b, float4 a) { return make_float4(a.x + b, a.y + b, a.z + b, a.w + b); } inline __host__ __device__ void operator+=(float4 &a, float b) { a.x += b; a.y += b; a.z += b; a.w += b; } inline __host__ __device__ int4 operator+(int4 a, int4 b) { return make_int4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); } inline __host__ __device__ void operator+=(int4 &a, int4 b) { a.x += b.x; a.y += b.y; a.z += b.z; a.w += b.w; } inline __host__ __device__ int4 operator+(int4 a, int b) { return make_int4(a.x + b, a.y + b, a.z + b, a.w + b); } inline __host__ __device__ int4 operator+(int b, int4 a) { return make_int4(a.x + b, a.y + b, a.z + b, a.w + b); } inline __host__ __device__ void operator+=(int4 &a, int b) { a.x += b; a.y += b; a.z += b; a.w += b; } inline __host__ __device__ uint4 operator+(uint4 a, uint4 b) { return make_uint4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); } inline __host__ __device__ void operator+=(uint4 &a, uint4 b) { a.x += b.x; a.y += b.y; a.z += b.z; a.w += b.w; } inline __host__ __device__ uint4 operator+(uint4 a, uint b) { return make_uint4(a.x + b, a.y + b, a.z + b, a.w + b); } inline __host__ __device__ uint4 operator+(uint b, uint4 a) { return make_uint4(a.x + b, a.y + b, a.z + b, a.w + b); } inline __host__ __device__ void operator+=(uint4 &a, uint b) { a.x += b; a.y += b; a.z += b; a.w += b; } //////////////////////////////////////////////////////////////////////////////// // subtract //////////////////////////////////////////////////////////////////////////////// inline __host__ __device__ float2 operator-(float2 a, float2 b) { return make_float2(a.x - b.x, a.y - b.y); } inline __host__ __device__ void operator-=(float2 &a, float2 b) { a.x -= b.x; a.y -= b.y; } inline __host__ __device__ float2 operator-(float2 a, float b) { return make_float2(a.x - b, a.y - b); } inline __host__ __device__ float2 operator-(float b, float2 a) { return make_float2(b - a.x, b - a.y); } inline __host__ __device__ void operator-=(float2 &a, float b) { a.x -= b; a.y -= b; } inline __host__ __device__ int2 operator-(int2 a, int2 b) { return make_int2(a.x - b.x, a.y - b.y); } inline __host__ __device__ void operator-=(int2 &a, int2 b) { a.x -= b.x; a.y -= b.y; } inline __host__ __device__ int2 operator-(int2 a, int b) { return make_int2(a.x - b, a.y - b); } inline __host__ __device__ int2 operator-(int b, int2 a) { return make_int2(b - a.x, b - a.y); } inline __host__ __device__ void operator-=(int2 &a, int b) { a.x -= b; a.y -= b; } inline __host__ __device__ uint2 operator-(uint2 a, uint2 b) { return make_uint2(a.x - b.x, a.y - b.y); } inline __host__ __device__ void operator-=(uint2 &a, uint2 b) { a.x -= b.x; a.y -= b.y; } inline __host__ __device__ uint2 operator-(uint2 a, uint b) { return make_uint2(a.x - b, a.y - b); } inline __host__ __device__ uint2 operator-(uint b, uint2 a) { return make_uint2(b - a.x, b - a.y); } inline __host__ __device__ void operator-=(uint2 &a, uint b) { a.x -= b; a.y -= b; } inline __host__ __device__ float3 operator-(float3 a, float3 b) { return make_float3(a.x - b.x, a.y - b.y, a.z - b.z); } inline __host__ __device__ void operator-=(float3 &a, float3 b) { a.x -= b.x; a.y -= b.y; a.z -= b.z; } inline __host__ __device__ float3 operator-(float3 a, float b) { return make_float3(a.x - b, a.y - b, a.z - b); } inline __host__ __device__ float3 operator-(float b, float3 a) { return make_float3(b - a.x, b - a.y, b - a.z); } inline __host__ __device__ void operator-=(float3 &a, float b) { a.x -= b; a.y -= b; a.z -= b; } inline __host__ __device__ int3 operator-(int3 a, int3 b) { return make_int3(a.x - b.x, a.y - b.y, a.z - b.z); } inline __host__ __device__ void operator-=(int3 &a, int3 b) { a.x -= b.x; a.y -= b.y; a.z -= b.z; } inline __host__ __device__ int3 operator-(int3 a, int b) { return make_int3(a.x - b, a.y - b, a.z - b); } inline __host__ __device__ int3 operator-(int b, int3 a) { return make_int3(b - a.x, b - a.y, b - a.z); } inline __host__ __device__ void operator-=(int3 &a, int b) { a.x -= b; a.y -= b; a.z -= b; } inline __host__ __device__ uint3 operator-(uint3 a, uint3 b) { return make_uint3(a.x - b.x, a.y - b.y, a.z - b.z); } inline __host__ __device__ void operator-=(uint3 &a, uint3 b) { a.x -= b.x; a.y -= b.y; a.z -= b.z; } inline __host__ __device__ uint3 operator-(uint3 a, uint b) { return make_uint3(a.x - b, a.y - b, a.z - b); } inline __host__ __device__ uint3 operator-(uint b, uint3 a) { return make_uint3(b - a.x, b - a.y, b - a.z); } inline __host__ __device__ void operator-=(uint3 &a, uint b) { a.x -= b; a.y -= b; a.z -= b; } inline __host__ __device__ float4 operator-(float4 a, float4 b) { return make_float4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); } inline __host__ __device__ void operator-=(float4 &a, float4 b) { a.x -= b.x; a.y -= b.y; a.z -= b.z; a.w -= b.w; } inline __host__ __device__ float4 operator-(float4 a, float b) { return make_float4(a.x - b, a.y - b, a.z - b, a.w - b); } inline __host__ __device__ void operator-=(float4 &a, float b) { a.x -= b; a.y -= b; a.z -= b; a.w -= b; } inline __host__ __device__ int4 operator-(int4 a, int4 b) { return make_int4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); } inline __host__ __device__ void operator-=(int4 &a, int4 b) { a.x -= b.x; a.y -= b.y; a.z -= b.z; a.w -= b.w; } inline __host__ __device__ int4 operator-(int4 a, int b) { return make_int4(a.x - b, a.y - b, a.z - b, a.w - b); } inline __host__ __device__ int4 operator-(int b, int4 a) { return make_int4(b - a.x, b - a.y, b - a.z, b - a.w); } inline __host__ __device__ void operator-=(int4 &a, int b) { a.x -= b; a.y -= b; a.z -= b; a.w -= b; } inline __host__ __device__ uint4 operator-(uint4 a, uint4 b) { return make_uint4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); } inline __host__ __device__ void operator-=(uint4 &a, uint4 b) { a.x -= b.x; a.y -= b.y; a.z -= b.z; a.w -= b.w; } inline __host__ __device__ uint4 operator-(uint4 a, uint b) { return make_uint4(a.x - b, a.y - b, a.z - b, a.w - b); } inline __host__ __device__ uint4 operator-(uint b, uint4 a) { return make_uint4(b - a.x, b - a.y, b - a.z, b - a.w); } inline __host__ __device__ void operator-=(uint4 &a, uint b) { a.x -= b; a.y -= b; a.z -= b; a.w -= b; } //////////////////////////////////////////////////////////////////////////////// // multiply //////////////////////////////////////////////////////////////////////////////// inline __host__ __device__ float2 operator*(float2 a, float2 b) { return make_float2(a.x * b.x, a.y * b.y); } inline __host__ __device__ void operator*=(float2 &a, float2 b) { a.x *= b.x; a.y *= b.y; } inline __host__ __device__ float2 operator*(float2 a, float b) { return make_float2(a.x * b, a.y * b); } inline __host__ __device__ float2 operator*(float b, float2 a) { return make_float2(b * a.x, b * a.y); } inline __host__ __device__ void operator*=(float2 &a, float b) { a.x *= b; a.y *= b; } inline __host__ __device__ int2 operator*(int2 a, int2 b) { return make_int2(a.x * b.x, a.y * b.y); } inline __host__ __device__ void operator*=(int2 &a, int2 b) { a.x *= b.x; a.y *= b.y; } inline __host__ __device__ int2 operator*(int2 a, int b) { return make_int2(a.x * b, a.y * b); } inline __host__ __device__ int2 operator*(int b, int2 a) { return make_int2(b * a.x, b * a.y); } inline __host__ __device__ void operator*=(int2 &a, int b) { a.x *= b; a.y *= b; } inline __host__ __device__ uint2 operator*(uint2 a, uint2 b) { return make_uint2(a.x * b.x, a.y * b.y); } inline __host__ __device__ void operator*=(uint2 &a, uint2 b) { a.x *= b.x; a.y *= b.y; } inline __host__ __device__ uint2 operator*(uint2 a, uint b) { return make_uint2(a.x * b, a.y * b); } inline __host__ __device__ uint2 operator*(uint b, uint2 a) { return make_uint2(b * a.x, b * a.y); } inline __host__ __device__ void operator*=(uint2 &a, uint b) { a.x *= b; a.y *= b; } inline __host__ __device__ float3 operator*(float3 a, float3 b) { return make_float3(a.x * b.x, a.y * b.y, a.z * b.z); } inline __host__ __device__ void operator*=(float3 &a, float3 b) { a.x *= b.x; a.y *= b.y; a.z *= b.z; } inline __host__ __device__ float3 operator*(float3 a, float b) { return make_float3(a.x * b, a.y * b, a.z * b); } inline __host__ __device__ float3 operator*(float b, float3 a) { return make_float3(b * a.x, b * a.y, b * a.z); } inline __host__ __device__ void operator*=(float3 &a, float b) { a.x *= b; a.y *= b; a.z *= b; } inline __host__ __device__ int3 operator*(int3 a, int3 b) { return make_int3(a.x * b.x, a.y * b.y, a.z * b.z); } inline __host__ __device__ void operator*=(int3 &a, int3 b) { a.x *= b.x; a.y *= b.y; a.z *= b.z; } inline __host__ __device__ int3 operator*(int3 a, int b) { return make_int3(a.x * b, a.y * b, a.z * b); } inline __host__ __device__ int3 operator*(int b, int3 a) { return make_int3(b * a.x, b * a.y, b * a.z); } inline __host__ __device__ void operator*=(int3 &a, int b) { a.x *= b; a.y *= b; a.z *= b; } inline __host__ __device__ uint3 operator*(uint3 a, uint3 b) { return make_uint3(a.x * b.x, a.y * b.y, a.z * b.z); } inline __host__ __device__ void operator*=(uint3 &a, uint3 b) { a.x *= b.x; a.y *= b.y; a.z *= b.z; } inline __host__ __device__ uint3 operator*(uint3 a, uint b) { return make_uint3(a.x * b, a.y * b, a.z * b); } inline __host__ __device__ uint3 operator*(uint b, uint3 a) { return make_uint3(b * a.x, b * a.y, b * a.z); } inline __host__ __device__ void operator*=(uint3 &a, uint b) { a.x *= b; a.y *= b; a.z *= b; } inline __host__ __device__ float4 operator*(float4 a, float4 b) { return make_float4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w); } inline __host__ __device__ void operator*=(float4 &a, float4 b) { a.x *= b.x; a.y *= b.y; a.z *= b.z; a.w *= b.w; } inline __host__ __device__ float4 operator*(float4 a, float b) { return make_float4(a.x * b, a.y * b, a.z * b, a.w * b); } inline __host__ __device__ float4 operator*(float b, float4 a) { return make_float4(b * a.x, b * a.y, b * a.z, b * a.w); } inline __host__ __device__ void operator*=(float4 &a, float b) { a.x *= b; a.y *= b; a.z *= b; a.w *= b; } inline __host__ __device__ int4 operator*(int4 a, int4 b) { return make_int4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w); } inline __host__ __device__ void operator*=(int4 &a, int4 b) { a.x *= b.x; a.y *= b.y; a.z *= b.z; a.w *= b.w; } inline __host__ __device__ int4 operator*(int4 a, int b) { return make_int4(a.x * b, a.y * b, a.z * b, a.w * b); } inline __host__ __device__ int4 operator*(int b, int4 a) { return make_int4(b * a.x, b * a.y, b * a.z, b * a.w); } inline __host__ __device__ void operator*=(int4 &a, int b) { a.x *= b; a.y *= b; a.z *= b; a.w *= b; } inline __host__ __device__ uint4 operator*(uint4 a, uint4 b) { return make_uint4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w); } inline __host__ __device__ void operator*=(uint4 &a, uint4 b) { a.x *= b.x; a.y *= b.y; a.z *= b.z; a.w *= b.w; } inline __host__ __device__ uint4 operator*(uint4 a, uint b) { return make_uint4(a.x * b, a.y * b, a.z * b, a.w * b); } inline __host__ __device__ uint4 operator*(uint b, uint4 a) { return make_uint4(b * a.x, b * a.y, b * a.z, b * a.w); } inline __host__ __device__ void operator*=(uint4 &a, uint b) { a.x *= b; a.y *= b; a.z *= b; a.w *= b; } //////////////////////////////////////////////////////////////////////////////// // divide //////////////////////////////////////////////////////////////////////////////// inline __host__ __device__ float2 operator/(float2 a, float2 b) { return make_float2(a.x / b.x, a.y / b.y); } inline __host__ __device__ void operator/=(float2 &a, float2 b) { a.x /= b.x; a.y /= b.y; } inline __host__ __device__ float2 operator/(float2 a, float b) { return make_float2(a.x / b, a.y / b); } inline __host__ __device__ void operator/=(float2 &a, float b) { a.x /= b; a.y /= b; } inline __host__ __device__ float2 operator/(float b, float2 a) { return make_float2(b / a.x, b / a.y); } inline __host__ __device__ float3 operator/(float3 a, float3 b) { return make_float3(a.x / b.x, a.y / b.y, a.z / b.z); } inline __host__ __device__ void operator/=(float3 &a, float3 b) { a.x /= b.x; a.y /= b.y; a.z /= b.z; } inline __host__ __device__ float3 operator/(float3 a, float b) { return make_float3(a.x / b, a.y / b, a.z / b); } inline __host__ __device__ void operator/=(float3 &a, float b) { a.x /= b; a.y /= b; a.z /= b; } inline __host__ __device__ float3 operator/(float b, float3 a) { return make_float3(b / a.x, b / a.y, b / a.z); } inline __host__ __device__ float4 operator/(float4 a, float4 b) { return make_float4(a.x / b.x, a.y / b.y, a.z / b.z, a.w / b.w); } inline __host__ __device__ void operator/=(float4 &a, float4 b) { a.x /= b.x; a.y /= b.y; a.z /= b.z; a.w /= b.w; } inline __host__ __device__ float4 operator/(float4 a, float b) { return make_float4(a.x / b, a.y / b, a.z / b, a.w / b); } inline __host__ __device__ void operator/=(float4 &a, float b) { a.x /= b; a.y /= b; a.z /= b; a.w /= b; } inline __host__ __device__ float4 operator/(float b, float4 a) { return make_float4(b / a.x, b / a.y, b / a.z, b / a.w); } //////////////////////////////////////////////////////////////////////////////// // min //////////////////////////////////////////////////////////////////////////////// inline __host__ __device__ float2 fminf(float2 a, float2 b) { return make_float2(fminf(a.x, b.x), fminf(a.y, b.y)); } inline __host__ __device__ float3 fminf(float3 a, float3 b) { return make_float3(fminf(a.x, b.x), fminf(a.y, b.y), fminf(a.z, b.z)); } inline __host__ __device__ float4 fminf(float4 a, float4 b) { return make_float4(fminf(a.x, b.x), fminf(a.y, b.y), fminf(a.z, b.z), fminf(a.w, b.w)); } inline __host__ __device__ int2 min(int2 a, int2 b) { return make_int2(min(a.x, b.x), min(a.y, b.y)); } inline __host__ __device__ int3 min(int3 a, int3 b) { return make_int3(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z)); } inline __host__ __device__ int4 min(int4 a, int4 b) { return make_int4(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z), min(a.w, b.w)); } inline __host__ __device__ uint2 min(uint2 a, uint2 b) { return make_uint2(min(a.x, b.x), min(a.y, b.y)); } inline __host__ __device__ uint3 min(uint3 a, uint3 b) { return make_uint3(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z)); } inline __host__ __device__ uint4 min(uint4 a, uint4 b) { return make_uint4(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z), min(a.w, b.w)); } //////////////////////////////////////////////////////////////////////////////// // max //////////////////////////////////////////////////////////////////////////////// inline __host__ __device__ float2 fmaxf(float2 a, float2 b) { return make_float2(fmaxf(a.x, b.x), fmaxf(a.y, b.y)); } inline __host__ __device__ float3 fmaxf(float3 a, float3 b) { return make_float3(fmaxf(a.x, b.x), fmaxf(a.y, b.y), fmaxf(a.z, b.z)); } inline __host__ __device__ float4 fmaxf(float4 a, float4 b) { return make_float4(fmaxf(a.x, b.x), fmaxf(a.y, b.y), fmaxf(a.z, b.z), fmaxf(a.w, b.w)); } inline __host__ __device__ int2 max(int2 a, int2 b) { return make_int2(max(a.x, b.x), max(a.y, b.y)); } inline __host__ __device__ int3 max(int3 a, int3 b) { return make_int3(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z)); } inline __host__ __device__ int4 max(int4 a, int4 b) { return make_int4(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z), max(a.w, b.w)); } inline __host__ __device__ uint2 max(uint2 a, uint2 b) { return make_uint2(max(a.x, b.x), max(a.y, b.y)); } inline __host__ __device__ uint3 max(uint3 a, uint3 b) { return make_uint3(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z)); } inline __host__ __device__ uint4 max(uint4 a, uint4 b) { return make_uint4(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z), max(a.w, b.w)); } //////////////////////////////////////////////////////////////////////////////// // lerp // - linear interpolation between a and b, based on value t in [0, 1] range //////////////////////////////////////////////////////////////////////////////// inline __device__ __host__ float lerp(float a, float b, float t) { return a + t * (b - a); } inline __device__ __host__ float2 lerp(float2 a, float2 b, float t) { return a + t * (b - a); } inline __device__ __host__ float3 lerp(float3 a, float3 b, float t) { return a + t * (b - a); } inline __device__ __host__ float4 lerp(float4 a, float4 b, float t) { return a + t * (b - a); } //////////////////////////////////////////////////////////////////////////////// // clamp // - clamp the value v to be in the range [a, b] //////////////////////////////////////////////////////////////////////////////// inline __device__ __host__ float clamp(float f, float a, float b) { return fmaxf(a, fminf(f, b)); } inline __device__ __host__ int clamp(int f, int a, int b) { return max(a, min(f, b)); } inline __device__ __host__ uint clamp(uint f, uint a, uint b) { return max(a, min(f, b)); } inline __device__ __host__ float2 clamp(float2 v, float a, float b) { return make_float2(clamp(v.x, a, b), clamp(v.y, a, b)); } inline __device__ __host__ float2 clamp(float2 v, float2 a, float2 b) { return make_float2(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y)); } inline __device__ __host__ float3 clamp(float3 v, float a, float b) { return make_float3(clamp(v.x, a, b), clamp(v.y, a, b), clamp(v.z, a, b)); } inline __device__ __host__ float3 clamp(float3 v, float3 a, float3 b) { return make_float3(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y), clamp(v.z, a.z, b.z)); } inline __device__ __host__ float4 clamp(float4 v, float a, float b) { return make_float4(clamp(v.x, a, b), clamp(v.y, a, b), clamp(v.z, a, b), clamp(v.w, a, b)); } inline __device__ __host__ float4 clamp(float4 v, float4 a, float4 b) { 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)); } inline __device__ __host__ int2 clamp(int2 v, int a, int b) { return make_int2(clamp(v.x, a, b), clamp(v.y, a, b)); } inline __device__ __host__ int2 clamp(int2 v, int2 a, int2 b) { return make_int2(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y)); } inline __device__ __host__ int3 clamp(int3 v, int a, int b) { return make_int3(clamp(v.x, a, b), clamp(v.y, a, b), clamp(v.z, a, b)); } inline __device__ __host__ int3 clamp(int3 v, int3 a, int3 b) { return make_int3(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y), clamp(v.z, a.z, b.z)); } inline __device__ __host__ int4 clamp(int4 v, int a, int b) { return make_int4(clamp(v.x, a, b), clamp(v.y, a, b), clamp(v.z, a, b), clamp(v.w, a, b)); } inline __device__ __host__ int4 clamp(int4 v, int4 a, int4 b) { 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)); } inline __device__ __host__ uint2 clamp(uint2 v, uint a, uint b) { return make_uint2(clamp(v.x, a, b), clamp(v.y, a, b)); } inline __device__ __host__ uint2 clamp(uint2 v, uint2 a, uint2 b) { return make_uint2(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y)); } inline __device__ __host__ uint3 clamp(uint3 v, uint a, uint b) { return make_uint3(clamp(v.x, a, b), clamp(v.y, a, b), clamp(v.z, a, b)); } inline __device__ __host__ uint3 clamp(uint3 v, uint3 a, uint3 b) { return make_uint3(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y), clamp(v.z, a.z, b.z)); } inline __device__ __host__ uint4 clamp(uint4 v, uint a, uint b) { return make_uint4(clamp(v.x, a, b), clamp(v.y, a, b), clamp(v.z, a, b), clamp(v.w, a, b)); } inline __device__ __host__ uint4 clamp(uint4 v, uint4 a, uint4 b) { 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)); } //////////////////////////////////////////////////////////////////////////////// // dot product //////////////////////////////////////////////////////////////////////////////// inline __host__ __device__ float dot(float2 a, float2 b) { return a.x * b.x + a.y * b.y; } inline __host__ __device__ float dot(float3 a, float3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; } inline __host__ __device__ float dot(float4 a, float4 b) { return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; } inline __host__ __device__ int dot(int2 a, int2 b) { return a.x * b.x + a.y * b.y; } inline __host__ __device__ int dot(int3 a, int3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; } inline __host__ __device__ int dot(int4 a, int4 b) { return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; } inline __host__ __device__ uint dot(uint2 a, uint2 b) { return a.x * b.x + a.y * b.y; } inline __host__ __device__ uint dot(uint3 a, uint3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; } inline __host__ __device__ uint dot(uint4 a, uint4 b) { return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; } //////////////////////////////////////////////////////////////////////////////// // length //////////////////////////////////////////////////////////////////////////////// inline __host__ __device__ float length(float2 v) { return sqrtf(dot(v, v)); } inline __host__ __device__ float length(float3 v) { return sqrtf(dot(v, v)); } inline __host__ __device__ float length(float4 v) { return sqrtf(dot(v, v)); } //////////////////////////////////////////////////////////////////////////////// // normalize //////////////////////////////////////////////////////////////////////////////// inline __host__ __device__ float2 normalize(float2 v) { float invLen = rsqrtf(dot(v, v)); return v * invLen; } inline __host__ __device__ float3 normalize(float3 v) { float invLen = rsqrtf(dot(v, v)); return v * invLen; } inline __host__ __device__ float4 normalize(float4 v) { float invLen = rsqrtf(dot(v, v)); return v * invLen; } //////////////////////////////////////////////////////////////////////////////// // floor //////////////////////////////////////////////////////////////////////////////// inline __host__ __device__ float2 floorf(float2 v) { return make_float2(floorf(v.x), floorf(v.y)); } inline __host__ __device__ float3 floorf(float3 v) { return make_float3(floorf(v.x), floorf(v.y), floorf(v.z)); } inline __host__ __device__ float4 floorf(float4 v) { return make_float4(floorf(v.x), floorf(v.y), floorf(v.z), floorf(v.w)); } //////////////////////////////////////////////////////////////////////////////// // frac - returns the fractional portion of a scalar or each vector component //////////////////////////////////////////////////////////////////////////////// inline __host__ __device__ float fracf(float v) { return v - floorf(v); } inline __host__ __device__ float2 fracf(float2 v) { return make_float2(fracf(v.x), fracf(v.y)); } inline __host__ __device__ float3 fracf(float3 v) { return make_float3(fracf(v.x), fracf(v.y), fracf(v.z)); } inline __host__ __device__ float4 fracf(float4 v) { return make_float4(fracf(v.x), fracf(v.y), fracf(v.z), fracf(v.w)); } //////////////////////////////////////////////////////////////////////////////// // fmod //////////////////////////////////////////////////////////////////////////////// inline __host__ __device__ float2 fmodf(float2 a, float2 b) { return make_float2(fmodf(a.x, b.x), fmodf(a.y, b.y)); } inline __host__ __device__ float3 fmodf(float3 a, float3 b) { return make_float3(fmodf(a.x, b.x), fmodf(a.y, b.y), fmodf(a.z, b.z)); } inline __host__ __device__ float4 fmodf(float4 a, float4 b) { return make_float4(fmodf(a.x, b.x), fmodf(a.y, b.y), fmodf(a.z, b.z), fmodf(a.w, b.w)); } //////////////////////////////////////////////////////////////////////////////// // absolute value //////////////////////////////////////////////////////////////////////////////// inline __host__ __device__ float2 fabs(float2 v) { return make_float2(fabs(v.x), fabs(v.y)); } inline __host__ __device__ float3 fabs(float3 v) { return make_float3(fabs(v.x), fabs(v.y), fabs(v.z)); } inline __host__ __device__ float4 fabs(float4 v) { return make_float4(fabs(v.x), fabs(v.y), fabs(v.z), fabs(v.w)); } inline __host__ __device__ int2 abs(int2 v) { return make_int2(abs(v.x), abs(v.y)); } inline __host__ __device__ int3 abs(int3 v) { return make_int3(abs(v.x), abs(v.y), abs(v.z)); } inline __host__ __device__ int4 abs(int4 v) { return make_int4(abs(v.x), abs(v.y), abs(v.z), abs(v.w)); } //////////////////////////////////////////////////////////////////////////////// // reflect // - returns reflection of incident ray I around surface normal N // - N should be normalized, reflected vector's length is equal to length of I //////////////////////////////////////////////////////////////////////////////// inline __host__ __device__ float3 reflect(float3 i, float3 n) { return i - 2.0f * n * dot(n, i); } //////////////////////////////////////////////////////////////////////////////// // cross product //////////////////////////////////////////////////////////////////////////////// inline __host__ __device__ float3 cross(float3 a, float3 b) { 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); } //////////////////////////////////////////////////////////////////////////////// // smoothstep // - returns 0 if x < a // - returns 1 if x > b // - otherwise returns smooth interpolation between 0 and 1 based on x //////////////////////////////////////////////////////////////////////////////// inline __device__ __host__ float smoothstep(float a, float b, float x) { float y = clamp((x - a) / (b - a), 0.0f, 1.0f); return (y * y * (3.0f - (2.0f * y))); } inline __device__ __host__ float2 smoothstep(float2 a, float2 b, float2 x) { float2 y = clamp((x - a) / (b - a), 0.0f, 1.0f); return (y * y * (make_float2(3.0f) - (make_float2(2.0f) * y))); } inline __device__ __host__ float3 smoothstep(float3 a, float3 b, float3 x) { float3 y = clamp((x - a) / (b - a), 0.0f, 1.0f); return (y * y * (make_float3(3.0f) - (make_float3(2.0f) * y))); } inline __device__ __host__ float4 smoothstep(float4 a, float4 b, float4 x) { float4 y = clamp((x - a) / (b - a), 0.0f, 1.0f); return (y * y * (make_float4(3.0f) - (make_float4(2.0f) * y))); } #endif ================================================ FILE: third-party/sudovda/sudovda-ioctl.h ================================================ #pragma once #include #ifdef __cplusplus namespace SUDOVDA { #endif #define IOCTL_ADD_VIRTUAL_DISPLAY CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_REMOVE_VIRTUAL_DISPLAY CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_SET_RENDER_ADAPTER CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_GET_WATCHDOG CTL_CODE(FILE_DEVICE_UNKNOWN, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_DRIVER_PING CTL_CODE(FILE_DEVICE_UNKNOWN, 0x888, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_GET_PROTOCOL_VERSION CTL_CODE(FILE_DEVICE_UNKNOWN, 0x8FF, METHOD_BUFFERED, FILE_ANY_ACCESS) typedef struct _SUVDA_PROTOCAL_VERSION { uint8_t Major; uint8_t Minor; uint8_t Incremental; bool TestBuild; } SUVDA_PROTOCAL_VERSION, * PSUVDA_PROTOCAL_VERSION; // Please update the version after ioctl changed static const SUVDA_PROTOCAL_VERSION VDAProtocolVersion = { 0, 2, 1, true }; static const char* SUVDA_HARDWARE_ID = "root\\sudomaker\\sudovda"; // DO NOT CHANGE // {4d36e968-e325-11ce-bfc1-08002be10318} static const GUID SUVDA_CLASS_GUID = { 0x4d36e968, 0xe325, 0x11ce, { 0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18 } }; // {e5bcc234-1e0c-418a-a0d4-ef8b7501414d} static const GUID SUVDA_INTERFACE_GUID = { 0xe5bcc234, 0x1e0c, 0x418a, { 0xa0, 0xd4, 0xef, 0x8b, 0x75, 0x01, 0x41, 0x4d } }; typedef struct _VIRTUAL_DISPLAY_ADD_PARAMS { UINT Width; UINT Height; UINT RefreshRate; GUID MonitorGuid; CHAR DeviceName[14]; CHAR SerialNumber[14]; } VIRTUAL_DISPLAY_ADD_PARAMS, * PVIRTUAL_DISPLAY_ADD_PARAMS; typedef struct _VIRTUAL_DISPLAY_REMOVE_PARAMS { GUID MonitorGuid; } VIRTUAL_DISPLAY_REMOVE_PARAMS, * PVIRTUAL_DISPLAY_REMOVE_PARAMS; typedef struct _VIRTUAL_DISPLAY_ADD_OUT { LUID AdapterLuid; UINT TargetId; } VIRTUAL_DISPLAY_ADD_OUT, * PVIRTUAL_DISPLAY_ADD_OUT; typedef struct _VIRTUAL_DISPLAY_SET_RENDER_ADAPTER_PARAMS { LUID AdapterLuid; } VIRTUAL_DISPLAY_SET_RENDER_ADAPTER_PARAMS, * PVIRTUAL_DISPLAY_SET_RENDER_ADAPTER_PARAMS; typedef struct _VIRTUAL_DISPLAY_GET_WATCHDOG_OUT { UINT Timeout; UINT Countdown; } VIRTUAL_DISPLAY_GET_WATCHDOG_OUT, * PVIRTUAL_DISPLAY_GET_WATCHDOG_OUT; typedef struct _VIRTUAL_DISPLAY_GET_PROTOCOL_VERSION_OUT { SUVDA_PROTOCAL_VERSION Version; } VIRTUAL_DISPLAY_GET_PROTOCOL_VERSION_OUT, * PVIRTUAL_DISPLAY_GET_PROTOCOL_VERSION_OUT; #ifdef __cplusplus } // namespace SUDOVDA #endif ================================================ FILE: third-party/sudovda/sudovda.h ================================================ #pragma once #include #include #include #include #include #include #include "sudovda-ioctl.h" #ifdef _MSC_VER #pragma comment(lib, "cfgmgr32.lib") #pragma comment(lib, "setupapi.lib") #endif #ifdef __cplusplus namespace SUDOVDA { #endif static const HANDLE OpenDevice(const GUID* interfaceGuid) { // Get the device information set for the specified interface GUID HDEVINFO deviceInfoSet = SetupDiGetClassDevs(interfaceGuid, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); if (deviceInfoSet == INVALID_HANDLE_VALUE) { return INVALID_HANDLE_VALUE; } HANDLE handle = INVALID_HANDLE_VALUE; SP_DEVICE_INTERFACE_DATA deviceInterfaceData; ZeroMemory(&deviceInterfaceData, sizeof(SP_DEVICE_INTERFACE_DATA)); deviceInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA); for (DWORD i = 0; SetupDiEnumDeviceInterfaces(deviceInfoSet, nullptr, interfaceGuid, i, &deviceInterfaceData); ++i) { DWORD detailSize = 0; SetupDiGetDeviceInterfaceDetailA(deviceInfoSet, &deviceInterfaceData, NULL, 0, &detailSize, NULL); SP_DEVICE_INTERFACE_DETAIL_DATA_A *detail = (SP_DEVICE_INTERFACE_DETAIL_DATA_A *)calloc(1, detailSize); detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_A); if (SetupDiGetDeviceInterfaceDetailA(deviceInfoSet, &deviceInterfaceData, detail, detailSize, &detailSize, NULL)) { handle = CreateFileA(detail->DevicePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED | FILE_FLAG_WRITE_THROUGH, NULL); if (handle != NULL && handle != INVALID_HANDLE_VALUE) { break; } } free(detail); } SetupDiDestroyDeviceInfoList(deviceInfoSet); return handle; } static const bool AddVirtualDisplay(HANDLE hDevice, UINT Width, UINT Height, UINT RefreshRate, const GUID& MonitorGuid, const CHAR* DeviceName, const CHAR* SerialNumber, VIRTUAL_DISPLAY_ADD_OUT& output) { VIRTUAL_DISPLAY_ADD_PARAMS params{Width, Height, RefreshRate, MonitorGuid, {}, {}}; strncpy(params.DeviceName, DeviceName, 13); strncpy(params.SerialNumber, SerialNumber, 13); DWORD bytesReturned; BOOL success = DeviceIoControl( hDevice, IOCTL_ADD_VIRTUAL_DISPLAY, (LPVOID)¶ms, sizeof(params), (LPVOID)&output, sizeof(output), &bytesReturned, nullptr ); if (!success) { std::cerr << "[SUVDA] AddVirtualDisplay failed: " << GetLastError() << std::endl; } return success; } static const bool RemoveVirtualDisplay(HANDLE hDevice, const GUID& MonitorGuid) { VIRTUAL_DISPLAY_REMOVE_PARAMS params{MonitorGuid}; DWORD bytesReturned; BOOL success = DeviceIoControl( hDevice, IOCTL_REMOVE_VIRTUAL_DISPLAY, (LPVOID)¶ms, sizeof(params), nullptr, 0, &bytesReturned, nullptr ); if (!success) { std::cerr << "[SUVDA] RemoveVirtualDisplay failed: " << GetLastError() << std::endl; } return success; } static const bool SetRenderAdapter(HANDLE hDevice, const LUID& AdapterLuid) { VIRTUAL_DISPLAY_SET_RENDER_ADAPTER_PARAMS params{AdapterLuid}; DWORD bytesReturned; BOOL success = DeviceIoControl( hDevice, IOCTL_SET_RENDER_ADAPTER, (LPVOID)¶ms, sizeof(params), nullptr, 0, &bytesReturned, nullptr ); if (!success) { std::cerr << "[SUVDA] SetRenderAdapter failed: " << GetLastError() << std::endl; } return success; } static const bool GetWatchdogTimeout(HANDLE hDevice, VIRTUAL_DISPLAY_GET_WATCHDOG_OUT& output) { DWORD bytesReturned; BOOL success = DeviceIoControl( hDevice, IOCTL_GET_WATCHDOG, nullptr, 0, (LPVOID)&output, sizeof(output), &bytesReturned, nullptr ); if (!success) { std::cerr << "[SUVDA] GetWatchdogTimeout failed: " << GetLastError() << std::endl; } return success; } static const bool GetProtocolVersion(HANDLE hDevice, VIRTUAL_DISPLAY_GET_PROTOCOL_VERSION_OUT& output) { DWORD bytesReturned; BOOL success = DeviceIoControl( hDevice, IOCTL_GET_PROTOCOL_VERSION, nullptr, 0, (LPVOID)&output, sizeof(output), &bytesReturned, nullptr ); if (!success) { std::cerr << "[SUVDA] GetProtocolVersion failed: " << GetLastError() << std::endl; } return success; } static const bool isProtocolCompatible(const SUVDA_PROTOCAL_VERSION& otherVersion) { // Changes to existing ioctl must be marked as major if (VDAProtocolVersion.Major != otherVersion.Major) { return false; } // We shouldn't break compatibility with minor/incremental changes // e.g. add new ioctl in the driver // But if our minor version is newer than the driver version, break if (VDAProtocolVersion.Minor > otherVersion.Minor) { return false; } return true; }; static const bool CheckProtocolCompatible(HANDLE hDevice) { VIRTUAL_DISPLAY_GET_PROTOCOL_VERSION_OUT protocolVersion; if (GetProtocolVersion(hDevice, protocolVersion)) { return isProtocolCompatible(protocolVersion.Version); } return false; } static const bool PingDriver(HANDLE hDevice) { DWORD bytesReturned; BOOL success = DeviceIoControl( hDevice, IOCTL_DRIVER_PING, nullptr, 0, nullptr, 0, &bytesReturned, nullptr ); if (!success) { std::cerr << "[SUVDA] PingDriver failed: " << GetLastError() << std::endl; } return success; } static const bool GetAddedDisplayName(const VIRTUAL_DISPLAY_ADD_OUT& addedDisplay, wchar_t* deviceName) { // get all paths UINT pathCount; UINT modeCount; if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &pathCount, &modeCount)) return 0; std::vector paths(pathCount); std::vector modes(modeCount); if (QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &pathCount, paths.data(), &modeCount, modes.data(), nullptr)) return 0; auto path = std::find_if(paths.begin(), paths.end(), [&addedDisplay](DISPLAYCONFIG_PATH_INFO _path) { return _path.targetInfo.id == addedDisplay.TargetId; }); if (path == paths.end()) { return false; } DISPLAYCONFIG_SOURCE_DEVICE_NAME sourceName = {}; sourceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; sourceName.header.size = sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME); sourceName.header.adapterId = addedDisplay.AdapterLuid; sourceName.header.id = path->sourceInfo.id; if (DisplayConfigGetDeviceInfo((DISPLAYCONFIG_DEVICE_INFO_HEADER*)&sourceName)) { return false; } wcscpy_s(deviceName, CCHDEVICENAME, sourceName.viewGdiDeviceName); return true; } #ifdef __cplusplus } // namespace SUDOVDA #endif ================================================ FILE: tools/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.20) project(sunshine_tools) include_directories(${CMAKE_SOURCE_DIR}) add_executable(dxgi-info dxgi.cpp) set_target_properties(dxgi-info PROPERTIES CXX_STANDARD 23) target_link_libraries(dxgi-info ${CMAKE_THREAD_LIBS_INIT} dxgi ${PLATFORM_LIBRARIES}) target_compile_options(dxgi-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS}) add_executable(audio-info audio.cpp utils.cpp) set_target_properties(audio-info PROPERTIES CXX_STANDARD 23) target_link_libraries(audio-info ${Boost_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} ksuser ${PLATFORM_LIBRARIES}) target_compile_options(audio-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS}) add_executable(sunshinesvc sunshinesvc.cpp) set_target_properties(sunshinesvc PROPERTIES CXX_STANDARD 23) target_link_libraries(sunshinesvc ${CMAKE_THREAD_LIBS_INIT} wtsapi32 ${PLATFORM_LIBRARIES}) target_compile_options(sunshinesvc PRIVATE ${SUNSHINE_COMPILE_OPTIONS}) ================================================ FILE: tools/audio.cpp ================================================ /** * @file tools/audio.cpp * @brief Handles collecting audio device information from Windows. */ #define INITGUID #include "src/utility.h" #include "utils.h" // platform includes #include #include #include #include #include #include #include DEFINE_PROPERTYKEY(PKEY_Device_DeviceDesc, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 2); // DEVPROP_TYPE_STRING DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14); // DEVPROP_TYPE_STRING DEFINE_PROPERTYKEY(PKEY_DeviceInterface_FriendlyName, 0x026e516e, 0xb814, 0x414b, 0x83, 0xcd, 0x85, 0x6d, 0x6f, 0xef, 0x48, 0x22, 2); using namespace std::literals; int device_state_filter = DEVICE_STATE_ACTIVE; namespace audio { template void Release(T *p) { p->Release(); } template void co_task_free(T *p) { CoTaskMemFree((LPVOID) p); } using device_enum_t = util::safe_ptr>; using collection_t = util::safe_ptr>; using prop_t = util::safe_ptr>; using device_t = util::safe_ptr>; using audio_client_t = util::safe_ptr>; using audio_capture_t = util::safe_ptr>; using wave_format_t = util::safe_ptr>; using wstring_t = util::safe_ptr>; using handle_t = util::safe_ptr_v2; class prop_var_t { public: prop_var_t() { PropVariantInit(&prop); } ~prop_var_t() { PropVariantClear(&prop); } PROPVARIANT prop; }; const wchar_t *no_null(const wchar_t *str) { return str ? str : L"Unknown"; } struct format_t { std::string_view name; int channels; int channel_mask; } formats[] { {"Mono"sv, 1, SPEAKER_FRONT_CENTER}, {"Stereo"sv, 2, SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT}, {"Quadraphonic"sv, 4, SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT}, {"Surround 5.1 (Side)"sv, 6, SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT}, {"Surround 5.1 (Back)"sv, 6, SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT}, {"Surround 7.1"sv, 8, SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT} }; void set_wave_format(audio::wave_format_t &wave_format, const format_t &format) { wave_format->nChannels = format.channels; wave_format->nBlockAlign = wave_format->nChannels * wave_format->wBitsPerSample / 8; wave_format->nAvgBytesPerSec = wave_format->nSamplesPerSec * wave_format->nBlockAlign; if (wave_format->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { ((PWAVEFORMATEXTENSIBLE) wave_format.get())->dwChannelMask = format.channel_mask; } } audio_client_t make_audio_client(device_t &device, const format_t &format) { audio_client_t audio_client; auto status = device->Activate( IID_IAudioClient, CLSCTX_ALL, nullptr, (void **) &audio_client ); if (FAILED(status)) { std::cout << "Couldn't activate Device: [0x"sv << util::hex(status).to_string_view() << ']' << std::endl; return nullptr; } wave_format_t wave_format; status = audio_client->GetMixFormat(&wave_format); if (FAILED(status)) { std::cout << "Couldn't acquire Wave Format [0x"sv << util::hex(status).to_string_view() << ']' << std::endl; return nullptr; } set_wave_format(wave_format, format); status = audio_client->Initialize( AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK, 0, 0, wave_format.get(), nullptr ); if (status) { return nullptr; } return audio_client; } void print_device(device_t &device) { audio::wstring_t wstring; DWORD device_state; device->GetState(&device_state); device->GetId(&wstring); audio::prop_t prop; device->OpenPropertyStore(STGM_READ, &prop); prop_var_t adapter_friendly_name; prop_var_t device_friendly_name; prop_var_t device_desc; prop->GetValue(PKEY_Device_FriendlyName, &device_friendly_name.prop); prop->GetValue(PKEY_DeviceInterface_FriendlyName, &adapter_friendly_name.prop); prop->GetValue(PKEY_Device_DeviceDesc, &device_desc.prop); if (!(device_state & device_state_filter)) { return; } std::wstring device_state_string = L"Unknown"s; switch (device_state) { case DEVICE_STATE_ACTIVE: device_state_string = L"Active"s; break; case DEVICE_STATE_DISABLED: device_state_string = L"Disabled"s; break; case DEVICE_STATE_UNPLUGGED: device_state_string = L"Unplugged"s; break; case DEVICE_STATE_NOTPRESENT: device_state_string = L"Not present"s; break; } std::wstring current_format = L"Unknown"s; for (const auto &format : formats) { // This will fail for any format that's not the mix format for this device, // so we can take the first match as the current format to display. auto audio_client = make_audio_client(device, format); if (audio_client) { current_format = from_utf8(format.name); break; } } wprintf( L"===== Device =====\n" L"Device ID : %ls\n" L"Device name : %ls\n" L"Adapter name : %ls\n" L"Device description : %ls\n" L"Device state : %ls\n" L"Current format : %ls\n\n", wstring.get(), no_null((LPWSTR)device_friendly_name.prop.pszVal), no_null((LPWSTR)adapter_friendly_name.prop.pszVal), no_null((LPWSTR)device_desc.prop.pszVal), device_state_string.c_str(), current_format.c_str() ); } } // namespace audio void print_help() { std::cout << "==== Help ===="sv << std::endl << "Usage:"sv << std::endl << " audio-info [Active|Disabled|Unplugged|Not-Present]" << std::endl; } int main(int argc, char *argv[]) { CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY); auto fg = util::fail_guard([]() { CoUninitialize(); }); if (argc > 1) { device_state_filter = 0; } // Set the locale to support wide characters std::setlocale(LC_ALL, ""); for (auto x = 1; x < argc; ++x) { for (auto p = argv[x]; *p != '\0'; ++p) { if (*p == ' ') { *p = '-'; continue; } *p = std::tolower(*p); } if (argv[x] == "active"sv) { device_state_filter |= DEVICE_STATE_ACTIVE; } else if (argv[x] == "disabled"sv) { device_state_filter |= DEVICE_STATE_DISABLED; } else if (argv[x] == "unplugged"sv) { device_state_filter |= DEVICE_STATE_UNPLUGGED; } else if (argv[x] == "not-present"sv) { device_state_filter |= DEVICE_STATE_NOTPRESENT; } else { print_help(); return 2; } } HRESULT status; audio::device_enum_t device_enum; status = CoCreateInstance( CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, IID_IMMDeviceEnumerator, (void **) &device_enum ); if (FAILED(status)) { std::cout << "Couldn't create Device Enumerator: [0x"sv << util::hex(status).to_string_view() << ']' << std::endl; return -1; } audio::collection_t collection; status = device_enum->EnumAudioEndpoints(eRender, device_state_filter, &collection); if (FAILED(status)) { std::cout << "Couldn't enumerate: [0x"sv << util::hex(status).to_string_view() << ']' << std::endl; return -1; } UINT count; collection->GetCount(&count); std::cout << "====== Found "sv << count << " audio devices ======"sv << std::endl; for (auto x = 0; x < count; ++x) { audio::device_t device; collection->Item(x, &device); audio::print_device(device); } system("pause"); return 0; } ================================================ FILE: tools/dxgi.cpp ================================================ /** * @file tools/dxgi.cpp * @brief Displays information about connected displays and GPUs */ #define WINVER 0x0A00 #include "src/utility.h" #include #include #include using namespace std::literals; namespace dxgi { template void Release(T *dxgi) { dxgi->Release(); } using factory1_t = util::safe_ptr>; using adapter_t = util::safe_ptr>; using output_t = util::safe_ptr>; } // namespace dxgi int main(int argc, char *argv[]) { HRESULT status; // Set ourselves as per-monitor DPI aware for accurate resolution values on High DPI systems SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); dxgi::factory1_t::pointer factory_p {}; status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory_p); dxgi::factory1_t factory {factory_p}; if (FAILED(status)) { std::cout << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']' << std::endl; return -1; } dxgi::adapter_t::pointer adapter_p {}; for (int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) { dxgi::adapter_t adapter {adapter_p}; DXGI_ADAPTER_DESC1 adapter_desc; adapter->GetDesc1(&adapter_desc); std::cout << "====== ADAPTER ====="sv << std::endl; std::wcout << L"Device Name : "sv << adapter_desc.Description << std::endl; std::cout << "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl << "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl << "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl << "Device Sys Mem : "sv << adapter_desc.DedicatedSystemMemory / 1048576 << " MiB"sv << std::endl << "Share Sys Mem : "sv << adapter_desc.SharedSystemMemory / 1048576 << " MiB"sv << std::endl << std::endl << " ====== OUTPUT ======"sv << std::endl; dxgi::output_t::pointer output_p {}; for (int y = 0; adapter->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) { dxgi::output_t output {output_p}; DXGI_OUTPUT_DESC desc; output->GetDesc(&desc); auto width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left; auto height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top; std::wcout << L" Output Name : "sv << desc.DeviceName << std::endl; std::cout << " AttachedToDesktop : "sv << (desc.AttachedToDesktop ? "yes"sv : "no"sv) << std::endl << " Resolution : "sv << width << 'x' << height << std::endl << std::endl; } } system("pause"); return 0; } ================================================ FILE: tools/sunshinesvc.cpp ================================================ /** * @file tools/sunshinesvc.cpp * @brief Handles launching Sunshine.exe into user sessions as SYSTEM */ #define WIN32_LEAN_AND_MEAN #include #include #include #include // PROC_THREAD_ATTRIBUTE_JOB_LIST is currently missing from MinGW headers #ifndef PROC_THREAD_ATTRIBUTE_JOB_LIST #define PROC_THREAD_ATTRIBUTE_JOB_LIST ProcThreadAttributeValue(13, FALSE, TRUE, FALSE) #endif SERVICE_STATUS_HANDLE service_status_handle; SERVICE_STATUS service_status; HANDLE stop_event; HANDLE session_change_event; #define SERVICE_NAME "ApolloService" DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext) { switch (dwControl) { case SERVICE_CONTROL_INTERROGATE: return NO_ERROR; case SERVICE_CONTROL_SESSIONCHANGE: // If a new session connects to the console, restart Sunshine // to allow it to spawn inside the new console session. if (dwEventType == WTS_CONSOLE_CONNECT) { SetEvent(session_change_event); } return NO_ERROR; case SERVICE_CONTROL_PRESHUTDOWN: // The system is shutting down case SERVICE_CONTROL_STOP: // Let SCM know we're stopping in up to 30 seconds service_status.dwCurrentState = SERVICE_STOP_PENDING; service_status.dwControlsAccepted = 0; service_status.dwWaitHint = 30 * 1000; SetServiceStatus(service_status_handle, &service_status); // Trigger ServiceMain() to start cleanup SetEvent(stop_event); return NO_ERROR; default: return ERROR_CALL_NOT_IMPLEMENTED; } } HANDLE CreateJobObjectForChildProcess() { HANDLE job_handle = CreateJobObjectW(nullptr, nullptr); if (!job_handle) { return nullptr; } JOBOBJECT_EXTENDED_LIMIT_INFORMATION job_limit_info = {}; // Kill Sunshine.exe when the final job object handle is closed (which will happen if we terminate unexpectedly). // This ensures we don't leave an orphaned Sunshine.exe running with an inherited handle to our log file. job_limit_info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; // Allow Sunshine.exe to use CREATE_BREAKAWAY_FROM_JOB when spawning processes to ensure they can to live beyond // the lifetime of SunshineSvc.exe. This avoids unexpected user data loss if we crash or are killed. job_limit_info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_BREAKAWAY_OK; if (!SetInformationJobObject(job_handle, JobObjectExtendedLimitInformation, &job_limit_info, sizeof(job_limit_info))) { CloseHandle(job_handle); return nullptr; } return job_handle; } LPPROC_THREAD_ATTRIBUTE_LIST AllocateProcThreadAttributeList(DWORD attribute_count) { SIZE_T size; InitializeProcThreadAttributeList(nullptr, attribute_count, 0, &size); auto list = (LPPROC_THREAD_ATTRIBUTE_LIST) HeapAlloc(GetProcessHeap(), 0, size); if (list == nullptr) { return nullptr; } if (!InitializeProcThreadAttributeList(list, attribute_count, 0, &size)) { HeapFree(GetProcessHeap(), 0, list); return nullptr; } return list; } HANDLE DuplicateTokenForSession(DWORD console_session_id) { HANDLE current_token; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE, ¤t_token)) { return nullptr; } // Duplicate our own LocalSystem token HANDLE new_token; if (!DuplicateTokenEx(current_token, TOKEN_ALL_ACCESS, nullptr, SecurityImpersonation, TokenPrimary, &new_token)) { CloseHandle(current_token); return nullptr; } CloseHandle(current_token); // Change the duplicated token to the console session ID if (!SetTokenInformation(new_token, TokenSessionId, &console_session_id, sizeof(console_session_id))) { CloseHandle(new_token); return nullptr; } return new_token; } HANDLE OpenLogFileHandle() { WCHAR log_file_name[MAX_PATH]; // Create sunshine.log in the Temp folder (usually %SYSTEMROOT%\Temp) GetTempPathW(_countof(log_file_name), log_file_name); wcscat_s(log_file_name, L"sunshine.log"); // The file handle must be inheritable for our child process to use it SECURITY_ATTRIBUTES security_attributes = {sizeof(security_attributes), nullptr, TRUE}; // Overwrite the old sunshine.log return CreateFileW(log_file_name, GENERIC_WRITE, FILE_SHARE_READ, &security_attributes, CREATE_ALWAYS, 0, nullptr); } bool RunTerminationHelper(HANDLE console_token, DWORD pid) { WCHAR module_path[MAX_PATH]; GetModuleFileNameW(nullptr, module_path, _countof(module_path)); std::wstring command; command += L'"'; command += module_path; command += L'"'; command += std::format(L" --terminate {}", pid); STARTUPINFOW startup_info = {}; startup_info.cb = sizeof(startup_info); startup_info.lpDesktop = (LPWSTR) L"winsta0\\default"; // Execute ourselves as a detached process in the user session with the --terminate argument. // This will allow us to attach to Sunshine's console and send it a Ctrl-C event. PROCESS_INFORMATION process_info; if (!CreateProcessAsUserW(console_token, module_path, (LPWSTR) command.c_str(), nullptr, nullptr, FALSE, CREATE_UNICODE_ENVIRONMENT | DETACHED_PROCESS, nullptr, nullptr, &startup_info, &process_info)) { return false; } // Wait for the termination helper to complete WaitForSingleObject(process_info.hProcess, INFINITE); // Check the exit status of the helper process DWORD exit_code; GetExitCodeProcess(process_info.hProcess, &exit_code); // Cleanup handles CloseHandle(process_info.hProcess); CloseHandle(process_info.hThread); // If the helper process returned 0, it succeeded return exit_code == 0; } VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) { service_status_handle = RegisterServiceCtrlHandlerEx(SERVICE_NAME, HandlerEx, nullptr); if (service_status_handle == nullptr) { // Nothing we can really do here but terminate ourselves ExitProcess(GetLastError()); return; } // Tell SCM we're starting service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS; service_status.dwServiceSpecificExitCode = 0; service_status.dwWin32ExitCode = NO_ERROR; service_status.dwWaitHint = 0; service_status.dwControlsAccepted = 0; service_status.dwCheckPoint = 0; service_status.dwCurrentState = SERVICE_START_PENDING; SetServiceStatus(service_status_handle, &service_status); // Create a manual-reset stop event stop_event = CreateEventA(nullptr, TRUE, FALSE, nullptr); if (stop_event == nullptr) { // Tell SCM we failed to start service_status.dwWin32ExitCode = GetLastError(); service_status.dwCurrentState = SERVICE_STOPPED; SetServiceStatus(service_status_handle, &service_status); return; } // Create an auto-reset session change event session_change_event = CreateEventA(nullptr, FALSE, FALSE, nullptr); if (session_change_event == nullptr) { // Tell SCM we failed to start service_status.dwWin32ExitCode = GetLastError(); service_status.dwCurrentState = SERVICE_STOPPED; SetServiceStatus(service_status_handle, &service_status); return; } auto log_file_handle = OpenLogFileHandle(); if (log_file_handle == INVALID_HANDLE_VALUE) { // Tell SCM we failed to start service_status.dwWin32ExitCode = GetLastError(); service_status.dwCurrentState = SERVICE_STOPPED; SetServiceStatus(service_status_handle, &service_status); return; } // We can use a single STARTUPINFOEXW for all the processes that we launch STARTUPINFOEXW startup_info = {}; startup_info.StartupInfo.cb = sizeof(startup_info); startup_info.StartupInfo.lpDesktop = (LPWSTR) L"winsta0\\default"; startup_info.StartupInfo.dwFlags = STARTF_USESTDHANDLES; startup_info.StartupInfo.hStdInput = nullptr; startup_info.StartupInfo.hStdOutput = log_file_handle; startup_info.StartupInfo.hStdError = log_file_handle; // Allocate an attribute list with space for 2 entries startup_info.lpAttributeList = AllocateProcThreadAttributeList(2); if (startup_info.lpAttributeList == nullptr) { // Tell SCM we failed to start service_status.dwWin32ExitCode = GetLastError(); service_status.dwCurrentState = SERVICE_STOPPED; SetServiceStatus(service_status_handle, &service_status); return; } // Only allow Sunshine.exe to inherit the log file handle, not all inheritable handles UpdateProcThreadAttribute(startup_info.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &log_file_handle, sizeof(log_file_handle), nullptr, nullptr); // Tell SCM we're running (and stoppable now) service_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PRESHUTDOWN | SERVICE_ACCEPT_SESSIONCHANGE; service_status.dwCurrentState = SERVICE_RUNNING; SetServiceStatus(service_status_handle, &service_status); // Loop every 3 seconds until the stop event is set or Sunshine.exe is running while (WaitForSingleObject(stop_event, 3000) != WAIT_OBJECT_0) { auto console_session_id = WTSGetActiveConsoleSessionId(); if (console_session_id == 0xFFFFFFFF) { // No console session yet continue; } auto console_token = DuplicateTokenForSession(console_session_id); if (console_token == nullptr) { continue; } // Job objects cannot span sessions, so we must create one for each process auto job_handle = CreateJobObjectForChildProcess(); if (job_handle == nullptr) { CloseHandle(console_token); continue; } // Start Sunshine.exe inside our job object UpdateProcThreadAttribute(startup_info.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_JOB_LIST, &job_handle, sizeof(job_handle), nullptr, nullptr); PROCESS_INFORMATION process_info; if (!CreateProcessAsUserW(console_token, L"Sunshine.exe", nullptr, nullptr, nullptr, TRUE, CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr, (LPSTARTUPINFOW) &startup_info, &process_info)) { CloseHandle(console_token); CloseHandle(job_handle); continue; } bool still_running; do { // Wait for the stop event to be set, Sunshine.exe to terminate, or the console session to change const HANDLE wait_objects[] = {stop_event, process_info.hProcess, session_change_event}; switch (WaitForMultipleObjects(_countof(wait_objects), wait_objects, FALSE, INFINITE)) { case WAIT_OBJECT_0 + 2: if (WTSGetActiveConsoleSessionId() == console_session_id) { // The active console session didn't actually change. Let Sunshine keep running. still_running = true; continue; } // Fall-through to terminate Sunshine.exe and start it again. case WAIT_OBJECT_0: // The service is shutting down, so try to gracefully terminate Sunshine.exe. // If it doesn't terminate in 20 seconds, we will forcefully terminate it. if (!RunTerminationHelper(console_token, process_info.dwProcessId) || WaitForSingleObject(process_info.hProcess, 20000) != WAIT_OBJECT_0) { // If it won't terminate gracefully, kill it now TerminateProcess(process_info.hProcess, ERROR_PROCESS_ABORTED); } still_running = false; break; case WAIT_OBJECT_0 + 1: { // Sunshine terminated itself. DWORD exit_code; if (GetExitCodeProcess(process_info.hProcess, &exit_code) && exit_code == ERROR_SHUTDOWN_IN_PROGRESS) { // Sunshine is asking for us to shut down, so gracefully stop ourselves. SetEvent(stop_event); } still_running = false; break; } } } while (still_running); CloseHandle(process_info.hThread); CloseHandle(process_info.hProcess); CloseHandle(console_token); CloseHandle(job_handle); } // Let SCM know we've stopped service_status.dwCurrentState = SERVICE_STOPPED; SetServiceStatus(service_status_handle, &service_status); } // This will run in a child process in the user session int DoGracefulTermination(DWORD pid) { // Attach to Sunshine's console if (!AttachConsole(pid)) { return GetLastError(); } // Disable our own Ctrl-C handling SetConsoleCtrlHandler(nullptr, TRUE); // Send a Ctrl-C event to Sunshine if (!GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0)) { return GetLastError(); } return 0; } int main(int argc, char *argv[]) { static const SERVICE_TABLE_ENTRY service_table[] = { {(LPSTR) SERVICE_NAME, ServiceMain}, {nullptr, nullptr} }; // Check if this is a reinvocation of ourselves to send Ctrl-C to Sunshine.exe if (argc == 3 && strcmp(argv[1], "--terminate") == 0) { return DoGracefulTermination(atol(argv[2])); } // By default, services have their current directory set to %SYSTEMROOT%\System32. // We want to use the directory where Sunshine.exe is located instead of system32. // This requires stripping off 2 path components: the file name and the last folder WCHAR module_path[MAX_PATH]; GetModuleFileNameW(nullptr, module_path, _countof(module_path)); for (auto i = 0; i < 2; i++) { auto last_sep = wcsrchr(module_path, '\\'); if (last_sep) { *last_sep = 0; } } SetCurrentDirectoryW(module_path); // Trigger our ServiceMain() return StartServiceCtrlDispatcher(service_table); } ================================================ FILE: tools/utils.cpp ================================================ #include "utils.h" #include std::wstring from_utf8(const std::string_view &string) { // No conversion needed if the string is empty if (string.empty()) { return {}; } // Get the output size required to store the string auto output_size = MultiByteToWideChar(CP_UTF8, 0, string.data(), string.size(), nullptr, 0); if (output_size == 0) { // auto winerr = GetLastError(); // BOOST_LOG(error) << "Failed to get UTF-16 buffer size: "sv << winerr; return {}; } // Perform the conversion std::wstring output(output_size, L'\0'); output_size = MultiByteToWideChar(CP_UTF8, 0, string.data(), string.size(), output.data(), output.size()); if (output_size == 0) { // auto winerr = GetLastError(); // BOOST_LOG(error) << "Failed to convert string to UTF-16: "sv << winerr; return {}; } return output; } std::string to_utf8(const std::wstring_view &string) { // No conversion needed if the string is empty if (string.empty()) { return {}; } // Get the output size required to store the string auto output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), nullptr, 0, nullptr, nullptr); if (output_size == 0) { // auto winerr = GetLastError(); // BOOST_LOG(error) << "Failed to get UTF-8 buffer size: "sv << winerr; return {}; } // Perform the conversion std::string output(output_size, '\0'); output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), output.data(), output.size(), nullptr, nullptr); if (output_size == 0) { // auto winerr = GetLastError(); // BOOST_LOG(error) << "Failed to convert string to UTF-8: "sv << winerr; return {}; } return output; } ================================================ FILE: tools/utils.h ================================================ #include /** * @brief Convert a UTF-8 string into a UTF-16 wide string. * @param string The UTF-8 string. * @return The converted UTF-16 wide string. */ std::wstring from_utf8(const std::string_view &string); /** * @brief Convert a UTF-16 wide string into a UTF-8 string. * @param string The UTF-16 wide string. * @return The converted UTF-8 string. */ std::string to_utf8(const std::wstring_view &string); ================================================ FILE: vite.config.js ================================================ import { fileURLToPath, URL } from 'node:url' import fs from 'fs'; import { resolve } from 'path' import { defineConfig } from 'vite' import { ViteEjsPlugin } from "vite-plugin-ejs"; import { codecovVitePlugin } from "@codecov/vite-plugin"; import vue from '@vitejs/plugin-vue' import process from 'process' /** * Before actually building the pages with Vite, we do an intermediate build step using ejs * Importing this separately and joining them using ejs * allows us to split some repeating HTML that cannot be added * by Vue itself (e.g. style/script loading, common meta head tags, Widgetbot) * The vite-plugin-ejs handles this automatically */ let assetsSrcPath = 'src_assets/common/assets/web'; let assetsDstPath = 'build/assets/web'; if (process.env.SUNSHINE_BUILD_HOMEBREW) { console.log("Building for homebrew, using default paths") } else { // If the paths supplied in the environment variables contain any symbolic links // at any point in the series of directories, the entire build will fail with // a cryptic error message like this: // RollupError: The "fileName" or "name" properties of emitted chunks and assets // must be strings that are neither absolute nor relative paths. // To avoid this, we resolve the potential symlinks using `fs.realpathSync` before // doing anything else with the paths. if (process.env.SUNSHINE_SOURCE_ASSETS_DIR) { let path = resolve(fs.realpathSync(process.env.SUNSHINE_SOURCE_ASSETS_DIR), "common/assets/web"); console.log("Using srcdir from Cmake: " + path); assetsSrcPath = path; } if (process.env.SUNSHINE_ASSETS_DIR) { let path = resolve(fs.realpathSync(process.env.SUNSHINE_ASSETS_DIR), "assets/web"); console.log("Using destdir from Cmake: " + path); assetsDstPath = path; } } let header = fs.readFileSync(resolve(assetsSrcPath, "template_header.html")) // https://vitejs.dev/config/ export default defineConfig({ resolve: { alias: { vue: 'vue/dist/vue.esm-bundler.js' } }, base: './', plugins: [ vue(), ViteEjsPlugin({ header }), // The Codecov vite plugin should be after all other plugins codecovVitePlugin({ enableBundleAnalysis: process.env.CODECOV_TOKEN !== undefined, bundleName: "sunshine", uploadToken: process.env.CODECOV_TOKEN, }), ], root: resolve(assetsSrcPath), build: { outDir: resolve(assetsDstPath), emptyOutDir: true, rollupOptions: { input: { apps: resolve(assetsSrcPath, 'apps.html'), config: resolve(assetsSrcPath, 'config.html'), index: resolve(assetsSrcPath, 'index.html'), password: resolve(assetsSrcPath, 'password.html'), pin: resolve(assetsSrcPath, 'pin.html'), troubleshooting: resolve(assetsSrcPath, 'troubleshooting.html'), welcome: resolve(assetsSrcPath, 'welcome.html'), login: resolve(assetsSrcPath, 'login.html') }, }, }, })