sync
This commit is contained in:
parent
26ba098183
commit
2f7bcbb14e
300
Cargo.lock
generated
300
Cargo.lock
generated
|
@ -29,6 +29,15 @@ dependencies = [
|
||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android-tzdata"
|
name = "android-tzdata"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
@ -204,6 +213,12 @@ version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder-lite"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.7.1"
|
version = "1.7.1"
|
||||||
|
@ -215,6 +230,10 @@ name = "cc"
|
||||||
version = "1.1.8"
|
version = "1.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549"
|
checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549"
|
||||||
|
dependencies = [
|
||||||
|
"jobserver",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
|
@ -287,6 +306,25 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-deque"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-epoch"
|
||||||
|
version = "0.9.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.20"
|
version = "0.8.20"
|
||||||
|
@ -364,6 +402,12 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
@ -448,6 +492,19 @@ dependencies = [
|
||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generator"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "979f00864edc7516466d6b3157706e06c032f22715700ddd878228a91d02bc56"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"rustversion",
|
||||||
|
"windows",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
|
@ -475,6 +532,12 @@ version = "0.28.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "haku"
|
name = "haku"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -497,6 +560,7 @@ dependencies = [
|
||||||
"dlmalloc",
|
"dlmalloc",
|
||||||
"haku",
|
"haku",
|
||||||
"log",
|
"log",
|
||||||
|
"paste",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -626,7 +690,7 @@ dependencies = [
|
||||||
"iana-time-zone-haiku",
|
"iana-time-zone-haiku",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-core",
|
"windows-core 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -648,6 +712,17 @@ dependencies = [
|
||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "image"
|
||||||
|
version = "0.25.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"byteorder-lite",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indenter"
|
name = "indenter"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
|
@ -670,6 +745,15 @@ version = "1.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.69"
|
version = "0.3.69"
|
||||||
|
@ -708,6 +792,16 @@ dependencies = [
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libwebp-sys"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "829b6b604f31ed6d2bccbac841fe0788de93dbd87e4eb1ba2c4adfe8c012a838"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"glob",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
|
@ -724,6 +818,28 @@ version = "0.4.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "loom"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"generator",
|
||||||
|
"scoped-tls",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchers"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||||
|
dependencies = [
|
||||||
|
"regex-automata 0.1.10",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
|
@ -842,6 +958,12 @@ dependencies = [
|
||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paste"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
|
@ -943,6 +1065,26 @@ dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon"
|
||||||
|
version = "1.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"rayon-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon-core"
|
||||||
|
version = "1.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
@ -952,6 +1094,50 @@ dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata 0.4.7",
|
||||||
|
"regex-syntax 0.8.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||||
|
dependencies = [
|
||||||
|
"regex-syntax 0.6.29",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax 0.8.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.6.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rkgk"
|
name = "rkgk"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -965,8 +1151,10 @@ dependencies = [
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"eyre",
|
"eyre",
|
||||||
"haku",
|
"haku",
|
||||||
|
"indexmap",
|
||||||
"rand",
|
"rand",
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
|
"rayon",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -975,8 +1163,14 @@ dependencies = [
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"tracy-client",
|
||||||
|
"webp",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rkgk-image-ops"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rusqlite"
|
name = "rusqlite"
|
||||||
version = "0.32.1"
|
version = "0.32.1"
|
||||||
|
@ -1018,6 +1212,12 @@ dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scoped-tls"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
@ -1442,14 +1642,38 @@ version = "0.3.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
|
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"matchers",
|
||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
"sharded-slab",
|
"sharded-slab",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thread_local",
|
"thread_local",
|
||||||
|
"tracing",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracy-client"
|
||||||
|
version = "0.17.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63de1e1d4115534008d8fd5788b39324d6f58fc707849090533828619351d855"
|
||||||
|
dependencies = [
|
||||||
|
"loom",
|
||||||
|
"once_cell",
|
||||||
|
"tracy-client-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracy-client-sys"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "98b98232a2447ce0a58f9a0bfb5f5e39647b5c597c994b63945fcccd1306fafb"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tungstenite"
|
name = "tungstenite"
|
||||||
version = "0.21.0"
|
version = "0.21.0"
|
||||||
|
@ -1610,6 +1834,16 @@ version = "0.2.92"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webp"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f53152f51fb5af0c08484c33d16cca96175881d1f3dec068c23b31a158c2d99"
|
||||||
|
dependencies = [
|
||||||
|
"image",
|
||||||
|
"libwebp-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
@ -1641,6 +1875,16 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.58.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core 0.58.0",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
@ -1650,6 +1894,60 @@ dependencies = [
|
||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.58.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.58.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.58.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||||
|
dependencies = [
|
||||||
|
"windows-result",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
|
|
@ -5,6 +5,10 @@ members = ["crates/*"]
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
haku.path = "crates/haku"
|
haku.path = "crates/haku"
|
||||||
log = "0.4.22"
|
log = "0.4.22"
|
||||||
|
rkgk-image-ops.path = "crates/rkgk-image-ops"
|
||||||
|
|
||||||
|
[profile.dev.package.rkgk-image-ops]
|
||||||
|
opt-level = 3
|
||||||
|
|
||||||
[profile.wasm-dev]
|
[profile.wasm-dev]
|
||||||
inherits = "dev"
|
inherits = "dev"
|
||||||
|
|
|
@ -11,4 +11,5 @@ arrayvec = { version = "0.7.4", default-features = false }
|
||||||
dlmalloc = { version = "0.2.6", features = ["global"] }
|
dlmalloc = { version = "0.2.6", features = ["global"] }
|
||||||
haku.workspace = true
|
haku.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
paste = "1.0.15"
|
||||||
|
|
||||||
|
|
|
@ -14,10 +14,10 @@ use haku::{
|
||||||
},
|
},
|
||||||
sexp::{parse_toplevel, Ast, Parser},
|
sexp::{parse_toplevel, Ast, Parser},
|
||||||
system::{ChunkId, System, SystemImage},
|
system::{ChunkId, System, SystemImage},
|
||||||
value::{BytecodeLoc, Closure, FunctionName, Ref},
|
value::{BytecodeLoc, Closure, FunctionName, Ref, Value},
|
||||||
vm::{Exception, Vm, VmImage, VmLimits},
|
vm::{Exception, Vm, VmImage, VmLimits},
|
||||||
};
|
};
|
||||||
use log::info;
|
use log::{debug, info};
|
||||||
|
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
mod panicking;
|
mod panicking;
|
||||||
|
@ -66,6 +66,41 @@ impl Default for Limits {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
extern "C" fn haku_limits_new() -> *mut Limits {
|
||||||
|
Box::leak(Box::new(Limits::default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe extern "C" fn haku_limits_destroy(limits: *mut Limits) {
|
||||||
|
drop(Box::from_raw(limits))
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! limit_setter {
|
||||||
|
($name:tt) => {
|
||||||
|
paste::paste! {
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe extern "C" fn [<haku_limits_set_ $name>](limits: *mut Limits, value: usize) {
|
||||||
|
debug!("set limit {} = {value}", stringify!($name));
|
||||||
|
|
||||||
|
let limits = &mut *limits;
|
||||||
|
limits.$name = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
limit_setter!(max_chunks);
|
||||||
|
limit_setter!(max_defs);
|
||||||
|
limit_setter!(ast_capacity);
|
||||||
|
limit_setter!(chunk_capacity);
|
||||||
|
limit_setter!(stack_capacity);
|
||||||
|
limit_setter!(call_stack_capacity);
|
||||||
|
limit_setter!(ref_capacity);
|
||||||
|
limit_setter!(fuel);
|
||||||
|
limit_setter!(pixmap_stack_capacity);
|
||||||
|
limit_setter!(transform_stack_capacity);
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct Instance {
|
struct Instance {
|
||||||
limits: Limits,
|
limits: Limits,
|
||||||
|
@ -76,13 +111,16 @@ struct Instance {
|
||||||
defs_image: DefsImage,
|
defs_image: DefsImage,
|
||||||
vm: Vm,
|
vm: Vm,
|
||||||
vm_image: VmImage,
|
vm_image: VmImage,
|
||||||
|
|
||||||
|
value: Value,
|
||||||
exception: Option<Exception>,
|
exception: Option<Exception>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
unsafe extern "C" fn haku_instance_new() -> *mut Instance {
|
unsafe extern "C" fn haku_instance_new(limits: *const Limits) -> *mut Instance {
|
||||||
// TODO: This should be a parameter.
|
let limits = *limits;
|
||||||
let limits = Limits::default();
|
debug!("creating new instance with limits: {limits:?}");
|
||||||
|
|
||||||
let system = System::new(limits.max_chunks);
|
let system = System::new(limits.max_chunks);
|
||||||
|
|
||||||
let defs = Defs::new(limits.max_defs);
|
let defs = Defs::new(limits.max_defs);
|
||||||
|
@ -108,6 +146,7 @@ unsafe extern "C" fn haku_instance_new() -> *mut Instance {
|
||||||
defs_image,
|
defs_image,
|
||||||
vm,
|
vm,
|
||||||
vm_image,
|
vm_image,
|
||||||
|
value: Value::Nil,
|
||||||
exception: None,
|
exception: None,
|
||||||
});
|
});
|
||||||
Box::leak(instance)
|
Box::leak(instance)
|
||||||
|
@ -125,6 +164,12 @@ unsafe extern "C" fn haku_reset(instance: *mut Instance) {
|
||||||
instance.defs.restore_image(&instance.defs_image);
|
instance.defs.restore_image(&instance.defs_image);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe extern "C" fn haku_reset_vm(instance: *mut Instance) {
|
||||||
|
let instance = &mut *instance;
|
||||||
|
instance.vm.restore_image(&instance.vm_image);
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
unsafe extern "C" fn haku_has_exception(instance: *mut Instance) -> bool {
|
unsafe extern "C" fn haku_has_exception(instance: *mut Instance) -> bool {
|
||||||
(*instance).exception.is_some()
|
(*instance).exception.is_some()
|
||||||
|
@ -285,13 +330,13 @@ unsafe extern "C" fn haku_compile_brush(
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PixmapLock {
|
struct PixmapLock {
|
||||||
pixmap: Option<Pixmap>,
|
pixmap: Pixmap,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
extern "C" fn haku_pixmap_new(width: u32, height: u32) -> *mut PixmapLock {
|
extern "C" fn haku_pixmap_new(width: u32, height: u32) -> *mut PixmapLock {
|
||||||
Box::leak(Box::new(PixmapLock {
|
Box::leak(Box::new(PixmapLock {
|
||||||
pixmap: Some(Pixmap::new(width, height).expect("invalid pixmap size")),
|
pixmap: Pixmap::new(width, height).expect("invalid pixmap size"),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -302,32 +347,18 @@ unsafe extern "C" fn haku_pixmap_destroy(pixmap: *mut PixmapLock) {
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
unsafe extern "C" fn haku_pixmap_data(pixmap: *mut PixmapLock) -> *mut u8 {
|
unsafe extern "C" fn haku_pixmap_data(pixmap: *mut PixmapLock) -> *mut u8 {
|
||||||
let pixmap = (*pixmap)
|
let pixmap = &mut (*pixmap).pixmap;
|
||||||
.pixmap
|
|
||||||
.as_mut()
|
|
||||||
.expect("pixmap is already being rendered to");
|
|
||||||
|
|
||||||
pixmap.pixels_mut().as_mut_ptr() as *mut u8
|
pixmap.pixels_mut().as_mut_ptr() as *mut u8
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
unsafe extern "C" fn haku_pixmap_clear(pixmap: *mut PixmapLock) {
|
unsafe extern "C" fn haku_pixmap_clear(pixmap: *mut PixmapLock) {
|
||||||
let pixmap = (*pixmap)
|
let pixmap = &mut (*pixmap).pixmap;
|
||||||
.pixmap
|
|
||||||
.as_mut()
|
|
||||||
.expect("pixmap is already being rendered to");
|
|
||||||
pixmap.pixels_mut().fill(PremultipliedColorU8::TRANSPARENT);
|
pixmap.pixels_mut().fill(PremultipliedColorU8::TRANSPARENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
unsafe extern "C" fn haku_render_brush(
|
unsafe extern "C" fn haku_eval_brush(instance: *mut Instance, brush: *const Brush) -> StatusCode {
|
||||||
instance: *mut Instance,
|
|
||||||
brush: *const Brush,
|
|
||||||
pixmap_a: *mut PixmapLock,
|
|
||||||
pixmap_b: *mut PixmapLock,
|
|
||||||
translation_x: f32,
|
|
||||||
translation_y: f32,
|
|
||||||
) -> StatusCode {
|
|
||||||
let instance = &mut *instance;
|
let instance = &mut *instance;
|
||||||
let brush = &*brush;
|
let brush = &*brush;
|
||||||
|
|
||||||
|
@ -347,7 +378,7 @@ unsafe extern "C" fn haku_render_brush(
|
||||||
return StatusCode::OutOfRefSlots;
|
return StatusCode::OutOfRefSlots;
|
||||||
};
|
};
|
||||||
|
|
||||||
let scribble = match instance.vm.run(&instance.system, closure_id) {
|
instance.value = match instance.vm.run(&instance.system, closure_id) {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(exn) => {
|
Err(exn) => {
|
||||||
instance.exception = Some(exn);
|
instance.exception = Some(exn);
|
||||||
|
@ -355,47 +386,36 @@ unsafe extern "C" fn haku_render_brush(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut render = |pixmap: *mut PixmapLock| {
|
StatusCode::Ok
|
||||||
let pixmap_locked = (*pixmap)
|
}
|
||||||
.pixmap
|
|
||||||
.take()
|
|
||||||
.expect("pixmap is already being rendered to");
|
|
||||||
|
|
||||||
let mut renderer = Renderer::new(
|
#[no_mangle]
|
||||||
pixmap_locked,
|
unsafe extern "C" fn haku_render_value(
|
||||||
&RendererLimits {
|
instance: *mut Instance,
|
||||||
pixmap_stack_capacity: instance.limits.pixmap_stack_capacity,
|
pixmap: *mut PixmapLock,
|
||||||
transform_stack_capacity: instance.limits.transform_stack_capacity,
|
translation_x: f32,
|
||||||
},
|
translation_y: f32,
|
||||||
);
|
) -> StatusCode {
|
||||||
renderer.translate(translation_x, translation_y);
|
let instance = &mut *instance;
|
||||||
match renderer.render(&instance.vm, scribble) {
|
|
||||||
Ok(()) => (),
|
|
||||||
Err(exn) => {
|
|
||||||
instance.exception = Some(exn);
|
|
||||||
return StatusCode::RenderException;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let pixmap_locked = renderer.finish();
|
let pixmap_locked = &mut (*pixmap).pixmap;
|
||||||
|
|
||||||
(*pixmap).pixmap = Some(pixmap_locked);
|
let mut renderer = Renderer::new(
|
||||||
|
pixmap_locked,
|
||||||
StatusCode::Ok
|
&RendererLimits {
|
||||||
};
|
pixmap_stack_capacity: instance.limits.pixmap_stack_capacity,
|
||||||
|
transform_stack_capacity: instance.limits.transform_stack_capacity,
|
||||||
match render(pixmap_a) {
|
},
|
||||||
StatusCode::Ok => (),
|
);
|
||||||
other => return other,
|
renderer.translate(translation_x, translation_y);
|
||||||
}
|
match renderer.render(&instance.vm, instance.value) {
|
||||||
if !pixmap_b.is_null() {
|
Ok(()) => (),
|
||||||
match render(pixmap_b) {
|
Err(exn) => {
|
||||||
StatusCode::Ok => (),
|
instance.exception = Some(exn);
|
||||||
other => return other,
|
instance.vm.restore_image(&instance.vm_image);
|
||||||
|
return StatusCode::RenderException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
instance.vm.restore_image(&instance.vm_image);
|
|
||||||
|
|
||||||
StatusCode::Ok
|
StatusCode::Ok
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,18 +15,23 @@ pub struct RendererLimits {
|
||||||
pub transform_stack_capacity: usize,
|
pub transform_stack_capacity: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Renderer {
|
pub enum RenderTarget<'a> {
|
||||||
pixmap_stack: Vec<Pixmap>,
|
Borrowed(&'a mut Pixmap),
|
||||||
|
Owned(Pixmap),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Renderer<'a> {
|
||||||
|
pixmap_stack: Vec<RenderTarget<'a>>,
|
||||||
transform_stack: Vec<Transform>,
|
transform_stack: Vec<Transform>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Renderer {
|
impl<'a> Renderer<'a> {
|
||||||
pub fn new(pixmap: Pixmap, limits: &RendererLimits) -> Self {
|
pub fn new(pixmap: &'a mut Pixmap, limits: &RendererLimits) -> Self {
|
||||||
assert!(limits.pixmap_stack_capacity > 0);
|
assert!(limits.pixmap_stack_capacity > 0);
|
||||||
assert!(limits.transform_stack_capacity > 0);
|
assert!(limits.transform_stack_capacity > 0);
|
||||||
|
|
||||||
let mut blend_stack = Vec::with_capacity(limits.pixmap_stack_capacity);
|
let mut blend_stack = Vec::with_capacity(limits.pixmap_stack_capacity);
|
||||||
blend_stack.push(pixmap);
|
blend_stack.push(RenderTarget::Borrowed(pixmap));
|
||||||
|
|
||||||
let mut transform_stack = Vec::with_capacity(limits.transform_stack_capacity);
|
let mut transform_stack = Vec::with_capacity(limits.transform_stack_capacity);
|
||||||
transform_stack.push(Transform::identity());
|
transform_stack.push(Transform::identity());
|
||||||
|
@ -55,7 +60,10 @@ impl Renderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pixmap_mut(&mut self) -> &mut Pixmap {
|
fn pixmap_mut(&mut self) -> &mut Pixmap {
|
||||||
self.pixmap_stack.last_mut().unwrap()
|
match self.pixmap_stack.last_mut().unwrap() {
|
||||||
|
RenderTarget::Borrowed(pixmap) => pixmap,
|
||||||
|
RenderTarget::Owned(pixmap) => pixmap,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&mut self, vm: &Vm, value: Value) -> Result<(), Exception> {
|
pub fn render(&mut self, vm: &Vm, value: Value) -> Result<(), Exception> {
|
||||||
|
@ -123,10 +131,6 @@ impl Renderer {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn finish(mut self) -> Pixmap {
|
|
||||||
self.pixmap_stack.drain(..).next().unwrap()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_paint() -> Paint<'static> {
|
fn default_paint() -> Paint<'static> {
|
||||||
|
|
6
crates/rkgk-image-ops/Cargo.toml
Normal file
6
crates/rkgk-image-ops/Cargo.toml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[package]
|
||||||
|
name = "rkgk-image-ops"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
14
crates/rkgk-image-ops/src/lib.rs
Normal file
14
crates/rkgk-image-ops/src/lib.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
pub fn add(left: u64, right: u64) -> u64 {
|
||||||
|
left + right
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_works() {
|
||||||
|
let result = add(2, 2);
|
||||||
|
assert_eq!(result, 4);
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,8 +13,10 @@ dashmap = "6.0.1"
|
||||||
derive_more = { version = "1.0.0", features = ["try_from"] }
|
derive_more = { version = "1.0.0", features = ["try_from"] }
|
||||||
eyre = "0.6.12"
|
eyre = "0.6.12"
|
||||||
haku.workspace = true
|
haku.workspace = true
|
||||||
|
indexmap = "2.4.0"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
rand_chacha = "0.3.1"
|
rand_chacha = "0.3.1"
|
||||||
|
rayon = "1.10.0"
|
||||||
rusqlite = { version = "0.32.1", features = ["bundled"] }
|
rusqlite = { version = "0.32.1", features = ["bundled"] }
|
||||||
serde = { version = "1.0.206", features = ["derive"] }
|
serde = { version = "1.0.206", features = ["derive"] }
|
||||||
serde_json = "1.0.124"
|
serde_json = "1.0.124"
|
||||||
|
@ -23,3 +25,9 @@ toml = "0.8.19"
|
||||||
tower-http = { version = "0.5.2", features = ["fs"] }
|
tower-http = { version = "0.5.2", features = ["fs"] }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
||||||
|
tracy-client = { version = "0.17.1", optional = true}
|
||||||
|
webp = "0.3.0"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
memory-profiling = ["dep:tracy-client"]
|
||||||
|
|
|
@ -9,15 +9,20 @@ use axum::{
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::Databases;
|
use crate::{config::Config, Databases};
|
||||||
|
|
||||||
mod wall;
|
mod wall;
|
||||||
|
|
||||||
pub fn router<S>(dbs: Arc<Databases>) -> Router<S> {
|
pub struct Api {
|
||||||
|
pub config: Config,
|
||||||
|
pub dbs: Arc<Databases>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn router<S>(api: Arc<Api>) -> Router<S> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/login", post(login_new))
|
.route("/login", post(login_new))
|
||||||
.route("/wall", get(wall::wall))
|
.route("/wall", get(wall::wall))
|
||||||
.with_state(dbs)
|
.with_state(api)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -35,7 +40,7 @@ enum NewUserResponse {
|
||||||
Error { message: String },
|
Error { message: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn login_new(dbs: State<Arc<Databases>>, params: Json<NewUserParams>) -> impl IntoResponse {
|
async fn login_new(api: State<Arc<Api>>, params: Json<NewUserParams>) -> impl IntoResponse {
|
||||||
if !(1..=32).contains(¶ms.nickname.len()) {
|
if !(1..=32).contains(¶ms.nickname.len()) {
|
||||||
return (
|
return (
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
|
@ -45,7 +50,7 @@ async fn login_new(dbs: State<Arc<Databases>>, params: Json<NewUserParams>) -> i
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
match dbs.login.new_user(params.0.nickname).await {
|
match api.dbs.login.new_user(params.0.nickname).await {
|
||||||
Ok(user_id) => (
|
Ok(user_id) => (
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(NewUserResponse::Ok {
|
Json(NewUserResponse::Ok {
|
||||||
|
|
|
@ -8,21 +8,30 @@ use axum::{
|
||||||
response::Response,
|
response::Response,
|
||||||
};
|
};
|
||||||
use eyre::{bail, Context, OptionExt};
|
use eyre::{bail, Context, OptionExt};
|
||||||
use schema::{Error, LoginRequest, LoginResponse, Online, Version, WallInfo};
|
use haku::value::Value;
|
||||||
|
use schema::{
|
||||||
|
ChunkInfo, Error, LoginRequest, LoginResponse, Notify, Online, Request, Version, WallInfo,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::select;
|
use tokio::{select, sync::mpsc, time::Instant};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
haku::{Haku, Limits},
|
||||||
login::database::LoginStatus,
|
login::database::LoginStatus,
|
||||||
wall::{Event, JoinError, Session},
|
schema::Vec2,
|
||||||
Databases,
|
wall::{
|
||||||
|
self, chunk_encoder::ChunkEncoder, chunk_iterator::ChunkIterator, ChunkPosition, JoinError,
|
||||||
|
SessionHandle, UserInit, Wall,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::Api;
|
||||||
|
|
||||||
mod schema;
|
mod schema;
|
||||||
|
|
||||||
pub async fn wall(State(dbs): State<Arc<Databases>>, ws: WebSocketUpgrade) -> Response {
|
pub async fn wall(State(api): State<Arc<Api>>, ws: WebSocketUpgrade) -> Response {
|
||||||
ws.on_upgrade(|ws| websocket(dbs, ws))
|
ws.on_upgrade(|ws| websocket(api, ws))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_message<T>(value: &T) -> Message
|
fn to_message<T>(value: &T) -> Message
|
||||||
|
@ -51,8 +60,8 @@ async fn recv_expect(ws: &mut WebSocket) -> eyre::Result<Message> {
|
||||||
.ok_or_eyre("connection closed unexpectedly")??)
|
.ok_or_eyre("connection closed unexpectedly")??)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn websocket(dbs: Arc<Databases>, mut ws: WebSocket) {
|
async fn websocket(api: Arc<Api>, mut ws: WebSocket) {
|
||||||
match fallible_websocket(dbs, &mut ws).await {
|
match fallible_websocket(api, &mut ws).await {
|
||||||
Ok(()) => (),
|
Ok(()) => (),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
_ = ws
|
_ = ws
|
||||||
|
@ -64,7 +73,7 @@ async fn websocket(dbs: Arc<Databases>, mut ws: WebSocket) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fallible_websocket(dbs: Arc<Databases>, ws: &mut WebSocket) -> eyre::Result<()> {
|
async fn fallible_websocket(api: Arc<Api>, ws: &mut WebSocket) -> eyre::Result<()> {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
let version = format!("{}-dev", env!("CARGO_PKG_VERSION"));
|
let version = format!("{}-dev", env!("CARGO_PKG_VERSION"));
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
|
@ -73,9 +82,10 @@ async fn fallible_websocket(dbs: Arc<Databases>, ws: &mut WebSocket) -> eyre::Re
|
||||||
ws.send(to_message(&Version { version })).await?;
|
ws.send(to_message(&Version { version })).await?;
|
||||||
|
|
||||||
let login_request: LoginRequest = from_message(&recv_expect(ws).await?)?;
|
let login_request: LoginRequest = from_message(&recv_expect(ws).await?)?;
|
||||||
let user_id = *login_request.user_id();
|
let user_id = login_request.user;
|
||||||
|
|
||||||
match dbs
|
match api
|
||||||
|
.dbs
|
||||||
.login
|
.login
|
||||||
.log_in(user_id)
|
.log_in(user_id)
|
||||||
.await
|
.await
|
||||||
|
@ -88,14 +98,24 @@ async fn fallible_websocket(dbs: Arc<Databases>, ws: &mut WebSocket) -> eyre::Re
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let user_info = api
|
||||||
|
.dbs
|
||||||
|
.login
|
||||||
|
.user_info(user_id)
|
||||||
|
.await
|
||||||
|
.context("cannot get user info")?
|
||||||
|
.ok_or_eyre("user seems to have vanished")?;
|
||||||
|
|
||||||
let wall_id = match login_request {
|
let wall_id = match login_request.wall {
|
||||||
LoginRequest::New { .. } => dbs.wall_broker.generate_id().await,
|
Some(wall) => wall,
|
||||||
LoginRequest::Join { wall, .. } => wall,
|
None => api.dbs.wall_broker.generate_id().await,
|
||||||
};
|
};
|
||||||
let wall = dbs.wall_broker.open(wall_id);
|
let open_wall = api.dbs.wall_broker.open(wall_id);
|
||||||
|
|
||||||
let mut session_handle = match wall.join(Session::new(user_id)) {
|
let session_handle = match open_wall
|
||||||
|
.wall
|
||||||
|
.join(wall::Session::new(user_id, login_request.init.clone()))
|
||||||
|
{
|
||||||
Ok(handle) => handle,
|
Ok(handle) => handle,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
ws.send(to_message(&match error {
|
ws.send(to_message(&match error {
|
||||||
|
@ -110,8 +130,8 @@ async fn fallible_websocket(dbs: Arc<Databases>, ws: &mut WebSocket) -> eyre::Re
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut users_online = vec![];
|
let mut users_online = vec![];
|
||||||
for online in wall.online() {
|
for online in open_wall.wall.online() {
|
||||||
let user_info = match dbs.login.user_info(online.user_id).await {
|
let user_info = match api.dbs.login.user_info(online.user_id).await {
|
||||||
Ok(Some(user_info)) => user_info,
|
Ok(Some(user_info)) => user_info,
|
||||||
Ok(None) | Err(_) => {
|
Ok(None) | Err(_) => {
|
||||||
error!(?online, "could not get info about online user");
|
error!(?online, "could not get info about online user");
|
||||||
|
@ -122,6 +142,9 @@ async fn fallible_websocket(dbs: Arc<Databases>, ws: &mut WebSocket) -> eyre::Re
|
||||||
session_id: online.session_id,
|
session_id: online.session_id,
|
||||||
nickname: user_info.nickname,
|
nickname: user_info.nickname,
|
||||||
cursor: online.cursor,
|
cursor: online.cursor,
|
||||||
|
init: UserInit {
|
||||||
|
brush: online.brush,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
let users_online = users_online;
|
let users_online = users_online;
|
||||||
|
@ -129,25 +152,278 @@ async fn fallible_websocket(dbs: Arc<Databases>, ws: &mut WebSocket) -> eyre::Re
|
||||||
ws.send(to_message(&LoginResponse::LoggedIn {
|
ws.send(to_message(&LoginResponse::LoggedIn {
|
||||||
wall: wall_id,
|
wall: wall_id,
|
||||||
wall_info: WallInfo {
|
wall_info: WallInfo {
|
||||||
chunk_size: wall.settings().chunk_size,
|
chunk_size: open_wall.wall.settings().chunk_size,
|
||||||
|
paint_area: open_wall.wall.settings().paint_area,
|
||||||
online: users_online,
|
online: users_online,
|
||||||
|
haku_limits: api.config.haku.clone(),
|
||||||
},
|
},
|
||||||
session_id: session_handle.session_id,
|
session_id: session_handle.session_id,
|
||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
loop {
|
open_wall.wall.event(wall::Event {
|
||||||
select! {
|
session_id: session_handle.session_id,
|
||||||
Some(message) = ws.recv() => {
|
kind: wall::EventKind::Join {
|
||||||
let kind = from_message(&message?)?;
|
nickname: user_info.nickname,
|
||||||
wall.event(Event { session_id: session_handle.session_id, kind });
|
init: login_request.init.clone(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Leave event is sent in SessionHandle's Drop implementation.
|
||||||
|
// This technically means that inbetween the user getting logged in and sending the Join event,
|
||||||
|
// we may end up losing the user and sending a Leave event, but Leave events are idempotent -
|
||||||
|
// they're only used to clean up the state of an existing user, but the user is not _required_
|
||||||
|
// to exist on clients already.
|
||||||
|
// ...Well, we'll see how much havoc that wreaks in practice. Especially without TypeScript
|
||||||
|
// to remind us about unexpected nulls.
|
||||||
|
|
||||||
|
SessionLoop::start(
|
||||||
|
open_wall.wall,
|
||||||
|
open_wall.chunk_encoder,
|
||||||
|
session_handle,
|
||||||
|
api.config.haku.clone(),
|
||||||
|
login_request.init.brush,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.event_loop(ws)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SessionLoop {
|
||||||
|
wall: Arc<Wall>,
|
||||||
|
chunk_encoder: Arc<ChunkEncoder>,
|
||||||
|
handle: SessionHandle,
|
||||||
|
render_commands_tx: mpsc::Sender<RenderCommand>,
|
||||||
|
viewport_chunks: ChunkIterator,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RenderCommand {
|
||||||
|
SetBrush { brush: String },
|
||||||
|
Plot { points: Vec<Vec2> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionLoop {
|
||||||
|
async fn start(
|
||||||
|
wall: Arc<Wall>,
|
||||||
|
chunk_encoder: Arc<ChunkEncoder>,
|
||||||
|
handle: SessionHandle,
|
||||||
|
limits: Limits,
|
||||||
|
brush: String,
|
||||||
|
) -> eyre::Result<Self> {
|
||||||
|
// Limit how many commands may come in _pretty darn hard_ because these can be really
|
||||||
|
// CPU-intensive.
|
||||||
|
// If this ends up dropping commands - it's your fault for trying to DoS my server!
|
||||||
|
let (render_commands_tx, render_commands_rx) = mpsc::channel(1);
|
||||||
|
|
||||||
|
render_commands_tx
|
||||||
|
.send(RenderCommand::SetBrush { brush })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// We spawn our own thread so as not to clog the tokio blocking thread pool with our
|
||||||
|
// rendering shenanigans.
|
||||||
|
std::thread::Builder::new()
|
||||||
|
.name(String::from("haku render thread"))
|
||||||
|
.spawn({
|
||||||
|
let wall = Arc::clone(&wall);
|
||||||
|
let chunk_encoder = Arc::clone(&chunk_encoder);
|
||||||
|
move || Self::render_thread(wall, chunk_encoder, limits, render_commands_rx)
|
||||||
|
})
|
||||||
|
.context("could not spawn render thread")?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
wall,
|
||||||
|
chunk_encoder,
|
||||||
|
handle,
|
||||||
|
render_commands_tx,
|
||||||
|
viewport_chunks: ChunkIterator::new(ChunkPosition::new(0, 0), ChunkPosition::new(0, 0)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn event_loop(&mut self, ws: &mut WebSocket) -> eyre::Result<()> {
|
||||||
|
loop {
|
||||||
|
select! {
|
||||||
|
Some(message) = ws.recv() => {
|
||||||
|
let request = from_message(&message?)?;
|
||||||
|
self.process_request(ws, request).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(wall_event) = self.handle.event_receiver.recv() => {
|
||||||
|
ws.send(to_message(&Notify::Wall { wall_event })).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
else => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_request(&mut self, ws: &mut WebSocket, request: Request) -> eyre::Result<()> {
|
||||||
|
match request {
|
||||||
|
Request::Wall { wall_event } => {
|
||||||
|
match &wall_event {
|
||||||
|
// This match only concerns itself with drawing-related events to offload
|
||||||
|
// all the evaluation and drawing work to this session's drawing thread.
|
||||||
|
wall::EventKind::Join { .. }
|
||||||
|
| wall::EventKind::Leave
|
||||||
|
| wall::EventKind::Cursor { .. } => (),
|
||||||
|
|
||||||
|
wall::EventKind::SetBrush { brush } => {
|
||||||
|
// SetBrush is not dropped because it is a very important event.
|
||||||
|
_ = self
|
||||||
|
.render_commands_tx
|
||||||
|
.send(RenderCommand::SetBrush {
|
||||||
|
brush: brush.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
wall::EventKind::Plot { points } => {
|
||||||
|
// We drop commands if we take too long to render instead of lagging
|
||||||
|
// the WebSocket thread.
|
||||||
|
// Theoretically this will yield much better responsiveness, but it _will_
|
||||||
|
// result in some visual glitches if we're getting bottlenecked.
|
||||||
|
_ = self.render_commands_tx.try_send(RenderCommand::Plot {
|
||||||
|
points: points.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.wall.event(wall::Event {
|
||||||
|
session_id: self.handle.session_id,
|
||||||
|
kind: wall_event,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(event) = session_handle.event_receiver.recv() => {
|
Request::Viewport {
|
||||||
ws.send(to_message(&event)).await?;
|
top_left,
|
||||||
|
bottom_right,
|
||||||
|
} => {
|
||||||
|
self.viewport_chunks = ChunkIterator::new(top_left, bottom_right);
|
||||||
|
self.send_chunks(ws).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
else => break,
|
Request::MoreChunks => {
|
||||||
|
self.send_chunks(ws).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_chunks(&mut self, ws: &mut WebSocket) -> eyre::Result<()> {
|
||||||
|
let mut chunk_infos = vec![];
|
||||||
|
let mut packet = vec![];
|
||||||
|
|
||||||
|
// Number of chunks iterated is limited to 300 per packet, so as not to let the client
|
||||||
|
// stall the server by sending in a huge viewport.
|
||||||
|
for _ in 0..300 {
|
||||||
|
if let Some(position) = self.viewport_chunks.next() {
|
||||||
|
if let Some(encoded) = self.chunk_encoder.encoded(position).await {
|
||||||
|
let offset = packet.len();
|
||||||
|
packet.extend_from_slice(&encoded);
|
||||||
|
chunk_infos.push(ChunkInfo {
|
||||||
|
position,
|
||||||
|
offset: u32::try_from(offset).context("packet too big")?,
|
||||||
|
length: u32::try_from(encoded.len()).context("chunk image too big")?,
|
||||||
|
});
|
||||||
|
|
||||||
|
// The actual number of chunks per packet is limited by the packet's size, which
|
||||||
|
// we don't want to be too big, to maintain responsiveness - the client will
|
||||||
|
// only request more chunks once per frame, so interactions still have time to
|
||||||
|
// execute. We cap it to 256KiB in hopes that noone has Internet slow enough for
|
||||||
|
// this to cause a disconnect.
|
||||||
|
if packet.len() >= 256 * 1024 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Length 0 indicates the server acknowledged the chunk, but it has no
|
||||||
|
// image data.
|
||||||
|
// This is used by clients to know that the chunk doesn't need downloading.
|
||||||
|
chunk_infos.push(ChunkInfo {
|
||||||
|
position,
|
||||||
|
offset: 0,
|
||||||
|
length: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.send(to_message(&Notify::Chunks {
|
||||||
|
chunks: chunk_infos,
|
||||||
|
}))
|
||||||
|
.await?;
|
||||||
|
ws.send(Message::Binary(packet)).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_thread(
|
||||||
|
wall: Arc<Wall>,
|
||||||
|
chunk_encoder: Arc<ChunkEncoder>,
|
||||||
|
limits: Limits,
|
||||||
|
mut commands: mpsc::Receiver<RenderCommand>,
|
||||||
|
) {
|
||||||
|
let mut haku = Haku::new(limits);
|
||||||
|
let mut brush_ok = false;
|
||||||
|
|
||||||
|
while let Some(command) = commands.blocking_recv() {
|
||||||
|
match command {
|
||||||
|
RenderCommand::SetBrush { brush } => {
|
||||||
|
brush_ok = haku.set_brush(&brush).is_ok();
|
||||||
|
}
|
||||||
|
RenderCommand::Plot { points } => {
|
||||||
|
if brush_ok {
|
||||||
|
if let Ok(value) = haku.eval_brush() {
|
||||||
|
for point in points {
|
||||||
|
// Ignore the result. It's better if we render _something_ rather
|
||||||
|
// than nothing.
|
||||||
|
_ = draw_to_chunks(&haku, value, point, &wall, &chunk_encoder);
|
||||||
|
}
|
||||||
|
haku.reset_vm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_to_chunks(
|
||||||
|
haku: &Haku,
|
||||||
|
value: Value,
|
||||||
|
center: Vec2,
|
||||||
|
wall: &Wall,
|
||||||
|
chunk_encoder: &ChunkEncoder,
|
||||||
|
) -> eyre::Result<()> {
|
||||||
|
let settings = wall.settings();
|
||||||
|
|
||||||
|
let chunk_size = settings.chunk_size as f32;
|
||||||
|
let paint_area = settings.paint_area as f32;
|
||||||
|
|
||||||
|
let left = center.x - paint_area / 2.0;
|
||||||
|
let top = center.y - paint_area / 2.0;
|
||||||
|
|
||||||
|
let left_chunk = f32::floor(left / chunk_size) as i32;
|
||||||
|
let top_chunk = f32::floor(top / chunk_size) as i32;
|
||||||
|
let right_chunk = f32::ceil((left + paint_area) / chunk_size) as i32;
|
||||||
|
let bottom_chunk = f32::ceil((top + paint_area) / chunk_size) as i32;
|
||||||
|
for chunk_y in top_chunk..bottom_chunk {
|
||||||
|
for chunk_x in left_chunk..right_chunk {
|
||||||
|
let x = f32::floor(-chunk_x as f32 * chunk_size + center.x);
|
||||||
|
let y = f32::floor(-chunk_y as f32 * chunk_size + center.y);
|
||||||
|
let chunk_ref = wall.get_or_create_chunk(ChunkPosition::new(chunk_x, chunk_y));
|
||||||
|
let mut chunk = chunk_ref.blocking_lock();
|
||||||
|
haku.render_value(&mut chunk.pixmap, value, Vec2 { x, y })?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for chunk_y in top_chunk..bottom_chunk {
|
||||||
|
for chunk_x in left_chunk..right_chunk {
|
||||||
|
chunk_encoder.invalidate_blocking(ChunkPosition::new(chunk_x, chunk_y))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use crate::{
|
use crate::{
|
||||||
login::UserId,
|
login::UserId,
|
||||||
schema::Vec2,
|
schema::Vec2,
|
||||||
wall::{self, SessionId, WallId},
|
wall::{self, ChunkPosition, SessionId, UserInit, WallId},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
|
@ -19,23 +19,12 @@ pub struct Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||||
#[serde(
|
#[serde(rename_all = "camelCase")]
|
||||||
tag = "login",
|
pub struct LoginRequest {
|
||||||
rename_all = "camelCase",
|
pub user: UserId,
|
||||||
rename_all_fields = "camelCase"
|
/// If null, a new wall is created.
|
||||||
)]
|
pub wall: Option<WallId>,
|
||||||
pub enum LoginRequest {
|
pub init: UserInit,
|
||||||
New { user: UserId },
|
|
||||||
Join { user: UserId, wall: WallId },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LoginRequest {
|
|
||||||
pub fn user_id(&self) -> &UserId {
|
|
||||||
match self {
|
|
||||||
LoginRequest::New { user } => user,
|
|
||||||
LoginRequest::Join { user, .. } => user,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
@ -44,12 +33,15 @@ pub struct Online {
|
||||||
pub session_id: SessionId,
|
pub session_id: SessionId,
|
||||||
pub nickname: String,
|
pub nickname: String,
|
||||||
pub cursor: Option<Vec2>,
|
pub cursor: Option<Vec2>,
|
||||||
|
pub init: UserInit,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct WallInfo {
|
pub struct WallInfo {
|
||||||
pub chunk_size: u32,
|
pub chunk_size: u32,
|
||||||
|
pub paint_area: u32,
|
||||||
|
pub haku_limits: crate::haku::Limits,
|
||||||
pub online: Vec<Online>,
|
pub online: Vec<Online>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,3 +60,40 @@ pub enum LoginResponse {
|
||||||
UserDoesNotExist,
|
UserDoesNotExist,
|
||||||
TooManySessions,
|
TooManySessions,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(
|
||||||
|
tag = "request",
|
||||||
|
rename_all = "camelCase",
|
||||||
|
rename_all_fields = "camelCase"
|
||||||
|
)]
|
||||||
|
pub enum Request {
|
||||||
|
Wall {
|
||||||
|
wall_event: wall::EventKind,
|
||||||
|
},
|
||||||
|
|
||||||
|
Viewport {
|
||||||
|
top_left: ChunkPosition,
|
||||||
|
bottom_right: ChunkPosition,
|
||||||
|
},
|
||||||
|
|
||||||
|
MoreChunks,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct ChunkInfo {
|
||||||
|
pub position: ChunkPosition,
|
||||||
|
pub offset: u32,
|
||||||
|
pub length: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(
|
||||||
|
tag = "notify",
|
||||||
|
rename_all = "camelCase",
|
||||||
|
rename_all_fields = "camelCase"
|
||||||
|
)]
|
||||||
|
pub enum Notify {
|
||||||
|
Wall { wall_event: wall::Event },
|
||||||
|
Chunks { chunks: Vec<ChunkInfo> },
|
||||||
|
}
|
||||||
|
|
|
@ -5,4 +5,5 @@ use crate::wall;
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub wall: wall::Settings,
|
pub wall: wall::Settings,
|
||||||
|
pub haku: crate::haku::Limits,
|
||||||
}
|
}
|
||||||
|
|
163
crates/rkgk/src/haku.rs
Normal file
163
crates/rkgk/src/haku.rs
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
//! High-level wrapper for Haku.
|
||||||
|
|
||||||
|
// TODO: This should be used as the basis for haku-wasm as well as haku tests in the future to
|
||||||
|
// avoid duplicating code.
|
||||||
|
|
||||||
|
use eyre::{bail, Context, OptionExt};
|
||||||
|
use haku::{
|
||||||
|
bytecode::{Chunk, Defs, DefsImage},
|
||||||
|
compiler::{Compiler, Source},
|
||||||
|
render::{tiny_skia::Pixmap, Renderer, RendererLimits},
|
||||||
|
sexp::{Ast, Parser},
|
||||||
|
system::{ChunkId, System, SystemImage},
|
||||||
|
value::{BytecodeLoc, Closure, FunctionName, Ref, Value},
|
||||||
|
vm::{Vm, VmImage, VmLimits},
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::schema::Vec2;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
// NOTE: For serialization, this struct does _not_ have serde(rename_all = "camelCase") on it,
|
||||||
|
// because we do some dynamic typing magic over on the JavaScript side to automatically call all
|
||||||
|
// the appropriate functions for setting these limits on the client side.
|
||||||
|
pub struct Limits {
|
||||||
|
pub max_chunks: usize,
|
||||||
|
pub max_defs: usize,
|
||||||
|
pub ast_capacity: usize,
|
||||||
|
pub chunk_capacity: usize,
|
||||||
|
pub stack_capacity: usize,
|
||||||
|
pub call_stack_capacity: usize,
|
||||||
|
pub ref_capacity: usize,
|
||||||
|
pub fuel: usize,
|
||||||
|
pub pixmap_stack_capacity: usize,
|
||||||
|
pub transform_stack_capacity: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Haku {
|
||||||
|
limits: Limits,
|
||||||
|
|
||||||
|
system: System,
|
||||||
|
system_image: SystemImage,
|
||||||
|
defs: Defs,
|
||||||
|
defs_image: DefsImage,
|
||||||
|
vm: Vm,
|
||||||
|
vm_image: VmImage,
|
||||||
|
|
||||||
|
brush: Option<ChunkId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Haku {
|
||||||
|
pub fn new(limits: Limits) -> Self {
|
||||||
|
let system = System::new(limits.max_chunks);
|
||||||
|
let defs = Defs::new(limits.max_defs);
|
||||||
|
let vm = Vm::new(
|
||||||
|
&defs,
|
||||||
|
&VmLimits {
|
||||||
|
stack_capacity: limits.stack_capacity,
|
||||||
|
call_stack_capacity: limits.call_stack_capacity,
|
||||||
|
ref_capacity: limits.ref_capacity,
|
||||||
|
fuel: limits.fuel,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let system_image = system.image();
|
||||||
|
let defs_image = defs.image();
|
||||||
|
let vm_image = vm.image();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
limits,
|
||||||
|
system,
|
||||||
|
system_image,
|
||||||
|
defs,
|
||||||
|
defs_image,
|
||||||
|
vm,
|
||||||
|
vm_image,
|
||||||
|
brush: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.system.restore_image(&self.system_image);
|
||||||
|
self.defs.restore_image(&self.defs_image);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_brush(&mut self, code: &str) -> eyre::Result<()> {
|
||||||
|
self.reset();
|
||||||
|
|
||||||
|
let ast = Ast::new(self.limits.ast_capacity);
|
||||||
|
let mut parser = Parser::new(ast, code);
|
||||||
|
let root = haku::sexp::parse_toplevel(&mut parser);
|
||||||
|
let ast = parser.ast;
|
||||||
|
|
||||||
|
let src = Source {
|
||||||
|
code,
|
||||||
|
ast: &ast,
|
||||||
|
system: &self.system,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut chunk = Chunk::new(self.limits.chunk_capacity)
|
||||||
|
.expect("chunk capacity must be representable as a 16-bit number");
|
||||||
|
let mut compiler = Compiler::new(&mut self.defs, &mut chunk);
|
||||||
|
haku::compiler::compile_expr(&mut compiler, &src, root)
|
||||||
|
.context("failed to compile the chunk")?;
|
||||||
|
|
||||||
|
if !compiler.diagnostics.is_empty() {
|
||||||
|
bail!("diagnostics were emitted");
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunk_id = self.system.add_chunk(chunk).context("too many chunks")?;
|
||||||
|
self.brush = Some(chunk_id);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval_brush(&mut self) -> eyre::Result<Value> {
|
||||||
|
let brush = self
|
||||||
|
.brush
|
||||||
|
.ok_or_eyre("brush is not compiled and ready to be used")?;
|
||||||
|
|
||||||
|
let closure_id = self
|
||||||
|
.vm
|
||||||
|
.create_ref(Ref::Closure(Closure {
|
||||||
|
start: BytecodeLoc {
|
||||||
|
chunk_id: brush,
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
name: FunctionName::Anonymous,
|
||||||
|
param_count: 0,
|
||||||
|
captures: vec![],
|
||||||
|
}))
|
||||||
|
.context("not enough ref slots to create initial closure")?;
|
||||||
|
|
||||||
|
let scribble = self
|
||||||
|
.vm
|
||||||
|
.run(&self.system, closure_id)
|
||||||
|
.context("an exception occurred while evaluating the scribble")?;
|
||||||
|
|
||||||
|
Ok(scribble)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_value(
|
||||||
|
&self,
|
||||||
|
pixmap: &mut Pixmap,
|
||||||
|
value: Value,
|
||||||
|
translation: Vec2,
|
||||||
|
) -> eyre::Result<()> {
|
||||||
|
let mut renderer = Renderer::new(
|
||||||
|
pixmap,
|
||||||
|
&RendererLimits {
|
||||||
|
pixmap_stack_capacity: self.limits.pixmap_stack_capacity,
|
||||||
|
transform_stack_capacity: self.limits.transform_stack_capacity,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
renderer.translate(translation.x, translation.y);
|
||||||
|
let result = renderer.render(&self.vm, value);
|
||||||
|
|
||||||
|
result.context("an exception occurred while rendering the scribble")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_vm(&mut self) {
|
||||||
|
self.vm.restore_image(&self.vm_image);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ use base64::Engine;
|
||||||
pub fn serialize(f: &mut fmt::Formatter<'_>, prefix: &str, bytes: &[u8; 32]) -> fmt::Result {
|
pub fn serialize(f: &mut fmt::Formatter<'_>, prefix: &str, bytes: &[u8; 32]) -> fmt::Result {
|
||||||
f.write_str(prefix)?;
|
f.write_str(prefix)?;
|
||||||
let mut buffer = [b'0'; 43];
|
let mut buffer = [b'0'; 43];
|
||||||
base64::engine::general_purpose::STANDARD_NO_PAD
|
base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||||
.encode_slice(bytes, &mut buffer)
|
.encode_slice(bytes, &mut buffer)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
f.write_str(std::str::from_utf8(&buffer).unwrap())?;
|
f.write_str(std::str::from_utf8(&buffer).unwrap())?;
|
||||||
|
@ -17,7 +17,7 @@ pub struct InvalidId;
|
||||||
pub fn deserialize(s: &str, prefix: &str) -> Result<[u8; 32], InvalidId> {
|
pub fn deserialize(s: &str, prefix: &str) -> Result<[u8; 32], InvalidId> {
|
||||||
let mut bytes = [0; 32];
|
let mut bytes = [0; 32];
|
||||||
let b64 = s.strip_prefix(prefix).ok_or(InvalidId)?;
|
let b64 = s.strip_prefix(prefix).ok_or(InvalidId)?;
|
||||||
let decoded = base64::engine::general_purpose::STANDARD_NO_PAD
|
let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||||
.decode_slice(b64, &mut bytes)
|
.decode_slice(b64, &mut bytes)
|
||||||
.map_err(|_| InvalidId)?;
|
.map_err(|_| InvalidId)?;
|
||||||
if decoded != bytes.len() {
|
if decoded != bytes.len() {
|
||||||
|
|
|
@ -4,6 +4,7 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use api::Api;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use copy_dir::copy_dir;
|
use copy_dir::copy_dir;
|
||||||
|
@ -16,6 +17,7 @@ use tracing_subscriber::fmt::format::FmtSpan;
|
||||||
mod api;
|
mod api;
|
||||||
mod binary;
|
mod binary;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod haku;
|
||||||
mod id;
|
mod id;
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
mod live_reload;
|
mod live_reload;
|
||||||
|
@ -24,6 +26,11 @@ pub mod schema;
|
||||||
mod serialization;
|
mod serialization;
|
||||||
mod wall;
|
mod wall;
|
||||||
|
|
||||||
|
#[cfg(feature = "memory-profiling")]
|
||||||
|
#[global_allocator]
|
||||||
|
static GLOBAL_ALLOCATOR: tracy_client::ProfiledAllocator<std::alloc::System> =
|
||||||
|
tracy_client::ProfiledAllocator::new(std::alloc::System, 100);
|
||||||
|
|
||||||
struct Paths<'a> {
|
struct Paths<'a> {
|
||||||
target_dir: &'a Path,
|
target_dir: &'a Path,
|
||||||
database_dir: &'a Path,
|
database_dir: &'a Path,
|
||||||
|
@ -81,13 +88,15 @@ async fn fallible_main() -> eyre::Result<()> {
|
||||||
build(&paths)?;
|
build(&paths)?;
|
||||||
let dbs = Arc::new(database(&config, &paths)?);
|
let dbs = Arc::new(database(&config, &paths)?);
|
||||||
|
|
||||||
|
let api = Arc::new(Api { config, dbs });
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route_service(
|
.route_service(
|
||||||
"/",
|
"/",
|
||||||
ServeFile::new(paths.target_dir.join("static/index.html")),
|
ServeFile::new(paths.target_dir.join("static/index.html")),
|
||||||
)
|
)
|
||||||
.nest_service("/static", ServeDir::new(paths.target_dir.join("static")))
|
.nest_service("/static", ServeDir::new(paths.target_dir.join("static")))
|
||||||
.nest("/api", api::router(dbs.clone()));
|
.nest("/api", api::router(api));
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
let app = app.nest("/dev/live-reload", live_reload::router());
|
let app = app.nest("/dev/live-reload", live_reload::router());
|
||||||
|
@ -103,6 +112,9 @@ async fn fallible_main() -> eyre::Result<()> {
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
#[cfg(feature = "memory-profiling")]
|
||||||
|
let _client = tracy_client::Client::start();
|
||||||
|
|
||||||
color_eyre::install().unwrap();
|
color_eyre::install().unwrap();
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_span_events(FmtSpan::ACTIVE)
|
.with_span_events(FmtSpan::ACTIVE)
|
||||||
|
|
|
@ -13,10 +13,13 @@ use haku::render::tiny_skia::Pixmap;
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::{broadcast, Mutex};
|
use tokio::sync::{broadcast, Mutex};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
use crate::{id, login::UserId, schema::Vec2, serialization::DeserializeFromStr};
|
use crate::{id, login::UserId, schema::Vec2, serialization::DeserializeFromStr};
|
||||||
|
|
||||||
pub mod broker;
|
pub mod broker;
|
||||||
|
pub mod chunk_encoder;
|
||||||
|
pub mod chunk_iterator;
|
||||||
|
|
||||||
pub use broker::Broker;
|
pub use broker::Broker;
|
||||||
|
|
||||||
|
@ -79,8 +82,20 @@ impl fmt::Display for InvalidWallId {
|
||||||
|
|
||||||
impl Error for InvalidWallId {}
|
impl Error for InvalidWallId {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||||
|
pub struct ChunkPosition {
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChunkPosition {
|
||||||
|
pub fn new(x: i32, y: i32) -> Self {
|
||||||
|
Self { x, y }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Chunk {
|
pub struct Chunk {
|
||||||
pixmap: Pixmap,
|
pub pixmap: Pixmap,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Chunk {
|
impl Chunk {
|
||||||
|
@ -95,13 +110,23 @@ impl Chunk {
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub max_chunks: usize,
|
pub max_chunks: usize,
|
||||||
pub max_sessions: usize,
|
pub max_sessions: usize,
|
||||||
|
pub paint_area: u32,
|
||||||
pub chunk_size: u32,
|
pub chunk_size: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Settings {
|
||||||
|
pub fn chunk_at(&self, position: Vec2) -> ChunkPosition {
|
||||||
|
ChunkPosition::new(
|
||||||
|
f32::floor(position.x / self.chunk_size as f32) as i32,
|
||||||
|
f32::floor(position.y / self.chunk_size as f32) as i32,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Wall {
|
pub struct Wall {
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
|
|
||||||
chunks: DashMap<(i32, i32), Arc<Mutex<Chunk>>>,
|
chunks: DashMap<ChunkPosition, Arc<Mutex<Chunk>>>,
|
||||||
|
|
||||||
sessions: DashMap<SessionId, Session>,
|
sessions: DashMap<SessionId, Session>,
|
||||||
session_id_counter: AtomicU32,
|
session_id_counter: AtomicU32,
|
||||||
|
@ -109,9 +134,17 @@ pub struct Wall {
|
||||||
event_sender: broadcast::Sender<Event>,
|
event_sender: broadcast::Sender<Event>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UserInit {
|
||||||
|
// Provide a brush upon initialization, so that the user always has a valid brush set.
|
||||||
|
pub brush: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
pub user_id: UserId,
|
pub user_id: UserId,
|
||||||
pub cursor: Option<Vec2>,
|
pub cursor: Option<Vec2>,
|
||||||
|
pub brush: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SessionHandle {
|
pub struct SessionHandle {
|
||||||
|
@ -134,6 +167,9 @@ pub struct Event {
|
||||||
rename_all_fields = "camelCase"
|
rename_all_fields = "camelCase"
|
||||||
)]
|
)]
|
||||||
pub enum EventKind {
|
pub enum EventKind {
|
||||||
|
Join { nickname: String, init: UserInit },
|
||||||
|
Leave,
|
||||||
|
|
||||||
Cursor { position: Vec2 },
|
Cursor { position: Vec2 },
|
||||||
|
|
||||||
SetBrush { brush: String },
|
SetBrush { brush: String },
|
||||||
|
@ -146,6 +182,7 @@ pub struct Online {
|
||||||
pub session_id: SessionId,
|
pub session_id: SessionId,
|
||||||
pub user_id: UserId,
|
pub user_id: UserId,
|
||||||
pub cursor: Option<Vec2>,
|
pub cursor: Option<Vec2>,
|
||||||
|
pub brush: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Wall {
|
impl Wall {
|
||||||
|
@ -163,11 +200,11 @@ impl Wall {
|
||||||
&self.settings
|
&self.settings
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_chunk(&self, at: (i32, i32)) -> Option<Arc<Mutex<Chunk>>> {
|
pub fn get_chunk(&self, at: ChunkPosition) -> Option<Arc<Mutex<Chunk>>> {
|
||||||
self.chunks.get(&at).map(|chunk| Arc::clone(&chunk))
|
self.chunks.get(&at).map(|chunk| Arc::clone(&chunk))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_or_create_chunk(&self, at: (i32, i32)) -> Arc<Mutex<Chunk>> {
|
pub fn get_or_create_chunk(&self, at: ChunkPosition) -> Arc<Mutex<Chunk>> {
|
||||||
Arc::clone(
|
Arc::clone(
|
||||||
&self
|
&self
|
||||||
.chunks
|
.chunks
|
||||||
|
@ -198,6 +235,7 @@ impl Wall {
|
||||||
session_id: *r.key(),
|
session_id: *r.key(),
|
||||||
user_id: r.user_id,
|
user_id: r.user_id,
|
||||||
cursor: r.value().cursor,
|
cursor: r.value().cursor,
|
||||||
|
brush: r.value().brush.clone(),
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
@ -205,12 +243,17 @@ impl Wall {
|
||||||
pub fn event(&self, event: Event) {
|
pub fn event(&self, event: Event) {
|
||||||
if let Some(mut session) = self.sessions.get_mut(&event.session_id) {
|
if let Some(mut session) = self.sessions.get_mut(&event.session_id) {
|
||||||
match &event.kind {
|
match &event.kind {
|
||||||
EventKind::SetBrush { brush } => {}
|
// Join and Leave are events that only get broadcasted through the wall such that
|
||||||
|
// all users get them. We don't need to react to them in any way.
|
||||||
|
EventKind::Join { .. } | EventKind::Leave => (),
|
||||||
|
|
||||||
EventKind::Cursor { position } => {
|
EventKind::Cursor { position } => {
|
||||||
session.cursor = Some(*position);
|
session.cursor = Some(*position);
|
||||||
}
|
}
|
||||||
EventKind::Plot { points } => {}
|
|
||||||
|
// Drawing events are handled by the owner session's thread to make drawing as
|
||||||
|
// parallel as possible.
|
||||||
|
EventKind::SetBrush { .. } | EventKind::Plot { .. } => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,10 +262,11 @@ impl Wall {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
pub fn new(user_id: UserId) -> Self {
|
pub fn new(user_id: UserId, user_init: UserInit) -> Self {
|
||||||
Self {
|
Self {
|
||||||
user_id,
|
user_id,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
|
brush: user_init.brush,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -231,6 +275,10 @@ impl Drop for SessionHandle {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some(wall) = self.wall.upgrade() {
|
if let Some(wall) = self.wall.upgrade() {
|
||||||
wall.sessions.remove(&self.session_id);
|
wall.sessions.remove(&self.session_id);
|
||||||
|
wall.event(Event {
|
||||||
|
session_id: self.session_id,
|
||||||
|
kind: EventKind::Leave,
|
||||||
|
});
|
||||||
// After the session is removed, the wall will be garbage collected later.
|
// After the session is removed, the wall will be garbage collected later.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -240,7 +288,3 @@ pub enum JoinError {
|
||||||
TooManyCurrentSessions,
|
TooManyCurrentSessions,
|
||||||
IdsExhausted,
|
IdsExhausted,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum EventError {
|
|
||||||
DeadSession,
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ use rand_chacha::ChaCha20Rng;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use super::{Settings, Wall, WallId};
|
use super::{chunk_encoder::ChunkEncoder, Settings, Wall, WallId};
|
||||||
|
|
||||||
/// The broker is the main way to access wall data.
|
/// The broker is the main way to access wall data.
|
||||||
///
|
///
|
||||||
|
@ -18,8 +18,10 @@ pub struct Broker {
|
||||||
rng: Mutex<ChaCha20Rng>,
|
rng: Mutex<ChaCha20Rng>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct OpenWall {
|
#[derive(Clone)]
|
||||||
wall: Arc<Wall>,
|
pub struct OpenWall {
|
||||||
|
pub wall: Arc<Wall>,
|
||||||
|
pub chunk_encoder: Arc<ChunkEncoder>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Broker {
|
impl Broker {
|
||||||
|
@ -39,15 +41,14 @@ impl Broker {
|
||||||
WallId::new(&mut *rng)
|
WallId::new(&mut *rng)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open(&self, wall_id: WallId) -> Arc<Wall> {
|
pub fn open(&self, wall_id: WallId) -> OpenWall {
|
||||||
Arc::clone(
|
let wall = Arc::new(Wall::new(self.wall_settings));
|
||||||
&self
|
self.open_walls
|
||||||
.open_walls
|
.entry(wall_id)
|
||||||
.entry(wall_id)
|
.or_insert_with(|| OpenWall {
|
||||||
.or_insert_with(|| OpenWall {
|
chunk_encoder: Arc::new(ChunkEncoder::start(Arc::clone(&wall))),
|
||||||
wall: Arc::new(Wall::new(self.wall_settings)),
|
wall,
|
||||||
})
|
})
|
||||||
.wall,
|
.clone()
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
104
crates/rkgk/src/wall/chunk_encoder.rs
Normal file
104
crates/rkgk/src/wall/chunk_encoder.rs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
|
||||||
|
use super::{ChunkPosition, Wall};
|
||||||
|
|
||||||
|
/// Service which encodes chunks to WebP images and caches them in an LRU fashion.
|
||||||
|
pub struct ChunkEncoder {
|
||||||
|
commands_tx: mpsc::Sender<Command>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Command {
|
||||||
|
GetEncoded {
|
||||||
|
chunk: ChunkPosition,
|
||||||
|
reply: oneshot::Sender<Option<Arc<[u8]>>>,
|
||||||
|
},
|
||||||
|
|
||||||
|
Invalidate {
|
||||||
|
chunk: ChunkPosition,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChunkEncoder {
|
||||||
|
pub fn start(wall: Arc<Wall>) -> Self {
|
||||||
|
let (commands_tx, commands_rx) = mpsc::channel(32);
|
||||||
|
|
||||||
|
tokio::spawn(Self::service(wall, commands_rx));
|
||||||
|
|
||||||
|
Self { commands_tx }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn encoded(&self, chunk: ChunkPosition) -> Option<Arc<[u8]>> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
self.commands_tx
|
||||||
|
.send(Command::GetEncoded { chunk, reply: tx })
|
||||||
|
.await
|
||||||
|
.ok()?;
|
||||||
|
rx.await.ok().flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn invalidate(&self, chunk: ChunkPosition) {
|
||||||
|
_ = self.commands_tx.send(Command::Invalidate { chunk }).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invalidate_blocking(&self, chunk: ChunkPosition) {
|
||||||
|
_ = self
|
||||||
|
.commands_tx
|
||||||
|
.blocking_send(Command::Invalidate { chunk });
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn encode(wall: &Wall, chunk: ChunkPosition) -> Option<Arc<[u8]>> {
|
||||||
|
let pixmap = {
|
||||||
|
// Clone out the pixmap to avoid unnecessary chunk mutex contention while the
|
||||||
|
// chunk is being encoded.
|
||||||
|
let chunk_ref = wall.get_chunk(chunk)?;
|
||||||
|
let chunk = chunk_ref.lock().await;
|
||||||
|
chunk.pixmap.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let image = tokio::task::spawn_blocking(move || {
|
||||||
|
let webp = webp::Encoder::new(
|
||||||
|
pixmap.data(),
|
||||||
|
webp::PixelLayout::Rgba,
|
||||||
|
pixmap.width(),
|
||||||
|
pixmap.height(),
|
||||||
|
);
|
||||||
|
// NOTE: There's an unnecessary copy here. Wonder if that kills performance much.
|
||||||
|
webp.encode_lossless().to_vec()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
Some(Arc::from(image))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn service(wall: Arc<Wall>, mut commands_rx: mpsc::Receiver<Command>) {
|
||||||
|
let mut encoded_lru: IndexMap<ChunkPosition, Option<Arc<[u8]>>> = IndexMap::new();
|
||||||
|
|
||||||
|
while let Some(command) = commands_rx.recv().await {
|
||||||
|
match command {
|
||||||
|
Command::GetEncoded { chunk, reply } => {
|
||||||
|
if let Some(encoded) = encoded_lru.get(&chunk) {
|
||||||
|
_ = reply.send(encoded.clone())
|
||||||
|
} else {
|
||||||
|
let encoded = Self::encode(&wall, chunk).await;
|
||||||
|
// TODO: Make this capacity configurable.
|
||||||
|
// 598 is chosen because under the default configuration, it would
|
||||||
|
// correspond to roughly two 3840x2160 displays.
|
||||||
|
if encoded_lru.len() >= 598 {
|
||||||
|
encoded_lru.shift_remove_index(0);
|
||||||
|
}
|
||||||
|
encoded_lru.insert(chunk, encoded.clone());
|
||||||
|
_ = reply.send(encoded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Command::Invalidate { chunk } => {
|
||||||
|
encoded_lru.shift_remove(&chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
crates/rkgk/src/wall/chunk_iterator.rs
Normal file
37
crates/rkgk/src/wall/chunk_iterator.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
use super::ChunkPosition;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct ChunkIterator {
|
||||||
|
cursor: ChunkPosition,
|
||||||
|
left: i32,
|
||||||
|
bottom_right: ChunkPosition,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChunkIterator {
|
||||||
|
pub fn new(top_left: ChunkPosition, bottom_right: ChunkPosition) -> Self {
|
||||||
|
Self {
|
||||||
|
cursor: top_left,
|
||||||
|
left: top_left.x,
|
||||||
|
bottom_right,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for ChunkIterator {
|
||||||
|
type Item = ChunkPosition;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let position = self.cursor;
|
||||||
|
|
||||||
|
self.cursor.x += 1;
|
||||||
|
if self.cursor.y > self.bottom_right.y {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if self.cursor.x > self.bottom_right.x {
|
||||||
|
self.cursor.x = self.left;
|
||||||
|
self.cursor.y += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(position)
|
||||||
|
}
|
||||||
|
}
|
52
docs/haku.dj
Normal file
52
docs/haku.dj
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# haku
|
||||||
|
|
||||||
|
Haku is a little scripting language used by rakugaki for programming brushes.
|
||||||
|
Here's a brief tour of the language.
|
||||||
|
|
||||||
|
## Your brush
|
||||||
|
|
||||||
|
Your brush is a piece of code that describes what's to be drawn on the wall.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```haku
|
||||||
|
(stroke
|
||||||
|
8
|
||||||
|
(rgba 0 0 0 1)
|
||||||
|
(vec 0 0))
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the simplest brush you can write.
|
||||||
|
It demonstrates a few things:
|
||||||
|
|
||||||
|
- The brush's task is to produce a description of what's to be drawn.
|
||||||
|
Brushes produce *scribbles* - commands that instruct rakugaki draw something on the wall.
|
||||||
|
|
||||||
|
- This brush produces the `(stroke)` scribble.
|
||||||
|
This scribble is composed out of three things:
|
||||||
|
|
||||||
|
- The stroke thickness - in this case `8`.
|
||||||
|
- The stroke color - in this case `(rgba 0 0 0 1)`.
|
||||||
|
Note that unlike most drawing programs, rakugaki brushes represent color channels with decimal numbers from 0 to 1, rather than integers from 0 to 255.
|
||||||
|
- The shape to draw - in this case a `(vec 0 0)`.
|
||||||
|
|
||||||
|
- Vectors are aggregations of four generic decimal numbers, most often used to represent positions in the wall's Cartesian coordinate space.
|
||||||
|
Although vectors are mathematically not the same as points, brushes always execute in a coordinate space relative to where you want to draw with the brush, so a separate `(point)` type isn't needed.
|
||||||
|
|
||||||
|
- Vectors in haku are four-dimensional, but the wall is two-dimensional, so the extra dimensions are discarded when drawing to the wall.
|
||||||
|
|
||||||
|
- haku permits constructing vectors from zero two four values - from `(vec)`, up to `(vec x y w h)`.
|
||||||
|
Any values that you leave out end up being zero.
|
||||||
|
|
||||||
|
- Note that a brush can only produce *one* scribble - this is because scribbles may be composed together using lists (described later.)
|
||||||
|
|
||||||
|
I highly recommend that you play around with the brush to get a feel for editing haku code!
|
||||||
|
|
||||||
|
## Limits
|
||||||
|
|
||||||
|
The wall is infinite, but your brush may only draw in a small area around your cursor (~500 pixels.)
|
||||||
|
Drawing outside this area may result in pixels getting dropped in ugly ways, but it can also be used to your advantage in order to produce cool glitch art.
|
||||||
|
|
||||||
|
Additionally, haku code has some pretty strong limitations on what it can do.
|
||||||
|
It cannot be too big, it cannot execute for too long, and it cannot consume too much memory.
|
||||||
|
It does not have access to the world outside the wall.
|
71
rkgk.toml
71
rkgk.toml
|
@ -1,5 +1,76 @@
|
||||||
[wall]
|
[wall]
|
||||||
|
|
||||||
|
# The settings below control the creation of new walls.
|
||||||
|
|
||||||
|
# The maximum number of chunks on a wall.
|
||||||
|
# It is recommended to cap this to something reasonable so that users can't trash the server's
|
||||||
|
# disk space very easily.
|
||||||
max_chunks = 65536
|
max_chunks = 65536
|
||||||
|
|
||||||
|
# Maximum concurrent sessions connected to a wall.
|
||||||
|
# Note that a single user can have multiple sessions at a time.
|
||||||
max_sessions = 128
|
max_sessions = 128
|
||||||
|
|
||||||
|
# The size of chunks.
|
||||||
|
# Choosing an appropriate size for chunks is a tradeoff between performance and disk space - 168 is
|
||||||
|
# chosen as a reasonable default
|
||||||
chunk_size = 168
|
chunk_size = 168
|
||||||
|
|
||||||
|
# The size of the area that can be drawn over by a brush, in pixels.
|
||||||
|
# The larger this area, the more CPU-expensive brushes get overall, but the larger the image a brush
|
||||||
|
# can produce.
|
||||||
|
paint_area = 504
|
||||||
|
|
||||||
|
[haku]
|
||||||
|
|
||||||
|
# The settings below control the Haku runtime on the server side.
|
||||||
|
# Technically clients may override these settings with some hackery, but then the server may not
|
||||||
|
# register changes they make to the canvas.
|
||||||
|
|
||||||
|
# Maximum amount of source code chunks.
|
||||||
|
# This should be at least 2, to allow for loading in a standard library chunk.
|
||||||
|
max_chunks = 2
|
||||||
|
|
||||||
|
# Maximum amount of defs across all source code chunks.
|
||||||
|
max_defs = 256
|
||||||
|
|
||||||
|
# Maximum amount of AST nodes in a single parse.
|
||||||
|
ast_capacity = 1024
|
||||||
|
|
||||||
|
# Maximum size of a bytecode chunk.
|
||||||
|
# This must be <= 65536 due to bytecode limitations - offsets are stored as 16-bit integers.
|
||||||
|
chunk_capacity = 65536
|
||||||
|
|
||||||
|
# Maximum size of the value stack.
|
||||||
|
# This defines how many local variables and temporary values can be in scope at a given moment.
|
||||||
|
# Effectively, this limits how deep and complex a single expression can get.
|
||||||
|
stack_capacity = 1024
|
||||||
|
|
||||||
|
# Maximum call stack capacity.
|
||||||
|
# This defines how much code is allowed to call itself recursively.
|
||||||
|
call_stack_capacity = 256
|
||||||
|
|
||||||
|
# Maximum amount of refs.
|
||||||
|
# Refs are big, reused, unique values that do not fit on the value stack - akin to objects in
|
||||||
|
# languages like Python, but immutable.
|
||||||
|
ref_capacity = 2048
|
||||||
|
|
||||||
|
# Amount of fuel given to the VM.
|
||||||
|
# Each instruction executed by the VM consumes fuel. The VM will continue running until it runs out
|
||||||
|
# of fuel completely.
|
||||||
|
# An unfortunate side effect of this is that since Haku is a functional language, a brush running
|
||||||
|
# out of fuel means it will not be rendered at all, because there is no complete value returned.
|
||||||
|
fuel = 65536
|
||||||
|
|
||||||
|
# Capacity of the renderer's pixmap stack.
|
||||||
|
# The pixmap stack is used for blending layers together within a brush.
|
||||||
|
# Each (composite)-type scribble requires a single entry on this pixmap stack.
|
||||||
|
# In the end, this defines how deep compositing operations may nest.
|
||||||
|
pixmap_stack_capacity = 4
|
||||||
|
|
||||||
|
# Capacity of the renderer's transformation stack.
|
||||||
|
# The transformation stack is used for operations on the transform matrix, such as (translate).
|
||||||
|
# To render each transformed operation, a single entry of the transform stack is used.
|
||||||
|
# In the end, this defines how deep matrix transform operations may nest.
|
||||||
|
transform_stack_capacity = 16
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,13 @@ class CanvasRenderer extends HTMLElement {
|
||||||
this.#render();
|
this.#render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getWindowSize() {
|
||||||
|
return {
|
||||||
|
width: this.clientWidth,
|
||||||
|
height: this.clientHeight,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
#render() {
|
#render() {
|
||||||
// NOTE: We should probably render on-demand only when it's needed.
|
// NOTE: We should probably render on-demand only when it's needed.
|
||||||
requestAnimationFrame(() => this.#render());
|
requestAnimationFrame(() => this.#render());
|
||||||
|
@ -41,6 +48,19 @@ class CanvasRenderer extends HTMLElement {
|
||||||
this.#renderWall();
|
this.#renderWall();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getVisibleRect() {
|
||||||
|
return this.viewport.getVisibleRect(this.getWindowSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
getVisibleChunkRect() {
|
||||||
|
let visibleRect = this.viewport.getVisibleRect(this.getWindowSize());
|
||||||
|
let left = Math.floor(visibleRect.x / this.wall.chunkSize);
|
||||||
|
let top = Math.floor(visibleRect.y / this.wall.chunkSize);
|
||||||
|
let right = Math.ceil((visibleRect.x + visibleRect.width) / this.wall.chunkSize);
|
||||||
|
let bottom = Math.ceil((visibleRect.y + visibleRect.height) / this.wall.chunkSize);
|
||||||
|
return { left, top, right, bottom };
|
||||||
|
}
|
||||||
|
|
||||||
#renderWall() {
|
#renderWall() {
|
||||||
if (this.wall == null) {
|
if (this.wall == null) {
|
||||||
console.debug("wall is not available, skipping rendering");
|
console.debug("wall is not available, skipping rendering");
|
||||||
|
@ -55,10 +75,7 @@ class CanvasRenderer extends HTMLElement {
|
||||||
this.ctx.scale(this.viewport.zoom, this.viewport.zoom);
|
this.ctx.scale(this.viewport.zoom, this.viewport.zoom);
|
||||||
this.ctx.translate(-this.viewport.panX, -this.viewport.panY);
|
this.ctx.translate(-this.viewport.panX, -this.viewport.panY);
|
||||||
|
|
||||||
let visibleRect = this.viewport.getVisibleRect({
|
let visibleRect = this.viewport.getVisibleRect(this.getWindowSize());
|
||||||
width: this.clientWidth,
|
|
||||||
height: this.clientHeight,
|
|
||||||
});
|
|
||||||
let left = Math.floor(visibleRect.x / this.wall.chunkSize);
|
let left = Math.floor(visibleRect.x / this.wall.chunkSize);
|
||||||
let top = Math.floor(visibleRect.y / this.wall.chunkSize);
|
let top = Math.floor(visibleRect.y / this.wall.chunkSize);
|
||||||
let right = Math.ceil((visibleRect.x + visibleRect.width) / this.wall.chunkSize);
|
let right = Math.ceil((visibleRect.x + visibleRect.width) / this.wall.chunkSize);
|
||||||
|
@ -88,10 +105,7 @@ class CanvasRenderer extends HTMLElement {
|
||||||
let [x, y] = this.viewport.toViewportSpace(
|
let [x, y] = this.viewport.toViewportSpace(
|
||||||
event.clientX - this.clientLeft,
|
event.clientX - this.clientLeft,
|
||||||
event.offsetY - this.clientTop,
|
event.offsetY - this.clientTop,
|
||||||
{
|
this.getWindowSize(),
|
||||||
width: this.clientWidth,
|
|
||||||
height: this.clientHeight,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
this.dispatchEvent(Object.assign(new Event(".cursor"), { x, y }));
|
this.dispatchEvent(Object.assign(new Event(".cursor"), { x, y }));
|
||||||
}
|
}
|
||||||
|
@ -127,10 +141,7 @@ class CanvasRenderer extends HTMLElement {
|
||||||
|
|
||||||
async #paintingBehaviour() {
|
async #paintingBehaviour() {
|
||||||
const paint = (x, y) => {
|
const paint = (x, y) => {
|
||||||
let [wallX, wallY] = this.viewport.toViewportSpace(x, y, {
|
let [wallX, wallY] = this.viewport.toViewportSpace(x, y, this.getWindowSize());
|
||||||
width: this.clientWidth,
|
|
||||||
height: this.clientHeight,
|
|
||||||
});
|
|
||||||
this.dispatchEvent(Object.assign(new Event(".paint"), { x: wallX, y: wallY }));
|
this.dispatchEvent(Object.assign(new Event(".paint"), { x: wallX, y: wallY }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -118,9 +118,16 @@ export class Haku {
|
||||||
#pBrush = 0;
|
#pBrush = 0;
|
||||||
#brushCode = null;
|
#brushCode = null;
|
||||||
|
|
||||||
constructor() {
|
constructor(limits) {
|
||||||
this.#pInstance = w.haku_instance_new();
|
let pLimits = w.haku_limits_new();
|
||||||
|
for (let name of Object.keys(limits)) {
|
||||||
|
w[`haku_limits_set_${name}`](pLimits, limits[name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#pInstance = w.haku_instance_new(pLimits);
|
||||||
this.#pBrush = w.haku_brush_new();
|
this.#pBrush = w.haku_brush_new();
|
||||||
|
|
||||||
|
w.haku_limits_destroy(pLimits);
|
||||||
}
|
}
|
||||||
|
|
||||||
setBrush(code) {
|
setBrush(code) {
|
||||||
|
@ -166,18 +173,7 @@ export class Haku {
|
||||||
return { status: "ok" };
|
return { status: "ok" };
|
||||||
}
|
}
|
||||||
|
|
||||||
renderBrush(pixmap, translationX, translationY) {
|
#statusCodeToResultObject(statusCode) {
|
||||||
let statusCode = w.haku_render_brush(
|
|
||||||
this.#pInstance,
|
|
||||||
this.#pBrush,
|
|
||||||
pixmap.ptr,
|
|
||||||
// If we ever want to detect which pixels were touched (USING A SHADER.), we can use
|
|
||||||
// this to rasterize the brush _twice_, and then we can detect which pixels are the same
|
|
||||||
// between the two pixmaps.
|
|
||||||
0,
|
|
||||||
translationX,
|
|
||||||
translationY,
|
|
||||||
);
|
|
||||||
if (!w.haku_is_ok(statusCode)) {
|
if (!w.haku_is_ok(statusCode)) {
|
||||||
if (w.haku_is_exception(statusCode)) {
|
if (w.haku_is_exception(statusCode)) {
|
||||||
return {
|
return {
|
||||||
|
@ -196,8 +192,22 @@ export class Haku {
|
||||||
message: readCString(w.haku_status_string(statusCode)),
|
message: readCString(w.haku_status_string(statusCode)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return { status: "ok" };
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { status: "ok" };
|
evalBrush() {
|
||||||
|
return this.#statusCodeToResultObject(w.haku_eval_brush(this.#pInstance, this.#pBrush));
|
||||||
|
}
|
||||||
|
|
||||||
|
renderValue(pixmap, translationX, translationY) {
|
||||||
|
return this.#statusCodeToResultObject(
|
||||||
|
w.haku_render_value(this.#pInstance, pixmap.ptr, translationX, translationY),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetVm() {
|
||||||
|
w.haku_reset_vm(this.#pInstance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -178,8 +178,8 @@ rkgk-reticle-renderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rkgk-reticle {
|
rkgk-reticle-cursor {
|
||||||
--color: black;
|
--color: black; /* Overridden by JavaScript to set a per-user color. */
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
166
static/index.js
166
static/index.js
|
@ -1,73 +1,165 @@
|
||||||
import { Painter } from "./painter.js";
|
|
||||||
import { Wall } from "./wall.js";
|
import { Wall } from "./wall.js";
|
||||||
import { Haku } from "./haku.js";
|
|
||||||
import { getUserId, newSession, waitForLogin } from "./session.js";
|
import { getUserId, newSession, waitForLogin } from "./session.js";
|
||||||
import { debounce } from "./framework.js";
|
import { debounce } from "./framework.js";
|
||||||
|
import { ReticleCursor } from "./reticle-renderer.js";
|
||||||
|
|
||||||
|
const updateInterval = 1000 / 60;
|
||||||
|
|
||||||
let main = document.querySelector("main");
|
let main = document.querySelector("main");
|
||||||
let canvasRenderer = main.querySelector("rkgk-canvas-renderer");
|
let canvasRenderer = main.querySelector("rkgk-canvas-renderer");
|
||||||
let reticleRenderer = main.querySelector("rkgk-reticle-renderer");
|
let reticleRenderer = main.querySelector("rkgk-reticle-renderer");
|
||||||
let brushEditor = main.querySelector("rkgk-brush-editor");
|
let brushEditor = main.querySelector("rkgk-brush-editor");
|
||||||
|
|
||||||
let haku = new Haku();
|
|
||||||
let painter = new Painter(512);
|
|
||||||
|
|
||||||
reticleRenderer.connectViewport(canvasRenderer.viewport);
|
reticleRenderer.connectViewport(canvasRenderer.viewport);
|
||||||
canvasRenderer.addEventListener(".viewportUpdate", () => reticleRenderer.updateTransform());
|
|
||||||
|
|
||||||
// In the background, connect to the server.
|
// In the background, connect to the server.
|
||||||
(async () => {
|
(async () => {
|
||||||
await waitForLogin();
|
await waitForLogin();
|
||||||
console.info("login ready! starting session");
|
console.info("login ready! starting session");
|
||||||
|
|
||||||
let session = await newSession(getUserId(), localStorage.getItem("rkgk.mostRecentWallId"));
|
let session = await newSession(getUserId(), localStorage.getItem("rkgk.mostRecentWallId"), {
|
||||||
|
brush: brushEditor.code,
|
||||||
|
});
|
||||||
localStorage.setItem("rkgk.mostRecentWallId", session.wallId);
|
localStorage.setItem("rkgk.mostRecentWallId", session.wallId);
|
||||||
|
|
||||||
let wall = new Wall(session.wallInfo.chunkSize);
|
let wall = new Wall(session.wallInfo);
|
||||||
canvasRenderer.initialize(wall);
|
canvasRenderer.initialize(wall);
|
||||||
|
|
||||||
for (let onlineUser of session.wallInfo.online) {
|
for (let onlineUser of session.wallInfo.online) {
|
||||||
wall.onlineUsers.addUser(onlineUser.sessionId, { nickname: onlineUser.nickname });
|
wall.onlineUsers.addUser(onlineUser.sessionId, {
|
||||||
|
nickname: onlineUser.nickname,
|
||||||
|
brush: onlineUser.init.brush,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentUser = wall.onlineUsers.getUser(session.sessionId);
|
||||||
|
|
||||||
session.addEventListener("error", (event) => console.error(event));
|
session.addEventListener("error", (event) => console.error(event));
|
||||||
session.addEventListener("action", (event) => {
|
|
||||||
if (event.kind.event == "cursor") {
|
session.addEventListener("wallEvent", (event) => {
|
||||||
let reticle = reticleRenderer.getOrAddReticle(wall.onlineUsers, event.sessionId);
|
let wallEvent = event.wallEvent;
|
||||||
let { x, y } = event.kind.position;
|
if (wallEvent.sessionId != session.sessionId) {
|
||||||
reticle.setCursor(x, y);
|
if (wallEvent.kind.event == "join") {
|
||||||
|
wall.onlineUsers.addUser(wallEvent.sessionId, {
|
||||||
|
nickname: wallEvent.kind.nickname,
|
||||||
|
brush: wallEvent.kind.init.brush,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = wall.onlineUsers.getUser(wallEvent.sessionId);
|
||||||
|
if (user == null) {
|
||||||
|
console.warn("received event for an unknown user", wallEvent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wallEvent.kind.event == "leave") {
|
||||||
|
if (user.reticle != null) {
|
||||||
|
reticleRenderer.removeReticle(user.reticle);
|
||||||
|
}
|
||||||
|
wall.onlineUsers.removeUser(wallEvent.sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wallEvent.kind.event == "cursor") {
|
||||||
|
if (user.reticle == null) {
|
||||||
|
user.reticle = new ReticleCursor(
|
||||||
|
wall.onlineUsers.getUser(wallEvent.sessionId).nickname,
|
||||||
|
);
|
||||||
|
reticleRenderer.addReticle(user.reticle);
|
||||||
|
}
|
||||||
|
|
||||||
|
let { x, y } = wallEvent.kind.position;
|
||||||
|
user.reticle.setCursor(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wallEvent.kind.event == "setBrush") {
|
||||||
|
user.setBrush(wallEvent.kind.brush);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wallEvent.kind.event == "plot") {
|
||||||
|
for (let { x, y } of wallEvent.kind.points) {
|
||||||
|
user.renderBrushToChunks(wall, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let compileBrush = () => haku.setBrush(brushEditor.code);
|
let pendingChunks = 0;
|
||||||
compileBrush();
|
let chunkDownloadStates = new Map();
|
||||||
brushEditor.addEventListener(".codeChanged", () => compileBrush());
|
|
||||||
|
|
||||||
let reportCursor = debounce(1000 / 60, (x, y) => session.reportCursor(x, y));
|
function sendViewportUpdate() {
|
||||||
|
let visibleRect = canvasRenderer.getVisibleChunkRect();
|
||||||
|
session.sendViewport(visibleRect);
|
||||||
|
|
||||||
|
for (let chunkY = visibleRect.top; chunkY < visibleRect.bottom; ++chunkY) {
|
||||||
|
for (let chunkX = visibleRect.left; chunkX < visibleRect.right; ++chunkX) {
|
||||||
|
let key = Wall.chunkKey(chunkX, chunkY);
|
||||||
|
let currentState = chunkDownloadStates.get(key);
|
||||||
|
if (currentState == null) {
|
||||||
|
chunkDownloadStates.set(key, "requested");
|
||||||
|
pendingChunks += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.info("pending chunks after viewport update", pendingChunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasRenderer.addEventListener(".viewportUpdate", sendViewportUpdate);
|
||||||
|
sendViewportUpdate();
|
||||||
|
|
||||||
|
session.addEventListener("chunks", (event) => {
|
||||||
|
let { chunkInfo, chunkData } = event;
|
||||||
|
|
||||||
|
console.info("received data for chunks", {
|
||||||
|
chunkInfoLength: chunkInfo.length,
|
||||||
|
chunkDataSize: chunkData.size,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let info of event.chunkInfo) {
|
||||||
|
let key = Wall.chunkKey(info.position.x, info.position.y);
|
||||||
|
if (chunkDownloadStates.get(key) == "requested") {
|
||||||
|
pendingChunks -= 1;
|
||||||
|
}
|
||||||
|
chunkDownloadStates.set(key, "downloaded");
|
||||||
|
|
||||||
|
if (info.length > 0) {
|
||||||
|
let blob = chunkData.slice(info.offset, info.offset + info.length, "image/webp");
|
||||||
|
createImageBitmap(blob).then((bitmap) => {
|
||||||
|
let chunk = wall.getOrCreateChunk(info.position.x, info.position.y);
|
||||||
|
chunk.ctx.globalCompositeOperation = "copy";
|
||||||
|
chunk.ctx.drawImage(bitmap, 0, 0);
|
||||||
|
chunk.syncToPixmap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let reportCursor = debounce(updateInterval, (x, y) => session.sendCursor(x, y));
|
||||||
canvasRenderer.addEventListener(".cursor", async (event) => {
|
canvasRenderer.addEventListener(".cursor", async (event) => {
|
||||||
reportCursor(event.x, event.y);
|
reportCursor(event.x, event.y);
|
||||||
});
|
});
|
||||||
|
|
||||||
canvasRenderer.addEventListener(".paint", async (event) => {
|
let plotQueue = [];
|
||||||
painter.renderBrush(haku);
|
async function flushPlotQueue() {
|
||||||
let imageBitmap = await painter.createImageBitmap();
|
let points = plotQueue.splice(0, plotQueue.length);
|
||||||
|
if (points.length != 0) {
|
||||||
let left = event.x - painter.paintArea / 2;
|
session.sendPlot(points);
|
||||||
let top = event.y - painter.paintArea / 2;
|
|
||||||
|
|
||||||
let leftChunk = Math.floor(left / wall.chunkSize);
|
|
||||||
let topChunk = Math.floor(top / wall.chunkSize);
|
|
||||||
let rightChunk = Math.ceil((left + painter.paintArea) / wall.chunkSize);
|
|
||||||
let bottomChunk = Math.ceil((top + painter.paintArea) / wall.chunkSize);
|
|
||||||
for (let chunkY = topChunk; chunkY < bottomChunk; ++chunkY) {
|
|
||||||
for (let chunkX = leftChunk; chunkX < rightChunk; ++chunkX) {
|
|
||||||
let chunk = wall.getOrCreateChunk(chunkX, chunkY);
|
|
||||||
let x = Math.floor(-chunkX * wall.chunkSize + left);
|
|
||||||
let y = Math.floor(-chunkY * wall.chunkSize + top);
|
|
||||||
chunk.ctx.drawImage(imageBitmap, x, y);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
imageBitmap.close();
|
}
|
||||||
|
|
||||||
|
setInterval(flushPlotQueue, updateInterval);
|
||||||
|
|
||||||
|
canvasRenderer.addEventListener(".paint", async (event) => {
|
||||||
|
plotQueue.push({ x: event.x, y: event.y });
|
||||||
|
currentUser.renderBrushToChunks(wall, event.x, event.y);
|
||||||
|
});
|
||||||
|
|
||||||
|
canvasRenderer.addEventListener(".viewportUpdate", () => reticleRenderer.render());
|
||||||
|
|
||||||
|
currentUser.setBrush(brushEditor.code);
|
||||||
|
brushEditor.addEventListener(".codeChanged", async () => {
|
||||||
|
flushPlotQueue();
|
||||||
|
currentUser.setBrush(brushEditor.code);
|
||||||
|
session.sendSetBrush(brushEditor.code);
|
||||||
});
|
});
|
||||||
|
|
||||||
session.eventLoop();
|
session.eventLoop();
|
||||||
|
|
|
@ -1,12 +1,53 @@
|
||||||
export class OnlineUsers extends EventTarget {
|
import { Haku } from "./haku.js";
|
||||||
#users = new Map();
|
import { Painter } from "./painter.js";
|
||||||
|
|
||||||
constructor() {
|
export class User {
|
||||||
super();
|
nickname = "";
|
||||||
|
brush = "";
|
||||||
|
reticle = null;
|
||||||
|
|
||||||
|
isBrushOk = false;
|
||||||
|
|
||||||
|
constructor(wallInfo, nickname) {
|
||||||
|
this.nickname = nickname;
|
||||||
|
|
||||||
|
this.haku = new Haku(wallInfo.hakuLimits);
|
||||||
|
this.painter = new Painter(wallInfo.paintArea);
|
||||||
}
|
}
|
||||||
|
|
||||||
addUser(sessionId, userInfo) {
|
setBrush(brush) {
|
||||||
this.#users.set(sessionId, userInfo);
|
let compileResult = this.haku.setBrush(brush);
|
||||||
|
this.isBrushOk = compileResult.status == "ok";
|
||||||
|
return compileResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBrushToChunks(wall, x, y) {
|
||||||
|
this.painter.renderBrushToWall(this.haku, x, y, wall);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OnlineUsers extends EventTarget {
|
||||||
|
#wallInfo;
|
||||||
|
#users = new Map();
|
||||||
|
|
||||||
|
constructor(wallInfo) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.#wallInfo = wallInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
addUser(sessionId, { nickname, brush }) {
|
||||||
|
if (!this.#users.has(sessionId)) {
|
||||||
|
console.info("user added", sessionId, nickname);
|
||||||
|
|
||||||
|
let user = new User(this.#wallInfo, nickname);
|
||||||
|
user.setBrush(brush);
|
||||||
|
this.#users.set(sessionId, user);
|
||||||
|
return user;
|
||||||
|
} else {
|
||||||
|
console.info("user already exists", sessionId, nickname);
|
||||||
|
return this.#users.get(sessionId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getUser(sessionId) {
|
getUser(sessionId) {
|
||||||
|
@ -14,6 +55,11 @@ export class OnlineUsers extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
removeUser(sessionId) {
|
removeUser(sessionId) {
|
||||||
this.#users.delete(sessionId);
|
if (this.#users.has(sessionId)) {
|
||||||
|
let user = this.#users.get(sessionId);
|
||||||
|
console.info("user removed", sessionId, user.nickname);
|
||||||
|
// TODO: Cleanup reticles
|
||||||
|
this.#users.delete(sessionId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,36 @@
|
||||||
import { Pixmap } from "./haku.js";
|
|
||||||
|
|
||||||
export class Painter {
|
export class Painter {
|
||||||
#pixmap;
|
|
||||||
imageBitmap;
|
|
||||||
|
|
||||||
constructor(paintArea) {
|
constructor(paintArea) {
|
||||||
this.paintArea = paintArea;
|
this.paintArea = paintArea;
|
||||||
this.#pixmap = new Pixmap(paintArea, paintArea);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createImageBitmap() {
|
renderBrushToWall(haku, centerX, centerY, wall) {
|
||||||
return await createImageBitmap(this.#pixmap.imageData);
|
let evalResult = haku.evalBrush();
|
||||||
}
|
if (evalResult.status != "ok") return evalResult;
|
||||||
|
|
||||||
renderBrush(haku) {
|
let left = centerX - this.paintArea / 2;
|
||||||
this.#pixmap.clear(0, 0, 0, 0);
|
let top = centerY - this.paintArea / 2;
|
||||||
let result = haku.renderBrush(this.#pixmap, this.paintArea / 2, this.paintArea / 2);
|
|
||||||
|
|
||||||
return result;
|
let leftChunk = Math.floor(left / wall.chunkSize);
|
||||||
|
let topChunk = Math.floor(top / wall.chunkSize);
|
||||||
|
let rightChunk = Math.ceil((left + this.paintArea) / wall.chunkSize);
|
||||||
|
let bottomChunk = Math.ceil((top + this.paintArea) / wall.chunkSize);
|
||||||
|
for (let chunkY = topChunk; chunkY < bottomChunk; ++chunkY) {
|
||||||
|
for (let chunkX = leftChunk; chunkX < rightChunk; ++chunkX) {
|
||||||
|
let x = Math.floor(-chunkX * wall.chunkSize + centerX);
|
||||||
|
let y = Math.floor(-chunkY * wall.chunkSize + centerY);
|
||||||
|
let chunk = wall.getOrCreateChunk(chunkX, chunkY);
|
||||||
|
|
||||||
|
let renderResult = haku.renderValue(chunk.pixmap, x, y);
|
||||||
|
if (renderResult.status != "ok") return renderResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
haku.resetVm();
|
||||||
|
|
||||||
|
for (let y = topChunk; y < bottomChunk; ++y) {
|
||||||
|
for (let x = leftChunk; x < rightChunk; ++x) {
|
||||||
|
let chunk = wall.getChunk(x, y);
|
||||||
|
chunk.syncFromPixmap();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
export class Reticle extends HTMLElement {
|
export class Reticle extends HTMLElement {
|
||||||
#kind = null;
|
render(_viewport, _windowSize) {
|
||||||
#data = {};
|
throw new Error("Reticle.render must be overridden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReticleCursor extends Reticle {
|
||||||
#container;
|
#container;
|
||||||
|
|
||||||
constructor(nickname) {
|
constructor(nickname) {
|
||||||
|
@ -14,84 +17,60 @@ export class Reticle extends HTMLElement {
|
||||||
|
|
||||||
this.#container = this.appendChild(document.createElement("div"));
|
this.#container = this.appendChild(document.createElement("div"));
|
||||||
this.#container.classList.add("container");
|
this.#container.classList.add("container");
|
||||||
|
|
||||||
|
this.classList.add("cursor");
|
||||||
|
|
||||||
|
let arrow = this.#container.appendChild(document.createElement("div"));
|
||||||
|
arrow.classList.add("arrow");
|
||||||
|
|
||||||
|
let nickname = this.#container.appendChild(document.createElement("div"));
|
||||||
|
nickname.classList.add("nickname");
|
||||||
|
nickname.textContent = this.nickname;
|
||||||
}
|
}
|
||||||
|
|
||||||
getColor() {
|
getColor() {
|
||||||
let hash = 5381;
|
let hash = 8803;
|
||||||
for (let i = 0; i < this.nickname.length; ++i) {
|
for (let i = 0; i < this.nickname.length; ++i) {
|
||||||
hash <<= 5;
|
hash = (hash << 5) - hash + this.nickname.charCodeAt(i);
|
||||||
hash += hash;
|
hash |= 0;
|
||||||
hash += this.nickname.charCodeAt(i);
|
|
||||||
hash &= 0xffff;
|
|
||||||
}
|
}
|
||||||
return `oklch(70% 0.2 ${(hash / 0xffff) * 360}deg)`;
|
return `oklch(65% 0.2 ${(hash / 0xffff) * 360}deg)`;
|
||||||
}
|
|
||||||
|
|
||||||
#update(kind, data) {
|
|
||||||
this.#data = data;
|
|
||||||
|
|
||||||
if (kind != this.#kind) {
|
|
||||||
this.classList = "";
|
|
||||||
this.#container.replaceChildren();
|
|
||||||
this.#kind = kind;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dispatchEvent(new Event(".update"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setCursor(x, y) {
|
setCursor(x, y) {
|
||||||
this.#update("cursor", { x, y });
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.dispatchEvent(new Event(".update"));
|
||||||
}
|
}
|
||||||
|
|
||||||
render(viewport, windowSize) {
|
render(viewport, windowSize) {
|
||||||
if (!this.rendered) {
|
let [viewportX, viewportY] = viewport.toScreenSpace(this.x, this.y, windowSize);
|
||||||
if (this.#kind == "cursor") {
|
this.style.transform = `translate(${viewportX}px, ${viewportY}px)`;
|
||||||
this.classList.add("cursor");
|
|
||||||
|
|
||||||
let arrow = this.#container.appendChild(document.createElement("div"));
|
|
||||||
arrow.classList.add("arrow");
|
|
||||||
|
|
||||||
let nickname = this.#container.appendChild(document.createElement("div"));
|
|
||||||
nickname.classList.add("nickname");
|
|
||||||
nickname.textContent = this.nickname;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.#kind == "cursor") {
|
|
||||||
let { x, y } = this.#data;
|
|
||||||
let [viewportX, viewportY] = viewport.toScreenSpace(x, y, windowSize);
|
|
||||||
this.style.transform = `translate(${viewportX}px, ${viewportY}px)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rendered = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("rkgk-reticle", Reticle);
|
customElements.define("rkgk-reticle-cursor", ReticleCursor);
|
||||||
|
|
||||||
export class ReticleRenderer extends HTMLElement {
|
export class ReticleRenderer extends HTMLElement {
|
||||||
#reticles = new Map();
|
#reticles = new Set();
|
||||||
#reticlesDiv;
|
#reticlesDiv;
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.#reticlesDiv = this.appendChild(document.createElement("div"));
|
this.#reticlesDiv = this.appendChild(document.createElement("div"));
|
||||||
this.#reticlesDiv.classList.add("reticles");
|
this.#reticlesDiv.classList.add("reticles");
|
||||||
|
|
||||||
this.updateTransform();
|
this.render();
|
||||||
let resizeObserver = new ResizeObserver(() => this.updateTransform());
|
let resizeObserver = new ResizeObserver(() => this.render());
|
||||||
resizeObserver.observe(this);
|
resizeObserver.observe(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectViewport(viewport) {
|
connectViewport(viewport) {
|
||||||
this.viewport = viewport;
|
this.viewport = viewport;
|
||||||
this.updateTransform();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
getOrAddReticle(onlineUsers, sessionId) {
|
addReticle(reticle) {
|
||||||
if (this.#reticles.has(sessionId)) {
|
if (!this.#reticles.has(reticle)) {
|
||||||
return this.#reticles.get(sessionId);
|
|
||||||
} else {
|
|
||||||
let reticle = new Reticle(onlineUsers.getUser(sessionId).nickname);
|
|
||||||
reticle.addEventListener(".update", () => {
|
reticle.addEventListener(".update", () => {
|
||||||
if (this.viewport != null) {
|
if (this.viewport != null) {
|
||||||
reticle.render(this.viewport, {
|
reticle.render(this.viewport, {
|
||||||
|
@ -100,28 +79,26 @@ export class ReticleRenderer extends HTMLElement {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.#reticles.set(sessionId, reticle);
|
this.#reticles.add(reticle);
|
||||||
this.#reticlesDiv.appendChild(reticle);
|
this.#reticlesDiv.appendChild(reticle);
|
||||||
return reticle;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removeReticle(sessionId) {
|
removeReticle(reticle) {
|
||||||
if (this.#reticles.has(sessionId)) {
|
if (this.#reticles.has(reticle)) {
|
||||||
let reticle = this.#reticles.get(sessionId);
|
this.#reticles.delete(reticle);
|
||||||
this.#reticles.delete(sessionId);
|
|
||||||
this.#reticlesDiv.removeChild(reticle);
|
this.#reticlesDiv.removeChild(reticle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTransform() {
|
render() {
|
||||||
if (this.viewport == null) {
|
if (this.viewport == null) {
|
||||||
console.debug("viewport is disconnected, skipping transform update");
|
console.debug("viewport is disconnected, skipping transform update");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let windowSize = { width: this.clientWidth, height: this.clientHeight };
|
let windowSize = { width: this.clientWidth, height: this.clientHeight };
|
||||||
for (let [_, reticle] of this.#reticles) {
|
for (let reticle of this.#reticles.values()) {
|
||||||
reticle.render(this.viewport, windowSize);
|
reticle.render(this.viewport, windowSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,8 +85,16 @@ class Session extends EventTarget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async #recvBinary() {
|
||||||
|
let event = await listen([this.ws, "message"]);
|
||||||
|
if (event.data instanceof Blob) {
|
||||||
|
return event.data;
|
||||||
|
} else {
|
||||||
|
throw new Error("received a text message where a binary message was expected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#sendJson(object) {
|
#sendJson(object) {
|
||||||
console.debug("sendJson", object);
|
|
||||||
this.ws.send(JSON.stringify(object));
|
this.ws.send(JSON.stringify(object));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +108,7 @@ class Session extends EventTarget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async join(wallId) {
|
async join(wallId, userInit) {
|
||||||
console.info("joining wall", wallId);
|
console.info("joining wall", wallId);
|
||||||
this.wallId = wallId;
|
this.wallId = wallId;
|
||||||
|
|
||||||
|
@ -123,22 +131,32 @@ class Session extends EventTarget {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await listen([this.ws, "open"]);
|
await listen([this.ws, "open"]);
|
||||||
await this.joinInner();
|
await this.joinInner(wallId, userInit);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.#dispatchError(error, "connection", `communication failed: ${error.toString()}`);
|
this.#dispatchError(error, "connection", `communication failed: ${error.toString()}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async joinInner() {
|
async joinInner(wallId, userInit) {
|
||||||
let version = await this.#recvJson();
|
let version = await this.#recvJson();
|
||||||
console.info("protocol version", version.version);
|
console.info("protocol version", version.version);
|
||||||
// TODO: This should probably verify that the version is compatible.
|
// TODO: This should probably verify that the version is compatible.
|
||||||
// We don't have a way of sending Rust stuff to JavaScript just yet, so we don't care about it.
|
// We don't have a way of sending Rust stuff to JavaScript just yet, so we don't care about it.
|
||||||
|
|
||||||
|
let init = {
|
||||||
|
brush: userInit.brush,
|
||||||
|
};
|
||||||
if (this.wallId == null) {
|
if (this.wallId == null) {
|
||||||
this.#sendJson({ login: "new", user: this.userId });
|
this.#sendJson({
|
||||||
|
user: this.userId,
|
||||||
|
init,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.#sendJson({ login: "join", user: this.userId, wall: this.wallId });
|
this.#sendJson({
|
||||||
|
user: this.userId,
|
||||||
|
wall: wallId,
|
||||||
|
init,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let loginResponse = await this.#recvJson();
|
let loginResponse = await this.#recvJson();
|
||||||
|
@ -164,9 +182,9 @@ class Session extends EventTarget {
|
||||||
while (true) {
|
while (true) {
|
||||||
let event = await listen([this.ws, "message"]);
|
let event = await listen([this.ws, "message"]);
|
||||||
if (typeof event.data == "string") {
|
if (typeof event.data == "string") {
|
||||||
await this.#processEvent(JSON.parse(event.data));
|
await this.#processNotify(JSON.parse(event.data));
|
||||||
} else {
|
} else {
|
||||||
console.warn("binary event not yet supported");
|
console.warn("unhandled binary event", event.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -174,27 +192,74 @@ class Session extends EventTarget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async #processEvent(event) {
|
async #processNotify(notify) {
|
||||||
if (event.kind != null) {
|
if (notify.notify == "wall") {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
Object.assign(new Event("action"), {
|
Object.assign(new Event("wallEvent"), {
|
||||||
sessionId: event.sessionId,
|
sessionId: notify.sessionId,
|
||||||
kind: event.kind,
|
wallEvent: notify.wallEvent,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notify.notify == "chunks") {
|
||||||
|
let chunkData = await this.#recvBinary();
|
||||||
|
this.dispatchEvent(
|
||||||
|
Object.assign(new Event("chunks"), {
|
||||||
|
chunkInfo: notify.chunks,
|
||||||
|
chunkData,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async reportCursor(x, y) {
|
sendCursor(x, y) {
|
||||||
this.#sendJson({
|
this.#sendJson({
|
||||||
event: "cursor",
|
request: "wall",
|
||||||
position: { x, y },
|
wallEvent: {
|
||||||
|
event: "cursor",
|
||||||
|
position: { x, y },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendPlot(points) {
|
||||||
|
this.#sendJson({
|
||||||
|
request: "wall",
|
||||||
|
wallEvent: {
|
||||||
|
event: "plot",
|
||||||
|
points,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendSetBrush(brush) {
|
||||||
|
this.#sendJson({
|
||||||
|
request: "wall",
|
||||||
|
wallEvent: {
|
||||||
|
event: "setBrush",
|
||||||
|
brush,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendViewport({ left, top, right, bottom }) {
|
||||||
|
this.#sendJson({
|
||||||
|
request: "viewport",
|
||||||
|
topLeft: { x: left, y: top },
|
||||||
|
bottomRight: { x: right, y: bottom },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMoreChunks() {
|
||||||
|
this.#sendJson({
|
||||||
|
request: "moreChunks",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function newSession(userId, wallId) {
|
export async function newSession(userId, wallId, userInit) {
|
||||||
let session = new Session(userId);
|
let session = new Session(userId);
|
||||||
await session.join(wallId);
|
await session.join(wallId, userInit);
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,29 @@
|
||||||
|
import { Pixmap } from "./haku.js";
|
||||||
import { OnlineUsers } from "./online-users.js";
|
import { OnlineUsers } from "./online-users.js";
|
||||||
|
|
||||||
export class Chunk {
|
export class Chunk {
|
||||||
constructor(size) {
|
constructor(size) {
|
||||||
|
this.pixmap = new Pixmap(size, size);
|
||||||
this.canvas = new OffscreenCanvas(size, size);
|
this.canvas = new OffscreenCanvas(size, size);
|
||||||
this.ctx = this.canvas.getContext("2d");
|
this.ctx = this.canvas.getContext("2d");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncFromPixmap() {
|
||||||
|
this.ctx.putImageData(this.pixmap.imageData, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
syncToPixmap() {
|
||||||
|
let imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
this.pixmap.imageData.data.set(imageData.data, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Wall {
|
export class Wall {
|
||||||
#chunks = new Map();
|
#chunks = new Map();
|
||||||
onlineUsers = new OnlineUsers();
|
|
||||||
|
|
||||||
constructor(chunkSize) {
|
constructor(wallInfo) {
|
||||||
this.chunkSize = chunkSize;
|
this.chunkSize = wallInfo.chunkSize;
|
||||||
|
this.onlineUsers = new OnlineUsers(wallInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
static chunkKey(x, y) {
|
static chunkKey(x, y) {
|
||||||
|
|
Loading…
Reference in a new issue