From f957c93b03aafcca925fcf35a2f4949f7b921f58 Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Mon, 19 Feb 2024 20:02:08 +0100 Subject: [PATCH] Implement Layout Backgrounds (#774) This implements background for the layouts. This is a lot more involved than what it would seem. Part of it is a huge refactoring that changes all layout states to be truly absolute. Previously they only included image changes whenever an image changes. We did this because we didn't want to serialize all the image data on every single frame. The new approach is to have a cache for the images and only serialize the image IDs. The image IDs are SHA-256 hashes, which allow deduplicating the images as well. --- .github/workflows/build.yml | 26 +- Cargo.toml | 8 +- benches/layout_state.rs | 30 +- benches/scene_management.rs | 13 +- benches/software_rendering.rs | 23 +- capi/Cargo.toml | 2 +- capi/bind_gen/src/main.rs | 14 - capi/bind_gen/src/typescript.ts | 111 +- capi/bind_gen/src/wasm.rs | 990 ------------------ capi/bind_gen/src/wasm_bindgen.rs | 91 ++ capi/src/auto_splitting_runtime.rs | 2 +- capi/src/detailed_timer_component.rs | 17 +- capi/src/detailed_timer_component_state.rs | 25 +- capi/src/image_cache.rs | 94 ++ capi/src/layout.rs | 49 +- capi/src/layout_editor.rs | 30 +- capi/src/lib.rs | 41 +- capi/src/run.rs | 5 +- capi/src/run_editor.rs | 18 +- capi/src/setting_value.rs | 27 +- capi/src/software_renderer.rs | 19 +- capi/src/splits_component.rs | 15 +- capi/src/splits_component_state.rs | 45 +- capi/src/title_component.rs | 17 +- capi/src/title_component_state.rs | 19 +- crates/livesplit-auto-splitting/Cargo.toml | 2 +- crates/livesplit-hotkey/Cargo.toml | 2 +- crates/livesplit-hotkey/src/windows/mod.rs | 2 +- .../livesplit-title-abbreviations/Cargo.toml | 2 +- src/analysis/pb_chance/mod.rs | 12 +- src/analysis/state_helper.rs | 44 +- src/comparison/mod.rs | 5 +- src/component/detailed_timer/mod.rs | 53 +- src/component/detailed_timer/tests.rs | 125 +-- src/component/separator.rs | 3 +- src/component/splits/mod.rs | 72 +- src/component/splits/tests/column.rs | 71 +- src/component/splits/tests/mod.rs | 31 +- src/component/text/mod.rs | 2 +- src/component/title/mod.rs | 58 +- src/component/title/tests.rs | 34 +- src/layout/component.rs | 48 +- src/layout/component_state.rs | 3 +- src/layout/editor/mod.rs | 53 +- src/layout/editor/state.rs | 18 +- src/layout/general_settings.rs | 27 +- src/layout/layout_state.rs | 7 +- src/layout/mod.rs | 45 +- src/layout/parser/mod.rs | 100 +- src/lib.rs | 3 +- src/rendering/component/detailed_timer.rs | 21 +- src/rendering/component/key_value.rs | 10 +- src/rendering/component/mod.rs | 20 +- src/rendering/component/splits.rs | 22 +- src/rendering/component/text.rs | 10 +- src/rendering/component/timer.rs | 10 +- src/rendering/component/title.rs | 17 +- src/rendering/default_text_engine/mod.rs | 10 +- src/rendering/entity.rs | 30 +- src/rendering/icon.rs | 28 +- src/rendering/mod.rs | 110 +- src/rendering/resource/allocation.rs | 35 +- src/rendering/scene.rs | 8 +- src/rendering/software.rs | 277 ++++- src/run/editor/fuzzy_list.rs | 67 +- src/run/editor/mod.rs | 20 +- src/run/editor/segment_row.rs | 98 +- src/run/editor/state.rs | 45 +- src/run/editor/tests/mark_as_modified.rs | 14 +- src/run/mod.rs | 12 +- src/run/parser/face_split.rs | 6 +- src/run/parser/livesplit.rs | 39 +- src/run/parser/llanfair.rs | 5 +- src/run/parser/llanfair_gered.rs | 7 +- src/run/parser/splitterz.rs | 1 + src/run/parser/time_split_tracker.rs | 4 +- src/run/parser/wsplit.rs | 6 +- src/run/segment.rs | 4 +- src/settings/image/cache.rs | 296 ++++++ src/settings/image/image_id.rs | 141 +++ src/settings/image/mod.rs | 263 ++--- src/settings/image/tests.rs | 6 +- src/settings/layout_background.rs | 102 ++ src/settings/mod.rs | 4 +- src/settings/value.rs | 28 +- src/timing/time_span.rs | 12 + src/timing/timer/mod.rs | 2 + src/util/xml/helper.rs | 58 +- tests/rendering.rs | 93 +- 89 files changed, 2365 insertions(+), 2129 deletions(-) delete mode 100644 capi/bind_gen/src/wasm.rs create mode 100644 capi/src/image_cache.rs create mode 100644 src/settings/image/cache.rs create mode 100644 src/settings/image/image_id.rs create mode 100644 src/settings/layout_background.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5d3a6b78..fbb500c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -288,11 +288,15 @@ jobs: - label: Linux i586 target: i586-unknown-linux-gnu auto_splitting: skip + # FIXME: rustls currently does not support i586. + networking: skip - label: Linux i586 musl target: i586-unknown-linux-musl auto_splitting: skip dylib: skip + # FIXME: rustls currently does not support i586. + networking: skip - label: Linux i686 target: i686-unknown-linux-gnu @@ -544,10 +548,10 @@ jobs: steps: - name: Checkout Commit - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust - uses: hecrj/setup-rust-action@v1 + uses: hecrj/setup-rust-action@v2 with: rust-version: ${{ matrix.toolchain || 'stable' }} @@ -557,7 +561,7 @@ jobs: - name: Download cross if: matrix.cross == '' && matrix.no_std == '' - uses: robinraju/release-downloader@v1.7 + uses: robinraju/release-downloader@v1.9 with: repository: "cross-rs/cross" latest: true @@ -619,10 +623,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Commit - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust - uses: hecrj/setup-rust-action@v1 + uses: hecrj/setup-rust-action@v2 - name: Generate bindings run: | @@ -635,10 +639,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Commit - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust - uses: hecrj/setup-rust-action@v1 + uses: hecrj/setup-rust-action@v2 with: components: clippy @@ -650,10 +654,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Commit - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust - uses: hecrj/setup-rust-action@v1 + uses: hecrj/setup-rust-action@v2 with: components: rustfmt @@ -667,10 +671,10 @@ jobs: CRITERION_TOKEN: ${{ secrets.CRITERION_TOKEN }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust - uses: hecrj/setup-rust-action@v1 + uses: hecrj/setup-rust-action@v2 - name: Run benchmarks run: | diff --git a/Cargo.toml b/Cargo.toml index 4ddcd654..0d1458d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.13.0" authors = ["Christopher Serr "] documentation = "https://docs.rs/livesplit-core/" repository = "https://github.com/LiveSplit/livesplit-core" -license = "Apache-2.0/MIT" +license = "MIT OR Apache-2.0" description = "livesplit-core is a library that provides a lot of functionality for creating a speedrun timer." readme = "README.md" keywords = ["speedrun", "timer", "livesplit", "gaming"] @@ -47,7 +47,7 @@ libm = "0.2.1" livesplit-hotkey = { path = "crates/livesplit-hotkey", version = "0.7.0", default-features = false } livesplit-title-abbreviations = { path = "crates/livesplit-title-abbreviations", version = "0.3.0" } memchr = { version = "2.3.4", default-features = false } -simdutf8 = { version = "0.1.4", default-features = false, features = [ +simdutf8 = { git = "https://github.com/CryZe/simdutf8", branch = "wasm-ub-panic", default-features = false, features = [ "aarch64_neon", ] } serde = { version = "1.0.186", default-features = false, features = ["alloc"] } @@ -55,6 +55,8 @@ serde_derive = { version = "1.0.186", default_features = false } serde_json = { version = "1.0.60", default-features = false, features = [ "alloc", ] } +sha2 = { version = "0.10.8", default-features = false } +slab = { version = "0.4.9", default-features = false } smallstr = { version = "0.3.0", default-features = false } snafu = { version = "0.8.0", default-features = false } unicase = "2.6.0" @@ -135,6 +137,8 @@ std = [ "cosmic-text?/std", "serde_json/std", "serde/std", + "sha2/std", + "slab/std", "simdutf8/std", "snafu/std", "time/local-offset", diff --git a/benches/layout_state.rs b/benches/layout_state.rs index 4cfdcd28..d9a5fe69 100644 --- a/benches/layout_state.rs +++ b/benches/layout_state.rs @@ -1,5 +1,5 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use livesplit_core::{run::parser::livesplit, Layout, Run, Segment, Timer}; +use livesplit_core::{run::parser::livesplit, settings::ImageCache, Layout, Run, Segment, Timer}; use std::fs; criterion_main!(benches); @@ -11,7 +11,7 @@ criterion_group!( reuse_artificial ); -fn artificial() -> (Timer, Layout) { +fn artificial() -> (Timer, Layout, ImageCache) { let mut run = Run::new(); run.set_game_name("Game"); run.set_category_name("Category"); @@ -20,51 +20,51 @@ fn artificial() -> (Timer, Layout) { let mut timer = Timer::new(run).unwrap(); timer.start(); - (timer, Layout::default_layout()) + (timer, Layout::default_layout(), ImageCache::new()) } -fn real() -> (Timer, Layout) { +fn real() -> (Timer, Layout, ImageCache) { let buf = fs::read_to_string("tests/run_files/Celeste - Any% (1.2.1.5).lss").unwrap(); let run = livesplit::parse(&buf).unwrap(); let mut timer = Timer::new(run).unwrap(); timer.start(); - (timer, Layout::default_layout()) + (timer, Layout::default_layout(), ImageCache::new()) } fn no_reuse_real(c: &mut Criterion) { - let (timer, mut layout) = real(); + let (timer, mut layout, mut image_cache) = real(); c.bench_function("No Reuse (Real)", move |b| { - b.iter(|| layout.state(&timer.snapshot())) + b.iter(|| layout.state(&mut image_cache, &timer.snapshot())) }); } fn reuse_real(c: &mut Criterion) { - let (timer, mut layout) = real(); + let (timer, mut layout, mut image_cache) = real(); - let mut state = layout.state(&timer.snapshot()); + let mut state = layout.state(&mut image_cache, &timer.snapshot()); c.bench_function("Reuse (Real)", move |b| { - b.iter(|| layout.update_state(&mut state, &timer.snapshot())) + b.iter(|| layout.update_state(&mut state, &mut image_cache, &timer.snapshot())) }); } fn no_reuse_artificial(c: &mut Criterion) { - let (timer, mut layout) = artificial(); + let (timer, mut layout, mut image_cache) = artificial(); c.bench_function("No Reuse (Artificial)", move |b| { - b.iter(|| layout.state(&timer.snapshot())) + b.iter(|| layout.state(&mut image_cache, &timer.snapshot())) }); } fn reuse_artificial(c: &mut Criterion) { - let (timer, mut layout) = artificial(); + let (timer, mut layout, mut image_cache) = artificial(); - let mut state = layout.state(&timer.snapshot()); + let mut state = layout.state(&mut image_cache, &timer.snapshot()); c.bench_function("Reuse (Artificial)", move |b| { - b.iter(|| layout.update_state(&mut state, &timer.snapshot())) + b.iter(|| layout.update_state(&mut state, &mut image_cache, &timer.snapshot())) }); } diff --git a/benches/scene_management.rs b/benches/scene_management.rs index 948c8553..be011731 100644 --- a/benches/scene_management.rs +++ b/benches/scene_management.rs @@ -7,7 +7,7 @@ cfg_if::cfg_if! { PathBuilder, ResourceAllocator, SceneManager, Label, FontKind, SharedOwnership, }, run::parser::livesplit, - settings::Font, + settings::{Font, ImageCache}, Run, Segment, TimeSpan, Timer, TimingMethod, }; use std::fs; @@ -81,16 +81,17 @@ cfg_if::cfg_if! { run.set_attempt_count(1337); let mut timer = Timer::new(run).unwrap(); let mut layout = Layout::default_layout(); + let mut image_cache = ImageCache::new(); start_run(&mut timer); make_progress_run_with_splits_opt(&mut timer, &[Some(5.0), None, Some(10.0)]); - let state = layout.state(&timer.snapshot()); + let state = layout.state(&mut image_cache, &timer.snapshot()); let mut manager = SceneManager::new(Dummy); c.bench_function("Scene Management (Default)", move |b| { - b.iter(|| manager.update_scene(Dummy, (300.0, 500.0), &state)) + b.iter(|| manager.update_scene(Dummy, (300.0, 500.0), &state, &image_cache)) }); } @@ -98,6 +99,7 @@ cfg_if::cfg_if! { let run = lss("tests/run_files/Celeste - Any% (1.2.1.5).lss"); let mut timer = Timer::new(run).unwrap(); let mut layout = lsl("tests/layout_files/subsplits.lsl"); + let mut image_cache = ImageCache::new(); start_run(&mut timer); make_progress_run_with_splits_opt( @@ -106,13 +108,12 @@ cfg_if::cfg_if! { ); let snapshot = timer.snapshot(); - let mut state = layout.state(&snapshot); - layout.update_state(&mut state, &snapshot); + let state = layout.state(&mut image_cache, &snapshot); let mut manager = SceneManager::new(Dummy); c.bench_function("Scene Management (Subsplits Layout)", move |b| { - b.iter(|| manager.update_scene(Dummy, (300.0, 800.0), &state)) + b.iter(|| manager.update_scene(Dummy, (300.0, 800.0), &state, &image_cache)) }); } diff --git a/benches/software_rendering.rs b/benches/software_rendering.rs index 19b0c374..af0c3296 100644 --- a/benches/software_rendering.rs +++ b/benches/software_rendering.rs @@ -6,6 +6,7 @@ cfg_if::cfg_if! { layout::{self, Layout}, rendering::software::Renderer, run::parser::livesplit, + settings::ImageCache, Run, Segment, TimeSpan, Timer, TimingMethod, }, std::fs, @@ -21,21 +22,16 @@ cfg_if::cfg_if! { run.set_attempt_count(1337); let mut timer = Timer::new(run).unwrap(); let mut layout = Layout::default_layout(); + let mut image_cache = ImageCache::new(); start_run(&mut timer); make_progress_run_with_splits_opt(&mut timer, &[Some(5.0), None, Some(10.0)]); - let snapshot = timer.snapshot(); - let mut state = layout.state(&snapshot); + let state = layout.state(&mut image_cache, &timer.snapshot()); let mut renderer = Renderer::new(); - // Do a single frame beforehand as otherwise the layout state will - // keep saying that the icons changed. - renderer.render(&state, [300, 500]); - layout.update_state(&mut state, &snapshot); - c.bench_function("Software Rendering (Default)", move |b| { - b.iter(|| renderer.render(&state, [300, 500])) + b.iter(|| renderer.render(&state, &image_cache, [300, 500])) }); } @@ -43,21 +39,16 @@ cfg_if::cfg_if! { let run = lss("tests/run_files/Celeste - Any% (1.2.1.5).lss"); let mut timer = Timer::new(run).unwrap(); let mut layout = lsl("tests/layout_files/subsplits.lsl"); + let mut image_cache = ImageCache::new(); start_run(&mut timer); make_progress_run_with_splits_opt(&mut timer, &[Some(10.0), None, Some(20.0), Some(55.0)]); - let snapshot = timer.snapshot(); - let mut state = layout.state(&snapshot); + let state = layout.state(&mut image_cache, &timer.snapshot()); let mut renderer = Renderer::new(); - // Do a single frame beforehand as otherwise the layout state will - // keep saying that the icons changed. - renderer.render(&state, [300, 800]); - layout.update_state(&mut state, &snapshot); - c.bench_function("Software Rendering (Subsplits Layout)", move |b| { - b.iter(|| renderer.render(&state, [300, 800])) + b.iter(|| renderer.render(&state, &image_cache, [300, 800])) }); } diff --git a/capi/Cargo.toml b/capi/Cargo.toml index 3a185350..d62e596e 100644 --- a/capi/Cargo.toml +++ b/capi/Cargo.toml @@ -12,7 +12,7 @@ crate-type = ["staticlib", "cdylib"] livesplit-core = { path = "..", default-features = false, features = ["std"] } serde_json = { version = "1.0.8", default-features = false } time = { version = "0.3.4", default-features = false, features = ["formatting"] } -simdutf8 = { version = "0.1.4", default-features = false } +simdutf8 = { git = "https://github.com/CryZe/simdutf8", branch = "wasm-ub-panic", default-features = false } [features] default = ["image-shrinking"] diff --git a/capi/bind_gen/src/main.rs b/capi/bind_gen/src/main.rs index 6c5c7098..ca782651 100644 --- a/capi/bind_gen/src/main.rs +++ b/capi/bind_gen/src/main.rs @@ -10,7 +10,6 @@ mod python; mod ruby; mod swift; mod typescript; -mod wasm; mod wasm_bindgen; use clap::Parser; @@ -310,19 +309,6 @@ fn write_files(classes: &BTreeMap, opt: &Opt) -> Result<()> { } path.pop(); - path.push("wasm"); - create_dir_all(&path)?; - { - path.push("livesplit_core.js"); - wasm::write(BufWriter::new(File::create(&path)?), classes, false)?; - path.pop(); - - path.push("livesplit_core.ts"); - wasm::write(BufWriter::new(File::create(&path)?), classes, true)?; - path.pop(); - } - path.pop(); - path.push("wasm_bindgen"); create_dir_all(&path)?; { diff --git a/capi/bind_gen/src/typescript.ts b/capi/bind_gen/src/typescript.ts index e71c8b46..6677ec8a 100644 --- a/capi/bind_gen/src/typescript.ts +++ b/capi/bind_gen/src/typescript.ts @@ -35,6 +35,48 @@ export type ListGradient = { Same: Gradient } | { Alternating: Color[] }; +/** + * The ID of an image that can be used for looking up an image in an image + * cache. + */ +export type ImageId = string; + +/** + * A constant that is part of the formula to calculate the sigma of a gaussian + * blur for a background image. Check its documentation for a deeper + * explanation. + */ +export const BLUR_FACTOR = 0.05; + +export interface BackgroundImage { + /** The image ID to look up the actual image in an image cache. */ + image: ImageId, + /** + * The brightness of the image in the range from `0` to `1`. This is for + * darkening the image if it's too bright. + */ + brightness: number, + /** + * The opacity of the image in the range from `0` to `1`. This is for making + * the image more transparent. + */ + opacity: number, + /** + * An additional gaussian blur that is applied to the image. It is in the + * range from `0` to `1` and is meant to be multiplied with the larger of + * the two dimensions of the image to ensure that the blur is independent of + * the resolution of the image and then multiplied by `BLUR_FACTOR` to scale + * it to a reasonable value. The resulting value is the sigma (standard + * deviation) of the gaussian blur. + * + * sigma = BLUR_FACTOR * blur * max(width, height) + */ + blur: number, +} + +/** The background of a layout. */ +export type LayoutBackground = Gradient | BackgroundImage; + /** Describes the Alignment of the Title in the Title Component. */ export type Alignment = "Auto" | "Left" | "Center"; @@ -215,12 +257,11 @@ export interface TitleComponentStateJson { */ text_color: Color | null, /** - * The game's icon encoded as a Data URL. This value is only specified - * whenever the icon changes. If you explicitly want to query this value, - * remount the component. The String itself may be empty. This indicates - * that there is no icon. + * The game icon to show. The associated image can be looked up in the image + * cache. The image may be the empty image. This indicates that there is no + * icon. */ - icon_change: string | null, + icon: ImageId, /** * The first title line to show. This is either the game's name, or a * combination of the game's name and the category. This is a list of all @@ -265,13 +306,6 @@ export interface SplitsComponentStateJson { column_labels: string[] | null, /** The list of all the segments to visualize. */ splits: SplitStateJson[], - /** - * This list describes all the icon changes that happened. Each time a - * segment is first shown or its icon changes, the new icon is provided in - * this list. If necessary, you may remount this component to reset the - * component into a state where these icons are provided again. - */ - icon_changes: SplitsComponentIconChangeJson[], /** * Specifies whether the current run has any icons, even those that are not * currently visible by the splits component. This allows for properly @@ -301,29 +335,14 @@ export interface SplitsComponentStateJson { current_split_gradient: Gradient, } -/** - * Describes the icon to be shown for a certain segment. This is provided - * whenever a segment is first shown or whenever its icon changes. If necessary, - * you may remount this component to reset the component into a state where - * these icons are provided again. - */ -export interface SplitsComponentIconChangeJson { - /** - * The index of the segment of which the icon changed. This is based on the - * index in the run, not on the index of the `SplitStateJson` in the - * `SplitsComponentStateJson` object. The corresponding index is the `index` - * field of the `SplitStateJson` object. - */ - segment_index: number, - /** - * The segment's icon encoded as a Data URL. The String itself may be empty. - * This indicates that there is no icon. - */ - icon: string, -} - /** The state object that describes a single segment's information to visualize. */ export interface SplitStateJson { + /** + * The icon of the segment. The associated image can be looked up in the + * image cache. The image may be the empty image. This indicates that there + * is no icon. + */ + icon: ImageId, /** The name of the segment. */ name: string, /** @@ -524,12 +543,11 @@ export interface DetailedTimerComponentStateJson { */ segment_name: string | null, /** - * The segment's icon encoded as a Data URL. This value is only specified - * whenever the icon changes. If you explicitly want to query this value, - * remount the component. The String itself may be empty. This indicates - * that there is no icon. + * The icon of the segment. The associated image can be looked up in the + * image cache. The image may be the empty image. This indicates that there + * is no icon. */ - icon_change: string | null, + icon: ImageId, /** * The color of the segment name if it's shown. If `null` is specified, the * color is taken from the layout. @@ -644,6 +662,7 @@ export type SettingsDescriptionValueJson = { LayoutDirection: LayoutDirection } | { Font: Font | null } | { DeltaGradient: DeltaGradient } | + { LayoutBackground: LayoutBackground } | { CustomCombobox: CustomCombobox }; /** Describes the kind of a column. */ @@ -725,11 +744,11 @@ export type DigitsFormatJson = */ export interface RunEditorStateJson { /** - * The game's icon encoded as a Data URL. This value is only specified - * whenever the icon changes. The String itself may be empty. This - * indicates that there is no icon. + * The game icon of the run. The associated image can be looked up in the + * image cache. The image may be the empty image. This indicates that there + * is no icon. */ - icon_change: string | null, + icon: ImageId, /** The name of the game the Run is for. */ game: string, /** The name of the category the Run is for. */ @@ -854,11 +873,11 @@ export interface RunEditorButtonsJson { /** Describes the current state of a segment. */ export interface RunEditorRowJson { /** - * The segment's icon encoded as a Data URL. This value is only specified - * whenever the icon changes. The String itself may be empty. This - * indicates that there is no icon. + * The icon of the segment. The associated image can be looked up in the + * image cache. The image may be the empty image. This indicates that there + * is no icon. */ - icon_change: string | null, + icon: ImageId, /** The name of the segment. */ name: string, /** The segment's split time for the active timing method. */ diff --git a/capi/bind_gen/src/wasm.rs b/capi/bind_gen/src/wasm.rs deleted file mode 100644 index 1b2bfb7e..00000000 --- a/capi/bind_gen/src/wasm.rs +++ /dev/null @@ -1,990 +0,0 @@ -use crate::{typescript, Class, Function, Type, TypeKind}; -use heck::ToLowerCamelCase; -use std::{ - collections::BTreeMap, - io::{Result, Write}, -}; - -fn get_hl_type_with_null(ty: &Type) -> String { - let mut formatted = get_hl_type_without_null(ty); - if ty.is_nullable { - formatted.push_str(" | null"); - } - formatted -} - -fn get_hl_type_without_null(ty: &Type) -> String { - if ty.is_custom { - match ty.kind { - TypeKind::Ref => format!("{}Ref", ty.name), - TypeKind::RefMut => format!("{}RefMut", ty.name), - TypeKind::Value => ty.name.clone(), - } - } else { - match (ty.kind, ty.name.as_str()) { - (TypeKind::Ref, "c_char") => "string", - (_, t) if !ty.is_custom => match t { - "i8" => "number", - "i16" => "number", - "i32" => "number", - "i64" => "number", - "u8" => "number", - "u16" => "number", - "u32" => "number", - "u64" => "number", - "usize" => "number", - "isize" => "number", - "f32" => "number", - "f64" => "number", - "bool" => "boolean", - "()" => "void", - "c_char" => "string", - "Json" => "any", - x => x, - }, - _ => unreachable!(), - } - .to_string() - } -} - -fn write_class_comments(mut writer: W, comments: &[String]) -> Result<()> { - write!( - writer, - r#" -/**"# - )?; - - for comment in comments { - write!( - writer, - r#" - * {}"#, - comment - .replace("", "null") - .replace("", "true") - .replace("", "false") - )?; - } - - write!( - writer, - r#" - */"# - ) -} - -fn write_fn(mut writer: W, function: &Function, type_script: bool) -> Result<()> { - let is_static = function.is_static(); - let has_return_type = function.has_return_type(); - let return_type_with_null = get_hl_type_with_null(&function.output); - let return_type_without_null = get_hl_type_without_null(&function.output); - let method = function.method.to_lower_camel_case(); - let is_json = has_return_type && function.output.name == "Json"; - - if !function.comments.is_empty() || !type_script { - write!( - writer, - r#" - /**"# - )?; - - for comment in &function.comments { - write!( - writer, - r#" - * {}"#, - comment - .replace("", "null") - .replace("", "true") - .replace("", "false") - )?; - } - - if type_script { - write!( - writer, - r#" - */"# - )?; - } - } - - if !type_script { - for (name, ty) in function.inputs.iter().skip(usize::from(!is_static)) { - write!( - writer, - r#" - * @param {{{}}} {}"#, - get_hl_type_with_null(ty), - name.to_lower_camel_case() - )?; - } - - if has_return_type { - write!( - writer, - r#" - * @return {{{return_type_with_null}}}"# - )?; - } - - write!( - writer, - r#" - */"# - )?; - } - - write!( - writer, - r#" - {}{}("#, - if is_static { "static " } else { "" }, - method - )?; - - for (i, (name, ty)) in function - .inputs - .iter() - .skip(usize::from(!is_static)) - .enumerate() - { - if i != 0 { - write!(writer, ", ")?; - } - write!(writer, "{}", name.to_lower_camel_case())?; - if type_script { - write!(writer, ": {}", get_hl_type_with_null(ty))?; - } - } - - if type_script && has_return_type { - write!( - writer, - r#"): {return_type_with_null} {{ - "# - )?; - } else { - write!( - writer, - r#") {{ - "# - )?; - } - - for (name, typ) in function.inputs.iter() { - if typ.is_custom { - write!( - writer, - r#"if ({name}.ptr == 0) {{ - throw "{name} is disposed"; - }} - "#, - name = name.to_lower_camel_case() - )?; - } - } - - for (name, typ) in function.inputs.iter() { - let hl_type = get_hl_type_without_null(typ); - if hl_type == "string" { - write!( - writer, - r#"const {0}_allocated = allocString({0}); - "#, - name.to_lower_camel_case() - )?; - } else if typ.name == "Json" { - write!( - writer, - r#"const {0}_allocated = allocString(JSON.stringify({0})); - "#, - name.to_lower_camel_case() - )?; - } - } - - if has_return_type { - if function.output.is_custom { - write!(writer, r#"const result = new {return_type_without_null}("#)?; - } else { - write!(writer, "const result = ")?; - } - } - - write!(writer, r#"instance().exports.{}("#, &function.name)?; - - for (i, (name, typ)) in function.inputs.iter().enumerate() { - let type_name = get_hl_type_without_null(typ); - if i != 0 { - write!(writer, ", ")?; - } - write!( - writer, - "{}", - if name == "this" { - "this.ptr".to_string() - } else if type_name == "string" || typ.name == "Json" { - format!("{}_allocated.ptr", name.to_lower_camel_case()) - } else if typ.is_custom { - format!("{}.ptr", name.to_lower_camel_case()) - } else { - name.to_lower_camel_case() - } - )?; - if type_name == "boolean" { - write!(writer, " ? 1 : 0")?; - } - } - - write!( - writer, - "){}", - if return_type_without_null == "boolean" { - " != 0" - } else { - "" - } - )?; - - if has_return_type && function.output.is_custom { - write!(writer, r#")"#)?; - } - - write!(writer, r#";"#)?; - - for (name, typ) in function.inputs.iter() { - let hl_type = get_hl_type_without_null(typ); - if hl_type == "string" || typ.name == "Json" { - write!( - writer, - r#" - dealloc({}_allocated);"#, - name.to_lower_camel_case() - )?; - } - } - - for (name, typ) in function.inputs.iter() { - if typ.is_custom && typ.kind == TypeKind::Value { - write!( - writer, - r#" - {}.ptr = 0;"#, - name.to_lower_camel_case() - )?; - } - } - - if has_return_type { - if function.output.is_nullable { - if function.output.is_custom { - write!( - writer, - r#" - if (result.ptr == 0) {{ - return null; - }}"# - )?; - } else { - write!( - writer, - r#" - if (result == 0) {{ - return null; - }}"# - )?; - } - } - if is_json { - write!( - writer, - r#" - return JSON.parse(decodeString(result));"# - )?; - } else if return_type_without_null == "string" { - write!( - writer, - r#" - return decodeString(result);"# - )?; - } else { - write!( - writer, - r#" - return result;"# - )?; - } - } - - write!( - writer, - r#" - }}"# - )?; - - Ok(()) -} - -pub fn write( - mut writer: W, - classes: &BTreeMap, - type_script: bool, -) -> Result<()> { - if type_script { - writeln!( - writer, - "{}{}", - r#"// tslint:disable -let wasm: WebAssembly.ResultObject | null = null; - -declare namespace WebAssembly { - class Module { - constructor(bufferSource: ArrayBuffer | Uint8Array); - - public static customSections(module: Module, sectionName: string): ArrayBuffer[]; - public static exports(module: Module): Array<{ - name: string; - kind: string; - }>; - public static imports(module: Module): Array<{ - module: string; - name: string; - kind: string; - }>; - } - - class Instance { - public readonly exports: any; - constructor(module: Module, importObject?: any); - } - - interface ResultObject { - module: Module; - instance: Instance; - } - - function instantiate(bufferSource: ArrayBuffer | Uint8Array, importObject?: any): Promise; - function instantiateStreaming(source: Response | Promise, importObject?: any): Promise; -} - -declare class TextEncoder { - constructor(label?: string, options?: TextEncoding.TextEncoderOptions); - encoding: string; - encode(input?: string, options?: TextEncoding.TextEncodeOptions): Uint8Array; -} - -declare class TextDecoder { - constructor(utfLabel?: string, options?: TextEncoding.TextDecoderOptions) - encoding: string; - fatal: boolean; - ignoreBOM: boolean; - decode(input?: ArrayBufferView, options?: TextEncoding.TextDecodeOptions): string; -} - -declare namespace TextEncoding { - interface TextDecoderOptions { - fatal?: boolean; - ignoreBOM?: boolean; - } - - interface TextDecodeOptions { - stream?: boolean; - } - - interface TextEncoderOptions { - NONSTANDARD_allowLegacyEncoding?: boolean; - } - - interface TextEncodeOptions { - stream?: boolean; - } - - interface TextEncodingStatic { - TextDecoder: typeof TextDecoder; - TextEncoder: typeof TextEncoder; - } -} - -function instance(): WebAssembly.Instance { - if (wasm == null) { - throw "You need to await load()"; - } - return wasm.instance; -} - -const handleMap: Map = new Map(); - -export async function load(path?: string) { - const imports = { - env: { - Instant_now: function (): number { - return performance.now() / 1000; - }, - Date_now: function (ptr: number) { - const date = new Date(); - const milliseconds = date.valueOf(); - const u32Max = 0x100000000; - const seconds = milliseconds / 1000; - const secondsHigh = (seconds / u32Max) | 0; - const secondsLow = (seconds % u32Max) | 0; - const nanos = ((milliseconds % 1000) * 1000000) | 0; - const u32Slice = new Uint32Array(instance().exports.memory.buffer, ptr); - u32Slice[0] = secondsLow; - u32Slice[1] = secondsHigh; - u32Slice[2] = nanos; - }, - HotkeyHook_new: function (handle: number) { - const listener = (ev: KeyboardEvent) => { - const { ptr, len } = allocString(ev.code); - instance().exports.HotkeyHook_callback(ptr, len - 1, handle); - dealloc({ ptr, len }); - }; - window.addEventListener("keypress", listener); - handleMap.set(handle, listener); - }, - HotkeyHook_drop: function (handle: number) { - window.removeEventListener("keypress", handleMap.get(handle)); - handleMap.delete(handle); - }, - }, - }; - - let request = fetch(path || 'livesplit_core.wasm'); - if (typeof WebAssembly.instantiateStreaming === "function") { - try { - wasm = await WebAssembly.instantiateStreaming(request, imports); - return; - } catch { } - // We retry with the normal instantiate here because Chrome 60 seems to - // have instantiateStreaming, but it doesn't actually work. - request = fetch(path || 'livesplit_core.wasm'); - } - const response = await request; - const bytes = await response.arrayBuffer(); - wasm = await WebAssembly.instantiate(bytes, imports); -} - -const encoder = new TextEncoder("UTF-8"); -const decoder = new TextDecoder("UTF-8"); -const encodeUtf8: (str: string) => Uint8Array = (str) => encoder.encode(str); -const decodeUtf8: (data: Uint8Array) => string = (data) => decoder.decode(data); - -interface Slice { - ptr: number, - len: number, -} - -function allocInt8Array(src: Int8Array): Slice { - const len = src.length; - const ptr = instance().exports.alloc(len); - const slice = new Uint8Array(instance().exports.memory.buffer, ptr, len); - - slice.set(src); - - return { ptr, len }; -} - -function allocString(str: string): Slice { - const stringBuffer = encodeUtf8(str); - const len = stringBuffer.length + 1; - const ptr = instance().exports.alloc(len); - const slice = new Uint8Array(instance().exports.memory.buffer, ptr, len); - - slice.set(stringBuffer); - slice[len - 1] = 0; - - return { ptr, len }; -} - -function decodeString(ptr: number): string { - const memory = new Uint8Array(instance().exports.memory.buffer); - let end = ptr; - while (memory[end] !== 0) { - end += 1; - } - const slice = memory.slice(ptr, end); - return decodeUtf8(slice); -} - -function dealloc(slice: Slice) { - instance().exports.dealloc(slice.ptr, slice.len); -} - -"#, - typescript::HEADER, - )?; - } else { - writeln!( - writer, - "{}", - r#"let wasm = null; - -function instance() { - if (wasm == null) { - throw "You need to await load()"; - } - return wasm.instance; -} - -const handleMap = new Map(); - -exports.load = async function (path) { - const imports = { - env: { - Instant_now: function () { - return performance.now() / 1000; - }, - Date_now: function (ptr) { - const date = new Date(); - const milliseconds = date.valueOf(); - const u32Max = 0x100000000; - const seconds = milliseconds / 1000; - const secondsHigh = (seconds / u32Max) | 0; - const secondsLow = (seconds % u32Max) | 0; - const nanos = ((milliseconds % 1000) * 1000000) | 0; - const u32Slice = new Uint32Array(instance().exports.memory.buffer, ptr); - u32Slice[0] = secondsLow; - u32Slice[1] = secondsHigh; - u32Slice[2] = nanos; - }, - HotkeyHook_new: function (handle) { - const listener = (ev) => { - const { ptr, len } = allocString(ev.code); - instance().exports.HotkeyHook_callback(ptr, len - 1, handle); - dealloc({ ptr, len }); - }; - window.addEventListener("keypress", listener); - handleMap.set(handle, listener); - }, - HotkeyHook_drop: function (handle) { - window.removeEventListener("keypress", handleMap.get(handle)); - handleMap.delete(handle); - }, - }, - }; - - let request = fetch(path || 'livesplit_core.wasm'); - if (typeof WebAssembly.instantiateStreaming === "function") { - try { - wasm = await WebAssembly.instantiateStreaming(request, imports); - return; - } catch { } - // We retry with the normal instantiate here because Chrome 60 seems to - // have instantiateStreaming, but it doesn't actually work. - request = fetch(path || 'livesplit_core.wasm'); - } - const response = await request; - const bytes = await response.arrayBuffer(); - wasm = await WebAssembly.instantiate(bytes, imports); -} - -const encoder = new TextEncoder("UTF-8"); -const decoder = new TextDecoder("UTF-8"); -const encodeUtf8 = (str) => encoder.encode(str); -const decodeUtf8 = (data) => decoder.decode(data); - -function allocInt8Array(src) { - const len = src.length; - const ptr = instance().exports.alloc(len); - const slice = new Uint8Array(instance().exports.memory.buffer, ptr, len); - - slice.set(src); - - return { ptr, len }; -} - -function allocString(str) { - const stringBuffer = encodeUtf8(str); - const len = stringBuffer.length + 1; - const ptr = instance().exports.alloc(len); - const slice = new Uint8Array(instance().exports.memory.buffer, ptr, len); - - slice.set(stringBuffer); - slice[len - 1] = 0; - - return { ptr, len }; -} - -function decodeString(ptr) { - const memory = new Uint8Array(instance().exports.memory.buffer); - let end = ptr; - while (memory[end] !== 0) { - end += 1; - } - const slice = memory.slice(ptr, end); - return decodeUtf8(slice); -} - -function dealloc(slice) { - instance().exports.dealloc(slice.ptr, slice.len); -}"#, - )?; - } - - for (class_name, class) in classes { - let class_name_ref = format!("{class_name}Ref"); - let class_name_ref_mut = format!("{class_name}RefMut"); - - write_class_comments(&mut writer, &class.comments)?; - - write!( - writer, - r#" -{export}class {class} {{"#, - class = class_name_ref, - export = if type_script { "export " } else { "" } - )?; - - if type_script { - write!( - writer, - r#" - ptr: number;"# - )?; - } - - for function in &class.shared_fns { - write_fn(&mut writer, function, type_script)?; - } - - if class_name == "SharedTimer" { - if type_script { - write!( - writer, - "{}", - r#" - readWith(action: (timer: TimerRef) => T): T { - return this.read().with(function (lock) { - return action(lock.timer()); - }); - } - writeWith(action: (timer: TimerRefMut) => T): T { - return this.write().with(function (lock) { - return action(lock.timer()); - }); - }"# - )?; - } else { - write!( - writer, - "{}", - r#" - /** - * @param {function(TimerRef)} action - */ - readWith(action) { - return this.read().with(function (lock) { - return action(lock.timer()); - }); - } - /** - * @param {function(TimerRefMut)} action - */ - writeWith(action) { - return this.write().with(function (lock) { - return action(lock.timer()); - }); - }"# - )?; - } - } - - if type_script { - write!( - writer, - r#" - /** - * This constructor is an implementation detail. Do not use this. - */ - constructor(ptr: number) {{"# - )?; - } else { - write!( - writer, - r#" - /** - * This constructor is an implementation detail. Do not use this. - * @param {{number}} ptr - */ - constructor(ptr) {{"# - )?; - } - - write!( - writer, - r#" - this.ptr = ptr; - }} -}} -"# - )?; - - if !type_script { - writeln!(writer, r#"exports.{class_name_ref} = {class_name_ref};"#)?; - } - - write_class_comments(&mut writer, &class.comments)?; - - write!( - writer, - r#" -{export}class {class} extends {base_class} {{"#, - class = class_name_ref_mut, - base_class = class_name_ref, - export = if type_script { "export " } else { "" } - )?; - - for function in &class.mut_fns { - write_fn(&mut writer, function, type_script)?; - } - - if class_name == "RunEditor" { - if type_script { - write!( - writer, - "{}", - r#" - setGameIconFromArray(data: Int8Array) { - const slice = allocInt8Array(data); - this.setGameIcon(slice.ptr, slice.len); - dealloc(slice); - } - activeSetIconFromArray(data: Int8Array) { - const slice = allocInt8Array(data); - this.activeSetIcon(slice.ptr, slice.len); - dealloc(slice); - }"# - )?; - } else { - write!( - writer, - "{}", - r#" - /** - * @param {Int8Array} data - */ - setGameIconFromArray(data) { - const slice = allocInt8Array(data); - this.setGameIcon(slice.ptr, slice.len); - dealloc(slice); - } - /** - * @param {Int8Array} data - */ - activeSetIconFromArray(data) { - const slice = allocInt8Array(data); - this.activeSetIcon(slice.ptr, slice.len); - dealloc(slice); - }"# - )?; - } - } - - write!( - writer, - r#" -}} -"# - )?; - - if !type_script { - writeln!( - writer, - r#"exports.{class_name_ref_mut} = {class_name_ref_mut};"# - )?; - } - - write_class_comments(&mut writer, &class.comments)?; - - write!( - writer, - r#" -{export}class {class} extends {base_class} {{"#, - class = class_name, - base_class = class_name_ref_mut, - export = if type_script { "export " } else { "" } - )?; - - if type_script { - write!( - writer, - r#" - /** - * Allows for scoped usage of the object. The object is guaranteed to get - * disposed once this function returns. You are free to dispose the object - * early yourself anywhere within the scope. The scope's return value gets - * carried to the outside of this function. - */ - with(closure: (obj: {class_name}) => T): T {{"# - )?; - } else { - write!( - writer, - r#" - /** - * Allows for scoped usage of the object. The object is guaranteed to get - * disposed once this function returns. You are free to dispose the object - * early yourself anywhere within the scope. The scope's return value gets - * carried to the outside of this function. - * @param {{function({class_name})}} closure - */ - with(closure) {{"# - )?; - } - - write!( - writer, - r#" - try {{ - return closure(this); - }} finally {{ - this.dispose(); - }} - }} - /** - * Disposes the object, allowing it to clean up all of its memory. You need - * to call this for every object that you don't use anymore and hasn't - * already been disposed. - */ - dispose() {{ - if (this.ptr != 0) {{"# - )?; - - if let Some(function) = class.own_fns.iter().find(|f| f.method == "drop") { - write!( - writer, - r#" - instance().exports.{}(this.ptr);"#, - function.name - )?; - } - - write!( - writer, - r#" - this.ptr = 0; - }} - }}"# - )?; - - for function in class.static_fns.iter().chain(class.own_fns.iter()) { - if function.method != "drop" { - write_fn(&mut writer, function, type_script)?; - } - } - - if class_name == "Run" { - if type_script { - write!( - writer, - "{}", - r#" - static parseArray(data: Int8Array, loadFilesPath: string): ParseRunResult { - const slice = allocInt8Array(data); - const result = Run.parse(slice.ptr, slice.len, loadFilesPath); - dealloc(slice); - return result; - } - static parseString(text: string, loadFilesPath: string): ParseRunResult { - const slice = allocString(text); - const result = Run.parse(slice.ptr, slice.len, loadFilesPath); - dealloc(slice); - return result; - }"# - )?; - } else { - write!( - writer, - "{}", - r#" - /** - * @param {Int8Array} data - * @param {string} loadFilesPath - * @return {ParseRunResult} - */ - static parseArray(data, loadFilesPath) { - const slice = allocInt8Array(data); - const result = Run.parse(slice.ptr, slice.len, loadFilesPath); - dealloc(slice); - return result; - } - /** - * @param {string} text - * @param {string} loadFilesPath - * @return {ParseRunResult} - */ - static parseString(text, loadFilesPath) { - const slice = allocString(text); - const result = Run.parse(slice.ptr, slice.len, loadFilesPath); - dealloc(slice); - return result; - }"# - )?; - } - } else if class_name == "Layout" { - if type_script { - write!( - writer, - "{}", - r#" - static parseOriginalLivesplitArray(data: Int8Array): Layout | null { - const slice = allocInt8Array(data); - const result = Layout.parseOriginalLivesplit(slice.ptr, slice.len); - dealloc(slice); - return result; - } - static parseOriginalLivesplitString(text: string): Layout | null { - const slice = allocString(text); - const result = Layout.parseOriginalLivesplit(slice.ptr, slice.len); - dealloc(slice); - return result; - }"# - )?; - } else { - write!( - writer, - "{}", - r#" - /** - * @param {Int8Array} data - * @return {Layout | null} - */ - static parseOriginalLivesplitArray(data) { - const slice = allocInt8Array(data); - const result = Layout.parseOriginalLivesplit(slice.ptr, slice.len); - dealloc(slice); - return result; - } - /** - * @param {string} text - * @return {Layout | null} - */ - static parseOriginalLivesplitString(text) { - const slice = allocString(text); - const result = Layout.parseOriginalLivesplit(slice.ptr, slice.len); - dealloc(slice); - return result; - }"# - )?; - } - } - - writeln!( - writer, - r#" -}}{export}"#, - export = if type_script { - "".to_string() - } else { - format!( - r#" -exports.{class_name} = {class_name};"# - ) - } - )?; - } - - Ok(()) -} diff --git a/capi/bind_gen/src/wasm_bindgen.rs b/capi/bind_gen/src/wasm_bindgen.rs index ac9d553f..7cc1ae46 100644 --- a/capi/bind_gen/src/wasm_bindgen.rs +++ b/capi/bind_gen/src/wasm_bindgen.rs @@ -425,6 +425,11 @@ function decodeSlice(ptr: number): Uint8Array { return memory.slice(ptr, ptr + len); } +function decodePtrLen(ptr: number, len: number): Uint8Array { + const memory = new Uint8Array(wasm.memory.buffer); + return memory.slice(ptr, ptr + len); +} + function decodeString(ptr: number): string { return decodeUtf8(decodeSlice(ptr)); } @@ -476,6 +481,11 @@ function decodeSlice(ptr) { return memory.slice(ptr, ptr + len); } +function decodePtrLen(ptr, len) { + const memory = new Uint8Array(wasm.memory.buffer); + return memory.slice(ptr, ptr + len); +} + function decodeString(ptr) { return decodeUtf8(decodeSlice(ptr)); } @@ -609,6 +619,61 @@ export class {class_name_ref} {{"#, } const result = wasm.Run_save_as_lss(this.ptr); return decodeSlice(result); + }"# + )?; + } + } else if class_name == "ImageCache" { + if type_script { + write!( + writer, + "{}", + r#" + /** + * Looks up an image in the cache based on its image ID. The bytes are the image in its original + * file format. The format is not specified and can be any image format. The + * data may not even represent a valid image at all. If the image is not in the + * cache, null is returned. This does not mark the image as visited. + */ + lookupData(key: string): Uint8Array | undefined { + if (this.ptr == 0) { + throw "this is disposed"; + } + const key_allocated = allocString(key); + const ptr = wasm.ImageCache_lookup_data_ptr(this.ptr, key_allocated.ptr); + const len = wasm.ImageCache_lookup_data_len(this.ptr, key_allocated.ptr); + dealloc(key_allocated); + if (ptr === 0) { + return undefined; + } + return decodePtrLen(ptr, len); + }"# + )?; + } else { + write!( + writer, + "{}", + r#" + /** + * Looks up an image in the cache based on its image ID. The bytes are the image in its original + * file format. The format is not specified and can be any image format. The + * data may not even represent a valid image at all. If the image is not in the + * cache, null is returned. This does not mark the image as visited. + * + * @param {string} key + * @return {Uint8Array | undefined} + */ + lookupData(key) { + if (this.ptr == 0) { + throw "this is disposed"; + } + const key_allocated = allocString(key); + const ptr = wasm.ImageCache_lookup_data_ptr(this.ptr, key_allocated.ptr); + const len = wasm.ImageCache_lookup_data_len(this.ptr, key_allocated.ptr); + dealloc(key_allocated); + if (ptr === 0) { + return undefined; + } + return decodePtrLen(ptr, len); }"# )?; } @@ -693,6 +758,32 @@ export class {class_name_ref_mut} extends {class_name_ref} {{"#, const slice = allocUint8Array(data); this.activeSetIcon(slice.ptr, slice.len); dealloc(slice); + }"# + )?; + } + } else if class_name == "ImageCache" { + if type_script { + write!( + writer, + "{}", + r#" + cacheFromArray(data: Uint8Array, isLarge: boolean): string { + const slice = allocUint8Array(data); + const result = this.cache(slice.ptr, slice.len, isLarge); + dealloc(slice); + return result; + }"# + )?; + } else { + write!( + writer, + "{}", + r#" + cacheFromArray(data, isLarge) { + const slice = allocUint8Array(data); + const result = this.cache(slice.ptr, slice.len, isLarge); + dealloc(slice); + return result; }"# )?; } diff --git a/capi/src/auto_splitting_runtime.rs b/capi/src/auto_splitting_runtime.rs index f6ad19a3..68f66cdf 100644 --- a/capi/src/auto_splitting_runtime.rs +++ b/capi/src/auto_splitting_runtime.rs @@ -15,7 +15,7 @@ use livesplit_core::SharedTimer; #[allow(missing_docs)] pub struct AutoSplittingRuntime; -#[allow(missing_docs)] +#[allow(warnings)] #[cfg(not(feature = "auto-splitting"))] impl AutoSplittingRuntime { pub fn new() -> Self { diff --git a/capi/src/detailed_timer_component.rs b/capi/src/detailed_timer_component.rs index cbff4e23..58f6769c 100644 --- a/capi/src/detailed_timer_component.rs +++ b/capi/src/detailed_timer_component.rs @@ -4,10 +4,13 @@ //! comparisons, the segment icon, and the segment's name, can also be shown. use super::{output_vec, Json}; -use crate::component::OwnedComponent; -use crate::detailed_timer_component_state::OwnedDetailedTimerComponentState; -use livesplit_core::component::detailed_timer::Component as DetailedTimerComponent; -use livesplit_core::{GeneralLayoutSettings, Timer}; +use crate::{ + component::OwnedComponent, detailed_timer_component_state::OwnedDetailedTimerComponentState, +}; +use livesplit_core::{ + component::detailed_timer::Component as DetailedTimerComponent, settings::ImageCache, + GeneralLayoutSettings, Timer, +}; /// type pub type OwnedDetailedTimerComponent = Box; @@ -37,11 +40,12 @@ pub extern "C" fn DetailedTimerComponent_into_generic( #[no_mangle] pub extern "C" fn DetailedTimerComponent_state_as_json( this: &mut DetailedTimerComponent, + image_cache: &mut ImageCache, timer: &Timer, layout_settings: &GeneralLayoutSettings, ) -> Json { output_vec(|o| { - this.state(&timer.snapshot(), layout_settings) + this.state(image_cache, &timer.snapshot(), layout_settings) .write_json(o) .unwrap(); }) @@ -52,8 +56,9 @@ pub extern "C" fn DetailedTimerComponent_state_as_json( #[no_mangle] pub extern "C" fn DetailedTimerComponent_state( this: &mut DetailedTimerComponent, + image_cache: &mut ImageCache, timer: &Timer, layout_settings: &GeneralLayoutSettings, ) -> OwnedDetailedTimerComponentState { - Box::new(this.state(&timer.snapshot(), layout_settings)) + Box::new(this.state(image_cache, &timer.snapshot(), layout_settings)) } diff --git a/capi/src/detailed_timer_component_state.rs b/capi/src/detailed_timer_component_state.rs index f2b4370d..5587925d 100644 --- a/capi/src/detailed_timer_component_state.rs +++ b/capi/src/detailed_timer_component_state.rs @@ -2,9 +2,7 @@ use super::{output_str, output_vec, Nullablec_char}; use livesplit_core::component::detailed_timer::State as DetailedTimerComponentState; -use std::io::Write; -use std::os::raw::c_char; -use std::ptr; +use std::{io::Write, os::raw::c_char, ptr}; /// type pub type OwnedDetailedTimerComponentState = Box; @@ -132,25 +130,14 @@ pub extern "C" fn DetailedTimerComponentState_comparison2_time( ) } -/// The data of the segment's icon. This value is only specified whenever the -/// icon changes. If you explicitly want to query this value, remount the -/// component. The buffer itself may be empty. This indicates that there is no +/// The icon of the segment. The associated image can be looked up in the image +/// cache. The image may be the empty image. This indicates that there is no /// icon. #[no_mangle] -pub extern "C" fn DetailedTimerComponentState_icon_change_ptr( +pub extern "C" fn DetailedTimerComponentState_icon( this: &DetailedTimerComponentState, -) -> *const u8 { - this.icon_change - .as_ref() - .map_or_else(ptr::null, |i| i.as_ptr()) -} - -/// The length of the data of the segment's icon. -#[no_mangle] -pub extern "C" fn DetailedTimerComponentState_icon_change_len( - this: &DetailedTimerComponentState, -) -> usize { - this.icon_change.as_ref().map_or(0, |i| i.len()) +) -> *const c_char { + output_str(this.icon.format_str(&mut [0; 64])) } /// The name of the segment. This may be if it's not supposed to be diff --git a/capi/src/image_cache.rs b/capi/src/image_cache.rs new file mode 100644 index 00000000..a98b3370 --- /dev/null +++ b/capi/src/image_cache.rs @@ -0,0 +1,94 @@ +//! A cache for images that allows looking up images by their ID. The cache uses +//! a garbage collection algorithm to remove images that have not been visited +//! since the last garbage collection. Functions updating the cache usually +//! don't run the garbage collection themselves, so make sure to call `collect` +//! every now and then to remove unvisited images. + +use std::{ffi::c_char, ptr, str::FromStr}; + +use livesplit_core::settings::{HasImageId, Image, ImageCache, ImageId}; + +use crate::{output_str, slice, str}; + +/// type +pub type OwnedImageCache = Box; +/// type +pub type NullableOwnedImageCache = Option; + +/// Creates a new image cache. +#[no_mangle] +pub extern "C" fn ImageCache_new() -> OwnedImageCache { + Box::new(ImageCache::new()) +} + +/// drop +#[no_mangle] +pub extern "C" fn ImageCache_drop(this: OwnedImageCache) { + drop(this); +} + +/// Looks up an image in the cache based on its image ID and returns a pointer +/// to the bytes that make up the image. The bytes are the image in its original +/// file format. The format is not specified and can be any image format. The +/// data may not even represent a valid image at all. If the image is not in the +/// cache, is returned. This does not mark the image as visited. +#[no_mangle] +pub unsafe extern "C" fn ImageCache_lookup_data_ptr( + this: &ImageCache, + key: *const c_char, +) -> *const u8 { + ImageId::from_str(str(key)) + .ok() + .and_then(|key| this.lookup(&key)) + .filter(|image| !image.is_empty()) + .map(|image| image.data().as_ptr()) + .unwrap_or(ptr::null()) +} + +/// Looks up an image in the cache based on its image ID and returns its byte +/// length. If the image is not in the cache, 0 is returned. This does not mark +/// the image as visited. +#[no_mangle] +pub unsafe extern "C" fn ImageCache_lookup_data_len( + this: &ImageCache, + key: *const c_char, +) -> usize { + ImageId::from_str(str(key)) + .ok() + .and_then(|key| this.lookup(&key)) + .map(|image| image.data().len()) + .unwrap_or_default() +} + +/// Caches an image and returns its image ID. The image is provided as a byte +/// array. The image ID is the hash of the image data and can be used to look up +/// the image in the cache. The image is marked as visited in the cache. If you +/// specify that the image is large, it gets considered a large image that may +/// be used as a background image. Otherwise it gets considered an icon. The +/// image is resized according to this information. +#[no_mangle] +pub unsafe extern "C" fn ImageCache_cache( + this: &mut ImageCache, + data: *const u8, + len: usize, + is_large: bool, +) -> *const c_char { + let image = Image::new( + slice(data, len).into(), + if is_large { Image::LARGE } else { Image::ICON }, + ); + let image_id = *image.image_id(); + this.cache(&image_id, || image); + let mut buf = [0; 64]; + output_str(image_id.format_str(&mut buf)) +} + +/// Runs the garbage collection of the cache. This removes images from the cache +/// that have not been visited since the last garbage collection. Not every +/// image that has not been visited is removed. There is a heuristic that keeps +/// a certain amount of images in the cache regardless of whether they have been +/// visited or not. Returns the amount of images that got collected. +#[no_mangle] +pub extern "C" fn ImageCache_collect(this: &mut ImageCache) -> usize { + this.collect() +} diff --git a/capi/src/layout.rs b/capi/src/layout.rs index 7e2e8428..ec629770 100644 --- a/capi/src/layout.rs +++ b/capi/src/layout.rs @@ -2,15 +2,13 @@ //! variety of information the runner is interested in. use super::{get_file, output_vec, str, Json}; -use crate::{component::OwnedComponent, layout_state::OwnedLayoutState}; +use crate::{component::OwnedComponent, layout_state::OwnedLayoutState, slice}; use livesplit_core::{ layout::{parser, LayoutSettings, LayoutState}, + settings::ImageCache, Layout, Timer, }; -use std::{ - io::{BufReader, Cursor}, - slice, -}; +use std::io::{BufReader, Cursor}; /// type pub type OwnedLayout = Box; @@ -79,20 +77,29 @@ pub unsafe extern "C" fn Layout_parse_original_livesplit( data: *const u8, length: usize, ) -> NullableOwnedLayout { - let data = simdutf8::basic::from_utf8(slice::from_raw_parts(data, length)).ok()?; + let data = simdutf8::basic::from_utf8(slice(data, length)).ok()?; Some(Box::new(parser::parse(data).ok()?)) } /// Calculates and returns the layout's state based on the timer provided. #[no_mangle] -pub extern "C" fn Layout_state(this: &mut Layout, timer: &Timer) -> OwnedLayoutState { - Box::new(this.state(&timer.snapshot())) +pub extern "C" fn Layout_state( + this: &mut Layout, + image_cache: &mut ImageCache, + timer: &Timer, +) -> OwnedLayoutState { + Box::new(this.state(image_cache, &timer.snapshot())) } /// Updates the layout's state based on the timer provided. #[no_mangle] -pub extern "C" fn Layout_update_state(this: &mut Layout, state: &mut LayoutState, timer: &Timer) { - this.update_state(state, &timer.snapshot()) +pub extern "C" fn Layout_update_state( + this: &mut Layout, + state: &mut LayoutState, + image_cache: &mut ImageCache, + timer: &Timer, +) { + this.update_state(state, image_cache, &timer.snapshot()) } /// Updates the layout's state based on the timer provided and encodes it as @@ -101,9 +108,10 @@ pub extern "C" fn Layout_update_state(this: &mut Layout, state: &mut LayoutState pub extern "C" fn Layout_update_state_as_json( this: &mut Layout, state: &mut LayoutState, + image_cache: &mut ImageCache, timer: &Timer, ) -> Json { - this.update_state(state, &timer.snapshot()); + this.update_state(state, image_cache, &timer.snapshot()); output_vec(|o| { state.write_json(o).unwrap(); }) @@ -112,9 +120,15 @@ pub extern "C" fn Layout_update_state_as_json( /// Calculates the layout's state based on the timer provided and encodes it as /// JSON. You can use this to visualize all of the components of a layout. #[no_mangle] -pub extern "C" fn Layout_state_as_json(this: &mut Layout, timer: &Timer) -> Json { +pub extern "C" fn Layout_state_as_json( + this: &mut Layout, + image_cache: &mut ImageCache, + timer: &Timer, +) -> Json { output_vec(|o| { - this.state(&timer.snapshot()).write_json(o).unwrap(); + this.state(image_cache, &timer.snapshot()) + .write_json(o) + .unwrap(); }) } @@ -143,12 +157,3 @@ pub extern "C" fn Layout_scroll_up(this: &mut Layout) { pub extern "C" fn Layout_scroll_down(this: &mut Layout) { this.scroll_down(); } - -/// Remounts all the components as if they were freshly initialized. Some -/// components may only provide some information whenever it changes or when -/// their state is first queried. Remounting returns this information again, -/// whenever the layout's state is queried the next time. -#[no_mangle] -pub extern "C" fn Layout_remount(this: &mut Layout) { - this.remount(); -} diff --git a/capi/src/layout_editor.rs b/capi/src/layout_editor.rs index 89585266..74de44e2 100644 --- a/capi/src/layout_editor.rs +++ b/capi/src/layout_editor.rs @@ -8,7 +8,7 @@ use crate::{ component::OwnedComponent, layout::OwnedLayout, layout_editor_state::OwnedLayoutEditorState, setting_value::OwnedSettingValue, }; -use livesplit_core::{layout::LayoutState, LayoutEditor, Timer}; +use livesplit_core::{layout::LayoutState, settings::ImageCache, LayoutEditor, Timer}; /// type pub type OwnedLayoutEditor = Box; @@ -33,16 +33,22 @@ pub extern "C" fn LayoutEditor_close(this: OwnedLayoutEditor) -> OwnedLayout { /// Encodes the Layout Editor's state as JSON in order to visualize it. #[no_mangle] -pub extern "C" fn LayoutEditor_state_as_json(this: &LayoutEditor) -> Json { +pub extern "C" fn LayoutEditor_state_as_json( + this: &LayoutEditor, + image_cache: &mut ImageCache, +) -> Json { output_vec(|o| { - this.state().write_json(o).unwrap(); + this.state(image_cache).write_json(o).unwrap(); }) } /// Returns the state of the Layout Editor. #[no_mangle] -pub extern "C" fn LayoutEditor_state(this: &LayoutEditor) -> OwnedLayoutEditorState { - Box::new(this.state()) +pub extern "C" fn LayoutEditor_state( + this: &LayoutEditor, + image_cache: &mut ImageCache, +) -> OwnedLayoutEditorState { + Box::new(this.state(image_cache)) } /// Encodes the layout's state as JSON based on the timer provided. You can use @@ -51,10 +57,13 @@ pub extern "C" fn LayoutEditor_state(this: &LayoutEditor) -> OwnedLayoutEditorSt #[no_mangle] pub extern "C" fn LayoutEditor_layout_state_as_json( this: &mut LayoutEditor, + image_cache: &mut ImageCache, timer: &Timer, ) -> Json { output_vec(|o| { - this.layout_state(&timer.snapshot()).write_json(o).unwrap(); + this.layout_state(image_cache, &timer.snapshot()) + .write_json(o) + .unwrap(); }) } @@ -63,9 +72,10 @@ pub extern "C" fn LayoutEditor_layout_state_as_json( pub extern "C" fn LayoutEditor_update_layout_state( this: &mut LayoutEditor, state: &mut LayoutState, + image_cache: &mut ImageCache, timer: &Timer, ) { - this.update_layout_state(state, &timer.snapshot()) + this.update_layout_state(state, image_cache, &timer.snapshot()) } /// Updates the layout's state based on the timer provided and encodes it as @@ -74,9 +84,10 @@ pub extern "C" fn LayoutEditor_update_layout_state( pub extern "C" fn LayoutEditor_update_layout_state_as_json( this: &mut LayoutEditor, state: &mut LayoutState, + image_cache: &mut ImageCache, timer: &Timer, ) -> Json { - this.update_layout_state(state, &timer.snapshot()); + this.update_layout_state(state, image_cache, &timer.snapshot()); output_vec(|o| { state.write_json(o).unwrap(); }) @@ -159,6 +170,7 @@ pub extern "C" fn LayoutEditor_set_general_settings_value( this: &mut LayoutEditor, index: usize, value: OwnedSettingValue, + image_cache: &ImageCache, ) { - this.set_general_settings_value(index, *value); + this.set_general_settings_value(index, *value, image_cache); } diff --git a/capi/src/lib.rs b/capi/src/lib.rs index c75fad5b..b2d1aa8a 100644 --- a/capi/src/lib.rs +++ b/capi/src/lib.rs @@ -1,5 +1,13 @@ +#![warn( + clippy::complexity, + clippy::correctness, + clippy::perf, + clippy::style, + clippy::needless_pass_by_ref_mut, + missing_docs, + rust_2018_idioms +)] #![allow(clippy::missing_safety_doc, non_camel_case_types, non_snake_case)] -#![warn(missing_docs)] //! mod @@ -9,7 +17,7 @@ use std::{ fs::File, mem::ManuallyDrop, os::raw::c_char, - ptr, + ptr, slice, }; pub mod analysis; @@ -30,6 +38,7 @@ pub mod graph_component; pub mod graph_component_state; pub mod hotkey_config; pub mod hotkey_system; +pub mod image_cache; pub mod key_value_component_state; pub mod layout; pub mod layout_editor; @@ -88,12 +97,12 @@ pub type Json = *const c_char; pub type Nullablec_char = c_char; thread_local! { - static OUTPUT_VEC: RefCell> = RefCell::new(Vec::new()); - static TIME_SPAN: Cell = Cell::default(); - static TIME: Cell