Showing preview only (1,270K chars total). Download the full file or copy to clipboard to get everything.
Repository: colinmarc/magic-mirror
Branch: main
Commit: f2be8a13bdac
Files: 155
Total size: 1.2 MB
Directory structure:
gitextract_32ud61vu/
├── .github/
│ ├── actions/
│ │ └── install-slang/
│ │ └── action.yaml
│ └── workflows/
│ ├── bump-version.yaml
│ ├── cliff.toml
│ ├── docs.yaml
│ ├── release-mmclient.yaml
│ ├── release-mmserver.yaml
│ └── tests.yaml
├── .gitignore
├── .gitmodules
├── .rustfmt.toml
├── BUILD.md
├── CHANGELOG.md
├── LICENSES/
│ ├── BUSL-1.1.txt
│ └── MIT.txt
├── README.md
├── auto-release.sh
├── docs/
│ ├── .gitignore
│ ├── config.toml
│ ├── content/
│ │ ├── _index.md
│ │ └── setup/
│ │ ├── client.md
│ │ └── server.md
│ └── templates/
│ └── footer.html
├── mm-client/
│ ├── Cargo.toml
│ ├── build.rs
│ └── src/
│ ├── audio/
│ │ └── buffer.rs
│ ├── audio.rs
│ ├── bin/
│ │ ├── latency-test.rs
│ │ └── mmclient.rs
│ ├── cursor.rs
│ ├── delegate.rs
│ ├── flash.rs
│ ├── font.rs
│ ├── gamepad.rs
│ ├── keys.rs
│ ├── lib.rs
│ ├── overlay.rs
│ ├── render.rs
│ ├── render.slang
│ ├── stats.rs
│ ├── video.rs
│ └── vulkan.rs
├── mm-client-common/
│ ├── Cargo.toml
│ ├── bin/
│ │ └── uniffi-bindgen.rs
│ └── src/
│ ├── attachment.rs
│ ├── codec.rs
│ ├── conn/
│ │ └── hostport.rs
│ ├── conn.rs
│ ├── display_params.rs
│ ├── input.rs
│ ├── lib.rs
│ ├── logging.rs
│ ├── packet/
│ │ └── ring.rs
│ ├── packet.rs
│ ├── pixel_scale.rs
│ ├── session.rs
│ ├── stats.rs
│ └── validation.rs
├── mm-docgen/
│ ├── Cargo.toml
│ └── src/
│ └── bin/
│ ├── config-docgen.rs
│ └── protocol-docgen.rs
├── mm-protocol/
│ ├── Cargo.toml
│ ├── build.rs
│ └── src/
│ ├── lib.rs
│ ├── messages.proto
│ └── timestamp.rs
├── mm-server/
│ ├── Cargo.toml
│ ├── build.rs
│ ├── deny.toml
│ └── src/
│ ├── codec.rs
│ ├── color.rs
│ ├── config.rs
│ ├── container/
│ │ ├── ipc.rs
│ │ └── runtime.rs
│ ├── container.rs
│ ├── encoder/
│ │ ├── dpb.rs
│ │ ├── gop_structure.rs
│ │ ├── h264.rs
│ │ ├── h265.rs
│ │ ├── rate_control.rs
│ │ └── stats.rs
│ ├── encoder.rs
│ ├── main.rs
│ ├── pixel_scale.rs
│ ├── server/
│ │ ├── handlers/
│ │ │ ├── attachment/
│ │ │ │ └── stats.rs
│ │ │ ├── attachment.rs
│ │ │ └── validation.rs
│ │ ├── handlers.rs
│ │ ├── mdns.rs
│ │ ├── sendmmsg.rs
│ │ └── stream.rs
│ ├── server.rs
│ ├── session/
│ │ ├── audio/
│ │ │ ├── buffer.rs
│ │ │ └── pulse.rs
│ │ ├── audio.rs
│ │ ├── compositor/
│ │ │ ├── buffers/
│ │ │ │ ├── modifiers.rs
│ │ │ │ └── syncobj_timeline.rs
│ │ │ ├── buffers.rs
│ │ │ ├── dispatch/
│ │ │ │ ├── shm.rs
│ │ │ │ ├── wl_buffer.rs
│ │ │ │ ├── wl_compositor.rs
│ │ │ │ ├── wl_data_device_manager.rs
│ │ │ │ ├── wl_drm.rs
│ │ │ │ ├── wl_output.rs
│ │ │ │ ├── wl_seat.rs
│ │ │ │ ├── wl_shm.rs
│ │ │ │ ├── wp_fractional_scale.rs
│ │ │ │ ├── wp_linux_dmabuf.rs
│ │ │ │ ├── wp_linux_drm_syncobj.rs
│ │ │ │ ├── wp_pointer_constraints.rs
│ │ │ │ ├── wp_presentation.rs
│ │ │ │ ├── wp_relative_pointer.rs
│ │ │ │ ├── wp_text_input.rs
│ │ │ │ ├── xdg_shell.rs
│ │ │ │ └── xwayland_shell.rs
│ │ │ ├── dispatch.rs
│ │ │ ├── oneshot_render.rs
│ │ │ ├── output.rs
│ │ │ ├── protocols/
│ │ │ │ ├── wayland-drm.xml
│ │ │ │ └── wl_drm.rs
│ │ │ ├── protocols.rs
│ │ │ ├── sealed.rs
│ │ │ ├── seat.rs
│ │ │ ├── serial.rs
│ │ │ ├── shm.rs
│ │ │ ├── stack.rs
│ │ │ ├── surface.rs
│ │ │ ├── xwayland/
│ │ │ │ └── xwm.rs
│ │ │ └── xwayland.rs
│ │ ├── compositor.rs
│ │ ├── control.rs
│ │ ├── handle.rs
│ │ ├── input/
│ │ │ └── udevfs.rs
│ │ ├── input.rs
│ │ ├── reactor.rs
│ │ ├── video/
│ │ │ ├── composite.rs
│ │ │ ├── composite.slang
│ │ │ ├── convert.rs
│ │ │ └── convert.slang
│ │ └── video.rs
│ ├── session.rs
│ ├── state.rs
│ ├── vulkan/
│ │ ├── chain.rs
│ │ ├── drm.rs
│ │ ├── timeline.rs
│ │ └── video.rs
│ ├── vulkan.rs
│ └── waking_sender.rs
├── mmserver.default.toml
├── shader-common/
│ └── color.slang
└── test-apps/
├── Cargo.toml
├── bin/
│ ├── color.rs
│ ├── cursorlock.rs
│ └── latency.rs
├── build.rs
└── src/
└── color-test.slang
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/actions/install-slang/action.yaml
================================================
name: Install slang
inputs:
version:
required: true
target:
required: true
token:
required: true
runs:
using: "composite"
steps:
- name: install slang
shell: bash
run: |
mkdir $RUNNER_TEMP/slang
slang_url=$( gh api https://api.github.com/repos/shader-slang/slang/releases/tags/${{ inputs.version }} |\
jq -r '.assets[].browser_download_url' | grep ${{ inputs.target }}.tar.gz | head -1 )
(cd $RUNNER_TEMP/slang && curl -o - -fsSL "$slang_url" | tar zxv)
echo "SLANG_DIR=$RUNNER_TEMP/slang" >> "$GITHUB_ENV"
echo "LD_LIBRARY_PATH=$RUNNER_TEMP/slang/lib" >> "$GITHUB_ENV"
echo "DYLD_LIBRARY_PATH=$RUNNER_TEMP/slang/lib" >> "$GITHUB_ENV"
env:
GH_TOKEN: ${{ inputs.token }}
================================================
FILE: .github/workflows/bump-version.yaml
================================================
on:
push:
branches: [main]
name: Open a PR to bump the version
jobs:
open_pr:
strategy:
matrix:
component: ["server", "client"]
name: Open PR
runs-on: ubuntu-24.04
permissions:
pull-requests: write
contents: write
steps:
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- uses: swatinem/rust-cache@v2
- run: cargo install git-cliff@^2.6 cargo-edit@^0.12
- name: determine version
run: |
echo "COMPONENT=${{ matrix.component }}" | tee -a "$GITHUB_ENV"
echo "CURRENT_VERSION=$( git tag | grep "${{ matrix.component }}" | tail -1 )" | tee -a "$GITHUB_ENV"
mm_component="mm-${{ matrix.component }}"
echo "MM_COMPONENT=$mm_component" | tee -a "$GITHUB_ENV"
version=$( git cliff -c .github/workflows/cliff.toml \
--bumped-version \
--include-path "$MM_COMPONENT*/**/*" \
--tag-pattern "${{ matrix.component }}" )
echo "BUMPED_VERSION=$version" | tee -a "$GITHUB_ENV"
echo "BUMPED_VERSION_SHORT=$( echo $version | sed -E 's/^[a-z]+-v(.*)/\1/' )" | tee -a "$GITHUB_ENV"
- name: replace version in files
if: ${{ env.BUMPED_VERSION != env.CURRENT_VERSION }}
run: |
git grep --cached -l '' | grep -v CHANGELOG |\
xargs sed -i -E "s/mm$COMPONENT-v[0-9]+\.[0-9]+\.[0-9]+/$BUMPED_VERSION/g"
- name: replace version in Cargo.toml
if: ${{ env.BUMPED_VERSION != env.CURRENT_VERSION }}
run: (cd $MM_COMPONENT && cargo set-version --offline $BUMPED_VERSION_SHORT)
- name: cargo update
if: ${{ env.BUMPED_VERSION != env.CURRENT_VERSION }}
run: (cd $MM_COMPONENT && cargo update $MM_COMPONENT)
- name: update BUSL change date
if: ${{ env.BUMPED_VERSION != env.CURRENT_VERSION && matrix.component == 'server' }}
run: |
change_date=$(date -d "4 years hence" +%Y-%m-01) # Round down to the 1st of the month
sed -i -E "/Change/s/[0-9]{4}-[0-9]{2}-[0-9]{2}/$change_date/" LICENSES/BUSL-1.1.txt
- name: update CHANGELOG.md
if: ${{ env.BUMPED_VERSION != env.CURRENT_VERSION }}
run: |
git cliff -c .github/workflows/cliff.toml \
--include-path "$MM_COMPONENT*/**/*" \
--tag-pattern "$COMPONENT" \
-t "$BUMPED_VERSION" -u \
-p CHANGELOG.md
- name: generate PR body
if: ${{ env.BUMPED_VERSION != env.CURRENT_VERSION }}
run: |
git cliff -c .github/workflows/cliff.toml \
--include-path "$MM_COMPONENT*/**/*" \
--tag-pattern "$COMPONENT" \
-t "$BUMPED_VERSION" -u > "$RUNNER_TEMP/pr-body.txt"
- name: open PR
if: ${{ env.BUMPED_VERSION != env.CURRENT_VERSION }}
id: cpr
uses: peter-evans/create-pull-request@6d6857d36972b65feb161a90e484f2984215f83e
with:
draft: true
branch: "auto-bump-${{ matrix.component }}"
title: ":robot: bump mm${{ matrix.component }} to ${{ env.BUMPED_VERSION }}"
commit-message: "chore: release ${{ env.BUMPED_VERSION }}"
body-path: "${{ runner.temp }}/pr-body.txt"
================================================
FILE: .github/workflows/cliff.toml
================================================
[changelog]
render_always = true
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits %}
- {{ commit.message | upper_first }} \
({{ commit.id }})\
{% endfor %}
{% endfor %}\n
"""
[git]
commit_parsers = [
{ message = "^feat", group = "<!-- 0 -->New Features" },
{ message = "^fix", group = "<!-- 1 -->Bugfixes" },
{ message = "^doc", skip = true },
{ message = "^perf", skip = true },
{ message = "^refactor", skip = true },
{ message = "^style", skip = true },
{ message = "^test", skip = true },
{ message = "^chore|^ci", skip = true },
{ message = "build", skip = true },
{ body = ".*security", skip = true },
{ message = "^revert", skip = true },
]
[bump]
features_always_bump_minor = false
breaking_always_bump_major = false
================================================
FILE: .github/workflows/docs.yaml
================================================
on:
push:
branches: [main, docs]
name: Build documentation site
jobs:
build:
name: Build
runs-on: ubuntu-24.04
steps:
- uses: dtolnay/rust-toolchain@stable
- uses: actions/checkout@v4
with:
submodules: true
- uses: swatinem/rust-cache@v2
with:
workspaces: |
mm-protocol
mm-client-common
- name: install protoc
run: |
sudo apt update
sudo apt install protobuf-compiler
- name: install zola
uses: taiki-e/install-action@v2
with:
tool: zola@0.19.2
- name: generate config reference
run: |
mkdir -p docs/content/reference
cargo run --manifest-path mm-docgen/Cargo.toml --bin config-docgen \
mmserver.default.toml > docs/content/reference/config.md
- name: generate protocol reference
run: |
cargo run --manifest-path mm-docgen/Cargo.toml --bin protocol-docgen \
mm-protocol/src/messages.proto > docs/content/reference/protocol.md
- name: zola build
run: zola -r docs build -o docs/build
- name: generate rustdoc for mm-protocol
run: |
cargo doc --manifest-path mm-protocol/Cargo.toml \
--no-deps --target-dir docs/build
- name: generate rustdoc for mm-client-common
run: |
cargo doc --manifest-path mm-client-common/Cargo.toml \
--no-deps --target-dir docs/build
- name: Upload static files
id: deployment
uses: actions/upload-pages-artifact@v3
with:
path: docs/build
deploy:
name: Deploy
runs-on: ubuntu-latest
needs: build
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
================================================
FILE: .github/workflows/release-mmclient.yaml
================================================
on:
push:
tags:
- 'mmclient-v*.*.*'
name: Release mmclient
jobs:
create_tarball_linux:
name: Build mmclient (linux)
runs-on: ubuntu-24.04
steps:
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu
- name: install deps
run: |
sudo apt update
sudo apt install \
nasm cmake protobuf-compiler libxkbcommon-dev libwayland-dev libasound2-dev \
ffmpeg libavutil-dev libavformat-dev libavdevice-dev libavfilter-dev \
libfontconfig-dev libfreetype-dev libudev-dev
- uses: actions/checkout@v4
- uses: ./.github/actions/install-slang
with:
token: ${{ secrets.GITHUB_TOKEN }}
target: linux-x86_64
version: v2025.5
- uses: swatinem/rust-cache@v2
with:
workspaces: |
mm-client
mm-protocol
- name: cargo build
run: (cd mm-client && cargo build --bin mmclient --release --target x86_64-unknown-linux-gnu)
- name: create release tarball
run: |-
mkdir "${RUNNER_TEMP}/${GITHUB_REF_NAME}"
cp -r mm-client/target/x86_64-unknown-linux-gnu/release/mmclient \
README.md CHANGELOG.md \
"${RUNNER_TEMP}/${GITHUB_REF_NAME}"
cp LICENSES/MIT.txt "${RUNNER_TEMP}/${GITHUB_REF_NAME}/LICENSE.txt"
tar -C "${RUNNER_TEMP}" --numeric-owner -cvzf "${GITHUB_REF_NAME}-linux-amd64.tar.gz" "$GITHUB_REF_NAME"
- name: upload tarball
uses: actions/upload-artifact@v4
with:
name: mmclient-linux
path: mmclient-*.tar.gz
create_tarball_macos:
name: Build mmclient (macos arm)
runs-on: macos-latest
steps:
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
- name: install deps
run: |
brew install ffmpeg@6 protobuf
brew link ffmpeg@6
- uses: actions/checkout@v4
- uses: ./.github/actions/install-slang
with:
token: ${{ secrets.GITHUB_TOKEN }}
target: macos-aarch64
version: v2024.15.2
- uses: swatinem/rust-cache@v2
with:
workspaces: |
mm-client
mm-protocol
- name: cargo build
run: (cd mm-client && cargo build --bin mmclient --release --features moltenvk_static --target aarch64-apple-darwin)
- name: create release tarball
run: |-
mkdir "${RUNNER_TEMP}/${GITHUB_REF_NAME}"
cp -r mm-client/target/aarch64-apple-darwin/release/mmclient \
README.md CHANGELOG.md \
"${RUNNER_TEMP}/${GITHUB_REF_NAME}"
cp LICENSES/MIT.txt "${RUNNER_TEMP}/${GITHUB_REF_NAME}/LICENSE.txt"
gtar -C "${RUNNER_TEMP}" --numeric-owner -cvzf "${GITHUB_REF_NAME}-darwin-arm64.tar.gz" "$GITHUB_REF_NAME"
- name: upload tarball
uses: actions/upload-artifact@v4
with:
name: mmclient-mac
path: mmclient-*.tar.gz
create_tarball_macos_intel:
name: Build mmclient (macos intel)
runs-on: macos-13
steps:
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-apple-darwin
- name: install deps
run: |
brew install ffmpeg@6 protobuf
brew link ffmpeg@6
- uses: actions/checkout@v4
- uses: ./.github/actions/install-slang
with:
token: ${{ secrets.GITHUB_TOKEN }}
target: macos-x86_64
version: v2024.15.2
- uses: swatinem/rust-cache@v2
with:
workspaces: |
mm-client
mm-protocol
- name: cargo build
run: (cd mm-client && cargo build --bin mmclient --release --features moltenvk_static --target x86_64-apple-darwin)
- name: create release tarball
run: |-
mkdir "${RUNNER_TEMP}/${GITHUB_REF_NAME}"
cp -r mm-client/target/x86_64-apple-darwin/release/mmclient \
README.md CHANGELOG.md \
"${RUNNER_TEMP}/${GITHUB_REF_NAME}"
cp LICENSES/MIT.txt "${RUNNER_TEMP}/${GITHUB_REF_NAME}/LICENSE.txt"
gtar -C "${RUNNER_TEMP}" --numeric-owner -cvzf "${GITHUB_REF_NAME}-darwin-amd64.tar.gz" "$GITHUB_REF_NAME"
- name: upload tarball
uses: actions/upload-artifact@v4
with:
name: mmclient-mac-intel
path: mmclient-*.tar.gz
create_release:
name: Create release
needs: [create_tarball_linux, create_tarball_macos, create_tarball_macos_intel]
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- uses: dtolnay/rust-toolchain@stable
- name: install git-cliff
run: cargo install git-cliff
- name: generate release notes
run: |-
echo "# Client version ${GITHUB_REF_NAME/mmclient-v/}" >> release-notes.txt
git cliff -c .github/workflows/cliff.toml \
--include-path "mm-client/**/*" \
--include-path "mm-client-common/**/*" \
--tag-pattern "client" \
--latest | tail -n +2 | tee -a release-notes.txt
- name: download artifacts
uses: actions/download-artifact@v4
with:
merge-multiple: true
- name: create release
uses: softprops/action-gh-release@v2
with:
body_path: release-notes.txt
files: "mmclient-*.tar.gz"
================================================
FILE: .github/workflows/release-mmserver.yaml
================================================
on:
push:
tags:
- 'mmserver-v*.*.*'
name: Release mmserver
jobs:
create_release:
name: Create mmserver release
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu
- name: install deps
run: |
sudo apt update
sudo apt install nasm cmake protobuf-compiler libxkbcommon-dev
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- uses: ./.github/actions/install-slang
with:
token: ${{ secrets.GITHUB_TOKEN }}
target: linux-x86_64
version: v2025.5
- uses: swatinem/rust-cache@v2
with:
workspaces: |
mm-server
mm-client
mm-protocol
- name: cargo build
run: (cd mm-server && cargo build --bin mmserver --release --target x86_64-unknown-linux-gnu)
- name: create release tarball
run: |-
mkdir "${RUNNER_TEMP}/${GITHUB_REF_NAME}"
cp -r mm-server/target/x86_64-unknown-linux-gnu/release/mmserver \
README.md CHANGELOG.md mmserver.default.toml \
"${RUNNER_TEMP}/${GITHUB_REF_NAME}"
cp LICENSES/BUSL-1.1.txt "${RUNNER_TEMP}/${GITHUB_REF_NAME}/LICENSE.txt"
tar -C "${RUNNER_TEMP}" --numeric-owner -cvzf "${GITHUB_REF_NAME}-linux-amd64.tar.gz" "$GITHUB_REF_NAME"
- name: install git-cliff
run: cargo install git-cliff
- name: generate release notes
run: |-
echo "# Server version ${GITHUB_REF_NAME/mmserver-v/}" >> release-notes.txt
git cliff -c .github/workflows/cliff.toml \
--include-path "mm-server/**/*" \
--tag-pattern "server" \
--latest | tail -n +2 | tee -a release-notes.txt
- name: create release
uses: softprops/action-gh-release@v2
with:
body_path: release-notes.txt
files: "mmserver-*.tar.gz"
================================================
FILE: .github/workflows/tests.yaml
================================================
on:
push:
branches: [main, test-ci]
pull_request:
branches: [main]
name: Tests
jobs:
tests:
name: Tests
runs-on: ubuntu-24.04
steps:
- uses: dtolnay/rust-toolchain@stable
- name: install deps
run: |
sudo apt update
sudo apt install \
nasm cmake protobuf-compiler libxkbcommon-dev libwayland-dev libasound2-dev \
ffmpeg libavutil-dev libavformat-dev libavdevice-dev libavfilter-dev \
libfontconfig-dev libfreetype-dev libudev-dev
- uses: actions/checkout@v4
- uses: ./.github/actions/install-slang
with:
token: ${{ secrets.GITHUB_TOKEN }}
target: linux-x86_64
version: v2025.5
- uses: swatinem/rust-cache@v2
with:
workspaces: |
mm-server
mm-client
mm-protocol
- name: install deny
run: cargo install cargo-deny
- name: server deny
run: (cd mm-server && cargo deny check)
- name: server tests
run: |
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER='sudo -E'
(cd mm-server && cargo test -- --test-threads=1)
- name: protocol tests
run: (cd mm-protocol && cargo test)
- name: client tests
run: (cd mm-client && cargo test)
- name: server cargo clippy
run: (cd mm-server && cargo clippy)
- name: protocol cargo clippy
run: (cd mm-protocol && cargo clippy)
- name: client cargo clippy
run: (cd mm-client && cargo clippy)
================================================
FILE: .gitignore
================================================
// Copyright 2024 Colin Marc <hi@colinmarc.com>
//
// SPDX-License-Identifier: MIT
target
.vscode
.reuse
*.log
mm-protocol/Cargo.lock
mm-client-common/Cargo.lock
================================================
FILE: .gitmodules
================================================
[submodule "docs/themes/anemone"]
path = docs/themes/anemone
url = https://github.com/Speyll/anemone
================================================
FILE: .rustfmt.toml
================================================
use_field_init_shorthand = true
use_try_shorthand = true
unstable_features = true
format_code_in_doc_comments = true
format_macro_bodies = true
format_macro_matchers = true
format_strings = true
group_imports = "StdExternalCrate"
normalize_doc_attributes = true
wrap_comments = true
================================================
FILE: BUILD.md
================================================
## Building `mmserver`
The following are required to build the server and its dependencies:
```
rust (MSRV 1.77.2)
nasm
cmake
protoc
libxkbcommon
```
Besides rust, the following command will install everything on ubuntu:
```
apt install nasm cmake protobuf-compiler libxkbcommon-dev
```
Then you should be good to go:
```
cd mm-server
cargo build --bin mmserver [--release]
```
### Feature flags
The following feature flags are available:
- `vulkan_encode` (on by default) - enables hardware encode
- `svt_encode` (on by default) - enables svt-av1 and svt-hevc for CPU encode
- `ffmpeg_encode` - allows using system-installed ffmpeg to do CPU encode
Note that `ffmpeg_encode` takes precedence over `svt_encode` if enabled, but the server will always choose hardware encode if available on your platform.
## Building `mmclient`
The following are required to build the client and its dependencies:
```
rust (MSRV 1.77.2)
nasm
cmake
protoc
libxkbcommon (only linux)
libwayland-client (only linux)
alsa (only linux)
ffmpeg 6.x
```
Besides rust, the following command will install everything on ubuntu:
```
apt install \
nasm cmake protobuf-compiler libxkbcommon-dev libwayland-dev libasound2-dev \
ffmpeg libavutil-dev libavformat-dev libavdevice-dev libavfilter-dev
```
Or using homebrew on macOS:
```
brew install nasm cmake ffmpeg@6 protobuf
```
================================================
FILE: CHANGELOG.md
================================================
## [mmserver-v0.8.4] - 2025-05-21
### Bugfixes
- Make missing hardware encode support a hard error (b16dccb01902b854a2c345406f4df416d3024811)
## [mmserver-v0.8.3] - 2025-03-12
### Bugfixes
- Try to avoid colliding with the system x11 socket (3af95ba9ab012e723d21415baf0b6f4679ba1534)
- Follow symlinks when calling move_mount (a7505aed296cab4648e2f3752d5901e5d95ded45)
## [mmserver-v0.8.2] - 2025-02-20
### Bugfixes
- Drop application frames if the application is too slow (d73d78dfc37a17fd011c1d3ef1dbfe12c85ed856)
## [mmclient-v0.7.0] - 2025-02-12
### New Features
- Deprecate KeepAlive in favor of connection keepalives (ad3cdca8faf089b85902977e3e48b4a35d5f89e3)
- Send hierarchical_layer as video frame metadata (ddbe84346fa03f55ebed7289b005b2e36ec23d36)
- Expose hierarchical_layer (b4bd4c66b62439c71c6a3ed52c26046b3a2f0b6f)
- Allow clients to configure their connection timeout (452ef70eb118280df9170bed382b8539813e7802)
### Bugfixes
- Remove a useless warning (c98bbe5382914dec22c92f4f160b2f276fb811ef)
## [mmserver-v0.8.0] - 2025-02-12
### New Features
- Make the session timeout configurable (39fa20cadfe7a780088c86f78ef2eae87e0c1222)
- Send hierarchical_layer as video frame metadata (ddbe84346fa03f55ebed7289b005b2e36ec23d36)
### Bugfixes
- Increment stream_seq when a refresh packet is sent (0fe282ae0fb71b476929ddf61bf71e4041ac0323)
- Send headers with H265 keyframes (dc7084412c3c4eec661a9887f8c0d031f8dc8a19)
- Add a warning if users are about to hit #29 (d5591bb4d59a635ffb9796b9c1f1cba9eba22b36)
## [mmserver-v0.7.2] - 2025-02-05
### New Features
- Deprecate KeepAlive in favor of connection keepalives (ad3cdca8faf089b85902977e3e48b4a35d5f89e3)
### Bugfixes
- Remove noexec from /tmp in the container (0c534f6677e07cda77e0384854dded47dd8a949b)
- Support resampling app audio output (897a053abc568255040a66c356703e3e6c3c9070)
- Support downmixing by throwing away extra channels (17d81d866efc94ed2c2839589541362be3d5aae1)
- Allow subtitles in application names (2fcac04765ce4af02923314667289ed88094f824)
- Use aligned width and height for DPB images (d6f3bf713373bbadde0590f44659f8146e44c28d)
- Relax the app startup timeout (a840c2b27b7adda073820a62d72fd64dc90e752b)
- Use QUIC PING frames to keep clients alive (95ddb3d6bdc7e761ff596e249cc7be83b3d14cfb)
- Don't flood the client with pings (b3b3194c042b8d56fda1f8b08f230042bf4461f7)
- Turn down the heat on udevfs logging (53f448e45110ef722a60f927edd7c5fe58455a19)
## [mmserver-v0.7.1] - 2025-01-30
### New Features
- Let the encoder swap frames (b519680e3e8c552874f53cd88e98859e90698ac8)
### Bugfixes
- Update quiche (c3d1e0080c1040151ecdc08e85584ff267f6eed6)
- Remove an extra copy on the dgram path (0d204fa2549f4fd9abc804f4996f8fe11162e67b)
- Fix clippy warning (7b041bbff7c908e57928d621ad74751bb7b76355)
- Respect layer limits in VkVideoEncodeCapabilities (ce515b4d85b8af4da83a3fed0281f907e28253b1)
- Print child logs correctly (e8cd88fb344ec74398eae09e757589700de3bff3)
- Print an error when dmabuf importing fails (112d48d706fc19ce8882e2767c33672e7a044527)
- Change target for vulkan error logs (641f51675a572402710fe3ee2ff0721857228ab9)
- Add context (7572ac6a5131486f6d4cf6951742eea9c0f24d25)
- Use the default congestion control algorithm (7245e624f785b2b01e0b5da507380a88121de542)
- Get explicit sync working on NVIDIA (8d5786445e56629c67338258b1bc8cc7debb410e)
- Remove unused import (df03d5d38236b325e826b998cef26ea2d9008e75)
- Fix explicit sync on nvidia proprietary (8f806b233f537395d72de6e06d3861e73963bec2)
- Check for the right nvidia version (08b6462a320ee76eabaf4387c354a1a6634ec8df)
- Fix explicit sync on nvidia proprietary (3c70a79cda9cc545acf665ccacc495ed30f3440a)
## [mmclient-v0.6.0] - 2025-01-26
### New Features
- Support ffmpeg vulkan decode (5c76b29273d3c0b29edb9e34e33096af76814398)
- Explicit video refresh (60dffc04f4f338c3fce6d791211c12d7471a187a)
- Implement forward error correction (729e652a001d155345c80b7f5fef397a884a1a98)
### Bugfixes
- Enforce non-fractional scales from the client (2a25ca95db01ff8460328f8f258faadf55d948bb)
- Take application names with path included (100d51e8f44129a23b1df944a897a3123ef12d1c)
## [mmserver-v0.7.0] - 2025-01-26
### New Features
- Enable hierarchical coding (90d636ffba8379da420e09c6f228fb65c334a7f2)
- Explicit video refresh (60dffc04f4f338c3fce6d791211c12d7471a187a)
- Implement forward error correction (729e652a001d155345c80b7f5fef397a884a1a98)
- Remove support for CPU encoders (a5d069cb7bde15931748e41ae3d9e12a6f917445)
- Log basic attachment stats (b42cb40cb3d5fbedd2a17d37e09da8984029998c)
### Bugfixes
- Pass correct flags to move_mount (af519eebc5a8f251624b3d063a7241910cddf2cc)
- Pass correct flags to move_mount (take 2) (2e6053675a229dba4fc012b5de4afb723e9a0aca)
- Enforce non-fractional scales from the client (2a25ca95db01ff8460328f8f258faadf55d948bb)
- Disable explicit sync if the syncobj_surface is destroyed (e6017dec6bb9daadbbe50898f7cd9cf7c14b19aa)
- Reduce the verbosity of some logging (b0abe2a76466e98ba8d4f844e88fd4ad4ce6c7ee)
- Print frame duration from encoder (4a9af4f712ce723306364bece359c9bf18515554)
- Add overall encode timing to tracy (334d5b37fe394e652bd27224c1c8e905e9c8a794)
## [mmserver-v0.6.1] - 2024-12-17
### New Features
- Save vulkaninfo for --bug-report (6deae3feb5a72a7e0099edd4983814d7fc873f15)
### Bugfixes
- Avoid an endless loop when printing child output (7d700dfa4b9ef6d02e58c4a32151e69055fa3929)
## [mmserver-v0.6.0] - 2024-12-11
### New Features
- Support wp_linux_drm_syncobj_timeline (54f311653d800cf5a7aefe1b54edd27010f219ce)
- Officially support nvidia proprietary (204126cdfcce09f4971de2e1bb9c86a4adf04d97)
### Bugfixes
- Bind-mount nvidia devices, if present (4bb63d3c1e85f297c5d169219943694f133bbcfa)
## [mmclient-v0.5.0] - 2024-12-11
### New Features
- Add 'app_path' for organizing apps (b417559625c97e182dc074a5732ea35617332f36)
- Add header images to the application list (756bfa866020da57be18d383367e0a2b189051aa)
### Bugfixes
- Use Error::ServerError to communicate server errors (a857e0f186b9514cd3e1dc9b0f60df04b4abe3fe)
- Correctly represent cursor images as optional (b08c76c9c65441fa92156f5282e9b02e98fa3ed9)
- Be more resilient sending messages on closed streams (8e3eea65ccff2b6448dd9993b9afef9996c6650d)
- Ensure attachment_ended is called on delegate (fd4d1c41e7da5ec949e26c91cc6171db1a41b1ce)
- Always send pointer_left events (06010c9cf336d637526dcc308d1ee842e3a21cc1)
- Handle ipv6 addresses correctly (9d442d2c8ad4c8cbfef96cb378289e1699d17e02)
- Log connection errors (0ecc6ef05a5470991f1df8d0feaf18ace99b8de8)
- Remove zone identifiers from hostnames (f9cee190718dc71aad8e9a0372b581a611551289)
## [mmserver-v0.5.6] - 2024-12-08
### New Features
- Warn if the client is using shm buffers (461e8913d9645c240d30a2ce1d269f8ba8aa0e39)
- Support wp_fractional_scale (2a267e102add6fb72504652375d9ea48ec2c6484)
### Bugfixes
- Handle invalid executables more comprehensively (f51174eb1509cecc73c10ab57cf991ee12a5cce7)
- Throw an error if the app exe would be shadowed (cc6ee7e3df086bba443bb41471d671a2bd1b191b)
- Reset staging query state each frame (982afb811ec062ddb6cc498a9cb92e6a4b5472ef)
- Handle stride of shm buffers correctly (e8e1ee5eeba71d767c543ae83c4fa09b381beba1)
- Log when container setup fails (83ea7b46fb95e1f1811cf516c55343622f9d9d35)
- Put the XWayland socket in a canonical location (76056acbdc084307c6d71a66d2c7a343adea9b77)
- Never discard surface content (f28e947201bc53be91ed13a53ad0221c27f931fb)
- Handle xdg_popups more gracefully (03e392506a52349a4fdb075f4a4e53008a237958)
- Translate surface coordinates correctly (9107636d2cb835409df3f604c47eed2d7397e819)
- Shadow /run/user but not /run (c810f24305a169d896cbe92b57d53fd732bdef09)
## [mmserver-v0.5.5] - 2024-12-05
### New Features
- MDNS service discovery (152d82ca7595063aa77db7470e1dfdace9ae7ac2)
- Add 'app_path' for organizing apps (b417559625c97e182dc074a5732ea35617332f36)
- Make the mDNS instance name configurable (17e632ccbee15132e2420a5fc162c94171d4a34a)
- Add header images to the application list (756bfa866020da57be18d383367e0a2b189051aa)
### Bugfixes
- Align h265 bitstreams correctly (fc0543889b70eb0a151084d6a117e464cbeaaca0)
- Improve error message when using self-signed certs (211dbcded77dc6fd0d97f19a415ca4b286327fb9)
- Handle differing width/height in encode granularity (6b4b2dac3473d3631da6daa31fd09dc1bd3e2059)
- Update the maximum message size to reflect the protocol docs (c517624d3683b7ad1e37fc7ea6a18d86c09ccb75)
- Remove unecessary casts (d28b0b4335eb3e220b004421395e1f7d1d874939)
- Warn when no hardware encoder is available (bef772948bbb7ff04788016fe74a84eefa7dee8c)
- Bail early on mesa 24.2 (17758e3269ba661541ee2e94616606f2d935c626)
## [mmserver-v0.5.4] - 2024-11-18
### Bugfixes
- Handle missing /sys/devices/virtual/input (8f316fe41c41101ae18156a41abe2e9ba1e3497f)
- Lock pointer based on pointer focus (4ce202d3bd9cb764c0586cdc83e890843c3c04d7)
- Correctly handle an edge case with pointer locks (7c3428932651a372c69b25d1f77dc973746273a9)
## [mmserver-v0.5.3] - 2024-10-24
### Bugfixes
- Be consistent in xwayland socket naming (f6f6db3ab8b61e7af7684f14202d2b203b7e7760)
- Never use a 0 audio stream_seq (632bcb1f7c79d35701f31a29d2dbe659ab411e3c)
- Use the attachment coordinate space (57a59f478a6e4e248490b04b8c1ab42d2b1ae115)
- Don't close streams while partial writes are pending (0add85078734a27e121dda97293f0e48d8ebd214)
## [mmclient-v0.4.1] - 2024-10-24
### Bugfixes
- Handle video/audio stream seq more intelligently (4bab3902d1e7d88c7222ed6ef404190c512b1940)
- Make the overlay work again (0b1579bf68b2cd31611ca10a735061ef58e64604)
- Use the attachment coordinate space (57a59f478a6e4e248490b04b8c1ab42d2b1ae115)
- Don't close streams while partial writes are pending (0add85078734a27e121dda97293f0e48d8ebd214)
- Send relative pointer motion again (7fced702ebe37de5b2f96e46091c6b862806f757)
## [mmserver-v0.5.2] - 2024-10-19
### Bugfixes
- Use getgid if we want the group ID (6a9c71d25d58ff6b5bc4564b99230d76a6599f0e)
- Use _exit instead of exit or abort (c33a7b8989121706e0286af5efcdd8b5cf1291f1)
- Pass locale environment variables through to child apps (8022fd1bdb8e64918e15f38b2b4197361841f9d5)
## [mmserver-v0.5.1] - 2024-10-18
### Bugfixes
- Correctly emulate input nodes in udevfs (3fec928dcb5d7d5054d6ca7821864bae74559b9b)
- Increase the ready timeout (df5ba10642c5ec18064a67f8279d40d3b12baa76)
- Stub wl_data_device_manager (af1853aaf34c373617b78ddbfbde2d37a977d3df)
- Don't discard buffers when resending frame callbacks (3b9ce4164bb617ce7e0fd0840bad74fd281fda99)
- Organize bug report files slightly better (1806d3eea0e33c124f58d413fc3843e288cc0b0e)
## [mmclient-v0.4.0] - 2024-10-18
### New Features
- Plumb controller input through to the server (990f48cdac4181e69ac3cb5dd1473fe16fca3390)
- Allow specifying 'permanent' gamepads for a session (1d5b7f0a38017e0589c928a9acb6a10075bfac52)
- Refactor out most of mmclient into a UniFFI rust lib (e8097e594b72a336ace6ef5fe7247304a18dd364)
- List applications the server can launch (5d042be0f51095e06bbf68cdc3d3e40523c3e5ad)
- Add a logging interface (b961041ce28b7da961f193b17cd03f4e36c14ea7)
### Bugfixes
- Remove unecessary clone (87c95e63f6c6ce2f63207f96da839408f4617785)
- Rename Gamepad* enums to reduce the possibility of collision (5fd2241beff203c5c09089456e9326102213c2c2)
- Prevent a reattaching doom loop (dfa5d75e8daefa3dc15468145f55a5d06e7cd6e1)
- Correctly invert joystick direction (a60eb398b5f1dd13e1ac660f856a03857decad5b)
- Round off window height (d4227e772a7d6c8d30919b1e08876ee4a2e55802)
- Handle gamepad connected events correctly (aed00821a8ce3add26ef3ff2226b26e0752c1971)
- Increase the ready timeout (df5ba10642c5ec18064a67f8279d40d3b12baa76)
## [mmserver-v0.5.0] - 2024-10-15
### New Features
- Plumb controller input through to the server (990f48cdac4181e69ac3cb5dd1473fe16fca3390)
- Allow specifying 'permanent' gamepads for a session (1d5b7f0a38017e0589c928a9acb6a10075bfac52)
- Add support for native linux containerization (a37b0db8c5006e4c7b02cc98e506cd68a6ac2aa1)
- Basic gamepad support (f0eceab777fd38cb085e0f5120fe54ab2a71d362)
- List applications the server can launch (5d042be0f51095e06bbf68cdc3d3e40523c3e5ad)
### Bugfixes
- Remove a bunch of dead code (b5e88bbe9e472866d9ddd5316a7a8187d7676778)
- Add description field to application configs (d786828a87ce2c5ed18f373e3be06a1808ad5c42)
- Include more context when reading config files (d39aaf46c09d2c6d4525dfb3b452374cd1476b9d)
- Require app names to start with a letter (4182a506ea3a15809c42010ef88da1aeac12278d)
- Handle unknown message types more gracefully (2978f9b2d41e4916f7a18905586466bb66e92c35)
- Add application name to session spans (eccca93fd50530d7d658e8a69bb22ef1b689b5a4)
- Sleep the compositor if no client is attached (e03d8f2914867cc733fa4b44f78f00f7f89ea361)
- Make reattaching slightly more robust (10cfede5b4ef625f9961b3582ac7dab33cba6dd7)
- If using layers > 0, pass that many rate control layers (3a201510794deaebf262a81e8b02e8a3d9359cfd)
- Get hierarchical coding working on H265 (7b63cc694b28eb7fd1e9155a182e5446b80ef998)
- Add some preflight checks at startup (91e00002073a1c07af73fb5a7f1e27a5779d66b3)
- Improve shutdown behavior (5e77d7719313c2c6d53fa3335aec06840a9fe92a)
- Use putenv instead of Command::env (0a832c0f606a9d130eeca0bcb334dc6c5d65e169)
- Remove unshare as a dependency (e5c4575e3cacc9d00656cda7af114a0eb471777c)
## [mmserver-v0.4.1] - 2024-08-16
### Bugfixes
- Time out if the compositor doesn't accept an attachment in a reasonable timeframe (c1d6c6ca82fe3ff5ffcbf204c7f90e149b82f0ae)
- Explicitly close QUIC streams when a worker finishes (a4b0c18e4af7455dcde689b241e4fe2737e50f57)
- Never use 0 as a stream_seq (8fc95e4ef0d4a01d9c1809860a633c7417913115)
- Raise the attachment timeout to account for slow clients (6b60df3e7625da72157b5a6ae8479e9e05469c71)
- Set a default for video_profile (b4f2e01548ad0d374b4fc816f6a2a5c7c11f1751)
- Correctly send vertical scroll events (6a25863b00f049d354dda5f598a3f507db653285)
- Change order of press/release when simulating repeat (6df3f5cea5f8e6b2e2634f1307b2c4ee054ed638)
## [mmserver-v0.4.0] - 2024-08-02
### New Features
- Rewrite compositor from scratch (945a7793abbbc377f8c9ad1a852715203a16b097)
- Allow attachments to be configured for HDR10 output (0c4b85af422378881f550f61882439b1a4abade1)
- Support streaming in HDR (713dbbdce931e0ba98cc51bf144a2fe26dd9e2a1)
### Bugfixes
- Improve compositor error messages with s/client/app (e5b24afe2ccd8ce77f74a5732a2e02f723256cda)
## [mmclient-v0.3.0] - 2024-08-02
### New Features
- Allow attachments to be configured for HDR10 output (0c4b85af422378881f550f61882439b1a4abade1)
- Support playing HDR streams (12ef76930f729af0331bb83c3ceadb110bf22a6f)
### Bugfixes
- Make --detach the default (7ca5ee3ea03bcc19f754c1542675be360e3216af)
- Take name or id for --kill (7a1f8c1483bd43c292e5ec8189535b0e59fc453c)
- Move the cursor before locking it (2a5cc571f868c7ade0c9798b41e96ee21209de4d)
- Calculate RTT correctly (4762c1ab0594897949e4ce81a7897fab30d9c7fe)
- Make sure session width/height are even (5a344ade0e3cd62c1c8e0f4b99d6be8dee7b513f)
- Handle ConnectionClosed (953b9d4398ccca75b4108da0c31589c56747ff70)
- Ensure --ui-scale overrides environment scaling (776b4dc2c5462a05c8520e769361f3136d5bcc6a)
- Swap order of lock/warp when locking cursor on not-mac (525622b29d46fc8e659d0e3c37cf920faf587866)
## [mmclient-v0.2.0] - 2024-05-08
### New Features
- Cursor locking and relative motion (e11dfec7e42802a528ac8c8b4629044e6d6b1c3f)
- Add --preset, for setting quality/bandwidth usage dynamically (6c590efaab02e31aae8413b683e8f8d228256b3b)
### Bugfixes
- Don't sync every frame (5a7f1cfe11e6684e11bd618e2f1adf4d043640f5)
## [mmserver-v0.3.0] - 2024-05-08
### New Features
- Cursor locking and relative motion (e11dfec7e42802a528ac8c8b4629044e6d6b1c3f)
- Add --preset, for setting quality/bandwidth usage dynamically (6c590efaab02e31aae8413b683e8f8d228256b3b)
### Bugfixes
- Remove debugging code (152a1714ca950256f136757f47b7b2cf587d6880)
- Un-transpose min and max QP (0570a6470b934e62dd4c9dcc42467a6db1a311e4)
- Correctly set max QP on lower presets (b3f73533bb896c93d4a1d4e5c8efc336e329042c)
- Prevent a segfault on nvidia (8b331b5de98a50dd3c59671a2dbfe37b966b95b9)
- Re-send cursor status when reattaching (eba4a368c33a5bcd1cdf27a8b791f31ff466bb29)
## [mmclient-v0.1.2] - 2024-05-05
### Bugfixes
- Actually sync video and audio (4822bda39b4a5f07ed74e4fd76d5b080ea1c2078)
- Tune verbosity of conn message (e9f0d18da517e1c7f1ab34d9c154b8ba70573f2e)
- Fix typo in conn init (d8dd70b25952e1d1155bf8e6930d2304ca51c79e)
## [mmserver-v0.2.0] - 2024-05-05
### New Features
- Add enable_datagrams, off by default (e1dc976ee3228b006b874e077cd2c6cf7f784927)
- Add glxinfo and eglinfo output to --bug-report (696464d9b980f1664e2b9dcce9e6f6dde83407f2)
### Bugfixes
- Don't panic on dmabuf cursors (9f87ce7d99289ba31ad11b5d1796b992fd21c796)
- Print version after initializing logging (f708ad2d8e5ddc9fb17ac023fef8f81706c31be7)
- Handle full send queues more gracefully (face8776acea8c22e4d83b62c54ece5682f95cee)
- Manually enable radv encode (26ba3f93f3da29921f9754181738f2087284a164)
- Correctly expose a vulkan fn (2c627c94569050d0b53429204e8153119d268560)
- Write xwayland logs to the bug report dir (0ba97f5f3bd72caf7df815e341c4c4f0a807b094)
- Support older versions of xwayland with wl_drm (54c9724a476d023547fb1c2ccc5d74bc6eadc6a3)
- Kill hung clients (5179e6688a2bc8fcceded03c0d92e2a00c38fb99)
- Implement basic rate control (781c97e3efde247ef437ad2e19e8cdf57b6d216e)
- Log entire config (b588f198d13122869936b52c0690e980586a7f88)
- Garbage-collect partial writes (a095994de28ec31bd49a54c2d757493f41fc0c06)
## [mmclient-v0.1.1] - 2024-05-05
### Bugfixes
- Increase the default timeout when waiting for frames (a8aefcb295803d087349625a37e1fdef3f2ec9d7)
- Handle video frames sent over the attachment stream (c0ecfba8fd5f06a64ab2e3c5d02731938a41170b)
- Handle VideoChunk messages on the attachment stream (75f409d1b2c0685bf6e4413a44535798a7a53a71)
- Handle AudioChunk messages on the attachment stream (3a63b07149fd36308d72378c66b53c41574abb1e)
- Be more robust in the face of bad stream data (7c920b66451e615205cea7a8d229c068c340324c)
- Respect hidden cursors (003fe97034cbbd71a8845841cf9d26e592c27696)
================================================
FILE: LICENSES/BUSL-1.1.txt
================================================
Business Source License 1.1
Parameters
----------
Licensor: Colin Marc <hi@colinmarc.com>
Licensed Work: Magic Mirror
Additional Use Grant: You may make use of the Licensed Work, provided that
you may not use the Licensed Work for a Game Streaming
or Remote Desktop service.
A "Game Streaming or Remote Desktop service” is a
commercial offering that allows third parties (other than
your employees and contractors) to access the
functionality of the Licensed Work, thereby utilizing
graphics processing hardware owned or operated by you.
Change Date: 2029-05-01
Change License: MIT License
For information about alternative licensing arrangements for the Software,
please contact the Licensor at hi@colinmarc.com.
Notice
The Business Source License (this document, or the “License”) is not an Open
Source license. However, the Licensed Work will eventually be made available
under an Open Source License, as stated in this License.
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
“Business Source License” is a trademark of MariaDB Corporation Ab.
-----------------------------------------------------------------------------
Business Source License 1.1
Terms
The Licensor hereby grants you the right to copy, modify, create derivative
works, redistribute, and make non-production use of the Licensed Work. The
Licensor may make an Additional Use Grant, above, permitting limited
production use.
Effective on the Change Date, or the fourth anniversary of the first publicly
available distribution of a specific version of the Licensed Work under this
License, whichever comes first, the Licensor hereby grants you rights under
the terms of the Change License, and the rights granted in the paragraph
above terminate.
If your use of the Licensed Work does not comply with the requirements
currently in effect as described in this License, you must purchase a
commercial license from the Licensor, its affiliated entities, or authorized
resellers, or you must refrain from using the Licensed Work.
All copies of the original and modified Licensed Work, and derivative works
of the Licensed Work, are subject to this License. This License applies
separately for each version of the Licensed Work and the Change Date may vary
for each version of the Licensed Work released by Licensor.
You must conspicuously display this License on each original or modified copy
of the Licensed Work. If you receive the Licensed Work in original or
modified form from a third party, the terms and conditions set forth in this
License apply to your use of that work.
Any use of the Licensed Work in violation of this License will automatically
terminate your rights under this License for the current and all other
versions of the Licensed Work.
This License does not grant you any right in any trademark or logo of
Licensor or its affiliates (provided that you may use a trademark or logo of
Licensor as expressly required by this License).
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
TITLE.
MariaDB hereby grants you permission to use this License’s text to license
your works, and to refer to it using the trademark “Business Source License”,
as long as you comply with the Covenants of Licensor below.
Covenants of Licensor
In consideration of the right to use this License’s text and the “Business
Source License” name and trademark, Licensor covenants to MariaDB, and to all
other recipients of the licensed work to be provided by Licensor:
1. To specify as the Change License the GPL Version 2.0 or any later version,
or a license that is compatible with GPL Version 2.0 or a later version,
where “compatible” means that software provided under the Change License can
be included in a program with software provided under GPL Version 2.0 or a
later version. Licensor may specify additional Change Licenses without
limitation.
2. To either: (a) specify an additional grant of rights to use that does not
impose any additional restriction on the right granted in this License, as
the Additional Use Grant; or (b) insert the text “None”.
3. To specify a Change Date.
4. Not to modify this License in any other way.
================================================
FILE: LICENSES/MIT.txt
================================================
MIT License
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# Magic Mirror 🪞✨
[](https://github.com/colinmarc/magic-mirror/actions/workflows/tests.yaml)
[](https://discord.gg/v22G644DzS)
This is a game streaming and remote desktop tool for Linux hosts, featuring:
- **Headless multitenant rendering:** Streamed applications are run offscreen, isolated from the rest of the system and any display hardware.
- **No system dependencies:** The server is a single static binary, and there's no dependency on docker, pipewire, or any other systemwide setup.
- **Native linux containerization:** apps are isolated in rootless containers with the equivalent of unshare(1), using new Linux namespace features
- **High quality, tunable, 4k streaming:** See the [list of supported codecs](https://colinmarc.github.io/magic-mirror/setup/server/#hardware-software-encoding). 10-bit HDR support is in progress.
- **Very low latency:** No extra CPU-GPU copy when using hardware encode. Total latency is less than one frame.
- **Local cursor rendering:** Use the client-side cursor for minimal input lag.
- **Client support for macOS and Linux:** A [SwiftUI client](https://github.com/colinmarc/magic-mirror-swiftui/releases/latest) is available for macOS, with tvOS/iOS support coming soon.
> [!WARNING]
> Alpha software! Please submit any issues you encounter. Run the server with `--bug-report` to generate detailed logs and record videos to attach to your report.
### Quick Links
- [Documentation Book](https://colinmarc.github.io/magic-mirror)
- [Latest Server Release [mmserver-v0.8.4]](https://github.com/colinmarc/magic-mirror/releases/tag/mmserver-v0.8.4)
- [Latest CLI Client Release [mmclient-v0.7.0]](https://github.com/colinmarc/magic-mirror/releases/tag/mmclient-v0.7.0)
- [Latest macOS Client Release](https://github.com/colinmarc/magic-mirror-swiftui/releases/latest)
- [Discord](https://discord.gg/v22G644DzS)
================================================
FILE: auto-release.sh
================================================
#!/bin/sh -e
die() {
RED="\033[31m"
RESET="\033[0m"
echo -e "${RED}$1${RESET}"
exit 1
}
case $1 in
"client" | "server")
component=$1
;;
*)
die "invalid component: $1"
exit 1
;;
esac
if [ -n "$(git status --untracked-files=no --porcelain)" ]; then
die "working directory not clean; exiting"
exit 1
fi
branch="auto-bump-${component}"
git fetch -q origin "${branch}"
tag="$(git show -s --format=%s origin/${branch} | awk '{print $NF}')"
if [ -n "$(git tag | grep ${tag})" ]; then
die "tag exists"
fi
echo "bumping mm${component} to ${tag}..."
git cherry-pick -S "origin/${branch}"
echo "generating release notes..."
release_notes="$(git cliff -v -c .github/workflows/cliff.toml \
--tag-pattern "${component}" \
--include-path "mm-${component}*/**/*" \
--unreleased --tag ${tag})"
git tag ${tag} -a -m "${release_notes}" --cleanup=verbatim
git show ${tag}
================================================
FILE: docs/.gitignore
================================================
# autogenerated
content/reference
build/
public/
================================================
FILE: docs/config.toml
================================================
base_url = "https://colinmarc.github.io/magic-mirror"
theme = "anemone"
compile_sass = false
build_search_index = false
[markdown]
highlight_code = true
[extra]
twitter_card = false
header_nav = [
{ url = "https://colinmarc.github.io/magic-mirror", name_en = "/home/"},
{ url = "https://github.com/colinmarc/magic-mirror", name_en = "/github/"},
{ url = "https://discord.gg/v22G644DzS", name_en = "/discord/"},
]
================================================
FILE: docs/content/_index.md
================================================
+++
+++
# Magic Mirror 🪞✨
<picture>
<source srcset="header_dark.png" media="(prefers-color-scheme: dark)" />
<img src="header_light.png" />
</picture>
This page contains documentation for [Magic Mirror](https://github.com/colinmarc/magic-mirror),
an open-source game streaming and remote desktop tool for linux hosts.
### Download
These links always point to the latest release.
- 💾 [Server [mmserver-v0.8.4]](https://github.com/colinmarc/magic-mirror/releases/tag/mmserver-v0.8.4)
- 💾 [Command-Line Client [mmclient-v0.7.0]](https://github.com/colinmarc/magic-mirror/releases/tag/mmclient-v0.7.0)
- 💾 [macOS GUI Client](https://github.com/colinmarc/magic-mirror-swiftui/releases/latest)
### Setup Guides
Start here to get things up and running.
- ⚙️ [Server Setup](@/setup/server.md)
- ⚙️ [Client Setup](@/setup/client.md)
<!-- - ⚙️ [Running on a Cloud VPS](./setup/vps.md) -->
<!-- - ⚙️ [Troubleshooting and Known Issues](@/setup/troubleshooting.md) -->
### Reference
Autogenerated from the code.
- 📖 [Configuration Reference](@/reference/config.md)
- 📖 [Protocol Reference](@/reference/protocol.md)
- 📖 [Rustdoc for `mm-protocol`](./doc/mm_protocol)
- 📖 [Rustdoc for `mm-client-common`](./doc/mm_client_common)
### Contact
Get help, report issues, make friends.
- ⁉️ [Issue Tracker](https://github.com/colinmarc/magic-mirror/issues)
- 💬 [Discord Chat](https://discord.gg/v22G644DzS)
================================================
FILE: docs/content/setup/client.md
================================================
+++
title = "Client Setup"
[extra]
toc = true
+++
## macOS GUI Client
The native macOS client can be downloaded from [the releases page](https://github.com/colinmarc/magic-mirror-swiftui/releases/latest).
It should work out of the box on ARM and Intel Macs running macOS 10.14 or
later.
## Installing the commandline client
There is also a cross-platform commandline client, `mmclient`. You can download
it [here](https://github.com/colinmarc/magic-mirror/releases/tag/mmclient-v0.7.0).
The commandline client requires `ffmpeg` 6.0 or later to be installed on the
system. It also requires up-to-date Vulkan drivers.
## Building mmclient
The following are required to build the client and its dependencies:
```
rust (MSRV 1.77.2)
nasm
cmake
protoc
libxkbcommon (linux only)
libwayland-client (linux only)
alsa (linux only)
ffmpeg 6.x
```
Besides Rust itself, the following command will install everything on ubuntu:
```
apt install \
nasm cmake protobuf-compiler libxkbcommon-dev libwayland-dev libasound2-dev \
ffmpeg libavutil-dev libavformat-dev libavdevice-dev libavfilter-dev
```
Or using homebrew on macOS:
```
brew install nasm cmake ffmpeg@6 protobuf
```
================================================
FILE: docs/content/setup/server.md
================================================
+++
title = "Server Setup"
[extra]
toc = true
+++
## Quickstart
First, grab [the latest server release](https://github.com/colinmarc/magic-mirror/releases/tag/mmserver-v0.8.4) and untar it somewhere:
```sh
curl -fsSL "https://github.com/colinmarc/magic-mirror/releases/download/mmserver-v0.8.4/mmserver-v0.8.4-linux-amd64.tar.gz" \
| tar zxv
cd mmserver-v0.8.4
```
Then, create a [configuration file](@/reference/config.md) with at least one application definition:
```toml
# mmserver.toml
[apps.steam-gamepadui]
command = ["steam", "-gamepadui"]
xwayland = true
```
Then you can start the server like so:
```
$ ./mmserver -C config.toml
2024-12-09T16:57:30.989261Z INFO mmserver: listening on [::1]:9599
```
You can also create a configuration directory, and add a file (json or toml) for each application:
```sh
mkdir apps.d
echo 'command = ["steam", "-gamepadui"]' > apps.d/steam.toml
./mmserver -i apps.d
```
## Connectivity
By default, mmserver only listens on `localhost`, which is not terribly
useful. There are a few different options to configure which socket address the
server listens for connections on.
The easiest is to bind to a local IP, or use a VPN like wireguard or tailscale:
```toml
# config.toml
[server]
bind = "192.168.1.37:9599"
```
Or from the command line:
```sh
mmserver --bind $(tailscale ip -4):9599
```
If you'd like to stream on a public IP, or on all interfaces (with `0.0.0.0`),
mmserver requires that you set up a TLS certificate and key:
```toml
# config.toml
[server]
tls_cert = "/path/to/tls.key"
tls_key = "/path/to/tls.cert"
```
Generating such certificates and adding them to the client is out of scope for
this guide. Note that while all Magic Mirror traffic is encrypted with TLS
(whether you supply certificates or not), no _authentication_ is performed on
incoming connections.
Finally, you can also use `--bind-systemd` or `bind_systemd = true` to bind to a
[systemd socket](https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html).
## System Requirements
The following is required to run the server:
- Linux 6.x (for Ubuntu, this means Mantic or Noble)
- (For AMD/Intel cards) Mesa 24.3.x or later
- (For NVIDIA cards) [Vulkan drivers](https://developer.nvidia.com/vulkan-driver) version 550 or later
- XWayland (for X11 apps)
## Hardware encoding
Magic Mirror uses hardware-based video compression codecs to stream the game over the wire.
To see if your GPU supports video encoding, see the following matrix for your vendor:
- [AMD](https://en.wikipedia.org/wiki/Unified_Video_Decoder#Format_support)
- [NVIDIA](https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new)
| Codec | AMD | NVIDIA | Intel |
| ----- | :-: | :----: | :---: |
| H.264 | ✅ | ✅ | ❔ |
| H.265 | ✅ | ✅ | ❔ |
| AV1 | ❌ | ❌ | ❌ |
## Building `mmserver` from source
The following are required to build the server and its dependencies:
```
rust (MSRV 1.77.2)
nasm
cmake
protoc
libxkbcommon
```
Besides Rust itself, the following command will install everything on ubuntu:
```
apt install nasm cmake protobuf-compiler libxkbcommon-dev
```
Then you should be good to go:
```
cd mm-server
cargo build --bin mmserver [--release]
```
================================================
FILE: docs/templates/footer.html
================================================
================================================
FILE: mm-client/Cargo.toml
================================================
# Copyright 2024 Colin Marc <hi@colinmarc.com>
#
# SPDX-License-Identifier: MIT
[package]
name = "mm-client"
version = "0.7.0"
edition = "2021"
[[bin]]
name = "mmclient"
path = "src/bin/mmclient.rs"
[[bin]]
name = "latency-test"
path = "src/bin/latency-test.rs"
[dependencies]
anyhow = "1"
ash = "0.38"
ash-window = "0.13.0"
bytes = "1"
clap = { version = "4", features = ["derive"] }
cpal = "0.15"
crossbeam-channel = "0.5"
cstr = "0.2"
ffmpeg-next = "7"
ffmpeg-sys-next = "7"
font-kit = "0.11"
gilrs = "0.10"
glam = "0.26"
histo = "1"
humantime = "2"
image = { version = "0.25", default-features = false, features = ["png"] }
imgui = { version = "0.12.0", features = ["tables-api"] }
imgui-sys = "0.12.0"
imgui-winit-support = "0.13.0"
imgui-rs-vulkan-renderer = { version = "1.16.0", features = ["dynamic-rendering"] }
lazy_static = "1"
oneshot = { version = "0.1", default-features = false, features = ["std"] }
opus = "0.3"
pollster = "0.3"
rand = "0.8"
raw-window-handle = "0.5"
simple_moving_average = "1"
tabwriter = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["time", "env-filter"] }
tracy-client = { version = "0.17", default-features = false }
tracing-tracy = { version = "0.11", default-features = false }
[dependencies.mm-protocol]
path = "../mm-protocol"
[dependencies.mm-client-common]
path = "../mm-client-common"
[dependencies.dasp]
version = "0.11"
features = ["slice", "signal", "interpolate", "interpolate-linear"]
[dependencies.winit]
version = "0.30"
default-features = false
features = ["wayland", "x11", "rwh_06"]
[target.'cfg(target_os = "macos")'.dependencies]
ash-molten = { version = "0.18", optional = true }
[build-dependencies.slang]
git = "https://github.com/colinmarc/slang-rs"
rev = "075daa4faa8d1ab6d7bfbb5293812b087a527207"
# Uses SLANG_DIR if set, otherwise builds slang from source
features = ["from-source"]
[features]
default = []
moltenvk_static = ["dep:ash-molten"]
tracy = ["tracy-client/enable"]
================================================
FILE: mm-client/build.rs
================================================
// Copyright 2024 Colin Marc <hi@colinmarc.com>
//
// SPDX-License-Identifier: MIT
// extern crate shaderc;
use std::path::PathBuf;
extern crate slang;
fn main() {
let mut session = slang::GlobalSession::new();
let out_dir = std::env::var("OUT_DIR").map(PathBuf::from).unwrap();
compile_shader(
&mut session,
"src/render.slang",
out_dir.join("shaders/frag.spv").to_str().unwrap(),
"frag",
slang::Stage::Fragment,
);
compile_shader(
&mut session,
"src/render.slang",
out_dir.join("shaders/vert.spv").to_str().unwrap(),
"vert",
slang::Stage::Vertex,
);
}
fn compile_shader(
session: &mut slang::GlobalSession,
in_path: &str,
out_path: &str,
entry_point: &str,
stage: slang::Stage,
) {
std::fs::create_dir_all(PathBuf::from(out_path).parent().unwrap())
.expect("failed to create output directory");
let mut compile_request = session.create_compile_request();
compile_request
.add_search_path("../shader-common")
.set_codegen_target(slang::CompileTarget::Spirv)
.set_optimization_level(slang::OptimizationLevel::Maximal)
.set_target_profile(session.find_profile("glsl_460"));
let entry_point = compile_request
.add_translation_unit(slang::SourceLanguage::Slang, None)
.add_source_file(in_path)
.add_entry_point(entry_point, stage);
let shader_bytecode = compile_request
.compile()
.expect("Shader compilation failed.");
std::fs::write(out_path, shader_bytecode.get_entry_point_code(entry_point))
.expect("failed to write shader bytecode to file");
println!("cargo::rerun-if-changed={}", in_path);
}
================================================
FILE: mm-client/src/audio/buffer.rs
================================================
// Copyright 2024 Colin Marc <hi@colinmarc.com>
//
// SPDX-License-Identifier: MIT
use std::collections::VecDeque;
pub struct PlaybackBuffer<F>
where
F: dasp::Frame,
{
/// A queue of audio frames.
samples: VecDeque<F>,
/// The PTS and packet length (in frames) for each packet. Kept in sync with
/// `samples`.
pts: VecDeque<(u64, usize)>,
}
impl<F> PlaybackBuffer<F>
where
F: dasp::Frame,
{
pub fn new() -> Self {
PlaybackBuffer {
samples: VecDeque::new(),
pts: VecDeque::new(),
}
}
/// Returns the number of frames in the buffer.
pub fn len(&self) -> usize {
self.samples.len()
}
/// Adds frames to the back of the buffer.
pub fn buffer(&mut self, pts: u64, frames: &[F]) {
self.pts.push_back((pts, frames.len()));
self.samples.extend(frames.iter());
}
/// Returns the PTS of the head packet in the audio buffer.
pub fn current_pts(&self) -> u64 {
self.pts
.front()
.expect("current_pts called before buffer")
.0
}
/// Returns an iterator that pops frames from the front of the buffer.
pub fn drain(&mut self) -> Draining<F> {
Draining { buffer: self }
}
/// Discards the first N frames from the buffer.
pub fn skip(&mut self, frames: usize) {
self.samples.drain(..frames);
let mut remaining = frames;
loop {
let (_, len) = self.pts.front_mut().expect("skip called before buffer");
if *len <= remaining {
remaining -= *len;
self.pts.pop_front();
} else {
*len -= remaining;
break;
}
}
}
}
pub struct Draining<'a, F>
where
F: dasp::Frame,
{
buffer: &'a mut PlaybackBuffer<F>,
}
impl<F> Iterator for Draining<'_, F>
where
F: dasp::Frame,
{
type Item = F;
fn next(&mut self) -> Option<Self::Item> {
let frame = self.buffer.samples.pop_front()?;
if let Some((_, remaining)) = self.buffer.pts.front_mut() {
*remaining -= 1;
if *remaining == 0 {
self.buffer.pts.pop_front();
}
}
Some(frame)
}
}
================================================
FILE: mm-client/src/audio.rs
================================================
// Copyright 2024 Colin Marc <hi@colinmarc.com>
//
// SPDX-License-Identifier: MIT
mod buffer;
use std::{
sync::{Arc, Mutex},
time,
};
use anyhow::{bail, Context as _};
use buffer::PlaybackBuffer;
use cpal::traits::{DeviceTrait as _, HostTrait as _, StreamTrait};
use crossbeam_channel as crossbeam;
use dasp::Signal;
use mm_client_common as client;
use tracing::{debug, error, info, trace};
trait DecodePacket<T> {
fn decode(&mut self, input: &[u8], output: &mut [T]) -> anyhow::Result<usize>;
}
impl DecodePacket<f32> for opus::Decoder {
fn decode(&mut self, packet: &[u8], output: &mut [f32]) -> anyhow::Result<usize> {
let len = self.decode_float(packet, output, false)?;
Ok(len)
}
}
impl DecodePacket<i16> for opus::Decoder {
fn decode(&mut self, packet: &[u8], output: &mut [i16]) -> anyhow::Result<usize> {
let len = self.decode(packet, output, false)?;
Ok(len)
}
}
// This is a trait object so we can erase the sample/frame generic type.
trait StreamWrapper {
#[allow(clippy::new_ret_no_self)]
fn new(
device: &cpal::Device,
conf: cpal::StreamConfig,
) -> anyhow::Result<(Box<dyn StreamWrapper>, cpal::Stream)>
where
Self: Sized;
fn sync(&mut self, pts: u64);
fn send_packet(&mut self, packet: Arc<client::Packet>) -> anyhow::Result<()>;
}
struct StreamInner<F: dasp::Frame> {
sync_point: Arc<Mutex<Option<(u64, time::Instant)>>>,
_buffer: Arc<Mutex<PlaybackBuffer<F>>>,
thread_handle: Option<std::thread::JoinHandle<anyhow::Result<()>>>,
undecoded_tx: Option<crossbeam::Sender<Arc<client::Packet>>>,
}
impl<F> StreamWrapper for StreamInner<F>
where
F: dasp::Frame + Send + 'static,
F::Sample: cpal::SizedSample + dasp::sample::Duplex<f64> + Default,
opus::Decoder: DecodePacket<F::Sample>,
for<'a> &'a [F::Sample]: dasp::slice::ToFrameSlice<'a, F>,
{
fn new(
device: &cpal::Device,
conf: cpal::StreamConfig,
) -> anyhow::Result<(Box<dyn StreamWrapper>, cpal::Stream)> {
let sample_rate = conf.sample_rate.0;
let mut decoder = {
let ch = match F::CHANNELS {
1 => opus::Channels::Mono,
2 => opus::Channels::Stereo,
_ => bail!("unsupported number of channels: {}", F::CHANNELS),
};
opus::Decoder::new(sample_rate, ch)?
};
let buffer = Arc::new(Mutex::new(PlaybackBuffer::new()));
let (undecoded_tx, undecoded_recv) = crossbeam::unbounded::<Arc<client::Packet>>();
// Spawn a thread to eagerly decode packets.
let buffer_clone = buffer.clone();
let thread_handle = std::thread::Builder::new()
.name("audio decode".into())
.spawn(move || {
// Handles up to 100ms of decoded audio.
let mut output =
vec![Default::default(); (sample_rate * F::CHANNELS as u32 / 10) as usize];
loop {
let packet = match undecoded_recv.recv() {
Ok(packet) => packet,
Err(crossbeam::RecvError) => return Ok(()),
};
let pts = packet.pts();
let packet = packet.data();
match DecodePacket::decode(&mut decoder, &packet, &mut output) {
Ok(len) => {
if len == 0 {
continue;
}
let frames =
dasp::slice::to_frame_slice(&output[..(len * F::CHANNELS)])
.expect("invalid sample count");
let mut guard = buffer_clone.lock().unwrap();
guard.buffer(pts, frames);
#[cfg(feature = "tracy")]
{
let len_us = guard.len() as f64 / sample_rate as f64 * 1_000_000.0;
tracy_client::plot!("audio buffer (μs)", len_us);
}
}
Err(e) => {
error!("opus decode error: {}", e);
continue;
}
};
}
})?;
// The current PTS of the video stream, which we want to sync to.
let sync_point = Arc::new(Mutex::new(None));
let sync_point_clone = sync_point.clone();
let buffer_clone = buffer.clone();
let stream = device.build_output_stream(
&conf,
move |out, _info| {
let mut buffer = buffer_clone.lock().unwrap();
let frames_needed = out.len() / F::CHANNELS;
let frames_remaining = buffer.len(); // In frames.
let frames_per_ms = sample_rate / 1000;
if frames_remaining < frames_needed {
out.fill(Default::default());
trace!("audio buffer underrun");
return;
}
let sync_point: Option<(u64, time::Instant)> =
sync_point_clone.lock().unwrap().as_ref().copied();
if let Some((pts, ts)) = sync_point {
let target_pts = pts + ts.elapsed().as_millis() as u64;
let pts = buffer.current_pts();
let delay = target_pts as i64 - pts as i64;
#[cfg(feature = "tracy")]
tracy_client::plot!("audio drift (ms)", delay as f64);
// Outside these bounds, skip or play silence in order to sync.
const TOO_EARLY: i64 = 20;
const TOO_LATE: i64 = 60;
if delay < TOO_EARLY {
// Play silence until the video catches up.
out.fill(Default::default());
return;
}
if delay > TOO_LATE {
// Skip ahead.
let skip = std::cmp::min(
(delay * frames_per_ms as i64) as usize,
frames_remaining.saturating_sub(frames_needed * 2),
);
buffer.skip(skip);
}
}
let mut signal = dasp::signal::from_iter(buffer.drain()).into_interleaved_samples();
for sample in out.iter_mut() {
*sample = signal.next_sample();
}
#[cfg(feature = "tracy")]
{
let len_us = buffer.len() as f64 / sample_rate as f64 * 1_000_000.0;
tracy_client::plot!("audio buffer (μs)", len_us);
}
},
move |err| {
error!("audio playback error: {}", err);
},
None,
)?;
Ok((
Box::new(Self {
// decoded_packets,
_buffer: buffer,
sync_point,
thread_handle: Some(thread_handle),
undecoded_tx: Some(undecoded_tx),
}),
stream,
))
}
fn sync(&mut self, pts: u64) {
*self.sync_point.lock().unwrap() = Some((pts, time::Instant::now()));
}
fn send_packet(&mut self, packet: Arc<client::Packet>) -> anyhow::Result<()> {
self.undecoded_tx
.as_ref()
.unwrap()
.send(packet)
.map_err(|_| anyhow::anyhow!("audio decode thread died"))?;
Ok(())
}
}
impl<T: dasp::Frame> Drop for StreamInner<T> {
fn drop(&mut self) {
let _ = self.undecoded_tx.take();
if let Some(handle) = self.thread_handle.take() {
match handle.join() {
Ok(Ok(())) => (),
Ok(Err(e)) => {
error!("audio decode thread error: {}", e);
}
Err(_) => {
error!("audio decode thread panicked");
}
}
}
}
}
pub struct AudioStream {
device: cpal::Device,
stream: Option<cpal::Stream>,
inner: Option<Box<dyn StreamWrapper>>,
stream_waiting: bool,
stream_seq: u64,
packet_count: u64,
}
impl AudioStream {
pub fn new() -> anyhow::Result<Self> {
let device = cpal::default_host()
.default_output_device()
.context("unable to find default audio output device")?;
info!("using audio output device: {}", device.name()?);
Ok(Self {
device,
stream: None,
inner: None,
stream_waiting: true,
packet_count: 0,
stream_seq: 0,
})
}
pub fn sync(&mut self, pts: u64) {
if let Some(inner) = &mut self.inner {
inner.sync(pts);
}
}
pub fn reset(
&mut self,
stream_seq: u64,
sample_rate: u32,
channels: u32,
) -> anyhow::Result<()> {
debug!(
stream_seq,
sample_rate, channels, "starting or restarting audio stream"
);
let (format, conf) = select_conf(&self.device, sample_rate, channels)?;
let (inner, stream) = match (format, channels) {
(cpal::SampleFormat::F32, 1) => StreamInner::<[f32; 1]>::new(&self.device, conf),
(cpal::SampleFormat::F32, 2) => StreamInner::<[f32; 2]>::new(&self.device, conf),
(cpal::SampleFormat::I16, 1) => StreamInner::<[i16; 1]>::new(&self.device, conf),
(cpal::SampleFormat::I16, 2) => StreamInner::<[i16; 2]>::new(&self.device, conf),
_ => bail!("unsupported sample rate / format"),
}?;
self.stream_seq = stream_seq;
self.stream = Some(stream);
self.inner = Some(inner);
self.stream_waiting = true;
self.packet_count = 0;
Ok(())
}
pub fn recv_packet(&mut self, packet: Arc<client::Packet>) -> anyhow::Result<()> {
if let Some(inner) = &mut self.inner {
trace!(
stream_seq = packet.stream_seq(),
seq = packet.seq(),
pts = packet.pts(),
len = packet.len(),
"received full audio packet"
);
self.packet_count += 1;
inner.send_packet(packet)?;
}
if self.stream.is_some() && self.stream_waiting && self.packet_count > 2 {
self.stream_waiting = false;
self.stream.as_ref().unwrap().play()?;
}
Ok(())
}
}
fn select_conf(
device: &cpal::Device,
sample_rate: u32,
channels: u32,
) -> anyhow::Result<(cpal::SampleFormat, cpal::StreamConfig)> {
let mut confs = device
.supported_output_configs()
.context("unable to query supported audio playback formats")?;
let valid = |format: cpal::SampleFormat| {
move |conf: &cpal::SupportedStreamConfigRange| {
conf.sample_format() == format
&& conf.min_sample_rate() <= cpal::SampleRate(sample_rate)
&& conf.max_sample_rate() >= cpal::SampleRate(sample_rate)
&& conf.channels() == channels as u16
}
};
if let Some(conf_range) = confs
.find(valid(cpal::SampleFormat::F32))
.or_else(|| confs.find(valid(cpal::SampleFormat::I16)))
{
let sample_format = conf_range.sample_format();
let buffer_size = match conf_range.buffer_size() {
cpal::SupportedBufferSize::Unknown => cpal::BufferSize::Default,
cpal::SupportedBufferSize::Range { min, .. } => {
cpal::BufferSize::Fixed(std::cmp::max(*min, sample_rate / 100))
}
};
let mut conf =
cpal::StreamConfig::from(conf_range.with_sample_rate(cpal::SampleRate(sample_rate)));
conf.buffer_size = buffer_size;
return Ok((sample_format, conf));
}
bail!("no valid audio output configuration found");
}
================================================
FILE: mm-client/src/bin/latency-test.rs
================================================
// Copyright 2024 Colin Marc <hi@colinmarc.com>
//
// SPDX-License-Identifier: MIT
use std::{sync::Arc, time};
use anyhow::{bail, Context as _};
use ash::vk;
use clap::Parser;
use mm_client::{
delegate::{AttachmentEvent, AttachmentProxy},
video::*,
vulkan::*,
};
use mm_client_common as client;
use mm_protocol as protocol;
use pollster::FutureExt as _;
use tracing::{debug, error, warn};
use winit::event_loop::EventLoop;
const APP_DIMENSION: u32 = 256;
const DEFAULT_TIMEOUT: time::Duration = time::Duration::from_secs(1);
#[derive(Debug, Parser)]
#[command(name = "mmclient")]
#[command(about = "The Magic Mirror reference client", long_about = None)]
struct Cli {
/// The server to connect to.
#[arg(value_name = "HOST[:PORT]")]
host: String,
/// The codec to use. Defaults to h265.
#[arg(long)]
codec: Option<String>,
/// The framerate to use. Defaults to 60.
#[arg(long)]
framerate: Option<u32>,
/// The number of tests to run. Defaults to 256.
#[arg(short('n'), long)]
samples: Option<usize>,
}
pub enum AppEvent {
VideoStreamReady(Arc<VkImage>, VideoStreamParams),
VideoFrameAvailable,
AttachmentEvent(AttachmentEvent),
}
impl std::fmt::Debug for AppEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use AppEvent::*;
match self {
VideoStreamReady(_, params) => write!(f, "VideoStreamReady({params:?})"),
VideoFrameAvailable => write!(f, "VideoFrameAvailable"),
AttachmentEvent(ev) => std::fmt::Debug::fmt(ev, f),
}
}
}
impl From<AttachmentEvent> for AppEvent {
fn from(event: AttachmentEvent) -> Self {
Self::AttachmentEvent(event)
}
}
impl From<VideoStreamEvent> for AppEvent {
fn from(event: VideoStreamEvent) -> Self {
use VideoStreamEvent::*;
match event {
VideoStreamReady(tex, params) => AppEvent::VideoStreamReady(tex, params),
VideoFrameAvailable => AppEvent::VideoFrameAvailable,
}
}
}
struct App {
client: client::Client,
args: Cli,
proxy: winit::event_loop::EventLoopProxy<AppEvent>,
win: Option<LatencyTest>,
}
struct LatencyTest {
attachment: client::Attachment,
session_id: u64,
stream: VideoStream<AppEvent>,
video_texture: Option<Arc<VkImage>>,
frames_recvd: usize,
copy_cb: vk::CommandBuffer,
copy_fence: vk::Fence,
copy_buffer: VkHostBuffer,
next_block: usize,
block_started: time::Instant,
num_tests: usize,
histogram: histo::Histogram,
first_frame_recvd: Option<time::Instant>,
total_video_bytes: usize,
vk: Arc<VkContext>,
}
fn main() -> anyhow::Result<()> {
init_logging()?;
let args = Cli::parse();
// Invisible window.
let event_loop: EventLoop<AppEvent> = EventLoop::with_user_event().build()?;
let proxy = event_loop.create_proxy();
let client = client::Client::new(&args.host, "latency-test", time::Duration::from_secs(1))
.block_on()
.context("failed to connect")?;
let mut app = App {
client,
args,
proxy,
win: None,
};
event_loop.run_app(&mut app)?;
if let Some(win) = app.win.take() {
drop(win.stream);
unsafe {
win.vk
.device
.free_command_buffers(win.vk.present_queue.command_pool, &[win.copy_cb]);
win.vk.device.destroy_fence(win.copy_fence, None);
destroy_host_buffer(&win.vk.device, &win.copy_buffer);
}
println!("{}", win.histogram);
if let Some(first_frame_recvd) = win.first_frame_recvd {
println!(
"transfer rate: {:.2} mpbs ({:.2}kb per frame)",
win.total_video_bytes as f64 * 8.0
/ 1_000_000.0
/ first_frame_recvd.elapsed().as_secs_f64(),
win.total_video_bytes as f64 / 1_000.0 / win.frames_recvd as f64
);
}
}
Ok(())
}
impl winit::application::ApplicationHandler<AppEvent> for App {
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
if self.win.is_some() {
return;
}
match start_test(&self.args, &self.client, event_loop, self.proxy.clone()) {
Ok(w) => {
self.win = Some(w);
}
Err(e) => {
error!("failed to start test: {:#}", e);
event_loop.exit();
}
}
}
fn window_event(
&mut self,
_event_loop: &winit::event_loop::ActiveEventLoop,
_window_id: winit::window::WindowId,
_event: winit::event::WindowEvent,
) {
}
fn about_to_wait(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
let Some(win) = &self.win else {
return;
};
if win.block_started.elapsed() > time::Duration::from_secs(3) {
error!("timed out waiting for block");
event_loop.exit();
}
}
fn user_event(&mut self, event_loop: &winit::event_loop::ActiveEventLoop, event: AppEvent) {
let Some(win) = &mut self.win else {
return;
};
match win.event(event) {
Ok(true) => (),
Ok(false) => event_loop.exit(),
Err(e) => {
error!("error: {}", e);
event_loop.exit();
}
}
}
fn exiting(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {
let Some(win) = &self.win else {
return;
};
let _ = win.attachment.detach().block_on();
let _ = self
.client
.end_session(win.session_id, DEFAULT_TIMEOUT)
.block_on();
}
}
impl LatencyTest {
fn event(&mut self, event: AppEvent) -> anyhow::Result<bool> {
match event {
AppEvent::AttachmentEvent(AttachmentEvent::VideoStreamStart(stream_seq, params)) => {
assert_eq!(params.width, APP_DIMENSION);
assert_eq!(params.height, APP_DIMENSION);
self.stream
.reset(stream_seq, APP_DIMENSION, APP_DIMENSION, params.codec)?;
}
AppEvent::AttachmentEvent(AttachmentEvent::VideoPacket(packet)) => {
if self.first_frame_recvd.is_none() {
self.first_frame_recvd = Some(time::Instant::now());
}
self.total_video_bytes += packet.len();
self.stream.recv_packet(packet)?;
}
AppEvent::AttachmentEvent(AttachmentEvent::AttachmentEnded) => {
bail!("server closed connection");
}
AppEvent::AttachmentEvent(_) => (),
AppEvent::VideoStreamReady(tex, params) => {
assert_eq!(params.width, APP_DIMENSION);
assert_eq!(params.height, APP_DIMENSION);
self.video_texture = Some(tex);
}
AppEvent::VideoFrameAvailable => {
if self.stream.prepare_frame()?.is_some() {
self.frames_recvd += 1;
match self.frames_recvd.cmp(&100) {
std::cmp::Ordering::Less => (),
std::cmp::Ordering::Equal => {
debug!("starting test...");
self.send_space();
self.block_started = time::Instant::now();
self.next_block = 0;
}
std::cmp::Ordering::Greater => {
self.check_frame()?;
if self.next_block >= self.num_tests {
return Ok(false);
}
}
}
}
}
}
Ok(true)
}
fn send_space(&mut self) {
debug!("sending space");
self.attachment.keyboard_input(
client::input::Key::Space,
client::input::KeyState::Pressed,
0,
);
self.attachment.keyboard_input(
client::input::Key::Space,
client::input::KeyState::Released,
0,
);
}
fn check_frame(&mut self) -> anyhow::Result<()> {
unsafe {
self.submit_copy()?;
}
// Check the current block.
if self.check_block(self.next_block.wrapping_sub(1)) {
// Waiting...
} else if self.check_block(self.next_block) {
// Success!
let elapsed = self.block_started.elapsed();
debug!("block {} took {}ms", self.next_block, elapsed.as_millis());
self.histogram.add(elapsed.as_millis() as u64);
// Start the next one.
// Sleep 10-100ms.
use rand::Rng;
let ms = (rand::thread_rng().gen::<u64>() % 90) + 10;
std::thread::sleep(time::Duration::from_millis(ms));
self.next_block += 1;
self.block_started = time::Instant::now();
self.send_space();
} else if self.next_block > 0 {
warn!("neither current or next block are highlighted");
}
if self.block_started.elapsed() > time::Duration::from_secs(3) {
bail!("timed out waiting for block {}", self.next_block);
}
Ok(())
}
fn check_block(&mut self, idx: usize) -> bool {
let data =
unsafe { std::slice::from_raw_parts(self.copy_buffer.access as *mut u8, 256 * 256) };
// Blocks are arranged in an 8x8 grid, and are 32x32 pixels.
let idx = idx % 64;
let y = (idx / 8) * 32 + 16;
let x = (idx % 8) * 32 + 16;
data[y * 256 + x] > 20
}
unsafe fn submit_copy(&mut self) -> anyhow::Result<()> {
let device = &self.vk.device;
let texture = self.video_texture.as_ref().unwrap();
// Reset the command buffer.
device.reset_command_buffer(self.copy_cb, vk::CommandBufferResetFlags::empty())?;
// Begin the command buffer.
{
let begin_info = vk::CommandBufferBeginInfo::default()
.flags(vk::CommandBufferUsageFlags::SIMULTANEOUS_USE);
device.begin_command_buffer(self.copy_cb, &begin_info)?;
}
// Transfer the image to be readable.
cmd_image_barrier(
device,
self.copy_cb,
texture.image,
vk::PipelineStageFlags::TOP_OF_PIPE,
vk::AccessFlags::empty(),
vk::PipelineStageFlags::TRANSFER,
vk::AccessFlags::TRANSFER_READ,
vk::ImageLayout::UNDEFINED,
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
);
// Copy the texture to the staging buffer.
{
let region = vk::BufferImageCopy::default()
.buffer_row_length(256)
.buffer_image_height(256)
.image_subresource(vk::ImageSubresourceLayers {
aspect_mask: vk::ImageAspectFlags::PLANE_0,
mip_level: 0,
base_array_layer: 0,
layer_count: 1,
})
.image_extent(vk::Extent3D {
width: 256,
height: 256,
depth: 1,
});
let regions = [region];
device.cmd_copy_image_to_buffer(
self.copy_cb,
texture.image,
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
self.copy_buffer.buffer,
®ions,
)
}
device.end_command_buffer(self.copy_cb)?;
device.reset_fences(&[self.copy_fence])?;
device.queue_submit(
self.vk.present_queue.queue,
&[vk::SubmitInfo::default().command_buffers(&[self.copy_cb])],
self.copy_fence,
)?;
device.wait_for_fences(&[self.copy_fence], true, u64::MAX)?;
Ok(())
}
}
fn start_test(
args: &Cli,
client: &client::Client,
event_loop: &winit::event_loop::ActiveEventLoop,
proxy: winit::event_loop::EventLoopProxy<AppEvent>,
) -> anyhow::Result<LatencyTest> {
let attr = winit::window::Window::default_attributes().with_visible(false);
let window = Arc::new(event_loop.create_window(attr)?);
let vk = unsafe { Arc::new(VkContext::new(window.clone(), cfg!(debug_assertions))?) };
let codec = match args.codec.as_deref() {
Some("h264") => protocol::VideoCodec::H264,
Some("h265") | None => protocol::VideoCodec::H265,
Some("av1") => protocol::VideoCodec::Av1,
Some(v) => bail!("invalid codec: {:?}", v),
};
// Create session, attach
let sess = client
.launch_session(
"latency-test".to_string(),
client::display_params::DisplayParams {
width: APP_DIMENSION,
height: APP_DIMENSION,
framerate: args.framerate.unwrap_or(60),
ui_scale: client::pixel_scale::PixelScale::ONE,
},
vec![],
DEFAULT_TIMEOUT,
)
.block_on()
.context("failed to create session")?;
let config = client::AttachmentConfig {
width: APP_DIMENSION,
height: APP_DIMENSION,
video_codec: codec.into(),
video_profile: None,
quality_preset: Some(6),
audio_codec: None,
sample_rate: None,
channels: vec![],
video_stream_seq_offset: 0,
audio_stream_seq_offset: 0,
};
let delegate = Arc::new(AttachmentProxy::new(proxy.clone()));
let attachment = client
.attach_session(sess.id, config, delegate, DEFAULT_TIMEOUT)
.block_on()
.context("failed to attach")?;
// Just big enough for the Y plane.
let copy_buffer = create_host_buffer(
&vk.device,
vk.device_info.host_visible_mem_type_index,
vk::BufferUsageFlags::TRANSFER_DST,
(APP_DIMENSION * APP_DIMENSION) as usize,
)?;
let copy_cb = create_command_buffer(&vk.device, vk.present_queue.command_pool)?;
let copy_fence = create_fence(&vk.device, false)?;
Ok(LatencyTest {
attachment,
session_id: sess.id,
stream: VideoStream::new(vk.clone(), proxy.clone()),
video_texture: None,
frames_recvd: 0,
copy_cb,
copy_fence,
copy_buffer,
next_block: 0,
block_started: time::Instant::now(),
num_tests: args.samples.unwrap_or(256),
histogram: histo::Histogram::with_buckets(10),
first_frame_recvd: None,
total_video_bytes: 0,
vk: vk.clone(),
})
}
fn init_logging() -> anyhow::Result<()> {
if let Ok(env_filter) = tracing_subscriber::EnvFilter::try_from_default_env() {
tracing_subscriber::fmt().with_env_filter(env_filter).init();
} else {
let filter = tracing_subscriber::EnvFilter::builder()
.with_default_directive(tracing::level_filters::LevelFilter::INFO.into())
.from_env()?
.add_directive("mm_client=info".parse()?)
.add_directive("mm_client_common=info".parse()?);
tracing_subscriber::fmt().with_env_filter(filter).init();
}
Ok(())
}
================================================
FILE: mm-client/src/bin/mmclient.rs
================================================
// Copyright 2024 Colin Marc <hi@colinmarc.com>
//
// SPDX-License-Identifier: MIT
use std::{sync::Arc, time};
use anyhow::{anyhow, bail};
use clap::Parser;
use ffmpeg_sys_next as ffmpeg_sys;
use mm_client::{
audio,
cursor::{cursor_icon_from_proto, load_cursor_image},
delegate::{AttachmentEvent, AttachmentProxy},
flash::Flash,
gamepad::{spawn_gamepad_monitor, GamepadEvent},
keys::winit_key_to_proto,
overlay::Overlay,
render::Renderer,
stats::STATS,
video::{self, VideoStreamEvent},
vulkan,
};
use mm_client_common as client;
use mm_protocol as protocol;
use pollster::FutureExt as _;
use tracing::{debug, error, info, trace, warn};
use tracing_subscriber::Layer as _;
use winit::{event_loop::ControlFlow, window};
const DEFAULT_CONNECT_TIMEOUT: time::Duration = time::Duration::from_secs(1);
const DEFAULT_REQUEST_TIMEOUT: time::Duration = time::Duration::from_secs(30);
const MAX_FRAME_TIME: time::Duration = time::Duration::from_nanos(1_000_000_000 / 24);
const RESIZE_COOLDOWN: time::Duration = time::Duration::from_millis(500);
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
enum Resolution {
#[default]
Auto,
Height(u32),
Custom(u32, u32),
}
impl From<&str> for Resolution {
fn from(s: &str) -> Self {
if s == "auto" {
Resolution::Auto
} else if let Some((w, h)) = s.split_once('x') {
Resolution::Custom(
w.parse().expect("invalid resolution width"),
h.parse().expect("invalid resolution height"),
)
} else {
Resolution::Height(s.parse().expect("invalid resolution height"))
}
}
}
#[derive(Debug, Parser)]
#[command(name = "mmclient")]
#[command(about = "The Magic Mirror reference client", long_about = None)]
struct Cli {
/// The server to connect to.
#[arg(value_name = "HOST[:PORT]")]
host: String,
/// The id of the app, or the ID of an existing session.
app: Option<String>,
/// Print a list of launchable applications and exit.
#[arg(long)]
list_apps: bool,
/// Print a list of matching sessions and exit.
#[arg(short = 'L', long)]
list: bool,
/// End a session (which may be specified by name or ID) and exit.
#[arg(short = 'K', long)]
kill: bool,
/// Always resume an existing session, and error if none match.
#[arg(short, long)]
resume: bool,
/// Always launch a new session, even if one exists that matches.
#[arg(short, long)]
launch: bool,
/// On exit, automatically kill the session.
#[arg(short = 'x', long)]
kill_on_exit: bool,
/// The streaming resolution to use. If not specified, this will be tied to
/// the client resolution, and automatically change when the client window
/// resizes.
#[arg(long, required = false, default_value = "auto")]
resolution: Resolution,
/// Request 10-bit video output from the server. This will only work if
/// both your display and the application in question support rendering
/// HDR color.
#[arg(long, required = false)]
hdr: bool,
/// The UI scale to communicate to the server. If not specified, this will
/// be determined from the client-side window scale factor.
#[arg(long, required = false)]
ui_scale: Option<f64>,
/// Video codec to use.
#[arg(long, default_value = "h265")]
codec: Option<String>,
/// Framerate to render at on the server side.
#[arg(long, default_value = "30")]
framerate: u32,
/// The quality preset to use, from 0-9.
#[arg(short, long, default_value = "6")]
preset: u32,
/// Open in fullscreen mode.
#[arg(long)]
fullscreen: bool,
/// Enable the overlay, which shows various stats.
#[arg(long)]
overlay: bool,
}
struct AttachmentWindow {
configured_resolution: Resolution,
configured_ui_scale: Option<f64>,
configured_framerate: u32,
window: Arc<winit::window::Window>,
attachment: client::Attachment,
attachment_config: client::AttachmentConfig,
delegate: Arc<AttachmentProxy<AppEvent>>,
session: client::Session,
video_stream: video::VideoStream<AppEvent>,
audio_stream: audio::AudioStream,
renderer: Renderer,
window_width: u32,
window_height: u32,
window_ui_scale: f64,
minimized: bool,
next_frame: time::Instant,
last_frame_received: time::Instant,
resize_cooldown: Option<time::Instant>,
needs_refresh: Option<u64>,
refresh_cooldown: Option<time::Instant>,
cursor_modifiers: winit::keyboard::ModifiersState,
cursor_pos: Option<(f64, f64)>,
flash: Flash,
overlay: Option<Overlay>,
stats_timer: time::Instant,
_vk: Arc<vulkan::VkContext>,
}
struct App {
client: client::Client,
args: Cli,
attachment_window: Option<AttachmentWindow>,
proxy: winit::event_loop::EventLoopProxy<AppEvent>,
end_session_on_exit: bool,
}
pub enum AppEvent {
VideoStreamReady(Arc<vulkan::VkImage>, video::VideoStreamParams),
VideoFrameAvailable,
AttachmentEvent(AttachmentEvent),
GamepadEvent(GamepadEvent),
}
impl From<VideoStreamEvent> for AppEvent {
fn from(event: VideoStreamEvent) -> Self {
use VideoStreamEvent::*;
match event {
VideoStreamReady(tex, params) => AppEvent::VideoStreamReady(tex, params),
VideoFrameAvailable => AppEvent::VideoFrameAvailable,
}
}
}
impl From<AttachmentEvent> for AppEvent {
fn from(value: AttachmentEvent) -> Self {
Self::AttachmentEvent(value)
}
}
impl From<GamepadEvent> for AppEvent {
fn from(event: GamepadEvent) -> Self {
Self::GamepadEvent(event)
}
}
impl std::fmt::Debug for AppEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AppEvent::VideoStreamReady(_, params) => write!(f, "VideoStreamReady({params:?})"),
AppEvent::VideoFrameAvailable => write!(f, "VideoFrameAvailable"),
AppEvent::AttachmentEvent(ev) => std::fmt::Debug::fmt(ev, f),
AppEvent::GamepadEvent(ev) => std::fmt::Debug::fmt(ev, f),
}
}
}
impl winit::application::ApplicationHandler<AppEvent> for App {
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
if self.attachment_window.is_none() {
let window = match init_window(&self.args, &self.client, event_loop, &self.proxy) {
Ok(w) => w,
Err(e) => {
error!("failed to attach to session: {:#}", e);
event_loop.exit();
return;
}
};
self.attachment_window = Some(window);
}
}
fn window_event(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
window_id: winit::window::WindowId,
event: winit::event::WindowEvent,
) {
let Some(win) = &mut self.attachment_window else {
return;
};
if win.window.id() != window_id {
return;
}
if let Err(e) = win.renderer.handle_event(&event) {
error!("renderer error: {:#}", e);
event_loop.exit();
return;
}
let res = win.handle_window_event(event);
win.schedule_next_frame(event_loop, res);
}
fn device_event(
&mut self,
_event_loop: &winit::event_loop::ActiveEventLoop,
_device_id: winit::event::DeviceId,
event: winit::event::DeviceEvent,
) {
let Some(win) = &mut self.attachment_window else {
return;
};
let winit::event::DeviceEvent::MouseMotion { delta: (x, y) } = event else {
return;
};
if let Some((x, y)) = win.motion_vector_to_attachment_space(x, y) {
win.attachment.relative_pointer_motion(x, y)
}
}
fn user_event(&mut self, event_loop: &winit::event_loop::ActiveEventLoop, event: AppEvent) {
let Some(win) = &mut self.attachment_window else {
return;
};
let res = win.handle_app_event(event_loop, &self.client, event);
win.schedule_next_frame(event_loop, res);
}
fn about_to_wait(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
let Some(win) = &mut self.attachment_window else {
return;
};
let res = win.idle(&self.client);
win.schedule_next_frame(event_loop, res);
}
fn exiting(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {
if let Some(AttachmentWindow {
attachment,
session,
..
}) = self.attachment_window.take()
{
debug!("detaching from session");
match attachment.detach().block_on() {
Ok(()) | Err(client::ClientError::Detached) => (),
Err(err) => error!(?err, "failed to detach cleanly"),
}
if self.end_session_on_exit {
debug!("ending session");
match self
.client
.end_session(session.id, DEFAULT_REQUEST_TIMEOUT)
.block_on()
{
Ok(()) => (),
Err(client::ClientError::ServerError(err))
if err.err_code() == protocol::error::ErrorCode::ErrorSessionNotFound => {}
Err(err) => error!(?err, "failed to end session"),
}
}
}
}
}
impl AttachmentWindow {
fn handle_window_event(&mut self, event: winit::event::WindowEvent) -> anyhow::Result<bool> {
trace!(?event, "handling window event");
use winit::event::*;
match event {
WindowEvent::RedrawRequested => {
self.video_stream.prepare_frame()?;
self.video_stream.mark_frame_rendered();
if !self.minimized && self.video_stream.is_ready() {
unsafe {
self.renderer.render(|ui| {
self.flash.build(ui)?;
if let Some(ref mut overlay) = self.overlay {
overlay.build(ui)?;
}
Ok(())
})?;
};
}
self.next_frame = time::Instant::now() + MAX_FRAME_TIME;
}
WindowEvent::CloseRequested => return Ok(false),
WindowEvent::Resized(size) => {
if size.width == 0 || size.height == 0 {
self.minimized = true;
} else {
debug!("resize event: {}x{}", size.width, size.height);
if size.width != self.window_width || size.height != self.window_height {
if let Some(ref mut overlay) = self.overlay {
overlay.reposition();
}
// Trigger a stream resize, but debounce first.
self.resize_cooldown = Some(time::Instant::now() + RESIZE_COOLDOWN);
}
self.minimized = false;
}
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
debug!("window scale factor changed to {}", scale_factor);
// Winit sends us a Resized event, immediately after this
// one, with the new physical resolution.
}
WindowEvent::ModifiersChanged(modifiers) => {
self.cursor_modifiers = modifiers.state();
}
WindowEvent::KeyboardInput {
event:
KeyEvent {
physical_key: winit::keyboard::PhysicalKey::Code(code),
logical_key,
state,
repeat,
..
},
..
} => {
if state == ElementState::Pressed
&& logical_key == winit::keyboard::Key::Character("d".into())
&& self.cursor_modifiers.control_key()
{
return Ok(false);
} else {
let char = match logical_key {
winit::keyboard::Key::Character(text) => text.chars().next(),
_ => None,
};
let state = match state {
_ if repeat => client::input::KeyState::Repeat,
ElementState::Pressed => client::input::KeyState::Pressed,
ElementState::Released => client::input::KeyState::Released,
};
let key = winit_key_to_proto(code);
if key == protocol::keyboard_input::Key::Unknown {
debug!("unknown key: {:?}", code);
} else {
self.attachment
.keyboard_input(key, state, char.map_or(0, Into::into));
}
}
}
WindowEvent::CursorMoved { position, .. } => {
let new_position = self.renderer.get_texture_aspect().and_then(|aspect| {
// Calculate coordinates in [-1.0, 1.0];
let (clip_x, clip_y) = (
(position.x / self.window_width as f64) * 2.0 - 1.0,
(position.y / self.window_height as f64) * 2.0 - 1.0,
);
// Stretch the space to account for letterboxing.
let clip_x = clip_x * aspect.0;
let clip_y = clip_y * aspect.1;
// In the letterbox.
if clip_x.abs() > 1.0 || clip_y.abs() > 1.0 {
return None;
}
// Convert to texture coordinates.
let x = (clip_x + 1.0) / 2.0;
let y = (clip_y + 1.0) / 2.0;
// Convert the position to physical coordinates in the remote display.
let cursor_x = x * self.attachment_config.width as f64;
let cursor_y = y * self.attachment_config.height as f64;
Some((cursor_x, cursor_y))
});
if let Some((cursor_x, cursor_y)) = new_position {
self.attachment.pointer_motion(cursor_x, cursor_y);
if new_position.is_some() && self.cursor_pos.is_none() {
self.attachment.pointer_entered();
} else if new_position.is_none() && self.cursor_pos.is_some() {
self.attachment.pointer_left();
}
self.cursor_pos = new_position;
}
}
WindowEvent::CursorEntered { .. } => {
// Handled on the CursorMoved event.
}
WindowEvent::CursorLeft { .. } => {
if self.cursor_pos.take().is_some() {
self.attachment.pointer_left()
}
}
WindowEvent::MouseInput { state, button, .. } => {
use protocol::pointer_input::*;
if self.cursor_pos.is_none() {
return Ok(true);
}
let button = match button {
winit::event::MouseButton::Left => Button::Left,
winit::event::MouseButton::Right => Button::Right,
winit::event::MouseButton::Middle => Button::Middle,
winit::event::MouseButton::Back => Button::Back,
winit::event::MouseButton::Forward => Button::Forward,
winit::event::MouseButton::Other(id) => {
debug!("skipping unknown mouse button: {}", id);
return Ok(true);
}
};
let state = match state {
ElementState::Pressed => ButtonState::Pressed,
ElementState::Released => ButtonState::Released,
};
let (cursor_x, cursor_y) = self.cursor_pos.unwrap();
self.attachment
.pointer_input(button, state, cursor_x, cursor_y);
}
WindowEvent::MouseWheel {
delta: MouseScrollDelta::LineDelta(x, y),
phase: TouchPhase::Moved,
..
} => self.attachment.pointer_scroll(
client::input::ScrollType::Discrete,
x as f64,
y as f64,
),
WindowEvent::MouseWheel {
delta: MouseScrollDelta::PixelDelta(vector),
phase: TouchPhase::Moved,
..
} => {
if let Some((x, y)) = self.motion_vector_to_attachment_space(vector.x, vector.y) {
self.attachment
.pointer_scroll(client::input::ScrollType::Continuous, x, y);
}
}
_ => (),
}
Ok(true)
}
fn handle_app_event(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
client: &client::Client,
event: AppEvent,
) -> anyhow::Result<bool> {
trace!(?event, "handling event");
use AttachmentEvent::*;
match event {
AppEvent::AttachmentEvent(ev) => match ev {
VideoStreamStart(stream_seq, params) => {
self.attachment_config.video_stream_seq_offset =
stream_seq.max(self.attachment_config.video_stream_seq_offset);
self.video_stream.reset(
stream_seq,
params.width,
params.height,
params.codec,
)?;
self.needs_refresh = None;
}
VideoPacket(packet) => {
self.last_frame_received = time::Instant::now();
self.video_stream.recv_packet(packet)?;
}
DroppedVideoPacket(dropped) => {
// Only request a keyframe once every ten seconds.
if dropped.hierarchical_layer == 0 {
self.needs_refresh = Some(dropped.stream_seq);
}
}
AudioStreamStart(stream_seq, params) => {
self.attachment_config.audio_stream_seq_offset =
stream_seq.max(self.attachment_config.audio_stream_seq_offset);
self.audio_stream.reset(
stream_seq,
params.sample_rate,
params.channels.len() as u32,
)?;
}
AudioPacket(packet) => {
self.audio_stream.recv_packet(packet)?;
}
UpdateCursor {
icon,
image,
hotspot_x,
hotspot_y,
} => {
if let Some(image) = image {
if let Ok(cursor) = load_cursor_image(&image, hotspot_x, hotspot_y)
.map(|src| event_loop.create_custom_cursor(src))
{
self.window.set_cursor(cursor);
self.window.set_cursor_visible(true);
} else {
error!(image_len = image.len(), "custom cursor image update failed");
}
} else if icon == protocol::update_cursor::CursorIcon::None {
self.window.set_cursor_visible(false);
} else {
self.window.set_cursor(cursor_icon_from_proto(icon));
self.window.set_cursor_visible(true);
}
}
LockPointer(x, y) => {
debug!(x, y, "cursor locked");
// On most platforms, we have to lock the cursor before we
// warp it. On mac, it's the other way around.
#[cfg(not(target_vendor = "apple"))]
self.window
.set_cursor_grab(winit::window::CursorGrabMode::Locked)?;
if let Some(aspect) = self.renderer.get_texture_aspect() {
let width = self.attachment_config.width;
let height = self.attachment_config.height;
// Map vector to [-0.5, 0.5].
let x = (x / width as f64) - 0.5;
let y = (y / height as f64) - 0.5;
// Squish the space to account for letterboxing.
let x = x / aspect.0;
let y = y / aspect.1;
// Map to the screen size.
let x = (x + 0.5) * self.window_width as f64;
let y = (y + 0.5) * self.window_height as f64;
let pos: winit::dpi::PhysicalPosition<f64> = (x, y).into();
self.window.set_cursor_position(pos)?;
}
#[cfg(target_vendor = "apple")]
self.window
.set_cursor_grab(winit::window::CursorGrabMode::Locked)?;
}
ReleasePointer => {
self.window
.set_cursor_grab(winit::window::CursorGrabMode::None)?;
}
DisplayParamsChanged {
params,
reattach_required,
} => {
if reattach_required {
self.attachment_config.width = params.width;
self.attachment_config.height = params.height;
// TODO: this blocks the app, which is not ideal.
// We could spawn a thread for this, or reuse one.
debug!("reattaching to session after resize");
self.attachment = client
.attach_session(
self.session.id,
self.attachment_config.clone(),
self.delegate.clone(),
DEFAULT_REQUEST_TIMEOUT,
)
.block_on()?;
}
self.session.display_params = params;
}
AttachmentEnded => {
info!("attachment ended by server");
return Ok(false);
}
},
AppEvent::VideoStreamReady(texture, params) => {
self.renderer.bind_video_texture(texture, params)?;
}
AppEvent::VideoFrameAvailable => {
if self.video_stream.prepare_frame()?.is_some() {
self.window.request_redraw();
}
}
AppEvent::GamepadEvent(gev) => match gev {
GamepadEvent::Available(pad) => self.attachment.gamepad_available(pad),
GamepadEvent::Unavailable(id) => self.attachment.gamepad_unavailable(id),
GamepadEvent::Input(id, button, state) => {
self.attachment.gamepad_input(id, button, state)
}
GamepadEvent::Motion(id, axis, value) => {
self.attachment.gamepad_motion(id, axis, value)
}
},
}
Ok(true)
}
fn idle(&mut self, client: &client::Client) -> anyhow::Result<bool> {
if self.next_frame.elapsed() > time::Duration::ZERO {
self.window.request_redraw();
}
if self.stats_timer.elapsed() > time::Duration::from_millis(100) {
STATS.set_connection_rtt(client.stats().rtt)
}
let last_frame = self.last_frame_received.elapsed();
if last_frame > time::Duration::from_secs(1) {
if last_frame > DEFAULT_REQUEST_TIMEOUT {
// TODO: this fires when we've tabbed away.
bail!("timed out waiting for video frames");
} else {
self.flash.set_message("waiting for server...");
}
}
// Debounced processing of the resize event.
if self.resize_cooldown.is_some()
&& self.resize_cooldown.unwrap().elapsed() > time::Duration::ZERO
{
let size = self.window.inner_size();
let scale_factor = self.window.scale_factor();
if size.width != self.window_width
|| size.height != self.window_height
|| scale_factor != self.window_ui_scale
{
debug!(
width = size.width,
height = size.height,
scale_factor,
"window resized"
);
self.window_width = size.width;
self.window_height = size.height;
self.window_ui_scale = scale_factor;
let desired_ui_scale = determine_ui_scale(
self.configured_ui_scale
.unwrap_or(self.window.scale_factor()),
);
let (desired_width, desired_height) = determine_resolution(
self.configured_resolution,
self.window_width,
self.window_height,
);
let desired_params = client::display_params::DisplayParams {
width: desired_width,
height: desired_height,
ui_scale: desired_ui_scale,
framerate: self.configured_framerate,
};
// Update the session to match our desired resolution or
// scale. Note that this is skipped if there is no
// current attachment (and `current_streaming_res` is
// None).
if desired_params != self.session.display_params {
debug!(
"resizing session to {}x{}@{} (scale: {})",
desired_width, desired_height, self.configured_framerate, desired_ui_scale,
);
self.flash.set_message("resizing...");
// TODO: this blocks the app.
client
.update_session_display_params(
self.session.id,
desired_params,
DEFAULT_REQUEST_TIMEOUT,
)
.block_on()?;
}
}
self.resize_cooldown = None;
}
// Request a video refresh if we need one, but only every ten seconds.
if self.needs_refresh.is_some()
&& self
.refresh_cooldown
.is_none_or(|t| t.elapsed() > time::Duration::from_secs(10))
{
let stream_seq = self.needs_refresh.unwrap();
debug!(stream_seq, "requesting video refresh");
self.attachment.request_video_refresh(stream_seq);
self.refresh_cooldown = Some(time::Instant::now());
self.needs_refresh = None;
}
Ok(true)
}
fn schedule_next_frame(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
res: anyhow::Result<bool>,
) {
match res {
Ok(true) => {
event_loop.set_control_flow(ControlFlow::WaitUntil(self.next_frame));
}
Ok(false) => event_loop.exit(),
Err(e) => {
error!("{:#}", e);
event_loop.exit()
}
}
}
fn motion_vector_to_attachment_space(&self, x: f64, y: f64) -> Option<(f64, f64)> {
let (aspect_x, aspect_y) = self.renderer.get_texture_aspect()?;
// Map vector to [0, 1]. (It can also be negative.)
let (x, y) = (
(x / self.window_width as f64),
(y / self.window_height as f64),
);
// Stretch the space to account for letterboxing. For
// example, if the video texture only takes up one third
// of the screen vertically, and we scroll up one third
// of the window height, the resulting vector should be [0,
// -1.0].
let x = x * aspect_x;
let y = y * aspect_y;
Some((
x * self.attachment_config.width as f64,
y * self.attachment_config.height as f64,
))
}
}
pub fn main() -> anyhow::Result<()> {
init_logging()?;
let args = Cli::parse();
let cmds: u8 = vec![
args.list_apps,
args.list,
args.kill,
args.launch,
args.resume,
]
.into_iter()
.map(|b| b as u8)
.sum();
if cmds > 1 {
bail!("only one of --launch, --resume, --list, or --kill may be specified");
} else if !(args.list || args.list_apps) && args.app.is_none() {
bail!("an app name or session ID must be specified");
} else if args.list_apps && args.app.is_some() {
bail!("an app name or session ID may not be specified alongside --list-apps")
}
debug!("establishing connection to {:}", &args.host);
let client = client::Client::new(&args.host, "mmclient", DEFAULT_CONNECT_TIMEOUT).block_on()?;
if args.list_apps {
return cmd_list_apps(&client);
} else if args.list {
return cmd_list_sessions(&args, &client);
} else if args.kill {
return cmd_kill(&args, &client);
}
let event_loop = winit::event_loop::EventLoop::with_user_event().build()?;
let proxy = event_loop.create_proxy();
let end_session_on_exit = args.kill_on_exit;
let mut app = App {
client,
args,
attachment_window: None,
proxy,
end_session_on_exit,
};
event_loop.run_app(&mut app)?;
Ok(())
}
fn init_window(
args: &Cli,
client: &client::Client,
event_loop: &winit::event_loop::ActiveEventLoop,
proxy: &winit::event_loop::EventLoopProxy<AppEvent>,
) -> anyhow::Result<AttachmentWindow> {
let sessions = client.list_sessions(DEFAULT_REQUEST_TIMEOUT).block_on()?;
let target = args.app.clone().unwrap();
let matched = filter_sessions(sessions, args.app.as_ref().unwrap());
if !args.launch && matched.len() > 1 {
bail!(
"multiple sessions found matching {:?}, specify a session ID to attach or use \
--launch to create a new one.",
target,
);
} else if args.resume && matched.is_empty() {
bail!("no session found matching {:?}", target);
}
let configured_codec = match args.codec.as_deref() {
Some("h264") => client::codec::VideoCodec::H264,
Some("h265") | None => client::codec::VideoCodec::H265,
Some("av1") => client::codec::VideoCodec::Av1,
Some(v) => bail!("invalid codec: {:?}", v),
};
let configured_profile = if args.hdr {
protocol::VideoProfile::Hdr10
} else {
protocol::VideoProfile::Hd
};
let session = if args.launch || matched.is_empty() {
None
} else {
Some(matched[0].clone())
};
let window_attr = if args.fullscreen {
window::Window::default_attributes()
.with_fullscreen(Some(window::Fullscreen::Borderless(None)))
} else {
window::Window::default_attributes()
};
let window = Arc::new(event_loop.create_window(window_attr)?);
let vk = unsafe {
Arc::new(vulkan::VkContext::new(
window.clone(),
cfg!(debug_assertions),
)?)
};
let renderer = Renderer::new(vk.clone(), window.clone(), args.hdr)?;
let window_size = window.inner_size();
let window_ui_scale = window.scale_factor();
let (width, height) =
determine_resolution(args.resolution, window_size.width, window_size.height);
let desired_params = client::display_params::DisplayParams {
width,
height,
framerate: args.framerate,
ui_scale: determine_ui_scale(args.ui_scale.unwrap_or(window_ui_scale)),
};
let initial_gamepads = spawn_gamepad_monitor(proxy.clone())?;
let session_id = if let Some(session) = session {
if session.display_params != desired_params {
debug!("updating session params to {:?}", desired_params);
client
.update_session_display_params(session.id, desired_params, DEFAULT_REQUEST_TIMEOUT)
.block_on()?;
}
session.id
} else {
let target = args.app.as_ref().unwrap();
let target = target.rsplit("/").next().unwrap();
info!("launching a new session for for app {:?}", target);
client
.launch_session(
target.into(),
desired_params.clone(),
initial_gamepads.clone(),
DEFAULT_REQUEST_TIMEOUT,
)
.block_on()?
.id
};
// Refetch the session params.
let session = client
.list_sessions(DEFAULT_REQUEST_TIMEOUT)
.block_on()?
.into_iter()
.find(|s| s.id == session_id)
.ok_or(anyhow!("new session not found in session list"))?;
let now = time::Instant::now();
let mut flash = Flash::new();
flash.set_message("connecting...");
let overlay = if args.overlay {
Some(Overlay::new(args.framerate))
} else {
None
};
let delegate = Arc::new(AttachmentProxy::new(proxy.clone()));
let audio_stream = audio::AudioStream::new()?;
let video_stream = video::VideoStream::new(vk.clone(), proxy.clone());
spawn_gamepad_monitor(proxy.clone())?;
let attachment_config = client::AttachmentConfig {
width: session.display_params.width,
height: session.display_params.height,
video_codec: Some(configured_codec),
video_profile: Some(configured_profile),
quality_preset: Some(args.preset + 1),
audio_codec: None,
sample_rate: None,
channels: Vec::new(),
video_stream_seq_offset: 0,
audio_stream_seq_offset: 0,
};
debug!(session_id = session.id, "attaching to session");
let attachment = client
.attach_session(
session.id,
attachment_config.clone(),
delegate.clone(),
DEFAULT_REQUEST_TIMEOUT,
)
.block_on()?;
Ok(AttachmentWindow {
configured_resolution: args.resolution,
configured_framerate: args.framerate,
configured_ui_scale: args.ui_scale,
window,
attachment,
attachment_config,
delegate,
session,
video_stream,
audio_stream,
renderer,
window_width: window_size.width,
window_height: window_size.height,
window_ui_scale,
minimized: false,
next_frame: now + MAX_FRAME_TIME,
last_frame_received: now,
resize_cooldown: None,
needs_refresh: None,
refresh_cooldown: None,
cursor_modifiers: winit::keyboard::ModifiersState::default(),
cursor_pos: None,
flash,
overlay,
stats_timer: now,
_vk: vk,
})
}
fn init_logging() -> anyhow::Result<()> {
if cfg!(feature = "tracy") {
use tracing_subscriber::layer::SubscriberExt;
let filter = tracing_subscriber::EnvFilter::builder()
.with_default_directive(tracing::level_filters::LevelFilter::INFO.into())
.from_env()?
.add_directive("mmclient=trace".parse()?)
.add_directive("mm_client=trace".parse()?)
.add_directive("mm_client_common=trace".parse()?);
tracing::subscriber::set_global_default(
tracing_subscriber::registry()
.with(tracing_tracy::TracyLayer::default().with_filter(filter)),
)
.expect("setup tracy layer");
} else if let Ok(env_filter) = tracing_subscriber::EnvFilter::try_from_default_env() {
tracing_subscriber::fmt().with_env_filter(env_filter).init();
} else {
let filter = tracing_subscriber::EnvFilter::builder()
.with_default_directive(tracing::level_filters::LevelFilter::INFO.into())
.from_env()?
.add_directive("mmclient=info".parse()?)
.add_directive("mm_client=info".parse()?)
.add_directive("mm_client_common=info".parse()?);
tracing_subscriber::fmt().with_env_filter(filter).init();
}
// Squash ffmpeg logs.
unsafe {
ffmpeg_sys::av_log_set_level(ffmpeg_sys::AV_LOG_QUIET);
// TODO: the callback has to be variadic, which means using nightly
// rust.
// ffmpeg_sys::av_log_set_callback(Some(ffmpeg_log_callback))
}
Ok(())
}
fn determine_ui_scale(scale_factor: f64) -> client::pixel_scale::PixelScale {
let scale = match scale_factor {
x if x < 1.0 => client::pixel_scale::PixelScale::ONE,
_ => {
// Multiplying by 6/6 captures most possible fractional scales.
let numerator = (scale_factor * 6.0).round() as u32;
let denominator = 6;
if numerator % denominator == 0 {
client::pixel_scale::PixelScale::new(numerator / denominator, 1)
} else {
client::pixel_scale::PixelScale::new(numerator, denominator)
}
}
};
if scale.is_fractional() {
let rounded = scale.round_up();
warn!(
requested = %scale,
using = %rounded,
"fractional scale not supported, rounding up"
);
return rounded;
}
scale
}
fn determine_resolution(resolution: Resolution, width: u32, height: u32) -> (u32, u32) {
match resolution {
Resolution::Auto => (width.next_multiple_of(2), height.next_multiple_of(2)),
Resolution::Height(h) => {
let h = std::cmp::min(h, height).next_multiple_of(2);
let w = (h * width / height).next_multiple_of(2);
(w, h)
}
Resolution::Custom(w, h) => (w, h),
}
}
fn filter_sessions(sessions: Vec<client::Session>, app: &str) -> Vec<client::Session> {
if let Ok(id) = app.parse::<u64>() {
return match sessions.into_iter().find(|s| s.id == id) {
Some(s) => vec![s],
None => vec![],
};
}
sessions
.into_iter()
.filter(|s| s.application_id == app)
.collect()
}
fn cmd_list_apps(client: &client::Client) -> anyhow::Result<()> {
let apps = client
.list_applications(DEFAULT_REQUEST_TIMEOUT)
.block_on()?;
if apps.is_empty() {
println!("No launchable applications found.");
return Ok(());
}
let mut apps = apps
.into_iter()
.map(|app| {
let mut name = String::new();
for dir in &app.folder {
name.push_str(dir);
name.push('/');
}
name.push_str(&app.id);
(name, app.description)
})
.collect::<Vec<_>>();
apps.sort();
let mut tw = tabwriter::TabWriter::new(std::io::stdout()).padding(4);
use std::io::Write as _;
writeln!(&mut tw, "Name\tDescription")?;
writeln!(&mut tw, "----\t-----------")?;
for (name, desc) in apps {
if desc.len() <= 80 {
writeln!(&mut tw, "{}\t{}", name, desc)?;
} else {
writeln!(&mut tw, "{}\t{}...", name, &desc[..77])?;
}
}
tw.flush()?;
Ok(())
}
fn cmd_list_sessions(args: &Cli, client: &client::Client) -> anyhow::Result<()> {
let sessions = client.list_sessions(DEFAULT_REQUEST_TIMEOUT).block_on()?;
let sessions = if let Some(target) = args.app.as_ref() {
filter_sessions(sessions, target)
} else {
sessions
};
if sessions.is_empty() {
println!("No (matching) sessions found.");
return Ok(());
}
let now = time::SystemTime::now();
let mut tw = tabwriter::TabWriter::new(std::io::stdout()).padding(4);
use std::io::Write as _;
writeln!(&mut tw, "Session ID\tApplication Name\tRuntime")?;
writeln!(&mut tw, "----------\t----------------\t-------")?;
for session in sessions {
let runtime = {
// Round to seconds.
let secs = now.duration_since(session.start)?.as_secs();
humantime::format_duration(time::Duration::from_secs(secs)).to_string()
};
writeln!(
&mut tw,
"{}\t{}\t{}",
session.id, session.application_id, runtime,
)?;
}
tw.flush()?;
Ok(())
}
fn cmd_kill(args: &Cli, client: &client::Client) -> anyhow::Result<()> {
let target = args.app.as_ref().unwrap();
let sessions = filter_sessions(
client.list_sessions(DEFAULT_REQUEST_TIMEOUT).block_on()?,
target,
);
if sessions.is_empty() {
println!("No (matching) sessions found.");
return Ok(());
} else if sessions.len() > 1 {
bail!("Multiple sessions matched!");
}
client
.end_session(sessions[0].id, DEFAULT_REQUEST_TIMEOUT)
.block_on()?;
Ok(())
}
================================================
FILE: mm-client/src/cursor.rs
================================================
// Copyright 2024 Colin Marc <hi@colinmarc.com>
//
// SPDX-License-Identifier: MIT
use mm_protocol as protocol;
use winit::window::{CursorIcon, CustomCursor, CustomCursorSource};
pub fn load_cursor_image(image: &[u8], hs_x: u32, hs_y: u32) -> anyhow::Result<CustomCursorSource> {
let cursor = image::load_from_memory_with_format(image, image::ImageFormat::Png)?;
let w = cursor.width().try_into()?;
let h = cursor.height().try_into()?;
let hs_x = hs_x.try_into()?;
let hs_y = hs_y.try_into()?;
Ok(CustomCursor::from_rgba(
cursor.to_rgba8().into_raw(),
w,
h,
hs_x,
hs_y,
)?)
}
pub fn cursor_icon_from_proto(icon: protocol::update_cursor::CursorIcon) -> CursorIcon {
match icon {
protocol::update_cursor::CursorIcon::ContextMenu => CursorIcon::ContextMenu,
protocol::update_cursor::CursorIcon::Help => CursorIcon::Help,
protocol::update_cursor::CursorIcon::Pointer => CursorIcon::Pointer,
protocol::update_cursor::CursorIcon::Progress => CursorIcon::Progress,
protocol::update_cursor::CursorIcon::Wait => CursorIcon::Wait,
protocol::update_cursor::CursorIcon::Cell => CursorIcon::Cell,
protocol::update_cursor::CursorIcon::Crosshair => CursorIcon::Crosshair,
protocol::update_cursor::CursorIcon::Text => CursorIcon::Text,
protocol::update_cursor::CursorIcon::VerticalText => CursorIcon::VerticalText,
protocol::update_cursor::CursorIcon::Alias => CursorIcon::Alias,
protocol::update_cursor::CursorIcon::Copy => CursorIcon::Copy,
protocol::update_cursor::CursorIcon::Move => CursorIcon::Move,
protocol::update_cursor::CursorIcon::NoDrop => CursorIcon::NoDrop,
protocol::update_cursor::CursorIcon::NotAllowed => CursorIcon::NotAllowed,
protocol::update_cursor::CursorIcon::Grab => CursorIcon::Grab,
protocol::update_cursor::CursorIcon::Grabbing => CursorIcon::Grabbing,
protocol::update_cursor::CursorIcon::EResize => CursorIcon::EResize,
protocol::update_cursor::CursorIcon::NResize => CursorIcon::NResize,
protocol::update_cursor::CursorIcon::NeResize => CursorIcon::NeResize,
protocol::update_cursor::CursorIcon::NwResize => CursorIcon::NwResize,
protocol::update_cursor::CursorIcon::SResize => CursorIcon::SResize,
protocol::update_cursor::CursorIcon::SeResize => CursorIcon::SeResize,
protocol::update_cursor::CursorIcon::SwResize => CursorIcon::SwResize,
protocol::update_cursor::CursorIcon::WResize => CursorIcon::WResize,
protocol::update_cursor::CursorIcon::EwResize => CursorIcon::EwResize,
protocol::update_cursor::CursorIcon::NsResize => CursorIcon::NsResize,
protocol::update_cursor::CursorIcon::NeswResize => CursorIcon::NeswResize,
protocol::update_cursor::CursorIcon::NwseResize => CursorIcon::NwseResize,
protocol::update_cursor::CursorIcon::ColResize => CursorIcon::ColResize,
protocol::update_cursor::CursorIcon::RowResize => CursorIcon::RowResize,
protocol::update_cursor::CursorIcon::AllScroll => CursorIcon::AllScroll,
protocol::update_cursor::CursorIcon::ZoomIn => CursorIcon::ZoomIn,
protocol::update_cursor::CursorIcon::ZoomOut => CursorIcon::ZoomOut,
_ => CursorIcon::Default,
}
}
================================================
FILE: mm-client/src/delegate.rs
================================================
// Copyright 2024 Colin Marc <hi@colinmarc.com>
//
// SPDX-License-Identifier: MIT
use std::sync::Arc;
use mm_client_common as client;
use tracing::error;
// An implementation of client-common's AttachmentDelegate that converts
// callbacks into winit events.
#[derive(Debug)]
pub struct AttachmentProxy<T: From<AttachmentEvent> + std::fmt::Debug + Send + 'static>(
winit::event_loop::EventLoopProxy<T>,
);
impl<T: From<AttachmentEvent> + std::fmt::Debug + Send + 'static> AttachmentProxy<T> {
pub fn new(proxy: winit::event_loop::EventLoopProxy<T>) -> Self {
Self(proxy)
}
fn proxy(&self, ev: AttachmentEvent) {
let _ = self.0.send_event(ev.into());
}
}
pub enum AttachmentEvent {
VideoStreamStart(u64, client::VideoStreamParams),
VideoPacket(Arc<client::Packet>),
DroppedVideoPacket(client::DroppedPacket),
AudioStreamStart(u64, client::AudioStreamParams),
AudioPacket(Arc<client::Packet>),
UpdateCursor {
icon: client::input::CursorIcon,
image: Option<Vec<u8>>,
hotspot_x: u32,
hotspot_y: u32,
},
LockPointer(f64, f64),
ReleasePointer,
DisplayParamsChanged {
params: client::display_params::DisplayParams,
reattach_required: bool,
},
AttachmentEnded,
}
impl std::fmt::Debug for AttachmentEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AttachmentEvent::VideoStreamStart(stream_seq, _) => {
write!(f, "VideoStreamStart({})", stream_seq)
}
AttachmentEvent::VideoPacket(packet) => {
write!(f, "VideoPacket({}, {})", packet.stream_seq(), packet.seq())
}
AttachmentEvent::DroppedVideoPacket(dropped) => {
write!(
f,
"DroppedVideoPacket({}, {}, layer={})",
dropped.stream_seq, dropped.seq, dropped.hierarchical_layer
)
}
AttachmentEvent::AudioStreamStart(stream_seq, _) => {
write!(f, "AudioStreamStart({})", stream_seq)
}
AttachmentEvent::AudioPacket(packet) => {
write!(f, "AudioPacket({}, {})", packet.stream_seq(), packet.seq())
}
AttachmentEvent::UpdateCursor { icon, image, .. } => {
let len = image.as_ref().map(|img| img.len()).unwrap_or_default();
write!(f, "UpdateCursor({icon:?} image_len={len})",)
}
AttachmentEvent::LockPointer(x, y) => {
write!(f, "LockPointer({}, {})", x, y)
}
AttachmentEvent::ReleasePointer => {
write!(f, "ReleasePointer()")
}
AttachmentEvent::DisplayParamsChanged {
reattach_required, ..
} => {
write!(f, "DisplayParamsChanged(reattach={})", reattach_required)
}
AttachmentEvent::AttachmentEnded => {
write!(f, "AttachmentEnded")
}
}
}
}
impl<T: From<AttachmentEvent> + std::fmt::Debug + Send + 'static> client::AttachmentDelegate
for AttachmentProxy<T>
{
fn video_stream_start(&self, stream_seq: u64, params: client::VideoStreamParams) {
self.proxy(AttachmentEvent::VideoStreamStart(stream_seq, params))
}
fn video_packet(&self, packet: Arc<client::Packet>) {
self.proxy(AttachmentEvent::VideoPacket(packet))
}
fn dropped_video_packet(&self, dropped: client::DroppedPacket) {
self.proxy(AttachmentEvent::DroppedVideoPacket(dropped))
}
fn audio_stream_start(&self, stream_seq: u64, params: client::AudioStreamParams) {
self.proxy(AttachmentEvent::AudioStreamStart(stream_seq, params))
}
fn audio_packet(&self, packet: Arc<client::Packet>) {
self.proxy(AttachmentEvent::AudioPacket(packet))
}
fn update_cursor(
&self,
icon: client::input::CursorIcon,
image: Option<Vec<u8>>,
hotspot_x: u32,
hotspot_y: u32,
) {
self.proxy(AttachmentEvent::UpdateCursor {
icon,
image,
hotspot_x,
hotspot_y,
})
}
fn lock_pointer(&self, x: f64, y: f64) {
self.proxy(AttachmentEvent::LockPointer(x, y))
}
fn release_pointer(&self) {
self.proxy(AttachmentEvent::ReleasePointer)
}
fn display_params_changed(
&self,
params: client::display_params::DisplayParams,
reattach_required: bool,
) {
self.proxy(AttachmentEvent::DisplayParamsChanged {
params,
reattach_required,
})
}
fn error(&self, err: client::ClientError) {
error!("error: {err:?}");
self.proxy(AttachmentEvent::AttachmentEnded)
}
fn attachment_ended(&self) {
self.proxy(AttachmentEvent::AttachmentEnded)
}
}
================================================
FILE: mm-client/src/flash.rs
================================================
// Copyright 2024 Colin Marc <hi@colinmarc.com>
//
// SPDX-License-Identifier: MIT
use std::time;
const FLASH_DURATION: time::Duration = time::Duration::from_millis(1350);
const FADE_OUT_AFTER: time::Duration = time::Duration::from_millis(1000);
pub struct Flash {
message: Option<(String, time::Instant)>,
}
impl Flash {
pub fn new() -> Self {
Self { message: None }
}
pub fn set_message(&mut self, s: &str) {
self.message = Some((s.to_owned(), time::Instant::now()));
}
pub fn build(&mut self, ui: &imgui::Ui) -> anyhow::Result<()> {
if self.message.is_none() {
return Ok(());
}
let start = self.message.as_ref().unwrap().1;
if start.elapsed() > FLASH_DURATION {
self.message = None;
return Ok(());
}
let alpha = if start.elapsed() > FADE_OUT_AFTER {
let remaining = FLASH_DURATION - start.elapsed();
remaining.as_secs_f32() / (FLASH_DURATION - FADE_OUT_AFTER).as_secs_f32()
} else {
1.0
};
// Exponentially ease the alpha.
let alpha = alpha * alpha;
let _style_alpha = ui.push_style_var(imgui::StyleVar::Alpha(alpha));
let _style_border = ui.push_style_var(imgui::StyleVar::WindowBorderSize(0.0));
let [_width, height] = ui.io().display_size;
if let Some(_window) = ui
.window("flash")
.position([0.0, height], imgui::Condition::Always)
.position_pivot([0.0, 1.0])
.no_decoration()
.no_nav()
.movable(false)
.always_auto_resize(true)
.bg_alpha(0.5 * alpha)
.begin()
{
ui.set_window_font_scale(2.0);
ui.text(&self.message.as_ref().unwrap().0);
}
Ok(())
}
}
impl Default for Flash {
fn default() -> Self {
Self::new()
}
}
================================================
FILE: mm-client/src/font.rs
================================================
// Copyright 2024 Colin Marc <hi@colinmarc.com>
//
// SPDX-License-Identifier: MIT
use font_kit::{
family_name::FamilyName,
font::Font,
properties::{Properties, Weight},
source::SystemSource,
};
use tracing::debug;
pub fn load_ui_font() -> anyhow::Result<Font> {
let font = SystemSource::new()
.select_best_match(
&[FamilyName::Monospace, FamilyName::SansSerif],
Properties::new().weight(Weight::THIN),
)?
.load()?;
debug!("font: {:?}", font);
Ok(font)
}
// #[cfg(target_os = "macos")]
// pub fn load_ui_font() -> anyhow::Result<Font> {
// let ctf = core_text::font::new_ui_font_for_language();
// let font = unsafe { Font::from_native_font(ctf) };
// Ok(font)
// }
================================================
FILE: mm-client/src/gamepad.rs
================================================
// Copyright 2024 Colin Marc <hi@colinmarc.com>
//
// SPDX-License-Identifier: MIT
use std::{collections::HashMap, time};
use anyhow::{anyhow, bail};
use gilrs::{Event, EventType};
use mm_client_common::input::{
Gamepad, GamepadAxis, GamepadButton, GamepadButtonState, GamepadLayout,
};
use tracing::{debug, error, trace};
#[derive(Debug, Clone)]
pub enum GamepadEvent {
Available(Gamepad),
Unavailable(u64),
Input(u64, GamepadButton, GamepadButtonState),
Motion(u64, GamepadAxis, f64),
}
#[derive(Debug, Default, Clone, Copy)]
struct RemoteGamepad {
id: u64,
dpad: DpadState,
}
// Some gamepads treat the dpad as an axis, but we treat it as a
// bunch of buttons. Therefore, it requires a bit of special handling.
#[derive(Debug, Default, Clone, Copy)]
struct DpadState {
up: bool,
down: bool,
left: bool,
right: bool,
}
impl RemoteGamepad {
fn update_dpad<T>(
&mut self,
axis: gilrs::Axis,
value: f32,
proxy: &winit::event_loop::EventLoopProxy<T>,
) -> Result<(), winit::event_loop::EventLoopClosed<T>>
where
T: From<GamepadEvent> + Send,
{
let set_pressed = |state: &mut bool, button| {
if !*state {
proxy.send_event(
GamepadEvent::Input(self.id, button, GamepadButtonState::Pressed).into(),
)?;
}
*state = true;
Ok(())
};
let set_released = |state: &mut bool, button| {
if *state {
proxy.send_event(
GamepadEvent::Input(self.id, button, GamepadButtonState::Released).into(),
)?;
}
*state = false;
Ok(())
};
match axis {
gilrs::Axis::DPadX if value == 0.0 => {
set_released(&mut self.dpad.left, GamepadButton::DpadLeft)?;
set_released(&mut self.dpad.right, GamepadButton::DpadRight)?;
}
gilrs::Axis::DPadX if value < 0.0 => {
set_pressed(&mut self.dpad.left, GamepadButton::DpadLeft)?;
set_released(&mut self.dpad.right, GamepadButton::DpadRight)?;
}
gilrs::Axis::DPadX if value > 0.0 => {
set_released(&mut self.dpad.left, GamepadButton::DpadLeft)?;
set_pressed(&mut self.dpad.right, GamepadButton::DpadRight)?;
}
gilrs::Axis::DPadY if value == 0.0 => {
set_released(&mut self.dpad.up, GamepadButton::DpadUp)?;
set_released(&mut self.dpad.down, GamepadButton::DpadDown)?;
}
gilrs::Axis::DPadY if value < 0.0 => {
set_pressed(&mut self.dpad.up, GamepadButton::DpadUp)?;
set_released(&mut self.dpad.down, GamepadButton::DpadDown)?;
}
gilrs::Axis::DPadY if value > 0.0 => {
set_released(&mut self.dpad.up, GamepadButton::DpadUp)?;
set_pressed(&mut self.dpad.down, GamepadButton::DpadDown)?;
}
_ => unreachable!(),
};
Ok(())
}
}
/// Spawns a thread to watch for gamepad events. Returns the initial list of
/// available gamepads.
pub fn spawn_gamepad_monitor<T>(
proxy: winit::event_loop::EventLoopProxy<T>,
) -> anyhow::Result<Vec<Gamepad>>
where
T: From<GamepadEvent> + Send,
{
let mut gilrs =
gilrs::Gilrs::new().map_err(|e| anyhow!("failed to create gilrs context: {e:?}"))?;
let (initial_tx, initial_rx) = oneshot::channel();
std::thread::spawn(move || {
let mut remote_gamepads = HashMap::new();
let mut initial = Vec::new();
for (id, pad) in gilrs.gamepads() {
let protocol_id = gamepad_id(pad.uuid());
let layout = layout(pad);
remote_gamepads.insert(
id,
RemoteGamepad {
id: protocol_id,
..Default::default()
},
);
initial.push(Gamepad {
id: protocol_id,
layout,
});
}
if initial_tx.send(initial).is_err() {
return;
}
loop {
let Some(Event { id, event: ev, .. }) = gilrs.next_event_blocking(None) else {
continue;
};
trace!(?id, ?ev, "gamepad event");
if let EventType::Disconnected = ev {
if let Some(pad) = remote_gamepads.remove(&id) {
if proxy
.send_event(GamepadEvent::Unavailable(pad.id).into())
.is_err()
{
break;
}
}
continue;
};
if let EventType::Connected = ev {
let Some(pad) = gilrs.connected_gamepad(id) else {
error!(?ev, "no gamepad matching event");
continue;
};
let protocol_id = gamepad_id(pad.uuid());
remote_gamepads.insert(
id,
RemoteGamepad {
id: protocol_id,
..Default::default()
},
);
if proxy
.send_event(
GamepadEvent::Available(Gamepad {
id: protocol_id,
layout: layout(pad),
})
.into(),
)
.is_err()
{
break;
}
continue;
}
let pad = remote_gamepads.get_mut(&id).unwrap();
if handle_gilrs_event(&proxy, pad, ev).is_err() {
break;
};
}
});
match initial_rx.recv_timeout(time::Duration::from_secs(1)) {
Ok(initial) => Ok(initial),
Err(_) => bail!("gamepad monitor thread panicked"),
}
}
fn handle_gilrs_event<T>(
proxy: &winit::event_loop::EventLoopProxy<T>,
pad: &mut RemoteGamepad,
ev: gilrs::EventType,
) -> Result<(), winit::event_loop::EventLoopClosed<T>>
where
T: From<GamepadEvent> + Send,
{
let gev = match ev {
EventType::ButtonPressed(button, _) => {
input_event(pad.id, button, GamepadButtonState::Pressed)
}
EventType::ButtonReleased(button, _) => {
input_event(pad.id, button, GamepadButtonState::Released)
}
EventType::AxisChanged(axis, mut value, _) => {
// Some gamepads treat the dpad as an axis. The protocol
// treats it as a bunch of buttons.
if matches!(axis, gilrs::Axis::DPadX | gilrs::Axis::DPadY) {
pad.update_dpad(axis, value, proxy)?;
return Ok(());
}
let Some(axis) = girls_axis_to_proto(axis) else {
debug!(?ev, "skipping unknown axis event");
return Ok(());
};
// Gilrs treats 1.0 as up.
if matches!(axis, GamepadAxis::LeftY | GamepadAxis::RightY) {
value *= -1.0;
}
Some(GamepadEvent::Motion(pad.id, axis, value as _))
}
EventType::ButtonChanged(button, value, _) => {
// Not sure why gilrs doesn't consider this an axis.
match button {
gilrs::Button::LeftTrigger2 => Some(GamepadEvent::Motion(
pad.id,
GamepadAxis::LeftTrigger,
value.max(0.0) as _,
)),
gilrs::Button::RightTrigger2 => Some(GamepadEvent::Motion(
pad.id,
GamepadAxis::RightTrigger,
value.max(0.0) as _,
)),
_ => None,
}
}
EventType::Dropped => None,
// TODO: do we need these?
EventType::ButtonRepeated(_, _) => None,
// Handled above.
EventType::Connected | EventType::Disconnected => unreachable!(),
};
if let Some(ev) = gev {
proxy.send_event(ev.into())?;
} else {
debug!(?ev, "ignoring gamepad event")
}
Ok(())
}
fn input_event(
protocol_id: u64,
button: gilrs::Button,
state: GamepadButtonState,
) -> Option<GamepadEvent> {
gilrs_button_to_proto(button).map(|button| GamepadEvent::Input(protocol_id, button, state))
}
fn gamepad_id(uuid: [u8; 16]) -> u64 {
// Truncating a UUID is squicky, but serves our purposes fine.
let (_, last_64) = uuid.split_at(8);
let last_64: [u8; 8] = last_64.try_into().unwrap();
u64::from_ne_bytes(last_64)
}
fn layout(pad: gilrs::Gamepad) -> GamepadLayout {
match pad.vendor_id() {
Some(0x54c) => GamepadLayout::SonyDualshock,
_ => GamepadLayout::GenericDualStick,
}
}
fn girls_axis_to_proto(axis: gilrs::Axis) -> Option<GamepadAxis> {
let axis = match axis {
gilrs::Axis::LeftStickX => GamepadAxis::LeftX,
gilrs::Axis::LeftStickY => GamepadAxis::LeftY,
gilrs::Axis::RightStickX => GamepadAxis::RightX,
gilrs::Axis::RightStickY => GamepadAxis::RightY,
gilrs::Axis::LeftZ => GamepadAxis::RightTrigger,
gilrs::Axis::RightZ => GamepadAxis::RightTrigger,
_ => return None,
};
Some(axis)
}
fn gilrs_button_to_proto(button: gilrs::Button) -> Option<GamepadButton> {
let button = match button {
gilrs::Button::South => GamepadButton::South,
gilrs::Button::East => GamepadButton::East,
gilrs::Button::North => GamepadButton::North,
gilrs::Button::West => GamepadButton::West,
gilrs::Button::C => GamepadButton::C,
gilrs::Button::Z => GamepadButton::Z,
gilrs::Button::LeftTrigger => GamepadButton::ShoulderLeft,
gilrs::Button::LeftTrigger2 => GamepadButton::TriggerLeft,
gilrs::Button::RightTrigger => GamepadButton::ShoulderRight,
gilrs::Button::RightTrigger2 => GamepadButton::TriggerRight,
gilrs::Button::Select => GamepadButton::Select,
gilrs::Button::Start => GamepadButton::Start,
gilrs::Button::Mode => GamepadButton::Logo,
gilrs::Button::LeftThumb => GamepadButton::JoystickLeft,
gilrs::Button::RightThumb => GamepadButton::JoystickRight,
gilrs::Button::DPadUp => GamepadButton::DpadUp,
gilrs::Button::DPadDown => GamepadButton::DpadDown,
gilrs::Button::DPadLeft => GamepadButton::DpadLeft,
gilrs::Button::DPadRight => GamepadButton::DpadRight,
_ => return None,
};
Some(button)
}
================================================
FILE: mm-client/src/keys.rs
================================================
// Copyright 2024 Colin Marc <hi@colinmarc.com>
//
// SPDX-License-Identifier: MIT
use mm_protocol::keyboard_input::Key;
use winit::keyboard::KeyCode;
pub fn winit_key_to_proto(key: KeyCode) -> Key {
match key {
KeyCode::Backquote => Key::Backquote,
KeyCode::Backslash => Key::Backslash,
KeyCode::BracketLeft => Key::BracketLeft,
KeyCode::BracketRight => Key::BracketRight,
KeyCode::Comma => Key::Comma,
KeyCode::Digit0 => Key::Digit0,
KeyCode::Digit1 => Key::Digit1,
KeyCode::Digit2 => Key::Digit2,
KeyCode::Digit3 => Key::Digit3,
KeyCode::Digit4 => Key::Digit4,
KeyCode::Digit5 => Key::Digit5,
KeyCode::Digit6 => Key::Digit6,
KeyCode::Digit7 => Key::Digit7,
KeyCode::Digit8 => Key::Digit8,
KeyCode::Digit9 => Key::Digit9,
KeyCode::Equal => Key::Equal,
KeyCode::IntlBackslash => Key::IntlBackslash,
KeyCode::IntlRo => Key::IntlRo,
KeyCode::IntlYen => Key::IntlYen,
KeyCode::KeyA => Key::A,
KeyCode::KeyB => Key::B,
KeyCode::KeyC => Key::C,
KeyCode::KeyD => Key::D,
KeyCode::KeyE => Key::E,
KeyCode::KeyF => Key::F,
KeyCode::KeyG => Key::G,
KeyCode::KeyH => Key::H,
KeyCode::KeyI => Key::I,
KeyCode::KeyJ => Key::J,
KeyCode::KeyK => Key::K,
KeyCode::KeyL => Key::L,
KeyCode::KeyM => Key::M,
KeyCode::KeyN => Key::N,
KeyCode::KeyO => Key::O,
KeyCode::KeyP => Key::P,
KeyCode::KeyQ => Key::Q,
KeyCode::KeyR => Key::R,
KeyCode::KeyS => Key::S,
KeyCode::KeyT => Key::T,
KeyCode::KeyU => Key::U,
KeyCode::KeyV => Key::V,
KeyCode::KeyW => Key::W,
KeyCode::KeyX => Key::X,
KeyCode::KeyY => Key::Y,
KeyCode::KeyZ => Key::Z,
KeyCode::Minus => Key::Minus,
KeyCode::Period => Key::Period,
KeyCode::Quote => Key::Quote,
KeyCode::Semicolon => Key::Semicolon,
KeyCode::Slash => Key::Slash,
KeyCode::AltLeft => Key::AltLeft,
KeyCode::AltRight => Key::AltRight,
KeyCode::Backspace => Key::Backspace,
KeyCode::CapsLock => Key::CapsLock,
KeyCode::ContextMenu => Key::ContextMenu,
KeyCode::ControlLeft => Key::ControlLeft,
KeyCode::ControlRight => Key::ControlRight,
KeyCode::Enter => Key::Enter,
KeyCode::SuperLeft => Key::MetaLeft,
KeyCode::SuperRight => Key::MetaRight,
KeyCode::ShiftLeft => Key::ShiftLeft,
KeyCode::ShiftRight => Key::ShiftRight,
KeyCode::Space => Key::Space,
KeyCode::Tab => Key::Tab,
KeyCode::Convert => Key::Convert,
KeyCode::KanaMode => Key::KanaMode,
KeyCode::Lang1 => Key::Lang1,
KeyCode::Lang2 => Key::Lang2,
KeyCode::Lang3 => Key::Lang3,
KeyCode::Lang4 => Key::Lang4,
KeyCode::Lang5 => Key::Lang5,
KeyCode::NonConvert => Key::NonConvert,
KeyCode::Delete => Key::Delete,
KeyCode::End => Key::End,
KeyCode::Help => Key::Help,
KeyCode::Home => Key::Home,
KeyCode::Insert => Key::Insert,
KeyCode::PageDown => Key::PageDown,
KeyCode::PageUp => Key::PageUp,
KeyCode::ArrowDown => Key::ArrowDown,
KeyCode::ArrowLeft => Key::ArrowLeft,
KeyCode::ArrowRight => Key::ArrowRight,
KeyCode::ArrowUp => Key::ArrowUp,
KeyCode::NumLock => Key::NumLock,
KeyCode::Numpad0 => Key::Numpad0,
KeyCode::Numpad1 => Key::Numpad1,
KeyCode::Numpad2 => Key::Numpad2,
KeyCode::Numpad3 => Key::Numpad3,
KeyCode::Numpad4 => Key::Numpad4,
KeyCode::Numpad5 => Key::Numpad5,
KeyCode::Numpad6 => Key::Numpad6,
KeyCode::Numpad7 => Key::Numpad7,
KeyCode::Numpad8 => Key::Numpad8,
KeyCode::Numpad9 => Key::Numpad9,
KeyCode::NumpadAdd => Key::NumpadAdd,
KeyCode::NumpadBackspace => Key::NumpadBackspace,
KeyCode::NumpadClear => Key::NumpadClear,
KeyCode::NumpadClearEntry => Key::NumpadClearEntry,
KeyCode::NumpadComma => Key::NumpadComma,
KeyCode::NumpadDecimal => Key::NumpadDecimal,
KeyCode::NumpadDivide => Key::NumpadDivide,
KeyCode::NumpadEnter => Key::NumpadEnter,
KeyCode::NumpadEqual => Key::NumpadEqual,
KeyCode::NumpadHash => Key::NumpadHash,
KeyCode::NumpadMemoryAdd => Key::NumpadMemoryAdd,
KeyCode::NumpadMemoryClear => Key::NumpadMemoryClear,
KeyCode::NumpadMemoryRecall => Key::NumpadMemoryRecall,
KeyCode::NumpadMemoryStore => Key::NumpadMemoryStore,
KeyCode::NumpadMultiply => Key::NumpadMultiply,
KeyCode::NumpadParenLeft => Key::NumpadParenLeft,
KeyCode::NumpadParenRight => Key::NumpadParenRight,
KeyCode::NumpadSubtract => Key::NumpadSubtract,
KeyCode::Escape => Key::Escape,
KeyCode::F1 => Key::F1,
KeyCode::F2 => Key::F2,
KeyCode::F3 => Key::F3,
KeyCode::F4 => Key::F4,
KeyCode::F5 => Key::F5,
KeyCode::F6 => Key::F6,
KeyCode::F7 => Key::F7,
KeyCode::F8 => Key::F8,
KeyCode::F9 => Key::F9,
KeyCode::F10 => Key::F10,
KeyCode::F11 => Key::F11,
KeyCode::F12 => Key::F12,
KeyCode::Fn => Key::Fn,
KeyCode::FnLock => Key::FnLock,
KeyCode::PrintScreen => Key::PrintScreen,
KeyCode::ScrollLock => Key::ScrollLock,
KeyCode::Pause => Key::Pause,
KeyCode::Hiragana => Key::Hiragana,
KeyCode::Katakana => Key::Katakana,
_ => Key::Unknown,
}
}
================================================
FILE: mm-client/src/lib.rs
================================================
// Copyright 2024 Colin Marc <hi@colinmarc.com>
//
// SPDX-License-Identifier: MIT
pub mod audio;
pub mod cursor;
pub mod delegate;
pub mod flash;
pub mod font;
pub mod gamepad;
pub mod keys;
pub mod overlay;
pub mod render;
pub mod stats;
pub mod video;
pub mod vulkan;
================================================
FILE: mm-client/src/overlay.rs
================================================
// Copyright 2024 Colin Marc <hi@colinmarc.com>
//
// SPDX-License-Identifier: MIT
use std::{collections::VecDeque, vec};
use mm_protocol as protocol;
use crate::stats::STATS;
pub struct Overlay {
streaming_width: u32,
streaming_height: u32,
codec: protocol::VideoCodec,
video_latency_measurements: VecDeque<f32>,
reposition: bool,
}
impl Overlay {
pub fn new(fps: u32) -> Self {
Self {
streaming_width: 0,
streaming_height: 0,
codec: protocol::VideoCodec::H264,
video_latency_measurements: VecDeque::from(vec![0.0; 10 * fps as usize]),
reposition: true,
}
}
pub fn reposition(&mut self) {
self.reposition = true;
}
pub fn update_params(&mut self, params: &protocol::Attached) {
self.streaming_width = params.streaming_resolution.as_ref().unwrap().width;
self.streaming_height = params.streaming_resolution.as_ref().unwrap().height;
self.codec = params.video_codec();
}
pub fn build(&mut self, ui: &imgui::Ui) -> anyhow::Result<()> {
// Record a latency measurement.
let latency = STATS.video_latency();
self.video_latency_measurements.rotate_left(1);
*self.video_latency_measurements.back_mut().unwrap() = latency;
let [width, height] = ui.io().display_size;
let [scale_x, scale_y] = ui.io().display_framebuffer_scale;
let condition = if self.reposition {
self.reposition = false;
imgui::Condition::Always
} else {
imgui::Condition::Once
};
let _padding = ui.push_style_var(imgui::StyleVar::WindowPadding([8.0, 8.0]));
let _rounding = ui.push_style_var(imgui::StyleVar::WindowRounding(4.0));
let _frame_rounding = ui.push_style_var(imgui::StyleVar::FrameRounding(4.0));
if let Some(_window) = ui
.window("overlay")
.position([width - 16.0, 16.0], condition)
.position_pivot([1.0, 0.0])
.title_bar(false)
.scroll_bar(false)
.no_nav()
.movable(true)
.resizable(true)
.bg_alpha(0.8)
.begin()
{
ui.set_window_font_scale(1.5);
let _stretch = ui.push_item_width(-1.0);
if let Some(_table) =
ui.begin_table_with_flags("stats", 2, imgui::TableFlags::SIZING_FIXED_FIT)
{
stat_row(
ui,
"streaming res:",
format!("{}x{}", self.streaming_width, self.streaming_height),
);
stat_row(
ui,
"render res:",
format!("{}x{}", width * scale_x, height * scale_y),
);
stat_row(
ui,
"codec:",
match self.codec {
protocol::VideoCodec::H264 => "H.264",
protocol::VideoCodec::H265 => "H.265",
protocol::VideoCodec::Av1 => "AV1",
_ => "unknown",
},
);
stat_row(
ui,
"bitrate:",
format!("{:.1} mbps", STATS.video_bitrate() / 1_000_000.0),
);
}
let [width, height] = ui.window_size();
let cursor_pos = ui.cursor_pos();
let measurements = self.video_latency_measurements.make_contiguous();
let max_latency = measurements.iter().copied().reduce(f32::max).unwrap();
let scale = (max_latency.round() as u32).next_multiple_of(10) * 2;
ui.plot_lines("", measurements)
.scale_min(0.0)
.scale_max(scale as f32)
.graph_size([width - 16.0, 50.0_f32.max(height - cursor_pos[1] - 8.0)])
.overlay_text(format!("latency: {:.1} ms", latency).as_str())
.build();
}
Ok(())
}
}
fn stat_row(ui: &imgui::Ui, label: impl AsRef<str>, value: impl AsRef<str>) {
ui.table_next_row();
ui.table_next_column();
let cursor_pos = ui.cursor_pos();
let pos_x = cursor_pos[0] + ui.column_width(0) - ui.calc_text_size(&label)[0];
if pos_x > cursor_pos[0] {
ui.set_cursor_pos([pos_x, cursor_pos[1]]);
}
ui.text_colored([0.6, 0.6, 0.6, 1.0], label);
ui.table_next_column();
ui.text(value);
}
================================================
FILE: mm-client/src/render.rs
================================================
// Copyright 2024 Colin Marc <hi@colinmarc.com>
//
// SPDX-License-Identifier: MIT
#![allow(clippy::missing_safety_doc)]
use std::sync::Arc;
use std::time;
use anyhow::{anyhow, Context, Result};
use ash::vk;
use cstr::cstr;
use imgui_rs_vulkan_renderer as imgui_vulkan;
use tracing::debug;
use tracing::instrument;
use tracing::trace;
use tracing::trace_span;
use tracing::warn;
use tracy_client::span_location;
use crate::font;
use crate::video::*;
use crate::vulkan::*;
const FONT_SIZE: f32 = 8.0;
// Matches the definition in render.slang.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[repr(u32)]
enum TextureColorSpace {
Bt709 = 0,
Bt2020Pq = 1,
}
impl From<crate::video::ColorSpace> for TextureColorSpace {
fn from(cs: crate::video::ColorSpace) -> Self {
match cs {
crate::video::ColorSpace::Bt709 => TextureColorSpace::Bt709,
crate::video::ColorSpace::Bt2020Pq => TextureColorSpace::Bt2020Pq,
}
}
}
#[derive(Copy, Clone, Debug)]
#[repr(C)]
struct PushConstants {
aspect: glam::Vec2,
texture_color_space: TextureColorSpace,
output_color_space: vk::ColorSpaceKHR,
}
pub struct Renderer {
width: u32,
height: u32,
scale_factor: f64,
hdr_mode: bool,
imgui: imgui::Context,
imgui_platform: imgui_winit_support::WinitPlatform,
imgui_font: font_kit::font::Font,
imgui_fontid_big: imgui::FontId,
imgui_time: time::Instant,
swapchain: Option<Swapchain>,
swapchain_dirty: bool,
new_video_texture: Option<(Arc<VkImage>, VideoStreamParams)>,
vk: Arc<VkContext>,
window: Arc<winit::window::Window>,
}
struct VideoTexture {
image: Arc<VkImage>,
view: vk::ImageView,
color_space: TextureColorSpace,
}
struct Swapchain {
swapchain: vk::SwapchainKHR,
frames: Vec<InFlightFrame>,
present_images: Vec<SwapImage>,
current_frame: usize,
sampler_conversion: vk::SamplerYcbcrConversion,
sampler: vk::Sampler,
bound_video_texture: Option<VideoTexture>,
/// The normalized relationship between the output and the video texture,
/// after scaling. For example, a 500x500 video texture in a 1000x500
/// swapchain would have the aspect (2.0, 1.0), as would a 250x250 texture.
aspect: (f64, f64),
surface_format: vk::SurfaceFormatKHR,
descriptor_set_layout: vk::DescriptorSetLayout,
descriptor_pool: vk::DescriptorPool,
pipeline_layout: vk::PipelineLayout,
pipeline: vk::Pipeline,
imgui_renderer: imgui_vulkan::Renderer,
}
struct InFlightFrame {
render_cb: vk::CommandBuffer,
render_fence: vk::Fence,
image_acquired_sema: vk::Semaphore,
render_complete_sema: vk::Semaphore,
descriptor_set: vk::DescriptorSet,
ts_pool: VkTimestampQueryPool,
tracy_span: Option<tracy_client::GpuSpan>,
}
struct SwapImage {
image: vk::Image,
view: vk::ImageView,
}
impl Renderer {
pub fn new(
vk: Arc<VkContext>,
window: Arc<winit::window::Window>,
hdr_mode: bool,
) -> Result<Self> {
let window_size = window.inner_size();
let scale_factor = window.scale_factor();
let mut imgui = imgui::Context::create();
imgui.set_ini_filename(None);
let mut imgui_platform = imgui_winit_support::WinitPlatform::new(&mut imgui);
imgui_platform.attach_window(
imgui.io_mut(),
&window,
imgui_winit_support::HiDpiMode::Default,
);
let imgui_font = font::load_ui_font()?;
let imgui_fontid_big = import_imgui_font(&mut imgui, &imgui_font, FONT_SIZE, scale_factor)?;
let mut renderer = Self {
width: window_size.width,
height: window_size.height,
scale_factor,
hdr_mode,
window,
imgui,
imgui_platform,
imgui_font,
imgui_fontid_big,
imgui_time: time::Instant::now(),
swapchain: None,
swapchain_dirty: false,
new_video_texture: None,
vk,
};
unsafe { renderer.recreate_swapchain()? };
Ok(renderer)
}
#[instrument(skip_all, level = "trace")]
unsafe fn recreate_swapchain(&mut self) -> Result<()> {
let start = time::Instant::now();
let device = &self.vk.device;
let surface_format = select_surface_format(self.vk.clone(), self.hdr_mode)?;
let surface_capabilities = self
.vk
.surface_loader
.get_physical_device_surface_capabilities(self.vk.pdevice, self.vk.surface)
.unwrap();
let mut desired_image_count = surface_capabilities.min_image_count + 1;
if surface_capabilities.max_image_count > 0
&& desired_image_count > surface_capabilities.max_image_count
{
desired_image_count = surface_capabilities.max_image_count;
}
let surface_resolution = match surface_capabilities.current_extent.width {
std::u32::MAX => vk::Extent2D {
width: self.width,
height: self.height,
},
_ => surface_capabilities.current_extent,
};
let pre_transform = if surface_capabilities
.supported_transforms
.contains(vk::SurfaceTransformFlagsKHR::IDENTITY)
{
vk::SurfaceTransformFlagsKHR::IDENTITY
} else {
surface_capabilities.current_transform
};
let present_modes = self
.vk
.surface_loader
.get_physical_device_surface_present_modes(self.vk.pdevice, self.vk.surface)
.unwrap();
let mut present_modes = present_modes.clone();
present_modes.sort_by_key(|&mode| match mode {
vk::PresentModeKHR::MAILBOX => 0,
vk::PresentModeKHR::FIFO => 1,
vk::PresentModeKHR::IMMEDIATE => 2,
_ => 4,
});
let present_mode = present_modes.first().unwrap();
if *present_mode != vk::PresentModeKHR::MAILBOX {
warn!(
"present mode MAILBOX not available, using {:?} (available: {:?})",
present_mode, present_modes
);
}
let mut swapchain_create_info = vk::SwapchainCreateInfoKHR::default()
.surface(self.vk.surface)
.min_image_count(desired_image_count)
.image_color_space(surface_format.color_space)
.image_format(surface_format.format)
.image_extent(surface_resolution)
.image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT)
.image_sharing_mode(vk::SharingMode::EXCLUSIVE)
.pre_transform(pre_transform)
.composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE)
.present_mode(*present_mode)
.clipped(true)
.image_array_layers(1);
if let Some(old_swapchain) = self.swapchain.as_ref() {
swapchain_create_info = swapchain_create_info.old_swapchain(old_swapchain.swapchain);
}
let swapchain = self
.vk
.swapchain_loader
.create_swapchain(&swapchain_create_info, None)?;
let swapchain_images = self.vk.swapchain_loader.get_swapchain_images(swapchain)?;
// TODO: rather than recreate the swapchain if the video texture
// changes, we can just recreate the pipeline. This is tricky because
// we create a descriptor set for each SwapFrame, which refers to the
// layout, which includes the immutable sampler.
// We need to create a sampler, even if we don't have a video stream yet
// and don't know what the fields should be.
let (video_texture_format, video_params) = match self.new_video_texture.as_ref() {
Some((tex, params)) => (tex.format, *params),
None => (
vk::Format::G8_B8_R8_3PLANE_420_UNORM,
VideoStreamParams::default(),
),
};
let sampler_conversion =
create_ycbcr_sampler_conversion(device, video_texture_format, &video_params)?;
let sampler = {
let mut conversion_info =
vk::SamplerYcbcrConversionInfo::default().conversion(sampler_conversion);
let create_info = vk::SamplerCreateInfo::default()
.mag_filter(vk::Filter::LINEAR)
.min_filter(vk::Filter::LINEAR)
.compare_enable(true)
.address_mode_u(vk::SamplerAddressMode::CLAMP_TO_EDGE)
.address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE)
.address_mode_w(vk::SamplerAddressMode::CLAMP_TO_EDGE)
.push_next(&mut conversion_info);
unsafe { device.create_sampler(&create_info, None)? }
};
let bound_video_texture = if let Some((tex, params)) = self.new_video_texture.as_ref() {
let view = create_image_view(
&self.vk.device,
tex.image,
tex.format,
Some(sampler_conversion),
)?;
// Increment the reference count on the texture.
Some(VideoTexture {
image: tex.clone(),
view,
color_space: params.color_space.into(),
})
} else {
None
};
let aspect = if let Some(tex) = bound_video_texture.as_ref() {
calculate_aspect(self.width, self.height, tex.image.width, tex.image.height)
} else {
(1.0, 1.0)
};
let descriptor_set_layout = {
// We're required to use an immutable sampler for YCbCr conversion
// by the vulkan spec.
let samplers = [sampler];
let binding = vk::DescriptorSetLayoutBinding::default()
.binding(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
.immutable_samplers(&samplers);
unsafe {
device.create_descriptor_set_layout(
&vk::DescriptorSetLayoutCreateInfo::default().bindings(&[binding]),
None,
)?
}
};
let descriptor_pool = {
let binding_multiplier = get_ycbcr_conversion_properties(
self.vk.pdevice,
&self.vk.instance,
video_texture_format,
)?
.combined_image_sampler_descriptor_count;
let sampler_size = [vk::DescriptorPoolSize::default()
.ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(swapchain_images.len() as u32 * binding_multiplier)];
let info = vk::DescriptorPoolCreateInfo::default()
.pool_sizes(&sampler_size)
.max_sets(swapchain_images.len() as u32);
unsafe { device.create_descriptor_pool(&info, None)? }
};
let pipeline_layout = {
let pc_ranges = [vk::PushConstantRange::default()
.stage_flags(vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT)
.offset(0)
.size(std::mem::size_of::<PushConstants>() as u32)];
let set_layouts = [descriptor_set_layout];
let create_info = vk::PipelineLayoutCreateInfo::default()
.set_layouts(&set_layouts)
.push_constant_ranges(&pc_ranges);
unsafe { device.create_pipeline_layout(&create_info, None)? }
};
let pipeline = {
let vert_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/shaders/vert.spv"));
let frag_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/shaders/frag.spv"));
let vert_shader = load_shader(device, vert_bytes).context("loading vert.spv")?;
let frag_shader = load_shader(device, frag_bytes).context("loading frag.spv")?;
let vert_stage = vk::PipelineShaderStageCreateInfo::default()
.stage(vk::ShaderStageFlags::VERTEX)
.module(vert_shader)
.name(cstr!("main"));
let frag_stage = vk::PipelineShaderStageCreateInfo::default()
.stage(vk::ShaderStageFlags::FRAGMENT)
.module(frag_shader)
.name(cstr!("main"));
let vertex_input_state = vk::PipelineVertexInputStateCreateInfo::default();
let input_assembly_state = vk::PipelineInputAssemblyStateCreateInfo::default()
.topology(vk::PrimitiveTopology::TRIANGLE_STRIP)
.primitive_restart_enable(false);
let viewport = [vk::Viewport::default()
.x(0.0)
.y(0.0)
.width(self.width as f32)
.height(self.height as f32)
.min_depth(0.0)
.max_depth(1.0)];
let scissor = [vk::Rect2D::default().extent(vk::Extent2D {
width: self.width,
height: self.height,
})];
let viewport_state = vk::PipelineViewportStateCreateInfo::default()
.viewports(&viewport)
.scissors(&scissor);
let rasterization_state = vk::PipelineRasterizationStateCreateInfo::default()
.depth_clamp_enable(false)
.rasterizer_discard_enable(false)
.polygon_mode(vk::PolygonMode::FILL)
.line_width(1.0)
.depth_bias_enable(false)
// Per https://www.saschawillems.de/blog/2016/08/13/vulkan-tutorial-on-rendering-a-fullscreen-quad-without-buffers
.cull_mode(vk::CullModeFlags::FRONT)
.front_face(vk::FrontFace::COUNTER_CLOCKWISE);
let multisample_state = vk::PipelineMultisampleStateCreateInfo::default()
.sample_shading_enable(false)
.rasterization_samples(vk::SampleCountFlags::TYPE_1);
let attachment = [vk::PipelineColorBlendAttachmentState::default()
.color_write_mask(vk::ColorComponentFlags::RGBA)
.blend_enable(true)
.src_color_blend_factor(vk::BlendFactor::SRC_ALPHA)
.dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
.color_blend_op(vk::BlendOp::ADD)
.src_alpha_blend_factor(vk::BlendFactor::ONE)
.dst_alpha_blend_factor(vk::BlendFactor::ZERO)
.alpha_blend_op(vk::BlendOp::ADD)];
let color_blend_state = vk::PipelineColorBlendStateCreateInfo::default()
.logic_op_enable(false)
.attachments(&attachment);
let formats = [surface_format.format];
let mut pipeline_rendering =
vk::PipelineRenderingCreateInfo::default().color_attachment_formats(&formats);
let stages = [vert_stage, frag_stage];
let create_info = vk::GraphicsPipelineCreateInfo::default()
.stages(&stages)
.vertex_input_state(&vertex_input_state)
.input_assembly_state(&input_assembly_state)
.viewport_state(&viewport_state)
.rasterization_state(&rasterization_state)
.multisample_state(&multisample_state)
.color_blend_state(&color_blend_state)
.layout(pipeline_layout)
.push_next(&mut pipeline_rendering);
unsafe {
let pipeline = match device.create_graphics_pipelines(
vk::PipelineCache::null(),
&[create_info],
None,
) {
Ok(pipelines) => Ok(pipelines[0]),
Err((_, e)) => Err(e),
}?;
device.destroy_shader_module(vert_shader, None);
device.destroy_shader_module(frag_shader, None);
pipeline
}
};
let create_frame = || -> Result<InFlightFrame> {
let render_cb = {
let create_info = vk::CommandBufferAllocateInfo::default()
.level(vk::CommandBufferLevel::PRIMARY)
.command_pool(self.vk.present_queue.command_pool)
.command_buffer_count(1);
let cbs = device
.allocate_command_buffers(&create_info)
.context("failed to allocate render command buffer")?;
cbs[0]
};
let descriptor_set = {
let layouts = &[descriptor_set_layout];
let create_info = vk::DescriptorSetAllocateInfo::default()
.descriptor_pool(descriptor_pool)
.set_layouts(layouts);
let ds = device
.allocate_descriptor_sets(&create_info)?
.pop()
.unwrap();
// TODO: do the write in bind_video_texture?
if let Some(tex) = bound_video_texture.as_ref() {
let info = [vk::DescriptorImageInfo::default()
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.image_view(tex.view)];
let sampler_write = vk::WriteDescriptorSet::default()
.dst_set(ds)
.dst_binding(0)
.dst_array_element(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(&info);
device.update_descriptor_sets(&[sampler_write], &[]);
}
ds
};
let render_fence = create_fence(device, true)?;
let image_acquired_sema = create_semaphore(device)?;
let render_complete_sema = create_semaphore(device)?;
let ts_pool = create_timestamp_query_pool(device, 2)?;
Ok(InFlightFrame {
render_cb,
render_fence,
image_acquired_sema,
render_complete_sema,
descriptor_set,
ts_pool,
tracy_span: None,
})
};
let frames = (0..swapchain_images.len())
.map(|_| create_frame())
.collect::<Result<Vec<_>>>()?;
let swapchain_images = swapchain_images
.into_iter()
.map(|image| {
let image_view = create_image_view(device, image, surface_format.format, None)?;
Ok(SwapImage {
image,
view: image_view,
})
})
.collect::<Result<Vec<_>>>()?;
let mut imgui_renderer = imgui_vulkan::Renderer::with_default_allocator(
&self.vk.instance,
self.vk.pdevice,
self.vk.device.clone(),
self.vk.present_queue.queue,
self.vk.present_queue.command_pool,
imgui_vulkan::DynamicRendering {
color_attachment_format: surface_format.format,
depth_attachment_format: None,
},
&mut self.imgui,
Some(imgui_vulkan::Options {
in_flight_frames: frames.len(),
..Default::default()
}),
)?;
imgui_renderer.update_fonts_texture(
self.vk.present_queue.queue,
self.vk.present_queue.command_pool,
&mut self.imgui,
)?;
let swapchain = Swapchain {
swapchain,
frames,
present_images: swapchain_images,
current_frame: 0,
descriptor_pool,
descriptor_set_layout,
sampler_conversion,
sampler,
bound_video_texture,
aspect,
surface_format,
pipeline_layout,
pipeline,
imgui_renderer,
};
debug!("recreated swapchain in {:?}", start.elapsed());
if let Some(old_swapchain) = self.swapchain.replace(swapchain) {
self.destroy_swapchain(old_swapchain);
};
Ok(())
}
pub fn handle_event(&mut self, event: &winit::event::WindowEvent) -> anyhow::Result<()> {
let now = time::Instant::now();
self.imgui.io_mut().update_delta_time(now - self.imgui_time);
self.imgui_time = now;
let wrapped: winit::event::Event<()> = winit::event::Event::WindowEvent {
window_id: self.window.id(),
event: event.clone(),
};
self.imgui_platform
.handle_event(self.imgui.io_mut(), self.window.as_ref(), &wrapped);
match event {
winit::event::WindowEvent::Resized(size) => {
self.resize(size.width, size.height);
}
winit::event::WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
self.scale_factor_changed(*scale_factor)?;
}
_ => (),
}
Ok(())
}
pub fn resize(&mut self, width: u32, height: u32) {
if self.width == width && self.height == height {
return;
}
self.width = width;
self.height = height;
self.swapchain_dirty = true;
}
fn scale_factor_changed(&mut self, scale_factor: f64) -> anyhow::Result<()> {
if self.scale_factor == scale_factor {
return Ok(());
}
// Resize fonts.
self.imgui_fontid_big =
import_imgui_font(&mut self.imgui, &self.imgui_font, FONT_SIZE, scale_factor)?;
self.scale_factor = scale_factor;
Ok(())
}
pub fn bind_video_texture(
&mut self,
texture: Arc<VkImage>,
params: VideoStreamParams,
) -> Result<()> {
// TODO: no need to recreate the sampler if the params match.
self.new_video_texture = Some((texture, params));
self.swapchain_dirty = true;
Ok(())
}
// Returns the normalized relationship between the output dimensions and the
// video texture dimensions, after scaling. For example, if the video
// texture is 250x250 and the output is 1000x500, the aspect would be (2.0,
// 1.0).
pub fn get_texture_aspect(&self) -> Option<(f64, f64)> {
if let Some(Swapchain {
bound_video_texture: Some(_),
aspect,
..
}) = self.swapchain.as_ref()
{
Some(*aspect)
} else {
None
}
}
#[instrument(skip_all, level = "trace")]
pub unsafe fn render<F>(&mut self, ui_builder: F) -> Result<()>
where
F: FnOnce(&imgui::Ui) -> anyhow::Result<()>,
{
if self.swapchain_dirty || self.swapchain.is_none() {
self.recreate_swapchain()?;
self.swapchain_dirty = false;
}
let device = &self.vk.device;
let swapchain = self.swapchain.as_mut().unwrap();
let num_frames = swapchain.frames.len();
let frame = &mut swapchain.frames[swapchain.current_frame];
swapchain.current_frame = (swapchain.current_frame + 1) % num_frames;
// Wait for the gpu to catch up.
device.wait_for_fences(&[frame.render_fence], true, u64::MAX)?;
// Trace the frame on the GPU side.
if let Some(ctx) = &self.vk.tracy_context {
if let Some(span) = frame.tracy_span.take() {
let timestamps = frame.ts_pool.fetch_results(device)?;
span.upload_timestamp(timestamps[0], timestamps[1]);
}
frame.tracy_span = Some(ctx.span(span_location!())?);
}
let result = self.vk.swapchain_loader.acquire_next_image(
swapchain.swapchain,
u64::MAX,
frame.image_acquired_sema,
vk::Fence::null(),
);
let swapchain_index = match result {
Ok((image_index, _)) => image_index,
Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
// Recreate and try again.
self.swapchain_dirty = true;
return self.render(ui_builder);
}
Err(e) => return Err(e.into()),
};
let present_image = swapchain
.present_images
.get(swapchain_index as usize)
.unwrap();
// Reset the command buffer.
device.reset_command_buffer(frame.render_cb, vk::CommandBufferResetFlags::empty())?;
// Begin the command buffer.
{
let begin_info = vk::CommandBufferBeginInfo::default()
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
device.begin_command_buffer(frame.render_cb, &begin_info)?;
}
// Record the start timestamp.
frame.ts_pool.cmd_reset(device, frame.render_cb);
device.cmd_write_timestamp(
frame.render_cb,
vk::PipelineStageFlags::TOP_OF_PIPE,
frame.ts_pool.pool,
0,
);
// Transition the present image to be writable.
cmd_image_barrier(
device,
frame.render_cb,
present_image.image,
vk::PipelineStageFlags::TOP_OF_PIPE,
vk::AccessFlags::empty(),
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
vk::ImageLayout::UNDEFINED,
vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
);
// Begin rendering.
{
let rect: vk::Rect2D = vk::Rect2D::default().extent(vk::Extent2D {
width: self.width,
height: self.height,
});
let clear_value = vk::ClearValue {
color: vk::ClearColorValue {
float32: [0.0, 0.0, 0.0, 1.0],
},
};
let color_attachment = vk::RenderingAttachmentInfo::default()
.image_view(present_image.view)
.image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
.load_op(vk::AttachmentLoadOp::CLEAR)
.store_op(vk::AttachmentStoreOp::STORE)
.clear_value(clear_value);
let color_attachments = [color_attachment];
let rendering_info = vk::RenderingInfo::default()
.render_area(rect)
.color_attachments(&color_attachments)
.layer_count(1);
self.vk
.dynamic_rendering_loader
.cmd_begin_rendering(frame.render_cb, &rendering_info);
device.cmd_bind_pipeline(
frame.render_cb,
vk::PipelineBindPoint::GRAPHICS,
swapchain.pipeline,
);
}
if self.new_video_texture.is_none() || swapchain.aspect != (1.0, 1.0) {
// TODO Draw the background
// https://www.toptal.com/designers/subtlepatterns/prism/
}
// Draw the video texture.
if let Some(tex) = &swapchain.bound_video_texture {
let pc = PushConstants {
aspect: glam::Vec2::new(swapchain.aspect.0 as f32, swapchain.aspect.1 as f32),
texture_color_space: tex.color_space,
output_color_space: swapchain.surface_format.color_space,
};
device.cmd_push_constants(
frame.render_cb,
swapchain.pipeline_layout,
vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
0,
std::slice::from_raw_parts(
&pc as *const _ as *const u8,
std::mem::size_of::<PushConstants>(),
),
);
device.cmd_bind_descriptor_sets(
frame.render_cb,
vk::PipelineBindPoint::GRAPHICS,
swapchain.pipeline_layout,
0,
&[frame.descriptor_set],
&[],
);
// Draw the video texture.
device.cmd_draw(frame.render_cb, 4, 1, 0, 0);
}
// Draw the overlay.
{
self.imgui_platform
.prepare_frame(self.imgui.io_mut(), &self.window)?;
{
let ui = self.imgui.new_frame();
let _font_stack = ui.push_font(self.imgui_fontid_big);
ui_builder(ui)?;
self.imgui_platform.prepare_render(ui, &self.window);
}
swapchain
.imgui_renderer
.cmd_draw(frame.render_cb, self.imgui.render())?;
};
// Done rendereng.
self.vk
.dynamic_rendering_loader
.cmd_end_rendering(frame.render_cb);
// Transition the present image to be presentable.
cmd_image_barrier(
device,
frame.render_cb,
present_image.image,
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
vk::PipelineStageFlags::BOTTOM_OF_PIPE,
vk::AccessFlags::empty(),
vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
vk::ImageLayout::PRESENT_SRC_KHR,
);
// Record the end timestamp.
device.cmd_write_timestamp(
frame.render_cb,
vk::PipelineStageFlags::BOTTOM_OF_PIPE,
frame.ts_pool.pool,
1,
);
if let Some(span) = &mut frame.tracy_span {
span.end_zone();
}
// Submit and present!
{
let present_queue = self.vk.present_queue.queue;
device.end_command_buffer(frame.render_cb)?;
device.reset_fences(&[frame.render_fence])?;
let cbs = [frame.render_cb];
let wait_semas = [frame.image_acquired_sema];
let signal_semas = [frame.render_complete_sema];
let wait_stages = [vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT];
let submit_info = vk::SubmitInfo::default()
.command_buffers(&cbs)
.wait_semaphores(&wait_semas)
.wait_dst_stage_mask(&wait_stages)
.signal_semaphores(&signal_semas);
trace!(queue = ?present_queue, "queue submit for render");
device.queue_submit(present_queue, &[submit_info], frame.render_fence)?;
// This "helps winit [with stuff]". It also seems to increase latency.
self.window.pre_present_notify();
trace!(queue = ?present_queue, index = swapchain_index, "queue present");
let wait_semas = [frame.render_complete_sema];
let swapchains = [swapchain.swapchain];
let image_indices = [swapchain_index];
let present_info = vk::PresentInfoKHR::default()
.wait_semaphores(&wait_semas)
.swapchains(&swapchains)
.image_indices(&image_indices);
let res = trace_span!("render.queue_present").in_scope(|| {
self.vk
.swapchain_loader
.queue_present(present_queue, &present_info)
});
self.swapchain_dirty = match res {
Ok(false) => false,
Ok(true) => true,
Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => true,
Err(e) => return Err(e.into()),
};
}
tracy_client::frame_mark();
Ok(())
}
unsafe fn destroy_swapchain(&mut self, mut swapchain: Swapchain) {
let device = &self.vk.device;
device.device_wait_idle().unwrap();
for frame in swapchain.frames.drain(..) {
device.free_command_buffers(self.vk.present_queue.command_pool, &[frame.render_cb]);
device.destroy_fence(frame.render_fence, None);
device.destroy_semaphore(frame.image_acquired_sema, None);
device.destroy_semaphore(frame.render_complete_sema, None);
device.destroy_query_pool(frame.ts_pool.pool, None);
}
for swap_img in swapchain.present_images.drain(..) {
// Destroying the swapchain does this.
// device.destroy_image(swap_img.image, None);
device.destroy_image_view(swap_img.view, None);
}
device.destroy_pipeline_layout(swapchain.pipeline_layout, None);
device.destroy_descriptor_pool(swapchain.descriptor_pool, None);
device.destroy_descriptor_set_layout(swapchain.descriptor_set_layout, None);
device.destroy_sampler(swapchain.sampler, None);
device.destroy_sampler_ycbcr_conversion(swapchain.sampler_conversion, None);
if let Some(tex) = swapchain.bound_video_texture.take() {
device.destroy_image_view(tex.view, None);
// We probably drop the last reference to the image here, which then
// gets destroyed.
}
device.destroy_pipeline(swapchain.pipeline, None);
self.vk
.swapchain_loader
.destroy_swapchain(swapchain.swapchain, None)
}
}
fn select_surface_format(
vk: Arc<VkContext>,
hdr_mode: bool,
) -> Result<vk::SurfaceFormatKHR, vk::Result> {
let mut surface_formats = unsafe {
vk.surface_loader
.get_physical_device_surface_formats(vk.pdevice, vk.surface)?
};
let preferred_formats = [
vk::Format::R16G16B16A16_SFLOAT,
vk::Format::R8G8B8A8_UNORM,
vk::Format::B8G8R8A8_UNORM,
];
let preferred_color_spaces = if hdr_mode {
vec![
vk::ColorSpaceKHR::HDR10_ST2084_EXT,
vk::ColorSpaceKHR::EXTENDED_SRGB_LINEAR_EXT,
vk::ColorSpaceKHR::DISPLAY_P3_NONLINEAR_EXT,
vk::ColorSpaceKHR::SRGB_NONLINEAR,
]
} else {
vec![
vk::ColorSpaceKHR::BT709_NONLINEAR_EXT,
vk::ColorSpaceKHR::SRGB_NONLINEAR,
]
};
surface_formats.sort_by_key(|sf| {
let color_space_score = preferred_color_spaces
.iter()
.position(|&cs| cs == sf.color_space)
.unwrap_or(preferred_color_spaces.len());
let format_score = preferred_formats
.iter()
.position(|&f| f == sf.format)
.unwrap_or(preferred_formats.len());
(color_space_score, format_score)
});
let surface_format = surface_formats[0];
debug!(?surface_format, "selected surface format");
Ok(surface_format)
}
impl Drop for Renderer {
fn drop(&mut self) {
if let Some(swapchain) = self.swapchain.take() {
unsafe {
self.destroy_swapchain(swapchain);
};
}
}
}
fn import_imgui_font(
imgui: &mut imgui::Context,
font: &font_kit::font::Font,
size: f32,
scale_factor: f64,
) -> anyhow::Result<imgui::FontId> {
let font_size = size * scale_factor as f32;
imgui.io_mut().font_global_scale = (1.0 / scale_factor) as f32;
let data = match font.copy_font_data() {
Some(data) => data,
None => return Err(anyhow!("failed to load font data for {:?}", font)),
};
let id = imgui.fonts().add_font(&[imgui::FontSource::TtfData {
size_pixels: font_size,
data: &data,
config: Some(imgui::FontConfig {
pixel_snap_h: true,
gitextract_32ud61vu/
├── .github/
│ ├── actions/
│ │ └── install-slang/
│ │ └── action.yaml
│ └── workflows/
│ ├── bump-version.yaml
│ ├── cliff.toml
│ ├── docs.yaml
│ ├── release-mmclient.yaml
│ ├── release-mmserver.yaml
│ └── tests.yaml
├── .gitignore
├── .gitmodules
├── .rustfmt.toml
├── BUILD.md
├── CHANGELOG.md
├── LICENSES/
│ ├── BUSL-1.1.txt
│ └── MIT.txt
├── README.md
├── auto-release.sh
├── docs/
│ ├── .gitignore
│ ├── config.toml
│ ├── content/
│ │ ├── _index.md
│ │ └── setup/
│ │ ├── client.md
│ │ └── server.md
│ └── templates/
│ └── footer.html
├── mm-client/
│ ├── Cargo.toml
│ ├── build.rs
│ └── src/
│ ├── audio/
│ │ └── buffer.rs
│ ├── audio.rs
│ ├── bin/
│ │ ├── latency-test.rs
│ │ └── mmclient.rs
│ ├── cursor.rs
│ ├── delegate.rs
│ ├── flash.rs
│ ├── font.rs
│ ├── gamepad.rs
│ ├── keys.rs
│ ├── lib.rs
│ ├── overlay.rs
│ ├── render.rs
│ ├── render.slang
│ ├── stats.rs
│ ├── video.rs
│ └── vulkan.rs
├── mm-client-common/
│ ├── Cargo.toml
│ ├── bin/
│ │ └── uniffi-bindgen.rs
│ └── src/
│ ├── attachment.rs
│ ├── codec.rs
│ ├── conn/
│ │ └── hostport.rs
│ ├── conn.rs
│ ├── display_params.rs
│ ├── input.rs
│ ├── lib.rs
│ ├── logging.rs
│ ├── packet/
│ │ └── ring.rs
│ ├── packet.rs
│ ├── pixel_scale.rs
│ ├── session.rs
│ ├── stats.rs
│ └── validation.rs
├── mm-docgen/
│ ├── Cargo.toml
│ └── src/
│ └── bin/
│ ├── config-docgen.rs
│ └── protocol-docgen.rs
├── mm-protocol/
│ ├── Cargo.toml
│ ├── build.rs
│ └── src/
│ ├── lib.rs
│ ├── messages.proto
│ └── timestamp.rs
├── mm-server/
│ ├── Cargo.toml
│ ├── build.rs
│ ├── deny.toml
│ └── src/
│ ├── codec.rs
│ ├── color.rs
│ ├── config.rs
│ ├── container/
│ │ ├── ipc.rs
│ │ └── runtime.rs
│ ├── container.rs
│ ├── encoder/
│ │ ├── dpb.rs
│ │ ├── gop_structure.rs
│ │ ├── h264.rs
│ │ ├── h265.rs
│ │ ├── rate_control.rs
│ │ └── stats.rs
│ ├── encoder.rs
│ ├── main.rs
│ ├── pixel_scale.rs
│ ├── server/
│ │ ├── handlers/
│ │ │ ├── attachment/
│ │ │ │ └── stats.rs
│ │ │ ├── attachment.rs
│ │ │ └── validation.rs
│ │ ├── handlers.rs
│ │ ├── mdns.rs
│ │ ├── sendmmsg.rs
│ │ └── stream.rs
│ ├── server.rs
│ ├── session/
│ │ ├── audio/
│ │ │ ├── buffer.rs
│ │ │ └── pulse.rs
│ │ ├── audio.rs
│ │ ├── compositor/
│ │ │ ├── buffers/
│ │ │ │ ├── modifiers.rs
│ │ │ │ └── syncobj_timeline.rs
│ │ │ ├── buffers.rs
│ │ │ ├── dispatch/
│ │ │ │ ├── shm.rs
│ │ │ │ ├── wl_buffer.rs
│ │ │ │ ├── wl_compositor.rs
│ │ │ │ ├── wl_data_device_manager.rs
│ │ │ │ ├── wl_drm.rs
│ │ │ │ ├── wl_output.rs
│ │ │ │ ├── wl_seat.rs
│ │ │ │ ├── wl_shm.rs
│ │ │ │ ├── wp_fractional_scale.rs
│ │ │ │ ├── wp_linux_dmabuf.rs
│ │ │ │ ├── wp_linux_drm_syncobj.rs
│ │ │ │ ├── wp_pointer_constraints.rs
│ │ │ │ ├── wp_presentation.rs
│ │ │ │ ├── wp_relative_pointer.rs
│ │ │ │ ├── wp_text_input.rs
│ │ │ │ ├── xdg_shell.rs
│ │ │ │ └── xwayland_shell.rs
│ │ │ ├── dispatch.rs
│ │ │ ├── oneshot_render.rs
│ │ │ ├── output.rs
│ │ │ ├── protocols/
│ │ │ │ ├── wayland-drm.xml
│ │ │ │ └── wl_drm.rs
│ │ │ ├── protocols.rs
│ │ │ ├── sealed.rs
│ │ │ ├── seat.rs
│ │ │ ├── serial.rs
│ │ │ ├── shm.rs
│ │ │ ├── stack.rs
│ │ │ ├── surface.rs
│ │ │ ├── xwayland/
│ │ │ │ └── xwm.rs
│ │ │ └── xwayland.rs
│ │ ├── compositor.rs
│ │ ├── control.rs
│ │ ├── handle.rs
│ │ ├── input/
│ │ │ └── udevfs.rs
│ │ ├── input.rs
│ │ ├── reactor.rs
│ │ ├── video/
│ │ │ ├── composite.rs
│ │ │ ├── composite.slang
│ │ │ ├── convert.rs
│ │ │ └── convert.slang
│ │ └── video.rs
│ ├── session.rs
│ ├── state.rs
│ ├── vulkan/
│ │ ├── chain.rs
│ │ ├── drm.rs
│ │ ├── timeline.rs
│ │ └── video.rs
│ ├── vulkan.rs
│ └── waking_sender.rs
├── mmserver.default.toml
├── shader-common/
│ └── color.slang
└── test-apps/
├── Cargo.toml
├── bin/
│ ├── color.rs
│ ├── cursorlock.rs
│ └── latency.rs
├── build.rs
└── src/
└── color-test.slang
SYMBOL INDEX (1305 symbols across 114 files)
FILE: mm-client-common/bin/uniffi-bindgen.rs
function main (line 5) | fn main() {
FILE: mm-client-common/src/attachment.rs
type AttachmentConfig (line 20) | pub struct AttachmentConfig {
type VideoStreamParams (line 65) | pub struct VideoStreamParams {
type AudioStreamParams (line 75) | pub struct AudioStreamParams {
type Attachment (line 87) | pub struct Attachment {
method new (line 102) | pub(crate) async fn new(
method send (line 220) | fn send(&self, msg: impl Into<protocol::MessageType>, fin: bool) {
method request_video_refresh (line 234) | pub fn request_video_refresh(&self, stream_seq: u64) {
method keyboard_input (line 244) | pub fn keyboard_input(&self, key: input::Key, state: input::KeyState, ...
method pointer_entered (line 257) | pub fn pointer_entered(&self) {
method pointer_left (line 263) | pub fn pointer_left(&self) {
method pointer_motion (line 268) | pub fn pointer_motion(&self, x: f64, y: f64) {
method relative_pointer_motion (line 273) | pub fn relative_pointer_motion(&self, x: f64, y: f64) {
method pointer_input (line 278) | pub fn pointer_input(&self, button: input::Button, state: input::Butto...
method pointer_scroll (line 291) | pub fn pointer_scroll(&self, scroll_type: input::ScrollType, x: f64, y...
method gamepad_available (line 303) | pub fn gamepad_available(&self, pad: input::Gamepad) {
method gamepad_unavailable (line 313) | pub fn gamepad_unavailable(&self, id: u64) {
method gamepad_motion (line 318) | pub fn gamepad_motion(&self, id: u64, axis: input::GamepadAxis, value:...
method gamepad_input (line 330) | pub fn gamepad_input(
method detach (line 347) | pub async fn detach(&self) -> Result<(), ClientError> {
type AttachmentDelegate (line 171) | pub trait AttachmentDelegate: Send + Sync + std::fmt::Debug {
method video_stream_start (line 173) | fn video_stream_start(&self, stream_seq: u64, params: VideoStreamParams);
method video_packet (line 176) | fn video_packet(&self, packet: Arc<packet::Packet>);
method dropped_video_packet (line 179) | fn dropped_video_packet(&self, dropped: packet::DroppedPacket);
method audio_stream_start (line 182) | fn audio_stream_start(&self, stream_seq: u64, params: AudioStreamParams);
method audio_packet (line 185) | fn audio_packet(&self, packet: Arc<packet::Packet>);
method update_cursor (line 188) | fn update_cursor(
method lock_pointer (line 197) | fn lock_pointer(&self, x: f64, y: f64);
method release_pointer (line 200) | fn release_pointer(&self);
method display_params_changed (line 205) | fn display_params_changed(
method error (line 213) | fn error(&self, err: ClientError);
method attachment_ended (line 216) | fn attachment_ended(&self);
type AttachmentState (line 354) | pub(crate) struct AttachmentState {
method handle_message (line 378) | pub(crate) fn handle_message(&mut self, msg: protocol::MessageType) {
method handle_close (line 538) | pub(crate) fn handle_close(mut self, err: Option<ClientError>) {
FILE: mm-client-common/src/conn.rs
constant DEFAULT_PORT (line 7) | const DEFAULT_PORT: u16 = 9599;
constant MAX_QUIC_PACKET_SIZE (line 8) | const MAX_QUIC_PACKET_SIZE: usize = 1350;
constant SOCKET (line 10) | const SOCKET: mio::Token = mio::Token(0);
constant WAKER (line 11) | const WAKER: mio::Token = mio::Token(1);
type ConnError (line 27) | pub enum ConnError {
method from (line 49) | fn from(e: std::io::Error) -> Self {
type ConnEvent (line 55) | pub(crate) enum ConnEvent {
type OutgoingMessage (line 61) | pub(crate) struct OutgoingMessage {
type Conn (line 67) | pub(crate) struct Conn {
method new (line 90) | pub fn new(
method waker (line 167) | pub fn waker(&self) -> Arc<mio::Waker> {
method run (line 171) | pub fn run(&mut self, connect_timeout: time::Duration) -> Result<(), C...
method pump_stream (line 351) | fn pump_stream(&mut self, sid: u64) -> Result<bool, ConnError> {
method send_message (line 415) | fn send_message(
method start_shutdown (line 440) | fn start_shutdown(&mut self) -> Result<(), ConnError> {
function gen_scid (line 450) | fn gen_scid() -> quiche::ConnectionId<'static> {
function resolve_server (line 459) | fn resolve_server(hostport: &str) -> Result<(String, SocketAddr), ConnEr...
FILE: mm-client-common/src/conn/hostport.rs
type MalformedHostPort (line 6) | pub(crate) struct MalformedHostPort;
method fmt (line 9) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
function split_host_port (line 26) | pub(crate) fn split_host_port(
function find (line 77) | fn find(buf: &[u8], c: u8) -> Option<usize> {
function rfind (line 81) | fn rfind(buf: &[u8], c: u8) -> Option<usize> {
function test_split_host_port (line 90) | fn test_split_host_port() {
FILE: mm-client-common/src/display_params.rs
type DisplayParams (line 10) | pub struct DisplayParams {
type Error (line 18) | type Error = ValidationError;
method try_from (line 20) | fn try_from(msg: protocol::VirtualDisplayParameters) -> Result<Self, S...
function from (line 33) | fn from(value: DisplayParams) -> Self {
FILE: mm-client-common/src/input.rs
type Gamepad (line 18) | pub struct Gamepad {
type Error (line 33) | type Error = ValidationError;
method try_from (line 35) | fn try_from(value: protocol::Gamepad) -> Result<Self, Self::Error> {
function from (line 24) | fn from(value: Gamepad) -> Self {
FILE: mm-client-common/src/lib.rs
type ClientError (line 40) | pub enum ClientError {
type ConnHandle (line 62) | struct ConnHandle {
method close (line 73) | fn close(self) -> Result<(), Option<conn::ConnError>> {
type ClientState (line 94) | enum ClientState {
type Roundtrip (line 99) | struct Roundtrip {
type InnerClient (line 105) | struct InnerClient {
method next_stream_id (line 111) | fn next_stream_id(&mut self) -> u64 {
method close (line 118) | fn close(&mut self) -> Result<(), ClientError> {
type Client (line 145) | pub struct Client {
method reconnect (line 154) | async fn reconnect(&self) -> Result<AsyncMutexGuard<InnerClient>, Clie...
method initiate_stream (line 189) | async fn initiate_stream(
method roundtrip (line 251) | async fn roundtrip(
method new (line 264) | pub async fn new(
method stats (line 287) | pub fn stats(&self) -> stats::ClientStats {
method list_applications (line 291) | pub async fn list_applications(
method fetch_application_image (line 311) | pub async fn fetch_application_image(
method list_sessions (line 329) | pub async fn list_sessions(
method launch_session (line 346) | pub async fn launch_session(
method end_session (line 373) | pub async fn end_session(&self, id: u64, timeout: time::Duration) -> R...
method update_session_display_params (line 382) | pub async fn update_session_display_params(
method attach_session (line 403) | pub async fn attach_session(
function spawn_conn (line 456) | async fn spawn_conn(
type InFlight (line 507) | struct InFlight {
function conn_reactor (line 513) | fn conn_reactor(
function conn_reactor_handle_incoming (line 602) | fn conn_reactor_handle_incoming(in_flight: &mut InFlight, ev: conn::Conn...
FILE: mm-client-common/src/logging.rs
type LogLevel (line 8) | pub enum LogLevel {
method from (line 18) | fn from(value: log::Level) -> Self {
type LogDelegate (line 31) | pub trait LogDelegate: Send + Sync + std::fmt::Debug {
method log (line 32) | fn log(&self, level: LogLevel, target: String, msg: String);
type LogWrapper (line 35) | struct LogWrapper(Arc<dyn LogDelegate>);
method enabled (line 38) | fn enabled(&self, metadata: &log::Metadata) -> bool {
method log (line 42) | fn log(&self, record: &log::Record) {
method flush (line 53) | fn flush(&self) {}
function set_log_level (line 58) | fn set_log_level(level: LogLevel) {
function set_logger (line 73) | fn set_logger(logger: Arc<dyn LogDelegate>) {
FILE: mm-client-common/src/packet.rs
type Packet (line 11) | pub struct Packet {
method pts (line 29) | pub fn pts(&self) -> u64 {
method stream_seq (line 33) | pub fn stream_seq(&self) -> u64 {
method seq (line 37) | pub fn seq(&self) -> u64 {
method hierarchical_layer (line 41) | pub fn hierarchical_layer(&self) -> u32 {
method data (line 45) | pub fn data(&self) -> Vec<u8> {
method len (line 62) | pub fn len(&self) -> usize {
method is_empty (line 66) | pub fn is_empty(&self) -> bool {
method copy_to_slice (line 71) | pub fn copy_to_slice(&self, mut dst: &mut [u8]) {
type DroppedPacket (line 20) | pub struct DroppedPacket {
FILE: mm-client-common/src/packet/ring.rs
constant RING_TARGET_SIZE (line 12) | const RING_TARGET_SIZE: usize = 5;
type Chunk (line 14) | pub(crate) trait Chunk {
method seq (line 15) | fn seq(&self) -> u64;
method stream_seq (line 16) | fn stream_seq(&self) -> u64;
method chunk (line 17) | fn chunk(&self) -> u32;
method num_chunks (line 18) | fn num_chunks(&self) -> u32;
method data (line 19) | fn data(&self) -> bytes::Bytes;
method pts (line 20) | fn pts(&self) -> u64;
method hierarchical_layer (line 21) | fn hierarchical_layer(&self) -> u32;
method fec_metadata (line 22) | fn fec_metadata(&self) -> Option<protocol::FecMetadata>;
method seq (line 26) | fn seq(&self) -> u64 {
method stream_seq (line 30) | fn stream_seq(&self) -> u64 {
method chunk (line 34) | fn chunk(&self) -> u32 {
method num_chunks (line 38) | fn num_chunks(&self) -> u32 {
method data (line 42) | fn data(&self) -> bytes::Bytes {
method pts (line 46) | fn pts(&self) -> u64 {
method hierarchical_layer (line 50) | fn hierarchical_layer(&self) -> u32 {
method fec_metadata (line 54) | fn fec_metadata(&self) -> Option<mm_protocol::FecMetadata> {
method seq (line 60) | fn seq(&self) -> u64 {
method stream_seq (line 64) | fn stream_seq(&self) -> u64 {
method chunk (line 68) | fn chunk(&self) -> u32 {
method num_chunks (line 72) | fn num_chunks(&self) -> u32 {
method data (line 76) | fn data(&self) -> bytes::Bytes {
method pts (line 80) | fn pts(&self) -> u64 {
method hierarchical_layer (line 84) | fn hierarchical_layer(&self) -> u32 {
method fec_metadata (line 88) | fn fec_metadata(&self) -> Option<mm_protocol::FecMetadata> {
type FECDecoder (line 94) | enum FECDecoder {
type WipPacket (line 103) | struct WipPacket {
method new (line 112) | fn new(incoming: impl Chunk) -> Result<Self, PacketRingError> {
method insert (line 146) | fn insert(&mut self, incoming: impl Chunk) -> Result<(), PacketRingErr...
method is_complete (line 179) | fn is_complete(&mut self) -> bool {
method complete (line 197) | fn complete(self) -> Packet {
type PacketRingError (line 227) | pub(crate) enum PacketRingError {
type PacketRing (line 239) | pub(crate) struct PacketRing {
method new (line 248) | pub(crate) fn new() -> Self {
method recv_chunk (line 252) | pub(crate) fn recv_chunk(&mut self, incoming: impl Chunk) -> Result<()...
method drain_completed (line 321) | pub(crate) fn drain_completed(&mut self, stream_seq: u64) -> DrainComp...
method discard (line 326) | pub(crate) fn discard(&mut self, stream_seq: u64) {
type DrainCompleted (line 333) | pub(crate) struct DrainCompleted<'a>(&'a mut PacketRing, u64);
type Item (line 336) | type Item = Result<Packet, DroppedPacket>;
method next (line 338) | fn next(&mut self) -> Option<Self::Item> {
function test_ring (line 374) | fn test_ring() {
function test_ring_drop (line 414) | fn test_ring_drop() {
function make_chunks (line 457) | fn make_chunks(seq: u64, chunks: &[&[u8]]) -> Vec<protocol::VideoChunk> {
FILE: mm-client-common/src/pixel_scale.rs
type PixelScale (line 10) | pub struct PixelScale {
constant ONE (line 16) | pub const ONE: Self = Self {
method new (line 21) | pub fn new(numerator: u32, denominator: u32) -> Self {
method is_fractional (line 28) | pub fn is_fractional(&self) -> bool {
method round_up (line 32) | pub fn round_up(self) -> Self {
method fmt (line 41) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
type Error (line 47) | type Error = ValidationError;
method try_from (line 49) | fn try_from(scale: protocol::PixelScale) -> Result<Self, Self::Error> {
function from (line 64) | fn from(scale: PixelScale) -> Self {
FILE: mm-client-common/src/session.rs
type Application (line 15) | pub struct Application {
type Error (line 23) | type Error = ValidationError;
method try_from (line 25) | fn try_from(value: protocol::application_list::Application) -> Result<...
type Session (line 48) | pub struct Session {
type Error (line 56) | type Error = ValidationError;
method try_from (line 58) | fn try_from(msg: protocol::session_list::Session) -> Result<Self, Self...
FILE: mm-client-common/src/stats.rs
type StatsCollector (line 8) | pub(crate) struct StatsCollector {
method snapshot (line 15) | pub(crate) fn snapshot(&self) -> ClientStats {
type ClientStats (line 27) | pub struct ClientStats {
FILE: mm-client-common/src/validation.rs
type ValidationError (line 6) | pub enum ValidationError {
FILE: mm-client/build.rs
function main (line 11) | fn main() {
function compile_shader (line 32) | fn compile_shader(
FILE: mm-client/src/audio.rs
type DecodePacket (line 20) | trait DecodePacket<T> {
method decode (line 21) | fn decode(&mut self, input: &[u8], output: &mut [T]) -> anyhow::Result...
function decode (line 25) | fn decode(&mut self, packet: &[u8], output: &mut [f32]) -> anyhow::Resul...
function decode (line 32) | fn decode(&mut self, packet: &[u8], output: &mut [i16]) -> anyhow::Resul...
type StreamWrapper (line 39) | trait StreamWrapper {
method new (line 41) | fn new(
method sync (line 48) | fn sync(&mut self, pts: u64);
method send_packet (line 49) | fn send_packet(&mut self, packet: Arc<client::Packet>) -> anyhow::Resu...
method new (line 66) | fn new(
method sync (line 213) | fn sync(&mut self, pts: u64) {
method send_packet (line 217) | fn send_packet(&mut self, packet: Arc<client::Packet>) -> anyhow::Resu...
type StreamInner (line 52) | struct StreamInner<F: dasp::Frame> {
method drop (line 228) | fn drop(&mut self) {
type AudioStream (line 244) | pub struct AudioStream {
method new (line 257) | pub fn new() -> anyhow::Result<Self> {
method sync (line 276) | pub fn sync(&mut self, pts: u64) {
method reset (line 282) | pub fn reset(
method recv_packet (line 312) | pub fn recv_packet(&mut self, packet: Arc<client::Packet>) -> anyhow::...
function select_conf (line 335) | fn select_conf(
FILE: mm-client/src/audio/buffer.rs
type PlaybackBuffer (line 7) | pub struct PlaybackBuffer<F>
function new (line 22) | pub fn new() -> Self {
function len (line 30) | pub fn len(&self) -> usize {
function buffer (line 35) | pub fn buffer(&mut self, pts: u64, frames: &[F]) {
function current_pts (line 41) | pub fn current_pts(&self) -> u64 {
function drain (line 49) | pub fn drain(&mut self) -> Draining<F> {
function skip (line 54) | pub fn skip(&mut self, frames: usize) {
type Draining (line 71) | pub struct Draining<'a, F>
type Item (line 82) | type Item = F;
method next (line 84) | fn next(&mut self) -> Option<Self::Item> {
FILE: mm-client/src/bin/latency-test.rs
constant APP_DIMENSION (line 21) | const APP_DIMENSION: u32 = 256;
constant DEFAULT_TIMEOUT (line 22) | const DEFAULT_TIMEOUT: time::Duration = time::Duration::from_secs(1);
type Cli (line 27) | struct Cli {
type AppEvent (line 42) | pub enum AppEvent {
method fmt (line 49) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
method from (line 61) | fn from(event: AttachmentEvent) -> Self {
method from (line 67) | fn from(event: VideoStreamEvent) -> Self {
type App (line 77) | struct App {
method resumed (line 156) | fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
method window_event (line 172) | fn window_event(
method about_to_wait (line 180) | fn about_to_wait(&mut self, event_loop: &winit::event_loop::ActiveEven...
method user_event (line 191) | fn user_event(&mut self, event_loop: &winit::event_loop::ActiveEventLo...
method exiting (line 206) | fn exiting(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {
type LatencyTest (line 84) | struct LatencyTest {
method event (line 220) | fn event(&mut self, event: AppEvent) -> anyhow::Result<bool> {
method send_space (line 273) | fn send_space(&mut self) {
method check_frame (line 289) | fn check_frame(&mut self) -> anyhow::Result<()> {
method check_block (line 323) | fn check_block(&mut self, idx: usize) -> bool {
method submit_copy (line 335) | unsafe fn submit_copy(&mut self) -> anyhow::Result<()> {
function main (line 107) | fn main() -> anyhow::Result<()> {
function start_test (line 403) | fn start_test(
function init_logging (line 491) | fn init_logging() -> anyhow::Result<()> {
FILE: mm-client/src/bin/mmclient.rs
constant DEFAULT_CONNECT_TIMEOUT (line 30) | const DEFAULT_CONNECT_TIMEOUT: time::Duration = time::Duration::from_sec...
constant DEFAULT_REQUEST_TIMEOUT (line 31) | const DEFAULT_REQUEST_TIMEOUT: time::Duration = time::Duration::from_sec...
constant MAX_FRAME_TIME (line 33) | const MAX_FRAME_TIME: time::Duration = time::Duration::from_nanos(1_000_...
constant RESIZE_COOLDOWN (line 34) | const RESIZE_COOLDOWN: time::Duration = time::Duration::from_millis(500);
type Resolution (line 37) | enum Resolution {
method from (line 45) | fn from(s: &str) -> Self {
type Cli (line 62) | struct Cli {
type AttachmentWindow (line 117) | struct AttachmentWindow {
method handle_window_event (line 315) | fn handle_window_event(&mut self, event: winit::event::WindowEvent) ->...
method handle_app_event (line 504) | fn handle_app_event(
method idle (line 659) | fn idle(&mut self, client: &client::Client) -> anyhow::Result<bool> {
method schedule_next_frame (line 761) | fn schedule_next_frame(
method motion_vector_to_attachment_space (line 778) | fn motion_vector_to_attachment_space(&self, x: f64, y: f64) -> Option<...
type App (line 156) | struct App {
method resumed (line 207) | fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
method window_event (line 222) | fn window_event(
method device_event (line 246) | fn device_event(
method user_event (line 265) | fn user_event(&mut self, event_loop: &winit::event_loop::ActiveEventLo...
method about_to_wait (line 274) | fn about_to_wait(&mut self, event_loop: &winit::event_loop::ActiveEven...
method exiting (line 283) | fn exiting(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {
type AppEvent (line 165) | pub enum AppEvent {
method from (line 173) | fn from(event: VideoStreamEvent) -> Self {
method from (line 184) | fn from(value: AttachmentEvent) -> Self {
method from (line 190) | fn from(event: GamepadEvent) -> Self {
method fmt (line 196) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
function main (line 802) | pub fn main() -> anyhow::Result<()> {
function init_window (line 853) | fn init_window(
function init_logging (line 1038) | fn init_logging() -> anyhow::Result<()> {
function determine_ui_scale (line 1077) | fn determine_ui_scale(scale_factor: f64) -> client::pixel_scale::PixelSc...
function determine_resolution (line 1106) | fn determine_resolution(resolution: Resolution, width: u32, height: u32)...
function filter_sessions (line 1118) | fn filter_sessions(sessions: Vec<client::Session>, app: &str) -> Vec<cli...
function cmd_list_apps (line 1132) | fn cmd_list_apps(client: &client::Client) -> anyhow::Result<()> {
function cmd_list_sessions (line 1174) | fn cmd_list_sessions(args: &Cli, client: &client::Client) -> anyhow::Res...
function cmd_kill (line 1212) | fn cmd_kill(args: &Cli, client: &client::Client) -> anyhow::Result<()> {
FILE: mm-client/src/cursor.rs
function load_cursor_image (line 8) | pub fn load_cursor_image(image: &[u8], hs_x: u32, hs_y: u32) -> anyhow::...
function cursor_icon_from_proto (line 25) | pub fn cursor_icon_from_proto(icon: protocol::update_cursor::CursorIcon)...
FILE: mm-client/src/delegate.rs
type AttachmentProxy (line 13) | pub struct AttachmentProxy<T: From<AttachmentEvent> + std::fmt::Debug + ...
function new (line 18) | pub fn new(proxy: winit::event_loop::EventLoopProxy<T>) -> Self {
function proxy (line 22) | fn proxy(&self, ev: AttachmentEvent) {
type AttachmentEvent (line 27) | pub enum AttachmentEvent {
method fmt (line 49) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
function video_stream_start (line 95) | fn video_stream_start(&self, stream_seq: u64, params: client::VideoStrea...
function video_packet (line 99) | fn video_packet(&self, packet: Arc<client::Packet>) {
function dropped_video_packet (line 103) | fn dropped_video_packet(&self, dropped: client::DroppedPacket) {
function audio_stream_start (line 107) | fn audio_stream_start(&self, stream_seq: u64, params: client::AudioStrea...
function audio_packet (line 111) | fn audio_packet(&self, packet: Arc<client::Packet>) {
function update_cursor (line 115) | fn update_cursor(
function lock_pointer (line 130) | fn lock_pointer(&self, x: f64, y: f64) {
function release_pointer (line 134) | fn release_pointer(&self) {
function display_params_changed (line 138) | fn display_params_changed(
function error (line 149) | fn error(&self, err: client::ClientError) {
function attachment_ended (line 154) | fn attachment_ended(&self) {
FILE: mm-client/src/flash.rs
constant FLASH_DURATION (line 7) | const FLASH_DURATION: time::Duration = time::Duration::from_millis(1350);
constant FADE_OUT_AFTER (line 8) | const FADE_OUT_AFTER: time::Duration = time::Duration::from_millis(1000);
type Flash (line 10) | pub struct Flash {
method new (line 15) | pub fn new() -> Self {
method set_message (line 19) | pub fn set_message(&mut self, s: &str) {
method build (line 23) | pub fn build(&mut self, ui: &imgui::Ui) -> anyhow::Result<()> {
method default (line 69) | fn default() -> Self {
FILE: mm-client/src/font.rs
function load_ui_font (line 13) | pub fn load_ui_font() -> anyhow::Result<Font> {
FILE: mm-client/src/gamepad.rs
type GamepadEvent (line 15) | pub enum GamepadEvent {
type RemoteGamepad (line 23) | struct RemoteGamepad {
method update_dpad (line 39) | fn update_dpad<T>(
type DpadState (line 31) | struct DpadState {
function spawn_gamepad_monitor (line 104) | pub fn spawn_gamepad_monitor<T>(
function handle_gilrs_event (line 205) | fn handle_gilrs_event<T>(
function input_event (line 272) | fn input_event(
function gamepad_id (line 280) | fn gamepad_id(uuid: [u8; 16]) -> u64 {
function layout (line 287) | fn layout(pad: gilrs::Gamepad) -> GamepadLayout {
function girls_axis_to_proto (line 294) | fn girls_axis_to_proto(axis: gilrs::Axis) -> Option<GamepadAxis> {
function gilrs_button_to_proto (line 308) | fn gilrs_button_to_proto(button: gilrs::Button) -> Option<GamepadButton> {
FILE: mm-client/src/keys.rs
function winit_key_to_proto (line 8) | pub fn winit_key_to_proto(key: KeyCode) -> Key {
FILE: mm-client/src/overlay.rs
type Overlay (line 11) | pub struct Overlay {
method new (line 22) | pub fn new(fps: u32) -> Self {
method reposition (line 34) | pub fn reposition(&mut self) {
method update_params (line 38) | pub fn update_params(&mut self, params: &protocol::Attached) {
method build (line 44) | pub fn build(&mut self, ui: &imgui::Ui) -> anyhow::Result<()> {
function stat_row (line 131) | fn stat_row(ui: &imgui::Ui, label: impl AsRef<str>, value: impl AsRef<st...
FILE: mm-client/src/render.rs
constant FONT_SIZE (line 25) | const FONT_SIZE: f32 = 8.0;
type TextureColorSpace (line 30) | enum TextureColorSpace {
method from (line 36) | fn from(cs: crate::video::ColorSpace) -> Self {
type PushConstants (line 46) | struct PushConstants {
type Renderer (line 52) | pub struct Renderer {
method new (line 118) | pub fn new(
method recreate_swapchain (line 162) | unsafe fn recreate_swapchain(&mut self) -> Result<()> {
method handle_event (line 578) | pub fn handle_event(&mut self, event: &winit::event::WindowEvent) -> a...
method resize (line 604) | pub fn resize(&mut self, width: u32, height: u32) {
method scale_factor_changed (line 614) | fn scale_factor_changed(&mut self, scale_factor: f64) -> anyhow::Resul...
method bind_video_texture (line 627) | pub fn bind_video_texture(
method get_texture_aspect (line 642) | pub fn get_texture_aspect(&self) -> Option<(f64, f64)> {
method render (line 656) | pub unsafe fn render<F>(&mut self, ui_builder: F) -> Result<()>
method destroy_swapchain (line 914) | unsafe fn destroy_swapchain(&mut self, mut swapchain: Swapchain) {
type VideoTexture (line 73) | struct VideoTexture {
type Swapchain (line 79) | struct Swapchain {
type InFlightFrame (line 102) | struct InFlightFrame {
type SwapImage (line 112) | struct SwapImage {
function select_surface_format (line 951) | fn select_surface_format(
method drop (line 999) | fn drop(&mut self) {
function import_imgui_font (line 1008) | fn import_imgui_font(
function calculate_aspect (line 1036) | fn calculate_aspect(width: u32, height: u32, tex_width: u32, tex_height:...
FILE: mm-client/src/stats.rs
type Stats (line 19) | pub struct Stats {
method set_connection_rtt (line 37) | pub fn set_connection_rtt(&self, rtt: time::Duration) {
method frame_received (line 42) | pub fn frame_received(&self, stream_seq: u64, seq: u64, len: usize) {
method frame_rendered (line 56) | pub fn frame_rendered(&self, stream_seq: u64, seq: u64) {
method frame_discarded (line 76) | pub fn frame_discarded(&self, stream_seq: u64, seq: u64) {
method video_bitrate (line 85) | pub fn video_bitrate(&self) -> f32 {
method video_latency (line 90) | pub fn video_latency(&self) -> f32 {
type InFlightFrame (line 23) | struct InFlightFrame(time::Instant);
type Inner (line 25) | struct Inner {
method default (line 99) | fn default() -> Self {
FILE: mm-client/src/video.rs
constant DECODER_INIT_TIMEOUT (line 21) | const DECODER_INIT_TIMEOUT: time::Duration = time::Duration::from_secs(5);
type Undecoded (line 23) | type Undecoded = std::sync::Arc<client::Packet>;
type FrameMetadata (line 26) | pub struct FrameMetadata {
type YUVPicture (line 33) | struct YUVPicture {
type ColorSpace (line 40) | pub enum ColorSpace {
type VideoStreamParams (line 46) | pub struct VideoStreamParams {
method default (line 54) | fn default() -> Self {
type VideoStreamEvent (line 64) | pub enum VideoStreamEvent {
type StreamState (line 69) | enum StreamState {
method fmt (line 77) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
type VideoStream (line 91) | pub struct VideoStream<T: From<VideoStreamEvent> + Send + 'static> {
function new (line 98) | pub fn new(vk: Arc<VkContext>, proxy: winit::event_loop::EventLoopProxy<...
function reset (line 109) | pub fn reset(
function recv_packet (line 137) | pub fn recv_packet(&mut self, buf: Undecoded) -> anyhow::Result<()> {
function prepare_frame (line 200) | pub fn prepare_frame(&mut self) -> anyhow::Result<Option<FrameMetadata>> {
function mark_frame_rendered (line 209) | pub fn mark_frame_rendered(&mut self) {
function is_ready (line 218) | pub fn is_ready(&self) -> bool {
type CPUDecoder (line 226) | struct CPUDecoder {
method send_packet (line 679) | fn send_packet(&mut self, buf: Undecoded) -> anyhow::Result<()> {
method prepare_frame (line 695) | pub fn prepare_frame(&mut self) -> anyhow::Result<Option<FrameMetadata...
method mark_frame_rendered (line 730) | pub fn mark_frame_rendered(&mut self) {
method upload (line 736) | unsafe fn upload(&mut self, pic: YUVPicture) -> anyhow::Result<()> {
method prerecord_upload (line 785) | unsafe fn prerecord_upload(&self) -> anyhow::Result<()> {
type DecoderInit (line 253) | struct DecoderInit {
method new (line 265) | fn new(
method send_packet (line 338) | fn send_packet(&mut self, buf: Undecoded) -> anyhow::Result<bool> {
method into_decoder (line 417) | fn into_decoder<T>(
method drop (line 919) | fn drop(&mut self) {
function receive_frame (line 934) | fn receive_frame(
function copy_packet (line 958) | fn copy_packet(pkt: &mut ffmpeg::Packet, buf: Undecoded) -> anyhow::Resu...
function copy_frame (line 983) | fn copy_frame(
function get_hw_format (line 1025) | unsafe extern "C" fn get_hw_format(
function read_format_list (line 1067) | unsafe fn read_format_list(
FILE: mm-client/src/vulkan.rs
type VkDebugContext (line 27) | pub struct VkDebugContext {
type VkQueue (line 32) | pub struct VkQueue {
type VkDeviceInfo (line 37) | pub struct VkDeviceInfo {
method query (line 74) | fn query(
method is_integrated (line 217) | pub fn is_integrated(&self) -> bool {
type VkContext (line 52) | pub struct VkContext {
method new (line 223) | pub unsafe fn new(window: Arc<winit::window::Window>, debug: bool) -> ...
method drop (line 509) | fn drop(&mut self) {
function contains (line 531) | fn contains(list: &[CString], str: &'static CStr) -> bool {
function init_tracy_context (line 535) | fn init_tracy_context(
function select_memory_type (line 596) | pub fn select_memory_type(
function get_queue_with_command_pool (line 620) | fn get_queue_with_command_pool(device: &ash::Device, idx: u32) -> Result...
function create_command_buffer (line 637) | pub fn create_command_buffer(
type VkImage (line 657) | pub struct VkImage {
method new (line 667) | pub fn new(
method wrap (line 714) | pub fn wrap(
method extent (line 732) | pub fn extent(&self) -> vk::Extent2D {
method rect (line 739) | pub fn rect(&self) -> vk::Rect2D {
method drop (line 748) | fn drop(&mut self) {
function bind_memory_for_image (line 756) | pub unsafe fn bind_memory_for_image(
function create_image_view (line 797) | pub unsafe fn create_image_view(
type VkHostBuffer (line 834) | pub struct VkHostBuffer {
function create_host_buffer (line 840) | pub fn create_host_buffer(
function destroy_host_buffer (line 877) | pub unsafe fn destroy_host_buffer(device: &ash::Device, buffer: &VkHostB...
type VkTimestampQueryPool (line 883) | pub struct VkTimestampQueryPool {
method cmd_reset (line 889) | pub unsafe fn cmd_reset(&self, device: &ash::Device, command_buffer: v...
method fetch_results (line 893) | pub fn fetch_results(&self, device: &ash::Device) -> anyhow::Result<Ve...
function create_timestamp_query_pool (line 909) | pub fn create_timestamp_query_pool(
function create_fence (line 929) | pub fn create_fence(device: &ash::Device, signalled: bool) -> Result<vk:...
function create_semaphore (line 940) | pub fn create_semaphore(device: &ash::Device) -> Result<vk::Semaphore, v...
function load_shader (line 945) | pub fn load_shader(device: &ash::Device, bytes: &[u8]) -> anyhow::Result...
function create_ycbcr_sampler_conversion (line 954) | pub fn create_ycbcr_sampler_conversion(
function get_ycbcr_conversion_properties (line 982) | pub fn get_ycbcr_conversion_properties(
function vulkan_debug_utils_callback (line 1007) | unsafe extern "system" fn vulkan_debug_utils_callback(
function cmd_image_barrier (line 1034) | pub fn cmd_image_barrier(
FILE: mm-docgen/src/bin/config-docgen.rs
constant FRONT_MATTER (line 11) | const FRONT_MATTER: &str = r#"
function main (line 20) | fn main() {
FILE: mm-docgen/src/bin/protocol-docgen.rs
constant FRONT_MATTER (line 9) | const FRONT_MATTER: &str = r#"
function main (line 18) | fn main() {
function emit_comments (line 57) | fn emit_comments(lines: &mut Vec<String>) {
function emit_message_code_block (line 77) | fn emit_message_code_block(lines: &mut Vec<String>) {
FILE: mm-protocol/build.rs
function main (line 5) | fn main() -> std::io::Result<()> {
FILE: mm-protocol/src/lib.rs
type ProtobufError (line 16) | enum ProtobufError {
type ProtocolError (line 24) | pub enum ProtocolError {
constant MAX_MESSAGE_SIZE (line 39) | pub const MAX_MESSAGE_SIZE: usize = 1048576;
constant ALPN_PROTOCOL_VERSION (line 42) | pub const ALPN_PROTOCOL_VERSION: &[u8] = b"mm00";
function decode_message (line 143) | pub fn decode_message(buf: &[u8]) -> Result<(MessageType, usize), Protoc...
function encode_message (line 174) | pub fn encode_message(msg: &MessageType, buf: &mut [u8]) -> Result<usize...
function encode_header (line 193) | fn encode_header(msg_type: u32, msg_len: usize, buf: &mut [u8]) -> Resul...
function get_varint32 (line 215) | fn get_varint32(buf: &mut octets::Octets) -> Result<u32, ProtocolError> {
function invalid_message_type (line 275) | fn invalid_message_type() {
FILE: mm-protocol/src/timestamp.rs
type Error (line 10) | type Error = ProtocolError;
function try_from (line 12) | fn try_from(value: Timestamp) -> Result<Self, Self::Error> {
method from (line 25) | fn from(value: time::SystemTime) -> Self {
FILE: mm-server/build.rs
function main (line 11) | fn main() {
function compile_shader (line 76) | fn compile_shader<'a>(
function save_keymap (line 114) | fn save_keymap(
FILE: mm-server/src/codec.rs
type VideoCodec (line 14) | pub enum VideoCodec {
type Error (line 27) | type Error = anyhow::Error;
method try_from (line 29) | fn try_from(codec: protocol::VideoCodec) -> anyhow::Result<Self> {
type AudioCodec (line 22) | pub enum AudioCodec {
type Error (line 50) | type Error = anyhow::Error;
method try_from (line 52) | fn try_from(codec: protocol::AudioCodec) -> anyhow::Result<Self> {
function from (line 40) | fn from(codec: VideoCodec) -> Self {
function from (line 61) | fn from(codec: AudioCodec) -> Self {
function probe_codec (line 68) | pub fn probe_codec(_vk: Arc<VkContext>, codec: VideoCodec) -> bool {
FILE: mm-server/src/color.rs
type ColorSpace (line 13) | pub enum ColorSpace {
method from_primaries_and_tf (line 25) | pub fn from_primaries_and_tf(
type VideoProfile (line 40) | pub enum VideoProfile {
type Error (line 48) | type Error = String;
method try_from (line 50) | fn try_from(profile: protocol::VideoProfile) -> Result<Self, Self::Err...
function from (line 60) | fn from(profile: VideoProfile) -> Self {
type TransferFunction (line 69) | pub enum TransferFunction {
type Primaries (line 76) | pub enum Primaries {
FILE: mm-server/src/config.rs
constant MAX_APP_PATH_COMPONENTS (line 27) | const MAX_APP_PATH_COMPONENTS: usize = 8;
constant MAX_IMAGE_SIZE (line 28) | pub const MAX_IMAGE_SIZE: u64 = 1024 * 1024;
type NonZeroOrInf (line 38) | pub(super) enum NonZeroOrInf {
method deserialize (line 44) | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
type Config (line 72) | pub(super) struct Config {
method new (line 170) | pub fn new(path: Option<&PathBuf>, includes: &[PathBuf]) -> anyhow::Re...
method build (line 191) | fn build(cfg: Option<parsed::Config>, includes: &[PathBuf]) -> anyhow:...
method validate (line 277) | fn validate(&self) -> anyhow::Result<()> {
type ServerConfig (line 86) | pub(super) struct ServerConfig {
type DefaultAppSettings (line 101) | pub(super) struct DefaultAppSettings {
type AppConfig (line 111) | pub(super) struct AppConfig {
type Config (line 127) | pub struct Config {
method new (line 170) | pub fn new(path: Option<&PathBuf>, includes: &[PathBuf]) -> anyhow::Re...
method build (line 191) | fn build(cfg: Option<parsed::Config>, includes: &[PathBuf]) -> anyhow:...
method validate (line 277) | fn validate(&self) -> anyhow::Result<()> {
type ServerConfig (line 136) | pub struct ServerConfig {
type AppConfig (line 150) | pub struct AppConfig {
type HomeIsolationMode (line 163) | pub enum HomeIsolationMode {
method default (line 323) | fn default() -> Self {
function collect_includes (line 328) | fn collect_includes(p: impl AsRef<Path>) -> anyhow::Result<Vec<(String, ...
function include_file (line 354) | fn include_file(p: impl AsRef<Path>) -> anyhow::Result<(String, parsed::...
function locate_default_config_file (line 372) | fn locate_default_config_file() -> Option<PathBuf> {
function validate_app (line 385) | fn validate_app(
function validate_app_path (line 466) | fn validate_app_path(p: String) -> anyhow::Result<Vec<String>> {
function validate_app_path_component (line 485) | fn validate_app_path_component(component: Component) -> Option<String> {
function config_from_str (line 519) | fn config_from_str(s: &str) -> anyhow::Result<Config> {
function test_default (line 525) | fn test_default() {
function test_only_app (line 536) | fn test_only_app() {
function tls_required_for_global_addr (line 557) | fn tls_required_for_global_addr() {
function tls_required_for_unspecified (line 582) | fn tls_required_for_unspecified() {
function tls_not_required_for_tailscale (line 605) | fn tls_not_required_for_tailscale() {
function app_paths (line 622) | fn app_paths() {
FILE: mm-server/src/container.rs
type ContainerHandle (line 23) | pub struct ContainerHandle {
method pid (line 37) | pub fn pid(&self) -> Pid {
method pidfd (line 41) | pub(crate) fn pidfd(&self) -> BorrowedFd<'_> {
method signal (line 45) | pub fn signal(&mut self, signal: Signal) -> anyhow::Result<()> {
method wait (line 52) | pub fn wait(&mut self) -> anyhow::Result<()> {
method fs_mount (line 68) | pub fn fs_mount<S>(
method fuse_mount (line 90) | pub fn fuse_mount(
method as_fd (line 31) | fn as_fd(&self) -> BorrowedFd<'_> {
method drop (line 103) | fn drop(&mut self) {
FILE: mm-server/src/container/ipc.rs
type EventfdBarrier (line 12) | pub struct EventfdBarrier {
method new (line 19) | pub fn new() -> io::Result<(Self, Self)> {
method sync (line 37) | pub fn sync(&self, timeout: time::Duration) -> rustix::io::Result<()> {
function fd_oneshot (line 51) | pub fn fd_oneshot() -> io::Result<(FdSender, FdReceiver)> {
type FdSender (line 56) | pub struct FdSender(uds::UnixSeqpacketConn);
method send_timeout (line 59) | pub fn send_timeout(self, fd: OwnedFd, timeout: time::Duration) -> io:...
type FdReceiver (line 70) | pub struct FdReceiver(uds::UnixSeqpacketConn);
method recv_timeout (line 73) | pub fn recv_timeout(self, timeout: time::Duration) -> io::Result<Owned...
function signal_eventfd (line 91) | fn signal_eventfd(fd: impl AsFd) -> rustix::io::Result<()> {
function wait_eventfd (line 100) | fn wait_eventfd(fd: impl AsFd, timeout: time::Duration) -> rustix::io::R...
FILE: mm-server/src/container/runtime.rs
constant SYNC_TIMEOUT (line 40) | const SYNC_TIMEOUT: time::Duration = time::Duration::from_secs(5);
constant SYNC_TIMEOUT (line 43) | const SYNC_TIMEOUT: time::Duration = time::Duration::from_secs(1);
type DevBindMount (line 46) | struct DevBindMount {
constant DEV_BIND_MOUNTS (line 51) | const DEV_BIND_MOUNTS: &[DevBindMount] = &[
type UnbufferedStderr (line 112) | struct UnbufferedStderr<'a>(BorrowedFd<'a>);
function write_str (line 116) | fn write_str(&mut self, s: &str) -> std::fmt::Result {
function _must (line 139) | unsafe fn _must<T>(_op: &str, res: rustix::io::Result<T>) -> T {
type SetupHook (line 167) | type SetupHook = Box<dyn FnOnce(&mut super::ContainerHandle) -> anyhow::...
type Container (line 182) | pub struct Container {
method new (line 209) | pub fn new(
method intern_run_path (line 300) | pub fn intern_run_path(&self) -> &Path {
method extern_run_path (line 304) | pub fn extern_run_path(&self) -> &Path {
method bind_mount (line 308) | pub fn bind_mount(&mut self, src: impl AsRef<Path>, dst: impl AsRef<Pa...
method internal_bind_mount (line 313) | pub fn internal_bind_mount(&mut self, src: impl AsRef<Path>, dst: impl...
method setup_hook (line 318) | pub fn setup_hook(
method pre_exec (line 325) | pub unsafe fn pre_exec(&mut self, f: impl FnMut() -> io::Result<()> + ...
method set_env (line 329) | pub fn set_env<K, V>(&mut self, key: K, val: V)
method set_stdout (line 337) | pub fn set_stdout<T: AsFd>(&mut self, stdio: T) -> anyhow::Result<()> {
method set_stderr (line 344) | pub fn set_stderr<T: AsFd>(&mut self, stdio: T) -> anyhow::Result<()> {
method spawn (line 354) | pub fn spawn(mut self) -> anyhow::Result<super::ContainerHandle> {
method child_after_fork (line 427) | unsafe fn child_after_fork<FD>(
function set_uid_map (line 650) | fn set_uid_map(child_pid: i32, uid: rustix::fs::Uid, gid: rustix::fs::Gi...
function run_in_container (line 683) | fn run_in_container<F>(ns_pidfd: impl AsFd, stderr: Option<BorrowedFd<'_...
function fs_mount_into (line 732) | pub(super) fn fs_mount_into(
function fuse_mount_into (line 752) | pub(super) fn fuse_mount_into(
function touch (line 827) | fn touch(path: impl AsRef<Path>, mode: impl Into<Mode>) -> rustix::io::R...
function detach_mount (line 838) | fn detach_mount(path: impl AsRef<Path>) -> rustix::io::Result<OwnedFd> {
function reattach_mount (line 848) | fn reattach_mount(fd: OwnedFd, path: impl AsRef<Path>) -> rustix::io::Re...
function mount_fs (line 858) | fn mount_fs(
function sync_barrier (line 892) | fn sync_barrier(barrier: &ipc::EventfdBarrier) -> rustix::io::Result<()> {
function make_putenv (line 897) | fn make_putenv(k: impl AsRef<OsStr>, v: impl AsRef<OsStr>) -> CString {
function validate_exe (line 907) | fn validate_exe(p: impl AsRef<Path>) -> anyhow::Result<PathBuf> {
function echo (line 938) | fn echo() -> anyhow::Result<()> {
function test_validate_exe (line 955) | fn test_validate_exe() {
FILE: mm-server/src/encoder.rs
type Encoder (line 34) | pub enum Encoder {
method new (line 40) | pub fn new(
method submit_encode (line 53) | pub unsafe fn submit_encode(
method input_format (line 65) | pub fn input_format(&self) -> vk::Format {
method create_input_image (line 72) | pub fn create_input_image(&mut self) -> anyhow::Result<VkImage> {
method request_refresh (line 79) | pub fn request_refresh(&mut self) {
type EncoderInner (line 87) | struct EncoderInner {
method new (line 110) | pub fn new(
method create_input_image (line 281) | fn create_input_image(&self, profile: &mut vk::VideoProfileInfoKHR) ->...
method submit_encode (line 350) | pub unsafe fn submit_encode(
method drop (line 707) | fn drop(&mut self) {
type EncoderOutputFrame (line 743) | struct EncoderOutputFrame {
method new (line 771) | pub fn new(
type TracingContext (line 763) | struct TracingContext {
method drop (line 879) | fn drop(&mut self) {
type Sink (line 900) | pub trait Sink: Send + 'static {
method write_frame (line 901) | fn write_frame(
type QueryResults (line 912) | struct QueryResults {
function writer_thread (line 921) | fn writer_thread(
function list_format_props (line 1011) | fn list_format_props<'a>(
function bind_session_memory (line 1031) | fn bind_session_memory(
function default_profile (line 1090) | fn default_profile(op: vk::VideoCodecOperationFlagsKHR) -> vk::VideoProf...
function default_hdr10_profile (line 1098) | fn default_hdr10_profile(op: vk::VideoCodecOperationFlagsKHR) -> vk::Vid...
function default_encode_usage (line 1106) | fn default_encode_usage(driver_version: DriverVersion) -> vk::VideoEncod...
function single_profile_list_info (line 1120) | fn single_profile_list_info<'a>(
function default_structure (line 1130) | fn default_structure(
FILE: mm-server/src/encoder/dpb.rs
type DpbPicture (line 12) | pub struct DpbPicture {
type DpbPool (line 21) | pub struct DpbPool {
method new (line 31) | pub fn new(
method new_separate_images (line 65) | pub fn new_separate_images(
method setup_pic (line 104) | pub fn setup_pic(&self) -> DpbPicture {
method get_pic (line 116) | pub fn get_pic(&self, id: u32) -> Option<DpbPicture> {
method mark_active (line 127) | pub fn mark_active(&mut self, slot: usize, id: u32) {
method mark_inactive (line 136) | pub fn mark_inactive(&mut self, slot: usize) {
method clear (line 142) | pub fn clear(&mut self) {
function create_dpb_image (line 151) | fn create_dpb_image(
FILE: mm-server/src/encoder/gop_structure.rs
type GopFrame (line 7) | pub struct GopFrame {
type HierarchicalP (line 29) | pub struct HierarchicalP {
method new (line 40) | pub fn new(layers: u32, gop_size: u32) -> Self {
method next_frame (line 57) | pub fn next_frame(&mut self) -> GopFrame {
method request_refresh (line 108) | pub fn request_refresh(&mut self) {
method required_dpb_size (line 112) | pub fn required_dpb_size(&self) -> usize {
method layer_framerate (line 119) | pub fn layer_framerate(&self, layer: u32, base_framerate: u32) -> (u32...
function temporal_layer (line 131) | fn temporal_layer(frame: u32, layers: u32) -> u32 {
function test_temporal_layer_4_layers (line 144) | fn test_temporal_layer_4_layers() {
function test_gop (line 156) | fn test_gop() {
function test_flat (line 234) | fn test_flat() {
FILE: mm-server/src/encoder/h264.rs
type H264Metadata (line 46) | struct H264Metadata {
type H264Encoder (line 51) | pub struct H264Encoder {
method new (line 65) | pub fn new(
method submit_encode (line 296) | pub unsafe fn submit_encode(
method input_format (line 529) | pub fn input_format(&self) -> vk::Format {
method create_input_image (line 533) | pub fn create_input_image(&mut self) -> anyhow::Result<VkImage> {
method request_refresh (line 537) | pub fn request_refresh(&mut self) {
FILE: mm-server/src/encoder/h265.rs
type H265Metadata (line 42) | struct H265Metadata {
type H265Encoder (line 48) | pub struct H265Encoder {
method new (line 62) | pub fn new(
method submit_encode (line 382) | pub unsafe fn submit_encode(
method input_format (line 663) | pub fn input_format(&self) -> vk::Format {
method create_input_image (line 667) | pub fn create_input_image(&mut self) -> anyhow::Result<VkImage> {
method request_refresh (line 671) | pub fn request_refresh(&mut self) {
FILE: mm-server/src/encoder/rate_control.rs
constant BASELINE_AVG_BITRATE_MBPS (line 13) | const BASELINE_AVG_BITRATE_MBPS: [f32; 10] = [2.5, 3.0, 4.0, 5.0, 6.0, 8...
constant BASELINE_PEAK_BITRATE_MBPS (line 14) | const BASELINE_PEAK_BITRATE_MBPS: [f32; 10] =
constant BASELINE_DIMS (line 16) | const BASELINE_DIMS: f32 = 1920.0 * 1080.0;
constant VBV_SIZE (line 17) | const VBV_SIZE: u32 = 2500;
type RateControlMode (line 20) | pub enum RateControlMode {
method as_vk_flags (line 27) | pub fn as_vk_flags(&self) -> vk::VideoEncodeRateControlModeFlagsKHR {
type CascadingQp (line 37) | pub struct CascadingQp {
method layer (line 43) | pub fn layer(&self, layer: u32) -> u32 {
type VbrSettings (line 49) | pub struct VbrSettings {
type LayeredVbr (line 57) | pub struct LayeredVbr {
method layer (line 65) | pub fn layer(&self, layer: u32) -> VbrSettings {
function select_rc_mode (line 82) | pub fn select_rc_mode(
function layer_qp (line 141) | fn layer_qp(target_qp: u32, layer: u32) -> u32 {
FILE: mm-server/src/encoder/stats.rs
type EncodeStats (line 10) | pub struct EncodeStats {
method record_frame_size (line 79) | pub fn record_frame_size(&self, is_keyframe: bool, layer: u32, len: us...
method fmt (line 100) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
type Inner (line 14) | struct Inner {
method default (line 22) | fn default() -> Self {
type LayerStats (line 34) | struct LayerStats {
method new (line 42) | fn new(start: time::Instant) -> Self {
method record_frame_size (line 51) | fn record_frame_size(&mut self, len: usize) {
method fmt (line 65) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
function calculate_rate (line 116) | fn calculate_rate(dur: time::Duration, total: u64) -> f32 {
FILE: mm-server/src/main.rs
type Cli (line 33) | struct Cli {
function main (line 57) | fn main() -> Result<()> {
function init_logging (line 141) | fn init_logging(bug_report_dir: Option<impl AsRef<Path>>) -> Result<()> {
function preflight_checks (line 181) | fn preflight_checks(cfg: &config::Config, vk: &vulkan::VkContext) -> any...
function linux_version (line 229) | fn linux_version() -> Option<(u32, u32)> {
function sysctl (line 241) | fn sysctl(name: &str) -> bool {
function save_vulkaninfo (line 250) | fn save_vulkaninfo(bug_report_dir: impl AsRef<Path>) {
function test_linux_version (line 263) | fn test_linux_version() {
FILE: mm-server/src/pixel_scale.rs
type PixelScale (line 11) | pub struct PixelScale(pub u32, pub u32);
constant ONE (line 14) | pub const ONE: Self = Self(1, 1);
method is_fractional (line 16) | pub fn is_fractional(&self) -> bool {
method ceil (line 20) | pub fn ceil(self) -> Self {
method fmt (line 26) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
type Error (line 47) | type Error = anyhow::Error;
method try_from (line 49) | fn try_from(scale: protocol::PixelScale) -> anyhow::Result<Self> {
method default (line 32) | fn default() -> Self {
type FractionalScaleError (line 38) | pub struct FractionalScaleError;
method fmt (line 41) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
function from (line 65) | fn from(value: PixelScale) -> Self {
type Error (line 71) | type Error = FractionalScaleError;
function try_from (line 73) | fn try_from(value: PixelScale) -> Result<Self, Self::Error> {
function from (line 83) | fn from(scale: PixelScale) -> Self {
FILE: mm-server/src/server.rs
constant MAX_QUIC_PACKET_SIZE (line 34) | const MAX_QUIC_PACKET_SIZE: usize = 1350;
constant SOCKET (line 36) | const SOCKET: mio::Token = mio::Token(0);
constant WAKER (line 37) | const WAKER: mio::Token = mio::Token(1);
type Server (line 39) | pub struct Server {
method new (line 90) | pub fn new(
method local_addr (line 193) | pub fn local_addr(&self) -> anyhow::Result<SocketAddr> {
method closer (line 197) | pub fn closer(&self) -> WakingSender<()> {
method run (line 202) | pub fn run(&mut self) -> anyhow::Result<()> {
method recv (line 485) | fn recv(&mut self, mut pkt: BytesMut, from: SocketAddr) -> anyhow::Res...
type Outgoing (line 61) | struct Outgoing {
type StreamWorker (line 66) | pub struct StreamWorker {
type ClientConnection (line 72) | pub struct ClientConnection {
method update_timeout (line 695) | fn update_timeout(&mut self) -> anyhow::Result<()> {
method read_messages (line 705) | fn read_messages(
method write_message (line 776) | fn write_message(
method flush_partial_write (line 818) | fn flush_partial_write(&mut self, sid: u64) -> anyhow::Result<bool> {
method send_dgram (line 847) | fn send_dgram(&mut self, msg: Vec<u8>) -> anyhow::Result<()> {
method err_stream (line 862) | fn err_stream(
method send_periodic_keepalive (line 883) | fn send_periodic_keepalive(&mut self) -> quiche::Result<()> {
function self_signed_tls_ctx (line 674) | fn self_signed_tls_ctx(addr: SocketAddr) -> anyhow::Result<boring::ssl::...
function gen_random_cid (line 898) | fn gen_random_cid() -> quiche::ConnectionId<'static> {
FILE: mm-server/src/server/handlers.rs
type ServerError (line 26) | struct ServerError(protocol::error::ErrorCode, Option<String>);
type Context (line 28) | struct Context {
method send_err (line 37) | fn send_err(&self, err: ServerError) {
type Result (line 55) | type Result<M> = std::result::Result<M, ServerError>;
function dispatch (line 57) | pub fn dispatch(
function roundtrip (line 113) | fn roundtrip<F, Req, Resp>(f: F, ctx: &Context, req: Req)
function list_applications (line 131) | fn list_applications(
function fetch_img (line 156) | fn fetch_img(
function launch_session (line 197) | fn launch_session(
function list_sessions (line 273) | fn list_sessions(ctx: &Context, _msg: protocol::ListSessions) -> Result<...
function update_session (line 292) | fn update_session(ctx: &Context, msg: protocol::UpdateSession) -> Result...
function end_session (line 321) | fn end_session(ctx: &Context, msg: protocol::EndSession) -> Result<proto...
function generate_streaming_res (line 333) | fn generate_streaming_res(display_params: &DisplayParams) -> Vec<protoco...
function read_file (line 342) | fn read_file(p: impl AsRef<Path>, max_size: u64) -> anyhow::Result<Bytes> {
function test_read_file (line 366) | fn test_read_file() -> anyhow::Result<()> {
FILE: mm-server/src/server/handlers/attachment.rs
function from (line 23) | fn from(params: DisplayParams) -> Self {
type AttachmentHandler (line 35) | struct AttachmentHandler<'a> {
type AttachmentError (line 57) | enum AttachmentError {
function attach (line 62) | pub fn attach(ctx: &super::Context, msg: protocol::Attach) -> Result<(),...
function new (line 78) | fn new(ctx: &'a super::Context, msg: protocol::Attach) -> Result<Self, S...
function run (line 203) | fn run(&mut self) -> Result<(), ServerError> {
function handle_attachment_message (line 265) | fn handle_attachment_message(
function handle_session_event (line 535) | fn handle_session_event(&mut self, event: SessionEvent) -> Result<(), At...
function send (line 644) | fn send(&self, msg: impl Into<protocol::MessageType>) {
function key_to_evdev (line 649) | fn key_to_evdev(key: protocol::keyboard_input::Key) -> Option<u32> {
function axis_to_evdev (line 788) | fn axis_to_evdev(axis: protocol::gamepad_motion::GamepadAxis) -> Option<...
function gamepad_button_to_evdev (line 801) | fn gamepad_button_to_evdev(button: protocol::gamepad_input::GamepadButto...
function cursor_icon_to_proto (line 829) | fn cursor_icon_to_proto(icon: cursor_icon::CursorIcon) -> protocol::upda...
FILE: mm-server/src/server/handlers/attachment/stats.rs
type AttachmentStats (line 10) | pub struct AttachmentStats {
method new (line 21) | pub fn new(app_id: String) -> Self {
method record_frame (line 35) | pub fn record_frame(&mut self, _seq: u64, len: usize, duration: time::...
FILE: mm-server/src/server/handlers/validation.rs
type ValidationError (line 18) | pub enum ValidationError {
type Result (line 23) | type Result<T> = std::result::Result<T, ValidationError>;
function validate_display_params (line 25) | pub fn validate_display_params(
function validate_attachment (line 46) | pub fn validate_attachment(
function validate_resolution (line 74) | pub fn validate_resolution(resolution: Option<protocol::Size>) -> Result...
function validate_ui_scale (line 91) | pub fn validate_ui_scale(ui_scale: Option<protocol::PixelScale>) -> Resu...
function validate_profile (line 104) | fn validate_profile(profile: i32) -> Result<VideoProfile> {
function validate_video_codec (line 119) | pub fn validate_video_codec(codec: i32) -> Result<VideoCodec> {
function validate_preset (line 132) | pub fn validate_preset(preset: u32) -> Result<u32> {
function validate_framerate (line 139) | pub fn validate_framerate(framerate: u32) -> Result<u32> {
function validate_audio_codec (line 146) | pub fn validate_audio_codec(codec: i32) -> Result<AudioCodec> {
function validate_sample_rate (line 159) | pub fn validate_sample_rate(sample_rate: u32) -> Result<u32> {
function validate_channels (line 169) | pub fn validate_channels(channels: Option<protocol::AudioChannels>) -> R...
function validate_gamepad (line 191) | pub fn validate_gamepad(gamepad: Option<protocol::Gamepad>) -> Result<(u...
function validate_gamepad_id (line 201) | pub fn validate_gamepad_id(id: u64) -> Result<u64> {
function validate_gamepad_layout (line 209) | pub fn validate_gamepad_layout(layout: i32) -> Result<GamepadLayout> {
FILE: mm-server/src/server/mdns.rs
type MdnsService (line 10) | pub struct MdnsService {
method new (line 16) | pub fn new(
method drop (line 71) | fn drop(&mut self) {
function mdns_hostname (line 96) | fn mdns_hostname() -> anyhow::Result<String> {
function mdns_instance_name (line 113) | fn mdns_instance_name(hostname: &str) -> anyhow::Result<String> {
FILE: mm-server/src/server/sendmmsg.rs
type SendMmsg (line 15) | pub struct SendMmsg<'a> {
function sendmsg (line 23) | pub fn sendmsg(mut self, buf: &'a [u8], addr: SocketAddr, txtime: time::...
function finish (line 34) | pub fn finish(&mut self, fd: &impl AsRawFd) -> Result<(), nix::Error> {
function new (line 64) | pub fn new<'a>() -> SendMmsg<'a> {
function set_so_txtime (line 69) | pub fn set_so_txtime(sock: &impl AsFd) -> anyhow::Result<()> {
function std_time_to_u64 (line 81) | fn std_time_to_u64(time: &std::time::Instant) -> u64 {
FILE: mm-server/src/server/stream.rs
type StreamWriter (line 14) | pub struct StreamWriter {
method new (line 31) | pub fn new(
method write_video_frame (line 64) | pub fn write_video_frame(
method write_audio_frame (line 128) | pub fn write_audio_frame(
type Chunk (line 183) | pub struct Chunk {
function iter_chunks (line 190) | pub fn iter_chunks(
function iter_chunks_fec (line 228) | fn iter_chunks_fec(buf: Bytes, mtu: usize, ratio: f32) -> impl Iterator<...
function test_iter_chunks (line 254) | fn test_iter_chunks() {
function test_iter_chunks_fec (line 279) | fn test_iter_chunks_fec() {
FILE: mm-server/src/session.rs
constant ATTACH_TIMEOUT (line 33) | const ATTACH_TIMEOUT: time::Duration = time::Duration::from_secs(10);
type Session (line 35) | pub struct Session {
method launch (line 63) | pub fn launch(
method update_display_params (line 133) | pub fn update_display_params(&mut self, display_params: DisplayParams)...
method attach (line 153) | pub fn attach(
method detach (line 210) | pub fn detach(&mut self, attachment: Attachment) -> anyhow::Result<()> {
method stop (line 229) | pub fn stop(self) -> anyhow::Result<()> {
method supports_stream (line 243) | pub fn supports_stream(&self, params: VideoStreamParams) -> bool {
type Attachment (line 53) | pub struct Attachment {
FILE: mm-server/src/session/audio.rs
type EncodeFrame (line 21) | struct EncodeFrame {
type Encoder (line 26) | struct Encoder {
method drop (line 32) | fn drop(&mut self) {
type EncodePipeline (line 45) | pub struct EncodePipeline {
method new (line 57) | pub fn new(
method stop_stream (line 93) | pub fn stop_stream(&mut self) {
method restart_stream (line 97) | pub fn restart_stream(&mut self, params: AudioStreamParams) -> anyhow:...
method drop (line 186) | fn drop(&mut self) {
FILE: mm-server/src/session/audio/buffer.rs
type PlaybackBuffer (line 12) | pub enum PlaybackBuffer<F>
function new (line 27) | pub fn new(sample_spec: pulse::SampleSpec, output_spec: pulse::SampleSpe...
function buffer (line 52) | fn buffer(&self) -> &Buffer<F> {
function buffer_mut (line 59) | fn buffer_mut(&mut self) -> &mut Buffer<F> {
function len_bytes (line 66) | pub fn len_bytes(&self) -> usize {
function len_frames (line 70) | pub fn len_frames(&self) -> usize {
function is_empty (line 74) | pub fn is_empty(&self) -> bool {
function write (line 78) | pub fn write(&mut self, payload: &[u8]) {
function drain (line 86) | pub fn drain(&mut self, num_frames: usize) -> Option<impl dasp::Signal<F...
function clear (line 109) | pub fn clear(&mut self) {
type EitherSignal (line 114) | enum EitherSignal<L, R> {
type Frame (line 124) | type Frame = L::Frame;
function next (line 126) | fn next(&mut self) -> Self::Frame {
function is_exhausted (line 133) | fn is_exhausted(&self) -> bool {
type Buffer (line 141) | pub struct Buffer<F>
function new (line 155) | pub fn new(sample_spec: pulse::SampleSpec) -> Self {
function len_bytes (line 164) | fn len_bytes(&self) -> usize {
function len_frames (line 168) | fn len_frames(&self) -> usize {
function read_frame (line 173) | fn read_frame(&mut self) -> Option<F> {
function read_sample (line 190) | fn read_sample(&mut self) -> Option<F::Sample> {
function drain (line 211) | fn drain(&mut self, num_frames: usize) -> Option<Drain<Self>> {
type Frame (line 227) | type Frame = F;
function next (line 229) | fn next(&mut self) -> Self::Frame {
type Drain (line 235) | struct Drain<'a, S: dasp::Signal> {
type Frame (line 241) | type Frame = S::Frame;
function is_exhausted (line 243) | fn is_exhausted(&self) -> bool {
function next (line 247) | fn next(&mut self) -> Self::Frame {
method drop (line 258) | fn drop(&mut self) {
function passthrough (line 277) | fn passthrough() {
function downmix (line 314) | fn downmix() {
FILE: mm-server/src/session/audio/pulse.rs
constant WAKER (line 26) | const WAKER: mio::Token = mio::Token(0);
constant LISTENER (line 27) | const LISTENER: mio::Token = mio::Token(1);
constant CLOCK (line 28) | const CLOCK: mio::Token = mio::Token(2);
constant CAPTURE_SAMPLE_RATE (line 31) | pub const CAPTURE_SAMPLE_RATE: u32 = 48000;
constant CAPTURE_CHANNEL_COUNT (line 32) | pub const CAPTURE_CHANNEL_COUNT: u32 = 2;
constant CAPTURE_SPEC (line 33) | pub const CAPTURE_SPEC: pulse::SampleSpec = pulse::SampleSpec {
constant CLOCK_RATE_HZ (line 40) | const CLOCK_RATE_HZ: u32 = 100;
constant SINK_NAME (line 42) | const SINK_NAME: &CStr = cstr!("magic_mirror");
type StreamState (line 45) | pub enum StreamState {
type PlaybackStream (line 52) | struct PlaybackStream {
type Client (line 62) | struct Client {
type ServerState (line 71) | struct ServerState {
type PulseServer (line 79) | pub struct PulseServer {
method new (line 93) | pub fn new(
method run (line 210) | pub fn run(&mut self) -> anyhow::Result<()> {
method recv (line 292) | fn recv(&mut self, client_token: mio::Token) -> anyhow::Result<()> {
method clock_tick (line 366) | fn clock_tick(&mut self) -> anyhow::Result<()> {
function handle_command (line 494) | fn handle_command(
function sample_spec_from_format (line 786) | fn sample_spec_from_format(f: &pulse::FormatInfo) -> anyhow::Result<puls...
function sanitize_prop_str (line 828) | fn sanitize_prop_str(b: &[u8]) -> anyhow::Result<&str> {
function handle_stream_write (line 834) | fn handle_stream_write(
function configure_buffer (line 902) | fn configure_buffer(attr: &mut pulse::stream::BufferAttr, spec: &pulse::...
function write_reply (line 956) | fn write_reply<T: pulse::CommandReply + std::fmt::Debug>(
FILE: mm-server/src/session/compositor.rs
type Compositor (line 54) | pub struct Compositor {
method new (line 84) | pub fn new(
method update_display_params (line 119) | pub fn update_display_params(
method composite_frame (line 171) | pub fn composite_frame(
method idle (line 242) | pub fn idle(&mut self, active: bool) -> anyhow::Result<()> {
type ClientState (line 266) | pub struct ClientState {
method initialized (line 271) | fn initialized(&self, _client_id: wayland_server::backend::ClientId) {}
method disconnected (line 272) | fn disconnected(
function create_globals (line 280) | pub fn create_globals(dh: &wayland_server::DisplayHandle) {
function create_global (line 301) | fn create_global<G: wayland_server::Resource + 'static>(
FILE: mm-server/src/session/compositor/buffers.rs
type Buffer (line 29) | pub struct Buffer {
method dimensions (line 38) | pub fn dimensions(&self) -> glam::UVec2 {
type BufferBacking (line 46) | pub enum BufferBacking {
type PlaneMetadata (line 65) | pub struct PlaneMetadata {
method release_buffers (line 76) | pub fn release_buffers(&mut self) -> anyhow::Result<()> {
function import_shm_buffer (line 146) | pub fn import_shm_buffer(
function import_dmabuf_buffer (line 193) | pub fn import_dmabuf_buffer(
function validate_buffer_parameters (line 331) | pub fn validate_buffer_parameters(
constant DMA_BUF_SYNC_READ (line 373) | pub(super) const DMA_BUF_SYNC_READ: u32 = 1 << 0;
constant DMA_BUF_SYNC_WRITE (line 374) | pub(super) const DMA_BUF_SYNC_WRITE: u32 = 1 << 1;
type dma_buf_export_sync_file (line 378) | struct dma_buf_export_sync_file {
type dma_buf_import_sync_file (line 385) | struct dma_buf_import_sync_file {
type ExportSyncFile (line 390) | pub(super) struct ExportSyncFile(dma_buf_export_sync_file);
method new (line 393) | pub(super) fn new(flags: u32) -> Self {
type Output (line 407) | type Output = RawFd;
constant IS_MUTATING (line 409) | const IS_MUTATING: bool = true;
method opcode (line 411) | fn opcode(&self) -> Opcode {
method as_ptr (line 415) | fn as_ptr(&mut self) -> *mut c_void {
method output_from_ptr (line 419) | unsafe fn output_from_ptr(
type ImportSyncFile (line 398) | pub(super) struct ImportSyncFile(dma_buf_import_sync_file);
method new (line 401) | pub(super) fn new(fd: RawFd, flags: u32) -> Self {
type Output (line 435) | type Output = ();
constant IS_MUTATING (line 437) | const IS_MUTATING: bool = true;
method opcode (line 439) | fn opcode(&self) -> Opcode {
method as_ptr (line 443) | fn as_ptr(&mut self) -> *mut c_void {
method output_from_ptr (line 447) | unsafe fn output_from_ptr(
function import_dmabuf_fence_as_semaphore (line 464) | pub fn import_dmabuf_fence_as_semaphore(
function import_sync_file_as_semaphore (line 476) | pub unsafe fn import_sync_file_as_semaphore(
function export_sync_file (line 494) | pub unsafe fn export_sync_file(dmabuf: impl AsFd, flags: u32) -> anyhow:...
function attach_sync_file (line 507) | pub unsafe fn attach_sync_file(
FILE: mm-server/src/session/compositor/buffers/modifiers.rs
constant SUPPORTED_DRM_FORMATS (line 21) | pub const SUPPORTED_DRM_FORMATS: &[(DrmFourcc, vk::Format, bool, usize)]...
function fourcc_to_vk (line 52) | pub fn fourcc_to_vk(fourcc: DrmFourcc) -> Option<(vk::Format, bool)> {
function fourcc_bpp (line 59) | pub fn fourcc_bpp(fourcc: DrmFourcc) -> Option<usize> {
type CachedDmabufFeedback (line 66) | pub struct CachedDmabufFeedback {
method contains (line 73) | pub fn contains(&self, modifier: u64) -> bool {
method new (line 79) | pub fn new(vk: Arc<VkContext>) -> anyhow::Result<Self> {
method emit_dmabuf_feedback (line 129) | pub fn emit_dmabuf_feedback(
function query_drm_format_modifiers (line 149) | unsafe fn query_drm_format_modifiers(
function verify_dmabuf_support (line 171) | pub unsafe fn verify_dmabuf_support(
FILE: mm-server/src/session/compositor/buffers/syncobj_timeline.rs
type SyncobjTimeline (line 20) | pub struct SyncobjTimeline(Arc<TimelineHandle>);
method import (line 79) | pub fn import(
method new_timeline_point (line 93) | pub fn new_timeline_point(&self, value: u64) -> SyncobjTimelinePoint {
type TimelineHandle (line 22) | struct TimelineHandle {
method drop (line 29) | fn drop(&mut self) {
type SyncobjTimelinePoint (line 35) | pub struct SyncobjTimelinePoint {
method signal (line 41) | pub fn signal(&self) -> io::Result<()> {
method import_as_semaphore (line 51) | pub fn import_as_semaphore(&self, semaphore: vk::Semaphore) -> anyhow:...
FILE: mm-server/src/session/compositor/dispatch.rs
function make_u64 (line 22) | fn make_u64(hi: u32, lo: u32) -> u64 {
FILE: mm-server/src/session/compositor/dispatch/shm.rs
type ShmPool (line 21) | pub struct ShmPool {
method bind (line 28) | fn bind(
method request (line 41) | fn request(
method request (line 95) | fn request(
method destroyed (line 188) | fn destroyed(
FILE: mm-server/src/session/compositor/dispatch/wl_buffer.rs
method request (line 10) | fn request(
method destroyed (line 25) | fn destroyed(
FILE: mm-server/src/session/compositor/dispatch/wl_compositor.rs
method bind (line 20) | fn bind(
method request (line 33) | fn request(
method request (line 58) | fn request(
method destroyed (line 134) | fn destroyed(
method request (line 145) | fn request(
method request (line 158) | fn request(
FILE: mm-server/src/session/compositor/dispatch/wl_data_device_manager.rs
method bind (line 15) | fn bind(
method request (line 28) | fn request(
method request (line 50) | fn request(
method request (line 63) | fn request(
FILE: mm-server/src/session/compositor/dispatch/wl_drm.rs
method bind (line 8) | fn bind(
method request (line 24) | fn request(
function dev_path (line 36) | pub fn dev_path(dev: libc::dev_t) -> std::io::Result<String> {
FILE: mm-server/src/session/compositor/dispatch/wl_output.rs
method bind (line 10) | fn bind(
method request (line 26) | fn request(
method destroyed (line 37) | fn destroyed(
FILE: mm-server/src/session/compositor/dispatch/wl_seat.rs
method bind (line 13) | fn bind(
method request (line 27) | fn request(
method request (line 57) | fn request(
method destroyed (line 94) | fn destroyed(
method request (line 105) | fn request(
method destroyed (line 116) | fn destroyed(
FILE: mm-server/src/session/compositor/dispatch/wl_shm.rs
method bind (line 23) | fn bind(
method request (line 38) | fn request(
method request (line 83) | fn request(
method destroyed (line 174) | fn destroyed(
FILE: mm-server/src/session/compositor/dispatch/wp_fractional_scale.rs
method bind (line 15) | fn bind(
method request (line 30) | fn request(
method request (line 68) | fn request(
FILE: mm-server/src/session/compositor/dispatch/wp_linux_dmabuf.rs
method bind (line 24) | fn bind(
method request (line 37) | fn request(
type Params (line 65) | enum Params {
method request (line 82) | fn request(
function validate_create (line 206) | fn validate_create(
method request (line 278) | fn request(
FILE: mm-server/src/session/compositor/dispatch/wp_linux_drm_syncobj.rs
method bind (line 21) | fn bind(
method request (line 36) | fn request(
method request (line 89) | fn request(
method destroyed (line 140) | fn destroyed(
method request (line 160) | fn request(
FILE: mm-server/src/session/compositor/dispatch/wp_pointer_constraints.rs
method bind (line 15) | fn bind(
method request (line 30) | fn request(
method request (line 74) | fn request(
method destroyed (line 85) | fn destroyed(
method request (line 96) | fn request(
FILE: mm-server/src/session/compositor/dispatch/wp_presentation.rs
method bind (line 11) | fn bind(
method request (line 25) | fn request(
method request (line 64) | fn request(
FILE: mm-server/src/session/compositor/dispatch/wp_relative_pointer.rs
method bind (line 15) | fn bind(
method request (line 30) | fn request(
method request (line 54) | fn request(
method destroyed (line 65) | fn destroyed(
FILE: mm-server/src/session/compositor/dispatch/wp_text_input.rs
method bind (line 14) | fn bind(
method request (line 27) | fn request(
method request (line 49) | fn request(
method destroyed (line 60) | fn destroyed(
FILE: mm-server/src/session/compositor/dispatch/xdg_shell.rs
method bind (line 16) | fn bind(
method request (line 29) | fn request(
method request (line 70) | fn request(
method destroyed (line 113) | fn destroyed(
method request (line 137) | fn request(
method request (line 151) | fn request(
method request (line 176) | fn request(
method destroyed (line 216) | fn destroyed(
FILE: mm-server/src/session/compositor/dispatch/xwayland_shell.rs
method bind (line 15) | fn bind(
method can_view (line 26) | fn can_view(client: wayland_server::Client, _global_data: &()) -> bool {
method request (line 35) | fn request(
method request (line 59) | fn request(
FILE: mm-server/src/session/compositor/oneshot_render.rs
function shm_to_png (line 14) | pub fn shm_to_png(buffer: &VkHostBuffer, format: PlaneMetadata) -> anyho...
FILE: mm-server/src/session/compositor/output.rs
method emit_output_params (line 10) | pub fn emit_output_params(&mut self) {
function configure_output (line 18) | pub fn configure_output(output: &wl_output::WlOutput, params: DisplayPar...
FILE: mm-server/src/session/compositor/sealed.rs
type SealedFile (line 14) | pub struct SealedFile {
method new (line 20) | pub fn new(name: impl AsRef<CStr>, contents: &[u8]) -> anyhow::Result<...
method size (line 42) | pub fn size(&self) -> usize {
method as_raw_fd (line 48) | fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
method as_fd (line 54) | fn as_fd(&self) -> BorrowedFd<'_> {
FILE: mm-server/src/session/compositor/seat.rs
type KeyState (line 31) | pub enum KeyState {
type ButtonState (line 38) | pub enum ButtonState {
function from (line 44) | fn from(value: ButtonState) -> Self {
type Pointer (line 53) | struct Pointer {
type PointerLock (line 59) | struct PointerLock {
type Cursor (line 67) | pub enum Cursor {
type Seat (line 81) | pub struct Seat {
method get_pointer (line 127) | pub fn get_pointer(&mut self, wl_pointer: wl_pointer::WlPointer) {
method get_relative_pointer (line 139) | pub fn get_relative_pointer(
method get_keyboard (line 148) | pub fn get_keyboard(&mut self, wl_keyboard: wl_keyboard::WlKeyboard) {
method get_text_input (line 165) | pub fn get_text_input(&mut self, wp_text_input: zwp_text_input_v3::Zwp...
method destroy_pointer (line 169) | pub fn destroy_pointer(&mut self, wl_pointer: &wl_pointer::WlPointer) {
method destroy_relative_pointer (line 189) | pub fn destroy_relative_pointer(
method destroy_keyboard (line 196) | pub fn destroy_keyboard(&mut self, wl_keyboard: &wl_keyboard::WlKeyboa...
method destroy_text_input (line 200) | pub fn destroy_text_input(&mut self, wp_text_input: &zwp_text_input_v3...
method lift_pointer (line 204) | pub fn lift_pointer(&mut self, serial: &Serial) {
method update_pointer (line 222) | pub fn update_pointer(
method relative_pointer_motion (line 281) | pub fn relative_pointer_motion(&mut self, surface_vector: impl Into<gl...
method pointer_axis (line 310) | pub fn pointer_axis(&mut self, surface_vector: impl Into<glam::DVec2>) {
method pointer_axis_discrete (line 326) | pub fn pointer_axis_discrete(&mut self, vector: impl Into<glam::DVec2>) {
method pointer_input (line 341) | pub fn pointer_input(
method pointer_frame (line 364) | pub fn pointer_frame(&mut self) {
method focused_pointers (line 376) | fn focused_pointers(&mut self) -> impl Iterator<Item = (&wl_pointer::W...
method set_keyboard_focus (line 388) | pub fn set_keyboard_focus(&mut self, serial: &Serial, surface: Option<...
method keyboard_input (line 435) | pub fn keyboard_input(&mut self, serial: &Serial, scancode: u32, state...
method focused_keyboards (line 452) | pub fn focused_keyboards(&self) -> impl Iterator<Item = &wl_keyboard::...
method has_text_input (line 464) | pub fn has_text_input(&mut self) -> bool {
method text_input_char (line 468) | pub fn text_input_char(&mut self, serial: &Serial, ch: char) {
method focused_text_inputs (line 481) | fn focused_text_inputs(&mut self) -> impl Iterator<Item = &zwp_text_in...
method pointer_focus (line 493) | pub fn pointer_focus(&self) -> Option<wl_surface::WlSurface> {
method keyboard_focus (line 498) | pub fn keyboard_focus(&self) -> Option<wl_surface::WlSurface> {
method pointer_coords (line 502) | pub fn pointer_coords(&self) -> Option<glam::DVec2> {
method pointer_locked (line 506) | pub fn pointer_locked(&self) -> Option<glam::DVec2> {
method has_lock (line 514) | pub fn has_lock(&self, wl_surface: &wl_surface::WlSurface) -> bool {
method create_lock (line 529) | pub fn create_lock(
method destroy_lock (line 553) | pub fn destroy_lock(&mut self, wp_locked_pointer: &zwp_locked_pointer_...
method default (line 100) | fn default() -> Self {
method handle_input_event (line 575) | pub fn handle_input_event(&mut self, ev: ControlMessage) {
method update_pointer_lock (line 683) | pub fn update_pointer_lock(&mut self) {
method set_cursor (line 729) | pub fn set_cursor(&mut self, wl_pointer: &wl_pointer::WlPointer, cursor:...
method dispatch_cursor (line 773) | pub fn dispatch_cursor(&mut self) {
method render_cursor (line 801) | pub fn render_cursor(&mut self) -> anyhow::Result<()> {
function send_axis_discrete (line 865) | fn send_axis_discrete(pointer: &wl_pointer::WlPointer, axis: wl_pointer:...
FILE: mm-server/src/session/compositor/serial.rs
type Serial (line 7) | pub struct Serial(AtomicU32);
method new (line 12) | pub fn new() -> Self {
method next (line 16) | pub fn next(&self) -> u32 {
constant START (line 9) | const START: u32 = 1000;
FILE: mm-server/src/session/compositor/shm.rs
type ShmPool (line 19) | pub struct ShmPool {
type Pool (line 26) | pub struct Pool {
method new (line 33) | pub fn new(fd: OwnedFd, size: usize) -> anyhow::Result<Self> {
method data (line 39) | pub fn data(&self, offset: usize, len: usize) -> &[u8] {
method resize (line 44) | pub fn resize(&mut self, new_size: usize) -> anyhow::Result<()> {
method unmap (line 58) | fn unmap(&mut self) {
function map (line 71) | unsafe fn map(fd: impl AsFd, size: usize) -> anyhow::Result<*mut u8> {
method drop (line 89) | fn drop(&mut self) {
FILE: mm-server/src/session/compositor/stack.rs
method map_surface (line 16) | pub fn map_surface(&mut self, id: SurfaceKey, buffer_id: BufferKey) {
method unmap_surface (line 51) | pub fn unmap_surface(&mut self, id: SurfaceKey) {
method raise_x11_surface (line 64) | pub fn raise_x11_surface(&mut self, serial: u64) {
method raise_surface_at (line 75) | fn raise_surface_at(&mut self, position: usize) {
method update_focus_and_visibility (line 87) | pub fn update_focus_and_visibility(&mut self, active: bool) -> anyhow::R...
method surface_under (line 176) | pub fn surface_under(
method surfaces_ready (line 195) | pub fn surfaces_ready(&self) -> bool {
FILE: mm-server/src/session/compositor/surface.rs
type Surface (line 34) | pub struct Surface {
method new (line 59) | pub fn new(wl_surface: wl_surface::WlSurface) -> Self {
method reconfigure (line 84) | pub fn reconfigure(&mut self, params: DisplayParams, xwin: Option<&xwa...
method surface_coords (line 134) | pub fn surface_coords(&self, coords: impl Into<glam::DVec2>) -> Option...
method effective_scale (line 163) | pub fn effective_scale(&self) -> PixelScale {
method fmt (line 169) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
type DoubleBuffered (line 197) | pub struct DoubleBuffered<T: Clone + Eq + PartialEq> {
method default (line 203) | fn default() -> Self {
type CommitResult (line 212) | pub enum CommitResult<T> {
function promote (line 219) | pub fn promote(&mut self) -> CommitResult<T> {
type SurfaceRole (line 240) | pub enum SurfaceRole {
type Visibility (line 252) | pub enum Visibility {
type SurfaceConfiguration (line 260) | pub struct SurfaceConfiguration {
type PendingBuffer (line 273) | pub enum PendingBuffer {
type ContentUpdate (line 279) | pub struct ContentUpdate {
type PendingPresentationFeedback (line 298) | pub struct PendingPresentationFeedback(
type CommitError (line 303) | pub struct CommitError(pub xdg_surface::Error, pub String);
method surface_commit (line 307) | pub fn surface_commit(&mut self, id: SurfaceKey) -> Result<(), CommitErr...
method surface_destroyed (line 474) | pub fn surface_destroyed(&mut self, id: SurfaceKey) {
method set_surface_role (line 485) | pub fn set_surface_role(&mut self, id: SurfaceKey, role: SurfaceRole) ->...
method configure_surfaces (line 497) | pub fn configure_surfaces(&mut self) -> anyhow::Result<()> {
method send_presentation_feedback (line 589) | pub fn send_presentation_feedback(&mut self) -> anyhow::Result<()> {
function buffer_vector_to_surface (line 631) | pub fn buffer_vector_to_surface(coords: impl Into<glam::DVec2>, scale: P...
function surface_vector_to_buffer (line 638) | pub fn surface_vector_to_buffer(coords: impl Into<glam::DVec2>, scale: P...
FILE: mm-server/src/session/compositor/xwayland.rs
type XWayland (line 24) | pub struct XWayland {
method spawn (line 75) | pub fn spawn(
method poll_ready (line 161) | pub fn poll_ready(&mut self) -> anyhow::Result<Option<mio::net::UnixSt...
method prepare_socket (line 184) | pub fn prepare_socket(&self, container: &mut Container) {
constant CONONICAL_DISPLAY_PATH (line 34) | const CONONICAL_DISPLAY_PATH: &str = "/tmp/.X11-unix";
type DisplaySocket (line 36) | pub struct DisplaySocket(u32);
method pick_unused (line 39) | fn pick_unused() -> anyhow::Result<Self> {
method display (line 65) | pub fn display(&self) -> String {
method inner_path (line 69) | pub fn inner_path(&self) -> PathBuf {
function unset_cloexec (line 190) | fn unset_cloexec(socket_fd: impl AsFd) -> Result<(), rustix::io::Errno> {
FILE: mm-server/src/session/compositor/xwayland/xwm.rs
type XWindow (line 65) | pub struct XWindow {
method fmt (line 87) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
type Xwm (line 110) | pub struct Xwm {
method new (line 124) | pub fn new(x11_socket: mio::net::UnixStream) -> anyhow::Result<Self> {
method display_fd (line 227) | pub fn display_fd(&self) -> BorrowedFd {
method xwindow_for_serial (line 231) | pub fn xwindow_for_serial(&self, serial: u64) -> Option<&XWindow> {
method configure_window (line 237) | pub fn configure_window(
method set_focus (line 327) | pub fn set_focus(&self, window: Option<u32>) -> anyhow::Result<()> {
method insert_xwayland (line 383) | pub fn insert_xwayland(
method dispatch_xwm (line 392) | pub fn dispatch_xwm(&mut self) -> anyhow::Result<()> {
method delayed_map_xwin (line 401) | pub fn delayed_map_xwin(&mut self, serial: u64) {
function handle_event (line 423) | fn handle_event(state: &mut Compositor, ev: protocol::Event) -> anyhow::...
function fetch_string_property (line 783) | fn fetch_string_property(
function fetch_class (line 811) | fn fetch_class(
function fetch_hints (line 830) | fn fetch_hints(
function fetch_protocols (line 841) | fn fetch_protocols(
function replace_window_list (line 862) | fn replace_window_list(
function get_atom_name (line 877) | fn get_atom_name(
FILE: mm-server/src/session/control.rs
type DisplayParams (line 16) | pub struct DisplayParams {
type VideoStreamParams (line 24) | pub struct VideoStreamParams {
type AudioStreamParams (line 33) | pub struct AudioStreamParams {
type ControlMessage (line 39) | pub enum ControlMessage {
type SessionEvent (line 89) | pub enum SessionEvent {
FILE: mm-server/src/session/handle.rs
type Client (line 13) | struct Client {
type Inner (line 18) | struct Inner {
type SessionHandle (line 23) | pub struct SessionHandle(Arc<Mutex<Inner>>, Arc<mio::Waker>);
method new (line 26) | pub fn new(waker: Arc<mio::Waker>) -> Self {
method insert_client (line 35) | pub fn insert_client(
method remove_client (line 47) | pub fn remove_client(&self, id: u64) {
method remove_all (line 51) | pub fn remove_all(&self) {
method dispatch (line 55) | pub fn dispatch(&self, event: SessionEvent) {
method dispatch_audio_frame (line 62) | pub fn dispatch_audio_frame(&self, pts: u64, frame: bytes::Bytes, stre...
method dispatch_video_frame (line 77) | pub fn dispatch_video_frame(
method wake (line 101) | pub fn wake(&self) -> std::io::Result<()> {
method kick_clients (line 105) | pub fn kick_clients(&self) {
method num_attachments (line 112) | pub fn num_attachments(&self) -> usize {
FILE: mm-server/src/session/input.rs
type GamepadLayout (line 28) | pub enum GamepadLayout {
type InputDeviceManager (line 35) | pub struct InputDeviceManager {
method new (line 129) | pub fn new(container: &mut Container) -> anyhow::Result<Self> {
method plug_gamepad (line 186) | pub fn plug_gamepad(
type DeviceState (line 40) | struct DeviceState {
type InputManagerState (line 48) | struct InputManagerState {
method device_by_id (line 54) | fn device_by_id(&self, id: u64) -> Option<&DeviceState> {
method device_by_devname (line 58) | fn device_by_devname(&self, name: impl AsRef<OsStr>) -> Option<&Device...
method device_by_eventname (line 62) | fn device_by_eventname(&self, name: impl AsRef<OsStr>) -> Option<&Devi...
type GamepadHandle (line 70) | pub struct GamepadHandle {
method axis (line 76) | pub(crate) fn axis(&mut self, axis_code: u32, value: f64) {
method trigger (line 85) | pub(crate) fn trigger(&mut self, trigger_code: u32, value: f64) {
method input (line 94) | pub(crate) fn input(&mut self, button_code: u32, state: ButtonState) {
method frame (line 119) | pub(crate) fn frame(&mut self) {
function run_in_container_with_gamepads (line 274) | fn run_in_container_with_gamepads<T>(cmd: impl AsRef<[T]>) -> anyhow::Re...
function list_devices_subsystem (line 304) | fn list_devices_subsystem() -> anyhow::Result<()> {
FILE: mm-server/src/session/input/udevfs.rs
constant ENOENT (line 20) | const ENOENT: i32 = rustix::io::Errno::NOENT.raw_os_error();
constant UDEV_INPUT_DATA (line 22) | const UDEV_INPUT_DATA: &[u8] = r#"E:ID_INPUT=1
constant ZERO_TTL (line 33) | const ZERO_TTL: time::Duration = time::Duration::ZERO;
type Entry (line 36) | struct Entry {
type InodeCache (line 43) | struct InodeCache {
method get_or_insert (line 50) | fn get_or_insert(
method lookup_name (line 78) | fn lookup_name(&self, inode: u64) -> Option<(PathBuf, Option<u64>)> {
method reply_add_dirs (line 88) | fn reply_add_dirs<P>(
method cache_dir (line 117) | fn cache_dir(&mut self, p: impl AsRef<Path>, dev: Option<u64>) -> fuse...
method cache_file (line 139) | fn cache_file(&mut self, p: impl AsRef<Path>, dev: Option<u64>, len: u...
method cache_symlink (line 161) | fn cache_symlink(&mut self, p: impl AsRef<Path>, dev: Option<u64>) -> ...
type UdevFs (line 194) | pub struct UdevFs {
method new (line 200) | pub fn new(state: Arc<Mutex<super::InputManagerState>>) -> Self {
method lookup (line 213) | fn lookup(
method getattr (line 381) | fn getattr(
method readlink (line 396) | fn readlink(&mut self, _req: &fuse::Request<'_>, ino: u64, reply: fuse...
method read (line 424) | fn read(
method readdir (line 471) | fn readdir(
method access (line 558) | fn access(&mut self, _req: &fuse::Request<'_>, _ino: u64, _mask: i32, ...
method release (line 562) | fn release(
function make_input_uevent (line 576) | fn make_input_uevent(_dev: &DeviceState) -> Vec<u8> {
function make_evdev_uevent (line 588) | fn make_evdev_uevent(dev: &DeviceState) -> Vec<u8> {
function matches_prefix_with_name (line 598) | fn matches_prefix_with_name(p: &Path, prefix: impl AsRef<Path>) -> Optio...
FILE: mm-server/src/session/reactor.rs
constant READY_TIMEOUT (line 36) | const READY_TIMEOUT: std::time::Duration = time::Duration::from_secs(30);
constant DISPLAY (line 38) | const DISPLAY: mio::Token = mio::Token(0);
constant ACCEPT (line 39) | const ACCEPT: mio::Token = mio::Token(1);
constant CHILD (line 40) | const CHILD: mio::Token = mio::Token(2);
constant WAKER (line 41) | const WAKER: mio::Token = mio::Token(3);
constant TIMER (line 42) | const TIMER: mio::Token = mio::Token(4);
constant XDISPLAY (line 44) | const XDISPLAY: mio::Token = mio::Token(10);
constant XWAYLAND (line 45) | const XWAYLAND: mio::Token = mio::Token(11);
constant XWAYLAND_READY (line 46) | const XWAYLAND_READY: mio::Token = mio::Token(12);
type Reactor (line 48) | pub struct Reactor {
method run (line 86) | pub fn run(
method main_loop (line 325) | fn main_loop(
method idle (line 470) | fn idle(&mut self) -> anyhow::Result<()> {
method active (line 511) | fn active(&self) -> bool {
method update_display_params (line 515) | fn update_display_params(&mut self, params: DisplayParams) -> anyhow::...
method frame (line 602) | fn frame(&mut self) -> anyhow::Result<()> {
method attach (line 636) | fn attach(
method handle_control_message (line 663) | fn handle_control_message(&mut self, msg: ControlMessage) -> anyhow::R...
function gen_socket_name (line 763) | fn gen_socket_name() -> OsString {
function dump_child_output (line 769) | fn dump_child_output(pipe: &mut impl BufRead, debug_log: &mut Option<std...
function save_glxinfo_eglinfo (line 795) | fn save_glxinfo_eglinfo(
FILE: mm-server/src/session/video.rs
type Sink (line 26) | struct Sink(SessionHandle);
method write_frame (line 29) | fn write_frame(
type SwapFrame (line 46) | pub struct SwapFrame {
type TextureSync (line 74) | pub enum TextureSync {
type EncodePipeline (line 79) | pub struct EncodePipeline {
method new (line 95) | pub fn new(
method begin (line 149) | pub unsafe fn begin(&mut self) -> anyhow::Result<bool> {
method composite_surface (line 217) | pub unsafe fn composite_surface(
method end_and_submit (line 365) | pub unsafe fn end_and_submit(&mut self) -> anyhow::Result<VkTimelinePo...
method request_refresh (line 551) | pub fn request_refresh(&mut self) {
method drop (line 557) | fn drop(&mut self) {
function new_swapframe (line 589) | fn new_swapframe(
function allocate_texture_semaphore (line 687) | fn allocate_texture_semaphore(
function format_is_semiplanar (line 707) | fn format_is_semiplanar(format: vk::Format) -> bool {
function cmd_upload_shm (line 726) | pub unsafe fn cmd_upload_shm(
function disjoint_plane_formats (line 759) | fn disjoint_plane_formats(format: vk::Format) -> Option<(vk::Format, vk:...
FILE: mm-server/src/session/video/composite.rs
constant BLEND_FORMAT (line 13) | pub const BLEND_FORMAT: vk::Format = vk::Format::R16G16B16A16_SFLOAT;
type SurfaceColorSpace (line 18) | enum SurfaceColorSpace {
method from (line 25) | fn from(cs: ColorSpace) -> Self {
type SurfacePC (line 37) | struct SurfacePC {
type CompositePipeline (line 49) | pub struct CompositePipeline {
method new (line 58) | pub fn new(vk: Arc<VkContext>) -> anyhow::Result<Self> {
method begin_compositing (line 202) | pub unsafe fn begin_compositing(&self, cb: vk::CommandBuffer, render_t...
method composite_surface (line 249) | pub unsafe fn composite_surface(
method end_compositing (line 311) | pub unsafe fn end_compositing(&self, cb: vk::CommandBuffer) {
method drop (line 317) | fn drop(&mut self) {
FILE: mm-server/src/session/video/convert.rs
type InputTextureColorSpace (line 18) | enum InputTextureColorSpace {
method from (line 25) | fn from(cs: ColorSpace) -> Self {
type OutputProfile (line 37) | enum OutputProfile {
method from (line 43) | fn from(profile: VideoProfile) -> Self {
type ConvertPushConstants (line 53) | struct ConvertPushConstants {
type ConvertPipeline (line 58) | pub struct ConvertPipeline {
method new (line 69) | pub fn new(vk: Arc<VkContext>, semiplanar: bool) -> anyhow::Result<Sel...
method cmd_convert (line 178) | pub unsafe fn cmd_convert(
method ds_for_conversion (line 225) | pub fn ds_for_conversion(
method drop (line 310) | fn drop(&mut self) {
FILE: mm-server/src/state.rs
type SharedState (line 14) | pub type SharedState = Arc<Mutex<ServerState>>;
type ServerState (line 16) | pub struct ServerState {
method new (line 27) | pub fn new(vk: Arc<VkContext>, cfg: Config) -> Self {
method generate_session_id (line 37) | pub fn generate_session_id(&mut self) -> (usize, u64) {
method tick (line 45) | pub fn tick(&mut self) -> anyhow::Result<()> {
FILE: mm-server/src/vulkan.rs
type Vendor (line 26) | pub enum Vendor {
type DriverVersion (line 33) | pub enum DriverVersion {
type VkContext (line 39) | pub struct VkContext {
method new (line 353) | pub fn new(enable_debug: bool) -> Result<Self> {
type VkDebugContext (line 56) | pub struct VkDebugContext {
type VkQueue (line 62) | pub struct VkQueue {
method new (line 72) | pub fn new(
type VkDeviceInfo (line 120) | pub struct VkDeviceInfo {
method query (line 140) | fn query(instance: &ash::Instance, device: vk::PhysicalDevice) -> Resu...
function vulkan_debug_utils_callback (line 633) | unsafe extern "system" fn vulkan_debug_utils_callback(
method drop (line 666) | fn drop(&mut self) {
function init_tracy_context (line 693) | fn init_tracy_context(
function select_memory_type (line 756) | pub fn select_memory_type(
type VkImage (line 780) | pub struct VkImage {
method new (line 791) | pub fn new(
method wrap (line 842) | pub fn wrap(
method extent (line 862) | pub fn extent(&self) -> vk::Extent2D {
method rect (line 869) | pub fn rect(&self) -> vk::Rect2D {
method drop (line 878) | fn drop(&mut self) {
function bind_memory_for_image (line 887) | pub unsafe fn bind_memory_for_image(
function create_image_view (line 928) | pub unsafe fn create_image_view(
type VkHostBuffer (line 963) | pub struct VkHostBuffer {
method new (line 974) | pub fn new(
method wrap (line 1028) | pub(crate) fn wrap(
method copy_from_slice (line 1050) | pub fn copy_from_slice(&mut self, src: &[u8]) {
method drop (line 1057) | fn drop(&mut self) {
type VkTimestampQueryPool (line 1066) | pub struct VkTimestampQueryPool {
method cmd_reset (line 1072) | pub unsafe fn cmd_reset(&self, device: &ash::Device, command_buffer: v...
method fetch_results (line 1076) | pub fn fetch_results(&self, device: &ash::Device) -> anyhow::Result<Ve...
function create_timestamp_query_pool (line 1092) | pub fn create_timestamp_query_pool(
function load_shader (line 1112) | pub fn load_shader(device: &ash::Device, bytes: &[u8]) -> anyhow::Result...
function allocate_command_buffer (line 1121) | pub fn allocate_command_buffer(
function begin_command_buffer (line 1142) | pub unsafe fn begin_command_buffer(
function insert_image_barrier (line 1155) | pub fn insert_image_barrier(
function contains_extension (line 1196) | fn contains_extension(list: &[CString], str: &CStr) -> bool {
FILE: mm-server/src/vulkan/chain.rs
function test_chain (line 134) | fn test_chain() {
FILE: mm-server/src/vulkan/drm.rs
type DrmDevice (line 13) | pub struct DrmDevice(File);
method new (line 25) | pub fn new(dev: dev_t) -> anyhow::Result<Self> {
method as_fd (line 16) | fn as_fd(&self) -> BorrowedFd<'_> {
FILE: mm-server/src/vulkan/timeline.rs
type VkTimelineSemaphore (line 17) | pub struct VkTimelineSemaphore(Arc<Inner>);
method new (line 56) | pub fn new(vk: Arc<VkContext>, initial_value: u64) -> anyhow::Result<S...
method from_syncobj_fd (line 73) | pub fn from_syncobj_fd(vk: Arc<VkContext>, fd: OwnedFd) -> anyhow::Res...
method new_point (line 90) | pub fn new_point(&self, value: u64) -> VkTimelinePoint {
method as_semaphore (line 94) | pub fn as_semaphore(&self) -> vk::Semaphore {
type Inner (line 19) | struct Inner {
type VkTimelinePoint (line 25) | pub struct VkTimelinePoint(Arc<Inner>, u64);
type Output (line 34) | type Output = Self;
method add (line 36) | fn add(self, rhs: u64) -> Self {
method add_assign (line 50) | fn add_assign(&mut self, rhs: u64) {
method value (line 100) | pub fn value(&self) -> u64 {
method timeline (line 104) | pub fn timeline(&self) -> VkTimelineSemaphore {
method wait (line 109) | pub unsafe fn wait(&self) -> anyhow::Result<()> {
method signal (line 122) | pub unsafe fn signal(&self) -> anyhow::Result<()> {
method poll (line 133) | pub unsafe fn poll(&self) -> anyhow::Result<bool> {
function from (line 28) | fn from(value: VkTimelinePoint) -> Self {
type Output (line 42) | type Output = VkTimelinePoint;
function add (line 44) | fn add(self, rhs: u64) -> Self::Output {
method drop (line 141) | fn drop(&mut self) {
FILE: mm-server/src/vulkan/video.rs
type VideoQueueExt (line 9) | pub struct VideoQueueExt {
method new (line 16) | pub fn new(entry: &ash::Entry, instance: &ash::Instance, device: &ash:...
method name (line 26) | pub fn name() -> &'static std::ffi::CStr {
method bind_video_session_memory (line 32) | pub unsafe fn bind_video_session_memory(
method cmd_begin_video_coding (line 49) | pub unsafe fn cmd_begin_video_coding(
method cmd_control_video_coding (line 59) | pub unsafe fn cmd_control_video_coding(
method cmd_end_video_coding (line 69) | pub unsafe fn cmd_end_video_coding(
method create_video_session (line 79) | pub unsafe fn create_video_session(
method create_video_session_parameters (line 96) | pub unsafe fn create_video_session_parameters(
method destroy_video_session (line 113) | pub unsafe fn destroy_video_session(
method destroy_video_session_parameters (line 127) | pub unsafe fn destroy_video_session_parameters(
method get_physical_device_video_capabilities (line 141) | pub unsafe fn get_physical_device_video_capabilities(
method get_physical_device_video_format_properties (line 157) | pub unsafe fn get_physical_device_video_format_properties(
method get_video_session_memory_requirements (line 174) | pub unsafe fn get_video_session_memory_requirements(
method update_video_session_parameters (line 190) | pub unsafe fn update_video_session_parameters(
type VideoDecodeQueueExt (line 204) | pub struct VideoDecodeQueueExt {
method new (line 210) | pub fn new(entry: &ash::Entry, instance: &ash::Instance) -> Self {
method cmd_decode_video (line 220) | pub unsafe fn cmd_decode_video(
type VideoEncodeQueueExt (line 229) | pub struct VideoEncodeQueueExt {
method new (line 236) | pub fn new(entry: &ash::Entry, instance: &ash::Instance, device: &ash:...
method get_physical_device_video_encode_quality_level_properties (line 247) | pub unsafe fn get_physical_device_video_encode_quality_level_properties(
method cmd_encode_video (line 265) | pub unsafe fn cmd_encode_video(
method get_encoded_video_session_parameters (line 275) | pub unsafe fn get_encoded_video_session_parameters(
function read_into_uninitialized_vector (line 303) | pub(crate) unsafe fn read_into_uninitialized_vector<N: Copy + Default + ...
function read_into_defaulted_vector (line 340) | pub(crate) unsafe fn read_into_defaulted_vector<
FILE: mm-server/src/waking_sender.rs
type WakingSender (line 7) | pub struct WakingSender<T> {
method clone (line 13) | fn clone(&self) -> Self {
function new (line 22) | pub fn new(waker: Arc<mio::Waker>, sender: crossbeam_channel::Sender<T>)...
function send (line 31) | pub fn send(&self, msg: T) -> Result<(), crossbeam_channel::SendError<T>> {
function try_send (line 37) | pub fn try_send(&self, msg: T) -> Result<(), crossbeam_channel::TrySendE...
type WakingOneshot (line 44) | pub struct WakingOneshot<T> {
function new (line 50) | pub fn new(waker: Arc<mio::Waker>, sender: oneshot::Sender<T>) -> Self {
function send (line 54) | pub fn send(self, msg: T) -> Result<(), oneshot::SendError<T>> {
FILE: test-apps/bin/color.rs
type ImguiContext (line 28) | struct ImguiContext {
type PushConstants (line 35) | struct PushConstants {
type VkDebugContext (line 42) | struct VkDebugContext {
type DeviceInfo (line 47) | struct DeviceInfo {
type VkQueue (line 53) | pub struct VkQueue {
type Renderer (line 58) | struct Renderer {
method new (line 115) | fn new(window: Rc<winit::window::Window>, debug: bool) -> anyhow::Resu...
method recreate_swapchain (line 411) | unsafe fn recreate_swapchain(&mut self) -> anyhow::Result<()> {
method handle_event (line 745) | fn handle_event<T>(&mut self, event: &winit::event::Event<T>) -> anyho...
method resize (line 766) | fn resize(&mut self, width: u32, height: u32) {
method render (line 776) | unsafe fn render(&mut self) -> anyhow::Result<()> {
method destroy_swapchain (line 1049) | unsafe fn destroy_swapchain(&mut self, mut swapchain: Swapchain) {
type Swapchain (line 88) | struct Swapchain {
type InFlightFrame (line 102) | struct InFlightFrame {
type SwapImage (line 109) | struct SwapImage {
method drop (line 1077) | fn drop(&mut self) {
function main (line 1103) | fn main() -> anyhow::Result<()> {
function query_device (line 1166) | fn query_device(
function get_queue_with_command_pool (line 1223) | fn get_queue_with_command_pool(device: &ash::Device, idx: u32) -> Result...
function create_fence (line 1240) | fn create_fence(device: &ash::Device, signalled: bool) -> Result<vk::Fen...
function create_semaphore (line 1251) | fn create_semaphore(device: &ash::Device) -> Result<vk::Semaphore, vk::R...
function cmd_image_barrier (line 1257) | fn cmd_image_barrier(
function load_shader (line 1296) | fn load_shader(device: &ash::Device, bytes: &[u8]) -> anyhow::Result<vk:...
function format_is_srgb (line 1305) | fn format_is_srgb(format: vk::Format) -> bool {
function colorspace_supported (line 1340) | fn colorspace_supported(colorspace: vk::ColorSpaceKHR) -> bool {
function vulkan_debug_utils_callback (line 1354) | unsafe extern "system" fn vulkan_debug_utils_callback(
FILE: test-apps/bin/cursorlock.rs
function main (line 15) | fn main() {
type Player (line 25) | struct Player;
type CameraSensitivity (line 28) | struct CameraSensitivity(Vec2);
method default (line 31) | fn default() -> Self {
type WorldModelCamera (line 44) | struct WorldModelCamera;
type CursorLocked (line 47) | struct CursorLocked(bool);
constant DEFAULT_RENDER_LAYER (line 52) | const DEFAULT_RENDER_LAYER: usize = 0;
constant VIEW_MODEL_RENDER_LAYER (line 56) | const VIEW_MODEL_RENDER_LAYER: usize = 1;
function spawn_view_model (line 58) | fn spawn_view_model(
function spawn_world_model (line 112) | fn spawn_world_model(
function spawn_lights (line 139) | fn spawn_lights(mut commands: Commands) {
function move_player (line 152) | fn move_player(
function toggle_cursor_lock (line 171) | fn toggle_cursor_lock(input: Res<ButtonInput<KeyCode>>, mut cursor_locke...
function update_cursor (line 177) | fn update_cursor(
FILE: test-apps/bin/latency.rs
constant BLOCK_SIZE (line 11) | const BLOCK_SIZE: f32 = 32.0;
constant STARTING_POS (line 12) | const STARTING_POS: Vec3 = Vec3::new(-BLOCK_SIZE / 2.0, BLOCK_SIZE / 2.0...
type InputMode (line 15) | enum InputMode {
type Cli (line 24) | struct Cli {
type Box (line 33) | struct Box(i8);
function main (line 35) | fn main() {
function setup (line 65) | fn setup(mut commands: Commands, input_mode: Res<InputMode>) {
function move_box (line 86) | fn move_box(
FILE: test-apps/build.rs
function main (line 9) | fn main() {
function compile_shader (line 30) | fn compile_shader(
Condensed preview — 155 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,293K chars).
[
{
"path": ".github/actions/install-slang/action.yaml",
"chars": 782,
"preview": "name: Install slang\ninputs:\n version:\n required: true\n target:\n required: true\n token:\n required: true\nruns:"
},
{
"path": ".github/workflows/bump-version.yaml",
"chars": 3374,
"preview": "on:\n push:\n branches: [main]\nname: Open a PR to bump the version\njobs:\n open_pr:\n strategy:\n matrix:\n "
},
{
"path": ".github/workflows/cliff.toml",
"chars": 1025,
"preview": "[changelog]\nrender_always = true\nbody = \"\"\"\n{% if version %}\\\n ## [{{ version | trim_start_matches(pat=\"v\") }}] - {{ "
},
{
"path": ".github/workflows/docs.yaml",
"chars": 1982,
"preview": "on:\n push:\n branches: [main, docs]\n\nname: Build documentation site\njobs:\n build:\n name: Build\n runs-on: ubunt"
},
{
"path": ".github/workflows/release-mmclient.yaml",
"chars": 5544,
"preview": "on:\n push:\n tags:\n - 'mmclient-v*.*.*'\n\nname: Release mmclient\njobs:\n create_tarball_linux:\n name: Build mm"
},
{
"path": ".github/workflows/release-mmserver.yaml",
"chars": 2050,
"preview": "on:\n push:\n tags:\n - 'mmserver-v*.*.*'\n\nname: Release mmserver\njobs:\n create_release:\n name: Create mmserve"
},
{
"path": ".github/workflows/tests.yaml",
"chars": 1567,
"preview": "on:\n push:\n branches: [main, test-ci]\n pull_request:\n branches: [main]\nname: Tests\njobs:\n tests:\n name: Test"
},
{
"path": ".gitignore",
"chars": 163,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\ntarget\n.vscode\n.reuse\n*.log\nmm-proto"
},
{
"path": ".gitmodules",
"chars": 103,
"preview": "[submodule \"docs/themes/anemone\"]\n\tpath = docs/themes/anemone\n\turl = https://github.com/Speyll/anemone\n"
},
{
"path": ".rustfmt.toml",
"chars": 284,
"preview": "use_field_init_shorthand = true\nuse_try_shorthand = true\n\nunstable_features = true\nformat_code_in_doc_comments = true\nfo"
},
{
"path": "BUILD.md",
"chars": 1372,
"preview": "## Building `mmserver`\n\nThe following are required to build the server and its dependencies:\n\n```\nrust (MSRV 1.77.2)\nnas"
},
{
"path": "CHANGELOG.md",
"chars": 18355,
"preview": "## [mmserver-v0.8.4] - 2025-05-21\n\n### Bugfixes\n\n- Make missing hardware encode support a hard error (b16dccb01902b854a2"
},
{
"path": "LICENSES/BUSL-1.1.txt",
"chars": 4609,
"preview": "Business Source License 1.1\n\nParameters\n----------\n\nLicensor: Colin Marc <hi@colinmarc.com>\nLicensed Work: "
},
{
"path": "LICENSES/MIT.txt",
"chars": 1078,
"preview": "MIT License\n\nCopyright (c) <year> <copyright holders>\n\nPermission is hereby granted, free of charge, to any person obtai"
},
{
"path": "README.md",
"chars": 2090,
"preview": "# Magic Mirror 🪞✨\n[ {\n RED=\"\\033[31m\"\n RESET=\"\\033[0m\"\n echo -e \"${RED}$1${RESET}\"\n exit 1\n}\n\ncase $1 in\n\"cl"
},
{
"path": "docs/.gitignore",
"chars": 49,
"preview": "# autogenerated\ncontent/reference\nbuild/\npublic/\n"
},
{
"path": "docs/config.toml",
"chars": 421,
"preview": "base_url = \"https://colinmarc.github.io/magic-mirror\"\ntheme = \"anemone\"\ncompile_sass = false\nbuild_search_index = false\n"
},
{
"path": "docs/content/_index.md",
"chars": 1421,
"preview": "+++\n+++\n\n# Magic Mirror 🪞✨\n\n<picture>\n <source srcset=\"header_dark.png\" media=\"(prefers-color-scheme: dark)\" />\n <img "
},
{
"path": "docs/content/setup/client.md",
"chars": 1184,
"preview": "+++\ntitle = \"Client Setup\"\n\n[extra]\ntoc = true\n+++\n\n## macOS GUI Client\n\nThe native macOS client can be downloaded from "
},
{
"path": "docs/content/setup/server.md",
"chars": 3255,
"preview": "+++\ntitle = \"Server Setup\"\n\n[extra]\ntoc = true\n+++\n\n## Quickstart\n\nFirst, grab [the latest server release](https://githu"
},
{
"path": "docs/templates/footer.html",
"chars": 0,
"preview": ""
},
{
"path": "mm-client/Cargo.toml",
"chars": 1979,
"preview": "# Copyright 2024 Colin Marc <hi@colinmarc.com>\n#\n# SPDX-License-Identifier: MIT\n\n[package]\nname = \"mm-client\"\nversion = "
},
{
"path": "mm-client/build.rs",
"chars": 1748,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\n// extern crate shaderc;\n\nuse std::p"
},
{
"path": "mm-client/src/audio/buffer.rs",
"chars": 2269,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse std::collections::VecDeque;\n\npub"
},
{
"path": "mm-client/src/audio.rs",
"chars": 12231,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nmod buffer;\n\nuse std::{\n sync::{A"
},
{
"path": "mm-client/src/bin/latency-test.rs",
"chars": 15449,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse std::{sync::Arc, time};\n\nuse any"
},
{
"path": "mm-client/src/bin/mmclient.rs",
"chars": 41975,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse std::{sync::Arc, time};\n\nuse any"
},
{
"path": "mm-client/src/cursor.rs",
"chars": 3356,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse mm_protocol as protocol;\nuse win"
},
{
"path": "mm-client/src/delegate.rs",
"chars": 4962,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse std::sync::Arc;\n\nuse mm_client_c"
},
{
"path": "mm-client/src/flash.rs",
"chars": 1937,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse std::time;\n\nconst FLASH_DURATION"
},
{
"path": "mm-client/src/font.rs",
"chars": 760,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse font_kit::{\n family_name::Fam"
},
{
"path": "mm-client/src/gamepad.rs",
"chars": 10767,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse std::{collections::HashMap, time"
},
{
"path": "mm-client/src/keys.rs",
"chars": 5699,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse mm_protocol::keyboard_input::Key"
},
{
"path": "mm-client/src/lib.rs",
"chars": 272,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\npub mod audio;\npub mod cursor;\npub m"
},
{
"path": "mm-client/src/overlay.rs",
"chars": 4553,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse std::{collections::VecDeque, vec"
},
{
"path": "mm-client/src/render.rs",
"chars": 36422,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\n#![allow(clippy::missing_safety_doc)"
},
{
"path": "mm-client/src/render.slang",
"chars": 3267,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nimport color;\n\n// Should match the d"
},
{
"path": "mm-client/src/stats.rs",
"chars": 3033,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse std::{\n collections::BTreeMap"
},
{
"path": "mm-client/src/video.rs",
"chars": 36512,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse std::{\n sync::{mpsc, Arc},\n "
},
{
"path": "mm-client/src/vulkan.rs",
"chars": 34151,
"preview": "#![allow(clippy::missing_safety_doc)]\n\n// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MI"
},
{
"path": "mm-client-common/Cargo.toml",
"chars": 783,
"preview": "[package]\nname = \"mm-client-common\"\nversion = \"0.1.0\"\nedition = \"2021\"\nlicense = \"MIT\"\n\n[lib]\ncrate-type = [\"lib\", \"stat"
},
{
"path": "mm-client-common/bin/uniffi-bindgen.rs",
"chars": 132,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nfn main() {\n uniffi::uniffi_bindg"
},
{
"path": "mm-client-common/src/attachment.rs",
"chars": 19270,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse std::sync::Arc;\n\nuse async_mutex"
},
{
"path": "mm-client-common/src/codec.rs",
"chars": 172,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse mm_protocol as protocol;\n\npub us"
},
{
"path": "mm-client-common/src/conn/hostport.rs",
"chars": 3210,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\n#[derive(Debug, Eq, PartialEq)]\npub("
},
{
"path": "mm-client-common/src/conn.rs",
"chars": 16165,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nmod hostport;\n\nconst DEFAULT_PORT: u"
},
{
"path": "mm-client-common/src/display_params.rs",
"chars": 1212,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse mm_protocol as protocol;\n\nuse cr"
},
{
"path": "mm-client-common/src/input.rs",
"chars": 1308,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse mm_protocol as protocol;\n\npub us"
},
{
"path": "mm-client-common/src/lib.rs",
"chars": 21007,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse std::{\n collections::{HashMap"
},
{
"path": "mm-client-common/src/logging.rs",
"chars": 2144,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse std::sync::{Arc, OnceLock};\n\n#[d"
},
{
"path": "mm-client-common/src/packet/ring.rs",
"chars": 14948,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse std::collections::{BTreeMap, Vec"
},
{
"path": "mm-client-common/src/packet.rs",
"chars": 1605,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nmod ring;\nuse std::collections::VecD"
},
{
"path": "mm-client-common/src/pixel_scale.rs",
"chars": 1760,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse mm_protocol as protocol;\n\nuse cr"
},
{
"path": "mm-client-common/src/session.rs",
"chars": 2160,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse std::time;\n\nuse mm_protocol as p"
},
{
"path": "mm-client-common/src/stats.rs",
"chars": 887,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse std::{sync::atomic::AtomicU64, t"
},
{
"path": "mm-client-common/src/validation.rs",
"chars": 603,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\n#[derive(Debug, Clone, thiserror::Er"
},
{
"path": "mm-docgen/Cargo.toml",
"chars": 171,
"preview": "[package]\nname = \"mmserver-config-docgen\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[[bin]]\nname = \"config-docgen\"\n\n[[bin]]\nna"
},
{
"path": "mm-docgen/src/bin/config-docgen.rs",
"chars": 3129,
"preview": "//! Generates markdown docs from mmserver.default.toml. Tightly coupled\n//! to the format of that file.\n\nuse std::{\n "
},
{
"path": "mm-docgen/src/bin/protocol-docgen.rs",
"chars": 2218,
"preview": "//! Generates markdown docs from mm-protoco/src/messages.proto. Tightly coupled\n//! to the format of that file.\n\nuse std"
},
{
"path": "mm-protocol/Cargo.toml",
"chars": 363,
"preview": "# Copyright 2024 Colin Marc <hi@colinmarc.com>\n#\n# SPDX-License-Identifier: MIT\n\n[package]\nname = \"mm-protocol\"\nversion "
},
{
"path": "mm-protocol/build.rs",
"chars": 391,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nfn main() -> std::io::Result<()> {\n "
},
{
"path": "mm-protocol/src/lib.rs",
"chars": 8958,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse prost::Message as _;\n\n#[cfg(feat"
},
{
"path": "mm-protocol/src/messages.proto",
"chars": 44393,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nsyntax = \"proto3\";\n\npackage messages"
},
{
"path": "mm-protocol/src/timestamp.rs",
"chars": 948,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse std::time;\n\nuse crate::{Protocol"
},
{
"path": "mm-server/Cargo.toml",
"chars": 3440,
"preview": "# Copyright 2024 Colin Marc <hi@colinmarc.com>\n#\n# SPDX-License-Identifier: BUSL-1.1\n\n[package]\nname = \"mm-server\"\nversi"
},
{
"path": "mm-server/build.rs",
"chars": 3836,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{ffi::CString, path::P"
},
{
"path": "mm-server/deny.toml",
"chars": 317,
"preview": "[licenses]\nallow = [\n \"MIT\",\n \"Apache-2.0\",\n \"Apache-2.0 WITH LLVM-exception\",\n \"BSD-2-Clause\",\n \"BSD-3-C"
},
{
"path": "mm-server/src/codec.rs",
"chars": 1989,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::sync::Arc;\n\nuse anyhow"
},
{
"path": "mm-server/src/color.rs",
"chars": 2369,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\n#![allow(dead_code)]\n\nuse mm_pr"
},
{
"path": "mm-server/src/config.rs",
"chars": 19382,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{\n collections::BTr"
},
{
"path": "mm-server/src/container/ipc.rs",
"chars": 3066,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::os::fd::{AsFd, AsRawFd"
},
{
"path": "mm-server/src/container/runtime.rs",
"chars": 29437,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{\n ffi::{CStr, CStr"
},
{
"path": "mm-server/src/container.rs",
"chars": 2641,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{\n ffi::CStr,\n o"
},
{
"path": "mm-server/src/encoder/dpb.rs",
"chars": 6912,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{collections::BTreeMap"
},
{
"path": "mm-server/src/encoder/gop_structure.rs",
"chars": 7941,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\n#[derive(Debug, Clone, PartialE"
},
{
"path": "mm-server/src/encoder/h264.rs",
"chars": 18780,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::sync::Arc;\n\nuse anyhow"
},
{
"path": "mm-server/src/encoder/h265.rs",
"chars": 24138,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::sync::Arc;\n\nuse anyhow"
},
{
"path": "mm-server/src/encoder/rate_control.rs",
"chars": 4458,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse ash::vk;\nuse tracing::warn;"
},
{
"path": "mm-server/src/encoder/stats.rs",
"chars": 2894,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{sync::Arc, time};\n\nus"
},
{
"path": "mm-server/src/encoder.rs",
"chars": 39151,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\n// It's not me, it's vulkan.\n#!"
},
{
"path": "mm-server/src/main.rs",
"chars": 7897,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nmod codec;\nmod color;\nmod confi"
},
{
"path": "mm-server/src/pixel_scale.rs",
"chars": 2098,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::fmt;\n\nuse anyhow::anyh"
},
{
"path": "mm-server/src/server/handlers/attachment/stats.rs",
"chars": 1484,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::time;\n\nuse simple_movi"
},
{
"path": "mm-server/src/server/handlers/attachment.rs",
"chars": 32934,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{collections::BTreeMap"
},
{
"path": "mm-server/src/server/handlers/validation.rs",
"chars": 6765,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse mm_protocol as protocol;\nus"
},
{
"path": "mm-server/src/server/handlers.rs",
"chars": 12163,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{fs::File, path::Path}"
},
{
"path": "mm-server/src/server/mdns.rs",
"chars": 3078,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::net::SocketAddr;\n\nuse "
},
{
"path": "mm-server/src/server/sendmmsg.rs",
"chars": 2425,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::os::fd::{AsFd, AsRawFd"
},
{
"path": "mm-server/src/server/stream.rs",
"chars": 9801,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse bytes::Bytes;\nuse either::E"
},
{
"path": "mm-server/src/server.rs",
"chars": 31422,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nmod handlers;\nmod mdns;\nmod sen"
},
{
"path": "mm-server/src/session/audio/buffer.rs",
"chars": 10120,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{collections::VecDeque"
},
{
"path": "mm-server/src/session/audio/pulse.rs",
"chars": 34389,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{\n collections::BTr"
},
{
"path": "mm-server/src/session/audio.rs",
"chars": 6271,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{path::Path, sync::Arc"
},
{
"path": "mm-server/src/session/compositor/buffers/modifiers.rs",
"chars": 6950,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{os::fd::AsFd as _, sy"
},
{
"path": "mm-server/src/session/compositor/buffers/syncobj_timeline.rs",
"chars": 2811,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{\n io,\n os::fd::"
},
{
"path": "mm-server/src/session/compositor/buffers.rs",
"chars": 15289,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nmod modifiers;\nmod syncobj_time"
},
{
"path": "mm-server/src/session/compositor/dispatch/shm.rs",
"chars": 6751,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{cell::RefCell, os::fd"
},
{
"path": "mm-server/src/session/compositor/dispatch/wl_buffer.rs",
"chars": 1110,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse wayland_server::protocol::w"
},
{
"path": "mm-server/src/session/compositor/dispatch/wl_compositor.rs",
"chars": 5801,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse tracing::warn;\nuse wayland_"
},
{
"path": "mm-server/src/session/compositor/dispatch/wl_data_device_manager.rs",
"chars": 2351,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse wayland_server::protocol::{"
},
{
"path": "mm-server/src/session/compositor/dispatch/wl_drm.rs",
"chars": 1789,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse crate::session::compositor:"
},
{
"path": "mm-server/src/session/compositor/dispatch/wl_output.rs",
"chars": 1347,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse wayland_server::protocol::w"
},
{
"path": "mm-server/src/session/compositor/dispatch/wl_seat.rs",
"chars": 3879,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse wayland_server::{\n proto"
},
{
"path": "mm-server/src/session/compositor/dispatch/wl_shm.rs",
"chars": 6404,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{\n os::fd::AsRawFd "
},
{
"path": "mm-server/src/session/compositor/dispatch/wp_fractional_scale.rs",
"chars": 2759,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse wayland_protocols::wp::frac"
},
{
"path": "mm-server/src/session/compositor/dispatch/wp_linux_dmabuf.rs",
"chars": 9102,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{\n os::fd::OwnedFd,"
},
{
"path": "mm-server/src/session/compositor/dispatch/wp_linux_drm_syncobj.rs",
"chars": 6131,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse tracing::error;\nuse wayland"
},
{
"path": "mm-server/src/session/compositor/dispatch/wp_pointer_constraints.rs",
"chars": 3596,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse wayland_protocols::wp::poin"
},
{
"path": "mm-server/src/session/compositor/dispatch/wp_presentation.rs",
"chars": 2629,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse wayland_protocols::wp::pres"
},
{
"path": "mm-server/src/session/compositor/dispatch/wp_relative_pointer.rs",
"chars": 2387,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse wayland_protocols::wp::rela"
},
{
"path": "mm-server/src/session/compositor/dispatch/wp_text_input.rs",
"chars": 2167,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse wayland_protocols::wp::text"
},
{
"path": "mm-server/src/session/compositor/dispatch/xdg_shell.rs",
"chars": 8197,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse wayland_protocols::xdg::she"
},
{
"path": "mm-server/src/session/compositor/dispatch/xwayland_shell.rs",
"chars": 2945,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse tracing::trace;\nuse wayland"
},
{
"path": "mm-server/src/session/compositor/dispatch.rs",
"chars": 481,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nmod wl_buffer;\nmod wl_composito"
},
{
"path": "mm-server/src/session/compositor/oneshot_render.rs",
"chars": 1724,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse bytes::Bytes;\nuse drm_fourc"
},
{
"path": "mm-server/src/session/compositor/output.rs",
"chars": 1425,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse wayland_server::{protocol::"
},
{
"path": "mm-server/src/session/compositor/protocols/wayland-drm.xml",
"chars": 7972,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<protocol name=\"drm\">\n\n <copyright>\n Copyright © 2008-2011 Kristian Høgsberg\n"
},
{
"path": "mm-server/src/session/compositor/protocols/wl_drm.rs",
"chars": 573,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\n#![allow(non_upper_case_globals"
},
{
"path": "mm-server/src/session/compositor/protocols.rs",
"chars": 105,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\npub mod wl_drm;\n"
},
{
"path": "mm-server/src/session/compositor/sealed.rs",
"chars": 1242,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{\n ffi::CStr,\n f"
},
{
"path": "mm-server/src/session/compositor/seat.rs",
"chars": 27802,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse bytes::Bytes;\nuse cstr::cst"
},
{
"path": "mm-server/src/session/compositor/serial.rs",
"chars": 516,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::sync::atomic::{AtomicU"
},
{
"path": "mm-server/src/session/compositor/shm.rs",
"chars": 2068,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{\n os::fd::{AsFd, O"
},
{
"path": "mm-server/src/session/compositor/stack.rs",
"chars": 7452,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse tracing::{debug, trace};\nus"
},
{
"path": "mm-server/src/session/compositor/surface.rs",
"chars": 22826,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::time;\n\nuse tracing::{d"
},
{
"path": "mm-server/src/session/compositor/xwayland/xwm.rs",
"chars": 29134,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{\n collections::BTr"
},
{
"path": "mm-server/src/session/compositor/xwayland.rs",
"chars": 6316,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nmod xwm;\nuse std::{\n io::{se"
},
{
"path": "mm-server/src/session/compositor.rs",
"chars": 9887,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{collections::BTreeMap"
},
{
"path": "mm-server/src/session/control.rs",
"chars": 2454,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse crossbeam_channel::Sender;\n"
},
{
"path": "mm-server/src/session/handle.rs",
"chars": 3021,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{collections::BTreeMap"
},
{
"path": "mm-server/src/session/input/udevfs.rs",
"chars": 19365,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{\n collections::BTr"
},
{
"path": "mm-server/src/session/input.rs",
"chars": 10225,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{\n ffi::{OsStr, OsS"
},
{
"path": "mm-server/src/session/reactor.rs",
"chars": 28924,
"preview": "use std::{\n collections::BTreeMap,\n ffi::{OsStr, OsString},\n fs::File,\n io::{BufRead, BufReader},\n os::fd"
},
{
"path": "mm-server/src/session/video/composite.rs",
"chars": 11851,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::sync::Arc;\n\nuse anyhow"
},
{
"path": "mm-server/src/session/video/composite.slang",
"chars": 3287,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nimport color;\n\nconst Sampler2D "
},
{
"path": "mm-server/src/session/video/convert.rs",
"chars": 10633,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::sync::Arc;\n\nuse ash::v"
},
{
"path": "mm-server/src/session/video/convert.slang",
"chars": 3331,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nimport color;\n\nconst Sampler2D "
},
{
"path": "mm-server/src/session/video.rs",
"chars": 27876,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{mem::ManuallyDrop, sy"
},
{
"path": "mm-server/src/session.rs",
"chars": 7681,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{path::PathBuf, sync::"
},
{
"path": "mm-server/src/state.rs",
"chars": 2039,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::sync::Arc;\n\nuse hashbr"
},
{
"path": "mm-server/src/vulkan/chain.rs",
"chars": 5410,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\n/// Used to construct a pinned "
},
{
"path": "mm-server/src/vulkan/drm.rs",
"chars": 764,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{\n fs::{File, OpenO"
},
{
"path": "mm-server/src/vulkan/timeline.rs",
"chars": 3672,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::{\n os::fd::{IntoRaw"
},
{
"path": "mm-server/src/vulkan/video.rs",
"chars": 13006,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse ash::prelude::*;\nuse ash::v"
},
{
"path": "mm-server/src/vulkan.rs",
"chars": 38029,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\n#![allow(clippy::too_many_argum"
},
{
"path": "mm-server/src/waking_sender.rs",
"chars": 1427,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: BUSL-1.1\n\nuse std::sync::Arc;\n\npub struct"
},
{
"path": "mmserver.default.toml",
"chars": 7432,
"preview": "## Copyright 2024 Colin Marc <hi@colinmarc.com>\n##\n## SPDX-License-Identifier: MIT\n##\n## This file specifies the configu"
},
{
"path": "shader-common/color.slang",
"chars": 6587,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nmodule color;\n\n// A set of color pri"
},
{
"path": "test-apps/Cargo.toml",
"chars": 1638,
"preview": "# Copyright 2024 Colin Marc <hi@colinmarc.com>\n#\n# SPDX-License-Identifier: MIT\n\n[package]\nname = \"latency-test\"\nversion"
},
{
"path": "test-apps/bin/color.rs",
"chars": 48130,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse std::{\n ffi::{c_void, CStr, C"
},
{
"path": "test-apps/bin/cursorlock.rs",
"chars": 6206,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\n// Adapted from:\n// https://bevyengi"
},
{
"path": "test-apps/bin/latency.rs",
"chars": 3999,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse bevy::{\n prelude::*,\n wind"
},
{
"path": "test-apps/build.rs",
"chars": 1691,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nuse std::path::PathBuf;\n\nextern crat"
},
{
"path": "test-apps/src/color-test.slang",
"chars": 5509,
"preview": "// Copyright 2024 Colin Marc <hi@colinmarc.com>\n//\n// SPDX-License-Identifier: MIT\n\nstruct PushConstants\n{\n float2 si"
}
]
About this extraction
This page contains the full source code of the colinmarc/magic-mirror GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 155 files (1.2 MB), approximately 292.1k tokens, and a symbol index with 1305 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.