commit 5e9336c6c738dadce16160af374f8af4033b5c70 Author: Mark Kalsbeek Date: Wed Dec 10 23:50:28 2025 +0100 first commit, not pretty diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..65326bb --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb8c9e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.direnv \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..43c6dda --- /dev/null +++ b/Cargo.lock @@ -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", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8043236 --- /dev/null +++ b/Cargo.toml @@ -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" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9928ad4 --- /dev/null +++ b/README.md @@ -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) diff --git a/examples/bouncing_ball.rs b/examples/bouncing_ball.rs new file mode 100644 index 0000000..3b6ca0d --- /dev/null +++ b/examples/bouncing_ball.rs @@ -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>, 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>) { + 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>, 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>, frame: u32) { + teleprof::span!("print_status"); + + let ball = ball.lock().unwrap(); + println!( + "Frame {}: Ball at ({:.1}, {:.1})", + frame, ball.x, ball.y + ); +} \ No newline at end of file diff --git a/examples/demo.rs b/examples/demo.rs new file mode 100644 index 0000000..79f86ec --- /dev/null +++ b/examples/demo.rs @@ -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)); +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..1b52abb --- /dev/null +++ b/shell.nix @@ -0,0 +1,28 @@ +with import { }; + +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" + ''; +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..75a881b --- /dev/null +++ b/src/lib.rs @@ -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>> = 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, + thread_id: u64, + name: &'static str, + timestamp: Instant, + }, + SpanEnd { + span_id: u64, + timestamp: Instant, + }, +} + +thread_local! { + static PARENT_SPAN: Cell> = Cell::new(None); +} + +static EVENT_SENDER: Lazy> = Lazy::new(|| { + let (tx, rx) = unbounded(); + *EVENT_RECEIVER.lock().unwrap() = Some(rx); + tx +}); + +static EVENT_RECEIVER: Lazy>>>> = + Lazy::new(|| Arc::new(Mutex::new(None))); + +static SPAN_ID_COUNTER: Lazy>> = 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, + thread_id: u64, + name: &'static str, + start: Instant, + end: Instant, + } + + struct RingBuffer { + spans: Vec, + head: usize, + pending_starts: HashMap, 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 + '_> { + 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>, + } + + pub fn run() -> Result<(), Box> { + 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> = 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>, + 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> = 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()] + } +} \ No newline at end of file