diff --git a/.github/workflows/ci-flatpak.yml b/.github/workflows/ci-flatpak.yml index 47d9ac75cc3..fc0a4f6bab0 100644 --- a/.github/workflows/ci-flatpak.yml +++ b/.github/workflows/ci-flatpak.yml @@ -145,11 +145,25 @@ jobs: --no-header \ --output-file glad-requirements.txt + # Install all exported Python dependencies under /app without modifying the SDK. + ignore_installed="$( + uv run --locked --no-build --no-sync python - <<'PY' + from pathlib import Path + + import requirements + + with Path("glad-requirements.txt").open(encoding="utf-8") as requirements_file: + packages = requirements.parse(requirements_file) + print(",".join(sorted({package.name.casefold() for package in packages if package.name}))) + PY + )" + uv run --locked --no-build --no-sync python \ ./packaging/linux/flatpak/deps/flatpak-builder-tools/pip/flatpak-pip-generator.py \ --runtime="org.kde.Sdk//${KDE_PLATFORM_VERSION}" \ --output glad-dependencies \ --build-only \ + --ignore-installed="${ignore_installed}" \ --requirements-file=glad-requirements.txt # Copy generated pip sources into build dir alongside the manifest diff --git a/AGENTS.md b/AGENTS.md index 018b8d23416..6597c9f744d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,12 @@ the build directory. The project uses gtest as a test framework. -Add or update tests for new or modified methods and code. Target 100% coverage on changed code. +Add or update regression tests for new or modified source-code methods and behavior. +Target 100% coverage on changed source code. + +The regression-test requirement does not apply to build-system files, such as CMake scripts and dependency configuration. +Validate build-system changes with appropriate configuration, build, and lint checks. +Do not add dedicated build-system regression-test projects or fixtures for these changes. ## Python tooling and formatting diff --git a/PATCHES.md b/PATCHES.md index 7f7c4dad3df..1c6062b6250 100644 --- a/PATCHES.md +++ b/PATCHES.md @@ -8,7 +8,7 @@ how to carry it to a newer Sunshine. The user-facing guide is | | | |---|---| -| Upstream base | `0594f62d` — *chore: update global workflows (#5868)*, tag `v2026.1006.152353` | +| Upstream base | `5b2a4cfd` — *chore(deps): update dependency @lucide/vue to v1.58.0 (#5945)*, after tag `v2026.1015.174736` | | Fork branch | `master` (development on `webrtc-tizen`) | | WebRTC library | libdatachannel v0.24.6, built from source when no system package exists | | Footprint in upstream files | 17 files, +441 / −14 lines; the rest is new files | @@ -80,7 +80,7 @@ only when no Moonlight session and no other TV stream is running, and calls | File | Change | |---|---| -| `cmake/dependencies/libdatachannel.cmake` (new) | Uses a system `LibDataChannel` ≥ 0.24 when available, otherwise `FetchContent` of v0.24.6 with its `plog`, `usrsctp`, `libjuice` and `libsrtp` submodules, built static, with OpenSSL (Sunshine's), media and WebSocket support, and no examples or tests. libSRTP's `ENABLE_WARNINGS_AS_ERRORS` is turned off because MinGW printf-format warnings break it, and Sunshine's own nlohmann_json is reused. | +| `cmake/dependencies/libdatachannel.cmake` (new) | Uses a system `LibDataChannel` ≥ 0.24 when available, otherwise `FetchContent` of v0.24.6 with its `plog`, `usrsctp`, `libjuice` and `libsrtp` submodules, built static, with OpenSSL (Sunshine's), media and WebSocket support, and no examples or tests. libSRTP's `ENABLE_WARNINGS_AS_ERRORS` is turned off because MinGW printf-format warnings break it, and Sunshine's own nlohmann_json is reused. Its `src/configuration.cpp` is compiled with `-include algorithm`: v0.24.6 uses `std::transform` without including ``, which libc++ (Windows ARM64) does not include transitively; drop this once a newer release is used. | | `cmake/dependencies/common.cmake` | Includes it after nlohmann_json. | | `cmake/compile_definitions/common.cmake` | Adds `src/webrtc/*` to `SUNSHINE_TARGET_FILES` and `${SUNSHINE_LIBDATACHANNEL_TARGET}` to `SUNSHINE_EXTERNAL_LIBRARIES`. | diff --git a/cmake/FindLibva.cmake b/cmake/FindLibva.cmake index f8dfddc1496..370ea8cc83e 100644 --- a/cmake/FindLibva.cmake +++ b/cmake/FindLibva.cmake @@ -10,29 +10,47 @@ # 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() +if(EXISTS "${FFMPEG_PREPARED_BINARIES}/lib/libva.a") + # The display context ABI is private to libva. Keep its core, DRM backend, + # and headers from the same build as FFmpeg instead of mixing system libva. + set(LIBVA_INCLUDE_DIR "${FFMPEG_PREPARED_BINARIES}/include") + set(LIBVA_LIB "${FFMPEG_PREPARED_BINARIES}/lib/libva.a") + set(LIBVA_DRM_LIB "${FFMPEG_PREPARED_BINARIES}/lib/libva-drm.a") + foreach(required_file IN ITEMS + "${LIBVA_INCLUDE_DIR}/va/va.h" + "${LIBVA_INCLUDE_DIR}/va/va_drm.h" + "${LIBVA_DRM_LIB}") + if(NOT EXISTS "${required_file}") + message(FATAL_ERROR "The bundled libva package is incomplete: missing ${required_file}") + endif() + endforeach() + # libva-drm.pc does not declare the libdrm dependency of its static archive. + find_package(LIBDRM REQUIRED) +else() + 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_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_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) + 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) +endif() include(FindPackageHandleStandardArgs) find_package_handle_standard_args(Libva REQUIRED_VARS LIBVA_INCLUDE_DIR LIBVA_LIB LIBVA_DRM_LIB) @@ -65,6 +83,13 @@ if(LIBVA_FOUND) endif() set_target_properties(Libva::drm PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${LIBVA_INCLUDE_DIRS}") + if(EXISTS "${FFMPEG_PREPARED_BINARIES}/lib/libva.a") + set_target_properties(Libva::drm PROPERTIES + INTERFACE_LINK_LIBRARIES "Libva::va;${LIBDRM_LIBRARIES}") + endif() + endif() + if(EXISTS "${FFMPEG_PREPARED_BINARIES}/lib/libva.a") + set(LIBVA_DRM_LIBRARIES Libva::drm) endif() endif() diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake index 485e419adb5..d8fefc0fbce 100644 --- a/cmake/compile_definitions/linux.cmake +++ b/cmake/compile_definitions/linux.cmake @@ -80,8 +80,10 @@ if(CUDA_FOUND) add_compile_definitions(SUNSHINE_BUILD_CUDA) endif() -# libdrm is required for DRM (KMS). Only the headers are required for Wayland, -# Vulkan, and PipeWire (KWin, Portal). +# libdrm headers are required for DRM (KMS), Wayland, Vulkan, and PipeWire +# (KWin, Portal). The library itself is linked for DRM (KMS) and for Wayland, +# where capture queries the render node's PCI identity to choose between +# zero-copy VRAM and the RAM bridge. if(${SUNSHINE_ENABLE_DRM} OR ${SUNSHINE_ENABLE_WAYLAND} OR ${SUNSHINE_ENABLE_VULKAN} OR ${SUNSHINE_ENABLE_KWIN} OR ${SUNSHINE_ENABLE_PORTAL}) find_package(LIBDRM REQUIRED) @@ -90,8 +92,10 @@ else() endif() if(LIBDRM_FOUND) include_directories(SYSTEM ${LIBDRM_INCLUDE_DIRS}) - if(${SUNSHINE_ENABLE_DRM}) + if(${SUNSHINE_ENABLE_DRM} OR ${SUNSHINE_ENABLE_WAYLAND}) list(APPEND PLATFORM_LIBRARIES ${LIBDRM_LIBRARIES}) + endif() + if(${SUNSHINE_ENABLE_DRM}) add_compile_definitions(SUNSHINE_BUILD_DRM) list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/linux/kmsgrab.cpp") diff --git a/cmake/dependencies/ffmpeg.cmake b/cmake/dependencies/ffmpeg.cmake index 79e0e2915e6..be85f5a004b 100644 --- a/cmake/dependencies/ffmpeg.cmake +++ b/cmake/dependencies/ffmpeg.cmake @@ -176,6 +176,15 @@ else() list(APPEND FFMPEG_LIBRARIES ${FFMPEG_PLATFORM_LIBRARIES}) endif() +# Legacy library lists can also refer to libva backends by name. Resolve those +# names to the matching archives whenever the FFmpeg package supplies libva. +if(EXISTS "${FFMPEG_PREPARED_BINARIES}/lib/libva.a") + foreach(library IN ITEMS va va-drm va-x11) + list(TRANSFORM FFMPEG_LIBRARIES REPLACE + "^${library}$" "${FFMPEG_PREPARED_BINARIES}/lib/lib${library}.a") + endforeach() +endif() + # Shared packages supply the full static dependency list, including decoders. # Older packages retain the library list above. set(_sunshine_ffmpeg_pc "${FFMPEG_PREPARED_BINARIES}/lib/pkgconfig/libavcodec.pc") diff --git a/cmake/dependencies/libdatachannel.cmake b/cmake/dependencies/libdatachannel.cmake index ac991667c58..2f9b13fd7ac 100644 --- a/cmake/dependencies/libdatachannel.cmake +++ b/cmake/dependencies/libdatachannel.cmake @@ -50,6 +50,12 @@ FetchContent_MakeAvailable(libdatachannel) set(BUILD_SHARED_LIBS "${_sunshine_build_shared_libs}") unset(_sunshine_build_shared_libs) +# v0.24.6 uses std::transform in configuration.cpp without including . libstdc++ includes it +# transitively, libc++ (Windows ARM64 with clang) does not. Fixed upstream after v0.24.6. +set_source_files_properties("${libdatachannel_SOURCE_DIR}/src/configuration.cpp" + TARGET_DIRECTORY datachannel-static + PROPERTIES COMPILE_OPTIONS "-include;algorithm") + set(SUNSHINE_LIBDATACHANNEL_TARGET LibDataChannel::LibDataChannelStatic) if(NOT TARGET ${SUNSHINE_LIBDATACHANNEL_TARGET}) set(SUNSHINE_LIBDATACHANNEL_TARGET datachannel-static) diff --git a/docs/building.md b/docs/building.md index 726ba52a5dd..7d1a32859b3 100644 --- a/docs/building.md +++ b/docs/building.md @@ -22,6 +22,10 @@ Sunshine's build and packaging automation uses packaged static Boost libraries o Ubuntu 26.04 or newer, macOS with Homebrew, Windows with MSYS2, and FreeBSD. Older supported distributions continue to use the CPM fallback because their repositories do not meet the minimum Boost version. +When the prebuilt FFmpeg package supplies libva, Sunshine uses its bundled headers, core library, and DRM backend +together. These components share a private display-context ABI and must come from the same libva build. The system +still provides libdrm and the GPU's VA-API driver. FFmpeg packages without bundled libva use the system libva libraries. + #### Prebuilt Web UI Sunshine releases publish the platform-independent Web UI as the `@lizardbyte/sunshine` package to npm and GitHub diff --git a/docs/configuration.md b/docs/configuration.md index 8b73ecdb53f..55920e78298 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -2397,7 +2397,13 @@ consent prompt for directory ACLs, so the Web UI shows setup steps for correctin kwin - Capture with KDE/KWin Wayland compositor via KDE screencasting. + Capture with KDE/KWin Wayland compositor directly via KDE screencasting. + @note{Applies to Linux only.} + @warning{Flatpak users currently shouldn't use this capture method and use `portal` instead. KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 is necessary to make this capture method work with Flatpak but that has security implications.} + + + portal + Capture via XDG Desktop Portal. @note{Applies to Linux only.} diff --git a/package-lock.json b/package-lock.json index 28dc447fd2f..79284894bfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,23 +9,24 @@ "version": "0.0.0", "license": "GPL-3.0-only", "dependencies": { - "@lucide/vue": "1.55.0", + "@intlify/message-compiler": "11.4.14", + "@lucide/vue": "1.58.0", "bootstrap": "5.3.8", "date-fns": "4.4.0", "marked": "18.0.12", - "vue": "3.5.43", + "vue": "3.5.44", "vue-i18n": "11.4.14", "vue-router": "5.4.0", "vue3-simple-icons": "16.10.0" }, "devDependencies": { "@codecov/vite-plugin": "2.0.1", - "@vitejs/plugin-vue": "6.0.9", + "@vitejs/plugin-vue": "6.0.10", "@vitest/coverage-v8": "5.0.3", "@vue/test-utils": "2.5.1", "jsdom": "30.1.2", "serve": "14.2.6", - "vite": "8.3.3", + "vite": "8.3.4", "vitest": "5.0.3" }, "funding": { @@ -99,14 +100,14 @@ "license": "MIT" }, "node_modules/@asamuzakjp/css-color": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.1.3.tgz", - "integrity": "sha512-1t1U8Cm3RBl9vh3RAZhjkPaduzLSvQJRToRrxpGR/eDRW99mOREleuu+mWfiFa795bUzjwht7NgphCKTs2ianQ==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.1.5.tgz", + "integrity": "sha512-jCpAvj8apa7SKHZYogk0rbXB3Kb0hLFrpkWF+Ap+vBA5rirItnpv9NKyAyEuDuFizvl0qwnj6JWR70RGJ0rOlw==", "dev": true, "license": "MIT", "dependencies": { - "@csstools/css-calc": "^3.4.2", - "@csstools/css-color-parser": "^4.2.5", + "@csstools/css-calc": "^3.4.3", + "@csstools/css-color-parser": "^4.2.6", "@csstools/css-parser-algorithms": "^4.0.2", "@csstools/css-tokenizer": "^4.0.2", "lru-cache": "^11.5.3" @@ -188,13 +189,13 @@ } }, "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.3.tgz", + "integrity": "sha512-0GEz8qJWokkUVYQYRukmkSHIFNoBH8QTreeHrAFbvz1XFYc3Y+puhMs8PQ5x8GNugiC2l5jvDBXKLbBIByeSBw==", "dev": true, "license": "MIT", "dependencies": { - "css-tree": "^3.0.0" + "css-tree": "^3.2.1" }, "bin": { "specificity": "bin/cli.js" @@ -500,9 +501,9 @@ } }, "node_modules/@lucide/vue": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/@lucide/vue/-/vue-1.55.0.tgz", - "integrity": "sha512-3lpuTiVGIMbG7vEVhcDnHvIu+ZkjXka7eOp9tIA9iDPu6GHpfqwykVyAb+LM1uYYVgSI9Myo4HGzTvVnTS2CyA==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@lucide/vue/-/vue-1.58.0.tgz", + "integrity": "sha512-HW7yrGw9OUC1TzldLep1d4p1Uc1OCRWLJwzKHm9C3V9Z4/9GADN84NDlyeTzgrcD8yxXs8/SVh8CpKFaA+OPRg==", "license": "ISC", "peerDependencies": { "vue": ">=3.0.1" @@ -688,9 +689,9 @@ "license": "MIT" }, "node_modules/@oxc-project/types": { - "version": "0.151.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", - "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.153.0.tgz", + "integrity": "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==", "devOptional": true, "license": "MIT", "funding": { @@ -709,9 +710,9 @@ } }, "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", - "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.13.tgz", + "integrity": "sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==", "cpu": [ "arm" ], @@ -725,9 +726,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", - "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.13.tgz", + "integrity": "sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==", "cpu": [ "arm64" ], @@ -741,9 +742,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", - "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.13.tgz", + "integrity": "sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==", "cpu": [ "arm64" ], @@ -757,9 +758,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", - "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.13.tgz", + "integrity": "sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==", "cpu": [ "x64" ], @@ -773,9 +774,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", - "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.13.tgz", + "integrity": "sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==", "cpu": [ "x64" ], @@ -789,9 +790,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", - "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.13.tgz", + "integrity": "sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==", "cpu": [ "arm" ], @@ -805,9 +806,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", - "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.13.tgz", + "integrity": "sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==", "cpu": [ "arm64" ], @@ -824,9 +825,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", - "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.13.tgz", + "integrity": "sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==", "cpu": [ "arm64" ], @@ -843,9 +844,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", - "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.13.tgz", + "integrity": "sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==", "cpu": [ "ppc64" ], @@ -862,9 +863,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", - "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.13.tgz", + "integrity": "sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==", "cpu": [ "s390x" ], @@ -881,9 +882,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", - "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.13.tgz", + "integrity": "sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==", "cpu": [ "x64" ], @@ -900,9 +901,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", - "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.13.tgz", + "integrity": "sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==", "cpu": [ "x64" ], @@ -919,9 +920,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", - "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.13.tgz", + "integrity": "sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==", "cpu": [ "arm64" ], @@ -935,9 +936,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", - "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.13.tgz", + "integrity": "sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==", "cpu": [ "arm64" ], @@ -951,9 +952,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", - "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.13.tgz", + "integrity": "sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==", "cpu": [ "x64" ], @@ -998,9 +999,9 @@ "license": "MIT" }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.9", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.9.tgz", - "integrity": "sha512-rD/MORlhaZMlXWW0rEn4FB4wMinWC0z/D7Ye160S5+Rs1mC8ZDAdcDp4SjKfUhG5t8q1ktcPVw4xuTkPRlzrSA==", + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.10.tgz", + "integrity": "sha512-b80aUQh3n+qEzzZm5oQp3rT2QImeCx58K6eFl/RkJ3d65KW0kTuv5lNsLRVAjbW9YRUpo/wzmvzMgV34bTAItA==", "dev": true, "license": "MIT", "dependencies": { @@ -1011,7 +1012,7 @@ }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "vue": "^3.2.25" + "vue": "^3.2.25 || ^3.6.0-rc.10" } }, "node_modules/@vitest/coverage-v8": { @@ -1142,43 +1143,43 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.43.tgz", - "integrity": "sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.44.tgz", + "integrity": "sha512-k34nCytbK8PQ1gR2mGROYOw6Pnv/mumqIdrZ3x2tbyXZDWm7ViKGLTfdB+DRrWqEQsLgGJvn0WN9jBif5zCTLQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.8", - "@vue/shared": "3.5.43", + "@babel/parser": "^7.29.9", + "@vue/shared": "3.5.44", "entities": "^7.0.1", "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.43.tgz", - "integrity": "sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.44.tgz", + "integrity": "sha512-W5AoBFWdw1lQAKiHowkfgFKFab82oSRT6sGsPfw/q1GRjsy/ZJFLHGhJyBRIFN9APLTUduDH9U2JAiPOIyKuYg==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.43", - "@vue/shared": "3.5.43" + "@vue/compiler-core": "3.5.44", + "@vue/shared": "3.5.44" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.43.tgz", - "integrity": "sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.44.tgz", + "integrity": "sha512-dzHmQ4jXTpdr5FjIQUeykVakN/uzqFbUo9/C5ikBFg3a3PeM6uRxeczx1ZbXBML0/zJstEh3ygYipzR5Z7b2Pw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.8", - "@vue/compiler-core": "3.5.43", - "@vue/compiler-dom": "3.5.43", - "@vue/compiler-ssr": "3.5.43", - "@vue/shared": "3.5.43", + "@babel/parser": "^7.29.9", + "@vue/compiler-core": "3.5.44", + "@vue/compiler-dom": "3.5.44", + "@vue/compiler-ssr": "3.5.44", + "@vue/shared": "3.5.44", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.28", - "source-map-js": "^1.2.1" + "postcss": "^8.5.29", + "source-map-js": "^1.2.2" } }, "node_modules/@vue/compiler-sfc/node_modules/magic-string": { @@ -1191,13 +1192,13 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.43.tgz", - "integrity": "sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.44.tgz", + "integrity": "sha512-xr5gb1JVHlfXJwSlRz1FeP6uxoazdzHuMTVLE7up+4Vrqxk219CvxLPxD9NXBt47yvZW7+p4DXfyp4O1fkByIA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.43", - "@vue/shared": "3.5.43" + "@vue/compiler-dom": "3.5.44", + "@vue/shared": "3.5.44" } }, "node_modules/@vue/devtools-api": { @@ -1225,51 +1226,51 @@ "license": "MIT" }, "node_modules/@vue/reactivity": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.43.tgz", - "integrity": "sha512-G/c9GyOZNI2jVaaS6OX1EF1SSFSv7H0ERqNTl4+DTFMlZmB5eVAB53aLQNam/7NL2NPtaDD7RdVrzf8uJzMuOA==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.44.tgz", + "integrity": "sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.43" + "@vue/shared": "3.5.44" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.43.tgz", - "integrity": "sha512-hU6U6VnVhBGQDpvlnnDlIB8ZGJBiOcgk2lh/0InltHiz3D8oSkluvuvY+do1G2H3+udeKFsmaBlgVYP7gXQzEw==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.44.tgz", + "integrity": "sha512-uEa/ndwjhrrhnOfb2p+sZy6zeoamJOISqaLSJGk/8R6nABT1ZldpzcJoPzKj+zHVomVBPcmQYyC2r4LtvDV6CQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.43", - "@vue/shared": "3.5.43" + "@vue/reactivity": "3.5.44", + "@vue/shared": "3.5.44" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.43.tgz", - "integrity": "sha512-Bb2Jc0YjjJdMt1SJmb9b2L/IWd3I8lIT9x9eS/xvvP9CiVgna0ffua74xKRmt4/uSJ+0r4iN8ex1jrqkhQGWQw==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.44.tgz", + "integrity": "sha512-3fc4ARcXVhVBaJ3ujWiNZJARD6pkyRcwG+ZYhMzjKtKUp7MAY0HLwr7r3Smep3Li6ZaWT6ewdmx8XqdaD5oNZg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.43", - "@vue/runtime-core": "3.5.43", - "@vue/shared": "3.5.43", + "@vue/reactivity": "3.5.44", + "@vue/runtime-core": "3.5.44", + "@vue/shared": "3.5.44", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.43.tgz", - "integrity": "sha512-l2Ygjv9NehV94PSBxNWsAHC0j/eIIKbn92mBuWXAPGLnn6HfJ6MH5ubsd+Nk0YoZ5FRuxWI1P2VSoh+dbPQhCQ==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.44.tgz", + "integrity": "sha512-R4S1UoH4h5Frrauv8Z5XE5TxV9oBiUSgWIC4TyVK1FK+RocRnOLwFA5Xcmo2011RlgJp+AbZCnw6afrhlgRuUQ==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.43", - "@vue/runtime-dom": "3.5.43", - "@vue/shared": "3.5.43" + "@vue/compiler-ssr": "3.5.44", + "@vue/runtime-dom": "3.5.44", + "@vue/shared": "3.5.44" } }, "node_modules/@vue/shared": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.43.tgz", - "integrity": "sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.44.tgz", + "integrity": "sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg==", "license": "MIT" }, "node_modules/@vue/test-utils": { @@ -1311,9 +1312,9 @@ } }, "node_modules/acorn": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", - "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1716,9 +1717,9 @@ } }, "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", "dev": true, "license": "MIT", "engines": { @@ -2103,9 +2104,9 @@ } }, "node_modules/exsolve": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", - "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.3.tgz", + "integrity": "sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA==", "license": "MIT" }, "node_modules/fast-deep-equal": { @@ -2901,9 +2902,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "funding": [ { "type": "github", @@ -3112,9 +3113,9 @@ } }, "node_modules/postcss": { - "version": "8.5.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", - "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", "funding": [ { "type": "opencollective", @@ -3131,9 +3132,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.18", + "nanoid": "^3.3.19", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -3246,13 +3247,13 @@ } }, "node_modules/rolldown": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", - "integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.13.tgz", + "integrity": "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==", "devOptional": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.151.0", + "@oxc-project/types": "=0.153.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -3262,21 +3263,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.11", - "@rolldown/binding-android-arm64": "1.2.11", - "@rolldown/binding-darwin-arm64": "1.2.11", - "@rolldown/binding-darwin-x64": "1.2.11", - "@rolldown/binding-freebsd-x64": "1.2.11", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", - "@rolldown/binding-linux-arm64-gnu": "1.2.11", - "@rolldown/binding-linux-arm64-musl": "1.2.11", - "@rolldown/binding-linux-ppc64-gnu": "1.2.11", - "@rolldown/binding-linux-s390x-gnu": "1.2.11", - "@rolldown/binding-linux-x64-gnu": "1.2.11", - "@rolldown/binding-linux-x64-musl": "1.2.11", - "@rolldown/binding-openharmony-arm64": "1.2.11", - "@rolldown/binding-win32-arm64-msvc": "1.2.11", - "@rolldown/binding-win32-x64-msvc": "1.2.11" + "@rolldown/binding-android-arm-eabi": "1.2.13", + "@rolldown/binding-android-arm64": "1.2.13", + "@rolldown/binding-darwin-arm64": "1.2.13", + "@rolldown/binding-darwin-x64": "1.2.13", + "@rolldown/binding-freebsd-x64": "1.2.13", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.13", + "@rolldown/binding-linux-arm64-gnu": "1.2.13", + "@rolldown/binding-linux-arm64-musl": "1.2.13", + "@rolldown/binding-linux-ppc64-gnu": "1.2.13", + "@rolldown/binding-linux-s390x-gnu": "1.2.13", + "@rolldown/binding-linux-x64-gnu": "1.2.13", + "@rolldown/binding-linux-x64-musl": "1.2.13", + "@rolldown/binding-openharmony-arm64": "1.2.13", + "@rolldown/binding-win32-arm64-msvc": "1.2.13", + "@rolldown/binding-win32-x64-msvc": "1.2.13" } }, "node_modules/safe-buffer": { @@ -3459,18 +3460,18 @@ "license": "ISC" }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/std-env": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", - "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", "dev": true, "license": "MIT" }, @@ -3542,9 +3543,9 @@ } }, "node_modules/tinybench": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz", - "integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz", + "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==", "dev": true, "license": "MIT", "engines": { @@ -3578,9 +3579,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", - "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz", + "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", "dev": true, "license": "MIT", "engines": { @@ -3588,22 +3589,22 @@ } }, "node_modules/tldts": { - "version": "7.4.16", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.16.tgz", - "integrity": "sha512-QwBER5KMR86IIjpIiO7H/Z3IMJPsZ1A6RKPAqzTTgOyUQUSt9FdnKcqhTaJmkY6HVrgouZHZR0ncK5QxvmnQeg==", + "version": "7.4.18", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.18.tgz", + "integrity": "sha512-somxA8xKAUzlAxl0TX/jMSYIbRNFIVGnI96pVGJPeMc5BsHmfVXn6mOJT6U++ELnl04eGfzpfcEuZe8WiASkXg==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.4.16" + "tldts-core": "^7.4.18" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.4.16", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.16.tgz", - "integrity": "sha512-MDolfaSJtlSK5Y0A1xl3277ekubZwobpBjugknDizI9O5Rm60a1m8k4ICK+MRsCDzPygT81mp3BBf5RKDlFRfA==", + "version": "7.4.18", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.18.tgz", + "integrity": "sha512-KI2PKvCmvQI/xD6ypPqkU0i7oxNXR/O58RLRaHR4svsgVUQ2JE/XC8BtMbG6PSehD27I8sZJRcC1ASnCy6lp3g==", "dev": true, "license": "MIT" }, @@ -3731,16 +3732,16 @@ } }, "node_modules/vite": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.3.tgz", - "integrity": "sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", "devOptional": true, "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", - "postcss": "^8.5.28", - "rolldown": "~1.2.11", + "postcss": "^8.5.29", + "rolldown": "~1.2.12", "tinyglobby": "^0.2.17" }, "bin": { @@ -3891,30 +3892,17 @@ } } }, - "node_modules/vitest/node_modules/why-is-node-running": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", - "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", - "dev": true, - "license": "MIT", - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=20.11" - } - }, "node_modules/vue": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.43.tgz", - "integrity": "sha512-o5qZoksdnjIKvW1srZ3ab7pcDNYAerBjRe54D0LBLfRdCYFrSgBHVXokMas35czQc0//lmx4/tuY4ZNQ+Rf2Ng==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.44.tgz", + "integrity": "sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.43", - "@vue/compiler-sfc": "3.5.43", - "@vue/runtime-dom": "3.5.43", - "@vue/server-renderer": "3.5.43", - "@vue/shared": "3.5.43" + "@vue/compiler-dom": "3.5.44", + "@vue/compiler-sfc": "3.5.44", + "@vue/runtime-dom": "3.5.44", + "@vue/server-renderer": "3.5.44", + "@vue/shared": "3.5.44" }, "peerDependencies": { "typescript": "*" @@ -3926,9 +3914,9 @@ } }, "node_modules/vue-component-type-helpers": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.11.tgz", - "integrity": "sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA==", + "version": "3.3.13", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.13.tgz", + "integrity": "sha512-02KCxSmKDhIITjKyJ4QT5csP6L0WoRborLqY/34a40+S9uvMvdNgMV0yVjO3p2Gkz3ZoPmFz/0JsGHacwJYTxw==", "dev": true, "license": "MIT" }, @@ -4147,6 +4135,19 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, "node_modules/widest-line": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", diff --git a/package.json b/package.json index dd1009c38e9..d0f1ee94a3a 100644 --- a/package.json +++ b/package.json @@ -24,23 +24,24 @@ }, "type": "module", "dependencies": { - "@lucide/vue": "1.55.0", + "@intlify/message-compiler": "11.4.14", + "@lucide/vue": "1.58.0", "bootstrap": "5.3.8", "date-fns": "4.4.0", "marked": "18.0.12", - "vue": "3.5.43", + "vue": "3.5.44", "vue-i18n": "11.4.14", "vue-router": "5.4.0", "vue3-simple-icons": "16.10.0" }, "devDependencies": { "@codecov/vite-plugin": "2.0.1", - "@vitejs/plugin-vue": "6.0.9", + "@vitejs/plugin-vue": "6.0.10", "@vitest/coverage-v8": "5.0.3", "@vue/test-utils": "2.5.1", "jsdom": "30.1.2", "serve": "14.2.6", - "vite": "8.3.3", + "vite": "8.3.4", "vitest": "5.0.3" }, "overrides": { diff --git a/patches/0001-core-per-session-packet-queues.patch b/patches/0001-core-per-session-packet-queues.patch index f0534f0fac0..9737d5c7908 100644 --- a/patches/0001-core-per-session-packet-queues.patch +++ b/patches/0001-core-per-session-packet-queues.patch @@ -1,29 +1,29 @@ diff --git a/src/audio.cpp b/src/audio.cpp -index 28a3d810..add54a75 100644 +index 00ddb12d..b7d1df15 100644 --- a/src/audio.cpp +++ b/src/audio.cpp -@@ -103,11 +103,11 @@ namespace audio { +@@ -104,11 +104,11 @@ namespace audio { * @brief Encode captured PCM samples into Opus packets on the audio worker thread. * * @param samples Queue of captured PCM sample buffers to encode. + * @param packets Queue that receives the encoded packets. * @param config Audio stream settings negotiated with the client. - * @param channel_data Platform-specific audio capture context passed to packet metadata. + * @param channel_data Stream session carried with each encoded packet. */ -- void encodeThread(sample_queue_t samples, config_t config, void *channel_data) { +- void encodeThread(sample_queue_t samples, config_t config, stream::session_t *channel_data) { - auto packets = mail::man->queue(mail::audio_packets); -+ void encodeThread(sample_queue_t samples, safe::mail_raw_t::queue_t packets, config_t config, void *channel_data) { ++ void encodeThread(sample_queue_t samples, safe::mail_raw_t::queue_t packets, config_t config, stream::session_t *channel_data) { 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); -@@ -234,7 +234,7 @@ namespace audio { +@@ -235,7 +235,7 @@ namespace audio { platf::adjust_thread_priority(platf::thread_priority_e::critical); auto samples = std::make_shared(30); - std::jthread thread {encodeThread, samples, config, channel_data}; + std::jthread thread {encodeThread, samples, mail::packet_queue(mail, mail::audio_packets), config, channel_data}; - auto fg = util::fail_guard([&]() { + auto fg = util::fail_guard([&samples, &thread, &shutdown_event]() { samples->stop(); diff --git a/src/globals.h b/src/globals.h index 2c7ee9b1..f3983c9d 100644 @@ -54,10 +54,10 @@ index 2c7ee9b1..f3983c9d 100644 + } // namespace mail diff --git a/src/thread_safe.h b/src/thread_safe.h -index f3061e2b..de5fe110 100644 +index a2fc99e7..80093ee8 100644 --- a/src/thread_safe.h +++ b/src/thread_safe.h -@@ -854,6 +854,19 @@ namespace safe { +@@ -852,6 +852,19 @@ namespace safe { return post; } @@ -78,7 +78,7 @@ index f3061e2b..de5fe110 100644 * @brief Run cleanup for completed asynchronous work. */ diff --git a/src/video.cpp b/src/video.cpp -index 58535864..c0298e88 100644 +index 82a1a4ad..df34db69 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -2438,7 +2438,7 @@ namespace video { @@ -90,7 +90,7 @@ index 58535864..c0298e88 100644 auto idr_events = mail->event(mail::idr); auto invalidate_ref_frames_events = mail->event>(mail::invalidate_ref_frames); -@@ -2983,7 +2983,7 @@ namespace video { +@@ -2979,7 +2979,7 @@ namespace video { ref->encode_session_ctx_queue.raise(sync_session_ctx_t { &join_event, mail->event(mail::shutdown), diff --git a/patches/0002-core-webrtc-server-integration.patch b/patches/0002-core-webrtc-server-integration.patch index c2874022371..02494914a4a 100644 --- a/patches/0002-core-webrtc-server-integration.patch +++ b/patches/0002-core-webrtc-server-integration.patch @@ -1,8 +1,8 @@ diff --git a/src/config.cpp b/src/config.cpp -index ad44a29d..b90a1164 100644 +index 7705aa64..2628e4aa 100644 --- a/src/config.cpp +++ b/src/config.cpp -@@ -824,6 +824,37 @@ namespace config { +@@ -846,6 +846,37 @@ namespace config { 0, // packetsize }; @@ -40,9 +40,9 @@ index ad44a29d..b90a1164 100644 /** * @brief Default NVHTTP server configuration values used before file and CLI overrides. */ -@@ -1804,6 +1835,18 @@ namespace config { +@@ -1827,6 +1858,18 @@ namespace config { - int_between_f(vars, "fec_percentage", stream.fec_percentage, {1, 255}); + int_between_f(vars, "fec_percentage", stream.fec_percentage, {0, 100}); + // webrtc_enabled predates stream_protocol: disabled still means Moonlight only. + if (const auto legacy = vars.find("webrtc_enabled"); legacy != std::end(vars)) { @@ -60,10 +60,10 @@ index ad44a29d..b90a1164 100644 // This config option will only be used by the UI diff --git a/src/config.h b/src/config.h -index 76176e00..419ff7b2 100644 +index d13ddfd4..a67f8be4 100644 --- a/src/config.h +++ b/src/config.h -@@ -264,6 +264,25 @@ namespace config { +@@ -274,6 +274,25 @@ namespace config { int packetsize; ///< Maximum payload size for network packets. }; @@ -89,7 +89,7 @@ index 76176e00..419ff7b2 100644 /** * @brief HTTP and HTTPS settings used by the GameStream pairing server. */ -@@ -398,10 +417,33 @@ namespace config { +@@ -410,10 +429,33 @@ namespace config { extern video_t video; extern audio_t audio; extern stream_t stream; @@ -124,7 +124,7 @@ index 76176e00..419ff7b2 100644 /** * @brief Parse and apply serialized configuration text for unit tests. diff --git a/src/confighttp.cpp b/src/confighttp.cpp -index 4e6ba138..be9dc36a 100644 +index edf1c575..ab2b3c98 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -62,6 +62,7 @@ @@ -230,7 +230,7 @@ index 4e6ba138..be9dc36a 100644 } catch (std::exception &e) { BOOST_LOG(warning) << "SavePin: "sv << e.what(); diff --git a/src/main.cpp b/src/main.cpp -index fb525548..d8ae0a51 100644 +index 841e5c8c..f34882e8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -45,6 +45,7 @@ @@ -278,10 +278,10 @@ index fb525548..d8ae0a51 100644 #ifdef _WIN32 // If we're using the default port and GameStream is enabled, warn the user diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp -index 98f59f90..be052e1b 100644 +index 8872b13d..d11cb2b0 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp -@@ -38,6 +38,7 @@ +@@ -40,6 +40,7 @@ #include "utility.h" #include "uuid.h" #include "video.h" @@ -289,7 +289,7 @@ index 98f59f90..be052e1b 100644 using namespace std::literals; -@@ -1385,7 +1386,8 @@ namespace nvhttp { +@@ -1419,7 +1420,8 @@ namespace nvhttp { host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); auto launch_session = make_launch_session(host_audio, args); @@ -299,7 +299,7 @@ index 98f59f90..be052e1b 100644 // The display should be restored in case something fails as there are no other sessions. revert_display_configuration = true; -@@ -1494,7 +1496,7 @@ namespace nvhttp { +@@ -1528,7 +1530,7 @@ namespace nvhttp { // 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. @@ -308,7 +308,7 @@ index 98f59f90..be052e1b 100644 if (no_active_sessions && args.find("localAudioPlayMode"s) != std::end(args)) { host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); } -@@ -1627,6 +1629,13 @@ namespace nvhttp { +@@ -1661,6 +1663,13 @@ namespace nvhttp { auto cert = file_handler::read_file(config::nvhttp.cert.c_str()); setup(pkey, cert); diff --git a/patches/0003-build-libdatachannel.patch b/patches/0003-build-libdatachannel.patch index 5c2b5e363bf..55b606b5a78 100644 --- a/patches/0003-build-libdatachannel.patch +++ b/patches/0003-build-libdatachannel.patch @@ -41,10 +41,10 @@ index 9cc17c85..56049ea5 100644 pkg_check_modules(CURL REQUIRED libcurl) diff --git a/cmake/dependencies/libdatachannel.cmake b/cmake/dependencies/libdatachannel.cmake new file mode 100644 -index 00000000..ac991667 +index 00000000..2f9b13fd --- /dev/null +++ b/cmake/dependencies/libdatachannel.cmake -@@ -0,0 +1,56 @@ +@@ -0,0 +1,62 @@ +# +# Loads libdatachannel for the Moonlight WebRTC TV stream, giving the priority to the system package first, +# with a fallback to FetchContent. MSYS2 does not package libdatachannel, so Windows builds use the fallback. @@ -97,6 +97,12 @@ index 00000000..ac991667 +set(BUILD_SHARED_LIBS "${_sunshine_build_shared_libs}") +unset(_sunshine_build_shared_libs) + ++# v0.24.6 uses std::transform in configuration.cpp without including . libstdc++ includes it ++# transitively, libc++ (Windows ARM64 with clang) does not. Fixed upstream after v0.24.6. ++set_source_files_properties("${libdatachannel_SOURCE_DIR}/src/configuration.cpp" ++ TARGET_DIRECTORY datachannel-static ++ PROPERTIES COMPILE_OPTIONS "-include;algorithm") ++ +set(SUNSHINE_LIBDATACHANNEL_TARGET LibDataChannel::LibDataChannelStatic) +if(NOT TARGET ${SUNSHINE_LIBDATACHANNEL_TARGET}) + set(SUNSHINE_LIBDATACHANNEL_TARGET datachannel-static) diff --git a/patches/0004-webrtc-module.patch b/patches/0004-webrtc-module.patch index d4f750276c8..371a112d485 100644 --- a/patches/0004-webrtc-module.patch +++ b/patches/0004-webrtc-module.patch @@ -1093,10 +1093,10 @@ index 00000000..6acae118 +} // namespace webrtc_stream diff --git a/src/webrtc/protocol.cpp b/src/webrtc/protocol.cpp new file mode 100644 -index 00000000..043d8953 +index 00000000..1ed31157 --- /dev/null +++ b/src/webrtc/protocol.cpp -@@ -0,0 +1,464 @@ +@@ -0,0 +1,510 @@ +/** + * @file src/webrtc/protocol.cpp + * @brief Definitions for the Moonlight WebRTC TV protocol (version 2). @@ -1137,6 +1137,37 @@ index 00000000..043d8953 + } + + /** ++ * @brief Read the lines of a `client-log` message. ++ * ++ * Each line becomes one line of Sunshine's log, so control characters, which would let a ++ * line pass itself off as several, are replaced and long lines are cut. ++ * @param message Parsed message. ++ * @return The lines. ++ */ ++ std::vector client_log_lines(const json &message) { ++ const auto &lines = message.at("lines"); ++ if (!lines.is_array() || lines.size() > MAX_CLIENT_LOG_LINES) { ++ throw protocol_error_t("invalid-message", "lines must be an array of at most 100 strings"); ++ } ++ std::vector result; ++ result.reserve(lines.size()); ++ for (const auto &line : lines) { ++ if (!line.is_string()) { ++ throw protocol_error_t("invalid-message", "lines must be an array of at most 100 strings"); ++ } ++ auto text = line.get(); ++ if (text.size() > MAX_CLIENT_LOG_LINE_LENGTH) { ++ text.resize(MAX_CLIENT_LOG_LINE_LENGTH); ++ } ++ std::ranges::replace_if(text, [](unsigned char character) { ++ return character < 0x20 || character == 0x7F; ++ }, ' '); ++ result.push_back(std::move(text)); ++ } ++ return result; ++ } ++ ++ /** + * @brief Read a positive session ID. + * @param message Parsed message. + * @return The session ID. @@ -1407,6 +1438,9 @@ index 00000000..043d8953 + if (type == "candidate") { + return {type, candidate_t {session_id(message), message.at("candidate").get(), message.at("mid").get()}}; + } ++ if (type == "client-log") { ++ return {type, client_log_t {client_log_lines(message)}}; ++ } + throw protocol_error_t("unsupported-message", "Unsupported message type: " + type); + } catch (const protocol_error_t &) { + throw; @@ -1560,13 +1594,25 @@ index 00000000..043d8953 + message.update({{"sessionId", session_id_value}, {"candidate", candidate}, {"mid", mid}}); + return message; + } ++ ++ bool is_keyframe_request(std::string_view text) { ++ if (text.find("request-keyframe") == std::string_view::npos) { ++ return false; ++ } ++ try { ++ const auto message = json::parse(text); ++ return message.is_object() && message.contains("type") && message["type"].is_string() && message["type"].get() == "request-keyframe"; ++ } catch (const json::exception &) { ++ return false; ++ } ++ } +} // namespace webrtc_stream::protocol diff --git a/src/webrtc/protocol.h b/src/webrtc/protocol.h new file mode 100644 -index 00000000..657ff023 +index 00000000..36cf41bb --- /dev/null +++ b/src/webrtc/protocol.h -@@ -0,0 +1,417 @@ +@@ -0,0 +1,444 @@ +/** + * @file src/webrtc/protocol.h + * @brief Declarations for the Moonlight WebRTC TV protocol (version 2). @@ -1804,9 +1850,26 @@ index 00000000..657ff023 + }; + + /** ++ * @brief Lines of the TV application's own log, written into Sunshine's log. ++ */ ++ struct client_log_t { ++ std::vector lines; ///< Log lines, single-line and bounded in length. ++ }; ++ ++ /** ++ * @brief Largest number of lines one `client-log` message may carry. ++ */ ++ constexpr std::size_t MAX_CLIENT_LOG_LINES = 100; ++ ++ /** ++ * @brief Longest client log line kept; longer lines are cut. ++ */ ++ constexpr std::size_t MAX_CLIENT_LOG_LINE_LENGTH = 4000; ++ ++ /** + * @brief Any message the TV may send. + */ -+ using client_payload_t = std::variant; ++ using client_payload_t = std::variant; + + /** + * @brief A parsed TV message. @@ -1853,6 +1916,16 @@ index 00000000..657ff023 + client_message_t parse_client_message(std::string_view text); + + /** ++ * @brief Check whether a control-channel message is the TV app asking for a keyframe. ++ * ++ * The TV sends `{"type": "request-keyframe"}` when its picture stops advancing while video ++ * still arrives, a stall its WebRTC stack does not always answer with a PLI of its own. ++ * @param text Control-channel message. ++ * @return True for a `request-keyframe` message, false for anything else or malformed JSON. ++ */ ++ bool is_keyframe_request(std::string_view text); ++ ++ /** + * @brief Build the greeting carrying the authentication nonce. + * + * It also announces `"pairing": "client-pin"`: an unpaired TV shows its own PIN and sends @@ -2795,10 +2868,10 @@ index 00000000..406ed408 +} // namespace webrtc_stream::tv_auth diff --git a/src/webrtc/webrtc_stream.cpp b/src/webrtc/webrtc_stream.cpp new file mode 100644 -index 00000000..2770fca1 +index 00000000..d7f4fae5 --- /dev/null +++ b/src/webrtc/webrtc_stream.cpp -@@ -0,0 +1,1994 @@ +@@ -0,0 +1,2145 @@ +/** + * @file src/webrtc/webrtc_stream.cpp + * @brief Definitions for streaming to Moonlight WebRTC TVs directly from Sunshine. @@ -3090,6 +3163,76 @@ index 00000000..2770fca1 + } + + /** ++ * @brief Bytes sent per millisecond at most: 80% of 1 Gbps, the GameStream sender's rate. ++ */ ++ constexpr std::size_t PACED_BYTES_PER_MS = 1'000'000'000ULL * 80 / 100 / 8 / 1000; ++ ++ /** ++ * @brief Paces a frame's RTP packets the way the GameStream sender does. ++ * ++ * The packetizer hands over a whole frame at once. A 4K keyframe is hundreds of packets, ++ * and written back to back they overflow the UDP socket's send buffer, which drops them ++ * without telling anyone. The TV is left without the keyframe it asked for, asks again and ++ * receives another burst, and the picture stays black while audio, a few small packets ++ * at a time, carries on. Sending a millisecond's worth at most, then waiting for the next ++ * millisecond, keeps every frame inside the buffer at the cost of a few milliseconds on the ++ * largest ones. It is the last handler of the chain and runs on the video sender thread, ++ * which has nothing else to do meanwhile; NACK retransmissions do not pass through it. ++ */ ++ class frame_pacer_t final: public rtc::MediaHandler { ++ public: ++ void outgoing(rtc::message_vector &messages, const rtc::message_callback &send) override { ++ if (!_timer) { ++ _timer = platf::create_high_precision_timer(); ++ } ++ // A frame that follows a large one closely starts where that one's pacing ended. ++ const auto frame_start = std::max(_next_frame_start, std::chrono::steady_clock::now()); ++ std::size_t frame_bytes = 0; ++ std::size_t group_bytes = 0; ++ for (auto &message : messages) { ++ if (group_bytes >= PACED_BYTES_PER_MS) { ++ wait_until(frame_start + paced_duration(frame_bytes)); ++ group_bytes = 0; ++ } ++ frame_bytes += message->size(); ++ group_bytes += message->size(); ++ send(std::move(message)); ++ } ++ messages.clear(); ++ _next_frame_start = frame_start + paced_duration(frame_bytes); ++ } ++ ++ private: ++ /** ++ * @brief Time the given number of bytes takes at the paced rate. ++ * @param bytes Bytes sent. ++ * @return Duration. ++ */ ++ static std::chrono::nanoseconds paced_duration(std::size_t bytes) { ++ return std::chrono::nanoseconds {std::chrono::nanoseconds {1ms}.count() * static_cast(bytes) / static_cast(PACED_BYTES_PER_MS)}; ++ } ++ ++ /** ++ * @brief Sleep until a point in time, precisely when the platform allows it. ++ * @param due When to wake. ++ */ ++ void wait_until(std::chrono::steady_clock::time_point due) { ++ const auto now = std::chrono::steady_clock::now(); ++ if (now >= due) { ++ return; ++ } ++ if (_timer && *_timer) { ++ _timer->sleep_for(due - now); ++ } else { ++ std::this_thread::sleep_until(due); ++ } ++ } ++ ++ std::unique_ptr _timer; ///< Created on the sending thread. ++ std::chrono::steady_clock::time_point _next_frame_start {}; ///< When the previous frame's pacing ends. ++ }; ++ ++ /** + * @brief One WebRTC stream: its PeerConnection and, once connected, its capture pipeline. + */ + struct stream_t { @@ -3119,6 +3262,9 @@ index 00000000..2770fca1 + bool stop_requested = false; ///< The stream must end. + std::optional failure; ///< Why the host ended the stream. + safe::mail_raw_t::event_t idr_event; ///< Keyframe requests, once capture runs. ++ std::uint32_t pli_requests = 0; ///< Keyframes the TV's WebRTC stack asked for since the last log line. ++ std::uint32_t watchdog_requests = 0; ///< Keyframes the TV app's stall watchdog asked for since the last log line. ++ std::chrono::steady_clock::time_point last_request_log {}; ///< When keyframe requests were last logged. + + std::mutex candidate_mutex; ///< Protects candidate buffering. + bool has_remote_description = false; ///< Whether the answer was applied. @@ -3142,6 +3288,28 @@ index 00000000..2770fca1 + } + + /** ++ * @brief Encode the next frame as IDR, logging the requests at most once a second. ++ * ++ * A PLI storm would otherwise flood the log, while a single line per second still shows ++ * whether the TV asked at all while its picture was black. ++ * @param from_watchdog Whether the TV app asked because its picture stopped, rather than its WebRTC stack through PLI. ++ */ ++ void request_keyframe(bool from_watchdog) { ++ std::lock_guard lock(mutex); ++ ++(from_watchdog ? watchdog_requests : pli_requests); ++ if (idr_event) { ++ idr_event->raise(true); ++ } ++ const auto now = std::chrono::steady_clock::now(); ++ if (now - last_request_log >= 1s) { ++ BOOST_LOG(info) << "WebRTC: session "sv << id << " keyframe requested: "sv << pli_requests << " PLI, "sv << watchdog_requests << " from the TV stall watchdog"sv; ++ pli_requests = 0; ++ watchdog_requests = 0; ++ last_request_log = now; ++ } ++ } ++ ++ /** + * @brief Check whether the stream must end. + * @return True once a stop was requested. + */ @@ -3619,6 +3787,8 @@ index 00000000..2770fca1 + handle_answer(connection, *answer); + } else if (const auto *candidate = std::get_if(&message.payload)) { + handle_candidate(connection, *candidate); ++ } else if (const auto *client_log = std::get_if(&message.payload)) { ++ write_client_log(connection, *client_log); + } + } catch (const protocol::protocol_error_t &e) { + BOOST_LOG(warning) << "WebRTC: protocol error: "sv << e.what(); @@ -3630,6 +3800,25 @@ index 00000000..2770fca1 + } + + /** ++ * @brief Write the TV application's log lines into Sunshine's log. ++ * ++ * What the TV saw - a stall, a recovery, its decoder counters - then sits in one file ++ * next to what Sunshine did at the same moment, with no tooling needed on the TV. ++ * @param connection Connection. ++ * @param client_log Lines sent by the TV. ++ */ ++ void write_client_log(const std::shared_ptr &connection, const protocol::client_log_t &client_log) { ++ std::string name; ++ { ++ std::lock_guard lock(connection->mutex); ++ name = connection->client_name; ++ } ++ for (const auto &line : client_log.lines) { ++ BOOST_LOG(info) << "WebRTC TV "sv << name << ": "sv << line; ++ } ++ } ++ ++ /** + * @brief Check a paired TV's proof against the connection nonce. + * @param connection Connection. + * @param request Authentication request. @@ -4034,7 +4223,13 @@ index 00000000..2770fca1 + stream->control_channel = stream->peer->createDataChannel("control", control_options); + stream->control_channel->onMessage([weak_stream](rtc::message_variant data) { + const auto stream = weak_stream.lock(); -+ if (stream && std::holds_alternative(data) && !stream->input_bridge->handle_control_message(std::get(data))) { ++ if (!stream || !std::holds_alternative(data)) { ++ return; ++ } ++ const auto &text = std::get(data); ++ if (protocol::is_keyframe_request(text)) { ++ stream->request_keyframe(true); ++ } else if (!stream->input_bridge->handle_control_message(text)) { + BOOST_LOG(warning) << "WebRTC: rejected malformed gamepad control message"sv; + } + }); @@ -4103,11 +4298,9 @@ index 00000000..2770fca1 + return; + } + // Sunshine encodes the next frame as IDR; no intermediate client has to ask for it. -+ std::lock_guard lock(stream->mutex); -+ if (stream->idr_event) { -+ stream->idr_event->raise(true); -+ } ++ stream->request_keyframe(false); + })); ++ packetizer->addToChain(std::make_shared()); + stream->video_track->setMediaHandler(packetizer); + stream->video_track->onOpen([weak_stream] { + if (const auto stream = weak_stream.lock()) { @@ -4341,18 +4534,24 @@ index 00000000..2770fca1 + const auto epoch = std::chrono::steady_clock::now(); + std::jthread video_capture([mail, video_config, stream] { + platf::set_thread_name("webrtc::video"); -+ video::capture(mail, video_config, stream.get()); ++ // The packets come back through this stream's own queues, so no GameStream session rides with them. ++ video::capture(mail, video_config, nullptr); + stream->request_stop("Video capture stopped"); + }); + std::jthread audio_capture([mail, audio_config, stream] { + platf::set_thread_name("webrtc::audio"); -+ audio::capture(mail, audio_config, stream.get()); ++ audio::capture(mail, audio_config, nullptr); + }); + std::jthread video_sender([video_packets, stream, epoch] { + platf::set_thread_name("webrtc::video_send"); + platf::adjust_thread_priority(platf::thread_priority_e::high); + std::optional last_timestamp; + const std::uint32_t frame_ticks = VIDEO_CLOCK_RATE / std::max(1, stream->settings.fps); ++ // IDR frames are logged at most once a second, with the largest one, so a black ++ // picture on the TV can be matched against whether keyframes were actually sent. ++ std::uint32_t idr_frames = 0; ++ std::size_t largest_idr = 0; ++ std::chrono::steady_clock::time_point last_idr_log {}; + while (auto packet = video_packets->pop()) { + std::string_view payload {reinterpret_cast(packet->data()), packet->data_size()}; + std::vector replaced; @@ -4360,6 +4559,17 @@ index 00000000..2770fca1 + replaced = apply_replacements(payload, *packet->replacements); + payload = {reinterpret_cast(replaced.data()), replaced.size()}; + } ++ if (packet->is_idr()) { ++ ++idr_frames; ++ largest_idr = std::max(largest_idr, payload.size()); ++ const auto now = std::chrono::steady_clock::now(); ++ if (now - last_idr_log >= 1s) { ++ BOOST_LOG(info) << "WebRTC: session "sv << stream->id << " sent "sv << idr_frames << " IDR frame(s), largest "sv << largest_idr / 1024 << " KiB"sv; ++ idr_frames = 0; ++ largest_idr = 0; ++ last_idr_log = now; ++ } ++ } + // The capture time drives the RTP clock so the TV paces frames as they were captured. + auto timestamp = last_timestamp ? *last_timestamp + frame_ticks : 0U; + if (packet->frame_timestamp) { @@ -4706,11 +4916,25 @@ index 00000000..2770fca1 + BOOST_LOG(info) << "WebRTC: TV server disabled by stream_protocol"sv; + return; + } -+ rtc::InitLogger(rtc::LogLevel::Warning, [](rtc::LogLevel level, const std::string &message) { ++ // libjuice reports a full send buffer, which silently drops a packet, only at Info level. ++ // It is the one Info message worth seeing, so it is let through and summarized each second. ++ rtc::InitLogger(rtc::LogLevel::Info, [](rtc::LogLevel level, const std::string &message) { + if (level <= rtc::LogLevel::Error) { + BOOST_LOG(error) << "libdatachannel: "sv << message; -+ } else { ++ } else if (level == rtc::LogLevel::Warning) { + BOOST_LOG(warning) << "libdatachannel: "sv << message; ++ } else if (message.find("buffer is full"sv) != std::string::npos) { ++ static std::mutex dropped_mutex; ++ static std::uint64_t dropped = 0; ++ static std::chrono::steady_clock::time_point last_log {}; ++ std::lock_guard lock(dropped_mutex); ++ ++dropped; ++ const auto now = std::chrono::steady_clock::now(); ++ if (now - last_log >= 1s) { ++ BOOST_LOG(warning) << "WebRTC: UDP send buffer full, "sv << dropped << " packet(s) dropped since the last report"sv; ++ dropped = 0; ++ last_log = now; ++ } + } + }); + rtc::Preload(); diff --git a/patches/0005-web-ui-tv-pairing-and-options.patch b/patches/0005-web-ui-tv-pairing-and-options.patch index 12d0192ccd3..63730f579a3 100644 --- a/patches/0005-web-ui-tv-pairing-and-options.patch +++ b/patches/0005-web-ui-tv-pairing-and-options.patch @@ -33,10 +33,10 @@ index 95330fa4..38564ab2 100644 this.status = {type: 'danger', message: this.i18n.t('pin.pair_failure')}; } diff --git a/src_assets/common/assets/web/configs/config_tabs.json b/src_assets/common/assets/web/configs/config_tabs.json -index c477936c..51c0b628 100644 +index 71ab0211..52500c62 100644 --- a/src_assets/common/assets/web/configs/config_tabs.json +++ b/src_assets/common/assets/web/configs/config_tabs.json -@@ -76,7 +76,11 @@ +@@ -77,7 +77,11 @@ "lan_encryption_mode": 0, "wan_encryption_mode": 1, "ping_timeout": 10000, @@ -168,10 +168,10 @@ index 9988dcc7..d8f8ba50 100644 diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json -index 030f733c..8f31e19c 100644 +index 416b274d..8514647e 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json -@@ -389,6 +389,10 @@ +@@ -393,6 +393,10 @@ "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", @@ -182,7 +182,7 @@ index 030f733c..8f31e19c 100644 "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)", -@@ -406,6 +410,11 @@ +@@ -410,6 +414,11 @@ "search_options": "Search configuration options...", "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.", @@ -194,7 +194,7 @@ index 030f733c..8f31e19c 100644 "sunshine_name": "Sunshine Name", "sunshine_name_desc": "The name displayed by Moonlight. If not specified, the PC's hostname is used", "sw_preset": "SW Presets", -@@ -433,6 +442,12 @@ +@@ -437,6 +446,12 @@ "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", diff --git a/patches/0006-tests.patch b/patches/0006-tests.patch index bfa2ebe1c7a..1a9ba89ad67 100644 --- a/patches/0006-tests.patch +++ b/patches/0006-tests.patch @@ -1,3 +1,27 @@ +diff --git a/tests/unit/test_audio.cpp b/tests/unit/test_audio.cpp +index 8561cb3d..88031dc9 100644 +--- a/tests/unit/test_audio.cpp ++++ b/tests/unit/test_audio.cpp +@@ -12,7 +12,7 @@ + #include + + namespace audio { +- void encodeThread(std::shared_ptr>> samples, config_t config, stream::session_t *channel_data); ++ void encodeThread(std::shared_ptr>> samples, safe::mail_raw_t::queue_t packets, config_t config, stream::session_t *channel_data); + } + + using namespace audio; +@@ -42,8 +42,8 @@ TEST(AudioPacketSessionTest, EncoderPreservesTheReceivingSession) { + config_t config {.packetDuration = 5, .channels = 2, .mask = 0x3}; + auto samples = std::make_shared>>(); + auto packets = mail::man->queue(mail::audio_packets); +- std::jthread worker([&samples, &config, &session] { +- audio::encodeThread(samples, config, session.get()); ++ std::jthread worker([&samples, &packets, &config, &session] { ++ audio::encodeThread(samples, packets, config, session.get()); + }); + auto stop_worker = util::fail_guard([&samples] { + samples->stop(); diff --git a/tests/unit/webrtc/test_webrtc_input_bridge.cpp b/tests/unit/webrtc/test_webrtc_input_bridge.cpp new file mode 100644 index 00000000..d4d98269 @@ -264,10 +288,10 @@ index 00000000..d4d98269 +} diff --git a/tests/unit/webrtc/test_webrtc_protocol.cpp b/tests/unit/webrtc/test_webrtc_protocol.cpp new file mode 100644 -index 00000000..2c8e1eee +index 00000000..fceccbf2 --- /dev/null +++ b/tests/unit/webrtc/test_webrtc_protocol.cpp -@@ -0,0 +1,162 @@ +@@ -0,0 +1,202 @@ +/** + * @file tests/unit/webrtc/test_webrtc_protocol.cpp + * @brief Tests for the Moonlight WebRTC TV protocol. @@ -430,6 +454,46 @@ index 00000000..2c8e1eee + EXPECT_TRUE(message.at("video").at("hdr")); + EXPECT_EQ(message.at("audio").at("sampleRate"), 48000); +} ++ ++TEST(WebrtcProtocolTest, RecognizesKeyframeRequests) { ++ EXPECT_TRUE(is_keyframe_request(R"({"v":1,"type":"request-keyframe","reason":"stall"})")); ++ EXPECT_TRUE(is_keyframe_request(R"({"type":"request-keyframe"})")); ++} ++ ++TEST(WebrtcProtocolTest, RejectsOtherControlMessagesAsKeyframeRequests) { ++ EXPECT_FALSE(is_keyframe_request(R"({"v":1,"type":"gamepad-connected","index":0})")); ++ EXPECT_FALSE(is_keyframe_request(R"({"type":"gamepad-connected","id":"request-keyframe"})")); ++ EXPECT_FALSE(is_keyframe_request(R"({"type":7,"note":"request-keyframe"})")); ++ EXPECT_FALSE(is_keyframe_request(R"(["request-keyframe"])")); ++ EXPECT_FALSE(is_keyframe_request(R"({"type":"request-keyframe")")); ++ EXPECT_FALSE(is_keyframe_request("")); ++} ++ ++TEST(WebrtcProtocolTest, ParsesClientLogLines) { ++ const auto message = parse_client_message(R"({"version":2,"type":"client-log","lines":["Video stalled","second\nline\twith controls"]})"); ++ const auto *log = std::get_if(&message.payload); ++ ASSERT_NE(log, nullptr); ++ ASSERT_EQ(log->lines.size(), 2U); ++ EXPECT_EQ(log->lines[0], "Video stalled"); ++ EXPECT_EQ(log->lines[1], "second line with controls"); ++} ++ ++TEST(WebrtcProtocolTest, CutsLongClientLogLines) { ++ const nlohmann::json request = {{"version", 2}, {"type", "client-log"}, {"lines", {std::string(MAX_CLIENT_LOG_LINE_LENGTH + 10, 'x')}}}; ++ const auto message = parse_client_message(request.dump()); ++ EXPECT_EQ(std::get(message.payload).lines.at(0).size(), MAX_CLIENT_LOG_LINE_LENGTH); ++} ++ ++TEST(WebrtcProtocolTest, RejectsMalformedClientLogs) { ++ EXPECT_THROW(parse_client_message(R"({"version":2,"type":"client-log","lines":"one line"})"), protocol_error_t); ++ EXPECT_THROW(parse_client_message(R"({"version":2,"type":"client-log","lines":[1]})"), protocol_error_t); ++ EXPECT_THROW(parse_client_message(R"({"version":2,"type":"client-log"})"), protocol_error_t); ++ nlohmann::json too_many = {{"version", 2}, {"type", "client-log"}, {"lines", nlohmann::json::array()}}; ++ for (std::size_t index = 0; index <= MAX_CLIENT_LOG_LINES; ++index) { ++ too_many["lines"].push_back("line"); ++ } ++ EXPECT_THROW(parse_client_message(too_many.dump()), protocol_error_t); ++} diff --git a/tests/unit/webrtc/test_webrtc_sdp.cpp b/tests/unit/webrtc/test_webrtc_sdp.cpp new file mode 100644 index 00000000..7680603b diff --git a/patches/0007-docs.patch b/patches/0007-docs.patch index e2174291642..29caaee8776 100644 --- a/patches/0007-docs.patch +++ b/patches/0007-docs.patch @@ -14,10 +14,10 @@ index 2f65f3f9..8bad65ba 100644 diff --git a/docs/configuration.md b/docs/configuration.md -index fb6ce69c..bf845efb 100644 +index e19ddd15..55920e78 100644 --- a/docs/configuration.md +++ b/docs/configuration.md -@@ -1880,6 +1880,134 @@ consent prompt for directory ACLs, so the Web UI shows setup steps for correctin +@@ -1914,6 +1914,134 @@ consent prompt for directory ACLs, so the Web UI shows setup steps for correctin diff --git a/src/audio.cpp b/src/audio.cpp index 314f76f1f88..b7d1df15f9c 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -106,9 +106,9 @@ namespace audio { * @param samples Queue of captured PCM sample buffers to encode. * @param packets Queue that receives the encoded packets. * @param config Audio stream settings negotiated with the client. - * @param channel_data Platform-specific audio capture context passed to packet metadata. + * @param channel_data Stream session carried with each encoded packet. */ - void encodeThread(sample_queue_t samples, safe::mail_raw_t::queue_t packets, config_t config, void *channel_data) { + void encodeThread(sample_queue_t samples, safe::mail_raw_t::queue_t packets, config_t config, stream::session_t *channel_data) { 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); @@ -155,7 +155,7 @@ namespace audio { /** * @brief Run the capture loop for this backend. */ - void capture(safe::mail_t mail, config_t config, void *channel_data) { + void capture(safe::mail_t mail, config_t config, stream::session_t *channel_data) { auto shutdown_event = mail->event(mail::shutdown); if (!config::audio.stream) { shutdown_event->view(); @@ -237,7 +237,7 @@ namespace audio { auto samples = std::make_shared(30); std::jthread thread {encodeThread, samples, mail::packet_queue(mail, mail::audio_packets), config, channel_data}; - auto fg = util::fail_guard([&]() { + auto fg = util::fail_guard([&samples, &thread, &shutdown_event]() { samples->stop(); thread.join(); diff --git a/src/audio.h b/src/audio.h index f2141728342..a67092510ab 100644 --- a/src/audio.h +++ b/src/audio.h @@ -12,6 +12,10 @@ #include #include +namespace stream { + struct session_t; +} + namespace audio { /** * @brief Supported Opus channel layouts advertised to Moonlight clients. @@ -95,9 +99,9 @@ namespace audio { */ using buffer_t = util::buffer_t; /** - * @brief Encoded audio packet paired with platform channel metadata. + * @brief Encoded audio packet paired with its owning stream session. */ - using packet_t = std::pair; + using packet_t = std::pair; /** * @brief Shared mailbox reference to the global audio context. */ @@ -108,9 +112,9 @@ namespace audio { * * @param mail Mailbox used to publish encoded audio packets. * @param config Audio capture and encoder settings. - * @param channel_data Platform-specific capture channel pointer. + * @param channel_data Stream session that receives the encoded packets. */ - void capture(safe::mail_t mail, config_t config, void *channel_data); + void capture(safe::mail_t mail, config_t config, stream::session_t *channel_data); /** * @brief Get the reference to the audio context. diff --git a/src/cbs.cpp b/src/cbs.cpp index d933d870a43..54464e94900 100644 --- a/src/cbs.cpp +++ b/src/cbs.cpp @@ -32,7 +32,7 @@ namespace cbs { /** * @brief Owning coded bitstream context pointer that calls ff_cbs_close. */ - using ctx_t = util::safe_ptr; + using ctx_t = util::safe_ptr; /** * @brief Owns an FFmpeg coded bitstream fragment and frees fragment buffers on destruction. @@ -78,17 +78,21 @@ namespace cbs { }; /** - * @brief Serialize a prepared coded bitstream unit with an existing context. + * @brief Serialize a mutable copy of a coded bitstream unit without modifying its source. * * @param cbs_ctx Coded bitstream context initialized for the codec. * @param nal NAL unit type to insert into the fragment. - * @param uh Pointer to the FFmpeg raw unit header/content structure. + * @tparam Unit Complete codec-specific parameter-set type. + * @param unit Parameter set to copy for FFmpeg's mutable writer interface. * @param codec_id FFmpeg codec identifier used by the context. * @return Serialized NAL unit bytes, or an empty buffer if FFmpeg rejects the fragment. */ - util::buffer_t write(cbs::ctx_t &cbs_ctx, std::uint8_t nal, void *uh, AVCodecID codec_id) { + template + util::buffer_t write(cbs::ctx_t &cbs_ctx, std::uint8_t nal, const Unit &unit, AVCodecID codec_id) { + // Parameter sets can be too large for the stack, and the fragment borrows this mutable copy. + auto content = std::make_unique(unit); cbs::frag_t frag; - auto err = ff_cbs_insert_unit_content(&frag, -1, nal, uh, nullptr); + auto err = ff_cbs_insert_unit_content(&frag, -1, nal, content.get(), nullptr); if (err < 0) { std::array err_str {0}; BOOST_LOG(error) << "Could not insert NAL unit SPS: "sv << av_make_error_string(err_str.data(), AV_ERROR_MAX_STRING_SIZE, err); @@ -115,15 +119,17 @@ namespace cbs { * @brief Serialize a prepared coded bitstream unit with a temporary context. * * @param nal NAL unit type to insert into the fragment. - * @param uh Pointer to the FFmpeg raw unit header/content structure. + * @tparam Unit Complete codec-specific parameter-set type. + * @param unit Parameter set to copy for FFmpeg's mutable writer interface. * @param codec_id FFmpeg codec identifier used to initialize the temporary context. * @return Serialized NAL unit bytes, or an empty buffer if FFmpeg rejects the fragment. */ - util::buffer_t write(std::uint8_t nal, void *uh, AVCodecID codec_id) { + template + util::buffer_t write(std::uint8_t nal, const Unit &unit, AVCodecID codec_id) { cbs::ctx_t cbs_ctx; ff_cbs_init(&cbs_ctx, codec_id, nullptr); - return write(cbs_ctx, nal, uh, codec_id); + return write(cbs_ctx, nal, unit, codec_id); } /** @@ -180,8 +186,8 @@ namespace cbs { 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) + write(write_ctx, sps->nal_unit_header.nal_unit_type, *sps, AV_CODEC_ID_H264), + write(ctx, sps_p->nal_unit_header.nal_unit_type, *sps_p, AV_CODEC_ID_H264) }; } @@ -249,13 +255,13 @@ namespace cbs { 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), + write(write_ctx, vps->nal_unit_header.nal_unit_type, *vps, AV_CODEC_ID_H265), + write(ctx, vps_p->nal_unit_header.nal_unit_type, *vps_p, 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), + write(write_ctx, sps->nal_unit_header.nal_unit_type, *sps, AV_CODEC_ID_H265), + write(ctx, sps_p->nal_unit_header.nal_unit_type, *sps_p, AV_CODEC_ID_H265), }, }; } diff --git a/src/config.cpp b/src/config.cpp index 10491fccc7e..2628e4aa922 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -273,7 +273,9 @@ namespace config { _CONVERT_(balanced); _CONVERT_(quality); _CONVERT_(speed); -#undef _CONVERT_ +#ifndef DOXYGEN + #undef _CONVERT_ +#endif return original; } @@ -295,7 +297,9 @@ namespace config { _CONVERT_(cqp); _CONVERT_(vbr_latency); _CONVERT_(vbr_peak); -#undef _CONVERT_ +#ifndef DOXYGEN + #undef _CONVERT_ +#endif return original; } @@ -318,7 +322,9 @@ namespace config { _CONVERT_(transcoding); _CONVERT_(ultralowlatency); _CONVERT_(webcam); -#undef _CONVERT_ +#ifndef DOXYGEN + #undef _CONVERT_ +#endif return original; } @@ -385,7 +391,9 @@ namespace config { _CONVERT_(fast); _CONVERT_(faster); _CONVERT_(veryfast); -#undef _CONVERT_ +#ifndef DOXYGEN + #undef _CONVERT_ +#endif return std::nullopt; } @@ -590,7 +598,9 @@ namespace config { _CONVERT_(veryfast, 10); _CONVERT_(superfast, 11); _CONVERT_(ultrafast, 12); -#undef _CONVERT_ +#ifndef DOXYGEN + #undef _CONVERT_ +#endif return 11; // Default to superfast } } // namespace sw @@ -613,7 +623,9 @@ namespace config { _CONVERT_(ensure_active); _CONVERT_(ensure_primary); _CONVERT_(ensure_only_display); -#undef _CONVERT_ +#ifndef DOXYGEN + #undef _CONVERT_ +#endif return video_t::dd_t::config_option_e::disabled; // Default to this if value is invalid } @@ -633,8 +645,10 @@ namespace config { _CONVERT_(disabled); _CONVERT_2_ARG_(auto, automatic); _CONVERT_(manual); -#undef _CONVERT_ -#undef _CONVERT_2_ARG_ +#ifndef DOXYGEN + #undef _CONVERT_ + #undef _CONVERT_2_ARG_ +#endif return video_t::dd_t::resolution_option_e::disabled; // Default to this if value is invalid } @@ -654,8 +668,10 @@ namespace config { _CONVERT_(disabled); _CONVERT_2_ARG_(auto, automatic); _CONVERT_(manual); -#undef _CONVERT_ -#undef _CONVERT_2_ARG_ +#ifndef DOXYGEN + #undef _CONVERT_ + #undef _CONVERT_2_ARG_ +#endif return video_t::dd_t::refresh_rate_option_e::disabled; // Default to this if value is invalid } @@ -674,8 +690,10 @@ namespace config { #endif _CONVERT_(disabled); _CONVERT_2_ARG_(auto, automatic); -#undef _CONVERT_ -#undef _CONVERT_2_ARG_ +#ifndef DOXYGEN + #undef _CONVERT_ + #undef _CONVERT_2_ARG_ +#endif return video_t::dd_t::hdr_option_e::disabled; // Default to this if value is invalid } @@ -742,20 +760,20 @@ namespace config { }, // 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) + std::to_underlying(amd::usage_h264_e::ultralowlatency), // usage (h264) + std::to_underlying(amd::usage_hevc_e::ultralowlatency), // usage (hevc) + std::to_underlying(amd::usage_av1_e::ultralowlatency), // usage (av1) + std::to_underlying(amd::rc_h264_e::vbr_latency), // rate control (h264) + std::to_underlying(amd::rc_hevc_e::vbr_latency), // rate control (hevc) + std::to_underlying(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) + std::to_underlying(amd::quality_h264_e::balanced), // quality (h264) + std::to_underlying(amd::quality_hevc_e::balanced), // quality (hevc) + std::to_underlying(amd::quality_av1_e::balanced), // quality (av1) 0, // preanalysis 1, // vbaq {}, // max_au_size (disabled by default) - (int) amd::coder_e::_auto, // coder + std::to_underlying(amd::coder_e::_auto), // coder }, // amd { @@ -1052,14 +1070,13 @@ namespace config { /** * @brief Parse Sunshine configuration text into key-value entries. */ - std::unordered_map parse_config(const std::string_view &file_content) { - std::unordered_map vars; + config_map_t parse_config(const std::string_view &file_content) { + config_map_t 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; @@ -1071,7 +1088,7 @@ namespace config { continue; } - vars.emplace(std::move(*var)); + vars.try_emplace(std::move(var->first), std::move(var->second)); } return vars; @@ -1110,15 +1127,15 @@ namespace config { * * @param vars Parsed configuration entries; consumed keys are erased. * @param name Configuration key to consume. - * @param input Destination field updated when the setting exists and parses successfully. + * @param destination Destination field updated when the setting exists and parses successfully. */ - void string_f(std::unordered_map &vars, const std::string &name, std::string &input) { + void string_f(config_map_t &vars, const std::string &name, std::string &destination) { auto it = vars.find(name); if (it == std::end(vars)) { return; } - input = std::move(it->second); + destination = std::move(it->second); vars.erase(it); } @@ -1128,15 +1145,15 @@ namespace config { * * @param vars Parsed configuration entries; consumed keys are erased. * @param name Configuration key to consume. - * @param input Destination field updated when the setting exists and parses successfully. + * @param destination Destination field updated when the setting exists and parses successfully. * @param f Converter applied to the raw configuration string. */ template - void generic_f(std::unordered_map &vars, const std::string &name, T &input, F &&f) { + void generic_f(config_map_t &vars, const std::string &name, T &destination, F &&f) { std::string tmp; string_f(vars, name, tmp); if (!tmp.empty()) { - input = f(tmp); + destination = f(tmp); } } @@ -1145,16 +1162,16 @@ namespace config { * * @param vars Parsed configuration entries; consumed keys are erased. * @param name Configuration key to consume. - * @param input Destination field updated when the setting exists and parses successfully. + * @param destination Destination field updated when the setting exists and parses successfully. * @param allowed_vals Accepted string values for this setting. */ - void string_restricted_f(std::unordered_map &vars, const std::string &name, std::string &input, const std::vector &allowed_vals) { + void string_restricted_f(config_map_t &vars, const std::string &name, std::string &destination, const std::vector &allowed_vals) { std::string temp; string_f(vars, name, temp); for (auto &allowed_val : allowed_vals) { if (temp == allowed_val) { - input = std::move(temp); + destination = std::move(temp); return; } } @@ -1167,7 +1184,7 @@ namespace config { * @param name Setting name. * @param output Parsed string list. */ - void string_list_f(std::unordered_map &vars, const std::string &name, std::vector &output) { // NOSONAR(cpp:S6045): transparent hasher not available for unordered_map in this codebase + void string_list_f(config_map_t &vars, const std::string &name, std::vector &output) { std::string temp; string_f(vars, name, temp); @@ -1193,9 +1210,9 @@ namespace config { * * @param vars Parsed configuration entries; consumed keys are erased. * @param name Configuration key to consume. - * @param input Destination field updated when the setting exists and parses successfully. + * @param destination Destination field updated when the setting exists and parses successfully. */ - void path_f(std::unordered_map &vars, const std::string &name, fs::path &input) { + void path_f(config_map_t &vars, const std::string &name, fs::path &destination) { // appdata needs to be retrieved once only static auto appdata = platf::appdata(); @@ -1203,14 +1220,14 @@ namespace config { string_f(vars, name, temp); if (!temp.empty()) { - input = temp; + destination = temp; } - if (input.is_relative()) { - input = appdata / input; + if (destination.is_relative()) { + destination = appdata / destination; } - auto dir = input; + auto dir = destination; dir.remove_filename(); // Ensure the directories exists @@ -1224,14 +1241,14 @@ namespace config { * * @param vars Parsed configuration entries; consumed keys are erased. * @param name Configuration key to consume. - * @param input Destination field updated when the setting exists and parses successfully. + * @param destination Destination field updated when the setting exists and parses successfully. */ - void path_f(std::unordered_map &vars, const std::string &name, std::string &input) { - fs::path temp = input; + void path_f(config_map_t &vars, const std::string &name, std::string &destination) { + fs::path temp = destination; path_f(vars, name, temp); - input = temp.string(); + destination = temp.string(); } /** @@ -1256,16 +1273,16 @@ namespace config { * * @param vars Parsed configuration entries; consumed keys are erased. * @param name Configuration key to consume. - * @param input Destination field updated when the setting exists and parses successfully. + * @param destination Destination field updated when the setting exists and parses successfully. */ - void int_f(std::unordered_map &vars, const std::string &name, int &input) { + void int_f(config_map_t &vars, const std::string &name, int &destination) { auto it = vars.find(name); if (it == std::end(vars)) { return; } - input = parse_config_integer(it->second); + destination = parse_config_integer(it->second); vars.erase(it); } @@ -1274,16 +1291,16 @@ namespace config { * * @param vars Parsed configuration entries; consumed keys are erased. * @param name Configuration key to consume. - * @param input Destination field updated when the setting exists and parses successfully. + * @param destination Destination field updated when the setting exists and parses successfully. */ - void int_f(std::unordered_map &vars, const std::string &name, std::optional &input) { + void int_f(config_map_t &vars, const std::string &name, std::optional &destination) { auto it = vars.find(name); if (it == std::end(vars)) { return; } - input = parse_config_integer(it->second); + destination = parse_config_integer(it->second); vars.erase(it); } @@ -1292,15 +1309,15 @@ namespace config { * * @param vars Parsed configuration entries; consumed keys are erased. * @param name Configuration key to consume. - * @param input Destination field updated when the setting exists and parses successfully. + * @param destination Destination field updated when the setting exists and parses successfully. * @param f Converter applied to the raw configuration string. */ template - void int_f(std::unordered_map &vars, const std::string &name, int &input, F &&f) { + void int_f(config_map_t &vars, const std::string &name, int &destination, F &&f) { std::string tmp; string_f(vars, name, tmp); if (!tmp.empty()) { - input = f(tmp); + destination = f(tmp); } } @@ -1309,15 +1326,15 @@ namespace config { * * @param vars Parsed configuration entries; consumed keys are erased. * @param name Configuration key to consume. - * @param input Destination field updated when the setting exists and parses successfully. + * @param destination Destination field updated when the setting exists and parses successfully. * @param f Converter applied to the raw configuration string. */ template - void int_f(std::unordered_map &vars, const std::string &name, std::optional &input, F &&f) { + void int_f(config_map_t &vars, const std::string &name, std::optional &destination, F &&f) { std::string tmp; string_f(vars, name, tmp); if (!tmp.empty()) { - input = f(tmp); + destination = f(tmp); } } @@ -1326,17 +1343,17 @@ namespace config { * * @param vars Parsed configuration entries; consumed keys are erased. * @param name Configuration key to consume. - * @param input Destination field updated when the setting exists and parses successfully. + * @param destination Destination field updated when the setting exists and parses successfully. * @param range Inclusive range accepted for the parsed value. */ - void int_between_f(std::unordered_map &vars, const std::string &name, int &input, const std::pair &range) { - int temp = input; + void int_between_f(config_map_t &vars, const std::string &name, int &destination, const std::pair &range) { + int temp = destination; int_f(vars, name, temp); TUPLE_2D_REF(lower, upper, range); if (temp >= lower && temp <= upper) { - input = temp; + destination = temp; } } @@ -1364,9 +1381,9 @@ namespace config { * * @param vars Parsed configuration entries; consumed keys are erased. * @param name Configuration key to consume. - * @param input Destination field updated when the setting exists and parses successfully. + * @param destination Destination field updated when the setting exists and parses successfully. */ - void bool_f(std::unordered_map &vars, const std::string &name, bool &input) { + void bool_f(config_map_t &vars, const std::string &name, bool &destination) { std::string tmp; string_f(vars, name, tmp); @@ -1374,7 +1391,7 @@ namespace config { return; } - input = to_bool(tmp); + destination = to_bool(tmp); } /** @@ -1382,9 +1399,9 @@ namespace config { * * @param vars Parsed configuration entries; consumed keys are erased. * @param name Configuration key to consume. - * @param input Destination field updated when the setting exists and parses successfully. + * @param destination Destination field updated when the setting exists and parses successfully. */ - void double_f(std::unordered_map &vars, const std::string &name, double &input) { + void double_f(config_map_t &vars, const std::string &name, double &destination) { std::string tmp; string_f(vars, name, tmp); @@ -1399,7 +1416,7 @@ namespace config { return; } - input = val; + destination = val; } /** @@ -1407,17 +1424,17 @@ namespace config { * * @param vars Parsed configuration entries; consumed keys are erased. * @param name Configuration key to consume. - * @param input Destination field updated when the setting exists and parses successfully. + * @param destination Destination field updated when the setting exists and parses successfully. * @param range Inclusive range accepted for the parsed value. */ - void double_between_f(std::unordered_map &vars, const std::string &name, double &input, const std::pair &range) { - double temp = input; + void double_between_f(config_map_t &vars, const std::string &name, double &destination, const std::pair &range) { + double temp = destination; double_f(vars, name, temp); TUPLE_2D_REF(lower, upper, range); if (temp >= lower && temp <= upper) { - input = temp; + destination = temp; } } @@ -1426,9 +1443,9 @@ namespace config { * * @param vars Parsed configuration entries; consumed keys are erased. * @param name Configuration key to consume. - * @param input Destination field updated when the setting exists and parses successfully. + * @param destination Destination field updated when the setting exists and parses successfully. */ - void list_string_f(std::unordered_map &vars, const std::string &name, std::vector &input) { + void list_string_f(config_map_t &vars, const std::string &name, std::vector &destination) { std::string string; string_f(vars, name, string); @@ -1436,7 +1453,7 @@ namespace config { return; } - input.clear(); + destination.clear(); auto begin = std::cbegin(string); if (*begin == '[') { @@ -1455,7 +1472,7 @@ namespace config { } else if (*pos == ']') { break; } else if (*pos == ',') { - input.emplace_back(begin, pos); + destination.emplace_back(begin, pos); pos = begin = std::find_if_not(pos + 1, std::cend(string), whitespace); } else { ++pos; @@ -1463,7 +1480,7 @@ namespace config { } if (pos != begin) { - input.emplace_back(begin, pos); + destination.emplace_back(begin, pos); } } @@ -1472,9 +1489,9 @@ namespace config { * * @param vars Parsed configuration entries; consumed keys are erased. * @param name Configuration key to consume. - * @param input Destination field updated when the setting exists and parses successfully. + * @param destination Destination field updated when the setting exists and parses successfully. */ - void list_prep_cmd_f(std::unordered_map &vars, const std::string &name, std::vector &input) { + void list_prep_cmd_f(config_map_t &vars, const std::string &name, std::vector &destination) { std::string string; string_f(vars, name, string); @@ -1496,7 +1513,7 @@ namespace config { 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)); + destination.emplace_back(do_cmd.value_or(""), undo_cmd.value_or(""), elevated.value_or(false)); } } @@ -1505,9 +1522,9 @@ namespace config { * * @param vars Parsed configuration entries; consumed keys are erased. * @param name Configuration key to consume. - * @param input Destination field updated when the setting exists and parses successfully. + * @param destination Destination field updated when the setting exists and parses successfully. */ - void list_int_f(std::unordered_map &vars, const std::string &name, std::vector &input) { + void list_int_f(config_map_t &vars, const std::string &name, std::vector &destination) { std::vector list; list_string_f(vars, name, list); @@ -1519,7 +1536,7 @@ namespace config { // 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(); + destination.clear(); for (auto &el : list) { std::string_view val = el; @@ -1536,7 +1553,7 @@ namespace config { } else { tmp = (int) util::from_view(val); } - input.emplace_back(tmp); + destination.emplace_back(tmp); } } @@ -1545,9 +1562,9 @@ namespace config { * * @param vars Parsed configuration entries; consumed keys are erased. * @param name Configuration key to consume. - * @param input Destination field updated when the setting exists and parses successfully. + * @param destination Destination field updated when the setting exists and parses successfully. */ - void map_int_int_f(std::unordered_map &vars, const std::string &name, std::unordered_map &input) { + void map_int_int_f(config_map_t &vars, const std::string &name, std::unordered_map &destination) { std::vector list; list_int_f(vars, name, list); @@ -1562,7 +1579,7 @@ namespace config { auto key = list[x++]; auto val = list[x++]; - input.emplace(key, val); + destination.emplace(key, val); } } @@ -1620,7 +1637,7 @@ namespace config { /** * @brief Log parsed configuration entries and optionally record them as modified. */ - void log_config_settings(const std::unordered_map &vars, bool save) { + void log_config_settings(const config_map_t &vars, bool save) { for (auto &[name, val] : vars) { bool is_redacted = std::ranges::find(config::redacted_config, name) != config::redacted_config.end(); @@ -1637,7 +1654,7 @@ namespace config { * * @param vars Parsed configuration entries; consumed keys are erased. */ - void apply_config(std::unordered_map &&vars) { + void apply_config(config_map_t &&vars) { log_config_settings(vars, true); int_f(vars, "qp", video.qp); @@ -2016,7 +2033,7 @@ namespace config { * @brief Parse serialized text into the corresponding runtime representation. */ int parse(int argc, char *argv[]) { - std::unordered_map cmd_vars; + config_map_t cmd_vars; #ifdef _WIN32 bool shortcut_launch = false; bool service_admin_launch = false; @@ -2068,7 +2085,7 @@ namespace config { cmd_vars.erase(it); } - cmd_vars.emplace(std::move(*var)); + cmd_vars.try_emplace(std::move(var->first), std::move(var->second)); } } } @@ -2087,7 +2104,7 @@ namespace config { 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)); + vars.insert_or_assign(name, std::move(value)); } // Apply the config. Note: This will try to create any paths diff --git a/src/config.h b/src/config.h index 6e194d8e4ee..a67f8be41bc 100644 --- a/src/config.h +++ b/src/config.h @@ -15,8 +15,14 @@ // local includes #include "nvenc/nvenc_config.h" +#include "string_hash.h" namespace config { + /** + * @brief Owning configuration entries supporting lookup by string view or C string. + */ + using config_map_t = std::unordered_map>; + namespace nv { /** * @brief Convert Sunshine's NVENC quality preset number to FFmpeg's stable symbolic name. @@ -39,7 +45,7 @@ namespace config { inline constexpr std::string_view GAMEPAD_DRIVER_NONE = "none"; ///< Disable virtual gamepads on Windows and macOS. // track modified config options - inline std::unordered_map modified_config_settings; ///< Configuration keys changed during the current parse or UI update. + inline config_map_t modified_config_settings; ///< Configuration keys changed during the current parse or UI update. // sensitive values that should be redacted from logging /** @@ -55,7 +61,7 @@ namespace config { * @param vars Parsed configuration entries to log. * @param save Whether modified configuration values should be written back to disk. */ - void log_config_settings(const std::unordered_map &vars, bool save); + void log_config_settings(const config_map_t &vars, bool save); /** * @brief Video encoder, capture, and color settings loaded from configuration. @@ -218,7 +224,9 @@ namespace config { bool config_revert_on_disconnect; ///< Specify whether to revert display configuration on client disconnect. mode_remapping_t mode_remapping; ///< Display mode remapping rules grouped by automatic selection mode. workarounds_t wa; ///< Display-device compatibility workarounds. - } dd; ///< Display-device integration settings. + }; + + dd_t dd; ///< Display-device integration settings. int max_bitrate; ///< Maximum bitrate ceiling in kbps for bitrate requested from the client. double minimum_fps_target; ///< Lowest framerate that will be used when streaming. Range 0-1000, 0 = half of client's requested framerate. @@ -359,7 +367,7 @@ namespace config { 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)) { + elevated(elevated) { } /** @@ -370,7 +378,7 @@ namespace config { */ explicit prep_cmd_t(std::string &&do_cmd, bool &&elevated): do_cmd(std::move(do_cmd)), - elevated(std::move(elevated)) { + elevated(elevated) { } std::string do_cmd; ///< Command to run before the application starts. @@ -400,7 +408,9 @@ namespace config { std::string name; ///< Executable name from the command line. int argc; ///< Number of command-line arguments. char **argv; ///< Command-line argument vector. - } cmd; ///< Command line used to launch the application. + }; + + cmd_t cmd; ///< Command line used to launch the application. std::uint16_t port; ///< TCP port used by Sunshine services. std::string address_family; ///< Address family requested for listening sockets. @@ -469,7 +479,7 @@ namespace config { * @param file_content Raw configuration file contents to parse. * @return Parsed configuration key-value entries. */ - std::unordered_map parse_config(const std::string_view &file_content); + config_map_t parse_config(const std::string_view &file_content); /** * @brief Persist a configuration option when the active file does not already define it. diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 28f0e5df2f5..ab2b3c98965 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -691,11 +691,11 @@ namespace confighttp { /** * @brief Send a 404 Not Found response. + * * @param response The HTTP response object. - * @param request The HTTP request object. * @param error_message The error message to include in the response. */ - void not_found(const resp_https_t &response, [[maybe_unused]] const req_https_t &request, const std::string &error_message) { + void not_found(const resp_https_t &response, const std::string &error_message) { constexpr auto code = SimpleWeb::StatusCode::client_error_not_found; nlohmann::json tree; @@ -712,11 +712,11 @@ namespace confighttp { /** * @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 to include in the response. */ - void bad_request(const resp_https_t &response, [[maybe_unused]] const req_https_t &request, const std::string &error_message) { + void bad_request(const resp_https_t &response, const std::string &error_message) { constexpr auto code = SimpleWeb::StatusCode::client_error_bad_request; nlohmann::json tree; @@ -738,7 +738,7 @@ namespace confighttp { bool check_content_type(const resp_https_t &response, const req_https_t &request, const std::string_view &contentType) { const auto requestContentType = request->header.find("content-type"); if (requestContentType == request->header.end()) { - bad_request(response, request, "Content type not provided"); + bad_request(response, "Content type not provided"); return false; } // Extract the media type part before any parameters (e.g., charset) @@ -755,7 +755,7 @@ namespace confighttp { boost::algorithm::to_lower(expectedContentType); if (actualContentType != expectedContentType) { - bad_request(response, request, "Content type mismatch"); + bad_request(response, "Content type mismatch"); return false; } return true; @@ -822,7 +822,7 @@ namespace confighttp { if (token_it == csrf_tokens.end()) { auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); BOOST_LOG(error) << "Web UI: ["sv << address << "] -- CSRF token validation failed: no token found for client"sv; - bad_request(response, request, "Invalid CSRF token"); + bad_request(response, "Invalid CSRF token"); return false; } @@ -830,14 +830,14 @@ namespace confighttp { csrf_tokens.erase(token_it); auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); BOOST_LOG(error) << "Web UI: ["sv << address << "] -- CSRF token validation failed: token expired"sv; - bad_request(response, request, "CSRF token expired"); + bad_request(response, "CSRF token expired"); return false; } if (token_it->second.token != provided_token) { auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); BOOST_LOG(error) << "Web UI: ["sv << address << "] -- CSRF token validation failed: token mismatch"sv; - bad_request(response, request, "Invalid CSRF token"); + bad_request(response, "Invalid CSRF token"); return false; } @@ -895,7 +895,7 @@ namespace confighttp { auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); BOOST_LOG(error) << "Web UI: ["sv << address << "] -- CSRF protection blocked request from origin: "sv << blocked_origin; BOOST_LOG(error) << "Web UI: To allow this origin, add it to the 'csrf_allowed_origins' option in your Sunshine configuration"sv; - bad_request(response, request, "Missing CSRF token"); + bad_request(response, "Missing CSRF token"); return false; } @@ -909,17 +909,17 @@ namespace confighttp { /** * @brief Validates the application index and sends an error response if invalid. */ - bool check_app_index(const resp_https_t &response, const req_https_t &request, int index) { + bool check_app_index(const resp_https_t &response, int index) { std::string file = file_handler::read_file(config::stream.file_apps.c_str()); nlohmann::json file_tree = nlohmann::json::parse(file); if (const auto &apps = file_tree["apps"]; index < 0 || index >= static_cast(apps.size())) { - std::string error; + std::string error_message; if (const int max_index = static_cast(apps.size()) - 1; max_index < 0) { - error = "No applications found"; + error_message = "No applications found"; } else { - error = std::format("'index' {} out of range, max index is {}", index, max_index); + error_message = std::format("'index' {} out of range, max index is {}", index, max_index); } - bad_request(response, request, error); + bad_request(response, error_message); return false; } return true; @@ -956,7 +956,7 @@ namespace confighttp { }; if (has_server_prefix("/api") || has_server_prefix("/assets") || has_server_prefix("/images")) { - not_found(response, request); + not_found(response); return; } @@ -1026,11 +1026,11 @@ namespace confighttp { // Don't do anything if the 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); + bad_request(response); return; } if (!fs::exists(filePath)) { - not_found(response, request); + not_found(response); return; } @@ -1040,7 +1040,7 @@ namespace confighttp { auto mimeType = mime_types.find(relPath.extension().string().substr(1)); // check if the extension is in the map at the x position if (mimeType == mime_types.end()) { - bad_request(response, request); + bad_request(response); return; } @@ -1131,7 +1131,7 @@ namespace confighttp { send_response(response, file_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "GetApps: "sv << e.what(); - bad_request(response, request, e.what()); + bad_request(response, e.what()); } } @@ -1231,7 +1231,7 @@ namespace confighttp { send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "SaveApp: "sv << e.what(); - bad_request(response, request, e.what()); + bad_request(response, e.what()); } } @@ -1285,7 +1285,7 @@ namespace confighttp { nlohmann::json new_apps = nlohmann::json::array(); const int index = std::stoi(request->path_match[1]); - if (!check_app_index(response, request, index)) { + if (!check_app_index(response, index)) { return; } @@ -1308,7 +1308,7 @@ namespace confighttp { send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "DeleteApp: "sv << e.what(); - bad_request(response, request, e.what()); + bad_request(response, e.what()); } } @@ -1395,7 +1395,7 @@ namespace confighttp { send_response(response, output_tree); } catch (nlohmann::json::exception &e) { BOOST_LOG(warning) << "Update Client: "sv << e.what(); - bad_request(response, request, e.what()); + bad_request(response, e.what()); } } @@ -1450,7 +1450,7 @@ namespace confighttp { send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "Unpair: "sv << e.what(); - bad_request(response, request, e.what()); + bad_request(response, e.what()); } } @@ -1583,7 +1583,7 @@ namespace confighttp { send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "SaveConfig: "sv << e.what(); - bad_request(response, request, e.what()); + bad_request(response, e.what()); } } @@ -1605,7 +1605,7 @@ namespace confighttp { try { const int index = std::stoi(request->path_match[1]); - if (!check_app_index(response, request, index)) { + if (!check_app_index(response, index)) { return; } @@ -1628,7 +1628,7 @@ namespace confighttp { // Check if we got the default image path (means validation failed or no image configured) if (validated_path == DEFAULT_APP_IMAGE_PATH) { BOOST_LOG(debug) << "Application at index " << index << " does not have a valid cover image"; - not_found(response, request, "Cover image not found"); + not_found(response, "Cover image not found"); return; } @@ -1636,7 +1636,7 @@ namespace confighttp { std::ifstream in(validated_path, std::ios::binary); if (!in) { BOOST_LOG(warning) << "Unable to read cover image file: " << validated_path; - bad_request(response, request, "Unable to read cover image file"); + bad_request(response, "Unable to read cover image file"); return; } @@ -1648,7 +1648,7 @@ namespace confighttp { response->write(SimpleWeb::StatusCode::success_ok, in, headers); } catch (std::exception &e) { BOOST_LOG(warning) << "GetCover: "sv << e.what(); - bad_request(response, request, e.what()); + bad_request(response, e.what()); } } @@ -1682,7 +1682,7 @@ namespace confighttp { std::string key = input_tree.value("key", ""); if (key.empty()) { - bad_request(response, request, "Cover key is required"); + bad_request(response, "Cover key is required"); return; } std::string url = input_tree.value("url", ""); @@ -1693,11 +1693,11 @@ namespace confighttp { std::basic_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"); + bad_request(response, "Only images.igdb.com is allowed"); return; } if (!http::download_file(url, path)) { - bad_request(response, request, "Failed to download cover"); + bad_request(response, "Failed to download cover"); return; } } else { @@ -1711,7 +1711,7 @@ namespace confighttp { send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "UploadCover: "sv << e.what(); - bad_request(response, request, e.what()); + bad_request(response, e.what()); } } @@ -1804,17 +1804,17 @@ namespace confighttp { if (!errors.empty()) { // join the errors array - std::string error = std::accumulate(errors.begin(), errors.end(), std::string(), [](const std::string &a, const std::string &b) { + std::string error_message = 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); + bad_request(response, error_message); return; } send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "SavePassword: "sv << e.what(); - bad_request(response, request, e.what()); + bad_request(response, e.what()); } } @@ -1885,7 +1885,7 @@ namespace confighttp { const nlohmann::json input_tree = nlohmann::json::parse(ss); const std::string pairing_id = input_tree.value("pairing_id", ""); if (!nvhttp::is_valid_pairing_id(pairing_id)) { - bad_request(response, request, "pairing_id must contain exactly 32 hexadecimal characters"); + bad_request(response, "pairing_id must contain exactly 32 hexadecimal characters"); return; } @@ -1894,7 +1894,7 @@ namespace confighttp { send_response(response, output_tree); } catch (nlohmann::json::exception &e) { BOOST_LOG(warning) << "CancelPairing: "sv << e.what(); - bad_request(response, request, e.what()); + bad_request(response, e.what()); } } @@ -1939,15 +1939,15 @@ namespace confighttp { const std::string pairing_id = input_tree.value("pairing_id", ""); const std::string pin = input_tree.value("pin", ""); if (!nvhttp::is_valid_pairing_id(pairing_id)) { - bad_request(response, request, "pairing_id must contain exactly 32 hexadecimal characters"); + bad_request(response, "pairing_id must contain exactly 32 hexadecimal characters"); return; } if (!nvhttp::is_valid_pairing_pin(pin)) { - bad_request(response, request, "PIN must contain exactly 4 numeric digits"); + bad_request(response, "PIN must contain exactly 4 numeric digits"); return; } if (!nvhttp::is_valid_pairing_name(name)) { - bad_request(response, request, "Client name must contain between 1 and 128 bytes"); + bad_request(response, "Client name must contain between 1 and 128 bytes"); return; } @@ -1956,7 +1956,7 @@ namespace confighttp { send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "SavePin: "sv << e.what(); - bad_request(response, request, e.what()); + bad_request(response, e.what()); } } @@ -2105,18 +2105,18 @@ namespace confighttp { try { const auto input = nlohmann::json::parse(request->content.string()); if (!input.is_object() || !input.contains("id") || !input["id"].is_string()) { - bad_request(response, request, "A permission ID is required"); + bad_request(response, "A permission ID is required"); return; } const bool requested = platf::request_permission(input["id"].get()); if (!requested) { - bad_request(response, request, "Unknown or unavailable permission"); + bad_request(response, "Unknown or unavailable permission"); return; } send_response(response, {{"status", true}}); } catch (const nlohmann::json::exception &) { - bad_request(response, request, "Invalid permission request"); + bad_request(response, "Invalid permission request"); } } @@ -2262,7 +2262,7 @@ namespace confighttp { auto license_key = input_tree.value("license_key", ""); input_tree["license_key"] = ""; if (license_key.empty()) { - bad_request(response, request, "License key is required"); + bad_request(response, "License key is required"); return; } @@ -2273,7 +2273,7 @@ namespace confighttp { } else if (action == "deactivate") { result = lvh::deactivate_license(); } else { - bad_request(response, request, "Unknown license action"); + bad_request(response, "Unknown license action"); return; } @@ -2290,7 +2290,7 @@ namespace confighttp { #endif send_response(response, build_virtualhid_license_status(result)); } catch (const nlohmann::json::exception &) { - bad_request(response, request, "Invalid license request"); + bad_request(response, "Invalid license request"); } } @@ -2458,7 +2458,7 @@ namespace confighttp { } if (!fs::is_directory(dir_path, ec)) { - bad_request(response, request, "Path is not a directory"); + bad_request(response, "Path is not a directory"); return; } @@ -2477,7 +2477,7 @@ namespace confighttp { send_response(response, output_tree); } catch (const fs::filesystem_error &e) { BOOST_LOG(warning) << "BrowseDirectory: "sv << e.what(); - bad_request(response, request, e.what()); + bad_request(response, e.what()); } } @@ -2500,9 +2500,9 @@ namespace confighttp { }; }; - // Default resource handlers - const https_handler_t bad_request_handler = [](const resp_https_t &response, const req_https_t &request) { - bad_request(response, request); + // Unsupported methods use the common HTTP handler signature and do not inspect the request. + const https_handler_t bad_request_handler = [](const resp_https_t &response, const req_https_t &) { + bad_request(response); }; // error by default server.default_resource["DELETE"] = bad_request_handler; @@ -2556,10 +2556,10 @@ namespace confighttp { const auto display_addr = net::get_bind_address_url_host(); - auto accept_and_run = [&](auto *server) { + auto accept_and_run = [&](auto *https_server) { try { platf::set_thread_name("confighttp::tcp"); - server->start([&display_addr](const unsigned short port) { + https_server->start([&display_addr](const unsigned short port) { BOOST_LOG(info) << "Configuration UI available at [https://"sv << display_addr << ":" << port << "]"; }); } catch (boost::system::system_error &err) { diff --git a/src/confighttp.h b/src/confighttp.h index 2e1e53d1c0a..1b8e9b2ff56 100644 --- a/src/confighttp.h +++ b/src/confighttp.h @@ -41,8 +41,8 @@ namespace confighttp { void send_unauthorized(const resp_https_t &response, const req_https_t &request); void send_redirect(const resp_https_t &response, const req_https_t &request, const char *path); bool authenticate(const resp_https_t &response, const req_https_t &request); - void not_found(const resp_https_t &response, const req_https_t &request, const std::string &error_message = "Not Found"); - void bad_request(const resp_https_t &response, const req_https_t &request, const std::string &error_message = "Bad Request"); + void not_found(const resp_https_t &response, const std::string &error_message = "Not Found"); + void bad_request(const resp_https_t &response, const std::string &error_message = "Bad Request"); /** * @brief Check content type. * @@ -64,14 +64,13 @@ namespace confighttp { bool validate_csrf_token(const resp_https_t &response, const req_https_t &request, const std::string &client_id); std::string get_client_id(const req_https_t &request); /** - * @brief Check app index. + * @brief Validate an application index and report an invalid index through the response. * * @param response HTTP response object to populate. - * @param request HTTP request data from the client. * @param index Zero-based index of the item being addressed. - * @return True when the request passes validation and processing may continue. + * @return True when the index identifies an existing application. */ - bool check_app_index(const resp_https_t &response, const req_https_t &request, int index); + bool check_app_index(const resp_https_t &response, int index); /** * @brief Serve the Web UI single-page application entry document. * @@ -275,7 +274,7 @@ namespace confighttp { /** * @brief File-extension to MIME-type mapping used when serving the Web UI. */ -const std::map mime_types = { +const std::map> mime_types = { {"css", "text/css"}, {"gif", "image/gif"}, {"htm", "text/html"}, diff --git a/src/crypto.cpp b/src/crypto.cpp index 5e39122de34..3e113b2b15c 100644 --- a/src/crypto.cpp +++ b/src/crypto.cpp @@ -13,17 +13,14 @@ namespace crypto { /** * @brief OpenSSL ASN.1 string pointer with automatic release. */ - using asn1_string_t = util::safe_ptr; + using asn1_string_t = util::safe_ptr; /** * @brief OpenSSL X.509 subject/issuer name pointer with automatic release. */ using x509_name_t = util::safe_ptr; constexpr char UNPAIRED_CERTIFICATE_ERROR[] = "Client certificate identity is not paired"; ///< Error returned when no exact stored certificate matches. - cert_chain_t::cert_chain_t(): - _certs {}, - _cert_ctx {X509_STORE_CTX_new()} { - } + cert_chain_t::cert_chain_t() = default; void cert_chain_t::add(x509_t &&cert) { x509_store_t x509_store {X509_STORE_new()}; @@ -67,9 +64,7 @@ namespace crypto { // 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) { + if (auto err = X509_verify_cert(_cert_ctx.get()); err == 1) { if (X509_cmp(trusted_cert.get(), cert) == 0) { return nullptr; } @@ -310,16 +305,16 @@ namespace crypto { 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} { + ecb_t::ecb_t(const aes_t &key_material, bool use_padding): + cipher_t {cipher_ctx_t {EVP_CIPHER_CTX_new()}, cipher_ctx_t {EVP_CIPHER_CTX_new()}, key_material, use_padding} { } - cbc_t::cbc_t(const aes_t &key, bool padding): - cipher_t {nullptr, nullptr, key, padding} { + cbc_t::cbc_t(const aes_t &key_material, bool use_padding): + cipher_t {nullptr, nullptr, key_material, use_padding} { } - gcm_t::gcm_t(const crypto::aes_t &key, bool padding): - cipher_t {nullptr, nullptr, key, padding} { + gcm_t::gcm_t(const crypto::aes_t &key_material, bool use_padding): + cipher_t {nullptr, nullptr, key_material, use_padding} { } } // namespace cipher @@ -407,8 +402,6 @@ namespace crypto { * @brief Return the certificate signature bytes. */ std::string_view signature(const x509_t &x) { - // X509_ALGOR *_ = nullptr; - const ASN1_BIT_STRING *asn1 = nullptr; X509_get0_signature(&asn1, nullptr, x.get()); diff --git a/src/crypto.h b/src/crypto.h index 6622c17d8d0..7274d34be88 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -44,39 +44,39 @@ namespace crypto { /** * @brief Owning pointer for an OpenSSL X.509 certificate. */ - using x509_t = util::safe_ptr; + using x509_t = util::safe_ptr; /** * @brief Owning pointer for an OpenSSL certificate store. */ - using x509_store_t = util::safe_ptr; + using x509_store_t = util::safe_ptr; /** * @brief Owning pointer for an OpenSSL certificate verification context. */ - using x509_store_ctx_t = util::safe_ptr; + using x509_store_ctx_t = util::safe_ptr; /** * @brief Owning pointer for an OpenSSL cipher context. */ - using cipher_ctx_t = util::safe_ptr; + using cipher_ctx_t = util::safe_ptr; /** * @brief Owning pointer for an OpenSSL message digest context. */ - using md_ctx_t = util::safe_ptr; + using md_ctx_t = util::safe_ptr; /** * @brief Owning pointer for an OpenSSL BIO chain. */ - using bio_t = util::safe_ptr; + using bio_t = util::safe_ptr; /** * @brief Owning pointer for an OpenSSL public or private key. */ - using pkey_t = util::safe_ptr; + using pkey_t = util::safe_ptr; /** * @brief Owning pointer for an OpenSSL key-generation context. */ - using pkey_ctx_t = util::safe_ptr; + using pkey_ctx_t = util::safe_ptr; /** * @brief Owning pointer for an OpenSSL BIGNUM. */ - using bignum_t = util::safe_ptr; + using bignum_t = util::safe_ptr; /** * @brief Hashes the given plaintext using SHA-256. @@ -174,7 +174,7 @@ namespace crypto { std::string rand_alphabet(std::size_t bytes, const std::string_view &alphabet = std::string_view {"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!%&()=-"}); /** - * @brief Owns the certificate chain returned by Sunshine's TLS certificate loader. + * @brief Owns trusted certificates and a verification context initialized for each chain. */ class cert_chain_t { public: @@ -202,7 +202,7 @@ namespace crypto { private: std::vector> _certs; - x509_store_ctx_t _cert_ctx; + x509_store_ctx_t _cert_ctx {X509_STORE_CTX_new()}; ///< Verification context created for each certificate chain. }; namespace cipher { @@ -249,12 +249,12 @@ namespace crypto { ecb_t &operator=(ecb_t &&) noexcept = default; /** - * @brief Construct an AES-ECB cipher helper with key material. + * @brief Explicitly construct an AES-ECB cipher helper with key material. * - * @param key AES key material used to initialize the cipher. - * @param padding Whether the cipher should use block padding. + * @param key_material AES key material used to initialize the cipher. + * @param use_padding Whether the cipher should use block padding. */ - ecb_t(const aes_t &key, bool padding = true); + explicit ecb_t(const aes_t &key_material, bool use_padding = true); /** * @brief Encrypt plaintext into the supplied ciphertext buffer. @@ -292,12 +292,12 @@ namespace crypto { gcm_t &operator=(gcm_t &&) noexcept = default; /** - * @brief Construct an AES-GCM cipher helper with key material and IV state. + * @brief Explicitly construct an AES-GCM cipher helper with key material and IV state. * - * @param key AES key material used to initialize the cipher. - * @param padding Whether the cipher should use block padding. + * @param key_material AES key material used to initialize the cipher. + * @param use_padding Whether the cipher should use block padding. */ - gcm_t(const crypto::aes_t &key, bool padding = true); + explicit gcm_t(const crypto::aes_t &key_material, bool use_padding = true); /** * @brief Encrypts the plaintext using AES GCM mode. @@ -348,12 +348,12 @@ namespace crypto { cbc_t &operator=(cbc_t &&) noexcept = default; /** - * @brief Construct an AES-CBC cipher helper with key material and IV state. + * @brief Explicitly construct an AES-CBC cipher helper with key material and IV state. * - * @param key AES key material used to initialize the cipher. - * @param padding Whether the cipher should use block padding. + * @param key_material AES key material used to initialize the cipher. + * @param use_padding Whether the cipher should use block padding. */ - cbc_t(const crypto::aes_t &key, bool padding = true); + explicit cbc_t(const crypto::aes_t &key_material, bool use_padding = true); /** * @brief Encrypts the plaintext using AES CBC mode. diff --git a/src/entry_handler.cpp b/src/entry_handler.cpp index c2249af0a11..1478093349b 100644 --- a/src/entry_handler.cpp +++ b/src/entry_handler.cpp @@ -131,11 +131,11 @@ namespace service_ctrl { class service_controller { public: /** - * @brief Open the Windows service manager and Sunshine service handle. + * @brief Explicitly open the Windows service manager and Sunshine service handle. * * @param service_desired_access SERVICE_* desired access flags. */ - service_controller(DWORD service_desired_access) { + explicit service_controller(DWORD service_desired_access) { scm_handle = OpenSCManagerA(nullptr, nullptr, SC_MANAGER_CONNECT); if (!scm_handle) { auto winerr = GetLastError(); @@ -271,8 +271,8 @@ namespace service_ctrl { } 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]; + for (DWORD entry_index = 0; entry_index < tcp_table->dwNumEntries; entry_index++) { + auto &entry = tcp_table->table[entry_index]; // Look for our port in the listening state if (entry.dwLocalPort == port_nbo && entry.dwState == MIB_TCP_STATE_LISTEN) { diff --git a/src/input.cpp b/src/input.cpp index 79b5e30a6c8..d54861edf1f 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -109,7 +109,7 @@ namespace input { /** * @brief Packed identifier for a pressed key and its modifier flags. */ - typedef uint32_t key_press_id_t; + using key_press_id_t = uint32_t; /** * @brief Create a key-press identifier from the virtual-key code and flags. @@ -212,12 +212,12 @@ namespace input { /** * @brief Release all platform resources associated with a virtual gamepad. * - * @param platf_input Platf input. + * @param platform_input Platf input. * @param id Identifier for the controller, session, display, or resource. */ - 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); + void free_gamepad(platf::input_t &platform_input, int id) { + platf::gamepad_update(platform_input, id, platf::gamepad_state_t {}); + platf::free_gamepad(platform_input, id); free_id(gamepadMask, id); } @@ -226,12 +226,10 @@ namespace input { * @brief Per-client gamepad slot and feedback state. */ struct gamepad_t { - gamepad_t(): - gamepad_state {}, - back_timeout_id {}, - id {-1}, - back_button_state {button_state_e::NONE} { - } + /** + * @brief Initialize an unassigned gamepad slot with no pending button timeout. + */ + gamepad_t() = default; ~gamepad_t() { if (id >= 0) { @@ -245,18 +243,18 @@ namespace input { } } - platf::gamepad_state_t gamepad_state; ///< Gamepad state. + platf::gamepad_state_t gamepad_state {}; ///< Gamepad state. - thread_pool_util::ThreadPool::task_id_t back_timeout_id; ///< Back timeout ID. + thread_pool_util::ThreadPool::task_id_t back_timeout_id {}; ///< Back timeout ID. - int id; ///< Global gamepad slot assigned to this client controller. + int id {-1}; ///< Global gamepad slot assigned to this client controller, or -1 when unassigned. // 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; ///< Back button state. + button_state_e back_button_state {button_state_e::NONE}; ///< Back button state. }; /** @@ -292,7 +290,7 @@ namespace input { }; /** - * @brief Construct input state from the mailbox and platform backend. + * @brief Construct input state with default controller slots and scroll accumulators. * * @param touch_port_event Event carrying the active touch port. * @param feedback_queue Queue used for controller feedback. @@ -301,26 +299,19 @@ namespace input { 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}, 0, 0, 0.0f, 0.0f, 1.0f, 1.0f, 0, 0}, - accumulated_vscroll_delta {}, - accumulated_hscroll_delta {} { + feedback_queue {std::move(feedback_queue)} { } // Keep track of alt+ctrl+shift key combo - int shortcutFlags; ///< Shortcut flags. + int shortcutFlags {}; ///< Shortcut flags. modifier_state_t shift_keys; ///< Client Shift keys contributing to the aggregate Shift flag. modifier_state_t control_keys; ///< Client Control keys contributing to the aggregate Control flag. modifier_state_t alt_keys; ///< Client Alt keys contributing to the aggregate Alt flag. - std::vector gamepads; ///< Virtual gamepad slots tracked for the stream. - std::unique_ptr client_context; ///< Client context. + std::vector gamepads {MAX_GAMEPADS}; ///< Virtual gamepad slots tracked for the stream. + std::unique_ptr client_context {platf::allocate_client_input_context(platf_input)}; ///< Client context. safe::mail_raw_t::event_t touch_port_event; ///< Touch port event. platf::feedback_queue_t feedback_queue; ///< Queue used to deliver controller feedback to the platform backend. @@ -329,12 +320,12 @@ namespace input { std::mutex input_queue_lock; ///< Input queue lock. bool input_task_scheduled {}; ///< Whether one worker task owns packet dispatch for this stream; guarded by input_queue_lock. - thread_pool_util::ThreadPool::task_id_t mouse_left_button_timeout; ///< Mouse left button timeout. + thread_pool_util::ThreadPool::task_id_t mouse_left_button_timeout {}; ///< Mouse left button timeout. - input::touch_port_t touch_port; ///< Touch coordinate bounds for the current stream. + input::touch_port_t touch_port {{0, 0, 0, 0, 0, 0}, 0, 0, 0.0f, 0.0f, 1.0f, 1.0f, 0, 0}; ///< Touch coordinate bounds for the current stream. - int32_t accumulated_vscroll_delta; ///< Accumulated vscroll delta. - int32_t accumulated_hscroll_delta; ///< Accumulated hscroll delta. + int32_t accumulated_vscroll_delta {}; ///< Accumulated vscroll delta. + int32_t accumulated_hscroll_delta {}; ///< Accumulated hscroll delta. }; /** @@ -684,10 +675,12 @@ namespace input { } /** - * @brief Write a debug log representation of the input packet. + * @brief Decode the input header and log the matching packet representation. + * + * @param payload Header at the start of the input packet. */ - void print(void *payload) { - auto header = (PNV_INPUT_HEADER) payload; + void print(PNV_INPUT_HEADER payload) { + auto header = payload; switch (util::endian::little(header->magic)) { case MOUSE_MOVE_REL_MAGIC_GEN5: @@ -954,9 +947,8 @@ namespace input { * 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) { + auto f = [input, release]() { + if (auto left_released = mouse_press[BUTTON_LEFT]; left_released) { // Already released left button return; } @@ -992,8 +984,7 @@ namespace input { * @return Remapped keycode when configured, otherwise the original keycode. */ short map_keycode(short keycode) { - auto it = config::input.keybindings.find(keycode); - if (it != std::end(config::input.keybindings)) { + if (auto it = config::input.keybindings.find(keycode); it != std::end(config::input.keybindings)) { return it->second; } @@ -1140,19 +1131,19 @@ namespace input { * @param key_code Moonlight keyboard packet key code. * @param release Whether the key or button event is a release. * @param flags Bit flags that modify the requested operation. - * @param synthetic_modifiers Synthetic modifiers. + * @param synthetic_modifiers Byte mask of modifiers to press around the key event. * @param extended Whether the client identified an extended key. */ - void send_key_and_modifiers(uint16_t key_code, bool release, uint8_t flags, uint8_t synthetic_modifiers, bool extended) { + void send_key_and_modifiers(uint16_t key_code, bool release, uint8_t flags, std::byte synthetic_modifiers, bool extended) { if (!release) { // Press any synthetic modifiers required for this key - if (synthetic_modifiers & MODIFIER_SHIFT) { + if ((synthetic_modifiers & std::byte {MODIFIER_SHIFT}) != std::byte {0}) { emit_keyboard_update(VKEY_SHIFT, false, flags); } - if (synthetic_modifiers & MODIFIER_CTRL) { + if ((synthetic_modifiers & std::byte {MODIFIER_CTRL}) != std::byte {0}) { emit_keyboard_update(VKEY_CONTROL, false, flags); } - if (synthetic_modifiers & MODIFIER_ALT) { + if ((synthetic_modifiers & std::byte {MODIFIER_ALT}) != std::byte {0}) { emit_keyboard_update(VKEY_MENU, false, flags); } } @@ -1161,13 +1152,13 @@ namespace input { if (!release) { // Raise any synthetic modifier keys we pressed - if (synthetic_modifiers & MODIFIER_SHIFT) { + if ((synthetic_modifiers & std::byte {MODIFIER_SHIFT}) != std::byte {0}) { emit_keyboard_update(VKEY_SHIFT, true, flags); } - if (synthetic_modifiers & MODIFIER_CTRL) { + if ((synthetic_modifiers & std::byte {MODIFIER_CTRL}) != std::byte {0}) { emit_keyboard_update(VKEY_CONTROL, true, flags); } - if (synthetic_modifiers & MODIFIER_ALT) { + if ((synthetic_modifiers & std::byte {MODIFIER_ALT}) != std::byte {0}) { emit_keyboard_update(VKEY_MENU, true, flags); } } @@ -1178,9 +1169,9 @@ namespace input { * * @param key_code Moonlight keyboard packet key code. * @param flags Bit flags that modify the requested operation. - * @param synthetic_modifiers Synthetic modifiers. + * @param synthetic_modifiers Byte mask of modifiers to press around the repeated key event. */ - void repeat_key(uint16_t key_code, uint8_t flags, uint8_t synthetic_modifiers) { + void repeat_key(uint16_t key_code, uint8_t flags, std::byte synthetic_modifiers) { // If key no longer pressed, stop repeating const auto state = key_press[make_kpid(key_code, flags)]; if (!state.pressed) { @@ -1217,16 +1208,16 @@ namespace input { // Set synthetic modifier flags if the keyboard packet is requesting modifier // keys that are not current pressed. - uint8_t synthetic_modifiers = 0; + std::byte synthetic_modifiers {0}; if (!release && !is_modifier(keyCode)) { if (!(input->shortcutFlags & input_t::SHIFT) && (modifiers & MODIFIER_SHIFT)) { - synthetic_modifiers |= MODIFIER_SHIFT; + synthetic_modifiers |= std::byte {MODIFIER_SHIFT}; } if (!(input->shortcutFlags & input_t::CTRL) && (modifiers & MODIFIER_CTRL)) { - synthetic_modifiers |= MODIFIER_CTRL; + synthetic_modifiers |= std::byte {MODIFIER_CTRL}; } if (!(input->shortcutFlags & input_t::ALT) && (modifiers & MODIFIER_ALT)) { - synthetic_modifiers |= MODIFIER_ALT; + synthetic_modifiers |= std::byte {MODIFIER_ALT}; } } @@ -2118,7 +2109,7 @@ namespace input { // 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); + std::scoped_lock lg(input->input_queue_lock); // If all entries have already been processed, nothing to do if (input->input_queue.empty()) { @@ -2152,7 +2143,7 @@ namespace input { } // Print the final input packet - input::print((void *) payload); + input::print(payload); #ifdef SUNSHINE_TESTS if (const auto &hook = input_packet_hook(); hook) { diff --git a/src/logging.cpp b/src/logging.cpp index 39a3f00713d..6e01979e137 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -7,6 +7,7 @@ #include #include #include +#include // lib includes #include @@ -66,9 +67,9 @@ namespace logging { */ void formatter(const boost::log::record_view &view, boost::log::formatting_ostream &os) { constexpr const char *message = "Message"; - constexpr const char *severity = "Severity"; + constexpr const char *severity_attribute = "Severity"; - auto log_level = view.attribute_values()[severity].extract().get(); + auto log_level = view.attribute_values()[severity_attribute].extract().get(); std::string_view log_type; switch (log_level) { @@ -95,7 +96,7 @@ namespace logging { log_type = "Tests: "sv; break; #endif - }; + } auto now = std::chrono::system_clock::now(); auto ms = std::chrono::duration_cast( @@ -203,7 +204,7 @@ namespace logging { } else { av_log_set_level(AV_LOG_DEBUG); } - av_log_set_callback([](void *ptr, int level, const char *fmt, va_list vl) { + av_log_set_callback([](void *ptr, int level, const char *fmt, va_list vl) { // NOSONAR(cpp:S5008): FFmpeg's av_log_set_callback requires a void pointer to its logging context. static int print_prefix = 1; std::array buffer; @@ -226,8 +227,8 @@ namespace logging { } 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)}; + constexpr int min_level {std::to_underlying(display_device::Logger::LogLevel::verbose)}; + constexpr int max_level {std::to_underlying(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); diff --git a/src/logging.h b/src/logging.h index cb33988b4dd..abbba5f11ab 100644 --- a/src/logging.h +++ b/src/logging.h @@ -123,8 +123,9 @@ namespace logging { void setup_av_logging(int min_log_level); /** - * @brief Setup logging for libdisplaydevice. - * @param min_log_level The log level. + * @brief Set up libdisplaydevice logging, clamping to its supported enum values. + * + * @param min_log_level Numeric minimum severity, clamped from verbose through fatal. */ void setup_libdisplaydevice_logging(int min_log_level); @@ -178,13 +179,13 @@ namespace logging { } /** - * @brief Collect a metric sample and write it to the periodic log when due. + * @brief Collect a metric sample and report it with this logger's configured message and units when due. * * @param value Numeric sample to include in the next periodic aggregate. */ void collect_and_log(const T &value) { if (enabled) { - auto print_info = [&](const T &min_value, const T &max_value, double avg_value) { + auto print_info = [this](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; diff --git a/src/main.cpp b/src/main.cpp index d8ae0a513f9..f34882e8bce 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -74,20 +74,20 @@ void on_signal(int sig, FN &&fn) { } /** - * @brief Cmd to func. + * @brief Dispatch commands through a common signature, ignoring arguments each command does not need. */ 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) { + {"help"sv, [](const char *name, int, char **) { return args::help(name); }}, - {"version"sv, [](const char *name, int argc, char **argv) { + {"version"sv, [](const char *, int, char **) { return args::version(); }}, #ifdef _WIN32 - {"restore-nvprefs-undo"sv, [](const char *name, int argc, char **argv) { + {"restore-nvprefs-undo"sv, [](const char *, int, char **) { return args::restore_nvprefs_undo(); }}, #endif diff --git a/src/move_by_copy.h b/src/move_by_copy.h index f6d1c2493e9..ffb6e1865f1 100644 --- a/src/move_by_copy.h +++ b/src/move_by_copy.h @@ -21,7 +21,7 @@ namespace move_by_copy_util { /** * @brief Wrapped type moved through copy-shaped APIs. */ - typedef T move_type; + using move_type = T; private: move_type _to_move; @@ -73,9 +73,11 @@ namespace move_by_copy_util { } /** - * @brief Move the wrapped object out of this helper. + * @brief Explicitly move the wrapped object out of this helper. + * + * @return Wrapped value whose ownership is transferred to the caller. */ - operator move_type() { + explicit operator move_type() { return std::move(_to_move); } }; diff --git a/src/network.cpp b/src/network.cpp index b04d6f888ef..16b86297350 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -18,15 +18,15 @@ namespace ip = boost::asio::ip; namespace net { /** - * @brief Pc ips v4. + * @brief IPv4 loopback networks used for local-client classification. */ - std::vector pc_ips_v4 { + std::vector pc_ips_v4 { ip::make_network_v4("127.0.0.0/8"sv), }; /** - * @brief Lan ips v4. + * @brief IPv4 private and link-local networks used for LAN classification. */ - std::vector lan_ips_v4 { + 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), @@ -35,15 +35,15 @@ namespace net { }; /** - * @brief Pc ips v6. + * @brief IPv6 loopback networks used for local-client classification. */ - std::vector pc_ips_v6 { + std::vector pc_ips_v6 { ip::make_network_v6("::1/128"sv), }; /** - * @brief Lan ips v6. + * @brief IPv6 private and link-local networks used for LAN classification. */ - std::vector lan_ips_v6 { + std::vector lan_ips_v6 { ip::make_network_v6("fc00::/7"sv), ip::make_network_v6("fe80::/64"sv), }; @@ -63,31 +63,29 @@ namespace net { } /** - * @brief Convert a Boost address family to Sunshine network enum value. + * @brief Classify an address using the configured loopback and LAN ranges without modifying them. */ 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 (auto addr = normalize_address(ip::make_address(view)); addr.is_v6()) { + for (const auto &range : pc_ips_v6) { if (range.hosts().find(addr.to_v6()) != range.hosts().end()) { return PC; } } - for (auto &range : lan_ips_v6) { + for (const auto &range : lan_ips_v6) { if (range.hosts().find(addr.to_v6()) != range.hosts().end()) { return LAN; } } } else { - for (auto &range : pc_ips_v4) { + for (const auto &range : pc_ips_v4) { if (range.hosts().find(addr.to_v4()) != range.hosts().end()) { return PC; } } - for (auto &range : lan_ips_v4) { + for (const auto &range : lan_ips_v4) { if (range.hosts().find(addr.to_v4()) != range.hosts().end()) { return LAN; } diff --git a/src/network.h b/src/network.h index 137dcdef8e3..f682fd35152 100644 --- a/src/network.h +++ b/src/network.h @@ -37,7 +37,7 @@ namespace net { /** * @brief Owning ENet host pointer released with `enet_host_destroy`. */ - using host_t = util::safe_ptr; + using host_t = util::safe_ptr; /** * @brief Raw ENet peer handle owned by an ENet host. */ @@ -45,7 +45,7 @@ namespace net { /** * @brief Owning ENet packet pointer released with `enet_packet_destroy`. */ - using packet_t = util::safe_ptr; + using packet_t = util::safe_ptr; /** * @brief Enumerates supported net options. diff --git a/src/nvenc/nvenc_base.cpp b/src/nvenc/nvenc_base.cpp index d20ca836cc7..42101b626b5 100644 --- a/src/nvenc/nvenc_base.cpp +++ b/src/nvenc/nvenc_base.cpp @@ -127,10 +127,6 @@ namespace { } switch (number) { - case 1: - default: - return NV_ENC_PRESET_P1_GUID; - case 2: return NV_ENC_PRESET_P2_GUID; @@ -148,6 +144,10 @@ namespace { case 7: return NV_ENC_PRESET_P7_GUID; + + case 1: + default: + return NV_ENC_PRESET_P1_GUID; } }; @@ -188,9 +188,7 @@ namespace NVENC_NAMESPACE { device_type(device_type) { } - nvenc_base::~nvenc_base() { - // Use destroy_encoder() instead - } + nvenc_base::~nvenc_base() = default; int nvenc_base::get_encoder_cap(const GUID &encode_guid, NV_ENC_CAPS cap) const { NV_ENC_CAPS_PARAM param = {.version = NV_ENC_CAPS_PARAM_VER}; @@ -590,10 +588,16 @@ namespace NVENC_NAMESPACE { encoder_params.rfi = true; NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS session_params = {.version = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER}; - session_params.device = device; + std::visit([&session_params](auto *native_device) { + session_params.device = native_device; + }, + device); session_params.deviceType = device_type; session_params.apiVersion = NVENCAPI_VERSION; - if (nvenc_failed(nvenc->nvEncOpenEncodeSessionEx(&session_params, &encoder))) { + void *native_encoder = nullptr; + const auto open_status = nvenc->nvEncOpenEncodeSessionEx(&session_params, &native_encoder); + encoder = static_cast(native_encoder); + if (nvenc_failed(open_status)) { BOOST_LOG(error) << "NvEnc: NvEncOpenEncodeSessionEx() failed: " << last_nvenc_error_string; return false; } diff --git a/src/nvenc/nvenc_base.h b/src/nvenc/nvenc_base.h index 8f9c8b01be3..ceccb51c54f 100644 --- a/src/nvenc/nvenc_base.h +++ b/src/nvenc/nvenc_base.h @@ -4,6 +4,9 @@ */ #pragma once +// standard includes +#include + // local includes #include "nvenc_colorspace.h" #include "nvenc_config.h" @@ -14,11 +17,22 @@ #include "src/video.h" #include "src/video_colorspace.h" +struct ID3D11Device; + /** * @brief Standalone NVENC encoder */ namespace NVENC_NAMESPACE { + /** + * @brief Opaque encoder session created and destroyed by the NVENC driver. + */ + struct encoder_session_t; + /** + * @brief Opaque native event registered for asynchronous NVENC completion. + */ + struct completion_event_t; + /** * @brief Abstract platform-agnostic base of standalone NVENC encoder. * Derived classes perform platform-specific operations. @@ -30,6 +44,9 @@ namespace NVENC_NAMESPACE { * @param device_type Underlying device type used by derived class. */ explicit nvenc_base(NV_ENC_DEVICE_TYPE device_type); + /** + * @brief Destroy the encoder wrapper; callers release the encoder session with destroy_encoder(). + */ ~nvenc_base() override; nvenc_base(const nvenc_base &) = delete; @@ -120,7 +137,7 @@ namespace NVENC_NAMESPACE { const NV_ENC_DEVICE_TYPE device_type; ///< NVENC device backend used by this encoder instance. - void *encoder = nullptr; ///< Opaque NVENC encoder session handle returned by the driver. + encoder_session_t *encoder = nullptr; ///< Opaque NVENC encoder session handle returned by the driver. struct { uint32_t width = 0; @@ -133,14 +150,13 @@ namespace NVENC_NAMESPACE { std::string last_nvenc_error_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::variant device {static_cast(nullptr)}; ///< Native D3D11 device or CUDA context, set by the derived backend. 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()`. + completion_event_t *async_event_handle = nullptr; ///< (optional) Native event registered for encode completion. + ///< Can be set in constructor or `init_library()`, must override `wait_for_async_event()`. private: /** diff --git a/src/nvenc/nvenc_d3d11.cpp b/src/nvenc/nvenc_d3d11.cpp index 64829e25ea5..d0f2bf94857 100644 --- a/src/nvenc/nvenc_d3d11.cpp +++ b/src/nvenc/nvenc_d3d11.cpp @@ -13,7 +13,7 @@ namespace NVENC_NAMESPACE { nvenc_d3d11::nvenc_d3d11(NV_ENC_DEVICE_TYPE device_type, ::nvenc::shared_dll dll): nvenc_base(device_type), dll(std::move(dll)) { - async_event_handle = CreateEvent(nullptr, FALSE, FALSE, nullptr); + async_event_handle = static_cast(CreateEvent(nullptr, FALSE, FALSE, nullptr)); } nvenc_d3d11::~nvenc_d3d11() { diff --git a/src/nvenc/nvenc_d3d11_on_cuda.cpp b/src/nvenc/nvenc_d3d11_on_cuda.cpp index 6738957da18..2cff61e1196 100644 --- a/src/nvenc/nvenc_d3d11_on_cuda.cpp +++ b/src/nvenc/nvenc_d3d11_on_cuda.cpp @@ -92,7 +92,10 @@ namespace NVENC_NAMESPACE { } } - return device != nullptr; + return std::visit([](auto *native_device) { + return native_device != nullptr; + }, + device); } bool nvenc_d3d11_on_cuda::create_and_register_input_buffer() { @@ -175,7 +178,7 @@ namespace NVENC_NAMESPACE { return false; } - auto unmap = [&]() -> bool { + auto unmap = [&]() { if (cuda_failed(cuda_functions.cuGraphicsUnmapResources(1, &cuda_d3d_input_texture, 0))) { BOOST_LOG(error) << "NvEnc: cuGraphicsUnmapResources() failed: error " << last_cuda_error; return false; diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index e3d49ccef5c..d11cb2b06b0 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -35,6 +35,7 @@ #include "platform/common.h" #include "process.h" #include "rtsp.h" +#include "string_hash.h" #include "system_tray.h" #include "utility.h" #include "uuid.h" @@ -74,8 +75,7 @@ namespace nvhttp { * @param private_key_file Path to the matching private key file. */ SunshineHTTPSServer(const std::string &certification_file, const std::string &private_key_file): - ServerBase::ServerBase(443), - context(boost::asio::ssl::context::tls_server) { + ServerBase::ServerBase(443) { // 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); @@ -87,64 +87,72 @@ namespace nvhttp { std::function, std::shared_ptr)> on_verify_failed; ///< Handler used to return the pairing challenge when client verification fails. protected: - boost::asio::ssl::context context; ///< TLS server context configured with Sunshine's certificate and protocol policy. + boost::asio::ssl::context context {boost::asio::ssl::context::tls_server}; ///< TLS server context configured with Sunshine's certificate and protocol policy. /** * @brief Enable client-certificate verification after the listening socket is bound. + * @details Ignore the TLS preverification result and context so failed authorization can receive an HTTP error. */ 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) { + context.set_verify_callback([](int, boost::asio::ssl::verify_context &) { // To respond with an error message, a connection must be established return 1; }); } } + /** + * @brief Process a completed TLS handshake under the callback's server lifetime guard. + * + * @param session Session whose TLS handshake has completed. + * @param ec Result of the asynchronous handshake. + * @pre The caller holds the session's handler scope guard. + */ + void handle_handshake(const std::shared_ptr &session, const SimpleWeb::error_code &ec) { + if (!ec) { + if (verify && !verify(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); + } + } + // This is Server::accept() with SSL validation support added /** * @brief Accept a pending connection and arm the server for the next client. + * @details Keep each handler's scope guard alive until all access to the server has finished. */ 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 (auto lock = connection->handler_runner->continue_lock(); lock) { + if (ec != SimpleWeb::error::operation_aborted) { + this->accept(); + } - 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 option_error; + session->connection->socket->lowest_layer().set_option(option, option_error); - 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->connection->socket->native_handle())) { - this->write(session, on_verify_failed); - } else { - this->read(session); + 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(); + if (auto lock = session->connection->handler_runner->continue_lock(); lock) { + this->handle_handshake(session, ec); } - } else if (this->on_error) { - this->on_error(session->request, ec); - } - }); - } else if (this->on_error) { - this->on_error(session->request, ec); + }); + } else if (this->on_error) { + this->on_error(session->request, ec); + } } }); } @@ -165,7 +173,9 @@ namespace nvhttp { struct conf_intern_t { std::string servercert; ///< Server certificate PEM string. std::string pkey; ///< Private key PEM string or path. - } conf_intern; ///< TLS credential paths loaded from Sunshine's runtime configuration. + }; + + conf_intern_t conf_intern; ///< TLS credential paths loaded from Sunshine's runtime configuration. /** * @brief Certificate entry associated with a client name and UUID. @@ -185,7 +195,7 @@ namespace nvhttp { }; // uniqueID, session - std::unordered_map map_id_sess; ///< Pairing sessions keyed by temporary unique ID. + std::unordered_map> map_id_sess; ///< Pairing sessions keyed by temporary unique ID. /** * @brief Get the mutex protecting pairing-session state. @@ -484,7 +494,7 @@ namespace nvhttp { std::copy(rikey.cbegin(), rikey.cend(), std::back_inserter(launch_session->gcm_key)); launch_session->host_audio = host_audio; - std::stringstream mode = std::stringstream(get_arg(args, "mode", "0x0x0")); + auto mode = std::stringstream(get_arg(args, "mode", "0x0x0")); // Split mode by the char "x", to populate width/height/fps int x = 0; std::string segment; @@ -621,10 +631,22 @@ namespace nvhttp { sess.async_insert_pin.id = pairing_id; sess.async_insert_pin.expires_at = now + PAIRING_SESSION_TIMEOUT; - map_id_sess.emplace(sess.client.uniqueID, std::move(sess)); + map_id_sess.try_emplace(sess.client.uniqueID, std::move(sess)); return pair_session_insert_e::ADDED; } + bool evict_abandoned_pair_session(const std::string &unique_id) { + std::scoped_lock lock {map_id_sess_mutex()}; + const auto sess_it = map_id_sess.find(unique_id); + if (sess_it == map_id_sess.end() || sess_it->second.last_phase == PAIR_PHASE::NONE) { + return false; + } + + complete_pairing(sess_it->second, false); + map_id_sess.erase(sess_it); + return true; + } + bool is_valid_pairing_id(const std::string_view pairing_id) { return pairing_id.size() == PAIRING_ID_SIZE && std::ranges::all_of(pairing_id, [](const char character) { return (character >= '0' && character <= '9') || (character >= 'a' && character <= 'f') || (character >= 'A' && character <= 'F'); @@ -713,7 +735,7 @@ namespace nvhttp { * @param tree XML property tree used for the response body. * @param status_msg Status msg. */ - void fail_pair(pair_session_t &sess, pt::ptree &tree, const std::string status_msg) { + 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); @@ -980,21 +1002,30 @@ namespace nvhttp { pt::ptree &tree, const std::string &unique_id ) { - pair_session_t sess; - sess.client.uniqueID = unique_id; - sess.client.cert = util::from_hex_vec(get_arg(args, "clientcert"), true); - sess.async_insert_pin.salt = get_arg(args, "salt"); - sess.async_insert_pin.device_name = get_arg(args, "devicename"); - sess.async_insert_pin.address = net::addr_to_normalized_string(request->remote_endpoint().address()); - - BOOST_LOG(debug) << sess.client.cert; const bool pin_stdin = config::sunshine.flags[config::flag::PIN_STDIN]; - if (!pin_stdin) { - sess.async_insert_pin.response = response; - } + auto make_session = [&]() { + pair_session_t fresh_sess; + fresh_sess.client.uniqueID = unique_id; + fresh_sess.client.cert = util::from_hex_vec(get_arg(args, "clientcert"), true); + fresh_sess.async_insert_pin.salt = get_arg(args, "salt"); + fresh_sess.async_insert_pin.device_name = get_arg(args, "devicename"); + fresh_sess.async_insert_pin.address = net::addr_to_normalized_string(request->remote_endpoint().address()); + if (!pin_stdin) { + fresh_sess.async_insert_pin.response = response; + } + + BOOST_LOG(debug) << fresh_sess.client.cert; + return fresh_sess; + }; std::string pairing_id; - switch (insert_pair_session(std::move(sess), pairing_id)) { + auto insert_result = insert_pair_session(make_session(), pairing_id); + if (insert_result == pair_session_insert_e::ALREADY_EXISTS && evict_abandoned_pair_session(unique_id)) { + // The previous handshake was abandoned mid-flight; the old session cannot resume, + // so replace it with a fresh one and retry the insert once. + insert_result = insert_pair_session(make_session(), pairing_id); + } + switch (insert_result) { using enum pair_session_insert_e; case ALREADY_EXISTS: @@ -1009,6 +1040,9 @@ namespace nvhttp { return false; case ADDED: break; + default: + // All enumerators are handled above; any other value cannot occur. + std::unreachable(); } if (!pin_stdin) { @@ -1051,7 +1085,7 @@ namespace nvhttp { pt::ptree tree; - auto fg = util::fail_guard([&]() { + auto fg = util::fail_guard([&tree, &response]() { std::ostringstream data; pt::write_xml(data, tree); @@ -1093,15 +1127,14 @@ namespace nvhttp { } bool pairing_complete = false; - args_t::const_iterator it; - if (it = args.find("clientchallenge"); it != std::end(args)) { - auto challenge = util::from_hex_vec(it->second, true); + if (const auto challenge_it = args.find("clientchallenge"); challenge_it != std::end(args)) { + auto challenge = util::from_hex_vec(challenge_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); + } else if (const auto response_it = args.find("serverchallengeresp"); response_it != std::end(args)) { + auto encrypted_response = util::from_hex_vec(response_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); + } else if (const auto secret_it = args.find("clientpairingsecret"); secret_it != std::end(args)) { + auto pairingsecret = util::from_hex_vec(secret_it->second, true); clientpairingsecret(sess_it->second, tree, pairingsecret); pairing_complete = true; } else { @@ -1113,7 +1146,7 @@ namespace nvhttp { } } - bool pin(const std::string_view pairing_id, std::string pin, std::string name) { + bool pin(const std::string_view pairing_id, const std::string &pin, std::string name) { if (!is_valid_pairing_id(pairing_id) || !is_valid_pairing_pin(pin) || !is_valid_pairing_name(name)) { return false; } @@ -1308,7 +1341,7 @@ namespace nvhttp { pt::ptree tree; - auto g = util::fail_guard([&]() { + auto g = util::fail_guard([&tree, &response]() { std::ostringstream data; pt::write_xml(data, tree); @@ -1327,7 +1360,7 @@ namespace nvhttp { app.put("AppTitle"s, proc.name); app.put("ID", proc.id); - apps.push_back(std::make_pair("App", std::move(app))); + apps.push_back(std::make_pair("App", app)); } } @@ -1558,7 +1591,7 @@ namespace nvhttp { print_req(request); pt::ptree tree; - auto g = util::fail_guard([&]() { + auto g = util::fail_guard([&tree, &response]() { std::ostringstream data; pt::write_xml(data, tree); @@ -1693,7 +1726,7 @@ namespace nvhttp { https_server.on_verify_failed = [](resp_https_t resp, req_https_t req) { pt::ptree tree; - auto g = util::fail_guard([&]() { + auto g = util::fail_guard([&tree, &resp]() { std::ostringstream data; pt::write_xml(data, tree); @@ -1735,11 +1768,11 @@ namespace nvhttp { http_server.config.address = net::get_bind_address(address_family); http_server.config.port = port_http; - auto accept_and_run = [&](auto *http_server) { + auto accept_and_run = [&](auto *web_server) { try { - std::string name = "nvhttp::" + std::to_string(http_server->config.port); + std::string name = "nvhttp::" + std::to_string(web_server->config.port); platf::set_thread_name(name); - http_server->start(); + web_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()) { @@ -1829,6 +1862,25 @@ namespace nvhttp { #ifdef SUNSHINE_TESTS namespace test_support { + std::unique_ptr> make_https_server( + const std::string &certificate_file, + const std::string &private_key_file, + std::shared_ptr &handler_scope + ) { + /** + * @brief Expose the production HTTPS handler scope to lifetime regression tests. + */ + class test_https_server_t: public SunshineHTTPSServer { + public: + using SunshineHTTPSServer::handler_runner; + using SunshineHTTPSServer::SunshineHTTPSServer; + }; + + auto server = std::make_unique(certificate_file, private_key_file); + handler_scope = server->handler_runner; + return server; + } + void pair_http( std::shared_ptr::Response> response, std::shared_ptr::Request> request diff --git a/src/nvhttp.h b/src/nvhttp.h index be360ce2926..2b31a35ffe7 100644 --- a/src/nvhttp.h +++ b/src/nvhttp.h @@ -215,6 +215,23 @@ namespace nvhttp { */ pair_session_insert_e insert_pair_session(pair_session_t sess, std::string &pairing_id); + /** + * @brief Evict an abandoned in-progress pairing session for a client. + * + * A fresh `getservercert` request means the client abandoned its previous handshake and + * started over: the phase machine never accepts `getservercert` twice for one session, so + * the old handshake cannot resume. Evicting it lets the retry proceed instead of failing + * with 409 until the session expires. Sessions still waiting for operator PIN approval + * (`last_phase == NONE`) are left untouched, so a pending approval cannot be displaced + * by a stray `getservercert`. Eviction completes the old session as failed, waking any + * PIN submitter waiting on it, and orphans no live HTTP response: the response is + * consumed when PIN approval advances the session past the certificate phase. + * + * @param unique_id Client-provided pairing-session identifier. + * @return `true` when an in-progress session was evicted. + */ + bool evict_abandoned_pair_session(const std::string &unique_id); + /** * @brief Remove pairing sessions whose completion deadline has passed. * @@ -317,7 +334,7 @@ namespace nvhttp { * bool pin_status = nvhttp::pin("0123456789abcdef0123456789abcdef", "1234", "laptop"); * @examples_end */ - bool pin(std::string_view pairing_id, std::string pin, std::string name); + bool pin(std::string_view pairing_id, const std::string &pin, std::string name); /** * @brief Remove single client. @@ -367,6 +384,20 @@ namespace nvhttp { * @brief Test-only accessors for paired-client authorization state. */ namespace test_support { + /** + * @brief Create the production HTTPS server and expose its callback lifetime guard. + * + * @param certificate_file Path to the server certificate PEM file. + * @param private_key_file Path to the matching private key PEM file. + * @param handler_scope Receives the scope runner that prevents destruction during callbacks. + * @return HTTPS server ready for test endpoint configuration. + */ + std::unique_ptr> make_https_server( + const std::string &certificate_file, + const std::string &private_key_file, + std::shared_ptr &handler_scope + ); + /** * @brief Dispatch a plain-HTTP pairing request through the production handler. * diff --git a/src/platform/common.h b/src/platform/common.h index adb97f3521e..62961d7a310 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -42,6 +42,8 @@ struct AVCodecContext; struct AVDictionary; #ifdef _WIN32 +struct ID3D11Device; + // 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 { @@ -63,7 +65,7 @@ namespace boost { /** * @brief Map of environment variable names to values. */ - typedef basic_environment environment; + using environment = basic_environment; } // namespace process::v1 } // namespace boost #endif @@ -373,7 +375,9 @@ namespace platf { _CONVERT(y410); _CONVERT(unknown); } -#undef _CONVERT +#ifndef DOXYGEN + #undef _CONVERT +#endif return "unknown"sv; } @@ -398,7 +402,7 @@ namespace platf { /** * @brief Bitset containing platform capability flags. */ - typedef uint32_t caps_t; + using caps_t = uint32_t; /** * @brief Capability bit indicating native pen and touch support. @@ -590,7 +594,15 @@ namespace platf { * @brief AVCodec-backed encode device and frame state. */ struct avcodec_encode_device_t: encode_device_t { - void *data {}; ///< Backend-specific conversion state. + bool hardware = false; ///< Whether this device supplies hardware frames instead of software conversion. + /** + * @brief Callback signature for backend-specific FFmpeg hardware device initialization. + */ + using init_hw_device_fn_t = int (*)(avcodec_encode_device_t *encode_device, AVBufferRef **hw_device_buf); + init_hw_device_fn_t init_hw_device = nullptr; ///< Optional backend-specific hardware device initializer. +#ifdef _WIN32 + ID3D11Device *d3d11_device = nullptr; ///< Native D3D11 device borrowed while the encode device is alive. +#endif AVFrame *frame {}; ///< FFmpeg frame currently owned by the encode device. /** @@ -607,17 +619,18 @@ namespace platf { * @brief Apply the configured colorspace metadata to the active frame. */ virtual void apply_colorspace() { + // Backends without additional colorspace state need no update here. } /** * @brief Set the frame to be encoded. - * @note Implementations must take ownership of 'frame'. + * @note Implementations must take ownership of 'new_frame'. * - * @param frame Video or graphics frame being processed. + * @param new_frame Video or graphics frame being processed. * @param hw_frames_ctx FFmpeg hardware frames context associated with the frame. * @return Status from updating frame. */ - virtual int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { + virtual int set_frame(AVFrame *new_frame, AVBufferRef *hw_frames_ctx) { BOOST_LOG(error) << "Illegal call to hwdevice_t::set_frame(). Did you forget to override it?"; return -1; }; @@ -628,16 +641,20 @@ namespace platf { * * @param frames FFmpeg hardware frames context to initialize. */ - virtual void init_hwframes(AVHWFramesContext *frames) {}; + virtual void init_hwframes(AVHWFramesContext *frames) { + // The base implementation leaves FFmpeg's hardware-frame parameters unchanged. + }; /** - * @brief Provides a hook for allow platform-specific code to adjust codec options. + * @brief Allow platform-specific code to adjust codec options. * @note Implementations may set or modify codec options prior to codec initialization. * * @param ctx Native context object used by the operation or callback. * @param options Request options or socket options to apply. */ - virtual void init_codec_options(AVCodecContext *ctx, AVDictionary **options) {}; + virtual void init_codec_options(AVCodecContext *ctx, AVDictionary **options) { + // The base implementation requires no platform-specific codec options. + }; /** * @brief Prepare to derive a context. @@ -675,7 +692,7 @@ namespace platf { reinit, ///< Need to reinitialize timeout, ///< Timeout interrupted, ///< Capture was interrupted - error ///< Error + capture_error ///< Capture failed. }; /** @@ -712,7 +729,7 @@ namespace platf { * 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::capture_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; @@ -1040,11 +1057,11 @@ namespace platf { boost::asio::ip::address &source_address; ///< Local source IP address for outgoing packets. /** - * @brief Returns a payload buffer descriptor for the given payload offset. + * @brief Locate a payload region without changing the batch descriptor or its buffers. * @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) { + buffer_descriptor_t buffer_for_payload_offset(ptrdiff_t offset) const { for (const auto &desc : payload_buffers) { if (offset < desc.size) { return { @@ -1060,12 +1077,12 @@ namespace platf { }; /** - * @brief Send multiple fixed-size UDP payload blocks using the platform backend. + * @brief Send fixed-size UDP payload blocks without modifying the batch descriptor. * * @param send_info Socket addresses, buffers, and sizes for the send operation. * @return True when all requested packet blocks are submitted to the socket. */ - bool send_batch(batched_send_info_t &send_info); + bool send_batch(const batched_send_info_t &send_info); /** * @brief Destination address and payload data for one UDP send. @@ -1223,7 +1240,7 @@ namespace platf { /** * @brief Per-client input context allocated by a platform backend. */ - typedef deinit_t client_input_t; + using client_input_t = deinit_t; /** * @brief Allocate a context to store per-client input data. @@ -1351,10 +1368,10 @@ namespace platf { virtual void sleep_for(const std::chrono::nanoseconds &duration) = 0; /** - * @brief Check if platform-specific timer backend has been initialized successfully + * @brief Explicitly check if platform-specific timer backend has been initialized successfully * @return `true` on success, `false` on error */ - virtual operator bool() = 0; + virtual explicit operator bool() = 0; }; /** diff --git a/src/platform/linux/audio.cpp b/src/platform/linux/audio.cpp index 43db87b1075..f13e0db7cc2 100644 --- a/src/platform/linux/audio.cpp +++ b/src/platform/linux/audio.cpp @@ -67,7 +67,7 @@ namespace platf { * @brief PulseAudio recording stream and channel metadata. */ struct mic_attr_t: public mic_t { - util::safe_ptr mic; ///< PulseAudio simple recording stream for microphone capture. + util::safe_ptr mic; ///< PulseAudio simple recording stream for microphone capture. /** * @brief Deliver a captured audio sample to Sunshine's audio pipeline. @@ -83,7 +83,7 @@ namespace platf { 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::capture_error; } return capture_e::ok; @@ -119,16 +119,15 @@ namespace platf { /** * @brief Create a microphone capture stream for the requested layout. + * @details Capture always uses the canonical channel layout; Opus mapping is applied by the encoder. * - * @param mapping Opus channel mapping table for the requested layout. Ignored: capture always - * uses the canonical layout, as on the WASAPI and macOS backends. * @param channels Number of audio channels in the stream. * @param sample_rate Audio sample rate in hertz. * @param frame_size Number of samples captured per audio frame. * @param source_name Source name. * @return Microphone capture object for the requested audio layout, or nullptr on failure. */ - std::unique_ptr microphone([[maybe_unused]] const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, std::string source_name) { + std::unique_ptr microphone(int channels, std::uint32_t sample_rate, std::uint32_t frame_size, std::string source_name) { // Capture in the canonical channel layout, not the one described by the Opus mapping. // // The Opus mapping is applied by the multistream encoder in audio.cpp and is undone by @@ -218,19 +217,19 @@ namespace platf { /** * @brief Owning pointer for a PulseAudio context. */ - using ctx_t = util::safe_ptr; + using ctx_t = util::safe_ptr; /** * @brief Owning pointer for a PulseAudio mainloop. */ - using loop_t = util::safe_ptr; + using loop_t = util::safe_ptr; /** * @brief Owning pointer for a PulseAudio asynchronous operation. */ - using op_t = util::safe_ptr; + using op_t = util::safe_ptr; /** * @brief Owning pointer for PulseAudio strings allocated with `pa_xmalloc`. */ - using string_t = util::safe_ptr>; + using string_t = util::safe_ptr>; /** * @brief Callback wrapper for PulseAudio introspection results without an end marker. @@ -246,7 +245,7 @@ namespace platf { * @param userdata Caller-provided pointer passed through the callback. */ template - void cb(ctx_t::pointer ctx, add_const_t i, void *userdata) { + void cb(ctx_t::pointer ctx, add_const_t i, void *userdata) { // NOSONAR(cpp:S5008): PulseAudio introspection callbacks require a void pointer for user data. auto &f = *(cb_simple_t *) userdata; // Cannot similarly filter on eol here. Unless reported otherwise assume @@ -269,7 +268,7 @@ namespace platf { * @param userdata Caller-provided pointer passed through the callback. */ template - void cb(ctx_t::pointer ctx, add_const_t i, int eol, void *userdata) { + void cb(ctx_t::pointer ctx, add_const_t i, int eol, void *userdata) { // NOSONAR(cpp:S5008): PulseAudio introspection callbacks require a void pointer for user data. auto &f = *(cb_t *) userdata; // For some reason, pulseaudio calls this callback after disconnecting @@ -282,12 +281,12 @@ namespace platf { /** * @brief Forward a PulseAudio integer callback value into a Sunshine alarm. + * @details The PulseAudio callback signature supplies a context that is not needed to ring the alarm. * - * @param ctx PulseAudio context that emitted the callback. * @param i Integer value returned by the PulseAudio operation. * @param userdata Caller-provided pointer passed through the callback. */ - void cb_i(ctx_t::pointer ctx, std::uint32_t i, void *userdata) { + void cb_i(ctx_t::pointer, std::uint32_t i, void *userdata) { auto alarm = (safe::alarm_raw_t *) userdata; alarm->ring(i); @@ -300,19 +299,19 @@ namespace platf { * @param userdata Caller-provided pointer passed through the callback. */ void ctx_state_cb(ctx_t::pointer ctx, void *userdata) { - auto &f = *(std::function *) userdata; + const auto &f = *(std::function *) userdata; f(ctx); } /** * @brief Record completion of a PulseAudio asynchronous operation. + * @details The PulseAudio callback signature supplies a context that is not needed to report completion. * - * @param ctx Native context object used by the operation or callback. * @param status Native status code returned by the platform API. * @param userdata Caller-provided pointer passed through the callback. */ - void success_cb(ctx_t::pointer ctx, int status, void *userdata) { + void success_cb(ctx_t::pointer, int status, void *userdata) { assert(userdata != nullptr); auto alarm = (safe::alarm_raw_t *) userdata; @@ -355,8 +354,8 @@ namespace platf { 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)) { + events_cb = std::make_unique>([this](ctx_t::pointer callback_ctx) { + switch (pa_context_get_state(callback_ctx)) { case PA_CONTEXT_READY: events->raise(ready); break; @@ -476,10 +475,10 @@ namespace platf { // 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) { + cb_t f = [&](ctx_t::pointer callback_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)); + BOOST_LOG(error) << "Couldn't get pulseaudio sink info: "sv << pa_strerror(pa_context_errno(callback_ctx)); alarm->ring(-1); } @@ -568,9 +567,9 @@ namespace platf { std::string sink_name; auto alarm = safe::make_alarm(); - cb_simple_t server_f = [&](ctx_t::pointer ctx, const pa_server_info *server_info) { + cb_simple_t server_f = [&](ctx_t::pointer callback_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)); + BOOST_LOG(error) << "Couldn't get pulseaudio server info: "sv << pa_strerror(pa_context_errno(callback_ctx)); alarm->ring(-1); } @@ -600,11 +599,11 @@ namespace platf { return monitor_name; } - cb_t sink_f = [&](ctx_t::pointer ctx, const pa_sink_info *sink_info, int eol) { + cb_t sink_f = [&](ctx_t::pointer callback_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)); + << "]: "sv << pa_strerror(pa_context_errno(callback_ctx)); alarm->ring(-1); } @@ -625,8 +624,8 @@ namespace platf { /** * @brief Create a microphone capture stream for the requested layout. + * @details The common audio interface supplies an Opus mapping that canonical capture does not need. * - * @param mapping Opus channel mapping table for the requested layout. * @param channels Number of audio channels in the stream. * @param sample_rate Audio sample rate in hertz. * @param frame_size Number of samples captured per audio frame. @@ -634,7 +633,7 @@ namespace platf { * @param host_audio_enabled Whether host playback should remain enabled during capture. * @return Microphone capture object for the requested audio layout. */ - std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, bool continuous_audio, [[maybe_unused]] bool host_audio_enabled) override { + std::unique_ptr microphone(const std::uint8_t *, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, bool continuous_audio, [[maybe_unused]] bool host_audio_enabled) override { // Sink choice priority: // 1. Config sink // 2. Last sink swapped to (Usually virtual in this case) @@ -650,7 +649,7 @@ namespace platf { sink_name = get_default_sink_name(); } - return ::platf::microphone(mapping, channels, sample_rate, frame_size, get_monitor_name(sink_name)); + return ::platf::microphone(channels, sample_rate, frame_size, get_monitor_name(sink_name)); } bool is_sink_available(const std::string &sink) override { diff --git a/src/platform/linux/cuda.cpp b/src/platform/linux/cuda.cpp index a87e987986f..ba4d11d69f0 100644 --- a/src/platform/linux/cuda.cpp +++ b/src/platform/linux/cuda.cpp @@ -5,9 +5,11 @@ // standard includes #include #include +#include #include #include #include +#include // lib includes #include @@ -170,7 +172,7 @@ namespace cuda { return -1; } - data = (void *) 0x1; + hardware = true; width = in_width; height = in_height; @@ -181,13 +183,13 @@ namespace cuda { /** * @brief Attach frame resources used by the next conversion or encode operation. * - * @param frame Video or graphics frame being processed. + * @param new_frame Video or graphics frame being processed. * @param hw_frames_ctx FFmpeg hardware frames context associated with the frame. * @return Status from updating frame. */ - int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { - this->hwframe.reset(frame); - this->frame = frame; + int set_frame(AVFrame *new_frame, AVBufferRef *hw_frames_ctx) override { + this->hwframe.reset(new_frame); + this->frame = new_frame; auto hwframe_ctx = (AVHWFramesContext *) hw_frames_ctx->data; @@ -196,8 +198,8 @@ namespace cuda { return -1; } - if (!frame->buf[0]) { - if (av_hwframe_get_buffer(hw_frames_ctx, frame, 0)) { + if (!new_frame->buf[0]) { + if (av_hwframe_get_buffer(hw_frames_ctx, new_frame, 0)) { BOOST_LOG(error) << "Couldn't get hwframe for NVENC"sv; return -1; } @@ -214,14 +216,14 @@ namespace cuda { cuda_ctx->stream = stream.get(); - auto sws_opt = sws_t::make(width, height, frame->width, frame->height, width * 4); + auto sws_opt = sws_t::make(width, height, new_frame->width, new_frame->height, width * 4); if (!sws_opt) { return -1; } sws = std::move(*sws_opt); - linear_interpolation = width != frame->width || height != frame->height; + linear_interpolation = width != new_frame->width || height != new_frame->height; return 0; } @@ -306,12 +308,12 @@ namespace cuda { /** * @brief Attach frame resources used by the next conversion or encode operation. * - * @param frame Video or graphics frame being processed. + * @param new_frame Video or graphics frame being processed. * @param hw_frames_ctx FFmpeg hardware frames context associated with the frame. * @return Status from updating frame. */ - int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { - if (cuda_t::set_frame(frame, hw_frames_ctx)) { + int set_frame(AVFrame *new_frame, AVBufferRef *hw_frames_ctx) override { + if (cuda_t::set_frame(new_frame, hw_frames_ctx)) { return -1; } @@ -347,6 +349,73 @@ namespace cuda { } }; + namespace { + bool cuda_is_initialized() { + return static_cast(cdf); + } + + CUresult cuda_get_device(CUdevice *device, int index) { + return cdf->cuDeviceGet(device, index); + } + + CUresult cuda_get_pci_bus_id(char *pci_bus_id, int len, CUdevice device) { + return cdf->cuDeviceGetPCIBusId(pci_bus_id, len, device); + } + + const cuda_pci_accessors_t cuda_pci_accessors { + .is_initialized = cuda_is_initialized, + .initialize = init, + .get_device = cuda_get_device, + .get_pci_bus_id = cuda_get_pci_bus_id, + }; + } // namespace + + /** + * @brief Query the PCI bus id of a CUDA device. + * @param index CUDA device index to query. + * @return Lowercase PCI bus id (e.g. "00000000:0f:00.0"), or no value when + * CUDA is unavailable or the query fails. + */ + std::optional device_pci_bus_id(int index) { + return device_pci_bus_id(index, cuda_pci_accessors); + } + + /** + * @brief Query the PCI bus id of a CUDA device with injected driver operations. + * @param index CUDA device index to query. + * @param accessors Driver operations used to initialize CUDA and query the device. + * @return Lowercase PCI bus id (e.g. "00000000:0f:00.0"), or no value when + * CUDA is unavailable or the query fails. + */ + std::optional device_pci_bus_id(int index, const cuda_pci_accessors_t &accessors) { + if (!accessors.is_initialized() && accessors.initialize() != 0) { + return std::nullopt; + } + + CUdevice device; + const auto device_status = accessors.get_device(&device, index); + if (device_status != CUDA_SUCCESS) { + BOOST_LOG(error) << "Couldn't get CUDA device: "sv << device_status; + return std::nullopt; + } + + // Room for both the legacy "DDDDDDDD:BB:DD.F" rendering and the short + // "DDDD:BB:DD.F" form current drivers return, plus NUL. + std::array pci_bus_id {}; + const auto bus_id_status = accessors.get_pci_bus_id(pci_bus_id.data(), pci_bus_id.size(), device); + if (bus_id_status != CUDA_SUCCESS) { + BOOST_LOG(error) << "Couldn't get CUDA device PCI bus ID: "sv << bus_id_status; + return std::nullopt; + } + + // Linux renders PCI addresses lowercase while CUDA uses uppercase. + std::string bus_id {pci_bus_id.data()}; + std::transform(bus_id.begin(), bus_id.end(), bus_id.begin(), [](char c) { + return static_cast(std::tolower(static_cast(c))); + }); + return bus_id; + } + /** * @brief Opens the DRM device associated with the CUDA device index. * @param index CUDA device index to open. @@ -363,7 +432,7 @@ namespace cuda { 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) { + std::ranges::transform(pci_bus_id, pci_bus_id.begin(), [](char c) { return std::tolower(c); }); @@ -412,13 +481,13 @@ namespace cuda { * @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. + * @param in_offset_x Offset of content in captured frame. + * @param in_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) { + int init(int in_width, int in_height, int in_offset_x, int in_offset_y) { // This must be non-zero to tell the video core that it's a hardware encoding device. - data = (void *) 0x1; + hardware = true; // TODO: Support more than one CUDA device file = std::move(open_drm_fd_for_cuda_device(0)); @@ -451,21 +520,21 @@ namespace cuda { sequence = 0; - this->offset_x = offset_x; - this->offset_y = offset_y; + this->offset_x = in_offset_x; + this->offset_y = in_offset_y; return 0; } /** * @brief Initialize color conversion into target CUDA frame. - * @param frame Destination CUDA frame to write into. + * @param new_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; + int set_frame(AVFrame *new_frame, AVBufferRef *hw_frames_ctx_buf) override { + this->hwframe.reset(new_frame); + this->frame = new_frame; auto hw_frames_ctx = (AVHWFramesContext *) hw_frames_ctx_buf->data; @@ -474,8 +543,8 @@ namespace cuda { return -1; } - if (!frame->buf[0]) { - if (av_hwframe_get_buffer(hw_frames_ctx_buf, frame, 0)) { + if (!new_frame->buf[0]) { + if (av_hwframe_get_buffer(hw_frames_ctx_buf, new_frame, 0)) { BOOST_LOG(error) << "Couldn't get hwframe for NVENC_GL"sv; return -1; } @@ -484,7 +553,7 @@ namespace cuda { sw_format = hw_frames_ctx->sw_format; is_yuv444 = (sw_format == AV_PIX_FMT_YUV444P || sw_format == AV_PIX_FMT_YUV444P16LE); - auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height, sw_format, is_yuv444); + auto sws_opt = egl::sws_t::make(width, height, new_frame->width, new_frame->height, sw_format, is_yuv444); if (!sws_opt) { return -1; } @@ -492,13 +561,13 @@ namespace cuda { this->sws = std::move(*sws_opt); if (is_yuv444) { - auto yuv444_opt = egl::create_yuv444_target(frame->width, frame->height, sw_format); + auto yuv444_opt = egl::create_yuv444_target(new_frame->width, new_frame->height, sw_format); if (!yuv444_opt) { return -1; } this->yuv444 = std::move(*yuv444_opt); } else { - auto nv12_opt = egl::create_nv12_target(frame->width, frame->height, sw_format); + auto nv12_opt = egl::create_nv12_target(new_frame->width, new_frame->height, sw_format); if (!nv12_opt) { return -1; } @@ -697,11 +766,16 @@ namespace cuda { static PNVFBCCREATEINSTANCE createInstance {}; static NVFBC_API_FUNCTION_LIST func {.dwVersion = NVFBC_VERSION}; - static constexpr inline NVFBC_BOOL nv_bool(bool b) { + /** + * @brief Convert a C++ boolean to the corresponding NvFBC flag. + * @param b Boolean state to convert. + * @return NVFBC_TRUE when b is true, otherwise NVFBC_FALSE. + */ + static constexpr NVFBC_BOOL nv_bool(bool b) { return b ? NVFBC_TRUE : NVFBC_FALSE; } - static void *handle {nullptr}; + static dyn::library_t *library_handle {nullptr}; ///< Loaded NvFBC library retained while its function table is in use. /** * @brief Load NvFBC and create the CUDA capture helper. @@ -715,9 +789,9 @@ namespace cuda { return 0; } - if (!handle) { - handle = dyn::handle({"libnvidia-fbc.so.1", "libnvidia-fbc.so"}); - if (!handle) { + if (!library_handle) { + library_handle = dyn::handle({"libnvidia-fbc.so.1", "libnvidia-fbc.so"}); + if (!library_handle) { return -1; } } @@ -726,9 +800,9 @@ namespace cuda { {(dyn::apiproc *) &createInstance, "NvFBCCreateInstance"}, }; - if (dyn::load(handle, funcs)) { - dlclose(handle); - handle = nullptr; + if (dyn::load(library_handle, funcs)) { + dlclose(library_handle); + library_handle = nullptr; return -1; } @@ -737,8 +811,8 @@ namespace cuda { if (status) { BOOST_LOG(error) << "Unable to create NvFBC instance"sv; - dlclose(handle); - handle = nullptr; + dlclose(library_handle); + library_handle = nullptr; return -1; } @@ -752,18 +826,18 @@ namespace cuda { class ctx_t { public: /** - * @brief Create an NvFBC session context for a native capture handle. + * @brief Explicitly create an NvFBC session context for a native capture handle. * - * @param handle Native library or object handle used by the operation. + * @param session_handle Native library or object handle used by the operation. */ - ctx_t(NVFBC_SESSION_HANDLE handle) { + explicit ctx_t(NVFBC_SESSION_HANDLE session_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); + if (func.nvFBCBindContext(session_handle, ¶ms)) { + BOOST_LOG(error) << "Couldn't bind NvFBC context to current thread: " << func.nvFBCGetLastErrorStr(session_handle); } - this->handle = handle; + this->handle = session_handle; } ~ctx_t() { @@ -1049,7 +1123,7 @@ namespace cuda { 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::capture_error: case platf::capture_e::interrupted: return status; case platf::capture_e::timeout: @@ -1063,7 +1137,7 @@ namespace cuda { } break; default: - BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; + BOOST_LOG(error) << "Unrecognized capture status ["sv << std::to_underlying(status) << ']'; return status; } } @@ -1080,7 +1154,7 @@ namespace cuda { */ platf::capture_e reinit(bool cursor) { if (handle.stop()) { - return platf::capture_e::error; + return platf::capture_e::capture_error; } cursor_visible = cursor; @@ -1095,19 +1169,19 @@ namespace cuda { } if (handle.capture(capture_params)) { - return platf::capture_e::error; + return platf::capture_e::capture_error; } // If trying to capture directly, test if it actually does. if (capture_params.bAllowDirectCapture) { CUdeviceptr device_ptr; - NVFBC_FRAME_GRAB_INFO info; + NVFBC_FRAME_GRAB_INFO grab_info; NVFBC_TOCUDA_GRAB_FRAME_PARAMS grab { NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER, NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT, &device_ptr, - &info, + &grab_info, 0, }; @@ -1120,17 +1194,17 @@ namespace cuda { BOOST_LOG(error) << "Couldn't capture nvFramebuffer: "sv << handle.last_error(); - return platf::capture_e::error; + return platf::capture_e::capture_error; } - if (info.bDirectCapture) { + if (grab_info.bDirectCapture) { break; } BOOST_LOG(debug) << "Direct capture failed attempt ["sv << x << ']'; } - if (!info.bDirectCapture) { + if (!grab_info.bDirectCapture) { BOOST_LOG(debug) << "Direct capture failed, trying the extra copy method"sv; // Direct capture failed capture_params.bPushModel = nv_bool(false); @@ -1138,7 +1212,7 @@ namespace cuda { capture_params.bAllowDirectCapture = nv_bool(false); if (handle.stop() || handle.capture(capture_params)) { - return platf::capture_e::error; + return platf::capture_e::capture_error; } } } @@ -1164,13 +1238,13 @@ namespace cuda { } CUdeviceptr device_ptr; - NVFBC_FRAME_GRAB_INFO info; + NVFBC_FRAME_GRAB_INFO grab_info; NVFBC_TOCUDA_GRAB_FRAME_PARAMS grab { NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER, NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT, &device_ptr, - &info, + &grab_info, (std::uint32_t) timeout.count(), }; @@ -1180,7 +1254,7 @@ namespace cuda { } BOOST_LOG(error) << "Couldn't capture nvFramebuffer: "sv << handle.last_error(); - return platf::capture_e::error; + return platf::capture_e::capture_error; } if (!pull_free_image_cb(img_out)) { @@ -1189,7 +1263,7 @@ namespace cuda { 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::capture_error; } return platf::capture_e::ok; diff --git a/src/platform/linux/cuda.cu b/src/platform/linux/cuda.cu index fbc5401fe3a..db70b3f7151 100644 --- a/src/platform/linux/cuda.cu +++ b/src/platform/linux/cuda.cu @@ -292,7 +292,7 @@ namespace cuda { texture {INVALID_TEXTURE, INVALID_TEXTURE} { } - tex_t::tex_t(tex_t &&other): + tex_t::tex_t(tex_t &&other) noexcept: array {other.array}, texture {other.texture} { other.array = 0; @@ -300,7 +300,7 @@ namespace cuda { other.texture.linear = INVALID_TEXTURE; } - tex_t &tex_t::operator=(tex_t &&other) { + tex_t &tex_t::operator=(tex_t &&other) noexcept { std::swap(array, other.array); std::swap(texture, other.texture); diff --git a/src/platform/linux/cuda.h b/src/platform/linux/cuda.h index 7149e76072f..80c222a8de5 100644 --- a/src/platform/linux/cuda.h +++ b/src/platform/linux/cuda.h @@ -12,6 +12,9 @@ #include #include + // lib includes + #include + // local includes #include "src/video_colorspace.h" @@ -38,14 +41,50 @@ namespace cuda { */ std::unique_ptr make_avcodec_gl_encode_device(int width, int height, int offset_x, int offset_y); + /** + * @brief Functions used to query the PCI bus id of a CUDA device. + */ + struct cuda_pci_accessors_t { + bool (*is_initialized)(); ///< Report whether the CUDA function table is loaded. + int (*initialize)(); ///< Load the CUDA function table, returning 0 on success. + tcuDeviceGet *get_device; ///< Resolve a CUDA device index, returning CUDA_SUCCESS on success. + tcuDeviceGetPCIBusId *get_pci_bus_id; ///< Write the device bus id, returning CUDA_SUCCESS on success. + }; + + /** + * @brief Query the PCI bus id of a CUDA device. + * @param index CUDA device index to query. + * @return Lowercase PCI bus id (e.g. "00000000:0f:00.0"), or no value when + * CUDA is unavailable or the query fails. + */ + std::optional device_pci_bus_id(int index); + + /** + * @brief Query the PCI bus id of a CUDA device with injected driver operations. + * @param index CUDA device index to query. + * @param accessors Driver operations used to initialize CUDA and query the device. + * @return Lowercase PCI bus id (e.g. "00000000:0f:00.0"), or no value when + * CUDA is unavailable or the query fails. + */ + std::optional device_pci_bus_id(int index, const cuda_pci_accessors_t &accessors); + int init(); } // namespace cuda -typedef struct cudaArray *cudaArray_t; +/** + * @brief Opaque CUDA array handle shared by host and CUDA compiler declarations. + */ +using cudaArray_t = struct cudaArray *; #if !defined(__CUDACC__) -typedef struct CUstream_st *cudaStream_t; -typedef unsigned long long cudaTextureObject_t; +/** + * @brief Opaque CUDA stream handle for host compilation. + */ +using cudaStream_t = struct CUstream_st *; +/** + * @brief CUDA texture object identifier for host compilation. + */ +using cudaTextureObject_t = unsigned long long; #else /* defined(__CUDACC__) */ typedef __location__(device_builtin) struct CUstream_st *cudaStream_t; typedef __location__(device_builtin) unsigned long long cudaTextureObject_t; @@ -80,9 +119,9 @@ namespace cuda { static std::optional make(int height, int pitch); tex_t(); - tex_t(tex_t &&); + tex_t(tex_t &&) noexcept; - tex_t &operator=(tex_t &&other); + tex_t &operator=(tex_t &&other) noexcept; ~tex_t(); @@ -90,10 +129,15 @@ namespace cuda { cudaArray_t array; - struct texture { + /** + * @brief CUDA texture objects for point and linear sampling of the same image. + */ + struct texture_t { cudaTextureObject_t point; cudaTextureObject_t linear; - } texture; + }; + + texture_t texture; ///< Point and linear sampling textures for the CUDA array. }; class sws_t { diff --git a/src/platform/linux/graphics.cpp b/src/platform/linux/graphics.cpp index 74a4c8b24da..24b33139ca4 100644 --- a/src/platform/linux/graphics.cpp +++ b/src/platform/linux/graphics.cpp @@ -7,6 +7,7 @@ #include #include #include +#include // local includes #include "graphics.h" @@ -307,7 +308,7 @@ namespace gbm { * @brief Load GBM symbols required for EGL device creation. */ int init() { - static void *handle {nullptr}; + static dyn::library_t *handle {nullptr}; static bool funcs_loaded = false; if (funcs_loaded) { @@ -513,7 +514,7 @@ namespace egl { return nullptr; } - // native_display.left() equals native_display.right() + // Initialize EGL with the selected platform and its native display handle. EGLDisplay raw_display = EGL_NO_DISPLAY; if (eglGetPlatformDisplayEXT) { @@ -557,7 +558,7 @@ namespace egl { }; for (auto ext : extensions) { - if (!std::strstr(extension_st, ext)) { + if (!std::string_view {extension_st}.contains(ext)) { BOOST_LOG(error) << "Missing extension: ["sv << ext << ']'; return nullptr; } @@ -1341,7 +1342,7 @@ namespace egl { } int sws_t::blank(gl::frame_buf_t &fb, int offsetX_, int offsetY_, int width, int height, bool is_yuv444) { - auto f = [&]() { + auto f = [this, &offsetX_, &offsetY_, &width, &height]() { std::swap(offsetX_, this->offsetX); std::swap(offsetY_, this->offsetY); std::swap(width, this->out_width); diff --git a/src/platform/linux/graphics.h b/src/platform/linux/graphics.h index fbcdc728a27..7b33ad8b236 100644 --- a/src/platform/linux/graphics.h +++ b/src/platform/linux/graphics.h @@ -63,7 +63,7 @@ void free_frame(AVFrame *frame); /** * @brief Owning pointer for an EGL frame object. */ -using frame_t = util::safe_ptr; +using frame_t = util::safe_ptr; namespace gl { extern GladGLContext ctx; @@ -358,11 +358,11 @@ namespace gbm { /** * @brief Function pointer used to destroy a GBM device. */ - typedef void (*device_destroy_fn)(device *gbm); + using device_destroy_fn = void (*)(device *gbm); /** * @brief Function pointer used to create a GBM device from a file descriptor. */ - typedef device *(*create_device_fn)(int fd); + using create_device_fn = device *(*)(int fd); extern device_destroy_fn device_destroy; extern create_device_fn create_device; @@ -408,7 +408,6 @@ namespace egl { gl::tex_t tex; ///< Textures containing the imported Y and UV planes. gl::frame_buf_t buf; ///< OpenGL framebuffer object used for rendering. - // sizeof(va::DRMPRIMESurfaceDescriptor::objects) / sizeof(va::DRMPRIMESurfaceDescriptor::objects[0]); static constexpr std::size_t num_fds = 4; ///< Maximum number of DMA-BUF plane file descriptors exported by VAAPI. std::array fds; ///< DMA-BUF file descriptors for each exported plane. @@ -616,11 +615,11 @@ namespace egl { * @brief Reset the object to its initial empty state. */ void reset() { - for (auto x = 0; x < 4; ++x) { - if (sd.fds[x] >= 0) { - close(sd.fds[x]); + for (auto fd_index = 0; fd_index < 4; ++fd_index) { + if (sd.fds[fd_index] >= 0) { + close(sd.fds[fd_index]); - sd.fds[x] = -1; + sd.fds[fd_index] = -1; } } } diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp index 988f5c96417..0274f0b755a 100644 --- a/src/platform/linux/kmsgrab.cpp +++ b/src/platform/linux/kmsgrab.cpp @@ -12,6 +12,7 @@ #include #include #include +#include // platform includes #include @@ -237,9 +238,9 @@ namespace platf { fb {fb}, fb_id {fb->fb_id}, width {fb->width}, - height {fb->height} { - pixel_format = DRM_FORMAT_XRGB8888; - modifier = DRM_FORMAT_MOD_INVALID; + height {fb->height}, + pixel_format {DRM_FORMAT_XRGB8888}, + modifier {DRM_FORMAT_MOD_INVALID} { handles.fill(0); pitches.fill(0); offsets.fill(0); @@ -258,10 +259,9 @@ namespace platf { 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; - + height {fb2->height}, + pixel_format {fb2->pixel_format}, + modifier {(fb2->flags & DRM_MODE_FB_MODIFIERS) ? fb2->modifier : DRM_FORMAT_MOD_INVALID} { std::copy_n(fb2->handles, handles.size(), handles.begin()); std::copy_n(fb2->pitches, pitches.size(), pitches.begin()); std::copy_n(fb2->offsets, offsets.size(), offsets.begin()); @@ -300,19 +300,19 @@ namespace platf { /** * @brief DRM plane resource list released with `drmModeFreePlaneResources`. */ - using plane_res_t = util::safe_ptr; + using plane_res_t = util::safe_ptr; /** * @brief DRM encoder pointer released with `drmModeFreeEncoder`. */ - using encoder_t = util::safe_ptr; + using encoder_t = util::safe_ptr; /** * @brief DRM resource list released with `drmModeFreeResources`. */ - using res_t = util::safe_ptr; + using res_t = util::safe_ptr; /** * @brief DRM plane pointer released with `drmModeFreePlane`. */ - using plane_t = util::safe_ptr; + using plane_t = util::safe_ptr; /** * @brief DRM framebuffer pointer released with `drmModeFreeFB2`. */ @@ -320,23 +320,23 @@ namespace platf { /** * @brief DRM CRTC pointer released with `drmModeFreeCrtc`. */ - using crtc_t = util::safe_ptr; + using crtc_t = util::safe_ptr; /** * @brief DRM object-property list released with `drmModeFreeObjectProperties`. */ - using obj_prop_t = util::safe_ptr; + using obj_prop_t = util::safe_ptr; /** * @brief DRM property pointer released with `drmModeFreeProperty`. */ - using prop_t = util::safe_ptr; + using prop_t = util::safe_ptr; /** * @brief DRM property-blob pointer released with `drmModeFreePropertyBlob`. */ - using prop_blob_t = util::safe_ptr; + using prop_blob_t = util::safe_ptr; /** * @brief DRM version pointer released with `drmFreeVersion`. */ - using version_t = util::safe_ptr; + using version_t = util::safe_ptr; /** * @brief Counter map keyed by DRM connector type. @@ -510,13 +510,13 @@ namespace platf { this->plane.reset(); for (; plane_p != end; ++plane_p) { - plane_t plane = drmModeGetPlane(fd, *plane_p); - if (!plane) { + plane_t next_plane {drmModeGetPlane(fd, *plane_p)}; + if (!next_plane) { BOOST_LOG(error) << "Couldn't get drm plane ["sv << (end - plane_p) << "]: "sv << strerror(errno); continue; } - this->plane = util::make_shared(plane.release()); + this->plane = util::make_shared(next_plane.release()); break; } } @@ -689,7 +689,7 @@ namespace platf { /** * @brief Internal connector metadata tuple used while selecting a monitor. */ - using connector_interal_t = util::safe_ptr; + using connector_interal_t = util::safe_ptr; /** * @brief Open a DRM connector and cache its monitor metadata. @@ -776,7 +776,7 @@ namespace platf { * @return Owning pointer to the DRM CRTC object. */ crtc_t crtc(std::uint32_t id) { - return drmModeGetCrtc(fd.el, id); + return crtc_t {drmModeGetCrtc(fd.el, id)}; } /** @@ -786,7 +786,7 @@ namespace platf { * @return Owning pointer to the DRM encoder object. */ encoder_t encoder(std::uint32_t id) { - return drmModeGetEncoder(fd.el, id); + return encoder_t {drmModeGetEncoder(fd.el, id)}; } /** @@ -795,7 +795,7 @@ namespace platf { * @return DRM card resource list. */ res_t res() { - return drmModeGetResources(fd.el); + return res_t {drmModeGetResources(fd.el)}; } /** @@ -854,7 +854,7 @@ namespace platf { 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) { + if (value.has_value()) { return *value; } @@ -885,7 +885,7 @@ namespace platf { * @return Owning pointer to the DRM connector object. */ connector_interal_t connector(std::uint32_t id) { - return drmModeGetConnector(fd.el, id); + return connector_interal_t {drmModeGetConnector(fd.el, id)}; } /** @@ -953,7 +953,7 @@ namespace platf { * @return DRM properties and their current values. */ std::vector> props(std::uint32_t id, std::uint32_t type) { - obj_prop_t obj_prop = drmModeObjectGetProperties(fd.el, id, type); + obj_prop_t obj_prop {drmModeObjectGetProperties(fd.el, id, type)}; if (!obj_prop) { return {}; } @@ -1005,7 +1005,7 @@ namespace platf { * @return Plane metadata for the requested DRM plane. */ plane_t operator[](std::uint32_t index) { - return drmModeGetPlane(fd.el, plane_res->planes[index]); + return plane_t {drmModeGetPlane(fd.el, plane_res->planes[index])}; } /** @@ -1050,10 +1050,10 @@ namespace platf { std::map result; for (auto &connector : connectors) { - result.emplace(connector.crtc_id, monitor_t { - .type = connector.type, - .index = connector.index, - }); + result.try_emplace(connector.crtc_id, monitor_t { + .type = connector.type, + .index = connector.index, + }); } return result; @@ -1138,11 +1138,11 @@ namespace platf { class display_t: public platf::display_t { public: /** - * @brief Initialize common KMS display state for the requested memory type. + * @brief Explicitly initialize common KMS display state for the requested memory type. * * @param mem_type Mem type. */ - display_t(mem_type_e mem_type): + explicit display_t(mem_type_e mem_type): platf::display_t(), mem_type {mem_type} { } @@ -1265,9 +1265,9 @@ namespace platf { this->env_logical_width = ::platf::kms::env_logical_width; this->env_logical_height = ::platf::kms::env_logical_height; - auto monitor = pos->crtc_to_monitor.find(plane->crtc_id); - if (monitor != std::end(pos->crtc_to_monitor)) { - auto &viewport = monitor->second.viewport; + auto monitor_pos = pos->crtc_to_monitor.find(plane->crtc_id); + if (monitor_pos != std::end(pos->crtc_to_monitor)) { + auto &viewport = monitor_pos->second.viewport; width = viewport.width; height = viewport.height; @@ -1367,12 +1367,12 @@ namespace platf { * * @return True when the active display mode is HDR. */ - bool is_hdr() { - if (!hdr_metadata_blob_id || *hdr_metadata_blob_id == 0) { + bool is_hdr() override { + if (!hdr_metadata_blob_id.has_value() || *hdr_metadata_blob_id == 0) { return false; } - prop_blob_t hdr_metadata_blob = drmModeGetPropertyBlob(card.fd.el, *hdr_metadata_blob_id); + 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; @@ -1419,13 +1419,13 @@ namespace platf { * @param metadata Output structure populated with HDR metadata. * @return True when HDR metadata was written to the output structure. */ - bool get_hdr_metadata(SS_HDR_METADATA &metadata) { + bool get_hdr_metadata(SS_HDR_METADATA &metadata) override { // 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); + 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; @@ -1456,7 +1456,7 @@ namespace platf { return; } - plane_t plane = drmModeGetPlane(card.fd.el, cursor_plane_id); + plane_t plane {drmModeGetPlane(card.fd.el, cursor_plane_id)}; std::optional prop_crtc_x; std::optional prop_crtc_y; @@ -1489,14 +1489,14 @@ namespace platf { } } - if (!prop_crtc_w || !prop_crtc_h || !prop_crtc_x || !prop_crtc_y) { + if (!prop_crtc_w.has_value() || !prop_crtc_h.has_value() || !prop_crtc_x.has_value() || !prop_crtc_y.has_value()) { 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) { + if (!prop_src_x.has_value() || !prop_src_y.has_value() || !prop_src_w.has_value() || !prop_src_h.has_value()) { 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; @@ -1650,7 +1650,7 @@ namespace platf { */ 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) { + if (connector_id.has_value()) { 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; @@ -1658,7 +1658,7 @@ namespace platf { } } - plane_t plane = drmModeGetPlane(card.fd.el, plane_id); + plane_t plane {drmModeGetPlane(card.fd.el, plane_id)}; frame_timestamp = std::chrono::steady_clock::now(); // A plane that stays without a framebuffer will not get the picture back by itself: @@ -1678,7 +1678,7 @@ namespace platf { 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; + return capture_e::capture_error; } for (int y = 0; y < 4; ++y) { @@ -1695,7 +1695,7 @@ namespace platf { 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; + return capture_e::capture_error; } sd->fds[y] = file[y].el; @@ -1750,11 +1750,11 @@ namespace platf { class display_ram_t: public display_t { public: /** - * @brief Initialize a KMS display backend that copies frames through RAM. + * @brief Explicitly initialize a KMS display backend that copies frames through RAM. * * @param mem_type Mem type. */ - display_ram_t(mem_type_e mem_type): + explicit display_ram_t(mem_type_e mem_type): display_t(mem_type) { } @@ -1796,7 +1796,17 @@ namespace platf { return 0; } + /** + * @brief Capture KMS frames until capture stops or the display needs reinitialization. + * + * @param push_captured_image_cb Callback receiving a captured image or a capture timeout. + * @param pull_free_image_cb Callback providing an available image buffer. + * @param cursor Whether the cursor should be included in the captured image. + * @return Final capture status reported to the streaming pipeline. + */ 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 { + using enum platf::capture_e; + auto next_frame = std::chrono::steady_clock::now(); sleep_overshoot_logger.reset(); @@ -1807,27 +1817,27 @@ namespace platf { 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: + case reinit: + case capture_error: + case interrupted: return status; - case platf::capture_e::timeout: + case timeout: if (!push_captured_image_cb(std::move(img_out), false)) { - return platf::capture_e::ok; + return ok; } break; - case platf::capture_e::ok: + case ok: if (!push_captured_image_cb(std::move(img_out), true)) { - return platf::capture_e::ok; + return ok; } break; default: - BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; + BOOST_LOG(error) << "Unrecognized capture status ["sv << std::to_underlying(status) << ']'; return status; } } - return capture_e::ok; + return ok; } /** @@ -1928,7 +1938,7 @@ namespace platf { auto rgb_opt = egl::import_source(display.get(), sd); if (!rgb_opt) { - return capture_e::error; + return capture_e::capture_error; } auto &rgb = *rgb_opt; @@ -1994,11 +2004,11 @@ namespace platf { class display_vram_t: public display_t { public: /** - * @brief Initialize a KMS display backend that exports frames as GPU resources. + * @brief Explicitly initialize a KMS display backend that exports frames as GPU resources. * * @param mem_type Mem type. */ - display_vram_t(mem_type_e mem_type): + explicit display_vram_t(mem_type_e mem_type): display_t(mem_type) { } @@ -2062,7 +2072,17 @@ namespace platf { 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) { + /** + * @brief Capture KMS frames until capture stops or the display needs reinitialization. + * + * @param push_captured_image_cb Callback receiving a captured image or a capture timeout. + * @param pull_free_image_cb Callback providing an available image buffer. + * @param cursor Whether the cursor should be included in the captured image. + * @return Final capture status reported to the streaming pipeline. + */ + 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 { + using enum platf::capture_e; + auto next_frame = std::chrono::steady_clock::now(); sleep_overshoot_logger.reset(); @@ -2073,27 +2093,27 @@ namespace platf { 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: + case reinit: + case capture_error: + case interrupted: return status; - case platf::capture_e::timeout: + case timeout: if (!push_captured_image_cb(std::move(img_out), false)) { - return platf::capture_e::ok; + return ok; } break; - case platf::capture_e::ok: + case ok: if (!push_captured_image_cb(std::move(img_out), true)) { - return platf::capture_e::ok; + return ok; } break; default: - BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; + BOOST_LOG(error) << "Unrecognized capture status ["sv << std::to_underlying(status) << ']'; return status; } } - return capture_e::ok; + return ok; } /** @@ -2409,11 +2429,11 @@ namespace platf { 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)); + kms::env_width = std::max(kms::env_width, monitor_descriptor.viewport.offset_x + monitor_descriptor.viewport.width); + kms::env_height = std::max(kms::env_height, monitor_descriptor.viewport.offset_y + monitor_descriptor.viewport.height); - kms::env_logical_height = std::max(kms::env_logical_height, (int) (monitor_descriptor.viewport.offset_y + monitor_descriptor.viewport.logical_height)); - kms::env_logical_width = std::max(kms::env_logical_width, (int) (monitor_descriptor.viewport.offset_x + monitor_descriptor.viewport.logical_width)); + kms::env_logical_height = std::max(kms::env_logical_height, monitor_descriptor.viewport.offset_y + monitor_descriptor.viewport.logical_height); + kms::env_logical_width = std::max(kms::env_logical_width, monitor_descriptor.viewport.offset_x + monitor_descriptor.viewport.logical_width); } } diff --git a/src/platform/linux/kwingrab.cpp b/src/platform/linux/kwingrab.cpp index f7c3b08c70c..0fcfb507d10 100644 --- a/src/platform/linux/kwingrab.cpp +++ b/src/platform/linux/kwingrab.cpp @@ -523,7 +523,7 @@ namespace kwin { } // Registry listener - static void on_registry_global(void *data, struct wl_registry *reg, const uint32_t name, const char *interface, const uint32_t version) { + static void on_registry_global(void *data, struct wl_registry *reg, const uint32_t name, const char *interface, const uint32_t version) { // NOSONAR(cpp:S5008): Wayland listener callbacks require a void pointer for user data. auto *self = static_cast(data); if (!std::strcmp(interface, kde_output_order_v1_interface.name)) { // Bind version 1 @@ -560,7 +560,7 @@ namespace kwin { } } - static void on_registry_global_remove(void *data [[maybe_unused]], struct wl_registry *reg [[maybe_unused]], uint32_t name [[maybe_unused]]) { + static void on_registry_global_remove(void *data [[maybe_unused]], struct wl_registry *reg [[maybe_unused]], uint32_t name [[maybe_unused]]) { // NOSONAR(cpp:S5008): Wayland listener callbacks require a void pointer for user data. // We don't handle output hot-unplug during init } @@ -570,14 +570,14 @@ namespace kwin { }; // wl_output listener (for mode/dimensions/name) - static void on_output_geometry(void *data, struct wl_output *output, int32_t x, int32_t y, int32_t pw [[maybe_unused]], int32_t ph [[maybe_unused]], int32_t subpixel [[maybe_unused]], const char *make [[maybe_unused]], const char *model [[maybe_unused]], int32_t transform [[maybe_unused]]) { + static void on_output_geometry(void *data, struct wl_output *output, int32_t x, int32_t y, int32_t pw [[maybe_unused]], int32_t ph [[maybe_unused]], int32_t subpixel [[maybe_unused]], const char *make [[maybe_unused]], const char *model [[maybe_unused]], int32_t transform [[maybe_unused]]) { // NOSONAR(cpp:S5008): Wayland listener callbacks require a void pointer for user data. const auto *self = static_cast(data); const auto output_parameter = self->outputs.at(output); output_parameter->pos_x = x; output_parameter->pos_y = y; } - static void on_output_mode(void *data, struct wl_output *output, uint32_t flags, int32_t width, int32_t height, int32_t refresh [[maybe_unused]]) { + static void on_output_mode(void *data, struct wl_output *output, uint32_t flags, int32_t width, int32_t height, int32_t refresh [[maybe_unused]]) { // NOSONAR(cpp:S5008): Wayland listener callbacks require a void pointer for user data. if (!(flags & WL_OUTPUT_MODE_CURRENT)) { return; } @@ -587,20 +587,20 @@ namespace kwin { output_parameter->height = height; } - static void on_output_done(void *data [[maybe_unused]], struct wl_output *output [[maybe_unused]]) { + static void on_output_done(void *data [[maybe_unused]], struct wl_output *output [[maybe_unused]]) { // NOSONAR(cpp:S5008): Wayland listener callbacks require a void pointer for user data. // Currently unused } - static void on_output_scale(void *data [[maybe_unused]], struct wl_output *output [[maybe_unused]], int32_t factor [[maybe_unused]]) { + static void on_output_scale(void *data [[maybe_unused]], struct wl_output *output [[maybe_unused]], int32_t factor [[maybe_unused]]) { // NOSONAR(cpp:S5008): Wayland listener callbacks require a void pointer for user data. // Currently unused } - static void on_output_name(void *data, struct wl_output *output, const char *name) { + static void on_output_name(void *data, struct wl_output *output, const char *name) { // NOSONAR(cpp:S5008): Wayland listener callbacks require a void pointer for user data. const auto *self = static_cast(data); self->outputs.at(output)->name = name; } - static void on_output_description(void *data [[maybe_unused]], struct wl_output *output [[maybe_unused]], const char *description [[maybe_unused]]) { + static void on_output_description(void *data [[maybe_unused]], struct wl_output *output [[maybe_unused]], const char *description [[maybe_unused]]) { // NOSONAR(cpp:S5008): Wayland listener callbacks require a void pointer for user data. // Currently unused } @@ -614,12 +614,12 @@ namespace kwin { }; // Output order listener - static void on_output_order_output(void *data, struct kde_output_order_v1 *kde_output_order_v1 [[maybe_unused]], const char *output_name) { + static void on_output_order_output(void *data, struct kde_output_order_v1 *kde_output_order_v1 [[maybe_unused]], const char *output_name) { // NOSONAR(cpp:S5008): Wayland listener callbacks require a void pointer for user data. auto *self = static_cast(data); self->output_order.emplace_back(output_name); } - static void on_output_order_done(void *data [[maybe_unused]], struct kde_output_order_v1 *kde_output_order_v1 [[maybe_unused]]) { + static void on_output_order_done(void *data [[maybe_unused]], struct kde_output_order_v1 *kde_output_order_v1 [[maybe_unused]]) { // NOSONAR(cpp:S5008): Wayland listener callbacks require a void pointer for user data. // Currently unused } @@ -629,7 +629,7 @@ namespace kwin { }; // ScreenCast v1 stream listener - static void on_stream_closed(void *data, struct zkde_screencast_stream_unstable_v1 *stream [[maybe_unused]]) { + static void on_stream_closed(void *data, struct zkde_screencast_stream_unstable_v1 *stream [[maybe_unused]]) { // NOSONAR(cpp:S5008): Wayland listener callbacks require a void pointer for user data. auto *self = static_cast(data); BOOST_LOG(warning) << "[kwingrab] stream closed by server"sv; self->stream_failed = false; @@ -637,7 +637,7 @@ namespace kwin { self->stream_error_msg = "stream closed by server"; } - static void on_stream_created(void *data, struct zkde_screencast_stream_unstable_v1 *stream [[maybe_unused]], const uint32_t node) { + static void on_stream_created(void *data, struct zkde_screencast_stream_unstable_v1 *stream [[maybe_unused]], const uint32_t node) { // NOSONAR(cpp:S5008): Wayland listener callbacks require a void pointer for user data. auto *self = static_cast(data); self->out_node_id = node; self->stream_failed = false; @@ -645,7 +645,7 @@ namespace kwin { BOOST_LOG(debug) << "[kwingrab] created event, node_id="sv << node; } - static void on_stream_failed(void *data, struct zkde_screencast_stream_unstable_v1 *stream [[maybe_unused]], const char *err_msg) { + static void on_stream_failed(void *data, struct zkde_screencast_stream_unstable_v1 *stream [[maybe_unused]], const char *err_msg) { // NOSONAR(cpp:S5008): Wayland listener callbacks require a void pointer for user data. auto *self = static_cast(data); self->stream_failed = true; self->stream_ready = false; @@ -653,7 +653,7 @@ namespace kwin { BOOST_LOG(error) << "[kwingrab] failed event: "sv << self->stream_error_msg; } - static void on_stream_serial(void *data, struct zkde_screencast_stream_unstable_v1 *stream [[maybe_unused]], uint32_t object_serial_hi, uint32_t object_serial_low) { + static void on_stream_serial(void *data, struct zkde_screencast_stream_unstable_v1 *stream [[maybe_unused]], uint32_t object_serial_hi, uint32_t object_serial_low) { // NOSONAR(cpp:S5008): Wayland listener callbacks require a void pointer for user data. auto *self = static_cast(data); self->out_objectserial = static_cast(object_serial_hi) << 32 | object_serial_low; // serial event always preceded the created event with the node id, so we only set stream_ready in created for v1 diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index babf0a4881f..b89b9b97dd0 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -20,6 +20,7 @@ #include #include #include +#include // platform includes #include @@ -107,15 +108,15 @@ window_system_e window_system; ///< Window system. namespace dyn { /** - * @brief Return the native handle owned by the wrapper. + * @brief Open the first available shared library from a nonempty list. */ - void *handle(const std::vector &libs) { + library_t *handle(const std::vector &libs) { void *handle; for (auto lib : libs) { handle = dlopen(lib, RTLD_LAZY | RTLD_LOCAL); if (handle) { - return handle; + return static_cast(handle); } } @@ -133,9 +134,9 @@ namespace dyn { } /** - * @brief Load persisted state from its backing store. + * @brief Load named functions from an open shared library. */ - int load(void *handle, const std::vector> &funcs, bool strict) { + int load(library_t *handle, const std::vector> &funcs, bool strict) { int err = 0; for (auto &func : funcs) { TUPLE_2D_REF(fn, name, func); @@ -200,7 +201,7 @@ namespace platf { /** * @brief Owning pointer for `getifaddrs` results. */ - using ifaddr_t = util::safe_ptr; + using ifaddr_t = util::safe_ptr; /** * @brief Open a DRM card node, dropping implicit DRM master when possible. @@ -471,7 +472,11 @@ namespace platf { return "00:00:00:00:00:00"s; } - 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) { + /** + * @brief Launch a command using the caller's Linux privileges and session. + * @details The common platform signature's elevation and interactivity flags do not apply on Linux. + */ + bp::child run_command(bool, bool, 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) { @@ -620,7 +625,7 @@ namespace platf { executable[len] = '\0'; // ASIO doesn't use O_CLOEXEC, so we have to close all fds ourselves - int openmax = (int) sysconf(_SC_OPEN_MAX); + auto openmax = (int) sysconf(_SC_OPEN_MAX); for (int fd = STDERR_FILENO + 1; fd < openmax; fd++) { close(fd); } @@ -751,7 +756,7 @@ namespace platf { /** * @brief Send multiple fixed-size UDP payload blocks using the platform backend. */ - bool send_batch(batched_send_info_t &send_info) { + bool send_batch(const batched_send_info_t &send_info) { auto sockfd = (int) send_info.native_socket; struct msghdr msg = {}; @@ -1016,7 +1021,7 @@ namespace platf { // 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; + static std::atomic qos_ref_count = 0; /** * @brief Linux QoS state used to tune socket priority while streaming. @@ -1027,9 +1032,9 @@ namespace platf { * @brief Apply Linux socket priority and DSCP QoS settings for scoped cleanup. * * @param sockfd Native socket descriptor whose options are updated. - * @param options Request options or socket options to apply. + * @param options Socket options copied into this cleanup object's owned storage. */ - qos_t(int sockfd, std::vector> options): + qos_t(int sockfd, const std::vector> &options): sockfd(sockfd), options(options) { qos_ref_count++; @@ -1037,9 +1042,8 @@ namespace platf { 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) { + for (const auto &[level, option, reset_val] : options) { + if (setsockopt(sockfd, level, option, &reset_val, sizeof(reset_val)) < 0) { BOOST_LOG(warning) << "Failed to reset option: "sv << errno; } } @@ -1055,7 +1059,7 @@ namespace platf { * @brief Enables QoS on the given socket for traffic to the specified destination. */ 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; + auto sockfd = (int) native_socket; std::vector> reset_options; if (dscp_tagging) { @@ -1083,7 +1087,7 @@ namespace platf { dscp = 48; break; default: - BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type; + BOOST_LOG(error) << "Unknown traffic type: "sv << std::to_underlying(data_type); break; } @@ -1530,7 +1534,7 @@ namespace platf { } /** - * @brief Linux high-precision timer implementation backed by `timerfd`. + * @brief Explicitly linux high-precision timer implementation backed by `timerfd`. */ class linux_high_precision_timer: public high_precision_timer { public: @@ -1538,7 +1542,7 @@ namespace platf { std::this_thread::sleep_for(duration); } - operator bool() override { + explicit operator bool() override { return true; } }; diff --git a/src/platform/linux/misc.h b/src/platform/linux/misc.h index 5d88fbb04c5..056db4dd014 100644 --- a/src/platform/linux/misc.h +++ b/src/platform/linux/misc.h @@ -40,13 +40,29 @@ enum class window_system_e { extern window_system_e window_system; ///< Window system. namespace dyn { + /** + * @brief Opaque shared-library handle returned by the dynamic loader. + */ + struct library_t; + /** * @brief Generic GLX procedure pointer returned by the loader. */ - typedef void (*apiproc)(void); + using apiproc = void (*)(void); - int load(void *handle, const std::vector> &funcs, bool strict = true); - void *handle(const std::vector &libs); + /** + * @brief Load named functions from an open shared library. + * @param handle Library whose symbols are queried. + * @param funcs Function pointer destinations and corresponding symbol names. + * @param strict Whether missing symbols make the operation fail. + * @return Zero on success or minus one when a required symbol is missing. + */ + int load(library_t *handle, const std::vector> &funcs, bool strict = true); + /** + * @brief Open the first available shared library from a nonempty list. + * @return Open library handle, or null if none could be loaded. + */ + library_t *handle(const std::vector &libs); } // namespace dyn diff --git a/src/platform/linux/pipewire.cpp b/src/platform/linux/pipewire.cpp index 6ce9d3e2102..ec53d245d83 100644 --- a/src/platform/linux/pipewire.cpp +++ b/src/platform/linux/pipewire.cpp @@ -762,11 +762,11 @@ namespace pipewire { return static_cast(spa_pod_builder_pop(b, &object_frame)); } - static void on_core_info_cb([[maybe_unused]] void *user_data, const struct pw_core_info *pw_info) { + static void on_core_info_cb([[maybe_unused]] void *user_data, const struct pw_core_info *pw_info) { // NOSONAR(cpp:S5008): PipeWire event callbacks require a void pointer for user data. BOOST_LOG(info) << "[pipewire] Connected to pipewire version "sv << pw_info->version; } - static void on_core_error_cb([[maybe_unused]] void *user_data, const uint32_t id, const int seq, [[maybe_unused]] int res, const char *message) { + static void on_core_error_cb([[maybe_unused]] void *user_data, const uint32_t id, const int seq, [[maybe_unused]] int res, const char *message) { // NOSONAR(cpp:S5008): PipeWire event callbacks require a void pointer for user data. BOOST_LOG(info) << "[pipewire] Pipewire Error, id:"sv << id << " seq:"sv << seq << " message: "sv << message; } @@ -776,7 +776,7 @@ namespace pipewire { .error = on_core_error_cb, }; - static void on_stream_state_changed(void *user_data, enum pw_stream_state old, enum pw_stream_state state, const char *err_msg) { + static void on_stream_state_changed(void *user_data, enum pw_stream_state old, enum pw_stream_state state, const char *err_msg) { // NOSONAR(cpp:S5008): PipeWire event callbacks require a void pointer for user data. if (err_msg != nullptr) { BOOST_LOG(info) << "[pipewire] PipeWire stream error '" << err_msg << "' on state: " << pw_stream_state_as_string(old) << " -> " << pw_stream_state_as_string(state); @@ -821,7 +821,7 @@ namespace pipewire { } } - static void on_process(void *user_data) { + static void on_process(void *user_data) { // NOSONAR(cpp:S5008): PipeWire event callbacks require a void pointer for user data. const auto d = static_cast(user_data); struct pw_buffer *b = nullptr; @@ -873,7 +873,7 @@ namespace pipewire { d->frame_cv.notify_one(); } - static void on_param_changed(void *user_data, uint32_t id, const struct spa_pod *param) { + static void on_param_changed(void *user_data, uint32_t id, const struct spa_pod *param) { // NOSONAR(cpp:S5008): PipeWire event callbacks require a void pointer for user data. const auto d = static_cast(user_data); d->current_buffer = nullptr; @@ -1217,12 +1217,22 @@ namespace pipewire { return false; // Return to default stream dead handling. } + /** + * @brief Capture PipeWire frames until capture stops or the stream needs reinitialization. + * + * @param push_captured_image_cb Callback receiving a captured image or a capture timeout. + * @param pull_free_image_cb Callback providing an available image buffer. + * @param cursor Whether the cursor should be included in the captured image. + * @return Final capture status reported to the streaming pipeline. + */ 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 { + using enum platf::capture_e; + auto next_frame = std::chrono::steady_clock::now(); if (pipewire.ensure_stream(mem_type, width, height, target_framerate, dmabuf_infos.data(), n_dmabuf_infos, display_is_nvidia) < 0) { BOOST_LOG(error) << "[pipewire] Failed to ensure pipewire stream. capture() failed with error."; - return platf::capture_e::error; + return capture_error; } sleep_overshoot_logger.reset(); @@ -1238,7 +1248,7 @@ namespace pipewire { } // Re-init the capture if the stream is dead for any other reason BOOST_LOG(warning) << "[pipewire] PipeWire stream disconnected. Forcing session reset."sv; - return platf::capture_e::reinit; + return reinit; } // Use unpaced event driven capture when possible @@ -1248,27 +1258,27 @@ namespace pipewire { std::shared_ptr img_out; switch (const auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor)) { - case platf::capture_e::reinit: - case platf::capture_e::error: - case platf::capture_e::interrupted: + case reinit: + case capture_error: + case interrupted: pipewire.frame_cv().notify_all(); return status; - case platf::capture_e::timeout: + case timeout: if (!pull_free_image_cb(img_out)) { // Detect if shutdown is pending BOOST_LOG(debug) << "[pipewire] PipeWire: timeout -> shutdown pending -> interrupt nudge"; pipewire.frame_cv().notify_all(); - return platf::capture_e::interrupted; + return interrupted; } if (!push_captured_image_cb(std::move(img_out), false)) { BOOST_LOG(debug) << "[pipewire] PipeWire: timeout -> !push_captured_image_cb -> ok"; - return platf::capture_e::ok; + return ok; } break; - case platf::capture_e::ok: + case ok: if (!push_captured_image_cb(std::move(img_out), true)) { BOOST_LOG(debug) << "[pipewire] PipeWire: ok -> !push_captured_image_cb -> ok"; - return platf::capture_e::ok; + return ok; } break; default: @@ -1277,7 +1287,7 @@ namespace pipewire { } } - return platf::capture_e::ok; + return ok; } /** diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp index b530ae6d407..6cbc21b9dd2 100644 --- a/src/platform/linux/portalgrab.cpp +++ b/src/platform/linux/portalgrab.cpp @@ -1063,7 +1063,7 @@ namespace portal { if (dbus.is_session_closed()) { BOOST_LOG(warning) << "[portalgrab] PipeWire stream stopped by closed portal session."sv; pipewire.frame_cv().notify_all(); - out_status = platf::capture_e::error; + out_status = platf::capture_e::capture_error; return true; // Stop capture with error (due to out_status) } // Disable maxframerate negotiation if the stream died without having ever started (e.g. GNOME mutter does not support it) @@ -1081,7 +1081,7 @@ namespace portal { dbus_t dbus; ///< DBus connection used for portal screencast requests. // Class variable to store runtime state of maxFramerate negotiation - static inline std::atomic negotiate_maxframerate {true}; ///< Whether portal negotiation should request the maximum frame rate. + static inline std::atomic negotiate_maxframerate {true}; ///< Whether portal negotiation should request the maximum frame rate. }; } // namespace portal diff --git a/src/platform/linux/publish.cpp b/src/platform/linux/publish.cpp index 5a2f45825fc..5aedc160f2a 100644 --- a/src/platform/linux/publish.cpp +++ b/src/platform/linux/publish.cpp @@ -4,10 +4,28 @@ * @note Adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html */ // standard includes +#include +#include +#include +#include +#ifdef __linux__ + // Netlink is a Linux-kernel API: these headers exist only on Linux. + #include + #include +#endif +#include +#include +#include +#include +#include #include +#include +#include // local includes #include "misc.h" +#include "publish.h" +#include "src/config.h" #include "src/logging.h" #include "src/network.h" #include "src/nvhttp.h" @@ -19,7 +37,9 @@ using namespace std::literals; namespace avahi { /** - * @brief Error codes used by avahi. + * @brief Error codes used by Avahi. + * @see https://www.rfc-editor.org/rfc/rfc2136.html#section-2.2 + * @see https://www.iana.org/assignments/dns-parameters/ */ enum err_e { OK = 0, ///< OK @@ -67,11 +87,11 @@ namespace avahi { 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_DNS_YXDOMAIN = -41, ///< DNS Error: A name exists despite a prerequisite requiring its absence + ERR_DNS_YXRRSET = -42, ///< DNS Error: A resource record set exists despite a prerequisite requiring its absence + ERR_DNS_NXRRSET = -43, ///< DNS Error: A required resource record set is missing or has different records + ERR_DNS_NOTAUTH = -44, ///< DNS Error: The server lacks zone authority or TSIG authorization failed + ERR_DNS_NOTZONE = -45, ///< DNS Error: An update or prerequisite name lies outside the target DNS zone ERR_INVALID_RDATA = -46, ///< Invalid RDATA ERR_INVALID_DNS_CLASS = -47, ///< Invalid DNS class ERR_INVALID_DNS_TYPE = -48, ///< Invalid DNS type @@ -82,22 +102,11 @@ namespace avahi { 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; ///< Protocol or platform constant for if unspec. - - /** - * @brief Enumerates supported proto options. - */ - enum proto { - PROTO_INET = 0, ///< IPv4 - PROTO_INET6 = 1, ///< IPv6 - PROTO_UNSPEC = -1 ///< Unspecified/all protocol(s) + ERR_MAX = -54 ///< Sentinel below the valid Avahi error-code range; not an error code }; /** - * @brief Enumerates supported server state options. + * @brief Avahi daemon lifecycle states. */ enum ServerState { SERVER_INVALID, ///< Invalid state (initial) @@ -137,160 +146,113 @@ namespace avahi { 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 - }; - - /** - * @brief Network interface index type used by Avahi. - */ - using IfIndex = int; - /** - * @brief Avahi protocol/address-family selector type. - */ - using Protocol = int; - - struct EntryGroup; struct Poll; struct SimplePoll; - struct Client; /** * @brief Avahi client state-change callback signature. */ - typedef void (*ClientCallback)(Client *, ClientState, void *userdata); + using ClientCallback = void (*)(Client *, ClientState, void *userdata); /** * @brief Avahi entry-group state-change callback signature. */ - typedef void (*EntryGroupCallback)(EntryGroup *g, EntryGroupState state, void *userdata); - - /** - * @brief Function pointer used to free Avahi-allocated memory. - */ - typedef void (*free_fn)(void *); + using EntryGroupCallback = void (*)(EntryGroup *g, EntryGroupState state, void *userdata); /** * @brief Function pointer used to create an Avahi client. */ - typedef Client *(*client_new_fn)(const Poll *poll_api, ClientFlags flags, ClientCallback callback, void *userdata, int *error); - /** - * @brief Function pointer used to destroy an Avahi client. - */ - typedef void (*client_free_fn)(Client *); - /** - * @brief Function pointer used to request an alternative Avahi service name. - */ - typedef char *(*alternative_service_name_fn)(char *); - - /** - * @brief Function pointer used to get an Avahi client from an entry group. - */ - typedef Client *(*entry_group_get_client_fn)(EntryGroup *); + using client_new_fn = Client *(*)(const Poll *poll_api, ClientFlags flags, ClientCallback callback, void *userdata, int *error); /** * @brief Function pointer used to create an Avahi entry group. */ - typedef EntryGroup *(*entry_group_new_fn)(Client *, EntryGroupCallback, void *userdata); - /** - * @brief Function pointer used to add a service to an Avahi entry group. - */ - 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, - ... - ); - - /** - * @brief Function pointer used to test whether an Avahi entry group is empty. - */ - typedef int (*entry_group_is_empty_fn)(EntryGroup *); - /** - * @brief Function pointer used to reset an Avahi entry group. - */ - typedef int (*entry_group_reset_fn)(EntryGroup *); - /** - * @brief Function pointer used to publish an Avahi entry group. - */ - typedef int (*entry_group_commit_fn)(EntryGroup *); + using entry_group_new_fn = EntryGroup *(*)(Client *, EntryGroupCallback, void *userdata); /** * @brief Function pointer used to duplicate Avahi strings. */ - typedef char *(*strdup_fn)(const char *); - /** - * @brief Function pointer used to format Avahi error codes. - */ - typedef char *(*strerror_fn)(int); - /** - * @brief Function pointer used to read the last Avahi client error. - */ - typedef int (*client_errno_fn)(Client *); + using strdup_fn = char *(*)(const char *); - /** - * @brief Function pointer used to get the Avahi simple-poll API. - */ - typedef Poll *(*simple_poll_get_fn)(SimplePoll *); /** * @brief Function pointer used to run the Avahi simple-poll loop. */ - typedef int (*simple_poll_loop_fn)(SimplePoll *); - /** - * @brief Function pointer used to stop the Avahi simple-poll loop. - */ - typedef void (*simple_poll_quit_fn)(SimplePoll *); + using simple_poll_loop_fn = int (*)(SimplePoll *); /** * @brief Function pointer used to allocate an Avahi simple-poll instance. */ - typedef SimplePoll *(*simple_poll_new_fn)(); - /** - * @brief Function pointer used to free an Avahi simple-poll instance. - */ - typedef void (*simple_poll_free_fn)(SimplePoll *); + using simple_poll_new_fn = SimplePoll *(*)(); - free_fn free; ///< Free. client_new_fn client_new; ///< Client new. + entry_group_new_fn entry_group_new; ///< Entry group new. + strdup_fn strdup; ///< Strdup. + simple_poll_loop_fn simple_poll_loop; ///< Simple poll loop. + simple_poll_new_fn simple_poll_new; ///< Simple poll new. + + free_fn free; ///< Free. client_free_fn client_free; ///< Client free. + simple_poll_free_fn simple_poll_free; ///< Simple poll free. alternative_service_name_fn alternative_service_name; ///< Alternative service name. entry_group_get_client_fn entry_group_get_client; ///< Entry group get client. - entry_group_new_fn entry_group_new; ///< Entry group new. entry_group_add_service_fn entry_group_add_service; ///< Entry group add service. + entry_group_add_address_fn entry_group_add_address; ///< Entry group add address. entry_group_is_empty_fn entry_group_is_empty; ///< Entry group is empty. entry_group_reset_fn entry_group_reset; ///< Entry group reset. entry_group_commit_fn entry_group_commit; ///< Entry group commit. - strdup_fn strdup; ///< Strdup. strerror_fn strerror; ///< Strerror. client_errno_fn client_errno; ///< Client errno. - simple_poll_get_fn simple_poll_get; ///< Simple poll get. - simple_poll_loop_fn simple_poll_loop; ///< Simple poll loop. simple_poll_quit_fn simple_poll_quit; ///< Simple poll quit. - simple_poll_new_fn simple_poll_new; ///< Simple poll new. - simple_poll_free_fn simple_poll_free; ///< Simple poll free. + simple_poll_get_fn simple_poll_get; ///< Simple poll get. + + /** + * @brief Event types for Avahi poll watches. + * @details Layout-compatible with `AvahiWatchEvent` (see + * avahi-common/watch.h): the values are the `poll(2)` event flags. + * Scoped enum with an explicit `int` underlying type, so the layout + * stays identical to the C enum. + */ + enum class WatchEvent : int { + WATCH_IN = POLLIN, ///< Input event + WATCH_OUT = POLLOUT, ///< Output event + WATCH_ERR = POLLERR, ///< Error event + WATCH_HUP = POLLHUP ///< Hangup event + }; + + struct Watch; + struct Timeout; /** - * @brief Load common Avahi client and entry-group function pointers. + * @brief Avahi poll-watch callback signature. + */ + using WatchCallback = void (*)(Watch *w, int fd, WatchEvent event, void *userdata); + /** + * @brief Avahi poll-timeout callback signature. + */ + using TimeoutCallback = void (*)(Timeout *t, void *userdata); + + /** + * @brief Abstracted event polling API. + * @details Layout-compatible with `AvahiPoll` (see avahi-common/watch.h): + * a user-data pointer followed by the watch and timeout operations. Only + * the watch operations are used here. + */ + struct Poll { + void *userdata; ///< User data for the poll provider. + Watch *(*watch_new)(const Poll *api, int fd, WatchEvent event, WatchCallback callback, void *userdata); ///< Create a watch. + void (*watch_update)(Watch *w, WatchEvent event); ///< Update a watch's events. + WatchEvent (*watch_get_events)(Watch *w); ///< Events that happened on a watch. + void (*watch_free)(Watch *w); ///< Free a watch. + Timeout *(*timeout_new)(const Poll *api, const timeval *tv, TimeoutCallback callback, void *userdata); ///< Create a timeout. + void (*timeout_update)(Timeout *t, const timeval *tv); ///< Update a timeout. + void (*timeout_free)(Timeout *t); ///< Free a timeout. + }; + + /** + * @brief Load Avahi common-library memory, naming, error, and simple-poll functions. * * @return 0 when common Avahi functions are available; nonzero otherwise. */ int init_common() { - static void *handle {nullptr}; + static dyn::library_t *handle {nullptr}; static bool funcs_loaded = false; if (funcs_loaded) { @@ -304,19 +266,18 @@ namespace avahi { } } - 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)) { + if (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"}, + }; + dyn::load(handle, funcs)) { return -1; } @@ -325,7 +286,7 @@ namespace avahi { } /** - * @brief Load Avahi threaded-poll and client function pointers. + * @brief Load Avahi client and service-publication function pointers. * * @return 0 when all client functions are available; nonzero otherwise. */ @@ -334,7 +295,7 @@ namespace avahi { return -1; } - static void *handle {nullptr}; + static dyn::library_t *handle {nullptr}; static bool funcs_loaded = false; if (funcs_loaded) { @@ -348,19 +309,19 @@ namespace avahi { } } - 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)) { + if (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_add_address, "avahi_entry_group_add_address"}, + {(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"}, + }; + dyn::load(handle, funcs)) { return -1; } @@ -371,30 +332,6 @@ namespace avahi { namespace platf::publish { - /** - * @brief Release memory allocated by Avahi. - * - * @param p Avahi-allocated pointer to release. - */ - template - void free(T *p) { - avahi::free(p); - } - - /** - * @brief Owning pointer for Avahi allocations released with `avahi_free`. - */ - template - using ptr_t = util::safe_ptr>; - /** - * @brief Owning pointer for an Avahi client. - */ - using client_t = util::dyn_safe_ptr; - /** - * @brief Owning pointer for an Avahi simple poll loop. - */ - using poll_t = util::dyn_safe_ptr; - avahi::EntryGroup *group = nullptr; ///< Active Avahi entry group that owns the published service. poll_t poll; ///< Avahi poll loop used while the service is published. @@ -402,12 +339,121 @@ namespace platf::publish { ptr_t name; ///< Current service name, updated when Avahi reports a collision. - /** - * @brief Create or update the Avahi service entry group. - * - * @param c Avahi client used to allocate and commit the service entry group. - */ - void create_services(avahi::Client *c); + unsigned host_name_collision_count = 0; ///< Suffix counter for regenerating the service host name after collisions. + + std::optional> published_addresses; ///< Committed snapshot; nullopt while withdrawn. + +#ifdef __linux__ + int netlink_fd = -1; ///< Netlink socket watched for interface/address changes, -1 when not installed. + avahi::Watch *netlink_watch = nullptr; ///< Avahi poll watch for the netlink socket, nullptr when not installed. +#endif + + /* Enumerate the local interface addresses available for publication. */ + std::vector enumerate_interface_addresses() { + ifaddrs *interfaces = nullptr; + if (getifaddrs(&interfaces) != 0) { + BOOST_LOG(error) << "Failed to enumerate network interfaces."sv; + return {}; + } + auto cleanup = util::fail_guard([&interfaces]() { + freeifaddrs(interfaces); + }); + + std::vector addresses; + for (const auto *ifa = interfaces; ifa != nullptr; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == nullptr || (ifa->ifa_flags & IFF_UP) == 0) { + continue; + } + + // The owning interface index scopes the record to the link it is + // reachable on; if_nametoindex() only fails for names that vanished + // mid-enumeration, and those records fall back to IF_UNSPEC. + avahi::IfIndex interface = avahi::IF_UNSPEC; + if (const auto index = if_nametoindex(ifa->ifa_name); index != 0) { + interface = static_cast(index); + } + + if (ifa->ifa_addr->sa_family == AF_INET) { + const auto *inet_address = reinterpret_cast(ifa->ifa_addr); + boost::asio::ip::address_v4::bytes_type bytes {}; + std::memcpy(bytes.data(), &inet_address->sin_addr, bytes.size()); + addresses.emplace_back(interface, boost::asio::ip::address_v4(bytes)); + } else if (ifa->ifa_addr->sa_family == AF_INET6) { + const auto *inet6_address = reinterpret_cast(ifa->ifa_addr); + boost::asio::ip::address_v6::bytes_type bytes {}; + std::memcpy(bytes.data(), &inet6_address->sin6_addr, bytes.size()); + addresses.emplace_back(interface, boost::asio::ip::address_v6(bytes, inet6_address->sin6_scope_id)); + } + } + return addresses; + } + + /* Determine the addresses published for the service host name. */ + std::vector served_addresses(net::af_e effective_family) { + // An explicit, non-wildcard bind address is, by definition, an address + // Sunshine serves: publish exactly it, with the interface index of the + // interface that owns it when one does. + if (config::sunshine.bind_address.empty()) { + // A wildcard bind serves every local interface address of the effective family. + return select_served_addresses(enumerate_interface_addresses(), effective_family); + } + + boost::system::error_code ec; + auto bind_address = boost::asio::ip::make_address(config::sunshine.bind_address, ec); + if (ec) { + BOOST_LOG(warning) << "Configured bind address is not a valid IP address, falling back to interface enumeration: "sv + << config::sunshine.bind_address; + // An unparsable bind serves every local interface address of the effective family. + return select_served_addresses(enumerate_interface_addresses(), effective_family); + } + + bind_address = net::normalize_address(bind_address); + if (bind_address.is_unspecified()) { + // A wildcard bind serves every local interface address of the effective family. + return select_served_addresses(enumerate_interface_addresses(), effective_family); + } + + if (effective_family == net::IPV4 && !bind_address.is_v4()) { + return {}; + } + + const auto enumerated = enumerate_interface_addresses(); + const auto owner = std::find_if( + enumerated.begin(), + enumerated.end(), + [&bind_address](const ServedAddress &entry) { + return entry.address == bind_address; + } + ); + const auto interface = owner != enumerated.end() ? owner->interface : avahi::IF_UNSPEC; + return {ServedAddress {interface, bind_address}}; + } + + /* Add address records for the service host name to the entry group. */ + int add_service_addresses( + avahi::EntryGroup *entry_group, + const std::string &service_host, + const std::vector &addresses + ) { + using enum avahi::proto; + + for (const auto &address : addresses) { + const auto avahi_address = to_avahi_address(address.address); + const int ret = avahi::entry_group_add_address( + entry_group, + address.interface, + std::to_underlying(PROTO_UNSPEC), + avahi::PUBLISH_NO_REVERSE, + service_host.c_str(), + &avahi_address + ); + if (ret < 0) { + BOOST_LOG(error) << "Failed to add avahi address for "sv << service_host << ": "sv << avahi::strerror(ret); + return ret; + } + } + return 0; + } /** * @brief React to Avahi entry-group state changes. @@ -415,7 +461,7 @@ namespace platf::publish { * @param g Entry group that emitted the state change. * @param state Avahi entry-group state reported by the callback. */ - void entry_group_callback(avahi::EntryGroup *g, avahi::EntryGroupState state, void *) { + void entry_group_callback(avahi::EntryGroup *g, avahi::EntryGroupState state, void *) { // NOSONAR(cpp:S5008): Avahi callbacks require a void pointer for user data. group = g; switch (state) { @@ -427,6 +473,11 @@ namespace platf::publish { BOOST_LOG(info) << "Avahi service name collision, renaming service to " << name.get(); + // The collision may have been on the service host name rather than + // the service name: regenerate the host name as well so the retry + // does not collide on the same host name again. + ++host_name_collision_count; + create_services(avahi::entry_group_get_client(g)); break; case avahi::ENTRY_GROUP_FAILURE: @@ -438,14 +489,16 @@ namespace platf::publish { } } - /** - * @brief Publish Sunshine's mDNS service through Avahi. - */ - void create_services(avahi::Client *c) { + /* Publish Sunshine's mDNS service through Avahi. */ + void create_services(avahi::Client *c, bool quit_on_failure) { + using enum avahi::proto; + int ret; - auto fg = util::fail_guard([]() { - avahi::simple_poll_quit(poll.get()); + auto fg = util::fail_guard([quit_on_failure]() { + if (quit_on_failure) { + avahi::simple_poll_quit(poll.get()); + } }); if (!group) { @@ -456,17 +509,58 @@ namespace platf::publish { } if (avahi::entry_group_is_empty(group)) { + // A fresh registration attempt starts from a withdrawn snapshot: the + // group is empty, so no previously committed snapshot is still live. + published_addresses.reset(); BOOST_LOG(info) << "Adding avahi service "sv << name.get(); + // Discovery transport stays on both protocols: the transport only + // decides which multicast groups carry the announcement, it does not + // filter the advertised host addresses (see #5860). The filter is the + // service-specific SRV target below: the daemon publishes address + // records for that host name only from the explicit records added + // here, so an IPv4-only configuration advertises no AAAA record even + // though the daemon's shared host name still carries both A and AAAA + // (publish-aaaa-on-ipv4). The effective family accounts for + // bind_address, matching the listener setup. On non-Linux platforms + // there is no address-change watch, so the service instead uses the + // daemon-managed host name (a null SRV target) and the daemon + // maintains the address records. + const auto effective_family = net::get_effective_address_family( + net::af_from_enum_string(config::sunshine.address_family) + ); + const auto addresses = served_addresses(effective_family); + + // The service-specific host name and its fixed address records are + // Linux-only: only Linux installs the address-change watch that + // refreshes them (see install_address_change_watch()). + const char *service_target = nullptr; +#ifdef __linux__ + const auto service_host = service_host_name(platf::get_host_name(), host_name_collision_count); + if (addresses.empty()) { + BOOST_LOG(warning) << "No served addresses determined for "sv << service_host + << "; falling back to the daemon host name, which may advertise unserved addresses (see #5860)."; + } else { + service_target = service_host.c_str(); + ret = add_service_addresses(group, service_host, addresses); + if (ret < 0) { + published_addresses.reset(); + avahi::entry_group_reset(group); + return; + } + } + +#endif + ret = avahi::entry_group_add_service( group, avahi::IF_UNSPEC, - avahi::PROTO_UNSPEC, + std::to_underlying(PROTO_UNSPEC), avahi::PublishFlags(0), name.get(), platf::SERVICE_TYPE, nullptr, - nullptr, + service_target, net::map_port(nvhttp::PORT_HTTP), nullptr ); @@ -477,15 +571,22 @@ namespace platf::publish { name.reset(avahi::alternative_service_name(name.get())); BOOST_LOG(info) << "Service name collision, renaming service to "sv << name.get(); + // The collision may have been on the service host name instead: + // regenerate it as well so the retry does not collide again. + ++host_name_collision_count; + + published_addresses.reset(); avahi::entry_group_reset(group); - create_services(c); + create_services(c, quit_on_failure); fg.disable(); return; } BOOST_LOG(error) << "Failed to add "sv << platf::SERVICE_TYPE << " service: "sv << avahi::strerror(ret); + published_addresses.reset(); + avahi::entry_group_reset(group); return; } @@ -494,18 +595,119 @@ namespace platf::publish { BOOST_LOG(error) << "Failed to commit entry group: "sv << avahi::strerror(ret); return; } + + published_addresses = addresses; } fg.disable(); } + /* Rebuild the service records after local interface addresses change. */ + void refresh_service_addresses() { + if (!client || !group) { + return; + } + + const auto effective_family = net::get_effective_address_family( + net::af_from_enum_string(config::sunshine.address_family) + ); + const auto current = served_addresses(effective_family); + if (published_addresses && current == *published_addresses) { + return; + } + + BOOST_LOG(info) << "Local interface addresses changed, republishing Avahi service."sv; + published_addresses.reset(); + avahi::entry_group_reset(group); + // A refresh failure must not kill discovery: the group is already reset + // (records withdrawn), and the next change notification retries, while + // quitting the poll loop here would leave the service withdrawn with no + // recovery short of restarting Sunshine. + create_services(client.get(), false); + } + +#ifdef __linux__ + /** + * @brief Refresh service publication after an interface/address change. + * + * @param fd Netlink route socket to drain before refreshing the addresses. + */ + void address_change_callback(avahi::Watch * /*watch*/, int fd, avahi::WatchEvent /*event*/, void * /*userdata*/) { + // Drain the netlink socket: a burst of address/link messages must not + // leave the fd readable, or the poll loop would spin on it. + std::array buffer {}; + while (recv(fd, buffer.data(), buffer.size(), MSG_DONTWAIT) > 0) { + // Discard: the snapshot is re-read from the interfaces, not from the message. + } + + refresh_service_addresses(); + } +#endif + + /* Subscribe to interface/address changes on the Avahi poll thread. */ + bool install_address_change_watch() { +#ifdef __linux__ + if (netlink_watch) { + return true; + } + + // No existing network-change notification exists in Sunshine to reuse: + // the only related code is UPnP's WAN-IP renewal, which is unrelated to + // mDNS. A netlink route socket is the kernel's address/link change + // feed; registering it as an Avahi poll watch dispatches the callback + // on the existing poll thread, so no new thread and no cross-thread + // Avahi calls are introduced. Failure degrades to the old behavior + // (no refresh) instead of failing startup. + const int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, NETLINK_ROUTE); + if (fd < 0) { + BOOST_LOG(warning) << "Failed to open netlink socket for address-change notifications, mDNS records will not refresh on network changes."sv; + return false; + } + + sockaddr_nl address {}; + address.nl_family = AF_NETLINK; + address.nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR; + if (bind(fd, (sockaddr *) &address, sizeof(address)) < 0) { + BOOST_LOG(warning) << "Failed to bind netlink socket for address-change notifications, mDNS records will not refresh on network changes."sv; + close(fd); + return false; + } + + const auto *poll_api = avahi::simple_poll_get(poll.get()); + if (!poll_api) { + BOOST_LOG(warning) << "Failed to get Avahi poll API for address-change notifications, mDNS records will not refresh on network changes."sv; + close(fd); + return false; + } + + auto *watch = poll_api->watch_new(poll_api, fd, avahi::WatchEvent::WATCH_IN, address_change_callback, nullptr); + if (!watch) { + BOOST_LOG(warning) << "Failed to install Avahi watch for address-change notifications, mDNS records will not refresh on network changes."sv; + close(fd); + return false; + } + + netlink_fd = fd; + netlink_watch = watch; + return true; +#else + // Netlink is a Linux-kernel API, so no address-change watch exists on + // other platforms. That is safe because create_services() publishes + // the service with a null SRV target there, selecting the Avahi + // daemon-managed host name: the daemon owns the interface address + // records and updates and withdraws them when addresses change, so + // discovery stays correct without a refresh. + return false; +#endif + } + /** * @brief React to Avahi client state changes and register services when ready. * * @param c Avahi client that emitted the state change. * @param state Avahi client state reported by the callback. */ - void client_callback(avahi::Client *c, avahi::ClientState state, void *) { + void client_callback(avahi::Client *c, avahi::ClientState state, void *) { // NOSONAR(cpp:S5008): Avahi callbacks require a void pointer for user data. switch (state) { case avahi::CLIENT_S_RUNNING: create_services(c); @@ -517,6 +719,9 @@ namespace platf::publish { case avahi::CLIENT_S_COLLISION: case avahi::CLIENT_S_REGISTERING: if (group) { + // The daemon withdraws the entries on collision/re-registration, + // so the committed snapshot is no longer live. + published_addresses.reset(); avahi::entry_group_reset(group); } break; @@ -532,11 +737,11 @@ namespace platf::publish { std::jthread poll_thread; ///< Poll thread. /** - * @brief Store the Avahi polling thread for shutdown on destruction. + * @brief Explicitly store the Avahi polling thread for shutdown on destruction. * * @param poll_thread Poll thread. */ - deinit_t(std::jthread poll_thread): + explicit deinit_t(std::jthread poll_thread): poll_thread {std::move(poll_thread)} { } @@ -552,6 +757,21 @@ namespace platf::publish { poll_thread.join(); } + // The poll thread has exited, so no watch callback can be in flight: + // release the address-change watch and its socket now. +#ifdef __linux__ + if (netlink_watch) { + if (const auto *poll_api = avahi::simple_poll_get(poll.get()); poll_api) { + poll_api->watch_free(netlink_watch); + } + netlink_watch = nullptr; + } + if (netlink_fd >= 0) { + close(netlink_fd); + netlink_fd = -1; + } +#endif + poll.reset(); } }; @@ -583,6 +803,12 @@ namespace platf::publish { return nullptr; } + // Best-effort: without the watch, discovery still works. On Linux the + // records go stale after a network change (the pre-watch behavior); + // on other platforms the daemon-managed host name delegates address + // maintenance to the Avahi daemon, so no refresh is needed. + install_address_change_watch(); + return std::make_unique(std::jthread {[]() { avahi::simple_poll_loop(poll.get()); diff --git a/src/platform/linux/publish.h b/src/platform/linux/publish.h new file mode 100644 index 00000000000..40691eea318 --- /dev/null +++ b/src/platform/linux/publish.h @@ -0,0 +1,402 @@ +/** + * @file src/platform/linux/publish.h + * @brief Declarations for publishing services on Linux (Avahi). + */ +#pragma once + +// standard includes +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// local includes +#include "src/network.h" + +namespace avahi { + /** + * @brief Enumerates supported proto options. + */ + enum class proto : int { + PROTO_INET = 0, ///< IPv4 + PROTO_INET6 = 1, ///< IPv6 + PROTO_UNSPEC = -1 ///< Unspecified/all protocol(s) + }; + + /** + * @brief Avahi protocol/address-family selector type. + */ + using Protocol = int; + + /** + * @brief Address record published for a host name. + * @details Layout-compatible with Avahi's `AvahiAddress` (see + * avahi-common/address.h): a protocol selector followed by 16 bytes of + * address data. An IPv4 address occupies the first 4 bytes in network byte + * order and the remaining bytes are zero; an IPv6 address occupies all 16 + * bytes. + */ + struct Address { + Protocol proto; ///< Address family of the record. + std::array data; ///< Address bytes, see details above. + + /** + * @brief Compare two address records for equality. + * @param other Address record to compare against. + * @return True when the protocol selector and address bytes both match. + */ + bool operator==(const Address &other) const = default; + }; + + static_assert(sizeof(Address) == sizeof(Protocol) + 16, "avahi::Address must match the AvahiAddress layout"); + + /** + * @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 + }; + + /** + * @brief Network interface index type used by Avahi. + */ + using IfIndex = int; + + constexpr auto IF_UNSPEC = -1; ///< Protocol or platform constant for if unspec. + + struct EntryGroup; + struct Client; + struct Poll; + struct SimplePoll; + + /** + * @brief Function pointer used to free Avahi-allocated memory. + */ + using free_fn = void (*)(void *); + + /** + * @brief Function pointer used to destroy an Avahi client. + */ + using client_free_fn = void (*)(Client *); + + /** + * @brief Function pointer used to free an Avahi simple-poll instance. + */ + using simple_poll_free_fn = void (*)(SimplePoll *); + + /** + * @brief Function pointer used to request an alternative Avahi service name. + */ + using alternative_service_name_fn = char *(*)(char *); + + /** + * @brief Function pointer used to get an Avahi client from an entry group. + */ + using entry_group_get_client_fn = Client *(*)(EntryGroup *); + + /** + * @brief Function pointer used to add a service to an Avahi entry group. + */ + using entry_group_add_service_fn = int (*)( + EntryGroup *group, + IfIndex interface, + Protocol protocol, + PublishFlags flags, + const char *name, + const char *type, + const char *domain, + const char *host, + uint16_t port, + ... + ); + + /** + * @brief Function pointer used to add an address record to an Avahi entry group. + */ + using entry_group_add_address_fn = int (*)( + EntryGroup *group, + IfIndex interface, + Protocol protocol, + PublishFlags flags, + const char *name, + const Address *address + ); + + /** + * @brief Function pointer used to test whether an Avahi entry group is empty. + */ + using entry_group_is_empty_fn = int (*)(EntryGroup *); + + /** + * @brief Function pointer used to reset an Avahi entry group. + */ + using entry_group_reset_fn = int (*)(EntryGroup *); + + /** + * @brief Function pointer used to publish an Avahi entry group. + */ + using entry_group_commit_fn = int (*)(EntryGroup *); + + /** + * @brief Function pointer used to format Avahi error codes. + */ + using strerror_fn = char *(*)(int); + + /** + * @brief Function pointer used to read the last Avahi client error. + */ + using client_errno_fn = int (*)(Client *); + + /** + * @brief Function pointer used to stop the Avahi simple-poll loop. + */ + using simple_poll_quit_fn = void (*)(SimplePoll *); + + /** + * @brief Function pointer used to get the Avahi simple-poll API. + */ + using simple_poll_get_fn = Poll *(*)(SimplePoll *); + + extern free_fn free; ///< Free. + extern client_free_fn client_free; ///< Client free. + extern simple_poll_free_fn simple_poll_free; ///< Simple poll free. + extern alternative_service_name_fn alternative_service_name; ///< Alternative service name. + extern entry_group_get_client_fn entry_group_get_client; ///< Entry group get client. + extern entry_group_add_service_fn entry_group_add_service; ///< Entry group add service. + extern entry_group_add_address_fn entry_group_add_address; ///< Entry group add address. + extern entry_group_is_empty_fn entry_group_is_empty; ///< Entry group is empty. + extern entry_group_reset_fn entry_group_reset; ///< Entry group reset. + extern entry_group_commit_fn entry_group_commit; ///< Entry group commit. + extern strerror_fn strerror; ///< Strerror. + extern client_errno_fn client_errno; ///< Client errno. + extern simple_poll_quit_fn simple_poll_quit; ///< Simple poll quit. + extern simple_poll_get_fn simple_poll_get; ///< Simple poll get. +} // namespace avahi + +namespace platf::publish { + /** + * @brief Local address and the interface on which it is reachable. + */ + struct ServedAddress { + avahi::IfIndex interface; ///< Owning interface index. + boost::asio::ip::address address; ///< Local address on that interface. + + /** + * @brief Compare two served addresses for equality. + * @param other Served address to compare against. + * @return True when the interface index and address both match. + */ + bool operator==(const ServedAddress &other) const = default; + }; + + /** + * @brief Release memory allocated by Avahi. + * + * @param p Avahi-allocated pointer to release. + */ + template + void free(T *p) { + avahi::free(p); + } + + /** + * @brief Owning pointer for Avahi allocations released with `avahi_free`. + */ + template + using ptr_t = util::safe_ptr>; + /** + * @brief Owning pointer for an Avahi client. + */ + using client_t = util::dyn_safe_ptr; + /** + * @brief Owning pointer for an Avahi simple poll loop. + */ + using poll_t = util::dyn_safe_ptr; + + /** + * @brief Build the service-specific host name used as the mDNS SRV target. + * @details The name must differ from the daemon's shared host name: the + * daemon publishes both A and AAAA records for its own host name, so + * pointing the service at it would advertise an IPv6 endpoint an IPv4-only + * configuration does not serve (see #5860). The label is sanitized from + * the machine host name (alphanumerics and dashes; spaces become dashes; + * anything else ends the label), suffixed with `-sunshine` (plus + * `-` after a host-name collision), and qualified with + * `.local`. + * @param hostname Machine host name the label is derived from. + * @param collision_suffix Suffix appended after a host-name collision; 0 for the first attempt. + * @return Fully qualified service host name, e.g. `myhost-sunshine.local`. + */ + [[nodiscard]] inline std::string service_host_name(std::string_view hostname, unsigned collision_suffix) { + std::string label; + for (const char c : hostname) { + if (c == ' ') { + label.push_back('-'); + } else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-') { + label.push_back(c); + } else { + break; + } + } + if (label.empty()) { + label = "sunshine"; + } + + std::string suffix = "-sunshine"; + if (collision_suffix > 0) { + suffix += std::format("-{}", collision_suffix); + } + + // A DNS label holds at most 63 characters. + if (constexpr std::size_t MAX_LABEL = 63; label.size() + suffix.size() > MAX_LABEL) { + label.resize(MAX_LABEL - suffix.size()); + } + + return label + suffix + ".local"; + } + + /** + * @brief Select the addresses published for the service host name. + * @details Keeps only addresses Sunshine serves: the candidate is + * normalized, dropped when it is unspecified or loopback, dropped when its + * family falls outside the effective family, and deduplicated by interface + * plus address, so the same address on two interfaces is published once + * per interface it is reachable on. In particular, an IPv4-only + * configuration selects no IPv6 address, so no AAAA endpoint is published + * for the service host name (see #5860). + * @param candidates Candidate addresses, e.g. from interface enumeration. + * @param effective_family Address family Sunshine actually serves. + * @return Addresses to publish, containing no address outside the effective family. + */ + [[nodiscard]] inline std::vector select_served_addresses( + const std::vector &candidates, + net::af_e effective_family + ) { + std::vector selected; + for (auto candidate : candidates) { + candidate.address = net::normalize_address(candidate.address); + if (candidate.address.is_unspecified() || candidate.address.is_loopback()) { + continue; + } + if (effective_family == net::IPV4 && !candidate.address.is_v4()) { + continue; + } + if (std::find(selected.begin(), selected.end(), candidate) == selected.end()) { + selected.push_back(candidate); + } + } + return selected; + } + + /** + * @brief Convert a generic address to the Avahi address-record layout. + * @details The address is normalized first, so IPv4-mapped IPv6 addresses + * become plain IPv4 addresses. + * @param address Address to convert. + * @return Avahi-compatible address record for `address`. + */ + [[nodiscard]] inline avahi::Address to_avahi_address(boost::asio::ip::address address) { + address = net::normalize_address(address); + + avahi::Address result {}; + if (address.is_v4()) { + result.proto = std::to_underlying(avahi::proto::PROTO_INET); + const auto bytes = address.to_v4().to_bytes(); + std::ranges::copy(bytes, result.data.begin()); + } else { + result.proto = std::to_underlying(avahi::proto::PROTO_INET6); + result.data = address.to_v6().to_bytes(); + } + return result; + } + + /** + * @brief Enumerate the local interface addresses available for publication. + * @return Interface addresses with their owning interface index, empty when enumeration fails. + */ + [[nodiscard]] std::vector enumerate_interface_addresses(); + + /** + * @brief Determine the addresses published for the service host name. + * @details An explicit, non-wildcard bind address is published exactly as + * configured, with the interface index of the interface that owns it + * (`IF_UNSPEC` when no local interface owns it). A wildcard (or + * unparsable) bind address publishes the selected local interface + * addresses. + * @param effective_family Address family Sunshine actually serves. + * @return Addresses to publish; empty when none can be determined. + */ + [[nodiscard]] std::vector served_addresses(net::af_e effective_family); + + /** + * @brief Add address records for the service host name to the entry group. + * + * @param entry_group Entry group the address records are added to. + * @param service_host Service host name the address records are published for. + * @param addresses Addresses to publish for the service host name. + * @return 0 when all address records were added; otherwise the Avahi error code from the failed call. + */ + int add_service_addresses( + avahi::EntryGroup *entry_group, + const std::string &service_host, + const std::vector &addresses + ); + + /** + * @brief Create or update the Avahi service entry group. + * + * @param c Avahi client used to allocate and commit the service entry group. + * @param quit_on_failure Quit the Avahi poll loop when registration fails. Initial + * registration keeps the existing fail-fast behavior; a refresh triggered by a + * network change passes false so a transient failure does not kill discovery. + */ + void create_services(avahi::Client *c, bool quit_on_failure = true); + + /** + * @brief Rebuild the service records after local interface addresses change. + * @details Compares the current served addresses against the last published + * snapshot and, when they differ, resets the entry group (withdrawing the + * stale records) and republishes. A no-op when the client or group is not + * yet set up, or when the snapshot is unchanged. + */ + void refresh_service_addresses(); + + /** + * @brief Subscribe to interface/address changes on the Avahi poll thread. + * @details Opens a netlink route socket and registers it as an Avahi poll + * watch, so address/link changes dispatch the refresh on the existing + * poll thread. Best-effort: returns false when the watch cannot be + * installed, and discovery continues without refresh. Linux-only: + * netlink is a Linux-kernel API, so on other platforms this always + * returns false and no watch is installed; the service there uses the + * daemon-managed host name (a null SRV target), so the Avahi daemon + * maintains the address records and no watch is needed. + * @return True when the watch is installed (or was already installed). + */ + bool install_address_change_watch(); + +#ifdef SUNSHINE_TESTS + // Test seams: registration tests drive the publisher with stubbed Avahi + // calls, so the publisher state they need to set is visible here. These + // declarations exist only in the test build. + extern avahi::EntryGroup *group; ///< Active Avahi entry group that owns the published service. + extern poll_t poll; ///< Avahi poll loop used while the service is published. + extern client_t client; ///< Avahi client used to register the Sunshine service. + extern ptr_t name; ///< Current service name, updated when Avahi reports a collision. + extern unsigned host_name_collision_count; ///< Suffix counter for regenerating the service host name after collisions. + extern std::optional> published_addresses; ///< Committed snapshot; nullopt while withdrawn. +#endif +} // namespace platf::publish diff --git a/src/platform/linux/vaapi.cpp b/src/platform/linux/vaapi.cpp index 1badbb7a8c9..20f4f949283 100644 --- a/src/platform/linux/vaapi.cpp +++ b/src/platform/linux/vaapi.cpp @@ -37,11 +37,11 @@ extern "C" { #if !VA_CHECK_VERSION(1, 21, 0) /** * @brief Stub vaMapBuffer2 when building against libva before 2.21.0. + * @details The older vaMapBuffer API does not accept the mapping flags supplied by the new ABI. * * @param dpy VA display. * @param buf_id VA buffer ID. * @param pbuf Output mapped buffer pointer. - * @param flags Mapping flags. * @return VA status code. */ VAStatus @@ -49,7 +49,7 @@ extern "C" { VADisplay dpy, VABufferID buf_id, void **pbuf, - uint32_t flags + uint32_t ) { return vaMapBuffer(dpy, buf_id, pbuf); } @@ -77,7 +77,7 @@ namespace va { /** * @brief Native VA display handle. */ - using VADisplay = void *; + using VADisplay = void *; // NOSONAR(cpp:S5008): The libva ABI defines VADisplay as a void pointer. /** * @brief Status code returned by VAAPI functions. */ @@ -154,7 +154,7 @@ namespace va { /** * @brief VA display handle released with `vaTerminate`. */ - using display_t = util::safe_ptr_v2; + using display_t = util::safe_ptr_v2; /** * @brief Create an FFmpeg VA-API hardware device context from a Sunshine encode device. @@ -186,7 +186,8 @@ namespace va { return -1; } - this->data = (void *) vaapi_init_avcodec_hardware_input_buffer; + hardware = true; + init_hw_device = &vaapi_init_avcodec_hardware_input_buffer; gbm.reset(gbm::create_device(file.el)); if (!gbm) { @@ -263,17 +264,17 @@ namespace va { /** * @brief Determines the matching VA profile for the codec configuration. - * @param ctx The FFmpeg codec context. + * @param codec_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) { + VAProfile get_va_profile(AVCodecContext *codec_ctx) { + if (codec_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) { + } else if (codec_ctx->codec_id == AV_CODEC_ID_HEVC) { + switch (codec_ctx->profile) { case AV_PROFILE_HEVC_REXT: - switch (av_pix_fmt_desc_get(ctx->sw_pix_fmt)->comp[0].depth) { + switch (av_pix_fmt_desc_get(codec_ctx->sw_pix_fmt)->comp[0].depth) { case 10: return VAProfileHEVCMain444_10; case 8: @@ -285,8 +286,8 @@ namespace va { case AV_PROFILE_HEVC_MAIN: return VAProfileHEVCMain; } - } else if (ctx->codec_id == AV_CODEC_ID_AV1) { - switch (ctx->profile) { + } else if (codec_ctx->codec_id == AV_CODEC_ID_AV1) { + switch (codec_ctx->profile) { case AV_PROFILE_AV1_HIGH: return VAProfileAV1Profile1; case AV_PROFILE_AV1_MAIN: @@ -294,18 +295,18 @@ namespace va { } } - BOOST_LOG(error) << "Unknown encoder profile: "sv << ctx->profile; + BOOST_LOG(error) << "Unknown encoder profile: "sv << codec_ctx->profile; return VAProfileNone; } /** * @brief Initialize codec options. * - * @param ctx Native context object used by the operation or callback. + * @param codec_ctx Native context object used by the operation or callback. * @param options Request options or socket options to apply. */ - void init_codec_options(AVCodecContext *ctx, AVDictionary **options) override { - auto va_profile = get_va_profile(ctx); + void init_codec_options(AVCodecContext *codec_ctx, AVDictionary **options) override { + auto va_profile = get_va_profile(codec_ctx); if (va_profile == VAProfileNone || !is_va_profile_supported(va_profile)) { // Don't bother doing anything if the profile isn't supported return; @@ -351,8 +352,8 @@ namespace va { break; } if (target_quality > 0) { - ctx->compression_level = target_quality; - BOOST_LOG(info) << "[VAAPI] Quality level set to "sv << ctx->compression_level << " (fastest level: "sv << quality_attr.value << ")"sv; + codec_ctx->compression_level = target_quality; + BOOST_LOG(info) << "[VAAPI] Quality level set to "sv << codec_ctx->compression_level << " (fastest level: "sv << quality_attr.value << ")"sv; } VAConfigAttrib rc_attr = {.type = VAConfigAttribRateControl}; @@ -368,9 +369,9 @@ namespace va { // Assume only a single slice is supported slice_attr.value = 1; } - if (ctx->slices > slice_attr.value) { + if (codec_ctx->slices > slice_attr.value) { BOOST_LOG(info) << "Limiting slice count to encoder maximum: "sv << slice_attr.value; - ctx->slices = slice_attr.value; + codec_ctx->slices = slice_attr.value; } // Use VBR with a single frame VBV when the user forces it and for known good cases: @@ -394,7 +395,7 @@ namespace va { auto rc_val = config::video.vaapi.vaapi_rc.value_or(0); // Detect whitelisted configurations - if ((vendor && std::string_view(vendor).contains("Intel") == true) || ctx->codec_id == AV_CODEC_ID_AV1) { + if ((vendor && std::string_view(vendor).contains("Intel") == true) || codec_ctx->codec_id == AV_CODEC_ID_AV1) { auto_whitelist = true; rc_whitelist = " (whitelist override)"; } @@ -416,7 +417,7 @@ namespace va { } if (config::video.vaapi.strict_rc_buffer || auto_whitelist) { - ctx->rc_buffer_size = ctx->bit_rate * ctx->framerate.den / ctx->framerate.num; + codec_ctx->rc_buffer_size = codec_ctx->bit_rate * codec_ctx->framerate.den / codec_ctx->framerate.num; rc_vbv = "with single frame VBV size"; } @@ -435,23 +436,23 @@ namespace va { /** * @brief Attach frame resources used by the next conversion or encode operation. * - * @param frame Video or graphics frame being processed. + * @param new_frame Video or graphics frame being processed. * @param hw_frames_ctx_buf Hardware frames context buffer. * @return Status from updating frame. */ - int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx_buf) override { - this->hwframe.reset(frame); - this->frame = frame; + int set_frame(AVFrame *new_frame, AVBufferRef *hw_frames_ctx_buf) override { + this->hwframe.reset(new_frame); + this->frame = new_frame; - if (!frame->buf[0]) { - if (av_hwframe_get_buffer(hw_frames_ctx_buf, frame, 0)) { + if (!new_frame->buf[0]) { + if (av_hwframe_get_buffer(hw_frames_ctx_buf, new_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]; + va::VASurfaceID surface = (std::uintptr_t) new_frame->data[3]; auto hw_frames_ctx = (AVHWFramesContext *) hw_frames_ctx_buf->data; auto status = vaExportSurfaceHandle( @@ -505,7 +506,7 @@ namespace va { return -1; } - auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height, hw_frames_ctx->sw_format, false); + auto sws_opt = egl::sws_t::make(width, height, new_frame->width, new_frame->height, hw_frames_ctx->sw_format, false); if (!sws_opt) { return -1; } @@ -603,19 +604,19 @@ namespace va { * @param in_width In width. * @param in_height In height. * @param render_device Render device. - * @param offset_x Offset x. - * @param offset_y Offset y. + * @param in_offset_x Offset x. + * @param in_offset_y Offset y. * @return 0 on success; nonzero or negative platform status on failure. */ - int init(int in_width, int in_height, file_t &&render_device, int offset_x, int offset_y) { + int init(int in_width, int in_height, file_t &&render_device, int in_offset_x, int in_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; + this->offset_x = in_offset_x; + this->offset_y = in_offset_y; return 0; } @@ -628,26 +629,25 @@ namespace va { }; /** - * This is a private structure of FFmpeg, I need this to manually create - * a VAAPI hardware context + * @brief FFmpeg-private device data used to create a VA-API hardware context. * - * xdisplay will not be used internally by FFmpeg + * The X11 display pointer is not used internally by FFmpeg. */ - typedef struct VAAPIDevicePriv { + struct VAAPIDevicePriv { union { void *xdisplay; ///< Native X11 display handle for display-backed contexts. int fd; ///< DRM file descriptor for device-backed contexts. } drm; ///< Native display or DRM fd passed to FFmpeg's VA-API context. int drm_fd; ///< DRM fd. - } VAAPIDevicePriv; + }; /** - * VAAPI connection details. + * @brief VA-API connection details allocated as AVHWDeviceContext.hwctx. * - * Allocated as AVHWDeviceContext.hwctx + * The layout must match the corresponding FFmpeg hardware context. */ - typedef struct AVVAAPIDeviceContext { + struct AVVAAPIDeviceContext { /** * The VADisplay handle, to be filled by the user. */ @@ -660,7 +660,7 @@ namespace va { * 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; diff --git a/src/platform/linux/vulkan_encode.cpp b/src/platform/linux/vulkan_encode.cpp index cc1b7972f60..b7c9c6d21d8 100644 --- a/src/platform/linux/vulkan_encode.cpp +++ b/src/platform/linux/vulkan_encode.cpp @@ -450,7 +450,8 @@ namespace vk { height = in_height; offset_x = in_offset_x; offset_y = in_offset_y; - this->data = (void *) &init_hw_device; + hardware = true; + avcodec_encode_device_t::init_hw_device = &init_hw_device; return 0; } diff --git a/src/platform/linux/wayland.cpp b/src/platform/linux/wayland.cpp index 46b5eefd3a9..5ec50c0e031 100644 --- a/src/platform/linux/wayland.cpp +++ b/src/platform/linux/wayland.cpp @@ -58,17 +58,17 @@ namespace wl { reset(); } - bool gbm_device_t::init(const std::string &render_path, const gbm_device_accessors_t &accessors) { + bool gbm_device_t::init(const std::string &render_path, const gbm_device_accessors_t &device_accessors) { reset(); - this->accessors = accessors; + accessors = device_accessors; - drm_fd = accessors.open_render_node(render_path.c_str()); + drm_fd = device_accessors.open_render_node(render_path.c_str()); if (drm_fd < 0) { BOOST_LOG(error) << "[wayland] Failed to open DRM render node: "sv << render_path; return false; } - device = accessors.create_device(drm_fd); + device = device_accessors.create_device(drm_fd); if (!device) { BOOST_LOG(error) << "[wayland] Failed to create GBM device"sv; reset(); @@ -93,7 +93,7 @@ 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...)) { + static auto classCall(void *data, Params... params) -> decltype(((*reinterpret_cast(data)).*m)(params...)) { // NOSONAR(cpp:S5008): Wayland listener callbacks require a void pointer for user data. return ((*reinterpret_cast(data)).*m)(params...); } @@ -257,6 +257,7 @@ namespace wl { } void interface_t::dmabuf_format(zwp_linux_dmabuf_v1 *zwp_linux_dmabuf, uint32_t format) { + // Modifier events supply the explicit format/modifier pairs used for allocation. } void interface_t::dmabuf_modifier(zwp_linux_dmabuf_v1 *zwp_linux_dmabuf, uint32_t format, uint32_t modifier_hi, uint32_t modifier_lo) { @@ -269,35 +270,35 @@ namespace wl { void interface_t::add_interface( wl_registry *registry, std::uint32_t id, - const char *interface, + const char *interface_name, std::uint32_t version ) { - BOOST_LOG(debug) << "[wayland] Available interface: "sv << interface << '(' << id << ") version "sv << version; + BOOST_LOG(debug) << "[wayland] Available interface: "sv << interface_name << '(' << id << ") version "sv << version; - if (!std::strcmp(interface, wl_output_interface.name)) { - BOOST_LOG(info) << "[wayland] Found interface: "sv << interface << '(' << id << ") version "sv << version; + if (!std::strcmp(interface_name, wl_output_interface.name)) { + BOOST_LOG(info) << "[wayland] Found interface: "sv << interface_name << '(' << 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) << "[wayland] Found interface: "sv << interface << '(' << id << ") version "sv << version; + } else if (!std::strcmp(interface_name, zxdg_output_manager_v1_interface.name)) { + BOOST_LOG(info) << "[wayland] Found interface: "sv << interface_name << '(' << 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) << "[wayland] Found interface: "sv << interface << '(' << id << ") version "sv << version; + interface[XDG_OUTPUT] = true; + } else if (!std::strcmp(interface_name, zwlr_screencopy_manager_v1_interface.name)) { + BOOST_LOG(info) << "[wayland] Found interface: "sv << interface_name << '(' << id << ") version "sv << version; screencopy_manager = (zwlr_screencopy_manager_v1 *) wl_registry_bind(registry, id, &zwlr_screencopy_manager_v1_interface, version); screencopy_version = version; - this->interface[WLR_EXPORT_DMABUF] = true; - } else if (!std::strcmp(interface, zwp_linux_dmabuf_v1_interface.name)) { - BOOST_LOG(info) << "[wayland] Found interface: "sv << interface << '(' << id << ") version "sv << version; + interface[WLR_EXPORT_DMABUF] = true; + } else if (!std::strcmp(interface_name, zwp_linux_dmabuf_v1_interface.name)) { + BOOST_LOG(info) << "[wayland] Found interface: "sv << interface_name << '(' << id << ") version "sv << version; dmabuf_interface = (zwp_linux_dmabuf_v1 *) wl_registry_bind(registry, id, &zwp_linux_dmabuf_v1_interface, std::min(version, 3u)); zwp_linux_dmabuf_v1_add_listener(dmabuf_interface, &dmabuf_listener, this); - this->interface[LINUX_DMABUF] = true; + interface[LINUX_DMABUF] = true; } } @@ -358,9 +359,20 @@ namespace wl { return result; } + bool should_attempt_linear_copy(bool prefer_linear_copy, const std::vector &modifiers) { + return prefer_linear_copy && std::find(modifiers.begin(), modifiers.end(), DRM_FORMAT_MOD_LINEAR) != modifiers.end(); + } + + const std::vector *find_advertised_modifiers(const std::map> *modifiers, std::uint32_t format) { + if (modifiers) { + if (const auto it = modifiers->find(format); it != modifiers->end() && !it->second.empty()) { + return &it->second; + } + } + return nullptr; + } + dmabuf_t::dmabuf_t(): - status {READY}, - frames {}, current_frame {&frames[0]}, listener { &CLASS_CALL(dmabuf_t, buffer), @@ -376,15 +388,17 @@ namespace wl { // Start capture void dmabuf_t::listen( zwlr_screencopy_manager_v1 *screencopy_manager, - zwp_linux_dmabuf_v1 *dmabuf_interface, - const std::map> *supported_modifiers, + zwp_linux_dmabuf_v1 *dmabuf_interface_in, + const std::map> *compositor_formats_in, wl_output *output, bool blend_cursor, - const std::map> *encoder_modifiers + const std::map> *encoder_formats_in, + bool prefer_linear_copy_in ) { - this->dmabuf_interface = dmabuf_interface; - this->supported_modifiers = supported_modifiers; - this->encoder_modifiers = encoder_modifiers; + dmabuf_interface = dmabuf_interface_in; + supported_modifiers = compositor_formats_in; + encoder_modifiers = encoder_formats_in; + prefer_linear_copy = prefer_linear_copy_in; // Reset state shm_info.supported = false; dmabuf_info.supported = false; @@ -485,10 +499,20 @@ namespace wl { // Create GBM buffer - prefer encoder-intersected modifiers when available const std::map> *modifiers_to_use = encoder_modifiers ? encoder_modifiers : supported_modifiers; - if (modifiers_to_use) { - auto it = modifiers_to_use->find(dmabuf_info.format); - if (it != modifiers_to_use->end() && !it->second.empty()) { - current_bo = gbm_bo_create_with_modifiers2(gbm.get(), dmabuf_info.width, dmabuf_info.height, dmabuf_info.format, it->second.data(), it->second.size(), GBM_BO_USE_RENDERING); + if (const auto *advertised = find_advertised_modifiers(modifiers_to_use, dmabuf_info.format); advertised) { + if (should_attempt_linear_copy(prefer_linear_copy, *advertised)) { + // Cross-GPU encode (e.g. NVENC on a headless NVIDIA GPU while the compositor + // renders on AMD/Intel) cannot import vendor-tiled modifiers, so request a + // linear buffer the encoder is guaranteed to import instead. Note: no + // GBM_BO_USE_LINEAR here, Mesa rejects that flag with modifiers2. + constexpr std::uint64_t linear_modifier = DRM_FORMAT_MOD_LINEAR; + current_bo = gbm_bo_create_with_modifiers2(gbm.get(), dmabuf_info.width, dmabuf_info.height, dmabuf_info.format, &linear_modifier, 1, GBM_BO_USE_RENDERING); + if (!current_bo) { + BOOST_LOG(debug) << "Linear screencopy buffer unavailable, falling back to compositor modifiers"sv; + } + } + if (!current_bo) { + current_bo = gbm_bo_create_with_modifiers2(gbm.get(), dmabuf_info.width, dmabuf_info.height, dmabuf_info.format, advertised->data(), advertised->size(), GBM_BO_USE_RENDERING); } } @@ -506,7 +530,7 @@ namespace wl { // Export every memory plane into the surface descriptor auto next_frame = get_next_frame(); const auto plane_count = export_gbm_bo_planes(current_bo, *next_frame, gbm_bo_accessors); - if (!plane_count) { + if (!plane_count.has_value()) { gbm_bo_destroy(current_bo); current_bo = nullptr; zwlr_screencopy_frame_v1_destroy(frame); @@ -666,7 +690,9 @@ namespace wl { std::uint32_t y, std::uint32_t width, std::uint32_t height - ) {}; + ) { + // Capture and encoding consume the whole buffer, so no damage rectangles are tracked. + }; void frame_t::destroy() { for (auto x = 0; x < 4; ++x) { diff --git a/src/platform/linux/wayland.h b/src/platform/linux/wayland.h index 0eb4edc9e28..616d51ec891 100644 --- a/src/platform/linux/wayland.h +++ b/src/platform/linux/wayland.h @@ -9,9 +9,11 @@ #include #include #include +#include #include #include #include +#include #include #ifdef SUNSHINE_BUILD_WAYLAND @@ -31,15 +33,144 @@ struct gbm_bo; struct gbm_device; +struct _drmDevice; namespace wl { + /** + * @brief PCI bus address identifying one GPU. + */ + struct pci_bus_id { + std::uint32_t domain {}; ///< PCI domain number. + std::uint8_t bus {}; ///< PCI bus number. + std::uint8_t device {}; ///< PCI device number. + std::uint8_t function {}; ///< PCI function number. + + /** + * @brief Compare two PCI bus addresses for equality. + * @return True when domain, bus, device, and function all match. + */ + bool operator==(const pci_bus_id &) const = default; + }; + + /** + * @brief Read the PCI bus id of a DRM render node, without parsing it. + * + * @param render_path Render node path to query. + * @return Raw bus id (e.g. "0000:0f:00.0"), or no value when it cannot be read. + */ + using render_node_pci_reader_t = std::function(const std::string &render_path)>; + + /** + * @brief Read the PCI bus id of a CUDA device, without parsing it. + * + * @param cuda_index CUDA device index to query. + * @return Raw bus id reported by the driver (e.g. "00000000:0F:00.0"), + * or no value when CUDA is unavailable or the query fails. + */ + using cuda_device_pci_reader_t = std::function(int cuda_index)>; + + /** + * @brief Resolve a DRM render node path to its canonical device node. + * + * @param path Render node path to resolve. + * @return Canonical device path supplied by the resolver. + */ + using render_path_resolver_t = std::function; + + /** + * @brief Resolve a DRM render node path, following udev by-path aliases. + * + * @param path Render node path to resolve, possibly a by-path alias. + * @return Canonical device node path, or the input path verbatim when it cannot be resolved. + */ + std::string resolve_drm_node_path(const std::string &path); + + /** + * @brief Parse a PCI bus id in sysfs or CUDA form. + * + * Accepts both the sysfs rendering ("dddd:bb:dd.f") and the CUDA rendering + * ("dddddddd:BB:DD.F"); comparison is numeric so width and case differences + * do not matter. + * + * @param bus_id Bus id text to parse. + * @return Parsed address, or no value when the text is malformed. + */ + std::optional parse_pci_bus_id(std::string_view bus_id); + + /** + * @brief Functions used to query the PCI identity of a DRM render node. + */ + struct drm_device_accessors_t { + int (*open_render_node)(const char *path); ///< Open the render node and return its descriptor, or -1. + int (*get_device)(int fd, std::uint32_t flags, _drmDevice **device); ///< Query the DRM device, returning 0 on success. + void (*free_device)(_drmDevice **device); ///< Release a DRM device returned by the query. + }; + + /** + * @brief Query the PCI bus id of a DRM render node from its device. + * + * @param render_path Render node path to query. + * @return Bus id text, or no value when the node cannot be opened or is not a PCI device. + */ + std::optional render_node_pci_bus_id(const std::string &render_path); + + /** + * @brief Query the PCI bus id of a DRM render node with injected device operations. + * + * @param render_path Render node path to query. + * @param accessors Device operations used to open and query the node. + * @return Bus id text, or no value when the node cannot be opened or is not a PCI device. + */ + std::optional render_node_pci_bus_id(const std::string &render_path, const drm_device_accessors_t &accessors); + + /** + * @brief Check whether the capture render node is the CUDA encode device. + * + * Zero-copy VRAM capture hands the compositor's DMA-BUF directly to the + * encoder's GL context, which is only valid when capture and encode run on + * the same GPU, so identity is compared by PCI bus address. Anything + * unprovable (unreadable node, unavailable CUDA, unparsable ids, distinct + * addresses) selects the GPU -> RAM -> GPU bridge. + * + * @param read_render_pci Render node PCI reader; queries the node directly when empty. + * @param read_cuda_pci CUDA device PCI reader; queries the driver directly when empty. + * @param resolve_path Render node path resolver; resolves aliases directly when empty. + * @param cuda_index CUDA device index NVENC encodes on (always 0: video.cpp creates the CUDA hwdevice with a null device string). + * @return True when the capture node and the CUDA device share a PCI address. + */ + bool capture_node_is_cuda_device(const render_node_pci_reader_t &read_render_pci = {}, const cuda_device_pci_reader_t &read_cuda_pci = {}, const render_path_resolver_t &resolve_path = {}, int cuda_index = 0); + + /** + * @brief Check whether a linear copy buffer should be attempted for the format. + * + * The linear attempt is only valid when the compositor advertised the linear + * modifier for the format; otherwise allocation falls through to the + * advertised modifier list. + * + * @param prefer_linear_copy Whether linear copy buffers are preferred for cross-GPU encode. + * @param modifiers Effective modifier list advertised for the format. + * @return True only when linear copy is preferred and the linear modifier is advertised. + */ + bool should_attempt_linear_copy(bool prefer_linear_copy, const std::vector &modifiers); + + /** + * @brief Look up the advertised modifier list for a DMA-BUF format. + * + * @param modifiers Modifier map to search, or nullptr when no list is available. + * @param format DMA-BUF format to look up. + * @return Pointer to the advertised list, or nullptr when the map is missing, the format is absent, or the list is empty. + */ + const std::vector *find_advertised_modifiers(const std::map> *modifiers, std::uint32_t format); + /** * @brief Determine whether wlroots capture should keep frames in VRAM for the requested memory type. * * @param hwdevice_type Hardware device type requested for capture or encode. + * @param read_render_pci Render node PCI reader forwarded to the CUDA-device identity check. + * @param read_cuda_pci CUDA device PCI reader forwarded to the CUDA-device identity check. * @return `true` when the requested memory type should use the wlroots VRAM path. */ - bool use_vram_capture(platf::mem_type_e hwdevice_type); + bool use_vram_capture(platf::mem_type_e hwdevice_type, const render_node_pci_reader_t &read_render_pci = {}, const cuda_device_pci_reader_t &read_cuda_pci = {}); /** * @brief Determine whether wlroots capture should ask for frames with `copy_with_damage`. @@ -214,10 +345,10 @@ namespace wl { * @brief Open the render node and create the GBM device on it. * * @param render_path Path of the DRM render node. - * @param accessors Functions used to open the node and create the device. + * @param device_accessors Functions used to open the node and create the device. * @return `true` when the device is ready, `false` when nothing is held. */ - bool init(const std::string &render_path, const gbm_device_accessors_t &accessors); + bool init(const std::string &render_path, const gbm_device_accessors_t &device_accessors); /** * @brief Destroy the GBM device and close the render-node descriptor. @@ -302,13 +433,14 @@ namespace wl { * @brief Start a screencopy request and attach the DMA-BUF listener callbacks. * * @param screencopy_manager Compositor screencopy manager used to request frames. - * @param dmabuf_interface Compositor DMA-BUF interface used to allocate buffers. - * @param supported_modifiers DMA-BUF format modifiers supported by the compositor. + * @param dmabuf_interface_in Compositor DMA-BUF interface used to allocate buffers. + * @param compositor_formats_in DMA-BUF format modifiers supported by the compositor. * @param output Wayland output to capture. * @param blend_cursor Whether the compositor should include the cursor in the frame. - * @param encoder_modifiers Optional modifiers supported by the encoder for format intersection. + * @param encoder_formats_in Optional modifiers supported by the encoder for format intersection. + * @param prefer_linear_copy_in Allocate linear copy buffers for cross-GPU encode import. */ - void listen(zwlr_screencopy_manager_v1 *screencopy_manager, zwp_linux_dmabuf_v1 *dmabuf_interface, const std::map> *supported_modifiers, wl_output *output, bool blend_cursor = false, const std::map> *encoder_modifiers = nullptr); + void listen(zwlr_screencopy_manager_v1 *screencopy_manager, zwp_linux_dmabuf_v1 *dmabuf_interface_in, const std::map> *compositor_formats_in, wl_output *output, bool blend_cursor = false, const std::map> *encoder_formats_in = nullptr, bool prefer_linear_copy_in = false); /** * @brief Store the Wayland buffer created for a DMA-BUF parameter request. * @@ -401,9 +533,9 @@ namespace wl { return current_frame == &frames[0] ? &frames[1] : &frames[0]; } - status_e status; ///< Current state of the active screencopy request. + status_e status {READY}; ///< Current state of the active screencopy request, initially ready. bool with_damage {false}; ///< Ask the compositor for the next frame that differs, rather than for a frame now. - std::array frames; ///< Double-buffered frame descriptors. + std::array frames {}; ///< Double-buffered frame descriptors. frame_t *current_frame; ///< Frame descriptor currently being filled by the compositor. zwlr_screencopy_frame_v1_listener listener; ///< Callback table registered on screencopy frames. @@ -415,6 +547,7 @@ namespace wl { zwp_linux_dmabuf_v1 *dmabuf_interface {nullptr}; const std::map> *supported_modifiers {nullptr}; const std::map> *encoder_modifiers {nullptr}; ///< Optional encoder modifiers for intersection. + bool prefer_linear_copy {false}; ///< Allocate linear copy buffers for cross-GPU encode import. struct { bool supported {false}; @@ -489,24 +622,19 @@ namespace wl { void xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height); /** - * @brief Acknowledge completion of an xdg-output metadata update. + * @brief Receive completion of an xdg-output metadata update. */ - void xdg_done(zxdg_output_v1 *) {} + void xdg_done(zxdg_output_v1 *) { + // Size and position callbacks already store the metadata; completion needs no action. + } /** - * @brief Receive physical wl-output geometry metadata. - * - * @param wl_output Wayland output. - * @param x Horizontal coordinate reported by Wayland. - * @param y Vertical coordinate reported by Wayland. - * @param physical_width Physical width in millimeters. - * @param physical_height Physical height in millimeters. - * @param subpixel Wayland subpixel layout enum. - * @param make Monitor manufacturer string reported by Wayland. - * @param model Monitor model string reported by Wayland. - * @param transform Wayland output transform value. + * @brief Ignore physical wl-output geometry; monitor placement comes from xdg-output metadata. + * @details Retain the wl-output listener signature while ignoring all event arguments. */ - 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_geometry(wl_output *, std::int32_t, std::int32_t, std::int32_t, std::int32_t, std::int32_t, const char *, const char *, std::int32_t) { + // Logical placement comes from xdg-output, so physical geometry has nothing to update. + } /** * @brief Store the active pixel mode reported by wl-output. @@ -521,18 +649,19 @@ namespace wl { /** * @brief Acknowledge completion of a wl-output metadata update. - * - * @param wl_output Wayland output. + * @details Retain the wl-output listener signature while ignoring the output argument. */ - void wl_done(wl_output *wl_output) {} + void wl_done(wl_output *) { + // Metadata is stored as each event arrives; completion requires no additional work. + } /** - * @brief Receive the wl-output scale factor. - * - * @param wl_output Wayland output. - * @param factor Wayland output scale factor. + * @brief Ignore wl-output scale; capture uses the active pixel mode and xdg-output logical size. + * @details Retain the wl-output listener signature while ignoring both event arguments. */ - void wl_scale(wl_output *wl_output, std::int32_t factor) {} + void wl_scale(wl_output *, std::int32_t) { + // Pixel mode and xdg-output logical size already describe the capture and input coordinate spaces. + } wl_output *output; ///< Wayland output associated with this monitor. std::string name; ///< xdg-output name used for display selection. @@ -613,7 +742,15 @@ namespace wl { zxdg_output_manager_v1 *output_manager {nullptr}; ///< xdg-output global used to query monitor names and sizes. private: - void add_interface(wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version); + /** + * @brief Bind supported globals advertised by the Wayland registry. + * + * @param registry Registry advertising the global. + * @param id Numeric name used to bind the global. + * @param interface_name Protocol interface name advertised by the compositor. + * @param version Protocol version advertised for the global. + */ + void add_interface(wl_registry *registry, std::uint32_t id, const char *interface_name, std::uint32_t version); void del_interface(wl_registry *registry, uint32_t id); std::bitset interface; diff --git a/src/platform/linux/wlgrab.cpp b/src/platform/linux/wlgrab.cpp index c5cd887c786..5bd32032128 100644 --- a/src/platform/linux/wlgrab.cpp +++ b/src/platform/linux/wlgrab.cpp @@ -3,8 +3,21 @@ * @brief Definitions for wlgrab capture. */ // standard includes +#include +#include +#include +#include +#include +#include +#include +#include #include +// platform includes +#include +#include +#include + // local includes #include "cuda.h" #include "src/logging.h" @@ -20,18 +33,149 @@ namespace wl { static int env_width; static int env_height; - bool use_vram_capture(platf::mem_type_e hwdevice_type) { - if (hwdevice_type == platf::mem_type_e::vaapi) { + namespace { + int open_render_node_read_only(const char *path) { + return open(path, O_RDONLY | O_CLOEXEC); + } + + const drm_device_accessors_t drm_device_accessors { + .open_render_node = open_render_node_read_only, + .get_device = drmGetDevice2, + .free_device = drmFreeDevice, + }; + } // namespace + + /** + * @brief Resolve a DRM render node path, following udev by-path aliases. + */ + std::string resolve_drm_node_path(const std::string &path) { + std::error_code ec; + const auto canonical = std::filesystem::weakly_canonical(path, ec); + if (ec) { + return path; + } + return canonical.string(); + } + + /** + * @brief Parse one hexadecimal PCI address component with full consumption. + * + * @param token Component text to parse, without any 0x prefix. + * @param max_digits Maximum accepted component width. + * @return Parsed value, or no value when the token is empty, too wide, or not plain hex. + */ + static std::optional parse_pci_component(std::string_view token, std::size_t max_digits) { + if (token.empty() || token.size() > max_digits) { + return std::nullopt; + } + std::uint32_t value {}; + if (const auto result = std::from_chars(token.data(), token.data() + token.size(), value, 16); result.ec == std::errc {} && result.ptr == token.data() + token.size()) { + return value; + } + return std::nullopt; + } + + std::optional parse_pci_bus_id(std::string_view bus_id) { + const auto first = bus_id.find(':'); + if (first == std::string_view::npos) { + return std::nullopt; + } + const auto second = bus_id.find(':', first + 1); + if (second == std::string_view::npos) { + return std::nullopt; + } + const auto dot = bus_id.find('.', second + 1); + if (dot == std::string_view::npos) { + return std::nullopt; + } + const auto domain = parse_pci_component(bus_id.substr(0, first), 8); + const auto bus = parse_pci_component(bus_id.substr(first + 1, second - first - 1), 2); + const auto device = parse_pci_component(bus_id.substr(second + 1, dot - second - 1), 2); + const auto function = parse_pci_component(bus_id.substr(dot + 1), 1); + if (!domain.has_value() || !bus.has_value() || !device.has_value() || !function.has_value() || *function > 0x7) { + return std::nullopt; + } + return pci_bus_id {*domain, static_cast(*bus), static_cast(*device), static_cast(*function)}; + } + + std::optional render_node_pci_bus_id(const std::string &render_path) { + return render_node_pci_bus_id(render_path, drm_device_accessors); + } + + std::optional render_node_pci_bus_id(const std::string &render_path, const drm_device_accessors_t &accessors) { + const int fd = accessors.open_render_node(render_path.c_str()); + if (fd < 0) { + return std::nullopt; + } + drmDevicePtr device {}; + const int status = accessors.get_device(fd, 0, &device); + close(fd); + if (status != 0 || !device) { + return std::nullopt; + } + if (device->bustype != DRM_BUS_PCI || !device->businfo.pci) { + accessors.free_device(&device); + return std::nullopt; + } + const auto pci = device->businfo.pci; + std::string bus_id = std::format("{:04x}:{:02x}:{:02x}.{:x}", pci->domain, pci->bus, pci->dev, pci->func); + accessors.free_device(&device); + return bus_id; + } + +#ifdef SUNSHINE_BUILD_CUDA + static std::optional default_cuda_pci_bus_id(int cuda_index) { + return cuda::device_pci_bus_id(cuda_index); + } +#else + static std::optional default_cuda_pci_bus_id(int /*cuda_index*/) { + return std::nullopt; + } +#endif + + bool capture_node_is_cuda_device(const render_node_pci_reader_t &read_render_pci, const cuda_device_pci_reader_t &read_cuda_pci, const render_path_resolver_t &resolve_path, int cuda_index) { + const auto render_path = platf::resolve_render_device(); + const auto resolved_path = resolve_path ? resolve_path(render_path) : resolve_drm_node_path(render_path); + const auto render_pci = read_render_pci ? read_render_pci(resolved_path) : render_node_pci_bus_id(resolved_path); + if (!render_pci) { + BOOST_LOG(warning) << "[wlgrab] Could not read PCI bus id for ["sv << render_path << "], assuming cross-GPU capture"sv; + return false; + } + const auto cuda_pci = read_cuda_pci ? read_cuda_pci(cuda_index) : default_cuda_pci_bus_id(cuda_index); + if (!cuda_pci) { + BOOST_LOG(warning) << "[wlgrab] Could not read PCI bus id for CUDA device ["sv << cuda_index << "], assuming cross-GPU capture"sv; + return false; + } + const auto render_id = parse_pci_bus_id(*render_pci); + const auto cuda_id = parse_pci_bus_id(*cuda_pci); + if (!render_id || !cuda_id) { + BOOST_LOG(warning) << "[wlgrab] Could not parse PCI bus ids (capture ["sv << *render_pci << "], CUDA ["sv << *cuda_pci << "]), assuming cross-GPU capture"sv; + return false; + } + return *render_id == *cuda_id; + } + + bool use_vram_capture(platf::mem_type_e hwdevice_type, const render_node_pci_reader_t &read_render_pci, const cuda_device_pci_reader_t &read_cuda_pci) { + using enum platf::mem_type_e; + if (hwdevice_type == vaapi) { return true; } - if (hwdevice_type == platf::mem_type_e::vulkan) { + if (hwdevice_type == vulkan) { return true; } #ifdef SUNSHINE_BUILD_CUDA - if (hwdevice_type == platf::mem_type_e::cuda) { - return true; + if (hwdevice_type == cuda) { + // Zero-copy is only valid when capture and NVENC run on the same GPU, so + // compare PCI identity against the CUDA encode device instead of trusting + // the vendor name: split render/encode across two discrete NVIDIA GPUs + // must take the bridge too. Otherwise (e.g. AMD/Intel render + NVIDIA + // NVENC) fall back to GPU -> RAM -> GPU: wlr_ram_t capture plus cuda_ram_t + // conversion. + const bool same_gpu = capture_node_is_cuda_device(read_render_pci, read_cuda_pci); + BOOST_LOG(info) << "[wlgrab] CUDA capture path: "sv << (same_gpu ? "zero-copy VRAM (capture node is the CUDA device)"sv : "GPU -> RAM -> GPU bridge (cross-GPU system)"sv); + return same_gpu; } #endif @@ -63,8 +207,9 @@ namespace wl { } screencopy_request_e next_screencopy_request(bool pending, bool requested_cursor, bool cursor, bool event_driven, bool have_frame) { + using enum screencopy_request_e; if (pending) { - return requested_cursor == cursor ? screencopy_request_e::keep : screencopy_request_e::copy; + return requested_cursor == cursor ? keep : copy; } // The first frame is asked for outright, and so is the first one after @@ -72,7 +217,7 @@ namespace wl { // request for the next change would leave the stream without anything // to show, or with the cursor as it was, until something moved. const bool up_to_date = have_frame && requested_cursor == cursor; - return event_driven && up_to_date ? screencopy_request_e::copy_with_damage : screencopy_request_e::copy; + return event_driven && up_to_date ? copy_with_damage : copy; } bool should_wait_for_damage_request( @@ -120,6 +265,12 @@ namespace wl { } mem_type = hwdevice_type; +#ifdef SUNSHINE_BUILD_CUDA + // Cross-GPU NVENC cannot import tiled dmabufs, so resolve once here whether capture + // must allocate linear buffers instead. The render node cannot change mid-stream, and + // snapshot() runs per frame and must not redo device queries (or repeat warnings) per frame. + prefer_linear_capture = mem_type == platf::mem_type_e::cuda && !capture_node_is_cuda_device(); +#endif if (display.init()) { return -1; @@ -223,7 +374,7 @@ namespace wl { * @brief Populate a fallback image when real capture data is unavailable. * * @param img Image or frame object to read from or populate. - * @return Capture status reported to the streaming pipeline. + * @return Final capture status when callbacks stop capture or the display needs reinitialization. */ int dummy_img(platf::img_t *img) override { return 0; @@ -254,7 +405,7 @@ namespace wl { if (request != screencopy_request_e::keep) { dmabuf.with_damage = request == screencopy_request_e::copy_with_damage; requested_cursor = cursor; - dmabuf.listen(interface.screencopy_manager, interface.dmabuf_interface, &interface.supported_modifiers, output, cursor, encoder_modifiers); + dmabuf.listen(interface.screencopy_manager, interface.dmabuf_interface, &interface.supported_modifiers, output, cursor, encoder_modifiers, prefer_linear_capture); } do { auto remaining_time_ms = std::chrono::duration_cast(to - std::chrono::steady_clock::now()); @@ -319,6 +470,8 @@ namespace wl { * @return Capture status reported to the streaming pipeline. */ 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 { + using enum platf::capture_e; + auto next_frame = std::chrono::steady_clock::now(); sleep_overshoot_logger.reset(); @@ -329,14 +482,14 @@ namespace wl { 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: + case reinit: + case capture_error: + case interrupted: return status; - case platf::capture_e::timeout: - case platf::capture_e::ok: - if (!push_captured_image_cb(std::move(img_out), status == platf::capture_e::ok)) { - return platf::capture_e::ok; + case timeout: + case ok: + if (!push_captured_image_cb(std::move(img_out), status == ok)) { + return ok; } break; default: @@ -345,7 +498,7 @@ namespace wl { } } - return platf::capture_e::ok; + return ok; } /** @@ -360,6 +513,7 @@ namespace wl { virtual 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) = 0; platf::mem_type_e mem_type; ///< Mem type. + bool prefer_linear_capture {false}; ///< Linear copy buffers, resolved once in init() for cross-GPU CUDA encode. std::chrono::nanoseconds delay; ///< Delay before the timer task becomes eligible to run. std::chrono::steady_clock::time_point next_request {}; ///< Earliest time the next frame may be asked for when event-driven. diff --git a/src/platform/linux/x11grab.cpp b/src/platform/linux/x11grab.cpp index a14b3b66d09..a81b1b89d61 100644 --- a/src/platform/linux/x11grab.cpp +++ b/src/platform/linux/x11grab.cpp @@ -3,9 +3,17 @@ * @brief Definitions for x11 capture. */ // standard includes +#include +#include +#include #include #include #include +#include + +#ifdef SUNSHINE_TESTS + #include +#endif // plaform includes #include @@ -80,7 +88,7 @@ namespace platf { _FN(FreeCrtcInfo, void, (XRRCrtcInfo * crtcInfo)); static int init() { - static void *handle {nullptr}; + static dyn::library_t *handle {nullptr}; static bool funcs_loaded = false; if (funcs_loaded) { @@ -94,16 +102,15 @@ namespace platf { } } - 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)) { + if (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"}, + }; + dyn::load(handle, funcs)) { return -1; } @@ -117,7 +124,7 @@ namespace platf { _FN(GetCursorImage, XFixesCursorImage *, (Display * dpy)); static int init() { - static void *handle {nullptr}; + static dyn::library_t *handle {nullptr}; static bool funcs_loaded = false; if (funcs_loaded) { @@ -131,11 +138,10 @@ namespace platf { } } - std::vector> funcs { - {(dyn::apiproc *) &GetCursorImage, "XFixesGetCursorImage"}, - }; - - if (dyn::load(handle, funcs)) { + if (std::vector> funcs { + {(dyn::apiproc *) &GetCursorImage, "XFixesGetCursorImage"}, + }; + dyn::load(handle, funcs)) { return -1; } @@ -145,7 +151,7 @@ namespace platf { } // namespace fix static int init() { - static void *handle {nullptr}; + static dyn::library_t *handle {nullptr}; static bool funcs_loaded = false; if (funcs_loaded) { @@ -159,16 +165,15 @@ namespace platf { } } - 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)) { + if (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"}, + }; + dyn::load(handle, funcs)) { return -1; } @@ -201,7 +206,7 @@ namespace platf { * @return 0 when XCB shared-memory functions are loaded; nonzero otherwise. */ int init_shm() { - static void *handle {nullptr}; + static dyn::library_t *handle {nullptr}; static bool funcs_loaded = false; if (funcs_loaded) { @@ -215,14 +220,13 @@ namespace platf { } } - 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)) { + if (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"}, + }; + dyn::load(handle, funcs)) { return -1; } @@ -236,7 +240,7 @@ namespace platf { * @return 0 on success; nonzero or negative platform status on failure. */ int init() { - static void *handle {nullptr}; + static dyn::library_t *handle {nullptr}; static bool funcs_loaded = false; if (funcs_loaded) { @@ -250,17 +254,16 @@ namespace platf { } } - 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)) { + if (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"}, + }; + dyn::load(handle, funcs)) { return -1; } @@ -296,11 +299,11 @@ namespace platf { /** * @brief XImage pointer released with `XDestroyImage`. */ - using ximg_t = util::safe_ptr; + using ximg_t = util::safe_ptr; /** * @brief XFixes cursor image pointer released with `XFree`. */ - using xcursor_t = util::safe_ptr; + using xcursor_t = util::safe_ptr; /** * @brief XRandR CRTC info pointer released with `XRRFreeCrtcInfo`. @@ -325,11 +328,11 @@ namespace platf { } /** - * @brief Take ownership of a SysV shared-memory segment ID. + * @brief Explicitly take ownership of a SysV shared-memory segment ID. * * @param id SysV shared-memory segment ID. */ - shm_id_t(int id): + explicit shm_id_t(int id): id {id} { } @@ -354,20 +357,20 @@ namespace platf { }; /** - * @brief RAII wrapper that detaches mapped SysV shared memory. + * @brief RAII wrapper that detaches mapped SysV shared memory, with null representing an empty mapping. */ class shm_data_t { public: shm_data_t(): - data {(void *) -1} { + data {nullptr} { } /** - * @brief Take ownership of an attached shared-memory mapping. + * @brief Explicitly take ownership of an attached shared-memory mapping. * * @param data Pointer returned by shmat. */ - shm_data_t(void *data): + explicit shm_data_t(std::uint8_t *data): data {data} { } @@ -378,18 +381,54 @@ namespace platf { */ shm_data_t(shm_data_t &&other) noexcept: data(other.data) { - other.data = (void *) -1; + other.data = nullptr; } ~shm_data_t() { - if ((std::uintptr_t) data != -1) { + if (data) { shmdt(data); } } - void *data; ///< Address returned by shmat for the shared-memory segment. + std::uint8_t *data; ///< Attached shared-memory bytes, or null when this wrapper owns no attachment. }; +#ifdef SUNSHINE_TESTS + TEST(X11SharedMemoryMappingTest, EmptyAndMovedEmptyMappingsDoNotDetachMemory) { + errno = 0; + { + shm_data_t empty; + EXPECT_EQ(empty.data, nullptr); + shm_data_t moved {std::move(empty)}; + EXPECT_EQ(empty.data, nullptr); + EXPECT_EQ(moved.data, nullptr); + } + EXPECT_EQ(errno, 0); + } + + TEST(X11SharedMemoryMappingTest, MoveTransfersTheOnlyAttachment) { + const auto segment = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0600); + ASSERT_NE(segment, -1); + auto remove_segment = util::fail_guard([segment] { + shmctl(segment, IPC_RMID, nullptr); + }); + auto *mapping = shmat(segment, nullptr, 0); + ASSERT_NE(std::bit_cast(mapping), -1); + auto *bytes = static_cast(mapping); + { + shm_data_t owner {bytes}; + owner.data[0] = 42; + shm_data_t moved {std::move(owner)}; + EXPECT_EQ(owner.data, nullptr); + EXPECT_EQ(moved.data, bytes); + EXPECT_EQ(moved.data[0], 42); + } + errno = 0; + EXPECT_EQ(shmdt(mapping), -1); + EXPECT_EQ(errno, EINVAL); + } +#endif + /** * @brief X11 image wrapper used by the software capture path. */ @@ -438,7 +477,7 @@ namespace platf { 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 pixel_p = (int *) &pixel; auto colors_in = (uint8_t *) pixels_begin; @@ -462,9 +501,9 @@ namespace platf { struct x11_attr_t: public display_t { std::chrono::nanoseconds delay; ///< Delay before the timer task becomes eligible to run. - x11::xdisplay_t xdisplay; ///< X11 display connection used for capture. - Window xwindow; ///< Root window being captured. - XWindowAttributes xattr; ///< Cached X11 window attributes used to detect size changes. + x11::xdisplay_t xdisplay {x11::OpenDisplay(nullptr)}; ///< X11 display connection opened for capture. + Window xwindow {}; ///< Root window being captured. + XWindowAttributes xattr {}; ///< Cached X11 window attributes used to detect size changes. mem_type_e mem_type; ///< Mem type. @@ -472,17 +511,13 @@ namespace platf { * Last X (NOT the streamed monitor!) size. * This way we can trigger reinitialization if the dimensions changed while streaming */ - // int env_width, env_height; /** - * @brief Open the X11 display and initialize capture attributes. + * @brief Explicitly open the X11 display and initialize capture attributes. * * @param mem_type Requested memory path for the capture backend. */ - x11_attr_t(mem_type_e mem_type): - xdisplay {x11::OpenDisplay(nullptr)}, - xwindow {}, - xattr {}, + explicit x11_attr_t(mem_type_e mem_type): mem_type {mem_type} { x11::InitThreads(); } @@ -559,7 +594,17 @@ namespace platf { x11::GetWindowAttributes(xdisplay.get(), xwindow, &xattr); // Update xattr's } + /** + * @brief Capture X11 frames until capture stops or the display needs reinitialization. + * + * @param push_captured_image_cb Callback receiving a captured image or a capture timeout. + * @param pull_free_image_cb Callback providing an available image buffer. + * @param cursor Whether the cursor should be included in the captured image. + * @return Final capture status reported to the streaming pipeline. + */ 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 { + using enum platf::capture_e; + auto next_frame = std::chrono::steady_clock::now(); sleep_overshoot_logger.reset(); @@ -570,27 +615,27 @@ namespace platf { 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: + case reinit: + case capture_error: + case interrupted: return status; - case platf::capture_e::timeout: + case timeout: if (!push_captured_image_cb(std::move(img_out), false)) { - return platf::capture_e::ok; + return ok; } break; - case platf::capture_e::ok: + case ok: if (!push_captured_image_cb(std::move(img_out), true)) { - return platf::capture_e::ok; + return ok; } break; default: - BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; + BOOST_LOG(error) << "Unrecognized capture status ["sv << std::to_underlying(status) << ']'; return status; } } - return capture_e::ok; + return ok; } /** @@ -674,8 +719,8 @@ namespace platf { // TODO: stop cheating and give black image if (!img) { return -1; - }; - auto pull_dummy_img_callback = [&img](std::shared_ptr &img_out) -> bool { + } + auto pull_dummy_img_callback = [&img](std::shared_ptr &img_out) { img_out = img->shared_from_this(); return true; }; @@ -689,7 +734,7 @@ namespace platf { * @brief X11 shared-memory image dimensions and identifiers. */ struct shm_attr_t: public x11_attr_t { - x11::xdisplay_t shm_xdisplay; ///< X11 display held separately to prevent races with x11_attr_t::xdisplay. + x11::xdisplay_t shm_xdisplay {x11::OpenDisplay(nullptr)}; ///< X11 display held separately to prevent races with x11_attr_t::xdisplay. xcb_connect_t xcb; ///< XCB connection used by the shared-memory capture path. xcb_screen_t *display; ///< XCB screen containing the captured root window. std::uint32_t seg; ///< XCB shared-memory segment ID attached to the image. @@ -710,13 +755,12 @@ namespace platf { } /** - * @brief Open an X11 shared-memory capture backend. + * @brief Explicitly open an X11 shared-memory capture backend. * * @param mem_type Requested memory path for the capture backend. */ - shm_attr_t(mem_type_e mem_type): - x11_attr_t(mem_type), - shm_xdisplay {x11::OpenDisplay(nullptr)} { + explicit shm_attr_t(mem_type_e mem_type): + x11_attr_t(mem_type) { refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id; } @@ -724,7 +768,17 @@ namespace platf { while (!task_pool.cancel(refresh_task_id)); } + /** + * @brief Capture X11 shared-memory frames until capture stops or reinitialization is needed. + * + * @param push_captured_image_cb Callback receiving a captured image or a capture timeout. + * @param pull_free_image_cb Callback providing an available image buffer. + * @param cursor Whether the cursor should be included in the captured image. + * @return Final capture status reported to the streaming pipeline. + */ 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 { + using enum platf::capture_e; + auto next_frame = std::chrono::steady_clock::now(); sleep_overshoot_logger.reset(); @@ -735,27 +789,27 @@ namespace platf { 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: + case reinit: + case capture_error: + case interrupted: return status; - case platf::capture_e::timeout: + case timeout: if (!push_captured_image_cb(std::move(img_out), false)) { - return platf::capture_e::ok; + return ok; } break; - case platf::capture_e::ok: + case ok: if (!push_captured_image_cb(std::move(img_out), true)) { - return platf::capture_e::ok; + return ok; } break; default: - BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; + BOOST_LOG(error) << "Unrecognized capture status ["sv << std::to_underlying(status) << ']'; return status; } } - return capture_e::ok; + return ok; } /** @@ -786,7 +840,7 @@ namespace platf { return platf::capture_e::interrupted; } - std::copy_n((std::uint8_t *) data.data, frame_size(), img_out->data); + std::copy_n(data.data, frame_size(), img_out->data); img_out->frame_timestamp = frame_timestamp; if (cursor) { @@ -858,9 +912,10 @@ namespace platf { } xcb::shm_attach(xcb.get(), seg, shm_id.id, false); - data.data = shmat(shm_id.id, nullptr, 0); + data.data = static_cast(shmat(shm_id.id, nullptr, 0)); if ((uintptr_t) data.data == -1) { + data.data = nullptr; BOOST_LOG(error) << "shmat failed"sv; return -1; @@ -1009,7 +1064,7 @@ namespace platf { void cursor_t::capture(egl::cursor_t &img) { auto display = (xdisplay_t::pointer) ctx.get(); - xcursor_t xcursor = fix::GetCursorImage(display); + xcursor_t xcursor {fix::GetCursorImage(display)}; if (img.serial != xcursor->cursor_serial) { auto buf_size = xcursor->width * xcursor->height * sizeof(int); @@ -1041,7 +1096,7 @@ namespace platf { * @brief Open and initialize the display connection used for capture. */ xdisplay_t make_display() { - return OpenDisplay(nullptr); + return xdisplay_t {OpenDisplay(nullptr)}; } /** diff --git a/src/platform/linux/x11grab.h b/src/platform/linux/x11grab.h index fd53c0fc293..a48c28af4b7 100644 --- a/src/platform/linux/x11grab.h +++ b/src/platform/linux/x11grab.h @@ -36,11 +36,11 @@ namespace platf::x11 { /** * @brief XFixes cursor image pointer released with `XFree`. */ - using cursor_ctx_t = util::safe_ptr; + using cursor_ctx_t = util::safe_ptr; /** * @brief X11 display pointer released with `XCloseDisplay`. */ - using xdisplay_t = util::safe_ptr<_XDisplay, freeDisplay>; + using xdisplay_t = util::safe_ptr<_XDisplay, &freeDisplay>; /** * @brief X11 cursor image and positioning state used during capture. diff --git a/src/platform/macos/display.mm b/src/platform/macos/display.mm index 903a139fe0a..4aec4ebedd5 100644 --- a/src/platform/macos/display.mm +++ b/src/platform/macos/display.mm @@ -160,7 +160,7 @@ capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const } 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); + device->init(av_capture, pix_fmt, setResolution, setPixelFormat); return device; } else { @@ -225,8 +225,8 @@ int dummy_img(img_t *img) override { * @param width Frame or display width in pixels. * @param height Frame or display height in pixels. */ - static void setResolution(void *display, int width, int height) { - [static_cast(display) setFrameWidth:width frameHeight:height]; + static void setResolution(AVVideo *display, int width, int height) { + [display setFrameWidth:width frameHeight:height]; } /** @@ -235,8 +235,8 @@ static void setResolution(void *display, int width, int height) { * @param display Display object or identifier associated with the operation. * @param pixelFormat Pixel format. */ - static void setPixelFormat(void *display, OSType pixelFormat) { - static_cast(display).pixelFormat = pixelFormat; + static void setPixelFormat(AVVideo *display, OSType pixelFormat) { + display.pixelFormat = pixelFormat; } }; diff --git a/src/platform/macos/misc.mm b/src/platform/macos/misc.mm index 703ace5e284..9fde3784788 100644 --- a/src/platform/macos/misc.mm +++ b/src/platform/macos/misc.mm @@ -654,7 +654,7 @@ struct sockaddr_in6 to_sockaddr(boost::asio::ip::address_v6 address, uint16_t po return saddr_v6; } - bool send_batch(batched_send_info_t &send_info) { + bool send_batch(const batched_send_info_t &send_info) { // Fall back to unbatched send calls return false; } @@ -887,7 +887,12 @@ void sleep_for(const std::chrono::nanoseconds &duration) override { std::this_thread::sleep_for(duration); } - operator bool() override { + /** + * @brief Explicitly report that the thread-based timer is always available. + * + * @return True because sleeping needs no additional timer resource. + */ + explicit operator bool() override { return true; } }; diff --git a/src/platform/macos/nv12_zero_device.cpp b/src/platform/macos/nv12_zero_device.cpp index 97701987923..80b98e60a1b 100644 --- a/src/platform/macos/nv12_zero_device.cpp +++ b/src/platform/macos/nv12_zero_device.cpp @@ -33,7 +33,7 @@ namespace platf { CVPixelBufferRelease((CVPixelBufferRef) data); } - util::safe_ptr av_frame; ///< AV frame. + util::safe_ptr av_frame; ///< AV frame. int nv12_zero_device::convert(platf::img_t &img) { auto *av_img = (av_img_t *) &img; @@ -54,25 +54,23 @@ namespace platf { return 0; } - int nv12_zero_device::set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { - this->frame = frame; + int nv12_zero_device::set_frame(AVFrame *new_frame, AVBufferRef *hw_frames_ctx) { + this->frame = new_frame; - av_frame.reset(frame); + av_frame.reset(new_frame); - resolution_fn(this->display, frame->width, frame->height); + resolution_fn(this->display, new_frame->width, new_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); + int nv12_zero_device::init(AVVideo *capture_display, pix_fmt_e pix_fmt, resolution_fn_t set_resolution, const pixel_format_fn_t &pixel_format_fn) { + pixel_format_fn(capture_display, pix_fmt == pix_fmt_e::nv12 ? kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange : kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange); - this->display = display; - this->resolution_fn = std::move(resolution_fn); + this->display = capture_display; + this->resolution_fn = std::move(set_resolution); - // we never use this pointer, but its existence is checked/used - // by the platform independent code - data = this; + hardware = true; return 0; } diff --git a/src/platform/macos/nv12_zero_device.h b/src/platform/macos/nv12_zero_device.h index bd92b025bd0..693ab6f4f7d 100644 --- a/src/platform/macos/nv12_zero_device.h +++ b/src/platform/macos/nv12_zero_device.h @@ -9,6 +9,15 @@ struct AVFrame; +#ifdef __OBJC__ +@class AVVideo; +#else +/** + * @brief Opaque AVFoundation capture controller, defined in Objective-C. + */ +struct AVVideo; +#endif + namespace platf { /** * @brief Release an FFmpeg frame allocated by the capture or conversion backend. @@ -21,9 +30,7 @@ namespace platf { * @brief macOS zero-copy encode device that forwards NV12 frames from AVFoundation to FFmpeg. */ 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; + AVVideo *display = nullptr; ///< Borrowed AVFoundation controller, forward-declared to avoid FFmpeg name conflicts. public: // this function is used to set the resolution on an av_video object that we cannot @@ -31,23 +38,23 @@ namespace platf { /** * @brief Callback signature used to update the opaque AVFoundation display resolution. */ - using resolution_fn_t = std::function; + using resolution_fn_t = std::function; resolution_fn_t resolution_fn; ///< Callback stored for later AVFoundation resolution updates. /** * @brief Callback signature used to update the opaque AVFoundation pixel format. */ - using pixel_format_fn_t = std::function; + using pixel_format_fn_t = std::function; /** * @brief Initialize zero-copy NV12 encoding for an AVFoundation display. * - * @param display Display object or identifier associated with the operation. + * @param capture_display Display object or identifier associated with the operation. * @param pix_fmt Sunshine pixel format to convert or allocate for. - * @param resolution_fn Callback used to resize the AVFoundation capture output. + * @param set_resolution Callback used to resize the AVFoundation capture output. * @param pixel_format_fn Pixel format. * @return 0 on success; nonzero or negative platform status on failure. */ - int init(void *display, pix_fmt_e pix_fmt, resolution_fn_t resolution_fn, const pixel_format_fn_t &pixel_format_fn); + int init(AVVideo *capture_display, pix_fmt_e pix_fmt, resolution_fn_t set_resolution, const pixel_format_fn_t &pixel_format_fn); /** * @brief Forward a captured AVFoundation frame to FFmpeg without copying. @@ -59,14 +66,14 @@ namespace platf { /** * @brief Attach frame resources used by the next conversion or encode operation. * - * @param frame Video or graphics frame being processed. + * @param new_frame Video or graphics frame being processed. * @param hw_frames_ctx FFmpeg hardware frames context associated with the frame. * @return Status from updating frame. */ - int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override; + int set_frame(AVFrame *new_frame, AVBufferRef *hw_frames_ctx) override; private: - util::safe_ptr av_frame; + util::safe_ptr av_frame; }; } // namespace platf diff --git a/src/platform/macos/publish.cpp b/src/platform/macos/publish.cpp index edd8300539c..206ee28afb2 100644 --- a/src/platform/macos/publish.cpp +++ b/src/platform/macos/publish.cpp @@ -20,7 +20,7 @@ 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. + using pointer = DNSServiceRef; ///< DNS service handle released by the deleter. void operator()(pointer serviceRef) { DNSServiceRefDeallocate(serviceRef); @@ -33,14 +33,15 @@ namespace platf::publish { */ class deinit_t: public ::platf::deinit_t, std::unique_ptr { public: - /** @brief Construct deinit_t object. + /** + * @brief Explicitly 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): + explicit deinit_t(DNSServiceRef serviceRef): unique_ptr(serviceRef) { _thread = std::jthread {[serviceRef, &_stopRequested = std::as_const(_stopRequested)]() { platf::set_thread_name("publish::mdns"); @@ -81,7 +82,7 @@ namespace platf::publish { /** @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*/) { + void registrationCallback(DNSServiceRef /*serviceRef*/, DNSServiceFlags /*flags*/, DNSServiceErrorType errorCode, const char * /*name*/, const char * /*regtype*/, const char * /*domain*/, void * /*context*/) { // NOSONAR(cpp:S5008): DNSServiceRegister callbacks require a void pointer for user data. if (errorCode != kDNSServiceErr_NoError) { BOOST_LOG(error) << "Failed to register DNS service: Error "sv << errorCode; return; diff --git a/src/platform/virtualhid_input.cpp b/src/platform/virtualhid_input.cpp index 30ab2c4f970..7e255df5231 100644 --- a/src/platform/virtualhid_input.cpp +++ b/src/platform/virtualhid_input.cpp @@ -1125,8 +1125,13 @@ namespace platf { } // namespace + /** + * @brief Create the libvirtualhid backend with explicit input-handle ownership. + * + * @return Owning handle for the global virtual input backend. + */ input_t input() { - return {new input_raw_t {}}; + return input_t {new input_raw_t {}}; } std::unique_ptr allocate_client_input_context(input_t &input) { diff --git a/src/platform/windows/audio.cpp b/src/platform/windows/audio.cpp index e00033f3397..d12d015c0d3 100644 --- a/src/platform/windows/audio.cpp +++ b/src/platform/windows/audio.cpp @@ -120,7 +120,7 @@ namespace { break; } - static_assert((int) sample_format_e::_size == 5, "Unrecognized sample_format_e"); + static_assert(std::to_underlying(sample_format_e::_size) == 5, "Unrecognized sample_format_e"); waveformat.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; waveformat.Format.nChannels = channel_count; @@ -246,43 +246,43 @@ namespace platf::audio { /** * @brief COM device enumerator pointer for WASAPI endpoint discovery. */ - using device_enum_t = util::safe_ptr>; + using device_enum_t = util::safe_ptr>; /** * @brief COM pointer to a Windows audio endpoint device. */ - using device_t = util::safe_ptr>; + using device_t = util::safe_ptr>; /** * @brief COM pointer to a collection of Windows audio endpoint devices. */ - using collection_t = util::safe_ptr>; + using collection_t = util::safe_ptr>; /** * @brief COM pointer to the WASAPI audio client interface. */ - using audio_client_t = util::safe_ptr>; + using audio_client_t = util::safe_ptr>; /** * @brief COM pointer to the WASAPI capture client interface. */ - using audio_capture_t = util::safe_ptr>; + using audio_capture_t = util::safe_ptr>; /** * @brief CoTaskMem-allocated WAVEFORMATEX pointer. */ - using wave_format_t = util::safe_ptr>; + using wave_format_t = util::safe_ptr>; /** * @brief CoTaskMem-allocated wide string pointer. */ - using wstring_t = util::safe_ptr>; + using wstring_t = util::safe_ptr>; /** * @brief Windows HANDLE wrapper closed with `CloseHandle`. */ - using handle_t = util::safe_ptr_v2; + using handle_t = util::safe_ptr_v2; /** * @brief COM pointer to the Windows policy configuration interface. */ - using policy_t = util::safe_ptr>; + using policy_t = util::safe_ptr>; /** * @brief COM pointer to a Windows property store. */ - using prop_t = util::safe_ptr>; + using prop_t = util::safe_ptr>; /** * @brief Initializes COM for the current thread and uninitializes it on exit. @@ -441,8 +441,7 @@ namespace platf::audio { */ class audio_notification_t: public ::IMMNotificationClient { public: - audio_notification_t() { - } + audio_notification_t() = default; // IUnknown implementation (unused by IMMDeviceEnumerator) /** @@ -450,7 +449,7 @@ namespace platf::audio { * * @return Static reference count because the callback lifetime is externally owned. */ - ULONG STDMETHODCALLTYPE AddRef() { + ULONG STDMETHODCALLTYPE AddRef() override { return 1; } @@ -459,18 +458,18 @@ namespace platf::audio { * * @return Reference count or status returned after releasing the object. */ - ULONG STDMETHODCALLTYPE Release() { + ULONG STDMETHODCALLTYPE Release() override { return 1; } /** - * @brief Return the supported COM interface for the notification callback. + * @brief Implement IUnknown's interface query for the endpoint notification callback. * * @param riid COM interface identifier requested by QueryInterface. * @param ppvInterface Output pointer receiving the requested interface. * @return S_OK when the interface is supported; E_NOINTERFACE otherwise. */ - HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID **ppvInterface) { + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID **ppvInterface) override { // NOSONAR(cpp:S5008): IUnknown::QueryInterface requires a void double pointer in the COM ABI. if (IID_IUnknown == riid) { AddRef(); *ppvInterface = (IUnknown *) this; @@ -494,7 +493,7 @@ namespace platf::audio { * @param pwstrDeviceId Windows endpoint ID for the new default device. * @return S_OK after recording the render-device change notification. */ - HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) { + HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) override { if (flow == eRender) { default_render_device_changed_flag.store(true); } @@ -507,7 +506,7 @@ namespace platf::audio { * @param pwstrDeviceId Windows endpoint ID for the added device. * @return S_OK because Sunshine does not act on this notification. */ - HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) { + HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) override { return S_OK; } @@ -517,7 +516,7 @@ namespace platf::audio { * @param pwstrDeviceId Windows endpoint ID for the removed device. * @return S_OK because Sunshine does not act on this notification. */ - HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) { + HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) override { return S_OK; } @@ -531,7 +530,7 @@ namespace platf::audio { HRESULT STDMETHODCALLTYPE OnDeviceStateChanged( LPCWSTR pwstrDeviceId, DWORD dwNewState - ) { + ) override { return S_OK; } @@ -545,7 +544,7 @@ namespace platf::audio { HRESULT STDMETHODCALLTYPE OnPropertyValueChanged( LPCWSTR pwstrDeviceId, const PROPERTYKEY key - ) { + ) override { return S_OK; } @@ -751,12 +750,16 @@ namespace platf::audio { struct sample_aligned_t { std::uint32_t uninitialized; float *samples; - } sample_aligned; + }; + + sample_aligned_t sample_aligned; // number of samples / number of channels struct block_aligned_t { std::uint32_t audio_sample_size; - } block_aligned; + }; + + block_aligned_t block_aligned; // Check if the default audio device has changed if (endpt_notification.check_default_render_device_changed()) { @@ -780,7 +783,7 @@ namespace platf::audio { 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; + return capture_e::capture_error; } std::uint32_t packet_size {}; @@ -804,7 +807,7 @@ namespace platf::audio { return capture_e::reinit; default: BOOST_LOG(error) << "Couldn't capture audio [0x"sv << util::hex(status).to_string_view() << ']'; - return capture_e::error; + return capture_e::capture_error; } if (buffer_flags & AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY) { @@ -834,7 +837,7 @@ namespace platf::audio { } if (FAILED(status)) { - return capture_e::error; + return capture_e::capture_error; } return capture_e::ok; @@ -942,8 +945,7 @@ namespace platf::audio { // Encoding format: // [virtual-(format name)]device_id std::string current = sink; - auto prefix = "virtual-"sv; - if (current.find(prefix) == 0) { + if (auto prefix = "virtual-"sv; current.find(prefix) == 0) { current = current.substr(prefix.size(), current.size() - prefix.size()); for (const auto &format : formats) { @@ -1449,8 +1451,7 @@ namespace platf::audio { /** * @brief Destroy the Windows audio control. */ - ~audio_control_t() override { - } + ~audio_control_t() override = default; policy_t policy; ///< Windows policy configuration interface used to switch default audio devices. audio::device_enum_t device_enum; ///< Device enumerator used to query and watch audio endpoints. diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h index cfd588ded31..802042ea86c 100644 --- a/src/platform/windows/display.h +++ b/src/platform/windows/display.h @@ -42,149 +42,149 @@ namespace platf::dxgi { /** * @brief Owning COM pointer for the DXGI factory used to enumerate adapters. */ - using factory1_t = util::safe_ptr>; + using factory1_t = util::safe_ptr>; /** * @brief Owning COM pointer for a DXGI device interface. */ - using dxgi_t = util::safe_ptr>; + using dxgi_t = util::safe_ptr>; /** * @brief Owning COM pointer for a DXGI 1.1 device interface. */ - using dxgi1_t = util::safe_ptr>; + using dxgi1_t = util::safe_ptr>; /** * @brief Owning COM pointer for the D3D11 device used by capture and conversion. */ - using device_t = util::safe_ptr>; + using device_t = util::safe_ptr>; /** * @brief Owning COM pointer for the D3D11.1 device interface. */ - using device1_t = util::safe_ptr>; + using device1_t = util::safe_ptr>; /** * @brief Owning COM pointer for the immediate D3D11 device context. */ - using device_ctx_t = util::safe_ptr>; + using device_ctx_t = util::safe_ptr>; /** * @brief Owning COM pointer for a DXGI display adapter. */ - using adapter_t = util::safe_ptr>; + using adapter_t = util::safe_ptr>; /** * @brief Owning COM pointer for a DXGI output. */ - using output_t = util::safe_ptr>; + using output_t = util::safe_ptr>; /** * @brief Owning COM pointer for a DXGI output that supports duplication. */ - using output1_t = util::safe_ptr>; + using output1_t = util::safe_ptr>; /** * @brief Owning COM pointer for a DXGI output with HDR metadata support. */ - using output5_t = util::safe_ptr>; + using output5_t = util::safe_ptr>; /** * @brief Owning COM pointer for a DXGI output with modern display descriptors. */ - using output6_t = util::safe_ptr>; + using output6_t = util::safe_ptr>; /** * @brief Owning COM pointer for the DXGI desktop duplication session. */ - using dup_t = util::safe_ptr>; + using dup_t = util::safe_ptr>; /** * @brief Owning COM pointer for a D3D11 2D texture. */ - using texture2d_t = util::safe_ptr>; + using texture2d_t = util::safe_ptr>; /** * @brief Owning COM pointer for a D3D11 1D texture. */ - using texture1d_t = util::safe_ptr>; + using texture1d_t = util::safe_ptr>; /** * @brief Owning COM pointer for the DXGI resource view of a shared texture. */ - using resource_t = util::safe_ptr>; + using resource_t = util::safe_ptr>; /** * @brief Owning COM pointer for the DXGI 1.1 resource sharing interface. */ - using resource1_t = util::safe_ptr>; + using resource1_t = util::safe_ptr>; /** * @brief Owning COM pointer for the D3D11 multithread-protection interface. */ - using multithread_t = util::safe_ptr>; + using multithread_t = util::safe_ptr>; /** * @brief Owning COM pointer for a D3D11 vertex shader. */ - using vs_t = util::safe_ptr>; + using vs_t = util::safe_ptr>; /** * @brief Owning COM pointer for a D3D11 pixel shader. */ - using ps_t = util::safe_ptr>; + using ps_t = util::safe_ptr>; /** * @brief Owning COM pointer for a D3D11 blend state. */ - using blend_t = util::safe_ptr>; + using blend_t = util::safe_ptr>; /** * @brief Owning COM pointer for a D3D11 input layout. */ - using input_layout_t = util::safe_ptr>; + using input_layout_t = util::safe_ptr>; /** * @brief Owning COM pointer for a D3D11 render-target view. */ - using render_target_t = util::safe_ptr>; + using render_target_t = util::safe_ptr>; /** * @brief Owning COM pointer for a D3D11 shader-resource view. */ - using shader_res_t = util::safe_ptr>; + using shader_res_t = util::safe_ptr>; /** * @brief Owning COM pointer for a D3D11 buffer. */ - using buf_t = util::safe_ptr>; + using buf_t = util::safe_ptr>; /** * @brief D3D rasterizer state handle type. */ - using raster_state_t = util::safe_ptr>; + using raster_state_t = util::safe_ptr>; /** * @brief D3D sampler state handle type. */ - using sampler_state_t = util::safe_ptr>; + using sampler_state_t = util::safe_ptr>; /** * @brief Owning COM pointer for a compiled shader bytecode blob. */ - using blob_t = util::safe_ptr>; + using blob_t = util::safe_ptr>; /** * @brief D3D depth-stencil state handle type. */ - using depth_stencil_state_t = util::safe_ptr>; + using depth_stencil_state_t = util::safe_ptr>; /** * @brief Owning COM pointer for a D3D11 depth-stencil view. */ - using depth_stencil_view_t = util::safe_ptr>; + using depth_stencil_view_t = util::safe_ptr>; /** * @brief Owning COM pointer for a keyed mutex on a shared DXGI resource. */ - using keyed_mutex_t = util::safe_ptr>; + using keyed_mutex_t = util::safe_ptr>; namespace video { /** * @brief Owning COM pointer for the D3D11 video device interface. */ - using device_t = util::safe_ptr>; + using device_t = util::safe_ptr>; /** * @brief Owning COM pointer for the D3D11 video context interface. */ - using ctx_t = util::safe_ptr>; + using ctx_t = util::safe_ptr>; /** * @brief Owning COM pointer for a D3D11 video processor. */ - using processor_t = util::safe_ptr>; + using processor_t = util::safe_ptr>; /** * @brief Owning COM pointer for a video processor output view. */ - using processor_out_t = util::safe_ptr>; + using processor_out_t = util::safe_ptr>; /** * @brief Owning COM pointer for a video processor input view. */ - using processor_in_t = util::safe_ptr>; + using processor_in_t = util::safe_ptr>; /** * @brief Owning COM pointer for a video processor capability enumerator. */ - using processor_enum_t = util::safe_ptr>; + using processor_enum_t = util::safe_ptr>; } // namespace video /** @@ -229,40 +229,42 @@ namespace platf::dxgi { */ class gpu_cursor_t { public: - gpu_cursor_t(): - cursor_view {0, 0, 0, 0, 0.0f, 1.0f} {}; + /** + * @brief Initialize a cursor viewport with no area and the full depth range. + */ + gpu_cursor_t() = default; /** * @brief Update the cursor position and display geometry used for rendering. * - * @param topleft_x Cursor left edge in desktop coordinates. - * @param topleft_y Cursor top edge in desktop coordinates. - * @param display_width Captured display width in pixels. - * @param display_height Captured display height in pixels. - * @param display_rotation DXGI rotation applied to the captured display. - * @param visible Whether the cursor should be visible in the frame. - */ - 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; + * @param new_topleft_x Cursor left edge in desktop coordinates. + * @param new_topleft_y Cursor top edge in desktop coordinates. + * @param new_display_width Captured display width in pixels. + * @param new_display_height Captured display height in pixels. + * @param new_display_rotation DXGI rotation applied to the captured display. + * @param new_visible Whether the cursor should be visible in the frame. + */ + void set_pos(LONG new_topleft_x, LONG new_topleft_y, LONG new_display_width, LONG new_display_height, DXGI_MODE_ROTATION new_display_rotation, bool new_visible) { + this->topleft_x = new_topleft_x; + this->topleft_y = new_topleft_y; + this->display_width = new_display_width; + this->display_height = new_display_height; + this->display_rotation = new_display_rotation; + this->visible = new_visible; update_viewport(); } /** * @brief Replace the cursor texture and update its render viewport. * - * @param texture_width Cursor texture width in pixels. - * @param texture_height Cursor texture height in pixels. - * @param texture D3D11 cursor texture to render. + * @param new_texture_width Cursor texture width in pixels. + * @param new_texture_height Cursor texture height in pixels. + * @param new_texture D3D11 cursor texture to render. */ - 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; + void set_texture(LONG new_texture_width, LONG new_texture_height, texture2d_t &&new_texture) { + this->texture = std::move(new_texture); + this->texture_width = new_texture_width; + this->texture_height = new_texture_height; update_viewport(); } @@ -315,7 +317,7 @@ namespace platf::dxgi { shader_res_t input_res; ///< Input res. - D3D11_VIEWPORT cursor_view; ///< Cursor view. + D3D11_VIEWPORT cursor_view {0, 0, 0, 0, 0.0f, 1.0f}; ///< Cursor view with the full depth range. bool visible; ///< Whether the cursor is visible. }; @@ -359,32 +361,36 @@ namespace platf::dxgi { /** * @brief Enumerates supported d3 DKMT SCHEDULINGPRIORITYCLASS options. */ - typedef enum _D3DKMT_SCHEDULINGPRIORITYCLASS { + 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; ///< Alias for D3 DKMT SCHEDULINGPRIORITYCLASS. + }; + + using D3DKMT_SCHEDULINGPRIORITYCLASS = _D3DKMT_SCHEDULINGPRIORITYCLASS; ///< Process scheduling priority class passed to D3DKMT. /** * @brief Kernel-mode D3DKMT adapter handle. */ - typedef UINT D3DKMT_HANDLE; + using D3DKMT_HANDLE = UINT; /** * @brief Win32 D3DKMT adapter-open request structure. */ - typedef struct _D3DKMT_OPENADAPTERFROMLUID { + struct _D3DKMT_OPENADAPTERFROMLUID { LUID AdapterLuid; ///< Adapter luid. D3DKMT_HANDLE hAdapter; ///< D3DKMT adapter handle used for low-level display queries. - } D3DKMT_OPENADAPTERFROMLUID; ///< Alias for D3 DKMT OPENADAPTERFROMLUID. + }; + + using D3DKMT_OPENADAPTERFROMLUID = _D3DKMT_OPENADAPTERFROMLUID; ///< Adapter-open request passed to D3DKMT. /** * @brief Win32 D3DKMT WDDM 2.7 capability flags. */ - typedef struct _D3DKMT_WDDM_2_7_CAPS { + struct _D3DKMT_WDDM_2_7_CAPS { union { struct { @@ -397,57 +403,63 @@ namespace platf::dxgi { UINT Value; ///< Packed representation of the WDDM 2.7 capability flags. }; - } D3DKMT_WDDM_2_7_CAPS; ///< Alias for D3 DKMT WDDM 2 7 CAPS. + }; + + using D3DKMT_WDDM_2_7_CAPS = _D3DKMT_WDDM_2_7_CAPS; ///< WDDM 2.7 capability flags returned by D3DKMT. /** * @brief Win32 D3DKMT adapter information query. */ - typedef struct _D3DKMT_QUERYADAPTERINFO { + struct _D3DKMT_QUERYADAPTERINFO { D3DKMT_HANDLE hAdapter; ///< D3DKMT adapter handle used for low-level display queries. UINT Type; ///< Type. VOID *pPrivateDriverData; ///< P private driver data. UINT PrivateDriverDataSize; ///< Private driver data size. - } D3DKMT_QUERYADAPTERINFO; ///< Alias for D3 DKMT QUERYADAPTERINFO. + }; + + using D3DKMT_QUERYADAPTERINFO = _D3DKMT_QUERYADAPTERINFO; ///< Adapter-information query passed to D3DKMT. const UINT KMTQAITYPE_WDDM_2_7_CAPS = 70; ///< Protocol or platform constant for kmtqaitype wddm 2 7 caps. /** * @brief Win32 D3DKMT adapter-close request structure. */ - typedef struct _D3DKMT_CLOSEADAPTER { + struct _D3DKMT_CLOSEADAPTER { D3DKMT_HANDLE hAdapter; ///< D3DKMT adapter handle used for low-level display queries. - } D3DKMT_CLOSEADAPTER; ///< Alias for D3 DKMT CLOSEADAPTER. + }; + + using D3DKMT_CLOSEADAPTER = _D3DKMT_CLOSEADAPTER; ///< Adapter-close request passed to D3DKMT. /** * @brief Function pointer for setting D3DKMT process scheduling priority. */ - typedef NTSTATUS(WINAPI *PD3DKMTSetProcessSchedulingPriorityClass)(HANDLE, D3DKMT_SCHEDULINGPRIORITYCLASS); + using PD3DKMTSetProcessSchedulingPriorityClass = NTSTATUS(WINAPI *)(HANDLE, D3DKMT_SCHEDULINGPRIORITYCLASS); /** * @brief Function pointer for opening a D3DKMT adapter by LUID. */ - typedef NTSTATUS(WINAPI *PD3DKMTOpenAdapterFromLuid)(D3DKMT_OPENADAPTERFROMLUID *); + using PD3DKMTOpenAdapterFromLuid = NTSTATUS(WINAPI *)(D3DKMT_OPENADAPTERFROMLUID *); /** * @brief Function pointer for querying D3DKMT adapter information. */ - typedef NTSTATUS(WINAPI *PD3DKMTQueryAdapterInfo)(D3DKMT_QUERYADAPTERINFO *); + using PD3DKMTQueryAdapterInfo = NTSTATUS(WINAPI *)(D3DKMT_QUERYADAPTERINFO *); /** * @brief Function pointer for closing a D3DKMT adapter handle. */ - typedef NTSTATUS(WINAPI *PD3DKMTCloseAdapter)(D3DKMT_CLOSEADAPTER *); + using PD3DKMTCloseAdapter = NTSTATUS(WINAPI *)(D3DKMT_CLOSEADAPTER *); /** * @brief Report whether the active display mode is HDR. * * @return True when the active display mode is HDR. */ - virtual bool is_hdr() override; + bool is_hdr() override; /** * @brief Read HDR metadata for the active display mode. * * @param metadata Output structure populated with HDR metadata. * @return True when HDR metadata was written to `metadata`. */ - virtual bool get_hdr_metadata(SS_HDR_METADATA &metadata) override; + bool get_hdr_metadata(SS_HDR_METADATA &metadata) override; /** * @brief Convert a DXGI format enum to a diagnostic string. diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 8005949cfe1..2a013367107 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -6,6 +6,7 @@ #include #include #include +#include // platform includes #include @@ -25,20 +26,22 @@ /** * @brief Windows NT status code returned by native APIs. */ -typedef long NTSTATUS; +using NTSTATUS = long; // Definition from the WDK's d3dkmthk.h /** * @brief Enumerates supported d3 DKMT GPU PREFERENCE QUERY STATE options. */ -typedef enum _D3DKMT_GPU_PREFERENCE_QUERY_STATE : DWORD { +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; ///< Alias for D3 DKMT GPU PREFERENCE QUERY STATE. +}; + +using D3DKMT_GPU_PREFERENCE_QUERY_STATE = _D3DKMT_GPU_PREFERENCE_QUERY_STATE; ///< GPU preference query state reported by D3DKMT. #include "display.h" #include "misc.h" @@ -172,7 +175,7 @@ namespace platf::dxgi { 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; + return capture_e::capture_error; } } @@ -184,27 +187,34 @@ namespace platf::dxgi { return capture_status; } + /** + * @brief Release the acquired desktop duplication frame and classify the DXGI result. + * + * @return Success, a required display reinitialization, or a capture error. + */ capture_e duplication_t::release_frame() { + using enum platf::capture_e; + if (!has_frame) { - return capture_e::ok; + return ok; } auto status = dup->ReleaseFrame(); has_frame = false; switch (status) { case S_OK: - return capture_e::ok; + return ok; case DXGI_ERROR_INVALID_CALL: BOOST_LOG(warning) << "Duplication frame already released"; - return capture_e::ok; + return ok; case DXGI_ERROR_ACCESS_LOST: - return capture_e::reinit; + return reinit; default: BOOST_LOG(error) << "Error while releasing duplication frame [0x"sv << util::hex(status).to_string_view(); - return capture_e::error; + return capture_error; } } @@ -330,7 +340,7 @@ namespace platf::dxgi { switch (status) { case platf::capture_e::reinit: - case platf::capture_e::error: + case platf::capture_e::capture_error: case platf::capture_e::interrupted: return status; case platf::capture_e::timeout: @@ -344,7 +354,7 @@ namespace platf::dxgi { } break; default: - BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; + BOOST_LOG(error) << "Unrecognized capture status ["sv << std::to_underlying(status) << ']'; return status; } @@ -456,7 +466,7 @@ namespace platf::dxgi { std::call_once(windows_cpp_once_flag, []() { DECLARE_HANDLE(DPI_AWARENESS_CONTEXT); - typedef BOOL (*User32_SetProcessDpiAwarenessContext)(DPI_AWARENESS_CONTEXT value); + using User32_SetProcessDpiAwarenessContext = BOOL (*)(DPI_AWARENESS_CONTEXT value); { auto user32 = LoadLibraryA("user32.dll"); @@ -637,7 +647,7 @@ namespace platf::dxgi { HMODULE gdi32 = GetModuleHandleA("GDI32"); if (gdi32) { - auto check_hags = [&](const LUID &adapter) -> bool { + auto check_hags = [&](const LUID &adapter_luid) { 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"); @@ -646,7 +656,7 @@ namespace platf::dxgi { return false; } - D3DKMT_OPENADAPTERFROMLUID d3dkmt_adapter = {.AdapterLuid = adapter}; + D3DKMT_OPENADAPTERFROMLUID d3dkmt_adapter = {.AdapterLuid = adapter_luid}; if (FAILED(d3dkmt_open_adapter(&d3dkmt_adapter))) { BOOST_LOG(error) << "D3DKMTOpenAdapterFromLuid() failed while trying to determine GPU HAGS status"; return false; @@ -1128,7 +1138,7 @@ namespace platf { 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); + auto lg = std::scoped_lock(reenumeration_state_lock); // Keep a reference to the DXGI factory, which will keep track of changes internally. static dxgi::factory1_t factory; diff --git a/src/platform/windows/display_ram.cpp b/src/platform/windows/display_ram.cpp index 9a4553bff93..947346806ba 100644 --- a/src/platform/windows/display_ram.cpp +++ b/src/platform/windows/display_ram.cpp @@ -241,7 +241,7 @@ namespace platf::dxgi { if (FAILED(status)) { BOOST_LOG(error) << "Failed to get new pointer shape [0x"sv << util::hex(status).to_string_view() << ']'; - return capture_e::error; + return capture_e::capture_error; } } @@ -258,7 +258,7 @@ namespace platf::dxgi { if (FAILED(status)) { BOOST_LOG(error) << "Couldn't query interface [0x"sv << util::hex(status).to_string_view() << ']'; - return capture_e::error; + return capture_e::capture_error; } D3D11_TEXTURE2D_DESC desc; @@ -279,11 +279,11 @@ namespace platf::dxgi { t.Format = capture_format; t.CPUAccessFlags = D3D11_CPU_ACCESS_READ; - auto status = device->CreateTexture2D(&t, nullptr, &texture); + auto texture_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; + if (FAILED(texture_status)) { + BOOST_LOG(error) << "Failed to create staging texture [0x"sv << util::hex(texture_status).to_string_view() << ']'; + return capture_e::capture_error; } } @@ -316,7 +316,7 @@ namespace platf::dxgi { BOOST_LOG(debug) << "Capture format is still unknown. Encoding a blank image"sv; if (dummy_img(img)) { - return capture_e::error; + return capture_e::capture_error; } } else { // Map the staging texture for CPU access (making it inaccessible for the GPU) @@ -324,14 +324,14 @@ namespace platf::dxgi { if (FAILED(status)) { BOOST_LOG(error) << "Failed to map texture [0x"sv << util::hex(status).to_string_view() << ']'; - return capture_e::error; + return capture_e::capture_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; + return capture_e::capture_error; } std::copy_n((std::uint8_t *) img_info.pData, height * img_info.RowPitch, (std::uint8_t *) img->data); diff --git a/src/platform/windows/display_vram.cpp b/src/platform/windows/display_vram.cpp index b7b3c743ab7..917c5c390ad 100644 --- a/src/platform/windows/display_vram.cpp +++ b/src/platform/windows/display_vram.cpp @@ -3,8 +3,10 @@ * @brief Definitions for handling video ram. */ // standard includes +#include #include #include +#include // platform includes #include @@ -47,7 +49,7 @@ static void free_frame(AVFrame *frame) { /** * @brief FFmpeg hardware frame pointer released with `av_frame_free`. */ -using frame_t = util::safe_ptr; +using frame_t = util::safe_ptr; namespace platf::dxgi { @@ -209,9 +211,9 @@ namespace platf::dxgi { * * @param other Helper whose acquired keyed mutex is transferred. */ - texture_lock_helper(texture_lock_helper &&other) { + texture_lock_helper(texture_lock_helper &&other): + _locked {other._locked} { _mutex.reset(other._mutex.release()); - _locked = other._locked; other._locked = false; } @@ -232,11 +234,11 @@ namespace platf::dxgi { } /** - * @brief Acquire a D3D keyed mutex for scoped texture access. + * @brief Explicitly acquire a D3D keyed mutex for scoped texture access. * * @param mutex Keyed mutex to acquire for texture access. */ - texture_lock_helper(IDXGIKeyedMutex *mutex): + explicit texture_lock_helper(IDXGIKeyedMutex *mutex): _mutex(mutex) { if (_mutex) { _mutex->AddRef(); @@ -269,7 +271,7 @@ namespace platf::dxgi { }; /** - * @brief Create cursor xor image. + * @brief Convert DXGI cursor mask bytes into pixels for XOR blending. * * @param img_data Raw pointer-shape bytes returned by DXGI. * @param shape_info DXGI metadata describing the pointer shape. @@ -286,7 +288,7 @@ namespace platf::dxgi { 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) { + std::ranges::for_each(std::span {(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. @@ -316,13 +318,13 @@ namespace platf::dxgi { 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; + const auto *and_mask = reinterpret_cast(std::begin(img_data)); + const auto *xor_mask = and_mask + 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); + const auto bit = std::byte {1} << c; + auto color_type = ((*and_mask & bit) != std::byte {0} ? 1 : 0) + ((*xor_mask & bit) != std::byte {0} ? 2 : 0); switch (color_type) { case 0: // Opaque black (handled by alpha-blending) @@ -345,7 +347,7 @@ namespace platf::dxgi { } /** - * @brief Create cursor alpha image. + * @brief Convert DXGI cursor mask bytes into pixels for alpha blending. * * @param img_data Raw pointer-shape bytes returned by DXGI. * @param shape_info DXGI metadata describing the pointer shape. @@ -360,7 +362,7 @@ namespace platf::dxgi { 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) { + std::ranges::for_each(std::span {(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(). @@ -394,13 +396,13 @@ namespace platf::dxgi { 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; + const auto *and_mask = reinterpret_cast(std::begin(img_data)); + const auto *xor_mask = and_mask + 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); + const auto bit = std::byte {1} << c; + auto color_type = ((*and_mask & bit) != std::byte {0} ? 1 : 0) + ((*xor_mask & bit) != std::byte {0} ? 2 : 0); switch (color_type) { case 0: // Opaque black @@ -601,7 +603,7 @@ namespace platf::dxgi { frame_texture->AddRef(); output_texture.reset(frame_texture); - HRESULT status = S_OK; + auto status = S_OK; #ifndef DOXYGEN #define create_vertex_shader_helper(x, y) \ @@ -783,14 +785,14 @@ namespace platf::dxgi { return -1; } - auto create_rtv = [&](auto &rt, DXGI_FORMAT rt_format) -> bool { + auto create_rtv = [&](auto &rt, DXGI_FORMAT rt_format) { 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); + auto view_status = device->CreateRenderTargetView(output_texture.get(), &rtv_desc, &rt); + if (FAILED(view_status)) { + BOOST_LOG(error) << "Failed to create render target view: " << util::log_hex(view_status); return false; } @@ -827,12 +829,12 @@ namespace platf::dxgi { /** * @brief Initialize shared D3D conversion resources for the encoder. * - * @param display Display object or identifier associated with the operation. + * @param capture_display Display object or identifier associated with the operation. * @param adapter_p Adapter p. * @param pix_fmt Sunshine pixel format to convert or allocate for. * @return 0 on success; nonzero or negative platform status on failure. */ - int init(std::shared_ptr display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) { + int init(std::shared_ptr capture_display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) { switch (pix_fmt) { using enum pix_fmt_e; case nv12: @@ -914,11 +916,11 @@ namespace platf::dxgi { device_ctx->VSSetConstantBuffers(3, 1, &color_matrix); device_ctx->PSSetConstantBuffers(0, 1, &color_matrix); - this->display = std::dynamic_pointer_cast(display); + this->display = std::dynamic_pointer_cast(capture_display); if (!this->display) { return -1; } - display = nullptr; + capture_display = nullptr; blend_disable = make_blend(device.get(), false, false); if (!blend_disable) { @@ -1120,7 +1122,8 @@ namespace platf::dxgi { */ 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(); + d3d11_device = base.device.get(); + hardware = d3d11_device != nullptr; return result; } @@ -1186,17 +1189,17 @@ namespace platf::dxgi { /** * @brief Attach frame resources used by the next conversion or encode operation. * - * @param frame Video or graphics frame being processed. + * @param new_frame Video or graphics frame being processed. * @param hw_frames_ctx FFmpeg hardware frames context associated with the frame. * @return Status from updating frame. */ - int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { - this->hwframe.reset(frame); - this->frame = frame; + int set_frame(AVFrame *new_frame, AVBufferRef *hw_frames_ctx) override { + this->hwframe.reset(new_frame); + this->frame = new_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 (!new_frame->buf[0]) { + auto err = av_hwframe_get_buffer(hw_frames_ctx, new_frame, 0); if (err) { std::array err_str {0}; BOOST_LOG(error) << "Failed to get hwframe buffer: "sv << av_make_error_string(err_str.data(), AV_ERROR_MAX_STRING_SIZE, err); @@ -1206,12 +1209,12 @@ namespace platf::dxgi { // 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) { + if (new_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); + auto err = av_hwframe_map(d3d11_frame.get(), new_frame, AV_HWFRAME_MAP_WRITE | AV_HWFRAME_MAP_OVERWRITE); if (err) { std::array err_str {0}; BOOST_LOG(error) << "Failed to map D3D11 frame: "sv << av_make_error_string(err_str.data(), AV_ERROR_MAX_STRING_SIZE, err); @@ -1222,10 +1225,10 @@ namespace platf::dxgi { 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]; + frame_texture = (ID3D11Texture2D *) new_frame->data[0]; } - return base.init_output(frame_texture, frame->width, frame->height); + return base.init_output(frame_texture, new_frame->width, new_frame->height); } private: @@ -1275,19 +1278,19 @@ namespace platf::dxgi { * @brief Initialize the platform encoder for the client stream configuration. * * @param client_config Client stream configuration negotiated for this session. - * @param colorspace Colorimetry information used for conversion or encoding. + * @param encoder_colorspace Colorimetry information used for conversion or encoding. * @return True when the NVENC encoder initializes for the client configuration. */ - bool init_encoder(const ::video::config_t &client_config, const ::video::sunshine_colorspace_t &colorspace) override { + bool init_encoder(const ::video::config_t &client_config, const ::video::sunshine_colorspace_t &encoder_colorspace) override { if (!nvenc_d3d) { return false; } - if (!nvenc_d3d->create_encoder(config::video.nv, client_config, colorspace, buffer_format)) { + if (!nvenc_d3d->create_encoder(config::video.nv, client_config, encoder_colorspace, buffer_format)) { return false; } - base.apply_colorspace(colorspace); + base.apply_colorspace(encoder_colorspace); return base.init_output(nvenc_d3d->get_input_texture(), client_config.width, client_config.height) == 0; } @@ -1396,14 +1399,14 @@ namespace platf::dxgi { if (FAILED(status)) { BOOST_LOG(error) << "Failed to get new pointer shape [0x"sv << util::hex(status).to_string_view() << ']'; - return capture_e::error; + return capture_e::capture_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; + return capture_e::capture_error; } } @@ -1421,7 +1424,7 @@ namespace platf::dxgi { 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; + return capture_e::capture_error; } D3D11_TEXTURE2D_DESC desc; @@ -1517,7 +1520,7 @@ namespace platf::dxgi { } } - auto create_surface = [&](texture2d_t &surface) -> bool { + auto create_surface = [&](texture2d_t &surface) { // 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()); @@ -1549,7 +1552,7 @@ namespace platf::dxgi { // 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}; + return {nullptr, texture_lock_helper {nullptr}}; } // This image is shared between capture direct3d device and encoders direct3d devices, @@ -1557,7 +1560,7 @@ namespace platf::dxgi { 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}; + return {nullptr, texture_lock_helper {nullptr}}; } // Clear the blank flag now that we're ready to capture into the image @@ -1577,7 +1580,7 @@ namespace platf::dxgi { auto p_surface = std::get_if(&last_frame_variant); if (!p_surface) { BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; - return capture_e::error; + return capture_e::capture_error; } std::shared_ptr img; @@ -1587,7 +1590,7 @@ namespace platf::dxgi { auto [d3d_img, lock] = get_locked_d3d_img(img); if (!d3d_img) { - return capture_e::error; + return capture_e::capture_error; } device_ctx->CopyResource(d3d_img->capture_texture.get(), p_surface->get()); @@ -1605,18 +1608,18 @@ namespace platf::dxgi { auto p_img = std::get_if>(&last_frame_variant); if (!p_img) { BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; - return capture_e::error; + return capture_e::capture_error; } auto [d3d_img, lock] = get_locked_d3d_img(*p_img); if (!d3d_img) { - return capture_e::error; + return capture_e::capture_error; } p_img = nullptr; last_frame_variant = texture2d_t {}; auto &surface = std::get(last_frame_variant); if (!create_surface(surface)) { - return capture_e::error; + return capture_e::capture_error; } device_ctx->CopyResource(surface.get(), d3d_img->capture_texture.get()); @@ -1634,7 +1637,7 @@ namespace platf::dxgi { auto [d3d_img, lock] = get_locked_d3d_img(img); if (!d3d_img) { - return capture_e::error; + return capture_e::capture_error; } device_ctx->CopyResource(d3d_img->capture_texture.get(), src.get()); @@ -1649,7 +1652,7 @@ namespace platf::dxgi { last_frame_variant = texture2d_t {}; p_surface = std::get_if(&last_frame_variant); if (!create_surface(*p_surface)) { - return capture_e::error; + return capture_e::capture_error; } } device_ctx->CopyResource(p_surface->get(), src.get()); @@ -1695,7 +1698,7 @@ namespace platf::dxgi { auto p_img = std::get_if>(&last_frame_variant); if (!p_img) { BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; - return capture_e::error; + return capture_e::capture_error; } img_out = *p_img; break; @@ -1706,11 +1709,11 @@ namespace platf::dxgi { auto p_surface = std::get_if(&last_frame_variant); if (!p_surface) { BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; - return capture_e::error; + return capture_e::capture_error; } if (!blend_mouse_cursor_flag) { BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; - return capture_e::error; + return capture_e::capture_error; } if (!pull_free_image_cb(img_out)) { @@ -1719,7 +1722,7 @@ namespace platf::dxgi { auto [d3d_img, lock] = get_locked_d3d_img(img_out); if (!d3d_img) { - return capture_e::error; + return capture_e::capture_error; } device_ctx->CopyResource(d3d_img->capture_texture.get(), p_surface->get()); @@ -1740,7 +1743,7 @@ namespace platf::dxgi { auto [d3d_img, lock] = get_locked_d3d_img(img_out, true); if (!d3d_img) { - return capture_e::error; + return capture_e::capture_error; } if (reclear_dummy) { @@ -1894,10 +1897,10 @@ namespace platf::dxgi { device_ctx->CopyResource(d3d_img->capture_texture.get(), src.get()); } else { BOOST_LOG(error) << "Failed to lock capture texture"; - return capture_e::error; + return capture_e::capture_error; } } else { - return capture_e::error; + return capture_e::capture_error; } img_out = img; if (img_out) { @@ -2134,19 +2137,19 @@ namespace platf::dxgi { } 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) { + auto encode_device = std::make_unique(); + if (encode_device->init(shared_from_this(), adapter.get(), pix_fmt) != 0) { return nullptr; } - return device; + return encode_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)) { + auto encode_device = std::make_unique(); + if (!encode_device->init_device(shared_from_this(), adapter.get(), pix_fmt)) { return nullptr; } - return device; + return encode_device; } /** diff --git a/src/platform/windows/display_wgc.cpp b/src/platform/windows/display_wgc.cpp index b85dbd232d6..26fcb3b5487 100644 --- a/src/platform/windows/display_wgc.cpp +++ b/src/platform/windows/display_wgc.cpp @@ -56,7 +56,7 @@ namespace winrt { * @param object Output pointer that receives the requested COM interface. * @return HRESULT from the WinRT object's interface query. */ - virtual HRESULT __stdcall GetInterface(REFIID id, void **object) = 0; + virtual HRESULT __stdcall GetInterface(REFIID id, void **object) = 0; // NOSONAR(cpp:S5008): IDirect3DDxgiInterfaceAccess::GetInterface requires a void double pointer in the WinRT ABI. }; } // namespace winrt #if !WINRT_IMPL_HAS_DECLSPEC_UUID @@ -212,7 +212,7 @@ namespace platf::dxgi { if (GetLastError() == ERROR_TIMEOUT) { return capture_e::timeout; } else { - return capture_e::error; + return capture_e::capture_error; } } if (produced_frame) { @@ -226,7 +226,7 @@ namespace platf::dxgi { auto capture_access = consumed_frame.Surface().as(); if (capture_access == nullptr) { - return capture_e::error; + return capture_e::capture_error; } capture_access->GetInterface(IID_ID3D11Texture2D, (void **) out); out_time = consumed_frame.SystemRelativeTime().count(); // raw ticks from query performance counter @@ -293,11 +293,11 @@ namespace platf::dxgi { t.Format = capture_format; t.CPUAccessFlags = D3D11_CPU_ACCESS_READ; - auto status = device->CreateTexture2D(&t, nullptr, &texture); + auto texture_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; + if (FAILED(texture_status)) { + BOOST_LOG(error) << "Failed to create staging texture [0x"sv << util::hex(texture_status).to_string_view() << ']'; + return capture_e::capture_error; } } @@ -326,14 +326,14 @@ namespace platf::dxgi { 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; + return capture_e::capture_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; + return capture_e::capture_error; } std::copy_n((std::uint8_t *) img_info.pData, height * img_info.RowPitch, (std::uint8_t *) img->data); diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp index 2a2176d5ef1..73f2acf0383 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -27,6 +27,7 @@ #include // local includes +#include "input.h" #include "misc.h" #include "src/config.h" #include "src/globals.h" @@ -40,11 +41,11 @@ namespace platf { /** * @brief ViGEm client pointer released with `vigem_free`. */ - using client_t = util::safe_ptr<_VIGEM_CLIENT_T, vigem_free>; + using client_t = util::safe_ptr<_VIGEM_CLIENT_T, &vigem_free>; /** * @brief ViGEm target pointer released with `vigem_target_free`. */ - using target_t = util::safe_ptr<_VIGEM_TARGET_T, vigem_target_free>; + using target_t = util::safe_ptr<_VIGEM_TARGET_T, &vigem_target_free>; /** * @brief Handle Xbox 360 virtual gamepad notification events. @@ -106,7 +107,7 @@ namespace platf { // Map from pointer ID to pointer index std::map pointer_id_map; ///< Pointer ID map. - uint8_t available_pointers; ///< Available pointers. + std::byte available_pointers; ///< Byte mask of available DualShock 4 touch slots. uint8_t client_relative_index; ///< Client relative index. @@ -255,13 +256,13 @@ namespace platf { 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()); + client_t probe_client {vigem_alloc()}; + VIGEM_ERROR status = vigem_connect(probe_client.get()); if (!VIGEM_SUCCESS(status)) { BOOST_LOG(warning) << "ViGEmBus is not installed or running, so its limited Xbox 360 and DualShock 4 gamepad support is unavailable"sv; return -1; } else { - vigem_disconnect(client.get()); + vigem_disconnect(probe_client.get()); } gamepads.resize(MAX_GAMEPADS); @@ -314,7 +315,7 @@ namespace platf { 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; + gamepad.available_pointers = std::byte {0x3}; } auto status = vigem_target_add(client.get(), gamepad.gp.get()); @@ -392,8 +393,8 @@ namespace platf { // 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())) { + for (auto &attached_gamepad : gamepads) { + if (attached_gamepad.gp && vigem_target_is_attached(attached_gamepad.gp.get())) { disconnect = false; break; } @@ -417,11 +418,9 @@ namespace platf { if (gamepad.gp.get() == target) { // Convert from 8-bit to 16-bit values - uint16_t normalizedLargeMotor = largeMotor << 8; - uint16_t normalizedSmallMotor = smallMotor << 8; - + const auto normalizedLargeMotor = vigem::scale_rumble(largeMotor); // Don't resend duplicate rumble data - if (normalizedSmallMotor != gamepad.last_rumble.data.rumble.highfreq || normalizedLargeMotor != gamepad.last_rumble.data.rumble.lowfreq) { + if (const auto normalizedSmallMotor = vigem::scale_rumble(smallMotor); 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, @@ -489,7 +488,7 @@ namespace platf { std::uint8_t largeMotor, std::uint8_t smallMotor, std::uint8_t /* led_number */, - void *userdata + void *userdata // NOSONAR(cpp:S5008): ViGEm notification callbacks require a void pointer for user data. ) { BOOST_LOG(debug) << "largeMotor: "sv << (int) largeMotor << std::endl @@ -504,7 +503,7 @@ namespace platf { std::uint8_t largeMotor, std::uint8_t smallMotor, DS4_LIGHTBAR_COLOR led_color, - void *userdata + void *userdata // NOSONAR(cpp:S5008): ViGEm notification callbacks require a void pointer for user data. ) { BOOST_LOG(debug) << "largeMotor: "sv << (int) largeMotor << std::endl @@ -832,8 +831,7 @@ namespace platf { } static DS4_DPAD_DIRECTIONS ds4_dpad(const gamepad_state_t &gamepad_state) { - auto flags = gamepad_state.buttonFlags; - if (flags & DPAD_UP) { + if (auto flags = gamepad_state.buttonFlags; flags & DPAD_UP) { if (flags & DPAD_RIGHT) { return DS4_BUTTON_DPAD_NORTHEAST; } else if (flags & DPAD_LEFT) { @@ -1039,104 +1037,80 @@ namespace platf { } } - /** - * @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 raw = (input_raw_t *) input.get(); - if (virtualhid::has_gamepad(raw->virtualhid, touch.id.globalIndex)) { - virtualhid::gamepad_touch(raw->virtualhid, touch); - return; - } - - auto *vigem = raw->vigem.get(); - - // 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; - } + std::uint16_t vigem::scale_rumble(std::uint8_t strength) { + return strength * 256; + } - auto &report = gamepad.report.ds4.Report; + bool vigem::update_touch(DS4_REPORT_EX &ds4_report, std::map &pointer_id_map, std::byte &available_pointers, const gamepad_touch_t &touch) { + auto &report = ds4_report.Report; - uint8_t pointerIndex; + uint8_t pointerIndex {}; if (touch.eventType == LI_TOUCH_EVENT_DOWN) { - if (gamepad.available_pointers & 0x1) { + if ((available_pointers & std::byte {0x1}) != std::byte {0}) { // Reserve pointer index 0 for this touch - gamepad.pointer_id_map[touch.pointerId] = pointerIndex = 0; - gamepad.available_pointers &= ~(1 << pointerIndex); + pointer_id_map[touch.pointerId] = pointerIndex = 0; + available_pointers &= ~(std::byte {1} << pointerIndex); // Set pointer 0 down - report.sCurrentTouch.bIsUpTrackingNum1 &= ~0x80; + report.sCurrentTouch.bIsUpTrackingNum1 = std::to_integer(std::byte {report.sCurrentTouch.bIsUpTrackingNum1} & ~std::byte {0x80}); report.sCurrentTouch.bIsUpTrackingNum1++; - } else if (gamepad.available_pointers & 0x2) { + } else if ((available_pointers & std::byte {0x2}) != std::byte {0}) { // Reserve pointer index 1 for this touch - gamepad.pointer_id_map[touch.pointerId] = pointerIndex = 1; - gamepad.available_pointers &= ~(1 << pointerIndex); + pointer_id_map[touch.pointerId] = pointerIndex = 1; + available_pointers &= ~(std::byte {1} << pointerIndex); // Set pointer 1 down - report.sCurrentTouch.bIsUpTrackingNum2 &= ~0x80; + report.sCurrentTouch.bIsUpTrackingNum2 = std::to_integer(std::byte {report.sCurrentTouch.bIsUpTrackingNum2} & ~std::byte {0x80}); report.sCurrentTouch.bIsUpTrackingNum2++; } else { BOOST_LOG(warning) << "No more free pointer indices! Did the client miss an touch up event?"sv; - return; + return false; } } else if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) { // Raise both pointers - report.sCurrentTouch.bIsUpTrackingNum1 |= 0x80; - report.sCurrentTouch.bIsUpTrackingNum2 |= 0x80; + report.sCurrentTouch.bIsUpTrackingNum1 = std::to_integer(std::byte {report.sCurrentTouch.bIsUpTrackingNum1} | std::byte {0x80}); + report.sCurrentTouch.bIsUpTrackingNum2 = std::to_integer(std::byte {report.sCurrentTouch.bIsUpTrackingNum2} | std::byte {0x80}); // Remove all pointer index mappings - gamepad.pointer_id_map.clear(); + pointer_id_map.clear(); // All pointers are now available - gamepad.available_pointers = 0x3; + available_pointers = std::byte {0x3}; } else { - auto i = gamepad.pointer_id_map.find(touch.pointerId); - if (i == gamepad.pointer_id_map.end()) { + auto i = pointer_id_map.find(touch.pointerId); + if (i == pointer_id_map.end()) { BOOST_LOG(warning) << "Pointer ID not found! Did the client miss a touch down event?"sv; - return; + return false; } 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); + pointer_id_map.erase(i); // Set pointer up if (pointerIndex == 0) { - report.sCurrentTouch.bIsUpTrackingNum1 |= 0x80; + report.sCurrentTouch.bIsUpTrackingNum1 = std::to_integer(std::byte {report.sCurrentTouch.bIsUpTrackingNum1} | std::byte {0x80}); } else { - report.sCurrentTouch.bIsUpTrackingNum2 |= 0x80; + report.sCurrentTouch.bIsUpTrackingNum2 = std::to_integer(std::byte {report.sCurrentTouch.bIsUpTrackingNum2} | std::byte {0x80}); } // Free the pointer index - gamepad.available_pointers |= (1 << pointerIndex); + available_pointers |= (std::byte {1} << pointerIndex); } else if (touch.eventType != LI_TOUCH_EVENT_MOVE) { BOOST_LOG(warning) << "Unsupported touch event for gamepad: "sv << (uint32_t) touch.eventType; - return; + return false; } } // Touchpad is 1920x943 according to ViGEm uint16_t x = touch.x * 1920; uint16_t y = touch.y * 943; - std::array 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 + std::array touchData = { + static_cast(x & 0xFF), // Low 8 bits of X + static_cast((x >> 8 & 0x0F) | (y & 0x0F) << 4), // High 4 bits of X and low 4 bits of Y + static_cast(y >> 4 & 0xFF) // High 8 bits of Y }; report.sCurrentTouch.bPacketCounter++; @@ -1148,7 +1122,93 @@ namespace platf { } } - ds4_update_ts_and_send(vigem, touch.id.globalIndex); + return true; + } + + void vigem::update_battery(DS4_REPORT_EX &ds4_report, const gamepad_battery_t &battery) { + // 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 = ds4_report.Report; + auto battery_status = std::byte {report.bBatteryLvlSpecial}; + + // 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) { + battery_status |= std::byte {0x10}; // Connected via USB + } else { + battery_status &= ~std::byte {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 ((battery_status & std::byte {0xF}) > std::byte {0xA}) { + battery_status = (battery_status & ~std::byte {0xF}) | std::byte {0x5}; + } + break; + + case LI_BATTERY_STATE_FULL: + battery_status = std::byte {0x1B}; // USB + Battery Full + report.bBatteryLvl = 0xFF; + break; + + case LI_BATTERY_STATE_NOT_PRESENT: + case LI_BATTERY_STATE_NOT_CHARGING: + battery_status = std::byte {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 ((battery_status & std::byte {0x10}) != std::byte {0} && (battery_status & std::byte {0xF}) <= std::byte {0xA}) { + battery_status = (battery_status & ~std::byte {0xF}) | static_cast((battery.percentage + 5) / 10); + } + } + + report.bBatteryLvlSpecial = std::to_integer(battery_status); + } + + /** + * @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 raw = (input_raw_t *) input.get(); + if (virtualhid::has_gamepad(raw->virtualhid, touch.id.globalIndex)) { + virtualhid::gamepad_touch(raw->virtualhid, touch); + return; + } + + auto *vigem = raw->vigem.get(); + + // 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; + } + + if (vigem::update_touch(gamepad.report.ds4, gamepad.pointer_id_map, gamepad.available_pointers, touch)) { + ds4_update_ts_and_send(vigem, touch.id.globalIndex); + } } /** @@ -1213,53 +1273,7 @@ namespace platf { 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); - } - } - + vigem::update_battery(gamepad.report.ds4, battery); ds4_update_ts_and_send(vigem, battery.id.globalIndex); } diff --git a/src/platform/windows/input.h b/src/platform/windows/input.h new file mode 100644 index 00000000000..cabedb8fa0e --- /dev/null +++ b/src/platform/windows/input.h @@ -0,0 +1,48 @@ +/** + * @file src/platform/windows/input.h + * @brief ViGEm report updates that do not submit input to a device. + */ +#pragma once + +// standard includes +#include +#include +#include + +// platform includes +#include + +// lib includes +#include + +// local includes +#include "src/platform/common.h" + +namespace platf::vigem { + /** + * @brief Scale an 8-bit rumble amplitude to the established 16-bit range. + * + * @param strength Rumble amplitude reported by ViGEm. + * @return Amplitude multiplied by 256. + */ + std::uint16_t scale_rumble(std::uint8_t strength); + + /** + * @brief Update a DualShock 4 touch report and its pointer allocation state. + * + * @param ds4_report Report receiving the touch event. + * @param pointer_id_map Mapping from client pointer IDs to the two report slots. + * @param available_pointers Byte mask of unallocated report slots. + * @param touch Client touch event. + * @return True when the updated report should be submitted to ViGEm. + */ + bool update_touch(DS4_REPORT_EX &ds4_report, std::map &pointer_id_map, std::byte &available_pointers, const gamepad_touch_t &touch); + + /** + * @brief Update DualShock 4 battery fields while preserving unknown values. + * + * @param ds4_report Report receiving the battery event. + * @param battery Client battery state and percentage. + */ + void update_battery(DS4_REPORT_EX &ds4_report, const gamepad_battery_t &battery); +} // namespace platf::vigem diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index c619138d47f..9556275828d 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -2,6 +2,16 @@ * @file src/platform/windows/misc.cpp * @brief Miscellaneous definitions for Windows. */ + +// TODO: Revisit this override when formalizing the Windows 11 minimum API target. +#ifndef NTDDI_VERSION + /** + * @def NTDDI_VERSION + * @brief Target Windows 10 APIs before Boost or Windows headers select a default. + */ + #define NTDDI_VERSION NTDDI_WIN10 +#endif + // standard includes #include #include @@ -10,12 +20,14 @@ #include #include #include +#include #include // lib includes #include #include #include +#include // local includes required before platform includes #include "src/boost_process_compat.h" @@ -35,16 +47,8 @@ #include #include #include -// clang-format on - -// Boost overrides NTDDI_VERSION, so we re-override it here -#undef NTDDI_VERSION -/** - * @def NTDDI_VERSION - * @brief Macro for NTDDI VERSION. - */ -#define NTDDI_VERSION NTDDI_WIN10 #include +// clang-format on // local includes #include "misc.h" @@ -100,7 +104,7 @@ extern "C" { namespace { - std::atomic used_nt_set_timer_resolution = false; + std::atomic used_nt_set_timer_resolution = false; bool nt_set_timer_resolution_max() { ULONG maximum; @@ -178,8 +182,7 @@ namespace platf { std::string from_sockaddr(const sockaddr *const socket_address) { std::array data = {}; - auto family = socket_address->sa_family; - if (family == AF_INET6) { + if (auto family = socket_address->sa_family; family == AF_INET6) { inet_ntop(AF_INET6, &((const sockaddr_in6 *) socket_address)->sin6_addr, data.data(), INET6_ADDRSTRLEN); } else if (family == AF_INET) { inet_ntop(AF_INET, &((const sockaddr_in *) socket_address)->sin_addr, data.data(), INET_ADDRSTRLEN); @@ -206,19 +209,19 @@ namespace platf { * @return Adapter-address list populated by GetAdaptersAddresses. */ adapteraddrs_t get_adapteraddrs() { - adapteraddrs_t info {nullptr}; + adapteraddrs_t adapter_addresses {nullptr}; ULONG size = 0; - while (GetAdaptersAddresses(AF_UNSPEC, 0, nullptr, info.get(), &size) == ERROR_BUFFER_OVERFLOW) { - info.reset((PIP_ADAPTER_ADDRESSES) malloc(size)); + while (GetAdaptersAddresses(AF_UNSPEC, 0, nullptr, adapter_addresses.get(), &size) == ERROR_BUFFER_OVERFLOW) { + adapter_addresses.reset((PIP_ADAPTER_ADDRESSES) malloc(size)); } - return info; + return adapter_addresses; } 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) { + adapteraddrs_t adapter_addresses = get_adapteraddrs(); + for (auto adapter_pos = adapter_addresses.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; @@ -376,6 +379,8 @@ namespace platf { /** * @brief Merge user environment variables into a Windows environment block. * + * Existing variable names retain their spelling when matched case-insensitively. + * * @param env Environment variables for the child process. * @param shell_token Shell token. * @return True when the user environment block was merged into `env`. @@ -395,7 +400,7 @@ namespace platf { std::string env_val = env_tuple.substr(env_tuple.find('=') + 1); // Perform a case-insensitive search to see if this variable name already exists - if (auto itr = std::find_if(env.begin(), env.end(), [&](const auto &e) { + if (auto itr = boost::range::find_if(env, [&env_name](const auto &e) { return boost::iequals(e.get_name(), env_name); }); itr != env.end()) { @@ -629,7 +634,7 @@ namespace platf { 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)); + auto 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; @@ -830,7 +835,7 @@ namespace platf { // 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); + auto lg = std::scoped_lock(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)) { @@ -1028,7 +1033,7 @@ namespace platf { 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) { + auto itr = boost::range::find_if(std::as_const(sunshine_wenv), [&path_var_name](const auto &e) { return boost::iequals(e.get_name(), path_var_name); }); if (itr != sunshine_wenv.cend()) { @@ -1042,7 +1047,7 @@ namespace platf { 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([&]() { + auto restore_path = util::fail_guard([&old_path_val, &sunshine_wenv, &path_var_name]() { if (old_path_val.empty()) { sunshine_wenv[path_var_name].clear(); } else { @@ -1145,7 +1150,7 @@ namespace platf { win32_priority = THREAD_PRIORITY_HIGHEST; break; default: - BOOST_LOG(error) << "Unknown thread priority: "sv << (int) priority; + BOOST_LOG(error) << "Unknown thread priority: "sv << std::to_underlying(priority); return; } @@ -1234,8 +1239,8 @@ namespace platf { // 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) { + auto wlan_status = fn_WlanSetInterface(wlan_handle, &wlan_interface_list->InterfaceInfo[i].InterfaceGuid, wlan_intf_opcode_media_streaming_mode, sizeof(value), &value, nullptr); + if (wlan_status == ERROR_SUCCESS) { BOOST_LOG(info) << "WLAN interface "sv << i << " is now in low latency mode"sv; } } @@ -1372,8 +1377,8 @@ namespace platf { 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; + auto close_error = GetLastError(); + BOOST_LOG(warning) << "Failed to send WM_CLOSE to PID ["sv << wnd_process_id << "]: " << close_error; } } @@ -1502,7 +1507,7 @@ namespace platf { // 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) { + bool send_batch(const batched_send_info_t &send_info) { WSAMSG msg; // Convert the target address into a SOCKADDR @@ -1633,11 +1638,11 @@ namespace platf { class qos_t: public deinit_t { public: /** - * @brief Store a Windows QoS flow ID for cleanup on destruction. + * @brief Explicitly store a Windows QoS flow ID for cleanup on destruction. * * @param flow_id Flow ID. */ - qos_t(QOS_FLOWID flow_id): + explicit qos_t(QOS_FLOWID flow_id): flow_id(flow_id) { } @@ -1750,7 +1755,7 @@ namespace platf { traffic_type = QOSTrafficTypeAudioVideo; break; default: - BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type; + BOOST_LOG(error) << "Unknown traffic type: "sv << std::to_underlying(data_type); return nullptr; } @@ -1768,8 +1773,7 @@ namespace platf { * @brief Read the current Windows high-resolution performance counter. */ int64_t qpc_counter() { - LARGE_INTEGER performance_counter; - if (QueryPerformanceCounter(&performance_counter)) { + if (LARGE_INTEGER performance_counter; QueryPerformanceCounter(&performance_counter)) { return performance_counter.QuadPart; } return 0; @@ -1785,8 +1789,7 @@ namespace platf { QueryPerformanceFrequency(&frequency); return frequency.QuadPart; }; - static const double frequency = get_frequency(); - if (frequency) { + if (static const double frequency = get_frequency(); frequency) { return std::chrono::nanoseconds((int64_t) ((performance_counter1 - performance_counter2) * frequency / std::nano::den)); } return {}; @@ -1843,7 +1846,12 @@ namespace platf { WaitForSingleObject(timer, INFINITE); } - operator bool() override { + /** + * @brief Explicitly check whether the timer backend is initialized. + * + * @return True when the platform timer is available. + */ + explicit operator bool() override { return timer != nullptr; } diff --git a/src/platform/windows/nvprefs/driver_settings.cpp b/src/platform/windows/nvprefs/driver_settings.cpp index 42410729a74..7e5f5ef97b8 100644 --- a/src/platform/windows/nvprefs/driver_settings.cpp +++ b/src/platform/windows/nvprefs/driver_settings.cpp @@ -70,8 +70,7 @@ namespace nvprefs { return false; } - NvAPI_Status status = NvAPI_DRS_LoadSettings(session_handle); - if (status != NVAPI_OK) { + if (NvAPI_Status status = NvAPI_DRS_LoadSettings(session_handle); status != NVAPI_OK) { nvapi_error_message(status); error_message("NvAPI_DRS_LoadSettings() failed"); destroy(); @@ -86,8 +85,7 @@ namespace nvprefs { return false; } - NvAPI_Status status = NvAPI_DRS_SaveSettings(session_handle); - if (status != NVAPI_OK) { + if (NvAPI_Status status = NvAPI_DRS_SaveSettings(session_handle); status != NVAPI_OK) { nvapi_error_message(status); error_message("NvAPI_DRS_SaveSettings() failed"); return false; @@ -101,8 +99,7 @@ namespace nvprefs { return false; } - const auto &swapchain_data = undo_data.get_opengl_swapchain(); - if (swapchain_data) { + if (const auto &swapchain_data = undo_data.get_opengl_swapchain(); swapchain_data) { NvAPI_Status status; NvDRSProfileHandle profile_handle = nullptr; @@ -118,7 +115,7 @@ namespace nvprefs { 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) { + if (swapchain_data->undo_value.has_value()) { setting = {}; setting.version = NVDRS_SETTING_VER1; setting.settingId = OGL_CPL_PREFER_DXPRESENT_ID; diff --git a/src/platform/windows/nvprefs/driver_settings.h b/src/platform/windows/nvprefs/driver_settings.h index 7e1f4b214f9..a111aeb6f07 100644 --- a/src/platform/windows/nvprefs/driver_settings.h +++ b/src/platform/windows/nvprefs/driver_settings.h @@ -71,6 +71,7 @@ namespace nvprefs { /** * @brief Restore global NVIDIA profile settings from undo data. + * @details A saved value of zero is restored; only an absent value removes the setting. * * @param undo_data Driver settings captured before Sunshine modified them. * @return True when the NVIDIA driver-settings operation succeeds. diff --git a/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp b/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp index 735a5805689..ba2ada20a9a 100644 --- a/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp +++ b/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp @@ -30,13 +30,6 @@ namespace { } // namespace -#undef NVAPI_INTERFACE -/** - * @def NVAPI_INTERFACE - * @brief Macro for NVAPI INTERFACE. - */ -#define NVAPI_INTERFACE NvAPI_Status __cdecl - /** * @brief NVAPI export used to resolve function pointers by interface ID. * @@ -45,27 +38,28 @@ namespace { */ extern void *__cdecl nvapi_QueryInterface(NvU32 id); -NVAPI_INTERFACE +NvAPI_Status __cdecl -/** - * @brief Load NVAPI and resolve the subset of interfaces used by Sunshine. - * - * @return NVAPI_OK on success, or NVAPI_LIBRARY_NOT_FOUND when the DLL cannot be loaded. - */ -NvAPI_Initialize() { + /** + * @brief Load NVAPI and resolve the subset of interfaces used by Sunshine. + * + * @return NVAPI_OK on success, or NVAPI_LIBRARY_NOT_FOUND when the DLL cannot be loaded. + */ + NvAPI_Initialize() { if (dll) { return NVAPI_OK; } + if (const auto *dll_name = [] { #if defined(_M_ARM64) || defined(__aarch64__) - auto dll_name = "nvapia64.dll"; + return "nvapia64.dll"; #elif defined(_WIN64) - auto dll_name = "nvapi64.dll"; + return "nvapi64.dll"; #else - auto dll_name = "nvapi.dll"; + return "nvapi.dll"; #endif - - if ((dll = LoadLibraryEx(dll_name, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32))) { + }(); + (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); @@ -83,7 +77,7 @@ NvAPI_Initialize() { * * @return NVAPI_OK after cleanup. */ -NVAPI_INTERFACE NvAPI_Unload() { +NvAPI_Status __cdecl NvAPI_Unload() { if (dll) { interfaces.clear(); FreeLibrary(dll); @@ -99,7 +93,7 @@ NVAPI_INTERFACE NvAPI_Unload() { * @param szDesc Output buffer receiving the human-readable error string. * @return NVAPI status returned by the loaded function. */ -NVAPI_INTERFACE NvAPI_GetErrorMessage(NvAPI_Status nr, NvAPI_ShortString szDesc) { +NvAPI_Status __cdecl NvAPI_GetErrorMessage(NvAPI_Status nr, NvAPI_ShortString szDesc) { return call_interface("NvAPI_GetErrorMessage", nr, szDesc); } @@ -111,7 +105,7 @@ NVAPI_INTERFACE NvAPI_GetErrorMessage(NvAPI_Status nr, NvAPI_ShortString szDesc) * @param phSession Output handle for the created DRS session. * @return NVAPI status returned by the loaded function. */ -NVAPI_INTERFACE NvAPI_DRS_CreateSession(NvDRSSessionHandle *phSession) { +NvAPI_Status __cdecl NvAPI_DRS_CreateSession(NvDRSSessionHandle *phSession) { return call_interface("NvAPI_DRS_CreateSession", phSession); } @@ -121,7 +115,7 @@ NVAPI_INTERFACE NvAPI_DRS_CreateSession(NvDRSSessionHandle *phSession) { * @param hSession DRS session handle to destroy. * @return NVAPI status returned by the loaded function. */ -NVAPI_INTERFACE NvAPI_DRS_DestroySession(NvDRSSessionHandle hSession) { +NvAPI_Status __cdecl NvAPI_DRS_DestroySession(NvDRSSessionHandle hSession) { return call_interface("NvAPI_DRS_DestroySession", hSession); } @@ -131,7 +125,7 @@ NVAPI_INTERFACE NvAPI_DRS_DestroySession(NvDRSSessionHandle hSession) { * @param hSession DRS session whose settings are loaded. * @return NVAPI status returned by the loaded function. */ -NVAPI_INTERFACE NvAPI_DRS_LoadSettings(NvDRSSessionHandle hSession) { +NvAPI_Status __cdecl NvAPI_DRS_LoadSettings(NvDRSSessionHandle hSession) { return call_interface("NvAPI_DRS_LoadSettings", hSession); } @@ -141,7 +135,7 @@ NVAPI_INTERFACE NvAPI_DRS_LoadSettings(NvDRSSessionHandle hSession) { * @param hSession DRS session whose settings are saved. * @return NVAPI status returned by the loaded function. */ -NVAPI_INTERFACE NvAPI_DRS_SaveSettings(NvDRSSessionHandle hSession) { +NvAPI_Status __cdecl NvAPI_DRS_SaveSettings(NvDRSSessionHandle hSession) { return call_interface("NvAPI_DRS_SaveSettings", hSession); } @@ -153,7 +147,7 @@ NVAPI_INTERFACE NvAPI_DRS_SaveSettings(NvDRSSessionHandle hSession) { * @param phProfile Output handle for the created profile. * @return NVAPI status returned by the loaded function. */ -NVAPI_INTERFACE NvAPI_DRS_CreateProfile(NvDRSSessionHandle hSession, NVDRS_PROFILE *pProfileInfo, NvDRSProfileHandle *phProfile) { +NvAPI_Status __cdecl NvAPI_DRS_CreateProfile(NvDRSSessionHandle hSession, NVDRS_PROFILE *pProfileInfo, NvDRSProfileHandle *phProfile) { return call_interface("NvAPI_DRS_CreateProfile", hSession, pProfileInfo, phProfile); } @@ -165,7 +159,7 @@ NVAPI_INTERFACE NvAPI_DRS_CreateProfile(NvDRSSessionHandle hSession, NVDRS_PROFI * @param phProfile Output handle for the matched profile. * @return NVAPI status returned by the loaded function. */ -NVAPI_INTERFACE NvAPI_DRS_FindProfileByName(NvDRSSessionHandle hSession, NvAPI_UnicodeString profileName, NvDRSProfileHandle *phProfile) { +NvAPI_Status __cdecl NvAPI_DRS_FindProfileByName(NvDRSSessionHandle hSession, NvAPI_UnicodeString profileName, NvDRSProfileHandle *phProfile) { return call_interface("NvAPI_DRS_FindProfileByName", hSession, profileName, phProfile); } @@ -177,7 +171,7 @@ NVAPI_INTERFACE NvAPI_DRS_FindProfileByName(NvDRSSessionHandle hSession, NvAPI_U * @param pApplication Application entry to create. * @return NVAPI status returned by the loaded function. */ -NVAPI_INTERFACE NvAPI_DRS_CreateApplication(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_APPLICATION *pApplication) { +NvAPI_Status __cdecl NvAPI_DRS_CreateApplication(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_APPLICATION *pApplication) { return call_interface("NvAPI_DRS_CreateApplication", hSession, hProfile, pApplication); } @@ -190,7 +184,7 @@ NVAPI_INTERFACE NvAPI_DRS_CreateApplication(NvDRSSessionHandle hSession, NvDRSPr * @param pApplication Output application information. * @return NVAPI status returned by the loaded function. */ -NVAPI_INTERFACE NvAPI_DRS_GetApplicationInfo(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvAPI_UnicodeString appName, NVDRS_APPLICATION *pApplication) { +NvAPI_Status __cdecl NvAPI_DRS_GetApplicationInfo(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvAPI_UnicodeString appName, NVDRS_APPLICATION *pApplication) { return call_interface("NvAPI_DRS_GetApplicationInfo", hSession, hProfile, appName, pApplication); } @@ -202,7 +196,7 @@ NVAPI_INTERFACE NvAPI_DRS_GetApplicationInfo(NvDRSSessionHandle hSession, NvDRSP * @param pSetting Setting value to apply. * @return NVAPI status returned by the loaded function. */ -NVAPI_INTERFACE NvAPI_DRS_SetSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_SETTING *pSetting) { +NvAPI_Status __cdecl NvAPI_DRS_SetSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_SETTING *pSetting) { return call_interface("NvAPI_DRS_SetSetting", hSession, hProfile, pSetting); } @@ -215,7 +209,7 @@ NVAPI_INTERFACE NvAPI_DRS_SetSetting(NvDRSSessionHandle hSession, NvDRSProfileHa * @param pSetting Output setting value. * @return NVAPI status returned by the loaded function. */ -NVAPI_INTERFACE NvAPI_DRS_GetSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvU32 settingId, NVDRS_SETTING *pSetting) { +NvAPI_Status __cdecl NvAPI_DRS_GetSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvU32 settingId, NVDRS_SETTING *pSetting) { return call_interface("NvAPI_DRS_GetSetting", hSession, hProfile, settingId, pSetting); } @@ -227,7 +221,7 @@ NVAPI_INTERFACE NvAPI_DRS_GetSetting(NvDRSSessionHandle hSession, NvDRSProfileHa * @param settingId Setting ID. * @return NVAPI status returned by the loaded function. */ -NVAPI_INTERFACE NvAPI_DRS_DeleteProfileSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvU32 settingId) { +NvAPI_Status __cdecl NvAPI_DRS_DeleteProfileSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvU32 settingId) { return call_interface("NvAPI_DRS_DeleteProfileSetting", hSession, hProfile, settingId); } @@ -238,6 +232,6 @@ NVAPI_INTERFACE NvAPI_DRS_DeleteProfileSetting(NvDRSSessionHandle hSession, NvDR * @param phProfile Output handle for the base profile. * @return NVAPI status returned by the loaded function. */ -NVAPI_INTERFACE NvAPI_DRS_GetBaseProfile(NvDRSSessionHandle hSession, NvDRSProfileHandle *phProfile) { +NvAPI_Status __cdecl NvAPI_DRS_GetBaseProfile(NvDRSSessionHandle hSession, NvDRSProfileHandle *phProfile) { return call_interface("NvAPI_DRS_GetBaseProfile", hSession, phProfile); } diff --git a/src/platform/windows/nvprefs/nvprefs_common.h b/src/platform/windows/nvprefs/nvprefs_common.h index f422f159bc6..e9cd1a57419 100644 --- a/src/platform/windows/nvprefs/nvprefs_common.h +++ b/src/platform/windows/nvprefs/nvprefs_common.h @@ -19,8 +19,8 @@ namespace nvprefs { /** * @brief Owning Windows HANDLE wrapper that closes handles automatically. */ - struct safe_handle: public util::safe_ptr_v2 { - using util::safe_ptr_v2::safe_ptr_v2; + struct safe_handle: public util::safe_ptr_v2 { + using util::safe_ptr_v2::safe_ptr_v2; /** * @brief Check whether the wrapped Windows handle is valid. @@ -38,9 +38,11 @@ namespace nvprefs { /** * @brief Free memory allocated by Windows local-memory APIs. * + * @tparam T Native allocation type returned by the Windows API. * @param p LocalAlloc-compatible pointer to release. */ - void operator()(void *p) { + template + void operator()(T *p) const { LocalFree(p); } }; @@ -54,7 +56,7 @@ namespace nvprefs { /** * @brief Safe pointer for Windows security identifiers released with `FreeSid`. */ - using safe_sid = util::safe_ptr_v2; + using safe_sid = util::safe_ptr_v2; /** * @brief Forward an informational NVPrefs message to the logger. diff --git a/src/platform/windows/nvprefs/nvprefs_interface.cpp b/src/platform/windows/nvprefs/nvprefs_interface.cpp index ce27cee09ca..c1e3a760a37 100644 --- a/src/platform/windows/nvprefs/nvprefs_interface.cpp +++ b/src/platform/windows/nvprefs/nvprefs_interface.cpp @@ -109,8 +109,7 @@ namespace nvprefs { } // Modify and save sunshine.exe application profile settings, if needed - bool modified = false; - if (!pimpl->driver_settings.check_and_modify_application_profile(modified)) { + if (bool modified = false; !pimpl->driver_settings.check_and_modify_application_profile(modified)) { error_message("Failed to modify application profile settings"); return false; } else if (modified) { @@ -142,7 +141,7 @@ namespace nvprefs { return true; } - auto make_undo_and_commit = [&]() -> bool { + auto make_undo_and_commit = [&]() { // 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 @@ -194,7 +193,7 @@ namespace nvprefs { return true; } - bool nvprefs_interface::owning_undo_file() { + bool nvprefs_interface::owning_undo_file() const { return pimpl->undo_file.has_value(); } diff --git a/src/platform/windows/nvprefs/nvprefs_interface.h b/src/platform/windows/nvprefs/nvprefs_interface.h index 1f682ce2931..243dbae5c6a 100644 --- a/src/platform/windows/nvprefs/nvprefs_interface.h +++ b/src/platform/windows/nvprefs/nvprefs_interface.h @@ -53,9 +53,9 @@ namespace nvprefs { /** * @brief Check whether this interface owns the active undo file. * - * @return True when the NVIDIA preferences operation succeeds. + * @return True when this interface owns the undo file; querying does not load NVIDIA APIs. */ - bool owning_undo_file(); + bool owning_undo_file() const; /** * @brief Restore NVIDIA global profile settings from saved undo data. diff --git a/src/platform/windows/nvprefs/undo_file.cpp b/src/platform/windows/nvprefs/undo_file.cpp index 477af03399c..950a2f2451b 100644 --- a/src/platform/windows/nvprefs/undo_file.cpp +++ b/src/platform/windows/nvprefs/undo_file.cpp @@ -19,8 +19,7 @@ namespace { } 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)) { + if (SID_IDENTIFIER_AUTHORITY nt_authorithy = SECURITY_NT_AUTHORITY; !AllocateAndInitializeSid(&nt_authorithy, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_USERS, 0, 0, 0, 0, 0, 0, &users_sid)) { return GetLastError(); } @@ -49,7 +48,7 @@ namespace { namespace nvprefs { - std::optional undo_file_t::open_existing_file(std::filesystem::path file_path, bool &access_denied) { + std::optional undo_file_t::open_existing_file(const 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) { @@ -62,7 +61,7 @@ namespace nvprefs { } } - std::optional undo_file_t::create_new_file(std::filesystem::path file_path) { + std::optional undo_file_t::create_new_file(const 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)); @@ -109,8 +108,7 @@ namespace nvprefs { return false; } - DWORD bytes_written = 0; - if (!WriteFile(file_handle.get(), buffer.data(), buffer.size(), &bytes_written, nullptr) || bytes_written != buffer.size()) { + if (DWORD bytes_written = 0; !WriteFile(file_handle.get(), buffer.data(), buffer.size(), &bytes_written, nullptr) || bytes_written != buffer.size()) { error_message("Couldn't write undo file"); return false; } @@ -139,8 +137,7 @@ namespace nvprefs { } 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()) { + if (DWORD bytes_read = 0; !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; } diff --git a/src/platform/windows/nvprefs/undo_file.h b/src/platform/windows/nvprefs/undo_file.h index fdf4e85d484..599caa50c12 100644 --- a/src/platform/windows/nvprefs/undo_file.h +++ b/src/platform/windows/nvprefs/undo_file.h @@ -21,19 +21,19 @@ namespace nvprefs { /** * @brief Open existing file. * - * @param file_path File path. + * @param file_path File path inspected without copying or modifying it. * @param access_denied Access denied. * @return Open undo file wrapper, or std::nullopt when the file cannot be opened. */ - static std::optional open_existing_file(std::filesystem::path file_path, bool &access_denied); + static std::optional open_existing_file(const std::filesystem::path &file_path, bool &access_denied); /** * @brief Create new file. * - * @param file_path File path. + * @param file_path File path inspected without copying or modifying it. * @return Created new file object or status. */ - static std::optional create_new_file(std::filesystem::path file_path); + static std::optional create_new_file(const std::filesystem::path &file_path); /** * @brief Delete the persisted NVIDIA settings undo file. diff --git a/src/platform/windows/publish.cpp b/src/platform/windows/publish.cpp index 7347b5a49b0..7509893fea4 100644 --- a/src/platform/windows/publish.cpp +++ b/src/platform/windows/publish.cpp @@ -139,7 +139,7 @@ namespace platf::publish { * @param pInstance Registered DNS-SD service instance returned by Windows. * @return Callback has no return value; completion is reported through the alarm. */ - VOID WINAPI register_cb(DWORD status, PVOID pQueryContext, PDNS_SERVICE_INSTANCE pInstance) { + VOID WINAPI register_cb(DWORD status, PVOID pQueryContext, PDNS_SERVICE_INSTANCE pInstance) { // NOSONAR(cpp:S5008): DnsServiceRegister completion callbacks require PVOID user data. auto alarm = (safe::alarm_t::element_type *) pQueryContext; if (status) { @@ -220,8 +220,10 @@ namespace platf::publish { */ class mdns_registration_t: public ::platf::deinit_t { public: - mdns_registration_t(): - existing_instance(nullptr) { + /** + * @brief Register Sunshine's DNS-SD service with no existing registration. + */ + mdns_registration_t() { if (service(true, existing_instance)) { BOOST_LOG(error) << "Unable to register Sunshine mDNS service"sv; return; @@ -242,7 +244,7 @@ namespace platf::publish { } private: - PDNS_SERVICE_INSTANCE existing_instance; + PDNS_SERVICE_INSTANCE existing_instance {nullptr}; ///< DNS-SD registration owned by this publisher. }; /** diff --git a/src/process.cpp b/src/process.cpp index a17dbaa8a6f..867b645883f 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -5,10 +5,12 @@ #define BOOST_BIND_GLOBAL_PLACEHOLDERS // standard includes +#include #include #include #include #include +#include #include // lib includes @@ -18,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -64,7 +67,7 @@ namespace proc { return std::make_unique(); } - void terminate_process_group(boost::process::v1::child &proc, boost::process::v1::group &group, std::chrono::seconds exit_timeout) { + void terminate_process_group(boost::process::v1::child &child_process, 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 @@ -96,9 +99,9 @@ namespace proc { group.detach(); } - if (proc.valid()) { + if (child_process.valid()) { // avoid zombie process - proc.detach(); + child_process.detach(); } } @@ -130,7 +133,7 @@ namespace proc { 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) { + if (parts.at(0).contains("://")) { return boost::filesystem::path(); } @@ -227,7 +230,7 @@ namespace proc { std::error_code ec; // Executed when returning from function - auto fg = util::fail_guard([&]() { + auto fg = util::fail_guard([this]() { terminate(); }); @@ -495,7 +498,7 @@ namespace proc { // 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) { + auto itr = boost::range::find_if(std::as_const(env), [&var_name](const auto &e) { return boost::iequals(e.get_name(), var_name); }); if (itr != env.cend()) { @@ -715,7 +718,7 @@ namespace proc { this_env[name] = parse_env_val(this_env, val.get_value()); } - std::set ids; + std::set> ids; std::vector apps; int i = 0; for (auto &[_, app_node] : apps_node) { @@ -821,7 +824,7 @@ namespace proc { } return proc::proc_t { - std::move(this_env), + this_env, std::move(apps) }; } catch (std::exception &e) { diff --git a/src/process.h b/src/process.h index 26bb470a196..132178be709 100644 --- a/src/process.h +++ b/src/process.h @@ -33,12 +33,12 @@ namespace proc { /** * @brief Boost.Process pipe stream used for child-process I/O. */ - using file_t = util::safe_ptr_v2; + using file_t = util::safe_ptr_v2; /** * @brief Parsed command arguments used when launching a child process. */ - typedef config::prep_cmd_t cmd_t; + using cmd_t = config::prep_cmd_t; /** * pre_cmds -- guaranteed to be executed unless any of the commands fail. @@ -236,11 +236,11 @@ namespace proc { /** * @brief Terminates all child processes in a process group. - * @param proc The child process itself. + * @param child_process 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); + void terminate_process_group(boost::process::v1::child &child_process, boost::process::v1::group &group, std::chrono::seconds exit_timeout); extern proc_t proc; } // namespace proc diff --git a/src/round_robin.h b/src/round_robin.h index bf32c60ab06..c8898c0c3b9 100644 --- a/src/round_robin.h +++ b/src/round_robin.h @@ -51,11 +51,11 @@ namespace round_robin_util { /** * @brief Concrete iterator type supplied by the CRTP child. */ - typedef T iterator; + using iterator = T; /** * @brief Signed offset type used when moving through the range. */ - typedef std::ptrdiff_t diff_t; + using diff_t = std::ptrdiff_t; /** * @brief Advance this iterator by repeatedly wrapping at the end of the range. diff --git a/src/rtsp.cpp b/src/rtsp.cpp index 481300d9a7e..71253733ce7 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -51,64 +51,12 @@ namespace rtsp_stream { delete msg; } -#pragma pack(push, 1) - - /** - * @brief Encrypted RTSP message header used by GameStream. - */ - 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; ///< Protocol or platform constant for encrypted message type bit. - - /** - * @brief Return a pointer to the protocol payload following the packet header. - * - * @return Parsed or serialized payload data. - */ - uint8_t *payload() { - return (uint8_t *) (this + 1); - } - - /** - * @brief Decode the RTSP interleaved-frame payload length. - * - * @return Payload byte count with the encrypted-message flag masked out. - */ - std::uint32_t payload_length() { - return util::endian::big(typeAndLength) & ~ENCRYPTED_MESSAGE_TYPE_BIT; - } - - /** - * @brief Check whether encrypted. - * - * @return True when the RTSP connection is using TLS. - */ - 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; ///< Type and length. - - // This field is the number used to initialize the bottom 4 bytes of the AES IV in big-endian - std::uint32_t sequenceNumber; ///< Sequence number. - - // This field is the AES GCM authentication tag - std::array tag; ///< Authentication tag appended to the encrypted payload. - }; - -#pragma pack(pop) - - static_assert(sizeof(encrypted_rtsp_header_t) == 24); - static_assert(offsetof(encrypted_rtsp_header_t, tag) == 8); - class rtsp_server_t; /** * @brief Owning pointer for an RTSP message allocated by moonlight-common-c. */ - using msg_t = util::safe_ptr; + using msg_t = util::safe_ptr; /** * @brief RTSP command handler signature used by the server dispatch table. */ @@ -375,7 +323,7 @@ namespace rtsp_stream { // 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) { + auto begin = std::ranges::find_if(content, [](auto ch) { return (bool) std::isdigit(ch); }); @@ -410,11 +358,11 @@ namespace rtsp_stream { 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); + boost::system::error_code close_error; + socket->sock.close(close_error); - if (ec) { - BOOST_LOG(error) << "RTSP: handle_read_plaintext(): Couldn't close tcp socket: "sv << ec.message(); + if (close_error) { + BOOST_LOG(error) << "RTSP: handle_read_plaintext(): Couldn't close tcp socket: "sv << close_error.message(); } return; @@ -529,8 +477,7 @@ namespace rtsp_stream { * @param req Parsed RTSP request being handled. */ 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)) { + if (auto func = _map_cmd_cb.find(req->message.request.command); func != std::end(_map_cmd_cb)) { func->second(this, sock, session, std::move(req)); } else { cmd_not_found(sock, session, std::move(req)); @@ -566,8 +513,8 @@ namespace rtsp_stream { 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); + boost::system::error_code close_error; + socket->sock.close(close_error); } // Queue another asynchronous accept for the next incoming connection @@ -586,7 +533,7 @@ namespace rtsp_stream { * @param cb Callback invoked for each matching message or session. */ void map(const std::string_view &type, cmd_func_t cb) { - _map_cmd_cb.emplace(type, std::move(cb)); + _map_cmd_cb.try_emplace(type, std::move(cb)); } /** @@ -804,12 +751,12 @@ namespace rtsp_stream { * @param resp RTSP response string to send to the client. */ void respond(tcp::socket &sock, launch_session_t &session, msg_t &resp) { - auto payload = std::make_pair(resp->payload, resp->payloadLength); + auto [payload, original_payload_length] = 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; + auto lg = util::fail_guard([&resp, &payload, &original_payload_length]() { + resp->payload = payload; + resp->payloadLength = original_payload_length; }); resp->payload = nullptr; @@ -820,7 +767,7 @@ namespace rtsp_stream { 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 + << std::string_view {payload, (std::size_t) original_payload_length} << std::endl << "---End Response---"sv << std::endl; // Encrypt the RTSP message if encryption is enabled @@ -840,21 +787,21 @@ namespace rtsp_stream { iv[11] = 'R'; // RTSP // Allocate the message with an empty header and reserved space for the payload - auto payload_length = serialized_len + payload.second; + auto message_length = serialized_len + original_payload_length; std::vector message(sizeof(encrypted_rtsp_header_t)); - message.reserve(message.size() + payload_length); + message.reserve(message.size() + message_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)); + std::copy_n(payload, original_payload_length, 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->typeAndLength = util::endian::big(encrypted_rtsp_header_t::ENCRYPTED_MESSAGE_TYPE_BIT + message_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.data(), &iv); + session.rtsp_cipher->encrypt(std::string_view {(const char *) header->payload(), (std::size_t) message_length}, header->tag.data(), &iv); // Send the full encrypted message send(sock, std::string_view {(char *) message.data(), message.size()}); @@ -867,7 +814,7 @@ namespace rtsp_stream { } // Send the plaintext RTSP message payload (if present) - send(sock, std::string_view {payload.first, (std::size_t) payload.second}); + send(sock, std::string_view {payload, (std::size_t) original_payload_length}); } } @@ -890,13 +837,13 @@ namespace rtsp_stream { /** * @brief Handle RTSP OPTIONS requests. + * @details The common command signature supplies a server pointer that this handler does not need. * - * @param server RTSP server instance handling the request. * @param sock Socket used to read or write the protocol message. * @param session Active streaming or pairing session for the request. * @param req Parsed RTSP request being handled. */ - void cmd_option(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { + void cmd_option(rtsp_server_t *, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified @@ -910,13 +857,13 @@ namespace rtsp_stream { /** * @brief Handle RTSP DESCRIBE requests. + * @details The common command signature supplies a server pointer that this handler does not need. * - * @param server RTSP server instance. * @param sock Client socket. * @param session Launch session. * @param req RTSP request message. */ - void cmd_describe(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { + void cmd_describe(rtsp_server_t *, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified @@ -935,8 +882,7 @@ namespace rtsp_stream { 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) { + if (auto encryption_mode = net::encryption_mode_for_address(sock.remote_endpoint().address()); encryption_mode != config::ENCRYPTION_MODE_NEVER) { // Advertise support for video encryption if it's not disabled encryption_flags_supported |= SS_ENC_VIDEO; @@ -1002,12 +948,12 @@ namespace rtsp_stream { /** * @brief Handle RTSP SETUP requests and prepare transport state. * - * @param server RTSP server instance handling the request. + * @param rtsp_server RTSP server instance handling the request. * @param sock Socket used to read or write the protocol message. * @param session Active streaming or pairing session for the request. * @param req Parsed RTSP request being handled. */ - void cmd_setup(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { + void cmd_setup(rtsp_server_t *rtsp_server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { std::array options {}; auto &seqn = options[0]; @@ -1021,7 +967,7 @@ namespace rtsp_stream { 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 begin = std::ranges::find(target, '=') + 1; auto end = std::find(begin, std::end(target), '/'); std::string_view type {begin, (size_t) std::distance(begin, end)}; @@ -1069,12 +1015,12 @@ namespace rtsp_stream { /** * @brief Handle RTSP ANNOUNCE requests from the client. * - * @param server RTSP server instance handling the request. + * @param rtsp_server RTSP server instance handling the request. * @param sock Socket used to read or write the protocol message. * @param session Active streaming or pairing session for the request. * @param req Parsed RTSP request being handled. */ - void cmd_announce(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { + void cmd_announce(rtsp_server_t *rtsp_server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified @@ -1122,7 +1068,7 @@ namespace rtsp_stream { if (val[val.size() - 1] == ' ') { val = val.substr(0, val.size() - 1); } - args.emplace(name, val); + args.try_emplace(name, val); } } @@ -1218,11 +1164,11 @@ namespace rtsp_stream { // 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}; + for (auto request_option = req->options; request_option != nullptr; request_option = request_option->next) { + if ("Host"sv == request_option->option) { + std::string_view content {request_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); + config.audio.flags[audio::config_t::HIGH_QUALITY] = !content.contains("0.0.0.0"sv); } } } else if (session.surround_params.length() > 3) { @@ -1303,12 +1249,12 @@ namespace rtsp_stream { } auto stream_session = stream::session::alloc(config, session); - server->insert(stream_session); + rtsp_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); + rtsp_server->remove(stream_session); respond(sock, session, &option, 500, "Internal Server Error", req->sequenceNumber, {}); return; } @@ -1318,13 +1264,13 @@ namespace rtsp_stream { /** * @brief Handle RTSP PLAY requests and start streaming. + * @details The common command signature supplies a server pointer that this handler does not need. * - * @param server RTSP server instance handling the request. * @param sock Socket used to read or write the protocol message. * @param session Active streaming or pairing session for the request. * @param req Parsed RTSP request being handled. */ - void cmd_play(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { + void cmd_play(rtsp_server_t *, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified diff --git a/src/rtsp.h b/src/rtsp.h index 8fa8dd6f5d8..108c881e200 100644 --- a/src/rtsp.h +++ b/src/rtsp.h @@ -5,7 +5,10 @@ #pragma once // standard includes +#include #include +#include +#include // local includes #include "crypto.h" @@ -14,6 +17,58 @@ namespace rtsp_stream { constexpr auto RTSP_SETUP_PORT = 21; ///< GameStream base-port offset used for the RTSP setup listener. +#pragma pack(push, 1) + + /** + * @brief Encrypted RTSP message header used by GameStream. + */ + 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; ///< Protocol or platform constant for encrypted message type bit. + + /** + * @brief Return a pointer to the protocol payload following the packet header. + * + * @return Parsed or serialized payload data. + */ + uint8_t *payload() { + return (uint8_t *) (this + 1); + } + + /** + * @brief Decode the RTSP interleaved-frame payload length. + * + * @return Payload byte count with the encrypted-message flag masked out. + */ + std::uint32_t payload_length() const { + return util::endian::big(typeAndLength) & ~ENCRYPTED_MESSAGE_TYPE_BIT; + } + + /** + * @brief Inspect the encrypted-message flag without modifying the wire header. + * + * @return True when the encrypted-message flag is set. + */ + bool is_encrypted() const { + 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; ///< Type and length. + + // This field is the number used to initialize the bottom 4 bytes of the AES IV in big-endian + std::uint32_t sequenceNumber; ///< Sequence number. + + // This field is the AES GCM authentication tag + std::array tag; ///< Authentication tag appended to the encrypted payload. + }; + +#pragma pack(pop) + + static_assert(sizeof(encrypted_rtsp_header_t) == 24); + static_assert(offsetof(encrypted_rtsp_header_t, tag) == 8); + /** * @brief RTSP launch session state shared with stream setup. */ diff --git a/src/stream.cpp b/src/stream.cpp index 8ecfb3ee319..2f2db0c2fc9 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -289,7 +289,7 @@ namespace stream { /** * @brief Packed encrypted control-channel envelope. */ - typedef struct control_encrypted_t { + struct control_encrypted_t { std::uint16_t encryptedHeaderType; ///< Always LE 0x0001. std::uint16_t length; ///< Size of seq, tag, secondary header, and data. @@ -306,7 +306,12 @@ namespace stream { } // encrypted control_header_v2 and payload data follow - } *control_encrypted_p; ///< Alias for control encrypted p. + }; + + /** + * @brief Pointer to a packed encrypted control-channel envelope. + */ + using control_encrypted_p = control_encrypted_t *; /** * @brief Packed RTP and FEC headers for an audio recovery packet. @@ -362,7 +367,7 @@ namespace stream { 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); + std::ranges::copy(plaintext, destination); return (int) plaintext.size(); } @@ -433,7 +438,7 @@ namespace stream { * @param cb Callback invoked for each matching message or session. */ void map(uint16_t type, std::function cb) { - _map_type_cb.emplace(type, std::move(cb)); + _map_type_cb.try_emplace(type, std::move(cb)); } /** @@ -444,8 +449,7 @@ namespace stream { * @return Network operation status. */ 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)) { + if (auto packet = enet_packet_create(payload.data(), payload.size(), ENET_PACKET_FLAG_RELIABLE); enet_peer_send(peer, 0, packet)) { enet_packet_destroy(packet); return -1; @@ -532,8 +536,7 @@ namespace stream { std::string ping_payload; ///< Payload expected from audio-channel ping packets. std::uint16_t sequenceNumber; ///< Next RTP sequence number for an audio packet. - // avRiKeyId == util::endian::big(First (sizeof(avRiKeyId)) bytes of launch_session->iv) - std::uint32_t avRiKeyId; ///< Base audio encryption key identifier derived from the launch-session IV. + std::uint32_t avRiKeyId; ///< Base audio encryption key identifier decoded from the first four IV bytes in big-endian order. std::uint32_t timestamp; ///< RTP timestamp for the next audio packet. udp::endpoint peer; ///< Client UDP endpoint for the audio channel. @@ -996,7 +999,7 @@ namespace stream { std::copy(begin, next, std::back_inserter(replaced)); if (next != end) { - std::copy(std::begin(_new), std::end(_new), std::back_inserter(replaced)); + std::ranges::copy(_new, std::back_inserter(replaced)); std::copy(next + old.size(), end, std::back_inserter(replaced)); } @@ -1185,7 +1188,7 @@ namespace stream { }); server->map(packetTypes[IDX_LOSS_STATS], [&](session_t *session, const std::string_view &payload) { - int32_t *stats = (int32_t *) payload.data(); + auto stats = (int32_t *) payload.data(); auto count = stats[0]; std::chrono::milliseconds t {stats[1]}; @@ -1457,14 +1460,14 @@ namespace stream { switch (socket_type) { case socket_e::video: if (message_queue) { - peer_to_video_session.emplace(session_id, message_queue); + peer_to_video_session.try_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); + peer_to_audio_session.try_emplace(session_id, message_queue); } else { peer_to_audio_session.erase(session_id); } @@ -1475,7 +1478,7 @@ namespace stream { 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([&]() { + auto fg = util::fail_guard([&sock, &buf, &peer, &recv_func, &buf_elem]() { sock.async_receive_from(asio::buffer(buf[buf_elem]), peer, 0, recv_func[buf_elem]); }); @@ -1562,7 +1565,7 @@ namespace stream { frame_network_latency_logger.first_point_now(); - auto session = (session_t *) packet->channel_data; + auto *session = packet->channel_data; auto lowseq = session->video.lowseq; std::string_view payload {(char *) packet->data(), packet->data_size()}; @@ -1687,9 +1690,9 @@ namespace stream { 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; + auto payload_packets = (current_payload.size() + (blocksize - 1)) / blocksize; - for (int x = 0; x < packets; ++x) { + for (int x = 0; x < payload_packets; ++x) { auto *inspect = (video_packet_raw_t *) ¤t_payload[x * blocksize]; inspect->packet.frameIndex = (uint32_t) packet->frame_index(); @@ -1703,7 +1706,7 @@ namespace stream { if (x == 0) { inspect->packet.flags |= FLAG_SOF; } - if (x == packets - 1) { + if (x == payload_packets - 1) { inspect->packet.flags |= FLAG_EOF; } } @@ -1887,7 +1890,7 @@ namespace stream { } TUPLE_2D_REF(channel_data, packet_data, *packet); - auto session = (session_t *) channel_data; + auto *session = channel_data; auto sequenceNumber = session->audio.sequenceNumber; auto timestamp = session->audio.timestamp; @@ -1939,7 +1942,7 @@ namespace stream { fec_packet.rtp.sequenceNumber = util::endian::big(sequenceNumber + x + 1); fec_packet.fecHeader.fecShardIndex = x; - auto send_info = platf::send_info_t { + auto fec_send_info = platf::send_info_t { (const char *) &fec_packet, sizeof(fec_packet), (const char *) shards_p[RTPA_DATA_SHARDS + x], @@ -1949,7 +1952,7 @@ namespace stream { session->audio.peer.port(), session->localAddress, }; - platf::send(send_info); + platf::send(fec_send_info); BOOST_LOG(verbose) << "Audio FEC ["sv << (sequenceNumber & ~(RTPA_DATA_SHARDS - 1)) << ' ' << x << "] :: send..."sv; } } @@ -2112,7 +2115,7 @@ namespace stream { } TUPLE_2D_REF(recv_peer, msg, *msg_opt); - if (msg.find(expected_payload) != std::string::npos) { + if (msg.contains(expected_payload)) { // 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) { diff --git a/src/string_hash.h b/src/string_hash.h new file mode 100644 index 00000000000..7b8d2508124 --- /dev/null +++ b/src/string_hash.h @@ -0,0 +1,29 @@ +/** + * @file src/string_hash.h + * @brief Transparent hashing for containers with owning string keys. + */ +#pragma once + +// standard includes +#include +#include +#include + +namespace util { + /** + * @brief Hash strings, string views, and C strings without temporary allocations. + */ + struct transparent_string_hash { + using is_transparent = void; ///< Enable heterogeneous unordered-container lookup. + + /** + * @brief Hash all characters in a string-like key, including embedded nulls in views. + * + * @param value Text to hash. + * @return Hash shared by equivalent owning and non-owning string keys. + */ + std::size_t operator()(std::string_view value) const noexcept { + return std::hash {}(value); + } + }; +} // namespace util diff --git a/src/sync.h b/src/sync.h index 23ed8666b77..1686aebc319 100644 --- a/src/sync.h +++ b/src/sync.h @@ -29,19 +29,19 @@ namespace sync_util { /** * @brief Acquire the underlying lock or keyed mutex. * - * @return Lock guard owning the synchronized object until destruction. + * @return Scoped lock owning the synchronized object until destruction. */ - std::lock_guard lock() { - return std::lock_guard {_lock}; + std::scoped_lock lock() { + return std::scoped_lock {_lock}; } /** - * @brief Initialize the protected object from forwarded arguments. + * @brief Explicitly initialize the protected object from forwarded arguments. * * @param args Arguments forwarded to the protected object's constructor. */ template - sync_t(Args &&...args): + explicit sync_t(Args &&...args): raw {std::forward(args)...} { } diff --git a/src/system_tray.cpp b/src/system_tray.cpp index 7343103099b..b553d4f4813 100644 --- a/src/system_tray.cpp +++ b/src/system_tray.cpp @@ -77,6 +77,7 @@ #include "platform/common.h" #include "process.h" #include "src/entry_handler.h" + #include "string_hash.h" #include "system_tray.h" #include "thread_safe.h" #ifdef _WIN32 @@ -118,23 +119,6 @@ namespace system_tray { return worker; } - /** - * @brief Hash string-like resource paths without allocating temporary strings. - */ - struct transparent_string_hash_t { - using is_transparent = void; ///< Enable heterogeneous unordered-map lookup. - - /** - * @brief Hash a string view. - * - * @param value String view to hash. - * @return Hash value for the supplied text. - */ - std::size_t operator()(const std::string_view value) const noexcept { - return std::hash {}(value); - } - }; - #if defined(_WIN32) || defined(__APPLE__) /** * @brief Access storage for dynamic Virtual HID Broker license menu labels. @@ -629,7 +613,7 @@ namespace system_tray { #if defined(_WIN32) || defined(__APPLE__) // Simple cache ensures our string pointers live forever. - static std::unordered_map> g_cache; + static std::unordered_map> g_cache; auto search = g_cache.find(std::string_view {relativePath}); if (search != g_cache.end()) { return search->second.c_str(); diff --git a/src/task_pool.h b/src/task_pool.h index 887db57f631..3827fbc78b7 100644 --- a/src/task_pool.h +++ b/src/task_pool.h @@ -26,9 +26,10 @@ namespace task_pool_util { */ class _ImplBase { public: - // _unique_base_type _this_ptr; - - inline virtual ~_ImplBase() = default; + /** + * @brief Destroy a queued callable through its polymorphic base. + */ + virtual ~_ImplBase() = default; /** * @brief Execute the queued task body. @@ -45,11 +46,11 @@ namespace task_pool_util { public: /** - * @brief Store a concrete callable behind the task type-erasure interface. + * @brief Explicitly store a concrete callable behind the task type-erasure interface. * * @param f Callable executed by the helper. */ - _Impl(Function &&f): + explicit _Impl(Function &&f): _func(std::forward(f)) { } @@ -69,16 +70,16 @@ namespace task_pool_util { /** * @brief Type-erased task owned by the task pool. */ - typedef std::unique_ptr<_ImplBase> __task; + using __task = std::unique_ptr<_ImplBase>; /** * @brief Stable pointer used to identify a queued task. */ - typedef _ImplBase *task_id_t; + using task_id_t = _ImplBase *; /** * @brief Steady-clock timestamp used to schedule delayed tasks. */ - typedef std::chrono::steady_clock::time_point __time_point; + using __time_point = std::chrono::steady_clock::time_point; /** * @brief Queued delayed task identifier paired with its future result. @@ -154,7 +155,7 @@ namespace task_pool_util { auto future = task.get_future(); - std::lock_guard lg(_task_mutex); + std::scoped_lock lg(_task_mutex); _tasks.emplace_back(toRunnable(std::move(task))); return future; @@ -166,7 +167,7 @@ namespace task_pool_util { * @param task Task object to enqueue or execute. */ void pushDelayed(std::pair<__time_point, __task> &&task) { - std::lock_guard lg(_task_mutex); + std::scoped_lock lg(_task_mutex); auto it = _timer_tasks.cbegin(); for (; it < _timer_tasks.cend(); ++it) { @@ -221,7 +222,7 @@ namespace task_pool_util { */ template void delay(task_id_t task_id, std::chrono::duration duration) { - std::lock_guard lg(_task_mutex); + std::scoped_lock lg(_task_mutex); auto it = _timer_tasks.begin(); for (; it < _timer_tasks.cend(); ++it) { @@ -257,7 +258,7 @@ namespace task_pool_util { * @return True when the task was found and cancelled. */ bool cancel(task_id_t task_id) { - std::lock_guard lg(_task_mutex); + std::scoped_lock lg(_task_mutex); auto it = _timer_tasks.begin(); for (; it < _timer_tasks.cend(); ++it) { @@ -280,9 +281,9 @@ namespace task_pool_util { * @return Removed queue item, or empty result when the queue is stopped or empty. */ std::optional> pop(task_id_t task_id) { - std::lock_guard lg(_task_mutex); + std::scoped_lock lg(_task_mutex); - auto pos = std::find_if(std::begin(_timer_tasks), std::end(_timer_tasks), [&task_id](const auto &t) { + auto pos = std::ranges::find_if(_timer_tasks, [&task_id](const auto &t) { return t.second.get() == task_id; }); @@ -299,7 +300,7 @@ namespace task_pool_util { * @return Removed queue item, or empty result when the queue is stopped or empty. */ std::optional<__task> pop() { - std::lock_guard lg(_task_mutex); + std::scoped_lock lg(_task_mutex); if (!_tasks.empty()) { __task task = std::move(_tasks.front()); @@ -322,7 +323,7 @@ namespace task_pool_util { * @return True when the task was found and cancelled. */ bool ready() { - std::lock_guard lg(_task_mutex); + std::scoped_lock lg(_task_mutex); return !_tasks.empty() || (!_timer_tasks.empty() && std::get<0>(_timer_tasks.back()) <= std::chrono::steady_clock::now()); } @@ -333,7 +334,7 @@ namespace task_pool_util { * @return Time point for the next queued timer task, or std::nullopt when none exist. */ std::optional<__time_point> next() { - std::lock_guard lg(_task_mutex); + std::scoped_lock lg(_task_mutex); if (_timer_tasks.empty()) { return std::nullopt; diff --git a/src/thread_pool.h b/src/thread_pool.h index d4d9b76257c..930ea8beee0 100644 --- a/src/thread_pool.h +++ b/src/thread_pool.h @@ -20,7 +20,7 @@ namespace thread_pool_util { /** * @brief Callable unit executed by a worker thread. */ - typedef TaskPool::__task __task; + using __task = TaskPool::__task; private: std::vector _thread; diff --git a/src/thread_safe.h b/src/thread_safe.h index de5fe1100e1..80093ee89f8 100644 --- a/src/thread_safe.h +++ b/src/thread_safe.h @@ -162,11 +162,11 @@ namespace safe { } /** - * @brief Inspect the next queued value without popping it. + * @brief Inspect whether this event has a value without modifying or consuming it. * * @return True when a value is available to inspect. */ - bool peek() { + bool peek() const { return _continue && (bool) _status; } @@ -645,9 +645,9 @@ namespace safe { } /** - * @brief Check whether this shared pointer wrapper owns a live object. + * @brief Explicitly check whether this shared pointer wrapper owns a live object. */ - operator bool() const { + explicit operator bool() const { return owner != nullptr; } @@ -766,13 +766,13 @@ namespace safe { class post_t: public T { public: /** - * @brief Construct a message-posting wrapper around an existing type. + * @brief Explicitly construct a message-posting wrapper around an existing type. * * @param mail Mailbox used to exchange messages with worker threads. * @param args Arguments forwarded to the wrapped type constructor. */ template - post_t(mail_t mail, Args &&...args): + explicit post_t(mail_t mail, Args &&...args): T(std::forward(args)...), mail {std::move(mail)} { } @@ -813,7 +813,7 @@ namespace safe { using queue_t = std::shared_ptr>>; /** - * @brief Create a typed event channel from the raw mailbox. + * @brief Create a typed event channel or reuse the live channel with the same identifier. * * @param id Identifier for the controller, session, display, or resource. * @return Typed event channel associated with the supplied identifier. @@ -822,19 +822,18 @@ namespace safe { 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)) { + if (auto it = id_to_post.find(id); 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}); + id_to_post.try_emplace(std::string {id}, post); return post; } /** - * @brief Create a typed queue channel from the raw mailbox. + * @brief Create a typed queue channel or reuse the live channel with the same identifier. * * @param id Identifier for the controller, session, display, or resource. * @return Typed queue channel associated with the supplied identifier. @@ -843,13 +842,12 @@ namespace safe { 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)) { + if (auto it = id_to_post.find(id); 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}); + id_to_post.try_emplace(std::string {id}, post); return post; } @@ -874,7 +872,7 @@ namespace safe { std::lock_guard lg {mutex}; for (auto it = std::begin(id_to_post); it != std::end(id_to_post); ++it) { - auto &weak = it->second; + const auto &weak = it->second; if (weak.expired()) { id_to_post.erase(it); diff --git a/src/upnp.cpp b/src/upnp.cpp index d983ce15d22..ea2bab0cdf6 100644 --- a/src/upnp.cpp +++ b/src/upnp.cpp @@ -39,16 +39,22 @@ namespace upnp { std::string description; ///< Human-readable UPnP lease description advertised to the gateway. }; - static std::string_view status_string(int status) { + static std::string_view status_string(igd_status_e status) { + using enum igd_status_e; + switch (status) { - case 0: + case none: return "No IGD device found"sv; - case 1: + case connected: 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; +#if (MINIUPNPC_API_VERSION >= 18) + case private_ip: + return "Valid IGD device found, but it has a private IP address"sv; +#endif + case disconnected: + return "Valid IGD device found, but it isn't connected"sv; + case unknown_device: + return "A UPnP device has been found, but it wasn't recognized as an IGD"sv; } return "Unknown status"sv; @@ -58,12 +64,28 @@ namespace upnp { * 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) { + igd_status_e 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(), (int) lan_addr.size(), nullptr, 0); + auto status = UPNP_GetValidIGD(device.get(), &urls->el, data, lan_addr.data(), (int) lan_addr.size(), nullptr, 0); #else - return UPNP_GetValidIGD(device.get(), &urls->el, data, lan_addr.data(), (int) lan_addr.size()); + auto status = UPNP_GetValidIGD(device.get(), &urls->el, data, lan_addr.data(), (int) lan_addr.size()); #endif + return static_cast(status); + } + + bool is_valid_igd(igd_status_e status) { + using enum igd_status_e; + + switch (status) { + case connected: +#if (MINIUPNPC_API_VERSION >= 18) + case private_ip: +#endif + case disconnected: + return true; + default: + return false; + } } /** @@ -122,7 +144,7 @@ namespace upnp { urls_t urls; std::array lan_addr; auto status = upnp::UPNP_GetValidIGDStatus(device, &urls, &data, lan_addr); - if (status != 1 && status != 2) { + if (!is_valid_igd(status)) { BOOST_LOG(debug) << "No valid IPv6 IGD: "sv << status_string(status); return false; } @@ -336,7 +358,7 @@ namespace upnp { urls_t urls; auto status = upnp::UPNP_GetValidIGDStatus(device, &urls, &data, lan_addr); - if (status != 1 && status != 2) { + if (!is_valid_igd(status)) { BOOST_LOG(error) << status_string(status); mapped = false; continue; diff --git a/src/upnp.h b/src/upnp.h index f34b90fa63a..1f68f48a3d5 100644 --- a/src/upnp.h +++ b/src/upnp.h @@ -29,19 +29,48 @@ namespace upnp { FreeUPNPUrls(&el); }); ///< Alias for element type. + /** + * @brief Result codes returned by the installed MiniUPnPc library. + * @details Project-owned equivalent of the UPNP_GetValidIGD() result codes. MiniUPnPc 2.2.8 + * (API version 18) introduced the private-address result, renumbering the values that follow it; + * the library's own UPNP_* symbolic names only exist starting with MiniUPnPc 2.3.0 (API version 19), + * so this enum centralizes the version-dependent numbering instead of relying on those names. + */ + enum class igd_status_e : int { + none = 0, ///< No IGD was found. + connected = 1, ///< A connected IGD was found. +#if (MINIUPNPC_API_VERSION >= 18) + private_ip = 2, ///< A connected IGD with a reserved address was found. +#endif + disconnected, ///< A disconnected IGD was found. + unknown_device, ///< The device was not recognized as an IGD. + }; + + /** + * @brief Check whether a UPNP_GetValidIGD() status code identifies a usable IGD. + * @details Accepts every status code that identifies a valid IGD (connected, private-IP, or + * disconnected). The private_ip member only exists with MiniUPnPc >= 2.2.8 (API version 18); + * disconnected is 2 before API 18 and 3 starting with API 18, and unknown_device is 3 or 4 + * respectively. Callers don't need to handle the version difference themselves. + * @param status The status returned by UPNP_GetValidIGDStatus(), converted to igd_status_e. + * @return `true` if the status identifies a usable IGD, `false` otherwise. + */ + [[nodiscard]] bool is_valid_igd(igd_status_e status); + /** * @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. + * @return The IGD status as an igd_status_e value, converted from the raw UPNP_GetValidIGD() result. + * @retval igd_status_e::none No IGD found. + * @retval igd_status_e::connected A valid connected IGD has been found. + * @retval igd_status_e::private_ip A valid connected IGD with a private IP address (MiniUPnPc >= 2.2.8 only). + * @retval igd_status_e::disconnected A valid IGD has been found but it reported as not connected. + * @retval igd_status_e::unknown_device 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); + igd_status_e UPNP_GetValidIGDStatus(device_t &device, urls_t *urls, IGDdatas *data, std::array &lan_addr); /** * @brief Start UPnP port mapping and return its shutdown guard. diff --git a/src/utility.h b/src/utility.h index cf7e7a567c1..d9a4dfa2d98 100644 --- a/src/utility.h +++ b/src/utility.h @@ -41,7 +41,7 @@ struct argument_type { /** * @brief Type extracted from the matched function signature. */ - typedef U type; + using type = U; }; /** @@ -330,11 +330,11 @@ namespace util { FailGuard() = delete; /** - * @brief Construct a fail guard that owns the cleanup callback. + * @brief Explicitly construct a fail guard that owns the cleanup callback. * * @param f Callable executed by the helper. */ - FailGuard(T &&f) noexcept: + explicit FailGuard(T &&f) noexcept: _func {std::forward(f)} { } @@ -344,9 +344,8 @@ namespace util { * @param other Source object whose state is copied or moved into this object. */ FailGuard(FailGuard &&other) noexcept: + failure {other.failure}, _func {std::move(other._func)} { - this->failure = other.failure; - other.failure = false; } @@ -413,7 +412,7 @@ namespace util { /** * @brief Integral type used for one formatted hex element. */ - typedef T elem_type; + using elem_type = T; private: const std::array _bits { @@ -439,23 +438,23 @@ namespace util { public: /** - * @brief Construct a hexadecimal byte view for the supplied value. + * @brief Format the supplied object's byte representation as hexadecimal text. * * @param elem Element value being serialized as bytes. * @param rev Whether bytes should be emitted in reverse order. */ Hex(const elem_type &elem, bool rev) { if (!rev) { - const uint8_t *data = reinterpret_cast(&elem) + sizeof(elem_type) - 1; + const auto *data = reinterpret_cast(&elem) + sizeof(elem_type) - 1; for (auto it = begin(); it < cend();) { - *it++ = _bits[*data / 16]; - *it++ = _bits[*data-- % 16]; + *it++ = _bits[std::to_integer(*data) / 16]; + *it++ = _bits[std::to_integer(*data--) % 16]; } } else { - const uint8_t *data = reinterpret_cast(&elem); + const auto *data = reinterpret_cast(&elem); for (auto it = begin(); it < cend();) { - *it++ = _bits[*data / 16]; - *it++ = _bits[*data++ % 16]; + *it++ = _bits[std::to_integer(*data) / 16]; + *it++ = _bits[std::to_integer(*data++) % 16]; } } } @@ -623,7 +622,9 @@ namespace util { } /** - * @brief Convert from hex. + * @brief Decode hexadecimal text into an object's byte representation. + * + * Digits beyond the size of the object are ignored, keeping the low-order bytes. * * @param hex Hexadecimal text to decode. * @param rev Whether bytes should be emitted in reverse order. @@ -631,62 +632,55 @@ namespace util { */ template T from_hex(const std::string_view &hex, bool rev = false) { - std::array buf; - - static char constexpr shift_bit = 'a' - 'A'; + std::array buf; - auto is_convertable = [](char ch) -> bool { + auto is_convertable = [](char ch) { if (isdigit(ch)) { return true; } - ch |= shift_bit; - - if ('a' > ch || ch > 'z') { - return false; - } - - return true; + return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z'); }; - auto buf_size = std::count_if(std::begin(hex), std::end(hex), is_convertable) / 2; + auto buf_size = std::min(std::ranges::count_if(hex, is_convertable) / 2, sizeof(T)); auto padding = sizeof(T) - buf_size; const char *data = hex.data() + hex.size() - 1; - auto convert = [](char ch) -> std::uint8_t { + auto convert = [](char ch) { if (ch >= '0' && ch <= '9') { - return (std::uint8_t) ch - '0'; + return static_cast(ch - '0'); } - return (std::uint8_t) (ch | (char) 32) - 'a' + (char) 10; + const auto lowercase = std::byte {static_cast(ch)} | std::byte {'a' - 'A'}; + return static_cast(std::to_integer(lowercase) - 'a' + 10); }; - std::fill_n(buf.begin() + buf_size, padding, 0); + std::fill_n(buf.begin() + buf_size, padding, std::byte {0}); std::for_each_n(buf.begin(), buf_size, [&](auto &el) { while (!is_convertable(*data)) { --data; } - std::uint8_t ch_r = convert(*data--); + const auto ch_r = convert(*data--); while (!is_convertable(*data)) { --data; } - std::uint8_t ch_l = convert(*data--); + const auto ch_l = convert(*data--); el = (ch_l << 4) | ch_r; }); if (rev) { - std::reverse(std::begin(buf), std::end(buf)); + std::ranges::reverse(buf); } return std::bit_cast(buf); } /** - * @brief Convert from hex vec. + * @brief Decode hexadecimal text into a string containing the decoded bytes. * * @param hex Hexadecimal text to decode. * @param rev Whether bytes should be emitted in reverse order. @@ -695,50 +689,44 @@ namespace util { 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 { + auto is_convertable = [](char ch) { if (isdigit(ch)) { return true; } - ch |= shift_bit; - - if ('a' > ch || ch > 'z') { - return false; - } - - return true; + return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z'); }; - auto buf_size = std::count_if(std::begin(hex), std::end(hex), is_convertable) / 2; + auto buf_size = std::ranges::count_if(hex, is_convertable) / 2; buf.resize(buf_size); const char *data = hex.data() + hex.size() - 1; - auto convert = [](char ch) -> std::uint8_t { + auto convert = [](char ch) { if (ch >= '0' && ch <= '9') { - return (std::uint8_t) ch - '0'; + return static_cast(ch - '0'); } - return (std::uint8_t) (ch | (char) 32) - 'a' + (char) 10; + const auto lowercase = std::byte {static_cast(ch)} | std::byte {'a' - 'A'}; + return static_cast(std::to_integer(lowercase) - 'a' + 10); }; for (auto &el : buf) { while (!is_convertable(*data)) { --data; } - std::uint8_t ch_r = convert(*data--); + const auto ch_r = convert(*data--); while (!is_convertable(*data)) { --data; } - std::uint8_t ch_l = convert(*data--); + const auto ch_l = convert(*data--); - el = (ch_l << 4) | ch_r; + el = std::to_integer((ch_l << 4) | ch_r); } if (rev) { - std::reverse(std::begin(buf), std::end(buf)); + std::ranges::reverse(buf); } return buf; @@ -756,15 +744,14 @@ namespace util { using value_type = T; /** - * @brief Hash a value by viewing its object representation as bytes. + * @brief Hash a copy of the object's representation using the standard string-view hash. * * @param value Value whose raw bytes are hashed. * @return Hash value for the byte representation. */ 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)}); + const auto bytes = std::bit_cast>(value); + return std::hash {}(std::string_view {bytes.data(), bytes.size()}); } }; @@ -900,24 +887,28 @@ namespace util { /** * @brief Pointer type stored by the unique pointer wrapper. */ - using pointer = element_type *; + using pointer = element_type *; // NOSONAR(cpp:S5008): Native C handle instantiations must retain HANDLE, PSID, EGLDisplay and VADisplay pointer types. /** * @brief Const pointer type exposed by the unique pointer wrapper. */ - using const_pointer = element_type const *; + using const_pointer = element_type const *; // NOSONAR(cpp:S5008): Native C handle instantiations must retain HANDLE, PSID, EGLDisplay and VADisplay pointer types. /** * @brief Callable type used to release the managed pointer. */ using deleter_type = D; - constexpr uniq_ptr() noexcept: - _p {nullptr} { - } + /** + * @brief Initialize an empty unique ownership wrapper. + */ + constexpr uniq_ptr() noexcept = default; /** - * @brief Construct a unique ownership wrapper. + * @brief Construct an empty wrapper implicitly from nullptr, matching std::unique_ptr. */ - constexpr uniq_ptr(std::nullptr_t) noexcept: +#ifndef DOXYGEN + explicit(false) +#endif + constexpr uniq_ptr(std::nullptr_t) noexcept: _p {nullptr} { } @@ -925,34 +916,40 @@ namespace util { uniq_ptr &operator=(const uniq_ptr &other) noexcept = delete; /** - * @brief Construct a unique ownership wrapper. + * @brief Explicitly construct a unique ownership wrapper. * * @param p Pointer passed to the deleter or conversion helper. */ template - uniq_ptr(V *p) noexcept: + explicit 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"); } /** - * @brief Construct a unique ownership wrapper. + * @brief Transfer compatible standard unique-pointer ownership implicitly. * * @param uniq Unique pointer whose owned value is transferred. */ template - uniq_ptr(std::unique_ptr &&uniq) noexcept: +#ifndef DOXYGEN + explicit(false) +#endif + 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"); } /** - * @brief Construct a unique ownership wrapper. + * @brief Transfer compatible Sunshine unique-pointer ownership implicitly. * * @param other Source object whose state is copied or moved into this object. */ template - uniq_ptr(uniq_ptr &&other) noexcept: +#ifndef DOXYGEN + explicit(false) +#endif + 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"); } @@ -1112,7 +1109,7 @@ namespace util { } protected: - pointer _p; ///< Pointer currently owned by the wrapper. + pointer _p {nullptr}; ///< Pointer currently owned by the wrapper, initially empty. deleter_type _deleter; ///< Callable used to release `_p`. }; @@ -1282,21 +1279,21 @@ namespace util { } /** - * @brief Construct an owning or non-owning wrapper around a raw pointer. + * @brief Explicitly construct an owning or non-owning wrapper around a raw pointer. * * @param p Pointer passed to the deleter or conversion helper. */ - wrap_ptr(pointer p): + explicit wrap_ptr(pointer p): _own_ptr {false}, _p {p} { } /** - * @brief Construct an owning or non-owning wrapper around a raw pointer. + * @brief Explicitly construct an owning or non-owning wrapper around a raw pointer. * * @param uniq_p Uniq p. */ - wrap_ptr(std::unique_ptr &&uniq_p): + explicit wrap_ptr(std::unique_ptr &&uniq_p): _own_ptr {true}, _p {uniq_p.release()} { } @@ -1429,15 +1426,17 @@ namespace util { * @brief Helper specialization for optional values. */ template - struct __false_v>> { + requires instantiation_of_v + struct __false_v { static constexpr std::nullopt_t value = std::nullopt; ///< Value. }; /** - * @brief Enables an overload only when the provided type is a pointer. + * @brief Select a null sentinel for raw and supported smart pointer types. */ template - struct __false_v>> { + requires is_pointer_v + struct __false_v { static constexpr std::nullptr_t value = nullptr; ///< Value. }; @@ -1445,7 +1444,8 @@ namespace util { * @brief Type trait comparing a value with a boolean template argument. */ template - struct __false_v>> { + requires std::is_same_v + struct __false_v { static constexpr bool value = false; ///< Trait value for the false specialization. }; @@ -1482,14 +1482,14 @@ namespace util { } /** - * @brief Construct an owning contiguous buffer. + * @brief Copy the source buffer's logical range into an independent allocation. * * @param o Source object used for comparison or assignment. */ buffer_t(const buffer_t &o): _els {o._els}, _buf(_els) { - std::copy(o.begin(), o.end(), begin()); + std::ranges::copy(o, begin()); } /** @@ -1628,9 +1628,9 @@ namespace util { template struct Function { /** - * @brief Type extracted from the matched function signature. + * @brief Pointer type for a function with the specified return and argument types. */ - typedef ReturnType (*type)(Args...); + using type = ReturnType (*)(Args...); }; /** @@ -1641,14 +1641,14 @@ namespace util { /** * @brief Pointer type accepted by this deleter. */ - typedef T pointer; + using pointer = T; // NOSONAR(cpp:S5008): Native C handle instantiations must retain HANDLE, PSID, EGLDisplay and VADisplay pointer types. /** * @brief Invoke the configured destroy function on a pointer. * * @param p Pointer to release. */ - void operator()(pointer p) { + void operator()(pointer p) const { function(p); } }; @@ -1776,10 +1776,11 @@ namespace util { struct endian_helper {}; /** - * @brief Helper specialization for optional values. + * @brief Convert non-optional values between native and requested byte order. */ template - struct endian_helper)>> { + requires(!instantiation_of_v) + struct endian_helper { /** * @brief Convert a value to or from big-endian byte order. * @@ -1788,7 +1789,7 @@ namespace util { */ static inline T big(T x) { if constexpr (endianness::little) { - uint8_t *data = reinterpret_cast(&x); + auto *data = reinterpret_cast(&x); std::reverse(data, data + sizeof(x)); } @@ -1804,7 +1805,7 @@ namespace util { */ static inline T little(T x) { if constexpr (endianness::big) { - uint8_t *data = reinterpret_cast(&x); + auto *data = reinterpret_cast(&x); std::reverse(data, data + sizeof(x)); } @@ -1817,7 +1818,8 @@ namespace util { * @brief Helper specialization for optional values. */ template - struct endian_helper>> { + requires instantiation_of_v + struct endian_helper { /** * @brief Convert a value to or from little-endian byte order. * @@ -1830,7 +1832,7 @@ namespace util { } if constexpr (endianness::big) { - auto *data = reinterpret_cast(&*x); + auto *data = reinterpret_cast(&*x); std::reverse(data, data + sizeof(*x)); } @@ -1850,7 +1852,7 @@ namespace util { } if constexpr (endianness::little) { - auto *data = reinterpret_cast(&*x); + auto *data = reinterpret_cast(&*x); std::reverse(data, data + sizeof(*x)); } diff --git a/src/uuid.h b/src/uuid.h index b1c4ed900ea..1467dccd5ba 100644 --- a/src/uuid.h +++ b/src/uuid.h @@ -6,6 +6,7 @@ // standard includes #include +#include #include /** @@ -22,7 +23,7 @@ namespace uuid_util { std::array b64; ///< UUID viewed as 64-bit words. /** - * @brief Generate a UUID value. + * @brief Generate UUID bytes and apply the established version and variant masks. * * @param engine Random-number engine used to generate UUID bytes. * @return Random UUID generated from the supplied engine. @@ -35,8 +36,8 @@ namespace uuid_util { el = dist(engine); } - buf.b8[7] &= (std::uint8_t) 0b00101111; - buf.b8[9] &= (std::uint8_t) 0b10011111; + buf.b8[7] = std::to_integer(std::byte {buf.b8[7]} & std::byte {0b00101111}); + buf.b8[9] = std::to_integer(std::byte {buf.b8[9]} & std::byte {0b10011111}); return buf; } @@ -76,12 +77,12 @@ namespace uuid_util { auto last_slice = hex_view.substr(20, 12); for (auto &slice : slices) { - std::copy(std::begin(slice), std::end(slice), std::back_inserter(result)); + std::ranges::copy(slice, std::back_inserter(result)); result.push_back('-'); } - std::copy(std::begin(last_slice), std::end(last_slice), std::back_inserter(result)); + std::ranges::copy(last_slice, std::back_inserter(result)); return result; } diff --git a/src/video.cpp b/src/video.cpp index 526cbc24614..df34db693ef 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -286,10 +286,10 @@ namespace video { // 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) { + auto transfer_status = av_hwframe_transfer_data(frame, sw_frame.get(), 0); + if (transfer_status < 0) { std::array string; - BOOST_LOG(error) << "Failed to transfer image data to hardware frame: "sv << av_make_error_string(string.data(), AV_ERROR_MAX_STRING_SIZE, status); + BOOST_LOG(error) << "Failed to transfer image data to hardware frame: "sv << av_make_error_string(string.data(), AV_ERROR_MAX_STRING_SIZE, transfer_status); return -1; } } @@ -327,9 +327,9 @@ namespace video { av_image_fill_black(active_frame->data, linesize.data(), static_cast(active_frame->format), active_frame->color_range, active_frame->width, active_frame->height); } - int avcodec_software_encode_device_t::init(int in_width, int in_height, AVFrame *in_frame, AVPixelFormat format, bool hardware) { + int avcodec_software_encode_device_t::init(int in_width, int in_height, AVFrame *in_frame, AVPixelFormat format, bool hardware_frame) { // If the device used is hardware, yet the image resides on main memory - if (hardware) { + if (hardware_frame) { sw_frame.reset(av_frame_alloc()); sw_frame->width = in_frame->width; @@ -546,11 +546,11 @@ namespace video { class nvenc_encode_session_t: public encode_session_t { public: /** - * @brief Initialize an NVENC encode session and take ownership of its device. + * @brief Explicitly initialize an NVENC encode session and take ownership of its device. * * @param encode_device Encode device. */ - nvenc_encode_session_t(std::unique_ptr encode_device): + explicit nvenc_encode_session_t(std::unique_ptr encode_device): device(std::move(encode_device)) { } @@ -631,7 +631,7 @@ namespace video { config_t config; ///< Stream or encoder configuration captured for the worker. int frame_nr; ///< Next capture-frame number assigned to encoded packets. - void *channel_data; ///< Platform-specific channel data forwarded to packet senders. + stream::session_t *channel_data; ///< Stream session forwarded to packet senders. }; /** @@ -681,14 +681,14 @@ namespace video { /** * @brief Start the synchronous multi-client capture thread. * - * @param ctx Native context object used by the operation or callback. + * @param ctx Unused state required by the shared capture lifetime callback signature. * @return 0 when the capture thread is started. */ int start_capture_sync(capture_thread_sync_ctx_t &ctx); /** - * @brief Stop capture sync processing. + * @brief Leave synchronous capture cleanup to the capture thread. * - * @param ctx Native context object used by the operation or callback. + * @param ctx Unused state required by the shared capture lifetime callback signature. */ void end_capture_sync(capture_thread_sync_ctx_t &ctx); /** @@ -1034,9 +1034,8 @@ namespace video { {"enforce_hrd"s, &config::video.amd.amd_enforce_hrd}, {"max_au_size"s, &config::video.amd.amd_max_au_size}, {"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 (auto size = cfg.width * cfg.height; size <= 8912896) { if (size * cfg.framerate <= 534773760) { return "5.1"s; } else if (size * cfg.framerate <= 1069547520) { @@ -1553,6 +1552,7 @@ namespace video { /** * @brief Run the shared display capture thread for asynchronous encoding. + * @details Capture callbacks share this thread's session queues and reusable image pool. * * @param capture_ctx_queue Capture context queue. * @param display_wp Weak pointer holder for the active display. @@ -1567,7 +1567,7 @@ namespace video { ) { std::vector capture_ctxs; - auto fg = util::fail_guard([&]() { + auto fg = util::fail_guard([&capture_ctx_queue, &capture_ctxs]() { capture_ctx_queue->stop(); // Stop all sessions listening to this thread @@ -1604,7 +1604,7 @@ namespace video { std::vector> imgs_used_timestamps; const std::chrono::seconds trim_timeot = 3s; - auto trim_imgs = [&]() { + auto trim_imgs = [&imgs, &imgs_used_timestamps, &trim_timeot]() { // count allocated and used within current pool size_t allocated_count = 0; size_t used_count = 0; @@ -1652,7 +1652,7 @@ namespace video { } }; - auto pull_free_image_callback = [&](std::shared_ptr &img_out) -> bool { + auto pull_free_image_callback = [&capture_ctx_queue, &imgs, &disp, &trim_imgs](std::shared_ptr &img_out) { img_out.reset(); while (capture_ctx_queue->running()) { // pick first allocated but unused @@ -1703,7 +1703,7 @@ namespace video { while (capture_ctx_queue->running()) { bool artificial_reinit = false; - auto push_captured_image_callback = [&](std::shared_ptr &&img, bool frame_captured) -> bool { + auto push_captured_image_callback = [&capture_ctxs, &capture_ctx_queue, &switch_display_event, &artificial_reinit](std::shared_ptr &&img, bool frame_captured) { 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); @@ -1736,7 +1736,7 @@ namespace video { auto status = disp->capture(push_captured_image_callback, pull_free_image_callback, &display_cursor); - if (artificial_reinit && status != platf::capture_e::error) { + if (artificial_reinit && status != platf::capture_e::capture_error) { status = platf::capture_e::reinit; artificial_reinit = false; @@ -1803,7 +1803,7 @@ namespace video { reinit_event.reset(); continue; } - case platf::capture_e::error: + case platf::capture_e::capture_error: case platf::capture_e::ok: case platf::capture_e::timeout: case platf::capture_e::interrupted: @@ -1821,11 +1821,11 @@ namespace video { * @param frame_nr Monotonic frame index assigned by the video pipeline. * @param session Active FFmpeg encoder session. * @param packets Output queue that receives encoded packets. - * @param channel_data Platform or protocol state attached to each packet. + * @param channel_data Stream session that receives each encoded packet. * @param frame_timestamp Capture timestamp associated with the encoded frame. * @return 0 when packets are queued; nonzero when encoding or packetization fails. */ - 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) { + int encode_avcodec(int64_t frame_nr, avcodec_encode_session_t &session, safe::mail_raw_t::queue_t &packets, stream::session_t *channel_data, std::optional frame_timestamp) { auto &frame = session.device->frame; frame->pts = frame_nr; @@ -1905,11 +1905,11 @@ namespace video { * @param frame_nr Monotonic frame index assigned by the video pipeline. * @param session Active NVENC encoder session. * @param packets Output queue that receives the encoded packet. - * @param channel_data Platform or protocol state attached to the packet. + * @param channel_data Stream session that receives the encoded packet. * @param frame_timestamp Capture timestamp associated with the encoded frame. * @return 0 when packets are queued; nonzero when NVENC encoding fails. */ - 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) { + int encode_nvenc(int64_t frame_nr, nvenc_encode_session_t &session, safe::mail_raw_t::queue_t &packets, stream::session_t *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"; @@ -1939,7 +1939,7 @@ namespace video { * @param frame_timestamp Frame timestamp. * @return 0 when the frame is encoded and queued; nonzero on encoder failure. */ - int encode(int64_t frame_nr, encode_session_t &session, safe::mail_raw_t::queue_t &packets, void *channel_data, std::optional frame_timestamp) { + int encode(int64_t frame_nr, encode_session_t &session, safe::mail_raw_t::queue_t &packets, stream::session_t *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)) { @@ -2165,29 +2165,29 @@ namespace video { auto handle_option = [&options, &config](const encoder_t::option_t &option) { std::visit( util::overloaded { - [&](int v) { + [&options, &option](int v) { av_dict_set_int(&options, option.name.c_str(), v, 0); }, - [&](int *v) { + [&options, &option](int *v) { av_dict_set_int(&options, option.name.c_str(), *v, 0); }, - [&](std::optional *v) { - if (*v) { + [&options, &option](std::optional *v) { + if (v->has_value()) { av_dict_set_int(&options, option.name.c_str(), **v, 0); } }, - [&](const std::function &v) { + [&options, &option](const std::function &v) { av_dict_set_int(&options, option.name.c_str(), v(), 0); }, - [&](const std::string &v) { + [&options, &option](const std::string &v) { av_dict_set(&options, option.name.c_str(), v.c_str(), 0); }, - [&](std::string *v) { + [&options, &option](std::string *v) { if (!v->empty()) { av_dict_set(&options, option.name.c_str(), v->c_str(), 0); } }, - [&](const std::function &v) { + [&options, &option, &config](const std::function &v) { av_dict_set(&options, option.name.c_str(), v(config).c_str(), 0); } }, @@ -2318,7 +2318,7 @@ namespace video { std::unique_ptr encode_device_final; - if (!encode_device->data) { + if (!encode_device->hardware) { auto software_encode_device = std::make_unique(); if (software_encode_device->init(width, height, frame.get(), sw_fmt, hardware)) { @@ -2397,7 +2397,7 @@ namespace video { * @param encode_device Platform encode device. * @param reinit_event Signal raised while the encoder/display is reinitializing. * @param encoder Selected encoder. - * @param channel_data Opaque channel data passed to packets. + * @param channel_data Stream session carried with each encoded packet. */ void encode_run( int &frame_nr, // Store progress of the frame number @@ -2408,7 +2408,7 @@ namespace video { std::unique_ptr encode_device, safe::signal_t &reinit_event, const encoder_t &encoder, - void *channel_data + stream::session_t *channel_data ) { auto session = make_encode_session(disp.get(), encoder, config, disp->width, disp->height, std::move(encode_device)); if (!session) { @@ -2645,7 +2645,7 @@ namespace video { 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); + auto 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; @@ -2718,19 +2718,19 @@ namespace video { } if (!disp) { - return encode_e::error; + return encode_e::capture_error; } auto img = disp->alloc_img(); if (!img || disp->dummy_img(img.get())) { - return encode_e::error; + return encode_e::capture_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; + return encode_e::capture_error; } synced_sessions.emplace_back(std::move(*synced_session)); @@ -2738,7 +2738,7 @@ namespace video { 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 { + auto push_captured_image_callback = [&](std::shared_ptr &&captured_image, bool frame_captured) { while (encode_session_ctx_queue.peek()) { auto encode_session_ctx = encode_session_ctx_queue.pop(); if (!encode_session_ctx) { @@ -2747,9 +2747,9 @@ namespace video { 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()); + auto encode_session = make_synced_session(disp.get(), encoder, *captured_image, *synced_session_ctxs.back()); if (!encode_session) { - ec = platf::capture_e::error; + ec = platf::capture_e::capture_error; return false; } @@ -2779,7 +2779,7 @@ namespace video { ctx->idr_events->pop(); } - if (frame_captured && pos->session->convert(*img)) { + if (frame_captured && pos->session->convert(*captured_image)) { BOOST_LOG(error) << "Could not convert image"sv; ctx->shutdown_event->raise(true); @@ -2787,8 +2787,8 @@ namespace video { } std::optional frame_timestamp; - if (img) { - frame_timestamp = img->frame_timestamp; + if (captured_image) { + frame_timestamp = captured_image->frame_timestamp; } if (encode(ctx->frame_nr++, *pos->session, ctx->packets, ctx->channel_data, frame_timestamp)) { @@ -2811,7 +2811,7 @@ namespace video { return true; }; - auto pull_free_image_callback = [&img](std::shared_ptr &img_out) -> bool { + auto pull_free_image_callback = [&img](std::shared_ptr &img_out) { img_out = img; img_out->frame_timestamp.reset(); return true; @@ -2820,7 +2820,7 @@ namespace video { 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::capture_error: case platf::capture_e::ok: case platf::capture_e::timeout: case platf::capture_e::interrupted: @@ -2840,17 +2840,17 @@ namespace video { std::vector> synced_session_ctxs; auto &ctx = ref->encode_session_ctx_queue; - auto lg = util::fail_guard([&]() { + auto lg = util::fail_guard([&ctx, &synced_session_ctxs]() { ctx.stop(); - for (auto &ctx : synced_session_ctxs) { - ctx->shutdown_event->raise(true); - ctx->join_event->raise(true); + for (const auto &session_ctx : synced_session_ctxs) { + session_ctx->shutdown_event->raise(true); + session_ctx->join_event->raise(true); } - for (auto &ctx : ctx.unsafe()) { - ctx.shutdown_event->raise(true); - ctx.join_event->raise(true); + for (auto &pending_ctx : ctx.unsafe()) { + pending_ctx.shutdown_event->raise(true); + pending_ctx.join_event->raise(true); } }); @@ -2868,17 +2868,17 @@ namespace video { * * @param mail Session mail bus. * @param config Video configuration. - * @param channel_data Opaque channel data passed to packets. + * @param channel_data Stream session carried with each encoded packet. */ void capture_async( safe::mail_t mail, config_t &config, - void *channel_data + stream::session_t *channel_data ) { auto shutdown_event = mail->event(mail::shutdown); auto images = std::make_shared(); - auto lg = util::fail_guard([&]() { + auto lg = util::fail_guard([&images, &shutdown_event]() { images->stop(); shutdown_event->raise(true); }); @@ -2911,7 +2911,7 @@ namespace video { // Wait for the display to be ready std::shared_ptr display; { - auto lg = ref->display_wp.lock(); + auto display_lock = ref->display_wp.lock(); if (ref->display_wp->expired()) { continue; } @@ -2930,7 +2930,7 @@ namespace video { 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); + auto 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; @@ -2959,12 +2959,12 @@ namespace video { * * @param mail Session mail bus. * @param config Client-requested video configuration, normalized before capture begins. - * @param channel_data Opaque channel data passed to packets. + * @param channel_data Stream session carried with each encoded packet. */ void capture( safe::mail_t mail, config_t config, - void *channel_data + stream::session_t *channel_data ) { config = resolve_dynamic_range(*chosen_encoder, config); @@ -3503,12 +3503,6 @@ namespace video { return 0; } - // Linux only declaration - /** - * @brief Callback signature for VA-API AVCodec hardware input initialization. - */ - typedef int (*vaapi_init_avcodec_hardware_input_buffer_fn)(platf::avcodec_encode_device_t *encode_device, AVBufferRef **hw_device_buf); - /** * @brief Initialize AVCodec hardware input buffers for VA-API. */ @@ -3516,8 +3510,8 @@ namespace video { 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)) { + if (encode_device->init_hw_device) { + if (encode_device->init_hw_device(encode_device, &hw_device_buf)) { return -1; } @@ -3537,13 +3531,11 @@ namespace video { } #ifdef SUNSHINE_BUILD_VULKAN - using vulkan_init_avcodec_hardware_input_buffer_fn = int (*)(platf::avcodec_encode_device_t *encode_device, AVBufferRef **hw_device_buf); - util::Either vulkan_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) { avcodec_buffer_t hw_device_buf; - if (encode_device && encode_device->data) { - if (((vulkan_init_avcodec_hardware_input_buffer_fn) encode_device->data)(encode_device, &hw_device_buf)) { + if (encode_device && encode_device->init_hw_device) { + if (encode_device->init_hw_device(encode_device, &hw_device_buf)) { return -1; } return hw_device_buf; @@ -3611,7 +3603,7 @@ namespace video { /** * @brief No-op lock callback used when FFmpeg requires a D3D11VA lock function. */ -void do_nothing(void *) { +void do_nothing(void *) { // NOSONAR(cpp:S5008): FFmpeg's D3D11VA lock callbacks require a void pointer for user data. } namespace video { @@ -3624,7 +3616,7 @@ namespace video { std::fill_n((std::uint8_t *) ctx, sizeof(AVD3D11VADeviceContext), 0); - auto device = static_cast(encode_device->data); + auto device = encode_device->d3d11_device; device->AddRef(); ctx->device = device; @@ -3676,16 +3668,18 @@ namespace video { /** * @brief Start capture sync. + * @details The shared lifetime callback supplies unused state; the thread accesses the shared capture queue directly. */ - int start_capture_sync(capture_thread_sync_ctx_t &ctx) { + int start_capture_sync(capture_thread_sync_ctx_t &) { std::jthread {&captureThreadSync}.detach(); return 0; } /** - * @brief Stop capture sync processing. + * @brief Leave synchronous capture cleanup to the capture thread. */ - void end_capture_sync(capture_thread_sync_ctx_t &ctx) { + void end_capture_sync(capture_thread_sync_ctx_t &) { + // The capture thread stops its queue and signals its sessions when it exits. } /** diff --git a/src/video.h b/src/video.h index 63f3cf55cbf..a6073b0c48e 100644 --- a/src/video.h +++ b/src/video.h @@ -21,6 +21,10 @@ extern "C" { struct AVPacket; +namespace stream { + struct session_t; +} + namespace video { /** @@ -99,21 +103,21 @@ namespace video { void free_buffer(AVBufferRef *ref); /** - * @brief Owning pointer for an FFmpeg codec context. + * @brief Owning pointer for an FFmpeg codec context released with free_ctx(). */ - using avcodec_ctx_t = util::safe_ptr; + using avcodec_ctx_t = util::safe_ptr; /** - * @brief Owning pointer for an FFmpeg frame. + * @brief Owning pointer for an FFmpeg frame released with free_frame(). */ - using avcodec_frame_t = util::safe_ptr; + using avcodec_frame_t = util::safe_ptr; /** - * @brief Owning pointer for an FFmpeg buffer reference. + * @brief Owning pointer for an FFmpeg buffer reference released with free_buffer(). */ - using avcodec_buffer_t = util::safe_ptr; + using avcodec_buffer_t = util::safe_ptr; /** - * @brief Owning pointer for an FFmpeg software-scaling context. + * @brief Owning pointer for an FFmpeg software-scaling context released with sws_freeContext(). */ - using sws_t = util::safe_ptr; + using sws_t = util::safe_ptr; /** * @brief Shared event that transports captured images between capture and encode threads. */ @@ -263,24 +267,24 @@ namespace video { /** * @brief Construct NVENC platform format mappings. * - * @param dev_type Platform memory type. - * @param pix_fmt_8bit Platform 8-bit pixel format. - * @param pix_fmt_10bit Platform 10-bit pixel format. - * @param pix_fmt_yuv444_8bit Platform 8-bit YUV444 pixel format. - * @param pix_fmt_yuv444_10bit Platform 10-bit YUV444 pixel format. + * @param device_type Platform memory type. + * @param format_8bit Platform 8-bit pixel format. + * @param format_10bit Platform 10-bit pixel format. + * @param format_yuv444_8bit Platform 8-bit YUV444 pixel format. + * @param format_yuv444_10bit Platform 10-bit YUV444 pixel format. */ 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 + const platf::mem_type_e &device_type, + const platf::pix_fmt_e &format_8bit, + const platf::pix_fmt_e &format_10bit, + const platf::pix_fmt_e &format_yuv444_8bit, + const platf::pix_fmt_e &format_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; + encoder_platform_formats_t::dev_type = device_type; + encoder_platform_formats_t::pix_fmt_8bit = format_8bit; + encoder_platform_formats_t::pix_fmt_10bit = format_10bit; + encoder_platform_formats_t::pix_fmt_yuv444_8bit = format_yuv444_8bit; + encoder_platform_formats_t::pix_fmt_yuv444_10bit = format_yuv444_10bit; } }; @@ -324,7 +328,9 @@ namespace video { _CONVERT(VUI_PARAMETERS); _CONVERT(MAX_FLAGS); } -#undef _CONVERT +#ifndef DOXYGEN + #undef _CONVERT +#endif return {"unknown"}; } @@ -516,19 +522,19 @@ namespace video { KITTY_DEFAULT_CONSTR_MOVE(replace_t) /** - * @brief Store an encoded-byte replacement rule. + * @brief Copy the non-owning byte views for an encoded-byte replacement rule. * * @param old Byte sequence to replace in encoded packets. * @param _new Replacement byte sequence inserted into encoded packets. */ replace_t(std::string_view old, std::string_view _new) noexcept: - old {std::move(old)}, - _new {std::move(_new)} { + old {old}, + _new {_new} { } }; std::vector *replacements = nullptr; ///< Optional encoded-byte substitutions applied before packetization. - void *channel_data = nullptr; ///< Platform or protocol state carried with this packet. + stream::session_t *channel_data = nullptr; ///< Stream session that receives this packet. bool after_ref_frame_invalidation = false; ///< Whether the frame follows reference-frame invalidation. std::optional frame_timestamp; ///< Capture timestamp associated with the frame. }; @@ -697,12 +703,12 @@ namespace video { * * @param mail Session mail bus. * @param config Client-requested video configuration, normalized before capture begins. - * @param channel_data Opaque channel data passed to packets. + * @param channel_data Stream session carried with each encoded packet. */ void capture( safe::mail_t mail, config_t config, - void *channel_data + stream::session_t *channel_data ); /** diff --git a/src/video_colorspace.cpp b/src/video_colorspace.cpp index a8f23f482be..5479a77d022 100644 --- a/src/video_colorspace.cpp +++ b/src/video_colorspace.cpp @@ -157,7 +157,7 @@ namespace video { */ const color_t *color_vectors_from_colorspace(const sunshine_colorspace_t &colorspace, bool unorm_output) { using enum colorspace_e; - constexpr auto generate_color_vectors = [](const sunshine_colorspace_t &colorspace, bool unorm_output) -> color_t { + constexpr auto generate_color_vectors = [](const sunshine_colorspace_t &colorspace, bool unorm_output) { // "Table 4 – Interpretation of matrix coefficients (MatrixCoefficients) value" section of ITU-T H.273 double Kr; double Kb; @@ -166,16 +166,16 @@ namespace video { Kr = 0.299; Kb = 0.114; break; - case rec709: - default: - Kr = 0.2126; - Kb = 0.0722; - break; case bt2020: case bt2020sdr: Kr = 0.2627; Kb = 0.0593; break; + case rec709: + default: + Kr = 0.2126; + Kb = 0.0722; + break; } double Kg = 1.0 - Kr - Kb; @@ -269,14 +269,14 @@ namespace video { case rec601: result = &colors[0]; break; - case rec709: - default: - result = &colors[4]; - break; case bt2020: case bt2020sdr: result = &colors[8]; break; + case rec709: + default: + result = &colors[4]; + break; } if (colorspace.bit_depth == 10) { diff --git a/src/webrtc/webrtc_stream.cpp b/src/webrtc/webrtc_stream.cpp index 71d288e5828..d7f4fae5388 100644 --- a/src/webrtc/webrtc_stream.cpp +++ b/src/webrtc/webrtc_stream.cpp @@ -1660,12 +1660,13 @@ namespace webrtc_stream { const auto epoch = std::chrono::steady_clock::now(); std::jthread video_capture([mail, video_config, stream] { platf::set_thread_name("webrtc::video"); - video::capture(mail, video_config, stream.get()); + // The packets come back through this stream's own queues, so no GameStream session rides with them. + video::capture(mail, video_config, nullptr); stream->request_stop("Video capture stopped"); }); std::jthread audio_capture([mail, audio_config, stream] { platf::set_thread_name("webrtc::audio"); - audio::capture(mail, audio_config, stream.get()); + audio::capture(mail, audio_config, nullptr); }); std::jthread video_sender([video_packets, stream, epoch] { platf::set_thread_name("webrtc::video_send"); diff --git a/src_assets/common/assets/web/Featured.vue b/src_assets/common/assets/web/Featured.vue index 8c2ba7f940a..4c25011dec1 100644 --- a/src_assets/common/assets/web/Featured.vue +++ b/src_assets/common/assets/web/Featured.vue @@ -8,24 +8,27 @@
-
+
{{ $t('featured.category_filter') }} - - + +
+ + +
@@ -67,7 +70,7 @@
{{ app.name }}

{{ app.tagline }}

- {{ $t('featured.official') }} + {{ $t('featured.official') }}

{{ app.description }}

@@ -117,7 +120,7 @@
- + diff --git a/src_assets/common/assets/web/Navbar.vue b/src_assets/common/assets/web/Navbar.vue index 9f2a126e685..9b8d4fe0ba1 100644 --- a/src_assets/common/assets/web/Navbar.vue +++ b/src_assets/common/assets/web/Navbar.vue @@ -122,8 +122,8 @@ export default { diff --git a/src_assets/common/assets/web/locale.js b/src_assets/common/assets/web/locale.js index ed7c5330767..1125699aec7 100644 --- a/src_assets/common/assets/web/locale.js +++ b/src_assets/common/assets/web/locale.js @@ -1,4 +1,5 @@ import {createI18n} from "vue-i18n"; +import {createParser, defaultOnError} from "@intlify/message-compiler"; // Import only the fallback language files import en from './public/assets/locale/en.json' @@ -42,7 +43,32 @@ function resolveLocale(configuredLocale) { } /** - * @brief Load messages for a supported locale with an English fallback. + * @brief Omit malformed translation messages so Vue I18n can fall back to English. + * + * @param {object} messages Nested translation messages. + * @param {string} locale Locale identifier used in diagnostics. + * @param {string} [prefix] Parent message key used in diagnostics. + * @return {object} Messages with valid Vue I18n syntax. + */ +function filterLocaleMessages(messages, locale, prefix = '') { + return Object.fromEntries(Object.entries(messages).flatMap(([key, message]) => { + const path = prefix ? `${prefix}.${key}` : key; + if (typeof message !== 'string') { + return [[key, filterLocaleMessages(message, locale, path)]]; + } + + try { + createParser({ onError: defaultOnError }).parse(message); + return [[key, message]]; + } catch (error) { + console.error('Ignoring invalid translation', locale, path, error); + return []; + } + })); +} + +/** + * @brief Load valid messages for a supported locale with an English fallback. * * @param {string} locale Supported locale identifier. * @param {Function} loadTranslation Function that loads a translation path. @@ -59,7 +85,7 @@ export async function loadLocaleMessages( const translation = await loadTranslation(localePaths.get(locale)); messages = Object.fromEntries([ ['en', en], - [locale, translation], + [locale, filterLocaleMessages(translation, locale)], ]); } } catch (e) { diff --git a/src_assets/common/assets/web/public/assets/locale/en_US.json b/src_assets/common/assets/web/public/assets/locale/en_US.json index 83a4a3c1811..416b274d9ff 100644 --- a/src_assets/common/assets/web/public/assets/locale/en_US.json +++ b/src_assets/common/assets/web/public/assets/locale/en_US.json @@ -252,7 +252,7 @@ "external_ip": "External IP", "external_ip_desc": "If no external IP address is given, Sunshine 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.", + "fec_percentage_desc": "Percentage of error correcting packets per data packet in each video frame. Set to 0 to disable forward error correction. 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 Sunshine are stored.", diff --git a/src_assets/common/assets/web/public/assets/locale/ja.json b/src_assets/common/assets/web/public/assets/locale/ja.json index c7b9d472dc9..4207e83d30a 100644 --- a/src_assets/common/assets/web/public/assets/locale/ja.json +++ b/src_assets/common/assets/web/public/assets/locale/ja.json @@ -266,7 +266,7 @@ "gamepad_driver": "Windows Gamepad ドライバ", "gamepad_driver_all": "利用可能なすべてのドライバ", "gamepad_driver_desc": "Sunshineが使用できる仮想ゲームパッドドライバを選択します。ライセンスされた場合は、すべての利用可能なドライバがVirtual HIDドライバを好み、ViGEmBusに戻ります。 Virtual HIDドライバはオプションの有料アップグレードです。ViGEmBusはXbox 360とDualShock4のみをサポートし、限られた機能を備えており、人生の終わりに達しています。", - "gamepad_driver_macos_desc": "macOSで仮想ゲームパッドのバーチャルHIDブローカーを選択するか、ゲームパッドとブローカーの通知を無効にするにはformat@@0を選択します。キーボードとマウスの入力は影響を受けません。", + "gamepad_driver_macos_desc": "macOSで仮想ゲームパッドのバーチャルHIDブローカーを選択するか、ゲームパッドとブローカーの通知を無効にするには「なし」を選択します。キーボードとマウスの入力は影響を受けません。", "gamepad_driver_none": "なし (ゲームパッドを無効にする)", "gamepad_driver_select": "ドライバを選択してください...", "gamepad_driver_vigembus": "ViGEmBus", diff --git a/src_assets/common/assets/web/public/assets/locale/pt.json b/src_assets/common/assets/web/public/assets/locale/pt.json index 1a7e7d839cf..b0d8092c1df 100644 --- a/src_assets/common/assets/web/public/assets/locale/pt.json +++ b/src_assets/common/assets/web/public/assets/locale/pt.json @@ -20,7 +20,7 @@ "enabled_def": "Ativado (padrão)", "enabled_def_cbox": "Padrão: marcado", "error": "Erro!", - "loading": "Carregandochar@@0", + "loading": "Carregando...", "note": "Nota:", "password": "Palavra-passe", "run_as": "Executar como Administrador", @@ -88,7 +88,7 @@ "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, Sunshine irá enviar a imagem da caixa padrão.", - "loading": "Carregandochar@@0", + "loading": "Carregando...", "name": "Nome", "no_applications": "Nenhum aplicativo foi adicionado ainda", "no_covers_found": "Nenhuma tampa encontrada", @@ -605,7 +605,7 @@ "permissions_title": "Permissões", "permissions_desc": "Conceda o acesso necessário à sua configuração de Sunshine. Reinicia Sunshine depois que detectar todos os acessos necessários.", "permissions_refresh": "Atualizar Permissões", - "permissions_name": "Permisschar@@0o", + "permissions_name": "Permissão", "permissions_requirement": "Requisito", "permissions_status": "SItuação", "permissions_action": "Acão", @@ -654,7 +654,7 @@ "driver_name": "Motorista", "driver_not_installed": "Não instalado", "driver_refresh": "Atualizar Drivers", - "driver_release_checking": "Verificandochar@@0", + "driver_release_checking": "Verificando...", "driver_release_current": "Última versão instalada", "driver_release_eol": "Fim de vida", "driver_release_loading": "Checking GitHub...", diff --git a/src_assets/common/assets/web/public/assets/locale/pt_BR.json b/src_assets/common/assets/web/public/assets/locale/pt_BR.json index 7464c095d05..3143ad29686 100644 --- a/src_assets/common/assets/web/public/assets/locale/pt_BR.json +++ b/src_assets/common/assets/web/public/assets/locale/pt_BR.json @@ -20,7 +20,7 @@ "enabled_def": "Ativado (padrão)", "enabled_def_cbox": "Padrão: marcado", "error": "Erro!", - "loading": "Carregandochar@@0", + "loading": "Carregando...", "note": "Observação:", "password": "Senha", "run_as": "Executar como administrador", @@ -605,7 +605,7 @@ "permissions_title": "Permissões", "permissions_desc": "Conceda o acesso necessário à sua configuração de Sunshine. Reinicia Sunshine depois que detectar todos os acessos necessários.", "permissions_refresh": "Atualizar Permissões", - "permissions_name": "Permisschar@@0o", + "permissions_name": "Permissão", "permissions_requirement": "Requisito", "permissions_status": "SItuação", "permissions_action": "Acão", @@ -654,7 +654,7 @@ "driver_name": "Motorista", "driver_not_installed": "Não instalado", "driver_refresh": "Atualizar Drivers", - "driver_release_checking": "Verificandochar@@0", + "driver_release_checking": "Verificando...", "driver_release_current": "Última versão instalada", "driver_release_eol": "Fim de vida", "driver_release_loading": "Checking GitHub...", diff --git a/src_assets/common/assets/web/sunshine.css b/src_assets/common/assets/web/sunshine.css index 107c33c8198..9fbd18b9f25 100644 --- a/src_assets/common/assets/web/sunshine.css +++ b/src_assets/common/assets/web/sunshine.css @@ -2,6 +2,13 @@ Sunshine UI - Modern Design System ========================================================================== */ +/** + * @brief Theme Bootstrap components through source order and component selectors. + * + * Load this stylesheet after Bootstrap. Use badge-primary and badge-secondary + * for themed badges so Bootstrap background utilities do not override them. + */ + /* Hide pages while localization is loading */ [v-cloak] { display: none; @@ -969,8 +976,8 @@ html { } body { - background-color: var(--color-bg-base) !important; - color: var(--color-text-base) !important; + background-color: var(--color-bg-base); + color: var(--color-text-base); transition: background-color var(--transition-slow), color var(--transition-slow); min-height: 100vh; } @@ -993,11 +1000,11 @@ body { } .card { - background-color: var(--color-surface) !important; - border: 1px solid var(--color-border) !important; + background-color: var(--color-surface); + border: 1px solid var(--color-border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); - color: var(--color-text-base) !important; + color: var(--color-text-base); transition: all var(--transition-base); } @@ -1006,109 +1013,123 @@ body { } .card-header { - background-color: var(--color-bg-subtle) !important; - border-color: var(--color-border) !important; - color: var(--color-text-base) !important; + background-color: var(--color-bg-subtle); + border-color: var(--color-border); + color: var(--color-text-base); } .card-footer { - background-color: var(--color-bg-subtle) !important; - border-color: var(--color-border) !important; + background-color: var(--color-bg-subtle); + border-color: var(--color-border); } .modal-content { - background-color: var(--color-surface) !important; - border-color: var(--color-border) !important; + background-color: var(--color-surface); + border-color: var(--color-border); } .modal-header, .modal-footer { - border-color: var(--color-border) !important; - background-color: var(--color-surface-raised) !important; + border-color: var(--color-border); + background-color: var(--color-surface-raised); } .list-group-item { - background-color: var(--color-surface) !important; - border-color: var(--color-border) !important; - color: var(--color-text-base) !important; + --bs-list-group-disabled-bg: var(--color-surface); + --bs-list-group-disabled-color: var(--color-text-base); + --bs-list-group-action-hover-bg: var(--color-surface); + --bs-list-group-action-hover-color: var(--color-text-base); + --bs-list-group-action-active-bg: var(--color-surface); + --bs-list-group-action-active-color: var(--color-text-base); + --bs-list-group-active-bg: var(--color-surface); + --bs-list-group-active-border-color: var(--color-border); + --bs-list-group-active-color: var(--color-text-base); + background-color: var(--color-surface); + border-color: var(--color-border); + color: var(--color-text-base); } -.list-group-item:hover { - background-color: var(--color-bg-subtle) !important; +.list-group-item:hover, +.list-group-item-action:not(.active):hover { + background-color: var(--color-bg-subtle); } .accordion-item { - background-color: var(--color-surface) !important; - border-color: var(--color-border) !important; + background-color: var(--color-surface); + border-color: var(--color-border); } .accordion-button { - background-color: var(--color-bg-subtle) !important; - color: var(--color-text-base) !important; + background-color: var(--color-bg-subtle); + color: var(--color-text-base); } .accordion-button:not(.collapsed) { - background-color: var(--color-bg-muted) !important; - color: var(--color-text-base) !important; + background-color: var(--color-bg-muted); + color: var(--color-text-base); } .offcanvas { - background-color: var(--color-surface) !important; + background-color: var(--color-surface); } .offcanvas-header, .offcanvas-body { - color: var(--color-text-base) !important; + color: var(--color-text-base); } .toast { - background-color: var(--color-surface) !important; - border-color: var(--color-border) !important; + background-color: var(--color-surface); + border-color: var(--color-border); } .toast-header { - background-color: var(--color-bg-subtle) !important; - border-color: var(--color-border) !important; - color: var(--color-text-base) !important; + background-color: var(--color-bg-subtle); + border-color: var(--color-border); + color: var(--color-text-base); } .toast-body { - color: var(--color-text-base) !important; + color: var(--color-text-base); } .popover { - background-color: var(--color-surface) !important; - border-color: var(--color-border) !important; + background-color: var(--color-surface); + border-color: var(--color-border); } .popover-header { - background-color: var(--color-bg-subtle) !important; - border-color: var(--color-border) !important; - color: var(--color-text-base) !important; + background-color: var(--color-bg-subtle); + border-color: var(--color-border); + color: var(--color-text-base); } .popover-body { - color: var(--color-text-base) !important; + color: var(--color-text-base); } .tooltip-inner { - background-color: var(--color-bg-muted) !important; - color: var(--color-text-base) !important; + background-color: var(--color-bg-muted); + color: var(--color-text-base); } .breadcrumb { - background-color: var(--color-bg-subtle) !important; + background-color: var(--color-bg-subtle); } .breadcrumb-item a { - color: var(--color-primary) !important; + color: var(--color-primary); } .breadcrumb-item.active { - color: var(--color-text-muted) !important; + color: var(--color-text-muted); } .pagination { + --bs-pagination-hover-color: var(--color-text-base); + --bs-pagination-focus-color: var(--color-text-base); + --bs-pagination-active-color: var(--color-text-base); + --bs-pagination-disabled-color: var(--color-text-base); --bs-pagination-bg: var(--color-surface); --bs-pagination-border-color: var(--color-border); --bs-pagination-hover-bg: var(--color-bg-subtle); @@ -1119,7 +1140,7 @@ body { } .page-link { - color: var(--color-text-base) !important; + color: var(--color-text-base); } /* ========================================================================== @@ -1127,7 +1148,8 @@ body { ========================================================================== */ .navbar-sunshine { - background: var(--navbar-bg) !important; + --bs-navbar-nav-link-padding-x: 1rem; + background: var(--navbar-bg); box-shadow: var(--shadow-md); border: none; transition: all var(--transition-base); @@ -1142,9 +1164,9 @@ body { } .navbar-sunshine .nav-link { - color: var(--navbar-text-muted) !important; + color: var(--navbar-text-muted); font-weight: 500; - padding: 0.5rem 1rem !important; + padding: 0.5rem 1rem; min-height: calc(1.5em + 1rem); border-radius: var(--radius-md); transition: all var(--transition-fast); @@ -1154,25 +1176,25 @@ body { } .navbar-sunshine .nav-link:hover { - color: var(--navbar-text) !important; + color: var(--navbar-text); background-color: rgba(255, 255, 255, 0.1); transform: translateY(-1px); } .navbar-sunshine .nav-link.active { - color: var(--navbar-text) !important; + color: var(--navbar-text); background-color: rgba(255, 255, 255, 0.15); font-weight: 600; } .navbar-sunshine .nav-link.dropdown-toggle.show { - color: var(--navbar-text) !important; + color: var(--navbar-text); background-color: rgba(255, 255, 255, 0.15); } .navbar-sunshine .navbar-toggler { - color: var(--navbar-text) !important; - border-color: rgba(255, 255, 255, 0.2) !important; + color: var(--navbar-text); + border-color: rgba(255, 255, 255, 0.2); } .navbar-sunshine .navbar-toggler:focus { @@ -1321,7 +1343,7 @@ p { .btn:disabled { opacity: 0.6; cursor: not-allowed; - transform: none !important; + transform: none; } .btn-primary { @@ -1420,14 +1442,14 @@ p { Badge Styles ========================================================================== */ -.badge.bg-primary { - background-color: var(--color-primary) !important; - color: var(--color-on-primary) !important; +.badge.badge-primary { + background-color: var(--color-primary); + color: var(--color-on-primary); } -.badge.bg-secondary { - background-color: var(--color-bg-muted) !important; - color: var(--color-text-base) !important; +.badge.badge-secondary { + background-color: var(--color-bg-muted); + color: var(--color-text-base); } /* ========================================================================== @@ -2242,7 +2264,7 @@ p { } .cover-finder .cover-results.busy * { - cursor: wait !important; + cursor: wait; pointer-events: none; } @@ -2313,8 +2335,8 @@ p { .markdown-body.release-notes { width: 100%; max-width: 100%; - column-count: 1 !important; - display: block !important; + column-count: 1; + display: block; } .markdown-body h1, @@ -2432,8 +2454,8 @@ p { ========================================================================== */ .dropdown-menu { - background-color: var(--color-surface) !important; - border: 1px solid var(--color-border) !important; + background-color: var(--color-surface); + border: 1px solid var(--color-border); border-radius: var(--radius-md); box-shadow: var(--shadow-lg); padding: var(--spacing-sm); @@ -2446,27 +2468,31 @@ p { .dropdown-item { padding: var(--spacing-sm) var(--spacing-md); border-radius: var(--radius-sm); - color: var(--color-text-base) !important; + --bs-dropdown-link-hover-color: var(--color-text-base); + --bs-dropdown-link-active-color: var(--color-text-base); + --bs-dropdown-link-disabled-color: var(--color-text-base); + color: var(--color-text-base); transition: all var(--transition-fast); } -.dropdown-item:hover { - background-color: var(--color-bg-muted) !important; - color: var(--color-text-base) !important; -} - .dropdown-item.active { background-color: var(--color-primary-light); - color: var(--color-primary); + color: var(--color-text-base); } [data-bs-theme="dark"] .dropdown-item.active { background-color: var(--color-primary); - color: white; + color: var(--color-text-base); +} + +.dropdown-item:hover, +.dropdown-menu .dropdown-item:hover { + background-color: var(--color-bg-muted); + color: var(--color-text-base); } .dropdown-divider { - border-color: var(--color-border) !important; + border-color: var(--color-border); } /* Add spacing between icons and text in dropdown items */ @@ -2705,8 +2731,8 @@ p { } .featured-app-icon img { - max-width: 64px !important; - max-height: 64px !important; + max-width: 64px; + max-height: 64px; width: auto; height: auto; object-fit: contain; @@ -2791,9 +2817,9 @@ p { } .screenshot-thumbnail { - height: 120px !important; + height: 120px; width: auto; - max-width: 200px !important; + max-width: 200px; min-width: 100px; border-radius: var(--radius-sm); cursor: pointer; diff --git a/tests/tests_log_checker.h b/tests/tests_log_checker.h index 1b5ce2fade1..aa6ad5d6f65 100644 --- a/tests/tests_log_checker.h +++ b/tests/tests_log_checker.h @@ -96,7 +96,7 @@ namespace log_checker { } /** - * @brief Check if a log file contains a line that contains the given substring. + * @brief Check whether any log line contains the substring, including an empty 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. @@ -122,7 +122,7 @@ namespace log_checker { // 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) { + if (line.contains(search_str)) { return true; } } diff --git a/tests/unit/platform/linux/test_audio.cpp b/tests/unit/platform/linux/test_audio.cpp index f905bd48fc4..5358f65df97 100644 --- a/tests/unit/platform/linux/test_audio.cpp +++ b/tests/unit/platform/linux/test_audio.cpp @@ -6,6 +6,7 @@ #ifdef __linux__ // standard includes + #include #include // lib includes @@ -14,9 +15,26 @@ // local includes #include "src/platform/common.h" + #include "src/thread_safe.h" namespace platf { bool fill_canonical_channel_map(int channels, pa_channel_map &pa_map); + + namespace pa { + /** + * @brief Forward a PulseAudio integer result to the caller's alarm. + * @param value Result supplied by PulseAudio. + * @param userdata Alarm receiving the result. + */ + void cb_i(pa_context *, std::uint32_t value, void *userdata); + + /** + * @brief Forward PulseAudio operation completion to the caller's alarm. + * @param status Nonzero on success, zero on failure. + * @param userdata Alarm receiving the translated result. + */ + void success_cb(pa_context *, int status, void *userdata); + } // namespace pa } // namespace platf namespace { @@ -109,4 +127,30 @@ TEST(LinuxAudioChannelMap, RejectsUnsupportedChannelCounts) { EXPECT_FALSE(platf::fill_canonical_channel_map(platf::speaker::MAX_SPEAKERS + 1, pa_map)); } +/** + * @brief PulseAudio integer callbacks ring the supplied alarm without requiring a context. + */ +TEST(LinuxAudioCallbackTest, ForwardsIntegerResultWithoutContext) { + safe::alarm_raw_t alarm; + platf::pa::cb_i(nullptr, 42, &alarm); + + ASSERT_TRUE(alarm.wait_for(std::chrono::milliseconds {0})); + ASSERT_TRUE(alarm.status().has_value()); + EXPECT_EQ(*alarm.status(), 42); +} + +/** + * @brief PulseAudio success callbacks preserve the zero-success and one-failure alarm convention. + */ +TEST(LinuxAudioCallbackTest, TranslatesSuccessAndFailureWithoutContext) { + for (const int status : {0, 1, -1}) { + safe::alarm_raw_t alarm; + platf::pa::success_cb(nullptr, status, &alarm); + + ASSERT_TRUE(alarm.wait_for(std::chrono::milliseconds {0})); + ASSERT_TRUE(alarm.status().has_value()); + EXPECT_EQ(*alarm.status(), status == 0 ? 1 : 0); + } +} + #endif // __linux__ diff --git a/tests/unit/platform/linux/test_dynamic_loader.cpp b/tests/unit/platform/linux/test_dynamic_loader.cpp new file mode 100644 index 00000000000..9a9008db2fe --- /dev/null +++ b/tests/unit/platform/linux/test_dynamic_loader.cpp @@ -0,0 +1,48 @@ +/** + * @file tests/unit/platform/linux/test_dynamic_loader.cpp + * @brief Tests for opaque Linux shared-library handles and symbol loading. + */ +#ifdef __linux__ + + // standard includes + #include + + // platform includes + #include + + // lib includes + #include + + // local includes + #include "src/platform/linux/misc.h" + +TEST(DynamicLibraryTest, FallsBackToAnAvailableLibraryAndLoadsCallableSymbols) { + auto *library = dyn::handle({"libsunshine-does-not-exist.so", "libc.so.6"}); + ASSERT_NE(library, nullptr); + auto close_library = util::fail_guard([library] { + dlclose(library); + }); + dyn::apiproc symbol = nullptr; + ASSERT_EQ(dyn::load(library, {{&symbol, "getpid"}}), 0); + ASSERT_NE(symbol, nullptr); + const auto loaded_getpid = std::bit_cast(symbol); + EXPECT_EQ(loaded_getpid(), getpid()); +} + +TEST(DynamicLibraryTest, MissingOptionalSymbolDoesNotFailLoading) { + auto *library = dyn::handle({"libc.so.6"}); + ASSERT_NE(library, nullptr); + auto close_library = util::fail_guard([library] { + dlclose(library); + }); + dyn::apiproc symbol = nullptr; + EXPECT_EQ(dyn::load(library, {{&symbol, "sunshine_missing_symbol"}}, false), 0); + EXPECT_EQ(symbol, nullptr); + EXPECT_EQ(dyn::load(library, {{&symbol, "sunshine_missing_symbol"}}, true), -1); +} + +TEST(DynamicLibraryTest, MissingLibraryReturnsNull) { + EXPECT_EQ(dyn::handle({"libsunshine-does-not-exist.so", "libsunshine-also-does-not-exist.so"}), nullptr); +} + +#endif diff --git a/tests/unit/platform/linux/test_wayland.cpp b/tests/unit/platform/linux/test_wayland.cpp index 6e260889ff8..1b794fddf7a 100644 --- a/tests/unit/platform/linux/test_wayland.cpp +++ b/tests/unit/platform/linux/test_wayland.cpp @@ -7,17 +7,32 @@ #include #include #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include // system includes #include #include #include + #include // test includes #include "../../../tests_common.h" // local includes #include + #ifdef SUNSHINE_BUILD_CUDA + #include + #endif namespace { struct fake_gbm_bo_t { @@ -102,6 +117,32 @@ TEST(WaylandMonitorTest, RecordsRefreshOfCurrentMode) { EXPECT_EQ(monitor.refresh_mhz, 120000); } +/** + * @brief Ignored wl-output events must not overwrite xdg-output placement or the active pixel mode. + */ +TEST(WaylandMonitorTest, ListenerIgnoresPhysicalGeometryScaleAndDone) { + wl::monitor_t monitor {nullptr}; + monitor.xdg_position(nullptr, 100, 200); + monitor.xdg_size(nullptr, 1280, 720); + monitor.wl_mode(nullptr, WL_OUTPUT_MODE_CURRENT, 2560, 1440, 120000); + monitor.name = "DP-1"; + monitor.description = "Logical monitor"; + + monitor.wl_listener.geometry(&monitor, nullptr, -500, -600, 600, 340, WL_OUTPUT_SUBPIXEL_UNKNOWN, "Other", "Physical monitor", WL_OUTPUT_TRANSFORM_90); + monitor.wl_listener.scale(&monitor, nullptr, 3); + monitor.wl_listener.done(&monitor, nullptr); + + EXPECT_EQ(monitor.viewport.offset_x, 100); + EXPECT_EQ(monitor.viewport.offset_y, 200); + EXPECT_EQ(monitor.viewport.logical_width, 1280); + EXPECT_EQ(monitor.viewport.logical_height, 720); + EXPECT_EQ(monitor.viewport.width, 2560); + EXPECT_EQ(monitor.viewport.height, 1440); + EXPECT_EQ(monitor.refresh_mhz, 120000); + EXPECT_EQ(monitor.name, "DP-1"); + EXPECT_EQ(monitor.description, "Logical monitor"); +} + TEST(WaylandCaptureTest, UsesDamageCaptureOnlyOnAFasterOutput) { // An output at twice the stream's rate, and one just at the threshold. EXPECT_TRUE(wl::use_damage_capture(3, 60.0, 120000)); @@ -310,14 +351,194 @@ TEST(WaylandCaptureTest, UsesSystemMemoryForSoftwareEncoding) { EXPECT_FALSE(wl::use_vram_capture(platf::mem_type_e::system)); } -TEST(WaylandCaptureTest, UsesVramForCudaOnlyWhenCudaSupportIsBuilt) { +TEST(WaylandCaptureTest, UsesVramForCudaOnlyWhenCaptureNodeMatchesCudaPci) { #ifdef SUNSHINE_BUILD_CUDA - EXPECT_TRUE(wl::use_vram_capture(platf::mem_type_e::cuda)); + const wl::render_node_pci_reader_t render_reader = [](const std::string &) { + return std::optional {"0000:0f:00.0"}; + }; + const wl::cuda_device_pci_reader_t matching_cuda_reader = [](int) { + return std::optional {"00000000:0F:00.0"}; + }; + const wl::cuda_device_pci_reader_t other_cuda_reader = [](int) { + return std::optional {"00000000:0E:00.0"}; + }; + EXPECT_TRUE(wl::use_vram_capture(platf::mem_type_e::cuda, render_reader, matching_cuda_reader)); + EXPECT_FALSE(wl::use_vram_capture(platf::mem_type_e::cuda, render_reader, other_cuda_reader)); #else EXPECT_FALSE(wl::use_vram_capture(platf::mem_type_e::cuda)); #endif } +TEST(WaylandCaptureTest, ParsesCudaAndSysfsPciBusIds) { + const auto cuda_form = wl::parse_pci_bus_id("00000000:0F:00.0"); + const auto short_cuda_form = wl::parse_pci_bus_id("0000:0F:00.0"); + const auto sysfs_form = wl::parse_pci_bus_id("0000:0f:00.0"); + ASSERT_TRUE(cuda_form.has_value()); + ASSERT_TRUE(short_cuda_form.has_value()); + ASSERT_TRUE(sysfs_form.has_value()); + EXPECT_EQ(*cuda_form, *sysfs_form); + EXPECT_EQ(*short_cuda_form, *sysfs_form); + EXPECT_EQ(cuda_form->domain, 0u); + EXPECT_EQ(cuda_form->bus, 0x0fu); + EXPECT_EQ(cuda_form->device, 0x00u); + EXPECT_EQ(cuda_form->function, 0u); +} + +TEST(WaylandCaptureTest, RejectsMalformedPciBusIds) { + for (const char *bus_id : {"", "0000:0f:00", "0000:0f:00.0:1", "0000:0f:00.8", "0000:0f:00.0 ", "pci:0000:0f:00.0", "0000:0g:00.0", "0x10de"}) { + EXPECT_FALSE(wl::parse_pci_bus_id(bus_id).has_value()) << bus_id; + } +} + +TEST(WaylandCaptureTest, FallsBackToRamBridgeWhenPciIsUnprovable) { + const wl::render_node_pci_reader_t render_reader = [](const std::string &) { + return std::optional {"0000:0f:00.0"}; + }; + const wl::render_node_pci_reader_t missing_render_reader = [](const std::string &) { + return std::optional {}; + }; + const wl::cuda_device_pci_reader_t cuda_reader = [](int) { + return std::optional {"00000000:0F:00.0"}; + }; + const wl::cuda_device_pci_reader_t missing_cuda_reader = [](int) { + return std::optional {}; + }; + EXPECT_FALSE(wl::capture_node_is_cuda_device(missing_render_reader, cuda_reader)); + EXPECT_FALSE(wl::capture_node_is_cuda_device(render_reader, missing_cuda_reader)); + EXPECT_FALSE(wl::capture_node_is_cuda_device(missing_render_reader, missing_cuda_reader)); + #ifdef SUNSHINE_BUILD_CUDA + EXPECT_FALSE(wl::use_vram_capture(platf::mem_type_e::cuda, missing_render_reader, cuda_reader)); + EXPECT_FALSE(wl::use_vram_capture(platf::mem_type_e::cuda, render_reader, missing_cuda_reader)); + #endif +} + +TEST(WaylandCaptureTest, ResolvesRenderNodeAliasBeforePciLookup) { + const wl::render_path_resolver_t fake_resolver = [](const std::string &) { + return "/dev/dri/renderD128"; + }; + std::string observed_path; + const wl::render_node_pci_reader_t recording_reader = [&observed_path](std::string_view render_path) { + observed_path = render_path; + return std::optional {"0000:0f:00.0"}; + }; + const wl::cuda_device_pci_reader_t cuda_reader = [](int) { + return std::optional {"00000000:0F:00.0"}; + }; + EXPECT_TRUE(wl::capture_node_is_cuda_device(recording_reader, cuda_reader, fake_resolver)); + EXPECT_EQ(observed_path, "/dev/dri/renderD128"); +} + +TEST(WaylandLinearCopyTest, SkipsLinearAttemptUnlessAdvertised) { + const std::vector without_linear {0x100000000000004}; + const std::vector with_linear {0x100000000000004, DRM_FORMAT_MOD_LINEAR}; + EXPECT_FALSE(wl::should_attempt_linear_copy(false, with_linear)); + EXPECT_FALSE(wl::should_attempt_linear_copy(true, without_linear)); + EXPECT_FALSE(wl::should_attempt_linear_copy(true, {})); + EXPECT_TRUE(wl::should_attempt_linear_copy(true, with_linear)); +} + +TEST(WaylandAdvertisedModifiersTest, ReturnsNullWhenUnavailable) { + const std::map> empty_map {}; + const std::map> map_with_empty_list {{0x34325258, {}}}; + EXPECT_EQ(wl::find_advertised_modifiers(nullptr, 0x34325258), nullptr); + EXPECT_EQ(wl::find_advertised_modifiers(&empty_map, 0x34325258), nullptr); + EXPECT_EQ(wl::find_advertised_modifiers(&map_with_empty_list, 0x34325258), nullptr); + EXPECT_EQ(wl::find_advertised_modifiers(&map_with_empty_list, 0xdeadbeef), nullptr); +} + +TEST(WaylandAdvertisedModifiersTest, ReturnsAdvertisedListWhenPresent) { + const std::vector expected {0x100000000000004, DRM_FORMAT_MOD_LINEAR}; + const std::map> modifiers {{0x34325258, expected}}; + const auto *actual = wl::find_advertised_modifiers(&modifiers, 0x34325258); + ASSERT_NE(actual, nullptr); + EXPECT_EQ(*actual, expected); +} + +/** + * @brief Test fixture for DRM render-node path resolution. + */ +class WaylandResolvePathTest: public BaseTest { +protected: + /** + * @brief Create an empty directory for the current test. + */ + void SetUp() override { + BaseTest::SetUp(); + std::filesystem::remove_all(test_dir); + std::filesystem::create_directories(test_dir); + } + + /** + * @brief Remove files created by the current test. + */ + void TearDown() override { + std::filesystem::remove_all(test_dir); + BaseTest::TearDown(); + } + + const std::filesystem::path test_dir {std::filesystem::path {SUNSHINE_TEST_BIN_DIR} / "wayland_resolve_path_tests"}; ///< Directory containing resolve-path test files. +}; + +TEST_F(WaylandResolvePathTest, FollowsSymlinkedAliasToTarget) { + std::error_code ec; + const auto dir = std::filesystem::weakly_canonical(test_dir, ec); + ASSERT_FALSE(ec); + const auto target = dir / "renderD128"; + const auto alias = dir / "by-path" / "pci-0000:00:02.0-render"; + std::filesystem::create_directories(alias.parent_path(), ec); + ASSERT_FALSE(ec) << ec.message(); + { + std::ofstream out {target}; + ASSERT_TRUE(out.good()); + } + std::filesystem::remove(alias, ec); + std::filesystem::create_symlink(target, alias, ec); + ASSERT_FALSE(ec) << ec.message(); + EXPECT_EQ(wl::resolve_drm_node_path(alias.string()), target.string()); +} + +TEST_F(WaylandResolvePathTest, FallsBackToVerbatimPath) { + std::error_code ec; + const auto dir = std::filesystem::weakly_canonical(test_dir, ec); + ASSERT_FALSE(ec); + const auto missing = dir / "renderD999"; + EXPECT_EQ(wl::resolve_drm_node_path(missing.string()), missing.string()); + + const auto dangling_target = dir / "no-such-target"; + const auto dangling_link = dir / "by-path" / "pci-0000:00:02.0-render"; + std::filesystem::create_directories(dangling_link.parent_path(), ec); + ASSERT_FALSE(ec) << ec.message(); + std::filesystem::remove(dangling_link, ec); + std::filesystem::create_symlink(dangling_target, dangling_link, ec); + ASSERT_FALSE(ec) << ec.message(); + EXPECT_EQ(wl::resolve_drm_node_path(dangling_link.string()), dangling_link.string()); +} + +TEST_F(WaylandResolvePathTest, FallsBackToVerbatimPathOnSymlinkLoop) { + std::error_code ec; + const auto dir = std::filesystem::weakly_canonical(test_dir, ec); + ASSERT_FALSE(ec); + // A symlink pointing at itself cannot be canonicalized (ELOOP), forcing the error_code path. + const auto loop = dir / "self-loop"; + std::filesystem::remove(loop, ec); + std::filesystem::create_symlink(loop, loop, ec); + ASSERT_FALSE(ec) << ec.message(); + EXPECT_EQ(wl::resolve_drm_node_path(loop.string()), loop.string()); +} + +TEST_F(WaylandResolvePathTest, ReturnsNoPciIdentityWhenRenderNodeCannotOpen) { + EXPECT_FALSE(wl::render_node_pci_bus_id((test_dir / "missing-render-node").string()).has_value()); +} + +TEST_F(WaylandResolvePathTest, ReturnsNoPciIdentityWhenDrmQueryFails) { + const auto regular_file = test_dir / "regular-file"; + { + std::ofstream output {regular_file}; + ASSERT_TRUE(output.good()); + } + EXPECT_FALSE(wl::render_node_pci_bus_id(regular_file.string()).has_value()); +} + TEST(WaylandInterfaceTest, RecordsOnlyExplicitDmabufModifiers) { constexpr std::uint32_t format = DRM_FORMAT_XRGB8888; constexpr std::uint64_t explicit_modifier = 0x100000000000004; @@ -521,4 +742,373 @@ TEST(WaylandGbmDeviceTest, DestructorClosesRenderNodeDescriptor) { EXPECT_FALSE(descriptor_is_open(fd)); } +namespace { + /** + * @brief Scripted answer for query_fake_drm_device(). + */ + struct fake_drm_query_t { + int result = 0; + drmDevice *device = nullptr; + }; + + /** + * @brief Scripted answer returned by query_fake_drm_device() (reset per test). + */ + fake_drm_query_t &fake_drm_query() { + static fake_drm_query_t query; + return query; + } + + /** + * @brief Count of query_fake_drm_device() calls (reset per test). + */ + int &drm_get_device_calls() { + static int count = 0; + return count; + } + + /** + * @brief Count of free_fake_drm_device() calls (reset per test). + */ + int &drm_free_device_calls() { + static int count = 0; + return count; + } + + /** + * @brief Fake PCI bus info backing query_fake_drm_device() (reset per test). + */ + drmPciBusInfo &fake_pci_bus_info() { + static drmPciBusInfo bus_info {}; + return bus_info; + } + + /** + * @brief Fake DRM device backing query_fake_drm_device() (reset per test). + */ + drmDevice &fake_drm_device() { + static drmDevice device {}; + return device; + } + + int query_fake_drm_device(int, std::uint32_t, _drmDevice **device) { + ++drm_get_device_calls(); + *device = fake_drm_query().device; + return fake_drm_query().result; + } + + void free_fake_drm_device(_drmDevice **device) { + ++drm_free_device_calls(); + if (device) { + *device = nullptr; + } + } + + /** + * @brief Reset the fake DRM query state before a PCI reader test. + */ + void reset_drm_pci_fakes() { + drm_get_device_calls() = 0; + drm_free_device_calls() = 0; + last_fake_render_node() = -1; + fake_pci_bus_info() = drmPciBusInfo {}; + fake_drm_device() = drmDevice {}; + fake_drm_query() = fake_drm_query_t {}; + } +} // namespace + +TEST(WaylandPciReaderTest, ReturnsFormattedPciIdentityOnSuccess) { + reset_drm_pci_fakes(); + fake_pci_bus_info() = drmPciBusInfo {.domain = 0x00a5, .bus = 0x0f, .dev = 0xa0, .func = 0x1}; + fake_drm_device().bustype = DRM_BUS_PCI; + fake_drm_device().businfo.pci = &fake_pci_bus_info(); + fake_drm_query() = {.result = 0, .device = &fake_drm_device()}; + const wl::drm_device_accessors_t accessors { + .open_render_node = open_fake_render_node, + .get_device = query_fake_drm_device, + .free_device = free_fake_drm_device, + }; + + const auto bus_id = wl::render_node_pci_bus_id("/dev/dri/renderD128", accessors); + + ASSERT_TRUE(bus_id.has_value()); + EXPECT_EQ(*bus_id, "00a5:0f:a0.1"); + EXPECT_EQ(drm_get_device_calls(), 1); + EXPECT_EQ(drm_free_device_calls(), 1); + const int fd = last_fake_render_node(); + ASSERT_GE(fd, 0); + EXPECT_FALSE(descriptor_is_open(fd)); +} + +TEST(WaylandPciReaderTest, ReturnsNoPciIdentityWhenRenderNodeCannotOpen) { + reset_drm_pci_fakes(); + const wl::drm_device_accessors_t accessors { + .open_render_node = fail_to_open_render_node, + .get_device = query_fake_drm_device, + .free_device = free_fake_drm_device, + }; + + EXPECT_FALSE(wl::render_node_pci_bus_id("/dev/dri/renderD128", accessors).has_value()); + EXPECT_EQ(drm_get_device_calls(), 0); + EXPECT_EQ(drm_free_device_calls(), 0); +} + +TEST(WaylandPciReaderTest, ReturnsNoPciIdentityWhenDrmQueryFails) { + reset_drm_pci_fakes(); + fake_drm_query() = {.result = -1, .device = nullptr}; + const wl::drm_device_accessors_t accessors { + .open_render_node = open_fake_render_node, + .get_device = query_fake_drm_device, + .free_device = free_fake_drm_device, + }; + + EXPECT_FALSE(wl::render_node_pci_bus_id("/dev/dri/renderD128", accessors).has_value()); + EXPECT_EQ(drm_get_device_calls(), 1); + EXPECT_EQ(drm_free_device_calls(), 0); + const int fd = last_fake_render_node(); + ASSERT_GE(fd, 0); + EXPECT_FALSE(descriptor_is_open(fd)); +} + +TEST(WaylandPciReaderTest, ReturnsNoPciIdentityWhenDeviceIsMissing) { + reset_drm_pci_fakes(); + fake_drm_query() = {.result = 0, .device = nullptr}; + const wl::drm_device_accessors_t accessors { + .open_render_node = open_fake_render_node, + .get_device = query_fake_drm_device, + .free_device = free_fake_drm_device, + }; + + EXPECT_FALSE(wl::render_node_pci_bus_id("/dev/dri/renderD128", accessors).has_value()); + EXPECT_EQ(drm_free_device_calls(), 0); + const int fd = last_fake_render_node(); + ASSERT_GE(fd, 0); + EXPECT_FALSE(descriptor_is_open(fd)); +} + +TEST(WaylandPciReaderTest, ReturnsNoPciIdentityForNonPciDevice) { + reset_drm_pci_fakes(); + fake_drm_device().bustype = DRM_BUS_PLATFORM; + fake_drm_query() = {.result = 0, .device = &fake_drm_device()}; + const wl::drm_device_accessors_t accessors { + .open_render_node = open_fake_render_node, + .get_device = query_fake_drm_device, + .free_device = free_fake_drm_device, + }; + + EXPECT_FALSE(wl::render_node_pci_bus_id("/dev/dri/renderD128", accessors).has_value()); + EXPECT_EQ(drm_free_device_calls(), 1); + const int fd = last_fake_render_node(); + ASSERT_GE(fd, 0); + EXPECT_FALSE(descriptor_is_open(fd)); +} + +TEST(WaylandPciReaderTest, ReturnsNoPciIdentityWhenPciBusInfoMissing) { + reset_drm_pci_fakes(); + fake_drm_device().bustype = DRM_BUS_PCI; + fake_drm_device().businfo.pci = nullptr; + fake_drm_query() = {.result = 0, .device = &fake_drm_device()}; + const wl::drm_device_accessors_t accessors { + .open_render_node = open_fake_render_node, + .get_device = query_fake_drm_device, + .free_device = free_fake_drm_device, + }; + + EXPECT_FALSE(wl::render_node_pci_bus_id("/dev/dri/renderD128", accessors).has_value()); + EXPECT_EQ(drm_free_device_calls(), 1); + const int fd = last_fake_render_node(); + ASSERT_GE(fd, 0); + EXPECT_FALSE(descriptor_is_open(fd)); +} + + #ifdef SUNSHINE_BUILD_CUDA +namespace { + /** + * @brief Whether the fake CUDA function table counts as loaded (reset per test). + */ + bool &fake_cuda_initialized() { + static bool initialized = true; + return initialized; + } + + /** + * @brief Count of fake_cuda_initialize() calls (reset per test). + */ + int &cuda_initialize_calls() { + static int count = 0; + return count; + } + + /** + * @brief Result returned by fake_cuda_initialize() (reset per test). + */ + int &cuda_initialize_result() { + static int result = 0; + return result; + } + + /** + * @brief Count of fake_cuda_get_device() calls (reset per test). + */ + int &cuda_get_device_calls() { + static int count = 0; + return count; + } + + /** + * @brief Result returned by fake_cuda_get_device() (reset per test). + */ + CUresult &cuda_get_device_result() { + static CUresult result = CUDA_SUCCESS; + return result; + } + + /** + * @brief Count of fake_cuda_get_pci_bus_id() calls (reset per test). + */ + int &cuda_get_pci_bus_id_calls() { + static int count = 0; + return count; + } + + /** + * @brief Result returned by fake_cuda_get_pci_bus_id() (reset per test). + */ + CUresult &cuda_get_pci_bus_id_result() { + static CUresult result = CUDA_SUCCESS; + return result; + } + + /** + * @brief Bus id text written by fake_cuda_get_pci_bus_id() (reset per test). + */ + std::string &cuda_fake_bus_id() { + static std::string bus_id; + return bus_id; + } + + /** + * @brief Device handle observed by fake_cuda_get_pci_bus_id() (reset per test). + */ + CUdevice &cuda_seen_device() { + static CUdevice device = 0; + return device; + } + + bool fake_cuda_is_initialized() { + return fake_cuda_initialized(); + } + + int fake_cuda_initialize() { + ++cuda_initialize_calls(); + return cuda_initialize_result(); + } + + CUresult fake_cuda_get_device(CUdevice *device, int) { + ++cuda_get_device_calls(); + if (device) { + *device = 7; + } + return cuda_get_device_result(); + } + + CUresult fake_cuda_get_pci_bus_id(char *pci_bus_id, int len, CUdevice device) { + ++cuda_get_pci_bus_id_calls(); + cuda_seen_device() = device; + if (cuda_get_pci_bus_id_result() == CUDA_SUCCESS && pci_bus_id != nullptr && len > 0) { + const auto end = std::format_to_n(pci_bus_id, len - 1, "{}", cuda_fake_bus_id()).out; + *end = '\0'; + } + return cuda_get_pci_bus_id_result(); + } + + const cuda::cuda_pci_accessors_t fake_cuda_pci_accessors { + .is_initialized = fake_cuda_is_initialized, + .initialize = fake_cuda_initialize, + .get_device = fake_cuda_get_device, + .get_pci_bus_id = fake_cuda_get_pci_bus_id, + }; + + /** + * @brief Reset the fake CUDA query state before a PCI reader test. + */ + void reset_cuda_pci_fakes() { + fake_cuda_initialized() = true; + cuda_initialize_calls() = 0; + cuda_initialize_result() = 0; + cuda_get_device_calls() = 0; + cuda_get_device_result() = CUDA_SUCCESS; + cuda_get_pci_bus_id_calls() = 0; + cuda_get_pci_bus_id_result() = CUDA_SUCCESS; + cuda_fake_bus_id().clear(); + cuda_seen_device() = 0; + } +} // namespace + +TEST(CudaPciReaderTest, ReturnsLowercaseBusIdWithoutReinitializing) { + reset_cuda_pci_fakes(); + cuda_fake_bus_id() = "00000000:0F:00.0"; + + const auto bus_id = cuda::device_pci_bus_id(0, fake_cuda_pci_accessors); + + ASSERT_TRUE(bus_id.has_value()); + EXPECT_EQ(*bus_id, "00000000:0f:00.0"); + EXPECT_EQ(cuda_initialize_calls(), 0); + EXPECT_EQ(cuda_get_device_calls(), 1); + EXPECT_EQ(cuda_get_pci_bus_id_calls(), 1); + EXPECT_EQ(cuda_seen_device(), 7); +} + +TEST(CudaPciReaderTest, ReturnsShortBusIdLowercased) { + reset_cuda_pci_fakes(); + cuda_fake_bus_id() = "0000:0F:00.0"; + + const auto bus_id = cuda::device_pci_bus_id(0, fake_cuda_pci_accessors); + + ASSERT_TRUE(bus_id.has_value()); + EXPECT_EQ(*bus_id, "0000:0f:00.0"); +} + +TEST(CudaPciReaderTest, InitializesUninitializedFunctionTable) { + reset_cuda_pci_fakes(); + fake_cuda_initialized() = false; + cuda_fake_bus_id() = "0000:0F:00.0"; + + const auto bus_id = cuda::device_pci_bus_id(0, fake_cuda_pci_accessors); + + ASSERT_TRUE(bus_id.has_value()); + EXPECT_EQ(*bus_id, "0000:0f:00.0"); + EXPECT_EQ(cuda_initialize_calls(), 1); + EXPECT_EQ(cuda_get_device_calls(), 1); +} + +TEST(CudaPciReaderTest, ReturnsNoBusIdWhenInitFails) { + reset_cuda_pci_fakes(); + fake_cuda_initialized() = false; + cuda_initialize_result() = -1; + + EXPECT_FALSE(cuda::device_pci_bus_id(0, fake_cuda_pci_accessors).has_value()); + EXPECT_EQ(cuda_initialize_calls(), 1); + EXPECT_EQ(cuda_get_device_calls(), 0); + EXPECT_EQ(cuda_get_pci_bus_id_calls(), 0); +} + +TEST(CudaPciReaderTest, ReturnsNoBusIdWhenDeviceQueryFails) { + reset_cuda_pci_fakes(); + cuda_get_device_result() = CUDA_ERROR_UNKNOWN; + + EXPECT_FALSE(cuda::device_pci_bus_id(0, fake_cuda_pci_accessors).has_value()); + EXPECT_EQ(cuda_get_device_calls(), 1); + EXPECT_EQ(cuda_get_pci_bus_id_calls(), 0); +} + +TEST(CudaPciReaderTest, ReturnsNoBusIdWhenBusIdQueryFails) { + reset_cuda_pci_fakes(); + cuda_get_pci_bus_id_result() = CUDA_ERROR_UNKNOWN; + + EXPECT_FALSE(cuda::device_pci_bus_id(0, fake_cuda_pci_accessors).has_value()); + EXPECT_EQ(cuda_get_pci_bus_id_calls(), 1); +} + #endif + #endif diff --git a/tests/unit/platform/test_common.cpp b/tests/unit/platform/test_common.cpp index d7f63f11772..090b91bc768 100644 --- a/tests/unit/platform/test_common.cpp +++ b/tests/unit/platform/test_common.cpp @@ -43,6 +43,34 @@ TEST(SocketAddressTest, UnsupportedAddressFamiliesProduceEmptyAddressesAndZeroPo EXPECT_EQ(platf::from_sockaddr_ex(&address), (std::pair {0, {}})); } +/** + * @brief Const batch descriptors locate payload boundaries and reject offsets past all buffers. + */ +TEST(BatchedSendInfoTest, ConstLookupPreservesBuffersAndFindsEveryBoundary) { + const std::string_view first = "abc"; + const std::string_view second = "defg"; + std::vector buffers {{nullptr, 0}, {first.data(), first.size()}, {second.data(), second.size()}}; + auto address = boost::asio::ip::address_v4::loopback(); + boost::asio::ip::address endpoint = address; + const platf::batched_send_info_t batch {nullptr, 0, buffers, 1, 0, 7, 0, endpoint, 0, endpoint}; + + EXPECT_EQ(batch.buffer_for_payload_offset(0).buffer, first.data()); + EXPECT_EQ(batch.buffer_for_payload_offset(0).size, first.size()); + EXPECT_EQ(batch.buffer_for_payload_offset(2).buffer, first.data() + 2); + EXPECT_EQ(batch.buffer_for_payload_offset(2).size, 1); + EXPECT_EQ(batch.buffer_for_payload_offset(3).buffer, second.data()); + EXPECT_EQ(batch.buffer_for_payload_offset(3).size, second.size()); + EXPECT_EQ(batch.buffer_for_payload_offset(6).buffer, second.data() + 3); + EXPECT_EQ(batch.buffer_for_payload_offset(6).size, 1); + for (const auto offset : {7, 8}) { + EXPECT_EQ(batch.buffer_for_payload_offset(offset).buffer, nullptr); + EXPECT_EQ(batch.buffer_for_payload_offset(offset).size, 0); + } + EXPECT_EQ(buffers.size(), 3); + EXPECT_EQ(buffers[1].buffer, first.data()); + EXPECT_EQ(buffers[1].size, first.size()); +} + #if defined(_WIN32) || defined(__linux__) || defined(__FreeBSD__) /** * @brief Exercise source-address control messages on bound UDP sockets with optional headers. @@ -64,7 +92,7 @@ TEST_P(UdpSourceAddressTest, SendsTheExpectedDatagramFromTheRequestedAddress) { const std::string_view headers = "headerheader"; const std::string_view payloads = "payloadpayload"; std::vector buffers {{payloads.data(), payloads.size()}}; - platf::batched_send_info_t batch_info { + const platf::batched_send_info_t batch_info { with_header ? headers.data() : nullptr, with_header ? header.size() : 0, buffers, diff --git a/tests/unit/platform/windows/test_display_abi.cpp b/tests/unit/platform/windows/test_display_abi.cpp new file mode 100644 index 00000000000..fea98c59973 --- /dev/null +++ b/tests/unit/platform/windows/test_display_abi.cpp @@ -0,0 +1,88 @@ +/** + * @file tests/unit/platform/windows/test_display_abi.cpp + * @brief Verify the Windows kernel ABI of Sunshine's D3DKMT declarations. + */ + +// test includes +#include "../../../tests_common.h" + +#ifdef _WIN32 + // standard includes + #include + #include + #include + + // local includes + #include + +/** + * @brief Adapter requests retain the sizes and field offsets expected by D3DKMT. + */ +TEST(D3dkmtAbiTests, AdapterRequestsMatchKernelLayouts) { + using display = platf::dxgi::display_base_t; + using open_request = display::D3DKMT_OPENADAPTERFROMLUID; + using query_request = display::D3DKMT_QUERYADAPTERINFO; + using close_request = display::D3DKMT_CLOSEADAPTER; + + EXPECT_TRUE(std::is_standard_layout_v); + EXPECT_EQ(sizeof(open_request), 12U); + EXPECT_EQ(alignof(open_request), 4U); + EXPECT_EQ(offsetof(open_request, AdapterLuid), 0U); + EXPECT_EQ(offsetof(open_request, hAdapter), 8U); + + EXPECT_TRUE(std::is_standard_layout_v); + EXPECT_EQ(sizeof(query_request), sizeof(void *) == 8 ? 24U : 16U); + EXPECT_EQ(alignof(query_request), alignof(void *)); + EXPECT_EQ(offsetof(query_request, hAdapter), 0U); + EXPECT_EQ(offsetof(query_request, Type), 4U); + EXPECT_EQ(offsetof(query_request, pPrivateDriverData), 8U); + EXPECT_EQ(offsetof(query_request, PrivateDriverDataSize), 8U + sizeof(void *)); + + EXPECT_TRUE(std::is_standard_layout_v); + EXPECT_EQ(sizeof(close_request), 4U); + EXPECT_EQ(alignof(close_request), 4U); + EXPECT_EQ(offsetof(close_request, hAdapter), 0U); +} + +/** + * @brief WDDM 2.7 capability fields map to their documented kernel flag bits. + * + * Inspect the object representation without reading an inactive union member. + */ +TEST(D3dkmtAbiTests, WddmCapabilitiesUseKernelBitPositions) { + using capabilities = platf::dxgi::display_base_t::D3DKMT_WDDM_2_7_CAPS; + EXPECT_TRUE(std::is_standard_layout_v); + EXPECT_EQ(sizeof(capabilities), 4U); + EXPECT_EQ(alignof(capabilities), 4U); + + capabilities caps {}; + caps.HwSchSupported = 1; + EXPECT_EQ(std::bit_cast(caps), 0x01U); + + caps = {}; + caps.HwSchEnabled = 1; + EXPECT_EQ(std::bit_cast(caps), 0x02U); + + caps = {}; + caps.HwSchEnabledByDefault = 1; + EXPECT_EQ(std::bit_cast(caps), 0x04U); + + caps = {}; + caps.IndependentVidPnVSyncControl = 1; + EXPECT_EQ(std::bit_cast(caps), 0x08U); +} + +/** + * @brief Scheduling priority values retain the representation expected by D3DKMT. + */ +TEST(D3dkmtAbiTests, SchedulingPrioritiesMatchKernelValues) { + using display = platf::dxgi::display_base_t; + EXPECT_EQ(sizeof(display::D3DKMT_SCHEDULINGPRIORITYCLASS), 4U); + EXPECT_EQ(display::D3DKMT_SCHEDULINGPRIORITYCLASS_IDLE, 0); + EXPECT_EQ(display::D3DKMT_SCHEDULINGPRIORITYCLASS_BELOW_NORMAL, 1); + EXPECT_EQ(display::D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL, 2); + EXPECT_EQ(display::D3DKMT_SCHEDULINGPRIORITYCLASS_ABOVE_NORMAL, 3); + EXPECT_EQ(display::D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH, 4); + EXPECT_EQ(display::D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME, 5); +} +#endif diff --git a/tests/unit/platform/windows/test_display_vram.cpp b/tests/unit/platform/windows/test_display_vram.cpp index b169dae1560..c8b6b3d337a 100644 --- a/tests/unit/platform/windows/test_display_vram.cpp +++ b/tests/unit/platform/windows/test_display_vram.cpp @@ -1,6 +1,6 @@ /** * @file tests/unit/platform/windows/test_display_vram.cpp - * @brief Test src/platform/windows/display_vram.cpp FP16 shader transfer selection. + * @brief Tests for Windows FP16 shader selection and cursor mask conversion. */ // test includes @@ -8,11 +8,107 @@ #ifdef _WIN32 // standard includes + #include + #include + #include + #include #include // local includes #include +namespace platf::dxgi { + /** + * @brief Convert cursor mask bytes to pixels for XOR blending. + * + * @param img_data Raw DXGI cursor data. + * @param shape_info Cursor format and dimensions. + * @return Cursor pixels for XOR blending. + */ + util::buffer_t make_cursor_xor_image(const util::buffer_t &img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info); + + /** + * @brief Convert cursor mask bytes to pixels for alpha blending. + * + * @param img_data Raw DXGI cursor data. + * @param shape_info Cursor format and dimensions. + * @return Cursor pixels for alpha blending. + */ + util::buffer_t make_cursor_alpha_image(const util::buffer_t &img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info); +} // namespace platf::dxgi + +TEST(WindowsCursorTest, DecodesEveryMonochromeMaskCombinationAcrossByteBoundaries) { + // Each pair of AND/XOR bits selects black, white, transparent, or inverted. + util::buffer_t masks {4}; + masks[0] = 0x33; + masks[1] = 0x33; + masks[2] = 0x55; + masks[3] = 0x55; + const DXGI_OUTDUPL_POINTER_SHAPE_INFO shape { + .Type = DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME, + .Width = 16, + .Height = 2, + .Pitch = 2, + }; + const auto alpha = platf::dxgi::make_cursor_alpha_image(masks, shape); + const auto xor_image = platf::dxgi::make_cursor_xor_image(masks, shape); + ASSERT_EQ(alpha.size(), 16U * sizeof(std::uint32_t)); + ASSERT_EQ(xor_image.size(), alpha.size()); + const std::array expected_alpha {0xFF000000, 0xFFFFFFFF, 0, 0}; + const std::array expected_xor {0, 0, 0, 0xFFFFFFFF}; + for (std::size_t pixel = 0; pixel < 16; ++pixel) { + std::array alpha_bytes; + std::array xor_bytes; + std::copy_n(alpha.begin() + pixel * 4, 4, alpha_bytes.begin()); + std::copy_n(xor_image.begin() + pixel * 4, 4, xor_bytes.begin()); + EXPECT_EQ(std::bit_cast(alpha_bytes), expected_alpha[pixel % 4]); + EXPECT_EQ(std::bit_cast(xor_bytes), expected_xor[pixel % 4]); + } +} + +TEST(WindowsCursorTest, StopsAtTheLastPixelInAPartialMaskByte) { + util::buffer_t masks {2}; + masks[0] = 0xFF; + masks[1] = 0xFF; + const DXGI_OUTDUPL_POINTER_SHAPE_INFO shape { + .Type = DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME, + .Width = 3, + .Height = 2, + .Pitch = 1, + }; + const auto alpha = platf::dxgi::make_cursor_alpha_image(masks, shape); + const auto xor_image = platf::dxgi::make_cursor_xor_image(masks, shape); + EXPECT_EQ(alpha.size(), 12U); + EXPECT_EQ(xor_image.size(), 12U); + EXPECT_TRUE(std::all_of(alpha.begin(), alpha.end(), [](auto byte) { + return byte == 0; + })); + EXPECT_TRUE(std::all_of(xor_image.begin(), xor_image.end(), [](auto byte) { + return byte == 0xFF; + })); +} + +TEST(WindowsCursorTest, ProcessesEveryMaskedColorPixelAndPreservesTheInput) { + const std::array pixels {0xFF112233, 0x00123456, 0x7F445566}; + const auto bytes = std::bit_cast>(pixels); + util::buffer_t input {bytes.size()}; + std::ranges::copy(bytes, input.begin()); + const DXGI_OUTDUPL_POINTER_SHAPE_INFO shape { + .Type = DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR, + .Width = 3, + .Height = 1, + .Pitch = 12, + }; + + const auto alpha = platf::dxgi::make_cursor_alpha_image(input, shape); + const auto xor_image = platf::dxgi::make_cursor_xor_image(input, shape); + const std::array expected_alpha {0, 0xFF123456, 0x7F445566}; + const std::array expected_xor {0xFF112233, 0, 0x7F445566}; + EXPECT_TRUE((std::ranges::equal(alpha, std::bit_cast>(expected_alpha)))); + EXPECT_TRUE((std::ranges::equal(xor_image, std::bit_cast>(expected_xor)))); + EXPECT_TRUE(std::ranges::equal(input, bytes)); +} + using platf::dxgi::fp16_transfer_e; using platf::dxgi::select_fp16_transfer; diff --git a/tests/unit/platform/windows/test_input.cpp b/tests/unit/platform/windows/test_input.cpp new file mode 100644 index 00000000000..40a82f72150 --- /dev/null +++ b/tests/unit/platform/windows/test_input.cpp @@ -0,0 +1,210 @@ +/** + * @file tests/unit/platform/windows/test_input.cpp + * @brief Tests for ViGEm rumble scaling and DualShock 4 report bytes. + */ + +// test includes +#include "../../../tests_common.h" + +#ifdef _WIN32 + // standard includes + #include + #include + #include + #include + #include + + // local includes + #include + +TEST(WindowsRumbleTest, PreservesTheEstablishedRangeForEveryAmplitude) { + for (unsigned strength = 0; strength <= 255; ++strength) { + EXPECT_EQ(platf::vigem::scale_rumble(static_cast(strength)), strength * 256); + } +} + +/** + * @brief Driver-free DualShock 4 touch report and pointer state. + */ +class WindowsTouchTest: public testing::Test { +private: + DS4_REPORT_EX report_ {}; ///< Current device report. + std::map pointers_; ///< Allocated client pointer IDs. + std::byte available_ {0x3}; ///< Both touch slots start available. + +protected: + /** + * @brief Read the current DualShock 4 touch report. + * + * @return Const reference to the touch report. + */ + const DS4_TOUCH &report() const { + return report_.Report.sCurrentTouch; + } + + /** + * @brief Read the allocated client pointer IDs. + * + * @return Const reference to the pointer-to-slot mapping. + */ + const std::map &pointers() const { + return pointers_; + } + + /** + * @brief Read the mask of free touch slots. + * + * @return Byte mask of available slots. + */ + std::byte available() const { + return available_; + } + + /** + * @brief Set report counters before testing their byte wraparound. + */ + void prepare_counter_wraparound() { + report_.Report.sCurrentTouch.bPacketCounter = 0xFF; + report_.Report.sCurrentTouch.bIsUpTrackingNum1 = 0xFF; + } + + /** + * @brief Mark both report slots up before each test. + */ + void SetUp() override { + report_.Report.sCurrentTouch.bIsUpTrackingNum1 = 0x80; + report_.Report.sCurrentTouch.bIsUpTrackingNum2 = 0x80; + } + + /** + * @brief Apply a client touch event without submitting it to a driver. + * + * @param type Client event type. + * @param id Client pointer ID. + * @param x Normalized horizontal coordinate. + * @param y Normalized vertical coordinate. + * @return Whether the resulting report should be submitted. + */ + bool touch(std::uint8_t type, std::uint32_t id, float x = 0.0f, float y = 0.0f) { + return platf::vigem::update_touch(report_, pointers_, available_, {{0, 0}, type, id, x, y, 1.0f}); + } +}; + +TEST_F(WindowsTouchTest, AllocatesBothSlotsAndPacksCoordinatesInWireOrder) { + ASSERT_TRUE(touch(LI_TOUCH_EVENT_DOWN, 10, 1.0f, 1.0f)); + EXPECT_EQ(pointers().at(10), 0); + EXPECT_EQ(available(), std::byte {0x2}); + EXPECT_EQ(report().bIsUpTrackingNum1, 1); + const std::array first {0x80, 0xF7, 0x3A}; + EXPECT_TRUE(std::equal(first.begin(), first.end(), report().bTouchData1)); + + ASSERT_TRUE(touch(LI_TOUCH_EVENT_DOWN, 20, 0.5f, 0.5f)); + EXPECT_EQ(pointers().at(20), 1); + EXPECT_EQ(available(), std::byte {0}); + EXPECT_EQ(report().bIsUpTrackingNum2, 1); + const std::array second {0xC0, 0x73, 0x1D}; + EXPECT_TRUE(std::equal(second.begin(), second.end(), report().bTouchData2)); + EXPECT_EQ(report().bPacketCounter, 2); + + EXPECT_FALSE(touch(LI_TOUCH_EVENT_DOWN, 30)); + EXPECT_EQ(pointers().size(), 2U); + EXPECT_EQ(report().bPacketCounter, 2); +} + +TEST_F(WindowsTouchTest, MovesReleasesCancelsAndReusesTheCorrectSlot) { + ASSERT_TRUE(touch(LI_TOUCH_EVENT_DOWN, 10)); + ASSERT_TRUE(touch(LI_TOUCH_EVENT_DOWN, 20)); + ASSERT_TRUE(touch(LI_TOUCH_EVENT_MOVE, 20, 1.0f, 1.0f)); + EXPECT_EQ(report().bTouchData2[0], 0x80); + EXPECT_EQ(report().bTouchData1[0], 0); + EXPECT_EQ(available(), std::byte {0}); + + ASSERT_TRUE(touch(LI_TOUCH_EVENT_UP, 10)); + EXPECT_EQ(report().bIsUpTrackingNum1, 0x81); + EXPECT_EQ(available(), std::byte {0x1}); + EXPECT_FALSE(pointers().contains(10)); + + ASSERT_TRUE(touch(LI_TOUCH_EVENT_CANCEL, 20)); + EXPECT_EQ(report().bIsUpTrackingNum2, 0x81); + EXPECT_EQ(available(), std::byte {0x3}); + EXPECT_TRUE(pointers().empty()); + + ASSERT_TRUE(touch(LI_TOUCH_EVENT_DOWN, 30)); + EXPECT_EQ(pointers().at(30), 0); + EXPECT_EQ(report().bIsUpTrackingNum1, 2); +} + +TEST_F(WindowsTouchTest, CancelAllPreservesCoordinatesAndReleasesBothSlots) { + ASSERT_TRUE(touch(LI_TOUCH_EVENT_DOWN, 10, 1.0f, 1.0f)); + ASSERT_TRUE(touch(LI_TOUCH_EVENT_DOWN, 20, 0.5f, 0.5f)); + ASSERT_TRUE(touch(LI_TOUCH_EVENT_CANCEL_ALL, 0)); + EXPECT_EQ(report().bIsUpTrackingNum1, 0x81); + EXPECT_EQ(report().bIsUpTrackingNum2, 0x81); + EXPECT_EQ(report().bTouchData1[0], 0x80); + EXPECT_EQ(report().bTouchData2[0], 0xC0); + EXPECT_EQ(report().bPacketCounter, 3); + EXPECT_EQ(available(), std::byte {0x3}); + EXPECT_TRUE(pointers().empty()); +} + +TEST_F(WindowsTouchTest, RejectsUnknownPointersAndUnsupportedEventsWithoutUpdatingTheReport) { + EXPECT_FALSE(touch(LI_TOUCH_EVENT_MOVE, 10)); + EXPECT_FALSE(touch(LI_TOUCH_EVENT_UP, 10)); + ASSERT_TRUE(touch(LI_TOUCH_EVENT_DOWN, 10)); + EXPECT_FALSE(touch(0xFF, 10)); + EXPECT_EQ(report().bPacketCounter, 1); + EXPECT_EQ(report().bIsUpTrackingNum1, 1); + EXPECT_EQ(pointers().at(10), 0); +} + +TEST_F(WindowsTouchTest, PreservesByteCounterWraparound) { + prepare_counter_wraparound(); + ASSERT_TRUE(touch(LI_TOUCH_EVENT_DOWN, 10)); + EXPECT_EQ(report().bPacketCounter, 0); + EXPECT_EQ(report().bIsUpTrackingNum1, 0x80); +} + +/** + * @brief Initial status, client state, percentage, and expected status and level. + */ +using BatteryParam = std::tuple; + +/** + * @brief Battery report cases including special status and unknown percentage. + */ +struct WindowsBatteryTest: testing::TestWithParam {}; + +TEST_P(WindowsBatteryTest, PreservesTheExpectedStatusAndBatteryLevelBytes) { + const auto &[initial, state, percentage, expected_status, expected_level] = GetParam(); + DS4_REPORT_EX report {}; + report.Report.bBatteryLvlSpecial = initial; + report.Report.bBatteryLvl = 42; + platf::vigem::update_battery(report, {{0, 0}, state, percentage}); + EXPECT_EQ(report.Report.bBatteryLvlSpecial, expected_status); + EXPECT_EQ(report.Report.bBatteryLvl, expected_level); +} + +INSTANTIATE_TEST_SUITE_P( + BatteryStates, + WindowsBatteryTest, + testing::Values( + BatteryParam {0x1F, LI_BATTERY_STATE_CHARGING, LI_BATTERY_PERCENTAGE_UNKNOWN, 0x15, 42}, + BatteryParam {0x1F, LI_BATTERY_STATE_DISCHARGING, LI_BATTERY_PERCENTAGE_UNKNOWN, 0x05, 42}, + BatteryParam {0x1F, LI_BATTERY_STATE_CHARGING, 75, 0x18, 191}, + BatteryParam {0x1F, LI_BATTERY_STATE_DISCHARGING, 75, 0x05, 191}, + BatteryParam {0x13, LI_BATTERY_STATE_CHARGING, 50, 0x15, 127}, + BatteryParam {0x13, LI_BATTERY_STATE_DISCHARGING, LI_BATTERY_PERCENTAGE_UNKNOWN, 0x03, 42}, + BatteryParam {0x13, LI_BATTERY_STATE_FULL, LI_BATTERY_PERCENTAGE_UNKNOWN, 0x1B, 255}, + BatteryParam {0x13, LI_BATTERY_STATE_NOT_PRESENT, LI_BATTERY_PERCENTAGE_UNKNOWN, 0x1F, 42}, + BatteryParam {0x13, LI_BATTERY_STATE_NOT_CHARGING, 50, 0x1F, 127}, + BatteryParam {0x13, LI_BATTERY_STATE_UNKNOWN, LI_BATTERY_PERCENTAGE_UNKNOWN, 0x13, 42}, + BatteryParam {0x03, LI_BATTERY_STATE_UNKNOWN, 50, 0x03, 127}, + BatteryParam {0x1B, LI_BATTERY_STATE_UNKNOWN, 75, 0x1B, 191}, + BatteryParam {0x13, LI_BATTERY_STATE_UNKNOWN, 0, 0x10, 0}, + BatteryParam {0x13, LI_BATTERY_STATE_UNKNOWN, 4, 0x10, 10}, + BatteryParam {0x13, LI_BATTERY_STATE_UNKNOWN, 5, 0x11, 12}, + BatteryParam {0x13, LI_BATTERY_STATE_UNKNOWN, 95, 0x1A, 242}, + BatteryParam {0x13, LI_BATTERY_STATE_UNKNOWN, 100, 0x1A, 255} + ) +); +#endif diff --git a/tests/unit/platform/windows/test_nvprefs_undo_data.cpp b/tests/unit/platform/windows/test_nvprefs_undo_data.cpp new file mode 100644 index 00000000000..59721347b58 --- /dev/null +++ b/tests/unit/platform/windows/test_nvprefs_undo_data.cpp @@ -0,0 +1,72 @@ +/** + * @file tests/unit/platform/windows/test_nvprefs_undo_data.cpp + * @brief Regression coverage for absent and zero NVIDIA preference restore values. + */ + +// test includes +#include "../../../tests_common.h" + +#ifdef _WIN32 + + // standard includes + #include + + // lib includes + #include + + // local includes + #include "src/platform/windows/nvprefs/undo_data.h" + #include "src/platform/windows/nvprefs/undo_file.h" + +static_assert(std::is_same_v (*)(const std::filesystem::path &, bool &)>); +static_assert(std::is_same_v (*)(const std::filesystem::path &)>); + +/** + * @brief Input-only paths preserve creation, sharing-error, and missing-file behavior. + */ +TEST_F(BaseTest, NvPrefsUndoFilesAcceptConstPathsAndReportOpenFailures) { + const auto path = std::filesystem::path {SUNSHINE_TEST_BIN_DIR} / "nvprefs_const_path_test.json"; + std::filesystem::remove(path); + auto cleanup = util::fail_guard([&path] { + std::error_code ignored; + std::filesystem::remove(path, ignored); + }); + + { + const auto created = nvprefs::undo_file_t::create_new_file(path); + ASSERT_TRUE(created.has_value()); + bool access_denied = false; + EXPECT_FALSE(nvprefs::undo_file_t::open_existing_file(path, access_denied).has_value()); + EXPECT_TRUE(access_denied); + } + + bool access_denied = true; + auto reopened = nvprefs::undo_file_t::open_existing_file(path, access_denied); + ASSERT_TRUE(reopened.has_value()); + EXPECT_FALSE(access_denied); + reopened.reset(); + EXPECT_FALSE(nvprefs::undo_file_t::create_new_file(path).has_value()); + + ASSERT_TRUE(std::filesystem::remove(path)); + EXPECT_FALSE(nvprefs::undo_file_t::open_existing_file(path, access_denied).has_value()); + EXPECT_FALSE(access_denied); +} + +/** + * @brief Serialization preserves the distinction between zero and an absent driver setting. + */ +TEST(NvPrefsUndoDataTest, PreservesZeroAndAbsentRestoreValues) { + for (const auto value : {std::optional {}, std::optional {0}, std::optional {7}}) { + nvprefs::undo_data_t original; + original.set_opengl_swapchain(1, value); + const auto serialized = original.write(); + nvprefs::undo_data_t restored; + restored.read(std::vector {serialized.begin(), serialized.end()}); + const auto setting = restored.get_opengl_swapchain(); + ASSERT_TRUE(setting.has_value()); + EXPECT_EQ(setting->our_value, 1); + EXPECT_EQ(setting->undo_value, value); + } +} + +#endif diff --git a/tests/unit/test_audio.cpp b/tests/unit/test_audio.cpp index 851f250d47a..88031dc9adc 100644 --- a/tests/unit/test_audio.cpp +++ b/tests/unit/test_audio.cpp @@ -8,6 +8,12 @@ // local includes #include +#include +#include + +namespace audio { + void encodeThread(std::shared_ptr>> samples, safe::mail_raw_t::queue_t packets, config_t config, stream::session_t *channel_data); +} using namespace audio; @@ -26,6 +32,29 @@ TEST(AudioLayoutTest, CustomMappingRemainsContiguousAndUsesAllEightChannels) { EXPECT_EQ(stream_configs[HIGH_SURROUND71].channelCount, 8); } +TEST(AudioPacketSessionTest, EncoderPreservesTheReceivingSession) { + stream::config_t stream_config {}; + rtsp_stream::launch_session_t launch_session {}; + launch_session.gcm_key.resize(16); + launch_session.iv.resize(16); + auto session = stream::session::alloc(stream_config, launch_session); + ASSERT_NE(session.get(), nullptr); + config_t config {.packetDuration = 5, .channels = 2, .mask = 0x3}; + auto samples = std::make_shared>>(); + auto packets = mail::man->queue(mail::audio_packets); + std::jthread worker([&samples, &packets, &config, &session] { + audio::encodeThread(samples, packets, config, session.get()); + }); + auto stop_worker = util::fail_guard([&samples] { + samples->stop(); + }); + ASSERT_TRUE(samples->raise(std::vector(480, 0.0F))); + auto packet = packets->pop(2s); + ASSERT_TRUE(packet); + EXPECT_EQ(packet->first, session.get()); + EXPECT_GT(packet->second.size(), 0U); +} + struct AudioTest: PlatformTestSuite, testing::WithParamInterface, config_t>> { void SetUp() override { BaseTest::SetUp(); @@ -60,7 +89,7 @@ INSTANTIATE_TEST_SUITE_P( ); TEST_P(AudioTest, TestEncode) { - std::jthread timer([&] { + std::jthread timer([this] { // Terminate the audio capture after 100 ms std::this_thread::sleep_for(100ms); const auto shutdown_event = m_mail->event(mail::shutdown); @@ -68,7 +97,7 @@ TEST_P(AudioTest, TestEncode) { shutdown_event->raise(true); audio_packets->stop(); }); - std::jthread capture([&] { + std::jthread capture([this] { const auto packets = m_mail->queue(mail::audio_packets); const auto shutdown_event = m_mail->event(mail::shutdown); while (const auto packet = packets->pop()) { diff --git a/tests/unit/test_cbs.cpp b/tests/unit/test_cbs.cpp new file mode 100644 index 00000000000..cd70525f8c6 --- /dev/null +++ b/tests/unit/test_cbs.cpp @@ -0,0 +1,116 @@ +/** + * @file tests/unit/test_cbs.cpp + * @brief Regression tests for typed H.264 and HEVC parameter-set serialization. + */ + +// test includes +#include "../tests_common.h" + +// standard includes +#include +#include +#include +#include + +// local includes +#include +#include + +namespace { + /** + * @brief Release a packet allocated by FFmpeg. + * @param packet Packet whose encoded payload is released. + */ + void release_packet(AVPacket *packet) { + av_packet_free(&packet); + } + + /** + * @brief Verify parameter-set rewriting with real software-encoder output. + */ + struct CodedBitstreamTest: testing::TestWithParam {}; +} // namespace + +TEST_P(CodedBitstreamTest, RewrittenParameterSetsRetainValidVuiMetadata) { + const auto codec_id = GetParam(); + const auto *codec = avcodec_find_encoder_by_name(codec_id == AV_CODEC_ID_H264 ? "libx264" : "libx265"); + ASSERT_NE(codec, nullptr); + video::avcodec_ctx_t context {avcodec_alloc_context3(codec)}; + ASSERT_NE(context.get(), nullptr); + context->width = 64; + context->height = 64; + context->pix_fmt = AV_PIX_FMT_YUV420P; + context->time_base = {1, 30}; + context->framerate = {30, 1}; + context->gop_size = 1; + context->max_b_frames = 0; + context->refs = 1; + context->color_range = AVCOL_RANGE_MPEG; + context->color_primaries = AVCOL_PRI_BT709; + context->color_trc = AVCOL_TRC_BT709; + context->colorspace = AVCOL_SPC_BT709; + + AVDictionary *options = nullptr; + auto free_options = util::fail_guard([&options] { + av_dict_free(&options); + }); + av_dict_set(&options, "preset", "ultrafast", 0); + av_dict_set(&options, "tune", "zerolatency", 0); + if (codec_id == AV_CODEC_ID_H264) { + av_dict_set(&options, "x264-params", "repeat-headers=1:threads=1", 0); + } else { + av_dict_set(&options, "x265-params", "repeat-headers=1:pools=none:frame-threads=1", 0); + } + ASSERT_EQ(avcodec_open2(context.get(), codec, &options), 0); + + video::avcodec_frame_t frame {av_frame_alloc()}; + ASSERT_NE(frame.get(), nullptr); + frame->width = context->width; + frame->height = context->height; + frame->format = context->pix_fmt; + frame->pts = 0; + ASSERT_EQ(av_frame_get_buffer(frame.get(), 0), 0); + for (int plane = 0; plane < 3; ++plane) { + const auto height = plane == 0 ? frame->height : frame->height / 2; + std::fill_n(frame->data[plane], frame->linesize[plane] * height, plane == 0 ? 16 : 128); + } + ASSERT_EQ(avcodec_send_frame(context.get(), frame.get()), 0); + util::safe_ptr packet {av_packet_alloc()}; + ASSERT_NE(packet.get(), nullptr); + ASSERT_EQ(avcodec_receive_packet(context.get(), packet.get()), 0); + ASSERT_TRUE(cbs::validate_sps(packet.get(), codec_id)); + + const std::vector original_packet(packet->data, packet->data + packet->size); + std::vector rewritten(packet->data, packet->data + packet->size); + const auto replace_unit = [&rewritten](const cbs::nal_t &unit) { + const auto original = std::ranges::search(rewritten, unit.old); + if (original.empty()) { + return false; + } + const auto position = rewritten.erase(original.begin(), original.end()); + rewritten.insert(position, unit._new.begin(), unit._new.end()); + return true; + }; + if (codec_id == AV_CODEC_ID_H264) { + const auto result = cbs::make_sps_h264(context.get(), packet.get()); + ASSERT_GT(result.sps.old.size(), 0U); + ASSERT_GT(result.sps._new.size(), 0U); + ASSERT_TRUE(replace_unit(result.sps)); + } else { + const auto result = cbs::make_sps_hevc(context.get(), packet.get()); + ASSERT_GT(result.vps.old.size(), 0U); + ASSERT_GT(result.vps._new.size(), 0U); + ASSERT_GT(result.sps.old.size(), 0U); + ASSERT_GT(result.sps._new.size(), 0U); + ASSERT_TRUE(replace_unit(result.vps)); + ASSERT_TRUE(replace_unit(result.sps)); + } + EXPECT_TRUE(std::ranges::equal(original_packet, std::span(packet->data, packet->size))); + util::safe_ptr rewritten_packet {av_packet_alloc()}; + ASSERT_NE(rewritten_packet.get(), nullptr); + ASSERT_EQ(av_new_packet(rewritten_packet.get(), static_cast(rewritten.size())), 0); + std::ranges::copy(rewritten, rewritten_packet->data); + EXPECT_TRUE(cbs::validate_sps(rewritten_packet.get(), codec_id)); +} + +INSTANTIATE_TEST_SUITE_P(SoftwareCodecs, CodedBitstreamTest, testing::Values(AV_CODEC_ID_H264, AV_CODEC_ID_HEVC)); diff --git a/tests/unit/test_config.cpp b/tests/unit/test_config.cpp index 35b2ed71bb5..89cf41b8c3d 100644 --- a/tests/unit/test_config.cpp +++ b/tests/unit/test_config.cpp @@ -20,6 +20,45 @@ using namespace std::literals; +/** + * @brief Preparation commands own their command strings and copy privilege settings. + */ +TEST(ConfigPrepCommandTest, PreservesCommandsAndElevationWithBothConstructors) { + const config::prep_cmd_t paired {"launch"s, "undo"s, true}; + EXPECT_EQ(paired.do_cmd, "launch"); + EXPECT_EQ(paired.undo_cmd, "undo"); + EXPECT_TRUE(paired.elevated); + + const config::prep_cmd_t single {"launch-only"s, false}; + EXPECT_EQ(single.do_cmd, "launch-only"); + EXPECT_TRUE(single.undo_cmd.empty()); + EXPECT_FALSE(single.elevated); +} + +TEST(ConfigParsingTest, ParsedEntriesOwnKeysAndSupportStringViewLookup) { + std::string input = "encoder = software\nencoder = nvenc\n"; + const auto entries = config::parse_config(input); + input.assign(input.size(), 'x'); + + const std::string padded = "__encoder__"; + const auto key = std::string_view {padded}.substr(2, 7); + const auto found = entries.find(key); + ASSERT_NE(found, entries.end()); + EXPECT_EQ(found->second, "software"); + EXPECT_FALSE(entries.contains("missing"sv)); + EXPECT_EQ(entries.size(), 1); +} + +/** + * @brief Duplicate configuration keys retain their first nonempty entry. + */ +TEST(ConfigParsingTest, DuplicateKeysPreserveFirstValueAfterIgnoredEmptyEntry) { + const auto entries = config::parse_config("encoder = software\nencoder = nvenc\noutput_name =\noutput_name = later\noutput_name = ignored\n"); + EXPECT_EQ(entries.at("encoder"), "software"); + EXPECT_EQ(entries.at("output_name"), "later"); + EXPECT_EQ(entries.size(), 2); +} + TEST(ConfigDefaultsTest, UsesAutomaticGamepadSelection) { EXPECT_EQ(config::input.gamepad, "auto"); } @@ -28,6 +67,19 @@ TEST(ConfigDefaultsTest, TreatsFp16SdrCapturesAsLinear) { EXPECT_FALSE(config::video.fp16_sdr_gamma_encoded); } +TEST(ConfigDefaultsTest, PreservesAmdEncoderNumericDefaults) { + EXPECT_EQ(config::video.amd.amd_usage_h264, 1); + EXPECT_EQ(config::video.amd.amd_usage_hevc, 1); + EXPECT_EQ(config::video.amd.amd_usage_av1, 2); + EXPECT_EQ(config::video.amd.amd_rc_h264, 3); + EXPECT_EQ(config::video.amd.amd_rc_hevc, 1); + EXPECT_EQ(config::video.amd.amd_rc_av1, 1); + EXPECT_EQ(config::video.amd.amd_quality_h264, 0); + EXPECT_EQ(config::video.amd.amd_quality_hevc, 5); + EXPECT_EQ(config::video.amd.amd_quality_av1, 70); + EXPECT_EQ(config::video.amd.amd_coder, 0); +} + using NvencPresetNameParam = std::pair; /** @@ -168,6 +220,19 @@ TEST_F(ConfigPersistenceTest, ParsesFp16SdrGammaEncodedOption) { EXPECT_FALSE(config::video.fp16_sdr_gamma_encoded); } +TEST_F(ConfigPersistenceTest, ParsesHexIntegersWiderThanAnInt) { + const auto original_video = config::video; + const auto restore_video = util::fail_guard([original_video]() { + config::video = original_video; + }); + const auto apps_file = config_file().parent_path() / "sunshine_test_config_apps.json"; + ASSERT_EQ(file_handler::write_file(apps_file.string().c_str(), "{}"), 0); + config::stream.file_apps = apps_file.string(); + + config::apply_config_for_test("min_threads = 0x1234567890\n"); + EXPECT_EQ(config::video.min_threads, 0x34567890); +} + TEST_F(ConfigPersistenceTest, ParsesFecPercentagesAndRejectsOutOfRangeValues) { const auto original_stream = config::stream; const auto restore_stream = util::fail_guard([original_stream]() { diff --git a/tests/unit/test_confighttp.cpp b/tests/unit/test_confighttp.cpp index 3d6688184ff..6aaf9f11c1a 100644 --- a/tests/unit/test_confighttp.cpp +++ b/tests/unit/test_confighttp.cpp @@ -215,19 +215,19 @@ class ConfigHttpTest: public BaseTest { // NOSONAR(cpp:S3656): protected member // Add a route to test not_found server->resource["^/notfound-test$"]["GET"] = []( const std::shared_ptr::Response> &response, - const std::shared_ptr::Request> &request + const std::shared_ptr::Request> & ) { // Call the actual confighttp::not_found function - confighttp::not_found(response, request, "Test not found"); + confighttp::not_found(response, "Test not found"); }; // Add a route to test bad_request server->resource["^/badrequest-test$"]["GET"] = []( const std::shared_ptr::Response> &response, - const std::shared_ptr::Request> &request + const std::shared_ptr::Request> & ) { // Call the actual confighttp::bad_request function - confighttp::bad_request(response, request, "Test bad request"); + confighttp::bad_request(response, "Test bad request"); }; // Add a route to test send_response with JSON diff --git a/tests/unit/test_constructor_contracts.cpp b/tests/unit/test_constructor_contracts.cpp new file mode 100644 index 00000000000..0926c3cc244 --- /dev/null +++ b/tests/unit/test_constructor_contracts.cpp @@ -0,0 +1,196 @@ +/** + * @file tests/unit/test_constructor_contracts.cpp + * @brief Tests for explicit conversions, ownership transfers, and member initialization. + */ + +// standard includes +#include +#include +#include +#include +#include +#include +#include + +// lib includes +#include + +// local includes +#include "src/crypto.h" +#include "src/move_by_copy.h" +#include "src/platform/common.h" +#include "src/sync.h" +#include "src/task_pool.h" +#include "src/thread_safe.h" +#include "src/utility.h" + +#ifdef _WIN32 + #include "src/platform/windows/display.h" +#endif + +namespace { + using callback_t = std::function; ///< Callable used by task and cleanup wrappers. + using int_ptr_t = util::uniq_ptr>; ///< Integer owned by a Sunshine unique pointer. + using shared_ptr_t = safe::shared_t::ptr_t; ///< Reference to shared integer storage. + using move_wrapper_t = move_by_copy_util::MoveByCopy>; ///< Copy-shaped owner of a move-only integer. + + static_assert(std::is_constructible_v); + static_assert(!std::is_convertible_v); + static_assert(std::is_convertible_v); + static_assert(std::is_convertible_v, int_ptr_t>); + static_assert(!std::is_convertible_v>); + static_assert(!std::is_convertible_v, util::wrap_ptr>); + static_assert(!std::is_convertible_v>); + static_assert(!std::is_convertible_v>); + static_assert(!std::is_convertible_v>); + static_assert(!std::is_convertible_v>>); + static_assert(!std::is_convertible_v); + static_assert(!std::is_convertible_v>); + static_assert(!std::is_convertible_v); + static_assert(!std::is_convertible_v); + static_assert(!std::is_convertible_v); + static_assert(!std::is_convertible_v); + + /** + * @brief Polymorphic resource used to check ownership-preserving pointer conversions. + */ + struct base_t { + virtual ~base_t() = default; + int value {42}; ///< Value retained when ownership is moved to a base pointer. + }; + + /** + * @brief Derived resource that can be owned through its base class. + */ + struct derived_t: base_t {}; + + using base_ptr_t = util::uniq_ptr>; ///< Base-class owner with a virtual destructor. + using derived_ptr_t = util::uniq_ptr>; ///< Compatible derived-class owner. + + static_assert(std::is_convertible_v); + static_assert(std::is_convertible_v>, base_ptr_t>); +} // namespace + +TEST(ConstructorContracts, EmptyUniquePointersHaveNoOwnedResource) { + int_ptr_t default_pointer; + int_ptr_t null_pointer = nullptr; + + EXPECT_EQ(default_pointer.get(), nullptr); + EXPECT_EQ(null_pointer.get(), nullptr); +} + +TEST(ConstructorContracts, ExplicitRawPointerConstructionTakesOwnership) { + auto original = std::make_unique(7); + const auto address = original.get(); + int_ptr_t pointer {original.release()}; + + EXPECT_EQ(pointer.get(), address); + EXPECT_EQ(*pointer, 7); +} + +TEST(ConstructorContracts, CompatibleUniquePointerMovesPreserveOwnership) { + std::unique_ptr> original {std::make_unique()}; + const auto address = original.get(); + derived_ptr_t derived = std::move(original); + base_ptr_t base = std::move(derived); + + EXPECT_EQ(original.get(), nullptr); + EXPECT_EQ(derived.get(), nullptr); + EXPECT_EQ(base.get(), address); + EXPECT_EQ(base->value, 42); +} + +TEST(ConstructorContracts, CompatibleStandardPointerMovesCanTargetBaseOwners) { + std::unique_ptr> original {std::make_unique()}; + const auto address = original.get(); + base_ptr_t base = std::move(original); + + EXPECT_EQ(original.get(), nullptr); + EXPECT_EQ(base.get(), address); +} + +TEST(ConstructorContracts, WrapPointersKeepBorrowedAndOwnedValuesAccessible) { + int borrowed_value = 7; + util::wrap_ptr borrowed {&borrowed_value}; + util::wrap_ptr owned {std::make_unique(42)}; + + EXPECT_EQ(*borrowed, 7); + EXPECT_EQ(*owned, 42); +} + +TEST(ConstructorContracts, MovingDisabledFailGuardsKeepsCleanupDisabled) { + int calls = 0; + { + auto original = util::fail_guard([&calls]() { + ++calls; + }); + original.disable(); + auto moved = std::move(original); + EXPECT_FALSE(moved.failure); + } + EXPECT_EQ(calls, 0); +} + +TEST(ConstructorContracts, ExplicitTaskConstructionRunsTheSuppliedCallable) { + int calls = 0; + task_pool_util::_Impl task {callback_t {[&calls]() { + ++calls; + }}}; + task.run(); + + EXPECT_EQ(calls, 1); +} + +TEST(ConstructorContracts, ExplicitSynchronizedConstructionKeepsTheInitialValue) { + sync_util::sync_t value {42}; + EXPECT_EQ(value.raw, 42); +} + +TEST(ConstructorContracts, ExplicitMoveWrapperConversionTransfersTheValue) { + auto original = std::make_unique(42); + auto wrapper = move_by_copy_util::cmove(original); + auto value = static_cast>(wrapper); + + EXPECT_EQ(original.get(), nullptr); + ASSERT_NE(value.get(), nullptr); + EXPECT_EQ(*value, 42); +} + +TEST(ConstructorContracts, SharedPointersSupportExplicitBooleanChecks) { + shared_ptr_t pointer; + + EXPECT_FALSE(static_cast(pointer)); +} + +TEST(ConstructorContracts, ExplicitCipherConstructionKeepsEncryptionWorking) { + const crypto::aes_t key {16}; + crypto::aes_t iv {16}; + constexpr std::string_view plaintext = "0123456789abcdef"; + crypto::cipher::ecb_t ecb {key, false}; + crypto::cipher::cbc_t cbc {key, false}; + crypto::cipher::gcm_t gcm {key, false}; + std::vector encrypted; + std::vector decrypted; + std::array cipher {}; + std::array tag {}; + + ASSERT_EQ(ecb.encrypt(plaintext, encrypted), 0); + const std::string ciphertext {encrypted.begin(), encrypted.end()}; + ASSERT_EQ(ecb.decrypt(ciphertext, decrypted), 0); + EXPECT_EQ((std::string {decrypted.begin(), decrypted.end()}), plaintext); + EXPECT_EQ(cbc.encrypt(plaintext, cipher.data(), &iv), plaintext.size()); + EXPECT_EQ(gcm.encrypt(plaintext, tag.data(), cipher.data(), &iv), plaintext.size()); +} + +#ifdef _WIN32 +TEST(ConstructorContracts, CursorViewportsStartEmptyWithTheFullDepthRange) { + platf::dxgi::gpu_cursor_t cursor; + + EXPECT_FLOAT_EQ(cursor.cursor_view.TopLeftX, 0.0f); + EXPECT_FLOAT_EQ(cursor.cursor_view.TopLeftY, 0.0f); + EXPECT_FLOAT_EQ(cursor.cursor_view.Width, 0.0f); + EXPECT_FLOAT_EQ(cursor.cursor_view.Height, 0.0f); + EXPECT_FLOAT_EQ(cursor.cursor_view.MinDepth, 0.0f); + EXPECT_FLOAT_EQ(cursor.cursor_view.MaxDepth, 1.0f); +} +#endif diff --git a/tests/unit/test_display_device.cpp b/tests/unit/test_display_device.cpp index 5bf999343b0..7044e64b9a3 100644 --- a/tests/unit/test_display_device.cpp +++ b/tests/unit/test_display_device.cpp @@ -7,6 +7,7 @@ #include "../tests_common.h" // standard includes +#include #include #include #include @@ -109,20 +110,23 @@ TEST_P(ParseConfigOption, IntegrationTest) { } 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), +/** + * @brief HDR parsing cases, including the platform-specific automatic policy. + */ +const auto hdr_option_cases = std::to_array({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), #ifdef __APPLE__ - std::make_pair(std::make_pair(hdr_option_e::automatic, client_wants_hdr_t {true}), std::nullopt), - std::make_pair(std::make_pair(hdr_option_e::automatic, client_wants_hdr_t {false}), std::nullopt) + std::make_pair(std::make_pair(hdr_option_e::automatic, client_wants_hdr_t {true}), std::nullopt), + std::make_pair(std::make_pair(hdr_option_e::automatic, client_wants_hdr_t {false}), std::nullopt) #else - 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) + 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) #endif - ) +}); + +INSTANTIATE_TEST_SUITE_P( + DisplayDeviceConfigTest, + ParseHdrOption, + testing::ValuesIn(hdr_option_cases) ); TEST_P(ParseHdrOption, IntegrationTest) { diff --git a/tests/unit/test_encode_device.cpp b/tests/unit/test_encode_device.cpp new file mode 100644 index 00000000000..f17bdc3141b --- /dev/null +++ b/tests/unit/test_encode_device.cpp @@ -0,0 +1,112 @@ +/** + * @file tests/unit/test_encode_device.cpp + * @brief Tests for typed AVCodec hardware device initialization. + */ + +// test includes +#include "../tests_common.h" + +// standard includes +#include +#include + +// local includes +#include +#include + +namespace video { + util::Either vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device); +#ifdef SUNSHINE_BUILD_VULKAN + util::Either vulkan_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device); +#endif +} // namespace video + +namespace { + /** + * @brief Fake backend that records initialization and supplies an FFmpeg buffer. + */ + struct tracked_encode_device_t: platf::avcodec_encode_device_t { + int calls = 0; ///< Number of backend initialization calls. + int status = 0; ///< Status returned by the fake backend. + AVBufferRef *created_buffer = nullptr; ///< Non-owning identity of the buffer returned to FFmpeg. + + tracked_encode_device_t() { + hardware = true; + init_hw_device = &initialize; + } + + /** + * @brief Initialize a tracked hardware buffer or return the configured error. + * @param device Fake encode device whose invocation is recorded. + * @param buffer Receives the hardware buffer on success. + * @return Configured failure status or zero on success. + */ + static int initialize(platf::avcodec_encode_device_t *device, AVBufferRef **buffer) { + auto *tracked = static_cast(device); + ++tracked->calls; + if (tracked->status) { + return tracked->status; + } + tracked->created_buffer = av_buffer_alloc(1); + *buffer = tracked->created_buffer; + return *buffer ? 0 : AVERROR(ENOMEM); + } + }; + + /** + * @brief Supported hardware initialization entry points under test. + */ + using initializer_t = util::Either (*)(platf::avcodec_encode_device_t *); + + /** + * @brief Hardware initialization entry points available in this build. + */ + constexpr std::array hardware_initializers { + &video::vaapi_init_avcodec_hardware_input_buffer, +#ifdef SUNSHINE_BUILD_VULKAN + &video::vulkan_init_avcodec_hardware_input_buffer, +#endif + }; + + /** + * @brief Exercise the same typed backend contract for each hardware entry point. + */ + struct EncodeDeviceInitializationTest: testing::TestWithParam {}; +} // namespace + +static_assert(std::is_same_v); +static_assert(std::is_same_v); + +TEST(EncodeDeviceStateTest, SoftwareDeviceDoesNotAdvertiseHardwareResources) { + platf::avcodec_encode_device_t device; + EXPECT_FALSE(device.hardware); + EXPECT_EQ(device.init_hw_device, nullptr); +#ifdef _WIN32 + EXPECT_EQ(device.d3d11_device, nullptr); +#endif +} + +TEST_P(EncodeDeviceInitializationTest, UsesBackendBufferWithoutOpeningAnotherDevice) { + tracked_encode_device_t device; + auto result = GetParam()(&device); + ASSERT_TRUE(result.has_left()); + ASSERT_NE(result.left().get(), nullptr); + EXPECT_EQ(result.left().get(), device.created_buffer); + EXPECT_EQ(device.calls, 1); +} + +TEST_P(EncodeDeviceInitializationTest, PropagatesBackendFailureWithoutUsingFallback) { + tracked_encode_device_t device; + device.status = AVERROR(EINVAL); + auto result = GetParam()(&device); + ASSERT_FALSE(result.has_left()); + EXPECT_EQ(result.right(), -1); + EXPECT_EQ(device.created_buffer, nullptr); + EXPECT_EQ(device.calls, 1); +} + +INSTANTIATE_TEST_SUITE_P( + HardwareBackends, + EncodeDeviceInitializationTest, + testing::ValuesIn(hardware_initializers) +); diff --git a/tests/unit/test_http_pairing.cpp b/tests/unit/test_http_pairing.cpp index 353180d6714..142e3d59fac 100644 --- a/tests/unit/test_http_pairing.cpp +++ b/tests/unit/test_http_pairing.cpp @@ -521,6 +521,21 @@ TEST_F(PairingHttpHandlerTest, ImmediateRequestsReturnExpectedStatus) { EXPECT_NE(request("/pair?uniqueid=out-of-order&clientchallenge=00"sv).find("Out of order"), std::string::npos); } +/** + * @brief HTTP dispatch reaches every pairing phase and rejects an unknown phase. + */ +TEST_F(PairingHttpHandlerTest, DispatchesEachPhaseAndRejectsUnknownRequests) { + for (const auto phase : {"clientchallenge", "serverchallengeresp", "clientpairingsecret", "unknown"}) { + std::string pairing_id; + ASSERT_EQ(insert_pair_session(pending_session(phase, "Client", "192.0.2.10"), pairing_id), pair_session_insert_e::ADDED); + const auto response = request(std::format("/pair?uniqueid={}&{}=00", phase, phase)); + EXPECT_NE(response.find("status_code=\"400\""), std::string::npos); + const auto expected = std::string_view {phase} == "unknown" ? "Invalid pairing request" : "Out of order"; + EXPECT_NE(response.find(expected), std::string::npos) << phase; + EXPECT_TRUE(get_pending_pairings().empty()); + } +} + TEST_F(PairingHttpHandlerTest, WebApprovalRequestRemainsPendingUntilCancelled) { std::packaged_task request_task {[this]() { return request(server_certificate_target("pending")); @@ -588,6 +603,47 @@ TEST_F(PairingHttpHandlerTest, DuplicateAndCapacityErrorsReturnImmediately) { EXPECT_NE(request(server_certificate_target("overflow")).find("status_code=\"503\""), std::string::npos); } +TEST_F(PairingHttpHandlerTest, RetriedGetservercertReplacesAbandonedHandshake) { + // First attempt: start pairing and approve the PIN, leaving the session mid-handshake. + std::packaged_task first_request_task {[this]() { + return request(server_certificate_target("retry")); + }}; + auto first_response = first_request_task.get_future(); + std::jthread first_request_thread {std::move(first_request_task)}; + + const auto first_pairing_id = wait_for_pending_pairing(); + ASSERT_FALSE(first_pairing_id.empty()); + config::stream.ping_timeout = std::chrono::seconds {10}; + std::packaged_task pin_task {[&first_pairing_id]() { + return pin(first_pairing_id, "5338", "Test client"); + }}; + auto pin_result = pin_task.get_future(); + std::jthread pin_thread {std::move(pin_task)}; + + // PIN approval answers the held request and advances the session past the certificate + // phase, so it no longer appears in the pending list. + EXPECT_NE(first_response.get().find("status_code=\"200\""), std::string::npos); + for (int attempt = 0; attempt < 100 && !get_pending_pairings().empty(); ++attempt) { + std::this_thread::sleep_for(std::chrono::milliseconds {10}); + } + EXPECT_TRUE(get_pending_pairings().empty()); + + // A fresh getservercert for the same client replaces the abandoned handshake instead + // of failing with 409 until the session expires. + std::packaged_task retry_request_task {[this]() { + return request(server_certificate_target("retry")); + }}; + auto retry_response = retry_request_task.get_future(); + std::jthread retry_request_thread {std::move(retry_request_task)}; + + const auto second_pairing_id = wait_for_pending_pairing(); + ASSERT_FALSE(second_pairing_id.empty()); + EXPECT_NE(second_pairing_id, first_pairing_id); + EXPECT_FALSE(pin_result.get()); // The evicted handshake's waiter is woken with failure. + EXPECT_TRUE(cancel_pairing(second_pairing_id)); + EXPECT_NE(retry_response.get().find("cancelled by operator"), std::string::npos); +} + TEST_F(PairingHttpHandlerTest, ConsolePinCompletesOrRejectsTheCertificatePhase) { setup(PRIVATE_KEY, PUBLIC_CERT); config::sunshine.flags[config::flag::PIN_STDIN] = true; diff --git a/tests/unit/test_keyboard.cpp b/tests/unit/test_keyboard.cpp index dd52fad5502..20c002cdb2b 100644 --- a/tests/unit/test_keyboard.cpp +++ b/tests/unit/test_keyboard.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,17 @@ #include "src/input.h" #include "src/platform/virtualhid_input.h" +namespace input { + /** + * @brief Execute one repeat callback directly for a deterministic keyboard test. + * + * @param key_code Client key code being repeated. + * @param flags Client packet flags. + * @param synthetic_modifiers Byte mask of synthetic modifiers around the repeated key. + */ + void repeat_key(std::uint16_t key_code, std::uint8_t flags, std::byte synthetic_modifiers); +} // namespace input + namespace { using namespace std::chrono_literals; @@ -576,7 +588,7 @@ TEST_F(KeyboardPassthroughTest, InjectsSyntheticModifiersTheClientHoldsWithoutSe {MODIFIER_ALT, VKEY_MENU}, }}; - for (unsigned combination = 1; combination < (1u << injectable.size()); ++combination) { + for (unsigned combination = 0; combination < (1u << injectable.size()); ++combination) { std::vector expected; unsigned claimed = 0; for (std::size_t index = 0; index < injectable.size(); ++index) { @@ -610,6 +622,29 @@ TEST_F(KeyboardPassthroughTest, NeverSynthesizesTheMetaModifier) { EXPECT_EQ(taken(), (std::vector {pressed(VKEY_A), released(VKEY_A)})); } +TEST_F(KeyboardPassthroughTest, RepeatsByteMaskedModifiersAndStopsAfterStateIsCleared) { + config::input.key_repeat_period = 1h; + constexpr auto synthetic_modifiers = std::byte {MODIFIER_SHIFT} | std::byte {MODIFIER_CTRL} | std::byte {MODIFIER_ALT}; + constexpr std::uint8_t flags = SS_KBE_FLAG_NON_NORMALIZED; + press(VKEY_A, std::to_integer(synthetic_modifiers) | MODIFIER_EXTENDED, flags); + taken_events(); + + input::repeat_key(VKEY_A, flags, synthetic_modifiers); + const auto events = taken_events(); + ASSERT_EQ(events.size(), 7U); + const std::array expected_keys {VKEY_SHIFT, VKEY_CONTROL, VKEY_MENU, VKEY_A, VKEY_SHIFT, VKEY_CONTROL, VKEY_MENU}; + for (std::size_t index = 0; index < events.size(); ++index) { + EXPECT_EQ(events[index].key_code, expected_keys[index]); + EXPECT_EQ(events[index].flags, flags); + EXPECT_EQ(events[index].release, index > 3); + } + EXPECT_TRUE(events[3].extended); + + input::testing::reset_keyboard_state(); + input::repeat_key(VKEY_A, flags, synthetic_modifiers); + EXPECT_TRUE(taken_events().empty()); +} + TEST_F(KeyboardPassthroughTest, PropagatesPacketFlagsToEverySyntheticEvent) { constexpr std::uint8_t flags = SS_KBE_FLAG_NON_NORMALIZED; diff --git a/tests/unit/test_logging.cpp b/tests/unit/test_logging.cpp index bd69c9159ae..376074d3d5e 100644 --- a/tests/unit/test_logging.cpp +++ b/tests/unit/test_logging.cpp @@ -127,8 +127,36 @@ TEST_F(LogRotationTest, ReportsFilesystemErrors) { EXPECT_EQ(read_log_file(log_path), "current"); } +TEST_F(LogRotationTest, FindsSubstringsAtBoundariesAndWithCaseFolding) { + write_log_file(log_path, "prefix middle suffix\nMiXeD\n\n"); + const auto path = log_path.string(); + EXPECT_TRUE(log_checker::line_contains(path, "prefix")); + EXPECT_TRUE(log_checker::line_contains(path, "middle")); + EXPECT_TRUE(log_checker::line_contains(path, "suffix")); + EXPECT_TRUE(log_checker::line_contains(path, "")); + EXPECT_TRUE(log_checker::line_contains(path, "mixed", true)); + EXPECT_FALSE(log_checker::line_contains(path, "mixed")); + EXPECT_FALSE(log_checker::line_contains(path, "absent")); +} + struct LogLevelsTest: BaseTest, testing::WithParamInterface {}; +/** + * @brief Periodic logger callbacks preserve their owner's message and numeric formatting. + */ +TEST(PeriodicLoggerTest, ReportsIntegralAndFloatingSamplesWithConfiguredUnits) { + logging::min_max_avg_periodic_logger integral {info, "Integral capture", "ms", std::chrono::seconds {-1}}; + logging::min_max_avg_periodic_logger floating {info, "Floating capture", "s", std::chrono::seconds {-1}}; + + integral.collect_and_log(3); + floating.collect_and_log(1.25); + integral.collect_and_log(7); + floating.collect_and_log(2.5); + + EXPECT_TRUE(log_checker::line_contains(log_file, "Integral capture (min/max/avg): 3ms/3ms/3.00ms")); + EXPECT_TRUE(log_checker::line_contains(log_file, "Floating capture (min/max/avg): 1.25s/1.25s/1.25s")); +} + INSTANTIATE_TEST_SUITE_P( Logging, LogLevelsTest, diff --git a/tests/unit/test_network.cpp b/tests/unit/test_network.cpp index f3a8987e1bc..7ac7fff1f48 100644 --- a/tests/unit/test_network.cpp +++ b/tests/unit/test_network.cpp @@ -9,6 +9,29 @@ // local includes #include +TEST(NetworkClassificationTest, DeducedNetworkListsPreserveAddressScopes) { + EXPECT_EQ(net::from_address("127.0.0.1"), net::PC); + EXPECT_EQ(net::from_address("192.168.1.2"), net::LAN); + EXPECT_EQ(net::from_address("100.64.0.1"), net::LAN); + EXPECT_EQ(net::from_address("169.254.1.2"), net::LAN); + EXPECT_EQ(net::from_address("8.8.8.8"), net::WAN); + EXPECT_EQ(net::from_address("::1"), net::PC); + EXPECT_EQ(net::from_address("fc00::1"), net::LAN); + EXPECT_EQ(net::from_address("fe80::1"), net::LAN); + EXPECT_EQ(net::from_address("2001:4860:4860::8888"), net::WAN); +} + +/** + * @brief Loopback and documentation addresses retain their classification when mapped to IPv6. + */ +TEST(NetworkClassificationTest, ClassifiesMappedIpv4AddressesWithoutChangingSubsequentLookups) { + for (const auto *address : {"127.0.0.1", "192.0.2.1", "198.51.100.2", "203.0.113.3"}) { + const auto expected = net::from_address(address); + EXPECT_EQ(net::from_address(std::string {"::ffff:"} + address), expected); + EXPECT_EQ(net::from_address(address), expected); + } +} + struct MdnsInstanceNameTest: BaseTest, testing::WithParamInterface> {}; TEST_P(MdnsInstanceNameTest, Run) { diff --git a/tests/unit/test_nvenc_dynamic_factory.cpp b/tests/unit/test_nvenc_dynamic_factory.cpp index 4b3637dca0e..e67be19924c 100644 --- a/tests/unit/test_nvenc_dynamic_factory.cpp +++ b/tests/unit/test_nvenc_dynamic_factory.cpp @@ -49,6 +49,7 @@ namespace { } void destroy_encoder() override { + // The fake encoder allocates no driver resources to release. } nvenc::nvenc_encoded_frame encode_frame(std::uint64_t frame_index, bool force_idr) override { @@ -74,6 +75,7 @@ namespace { return { reinterpret_cast(fake_handle_value), [](HMODULE) { + // The synthetic handle is not an owned module, so it must not be unloaded. }, }; } diff --git a/tests/unit/test_nvenc_utils.cpp b/tests/unit/test_nvenc_utils.cpp index e371254b35d..f59b8428ab2 100644 --- a/tests/unit/test_nvenc_utils.cpp +++ b/tests/unit/test_nvenc_utils.cpp @@ -1,19 +1,237 @@ /** * @file tests/unit/test_nvenc_utils.cpp - * @brief Tests for Sunshine pixel format conversion in the NVENC SDK 13.1 implementation. + * @brief Tests for NVENC SDK 13.1 format conversion and native handle forwarding. */ #ifdef _WIN32 // standard includes #include + #include + #include + #include + #include + #include #include // lib includes #include // local includes + #include "src/nvenc/nvenc_base.h" + #include "src/nvenc/nvenc_d3d11.h" #include "src/nvenc/nvenc_utils.h" +namespace { + using namespace nvenc_1301; + + /** + * @brief Driver observations used to verify opaque encoder-session cleanup. + */ + struct driver_state_t { + NVENCSTATUS open_status = NV_ENC_SUCCESS; ///< Status returned by the fake session-open callback. + std::uintptr_t opened_device = 0; ///< Address of the native device forwarded to NVENC. + int opens = 0; ///< Session-open calls observed by the driver. + int destructions = 0; ///< Session-destroy calls observed by the driver. + GUID selected_preset {}; ///< Preset GUID forwarded to the driver. + int preset_queries = 0; ///< Preset configuration calls observed by the driver. + }; + + driver_state_t driver; ///< Fake driver state and opaque session identity. + + /** + * @brief Return an opaque session and record the underlying native device. + * @param params NVENC parameters supplied by the base encoder. + * @param encoder Receives the fake driver's opaque session identity. + * @return Configured session-open status. + */ + NVENCSTATUS NVENCAPI open_session(NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS *params, void **encoder) { // NOSONAR(cpp:S5008): NVENC's session-open callback requires a void double pointer for its opaque encoder handle. + driver.opened_device = std::bit_cast(params->device); + ++driver.opens; + *encoder = &driver; + return driver.open_status; + } + + /** + * @brief Stop initialization after the session has been opened. + * @param encoder Opaque session supplied by NVENC. + * @param count Receives the number of supported codec GUIDs. + * @return An error that forces the base encoder to release the session. + */ + NVENCSTATUS NVENCAPI fail_guid_count(void *encoder, std::uint32_t *count) { // NOSONAR(cpp:S5008): NVENC's GUID-count callback requires a void pointer for its opaque encoder handle. + EXPECT_EQ(encoder, &driver); + *count = 0; + return NV_ENC_ERR_GENERIC; + } + + /** + * @brief Verify and release the opaque session returned by the fake driver. + * @param encoder Opaque session identity being destroyed. + * @return Success after recording the destruction. + */ + NVENCSTATUS NVENCAPI destroy_session(void *encoder) { // NOSONAR(cpp:S5008): NVENC's session-destroy callback requires a void pointer for its opaque encoder handle. + EXPECT_EQ(encoder, &driver); + ++driver.destructions; + return NV_ENC_SUCCESS; + } + + /** + * @brief Base encoder with a fake driver and a typed D3D11 or CUDA device. + */ + class fake_session_encoder_t: public nvenc_base { + public: + /** + * @brief Install fake driver callbacks for one native device type. + * @param cuda Whether the test uses a CUDA context instead of a D3D11 device. + * @param observe_preset Whether the driver should continue through preset selection. + * @note Unused session parameter types are inferred from the SDK callback signatures. + */ + explicit fake_session_encoder_t(bool cuda, bool observe_preset = false): + nvenc_base(cuda ? NV_ENC_DEVICE_TYPE_CUDA : NV_ENC_DEVICE_TYPE_DIRECTX) { + nvenc = std::make_shared(); + nvenc->nvEncOpenEncodeSessionEx = &open_session; + nvenc->nvEncGetEncodeGUIDCount = &fail_guid_count; + nvenc->nvEncDestroyEncoder = &destroy_session; + if (observe_preset) { + nvenc->nvEncGetEncodeGUIDCount = [](auto *, std::uint32_t *count) { + *count = 1; + return NV_ENC_SUCCESS; + }; + nvenc->nvEncGetEncodeGUIDs = [](auto *, GUID *guids, std::uint32_t, std::uint32_t *count) { + *guids = NV_ENC_CODEC_H264_GUID; + *count = 1; + return NV_ENC_SUCCESS; + }; + nvenc->nvEncGetEncodeCaps = [](auto *, GUID, NV_ENC_CAPS_PARAM *params, int *value) { + *value = params->capsToQuery == NV_ENC_CAPS_WIDTH_MAX || params->capsToQuery == NV_ENC_CAPS_HEIGHT_MAX ? 4096 : 0; + return NV_ENC_SUCCESS; + }; + nvenc->nvEncGetEncodePresetConfigEx = [](auto *, GUID, GUID preset, NV_ENC_TUNING_INFO, NV_ENC_PRESET_CONFIG *) { + driver.selected_preset = preset; + ++driver.preset_queries; + return NV_ENC_ERR_GENERIC; + }; + } + constexpr std::uintptr_t device_address = 0x1234; + if (cuda) { + device = std::bit_cast(device_address); + } else { + device = std::bit_cast(device_address); + } + } + + protected: + bool init_library() override { + return false; + } + + bool create_and_register_input_buffer() override { + ADD_FAILURE() << "Initialization should stop before registering an input buffer"; + return false; + } + }; + + /** + * @brief Device type and session-open status exercised by the fake driver. + */ + struct NvencNativeHandleTest: testing::TestWithParam> {}; + + /** + * @brief Native encoder exposing its typed completion event for lifetime checks. + */ + class completion_event_encoder_t: public nvenc_d3d11 { + public: + completion_event_encoder_t(): + nvenc_d3d11(NV_ENC_DEVICE_TYPE_DIRECTX, {}) { + } + + ID3D11Texture2D *get_input_texture() override { + return nullptr; + } + + /** + * @brief Borrow the event registered for asynchronous encoding. + * @return Native completion event owned by this encoder. + */ + completion_event_t *completion_event() const { + return async_event_handle; + } + + protected: + bool create_and_register_input_buffer() override { + return false; + } + }; +} // namespace + +/** + * @brief Preset selection forwards every supported GUID and both boundary fallbacks to the driver. + */ +TEST(NvencPresetTest, PreservesGuidMappingsAndBoundaryFallbacks) { + using namespace nvenc_1301; + const std::array cases { + std::pair {0, NV_ENC_PRESET_P1_GUID}, + std::pair {1, NV_ENC_PRESET_P1_GUID}, + std::pair {2, NV_ENC_PRESET_P2_GUID}, + std::pair {3, NV_ENC_PRESET_P3_GUID}, + std::pair {4, NV_ENC_PRESET_P4_GUID}, + std::pair {5, NV_ENC_PRESET_P5_GUID}, + std::pair {6, NV_ENC_PRESET_P6_GUID}, + std::pair {7, NV_ENC_PRESET_P7_GUID}, + std::pair {8, NV_ENC_PRESET_P7_GUID}, + std::pair {-1, NV_ENC_PRESET_P7_GUID}, + std::pair {std::numeric_limits::max(), NV_ENC_PRESET_P7_GUID}, + }; + for (const auto &[number, expected] : cases) { + driver = {}; + fake_session_encoder_t encoder {false, true}; + nvenc::nvenc_config config {}; + config.quality_preset = number; + video::config_t client_config {}; + client_config.width = 64; + client_config.height = 64; + const auto colorspace = video::colorspace_from_client_config(client_config, false); + EXPECT_FALSE(encoder.create_encoder(config, client_config, colorspace, platf::pix_fmt_e::nv12)); + EXPECT_EQ(driver.preset_queries, 1); + EXPECT_EQ(std::memcmp(&driver.selected_preset, &expected, sizeof(GUID)), 0) << number; + EXPECT_EQ(driver.destructions, 1); + } +} + +TEST(NvencCompletionEventTest, TypedEventCanBeSignaledAndIsClosedWithTheEncoder) { + nvenc_1301::completion_event_t *event = nullptr; + { + completion_event_encoder_t encoder; + event = encoder.completion_event(); + ASSERT_NE(event, nullptr); + EXPECT_EQ(WaitForSingleObject(event, 0), WAIT_TIMEOUT); + ASSERT_TRUE(SetEvent(event)); + EXPECT_EQ(WaitForSingleObject(event, 0), WAIT_OBJECT_0); + } + EXPECT_EQ(WaitForSingleObject(event, 0), WAIT_FAILED); + EXPECT_EQ(GetLastError(), ERROR_INVALID_HANDLE); +} + +TEST_P(NvencNativeHandleTest, PreservesNativeDeviceAndReleasesReturnedSessionOnFailure) { + const auto &[cuda, status] = GetParam(); + driver = {}; + driver.open_status = status; + fake_session_encoder_t encoder {cuda}; + video::config_t client_config {}; + client_config.width = 64; + client_config.height = 64; + const auto colorspace = video::colorspace_from_client_config(client_config, false); + EXPECT_FALSE(encoder.create_encoder({}, client_config, colorspace, platf::pix_fmt_e::nv12)); + EXPECT_EQ(driver.opened_device, 0x1234U); + EXPECT_EQ(driver.opens, 1); + EXPECT_EQ(driver.destructions, 1); +} + +INSTANTIATE_TEST_SUITE_P( + DeviceTypesAndOpenStatuses, + NvencNativeHandleTest, + testing::Combine(testing::Bool(), testing::Values(NV_ENC_SUCCESS, NV_ENC_ERR_GENERIC)) +); + TEST(NvencFormatMappingTest, MapsSupportedFormatsAndRejectsUnsupportedOnes) { using enum platf::pix_fmt_e; using enum nvenc_1301::NV_ENC_BUFFER_FORMAT; diff --git a/tests/unit/test_nvhttp_client_auth.cpp b/tests/unit/test_nvhttp_client_auth.cpp index 96bfcae7e4b..6cf416dd8fb 100644 --- a/tests/unit/test_nvhttp_client_auth.cpp +++ b/tests/unit/test_nvhttp_client_auth.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include // local includes @@ -18,6 +19,8 @@ namespace fs = std::filesystem; +static_assert(std::is_same_v); + /** * @brief Isolate paired-client authorization tests from the user's Sunshine state. */ diff --git a/tests/unit/test_nvhttp_https.cpp b/tests/unit/test_nvhttp_https.cpp new file mode 100644 index 00000000000..3bb94e54ad6 --- /dev/null +++ b/tests/unit/test_nvhttp_https.cpp @@ -0,0 +1,160 @@ +/** + * @file tests/unit/test_nvhttp_https.cpp + * @brief Verify HTTPS callbacks retain the guard protecting server lifetime. + */ + +// test includes +#include "../tests_common.h" + +// standard includes +#include +#include +#include +#include +#include + +// local includes +#include +#include + +namespace { + using namespace std::chrono_literals; + using boost::asio::ip::tcp; + + /** + * @brief Run real HTTPS callbacks against an isolated loopback listener. + */ + struct HttpsHandlerLifetimeTest: BaseTest { + /** + * @brief Create credentials and start the production HTTPS accept handler. + */ + void SetUp() override { + BaseTest::SetUp(); + const auto credentials = crypto::gen_creds("Sunshine HTTPS lifetime test", 2048); + certificate_file = std::filesystem::path {SUNSHINE_TEST_BIN_DIR} / "https_lifetime_cert.pem"; + private_key_file = std::filesystem::path {SUNSHINE_TEST_BIN_DIR} / "https_lifetime_key.pem"; + std::ofstream {certificate_file} << credentials.x509; + std::ofstream {private_key_file} << credentials.pkey; + + server = nvhttp::test_support::make_https_server(certificate_file.string(), private_key_file.string(), handler_scope); + server->io_service = std::make_shared(); + server->config.address = "127.0.0.1"; + server->config.port = 0; + server->start([this](unsigned short assigned_port) { + port = assigned_port; + }); + server->io_service->poll(); + ASSERT_NE(port, 0); + } + + /** + * @brief Drain canceled callbacks and remove the generated credentials. + */ + void TearDown() override { + if (server) { + server->stop(); + server->io_service->restart(); + server->io_service->poll(); + server.reset(); + } + if (stop_thread.joinable()) { + stop_thread.join(); + } + std::error_code cleanup_error; + std::filesystem::remove(certificate_file, cleanup_error); + std::filesystem::remove(private_key_file, cleanup_error); + BaseTest::TearDown(); + } + + /** + * @brief Check that stopping the scope runner waits until the error callback exits. + * + * @param callback_error Error delivered by the real accept or TLS handshake operation. + */ + void check_handler_guard(const boost::system::error_code &callback_error) { + EXPECT_TRUE(callback_error); + ++error_callbacks; + std::promise stopping; + auto stopping_future = stopping.get_future(); + std::promise stopped; + stopped_future = stopped.get_future(); + stop_thread = std::jthread([this, stopping = std::move(stopping), stopped = std::move(stopped)]() mutable { + stopping.set_value(); + handler_scope->stop(); + stopped.set_value(); + }); + stopping_future.wait(); + EXPECT_EQ(stopped_future.wait_for(50ms), std::future_status::timeout); + } + + std::unique_ptr> server; ///< Server running production accept and handshake callbacks. + std::shared_ptr handler_scope; ///< Guard used by the server destructor. + std::jthread stop_thread; ///< Concurrent attempt to stop the handler scope. + std::future stopped_future; ///< Signals that all callback guards have been released. + unsigned short port = 0; ///< Assigned loopback port. + int error_callbacks = 0; ///< Number of errors delivered while testing a handler. + std::filesystem::path certificate_file; ///< Generated server certificate. + std::filesystem::path private_key_file; ///< Generated server private key. + }; + + /** + * @brief Canceling a pending accept holds the server guard through its error callback. + */ + TEST_F(HttpsHandlerLifetimeTest, AcceptErrorRetainsGuardUntilCallbackReturns) { + server->on_error = [this](auto, const auto &callback_error) { + check_handler_guard(callback_error); + }; + server->stop(); + server->io_service->run_for(2s); + ASSERT_EQ(error_callbacks, 1); + ASSERT_TRUE(stopped_future.valid()); + EXPECT_EQ(stopped_future.wait_for(2s), std::future_status::ready); + } + + /** + * @brief A failed TLS handshake holds the server guard through its error callback. + */ + TEST_F(HttpsHandlerLifetimeTest, HandshakeErrorRetainsGuardUntilCallbackReturns) { + server->on_error = [this](auto, const auto &callback_error) { + check_handler_guard(callback_error); + }; + boost::asio::io_context client_io; + tcp::socket client {client_io}; + client.connect({boost::asio::ip::address_v4::loopback(), port}); + client.close(); + server->io_service->run_for(2s); + ASSERT_EQ(error_callbacks, 1); + ASSERT_TRUE(stopped_future.valid()); + EXPECT_EQ(stopped_future.wait_for(2s), std::future_status::ready); + } + + /** + * @brief A stopped server scope prevents a canceled accept from dispatching an error. + */ + TEST_F(HttpsHandlerLifetimeTest, StoppedScopeSkipsAcceptCallback) { + server->on_error = [this](auto, const auto &) { + ++error_callbacks; + }; + handler_scope->stop(); + server->stop(); + server->io_service->run_for(2s); + EXPECT_EQ(error_callbacks, 0); + } + + /** + * @brief A stopped server scope prevents an outstanding handshake from dispatching an error. + */ + TEST_F(HttpsHandlerLifetimeTest, StoppedScopeSkipsHandshakeCallback) { + server->on_error = [this](auto, const auto &) { + ++error_callbacks; + }; + boost::asio::io_context client_io; + tcp::socket client {client_io}; + client.connect({boost::asio::ip::address_v4::loopback(), port}); + server->io_service->poll(); + handler_scope->stop(); + client.close(); + server->io_service->run_for(2s); + EXPECT_EQ(error_callbacks, 0); + } +} // namespace diff --git a/tests/unit/test_process.cpp b/tests/unit/test_process.cpp index 3cae66d6c1d..d8561d13e9b 100644 --- a/tests/unit/test_process.cpp +++ b/tests/unit/test_process.cpp @@ -15,9 +15,57 @@ // local includes #include +#include namespace fs = std::filesystem; +namespace proc { + /** + * @brief Resolve a command's working directory, returning an empty path for URLs. + * + * @param cmd Command line to inspect. + * @param env Environment used for executable lookup. + * @return Working directory, or an empty path when the command is a URL. + */ + boost::filesystem::path find_working_directory(const std::string &cmd, boost::process::v1::environment &env); + + /** + * @brief Expand environment references while preserving Windows variable-name casing. + * + * @param env Environment whose variables are expanded. + * @param val_raw Text containing variable references. + * @return Text with environment references expanded. + */ + std::string parse_env_val(boost::process::v1::native_environment &env, const std::string_view &val_raw); +} // namespace proc + +#ifdef _WIN32 +TEST(ProcessTest, ExpandsEnvironmentNamesWithoutChangingTheirCase) { + auto env = boost::this_process::environment(); + const std::string key = "SUNSHINE_RANGE_SEARCH_REGRESSION"; + const auto original = env.find(key); + const std::optional saved = original == env.end() ? std::nullopt : std::optional {original->to_string()}; + const auto restore = util::fail_guard([&env, &key, &saved]() { + if (saved.has_value()) { + env[key] = *saved; + } else { + env[key].clear(); + } + }); + env[key] = "case-preserved"; + + EXPECT_EQ(proc::parse_env_val(env, "before-$(sunshine_range_search_regression)-after"), "before-case-preserved-after"); + EXPECT_EQ(proc::parse_env_val(env, "$(SUNSHINE_RANGE_SEARCH_REGRESSION)"), "case-preserved"); +} +#endif + +TEST(ProcessTest, UrlCommandsDoNotResolveAnExecutableWorkingDirectory) { + boost::process::v1::environment env; + EXPECT_TRUE(proc::find_working_directory("https://example.com", env).empty()); + EXPECT_TRUE(proc::find_working_directory("steam://run/123", env).empty()); + EXPECT_TRUE(proc::find_working_directory("custom+scheme://host/path", env).empty()); +} + TEST(ProcessTest, PrepareCommand) { #ifdef SUNSHINE_BUILD_FLATPAK EXPECT_EQ(proc::prepare_command("steam"), "flatpak-spawn --host steam"); diff --git a/tests/unit/test_publish.cpp b/tests/unit/test_publish.cpp new file mode 100644 index 00000000000..7903dcbd4d4 --- /dev/null +++ b/tests/unit/test_publish.cpp @@ -0,0 +1,640 @@ +/** + * @file tests/unit/test_publish.cpp + * @brief Tests for Linux mDNS (Avahi) service publication. + */ + +// The publisher (src/platform/linux/publish.cpp) is built on every +// non-Apple Unix platform (see cmake/compile_definitions/linux.cmake), +// so the stubbed-registration tests below compile wherever it is built. +// The plain unit tests stay Linux-gated; only the fallback regression +// runs on non-Linux, where the address-change watch is unsupported. +#if !defined(_WIN32) && !defined(__APPLE__) + + // test includes + #include "../tests_common.h" + + // standard includes + #include + #include + #include + #include + #include + #include + + // local includes + #include + + #if defined(__linux__) + // Linux-only includes for the interface-enumeration tests. + #include + #include + #include + #endif + + #if defined(__linux__) +TEST(ServiceHostNameTest, IsDistinctFromDaemonHostName) { + // The SRV target must not be the daemon's shared host name: the daemon + // publishes both A and AAAA records for its own host name, so pointing the + // service at it would advertise an IPv6 endpoint an IPv4-only + // configuration does not serve (see #5860). + ASSERT_EQ(platf::publish::service_host_name("myhost", 0), "myhost-sunshine.local"); +} + +TEST(ServiceHostNameTest, SanitizesInvalidLabelCharacters) { + ASSERT_EQ(platf::publish::service_host_name("my host.example", 0), "my-host-sunshine.local"); + ASSERT_EQ(platf::publish::service_host_name("", 0), "sunshine-sunshine.local"); +} + +TEST(ServiceHostNameTest, CollisionSuffixChangesName) { + ASSERT_EQ(platf::publish::service_host_name("myhost", 1), "myhost-sunshine-1.local"); + ASSERT_NE(platf::publish::service_host_name("myhost", 0), platf::publish::service_host_name("myhost", 1)); +} + +TEST(ServiceHostNameTest, LabelStaysWithinDnsLimit) { + const auto host_name = platf::publish::service_host_name(std::string(100, 'a'), 0); + // The label (everything before ".local") must fit in 63 characters. + ASSERT_TRUE(host_name.find('.') <= 63); +} + +TEST(SelectServedAddressesTest, Ipv4OnlyPublishesNoIpv6) { + // Regression core for #5860: with an IPv4-only configuration the selection + // contains no IPv6 address, so no AAAA endpoint is published for the + // service host name. + const auto selected = platf::publish::select_served_addresses( + {{1, boost::asio::ip::make_address("192.168.1.10")}, + {1, boost::asio::ip::make_address("fe80::1")}, + {2, boost::asio::ip::make_address("2001:db8::1")}}, + net::IPV4 + ); + ASSERT_EQ(selected.size(), 1U); + ASSERT_EQ(selected[0].interface, 1); + ASSERT_EQ(selected[0].address, boost::asio::ip::make_address("192.168.1.10")); +} + +TEST(SelectServedAddressesTest, BothPublishesBothFamilies) { + const auto selected = platf::publish::select_served_addresses( + {{1, boost::asio::ip::make_address("192.168.1.10")}, {2, boost::asio::ip::make_address("2001:db8::1")}}, + net::BOTH + ); + ASSERT_EQ(selected.size(), 2U); + ASSERT_EQ(selected[0].interface, 1); + ASSERT_EQ(selected[1].interface, 2); +} + +TEST(SelectServedAddressesTest, DropsUnusableAndDuplicateAddresses) { + const auto selected = platf::publish::select_served_addresses( + {{1, boost::asio::ip::make_address("127.0.0.1")}, + {1, boost::asio::ip::make_address("0.0.0.0")}, + {1, boost::asio::ip::make_address("::1")}, + {1, boost::asio::ip::make_address("192.168.1.10")}, + {1, boost::asio::ip::make_address("192.168.1.10")}, + {1, boost::asio::ip::make_address("::ffff:192.168.1.10")}}, + net::BOTH + ); + ASSERT_EQ(selected.size(), 1U); + ASSERT_EQ(selected[0].address, boost::asio::ip::make_address("192.168.1.10")); +} + +TEST(SelectServedAddressesTest, DeduplicatesByInterfaceAndAddress) { + // The same address on two interfaces is reachable on both, so it is kept + // once per interface; the same interface-plus-address pair is kept once. + // A v4-mapped duplicate normalizes to the same IPv4 address. + const auto selected = platf::publish::select_served_addresses( + {{1, boost::asio::ip::make_address("192.168.1.10")}, + {1, boost::asio::ip::make_address("192.168.1.10")}, + {2, boost::asio::ip::make_address("192.168.1.10")}, + {1, boost::asio::ip::make_address("::ffff:192.168.1.10")}}, + net::BOTH + ); + ASSERT_EQ(selected.size(), 2U); + ASSERT_EQ(selected[0], (platf::publish::ServedAddress {1, boost::asio::ip::make_address("192.168.1.10")})); + ASSERT_EQ(selected[1], (platf::publish::ServedAddress {2, boost::asio::ip::make_address("192.168.1.10")})); +} + +TEST(ServedAddressesTest, ExplicitBindResolvesOwningInterface) { + // An explicit bind address is published with the interface index of the + // interface that owns it, not IF_UNSPEC. + const auto enumerated = platf::publish::enumerate_interface_addresses(); + if (enumerated.empty()) { + GTEST_SKIP() << "No local interface addresses to resolve a bind address against."; + } + + const auto &entry = enumerated[0]; + const auto saved_bind = config::sunshine.bind_address; + config::sunshine.bind_address = entry.address.to_string(); + + const auto served = platf::publish::served_addresses(net::BOTH); + + config::sunshine.bind_address = saved_bind; + + ASSERT_EQ(served.size(), 1U); + ASSERT_EQ(served[0], entry); +} + +TEST(ServedAddressesTest, ExplicitBindWithoutOwnerFallsBackToUnspec) { + // A bind address no local interface owns is still published exactly as + // configured, with IF_UNSPEC. + const auto saved_bind = config::sunshine.bind_address; + // TEST-NET-1 (RFC 5737) is never assigned to a real interface, so no + // local interface can own it. + config::sunshine.bind_address = "192.0.2.10"; + + const auto served = platf::publish::served_addresses(net::BOTH); + + config::sunshine.bind_address = saved_bind; + + ASSERT_EQ(served.size(), 1U); + ASSERT_EQ(served[0].interface, avahi::IF_UNSPEC); + ASSERT_EQ(served[0].address, boost::asio::ip::make_address("192.0.2.10")); +} + +TEST(ServedAddressesTest, ExplicitBindScopedLinkLocalResolvesOwningInterface) { + // A scoped link-local bind must resolve to the interface that owns it: + // getifaddrs() reports the zone (sin6_scope_id), and the enumeration must + // preserve it, or the scoped bind never equals its enumerated owner and + // the address falls back to IF_UNSPEC, advertising the link-local + // endpoint on interfaces where Sunshine does not serve it. + ifaddrs *interfaces = nullptr; + if (getifaddrs(&interfaces) != 0) { + GTEST_SKIP() << "getifaddrs() failed; cannot find a scoped IPv6 address."; + } + + std::string scoped_bind; + avahi::IfIndex expected_interface = avahi::IF_UNSPEC; + for (const auto *ifa = interfaces; ifa != nullptr; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == nullptr || ifa->ifa_addr->sa_family != AF_INET6) { + continue; + } + const auto *inet6_address = reinterpret_cast(ifa->ifa_addr); + if (inet6_address->sin6_scope_id == 0) { + continue; + } + boost::asio::ip::address_v6::bytes_type bytes {}; + std::memcpy(bytes.data(), &inet6_address->sin6_addr, bytes.size()); + const auto address = boost::asio::ip::address_v6(bytes, inet6_address->sin6_scope_id); + const auto index = if_nametoindex(ifa->ifa_name); + if (index == 0) { + continue; + } + scoped_bind = address.to_string(); + expected_interface = static_cast(index); + break; + } + freeifaddrs(interfaces); + if (scoped_bind.empty()) { + GTEST_SKIP() << "No scoped IPv6 interface address to resolve a bind address against."; + } + + const auto saved_bind = config::sunshine.bind_address; + config::sunshine.bind_address = scoped_bind; + + const auto served = platf::publish::served_addresses(net::BOTH); + + config::sunshine.bind_address = saved_bind; + + ASSERT_EQ(served.size(), 1U); + ASSERT_EQ(served[0].interface, expected_interface); + ASSERT_EQ(served[0].address, boost::asio::ip::make_address(scoped_bind)); +} + +TEST(ToAvahiAddressTest, Ipv4EncodesNetworkOrderBytes) { + const auto address = platf::publish::to_avahi_address(boost::asio::ip::make_address("192.168.1.10")); + ASSERT_EQ(address.proto, std::to_underlying(avahi::proto::PROTO_INET)); + ASSERT_EQ(address.data, (std::array {192, 168, 1, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})); +} + +TEST(ToAvahiAddressTest, Ipv6EncodesAllSixteenBytes) { + const auto address = platf::publish::to_avahi_address(boost::asio::ip::make_address("2001:db8::1")); + ASSERT_EQ(address.proto, std::to_underlying(avahi::proto::PROTO_INET6)); + ASSERT_EQ( + address.data, + (std::array {0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01}) + ); +} + + #endif // defined(__linux__) + +namespace { + /** + * @brief Captured Avahi entry-group calls for registration tests. + */ + struct AddressCall { + avahi::IfIndex interface; ///< Interface index the record was published on. + avahi::PublishFlags flags; ///< Flags the record was published with. + avahi::Address address; ///< Address record published. + }; + + std::vector g_address_calls; ///< Address-publication calls captured by the stub. + std::vector g_service_hosts; ///< SRV-target host names captured by the stub. + int g_reset_count = 0; ///< Entry-group resets captured by the stub. + int g_commit_count = 0; ///< Entry-group commits captured by the stub. + int g_quit_count = 0; ///< Poll-loop quits captured by the stub. + bool g_group_empty = true; ///< Entry-group emptiness modeled by the stub: reset empties, commit fills. + bool g_add_service_collide_once = false; ///< Next add-service call reports a collision. + bool g_add_address_fail_once = false; ///< Next add-address call reports a failure. + + /** + * @brief Avahi error code for a name collision (see `avahi::err_e`). + */ + constexpr int kErrCollision = -8; + + void FakeFree(void *p) { + std::free(p); + } + + void FakeClientFree(avahi::Client * /*client*/) { + // The test client is a fake pointer; nothing to release. + } + + char *FakeAlternativeServiceName(char *name) { + return ::strdup(name); + } + + char *FakeStrerror(int /*error*/) { + return const_cast("stub Avahi error"); + } + + int FakeClientErrno(avahi::Client * /*client*/) { + return 0; + } + + void FakeSimplePollQuit(avahi::SimplePoll * /*poll*/) { + ++g_quit_count; + } + + int FakeEntryGroupIsEmpty(avahi::EntryGroup * /*group*/) { + return g_group_empty ? 1 : 0; + } + + int FakeEntryGroupReset(avahi::EntryGroup * /*group*/) { + ++g_reset_count; + g_group_empty = true; + return 0; + } + + int FakeEntryGroupCommit(avahi::EntryGroup * /*group*/) { + ++g_commit_count; + g_group_empty = false; + return 0; + } + + int FakeEntryGroupAddService( + avahi::EntryGroup * /*group*/, + avahi::IfIndex /*interface*/, + avahi::Protocol /*protocol*/, + avahi::PublishFlags /*flags*/, + const char * /*name*/, + const char * /*type*/, + const char * /*domain*/, + const char *host, + std::uint16_t /*port*/, + ... + ) { + g_service_hosts.emplace_back(host ? host : ""); + if (g_add_service_collide_once) { + g_add_service_collide_once = false; + return kErrCollision; + } + return 0; + } + + /** + * @brief Stub address publication that models Avahi's reverse-PTR conflict. + * @details With the daemon's default `publish-addresses=yes`, the daemon + * already owns the reverse PTR for each local IP. Publishing the same IP + * for the service host without `PUBLISH_NO_REVERSE` creates a competing + * unique reverse PTR and Avahi rejects it with `ERR_COLLISION`. + */ + int FakeEntryGroupAddAddress( + avahi::EntryGroup * /*group*/, + avahi::IfIndex interface, + avahi::Protocol /*protocol*/, + avahi::PublishFlags flags, + const char * /*name*/, + const avahi::Address *address + ) { + if ((flags & avahi::PUBLISH_NO_REVERSE) == 0) { + return kErrCollision; + } + g_address_calls.push_back({interface, flags, *address}); + return 0; + } + + /** + * @brief Stub address publication without the reverse-PTR conflict check. + * @details The F2/F3 tests install this stub so their fail-before + * evidence isolates their own finding: with the strict stub, the missing + * `PUBLISH_NO_REVERSE` flag (F1) would fail those tests first. Linux-only: + * only the Linux-gated registration tests install it. + */ + #if defined(__linux__) + int FakeEntryGroupAddAddressLenient( + avahi::EntryGroup * /*group*/, + avahi::IfIndex interface, + avahi::Protocol /*protocol*/, + avahi::PublishFlags flags, + const char * /*name*/, + const avahi::Address *address + ) { + if (g_add_address_fail_once) { + g_add_address_fail_once = false; + return -1; + } + g_address_calls.push_back({interface, flags, *address}); + return 0; + } + #endif + + /** + * @brief Fixture driving the publisher with stubbed Avahi calls. + */ + class PublishRegistrationTest: public ::testing::Test { + protected: + void SetUp() override { + saved_bind_address_ = config::sunshine.bind_address; + saved_address_family_ = config::sunshine.address_family; + + avahi::free = FakeFree; + avahi::client_free = FakeClientFree; + avahi::alternative_service_name = FakeAlternativeServiceName; + avahi::strerror = FakeStrerror; + avahi::client_errno = FakeClientErrno; + avahi::simple_poll_quit = FakeSimplePollQuit; + avahi::entry_group_is_empty = FakeEntryGroupIsEmpty; + avahi::entry_group_reset = FakeEntryGroupReset; + avahi::entry_group_commit = FakeEntryGroupCommit; + avahi::entry_group_add_service = FakeEntryGroupAddService; + avahi::entry_group_add_address = FakeEntryGroupAddAddress; + + g_address_calls.clear(); + g_service_hosts.clear(); + g_reset_count = 0; + g_commit_count = 0; + g_quit_count = 0; + g_group_empty = true; + g_add_service_collide_once = false; + g_add_address_fail_once = false; + + platf::publish::group = reinterpret_cast(0x1000); + platf::publish::client.reset(reinterpret_cast(0x2000)); + platf::publish::name.reset(::strdup("test-host")); + platf::publish::host_name_collision_count = 0; + platf::publish::published_addresses.reset(); + } + + void TearDown() override { + // Release owned state through the stubs before uninstalling them: + // the deleters call the stubbed Avahi free functions. + platf::publish::client.reset(); + platf::publish::name.reset(); + platf::publish::group = nullptr; + platf::publish::host_name_collision_count = 0; + platf::publish::published_addresses.reset(); + + avahi::free = nullptr; + avahi::client_free = nullptr; + avahi::alternative_service_name = nullptr; + avahi::strerror = nullptr; + avahi::client_errno = nullptr; + avahi::simple_poll_quit = nullptr; + avahi::entry_group_is_empty = nullptr; + avahi::entry_group_reset = nullptr; + avahi::entry_group_commit = nullptr; + avahi::entry_group_add_service = nullptr; + avahi::entry_group_add_address = nullptr; + + config::sunshine.bind_address = saved_bind_address_; + config::sunshine.address_family = saved_address_family_; + } + + std::string saved_bind_address_; + std::string saved_address_family_; + }; +} // namespace + + #if defined(__linux__) +TEST_F(PublishRegistrationTest, AddressPublicationSkipsReversePtr) { + // The service-host alias must publish forward records only: with flags 0 + // the daemon rejects the competing reverse PTR with ERR_COLLISION, the + // group resets, and the poll loop quits before registration. + const auto ret = platf::publish::add_service_addresses( + platf::publish::group, + "myhost-sunshine.local", + {{1, boost::asio::ip::make_address("192.168.1.10")}} + ); + ASSERT_EQ(ret, 0); + ASSERT_EQ(g_address_calls.size(), 1U); + ASSERT_NE(g_address_calls[0].flags & avahi::PUBLISH_NO_REVERSE, 0); + ASSERT_EQ(g_quit_count, 0); +} + +TEST_F(PublishRegistrationTest, AddressPublicationUsesOwningInterface) { + // Lenient stub: isolate the interface finding from the flag finding (F1). + avahi::entry_group_add_address = FakeEntryGroupAddAddressLenient; + + // A LAN address and a Docker-bridge address must each be published on + // their owning interface, including distinct link-local IPv6 addresses: + // publishing everything with IF_UNSPEC would hand LAN clients the + // bridge address (and vice versa) under the same SRV target. + const auto ret = platf::publish::add_service_addresses( + platf::publish::group, + "myhost-sunshine.local", + {{1, boost::asio::ip::make_address("192.168.1.10")}, + {2, boost::asio::ip::make_address("172.17.0.1")}, + {1, boost::asio::ip::make_address("fe80::1")}, + {2, boost::asio::ip::make_address("fe80::2")}} + ); + ASSERT_EQ(ret, 0); + ASSERT_EQ(g_address_calls.size(), 4U); + ASSERT_EQ(g_address_calls[0].interface, 1); + ASSERT_EQ(g_address_calls[1].interface, 2); + ASSERT_EQ(g_address_calls[2].interface, 1); + ASSERT_EQ(g_address_calls[3].interface, 2); + ASSERT_EQ(g_address_calls[0].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("192.168.1.10"))); + ASSERT_EQ(g_address_calls[1].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("172.17.0.1"))); + ASSERT_EQ(g_address_calls[2].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("fe80::1"))); + ASSERT_EQ(g_address_calls[3].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("fe80::2"))); +} + +TEST_F(PublishRegistrationTest, RefreshRepublishesOnAddressChange) { + // Lenient stub: isolate the refresh finding from the flag finding (F1). + avahi::entry_group_add_address = FakeEntryGroupAddAddressLenient; + + // Register .10, change the snapshot to .20, deliver the change + // notification: the group must be reset (withdrawing .10) and the new + // snapshot committed, with no stale .10 record and the poll loop alive. + config::sunshine.bind_address = "192.168.1.10"; + config::sunshine.address_family = "both"; + + platf::publish::create_services(reinterpret_cast(0x2000)); + ASSERT_EQ(g_commit_count, 1); + ASSERT_EQ(g_quit_count, 0); + ASSERT_EQ(g_address_calls.size(), 1U); + ASSERT_EQ(g_address_calls[0].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("192.168.1.10"))); + + config::sunshine.bind_address = "192.168.1.20"; + platf::publish::refresh_service_addresses(); + + ASSERT_EQ(g_reset_count, 1); + ASSERT_EQ(g_commit_count, 2); + ASSERT_EQ(g_quit_count, 0); + ASSERT_EQ(g_address_calls.size(), 2U); + ASSERT_EQ(g_address_calls[1].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("192.168.1.20"))); + + // A second refresh with an unchanged snapshot is a no-op: no churn. + platf::publish::refresh_service_addresses(); + ASSERT_EQ(g_reset_count, 1); + ASSERT_EQ(g_commit_count, 2); +} + +TEST_F(PublishRegistrationTest, RefreshCollisionRetryDoesNotQuitPollLoop) { + // A service collision during a refresh retry must not quit the poll + // loop: the retry inherits the refresh's no-quit-on-failure behavior, + // and the service is re-registered under a new host name. + avahi::entry_group_add_address = FakeEntryGroupAddAddressLenient; + + config::sunshine.bind_address = "192.168.1.10"; + config::sunshine.address_family = "both"; + + platf::publish::create_services(reinterpret_cast(0x2000)); + ASSERT_EQ(g_commit_count, 1); + ASSERT_EQ(g_service_hosts.size(), 1U); + + config::sunshine.bind_address = "192.168.1.20"; + g_add_service_collide_once = true; + platf::publish::refresh_service_addresses(); + + ASSERT_EQ(g_quit_count, 0); + ASSERT_EQ(g_commit_count, 2); + // Three service registrations: the initial one, the refresh attempt that + // collided, and the retry under the regenerated host name. + ASSERT_EQ(g_service_hosts.size(), 3U); + ASSERT_EQ(g_service_hosts[1], g_service_hosts[0]); + ASSERT_NE(g_service_hosts[2], g_service_hosts[0]); + ASSERT_EQ(platf::publish::host_name_collision_count, 1U); +} + +TEST_F(PublishRegistrationTest, RefreshFailureThenRestoreRepublishes) { + // A -> failed B -> A: the failed refresh withdraws the group and must + // invalidate the snapshot, so restoring A republishes instead of + // early-returning on a stale snapshot while the group sits empty. + avahi::entry_group_add_address = FakeEntryGroupAddAddressLenient; + + config::sunshine.bind_address = "192.168.1.10"; + config::sunshine.address_family = "both"; + + platf::publish::create_services(reinterpret_cast(0x2000)); + ASSERT_EQ(g_commit_count, 1); + ASSERT_EQ(g_quit_count, 0); + ASSERT_TRUE(platf::publish::published_addresses.has_value()); + ASSERT_EQ(avahi::entry_group_is_empty(platf::publish::group), 0); + + config::sunshine.bind_address = "192.168.1.20"; + g_add_address_fail_once = true; + platf::publish::refresh_service_addresses(); + + // The refresh reset the group and the registration failed: the group is + // empty and no snapshot is committed. + ASSERT_EQ(g_commit_count, 1); + ASSERT_EQ(g_quit_count, 0); + ASSERT_FALSE(platf::publish::published_addresses.has_value()); + ASSERT_NE(avahi::entry_group_is_empty(platf::publish::group), 0); + + config::sunshine.bind_address = "192.168.1.10"; + platf::publish::refresh_service_addresses(); + + // Restoring A must commit a second time, with a live group and the + // poll loop still running. + ASSERT_EQ(g_commit_count, 2); + ASSERT_EQ(g_quit_count, 0); + ASSERT_TRUE(platf::publish::published_addresses.has_value()); + ASSERT_EQ(avahi::entry_group_is_empty(platf::publish::group), 0); + ASSERT_EQ(g_address_calls.size(), 2U); + ASSERT_EQ(g_address_calls[1].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("192.168.1.10"))); + + // A further refresh with an unchanged snapshot is a no-op: no churn. + platf::publish::refresh_service_addresses(); + ASSERT_EQ(g_commit_count, 2); +} + +TEST_F(PublishRegistrationTest, RefreshFailureThenRestoreEmptySnapshotRepublishes) { + // Empty-A variant: a successfully published empty snapshot (the + // daemon-hostname fallback) is a valid committed snapshot, not a + // withdrawn group, so the failed-B invalidation must still let the + // restored empty snapshot republish. + avahi::entry_group_add_address = FakeEntryGroupAddAddressLenient; + + // An IPv6 bind under an IPv4-only configuration serves no addresses: + // the service falls back to the daemon host name with no address records. + config::sunshine.bind_address = "2001:db8::1"; + config::sunshine.address_family = "ipv4"; + + platf::publish::create_services(reinterpret_cast(0x2000)); + ASSERT_EQ(g_commit_count, 1); + ASSERT_EQ(g_quit_count, 0); + ASSERT_TRUE(platf::publish::published_addresses.has_value()); + ASSERT_TRUE(platf::publish::published_addresses->empty()); + ASSERT_EQ(avahi::entry_group_is_empty(platf::publish::group), 0); + ASSERT_EQ(g_address_calls.size(), 0U); + ASSERT_EQ(g_service_hosts.size(), 1U); + + config::sunshine.bind_address = "192.168.1.20"; + config::sunshine.address_family = "both"; + g_add_address_fail_once = true; + platf::publish::refresh_service_addresses(); + + // The refresh reset the group and the registration failed: the group is + // empty and no snapshot is committed, even though the last good snapshot + // was itself empty. + ASSERT_EQ(g_commit_count, 1); + ASSERT_EQ(g_quit_count, 0); + ASSERT_FALSE(platf::publish::published_addresses.has_value()); + ASSERT_NE(avahi::entry_group_is_empty(platf::publish::group), 0); + + config::sunshine.bind_address = "2001:db8::1"; + config::sunshine.address_family = "ipv4"; + platf::publish::refresh_service_addresses(); + + // Restoring the empty snapshot must commit a second time, with a live + // group and the poll loop still running. + ASSERT_EQ(g_commit_count, 2); + ASSERT_EQ(g_quit_count, 0); + ASSERT_TRUE(platf::publish::published_addresses.has_value()); + ASSERT_TRUE(platf::publish::published_addresses->empty()); + ASSERT_EQ(avahi::entry_group_is_empty(platf::publish::group), 0); + ASSERT_EQ(g_address_calls.size(), 0U); + ASSERT_EQ(g_service_hosts.size(), 2U); + + // A further refresh with an unchanged snapshot is a no-op: no churn. + platf::publish::refresh_service_addresses(); + ASSERT_EQ(g_commit_count, 2); +} + + #else +TEST_F(PublishRegistrationTest, NonLinuxFallbackUsesDaemonHostName) { + // On platforms without the netlink address-change watch (e.g. FreeBSD), + // the service must not publish fixed address records for a + // service-specific host name: with no refresh, a DHCP change would + // leave clients with the stale snapshot. Instead the service uses the + // daemon-managed host name (a null SRV target), and the Avahi daemon + // maintains the interface address records. + ASSERT_FALSE(platf::publish::install_address_change_watch()); + + // An explicit bind with a populated snapshot is the regression case: + // the pre-fix code published the custom host plus its fixed addresses. + config::sunshine.bind_address = "192.168.1.10"; + config::sunshine.address_family = "both"; + + platf::publish::create_services(reinterpret_cast(0x2000)); + + ASSERT_EQ(g_commit_count, 1); + ASSERT_EQ(g_quit_count, 0); + // No fixed alias address records were published ... + ASSERT_TRUE(g_address_calls.empty()); + // ... and the service points at the daemon-managed host name. + ASSERT_EQ(g_service_hosts.size(), 1U); + ASSERT_TRUE(g_service_hosts[0].empty()); +} + #endif // defined(__linux__) + +#endif // !defined(_WIN32) && !defined(__APPLE__) diff --git a/tests/unit/test_resource_ownership.cpp b/tests/unit/test_resource_ownership.cpp new file mode 100644 index 00000000000..352118a352d --- /dev/null +++ b/tests/unit/test_resource_ownership.cpp @@ -0,0 +1,207 @@ +/** + * @file tests/unit/test_resource_ownership.cpp + * @brief Tests for resource lifetime through Sunshine's owning pointer aliases. + */ + +// test includes +#include "../tests_common.h" + +// standard includes +#include +#include +#include +#include + +// platform includes +#ifdef _WIN32 + // Winsock must precede Windows.h. + // clang-format off + #include + #include + // clang-format on + #include +#else + #include + #include +#endif + +// local includes +#include +#include +#include +#ifdef _WIN32 + #include + #include +#endif + +namespace { + /** + * @brief Allocate an FFmpeg buffer that records its final release. + * + * @param releases Counter incremented when the buffer has no remaining references. + * @param data Storage kept alive until the buffer is released. + * @return Owning reference to the tracked buffer. + */ + video::avcodec_buffer_t tracked_buffer(int &releases, uint8_t &data) { + return video::avcodec_buffer_t {av_buffer_create(&data, 1, [](auto *opaque, uint8_t *) { + ++*static_cast(opaque); + }, + &releases, + 0)}; + } +} // namespace + +/** + * @brief Moving and resetting a frame releases its attached buffer exactly once. + */ +TEST(ResourceOwnershipTest, FrameMoveTransfersBufferOwnership) { + int releases = 0; + uint8_t data = 0; + auto buffer = tracked_buffer(releases, data); + ASSERT_TRUE(buffer); + video::avcodec_frame_t frame {av_frame_alloc()}; + ASSERT_TRUE(frame); + frame->buf[0] = buffer.release(); + + auto moved = std::move(frame); + EXPECT_FALSE(frame); + EXPECT_EQ(releases, 0); + moved.reset(); + EXPECT_EQ(releases, 1); + moved.reset(); + EXPECT_EQ(releases, 1); +} + +/** + * @brief Destroying a codec context releases its hardware-frame context reference. + */ +TEST(ResourceOwnershipTest, CodecContextDestructionReleasesHardwareFramesReference) { + int releases = 0; + uint8_t data = 0; + { + auto buffer = tracked_buffer(releases, data); + ASSERT_TRUE(buffer); + video::avcodec_ctx_t context {avcodec_alloc_context3(nullptr)}; + ASSERT_TRUE(context); + context->hw_frames_ctx = buffer.release(); + EXPECT_EQ(releases, 0); + } + EXPECT_EQ(releases, 1); +} + +/** + * @brief An FFmpeg buffer remains alive until its final owning reference is destroyed. + */ +TEST(ResourceOwnershipTest, BufferDestructionPreservesOtherReferences) { + int releases = 0; + uint8_t data = 0; + { + auto first = tracked_buffer(releases, data); + ASSERT_TRUE(first); + video::avcodec_buffer_t second {av_buffer_ref(first.get())}; + ASSERT_TRUE(second); + first.reset(); + EXPECT_EQ(releases, 0); + EXPECT_EQ(av_buffer_get_ref_count(second.get()), 1); + } + EXPECT_EQ(releases, 1); +} + +/** + * @brief Resetting a packet owner destroys each displaced ENet packet exactly once. + */ +TEST(ResourceOwnershipTest, PacketResetReleasesReplacedPacket) { + int releases = 0; + const auto make_packet = [&releases]() { + net::packet_t packet {enet_packet_create(nullptr, 0, 0)}; + if (packet) { + packet->userData = &releases; + packet->freeCallback = [](ENetPacket *released) { + ++*static_cast(released->userData); + }; + } + return packet; + }; + + { + auto packet = make_packet(); + auto replacement = make_packet(); + ASSERT_TRUE(packet); + ASSERT_TRUE(replacement); + packet.reset(replacement.release()); + EXPECT_EQ(releases, 1); + } + EXPECT_EQ(releases, 2); +} + +/** + * @brief Resetting a file owner closes a securely created temporary file's underlying resource. + */ +TEST(ResourceOwnershipTest, FileResetClosesUnderlyingDescriptor) { +#ifdef _WIN32 + FILE *temporary_file = nullptr; + ASSERT_EQ(tmpfile_s(&temporary_file), 0); + proc::file_t file {temporary_file}; +#else + proc::file_t file {std::tmpfile()}; +#endif + ASSERT_TRUE(file); +#ifdef _WIN32 + const auto handle = std::bit_cast(_get_osfhandle(_fileno(file.get()))); + ASSERT_NE(handle, INVALID_HANDLE_VALUE); + file.reset(); + DWORD flags = 0; + EXPECT_FALSE(GetHandleInformation(handle, &flags)); + EXPECT_EQ(GetLastError(), ERROR_INVALID_HANDLE); +#else + const auto descriptor = fileno(file.get()); + ASSERT_GE(descriptor, 0); + file.reset(); + errno = 0; + EXPECT_EQ(fcntl(descriptor, F_GETFD), -1); + EXPECT_EQ(errno, EBADF); +#endif +} + +#ifdef _WIN32 +/** + * @brief Const NVIDIA preference inspection does not claim ownership before the interface is loaded. + */ +TEST(ResourceOwnershipTest, ConstNvPrefsInterfaceStartsWithoutAnUndoFile) { + const nvprefs::nvprefs_interface preferences; + EXPECT_FALSE(preferences.owning_undo_file()); + EXPECT_FALSE(preferences.owning_undo_file()); +} + +/** + * @brief A const LocalAlloc deleter releases a movable allocation and accepts null pointers. + */ +TEST(ResourceOwnershipTest, ConstLocalAllocDeleterReleasesTheAllocation) { + const auto allocation = LocalAlloc(LMEM_MOVEABLE, 16); + ASSERT_NE(allocation, nullptr); + ASSERT_NE(LocalFlags(allocation), LMEM_INVALID_HANDLE); + const nvprefs::safe_hlocal_deleter destroy; + destroy(allocation); + EXPECT_EQ(LocalFlags(allocation), LMEM_INVALID_HANDLE); + destroy(static_cast(nullptr)); +} + +/** + * @brief Moving an NVPrefs handle transfers responsibility for closing it. + */ +TEST(ResourceOwnershipTest, WindowsHandleMoveClosesHandleOnDestruction) { + HANDLE raw_handle = nullptr; + { + nvprefs::safe_handle handle {CreateEventW(nullptr, TRUE, FALSE, nullptr)}; + ASSERT_TRUE(handle); + raw_handle = handle.get(); + auto moved = std::move(handle); + EXPECT_FALSE(handle); + EXPECT_TRUE(moved); + EXPECT_EQ(WaitForSingleObject(raw_handle, 0), WAIT_TIMEOUT); + } + DWORD flags = 0; + EXPECT_FALSE(GetHandleInformation(raw_handle, &flags)); + EXPECT_EQ(GetLastError(), ERROR_INVALID_HANDLE); +} +#endif diff --git a/tests/unit/test_rtsp.cpp b/tests/unit/test_rtsp.cpp new file mode 100644 index 00000000000..ee07040aafb --- /dev/null +++ b/tests/unit/test_rtsp.cpp @@ -0,0 +1,117 @@ +/** + * @file tests/unit/test_rtsp.cpp + * @brief Verify RTSP response handlers through their common dispatcher signature. + */ + +// standard includes +#include +#include +#include + +// lib includes +#include +extern "C" { +#include +} + +// test includes +#include "../tests_common.h" + +// local includes +#include + +/** + * @brief Const RTSP wire headers decode network-order lengths independently of encryption flags. + */ +TEST(RtspHeaderTest, ConstAccessorsPreserveTheSerializedHeader) { + using rtsp_stream::encrypted_rtsp_header_t; + for (const auto length : {0U, 1U, 0x7fffffffU}) { + for (const bool encrypted : {false, true}) { + const auto flags = encrypted ? encrypted_rtsp_header_t::ENCRYPTED_MESSAGE_TYPE_BIT : 0U; + const encrypted_rtsp_header_t header {util::endian::big(length | flags), 0, {}}; + EXPECT_EQ(header.payload_length(), length); + EXPECT_EQ(header.is_encrypted(), encrypted); + EXPECT_EQ(header.typeAndLength, util::endian::big(length | flags)); + } + } +} + +namespace rtsp_stream { + class rtsp_server_t; + + /** + * @brief Release a parsed RTSP message and its allocation. + * @param message Message to release. + */ + void free_msg(PRTSP_MESSAGE message); + + /** + * @brief Owning RTSP message type used by the dispatcher. + */ + using msg_t = util::safe_ptr; + + /** + * @brief Respond to OPTIONS through the common dispatcher signature. + * @param sock Connected client socket. + * @param session Launch session. + * @param req Request to handle. + */ + void cmd_option(rtsp_server_t *, boost::asio::ip::tcp::socket &sock, launch_session_t &session, msg_t &&req); + + /** + * @brief Respond to DESCRIBE through the common dispatcher signature. + * @param sock Connected client socket. + * @param session Launch session. + * @param req Request to handle. + */ + void cmd_describe(rtsp_server_t *, boost::asio::ip::tcp::socket &sock, launch_session_t &session, msg_t &&req); + + /** + * @brief Respond to PLAY through the common dispatcher signature. + * @param sock Connected client socket. + * @param session Launch session. + * @param req Request to handle. + */ + void cmd_play(rtsp_server_t *, boost::asio::ip::tcp::socket &sock, launch_session_t &session, msg_t &&req); +} // namespace rtsp_stream + +/** + * @brief All three handlers respond with the request's sequence even without a server instance. + */ +TEST(RtspResponseTest, CommonDispatcherPreservesResponsesWithoutServerContext) { + using boost::asio::ip::tcp; + const std::array handlers { + &rtsp_stream::cmd_option, + &rtsp_stream::cmd_describe, + &rtsp_stream::cmd_play, + }; + + for (const auto handler : handlers) { + boost::asio::io_context io; + tcp::acceptor acceptor {io, {boost::asio::ip::address_v4::loopback(), 0}}; + tcp::socket client {io}; + client.connect(acceptor.local_endpoint()); + auto server_socket = acceptor.accept(); + + rtsp_stream::launch_session_t session {}; + rtsp_stream::msg_t request {new RTSP_MESSAGE {}}; + request->sequenceNumber = 42; + handler(nullptr, server_socket, session, std::move(request)); + server_socket.shutdown(tcp::socket::shutdown_send); + + std::string response; + boost::system::error_code read_error; + boost::asio::read(client, boost::asio::dynamic_buffer(response), read_error); + ASSERT_EQ(read_error, boost::asio::error::eof); + EXPECT_TRUE(response.starts_with("RTSP/1.0 200 OK\r\n")); + EXPECT_NE(response.find("CSeq: 42\r\n"), std::string::npos); + + if (handler == &rtsp_stream::cmd_describe) { + EXPECT_NE(response.find("a=x-ss-general.featureFlags:"), std::string::npos); + EXPECT_NE(response.find("a=x-ss-general.encryptionSupported:"), std::string::npos); + EXPECT_NE(response.find("a=fmtp:97 surround-params="), std::string::npos); + } else { + EXPECT_TRUE(response.ends_with("\r\n\r\n")); + } + } +} diff --git a/tests/unit/test_string_hash.cpp b/tests/unit/test_string_hash.cpp new file mode 100644 index 00000000000..c7c451786bc --- /dev/null +++ b/tests/unit/test_string_hash.cpp @@ -0,0 +1,42 @@ +/** + * @file tests/unit/test_string_hash.cpp + * @brief Regression coverage for heterogeneous owning-string containers. + */ + +// test includes +#include "../tests_common.h" + +// standard includes +#include +#include +#include + +// local includes +#include + +TEST(TransparentStringHashTest, EquivalentKeyRepresentationsFindTheSameEntry) { + const std::string key(128, 'k'); + const std::string padded = "prefix" + key + "suffix"; + const auto view = std::string_view {padded}.substr(6, key.size()); + std::unordered_map> entries {{key, 42}}; + + EXPECT_EQ(entries.find(key)->second, 42); + EXPECT_EQ(entries.find(key.c_str())->second, 42); + EXPECT_EQ(entries.find(view)->second, 42); + EXPECT_FALSE(entries.contains(std::string_view {padded})); + EXPECT_EQ(entries.size(), 1); +} + +TEST(TransparentStringHashTest, EmptyAndEmbeddedNullKeysRetainTheirLengths) { + using namespace std::literals; + std::unordered_map> entries { + {"", 1}, + {"key\0suffix"s, 2}, + {"key", 3}, + }; + + EXPECT_EQ(entries.find(""sv)->second, 1); + EXPECT_EQ(entries.find("key\0suffix"sv)->second, 2); + EXPECT_EQ(entries.find("key")->second, 3); + EXPECT_FALSE(entries.contains("key\0other"sv)); +} diff --git a/tests/unit/test_sync.cpp b/tests/unit/test_sync.cpp new file mode 100644 index 00000000000..7cef9b49042 --- /dev/null +++ b/tests/unit/test_sync.cpp @@ -0,0 +1,92 @@ +/** + * @file tests/unit/test_sync.cpp + * @brief Regression coverage for synchronized-value lock lifetimes. + */ + +// standard includes +#include +#include +#include +#include + +// lib includes +#include + +// local includes +#include + +namespace { + /** + * @brief BasicLockable mutex that detects an unreleased lock without requiring try_lock. + */ + class basic_mutex { + public: + /** + * @brief Acquire the mutex, rejecting recursive locking. + * + * @throws std::system_error If the mutex is already locked. + */ + void lock() { + if (held_) { + throw std::system_error {std::make_error_code(std::errc::resource_deadlock_would_occur)}; + } + held_ = true; + } + + /** + * @brief Release the mutex. + */ + void unlock() noexcept { + held_ = false; + } + + private: + bool held_ {false}; ///< Whether a guard currently owns this mutex. + }; +} // namespace + +TEST(SyncLockTest, RejectsRecursiveLockingAndReleasesTheOriginalGuard) { + sync_util::sync_t value {0}; + { + const auto guard = value.lock(); + EXPECT_THROW(value.lock(), std::system_error); + } + EXPECT_NO_THROW(value.lock()); +} + +TEST(SyncLockTest, SupportsBasicLockableAndReleasesOnException) { + sync_util::sync_t value {0}; + EXPECT_THROW( + [&value]() { + const auto guard = value.lock(); + ++*value; + throw std::runtime_error {"unwind the guard"}; + }(), + std::runtime_error + ); + + EXPECT_NO_THROW([&value]() { + const auto guard = value.lock(); + EXPECT_EQ(*value, 1); + }()); + EXPECT_NO_THROW([&value]() { + const auto guard = value.lock(); + EXPECT_EQ(*value, 1); + }()); +} + +TEST(SyncLockTest, SerializesConcurrentUpdates) { + sync_util::sync_t value {0}; + { + std::array workers; + for (auto &worker : workers) { + worker = std::jthread {[&value]() { + for (int i = 0; i < 250; ++i) { + const auto guard = value.lock(); + ++*value; + } + }}; + } + } + EXPECT_EQ(*value, 1000); +} diff --git a/tests/unit/test_task_pool.cpp b/tests/unit/test_task_pool.cpp new file mode 100644 index 00000000000..203b8a7a7e6 --- /dev/null +++ b/tests/unit/test_task_pool.cpp @@ -0,0 +1,38 @@ +/** + * @file tests/unit/test_task_pool.cpp + * @brief Regression coverage for delayed task lookup over the complete queue. + */ + +// lib includes +#include + +// local includes +#include + +TEST(TaskPoolTest, FindsRequestedDelayedTaskAndRejectsMissingIdentifiers) { + using namespace std::chrono_literals; + task_pool_util::TaskPool pool; + EXPECT_FALSE(pool.pop(nullptr).has_value()); + + auto first = pool.pushDelayed([]() { + return 11; + }, + 1h); + auto second = pool.pushDelayed([]() { + return 22; + }, + 2h); + EXPECT_FALSE(pool.pop(nullptr).has_value()); + + auto removed = pool.pop(first.task_id); + ASSERT_TRUE(removed.has_value()); + ASSERT_NE(removed->second, nullptr); + removed->second->run(); + EXPECT_EQ(first.future.get(), 11); + + auto other = pool.pop(second.task_id); + ASSERT_TRUE(other.has_value()); + ASSERT_NE(other->second, nullptr); + other->second->run(); + EXPECT_EQ(second.future.get(), 22); +} diff --git a/tests/unit/test_thread_safe.cpp b/tests/unit/test_thread_safe.cpp index 8ef951e14d0..2ec90318c05 100644 --- a/tests/unit/test_thread_safe.cpp +++ b/tests/unit/test_thread_safe.cpp @@ -15,6 +15,63 @@ static_assert(!std::is_convertible_v>); +/** + * @brief Const event inspection observes state transitions without consuming the pending value. + */ +TEST(ThreadSafeEvent, ConstPeekPreservesPendingValuesAcrossStateChanges) { + safe::event_t event; + const auto &view = event; + EXPECT_FALSE(view.peek()); + event.raise(42); + EXPECT_TRUE(view.peek()); + EXPECT_TRUE(view.peek()); + EXPECT_EQ(event.try_pop(), 42); + EXPECT_FALSE(view.peek()); + event.raise(7); + event.stop(); + EXPECT_FALSE(view.peek()); + event.reset(); + EXPECT_FALSE(view.peek()); + event.raise(9); + EXPECT_TRUE(view.peek()); +} + +/** + * @brief Mail cleanup removes an expired post while preserving an active post's owner. + */ +TEST(ThreadSafeMail, CleanupPreservesLivePostsAndRemovesExpiredPosts) { + safe::mail_raw_t mailbox; + const auto live = std::make_shared(7); + mailbox.id_to_post.try_emplace("live", live); + mailbox.id_to_post.try_emplace("expired", std::weak_ptr {}); + + mailbox.cleanup(); + + EXPECT_EQ(mailbox.id_to_post.size(), 1); + EXPECT_EQ(mailbox.id_to_post.at("live").lock(), live); + mailbox.cleanup(); + EXPECT_EQ(mailbox.id_to_post.size(), 1); +} + +/** + * @brief Repeated lookups reuse each live mailbox channel and preserve queued values. + */ +TEST(ThreadSafeMail, ReusesExistingEventAndQueueForDuplicateIdentifiers) { + const auto mailbox = std::make_shared(); + const auto event = mailbox->event("event"); + event->raise(0); + const auto same_event = mailbox->event("event"); + EXPECT_EQ(same_event, event); + EXPECT_EQ(same_event->pop(), 0); + + const auto queue = mailbox->queue("queue"); + ASSERT_TRUE(queue->raise(7)); + const auto same_queue = mailbox->queue("queue"); + EXPECT_EQ(same_queue, queue); + EXPECT_EQ(same_queue->pop(), 7); + EXPECT_EQ(mailbox->id_to_post.size(), 2); +} + TEST(ThreadSafeQueue, RejectsNewestItemAtCapacity) { safe::queue_t queue {1, safe::queue_t::overflow_policy_e::reject}; diff --git a/tests/unit/test_upnp.cpp b/tests/unit/test_upnp.cpp new file mode 100644 index 00000000000..bf0304d32a8 --- /dev/null +++ b/tests/unit/test_upnp.cpp @@ -0,0 +1,42 @@ +/** + * @file tests/unit/test_upnp.cpp + * @brief Test src/upnp.* + */ + +// test includes +#include "../tests_common.h" + +// local includes +#include + +TEST(UpnpIsValidIgdTest, RejectsNoIgd) { + ASSERT_FALSE(upnp::is_valid_igd(upnp::igd_status_e::none)); +} + +TEST(UpnpIsValidIgdTest, AcceptsConnectedIgd) { + ASSERT_TRUE(upnp::is_valid_igd(upnp::igd_status_e::connected)); +} + +#if (MINIUPNPC_API_VERSION >= 18) +TEST(UpnpIsValidIgdTest, AcceptsPrivateIpIgd) { + // MiniUPnPc 2.2.8+ reports a valid connected IGD with a private IP address as igd_status_e::private_ip. + ASSERT_TRUE(upnp::is_valid_igd(upnp::igd_status_e::private_ip)); +} +#endif + +TEST(UpnpIsValidIgdTest, AcceptsDisconnectedIgd) { + // Regression test for #5462: MiniUPnPc 2.2.8+ reports a valid but disconnected IGD as + // igd_status_e::disconnected. The old check (status == 1 || status == 2) rejected it, so UPnP port + // mapping silently failed for routers advertising a valid IGD with NewConnectionStatus=Disconnected. + ASSERT_TRUE(upnp::is_valid_igd(upnp::igd_status_e::disconnected)); +} + +TEST(UpnpIsValidIgdTest, RejectsUnknownDevice) { + ASSERT_FALSE(upnp::is_valid_igd(upnp::igd_status_e::unknown_device)); +} + +TEST(UpnpIsValidIgdTest, RejectsOutOfRange) { + ASSERT_FALSE(upnp::is_valid_igd(static_cast(-1))); + ASSERT_FALSE(upnp::is_valid_igd(static_cast(5))); + ASSERT_FALSE(upnp::is_valid_igd(static_cast(100))); +} diff --git a/tests/unit/test_utility.cpp b/tests/unit/test_utility.cpp index 4a4422c64f5..c5069795a49 100644 --- a/tests/unit/test_utility.cpp +++ b/tests/unit/test_utility.cpp @@ -7,16 +7,76 @@ #include "../tests_common.h" // standard includes +#include +#include #include +#include +#include +#include #include #include #include +#include #include #include // local includes #include +/** + * @brief Whether a type has a supported empty-state sentinel. + */ +template +concept has_false_value = requires { util::__false_v::value; }; + +/** + * @brief Sentinel constraints select empty states without accepting unrelated values. + */ +TEST(UtilityFalseValueTests, SelectsSentinelsForOptionalPointerAndBooleanTypes) { + static_assert(std::is_same_v>), const std::nullopt_t>); + static_assert(std::is_same_v), const std::nullptr_t>); + static_assert(std::is_same_v>), const std::nullptr_t>); + static_assert(std::is_same_v>), const std::nullptr_t>); + static_assert(std::is_same_v>), const std::nullptr_t>); + static_assert(std::is_same_v), const bool>); + static_assert(!has_false_value); + + EXPECT_FALSE(std::optional {util::false_v>}); + EXPECT_EQ(util::false_v, nullptr); + EXPECT_EQ(util::false_v>, nullptr); + EXPECT_EQ(util::false_v>, nullptr); + EXPECT_EQ(util::false_v>, nullptr); + EXPECT_FALSE(util::false_v); +} + +/** + * @brief A const resource deleter invokes its release function exactly once per call. + */ +TEST(UtilityDestroyTest, ConstDeleterInvokesTheConfiguredReleaseFunction) { + const util::Destroy + destroy; + int releases = 0; + destroy(&releases); + EXPECT_EQ(releases, 1); +} + +/** + * @brief Simulate the opposite byte order for a byte-array fixture. + */ +template<> +struct util::endian::endianness> { + static constexpr bool little = !util::endian::endianness<>::little; ///< Opposite native byte order. + static constexpr bool big = !little; ///< Whether the fixture simulates big endian. +}; + +/** + * @brief Use the simulated byte order for optional byte-array fixtures. + */ +template<> +struct util::endian::endianness>>: util::endian::endianness> {}; + struct HexConversionTest: testing::TestWithParam> {}; TEST_P(HexConversionTest, ToStringProducesExpectedHex) { @@ -248,13 +308,26 @@ TEST(UtilityBufferTests, MoveConstruction) { TEST(UtilityBufferTests, CopyConstruction) { util::buffer_t buf1(3, 55); + buf1[1] = 66; + buf1[2] = 77; util::buffer_t buf2(buf1); EXPECT_EQ(buf2.size(), 3U); EXPECT_EQ(buf2[0], 55); + EXPECT_EQ(buf2[1], 66); + EXPECT_EQ(buf2[2], 77); EXPECT_EQ(buf1.size(), 3U); EXPECT_EQ(buf1[0], 55); } +TEST(UtilityBufferTests, CopiesAnEmptyLogicalRange) { + util::buffer_t buf(3, 42); + buf.fake_resize(0); + const util::buffer_t copy(buf); + EXPECT_EQ(copy.size(), 0U); + EXPECT_EQ(copy.begin(), copy.end()); + EXPECT_EQ(buf[0], 42); +} + TEST(UtilityBufferTests, SizeConstructionValueInitializesEveryElement) { const util::buffer_t buf(4); EXPECT_EQ(buf.end() - buf.begin(), 4); @@ -333,11 +406,36 @@ TEST(UtilityFromHexTests, ShortInputPadsUnusedBytesWithZeros) { EXPECT_EQ(util::from_hex("ABCD", true), util::endian::big(std::uint64_t {0xABCD})); } +TEST(UtilityFromHexTests, LongInputKeepsTheLowOrderBytes) { + EXPECT_EQ(util::from_hex("123456789A"), util::endian::little(std::uint32_t {0x3456789A})); + EXPECT_EQ(util::from_hex("123456789A", true), util::endian::big(std::uint32_t {0x3456789A})); +} + TEST(UtilityFromHexTests, DecodesIntoAByteArrayWithoutPointerTypePunning) { const auto bytes = util::from_hex>("12:34:56:78", true); EXPECT_EQ(bytes, (std::array {0x12, 0x34, 0x56, 0x78})); } +TEST(UtilityFromHexTests, DecodesMixedCaseNibblesAndSkipsSeparators) { + const auto bytes = util::from_hex>("0a:B1-cD.2e 3F 49", true); + const std::array expected {std::byte {0x0A}, std::byte {0xB1}, std::byte {0xCD}, std::byte {0x2E}, std::byte {0x3F}, std::byte {0x49}}; + EXPECT_EQ(bytes, expected); + EXPECT_EQ(util::from_hex(""), 0U); +} + +TEST(UtilityFromHexVecTests, PreservesBinaryBytesWithMixedCaseAndSeparators) { + const std::string expected {0, static_cast(0x80), static_cast(0xFF), static_cast(0xAB), 0x19}; + EXPECT_EQ(util::from_hex_vec("00:80-fF aB.19", true), expected); + EXPECT_EQ(util::from_hex_vec("00:80-fF aB.19"), (std::string {expected.rbegin(), expected.rend()})); + EXPECT_TRUE(util::from_hex_vec("").empty()); +} + +TEST(UtilityHashTests, MatchesStringViewHashOfAllObjectBytes) { + const std::array value {0x00, 0x80, 0xFF, 0x12, 0x00, 0xAB}; + const std::string expected_bytes {0, static_cast(0x80), static_cast(0xFF), 0x12, 0, static_cast(0xAB)}; + EXPECT_EQ(util::hash {}(value), std::hash {}(expected_bytes)); +} + TEST(UtilityAppendStructTests, AppendsDataCorrectly) { struct test_struct_t { std::uint8_t a; @@ -375,6 +473,31 @@ TEST(UtilityEndianTests, LittleEndianConversion) { } } +TEST(UtilityEndianTests, OptionalValuesPreserveEmptyStateAndConvertContainedBytes) { + const std::optional value {0x01020304}; + EXPECT_EQ(util::endian::big(value), std::optional {util::endian::big(*value)}); + EXPECT_EQ(util::endian::little(value), std::optional {util::endian::little(*value)}); + EXPECT_FALSE(util::endian::big(std::optional {})); + EXPECT_FALSE(util::endian::little(std::optional {})); +} + +TEST(UtilityEndianTests, ConvertsValuesAndOptionalsOnTheOppositeByteOrder) { + const std::array value {std::byte {0x12}, std::byte {0x34}, std::byte {0x56}, std::byte {0x78}}; + const std::array reversed {std::byte {0x78}, std::byte {0x56}, std::byte {0x34}, std::byte {0x12}}; + const std::optional optional_value {value}; + if constexpr (util::endian::endianness<>::little) { + EXPECT_EQ(util::endian::little(value), reversed); + EXPECT_EQ(util::endian::big(value), value); + EXPECT_EQ(util::endian::little(optional_value), std::optional {reversed}); + EXPECT_EQ(util::endian::big(optional_value), optional_value); + } else { + EXPECT_EQ(util::endian::big(value), reversed); + EXPECT_EQ(util::endian::little(value), value); + EXPECT_EQ(util::endian::big(optional_value), std::optional {reversed}); + EXPECT_EQ(util::endian::little(optional_value), optional_value); + } +} + TEST(UtilityLogHexTests, FormatsWithPrefix) { constexpr std::uint8_t val = 0xAB; const std::string result = util::log_hex(val); diff --git a/tests/unit/test_uuid.cpp b/tests/unit/test_uuid.cpp index c86c80195e7..30681680d2a 100644 --- a/tests/unit/test_uuid.cpp +++ b/tests/unit/test_uuid.cpp @@ -38,6 +38,8 @@ TEST(UuidTest, GenerationPreservesDrawOrderAndMasks) { const auto uuid = uuid_util::uuid_t::generate(engine); EXPECT_EQ(uuid.b8, expected); + EXPECT_EQ(std::byte {uuid.b8[7]} & std::byte {0xD0}, std::byte {0}); + EXPECT_EQ(std::byte {uuid.b8[9]} & std::byte {0x60}, std::byte {0}); EXPECT_EQ(engine, expected_engine); EXPECT_EQ(uuid_util::uuid_t::generate().string().size(), 36U); } diff --git a/tests/unit/test_video.cpp b/tests/unit/test_video.cpp index 73297ca978d..d03dfc40e42 100644 --- a/tests/unit/test_video.cpp +++ b/tests/unit/test_video.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +27,23 @@ extern "C" { using namespace std::literals; +/** + * @brief Packet replacement rules retain the original non-owning byte views. + */ +TEST(VideoPacketReplacementTest, CopiesViewsWithoutCopyingTheirBackingBytes) { + std::string original = "__old__"; + std::string replacement = "__new__"; + const video::packet_raw_t::replace_t rule {std::string_view(original).substr(2, 3), std::string_view(replacement).substr(2, 3)}; + EXPECT_EQ(rule.old, "old"); + EXPECT_EQ(rule._new, "new"); + EXPECT_EQ(rule.old.data(), original.data() + 2); + EXPECT_EQ(rule._new.data(), replacement.data() + 2); + original[2] = 'O'; + replacement[2] = 'N'; + EXPECT_EQ(rule.old, "Old"); + EXPECT_EQ(rule._new, "New"); +} + TEST(VideoFormatMappingTest, MapsSupportedHardwareDevicesAndRejectsUnsupportedOnes) { using enum platf::mem_type_e; const std::array cases { @@ -83,25 +101,30 @@ struct EncoderTest: PlatformTestSuite, testing::WithParamInterface({ #if !defined(__APPLE__) - &video::nvenc, + &video::nvenc, #endif #ifdef _WIN32 - &video::amdvce, - &video::quicksync, + &video::amdvce, + &video::quicksync, #endif #if defined(__linux__) || defined(__FreeBSD__) - &video::vaapi, + &video::vaapi, #endif #ifdef __APPLE__ - &video::videotoolbox, + &video::videotoolbox, #endif - &video::software - ), + &video::software +}); + +INSTANTIATE_TEST_SUITE_P( + EncoderVariants, + EncoderTest, + testing::ValuesIn(encoder_variants), [](const auto &info) { return std::string(info.param->name); } diff --git a/tests/unit/test_video_colorspace.cpp b/tests/unit/test_video_colorspace.cpp index 1b34eee963e..45e1f6aadba 100644 --- a/tests/unit/test_video_colorspace.cpp +++ b/tests/unit/test_video_colorspace.cpp @@ -114,3 +114,18 @@ TEST(VideoColorspaceTest, ConvertsBlackWhiteAndPrimaryRedInEveryOutputRange) { } } } + +/** + * @brief Unknown colorspaces reuse the Rec. 709 matrix for every range and depth. + */ +TEST(VideoColorspaceTest, UnknownColorspaceUsesTheRec709Fallback) { + for (unsigned bit_depth : {8U, 10U}) { + for (bool full_range : {false, true}) { + for (bool unorm_output : {false, true}) { + const video::sunshine_colorspace_t fallback {video::colorspace_e::rec709, full_range, bit_depth}; + const video::sunshine_colorspace_t unknown {static_cast(-1), full_range, bit_depth}; + EXPECT_EQ(video::color_vectors_from_colorspace(unknown, unorm_output), video::color_vectors_from_colorspace(fallback, unorm_output)); + } + } + } +} diff --git a/tests/web/Troubleshooting.test.js b/tests/web/Troubleshooting.test.js index 0aa30c91bb8..98f0ff2c4c8 100644 --- a/tests/web/Troubleshooting.test.js +++ b/tests/web/Troubleshooting.test.js @@ -6,8 +6,20 @@ vi.mock('../../src_assets/common/assets/web/Navbar.vue', () => ({ })) import Troubleshooting from '../../src_assets/common/assets/web/Troubleshooting.vue' +import createSunshineI18n from '../../src_assets/common/assets/web/locale.js' +import en from '../../src_assets/common/assets/web/public/assets/locale/en.json' -async function mountTroubleshooting(platform, gamepadDriver, licenseStatus = {}, permissions = []) { +/** + * @brief Mount troubleshooting with mocked API responses and an optional Vue I18n instance. + * + * @param {string} platform Host platform. + * @param {string} gamepadDriver Selected gamepad driver. + * @param {object} licenseStatus License status overrides. + * @param {Array} permissions Platform permissions. + * @param {object|null} i18n Optional Vue I18n plugin. + * @return {Promise} Mounted troubleshooting wrapper. + */ +async function mountTroubleshooting(platform, gamepadDriver, licenseStatus = {}, permissions = [], i18n = null) { vi.stubGlobal('fetch', vi.fn(async (url) => { if (url === '/api/config') { return { json: async () => ({ platform, gamepad_driver: gamepadDriver }) } @@ -48,7 +60,8 @@ async function mountTroubleshooting(platform, gamepadDriver, licenseStatus = {}, const wrapper = mount(Troubleshooting, { global: { - mocks: { $t: key => key }, + mocks: i18n ? {} : { $t: key => key }, + plugins: i18n ? [i18n] : [], stubs: { Navbar: true, RouterLink: true }, }, }) @@ -58,9 +71,53 @@ async function mountTroubleshooting(platform, gamepadDriver, licenseStatus = {}, afterEach(() => { vi.unstubAllGlobals() + vi.restoreAllMocks() }) describe('virtual input troubleshooting', () => { + it.each([ + ['valid', false], + ['malformed', true], + ])('renders Linux permissions and device management with %s translation messages', async (_description, malformed) => { + const catalog = structuredClone(en) + catalog._common.loading = malformed ? 'Loadingchar@@0' : 'Translated loading' + catalog.troubleshooting.driver_release_checking = malformed ? 'Checkingchar@@0' : 'Translated checking' + catalog.troubleshooting.permissions_name = malformed ? 'Permissionchar@@0' : 'Translated permission' + catalog.apps.delete_confirm = 'Delete translated {name}?' + catalog.troubleshooting.troubleshooting = 'Translated troubleshooting' + catalog.troubleshooting.unpair_title = 'Translated unpair' + catalog.troubleshooting.logs = 'Translated logs' + + vi.spyOn(console, 'error').mockImplementation(() => {}) + vi.stubGlobal('fetch', vi.fn(async url => ({ + json: async () => url === './api/configLocale' ? { locale: 'de' } : catalog, + }))) + const i18n = await createSunshineI18n() + const wrapper = await mountTroubleshooting('linux', 'none', {}, [ + { id: 'input', status: 'granted', required: true }, + ], i18n) + + try { + expect(i18n.global.t('_common.loading')).toBe(malformed ? en._common.loading : catalog._common.loading) + expect(i18n.global.t('troubleshooting.driver_release_checking')).toBe( + malformed ? en.troubleshooting.driver_release_checking : catalog.troubleshooting.driver_release_checking + ) + expect(i18n.global.t('apps.delete_confirm', { name: 'Moonlight' })).toBe( + catalog.apps.delete_confirm.replace('{name}', 'Moonlight') + ) + expect(wrapper.get('h1').text()).toBe(catalog.troubleshooting.troubleshooting) + expect(wrapper.get('.permission-table-shell th').text()).toBe( + malformed ? en.troubleshooting.permissions_name : catalog.troubleshooting.permissions_name + ) + expect(wrapper.get('#unpair').text()).toBe(catalog.troubleshooting.unpair_title) + expect(wrapper.get('#logs').text()).toBe(catalog.troubleshooting.logs) + expect(wrapper.find('.permission-table-shell tbody tr').exists()).toBe(true) + } finally { + wrapper.unmount() + i18n.dispose() + } + }) + it('orders permission rows consistently across platforms', async () => { const permissions = [ { id: 'local_network', status: 'on_use', required: true }, diff --git a/tests/web/locale.test.js b/tests/web/locale.test.js index 7c3224aa277..c7df14c7391 100644 --- a/tests/web/locale.test.js +++ b/tests/web/locale.test.js @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('vue-i18n', () => ({ createI18n: vi.fn(options => options), @@ -8,6 +8,11 @@ import { createI18n } from 'vue-i18n' import createSunshineI18n, { loadLocaleMessages } from '../../src_assets/common/assets/web/locale.js' describe('locale initialization', () => { + afterEach(() => { + vi.unstubAllGlobals() + vi.restoreAllMocks() + }) + beforeEach(() => { createI18n.mockClear() document.documentElement.removeAttribute('lang') @@ -58,4 +63,40 @@ describe('locale initialization', () => { expect(error).toHaveBeenCalledWith('Failed to download translations', expect.any(Error)) error.mockRestore() }) + + it('omits malformed messages while preserving valid translations and interpolation syntax', async () => { + const error = vi.spyOn(console, 'error').mockImplementation(() => {}) + const catalog = { + broken: 'Loadingchar@@0', + nested: { + broken: 'Hello {name', + greeting: 'Hallo {name}', + list: '{0}', + literal: "{'@'}", + linked: '@:nested.greeting', + plural: 'none | one | {count} items', + empty: '', + }, + } + const original = structuredClone(catalog) + + const result = await loadLocaleMessages('de', async () => catalog) + + expect(result.locale).toBe('de') + expect(result.messages.de).toEqual({ + nested: { + greeting: 'Hallo {name}', + list: '{0}', + literal: "{'@'}", + linked: '@:nested.greeting', + plural: 'none | one | {count} items', + empty: '', + }, + }) + expect(result.messages.en).toBeDefined() + expect(catalog).toEqual(original) + expect(error).toHaveBeenCalledTimes(2) + expect(error).toHaveBeenCalledWith('Ignoring invalid translation', 'de', 'broken', expect.any(SyntaxError)) + expect(error).toHaveBeenCalledWith('Ignoring invalid translation', 'de', 'nested.broken', expect.any(SyntaxError)) + }) }) diff --git a/tests/web/localization_files.test.js b/tests/web/localization_files.test.js new file mode 100644 index 00000000000..49429d6f2a1 --- /dev/null +++ b/tests/web/localization_files.test.js @@ -0,0 +1,38 @@ +// @vitest-environment node + +import { readFileSync, readdirSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' + +const localeDirectory = resolve(dirname(fileURLToPath(import.meta.url)), '../../src_assets/common/assets/web/public/assets/locale') +const localeFiles = readdirSync(localeDirectory).filter(file => file.endsWith('.json')).sort() + +/** + * @brief Find corrupted numbered placeholders in nested localization messages. + * + * @param {object} messages Parsed localization messages. + * @param {string} [prefix] Parent message key. + * @return {string[]} Corrupted message keys and their values. + */ +function findCorruptedMessages(messages, prefix = '') { + return Object.entries(messages).flatMap(([key, value]) => { + const path = prefix ? `${prefix}.${key}` : key + if (typeof value === 'string') { + return /(?:char|format)@@\d+/u.test(value) ? [`${path}: ${value}`] : [] + } + return findCorruptedMessages(value, path) + }) +} + +describe('localization JSON files', () => { + it('discovers localization catalogs to validate', () => { + expect(localeFiles.length).toBeGreaterThan(0) + }) + + it.each(localeFiles)('%s contains no corrupted localization placeholders', file => { + const messages = JSON.parse(readFileSync(resolve(localeDirectory, file), 'utf8')) + + expect(findCorruptedMessages(messages), `Corrupted localization placeholders in ${file}`).toEqual([]) + }) +}) diff --git a/tests/web/sunshine_styles.test.js b/tests/web/sunshine_styles.test.js new file mode 100644 index 00000000000..afe8d862056 --- /dev/null +++ b/tests/web/sunshine_styles.test.js @@ -0,0 +1,184 @@ +import { mount, flushPromises } from '@vue/test-utils' +import { parse } from '@vue/compiler-sfc' +import { readFileSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' +import navbarSource from '../../src_assets/common/assets/web/Navbar.vue?raw' + +vi.mock('../../src_assets/common/assets/web/Navbar.vue', () => ({ + default: { template: '
' }, +})) + +import Featured from '../../src_assets/common/assets/web/Featured.vue' + +const navbarCss = parse(navbarSource).descriptor.styles[0].content +const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..') +const bootstrapCss = readFileSync(resolve(projectRoot, 'node_modules/bootstrap/dist/css/bootstrap.css'), 'utf8') +const sunshineCss = readFileSync(resolve(projectRoot, 'src_assets/common/assets/web/sunshine.css'), 'utf8') +let stylesheet +let wrapper + +beforeAll(() => { + stylesheet = document.createElement('style') + stylesheet.textContent = [bootstrapCss, sunshineCss, navbarCss].join('\n') + document.head.append(stylesheet) +}) + +afterEach(() => { + wrapper?.unmount() + wrapper = undefined + document.body.replaceChildren() + delete document.documentElement.dataset.theme + delete document.documentElement.dataset.bsTheme + vi.unstubAllGlobals() +}) + +afterAll(() => { + stylesheet.remove() +}) + +/** + * @brief Read the winning style with Bootstrap and the Sunshine theme loaded. + * + * @param {string} markup Component markup to insert into the document. + * @param {string} selector Element whose computed styles should be returned. + * @returns {CSSStyleDeclaration} Computed style, including unresolved theme variables in jsdom. + */ +function componentStyle(markup, selector) { + document.body.innerHTML = markup + return getComputedStyle(document.querySelector(selector)) +} + +/** + * @brief Mount the featured page with a deterministic app directory. + * + * @param {object} directory App and category data returned by the directory service. + * @returns {Promise} Mounted featured page. + */ +async function mountFeatured(directory) { + vi.stubGlobal('fetch', vi.fn(async () => ({ + ok: true, + json: async () => directory, + }))) + wrapper = mount(Featured, { + attachTo: document.body, + global: { mocks: { $t: key => key }, stubs: { Navbar: true, SimpleIcon: true } }, + }) + await flushPromises() + return wrapper +} + +const componentCases = [ + ['body', '
', 'body', { backgroundColor: 'var(--color-bg-base)', color: 'var(--color-text-base)' }], + ['card', '
', '.card', { backgroundColor: 'var(--color-surface)', color: 'var(--color-text-base)' }], + ['card header', '
', '.card-header', { backgroundColor: 'var(--color-bg-subtle)', color: 'var(--color-text-base)' }], + ['card footer', '', '.card-footer', { backgroundColor: 'var(--color-bg-subtle)' }], + ['modal', '', '.modal-content', { backgroundColor: 'var(--color-surface)' }], + ['modal header', '', '.modal-header', { backgroundColor: 'var(--color-surface-raised)' }], + ['modal footer', '', '.modal-footer', { backgroundColor: 'var(--color-surface-raised)' }], + ['list item', '
', '.list-group-item', { backgroundColor: 'var(--color-surface)', color: 'var(--color-text-base)' }], + ['accordion', '
', '.accordion-item', { backgroundColor: 'var(--color-surface)' }], + ['collapsed accordion button', '', 'button', { backgroundColor: 'var(--color-bg-subtle)', color: 'var(--color-text-base)' }], + ['expanded accordion button', '', 'button', { backgroundColor: 'var(--color-bg-muted)', color: 'var(--color-text-base)' }], + ['offcanvas', '
', '.offcanvas', { backgroundColor: 'var(--color-surface)' }], + ['offcanvas header', '
', '.offcanvas-header', { color: 'var(--color-text-base)' }], + ['offcanvas body', '
', '.offcanvas-body', { color: 'var(--color-text-base)' }], + ['toast', '
', '.toast', { backgroundColor: 'var(--color-surface)' }], + ['toast header', '
', '.toast-header', { backgroundColor: 'var(--color-bg-subtle)', color: 'var(--color-text-base)' }], + ['toast body', '
', '.toast-body', { color: 'var(--color-text-base)' }], + ['popover', '
', '.popover', { backgroundColor: 'var(--color-surface)' }], + ['popover header', '
', '.popover-header', { backgroundColor: 'var(--color-bg-subtle)', color: 'var(--color-text-base)' }], + ['popover body', '
', '.popover-body', { color: 'var(--color-text-base)' }], + ['tooltip', '
', '.tooltip-inner', { backgroundColor: 'var(--color-bg-muted)', color: 'var(--color-text-base)' }], + ['breadcrumb', '', '.breadcrumb', { backgroundColor: 'var(--color-bg-subtle)' }], + ['breadcrumb link', '', 'a', { color: 'var(--color-primary)' }], + ['active breadcrumb', '', '.breadcrumb-item', { color: 'var(--color-text-muted)' }], + ['page link', '', '.page-link', { color: 'var(--color-text-base)' }], + ['navbar link', '', '.nav-link', { color: 'var(--navbar-text-muted)', paddingTop: '8px', paddingRight: '16px', paddingBottom: '8px', paddingLeft: '16px' }], + ['simple navbar theme control', '', 'button', { color: 'var(--navbar-text-muted)', paddingTop: '8px', paddingRight: '16px', paddingBottom: '8px', paddingLeft: '16px' }], + ['active navbar link', '', '.nav-link', { color: 'var(--navbar-text)' }], + ['open navbar dropdown', '', 'button', { color: 'var(--navbar-text)' }], + ['navbar toggler', '', 'button', { color: 'var(--navbar-text)', borderTopColor: 'rgba(255, 255, 255, 0.2)' }], + ['disabled button', '', 'button', { transform: 'none' }], + ['busy cover', '
', 'button', { cursor: 'wait', pointerEvents: 'none' }], + ['release notes', '
', '.release-notes', { columnCount: '1', display: 'block' }], + ['dropdown', '', '.dropdown-menu', { backgroundColor: 'var(--color-surface)' }], + ['dropdown item', '', 'button', { color: 'var(--color-text-base)' }], + ['active dropdown item', '', 'button', { color: 'var(--color-text-base)' }], + ['dropdown divider', '', 'hr', { opacity: '1' }], + ['featured icon', '', 'img', { maxWidth: '64px', maxHeight: '64px' }], + ['screenshot', '', 'img', { height: '120px', maxWidth: '200px' }], +] + +describe.each(['light', 'dark'])('%s theme cascade', theme => { + it.each([0, 1, 2])('rounds the outside category filter corners with %i categories', async categoryCount => { + document.documentElement.dataset.theme = theme + document.documentElement.dataset.bsTheme = theme + const categories = [ + { id: 'clients', originalId: 'clients' }, + { id: 'tools', originalId: 'tools' }, + ].slice(0, categoryCount) + await mountFeatured({ apps: [], categories }) + + expect(wrapper.get('fieldset legend').text()).toBe('featured.category_filter') + const buttons = wrapper.findAll('.btn-group > .btn') + expect(buttons).toHaveLength(categoryCount + 1) + const firstStyle = getComputedStyle(buttons[0].element) + const lastStyle = getComputedStyle(buttons.at(-1).element) + expect(firstStyle.borderTopLeftRadius).not.toBe('0px') + expect(firstStyle.borderBottomLeftRadius).not.toBe('0px') + expect(lastStyle.borderTopRightRadius).not.toBe('0px') + expect(lastStyle.borderBottomRightRadius).not.toBe('0px') + if (categoryCount > 0) { + expect(firstStyle.borderTopRightRadius).toBe('0px') + expect(firstStyle.borderBottomRightRadius).toBe('0px') + expect(lastStyle.borderTopLeftRadius).toBe('0px') + expect(lastStyle.borderBottomLeftRadius).toBe('0px') + for (const button of buttons.slice(1, -1)) { + const style = getComputedStyle(button.element) + expect([style.borderTopLeftRadius, style.borderBottomLeftRadius, style.borderTopRightRadius, style.borderBottomRightRadius]).toEqual(['0px', '0px', '0px', '0px']) + } + await buttons[1].trigger('click') + expect(buttons[1].classes()).toContain('active') + await buttons[0].trigger('click') + expect(buttons[0].classes()).toContain('active') + } + }) + + it('allows specific component rules to override theme defaults', () => { + document.documentElement.dataset.theme = theme + document.documentElement.dataset.bsTheme = theme + const override = document.createElement('style') + override.textContent = '.card.custom-card { background-color: rgb(1, 2, 3); color: rgb(4, 5, 6); }' + document.head.append(override) + try { + const style = componentStyle('
', '.card') + expect(style.backgroundColor).toBe('rgb(1, 2, 3)') + expect(style.color).toBe('rgb(4, 5, 6)') + } finally { + override.remove() + } + }) + + it.each(componentCases)('preserves %s styles', (_name, markup, selector, expected) => { + document.documentElement.dataset.theme = theme + document.documentElement.dataset.bsTheme = theme + const style = componentStyle(markup, selector) + for (const [property, value] of Object.entries(expected)) { + expect(style[property], property).toBe(value) + } + }) + + it('themes the rendered official and platform badges without Bootstrap background utilities', async () => { + document.documentElement.dataset.theme = theme + document.documentElement.dataset.bsTheme = theme + await mountFeatured({ apps: [{ id: 'sample', name: 'Sample App', official: true, platforms: ['linux'] }] }) + + const [official, platform] = wrapper.findAll('.badge') + expect(getComputedStyle(official.element).backgroundColor).toBe('var(--color-primary)') + expect(getComputedStyle(official.element).color).toBe('var(--color-on-primary)') + expect(getComputedStyle(platform.element).backgroundColor).toBe('var(--color-bg-muted)') + expect(getComputedStyle(platform.element).color).toBe('var(--color-text-base)') + }) +}) diff --git a/third-party/dockle b/third-party/dockle index c1304a7ccab..13ef435901c 160000 --- a/third-party/dockle +++ b/third-party/dockle @@ -1 +1 @@ -Subproject commit c1304a7ccabf41c9ab53579289174d7bf820b895 +Subproject commit 13ef435901c2db762dc859a66272fe3460385c20 diff --git a/third-party/libvirtualhid b/third-party/libvirtualhid index ecc82912810..dfff3ea8c34 160000 --- a/third-party/libvirtualhid +++ b/third-party/libvirtualhid @@ -1 +1 @@ -Subproject commit ecc82912810fe09957abb9b8824255381a5f0fdf +Subproject commit dfff3ea8c34022c915799740806c17c0ed5c2492 diff --git a/third-party/moonlight-common-c b/third-party/moonlight-common-c index f900dd47677..aeba988e0a3 160000 --- a/third-party/moonlight-common-c +++ b/third-party/moonlight-common-c @@ -1 +1 @@ -Subproject commit f900dd4767759c7b9d0e93bcea666b55c69ea62f +Subproject commit aeba988e0a302145eff2e70fa3e11cd98ec01f5d diff --git a/tools/audio.cpp b/tools/audio.cpp index 0b8ca3b65c1..dbe96b4b805 100644 --- a/tools/audio.cpp +++ b/tools/audio.cpp @@ -35,17 +35,17 @@ namespace audio { CoTaskMemFree(static_cast(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 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 wstring_t = util::safe_ptr>; - using handle_t = util::safe_ptr_v2; + using handle_t = util::safe_ptr_v2; class prop_var_t { public: diff --git a/tools/dxgi.cpp b/tools/dxgi.cpp index f1f63e1c118..a52e656bcab 100644 --- a/tools/dxgi.cpp +++ b/tools/dxgi.cpp @@ -19,9 +19,9 @@ namespace dxgi { dxgi->Release(); } - using factory1_t = util::safe_ptr>; - using adapter_t = util::safe_ptr>; - using output_t = util::safe_ptr>; + 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[]) { diff --git a/tools/sunshinesvc.cpp b/tools/sunshinesvc.cpp index 9c2fef8585f..a5363f3f443 100644 --- a/tools/sunshinesvc.cpp +++ b/tools/sunshinesvc.cpp @@ -24,7 +24,15 @@ HANDLE session_change_event; constexpr auto SERVICE_NAME = "SunshineService"; -DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext) { +/** + * @brief Handle service control and console-session events from the Windows service manager. + * @details Event data and user context are unused but retained in the native callback signature. + * + * @param dwControl Service control code. + * @param dwEventType Session change event type when handling SERVICE_CONTROL_SESSIONCHANGE. + * @return NO_ERROR for handled controls or ERROR_CALL_NOT_IMPLEMENTED otherwise. + */ +DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID, LPVOID) { switch (dwControl) { case SERVICE_CONTROL_INTERROGATE: return NO_ERROR; @@ -185,7 +193,11 @@ void TerminateSunshine(HANDLE console_token, const PROCESS_INFORMATION &process_ } } -VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) { +/** + * @brief Run Sunshine in the active console session until the Windows service is stopped. + * @details The service manager's argument count and argument vector are unused; the native signature is retained. + */ +VOID WINAPI ServiceMain(DWORD, LPTSTR *) { service_status_handle = RegisterServiceCtrlHandlerEx(SERVICE_NAME, HandlerEx, nullptr); if (service_status_handle == nullptr) { // Nothing we can really do here but terminate ourselves @@ -311,8 +323,7 @@ VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) { { // Sunshine terminated itself. - DWORD exit_code; - if (GetExitCodeProcess(process_info.hProcess, &exit_code) && exit_code == ERROR_SHUTDOWN_IN_PROGRESS) { + if (DWORD exit_code; 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); } diff --git a/uv.lock b/uv.lock index 7e200527726..2f62a4806a0 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.14" [[package]] @@ -13,7 +13,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.14.3" +version = "3.14.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -24,49 +24,91 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/4c/bdccd81e9ee225b69c60e7766c9a5b05364f118f4d383713b89a682d772d/aiohttp-3.14.5.tar.gz", hash = "sha256:5558a7f5a05af9ecf744af91e5baefc436f93c9333e656c27ec253f9a6bbe178", size = 8078052, upload-time = "2026-10-11T01:05:12.408Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" }, - { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" }, - { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" }, - { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" }, - { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" }, - { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" }, - { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" }, - { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" }, - { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" }, - { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" }, - { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" }, - { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" }, - { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" }, - { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" }, - { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" }, - { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" }, - { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" }, - { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" }, - { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" }, - { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" }, - { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" }, - { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" }, - { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" }, - { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" }, - { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" }, - { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" }, - { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" }, - { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" }, - { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" }, - { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" }, - { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" }, - { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" }, - { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" }, - { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" }, - { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" }, - { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" }, - { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" }, - { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" }, + { url = "https://files.pythonhosted.org/packages/43/be/3184a1d34a8be665569eadb7e9e764b4629e4f3413e241cb2e4d6fecf3b3/aiohttp-3.14.5-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:939042d5cda21d41a6f512e7cc8b8e33a2aebff863352251da495fbd91b673b5", size = 540673, upload-time = "2026-10-11T01:01:41.354Z" }, + { url = "https://files.pythonhosted.org/packages/60/2a/d35f3ba4cf157b072e3b674bf9983047ca5ea5173c995d32d877e1191d36/aiohttp-3.14.5-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6da32b5ff3fd78d244e37300463434c7145162bfd2b6e9e915ab164da37f7343", size = 554087, upload-time = "2026-10-11T01:01:43.719Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d2/61a33880ca4eaca95a9c60ca3f6beed15555af1028652dfaac601627787b/aiohttp-3.14.5-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1b438b73c38111818d0c9d6a5c2bfed8584c8e503a49ef085d70e874ec846738", size = 522613, upload-time = "2026-10-11T01:01:46.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/de579b299d2225dc2c6fd99d579d91f16c02a913fb5af9a3cf2fe9bd88ba/aiohttp-3.14.5-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:755933b107ea7a6a9ac916f635a70595a5b1a32fac10a8ff0b9f2ab88555550c", size = 529682, upload-time = "2026-10-11T01:01:48.477Z" }, + { url = "https://files.pythonhosted.org/packages/f4/4a/ddb923564e15e053b6e060b0036e1694dcadcb13aa476c5a87dcad20e336/aiohttp-3.14.5-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b3cc509327c7b27f6f4727a8830f4004f6df7766e179f2f4b8e54e65c0bec5d3", size = 540120, upload-time = "2026-10-11T01:01:50.474Z" }, + { url = "https://files.pythonhosted.org/packages/3d/36/a640fbecaa53727a5900b892bdbe17b5f3e8cc88903e22864fe41b654def/aiohttp-3.14.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7bd8ac754ebd6733a3e2a0dd1674c4d8ab086196803fd8dcd776f07b4e2607d9", size = 818795, upload-time = "2026-10-11T01:01:52.665Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b6/d52ca608859e271b5fa7944074802dc45f60e52c318a4ddc34edbf73586e/aiohttp-3.14.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e724a7b6091f0b1ac064f9d1b15ff9ec52e6033a86cdae649e5f086e32a3c0db", size = 549667, upload-time = "2026-10-11T01:01:54.65Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b5/05b8ac39a76ff4bca89f42a4c2471560c71f894ff6e4bc16158c951874e3/aiohttp-3.14.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c32e26310cc10e547f53cd13d39a369034f69dcb7d749d5cb0e5f67bc196b6ba", size = 548922, upload-time = "2026-10-11T01:01:56.547Z" }, + { url = "https://files.pythonhosted.org/packages/19/b0/5aa186d56ce2334dabe29b70bd99dc8ae926ee44184c0de64a53d415a4f3/aiohttp-3.14.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1eb8167961ec4dfcc8cb9dd50bd0ee72519f7ef496be95203e49e27b01618382", size = 1882422, upload-time = "2026-10-11T01:01:59.258Z" }, + { url = "https://files.pythonhosted.org/packages/db/f7/7d5c91bb9620db300c8ddb05337a9014301acb626223faff3abcb8ea47d7/aiohttp-3.14.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c1d60eafd9c7e8e74abd03a5b00df44e7febfe6d9b89b559c0a6551eef0699d4", size = 1843621, upload-time = "2026-10-11T01:02:01.417Z" }, + { url = "https://files.pythonhosted.org/packages/30/0a/b208953b96d8f24b75f6da704f508e6c5cf3022f52b60c61933df082e89c/aiohttp-3.14.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:137351bf20bbed9a65e839f4a4452ac377389bdb2f2857d2acffef38f5e9f2d1", size = 1927915, upload-time = "2026-10-11T01:02:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/f6/79/90ebcccb55e2d1e11a1fed581d83bb966e38fb35fb4b2577fdc980f8707a/aiohttp-3.14.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fba47bc2c3d7303c3d027c6cf4d07626c37b1314ac81f5820c31032e0ca1f677", size = 2036339, upload-time = "2026-10-11T01:02:06.046Z" }, + { url = "https://files.pythonhosted.org/packages/a6/66/55a8904b3a129fafdf94f9cc0a2e4ca09a3c914650be52355db7ad0bbdb6/aiohttp-3.14.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94684b879ac1d71e4238850c99b62dc1b28d9086b156a2555f082010b85a865c", size = 1888041, upload-time = "2026-10-11T01:02:08.384Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b8/96b25da7329a52e42c812b1e8b076386039ec4fc312afa043d173d8147fc/aiohttp-3.14.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56572c42e3ecd636de8d2c3dd54cf5fc939cb5c32eb56297f176a0d366fac622", size = 1703849, upload-time = "2026-10-11T01:02:10.903Z" }, + { url = "https://files.pythonhosted.org/packages/1b/43/fbf976e3ae4c038d6f5c84945ab2150298c2d71201674d4e53d158063e75/aiohttp-3.14.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a95529a92a446db351675f4aab518feaf5e99842f63f5dd17160c2b74f382db3", size = 1849703, upload-time = "2026-10-11T01:02:13.15Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7d/218e912f4c1d89bde7ac551409be57ad2f6121638e56942d395a7cb1fa58/aiohttp-3.14.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:3edbece0379b8b4aaa67619b8aa2399bb66fce372cd5911098a434ea77220aa0", size = 1852695, upload-time = "2026-10-11T01:02:15.295Z" }, + { url = "https://files.pythonhosted.org/packages/5a/42/252a1b9287e3b6e393a1c3bd1776f36af30f5f25f071bbf2b0cb7eba9116/aiohttp-3.14.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:56d9828f204331a5ca8850fcfe2bcce95a149f1f223f60cc7216e5524978e480", size = 1893493, upload-time = "2026-10-11T01:02:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/78/97/71cae83d5100556fad1521684f7cd1e3e578432644f850245ed3bd969310/aiohttp-3.14.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:20064a177a070d789ee64a50b01a9161d3468e989baacfc6c714aa685c4b332f", size = 1693468, upload-time = "2026-10-11T01:02:20.666Z" }, + { url = "https://files.pythonhosted.org/packages/1f/69/73d88e97a8b5f0ca7a946d0011c0de99fb188b1687c7948ecd0553dc5bf0/aiohttp-3.14.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:81c2b3dfd56c62bee6108e4852d5970b4cf9086390b6983f52b666e878c1f115", size = 1915398, upload-time = "2026-10-11T01:02:23.011Z" }, + { url = "https://files.pythonhosted.org/packages/05/f0/881644bcb15d4b258daea9b720a0af9dc4330496cc8d6ade9090cdd0cffc/aiohttp-3.14.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:09ec102b4b8c9a920275733bbc11fdbb615efe6f9231a06007c0218d336fb77a", size = 1871470, upload-time = "2026-10-11T01:02:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/7f/de/19d9ebbcce5aedaa3242d8a99ff8816a60bdb7629fb0084bf4a45bfd1f62/aiohttp-3.14.5-cp314-cp314-win32.whl", hash = "sha256:9c428eb2bd8817588d16a0ab898aa4eb5d141f896aa2b394cc79a4cf61d9a8e2", size = 485517, upload-time = "2026-10-11T01:02:27.579Z" }, + { url = "https://files.pythonhosted.org/packages/a9/74/8cdaf0e58c2588371670d5a9a8215bbb971d36940b6e5d051967dd05c07d/aiohttp-3.14.5-cp314-cp314-win_amd64.whl", hash = "sha256:6f967dde489ca6a8c02d093ab245d2cbf50ccb5c36adf0188b17b0ca39d24b67", size = 511755, upload-time = "2026-10-11T01:02:29.685Z" }, + { url = "https://files.pythonhosted.org/packages/1a/6b/e0100e25502430a531c7cf1482a378d0b65bf728ab60c01ee270e56bc469/aiohttp-3.14.5-cp314-cp314-win_arm64.whl", hash = "sha256:1d2d981b53dd09a319e3570ef8cc3bbc3ef86f5a7abef0f6b2bff3867db3a9e7", size = 495151, upload-time = "2026-10-11T01:02:32.163Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c2/ca2ead7b655688c53c03aeeb6e96e6851c9ff08d6be7b13802f53a6ae8fd/aiohttp-3.14.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9ce66feae6ac65327379460380549bf1b8df8e17c4e25df2a2bcf168272e3bed", size = 855396, upload-time = "2026-10-11T01:02:34.443Z" }, + { url = "https://files.pythonhosted.org/packages/a7/70/22206fea409255a240c926ce11de48de354ae2bb90ca44f709c05497585d/aiohttp-3.14.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:27c2322e03f66101acb09869ce1cf1efc04994ee95e1735b69827bf8c8b9d781", size = 567734, upload-time = "2026-10-11T01:02:36.644Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e5/79a36c118308b56f8667d67e05d2fb6dc638ab45985704cdb199631bedaa/aiohttp-3.14.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff75a7537413a86e7cafe98e0e1d6e3dc4b15c6349896e7d5c6b881bfdb6d550", size = 567379, upload-time = "2026-10-11T01:02:38.757Z" }, + { url = "https://files.pythonhosted.org/packages/63/a3/2ebec7dece3b1f02c30d2e484647f6f7b13952b1bb40a4cb285b849e8432/aiohttp-3.14.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c061aa954daaf57d2a4b8374f9fca621ef0e1b603584431c220c22458c59b6d", size = 2050686, upload-time = "2026-10-11T01:02:41.169Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d2/7e4d093db2f4450482652e7ef19a9e19919028f5135aa52bc4078c3beb80/aiohttp-3.14.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1612fa5857b37bf32e5c1eaeefb96e3b01e9c70679eec81f0934e8a600080863", size = 1915077, upload-time = "2026-10-11T01:02:43.563Z" }, + { url = "https://files.pythonhosted.org/packages/a6/88/bd40d09958442a0a1df67da81de496361d6e2afc04f9db2950d835da750b/aiohttp-3.14.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:adbeee7d6fd4cf5fe0aece2fb3edc4243615d3180430ba8149d01a90670cac99", size = 2027204, upload-time = "2026-10-11T01:02:46.185Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/3a601c1f8d3103c1a60ea981f20924855da9f575d2007fc38f8898b792fb/aiohttp-3.14.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b2966998927d7bed9db12c0a4647b0c7b179755878fc9c357fe1ffd3e3b0c1a5", size = 2149520, upload-time = "2026-10-11T01:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/22/d0/4e41bfe1b1ce1cb6f6d2e59fa7a88ef5cf92c402b2d07e9018778ba9edbc/aiohttp-3.14.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e317e0fb6b16212c881d2205a7d87414c29acd69320b3aa6dce9d9c7b86fe4f", size = 2002039, upload-time = "2026-10-11T01:02:51.293Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/72067019545c502b881b031153c437752ecef48d7d213bc0218ccebb4bfb/aiohttp-3.14.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50343c1757b4b6f6708eeaf24534b32f19dfb99fb1b762c00420867a62fc81e0", size = 1786164, upload-time = "2026-10-11T01:02:53.533Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fe/7741efd6119bfb7a00827fe6f7b84b4409de58d888ae21adc5a9a6824992/aiohttp-3.14.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:083673c7a94c3ea035caaa5ca04288bdb44887abfe1f5ba23294e6a4b03efd2d", size = 1970689, upload-time = "2026-10-11T01:02:55.888Z" }, + { url = "https://files.pythonhosted.org/packages/43/e7/342a13bf67f34d269bf2f7e870ecd72b99c832cc6f0a271a9210c0ebfb84/aiohttp-3.14.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2528cb4c6b92008c76ac9ac6298624069bb2db91ff4929905512d1d84485f658", size = 1951967, upload-time = "2026-10-11T01:02:58.467Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d4/fdb3b27340617e5e64df18a70fa89097778652ea5c7c7e2f79def76999c3/aiohttp-3.14.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b1b8ece1e71132d2afba4dbc0c3d62c766e25165990b25db1196c04969eb3d84", size = 1968511, upload-time = "2026-10-11T01:03:00.883Z" }, + { url = "https://files.pythonhosted.org/packages/83/b2/e8f88298de78d1a951f36f9f966d38ec6ed1d4721303ba02064a545e8aa6/aiohttp-3.14.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fce9523df31cea6284f3e2c479876750d7687cf671d7b25d32b19effc0e86441", size = 1778497, upload-time = "2026-10-11T01:03:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/22/68/9ccdb93d664345c546be7f34480b921774d8c0d98f47e70d7e03b115d475/aiohttp-3.14.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:09e0eb18c7e0c8777e2f9149de63799195b9b3ca1b5c81ba6f32f2c6b8628210", size = 1999676, upload-time = "2026-10-11T01:03:05.829Z" }, + { url = "https://files.pythonhosted.org/packages/57/4a/a33cfa6dcb00e94194ae4fe710432ca4ed111e016356b4ba4d2f4c3a124c/aiohttp-3.14.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6774814fd5c338e72ee0da5cbb9432816df450e69c019f72b5d29bdec2a1792d", size = 1963437, upload-time = "2026-10-11T01:03:08.196Z" }, + { url = "https://files.pythonhosted.org/packages/f4/20/eacbecfea3b5c3dcbfc9b023e3a5460f43e5dda16d06a2877ebe7184c3f3/aiohttp-3.14.5-cp314-cp314t-win32.whl", hash = "sha256:33f706574e32c6e694f352a856e05caf18f7f2c871b3e87b41c55ea452b409ab", size = 503973, upload-time = "2026-10-11T01:03:10.481Z" }, + { url = "https://files.pythonhosted.org/packages/ff/78/18eec294f6c8c5dc845dcf6d730a0147d8d0f17e86138a7bdb85e43a30fa/aiohttp-3.14.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5ba14a839fbe87cf7c12a6b5661c05f324a296eb8363141edb3944ba63d4c9d3", size = 531190, upload-time = "2026-10-11T01:03:12.716Z" }, + { url = "https://files.pythonhosted.org/packages/9d/39/e53f8169acc85271ebd12b5b32ad7f1541b35639ccbe0f49034c64785d10/aiohttp-3.14.5-cp314-cp314t-win_arm64.whl", hash = "sha256:1061b364556e8172e8d46b0b183adeeb73e8c42d30ebc745591e1bd89acad52e", size = 510360, upload-time = "2026-10-11T01:03:15.08Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1c/06d89f58b2db3ee92dd377217659d587e06f973e57bf0a97a0d8a4586c0c/aiohttp-3.14.5-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:788ecaa9c10533b786ce5ba70c4f2df78ad41819fd00a6c99d92b66f9a32e1da", size = 540674, upload-time = "2026-10-11T01:03:17.483Z" }, + { url = "https://files.pythonhosted.org/packages/18/39/5e822e038f496f0540ada91e27099d48f9e7f919b6c6deb4cf6db36cc706/aiohttp-3.14.5-cp315-cp315-android_24_x86_64.whl", hash = "sha256:5c76f1802bab718a68ac3cce447160605c734551f95c67ae90fa1132b215cb29", size = 554183, upload-time = "2026-10-11T01:03:19.71Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/0cf2619d902b5b160762422a7e5e02091295766a7fe4fcf5b9a655e386c1/aiohttp-3.14.5-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:a6d02b4c38de03d9c7617813433e6a0fb6b522797974177d69d9dad431900833", size = 523160, upload-time = "2026-10-11T01:03:22.193Z" }, + { url = "https://files.pythonhosted.org/packages/86/99/3553abfc53a40849dbaacc3f54c730ec58410809ffa2d05885ae56108f2d/aiohttp-3.14.5-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:20726f9782d5c2744c1c66255842d1d163bb3edcf768b8de25216bf47f7b6ccf", size = 530311, upload-time = "2026-10-11T01:03:24.435Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a4/5d25f73754bc1e8f983ba704e86d641aea290c967f195f2aeebdaed2bd84/aiohttp-3.14.5-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:248d779ad720b49d4fb355720e60c9e5f444f95887bc16974fea48fc56c41789", size = 540274, upload-time = "2026-10-11T01:03:26.698Z" }, + { url = "https://files.pythonhosted.org/packages/29/a4/07eda5db2e3ee017d9590f36c12a94ec6f1f50516e8df78672373dfc7185/aiohttp-3.14.5-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:0a8ea271867e360ac985ae607f4a23ad9a38414b9aca1d49ec98839ae660e49f", size = 819239, upload-time = "2026-10-11T01:03:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/a8723dd987a696dd48d4cf2f0088e589ce77caebff0b96f2a78c20424380/aiohttp-3.14.5-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:823c910f046f23f4c713b8d99a2242dc65f591cb45ee86418fa11762a3c2963c", size = 549832, upload-time = "2026-10-11T01:03:32.02Z" }, + { url = "https://files.pythonhosted.org/packages/29/5c/969a1b72692055fd2a419590c41847ec9144fefb97b75ff1cde5b6372891/aiohttp-3.14.5-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:9b42db919715e91eb76acf3bc492a9a7ccd8bd9adc6745c1412b689735269f14", size = 549234, upload-time = "2026-10-11T01:03:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/38/05/8e3e07fd8a0d33d06955ff4e54a1cb92f4bce347ff55e441dc3e25a7b5e5/aiohttp-3.14.5-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3112585250b199296c26ca6e0131640b6a8d01bab8b232d2eb3763ed469de11", size = 1891314, upload-time = "2026-10-11T01:03:36.899Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b3/05a79ce2e25f024e93de30c94f39dc6aa6e2bc1e9c531a5c4b18dc61b7a4/aiohttp-3.14.5-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c147451b4a58e7050f7f7394e6c467867c84161560001f9ad4fb2d1446743946", size = 1884372, upload-time = "2026-10-11T01:03:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/94/52/0fd8af0717db109eea258191b326b5cb5847fb88928bfa6c862fd78b9ad3/aiohttp-3.14.5-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bf163cc701f3d4ac43ba7d97771bf5fd955220ef5500ef3ee847bc0ecfbf4ec1", size = 1930957, upload-time = "2026-10-11T01:03:42.172Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b3/fa78733da88812bf9fb193913fb0ce1548f8b6912047633fdf88a758ff8c/aiohttp-3.14.5-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8d40ce41991e9d56fab4f5dc4a51fe59bc3b5c77c27f4b148963064d00232e8", size = 2038886, upload-time = "2026-10-11T01:03:44.646Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f5/2fcc5e30053a938286f17d0edf3f0850b8061b984256fa7c26850b9c8809/aiohttp-3.14.5-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:276a4fc00b1d9ae492b802763a789c5b86328b989c5ea169f2faa447d6a11c7c", size = 1894470, upload-time = "2026-10-11T01:03:47.304Z" }, + { url = "https://files.pythonhosted.org/packages/62/2a/f87feb42abe8e6a7c03814dbcb711540849a1e90aa392055f3183c643610/aiohttp-3.14.5-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50983e3be33d8c0942ab88cec3905b10602f64c469b20153c48c5d4e558dd016", size = 1716273, upload-time = "2026-10-11T01:03:50.121Z" }, + { url = "https://files.pythonhosted.org/packages/81/b2/adf1f960dd977722ed1347d33da512a1624807114f57a3b91f5cc828e081/aiohttp-3.14.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:16c8abd5bca220a47efe667d26f8460124c81810787e79ee87b242677563d9dd", size = 1857513, upload-time = "2026-10-11T01:03:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/ef8d910e641de4160026a513ace5888b7f92826bbc3fd90ced05d55a828e/aiohttp-3.14.5-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:0790ec66fa4013e83c53b9025a45d454723da1a2fce28b3208c9b32d08af162f", size = 1887704, upload-time = "2026-10-11T01:03:55.641Z" }, + { url = "https://files.pythonhosted.org/packages/7e/db/6c9142f941cba8d35be8e1fae6ea2bd390e754fc14076b8147aee1a4592f/aiohttp-3.14.5-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:cb11a971a3aea10f9b8373be628f1df932964fc6c6b174516d318a48c3ac4412", size = 1895684, upload-time = "2026-10-11T01:03:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/e6/7c/6a6bd9a72e576d376c668333b00c51c6147aba4fb863ed6c94996d497eca/aiohttp-3.14.5-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:932ce7e694bbc29b2bf6f64f2343c27d148d4997c771d01bdade4639b6749ff4", size = 1706299, upload-time = "2026-10-11T01:04:00.8Z" }, + { url = "https://files.pythonhosted.org/packages/69/ec/d2cc494242f8c4d3d1cd70baf591742818a74386dc3b84af3195c5c4fced/aiohttp-3.14.5-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:a7d470cf7b206e6359fc77b1b860632fde400d5a2ed59cd0181b93a686bc81ee", size = 1916645, upload-time = "2026-10-11T01:04:03.892Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6e/e852c53647e815db09a1b6b5ab634f54bd736412397e11940feef4d2c89a/aiohttp-3.14.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:6e1d8637cf73eebc92eba2e11d4cfff98a3b562f2505bd75bba766d908926e8d", size = 1875408, upload-time = "2026-10-11T01:04:06.922Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f6/72ab6ef20c332399be593bac543d2c24a6d241e39d3540ce9f95a63e4bd2/aiohttp-3.14.5-cp315-cp315-win32.whl", hash = "sha256:fbdc5ec49f9ca3cd24955cf3520b10a4d4c901ba2572094c84274e9e7eb30534", size = 485590, upload-time = "2026-10-11T01:04:09.626Z" }, + { url = "https://files.pythonhosted.org/packages/06/eb/e9de75b8c6d2170c42c08ff303abf857ea8a6d9d9b6e99b5aba40f15e962/aiohttp-3.14.5-cp315-cp315-win_amd64.whl", hash = "sha256:a9d3983bd6ab7aa1cfd573544ae98df9b6cb6912a5185a198263e024a636861d", size = 511740, upload-time = "2026-10-11T01:04:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/fc/25/455f3c2785eb0d50748cffd0abd07500815f419a9495b14610b7622d8d2d/aiohttp-3.14.5-cp315-cp315-win_arm64.whl", hash = "sha256:e29347c142cf6e99e0dff5e2995ead1d50fa3b51bf37a7c726a7ccfe5419745a", size = 495171, upload-time = "2026-10-11T01:04:14.667Z" }, + { url = "https://files.pythonhosted.org/packages/e7/6c/497f0a98782eebfcf0f02a7fbdef5428148bd426027140cbad494cf842b5/aiohttp-3.14.5-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:9bf1d5dcc15204d9ec8b8ea4c18fd66e6b80e5de1f4ecbafb3a2f2740f8039d4", size = 855638, upload-time = "2026-10-11T01:04:17.154Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c5/55c0cef2572af9b1ee81608f7c0a1bf74e6c9151a73b04915d933f192335/aiohttp-3.14.5-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:14f04769cfefe4734016a856a83af36133cd17779cef9ae817f812b8ba9d6d51", size = 567455, upload-time = "2026-10-11T01:04:19.702Z" }, + { url = "https://files.pythonhosted.org/packages/4d/47/e1a0e39f4a2b881f6071225afaf94a6547001b5aefc1566734d73c685934/aiohttp-3.14.5-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:6e4251c0ba4624a68a2c11471a1ac54c3306876c21f0ae86de085cc9241c8905", size = 567836, upload-time = "2026-10-11T01:04:22.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1d/817d85836f52b687160064a326e62e037dc42f1ad5b6b5188a70e5c134b5/aiohttp-3.14.5-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4f5cf4dc72a71c4cfa9751b4950be22f733626670230d46e7d606592aa22d59", size = 2051409, upload-time = "2026-10-11T01:04:24.821Z" }, + { url = "https://files.pythonhosted.org/packages/f7/25/e8ea6fc212a9aabce982917346ce8ecab0929c74b60232a056dd11c99da0/aiohttp-3.14.5-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dbf53ae2601b7fd5a93c3944deea3a78d40f495226d582c35ef7a433425ce2b2", size = 1936493, upload-time = "2026-10-11T01:04:27.456Z" }, + { url = "https://files.pythonhosted.org/packages/24/33/de0517f71f1a19feea4aff78a2ec4ec2d98634129ec30ead78fce2f8f81b/aiohttp-3.14.5-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:657291433bf4dd3142f3abac495764cd47d0c7c92087751e6666c6447e65fcef", size = 2021405, upload-time = "2026-10-11T01:04:30.276Z" }, + { url = "https://files.pythonhosted.org/packages/c4/11/ddaf2e7930543e9f0cad3a1c54e1af0c1bd2fae3973d568c4263d3b010e9/aiohttp-3.14.5-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e51a27980c3788e6e6b3325d694fdd4898087fa8a86b2763af77b39353da41e", size = 2142017, upload-time = "2026-10-11T01:04:33.022Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7b/58784353c06de8adc20f426daad3d85dd86fd55331c713a3f4b638c94573/aiohttp-3.14.5-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82c7583cd3dfdc7dcc927835b4f6c7faae7ecc1ba3ca5879321621ae2e6f8e84", size = 2002482, upload-time = "2026-10-11T01:04:35.94Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f0/417d9535caa9e165ffe6a53e2347a78107e7c87dcb0e10aca315c3af0344/aiohttp-3.14.5-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6fdcd6af7e2e51d1ba1b4bea16e97b074bcb7b5dd0246a9d8201341bb28085a0", size = 1791774, upload-time = "2026-10-11T01:04:38.733Z" }, + { url = "https://files.pythonhosted.org/packages/98/01/25e49c2e8a01b9f0e19ca0a8448ad50aa2bdf96c8cd41e92bd45af044784/aiohttp-3.14.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c8859a013ae0de1074660992139a1a440df3e6b219b86cf0d3f11c2692bb4fe3", size = 1970358, upload-time = "2026-10-11T01:04:41.66Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fc/c132fd3465b6c7e4ce0193154f602c3e6c46b680e4da7eb3bd0d8a40d1c7/aiohttp-3.14.5-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:8966ecac808dd5f473c9c4cefd10cd3ffda71c18a4d3493b7c7d2ae1803bf2cc", size = 1969192, upload-time = "2026-10-11T01:04:44.66Z" }, + { url = "https://files.pythonhosted.org/packages/95/4e/d58b45e7dba4eb607eca11fc0a4aa77ae0f39c11afa804635a61ce18cff4/aiohttp-3.14.5-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3093b72c215bda16ce961a6d073f6e71d46e022962a9d5d457c5d4d421c78b57", size = 1963618, upload-time = "2026-10-11T01:04:47.505Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d9/f6ac50946efb3490428ef52b56e62c1c6b7f9c6ff3ec6083535526c83e60/aiohttp-3.14.5-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:149fb56caf7acb67073126f675d0958d9c4b3125fcd3f6d4877df98aa8a97ce9", size = 1784817, upload-time = "2026-10-11T01:04:50.287Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2f/f255eb63da788cd8a452fe350869c03266ccad7d884925f6963c87808f24/aiohttp-3.14.5-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:293d3ae7c6a0ed176a42e59a1b5fde825ead65c835360f734148e96729f928d2", size = 1995956, upload-time = "2026-10-11T01:04:53.213Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/241aa3393eaafda0034470add1625f82a4c252901108723b283a9b0b32ca/aiohttp-3.14.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:3f2dcc00191fd563e9075181a14ec31d7dd63223ced7582cc70a15a499de0c79", size = 1967794, upload-time = "2026-10-11T01:04:55.958Z" }, + { url = "https://files.pythonhosted.org/packages/5f/7f/a68e689288c9e4bfcf8d0979f2b12b774bf01861985420df6150eba5448e/aiohttp-3.14.5-cp315-cp315t-win32.whl", hash = "sha256:7779cd97e61ebe583ec2f1c5616cdd038aa08a4453b1848c67842176d054948e", size = 503876, upload-time = "2026-10-11T01:04:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/a4/78/49b0299da6d54de19fc6fdc6889d50233ac47d192a461624f4fc010fde83/aiohttp-3.14.5-cp315-cp315t-win_amd64.whl", hash = "sha256:0e6f16f5e49c4b8267988c05ab07760d7064cea57d077c3d068d04b0fbb992cb", size = 530957, upload-time = "2026-10-11T01:05:02.23Z" }, + { url = "https://files.pythonhosted.org/packages/21/d4/b0afc936aeb6d2f93157e3408b069ec5d7934429ae7d023de5e0953b1887/aiohttp-3.14.5-cp315-cp315t-win_arm64.whl", hash = "sha256:1aead151c3abbac6b32942e452020cb66d7efc099d253cc6c20f748e926c858b", size = 510408, upload-time = "2026-10-11T01:05:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/68/30/173960c42b05a6c59f7558e4b12a4b0d9ba376cf6aa9bde7f9e08a30ca8d/aiohttp-3.14.5-py3-none-any.whl", hash = "sha256:efc21a454892828368b11c2c780de0ff8bc991f73f6b99c6b66e56205470929b", size = 279517, upload-time = "2026-10-11T01:05:08.523Z" }, ] [[package]] @@ -421,127 +463,175 @@ wheels = [ [[package]] name = "markupsafe" -version = "3.0.3" +version = "3.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6", size = 153777, upload-time = "2026-10-02T23:07:22.29Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c3/a944f3b0df22bd129e96915b9f4e98d2eeca6516687d7618304a966c3c74/markupsafe-3.0.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed", size = 12787, upload-time = "2026-10-02T23:05:30.971Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d6/a44863f69d88b6c7e27889108f70d47aed259edf89d5df3c5fca1eac87d6/markupsafe-3.0.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59", size = 12525, upload-time = "2026-10-02T23:05:32.263Z" }, + { url = "https://files.pythonhosted.org/packages/17/8f/168ba80e532dd6a93f96f8f706f1ad41d7990b6e1aeedc1cc0d211a33497/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453", size = 11589, upload-time = "2026-10-02T23:05:33.251Z" }, + { url = "https://files.pythonhosted.org/packages/32/b3/aa2c95a574d3af39403a469b295886eb9b6d448da568cbebb5a2cbfdc2e5/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b", size = 11801, upload-time = "2026-10-02T23:05:34.315Z" }, + { url = "https://files.pythonhosted.org/packages/60/d0/34b810107d83840e768bf485de795893ebbae35b26ab061b487adfa0a692/markupsafe-3.0.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6", size = 11447, upload-time = "2026-10-02T23:05:35.302Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ab/2f8488f0f817a39fca068d2b17daf446bf5cdb3eae28c3720af534d873b4/markupsafe-3.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634", size = 11722, upload-time = "2026-10-02T23:05:36.363Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/e2d117b048d47282ade906fbfd92814cbee5647afc13fda88a3406039372/markupsafe-3.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f", size = 12061, upload-time = "2026-10-02T23:05:37.397Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a8/73a81135e85ba66217f5af7facb03bbb386807e1a729ab64532e4c802652/markupsafe-3.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9", size = 24434, upload-time = "2026-10-02T23:05:38.407Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/fa9216dd01efee2dfdacafe7df32b4d0170fbac694b0c258a193d6e53999/markupsafe-3.0.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f", size = 27183, upload-time = "2026-10-02T23:05:39.581Z" }, + { url = "https://files.pythonhosted.org/packages/fa/4e/a469509e538d37af51103b17b073126973f2b1cbf197ff32c7ddf025cfe5/markupsafe-3.0.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c", size = 24813, upload-time = "2026-10-02T23:05:40.671Z" }, + { url = "https://files.pythonhosted.org/packages/8f/db/d7282caf7ab03af44d5d6fdbaa019b35c7d7f1c90588b839c07cba640d6a/markupsafe-3.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300", size = 23049, upload-time = "2026-10-02T23:05:41.864Z" }, + { url = "https://files.pythonhosted.org/packages/30/f3/b6a425206e6964efda6acee544d0eb01d1501784d0b8e2dcc74986f33b17/markupsafe-3.0.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0", size = 22088, upload-time = "2026-10-02T23:05:43.014Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8a/84d3582fc1f0d5bd466cdf2eebf175e172158a6e70701aacec1de1b35430/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977", size = 23846, upload-time = "2026-10-02T23:05:44.098Z" }, + { url = "https://files.pythonhosted.org/packages/1c/65/db101cce51b7ba4864ac491a9859d297dd1adf0e55b103fee9db9c47c527/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7", size = 22674, upload-time = "2026-10-02T23:05:45.23Z" }, + { url = "https://files.pythonhosted.org/packages/e0/49/ddee9813d71db0c7a5c9d97c832125e6758a0c844777f1cf076569bb0e22/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17", size = 24104, upload-time = "2026-10-02T23:05:46.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0e/7d8518d726726870a2399d69fd30d0fa36c5e57a2132c336b58d7c491073/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c", size = 21637, upload-time = "2026-10-02T23:05:47.48Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b0/b505e8a361ba557dbf3b3aa7331ea39b00d2022a26e925ff8463b9714bb3/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4", size = 23081, upload-time = "2026-10-02T23:05:48.611Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/9cc3cea873f980c75cbdb6f4277ce30ee955de38be0b3d02f14c108e0698/markupsafe-3.0.4-cp314-cp314-win32.whl", hash = "sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c", size = 14272, upload-time = "2026-10-02T23:05:49.707Z" }, + { url = "https://files.pythonhosted.org/packages/80/f0/5792ff768a410f93ee3f84fc19345295ffc352d2c936b424cb37e514714c/markupsafe-3.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe", size = 14501, upload-time = "2026-10-02T23:05:50.788Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cf/3d074a8edffcc6899355232ff2543ae8d929733239596423b7db79698bc9/markupsafe-3.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a", size = 14364, upload-time = "2026-10-02T23:05:51.857Z" }, + { url = "https://files.pythonhosted.org/packages/d9/31/87ce42159aae2163cf3bbbd0c44bc87780510eecab1ea3859099aed95dcb/markupsafe-3.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2", size = 11759, upload-time = "2026-10-02T23:05:52.951Z" }, + { url = "https://files.pythonhosted.org/packages/5f/53/b047207eeb7752e960aca3eb1df5fb7eefa7dd4c62ac49bb156456c8a702/markupsafe-3.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977", size = 12096, upload-time = "2026-10-02T23:05:54.066Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/4326c88a13c7b755657d44b4bb986f8c3d9843ecba7e22d98661d87f9a57/markupsafe-3.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289", size = 25699, upload-time = "2026-10-02T23:05:55.15Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bb/990581b7474bfcf2cf34bed6ba5ea23bd87adb9d671213d68e88620e7a6b/markupsafe-3.0.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe", size = 27804, upload-time = "2026-10-02T23:05:56.29Z" }, + { url = "https://files.pythonhosted.org/packages/6b/89/89491878c28e8291f5aa2fffe2c2d57230d10ae366d55dd810b840513d78/markupsafe-3.0.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a", size = 25912, upload-time = "2026-10-02T23:05:57.416Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/680998b54efdea06fc114565cd739b6d059f826a0279219b218dfa750d29/markupsafe-3.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733", size = 23623, upload-time = "2026-10-02T23:05:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/ae/75/2709f5ac5de9467b40b10e2bb8f89cc63dfb74582e09aa734b1124a217de/markupsafe-3.0.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34", size = 23301, upload-time = "2026-10-02T23:05:59.94Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c8/39eadc6c5b14c9c7679bfb98f4d4c6a97863b5beb91839aca4d2d6e16e55/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978", size = 24950, upload-time = "2026-10-02T23:06:01.289Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5e/01037f8a43e8ccb0bffb4fbdc5212db05bf080fdd7286cd392332d58128a/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc", size = 23330, upload-time = "2026-10-02T23:06:02.441Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f4/23e83ce0596bb0cbe670502d31df8f757bbd01a392aa486fa3b40d1ed399/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc", size = 25239, upload-time = "2026-10-02T23:06:03.579Z" }, + { url = "https://files.pythonhosted.org/packages/88/5b/3708897368073cc683d524750474f41a77d2986152c380dcc55b20fdf340/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932", size = 22767, upload-time = "2026-10-02T23:06:04.699Z" }, + { url = "https://files.pythonhosted.org/packages/c6/61/ebda1307864b409e6b3115757a3d4a09cca46cfb6cc65191b5de226b424b/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6", size = 23668, upload-time = "2026-10-02T23:06:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/09/15/98075cceac3b5ba0dbb8e4762a847be967d2befc349a2cf2d0ac77f62c9d/markupsafe-3.0.4-cp314-cp314t-win32.whl", hash = "sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691", size = 14301, upload-time = "2026-10-02T23:06:07.109Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a3/768b560fcc4156685cb563d922b217810cfa7bc135773367f62f1f9d2078/markupsafe-3.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464", size = 14526, upload-time = "2026-10-02T23:06:08.276Z" }, + { url = "https://files.pythonhosted.org/packages/93/63/da554b4c97a6b0ea3229ca7fe8cbfb620be81613d517f482e85958550537/markupsafe-3.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c", size = 14421, upload-time = "2026-10-02T23:06:09.402Z" }, + { url = "https://files.pythonhosted.org/packages/a9/30/54d11c8ca027114898cab97421fb39e4ffd9ddf47cdbc44df2ec76722da9/markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65", size = 12791, upload-time = "2026-10-02T23:06:10.485Z" }, + { url = "https://files.pythonhosted.org/packages/10/6d/97c913e253a14bd3cd0e15a5c56d13203b823fa7ee32498342896a072dc4/markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163", size = 12527, upload-time = "2026-10-02T23:06:11.834Z" }, + { url = "https://files.pythonhosted.org/packages/26/f9/b86d032042a4d597d9e1997f0e5f63a3eedaf11258e0a05760b0a0a826ea/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92", size = 11602, upload-time = "2026-10-02T23:06:13.122Z" }, + { url = "https://files.pythonhosted.org/packages/f2/dc/73c14c1eedf0ac5fa3292ba43435e6c49d2c2050f33cebde541f8f4807f1/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a", size = 11817, upload-time = "2026-10-02T23:06:14.227Z" }, + { url = "https://files.pythonhosted.org/packages/8f/69/2c2fcaa5fcee22d72c7819c0d536fd181c74a688e6143845419579cd2863/markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429", size = 11446, upload-time = "2026-10-02T23:06:15.574Z" }, + { url = "https://files.pythonhosted.org/packages/88/54/9e5ec76c62e6e2834d5a93623018c943e8b3bb41d663e3fd4c03303b9b85/markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8", size = 11719, upload-time = "2026-10-02T23:06:16.701Z" }, + { url = "https://files.pythonhosted.org/packages/96/24/3ec292b44064c16229e064d770b2625bd8ea941aa61f44905a9fa44942c0/markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97", size = 12075, upload-time = "2026-10-02T23:06:17.855Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/b64fdb1f304848518742136983c24e96d967bfb59a0ea160e92736901ab0/markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b", size = 24656, upload-time = "2026-10-02T23:06:18.963Z" }, + { url = "https://files.pythonhosted.org/packages/9c/18/23997d4c65b355da6390d61cd56e0ab3befd6ba8dda25cb40c602bd0fa6b/markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9", size = 27737, upload-time = "2026-10-02T23:06:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/35998dead3c6af88c38265a56e58100211f036234ab88eb2283fd4cbce44/markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653", size = 25003, upload-time = "2026-10-02T23:06:21.284Z" }, + { url = "https://files.pythonhosted.org/packages/82/96/ef49135ce260db4ca4a12b119ed468449cd248db6b1468e2112b546d7a2e/markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369", size = 23111, upload-time = "2026-10-02T23:06:22.524Z" }, + { url = "https://files.pythonhosted.org/packages/50/7d/83126e338bd88c17a220668235368ad719fd4638e426739858cbb8508f77/markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19", size = 22403, upload-time = "2026-10-02T23:06:23.785Z" }, + { url = "https://files.pythonhosted.org/packages/83/dd/daf7e420de23c8206c365204e7b85e1251d8e19d34196a56336f316e5ed2/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e", size = 24083, upload-time = "2026-10-02T23:06:25.037Z" }, + { url = "https://files.pythonhosted.org/packages/19/3c/11eecdc06bc44ad5570350085b572ebf049e8f9a38d1ece6d76640b739cd/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811", size = 22931, upload-time = "2026-10-02T23:06:26.328Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9e/ac0fd77f2a726e56ecc3ca0235d095feace1358d1b822406c2a2ef26a4dc/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea", size = 24299, upload-time = "2026-10-02T23:06:27.742Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/c6bd842ad58ff5b3bc76eeed7e9a42a6f11adc5d090ec697b72c9672731e/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916", size = 21948, upload-time = "2026-10-02T23:06:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/a3/46/82f586711fed61e86faa1ee1bc317d68cd45a10c8bdbe3f7d1fdf9026ad8/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741", size = 23150, upload-time = "2026-10-02T23:06:30.583Z" }, + { url = "https://files.pythonhosted.org/packages/19/2d/2dfdce99318abbfa26925195fbc17db188c46a1ec6457be121b6f9cfeb42/markupsafe-3.0.4-cp315-cp315-win32.whl", hash = "sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b", size = 14269, upload-time = "2026-10-02T23:06:31.949Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ec/6000fd82e8791e58fcd0456ec20f098957e2b03d5ed02eb73241a577c0ba/markupsafe-3.0.4-cp315-cp315-win_amd64.whl", hash = "sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214", size = 14496, upload-time = "2026-10-02T23:06:33.258Z" }, + { url = "https://files.pythonhosted.org/packages/bc/66/e73bd5016421d5d6e2fb6de7dd609f9de020942ac8c626526bd8c6eeaf82/markupsafe-3.0.4-cp315-cp315-win_arm64.whl", hash = "sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67", size = 14361, upload-time = "2026-10-02T23:06:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/90/df/cb8c3dc98d313a951df2f8968f44e4cb5643df6d3cab749a530ce2f7d972/markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad", size = 11760, upload-time = "2026-10-02T23:06:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bb/4af9b3ca0753d654ac75f9531d5bd741bb77ca6e696f36807c475ffc099a/markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99", size = 12108, upload-time = "2026-10-02T23:06:37.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d4/b56429313aee5fd59b079c3df5615299959e25e7113eb6d8caadbdd7d38a/markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002", size = 25762, upload-time = "2026-10-02T23:06:38.419Z" }, + { url = "https://files.pythonhosted.org/packages/65/f5/34c181e891aa4f7d59c918584672e0c5eb7fffe76c1387d1246008bf4081/markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e", size = 28400, upload-time = "2026-10-02T23:06:39.819Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b5/ad14694fd0ac9a5ce30bc6498f2999378f418583dd1679cca5a1b512957e/markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c", size = 25946, upload-time = "2026-10-02T23:06:41.381Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a8/26b606445387d0ceb1eb1f21840094b84e4e3c3c3983d80d10b89823b490/markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8", size = 23634, upload-time = "2026-10-02T23:06:42.748Z" }, + { url = "https://files.pythonhosted.org/packages/39/a2/b8814de672f1f0094d498bf646f2fec9d6356b503d28ef500b71c5095377/markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe", size = 23488, upload-time = "2026-10-02T23:06:44.176Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/287223376fb73335a3cc5d6eb22c6ab01358cf33945a9c39c06b9dac3f4b/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2", size = 25037, upload-time = "2026-10-02T23:06:45.646Z" }, + { url = "https://files.pythonhosted.org/packages/f9/29/4df8355e313426d19e62ba33e0253c009ca12a0894ee77d67fa67255361c/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38", size = 23543, upload-time = "2026-10-02T23:06:47.264Z" }, + { url = "https://files.pythonhosted.org/packages/71/e5/8377731e8495668dcc768f645e717df18318c841edaf023a99395f6da9b4/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494", size = 25282, upload-time = "2026-10-02T23:06:48.795Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5f/373456e37ceb1478d657d6fe769cbe0a39f0a8dfc1548eeb19c471eefdd9/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d", size = 22993, upload-time = "2026-10-02T23:06:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/2cbd5628435afb6f541bbaced4bce0c2edac4b09a142e6e928b8b0da9858/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894", size = 23712, upload-time = "2026-10-02T23:06:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/81/99/157e10966b033b363aeda5263e82596ee232a0b1d082fdbf90aa417ff083/markupsafe-3.0.4-cp315-cp315t-win32.whl", hash = "sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78", size = 14300, upload-time = "2026-10-02T23:06:53.241Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/55884815414c9706a23deca150b72c25a62109e65b0b6ce232077802c719/markupsafe-3.0.4-cp315-cp315t-win_amd64.whl", hash = "sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c", size = 14522, upload-time = "2026-10-02T23:06:54.729Z" }, + { url = "https://files.pythonhosted.org/packages/92/f9/ecbde7149e95b8a0f18e16d5d747f7dc06049d5da2e4f77f6f5e4a1f46a8/markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", hash = "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba", size = 14417, upload-time = "2026-10-02T23:06:56.246Z" }, ] [[package]] name = "multidict" -version = "6.9.1" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/99/1d4d69c3512d0ddbfa3a1b69cfd9a151012ab2eb4eabbb096201b1f0b7d8/multidict-6.9.1.tar.gz", hash = "sha256:0f06e60fa190aa7abd0914c2a766736fdc8e9f34878c4346338534b73d1b20e2", size = 182404, upload-time = "2026-09-21T17:59:05.362Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/79/84ddb5ba16c4eb2c69c71db76ae3c579fe546e511f7170c7e27eedbab7c1/multidict-7.1.0.tar.gz", hash = "sha256:61a4e5d81b8d4e4ad61964b230129e7a2b914793d96289029078fc9009f074ec", size = 362341, upload-time = "2026-10-09T20:31:38.279Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/0f/c6041015aa2cdc11e1dff0cae58898ae5525d19e35efd65a4ecf3c6ea235/multidict-6.9.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:fc0dcb22fa9aeabfe3fa4e0430099acff985ec5d77a851382f76cc6146e780e5", size = 62336, upload-time = "2026-09-21T17:56:21.181Z" }, - { url = "https://files.pythonhosted.org/packages/23/db/95bd0afb130a0149c955f1f066143528d60864e64f41ff5a1e1313609361/multidict-6.9.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:024123f0ab402ab33828e24eb80fa8f25167d0d3783ba5f287e39ed741e6abf9", size = 54892, upload-time = "2026-09-21T17:56:22.63Z" }, - { url = "https://files.pythonhosted.org/packages/94/cc/8aa34ae8d09498e8da003f485c03371b55cf1bc6d56cff861b3a7de61251/multidict-6.9.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:af1b5a92315048c3e36bbebfa7d4760a9c3e910bc4166f11b20d77d20d6bcfca", size = 56251, upload-time = "2026-09-21T17:56:24.36Z" }, - { url = "https://files.pythonhosted.org/packages/52/48/22baa3b95375096369b04c4348af14139570f302f951773d2c68bf0ccf2e/multidict-6.9.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b2483da477932ad1983d1d33c18bc3771c6fb00cfbaaed70a875fd547ef8e840", size = 96345, upload-time = "2026-09-21T17:56:25.777Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3e/b96779dcac28ec6d491fd821112a0156b519b6701bba186cdf6f6df737f4/multidict-6.9.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db4d697b18b6ef5528b1f36bfa25072cd2a421869f5963bc0e92c8a34b9e2800", size = 58901, upload-time = "2026-09-21T17:56:27.364Z" }, - { url = "https://files.pythonhosted.org/packages/f9/b4/8e6d950f02ca6ecc00ecf671f2ddb5dab7017671a8d197326f75621f5786/multidict-6.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:854fd2f1bc6e8a56b89910b5cd7261a8b40f13ebb31572da985ce59c7da0886d", size = 56491, upload-time = "2026-09-21T17:56:28.901Z" }, - { url = "https://files.pythonhosted.org/packages/ed/8f/212ed7e03282c7bc271e3796bf985b53d5727c2827361e3dd9c5ee6b093f/multidict-6.9.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:03731f6fc036180700c9dc2308205a48e5ca6f3ff03087739ab746f294022201", size = 332297, upload-time = "2026-09-21T17:56:30.495Z" }, - { url = "https://files.pythonhosted.org/packages/a0/97/555aab000e03ecba85c0a1837fd5674d8bf10949490df7800da3a40660ce/multidict-6.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989261c5f1735a165f2e4e87cf6d5f17ab734fa18f9ad0383d5adfcdaefce701", size = 329197, upload-time = "2026-09-21T17:56:32.652Z" }, - { url = "https://files.pythonhosted.org/packages/3d/25/f3539b5bdc147beba66015795dfe589eedcc580348fb1a8c69b1df4196b5/multidict-6.9.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4c9e5d05b126b267ac048a89a0e2d9b48b1b648add62d4872906304a8590610", size = 308456, upload-time = "2026-09-21T17:56:34.657Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7d/4665cad8fc326787d218879be879d86dfe32f006c1f88ae47b04d3b52fd5/multidict-6.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9d0a21cf76153de8f2d96a877991d6bc59b9ab5180b949e50db73cc193b4a694", size = 343382, upload-time = "2026-09-21T17:56:36.606Z" }, - { url = "https://files.pythonhosted.org/packages/41/01/e1e9abe27f492b22dede492bc423015597412b14ce074f7475f4ecd80185/multidict-6.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7386aa18d98d6b8af44b92173654ec469237fda35f8e8523e43b581a86f476a", size = 346848, upload-time = "2026-09-21T17:56:38.344Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/8d54118bc228e64e1087f1729647b93bbea225eda4a3f914663a6f410bca/multidict-6.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b69651732c64afb691e50cdc3387cae305e0eeff8804fe3e3ce203876494932a", size = 333612, upload-time = "2026-09-21T17:56:40.041Z" }, - { url = "https://files.pythonhosted.org/packages/76/e1/9509dafc1fc68948e75842a74533ddfa64af57f2b17810e9cacfa68a829b/multidict-6.9.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1fee9a16d88a1c4865610de31ef5c666671020d7a81d1f510eaf3c97d00ebeba", size = 304246, upload-time = "2026-09-21T17:56:42.313Z" }, - { url = "https://files.pythonhosted.org/packages/1d/b6/96b93808e1bec0b51d49d89859e49b82a94f7b2e58eca0fc44ffcafd3ae5/multidict-6.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7814bbee202acd3bd240204c17d8b87a4c48c81064fd8674dbe527d94d5a4290", size = 321983, upload-time = "2026-09-21T17:56:44.399Z" }, - { url = "https://files.pythonhosted.org/packages/d9/1c/08e5184df026b9d8e05a6dce5da22628e46bc4a7c7f03b8bf3c6939a8c46/multidict-6.9.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:95f273bea318a194f656527ee2ed19494327bc500b8e87d9358e3222579aa28d", size = 315927, upload-time = "2026-09-21T17:56:46.208Z" }, - { url = "https://files.pythonhosted.org/packages/d7/5d/9ff39e7387f6fcc9a81af5dbf055fde08149849a04f8df10ca5b086b0d39/multidict-6.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:73bcafa21a78d0776b3ee7cd2a63c66f968eb7db8e8d594e32d7329950f6e828", size = 336827, upload-time = "2026-09-21T17:56:48.021Z" }, - { url = "https://files.pythonhosted.org/packages/5a/be/30fbb2cab22203128928066de94be247dc9a3230bf2bfc54437ac3b5f9f7/multidict-6.9.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1a938761c77e0e6edb0c93d02f4e988d44a69e5195e5a3b893e5553311347132", size = 336317, upload-time = "2026-09-21T17:56:49.778Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f0/2480aef6b7d8e7ab89b064ae14f12ec069570dab3a5c13e0155912293247/multidict-6.9.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:37245ca4105386194dd1d292a6f2aae09bfe1bd7ac6f9ec25093e3cf8e9b143b", size = 303258, upload-time = "2026-09-21T17:56:51.829Z" }, - { url = "https://files.pythonhosted.org/packages/4c/04/dbe59cf4ea3778600e8e70e30b31aa9118ad6da2844bc9f717804cbaa000/multidict-6.9.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f0700527dd5bfa8b7204b08330542f4f388899d3c14d885d8a368992e0eb562d", size = 337232, upload-time = "2026-09-21T17:56:53.537Z" }, - { url = "https://files.pythonhosted.org/packages/f4/8c/abe55548f06f12878588f08eb803c5a391283f01243f784cb00adac79231/multidict-6.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b7e54fd883671d1a8810704851c044b7173287c552b9f5c3d9e0eb9f00ae194", size = 330261, upload-time = "2026-09-21T17:56:55.335Z" }, - { url = "https://files.pythonhosted.org/packages/d0/f1/ba67f668478f152bdfa18abe72e7588bd078bf8b42c172fcf542fcf45826/multidict-6.9.1-cp314-cp314-win32.whl", hash = "sha256:e81ae656b9935ac4528a71f96bb7a14d949778ed1897c573d3e7ebb9187f8841", size = 51715, upload-time = "2026-09-21T17:56:57.048Z" }, - { url = "https://files.pythonhosted.org/packages/33/81/014bb2128aedc8157d2848f0ec8a0209c6b822fb88deea012834e1c4a4e8/multidict-6.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:acddcac38adc8342ba48aba98896faa7928854bebb62542362138655b5367ee3", size = 58621, upload-time = "2026-09-21T17:56:58.531Z" }, - { url = "https://files.pythonhosted.org/packages/66/34/8f43c03c2d8821a2320b8c560d8a49d0be2920627edfac3f546922f91969/multidict-6.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:32217133dddc58c927805cf6c0731d8144584176b768042ee886d51e71860bc9", size = 53959, upload-time = "2026-09-21T17:57:00.15Z" }, - { url = "https://files.pythonhosted.org/packages/08/64/4f2a5eeea10b6d42634ba168ec6efc732ef218772e57b8239cd485c202f6/multidict-6.9.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0b2fb8c349d1103863750b5d8cb5ace766917f4b35f3d883c8f778853eaa9f76", size = 107694, upload-time = "2026-09-21T17:57:01.785Z" }, - { url = "https://files.pythonhosted.org/packages/32/ce/c68d08ac2096c1528ff20d77fd10f7e401db3e534ac5a1c7726e36a77c2f/multidict-6.9.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:56d834b74c993a7d7cb2b8ab33a0d55c3e0d4a3d2f2da2808a4ad3d79189711b", size = 64766, upload-time = "2026-09-21T17:57:03.52Z" }, - { url = "https://files.pythonhosted.org/packages/3a/9a/9f5c270c88fe289b4876fe28f9932a70e015a7916aff1e5b0325f7597b11/multidict-6.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:783ba7d845d79ce976afd9c1e91a4e5714671defa198ee789e8b23316083a485", size = 62723, upload-time = "2026-09-21T17:57:05.104Z" }, - { url = "https://files.pythonhosted.org/packages/7c/63/c9a0b131354dd949426166cef712d708f436a8cf261114f8d5d2aec440ca/multidict-6.9.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d718fa1b5f0d0dd75e86fbbc5b0c93ea3a5d65216c85c61cd5d7cdddfe08455", size = 290445, upload-time = "2026-09-21T17:57:06.687Z" }, - { url = "https://files.pythonhosted.org/packages/9d/41/ed27d1d20ba91f4e13e8f7dcfaa592614ae0e1d5f45e03171d23c3b098ac/multidict-6.9.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a313bad717dde740959d50850c315b75fd4eb0c5e6b4dc8535db0f1c369be125", size = 304375, upload-time = "2026-09-21T17:57:08.717Z" }, - { url = "https://files.pythonhosted.org/packages/66/6f/a7b26167173c3b73d6e5ebb0856c5154ad005156c3a2fa4062329921819c/multidict-6.9.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ef01d29fca550ab871fd99154f82c6472aacf8e7def272dfb07b460123850390", size = 281746, upload-time = "2026-09-21T17:57:10.713Z" }, - { url = "https://files.pythonhosted.org/packages/34/1d/da27bdd49b0cb84263288f5f48dbd915f2d17fd228fee2078ca607e80277/multidict-6.9.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f3bd290711c6e9486173a6ee7cd4e7f00c3971c7908c1ec1b6e5437c5e4c6f9", size = 311499, upload-time = "2026-09-21T17:57:12.467Z" }, - { url = "https://files.pythonhosted.org/packages/d0/36/ab0299a7abf53e38ff75d96c64480f1e2ff8edde985f93f7a1ad5e08bd94/multidict-6.9.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:81cdc537e0a42e3c0170752fcadbe450246d5c3b4b7231d6eb9672456605ac94", size = 313553, upload-time = "2026-09-21T17:57:14.548Z" }, - { url = "https://files.pythonhosted.org/packages/5a/44/e21be702efc9c9ba097c8be874c68860b1cb6cf9c85611263039bf95b37e/multidict-6.9.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b193bf7a443c97d81c47052f60c486071a4bdef4a573fa2514f920089414d45", size = 299898, upload-time = "2026-09-21T17:57:16.192Z" }, - { url = "https://files.pythonhosted.org/packages/00/8d/e93bc0dc947c7f872e502cfaa7ad9d98f001e1c93cfa6f4940457a6e187a/multidict-6.9.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a0c98f6a636adf0d7edd60c61589eaf52d149239af754d0bfeb0effedba53a8", size = 273054, upload-time = "2026-09-21T17:57:17.793Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c9/945b44493fbe7af26ffe1e43c506b376daeab070fc1a0923bef9bbf572eb/multidict-6.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6e6b7e3a1520c39a2772f414cd9ddf5995f77e4e823dfa1522af383addd465a3", size = 294751, upload-time = "2026-09-21T17:57:19.631Z" }, - { url = "https://files.pythonhosted.org/packages/24/79/0969a9b38b814e5fa3af1b0a57708c49b3d9b3d55c208bac6e98b87bb028/multidict-6.9.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ca65cced0d67a9039e93bcd98a369920e499bf98296844ff56ead01c9085321f", size = 284523, upload-time = "2026-09-21T17:57:21.313Z" }, - { url = "https://files.pythonhosted.org/packages/1d/1e/c32ddf53234c228b1a374ddabca19f542ac7f32752f5155982abe2304833/multidict-6.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:372c063f37480f62c1ae32dc3a1a0a5942b180c883f99789075a8a8ec3c4e709", size = 290275, upload-time = "2026-09-21T17:57:23.206Z" }, - { url = "https://files.pythonhosted.org/packages/5d/09/7a99a3daafd987330abb865a2d36e432fd8bf18e790ddda55c5de15eb98f/multidict-6.9.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:827c92145b3b976b39430129c89d213b250dacbe5fce678e9a03940e6e848983", size = 303350, upload-time = "2026-09-21T17:57:25.086Z" }, - { url = "https://files.pythonhosted.org/packages/2e/09/7e6ebaecc4e226d7fe27973c271d58b75c14c24e84c23736d21dc39997dd/multidict-6.9.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9b0124b9c17e9890f0819b2e7a5f65ec9a2f5aabd6c8e7ad090c1675cf70dee6", size = 272232, upload-time = "2026-09-21T17:57:27.193Z" }, - { url = "https://files.pythonhosted.org/packages/1f/6e/420e9e879b21cbdb5036214ee238efabcfab7bf262d3fc124132050f79ff/multidict-6.9.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:23136f5a564654eb61061ec6d5620a4c1ea32c8f552b65e9982a12b72bff601b", size = 300525, upload-time = "2026-09-21T17:57:29.004Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7f/c6b0896850b3ceb190ca302760d63148843371f027c5dde3d1d732343a08/multidict-6.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f2524ec55b3e65cbe235a8b3c36e2af3b635be02ed05e20c94e70c5c943c009e", size = 295933, upload-time = "2026-09-21T17:57:30.837Z" }, - { url = "https://files.pythonhosted.org/packages/29/f2/f0ff3384a756227741c72f4ee94cadf1f9900df0ae961e48639f96115b1c/multidict-6.9.1-cp314-cp314t-win32.whl", hash = "sha256:35ba0263bae5dd3ad5aad767cc9afc01a8598c7dae30f1b3b2de98b1b32c28bd", size = 57776, upload-time = "2026-09-21T17:57:32.835Z" }, - { url = "https://files.pythonhosted.org/packages/8d/87/013e1eed30ad46bec1eb68947bc9caf55ba2fd0d6aed04fe964e3b44c23b/multidict-6.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:7c8d5882ba25ac0282258be435d8a05aa0cbcacfce15799154a338f847f159b9", size = 64881, upload-time = "2026-09-21T17:57:34.489Z" }, - { url = "https://files.pythonhosted.org/packages/7f/95/64dd049bb4c53426e2fee7afb874daaa8b4426c41878785883a94511ecf8/multidict-6.9.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ac348379cf4de5538a0a213be1532d289aa801ec5d267c5909446b9ea2f8e2c3", size = 58582, upload-time = "2026-09-21T17:57:36.19Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2c/26bc1723592cfe965b575652a7749026474b0625b04b04564e1fba265dc5/multidict-6.9.1-cp315-cp315-android_24_x86_64.whl", hash = "sha256:f04551dce5a7db8c9659f2e4245494c182d0663b83661803e08d46bfcae5eda1", size = 62339, upload-time = "2026-09-21T17:57:37.967Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c5/614d2eb9995232d066aae7f17d1d454060b6b5d882b22fbaf6150b05ef1d/multidict-6.9.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c0c88085affd35c33e124e36930c5ad96aff9294ce195eaa0fd9cec962b64a82", size = 55009, upload-time = "2026-09-21T17:57:39.746Z" }, - { url = "https://files.pythonhosted.org/packages/7f/24/f7179ecdf9cdd3581ace3292c3b883a1a500a374aa4e619bf6eb521c62e5/multidict-6.9.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a8b75dd3d3638d9a19f23e84af4ffab3b8940422c0df2da2a77005ef5aa3d7ea", size = 56273, upload-time = "2026-09-21T17:57:41.544Z" }, - { url = "https://files.pythonhosted.org/packages/ac/72/c9dffcecccd1f8b9a58f960ec4eaecab62c4edb08d5bb984d4de67fe3fdb/multidict-6.9.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:006c4478de0a1876f4834e14255776286f09b9846b505fe63f67f9d173a9487c", size = 96390, upload-time = "2026-09-21T17:57:43.385Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3f/76542360e655cecdb7c158c7af88c69c4db0d2aac3421659d41214927229/multidict-6.9.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2784090c30a586d5b45197bd9c32f87fb927216cde302f6cfd76d76577e90f08", size = 58900, upload-time = "2026-09-21T17:57:45.105Z" }, - { url = "https://files.pythonhosted.org/packages/04/15/f28c71af461dd4ff15653a165cfd8ddaf01f3387ff80d2dd0de057d527d0/multidict-6.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:7c708566da8014b120a64b1eb6d200c6c0c8cb36296383723cdb6fc82038270b", size = 56540, upload-time = "2026-09-21T17:57:46.913Z" }, - { url = "https://files.pythonhosted.org/packages/19/de/5ee4050c3ddad88c605a89fc1cd18e89abd40a64f183e6fb5276b6eb7f52/multidict-6.9.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:042fb0196047e786936a730bd302de83143950da45f2c16078da8f35e1cf7919", size = 331818, upload-time = "2026-09-21T17:57:48.825Z" }, - { url = "https://files.pythonhosted.org/packages/70/e2/2d4e3c3ee70f583334ff236fe6a3babf7a05ce56169a15df65453af647c0/multidict-6.9.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2212a0c842c723d919ea4a22a9296cb6b244b386e4e6ea92adfc7fbf3095519", size = 329800, upload-time = "2026-09-21T17:57:50.91Z" }, - { url = "https://files.pythonhosted.org/packages/d6/14/f64bd05667343a7250cdd99f81bda2270e3959f00832441f10c7f904e1c1/multidict-6.9.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd882aa29bf402b62bf1fd7c19fd5df4b6528cf468a908864b39368572b662a9", size = 312048, upload-time = "2026-09-21T17:57:53.053Z" }, - { url = "https://files.pythonhosted.org/packages/64/bf/a91256ce00199e7544e3eda79c4e9544eb7388c38fa22f11638c2e357ccb/multidict-6.9.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c57541034d12b215ab0a2bfa371d1a8a198da18176d0426c27105b9161a6862d", size = 343676, upload-time = "2026-09-21T17:57:55.069Z" }, - { url = "https://files.pythonhosted.org/packages/14/48/628b978413518159228c2b2ec38645362ceb6427e935d83d716e26490a49/multidict-6.9.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d73fed4e37158ff00cd271871170b79e40138db51e625fd352fa17c6acb34f67", size = 347028, upload-time = "2026-09-21T17:57:57.157Z" }, - { url = "https://files.pythonhosted.org/packages/8c/73/5f9dc2b5ac7dd7a82458ae9cd283b99c185834f23442ace295362dcc5e45/multidict-6.9.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:79da2491348b30810728050b4a8ec0416f85884125c2fd44655b3d01150d9c3e", size = 335670, upload-time = "2026-09-21T17:57:59.126Z" }, - { url = "https://files.pythonhosted.org/packages/f8/84/6f319ee000f5ab3fa0055b194ad706fddd552c60c76a21d581c87db1d46c/multidict-6.9.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b4da208a63434d21a3df64d29758e650fc4aa8cb05848554b76949c296539cca", size = 307804, upload-time = "2026-09-21T17:58:01.2Z" }, - { url = "https://files.pythonhosted.org/packages/3f/24/61be5fe1616d033532e8d63dc789c0f35b9793917834713eefe050589ba9/multidict-6.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:877ca17fdcfdf5c397493a71e5ff97a87bb181417fe717fdadc77c08c09301ac", size = 323409, upload-time = "2026-09-21T17:58:03.383Z" }, - { url = "https://files.pythonhosted.org/packages/22/a6/eab8aaf59892169a0f4e8e78a5516a6caa6abd8e96e6410111258f4ef0ef/multidict-6.9.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:40aec5299e1ed71fbb988389059da381c3c1a60e0c649acceb2a35d9b128848e", size = 315746, upload-time = "2026-09-21T17:58:05.372Z" }, - { url = "https://files.pythonhosted.org/packages/42/b1/d1e14bab80a74babf6e2ac971a878b84fdcf3a972f3ec3400ca12260fac5/multidict-6.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:4a409ccc42aefec904038695d5d7fd6d8f3af2721b7b6401d55135ec7d6d298e", size = 334529, upload-time = "2026-09-21T17:58:07.35Z" }, - { url = "https://files.pythonhosted.org/packages/f5/50/a05b1d36f6d3363a5a334cebbf11624c9c7a69d66fd23345eb53b1d43b7e/multidict-6.9.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:0a5769559e3312dd96731fbe15b4abb6033368ac1cad5a98dadd21946a4c7d6c", size = 336455, upload-time = "2026-09-21T17:58:09.112Z" }, - { url = "https://files.pythonhosted.org/packages/cd/0d/bf33cb097380252b08e1177b5ae0cab822c47ce80d236f3f724953c76721/multidict-6.9.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:03fac50ddfd8302175b77863a015eccfae76767cda5506eef86df559ba861e1f", size = 305865, upload-time = "2026-09-21T17:58:10.921Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ee/a2bd133391204b1cec5a5caf327dc9a06710439b7403220860a8fb164560/multidict-6.9.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:5ecace251ccfa705bf3d7d35c5032cf750f5c629809740405697bce5c118c4a4", size = 337531, upload-time = "2026-09-21T17:58:12.846Z" }, - { url = "https://files.pythonhosted.org/packages/a8/32/c0972486f81a06c3bbb23063c85afaac53c51608f7e35bcb101a1006408b/multidict-6.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ecc89dbd4155b2f8a47f4bbd89242a35ed15e8e0ec581cab5ac65fa38407329d", size = 332464, upload-time = "2026-09-21T17:58:14.673Z" }, - { url = "https://files.pythonhosted.org/packages/69/4f/5cbbe852b45d8a7f0ab3ed042386e4894e463010eb230603596cb15d0fee/multidict-6.9.1-cp315-cp315-win32.whl", hash = "sha256:a3ffe881246d28a862f1985824f484cb7361f44d6b99c4c620436ef56462f38d", size = 51714, upload-time = "2026-09-21T17:58:16.409Z" }, - { url = "https://files.pythonhosted.org/packages/d4/77/1fa118386deeae4e7a48870fa5cb073e748b0ac5265a6990651cefc48068/multidict-6.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:59c123d0e948d760a5f930f316cfefa07e8d632ab84327c0693ee6a88171154f", size = 58619, upload-time = "2026-09-21T17:58:18.369Z" }, - { url = "https://files.pythonhosted.org/packages/dd/ee/51c45748acfce9cb69d89dcfcd09c874b385407fd6993e885d6905659865/multidict-6.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:31199204b3ced121ff5407a2c342326a5d27e3870abbf94bd80dbd2451b7bc8f", size = 53969, upload-time = "2026-09-21T17:58:20.102Z" }, - { url = "https://files.pythonhosted.org/packages/aa/57/89d2d3dedfae2558853c1770c7cbcdfd5211eb4c4090b1e8cb570544a490/multidict-6.9.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:8879510a76940670517ea1cb589978da44b86e286ec3e50d664ef330817afce7", size = 107679, upload-time = "2026-09-21T17:58:21.98Z" }, - { url = "https://files.pythonhosted.org/packages/c8/df/0c2a28870181c1762f90952b897f6db46c934ce073efe0442604cbc276db/multidict-6.9.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:aec65b53a07f580606593f877eefbb29a45939bfc0d3fe6e6d9f42b41b749f68", size = 64753, upload-time = "2026-09-21T17:58:23.622Z" }, - { url = "https://files.pythonhosted.org/packages/43/7e/9c6a3e7619459407d8958f7e56ab3dda7f0738f5aed9bb4ad48076e8de83/multidict-6.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:14c56f73e78faa1f68bbb826197cd5871994e70e841b8590829c35912ece5c64", size = 62732, upload-time = "2026-09-21T17:58:25.46Z" }, - { url = "https://files.pythonhosted.org/packages/38/eb/fd56c9d83ba0cdc3cf66f9acb278b24ef1f043b620b96d145685a3b57338/multidict-6.9.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:38c9986f9ce50c459b10de216a05f4bd7ed5ac63887d56e500a52bb464b861ce", size = 288648, upload-time = "2026-09-21T17:58:27.115Z" }, - { url = "https://files.pythonhosted.org/packages/df/ef/51e79c2442b0f56ac4ac580108d713078b31297e028d468fc220971a305d/multidict-6.9.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15db6a102cbaf1949cf028ecf080aac76d20bcd29ad4e092574db6c6b7af78a5", size = 304919, upload-time = "2026-09-21T17:58:29.143Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e4/774700cc5739353fe88a7204ed30e78a9403d98d4bb2496cc91f096d2e71/multidict-6.9.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd82c4977196681a499bb6ca9e462afbc5c91c1c15b6a991dfbd72733fea4dd2", size = 289094, upload-time = "2026-09-21T17:58:30.938Z" }, - { url = "https://files.pythonhosted.org/packages/ca/41/0bec327bc3a8271825376b6d8a469d1f80e3ba1e2e730e019836e29663c3/multidict-6.9.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bb58ba73a3f96f9a3e46b1fab69929d7edbbddb3133ee74b5c3c54074a53c4f1", size = 312007, upload-time = "2026-09-21T17:58:32.964Z" }, - { url = "https://files.pythonhosted.org/packages/c3/7a/9906bf5f1fe20e8e755affccc1a69ecd8aebb079ac65386e5ffc8eefbe75/multidict-6.9.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c6eecfab7ce4cd9487ff8ba936fe38cdfd68c04faf3d5c710363c6a8e695659", size = 314657, upload-time = "2026-09-21T17:58:35.285Z" }, - { url = "https://files.pythonhosted.org/packages/33/92/cf6d665a45663bbe216420fe632da10e9f4f842b07ce02bc3ddf8eb882b8/multidict-6.9.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c62e71e6289d78c0108d8aeb495f8bd3cad4bc1632fedddc9297ddf287ecc20", size = 301563, upload-time = "2026-09-21T17:58:37.472Z" }, - { url = "https://files.pythonhosted.org/packages/97/4c/9f1b7945526905d58b4b2d422e2ea81326dda71f9bdc9c32c8002639234a/multidict-6.9.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:539c2cd5fed0947c135cd7eabaaac55f48300dfa1de0f3ca4edb5efa6606f471", size = 278540, upload-time = "2026-09-21T17:58:39.551Z" }, - { url = "https://files.pythonhosted.org/packages/5c/d9/133aa472fc1be181de29a0c2e1c4cd778280882f532b404395f646742e46/multidict-6.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3100d169ceb7bc8f05f89a6db11d1b21f119975fc26f29dc45a472e3569f0879", size = 294858, upload-time = "2026-09-21T17:58:41.791Z" }, - { url = "https://files.pythonhosted.org/packages/bd/38/21f2305dd20ac5038fa581b5d7e0d8dbbd70dc6b50231c22ec173dacddda/multidict-6.9.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:696477ad71385c4795e3b8e4cf10b0d2c28c2a1ca6a955e031cb1e62993e9ee3", size = 285461, upload-time = "2026-09-21T17:58:43.844Z" }, - { url = "https://files.pythonhosted.org/packages/ad/21/465a43c214d1d7e2d0c974e727d4b5935f8a0868838b5df9d0984c83c1dc/multidict-6.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f5844e7befc707367807586f550fa97e23dcfef0728f02b98c7bc498a961a5df", size = 288964, upload-time = "2026-09-21T17:58:46.16Z" }, - { url = "https://files.pythonhosted.org/packages/2d/58/72a3e8d56c1e05146d2d1841dbaacf8986375eded1e5a58d30eef191a580/multidict-6.9.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:abc7c2e4b47bfe6a9aea434d3fdebb9597ee636e914e92ba352f2068b9142f4c", size = 303924, upload-time = "2026-09-21T17:58:48.395Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b9/911084c04530a682a54536b4f738d3992a593b5ff6fea1d6a1f02f3acc6b/multidict-6.9.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7ab379f95caee071a37cbd8be86d4c65accc651d391f9f97748fef006f38769c", size = 278479, upload-time = "2026-09-21T17:58:50.494Z" }, - { url = "https://files.pythonhosted.org/packages/45/eb/9a333346c02c928a9e82534638bce85697c10c46b4901fcd70af19357356/multidict-6.9.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:557a4e1708df428ebe6c3081c83a273d275dad2446ce0d81fc648ac71afdb18d", size = 301650, upload-time = "2026-09-21T17:58:52.554Z" }, - { url = "https://files.pythonhosted.org/packages/67/2f/1d936c4e45c8199db6082b6836e9182e31aa393de0de652637790831ca4b/multidict-6.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:475d04d5192eba487a3e2f935976340baa24529046e9c1c9c7a3b7bf80445ae1", size = 298024, upload-time = "2026-09-21T17:58:55.03Z" }, - { url = "https://files.pythonhosted.org/packages/a4/0c/ca62ae2882b89a4cfeb0bbca873a4afdb5fc32994c4f8a37a7cb52ede7a0/multidict-6.9.1-cp315-cp315t-win32.whl", hash = "sha256:10083a8a0f4e1b26b599889e90b9802504ce5d3f7722f925bbb7ca47dd22a7c1", size = 57810, upload-time = "2026-09-21T17:58:57.141Z" }, - { url = "https://files.pythonhosted.org/packages/02/8b/b2c2805c3eb6fa4cdf5512fb95ac6174dde71e34dd601c5be52f4df5dc82/multidict-6.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e96ca64383efa107262ee3949f047af5ee4f1845ba09463466c04d35a83bd3ec", size = 64984, upload-time = "2026-09-21T17:58:59.446Z" }, - { url = "https://files.pythonhosted.org/packages/fd/34/03f1d204d698c408f4754c969882609b6b7a902c0e3d16a7033f219cdeda/multidict-6.9.1-cp315-cp315t-win_arm64.whl", hash = "sha256:501ed8b02a5990c67a91c732843609d43a6be1f7576fcdfc867331239f37fbd3", size = 58742, upload-time = "2026-09-21T17:59:01.726Z" }, - { url = "https://files.pythonhosted.org/packages/be/59/e26cb779be4c591d1a910f59d29aca9fba4de70349840a833beba2652371/multidict-6.9.1-py3-none-any.whl", hash = "sha256:7bf6478188f4e47bf5686e8a33da4ae28bf43b1b2528d9ee144d28492bfac60b", size = 19176, upload-time = "2026-09-21T17:59:03.501Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a7/c9f5c08348a6f903143b631546e22becf1b704a1304b17f7e1b9850308d2/multidict-7.1.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:128ea4142f81a79d430f3d0eb55206093e5eda03a12abbc7b03c34748ff6116b", size = 100435, upload-time = "2026-10-09T20:28:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/82/60/92fe617008c74cc019483b1c59202c48738c6f637ad5b12b36f01e21db43/multidict-7.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:439a19f7fbbff232ce96682c57e27030b8ac3a4b8121484c94f04bf99d08bfff", size = 95267, upload-time = "2026-10-09T20:28:27.856Z" }, + { url = "https://files.pythonhosted.org/packages/17/78/82182f311d673f17de15bc7f7465c7ea5cbd2987ca3a6bf6546fa721e9d8/multidict-7.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8090c35199d6b7bc6426bb8bdaf341e64f295cc2624a1fda7860c0837f1acc03", size = 97233, upload-time = "2026-10-09T20:28:29.817Z" }, + { url = "https://files.pythonhosted.org/packages/63/25/af4e482d053fd73b4b1d60de3ff5578c6cb1d319d39d3f2a454d65409835/multidict-7.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b7cc5333fcbfb27327d12612ed72322f221b61c2b69deb1155078c964f86e1a1", size = 163070, upload-time = "2026-10-09T20:28:31.495Z" }, + { url = "https://files.pythonhosted.org/packages/df/d5/eaed52e199451dac445305fb2631d3f4e7ac9536aeef01f23622a1d93f90/multidict-7.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b0e0040b0d8dd89bd0af9ab18901981e344ffba68bb30b8eabb4eab6c303279b", size = 94582, upload-time = "2026-10-09T20:28:33.263Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e3/ff58ecad5161baa98dec05716e2745449446f2424a9c727464bf452f7fd1/multidict-7.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:33376418ab2846b931a72b36cfa16810befc4f49485d0b3f4dc054a4d6d00038", size = 97406, upload-time = "2026-10-09T20:28:34.981Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a2/ae4eadf02d4bc035baa7cadf5548ca4fa441517193f76ce1aeb5ed276ae3/multidict-7.1.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1101aea5c3eb1d26e090b931c693488af0db9f3d52e68be8d4cdd807dad9841d", size = 449908, upload-time = "2026-10-09T20:28:36.659Z" }, + { url = "https://files.pythonhosted.org/packages/91/55/bd5101ef760d1af4c3f246330b29b48ee22bfdd5a83150b713dd93b431e2/multidict-7.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f996b19ac89e0dae65821ce65f788619e4286f78c62d005ecd3b75b5d9c0892b", size = 471605, upload-time = "2026-10-09T20:28:38.561Z" }, + { url = "https://files.pythonhosted.org/packages/4d/8f/3dea8a28b0416ab71d47c8ce274bb3cacedde2d9838647ac67cdaa3d48e6/multidict-7.1.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e0d91a4bcb59ac0d7af0d8e0da737332e1b7fe6831e53e47819b1b5349d431b2", size = 433523, upload-time = "2026-10-09T20:28:40.913Z" }, + { url = "https://files.pythonhosted.org/packages/24/97/805d2aa4f746cc43cf4b206f1b1bfe2566add95bbb145abd3d9cf61858bf/multidict-7.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e96d67914ddbf5466e4476a1cd7ff30a332cbab85ed895207acc3e58c979b6a7", size = 488277, upload-time = "2026-10-09T20:28:42.851Z" }, + { url = "https://files.pythonhosted.org/packages/dd/7b/eac247b7e76f6010071c7c2401da5255eeffd1b2ac981925e533413d21ec/multidict-7.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:170ba61761f59ab92afcc86ce5534a3f3d0b07c38b339b950a83213f22dd86ec", size = 501888, upload-time = "2026-10-09T20:28:44.75Z" }, + { url = "https://files.pythonhosted.org/packages/87/58/de62e27b09dd15756265765945f68f1c0d1e23eaba09e2e109fbb4112425/multidict-7.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33389fe084e5426d9fd85d7d9ca91a29cd0d88a83c7c96e411aca49a3f9967bc", size = 474883, upload-time = "2026-10-09T20:28:46.704Z" }, + { url = "https://files.pythonhosted.org/packages/21/fd/afb4e50ce3b44707b5ee39c6b5fa1573bdb50c4be660bb35b040362b5170/multidict-7.1.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77024596b9046572c4e90b34c1ff212346756dc48933f90c53cf6e233660788d", size = 424327, upload-time = "2026-10-09T20:28:48.681Z" }, + { url = "https://files.pythonhosted.org/packages/23/70/96c9abf933c4edae53b8a1c8d3f038120a3a60d363f16c6d94eee9b04851/multidict-7.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ff15531a376dc6f35984443fd1429e4b150c36ce27633e7cc52a9e5318546e20", size = 459255, upload-time = "2026-10-09T20:28:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/24/37/dbc1dba26dc1c9e42aa07ee01908131a67fee0e79ebe5c1d0e7fc00aad55/multidict-7.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:544f2642a456fa264614e975d921540ee8c3b368b04d5aa1ddbec33241b13e08", size = 432476, upload-time = "2026-10-09T20:28:52.361Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0a/eac70cc1461668bad8253a2da727670a56293eaba112aeb4079af8cd37d9/multidict-7.1.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:08834fb8b20e1a985c70e8380a10940234b4162de62694458727330376e58b33", size = 450030, upload-time = "2026-10-09T20:28:54.252Z" }, + { url = "https://files.pythonhosted.org/packages/23/2d/eb40652ea74f96df859c7c5d38f9997f420b9dc404300dda0c5745327a6f/multidict-7.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b4908e17867930b7ac77f89a18dc67308c67c511f037d8580489be86fb585912", size = 476169, upload-time = "2026-10-09T20:28:56.819Z" }, + { url = "https://files.pythonhosted.org/packages/44/d0/7454ab8335bc4ec9f8abcc4c83a314bab060f22db6da8436b396e07ed21a/multidict-7.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9b24e1f93b9b586ec03bc7bea1bf021ec90bf2528c729195028a3ca1c266b3f9", size = 420459, upload-time = "2026-10-09T20:28:58.744Z" }, + { url = "https://files.pythonhosted.org/packages/60/7a/cedb46b287b720a2c4eda2448faf162612d0b718beb32581e35ccd9219b3/multidict-7.1.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5c93473d0d7cd9bbb370973a9679a62f381c7050d7dff4ad6aaa92e8650f5a79", size = 483071, upload-time = "2026-10-09T20:29:01.114Z" }, + { url = "https://files.pythonhosted.org/packages/9a/11/e7ded22b008546dd30ae8c979aa5ac3282cad98876f7d5bbe93c1f2409a0/multidict-7.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bf14cfcc30b097583d698a6e2b8b68c9bcffab277c485d481881958360c2938d", size = 467470, upload-time = "2026-10-09T20:29:03.388Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f3/0136144cb0b1731fd93475cea75d974a55009358d3bf61dd55e2709a3dca/multidict-7.1.0-cp314-cp314-win32.whl", hash = "sha256:86bc779a0896e59e4be30a5be5cd6eeffd0b40b6f0e75e730218736b7bfc6f5c", size = 80205, upload-time = "2026-10-09T20:29:05.736Z" }, + { url = "https://files.pythonhosted.org/packages/74/d9/a62a690d780febc171026d3e3c5700fc8387aa3e5f77cd04ada33d23d3f4/multidict-7.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:6c9fd50f636a8fa9cb6324cd3eac962fec2bc5bb432452a3b583583a1059acfc", size = 92923, upload-time = "2026-10-09T20:29:07.337Z" }, + { url = "https://files.pythonhosted.org/packages/84/08/eb7a1c34dc37c5f2c6aef52e6861e6f1cdfc6c685dcb72581eb218c00953/multidict-7.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:e6906aa4bc62cde2c8aeb8a99a7b4401b241e274ae7b11df67d863d61ab3d5de", size = 92851, upload-time = "2026-10-09T20:29:09.15Z" }, + { url = "https://files.pythonhosted.org/packages/ff/3a/019abd746e8fbed9edd74b81259f20b278c011cef918868506d9b5bd6566/multidict-7.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fabfdd4cf97db033196b51af46b8a681d4785c2a66347f2a5af1b4bbb1182629", size = 177429, upload-time = "2026-10-09T20:29:10.989Z" }, + { url = "https://files.pythonhosted.org/packages/f7/41/c8dda935231305d9b83c4a4cc5b5b9e323615328d4704f00ba2cf6d89916/multidict-7.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a177a0ee5cf19931dcaeb3f662bc562754cfa4f4ace2351d9da24a954ef7db94", size = 102003, upload-time = "2026-10-09T20:29:12.78Z" }, + { url = "https://files.pythonhosted.org/packages/af/23/3c3e79a222eaaa59a32648cec34709d848657be5339f96876075731e7f8e/multidict-7.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0ae91de396d5c4ac97cb24dbada3d5c91a51454781e0a70476b008f4e879e4f0", size = 104861, upload-time = "2026-10-09T20:29:14.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5b/04637e7cea5729466fb89048520fde167c5404df1beedcc86f18aeabed7c/multidict-7.1.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:160bdb3520fdadcaa21e1b98aab2e011265070814ecab3804eb61674becbd400", size = 463221, upload-time = "2026-10-09T20:29:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/2d/42/b121767de213a9774399c200cb877b19c2e1e5a0b0a6bc8407c7325fa37e/multidict-7.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c2144785e42527404bbd5cfd11981fee4abe59a22aded0e498eb711a831d3f3", size = 502241, upload-time = "2026-10-09T20:29:18.657Z" }, + { url = "https://files.pythonhosted.org/packages/47/0a/1cccd17ebf39776df7d8a6c99d066fc3ac1823d044cfe86a22ebbf42b841/multidict-7.1.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7d0b4fec6a8d02d7e95de5cfa913261820f1ce04bd4c0381924de0da523179b8", size = 457128, upload-time = "2026-10-09T20:29:20.589Z" }, + { url = "https://files.pythonhosted.org/packages/19/a9/0616d20dee16255f8737d9017288d9304dabf3695ac0071baca25d00c713/multidict-7.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:943a9bce22180ad0f4d32d1b402a0949a4ecfe5a1257b47f54a1b51981d81b86", size = 511842, upload-time = "2026-10-09T20:29:22.569Z" }, + { url = "https://files.pythonhosted.org/packages/91/68/5a406b82ecfeee50c097c05fd519c836a61815d81c4a9a8c523d90fb4b35/multidict-7.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f76ceb623f7ff50df46ac57e1587c479d87a5766319c4f43d0c0a5158896afab", size = 519852, upload-time = "2026-10-09T20:29:24.527Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/d42234f9d4f0e33885c70149112bf62aa4436045090c15cec0810ada0b2f/multidict-7.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98beff85392ce435b28a0971ec21cade61ce8be8b632c9d855475a28ef92d31a", size = 500322, upload-time = "2026-10-09T20:29:26.569Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c6/323e921a9812a6ea82978d4e1d9713e0b709023be037e6e4c14efe5472eb/multidict-7.1.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:71196ebb8d523148e5975396a444de02367f204b53b14e26794c96b2be0ed742", size = 444070, upload-time = "2026-10-09T20:29:28.52Z" }, + { url = "https://files.pythonhosted.org/packages/93/87/109b7170de2168294f3e562f9d10caaaf946628d614b5a57246279acc1a6/multidict-7.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:35534b366410a36bb3d6f788691e37a76e4d1da48326b0ada3e5032580dd76af", size = 484862, upload-time = "2026-10-09T20:29:30.504Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/7f1f63c9cf13f47036c0c64c607e6eee87c94076e3c6876abb6990ee62ad/multidict-7.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4674b12701c3fcbdf7f88b9e4479701c93bec5da9eb576140d5fcc0092990af", size = 457314, upload-time = "2026-10-09T20:29:32.6Z" }, + { url = "https://files.pythonhosted.org/packages/93/ce/7de8b4ee6a847cc951915c279fc388127ec32a7c14aa7a01c4ad692be575/multidict-7.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ead852a5e906a43fcb6784eeac480f6a67919a51d480c1f80d32ddf9d615475", size = 462672, upload-time = "2026-10-09T20:29:34.776Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/02dd089475983b1f5b8729319caa0b02fa3a60b527f27c86b2d53c24ca11/multidict-7.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:afe36ca503c2ffe30fb6df82b20389fa3c4035b5d65888a61310921cf3ae91c5", size = 498109, upload-time = "2026-10-09T20:29:36.763Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/b9bdc59aa8e3eaf1f9e5d2460bbe0c428a01250d44417f8ccf965a8f6fa6/multidict-7.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a5f0bebb10aae010d3c9ee3abaf83ab2069c718457aea09c15532355dd7e061f", size = 442260, upload-time = "2026-10-09T20:29:38.872Z" }, + { url = "https://files.pythonhosted.org/packages/fa/83/21b044885ab81bf5c03ec5c4c16aa449977e428c5c122e38969481cec1df/multidict-7.1.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:b9d9b7d72975521434368fe8aed3f6b522060bf271adabaa5ca6c87c0c08e168", size = 498916, upload-time = "2026-10-09T20:29:41.091Z" }, + { url = "https://files.pythonhosted.org/packages/95/8b/31686730092e349ee2255fa630945ca344fc62e76f0b18d65b0f3dbb0ecf/multidict-7.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:274023bf952f849e0d05eba28a4c1f65f9796430d2b09ec16539386c0f76554c", size = 493525, upload-time = "2026-10-09T20:29:43.192Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d0/c9fddcd7ac42b70a46ac9cf15e21eb527874164e14d418fcb50ce7190a4b/multidict-7.1.0-cp314-cp314t-win32.whl", hash = "sha256:7e0bfa161df365ba3c88899ee3b7c94755200967284bdedef8c1b8b43e2c0f2b", size = 86481, upload-time = "2026-10-09T20:29:45.188Z" }, + { url = "https://files.pythonhosted.org/packages/24/ab/ce727c06680e72b5d381caa8587f561cb6fea1bfab6ba20c877019768a6f/multidict-7.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:34a35be8fb82d37087e8176aba907b9459f03d0e293c80f574c6337a436f4eaa", size = 99676, upload-time = "2026-10-09T20:29:47.441Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e7/d116d7ce514d04d9bf63774ed55e8033e2ce15ab5655a597bb947c53dfb8/multidict-7.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:d7dd46a8fcd7653c09ebe67eae9d4cb6636c7a905d9cbaf587dabcbd4eca6013", size = 98726, upload-time = "2026-10-09T20:29:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/28/42/963e2e38ffd79487b24775841cfe2abb85aa1fa08ee152afc4a9b64e1cdd/multidict-7.1.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:852c921217f330b3e81a822647ebadeae7e42cf503ec1992d0bfbc90121c09fb", size = 100450, upload-time = "2026-10-09T20:29:51.486Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6e/fbc4721aa0eaea2dff3274ff44e94f60a89841bbb4cdaecd9faa4eb64aa7/multidict-7.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cbec738d2ad551c6f70955d7eec95e339380ee1564e2afe86bfee05fed52ceec", size = 95440, upload-time = "2026-10-09T20:29:53.929Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/045aee749b599560d5348bb1bee5c9d61d8b12f31d16e0594778ef3472de/multidict-7.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a5f721a2437390ab69c10c6df5c142478d399af8dfb02e6d823cf2358e8a4748", size = 97496, upload-time = "2026-10-09T20:29:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/97/79/f2916b81324d629eba363f760fba26cafef5cb437489c24969ada979508d/multidict-7.1.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:99cf27791129d37e191ff013bfc29bf6631c29edb21680c00978567b91fc5d6b", size = 163241, upload-time = "2026-10-09T20:29:57.813Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f6/2aab2b2d7bf2d69204bde0cdabfefe4df3d1954f3f156bfc53714917e757/multidict-7.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2ba6611fc93c4b169d0e0ea376ebf4b8a529933d1f5f2c2ec7d8f8b93ef58ec2", size = 94586, upload-time = "2026-10-09T20:30:00.201Z" }, + { url = "https://files.pythonhosted.org/packages/b6/83/cf690ab80a0db0f50b300a7141f45dbf1bbbb6b32457f74bb55f47d38f5c/multidict-7.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:c45629c0049fbdef932dbe408ac2b271fdc8c7d9962ca31160f4a0fc3455fe4f", size = 97573, upload-time = "2026-10-09T20:30:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2e/951c1412c9803e8f87c8673305be1e448761c940cd867ccfdd3e6a551dfd/multidict-7.1.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cf606cfe3f67984b4064ac605d71e1eba12515fbabf5bd5a34a8952b8800dc66", size = 453476, upload-time = "2026-10-09T20:30:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/a1/55/12f9f95fc65470bf481ad3d1c0e0fc20094720d0af1985119656f2b4be25/multidict-7.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50fdfcb03be719d9573597b095b1175d2e9d0b30d065791dfd9fca727c499442", size = 473078, upload-time = "2026-10-09T20:30:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/87/31/ebe216194a68934de7053d582cde62c839bef4dbc4ae67e9bbccf80a61d2/multidict-7.1.0-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a8bba9d1f6db4ef2a6ebfc937a65d36e80e3aada00b382eaf56fea8f639322d5", size = 435633, upload-time = "2026-10-09T20:30:09.021Z" }, + { url = "https://files.pythonhosted.org/packages/46/e5/4aea764cd6a1d326d91f2bb92b1c8c39c3f5a8da624e7b20b88179dffb72/multidict-7.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46d4af0afc6eb9867b3ae50605787c80b868e2f52eac3801246034925fe578b8", size = 488656, upload-time = "2026-10-09T20:30:11.119Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f8/b1e5935233fa4b504b1a58b584fcdf03526635e4b5f268f716be9108a1a4/multidict-7.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5cc58ebb731200ddb64d55f1b345630fb5f7a8138cdbd242af9dce964a7cb03d", size = 502311, upload-time = "2026-10-09T20:30:13.396Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8e/cde07147b6fc56b9ae0fe01d9c0bebd730b2be2c3052d3a5a117e13f561e/multidict-7.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b87ad54e8d4adeb0a1f04889504d6ec7f04fb02609220810f51f1b6c66bc1cc", size = 477470, upload-time = "2026-10-09T20:30:15.894Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/11ac1880eacddcf698aa227ba9387cda60128e38034eda182cf4585109ee/multidict-7.1.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44f7e5dd83a615636b80182bdf446ece57ed61d5d51854acc5d9840631136d4e", size = 430088, upload-time = "2026-10-09T20:30:18.972Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5a/09926de0ec910704507a21c4f1ad76026aab9a5657ced868c57bbbe9324b/multidict-7.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9bc5e7f843d14a167cdc26fe2d22f6f3aa2feb57919cf3ff034262a57d8d95d0", size = 460632, upload-time = "2026-10-09T20:30:21.45Z" }, + { url = "https://files.pythonhosted.org/packages/ed/36/cc51eb3ffb09f475326bbf868c6af2805fbbe9175122ce6d7a65aa06d6c1/multidict-7.1.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:0631eb5f49f67de10bbdc3f64141326dbc62e8d319900966648381ce0845d8ca", size = 434056, upload-time = "2026-10-09T20:30:23.679Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f4/9e0e4dce595573eba1555495ac9ca6b5f7f6e14cf6c3a5cd87ce5bd33d6f/multidict-7.1.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:d1b1b32f3c32f734dde8f36ac1df8e275e768a7b333241cd637cb2538628a4b4", size = 453014, upload-time = "2026-10-09T20:30:26.192Z" }, + { url = "https://files.pythonhosted.org/packages/96/14/993a7ac9f3592f5628a12f6bc2495f5cce5814b883170e06f487dede6969/multidict-7.1.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:159976f9c40f96e3fe0952b708846a43a76bacb114e9cc828816f5080bddd5ec", size = 476343, upload-time = "2026-10-09T20:30:28.532Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/c2147c1e225a676ee0c97f97bb935fb8a70821bc12df240491bf9f140801/multidict-7.1.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:81a0e08c64dfdad27dab687b96f572b23bafa1999a39d1b6f70b3ddbb73e8bd0", size = 426813, upload-time = "2026-10-09T20:30:31.015Z" }, + { url = "https://files.pythonhosted.org/packages/5f/fa/8858b260a6662035670759e7b3a7da9decc227296491749073e76202de93/multidict-7.1.0-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:4e11e7299079718c78f8147e7206c22fe35bab4466d38992420795288a0b8096", size = 483882, upload-time = "2026-10-09T20:30:33.509Z" }, + { url = "https://files.pythonhosted.org/packages/c2/43/d42dc515d56d506f437e8a19c4f604719a306f8d2b60f70a14be34ac9231/multidict-7.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:34d2ee98e15d5cfe782a431bc913fce3b58cf3fdb34fcb437aeb275cdf9007ab", size = 469953, upload-time = "2026-10-09T20:30:35.996Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/1b6fb52f7e082b4d49b069d362169a2e6dbc43800f152f33c8f0e8933117/multidict-7.1.0-cp315-cp315-win32.whl", hash = "sha256:f376224572d1f5da1c871f969ab04765727f180e70d012d93e07bfc08442c64b", size = 80202, upload-time = "2026-10-09T20:30:38.121Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/5dec513dec950825529037703da89a597dc0b157e7b1b8d0304dd9e672cc/multidict-7.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:67fcf28db77b385820881521db7435e9f1c607cfaf07db6eb78aa9d1146bde86", size = 92921, upload-time = "2026-10-09T20:30:40.507Z" }, + { url = "https://files.pythonhosted.org/packages/dd/eb/fd62025f8cd3dad6f936df8bd7ff2642e2fe3fe0166ef2fc75699ee57cac/multidict-7.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:c7aafa4dd2f702ee2198005d6cba4309c1e25ed1c201d77beddefa47411bead8", size = 92848, upload-time = "2026-10-09T20:30:43.026Z" }, + { url = "https://files.pythonhosted.org/packages/42/f4/2dcd45731d2f57b87211a1dab1c29a72c6a4a527f370851c3e4cef5c4445/multidict-7.1.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1348ddc076251cd542f4a99ccda4b7c1f8444e8ab489d3541a978ca5901c7c1f", size = 177634, upload-time = "2026-10-09T20:30:45.125Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d6/d57b12a5bb19396a99ae66ec48ff69f764cc2c83e83bd9a003bd5ed5802a/multidict-7.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:9267bf8261a779abb2a6eab5f107f5db85b2d1745f2494081c731aaf28738ce3", size = 101954, upload-time = "2026-10-09T20:30:47.416Z" }, + { url = "https://files.pythonhosted.org/packages/46/8b/374e3f6e4581b8f8425712214cad854ff9370019483aaa49e6207c160adf/multidict-7.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:8a844b8b1685f38a2e8b2f3213b286e2a7abfe67508381780a0d4599ac337c1c", size = 105056, upload-time = "2026-10-09T20:30:49.618Z" }, + { url = "https://files.pythonhosted.org/packages/9e/0a/6c89e2179a84eaa78eec658b9a05f10d3e22654708d7b2e3ebdf0faa0614/multidict-7.1.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:18a447d46a3a2f1e61b365cbf5627db7030fdb707dad70c4f2760e5144166ecc", size = 467374, upload-time = "2026-10-09T20:30:51.933Z" }, + { url = "https://files.pythonhosted.org/packages/c5/62/431ed831b5e9b94c75ed0a410786d07176e879603601570eb25594039200/multidict-7.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88811f890db240a1c82bf0bcd52973763707a552c8113ac3fcebca183afb2fa8", size = 504024, upload-time = "2026-10-09T20:30:54.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dc/52e4204538a7824e407e531b742ff6f7a2ff614dd40382d29a62755699cb/multidict-7.1.0-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a60b720c329c0007feae692b7bf91cf17b3f9bd3727be96cc6f9a3336651041b", size = 460139, upload-time = "2026-10-09T20:30:57.193Z" }, + { url = "https://files.pythonhosted.org/packages/22/4c/0228d61a8fee69a7ac4d8d0426aa1810b9c36b679a8c51844de701230073/multidict-7.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b117ed1cd1a23df0902461c38093408b95971833dcee629112acda25b603c8d0", size = 513162, upload-time = "2026-10-09T20:30:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/56/50/a0b28bf4f036897bbd44c8761fbb384c014c34f621687799d07345b62820/multidict-7.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bbcae7a54050b7ad7bc7bf425ba63dea7d2cd31a92246ba787a2ce69a9b98dbc", size = 520312, upload-time = "2026-10-09T20:31:02.515Z" }, + { url = "https://files.pythonhosted.org/packages/4f/1d/ba96f77c24174eff9f6521cc7c83958f27598b924a9f7b6aca5d11c73f35/multidict-7.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4cba2b0b9235fe10e12301d6b4cfba0f353fa668d635f6e988b03623c2cd42ba", size = 502953, upload-time = "2026-10-09T20:31:05.187Z" }, + { url = "https://files.pythonhosted.org/packages/59/f2/14c3ffb649cf45a629ec38ca757a493f0caf729e3f3580816dc7779e4caa/multidict-7.1.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:535173fbcc3933d84f9929d49d7a59a0faec259ee07d07c34c7d2a980b4e3683", size = 451497, upload-time = "2026-10-09T20:31:07.597Z" }, + { url = "https://files.pythonhosted.org/packages/55/df/fa4f8f6ee2314bdd3e4bc830c25f1d8a737188198abeaf105c73454f0b95/multidict-7.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ea027bdeca1d7e498237634ee4e3a852e2723eef39996dec0ff0f77dff8a2336", size = 486212, upload-time = "2026-10-09T20:31:10.178Z" }, + { url = "https://files.pythonhosted.org/packages/32/5e/3227762b04a8ff1a90359ee2c04af9ed095fe3892e101705fa4233b51514/multidict-7.1.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:507151e1e3dee95e9e8159e329aed4f75aa5205ecd6505a4f6be546890eafbe1", size = 459864, upload-time = "2026-10-09T20:31:13.044Z" }, + { url = "https://files.pythonhosted.org/packages/5e/04/58524c7e82176438a48704a687fb383cc943dd7075c67b5bea288043b504/multidict-7.1.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9c10791e9f5ef132effc8fdce2009482c1cfb26618c5fc1b7952a47dd5eb632e", size = 465861, upload-time = "2026-10-09T20:31:15.503Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d6/28e549b6ec0c829647de4c75cf32e4c7ea0d0e87b91dfb399292f630c365/multidict-7.1.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f16ac8af2804855d3cae5fc3c5ab609c9fd0fc8ecacd92579c05ed3c173396fd", size = 498865, upload-time = "2026-10-09T20:31:18.548Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ac/3006640586b4d33442c53989b78fc290c8eae5fab5b62a31ac7a4c220e69/multidict-7.1.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b78de22bae456a976f33df34d598dfd16edc9a03df8f4cc8b7c17bdba4c97b4a", size = 449885, upload-time = "2026-10-09T20:31:21.03Z" }, + { url = "https://files.pythonhosted.org/packages/80/7e/1ece407cee9f9aff9ace9e44379ed937bccb790936ae448feb79a82c362e/multidict-7.1.0-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:db77888081431aaa69f3fd3480891746ddce6c2a571f6201869a24e2f06cf423", size = 500237, upload-time = "2026-10-09T20:31:23.611Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/c052b700ffcf689454848ee81ba87bedfdc10caff592c0d2a327c43939a5/multidict-7.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c39dfcaa0bf23443474c0cb58d8d8aea9529c1841d99654cb38e4dada7b1948a", size = 495143, upload-time = "2026-10-09T20:31:26.237Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ef/9ddad94c32fb0bdfc5ad0942d06ae6699bd30d261b0ff59b94810a133238/multidict-7.1.0-cp315-cp315t-win32.whl", hash = "sha256:16b21164797bde6f417066d02775975cc2e15ab8abf80efa55fe85e0b4894020", size = 86526, upload-time = "2026-10-09T20:31:28.734Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ce/2efa1f32a07adfe041d48c6d04c08b6f9db66b8512c765983172399b2855/multidict-7.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:55392202cb374dd1a1f89a8ce1586644870d9e936752059d053e576acc50bc89", size = 99671, upload-time = "2026-10-09T20:31:31.282Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b3/a44c301fe13716c5f2c08afd89a481018988cde4b337b36e28a2b0fcf3a3/multidict-7.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:f979a077d1c0a9a36dd4fab0d3a36b8de7b593bf935e13df85a380395b2c11ad", size = 98893, upload-time = "2026-10-09T20:31:33.62Z" }, + { url = "https://files.pythonhosted.org/packages/d0/86/a3de309c5e28ee85b314d0e3ba0e0dea6fd361c313322a05e67be4656e1e/multidict-7.1.0-py3-none-any.whl", hash = "sha256:d9ef29cfd98e17085b4f91bba8fa1570bec6787d5c52ce653ed33a58785585d0", size = 28087, upload-time = "2026-10-09T20:31:35.945Z" }, ] [[package]] diff --git a/vitest.config.js b/vitest.config.js index 94371ec235c..863fe0bdb22 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -11,6 +11,7 @@ export default defineConfig({ test: { coverage: { provider: 'v8', + reportOnFailure: true, ///< Preserve CI coverage artifacts after test failures. reporter: ['text', 'json', 'lcov'], reportsDirectory: 'coverage', },