diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 45e8cbf..0c408f0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -10,7 +10,7 @@ env: CARGO_TERM_COLOR: always jobs: - build: + tests: strategy: fail-fast: false matrix: @@ -19,23 +19,82 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Add key + run: wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null + - name: Add repo + env: + UBUNTU: ${{ matrix.os }} + run: echo $UBUNTU &&\ + [[ "${UBUNTU}" == "ubuntu-18.04" ]]&&\ + echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null || echo 1; + - name: Install binary compressor + run: sudo apt-get update && sudo apt-get install upx-ucl xcb libxcb-shape0 libxcb-xfixes0 libxcb-record0 libxcb-shape0-dev libxcb-xfixes0-dev libxcb-record0-dev cmake - name: Add target run: rustup target install x86_64-unknown-linux-musl - - name: Run clippy - run: cargo clippy -- -D warnings - name: Run fmt check run: cargo fmt -- --check - name: Run tests run: cargo test --verbose + clippy: + strategy: + fail-fast: false + matrix: + os: [ ubuntu-18.04, ubuntu-20.04 ] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + - name: Add key + run: wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null + - name: Add repo + env: + UBUNTU: ${{ matrix.os }} + run: echo $UBUNTU &&\ + [[ "${UBUNTU}" == "ubuntu-18.04" ]]&&\ + echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null || echo 1; - name: Install binary compressor - run: sudo apt-get update && sudo apt-get install upx-ucl - - name: Build - run: cargo build --release --verbose --target=x86_64-unknown-linux-musl && strip ./target/x86_64-unknown-linux-musl/release/amdfand && upx --best --lzma target/x86_64-unknown-linux-musl/release/amdfand + run: sudo apt-get update && sudo apt-get install upx-ucl xcb libxcb-shape0 libxcb-xfixes0 libxcb-record0 libxcb-shape0-dev libxcb-xfixes0-dev libxcb-record0-dev cmake + - name: Add target + run: rustup target install x86_64-unknown-linux-musl + - name: Run clippy + run: cargo clippy -- -D warnings + + build: + needs: [clippy, tests] + strategy: + fail-fast: false + matrix: + os: [ ubuntu-18.04, ubuntu-20.04 ] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + - name: Add key + run: wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null + - name: Add repo + env: + UBUNTU: ${{ matrix.os }} + run: echo $UBUNTU &&\ + [[ "${UBUNTU}" == "ubuntu-18.04" ]]&&\ + echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null || echo 1; + - name: Install binary compressor + run: sudo apt-get update && sudo apt-get install upx-ucl xcb libxcb-shape0 libxcb-xfixes0 libxcb-record0 libxcb-shape0-dev libxcb-xfixes0-dev libxcb-record0-dev cmake zip + - name: Add target + run: rustup target install x86_64-unknown-linux-musl + - name: Compile + run: bash ./scripts/compile.sh + - name: Optimize + run: bash ./scripts/build.sh + - name: Collect artifacts + env: + OS: ${{ matrix.os }} + run: ./scripts/zip-ci.sh $OS + - name: Upload a Build Artifact uses: actions/upload-artifact@v2.2.4 with: # Artifact name - name: amdfand-${{ matrix.os }} + name: binaries-${{ matrix.os }}.zip # A file, directory or wildcard pattern that describes what to upload - path: ./target/x86_64-unknown-linux-musl/release/amdfand + path: ./binaries-${{ matrix.os }}.zip # The desired behavior if no files are found using the provided path. diff --git a/Cargo.lock b/Cargo.lock index 7d20d1e..d6ef533 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,49 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ab_glyph" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b228f2c198f98d4337ceb560333fb12cbb2f4948a953bf8c57d09deb219603" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser 0.13.2", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13739d7177fbd22bb0ed28badfff9f372f8bef46c863db4e1c6248f6b223b6e" + +[[package]] +name = "addr2line" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "serde", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -13,13 +56,15 @@ dependencies = [ [[package]] name = "amdfand" -version = "1.0.8" +version = "1.0.9" dependencies = [ "amdgpu", "amdgpu-config", "gumdrop", "log", + "pidlock", "pretty_env_logger", + "ron 0.1.7", "serde", "thiserror", "toml", @@ -27,11 +72,14 @@ dependencies = [ [[package]] name = "amdgpu" -version = "1.0.8" +version = "1.0.9" dependencies = [ "gumdrop", "log", + "nix 0.23.1", + "pidlock", "pretty_env_logger", + "ron 0.7.0", "serde", "thiserror", "toml", @@ -39,7 +87,7 @@ dependencies = [ [[package]] name = "amdgpu-config" -version = "1.0.8" +version = "1.0.9" dependencies = [ "amdgpu", "csv", @@ -51,9 +99,73 @@ dependencies = [ "toml", ] +[[package]] +name = "amdgui-helper" +version = "1.0.9" +dependencies = [ + "amdgpu", + "amdgpu-config", + "amdmond-lib", + "gumdrop", + "log", + "nix 0.23.1", + "pretty_env_logger", + "ron 0.7.0", + "serde", + "sudo", + "thiserror", + "toml", +] + +[[package]] +name = "amdguid" +version = "1.0.9" +dependencies = [ + "amdgpu", + "amdgpu-config", + "amdmond-lib", + "egui", + "egui-winit", + "egui_glium", + "egui_vulkano", + "epaint", + "epi", + "glium", + "gumdrop", + "log", + "nix 0.23.1", + "parking_lot", + "pretty_env_logger", + "serde", + "thiserror", + "tokio", + "toml", + "vulkano", + "vulkano-shaders", + "vulkano-win", + "winit", +] + [[package]] name = "amdmond" -version = "1.0.8" +version = "1.0.9" +dependencies = [ + "amdgpu", + "amdgpu-config", + "amdmond-lib", + "chrono", + "csv", + "gumdrop", + "log", + "pretty_env_logger", + "serde", + "thiserror", + "toml", +] + +[[package]] +name = "amdmond-lib" +version = "1.0.9" dependencies = [ "amdgpu", "amdgpu-config", @@ -69,7 +181,7 @@ dependencies = [ [[package]] name = "amdvold" -version = "1.0.8" +version = "1.0.9" dependencies = [ "amdgpu", "amdgpu-config", @@ -81,6 +193,40 @@ dependencies = [ "toml", ] +[[package]] +name = "andrew" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4afb09dd642feec8408e33f92f3ffc4052946f6b20f32fb99c1f58cd4fa7cf" +dependencies = [ + "bitflags", + "rusttype", + "walkdir", + "xdg", + "xml-rs", +] + +[[package]] +name = "android_glue" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000444226fcff248f2bc4c7625be32c63caccfecc2723a2b9f78a7487a49c407" + +[[package]] +name = "ash" +version = "0.33.3+1.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4f1d82f164f838ae413296d1131aa6fa79b917d25bebaa7033d25620c09219" +dependencies = [ + "libloading 0.7.2", +] + +[[package]] +name = "atomic_refcell" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b5e5f48b927f04e952dedc932f31995a65a0bf65ec971c74436e51bf6e970d" + [[package]] name = "atty" version = "0.2.14" @@ -98,6 +244,39 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "backtrace" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a905d892734eea339e896738c14b9afce22b5318f64b951e70bf3844419b01" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "bstr" version = "0.2.17" @@ -110,12 +289,55 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "calloop" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b036167e76041694579972c28cf4877b4f92da222560ddb49008937b6a6727c" +dependencies = [ + "log", + "nix 0.18.0", +] + +[[package]] +name = "cc" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + [[package]] name = "chrono" version = "0.4.19" @@ -130,6 +352,235 @@ dependencies = [ "winapi", ] +[[package]] +name = "clipboard-win" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" +dependencies = [ + "lazy-bytes-cast", + "winapi", +] + +[[package]] +name = "cmake" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b858541263efe664aead4a5209a4ae5c5d2811167d4ed4ee0944503f8d2089" +dependencies = [ + "cc", +] + +[[package]] +name = "cocoa" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c49e86fc36d5704151f5996b7b3795385f50ce09e3be0f47a0cfde869681cf8" +dependencies = [ + "bitflags", + "block", + "core-foundation 0.7.0", + "core-graphics 0.19.2", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63902e9223530efb4e26ccd0cf55ec30d592d3b42e21a28defc42a9586e832" +dependencies = [ + "bitflags", + "block", + "cocoa-foundation", + "core-foundation 0.9.2", + "core-graphics 0.22.3", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" +dependencies = [ + "bitflags", + "block", + "core-foundation 0.9.2", + "core-graphics-types", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "copypasta" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4423d79fed83ebd9ab81ec21fa97144300a961782158287dc9bf7eddac37ff0b" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "smithay-clipboard", + "x11-clipboard", +] + +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +dependencies = [ + "core-foundation-sys 0.8.3", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "core-graphics" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" +dependencies = [ + "bitflags", + "core-foundation 0.7.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags", + "core-foundation 0.9.2", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" +dependencies = [ + "bitflags", + "core-foundation 0.9.2", + "foreign-types", + "libc", +] + +[[package]] +name = "core-video-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" +dependencies = [ + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "core-graphics 0.19.2", + "libc", + "objc", +] + +[[package]] +name = "crossbeam" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae5588f6b3c3cb05239e90bd110f257254aecd01e4635400391aeae07497845" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b979d76c9fcb84dffc80a73f7290da0f83e4c95773494674cb44b76d13a7a110" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +dependencies = [ + "cfg-if 1.0.0", + "lazy_static", +] + [[package]] name = "csv" version = "1.1.6" @@ -152,6 +603,179 @@ dependencies = [ "memchr", ] +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlib" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b11f15d1e3268f140f68d390637d5e76d849782d971ae7063e0da69fe9709a76" +dependencies = [ + "libloading 0.6.7", +] + +[[package]] +name = "dlib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" +dependencies = [ + "libloading 0.7.2", +] + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "egui" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c8d416a3343cbfc6f4d17bb1cba46b4d7efecb9ee541967763e0b5e04e5fae7" +dependencies = [ + "ahash", + "epaint", + "nohash-hasher", + "ron 0.7.0", + "serde", +] + +[[package]] +name = "egui-winit" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc403e91d1bd693239f1c734193cdf0eb38c8682bbfb9990c4b6cd2db5ee368e" +dependencies = [ + "copypasta", + "egui", + "epi", + "serde", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_glium" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26478ec89b8c9c41a45687a90f9c8fc18106e3ffd8a08559285d625185a2ac92" +dependencies = [ + "egui", + "egui-winit", + "epi", + "glium", +] + +[[package]] +name = "egui_vulkano" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7606011e9ea06cef4f81fde8baffb6b8d43314ca424b4b3f3c06c4c47c842403" +dependencies = [ + "egui", + "epaint", + "thiserror", + "vulkano", + "vulkano-shaders", +] + +[[package]] +name = "emath" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a1aaa922d55da6a2bf32957c3d153e7fb9d52ed8d69777a75092240172eb6e" +dependencies = [ + "serde", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -165,6 +789,170 @@ dependencies = [ "termcolor", ] +[[package]] +name = "epaint" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16bb4d3b8bbbd132c99d2a5efec8567e8b6d09b742f758ae6cf1e4b104fe0231" +dependencies = [ + "ab_glyph", + "ahash", + "atomic_refcell", + "emath", + "nohash-hasher", + "parking_lot", + "serde", +] + +[[package]] +name = "epi" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f5e4e08127f9b86e2c450c96a3032764b63546eb170c2fc54684dc70ff3fc82" +dependencies = [ + "directories-next", + "egui", + "ron 0.7.0", + "serde", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7" + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glium" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "506a2aa1564891d447ae5d1ba37519a8efd6d01ea3e7952da81aa30430c90007" +dependencies = [ + "backtrace", + "fnv", + "gl_generator", + "glutin", + "lazy_static", + "memoffset", + "smallvec", + "takeable-option", +] + +[[package]] +name = "glutin" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "762d6cd2e1b855d99668ebe591cc9058659d85ac39a9a2078000eb122ddba8f0" +dependencies = [ + "android_glue", + "cgl", + "cocoa 0.24.0", + "core-foundation 0.9.2", + "glutin_egl_sys", + "glutin_emscripten_sys", + "glutin_gles2_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "lazy_static", + "libloading 0.7.2", + "log", + "objc", + "osmesa-sys", + "parking_lot", + "wayland-client 0.28.6", + "wayland-egl", + "winapi", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2abb6aa55523480c4adc5a56bbaa249992e2dddb2fc63dc96e04a3355364c211" +dependencies = [ + "gl_generator", + "winapi", +] + +[[package]] +name = "glutin_emscripten_sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80de4146df76e8a6c32b03007bc764ff3249dcaeb4f675d68a06caf1bac363f1" + +[[package]] +name = "glutin_gles2_sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094e708b730a7c8a1954f4f8a31880af00eb8a1c5b5bf85d28a0a3c6d69103" +dependencies = [ + "gl_generator", + "objc", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e393c8fc02b807459410429150e9c4faffdb312d59b8c038566173c81991351" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5951a1569dbab865c6f2a863efafff193a93caf05538d193e9e3816d21696" +dependencies = [ + "gl_generator", +] + [[package]] name = "gumdrop" version = "0.8.0" @@ -185,6 +973,27 @@ dependencies = [ "syn", ] +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.19" @@ -203,12 +1012,64 @@ dependencies = [ "quick-error", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "itoa" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "lazy-bytes-cast" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" + [[package]] name = "lazy_static" version = "1.4.0" @@ -217,9 +1078,38 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.97" +version = "0.2.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" +checksum = "eef78b64d87775463c549fbd80e19249ef436ea3bf1de2a1eb7e717ec7fab1e9" + +[[package]] +name = "libloading" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" +dependencies = [ + "cfg-if 1.0.0", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52" +dependencies = [ + "cfg-if 1.0.0", + "winapi", +] + +[[package]] +name = "lock_api" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +dependencies = [ + "scopeguard", +] [[package]] name = "log" @@ -227,7 +1117,16 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", ] [[package]] @@ -236,6 +1135,219 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +[[package]] +name = "memmap2" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b70ca2a6103ac8b665dc150b142ef0e4e89df640c9e6cf295d189c3caebe5a" +dependencies = [ + "libc", +] + +[[package]] +name = "memmap2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b6c2ebff6180198788f5db08d7ce3bc1d0b617176678831a7510825973e357" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e198a0ee42bdbe9ef2c09d0b9426f3b2b47d90d93a4a9b0395c4cea605e92dc0" +dependencies = [ + "bitflags", + "block", + "cocoa 0.20.2", + "core-graphics 0.19.2", + "foreign-types", + "log", + "objc", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "mio" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "mio-misc" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47412f3a52115b936ff2a229b803498c7b4d332adeb87c2f1498c9da54c398c" +dependencies = [ + "crossbeam", + "crossbeam-queue", + "log", + "mio", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "ndk" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab" +dependencies = [ + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-glue" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" +dependencies = [ + "darling", + "proc-macro-crate 0.1.5", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ndk-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" + +[[package]] +name = "nix" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055" +dependencies = [ + "bitflags", + "cc", + "cfg-if 0.1.10", + "libc", +] + +[[package]] +name = "nix" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", +] + +[[package]] +name = "nix" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1e25ee6b412c2a1e3fcb6a4499a5c1bfe7f43e014bdce9a6b6666e5aa2d187" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", + "memoffset", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" +dependencies = [ + "memchr", + "minimal-lexical", + "version_check", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -255,6 +1367,171 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "720d3ea1055e4e4574c0c0b0f8c3fd4f24c4cdaf465948206dea090b57b526ad" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d992b768490d7fe0d8586d9b5745f6c49f557da6d81dc982b1d167ad4edbb21" +dependencies = [ + "proc-macro-crate 1.1.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55827317fb4c08822499848a14237d2874d6f139828893017237e7ab93eb386" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "osmesa-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88cfece6e95d2e717e0872a7f53a8684712ad13822a7979bc760b9c77ec0013b" +dependencies = [ + "shared_library", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f923fb806c46266c02ab4a5b239735c144bdeda724a50ed058e5226f594cde3" +dependencies = [ + "ttf-parser 0.6.2", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ee3f72636e6f164cc41c9f9057f4e58c4e13507699ea7f5e5242b64b8198ee" +dependencies = [ + "ttf-parser 0.13.4", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pidlock" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9803429506fea3b35e5febd46e954169dd2704c19b81605a3e4c54b84ea8dbe6" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" + +[[package]] +name = "pkg-config" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1a3ea4f0dd7f1f3e512cf97bf100819aa547f36a6eccac8dbaae839eb92363e" + [[package]] name = "pretty_env_logger" version = "0.4.0" @@ -265,6 +1542,25 @@ dependencies = [ "log", ] +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro-crate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" +dependencies = [ + "thiserror", + "toml", +] + [[package]] name = "proc-macro2" version = "1.0.27" @@ -280,6 +1576,15 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-xml" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.9" @@ -289,6 +1594,44 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "raw-window-handle" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28f55143d0548dad60bb4fbdc835a3d7ac6acc3324506450c5fdd6e42903a76" +dependencies = [ + "libc", + "raw-window-handle 0.4.2", +] + +[[package]] +name = "raw-window-handle" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba75eee94a9d5273a68c9e1e105d9cffe1ef700532325788389e5a83e2522b7" +dependencies = [ + "cty", +] + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] + [[package]] name = "regex" version = "1.5.4" @@ -312,12 +1655,69 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "ron" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da06feaa07f69125ab9ddc769b11de29090122170b402547f64b86fe16ebc399" +dependencies = [ + "serde", +] + +[[package]] +name = "ron" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b861ecaade43ac97886a512b360d01d66be9f41f3c61088b42cedf92e03d678" +dependencies = [ + "base64", + "bitflags", + "serde", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "rusttype" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc7c727aded0be18c5b80c1640eae0ac8e396abf6fa8477d96cb37d18ee5ec59" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser 0.6.0", +] + [[package]] name = "ryu" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c9613b5a66ab9ba26415184cfc41156594925a9cf3a2057e57f31ff145f6568" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "serde" version = "1.0.126" @@ -339,16 +1739,140 @@ dependencies = [ ] [[package]] -name = "syn" -version = "1.0.73" +name = "shaderc" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +checksum = "58da8aaf4ad3508598cdf098567114c98d5f455de7d69b1213232ac557bc67ea" +dependencies = [ + "libc", + "shaderc-sys", +] + +[[package]] +name = "shaderc-sys" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd76ec0bd25f2017a65250373485e43cdc81b5cb8fd83c6115375c8d018cdf9" +dependencies = [ + "cmake", + "libc", +] + +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" + +[[package]] +name = "smithay-client-toolkit" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4750c76fd5d3ac95fa3ed80fe667d6a3d8590a960e5b575b98eea93339a80b80" +dependencies = [ + "andrew", + "bitflags", + "calloop", + "dlib 0.4.2", + "lazy_static", + "log", + "memmap2 0.1.0", + "nix 0.18.0", + "wayland-client 0.28.6", + "wayland-cursor 0.28.6", + "wayland-protocols 0.28.6", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "210cf40de565aaaa085face1d860b17f6aee9f76f9d2816307ea2cc45eeb64f3" +dependencies = [ + "bitflags", + "dlib 0.5.0", + "lazy_static", + "log", + "memmap2 0.3.1", + "nix 0.22.0", + "pkg-config", + "wayland-client 0.29.1", + "wayland-cursor 0.29.1", + "wayland-protocols 0.29.1", +] + +[[package]] +name = "smithay-clipboard" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610b551bd25378bfd2b8e7a0fcbd83d427e8f2f6a40c47ae0f70688e9949dd55" +dependencies = [ + "smithay-client-toolkit 0.15.2", + "wayland-client 0.29.1", +] + +[[package]] +name = "spirv_headers" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f5b132530b1ac069df335577e3581765995cba5a13995cdbbdbc8fb057c532c" +dependencies = [ + "bitflags", + "num-traits", +] + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + +[[package]] +name = "sudo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bd84d4c082e18e37fef52c0088e4407dabcef19d23a607fb4b5ee03b7d5b83" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "syn" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] +[[package]] +name = "takeable-option" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36ae8932fcfea38b7d3883ae2ab357b0d57a02caaa18ebb4f5ece08beaec4aa0" + [[package]] name = "termcolor" version = "1.1.2" @@ -389,6 +1913,36 @@ dependencies = [ "winapi", ] +[[package]] +name = "tokio" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" +dependencies = [ + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml" version = "0.5.8" @@ -398,18 +1952,347 @@ dependencies = [ "serde", ] +[[package]] +name = "ttf-parser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e5d7cd7ab3e47dda6e56542f4bbf3824c15234958c6e1bd6aaa347e93499fdc" + +[[package]] +name = "ttf-parser" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76dacc724328b3d5e2ed67f9e30cdb56893a34ab239032502cc8f19f8dae4bbc" + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + [[package]] name = "unicode-xid" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "vk-parse" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c1538fa783f47fb5a6eb495f024f426599a4fe66ff3773ff2252156c64d350a" +dependencies = [ + "xml-rs", +] + +[[package]] +name = "vulkano" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e631daf0d54bf36908ccff9f38ced7ff253a327e396ac13503a11401bee8c38" +dependencies = [ + "ash", + "crossbeam-queue", + "fnv", + "half", + "heck", + "indexmap", + "lazy_static", + "parking_lot", + "regex", + "shared_library", + "smallvec", + "vk-parse", +] + +[[package]] +name = "vulkano-shaders" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f45834e124ae0fdc95a358d83f163d9b14e9ec7780d99cd5aecbf4e76b1a67e" +dependencies = [ + "num-traits", + "proc-macro2", + "quote", + "shaderc", + "spirv_headers", + "syn", +] + +[[package]] +name = "vulkano-win" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5a84fe7d636a1aefdaecabb89b8c47d0b3a785653a38ffbc69a34fb46f6784" +dependencies = [ + "cocoa 0.20.2", + "metal", + "objc", + "raw-window-handle 0.3.4", + "vulkano", + "winit", +] + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "wasm-bindgen" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" + +[[package]] +name = "wayland-client" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ab332350e502f159382201394a78e3cc12d0f04db863429260164ea40e0355" +dependencies = [ + "bitflags", + "downcast-rs", + "libc", + "nix 0.20.0", + "scoped-tls", + "wayland-commons 0.28.6", + "wayland-scanner 0.28.6", + "wayland-sys 0.28.6", +] + +[[package]] +name = "wayland-client" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9108ec1c37f4774d0c2937ba1a6c23d1786b2152c4a13bd9fdb20e42d16e8841" +dependencies = [ + "bitflags", + "downcast-rs", + "libc", + "nix 0.22.0", + "scoped-tls", + "wayland-commons 0.29.1", + "wayland-scanner 0.29.1", + "wayland-sys 0.29.1", +] + +[[package]] +name = "wayland-commons" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21817947c7011bbd0a27e11b17b337bfd022e8544b071a2641232047966fbda" +dependencies = [ + "nix 0.20.0", + "once_cell", + "smallvec", + "wayland-sys 0.28.6", +] + +[[package]] +name = "wayland-commons" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "265ef51b3b3e5c9ef098f10425c39624663f459c3821dcaacc4748be975f1beb" +dependencies = [ + "nix 0.22.0", + "once_cell", + "smallvec", + "wayland-sys 0.29.1", +] + +[[package]] +name = "wayland-cursor" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be610084edd1586d45e7bdd275fe345c7c1873598caa464c4fb835dee70fa65a" +dependencies = [ + "nix 0.20.0", + "wayland-client 0.28.6", + "xcursor", +] + +[[package]] +name = "wayland-cursor" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c19bb6628daf4097e58b7911481e8371e13318d5a60894779901bd3267407a7" +dependencies = [ + "nix 0.22.0", + "wayland-client 0.29.1", + "xcursor", +] + +[[package]] +name = "wayland-egl" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ba1ab1e18756b23982d36f08856d521d7df45015f404a2d7c4f0b2d2f66956" +dependencies = [ + "wayland-client 0.28.6", + "wayland-sys 0.28.6", +] + +[[package]] +name = "wayland-protocols" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "286620ea4d803bacf61fa087a4242ee316693099ee5a140796aaba02b29f861f" +dependencies = [ + "bitflags", + "wayland-client 0.28.6", + "wayland-commons 0.28.6", + "wayland-scanner 0.28.6", +] + +[[package]] +name = "wayland-protocols" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b3b6f1dc0193072ef4eadcb144da30d58c1f2895516c063804d213310703c8e" +dependencies = [ + "bitflags", + "wayland-client 0.29.1", + "wayland-commons 0.29.1", + "wayland-scanner 0.29.1", +] + +[[package]] +name = "wayland-scanner" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce923eb2deb61de332d1f356ec7b6bf37094dc5573952e1c8936db03b54c03f1" +dependencies = [ + "proc-macro2", + "quote", + "xml-rs", +] + +[[package]] +name = "wayland-scanner" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaaf2bc85e7b9143159af96bd23d954a5abe391c4376db712320643280fdc6f4" +dependencies = [ + "proc-macro2", + "quote", + "xml-rs", +] + +[[package]] +name = "wayland-sys" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d841fca9aed7febf9bed2e9796c49bf58d4152ceda8ac949ebe00868d8f0feb8" +dependencies = [ + "dlib 0.5.0", + "lazy_static", + "pkg-config", +] + +[[package]] +name = "wayland-sys" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba9e06acb775b3007f8d3094438306979e572d1d3b844d7a71557a84b055d959" +dependencies = [ + "dlib 0.5.0", + "lazy_static", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222b1ef9334f92a21d3fb53dc3fd80f30836959a90f9274a626d7e06315ba3c3" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecad156490d6b620308ed411cfee90d280b3cbd13e189ea0d3fada8acc89158a" +dependencies = [ + "web-sys", + "widestring", + "winapi", +] + +[[package]] +name = "widestring" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" + [[package]] name = "winapi" version = "0.3.9" @@ -440,3 +2323,90 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winit" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79610794594d5e86be473ef7763f604f2159cbac8c94debd00df8fb41e86c2f8" +dependencies = [ + "bitflags", + "cocoa 0.24.0", + "core-foundation 0.9.2", + "core-graphics 0.22.3", + "core-video-sys", + "dispatch", + "instant", + "lazy_static", + "libc", + "log", + "mio", + "mio-misc", + "ndk", + "ndk-glue", + "ndk-sys", + "objc", + "parking_lot", + "percent-encoding", + "raw-window-handle 0.3.4", + "scopeguard", + "smithay-client-toolkit 0.12.3", + "wayland-client 0.28.6", + "winapi", + "x11-dl", +] + +[[package]] +name = "x11-clipboard" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473068b7b80ac86a18328824f1054e5e007898c47b5bbc281bd7abe32bc3653c" +dependencies = [ + "xcb", +] + +[[package]] +name = "x11-dl" +version = "2.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea26926b4ce81a6f5d9d0f3a0bc401e5a37c6ae14a1bfaa8ff6099ca80038c59" +dependencies = [ + "lazy_static", + "libc", + "pkg-config", +] + +[[package]] +name = "xcb" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771e2b996df720cd1c6dd9ff90f62d91698fd3610cc078388d0564bdd6622a9c" +dependencies = [ + "libc", + "log", + "quick-xml", +] + +[[package]] +name = "xcursor" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" +dependencies = [ + "nom", +] + +[[package]] +name = "xdg" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a23fe958c70412687039c86f578938b4a0bb50ec788e96bce4d6ab00ddd5803" +dependencies = [ + "dirs", +] + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" diff --git a/Cargo.toml b/Cargo.toml index 8841d87..9bfa172 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,11 @@ [workspace] -members = ["amdgpu", "amdgpu-config", "amdfand", "amdvold", "amdmond"] +members = [ + "amdgpu", + "amdgpu-config", + "amdfand", + "amdvold", + "amdmond", + "amdmond-lib", + "amdguid", + "amdgui-helper", +] diff --git a/README.md b/README.md index de4356b..9ddfe9f 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,20 @@ This repository holds couple tools for AMD graphic cards * `amdfand` - fan speed daemon * `amdvold` - voltage and overclocking tool * `amdmond` - monitor daemon +* `amdguid` - GUI manager +* `amdgui-helper` - daemon with elevated privileges to scan for `amdfand` daemons, reload them and save config files For more information please check README each of them. ## Roadmap -* [ ] Add support for multiple cards +* [X] Add support for multiple cards * Multiple services must recognize card even if there's multiple same version cards is installed * Support should be by using `--config` option * [ ] CLI for fan config edit * [ ] CLI for voltage edit -* [ ] GUI application using native Rust framework (ex. egui, druid) +* [X] GUI application using native Rust framework (ex. egui, druid) -## :bookmark: License +## License This work is dual-licensed under Apache 2.0 and MIT. You can choose between one of them if you use this work. diff --git a/.cargo/config b/amdfand/.cargo/config.toml similarity index 100% rename from .cargo/config rename to amdfand/.cargo/config.toml diff --git a/amdfand/Cargo.toml b/amdfand/Cargo.toml index e238a65..2834cd2 100644 --- a/amdfand/Cargo.toml +++ b/amdfand/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "amdfand" -version = "1.0.8" +version = "1.0.9" edition = "2018" description = "AMDGPU fan control service" license = "MIT OR Apache-2.0" @@ -9,17 +9,21 @@ categories = ["hardware-support"] repository = "https://github.com/Eraden/amdgpud" [dependencies] -amdgpu = { path = "../amdgpu", version = "1.0.8" } -amdgpu-config = { path = "../amdgpu-config", version = "1.0.8", features = ["fan"] } +amdgpu = { path = "../amdgpu", version = "1.0.9" } +amdgpu-config = { path = "../amdgpu-config", version = "1.0.9", features = ["fan"] } serde = { version = "1.0.126", features = ["derive"] } toml = { version = "0.5.8" } -thiserror = "1.0.30" +ron = { version = "0.1.0" } + +thiserror = { version = "1.0.30" } gumdrop = { version = "0.8.0" } log = { version = "0.4.14" } pretty_env_logger = { version = "0.4.0" } +pidlock = { version = "0.1.4" } + [dev-dependencies] amdgpu = { path = "../amdgpu", version = "1.0" } amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan"] } diff --git a/amdfand/README.md b/amdfand/README.md index 8168c39..0f7e799 100644 --- a/amdfand/README.md +++ b/amdfand/README.md @@ -19,10 +19,10 @@ Optional arguments: ## Usage ```bash -cargo install argonfand +cargo install amdfand -sudo argonfand monitor # print current temperature, current fan speed, min and max fan speed -sudo argonfand service # check amdgpu temperature and adjust speed from config file +sudo amdfand monitor # print current temperature, current fan speed, min and max fan speed +sudo amdfand service # check amdgpu temperature and adjust speed from config file ``` ## Config file diff --git a/amdfand/src/command.rs b/amdfand/src/command.rs index 95d9707..173c2f3 100644 --- a/amdfand/src/command.rs +++ b/amdfand/src/command.rs @@ -90,7 +90,8 @@ impl Fan { v.into_iter().map(|hw| Self::wrap(hw, config)).collect() } - pub(crate) fn set_speed(&mut self, speed: f64) -> crate::Result<()> { + /// Change fan speed to given value if it's between minimal and maximal value + pub fn set_speed(&mut self, speed: f64) -> crate::Result<()> { let min = self.pwm_min() as f64; let max = self.pwm_max() as f64; let pwm = linear_map(speed, 0f64, 100f64, min, max).round() as u64; @@ -98,18 +99,22 @@ impl Fan { Ok(()) } - pub(crate) fn write_manual(&self) -> crate::Result<()> { + /// Change gpu fan speed management to manual (amdfand will manage speed) instead of + /// GPU embedded manager + pub fn write_manual(&self) -> crate::Result<()> { self.hw_mon_write(MODULATION_ENABLED_FILE, 1) .map_err(FanError::ManualSpeedFailed)?; Ok(()) } - pub(crate) fn write_automatic(&self) -> crate::Result<()> { + /// Change gpu fan speed management to automatic, speed will be managed by GPU embedded manager + pub fn write_automatic(&self) -> crate::Result<()> { self.hw_mon_write("pwm1_enable", 2) .map_err(FanError::AutomaticSpeedFailed)?; Ok(()) } + /// Change fan speed to given value with checking min-max range fn write_pwm(&self, value: u64) -> crate::Result<()> { if self.is_fan_automatic() { self.write_manual()?; @@ -119,12 +124,16 @@ impl Fan { Ok(()) } + /// Check if gpu fan is managed by GPU embedded manager pub fn is_fan_automatic(&self) -> bool { self.hw_mon_read(PULSE_WIDTH_MODULATION_MODE) .map(|s| s.as_str() == PULSE_WIDTH_MODULATION_AUTO) .unwrap_or_default() } + /// Get maximal GPU temperature from all inputs. + /// This is not recommended since GPU can heat differently in different parts and usually only + /// temp1 should be taken for consideration. pub fn max_gpu_temp(&self) -> crate::Result { if let Some(input) = self.temp_input.as_ref() { let value = self.read_gpu_temp(&input.as_string())?; @@ -143,7 +152,8 @@ impl Fan { Ok(value) } - pub(crate) fn read_gpu_temp(&self, name: &str) -> crate::Result { + /// Read temperature from given input sensor + pub fn read_gpu_temp(&self, name: &str) -> crate::Result { let value = self .hw_mon_read(name)? .parse::() @@ -151,6 +161,7 @@ impl Fan { Ok(value) } + /// Read minimal fan speed. Usually this is 0 pub fn pwm_min(&mut self) -> u32 { if self.pwm_min.is_none() { self.pwm_min = Some(self.value_or(PULSE_WIDTH_MODULATION_MIN, 0)); @@ -158,6 +169,7 @@ impl Fan { self.pwm_min.unwrap_or_default() } + /// Read minimal fan speed. Usually this is 255 pub fn pwm_max(&mut self) -> u32 { if self.pwm_max.is_none() { self.pwm_max = Some(self.value_or(PULSE_WIDTH_MODULATION_MAX, 255)); diff --git a/amdfand/src/main.rs b/amdfand/src/main.rs index 86fb9d4..3a0a4a3 100644 --- a/amdfand/src/main.rs +++ b/amdfand/src/main.rs @@ -2,7 +2,10 @@ extern crate log; use gumdrop::Options; -use amdgpu::utils::{ensure_config_dir, hw_mons}; +use amdgpu::{ + lock_file::PidLock, + utils::{ensure_config_dir, hw_mons}, +}; use amdgpu_config::fan::{load_config, Config, DEFAULT_FAN_CONFIG_PATH}; use crate::command::FanCommand; @@ -29,10 +32,16 @@ pub struct Opts { version: bool, #[options(help = "Config location")] config: Option, + #[options( + help = "Pid file name (exp. card1). This should not be path, only file name without extension" + )] + pid_file: Option, #[options(command)] command: Option, } +static DEFAULT_PID_FILE_NAME: &str = "amdfand"; + fn run(config: Config) -> Result<()> { let opts: Opts = Opts::parse_args_default_or_exit(); @@ -47,7 +56,17 @@ fn run(config: Config) -> Result<()> { match opts.command { None => service::run(config), - Some(FanCommand::Service(_)) => service::run(config), + Some(FanCommand::Service(_)) => { + let mut pid_file = PidLock::new( + "amdfand", + opts.pid_file + .unwrap_or_else(|| String::from(DEFAULT_PID_FILE_NAME)), + )?; + pid_file.acquire()?; + let res = service::run(config); + pid_file.release()?; + res + } Some(FanCommand::SetAutomatic(switcher)) => { change_mode::run(switcher, FanMode::Automatic, config) } diff --git a/amdfand/src/service.rs b/amdfand/src/service.rs index d6f37ad..261e1d2 100644 --- a/amdfand/src/service.rs +++ b/amdfand/src/service.rs @@ -1,13 +1,16 @@ use gumdrop::Options; use amdgpu::utils::hw_mons; +use amdgpu::{config_reloaded, is_reload_required, listen_unix_signal}; use amdgpu_config::fan::Config; use crate::command::Fan; use crate::AmdFanError; /// Start service which will change fan speed according to config and GPU temperature -pub fn run(config: Config) -> crate::Result<()> { +pub fn run(mut config: Config) -> crate::Result<()> { + listen_unix_signal(); + let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config); if hw_mons.is_empty() { @@ -15,6 +18,12 @@ pub fn run(config: Config) -> crate::Result<()> { } let mut cache = std::collections::HashMap::new(); loop { + if is_reload_required() { + log::info!("Reloading config..."); + config = config.reload()?; + log::info!(" config reloaded"); + config_reloaded(); + } for hw_mon in hw_mons.iter_mut() { let gpu_temp = config .temp_input() diff --git a/amdgpu-config/Cargo.toml b/amdgpu-config/Cargo.toml index 64423e2..cbe2316 100644 --- a/amdgpu-config/Cargo.toml +++ b/amdgpu-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "amdgpu-config" -version = "1.0.8" +version = "1.0.9" edition = "2021" description = "Subcomponent of AMDGPU tools" license = "MIT OR Apache-2.0" @@ -16,9 +16,10 @@ path = "./src/lib.rs" fan = [] voltage = [] monitor = [] +gui = [] [dependencies] -amdgpu = { path = "../amdgpu", version = "1.0.8" } +amdgpu = { path = "../amdgpu", version = "1.0.9" } serde = { version = "1.0.126", features = ["derive"] } toml = { version = "0.5.8" } diff --git a/amdgpu-config/src/fan.rs b/amdgpu-config/src/fan.rs index e310c18..25dcacf 100644 --- a/amdgpu-config/src/fan.rs +++ b/amdgpu-config/src/fan.rs @@ -3,20 +3,37 @@ use amdgpu::{LogLevel, TempInput}; pub static DEFAULT_FAN_CONFIG_PATH: &str = "/etc/amdfand/config.toml"; -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Default, PartialEq)] pub struct MatrixPoint { pub temp: f64, pub speed: f64, } +impl MatrixPoint { + pub const MIN: MatrixPoint = MatrixPoint { + temp: 0.0, + speed: 0.0, + }; + pub const MAX: MatrixPoint = MatrixPoint { + temp: 100.0, + speed: 100.0, + }; + + pub fn new(temp: f64, speed: f64) -> Self { + Self { temp, speed } + } +} + #[derive(serde::Serialize, serde::Deserialize, Debug)] pub struct Config { - cards: Option>, - log_level: LogLevel, - speed_matrix: Vec, + #[serde(skip)] + path: String, /// One of temperature inputs /sys/class/drm/card{X}/device/hwmon/hwmon{Y}/temp{Z}_input /// If nothing is provided higher reading will be taken (this is not good!) temp_input: Option, + log_level: LogLevel, + cards: Option>, + speed_matrix: Vec, } impl Config { @@ -28,6 +45,23 @@ impl Config { self.cards.as_ref() } + pub fn reload(self) -> Result { + let config = load_config(&self.path)?; + Ok(config) + } + + pub fn speed_matrix(&self) -> &[MatrixPoint] { + &self.speed_matrix + } + + pub fn speed_matrix_mut(&mut self) -> &mut [MatrixPoint] { + &mut self.speed_matrix + } + + pub fn speed_matrix_vec_mut(&mut self) -> &mut Vec { + &mut self.speed_matrix + } + pub fn speed_matrix_point(&self, temp: f64) -> Option<&MatrixPoint> { match self.speed_matrix.iter().rposition(|p| p.temp <= temp) { Some(idx) => self.speed_matrix.get(idx), @@ -62,6 +96,10 @@ impl Config { self.temp_input.as_ref() } + pub fn path(&self) -> &str { + &self.path + } + fn min_speed(&self) -> f64 { self.speed_matrix.first().map(|p| p.speed).unwrap_or(0f64) } @@ -74,6 +112,7 @@ impl Config { impl Default for Config { fn default() -> Self { Self { + path: String::from(DEFAULT_FAN_CONFIG_PATH), #[allow(deprecated)] cards: None, log_level: LogLevel::Error, @@ -143,7 +182,8 @@ pub enum ConfigError { } pub fn load_config(config_path: &str) -> Result { - let config = ensure_config::(config_path)?; + let mut config = ensure_config::(config_path)?; + config.path = String::from(config_path); let mut last_point: Option<&MatrixPoint> = None; @@ -314,3 +354,20 @@ mod speed_for_temp { assert_eq!(config.speed_for_temp(160f64), 100f64); } } + +#[cfg(test)] +mod serde_tests { + use crate::fan::Config; + + #[test] + fn serialize() { + let res = toml::to_string(&Config::default()); + assert!(res.is_ok()); + } + + #[test] + fn deserialize() { + let res = toml::from_str::(&toml::to_string(&Config::default()).unwrap()); + assert!(res.is_ok()); + } +} diff --git a/amdgpu-config/src/gui.rs b/amdgpu-config/src/gui.rs new file mode 100644 index 0000000..9ea4f07 --- /dev/null +++ b/amdgpu-config/src/gui.rs @@ -0,0 +1,51 @@ +use amdgpu::utils::ensure_config; +use amdgpu::LogLevel; + +#[derive(Debug, thiserror::Error)] +pub enum ConfigError { + #[error("{0}")] + Io(#[from] std::io::Error), +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct Config { + /// Minimal log level + log_level: LogLevel, +} + +impl Default for Config { + fn default() -> Self { + Self { + log_level: LogLevel::Error, + } + } +} + +impl Config { + pub fn log_level(&self) -> LogLevel { + self.log_level + } +} + +pub fn load_config(config_path: &str) -> Result { + let config: Config = ensure_config::(config_path)?; + + Ok(config) +} + +#[cfg(test)] +mod serde_tests { + use crate::gui::Config; + + #[test] + fn serialize() { + let res = toml::to_string(&Config::default()); + assert!(res.is_ok()); + } + + #[test] + fn deserialize() { + let res = toml::from_str::(&toml::to_string(&Config::default()).unwrap()); + assert!(res.is_ok()); + } +} diff --git a/amdgpu-config/src/lib.rs b/amdgpu-config/src/lib.rs index 9a127b7..d7a8f29 100644 --- a/amdgpu-config/src/lib.rs +++ b/amdgpu-config/src/lib.rs @@ -1,5 +1,7 @@ #[cfg(feature = "fan")] pub mod fan; +#[cfg(feature = "gui")] +pub mod gui; #[cfg(feature = "monitor")] pub mod monitor; #[cfg(feature = "voltage")] diff --git a/amdgpu-config/src/monitor.rs b/amdgpu-config/src/monitor.rs index 73601b1..881ec26 100644 --- a/amdgpu-config/src/monitor.rs +++ b/amdgpu-config/src/monitor.rs @@ -51,3 +51,20 @@ pub fn load_config(config_path: &str) -> Result { Ok(config) } + +#[cfg(test)] +mod serde_tests { + use crate::monitor::Config; + + #[test] + fn serialize() { + let res = toml::to_string(&Config::default()); + assert!(res.is_ok()); + } + + #[test] + fn deserialize() { + let res = toml::from_str::(&toml::to_string(&Config::default()).unwrap()); + assert!(res.is_ok()); + } +} diff --git a/amdgpu-config/src/voltage.rs b/amdgpu-config/src/voltage.rs index fb381a4..2ac3921 100644 --- a/amdgpu-config/src/voltage.rs +++ b/amdgpu-config/src/voltage.rs @@ -29,3 +29,20 @@ impl Config { pub fn load_config(config_path: &str) -> Result { ensure_config::(config_path) } + +#[cfg(test)] +mod serde_tests { + use crate::voltage::Config; + + #[test] + fn serialize() { + let res = toml::to_string(&Config::default()); + assert!(res.is_ok()); + } + + #[test] + fn deserialize() { + let res = toml::from_str::(&toml::to_string(&Config::default()).unwrap()); + assert!(res.is_ok()); + } +} diff --git a/amdgpu/.cargo/config.toml b/amdgpu/.cargo/config.toml new file mode 100644 index 0000000..a9db1c1 --- /dev/null +++ b/amdgpu/.cargo/config.toml @@ -0,0 +1,7 @@ +[build] +target = "x86_64-unknown-linux-musl" + +[profile.release] +lto = true +panic = "abort" +codegen-units = 1 diff --git a/amdgpu/Cargo.toml b/amdgpu/Cargo.toml index a19fd57..9cdf393 100644 --- a/amdgpu/Cargo.toml +++ b/amdgpu/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "amdgpu" -version = "1.0.8" +version = "1.0.9" edition = "2018" description = "Subcomponent of AMDGPU fan control service" license = "MIT OR Apache-2.0" @@ -8,11 +8,20 @@ keywords = ["hardware", "amdgpu"] categories = ["hardware-support"] repository = "https://github.com/Eraden/amdgpud" +[features] +gui-helper = [] + [dependencies] serde = { version = "1.0.126", features = ["derive"] } toml = { version = "0.5.8" } -thiserror = "1.0.30" +ron = { version = "0.7.0" } + +thiserror = { version = "1.0.30" } gumdrop = { version = "0.8.0" } log = { version = "0.4.14" } pretty_env_logger = { version = "0.4.0" } + +nix = { version = "0.23.1" } + +pidlock = { version = "0.1.4" } diff --git a/amdgpu/src/error.rs b/amdgpu/src/error.rs index b985f41..456154d 100644 --- a/amdgpu/src/error.rs +++ b/amdgpu/src/error.rs @@ -1,3 +1,30 @@ +#[cfg(feature = "gui-helper")] +use crate::helper_cmd::GuiHelperError; +use pidlock::PidlockError; +use std::fmt::{Debug, Display, Formatter}; + +pub struct IoFailure { + pub io: std::io::Error, + pub path: std::path::PathBuf, +} + +impl std::error::Error for IoFailure {} + +impl Debug for IoFailure { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "File system error for {:?}. {:?}", + self.path, self.io + )) + } +} + +impl Display for IoFailure { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{:?}", self)) + } +} + #[derive(Debug, thiserror::Error)] pub enum AmdGpuError { #[error("Card must starts with `card`.")] @@ -10,10 +37,32 @@ pub enum AmdGpuError { InvalidTempInput(String), #[error("Unable to read GPU vendor")] FailedReadVendor, - #[error("{0:?}")] - Io(#[from] std::io::Error), + #[error("{0}")] + Io(#[from] IoFailure), #[error("Card does not have hwmon")] NoAmdHwMon, + #[error("{0:?}")] + PidFile(#[from] PidLockError), + #[cfg(feature = "gui-helper")] + #[error("{0:?}")] + GuiHelper(#[from] GuiHelperError), +} + +#[derive(Debug, thiserror::Error)] +pub enum PidLockError { + #[error("A lock already exists")] + LockExists, + #[error("An operation was attempted in the wrong state, e.g. releasing before acquiring.")] + InvalidState, +} + +impl From for PidLockError { + fn from(e: PidlockError) -> Self { + match e { + pidlock::PidlockError::LockExists => PidLockError::LockExists, + pidlock::PidlockError::InvalidState => PidLockError::InvalidState, + } + } } impl PartialEq for AmdGpuError { @@ -26,7 +75,7 @@ impl PartialEq for AmdGpuError { (InvalidTempInput(a), InvalidTempInput(b)) => a == b, (FailedReadVendor, FailedReadVendor) => true, (NoAmdHwMon, NoAmdHwMon) => true, - (Io(a), Io(b)) => a.kind() == b.kind(), + (Io(a), Io(b)) => a.io.kind() == b.io.kind(), _ => false, } } diff --git a/amdgpu/src/helper_cmd.rs b/amdgpu/src/helper_cmd.rs new file mode 100644 index 0000000..a369d78 --- /dev/null +++ b/amdgpu/src/helper_cmd.rs @@ -0,0 +1,68 @@ +//! AMD GUI helper communication toolkit + +use std::io::{Read, Write}; +use std::ops::Deref; +use std::os::unix::net::UnixStream; + +#[derive(Debug, thiserror::Error)] +pub enum GuiHelperError { + #[error("GUI Helper socket file not found. Is service running?")] + NoSockFile, + #[error("Failed to connect to /var/lib/amdfand/helper.sock. {0}")] + UnableToConnect(#[from] std::io::Error), + #[error("Failed to service helper command. {0}")] + Serialize(#[from] ron::Error), +} + +#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)] +#[serde(transparent)] +pub struct Pid(pub i32); + +impl Deref for Pid { + type Target = i32; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub enum Command { + ReloadConfig { pid: Pid }, + FanServices, + SaveFanConfig { path: String, content: String }, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub enum Response { + NoOp, + Services(Vec), + ConfigFileSaved, + ConfigFileSaveFailed(String), +} + +pub fn sock_file() -> std::path::PathBuf { + std::path::Path::new("/tmp").join("amdgui-helper.sock") +} + +pub fn send_command(cmd: Command) -> crate::Result { + let sock_path = sock_file(); + + if !sock_path.exists() { + return Err(GuiHelperError::NoSockFile.into()); + } + + let mut stream = UnixStream::connect(&sock_path).map_err(GuiHelperError::UnableToConnect)?; + let s = ron::to_string(&cmd).map_err(GuiHelperError::Serialize)?; + if stream.write_all(format!("{}\n", s).as_bytes()).is_ok() { + log::info!("Command send"); + } + + let res: Response = { + let mut s = String::with_capacity(100); + let _ = stream.read_to_string(&mut s); + ron::from_str(&s).map_err(GuiHelperError::Serialize)? + }; + + Ok(res) +} diff --git a/amdgpu/src/hw_mon.rs b/amdgpu/src/hw_mon.rs index 5ee4962..9a09226 100644 --- a/amdgpu/src/hw_mon.rs +++ b/amdgpu/src/hw_mon.rs @@ -1,4 +1,4 @@ -use crate::{utils, AmdGpuError, Card, ROOT_DIR}; +use crate::{utils, AmdGpuError, Card, IoFailure, ROOT_DIR}; #[derive(Debug)] pub struct HwMonName(pub String); @@ -100,7 +100,10 @@ fn hw_mon_dirs_path(card: &Card) -> std::path::PathBuf { pub fn open_hw_mon(card: Card) -> crate::Result { let read_path = hw_mon_dirs_path(&card); - let entries = std::fs::read_dir(read_path)?; + let entries = std::fs::read_dir(&read_path).map_err(|io| IoFailure { + io, + path: read_path, + })?; let name = entries .filter_map(|entry| entry.ok()) .filter_map(|entry| { diff --git a/amdgpu/src/lib.rs b/amdgpu/src/lib.rs index 9413a15..6d158a8 100644 --- a/amdgpu/src/lib.rs +++ b/amdgpu/src/lib.rs @@ -6,7 +6,10 @@ pub use temp_input::*; mod card; mod error; +#[cfg(feature = "gui-helper")] +pub mod helper_cmd; pub mod hw_mon; +pub mod lock_file; mod temp_input; pub mod utils; @@ -30,6 +33,38 @@ pub static PULSE_WIDTH_MODULATION_MODE: &str = "pwm1_enable"; // static PULSE_WIDTH_MODULATION_DISABLED: &str = "0"; pub static PULSE_WIDTH_MODULATION_AUTO: &str = "2"; +static mut RELOAD_CONFIG: bool = false; + +extern "C" fn sig_reload(_n: i32) { + unsafe { + RELOAD_CONFIG = true; + }; +} + +/// Listen for SIGHUP signal. This signal is used to reload config +pub fn listen_unix_signal() { + use nix::sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal}; + unsafe { + let handler: SigHandler = SigHandler::Handler(sig_reload); + let action = SigAction::new(handler, SaFlags::SA_NOCLDWAIT, SigSet::empty()); + sigaction(Signal::SIGHUP, &action).expect("Failed to mount action handler"); + }; +} + +/// Check if application received SIGHUP and must reload config file +#[inline(always)] +pub fn is_reload_required() -> bool { + unsafe { RELOAD_CONFIG } +} + +/// Reset reload config flag +#[inline(always)] +pub fn config_reloaded() { + unsafe { + RELOAD_CONFIG = false; + } +} + pub type Result = std::result::Result; #[derive(Serialize, Deserialize, Debug, Copy, Clone)] diff --git a/amdgpu/src/lock_file.rs b/amdgpu/src/lock_file.rs new file mode 100644 index 0000000..926d990 --- /dev/null +++ b/amdgpu/src/lock_file.rs @@ -0,0 +1,44 @@ +//! Create lock file and prevent running 2 identical services. +//! NOTE: For 2 amdfand services you may just give 2 different names + +use crate::{IoFailure, PidLockError}; +use std::path::Path; + +pub struct PidLock(pidlock::Pidlock); + +impl PidLock { + pub fn new>( + sub_dir: P, + name: String, + ) -> std::result::Result { + let pid_dir_path = std::path::Path::new("/var").join("lib").join(sub_dir); + let pid_path = { + std::fs::create_dir_all(&pid_dir_path).map_err(|io| IoFailure { + io, + path: pid_dir_path.clone(), + })?; + pid_dir_path + .join(format!("{}.pid", name)) + .to_str() + .map(String::from) + .unwrap() + }; + let pid_file = pidlock::Pidlock::new(&pid_path); + Ok(Self(pid_file)) + } + + /// Create new lock file. File will be created if: + /// * pid file does not exists + /// * pid file exists but process is dead + pub fn acquire(&mut self) -> Result<(), crate::error::AmdGpuError> { + self.0.acquire().map_err(PidLockError::from)?; + Ok(()) + } + + /// Remove lock file + /// Remove lock file + pub fn release(&mut self) -> Result<(), crate::error::AmdGpuError> { + self.0.release().map_err(PidLockError::from)?; + Ok(()) + } +} diff --git a/amdgpu/src/temp_input.rs b/amdgpu/src/temp_input.rs index 5e395d3..ad13e1e 100644 --- a/amdgpu/src/temp_input.rs +++ b/amdgpu/src/temp_input.rs @@ -1,6 +1,7 @@ use crate::AmdGpuError; +use serde::Serializer; -#[derive(PartialEq, Debug, Copy, Clone, serde::Serialize)] +#[derive(PartialEq, Debug, Copy, Clone)] pub struct TempInput(pub u16); impl TempInput { @@ -35,6 +36,15 @@ impl std::str::FromStr for TempInput { } } +impl serde::Serialize for TempInput { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.as_string()) + } +} + impl<'de> serde::Deserialize<'de> for TempInput { fn deserialize(deserializer: D) -> Result where diff --git a/amdgpu/src/utils.rs b/amdgpu/src/utils.rs index 917181d..b60afcb 100644 --- a/amdgpu/src/utils.rs +++ b/amdgpu/src/utils.rs @@ -82,6 +82,8 @@ pub fn hw_mons(filter: bool) -> std::io::Result> { .collect()) } +/// Try to read from config file or create new config file. +/// Create only if it does not exists, malformed file will raise error pub fn ensure_config(config_path: P) -> std::result::Result where Config: serde::Serialize + serde::de::DeserializeOwned + Default + Sized, @@ -102,6 +104,7 @@ where } } +/// Scan sysfs for sensor files pub fn load_temp_inputs(hw_mon: &HwMon) -> Vec { let dir = match std::fs::read_dir(hw_mon.mon_dir()) { Ok(d) => d, @@ -117,6 +120,7 @@ pub fn load_temp_inputs(hw_mon: &HwMon) -> Vec { .collect() } +/// Create config directory if does not exists pub fn ensure_config_dir() -> std::io::Result<()> { if std::fs::read(CONFIG_DIR).map_err(|e| e.kind() == ErrorKind::NotFound) == Err(true) { std::fs::create_dir_all(CONFIG_DIR)?; diff --git a/amdgui-helper/.cargo/config b/amdgui-helper/.cargo/config new file mode 100644 index 0000000..a9db1c1 --- /dev/null +++ b/amdgui-helper/.cargo/config @@ -0,0 +1,7 @@ +[build] +target = "x86_64-unknown-linux-musl" + +[profile.release] +lto = true +panic = "abort" +codegen-units = 1 diff --git a/amdgui-helper/.cargo/config.toml b/amdgui-helper/.cargo/config.toml new file mode 100644 index 0000000..a9db1c1 --- /dev/null +++ b/amdgui-helper/.cargo/config.toml @@ -0,0 +1,7 @@ +[build] +target = "x86_64-unknown-linux-musl" + +[profile.release] +lto = true +panic = "abort" +codegen-units = 1 diff --git a/amdgui-helper/Cargo.toml b/amdgui-helper/Cargo.toml new file mode 100644 index 0000000..4b35307 --- /dev/null +++ b/amdgui-helper/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "amdgui-helper" +version = "1.0.9" +edition = "2018" +description = "AMDGPU fan control service" +license = "MIT OR Apache-2.0" +keywords = ["hardware", "amdgpu"] +categories = ["hardware-support"] +repository = "https://github.com/Eraden/amdgpud" + +[dependencies] +amdgpu = { path = "../amdgpu", version = "1.0.9", features = ["gui-helper"] } +amdgpu-config = { path = "../amdgpu-config", version = "1.0.9", features = ["fan", "gui"] } +amdmond-lib = { path = "../amdmond-lib", version = "1.0.9" } + +serde = { version = "1.0.126", features = ["derive"] } +toml = { version = "0.5.8" } +ron = { version = "0.7.0" } + +thiserror = { version = "1.0.30" } +gumdrop = { version = "0.8.0" } + +log = { version = "0.4.14" } +pretty_env_logger = { version = "0.4.0" } + +nix = { version = "0.23.1" } + +sudo = { version = "0.6.0" } + +[dev-dependencies] +amdgpu = { path = "../amdgpu", version = "1.0" } +amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan", "gui"] } +amdmond-lib = { path = "../amdmond-lib", version = "1.0" } diff --git a/amdgui-helper/README.md b/amdgui-helper/README.md new file mode 100644 index 0000000..cc724fe --- /dev/null +++ b/amdgui-helper/README.md @@ -0,0 +1,7 @@ +# amdgui-helper + +Daemon with elevated privileges to scan for `amdfand` daemons, reload them and save config files + +You can communicate with it using sock file `/tmp/amdgui-helper.sock` using helper `Command` from `amdgpu`. + +Each connection is single use and will be terminated after sending `Response`. diff --git a/amdgui-helper/src/main.rs b/amdgui-helper/src/main.rs new file mode 100644 index 0000000..f96f6ec --- /dev/null +++ b/amdgui-helper/src/main.rs @@ -0,0 +1,202 @@ +//! Special daemon with root privileges. Since GUI should not have (and sometimes can't have) root +//! privileges and service processes are designed to be as small as possible this is proxy. +//! +//! It is responsible for: +//! * Loading all amdfand processes. In order to do this process needs to be killed with signal 0 to check if it still is alive +//! * Reload amdfand process with signal SIGHUP +//! * Save changed config file +//! +//! It is using `/tmp/amdgui-helper.sock` file and `ron` serialization for communication. +//! After each operation connection is terminated so each command needs new connection. +#![allow(clippy::non_octal_unix_permissions)] + +use amdgpu::helper_cmd::{Command, Pid, Response}; +use amdgpu::IoFailure; +use std::ffi::OsStr; +use std::fs::Permissions; +use std::io::{Read, Write}; +use std::net::Shutdown; +use std::os::unix::fs::PermissionsExt; +use std::os::unix::net::{UnixListener, UnixStream}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("{0}")] + Io(#[from] amdgpu::IoFailure), + #[error("{0}")] + Lock(#[from] amdgpu::AmdGpuError), +} + +pub type Result = std::result::Result; + +fn main() -> Result<()> { + if std::env::var("RUST_LOG").is_err() { + std::env::set_var("RUST_LOG", "DEBUG"); + } + pretty_env_logger::init(); + + let mut lock = amdgpu::lock_file::PidLock::new("amdgui", String::from("helper"))?; + lock.acquire()?; + + let sock_path = amdgpu::helper_cmd::sock_file(); + let listener = { + let _ = std::fs::remove_file(&sock_path); + + UnixListener::bind(&sock_path).map_err(|io| IoFailure { + io, + path: sock_path.clone(), + })? + }; + if let Err(e) = std::fs::set_permissions(&sock_path, Permissions::from_mode(0x777)) { + log::error!("Failed to change gui helper socket file mode. {:?}", e); + } + + while let Ok((stream, _addr)) = listener.accept() { + handle_connection(stream); + } + + lock.release()?; + Ok(()) +} + +pub struct Service(UnixStream); + +impl Service { + /// Serialize and send command + pub fn write_response(&mut self, res: amdgpu::helper_cmd::Response) { + match ron::to_string(&res) { + Ok(buffer) => match self.0.write_all(buffer.as_bytes()) { + Ok(_) => { + log::info!("Response successfully written") + } + Err(e) => log::warn!("Failed to write response. {:?}", e), + }, + Err(e) => { + log::warn!("Failed to serialize response {:?}. {:?}", res, e) + } + } + } + + /// Read from `.sock` file new line separated commands + pub fn read_command(&mut self) -> Option { + let mut command = String::with_capacity(100); + log::info!("Reading stream..."); + read_line(&mut self.0, &mut command); + if command.is_empty() { + return None; + } + Some(command) + } + + /// Close connection with no operation response + pub fn kill(mut self) { + self.write_response(Response::NoOp); + self.close(); + } + + pub fn close(self) { + let _ = self.0.shutdown(Shutdown::Both); + } +} + +fn handle_connection(stream: UnixStream) { + let mut service = Service(stream); + + let command = match service.read_command() { + Some(s) => s, + _ => return service.kill(), + }; + + log::info!("Incoming {:?}", command); + let cmd = match ron::from_str::(command.trim()) { + Ok(cmd) => cmd, + Err(e) => { + log::warn!("Invalid message {:?}. {:?}", command, e); + return service.kill(); + } + }; + handle_command(service, cmd); +} + +fn handle_command(mut service: Service, cmd: Command) { + match cmd { + Command::ReloadConfig { pid } => { + log::info!("Reloading config file for pid {:?}", pid); + handle_reload_config(service, pid); + } + Command::FanServices => handle_fan_services(service), + Command::SaveFanConfig { path, content } => { + handle_save_fan_config(&mut service, path, content) + } + } +} + +fn handle_save_fan_config(service: &mut Service, path: String, content: String) { + match std::fs::write(path, content) { + Err(e) => service.write_response(Response::ConfigFileSaveFailed(format!("{:?}", e))), + Ok(..) => service.write_response(Response::ConfigFileSaved), + } +} + +fn handle_fan_services(mut service: Service) { + log::info!("Loading fan services"); + let services = read_fan_services(); + log::info!("Loaded fan services pid {:?}", services); + service.write_response(Response::Services(services)); +} + +fn read_line(stream: &mut UnixStream, command: &mut String) { + let mut buffer = [0]; + while stream.read_exact(&mut buffer).is_ok() { + if buffer[0] == b'\n' { + break; + } + match std::str::from_utf8(&buffer) { + Ok(s) => { + command.push_str(s); + } + Err(e) => { + log::error!("Failed to read from client. {:?}", e); + let _ = stream.shutdown(Shutdown::Both); + continue; + } + } + } +} + +fn handle_reload_config(service: Service, pid: Pid) { + unsafe { + nix::libc::kill(pid.0, nix::sys::signal::Signal::SIGHUP as i32); + } + service.kill(); +} + +fn read_fan_services() -> Vec { + if let Ok(entry) = std::fs::read_dir("/var/lib/amdfand") { + entry + .filter(|e| { + e.as_ref() + .map(|e| { + log::info!("Extension is {:?}", e.path().extension()); + e.path().extension().and_then(OsStr::to_str) == Some("pid") + }) + .ok() + .unwrap_or_default() + }) + .filter_map(|e| { + log::info!("Found entry {:?}", e); + match e { + Ok(entry) => std::fs::read_to_string(entry.path()) + .ok() + .and_then(|s| s.parse::().ok()) + .filter(|pid| unsafe { nix::libc::kill(*pid, 0) } == 0), + _ => None, + } + }) + .map(Pid) + .collect() + } else { + log::warn!("Directory /var/lib/amdfand not found"); + vec![] + } +} diff --git a/amdguid/.cargo/config b/amdguid/.cargo/config new file mode 100644 index 0000000..308d96b --- /dev/null +++ b/amdguid/.cargo/config @@ -0,0 +1,7 @@ +[build] +target = "x86_64-unknown-linux-gnu" + +[profile.release] +lto = true +panic = "abort" +codegen-units = 1 diff --git a/amdguid/.cargo/config.toml b/amdguid/.cargo/config.toml new file mode 100644 index 0000000..308d96b --- /dev/null +++ b/amdguid/.cargo/config.toml @@ -0,0 +1,7 @@ +[build] +target = "x86_64-unknown-linux-gnu" + +[profile.release] +lto = true +panic = "abort" +codegen-units = 1 diff --git a/amdguid/Cargo.toml b/amdguid/Cargo.toml new file mode 100644 index 0000000..405c39a --- /dev/null +++ b/amdguid/Cargo.toml @@ -0,0 +1,64 @@ +[package] +name = "amdguid" +version = "1.0.9" +edition = "2018" +description = "AMDGPU fan control service" +license = "MIT OR Apache-2.0" +keywords = ["hardware", "amdgpu"] +categories = ["hardware-support"] +repository = "https://github.com/Eraden/amdgpud" + +[features] +wayland = [ + "egui_vulkano", + "vulkano-win", + "vulkano", + "vulkano-shaders", + "_gui" +] +xorg = ["glium", "egui_glium", "_gui"] +default = ["wayland"] +_gui = [ + "egui", + "epaint", + "epi", + "winit", + "egui-winit", +] + +[dependencies] +amdgpu = { path = "../amdgpu", version = "1.0.9", features = ["gui-helper"] } +amdgpu-config = { path = "../amdgpu-config", version = "1.0.9", features = ["fan", "gui"] } +amdmond-lib = { path = "../amdmond-lib", version = "1.0.9" } + +serde = { version = "1.0.126", features = ["derive"] } +toml = { version = "0.5.8" } +thiserror = { version = "1.0.30" } +gumdrop = { version = "0.8.0" } + +log = { version = "0.4.14" } +pretty_env_logger = { version = "0.4.0" } + +egui = { version = "0.15.0", optional = true } +epaint = { version = "0.15.0", features = ["serialize"], optional = true } +epi = { version = "0.15.0", optional = true } +winit = { version = "0.25.0", optional = true } +egui-winit = { version = "0.15.0", optional = true } +# vulkan +egui_vulkano = { version = "0.4.0", optional = true } +vulkano-win = { version = "0.25.0", optional = true } +vulkano = { version = "0.25.0", optional = true } +vulkano-shaders = { version = "0.25.0", optional = true } +# xorg +glium = { version = "0.30", optional = true } +egui_glium = { version = "0.15.0", optional = true } + +tokio = { version = "1.15.0", features = ["full"] } +parking_lot = { version = "0.11.2" } + +nix = { version = "0.23.1" } + +[dev-dependencies] +amdgpu = { path = "../amdgpu", version = "1.0", features = ["gui-helper"] } +amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan", "gui"] } +amdmond-lib = { path = "../amdmond-lib", version = "1.0" } diff --git a/amdguid/README.md b/amdguid/README.md new file mode 100644 index 0000000..6a7eadf --- /dev/null +++ b/amdguid/README.md @@ -0,0 +1,15 @@ +# AMD GPU gui tool + +Provides basic FAN configuration. + +## Roadmap + +* amdvold config manipulation +* Fix Drag & drop functionality - mouse is not followed properly +* Program profiles + +## Screenshots + +![Alt text](https://static.ita-prog.pl/amdgpud/assets/config.png) +![Alt text](https://static.ita-prog.pl/amdgpud/assets/monitoring.png) +![Alt text](https://static.ita-prog.pl/amdgpud/assets/settings.png) diff --git a/amdguid/src/app.rs b/amdguid/src/app.rs new file mode 100644 index 0000000..adfde76 --- /dev/null +++ b/amdguid/src/app.rs @@ -0,0 +1,151 @@ +use crate::widgets::{ChangeFanSettings, CoolingPerformance}; +use amdgpu::helper_cmd::Pid; +use egui::{CtxRef, Ui}; +use epi::Frame; +use parking_lot::Mutex; +use std::collections::HashMap; +use std::sync::Arc; + +pub enum ChangeState { + New, + Reloading, + Success, + Failure(String), +} + +impl Default for ChangeState { + fn default() -> Self { + ChangeState::New + } +} + +pub struct FanService { + pub pid: Pid, + pub reload: ChangeState, +} + +impl FanService { + pub fn new(pid: Pid) -> FanService { + Self { + pid, + reload: Default::default(), + } + } +} + +pub struct FanServices(pub Vec); + +impl FanServices { + pub fn list_changed(&self, other: &[Pid]) -> bool { + if self.0.len() != other.len() { + return true; + } + let c = self + .0 + .iter() + .fold(HashMap::with_capacity(other.len()), |mut h, service| { + h.insert(service.pid.0, true); + h + }); + !other.iter().all(|s| c.contains_key(&s.0)) + } +} + +impl From> for FanServices { + fn from(v: Vec) -> Self { + Self(v.into_iter().map(FanService::new).collect()) + } +} + +#[derive(Debug, Copy, Clone)] +pub enum Page { + Config, + Monitoring, + Settings, +} + +impl Default for Page { + fn default() -> Self { + Self::Config + } +} + +pub type FanConfig = Arc>; + +static RELOAD_PID_LIST_DELAY: u8 = 18; + +pub struct StatefulConfig { + pub config: FanConfig, + pub state: ChangeState, +} + +pub struct AmdGui { + pub page: Page, + pid_files: FanServices, + cooling_performance: CoolingPerformance, + change_fan_settings: ChangeFanSettings, + config: StatefulConfig, + reload_pid_list_delay: u8, +} + +impl epi::App for AmdGui { + fn update(&mut self, _ctx: &CtxRef, _frame: &mut Frame<'_>) {} + + fn name(&self) -> &str { + "AMD GUI" + } +} + +impl AmdGui { + pub fn new_with_config(config: FanConfig) -> Self { + Self { + page: Default::default(), + pid_files: FanServices::from(vec![]), + cooling_performance: CoolingPerformance::new(100, config.clone()), + change_fan_settings: ChangeFanSettings::new(config.clone()), + config: StatefulConfig { + config, + state: ChangeState::New, + }, + reload_pid_list_delay: RELOAD_PID_LIST_DELAY, + } + } + + pub fn ui(&mut self, ui: &mut Ui) { + match self.page { + Page::Config => { + self.change_fan_settings + .draw(ui, &mut self.pid_files, &mut self.config); + } + Page::Monitoring => { + self.cooling_performance.draw(ui, &self.pid_files); + } + Page::Settings => {} + } + } + + pub fn tick(&mut self) { + self.cooling_performance.tick(); + if self.pid_files.0.is_empty() || self.reload_pid_list_delay.checked_sub(1).is_none() { + self.reload_pid_list_delay = RELOAD_PID_LIST_DELAY; + match amdgpu::helper_cmd::send_command(amdgpu::helper_cmd::Command::FanServices) { + Ok(amdgpu::helper_cmd::Response::Services(services)) + if self.pid_files.list_changed(&services) => + { + self.pid_files = FanServices::from(services); + } + Ok(amdgpu::helper_cmd::Response::Services(_services)) => { + // SKIP + } + Ok(res) => { + log::warn!("Unexpected response {:?} while loading fan services", res); + } + Err(e) => { + log::warn!("Failed to load amd fan services pid list. {:?}", e); + } + } + } else { + self.reload_pid_list_delay -= 1; + } + } +} diff --git a/amdguid/src/backend/mod.rs b/amdguid/src/backend/mod.rs new file mode 100644 index 0000000..c33963e --- /dev/null +++ b/amdguid/src/backend/mod.rs @@ -0,0 +1,10 @@ +#[cfg(feature = "wayland")] +pub mod wayland; +#[cfg(feature = "xorg")] +pub mod xorg; + +#[cfg(feature = "wayland")] +pub use wayland::run_app; + +#[cfg(feature = "xorg")] +pub use xorg::run_app; diff --git a/amdguid/src/backend/wayland.rs b/amdguid/src/backend/wayland.rs new file mode 100644 index 0000000..31ba9ce --- /dev/null +++ b/amdguid/src/backend/wayland.rs @@ -0,0 +1,393 @@ +use crate::app::{AmdGui, Page}; +use egui::panel::TopBottomSide; +use egui::{Layout, PointerButton}; +use epaint::TextStyle; +use parking_lot::Mutex; +use std::sync::Arc; +use vulkano::buffer::{BufferUsage, CpuAccessibleBuffer}; +use vulkano::command_buffer::{ + AutoCommandBufferBuilder, CommandBufferUsage, DynamicState, SubpassContents, +}; +use vulkano::format::Format; +use vulkano::image::view::ImageView; +use vulkano::image::{ImageUsage, SwapchainImage}; +use vulkano::render_pass::{Framebuffer, FramebufferAbstract, RenderPass, Subpass}; +use vulkano::swapchain::{AcquireError, ColorSpace, Swapchain, SwapchainCreationError}; +use vulkano::sync::{FlushError, GpuFuture}; +use vulkano::{swapchain, sync, Version}; +use vulkano_win::VkSurfaceBuild; +use winit::dpi::PhysicalSize; +use winit::event::{Event, WindowEvent}; +use winit::event_loop::ControlFlow; +use winit::window::Window; + +pub mod vs { + vulkano_shaders::shader! { + ty: "vertex", + src: " + #version 450 + layout(location = 0) in vec2 position; + void main() { + gl_Position = vec4(position, 0.0, 1.0); + } + " + } +} + +pub mod fs { + vulkano_shaders::shader! { + ty: "fragment", + src: " + #version 450 + layout(location = 0) out vec4 f_color; + void main() { + f_color = vec4(1.0, 0.0, 0.0, 1.0); + } + " + } +} + +#[derive(Default, Debug, Clone)] +struct Vertex { + position: [f32; 2], +} + +pub fn run_app(amd_gui: Arc>) { + let required_extensions = vulkano_win::required_extensions(); + let instance = + vulkano::instance::Instance::new(None, Version::V1_0, &required_extensions, None).unwrap(); + let physical = vulkano::device::physical::PhysicalDevice::enumerate(&instance) + .next() + .unwrap(); + + let event_loop = winit::event_loop::EventLoop::new(); + let surface = winit::window::WindowBuilder::new() + .with_inner_size(PhysicalSize::new(1024, 768)) + .with_title("AMD GUI") + .build_vk_surface(&event_loop, instance.clone()) + .unwrap(); + + // vulkan + let queue_family = physical + .queue_families() + .find(|&q| q.supports_graphics() && surface.is_supported(q).unwrap_or(false)) + .unwrap(); + + let device_ext = vulkano::device::DeviceExtensions { + khr_swapchain: true, + ..vulkano::device::DeviceExtensions::none() + }; + let (device, mut queues) = vulkano::device::Device::new( + physical, + physical.supported_features(), + &device_ext, + [(queue_family, 0.5)].iter().cloned(), + ) + .unwrap(); + + let queue = queues.next().unwrap(); + + let (mut swapchain, images) = { + let caps = surface.capabilities(physical).unwrap(); + let alpha = caps.supported_composite_alpha.iter().next().unwrap(); + + assert!(&caps + .supported_formats + .contains(&(Format::B8G8R8A8Srgb, ColorSpace::SrgbNonLinear))); + let format = Format::B8G8R8A8Srgb; + let dimensions: [u32; 2] = surface.window().inner_size().into(); + + Swapchain::start(device.clone(), surface.clone()) + .num_images(caps.min_image_count) + .format(format) + .dimensions(dimensions) + .usage(ImageUsage::color_attachment()) + .sharing_mode(&queue) + .composite_alpha(alpha) + .build() + .unwrap() + }; + + vulkano::impl_vertex!(Vertex, position); + + let vertex_buffer = { + CpuAccessibleBuffer::from_iter( + device.clone(), + BufferUsage::all(), + false, + [ + Vertex { + position: [-0.5, -0.25], + }, + Vertex { + position: [0.0, 0.5], + }, + Vertex { + position: [0.25, -0.1], + }, + ] + .iter() + .cloned(), + ) + .unwrap() + }; + + let vs = vs::Shader::load(device.clone()).unwrap(); + let fs = fs::Shader::load(device.clone()).unwrap(); + + let render_pass = Arc::new( + vulkano::ordered_passes_renderpass!( + device.clone(), + attachments: { + color: { + load: Clear, + store: Store, + format: swapchain.format(), + samples: 1, + } + }, + passes: [ + { color: [color], depth_stencil: {}, input: [] }, + { color: [color], depth_stencil: {}, input: [] } // Create a second render pass to draw egui + ] + ) + .unwrap(), + ); + + let pipeline = Arc::new( + vulkano::pipeline::GraphicsPipeline::start() + .vertex_input_single_buffer::() + .vertex_shader(vs.main_entry_point(), ()) + .triangle_list() + .viewports_dynamic_scissors_irrelevant(1) + .fragment_shader(fs.main_entry_point(), ()) + .render_pass(Subpass::from(render_pass.clone(), 0).unwrap()) + .build(device.clone()) + .unwrap(), + ); + + let mut dynamic_state = DynamicState { + line_width: None, + viewports: None, + scissors: None, + compare_mask: None, + write_mask: None, + reference: None, + }; + + let mut framebuffers = + window_size_dependent_setup(&images, render_pass.clone(), &mut dynamic_state); + + let mut recreate_swap_chain = false; + + let mut previous_frame_end = Some(sync::now(device.clone()).boxed()); + + let window = surface.window(); + let mut egui_ctx = egui::CtxRef::default(); + let mut egui_winit = egui_winit::State::new(window); + + let mut egui_painter = egui_vulkano::Painter::new( + device.clone(), + queue.clone(), + Subpass::from(render_pass.clone(), 1).unwrap(), + ) + .unwrap(); + + event_loop.run(move |event, _, control_flow| { + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => { + *control_flow = ControlFlow::Exit; + } + Event::WindowEvent { + event: WindowEvent::Resized(_), + .. + } => { + recreate_swap_chain = true; + } + Event::WindowEvent { event, .. } => { + let egui_consumed_event = egui_winit.on_event(&egui_ctx, &event); + if !egui_consumed_event { + // do your own event handling here + }; + } + Event::RedrawEventsCleared => { + previous_frame_end.as_mut().unwrap().cleanup_finished(); + + if recreate_swap_chain { + let dimensions: [u32; 2] = surface.window().inner_size().into(); + let (new_swap_chain, new_images) = + match swapchain.recreate().dimensions(dimensions).build() { + Ok(r) => r, + Err(SwapchainCreationError::UnsupportedDimensions) => return, + Err(e) => panic!("Failed to recreate swap chain: {:?}", e), + }; + + swapchain = new_swap_chain; + framebuffers = window_size_dependent_setup( + &new_images, + render_pass.clone(), + &mut dynamic_state, + ); + recreate_swap_chain = false; + } + + let (image_num, suboptimal, acquire_future) = + match swapchain::acquire_next_image(swapchain.clone(), None) { + Ok(r) => r, + Err(AcquireError::OutOfDate) => { + recreate_swap_chain = true; + return; + } + Err(e) => panic!("Failed to acquire next image: {:?}", e), + }; + + if suboptimal { + recreate_swap_chain = true; + } + + let clear_values = vec![[0.0, 0.0, 1.0, 1.0].into()]; + let mut builder = AutoCommandBufferBuilder::primary( + device.clone(), + queue.family(), + CommandBufferUsage::OneTimeSubmit, + ) + .unwrap(); + + // Do your usual rendering + builder + .begin_render_pass( + framebuffers[image_num].clone(), + SubpassContents::Inline, + clear_values, + ) + .unwrap() + .draw( + pipeline.clone(), + &dynamic_state, + vertex_buffer.clone(), + (), + (), + ) + .unwrap(); // Don't end the render pass yet + + egui_ctx.begin_frame(egui_winit.take_egui_input(surface.window())); + + egui::containers::TopBottomPanel::new(TopBottomSide::Top, "menu").show( + &egui_ctx, + |ui| { + let mut child = + ui.child_ui(ui.available_rect_before_wrap(), Layout::left_to_right()); + if child + .add(egui::Button::new("Config").text_style(TextStyle::Heading)) + .clicked_by(PointerButton::Primary) + { + amd_gui.lock().page = Page::Config; + } + if child + .add(egui::Button::new("Monitoring").text_style(TextStyle::Heading)) + .clicked_by(PointerButton::Primary) + { + amd_gui.lock().page = Page::Monitoring; + } + if child + .add(egui::Button::new("Settings").text_style(TextStyle::Heading)) + .clicked_by(PointerButton::Primary) + { + amd_gui.lock().page = Page::Settings; + } + }, + ); + + egui::containers::CentralPanel::default().show(&egui_ctx, |ui| { + let mut gui = amd_gui.lock(); + let page = gui.page; + match page { + Page::Config => { + gui.ui(ui); + } + Page::Monitoring => { + gui.ui(ui); + } + Page::Settings => { + egui_ctx.settings_ui(ui); + } + } + }); + + let (egui_output, clipped_shapes) = egui_ctx.end_frame(); + egui_winit.handle_output(surface.window(), &egui_ctx, egui_output); + let size = surface.window().inner_size(); + egui_painter + .draw( + &mut builder, + &dynamic_state, + [size.width as f32, size.height as f32], + &egui_ctx, + clipped_shapes, + ) + .unwrap(); + + // End the render pass as usual + builder.end_render_pass().unwrap(); + + let command_buffer = builder.build().unwrap(); + + let future = previous_frame_end + .take() + .unwrap() + .join(acquire_future) + .then_execute(queue.clone(), command_buffer) + .unwrap() + .then_swapchain_present(queue.clone(), swapchain.clone(), image_num) + .then_signal_fence_and_flush(); + + match future { + Ok(future) => { + previous_frame_end = Some(future.boxed()); + } + Err(FlushError::OutOfDate) => { + recreate_swap_chain = true; + previous_frame_end = Some(sync::now(device.clone()).boxed()); + } + Err(e) => { + println!("Failed to flush future: {:?}", e); + previous_frame_end = Some(sync::now(device.clone()).boxed()); + } + } + } + _ => (), + } + }); +} + +fn window_size_dependent_setup( + images: &[Arc>], + render_pass: Arc, + dynamic_state: &mut DynamicState, +) -> Vec> { + let dimensions = images[0].dimensions(); + + let viewport = vulkano::pipeline::viewport::Viewport { + origin: [0.0, 0.0], + dimensions: [dimensions[0] as f32, dimensions[1] as f32], + depth_range: 0.0..1.0, + }; + dynamic_state.viewports = Some(vec![viewport]); + + images + .iter() + .map(|image| { + let view = ImageView::new(image.clone()).unwrap(); + Arc::new( + Framebuffer::start(render_pass.clone()) + .add(view) + .unwrap() + .build() + .unwrap(), + ) as Arc + }) + .collect::>() +} diff --git a/amdguid/src/backend/xorg.rs b/amdguid/src/backend/xorg.rs new file mode 100644 index 0000000..abece09 --- /dev/null +++ b/amdguid/src/backend/xorg.rs @@ -0,0 +1,115 @@ +use egui::panel::TopBottomSide; +use egui::{Layout, PointerButton}; +use epaint::TextStyle; +use parking_lot::Mutex; +use std::sync::Arc; + +use crate::app::{AmdGui, Page}; +use glium::glutin; + +fn create_display(event_loop: &glutin::event_loop::EventLoop<()>) -> glium::Display { + let window_builder = glutin::window::WindowBuilder::new() + .with_resizable(true) + .with_inner_size(glutin::dpi::LogicalSize { + width: 800.0, + height: 600.0, + }) + .with_title("AMD GUI"); + + let context_builder = glutin::ContextBuilder::new() + .with_depth_buffer(0) + .with_srgb(true) + .with_stencil_buffer(0) + .with_vsync(true); + + glium::Display::new(window_builder, context_builder, event_loop).unwrap() +} + +pub fn run_app(amd_gui: Arc>) { + let event_loop = glutin::event_loop::EventLoop::with_user_event(); + let display = create_display(&event_loop); + + let mut egui = egui_glium::EguiGlium::new(&display); + + event_loop.run(move |event, _, control_flow| { + let mut redraw = || { + egui.begin_frame(&display); + + egui::containers::TopBottomPanel::new(TopBottomSide::Top, "menu").show( + egui.ctx(), + |ui| { + let mut child = + ui.child_ui(ui.available_rect_before_wrap(), Layout::left_to_right()); + if child + .add(egui::Button::new("Config").text_style(TextStyle::Heading)) + .clicked_by(PointerButton::Primary) + { + amd_gui.lock().page = Page::Config; + } + if child + .add(egui::Button::new("Monitoring").text_style(TextStyle::Heading)) + .clicked_by(PointerButton::Primary) + { + amd_gui.lock().page = Page::Monitoring; + } + if child + .add(egui::Button::new("Settings").text_style(TextStyle::Heading)) + .clicked_by(PointerButton::Primary) + { + amd_gui.lock().page = Page::Settings; + } + }, + ); + + egui::containers::CentralPanel::default().show(egui.ctx(), |ui| { + let mut gui = amd_gui.lock(); + let page = gui.page; + match page { + Page::Config => { + gui.ui(ui); + } + Page::Monitoring => { + gui.ui(ui); + } + Page::Settings => { + egui.ctx().settings_ui(ui); + } + } + }); + + let (needs_repaint, shapes) = egui.end_frame(&display); + + *control_flow = if needs_repaint { + display.gl_window().window().request_redraw(); + glutin::event_loop::ControlFlow::Poll + } else { + glutin::event_loop::ControlFlow::Wait + }; + + { + use glium::Surface as _; + let mut target = display.draw(); + + let color = egui::Rgba::from_rgb(0.1, 0.3, 0.2); + target.clear_color(color[0], color[1], color[2], color[3]); + egui.paint(&display, &mut target, shapes); + target.finish().unwrap(); + } + }; + + match event { + glutin::event::Event::RedrawRequested(_) => redraw(), + glutin::event::Event::WindowEvent { event, .. } => { + if egui.is_quit_event(&event) { + *control_flow = glium::glutin::event_loop::ControlFlow::Exit; + } + + egui.on_event(&event); + + display.gl_window().window().request_redraw(); + } + + _ => (), + } + }); +} diff --git a/amdguid/src/items.rs b/amdguid/src/items.rs new file mode 100644 index 0000000..8045f93 --- /dev/null +++ b/amdguid/src/items.rs @@ -0,0 +1,147 @@ +//! Contains items that can be added to a plot. + +use std::ops::RangeInclusive; + +use egui::Pos2; +use epaint::{Color32, Shape, Stroke}; + +pub use arrows::*; +pub use h_line::*; +pub use line::*; +pub use marker_shape::*; +pub use plot_image::*; +pub use plot_item::*; +pub use points::*; +pub use polygons::*; +pub use text::*; +pub use v_line::*; +pub use value::Value; +pub use values::Values; + +mod arrows; +mod h_line; +mod line; +mod marker_shape; +mod plot_image; +mod plot_item; +mod points; +mod polygons; +mod text; +mod v_line; +mod value; +mod values; + +const DEFAULT_FILL_ALPHA: f32 = 0.05; + +// ---------------------------------------------------------------------------- + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum LineStyle { + Solid, + Dotted { spacing: f32 }, + Dashed { length: f32 }, +} + +impl LineStyle { + pub fn dashed_loose() -> Self { + Self::Dashed { length: 10.0 } + } + + pub fn dashed_dense() -> Self { + Self::Dashed { length: 5.0 } + } + + pub fn dotted_loose() -> Self { + Self::Dotted { spacing: 10.0 } + } + + pub fn dotted_dense() -> Self { + Self::Dotted { spacing: 5.0 } + } + + fn style_line( + &self, + line: Vec, + mut stroke: Stroke, + highlight: bool, + shapes: &mut Vec, + ) { + match line.len() { + 0 => {} + 1 => { + let mut radius = stroke.width / 2.0; + if highlight { + radius *= 2f32.sqrt(); + } + shapes.push(Shape::circle_filled(line[0], radius, stroke.color)); + } + _ => { + match self { + LineStyle::Solid => { + if highlight { + stroke.width *= 2.0; + } + for point in line.iter() { + shapes.push(Shape::circle_filled( + *point, + stroke.width * 3.0, + Color32::DARK_BLUE, + )); + } + shapes.push(Shape::line(line, stroke)); + } + LineStyle::Dotted { spacing } => { + let mut radius = stroke.width; + if highlight { + radius *= 2f32.sqrt(); + } + shapes.extend(Shape::dotted_line(&line, stroke.color, *spacing, radius)); + } + LineStyle::Dashed { length } => { + if highlight { + stroke.width *= 2.0; + } + let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875 + shapes.extend(Shape::dashed_line( + &line, + stroke, + *length, + length * golden_ratio, + )); + } + } + } + } + } +} + +impl ToString for LineStyle { + fn to_string(&self) -> String { + match self { + LineStyle::Solid => "Solid".into(), + LineStyle::Dotted { spacing } => format!("Dotted{}Px", spacing), + LineStyle::Dashed { length } => format!("Dashed{}Px", length), + } + } +} + +// ---------------------------------------------------------------------------- + +// ---------------------------------------------------------------------------- + +/// Describes a function y = f(x) with an optional range for x and a number of points. +pub struct ExplicitGenerator { + function: Box f64>, + x_range: RangeInclusive, + points: usize, +} + +// ---------------------------------------------------------------------------- + +/// Returns the x-coordinate of a possible intersection between a line segment from `p1` to `p2` and +/// a horizontal line at the given y-coordinate. +#[inline(always)] +pub fn y_intersection(p1: &Pos2, p2: &Pos2, y: f32) -> Option { + ((p1.y > y && p2.y < y) || (p1.y < y && p2.y > y)) + .then(|| ((y * (p1.x - p2.x)) - (p1.x * p2.y - p1.y * p2.x)) / (p1.y - p2.y)) +} diff --git a/amdguid/src/items/arrows.rs b/amdguid/src/items/arrows.rs new file mode 100644 index 0000000..c6c4095 --- /dev/null +++ b/amdguid/src/items/arrows.rs @@ -0,0 +1,122 @@ +use crate::items::plot_item::PlotItem; +use crate::items::values::Values; +use crate::transform::{Bounds, ScreenTransform}; +use egui::Ui; +use epaint::{Color32, Shape}; +use std::ops::RangeInclusive; + +/// A set of arrows. +pub struct Arrows { + pub(crate) origins: Values, + pub(crate) tips: Values, + pub(crate) color: Color32, + pub(crate) name: String, + pub(crate) highlight: bool, +} + +impl Arrows { + pub fn new(origins: Values, tips: Values) -> Self { + Self { + origins, + tips, + color: Color32::TRANSPARENT, + name: Default::default(), + highlight: false, + } + } + + /// Highlight these arrows in the plot. + pub fn highlight(mut self) -> Self { + self.highlight = true; + self + } + + /// Set the arrows' color. + pub fn color(mut self, color: impl Into) -> Self { + self.color = color.into(); + self + } + + /// Name of this set of arrows. + /// + /// This name will show up in the plot legend, if legends are turned on. + /// + /// Multiple plot items may share the same name, in which case they will also share an entry in + /// the legend. + #[allow(clippy::needless_pass_by_value)] + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self + } +} + +impl PlotItem for Arrows { + fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { + use egui::emath::*; + use epaint::Stroke; + let Self { + origins, + tips, + color, + highlight, + .. + } = self; + let stroke = Stroke::new(if *highlight { 2.0 } else { 1.0 }, *color); + origins + .values + .iter() + .zip(tips.values.iter()) + .map(|(origin, tip)| { + ( + transform.position_from_value(origin), + transform.position_from_value(tip), + ) + }) + .for_each(|(origin, tip)| { + let vector = tip - origin; + let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0); + let tip_length = vector.length() / 4.0; + let tip = origin + vector; + let dir = vector.normalized(); + shapes.push(Shape::line_segment([origin, tip], stroke)); + shapes.push(Shape::line( + vec![ + tip - tip_length * (rot.inverse() * dir), + tip, + tip - tip_length * (rot * dir), + ], + stroke, + )); + }); + } + + fn initialize(&mut self, _x_range: RangeInclusive) { + self.origins + .generate_points(f64::NEG_INFINITY..=f64::INFINITY); + self.tips.generate_points(f64::NEG_INFINITY..=f64::INFINITY); + } + + fn name(&self) -> &str { + self.name.as_str() + } + + fn color(&self) -> Color32 { + self.color + } + + fn highlight(&mut self) { + self.highlight = true; + } + + fn highlighted(&self) -> bool { + self.highlight + } + + fn values(&self) -> Option<&Values> { + Some(&self.origins) + } + + fn get_bounds(&self) -> Bounds { + self.origins.get_bounds() + } +} diff --git a/amdguid/src/items/h_line.rs b/amdguid/src/items/h_line.rs new file mode 100644 index 0000000..91332f7 --- /dev/null +++ b/amdguid/src/items/h_line.rs @@ -0,0 +1,118 @@ +use crate::items::plot_item::PlotItem; +use crate::items::value::Value; +use crate::items::values::Values; +use crate::items::LineStyle; +use crate::transform::{Bounds, ScreenTransform}; +use egui::Ui; +use epaint::{Color32, Shape, Stroke}; +use std::ops::RangeInclusive; + +/// A horizontal line in a plot, filling the full width +#[derive(Clone, Debug, PartialEq)] +pub struct HLine { + pub(crate) y: f64, + pub(crate) stroke: Stroke, + pub(crate) name: String, + pub(crate) highlight: bool, + pub(crate) style: LineStyle, +} + +impl HLine { + pub fn new(y: impl Into) -> Self { + Self { + y: y.into(), + stroke: Stroke::new(1.0, Color32::TRANSPARENT), + name: String::default(), + highlight: false, + style: LineStyle::Solid, + } + } + + /// Highlight this line in the plot by scaling up the line. + pub fn highlight(mut self) -> Self { + self.highlight = true; + self + } + + /// Add a stroke. + pub fn stroke(mut self, stroke: impl Into) -> Self { + self.stroke = stroke.into(); + self + } + + /// Stroke width. A high value means the plot thickens. + pub fn width(mut self, width: impl Into) -> Self { + self.stroke.width = width.into(); + self + } + + /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. + pub fn color(mut self, color: impl Into) -> Self { + self.stroke.color = color.into(); + self + } + + /// Set the line's style. Default is `LineStyle::Solid`. + pub fn style(mut self, style: LineStyle) -> Self { + self.style = style; + self + } + + /// Name of this horizontal line. + /// + /// This name will show up in the plot legend, if legends are turned on. + /// + /// Multiple plot items may share the same name, in which case they will also share an entry in + /// the legend. + #[allow(clippy::needless_pass_by_value)] + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self + } +} + +impl PlotItem for HLine { + fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { + let HLine { + y, + stroke, + highlight, + style, + .. + } = self; + let points = vec![ + transform.position_from_value(&Value::new(transform.bounds().min[0], *y)), + transform.position_from_value(&Value::new(transform.bounds().max[0], *y)), + ]; + style.style_line(points, *stroke, *highlight, shapes); + } + + fn initialize(&mut self, _x_range: RangeInclusive) {} + + fn name(&self) -> &str { + &self.name + } + + fn color(&self) -> Color32 { + self.stroke.color + } + + fn highlight(&mut self) { + self.highlight = true; + } + + fn highlighted(&self) -> bool { + self.highlight + } + + fn values(&self) -> Option<&Values> { + None + } + + fn get_bounds(&self) -> Bounds { + let mut bounds = Bounds::NOTHING; + bounds.min[1] = self.y; + bounds.max[1] = self.y; + bounds + } +} diff --git a/amdguid/src/items/line.rs b/amdguid/src/items/line.rs new file mode 100644 index 0000000..4b0f249 --- /dev/null +++ b/amdguid/src/items/line.rs @@ -0,0 +1,170 @@ +use crate::items; +use crate::items::plot_item::PlotItem; +use crate::items::value::Value; +use crate::items::values::Values; +use crate::items::{LineStyle, DEFAULT_FILL_ALPHA}; +use crate::transform::{Bounds, ScreenTransform}; +use egui::{pos2, NumExt, Ui}; +use epaint::{Color32, Mesh, Rgba, Shape, Stroke}; +use std::ops::RangeInclusive; + +impl PlotItem for Line { + fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { + let Self { + series, + stroke, + highlight, + mut fill, + style, + .. + } = self; + + let values_tf: Vec<_> = series + .values + .iter() + .map(|v| transform.position_from_value(v)) + .collect(); + let n_values = values_tf.len(); + + // Fill the area between the line and a reference line, if required. + if n_values < 2 { + fill = None; + } + if let Some(y_reference) = fill { + let mut fill_alpha = DEFAULT_FILL_ALPHA; + if *highlight { + fill_alpha = (2.0 * fill_alpha).at_most(1.0); + } + let y = transform + .position_from_value(&Value::new(0.0, y_reference)) + .y; + let fill_color = Rgba::from(stroke.color) + .to_opaque() + .multiply(fill_alpha) + .into(); + let mut mesh = Mesh::default(); + let expected_intersections = 20; + mesh.reserve_triangles((n_values - 1) * 2); + mesh.reserve_vertices(n_values * 2 + expected_intersections); + values_tf[0..n_values - 1].windows(2).for_each(|w| { + let i = mesh.vertices.len() as u32; + mesh.colored_vertex(w[0], fill_color); + mesh.colored_vertex(pos2(w[0].x, y), fill_color); + if let Some(x) = items::y_intersection(&w[0], &w[1], y) { + let point = pos2(x, y); + mesh.colored_vertex(point, fill_color); + mesh.add_triangle(i, i + 1, i + 2); + mesh.add_triangle(i + 2, i + 3, i + 4); + } else { + mesh.add_triangle(i, i + 1, i + 2); + mesh.add_triangle(i + 1, i + 2, i + 3); + } + }); + let last = values_tf[n_values - 1]; + mesh.colored_vertex(last, fill_color); + mesh.colored_vertex(pos2(last.x, y), fill_color); + shapes.push(Shape::Mesh(mesh)); + } + + style.style_line(values_tf, *stroke, *highlight, shapes); + } + + fn initialize(&mut self, x_range: RangeInclusive) { + self.series.generate_points(x_range); + } + + fn name(&self) -> &str { + self.name.as_str() + } + + fn color(&self) -> Color32 { + self.stroke.color + } + + fn highlight(&mut self) { + self.highlight = true; + } + + fn highlighted(&self) -> bool { + self.highlight + } + + fn values(&self) -> Option<&Values> { + Some(&self.series) + } + + fn get_bounds(&self) -> Bounds { + self.series.get_bounds() + } +} + +/// A series of values forming a path. +pub struct Line { + pub series: Values, + pub stroke: Stroke, + pub name: String, + pub highlight: bool, + pub fill: Option, + pub style: LineStyle, +} + +impl Line { + pub fn new(series: Values) -> Self { + Self { + series, + stroke: Stroke::new(1.0, Color32::TRANSPARENT), + name: Default::default(), + highlight: false, + fill: None, + style: LineStyle::Solid, + } + } + + /// Highlight this line in the plot by scaling up the line. + pub fn highlight(mut self) -> Self { + self.highlight = true; + self + } + + /// Add a stroke. + pub fn stroke(mut self, stroke: impl Into) -> Self { + self.stroke = stroke.into(); + self + } + + /// Stroke width. A high value means the plot thickens. + pub fn width(mut self, width: impl Into) -> Self { + self.stroke.width = width.into(); + self + } + + /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. + pub fn color(mut self, color: impl Into) -> Self { + self.stroke.color = color.into(); + self + } + + /// Fill the area between this line and a given horizontal reference line. + pub fn fill(mut self, y_reference: impl Into) -> Self { + self.fill = Some(y_reference.into()); + self + } + + /// Set the line's style. Default is `LineStyle::Solid`. + pub fn style(mut self, style: LineStyle) -> Self { + self.style = style; + self + } + + /// Name of this line. + /// + /// This name will show up in the plot legend, if legends are turned on. + /// + /// Multiple plot items may share the same name, in which case they will also share an entry in + /// the legend. + #[allow(clippy::needless_pass_by_value)] + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self + } +} diff --git a/amdguid/src/items/marker_shape.rs b/amdguid/src/items/marker_shape.rs new file mode 100644 index 0000000..cd216b6 --- /dev/null +++ b/amdguid/src/items/marker_shape.rs @@ -0,0 +1,33 @@ +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum MarkerShape { + Circle, + Diamond, + Square, + Cross, + Plus, + Up, + Down, + Left, + Right, + Asterisk, +} + +impl MarkerShape { + /// Get a vector containing all marker shapes. + pub fn all() -> impl Iterator { + [ + Self::Circle, + Self::Diamond, + Self::Square, + Self::Cross, + Self::Plus, + Self::Up, + Self::Down, + Self::Left, + Self::Right, + Self::Asterisk, + ] + .iter() + .copied() + } +} diff --git a/amdguid/src/items/plot_image.rs b/amdguid/src/items/plot_image.rs new file mode 100644 index 0000000..b7b6d12 --- /dev/null +++ b/amdguid/src/items/plot_image.rs @@ -0,0 +1,148 @@ +use crate::items::plot_item::PlotItem; +use crate::items::value::Value; +use crate::items::values::Values; +use crate::transform::{Bounds, ScreenTransform}; +use egui::{pos2, Image, Rect, Ui, Vec2}; +use epaint::{Color32, Shape, Stroke, TextureId}; +use std::ops::RangeInclusive; + +/// An image in the plot. +pub struct PlotImage { + pub position: Value, + pub texture_id: TextureId, + pub uv: Rect, + pub size: Vec2, + pub bg_fill: Color32, + pub tint: Color32, + pub highlight: bool, + pub name: String, +} + +impl PlotImage { + /// Create a new image with position and size in plot coordinates. + pub fn new(texture_id: TextureId, position: Value, size: impl Into) -> Self { + Self { + position, + name: Default::default(), + highlight: false, + texture_id, + uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), + size: size.into(), + bg_fill: Default::default(), + tint: Color32::WHITE, + } + } + + /// Highlight this image in the plot. + pub fn highlight(mut self) -> Self { + self.highlight = true; + self + } + + /// Select UV range. Default is (0,0) in top-left, (1,1) bottom right. + pub fn uv(mut self, uv: impl Into) -> Self { + self.uv = uv.into(); + self + } + + /// A solid color to put behind the image. Useful for transparent images. + pub fn bg_fill(mut self, bg_fill: impl Into) -> Self { + self.bg_fill = bg_fill.into(); + self + } + + /// Multiply image color with this. Default is WHITE (no tint). + pub fn tint(mut self, tint: impl Into) -> Self { + self.tint = tint.into(); + self + } + + /// Name of this image. + /// + /// This name will show up in the plot legend, if legends are turned on. + /// + /// Multiple plot items may share the same name, in which case they will also share an entry in + /// the legend. + #[allow(clippy::needless_pass_by_value)] + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self + } +} + +impl PlotItem for PlotImage { + fn get_shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { + let Self { + position, + texture_id, + uv, + size, + bg_fill, + tint, + highlight, + .. + } = self; + let rect = { + let left_top = Value::new( + position.x as f32 - size.x / 2.0, + position.y as f32 - size.y / 2.0, + ); + let right_bottom = Value::new( + position.x as f32 + size.x / 2.0, + position.y as f32 + size.y / 2.0, + ); + let left_top_tf = transform.position_from_value(&left_top); + let right_bottom_tf = transform.position_from_value(&right_bottom); + Rect::from_two_pos(left_top_tf, right_bottom_tf) + }; + Image::new(*texture_id, *size) + .bg_fill(*bg_fill) + .tint(*tint) + .uv(*uv) + .paint_at(ui, rect); + if *highlight { + shapes.push(Shape::rect_stroke( + rect, + 0.0, + Stroke::new(1.0, ui.visuals().strong_text_color()), + )); + } + } + + fn initialize(&mut self, _x_range: RangeInclusive) {} + + fn name(&self) -> &str { + self.name.as_str() + } + + fn color(&self) -> Color32 { + Color32::TRANSPARENT + } + + fn highlight(&mut self) { + self.highlight = true; + } + + fn highlighted(&self) -> bool { + self.highlight + } + + fn values(&self) -> Option<&Values> { + None + } + + fn get_bounds(&self) -> Bounds { + let mut bounds = Bounds::NOTHING; + let left_top = Value::new( + self.position.x as f32 - self.size.x / 2.0, + self.position.y as f32 - self.size.y / 2.0, + ); + let right_bottom = Value::new( + self.position.x as f32 + self.size.x / 2.0, + self.position.y as f32 + self.size.y / 2.0, + ); + bounds.extend_with(&left_top); + bounds.extend_with(&right_bottom); + bounds + } +} diff --git a/amdguid/src/items/plot_item.rs b/amdguid/src/items/plot_item.rs new file mode 100644 index 0000000..38c6982 --- /dev/null +++ b/amdguid/src/items/plot_item.rs @@ -0,0 +1,17 @@ +use crate::items::Values; +use crate::transform::{Bounds, ScreenTransform}; +use egui::Ui; +use epaint::{Color32, Shape}; +use std::ops::RangeInclusive; + +/// Trait shared by things that can be drawn in the plot. +pub trait PlotItem { + fn get_shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec); + fn initialize(&mut self, x_range: RangeInclusive); + fn name(&self) -> &str; + fn color(&self) -> Color32; + fn highlight(&mut self); + fn highlighted(&self) -> bool; + fn values(&self) -> Option<&Values>; + fn get_bounds(&self) -> Bounds; +} diff --git a/amdguid/src/items/points.rs b/amdguid/src/items/points.rs new file mode 100644 index 0000000..599a9e2 --- /dev/null +++ b/amdguid/src/items/points.rs @@ -0,0 +1,239 @@ +use crate::items::marker_shape::MarkerShape; +use crate::items::plot_item::PlotItem; +use crate::items::value::Value; +use crate::items::values::Values; +use crate::transform::{Bounds, ScreenTransform}; +use egui::{pos2, vec2, Pos2, Ui}; +use epaint::{Color32, Shape, Stroke}; +use std::ops::RangeInclusive; + +/// A set of points. +pub struct Points { + pub(crate) series: Values, + pub(crate) shape: MarkerShape, + /// Color of the marker. `Color32::TRANSPARENT` means that it will be picked automatically. + pub(crate) color: Color32, + /// Whether to fill the marker. Does not apply to all types. + pub(crate) filled: bool, + /// The maximum extent of the marker from its center. + pub(crate) radius: f32, + pub(crate) name: String, + pub(crate) highlight: bool, + pub(crate) stems: Option, +} + +impl Points { + pub fn new(series: Values) -> Self { + Self { + series, + shape: MarkerShape::Circle, + color: Color32::TRANSPARENT, + filled: true, + radius: 1.0, + name: Default::default(), + highlight: false, + stems: None, + } + } + + /// Set the shape of the markers. + pub fn shape(mut self, shape: MarkerShape) -> Self { + self.shape = shape; + self + } + + /// Highlight these points in the plot by scaling up their markers. + pub fn highlight(mut self) -> Self { + self.highlight = true; + self + } + + /// Set the marker's color. + pub fn color(mut self, color: impl Into) -> Self { + self.color = color.into(); + self + } + + /// Whether to fill the marker. + pub fn filled(mut self, filled: bool) -> Self { + self.filled = filled; + self + } + + /// Whether to add stems between the markers and a horizontal reference line. + pub fn stems(mut self, y_reference: impl Into) -> Self { + self.stems = Some(y_reference.into()); + self + } + + /// Set the maximum extent of the marker around its position. + pub fn radius(mut self, radius: impl Into) -> Self { + self.radius = radius.into(); + self + } + + /// Name of this set of points. + /// + /// This name will show up in the plot legend, if legends are turned on. + /// + /// Multiple plot items may share the same name, in which case they will also share an entry in + /// the legend. + #[allow(clippy::needless_pass_by_value)] + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self + } +} + +impl PlotItem for Points { + fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { + let sqrt_3 = 3f32.sqrt(); + let frac_sqrt_3_2 = 3f32.sqrt() / 2.0; + let frac_1_sqrt_2 = 1.0 / 2f32.sqrt(); + + let Self { + series, + shape, + color, + filled, + mut radius, + highlight, + stems, + .. + } = self; + + let stroke_size = radius / 5.0; + + let default_stroke = Stroke::new(stroke_size, *color); + let mut stem_stroke = default_stroke; + let stroke = (!filled) + .then(|| default_stroke) + .unwrap_or_else(Stroke::none); + let fill = filled.then(|| *color).unwrap_or_default(); + + if *highlight { + radius *= 2f32.sqrt(); + stem_stroke.width *= 2.0; + } + + let y_reference = + stems.map(|y| transform.position_from_value(&Value::new(0.0, y)).y as f32); + + series + .values + .iter() + .map(|value| transform.position_from_value(value)) + .for_each(|center| { + let tf = |dx: f32, dy: f32| -> Pos2 { center + radius * vec2(dx, dy) }; + + if let Some(y) = y_reference { + let stem = Shape::line_segment([center, pos2(center.x, y)], stem_stroke); + shapes.push(stem); + } + + match shape { + MarkerShape::Circle => { + shapes.push(Shape::Circle(epaint::CircleShape { + center, + radius, + fill, + stroke, + })); + } + MarkerShape::Diamond => { + let points = vec![tf(1.0, 0.0), tf(0.0, -1.0), tf(-1.0, 0.0), tf(0.0, 1.0)]; + shapes.push(Shape::convex_polygon(points, fill, stroke)); + } + MarkerShape::Square => { + let points = vec![ + tf(frac_1_sqrt_2, frac_1_sqrt_2), + tf(frac_1_sqrt_2, -frac_1_sqrt_2), + tf(-frac_1_sqrt_2, -frac_1_sqrt_2), + tf(-frac_1_sqrt_2, frac_1_sqrt_2), + ]; + shapes.push(Shape::convex_polygon(points, fill, stroke)); + } + MarkerShape::Cross => { + let diagonal1 = [ + tf(-frac_1_sqrt_2, -frac_1_sqrt_2), + tf(frac_1_sqrt_2, frac_1_sqrt_2), + ]; + let diagonal2 = [ + tf(frac_1_sqrt_2, -frac_1_sqrt_2), + tf(-frac_1_sqrt_2, frac_1_sqrt_2), + ]; + shapes.push(Shape::line_segment(diagonal1, default_stroke)); + shapes.push(Shape::line_segment(diagonal2, default_stroke)); + } + MarkerShape::Plus => { + let horizontal = [tf(-1.0, 0.0), tf(1.0, 0.0)]; + let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)]; + shapes.push(Shape::line_segment(horizontal, default_stroke)); + shapes.push(Shape::line_segment(vertical, default_stroke)); + } + MarkerShape::Up => { + let points = + vec![tf(0.0, -1.0), tf(-0.5 * sqrt_3, 0.5), tf(0.5 * sqrt_3, 0.5)]; + shapes.push(Shape::convex_polygon(points, fill, stroke)); + } + MarkerShape::Down => { + let points = vec![ + tf(0.0, 1.0), + tf(-0.5 * sqrt_3, -0.5), + tf(0.5 * sqrt_3, -0.5), + ]; + shapes.push(Shape::convex_polygon(points, fill, stroke)); + } + MarkerShape::Left => { + let points = + vec![tf(-1.0, 0.0), tf(0.5, -0.5 * sqrt_3), tf(0.5, 0.5 * sqrt_3)]; + shapes.push(Shape::convex_polygon(points, fill, stroke)); + } + MarkerShape::Right => { + let points = vec![ + tf(1.0, 0.0), + tf(-0.5, -0.5 * sqrt_3), + tf(-0.5, 0.5 * sqrt_3), + ]; + shapes.push(Shape::convex_polygon(points, fill, stroke)); + } + MarkerShape::Asterisk => { + let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)]; + let diagonal1 = [tf(-frac_sqrt_3_2, 0.5), tf(frac_sqrt_3_2, -0.5)]; + let diagonal2 = [tf(-frac_sqrt_3_2, -0.5), tf(frac_sqrt_3_2, 0.5)]; + shapes.push(Shape::line_segment(vertical, default_stroke)); + shapes.push(Shape::line_segment(diagonal1, default_stroke)); + shapes.push(Shape::line_segment(diagonal2, default_stroke)); + } + } + }); + } + + fn initialize(&mut self, x_range: RangeInclusive) { + self.series.generate_points(x_range); + } + + fn name(&self) -> &str { + self.name.as_str() + } + + fn color(&self) -> Color32 { + self.color + } + + fn highlight(&mut self) { + self.highlight = true; + } + + fn highlighted(&self) -> bool { + self.highlight + } + + fn values(&self) -> Option<&Values> { + Some(&self.series) + } + + fn get_bounds(&self) -> Bounds { + self.series.get_bounds() + } +} diff --git a/amdguid/src/items/polygons.rs b/amdguid/src/items/polygons.rs new file mode 100644 index 0000000..4358937 --- /dev/null +++ b/amdguid/src/items/polygons.rs @@ -0,0 +1,137 @@ +use crate::items::plot_item::PlotItem; +use crate::items::values::Values; +use crate::items::{LineStyle, DEFAULT_FILL_ALPHA}; +use crate::transform::{Bounds, ScreenTransform}; +use egui::{NumExt, Ui}; +use epaint::{Color32, Rgba, Shape, Stroke}; +use std::ops::RangeInclusive; + +/// A convex polygon. +pub struct Polygon { + pub series: Values, + pub stroke: Stroke, + pub name: String, + pub highlight: bool, + pub fill_alpha: f32, + pub style: LineStyle, +} + +impl Polygon { + pub fn new(series: Values) -> Self { + Self { + series, + stroke: Stroke::new(1.0, Color32::TRANSPARENT), + name: Default::default(), + highlight: false, + fill_alpha: DEFAULT_FILL_ALPHA, + style: LineStyle::Solid, + } + } + + /// Highlight this polygon in the plot by scaling up the stroke and reducing the fill + /// transparency. + pub fn highlight(mut self) -> Self { + self.highlight = true; + self + } + + /// Add a custom stroke. + pub fn stroke(mut self, stroke: impl Into) -> Self { + self.stroke = stroke.into(); + self + } + + /// Set the stroke width. + pub fn width(mut self, width: impl Into) -> Self { + self.stroke.width = width.into(); + self + } + + /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. + pub fn color(mut self, color: impl Into) -> Self { + self.stroke.color = color.into(); + self + } + + /// Alpha of the filled area. + pub fn fill_alpha(mut self, alpha: impl Into) -> Self { + self.fill_alpha = alpha.into(); + self + } + + /// Set the outline's style. Default is `LineStyle::Solid`. + pub fn style(mut self, style: LineStyle) -> Self { + self.style = style; + self + } + + /// Name of this polygon. + /// + /// This name will show up in the plot legend, if legends are turned on. + /// + /// Multiple plot items may share the same name, in which case they will also share an entry in + /// the legend. + #[allow(clippy::needless_pass_by_value)] + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self + } +} + +impl PlotItem for Polygon { + fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { + let Self { + series, + stroke, + highlight, + mut fill_alpha, + style, + .. + } = self; + + if *highlight { + fill_alpha = (2.0 * fill_alpha).at_most(1.0); + } + + let mut values_tf: Vec<_> = series + .values + .iter() + .map(|v| transform.position_from_value(v)) + .collect(); + + let fill = Rgba::from(stroke.color).to_opaque().multiply(fill_alpha); + + let shape = Shape::convex_polygon(values_tf.clone(), fill, Stroke::none()); + shapes.push(shape); + values_tf.push(*values_tf.first().unwrap()); + style.style_line(values_tf, *stroke, *highlight, shapes); + } + + fn initialize(&mut self, x_range: RangeInclusive) { + self.series.generate_points(x_range); + } + + fn name(&self) -> &str { + self.name.as_str() + } + + fn color(&self) -> Color32 { + self.stroke.color + } + + fn highlight(&mut self) { + self.highlight = true; + } + + fn highlighted(&self) -> bool { + self.highlight + } + + fn values(&self) -> Option<&Values> { + Some(&self.series) + } + + fn get_bounds(&self) -> Bounds { + self.series.get_bounds() + } +} diff --git a/amdguid/src/items/text.rs b/amdguid/src/items/text.rs new file mode 100644 index 0000000..32a1cf9 --- /dev/null +++ b/amdguid/src/items/text.rs @@ -0,0 +1,122 @@ +use crate::items::plot_item::PlotItem; +use crate::items::value::Value; +use crate::items::values::Values; +use crate::transform::{Bounds, ScreenTransform}; +use egui::{Align2, Rect, Ui}; +use epaint::{Color32, Shape, Stroke, TextStyle}; +use std::ops::RangeInclusive; + +/// Text inside the plot. +pub struct Text { + pub(crate) text: String, + pub(crate) style: TextStyle, + pub(crate) position: Value, + pub(crate) name: String, + pub(crate) highlight: bool, + pub(crate) color: Color32, + pub(crate) anchor: Align2, +} + +impl Text { + #[allow(clippy::needless_pass_by_value)] + pub fn new(position: Value, text: impl ToString) -> Self { + Self { + text: text.to_string(), + style: TextStyle::Small, + position, + name: Default::default(), + highlight: false, + color: Color32::TRANSPARENT, + anchor: Align2::CENTER_CENTER, + } + } + + /// Highlight this text in the plot by drawing a rectangle around it. + pub fn highlight(mut self) -> Self { + self.highlight = true; + self + } + + /// Text style. Default is `TextStyle::Small`. + pub fn style(mut self, style: TextStyle) -> Self { + self.style = style; + self + } + + /// Text color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. + pub fn color(mut self, color: impl Into) -> Self { + self.color = color.into(); + self + } + + /// Anchor position of the text. Default is `Align2::CENTER_CENTER`. + pub fn anchor(mut self, anchor: Align2) -> Self { + self.anchor = anchor; + self + } + + /// Name of this text. + /// + /// This name will show up in the plot legend, if legends are turned on. + /// + /// Multiple plot items may share the same name, in which case they will also share an entry in + /// the legend. + #[allow(clippy::needless_pass_by_value)] + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self + } +} + +impl PlotItem for Text { + fn get_shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { + let color = if self.color == Color32::TRANSPARENT { + ui.style().visuals.text_color() + } else { + self.color + }; + let pos = transform.position_from_value(&self.position); + let galley = ui + .fonts() + .layout_no_wrap(self.text.clone(), self.style, color); + let rect = self + .anchor + .anchor_rect(Rect::from_min_size(pos, galley.size())); + shapes.push(Shape::galley(rect.min, galley)); + if self.highlight { + shapes.push(Shape::rect_stroke( + rect.expand(2.0), + 1.0, + Stroke::new(0.5, color), + )); + } + } + + fn initialize(&mut self, _x_range: RangeInclusive) {} + + fn name(&self) -> &str { + self.name.as_str() + } + + fn color(&self) -> Color32 { + self.color + } + + fn highlight(&mut self) { + self.highlight = true; + } + + fn highlighted(&self) -> bool { + self.highlight + } + + fn values(&self) -> Option<&Values> { + None + } + + fn get_bounds(&self) -> Bounds { + let mut bounds = Bounds::NOTHING; + bounds.extend_with(&self.position); + bounds + } +} diff --git a/amdguid/src/items/v_line.rs b/amdguid/src/items/v_line.rs new file mode 100644 index 0000000..7b35f6e --- /dev/null +++ b/amdguid/src/items/v_line.rs @@ -0,0 +1,118 @@ +use crate::items::plot_item::PlotItem; +use crate::items::value::Value; +use crate::items::values::Values; +use crate::items::LineStyle; +use crate::transform::{Bounds, ScreenTransform}; +use egui::Ui; +use epaint::{Color32, Shape, Stroke}; +use std::ops::RangeInclusive; + +impl PlotItem for VLine { + fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { + let VLine { + x, + stroke, + highlight, + style, + .. + } = self; + let points = vec![ + transform.position_from_value(&Value::new(*x, transform.bounds().min[1])), + transform.position_from_value(&Value::new(*x, transform.bounds().max[1])), + ]; + style.style_line(points, *stroke, *highlight, shapes); + } + + fn initialize(&mut self, _x_range: RangeInclusive) {} + + fn name(&self) -> &str { + &self.name + } + + fn color(&self) -> Color32 { + self.stroke.color + } + + fn highlight(&mut self) { + self.highlight = true; + } + + fn highlighted(&self) -> bool { + self.highlight + } + + fn values(&self) -> Option<&Values> { + None + } + + fn get_bounds(&self) -> Bounds { + let mut bounds = Bounds::NOTHING; + bounds.min[0] = self.x; + bounds.max[0] = self.x; + bounds + } +} + +/// A vertical line in a plot, filling the full width +#[derive(Clone, Debug, PartialEq)] +pub struct VLine { + pub(crate) x: f64, + pub(crate) stroke: Stroke, + pub(crate) name: String, + pub(crate) highlight: bool, + pub(crate) style: LineStyle, +} + +impl VLine { + pub fn new(x: impl Into) -> Self { + Self { + x: x.into(), + stroke: Stroke::new(1.0, Color32::TRANSPARENT), + name: String::default(), + highlight: false, + style: LineStyle::Solid, + } + } + + /// Highlight this line in the plot by scaling up the line. + pub fn highlight(mut self) -> Self { + self.highlight = true; + self + } + + /// Add a stroke. + pub fn stroke(mut self, stroke: impl Into) -> Self { + self.stroke = stroke.into(); + self + } + + /// Stroke width. A high value means the plot thickens. + pub fn width(mut self, width: impl Into) -> Self { + self.stroke.width = width.into(); + self + } + + /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. + pub fn color(mut self, color: impl Into) -> Self { + self.stroke.color = color.into(); + self + } + + /// Set the line's style. Default is `LineStyle::Solid`. + pub fn style(mut self, style: LineStyle) -> Self { + self.style = style; + self + } + + /// Name of this vertical line. + /// + /// This name will show up in the plot legend, if legends are turned on. + /// + /// Multiple plot items may share the same name, in which case they will also share an entry in + /// the legend. + #[allow(clippy::needless_pass_by_value)] + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self + } +} diff --git a/amdguid/src/items/value.rs b/amdguid/src/items/value.rs new file mode 100644 index 0000000..807670f --- /dev/null +++ b/amdguid/src/items/value.rs @@ -0,0 +1,22 @@ +/// A value in the value-space of the plot. +/// +/// Uses f64 for improved accuracy to enable plotting +/// large values (e.g. unix time on x axis). +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Value { + /// This is often something monotonically increasing, such as time, but doesn't have to be. + /// Goes from left to right. + pub x: f64, + /// Goes from bottom to top (inverse of everything else in egui!). + pub y: f64, +} + +impl Value { + #[inline(always)] + pub fn new(x: impl Into, y: impl Into) -> Self { + Self { + x: x.into(), + y: y.into(), + } + } +} diff --git a/amdguid/src/items/values.rs b/amdguid/src/items/values.rs new file mode 100644 index 0000000..6b0efd0 --- /dev/null +++ b/amdguid/src/items/values.rs @@ -0,0 +1,135 @@ +use crate::items::{ExplicitGenerator, Value}; +use crate::transform::Bounds; +use std::collections::Bound; +use std::ops::{RangeBounds, RangeInclusive}; + +pub struct Values { + pub values: Vec, + generator: Option, +} + +impl Values { + pub fn from_values(values: Vec) -> Self { + Self { + values, + generator: None, + } + } + + pub fn from_values_iter(iter: impl Iterator) -> Self { + Self::from_values(iter.collect()) + } + + /// Draw a line based on a function `y=f(x)`, a range (which can be infinite) for x and the number of points. + pub fn from_explicit_callback( + function: impl Fn(f64) -> f64 + 'static, + x_range: impl RangeBounds, + points: usize, + ) -> Self { + let start = match x_range.start_bound() { + Bound::Included(x) | Bound::Excluded(x) => *x, + Bound::Unbounded => f64::NEG_INFINITY, + }; + let end = match x_range.end_bound() { + Bound::Included(x) | Bound::Excluded(x) => *x, + Bound::Unbounded => f64::INFINITY, + }; + let x_range = start..=end; + + let generator = ExplicitGenerator { + function: Box::new(function), + x_range, + points, + }; + + Self { + values: Vec::new(), + generator: Some(generator), + } + } + + /// Draw a line based on a function `(x,y)=f(t)`, a range for t and the number of points. + /// The range may be specified as start..end or as start..=end. + pub fn from_parametric_callback( + function: impl Fn(f64) -> (f64, f64), + t_range: impl RangeBounds, + points: usize, + ) -> Self { + let start = match t_range.start_bound() { + Bound::Included(x) => x, + Bound::Excluded(_) => unreachable!(), + Bound::Unbounded => panic!("The range for parametric functions must be bounded!"), + }; + let end = match t_range.end_bound() { + Bound::Included(x) | Bound::Excluded(x) => x, + Bound::Unbounded => panic!("The range for parametric functions must be bounded!"), + }; + let last_point_included = matches!(t_range.end_bound(), Bound::Included(_)); + let increment = if last_point_included { + (end - start) / (points - 1) as f64 + } else { + (end - start) / points as f64 + }; + let values = (0..points).map(|i| { + let t = start + i as f64 * increment; + let (x, y) = function(t); + Value { x, y } + }); + Self::from_values_iter(values) + } + + /// From a series of y-values. + /// The x-values will be the indices of these values + pub fn from_ys_f32(ys: &[f32]) -> Self { + let values: Vec = ys + .iter() + .enumerate() + .map(|(i, &y)| Value { + x: i as f64, + y: y as f64, + }) + .collect(); + Self::from_values(values) + } + + /// Returns true if there are no data points available and there is no function to generate any. + pub fn is_empty(&self) -> bool { + self.generator.is_none() && self.values.is_empty() + } + + /// If initialized with a generator function, this will generate `n` evenly spaced points in the + /// given range. + pub fn generate_points(&mut self, x_range: RangeInclusive) { + if let Some(generator) = self.generator.take() { + if let Some(intersection) = Self::range_intersection(&x_range, &generator.x_range) { + let increment = + (intersection.end() - intersection.start()) / (generator.points - 1) as f64; + self.values = (0..generator.points) + .map(|i| { + let x = intersection.start() + i as f64 * increment; + let y = (generator.function)(x); + Value { x, y } + }) + .collect(); + } + } + } + + /// Returns the intersection of two ranges if they intersect. + fn range_intersection( + range1: &RangeInclusive, + range2: &RangeInclusive, + ) -> Option> { + let start = range1.start().max(*range2.start()); + let end = range1.end().min(*range2.end()); + (start < end).then(|| start..=end) + } + + pub(crate) fn get_bounds(&self) -> Bounds { + let mut bounds = Bounds::NOTHING; + self.values + .iter() + .for_each(|value| bounds.extend_with(value)); + bounds + } +} diff --git a/amdguid/src/main.rs b/amdguid/src/main.rs new file mode 100644 index 0000000..533331d --- /dev/null +++ b/amdguid/src/main.rs @@ -0,0 +1,37 @@ +use app::AmdGui; + +mod app; +mod backend; +pub mod items; +pub mod transform; +pub mod widgets; + +#[tokio::main] +async fn main() { + use std::sync::Arc; + + use parking_lot::Mutex; + + if std::env::var("RUST_LOG").is_err() { + std::env::set_var("RUST_LOG", "DEBUG"); + } + pretty_env_logger::init(); + let config = Arc::new(Mutex::new( + amdgpu_config::fan::load_config(amdgpu_config::fan::DEFAULT_FAN_CONFIG_PATH) + .expect("No FAN config"), + )); + let amd_gui = Arc::new(Mutex::new(AmdGui::new_with_config(config))); + + schedule_tick(amd_gui.clone()); + + backend::run_app(amd_gui); +} + +fn schedule_tick(amd_gui: std::sync::Arc>) { + tokio::spawn(async move { + loop { + amd_gui.lock().tick(); + tokio::time::sleep(tokio::time::Duration::from_millis(166)).await; + } + }); +} diff --git a/amdguid/src/transform.rs b/amdguid/src/transform.rs new file mode 100644 index 0000000..c22a919 --- /dev/null +++ b/amdguid/src/transform.rs @@ -0,0 +1,243 @@ +use std::ops::RangeInclusive; + +use egui::{pos2, remap, Pos2, Rect, Vec2}; + +use crate::items::Value; + +/// 2D bounding box of f64 precision. +/// The range of data values we show. +#[derive(Clone, Copy, PartialEq, Debug, serde::Serialize, serde::Deserialize)] +pub struct Bounds { + pub min: [f64; 2], + pub max: [f64; 2], +} + +impl Bounds { + pub const NOTHING: Self = Self { + min: [f64::INFINITY; 2], + max: [-f64::INFINITY; 2], + }; + + pub fn new_symmetrical(half_extent: f64) -> Self { + Self { + min: [-half_extent; 2], + max: [half_extent; 2], + } + } + + pub fn is_finite(&self) -> bool { + self.min[0].is_finite() + && self.min[1].is_finite() + && self.max[0].is_finite() + && self.max[1].is_finite() + } + + pub fn is_valid(&self) -> bool { + self.is_finite() && self.width() > 0.0 && self.height() > 0.0 + } + + pub fn width(&self) -> f64 { + self.max[0] - self.min[0] + } + + pub fn height(&self) -> f64 { + self.max[1] - self.min[1] + } + + pub fn extend_with(&mut self, value: &Value) { + self.extend_with_x(value.x); + self.extend_with_y(value.y); + } + + /// Expand to include the given x coordinate + pub fn extend_with_x(&mut self, x: f64) { + self.min[0] = self.min[0].min(x); + self.max[0] = self.max[0].max(x); + } + + /// Expand to include the given y coordinate + pub fn extend_with_y(&mut self, y: f64) { + self.min[1] = self.min[1].min(y); + self.max[1] = self.max[1].max(y); + } + + pub fn expand_x(&mut self, pad: f64) { + self.min[0] -= pad; + self.max[0] += pad; + } + + pub fn expand_y(&mut self, pad: f64) { + self.min[1] -= pad; + self.max[1] += pad; + } + + pub fn merge(&mut self, other: &Bounds) { + self.min[0] = self.min[0].min(other.min[0]); + self.min[1] = self.min[1].min(other.min[1]); + self.max[0] = self.max[0].max(other.max[0]); + self.max[1] = self.max[1].max(other.max[1]); + } + + pub fn translate_x(&mut self, delta: f64) { + self.min[0] += delta; + self.max[0] += delta; + } + + pub fn translate_y(&mut self, delta: f64) { + self.min[1] += delta; + self.max[1] += delta; + } + + pub fn translate(&mut self, delta: Vec2) { + self.translate_x(delta.x as f64); + self.translate_y(delta.y as f64); + } + + pub fn add_relative_margin(&mut self, margin_fraction: Vec2) { + let width = self.width().max(0.0); + let height = self.height().max(0.0); + self.expand_x(margin_fraction.x as f64 * width); + self.expand_y(margin_fraction.y as f64 * height); + } + + pub fn range_x(&self) -> RangeInclusive { + self.min[0]..=self.max[0] + } + + pub fn make_x_symmetrical(&mut self) { + let x_abs = self.min[0].abs().max(self.max[0].abs()); + self.min[0] = -x_abs; + self.max[0] = x_abs; + } + + pub fn make_y_symmetrical(&mut self) { + let y_abs = self.min[1].abs().max(self.max[1].abs()); + self.min[1] = -y_abs; + self.max[1] = y_abs; + } +} + +/// Contains the screen rectangle and the plot bounds and provides methods to transform them. +#[derive(Clone)] +pub struct ScreenTransform { + /// The screen rectangle. + frame: Rect, + /// The plot bounds. + bounds: Bounds, + /// Whether to always center the x-range of the bounds. + x_centered: bool, + /// Whether to always center the y-range of the bounds. + y_centered: bool, +} + +impl ScreenTransform { + pub fn new(frame: Rect, bounds: Bounds, x_centered: bool, y_centered: bool) -> Self { + Self { + frame, + bounds, + x_centered, + y_centered, + } + } + + pub fn frame(&self) -> &Rect { + &self.frame + } + + pub fn bounds(&self) -> &Bounds { + &self.bounds + } + + pub fn translate_bounds(&mut self, mut delta_pos: Vec2) { + if self.x_centered { + delta_pos.x = 0.; + } + if self.y_centered { + delta_pos.y = 0.; + } + delta_pos.x *= self.dvalue_dpos()[0] as f32; + delta_pos.y *= self.dvalue_dpos()[1] as f32; + self.bounds.translate(delta_pos); + } + + /// Zoom by a relative factor with the given screen position as center. + pub fn zoom(&mut self, zoom_factor: Vec2, center: Pos2) { + let center = self.value_from_position(center); + + let mut new_bounds = self.bounds; + new_bounds.min[0] = center.x + (new_bounds.min[0] - center.x) / (zoom_factor.x as f64); + new_bounds.max[0] = center.x + (new_bounds.max[0] - center.x) / (zoom_factor.x as f64); + new_bounds.min[1] = center.y + (new_bounds.min[1] - center.y) / (zoom_factor.y as f64); + new_bounds.max[1] = center.y + (new_bounds.max[1] - center.y) / (zoom_factor.y as f64); + + if new_bounds.is_valid() { + self.bounds = new_bounds; + } + } + + pub fn position_from_value(&self, value: &Value) -> Pos2 { + let x = remap( + value.x, + self.bounds.min[0]..=self.bounds.max[0], + (self.frame.left() as f64)..=(self.frame.right() as f64), + ); + let y = remap( + value.y, + self.bounds.min[1]..=self.bounds.max[1], + (self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis! + ); + pos2(x as f32, y as f32) + } + + pub fn value_from_position(&self, pos: Pos2) -> Value { + let x = remap( + pos.x as f64, + (self.frame.left() as f64)..=(self.frame.right() as f64), + self.bounds.min[0]..=self.bounds.max[0], + ); + let y = remap( + pos.y as f64, + (self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis! + self.bounds.min[1]..=self.bounds.max[1], + ); + Value::new(x, y) + } + + /// delta position / delta value + pub fn dpos_dvalue_x(&self) -> f64 { + self.frame.width() as f64 / self.bounds.width() + } + + /// delta position / delta value + pub fn dpos_dvalue_y(&self) -> f64 { + -self.frame.height() as f64 / self.bounds.height() // negated y axis! + } + + /// delta position / delta value + pub fn dpos_dvalue(&self) -> [f64; 2] { + [self.dpos_dvalue_x(), self.dpos_dvalue_y()] + } + + /// delta value / delta position + pub fn dvalue_dpos(&self) -> [f64; 2] { + [1.0 / self.dpos_dvalue_x(), 1.0 / self.dpos_dvalue_y()] + } + + pub fn get_aspect(&self) -> f64 { + let rw = self.frame.width() as f64; + let rh = self.frame.height() as f64; + (self.bounds.width() / rw) / (self.bounds.height() / rh) + } + + pub fn set_aspect(&mut self, aspect: f64) { + let epsilon = 1e-5; + let current_aspect = self.get_aspect(); + if current_aspect < aspect - epsilon { + self.bounds + .expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5); + } else if current_aspect > aspect + epsilon { + self.bounds + .expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5); + } + } +} diff --git a/amdguid/src/widgets/change_fan_settings.rs b/amdguid/src/widgets/change_fan_settings.rs new file mode 100644 index 0000000..1db44dd --- /dev/null +++ b/amdguid/src/widgets/change_fan_settings.rs @@ -0,0 +1,165 @@ +use amdgpu::helper_cmd::Command; +use amdgpu_config::fan::MatrixPoint; +use egui::{emath, pos2, Layout, PointerButton, Ui}; +use epaint::Color32; + +use crate::app::{ChangeState, FanConfig, FanServices, StatefulConfig}; +use crate::widgets::drag_plot::PlotMsg; +use crate::widgets::reload_section::ReloadSection; +use crate::{widgets, widgets::ConfigFile}; + +pub struct ChangeFanSettings { + config: FanConfig, + selected: Option, +} + +impl ChangeFanSettings { + pub fn new(config: FanConfig) -> Self { + Self { + config, + selected: None, + } + } + + pub fn select(&mut self, idx: usize) { + self.selected = Some(idx); + } + + pub fn deselect(&mut self) { + self.selected = None; + } + + pub fn draw(&mut self, ui: &mut Ui, pid_files: &mut FanServices, state: &mut StatefulConfig) { + let available = ui.available_rect_before_wrap(); + ui.horizontal_top(|ui| { + ui.child_ui( + emath::Rect { + min: available.min, + max: pos2(available.width() / 2.0, available.height()), + }, + Layout::left_to_right(), + ) + .vertical(|ui| { + egui::ScrollArea::vertical() + .enable_scrolling(true) + .id_source("plot-and-reload") + .show(ui, |ui| { + ui.add({ + let curve = { + let config = self.config.lock(); + let iter = config + .speed_matrix() + .iter() + .map(|v| crate::items::Value::new(v.speed, v.temp)); + crate::items::Line::new(crate::items::Values::from_values_iter( + iter, + )) + .color(Color32::BLUE) + }; + widgets::drag_plot::DragPlot::new("change fan settings") + .height(600.0) + .width(available.width() / 2.0) + .selected(self.selected) + .allow_drag(true) + .allow_zoom(false) + .line(curve) + .y_axis_name(String::from("Temperature")) + .x_axis_name(String::from("Speed")) + .hline(crate::items::HLine::new(100.0).color(Color32::TRANSPARENT)) + .vline(crate::items::VLine::new(100.0).color(Color32::TRANSPARENT)) + .on_event(|msg| match msg { + PlotMsg::Clicked(idx) => { + self.selected = Some(idx); + } + PlotMsg::Drag(delta) => { + if let Some(idx) = self.selected { + let mut config = self.config.lock(); + let min = idx + .checked_sub(1) + .and_then(|i| config.speed_matrix().get(i).copied()) + .unwrap_or(MatrixPoint::MIN); + let max = idx + .checked_add(1) + .and_then(|i| config.speed_matrix().get(i).copied()) + .unwrap_or(MatrixPoint::MAX); + let current = config.speed_matrix_mut().get_mut(idx); + + if let Some(point) = current { + point.speed = (point.speed + delta.x as f64) + .max(min.speed) + .min(max.speed); + point.temp = (point.temp + delta.y as f64) + .max(min.temp) + .min(max.temp); + } + } + } + }) + .legend(crate::widgets::legend::Legend::default()) + }); + ui.separator(); + Self::save_button(self.config.clone(), state, ui); + ui.add(ReloadSection::new(pid_files)); + }); + }); + + ui.child_ui( + emath::Rect { + min: pos2(available.width() / 2.0 + 20.0, available.min.y), + max: available.max, + }, + Layout::left_to_right(), + ) + .vertical(|ui| { + ui.add(ConfigFile::new(self.config.clone())); + }); + }); + } + + fn save_button(config: FanConfig, state: &mut StatefulConfig, ui: &mut Ui) { + ui.horizontal(|ui| { + if ui.button("Save").clicked_by(PointerButton::Primary) { + Self::save_config(config, state); + } + match &state.state { + ChangeState::New => {} + ChangeState::Reloading => { + ui.label("Saving..."); + } + ChangeState::Success => { + ui.add(egui::Label::new("Saved").text_color(Color32::GREEN)); + } + ChangeState::Failure(msg) => { + ui.add(egui::Label::new(format!("Failure. {}", msg)).text_color(Color32::RED)); + } + } + }); + } + + fn save_config(config: FanConfig, state: &mut StatefulConfig) { + state.state = ChangeState::Reloading; + + let config = config.lock(); + let c: &amdgpu_config::fan::Config = &*config; + let content = match toml::to_string(c) { + Err(e) => { + log::error!("Config file serialization failed. {:?}", e); + return; + } + Ok(content) => content, + }; + let command = Command::SaveFanConfig { + path: String::from(config.path()), + content, + }; + match amdgpu::helper_cmd::send_command(command) { + Ok(amdgpu::helper_cmd::Response::ConfigFileSaveFailed(msg)) => { + state.state = ChangeState::Failure(msg); + } + Ok(amdgpu::helper_cmd::Response::ConfigFileSaved) => { + state.state = ChangeState::Success; + } + _ => {} + } + } +} diff --git a/amdguid/src/widgets/config_file.rs b/amdguid/src/widgets/config_file.rs new file mode 100644 index 0000000..dcdc79d --- /dev/null +++ b/amdguid/src/widgets/config_file.rs @@ -0,0 +1,112 @@ +use egui::{PointerButton, Response, Sense, Ui, Widget}; + +use amdgpu_config::fan::MatrixPoint; + +use crate::app::FanConfig; + +pub struct ConfigFile { + config: FanConfig, +} + +impl ConfigFile { + pub fn new(config: FanConfig) -> Self { + Self { config } + } +} + +impl Widget for ConfigFile { + fn ui(self, ui: &mut Ui) -> Response { + ui.vertical(|ui| { + let mut matrix = self + .config + .lock() + .speed_matrix() + .to_vec() + .into_iter() + .enumerate() + .peekable(); + + let mut prev = None; + + while let Some((idx, mut current)) = matrix.next() { + let min = if current == MatrixPoint::MIN { + MatrixPoint::MIN + } else if let Some(prev) = prev { + prev + } else { + MatrixPoint::MIN + }; + let next = matrix.peek(); + let max = if current == MatrixPoint::MAX { + MatrixPoint::MAX + } else if let Some(next) = next.map(|(_, n)| n) { + *next + } else { + MatrixPoint::MAX + }; + + { + ui.label("Temperature"); + if ui + .add(egui::Slider::new(&mut current.temp, min.temp..=max.temp)) + .changed() + { + if let Some(entry) = self.config.lock().speed_matrix_mut().get_mut(idx) { + entry.temp = current.temp; + } + } + } + { + ui.label("Speed"); + if ui + .add(egui::Slider::new(&mut current.speed, min.speed..=max.speed)) + .changed() + { + if let Some(entry) = self.config.lock().speed_matrix_mut().get_mut(idx) { + entry.speed = current.speed; + } + } + } + + ui.horizontal(|ui| { + if next.is_some() { + if ui + .add(egui::Button::new("Add in middle")) + .clicked_by(PointerButton::Primary) + { + self.config.lock().speed_matrix_vec_mut().insert( + idx + 1, + MatrixPoint::new( + min.temp + ((max.temp - min.temp) / 2.0), + min.speed + ((max.speed - min.speed) / 2.0), + ), + ) + } + } else if next.is_none() + && current != MatrixPoint::MAX + && ui + .add(egui::Button::new("Add")) + .clicked_by(PointerButton::Primary) + { + self.config + .lock() + .speed_matrix_vec_mut() + .push(MatrixPoint::new(100.0, 100.0)) + } + if ui + .add(egui::Button::new("Remove")) + .clicked_by(PointerButton::Primary) + { + self.config.lock().speed_matrix_vec_mut().remove(idx); + } + }); + + ui.separator(); + prev = Some(current); + } + + ui.allocate_response(ui.available_size(), Sense::click()) + }) + .inner + } +} diff --git a/amdguid/src/widgets/cooling_performance.rs b/amdguid/src/widgets/cooling_performance.rs new file mode 100644 index 0000000..b70f84c --- /dev/null +++ b/amdguid/src/widgets/cooling_performance.rs @@ -0,0 +1,90 @@ +use crate::app::{FanConfig, FanServices}; +use amdgpu::Card; +use amdmond_lib::AmdMon; +use core::option::Option; +use core::option::Option::Some; +use egui::Ui; +use std::collections::vec_deque::VecDeque; + +pub struct CoolingPerformance { + capacity: usize, + data: VecDeque, + amd_mon: Option, +} + +impl CoolingPerformance { + pub fn new(capacity: usize, fan_config: FanConfig) -> Self { + let amd_mon = amdgpu::hw_mon::open_hw_mon(Card(0)) + .map(|hw| amdmond_lib::AmdMon::wrap(hw, &*fan_config.lock())) + .ok(); + + Self { + capacity, + data: VecDeque::with_capacity(capacity), + amd_mon, + } + } + + pub fn tick(&mut self) { + if let Some(temp) = self + .amd_mon + .as_ref() + .and_then(|mon| mon.gpu_temp_of(0)) + .and_then(|(_, value)| value.ok()) + { + self.push(temp as f64); + } + } + + pub fn draw(&self, ui: &mut Ui, pid_files: &FanServices) { + use egui::widgets::plot::*; + use epaint::color::Color32; + + let current = self.data.iter().last().copied().unwrap_or_default(); + + let iter = self + .data + .iter() + .enumerate() + .map(|(i, v)| Value::new(i as f64, *v)); + + let curve = Line::new(Values::from_values_iter(iter)).color(Color32::BLUE); + let zero = HLine::new(0.0).color(Color32::from_white_alpha(0)); + let optimal = HLine::new(45.0).name("Optimal").color(Color32::LIGHT_BLUE); + let target = HLine::new(80.0) + .name("Overheating") + .color(Color32::DARK_RED); + + let plot = Plot::new("cooling performance") + .allow_drag(false) + .allow_zoom(false) + .height(600.0) + .line(curve) + .hline(zero) + .hline(optimal) + .hline(target) + .legend(Legend::default()); + + ui.label("Temperature"); + ui.add(plot); + ui.horizontal(|ui| { + ui.label("Current temperature"); + ui.label(format!("{:<3.2}°C", current)); + }); + ui.label("Working services"); + if pid_files.0.is_empty() { + ui.label(" There's no working services"); + } else { + pid_files.0.iter().for_each(|service| { + ui.label(format!(" {}", service.pid.0)); + }); + } + } + + pub fn push(&mut self, v: f64) { + if self.data.len() >= self.capacity { + self.data.pop_front(); + } + self.data.push_back(v); + } +} diff --git a/amdguid/src/widgets/drag_plot.rs b/amdguid/src/widgets/drag_plot.rs new file mode 100644 index 0000000..d3b5bf7 --- /dev/null +++ b/amdguid/src/widgets/drag_plot.rs @@ -0,0 +1,415 @@ +use egui::{emath, vec2, CursorIcon, Id, NumExt, PointerButton, Response, Sense, Ui, Vec2}; +use epaint::ahash::AHashSet; +use epaint::color::Hsva; +use epaint::Color32; + +use crate::items::HLine; +use crate::items::*; +use crate::transform::{Bounds, ScreenTransform}; +use crate::widgets::drag_plot_prepared::DragPlotPrepared; +use crate::widgets::legend::Legend; +use crate::widgets::legend_widget::LegendWidget; + +#[derive(Debug)] +pub enum PlotMsg { + Clicked(usize), + Drag(emath::Vec2), +} + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +struct PlotMemory { + bounds: Bounds, + auto_bounds: bool, + hovered_entry: Option, + hidden_items: AHashSet, + min_auto_bounds: Bounds, +} + +pub struct DragPlot +where + OnEvent: FnMut(PlotMsg), +{ + id: egui::Id, + + items: Vec>, + + min_auto_bounds: Bounds, + min_size: Vec2, + width: Option, + height: Option, + data_aspect: Option, + view_aspect: Option, + legend_config: Option, + + next_auto_color_idx: usize, + allow_zoom: bool, + allow_drag: bool, + margin_fraction: Vec2, + selected: Option, + on_event: Option, + + lines: Vec, + axis_names: [String; 2], +} + +impl DragPlot +where + OnEvent: FnMut(PlotMsg), +{ + pub fn new(id_source: impl std::hash::Hash) -> Self { + Self { + id: Id::new(id_source), + + items: Default::default(), + min_size: Vec2::splat(64.0), + width: None, + height: None, + data_aspect: None, + view_aspect: None, + min_auto_bounds: Bounds::NOTHING, + legend_config: None, + next_auto_color_idx: 0, + allow_zoom: true, + allow_drag: true, + margin_fraction: Vec2::splat(0.05), + selected: None, + on_event: None, + lines: vec![], + axis_names: [String::from("x"), String::from("y")], + } + } + + pub fn x_axis_name(mut self, name: String) -> Self { + self.axis_names[0] = name; + self + } + + pub fn y_axis_name(mut self, name: String) -> Self { + self.axis_names[1] = name; + self + } + + /// Show a legend including all named items. + pub fn legend(mut self, legend: Legend) -> Self { + self.legend_config = Some(legend); + self + } + + /// Add a data lines. + pub fn line(mut self, mut line: Line) -> Self { + if line.series.is_empty() { + return self; + }; + + // Give the stroke an automatic color if no color has been assigned. + if line.stroke.color == Color32::TRANSPARENT { + line.stroke.color = self.auto_color(); + } + self.lines.push(line); + self + } + + pub fn selected(mut self, selected: Option) -> Self { + self.selected = selected; + self + } + + fn auto_color(&mut self) -> Color32 { + let i = self.next_auto_color_idx; + self.next_auto_color_idx += 1; + let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875 + let h = i as f32 * golden_ratio; + Hsva::new(h, 0.85, 0.5, 1.0).into() // TODO: OkLab or some other perspective color space + } + + /// width / height ratio of the data. + /// For instance, it can be useful to set this to `1.0` for when the two axes show the same + /// unit. + /// By default the plot window's aspect ratio is used. + pub fn data_aspect(mut self, data_aspect: f32) -> Self { + self.data_aspect = Some(data_aspect); + self + } + + /// width / height ratio of the plot region. + /// By default no fixed aspect ratio is set (and width/height will fill the ui it is in). + pub fn view_aspect(mut self, view_aspect: f32) -> Self { + self.view_aspect = Some(view_aspect); + self + } + + /// Width of plot. By default a plot will fill the ui it is in. + /// If you set [`Self::view_aspect`], the width can be calculated from the height. + pub fn width(mut self, width: f32) -> Self { + self.min_size.x = width; + self.width = Some(width); + self + } + + /// Height of plot. By default a plot will fill the ui it is in. + /// If you set [`Self::view_aspect`], the height can be calculated from the width. + pub fn height(mut self, height: f32) -> Self { + self.min_size.y = height; + self.height = Some(height); + self + } + + /// Minimum size of the plot view. + pub fn min_size(mut self, min_size: Vec2) -> Self { + self.min_size = min_size; + self + } + + pub fn allow_drag(mut self, allow_drag: bool) -> Self { + self.allow_drag = allow_drag; + self + } + + pub fn allow_zoom(mut self, allow_zoom: bool) -> Self { + self.allow_zoom = allow_zoom; + self + } + + /// Add a horizontal line. + /// Can be useful e.g. to show min/max bounds or similar. + /// Always fills the full width of the plot. + pub fn hline(mut self, mut hline: HLine) -> Self { + if hline.stroke.color == Color32::TRANSPARENT { + hline.stroke.color = self.auto_color(); + } + self.items.push(Box::new(hline)); + self + } + + /// Add a vertical line. + /// Can be useful e.g. to show min/max bounds or similar. + /// Always fills the full height of the plot. + pub fn vline(mut self, mut vline: VLine) -> Self { + if vline.stroke.color == Color32::TRANSPARENT { + vline.stroke.color = self.auto_color(); + } + self.items.push(Box::new(vline)); + self + } + + pub fn on_event(mut self, f: OnEvent) -> Self { + self.on_event = Some(f); + self + } +} + +impl egui::Widget for DragPlot +where + OnEvent: FnMut(PlotMsg), +{ + fn ui(self, ui: &mut Ui) -> Response { + let Self { + id, + mut items, + min_auto_bounds, + min_size, + width, + height, + data_aspect, + view_aspect, + legend_config, + next_auto_color_idx: _, + allow_zoom, + allow_drag, + margin_fraction, + selected: _, + on_event, + mut lines, + axis_names, + } = self; + let plot_id = ui.make_persistent_id(id); + let memory = ui + .memory() + .id_data + .get_mut_or_insert_with(plot_id, || PlotMemory { + bounds: min_auto_bounds, + auto_bounds: false, + hovered_entry: None, + hidden_items: Default::default(), + min_auto_bounds, + }) + .clone(); + + let PlotMemory { + mut bounds, + mut auto_bounds, + mut hovered_entry, + mut hidden_items, + min_auto_bounds, + } = memory; + + // Determine the size of the plot in the UI + let size = { + let width = width + .unwrap_or_else(|| { + if let (Some(height), Some(aspect)) = (height, view_aspect) { + height * aspect + } else { + ui.available_size_before_wrap().x + } + }) + .at_least(min_size.x); + + let height = height + .unwrap_or_else(|| { + if let Some(aspect) = view_aspect { + width / aspect + } else { + ui.available_size_before_wrap().y + } + }) + .at_least(min_size.y); + vec2(width, height) + }; + + let (rect, response) = ui.allocate_exact_size(size, Sense::click_and_drag()); + let plot_painter = ui.painter().sub_region(rect); + + plot_painter.add(epaint::RectShape { + rect, + corner_radius: 2.0, + fill: ui.visuals().extreme_bg_color, + stroke: ui.visuals().widgets.noninteractive.bg_stroke, + }); + + // Legend + let legend = legend_config + .and_then(|config| LegendWidget::try_new(rect, config, &items, &hidden_items)); + + // Remove the deselected items. + items.retain(|item| !hidden_items.contains(item.name())); + lines.retain(|item| !hidden_items.contains(&item.name)); + + // Highlight the hovered items. + if let Some(hovered_name) = &hovered_entry { + items + .iter_mut() + .filter(|entry| entry.name() == hovered_name) + .for_each(|entry| entry.highlight()); + lines + .iter_mut() + .filter(|entry| &entry.name == hovered_name) + .for_each(|entry| { + entry.highlight(); + }); + } + // Move highlighted items to front. + items.sort_by_key(|item| item.highlighted()); + lines.sort_by_key(|item| item.highlighted()); + + // Set bounds automatically based on content. + if auto_bounds || !bounds.is_valid() { + bounds = min_auto_bounds; + items + .iter() + .for_each(|item| bounds.merge(&item.get_bounds())); + lines + .iter() + .for_each(|item| bounds.merge(&item.get_bounds())); + bounds.add_relative_margin(margin_fraction); + } + // Make sure they are not empty. + if !bounds.is_valid() { + bounds = Bounds::new_symmetrical(1.0); + } + + let mut transform = ScreenTransform::new(rect, bounds, false, false); + // Enforce equal aspect ratio. + if let Some(data_aspect) = data_aspect { + transform.set_aspect(data_aspect as f64); + } + + // Zooming + if allow_zoom { + if let Some(hover_pos) = response.hover_pos() { + let zoom_factor = if data_aspect.is_some() { + Vec2::splat(ui.input().zoom_delta()) + } else { + ui.input().zoom_delta_2d() + }; + if zoom_factor != Vec2::splat(1.0) { + transform.zoom(zoom_factor, hover_pos); + auto_bounds = false; + } + + let scroll_delta = ui.input().scroll_delta; + if scroll_delta != Vec2::ZERO { + transform.translate_bounds(-scroll_delta); + auto_bounds = false; + } + } + } + + // Initialize values from functions. + items + .iter_mut() + .for_each(|item| item.initialize(transform.bounds().range_x())); + lines + .iter_mut() + .for_each(|line| line.initialize(transform.bounds().range_x())); + + let bounds = *transform.bounds(); + let prepared = DragPlotPrepared { + items, + lines, + show_x: true, + show_y: true, + show_axes: [true, true], + transform, + axis_names, + }; + if let Some(mut f) = on_event { + if let Some(pointer) = response.hover_pos() { + if response.mouse_down(PointerButton::Primary) { + if let Some(idx) = prepared.find_clicked(pointer) { + f(PlotMsg::Clicked(idx)); + } + } + } + if allow_drag && response.dragged_by(PointerButton::Primary) { + let delta = response.drag_delta() * Vec2::new(0.18, -0.18); + f(PlotMsg::Drag(delta)); + } + } + + prepared.ui(ui, &response); + + if let Some(mut legend) = legend { + ui.add(&mut legend); + hidden_items = legend.get_hidden_items(); + hovered_entry = legend.get_hovered_entry_name(); + } + + ui.memory().id_data.insert( + plot_id, + PlotMemory { + bounds, + auto_bounds, + hovered_entry, + hidden_items, + min_auto_bounds, + }, + ); + response.on_hover_cursor(CursorIcon::Crosshair) + } +} + +pub trait PointerExt { + fn mouse_down(&self, pointer: PointerButton) -> bool; +} + +impl PointerExt for Response { + fn mouse_down(&self, p: PointerButton) -> bool { + let pointer = &self.ctx.input().pointer; + match p { + PointerButton::Primary => pointer.primary_down(), + PointerButton::Secondary => pointer.secondary_down(), + PointerButton::Middle => pointer.middle_down(), + } + } +} diff --git a/amdguid/src/widgets/drag_plot_prepared.rs b/amdguid/src/widgets/drag_plot_prepared.rs new file mode 100644 index 0000000..c0fb1f6 --- /dev/null +++ b/amdguid/src/widgets/drag_plot_prepared.rs @@ -0,0 +1,265 @@ +use egui::{emath, pos2, remap_clamp, vec2, Align2, Layout, NumExt, Pos2, Response, Ui}; +use epaint::{Color32, Rgba, Shape, Stroke, TextStyle}; + +use crate::items::Value; +use crate::items::{Line, PlotItem}; +use crate::transform::ScreenTransform; + +pub struct DragPlotPrepared { + pub items: Vec>, + pub lines: Vec, + pub show_x: bool, + pub show_y: bool, + pub show_axes: [bool; 2], + pub transform: ScreenTransform, + pub axis_names: [String; 2], +} + +impl DragPlotPrepared { + pub fn ui(self, ui: &mut Ui, response: &Response) { + let mut shapes = Vec::new(); + + for d in 0..2 { + if self.show_axes[d] { + self.paint_axis(ui, d, &mut shapes); + } + } + + let transform = &self.transform; + + let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default()); + plot_ui.set_clip_rect(*transform.frame()); + for item in &self.items { + item.get_shapes(&mut plot_ui, transform, &mut shapes); + } + for item in &self.lines { + item.get_shapes(&mut plot_ui, transform, &mut shapes); + } + + if let Some(pointer) = response.hover_pos() { + self.hover(ui, pointer, &mut shapes); + } + + ui.painter().sub_region(*transform.frame()).extend(shapes); + } + + fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec) { + let Self { transform, .. } = self; + + let bounds = transform.bounds(); + let text_style = TextStyle::Body; + + let base: i64 = 10; + let base_f = base as f64; + + let min_line_spacing_in_points = 6.0; + let step_size = transform.dvalue_dpos()[axis] * min_line_spacing_in_points; + let step_size = base_f.powi(step_size.abs().log(base_f).ceil() as i32); + + let step_size_in_points = (transform.dpos_dvalue()[axis] * step_size).abs() as f32; + + // Where on the cross-dimension to show the label values + let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]); + + for i in 0.. { + let value_main = step_size * (bounds.min[axis] / step_size + i as f64).floor(); + if value_main > bounds.max[axis] { + break; + } + + let value = if axis == 0 { + Value::new(value_main, value_cross) + } else { + Value::new(value_cross, value_main) + }; + let pos_in_gui = transform.position_from_value(&value); + + let n = (value_main / step_size).round() as i64; + let spacing_in_points = if n % (base * base) == 0 { + step_size_in_points * (base_f * base_f) as f32 // think line (multiple of 100) + } else if n % base == 0 { + step_size_in_points * base_f as f32 // medium line (multiple of 10) + } else { + step_size_in_points // thin line + }; + + let line_alpha = remap_clamp( + spacing_in_points, + (min_line_spacing_in_points as f32)..=300.0, + 0.0..=0.15, + ); + + if line_alpha > 0.0 { + let line_color = color_from_alpha(ui, line_alpha); + + let mut p0 = pos_in_gui; + let mut p1 = pos_in_gui; + p0[1 - axis] = transform.frame().min[1 - axis]; + p1[1 - axis] = transform.frame().max[1 - axis]; + shapes.push(Shape::line_segment([p0, p1], Stroke::new(1.0, line_color))); + } + + let text_alpha = remap_clamp(spacing_in_points, 40.0..=150.0, 0.0..=0.4); + + if text_alpha > 0.0 { + let color = color_from_alpha(ui, text_alpha); + let text = emath::round_to_decimals(value_main, 5).to_string(); // hack + + let galley = ui.painter().layout_no_wrap(text, text_style, color); + + let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y); + + // Make sure we see the labels, even if the axis is off-screen: + text_pos[1 - axis] = text_pos[1 - axis] + .at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0) + .at_least(transform.frame().min[1 - axis] + 1.0); + + shapes.push(Shape::galley(text_pos, galley)); + } + } + + fn color_from_alpha(ui: &Ui, alpha: f32) -> Color32 { + if ui.visuals().dark_mode { + Rgba::from_white_alpha(alpha).into() + } else { + Rgba::from_black_alpha((4.0 * alpha).at_most(1.0)).into() + } + } + } + + pub fn find_clicked(&self, pointer: Pos2) -> Option { + let Self { + transform, lines, .. + } = self; + let interact_radius: f32 = 16.0; + let mut closest_value = None; + let mut closest_dist_sq = interact_radius.powi(2); + for item in lines { + if let Some(values) = item.values() { + for (idx, value) in values.values.iter().enumerate() { + let pos = transform.position_from_value(value); + let dist_sq = pointer.distance_sq(pos); + if dist_sq < closest_dist_sq { + closest_dist_sq = dist_sq; + closest_value = Some(idx); + } + } + } + } + + closest_value + } + + fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec) { + let Self { + transform, + show_x, + show_y, + items, + lines, + .. + } = self; + + if !show_x && !show_y { + return; + } + + let interact_radius: f32 = 16.0; + let mut closest_value = None; + let mut closest_item = None; + let mut closest_dist_sq = interact_radius.powi(2); + for item in items { + if let Some(values) = item.values() { + for value in &values.values { + let pos = transform.position_from_value(value); + let dist_sq = pointer.distance_sq(pos); + if dist_sq < closest_dist_sq { + closest_dist_sq = dist_sq; + closest_value = Some(value); + closest_item = Some(item.name()); + } + } + } + } + for item in lines { + if let Some(values) = item.values() { + for value in &values.values { + let pos = transform.position_from_value(value); + let dist_sq = pointer.distance_sq(pos); + if dist_sq < closest_dist_sq { + closest_dist_sq = dist_sq; + closest_value = Some(value); + closest_item = Some(&item.name); + } + } + } + } + + let mut prefix = String::new(); + if let Some(name) = closest_item { + if !name.is_empty() { + prefix = format!("{}\n", name); + } + } + + let line_color = if ui.visuals().dark_mode { + Color32::from_gray(100).additive() + } else { + Color32::from_black_alpha(180) + }; + + let value = if let Some(value) = closest_value { + let position = transform.position_from_value(value); + shapes.push(Shape::circle_filled(position, 3.0, line_color)); + *value + } else { + transform.value_from_position(pointer) + }; + let pointer = transform.position_from_value(&value); + + let rect = transform.frame(); + + if *show_x { + // vertical line + shapes.push(Shape::line_segment( + [pos2(pointer.x, rect.top()), pos2(pointer.x, rect.bottom())], + (1.0, line_color), + )); + } + if *show_y { + // horizontal line + shapes.push(Shape::line_segment( + [pos2(rect.left(), pointer.y), pos2(rect.right(), pointer.y)], + (1.0, line_color), + )); + } + + let text = { + let scale = transform.dvalue_dpos(); + let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).at_most(6); + let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6); + let [x_name, y_name] = &self.axis_names; + if *show_x && *show_y { + format!( + "{}{} = {:.*}\n{} = {:.*}", + prefix, x_name, x_decimals, value.x, y_name, y_decimals, value.y + ) + } else if *show_x { + format!("{}{} = {:.*}", prefix, x_name, x_decimals, value.x) + } else if *show_y { + format!("{}{} = {:.*}", prefix, y_name, y_decimals, value.y) + } else { + unreachable!() + } + }; + + shapes.push(Shape::text( + ui.fonts(), + pointer + vec2(3.0, -2.0), + Align2::LEFT_BOTTOM, + text, + TextStyle::Body, + ui.visuals().text_color(), + )); + } +} diff --git a/amdguid/src/widgets/legend.rs b/amdguid/src/widgets/legend.rs new file mode 100644 index 0000000..dc79a8c --- /dev/null +++ b/amdguid/src/widgets/legend.rs @@ -0,0 +1,150 @@ +use std::string::String; + +use egui::{pos2, vec2, Align, PointerButton, Rect, Response, Sense, WidgetInfo, WidgetType}; +use epaint::{Color32, TextStyle}; + +/// Where to place the plot legend. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Corner { + LeftTop, + RightTop, + LeftBottom, + RightBottom, +} + +impl Corner { + pub fn all() -> impl Iterator { + [ + Corner::LeftTop, + Corner::RightTop, + Corner::LeftBottom, + Corner::RightBottom, + ] + .iter() + .copied() + } +} + +/// The configuration for a plot legend. +#[derive(Clone, Copy, PartialEq)] +pub struct Legend { + pub text_style: TextStyle, + pub background_alpha: f32, + pub position: Corner, +} + +impl Default for Legend { + fn default() -> Self { + Self { + text_style: TextStyle::Body, + background_alpha: 0.75, + position: Corner::RightTop, + } + } +} + +impl Legend { + /// Which text style to use for the legend. Default: `TextStyle::Body`. + pub fn text_style(mut self, style: TextStyle) -> Self { + self.text_style = style; + self + } + + /// The alpha of the legend background. Default: `0.75`. + pub fn background_alpha(mut self, alpha: f32) -> Self { + self.background_alpha = alpha; + self + } + + /// In which corner to place the legend. Default: `Corner::RightTop`. + pub fn position(mut self, corner: Corner) -> Self { + self.position = corner; + self + } +} + +#[derive(Clone)] +pub struct LegendEntry { + pub color: Color32, + pub checked: bool, + pub hovered: bool, +} + +impl LegendEntry { + pub fn new(color: Color32, checked: bool) -> Self { + Self { + color, + checked, + hovered: false, + } + } + + pub fn ui(&mut self, ui: &mut egui::Ui, text: String) -> Response { + let Self { + color, + checked, + hovered, + } = self; + + let galley = + ui.fonts() + .layout_delayed_color(text, ui.style().body_text_style, f32::INFINITY); + + let icon_size = galley.size().y; + let icon_spacing = icon_size / 5.0; + let total_extra = vec2(icon_size + icon_spacing, 0.0); + + let desired_size = total_extra + galley.size(); + let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click()); + + response + .widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, galley.text())); + + let visuals = ui.style().interact(&response); + let label_on_the_left = ui.layout().horizontal_placement() == Align::RIGHT; + + let icon_position_x = if label_on_the_left { + rect.right() - icon_size / 2.0 + } else { + rect.left() + icon_size / 2.0 + }; + let icon_position = pos2(icon_position_x, rect.center().y); + let icon_rect = Rect::from_center_size(icon_position, vec2(icon_size, icon_size)); + + let painter = ui.painter(); + + painter.add(epaint::CircleShape { + center: icon_rect.center(), + radius: icon_size * 0.5, + fill: visuals.bg_fill, + stroke: visuals.bg_stroke, + }); + + if *checked { + let fill = if *color == Color32::TRANSPARENT { + ui.visuals().noninteractive().fg_stroke.color + } else { + *color + }; + painter.add(epaint::Shape::circle_filled( + icon_rect.center(), + icon_size * 0.4, + fill, + )); + } + + let text_position_x = if label_on_the_left { + rect.right() - icon_size - icon_spacing - galley.size().x + } else { + rect.left() + icon_size + icon_spacing + }; + + let text_position = pos2(text_position_x, rect.center().y - 0.5 * galley.size().y); + painter.galley_with_color(text_position, galley, visuals.text_color()); + + *checked ^= response.clicked_by(PointerButton::Primary); + *hovered = response.hovered(); + + response + } +} diff --git a/amdguid/src/widgets/legend_widget.rs b/amdguid/src/widgets/legend_widget.rs new file mode 100644 index 0000000..a85472e --- /dev/null +++ b/amdguid/src/widgets/legend_widget.rs @@ -0,0 +1,113 @@ +use std::collections::BTreeMap; + +use egui::{vec2, Align, Direction, Frame, Layout, Rect, Response, Ui, Widget}; +use epaint::ahash::AHashSet; +use epaint::Color32; + +use crate::items::PlotItem; +use crate::widgets::legend::{Corner, Legend, LegendEntry}; + +#[derive(Clone)] +pub struct LegendWidget { + rect: Rect, + entries: BTreeMap, + config: Legend, +} + +impl LegendWidget { + /// Create a new legend from items, the names of items that are hidden and the style of the + /// text. Returns `None` if the legend has no entries. + pub fn try_new( + rect: Rect, + config: Legend, + items: &[Box], + hidden_items: &AHashSet, + ) -> Option { + let mut entries: BTreeMap = BTreeMap::new(); + items + .iter() + .filter(|item| !item.name().is_empty()) + .for_each(|item| { + entries + .entry(item.name().to_string()) + .and_modify(|entry| { + if entry.color != item.color() { + // Multiple items with different colors + entry.color = Color32::TRANSPARENT; + } + }) + .or_insert_with(|| { + let color = item.color(); + let checked = !hidden_items.contains(item.name()); + LegendEntry::new(color, checked) + }); + }); + (!entries.is_empty()).then(|| Self { + rect, + entries, + config, + }) + } + + // Get the names of the hidden items. + pub fn get_hidden_items(&self) -> AHashSet { + self.entries + .iter() + .filter(|(_, entry)| !entry.checked) + .map(|(name, _)| name.clone()) + .collect() + } + + // Get the name of the hovered items. + pub fn get_hovered_entry_name(&self) -> Option { + self.entries + .iter() + .find(|(_, entry)| entry.hovered) + .map(|(name, _)| name.to_string()) + } +} + +impl Widget for &mut LegendWidget { + fn ui(self, ui: &mut Ui) -> Response { + let LegendWidget { + rect, + entries, + config, + } = self; + + let main_dir = match config.position { + Corner::LeftTop | Corner::RightTop => Direction::TopDown, + Corner::LeftBottom | Corner::RightBottom => Direction::BottomUp, + }; + let cross_align = match config.position { + Corner::LeftTop | Corner::LeftBottom => Align::LEFT, + Corner::RightTop | Corner::RightBottom => Align::RIGHT, + }; + let layout = Layout::from_main_dir_and_cross_align(main_dir, cross_align); + let legend_pad = 4.0; + let legend_rect = rect.shrink(legend_pad); + let mut legend_ui = ui.child_ui(legend_rect, layout); + legend_ui + .scope(|ui| { + ui.style_mut().body_text_style = config.text_style; + let background_frame = Frame { + margin: vec2(8.0, 4.0), + corner_radius: ui.style().visuals.window_corner_radius, + shadow: epaint::Shadow::default(), + fill: ui.style().visuals.extreme_bg_color, + stroke: ui.style().visuals.window_stroke(), + } + .multiply_with_opacity(config.background_alpha); + background_frame + .show(ui, |ui| { + entries + .iter_mut() + .map(|(name, entry)| entry.ui(ui, name.clone())) + .reduce(|r1, r2| r1.union(r2)) + .unwrap() + }) + .inner + }) + .inner + } +} diff --git a/amdguid/src/widgets/mod.rs b/amdguid/src/widgets/mod.rs new file mode 100644 index 0000000..9405256 --- /dev/null +++ b/amdguid/src/widgets/mod.rs @@ -0,0 +1,12 @@ +pub mod change_fan_settings; +pub mod config_file; +pub mod cooling_performance; +pub mod drag_plot; +pub mod drag_plot_prepared; +pub mod legend; +pub mod legend_widget; +pub mod reload_section; + +pub use change_fan_settings::*; +pub use config_file::*; +pub use cooling_performance::*; diff --git a/amdguid/src/widgets/reload_section.rs b/amdguid/src/widgets/reload_section.rs new file mode 100644 index 0000000..3d353da --- /dev/null +++ b/amdguid/src/widgets/reload_section.rs @@ -0,0 +1,60 @@ +use crate::app::{ChangeState, FanServices}; +use amdgpu::helper_cmd::Command; +use egui::{PointerButton, Response, Sense, Ui}; +use epaint::Color32; + +pub struct ReloadSection<'l> { + pub services: &'l mut FanServices, +} + +impl<'l> egui::Widget for ReloadSection<'l> { + fn ui(self, ui: &mut Ui) -> Response { + ui.vertical(|ui| { + ui.label("Reload config for service"); + + self.services.0.iter_mut().for_each(|service| { + ui.horizontal(|ui| { + ui.label(format!("PID {}", service.pid.0)); + if ui.button("Reload").clicked_by(PointerButton::Primary) { + service.reload = ChangeState::Reloading; + + match amdgpu::helper_cmd::send_command(Command::ReloadConfig { + pid: service.pid, + }) { + Ok(response) => { + service.reload = ChangeState::Success; + log::info!("{:?}", response) + } + Err(e) => { + service.reload = ChangeState::Failure(format!("{:?}", e)); + log::error!("Failed to reload config. {:?}", e) + } + } + } + match &service.reload { + ChangeState::New => {} + ChangeState::Reloading => { + ui.label("Reloading..."); + } + ChangeState::Success => { + ui.add(egui::Label::new("Reloaded").text_color(Color32::DARK_GREEN)); + } + ChangeState::Failure(msg) => { + ui.add( + egui::Label::new(format!("Failure. {}", msg)) + .text_color(Color32::RED), + ); + } + } + }); + }); + }); + ui.allocate_response(ui.available_size(), Sense::click()) + } +} + +impl<'l> ReloadSection<'l> { + pub fn new(services: &'l mut FanServices) -> Self { + Self { services } + } +} diff --git a/amdmond-lib/Cargo.toml b/amdmond-lib/Cargo.toml new file mode 100644 index 0000000..abd4679 --- /dev/null +++ b/amdmond-lib/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "amdmond-lib" +version = "1.0.9" +edition = "2021" +description = "AMD GPU monitoring tool for Linux" +license = "MIT OR Apache-2.0" +keywords = ["hardware", "amdgpu"] +categories = ["hardware-support"] +repository = "https://github.com/Eraden/amdgpud" + +[dependencies] +amdgpu = { path = "../amdgpu", version = "1.0.9" } +amdgpu-config = { path = "../amdgpu-config", version = "1.0.9", features = ["monitor", "fan"] } + +serde = { version = "1.0.126", features = ["derive"] } +toml = { version = "0.5.8" } +csv = { version = "1.1.6" } + +thiserror = "1.0.30" +gumdrop = { version = "0.8.0" } + +chrono = { version = "0.4.19", features = ["serde"] } + +log = { version = "0.4.14" } +pretty_env_logger = { version = "0.4.0" } + +[dev-dependencies] +amdgpu = { path = "../amdgpu", version = "1.0" } +amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["monitor", "fan"] } diff --git a/amdmond-lib/src/errors.rs b/amdmond-lib/src/errors.rs new file mode 100644 index 0000000..de953d6 --- /dev/null +++ b/amdmond-lib/src/errors.rs @@ -0,0 +1,26 @@ +use amdgpu::utils; +use amdgpu_config::{fan, monitor}; + +#[derive(Debug, thiserror::Error)] +pub enum AmdMonError { + #[error("Mon AMD GPU card was found")] + NoHwMon, + #[error("{0}")] + Io(#[from] std::io::Error), + #[error("{0}")] + MonConfigError(#[from] monitor::ConfigError), + #[error("{0}")] + FanConfigError(#[from] fan::ConfigError), + #[error("{0}")] + AmdUtils(#[from] utils::AmdGpuError), + #[error("{0}")] + Csv(#[from] csv::Error), + #[error("AMD GPU temperature is malformed. It should be number. {0:?}")] + NonIntTemp(std::num::ParseIntError), + #[error("AMD GPU fan speed is malformed. It should be number. {0:?}")] + NonIntPwm(std::num::ParseIntError), + #[error("Monitor format is not valid. Available values are: short, s, long l, verbose and v")] + InvalidMonitorFormat, + #[error("Failed to read AMD GPU temperatures from tempX_input. No input was found")] + EmptyTempSet, +} diff --git a/amdmond-lib/src/lib.rs b/amdmond-lib/src/lib.rs new file mode 100644 index 0000000..35d626e --- /dev/null +++ b/amdmond-lib/src/lib.rs @@ -0,0 +1,117 @@ +pub mod errors; + +use crate::errors::AmdMonError; +use amdgpu::hw_mon::HwMon; +use amdgpu::utils::load_temp_inputs; +use amdgpu::{ + TempInput, PULSE_WIDTH_MODULATION, PULSE_WIDTH_MODULATION_MAX, PULSE_WIDTH_MODULATION_MIN, +}; +use amdgpu_config::fan; + +pub type Result = std::result::Result; + +pub struct AmdMon { + temp_input: Option, + inputs: Vec, + hw_mon: HwMon, + /// Minimal modulation (between 0-255) + pub pwm_min: Option, + /// Maximal modulation (between 0-255) + pub pwm_max: Option, +} + +impl std::ops::Deref for AmdMon { + type Target = HwMon; + + fn deref(&self) -> &Self::Target { + &self.hw_mon + } +} + +impl AmdMon { + pub fn wrap_all(mons: Vec, config: &fan::Config) -> Vec { + mons.into_iter() + .map(|hw_mon| Self::wrap(hw_mon, config)) + .collect() + } + + pub fn wrap(hw_mon: HwMon, config: &fan::Config) -> Self { + Self { + temp_input: config.temp_input().cloned(), + inputs: load_temp_inputs(&hw_mon), + hw_mon, + pwm_min: None, + pwm_max: None, + } + } + + pub fn gpu_temp(&self) -> Vec<(String, crate::Result)> { + self.inputs + .clone() + .into_iter() + .map(|name| { + let temp = self + .read_gpu_temp(name.as_str()) + .map(|temp| temp as f64 / 1000f64); + (name, temp) + }) + .collect() + } + + pub fn gpu_temp_of(&self, input_idx: usize) -> Option<(&String, crate::Result)> { + self.inputs.get(input_idx).map(|name| { + let temp = self + .read_gpu_temp(name.as_str()) + .map(|temp| temp as f64 / 1000f64); + (name, temp) + }) + } + + pub fn read_gpu_temp(&self, name: &str) -> crate::Result { + let value = self + .hw_mon_read(name)? + .parse::() + .map_err(AmdMonError::NonIntTemp)?; + Ok(value) + } + + pub fn pwm(&self) -> crate::Result { + let value = self + .hw_mon_read(PULSE_WIDTH_MODULATION)? + .parse() + .map_err(AmdMonError::NonIntPwm)?; + Ok(value) + } + + pub fn pwm_min(&mut self) -> u32 { + if self.pwm_min.is_none() { + self.pwm_min = Some(self.value_or(PULSE_WIDTH_MODULATION_MIN, 0)); + }; + self.pwm_min.unwrap_or_default() + } + + pub fn pwm_max(&mut self) -> u32 { + if self.pwm_max.is_none() { + self.pwm_max = Some(self.value_or(PULSE_WIDTH_MODULATION_MAX, 255)); + }; + self.pwm_max.unwrap_or(255) + } + + pub fn max_gpu_temp(&self) -> crate::Result { + if let Some(input) = self.temp_input.as_ref() { + let value = self.read_gpu_temp(&input.as_string())?; + return Ok(value as f64 / 1000f64); + } + let mut results = Vec::with_capacity(self.inputs.len()); + for name in self.inputs.iter() { + results.push(self.read_gpu_temp(name).unwrap_or(0)); + } + results.sort_unstable(); + let value = results + .last() + .copied() + .map(|temp| temp as f64 / 1000f64) + .ok_or(AmdMonError::EmptyTempSet)?; + Ok(value) + } +} diff --git a/amdmond/Cargo.toml b/amdmond/Cargo.toml index 69f5ec1..36d856a 100644 --- a/amdmond/Cargo.toml +++ b/amdmond/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "amdmond" -version = "1.0.8" +version = "1.0.9" edition = "2021" description = "AMD GPU monitoring tool for Linux" license = "MIT OR Apache-2.0" @@ -9,14 +9,15 @@ categories = ["hardware-support"] repository = "https://github.com/Eraden/amdgpud" [dependencies] -amdgpu = { path = "../amdgpu", version = "1.0.8" } -amdgpu-config = { path = "../amdgpu-config", version = "1.0.8", features = ["monitor", "fan"] } +amdgpu = { path = "../amdgpu", version = "1.0.9" } +amdgpu-config = { path = "../amdgpu-config", version = "1.0.9", features = ["monitor", "fan"] } +amdmond-lib = { path = "../amdmond-lib", version = "1.0.9" } serde = { version = "1.0.126", features = ["derive"] } toml = { version = "0.5.8" } csv = { version = "1.1.6" } -thiserror = "1.0.30" +thiserror = { version = "1.0.30" } gumdrop = { version = "0.8.0" } chrono = { version = "0.4.19", features = ["serde"] } @@ -27,3 +28,4 @@ pretty_env_logger = { version = "0.4.0" } [dev-dependencies] amdgpu = { path = "../amdgpu", version = "1.0" } amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["monitor", "fan"] } +amdmond-lib = { path = "../amdmond-lib", version = "1.0" } diff --git a/amdmond/src/command.rs b/amdmond/src/command.rs index 7bbc03e..6695add 100644 --- a/amdmond/src/command.rs +++ b/amdmond/src/command.rs @@ -1,8 +1,4 @@ -use crate::{log_file, watch, AmdMonError}; -use amdgpu::hw_mon::HwMon; -use amdgpu::utils::load_temp_inputs; -use amdgpu::{TempInput, PULSE_WIDTH_MODULATION_MAX, PULSE_WIDTH_MODULATION_MIN}; -use amdgpu_config::{fan, PULSE_WIDTH_MODULATION}; +use crate::{log_file, watch}; #[derive(gumdrop::Options)] pub enum Command { @@ -15,100 +11,3 @@ impl Default for Command { Self::Watch(watch::Watch::default()) } } - -pub struct AmdMon { - temp_input: Option, - inputs: Vec, - hw_mon: HwMon, - /// Minimal modulation (between 0-255) - pub pwm_min: Option, - /// Maximal modulation (between 0-255) - pub pwm_max: Option, -} - -impl std::ops::Deref for AmdMon { - type Target = HwMon; - - fn deref(&self) -> &Self::Target { - &self.hw_mon - } -} - -impl AmdMon { - pub(crate) fn wrap_all(mons: Vec, config: &fan::Config) -> Vec { - mons.into_iter() - .map(|hw_mon| Self::wrap(hw_mon, config)) - .collect() - } - - pub fn wrap(hw_mon: HwMon, config: &fan::Config) -> Self { - Self { - temp_input: config.temp_input().cloned(), - inputs: load_temp_inputs(&hw_mon), - hw_mon, - pwm_min: None, - pwm_max: None, - } - } - - pub fn gpu_temp(&self) -> Vec<(String, crate::Result)> { - self.inputs - .clone() - .into_iter() - .map(|name| { - let temp = self - .read_gpu_temp(name.as_str()) - .map(|temp| temp as f64 / 1000f64); - (name, temp) - }) - .collect() - } - - pub fn read_gpu_temp(&self, name: &str) -> crate::Result { - let value = self - .hw_mon_read(name)? - .parse::() - .map_err(AmdMonError::NonIntTemp)?; - Ok(value) - } - - pub fn pwm(&self) -> crate::Result { - let value = self - .hw_mon_read(PULSE_WIDTH_MODULATION)? - .parse() - .map_err(AmdMonError::NonIntPwm)?; - Ok(value) - } - - pub fn pwm_min(&mut self) -> u32 { - if self.pwm_min.is_none() { - self.pwm_min = Some(self.value_or(PULSE_WIDTH_MODULATION_MIN, 0)); - }; - self.pwm_min.unwrap_or_default() - } - - pub fn pwm_max(&mut self) -> u32 { - if self.pwm_max.is_none() { - self.pwm_max = Some(self.value_or(PULSE_WIDTH_MODULATION_MAX, 255)); - }; - self.pwm_max.unwrap_or(255) - } - - pub fn max_gpu_temp(&self) -> crate::Result { - if let Some(input) = self.temp_input.as_ref() { - let value = self.read_gpu_temp(&input.as_string())?; - return Ok(value as f64 / 1000f64); - } - let mut results = Vec::with_capacity(self.inputs.len()); - for name in self.inputs.iter() { - results.push(self.read_gpu_temp(name).unwrap_or(0)); - } - results.sort_unstable(); - let value = results - .last() - .copied() - .map(|temp| temp as f64 / 1000f64) - .ok_or(AmdMonError::EmptyTempSet)?; - Ok(value) - } -} diff --git a/amdmond/src/log_file.rs b/amdmond/src/log_file.rs index b7715f9..79b9241 100644 --- a/amdmond/src/log_file.rs +++ b/amdmond/src/log_file.rs @@ -1,9 +1,9 @@ -use crate::command::AmdMon; -use crate::AmdMonError; use amdgpu::utils::hw_mons; use amdgpu_config::fan; use amdgpu_config::fan::DEFAULT_FAN_CONFIG_PATH; use amdgpu_config::monitor::Config; +use amdmond_lib::errors::AmdMonError; +use amdmond_lib::AmdMon; #[derive(gumdrop::Options)] pub struct LogFile { @@ -24,7 +24,7 @@ struct Stat { temperature_setting: f64, } -pub fn run(command: LogFile, config: Config) -> crate::Result<()> { +pub fn run(command: LogFile, config: Config) -> amdmond_lib::Result<()> { let fan_config = fan::load_config(DEFAULT_FAN_CONFIG_PATH)?; let duration = std::time::Duration::new( diff --git a/amdmond/src/main.rs b/amdmond/src/main.rs index a91a359..bbc80e6 100644 --- a/amdmond/src/main.rs +++ b/amdmond/src/main.rs @@ -2,39 +2,11 @@ mod command; mod log_file; mod watch; -use amdgpu::utils; use gumdrop::Options; use crate::command::Command; use amdgpu::utils::ensure_config_dir; use amdgpu_config::monitor::{load_config, Config, DEFAULT_MONITOR_CONFIG_PATH}; -use amdgpu_config::{fan, monitor}; - -#[derive(Debug, thiserror::Error)] -pub enum AmdMonError { - #[error("Mon AMD GPU card was found")] - NoHwMon, - #[error("{0}")] - Io(#[from] std::io::Error), - #[error("{0}")] - MonConfigError(#[from] monitor::ConfigError), - #[error("{0}")] - FanConfigError(#[from] fan::ConfigError), - #[error("{0}")] - AmdUtils(#[from] utils::AmdGpuError), - #[error("{0}")] - Csv(#[from] csv::Error), - #[error("AMD GPU temperature is malformed. It should be number. {0:?}")] - NonIntTemp(std::num::ParseIntError), - #[error("AMD GPU fan speed is malformed. It should be number. {0:?}")] - NonIntPwm(std::num::ParseIntError), - #[error("Monitor format is not valid. Available values are: short, s, long l, verbose and v")] - InvalidMonitorFormat, - #[error("Failed to read AMD GPU temperatures from tempX_input. No input was found")] - EmptyTempSet, -} - -pub type Result = std::result::Result; #[derive(gumdrop::Options)] pub struct Opts { @@ -48,7 +20,7 @@ pub struct Opts { pub command: Option, } -fn run(config: Config) -> Result<()> { +fn run(config: Config) -> amdmond_lib::Result<()> { let opts: Opts = Opts::parse_args_default_or_exit(); if opts.version { @@ -65,7 +37,7 @@ fn run(config: Config) -> Result<()> { } } -fn setup() -> Result<(String, Config)> { +fn setup() -> amdmond_lib::Result<(String, Config)> { if std::env::var("RUST_LOG").is_err() { std::env::set_var("RUST_LOG", "DEBUG"); } @@ -81,7 +53,7 @@ fn setup() -> Result<(String, Config)> { Ok((config_path, config)) } -fn main() -> Result<()> { +fn main() -> amdmond_lib::Result<()> { let (_config_path, config) = match setup() { Ok(config) => config, Err(e) => { diff --git a/amdmond/src/watch.rs b/amdmond/src/watch.rs index 7e6994a..950cdfc 100644 --- a/amdmond/src/watch.rs +++ b/amdmond/src/watch.rs @@ -3,9 +3,8 @@ use std::str::FromStr; use amdgpu::utils::{hw_mons, linear_map}; use amdgpu_config::fan::DEFAULT_FAN_CONFIG_PATH; use amdgpu_config::{fan, monitor}; - -use crate::command::AmdMon; -use crate::AmdMonError; +use amdmond_lib::errors::AmdMonError; +use amdmond_lib::AmdMon; #[derive(Debug)] pub enum MonitorFormat { @@ -49,7 +48,7 @@ impl Default for Watch { } /// Start print cards temperature and fan speed -pub fn run(monitor: Watch, _config: monitor::Config) -> crate::Result<()> { +pub fn run(monitor: Watch, _config: monitor::Config) -> amdmond_lib::Result<()> { let fan_config = fan::load_config(DEFAULT_FAN_CONFIG_PATH)?; match monitor.format { MonitorFormat::Short => short(fan_config), @@ -57,7 +56,7 @@ pub fn run(monitor: Watch, _config: monitor::Config) -> crate::Result<()> { } } -pub fn verbose(config: fan::Config) -> crate::Result<()> { +pub fn verbose(config: fan::Config) -> amdmond_lib::Result<()> { let mut hw_mons = AmdMon::wrap_all(hw_mons(true)?, &config); loop { @@ -100,7 +99,7 @@ pub fn verbose(config: fan::Config) -> crate::Result<()> { } } -pub fn short(config: fan::Config) -> crate::Result<()> { +pub fn short(config: fan::Config) -> amdmond_lib::Result<()> { let mut hw_mons = AmdMon::wrap_all(hw_mons(true)?, &config); loop { print!("{esc}[2J{esc}[1;1H", esc = 27 as char); diff --git a/amdvold/.cargo/config.toml b/amdvold/.cargo/config.toml new file mode 100644 index 0000000..a9db1c1 --- /dev/null +++ b/amdvold/.cargo/config.toml @@ -0,0 +1,7 @@ +[build] +target = "x86_64-unknown-linux-musl" + +[profile.release] +lto = true +panic = "abort" +codegen-units = 1 diff --git a/amdvold/Cargo.toml b/amdvold/Cargo.toml index 83ce529..6fd16df 100644 --- a/amdvold/Cargo.toml +++ b/amdvold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "amdvold" -version = "1.0.8" +version = "1.0.9" edition = "2018" description = "AMDGPU fan control service" license = "MIT OR Apache-2.0" @@ -9,12 +9,12 @@ categories = ["hardware-support"] repository = "https://github.com/Eraden/amdgpud" [dependencies] -amdgpu = { path = "../amdgpu", version = "1.0.8" } -amdgpu-config = { path = "../amdgpu-config", version = "1.0.8", features = ["voltage"] } +amdgpu = { path = "../amdgpu", version = "1.0.9" } +amdgpu-config = { path = "../amdgpu-config", version = "1.0.9", features = ["voltage"] } serde = { version = "1.0.126", features = ["derive"] } toml = { version = "0.5.8" } -thiserror = "1.0.30" +thiserror = { version = "1.0.30" } gumdrop = { version = "0.8.0" } log = { version = "0.4.14" } diff --git a/assets/config.png b/assets/config.png new file mode 100644 index 0000000..f20466c Binary files /dev/null and b/assets/config.png differ diff --git a/assets/monitoring.png b/assets/monitoring.png new file mode 100644 index 0000000..65abd4d Binary files /dev/null and b/assets/monitoring.png differ diff --git a/assets/settings.png b/assets/settings.png new file mode 100644 index 0000000..934df03 Binary files /dev/null and b/assets/settings.png differ diff --git a/build.sh b/build.sh deleted file mode 100755 index 298372e..0000000 --- a/build.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env zsh - -cargo build --release - -strip target/x86_64-unknown-linux-musl/release/amdfand -strip target/x86_64-unknown-linux-musl/release/amdvold -strip target/x86_64-unknown-linux-musl/release/amdmond - -upx --best --lzma target/x86_64-unknown-linux-musl/release/amdfand -upx --best --lzma target/x86_64-unknown-linux-musl/release/amdvold -upx --best --lzma target/x86_64-unknown-linux-musl/release/amdmond diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..9d95097 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,23 @@ +set -e +x + +cd "$(git rev-parse --show-toplevel)" + +./scripts/compile.sh + +strip target/x86_64-unknown-linux-musl/release/amdfand +strip target/x86_64-unknown-linux-musl/release/amdvold +strip target/x86_64-unknown-linux-musl/release/amdmond + +upx --best --lzma target/x86_64-unknown-linux-musl/release/amdfand +upx --best --lzma target/x86_64-unknown-linux-musl/release/amdvold +upx --best --lzma target/x86_64-unknown-linux-musl/release/amdmond + +cargo build --release --target x86_64-unknown-linux-gnu --bin amdguid --no-default-features --features xorg +strip target/x86_64-unknown-linux-gnu/release/amdguid +upx --best --lzma target/x86_64-unknown-linux-gnu/release/amdguid +zip ./target/amdguid-glium.zip ./target/x86_64-unknown-linux-gnu/release/amdguid + +cargo build --release --target x86_64-unknown-linux-gnu --bin amdguid --no-default-features --features wayland +strip target/x86_64-unknown-linux-gnu/release/amdguid +upx --best --lzma target/x86_64-unknown-linux-gnu/release/amdguid +zip ./target/amdguid-wayland.zip ./target/x86_64-unknown-linux-gnu/release/amdguid diff --git a/scripts/compile.sh b/scripts/compile.sh new file mode 100755 index 0000000..fcdf8b4 --- /dev/null +++ b/scripts/compile.sh @@ -0,0 +1,9 @@ +set -e +x + +cd "$(git rev-parse --show-toplevel)" + +cargo build --release --target x86_64-unknown-linux-musl --bin amdfand +cargo build --release --target x86_64-unknown-linux-musl --bin amdmond +cargo build --release --target x86_64-unknown-linux-musl --bin amdvold +cargo build --release --target x86_64-unknown-linux-musl --bin amdgui-helper +cargo build --release --target x86_64-unknown-linux-gnu --bin amdguid --no-default-features --features xorg diff --git a/publish.sh b/scripts/publish.sh similarity index 68% rename from publish.sh rename to scripts/publish.sh index 8b04658..514a305 100755 --- a/publish.sh +++ b/scripts/publish.sh @@ -14,5 +14,15 @@ cargo publish cd $(git_root)/amdvold cargo publish +cd $(git_root)/amdmond-lib +cargo publish + cd $(git_root)/amdmond cargo publish + +cd $(git_root)/amdgui-helper +cargo publish + + +cd $(git_root)/amdguid +cargo publish diff --git a/scripts/zip-ci.sh b/scripts/zip-ci.sh new file mode 100755 index 0000000..a2d7f8a --- /dev/null +++ b/scripts/zip-ci.sh @@ -0,0 +1,8 @@ +echo Building binaries-$1.zip + +zip binaries-$1.zip ./target/x86_64-unknown-linux-musl/release/amdfand; +zip binaries-$1.zip ./target/x86_64-unknown-linux-musl/release/amdmond; +zip binaries-$1.zip ./target/x86_64-unknown-linux-musl/release/amdvold; +zip binaries-$1.zip ./target/x86_64-unknown-linux-musl/release/amdgui-helper; +zip binaries-$1.zip ./target/amdguid-wayland.zip; +zip binaries-$1.zip ./target/amdguid-glium.zip diff --git a/services/amdfand.service b/services/amdfand.service index 50ade54..f911731 100644 --- a/services/amdfand.service +++ b/services/amdfand.service @@ -5,5 +5,6 @@ After=sysinit.target local-fs.target Restart=on-failure RestartSec=4 ExecStart=/usr/bin/amdfand service +Environment=RUST_LOG=ERROR [Install] WantedBy=multi-user.target diff --git a/services/amdgui-helper b/services/amdgui-helper new file mode 100755 index 0000000..9360f46 --- /dev/null +++ b/services/amdgui-helper @@ -0,0 +1,12 @@ +#!/sbin/openrc-run + +description="amdgui helper service" +pidfile="/run/${SVCNAME}.pid" +command="/usr/bin/amdgui-helper" +command_args="service" +command_user="root" +command_background=true + +depend() { + need udev +} diff --git a/services/amdgui-helper.service b/services/amdgui-helper.service new file mode 100644 index 0000000..bf2fca7 --- /dev/null +++ b/services/amdgui-helper.service @@ -0,0 +1,10 @@ +[Unit] +Description=AMD GPU gui helper +After=sysinit.target local-fs.target +[Service] +Restart=on-failure +RestartSec=4 +ExecStart=/usr/bin/amdgui-helper +Environment=RUST_LOG=ERROR +[Install] +WantedBy=multi-user.target diff --git a/services/amdmond.service b/services/amdmond.service index be2b65f..292728a 100644 --- a/services/amdmond.service +++ b/services/amdmond.service @@ -5,5 +5,6 @@ After=sysinit.target local-fs.target Restart=on-failure RestartSec=4 ExecStart=/usr/bin/amdmond log_file -s /var/log/amdmon.csv +Environment=RUST_LOG=ERROR [Install] WantedBy=multi-user.target diff --git a/services/amdvold.service b/services/amdvold.service index aa6e6cc..a5ca7ee 100644 --- a/services/amdvold.service +++ b/services/amdvold.service @@ -5,5 +5,6 @@ After=sysinit.target local-fs.target Restart=on-failure RestartSec=4 ExecStart=/usr/bin/amdvold service +Environment=RUST_LOG=ERROR [Install] WantedBy=multi-user.target