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