diff --git a/.gitmodules b/.gitmodules index 90176cbd..371247f3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "toml-config/toml-test"] - path = toml-config/toml-test +[submodule "crates/toml-config/toml-test"] + path = crates/toml-config/toml-test url = https://github.com/mahkoh/toml-tests.git diff --git a/Cargo.lock b/Cargo.lock index defd9291..9f84c039 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -625,11 +625,21 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jay-algorithms" -version = "0.4.0" +version = "1.12.0" dependencies = [ "smallvec", ] +[[package]] +name = "jay-allocator" +version = "1.12.0" +dependencies = [ + "jay-formats", + "jay-video-types", + "thiserror", + "uapi", +] + [[package]] name = "jay-ash" version = "0.3.0+1.4.344" @@ -639,6 +649,52 @@ dependencies = [ "libloading", ] +[[package]] +name = "jay-async-engine" +version = "1.12.0" +dependencies = [ + "jay-time", + "jay-tracy", + "jay-utils", +] + +[[package]] +name = "jay-bufio" +version = "1.12.0" +dependencies = [ + "jay-io-uring", + "jay-utils", + "thiserror", + "uapi", +] + +[[package]] +name = "jay-bugs" +version = "1.12.0" +dependencies = [ + "ahash", +] + +[[package]] +name = "jay-clientmem" +version = "1.12.0" +dependencies = [ + "jay-cpu-worker", + "jay-gfx-types", + "jay-tracy", + "jay-utils", + "log", + "thiserror", + "uapi", +] + +[[package]] +name = "jay-cmm" +version = "1.12.0" +dependencies = [ + "jay-utils", +] + [[package]] name = "jay-compositor" version = "1.12.0" @@ -664,9 +720,48 @@ dependencies = [ "indexmap", "isnt 0.2.0", "jay-algorithms", + "jay-allocator", "jay-ash", + "jay-async-engine", + "jay-bufio", + "jay-bugs", + "jay-clientmem", + "jay-cmm", "jay-config", + "jay-cpu-worker", + "jay-criteria", + "jay-damage", + "jay-dbus-core", + "jay-drm-feedback", + "jay-edid", + "jay-eventfd-cache", + "jay-formats", + "jay-geometry", + "jay-gfx-types", + "jay-input-types", + "jay-io-uring", + "jay-keyboard", + "jay-layout-animation", + "jay-libinput", + "jay-logger", + "jay-output-schedule", + "jay-output-types", + "jay-pango", + "jay-pr-caps", + "jay-sighand", + "jay-theme", + "jay-time", "jay-toml-config", + "jay-tracy", + "jay-tree-types", + "jay-udmabuf", + "jay-units", + "jay-utils", + "jay-video-types", + "jay-wheel", + "jay-wire-buf", + "jay-wire-types", + "jay-xcon", "kbvm", "libloading", "linearize", @@ -682,13 +777,11 @@ dependencies = [ "regex", "repc", "run-on-drop", - "rustc-demangle", "serde", "serde_json", "smallvec", "thiserror", "tiny-skia", - "tracy-client-sys", "uapi", "walkdir", "with_builtin_macros", @@ -696,10 +789,9 @@ dependencies = [ [[package]] name = "jay-config" -version = "1.10.0" +version = "1.12.0" dependencies = [ "backtrace", - "bincode", "bstr", "error_reporter", "futures-util", @@ -711,24 +803,409 @@ dependencies = [ "uapi", ] +[[package]] +name = "jay-config-schema" +version = "1.12.0" +dependencies = [ + "ahash", + "jay-config", +] + +[[package]] +name = "jay-cpu-worker" +version = "1.12.0" +dependencies = [ + "jay-async-engine", + "jay-geometry", + "jay-io-uring", + "jay-tracy", + "jay-utils", + "jay-wheel", + "log", + "parking_lot", + "thiserror", + "uapi", +] + +[[package]] +name = "jay-criteria" +version = "1.12.0" +dependencies = [ + "ahash", + "jay-utils", + "linearize", + "regex", +] + +[[package]] +name = "jay-damage" +version = "1.12.0" +dependencies = [ + "jay-geometry", + "jay-tree-types", + "jay-units", +] + +[[package]] +name = "jay-dbus-core" +version = "1.12.0" +dependencies = [ + "bstr", + "jay-bufio", + "jay-io-uring", + "jay-utils", + "thiserror", + "uapi", +] + +[[package]] +name = "jay-drm-feedback" +version = "1.12.0" +dependencies = [ + "ahash", + "byteorder", + "jay-video-types", + "thiserror", + "uapi", +] + +[[package]] +name = "jay-edid" +version = "1.12.0" +dependencies = [ + "bstr", + "thiserror", +] + +[[package]] +name = "jay-eventfd-cache" +version = "1.12.0" +dependencies = [ + "jay-async-engine", + "jay-io-uring", + "jay-utils", + "log", + "thiserror", + "uapi", +] + +[[package]] +name = "jay-formats" +version = "1.12.0" +dependencies = [ + "ahash", + "clap", + "jay-ash", + "jay-config", +] + +[[package]] +name = "jay-geometry" +version = "1.12.0" +dependencies = [ + "jay-algorithms", + "smallvec", +] + +[[package]] +name = "jay-gfx-types" +version = "1.12.0" +dependencies = [ + "uapi", +] + +[[package]] +name = "jay-input-types" +version = "1.12.0" +dependencies = [ + "jay-output-types", + "jay-units", + "jay-utils", + "linearize", +] + +[[package]] +name = "jay-io-uring" +version = "1.12.0" +dependencies = [ + "jay-async-engine", + "jay-time", + "jay-utils", + "log", + "run-on-drop", + "thiserror", + "uapi", +] + +[[package]] +name = "jay-keyboard" +version = "1.12.0" +dependencies = [ + "blake3", + "jay-input-types", + "jay-utils", + "kbvm", + "thiserror", + "uapi", +] + +[[package]] +name = "jay-layout-animation" +version = "1.12.0" +dependencies = [ + "jay-geometry", +] + +[[package]] +name = "jay-libinput" +version = "1.12.0" +dependencies = [ + "anyhow", + "bstr", + "cc", + "isnt 0.2.0", + "jay-utils", + "libloading", + "log", + "repc", + "thiserror", + "uapi", +] + +[[package]] +name = "jay-logger" +version = "1.12.0" +dependencies = [ + "backtrace", + "bstr", + "clap", + "dirs", + "humantime", + "jay-config", + "jay-utils", + "linearize", + "log", + "parking_lot", + "thiserror", + "uapi", +] + +[[package]] +name = "jay-output-schedule" +version = "1.12.0" +dependencies = [ + "futures-util", + "jay-async-engine", + "jay-io-uring", + "jay-utils", + "log", + "num-traits", +] + +[[package]] +name = "jay-output-types" +version = "1.12.0" +dependencies = [ + "blake3", + "jay-cmm", + "jay-formats", + "jay-utils", + "linearize", + "uapi", +] + +[[package]] +name = "jay-pango" +version = "1.12.0" +dependencies = [ + "anyhow", + "jay-geometry", + "repc", + "thiserror", + "uapi", +] + +[[package]] +name = "jay-pr-caps" +version = "1.12.0" +dependencies = [ + "jay-utils", + "opera", + "parking_lot", + "uapi", +] + +[[package]] +name = "jay-sighand" +version = "1.12.0" +dependencies = [ + "jay-async-engine", + "jay-io-uring", + "jay-utils", + "log", + "thiserror", + "uapi", +] + +[[package]] +name = "jay-theme" +version = "1.12.0" +dependencies = [ + "jay-cmm", + "jay-config", + "jay-gfx-types", + "jay-utils", + "linearize", + "num-traits", +] + +[[package]] +name = "jay-time" +version = "1.12.0" +dependencies = [ + "uapi", +] + +[[package]] +name = "jay-toml" +version = "1.12.0" +dependencies = [ + "bstr", + "indexmap", + "serde_json", + "thiserror", + "walkdir", +] + [[package]] name = "jay-toml-config" -version = "0.12.0" +version = "1.12.0" dependencies = [ "ahash", "bstr", "error_reporter", "indexmap", "jay-config", + "jay-config-schema", + "jay-toml", "kbvm", "log", "phf", "run-on-drop", - "serde_json", "simplelog", "thiserror", "uapi", - "walkdir", +] + +[[package]] +name = "jay-tracy" +version = "1.12.0" +dependencies = [ + "ahash", + "parking_lot", + "rustc-demangle", + "tracy-client-sys", +] + +[[package]] +name = "jay-tree-types" +version = "1.12.0" +dependencies = [ + "jay-config", + "jay-utils", + "linearize", +] + +[[package]] +name = "jay-udmabuf" +version = "1.12.0" +dependencies = [ + "jay-allocator", + "jay-formats", + "jay-utils", + "jay-video-types", + "log", + "thiserror", + "uapi", +] + +[[package]] +name = "jay-units" +version = "1.12.0" + +[[package]] +name = "jay-utils" +version = "1.12.0" +dependencies = [ + "ahash", + "arrayvec", + "bstr", + "cfg-if", + "isnt 0.2.0", + "jay-config", + "linearize", + "log", + "parking_lot", + "rand 0.10.0", + "serde", + "smallvec", + "thiserror", + "uapi", +] + +[[package]] +name = "jay-video-types" +version = "1.12.0" +dependencies = [ + "arrayvec", + "jay-formats", + "jay-utils", + "uapi", +] + +[[package]] +name = "jay-wheel" +version = "1.12.0" +dependencies = [ + "jay-async-engine", + "jay-io-uring", + "jay-time", + "jay-utils", + "log", + "thiserror", + "uapi", +] + +[[package]] +name = "jay-wire-buf" +version = "1.12.0" +dependencies = [ + "bstr", + "jay-io-uring", + "jay-time", + "jay-units", + "jay-utils", + "jay-wire-types", + "smallvec", + "thiserror", + "uapi", +] + +[[package]] +name = "jay-wire-types" +version = "1.12.0" + +[[package]] +name = "jay-xcon" +version = "1.12.0" +dependencies = [ + "bstr", + "jay-bufio", + "jay-io-uring", + "jay-utils", + "log", + "thiserror", + "uapi", ] [[package]] @@ -1522,7 +1999,7 @@ dependencies = [ [[package]] name = "toml-spec" -version = "0.1.0" +version = "1.12.0" dependencies = [ "anyhow", "error_reporter", @@ -1934,7 +2411,7 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "wire-to-xml" -version = "0.1.0" +version = "1.12.0" dependencies = [ "anyhow", "clap", @@ -2051,7 +2528,7 @@ dependencies = [ [[package]] name = "xml-to-wire" -version = "0.1.0" +version = "1.12.0" dependencies = [ "quick-xml", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 9a1b38a3..72eb71da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "jay-compositor" -version = "1.12.0" -edition = "2024" +version.workspace = true +edition.workspace = true build = "build/build.rs" -license = "GPL-3.0-only" +license.workspace = true description = "The Jay compositor" repository = "https://github.com/mahkoh/jay" default-run = "jay" @@ -13,7 +13,61 @@ name = "jay" path = "src/main.rs" [workspace] -members = ["jay-config", "toml-config", "algorithms", "toml-spec", "wire-to-xml", "xml-to-wire"] +resolver = "3" +members = [ + "crates/jay-config", + "crates/jay-config-schema", + "crates/geometry", + "crates/layout-animation", + "crates/formats", + "crates/edid", + "crates/units", + "crates/utils", + "crates/criteria", + "crates/cmm", + "crates/time", + "crates/tracy", + "crates/async-engine", + "crates/io-uring", + "crates/bufio", + "crates/dbus-core", + "crates/xcon", + "crates/wire-types", + "crates/wire-buf", + "crates/tree-types", + "crates/eventfd-cache", + "crates/wheel", + "crates/cpu-worker", + "crates/sighand", + "crates/pr-caps", + "crates/bugs", + "crates/logger", + "crates/video-types", + "crates/output-types", + "crates/input-types", + "crates/keyboard", + "crates/gfx-types", + "crates/theme", + "crates/clientmem", + "crates/allocator", + "crates/output-schedule", + "crates/drm-feedback", + "crates/udmabuf", + "crates/damage", + "crates/pango", + "crates/libinput", + "crates/toml-config", + "crates/toml-parser", + "crates/algorithms", + "crates/toml-spec", + "crates/wire-to-xml", + "crates/xml-to-wire", +] + +[workspace.package] +version = "1.12.0" +edition = "2024" +license = "GPL-3.0-only" [profile.release] panic = "abort" @@ -23,9 +77,48 @@ debug = "full" panic = "abort" [dependencies] -jay-config = { version = "1.10.0", path = "jay-config" } -jay-toml-config = { version = "0.12.0", path = "toml-config" } -jay-algorithms = { version = "0.4.0", path = "algorithms" } +jay-config = { path = "crates/jay-config" } +jay-toml-config = { path = "crates/toml-config" } +jay-algorithms = { path = "crates/algorithms" } +jay-geometry = { path = "crates/geometry" } +jay-layout-animation = { path = "crates/layout-animation" } +jay-formats = { path = "crates/formats" } +jay-edid = { path = "crates/edid" } +jay-units = { path = "crates/units" } +jay-utils = { path = "crates/utils" } +jay-criteria = { path = "crates/criteria" } +jay-cmm = { path = "crates/cmm" } +jay-time = { path = "crates/time" } +jay-tracy = { path = "crates/tracy" } +jay-async-engine = { path = "crates/async-engine" } +jay-io-uring = { path = "crates/io-uring" } +jay-bufio = { path = "crates/bufio" } +jay-dbus-core = { path = "crates/dbus-core" } +jay-xcon = { path = "crates/xcon" } +jay-wire-types = { path = "crates/wire-types" } +jay-wire-buf = { path = "crates/wire-buf" } +jay-tree-types = { path = "crates/tree-types" } +jay-eventfd-cache = { path = "crates/eventfd-cache" } +jay-wheel = { path = "crates/wheel" } +jay-cpu-worker = { path = "crates/cpu-worker" } +jay-sighand = { path = "crates/sighand" } +jay-pr-caps = { path = "crates/pr-caps" } +jay-bugs = { path = "crates/bugs" } +jay-logger = { path = "crates/logger" } +jay-video-types = { path = "crates/video-types" } +jay-output-types = { path = "crates/output-types" } +jay-input-types = { path = "crates/input-types" } +jay-keyboard = { path = "crates/keyboard" } +jay-gfx-types = { path = "crates/gfx-types" } +jay-theme = { path = "crates/theme" } +jay-clientmem = { path = "crates/clientmem" } +jay-allocator = { path = "crates/allocator" } +jay-output-schedule = { path = "crates/output-schedule" } +jay-drm-feedback = { path = "crates/drm-feedback" } +jay-udmabuf = { path = "crates/udmabuf" } +jay-damage = { path = "crates/damage" } +jay-pango = { path = "crates/pango" } +jay-libinput = { path = "crates/libinput" } uapi = "0.2.13" thiserror = "2.0.11" @@ -58,8 +151,6 @@ serde = { version = "1.0.196", features = ["derive"] } serde_json = "1.0.128" linearize = { version = "0.1.3", features = ["derive"] } png = "0.18.0" -rustc-demangle = { version = "0.1.24", optional = true } -tracy-client-sys = { version = "0.24.1", features = ["ondemand", "manual-lifetime", "debuginfod", "demangle"], optional = true } kbvm = { version = "0.1.6", features = ["compose"] } tiny-skia = { version = "0.12.0", default-features = false, features = ["std"] } regex = "1.11.1" @@ -90,5 +181,5 @@ opt-level = 3 [features] rc_tracking = [] -it = [] -tracy = ["dep:tracy-client-sys", "dep:rustc-demangle"] +it = ["jay-async-engine/it", "jay-cpu-worker/it"] +tracy = ["jay-tracy/tracy", "jay-async-engine/tracy", "jay-cpu-worker/tracy", "jay-clientmem/tracy"] diff --git a/README.md b/README.md index 14ae7ba4..56a5f577 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![crates.io](https://img.shields.io/crates/v/jay-compositor.svg)](http://crates.io/crates/jay-compositor) Jay is a Wayland compositor for Linux with an i3-like tiling layout, -Vulkan and OpenGL rendering, multi-GPU support, screen sharing, and more. +Vulkan and OpenGL rendering, multi-GPU support, and more. ![screenshot.png](static/screenshot.png) diff --git a/book/AGENTS.md b/book/AGENTS.md index 85e88261..8b5a0051 100644 --- a/book/AGENTS.md +++ b/book/AGENTS.md @@ -27,14 +27,14 @@ The table of contents is `SUMMARY.md`. Key chapter-to-topic mapping: | File | What it tells you | |------|-------------------| -| `toml-spec/spec/spec.yaml` | **Canonical** TOML config spec: every key, action, match criterion, type | -| `toml-config/src/default-config.toml` | Built-in default config (keybindings, startup actions) | -| `toml-config/src/config/parsers/action.rs` | Action parser — see which `type` strings are accepted | -| `toml-config/src/lib.rs` | Action dispatch — `window_or_seat!` macro shows which actions work in window rules | +| `crates/toml-spec/spec/spec.yaml` | **Canonical** TOML config spec: every key, action, match criterion, type | +| `crates/toml-config/src/default-config.toml` | Built-in default config (keybindings, startup actions) | +| `crates/toml-config/src/config/parsers/action.rs` | Action parser — see which `type` strings are accepted | +| `crates/toml-config/src/lib.rs` | Action dispatch — `window_or_seat!` macro shows which actions work in window rules | | `src/config/handler.rs` | Config handler; `update_capabilities` shows capability replacement semantics | | `src/cli/*.rs` | CLI subcommands (clap definitions) | | `src/control_center/cc_*.rs` | Control center pane implementations (verify field names/ordering here) | -| `toml-config/src/config/parsers/exec.rs` | Exec parser (string, array, or table forms) | +| `crates/toml-config/src/config/parsers/exec.rs` | Exec parser (string, array, or table forms) | ### Known spec.yaml bugs @@ -81,7 +81,7 @@ The table of contents is `SUMMARY.md`. Key chapter-to-topic mapping: - **Definition lists** for two-column term/description. Tables only for 3+ data columns. - **TOML formatting:** multiline with trailing commas, 4-space indent. - **Examples:** practical, not abstract. Link to - [spec.generated.md](https://github.com/mahkoh/jay/blob/master/toml-spec/spec/spec.generated.md) + [spec.generated.md](https://github.com/mahkoh/jay/blob/master/crates/toml-spec/spec/spec.generated.md) for exhaustive listings. - **Control center docs:** verify field names, ordering, and conditional visibility against `cc_*.rs` source files. Labels must match exactly. @@ -91,10 +91,10 @@ The table of contents is `SUMMARY.md`. Key chapter-to-topic mapping: ### Documenting a new action 1. Read `git diff` for the commit introducing the action. Key files: - - `toml-spec/spec/spec.yaml` — spec entry (description, fields, examples) - - `toml-config/src/config/parsers/action.rs` — parser (field names, types, defaults) - - `toml-config/src/lib.rs` — dispatch (check if `window_or_seat!` is used) - - `jay-config/src/input.rs` and/or `jay-config/src/window.rs` — Rust API + - `crates/toml-spec/spec/spec.yaml` — spec entry (description, fields, examples) + - `crates/toml-config/src/config/parsers/action.rs` — parser (field names, types, defaults) + - `crates/toml-config/src/lib.rs` — dispatch (check if `window_or_seat!` is used) + - `crates/jay-config/src/input.rs` and/or `crates/jay-config/src/window.rs` — Rust API 2. Edit `book/src/configuration/shortcuts.md`: - **Simple actions** (no fields): add to the appropriate list in the @@ -110,7 +110,7 @@ The table of contents is `SUMMARY.md`. Key chapter-to-topic mapping: ### Documenting a new config field -1. Read `toml-spec/spec/spec.yaml` for the field definition. +1. Read `crates/toml-spec/spec/spec.yaml` for the field definition. 2. Identify which book chapter covers that config section (see table above). 3. Add the field with a definition-list entry or example, matching the existing style of that chapter. diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 2bb5904a..29e51133 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -35,7 +35,6 @@ - [Mouse Interactions](mouse.md) - [Input Modes](input-modes.md) - [Window & Client Rules](window-rules.md) -- [Screen Sharing](screen-sharing.md) - [HDR & Color Management](hdr.md) # Reference diff --git a/book/src/cli.md b/book/src/cli.md index c7c222a6..e2b8697f 100644 --- a/book/src/cli.md +++ b/book/src/cli.md @@ -674,18 +674,6 @@ Show color management status: ## Other Commands -### `jay portal` - -Run the Jay desktop portal (provides screen sharing and other XDG desktop -portal interfaces): - -```shell -~$ jay portal -``` - -Normally the portal is started automatically. This command is for running it -manually or debugging. - ### `jay seat-test` Test input events from a seat. Prints all keyboard, pointer, touch, gesture, @@ -697,24 +685,6 @@ tablet, and switch events to stdout: ~$ jay seat-test -a # test all seats simultaneously ``` -### `jay run-privileged` - -Run a program with access to a privileged Wayland socket: - -```shell -~$ jay run-privileged my-program --arg1 -``` - -### `jay run-tagged` - -Run a program with a tagged Wayland connection. All Wayland connections from the -spawned process tree will carry the specified tag, which can be matched in -[client rules](window-rules.md): - -```shell -~$ jay run-tagged my-tag firefox -``` - ### `jay generate-completion` Generate shell completion scripts: diff --git a/book/src/configuration/idle.md b/book/src/configuration/idle.md index 033d1120..d67bfdc9 100644 --- a/book/src/configuration/idle.md +++ b/book/src/configuration/idle.md @@ -62,16 +62,10 @@ on-idle = { type = "exec", exec = { prog = "swaylock", - privileged = true, }, } ``` -> [!IMPORTANT] -> Screen lockers that use the Wayland session lock protocol (like swaylock) -> need `privileged = true` in the exec configuration. This grants the process -> the necessary permissions to lock the session. - You can also combine multiple actions: ```toml @@ -80,7 +74,6 @@ on-idle = [ type = "exec", exec = { prog = "swaylock", - privileged = true, }, }, { type = "exec", exec = ["notify-send", "System locked"] }, @@ -100,7 +93,6 @@ on-idle = { type = "exec", exec = { prog = "swaylock", - privileged = true, }, } ``` diff --git a/book/src/configuration/misc.md b/book/src/configuration/misc.md index 9974d3f7..33f0c8ba 100644 --- a/book/src/configuration/misc.md +++ b/book/src/configuration/misc.md @@ -30,9 +30,8 @@ the color management protocol. ## Libei [libei](https://gitlab.freedesktop.org/libinput/libei) allows applications to -emulate input events. By default, applications can only access libei through -the portal (which prompts the user for permission). Setting `enable-socket` -exposes an unauthenticated socket that any application can use without a prompt. +emulate input events. Setting `enable-socket` exposes an unauthenticated socket +that any application can use. ```toml libei.enable-socket = false # default diff --git a/book/src/configuration/shortcuts.md b/book/src/configuration/shortcuts.md index 6d372be6..b36f4857 100644 --- a/book/src/configuration/shortcuts.md +++ b/book/src/configuration/shortcuts.md @@ -145,7 +145,6 @@ alt-shift-r = "reload-config-toml" (the next pressed key identifies the mark). See [Marks](#marks) below. - `enable-window-management`, `disable-window-management` -- programmatically enable or disable [window management mode](../floating.md#window-management-mode) -- `reload-config-so` -- reload the shared-library configuration (`config.so`) See the [specification](https://github.com/mahkoh/jay/blob/master/toml-spec/spec/spec.generated.md) for the full list of simple actions. @@ -309,7 +308,6 @@ alt-s = { type = "exec", exec = { shell = "grim - | wl-copy", - privileged = true, }, } ``` @@ -328,12 +326,6 @@ Table fields: `env` : Per-process environment variables -`privileged` -: If `true`, grants access to privileged Wayland protocols (default: `false`) - -`tag` -: Tag to apply to all Wayland connections spawned by this process - ### Practical examples Volume control with `pactl`: @@ -362,7 +354,6 @@ Print = { type = "exec", exec = { shell = "grim - | wl-copy", - privileged = true, }, } ``` diff --git a/book/src/configuration/startup.md b/book/src/configuration/startup.md index 9b4c333a..93408650 100644 --- a/book/src/configuration/startup.md +++ b/book/src/configuration/startup.md @@ -63,15 +63,10 @@ on-idle = { type = "exec", exec = { prog = "swaylock", - privileged = true, }, } ``` -> [!NOTE] -> Screen lockers need `privileged = true` to access the privileged Wayland -> protocols required for locking the session. - You can combine idle with a grace period. The idle timeout and grace period are configured separately in the `[idle]` section (see [Idle & Screen Locking](idle.md)): @@ -83,7 +78,6 @@ on-idle = { type = "exec", exec = { prog = "swaylock", - privileged = true, }, } ``` @@ -97,7 +91,6 @@ on-idle = [ type = "exec", exec = { prog = "swaylock", - privileged = true, }, }, ] diff --git a/book/src/configuration/theme.md b/book/src/configuration/theme.md index 69448f5d..efd62d48 100644 --- a/book/src/configuration/theme.md +++ b/book/src/configuration/theme.md @@ -73,7 +73,7 @@ The available color keys in the `[theme]` table are: "Focused-inactive" refers to a window that was most recently focused in its container but whose container is not the active one. The "captured" colors apply -when a window is being recorded (e.g. via screen sharing). +when a window is being captured. ### Example diff --git a/book/src/configuration/xwayland.md b/book/src/configuration/xwayland.md index 8f5817af..2814a3b7 100644 --- a/book/src/configuration/xwayland.md +++ b/book/src/configuration/xwayland.md @@ -87,5 +87,5 @@ Xwayland client itself: ```toml [[clients]] match.is-xwayland = true -# ... grant capabilities, etc. +# ... configure client-specific behavior ``` diff --git a/book/src/features.md b/book/src/features.md index 1e7c69a3..efaefd74 100644 --- a/book/src/features.md +++ b/book/src/features.md @@ -61,10 +61,7 @@ Commands: unlock Unlocks the compositor screenshot Take a screenshot idle Inspect/modify the idle (screensaver) settings - run-privileged Run a privileged program - run-tagged Run a program with a connection tag seat-test Tests the events produced by a seat - portal Run the desktop portal randr Inspect/modify graphics card and connector settings input Inspect/modify input settings xwayland Inspect/modify xwayland settings @@ -101,17 +98,6 @@ runtime. See [GPUs](configuration/gpu.md) for details. -## Screen Sharing - -Jay supports screen sharing via xdg-desktop-portal. Three capture modes are -available: - -- **Window capture** -- share a single window. -- **Output capture** -- share an entire monitor. -- **Workspace capture** -- like output capture, but only one workspace is shown. - -See [Screen Sharing](screen-sharing.md) for setup instructions. - ## Screen Locking Jay can automatically lock your screen and disable outputs after inactivity. @@ -154,20 +140,6 @@ Jay supports running X11 applications seamlessly through Xwayland. See Jay supports clipboard managers via the `zwlr_data_control_manager_v1` and `ext_data_control_manager_v1` protocols. -## Privilege Separation - -Jay splits protocols into unprivileged and privileged protocols. By default, -applications only have access to unprivileged protocols. This means that tools -like screen lockers, status bars, and clipboard managers need to be explicitly -granted access. - -Jay provides several ways to grant privileges, from giving a program full -access to all privileged protocols down to granting individual capabilities to -specific tagged processes. See -[Granting Privileges](window-rules.md#granting-privileges) for a detailed -guide and the [Protocol Support](#protocol-support) section below for the full -list of protocols and their privilege requirements. - ## Push to Talk Jay's shortcut system allows you to execute an action when a key is pressed and @@ -252,7 +224,6 @@ granted access. See | wp_linux_drm_syncobj_manager_v1 | 1 | | | wp_pointer_warp_v1 | 1 | | | wp_presentation | 2 | | -| wp_security_context_manager_v1 | 1 | | | wp_single_pixel_buffer_manager_v1 | 1 | | | wp_tearing_control_manager_v1 | 1 | | | wp_viewporter | 1 | | diff --git a/book/src/installation.md b/book/src/installation.md index e29ec0b2..f331b284 100644 --- a/book/src/installation.md +++ b/book/src/installation.md @@ -63,7 +63,6 @@ For Vulkan, you also need the driver for your GPU: - **Linux 6.7 or later** -- required for explicit sync (needed for Nvidia GPUs). - **Xwayland** -- required for running X11 applications. -- **PipeWire** -- required for screen sharing. - **logind** (part of systemd) -- required when running Jay from a virtual terminal or display manager. ## Building @@ -129,14 +128,7 @@ retains `CAP_SYS_NICE` solely for creating elevated Vulkan queues later. > [!NOTE] > You need to re-run the `setcap` command each time you update the Jay binary. -### SCHED_RR and config.so - -`SCHED_RR` and `config.so` are mutually exclusive: running untrusted code at -real-time priority would be a security risk. Jay enforces this as follows: - -- If `config.so` exists in the config directory, Jay skips the `SCHED_RR` - elevation (elevated Vulkan queues are still created). -- If Jay has already elevated to `SCHED_RR`, it refuses to load `config.so`. +### SCHED_RR You can also skip `SCHED_RR` explicitly by setting `JAY_NO_REALTIME=1`: @@ -144,11 +136,7 @@ You can also skip `SCHED_RR` explicitly by setting `JAY_NO_REALTIME=1`: ~$ JAY_NO_REALTIME=1 jay run ``` -This still allows elevated Vulkan queues and does not affect `config.so` -loading. - -The mutual exclusion can be overridden at compile time by building Jay with -`JAY_ALLOW_REALTIME_CONFIG_SO=1`. +This still allows elevated Vulkan queues. ## Recommended Applications @@ -156,7 +144,6 @@ The following applications work well with Jay: - **[Alacritty](https://alacritty.org/)** -- the default terminal emulator in the built-in configuration. - **[bemenu](https://github.com/Cloudef/bemenu)** -- the default application launcher in the built-in configuration. -- **[xdg-desktop-portal-gtk4](https://github.com/mahkoh/xdg-desktop-portal-gtk4)** -- a file-picker portal with thumbnail support. Used automatically when installed. - **[wl-tray-bridge](https://github.com/mahkoh/wl-tray-bridge)** -- shows D-Bus StatusNotifierItem applications as tray icons. - **[mako](https://github.com/emersion/mako)** -- a notification daemon. Launched automatically by the default configuration. - **[window-to-tray](https://github.com/mahkoh/wl-proxy/tree/master/apps/window-to-tray)** -- run most Wayland applications as tray applications (e.g. `window-to-tray pavucontrol-qt`). diff --git a/book/src/introduction.md b/book/src/introduction.md index 306a5a46..9333dcc6 100644 --- a/book/src/introduction.md +++ b/book/src/introduction.md @@ -22,12 +22,11 @@ Jay is a Wayland compositor for Linux with an i3-inspired tiling layout. It supports Vulkan and OpenGL rendering, multi-GPU setups, fractional scaling, -variable refresh rate (VRR), tearing presentation, HDR, and screen sharing via -xdg-desktop-portal. X11 applications are supported through Xwayland. +variable refresh rate (VRR), tearing presentation, and HDR. X11 applications +are supported through Xwayland. -Jay is configured through a declarative TOML file, with an optional advanced -mode that uses a shared library for programmatic control. A comprehensive -command-line interface makes scripting and automation straightforward. +Jay is configured through a declarative TOML file. A comprehensive command-line +interface makes scripting and automation straightforward. See the [Features](features.md) chapter for a comprehensive overview of what Jay can do, or jump straight to [Installation](installation.md) to get started. diff --git a/book/src/mouse.md b/book/src/mouse.md index bfa92cd5..1b01a054 100644 --- a/book/src/mouse.md +++ b/book/src/mouse.md @@ -84,8 +84,8 @@ This is especially useful for: ## Other -**Toplevel selection.** Some actions (like screen sharing) ask you to select a -window, indicated by a purple overlay. During this selection, right-click a +**Toplevel selection.** Some actions ask you to select a window, indicated by a +purple overlay. During this selection, right-click a tile's title to select the entire container instead of an individual tile. **Canceling interactions.** Press `Escape` to cancel any in-progress mouse diff --git a/book/src/screen-sharing.md b/book/src/screen-sharing.md deleted file mode 100644 index 096c67de..00000000 --- a/book/src/screen-sharing.md +++ /dev/null @@ -1,111 +0,0 @@ -# Screen Sharing - -Jay supports screen sharing via -[xdg-desktop-portal](https://github.com/flatpak/xdg-desktop-portal). Three -capture types are available: - -- **Window capture** -- share a single window. -- **Output capture** -- share an entire monitor. -- **Workspace capture** -- like output capture, but only a single workspace is - shown. - -## Requirements - -[PipeWire](https://pipewire.org/) must be installed and running. Verify with: - -```shell -~$ systemctl --user status pipewire -``` - -## Portal Setup - -Jay implements its own portal backend for the `ScreenCast` and `RemoteDesktop` -interfaces. Two configuration files must be installed so that -`xdg-desktop-portal` knows to use Jay's backend. - -### If the Repository is Checked Out - -```shell -~$ sudo cp etc/jay.portal /usr/share/xdg-desktop-portal/portals/jay.portal -~$ sudo cp etc/jay-portals.conf /usr/share/xdg-desktop-portal/jay-portals.conf -``` - -### If Installed via cargo install - -Create the files manually: - -```shell -~$ sudo tee /usr/share/xdg-desktop-portal/portals/jay.portal > /dev/null << 'EOF' -[portal] -DBusName=org.freedesktop.impl.portal.desktop.jay -Interfaces=org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.RemoteDesktop; -EOF -``` - -```shell -~$ sudo tee /usr/share/xdg-desktop-portal/jay-portals.conf > /dev/null << 'EOF' -[preferred] -default=gtk -org.freedesktop.impl.portal.ScreenCast=jay -org.freedesktop.impl.portal.RemoteDesktop=jay -org.freedesktop.impl.portal.Inhibit=none -org.freedesktop.impl.portal.FileChooser=gtk4 -EOF -``` - -### Restart the Portal - -After installing the files, restart the portal service: - -```shell -~$ systemctl --user restart xdg-desktop-portal -``` - -## Configuration - -### workspace-capture - -The top-level `workspace-capture` setting controls whether newly created -workspaces can be captured via workspace capture. The default is `true`: - -```toml -workspace-capture = false -``` - -Set this to `false` if you want to prevent workspace-level capture by default. - -### Capture Indicator Colors - -When a window is being recorded, its title bar color changes to make the -capture visually obvious. You can customize these colors in the `[theme]` -table: - -```toml -[theme] -captured-focused-title-bg-color = "#900000" -captured-unfocused-title-bg-color = "#5f0000" -``` - -- `captured-focused-title-bg-color` -- background color of focused title bars - that are being recorded. -- `captured-unfocused-title-bg-color` -- background color of unfocused title - bars that are being recorded. - -## The jay portal Command - -Jay's portal backend is normally started automatically when a screen-sharing -request comes in via D-Bus activation. If you need to start it manually for -debugging purposes: - -```shell -~$ jay portal -``` - -## Troubleshooting - -If screen sharing does not work: - -1. Verify PipeWire is running: `systemctl --user status pipewire` -2. Verify the portal files are installed in `/usr/share/xdg-desktop-portal/`. -3. Restart the portal: `systemctl --user restart xdg-desktop-portal` -4. Check the Jay log for errors: `jay log` diff --git a/book/src/troubleshooting.md b/book/src/troubleshooting.md index 3f973bb5..a20a33fd 100644 --- a/book/src/troubleshooting.md +++ b/book/src/troubleshooting.md @@ -54,57 +54,6 @@ bindings. > when any config file exists. Always use `jay config init` to start with a > working configuration. -## Application doesn't have access to a protocol - -Jay splits Wayland protocols into unprivileged and privileged. By default, -applications only have access to unprivileged protocols. If a program like a -screen locker, status bar, clipboard manager, or screen-capture tool is not -working, it likely needs access to one or more privileged protocols. - -Common symptoms include: - -- **swaylock** does nothing or fails to lock the screen (needs `session-lock`). -- **waybar** or **i3bar** shows no workspace information (needs - `foreign-toplevel-list`). -- **wl-copy** / **cliphist** cannot access the clipboard (needs - `data-control`). -- **grim** or **slurp** cannot capture the screen (needs `screencopy`). - -**Quick fix -- grant all privileges:** - -The simplest approach is to launch the program with full access to all -privileged protocols. In your config, set `privileged = true` in the exec -action: - -```toml -on-idle = { - type = "exec", - exec = { - prog = "swaylock", - privileged = true, - }, -} -``` - -Or from the command line: - -```shell -~$ jay run-privileged waybar -``` - -**Better fix -- grant only the capabilities needed:** - -Use a client rule to grant specific capabilities: - -```toml -[[clients]] -match.comm = "waybar" -capabilities = ["layer-shell", "foreign-toplevel-list"] -``` - -See [Granting Privileges](window-rules.md#granting-privileges) for the full -list of capabilities and more advanced approaches using connection tags. - ## Wrong keyboard layout The default keyboard layout is US QWERTY. To change it: @@ -132,45 +81,6 @@ layout = "de" This takes effect immediately but does not persist across restarts unless configured in the config file. -## Screen sharing doesn't work - -Screen sharing requires PipeWire and the Jay desktop portal. - -**1. Check that PipeWire is running:** - -```shell -~$ systemctl --user status pipewire -``` - -If it is not running, start it: - -```shell -~$ systemctl --user start pipewire -``` - -**2. Check that the portal files are installed:** - -Jay needs two files to be found by the XDG desktop portal framework: - -- A portal definition file (e.g. `/usr/share/xdg-desktop-portal/portals/jay.portal`). -- A portal configuration file (e.g. `/usr/share/xdg-desktop-portal/jay-portals.conf`). - -These files are included in the Jay repository under `etc/`. If you built Jay -from source and did not install them, copy them manually: - -```shell -~$ sudo cp etc/jay.portal /usr/share/xdg-desktop-portal/portals/ -~$ sudo cp etc/jay-portals.conf /usr/share/xdg-desktop-portal/ -``` - -**3. Restart the portal:** - -```shell -~$ systemctl --user restart xdg-desktop-portal -``` - -See the [Screen Sharing](screen-sharing.md) chapter for more details. - ## X11 applications don't work Jay uses Xwayland to run X11 applications. diff --git a/book/src/window-rules.md b/book/src/window-rules.md index 43770b46..6d3cfca1 100644 --- a/book/src/window-rules.md +++ b/book/src/window-rules.md @@ -31,12 +31,6 @@ Each client rule can have the following fields: `latch` : An action to run when a client stops matching. -`capabilities` -: Wayland protocol access granted to matching clients. - -`sandbox-bounding-capabilities` -: Upper bounds for protocols available to child sandboxes. - ### Client Match Criteria All client match criteria are constant over the lifetime of a client. If no @@ -70,142 +64,6 @@ implicitly AND-combined. `exe` / `exe-regex` : The client's `/proc/pid/exe` path. -`tag` / `tag-regex` -: The connection tag of the client. - -### Granting Privileges - -Jay splits Wayland protocols into unprivileged and privileged. By default, -applications only have access to unprivileged protocols. This means that tools -like screen lockers, status bars, screen-capture utilities, and clipboard -managers will not work unless you explicitly grant them the necessary -privileges. - -See the [Protocol Support](features.md#protocol-support) table in the Features -chapter for the full list of protocols and whether they are privileged. - -There are three ways to grant privileges, from simplest to most fine-grained. - -#### 1. Grant all privileges via `privileged = true` (exec) or `jay run-privileged` - -The simplest approach gives a program access to **all** privileged protocols. -This is appropriate for trusted tools like screen lockers where you don't want -to think about which specific protocols they need. - -In the config, set `privileged = true` in the exec table: - -```toml -on-idle = { - type = "exec", - exec = { - prog = "swaylock", - privileged = true, - }, -} -``` - -From the command line, use `jay run-privileged`: - -```shell -~$ jay run-privileged waybar -``` - -Both methods connect the program to a privileged Wayland socket that grants -access to all privileged protocols. - -#### 2. Grant capabilities via connection tags - -Connection tags let you combine the CLI with client rules for precise control. -You tag a program at launch time, then write a client rule that matches -the tag and grants specific capabilities. - -First, launch the program with a tag -- either from the command line: - -```shell -~$ jay run-tagged bar waybar -``` - -Or from the config using the `tag` field in an exec action: - -```toml -[shortcuts] -alt-w = { - type = "exec", - exec = { - prog = "waybar", - tag = "bar", - }, -} -``` - -Then write a client rule that matches the tag and grants capabilities: - -```toml -[[clients]] -match.tag = "bar" -capabilities = ["layer-shell", "foreign-toplevel-list"] -``` - -This way, only the specific instance you launched with the tag receives the -privileges -- other programs with the same binary name do not. - -Available capability values: `none`, `all`, `data-control`, -`virtual-keyboard`, `foreign-toplevel-list`, `idle-notifier`, `session-lock`, -`layer-shell`, `screencopy`, `seat-manager`, `drm-lease`, `input-method`, -`workspace-manager`, `foreign-toplevel-manager`, `head-manager`, -`gamma-control-manager`, `virtual-pointer`. - -**Default capabilities:** unsandboxed clients receive `layer-shell` and -`drm-lease`. Sandboxed clients receive only `drm-lease`. If any client rule -matches, its capabilities **replace** the defaults entirely. If multiple rules -match, their capabilities are unioned together, but the defaults are not -included unless a matching rule also grants them. - -#### 3. Grant capabilities via client match rules - -Client rules can also match programs by properties like their executable name -instead of a tag. This is convenient when you always want a given program to -have certain capabilities, regardless of how it was launched: - -```toml -[[clients]] -match.comm = "waybar" -capabilities = ["layer-shell", "foreign-toplevel-list"] - -# Vim 9.2 uses the data-control protocol for seamless wayland integration. -[[clients]] -match.comm = "vim" -match.sandboxed = false -capabilities = "data-control" - -# Older versions use wl-copy and wl-paste. -[[clients]] -match.any = [ - { comm = "wl-copy" }, - { comm = "wl-paste" }, -] -match.sandboxed = false -capabilities = "data-control" -``` - -> [!NOTE] -> Client match criteria like `comm`, `exe`, and `pid` are checked when a -> client connects. Any process with a matching name receives the specified -> capabilities. If you need to restrict privileges to programs you launch -> yourself, use connection tags (method 2) instead. - -#### Bounding capabilities (sandboxes) - -Capabilities can never exceed the client's **bounding capabilities**. Use -`sandbox-bounding-capabilities` on a client rule to set the upper bound for -protocols available to sandboxes created by that client: - -```toml -[[clients]] -match.comm = "flatpak-portal" -sandbox-bounding-capabilities = ["drm-lease", "layer-shell"] -``` - ## Window Rules Window rules operate on individual windows. They are defined with `[[windows]]` @@ -456,18 +314,6 @@ action = { } ``` -### Grant Protocol Access to a Trusted App - -```toml -[[clients]] -match.comm = "swaylock" -capabilities = ["session-lock", "layer-shell"] - -[[clients]] -match.comm = "waybar" -capabilities = ["layer-shell", "foreign-toplevel-list"] -``` - ### Suppress Focus Stealing for Chromium Screen-Share Windows ```toml diff --git a/book/src/workspaces.md b/book/src/workspaces.md index 100e13b7..eb1bcfd3 100644 --- a/book/src/workspaces.md +++ b/book/src/workspaces.md @@ -123,16 +123,15 @@ laptop. ## Workspace Capture -By default, newly created workspaces can be captured for screen sharing. You -can disable this globally: +By default, newly created workspaces can be captured by capture clients. You can +disable this globally: ```toml workspace-capture = false ``` -When workspace capture is enabled, screen-sharing applications can share -individual workspaces (in addition to full outputs and individual windows). See -[Screen Sharing](screen-sharing.md) for more details. +When workspace capture is enabled, compositor-native capture clients may capture +individual workspaces instead of whole outputs. ## Matching Windows by Workspace diff --git a/build/enums.rs b/build/enums.rs index 8be06bf1..dc784903 100644 --- a/build/enums.rs +++ b/build/enums.rs @@ -4,18 +4,27 @@ use { std::{env, io::Write}, }; -#[expect(unused_macros)] -#[macro_use] -#[path = "../src/macros.rs"] -mod macros; +#[allow(unused_macros)] +macro_rules! cenum { + ($name:ident, $uc:ident; $($name2:ident = $val:expr,)*) => { + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + pub struct $name(pub i32); -#[path = "../src/libinput/consts.rs"] -mod libinput; + impl $name { + pub fn raw(self) -> i32 { + self.0 + } + } -#[path = "../src/pango/consts.rs"] -mod pango; + pub const $uc: &[i32] = &[$($val,)*]; -#[path = "../src/fontconfig/consts.rs"] + $( + pub const $name2: $name = $name($val); + )* + } +} + +#[path = "fontconfig_consts.rs"] mod fontconfig; fn get_target() -> repc::Target { @@ -49,108 +58,6 @@ fn write_ty(f: &mut W, vals: &[i32], ty: &str) -> anyhow::Result<()> { } pub fn main() -> anyhow::Result<()> { - let mut f = open("libinput_tys.rs")?; - write_ty( - &mut f, - libinput::LIBINPUT_LOG_PRIORITY, - "libinput_log_priority", - )?; - write_ty( - &mut f, - libinput::LIBINPUT_DEVICE_CAPABILITY, - "libinput_device_capability", - )?; - write_ty(&mut f, libinput::LIBINPUT_KEY_STATE, "libinput_key_state")?; - write_ty(&mut f, libinput::LIBINPUT_LED, "libinput_led")?; - write_ty( - &mut f, - libinput::LIBINPUT_BUTTON_STATE, - "libinput_button_state", - )?; - write_ty( - &mut f, - libinput::LIBINPUT_POINTER_AXIS, - "libinput_pointer_axis", - )?; - write_ty( - &mut f, - libinput::LIBINPUT_POINTER_AXIS_SOURCE, - "libinput_pointer_axis_source", - )?; - write_ty( - &mut f, - libinput::LIBINPUT_TABLET_PAD_RING_AXIS_SOURCE, - "libinput_tablet_pad_ring_axis_source", - )?; - write_ty( - &mut f, - libinput::LIBINPUT_TABLET_PAD_STRIP_AXIS_SOURCE, - "libinput_tablet_pad_strip_axis_source", - )?; - write_ty( - &mut f, - libinput::LIBINPUT_TABLET_TOOL_TYPE, - "libinput_tablet_tool_type", - )?; - write_ty( - &mut f, - libinput::LIBINPUT_TABLET_TOOL_PROXIMITY_STATE, - "libinput_tablet_tool_proximity_state", - )?; - write_ty( - &mut f, - libinput::LIBINPUT_TABLET_TOOL_TIP_STATE, - "libinput_tablet_tool_tip_state", - )?; - write_ty( - &mut f, - libinput::LIBINPUT_SWITCH_STATE, - "libinput_switch_state", - )?; - write_ty(&mut f, libinput::LIBINPUT_SWITCH, "libinput_switch")?; - write_ty(&mut f, libinput::LIBINPUT_EVENT_TYPE, "libinput_event_type")?; - write_ty( - &mut f, - libinput::LIBINPUT_CONFIG_STATUS, - "libinput_config_status", - )?; - write_ty( - &mut f, - libinput::LIBINPUT_CONFIG_ACCEL_PROFILE, - "libinput_config_accel_profile", - )?; - write_ty( - &mut f, - libinput::LIBINPUT_CONFIG_TAP_STATE, - "libinput_config_tap_state", - )?; - write_ty( - &mut f, - libinput::LIBINPUT_CONFIG_DRAG_STATE, - "libinput_config_drag_state", - )?; - write_ty( - &mut f, - libinput::LIBINPUT_CONFIG_DRAG_LOCK_STATE, - "libinput_config_drag_lock_state", - )?; - write_ty( - &mut f, - libinput::LIBINPUT_CONFIG_CLICK_METHOD, - "libinput_config_click_method", - )?; - write_ty( - &mut f, - libinput::LIBINPUT_CONFIG_MIDDLE_EMULATION_STATE, - "libinput_config_middle_emulation_state", - )?; - - let mut f = open("pango_tys.rs")?; - write_ty(&mut f, pango::CAIRO_FORMATS, "cairo_format_t")?; - write_ty(&mut f, pango::CAIRO_STATUSES, "cairo_status_t")?; - write_ty(&mut f, pango::CAIRO_OPERATORS, "cairo_operator_t")?; - write_ty(&mut f, pango::PANGO_ELLIPSIZE_MODES, "PangoEllipsizeMode_")?; - let mut f = open("fontconfig_tys.rs")?; write_ty(&mut f, fontconfig::FC_MATCH_KINDS, "FcMatchKind")?; write_ty(&mut f, fontconfig::FC_RESULTS, "FcResult")?; diff --git a/src/fontconfig/consts.rs b/build/fontconfig_consts.rs similarity index 100% rename from src/fontconfig/consts.rs rename to build/fontconfig_consts.rs diff --git a/build/logging.rs b/build/logging.rs index 7cae1ec4..5c7ce1f8 100644 --- a/build/logging.rs +++ b/build/logging.rs @@ -4,17 +4,10 @@ use { }; pub fn main() -> anyhow::Result<()> { - create_bridge()?; create_version()?; Ok(()) } -fn create_bridge() -> anyhow::Result<()> { - println!("cargo:rerun-if-changed=src/bridge.c"); - cc::Build::new().file("src/bridge.c").compile("bridge"); - Ok(()) -} - fn create_version() -> anyhow::Result<()> { let mut version_string = env!("CARGO_PKG_VERSION").to_string(); if let Ok(output) = Command::new("git").arg("rev-parse").arg("HEAD").output() diff --git a/build/wire.rs b/build/wire.rs index e5ca40db..b33f4ad9 100644 --- a/build/wire.rs +++ b/build/wire.rs @@ -417,6 +417,7 @@ fn write_file( let messages = parse_messages(&contents)?; writeln!(f)?; writeln!(f, "pub mod {} {{", obj_name)?; + writeln!(f, " #![allow(dead_code)]")?; writeln!(f, " use super::*;")?; for message in messages.requests.iter().chain(messages.events.iter()) { write_message(f, &camel_obj_name, &message.val)?; diff --git a/algorithms/Cargo.toml b/crates/algorithms/Cargo.toml similarity index 76% rename from algorithms/Cargo.toml rename to crates/algorithms/Cargo.toml index 45d5bf11..257e1d09 100644 --- a/algorithms/Cargo.toml +++ b/crates/algorithms/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "jay-algorithms" -version = "0.4.0" -edition = "2024" -license = "GPL-3.0-only" +version.workspace = true +edition.workspace = true +license.workspace = true description = "Internal dependency of the Jay compositor" repository = "https://github.com/mahkoh/jay" diff --git a/algorithms/src/lib.rs b/crates/algorithms/src/lib.rs similarity index 100% rename from algorithms/src/lib.rs rename to crates/algorithms/src/lib.rs diff --git a/algorithms/src/qoi.rs b/crates/algorithms/src/qoi.rs similarity index 100% rename from algorithms/src/qoi.rs rename to crates/algorithms/src/qoi.rs diff --git a/algorithms/src/rect.rs b/crates/algorithms/src/rect.rs similarity index 100% rename from algorithms/src/rect.rs rename to crates/algorithms/src/rect.rs diff --git a/algorithms/src/rect/region.rs b/crates/algorithms/src/rect/region.rs similarity index 100% rename from algorithms/src/rect/region.rs rename to crates/algorithms/src/rect/region.rs diff --git a/algorithms/src/windows.rs b/crates/algorithms/src/windows.rs similarity index 100% rename from algorithms/src/windows.rs rename to crates/algorithms/src/windows.rs diff --git a/crates/allocator/Cargo.toml b/crates/allocator/Cargo.toml new file mode 100644 index 00000000..d9ab5e43 --- /dev/null +++ b/crates/allocator/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "jay-allocator" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +jay-formats = { path = "../formats" } +jay-video-types = { path = "../video-types" } + +thiserror = "2.0.11" +uapi = "0.2.13" diff --git a/crates/allocator/src/lib.rs b/crates/allocator/src/lib.rs new file mode 100644 index 00000000..a5b86a4f --- /dev/null +++ b/crates/allocator/src/lib.rs @@ -0,0 +1,95 @@ +use { + jay_formats::Format, + jay_video_types::{ + Modifier, + dmabuf::{DmaBuf, DmaBufIds}, + }, + std::{ + error::Error, + ops::{BitOr, BitOrAssign, Not}, + rc::Rc, + }, + thiserror::Error, + uapi::{OwnedFd, c}, +}; + +#[derive(Debug, Error)] +#[error(transparent)] +pub struct AllocatorError(#[from] pub Box); + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct BufferUsage(u32); + +impl BufferUsage { + pub fn none() -> Self { + Self(0) + } + + pub fn contains(self, other: Self) -> bool { + self.0 & other.0 == other.0 + } +} + +impl BitOr for BufferUsage { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } +} + +impl BitOrAssign for BufferUsage { + fn bitor_assign(&mut self, rhs: Self) { + self.0 |= rhs.0; + } +} + +impl Not for BufferUsage { + type Output = Self; + + fn not(self) -> Self::Output { + Self(!self.0) + } +} + +pub const BO_USE_SCANOUT: BufferUsage = BufferUsage(1 << 0); +pub const BO_USE_CURSOR: BufferUsage = BufferUsage(1 << 1); +pub const BO_USE_RENDERING: BufferUsage = BufferUsage(1 << 2); +pub const BO_USE_WRITE: BufferUsage = BufferUsage(1 << 3); +pub const BO_USE_LINEAR: BufferUsage = BufferUsage(1 << 4); +pub const BO_USE_PROTECTED: BufferUsage = BufferUsage(1 << 5); + +pub trait Allocator { + fn drm(&self) -> Option<&dyn AllocatorDrm>; + fn create_bo( + &self, + dma_buf_ids: &DmaBufIds, + width: i32, + height: i32, + format: &'static Format, + modifiers: &[Modifier], + usage: BufferUsage, + ) -> Result, AllocatorError>; + fn import_dmabuf( + &self, + dmabuf: &DmaBuf, + usage: BufferUsage, + ) -> Result, AllocatorError>; +} + +pub trait AllocatorDrm { + fn dev(&self) -> c::dev_t; + fn dup_render_fd(&self) -> Result, AllocatorError>; +} + +pub trait BufferObject { + fn dmabuf(&self) -> &DmaBuf; + fn map_read(self: Rc) -> Result, AllocatorError>; + fn map_write(self: Rc) -> Result, AllocatorError>; +} + +pub trait MappedBuffer { + unsafe fn data(&self) -> &[u8]; + fn data_ptr(&self) -> *mut u8; + fn stride(&self) -> i32; +} diff --git a/crates/async-engine/Cargo.toml b/crates/async-engine/Cargo.toml new file mode 100644 index 00000000..b51d6d68 --- /dev/null +++ b/crates/async-engine/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "jay-async-engine" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +jay-time = { path = "../time" } +jay-tracy = { path = "../tracy" } +jay-utils = { path = "../utils" } + +[features] +it = [] +tracy = ["jay-tracy/tracy"] diff --git a/src/async_engine/ae_task.rs b/crates/async-engine/src/ae_task.rs similarity index 96% rename from src/async_engine/ae_task.rs rename to crates/async-engine/src/ae_task.rs index e980c89c..50647187 100644 --- a/src/async_engine/ae_task.rs +++ b/crates/async-engine/src/ae_task.rs @@ -1,11 +1,9 @@ use { - crate::{ - async_engine::{AsyncEngine, Phase}, - tracy::ZoneName, - utils::{ - numcell::NumCell, - ptr_ext::{MutPtrExt, PtrExt}, - }, + crate::{AsyncEngine, Phase}, + jay_tracy::ZoneName, + jay_utils::{ + numcell::NumCell, + ptr_ext::{MutPtrExt, PtrExt}, }, std::{ cell::{Cell, UnsafeCell}, @@ -142,7 +140,7 @@ impl AsyncEngine { }), waker: Cell::new(None), queue: self.clone(), - zone: create_zone_name!("task:{}", name), + zone: jay_tracy::create_zone_name!("task:{}", name), }); unsafe { f.schedule_run(); @@ -254,7 +252,7 @@ impl> Task { let mut ctx = Context::from_waker(&waker); let poll = { - dynamic_zone!(self.zone); + jay_tracy::dynamic_zone!(self.zone); Pin::new_unchecked(&mut *data.future).poll(&mut ctx) }; if let Poll::Ready(d) = poll { diff --git a/src/async_engine/ae_yield.rs b/crates/async-engine/src/ae_yield.rs similarity index 93% rename from src/async_engine/ae_yield.rs rename to crates/async-engine/src/ae_yield.rs index 7ada5c81..41c561c5 100644 --- a/src/async_engine/ae_yield.rs +++ b/crates/async-engine/src/ae_yield.rs @@ -1,5 +1,5 @@ use { - crate::async_engine::AsyncEngine, + crate::AsyncEngine, std::{ future::Future, pin::Pin, diff --git a/crates/async-engine/src/lib.rs b/crates/async-engine/src/lib.rs new file mode 100644 index 00000000..f482ca2d --- /dev/null +++ b/crates/async-engine/src/lib.rs @@ -0,0 +1,169 @@ +mod ae_task; +mod ae_yield; +mod run_toplevel; + +pub use {ae_task::SpawnedFuture, ae_yield::Yield, run_toplevel::*}; +use { + crate::ae_task::Runnable, + jay_time::Time, + jay_utils::{array, numcell::NumCell, syncqueue::SyncQueue}, + std::{ + cell::{Cell, RefCell}, + collections::VecDeque, + future::Future, + rc::Rc, + task::Waker, + }, +}; + +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum Phase { + EventHandling, + Layout, + PostLayout, + Present, +} +const NUM_PHASES: usize = 4; + +pub struct AsyncEngine { + num_queued: NumCell, + queues: [SyncQueue; NUM_PHASES], + iteration: NumCell, + yields: SyncQueue, + stash: RefCell>, + yield_stash: RefCell>, + stopped: Cell, + now: Cell>, + #[cfg(feature = "it")] + idle: Cell>, +} + +impl AsyncEngine { + pub fn new() -> Rc { + Rc::new(Self { + num_queued: Default::default(), + queues: array::from_fn(|_| Default::default()), + iteration: Default::default(), + yields: Default::default(), + stash: Default::default(), + yield_stash: Default::default(), + stopped: Cell::new(false), + now: Default::default(), + #[cfg(feature = "it")] + idle: Default::default(), + }) + } + + pub fn stop(&self) { + self.stopped.set(true); + } + + pub fn clear(&self) { + self.stash.borrow_mut().clear(); + self.yield_stash.borrow_mut().clear(); + self.yields.take(); + for queue in &self.queues { + queue.take(); + } + } + + pub fn spawn + 'static>( + self: &Rc, + name: &str, + f: F, + ) -> SpawnedFuture { + self.spawn_(name, Phase::EventHandling, f) + } + + pub fn spawn2 + 'static>( + self: &Rc, + name: &str, + phase: Phase, + f: F, + ) -> SpawnedFuture { + self.spawn_(name, phase, f) + } + + pub fn yield_now(self: &Rc) -> Yield { + Yield { + iteration: self.iteration(), + queue: self.clone(), + } + } + + pub fn dispatch(&self) { + let mut stash = self.stash.borrow_mut(); + let mut yield_stash = self.yield_stash.borrow_mut(); + loop { + if self.num_queued.get() == 0 { + #[cfg(feature = "it")] + if let Some(idle) = self.idle.take() { + idle.wake(); + continue; + } + break; + } + self.now.take(); + let mut phase = 0; + while phase < NUM_PHASES { + self.queues[phase].swap(&mut *stash); + if stash.is_empty() { + phase += 1; + continue; + } + self.num_queued.fetch_sub(stash.len()); + while let Some(runnable) = stash.pop_front() { + runnable.run(); + if self.stopped.get() { + return; + } + } + } + self.iteration.fetch_add(1); + self.yields.swap(&mut *yield_stash); + while let Some(waker) = yield_stash.pop_front() { + waker.wake(); + } + } + } + + #[cfg(feature = "it")] + pub async fn idle(&self) { + use std::{future::poll_fn, task::Poll}; + let mut register = true; + poll_fn(|ctx| { + if register { + self.idle.set(Some(ctx.waker().clone())); + register = false; + Poll::Pending + } else { + Poll::Ready(()) + } + }) + .await + } + + fn push(&self, runnable: Runnable, phase: Phase) { + self.queues[phase as usize].push(runnable); + self.num_queued.fetch_add(1); + } + + fn push_yield(&self, waker: Waker) { + self.yields.push(waker); + } + + pub fn iteration(&self) -> u64 { + self.iteration.get() + } + + pub fn now(&self) -> Time { + match self.now.get() { + Some(t) => t, + None => { + let now = Time::now_unchecked(); + self.now.set(Some(now)); + now + } + } + } +} diff --git a/src/utils/run_toplevel.rs b/crates/async-engine/src/run_toplevel.rs similarity index 89% rename from src/utils/run_toplevel.rs rename to crates/async-engine/src/run_toplevel.rs index b0422d32..e089fe4e 100644 --- a/src/utils/run_toplevel.rs +++ b/crates/async-engine/src/run_toplevel.rs @@ -1,8 +1,6 @@ use { - crate::{ - async_engine::{AsyncEngine, SpawnedFuture}, - utils::queue::AsyncQueue, - }, + crate::{AsyncEngine, SpawnedFuture}, + jay_utils::queue::AsyncQueue, std::rc::Rc, }; diff --git a/crates/bufio/Cargo.toml b/crates/bufio/Cargo.toml new file mode 100644 index 00000000..e2c964de --- /dev/null +++ b/crates/bufio/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "jay-bufio" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +jay-io-uring = { path = "../io-uring" } +jay-utils = { path = "../utils" } + +thiserror = "2.0.11" +uapi = "0.2.13" diff --git a/src/utils/bufio.rs b/crates/bufio/src/lib.rs similarity index 96% rename from src/utils/bufio.rs rename to crates/bufio/src/lib.rs index 5cbd5260..879b2be5 100644 --- a/src/utils/bufio.rs +++ b/crates/bufio/src/lib.rs @@ -1,11 +1,9 @@ use { - crate::{ - io_uring::{IoUring, IoUringError}, - utils::{ - buf::{Buf, DynamicBuf}, - queue::AsyncQueue, - stack::Stack, - }, + jay_io_uring::{IoUring, IoUringError}, + jay_utils::{ + buf::{Buf, DynamicBuf}, + queue::AsyncQueue, + stack::Stack, }, std::{ collections::VecDeque, diff --git a/crates/bugs/Cargo.toml b/crates/bugs/Cargo.toml new file mode 100644 index 00000000..ca696b8b --- /dev/null +++ b/crates/bugs/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "jay-bugs" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +ahash = "0.8.7" diff --git a/crates/bugs/src/lib.rs b/crates/bugs/src/lib.rs new file mode 100644 index 00000000..b62b4d37 --- /dev/null +++ b/crates/bugs/src/lib.rs @@ -0,0 +1,38 @@ +use {ahash::AHashMap, std::sync::LazyLock}; + +static BUGS: LazyLock> = LazyLock::new(|| { + let mut map = AHashMap::new(); + map.insert( + "chromium", + Bugs { + respect_min_max_size: true, + ..Default::default() + }, + ); + map.insert( + "Alacritty", + Bugs { + min_width: Some(100), + min_height: Some(100), + ..Default::default() + }, + ); + map +}); + +pub fn get(app_id: &str) -> &'static Bugs { + BUGS.get(app_id).unwrap_or(&NONE) +} + +pub static NONE: Bugs = Bugs { + respect_min_max_size: false, + min_width: None, + min_height: None, +}; + +#[derive(Default, Debug)] +pub struct Bugs { + pub respect_min_max_size: bool, + pub min_width: Option, + pub min_height: Option, +} diff --git a/crates/clientmem/Cargo.toml b/crates/clientmem/Cargo.toml new file mode 100644 index 00000000..c2e61a56 --- /dev/null +++ b/crates/clientmem/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "jay-clientmem" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +jay-cpu-worker = { path = "../cpu-worker" } +jay-gfx-types = { path = "../gfx-types" } +jay-tracy = { path = "../tracy" } +jay-utils = { path = "../utils" } + +log = "0.4.20" +thiserror = "2.0.11" +uapi = "0.2.13" + +[features] +tracy = ["jay-tracy/tracy"] diff --git a/crates/clientmem/src/lib.rs b/crates/clientmem/src/lib.rs new file mode 100644 index 00000000..7b947944 --- /dev/null +++ b/crates/clientmem/src/lib.rs @@ -0,0 +1,331 @@ +use { + jay_cpu_worker::{AsyncCpuWork, CpuJob, CpuWork, CpuWorker}, + jay_gfx_types::{ShmMemory, ShmMemoryBacking}, + jay_utils::{ + oserror::{OsError, OsErrorExt2}, + page_size::page_size, + vec_ext::VecExt, + }, + std::{ + cell::Cell, + error::Error, + mem::{ManuallyDrop, MaybeUninit}, + ops::Deref, + ptr, + rc::Rc, + sync::atomic::{Ordering, compiler_fence}, + }, + thiserror::Error, + uapi::{ + OwnedFd, Pod, + c::{self, raise}, + ftruncate, + }, +}; + +#[derive(Copy, Clone, Debug)] +pub struct ClientMemClient<'a> { + pub comm: &'a str, + pub id: u64, +} + +#[derive(Debug, Error)] +pub enum ClientMemError { + #[error("Could not install the sigbus handler")] + SigactionFailed(#[source] jay_utils::oserror::OsError), + #[error("A SIGBUS occurred while accessing mapped memory")] + Sigbus, + #[error("mmap failed")] + MmapFailed(#[source] jay_utils::oserror::OsError), + #[error("Length was not a multiple of the data element size")] + InvalidLength, +} + +pub struct ClientMem { + fd: ManuallyDrop>, + failed: Cell, + sigbus_impossible: bool, + data: *const [Cell], + cpu: Option>, +} + +#[derive(Clone)] +pub struct ClientMemOffset { + mem: Rc, + offset: usize, + data: *const [Cell], +} + +impl ClientMem { + pub fn new( + fd: &Rc, + len: usize, + read_only: bool, + client: Option>, + cpu: Option<&Rc>, + is_udmabuf: bool, + ) -> Result { + Self::new2(fd, len, read_only, client, cpu, c::MAP_SHARED, is_udmabuf) + } + + pub fn new_private( + fd: &Rc, + len: usize, + read_only: bool, + client: Option>, + cpu: Option<&Rc>, + ) -> Result { + Self::new2(fd, len, read_only, client, cpu, c::MAP_PRIVATE, false) + } + + fn new2( + fd: &Rc, + len: usize, + read_only: bool, + client: Option>, + cpu: Option<&Rc>, + flags: c::c_int, + is_udmabuf: bool, + ) -> Result { + let mut sigbus_impossible = is_udmabuf; + let mut real_size = None; + if !sigbus_impossible + && let Ok(seals) = uapi::fcntl_get_seals(fd.raw()) + && seals & c::F_SEAL_SHRINK != 0 + && let Ok(stat) = uapi::fstat(fd.raw()) + { + real_size = Some(stat.st_size as usize); + sigbus_impossible = stat.st_size as u64 >= len as u64; + } + if !sigbus_impossible && let Some(client) = client { + log::debug!( + "Client {} ({}) has created a shm buffer that might cause SIGBUS", + client.comm, + client.id, + ); + } + let len = len.next_multiple_of(page_size()); + if let Some(real_size) = real_size + && real_size < len + { + let _ = ftruncate(fd.raw(), len as _); + } + let data = if len == 0 { + &mut [][..] + } else { + let prot = match read_only { + true => c::PROT_READ, + false => c::PROT_READ | c::PROT_WRITE, + }; + unsafe { + let data = c::mmap64(ptr::null_mut(), len, prot, flags, fd.raw(), 0); + if data == c::MAP_FAILED { + return Err(ClientMemError::MmapFailed(OsError::default())); + } + std::slice::from_raw_parts_mut(data as *mut Cell, len) + } + }; + Ok(Self { + fd: ManuallyDrop::new(fd.clone()), + failed: Cell::new(false), + sigbus_impossible, + data, + cpu: cpu.cloned(), + }) + } + + pub fn len(&self) -> usize { + self.data.len() + } + + pub fn offset(self: &Rc, offset: usize, len: usize) -> ClientMemOffset { + let mem = unsafe { &*self.data }; + ClientMemOffset { + mem: self.clone(), + offset, + data: &mem[offset..][..len], + } + } + + pub fn fd(&self) -> &Rc { + &self.fd + } + + pub fn is_sealed_memfd(&self) -> bool { + self.sigbus_impossible + } +} + +impl ClientMemOffset { + pub fn pool(&self) -> &ClientMem { + &self.mem + } + + pub fn offset(&self) -> usize { + self.offset + } + + pub fn ptr(&self) -> *const [Cell] { + self.data + } + + pub fn access]) -> T>(&self, f: F) -> Result { + unsafe { + if self.mem.sigbus_impossible { + return Ok(f(&*self.data)); + } + let mref = MemRef { + mem: &*self.mem, + outer: MEM.get(), + }; + MEM.set(&mref); + compiler_fence(Ordering::SeqCst); + let res = f(&*self.data); + MEM.set(mref.outer); + compiler_fence(Ordering::SeqCst); + match self.mem.failed.get() { + true => Err(ClientMemError::Sigbus), + _ => Ok(res), + } + } + } + + pub fn read(&self, dst: &mut Vec) -> Result<(), ClientMemError> { + if self.data.len().checked_rem(std::mem::size_of::()) != Some(0) { + return Err(ClientMemError::InvalidLength); + } + self.access(|v| { + let len_elements = v.len() / std::mem::size_of::(); + dst.reserve(len_elements); + let (_, unused) = dst.split_at_spare_mut_bytes_ext(); + unused[..v.len()].copy_from_slice(uapi::as_maybe_uninit_bytes(v)); + unsafe { + dst.set_len(dst.len() + len_elements); + } + }) + } +} + +impl Drop for ClientMem { + fn drop(&mut self) { + let fd = unsafe { ManuallyDrop::take(&mut self.fd) }; + if let Some(cpu) = &self.cpu { + let pending = cpu.submit(Box::new(CloseMemWork { + fd: Rc::try_unwrap(fd).ok(), + data: self.data, + })); + pending.detach(); + } else { + unsafe { + c::munmap(self.data as _, self.len()); + } + } + } +} + +struct MemRef { + mem: *const ClientMem, + outer: *const MemRef, +} + +thread_local! { + static MEM: Cell<*const MemRef> = const { Cell::new(ptr::null()) }; +} + +unsafe fn kill() -> ! { + unsafe { + c::signal(c::SIGBUS, c::SIG_DFL); + raise(c::SIGBUS); + } + unreachable!(); +} + +unsafe extern "C" fn sigbus(sig: i32, info: &c::siginfo_t, _ucontext: *mut c::c_void) { + unsafe { + assert_eq!(sig, c::SIGBUS); + let mut memr_ptr = MEM.get(); + while !memr_ptr.is_null() { + let memr = &*memr_ptr; + let mem = &*memr.mem; + let lo = mem.data as *mut u8 as usize; + let hi = lo + mem.len(); + let fault_addr = info.si_addr() as usize; + if fault_addr < lo || fault_addr >= hi { + memr_ptr = memr.outer; + continue; + } + let res = c::mmap64( + lo as _, + hi - lo, + c::PROT_WRITE | c::PROT_READ, + c::MAP_ANONYMOUS | c::MAP_PRIVATE | c::MAP_FIXED, + -1, + 0, + ); + if res == c::MAP_FAILED { + kill(); + } + mem.failed.set(true); + return; + } + kill(); + } +} + +pub fn init() -> Result<(), ClientMemError> { + unsafe { + let mut action: c::sigaction = MaybeUninit::zeroed().assume_init(); + action.sa_sigaction = + sigbus as unsafe extern "C" fn(i32, &c::siginfo_t, *mut c::c_void) as _; + action.sa_flags = c::SA_NODEFER | c::SA_SIGINFO; + let res = c::sigaction(c::SIGBUS, &action, ptr::null_mut()); + uapi::map_err!(res) + .map(drop) + .map_os_err(ClientMemError::SigactionFailed) + } +} + +struct CloseMemWork { + fd: Option, + data: *const [Cell], +} + +unsafe impl Send for CloseMemWork {} + +impl CpuJob for CloseMemWork { + fn work(&mut self) -> &mut dyn CpuWork { + self + } + + fn completed(self: Box) { + // nothing + } +} + +impl CpuWork for CloseMemWork { + fn run(&mut self) -> Option> { + jay_tracy::zone!("CloseMemWork"); + self.fd.take(); + unsafe { + c::munmap(self.data as _, self.data.len()); + } + None + } +} + +impl ShmMemory for ClientMemOffset { + fn len(&self) -> usize { + self.data.len() + } + + fn safe_access(&self) -> ShmMemoryBacking { + match self.mem.is_sealed_memfd() { + true => ShmMemoryBacking::Ptr(self.data), + false => ShmMemoryBacking::Fd(self.mem.fd.deref().clone(), self.offset), + } + } + + fn access(&self, f: &mut dyn FnMut(&[Cell])) -> Result<(), Box> { + self.access(f).map_err(|e| e.into()) + } +} diff --git a/crates/cmm/Cargo.toml b/crates/cmm/Cargo.toml new file mode 100644 index 00000000..ade7abfd --- /dev/null +++ b/crates/cmm/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "jay-cmm" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +jay-utils = { path = "../utils" } diff --git a/src/cmm/cmm_description.rs b/crates/cmm/src/cmm_description.rs similarity index 85% rename from src/cmm/cmm_description.rs rename to crates/cmm/src/cmm_description.rs index 85a0a71a..dc8ecbd4 100644 --- a/src/cmm/cmm_description.rs +++ b/crates/cmm/src/cmm_description.rs @@ -1,15 +1,13 @@ use { crate::{ - cmm::{ - cmm_eotf::Eotf, - cmm_luminance::{Luminance, TargetLuminance, white_balance}, - cmm_manager::Shared, - cmm_primaries::{NamedPrimaries, Primaries}, - cmm_render_intent::RenderIntent, - cmm_transform::{ColorMatrix, Local, Xyz, bradford_adjustment}, - }, - utils::ordered_float::F64, + cmm_eotf::Eotf, + cmm_luminance::{Luminance, TargetLuminance, white_balance}, + cmm_manager::Shared, + cmm_primaries::{NamedPrimaries, Primaries}, + cmm_render_intent::RenderIntent, + cmm_transform::{ColorMatrix, Local, Xyz, bradford_adjustment}, }, + jay_utils::ordered_float::F64, std::rc::Rc, }; diff --git a/src/cmm/cmm_eotf.rs b/crates/cmm/src/cmm_eotf.rs similarity index 97% rename from src/cmm/cmm_eotf.rs rename to crates/cmm/src/cmm_eotf.rs index 89e123aa..afc26d0d 100644 --- a/src/cmm/cmm_eotf.rs +++ b/crates/cmm/src/cmm_eotf.rs @@ -1,4 +1,4 @@ -use crate::utils::ordered_float::F32; +use jay_utils::ordered_float::F32; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum Eotf { diff --git a/src/cmm/cmm_luminance.rs b/crates/cmm/src/cmm_luminance.rs similarity index 93% rename from src/cmm/cmm_luminance.rs rename to crates/cmm/src/cmm_luminance.rs index 8371a33d..584120da 100644 --- a/src/cmm/cmm_luminance.rs +++ b/crates/cmm/src/cmm_luminance.rs @@ -1,10 +1,8 @@ use crate::{ - cmm::{ - cmm_render_intent::RenderIntent, - cmm_transform::{ColorMatrix, Xyz}, - }, - utils::ordered_float::F64, + cmm_render_intent::RenderIntent, + cmm_transform::{ColorMatrix, Xyz}, }; +use jay_utils::ordered_float::F64; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct Luminance { @@ -38,7 +36,6 @@ impl Luminance { white: F64(203.0), }; - #[expect(dead_code)] pub const HLG: Self = Self { min: F64(0.005), max: F64(1000.0), diff --git a/src/cmm/cmm_manager.rs b/crates/cmm/src/cmm_manager.rs similarity index 94% rename from src/cmm/cmm_manager.rs rename to crates/cmm/src/cmm_manager.rs index f73e2c12..8826f9b5 100644 --- a/src/cmm/cmm_manager.rs +++ b/crates/cmm/src/cmm_manager.rs @@ -1,16 +1,14 @@ use { crate::{ - cmm::{ - cmm_description::{ - ColorDescription, ColorDescriptionIds, LinearColorDescription, - LinearColorDescriptionId, LinearColorDescriptionIds, - }, - cmm_eotf::Eotf, - cmm_luminance::{Luminance, TargetLuminance}, - cmm_primaries::{NamedPrimaries, Primaries}, + cmm_description::{ + ColorDescription, ColorDescriptionIds, LinearColorDescription, + LinearColorDescriptionId, LinearColorDescriptionIds, }, - utils::{copyhashmap::CopyHashMap, numcell::NumCell, ordered_float::F64}, + cmm_eotf::Eotf, + cmm_luminance::{Luminance, TargetLuminance}, + cmm_primaries::{NamedPrimaries, Primaries}, }, + jay_utils::{copyhashmap::CopyHashMap, numcell::NumCell, ordered_float::F64}, std::rc::{Rc, Weak}, }; diff --git a/src/cmm/cmm_primaries.rs b/crates/cmm/src/cmm_primaries.rs similarity index 98% rename from src/cmm/cmm_primaries.rs rename to crates/cmm/src/cmm_primaries.rs index 5541d8ee..a39f7450 100644 --- a/src/cmm/cmm_primaries.rs +++ b/crates/cmm/src/cmm_primaries.rs @@ -1,4 +1,4 @@ -use {crate::utils::ordered_float::F64, std::hash::Hash}; +use {jay_utils::ordered_float::F64, std::hash::Hash}; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub enum NamedPrimaries { diff --git a/src/cmm/cmm_render_intent.rs b/crates/cmm/src/cmm_render_intent.rs similarity index 50% rename from src/cmm/cmm_render_intent.rs rename to crates/cmm/src/cmm_render_intent.rs index afb16a30..77fbcf51 100644 --- a/src/cmm/cmm_render_intent.rs +++ b/crates/cmm/src/cmm_render_intent.rs @@ -1,11 +1,3 @@ -use crate::{ - ifs::color_management::{ - ABSOLUTE_NO_ADAPTATION_SINCE, RENDER_INTENT_ABSOLUTE_NO_ADAPTATION, - RENDER_INTENT_PERCEPTUAL, RENDER_INTENT_RELATIVE, RENDER_INTENT_RELATIVE_BPC, - }, - object::Version, -}; - #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default)] pub enum RenderIntent { #[default] @@ -16,19 +8,6 @@ pub enum RenderIntent { } impl RenderIntent { - pub fn from_wayland(intent: u32, version: Version) -> Option { - let res = match intent { - RENDER_INTENT_PERCEPTUAL => Self::Perceptual, - RENDER_INTENT_RELATIVE => Self::Relative, - RENDER_INTENT_RELATIVE_BPC => Self::RelativeBpc, - RENDER_INTENT_ABSOLUTE_NO_ADAPTATION if version >= ABSOLUTE_NO_ADAPTATION_SINCE => { - Self::AbsoluteNoAdaptation - } - _ => return None, - }; - Some(res) - } - pub fn black_point_compensation(self) -> bool { match self { RenderIntent::Perceptual => true, diff --git a/src/cmm/cmm_tests.rs b/crates/cmm/src/cmm_tests.rs similarity index 98% rename from src/cmm/cmm_tests.rs rename to crates/cmm/src/cmm_tests.rs index 5f3564df..84e5edd6 100644 --- a/src/cmm/cmm_tests.rs +++ b/crates/cmm/src/cmm_tests.rs @@ -1,5 +1,5 @@ mod matrices { - use crate::{cmm::cmm_primaries::Primaries, utils::ordered_float::F64}; + use {crate::cmm_primaries::Primaries, jay_utils::ordered_float::F64}; fn check(primaries: Primaries, expected: [[f64; 4]; 3]) { let (ltg, gtl) = primaries.matrices(); @@ -134,7 +134,7 @@ mod matrices { } mod transforms { - use crate::cmm::{ + use crate::{ cmm_eotf::Eotf, cmm_luminance::Luminance, cmm_manager::ColorManager, cmm_primaries::Primaries, cmm_render_intent::RenderIntent, }; diff --git a/src/cmm/cmm_transform.rs b/crates/cmm/src/cmm_transform.rs similarity index 90% rename from src/cmm/cmm_transform.rs rename to crates/cmm/src/cmm_transform.rs index 64e576ca..eaab4087 100644 --- a/src/cmm/cmm_transform.rs +++ b/crates/cmm/src/cmm_transform.rs @@ -1,10 +1,6 @@ use { - crate::{ - cmm::{cmm_eotf::Eotf, cmm_primaries::Primaries}, - gfx_api::AlphaMode, - theme::Color, - utils::ordered_float::F64, - }, + crate::cmm_primaries::Primaries, + jay_utils::ordered_float::F64, std::{ fmt, fmt::{Debug, Formatter}, @@ -129,29 +125,6 @@ impl Mul<[f64; 3]> for ColorMatrix { } } -impl Mul for ColorMatrix { - type Output = Color; - - fn mul(self, rhs: Color) -> Self::Output { - let mut rgba = rhs.to_array(Eotf::Linear); - let a = rgba[3]; - if a < 1.0 && a > 0.0 { - for c in &mut rgba[..3] { - *c /= a; - } - } - let [r, g, b] = self * [rgba[0] as f64, rgba[1] as f64, rgba[2] as f64]; - Color::new( - Eotf::Linear, - AlphaMode::Straight, - r as f32, - g as f32, - b as f32, - a, - ) - } -} - impl ColorMatrix { pub const fn new(m: [[f64; 4]; 3]) -> Self { let m = [ diff --git a/crates/cmm/src/lib.rs b/crates/cmm/src/lib.rs new file mode 100644 index 00000000..3bc9bf15 --- /dev/null +++ b/crates/cmm/src/lib.rs @@ -0,0 +1,53 @@ +macro_rules! linear_ids { + ($ids:ident, $id:ident, $ty:ty $(,)?) => { + #[derive(Debug)] + pub struct $ids { + next: jay_utils::numcell::NumCell<$ty>, + } + + impl Default for $ids { + fn default() -> Self { + Self { + next: jay_utils::numcell::NumCell::new(1), + } + } + } + + impl $ids { + pub fn next(&self) -> $id { + $id(self.next.fetch_add(1)) + } + } + + #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] + pub struct $id($ty); + + impl $id { + #[allow(dead_code)] + pub fn raw(&self) -> $ty { + self.0 + } + + #[allow(dead_code)] + pub fn from_raw(id: $ty) -> Self { + Self(id) + } + } + + impl std::fmt::Display for $id { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } + } + }; +} + +pub mod cmm_description; +pub mod cmm_eotf; +pub mod cmm_luminance; +pub mod cmm_manager; +pub mod cmm_primaries; +pub mod cmm_render_intent; +#[cfg(test)] +mod cmm_tests; +pub mod cmm_transform; diff --git a/crates/cpu-worker/Cargo.toml b/crates/cpu-worker/Cargo.toml new file mode 100644 index 00000000..0517a242 --- /dev/null +++ b/crates/cpu-worker/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "jay-cpu-worker" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +jay-async-engine = { path = "../async-engine" } +jay-geometry = { path = "../geometry" } +jay-io-uring = { path = "../io-uring" } +jay-tracy = { path = "../tracy" } +jay-utils = { path = "../utils" } + +log = { version = "0.4.20", features = ["std"] } +parking_lot = "0.12.1" +thiserror = "2.0.11" +uapi = "0.2.13" + +[dev-dependencies] +jay-wheel = { path = "../wheel" } + +[features] +it = [] +tracy = ["jay-tracy/tracy", "jay-async-engine/tracy"] diff --git a/src/cpu_worker/jobs.rs b/crates/cpu-worker/src/jobs.rs similarity index 100% rename from src/cpu_worker/jobs.rs rename to crates/cpu-worker/src/jobs.rs diff --git a/src/cpu_worker/jobs/img_copy.rs b/crates/cpu-worker/src/jobs/img_copy.rs similarity index 92% rename from src/cpu_worker/jobs/img_copy.rs rename to crates/cpu-worker/src/jobs/img_copy.rs index f50a7b25..8162cf27 100644 --- a/src/cpu_worker/jobs/img_copy.rs +++ b/crates/cpu-worker/src/jobs/img_copy.rs @@ -1,8 +1,6 @@ use { - crate::{ - cpu_worker::{AsyncCpuWork, CpuWork}, - rect::Rect, - }, + crate::{AsyncCpuWork, CpuWork}, + jay_geometry::Rect, std::ptr, }; @@ -34,7 +32,7 @@ impl ImgCopyWork { impl CpuWork for ImgCopyWork { fn run(&mut self) -> Option> { - zone!("ImgCopyWork"); + jay_tracy::zone!("ImgCopyWork"); for rect in &self.rects { let mut offset = rect.y1() * self.stride + rect.x1() * self.bpp; if rect.width() == self.width { diff --git a/src/cpu_worker/jobs/read_write.rs b/crates/cpu-worker/src/jobs/read_write.rs similarity index 95% rename from src/cpu_worker/jobs/read_write.rs rename to crates/cpu-worker/src/jobs/read_write.rs index 6ebcfba2..79d8cb29 100644 --- a/src/cpu_worker/jobs/read_write.rs +++ b/crates/cpu-worker/src/jobs/read_write.rs @@ -1,9 +1,7 @@ use { - crate::{ - async_engine::{AsyncEngine, SpawnedFuture}, - cpu_worker::{AsyncCpuWork, CompletedWork, CpuWork, WorkCompletion}, - io_uring::{IoUring, IoUringError, IoUringTaskId}, - }, + crate::{AsyncCpuWork, CompletedWork, CpuWork, WorkCompletion}, + jay_async_engine::{AsyncEngine, SpawnedFuture}, + jay_io_uring::{IoUring, IoUringError, IoUringTaskId}, std::{ any::Any, ptr, diff --git a/crates/cpu-worker/src/lib.rs b/crates/cpu-worker/src/lib.rs new file mode 100644 index 00000000..51045df1 --- /dev/null +++ b/crates/cpu-worker/src/lib.rs @@ -0,0 +1,509 @@ +pub mod jobs; +#[cfg(test)] +mod tests; + +use { + jay_async_engine::{AsyncEngine, SpawnedFuture}, + jay_io_uring::IoUring, + jay_utils::{ + buf::TypedBuf, + copyhashmap::CopyHashMap, + errorfmt::ErrorFmt, + numcell::NumCell, + oserror::{OsError, OsErrorExt2}, + pipe::{Pipe, pipe}, + ptr_ext::MutPtrExt, + queue::AsyncQueue, + stack::Stack, + }, + parking_lot::{Condvar, Mutex}, + std::{ + any::Any, + cell::{Cell, RefCell}, + collections::VecDeque, + mem, + ptr::NonNull, + rc::Rc, + sync::Arc, + thread, + }, + thiserror::Error, + uapi::{OwnedFd, c}, +}; + +pub trait CpuJob { + fn work(&mut self) -> &mut dyn CpuWork; + fn completed(self: Box); +} + +pub trait CpuWork: Send { + fn run(&mut self) -> Option>; + + fn cancel_async(&mut self, ring: &Rc) { + let _ = ring; + unreachable!(); + } + + fn async_work_done(&mut self, work: Box) { + let _ = work; + unreachable!(); + } +} + +pub trait AsyncCpuWork: Any { + fn run( + self: Box, + eng: &Rc, + ring: &Rc, + completion: WorkCompletion, + ) -> SpawnedFuture; +} + +pub struct WorkCompletion { + worker: Rc, + id: CpuJobId, +} + +pub struct CompletedWork(()); + +impl WorkCompletion { + pub fn complete(self, work: Box) -> CompletedWork { + let job = self.worker.async_jobs.remove(&self.id).unwrap(); + unsafe { + job.work.deref_mut().async_work_done(work); + } + self.worker.send_completion(self.id); + CompletedWork(()) + } +} + +pub struct CpuWorker { + data: Rc, + _completions_listener: SpawnedFuture<()>, + _job_enqueuer: SpawnedFuture<()>, +} + +#[must_use] +pub struct PendingJob { + id: CpuJobId, + thread_data: Rc, + job_data: Rc, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] +enum PendingJobState { + #[default] + Waiting, + Abandoned, + Completed, +} + +#[derive(Default)] +struct PendingJobData { + job: Cell>>, + state: Cell, +} + +enum Job { + New { + id: CpuJobId, + work: *mut dyn CpuWork, + }, + Cancel { + id: CpuJobId, + }, +} + +unsafe impl Send for Job {} + +#[derive(Default)] +struct CompletedJobsExchange { + queue: VecDeque, + condvar: Option>, +} + +struct CpuWorkerData { + next: CpuJobIds, + jobs_to_enqueue: AsyncQueue, + new_jobs: Arc>>, + have_new_jobs: Rc, + completed_jobs_remote: Arc>, + completed_jobs_local: RefCell>, + have_completed_jobs: Rc, + pending_jobs: CopyHashMap>, + ring: Rc, + _stop: OwnedFd, + pending_job_data_cache: Stack>, + sync_wake_condvar: Arc, +} + +#[derive(Debug)] +struct CpuJobIds { + next: NumCell, +} + +impl Default for CpuJobIds { + fn default() -> Self { + Self { + next: NumCell::new(1), + } + } +} + +impl CpuJobIds { + fn next(&self) -> CpuJobId { + CpuJobId(self.next.fetch_add(1)) + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] +struct CpuJobId(u64); + +#[derive(Debug, Error)] +pub enum CpuWorkerError { + #[error("Could not create a pipe")] + Pipe(#[source] OsError), + #[error("Could not create an eventfd")] + EventFd(#[source] OsError), + #[error("Could not dup an eventfd")] + Dup(#[source] OsError), +} + +impl PendingJob { + pub fn detach(self) { + match self.job_data.state.get() { + PendingJobState::Waiting => { + self.job_data.state.set(PendingJobState::Abandoned); + } + PendingJobState::Abandoned => { + unreachable!(); + } + PendingJobState::Completed => {} + } + } +} + +impl Drop for CpuWorker { + fn drop(&mut self) { + self.data.do_equeue_jobs(); + if self.data.pending_jobs.is_not_empty() { + log::warn!("CpuWorker dropped with pending jobs. Completed jobs will not be triggered.") + } + } +} + +impl Drop for PendingJob { + fn drop(&mut self) { + match self.job_data.state.get() { + PendingJobState::Waiting => { + log::warn!("PendingJob dropped before completion. Blocking."); + let data = &self.thread_data; + let id = self.id; + self.job_data.state.set(PendingJobState::Abandoned); + data.jobs_to_enqueue.push(Job::Cancel { id }); + data.do_equeue_jobs(); + loop { + data.dispatch_completions(); + if !data.pending_jobs.contains(&id) { + break; + } + let mut remote = data.completed_jobs_remote.lock(); + while remote.queue.is_empty() { + remote.condvar = Some(data.sync_wake_condvar.clone()); + data.sync_wake_condvar.wait(&mut remote); + } + } + } + PendingJobState::Abandoned => {} + PendingJobState::Completed => { + self.thread_data + .pending_job_data_cache + .push(self.job_data.clone()); + } + } + } +} + +impl CpuWorkerData { + fn clear(&self) { + self.jobs_to_enqueue.clear(); + self.new_jobs.lock().clear(); + self.completed_jobs_remote.lock().queue.clear(); + self.completed_jobs_local.borrow_mut().clear(); + self.pending_jobs.clear(); + self.pending_job_data_cache.take(); + } + + async fn wait_for_completions(self: Rc) { + let mut buf = TypedBuf::::new(); + loop { + if let Err(e) = self.ring.read(&self.have_completed_jobs, buf.buf()).await { + log::error!("Could not wait for job completions: {}", ErrorFmt(e)); + return; + } + self.dispatch_completions(); + } + } + + fn dispatch_completions(&self) { + let completions = &mut *self.completed_jobs_local.borrow_mut(); + mem::swap(completions, &mut self.completed_jobs_remote.lock().queue); + while let Some(id) = completions.pop_front() { + let job_data = self.pending_jobs.remove(&id).unwrap(); + let job = job_data.job.take().unwrap(); + let job = unsafe { Box::from_raw(job.as_ptr()) }; + match job_data.state.get() { + PendingJobState::Waiting => { + job_data.state.set(PendingJobState::Completed); + job.completed(); + } + PendingJobState::Abandoned => { + self.pending_job_data_cache.push(job_data); + } + PendingJobState::Completed => { + unreachable!(); + } + } + } + } + + async fn equeue_jobs(self: Rc) { + loop { + self.jobs_to_enqueue.non_empty().await; + self.do_equeue_jobs(); + } + } + + fn do_equeue_jobs(&self) { + self.jobs_to_enqueue.move_to(&mut self.new_jobs.lock()); + if let Err(e) = uapi::eventfd_write(self.have_new_jobs.raw(), 1) { + panic!("Could not signal eventfd: {}", ErrorFmt(e)); + } + } +} + +impl CpuWorker { + pub fn new(ring: &Rc, eng: &Rc) -> Result { + let new_jobs: Arc>> = Default::default(); + let completed_jobs: Arc> = Default::default(); + let Pipe { + read: stop_read, + write: stop_write, + } = pipe().map_err(CpuWorkerError::Pipe)?; + let have_new_jobs = uapi::eventfd(0, c::EFD_CLOEXEC).map_os_err(CpuWorkerError::EventFd)?; + let have_completed_jobs = + uapi::eventfd(0, c::EFD_CLOEXEC).map_os_err(CpuWorkerError::EventFd)?; + thread::Builder::new() + .name("cpu worker".to_string()) + .spawn({ + let new_jobs = new_jobs.clone(); + let completed_jobs = completed_jobs.clone(); + let have_new_jobs = uapi::fcntl_dupfd_cloexec(have_new_jobs.raw(), 0) + .map_os_err(CpuWorkerError::Dup)?; + let have_completed_jobs = uapi::fcntl_dupfd_cloexec(have_completed_jobs.raw(), 0) + .map_os_err(CpuWorkerError::Dup)?; + move || { + work( + new_jobs, + completed_jobs, + stop_write, + have_new_jobs, + have_completed_jobs, + ) + } + }) + .unwrap(); + let data = Rc::new(CpuWorkerData { + next: Default::default(), + jobs_to_enqueue: Default::default(), + new_jobs, + have_new_jobs: Rc::new(have_new_jobs), + completed_jobs_remote: completed_jobs, + completed_jobs_local: Default::default(), + have_completed_jobs: Rc::new(have_completed_jobs), + pending_jobs: Default::default(), + ring: ring.clone(), + _stop: stop_read, + pending_job_data_cache: Default::default(), + sync_wake_condvar: Arc::new(Condvar::new()), + }); + Ok(Self { + _completions_listener: eng.spawn( + "cpu worker completions", + data.clone().wait_for_completions(), + ), + _job_enqueuer: eng.spawn("cpu worker enqueue", data.clone().equeue_jobs()), + data, + }) + } + + pub fn clear(&self) { + self.data.clear(); + } + + pub fn submit(&self, job: Box) -> PendingJob { + let mut job = NonNull::from(Box::leak(job)); + let id = self.data.next.next(); + self.data.jobs_to_enqueue.push(Job::New { + id, + work: unsafe { job.as_mut().work() }, + }); + let job_data = self.data.pending_job_data_cache.pop().unwrap_or_default(); + job_data.job.set(Some(job)); + job_data.state.set(PendingJobState::Waiting); + self.data.pending_jobs.set(id, job_data.clone()); + PendingJob { + id, + thread_data: self.data.clone(), + job_data, + } + } + + #[cfg(feature = "it")] + pub fn wait_idle(&self) -> bool { + let was_idle = self.data.pending_jobs.is_empty(); + loop { + self.data.dispatch_completions(); + if self.data.pending_jobs.is_empty() { + break; + } + let mut remote = self.data.completed_jobs_remote.lock(); + while remote.queue.is_empty() { + remote.condvar = Some(self.data.sync_wake_condvar.clone()); + self.data.sync_wake_condvar.wait(&mut remote); + } + } + was_idle + } +} + +fn work( + new_jobs: Arc>>, + completed_jobs: Arc>, + stop: OwnedFd, + have_new_jobs: OwnedFd, + have_completed_jobs: OwnedFd, +) { + let eng = AsyncEngine::new(); + let ring = IoUring::new(&eng, 32).unwrap(); + let worker = Rc::new(Worker { + eng, + ring, + completed_jobs, + have_completed_jobs, + async_jobs: Default::default(), + stopped: Cell::new(false), + }); + let _stop_listener = worker + .eng + .spawn("stop listener", worker.clone().handle_stop(stop)); + let _new_job_listener = worker.eng.spawn( + "new job listener", + worker.clone().handle_new_jobs(new_jobs, have_new_jobs), + ); + if let Err(e) = worker.ring.run() { + panic!("io_uring failed: {}", ErrorFmt(e)); + } +} + +struct Worker { + eng: Rc, + ring: Rc, + completed_jobs: Arc>, + have_completed_jobs: OwnedFd, + async_jobs: CopyHashMap, + stopped: Cell, +} + +struct AsyncJob { + _future: SpawnedFuture, + work: *mut dyn CpuWork, +} + +impl Worker { + async fn handle_stop(self: Rc, stop: OwnedFd) { + let stop = Rc::new(stop); + if let Err(e) = self.ring.poll(&stop, 0).await { + log::error!( + "Could not wait for stop fd to become readable: {}", + ErrorFmt(e) + ); + } else { + assert!(self.async_jobs.is_empty()); + self.stopped.set(true); + self.ring.stop(); + } + } + + async fn handle_new_jobs( + self: Rc, + jobs_remote: Arc>>, + new_jobs: OwnedFd, + ) { + let mut buf = TypedBuf::::new(); + let new_jobs = Rc::new(new_jobs); + let mut jobs = VecDeque::new(); + loop { + if let Err(e) = self.ring.read(&new_jobs, buf.buf()).await { + if self.stopped.get() { + return; + } + panic!( + "Could not wait for new jobs fd to be signaled: {}", + ErrorFmt(e), + ); + } + mem::swap(&mut jobs, &mut *jobs_remote.lock()); + while let Some(job) = jobs.pop_front() { + self.handle_new_job(job); + } + } + } + + fn handle_new_job(self: &Rc, job: Job) { + match job { + Job::Cancel { id } => { + let mut jobs = self.async_jobs.lock(); + if let Some(job) = jobs.get_mut(&id) { + unsafe { + job.work.deref_mut().cancel_async(&self.ring); + } + } + } + Job::New { id, work } => match unsafe { work.deref_mut() }.run() { + None => { + self.send_completion(id); + return; + } + Some(w) => { + let completion = WorkCompletion { + worker: self.clone(), + id, + }; + let future = w.run(&self.eng, &self.ring, completion); + self.async_jobs.set( + id, + AsyncJob { + _future: future, + work, + }, + ); + } + }, + } + } + + fn send_completion(&self, id: CpuJobId) { + let cv = { + let mut exchange = self.completed_jobs.lock(); + exchange.queue.push_back(id); + exchange.condvar.take() + }; + if let Some(cv) = cv { + cv.notify_all(); + } + if let Err(e) = uapi::eventfd_write(self.have_completed_jobs.raw(), 1) { + panic!("Could not signal job completion: {}", ErrorFmt(e)); + } + } +} diff --git a/src/cpu_worker/tests.rs b/crates/cpu-worker/src/tests.rs similarity index 90% rename from src/cpu_worker/tests.rs rename to crates/cpu-worker/src/tests.rs index c77a8620..00382e03 100644 --- a/src/cpu_worker/tests.rs +++ b/crates/cpu-worker/src/tests.rs @@ -1,11 +1,9 @@ use { - crate::{ - async_engine::{AsyncEngine, SpawnedFuture}, - cpu_worker::{AsyncCpuWork, CompletedWork, CpuJob, CpuWork, CpuWorker, WorkCompletion}, - io_uring::IoUring, - utils::asyncevent::AsyncEvent, - wheel::Wheel, - }, + crate::{AsyncCpuWork, CompletedWork, CpuJob, CpuWork, CpuWorker, WorkCompletion}, + jay_async_engine::{AsyncEngine, SpawnedFuture}, + jay_io_uring::IoUring, + jay_utils::asyncevent::AsyncEvent, + jay_wheel::Wheel, std::{future::pending, rc::Rc, sync::Arc}, uapi::{OwnedFd, c::EFD_CLOEXEC}, }; diff --git a/crates/criteria/Cargo.toml b/crates/criteria/Cargo.toml new file mode 100644 index 00000000..20af59c2 --- /dev/null +++ b/crates/criteria/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "jay-criteria" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +jay-utils = { path = "../utils" } + +ahash = "0.8.7" +linearize = { version = "0.1.3", features = ["derive"] } +regex = "1.11.1" diff --git a/src/criteria/crit_graph.rs b/crates/criteria/src/crit_graph.rs similarity index 100% rename from src/criteria/crit_graph.rs rename to crates/criteria/src/crit_graph.rs diff --git a/src/criteria/crit_graph/crit_downstream.rs b/crates/criteria/src/crit_graph/crit_downstream.rs similarity index 98% rename from src/criteria/crit_graph/crit_downstream.rs rename to crates/criteria/src/crit_graph/crit_downstream.rs index cf3f50be..939661f8 100644 --- a/src/criteria/crit_graph/crit_downstream.rs +++ b/crates/criteria/src/crit_graph/crit_downstream.rs @@ -1,5 +1,5 @@ use { - crate::criteria::{ + crate::{ CritMatcherId, crit_graph::{CritTarget, crit_upstream::CritUpstreamNode}, }, diff --git a/src/criteria/crit_graph/crit_middle.rs b/crates/criteria/src/crit_graph/crit_middle.rs similarity index 99% rename from src/criteria/crit_graph/crit_middle.rs rename to crates/criteria/src/crit_graph/crit_middle.rs index f8e76041..924c7010 100644 --- a/src/criteria/crit_graph/crit_middle.rs +++ b/crates/criteria/src/crit_graph/crit_middle.rs @@ -1,5 +1,5 @@ use { - crate::criteria::{ + crate::{ CritUpstreamNode, crit_graph::{ CritDownstream, CritDownstreamData, CritTarget, CritUpstreamData, diff --git a/src/criteria/crit_graph/crit_root.rs b/crates/criteria/src/crit_graph/crit_root.rs similarity index 99% rename from src/criteria/crit_graph/crit_root.rs rename to crates/criteria/src/crit_graph/crit_root.rs index 3c2c6f4c..8c6f2c2a 100644 --- a/src/criteria/crit_graph/crit_root.rs +++ b/crates/criteria/src/crit_graph/crit_root.rs @@ -1,5 +1,5 @@ use { - crate::criteria::{ + crate::{ CritMatcherId, CritUpstreamNode, FixedRootMatcher, RootMatcherMap, crit_graph::{ CritTarget, CritUpstreamData, diff --git a/src/criteria/crit_graph/crit_target.rs b/crates/criteria/src/crit_graph/crit_target.rs similarity index 82% rename from src/criteria/crit_graph/crit_target.rs rename to crates/criteria/src/crit_graph/crit_target.rs index 18974748..b77d6796 100644 --- a/src/criteria/crit_graph/crit_target.rs +++ b/crates/criteria/src/crit_graph/crit_target.rs @@ -1,11 +1,9 @@ use { crate::{ - criteria::{ - CritDestroyListener, CritMatcherId, FixedRootMatcher, crit_leaf::CritLeafEvent, - crit_matchers::critm_constant::CritMatchConstant, - }, - utils::{copyhashmap::CopyHashMap, queue::AsyncQueue}, + CritDestroyListener, CritMatcherId, FixedRootMatcher, crit_leaf::CritLeafEvent, + crit_matchers::critm_constant::CritMatchConstant, }, + jay_utils::{copyhashmap::CopyHashMap, queue::AsyncQueue}, std::{ hash::Hash, rc::{Rc, Weak}, diff --git a/src/criteria/crit_graph/crit_upstream.rs b/crates/criteria/src/crit_graph/crit_upstream.rs similarity index 92% rename from src/criteria/crit_graph/crit_upstream.rs rename to crates/criteria/src/crit_graph/crit_upstream.rs index 096a5555..e7bded8e 100644 --- a/src/criteria/crit_graph/crit_upstream.rs +++ b/crates/criteria/src/crit_graph/crit_upstream.rs @@ -1,16 +1,14 @@ use { crate::{ - criteria::{ - CritDestroyListener, CritMatcherId, - crit_graph::{ - WeakCritTargetOwner, - crit_downstream::CritDownstream, - crit_target::{CritTarget, CritTargetOwner}, - }, - crit_per_target_data::CritPerTargetData, + CritDestroyListener, CritMatcherId, + crit_graph::{ + WeakCritTargetOwner, + crit_downstream::CritDownstream, + crit_target::{CritTarget, CritTargetOwner}, }, - utils::copyhashmap::CopyHashMap, + crit_per_target_data::CritPerTargetData, }, + jay_utils::copyhashmap::CopyHashMap, std::{ cell::RefMut, mem, diff --git a/src/criteria/crit_leaf.rs b/crates/criteria/src/crit_leaf.rs similarity index 91% rename from src/criteria/crit_leaf.rs rename to crates/criteria/src/crit_leaf.rs index 72b74ccd..9e22ba47 100644 --- a/src/criteria/crit_leaf.rs +++ b/crates/criteria/src/crit_leaf.rs @@ -1,12 +1,10 @@ use { crate::{ - criteria::{ - CritUpstreamNode, - crit_graph::{CritDownstream, CritDownstreamData, CritMgr, CritTarget}, - crit_per_target_data::{CritDestroyListenerBase, CritPerTargetData}, - }, - utils::{cell_ext::CellExt, queue::AsyncQueue}, + CritUpstreamNode, + crit_graph::{CritDownstream, CritDownstreamData, CritMgr, CritTarget}, + crit_per_target_data::{CritDestroyListenerBase, CritPerTargetData}, }, + jay_utils::{cell_ext::CellExt, queue::AsyncQueue}, std::{ cell::Cell, rc::{Rc, Weak}, @@ -24,7 +22,7 @@ where events: Rc>>, } -pub(in crate::criteria) struct NodeHolder +pub struct NodeHolder where Target: CritTarget, { @@ -77,7 +75,7 @@ impl CritLeafMatcher where Target: CritTarget, { - pub(in crate::criteria) fn new( + pub(crate) fn new( mgr: &Target::Mgr, upstream: &Rc>, on_match: impl Fn(Target::LeafData) -> Box + 'static, diff --git a/src/criteria/crit_matchers.rs b/crates/criteria/src/crit_matchers.rs similarity index 100% rename from src/criteria/crit_matchers.rs rename to crates/criteria/src/crit_matchers.rs diff --git a/src/criteria/crit_matchers/critm_any_or_all.rs b/crates/criteria/src/crit_matchers/critm_any_or_all.rs similarity index 94% rename from src/criteria/crit_matchers/critm_any_or_all.rs rename to crates/criteria/src/crit_matchers/critm_any_or_all.rs index 38c3eaaa..71bc8efe 100644 --- a/src/criteria/crit_matchers/critm_any_or_all.rs +++ b/crates/criteria/src/crit_matchers/critm_any_or_all.rs @@ -1,5 +1,5 @@ use { - crate::criteria::crit_graph::{CritMiddleCriterion, CritTarget, CritUpstreamNode}, + crate::crit_graph::{CritMiddleCriterion, CritTarget, CritUpstreamNode}, std::{marker::PhantomData, rc::Rc}, }; diff --git a/src/criteria/crit_matchers/critm_constant.rs b/crates/criteria/src/crit_matchers/critm_constant.rs similarity index 98% rename from src/criteria/crit_matchers/critm_constant.rs rename to crates/criteria/src/crit_matchers/critm_constant.rs index b45eea19..41d2cda3 100644 --- a/src/criteria/crit_matchers/critm_constant.rs +++ b/crates/criteria/src/crit_matchers/critm_constant.rs @@ -1,5 +1,5 @@ use { - crate::criteria::{ + crate::{ CritMatcherIds, FixedRootMatcher, crit_graph::{ CritFixedRootCriterion, CritFixedRootCriterionBase, CritMgr, CritRoot, CritRootFixed, diff --git a/src/criteria/crit_matchers/critm_exactly.rs b/crates/criteria/src/crit_matchers/critm_exactly.rs similarity index 93% rename from src/criteria/crit_matchers/critm_exactly.rs rename to crates/criteria/src/crit_matchers/critm_exactly.rs index fe4c3e0a..883cb14a 100644 --- a/src/criteria/crit_matchers/critm_exactly.rs +++ b/crates/criteria/src/crit_matchers/critm_exactly.rs @@ -1,5 +1,5 @@ use { - crate::criteria::crit_graph::{CritMiddleCriterion, CritTarget, CritUpstreamNode}, + crate::crit_graph::{CritMiddleCriterion, CritTarget, CritUpstreamNode}, std::{marker::PhantomData, rc::Rc}, }; diff --git a/src/criteria/crit_matchers/critm_string.rs b/crates/criteria/src/crit_matchers/critm_string.rs similarity index 97% rename from src/criteria/crit_matchers/critm_string.rs rename to crates/criteria/src/crit_matchers/critm_string.rs index 1464e2d6..968ceaff 100644 --- a/src/criteria/crit_matchers/critm_string.rs +++ b/crates/criteria/src/crit_matchers/critm_string.rs @@ -1,5 +1,5 @@ use { - crate::criteria::{ + crate::{ CritLiteralOrRegex, RootMatcherMap, crit_graph::{CritRootCriterion, CritTarget}, }, diff --git a/src/criteria/crit_per_target_data.rs b/crates/criteria/src/crit_per_target_data.rs similarity index 97% rename from src/criteria/crit_per_target_data.rs rename to crates/criteria/src/crit_per_target_data.rs index cf527167..26db1c2b 100644 --- a/src/criteria/crit_per_target_data.rs +++ b/crates/criteria/src/crit_per_target_data.rs @@ -1,5 +1,5 @@ use { - crate::criteria::{ + crate::{ CritMatcherId, crit_graph::{CritTarget, CritTargetOwner, WeakCritTargetOwner}, }, @@ -28,7 +28,7 @@ where data: T, } -pub(super) trait CritDestroyListenerBase: 'static +pub trait CritDestroyListenerBase: 'static where Target: CritTarget, { diff --git a/crates/criteria/src/lib.rs b/crates/criteria/src/lib.rs new file mode 100644 index 00000000..f5369669 --- /dev/null +++ b/crates/criteria/src/lib.rs @@ -0,0 +1,131 @@ +pub mod crit_graph; +pub mod crit_leaf; +pub mod crit_matchers; +pub mod crit_per_target_data; + +use { + crate::{ + crit_graph::{CritMgr, CritMiddle, CritRoot, CritRootCriterion, CritRootFixed}, + crit_leaf::CritLeafMatcher, + crit_matchers::{critm_any_or_all::CritMatchAnyOrAll, critm_exactly::CritMatchExactly}, + }, + jay_utils::{copyhashmap::CopyHashMap, numcell::NumCell}, + linearize::StaticMap, + regex::Regex, + std::rc::{Rc, Weak}, +}; +pub use { + crit_graph::{CritTarget, CritUpstreamNode}, + crit_per_target_data::CritDestroyListener, +}; + +#[derive(Debug)] +pub struct CritMatcherIds { + next: NumCell, +} + +impl Default for CritMatcherIds { + fn default() -> Self { + Self { + next: NumCell::new(1), + } + } +} + +impl CritMatcherIds { + pub fn next(&self) -> CritMatcherId { + CritMatcherId(self.next.fetch_add(1)) + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct CritMatcherId(u64); + +impl CritMatcherId { + #[allow(clippy::allow_attributes, dead_code)] + pub fn raw(&self) -> u64 { + self.0 + } + + #[allow(clippy::allow_attributes, dead_code)] + pub fn from_raw(id: u64) -> Self { + Self(id) + } +} + +impl std::fmt::Display for CritMatcherId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } +} + +pub type RootMatcherMap = CopyHashMap>>; +pub type FixedRootMatcher = + StaticMap>>>; + +#[derive(Clone)] +pub enum CritLiteralOrRegex { + Literal(String), + Regex(Regex), +} + +impl CritLiteralOrRegex { + fn matches(&self, string: &str) -> bool { + match self { + CritLiteralOrRegex::Literal(p) => string == p, + CritLiteralOrRegex::Regex(r) => r.is_match(string), + } + } +} + +pub trait CritMgrExt: CritMgr { + fn list( + &self, + upstream: &[Rc>], + all: bool, + ) -> Rc> { + if upstream.is_empty() { + return self.match_constant()[all].clone(); + } + CritMiddle::new(self, upstream, CritMatchAnyOrAll::new(upstream, all)) + } + + fn exactly( + &self, + upstream: &[Rc>], + num: usize, + ) -> Rc> { + if num > upstream.len() { + return self.match_constant()[false].clone(); + } + if num == 0 { + let upstream: Vec<_> = upstream.iter().map(|u| u.not(self)).collect(); + return self.list(&upstream, true); + } + CritMiddle::new(self, upstream, CritMatchExactly::new(upstream, num)) + } + + fn leaf( + &self, + upstream: &Rc>, + on_match: impl Fn(::LeafData) -> Box + 'static, + ) -> Rc> { + CritLeafMatcher::new(self, upstream, on_match) + } + + fn not( + &self, + upstream: &Rc>, + ) -> Rc> { + upstream.not(self) + } + + fn root(&self, criterion: T) -> Rc> + where + T: CritRootCriterion, + { + CritRoot::new(self.roots(), self.id(), criterion) + } +} + +impl CritMgrExt for T where T: CritMgr {} diff --git a/crates/damage/Cargo.toml b/crates/damage/Cargo.toml new file mode 100644 index 00000000..9114f528 --- /dev/null +++ b/crates/damage/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "jay-damage" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +jay-geometry = { path = "../geometry" } +jay-tree-types = { path = "../tree-types" } +jay-units = { path = "../units" } diff --git a/crates/damage/src/lib.rs b/crates/damage/src/lib.rs new file mode 100644 index 00000000..78bb1e60 --- /dev/null +++ b/crates/damage/src/lib.rs @@ -0,0 +1,116 @@ +use { + jay_geometry::Rect, + jay_tree_types::Transform, + jay_units::fixed::Fixed, +}; + +#[derive(Copy, Clone, Debug)] +pub struct DamageMatrix { + transform: Transform, + mx: f64, + my: f64, + dx: f64, + dy: f64, + smear: i32, +} + +impl Default for DamageMatrix { + fn default() -> Self { + Self { + transform: Default::default(), + mx: 1.0, + my: 1.0, + dx: 0.0, + dy: 0.0, + smear: 0, + } + } +} + +impl DamageMatrix { + pub fn apply(&self, dx: i32, dy: i32, rect: Rect) -> Rect { + let x1 = rect.x1() - self.smear; + let x2 = rect.x2() + self.smear; + let y1 = rect.y1() - self.smear; + let y2 = rect.y2() + self.smear; + let [x1, y1, x2, y2] = match self.transform { + Transform::None => [x1, y1, x2, y2], + Transform::Rotate90 => [-y2, x1, -y1, x2], + Transform::Rotate180 => [-x2, -y2, -x1, -y1], + Transform::Rotate270 => [y1, -x2, y2, -x1], + Transform::Flip => [-x2, y1, -x1, y2], + Transform::FlipRotate90 => [y1, x1, y2, x2], + Transform::FlipRotate180 => [x1, -y2, x2, -y1], + Transform::FlipRotate270 => [-y2, -x2, -y1, -x1], + }; + let x1 = (x1 as f64 * self.mx + self.dx).floor() as i32 + dx; + let y1 = (y1 as f64 * self.my + self.dy).floor() as i32 + dy; + let x2 = (x2 as f64 * self.mx + self.dx).ceil() as i32 + dx; + let y2 = (y2 as f64 * self.my + self.dy).ceil() as i32 + dy; + Rect::new_saturating(x1, y1, x2, y2) + } + + pub fn new( + transform: Transform, + legacy_scale: i32, + buffer_width: i32, + buffer_height: i32, + viewport: Option<[Fixed; 4]>, + dst_width: i32, + dst_height: i32, + ) -> DamageMatrix { + let mut buffer_width = buffer_width as f64; + let mut buffer_height = buffer_height as f64; + let dst_width = dst_width as f64; + let dst_height = dst_height as f64; + + let mut mx = 1.0; + let mut my = 1.0; + if legacy_scale != 1 { + let scale_inv = 1.0 / (legacy_scale as f64); + mx = scale_inv; + my = scale_inv; + buffer_width *= scale_inv; + buffer_height *= scale_inv; + } + let (mut buffer_width, mut buffer_height) = + transform.maybe_swap((buffer_width, buffer_height)); + let (mut dx, mut dy) = match transform { + Transform::None => (0.0, 0.0), + Transform::Rotate90 => (buffer_width, 0.0), + Transform::Rotate180 => (buffer_width, buffer_height), + Transform::Rotate270 => (0.0, buffer_height), + Transform::Flip => (buffer_width, 0.0), + Transform::FlipRotate90 => (0.0, 0.0), + Transform::FlipRotate180 => (0.0, buffer_height), + Transform::FlipRotate270 => (buffer_width, buffer_height), + }; + if let Some([x, y, w, h]) = viewport { + dx -= x.to_f64(); + dy -= y.to_f64(); + buffer_width = w.to_f64(); + buffer_height = h.to_f64(); + } + let mut smear = false; + if dst_width != buffer_width { + let scale = dst_width / buffer_width; + mx *= scale; + dx *= scale; + smear |= dst_width > buffer_width; + } + if dst_height != buffer_height { + let scale = dst_height / buffer_height; + my *= scale; + dy *= scale; + smear |= dst_height > buffer_height; + } + DamageMatrix { + transform, + mx, + my, + dx, + dy, + smear: smear as _, + } + } +} diff --git a/crates/dbus-core/Cargo.toml b/crates/dbus-core/Cargo.toml new file mode 100644 index 00000000..8122e63a --- /dev/null +++ b/crates/dbus-core/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "jay-dbus-core" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +jay-bufio = { path = "../bufio" } +jay-io-uring = { path = "../io-uring" } +jay-utils = { path = "../utils" } + +bstr = { version = "1.9.0", default-features = false, features = ["std"] } +thiserror = "2.0.11" +uapi = "0.2.13" diff --git a/src/dbus/dynamic_type.rs b/crates/dbus-core/src/dynamic_type.rs similarity index 95% rename from src/dbus/dynamic_type.rs rename to crates/dbus-core/src/dynamic_type.rs index 765da57f..14a020b5 100644 --- a/src/dbus/dynamic_type.rs +++ b/crates/dbus-core/src/dynamic_type.rs @@ -3,10 +3,8 @@ use { TY_ARRAY, TY_BOOLEAN, TY_BYTE, TY_DOUBLE, TY_INT16, TY_INT32, TY_INT64, TY_OBJECT_PATH, TY_SIGNATURE, TY_STRING, TY_UINT16, TY_UINT32, TY_UINT64, TY_UNIX_FD, TY_VARIANT, }, - crate::{ - dbus::{DbusError, DynamicType, Parser, types::Variant}, - utils::buf::DynamicBuf, - }, + crate::{types::Variant, DbusError, DynamicType, Parser}, + jay_utils::buf::DynamicBuf, std::ops::Deref, }; @@ -156,11 +154,8 @@ impl DynamicType { } let mut vals = vec![]; { - let mut parser = Parser { - buf: &parser.buf[..parser.pos + len], - pos: parser.pos, - fds: parser.fds, - }; + let mut parser = + Parser::new_at(&parser.buf[..parser.pos + len], parser.pos, parser.fds); while !parser.eof() { vals.push(el.parse(&mut parser)?); } diff --git a/src/dbus/formatter.rs b/crates/dbus-core/src/formatter.rs similarity index 97% rename from src/dbus/formatter.rs rename to crates/dbus-core/src/formatter.rs index 798332e2..edf39bba 100644 --- a/src/dbus/formatter.rs +++ b/crates/dbus-core/src/formatter.rs @@ -1,8 +1,6 @@ use { - crate::{ - dbus::{DbusType, Formatter, types::Variant}, - utils::buf::DynamicBuf, - }, + crate::{types::Variant, DbusType, Formatter}, + jay_utils::buf::DynamicBuf, std::rc::Rc, uapi::{OwnedFd, Packed}, }; diff --git a/crates/dbus-core/src/lib.rs b/crates/dbus-core/src/lib.rs new file mode 100644 index 00000000..c75d05a1 --- /dev/null +++ b/crates/dbus-core/src/lib.rs @@ -0,0 +1,232 @@ +pub use {property::{Get, GetReply}, types::*}; +use { + jay_bufio::BufIoError, + jay_io_uring::IoUringError, + jay_utils::{buf::DynamicBuf, oserror::OsError}, + std::{borrow::Cow, fmt::Display, rc::Rc}, + thiserror::Error, + uapi::OwnedFd, +}; + +mod dynamic_type; +mod formatter; +mod parser; +pub mod property; +pub mod types; + +#[derive(Debug)] +pub struct CallError { + pub name: String, + pub msg: Option, +} + +impl Display for CallError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(msg) = &self.msg { + write!(f, "{}: {}", self.name, msg) + } else { + write!(f, "{}", self.name) + } + } +} + +#[derive(Debug, Error)] +pub enum DbusError { + #[error("Encountered an unknown type in a signature")] + UnknownType, + #[error("Function call reply does not contain a reply serial")] + NoReplySerial, + #[error("Signal message contains no interface or member or path")] + MissingSignalHeaders, + #[error("Method call message contains no interface or member or path")] + MissingMethodCallHeaders, + #[error("Error has no error name")] + NoErrorName, + #[error("The socket was killed")] + Killed, + #[error("{0}")] + CallError(CallError), + #[error("FD index is out of bounds")] + OobFds, + #[error("Variant has an invalid type")] + InvalidVariantType, + #[error("Could not create a socket")] + Socket(#[source] OsError), + #[error("Could not connect")] + Connect(#[source] IoUringError), + #[error("Could not write to the dbus socket")] + WriteError(#[source] IoUringError), + #[error("Could not read from the dbus socket")] + ReadError(#[source] IoUringError), + #[error("timeout")] + IoUringError(#[source] Box), + #[error("Server did not send auth challenge")] + NoChallenge, + #[error("Server did not accept our authentication")] + Auth, + #[error("Array length is not a multiple of the element size")] + PodArrayLength, + #[error("Peer did not send enough fds")] + TooFewFds, + #[error("Variant signature is not a single type")] + TrailingVariantSignature, + #[error("Dict signature does not contain a terminating '}}'")] + UnterminatedDict, + #[error("Struct signature does not contain a terminating '}}'")] + UnterminatedStruct, + #[error("Dict signature contains trailing types")] + DictTrailing, + #[error("String does not contain valid UTF-8")] + InvalidUtf8, + #[error("Unexpected end of message")] + UnexpectedEof, + #[error("Boolean value was not 0 or 1")] + InvalidBoolValue, + #[error("Signature is empty")] + EmptySignature, + #[error("The session bus address is not set")] + SessionBusAddressNotSet, + #[error("Server does not support FD passing")] + UnixFd, + #[error("Server message has a different endianess than ourselves")] + InvalidEndianess, + #[error("Server speaks an unexpected protocol version")] + InvalidProtocol, + #[error("Signature contains an invalid type")] + InvalidSignatureType, + #[error("The signal already has a handler")] + AlreadyHandled, + #[error(transparent)] + BufIoError(#[from] BufIoError), + #[error(transparent)] + DbusError(Rc), +} + +impl From for DbusError { + fn from(e: IoUringError) -> Self { + DbusError::IoUringError(Box::new(e)) + } +} + +const TY_BYTE: u8 = b'y'; +const TY_BOOLEAN: u8 = b'b'; +const TY_INT16: u8 = b'n'; +const TY_UINT16: u8 = b'q'; +const TY_INT32: u8 = b'i'; +const TY_UINT32: u8 = b'u'; +const TY_INT64: u8 = b'x'; +const TY_UINT64: u8 = b't'; +const TY_DOUBLE: u8 = b'd'; +const TY_STRING: u8 = b's'; +const TY_OBJECT_PATH: u8 = b'o'; +const TY_SIGNATURE: u8 = b'g'; +const TY_ARRAY: u8 = b'a'; +const TY_VARIANT: u8 = b'v'; +const TY_UNIX_FD: u8 = b'h'; + +#[derive(Clone, Debug)] +pub enum DynamicType { + U8, + Bool, + I16, + U16, + I32, + U32, + I64, + U64, + F64, + String, + ObjectPath, + Signature, + Variant, + Fd, + Array(Box), + DictEntry(Box, Box), + Struct(Vec), +} + +pub struct Parser<'a> { + pub(crate) buf: &'a [u8], + pub(crate) pos: usize, + pub(crate) fds: &'a [Rc], +} + +pub struct Formatter<'a> { + fds: &'a mut Vec>, + buf: &'a mut DynamicBuf, +} + +pub unsafe trait Message<'a>: Sized + 'a { + const SIGNATURE: &'static str; + const INTERFACE: &'static str; + const MEMBER: &'static str; + type Generic<'b>: Message<'b>; + + fn marshal(&self, w: &mut Formatter); + fn unmarshal(p: &mut Parser<'a>) -> Result; + fn num_fds(&self) -> u32; +} + +pub struct ErrorMessage<'a> { + pub msg: Cow<'a, str>, +} + +unsafe impl<'a> Message<'a> for ErrorMessage<'a> { + const SIGNATURE: &'static str = "s"; + const INTERFACE: &'static str = ""; + const MEMBER: &'static str = ""; + type Generic<'b> = ErrorMessage<'b>; + + fn marshal(&self, w: &mut Formatter) { + self.msg.marshal(w) + } + + fn unmarshal(p: &mut Parser<'a>) -> Result { + Ok(Self { + msg: p.unmarshal()?, + }) + } + + fn num_fds(&self) -> u32 { + 0 + } +} + +pub trait Property { + const INTERFACE: &'static str; + const PROPERTY: &'static str; + type Type: DbusType<'static>; +} + +pub trait Signal<'a>: Message<'a> {} + +pub trait MethodCall<'a>: Message<'a> { + type Reply: Message<'static>; +} + +pub unsafe trait DbusType<'a>: Clone + 'a { + const ALIGNMENT: usize; + const IS_POD: bool; + type Generic<'b>: DbusType<'b> + 'b; + + fn consume_signature(s: &mut &[u8]) -> Result<(), DbusError>; + #[allow(clippy::allow_attributes, dead_code)] + fn write_signature(w: &mut Vec); + fn marshal(&self, fmt: &mut Formatter); + fn unmarshal(parser: &mut Parser<'a>) -> Result; + + fn num_fds(&self) -> u32 { + 0 + } +} + +pub mod prelude { + pub use { + super::{ + DbusError, DbusType, Formatter, Message, MethodCall, Parser, Property, Signal, + types::{Bool, DictEntry, ObjectPath, Variant}, + }, + std::{borrow::Cow, rc::Rc}, + uapi::OwnedFd, + }; +} diff --git a/src/dbus/parser.rs b/crates/dbus-core/src/parser.rs similarity index 92% rename from src/dbus/parser.rs rename to crates/dbus-core/src/parser.rs index 95b2525c..1821e283 100644 --- a/src/dbus/parser.rs +++ b/crates/dbus-core/src/parser.rs @@ -1,7 +1,7 @@ use { - crate::dbus::{ + crate::{ + types::{Bool, ObjectPath, Signature, Variant, FALSE, TRUE}, DbusError, DbusType, DynamicType, Parser, - types::{Bool, FALSE, ObjectPath, Signature, TRUE, Variant}, }, bstr::ByteSlice, std::{borrow::Cow, rc::Rc}, @@ -10,7 +10,11 @@ use { impl<'a> Parser<'a> { pub fn new(buf: &'a [u8], fds: &'a [Rc]) -> Self { - Self { buf, pos: 0, fds } + Self::new_at(buf, 0, fds) + } + + pub fn new_at(buf: &'a [u8], pos: usize, fds: &'a [Rc]) -> Self { + Self { buf, pos, fds } } pub fn eof(&self) -> bool { @@ -107,11 +111,7 @@ impl<'a> Parser<'a> { self.pos += len; Ok(Cow::Borrowed(slice)) } else { - let mut parser = Parser { - buf: &self.buf[..self.pos + len], - pos: self.pos, - fds: self.fds, - }; + let mut parser = Parser::new_at(&self.buf[..self.pos + len], self.pos, self.fds); self.pos += len; let mut res = vec![]; while !parser.eof() { diff --git a/src/dbus/property.rs b/crates/dbus-core/src/property.rs similarity index 95% rename from src/dbus/property.rs rename to crates/dbus-core/src/property.rs index 651e1767..3fd454fa 100644 --- a/src/dbus/property.rs +++ b/crates/dbus-core/src/property.rs @@ -1,5 +1,5 @@ use { - crate::dbus::{DbusError, DbusType, Formatter, Message, MethodCall, Parser}, + crate::{DbusError, DbusType, Formatter, Message, MethodCall, Parser}, std::{borrow::Cow, marker::PhantomData}, }; diff --git a/src/dbus/types.rs b/crates/dbus-core/src/types.rs similarity index 89% rename from src/dbus/types.rs rename to crates/dbus-core/src/types.rs index ee811e21..5e00897c 100644 --- a/src/dbus/types.rs +++ b/crates/dbus-core/src/types.rs @@ -1,12 +1,10 @@ use { crate::{ - dbus::{ - DbusError, DbusType, DynamicType, Formatter, Parser, TY_ARRAY, TY_BOOLEAN, TY_BYTE, - TY_DOUBLE, TY_INT16, TY_INT32, TY_INT64, TY_OBJECT_PATH, TY_SIGNATURE, TY_STRING, - TY_UINT16, TY_UINT32, TY_UINT64, TY_UNIX_FD, TY_VARIANT, - }, - utils::buf::DynamicBuf, + DbusError, DbusType, DynamicType, Formatter, Parser, TY_ARRAY, TY_BOOLEAN, TY_BYTE, + TY_DOUBLE, TY_INT16, TY_INT32, TY_INT64, TY_OBJECT_PATH, TY_SIGNATURE, TY_STRING, + TY_UINT16, TY_UINT32, TY_UINT64, TY_UNIX_FD, TY_VARIANT, }, + jay_utils::buf::DynamicBuf, std::{borrow::Cow, ops::Deref, rc::Rc}, uapi::{OwnedFd, Packed, Pod}, }; @@ -501,31 +499,6 @@ impl<'a> Variant<'a> { w.push(c); } - pub fn borrow<'b>(&'b self) -> Variant<'b> { - match self { - Variant::U8(v) => Variant::U8(*v), - Variant::Bool(v) => Variant::Bool(*v), - Variant::I16(v) => Variant::I16(*v), - Variant::U16(v) => Variant::U16(*v), - Variant::I32(v) => Variant::I32(*v), - Variant::U32(v) => Variant::U32(*v), - Variant::I64(v) => Variant::I64(*v), - Variant::U64(v) => Variant::U64(*v), - Variant::F64(v) => Variant::F64(*v), - Variant::String(v) => Variant::String(v.deref().into()), - Variant::ObjectPath(v) => Variant::ObjectPath(ObjectPath(v.0.deref().into())), - Variant::Signature(v) => Variant::Signature(Signature(v.0.deref().into())), - Variant::Variant(v) => Variant::Variant(Box::new(v.deref().borrow())), - Variant::Fd(v) => Variant::Fd(v.clone()), - Variant::Array(t, v) => { - Variant::Array(t.clone(), v.iter().map(|v| v.borrow()).collect()) - } - Variant::DictEntry(k, v) => { - Variant::DictEntry(Box::new(k.deref().borrow()), Box::new(v.deref().borrow())) - } - Variant::Struct(v) => Variant::Struct(v.iter().map(|v| v.borrow()).collect()), - } - } } unsafe impl<'a> DbusType<'a> for Variant<'a> { diff --git a/crates/drm-feedback/Cargo.toml b/crates/drm-feedback/Cargo.toml new file mode 100644 index 00000000..ac912d4b --- /dev/null +++ b/crates/drm-feedback/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "jay-drm-feedback" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +jay-video-types = { path = "../video-types" } + +ahash = "0.8.7" +byteorder = "1.5.0" +thiserror = "2.0.11" +uapi = "0.2.13" diff --git a/crates/drm-feedback/src/lib.rs b/crates/drm-feedback/src/lib.rs new file mode 100644 index 00000000..69ac676d --- /dev/null +++ b/crates/drm-feedback/src/lib.rs @@ -0,0 +1,149 @@ +use { + ahash::AHashMap, + byteorder::{NativeEndian, WriteBytesExt}, + jay_video_types::Modifier, + std::{io::Write, rc::Rc}, + thiserror::Error, + uapi::{OwnedFd, c}, +}; + +#[derive(Debug)] +pub struct DrmFeedbackIds { + next: std::cell::Cell, +} + +impl Default for DrmFeedbackIds { + fn default() -> Self { + Self { + next: std::cell::Cell::new(1), + } + } +} + +impl DrmFeedbackIds { + pub fn next(&self) -> DrmFeedbackId { + let id = self.next.get(); + self.next.set(id + 1); + DrmFeedbackId(id) + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct DrmFeedbackId(u64); + +#[derive(Debug)] +pub struct DrmFeedbackShared { + pub fd: Rc, + pub size: usize, + pub main_device: c::dev_t, + pub indices: AHashMap<(u32, Modifier), u16>, +} + +#[derive(Debug)] +pub struct DrmFeedback { + pub id: DrmFeedbackId, + pub shared: Rc, + pub tranches: Vec, +} + +#[derive(Clone, Debug)] +pub struct DrmFeedbackTranche { + pub device: c::dev_t, + pub indices: Vec, + pub scanout: bool, +} + +impl DrmFeedback { + pub fn new( + ids: &DrmFeedbackIds, + render_ctx: &C, + ) -> Result { + let main_device = match render_ctx.main_device() { + Some(dev) => dev, + _ => return Err(DrmFeedbackError::NoDrmDevice), + }; + let (data, index_map) = create_fd_data(render_ctx); + let mut memfd = + uapi::memfd_create("drm_feedback", c::MFD_CLOEXEC | c::MFD_ALLOW_SEALING).unwrap(); + memfd.write_all(&data).unwrap(); + uapi::lseek(memfd.raw(), 0, c::SEEK_SET).unwrap(); + uapi::fcntl_add_seals( + memfd.raw(), + c::F_SEAL_SEAL | c::F_SEAL_GROW | c::F_SEAL_SHRINK | c::F_SEAL_WRITE, + ) + .unwrap(); + Ok(Self { + id: ids.next(), + tranches: vec![DrmFeedbackTranche { + device: main_device, + indices: (0..index_map.len()).map(|v| v as u16).collect(), + scanout: false, + }], + shared: Rc::new(DrmFeedbackShared { + fd: Rc::new(memfd), + size: data.len(), + main_device, + indices: index_map, + }), + }) + } + + pub fn for_scanout( + &self, + ids: &DrmFeedbackIds, + devnum: c::dev_t, + formats: &[(u32, Modifier)], + ) -> Result, DrmFeedbackError> { + let mut tranches = vec![]; + { + let mut indices = vec![]; + for (format, modifier) in formats { + if let Some(idx) = self.shared.indices.get(&(*format, *modifier)) { + indices.push(*idx); + } + } + if indices.len() > 0 { + tranches.push(DrmFeedbackTranche { + device: devnum, + indices, + scanout: true, + }); + } else { + return Ok(None); + } + } + tranches.extend(self.tranches.iter().cloned()); + Ok(Some(Self { + id: ids.next(), + shared: self.shared.clone(), + tranches, + })) + } +} + +pub trait DrmFeedbackContext { + fn main_device(&self) -> Option; + fn for_each_read_format(&self, f: &mut dyn FnMut(u32, Modifier)); +} + +fn create_fd_data( + ctx: &C, +) -> (Vec, AHashMap<(u32, Modifier), u16>) { + let mut vec = vec![]; + let mut map = AHashMap::new(); + let mut pos = 0; + ctx.for_each_read_format(&mut |format, modifier| { + vec.write_u32::(format).unwrap(); + vec.write_u32::(0).unwrap(); + vec.write_u64::(modifier).unwrap(); + map.insert((format, modifier), pos); + pos += 1; + }); + (vec, map) +} + +#[derive(Debug, Error)] +pub enum DrmFeedbackError { + #[error("Graphics API does not have a DRM device")] + NoDrmDevice, +} diff --git a/crates/edid/Cargo.toml b/crates/edid/Cargo.toml new file mode 100644 index 00000000..b7f3416c --- /dev/null +++ b/crates/edid/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "jay-edid" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "EDID parsing for Jay" +repository = "https://github.com/mahkoh/jay" + +[dependencies] +bstr = { version = "1.9.0", default-features = false, features = ["std"] } +thiserror = "2.0.11" diff --git a/crates/edid/src/lib.rs b/crates/edid/src/lib.rs new file mode 100644 index 00000000..1272e8a7 --- /dev/null +++ b/crates/edid/src/lib.rs @@ -0,0 +1,1312 @@ +use { + bstr::{BString, ByteSlice}, + std::{ + cell::RefCell, + fmt::{Debug, Formatter}, + rc::Rc, + }, + thiserror::Error, +}; + +trait BitflagsExt { + fn contains(self, other: Self) -> bool; +} + +impl BitflagsExt for u8 { + fn contains(self, other: Self) -> bool { + self & other == other + } +} + +struct Stack(RefCell>); + +impl Default for Stack { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Stack { + fn push(&self, v: T) { + self.0.borrow_mut().push(v); + } + + fn pop(&self) -> Option { + self.0.borrow_mut().pop() + } + + fn to_vec(&self) -> Vec + where + T: Clone, + { + self.0.borrow().clone() + } +} + +#[derive(Copy, Clone, Debug)] +pub enum ColorBitDepth { + Undefined, + Bits6, + Bits8, + Bits10, + Bits12, + Bits14, + Bits16, + Reserved, +} + +#[derive(Copy, Clone, Debug)] +pub enum DigitalVideoInterfaceStandard { + Undefined, + Dvi, + HdmiA, + HdmiB, + MDDI, + DisplayPort, + Unknown(u8), +} + +#[derive(Copy, Clone)] +pub struct SignalLevelStandard(u8); + +impl Debug for SignalLevelStandard { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = match self.0 { + 0 => "+0.7/−0.3 V", + 1 => "+0.714/−0.286 V", + 2 => "+1.0/−0.4 V", + _ => "+0.7/0 V", + }; + Debug::fmt(s, f) + } +} + +#[derive(Copy, Clone, Debug)] +pub enum VideoInputDefinition { + Analog { + signal_level_standard: SignalLevelStandard, + blank_to_black_setup_or_pedestal: bool, + separate_h_v_sync_supported: bool, + composite_sync_on_horizontal_supported: bool, + composite_sync_on_green_supported: bool, + serration_on_vertical_sync_supported: bool, + }, + Digital { + bit_depth: ColorBitDepth, + video_interface: DigitalVideoInterfaceStandard, + }, +} + +#[derive(Copy, Clone, Debug)] +pub struct ScreenDimensions { + pub horizontal_screen_size_cm: Option, + pub vertical_screen_size_cm: Option, + pub landscape_aspect_ration: Option, + pub portrait_aspect_ration: Option, +} + +#[derive(Copy, Clone, Debug)] +pub struct ChromaticityCoordinates { + pub red_x: u16, + pub red_y: u16, + pub green_x: u16, + pub green_y: u16, + pub blue_x: u16, + pub blue_y: u16, + pub white_x: u16, + pub white_y: u16, +} + +#[derive(Copy, Clone, Debug)] +pub struct EstablishedTimings { + pub s_720x400_70: bool, + pub s_720x400_88: bool, + pub s_640x480_60: bool, + pub s_640x480_67: bool, + pub s_640x480_72: bool, + pub s_640x480_75: bool, + pub s_800x600_56: bool, + pub s_800x600_60: bool, + pub s_800x600_72: bool, + pub s_800x600_75: bool, + pub s_832x624_75: bool, + pub s_1024x768_87: bool, + pub s_1024x768_60: bool, + pub s_1024x768_70: bool, + pub s_1024x768_75: bool, + pub s_1280x1024_75: bool, + pub s_1152x870_75: bool, +} + +#[derive(Copy, Clone, Debug)] +pub enum AspectRatio { + A1_1, + A16_10, + A4_3, + A5_4, + A16_9, +} + +#[derive(Copy, Clone, Debug)] +pub struct StandardTiming { + pub x_resolution: u16, + pub aspect_ratio: AspectRatio, + pub vertical_frequency: u8, +} + +#[derive(Copy, Clone, Debug)] +pub enum AnalogSyncType { + AnalogComposite, + BipolarAnalogComposite, +} + +#[derive(Copy, Clone, Debug)] +pub enum SyncSignal { + Analog { + ty: AnalogSyncType, + with_serrations: bool, + sync_on_all_signals: bool, + }, + DigitalComposite { + with_serration: bool, + horizontal_sync_is_positive: bool, + }, + DigitalSeparate { + vertical_sync_is_positive: bool, + horizontal_sync_is_positive: bool, + }, +} + +#[derive(Copy, Clone)] +pub enum StereoViewingSupport { + None, + FieldSequentialRightDuringStereoSync, + FieldSequentialLeftDuringStereoSync, + TwoWayInterleavedRightImageOnEvenLines, + TwoWayInterleavedLeftImageOnEvenLines, + FourWayInterleaved, + SideBySideInterleaved, +} + +impl Debug for StereoViewingSupport { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let msg = match *self { + StereoViewingSupport::None => "none", + StereoViewingSupport::FieldSequentialRightDuringStereoSync => { + "field sequential, right during stereo sync" + } + StereoViewingSupport::FieldSequentialLeftDuringStereoSync => { + "field sequential, left during stereo sync" + } + StereoViewingSupport::TwoWayInterleavedRightImageOnEvenLines => { + "2-way interleaved, right image on even lines" + } + StereoViewingSupport::TwoWayInterleavedLeftImageOnEvenLines => { + "2-way interleaved, left image on even lines" + } + StereoViewingSupport::FourWayInterleaved => "4-way interleaved", + StereoViewingSupport::SideBySideInterleaved => "side-by-side interleaved", + }; + write!(f, "\"{}\"", msg) + } +} + +#[derive(Copy, Clone, Debug)] +pub struct DisplayRangeLimitsAndAdditionalTiming { + pub vertical_field_rate_min: u16, + pub vertical_field_rate_max: u16, + pub horizontal_field_rate_min: u16, + pub horizontal_field_rate_max: u16, + pub maximum_pixel_clock_mhz: u16, + pub extended_timing_information: ExtendedTimingInformation, +} + +#[derive(Copy, Clone, Debug)] +pub enum AspectRatioPreference { + A4_3, + A16_9, + A16_10, + A5_4, + A15_9, + Unknown(u8), +} + +#[derive(Copy, Clone, Debug)] +pub enum ExtendedTimingInformation { + DefaultGtf, + NoTimingInformation, + SecondaryGtf { + start_frequency: u16, + c_value: u16, + m_value: u16, + k_value: u8, + j_value: u16, + }, + Cvt { + cvt_major_version: u8, + cvt_minor_version: u8, + additional_clock_precision: u8, + maximum_active_pixels_per_line: Option, + ar_4_3: bool, + ar_16_9: bool, + ar_16_10: bool, + ar_5_4: bool, + ar_15_9: bool, + ar_preference: AspectRatioPreference, + cvt_rb_reduced_blanking_preferred: bool, + cvt_standard_blanking: bool, + scaling_support_horizontal_shrink: bool, + scaling_support_horizontal_stretch: bool, + scaling_support_vertical_shrink: bool, + scaling_support_vertical_stretch: bool, + preferred_vertical_refresh_rate_hz: u8, + }, + Unknown(u8), +} + +#[derive(Copy, Clone, Debug, Default)] +pub struct ColorPoint { + pub white_point_index: u8, + pub white_point_x: u16, + pub white_point_y: u16, + pub gamma: Option, +} + +#[derive(Copy, Clone, Debug)] +pub struct EstablishedTimings3 { + pub s640x350_85: bool, + pub s640x400_85: bool, + pub s720x400_85: bool, + pub s640x480_85: bool, + pub s848x480_60: bool, + pub s800x600_85: bool, + pub s1024x768_85: bool, + pub s1152x864_75: bool, + pub s1280x768_60_rb: bool, + pub s1280x768_60: bool, + pub s1280x768_75: bool, + pub s1280x768_85: bool, + pub s1280x960_60: bool, + pub s1280x960_85: bool, + pub s1280x1024_60: bool, + pub s1280x1024_85: bool, + pub s1360x768_60: bool, + pub s1440x900_60_rb: bool, + pub s1440x900_60: bool, + pub s1440x900_75: bool, + pub s1440x900_85: bool, + pub s1400x1050_60_rb: bool, + pub s1400x1050_60: bool, + pub s1400x1050_75: bool, + pub s1400x1050_85: bool, + pub s1680x1050_60_rb: bool, + pub s1680x1050_60: bool, + pub s1680x1050_75: bool, + pub s1680x1050_85: bool, + pub s1600x1200_60: bool, + pub s1600x1200_65: bool, + pub s1600x1200_70: bool, + pub s1600x1200_75: bool, + pub s1600x1200_85: bool, + pub s1792x1344_60: bool, + pub s1792x1344_75: bool, + pub s1856x1392_60: bool, + pub s1856x1392_75: bool, + pub s1920x1200_60_rb: bool, + pub s1920x1200_60: bool, + pub s1920x1200_75: bool, + pub s1920x1200_85: bool, + pub s1920x1440_60: bool, + pub s1920x1440_75: bool, +} + +#[derive(Copy, Clone, Debug)] +pub struct ColorManagementData { + pub red_a3: u16, + pub red_a2: u16, + pub green_a3: u16, + pub green_a2: u16, + pub blue_a3: u16, + pub blue_a2: u16, +} + +#[derive(Copy, Clone, Debug)] +pub enum CvtAspectRatio { + A4_3, + A16_9, + A16_10, + A15_9, +} + +#[derive(Copy, Clone, Debug)] +pub enum CvtPreferredVerticalRate { + R50, + R60, + R75, + R85, +} + +#[derive(Copy, Clone, Debug)] +pub struct Cvt3ByteCode { + pub addressable_lines_per_field: u16, + pub aspect_ration: CvtAspectRatio, + pub preferred_vertical_rate: CvtPreferredVerticalRate, + pub r50: bool, + pub r60: bool, + pub r75: bool, + pub r85: bool, + pub r60_reduced_blanking: bool, +} + +#[derive(Copy, Clone, Debug)] +pub struct DetailedTimingDescriptor { + pub pixel_clock_khz: u32, + pub horizontal_addressable_pixels: u16, + pub horizontal_blanking_pixels: u16, + pub vertical_addressable_lines: u16, + pub vertical_blanking_lines: u16, + pub horizontal_front_porch_pixels: u16, + pub horizontal_sync_pulse_pixels: u16, + pub vertical_front_porch_lines: u8, + pub vertical_sync_pulse_lines: u8, + pub horizontal_addressable_mm: u16, + pub vertical_addressable_mm: u16, + pub horizontal_left_border_pixels: u8, + pub vertical_top_border_pixels: u8, + pub interlaced: bool, + pub stereo_viewing_support: StereoViewingSupport, + pub sync: SyncSignal, +} + +#[derive(Clone, Debug)] +pub enum Descriptor { + Unknown(u8), + DetailedTimingDescriptor(DetailedTimingDescriptor), + DisplayProductSerialNumber(String), + AlphanumericDataString(String), + DisplayProductName(String), + DisplayRangeLimitsAndAdditionalTiming(DisplayRangeLimitsAndAdditionalTiming), + EstablishedTimings3(EstablishedTimings3), + ColorManagementData(ColorManagementData), + StandardTimingIdentifier([Option; 6]), + ColorPoint(ColorPoint, Option), + Cvt3ByteCode([Cvt3ByteCode; 4]), +} + +type EdidContext = (usize, EdidParseContext); + +struct EdidParser<'a> { + data: &'a [u8], + pos: usize, + context: Rc>, + saved_ctx: Vec, + errors: Vec<(EdidError, Vec)>, +} + +macro_rules! bail { + ($slf:expr, $err:expr) => {{ + $slf.saved_ctx = $slf.context.to_vec(); + return Err($err); + }}; +} + +#[derive(Clone, Debug)] +pub enum EdidParseContext { + ReadingBytes(usize), + BaseBlock, + Descriptors, + Descriptor, + ChromaticityCoordinates, + EstablishedTimings, + StandardTimings, + ScreenDimensions, + Gamma, + FeatureSupport, + Magic, + Extension, + IdManufacturerName, + VideoInputDefinition, +} + +struct EdidPushedContext { + stack: Rc>, +} + +impl Drop for EdidPushedContext { + fn drop(&mut self) { + self.stack.pop(); + } +} + +impl<'a> EdidParser<'a> { + fn push_ctx(&self, pc: EdidParseContext) -> EdidPushedContext { + self.context.push((self.pos, pc)); + EdidPushedContext { + stack: self.context.clone(), + } + } + + fn nest(&self, data: &'a [u8]) -> Self { + Self { + data, + pos: 0, + context: self.context.clone(), + saved_ctx: vec![], + errors: vec![], + } + } + + fn store_error(&mut self, error: EdidError) { + self.errors.push((error, self.saved_ctx.clone())); + } + + fn is_empty(&self) -> bool { + self.pos >= self.data.len() + } + + fn read_n(&mut self) -> Result<&'a [u8; N], EdidError> { + let _ctx = self.push_ctx(EdidParseContext::ReadingBytes(N)); + if self.data.len() - self.pos < N { + bail!(self, EdidError::UnexpectedEof); + } + let v = self.data[self.pos..self.pos + N].try_into().unwrap(); + self.pos += N; + Ok(v) + } + + fn read_var_n(&mut self, n: usize) -> Result<&'a [u8], EdidError> { + let _ctx = self.push_ctx(EdidParseContext::ReadingBytes(n)); + if self.data.len() - self.pos < n { + bail!(self, EdidError::UnexpectedEof); + } + let v = &self.data[self.pos..self.pos + n]; + self.pos += n; + Ok(v) + } + + fn read_u8(&mut self) -> Result { + let &[a] = self.read_n()?; + Ok(a) + } + + fn read_u16(&mut self) -> Result { + let &[lo, hi] = self.read_n()?; + Ok(((hi as u16) << 8) + lo as u16) + } + + fn read_u32(&mut self) -> Result { + let &[a, b, c, d] = self.read_n()?; + Ok(((d as u32) << 24) + ((c as u32) << 16) + ((b as u32) << 8) + a as u32) + } + + fn parse_magic(&mut self) -> Result<(), EdidError> { + let _ctx = self.push_ctx(EdidParseContext::Magic); + let magic = self.read_n::<8>()?; + if magic != &[0, 255, 255, 255, 255, 255, 255, 0] { + bail!(self, EdidError::InvalidMagic(magic.as_bstr().to_owned())); + } + Ok(()) + } + + fn parse_id_manufacturer_name(&mut self) -> Result { + let _ctx = self.push_ctx(EdidParseContext::IdManufacturerName); + let name = self.read_n::<2>()?; + let a = (name[0] >> 2) & 0b11111; + let b = ((name[0] & 0b11) << 3) | (name[1] >> 5); + let c = name[1] & 0b11111; + let name = [a + b'@', b + b'@', c + b'@'].as_bstr().to_owned(); + Ok(name) + } + + fn parse_video_input_definition(&mut self) -> Result { + let _ctx = self.push_ctx(EdidParseContext::VideoInputDefinition); + let val = self.read_u8()?; + let res = if val.contains(0x80) { + VideoInputDefinition::Digital { + bit_depth: match (val >> 4) & 0b111 { + 0b000 => ColorBitDepth::Undefined, + 0b001 => ColorBitDepth::Bits6, + 0b010 => ColorBitDepth::Bits8, + 0b011 => ColorBitDepth::Bits10, + 0b100 => ColorBitDepth::Bits12, + 0b101 => ColorBitDepth::Bits14, + 0b110 => ColorBitDepth::Bits16, + _ => ColorBitDepth::Reserved, + }, + video_interface: match val & 0b1111 { + 0b0000 => DigitalVideoInterfaceStandard::Undefined, + 0b0001 => DigitalVideoInterfaceStandard::Dvi, + 0b0010 => DigitalVideoInterfaceStandard::HdmiA, + 0b0011 => DigitalVideoInterfaceStandard::HdmiB, + 0b0100 => DigitalVideoInterfaceStandard::MDDI, + 0b0101 => DigitalVideoInterfaceStandard::DisplayPort, + n => DigitalVideoInterfaceStandard::Unknown(n), + }, + } + } else { + VideoInputDefinition::Analog { + signal_level_standard: SignalLevelStandard((val >> 5) & 0b11), + blank_to_black_setup_or_pedestal: (val >> 4).contains(1), + separate_h_v_sync_supported: (val >> 3).contains(1), + composite_sync_on_horizontal_supported: (val >> 2).contains(1), + composite_sync_on_green_supported: (val >> 1).contains(1), + serration_on_vertical_sync_supported: (val >> 0).contains(1), + } + }; + Ok(res) + } + + fn parse_screen_dimensions(&mut self) -> Result { + let _ctx = self.push_ctx(EdidParseContext::ScreenDimensions); + let &[hor, vert] = self.read_n()?; + let mut res = ScreenDimensions { + horizontal_screen_size_cm: None, + vertical_screen_size_cm: None, + landscape_aspect_ration: None, + portrait_aspect_ration: None, + }; + if hor != 0 && vert != 0 { + res.horizontal_screen_size_cm = Some(hor); + res.vertical_screen_size_cm = Some(vert); + } else if vert != 0 { + res.portrait_aspect_ration = Some(100.0 / (vert as f64 + 99.0)); + } else if hor != 0 { + res.landscape_aspect_ration = Some((hor as f64 + 99.0) / 100.0); + } + Ok(res) + } + + fn parse_gamma(&mut self) -> Result, EdidError> { + let _ctx = self.push_ctx(EdidParseContext::Gamma); + let val = self.read_u8()?; + if val == 0xff { + Ok(None) + } else { + Ok(Some((val as f64 + 100.0) / 100.0)) + } + } + + fn parse_feature_support(&mut self, digital: bool) -> Result { + let _ctx = self.push_ctx(EdidParseContext::FeatureSupport); + let val = self.read_u8()?; + Ok(FeatureSupport { + standby_supported: val.contains(0x80), + suspend_supported: val.contains(0x40), + active_off_supported: val.contains(0x20), + features: if digital { + FeatureSupport2::Digital { + rgb444_supported: true, + ycrcb422_supported: val.contains(0x10), + ycrcb444_supported: val.contains(0x08), + } + } else { + FeatureSupport2::Analog { + display_color_type: match (val >> 3) & 0b11 { + 0b00 => DisplayColorType::Monochrome, + 0b01 => DisplayColorType::Rgb, + 0b10 => DisplayColorType::NonRgb, + _ => DisplayColorType::Undefined, + }, + } + }, + srgb_is_default_color_space: val.contains(0x04), + preferred_mode_is_native: val.contains(0x02), + display_is_continuous_frequency: val.contains(0x01), + }) + } + + fn parse_chromaticity_coordinates(&mut self) -> Result { + let _ctx = self.push_ctx(EdidParseContext::ChromaticityCoordinates); + let b = self.read_n::<10>()?; + let rx = ((b[0] as u16 >> 6) & 0b11) + ((b[2] as u16) << 2); + let ry = ((b[0] as u16 >> 4) & 0b11) + ((b[3] as u16) << 2); + let gx = ((b[0] as u16 >> 2) & 0b11) + ((b[4] as u16) << 2); + let gy = ((b[0] as u16 >> 0) & 0b11) + ((b[5] as u16) << 2); + let bx = ((b[1] as u16 >> 6) & 0b11) + ((b[6] as u16) << 2); + let by = ((b[1] as u16 >> 4) & 0b11) + ((b[7] as u16) << 2); + let wx = ((b[1] as u16 >> 2) & 0b11) + ((b[8] as u16) << 2); + let wy = ((b[1] as u16 >> 0) & 0b11) + ((b[9] as u16) << 2); + Ok(ChromaticityCoordinates { + red_x: rx, + red_y: ry, + green_x: gx, + green_y: gy, + blue_x: bx, + blue_y: by, + white_x: wx, + white_y: wy, + }) + } + + fn parse_established_timings(&mut self) -> Result { + let _ctx = self.push_ctx(EdidParseContext::EstablishedTimings); + let b = self.read_n::<3>()?; + Ok(EstablishedTimings { + s_720x400_70: b[0].contains(0x80), + s_720x400_88: b[0].contains(0x40), + s_640x480_60: b[0].contains(0x20), + s_640x480_67: b[0].contains(0x10), + s_640x480_72: b[0].contains(0x08), + s_640x480_75: b[0].contains(0x04), + s_800x600_56: b[0].contains(0x02), + s_800x600_60: b[0].contains(0x01), + s_800x600_72: b[0].contains(0x80), + s_800x600_75: b[0].contains(0x40), + s_832x624_75: b[0].contains(0x20), + s_1024x768_87: b[0].contains(0x10), + s_1024x768_60: b[0].contains(0x08), + s_1024x768_70: b[0].contains(0x04), + s_1024x768_75: b[0].contains(0x02), + s_1280x1024_75: b[0].contains(0x01), + s_1152x870_75: b[0].contains(0x80), + }) + } + + fn parse_standard_timing(&mut self, revision: u8, a: u8, b: u8) -> Option { + if a == 0 { + return None; + } + Some(StandardTiming { + x_resolution: (a as u16 + 31) * 8, + aspect_ratio: match b >> 6 { + 0b00 if revision < 3 => AspectRatio::A1_1, + 0b00 => AspectRatio::A16_10, + 0b01 => AspectRatio::A4_3, + 0b10 => AspectRatio::A5_4, + _ => AspectRatio::A16_9, + }, + vertical_frequency: 60 + (b & 0b111111), + }) + } + + fn parse_standard_timings2( + &mut self, + revision: u8, + b: &[u8; 18], + ) -> [Option; 6] { + let mut res = [None; 6]; + for i in 0..6 { + let x = b[5 + 2 * i]; + let y = b[5 + 2 * i + 1]; + res[i] = self.parse_standard_timing(revision, x, y); + } + res + } + + fn parse_color_point(&mut self, b: &[u8; 18]) -> (ColorPoint, Option) { + let mut res = [Default::default(); 2]; + for n in 0..2 { + let b = &b[5 * (n + 1)..]; + res[n] = ColorPoint { + white_point_index: b[0], + white_point_x: ((b[2] as u16) << 2) | ((b[1] as u16) >> 2), + white_point_y: ((b[3] as u16) << 2) | ((b[1] as u16) & 0b11), + gamma: if b[4] == 0xff { + None + } else { + Some((b[5] as f64 + 100.0) / 100.0) + }, + }; + } + let second = if res[1].white_point_index != 0 { + Some(res[1]) + } else { + None + }; + (res[0], second) + } + + fn parse_standard_timings( + &mut self, + revision: u8, + ) -> Result<[Option; 8], EdidError> { + let _ctx = self.push_ctx(EdidParseContext::StandardTimings); + let bytes = self.read_n::<16>()?; + let mut res = [None; 8]; + for i in 0..8 { + let a = bytes[2 * i]; + let b = bytes[2 * i + 1]; + if (a, b) != (1, 1) { + res[i] = self.parse_standard_timing(revision, a, b); + } + } + Ok(res) + } + + fn parse_detailed_timing_descriptor(&self, b: &[u8; 18]) -> DetailedTimingDescriptor { + let l = b[17]; + DetailedTimingDescriptor { + pixel_clock_khz: u16::from_le_bytes([b[0], b[1]]) as u32 * 10_000, + horizontal_addressable_pixels: u16::from_le_bytes([b[2], b[4] >> 4]), + horizontal_blanking_pixels: u16::from_le_bytes([b[3], b[4] & 0b1111]), + vertical_addressable_lines: u16::from_le_bytes([b[5], b[7] >> 4]), + vertical_blanking_lines: u16::from_le_bytes([b[6], b[7] & 0b1111]), + horizontal_front_porch_pixels: u16::from_le_bytes([b[8], b[11] >> 6]), + horizontal_sync_pulse_pixels: u16::from_le_bytes([b[9], (b[11] >> 4) & 0b11]), + vertical_front_porch_lines: (b[10] >> 4) | ((b[11] & 0b1100) << 2), + vertical_sync_pulse_lines: (b[10] & 0b1111) | ((b[11] & 0b11) << 4), + horizontal_addressable_mm: u16::from_le_bytes([b[12], b[14] >> 4]), + vertical_addressable_mm: u16::from_le_bytes([b[13], b[14] & 0b1111]), + horizontal_left_border_pixels: b[15], + vertical_top_border_pixels: b[16], + interlaced: l.contains(0x80), + stereo_viewing_support: match ((l >> 4) & 0b110) | (l & 0b1) { + 0b010 => StereoViewingSupport::FieldSequentialRightDuringStereoSync, + 0b100 => StereoViewingSupport::FieldSequentialLeftDuringStereoSync, + 0b011 => StereoViewingSupport::TwoWayInterleavedRightImageOnEvenLines, + 0b101 => StereoViewingSupport::TwoWayInterleavedLeftImageOnEvenLines, + 0b110 => StereoViewingSupport::FourWayInterleaved, + 0b111 => StereoViewingSupport::SideBySideInterleaved, + _ => StereoViewingSupport::None, + }, + sync: if l.contains(0b10000) { + if l.contains(0b01000) { + SyncSignal::DigitalSeparate { + vertical_sync_is_positive: l.contains(0b100), + horizontal_sync_is_positive: l.contains(0b10), + } + } else { + SyncSignal::DigitalComposite { + with_serration: l.contains(0b100), + horizontal_sync_is_positive: l.contains(0b10), + } + } + } else { + SyncSignal::Analog { + ty: if l.contains(0b1000) { + AnalogSyncType::BipolarAnalogComposite + } else { + AnalogSyncType::AnalogComposite + }, + with_serrations: l.contains(0b100), + sync_on_all_signals: l.contains(0b10), + } + }, + } + } + + fn parse_display_range_limits_and_additional_timing( + &self, + b: &[u8; 18], + ) -> DisplayRangeLimitsAndAdditionalTiming { + let min_vert_off = b[4].contains(0b0001); + let max_vert_off = min_vert_off || b[4].contains(0b0010); + let min_horz_off = b[4].contains(0b0100); + let max_horz_off = min_horz_off || b[4].contains(0b1000); + DisplayRangeLimitsAndAdditionalTiming { + vertical_field_rate_min: b[5] as u16 + if min_vert_off { 255 } else { 0 }, + vertical_field_rate_max: b[6] as u16 + if max_vert_off { 255 } else { 0 }, + horizontal_field_rate_min: b[7] as u16 + if min_horz_off { 255 } else { 0 }, + horizontal_field_rate_max: b[8] as u16 + if max_horz_off { 255 } else { 0 }, + maximum_pixel_clock_mhz: b[9] as u16 * 10, + extended_timing_information: match b[10] { + 0x0 => ExtendedTimingInformation::DefaultGtf, + 0x1 => ExtendedTimingInformation::NoTimingInformation, + 0x2 => ExtendedTimingInformation::SecondaryGtf { + start_frequency: b[12] as u16, + c_value: b[13] as u16, + m_value: u16::from_le_bytes([b[14], b[15]]), + k_value: b[16], + j_value: b[17] as u16, + }, + 0x4 => ExtendedTimingInformation::Cvt { + cvt_major_version: b[11] >> 4, + cvt_minor_version: b[11] & 0b1111, + additional_clock_precision: b[12] >> 2, + maximum_active_pixels_per_line: if b[13] == 0 { + None + } else { + Some((((b[12] as u16 & 0b11) << 8) | b[13] as u16) * 8) + }, + ar_4_3: b[14].contains(0x80), + ar_16_9: b[14].contains(0x40), + ar_16_10: b[14].contains(0x20), + ar_5_4: b[14].contains(0x10), + ar_15_9: b[14].contains(0x08), + ar_preference: match b[15] >> 5 { + 0b000 => AspectRatioPreference::A4_3, + 0b001 => AspectRatioPreference::A16_9, + 0b010 => AspectRatioPreference::A16_10, + 0b011 => AspectRatioPreference::A5_4, + 0b100 => AspectRatioPreference::A15_9, + n => AspectRatioPreference::Unknown(n), + }, + cvt_rb_reduced_blanking_preferred: b[15].contains(0b10000), + cvt_standard_blanking: b[15].contains(0b1000), + scaling_support_horizontal_shrink: b[16].contains(0x80), + scaling_support_horizontal_stretch: b[16].contains(0x40), + scaling_support_vertical_shrink: b[16].contains(0x20), + scaling_support_vertical_stretch: b[16].contains(0x10), + preferred_vertical_refresh_rate_hz: b[17], + }, + n => ExtendedTimingInformation::Unknown(n), + }, + } + } + + fn parse_established_timings3(&self, b: &[u8; 18]) -> EstablishedTimings3 { + EstablishedTimings3 { + s640x350_85: b[6].contains(0x80), + s640x400_85: b[6].contains(0x40), + s720x400_85: b[6].contains(0x20), + s640x480_85: b[6].contains(0x10), + s848x480_60: b[6].contains(0x08), + s800x600_85: b[6].contains(0x04), + s1024x768_85: b[6].contains(0x02), + s1152x864_75: b[6].contains(0x01), + s1280x768_60_rb: b[7].contains(0x80), + s1280x768_60: b[7].contains(0x40), + s1280x768_75: b[7].contains(0x20), + s1280x768_85: b[7].contains(0x10), + s1280x960_60: b[7].contains(0x08), + s1280x960_85: b[7].contains(0x04), + s1280x1024_60: b[7].contains(0x02), + s1280x1024_85: b[7].contains(0x01), + s1360x768_60: b[8].contains(0x80), + s1440x900_60_rb: b[8].contains(0x40), + s1440x900_60: b[8].contains(0x20), + s1440x900_75: b[8].contains(0x10), + s1440x900_85: b[8].contains(0x08), + s1400x1050_60_rb: b[8].contains(0x04), + s1400x1050_60: b[8].contains(0x02), + s1400x1050_75: b[8].contains(0x01), + s1400x1050_85: b[9].contains(0x80), + s1680x1050_60_rb: b[9].contains(0x40), + s1680x1050_60: b[9].contains(0x20), + s1680x1050_75: b[9].contains(0x10), + s1680x1050_85: b[9].contains(0x08), + s1600x1200_60: b[9].contains(0x04), + s1600x1200_65: b[9].contains(0x02), + s1600x1200_70: b[9].contains(0x01), + s1600x1200_75: b[10].contains(0x80), + s1600x1200_85: b[10].contains(0x40), + s1792x1344_60: b[10].contains(0x20), + s1792x1344_75: b[10].contains(0x10), + s1856x1392_60: b[10].contains(0x08), + s1856x1392_75: b[10].contains(0x04), + s1920x1200_60_rb: b[10].contains(0x02), + s1920x1200_60: b[10].contains(0x01), + s1920x1200_75: b[11].contains(0x80), + s1920x1200_85: b[11].contains(0x40), + s1920x1440_60: b[11].contains(0x20), + s1920x1440_75: b[11].contains(0x10), + } + } + + fn parse_color_management_data(&self, b: &[u8; 18]) -> ColorManagementData { + ColorManagementData { + red_a3: u16::from_le_bytes([b[6], b[7]]), + red_a2: u16::from_le_bytes([b[8], b[9]]), + green_a3: u16::from_le_bytes([b[10], b[11]]), + green_a2: u16::from_le_bytes([b[12], b[13]]), + blue_a3: u16::from_le_bytes([b[14], b[15]]), + blue_a2: u16::from_le_bytes([b[16], b[17]]), + } + } + + fn parse_cvt3_byte_codes(&self, b: &[u8; 18]) -> [Cvt3ByteCode; 4] { + let parse = |n: usize| { + let b = &b[6 + 3 * n..]; + Cvt3ByteCode { + addressable_lines_per_field: u16::from_le_bytes([b[0], b[1] >> 4]), + aspect_ration: match (b[1] >> 2) & 0b11 { + 0 => CvtAspectRatio::A4_3, + 1 => CvtAspectRatio::A16_9, + 2 => CvtAspectRatio::A16_10, + _ => CvtAspectRatio::A15_9, + }, + preferred_vertical_rate: match (b[2] >> 5) & 0b11 { + 0 => CvtPreferredVerticalRate::R50, + 1 => CvtPreferredVerticalRate::R60, + 2 => CvtPreferredVerticalRate::R75, + _ => CvtPreferredVerticalRate::R85, + }, + r50: b[2].contains(0b10000), + r60: b[2].contains(0b01000), + r75: b[2].contains(0b00100), + r85: b[2].contains(0b00010), + r60_reduced_blanking: b[2].contains(0b00001), + } + }; + [parse(0), parse(1), parse(2), parse(3)] + } + + fn parse_descriptor(&mut self, revision: u8) -> Result, EdidError> { + let _ctx = self.push_ctx(EdidParseContext::Descriptor); + let b = self.read_n::<18>()?; + let str = || { + let mut s = &b[5..]; + if let Some(n) = s.find_byte(b'\n') { + s = &s[..n]; + }; + let mut res = String::new(); + for &b in s { + res.push_str(CP437[b as usize]); + } + res + }; + let res = if (b[0], b[1]) == (0, 0) { + match b[3] { + 0xff => Descriptor::DisplayProductSerialNumber(str()), + 0xfe => Descriptor::AlphanumericDataString(str()), + 0xfd => Descriptor::DisplayRangeLimitsAndAdditionalTiming( + self.parse_display_range_limits_and_additional_timing(b), + ), + 0xfc => Descriptor::DisplayProductName(str()), + 0xfb => { + let (first, second) = self.parse_color_point(b); + Descriptor::ColorPoint(first, second) + } + 0xfa => { + Descriptor::StandardTimingIdentifier(self.parse_standard_timings2(revision, b)) + } + 0xf9 => Descriptor::ColorManagementData(self.parse_color_management_data(b)), + 0xf8 => Descriptor::Cvt3ByteCode(self.parse_cvt3_byte_codes(b)), + 0xf7 => Descriptor::EstablishedTimings3(self.parse_established_timings3(b)), + 0x10 => return Ok(None), + n => Descriptor::Unknown(n), + } + } else { + Descriptor::DetailedTimingDescriptor(self.parse_detailed_timing_descriptor(b)) + }; + Ok(Some(res)) + } + + fn parse_descriptors(&mut self, revision: u8) -> Result<[Option; 4], EdidError> { + let _ctx = self.push_ctx(EdidParseContext::Descriptors); + let mut res = [None, None, None, None]; + for res in &mut res { + *res = self.parse_descriptor(revision)?; + } + Ok(res) + } + + fn parse_base_block(&mut self) -> Result { + let _ctx = self.push_ctx(EdidParseContext::BaseBlock); + self.parse_magic()?; + let id_manufacturer_name = self.parse_id_manufacturer_name()?; + let id_product_code = self.read_u16()?; + let id_serial_number = self.read_u32()?; + let mut week_of_manufacture = None; + let mut model_year = None; + let mut year_of_manufacture = None; + { + let &[a, b] = self.read_n()?; + if matches!(a, 1..=0x36) { + week_of_manufacture = Some(a); + } + let year = b as u16 + 1990; + if a == 0xff { + model_year = Some(year); + } else { + year_of_manufacture = Some(year); + } + } + let &[edid_version, edid_revision] = self.read_n()?; + let video_input_definition = self.parse_video_input_definition()?; + let is_digital = matches!(video_input_definition, VideoInputDefinition::Digital { .. }); + let screen_dimensions = self.parse_screen_dimensions()?; + let gamma = self.parse_gamma()?; + let feature_support = self.parse_feature_support(is_digital)?; + let chromaticity_coordinates = self.parse_chromaticity_coordinates()?; + let established_timings = self.parse_established_timings()?; + let standard_timings = self.parse_standard_timings(edid_revision)?; + let descriptors = self.parse_descriptors(edid_revision)?; + let num_extensions = self.read_u8()?; + let _checksum = self.read_u8()?; + Ok(EdidBaseBlock { + id_manufacturer_name, + id_product_code, + id_serial_number, + week_of_manufacture, + model_year, + year_of_manufacture, + edid_version, + edid_revision, + video_input_definition, + screen_dimensions, + gamma, + feature_support, + chromaticity_coordinates, + established_timings, + standard_timings, + descriptors, + num_extensions, + }) + } + + fn parse_cta_amd_vendor_data_block(&mut self) -> Result { + let _ = self.read_n::<2>()?; + Ok(CtaDataBlock::VendorAmd(CtaAmdVendorDataBlock { + minimum_refresh_hz: self.read_u8()?, + maximum_refresh_hz: self.read_u8()?, + })) + } + + fn parse_cta_vendor_data_block(&mut self) -> Result { + match self.read_n::<3>()? { + [0x1A, 0x00, 0x00] => self.parse_cta_amd_vendor_data_block(), + _ => Ok(CtaDataBlock::Unknown), + } + } + + fn parse_cta_colorimetry_data_block(&mut self) -> Result { + let [lo, hi] = *self.read_n::<2>()?; + Ok(CtaDataBlock::Colorimetry(CtaColorimetryDataBlock { + bt2020_rgb: lo.contains(0x80), + bt2020_ycc: lo.contains(0x40), + bt2020_cycc: lo.contains(0x20), + op_rgb: lo.contains(0x10), + op_ycc_601601: lo.contains(0x08), + s_ycc_601: lo.contains(0x04), + xv_ycc_709: lo.contains(0x02), + xv_ycc_601: lo.contains(0x01), + dci_p3: hi.contains(0x80), + })) + } + + fn parse_cta_hdr_static_metadata_data_block(&mut self) -> Result { + let et = self.read_u8()?; + let _ = self.read_u8()?; + let mut read_luminance = |min: bool| { + let v = self.read_u8().unwrap_or_default(); + if v == 0 { + None + } else if min { + Some((v as f64 / 255.0).powi(2) / 100.0) + } else { + Some(50.0 * 2.0f64.powf(v as f64 / 32.0)) + } + }; + Ok(CtaDataBlock::StaticHdrMetadata( + CtaStaticHdrMetadataDataBlock { + traditional_gamma_sdr_luminance: et.contains(0x01), + traditional_gamma_hdr_luminance: et.contains(0x02), + smpte_st_2084: et.contains(0x04), + hlg: et.contains(0x08), + max_luminance: read_luminance(false), + max_frame_average_luminance: read_luminance(false), + min_luminance: read_luminance(true), + }, + )) + } + + fn parse_cta_extended_data_block(&mut self) -> Result { + match self.read_u8()? { + 0x5 => self.parse_cta_colorimetry_data_block(), + 0x6 => self.parse_cta_hdr_static_metadata_data_block(), + _ => Ok(CtaDataBlock::Unknown), + } + } + + fn parse_cta_data_block(&mut self, tag: u8) -> Result { + match tag { + 0x3 => self.parse_cta_vendor_data_block(), + 0x7 => self.parse_cta_extended_data_block(), + _ => Ok(CtaDataBlock::Unknown), + } + } + + fn parse_cta_extension_v3(&mut self) -> Result { + let detailed_timing_descriptors_offset = self.read_u8()? as usize; + let _ = self.read_u8()?; + let mut data_blocks = vec![]; + while self.pos < detailed_timing_descriptors_offset { + let b1 = self.read_u8()?; + let data = self.read_var_n(b1 as usize & 0x1f)?; + let mut parser = self.nest(data); + match parser.parse_cta_data_block(b1 >> 5) { + Ok(d) => data_blocks.push(d), + Err(e) => { + self.saved_ctx = parser.saved_ctx; + self.store_error(e); + } + } + } + Ok(EdidExtension::CtaV3(CtaExtensionV3 { data_blocks })) + } + + fn parse_cta_extension(&mut self) -> Result { + // https://web.archive.org/web/20171201033424/https://standards.cta.tech/kwspub/published_docs/CTA-861-G_FINAL_revised_2017.pdf + match self.read_u8()? { + 0x3 => self.parse_cta_extension_v3(), + _ => Ok(EdidExtension::Unknown), + } + } + + fn parse_extension_impl(&mut self) -> Result { + match self.read_u8()? { + 0x2 => self.parse_cta_extension(), + _ => Ok(EdidExtension::Unknown), + } + } + + fn parse_extension(&mut self) -> Result { + let _ctx = self.push_ctx(EdidParseContext::Extension); + let data = self.read_n::<128>()?; + let mut parser = self.nest(data); + let res = parser.parse_extension_impl(); + if res.is_err() { + self.saved_ctx = parser.saved_ctx; + } + res + } + + fn parse(&mut self) -> Result { + let bb = self.parse_base_block()?; + let mut exts = vec![]; + while !self.is_empty() { + match self.parse_extension() { + Ok(e) => exts.push(e), + Err(e) => self.store_error(e), + } + } + Ok(EdidFile { + base_block: bb, + extension_blocks: exts, + }) + } +} + +#[derive(Debug)] +pub enum DisplayColorType { + Monochrome, + Rgb, + NonRgb, + Undefined, +} + +#[derive(Debug)] +pub enum FeatureSupport2 { + Analog { + display_color_type: DisplayColorType, + }, + Digital { + rgb444_supported: bool, + ycrcb444_supported: bool, + ycrcb422_supported: bool, + }, +} + +#[derive(Debug)] +pub struct FeatureSupport { + pub standby_supported: bool, + pub suspend_supported: bool, + pub active_off_supported: bool, + pub features: FeatureSupport2, + pub srgb_is_default_color_space: bool, + pub preferred_mode_is_native: bool, + pub display_is_continuous_frequency: bool, +} + +#[derive(Debug)] +pub struct EdidBaseBlock { + pub id_manufacturer_name: BString, + pub id_product_code: u16, + pub id_serial_number: u32, + pub week_of_manufacture: Option, + pub model_year: Option, + pub year_of_manufacture: Option, + pub edid_version: u8, + pub edid_revision: u8, + pub video_input_definition: VideoInputDefinition, + pub screen_dimensions: ScreenDimensions, + pub gamma: Option, + pub feature_support: FeatureSupport, + pub chromaticity_coordinates: ChromaticityCoordinates, + pub established_timings: EstablishedTimings, + pub standard_timings: [Option; 8], + pub descriptors: [Option; 4], + pub num_extensions: u8, +} + +#[derive(Debug)] +pub enum EdidExtension { + Unknown, + CtaV3(CtaExtensionV3), +} + +#[derive(Debug)] +pub struct CtaExtensionV3 { + pub data_blocks: Vec, +} + +#[derive(Debug)] +pub enum CtaDataBlock { + Unknown, + VendorAmd(CtaAmdVendorDataBlock), + Colorimetry(CtaColorimetryDataBlock), + StaticHdrMetadata(CtaStaticHdrMetadataDataBlock), +} + +#[derive(Debug)] +pub struct CtaAmdVendorDataBlock { + pub minimum_refresh_hz: u8, + pub maximum_refresh_hz: u8, +} + +#[derive(Copy, Clone, Debug)] +pub struct CtaColorimetryDataBlock { + pub bt2020_rgb: bool, + pub bt2020_ycc: bool, + pub bt2020_cycc: bool, + pub op_rgb: bool, + pub op_ycc_601601: bool, + pub s_ycc_601: bool, + pub xv_ycc_709: bool, + pub xv_ycc_601: bool, + pub dci_p3: bool, +} + +#[derive(Copy, Clone, Debug)] +pub struct CtaStaticHdrMetadataDataBlock { + pub traditional_gamma_sdr_luminance: bool, + pub traditional_gamma_hdr_luminance: bool, + pub smpte_st_2084: bool, + pub hlg: bool, + pub max_luminance: Option, + pub max_frame_average_luminance: Option, + pub min_luminance: Option, +} + +#[derive(Debug)] +pub struct EdidFile { + pub base_block: EdidBaseBlock, + pub extension_blocks: Vec, +} + +#[derive(Debug, Error)] +pub enum EdidError { + #[error("Unexpected end-of-file")] + UnexpectedEof, + #[error("Invalid magic header")] + InvalidMagic(BString), +} + +pub fn parse(data: &[u8]) -> Result { + let mut parser = EdidParser { + data, + pos: 0, + context: Rc::new(Default::default()), + saved_ctx: vec![], + errors: vec![], + }; + parser.parse() +} + +const CP437: &[&str] = &[ + "\u{0}", "☺", "☻", "♥", "♦", "♣", "♠", "•", "◘", "○", "◙", "♂", "♀", "♪", "♫", "☼", "►", "◄", + "↕", "‼", "¶", "§", "▬", "↨", "↑", "↓", "→", "←", "∟", "↔", "▲", "▼", " ", "!", "\"", "#", "$", + "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", + "8", "9", ":", ";", "<", "=", ">", "?", "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", + "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", + "^", "_", "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", + "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "~", "⌂", "Ç", "ü", "é", "â", + "ä", "à", "å", "ç", "ê", "ë", "è", "ï", "î", "ì", "Ä", "Å", "É", "æ", "Æ", "ô", "ö", "ò", "û", + "ù", "ÿ", "Ö", "Ü", "¢", "£", "¥", "₧", "ƒ", "á", "í", "ó", "ú", "ñ", "Ñ", "ª", "º", "¿", "⌐", + "¬", "½", "¼", "¡", "«", "»", "░", "▒", "▓", "│", "┤", "╡", "╢", "╖", "╕", "╣", "║", "╗", "╝", + "╜", "╛", "┐", "└", "┴", "┬", "├", "─", "┼", "╞", "╟", "╚", "╔", "╩", "╦", "╠", "═", "╬", "╧", + "╨", "╤", "╥", "╙", "╘", "╒", "╓", "╫", "╪", "┘", "┌", "█", "▄", "▌", "▐", "▀", "α", "ß", "Γ", + "π", "Σ", "σ", "µ", "τ", "Φ", "Θ", "Ω", "δ", "∞", "φ", "ε", "∩", "≡", "±", "≥", "≤", "⌠", "⌡", + "÷", "≈", "°", "∙", "·", "√", "ⁿ", "²", "■", "\u{a0}", +]; diff --git a/crates/eventfd-cache/Cargo.toml b/crates/eventfd-cache/Cargo.toml new file mode 100644 index 00000000..03604f58 --- /dev/null +++ b/crates/eventfd-cache/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "jay-eventfd-cache" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +jay-async-engine = { path = "../async-engine" } +jay-io-uring = { path = "../io-uring" } +jay-utils = { path = "../utils" } + +log = { version = "0.4.20", features = ["std"] } +thiserror = "2.0.11" +uapi = "0.2.13" diff --git a/crates/eventfd-cache/src/lib.rs b/crates/eventfd-cache/src/lib.rs new file mode 100644 index 00000000..336b2fde --- /dev/null +++ b/crates/eventfd-cache/src/lib.rs @@ -0,0 +1,157 @@ +use { + jay_async_engine::{AsyncEngine, SpawnedFuture}, + jay_io_uring::{IoUring, IoUringError}, + jay_utils::{ + buf::Buf, + errorfmt::ErrorFmt, + oserror::{OsError, OsErrorExt, OsErrorExt2}, + queue::AsyncQueue, + stack::Stack, + }, + std::{cell::Cell, future::poll_fn, pin::Pin, rc::Rc, slice, task::Poll}, + thiserror::Error, + uapi::{OwnedFd, c}, +}; + +#[cfg(test)] +mod tests; + +#[derive(Debug, Error)] +pub enum EventfdError { + #[error("Could not create an eventfd")] + CreateEventfd(#[source] OsError), +} + +pub struct EventfdCache { + inner: Rc, + _task: SpawnedFuture<()>, +} + +struct Inner { + ring: Rc, + fds: Stack>, + recycle: AsyncQueue>, +} + +pub struct Eventfd { + cache: Rc, + pub fd: Rc, + signaled: Cell, +} + +impl EventfdCache { + pub fn new(ring: &Rc, eng: &Rc) -> Rc { + let inner = Rc::new(Inner { + ring: ring.clone(), + fds: Default::default(), + recycle: Default::default(), + }); + let task = eng.spawn("eventfd-cache", inner.clone().recycle()); + Rc::new(Self { inner, _task: task }) + } + + pub fn acquire(&self) -> Result { + let fd = match self.inner.fds.pop() { + Some(fd) => fd, + _ => uapi::eventfd(0, c::EFD_CLOEXEC) + .map(Rc::new) + .map_os_err(EventfdError::CreateEventfd)?, + }; + Ok(Eventfd { + cache: self.inner.clone(), + fd, + signaled: Default::default(), + }) + } +} + +impl Eventfd { + pub fn is_signaled(&self) -> bool { + self.signaled.get() + } + + pub async fn signaled(&self) -> Result<(), IoUringError> { + if self.signaled.get() { + return Ok(()); + } + self.cache.ring.readable(&self.fd).await?; + self.signaled.set(true); + Ok(()) + } + + pub fn signaled_blocking(&self) -> Result<(), OsError> { + if self.signaled.get() { + return Ok(()); + } + let mut pollfd = c::pollfd { + fd: self.fd.raw(), + events: c::POLLIN, + revents: 0, + }; + uapi::poll(slice::from_mut(&mut pollfd), -1).to_os_error()?; + self.signaled.set(true); + Ok(()) + } +} + +impl Inner { + async fn recycle(self: Rc) { + let slf = &*self; + let mut fds = vec![]; + let mut bufs = vec![]; + let mut tasks = vec![]; + let mut todo = vec![]; + loop { + fds.clear(); + tasks.clear(); + todo.clear(); + slf.recycle.non_empty().await; + while let Some(fd) = slf.recycle.try_pop() { + fds.push(fd); + } + for (idx, fd) in fds.iter().enumerate() { + if idx >= bufs.len() { + bufs.push(Buf::new(size_of::())); + } + let fd = fd.clone(); + let buf = bufs[idx].clone(); + tasks.push(async move { slf.ring.read(&fd, buf).await }); + todo.push(idx); + } + poll_fn(|ctx| { + let mut i = 0; + while i < todo.len() { + let idx = todo[i]; + let task = unsafe { Pin::new_unchecked(&mut tasks[idx]) }; + if let Poll::Ready(res) = task.poll(ctx) { + todo.swap_remove(i); + match res { + Ok(_) => { + self.fds.push(fds[idx].clone()); + } + Err(e) => { + log::error!("Could not read from eventfd: {}", ErrorFmt(e)); + } + } + } else { + i += 1; + } + } + if todo.is_empty() { + Poll::Ready(()) + } else { + Poll::Pending + } + }) + .await; + } + } +} + +impl Drop for Eventfd { + fn drop(&mut self) { + if self.signaled.get() { + self.cache.recycle.push(self.fd.clone()); + } + } +} diff --git a/src/eventfd_cache/tests.rs b/crates/eventfd-cache/src/tests.rs similarity index 94% rename from src/eventfd_cache/tests.rs rename to crates/eventfd-cache/src/tests.rs index 2c536f6d..2e6ad069 100644 --- a/src/eventfd_cache/tests.rs +++ b/crates/eventfd-cache/src/tests.rs @@ -1,7 +1,8 @@ use { - crate::{ - async_engine::AsyncEngine, eventfd_cache::EventfdCache, io_uring::IoUring, utils::array, - }, + crate::EventfdCache, + jay_async_engine::AsyncEngine, + jay_io_uring::IoUring, + jay_utils::array, std::{rc::Rc, slice}, uapi::c, }; diff --git a/crates/formats/Cargo.toml b/crates/formats/Cargo.toml new file mode 100644 index 00000000..d44f7a3d --- /dev/null +++ b/crates/formats/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "jay-formats" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Pixel format tables for Jay" +repository = "https://github.com/mahkoh/jay" + +[dependencies] +ahash = "0.8.7" +ash = { package = "jay-ash", version = "0.3.0" } +clap = { version = "4.4.18", features = ["derive", "wrap_help"] } +jay-config = { path = "../jay-config" } diff --git a/crates/formats/src/lib.rs b/crates/formats/src/lib.rs new file mode 100644 index 00000000..0599e878 --- /dev/null +++ b/crates/formats/src/lib.rs @@ -0,0 +1,559 @@ +use { + ahash::AHashMap, + ash::vk, + clap::{ValueEnum, builder::PossibleValue}, + jay_config::video::Format as ConfigFormat, + std::{ + fmt::{self, Debug, Write}, + sync::LazyLock, + }, +}; + +pub type GLenum = u32; +pub type GLint = i32; + +const GL_RGBA: GLint = 0x1908; +const GL_RGBA8: GLenum = 0x8058; +const GL_BGRA_EXT: GLint = 0x80E1; +const GL_UNSIGNED_BYTE: GLint = 0x1401; + +#[derive(Copy, Clone, Debug)] +pub struct FormatShmInfo { + pub gl_format: GLint, + pub gl_internal_format: GLenum, + pub gl_type: GLint, +} + +#[derive(Copy, Clone, Debug)] +pub struct Format { + pub name: &'static str, + pub vk_format: vk::Format, + pub drm: u32, + pub wl_id: Option, + pub external_only_guess: bool, + pub has_alpha: bool, + pub opaque: Option<&'static Format>, + pub shm_info: Option, + pub config: ConfigFormat, + pub bpp: u32, +} + +const fn default(config: ConfigFormat) -> Format { + Format { + name: "", + vk_format: vk::Format::UNDEFINED, + drm: 0, + wl_id: None, + external_only_guess: false, + has_alpha: false, + opaque: None, + shm_info: None, + config, + bpp: 4, + } +} + +impl PartialEq for Format { + fn eq(&self, other: &Self) -> bool { + self.drm == other.drm + } +} + +impl Eq for Format {} + +impl ValueEnum for &'static Format { + fn value_variants<'a>() -> &'a [Self] { + ref_formats() + } + + fn to_possible_value(&self) -> Option { + Some(PossibleValue::new(self.name)) + } +} + +static FORMATS_MAP: LazyLock> = LazyLock::new(|| { + let mut map = AHashMap::new(); + for format in FORMATS { + assert!(map.insert(format.drm, format).is_none()); + } + map +}); + +static FORMATS_REFS: LazyLock> = LazyLock::new(|| FORMATS.iter().collect()); + +static FORMATS_NAMES: LazyLock> = LazyLock::new(|| { + let mut map = AHashMap::new(); + for format in FORMATS { + assert!(map.insert(format.name, format).is_none()); + } + map +}); + +static FORMATS_CONFIG: LazyLock> = LazyLock::new(|| { + let mut map = AHashMap::new(); + for format in FORMATS { + assert!(map.insert(format.config, format).is_none()); + } + map +}); + +#[test] +fn formats_dont_panic() { + formats(); + named_formats(); + config_formats(); +} + +pub fn formats() -> &'static AHashMap { + &FORMATS_MAP +} + +pub fn ref_formats() -> &'static [&'static Format] { + &FORMATS_REFS +} + +pub fn named_formats() -> &'static AHashMap<&'static str, &'static Format> { + &FORMATS_NAMES +} + +pub fn config_formats() -> &'static AHashMap { + &FORMATS_CONFIG +} + +const fn fourcc_code(a: char, b: char, c: char, d: char) -> u32 { + (a as u32) | ((b as u32) << 8) | ((c as u32) << 16) | ((d as u32) << 24) +} + +pub fn debug(fourcc: u32) -> impl Debug { + fmt::from_fn(move |fmt| { + fmt.write_char(fourcc as u8 as char)?; + fmt.write_char((fourcc >> 8) as u8 as char)?; + fmt.write_char((fourcc >> 16) as u8 as char)?; + fmt.write_char((fourcc >> 24) as u8 as char)?; + Ok(()) + }) +} + +const ARGB8888_ID: u32 = 0; +const ARGB8888_DRM: u32 = fourcc_code('A', 'R', '2', '4'); + +const XRGB8888_ID: u32 = 1; +const XRGB8888_DRM: u32 = fourcc_code('X', 'R', '2', '4'); + +pub fn map_wayland_format_id(id: u32) -> u32 { + match id { + ARGB8888_ID => ARGB8888_DRM, + XRGB8888_ID => XRGB8888_DRM, + _ => id, + } +} + +pub static ARGB8888: &Format = &Format { + name: "argb8888", + shm_info: Some(FormatShmInfo { + gl_format: GL_BGRA_EXT, + gl_internal_format: GL_RGBA8, + gl_type: GL_UNSIGNED_BYTE, + }), + vk_format: vk::Format::B8G8R8A8_UNORM, + bpp: 4, + drm: ARGB8888_DRM, + wl_id: Some(ARGB8888_ID), + external_only_guess: false, + has_alpha: true, + opaque: Some(XRGB8888), + config: ConfigFormat::ARGB8888, +}; + +pub static XRGB8888: &Format = &Format { + name: "xrgb8888", + shm_info: Some(FormatShmInfo { + gl_format: GL_BGRA_EXT, + gl_internal_format: GL_RGBA8, + gl_type: GL_UNSIGNED_BYTE, + }), + vk_format: vk::Format::B8G8R8A8_UNORM, + bpp: 4, + drm: XRGB8888_DRM, + wl_id: Some(XRGB8888_ID), + external_only_guess: false, + has_alpha: false, + opaque: None, + config: ConfigFormat::XRGB8888, +}; + +static ABGR8888: &Format = &Format { + name: "abgr8888", + shm_info: Some(FormatShmInfo { + gl_format: GL_RGBA, + gl_internal_format: GL_RGBA8, + gl_type: GL_UNSIGNED_BYTE, + }), + vk_format: vk::Format::R8G8B8A8_UNORM, + bpp: 4, + drm: fourcc_code('A', 'B', '2', '4'), + wl_id: None, + external_only_guess: false, + has_alpha: true, + opaque: Some(XBGR8888), + config: ConfigFormat::ABGR8888, +}; + +static XBGR8888: &Format = &Format { + name: "xbgr8888", + shm_info: Some(FormatShmInfo { + gl_format: GL_RGBA, + gl_internal_format: GL_RGBA8, + gl_type: GL_UNSIGNED_BYTE, + }), + vk_format: vk::Format::R8G8B8A8_UNORM, + bpp: 4, + drm: fourcc_code('X', 'B', '2', '4'), + wl_id: None, + external_only_guess: false, + has_alpha: false, + opaque: None, + config: ConfigFormat::XBGR8888, +}; + +static R8: &Format = &Format { + name: "r8", + vk_format: vk::Format::R8_UNORM, + bpp: 1, + drm: fourcc_code('R', '8', ' ', ' '), + ..default(ConfigFormat::R8) +}; + +static GR88: &Format = &Format { + name: "gr88", + vk_format: vk::Format::R8G8_UNORM, + bpp: 2, + drm: fourcc_code('G', 'R', '8', '8'), + ..default(ConfigFormat::GR88) +}; + +static RGB888: &Format = &Format { + name: "rgb888", + vk_format: vk::Format::B8G8R8_UNORM, + bpp: 3, + drm: fourcc_code('R', 'G', '2', '4'), + ..default(ConfigFormat::RGB888) +}; + +static BGR888: &Format = &Format { + name: "bgr888", + vk_format: vk::Format::R8G8B8_UNORM, + bpp: 3, + drm: fourcc_code('B', 'G', '2', '4'), + ..default(ConfigFormat::BGR888) +}; + +static RGBA4444: &Format = &Format { + name: "rgba4444", + vk_format: vk::Format::R4G4B4A4_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('R', 'A', '1', '2'), + has_alpha: true, + opaque: Some(RGBX4444), + ..default(ConfigFormat::RGBA4444) +}; + +static RGBX4444: &Format = &Format { + name: "rgbx4444", + vk_format: vk::Format::R4G4B4A4_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('R', 'X', '1', '2'), + ..default(ConfigFormat::RGBX4444) +}; + +static BGRA4444: &Format = &Format { + name: "bgra4444", + vk_format: vk::Format::B4G4R4A4_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('B', 'A', '1', '2'), + has_alpha: true, + opaque: Some(BGRX4444), + ..default(ConfigFormat::BGRA4444) +}; + +static BGRX4444: &Format = &Format { + name: "bgrx4444", + vk_format: vk::Format::B4G4R4A4_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('B', 'X', '1', '2'), + ..default(ConfigFormat::BGRX4444) +}; + +static RGB565: &Format = &Format { + name: "rgb565", + vk_format: vk::Format::R5G6B5_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('R', 'G', '1', '6'), + ..default(ConfigFormat::RGB565) +}; + +static BGR565: &Format = &Format { + name: "bgr565", + vk_format: vk::Format::B5G6R5_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('B', 'G', '1', '6'), + ..default(ConfigFormat::BGR565) +}; + +static RGBA5551: &Format = &Format { + name: "rgba5551", + vk_format: vk::Format::R5G5B5A1_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('R', 'A', '1', '5'), + has_alpha: true, + opaque: Some(RGBX5551), + ..default(ConfigFormat::RGBA5551) +}; + +static RGBX5551: &Format = &Format { + name: "rgbx5551", + vk_format: vk::Format::R5G5B5A1_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('R', 'X', '1', '5'), + ..default(ConfigFormat::RGBX5551) +}; + +static BGRA5551: &Format = &Format { + name: "bgra5551", + vk_format: vk::Format::B5G5R5A1_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('B', 'A', '1', '5'), + has_alpha: true, + opaque: Some(BGRX5551), + ..default(ConfigFormat::BGRA5551) +}; + +static BGRX5551: &Format = &Format { + name: "bgrx5551", + vk_format: vk::Format::B5G5R5A1_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('B', 'X', '1', '5'), + ..default(ConfigFormat::BGRX5551) +}; + +static ARGB1555: &Format = &Format { + name: "argb1555", + vk_format: vk::Format::A1R5G5B5_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('A', 'R', '1', '5'), + has_alpha: true, + opaque: Some(XRGB1555), + ..default(ConfigFormat::ARGB1555) +}; + +static XRGB1555: &Format = &Format { + name: "xrgb1555", + vk_format: vk::Format::A1R5G5B5_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('X', 'R', '1', '5'), + ..default(ConfigFormat::XRGB1555) +}; + +static ARGB2101010: &Format = &Format { + name: "argb2101010", + vk_format: vk::Format::A2R10G10B10_UNORM_PACK32, + bpp: 4, + drm: fourcc_code('A', 'R', '3', '0'), + has_alpha: true, + opaque: Some(XRGB2101010), + ..default(ConfigFormat::ARGB2101010) +}; + +static XRGB2101010: &Format = &Format { + name: "xrgb2101010", + vk_format: vk::Format::A2R10G10B10_UNORM_PACK32, + bpp: 4, + drm: fourcc_code('X', 'R', '3', '0'), + ..default(ConfigFormat::XRGB2101010) +}; + +static ABGR2101010: &Format = &Format { + name: "abgr2101010", + vk_format: vk::Format::A2B10G10R10_UNORM_PACK32, + bpp: 4, + drm: fourcc_code('A', 'B', '3', '0'), + has_alpha: true, + opaque: Some(XBGR2101010), + ..default(ConfigFormat::ABGR2101010) +}; + +static XBGR2101010: &Format = &Format { + name: "xbgr2101010", + vk_format: vk::Format::A2B10G10R10_UNORM_PACK32, + bpp: 4, + drm: fourcc_code('X', 'B', '3', '0'), + ..default(ConfigFormat::XBGR2101010) +}; + +static ABGR16161616: &Format = &Format { + name: "abgr16161616", + vk_format: vk::Format::R16G16B16A16_UNORM, + bpp: 8, + drm: fourcc_code('A', 'B', '4', '8'), + has_alpha: true, + opaque: Some(XBGR16161616), + ..default(ConfigFormat::ABGR16161616) +}; + +static XBGR16161616: &Format = &Format { + name: "xbgr16161616", + vk_format: vk::Format::R16G16B16A16_UNORM, + bpp: 8, + drm: fourcc_code('X', 'B', '4', '8'), + ..default(ConfigFormat::XBGR16161616) +}; + +pub static ABGR16161616F: &Format = &Format { + name: "abgr16161616f", + vk_format: vk::Format::R16G16B16A16_SFLOAT, + bpp: 8, + drm: fourcc_code('A', 'B', '4', 'H'), + has_alpha: true, + opaque: Some(XBGR16161616F), + ..default(ConfigFormat::ABGR16161616F) +}; + +static XBGR16161616F: &Format = &Format { + name: "xbgr16161616f", + vk_format: vk::Format::R16G16B16A16_SFLOAT, + bpp: 8, + drm: fourcc_code('X', 'B', '4', 'H'), + ..default(ConfigFormat::XBGR16161616F) +}; + +static BGR161616: &Format = &Format { + name: "bgr161616", + vk_format: vk::Format::R16G16B16_UNORM, + bpp: 6, + drm: fourcc_code('B', 'G', '4', '8'), + ..default(ConfigFormat::BGR161616) +}; + +static R16F: &Format = &Format { + name: "r16f", + vk_format: vk::Format::R16_SFLOAT, + bpp: 2, + drm: fourcc_code('R', ' ', ' ', 'H'), + ..default(ConfigFormat::R16F) +}; + +static GR1616F: &Format = &Format { + name: "gr1616f", + vk_format: vk::Format::R16G16_SFLOAT, + bpp: 4, + drm: fourcc_code('G', 'R', ' ', 'H'), + ..default(ConfigFormat::GR1616F) +}; + +static BGR161616F: &Format = &Format { + name: "bgr161616f", + vk_format: vk::Format::R16G16B16_SFLOAT, + bpp: 6, + drm: fourcc_code('B', 'G', 'R', 'H'), + ..default(ConfigFormat::BGR161616F) +}; + +static R32F: &Format = &Format { + name: "r32f", + vk_format: vk::Format::R32_SFLOAT, + bpp: 4, + drm: fourcc_code('R', ' ', ' ', 'F'), + ..default(ConfigFormat::R32F) +}; + +static GR3232F: &Format = &Format { + name: "gr3232f", + vk_format: vk::Format::R32G32_SFLOAT, + bpp: 8, + drm: fourcc_code('G', 'R', ' ', 'F'), + ..default(ConfigFormat::GR3232F) +}; + +static BGR323232F: &Format = &Format { + name: "bgr323232f", + vk_format: vk::Format::R32G32B32_SFLOAT, + bpp: 12, + drm: fourcc_code('B', 'G', 'R', 'F'), + ..default(ConfigFormat::BGR323232F) +}; + +static ABGR32323232F: &Format = &Format { + name: "abgr32323232f", + vk_format: vk::Format::R32G32B32A32_SFLOAT, + bpp: 16, + drm: fourcc_code('A', 'B', '8', 'F'), + has_alpha: true, + ..default(ConfigFormat::ABGR32323232F) +}; + +pub static FORMATS: &[Format] = &[ + *ARGB8888, + *XRGB8888, + *ABGR8888, + *XBGR8888, + *R8, + *GR88, + *RGB888, + *BGR888, + #[cfg(target_endian = "little")] + *RGBA4444, + #[cfg(target_endian = "little")] + *RGBX4444, + #[cfg(target_endian = "little")] + *BGRA4444, + #[cfg(target_endian = "little")] + *BGRX4444, + #[cfg(target_endian = "little")] + *RGB565, + #[cfg(target_endian = "little")] + *BGR565, + #[cfg(target_endian = "little")] + *RGBA5551, + #[cfg(target_endian = "little")] + *RGBX5551, + #[cfg(target_endian = "little")] + *BGRA5551, + #[cfg(target_endian = "little")] + *BGRX5551, + #[cfg(target_endian = "little")] + *ARGB1555, + #[cfg(target_endian = "little")] + *XRGB1555, + #[cfg(target_endian = "little")] + *ARGB2101010, + #[cfg(target_endian = "little")] + *XRGB2101010, + #[cfg(target_endian = "little")] + *ABGR2101010, + #[cfg(target_endian = "little")] + *XBGR2101010, + #[cfg(target_endian = "little")] + *ABGR16161616, + #[cfg(target_endian = "little")] + *XBGR16161616, + #[cfg(target_endian = "little")] + *ABGR16161616F, + #[cfg(target_endian = "little")] + *XBGR16161616F, + #[cfg(target_endian = "little")] + *BGR161616, + #[cfg(target_endian = "little")] + *R16F, + #[cfg(target_endian = "little")] + *GR1616F, + #[cfg(target_endian = "little")] + *BGR161616F, + #[cfg(target_endian = "little")] + *R32F, + #[cfg(target_endian = "little")] + *GR3232F, + #[cfg(target_endian = "little")] + *BGR323232F, + #[cfg(target_endian = "little")] + *ABGR32323232F, +]; diff --git a/crates/geometry/Cargo.toml b/crates/geometry/Cargo.toml new file mode 100644 index 00000000..a69caa9b --- /dev/null +++ b/crates/geometry/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "jay-geometry" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Geometry primitives for Jay" +repository = "https://github.com/mahkoh/jay" + +[dependencies] +jay-algorithms = { path = "../algorithms" } +smallvec = { version = "1.11.1", features = ["const_generics", "const_new", "union"] } diff --git a/crates/geometry/src/lib.rs b/crates/geometry/src/lib.rs new file mode 100644 index 00000000..e18ba86f --- /dev/null +++ b/crates/geometry/src/lib.rs @@ -0,0 +1,365 @@ +mod region; + +#[cfg(test)] +mod tests; + +pub use region::{DamageQueue, RegionBuilder}; +use { + jay_algorithms::rect::{NoTag, RectRaw, Tag}, + smallvec::SmallVec, + std::fmt::{Debug, Formatter}, +}; + +#[derive(Copy, Clone, Eq, PartialEq, Default)] +#[repr(transparent)] +pub struct Rect +where + T: Tag, +{ + raw: RectRaw, +} + +#[derive(Clone, Eq, PartialEq, Debug, Default)] +pub struct Region +where + T: Tag, +{ + rects: SmallVec<[RectRaw; 1]>, + extents: Rect, +} + +impl Debug for Rect { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Debug::fmt(&self.raw, f) + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] +pub struct RectOverflow { + pub left: i32, + pub right: i32, + pub top: i32, + pub bottom: i32, +} + +impl RectOverflow { + pub fn is_contained(&self) -> bool { + self.left <= 0 && self.right <= 0 && self.top <= 0 && self.bottom <= 0 + } + + pub fn x_overflow(&self) -> bool { + self.left > 0 || self.right > 0 + } + + pub fn y_overflow(&self) -> bool { + self.top > 0 || self.bottom > 0 + } +} + +impl Rect +where + T: Tag, +{ + pub fn untag(&self) -> Rect { + Rect { + raw: RectRaw { + x1: self.raw.x1, + y1: self.raw.y1, + x2: self.raw.x2, + y2: self.raw.y2, + tag: NoTag, + }, + } + } +} + +impl Rect { + pub fn new_empty(x: i32, y: i32) -> Self { + Self { + raw: RectRaw { + x1: x, + y1: y, + x2: x, + y2: y, + tag: NoTag, + }, + } + } + + pub fn new(x1: i32, y1: i32, x2: i32, y2: i32) -> Option { + if x2 < x1 || y2 < y1 { + return None; + } + Some(Self { + raw: RectRaw { + x1, + y1, + x2, + y2, + tag: NoTag, + }, + }) + } + + #[cfg_attr(not(test), expect(dead_code))] + fn new_unchecked_danger(x1: i32, y1: i32, x2: i32, y2: i32) -> Self { + Self { + raw: RectRaw { + x1, + y1, + x2, + y2, + tag: NoTag, + }, + } + } + + pub fn new_sized(x1: i32, y1: i32, width: i32, height: i32) -> Option { + if width < 0 || height < 0 { + return None; + } + Self::new(x1, y1, x1 + width, y1 + height) + } + + pub fn new_saturating(x1: i32, y1: i32, x2: i32, y2: i32) -> Self { + Self { + raw: RectRaw { + x1, + y1, + x2: x2.max(x1), + y2: y2.max(y1), + tag: NoTag, + }, + } + } + + pub fn new_sized_saturating(x1: i32, y1: i32, width: i32, height: i32) -> Self { + Self { + raw: RectRaw { + x1, + y1, + x2: x1.saturating_add(width.max(0)), + y2: y1.saturating_add(height.max(0)), + tag: NoTag, + }, + } + } + + pub fn union(&self, other: Self) -> Self { + Self { + raw: RectRaw { + x1: self.raw.x1.min(other.raw.x1), + y1: self.raw.y1.min(other.raw.y1), + x2: self.raw.x2.max(other.raw.x2), + y2: self.raw.y2.max(other.raw.y2), + tag: NoTag, + }, + } + } + + pub fn intersect(&self, other: Self) -> Self { + let x1 = self.raw.x1.max(other.raw.x1); + let y1 = self.raw.y1.max(other.raw.y1); + let x2 = self.raw.x2.min(other.raw.x2).max(x1); + let y2 = self.raw.y2.min(other.raw.y2).max(y1); + Self { + raw: RectRaw { + x1, + y1, + x2, + y2, + tag: NoTag, + }, + } + } + + pub fn with_size_saturating(&self, width: i32, height: i32) -> Self { + Self::new_sized_saturating(self.raw.x1, self.raw.y1, width, height) + } + + pub fn with_tag(&self, tag: u32) -> Rect { + Rect { + raw: RectRaw { + x1: self.raw.x1, + y1: self.raw.y1, + x2: self.raw.x2, + y2: self.raw.y2, + tag, + }, + } + } +} + +impl Rect +where + T: Tag, +{ + #[cfg_attr(not(test), expect(dead_code))] + fn new_unchecked_danger_tagged(x1: i32, y1: i32, x2: i32, y2: i32, tag: T) -> Self { + Self { + raw: RectRaw { + x1, + y1, + x2, + y2, + tag, + }, + } + } + + pub fn intersects(&self, other: &Self) -> bool { + self.raw.x1 < other.raw.x2 + && other.raw.x1 < self.raw.x2 + && self.raw.y1 < other.raw.y2 + && other.raw.y1 < self.raw.y2 + } + + pub fn contains(&self, x: i32, y: i32) -> bool { + self.raw.x1 <= x && self.raw.y1 <= y && self.raw.x2 > x && self.raw.y2 > y + } + + pub fn not_contains(&self, x: i32, y: i32) -> bool { + !self.contains(x, y) + } + + pub fn dist_squared(&self, x: i32, y: i32) -> i128 { + let x = x as i64; + let y = y as i64; + let x1 = self.raw.x1 as i64; + let x2 = self.raw.x2 as i64; + let y1 = self.raw.y1 as i64; + let y2 = self.raw.y2 as i64; + let mut dx = 0; + if x1 > x { + dx = x1 - x; + } else if x2 < x { + dx = x - x2; + } + let mut dy = 0; + if y1 > y { + dy = y1 - y; + } else if y2 < y { + dy = y - y2; + } + let dx = dx as i128; + let dy = dy as i128; + dx * dx + dy * dy + } + + pub fn contains_rect(&self, rect: &Rect) -> bool + where + U: Tag, + { + self.raw.x1 <= rect.raw.x1 + && self.raw.y1 <= rect.raw.x1 + && rect.raw.x2 <= self.raw.x2 + && rect.raw.y2 <= self.raw.y2 + } + + pub fn get_overflow(&self, child: &Rect) -> RectOverflow + where + U: Tag, + { + RectOverflow { + left: self.raw.x1 - child.raw.x1, + right: child.raw.x2 - self.raw.x2, + top: self.raw.y1 - child.raw.y1, + bottom: child.raw.y2 - self.raw.y2, + } + } + + pub fn is_empty(&self) -> bool { + self.raw.x1 == self.raw.x2 || self.raw.y1 == self.raw.y2 + } + + pub fn is_not_empty(&self) -> bool { + !self.is_empty() + } + + pub fn to_origin(&self) -> Self { + Self { + raw: RectRaw { + x1: 0, + y1: 0, + x2: self.raw.x2 - self.raw.x1, + y2: self.raw.y2 - self.raw.y1, + tag: self.raw.tag, + }, + } + } + + pub fn move_(&self, dx: i32, dy: i32) -> Self { + Self { + raw: RectRaw { + x1: self.raw.x1.saturating_add(dx), + y1: self.raw.y1.saturating_add(dy), + x2: self.raw.x2.saturating_add(dx), + y2: self.raw.y2.saturating_add(dy), + tag: self.raw.tag, + }, + } + } + + pub fn at_point(&self, x1: i32, y1: i32) -> Self { + Self { + raw: RectRaw { + x1, + y1, + x2: x1 + self.raw.x2 - self.raw.x1, + y2: y1 + self.raw.y2 - self.raw.y1, + tag: self.raw.tag, + }, + } + } + + pub fn translate(&self, x: i32, y: i32) -> (i32, i32) { + (x.wrapping_sub(self.raw.x1), y.wrapping_sub(self.raw.y1)) + } + + pub fn translate_inv(&self, x: i32, y: i32) -> (i32, i32) { + (x.wrapping_add(self.raw.x1), y.wrapping_add(self.raw.y1)) + } + + pub fn x1(&self) -> i32 { + self.raw.x1 + } + + pub fn x2(&self) -> i32 { + self.raw.x2 + } + + pub fn y1(&self) -> i32 { + self.raw.y1 + } + + pub fn y2(&self) -> i32 { + self.raw.y2 + } + + pub fn width(&self) -> i32 { + self.raw.x2 - self.raw.x1 + } + + pub fn height(&self) -> i32 { + self.raw.y2 - self.raw.y1 + } + + pub fn position(&self) -> (i32, i32) { + (self.raw.x1, self.raw.y1) + } + + pub fn size(&self) -> (i32, i32) { + (self.width(), self.height()) + } + + pub fn center(&self) -> (i32, i32) { + ( + self.raw.x1 + self.width() / 2, + self.raw.y1 + self.height() / 2, + ) + } + + pub fn tag(&self) -> T { + self.raw.tag + } +} diff --git a/src/rect/region.rs b/crates/geometry/src/region.rs similarity index 94% rename from src/rect/region.rs rename to crates/geometry/src/region.rs index bc1613c4..32425f21 100644 --- a/src/rect/region.rs +++ b/crates/geometry/src/region.rs @@ -1,11 +1,5 @@ use { - crate::{ - rect::{Rect, Region}, - utils::{ - array, - ptr_ext::{MutPtrExt, PtrExt}, - }, - }, + crate::{Rect, Region}, jay_algorithms::rect::{ RectRaw, Tag, region::{ @@ -15,6 +9,7 @@ use { }, smallvec::SmallVec, std::{ + array, borrow::Cow, cell::UnsafeCell, fmt::{Debug, Formatter}, @@ -176,7 +171,6 @@ where } } - #[cfg_attr(not(feature = "it"), expect(dead_code))] pub fn extents(&self) -> Rect { self.extents } @@ -274,7 +268,6 @@ impl RegionBuilder { self.base.clone() } - #[expect(dead_code)] pub fn clear(&mut self) { self.pending.clear(); self.base = Region::empty(); @@ -321,26 +314,26 @@ impl DamageQueue { } pub fn damage(&self, rects: &[Rect]) { - let datas = unsafe { self.datas.get().deref_mut() }; + let datas = unsafe { &mut *self.datas.get() }; for data in datas { data.extend(rects); } } pub fn clear(&self) { - let data = unsafe { &mut self.datas.get().deref_mut()[self.this] }; + let data = unsafe { &mut (&mut *self.datas.get())[self.this] }; data.clear(); } pub fn clear_all(&self) { - let datas = unsafe { self.datas.get().deref_mut() }; + let datas = unsafe { &mut *self.datas.get() }; for data in datas { data.clear(); } } pub fn get(&self) -> Region { - let data = unsafe { &self.datas.get().deref()[self.this] }; + let data = unsafe { &(&*self.datas.get())[self.this] }; Region::from_rects2(data) } } diff --git a/src/rect/tests.rs b/crates/geometry/src/tests.rs similarity index 99% rename from src/rect/tests.rs rename to crates/geometry/src/tests.rs index c673ef5b..1b2d205f 100644 --- a/src/rect/tests.rs +++ b/crates/geometry/src/tests.rs @@ -1,5 +1,5 @@ use { - crate::rect::{Rect, Region}, + crate::{Rect, Region}, jay_algorithms::rect::{NoTag, RectRaw}, }; diff --git a/crates/gfx-types/Cargo.toml b/crates/gfx-types/Cargo.toml new file mode 100644 index 00000000..52d9e565 --- /dev/null +++ b/crates/gfx-types/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "jay-gfx-types" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +uapi = "0.2.13" diff --git a/crates/gfx-types/src/lib.rs b/crates/gfx-types/src/lib.rs new file mode 100644 index 00000000..6fd3016f --- /dev/null +++ b/crates/gfx-types/src/lib.rs @@ -0,0 +1,38 @@ +use { + std::{cell::Cell, error::Error, rc::Rc}, + uapi::OwnedFd, +}; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default)] +pub enum AlphaMode { + #[default] + PremultipliedElectrical, + PremultipliedOptical, + Straight, +} + +pub trait ShmMemory { + fn len(&self) -> usize; + fn safe_access(&self) -> ShmMemoryBacking; + fn access(&self, f: &mut dyn FnMut(&[Cell])) -> Result<(), Box>; +} + +pub enum ShmMemoryBacking { + Ptr(*const [Cell]), + Fd(Rc, usize), +} + +impl ShmMemory for Vec> { + fn len(&self) -> usize { + self.len() + } + + fn safe_access(&self) -> ShmMemoryBacking { + ShmMemoryBacking::Ptr(&**self) + } + + fn access(&self, f: &mut dyn FnMut(&[Cell])) -> Result<(), Box> { + f(self); + Ok(()) + } +} diff --git a/crates/input-types/Cargo.toml b/crates/input-types/Cargo.toml new file mode 100644 index 00000000..9683f580 --- /dev/null +++ b/crates/input-types/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "jay-input-types" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Input data types for the Jay compositor" +repository = "https://github.com/mahkoh/jay" + +[dependencies] +jay-output-types = { path = "../output-types" } +jay-units = { path = "../units" } +jay-utils = { path = "../utils" } + +linearize = { version = "0.1.3", features = ["derive"] } diff --git a/crates/input-types/src/lib.rs b/crates/input-types/src/lib.rs new file mode 100644 index 00000000..ee58c13f --- /dev/null +++ b/crates/input-types/src/lib.rs @@ -0,0 +1,482 @@ +use { + jay_output_types::ConnectorId, + jay_units::Fixed, + jay_utils::{numcell::NumCell, static_text::StaticText}, + linearize::Linearize, + std::{ + fmt::{Display, Formatter}, + ops::{BitOr, BitOrAssign}, + }, +}; + +macro_rules! linear_ids { + ($ids:ident, $id:ident $(,)?) => { + linear_ids!($ids, $id, u32); + }; + ($ids:ident, $id:ident, $ty:ty $(,)?) => { + #[derive(Debug)] + pub struct $ids { + next: NumCell<$ty>, + } + + impl Default for $ids { + fn default() -> Self { + Self { + next: NumCell::new(1), + } + } + } + + impl $ids { + pub fn next(&self) -> $id { + $id(self.next.fetch_add(1)) + } + } + + #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] + pub struct $id($ty); + + impl $id { + pub fn raw(&self) -> $ty { + self.0 + } + + pub fn from_raw(id: $ty) -> Self { + Self(id) + } + } + + impl Display for $id { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.0, f) + } + } + }; +} + +#[derive(Debug, Copy, Clone, PartialEq, Linearize)] +pub enum InputDeviceAccelProfile { + Flat, + Adaptive, +} + +impl StaticText for InputDeviceAccelProfile { + fn text(&self) -> &'static str { + match self { + InputDeviceAccelProfile::Flat => "Flat", + InputDeviceAccelProfile::Adaptive => "Adaptive", + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Linearize)] +pub enum InputDeviceClickMethod { + None, + ButtonAreas, + Clickfinger, +} + +impl StaticText for InputDeviceClickMethod { + fn text(&self) -> &'static str { + match self { + InputDeviceClickMethod::None => "none", + InputDeviceClickMethod::ButtonAreas => "button-areas", + InputDeviceClickMethod::Clickfinger => "clickfinger", + } + } +} + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Linearize)] +pub enum InputDeviceCapability { + Keyboard, + Pointer, + Touch, + TabletTool, + TabletPad, + Gesture, + Switch, +} + +impl StaticText for InputDeviceCapability { + fn text(&self) -> &'static str { + match self { + InputDeviceCapability::Keyboard => "keyboard", + InputDeviceCapability::Pointer => "pointer", + InputDeviceCapability::Touch => "touch", + InputDeviceCapability::TabletTool => "tablet tool", + InputDeviceCapability::TabletPad => "tablet pad", + InputDeviceCapability::Gesture => "gesture", + InputDeviceCapability::Switch => "switch", + } + } +} + +linear_ids!(InputDeviceGroupIds, InputDeviceGroupId, usize); +linear_ids!(InputDeviceIds, InputDeviceId); + +pub type TransformMatrix = [[f64; 2]; 2]; + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum KeyState { + Released, + Pressed, + Repeated, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum ButtonState { + Released, + Pressed, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Linearize)] +pub enum ScrollAxis { + Vertical = 0, + Horizontal = 1, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum AxisSource { + Wheel, + Finger, + Continuous, +} + +pub const AXIS_120: i32 = 120; + +#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)] +pub struct Leds(pub u32); + +pub const LED_NUM_LOCK: Leds = Leds(1 << 0); +pub const LED_CAPS_LOCK: Leds = Leds(1 << 1); +pub const LED_SCROLL_LOCK: Leds = Leds(1 << 2); +pub const LED_COMPOSE: Leds = Leds(1 << 3); +pub const LED_KANA: Leds = Leds(1 << 4); + +impl Leds { + pub const fn none() -> Self { + Self(0) + } + + pub const fn raw(self) -> u32 { + self.0 + } +} + +impl BitOr for Leds { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } +} + +impl BitOrAssign for Leds { + fn bitor_assign(&mut self, rhs: Self) { + self.0 |= rhs.0; + } +} + +linear_ids!(TabletIds, TabletId); + +#[derive(Debug, Clone)] +pub struct TabletInit { + pub id: TabletId, + pub group: InputDeviceGroupId, + pub name: String, + pub pid: u32, + pub vid: u32, + pub bustype: Option, + pub path: String, +} + +linear_ids!(TabletToolIds, TabletToolId, usize); + +#[derive(Debug, Clone)] +pub struct TabletToolInit { + pub tablet_id: TabletId, + pub id: TabletToolId, + pub type_: TabletToolType, + pub hardware_serial: u64, + pub hardware_id_wacom: u64, + pub capabilities: Vec, +} + +linear_ids!(TabletPadIds, TabletPadId); + +#[derive(Debug, Clone)] +pub struct TabletPadInit { + pub id: TabletPadId, + pub group: InputDeviceGroupId, + pub path: String, + pub buttons: u32, + pub strips: u32, + pub rings: u32, + pub dials: u32, + pub groups: Vec, +} + +#[derive(Debug, Clone)] +pub struct TabletPadGroupInit { + pub buttons: Vec, + pub rings: Vec, + pub strips: Vec, + pub dials: Vec, + pub modes: u32, + pub mode: u32, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum PadButtonState { + Released, + Pressed, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum ToolButtonState { + Released, + Pressed, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum TabletToolType { + Pen, + Eraser, + Brush, + Pencil, + Airbrush, + Finger, + Mouse, + Lens, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum TabletToolCapability { + Tilt, + Pressure, + Distance, + Rotation, + Slider, + Wheel, +} + +#[derive(Copy, Clone, Debug)] +pub enum TabletRingEventSource { + Finger, +} + +#[derive(Copy, Clone, Debug)] +pub enum TabletStripEventSource { + Finger, +} + +#[derive(Debug, Default)] +pub struct TabletToolChanges { + pub down: Option, + pub pos: Option>, + pub pressure: Option, + pub distance: Option, + pub tilt: Option>, + pub rotation: Option, + pub slider: Option, + pub wheel: Option, +} + +#[derive(Copy, Clone, Debug)] +pub struct TabletTool2dChange { + pub x: T, + pub y: T, +} + +#[derive(Copy, Clone, Debug)] +pub struct TabletToolPositionChange { + pub x: f64, + pub dx: f64, +} + +#[derive(Copy, Clone, Debug)] +pub struct TabletToolWheelChange { + pub degrees: f64, + pub clicks: i32, +} + +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub enum SwitchEvent { + LidOpened, + LidClosed, + ConvertedToLaptop, + ConvertedToTablet, +} + +#[derive(Debug)] +pub enum InputEvent { + Key { + time_usec: u64, + key: u32, + state: KeyState, + }, + ConnectorPosition { + time_usec: u64, + connector: ConnectorId, + x: Fixed, + y: Fixed, + }, + Motion { + time_usec: u64, + dx: Fixed, + dy: Fixed, + dx_unaccelerated: Fixed, + dy_unaccelerated: Fixed, + }, + MotionAbsolute { + time_usec: u64, + x_normed: f32, + y_normed: f32, + }, + Button { + time_usec: u64, + button: u32, + state: ButtonState, + }, + + AxisPx { + dist: Fixed, + axis: ScrollAxis, + inverted: bool, + }, + AxisSource { + source: AxisSource, + }, + AxisStop { + axis: ScrollAxis, + }, + Axis120 { + dist: i32, + axis: ScrollAxis, + inverted: bool, + }, + AxisFrame { + time_usec: u64, + }, + SwipeBegin { + time_usec: u64, + finger_count: u32, + }, + SwipeUpdate { + time_usec: u64, + dx: Fixed, + dy: Fixed, + dx_unaccelerated: Fixed, + dy_unaccelerated: Fixed, + }, + SwipeEnd { + time_usec: u64, + cancelled: bool, + }, + PinchBegin { + time_usec: u64, + finger_count: u32, + }, + PinchUpdate { + time_usec: u64, + dx: Fixed, + dy: Fixed, + dx_unaccelerated: Fixed, + dy_unaccelerated: Fixed, + scale: Fixed, + rotation: Fixed, + }, + PinchEnd { + time_usec: u64, + cancelled: bool, + }, + HoldBegin { + time_usec: u64, + finger_count: u32, + }, + HoldEnd { + time_usec: u64, + cancelled: bool, + }, + + SwitchEvent { + time_usec: u64, + event: SwitchEvent, + }, + + TabletToolAdded { + time_usec: u64, + init: Box, + }, + TabletToolChanged { + time_usec: u64, + id: TabletToolId, + changes: Box, + }, + TabletToolButton { + time_usec: u64, + id: TabletToolId, + button: u32, + state: ToolButtonState, + }, + TabletToolRemoved { + time_usec: u64, + id: TabletToolId, + }, + + TabletPadButton { + time_usec: u64, + id: TabletPadId, + button: u32, + state: PadButtonState, + }, + TabletPadModeSwitch { + time_usec: u64, + pad: TabletPadId, + group: u32, + mode: u32, + }, + TabletPadRing { + time_usec: u64, + pad: TabletPadId, + ring: u32, + source: Option, + angle: Option, + }, + TabletPadStrip { + time_usec: u64, + pad: TabletPadId, + strip: u32, + source: Option, + position: Option, + }, + TabletPadDial { + time_usec: u64, + pad: TabletPadId, + dial: u32, + value120: i32, + }, + TouchDown { + time_usec: u64, + id: i32, + x_normed: Fixed, + y_normed: Fixed, + }, + TouchUp { + time_usec: u64, + id: i32, + }, + TouchMotion { + time_usec: u64, + id: i32, + x_normed: Fixed, + y_normed: Fixed, + }, + TouchCancel { + time_usec: u64, + id: i32, + }, + TouchFrame { + time_usec: u64, + }, +} diff --git a/crates/io-uring/Cargo.toml b/crates/io-uring/Cargo.toml new file mode 100644 index 00000000..126aac38 --- /dev/null +++ b/crates/io-uring/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "jay-io-uring" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +jay-async-engine = { path = "../async-engine" } +jay-time = { path = "../time" } +jay-utils = { path = "../utils" } + +log = { version = "0.4.20", features = ["std"] } +run-on-drop = "1.0.0" +thiserror = "2.0.11" +uapi = "0.2.13" diff --git a/src/io_uring/debounce.rs b/crates/io-uring/src/debounce.rs similarity index 93% rename from src/io_uring/debounce.rs rename to crates/io-uring/src/debounce.rs index f5b65f40..816a9d38 100644 --- a/src/io_uring/debounce.rs +++ b/crates/io-uring/src/debounce.rs @@ -1,5 +1,6 @@ use { - crate::{io_uring::IoUringData, utils::numcell::NumCell}, + crate::IoUringData, + jay_utils::numcell::NumCell, std::{cell::Cell, future::poll_fn, rc::Rc, task::Poll}, }; diff --git a/crates/io-uring/src/lib.rs b/crates/io-uring/src/lib.rs new file mode 100644 index 00000000..c6c623fa --- /dev/null +++ b/crates/io-uring/src/lib.rs @@ -0,0 +1,590 @@ +pub use ops::{ + TaskResultExt, + poll_external::{PendingPoll, PollCallback}, + timeout_external::{PendingTimeout, TimeoutCallback}, +}; +use { + crate::{ + debounce::Debouncer, + ops::{ + accept::AcceptTask, async_cancel::AsyncCancelTask, connect::ConnectTask, + poll::PollTask, poll_external::PollExternalTask, read_write::ReadWriteTask, + read_write_no_cancel::ReadWriteNoCancelTask, recvmsg::RecvmsgTask, + sendmsg::SendmsgTask, timeout::TimeoutTask, timeout_external::TimeoutExternalTask, + timeout_link::TimeoutLinkTask, + }, + pending_result::PendingResults, + sys::{ + IORING_ENTER_GETEVENTS, IORING_FEAT_NODROP, IORING_OFF_CQ_RING, IORING_OFF_SQ_RING, + IORING_OFF_SQES, IORING_SETUP_COOP_TASKRUN, IORING_SETUP_DEFER_TASKRUN, + IORING_SETUP_SINGLE_ISSUER, IORING_SETUP_SUBMIT_ALL, IOSQE_IO_LINK, io_uring_cqe, + io_uring_enter, io_uring_params, io_uring_setup, io_uring_sqe, + }, + }, + jay_async_engine::AsyncEngine, + jay_utils::{ + asyncevent::AsyncEvent, + bitflags::BitflagsExt, + buf::Buf, + copyhashmap::CopyHashMap, + errorfmt::ErrorFmt, + mmap::{Mmapped, mmap}, + numcell::NumCell, + oserror::OsError, + ptr_ext::{MutPtrExt, PtrExt}, + stack::Stack, + syncqueue::SyncQueue, + }, + std::{ + cell::{Cell, RefCell, UnsafeCell}, + rc::Rc, + sync::atomic::{ + AtomicU32, + Ordering::{Acquire, Relaxed, Release}, + }, + task::Waker, + }, + thiserror::Error, + uapi::{ + OwnedFd, + c::{self}, + }, +}; + +macro_rules! map_err { + ($n:expr) => {{ + let n = $n; + if n < 0 { + Err(jay_utils::oserror::OsError::from(-n as uapi::c::c_int)) + } else { + Ok(n) + } + }}; +} + +mod debounce; +pub mod line_logger; +pub mod object_drop_queue; +mod ops; +mod pending_result; +mod sys; +pub mod timer; + +#[derive(Debug, Error)] +pub enum IoUringError { + #[error(transparent)] + OsError(#[from] OsError), + #[error("Could not create an io-uring")] + CreateUring(#[source] OsError), + #[error("The kernel does not support the IORING_FEAT_NODROP feature")] + NoDrop, + #[error("Could not map the submission queue ring")] + MapSqRing(#[source] OsError), + #[error("Could not map the submission queue entries")] + MapSqEntries(#[source] OsError), + #[error("Could not map the completion queue ring")] + MapCqRing(#[source] OsError), + #[error("The io-uring has already been destroyed")] + Destroyed, + #[error("io_uring_enter failed")] + Enter(#[source] OsError), + #[error("Kernel sent invalid cmsg data")] + InvalidCmsgData, +} + +pub struct IoUring { + ring: Rc, +} + +impl Drop for IoUring { + fn drop(&mut self) { + self.ring.kill(); + } +} + +impl IoUring { + pub fn new(eng: &Rc, entries: u32) -> Result, IoUringError> { + let feature_levels = [ + IORING_SETUP_SUBMIT_ALL, // 5.18 + IORING_SETUP_COOP_TASKRUN, // 5.19 + IORING_SETUP_SINGLE_ISSUER, // 6.0 + IORING_SETUP_DEFER_TASKRUN, // 6.1 + ]; + let mut feature_levels = &feature_levels[..]; + let mut params; + let fd = loop { + params = io_uring_params::default(); + for &flags in feature_levels { + params.flags |= flags; + } + match io_uring_setup(entries, &mut params) { + Ok(f) => break f, + Err(e) => { + if let Some((_, levels)) = feature_levels.split_last() { + feature_levels = levels; + } else { + return Err(IoUringError::CreateUring(e)); + } + } + } + }; + if !params.features.contains(IORING_FEAT_NODROP) { + return Err(IoUringError::NoDrop); + } + let sqmap_map = mmap( + (params.sq_off.array + params.sq_entries * 4) as _, + c::PROT_READ | c::PROT_WRITE, + c::MAP_SHARED | c::MAP_POPULATE, + fd.raw(), + IORING_OFF_SQ_RING as _, + ); + let sqmap_map = match sqmap_map { + Ok(map) => map, + Err(e) => return Err(IoUringError::MapSqRing(e)), + }; + let sqesmap_map = mmap( + params.sq_entries as usize * size_of::(), + c::PROT_READ | c::PROT_WRITE, + c::MAP_SHARED | c::MAP_POPULATE, + fd.raw(), + IORING_OFF_SQES as _, + ); + let sqesmap_map = match sqesmap_map { + Ok(map) => map, + Err(e) => return Err(IoUringError::MapSqEntries(e)), + }; + let cqmap_map = mmap( + params.cq_off.cqes as usize + params.cq_entries as usize * size_of::(), + c::PROT_READ | c::PROT_WRITE, + c::MAP_SHARED | c::MAP_POPULATE, + fd.raw(), + IORING_OFF_CQ_RING as _, + ); + let cqmap_map = match cqmap_map { + Ok(map) => map, + Err(e) => return Err(IoUringError::MapCqRing(e)), + }; + let sqmask = unsafe { + *(sqmap_map.ptr as *const u8) + .add(params.sq_off.ring_mask as _) + .cast() + }; + let sqhead = unsafe { + (sqmap_map.ptr as *const u8) + .add(params.sq_off.head as _) + .cast() + }; + let sqtail = unsafe { + (sqmap_map.ptr as *const u8) + .add(params.sq_off.tail as _) + .cast() + }; + let sqmap = unsafe { + let base = (sqmap_map.ptr as *const u8) + .add(params.sq_off.array as _) + .cast(); + std::slice::from_raw_parts(base, params.sq_entries as _) + }; + let sqesmap = unsafe { + let base = (sqesmap_map.ptr as *const u8).cast(); + std::slice::from_raw_parts(base, params.sq_entries as _) + }; + let cqmask = unsafe { + *(cqmap_map.ptr as *const u8) + .add(params.cq_off.ring_mask as _) + .cast() + }; + let cqhead = unsafe { + (cqmap_map.ptr as *const u8) + .add(params.cq_off.head as _) + .cast() + }; + let cqtail = unsafe { + (cqmap_map.ptr as *const u8) + .add(params.cq_off.tail as _) + .cast() + }; + let cqmap = unsafe { + let base = (cqmap_map.ptr as *const u8) + .add(params.cq_off.cqes as _) + .cast(); + std::slice::from_raw_parts(base, params.cq_entries as _) + }; + let data = Rc::new(IoUringData { + destroyed: Cell::new(false), + fd, + eng: eng.clone(), + _sqesmap_map: sqesmap_map, + _sqmap_map: sqmap_map, + sqmask, + sqlen: params.sq_entries, + sqhead, + sqtail, + sqmap, + sqesmap, + _cqmap_map: cqmap_map, + cqmask, + cqhead, + cqtail, + cqmap, + cqes_consumed: Default::default(), + next: Default::default(), + to_encode: Default::default(), + pending_in_kernel: Default::default(), + tasks: Default::default(), + pending_results: Default::default(), + cached_read_writes: Default::default(), + cached_read_writes_no_cancel: Default::default(), + cached_cancels: Default::default(), + cached_polls: Default::default(), + cached_polls_external: Default::default(), + cached_sendmsg: Default::default(), + cached_recvmsg: Default::default(), + cached_timeouts: Default::default(), + cached_timeouts_external: Default::default(), + cached_timeout_links: Default::default(), + cached_cmsg_bufs: Default::default(), + cached_connects: Default::default(), + cached_accepts: Default::default(), + fd_ids_scratch: Default::default(), + iteration: Default::default(), + yields: Default::default(), + }); + Ok(Rc::new(Self { ring: data })) + } + + pub fn stop(&self) { + self.ring.kill(); + } + + pub fn run(&self) -> Result<(), IoUringError> { + let res = self.ring.run(); + self.ring.kill(); + res + } + + pub fn cancel(&self, id: IoUringTaskId) { + self.ring.cancel_task(id); + } + + pub fn debouncer(&self, max: u64) -> Debouncer { + Debouncer { + cur: Default::default(), + max, + iteration: Cell::new(self.ring.iteration.get()), + ring: self.ring.clone(), + } + } +} + +struct IoUringData { + destroyed: Cell, + + fd: OwnedFd, + eng: Rc, + + _sqesmap_map: Mmapped, + _sqmap_map: Mmapped, + sqmask: u32, + sqlen: u32, + sqhead: *const AtomicU32, + sqtail: *const AtomicU32, + sqmap: *const [Cell], + sqesmap: *const [UnsafeCell], + + _cqmap_map: Mmapped, + cqmask: u32, + cqhead: *const AtomicU32, + cqtail: *const AtomicU32, + cqmap: *const [Cell], + + cqes_consumed: AsyncEvent, + + next: IoUringTaskIds, + to_encode: SyncQueue, + pending_in_kernel: CopyHashMap, + tasks: CopyHashMap>, + + pending_results: PendingResults, + + cached_read_writes: Stack>, + cached_read_writes_no_cancel: Stack>, + cached_cancels: Stack>, + cached_polls: Stack>, + cached_polls_external: Stack>, + cached_sendmsg: Stack>, + cached_recvmsg: Stack>, + cached_timeouts: Stack>, + cached_timeouts_external: Stack>, + cached_timeout_links: Stack>, + cached_cmsg_bufs: Stack, + cached_connects: Stack>, + cached_accepts: Stack>, + + fd_ids_scratch: RefCell>, + + iteration: NumCell, + yields: SyncQueue, +} + +unsafe trait Task { + fn id(&self) -> IoUringTaskId; + fn complete(self: Box, ring: &IoUringData, res: i32); + fn encode(&self, sqe: &mut io_uring_sqe); + + fn is_cancel(&self) -> bool { + false + } + + fn has_timeout(&self) -> bool { + false + } +} + +impl IoUringData { + fn run(&self) -> Result<(), IoUringError> { + let mut to_submit = 0; + loop { + self.iteration.fetch_add(1); + while let Some(ev) = self.yields.pop() { + ev.wake(); + } + loop { + self.eng.dispatch(); + if self.destroyed.get() { + return Ok(()); + } + if !self.dispatch_completions() { + break; + } + } + to_submit += self.encode(); + let res = { + let (to_submit, mut min_complete, flags) = if to_submit == 0 { + (0, 1, IORING_ENTER_GETEVENTS) + } else if self.to_encode.is_empty() { + (to_submit as _, 1, IORING_ENTER_GETEVENTS) + } else { + (!0, 0, 0) + }; + if self.yields.is_not_empty() { + min_complete = 0; + } + io_uring_enter(self.fd.raw(), to_submit, min_complete, flags) + }; + let mut submitted_any = false; + match res { + Ok(n) => { + if n > 0 { + submitted_any = true; + } + to_submit -= n; + } + Err(e) => { + if !matches!(e.0, c::EAGAIN | c::EBUSY | c::EINTR) { + return Err(IoUringError::Enter(e)); + } + } + } + if to_submit > 0 && !submitted_any { + let res = io_uring_enter(self.fd.raw(), 0, 1, IORING_ENTER_GETEVENTS); + if let Err(e) = res { + if e.0 != c::EINTR { + return Err(IoUringError::Enter(e)); + } + } + } + } + } + + fn dispatch_completions(&self) -> bool { + unsafe { + let mut head = self.cqhead.deref().load(Relaxed); + let tail = self.cqtail.deref().load(Acquire); + if head == tail { + return false; + } + while head != tail { + let idx = (head & self.cqmask) as usize; + let entry = self.cqmap.deref()[idx].get(); + head = head.wrapping_add(1); + self.cqhead.deref().store(head, Release); + let id = IoUringTaskId(entry.user_data); + if let Some(pending) = self.tasks.remove(&id) { + self.pending_in_kernel.remove(&id); + pending.complete(self, entry.res); + } + } + self.cqhead.deref().store(head, Release); + self.cqes_consumed.trigger(); + true + } + } + + fn encode(&self) -> usize { + let tasks = self.tasks.lock(); + let mut encoded = 0; + unsafe { + let mut tail = self.sqtail.deref().load(Relaxed); + let head = self.sqhead.deref().load(Acquire); + let available = self.sqlen - tail.wrapping_sub(head); + while encoded < available { + let id = match self.to_encode.pop() { + Some(t) => t, + _ => break, + }; + let task = match tasks.get(&id) { + Some(t) => t, + _ => continue, + }; + let has_timeout = task.has_timeout(); + if has_timeout && (available - encoded) < 2 { + self.to_encode.push_front(id); + break; + } + self.pending_in_kernel.set(id, ()); + let idx = (tail & self.sqmask) as usize; + let sqe = self.sqesmap.deref()[idx].get().deref_mut(); + self.sqmap.deref()[idx].set(idx as _); + *sqe = Default::default(); + sqe.user_data = id.raw(); + task.encode(sqe); + if has_timeout { + sqe.flags |= IOSQE_IO_LINK; + } + tail = tail.wrapping_add(1); + encoded += 1; + } + self.sqtail.deref().store(tail, Release); + } + encoded as usize + } + + fn id(&self) -> Cancellable<'_> { + Cancellable { + id: self.id_raw(), + data: self, + } + } + + fn id_raw(&self) -> IoUringTaskId { + self.next.next() + } + + fn cancel_task(&self, id: IoUringTaskId) { + if !self.tasks.contains(&id) { + return; + } + if !self.pending_in_kernel.contains(&id) { + self.tasks + .remove(&id) + .unwrap() + .complete(self, -c::ECANCELED); + return; + } + self.cancel_task_in_kernel(id); + } + + fn schedule(&self, t: Box) { + assert!(!self.destroyed.get()); + self.to_encode.push(t.id()); + self.tasks.set(t.id(), t); + } + + fn check_destroyed(&self) -> Result<(), IoUringError> { + if self.destroyed.get() { + Err(IoUringError::Destroyed) + } else { + Ok(()) + } + } + + fn kill(&self) { + self.eng.stop(); + let mut to_cancel = vec![]; + for task in self.tasks.lock().values() { + if !task.is_cancel() { + to_cancel.push(task.id()); + } + } + for task in to_cancel { + self.cancel_task(task); + } + self.destroyed.set(true); + while !self.tasks.is_empty() { + self.encode(); + let _ = io_uring_enter(self.fd.raw(), u32::MAX, 0, 0); + let res = io_uring_enter(self.fd.raw(), 0, 1, IORING_ENTER_GETEVENTS); + if let Err(e) = res { + panic!("Could not wait for io_uring to drain: {}", ErrorFmt(e)); + } + while self.dispatch_completions() { + // nothing + } + } + } + + fn cmsg_buf(&self) -> Buf { + self.cached_cmsg_bufs + .pop() + .unwrap_or_else(|| Buf::new(1024)) + } +} + +#[derive(Debug)] +struct IoUringTaskIds { + next: NumCell, +} + +impl Default for IoUringTaskIds { + fn default() -> Self { + Self { + next: NumCell::new(1), + } + } +} + +impl IoUringTaskIds { + fn next(&self) -> IoUringTaskId { + IoUringTaskId(self.next.fetch_add(1)) + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct IoUringTaskId(u64); + +impl IoUringTaskId { + #[allow(clippy::allow_attributes, dead_code)] + pub fn raw(&self) -> u64 { + self.0 + } + + #[allow(clippy::allow_attributes, dead_code)] + pub fn from_raw(id: u64) -> Self { + Self(id) + } +} + +impl std::fmt::Display for IoUringTaskId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } +} + +#[expect(clippy::derivable_impls)] +impl Default for IoUringTaskId { + fn default() -> Self { + Self(0) + } +} + +struct Cancellable<'a> { + id: IoUringTaskId, + data: &'a IoUringData, +} + +impl<'a> Drop for Cancellable<'a> { + fn drop(&mut self) { + self.data.cancel_task(self.id); + } +} diff --git a/src/utils/line_logger.rs b/crates/io-uring/src/line_logger.rs similarity index 79% rename from src/utils/line_logger.rs rename to crates/io-uring/src/line_logger.rs index 91c75d42..aa5306f5 100644 --- a/src/utils/line_logger.rs +++ b/crates/io-uring/src/line_logger.rs @@ -1,9 +1,6 @@ use { - crate::{ - io_uring::{IoUring, IoUringError}, - utils::{buf::Buf, vecdeque_ext::VecDequeExt}, - }, - isnt::std_1::collections::IsntVecDequeExt, + crate::{IoUring, IoUringError}, + jay_utils::{buf::Buf, vecdeque_ext::VecDequeExt}, std::{collections::VecDeque, rc::Rc}, uapi::OwnedFd, }; @@ -28,7 +25,7 @@ pub async fn log_lines( buf.drain(..=pos); } } - if buf.is_not_empty() { + if !buf.is_empty() { let (left, right) = buf.as_slices(); f(left, right); } diff --git a/src/utils/object_drop_queue.rs b/crates/io-uring/src/object_drop_queue.rs similarity index 93% rename from src/utils/object_drop_queue.rs rename to crates/io-uring/src/object_drop_queue.rs index 56010e85..7417f544 100644 --- a/src/utils/object_drop_queue.rs +++ b/crates/io-uring/src/object_drop_queue.rs @@ -1,8 +1,6 @@ use { - crate::{ - io_uring::{IoUring, PendingPoll, PollCallback}, - utils::{errorfmt::ErrorFmt, oserror::OsError, stack::Stack}, - }, + crate::{IoUring, PendingPoll, PollCallback}, + jay_utils::{errorfmt::ErrorFmt, oserror::OsError, stack::Stack}, std::{ cell::{Cell, RefCell}, rc::Rc, diff --git a/src/io_uring/ops.rs b/crates/io-uring/src/ops.rs similarity index 91% rename from src/io_uring/ops.rs rename to crates/io-uring/src/ops.rs index e6abdbee..f77768d0 100644 --- a/src/io_uring/ops.rs +++ b/crates/io-uring/src/ops.rs @@ -1,4 +1,4 @@ -use crate::{io_uring::IoUringError, utils::oserror::OsError}; +use {crate::IoUringError, jay_utils::oserror::OsError}; pub mod accept; pub mod async_cancel; diff --git a/src/io_uring/ops/accept.rs b/crates/io-uring/src/ops/accept.rs similarity index 98% rename from src/io_uring/ops/accept.rs rename to crates/io-uring/src/ops/accept.rs index 0acee0d5..5fd47152 100644 --- a/src/io_uring/ops/accept.rs +++ b/crates/io-uring/src/ops/accept.rs @@ -1,5 +1,5 @@ use { - crate::io_uring::{ + crate::{ IoUring, IoUringData, IoUringError, IoUringTaskId, Task, TaskResultExt, pending_result::PendingResult, sys::{IORING_OP_ACCEPT, io_uring_sqe}, diff --git a/src/io_uring/ops/async_cancel.rs b/crates/io-uring/src/ops/async_cancel.rs similarity index 85% rename from src/io_uring/ops/async_cancel.rs rename to crates/io-uring/src/ops/async_cancel.rs index f21e7024..4a74b43d 100644 --- a/src/io_uring/ops/async_cancel.rs +++ b/crates/io-uring/src/ops/async_cancel.rs @@ -1,11 +1,9 @@ use { crate::{ - io_uring::{ - IoUringData, IoUringTaskId, Task, - sys::{IORING_OP_ASYNC_CANCEL, io_uring_sqe}, - }, - utils::errorfmt::ErrorFmt, + IoUringData, IoUringTaskId, Task, + sys::{IORING_OP_ASYNC_CANCEL, io_uring_sqe}, }, + jay_utils::errorfmt::ErrorFmt, uapi::c, }; diff --git a/src/io_uring/ops/connect.rs b/crates/io-uring/src/ops/connect.rs similarity index 98% rename from src/io_uring/ops/connect.rs rename to crates/io-uring/src/ops/connect.rs index cfb89039..4c0512f1 100644 --- a/src/io_uring/ops/connect.rs +++ b/crates/io-uring/src/ops/connect.rs @@ -1,5 +1,5 @@ use { - crate::io_uring::{ + crate::{ IoUring, IoUringData, IoUringError, IoUringTaskId, Task, TaskResultExt, pending_result::PendingResult, sys::{IORING_OP_CONNECT, io_uring_sqe}, diff --git a/src/io_uring/ops/poll.rs b/crates/io-uring/src/ops/poll.rs similarity index 97% rename from src/io_uring/ops/poll.rs rename to crates/io-uring/src/ops/poll.rs index 01e77679..dd7a88e4 100644 --- a/src/io_uring/ops/poll.rs +++ b/crates/io-uring/src/ops/poll.rs @@ -1,5 +1,5 @@ use { - crate::io_uring::{ + crate::{ IoUring, IoUringData, IoUringError, IoUringTaskId, Task, TaskResultExt, ops::TaskResult, pending_result::PendingResult, @@ -32,7 +32,6 @@ impl IoUring { self.poll(fd, c::POLLIN).await.merge() } - #[expect(dead_code)] pub async fn writable(&self, fd: &Rc) -> Result { self.poll(fd, c::POLLOUT).await.merge() } diff --git a/src/io_uring/ops/poll_external.rs b/crates/io-uring/src/ops/poll_external.rs similarity index 92% rename from src/io_uring/ops/poll_external.rs rename to crates/io-uring/src/ops/poll_external.rs index 6a86a125..be25e053 100644 --- a/src/io_uring/ops/poll_external.rs +++ b/crates/io-uring/src/ops/poll_external.rs @@ -1,11 +1,9 @@ use { crate::{ - io_uring::{ - IoUring, IoUringData, IoUringError, IoUringTaskId, Task, - sys::{IORING_OP_POLL_ADD, io_uring_sqe}, - }, - utils::oserror::OsError, + IoUring, IoUringData, IoUringError, IoUringTaskId, Task, + sys::{IORING_OP_POLL_ADD, io_uring_sqe}, }, + jay_utils::oserror::OsError, std::{cell::Cell, rc::Rc}, uapi::{OwnedFd, c}, }; @@ -61,7 +59,6 @@ impl IoUring { self.poll_external(fd, c::POLLIN, callback) } - #[expect(dead_code)] pub fn writable_external( &self, fd: &Rc, diff --git a/src/io_uring/ops/read_write.rs b/crates/io-uring/src/ops/read_write.rs similarity index 89% rename from src/io_uring/ops/read_write.rs rename to crates/io-uring/src/ops/read_write.rs index ddece5a3..e7af42fc 100644 --- a/src/io_uring/ops/read_write.rs +++ b/crates/io-uring/src/ops/read_write.rs @@ -1,13 +1,11 @@ use { crate::{ - io_uring::{ - IoUring, IoUringData, IoUringError, IoUringTaskId, Task, TaskResultExt, - pending_result::PendingResult, - sys::{IORING_OP_READ, IORING_OP_WRITE, io_uring_sqe}, - }, - time::Time, - utils::buf::Buf, + IoUring, IoUringData, IoUringError, IoUringTaskId, Task, TaskResultExt, + pending_result::PendingResult, + sys::{IORING_OP_READ, IORING_OP_WRITE, io_uring_sqe}, }, + jay_time::Time, + jay_utils::buf::Buf, std::rc::Rc, uapi::{OwnedFd, c}, }; diff --git a/src/io_uring/ops/read_write_no_cancel.rs b/crates/io-uring/src/ops/read_write_no_cancel.rs similarity index 92% rename from src/io_uring/ops/read_write_no_cancel.rs rename to crates/io-uring/src/ops/read_write_no_cancel.rs index a152d752..609aa464 100644 --- a/src/io_uring/ops/read_write_no_cancel.rs +++ b/crates/io-uring/src/ops/read_write_no_cancel.rs @@ -3,13 +3,11 @@ mod tests; use { crate::{ - io_uring::{ - IoUring, IoUringData, IoUringError, IoUringTaskId, Task, TaskResultExt, - pending_result::PendingResult, - sys::{IORING_OP_READ, IORING_OP_WRITE, io_uring_sqe}, - }, - time::Time, + IoUring, IoUringData, IoUringError, IoUringTaskId, Task, TaskResultExt, + pending_result::PendingResult, + sys::{IORING_OP_READ, IORING_OP_WRITE, io_uring_sqe}, }, + jay_time::Time, run_on_drop::on_drop, uapi::{Fd, c}, }; diff --git a/src/io_uring/ops/read_write_no_cancel/tests.rs b/crates/io-uring/src/ops/read_write_no_cancel/tests.rs similarity index 78% rename from src/io_uring/ops/read_write_no_cancel/tests.rs rename to crates/io-uring/src/ops/read_write_no_cancel/tests.rs index ef50bba0..20094f96 100644 --- a/src/io_uring/ops/read_write_no_cancel/tests.rs +++ b/crates/io-uring/src/ops/read_write_no_cancel/tests.rs @@ -1,10 +1,7 @@ use { - crate::{ - async_engine::AsyncEngine, - io_uring::{IoUring, IoUringError}, - utils::{oserror::OsError, queue::AsyncQueue}, - wheel::Wheel, - }, + crate::{IoUring, IoUringError}, + jay_async_engine::AsyncEngine, + jay_utils::{oserror::OsError, queue::AsyncQueue}, std::rc::Rc, uapi::c::ECANCELED, }; @@ -14,7 +11,6 @@ fn cancel(timeout: bool) { let ring = IoUring::new(&eng, 32).unwrap(); let ring2 = ring.clone(); let ring3 = ring.clone(); - let wheel = Wheel::new(&eng, &ring).unwrap(); let queue = Rc::new(AsyncQueue::new()); let queue2 = queue.clone(); let _fut1 = eng.spawn("", async move { @@ -32,7 +28,7 @@ fn cancel(timeout: bool) { let _fut2 = eng.spawn("", async move { let id = queue2.pop().await; if timeout { - wheel.timeout(1).await.unwrap(); + ring2.timeout(1).await.unwrap(); } ring2.cancel(id); }); diff --git a/src/io_uring/ops/recvmsg.rs b/crates/io-uring/src/ops/recvmsg.rs similarity index 94% rename from src/io_uring/ops/recvmsg.rs rename to crates/io-uring/src/ops/recvmsg.rs index 1c7e4f81..0aa2f883 100644 --- a/src/io_uring/ops/recvmsg.rs +++ b/crates/io-uring/src/ops/recvmsg.rs @@ -1,12 +1,10 @@ use { crate::{ - io_uring::{ - IoUring, IoUringData, IoUringError, IoUringTaskId, Task, - pending_result::PendingResult, - sys::{IORING_OP_RECVMSG, io_uring_sqe}, - }, - utils::buf::Buf, + IoUring, IoUringData, IoUringError, IoUringTaskId, Task, + pending_result::PendingResult, + sys::{IORING_OP_RECVMSG, io_uring_sqe}, }, + jay_utils::buf::Buf, std::{cell::Cell, collections::VecDeque, mem::MaybeUninit, rc::Rc}, uapi::{OwnedFd, c}, }; diff --git a/src/io_uring/ops/sendmsg.rs b/crates/io-uring/src/ops/sendmsg.rs similarity index 93% rename from src/io_uring/ops/sendmsg.rs rename to crates/io-uring/src/ops/sendmsg.rs index c6231b39..8a245740 100644 --- a/src/io_uring/ops/sendmsg.rs +++ b/crates/io-uring/src/ops/sendmsg.rs @@ -1,13 +1,11 @@ use { crate::{ - io_uring::{ - IoUring, IoUringData, IoUringError, IoUringTaskId, Task, - pending_result::PendingResult, - sys::{IORING_OP_SENDMSG, io_uring_sqe}, - }, - time::Time, - utils::{buf::Buf, compat::IovLength, vec_ext::UninitVecExt}, + IoUring, IoUringData, IoUringError, IoUringTaskId, Task, + pending_result::PendingResult, + sys::{IORING_OP_SENDMSG, io_uring_sqe}, }, + jay_time::Time, + jay_utils::{buf::Buf, compat::IovLength, vec_ext::UninitVecExt}, std::{mem::MaybeUninit, ptr, rc::Rc}, uapi::{OwnedFd, c}, }; diff --git a/src/io_uring/ops/timeout.rs b/crates/io-uring/src/ops/timeout.rs similarity index 98% rename from src/io_uring/ops/timeout.rs rename to crates/io-uring/src/ops/timeout.rs index d7e0c2fb..74e7be1c 100644 --- a/src/io_uring/ops/timeout.rs +++ b/crates/io-uring/src/ops/timeout.rs @@ -1,5 +1,5 @@ use { - crate::io_uring::{ + crate::{ IoUring, IoUringData, IoUringError, IoUringTaskId, Task, pending_result::PendingResult, sys::{IORING_OP_TIMEOUT, IORING_TIMEOUT_ABS, io_uring_sqe}, diff --git a/src/io_uring/ops/timeout_external.rs b/crates/io-uring/src/ops/timeout_external.rs similarity index 90% rename from src/io_uring/ops/timeout_external.rs rename to crates/io-uring/src/ops/timeout_external.rs index 860811de..1034d703 100644 --- a/src/io_uring/ops/timeout_external.rs +++ b/crates/io-uring/src/ops/timeout_external.rs @@ -1,12 +1,9 @@ use { crate::{ - io_uring::{ - IoUring, IoUringData, IoUringError, IoUringTaskId, Task, - ops::timeout::timespec64, - sys::{IORING_OP_TIMEOUT, IORING_TIMEOUT_ABS, io_uring_sqe}, - }, - utils::oserror::OsError, + IoUring, IoUringData, IoUringError, IoUringTaskId, Task, ops::timeout::timespec64, + sys::{IORING_OP_TIMEOUT, IORING_TIMEOUT_ABS, io_uring_sqe}, }, + jay_utils::oserror::OsError, std::{cell::Cell, rc::Rc}, uapi::c, }; diff --git a/src/io_uring/ops/timeout_link.rs b/crates/io-uring/src/ops/timeout_link.rs similarity index 83% rename from src/io_uring/ops/timeout_link.rs rename to crates/io-uring/src/ops/timeout_link.rs index edb9faa4..f349c879 100644 --- a/src/io_uring/ops/timeout_link.rs +++ b/crates/io-uring/src/ops/timeout_link.rs @@ -1,11 +1,8 @@ use crate::{ - io_uring::{ - IoUring, IoUringData, IoUringTaskId, Task, - ops::timeout::timespec64, - sys::{IORING_OP_LINK_TIMEOUT, IORING_TIMEOUT_ABS, io_uring_sqe}, - }, - time::Time, + IoUring, IoUringData, IoUringTaskId, Task, ops::timeout::timespec64, + sys::{IORING_OP_LINK_TIMEOUT, IORING_TIMEOUT_ABS, io_uring_sqe}, }; +use jay_time::Time; #[derive(Default)] pub struct TimeoutLinkTask { diff --git a/src/io_uring/pending_result.rs b/crates/io-uring/src/pending_result.rs similarity index 96% rename from src/io_uring/pending_result.rs rename to crates/io-uring/src/pending_result.rs index 544c182e..4d7c6a87 100644 --- a/src/io_uring/pending_result.rs +++ b/crates/io-uring/src/pending_result.rs @@ -1,5 +1,5 @@ use { - crate::utils::{numcell::NumCell, oserror::OsError, ptr_ext::PtrExt, stack::Stack}, + jay_utils::{numcell::NumCell, oserror::OsError, ptr_ext::PtrExt, stack::Stack}, std::{ cell::Cell, future::Future, diff --git a/src/io_uring/sys.rs b/crates/io-uring/src/sys.rs similarity index 99% rename from src/io_uring/sys.rs rename to crates/io-uring/src/sys.rs index d9ba4197..9a9b9665 100644 --- a/src/io_uring/sys.rs +++ b/crates/io-uring/src/sys.rs @@ -1,7 +1,7 @@ #![allow(non_camel_case_types, dead_code)] use { - crate::utils::oserror::OsError, + jay_utils::oserror::OsError, std::mem::MaybeUninit, uapi::{OwnedFd, c}, }; diff --git a/src/utils/timer.rs b/crates/io-uring/src/timer.rs similarity index 92% rename from src/utils/timer.rs rename to crates/io-uring/src/timer.rs index 1bff0c64..7eadc374 100644 --- a/src/utils/timer.rs +++ b/crates/io-uring/src/timer.rs @@ -1,10 +1,8 @@ use { - crate::{ - io_uring::{IoUring, IoUringError}, - utils::{ - buf::TypedBuf, - oserror::{OsError, OsErrorExt2}, - }, + crate::{IoUring, IoUringError}, + jay_utils::{ + buf::TypedBuf, + oserror::{OsError, OsErrorExt2}, }, std::{cell::RefCell, rc::Rc, time::Duration}, thiserror::Error, diff --git a/crates/jay-config-schema/Cargo.toml b/crates/jay-config-schema/Cargo.toml new file mode 100644 index 00000000..0ecc60fa --- /dev/null +++ b/crates/jay-config-schema/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "jay-config-schema" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Shared configuration schema declarations for the Jay compositor" +repository = "https://github.com/mahkoh/jay" + +[dependencies] +ahash = "0.8.11" +jay-config = { path = "../jay-config" } diff --git a/crates/jay-config-schema/src/action.rs b/crates/jay-config-schema/src/action.rs new file mode 100644 index 00000000..8a774a06 --- /dev/null +++ b/crates/jay-config-schema/src/action.rs @@ -0,0 +1,59 @@ +use jay_config::{ + Direction, + input::{LayerDirection, Timeline}, +}; + +#[derive(Debug, Copy, Clone)] +pub enum SimpleCommand { + Close, + DisablePointerConstraint, + Focus(Direction), + FocusParent, + Move(Direction), + None, + Quit, + ReloadConfigToml, + ToggleFloating, + SetFloating(bool), + ToggleFullscreen, + SetFullscreen(bool), + SendToScratchpad, + ToggleScratchpad, + CycleScratchpad, + Forward(bool), + EnableWindowManagement(bool), + SetFloatAboveFullscreen(bool), + ToggleFloatAboveFullscreen, + SetFloatPinned(bool), + ToggleFloatPinned, + KillClient, + ShowBar(bool), + ToggleBar, + ShowTitles(bool), + ToggleTitles, + FloatTitles(bool), + ToggleFloatTitles, + FocusHistory(Timeline), + FocusLayerRel(LayerDirection), + FocusTiles, + ToggleFocusFloatTiled, + CreateMark, + JumpToMark, + PopMode(bool), + EnableSimpleIm(bool), + ToggleSimpleImEnabled, + ReloadSimpleIm, + EnableUnicodeInput, + WarpMouseToFocus, + ToggleTab, + MakeGroupH, + MakeGroupV, + MakeGroupTab, + ChangeGroupOpposite, + Equalize, + EqualizeRecursive, + MoveTabLeft, + MoveTabRight, + SetAutotile(bool), + ToggleAutotile, +} diff --git a/crates/jay-config-schema/src/animations.rs b/crates/jay-config-schema/src/animations.rs new file mode 100644 index 00000000..a60ba034 --- /dev/null +++ b/crates/jay-config-schema/src/animations.rs @@ -0,0 +1,13 @@ +#[derive(Debug, Clone, Default)] +pub struct Animations { + pub enabled: Option, + pub duration_ms: Option, + pub style: Option, + pub curve: Option, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum AnimationCurveConfig { + Preset(String), + CubicBezier([f32; 4]), +} diff --git a/crates/jay-config-schema/src/command.rs b/crates/jay-config-schema/src/command.rs new file mode 100644 index 00000000..062dee52 --- /dev/null +++ b/crates/jay-config-schema/src/command.rs @@ -0,0 +1,16 @@ +use jay_config::status::MessageFormat; + +#[derive(Debug, Clone)] +pub struct Exec { + pub prog: String, + pub args: Vec, + pub envs: Vec<(String, String)>, + pub tag: Option, +} + +#[derive(Debug, Clone)] +pub struct Status { + pub format: MessageFormat, + pub exec: Exec, + pub separator: Option, +} diff --git a/crates/jay-config-schema/src/input.rs b/crates/jay-config-schema/src/input.rs new file mode 100644 index 00000000..e35cd6bc --- /dev/null +++ b/crates/jay-config-schema/src/input.rs @@ -0,0 +1,17 @@ +#[derive(Debug, Clone)] +pub enum InputMatch { + Any(Vec), + All { + tag: Option, + name: Option, + syspath: Option, + devnode: Option, + is_keyboard: Option, + is_pointer: Option, + is_touch: Option, + is_tablet_tool: Option, + is_tablet_pad: Option, + is_gesture: Option, + is_switch: Option, + }, +} diff --git a/crates/jay-config-schema/src/keymap.rs b/crates/jay-config-schema/src/keymap.rs new file mode 100644 index 00000000..30035a9f --- /dev/null +++ b/crates/jay-config-schema/src/keymap.rs @@ -0,0 +1,8 @@ +use jay_config::keyboard::Keymap; + +#[derive(Debug, Clone)] +pub enum ConfigKeymap { + Named(String), + Literal(Keymap), + Defined { name: String, map: Keymap }, +} diff --git a/crates/jay-config-schema/src/lib.rs b/crates/jay-config-schema/src/lib.rs new file mode 100644 index 00000000..051a5ee7 --- /dev/null +++ b/crates/jay-config-schema/src/lib.rs @@ -0,0 +1,34 @@ +//! Shared configuration schema declarations for Jay. +//! +//! This crate is the target home for option structs, defaults, validation +//! policy, and docs metadata that need to be consumed by TOML parsing, +//! generated config documentation, and compositor-side application code. + +pub mod action; +pub mod animations; +pub mod command; +pub mod input; +pub mod keymap; +pub mod model; +pub mod options; +pub mod output; +pub mod rules; +pub mod theme; + +pub use action::SimpleCommand; +pub use animations::{AnimationCurveConfig, Animations}; +pub use command::{Exec, Status}; +pub use input::InputMatch; +pub use keymap::ConfigKeymap; +pub use model::{ + Action, ClientRule, Config, Input, InputMode, NamedAction, Scratchpad, Shortcut, WindowRule, +}; +pub use options::{ + ColorManagement, Float, FocusHistory, Libei, RepeatRate, SimpleIm, Tearing, UiDrag, Vrr, + Xwayland, +}; +pub use output::{ + ConfigConnector, ConfigDrmDevice, ConnectorMatch, DrmDeviceMatch, Mode, Output, OutputMatch, +}; +pub use rules::{ClientMatch, GenericMatch, MatchExactly, WindowMatch}; +pub use theme::Theme; diff --git a/crates/jay-config-schema/src/model.rs b/crates/jay-config-schema/src/model.rs new file mode 100644 index 00000000..291bb448 --- /dev/null +++ b/crates/jay-config-schema/src/model.rs @@ -0,0 +1,256 @@ +use { + crate::{ + Animations, ClientMatch, ColorManagement, ConfigConnector, ConfigDrmDevice, ConfigKeymap, + DrmDeviceMatch, Exec, Float, FocusHistory, InputMatch, Libei, Output, OutputMatch, + RepeatRate, SimpleCommand, SimpleIm, Status, Tearing, Theme, UiDrag, Vrr, WindowMatch, + Xwayland, + }, + ahash::AHashMap, + jay_config::{ + Direction, Workspace, + input::{ + FallbackOutputMode, SwitchEvent, acceleration::AccelProfile, clickmethod::ClickMethod, + }, + keyboard::{ModifiedKeySym, mods::Modifiers, syms::KeySym}, + logging::LogLevel, + video::GfxApi, + window::TileState, + workspace::WorkspaceDisplayOrder, + }, + std::{rc::Rc, time::Duration}, +}; + +#[derive(Debug, Clone)] +#[expect(clippy::enum_variant_names)] +pub enum Action { + ConfigureConnector { + con: ConfigConnector, + }, + ConfigureDirectScanout { + enabled: bool, + }, + ConfigureDrmDevice { + dev: ConfigDrmDevice, + }, + ConfigureIdle { + idle: Option, + grace_period: Option, + }, + ConfigureInput { + input: Box, + }, + ConfigureOutput { + out: Output, + }, + Exec { + exec: Exec, + }, + MoveToWorkspace { + name: String, + }, + SendToScratchpad { + name: String, + }, + ToggleScratchpad { + name: String, + }, + CycleScratchpad { + name: String, + }, + Multi { + actions: Vec, + }, + SetEnv { + env: Vec<(String, String)>, + }, + SetGfxApi { + api: GfxApi, + }, + SetKeymap { + map: ConfigKeymap, + }, + SetLogLevel { + level: LogLevel, + }, + SetRenderDevice { + dev: Box, + }, + SetStatus { + status: Option, + }, + SetTheme { + theme: Box, + }, + ShowWorkspace { + name: String, + output: Option, + }, + SimpleCommand { + cmd: SimpleCommand, + }, + SwitchToVt { + num: u32, + }, + UnsetEnv { + env: Vec, + }, + MoveToOutput { + workspace: Option, + output: Option, + direction: Option, + }, + SetRepeatRate { + rate: RepeatRate, + }, + DefineAction { + name: String, + action: Box, + }, + UndefineAction { + name: String, + }, + NamedAction { + name: String, + }, + CreateMark(u32), + JumpToMark(u32), + CopyMark(u32, u32), + SetMode { + name: String, + latch: bool, + }, + CreateVirtualOutput { + name: String, + }, + RemoveVirtualOutput { + name: String, + }, + Resize { + dx1: i32, + dy1: i32, + dx2: i32, + dy2: i32, + }, +} + +#[derive(Debug, Clone)] +pub struct ClientRule { + pub name: Option, + pub match_: ClientMatch, + pub action: Option, + pub latch: Option, +} + +#[derive(Debug, Clone)] +pub struct WindowRule { + pub name: Option, + pub match_: WindowMatch, + pub action: Option, + pub latch: Option, + pub auto_focus: Option, + pub initial_tile_state: Option, +} + +#[derive(Debug, Clone)] +pub struct Input { + pub tag: Option, + pub match_: InputMatch, + pub accel_profile: Option, + pub accel_speed: Option, + pub tap_enabled: Option, + pub tap_drag_enabled: Option, + pub tap_drag_lock_enabled: Option, + pub left_handed: Option, + pub natural_scrolling: Option, + pub click_method: Option, + pub middle_button_emulation: Option, + pub px_per_wheel_scroll: Option, + pub transform_matrix: Option<[[f64; 2]; 2]>, + pub keymap: Option, + pub switch_actions: AHashMap, + pub output: Option>, + pub calibration_matrix: Option<[[f32; 3]; 2]>, +} + +#[derive(Debug, Clone)] +pub struct Shortcut { + pub mask: Modifiers, + pub keysym: ModifiedKeySym, + pub action: Action, + pub latch: Option, +} + +#[derive(Debug, Clone)] +pub struct NamedAction { + pub name: Rc, + pub action: Action, +} + +#[derive(Clone, Debug)] +pub struct InputMode { + pub parent: Option, + pub shortcuts: Vec, +} + +#[derive(Debug, Clone)] +pub struct Config { + pub keymap: Option, + pub repeat_rate: Option, + pub shortcuts: Vec, + pub on_graphics_initialized: Option, + pub on_idle: Option, + pub status: Option, + pub connectors: Vec, + pub outputs: Vec, + pub workspace_capture: bool, + pub env: Vec<(String, String)>, + pub on_startup: Option, + pub keymaps: Vec, + pub auto_reload: Option, + pub log_level: Option, + pub clean_logs_older_than: Option, + pub theme: Theme, + pub gfx_api: Option, + pub direct_scanout_enabled: Option, + pub drm_devices: Vec, + pub render_device: Option, + pub inputs: Vec, + pub idle: Option, + pub grace_period: Option, + pub key_press_enables_dpms: Option, + pub mouse_move_enables_dpms: Option, + pub explicit_sync_enabled: Option, + pub focus_follows_mouse: bool, + pub window_management_key: Option, + pub vrr: Option, + pub tearing: Option, + pub libei: Libei, + pub ui_drag: UiDrag, + pub animations: Animations, + pub xwayland: Option, + pub color_management: Option, + pub float: Option, + pub named_actions: Vec, + pub max_action_depth: u64, + pub client_rules: Vec, + pub window_rules: Vec, + pub pointer_revert_key: Option, + pub use_hardware_cursor: Option, + pub show_bar: Option, + pub show_titles: Option, + pub focus_history: Option, + pub middle_click_paste: Option, + pub input_modes: AHashMap, + pub workspace_display_order: Option, + pub simple_im: Option, + pub fallback_output_mode: Option, + pub mouse_follows_focus: Option, + pub scratchpads: Vec, + pub autotile: Option, +} + +#[derive(Debug, Clone)] +pub struct Scratchpad { + pub name: String, + pub exec: Option, +} diff --git a/crates/jay-config-schema/src/options.rs b/crates/jay-config-schema/src/options.rs new file mode 100644 index 00000000..b528dcf5 --- /dev/null +++ b/crates/jay-config-schema/src/options.rs @@ -0,0 +1,59 @@ +use jay_config::{ + video::{TearingMode, VrrMode}, + xwayland::XScalingMode, +}; + +#[derive(Debug, Clone, Default)] +pub struct UiDrag { + pub enabled: Option, + pub threshold: Option, +} + +#[derive(Clone, Debug)] +pub struct ColorManagement { + pub enabled: Option, +} + +#[derive(Debug, Clone)] +pub struct Float { + pub show_pin_icon: Option, +} + +#[derive(Debug, Clone)] +pub struct FocusHistory { + pub only_visible: Option, + pub same_workspace: Option, +} + +#[derive(Debug, Clone)] +pub struct RepeatRate { + pub rate: i32, + pub delay: i32, +} + +#[derive(Debug, Clone)] +pub struct Vrr { + pub mode: Option, + pub cursor_hz: Option, +} + +#[derive(Debug, Clone)] +pub struct SimpleIm { + pub enabled: Option, +} + +#[derive(Debug, Clone)] +pub struct Xwayland { + pub enabled: Option, + pub scaling_mode: Option, +} + +#[derive(Debug, Clone)] +pub struct Tearing { + pub mode: Option, +} + +#[derive(Debug, Clone, Default)] +pub struct Libei { + pub enable_socket: Option, +} diff --git a/crates/jay-config-schema/src/output.rs b/crates/jay-config-schema/src/output.rs new file mode 100644 index 00000000..93c1343f --- /dev/null +++ b/crates/jay-config-schema/src/output.rs @@ -0,0 +1,88 @@ +use { + crate::{Tearing, Vrr}, + jay_config::video::{BlendSpace, ColorSpace, Eotf, Format, GfxApi, Transform}, + std::fmt::{Display, Formatter}, +}; + +#[derive(Debug, Clone)] +pub enum OutputMatch { + Any(Vec), + All { + name: Option, + connector: Option, + serial_number: Option, + manufacturer: Option, + model: Option, + }, +} + +#[derive(Debug, Clone)] +pub enum DrmDeviceMatch { + Any(Vec), + All { + name: Option, + syspath: Option, + vendor: Option, + vendor_name: Option, + model: Option, + model_name: Option, + devnode: Option, + }, +} + +#[derive(Debug, Clone)] +pub struct Mode { + pub width: i32, + pub height: i32, + pub refresh_rate: Option, +} + +impl Display for Mode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{} x {}", self.width, self.height)?; + if let Some(rr) = self.refresh_rate { + write!(f, " @ {rr}")?; + } + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct Output { + pub name: Option, + pub match_: OutputMatch, + pub x: Option, + pub y: Option, + pub scale: Option, + pub transform: Option, + pub mode: Option, + pub vrr: Option, + pub tearing: Option, + pub format: Option, + pub color_space: Option, + pub eotf: Option, + pub brightness: Option>, + pub blend_space: Option, + pub use_native_gamut: Option, +} + +#[derive(Debug, Clone)] +pub enum ConnectorMatch { + Any(Vec), + All { connector: Option }, +} + +#[derive(Debug, Clone)] +pub struct ConfigConnector { + pub match_: ConnectorMatch, + pub enabled: bool, +} + +#[derive(Debug, Clone)] +pub struct ConfigDrmDevice { + pub name: Option, + pub match_: DrmDeviceMatch, + pub gfx_api: Option, + pub direct_scanout_enabled: Option, + pub flip_margin_ms: Option, +} diff --git a/crates/jay-config-schema/src/rules.rs b/crates/jay-config-schema/src/rules.rs new file mode 100644 index 00000000..563c60e1 --- /dev/null +++ b/crates/jay-config-schema/src/rules.rs @@ -0,0 +1,65 @@ +use jay_config::window::{ContentType, WindowType}; + +#[derive(Default, Debug, Clone)] +pub struct GenericMatch { + pub name: Option, + pub not: Option>, + pub all: Option>, + pub any: Option>, + pub exactly: Option>, +} + +#[derive(Debug, Clone)] +pub struct MatchExactly { + pub num: usize, + pub list: Vec, +} + +#[derive(Default, Debug, Clone)] +pub struct ClientMatch { + pub generic: GenericMatch, + pub sandbox_engine: Option, + pub sandbox_engine_regex: Option, + pub sandbox_app_id: Option, + pub sandbox_app_id_regex: Option, + pub sandbox_instance_id: Option, + pub sandbox_instance_id_regex: Option, + pub sandboxed: Option, + pub uid: Option, + pub pid: Option, + pub is_xwayland: Option, + pub comm: Option, + pub comm_regex: Option, + pub exe: Option, + pub exe_regex: Option, + pub tag: Option, + pub tag_regex: Option, +} + +#[derive(Default, Debug, Clone)] +pub struct WindowMatch { + pub generic: GenericMatch, + pub types: Option, + pub client: Option, + pub title: Option, + pub title_regex: Option, + pub app_id: Option, + pub app_id_regex: Option, + pub floating: Option, + pub visible: Option, + pub urgent: Option, + pub focused: Option, + pub fullscreen: Option, + pub just_mapped: Option, + pub tag: Option, + pub tag_regex: Option, + pub x_class: Option, + pub x_class_regex: Option, + pub x_instance: Option, + pub x_instance_regex: Option, + pub x_role: Option, + pub x_role_regex: Option, + pub workspace: Option, + pub workspace_regex: Option, + pub content_types: Option, +} diff --git a/crates/jay-config-schema/src/theme.rs b/crates/jay-config-schema/src/theme.rs new file mode 100644 index 00000000..ed21242d --- /dev/null +++ b/crates/jay-config-schema/src/theme.rs @@ -0,0 +1,48 @@ +use jay_config::theme::{BarPosition, Color}; + +#[derive(Debug, Clone, Default)] +pub struct Theme { + pub attention_requested_bg_color: Option, + pub bg_color: Option, + pub bar_bg_color: Option, + pub bar_status_text_color: Option, + pub border_color: Option, + pub active_border_color: Option, + pub captured_focused_title_bg_color: Option, + pub captured_unfocused_title_bg_color: Option, + pub focused_inactive_title_bg_color: Option, + pub focused_inactive_title_text_color: Option, + pub focused_title_bg_color: Option, + pub focused_title_text_color: Option, + pub separator_color: Option, + pub unfocused_title_bg_color: Option, + pub unfocused_title_text_color: Option, + pub highlight_color: Option, + pub border_width: Option, + pub title_height: Option, + pub bar_height: Option, + pub font: Option, + pub title_font: Option, + pub bar_font: Option, + pub bar_position: Option, + pub bar_separator_width: Option, + pub gap: Option, + pub floating_titles: Option, + pub title_gap: Option, + pub corner_radius: Option, + pub tab_active_bg_color: Option, + pub tab_active_border_color: Option, + pub tab_inactive_bg_color: Option, + pub tab_inactive_border_color: Option, + pub tab_active_text_color: Option, + pub tab_inactive_text_color: Option, + pub tab_bar_bg_color: Option, + pub tab_attention_bg_color: Option, + pub tab_bar_height: Option, + pub tab_bar_padding: Option, + pub tab_bar_radius: Option, + pub tab_bar_border_width: Option, + pub tab_bar_text_padding: Option, + pub tab_bar_gap: Option, + pub tab_title_align: Option, +} diff --git a/jay-config/Cargo.toml b/crates/jay-config/Cargo.toml similarity index 85% rename from jay-config/Cargo.toml rename to crates/jay-config/Cargo.toml index ce616ea3..cb9341f3 100644 --- a/jay-config/Cargo.toml +++ b/crates/jay-config/Cargo.toml @@ -1,13 +1,12 @@ [package] name = "jay-config" -version = "1.10.0" -edition = "2024" -license = "GPL-3.0-only" +version.workspace = true +edition.workspace = true +license.workspace = true description = "Configuration crate for the Jay compositor" repository = "https://github.com/mahkoh/jay" [dependencies] -bincode = "1.3.3" serde = { version = "1.0.196", features = ["derive"] } log = "0.4.14" futures-util = { version = "0.3.30", features = ["io"] } diff --git a/crates/jay-config/src/_private.rs b/crates/jay-config/src/_private.rs new file mode 100644 index 00000000..20ddec34 --- /dev/null +++ b/crates/jay-config/src/_private.rs @@ -0,0 +1,15 @@ +pub mod client; +mod logging; + +pub use crate::protocol::{ + ClientCriterionPayload, ClientCriterionStringField, ConfigEntry, ConfigHandler, + DEFAULT_SEAT_NAME, GenericCriterionPayload, PollableId, ServerHandler, Unref, VERSION, + WindowCriterionPayload, WindowCriterionStringField, WireMode, +}; + +pub mod messages { + pub use crate::protocol::{ + ClientMessage, InitMessage, Response, ServerFeature, ServerMessage, V1InitMessage, + WorkspaceSource, + }; +} diff --git a/jay-config/src/_private/client.rs b/crates/jay-config/src/_private/client.rs similarity index 93% rename from jay-config/src/_private/client.rs rename to crates/jay-config/src/_private/client.rs index 151e7591..21659395 100644 --- a/jay-config/src/_private/client.rs +++ b/crates/jay-config/src/_private/client.rs @@ -3,16 +3,10 @@ use { crate::{ _private::{ - ClientCriterionIpc, ClientCriterionStringField, Config, ConfigEntry, ConfigEntryGen, - GenericCriterionIpc, PollableId, VERSION, WindowCriterionIpc, - WindowCriterionStringField, WireMode, bincode_ops, - ipc::{ - ClientMessage, InitMessage, Response, ServerFeature, ServerMessage, WorkspaceSource, - }, - logging, + ServerHandler, Unref, }, Axis, Direction, ModifiedKeySym, PciId, Workspace, - client::{Client, ClientCapabilities, ClientCriterion, ClientMatcher, MatchedClient}, + client::{Client, ClientCriterion, ClientMatcher, MatchedClient}, exec::Command, input::{ FallbackOutputMode, FocusFollowsMouseMode, InputDevice, LayerDirection, Seat, @@ -25,6 +19,12 @@ use { syms::KeySym, }, logging::LogLevel, + protocol::{ + ClientCriterionPayload, ClientCriterionStringField, ClientMessage, + GenericCriterionPayload, InitMessage, PollableId, Response, ServerFeature, + ServerMessage, WindowCriterionPayload, WindowCriterionStringField, WireMode, + WorkspaceSource, + }, tasks::{JoinHandle, JoinSlot}, theme::{BarPosition, Color, colors::Colorable, sized::Resizable}, timer::Timer, @@ -40,7 +40,6 @@ use { workspace::WorkspaceDisplayOrder, xwayland::XScalingMode, }, - bincode::Options, futures_util::task::ArcWake, run_on_drop::{OnDrop, on_drop}, std::{ @@ -54,7 +53,6 @@ use { pin::Pin, ptr, rc::Rc, - slice, sync::{ Arc, Mutex, atomic::{AtomicBool, Ordering::Relaxed}, @@ -91,10 +89,10 @@ struct KeyHandler { } pub(crate) struct ConfigClient { - configure: extern "C" fn(), + configure: fn(), srv_data: *const u8, - srv_unref: unsafe extern "C" fn(data: *const u8), - srv_handler: unsafe extern "C" fn(data: *const u8, msg: *const u8, size: usize), + srv_unref: Unref, + srv_handler: ServerHandler, key_handlers: RefCell>, timer_handlers: RefCell>, response: RefCell>, @@ -111,7 +109,6 @@ pub(crate) struct ConfigClient { on_idle: RefCell>, on_switch_event: RefCell>>, on_unload: Cell>>>, - bufs: RefCell>>, reload: Cell, read_interests: RefCell>, write_interests: RefCell>, @@ -199,43 +196,14 @@ unsafe fn with_client T>(data: *const u8, f: F) - }) } -impl ConfigEntryGen { - pub const ENTRY: ConfigEntry = ConfigEntry { - version: VERSION, - init: Self::init, - unref, - handle_msg, - }; - - pub unsafe extern "C" fn init( - srv_data: *const u8, - srv_unref: unsafe extern "C" fn(data: *const u8), - srv_handler: unsafe extern "C" fn(data: *const u8, msg: *const u8, size: usize), - init_data: *const u8, - size: usize, - ) -> *const u8 { - logging::init(); - unsafe { - init( - srv_data, - srv_unref, - srv_handler, - init_data, - size, - T::configure, - ) - } - } -} - -pub unsafe extern "C" fn init( +pub unsafe fn init( srv_data: *const u8, - srv_unref: unsafe extern "C" fn(data: *const u8), - srv_handler: unsafe extern "C" fn(data: *const u8, msg: *const u8, size: usize), - init: *const u8, - size: usize, - f: extern "C" fn(), + srv_unref: Unref, + srv_handler: ServerHandler, + init: InitMessage, + f: fn(), ) -> *const u8 { + super::logging::init(); let client = Rc::new(ConfigClient { configure: f, srv_data, @@ -257,7 +225,6 @@ pub unsafe extern "C" fn init( on_idle: Default::default(), on_switch_event: Default::default(), on_unload: Default::default(), - bufs: Default::default(), reload: Cell::new(false), read_interests: Default::default(), write_interests: Default::default(), @@ -270,22 +237,20 @@ pub unsafe extern "C" fn init( feat_mod_mask: Cell::new(false), feat_show_workspace_on: Cell::new(false), }); - let init = unsafe { slice::from_raw_parts(init, size) }; client.handle_init_msg(init); Rc::into_raw(client) as *const u8 } -pub unsafe extern "C" fn unref(data: *const u8) { +pub unsafe fn unref(data: *const u8) { let client = data as *const ConfigClient; unsafe { drop(Rc::from_raw(client)); } } -pub unsafe extern "C" fn handle_msg(data: *const u8, msg: *const u8, size: usize) { +pub unsafe fn handle_msg(data: *const u8, msg: &ServerMessage) { unsafe { with_client(data, |client| { - let msg = slice::from_raw_parts(msg, size); client.handle_msg(msg); }); } @@ -315,13 +280,9 @@ enum GenericCriterion<'a, Crit, Matcher> { impl ConfigClient { fn send(&self, msg: &ClientMessage) { - let mut buf = self.bufs.borrow_mut().pop().unwrap_or_default(); - buf.clear(); - bincode_ops().serialize_into(&mut buf, msg).unwrap(); unsafe { - (self.srv_handler)(self.srv_data, buf.as_ptr(), buf.len()); + (self.srv_handler)(self.srv_data, msg); } - self.bufs.borrow_mut().push(buf); } fn send_with_response(&self, msg: &ClientMessage) -> Response { @@ -1363,6 +1324,14 @@ impl ConfigClient { self.send(&ClientMessage::SetIdle { timeout }) } + pub fn set_key_press_enables_dpms(&self, enabled: bool) { + self.send(&ClientMessage::SetKeyPressEnablesDpms { enabled }) + } + + pub fn set_mouse_move_enables_dpms(&self, enabled: bool) { + self.send(&ClientMessage::SetMouseMoveEnablesDpms { enabled }) + } + pub fn set_idle_grace_period(&self, period: Duration) { self.send(&ClientMessage::SetIdleGracePeriod { period }) } @@ -1564,22 +1533,6 @@ impl ConfigClient { connector } - pub fn set_client_matcher_capabilities( - &self, - matcher: ClientMatcher, - caps: ClientCapabilities, - ) { - self.send(&ClientMessage::SetClientMatcherCapabilities { matcher, caps }); - } - - pub fn set_client_matcher_bounding_capabilities( - &self, - matcher: ClientMatcher, - caps: ClientCapabilities, - ) { - self.send(&ClientMessage::SetClientMatcherBoundingCapabilities { matcher, caps }); - } - pub fn latch(&self, seat: Seat, f: F) { if !self.feat_mod_mask.get() { log::error!("compositor does not support latching"); @@ -1672,6 +1625,7 @@ impl ConfigClient { } } + #[allow(dead_code)] pub fn log(&self, level: LogLevel, msg: &str, file: Option<&str>, line: Option) { self.send(&ClientMessage::Log { level, @@ -1681,12 +1635,6 @@ impl ConfigClient { }) } - pub fn get_socket_path(&self) -> Option { - let res = self.send_with_response(&ClientMessage::GetSocketPath); - get_response!(res, None, GetSocketPath { path }); - Some(path) - } - pub fn create_pollable(&self, fd: i32) -> Result { let res = self.send_with_response(&ClientMessage::AddPollable { fd }); get_response!( @@ -1768,7 +1716,7 @@ impl ConfigClient { criterion: GenericCriterion<'_, Crit, Matcher>, child: bool, create_child_matcher: impl Fn(Crit) -> (Matcher, bool), - create_matcher: impl Fn(GenericCriterionIpc) -> Matcher, + create_matcher: impl Fn(GenericCriterionPayload) -> Matcher, destroy_matcher: impl Fn(Matcher), ) -> (Matcher, bool) where @@ -1795,18 +1743,18 @@ impl ConfigClient { if child { return (m, false); } - GenericCriterionIpc::Matcher(m) + GenericCriterionPayload::Matcher(m) } - GenericCriterion::Not(c) => GenericCriterionIpc::Not(create_child_matcher(*c)), - GenericCriterion::All(l) => GenericCriterionIpc::List { + GenericCriterion::Not(c) => GenericCriterionPayload::Not(create_child_matcher(*c)), + GenericCriterion::All(l) => GenericCriterionPayload::List { list: create_vec(l), all: true, }, - GenericCriterion::Any(l) => GenericCriterionIpc::List { + GenericCriterion::Any(l) => GenericCriterionPayload::List { list: create_vec(l), all: false, }, - GenericCriterion::Exactly(num, l) => GenericCriterionIpc::Exactly { + GenericCriterion::Exactly(num, l) => GenericCriterionPayload::Exactly { list: create_vec(l), num, }, @@ -1829,7 +1777,7 @@ impl ConfigClient { ) -> (ClientMatcher, bool) { macro_rules! string { ($t:expr, $field:ident, $regex:expr) => { - ClientCriterionIpc::String { + ClientCriterionPayload::String { string: $t.to_string(), field: ClientCriterionStringField::$field, regex: $regex, @@ -1838,7 +1786,7 @@ impl ConfigClient { } let create_matcher = |criterion| { let res = self.send_with_response(&ClientMessage::CreateClientMatcher { - criterion: ClientCriterionIpc::Generic(criterion), + criterion: ClientCriterionPayload::Generic(criterion), }); get_response!(res, ClientMatcher(0), CreateClientMatcher { matcher }); matcher @@ -1867,10 +1815,10 @@ impl ConfigClient { ClientCriterion::SandboxAppIdRegex(t) => string!(t, SandboxAppId, true), ClientCriterion::SandboxInstanceId(t) => string!(t, SandboxInstanceId, false), ClientCriterion::SandboxInstanceIdRegex(t) => string!(t, SandboxInstanceId, true), - ClientCriterion::Sandboxed => ClientCriterionIpc::Sandboxed, - ClientCriterion::Uid(p) => ClientCriterionIpc::Uid(p), - ClientCriterion::Pid(p) => ClientCriterionIpc::Pid(p), - ClientCriterion::IsXwayland => ClientCriterionIpc::IsXwayland, + ClientCriterion::Sandboxed => ClientCriterionPayload::Sandboxed, + ClientCriterion::Uid(p) => ClientCriterionPayload::Uid(p), + ClientCriterion::Pid(p) => ClientCriterionPayload::Pid(p), + ClientCriterion::IsXwayland => ClientCriterionPayload::IsXwayland, ClientCriterion::Comm(t) => string!(t, Comm, false), ClientCriterion::CommRegex(t) => string!(t, Comm, true), ClientCriterion::Exe(t) => string!(t, Exe, false), @@ -1932,7 +1880,7 @@ impl ConfigClient { ) -> (WindowMatcher, bool) { macro_rules! string { ($t:expr, $field:ident, $regex:expr) => { - WindowCriterionIpc::String { + WindowCriterionPayload::String { string: $t.to_string(), field: WindowCriterionStringField::$field, regex: $regex, @@ -1941,7 +1889,7 @@ impl ConfigClient { } let create_matcher = |criterion| { let res = self.send_with_response(&ClientMessage::CreateWindowMatcher { - criterion: WindowCriterionIpc::Generic(criterion), + criterion: WindowCriterionPayload::Generic(criterion), }); get_response!(res, WindowMatcher(0), CreateWindowMatcher { matcher }); matcher @@ -1965,24 +1913,24 @@ impl ConfigClient { WindowCriterion::All(c) => return generic(GenericCriterion::All(c)), WindowCriterion::Any(c) => return generic(GenericCriterion::Any(c)), WindowCriterion::Exactly(n, c) => return generic(GenericCriterion::Exactly(n, c)), - WindowCriterion::Types(t) => WindowCriterionIpc::Types(t), + WindowCriterion::Types(t) => WindowCriterionPayload::Types(t), WindowCriterion::Client(c) => { let (matcher, original) = self.create_client_matcher_(*c, true); if original { _destroy_client_matcher = on_drop(move || matcher.destroy()); } - WindowCriterionIpc::Client(matcher) + WindowCriterionPayload::Client(matcher) } WindowCriterion::Title(t) => string!(t, Title, false), WindowCriterion::TitleRegex(t) => string!(t, Title, true), WindowCriterion::AppId(t) => string!(t, AppId, false), WindowCriterion::AppIdRegex(t) => string!(t, AppId, true), - WindowCriterion::Floating => WindowCriterionIpc::Floating, - WindowCriterion::Visible => WindowCriterionIpc::Visible, - WindowCriterion::Urgent => WindowCriterionIpc::Urgent, - WindowCriterion::Focus(seat) => WindowCriterionIpc::SeatFocus(seat), - WindowCriterion::Fullscreen => WindowCriterionIpc::Fullscreen, - WindowCriterion::JustMapped => WindowCriterionIpc::JustMapped, + WindowCriterion::Floating => WindowCriterionPayload::Floating, + WindowCriterion::Visible => WindowCriterionPayload::Visible, + WindowCriterion::Urgent => WindowCriterionPayload::Urgent, + WindowCriterion::Focus(seat) => WindowCriterionPayload::SeatFocus(seat), + WindowCriterion::Fullscreen => WindowCriterionPayload::Fullscreen, + WindowCriterion::JustMapped => WindowCriterionPayload::JustMapped, WindowCriterion::Tag(t) => string!(t, Tag, false), WindowCriterion::TagRegex(t) => string!(t, Tag, true), WindowCriterion::XClass(t) => string!(t, XClass, false), @@ -1991,10 +1939,10 @@ impl ConfigClient { WindowCriterion::XInstanceRegex(t) => string!(t, XInstance, true), WindowCriterion::XRole(t) => string!(t, XRole, false), WindowCriterion::XRoleRegex(t) => string!(t, XRole, true), - WindowCriterion::Workspace(t) => WindowCriterionIpc::Workspace(t), + WindowCriterion::Workspace(t) => WindowCriterionPayload::Workspace(t), WindowCriterion::WorkspaceName(t) => string!(t, Workspace, false), WindowCriterion::WorkspaceNameRegex(t) => string!(t, Workspace, true), - WindowCriterion::ContentTypes(t) => WindowCriterionIpc::ContentTypes(t), + WindowCriterion::ContentTypes(t) => WindowCriterionPayload::ContentTypes(t), }; let res = self.send_with_response(&ClientMessage::CreateWindowMatcher { criterion }); get_response!( @@ -2109,7 +2057,7 @@ impl ConfigClient { self.send(&ClientMessage::SeatMoveTab { seat, right }); } - fn handle_msg(&self, msg: &[u8]) { + fn handle_msg(&self, msg: &ServerMessage) { self.handle_msg2(msg); self.dispatch_futures(); } @@ -2240,16 +2188,8 @@ impl ConfigClient { } } - fn handle_msg2(&self, msg: &[u8]) { - let res = bincode_ops().deserialize::(msg); - let msg = match res { - Ok(msg) => msg, - Err(e) => { - let msg = format!("could not deserialize message: {}", e); - self.log(LogLevel::Error, &msg, None, None); - return; - } - }; + fn handle_msg2(&self, msg: &ServerMessage) { + let msg = msg.clone(); match msg { ServerMessage::Configure { reload } => { self.reload.set(reload); @@ -2424,15 +2364,7 @@ impl ConfigClient { } } - fn handle_init_msg(&self, msg: &[u8]) { - let init = match bincode_ops().deserialize::(msg) { - Ok(m) => m, - Err(e) => { - let msg = format!("could not deserialize message: {}", e); - self.log(LogLevel::Error, &msg, None, None); - return; - } - }; + fn handle_init_msg(&self, init: InitMessage) { match init { InitMessage::V1(_) => {} } diff --git a/jay-config/src/_private/logging.rs b/crates/jay-config/src/_private/logging.rs similarity index 100% rename from jay-config/src/_private/logging.rs rename to crates/jay-config/src/_private/logging.rs diff --git a/jay-config/src/client.rs b/crates/jay-config/src/client.rs similarity index 51% rename from jay-config/src/client.rs rename to crates/jay-config/src/client.rs index 38a82d42..5a2e4173 100644 --- a/jay-config/src/client.rs +++ b/crates/jay-config/src/client.rs @@ -110,19 +110,6 @@ impl ClientCriterion<'_> { self.to_matcher().bind(cb); } - /// Sets the capabilities granted to clients matching this matcher. - /// - /// This leaks the matcher. - pub fn set_capabilities(self, caps: ClientCapabilities) { - self.to_matcher().set_capabilities(caps); - } - - /// Sets the upper capability bounds for clients in sandboxes created by this client. - /// - /// This leaks the matcher. - pub fn set_sandbox_bounding_capabilities(self, caps: ClientCapabilities) { - self.to_matcher().set_sandbox_bounding_capabilities(caps); - } } impl ClientMatcher { @@ -140,35 +127,6 @@ impl ClientMatcher { get!().set_client_matcher_handler(self, cb); } - /// Sets the capabilities granted to clients matching this matcher. - /// - /// If multiple matchers match a client, the capabilities are added. - /// - /// If no matcher matches a client, it is granted the default capabilities depending - /// on whether it's sandboxed or not. If it is not sandboxed, it is granted the - /// capabilities [`CC_LAYER_SHELL`] and [`CC_DRM_LEASE`]. Otherwise it is granted the - /// capability [`CC_DRM_LEASE`]. - /// - /// Regardless of any capabilities set through this function, the capabilities of the - /// client can never exceed its bounding capabilities. - pub fn set_capabilities(self, caps: ClientCapabilities) { - get!().set_client_matcher_capabilities(self, caps); - } - - /// Sets the upper capability bounds for clients in sandboxes created by this client. - /// - /// If multiple matchers match a client, the capabilities are added. - /// - /// If no matcher matches a client, the bounding capabilities for sandboxes depend on - /// whether the client is itself sandboxed. If it is sandboxed, the bounding - /// capabilities are the effective capabilities of the client. Otherwise the bounding - /// capabilities are all capabilities. - /// - /// Regardless of any capabilities set through this function, the capabilities set - /// through this function can never exceed the client's bounding capabilities. - pub fn set_sandbox_bounding_capabilities(self, caps: ClientCapabilities) { - get!().set_client_matcher_bounding_capabilities(self, caps); - } } impl MatchedClient { @@ -195,45 +153,3 @@ impl Deref for MatchedClient { &self.client } } - -bitflags! { - /// Capabilities granted to a client. - #[derive(Serialize, Deserialize, Copy, Clone, Hash, Eq, PartialEq)] - pub struct ClientCapabilities(pub u64) { - /// Grants access to the `ext_data_control_manager_v1` and - /// `zwlr_data_control_manager_v1` globals. - pub const CC_DATA_CONTROL = 1 << 0, - /// Grants access to the `zwp_virtual_keyboard_manager_v1` global. - pub const CC_VIRTUAL_KEYBOARD = 1 << 1, - /// Grants access to the `ext_foreign_toplevel_list_v1` global. - pub const CC_FOREIGN_TOPLEVEL_LIST = 1 << 2, - /// Grants access to the `ext_idle_notifier_v1` global. - pub const CC_IDLE_NOTIFIER = 1 << 3, - /// Grants access to the `ext_session_lock_manager_v1` global. - pub const CC_SESSION_LOCK = 1 << 4, - /// Grants access to the `zwlr_layer_shell_v1` global. - pub const CC_LAYER_SHELL = 1 << 6, - /// Grants access to the `ext_image_copy_capture_manager_v1` and - /// `zwlr_screencopy_manager_v1` globals. - pub const CC_SCREENCOPY = 1 << 7, - /// Grants access to the `ext_transient_seat_manager_v1` global. - pub const CC_SEAT_MANAGER = 1 << 8, - /// Grants access to the `wp_drm_lease_device_v1` global. - pub const CC_DRM_LEASE = 1 << 9, - /// Grants access to the `zwp_input_method_manager_v2` global. - pub const CC_INPUT_METHOD = 1 << 10, - /// Grants access to the `ext_workspace_manager_v1` global. - pub const CC_WORKSPACE_MANAGER = 1 << 11, - /// Grants access to the `zwlr_foreign_toplevel_manager_v1` global. - pub const CC_FOREIGN_TOPLEVEL_MANAGER = 1 << 12, - /// Grants access to the `jay_head_manager_v1` and `zwlr_output_manager_v1` - /// globals. - pub const CC_HEAD_MANAGER = 1 << 13, - /// Grants access to the `zwlr_gamma_control_manager_v1` global. - pub const CC_GAMMA_CONTROL_MANAGER = 1 << 14, - /// Grants access to the `zwlr_virtual_pointer_manager_v1` global. - pub const CC_VIRTUAL_POINTER = 1 << 15, - /// Grants access to the `ext_foreign_toplevel_geometry_tracking_manager_v1` global. - pub const CC_FOREIGN_TOPLEVEL_GEOMETRY_TRACKING = 1 << 16, - } -} diff --git a/jay-config/src/embedded.rs b/crates/jay-config/src/embedded.rs similarity index 100% rename from jay-config/src/embedded.rs rename to crates/jay-config/src/embedded.rs diff --git a/jay-config/src/exec.rs b/crates/jay-config/src/exec.rs similarity index 86% rename from jay-config/src/exec.rs rename to crates/jay-config/src/exec.rs index 61074167..bedc70c3 100644 --- a/jay-config/src/exec.rs +++ b/crates/jay-config/src/exec.rs @@ -84,21 +84,6 @@ impl Command { self.fd(2, fd) } - /// Runs the application with access to privileged wayland protocols. - /// - /// The default is `false`. - pub fn privileged(&mut self) -> &mut Self { - match get!(self).get_socket_path() { - Some(path) => { - self.env("WAYLAND_DISPLAY", &format!("{path}.jay")); - } - _ => { - log::error!("Compositor did not send the socket path"); - } - } - self - } - /// Adds a tag to Wayland connections created by the spawned command. pub fn tag(&mut self, tag: &str) -> &mut Self { self.tag = Some(tag.to_owned()); diff --git a/jay-config/src/input.rs b/crates/jay-config/src/input.rs similarity index 99% rename from jay-config/src/input.rs rename to crates/jay-config/src/input.rs index 450597e2..c052bba7 100644 --- a/jay-config/src/input.rs +++ b/crates/jay-config/src/input.rs @@ -6,10 +6,10 @@ pub mod clickmethod; use { crate::{ - _private::{DEFAULT_SEAT_NAME, ipc::WorkspaceSource}, Axis, Direction, ModifiedKeySym, Workspace, input::{acceleration::AccelProfile, capability::Capability, clickmethod::ClickMethod}, keyboard::{Keymap, mods::Modifiers, syms::KeySym}, + protocol::{DEFAULT_SEAT_NAME, WorkspaceSource}, video::Connector, window::Window, }, @@ -859,8 +859,6 @@ pub enum SwitchEvent { /// Enables or disables the unauthenticated libei socket. /// -/// Even if the socket is disabled, application can still request access via the portal. -/// /// The default is `false`. pub fn set_libei_socket_enabled(enabled: bool) { get!().set_ei_socket_enabled(enabled); diff --git a/jay-config/src/input/acceleration.rs b/crates/jay-config/src/input/acceleration.rs similarity index 100% rename from jay-config/src/input/acceleration.rs rename to crates/jay-config/src/input/acceleration.rs diff --git a/jay-config/src/input/capability.rs b/crates/jay-config/src/input/capability.rs similarity index 100% rename from jay-config/src/input/capability.rs rename to crates/jay-config/src/input/capability.rs diff --git a/jay-config/src/input/clickmethod.rs b/crates/jay-config/src/input/clickmethod.rs similarity index 100% rename from jay-config/src/input/clickmethod.rs rename to crates/jay-config/src/input/clickmethod.rs diff --git a/jay-config/src/io.rs b/crates/jay-config/src/io.rs similarity index 99% rename from jay-config/src/io.rs rename to crates/jay-config/src/io.rs index 6eece9ad..936af495 100644 --- a/jay-config/src/io.rs +++ b/crates/jay-config/src/io.rs @@ -1,7 +1,7 @@ //! Tools for IO operations. use { - crate::_private::PollableId, + crate::protocol::PollableId, futures_util::{AsyncWrite, io::AsyncRead}, std::{ future::poll_fn, diff --git a/jay-config/src/keyboard/mod.rs b/crates/jay-config/src/keyboard/mod.rs similarity index 100% rename from jay-config/src/keyboard/mod.rs rename to crates/jay-config/src/keyboard/mod.rs diff --git a/jay-config/src/keyboard/mods.rs b/crates/jay-config/src/keyboard/mods.rs similarity index 100% rename from jay-config/src/keyboard/mods.rs rename to crates/jay-config/src/keyboard/mods.rs diff --git a/jay-config/src/keyboard/syms.rs b/crates/jay-config/src/keyboard/syms.rs similarity index 100% rename from jay-config/src/keyboard/syms.rs rename to crates/jay-config/src/keyboard/syms.rs diff --git a/jay-config/src/lib.rs b/crates/jay-config/src/lib.rs similarity index 91% rename from jay-config/src/lib.rs rename to crates/jay-config/src/lib.rs index fff94506..91dbbcae 100644 --- a/jay-config/src/lib.rs +++ b/crates/jay-config/src/lib.rs @@ -1,36 +1,5 @@ -//! This crate allows you to configure the Jay compositor. -//! -//! A minimal example configuration looks as follows: -//! -//! ```rust -//! use jay_config::{config, quit, reload}; -//! use jay_config::input::get_default_seat; -//! use jay_config::keyboard::mods::ALT; -//! use jay_config::keyboard::syms::{SYM_q, SYM_r}; -//! -//! fn configure() { -//! let seat = get_default_seat(); -//! // Create a key binding to exit the compositor. -//! seat.bind(ALT | SYM_q, || quit()); -//! // Reload the configuration. -//! seat.bind(ALT | SYM_r, || reload()); -//! } -//! -//! config!(configure); -//! ``` -//! -//! You should configure your crate to be compiled as a shared library: -//! -//! ```toml -//! [lib] -//! crate-type = ["cdylib"] -//! ``` -//! -//! After compiling it, copy the shared library to `$HOME/.config/jay/config.so` and restart -//! the compositor. It should then use your configuration file. -//! -//! Note that you do not have to restart the compositor every time you want to reload your -//! configuration afterwards. Instead, simply invoke the [`reload`] function via a shortcut. +//! Internal Rust configuration API used by Jay's built-in TOML configuration +//! implementation. #![allow( clippy::zero_prefixed_literal, @@ -48,7 +17,7 @@ use crate::input::Seat; use { crate::{ - _private::ipc::WorkspaceSource, keyboard::ModifiedKeySym, video::Connector, window::Window, + keyboard::ModifiedKeySym, protocol::WorkspaceSource, video::Connector, window::Window, }, serde::{Deserialize, Serialize}, std::{ @@ -68,6 +37,7 @@ pub mod input; pub mod io; pub mod keyboard; pub mod logging; +pub mod protocol; pub mod status; pub mod tasks; pub mod theme; @@ -273,6 +243,20 @@ pub fn set_idle(timeout: Option) { get!().set_idle(timeout.unwrap_or_default()) } +/// Configures whether a key press turns monitors back on after `jay dpms off`. +/// +/// The default is `false`. +pub fn set_key_press_enables_dpms(enabled: bool) { + get!().set_key_press_enables_dpms(enabled) +} + +/// Configures whether mouse movement turns monitors back on after `jay dpms off`. +/// +/// The default is `false`. +pub fn set_mouse_move_enables_dpms(enabled: bool) { + get!().set_mouse_move_enables_dpms(enabled) +} + /// Configures the idle grace period. /// /// The grace period starts after the idle timeout expires. During the grace period, the diff --git a/jay-config/src/logging.rs b/crates/jay-config/src/logging.rs similarity index 100% rename from jay-config/src/logging.rs rename to crates/jay-config/src/logging.rs diff --git a/jay-config/src/macros.rs b/crates/jay-config/src/macros.rs similarity index 85% rename from jay-config/src/macros.rs rename to crates/jay-config/src/macros.rs index 03b87581..fca5db04 100644 --- a/jay-config/src/macros.rs +++ b/crates/jay-config/src/macros.rs @@ -1,21 +1,3 @@ -/// Declares the entry point of the configuration. -#[macro_export] -macro_rules! config { - ($f:path) => { - #[unsafe(no_mangle)] - #[used] - pub static mut JAY_CONFIG_ENTRY_V1: $crate::_private::ConfigEntry = { - struct X; - impl $crate::_private::Config for X { - extern "C" fn configure() { - $f(); - } - } - $crate::_private::ConfigEntryGen::::ENTRY - }; - }; -} - macro_rules! try_get { () => {{ unsafe { diff --git a/jay-config/src/_private/ipc.rs b/crates/jay-config/src/protocol.rs similarity index 86% rename from jay-config/src/_private/ipc.rs rename to crates/jay-config/src/protocol.rs index 743acc57..e45224a0 100644 --- a/jay-config/src/_private/ipc.rs +++ b/crates/jay-config/src/protocol.rs @@ -1,8 +1,7 @@ use { crate::{ - _private::{ClientCriterionIpc, PollableId, WindowCriterionIpc, WireMode}, Axis, Direction, PciId, Workspace, - client::{Client, ClientCapabilities, ClientMatcher}, + client::{Client, ClientMatcher}, input::{ FallbackOutputMode, FocusFollowsMouseMode, InputDevice, LayerDirection, Seat, SwitchEvent, Timeline, acceleration::AccelProfile, capability::Capability, @@ -13,7 +12,7 @@ use { theme::{BarPosition, Color, colors::Colorable, sized::Resizable}, timer::Timer, video::{ - BlendSpace, ColorSpace, Connector, DrmDevice, Eotf, Format, GfxApi, TearingMode, + BlendSpace, ColorSpace, Connector, DrmDevice, Eotf, Format, GfxApi, Mode, TearingMode, Transform, VrrMode, connector_type::ConnectorType, }, window::{ContentType, TileState, Window, WindowMatcher, WindowType}, @@ -24,7 +23,134 @@ use { std::time::{Duration, SystemTime}, }; -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub const VERSION: u32 = 1; + +pub type ServerHandler = unsafe fn(data: *const u8, msg: &ClientMessage<'_>); +pub type ConfigHandler = unsafe fn(data: *const u8, msg: &ServerMessage); +pub type Unref = unsafe fn(data: *const u8); + +pub struct ConfigEntry { + pub version: u32, + pub init: unsafe fn( + srv_data: *const u8, + srv_unref: Unref, + srv_handler: ServerHandler, + msg: InitMessage, + ) -> *const u8, + pub unref: Unref, + pub handle_msg: ConfigHandler, +} + +pub unsafe fn init_client( + srv_data: *const u8, + srv_unref: Unref, + srv_handler: ServerHandler, + msg: InitMessage, + configure: fn(), +) -> *const u8 { + unsafe { + crate::_private::client::init(srv_data, srv_unref, srv_handler, msg, configure) + } +} + +pub unsafe fn unref_client(data: *const u8) { + unsafe { + crate::_private::client::unref(data); + } +} + +pub unsafe fn handle_client_message(data: *const u8, msg: &ServerMessage) { + unsafe { + crate::_private::client::handle_msg(data, msg); + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct WireMode { + pub width: i32, + pub height: i32, + pub refresh_millihz: u32, +} + +impl WireMode { + pub fn to_mode(self) -> Mode { + Mode { + width: self.width, + height: self.height, + refresh_millihz: self.refresh_millihz, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct PollableId(pub u64); + +pub const DEFAULT_SEAT_NAME: &str = "default"; + +#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] +pub enum GenericCriterionPayload { + Matcher(T), + Not(T), + List { list: Vec, all: bool }, + Exactly { list: Vec, num: usize }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] +pub enum ClientCriterionPayload { + Generic(GenericCriterionPayload), + String { + string: String, + field: ClientCriterionStringField, + regex: bool, + }, + Sandboxed, + Uid(i32), + Pid(i32), + IsXwayland, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] +pub enum ClientCriterionStringField { + SandboxEngine, + SandboxAppId, + SandboxInstanceId, + Comm, + Exe, + Tag, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] +pub enum WindowCriterionPayload { + Generic(GenericCriterionPayload), + String { + string: String, + field: WindowCriterionStringField, + regex: bool, + }, + Types(WindowType), + Client(ClientMatcher), + Floating, + Visible, + Urgent, + SeatFocus(Seat), + Fullscreen, + JustMapped, + Workspace(Workspace), + ContentTypes(ContentType), +} + +#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] +pub enum WindowCriterionStringField { + Title, + AppId, + Tag, + XClass, + XInstance, + XRole, + Workspace, +} + +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Eq, PartialEq)] #[serde(transparent)] pub struct ServerFeature(u16); @@ -34,7 +160,7 @@ impl ServerFeature { pub const SHOW_WORKSPACE_ON: Self = Self(2); } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub enum ServerMessage { Configure { reload: bool, @@ -115,7 +241,7 @@ pub enum ServerMessage { }, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub enum ClientMessage<'a> { Reload, Quit, @@ -444,6 +570,13 @@ pub enum ClientMessage<'a> { env: Vec<(String, String)>, fds: Vec<(i32, i32)>, }, + Run3 { + prog: &'a str, + args: Vec, + env: Vec<(String, String)>, + fds: Vec<(i32, i32)>, + tag: Option<&'a str>, + }, DisableDefaultSeat, DestroyKeymap { keymap: Keymap, @@ -487,6 +620,12 @@ pub enum ClientMessage<'a> { SetIdle { timeout: Duration, }, + SetKeyPressEnablesDpms { + enabled: bool, + }, + SetMouseMoveEnablesDpms { + enabled: bool, + }, MoveToOutput { workspace: WorkspaceSource, connector: Connector, @@ -494,7 +633,6 @@ pub enum ClientMessage<'a> { SetExplicitSyncEnabled { enabled: bool, }, - GetSocketPath, DeviceSetKeymap { device: InputDevice, keymap: Keymap, @@ -718,7 +856,7 @@ pub enum ClientMessage<'a> { pinned: bool, }, CreateClientMatcher { - criterion: ClientCriterionIpc, + criterion: ClientCriterionPayload, }, DestroyClientMatcher { matcher: ClientMatcher, @@ -727,7 +865,7 @@ pub enum ClientMessage<'a> { matcher: ClientMatcher, }, CreateWindowMatcher { - criterion: WindowCriterionIpc, + criterion: WindowCriterionPayload, }, DestroyWindowMatcher { matcher: WindowMatcher, @@ -816,14 +954,6 @@ pub enum ClientMessage<'a> { SetTitleFont { font: &'a str, }, - SetClientMatcherCapabilities { - matcher: ClientMatcher, - caps: ClientCapabilities, - }, - SetClientMatcherBoundingCapabilities { - matcher: ClientMatcher, - caps: ClientCapabilities, - }, ShowWorkspaceOn { seat: Seat, workspace: Workspace, @@ -878,13 +1008,6 @@ pub enum ClientMessage<'a> { SetXWaylandEnabled { enabled: bool, }, - Run3 { - prog: &'a str, - args: Vec, - env: Vec<(String, String)>, - fds: Vec<(i32, i32)>, - tag: Option<&'a str>, - }, ConnectorSupportsArbitraryModes { connector: Connector, }, @@ -949,13 +1072,13 @@ pub enum ClientMessage<'a> { }, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub enum WorkspaceSource { Seat(Seat), Explicit(Workspace), } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub enum Response { None, GetSeats { @@ -1092,9 +1215,6 @@ pub enum Response { GetInputDeviceDevnode { devnode: String, }, - GetSocketPath { - path: String, - }, GetFloatAboveFullscreen { above: bool, }, @@ -1211,10 +1331,10 @@ pub enum Response { }, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub enum InitMessage { V1(V1InitMessage), } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct V1InitMessage {} diff --git a/jay-config/src/status.rs b/crates/jay-config/src/status.rs similarity index 100% rename from jay-config/src/status.rs rename to crates/jay-config/src/status.rs diff --git a/jay-config/src/tasks.rs b/crates/jay-config/src/tasks.rs similarity index 100% rename from jay-config/src/tasks.rs rename to crates/jay-config/src/tasks.rs diff --git a/jay-config/src/theme.rs b/crates/jay-config/src/theme.rs similarity index 100% rename from jay-config/src/theme.rs rename to crates/jay-config/src/theme.rs diff --git a/jay-config/src/timer.rs b/crates/jay-config/src/timer.rs similarity index 100% rename from jay-config/src/timer.rs rename to crates/jay-config/src/timer.rs diff --git a/jay-config/src/video.rs b/crates/jay-config/src/video.rs similarity index 99% rename from jay-config/src/video.rs rename to crates/jay-config/src/video.rs index ff0680eb..5ba15d98 100644 --- a/jay-config/src/video.rs +++ b/crates/jay-config/src/video.rs @@ -2,8 +2,8 @@ use { crate::{ - _private::WireMode, Direction, PciId, Workspace, + protocol::WireMode, video::connector_type::{ CON_9PIN_DIN, CON_COMPONENT, CON_COMPOSITE, CON_DISPLAY_PORT, CON_DPI, CON_DSI, CON_DVIA, CON_DVID, CON_DVII, CON_EDP, CON_EMBEDDED_WINDOW, CON_HDMIA, CON_HDMIB, diff --git a/jay-config/src/window.rs b/crates/jay-config/src/window.rs similarity index 100% rename from jay-config/src/window.rs rename to crates/jay-config/src/window.rs diff --git a/jay-config/src/workspace.rs b/crates/jay-config/src/workspace.rs similarity index 100% rename from jay-config/src/workspace.rs rename to crates/jay-config/src/workspace.rs diff --git a/jay-config/src/xwayland.rs b/crates/jay-config/src/xwayland.rs similarity index 100% rename from jay-config/src/xwayland.rs rename to crates/jay-config/src/xwayland.rs diff --git a/crates/keyboard/Cargo.toml b/crates/keyboard/Cargo.toml new file mode 100644 index 00000000..427432ac --- /dev/null +++ b/crates/keyboard/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "jay-keyboard" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Keyboard state and keymap helpers for the Jay compositor" +repository = "https://github.com/mahkoh/jay" + +[dependencies] +jay-input-types = { path = "../input-types" } +jay-utils = { path = "../utils" } + +blake3 = "1.8.2" +kbvm = { version = "0.1.6", features = ["compose"] } +thiserror = "2.0.11" +uapi = "0.2.13" diff --git a/crates/keyboard/src/lib.rs b/crates/keyboard/src/lib.rs new file mode 100644 index 00000000..f584787d --- /dev/null +++ b/crates/keyboard/src/lib.rs @@ -0,0 +1,357 @@ +use { + jay_input_types::{ + LED_CAPS_LOCK, LED_COMPOSE, LED_KANA, LED_NUM_LOCK, LED_SCROLL_LOCK, Leds, + }, + jay_utils::{ + event_listener::EventSource, + numcell::NumCell, + oserror::{OsError, OsErrorExt, OsErrorExt2}, + syncqueue::SyncQueue, + vecset::VecSet, + }, + kbvm::{ + Components, + lookup::LookupTable, + state_machine::{self, Event, StateMachine}, + xkb::{ + self, Keymap, + diagnostic::{Diagnostic, WriteToLog}, + keymap::{Indicator, IndicatorMatcher}, + rmlvo::Group, + }, + }, + std::{ + cell::{Ref, RefCell}, + io::Write, + rc::Rc, + }, + thiserror::Error, + uapi::{OwnedFd, c}, +}; + +#[derive(Debug, Error)] +pub enum KeyboardError { + #[error("Could not create a keymap memfd")] + KeymapMemfd(#[source] OsError), + #[error("Could not copy the keymap")] + KeymapCopy(#[source] OsError), +} + +#[derive(Debug)] +pub struct KeyboardStateIds { + next: NumCell, +} + +impl Default for KeyboardStateIds { + fn default() -> Self { + Self { + next: NumCell::new(1), + } + } +} + +impl KeyboardStateIds { + pub fn next(&self) -> KeyboardStateId { + KeyboardStateId(self.next.fetch_add(1)) + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct KeyboardStateId(u64); + +impl KeyboardStateId { + pub fn raw(&self) -> u64 { + self.0 + } + + pub fn from_raw(id: u64) -> Self { + Self(id) + } +} + +impl std::fmt::Display for KeyboardStateId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } +} + +#[derive(Debug, Error)] +pub enum KbvmError { + #[error("could not parse the keymap")] + CouldNotParseKeymap(#[source] Diagnostic), + #[error("Could not create a keymap memfd")] + KeymapMemfd(#[source] OsError), +} + +pub struct KbvmContext { + pub ctx: xkb::Context, +} + +impl Default for KbvmContext { + fn default() -> Self { + let mut ctx = xkb::Context::builder(); + ctx.enable_environment(true); + Self { ctx: ctx.build() } + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct KbvmMapId([u8; 32]); + +pub struct KbvmMap { + pub id: KbvmMapId, + pub state_machine: StateMachine, + pub lookup_table: LookupTable, + pub map: KeymapFd, + pub xwayland_map: KeymapFd, + pub has_indicators: bool, + pub num_lock: Option, + pub caps_lock: Option, + pub scroll_lock: Option, + pub compose: Option, + pub kana: Option, +} + +pub struct KbvmState { + pub map: Rc, + pub state: state_machine::State, + pub kb_state: KeyboardState, +} + +pub struct KeyboardState { + pub id: KeyboardStateId, + pub map: Rc, + pub pressed_keys: VecSet, + pub mods: Components, + pub leds: Leds, + pub leds_changed: EventSource, +} + +pub trait LedsListener { + fn leds(&self, leds: Leds); +} + +pub trait DynKeyboardState { + fn borrow(&self) -> Ref<'_, KeyboardState>; +} + +impl DynKeyboardState for RefCell { + fn borrow(&self) -> Ref<'_, KeyboardState> { + self.borrow() + } +} + +impl DynKeyboardState for RefCell { + fn borrow(&self) -> Ref<'_, KeyboardState> { + Ref::map(self.borrow(), |v| &v.kb_state) + } +} + +impl KeyboardState { + pub fn apply_event(&mut self, event: Event) -> bool { + let changed = self.mods.apply_event(event); + if changed && self.map.has_indicators { + self.update_leds(); + } + changed + } + + pub fn update_leds(&mut self) { + if !self.map.has_indicators { + return; + } + let mut new = Leds::none(); + macro_rules! map_led { + ($field:ident, $led:ident) => { + if let Some(m) = &self.map.$field + && m.matches(&self.mods) + { + new |= $led; + } + }; + } + map_led!(num_lock, LED_NUM_LOCK); + map_led!(caps_lock, LED_CAPS_LOCK); + map_led!(scroll_lock, LED_SCROLL_LOCK); + map_led!(compose, LED_COMPOSE); + map_led!(kana, LED_KANA); + if new != self.leds { + self.leds = new; + for listener in self.leds_changed.iter() { + listener.leds(new); + } + } + } +} + +#[derive(Clone)] +pub struct KeymapFd { + pub map: Rc, + pub len: usize, +} + +impl KeymapFd { + pub fn create_unprotected_fd(&self) -> Result { + let fd = uapi::memfd_create("shared-keymap", c::MFD_CLOEXEC) + .map_os_err(KeyboardError::KeymapMemfd)?; + let target = self.len as c::off_t; + let mut pos = 0; + while pos < target { + let rem = target - pos; + let res = uapi::sendfile(fd.raw(), self.map.raw(), Some(&mut pos), rem as usize) + .to_os_error(); + match res { + Ok(_) | Err(OsError(c::EINTR)) => {} + Err(e) => return Err(KeyboardError::KeymapCopy(e)), + } + } + Ok(Self { + map: Rc::new(fd), + len: self.len, + }) + } +} + +impl KbvmContext { + pub fn parse_keymap(&self, keymap: &[u8]) -> Result, KbvmError> { + let map = self + .ctx + .keymap_from_bytes(WriteToLog, None, keymap) + .map_err(KbvmError::CouldNotParseKeymap)?; + let id = KbvmMapId(*blake3::hash(keymap).as_bytes()); + self.create_keymap(id, map) + } + + pub fn keymap_from_rmlvo( + &self, + rules: Option<&str>, + model: Option<&str>, + layout: Option<&str>, + variant: Option<&str>, + options: Option<&str>, + ) -> Result, KbvmError> { + let mut groups = None::>; + if layout.is_some() || variant.is_some() { + groups = Some( + Group::from_layouts_and_variants( + layout.unwrap_or_default(), + variant.unwrap_or_default(), + ) + .collect(), + ); + } + let mut options_vec = None::>; + if let Some(options) = options { + options_vec = Some(options.split(",").collect()); + } + self.keymap_from_names(rules, model, groups.as_deref(), options_vec.as_deref()) + } + + pub fn keymap_from_names( + &self, + rules: Option<&str>, + model: Option<&str>, + groups: Option<&[Group<'_>]>, + options: Option<&[&str]>, + ) -> Result, KbvmError> { + let map = self + .ctx + .keymap_from_names(WriteToLog, rules, model, groups, options); + let id = KbvmMapId(*blake3::hash(map.format().to_string().as_bytes()).as_bytes()); + self.create_keymap(id, map) + } + + fn create_keymap(&self, id: KbvmMapId, map: Keymap) -> Result, KbvmError> { + let mut has_indicators = false; + let mut num_lock = None; + let mut caps_lock = None; + let mut scroll_lock = None; + let mut compose = None; + let mut kana = None; + for indicator in map.indicators() { + match indicator.name() { + Indicator::NUM_LOCK => num_lock = Some(indicator.matcher()), + Indicator::CAPS_LOCK => caps_lock = Some(indicator.matcher()), + Indicator::SCROLL_LOCK => scroll_lock = Some(indicator.matcher()), + Indicator::COMPOSE => compose = Some(indicator.matcher()), + Indicator::KANA => kana = Some(indicator.matcher()), + _ => continue, + } + has_indicators = true; + } + let builder = map.to_builder(); + let (_, xwayland_map) = create_keymap_memfd(&map, true).map_err(KbvmError::KeymapMemfd)?; + let (_, map) = create_keymap_memfd(&map, false).map_err(KbvmError::KeymapMemfd)?; + Ok(Rc::new(KbvmMap { + id, + state_machine: builder.build_state_machine(), + map, + xwayland_map, + lookup_table: builder.build_lookup_table(), + has_indicators, + num_lock, + caps_lock, + scroll_lock, + compose, + kana, + })) + } +} + +fn create_keymap_memfd(map: &Keymap, xwayland: bool) -> Result<(String, KeymapFd), OsError> { + let mut format = map.format(); + if xwayland { + format = format.lookup_only(true).rename_long_keys(true); + } + let str = format!("{}\n", format); + let mut memfd = + uapi::memfd_create("keymap", c::MFD_CLOEXEC | c::MFD_ALLOW_SEALING).to_os_error()?; + memfd.write_all(str.as_bytes())?; + memfd.write_all(&[0])?; + uapi::lseek(memfd.raw(), 0, c::SEEK_SET).to_os_error()?; + uapi::fcntl_add_seals( + memfd.raw(), + c::F_SEAL_SEAL | c::F_SEAL_GROW | c::F_SEAL_SHRINK | c::F_SEAL_WRITE, + ) + .to_os_error()?; + let fd = KeymapFd { + map: Rc::new(memfd), + len: str.len() + 1, + }; + Ok((str, fd)) +} + +impl KbvmMap { + pub fn state(self: &Rc, id: KeyboardStateId) -> KbvmState { + KbvmState { + map: self.clone(), + state: self.state_machine.create_state(), + kb_state: KeyboardState { + id, + map: self.clone(), + pressed_keys: Default::default(), + mods: Default::default(), + leds: Default::default(), + leds_changed: Default::default(), + }, + } + } +} + +impl KbvmState { + pub fn apply_events(&mut self, events: &SyncQueue) { + let state = &mut self.kb_state; + while let Some(event) = events.pop() { + state.apply_event(event); + match event { + Event::KeyDown(kc) => { + state.pressed_keys.insert(kc.to_evdev()); + } + Event::KeyUp(kc) => { + state.pressed_keys.remove(&kc.to_evdev()); + } + _ => {} + } + } + } +} diff --git a/crates/layout-animation/Cargo.toml b/crates/layout-animation/Cargo.toml new file mode 100644 index 00000000..b9b2b842 --- /dev/null +++ b/crates/layout-animation/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "jay-layout-animation" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Layout animation planning for Jay" +repository = "https://github.com/mahkoh/jay" + +[dependencies] +jay-geometry = { path = "../geometry" } diff --git a/crates/layout-animation/src/lib.rs b/crates/layout-animation/src/lib.rs new file mode 100644 index 00000000..c9ad258e --- /dev/null +++ b/crates/layout-animation/src/lib.rs @@ -0,0 +1,3570 @@ +use jay_geometry::Rect; + +const CURVE_MAX_POINTS: usize = 33; +const CURVE_FLATNESS_EPSILON: f32 = 0.001; +const CURVE_MAX_DEPTH: u8 = 8; + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum AnimationCurve { + Linear, + Piecewise(PiecewiseCurve), +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum AnimationStyle { + Plain, + Multiphase, +} + +impl AnimationStyle { + pub fn from_config(value: u32) -> Option { + match value { + 0 => Some(Self::Plain), + 1 => Some(Self::Multiphase), + _ => None, + } + } +} + +impl AnimationCurve { + pub fn from_config(value: u32) -> Self { + match value { + 0 => Self::Linear, + 1 => Self::from_cubic_bezier(0.25, 0.1, 0.25, 1.0).unwrap(), + 2 => Self::from_cubic_bezier(0.42, 0.0, 1.0, 1.0).unwrap(), + 4 => Self::from_cubic_bezier(0.42, 0.0, 0.58, 1.0).unwrap(), + _ => Self::from_cubic_bezier(0.0, 0.0, 0.58, 1.0).unwrap(), + } + } + + pub fn from_cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> Option { + if !x1.is_finite() + || !y1.is_finite() + || !x2.is_finite() + || !y2.is_finite() + || !(0.0..=1.0).contains(&x1) + || !(0.0..=1.0).contains(&x2) + { + return None; + } + Some(Self::Piecewise(PiecewiseCurve::from_cubic_bezier( + x1, y1, x2, y2, + ))) + } + + pub fn sample(self, t: f64) -> f64 { + let t = t.clamp(0.0, 1.0); + match self { + Self::Linear => t, + Self::Piecewise(curve) => curve.sample(t as f32) as f64, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct PiecewiseCurve { + len: u8, + points: [CurvePoint; CURVE_MAX_POINTS], +} + +impl PiecewiseCurve { + fn from_cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> Self { + let mut points = Vec::with_capacity(CURVE_MAX_POINTS); + let p0 = cubic_bezier_point(x1, y1, x2, y2, 0.0); + let p1 = cubic_bezier_point(x1, y1, x2, y2, 1.0); + points.push(p0); + flatten_cubic_bezier(&mut points, (x1, y1, x2, y2), 0.0, p0, 1.0, p1, 0); + let mut array = [CurvePoint::default(); CURVE_MAX_POINTS]; + let len = points.len().min(CURVE_MAX_POINTS); + array[..len].copy_from_slice(&points[..len]); + Self { + len: len as u8, + points: array, + } + } + + fn sample(self, x: f32) -> f32 { + let len = self.len as usize; + if len <= 1 { + return x; + } + let points = &self.points[..len]; + if x <= points[0].x { + return points[0].y; + } + if x >= points[len - 1].x { + return points[len - 1].y; + } + let mut lo = 0; + let mut hi = len - 1; + while lo + 1 < hi { + let mid = (lo + hi) / 2; + if points[mid].x <= x { + lo = mid; + } else { + hi = mid; + } + } + let from = points[lo]; + let to = points[hi]; + if to.x <= from.x { + return to.y; + } + let t = (x - from.x) / (to.x - from.x); + from.y + (to.y - from.y) * t + } +} + +#[derive(Copy, Clone, Debug, Default, PartialEq)] +struct CurvePoint { + x: f32, + y: f32, +} + +fn flatten_cubic_bezier( + points: &mut Vec, + controls: (f32, f32, f32, f32), + t0: f32, + p0: CurvePoint, + t1: f32, + p1: CurvePoint, + depth: u8, +) { + let tm = (t0 + t1) * 0.5; + let pm = cubic_bezier_point(controls.0, controls.1, controls.2, controls.3, tm); + let projected_y = if p1.x <= p0.x { + (p0.y + p1.y) * 0.5 + } else { + let tx = (pm.x - p0.x) / (p1.x - p0.x); + p0.y + (p1.y - p0.y) * tx + }; + if (pm.y - projected_y).abs() > CURVE_FLATNESS_EPSILON + && depth < CURVE_MAX_DEPTH + && points.len() + 2 < CURVE_MAX_POINTS + { + flatten_cubic_bezier(points, controls, t0, p0, tm, pm, depth + 1); + flatten_cubic_bezier(points, controls, tm, pm, t1, p1, depth + 1); + } else { + points.push(p1); + } +} + +fn cubic_bezier_point(x1: f32, y1: f32, x2: f32, y2: f32, t: f32) -> CurvePoint { + fn bezier(a: f32, b: f32, t: f32) -> f32 { + let inv = 1.0 - t; + 3.0 * inv * inv * t * a + 3.0 * inv * t * t * b + t * t * t + } + CurvePoint { + x: bezier(x1, x2, t), + y: bezier(y1, y2, t), + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct NodeId(pub u32); + +const MIN_SHRINK_DENOMINATOR: i32 = 8; +// Integer split remainders can make swapped siblings differ by one pixel. Do +// not spend a full animation phase on that imperceptible bookkeeping step. +const SWAP_AXIS_SNAP_PX: i32 = 1; + +#[derive(Clone, Debug)] +pub struct MultiphaseRequest { + pub bounds: Rect, + pub windows: Vec, + pub clearance: i32, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct MultiphaseWindow { + pub node_id: NodeId, + pub from: Rect, + pub to: Rect, + pub hierarchy: MultiphaseWindowHierarchy, +} + +impl MultiphaseWindow { + pub fn new(node_id: impl Into, from: Rect, to: Rect) -> Self { + Self { + node_id: node_id.into(), + from, + to, + hierarchy: Default::default(), + } + } + + pub fn with_hierarchy( + node_id: impl Into, + from: Rect, + to: Rect, + hierarchy: MultiphaseWindowHierarchy, + ) -> Self { + Self { + node_id: node_id.into(), + from, + to, + hierarchy, + } + } +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct MultiphaseWindowHierarchy { + pub source: MultiphaseHierarchyPosition, + pub target: MultiphaseHierarchyPosition, + pub transition: MultiphaseHierarchyTransition, +} + +impl MultiphaseWindowHierarchy { + pub fn new(source: MultiphaseHierarchyPosition, target: MultiphaseHierarchyPosition) -> Self { + let transition = if !source.parent_is_mono && target.parent_is_mono { + MultiphaseHierarchyTransition::EnteringMono + } else if source.parent_is_mono && !target.parent_is_mono { + MultiphaseHierarchyTransition::ExitingMono + } else if source.parent.is_none() || target.parent.is_none() { + MultiphaseHierarchyTransition::Unknown + } else if target.depth < source.depth { + MultiphaseHierarchyTransition::Ascending + } else if target.depth > source.depth { + MultiphaseHierarchyTransition::Descending + } else { + MultiphaseHierarchyTransition::SameLevel + }; + Self { + source, + target, + transition, + } + } + + fn reversed(self) -> Self { + Self { + source: self.target, + target: self.source, + transition: self.transition.reversed(), + } + } +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct MultiphaseHierarchyPosition { + pub parent: Option, + pub depth: u16, + pub sibling_index: Option, + pub split_axis: Option, + pub nearest_horizontal_split_depth: Option, + pub nearest_vertical_split_depth: Option, + pub parent_is_mono: bool, + pub mono_active: bool, +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub enum MultiphaseHierarchyTransition { + #[default] + Unknown, + SameLevel, + Ascending, + Descending, + EnteringMono, + ExitingMono, +} + +impl MultiphaseHierarchyTransition { + fn reversed(self) -> Self { + match self { + Self::Unknown => Self::Unknown, + Self::SameLevel => Self::SameLevel, + Self::Ascending => Self::Descending, + Self::Descending => Self::Ascending, + Self::EnteringMono => Self::ExitingMono, + Self::ExitingMono => Self::EnteringMono, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MultiphasePlan { + pub phases: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MultiphasePlanned { + pub plan: MultiphasePlan, + pub explanation: MultiphasePlanExplanation, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MultiphasePlanExplanation { + pub strategy: PlanStrategy, + pub phases: Vec, + pub validation: ValidationExplanation, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PhaseExplanation { + pub action: MultiphasePhaseAction, + pub reason: PhaseReason, + pub nodes: Vec, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct ValidationExplanation { + pub continuous_overlap_passed: bool, + pub final_rects_matched: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PlanStrategy { + NoOp, + SingleAction, + MixedSinglePhase, + HierarchyOrderedScales, + OrientationChange { from_axis: PhaseAxis }, + SwapLanes { axis: PhaseAxis }, + SpaceThenOrthogonalGrowth { axis: PhaseAxis }, + ReversedForwardPlan { original: Box }, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum PlanDirection { + Forward, + Reverse, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RejectedStrategy { + pub direction: PlanDirection, + pub strategy: PlanStrategy, + pub reason: MultiphasePlanFailure, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum PhaseReason { + SingleAction, + SameAxisRedistribution, + MixedAxisActions, + ShrinkIntoLanes { + lane_axis: PhaseAxis, + }, + MoveThroughFreedSpace, + GrowOutOfLanes, + CreateSpaceForAscendingChild, + MoveAscendingChildAfterSpaceExists, + OrthogonalGrowthAfterMove, + ParentAxisBeforeChildAxis { + parent_axis: PhaseAxis, + parent_depth: u16, + child_axis: PhaseAxis, + child_depth: u16, + }, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MultiphasePhase { + pub action: MultiphasePhaseAction, + pub steps: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum MultiphasePhaseAction { + Uniform(PhaseAction), + Mixed(Vec), +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct MultiphaseStep { + pub node_id: NodeId, + pub from: Rect, + pub to: Rect, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct PhaseAction { + pub kind: PhaseKind, + pub axis: PhaseAxis, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum PhaseKind { + Move, + Scale, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum PhaseAxis { + Horizontal, + Vertical, +} + +impl MultiphasePhaseAction { + fn from_step_actions(actions: Vec) -> Self { + debug_assert!(!actions.is_empty()); + let first = actions[0]; + if actions.iter().all(|action| *action == first) { + Self::Uniform(first) + } else { + Self::Mixed(actions) + } + } + + fn action_for_step(&self, idx: usize) -> Option { + match self { + Self::Uniform(action) => Some(*action), + Self::Mixed(actions) => actions.get(idx).copied(), + } + } + + #[cfg_attr(not(test), expect(dead_code))] + fn as_uniform(&self) -> Option { + match self { + Self::Uniform(action) => Some(*action), + Self::Mixed(_) => None, + } + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum MultiphaseError { + EmptyBounds, + EmptyWindow, + DuplicateWindow, + InitialOverlap, + FinalOverlap, + NoPlan, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MultiphasePlanDiagnostic { + pub forward: MultiphasePlanFailure, + pub reverse: Option, + pub attempted: Vec, +} + +impl MultiphasePlanDiagnostic { + fn legacy_error(self) -> MultiphaseError { + match self.forward { + MultiphasePlanFailure::Request(error) => error, + _ => MultiphaseError::NoPlan, + } + } +} + +impl ValidationExplanation { + fn passed() -> Self { + Self { + continuous_overlap_passed: true, + final_rects_matched: true, + } + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum MultiphasePlanFailure { + Request(MultiphaseError), + NoPattern, + ShrinkBound { + axis: PhaseAxis, + available: i32, + required: i32, + }, + InvalidPhaseStep { + action: PhaseAction, + node_id: NodeId, + }, + Validation(MultiphaseValidationError), +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum MultiphaseValidationError { + DuplicatePhaseStep { + phase: usize, + node_id: NodeId, + }, + PhaseActionCount { + phase: usize, + actions: usize, + steps: usize, + }, + UnknownPhaseStep { + phase: usize, + node_id: NodeId, + }, + StaleStepStart { + phase: usize, + node_id: NodeId, + }, + PhaseOverlap { + phase: usize, + a: NodeId, + b: NodeId, + }, + FinalMismatch { + node_id: NodeId, + }, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct PlanForwardFailure { + reason: MultiphasePlanFailure, + attempted: Vec, +} + +pub fn plan_no_overlap(request: &MultiphaseRequest) -> Result { + plan_no_overlap_with_diagnostics(request).map_err(|diagnostic| diagnostic.legacy_error()) +} + +pub fn plan_no_overlap_with_diagnostics( + request: &MultiphaseRequest, +) -> Result { + plan_no_overlap_explained(request).map(|planned| planned.plan) +} + +pub fn plan_no_overlap_explained( + request: &MultiphaseRequest, +) -> Result { + if let Err(error) = validate_request(request) { + return Err(MultiphasePlanDiagnostic { + forward: MultiphasePlanFailure::Request(error), + reverse: None, + attempted: vec![], + }); + } + if request + .windows + .iter() + .all(|window| window.from == window.to) + { + return Ok(MultiphasePlanned { + plan: MultiphasePlan { phases: vec![] }, + explanation: MultiphasePlanExplanation { + strategy: PlanStrategy::NoOp, + phases: vec![], + validation: ValidationExplanation::passed(), + }, + }); + } + if let Some(failure) = target_shrink_bound_failure(request) { + return Err(MultiphasePlanDiagnostic { + forward: failure, + reverse: None, + attempted: vec![], + }); + } + let forward = match plan_forward(request, PlanDirection::Forward) { + Ok(plan) => return Ok(plan), + Err(error) => error, + }; + let reversed = reverse_request(request); + match plan_forward(&reversed, PlanDirection::Reverse) { + Ok(plan) => Ok(reverse_planned(plan)), + Err(reverse) => { + let mut attempted = forward.attempted; + attempted.extend(reverse.attempted); + Err(MultiphasePlanDiagnostic { + forward: forward.reason, + reverse: Some(reverse.reason), + attempted, + }) + } + } +} + +pub fn validate_phase_paths( + request: &MultiphaseRequest, + paths: &[Vec<(Rect, Rect)>], +) -> Result { + if paths.len() != request.windows.len() { + return Err(MultiphasePlanFailure::NoPattern); + } + let phase_count = paths.iter().map(Vec::len).max().unwrap_or(0); + if phase_count == 0 { + return Err(MultiphasePlanFailure::NoPattern); + } + let mut phases = vec![]; + for phase_idx in 0..phase_count { + let mut steps = vec![]; + let mut actions = vec![]; + for (window_idx, path) in paths.iter().enumerate() { + let Some((from, to)) = path.get(phase_idx).copied() else { + continue; + }; + if from == to { + continue; + } + let step = MultiphaseStep { + node_id: request.windows[window_idx].node_id, + from, + to, + }; + let Some(action) = classify_step(step) else { + return Err(MultiphasePlanFailure::NoPattern); + }; + steps.push(step); + actions.push(action); + } + if !steps.is_empty() { + phases.push(MultiphasePhase { + action: MultiphasePhaseAction::from_step_actions(actions), + steps, + }); + } + } + let plan = MultiphasePlan { phases }; + validate_plan_continuous_diagnostic(request, &plan) + .map(|_| plan) + .map_err(MultiphasePlanFailure::Validation) +} + +pub fn partition_motion_groups( + windows: &[MultiphaseWindow], + clearance: i32, +) -> Vec> { + let clearance = clearance.max(0); + let mut groups = vec![]; + let mut seen = vec![false; windows.len()]; + for start in 0..windows.len() { + if seen[start] { + continue; + } + seen[start] = true; + let mut group = vec![]; + let mut pending = vec![start]; + while let Some(idx) = pending.pop() { + group.push(idx); + let bounds = motion_bounds_with_clearance(windows[idx], clearance); + for other in 0..windows.len() { + if seen[other] + || !bounds.intersects(&motion_bounds_with_clearance(windows[other], clearance)) + { + continue; + } + seen[other] = true; + pending.push(other); + } + } + group.sort_unstable(); + groups.push(group); + } + groups +} + +fn validate_request(request: &MultiphaseRequest) -> Result<(), MultiphaseError> { + if request.bounds.is_empty() { + return Err(MultiphaseError::EmptyBounds); + } + for (idx, window) in request.windows.iter().enumerate() { + if window.from.is_empty() || window.to.is_empty() { + return Err(MultiphaseError::EmptyWindow); + } + for other in &request.windows[..idx] { + if other.node_id == window.node_id { + return Err(MultiphaseError::DuplicateWindow); + } + } + } + if overlaps(request.windows.iter().map(|window| window.from)) { + return Err(MultiphaseError::InitialOverlap); + } + if overlaps(request.windows.iter().map(|window| window.to)) { + return Err(MultiphaseError::FinalOverlap); + } + Ok(()) +} + +fn target_shrink_bound_failure(request: &MultiphaseRequest) -> Option { + let min_width = sane_min_size(request.bounds.width()); + let min_height = sane_min_size(request.bounds.height()); + for window in &request.windows { + if window.to.width() < min_width { + return Some(MultiphasePlanFailure::ShrinkBound { + axis: PhaseAxis::Horizontal, + available: window.to.width(), + required: min_width, + }); + } + if window.to.height() < min_height { + return Some(MultiphasePlanFailure::ShrinkBound { + axis: PhaseAxis::Vertical, + available: window.to.height(), + required: min_height, + }); + } + } + None +} + +fn plan_forward( + request: &MultiphaseRequest, + direction: PlanDirection, +) -> Result { + let mut rejection = None; + let mut attempted = vec![]; + match plan_single_action_phase(request) { + Ok(plan) => return Ok(plan), + Err(error) => { + record_rejection(&mut attempted, direction, PlanStrategy::SingleAction, error); + if error != MultiphasePlanFailure::NoPattern { + rejection.get_or_insert(error); + } + } + } + for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { + match plan_space_then_orthogonal_growth(request, axis) { + Ok(plan) => return Ok(plan), + Err(error) => { + record_rejection( + &mut attempted, + direction, + PlanStrategy::SpaceThenOrthogonalGrowth { axis }, + error, + ); + if error != MultiphasePlanFailure::NoPattern { + rejection.get_or_insert(error); + } + } + } + } + match plan_hierarchy_ordered_axis_scales(request) { + Ok(plan) => return Ok(plan), + Err(error) => { + record_rejection( + &mut attempted, + direction, + PlanStrategy::HierarchyOrderedScales, + error, + ); + if error != MultiphasePlanFailure::NoPattern { + rejection.get_or_insert(error); + } + } + } + for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { + match plan_orientation_change(request, axis) { + Ok(plan) => return Ok(plan), + Err(error) => { + record_rejection( + &mut attempted, + direction, + PlanStrategy::OrientationChange { from_axis: axis }, + error, + ); + if error != MultiphasePlanFailure::NoPattern { + rejection.get_or_insert(error); + } + } + } + } + for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { + match plan_axis_crossing_lanes(request, axis) { + Ok(plan) => return Ok(plan), + Err(error) => { + record_rejection( + &mut attempted, + direction, + PlanStrategy::SwapLanes { axis }, + error, + ); + if error != MultiphasePlanFailure::NoPattern { + rejection.get_or_insert(error); + } + } + } + } + Err(PlanForwardFailure { + reason: rejection.unwrap_or(MultiphasePlanFailure::NoPattern), + attempted, + }) +} + +fn record_rejection( + attempted: &mut Vec, + direction: PlanDirection, + strategy: PlanStrategy, + reason: MultiphasePlanFailure, +) { + attempted.push(RejectedStrategy { + direction, + strategy, + reason, + }); +} + +fn plan_single_action_phase( + request: &MultiphaseRequest, +) -> Result { + let mut uniform_action = None; + let mut is_uniform = true; + let mut steps = vec![]; + let mut step_actions = vec![]; + for window in &request.windows { + if window.from == window.to { + continue; + } + let step = MultiphaseStep { + node_id: window.node_id, + from: window.from, + to: window.to, + }; + let Some(step_action) = classify_step(step) else { + return Err(MultiphasePlanFailure::NoPattern); + }; + if step_action.kind == PhaseKind::Scale { + let (available, required) = match step_action.axis { + PhaseAxis::Horizontal => (window.to.width(), sane_min_size(request.bounds.width())), + PhaseAxis::Vertical => (window.to.height(), sane_min_size(request.bounds.height())), + }; + if available < required { + return Err(MultiphasePlanFailure::ShrinkBound { + axis: step_action.axis, + available, + required, + }); + } + } + if uniform_action.is_some_and(|action| action != step_action) { + is_uniform = false; + } + uniform_action.get_or_insert(step_action); + steps.push(step); + step_actions.push(step_action); + } + if steps.is_empty() { + return Err(MultiphasePlanFailure::NoPattern); + } + if !is_uniform { + return build_validated_plan( + request, + PlanStrategy::MixedSinglePhase, + [phase_draft_mixed( + steps, + step_actions, + PhaseReason::MixedAxisActions, + )], + ); + } + let action = uniform_action.unwrap(); + build_validated_plan( + request, + PlanStrategy::SingleAction, + [phase_draft_uniform( + action, + steps, + single_action_reason(action), + )], + ) +} + +fn plan_hierarchy_ordered_axis_scales( + request: &MultiphaseRequest, +) -> Result { + let mut changed_axes = vec![]; + for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { + if request + .windows + .iter() + .any(|window| interval_changed(window.from, window.to, axis)) + { + changed_axes.push(axis); + } + } + let [first_axis, second_axis] = changed_axes + .try_into() + .map_err(|_| MultiphasePlanFailure::NoPattern)?; + let order = hierarchy_scale_axis_order(request, first_axis, second_axis) + .ok_or(MultiphasePlanFailure::NoPattern)?; + let mut current: Vec<_> = request + .windows + .iter() + .map(|window| (window.node_id, window.from)) + .collect(); + let mut phases = vec![]; + let reason = PhaseReason::ParentAxisBeforeChildAxis { + parent_axis: order.axes[0], + parent_depth: order.depths[0], + child_axis: order.axes[1], + child_depth: order.depths[1], + }; + for axis in order.axes { + let mut steps = vec![]; + for window in &request.windows { + let (_, rect) = current + .iter_mut() + .find(|(node_id, _)| *node_id == window.node_id) + .unwrap(); + let next = with_main_interval( + *rect, + axis, + main_start(window.to, axis), + main_end(window.to, axis), + ); + if next == *rect { + continue; + } + if main_size(*rect, axis) == main_size(next, axis) { + return Err(MultiphasePlanFailure::NoPattern); + } + steps.push(MultiphaseStep { + node_id: window.node_id, + from: *rect, + to: next, + }); + *rect = next; + } + if steps.is_empty() { + return Err(MultiphasePlanFailure::NoPattern); + } + phases.push(phase_draft(PhaseKind::Scale, axis, steps, reason)); + } + let [first, second] = phases + .try_into() + .map_err(|_| MultiphasePlanFailure::NoPattern)?; + build_validated_plan( + request, + PlanStrategy::HierarchyOrderedScales, + [first, second], + ) +} + +fn hierarchy_scale_axis_order( + request: &MultiphaseRequest, + first_axis: PhaseAxis, + second_axis: PhaseAxis, +) -> Option { + let first_priority = hierarchy_axis_priority(request, first_axis)?; + let second_priority = hierarchy_axis_priority(request, second_axis)?; + match first_priority.cmp(&second_priority) { + std::cmp::Ordering::Less => Some(HierarchyScaleAxisOrder { + axes: [first_axis, second_axis], + depths: [first_priority, second_priority], + }), + std::cmp::Ordering::Greater => Some(HierarchyScaleAxisOrder { + axes: [second_axis, first_axis], + depths: [second_priority, first_priority], + }), + std::cmp::Ordering::Equal => None, + } +} + +#[derive(Copy, Clone)] +struct HierarchyScaleAxisOrder { + axes: [PhaseAxis; 2], + depths: [u16; 2], +} + +fn hierarchy_axis_priority(request: &MultiphaseRequest, axis: PhaseAxis) -> Option { + request + .windows + .iter() + .filter(|window| interval_changed(window.from, window.to, axis)) + .flat_map(|window| { + [ + split_depth_for_axis(window.hierarchy.source, axis), + split_depth_for_axis(window.hierarchy.target, axis), + ] + }) + .flatten() + .min() +} + +fn plan_axis_crossing_lanes( + request: &MultiphaseRequest, + axis: PhaseAxis, +) -> Result { + let moving_windows: Vec<_> = request + .windows + .iter() + .copied() + .filter(|window| window.from != window.to) + .collect(); + if moving_windows.len() < 2 { + return Err(MultiphasePlanFailure::NoPattern); + } + let orth_min = request + .windows + .iter() + .map(|window| orth_start(window.from, axis)) + .min() + .ok_or(MultiphasePlanFailure::NoPattern)?; + let orth_max = request + .windows + .iter() + .map(|window| orth_end(window.from, axis)) + .max() + .ok_or(MultiphasePlanFailure::NoPattern)?; + if moving_windows.iter().any(|window| { + orth_start(window.from, axis) != orth_min + || orth_end(window.from, axis) != orth_max + || orth_start(window.to, axis) != orth_min + || orth_end(window.to, axis) != orth_max + || main_start(window.from, axis) == main_start(window.to, axis) + }) { + return Err(MultiphasePlanFailure::NoPattern); + } + let clearance = request.clearance.max(0); + let lane_count = moving_windows.len() as i32; + let available = (orth_max - orth_min) - clearance.saturating_mul(lane_count - 1); + if available <= 0 { + return Err(MultiphasePlanFailure::ShrinkBound { + axis: axis.other(), + available: 0, + required: sane_min_size(orth_max - orth_min), + }); + } + let lane_size = available / lane_count; + let mut lane_remainder = available % lane_count; + let required = sane_min_size(orth_max - orth_min); + if lane_size < required { + return Err(MultiphasePlanFailure::ShrinkBound { + axis: axis.other(), + available: lane_size, + required, + }); + } + + let mut windows = moving_windows; + windows.sort_by_key(|window| lane_sort_key(*window, axis)); + let mut phase1 = vec![]; + let mut phase2 = vec![]; + let mut phase3 = vec![]; + let mut phase4 = vec![]; + let mut lane_start = orth_min; + for (idx, window) in windows.iter().enumerate() { + let extra = if lane_remainder > 0 { + lane_remainder -= 1; + 1 + } else { + 0 + }; + let lane_end = lane_start + lane_size + extra; + let lane_from = with_orth_interval(window.from, axis, lane_start, lane_end); + let lane_to = with_main_interval( + lane_from, + axis, + main_start(window.to, axis), + main_end(window.to, axis), + ); + let mut lane_move = crossing_lane_move_rect(lane_from, window.to, axis); + if same_axis_delta_within(lane_move, lane_to, axis, SWAP_AXIS_SNAP_PX) { + lane_move = lane_to; + } + push_step(&mut phase1, window.node_id, window.from, lane_from); + push_step(&mut phase2, window.node_id, lane_from, lane_move); + push_step(&mut phase3, window.node_id, lane_move, lane_to); + push_step(&mut phase4, window.node_id, lane_to, window.to); + if idx + 1 < windows.len() { + lane_start = lane_end + clearance; + } + } + build_validated_plan( + request, + PlanStrategy::SwapLanes { axis }, + [ + phase_draft( + PhaseKind::Scale, + axis.other(), + phase1, + PhaseReason::ShrinkIntoLanes { + lane_axis: axis.other(), + }, + ), + phase_draft_classified( + phase2, + PhaseReason::MoveThroughFreedSpace, + )?, + phase_draft( + PhaseKind::Scale, + axis, + phase3, + PhaseReason::SameAxisRedistribution, + ), + phase_draft( + PhaseKind::Scale, + axis.other(), + phase4, + PhaseReason::GrowOutOfLanes, + ), + ], + ) +} + +fn phase_draft_classified( + steps: Vec, + reason: PhaseReason, +) -> Result { + let actions = steps + .iter() + .map(|step| classify_step(*step).ok_or(MultiphasePlanFailure::NoPattern)) + .collect::, _>>()?; + Ok(phase_draft_mixed(steps, actions, reason)) +} + +fn crossing_lane_move_rect(from: Rect, target: Rect, axis: PhaseAxis) -> Rect { + let size = main_size(from, axis); + if main_start(target, axis) > main_start(from, axis) { + let end = main_end(target, axis); + with_main_interval(from, axis, end - size, end) + } else { + let start = main_start(target, axis); + with_main_interval(from, axis, start, start + size) + } +} + +fn same_axis_delta_within(from: Rect, to: Rect, axis: PhaseAxis, max_delta: i32) -> bool { + let start_delta = (main_start(from, axis) - main_start(to, axis)).abs(); + let end_delta = (main_end(from, axis) - main_end(to, axis)).abs(); + start_delta.max(end_delta) <= max_delta +} + +fn lane_sort_key(window: MultiphaseWindow, axis: PhaseAxis) -> (usize, i32, i32, u32) { + let delta = main_start(window.to, axis) - main_start(window.from, axis); + let direction = match delta.cmp(&0) { + std::cmp::Ordering::Greater => 0, + std::cmp::Ordering::Less => 1, + std::cmp::Ordering::Equal => 2, + }; + ( + direction, + main_start(window.from, axis), + main_start(window.to, axis), + window.node_id.0, + ) +} + +fn plan_space_then_orthogonal_growth( + request: &MultiphaseRequest, + axis: PhaseAxis, +) -> Result { + if request.windows.len() < 2 { + return Err(MultiphasePlanFailure::NoPattern); + } + let orth_axis = axis.other(); + let min_width = sane_min_size(request.bounds.width()); + let min_height = sane_min_size(request.bounds.height()); + let mut phase1 = vec![]; + let mut phase2 = vec![]; + let mut phase3 = vec![]; + for window in &request.windows { + if window.to.width() < min_width { + return Err(MultiphasePlanFailure::ShrinkBound { + axis: PhaseAxis::Horizontal, + available: window.to.width(), + required: min_width, + }); + } + if window.to.height() < min_height { + return Err(MultiphasePlanFailure::ShrinkBound { + axis: PhaseAxis::Vertical, + available: window.to.height(), + required: min_height, + }); + } + let main_changes = main_start(window.from, axis) != main_start(window.to, axis) + || main_end(window.from, axis) != main_end(window.to, axis); + let orth_changes = orth_start(window.from, axis) != orth_start(window.to, axis) + || orth_end(window.from, axis) != orth_end(window.to, axis); + let mut orth_from = window.from; + if main_changes && main_size(window.from, axis) == main_size(window.to, axis) { + let after_move = with_main_interval( + window.from, + axis, + main_start(window.to, axis), + main_end(window.to, axis), + ); + push_step(&mut phase2, window.node_id, window.from, after_move); + orth_from = after_move; + } else if main_changes { + let target_size = main_size(window.to, axis); + let after_main_scale = if main_start(window.from, axis) == main_start(window.to, axis) + || main_end(window.from, axis) == main_end(window.to, axis) + { + with_main_interval( + window.from, + axis, + main_start(window.to, axis), + main_end(window.to, axis), + ) + } else if main_start(window.to, axis) < main_start(window.from, axis) { + with_main_interval( + window.from, + axis, + main_end(window.from, axis) - target_size, + main_end(window.from, axis), + ) + } else { + with_main_interval( + window.from, + axis, + main_start(window.from, axis), + main_start(window.from, axis) + target_size, + ) + }; + push_step(&mut phase1, window.node_id, window.from, after_main_scale); + orth_from = after_main_scale; + if main_start(after_main_scale, axis) != main_start(window.to, axis) + || main_end(after_main_scale, axis) != main_end(window.to, axis) + { + let after_move = with_main_interval( + after_main_scale, + axis, + main_start(window.to, axis), + main_end(window.to, axis), + ); + push_step(&mut phase2, window.node_id, after_main_scale, after_move); + orth_from = after_move; + } + } + if orth_changes { + push_step(&mut phase3, window.node_id, orth_from, window.to); + } + } + if phase1.is_empty() || phase2.is_empty() || phase3.is_empty() { + return Err(MultiphasePlanFailure::NoPattern); + } + build_validated_plan( + request, + PlanStrategy::SpaceThenOrthogonalGrowth { axis }, + [ + phase_draft( + PhaseKind::Scale, + axis, + phase1, + PhaseReason::CreateSpaceForAscendingChild, + ), + phase_draft( + PhaseKind::Move, + axis, + phase2, + PhaseReason::MoveAscendingChildAfterSpaceExists, + ), + phase_draft( + PhaseKind::Scale, + orth_axis, + phase3, + PhaseReason::OrthogonalGrowthAfterMove, + ), + ], + ) +} + +fn plan_orientation_change( + request: &MultiphaseRequest, + from_axis: PhaseAxis, +) -> Result { + if request.windows.len() < 2 { + return Err(MultiphasePlanFailure::NoPattern); + } + let to_axis = from_axis.other(); + let min_lane_size = sane_min_size(main_size(request.bounds, to_axis)); + let target_start = request + .windows + .first() + .map(|window| main_start(window.to, from_axis)) + .ok_or(MultiphasePlanFailure::NoPattern)?; + let target_end = request + .windows + .first() + .map(|window| main_end(window.to, from_axis)) + .ok_or(MultiphasePlanFailure::NoPattern)?; + let source_start = request + .windows + .first() + .map(|window| main_start(window.from, to_axis)) + .ok_or(MultiphasePlanFailure::NoPattern)?; + let source_end = request + .windows + .first() + .map(|window| main_end(window.from, to_axis)) + .ok_or(MultiphasePlanFailure::NoPattern)?; + if request.windows.iter().any(|window| { + main_start(window.from, to_axis) != source_start + || main_end(window.from, to_axis) != source_end + || main_start(window.to, from_axis) != target_start + || main_end(window.to, from_axis) != target_end + || main_size(window.to, to_axis) < min_lane_size + }) { + return Err(MultiphasePlanFailure::NoPattern); + } + + let mut phase1 = vec![]; + let mut phase2 = vec![]; + let mut phase3 = vec![]; + for window in &request.windows { + let lane = with_main_interval( + window.from, + to_axis, + main_start(window.to, to_axis), + main_end(window.to, to_axis), + ); + let moved = with_main_interval( + lane, + from_axis, + main_start(window.to, from_axis), + main_start(window.to, from_axis) + main_size(lane, from_axis), + ); + push_step(&mut phase1, window.node_id, window.from, lane); + push_step(&mut phase2, window.node_id, lane, moved); + push_step(&mut phase3, window.node_id, moved, window.to); + } + if phase1.is_empty() || phase3.is_empty() { + return Err(MultiphasePlanFailure::NoPattern); + } + build_validated_plan( + request, + PlanStrategy::OrientationChange { from_axis }, + [ + phase_draft( + PhaseKind::Scale, + to_axis, + phase1, + PhaseReason::ShrinkIntoLanes { lane_axis: to_axis }, + ), + phase_draft( + PhaseKind::Move, + from_axis, + phase2, + PhaseReason::MoveThroughFreedSpace, + ), + phase_draft( + PhaseKind::Scale, + from_axis, + phase3, + PhaseReason::GrowOutOfLanes, + ), + ], + ) +} + +struct MultiphasePhaseDraft { + action: MultiphasePhaseActionDraft, + steps: Vec, + reason: PhaseReason, +} + +enum MultiphasePhaseActionDraft { + Uniform(PhaseAction), + Mixed(Vec), +} + +fn phase_draft_uniform( + action: PhaseAction, + steps: Vec, + reason: PhaseReason, +) -> MultiphasePhaseDraft { + MultiphasePhaseDraft { + action: MultiphasePhaseActionDraft::Uniform(action), + steps, + reason, + } +} + +fn phase_draft( + kind: PhaseKind, + axis: PhaseAxis, + steps: Vec, + reason: PhaseReason, +) -> MultiphasePhaseDraft { + phase_draft_uniform(PhaseAction { kind, axis }, steps, reason) +} + +fn phase_draft_mixed( + steps: Vec, + actions: Vec, + reason: PhaseReason, +) -> MultiphasePhaseDraft { + MultiphasePhaseDraft { + action: MultiphasePhaseActionDraft::Mixed(actions), + steps, + reason, + } +} + +fn build_validated_plan( + request: &MultiphaseRequest, + strategy: PlanStrategy, + phases: [MultiphasePhaseDraft; N], +) -> Result { + let mut explanations = vec![]; + let phases: Vec<_> = phases + .into_iter() + .filter_map(|draft| { + if draft.steps.is_empty() { + return None; + } + let mut nodes: Vec<_> = draft.steps.iter().map(|step| step.node_id).collect(); + nodes.sort_by_key(|node_id| node_id.0); + let action = match draft.action { + MultiphasePhaseActionDraft::Uniform(action) => { + MultiphasePhaseAction::Uniform(action) + } + MultiphasePhaseActionDraft::Mixed(actions) => { + debug_assert_eq!(actions.len(), draft.steps.len()); + MultiphasePhaseAction::from_step_actions(actions) + } + }; + explanations.push(PhaseExplanation { + action: action.clone(), + reason: draft.reason, + nodes, + }); + Some(MultiphasePhase { + action, + steps: draft.steps, + }) + }) + .collect(); + for phase in &phases { + for (idx, step) in phase.steps.iter().enumerate() { + let action = phase.action.action_for_step(idx).unwrap(); + if classify_step(*step) != Some(action) { + return Err(MultiphasePlanFailure::InvalidPhaseStep { + action, + node_id: step.node_id, + }); + } + } + } + let plan = MultiphasePlan { phases }; + validate_plan_continuous_diagnostic(request, &plan) + .map(|_| MultiphasePlanned { + plan, + explanation: MultiphasePlanExplanation { + strategy, + phases: explanations, + validation: ValidationExplanation::passed(), + }, + }) + .map_err(MultiphasePlanFailure::Validation) +} + +#[cfg_attr(not(test), expect(dead_code))] +fn validate_plan_continuous(request: &MultiphaseRequest, plan: &MultiphasePlan) -> bool { + validate_plan_continuous_diagnostic(request, plan).is_ok() +} + +fn validate_plan_continuous_diagnostic( + request: &MultiphaseRequest, + plan: &MultiphasePlan, +) -> Result<(), MultiphaseValidationError> { + let mut current: Vec<_> = request + .windows + .iter() + .map(|window| (window.node_id, window.from)) + .collect(); + for (phase_idx, phase) in plan.phases.iter().enumerate() { + if let MultiphasePhaseAction::Mixed(actions) = &phase.action + && actions.len() != phase.steps.len() + { + return Err(MultiphaseValidationError::PhaseActionCount { + phase: phase_idx, + actions: actions.len(), + steps: phase.steps.len(), + }); + } + for (idx, step) in phase.steps.iter().enumerate() { + if phase.steps[..idx] + .iter() + .any(|prev| prev.node_id == step.node_id) + { + return Err(MultiphaseValidationError::DuplicatePhaseStep { + phase: phase_idx, + node_id: step.node_id, + }); + } + let Some((_, rect)) = current.iter().find(|(node_id, _)| *node_id == step.node_id) + else { + return Err(MultiphaseValidationError::UnknownPhaseStep { + phase: phase_idx, + node_id: step.node_id, + }); + }; + if *rect != step.from { + return Err(MultiphaseValidationError::StaleStepStart { + phase: phase_idx, + node_id: step.node_id, + }); + } + } + let motions: Vec<_> = current + .iter() + .map(|(node_id, rect)| { + let to = phase + .steps + .iter() + .find(|step| step.node_id == *node_id) + .map(|step| step.to) + .unwrap_or(*rect); + RectMotion { from: *rect, to } + }) + .collect(); + for (idx, motion) in motions.iter().enumerate() { + if let Some((other_idx, _)) = motions[idx + 1..] + .iter() + .enumerate() + .find(|(_, other)| motions_overlap_during_phase(*motion, **other)) + { + return Err(MultiphaseValidationError::PhaseOverlap { + phase: phase_idx, + a: current[idx].0, + b: current[idx + 1 + other_idx].0, + }); + } + } + for step in &phase.steps { + let (_, rect) = current + .iter_mut() + .find(|(node_id, _)| *node_id == step.node_id) + .unwrap(); + *rect = step.to; + } + } + for window in &request.windows { + if !current + .iter() + .find(|(node_id, _)| *node_id == window.node_id) + .is_some_and(|(_, rect)| *rect == window.to) + { + return Err(MultiphaseValidationError::FinalMismatch { + node_id: window.node_id, + }); + } + } + Ok(()) +} + +#[derive(Copy, Clone)] +struct RectMotion { + from: Rect, + to: Rect, +} + +fn motions_overlap_during_phase(a: RectMotion, b: RectMotion) -> bool { + let mut interval = TimeInterval::unit(); + interval.intersect_less_than(edge_delta(a.from.x1(), a.to.x1(), b.from.x2(), b.to.x2())) + && interval.intersect_less_than(edge_delta(b.from.x1(), b.to.x1(), a.from.x2(), a.to.x2())) + && interval.intersect_less_than(edge_delta(a.from.y1(), a.to.y1(), b.from.y2(), b.to.y2())) + && interval.intersect_less_than(edge_delta(b.from.y1(), b.to.y1(), a.from.y2(), a.to.y2())) + && interval.is_non_empty() +} + +fn edge_delta(a0: i32, a1: i32, b0: i32, b1: i32) -> LinearDelta { + let from = a0 as i64 - b0 as i64; + let to = a1 as i64 - b1 as i64; + LinearDelta { + start: from, + velocity: to - from, + } +} + +#[derive(Copy, Clone)] +struct LinearDelta { + start: i64, + velocity: i64, +} + +#[derive(Copy, Clone)] +struct TimeInterval { + lower: Rational, + lower_open: bool, + upper: Rational, + upper_open: bool, +} + +impl TimeInterval { + fn unit() -> Self { + Self { + lower: Rational::new(0, 1), + lower_open: false, + upper: Rational::new(1, 1), + upper_open: false, + } + } + + fn intersect_less_than(&mut self, delta: LinearDelta) -> bool { + if delta.velocity == 0 { + return delta.start < 0; + } + let boundary = Rational::new(-delta.start, delta.velocity); + if delta.velocity > 0 { + self.tighten_upper(boundary, true); + } else { + self.tighten_lower(boundary, true); + } + self.is_non_empty() + } + + fn tighten_lower(&mut self, value: Rational, open: bool) { + match value.cmp(&self.lower) { + std::cmp::Ordering::Greater => { + self.lower = value; + self.lower_open = open; + } + std::cmp::Ordering::Equal => { + self.lower_open |= open; + } + std::cmp::Ordering::Less => {} + } + } + + fn tighten_upper(&mut self, value: Rational, open: bool) { + match value.cmp(&self.upper) { + std::cmp::Ordering::Less => { + self.upper = value; + self.upper_open = open; + } + std::cmp::Ordering::Equal => { + self.upper_open |= open; + } + std::cmp::Ordering::Greater => {} + } + } + + fn is_non_empty(&self) -> bool { + match self.lower.cmp(&self.upper) { + std::cmp::Ordering::Less => true, + std::cmp::Ordering::Equal => !self.lower_open && !self.upper_open, + std::cmp::Ordering::Greater => false, + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq)] +struct Rational { + num: i64, + den: i64, +} + +impl Rational { + fn new(mut num: i64, mut den: i64) -> Self { + if den < 0 { + num = -num; + den = -den; + } + Self { num, den } + } + + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + (self.num as i128 * other.den as i128).cmp(&(other.num as i128 * self.den as i128)) + } +} + +fn classify_step(step: MultiphaseStep) -> Option { + let same_x = step.from.x1() == step.to.x1() && step.from.x2() == step.to.x2(); + let same_y = step.from.y1() == step.to.y1() && step.from.y2() == step.to.y2(); + let same_size = step.from.size() == step.to.size(); + match (same_x, same_y, same_size) { + (false, true, true) => Some(PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal, + }), + (true, false, true) => Some(PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Vertical, + }), + (false, true, false) => Some(PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal, + }), + (true, false, false) => Some(PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }), + _ => None, + } +} + +fn single_action_reason(action: PhaseAction) -> PhaseReason { + match action.kind { + PhaseKind::Move => PhaseReason::SingleAction, + PhaseKind::Scale => PhaseReason::SameAxisRedistribution, + } +} + +fn reverse_request(request: &MultiphaseRequest) -> MultiphaseRequest { + MultiphaseRequest { + bounds: request.bounds, + clearance: request.clearance, + windows: request + .windows + .iter() + .map(|window| MultiphaseWindow { + node_id: window.node_id, + from: window.to, + to: window.from, + hierarchy: window.hierarchy.reversed(), + }) + .collect(), + } +} + +fn reverse_plan(plan: MultiphasePlan) -> MultiphasePlan { + MultiphasePlan { + phases: plan + .phases + .into_iter() + .rev() + .map(|phase| MultiphasePhase { + action: phase.action, + steps: phase + .steps + .into_iter() + .map(|step| MultiphaseStep { + node_id: step.node_id, + from: step.to, + to: step.from, + }) + .collect(), + }) + .collect(), + } +} + +fn reverse_planned(planned: MultiphasePlanned) -> MultiphasePlanned { + let mut phases = planned.explanation.phases; + phases.reverse(); + MultiphasePlanned { + plan: reverse_plan(planned.plan), + explanation: MultiphasePlanExplanation { + strategy: PlanStrategy::ReversedForwardPlan { + original: Box::new(planned.explanation.strategy), + }, + phases, + validation: planned.explanation.validation, + }, + } +} + +fn overlaps(rects: impl IntoIterator) -> bool { + let rects: Vec<_> = rects.into_iter().collect(); + for (idx, rect) in rects.iter().enumerate() { + if rects[idx + 1..].iter().any(|other| rect.intersects(other)) { + return true; + } + } + false +} + +fn motion_bounds(window: MultiphaseWindow) -> Rect { + window.from.union(window.to) +} + +fn motion_bounds_with_clearance(window: MultiphaseWindow, clearance: i32) -> Rect { + let bounds = motion_bounds(window); + Rect::new_saturating( + bounds.x1().saturating_sub(clearance), + bounds.y1().saturating_sub(clearance), + bounds.x2().saturating_add(clearance), + bounds.y2().saturating_add(clearance), + ) +} + +fn interval_changed(from: Rect, to: Rect, axis: PhaseAxis) -> bool { + main_start(from, axis) != main_start(to, axis) || main_end(from, axis) != main_end(to, axis) +} + +fn split_depth_for_axis(position: MultiphaseHierarchyPosition, axis: PhaseAxis) -> Option { + match axis { + PhaseAxis::Horizontal => position.nearest_horizontal_split_depth, + PhaseAxis::Vertical => position.nearest_vertical_split_depth, + } +} + +fn push_step(steps: &mut Vec, node_id: NodeId, from: Rect, to: Rect) { + if from != to { + steps.push(MultiphaseStep { node_id, from, to }); + } +} + +fn sane_min_size(size: i32) -> i32 { + (size / MIN_SHRINK_DENOMINATOR).max(1) +} + +fn main_start(rect: Rect, axis: PhaseAxis) -> i32 { + match axis { + PhaseAxis::Horizontal => rect.x1(), + PhaseAxis::Vertical => rect.y1(), + } +} + +fn main_end(rect: Rect, axis: PhaseAxis) -> i32 { + match axis { + PhaseAxis::Horizontal => rect.x2(), + PhaseAxis::Vertical => rect.y2(), + } +} + +fn main_size(rect: Rect, axis: PhaseAxis) -> i32 { + main_end(rect, axis) - main_start(rect, axis) +} + +fn orth_start(rect: Rect, axis: PhaseAxis) -> i32 { + main_start(rect, axis.other()) +} + +fn orth_end(rect: Rect, axis: PhaseAxis) -> i32 { + main_end(rect, axis.other()) +} + +fn with_main_interval(rect: Rect, axis: PhaseAxis, start: i32, end: i32) -> Rect { + match axis { + PhaseAxis::Horizontal => Rect::new_saturating(start, rect.y1(), end, rect.y2()), + PhaseAxis::Vertical => Rect::new_saturating(rect.x1(), start, rect.x2(), end), + } +} + +fn with_orth_interval(rect: Rect, axis: PhaseAxis, start: i32, end: i32) -> Rect { + with_main_interval(rect, axis.other(), start, end) +} + +impl PhaseAxis { + fn other(self) -> Self { + match self { + Self::Horizontal => Self::Vertical, + Self::Vertical => Self::Horizontal, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn id(raw: u32) -> NodeId { + NodeId(raw) + } + + fn rect(x1: i32, y1: i32, x2: i32, y2: i32) -> Rect { + Rect::new_saturating(x1, y1, x2, y2) + } + + fn window(raw: u32, from: Rect, to: Rect) -> MultiphaseWindow { + MultiphaseWindow::new(id(raw), from, to) + } + + #[derive(Clone)] + enum TestTree { + Leaf(u32), + Split { + id: u32, + axis: PhaseAxis, + weights: Vec, + children: Vec, + }, + } + + struct TestLeaf { + node_id: NodeId, + rect: Rect, + hierarchy: MultiphaseHierarchyPosition, + } + + fn leaf(raw: u32) -> TestTree { + TestTree::Leaf(raw) + } + + fn split(id: u32, axis: PhaseAxis, weights: &[i32], children: Vec) -> TestTree { + TestTree::Split { + id, + axis, + weights: weights.to_vec(), + children, + } + } + + fn layout_tree(tree: &TestTree, bounds: Rect) -> Vec { + let mut leaves = vec![]; + layout_tree_inner( + tree, + bounds, + TestHierarchy { + parent: None, + depth: 0, + sibling_index: None, + split_axis: None, + nearest_horizontal_split_depth: None, + nearest_vertical_split_depth: None, + }, + &mut leaves, + ); + leaves.sort_by_key(|leaf| leaf.node_id.0); + leaves + } + + #[derive(Copy, Clone)] + struct TestHierarchy { + parent: Option, + depth: u16, + sibling_index: Option, + split_axis: Option, + nearest_horizontal_split_depth: Option, + nearest_vertical_split_depth: Option, + } + + fn layout_tree_inner( + tree: &TestTree, + bounds: Rect, + hierarchy: TestHierarchy, + leaves: &mut Vec, + ) { + match tree { + TestTree::Leaf(raw) => leaves.push(TestLeaf { + node_id: id(*raw), + rect: bounds, + hierarchy: MultiphaseHierarchyPosition { + parent: hierarchy.parent, + depth: hierarchy.depth, + sibling_index: hierarchy.sibling_index, + split_axis: hierarchy.split_axis, + nearest_horizontal_split_depth: hierarchy.nearest_horizontal_split_depth, + nearest_vertical_split_depth: hierarchy.nearest_vertical_split_depth, + ..Default::default() + }, + }), + TestTree::Split { + id: split_id, + axis, + weights, + children, + } => { + assert_eq!(weights.len(), children.len()); + let rects = split_rect_by_weights(bounds, *axis, weights); + for (idx, (child, rect)) in children.iter().zip(rects).enumerate() { + let depth = hierarchy.depth.saturating_add(1); + let mut child_hierarchy = TestHierarchy { + parent: Some(id(*split_id)), + depth, + sibling_index: Some(idx.min(u16::MAX as usize) as u16), + split_axis: Some(*axis), + nearest_horizontal_split_depth: hierarchy.nearest_horizontal_split_depth, + nearest_vertical_split_depth: hierarchy.nearest_vertical_split_depth, + }; + match axis { + PhaseAxis::Horizontal => { + child_hierarchy.nearest_horizontal_split_depth = Some(depth); + } + PhaseAxis::Vertical => { + child_hierarchy.nearest_vertical_split_depth = Some(depth); + } + } + layout_tree_inner(child, rect, child_hierarchy, leaves); + } + } + } + } + + fn split_rect_by_weights(bounds: Rect, axis: PhaseAxis, weights: &[i32]) -> Vec { + let total_weight: i32 = weights.iter().sum(); + assert!(total_weight > 0); + let total_size = match axis { + PhaseAxis::Horizontal => bounds.width(), + PhaseAxis::Vertical => bounds.height(), + }; + let mut pos = match axis { + PhaseAxis::Horizontal => bounds.x1(), + PhaseAxis::Vertical => bounds.y1(), + }; + let mut remaining_size = total_size; + let mut remaining_weight = total_weight; + let mut rects = vec![]; + for (idx, weight) in weights.iter().enumerate() { + let size = if idx + 1 == weights.len() { + remaining_size + } else { + total_size * *weight / total_weight + }; + let rect = match axis { + PhaseAxis::Horizontal => { + Rect::new_sized_saturating(pos, bounds.y1(), size, bounds.height()) + } + PhaseAxis::Vertical => { + Rect::new_sized_saturating(bounds.x1(), pos, bounds.width(), size) + } + }; + rects.push(rect); + pos += size; + remaining_size -= size; + remaining_weight -= *weight; + if remaining_weight == 0 { + assert_eq!(remaining_size, 0); + } + } + rects + } + + fn generated_request(old: &TestTree, new: &TestTree, bounds: Rect) -> MultiphaseRequest { + let old_leaves = layout_tree(old, bounds); + let new_leaves = layout_tree(new, bounds); + assert_eq!(old_leaves.len(), new_leaves.len()); + let mut windows = vec![]; + for old_leaf in &old_leaves { + let new_leaf = new_leaves + .iter() + .find(|leaf| leaf.node_id == old_leaf.node_id) + .unwrap(); + windows.push(MultiphaseWindow::with_hierarchy( + old_leaf.node_id, + old_leaf.rect, + new_leaf.rect, + MultiphaseWindowHierarchy::new(old_leaf.hierarchy, new_leaf.hierarchy), + )); + } + MultiphaseRequest { + bounds, + windows, + clearance: 0, + } + } + + fn assert_generated_case_plans(old: &TestTree, new: &TestTree, bounds: Rect) { + assert_generated_case_plans_deterministically(old, new, bounds); + } + + fn assert_generated_case_plans_deterministically( + old: &TestTree, + new: &TestTree, + bounds: Rect, + ) -> MultiphasePlanned { + let req = generated_request(old, new, bounds); + assert!(!overlaps(req.windows.iter().map(|window| window.from))); + assert!(!overlaps(req.windows.iter().map(|window| window.to))); + let first = plan_no_overlap_explained(&req).unwrap(); + let second = plan_no_overlap_explained(&req).unwrap(); + assert_eq!(first, second); + assert_eq!(first.plan.phases.len(), first.explanation.phases.len()); + assert_eq!( + first.explanation.validation, + ValidationExplanation::passed() + ); + for phase in &first.explanation.phases { + assert!(phase.nodes.windows(2).all(|pair| pair[0].0 < pair[1].0)); + } + assert!(validate_plan_continuous(&req, &first.plan)); + first + } + + fn bounds_for_axis(axis: PhaseAxis) -> Rect { + match axis { + PhaseAxis::Horizontal => rect(0, 0, 400, 100), + PhaseAxis::Vertical => rect(0, 0, 100, 400), + } + } + + fn push_generated_case_bidirectional( + cases: &mut Vec<(TestTree, TestTree, Rect)>, + old: TestTree, + new: TestTree, + bounds: Rect, + ) { + cases.push((old.clone(), new.clone(), bounds)); + cases.push((new, old, bounds)); + } + + fn request(windows: Vec) -> MultiphaseRequest { + let bounds = windows + .iter() + .map(|window| window.from.union(window.to)) + .reduce(|bounds, rect| bounds.union(rect)) + .unwrap_or_else(|| rect(0, 0, 1, 1)); + MultiphaseRequest { + bounds, + windows, + clearance: 0, + } + } + + fn actions(plan: &MultiphasePlan) -> Vec { + plan.phases + .iter() + .map(|phase| phase.action.as_uniform().unwrap()) + .collect() + } + + fn step_to(plan: &MultiphasePlan, phase: usize, node_id: NodeId) -> Rect { + plan.phases[phase] + .steps + .iter() + .find(|step| step.node_id == node_id) + .unwrap() + .to + } + + fn no_pattern_attempts(direction: PlanDirection) -> Vec { + vec![ + RejectedStrategy { + direction, + strategy: PlanStrategy::SingleAction, + reason: MultiphasePlanFailure::NoPattern, + }, + RejectedStrategy { + direction, + strategy: PlanStrategy::SpaceThenOrthogonalGrowth { + axis: PhaseAxis::Horizontal, + }, + reason: MultiphasePlanFailure::NoPattern, + }, + RejectedStrategy { + direction, + strategy: PlanStrategy::SpaceThenOrthogonalGrowth { + axis: PhaseAxis::Vertical, + }, + reason: MultiphasePlanFailure::NoPattern, + }, + RejectedStrategy { + direction, + strategy: PlanStrategy::HierarchyOrderedScales, + reason: MultiphasePlanFailure::NoPattern, + }, + RejectedStrategy { + direction, + strategy: PlanStrategy::OrientationChange { + from_axis: PhaseAxis::Horizontal, + }, + reason: MultiphasePlanFailure::NoPattern, + }, + RejectedStrategy { + direction, + strategy: PlanStrategy::OrientationChange { + from_axis: PhaseAxis::Vertical, + }, + reason: MultiphasePlanFailure::NoPattern, + }, + RejectedStrategy { + direction, + strategy: PlanStrategy::SwapLanes { + axis: PhaseAxis::Horizontal, + }, + reason: MultiphasePlanFailure::NoPattern, + }, + RejectedStrategy { + direction, + strategy: PlanStrategy::SwapLanes { + axis: PhaseAxis::Vertical, + }, + reason: MultiphasePlanFailure::NoPattern, + }, + ] + } + + #[test] + fn horizontal_swap_shrinks_moves_then_grows_without_overlap() { + let req = request(vec![ + window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)), + MultiphaseWindow { + node_id: id(2), + from: rect(100, 0, 200, 100), + to: rect(0, 0, 100, 100), + hierarchy: Default::default(), + }, + ]); + let planned = plan_no_overlap_explained(&req).unwrap(); + let plan = &planned.plan; + assert_eq!( + actions(plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical + }, + PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical + }, + ] + ); + assert_eq!(plan.phases[0].steps[0].to, rect(0, 0, 100, 50)); + assert_eq!(plan.phases[0].steps[1].to, rect(100, 50, 200, 100)); + assert_eq!( + planned.explanation.strategy, + PlanStrategy::SwapLanes { + axis: PhaseAxis::Horizontal + } + ); + assert_eq!( + planned + .explanation + .phases + .iter() + .map(|phase| phase.reason) + .collect::>(), + vec![ + PhaseReason::ShrinkIntoLanes { + lane_axis: PhaseAxis::Vertical + }, + PhaseReason::MoveThroughFreedSpace, + PhaseReason::GrowOutOfLanes, + ] + ); + assert_eq!(planned.explanation.phases[0].nodes, vec![id(1), id(2)]); + assert!(validate_plan_continuous(&req, plan)); + } + + #[test] + fn horizontal_swap_reverse_uses_equivalent_lanes() { + let req = request(vec![ + MultiphaseWindow { + node_id: id(1), + from: rect(100, 0, 200, 100), + to: rect(0, 0, 100, 100), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(2), + from: rect(0, 0, 100, 100), + to: rect(100, 0, 200, 100), + hierarchy: Default::default(), + }, + ]); + let plan = plan_no_overlap(&req).unwrap(); + assert_eq!(step_to(&plan, 0, id(2)), rect(0, 0, 100, 50)); + assert_eq!(step_to(&plan, 0, id(1)), rect(100, 50, 200, 100)); + assert!(validate_plan_continuous(&req, &plan)); + } + + #[test] + fn horizontal_swap_lanes_follow_motion_direction_not_node_id() { + let req = request(vec![ + MultiphaseWindow { + node_id: id(1), + from: rect(100, 0, 200, 100), + to: rect(0, 0, 100, 100), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(2), + from: rect(0, 0, 100, 100), + to: rect(100, 0, 200, 100), + hierarchy: Default::default(), + }, + ]); + let plan = plan_no_overlap(&req).unwrap(); + assert_eq!(step_to(&plan, 0, id(2)), rect(0, 0, 100, 50)); + assert_eq!(step_to(&plan, 0, id(1)), rect(100, 50, 200, 100)); + assert!(validate_plan_continuous(&req, &plan)); + } + + #[test] + fn swap_lanes_respect_requested_clearance() { + let mut req = request(vec![ + window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)), + window(2, rect(100, 0, 200, 100), rect(0, 0, 100, 100)), + ]); + req.clearance = 10; + + let plan = plan_no_overlap(&req).unwrap(); + assert_eq!(step_to(&plan, 0, id(1)), rect(0, 0, 100, 45)); + assert_eq!(step_to(&plan, 0, id(2)), rect(100, 55, 200, 100)); + assert!(validate_plan_continuous(&req, &plan)); + } + + #[test] + fn swap_lanes_tolerate_stationary_siblings_in_request() { + let req = request(vec![ + window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)), + window(2, rect(100, 0, 200, 100), rect(0, 0, 100, 100)), + window(3, rect(200, 0, 300, 100), rect(200, 0, 300, 100)), + ]); + + let planned = plan_no_overlap_explained(&req).unwrap(); + assert_eq!( + planned.explanation.strategy, + PlanStrategy::SwapLanes { + axis: PhaseAxis::Horizontal, + } + ); + assert!(validate_plan_continuous(&req, &planned.plan)); + } + + #[test] + fn one_pixel_uneven_swap_lanes_fold_remainder_into_crossing_phase() { + let req = request(vec![ + window(1, rect(0, 0, 101, 100), rect(101, 0, 201, 100)), + window(2, rect(101, 0, 201, 100), rect(0, 0, 101, 100)), + ]); + let planned = plan_no_overlap_explained(&req).unwrap(); + let plan = &planned.plan; + + assert_eq!( + planned.explanation.strategy, + PlanStrategy::SwapLanes { + axis: PhaseAxis::Horizontal, + } + ); + assert_eq!( + actions(plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + ] + ); + assert_eq!(step_to(plan, 1, id(1)), rect(101, 0, 201, 50)); + assert_eq!(step_to(plan, 1, id(2)), rect(0, 50, 101, 100)); + assert!(validate_plan_continuous(&req, plan)); + } + + #[test] + fn large_uneven_swap_lanes_split_move_and_same_axis_scale() { + let req = request(vec![ + window(1, rect(0, 0, 120, 100), rect(120, 0, 200, 100)), + window(2, rect(120, 0, 200, 100), rect(0, 0, 120, 100)), + ]); + let planned = plan_no_overlap_explained(&req).unwrap(); + let plan = &planned.plan; + + assert_eq!( + planned.explanation.strategy, + PlanStrategy::SwapLanes { + axis: PhaseAxis::Horizontal, + } + ); + assert_eq!( + actions(plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + ] + ); + assert_eq!(step_to(plan, 1, id(1)), rect(80, 0, 200, 50)); + assert_eq!(step_to(plan, 1, id(2)), rect(0, 50, 80, 100)); + assert_eq!(step_to(plan, 2, id(1)), rect(120, 0, 200, 50)); + assert_eq!(step_to(plan, 2, id(2)), rect(0, 50, 120, 100)); + assert!(validate_plan_continuous(&req, plan)); + } + + #[test] + fn uneven_swap_lanes_tolerate_stationary_sibling_in_odd_layout() { + let req = request(vec![ + window(1, rect(0, 0, 101, 100), rect(101, 0, 201, 100)), + window(2, rect(101, 0, 201, 100), rect(0, 0, 101, 100)), + window(3, rect(201, 0, 300, 100), rect(201, 0, 300, 100)), + ]); + let planned = plan_no_overlap_explained(&req).unwrap(); + + assert_eq!( + planned.explanation.strategy, + PlanStrategy::SwapLanes { + axis: PhaseAxis::Horizontal, + } + ); + assert_eq!( + actions(&planned.plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + ] + ); + assert!(validate_plan_continuous(&req, &planned.plan)); + } + + #[test] + fn horizontal_rotation_uses_crossing_lanes() { + let req = request(vec![ + window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)), + window(2, rect(100, 0, 200, 100), rect(200, 0, 300, 100)), + window(3, rect(200, 0, 300, 100), rect(0, 0, 100, 100)), + ]); + let planned = plan_no_overlap_explained(&req).unwrap(); + + assert_eq!( + planned.explanation.strategy, + PlanStrategy::SwapLanes { + axis: PhaseAxis::Horizontal, + } + ); + assert_eq!( + actions(&planned.plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + ] + ); + assert!(validate_plan_continuous(&req, &planned.plan)); + } + + #[test] + fn vertical_swap_lanes_follow_motion_direction_not_node_id() { + let req = request(vec![ + MultiphaseWindow { + node_id: id(1), + from: rect(0, 100, 100, 200), + to: rect(0, 0, 100, 100), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(2), + from: rect(0, 0, 100, 100), + to: rect(0, 100, 100, 200), + hierarchy: Default::default(), + }, + ]); + let plan = plan_no_overlap(&req).unwrap(); + assert_eq!(step_to(&plan, 0, id(2)), rect(0, 0, 50, 100)); + assert_eq!(step_to(&plan, 0, id(1)), rect(50, 100, 100, 200)); + assert!(validate_plan_continuous(&req, &plan)); + } + + #[test] + fn generated_sibling_swaps_plan_for_both_axes() { + let bounds = rect(0, 0, 240, 240); + for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { + let old = split(10, axis, &[1, 1], vec![leaf(1), leaf(2)]); + let new = split(10, axis, &[1, 1], vec![leaf(2), leaf(1)]); + assert_generated_case_plans(&old, &new, bounds); + } + } + + #[test] + fn generated_size_redistributions_plan_as_single_axis_scale() { + let horizontal_old = split( + 10, + PhaseAxis::Horizontal, + &[1, 1, 1], + vec![leaf(1), leaf(2), leaf(3)], + ); + let horizontal_new = split( + 10, + PhaseAxis::Horizontal, + &[1, 2, 1], + vec![leaf(1), leaf(2), leaf(3)], + ); + let horizontal_req = + generated_request(&horizontal_old, &horizontal_new, rect(0, 0, 400, 100)); + let horizontal_plan = plan_no_overlap(&horizontal_req).unwrap(); + assert_eq!( + actions(&horizontal_plan), + vec![PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal, + }] + ); + assert!(validate_plan_continuous(&horizontal_req, &horizontal_plan)); + + let vertical_old = split( + 10, + PhaseAxis::Vertical, + &[1, 1, 1], + vec![leaf(1), leaf(2), leaf(3)], + ); + let vertical_new = split( + 10, + PhaseAxis::Vertical, + &[1, 2, 1], + vec![leaf(1), leaf(2), leaf(3)], + ); + let vertical_req = generated_request(&vertical_old, &vertical_new, rect(0, 0, 100, 400)); + let vertical_plan = plan_no_overlap(&vertical_req).unwrap(); + assert_eq!( + actions(&vertical_plan), + vec![PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }] + ); + assert!(validate_plan_continuous(&vertical_req, &vertical_plan)); + } + + #[test] + fn mixed_single_phase_accepts_distinct_axis_actions_when_proven() { + let req = request(vec![ + window(1, rect(0, 0, 100, 100), rect(0, 0, 150, 100)), + window(2, rect(200, 0, 300, 100), rect(200, 0, 300, 150)), + ]); + let planned = plan_no_overlap_explained(&req).unwrap(); + + assert_eq!(planned.explanation.strategy, PlanStrategy::MixedSinglePhase); + assert_eq!(planned.plan.phases.len(), 1); + assert_eq!( + planned.plan.phases[0].action, + MultiphasePhaseAction::Mixed(vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + ]) + ); + assert_eq!( + planned.explanation.phases[0].reason, + PhaseReason::MixedAxisActions + ); + assert_eq!(planned.explanation.phases[0].nodes, vec![id(1), id(2)]); + assert!(validate_plan_continuous(&req, &planned.plan)); + } + + #[test] + fn mixed_single_phase_accepts_move_and_scale_when_proven() { + let req = request(vec![ + window(1, rect(0, 0, 80, 80), rect(40, 0, 120, 80)), + window(2, rect(200, 0, 280, 80), rect(200, 0, 280, 120)), + ]); + let planned = plan_no_overlap_explained(&req).unwrap(); + + assert_eq!(planned.explanation.strategy, PlanStrategy::MixedSinglePhase); + assert_eq!( + planned.plan.phases[0].action, + MultiphasePhaseAction::Mixed(vec![ + PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + ]) + ); + assert_eq!( + planned.explanation.phases[0].reason, + PhaseReason::MixedAxisActions + ); + assert_eq!(planned.explanation.phases[0].nodes, vec![id(1), id(2)]); + assert!(validate_plan_continuous(&req, &planned.plan)); + } + + #[test] + fn single_window_one_axis_group_is_still_multiphase_plannable() { + let req = request(vec![window(1, rect(0, 0, 100, 100), rect(40, 0, 140, 100))]); + let planned = plan_no_overlap_explained(&req).unwrap(); + + assert_eq!(planned.explanation.strategy, PlanStrategy::SingleAction); + assert_eq!( + planned.plan.phases[0].action, + MultiphasePhaseAction::Uniform(PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal, + }) + ); + assert_eq!(planned.explanation.phases[0].nodes, vec![id(1)]); + assert!(validate_plan_continuous(&req, &planned.plan)); + } + + #[test] + fn mixed_single_phase_still_rejects_diagonal_per_window_motion() { + let req = request(vec![ + window(1, rect(0, 0, 80, 80), rect(40, 40, 120, 120)), + window(2, rect(200, 0, 280, 80), rect(200, 0, 280, 120)), + ]); + + assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan)); + let diagnostic = plan_no_overlap_with_diagnostics(&req).unwrap_err(); + let rejection = MultiphasePlanFailure::InvalidPhaseStep { + action: PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal, + }, + node_id: id(1), + }; + assert_eq!(diagnostic.forward, rejection); + assert_eq!(diagnostic.reverse, Some(rejection)); + } + + #[test] + fn generated_nested_size_redistribution_scales_parent_axis_first() { + let old = split( + 10, + PhaseAxis::Horizontal, + &[1, 1], + vec![ + leaf(1), + split(11, PhaseAxis::Vertical, &[1, 1], vec![leaf(2), leaf(3)]), + ], + ); + let new = split( + 10, + PhaseAxis::Horizontal, + &[1, 3], + vec![ + leaf(1), + split(11, PhaseAxis::Vertical, &[3, 1], vec![leaf(2), leaf(3)]), + ], + ); + let req = generated_request(&old, &new, rect(0, 0, 400, 100)); + let planned = plan_no_overlap_explained(&req).unwrap(); + let plan = &planned.plan; + assert_eq!( + actions(plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + ] + ); + assert_eq!(step_to(plan, 0, id(1)), rect(0, 0, 100, 100)); + assert_eq!(step_to(plan, 0, id(2)), rect(100, 0, 400, 50)); + assert_eq!(step_to(plan, 0, id(3)), rect(100, 50, 400, 100)); + assert_eq!( + planned.explanation.strategy, + PlanStrategy::HierarchyOrderedScales + ); + assert_eq!( + planned.explanation.phases[0].reason, + PhaseReason::ParentAxisBeforeChildAxis { + parent_axis: PhaseAxis::Horizontal, + parent_depth: 1, + child_axis: PhaseAxis::Vertical, + child_depth: 2, + } + ); + assert_eq!( + planned.explanation.phases[0].nodes, + vec![id(1), id(2), id(3)] + ); + assert_eq!(planned.explanation.phases[1].nodes, vec![id(2), id(3)]); + assert_eq!( + planned.explanation.validation, + ValidationExplanation::passed() + ); + assert!(validate_plan_continuous(&req, plan)); + } + + #[test] + fn orientation_change_shrinks_moves_then_grows() { + let req = request(vec![ + window(1, rect(0, 0, 200, 400), rect(0, 0, 400, 200)), + window(2, rect(200, 0, 400, 400), rect(0, 200, 400, 400)), + ]); + let planned = plan_no_overlap_explained(&req).unwrap(); + let plan = &planned.plan; + + assert_eq!( + planned.explanation.strategy, + PlanStrategy::OrientationChange { + from_axis: PhaseAxis::Horizontal, + } + ); + assert_eq!( + actions(plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal, + }, + ] + ); + assert_eq!(step_to(plan, 0, id(1)), rect(0, 0, 200, 200)); + assert_eq!(step_to(plan, 0, id(2)), rect(200, 200, 400, 400)); + assert_eq!(step_to(plan, 1, id(2)), rect(0, 200, 200, 400)); + assert_eq!(step_to(plan, 2, id(1)), rect(0, 0, 400, 200)); + assert_eq!(step_to(plan, 2, id(2)), rect(0, 200, 400, 400)); + assert!(validate_plan_continuous(&req, plan)); + } + + #[test] + fn two_axis_redistribution_without_hierarchy_still_falls_back() { + let req = request(vec![ + window(1, rect(0, 0, 200, 100), rect(0, 0, 100, 100)), + window(2, rect(200, 0, 400, 50), rect(100, 0, 400, 75)), + window(3, rect(200, 50, 400, 100), rect(100, 75, 400, 100)), + ]); + assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan)); + } + + #[test] + fn generated_stack_extractions_plan_for_both_axes_and_directions() { + let horizontal_old = split( + 10, + PhaseAxis::Horizontal, + &[1, 1], + vec![ + leaf(1), + split(11, PhaseAxis::Vertical, &[1, 1], vec![leaf(2), leaf(3)]), + ], + ); + let horizontal_new = split( + 10, + PhaseAxis::Horizontal, + &[1, 2, 1], + vec![leaf(1), leaf(2), leaf(3)], + ); + assert_generated_case_plans(&horizontal_old, &horizontal_new, rect(0, 0, 400, 100)); + assert_generated_case_plans(&horizontal_new, &horizontal_old, rect(0, 0, 400, 100)); + + let vertical_old = split( + 20, + PhaseAxis::Vertical, + &[1, 1], + vec![ + leaf(1), + split(21, PhaseAxis::Horizontal, &[1, 1], vec![leaf(2), leaf(3)]), + ], + ); + let vertical_new = split( + 20, + PhaseAxis::Vertical, + &[1, 2, 1], + vec![leaf(1), leaf(2), leaf(3)], + ); + assert_generated_case_plans(&vertical_old, &vertical_new, rect(0, 0, 100, 400)); + assert_generated_case_plans(&vertical_new, &vertical_old, rect(0, 0, 100, 400)); + } + + #[test] + fn stack_extraction_with_resized_moving_child_still_moves_before_growth() { + let old = split( + 10, + PhaseAxis::Horizontal, + &[1, 1], + vec![ + leaf(1), + split(11, PhaseAxis::Vertical, &[1, 1], vec![leaf(2), leaf(3)]), + ], + ); + let new = split( + 10, + PhaseAxis::Horizontal, + &[1, 1, 1], + vec![leaf(1), leaf(2), leaf(3)], + ); + let req = generated_request(&old, &new, rect(0, 0, 300, 120)); + let planned = plan_no_overlap_explained(&req).unwrap(); + let plan = &planned.plan; + + assert_eq!( + planned.explanation.strategy, + PlanStrategy::SpaceThenOrthogonalGrowth { + axis: PhaseAxis::Horizontal, + } + ); + assert_eq!( + actions(plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + ] + ); + assert_eq!(step_to(plan, 0, id(1)), rect(0, 0, 100, 120)); + assert_eq!(step_to(plan, 0, id(2)), rect(200, 0, 300, 60)); + assert_eq!(step_to(plan, 0, id(3)), rect(200, 60, 300, 120)); + assert_eq!(step_to(plan, 1, id(2)), rect(100, 0, 200, 60)); + assert_eq!(step_to(plan, 2, id(2)), rect(100, 0, 200, 120)); + assert_eq!(step_to(plan, 2, id(3)), rect(200, 0, 300, 120)); + assert!(validate_plan_continuous(&req, plan)); + } + + #[test] + fn three_child_stack_extraction_plans_without_linear_fallback() { + let old = split( + 10, + PhaseAxis::Horizontal, + &[1, 1], + vec![ + leaf(1), + split( + 11, + PhaseAxis::Vertical, + &[1, 1, 1], + vec![leaf(2), leaf(3), leaf(4)], + ), + ], + ); + let new = split( + 10, + PhaseAxis::Horizontal, + &[1, 1, 1], + vec![ + leaf(1), + leaf(3), + split(11, PhaseAxis::Vertical, &[1, 1], vec![leaf(2), leaf(4)]), + ], + ); + let req = generated_request(&old, &new, rect(0, 0, 600, 300)); + let planned = plan_no_overlap_explained(&req).unwrap(); + + assert_eq!( + planned.explanation.strategy, + PlanStrategy::SpaceThenOrthogonalGrowth { + axis: PhaseAxis::Horizontal, + } + ); + assert_eq!( + actions(&planned.plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + ] + ); + assert!(validate_plan_continuous(&req, &planned.plan)); + } + + #[test] + fn validated_phase_paths_accept_interrupted_reverse_route() { + let a_current = rect(50, 0, 150, 50); + let b_current = rect(50, 50, 150, 100); + let req = request(vec![ + window(1, a_current, rect(0, 0, 100, 100)), + window(2, b_current, rect(100, 0, 200, 100)), + ]); + let paths = vec![ + vec![ + (a_current, rect(0, 0, 100, 50)), + (rect(0, 0, 100, 50), rect(0, 0, 100, 100)), + ], + vec![ + (b_current, rect(100, 50, 200, 100)), + (rect(100, 50, 200, 100), rect(100, 0, 200, 100)), + ], + ]; + + let plan = validate_phase_paths(&req, &paths).unwrap(); + assert_eq!( + actions(&plan), + vec![ + PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + ] + ); + assert!(validate_plan_continuous(&req, &plan)); + } + + #[test] + fn bounded_generated_supported_split_tree_corpus_is_deterministic() { + let mut cases = vec![]; + for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { + let child_axis = axis.other(); + let bounds = bounds_for_axis(axis); + + push_generated_case_bidirectional( + &mut cases, + split(10, axis, &[1, 1], vec![leaf(1), leaf(2)]), + split(10, axis, &[1, 1], vec![leaf(2), leaf(1)]), + bounds, + ); + push_generated_case_bidirectional( + &mut cases, + split(10, axis, &[1, 1, 1], vec![leaf(1), leaf(2), leaf(3)]), + split(10, axis, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)]), + bounds, + ); + push_generated_case_bidirectional( + &mut cases, + split( + 10, + axis, + &[1, 1], + vec![ + leaf(1), + split(11, child_axis, &[1, 1], vec![leaf(2), leaf(3)]), + ], + ), + split(10, axis, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)]), + bounds, + ); + push_generated_case_bidirectional( + &mut cases, + split( + 10, + axis, + &[1, 1], + vec![ + split(11, child_axis, &[1, 1], vec![leaf(1), leaf(2)]), + leaf(3), + ], + ), + split(10, axis, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)]), + bounds, + ); + push_generated_case_bidirectional( + &mut cases, + split( + 10, + axis, + &[1, 1], + vec![ + leaf(1), + split(11, child_axis, &[1, 1], vec![leaf(2), leaf(3)]), + ], + ), + split( + 10, + axis, + &[1, 3], + vec![ + leaf(1), + split(11, child_axis, &[3, 1], vec![leaf(2), leaf(3)]), + ], + ), + bounds, + ); + push_generated_case_bidirectional( + &mut cases, + split( + 10, + axis, + &[1, 1], + vec![ + split(11, child_axis, &[1, 1], vec![leaf(1), leaf(2)]), + leaf(3), + ], + ), + split( + 10, + axis, + &[3, 1], + vec![ + split(11, child_axis, &[1, 3], vec![leaf(1), leaf(2)]), + leaf(3), + ], + ), + bounds, + ); + } + + assert_eq!(cases.len(), 24); + for (old, new, bounds) in cases { + assert_generated_case_plans_deterministically(&old, &new, bounds); + } + } + + #[test] + fn stack_extraction_creates_space_before_moving_child() { + let req = request(vec![ + MultiphaseWindow { + node_id: id(1), + from: rect(0, 0, 200, 100), + to: rect(0, 0, 100, 100), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(2), + from: rect(200, 0, 400, 50), + to: rect(100, 0, 300, 100), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(3), + from: rect(200, 50, 400, 100), + to: rect(300, 0, 400, 100), + hierarchy: Default::default(), + }, + ]); + let plan = plan_no_overlap(&req).unwrap(); + assert_eq!( + actions(&plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal + }, + PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical + }, + ] + ); + assert_eq!(plan.phases[0].steps[0].to, rect(0, 0, 100, 100)); + assert_eq!(plan.phases[0].steps[1].to, rect(300, 50, 400, 100)); + assert_eq!(plan.phases[1].steps[0].to, rect(100, 0, 300, 50)); + assert_eq!(plan.phases[2].steps[0].to, rect(100, 0, 300, 100)); + assert_eq!(plan.phases[2].steps[1].to, rect(300, 0, 400, 100)); + assert!(validate_plan_continuous(&req, &plan)); + } + + #[test] + fn stack_extraction_reverse_replays_phases_in_reverse() { + let req = request(vec![ + MultiphaseWindow { + node_id: id(1), + from: rect(0, 0, 100, 100), + to: rect(0, 0, 200, 100), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(2), + from: rect(100, 0, 300, 100), + to: rect(200, 0, 400, 50), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(3), + from: rect(300, 0, 400, 100), + to: rect(200, 50, 400, 100), + hierarchy: Default::default(), + }, + ]); + let planned = plan_no_overlap_explained(&req).unwrap(); + let plan = &planned.plan; + assert_eq!( + actions(plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical + }, + PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal + }, + ] + ); + assert_eq!( + planned.explanation.strategy, + PlanStrategy::ReversedForwardPlan { + original: Box::new(PlanStrategy::SpaceThenOrthogonalGrowth { + axis: PhaseAxis::Horizontal + }) + } + ); + assert!(validate_plan_continuous(&req, plan)); + } + + #[test] + fn vertical_stack_extraction_creates_space_before_moving_child() { + let req = request(vec![ + MultiphaseWindow { + node_id: id(1), + from: rect(0, 0, 100, 200), + to: rect(0, 0, 100, 100), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(2), + from: rect(0, 200, 50, 400), + to: rect(0, 100, 100, 300), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(3), + from: rect(50, 200, 100, 400), + to: rect(0, 300, 100, 400), + hierarchy: Default::default(), + }, + ]); + let plan = plan_no_overlap(&req).unwrap(); + assert_eq!( + actions(&plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical + }, + PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Vertical + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal + }, + ] + ); + assert_eq!(plan.phases[0].steps[0].to, rect(0, 0, 100, 100)); + assert_eq!(plan.phases[0].steps[1].to, rect(50, 300, 100, 400)); + assert_eq!(plan.phases[1].steps[0].to, rect(0, 100, 50, 300)); + assert_eq!(plan.phases[2].steps[0].to, rect(0, 100, 100, 300)); + assert_eq!(plan.phases[2].steps[1].to, rect(0, 300, 100, 400)); + assert!(validate_plan_continuous(&req, &plan)); + } + + #[test] + fn vertical_stack_extraction_with_clearance_still_plans() { + let old = split( + 20, + PhaseAxis::Vertical, + &[1, 1], + vec![ + leaf(1), + split(21, PhaseAxis::Horizontal, &[1, 1], vec![leaf(2), leaf(3)]), + ], + ); + let new = split( + 20, + PhaseAxis::Vertical, + &[1, 2, 1], + vec![leaf(1), leaf(2), leaf(3)], + ); + let mut req = generated_request(&old, &new, rect(0, 0, 100, 400)); + req.clearance = 10; + let planned = plan_no_overlap_explained(&req).unwrap(); + + assert_eq!( + planned.explanation.strategy, + PlanStrategy::SpaceThenOrthogonalGrowth { + axis: PhaseAxis::Vertical, + } + ); + assert!(validate_plan_continuous(&req, &planned.plan)); + } + + #[test] + fn vertical_stack_extraction_reverse_replays_phases_in_reverse() { + let req = request(vec![ + MultiphaseWindow { + node_id: id(1), + from: rect(0, 0, 100, 100), + to: rect(0, 0, 100, 200), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(2), + from: rect(0, 100, 100, 300), + to: rect(0, 200, 50, 400), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(3), + from: rect(0, 300, 100, 400), + to: rect(50, 200, 100, 400), + hierarchy: Default::default(), + }, + ]); + let plan = plan_no_overlap(&req).unwrap(); + assert_eq!( + actions(&plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal + }, + PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Vertical + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical + }, + ] + ); + assert!(validate_plan_continuous(&req, &plan)); + } + + #[test] + fn unsupported_diagonal_motion_falls_back_to_linear() { + let req = request(vec![MultiphaseWindow { + node_id: id(1), + from: rect(0, 0, 100, 100), + to: rect(100, 100, 200, 200), + hierarchy: Default::default(), + }]); + assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan)); + let diagnostic = plan_no_overlap_with_diagnostics(&req).unwrap_err(); + assert_eq!(diagnostic.forward, MultiphasePlanFailure::NoPattern); + assert_eq!(diagnostic.reverse, Some(MultiphasePlanFailure::NoPattern)); + let mut expected = no_pattern_attempts(PlanDirection::Forward); + expected.extend(no_pattern_attempts(PlanDirection::Reverse)); + assert_eq!(diagnostic.attempted, expected); + } + + #[test] + fn diagnostics_report_shrink_bound_rejections() { + let req = MultiphaseRequest { + bounds: rect(0, 0, 400, 100), + clearance: 0, + windows: vec![ + MultiphaseWindow { + node_id: id(1), + from: rect(0, 0, 200, 100), + to: rect(0, 0, 10, 100), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(2), + from: rect(200, 0, 400, 100), + to: rect(10, 0, 400, 100), + hierarchy: Default::default(), + }, + ], + }; + + assert!(matches!( + plan_no_overlap_with_diagnostics(&req).unwrap_err().forward, + MultiphasePlanFailure::ShrinkBound { + axis: PhaseAxis::Horizontal, + available: 10, + required: 50, + } + )); + } + + #[test] + fn diagnostics_report_candidate_validation_rejections() { + let req = request(vec![ + MultiphaseWindow { + node_id: id(1), + from: rect(0, 0, 60, 60), + to: rect(180, 0, 240, 60), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(2), + from: rect(90, 0, 150, 60), + to: rect(90, 0, 150, 60), + hierarchy: Default::default(), + }, + ]); + let rejection = + MultiphasePlanFailure::Validation(MultiphaseValidationError::PhaseOverlap { + phase: 0, + a: id(1), + b: id(2), + }); + let diagnostic = plan_no_overlap_with_diagnostics(&req).unwrap_err(); + + assert_eq!(diagnostic.forward, rejection); + assert_eq!(diagnostic.reverse, Some(rejection)); + assert_eq!( + diagnostic.attempted[0], + RejectedStrategy { + direction: PlanDirection::Forward, + strategy: PlanStrategy::SingleAction, + reason: rejection, + } + ); + assert!(diagnostic.attempted.iter().any(|attempt| *attempt + == RejectedStrategy { + direction: PlanDirection::Reverse, + strategy: PlanStrategy::SingleAction, + reason: rejection, + })); + } + + #[test] + fn hierarchy_metadata_classifies_depth_and_mono_transitions() { + let source = MultiphaseHierarchyPosition { + parent: Some(id(10)), + depth: 2, + sibling_index: Some(0), + split_axis: Some(PhaseAxis::Vertical), + nearest_horizontal_split_depth: Some(1), + nearest_vertical_split_depth: Some(2), + ..Default::default() + }; + let target = MultiphaseHierarchyPosition { + parent: Some(id(11)), + depth: 1, + sibling_index: Some(2), + split_axis: Some(PhaseAxis::Horizontal), + nearest_horizontal_split_depth: Some(1), + ..Default::default() + }; + assert_eq!( + MultiphaseWindowHierarchy::new(source, target).transition, + MultiphaseHierarchyTransition::Ascending + ); + assert_eq!(source.nearest_vertical_split_depth, Some(2)); + + let entering_mono = MultiphaseWindowHierarchy::new( + source, + MultiphaseHierarchyPosition { + parent_is_mono: true, + mono_active: true, + ..target + }, + ); + assert_eq!( + entering_mono.transition, + MultiphaseHierarchyTransition::EnteringMono + ); + assert_eq!( + entering_mono.reversed().transition, + MultiphaseHierarchyTransition::ExitingMono + ); + } + + #[test] + fn continuous_validation_rejects_narrow_mid_phase_overlap() { + let req = request(vec![ + MultiphaseWindow { + node_id: id(1), + from: rect(0, 0, 10, 10), + to: rect(100, 0, 110, 10), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(2), + from: rect(13, 0, 14, 10), + to: rect(13, 0, 14, 10), + hierarchy: Default::default(), + }, + ]); + let plan = MultiphasePlan { + phases: vec![MultiphasePhase { + action: MultiphasePhaseAction::Uniform(PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal, + }), + steps: vec![MultiphaseStep { + node_id: id(1), + from: rect(0, 0, 10, 10), + to: rect(100, 0, 110, 10), + }], + }], + }; + + assert_eq!( + validate_plan_continuous_diagnostic(&req, &plan), + Err(MultiphaseValidationError::PhaseOverlap { + phase: 0, + a: id(1), + b: id(2), + }) + ); + } + + #[test] + fn continuous_validation_allows_edge_touching_motion() { + let req = request(vec![ + MultiphaseWindow { + node_id: id(1), + from: rect(0, 0, 10, 10), + to: rect(10, 0, 20, 10), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(2), + from: rect(20, 0, 30, 10), + to: rect(20, 0, 30, 10), + hierarchy: Default::default(), + }, + ]); + let plan = MultiphasePlan { + phases: vec![MultiphasePhase { + action: MultiphasePhaseAction::Uniform(PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal, + }), + steps: vec![MultiphaseStep { + node_id: id(1), + from: rect(0, 0, 10, 10), + to: rect(10, 0, 20, 10), + }], + }], + }; + + assert!(validate_plan_continuous(&req, &plan)); + } + + #[test] + fn continuous_validation_rejects_mixed_phase_action_count_mismatch() { + let req = request(vec![ + window(1, rect(0, 0, 40, 40), rect(40, 0, 80, 40)), + window(2, rect(100, 0, 140, 40), rect(100, 0, 140, 80)), + ]); + let plan = MultiphasePlan { + phases: vec![MultiphasePhase { + action: MultiphasePhaseAction::Mixed(vec![PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal, + }]), + steps: vec![ + MultiphaseStep { + node_id: id(1), + from: rect(0, 0, 40, 40), + to: rect(40, 0, 80, 40), + }, + MultiphaseStep { + node_id: id(2), + from: rect(100, 0, 140, 40), + to: rect(100, 0, 140, 80), + }, + ], + }], + }; + + assert_eq!( + validate_plan_continuous_diagnostic(&req, &plan), + Err(MultiphaseValidationError::PhaseActionCount { + phase: 0, + actions: 1, + steps: 2, + }) + ); + } + + #[test] + fn continuous_validation_rejects_stale_step_start_rect() { + let req = request(vec![MultiphaseWindow { + node_id: id(1), + from: rect(0, 0, 10, 10), + to: rect(20, 0, 30, 10), + hierarchy: Default::default(), + }]); + let plan = MultiphasePlan { + phases: vec![MultiphasePhase { + action: MultiphasePhaseAction::Uniform(PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal, + }), + steps: vec![MultiphaseStep { + node_id: id(1), + from: rect(5, 0, 15, 10), + to: rect(20, 0, 30, 10), + }], + }], + }; + + assert_eq!( + validate_plan_continuous_diagnostic(&req, &plan), + Err(MultiphaseValidationError::StaleStepStart { + phase: 0, + node_id: id(1), + }) + ); + } + + #[test] + fn motion_groups_split_disjoint_layout_changes() { + let windows = vec![ + MultiphaseWindow { + node_id: id(1), + from: rect(0, 0, 100, 100), + to: rect(100, 0, 200, 100), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(2), + from: rect(100, 0, 200, 100), + to: rect(0, 0, 100, 100), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(3), + from: rect(300, 0, 400, 100), + to: rect(400, 0, 500, 100), + hierarchy: Default::default(), + }, + ]; + assert_eq!(partition_motion_groups(&windows, 0), vec![vec![0, 1], vec![2]]); + } + + #[test] + fn motion_groups_are_transitive() { + let windows = vec![ + MultiphaseWindow { + node_id: id(1), + from: rect(0, 0, 100, 100), + to: rect(80, 0, 180, 100), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(2), + from: rect(170, 0, 270, 100), + to: rect(250, 0, 350, 100), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(3), + from: rect(90, 0, 180, 100), + to: rect(180, 0, 260, 100), + hierarchy: Default::default(), + }, + ]; + assert_eq!(partition_motion_groups(&windows, 0), vec![vec![0, 1, 2]]); + } + + #[test] + fn motion_groups_join_across_animation_clearance() { + let windows = vec![ + MultiphaseWindow { + node_id: id(1), + from: rect(0, 0, 100, 100), + to: rect(0, 0, 80, 100), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(2), + from: rect(120, 0, 220, 100), + to: rect(110, 0, 210, 100), + hierarchy: Default::default(), + }, + ]; + assert_eq!(partition_motion_groups(&windows, 0), vec![vec![0], vec![1]]); + assert_eq!(partition_motion_groups(&windows, 10), vec![vec![0, 1]]); + } +} diff --git a/crates/libinput/Cargo.toml b/crates/libinput/Cargo.toml new file mode 100644 index 00000000..9fff6e9d --- /dev/null +++ b/crates/libinput/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "jay-libinput" +version.workspace = true +edition.workspace = true +license.workspace = true +build = "build.rs" + +[dependencies] +jay-utils = { path = "../utils" } + +bstr = { version = "1.9.0", default-features = false, features = ["std"] } +isnt = "0.2.0" +libloading = "0.9.0" +log = { version = "0.4.20", features = ["std"] } +thiserror = "2.0.11" +uapi = "0.2.13" + +[build-dependencies] +anyhow = "1.0.79" +cc = "1.0.86" +repc = "0.1.1" diff --git a/crates/libinput/build.rs b/crates/libinput/build.rs new file mode 100644 index 00000000..19fd83f3 --- /dev/null +++ b/crates/libinput/build.rs @@ -0,0 +1,181 @@ +use { + repc::layout::{Type, TypeVariant}, + std::{ + env, + fs::{File, OpenOptions}, + io::{self, BufWriter, Write}, + path::PathBuf, + }, +}; + +#[allow(unused_macros)] +macro_rules! cenum { + ($name:ident, $uc:ident; $($name2:ident = $val:expr,)*) => { + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + pub struct $name(pub i32); + + impl $name { + pub fn raw(self) -> i32 { + self.0 + } + } + + $( + pub const $name2: $name = $name($val); + )* + + pub const $uc: &[i32] = &[$($val,)*]; + }; +} + +#[path = "src/consts.rs"] +mod consts; + +fn open(s: &str) -> io::Result> { + let mut path = PathBuf::from(env::var("OUT_DIR").unwrap()); + path.push(s); + Ok(BufWriter::new( + OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?, + )) +} + +fn get_target() -> repc::Target { + let rustc_target = env::var("TARGET").unwrap(); + repc::TARGET_MAP + .iter() + .cloned() + .find(|t| t.0 == rustc_target) + .unwrap() + .1 +} + +fn get_enum_ty(variants: Vec) -> anyhow::Result { + let target = get_target(); + let ty = Type { + layout: (), + annotations: vec![], + variant: TypeVariant::Enum(variants), + }; + let ty = repc::compute_layout(target, &ty)?; + assert!(ty.layout.pointer_alignment_bits <= ty.layout.size_bits); + Ok(ty.layout.size_bits) +} + +fn write_ty(f: &mut W, vals: &[i32], ty: &str) -> anyhow::Result<()> { + let variants: Vec<_> = vals.iter().cloned().map(|v| v as i128).collect(); + let size = get_enum_ty(variants)?; + writeln!(f, "#[allow(clippy::allow_attributes, dead_code)]")?; + writeln!(f, "pub type {} = i{};", ty, size)?; + Ok(()) +} + +fn main() -> anyhow::Result<()> { + println!("cargo:rerun-if-changed=src/bridge.c"); + cc::Build::new() + .file("src/bridge.c") + .opt_level(2) + .compile("jay-libinput-bridge"); + + let mut f = open("libinput_tys.rs")?; + write_ty( + &mut f, + consts::LIBINPUT_LOG_PRIORITY, + "libinput_log_priority", + )?; + write_ty( + &mut f, + consts::LIBINPUT_DEVICE_CAPABILITY, + "libinput_device_capability", + )?; + write_ty(&mut f, consts::LIBINPUT_KEY_STATE, "libinput_key_state")?; + write_ty(&mut f, consts::LIBINPUT_LED, "libinput_led")?; + write_ty( + &mut f, + consts::LIBINPUT_BUTTON_STATE, + "libinput_button_state", + )?; + write_ty( + &mut f, + consts::LIBINPUT_POINTER_AXIS, + "libinput_pointer_axis", + )?; + write_ty( + &mut f, + consts::LIBINPUT_POINTER_AXIS_SOURCE, + "libinput_pointer_axis_source", + )?; + write_ty( + &mut f, + consts::LIBINPUT_TABLET_PAD_RING_AXIS_SOURCE, + "libinput_tablet_pad_ring_axis_source", + )?; + write_ty( + &mut f, + consts::LIBINPUT_TABLET_PAD_STRIP_AXIS_SOURCE, + "libinput_tablet_pad_strip_axis_source", + )?; + write_ty( + &mut f, + consts::LIBINPUT_TABLET_TOOL_TYPE, + "libinput_tablet_tool_type", + )?; + write_ty( + &mut f, + consts::LIBINPUT_TABLET_TOOL_PROXIMITY_STATE, + "libinput_tablet_tool_proximity_state", + )?; + write_ty( + &mut f, + consts::LIBINPUT_TABLET_TOOL_TIP_STATE, + "libinput_tablet_tool_tip_state", + )?; + write_ty( + &mut f, + consts::LIBINPUT_SWITCH_STATE, + "libinput_switch_state", + )?; + write_ty(&mut f, consts::LIBINPUT_SWITCH, "libinput_switch")?; + write_ty(&mut f, consts::LIBINPUT_EVENT_TYPE, "libinput_event_type")?; + write_ty( + &mut f, + consts::LIBINPUT_CONFIG_STATUS, + "libinput_config_status", + )?; + write_ty( + &mut f, + consts::LIBINPUT_CONFIG_ACCEL_PROFILE, + "libinput_config_accel_profile", + )?; + write_ty( + &mut f, + consts::LIBINPUT_CONFIG_TAP_STATE, + "libinput_config_tap_state", + )?; + write_ty( + &mut f, + consts::LIBINPUT_CONFIG_DRAG_STATE, + "libinput_config_drag_state", + )?; + write_ty( + &mut f, + consts::LIBINPUT_CONFIG_DRAG_LOCK_STATE, + "libinput_config_drag_lock_state", + )?; + write_ty( + &mut f, + consts::LIBINPUT_CONFIG_CLICK_METHOD, + "libinput_config_click_method", + )?; + write_ty( + &mut f, + consts::LIBINPUT_CONFIG_MIDDLE_EMULATION_STATE, + "libinput_config_middle_emulation_state", + )?; + + println!("cargo:rerun-if-changed=src/consts.rs"); + Ok(()) +} diff --git a/src/bridge.c b/crates/libinput/src/bridge.c similarity index 100% rename from src/bridge.c rename to crates/libinput/src/bridge.c diff --git a/src/libinput/consts.rs b/crates/libinput/src/consts.rs similarity index 100% rename from src/libinput/consts.rs rename to crates/libinput/src/consts.rs diff --git a/src/libinput/device.rs b/crates/libinput/src/device.rs similarity index 99% rename from src/libinput/device.rs rename to crates/libinput/src/device.rs index 9d398880..596c047d 100644 --- a/src/libinput/device.rs +++ b/crates/libinput/src/device.rs @@ -1,5 +1,5 @@ use { - crate::libinput::{ + crate::{ LibInput, consts::{ AccelProfile, ConfigClickMethod, ConfigDragLockState, ConfigDragState, diff --git a/src/libinput/event.rs b/crates/libinput/src/event.rs similarity index 98% rename from src/libinput/event.rs rename to crates/libinput/src/event.rs index c296c56f..7fb16146 100644 --- a/src/libinput/event.rs +++ b/crates/libinput/src/event.rs @@ -1,5 +1,5 @@ use { - crate::libinput::{ + crate::{ consts::{ ButtonState, EventType, KeyState, PointerAxis, Switch, SwitchState, TabletPadRingAxisSource, TabletPadStripAxisSource, TabletToolProximityState, @@ -292,7 +292,7 @@ impl<'a> LibInputEventSwitch<'a> { macro_rules! has_changed { ($name:ident, $f:ident) => { pub fn $name(&self) -> bool { - unsafe { crate::libinput::sys::$f(self.event) != 0 } + unsafe { crate::sys::$f(self.event) != 0 } } }; } @@ -300,7 +300,7 @@ macro_rules! has_changed { macro_rules! get_double { ($name:ident, $f:ident) => { pub fn $name(&self) -> f64 { - unsafe { crate::libinput::sys::$f(self.event) } + unsafe { crate::sys::$f(self.event) } } }; } @@ -308,7 +308,7 @@ macro_rules! get_double { macro_rules! has_capability { ($name:ident, $f:ident) => { pub fn $name(&self) -> bool { - unsafe { crate::libinput::sys::$f(self.tool) != 0 } + unsafe { crate::sys::$f(self.tool) != 0 } } }; } diff --git a/crates/libinput/src/lib.rs b/crates/libinput/src/lib.rs new file mode 100644 index 00000000..421b13cd --- /dev/null +++ b/crates/libinput/src/lib.rs @@ -0,0 +1,205 @@ +#![allow(non_camel_case_types)] + +macro_rules! cenum { + ($name:ident, $uc:ident; $($name2:ident = $val:expr,)*) => { + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + pub struct $name(pub i32); + + impl $name { + #[allow(dead_code)] + pub fn raw(self) -> i32 { + self.0 + } + } + + $( + pub const $name2: $name = $name($val); + )* + + pub const $uc: &[i32] = &[$($val,)*]; + }; +} + +pub mod consts; +pub mod device; +pub mod event; +mod sys; + +use { + crate::{ + consts::{ + LIBINPUT_LOG_PRIORITY_DEBUG, LIBINPUT_LOG_PRIORITY_ERROR, LIBINPUT_LOG_PRIORITY_INFO, + LogPriority, + }, + device::RegisteredDevice, + event::LibInputEvent, + sys::{ + libinput, libinput_device_ref, libinput_dispatch, libinput_get_event, libinput_get_fd, + libinput_interface, libinput_log_priority, libinput_log_set_handler, + libinput_log_set_priority, libinput_path_add_device, libinput_path_create_context, + libinput_unref, + }, + }, + bstr::ByteSlice, + isnt::std_1::primitive::IsntConstPtrExt, + jay_utils::{errorfmt::ErrorFmt, oserror::OsError, ptr_ext::PtrExt}, + std::{ffi::CStr, rc::Rc}, + thiserror::Error, + uapi::{IntoUstr, OwnedFd, c}, +}; + +static INTERFACE: libinput_interface = libinput_interface { + open_restricted, + close_restricted, +}; + +unsafe extern "C" fn open_restricted( + path: *const c::c_char, + _flags: c::c_int, + user_data: *mut c::c_void, +) -> c::c_int { + unsafe { + let ud = (user_data as *const UserData).deref(); + match ud.adapter.open(CStr::from_ptr(path)) { + Ok(f) => f.unwrap(), + Err(e) => { + log::error!("Could not open device for libinput: {}", ErrorFmt(e)); + -1 + } + } + } +} + +unsafe extern "C" fn close_restricted(fd: c::c_int, _user_data: *mut c::c_void) { + drop(OwnedFd::new(fd)); +} + +struct UserData { + adapter: Rc, +} + +pub trait LibInputAdapter { + fn open(&self, path: &CStr) -> Result; +} + +#[derive(Debug, Error)] +pub enum LibInputError { + #[error("Could not create a libinput instance")] + New, + #[error("Could not open a libinput device")] + Open, + #[error("Could not dispatch libinput events")] + Dispatch(#[source] OsError), + #[error("The requested device is not available")] + DeviceUnavailable, + #[error("Dupfd failed")] + DupFd(#[source] OsError), + #[error("Stat failed")] + Stat(#[source] OsError), +} + +pub struct LibInput { + _data: Box, + li: *mut libinput, +} + +unsafe extern "C" { + fn jay_libinput_log_handler_bridge(); +} + +impl LibInput { + pub fn new(adapter: Rc) -> Result { + let mut ud = Box::new(UserData { adapter }); + let li = unsafe { + libinput_path_create_context(&INTERFACE, &mut *ud as *mut _ as *mut c::c_void) + }; + if li.is_null() { + return Err(LibInputError::New); + } + unsafe { + libinput_log_set_handler(li, jay_libinput_log_handler_bridge); + let priority = if log::log_enabled!(log::Level::Debug) { + LIBINPUT_LOG_PRIORITY_DEBUG + } else if log::log_enabled!(log::Level::Info) { + LIBINPUT_LOG_PRIORITY_INFO + } else { + LIBINPUT_LOG_PRIORITY_ERROR + }; + libinput_log_set_priority(li, priority.raw() as _); + } + Ok(Self { _data: ud, li }) + } + + pub fn fd(&self) -> c::c_int { + unsafe { libinput_get_fd(self.li) } + } + + pub fn open<'a>( + self: &Rc, + path: impl IntoUstr<'a>, + ) -> Result { + let path = path.into_ustr(); + let res = unsafe { libinput_path_add_device(self.li, path.as_ptr()) }; + if res.is_null() { + Err(LibInputError::Open) + } else { + unsafe { + libinput_device_ref(res); + } + Ok(RegisteredDevice { + _li: self.clone(), + dev: res, + }) + } + } + + pub fn dispatch(&self) -> Result<(), LibInputError> { + let res = unsafe { libinput_dispatch(self.li) }; + if res < 0 { + Err(LibInputError::Dispatch(OsError(-res))) + } else { + Ok(()) + } + } + + pub fn event(&self) -> Option> { + let res = unsafe { libinput_get_event(self.li) }; + if res.is_null() { + None + } else { + Some(LibInputEvent { + event: res, + _phantom: Default::default(), + }) + } + } +} + +impl Drop for LibInput { + fn drop(&mut self) { + unsafe { + libinput_unref(self.li); + } + } +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn jay_libinput_log_handler( + _libinput: *mut libinput, + priority: libinput_log_priority, + line: *const c::c_char, +) { + assert!(line.is_not_null()); + let str = unsafe { CStr::from_ptr(line) }; + let priority = match LogPriority(priority as _) { + LIBINPUT_LOG_PRIORITY_DEBUG => log::Level::Debug, + LIBINPUT_LOG_PRIORITY_INFO => log::Level::Info, + LIBINPUT_LOG_PRIORITY_ERROR => log::Level::Error, + _ => log::Level::Error, + }; + log::log!( + priority, + "libinput: {}", + str.to_bytes().trim_ascii().as_bstr() + ); +} diff --git a/src/libinput/sys.rs b/crates/libinput/src/sys.rs similarity index 100% rename from src/libinput/sys.rs rename to crates/libinput/src/sys.rs diff --git a/crates/logger/Cargo.toml b/crates/logger/Cargo.toml new file mode 100644 index 00000000..50e39217 --- /dev/null +++ b/crates/logger/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "jay-logger" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +jay-config = { path = "../jay-config" } +jay-utils = { path = "../utils" } + +backtrace = "0.3.69" +bstr = { version = "1.9.0", default-features = false, features = ["std"] } +clap = { version = "4.4.18", features = ["derive", "wrap_help"] } +dirs = "6.0.0" +humantime = "2.1.0" +linearize = { version = "0.1.3", features = ["derive"] } +log = { version = "0.4.20", features = ["std"] } +parking_lot = "0.12.1" +thiserror = "2.0.11" +uapi = "0.2.13" diff --git a/crates/logger/src/lib.rs b/crates/logger/src/lib.rs new file mode 100644 index 00000000..8752fdee --- /dev/null +++ b/crates/logger/src/lib.rs @@ -0,0 +1,374 @@ +use { + jay_config::logging::LogLevel as ConfigLogLevel, + jay_utils::{ + atomic_enum::AtomicEnum, + errorfmt::ErrorFmt, + oserror::{OsError, OsErrorExt, OsErrorExt2}, + static_text::StaticText, + }, + backtrace::Backtrace, + bstr::{BStr, BString, ByteSlice}, + clap::ValueEnum, + linearize::Linearize, + log::{LevelFilter, Log, Metadata, Record}, + parking_lot::Mutex, + std::{ + cell::Cell, + fmt::Arguments, + fs::DirBuilder, + io::Write, + os::unix::{ffi::OsStringExt, fs::DirBuilderExt}, + ptr, + sync::{ + Arc, + atomic::{AtomicI32, AtomicU32, Ordering::Relaxed}, + }, + thread, + time::SystemTime, + }, + thiserror::Error, + uapi::{AsUstr, Dirent, Fd, OwnedFd, Ustring, c, format_ustr}, +}; + +#[derive(ValueEnum, Debug, Copy, Clone, Hash, Default, Eq, PartialEq, Linearize)] +pub enum LogLevel { + Trace, + Debug, + #[default] + Info, + Warn, + Error, + Off, +} + +impl From for LevelFilter { + fn from(value: LogLevel) -> Self { + match value { + LogLevel::Trace => LevelFilter::Trace, + LogLevel::Debug => LevelFilter::Debug, + LogLevel::Info => LevelFilter::Info, + LogLevel::Warn => LevelFilter::Warn, + LogLevel::Error => LevelFilter::Error, + LogLevel::Off => LevelFilter::Off, + } + } +} + +impl From for LogLevel { + fn from(value: LevelFilter) -> Self { + match value { + LevelFilter::Trace => LogLevel::Trace, + LevelFilter::Debug => LogLevel::Debug, + LevelFilter::Info => LogLevel::Info, + LevelFilter::Warn => LogLevel::Warn, + LevelFilter::Error => LogLevel::Error, + LevelFilter::Off => LogLevel::Off, + } + } +} + +impl StaticText for LogLevel { + fn text(&self) -> &'static str { + match self { + LogLevel::Off => "Off", + LogLevel::Error => "Error", + LogLevel::Warn => "Warn", + LogLevel::Info => "Info", + LogLevel::Debug => "Debug", + LogLevel::Trace => "Trace", + } + } +} + +impl From for LogLevel { + fn from(value: ConfigLogLevel) -> Self { + match value { + ConfigLogLevel::Trace => LogLevel::Trace, + ConfigLogLevel::Debug => LogLevel::Debug, + ConfigLogLevel::Info => LogLevel::Info, + ConfigLogLevel::Warn => LogLevel::Warn, + ConfigLogLevel::Error => LogLevel::Error, + } + } +} + +fn fatal(args: Arguments<'_>) -> ! { + log::error!("{}", args); + std::process::exit(1); +} + +thread_local! { + static BUFFER: Cell<*mut Vec> = const { Cell::new(ptr::null_mut()) }; +} + +pub struct Logger { + level: AtomicEnum, + filter: AtomicU32, + path: Mutex>, + _file: Mutex, + file_fd: AtomicI32, +} + +impl Logger { + pub fn install_stderr(level: LogLevel) -> Arc { + let file = match uapi::fcntl_dupfd_cloexec(2, 0).to_os_error() { + Ok(fd) => fd, + Err(e) => { + fatal(format_args!("Error: Could not dup stderr: {}", ErrorFmt(e))); + } + }; + Self::install(level, b"STDERR", file) + } + + pub fn install_compositor(level: LogLevel) -> Arc { + let (path, file) = open_log_file("jay"); + Self::install(level, path.as_bytes(), file) + } + + pub fn install_pipe(file: OwnedFd, level: LogLevel) -> Arc { + Self::install(level, b"PIPE", file) + } + + fn install(level: LogLevel, path: &[u8], file: OwnedFd) -> Arc { + let filter: LevelFilter = level.into(); + let slf = Arc::new(Self { + level: AtomicEnum::new(level), + filter: AtomicU32::new(filter as _), + path: Mutex::new(Arc::new(path.to_vec().into())), + file_fd: AtomicI32::new(file.raw()), + _file: Mutex::new(file), + }); + log::set_boxed_logger(Box::new(LogWrapper { + logger: slf.clone(), + })) + .unwrap(); + log::set_max_level(filter); + set_panic_hook(); + slf + } + + pub fn set_level(&self, level: LogLevel) { + let filter: LevelFilter = level.into(); + self.level.store(level, Relaxed); + self.filter.store(filter as _, Relaxed); + log::set_max_level(filter); + } + + pub fn clean_logs_older_than(&self, time: SystemTime) { + let time_formatted = humantime::format_rfc3339_millis(time); + log::info!("Cleaning unused log files older than {}", time_formatted); + let path = self.path(); + thread::spawn(move || { + if let Err(e) = clean_logs_older_than(path.as_bstr(), time) { + log::error!("Could not clean log files: {}", ErrorFmt(e)); + } + }); + } + + pub fn path(&self) -> Arc { + self.path.lock().clone() + } + + pub fn redirect(&self, ty: &str) -> Ustring { + let (file, fd) = open_log_file(ty); + log::info!("Redirecting logs to {}", file.display()); + *self.path.lock() = Arc::new(file.as_bytes().into()); + self.file_fd.store(fd.raw(), Relaxed); + *self._file.lock() = fd; + file + } + + pub fn write_raw(&self, buf: &[u8]) { + let mut fd = Fd::new(self.file_fd.load(Relaxed)); + let _ = fd.write_all(buf); + } +} + +pub fn open_log_file(ty: &str) -> (Ustring, OwnedFd) { + let log_dir = create_log_dir(ty); + let mut flock_fail_count = 0; + for i in 0.. { + let file_name = format_ustr!( + "{}/{ty}-{}-{}.txt", + log_dir, + humantime::format_rfc3339_millis(SystemTime::now()), + i, + ); + match uapi::open( + &file_name, + c::O_CREAT | c::O_EXCL | c::O_CLOEXEC | c::O_WRONLY, + 0o644, + ) + .to_os_error() + { + Ok(f) => { + if let Err(e) = uapi::flock(f.raw(), c::LOCK_EX | c::LOCK_NB) { + log::warn!("Unable to flock just-opened logfile: {}", ErrorFmt(e)); + flock_fail_count += 1; + if flock_fail_count > 10 { + log::error!(concat!( + "Failed to flock just-opened logfile more than 10 times in a row. ", + "Not flocking the logfile, if the cleanup routine later succeeds to ", + "flock this logfile, it will be deleted even if it is still in use." + )); + } else { + continue; + } + } + return (file_name, f); + } + Err(OsError(c::EEXIST)) => {} + Err(e) => { + fatal(format_args!( + "Error: Could not create log file: {}", + ErrorFmt(e) + )); + } + } + } + unreachable!() +} + +fn create_log_dir(ty: &str) -> BString { + let mut log_dir = match dirs::data_local_dir() { + Some(d) => d, + None => fatal(format_args!("Error: $HOME is not set")), + }; + log_dir.push("jay"); + log_dir.push("logs"); + log_dir.push(ty); + let res = DirBuilder::new() + .recursive(true) + .mode(0o755) + .create(&log_dir); + if let Err(e) = res { + fatal(format_args!( + "Error: Could not create log directory {}: {}", + log_dir.display(), + ErrorFmt(e) + )); + } + log_dir.into_os_string().into_vec().into() +} + +fn set_panic_hook() { + std::panic::set_hook(Box::new(|p| { + if let Some(loc) = p.location() { + log::error!( + "Panic at {} line {} column {}", + loc.file(), + loc.line(), + loc.column() + ); + } else { + log::error!("Panic at unknown location"); + } + if let Some(msg) = p.payload().downcast_ref::<&str>() { + log::error!("Message: {}", msg); + } + if let Some(msg) = p.payload().downcast_ref::() { + log::error!("Message: {}", msg); + } + log::error!("Backtrace:\n{:?}", Backtrace::new()); + })); +} + +struct LogWrapper { + logger: Arc, +} + +impl Log for LogWrapper { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() as u32 <= self.logger.filter.load(Relaxed) + } + + fn log(&self, record: &Record) { + if record.level() as u32 > self.logger.filter.load(Relaxed) { + return; + } + let mut buffer = BUFFER.get(); + if buffer.is_null() { + buffer = Box::into_raw(Box::default()); + BUFFER.set(buffer); + } + let buffer = unsafe { &mut *buffer }; + buffer.clear(); + let now = SystemTime::now(); + let _ = writeln!( + buffer, + "[{} {:5} {}] {}", + humantime::format_rfc3339_millis(now), + record.level(), + record.target(), + record.args(), + ); + let mut fd = Fd::new(self.logger.file_fd.load(Relaxed)); + let _ = fd.write_all(buffer); + } + + fn flush(&self) { + // nothing + } +} + +#[derive(Debug, Error)] +enum CleanLogsError { + #[error("Log path has no parent")] + NoParent, + #[error("Could not open the log directory")] + OpenDir(#[source] OsError), + #[error("Could not enumerate directory entry")] + ReadDir(#[source] OsError), + #[error("Could not open the log file")] + OpenFile(#[source] OsError), + #[error("Could not stat the log file")] + Stat(#[source] OsError), + #[error("Could not unlink the log file")] + Unlink(#[source] OsError), +} + +fn clean_logs_older_than(current_log_path: &BStr, time: SystemTime) -> Result<(), CleanLogsError> { + let current_log_path = current_log_path.to_path_lossy(); + let parent = current_log_path.parent().ok_or(CleanLogsError::NoParent)?; + let mut dir = uapi::opendir(parent).map_os_err(CleanLogsError::OpenDir)?; + let parent = uapi::open(parent, c::O_PATH | c::O_CLOEXEC | c::O_DIRECTORY, 0) + .map_os_err(CleanLogsError::OpenDir)?; + let time = time + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as c::time_t; + while let Some(entry) = uapi::readdir(&mut dir) { + let entry = entry.map_os_err(CleanLogsError::ReadDir)?; + if let Err(err) = process_entry(parent.raw(), &entry, time) { + log::error!( + "Could not clean log file {}: {}", + entry.name().as_ustr().display(), + ErrorFmt(err), + ); + } + } + fn process_entry( + parent: c::c_int, + entry: &Dirent, + time: c::time_t, + ) -> Result<(), CleanLogsError> { + if entry.d_type != c::DT_REG { + return Ok(()); + } + let name = entry.name(); + let file = uapi::openat(parent, name, c::O_RDONLY | c::O_CLOEXEC, 0) + .map_os_err(CleanLogsError::OpenFile)?; + let stat = uapi::fstat(*file).map_os_err(CleanLogsError::Stat)?; + if stat.st_mtime >= time { + return Ok(()); + } + if uapi::flock(file.raw(), c::LOCK_EX | c::LOCK_NB).is_err() { + log::info!("Preserving file still in use: {}", name.as_ustr().display()); + return Ok(()); + } + uapi::unlinkat(parent, name, 0).map_os_err(CleanLogsError::Unlink)?; + log::info!("Deleted {}", name.as_ustr().display()); + Ok(()) + } + Ok(()) +} diff --git a/crates/output-schedule/Cargo.toml b/crates/output-schedule/Cargo.toml new file mode 100644 index 00000000..7d08af93 --- /dev/null +++ b/crates/output-schedule/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "jay-output-schedule" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +jay-async-engine = { path = "../async-engine" } +jay-io-uring = { path = "../io-uring" } +jay-utils = { path = "../utils" } + +futures-util = "0.3.30" +log = "0.4.20" +num-traits = "0.2.17" diff --git a/crates/output-schedule/src/lib.rs b/crates/output-schedule/src/lib.rs new file mode 100644 index 00000000..69933f2b --- /dev/null +++ b/crates/output-schedule/src/lib.rs @@ -0,0 +1,219 @@ +use { + jay_async_engine::AsyncEngine, + jay_io_uring::{IoUring, IoUringError}, + jay_utils::{ + asyncevent::AsyncEvent, cell_ext::CellExt, clonecell::CloneCell, errorfmt::ErrorFmt, + numcell::NumCell, + }, + futures_util::{FutureExt, select}, + num_traits::ToPrimitive, + std::{cell::Cell, rc::Rc}, +}; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum Change { + /// The backend has applied the latest changes. + None, + /// There are changes that the backend is not yet aware of. + Scheduled, + /// The backend is aware that there are changes and will apply them as part of the + /// next latch event. + AwaitingLatch, +} + +pub struct OutputSchedule { + changed: AsyncEvent, + run: Cell, + + damage_connector: Rc, + hardware_cursor_damage: CloneCell>>, + cursor_hz_changed: Rc)>, + + persistent: Rc, + + last_present_nsec: Cell, + cursor_delta_nsec: Cell>, + + ring: Rc, + eng: Rc, + + vrr_enabled: Cell, + + hardware_cursor_change: Cell, + software_cursor_change: Cell, + + iteration: NumCell, +} + +pub trait OutputSchedulePersistent { + fn vrr_cursor_hz(&self) -> Option; + fn set_vrr_cursor_hz(&self, hz: Option); +} + +impl OutputSchedule { + pub fn new( + ring: Rc, + eng: Rc, + persistent: Rc, + damage_connector: Rc, + cursor_hz_changed: Rc)>, + ) -> Self { + let slf = Self { + changed: Default::default(), + run: Default::default(), + damage_connector, + cursor_hz_changed, + ring, + eng, + vrr_enabled: Default::default(), + hardware_cursor_change: Cell::new(Change::None), + software_cursor_change: Cell::new(Change::None), + hardware_cursor_damage: Default::default(), + persistent: persistent.clone(), + last_present_nsec: Default::default(), + cursor_delta_nsec: Default::default(), + iteration: Default::default(), + }; + if let Some(hz) = persistent.vrr_cursor_hz() { + slf.set_cursor_hz(hz); + } + slf + } + + pub async fn drive(self: Rc) { + loop { + self.run_once().await; + while !self.run.take() { + self.changed.triggered().await; + } + } + } + + fn trigger(&self) { + let trigger = self.vrr_enabled.get() + && self.cursor_delta_nsec.is_some() + && (self.software_cursor_change.get() == Change::Scheduled + || self.hardware_cursor_change.get() == Change::Scheduled); + if trigger { + self.run.set(true); + self.changed.trigger(); + } + } + + pub fn latched(&self) { + self.last_present_nsec.set(self.eng.now().nsec()); + if self.software_cursor_change.get() == Change::AwaitingLatch { + self.software_cursor_change.set(Change::None); + } + if self.hardware_cursor_change.get() == Change::AwaitingLatch { + self.hardware_cursor_change.set(Change::None); + } + self.iteration.fetch_add(1); + self.trigger(); + } + + pub fn vrr_enabled(&self) -> bool { + self.vrr_enabled.get() + } + + pub fn set_vrr_enabled(&self, enabled: bool) { + self.vrr_enabled.set(enabled); + self.trigger(); + } + + pub fn set_cursor_hz(&self, hz: f64) { + let (hz, delta) = match map_cursor_hz(hz) { + None => { + log::warn!("Ignoring cursor frequency {hz}"); + return; + } + Some(v) => v, + }; + self.persistent.set_vrr_cursor_hz(hz); + (self.cursor_hz_changed)(hz); + self.cursor_delta_nsec.set(delta); + self.trigger(); + } + + pub fn set_hardware_cursor_damage(&self, damage: &Option>) { + self.hardware_cursor_damage.set(damage.clone()); + } + + pub fn defer_cursor_updates(&self) -> bool { + self.vrr_enabled.get() && self.cursor_delta_nsec.is_some() + } + + pub fn hardware_cursor_changed(&self) { + if self.hardware_cursor_change.get() == Change::None { + self.hardware_cursor_change.set(Change::Scheduled); + self.trigger(); + } + } + + pub fn software_cursor_changed(&self) { + if self.software_cursor_change.get() == Change::None { + self.software_cursor_change.set(Change::Scheduled); + self.trigger(); + } + } + + async fn run_once(&self) { + loop { + if self.hardware_cursor_change.get() != Change::Scheduled + && self.software_cursor_change.get() != Change::Scheduled + { + return; + } + if !self.vrr_enabled.get() { + return; + } + let Some(duration) = self.cursor_delta_nsec.get() else { + return; + }; + let iteration = self.iteration.get(); + let next_present = self.last_present_nsec.get().saturating_add(duration); + let res: Result<(), IoUringError> = select! { + _ = self.changed.triggered().fuse() => continue, + v = self.ring.timeout(next_present).fuse() => v, + }; + if let Err(e) = res { + log::error!("Could not wait for timer to expire: {}", ErrorFmt(e)); + return; + } + if iteration == self.iteration.get() { + break; + } + } + self.commit_cursor(); + } + + pub fn commit_cursor(&self) { + if self.hardware_cursor_change.get() == Change::Scheduled { + if let Some(damage) = self.hardware_cursor_damage.get() { + damage(); + } + self.hardware_cursor_change.set(Change::AwaitingLatch); + } + if self.software_cursor_change.get() == Change::Scheduled { + (self.damage_connector)(); + self.software_cursor_change.set(Change::AwaitingLatch); + } + } +} + +pub fn map_cursor_hz(hz: f64) -> Option<(Option, Option)> { + if hz <= 0.0 { + return Some((Some(0.0), Some(u64::MAX))); + } + let delta = (1_000_000_000.0 / hz).to_u64(); + if delta.is_none() { + if hz > 0.0 { + return Some((None, None)); + } + return None; + } + if delta == Some(0) { + return Some((None, None)); + } + Some((Some(hz), delta)) +} diff --git a/crates/output-types/Cargo.toml b/crates/output-types/Cargo.toml new file mode 100644 index 00000000..b0acc23d --- /dev/null +++ b/crates/output-types/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "jay-output-types" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Output identity types for the Jay compositor" +repository = "https://github.com/mahkoh/jay" + +[dependencies] +jay-cmm = { path = "../cmm" } +jay-formats = { path = "../formats" } +jay-utils = { path = "../utils" } + +blake3 = "1.8.2" +linearize = { version = "0.1.3", features = ["derive"] } +uapi = "0.2.13" diff --git a/crates/output-types/src/lib.rs b/crates/output-types/src/lib.rs new file mode 100644 index 00000000..7e4029c3 --- /dev/null +++ b/crates/output-types/src/lib.rs @@ -0,0 +1,348 @@ +use { + jay_cmm::cmm_primaries::Primaries, + jay_formats::Format, + jay_utils::numcell::NumCell, + linearize::Linearize, + std::{ + fmt::{self, Debug, Display, Formatter}, + hash::{Hash, Hasher}, + ops::{BitOr, BitOrAssign}, + rc::Rc, + }, + uapi::{Packed, Pod}, +}; + +macro_rules! linear_ids { + ($ids:ident, $id:ident $(,)?) => { + linear_ids!($ids, $id, u32); + }; + ($ids:ident, $id:ident, $ty:ty $(,)?) => { + #[derive(Debug)] + pub struct $ids { + next: NumCell<$ty>, + } + + impl Default for $ids { + fn default() -> Self { + Self { + next: NumCell::new(1), + } + } + } + + impl $ids { + pub fn next(&self) -> $id { + $id(self.next.fetch_add(1)) + } + } + + #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] + pub struct $id($ty); + + impl $id { + pub fn raw(&self) -> $ty { + self.0 + } + + pub fn from_raw(id: $ty) -> Self { + Self(id) + } + } + + impl Display for $id { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.0, f) + } + } + }; +} + +linear_ids!(ConnectorIds, ConnectorId); +linear_ids!(DrmDeviceIds, DrmDeviceId); +linear_ids!( + BackendConnectorStateSerials, + BackendConnectorStateSerial, + u64 +); + +#[derive(Copy, Clone, Eq, PartialEq, Default)] +pub struct ConnectorCaps(pub u32); + +pub const CONCAP_CONNECTOR: ConnectorCaps = ConnectorCaps(1 << 0); +pub const CONCAP_MODE_SETTING: ConnectorCaps = ConnectorCaps(1 << 1); +pub const CONCAP_PHYSICAL_DISPLAY: ConnectorCaps = ConnectorCaps(1 << 2); + +impl ConnectorCaps { + pub fn none() -> Self { + Self(0) + } + + pub fn contains(self, other: Self) -> bool { + self.0 & other.0 == other.0 + } +} + +impl BitOr for ConnectorCaps { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } +} + +impl BitOrAssign for ConnectorCaps { + fn bitor_assign(&mut self, rhs: Self) { + self.0 |= rhs.0; + } +} + +impl Debug for ConnectorCaps { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut any = false; + let mut v = self.0; + for (cap, name) in [ + (CONCAP_CONNECTOR, "CONCAP_CONNECTOR"), + (CONCAP_MODE_SETTING, "CONCAP_MODE_SETTING"), + (CONCAP_PHYSICAL_DISPLAY, "CONCAP_PHYSICAL_DISPLAY"), + ] { + if v & cap.0 == cap.0 { + if any { + write!(f, "|")?; + } + any = true; + write!(f, "{}", name)?; + v &= !cap.0; + } + } + if !any || v != 0 { + if any { + write!(f, "|")?; + } + write!(f, "0x{:x}", v)?; + } + Ok(()) + } +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)] +pub struct Mode { + pub width: i32, + pub height: i32, + pub refresh_rate_millihz: u32, +} + +impl Mode { + pub fn refresh_nsec(&self) -> u64 { + match self.refresh_rate_millihz { + 0 => u64::MAX, + n => 1_000_000_000_000 / (n as u64), + } + } + + pub fn size(&self) -> (i32, i32) { + (self.width, self.height) + } +} + +impl Display for Mode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}x{}@{}", + self.width, + self.height, + self.refresh_rate_millihz as f64 / 1000.0, + ) + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Linearize)] +pub enum BackendEotfs { + #[default] + Default, + Pq, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Linearize)] +pub enum BackendColorSpace { + #[default] + Default, + Bt2020, +} + +#[derive(Copy, Clone, Debug)] +pub struct BackendLuminance { + pub min: f64, + pub max: f64, + pub max_fall: f64, +} + +impl BackendEotfs { + pub fn to_drm(self) -> u8 { + match self { + BackendEotfs::Default => 0, + BackendEotfs::Pq => 2, + } + } + + pub const fn name(self) -> &'static str { + match self { + BackendEotfs::Default => "default", + BackendEotfs::Pq => "pq", + } + } +} + +impl BackendColorSpace { + pub fn to_drm(self) -> u64 { + match self { + BackendColorSpace::Default => 0, + BackendColorSpace::Bt2020 => 9, + } + } + + pub const fn name(self) -> &'static str { + match self { + BackendColorSpace::Default => "default", + BackendColorSpace::Bt2020 => "bt2020", + } + } +} + +// kernel: struct drm_color_lut +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)] +#[repr(C)] +pub struct BackendGammaLutElement { + pub red: u16, + pub green: u16, + pub blue: u16, + pub reserved: u16, +} + +unsafe impl Pod for BackendGammaLutElement {} +unsafe impl Packed for BackendGammaLutElement {} + +#[derive(Debug, Eq)] +pub struct BackendGammaLut { + id: [u8; 32], + pub gamma_lut: Vec, +} + +impl BackendGammaLut { + pub fn new(mut gamma_lut: Vec) -> Self { + for element in &mut gamma_lut { + element.reserved = 0; + } + let gamma_lut_bytes = uapi::as_bytes(&gamma_lut as &[_]); + let id = *blake3::hash(gamma_lut_bytes).as_bytes(); + Self { id, gamma_lut } + } +} + +impl PartialEq for BackendGammaLut { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BackendConnectorState { + pub serial: BackendConnectorStateSerial, + pub enabled: bool, + pub active: bool, + pub mode: Mode, + pub non_desktop_override: Option, + pub vrr: bool, + pub tearing: bool, + pub format: &'static Format, + pub color_space: BackendColorSpace, + pub eotf: BackendEotfs, + pub gamma_lut: Option>, +} + +#[derive(Clone, Debug)] +pub struct MonitorInfo { + pub modes: Option>, + pub output_id: Rc, + pub width_mm: i32, + pub height_mm: i32, + pub non_desktop: bool, + pub non_desktop_effective: bool, + pub vrr_capable: bool, + pub eotfs: Vec, + pub color_spaces: Vec, + pub primaries: Primaries, + pub luminance: Option, + pub state: BackendConnectorState, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct OutputIdHash(pub [u8; 32]); + +impl OutputIdHash { + pub fn hash(t: impl AsRef<[u8]>) -> Self { + Self(*blake3::hash(t.as_ref()).as_bytes()) + } +} + +#[derive(Eq, Debug)] +pub struct OutputId { + pub _connector: Option, + pub manufacturer: String, + pub model: String, + pub serial_number: String, + pub hash: OutputIdHash, +} + +impl PartialEq for OutputId { + fn eq(&self, other: &Self) -> bool { + self.hash == other.hash + } +} + +impl Hash for OutputId { + fn hash(&self, state: &mut H) { + self.hash.hash(state); + } +} + +impl OutputId { + pub fn new( + connector: impl Into, + manufacturer: impl Into, + model: impl Into, + serial_number: impl Into, + ) -> Rc { + let connector = connector.into(); + let manufacturer = manufacturer.into(); + let model = model.into(); + let serial_number = serial_number.into(); + Self::new_(connector, manufacturer, model, serial_number) + } + + fn new_( + connector: String, + manufacturer: String, + model: String, + serial_number: String, + ) -> Rc { + let connector = serial_number.is_empty().then_some(connector); + let mut hasher = blake3::Hasher::new(); + hasher.update(&[connector.is_some() as u8]); + let mut hash = |s: &str| { + hasher.update(&(s.len() as u64).to_le_bytes()); + hasher.update(s.as_bytes()); + }; + connector.as_deref().map(&mut hash); + hash(&manufacturer); + hash(&model); + hash(&serial_number); + Rc::new(Self { + _connector: connector, + manufacturer, + model, + serial_number, + hash: OutputIdHash(*hasher.finalize().as_bytes()), + }) + } +} diff --git a/crates/pango/Cargo.toml b/crates/pango/Cargo.toml new file mode 100644 index 00000000..f06b5e08 --- /dev/null +++ b/crates/pango/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "jay-pango" +version.workspace = true +edition.workspace = true +license.workspace = true +build = "build.rs" + +[dependencies] +jay-geometry = { path = "../geometry" } + +thiserror = "2.0.11" +uapi = "0.2.13" + +[build-dependencies] +anyhow = "1.0.79" +repc = "0.1.1" diff --git a/crates/pango/build.rs b/crates/pango/build.rs new file mode 100644 index 00000000..9eaf108a --- /dev/null +++ b/crates/pango/build.rs @@ -0,0 +1,85 @@ +use { + repc::layout::{Type, TypeVariant}, + std::{ + env, + fs::{File, OpenOptions}, + io::{self, BufWriter, Write}, + path::PathBuf, + }, +}; + +#[allow(unused_macros)] +macro_rules! cenum { + ($name:ident, $uc:ident; $($name2:ident = $val:expr,)*) => { + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + pub struct $name(pub i32); + + impl $name { + pub fn raw(self) -> i32 { + self.0 + } + } + + $( + pub const $name2: $name = $name($val); + )* + + pub const $uc: &[i32] = &[$($val,)*]; + }; +} + +#[path = "src/consts.rs"] +mod consts; + +fn open(s: &str) -> io::Result> { + let mut path = PathBuf::from(env::var("OUT_DIR").unwrap()); + path.push(s); + Ok(BufWriter::new( + OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?, + )) +} + +fn get_target() -> repc::Target { + let rustc_target = env::var("TARGET").unwrap(); + repc::TARGET_MAP + .iter() + .cloned() + .find(|t| t.0 == rustc_target) + .unwrap() + .1 +} + +fn get_enum_ty(variants: Vec) -> anyhow::Result { + let target = get_target(); + let ty = Type { + layout: (), + annotations: vec![], + variant: TypeVariant::Enum(variants), + }; + let ty = repc::compute_layout(target, &ty)?; + assert!(ty.layout.pointer_alignment_bits <= ty.layout.size_bits); + Ok(ty.layout.size_bits) +} + +fn write_ty(f: &mut W, vals: &[i32], ty: &str) -> anyhow::Result<()> { + let variants: Vec<_> = vals.iter().cloned().map(|v| v as i128).collect(); + let size = get_enum_ty(variants)?; + writeln!(f, "#[allow(clippy::allow_attributes, dead_code)]")?; + writeln!(f, "pub type {} = i{};", ty, size)?; + Ok(()) +} + +fn main() -> anyhow::Result<()> { + let mut f = open("pango_tys.rs")?; + write_ty(&mut f, consts::CAIRO_FORMATS, "cairo_format_t")?; + write_ty(&mut f, consts::CAIRO_STATUSES, "cairo_status_t")?; + write_ty(&mut f, consts::CAIRO_OPERATORS, "cairo_operator_t")?; + write_ty(&mut f, consts::PANGO_ELLIPSIZE_MODES, "PangoEllipsizeMode_")?; + + println!("cargo:rerun-if-changed=src/consts.rs"); + Ok(()) +} diff --git a/src/pango/consts.rs b/crates/pango/src/consts.rs similarity index 100% rename from src/pango/consts.rs rename to crates/pango/src/consts.rs diff --git a/crates/pango/src/lib.rs b/crates/pango/src/lib.rs new file mode 100644 index 00000000..c1eba34c --- /dev/null +++ b/crates/pango/src/lib.rs @@ -0,0 +1,438 @@ +#![allow(non_camel_case_types)] + +use { + crate::consts::{CairoFormat, CairoOperator, PangoEllipsizeMode}, + jay_geometry::Rect, + std::{cell::Cell, ptr, rc::Rc}, + thiserror::Error, + uapi::{ + IntoUstr, + c::{self, memset}, + }, +}; + +macro_rules! cenum { + ($name:ident, $uc:ident; $($name2:ident = $val:expr,)*) => { + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + pub struct $name(pub i32); + + impl $name { + #[allow(dead_code)] + pub fn raw(self) -> i32 { + self.0 + } + } + + $( + pub const $name2: $name = $name($val); + )* + + pub const $uc: &[i32] = &[$($val,)*]; + }; +} + +pub mod consts; + +include!(concat!(env!("OUT_DIR"), "/pango_tys.rs")); + +#[repr(transparent)] +struct cairo_surface_t(u8); +#[repr(transparent)] +struct cairo_t(u8); + +#[link(name = "cairo")] +unsafe extern "C" { + fn cairo_image_surface_create( + format: cairo_format_t, + width: c::c_int, + height: c::c_int, + ) -> *mut cairo_surface_t; + fn cairo_image_surface_create_for_data( + data: *mut u8, + format: cairo_format_t, + width: c::c_int, + height: c::c_int, + stride: c::c_int, + ) -> *mut cairo_surface_t; + fn cairo_image_surface_get_height(surface: *mut cairo_surface_t) -> c::c_int; + fn cairo_image_surface_get_stride(surface: *mut cairo_surface_t) -> c::c_int; + fn cairo_image_surface_get_data(surface: *mut cairo_surface_t) -> *mut u8; + + fn cairo_surface_destroy(surface: *mut cairo_surface_t); + fn cairo_surface_status(surface: *mut cairo_surface_t) -> cairo_status_t; + fn cairo_surface_flush(surface: *mut cairo_surface_t); + + fn cairo_create(surface: *mut cairo_surface_t) -> *mut cairo_t; + fn cairo_status(cairo: *mut cairo_t) -> cairo_status_t; + fn cairo_destroy(cairo: *mut cairo_t); + + fn cairo_set_operator(cr: *mut cairo_t, op: cairo_operator_t); + fn cairo_set_source_rgba(cr: *mut cairo_t, red: f64, green: f64, blue: f64, alpha: f64); + fn cairo_move_to(cr: *mut cairo_t, x: f64, y: f64); + + fn cairo_format_stride_for_width(format: cairo_format_t, width: c::c_int) -> c::c_int; +} + +#[repr(transparent)] +struct PangoContext_(u8); + +#[link(name = "pangocairo-1.0")] +unsafe extern "C" { + fn pango_cairo_create_context(cr: *mut cairo_t) -> *mut PangoContext_; + fn pango_cairo_show_layout(cr: *mut cairo_t, layout: *mut PangoLayout_); +} + +#[repr(transparent)] +struct GObject(u8); + +#[link(name = "gobject-2.0")] +unsafe extern "C" { + fn g_object_unref(object: *mut GObject); +} + +#[repr(transparent)] +struct PangoFontDescription_(u8); +#[repr(transparent)] +struct PangoLayout_(u8); + +#[link(name = "pango-1.0")] +unsafe extern "C" { + fn pango_font_description_from_string(str: *const c::c_char) -> *mut PangoFontDescription_; + fn pango_font_description_free(desc: *mut PangoFontDescription_); + fn pango_font_description_get_size(desc: *mut PangoFontDescription_) -> c::c_int; + fn pango_font_description_set_size(desc: *mut PangoFontDescription_, size: c::c_int); + + fn pango_layout_new(context: *mut PangoContext_) -> *mut PangoLayout_; + fn pango_layout_set_width(layout: *mut PangoLayout_, width: c::c_int); + fn pango_layout_set_ellipsize(layout: *mut PangoLayout_, ellipsize: PangoEllipsizeMode_); + fn pango_layout_set_font_description( + layout: *mut PangoLayout_, + desc: *const PangoFontDescription_, + ); + fn pango_layout_set_text(layout: *mut PangoLayout_, text: *const c::c_char, length: c::c_int); + fn pango_layout_set_markup(layout: *mut PangoLayout_, text: *const c::c_char, length: c::c_int); + fn pango_layout_get_pixel_size( + layout: *mut PangoLayout_, + width: *mut c::c_int, + height: *mut c::c_int, + ); + fn pango_layout_get_extents( + layout: *mut PangoLayout_, + ink_rect: *mut PangoRectangle, + logical_rect: *mut PangoRectangle, + ); + fn pango_layout_get_baseline(layout: *mut PangoLayout_) -> c::c_int; + + fn pango_extents_to_pixels(inclusive: *mut PangoRectangle, nearest: *mut PangoRectangle); +} + +#[derive(Debug, Error)] +pub enum PangoError { + #[error("Could not create an image surface: {0}")] + CreateSurface(u32), + #[error("Could not create a cairo context: {0}")] + CreateCairo(u32), + #[error("Could not create a pangocairo context")] + CreatePangoCairo, + #[error("Could not create a pango layout")] + CreateLayout, + #[error("Could not retrieve image data")] + GetData, +} + +#[repr(C)] +#[derive(Default)] +struct PangoRectangle { + x: c::c_int, + y: c::c_int, + width: c::c_int, + height: c::c_int, +} + +const PANGO_SCALE: i32 = 1024; + +pub struct CairoImageSurface { + s: *mut cairo_surface_t, +} + +impl CairoImageSurface { + pub fn new_image_surface( + format: CairoFormat, + width: i32, + height: i32, + ) -> Result, PangoError> { + unsafe { + let s = cairo_image_surface_create(format.raw() as _, width as _, height as _); + let status = cairo_surface_status(s); + if status != 0 { + return Err(PangoError::CreateSurface(status as _)); + } + Ok(Rc::new(Self { s })) + } + } + + pub unsafe fn new_image_surface_with_data( + format: CairoFormat, + data: *mut u8, + width: i32, + height: i32, + stride: i32, + ) -> Result, PangoError> { + unsafe { + memset(data.cast(), 0, (stride * height) as usize); + let s = cairo_image_surface_create_for_data( + data, + format.raw() as _, + width as _, + height as _, + stride as _, + ); + let status = cairo_surface_status(s); + if status != 0 { + return Err(PangoError::CreateSurface(status as _)); + } + Ok(Rc::new(Self { s })) + } + } + + pub fn create_context(self: &Rc) -> Result, PangoError> { + unsafe { + let c = cairo_create(self.s); + let status = cairo_status(c); + if status != 0 { + return Err(PangoError::CreateCairo(status as _)); + } + Ok(Rc::new(CairoContext { + _s: self.clone(), + c, + })) + } + } + + pub fn flush(&self) { + unsafe { + cairo_surface_flush(self.s); + } + } + + pub fn height(&self) -> i32 { + unsafe { cairo_image_surface_get_height(self.s) as _ } + } + + pub fn stride(&self) -> i32 { + unsafe { cairo_image_surface_get_stride(self.s) as _ } + } + + pub fn data(&self) -> Result<&[Cell], PangoError> { + unsafe { + let d = cairo_image_surface_get_data(self.s); + if d.is_null() { + return Err(PangoError::GetData); + } + let size = self.height() as usize * self.stride() as usize; + Ok(std::slice::from_raw_parts(d.cast(), size)) + } + } +} + +impl Drop for CairoImageSurface { + fn drop(&mut self) { + unsafe { + cairo_surface_destroy(self.s); + } + } +} + +pub struct CairoContext { + _s: Rc, + c: *mut cairo_t, +} + +impl CairoContext { + pub fn create_pango_context(self: &Rc) -> Result, PangoError> { + unsafe { + let p = pango_cairo_create_context(self.c); + if p.is_null() { + return Err(PangoError::CreatePangoCairo); + } + Ok(Rc::new(PangoCairoContext { c: self.clone(), p })) + } + } + + pub fn set_operator(&self, op: CairoOperator) { + unsafe { + cairo_set_operator(self.c, op.raw() as _); + } + } + + pub fn set_source_rgba(&self, r: f64, g: f64, b: f64, a: f64) { + unsafe { + cairo_set_source_rgba(self.c, r, g, b, a); + } + } + + pub fn move_to(&self, x: f64, y: f64) { + unsafe { + cairo_move_to(self.c, x, y); + } + } +} + +impl Drop for CairoContext { + fn drop(&mut self) { + unsafe { + cairo_destroy(self.c); + } + } +} + +pub struct PangoCairoContext { + c: Rc, + p: *mut PangoContext_, +} + +impl PangoCairoContext { + pub fn create_layout(self: &Rc) -> Result { + unsafe { + let l = pango_layout_new(self.p as _); + if l.is_null() { + return Err(PangoError::CreateLayout); + } + Ok(PangoLayout { c: self.clone(), l }) + } + } +} + +impl Drop for PangoCairoContext { + fn drop(&mut self) { + unsafe { + g_object_unref(self.p as _); + } + } +} + +pub struct PangoFontDescription { + s: *mut PangoFontDescription_, +} + +impl PangoFontDescription { + pub fn from_string<'a>(s: impl IntoUstr<'a>) -> Self { + let s = s.into_ustr(); + Self { + s: unsafe { pango_font_description_from_string(s.as_ptr()) }, + } + } + + pub fn size(&self) -> i32 { + unsafe { pango_font_description_get_size(self.s) as _ } + } + + pub fn set_size(&mut self, size: i32) { + unsafe { + pango_font_description_set_size(self.s, size); + } + } +} + +impl Drop for PangoFontDescription { + fn drop(&mut self) { + unsafe { + pango_font_description_free(self.s); + } + } +} + +pub struct PangoLayout { + c: Rc, + l: *mut PangoLayout_, +} + +impl PangoLayout { + pub fn set_width(&self, width: i32) { + unsafe { + pango_layout_set_width(self.l, width as _); + } + } + + pub fn set_ellipsize(&self, ellipsize: PangoEllipsizeMode) { + unsafe { + pango_layout_set_ellipsize(self.l, ellipsize.raw() as _); + } + } + + pub fn set_font_description(&self, fd: &PangoFontDescription) { + unsafe { + pango_layout_set_font_description(self.l, fd.s); + } + } + + pub fn set_text(&self, text: &str) { + unsafe { + pango_layout_set_text(self.l, text.as_ptr() as _, text.len() as _); + } + } + + pub fn set_markup(&self, text: &str) { + unsafe { + pango_layout_set_markup(self.l, text.as_ptr() as _, text.len() as _); + } + } + + pub fn pixel_size(&self) -> (i32, i32) { + unsafe { + let mut w = 0; + let mut h = 0; + pango_layout_get_pixel_size(self.l, &mut w, &mut h); + (w as _, h as _) + } + } + + pub fn inc_pixel_rect(&self) -> Rect { + unsafe { + let mut rect = PangoRectangle::default(); + pango_layout_get_extents(self.l, &mut rect, ptr::null_mut()); + pango_extents_to_pixels(&mut rect, ptr::null_mut()); + Rect::new_sized_saturating(rect.x, rect.y, rect.width, rect.height) + } + } + + pub fn logical_pixel_rect(&self) -> Rect { + unsafe { + let mut rect = PangoRectangle::default(); + pango_layout_get_extents(self.l, ptr::null_mut(), &mut rect); + pango_extents_to_pixels(&mut rect, ptr::null_mut()); + Rect::new_sized_saturating(rect.x, rect.y, rect.width, rect.height) + } + } + + pub fn pixel_baseline(&self) -> i32 { + unsafe { + let res = pango_layout_get_baseline(self.l); + (res as i32 + PANGO_SCALE - 1) / PANGO_SCALE + } + } + + pub fn show_layout(&self) { + unsafe { + pango_cairo_show_layout(self.c.c.c, self.l); + } + } +} + +impl Drop for PangoLayout { + fn drop(&mut self) { + unsafe { + g_object_unref(self.l as _); + } + } +} + +pub fn cairo_size(format: CairoFormat, width: i32, height: i32) -> Option<(i32, usize)> { + let stride = unsafe { cairo_format_stride_for_width(format.raw() as _, width as _) }; + if stride < 0 { + return None; + } + let stride = stride as i32; + let size = height.checked_mul(stride)? as usize; + Some((stride, size)) +} diff --git a/crates/pr-caps/Cargo.toml b/crates/pr-caps/Cargo.toml new file mode 100644 index 00000000..d58f93cd --- /dev/null +++ b/crates/pr-caps/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "jay-pr-caps" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +jay-utils = { path = "../utils" } + +opera = "1.0.1" +parking_lot = "0.12.1" +uapi = "0.2.13" diff --git a/crates/pr-caps/src/lib.rs b/crates/pr-caps/src/lib.rs new file mode 100644 index 00000000..256167b1 --- /dev/null +++ b/crates/pr-caps/src/lib.rs @@ -0,0 +1,264 @@ +use { + crate::sys::{ + _LINUX_CAPABILITY_U32S_3, _LINUX_CAPABILITY_VERSION_3, CAP_SYS_NICE, cap_user_data_t, + cap_user_header_t, + }, + jay_utils::{bitflags::BitflagsExt, errorfmt::ErrorFmt, oserror::OsErrorExt}, + opera::PhantomNotSend, + parking_lot::{Condvar, Mutex}, + std::{ + mem, + sync::Arc, + thread::{self, JoinHandle}, + }, + uapi::{ + c::{SYS_capget, SYS_capset, syscall}, + map_err, + }, +}; + +pub struct PrCaps { + effective: u64, + permitted: u64, + inheritable: u64, +} + +pub struct PrCompCaps { + caps: PrCaps, +} + +pub struct PrCapsThread { + thread: Option>, + data: Arc, + _no_send: PhantomNotSend, +} + +#[derive(Default)] +struct ThreadData { + cond: Condvar, + mutex: Mutex, +} + +#[derive(Default)] +struct MutData { + exit: bool, + fun: Option>, +} + +pub fn pr_caps() -> PrCaps { + let mut hdr = cap_user_header_t { + version: _LINUX_CAPABILITY_VERSION_3, + pid: 0, + }; + let mut caps = [cap_user_data_t::default(); _LINUX_CAPABILITY_U32S_3]; + let ret = unsafe { syscall(SYS_capget, &mut hdr, &mut caps) }; + if let Err(e) = map_err!(ret).to_os_error() { + eprintln!("Could not get process capabilities: {}", ErrorFmt(e)); + return PrCaps { + effective: 0, + permitted: 0, + inheritable: 0, + }; + } + PrCaps { + effective: caps[0].effective as u64 | ((caps[1].effective as u64) << 32), + permitted: caps[0].permitted as u64 | ((caps[1].permitted as u64) << 32), + inheritable: caps[0].inheritable as u64 | ((caps[1].inheritable as u64) << 32), + } +} + +pub fn drop_all_pr_caps() { + let mut hdr = cap_user_header_t { + version: _LINUX_CAPABILITY_VERSION_3, + pid: 0, + }; + let caps = [cap_user_data_t::default(); _LINUX_CAPABILITY_U32S_3]; + let ret = unsafe { syscall(SYS_capset, &mut hdr, &caps) }; + if let Err(e) = map_err!(ret).to_os_error() { + eprintln!("Could not get drop capabilities: {}", ErrorFmt(e)); + } +} + +impl PrCaps { + pub fn into_comp(mut self) -> PrCompCaps { + let mut caps = 0; + macro_rules! add_cap { + ($name:ident) => { + if self.permitted.contains(1 << $name) { + caps |= 1 << $name; + } + }; + } + add_cap!(CAP_SYS_NICE); + let mut hdr = cap_user_header_t { + version: _LINUX_CAPABILITY_VERSION_3, + pid: 0, + }; + let caps_hi = (caps >> 32) as u32; + let caps_lo = caps as u32; + let mut data = [cap_user_data_t::default(); _LINUX_CAPABILITY_U32S_3]; + data[0].effective = caps_lo; + data[1].effective = caps_hi; + data[0].permitted = caps_lo; + data[1].permitted = caps_hi; + let ret = unsafe { syscall(SYS_capset, &mut hdr, &data) }; + if let Err(e) = map_err!(ret).to_os_error() { + eprintln!("Could not get set compositor capabilities: {}", ErrorFmt(e)); + return PrCompCaps { caps: self }; + } + self.effective = caps; + self.permitted = caps; + self.inheritable = 0; + PrCompCaps { caps: self } + } +} + +impl PrCompCaps { + pub fn has_nice(&self) -> bool { + self.caps.effective.contains(1 << CAP_SYS_NICE) + } + + pub fn into_thread(self) -> PrCapsThread { + let data = Arc::new(ThreadData::default()); + let data2 = data.clone(); + let jh = thread::Builder::new() + .name("SYS_nice thread".to_string()) + .spawn(move || { + let data2 = data2; + let mut lock = data2.mutex.lock(); + loop { + if lock.exit { + return; + } + if let Some(f) = lock.fun.take() { + f(); + } + data2.cond.wait(&mut lock); + } + }) + .expect("Could not spawn SYS_nice thread"); + PrCapsThread { + thread: Some(jh), + data, + _no_send: Default::default(), + } + } +} + +impl PrCapsThread { + pub unsafe fn run(&self, f: F) -> T + where + F: FnOnce() -> T, + { + struct AssertSend(T); + unsafe impl Send for AssertSend {} + struct Data { + cond: Condvar, + mutex: Mutex>>, + } + let data = Arc::new(Data { + cond: Default::default(), + mutex: Default::default(), + }); + let data2 = data.clone(); + let f = AssertSend(f); + let fun = Box::new(move || { + let f = f; + let t = f.0(); + *data2.mutex.lock() = Some(AssertSend(t)); + data2.cond.notify_all(); + }); + let fun = unsafe { + mem::transmute::, Box>(fun) + }; + self.data.mutex.lock().fun = Some(fun); + self.data.cond.notify_all(); + let mut lock = data.mutex.lock(); + loop { + if let Some(t) = lock.take() { + return t.0; + } + data.cond.wait(&mut lock); + } + } +} + +impl Drop for PrCaps { + fn drop(&mut self) { + drop_all_pr_caps(); + } +} + +impl Drop for PrCapsThread { + fn drop(&mut self) { + self.data.mutex.lock().exit = true; + self.data.cond.notify_all(); + let _ = self.thread.take().unwrap().join(); + } +} + +mod sys { + #![allow(dead_code)] + + use uapi::c::pid_t; + + pub const _LINUX_CAPABILITY_VERSION_3: u32 = 0x20080522; + pub const _LINUX_CAPABILITY_U32S_3: usize = 2; + + #[repr(C)] + #[derive(Copy, Clone, Debug)] + pub struct cap_user_header_t { + pub version: u32, + pub pid: pid_t, + } + + #[repr(C)] + #[derive(Copy, Clone, Debug, Default)] + pub struct cap_user_data_t { + pub effective: u32, + pub permitted: u32, + pub inheritable: u32, + } + + pub const CAP_CHOWN: u32 = 0; + pub const CAP_DAC_OVERRIDE: u32 = 1; + pub const CAP_DAC_READ_SEARCH: u32 = 2; + pub const CAP_FOWNER: u32 = 3; + pub const CAP_FSETID: u32 = 4; + pub const CAP_KILL: u32 = 5; + pub const CAP_SETGID: u32 = 6; + pub const CAP_SETUID: u32 = 7; + pub const CAP_SETPCAP: u32 = 8; + pub const CAP_LINUX_IMMUTABLE: u32 = 9; + pub const CAP_NET_BIND_SERVICE: u32 = 10; + pub const CAP_NET_BROADCAST: u32 = 11; + pub const CAP_NET_ADMIN: u32 = 12; + pub const CAP_NET_RAW: u32 = 13; + pub const CAP_IPC_LOCK: u32 = 14; + pub const CAP_IPC_OWNER: u32 = 15; + pub const CAP_SYS_MODULE: u32 = 16; + pub const CAP_SYS_RAWIO: u32 = 17; + pub const CAP_SYS_CHROOT: u32 = 18; + pub const CAP_SYS_PTRACE: u32 = 19; + pub const CAP_SYS_PACCT: u32 = 20; + pub const CAP_SYS_ADMIN: u32 = 21; + pub const CAP_SYS_BOOT: u32 = 22; + pub const CAP_SYS_NICE: u32 = 23; + pub const CAP_SYS_RESOURCE: u32 = 24; + pub const CAP_SYS_TIME: u32 = 25; + pub const CAP_SYS_TTY_CONFIG: u32 = 26; + pub const CAP_MKNOD: u32 = 27; + pub const CAP_LEASE: u32 = 28; + pub const CAP_AUDIT_WRITE: u32 = 29; + pub const CAP_AUDIT_CONTROL: u32 = 30; + pub const CAP_SETFCAP: u32 = 31; + pub const CAP_MAC_OVERRIDE: u32 = 32; + pub const CAP_MAC_ADMIN: u32 = 33; + pub const CAP_SYSLOG: u32 = 34; + pub const CAP_WAKE_ALARM: u32 = 35; + pub const CAP_BLOCK_SUSPEND: u32 = 36; + pub const CAP_AUDIT_READ: u32 = 37; + pub const CAP_PERFMON: u32 = 38; + pub const CAP_BPF: u32 = 39; + pub const CAP_CHECKPOINT_RESTORE: u32 = 40; +} diff --git a/crates/sighand/Cargo.toml b/crates/sighand/Cargo.toml new file mode 100644 index 00000000..16140807 --- /dev/null +++ b/crates/sighand/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "jay-sighand" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +jay-async-engine = { path = "../async-engine" } +jay-io-uring = { path = "../io-uring" } +jay-utils = { path = "../utils" } + +log = { version = "0.4.20", features = ["std"] } +thiserror = "2.0.11" +uapi = "0.2.13" diff --git a/crates/sighand/src/lib.rs b/crates/sighand/src/lib.rs new file mode 100644 index 00000000..f58d46df --- /dev/null +++ b/crates/sighand/src/lib.rs @@ -0,0 +1,63 @@ +use { + jay_async_engine::{AsyncEngine, SpawnedFuture}, + jay_io_uring::IoUring, + jay_utils::{ + buf::TypedBuf, + errorfmt::ErrorFmt, + oserror::{OsError, OsErrorExt2}, + }, + std::rc::Rc, + thiserror::Error, + uapi::{OwnedFd, c}, +}; + +#[derive(Debug, Error)] +pub enum SighandError { + #[error("Could not block the signalfd signals")] + BlockFailed(#[source] OsError), + #[error("Could not create a signalfd")] + CreateFailed(#[source] OsError), +} + +pub fn install( + eng: &Rc, + ring: &Rc, +) -> Result, SighandError> { + let mut set: c::sigset_t = uapi::pod_zeroed(); + uapi::sigaddset(&mut set, c::SIGINT).unwrap(); + uapi::sigaddset(&mut set, c::SIGTERM).unwrap(); + uapi::sigaddset(&mut set, c::SIGPIPE).unwrap(); + uapi::pthread_sigmask(c::SIG_BLOCK, Some(&set), None).map_os_err(SighandError::BlockFailed)?; + let fd = uapi::signalfd_new(&set, c::SFD_CLOEXEC) + .map(Rc::new) + .map_os_err(SighandError::CreateFailed)?; + Ok(eng.spawn("signal handler", handle_signals(fd, ring.clone()))) +} + +async fn handle_signals(fd: Rc, ring: Rc) { + let mut buf = TypedBuf::::new(); + loop { + if let Err(e) = ring.read(&fd, buf.buf()).await { + log::error!("Could not read from signal fd: {}", ErrorFmt(e)); + return; + } + let sig = buf.t().ssi_signo as i32; + log::info!("Received signal {}", sig); + if matches!(sig, c::SIGINT | c::SIGTERM) { + log::info!("Exiting"); + ring.stop(); + } + } +} + +pub fn reset_all() { + const NSIG: c::c_int = 64; + unsafe { + for sig in 1..=NSIG { + c::signal(sig, c::SIG_DFL); + } + } + let mut set: c::sigset_t = uapi::pod_zeroed(); + uapi::sigfillset(&mut set).unwrap(); + let _ = uapi::pthread_sigmask(c::SIG_UNBLOCK, Some(&set), None); +} diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml new file mode 100644 index 00000000..2ca9d23c --- /dev/null +++ b/crates/theme/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "jay-theme" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +jay-cmm = { path = "../cmm" } +jay-config = { path = "../jay-config" } +jay-gfx-types = { path = "../gfx-types" } +jay-utils = { path = "../utils" } + +linearize = { version = "0.1.3", features = ["derive"] } +num-traits = "0.2.17" diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs new file mode 100644 index 00000000..94fde7b4 --- /dev/null +++ b/crates/theme/src/lib.rs @@ -0,0 +1,916 @@ +#![expect(clippy::excessive_precision)] + +use { + jay_cmm::cmm_eotf::{Eotf, bt1886_eotf_args, bt1886_inv_eotf_args}, + jay_config::theme::BarPosition as ConfigBarPosition, + jay_gfx_types::AlphaMode, + jay_utils::{clonecell::CloneCell, static_text::StaticText}, + linearize::Linearize, + num_traits::Float, + std::{ + cell::Cell, + cmp::Ordering, + ops::{Add, Div, Mul}, + sync::Arc, + }, +}; + +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct Color { + r: f32, + g: f32, + b: f32, + a: f32, +} + +impl Eq for Color {} + +impl Ord for Color { + fn cmp(&self, other: &Self) -> Ordering { + self.r + .total_cmp(&other.r) + .then_with(|| self.g.total_cmp(&other.g)) + .then_with(|| self.b.total_cmp(&other.b)) + .then_with(|| self.a.total_cmp(&other.a)) + } +} + +impl Mul for Color { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + Self { + r: self.r * rhs, + g: self.g * rhs, + b: self.b * rhs, + a: self.a * rhs, + } + } +} + +impl PartialOrd for Color { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +fn to_f32(c: u8) -> f32 { + c as f32 / 255f32 +} + +fn to_u8(c: f32) -> u8 { + (c * 255f32).round() as u8 +} + +impl Color { + pub const TRANSPARENT: Self = Self { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }; + + pub const SOLID_BLACK: Self = Self { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0, + }; + + pub fn new( + eotf: Eotf, + alpha_mode: AlphaMode, + mut r: f32, + mut g: f32, + mut b: f32, + a: f32, + ) -> Self { + if eotf == Eotf::Linear { + if alpha_mode == AlphaMode::Straight && a < 1.0 { + for c in [&mut r, &mut g, &mut b] { + *c *= a; + } + } + return Self { r, g, b, a }; + } + if alpha_mode == AlphaMode::PremultipliedElectrical && a < 1.0 && a > 0.0 { + for c in [&mut r, &mut g, &mut b] { + *c /= a; + } + } + #[inline(always)] + fn linear(c: f32) -> f32 { + c + } + fn st2084_pq(c: f32) -> f32 { + let cp = c.powf(1.0 / 78.84375); + let num = (cp - 0.8359375).max(0.0); + let den = 18.8515625 - 18.6875 * cp; + (num / den).powf(1.0 / 0.1593017578125) + } + fn st240(c: f32) -> f32 { + if c < 0.0913 { + c / 4.0 + } else { + ((c + 0.1115) / 1.1115).powf(1.0 / 0.45) + } + } + fn log100(c: f32) -> f32 { + 10.0.powf(2.0 * (c - 1.0)) + } + fn log316(c: f32) -> f32 { + 10.0.powf(2.5 * (c - 1.0)) + } + fn st428(c: f32) -> f32 { + c.powf(2.6) * 52.37 / 48.0 + } + fn gamma22(c: f32) -> f32 { + c.signum() * c.abs().powf(2.2) + } + fn gamma24(c: f32) -> f32 { + c.signum() * c.abs().powf(2.4) + } + fn gamma28(c: f32) -> f32 { + c.signum() * c.abs().powf(2.8) + } + fn compound_power_2_4(c: f32) -> f32 { + if c < 0.04045 { + c / 12.92 + } else { + ((c + 0.055) / 1.055).powf(2.4) + } + } + macro_rules! convert { + ($tf:ident) => {{ + r = $tf(r); + g = $tf(g); + b = $tf(b); + }}; + } + match eotf { + Eotf::Linear => convert!(linear), + Eotf::St2084Pq => convert!(st2084_pq), + Eotf::Bt1886(c) => { + let [a1, a2, a3, a4] = bt1886_eotf_args(c); + let bt1886 = |c: f32| -> f32 { a1 * ((a2 * c + a3).powf(2.4) - a4) }; + convert!(bt1886) + } + Eotf::Gamma22 => convert!(gamma22), + Eotf::Gamma24 => convert!(gamma24), + Eotf::Gamma28 => convert!(gamma28), + Eotf::St240 => convert!(st240), + Eotf::Log100 => convert!(log100), + Eotf::Log316 => convert!(log316), + Eotf::St428 => convert!(st428), + Eotf::Pow(n) => { + let e = n.eotf_f32(); + let pow = |c: f32| -> f32 { c.signum() * c.abs().powf(e) }; + convert!(pow) + } + Eotf::CompoundPower24 => convert!(compound_power_2_4), + } + if alpha_mode != AlphaMode::PremultipliedOptical && a < 1.0 { + for c in [&mut r, &mut g, &mut b] { + *c *= a; + } + } + Self { r, g, b, a } + } + + pub fn is_opaque(&self) -> bool { + self.a >= 1.0 + } + + pub fn from_gray_srgb(g: u8) -> Self { + Self::from_srgb(g, g, g) + } + + pub fn from_srgb(r: u8, g: u8, b: u8) -> Self { + Self::new( + Eotf::Gamma22, + AlphaMode::PremultipliedOptical, + to_f32(r), + to_f32(g), + to_f32(b), + 1.0, + ) + } + + pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { + Self::new( + Eotf::Gamma22, + AlphaMode::PremultipliedElectrical, + to_f32(r), + to_f32(g), + to_f32(b), + to_f32(a), + ) + } + + pub fn from_u32(eotf: Eotf, alpha_mode: AlphaMode, r: u32, g: u32, b: u32, a: u32) -> Self { + fn to_f32(c: u32) -> f32 { + ((c as f64) / (u32::MAX as f64)) as f32 + } + Self::new(eotf, alpha_mode, to_f32(r), to_f32(g), to_f32(b), to_f32(a)) + } + + pub fn from_srgba_straight(r: u8, g: u8, b: u8, a: u8) -> Self { + Self::new( + Eotf::Gamma22, + AlphaMode::Straight, + to_f32(r), + to_f32(g), + to_f32(b), + to_f32(a), + ) + } + + pub fn to_srgba_premultiplied(self) -> [u8; 4] { + let [r, g, b, a] = self.to_array(Eotf::Gamma22); + [to_u8(r), to_u8(g), to_u8(b), to_u8(a)] + } + + pub fn to_array(self, eotf: Eotf) -> [f32; 4] { + self.to_array2(eotf, None) + } + + pub fn to_array2(self, eotf: Eotf, alpha: Option) -> [f32; 4] { + let mut res = [self.r, self.g, self.b, self.a]; + fn linear(c: f32) -> f32 { + c + } + fn st2084_pq(c: f32) -> f32 { + let c = c.clamp(0.0, 1.0); + let num = 0.8359375 + 18.8515625 * c.powf(0.1593017578125); + let den = 1.0 + 18.6875 * c.powf(0.1593017578125); + (num / den).powf(78.84375) + } + fn st240(c: f32) -> f32 { + if c < 0.0228 { + 4.0 * c + } else { + 1.1115 * c.powf(0.45) - 0.1115 + } + } + fn log100(c: f32) -> f32 { + let c = c.clamp(0.0, 1.0); + if c < 0.01 { 0.0 } else { 1.0 + c.log10() / 2.0 } + } + fn log316(c: f32) -> f32 { + let c = c.clamp(0.0, 1.0); + if c < 10.0.sqrt() / 1000.0 { + 0.0 + } else { + 1.0 + c.log10() / 2.5 + } + } + fn st428(c: f32) -> f32 { + (48.0 * c / 52.37).powf(1.0 / 2.6) + } + fn gamma22(c: f32) -> f32 { + c.signum() * c.abs().powf(1.0 / 2.2) + } + fn gamma24(c: f32) -> f32 { + c.signum() * c.abs().powf(1.0 / 2.4) + } + fn gamma28(c: f32) -> f32 { + c.signum() * c.abs().powf(1.0 / 2.8) + } + fn compound_power_2_4(c: f32) -> f32 { + if c < 0.0031308 { + 12.92 * c + } else { + 1.055 * c.powf(1.0 / 2.4) - 0.055 + } + } + macro_rules! convert { + ($tf:ident) => {{ + for c in &mut res[..3] { + *c = $tf(*c); + } + }}; + } + if eotf != Eotf::Linear { + if self.a < 1.0 && self.a > 0.0 { + for c in &mut res[..3] { + *c /= self.a; + } + } + match eotf { + Eotf::Linear => convert!(linear), + Eotf::St2084Pq => convert!(st2084_pq), + Eotf::Bt1886(c) => { + let [a1, a2, a3, a4] = bt1886_inv_eotf_args(c); + let bt1886 = |c: f32| -> f32 { a1 * ((a2 * c + a3).powf(1.0 / 2.4) - a4) }; + convert!(bt1886) + } + Eotf::Gamma22 => convert!(gamma22), + Eotf::Gamma24 => convert!(gamma24), + Eotf::Gamma28 => convert!(gamma28), + Eotf::St240 => convert!(st240), + Eotf::Log100 => convert!(log100), + Eotf::Log316 => convert!(log316), + Eotf::St428 => convert!(st428), + Eotf::Pow(n) => { + let e = n.inv_eotf_f32(); + let pow = |c: f32| -> f32 { c.signum() * c.abs().powf(e) }; + convert!(pow) + } + Eotf::CompoundPower24 => convert!(compound_power_2_4), + } + if self.a < 1.0 { + for c in &mut res[..3] { + *c *= self.a; + } + } + } + if let Some(a) = alpha { + for c in &mut res { + *c *= a; + } + } + res + } + + pub fn and_then(self, other: &Color) -> Color { + Color { + r: self.r * (1.0 - other.a) + other.r, + g: self.g * (1.0 - other.a) + other.g, + b: self.b * (1.0 - other.a) + other.b, + a: self.a * (1.0 - other.a) + other.a, + } + } + + pub fn srgb_to_oklab(self) -> Oklab { + if self.a == 0.0 { + return Oklab { + l: 0.0, + a: 0.0, + b: 0.0, + }; + } + + let [r, g, b, _] = self.to_array2(Eotf::Linear, Some(1.0 / self.a)); + + let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b; + let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b; + let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b; + + let l_ = l.cbrt(); + let m_ = m.cbrt(); + let s_ = s.cbrt(); + + let l = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_; + let a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_; + let b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_; + + Oklab { l, a, b } + } +} + +impl From for Color { + fn from(f: jay_config::theme::Color) -> Self { + let [r, g, b, a] = f.to_f32_premultiplied(); + Self::new( + Eotf::Gamma22, + AlphaMode::PremultipliedElectrical, + r, + g, + b, + a, + ) + } +} + +macro_rules! colors { + ($($name:ident = $colors:tt,)*) => { + pub struct ThemeColors { + $( + pub $name: Cell, + )* + } + + #[derive(Copy, Clone, Debug, Linearize)] + #[expect(non_camel_case_types)] + pub enum ThemeColor { + $( + $name, + )* + } + + impl ThemeColor { + pub fn field(self, theme: &Theme) -> &Cell { + let colors = &theme.colors; + match self { + $( + Self::$name => &colors.$name, + )* + } + } + } + + impl ThemeColors { + pub fn reset(&self) { + let default = Self::default(); + $( + self.$name.set(default.$name.get()); + )* + } + } + + impl Default for ThemeColors { + fn default() -> Self { + Self { + $( + $name: Cell::new(colors!(@colors $colors)), + )* + } + } + } + }; + (@colors ($r:expr, $g:expr, $b:expr)) => { + Color::from_srgb($r, $g, $b) + }; + (@colors ($r:expr, $g:expr, $b:expr, $a:expr)) => { + Color::from_srgba_straight($r, $g, $b, $a) + }; +} + +colors! { + background = (0x00, 0x10, 0x19), + unfocused_title_background = (0x22, 0x22, 0x22), + focused_title_background = (0x28, 0x55, 0x77), + captured_unfocused_title_background = (0x22, 0x03, 0x03), + captured_focused_title_background = (0x77, 0x28, 0x31), + focused_inactive_title_background = (0x5f, 0x67, 0x6a), + unfocused_title_text = (0x88, 0x88, 0x88), + focused_title_text = (0xff, 0xff, 0xff), + focused_inactive_title_text = (0xff, 0xff, 0xff), + separator = (0x33, 0x33, 0x33), + border = (0x3f, 0x47, 0x4a), + active_border = (0x28, 0x55, 0x77), + bar_background = (0x00, 0x00, 0x00), + bar_text = (0xff, 0xff, 0xff), + attention_requested_background = (0x23, 0x09, 0x2c), + highlight = (0x9d, 0x28, 0xc6, 0x7f), + tab_active_background = (0x4c, 0x78, 0x99), + tab_active_border = (0x28, 0x55, 0x77), + tab_inactive_background = (0x22, 0x22, 0x22), + tab_inactive_border = (0x33, 0x33, 0x33), + tab_active_text = (0xff, 0xff, 0xff), + tab_inactive_text = (0x88, 0x88, 0x88), + tab_bar_background = (0x00, 0x00, 0x00, 0x00), + tab_attention_background = (0x23, 0x09, 0x2c), +} + +impl StaticText for ThemeColor { + fn text(&self) -> &'static str { + match self { + ThemeColor::background => "Background", + ThemeColor::unfocused_title_background => "Title Background (unfocused)", + ThemeColor::focused_title_background => "Title Background (focused)", + ThemeColor::captured_unfocused_title_background => { + "Title Background (unfocused, captured)" + } + ThemeColor::captured_focused_title_background => "Title Background (focused, captured)", + ThemeColor::focused_inactive_title_background => "Title Background (focused, inactive)", + ThemeColor::unfocused_title_text => "Title Text (unfocused)", + ThemeColor::focused_title_text => "Title Text (focused)", + ThemeColor::focused_inactive_title_text => "Title Text (focused, inactive)", + ThemeColor::separator => "Separator", + ThemeColor::border => "Border", + ThemeColor::active_border => "Border (active)", + ThemeColor::bar_background => "Bar Background", + ThemeColor::bar_text => "Bar Text", + ThemeColor::attention_requested_background => "Attention Requested", + ThemeColor::highlight => "Highlight", + ThemeColor::tab_active_background => "Tab Background (active)", + ThemeColor::tab_active_border => "Tab Border (active)", + ThemeColor::tab_inactive_background => "Tab Background (inactive)", + ThemeColor::tab_inactive_border => "Tab Border (inactive)", + ThemeColor::tab_active_text => "Tab Text (active)", + ThemeColor::tab_inactive_text => "Tab Text (inactive)", + ThemeColor::tab_bar_background => "Tab Bar Background", + ThemeColor::tab_attention_background => "Tab Attention Background", + } + } +} + +pub struct ThemeSize { + pub val: Cell, + pub set: Cell, +} + +impl ThemeSize { + pub fn get(&self) -> i32 { + self.val.get() + } +} + +macro_rules! sizes { + ($($name:ident = ($min:expr, $max:expr, $def:expr),)*) => { + pub struct ThemeSizes { + $( + pub $name: ThemeSize, + )* + } + + #[derive(Copy, Clone, Debug, Linearize)] + #[expect(non_camel_case_types)] + pub enum ThemeSized { + $( + $name, + )* + } + + impl ThemeSized { + pub fn min(self) -> i32 { + match self { + $( + Self::$name => $min, + )* + } + } + + pub fn max(self) -> i32 { + match self { + $( + Self::$name => $max, + )* + } + } + + pub fn field(self, theme: &Theme) -> &ThemeSize { + let sizes = &theme.sizes; + match self { + $( + Self::$name => &sizes.$name, + )* + } + } + + pub fn name(self) -> &'static str { + match self { + $( + Self::$name => stringify!($name), + )* + } + } + } + + impl ThemeSizes { + pub fn reset(&self) { + let default = Self::default(); + $( + self.$name.val.set(default.$name.val.get()); + self.$name.set.set(false); + )* + } + } + + impl Default for ThemeSizes { + fn default() -> Self { + Self { + $( + $name: ThemeSize { + val: Cell::new($def), + set: Cell::new(false), + }, + )* + } + } + } + } +} + +impl ThemeSizes { + pub fn bar_height(&self) -> i32 { + if self.bar_height.set.get() { + self.bar_height.val.get() + } else { + self.title_height.val.get() + } + } + + pub fn bar_separator_width(&self) -> i32 { + self.bar_separator_width.get() + } +} + +sizes! { + title_height = (0, 1000, 17), + bar_height = (0, 1000, 17), + border_width = (0, 1000, 4), + bar_separator_width = (0, 1000, 1), + gap = (0, 1000, 0), + title_gap = (0, 1000, 5), + tab_bar_height = (0, 1000, 22), + tab_bar_padding = (0, 1000, 6), + tab_bar_radius = (0, 1000, 6), + tab_bar_border_width = (0, 1000, 2), + tab_bar_text_padding = (0, 1000, 4), + tab_bar_gap = (0, 1000, 4), +} + +impl StaticText for ThemeSized { + fn text(&self) -> &'static str { + match self { + ThemeSized::title_height => "Title Height", + ThemeSized::bar_height => "Bar Height", + ThemeSized::border_width => "Border Width", + ThemeSized::bar_separator_width => "Bar Separator Width", + ThemeSized::gap => "Gap", + ThemeSized::title_gap => "Title Gap", + ThemeSized::tab_bar_height => "Tab Bar Height", + ThemeSized::tab_bar_padding => "Tab Bar Padding", + ThemeSized::tab_bar_radius => "Tab Bar Radius", + ThemeSized::tab_bar_border_width => "Tab Bar Border Width", + ThemeSized::tab_bar_text_padding => "Tab Bar Text Padding", + ThemeSized::tab_bar_gap => "Tab Bar Gap", + } + } +} + +pub const DEFAULT_FONT: &str = "monospace 8"; + +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Default, Linearize)] +pub enum BarPosition { + #[default] + Top, + Bottom, +} + +impl StaticText for BarPosition { + fn text(&self) -> &'static str { + match self { + BarPosition::Top => "Top", + BarPosition::Bottom => "Bottom", + } + } +} + +impl TryFrom for BarPosition { + type Error = (); + + fn try_from(value: ConfigBarPosition) -> Result { + let v = match value { + ConfigBarPosition::Top => Self::Top, + ConfigBarPosition::Bottom => Self::Bottom, + _ => return Err(()), + }; + Ok(v) + } +} + +impl Into for BarPosition { + fn into(self) -> ConfigBarPosition { + match self { + BarPosition::Top => ConfigBarPosition::Top, + BarPosition::Bottom => ConfigBarPosition::Bottom, + } + } +} + +/// Per-corner radius for rounded rectangles. +/// +/// Each field specifies the radius (in logical pixels) for one corner. +/// A radius of 0 means a square corner. +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub struct CornerRadius { + pub top_left: f32, + pub top_right: f32, + pub bottom_right: f32, + pub bottom_left: f32, +} + +impl From for CornerRadius { + fn from(value: f32) -> Self { + Self { + top_left: value, + top_right: value, + bottom_right: value, + bottom_left: value, + } + } +} + +impl From for [f32; 4] { + fn from(cr: CornerRadius) -> Self { + [cr.top_left, cr.top_right, cr.bottom_right, cr.bottom_left] + } +} + +impl CornerRadius { + /// Shrink or grow all radii by `width`. Radii that are 0 stay 0 (square + /// corners remain square). Negative `width` shrinks; the result is clamped + /// to 0. + pub fn expanded_by(mut self, width: f32) -> Self { + if self.top_left > 0.0 { + self.top_left = (self.top_left + width).max(0.0); + } + if self.top_right > 0.0 { + self.top_right = (self.top_right + width).max(0.0); + } + if self.bottom_right > 0.0 { + self.bottom_right = (self.bottom_right + width).max(0.0); + } + if self.bottom_left > 0.0 { + self.bottom_left = (self.bottom_left + width).max(0.0); + } + self + } + + /// Scale all radii by a factor (e.g. for HiDPI). + pub fn scaled_by(self, scale: f32) -> Self { + Self { + top_left: self.top_left * scale, + top_right: self.top_right * scale, + bottom_right: self.bottom_right * scale, + bottom_left: self.bottom_left * scale, + } + } + + /// Reduce all radii proportionally so that adjacent corners don't overlap, + /// following the CSS spec algorithm. + pub fn fit_to(self, width: f32, height: f32) -> Self { + let reduction = f32::min( + f32::min( + width / (self.top_left + self.top_right), + width / (self.bottom_left + self.bottom_right), + ), + f32::min( + height / (self.top_left + self.bottom_left), + height / (self.top_right + self.bottom_right), + ), + ); + let reduction = f32::min(1.0, reduction); + Self { + top_left: self.top_left * reduction, + top_right: self.top_right * reduction, + bottom_right: self.bottom_right * reduction, + bottom_left: self.bottom_left * reduction, + } + } + + pub fn is_zero(&self) -> bool { + self.top_left == 0.0 + && self.top_right == 0.0 + && self.bottom_right == 0.0 + && self.bottom_left == 0.0 + } +} + +/// Horizontal alignment of title text inside tab buttons. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum TabTitleAlign { + #[default] + Start, + Center, + End, +} + +pub struct Theme { + pub colors: ThemeColors, + pub sizes: ThemeSizes, + pub font: CloneCell>, + pub bar_font: CloneCell>>, + pub title_font: CloneCell>>, + pub default_font: Arc, + #[allow(dead_code)] + pub show_titles: Cell, + #[allow(dead_code)] + pub floating_titles: Cell, + pub bar_position: Cell, + pub corner_radius: Cell, + pub autotile_enabled: Cell, + pub tab_title_align: Cell, +} + +impl Default for Theme { + fn default() -> Self { + let default_font = Arc::new(DEFAULT_FONT.to_string()); + Self { + colors: Default::default(), + sizes: Default::default(), + font: CloneCell::new(default_font.clone()), + bar_font: Default::default(), + title_font: Default::default(), + default_font, + show_titles: Cell::new(true), + floating_titles: Cell::new(false), + bar_position: Default::default(), + corner_radius: Cell::new(CornerRadius::default()), + autotile_enabled: Cell::new(false), + tab_title_align: Cell::new(TabTitleAlign::default()), + } + } +} + +impl Theme { + pub fn bar_font(&self) -> Arc { + self.bar_font.get().unwrap_or_else(|| self.font.get()) + } + + pub fn title_font(&self) -> Arc { + self.title_font.get().unwrap_or_else(|| self.font.get()) + } + + pub fn title_height(&self) -> i32 { + 0 + } + + pub fn title_plus_underline_height(&self) -> i32 { + 0 + } +} + +#[derive(Copy, Clone, Debug)] +pub struct Oklch { + pub l: f32, + pub c: f32, + pub h: f32, +} + +#[derive(Copy, Clone, Debug)] +pub struct Oklab { + pub l: f32, + pub a: f32, + pub b: f32, +} + +impl Oklab { + pub fn to_srgb(self) -> Color { + let l_ = self.l + 0.3963377774 * self.a + 0.2158037573 * self.b; + let m_ = self.l - 0.1055613458 * self.a - 0.0638541728 * self.b; + let s_ = self.l - 0.0894841775 * self.a - 1.2914855480 * self.b; + + let l = l_ * l_ * l_; + let m = m_ * m_ * m_; + let s = s_ * s_ * s_; + + let r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s; + let g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s; + let b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s; + + Color::new( + Eotf::Linear, + AlphaMode::PremultipliedElectrical, + r, + g, + b, + 1.0, + ) + } + + pub fn to_oklch(self) -> Oklch { + let c = (self.a * self.a + self.b * self.b).sqrt(); + let h = self.b.atan2(self.a); + + Oklch { l: self.l, c, h } + } +} + +impl Oklch { + pub fn to_oklab(self) -> Oklab { + let a = self.c * self.h.cos(); + let b = self.c * self.h.sin(); + + Oklab { l: self.l, a, b } + } +} + +impl Add for Oklab { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self { + l: self.l + rhs.l, + a: self.a + rhs.a, + b: self.b + rhs.b, + } + } +} + +impl Mul for Oklab { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + Self { + l: self.l * rhs, + a: self.a * rhs, + b: self.b * rhs, + } + } +} + +impl Div for Oklab { + type Output = Self; + + fn div(self, rhs: f32) -> Self::Output { + Self { + l: self.l / rhs, + a: self.a / rhs, + b: self.b / rhs, + } + } +} diff --git a/crates/time/Cargo.toml b/crates/time/Cargo.toml new file mode 100644 index 00000000..d962a7b1 --- /dev/null +++ b/crates/time/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "jay-time" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +uapi = "0.2.13" diff --git a/crates/time/src/lib.rs b/crates/time/src/lib.rs new file mode 100644 index 00000000..aaddcf04 --- /dev/null +++ b/crates/time/src/lib.rs @@ -0,0 +1,119 @@ +use { + std::{ + cmp::Ordering, + fmt::{Debug, Formatter}, + ops::{Add, Sub}, + time::Duration, + }, + uapi::c, +}; + +#[derive(Copy, Clone)] +pub struct Time(pub c::timespec); + +impl Debug for Time { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Time") + .field("tv_sec", &self.0.tv_sec) + .field("tv_nsec", &self.0.tv_nsec) + .finish() + } +} + +impl Time { + pub fn now_unchecked() -> Time { + let mut time = uapi::pod_zeroed(); + let _ = uapi::clock_gettime(c::CLOCK_MONOTONIC, &mut time); + Self(time) + } + + pub fn round_to_ms(self) -> Time { + if self.0.tv_nsec > 999_000_000 { + Time(c::timespec { + tv_sec: self.0.tv_sec + 1, + tv_nsec: 0, + }) + } else { + Time(c::timespec { + tv_sec: self.0.tv_sec, + tv_nsec: (self.0.tv_nsec + 999_999) / 1_000_000 * 1_000_000, + }) + } + } + + pub fn nsec(self) -> u64 { + let sec = self.0.tv_sec as u64 * 1_000_000_000; + let nsec = self.0.tv_nsec as u64; + sec + nsec + } + + pub fn usec(self) -> u64 { + let sec = self.0.tv_sec as u64 * 1_000_000; + let nsec = self.0.tv_nsec as u64 / 1_000; + sec + nsec + } + + pub fn msec(self) -> u64 { + let sec = self.0.tv_sec as u64 * 1_000; + let nsec = self.0.tv_nsec as u64 / 1_000_000; + sec + nsec + } + + pub fn elapsed(self) -> Duration { + let now = Self::now_unchecked(); + now - self + } +} + +impl Eq for Time {} + +impl PartialEq for Time { + fn eq(&self, other: &Self) -> bool { + self.0.tv_sec == other.0.tv_sec && self.0.tv_nsec == other.0.tv_nsec + } +} + +impl Ord for Time { + fn cmp(&self, other: &Self) -> Ordering { + self.0 + .tv_sec + .cmp(&other.0.tv_sec) + .then_with(|| self.0.tv_nsec.cmp(&other.0.tv_nsec)) + } +} + +impl PartialOrd for Time { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Sub