first commit, not pretty

This commit is contained in:
2025-12-10 23:50:28 +01:00
commit 5e9336c6c7
9 changed files with 1748 additions and 0 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use nix

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
.direnv

818
Cargo.lock generated Normal file
View File

@@ -0,0 +1,818 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "bumpalo"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "cc"
version = "1.2.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "dlib"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading",
]
[[package]]
name = "downcast-rs"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "find-msvc-tools"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "js-sys"
version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "libredox"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
"bitflags 2.10.0",
"libc",
"redox_syscall",
]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "memoffset"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
dependencies = [
"autocfg",
]
[[package]]
name = "minifb"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c470a74618b43cd182c21b3dc1e6123501249f3bad9a0085e95d1304ca2478"
dependencies = [
"cc",
"dlib",
"futures",
"instant",
"js-sys",
"lazy_static",
"libc",
"orbclient",
"raw-window-handle",
"serde",
"serde_derive",
"tempfile",
"wasm-bindgen-futures",
"wayland-client",
"wayland-cursor",
"wayland-protocols",
"winapi",
"x11-dl",
]
[[package]]
name = "nix"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069"
dependencies = [
"bitflags 1.3.2",
"cfg-if",
"libc",
"memoffset",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "orbclient"
version = "0.3.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "247ad146e19b9437f8604c21f8652423595cf710ad108af40e77d3ae6e96b827"
dependencies = [
"libc",
"libredox",
"sdl2",
"sdl2-sys",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.16",
]
[[package]]
name = "raw-window-handle"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags 2.10.0",
]
[[package]]
name = "rustix"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
dependencies = [
"bitflags 2.10.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "sdl2"
version = "0.35.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a"
dependencies = [
"bitflags 1.3.2",
"lazy_static",
"libc",
"sdl2-sys",
]
[[package]]
name = "sdl2-sys"
version = "0.35.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0"
dependencies = [
"cfg-if",
"libc",
"version-compare",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "slab"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "syn"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "teleprof"
version = "0.1.0"
dependencies = [
"crossbeam-channel",
"minifb",
"once_cell",
"rand",
]
[[package]]
name = "tempfile"
version = "3.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "version-compare"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wayland-client"
version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715"
dependencies = [
"bitflags 1.3.2",
"downcast-rs",
"libc",
"nix",
"scoped-tls",
"wayland-commons",
"wayland-scanner",
"wayland-sys",
]
[[package]]
name = "wayland-commons"
version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902"
dependencies = [
"nix",
"once_cell",
"smallvec",
"wayland-sys",
]
[[package]]
name = "wayland-cursor"
version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661"
dependencies = [
"nix",
"wayland-client",
"xcursor",
]
[[package]]
name = "wayland-protocols"
version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6"
dependencies = [
"bitflags 1.3.2",
"wayland-client",
"wayland-commons",
"wayland-scanner",
]
[[package]]
name = "wayland-scanner"
version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53"
dependencies = [
"proc-macro2",
"quote",
"xml-rs",
]
[[package]]
name = "wayland-sys"
version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4"
dependencies = [
"dlib",
"lazy_static",
"pkg-config",
]
[[package]]
name = "web-sys"
version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "x11-dl"
version = "2.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
dependencies = [
"libc",
"once_cell",
"pkg-config",
]
[[package]]
name = "xcursor"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b"
[[package]]
name = "xml-rs"
version = "0.8.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
[[package]]
name = "zerocopy"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

13
Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "teleprof"
version = "0.1.0"
edition = "2021"
[dependencies]
minifb = "0.27"
crossbeam-channel = "0.5"
once_cell = "1.19"
[dev-dependencies]
rand = "0.8"
minifb = "0.27"

141
README.md Normal file
View File

@@ -0,0 +1,141 @@
# Teleprof
A lightweight, debug-only telemetry profiler for Rust applications. Shows thread activity and call stack hierarchy in real-time.
Inspired by RAD Telemetry - built in ~400 LOC with minimal dependencies.
## Features
- **Icicle graph** showing call stack hierarchy (top half)
- **Thread timeline** showing per-thread activity over time (bottom half)
- **Monokai color palette** for easy visual distinction
- **Pause mechanism** to freeze your application for inspection
- **Ringbuffer storage** (~16MB, 1M events) for recent history
- **Lock-free event recording** via MPSC channels
## Dependencies
Only 3 dependencies (~15 total including transitive):
- `minifb` - Window and framebuffer
- `crossbeam-channel` - Lock-free MPSC
- `once_cell` - Lazy statics
## Usage
### Add to your `Cargo.toml`:
```toml
[dependencies]
teleprof = { path = "../teleprof" } # or from crates.io when published
```
### In your code:
```rust
fn main() {
// Start the profiler window (separate thread)
#[cfg(debug_assertions)]
teleprof::start();
// Your application code
game_loop();
}
fn game_loop() {
loop {
// Profile a scope
teleprof::span!("game_loop");
update();
render();
// Check if paused (optional)
if teleprof::PAUSE.try_lock().is_err() {
// Wait until unpaused
while teleprof::PAUSE.try_lock().is_err() {
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
}
}
fn update() {
teleprof::span!("update");
// Your update code
}
fn render() {
teleprof::span!("render");
// Your render code
}
```
### For closures:
```rust
let work = || {
teleprof::span!("my_closure");
// work...
};
```
## Controls
- **Space**: Toggle pause (acquires `PAUSE` lock to freeze your app)
- **Escape**: Close profiler window
## How it works
1. `span!()` macro creates a `SpanGuard` that sends `SpanStart` on creation
2. When the guard drops, sends `SpanEnd`
3. Events are sent via lock-free MPSC channel
4. Window thread drains events into a fixed-size ringbuffer
5. Renders icicle graph (call hierarchy) and timeline (per-thread activity)
## Design Goals
- **Minimal overhead**: Lock-free event recording
- **Debug-only**: Compile out in release builds with `#[cfg(debug_assertions)]`
- **Separate window**: Doesn't interfere with your app's rendering
- **Simple API**: Just `span!("name")` and you're done
## Example Output
```
┌─────────────────────────────────────┐
│ Icicle Graph (Call Stack) │
│ ┌──────────────────────────┐ │
│ │ frame_work │ │
│ ├──────────┬───────────────┤ │
│ │ physics │ render │ │
│ ├────┬─────┤ │ │
│ │ w0 │ w1 │ │ │
│ └────┴─────┴───────────────┘ │
├─────────────────────────────────────┤
│ Thread Timeline │
│ Main: ████████████████████ │
│ Work 0: ░░██████░░░░░░░░░░░ │
│ Work 1: ░░░░░░██████░░░░░░░ │
└─────────────────────────────────────┘
```
## Examples
Run the included examples:
```bash
# Multi-threaded physics simulation
cargo run --example demo
# Bouncing ball with color-picking thread (30 FPS)
cargo run --example bouncing_ball
```
The bouncing ball example demonstrates:
- Main thread running at 30 FPS with clear frame gaps
- Background thread spawned on wall collision to pick colors
- Clear visual separation between thread activities
## License
MIT / Apache-2.0 (choose whichever you prefer)

204
examples/bouncing_ball.rs Normal file
View File

@@ -0,0 +1,204 @@
use minifb::{Key, Window, WindowOptions};
use std::thread;
use std::time::{Duration, Instant};
use std::sync::{Arc, Mutex};
use rand::Rng;
const WIDTH: usize = 800;
const HEIGHT: usize = 600;
const BALL_RADIUS: usize = 20;
// Simple ball state
struct Ball {
x: f32,
y: f32,
vx: f32,
vy: f32,
color: u32,
}
impl Ball {
fn new() -> Self {
Self {
x: 400.0,
y: 300.0,
vx: 200.0, // pixels per second
vy: 150.0,
color: 0xFF6464FF, // Red-ish
}
}
}
fn main() {
// Start the telemetry window
teleprof::start();
println!("Bouncing Ball Demo");
println!("The ball window should appear alongside the profiler");
println!("Press Space in profiler window to pause");
println!("Press Escape in either window to quit");
println!();
let mut window = Window::new(
"Bouncing Ball",
WIDTH,
HEIGHT,
WindowOptions::default(),
)
.expect("Failed to create window");
window.set_target_fps(30);
let ball = Arc::new(Mutex::new(Ball::new()));
let mut framebuffer = vec![0u32; WIDTH * HEIGHT];
// Target 30 FPS
let frame_time = Duration::from_millis(33);
let mut frame_count = 0;
while window.is_open() && !window.is_key_down(Key::Escape) {
let frame_start = Instant::now();
teleprof::span!("main_frame");
// Check if paused
if teleprof::PAUSE.try_lock().is_err() {
println!("Paused!");
while teleprof::PAUSE.try_lock().is_err() {
thread::sleep(Duration::from_millis(100));
}
println!("Resumed!");
}
// Update physics
let hit_wall = update_physics(&ball, frame_time.as_secs_f32());
// If we hit a wall, spawn a thread to pick a new color
if hit_wall {
let ball_clone = Arc::clone(&ball);
thread::spawn(move || {
pick_new_color(ball_clone);
});
}
// Render
render(&ball, &mut framebuffer);
// Update window
window
.update_with_buffer(&framebuffer, WIDTH, HEIGHT)
.expect("Failed to update window");
frame_count += 1;
if frame_count % 30 == 0 {
print_status(&ball, frame_count);
}
// Sleep to maintain 30fps
let elapsed = frame_start.elapsed();
if elapsed < frame_time {
thread::sleep(frame_time - elapsed);
}
}
}
fn update_physics(ball: &Arc<Mutex<Ball>>, dt: f32) -> bool {
teleprof::span!("update_physics");
let mut ball = ball.lock().unwrap();
// Update position
ball.x += ball.vx * dt;
ball.y += ball.vy * dt;
let mut hit_wall = false;
// Bounce off walls
let radius = BALL_RADIUS as f32;
if ball.x - radius < 0.0 || ball.x + radius > WIDTH as f32 {
ball.vx = -ball.vx;
ball.x = ball.x.clamp(radius, WIDTH as f32 - radius);
hit_wall = true;
}
if ball.y - radius < 0.0 || ball.y + radius > HEIGHT as f32 {
ball.vy = -ball.vy;
ball.y = ball.y.clamp(radius, HEIGHT as f32 - radius);
hit_wall = true;
}
// Simulate some physics computation
thread::sleep(Duration::from_millis(5));
hit_wall
}
fn pick_new_color(ball: Arc<Mutex<Ball>>) {
teleprof::span!("pick_new_color");
// Simulate some "expensive" color selection
thread::sleep(Duration::from_millis(10));
let mut rng = rand::thread_rng();
let r = rng.gen_range(50..255);
let g = rng.gen_range(50..255);
let b = rng.gen_range(50..255);
let color = ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
let mut ball = ball.lock().unwrap();
ball.color = color;
println!(" → New color selected: RGB({}, {}, {})", r, g, b);
}
fn render(ball: &Arc<Mutex<Ball>>, framebuffer: &mut [u32]) {
teleprof::span!("render");
{
teleprof::span!("clear_background");
// Clear to dark gray
framebuffer.fill(0x2A2A2AFF);
}
{
teleprof::span!("draw_ball");
let ball = ball.lock().unwrap();
// Draw ball as a filled circle
let cx = ball.x as i32;
let cy = ball.y as i32;
let radius = BALL_RADIUS as i32;
for dy in -radius..=radius {
for dx in -radius..=radius {
// Check if point is inside circle
if dx * dx + dy * dy <= radius * radius {
let x = cx + dx;
let y = cy + dy;
if x >= 0 && x < WIDTH as i32 && y >= 0 && y < HEIGHT as i32 {
let idx = y as usize * WIDTH + x as usize;
framebuffer[idx] = ball.color;
}
}
}
}
}
{
teleprof::span!("submit_frame");
// Simulate GPU submission
thread::sleep(Duration::from_millis(2));
}
}
fn print_status(ball: &Arc<Mutex<Ball>>, frame: u32) {
teleprof::span!("print_status");
let ball = ball.lock().unwrap();
println!(
"Frame {}: Ball at ({:.1}, {:.1})",
frame, ball.x, ball.y
);
}

83
examples/demo.rs Normal file
View File

@@ -0,0 +1,83 @@
use std::thread;
use std::time::Duration;
fn main() {
// Start the telemetry window
teleprof::start();
println!("Teleprof demo running...");
println!("Press Space in the profiler window to pause/unpause");
println!("Press Escape in the profiler window to quit");
// Simulate some work
for i in 0..1000 {
frame_work(i);
// Check if paused
if teleprof::PAUSE.try_lock().is_err() {
println!("Paused!");
while teleprof::PAUSE.try_lock().is_err() {
thread::sleep(Duration::from_millis(100));
}
println!("Resumed!");
}
thread::sleep(Duration::from_millis(16));
}
}
fn frame_work(frame: u32) {
teleprof::span!("frame_work");
physics_update();
render();
if frame % 10 == 0 {
occasional_task();
}
}
fn physics_update() {
teleprof::span!("physics_update");
// Spawn some worker threads
let handles: Vec<_> = (0..3).map(|i| {
thread::spawn(move || {
physics_worker(i);
})
}).collect();
for handle in handles {
handle.join().ok();
}
}
fn physics_worker(id: u32) {
teleprof::span!("physics_worker");
// Simulate work
let work_ms = 5 + (id * 2);
thread::sleep(Duration::from_millis(work_ms as u64));
}
fn render() {
teleprof::span!("render");
build_command_buffer();
submit_to_gpu();
}
fn build_command_buffer() {
teleprof::span!("build_command_buffer");
thread::sleep(Duration::from_millis(3));
}
fn submit_to_gpu() {
teleprof::span!("submit_to_gpu");
thread::sleep(Duration::from_millis(2));
}
fn occasional_task() {
teleprof::span!("occasional_task");
thread::sleep(Duration::from_millis(10));
}

28
shell.nix Normal file
View File

@@ -0,0 +1,28 @@
with import <nixpkgs> { };
let
buildInputs = [
# For minifb
xorg.libX11
xorg.libXcursor
xorg.libXrandr
xorg.libXi
# Wayland support
wayland
libxkbcommon
];
in
mkShell {
nativeBuildInputs = [
pkg-config
];
inherit buildInputs;
shellHook = ''
export LD_LIBRARY_PATH="${lib.makeLibraryPath buildInputs}:$LD_LIBRARY_PATH"
echo "Teleprof dev environment loaded"
echo "Run: cargo run --example demo"
'';
}

458
src/lib.rs Normal file
View File

@@ -0,0 +1,458 @@
use crossbeam_channel::{unbounded, Receiver, Sender};
use once_cell::sync::Lazy;
use std::cell::Cell;
use std::sync::{Arc, Mutex};
use std::time::Instant;
// ============================================================================
// Public API
// ============================================================================
/// Global PAUSE lock - acquire this to pause the application
pub static PAUSE: Lazy<Arc<Mutex<()>>> = Lazy::new(|| Arc::new(Mutex::new(())));
/// Start the telemetry window in a separate thread
pub fn start() {
std::thread::spawn(|| {
if let Err(e) = window::run() {
eprintln!("Teleprof window error: {}", e);
}
});
}
/// Create a profiling span - use via the `span!` macro
pub struct SpanGuard {
span_id: u64,
}
impl SpanGuard {
pub fn new(name: &'static str) -> Self {
let span_id = next_span_id();
let thread_id = std::thread::current().id();
let parent_id = PARENT_SPAN.with(|p| p.get());
// Set ourselves as the current parent for nested spans
PARENT_SPAN.with(|p| p.set(Some(span_id)));
EVENT_SENDER.send(Event::SpanStart {
span_id,
parent_id,
thread_id: thread_id_to_u64(thread_id),
name,
timestamp: Instant::now(),
}).ok();
Self { span_id }
}
}
impl Drop for SpanGuard {
fn drop(&mut self) {
EVENT_SENDER.send(Event::SpanEnd {
span_id: self.span_id,
timestamp: Instant::now(),
}).ok();
// Pop back to parent
PARENT_SPAN.with(|p| {
// Find parent by looking at active spans (simplified - just clear for now)
p.set(None);
});
}
}
/// Macro for creating a span
#[macro_export]
macro_rules! span {
($name:expr) => {
let _span_guard = $crate::SpanGuard::new($name);
};
}
// ============================================================================
// Internal types and state
// ============================================================================
#[derive(Debug, Clone)]
pub(crate) enum Event {
SpanStart {
span_id: u64,
parent_id: Option<u64>,
thread_id: u64,
name: &'static str,
timestamp: Instant,
},
SpanEnd {
span_id: u64,
timestamp: Instant,
},
}
thread_local! {
static PARENT_SPAN: Cell<Option<u64>> = Cell::new(None);
}
static EVENT_SENDER: Lazy<Sender<Event>> = Lazy::new(|| {
let (tx, rx) = unbounded();
*EVENT_RECEIVER.lock().unwrap() = Some(rx);
tx
});
static EVENT_RECEIVER: Lazy<Arc<Mutex<Option<Receiver<Event>>>>> =
Lazy::new(|| Arc::new(Mutex::new(None)));
static SPAN_ID_COUNTER: Lazy<Arc<Mutex<u64>>> = Lazy::new(|| Arc::new(Mutex::new(0)));
fn next_span_id() -> u64 {
let mut counter = SPAN_ID_COUNTER.lock().unwrap();
let id = *counter;
*counter += 1;
id
}
fn thread_id_to_u64(id: std::thread::ThreadId) -> u64 {
// Hack: ThreadId doesn't expose inner value, so we format and parse
let s = format!("{:?}", id);
s.trim_start_matches("ThreadId(")
.trim_end_matches(")")
.parse()
.unwrap_or(0)
}
// ============================================================================
// Window rendering
// ============================================================================
mod window {
use super::*;
use minifb::{Key, Window, WindowOptions};
use std::collections::HashMap;
const INITIAL_WIDTH: usize = 1280;
const INITIAL_HEIGHT: usize = 720;
const MAX_EVENTS: usize = 1_000_000; // ~16MB at 16 bytes per event
// Monokai palette
const COLORS: [u32; 8] = [
0xF92672, // Pink
0xA6E22E, // Green
0xFD971F, // Orange
0x66D9EF, // Cyan
0xAE81FF, // Purple
0xE6DB74, // Yellow
0xF8F8F2, // White
0x75715E, // Gray
];
const BG_COLOR: u32 = 0x272822;
const GRID_COLOR: u32 = 0x3E3D32;
struct CompletedSpan {
span_id: u64,
parent_id: Option<u64>,
thread_id: u64,
name: &'static str,
start: Instant,
end: Instant,
}
struct RingBuffer {
spans: Vec<CompletedSpan>,
head: usize,
pending_starts: HashMap<u64, (u64, Option<u64>, u64, &'static str, Instant)>,
}
impl RingBuffer {
fn new() -> Self {
Self {
spans: Vec::with_capacity(MAX_EVENTS),
head: 0,
pending_starts: HashMap::new(),
}
}
fn push_event(&mut self, event: Event) {
match event {
Event::SpanStart { span_id, parent_id, thread_id, name, timestamp } => {
self.pending_starts.insert(span_id, (span_id, parent_id, thread_id, name, timestamp));
}
Event::SpanEnd { span_id, timestamp } => {
if let Some((span_id, parent_id, thread_id, name, start)) = self.pending_starts.remove(&span_id) {
let span = CompletedSpan {
span_id,
parent_id,
thread_id,
name,
start,
end: timestamp,
};
if self.spans.len() < MAX_EVENTS {
self.spans.push(span);
} else {
self.spans[self.head] = span;
self.head = (self.head + 1) % MAX_EVENTS;
}
}
}
}
}
fn iter(&self) -> Box<dyn Iterator<Item = &CompletedSpan> + '_> {
if self.spans.len() < MAX_EVENTS {
Box::new(self.spans.iter())
} else {
// Return items in chronological order from ringbuffer
Box::new(self.spans[self.head..].iter().chain(self.spans[..self.head].iter()))
}
}
}
struct ViewState {
time_offset: f64, // seconds
time_scale: f64, // pixels per second
paused: bool,
pause_guard: Option<std::sync::MutexGuard<'static, ()>>,
}
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut window = Window::new(
"Teleprof",
INITIAL_WIDTH,
INITIAL_HEIGHT,
WindowOptions::default(),
)?;
window.set_target_fps(60);
let receiver = EVENT_RECEIVER.lock().unwrap().take()
.ok_or("Event receiver not initialized")?;
let mut buffer = RingBuffer::new();
let mut view = ViewState {
time_offset: 0.0,
time_scale: 100.0, // 100 pixels per second
paused: false,
pause_guard: None,
};
let mut framebuffer = vec![BG_COLOR; INITIAL_WIDTH * INITIAL_HEIGHT];
while window.is_open() && !window.is_key_down(Key::Escape) {
// Drain events from channel
while let Ok(event) = receiver.try_recv() {
buffer.push_event(event);
}
// Handle input
if window.is_key_pressed(Key::Space, minifb::KeyRepeat::No) {
view.paused = !view.paused;
if view.paused {
view.pause_guard = PAUSE.try_lock().ok();
} else {
view.pause_guard = None;
}
}
// Get current window size
let (width, height) = window.get_size();
if framebuffer.len() != width * height {
framebuffer.resize(width * height, BG_COLOR);
}
// Clear framebuffer
framebuffer.fill(BG_COLOR);
// Render
render_frame(&mut framebuffer, width, height, &buffer, &view);
window.update_with_buffer(&framebuffer, width, height)?;
}
Ok(())
}
fn render_frame(
framebuffer: &mut [u32],
width: usize,
height: usize,
buffer: &RingBuffer,
view: &ViewState,
) {
let icicle_height = height / 2;
let timeline_height = height - icicle_height;
// Find time range
let spans: Vec<_> = buffer.iter().collect();
if spans.is_empty() {
return;
}
let earliest = spans.iter().map(|s| s.start).min().unwrap();
let latest = spans.iter().map(|s| s.end).max().unwrap();
let duration = (latest - earliest).as_secs_f64();
// Draw icicle graph (top half)
render_icicle(framebuffer, width, icicle_height, &spans, earliest, duration, view);
// Draw timeline (bottom half)
render_timeline(
framebuffer,
width,
height,
icicle_height,
timeline_height,
&spans,
earliest,
duration,
view,
);
}
fn render_icicle(
framebuffer: &mut [u32],
width: usize,
height: usize,
spans: &[&CompletedSpan],
earliest: Instant,
duration: f64,
view: &ViewState,
) {
// Build tree structure
let mut roots = Vec::new();
let mut children: HashMap<u64, Vec<&CompletedSpan>> = HashMap::new();
for span in spans {
if let Some(parent) = span.parent_id {
children.entry(parent).or_default().push(span);
} else {
roots.push(*span);
}
}
// Render each root and its children recursively
let y_start = 0;
let row_height = 20;
for root in roots {
render_icicle_span(
framebuffer,
width,
height,
root,
&children,
earliest,
duration,
y_start,
row_height,
);
}
}
fn render_icicle_span(
framebuffer: &mut [u32],
width: usize,
height: usize,
span: &CompletedSpan,
children: &HashMap<u64, Vec<&CompletedSpan>>,
earliest: Instant,
duration: f64,
y: usize,
row_height: usize,
) {
if y + row_height > height {
return;
}
let start_time = (span.start - earliest).as_secs_f64();
let end_time = (span.end - earliest).as_secs_f64();
let x1 = ((start_time / duration) * width as f64) as usize;
let x2 = ((end_time / duration) * width as f64) as usize;
let color = get_color_for_name(span.name);
fill_rect(framebuffer, width, x1, y, x2 - x1, row_height, color);
// Render children
if let Some(child_spans) = children.get(&span.span_id) {
let child_y = y + row_height;
for child in child_spans {
render_icicle_span(
framebuffer,
width,
height,
child,
children,
earliest,
duration,
child_y,
row_height,
);
}
}
}
fn render_timeline(
framebuffer: &mut [u32],
width: usize,
_height: usize,
y_offset: usize,
timeline_height: usize,
spans: &[&CompletedSpan],
earliest: Instant,
duration: f64,
_view: &ViewState,
) {
// Group by thread
let mut threads: HashMap<u64, Vec<&CompletedSpan>> = HashMap::new();
for span in spans {
threads.entry(span.thread_id).or_default().push(*span);
}
let thread_ids: Vec<_> = threads.keys().copied().collect();
let num_threads = thread_ids.len();
if num_threads == 0 {
return;
}
let row_height = timeline_height / num_threads.max(1);
for (i, thread_id) in thread_ids.iter().enumerate() {
let y = y_offset + i * row_height;
let thread_spans = &threads[thread_id];
for span in thread_spans {
let start_time = (span.start - earliest).as_secs_f64();
let end_time = (span.end - earliest).as_secs_f64();
let x1 = ((start_time / duration) * width as f64) as usize;
let x2 = ((end_time / duration) * width as f64).max(x1 as f64 + 1.0) as usize;
let color = get_color_for_name(span.name);
fill_rect(framebuffer, width, x1, y, x2 - x1, row_height - 2, color);
}
}
}
fn fill_rect(framebuffer: &mut [u32], width: usize, x: usize, y: usize, w: usize, h: usize, color: u32) {
for dy in 0..h {
let row = y + dy;
for dx in 0..w {
let col = x + dx;
if col < width {
let idx = row * width + col;
if idx < framebuffer.len() {
framebuffer[idx] = color;
}
}
}
}
}
fn get_color_for_name(name: &str) -> u32 {
let hash = name.bytes().fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32));
COLORS[(hash as usize) % COLORS.len()]
}
}