This commit is contained in:
liquidex 2024-08-15 20:01:23 +02:00
parent 26ba098183
commit 2f7bcbb14e
30 changed files with 1691 additions and 315 deletions

300
Cargo.lock generated
View file

@ -29,6 +29,15 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
@ -204,6 +213,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.7.1"
@ -215,6 +230,10 @@ name = "cc"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549"
dependencies = [
"jobserver",
"libc",
]
[[package]]
name = "cfg-if"
@ -287,6 +306,25 @@ dependencies = [
"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]]
name = "crossbeam-utils"
version = "0.8.20"
@ -364,6 +402,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "equivalent"
version = "1.0.1"
@ -448,6 +492,19 @@ dependencies = [
"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]]
name = "generic-array"
version = "0.14.7"
@ -475,6 +532,12 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "haku"
version = "0.1.0"
@ -497,6 +560,7 @@ dependencies = [
"dlmalloc",
"haku",
"log",
"paste",
]
[[package]]
@ -626,7 +690,7 @@ dependencies = [
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
"windows-core 0.52.0",
]
[[package]]
@ -648,6 +712,17 @@ dependencies = [
"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]]
name = "indenter"
version = "0.3.3"
@ -670,6 +745,15 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "jobserver"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.69"
@ -708,6 +792,16 @@ dependencies = [
"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]]
name = "lock_api"
version = "0.4.12"
@ -724,6 +818,28 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "matchit"
version = "0.7.3"
@ -842,6 +958,12 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "percent-encoding"
version = "2.3.1"
@ -943,6 +1065,26 @@ dependencies = [
"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]]
name = "redox_syscall"
version = "0.5.3"
@ -952,6 +1094,50 @@ dependencies = [
"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]]
name = "rkgk"
version = "0.1.0"
@ -965,8 +1151,10 @@ dependencies = [
"derive_more",
"eyre",
"haku",
"indexmap",
"rand",
"rand_chacha",
"rayon",
"rusqlite",
"serde",
"serde_json",
@ -975,8 +1163,14 @@ dependencies = [
"tower-http",
"tracing",
"tracing-subscriber",
"tracy-client",
"webp",
]
[[package]]
name = "rkgk-image-ops"
version = "0.1.0"
[[package]]
name = "rusqlite"
version = "0.32.1"
@ -1018,6 +1212,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
@ -1442,14 +1642,38 @@ version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"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]]
name = "tungstenite"
version = "0.21.0"
@ -1610,6 +1834,16 @@ version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "winapi"
version = "0.3.9"
@ -1641,6 +1875,16 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "windows-core"
version = "0.52.0"
@ -1650,6 +1894,60 @@ dependencies = [
"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]]
name = "windows-sys"
version = "0.52.0"

View file

@ -5,6 +5,10 @@ members = ["crates/*"]
[workspace.dependencies]
haku.path = "crates/haku"
log = "0.4.22"
rkgk-image-ops.path = "crates/rkgk-image-ops"
[profile.dev.package.rkgk-image-ops]
opt-level = 3
[profile.wasm-dev]
inherits = "dev"

View file

@ -11,4 +11,5 @@ arrayvec = { version = "0.7.4", default-features = false }
dlmalloc = { version = "0.2.6", features = ["global"] }
haku.workspace = true
log.workspace = true
paste = "1.0.15"

View file

@ -14,10 +14,10 @@ use haku::{
},
sexp::{parse_toplevel, Ast, Parser},
system::{ChunkId, System, SystemImage},
value::{BytecodeLoc, Closure, FunctionName, Ref},
value::{BytecodeLoc, Closure, FunctionName, Ref, Value},
vm::{Exception, Vm, VmImage, VmLimits},
};
use log::info;
use log::{debug, info};
pub mod logging;
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)]
struct Instance {
limits: Limits,
@ -76,13 +111,16 @@ struct Instance {
defs_image: DefsImage,
vm: Vm,
vm_image: VmImage,
value: Value,
exception: Option<Exception>,
}
#[no_mangle]
unsafe extern "C" fn haku_instance_new() -> *mut Instance {
// TODO: This should be a parameter.
let limits = Limits::default();
unsafe extern "C" fn haku_instance_new(limits: *const Limits) -> *mut Instance {
let limits = *limits;
debug!("creating new instance with limits: {limits:?}");
let system = System::new(limits.max_chunks);
let defs = Defs::new(limits.max_defs);
@ -108,6 +146,7 @@ unsafe extern "C" fn haku_instance_new() -> *mut Instance {
defs_image,
vm,
vm_image,
value: Value::Nil,
exception: None,
});
Box::leak(instance)
@ -125,6 +164,12 @@ unsafe extern "C" fn haku_reset(instance: *mut Instance) {
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]
unsafe extern "C" fn haku_has_exception(instance: *mut Instance) -> bool {
(*instance).exception.is_some()
@ -285,13 +330,13 @@ unsafe extern "C" fn haku_compile_brush(
}
struct PixmapLock {
pixmap: Option<Pixmap>,
pixmap: Pixmap,
}
#[no_mangle]
extern "C" fn haku_pixmap_new(width: u32, height: u32) -> *mut 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]
unsafe extern "C" fn haku_pixmap_data(pixmap: *mut PixmapLock) -> *mut u8 {
let pixmap = (*pixmap)
.pixmap
.as_mut()
.expect("pixmap is already being rendered to");
let pixmap = &mut (*pixmap).pixmap;
pixmap.pixels_mut().as_mut_ptr() as *mut u8
}
#[no_mangle]
unsafe extern "C" fn haku_pixmap_clear(pixmap: *mut PixmapLock) {
let pixmap = (*pixmap)
.pixmap
.as_mut()
.expect("pixmap is already being rendered to");
let pixmap = &mut (*pixmap).pixmap;
pixmap.pixels_mut().fill(PremultipliedColorU8::TRANSPARENT);
}
#[no_mangle]
unsafe extern "C" fn haku_render_brush(
instance: *mut Instance,
brush: *const Brush,
pixmap_a: *mut PixmapLock,
pixmap_b: *mut PixmapLock,
translation_x: f32,
translation_y: f32,
) -> StatusCode {
unsafe extern "C" fn haku_eval_brush(instance: *mut Instance, brush: *const Brush) -> StatusCode {
let instance = &mut *instance;
let brush = &*brush;
@ -347,7 +378,7 @@ unsafe extern "C" fn haku_render_brush(
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,
Err(exn) => {
instance.exception = Some(exn);
@ -355,47 +386,36 @@ unsafe extern "C" fn haku_render_brush(
}
};
let mut render = |pixmap: *mut PixmapLock| {
let pixmap_locked = (*pixmap)
.pixmap
.take()
.expect("pixmap is already being rendered to");
StatusCode::Ok
}
let mut renderer = Renderer::new(
pixmap_locked,
&RendererLimits {
pixmap_stack_capacity: instance.limits.pixmap_stack_capacity,
transform_stack_capacity: instance.limits.transform_stack_capacity,
},
);
renderer.translate(translation_x, translation_y);
match renderer.render(&instance.vm, scribble) {
Ok(()) => (),
Err(exn) => {
instance.exception = Some(exn);
return StatusCode::RenderException;
}
}
#[no_mangle]
unsafe extern "C" fn haku_render_value(
instance: *mut Instance,
pixmap: *mut PixmapLock,
translation_x: f32,
translation_y: f32,
) -> StatusCode {
let instance = &mut *instance;
let pixmap_locked = renderer.finish();
let pixmap_locked = &mut (*pixmap).pixmap;
(*pixmap).pixmap = Some(pixmap_locked);
StatusCode::Ok
};
match render(pixmap_a) {
StatusCode::Ok => (),
other => return other,
}
if !pixmap_b.is_null() {
match render(pixmap_b) {
StatusCode::Ok => (),
other => return other,
let mut renderer = Renderer::new(
pixmap_locked,
&RendererLimits {
pixmap_stack_capacity: instance.limits.pixmap_stack_capacity,
transform_stack_capacity: instance.limits.transform_stack_capacity,
},
);
renderer.translate(translation_x, translation_y);
match renderer.render(&instance.vm, instance.value) {
Ok(()) => (),
Err(exn) => {
instance.exception = Some(exn);
instance.vm.restore_image(&instance.vm_image);
return StatusCode::RenderException;
}
}
instance.vm.restore_image(&instance.vm_image);
StatusCode::Ok
}

View file

@ -15,18 +15,23 @@ pub struct RendererLimits {
pub transform_stack_capacity: usize,
}
pub struct Renderer {
pixmap_stack: Vec<Pixmap>,
pub enum RenderTarget<'a> {
Borrowed(&'a mut Pixmap),
Owned(Pixmap),
}
pub struct Renderer<'a> {
pixmap_stack: Vec<RenderTarget<'a>>,
transform_stack: Vec<Transform>,
}
impl Renderer {
pub fn new(pixmap: Pixmap, limits: &RendererLimits) -> Self {
impl<'a> Renderer<'a> {
pub fn new(pixmap: &'a mut Pixmap, limits: &RendererLimits) -> Self {
assert!(limits.pixmap_stack_capacity > 0);
assert!(limits.transform_stack_capacity > 0);
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);
transform_stack.push(Transform::identity());
@ -55,7 +60,10 @@ impl Renderer {
}
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> {
@ -123,10 +131,6 @@ impl Renderer {
Ok(())
}
pub fn finish(mut self) -> Pixmap {
self.pixmap_stack.drain(..).next().unwrap()
}
}
fn default_paint() -> Paint<'static> {

View file

@ -0,0 +1,6 @@
[package]
name = "rkgk-image-ops"
version = "0.1.0"
edition = "2021"
[dependencies]

View 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);
}
}

View file

@ -13,8 +13,10 @@ dashmap = "6.0.1"
derive_more = { version = "1.0.0", features = ["try_from"] }
eyre = "0.6.12"
haku.workspace = true
indexmap = "2.4.0"
rand = "0.8.5"
rand_chacha = "0.3.1"
rayon = "1.10.0"
rusqlite = { version = "0.32.1", features = ["bundled"] }
serde = { version = "1.0.206", features = ["derive"] }
serde_json = "1.0.124"
@ -23,3 +25,9 @@ toml = "0.8.19"
tower-http = { version = "0.5.2", features = ["fs"] }
tracing = "0.1.40"
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"]

View file

@ -9,15 +9,20 @@ use axum::{
};
use serde::{Deserialize, Serialize};
use crate::Databases;
use crate::{config::Config, Databases};
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()
.route("/login", post(login_new))
.route("/wall", get(wall::wall))
.with_state(dbs)
.with_state(api)
}
#[derive(Deserialize)]
@ -35,7 +40,7 @@ enum NewUserResponse {
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(&params.nickname.len()) {
return (
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) => (
StatusCode::OK,
Json(NewUserResponse::Ok {

View file

@ -8,21 +8,30 @@ use axum::{
response::Response,
};
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 tokio::select;
use tokio::{select, sync::mpsc, time::Instant};
use tracing::{error, info};
use crate::{
haku::{Haku, Limits},
login::database::LoginStatus,
wall::{Event, JoinError, Session},
Databases,
schema::Vec2,
wall::{
self, chunk_encoder::ChunkEncoder, chunk_iterator::ChunkIterator, ChunkPosition, JoinError,
SessionHandle, UserInit, Wall,
},
};
use super::Api;
mod schema;
pub async fn wall(State(dbs): State<Arc<Databases>>, ws: WebSocketUpgrade) -> Response {
ws.on_upgrade(|ws| websocket(dbs, ws))
pub async fn wall(State(api): State<Arc<Api>>, ws: WebSocketUpgrade) -> Response {
ws.on_upgrade(|ws| websocket(api, ws))
}
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")??)
}
async fn websocket(dbs: Arc<Databases>, mut ws: WebSocket) {
match fallible_websocket(dbs, &mut ws).await {
async fn websocket(api: Arc<Api>, mut ws: WebSocket) {
match fallible_websocket(api, &mut ws).await {
Ok(()) => (),
Err(e) => {
_ = 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)]
let version = format!("{}-dev", env!("CARGO_PKG_VERSION"));
#[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?;
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
.log_in(user_id)
.await
@ -88,14 +98,24 @@ async fn fallible_websocket(dbs: Arc<Databases>, ws: &mut WebSocket) -> eyre::Re
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 {
LoginRequest::New { .. } => dbs.wall_broker.generate_id().await,
LoginRequest::Join { wall, .. } => wall,
let wall_id = match login_request.wall {
Some(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,
Err(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![];
for online in wall.online() {
let user_info = match dbs.login.user_info(online.user_id).await {
for online in open_wall.wall.online() {
let user_info = match api.dbs.login.user_info(online.user_id).await {
Ok(Some(user_info)) => user_info,
Ok(None) | Err(_) => {
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,
nickname: user_info.nickname,
cursor: online.cursor,
init: UserInit {
brush: online.brush,
},
})
}
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 {
wall: wall_id,
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,
haku_limits: api.config.haku.clone(),
},
session_id: session_handle.session_id,
}))
.await?;
loop {
select! {
Some(message) = ws.recv() => {
let kind = from_message(&message?)?;
wall.event(Event { session_id: session_handle.session_id, kind });
open_wall.wall.event(wall::Event {
session_id: session_handle.session_id,
kind: wall::EventKind::Join {
nickname: user_info.nickname,
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() => {
ws.send(to_message(&event)).await?;
Request::Viewport {
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))
}
}

View file

@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use crate::{
login::UserId,
schema::Vec2,
wall::{self, SessionId, WallId},
wall::{self, ChunkPosition, SessionId, UserInit, WallId},
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
@ -19,23 +19,12 @@ pub struct Error {
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(
tag = "login",
rename_all = "camelCase",
rename_all_fields = "camelCase"
)]
pub enum LoginRequest {
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,
}
}
#[serde(rename_all = "camelCase")]
pub struct LoginRequest {
pub user: UserId,
/// If null, a new wall is created.
pub wall: Option<WallId>,
pub init: UserInit,
}
#[derive(Debug, Clone, Serialize)]
@ -44,12 +33,15 @@ pub struct Online {
pub session_id: SessionId,
pub nickname: String,
pub cursor: Option<Vec2>,
pub init: UserInit,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WallInfo {
pub chunk_size: u32,
pub paint_area: u32,
pub haku_limits: crate::haku::Limits,
pub online: Vec<Online>,
}
@ -68,3 +60,40 @@ pub enum LoginResponse {
UserDoesNotExist,
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> },
}

View file

@ -5,4 +5,5 @@ use crate::wall;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
pub wall: wall::Settings,
pub haku: crate::haku::Limits,
}

163
crates/rkgk/src/haku.rs Normal file </