From 39cbf7036f65b68374583a0b71e2a07b916db4be Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 24 Mar 2026 15:29:39 +0900 Subject: [PATCH 01/31] feat(bench): add FrameLoop-based pan benchmark to grida-dev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `run_frameloop_pan_pass` — a real-time GPU benchmark that goes through the actual `FrameLoop` decision path instead of calling queue_unstable/ queue_stable directly. This is the only benchmark that captures the real user-facing bottleneck: stable frames interrupting pan interactions. The pass uses real `thread::sleep()` at 60fps cadence, real wall-clock timestamps for FrameLoop, and the exact same apply_changes → build_plan → flush_with_plan code path as `Application::frame()`. Seven scroll interval scenarios sweep from 16ms (fast flick) to 500ms (discrete clicks), revealing how FrameLoop's stable frame decisions affect frame time distribution at each interaction speed. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/grida-dev/src/bench/runner.rs | 585 ++++++++++++++++++++++----- 1 file changed, 494 insertions(+), 91 deletions(-) diff --git a/crates/grida-dev/src/bench/runner.rs b/crates/grida-dev/src/bench/runner.rs index 1dc4cd129f..51b57ac5d5 100644 --- a/crates/grida-dev/src/bench/runner.rs +++ b/crates/grida-dev/src/bench/runner.rs @@ -5,6 +5,7 @@ use cg::cg::prelude::*; use cg::node::factory::NodeFactory; use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::{Node, Scene, Size}; +use cg::runtime::frame_loop::{FrameLoop, FrameQuality}; use cg::runtime::scene::FrameFlushResult; use cg::window::headless::HeadlessGpu; use math2::transform::AffineTransform; @@ -44,7 +45,10 @@ fn warmup(renderer: &mut cg::runtime::scene::Renderer) { /// Measure a single frame including queue + flush. /// Returns (total_us, queue_us, draw_us, mid_flush_us, compositor_us, flush_us). -fn measure_frame(renderer: &mut cg::runtime::scene::Renderer, stable: bool) -> Option<(u64, u64, u64, u64, u64, u64)> { +fn measure_frame( + renderer: &mut cg::runtime::scene::Renderer, + stable: bool, +) -> Option<(u64, u64, u64, u64, u64, u64)> { let t0 = Instant::now(); if stable { renderer.queue_stable(); @@ -75,11 +79,7 @@ fn measure_frame(renderer: &mut cg::runtime::scene::Renderer, stable: bool) -> O /// Uses CONTINUOUS panning (one direction, then reverses) to trigger cache misses /// and expose frame drop outliers during area discovery/culling. /// Measures queue + flush per frame. Ends with a settle (stable) frame. -fn run_pan_pass_at( - renderer: &mut cg::runtime::scene::Renderer, - frames: u32, - dx: f32, -) -> PassStats { +fn run_pan_pass_at(renderer: &mut cg::runtime::scene::Renderer, frames: u32, dx: f32) -> PassStats { let wall_start = Instant::now(); let mut frame_times = Vec::with_capacity(frames as usize); let mut queue_us_acc = Vec::with_capacity(frames as usize); @@ -111,9 +111,14 @@ fn run_pan_pass_at( let settle_us = measure_settle(renderer); compute_pass_stats( - &frame_times, &queue_us_acc, &draw_us_acc, - &mid_flush_us_acc, &compositor_us_acc, &flush_us_acc, - wall, settle_us, + &frame_times, + &queue_us_acc, + &draw_us_acc, + &mid_flush_us_acc, + &compositor_us_acc, + &flush_us_acc, + wall, + settle_us, ) } @@ -178,9 +183,14 @@ fn run_zoom_pass_at( let settle_us = measure_settle(renderer); compute_pass_stats( - &frame_times, &queue_us_acc, &draw_us_acc, - &mid_flush_us_acc, &compositor_us_acc, &flush_us_acc, - wall, settle_us, + &frame_times, + &queue_us_acc, + &draw_us_acc, + &mid_flush_us_acc, + &compositor_us_acc, + &flush_us_acc, + wall, + settle_us, ) } @@ -299,8 +309,15 @@ fn compute_resize_stats( ) -> ResizePassStats { if total.is_empty() { return ResizePassStats { - avg_us: 0, min_us: 0, p50_us: 0, p95_us: 0, max_us: 0, - rebuild_us: 0, invalidate_us: 0, flush_us: 0, wall, + avg_us: 0, + min_us: 0, + p50_us: 0, + p95_us: 0, + max_us: 0, + rebuild_us: 0, + invalidate_us: 0, + flush_us: 0, + wall, }; } let mut sorted = total.to_vec(); @@ -331,8 +348,18 @@ fn compute_pass_stats( ) -> PassStats { if frame_times.is_empty() { return PassStats { - avg_us: 0, fps: 0.0, min_us: 0, p50_us: 0, p95_us: 0, p99_us: 0, max_us: 0, - queue_us: 0, draw_us: 0, mid_flush_us: 0, compositor_us: 0, flush_us: 0, + avg_us: 0, + fps: 0.0, + min_us: 0, + p50_us: 0, + p95_us: 0, + p99_us: 0, + max_us: 0, + queue_us: 0, + draw_us: 0, + mid_flush_us: 0, + compositor_us: 0, + flush_us: 0, settle_us: 0, }; } @@ -434,9 +461,14 @@ fn run_zigzag_pan_pass( let settle_us = measure_settle(renderer); compute_pass_stats( - &frame_times, &queue_us_acc, &draw_us_acc, - &mid_flush_us_acc, &compositor_us_acc, &flush_us_acc, - wall, settle_us, + &frame_times, + &queue_us_acc, + &draw_us_acc, + &mid_flush_us_acc, + &compositor_us_acc, + &flush_us_acc, + wall, + settle_us, ) } @@ -487,9 +519,14 @@ fn run_circle_pan_pass( let settle_us = measure_settle(renderer); compute_pass_stats( - &frame_times, &queue_us_acc, &draw_us_acc, - &mid_flush_us_acc, &compositor_us_acc, &flush_us_acc, - wall, settle_us, + &frame_times, + &queue_us_acc, + &draw_us_acc, + &mid_flush_us_acc, + &compositor_us_acc, + &flush_us_acc, + wall, + settle_us, ) } @@ -592,9 +629,153 @@ fn run_realtime_pan_pass( }; compute_pass_stats( - &frame_times, &queue_us_acc, &draw_us_acc, - &mid_flush_us_acc, &compositor_us_acc, &flush_us_acc, - wall, avg_settle, + &frame_times, + &queue_us_acc, + &draw_us_acc, + &mid_flush_us_acc, + &compositor_us_acc, + &flush_us_acc, + wall, + avg_settle, + ) +} + +/// Real-time FrameLoop pan pass. +/// +/// Reproduces **exactly** what happens in `Application::frame()` during +/// panning — same FrameLoop, same apply_changes/build_plan/flush_with_plan +/// path, same GPU backend, with real `thread::sleep()` between ticks so +/// the GPU pipeline sees realistic idle gaps. +/// +/// This is the only benchmark that captures the actual user-facing +/// bottleneck: stable frames interrupting pan interactions. +/// +/// # How it works +/// +/// Runs a 60fps RAF loop (real 16ms sleeps). Scroll events inject camera +/// translations at `scroll_interval_ms` intervals. `FrameLoop` decides +/// whether each tick produces a frame and at what quality. The output +/// is the same `PassStats` as other passes, but the frame time +/// distribution reflects the real interaction — including jank spikes +/// from stable frames. +fn run_frameloop_pan_pass( + renderer: &mut cg::runtime::scene::Renderer, + scroll_interval_ms: f64, + dx: f32, + duration_ms: f64, +) -> PassStats { + let raf_interval_us: u64 = 16_000; // 60fps host cadence + let t_origin = Instant::now(); + + let mut frame_loop = FrameLoop::new(); + + let mut frame_times = Vec::new(); + let mut queue_us_acc = Vec::new(); + let mut draw_us_acc = Vec::new(); + let mut mid_flush_us_acc = Vec::new(); + let mut compositor_us_acc = Vec::new(); + let mut flush_us_acc = Vec::new(); + let mut stable_count = 0u32; + let mut unstable_count = 0u32; + + let mut next_scroll_ms = 0.0f64; + let mut scroll_events_fired = 0u32; + let mut pan_direction = 1.0f32; + + loop { + // Real wall time since start → this is what FrameLoop sees. + let now_ms = t_origin.elapsed().as_secs_f64() * 1000.0; + if now_ms >= duration_ms { + break; + } + + // --- Inject scroll event if due --- + if now_ms >= next_scroll_ms { + let zoom = renderer.camera.get_zoom(); + renderer.camera.translate(dx * pan_direction / zoom, 0.0); + frame_loop.invalidate(now_ms); + next_scroll_ms += scroll_interval_ms; + scroll_events_fired += 1; + + if scroll_events_fired % 25 == 0 { + pan_direction = -pan_direction; + } + } + + // --- Application::frame() equivalent --- + // Steps 4-8 from Application::frame(), using real wall time. + if let Some(quality) = frame_loop.poll(now_ms) { + // Step 5: camera change + stable promotion + let camera_change = renderer.camera.change_kind(); + let stable = quality == FrameQuality::Stable || !camera_change.any_changed(); + + // Step: apply_changes (central invalidation dispatch) + renderer.apply_changes(camera_change, stable); + + // Step: warm camera cache + renderer.camera.warm_cache(); + + // Step: build frame plan + let rect = renderer.camera.rect(); + let zoom = renderer.camera.get_zoom(); + let plan = renderer.build_frame_plan(rect, zoom, stable, camera_change); + + // Step: consume camera change + renderer.camera.consume_change(); + + // Step: flush (draw + GPU submit) — MEASURED + let t0 = Instant::now(); + let stats_opt = renderer.flush_with_plan(plan); + let wall_time = t0.elapsed().as_micros() as u64; + + // Step: complete frame + frame_loop.complete(quality); + + if quality == FrameQuality::Stable { + stable_count += 1; + } else { + unstable_count += 1; + } + + if let Some(stats) = stats_opt { + frame_times.push(wall_time); + queue_us_acc.push(0); + draw_us_acc.push(stats.draw.painter_duration.as_micros() as u64); + mid_flush_us_acc.push(stats.mid_flush_duration.as_micros() as u64); + compositor_us_acc.push(stats.compositor_duration.as_micros() as u64); + flush_us_acc.push(stats.flush_duration.as_micros() as u64); + } + } + + // --- Real sleep to next RAF tick --- + let elapsed_us = t_origin.elapsed().as_micros() as u64; + let next_tick_us = (elapsed_us / raf_interval_us + 1) * raf_interval_us; + let sleep_us = next_tick_us.saturating_sub(t_origin.elapsed().as_micros() as u64); + if sleep_us > 500 { + std::thread::sleep(std::time::Duration::from_micros(sleep_us)); + } + } + + let wall = t_origin.elapsed(); + + eprintln!( + " [frameloop] scroll every {scroll_interval_ms:.0}ms | \ + {scroll_events_fired} events | \ + {} frames ({unstable_count} unstable, {stable_count} stable) | \ + wall: {:.0}ms", + frame_times.len(), + wall.as_millis(), + ); + + compute_pass_stats( + &frame_times, + &queue_us_acc, + &draw_us_acc, + &mid_flush_us_acc, + &compositor_us_acc, + &flush_us_acc, + wall, + 0, // settle is implicit — stable frames are in frame_times ) } @@ -612,8 +793,10 @@ fn run_realtime_diagnostic( eprintln!( "\n=== REALTIME DIAGNOSTIC: scroll every {scroll_interval_ms:.0}ms, settle after {settle_ticks} ticks ===", ); - eprintln!("{:>8} {:>6} {:>8} {:>8} {:>8}", - "time_ms", "event", "total_us", "draw_us", "note"); + eprintln!( + "{:>8} {:>6} {:>8} {:>8} {:>8}", + "time_ms", "event", "total_us", "draw_us", "note" + ); let mut clock: f64 = 0.0; let mut next_scroll = scroll_interval_ms; @@ -634,7 +817,10 @@ fn run_realtime_diagnostic( if settle_countdown == 0 { if let Some((total, _q, d, _mf, _c, _f)) = measure_frame(renderer, true) { let marker = if total > 1000 { " <<<" } else { "" }; - eprintln!("{:>8.1} {:>6} {:>8} {:>8} settle{marker}", clock, "SETTLE", total, d); + eprintln!( + "{:>8.1} {:>6} {:>8} {:>8} settle{marker}", + clock, "SETTLE", total, d + ); } } } @@ -647,7 +833,10 @@ fn run_realtime_diagnostic( if let Some((total, _q, d, _mf, _c, _f)) = measure_frame(renderer, false) { let note = if d > 0 { "full draw" } else { "cache hit" }; let marker = if total > 1000 { " <<<" } else { "" }; - eprintln!("{:>8.1} {:>6} {:>8} {:>8} {note}{marker}", clock, "scroll", total, d); + eprintln!( + "{:>8.1} {:>6} {:>8} {:>8} {note}{marker}", + clock, "scroll", total, d + ); } next_scroll += scroll_interval_ms; } @@ -662,7 +851,8 @@ fn run_pan_with_settle_pass( settle_interval: u32, ) -> PassStats { let wall_start = Instant::now(); - let mut frame_times = Vec::with_capacity(frames as usize + frames as usize / settle_interval as usize); + let mut frame_times = + Vec::with_capacity(frames as usize + frames as usize / settle_interval as usize); let mut queue_us_acc = Vec::new(); let mut draw_us_acc = Vec::new(); let mut mid_flush_us_acc = Vec::new(); @@ -713,9 +903,14 @@ fn run_pan_with_settle_pass( }; compute_pass_stats( - &frame_times, &queue_us_acc, &draw_us_acc, - &mid_flush_us_acc, &compositor_us_acc, &flush_us_acc, - wall, avg_settle, + &frame_times, + &queue_us_acc, + &draw_us_acc, + &mid_flush_us_acc, + &compositor_us_acc, + &flush_us_acc, + wall, + avg_settle, ) } @@ -730,8 +925,10 @@ fn run_pan_settle_diagnostic( settle_interval: u32, ) { eprintln!("\n=== PAN+SETTLE DIAGNOSTIC: dx={dx}, settle every {settle_interval} frames ==="); - eprintln!("{:>5} {:>4} {:>8} {:>8} {:>8} {:>8}", - "frame", "type", "total_us", "queue_us", "draw_us", "list"); + eprintln!( + "{:>5} {:>4} {:>8} {:>8} {:>8} {:>8}", + "frame", "type", "total_us", "queue_us", "draw_us", "list" + ); let mut since_settle = 0u32; for i in 0..frames { @@ -740,8 +937,10 @@ fn run_pan_settle_diagnostic( if let Some((total, q, d, _mf, _c, _f)) = measure_frame(renderer, false) { let list = 0; // Not available from measure_frame let marker = if total > 1000 { " <<<" } else { "" }; - eprintln!("{:>5} {:>4} {:>8} {:>8} {:>8} {:>8}{marker}", - i, "pan", total, q, d, list); + eprintln!( + "{:>5} {:>4} {:>8} {:>8} {:>8} {:>8}{marker}", + i, "pan", total, q, d, list + ); } since_settle += 1; @@ -749,8 +948,10 @@ fn run_pan_settle_diagnostic( since_settle = 0; if let Some((total, q, d, _mf, _c, _f)) = measure_frame(renderer, true) { let marker = if total > 1000 { " <<<" } else { "" }; - eprintln!("{:>5} {:>4} {:>8} {:>8} {:>8} {:>8}{marker}", - i, "STTL", total, q, d, 0); + eprintln!( + "{:>5} {:>4} {:>8} {:>8} {:>8} {:>8}{marker}", + i, "STTL", total, q, d, 0 + ); } } } @@ -761,14 +962,12 @@ fn run_pan_settle_diagnostic( /// Shows exactly where frame drops occur during the transition from /// "all nodes visible" to "some nodes culled". #[allow(dead_code)] -fn run_pan_diagnostic( - renderer: &mut cg::runtime::scene::Renderer, - frames: u32, - dx: f32, -) { +fn run_pan_diagnostic(renderer: &mut cg::runtime::scene::Renderer, frames: u32, dx: f32) { eprintln!("\n=== PAN DIAGNOSTIC: dx={dx}, {} frames ===", frames); - eprintln!("{:>5} {:>8} {:>8} {:>8} {:>8} {:>8} {:>8}", - "frame", "total_us", "queue_us", "draw_us", "mflush", "comp_us", "flush_us"); + eprintln!( + "{:>5} {:>8} {:>8} {:>8} {:>8} {:>8} {:>8}", + "frame", "total_us", "queue_us", "draw_us", "mflush", "comp_us", "flush_us" + ); for i in 0..frames { renderer.camera.translate(dx, 0.0); @@ -832,17 +1031,53 @@ fn standard_scenarios(fit_zoom: f32) -> (Vec, Vec) { let fit_hi = fit_zoom * 2.0; let pan_scenarios = vec![ - PanScenario { name: "pan_slow_fit", dx: 2.0, zoom: fit_zoom }, - PanScenario { name: "pan_fast_fit", dx: 50.0, zoom: fit_zoom }, - PanScenario { name: "pan_slow_zoomed", dx: 2.0, zoom: zoomed_in }, - PanScenario { name: "pan_fast_zoomed", dx: 50.0, zoom: zoomed_in }, + PanScenario { + name: "pan_slow_fit", + dx: 2.0, + zoom: fit_zoom, + }, + PanScenario { + name: "pan_fast_fit", + dx: 50.0, + zoom: fit_zoom, + }, + PanScenario { + name: "pan_slow_zoomed", + dx: 2.0, + zoom: zoomed_in, + }, + PanScenario { + name: "pan_fast_zoomed", + dx: 50.0, + zoom: zoomed_in, + }, ]; let zoom_scenarios = vec![ - ZoomScenario { name: "zoom_slow_around_fit", step: 0.005, z_min: fit_lo, z_max: fit_hi }, - ZoomScenario { name: "zoom_fast_around_fit", step: 0.05, z_min: fit_lo, z_max: fit_hi }, - ZoomScenario { name: "zoom_slow_high", step: 0.01, z_min: zoomed_in * 0.5, z_max: zoomed_in }, - ZoomScenario { name: "zoom_fast_high", step: 0.1, z_min: zoomed_in * 0.5, z_max: zoomed_in }, + ZoomScenario { + name: "zoom_slow_around_fit", + step: 0.005, + z_min: fit_lo, + z_max: fit_hi, + }, + ZoomScenario { + name: "zoom_fast_around_fit", + step: 0.05, + z_min: fit_lo, + z_max: fit_hi, + }, + ZoomScenario { + name: "zoom_slow_high", + step: 0.01, + z_min: zoomed_in * 0.5, + z_max: zoomed_in, + }, + ZoomScenario { + name: "zoom_fast_high", + step: 0.1, + z_min: zoomed_in * 0.5, + z_max: zoomed_in, + }, ]; (pan_scenarios, zoom_scenarios) @@ -892,10 +1127,26 @@ fn run_scenarios( let zoomed_in_c = (fit_zoom * 4.0).min(10.0); let circle_scenarios = vec![ - CirclePanScenario { name: "circle_small_fit", radius: 200.0, zoom: fit_zoom }, - CirclePanScenario { name: "circle_large_fit", radius: 2000.0, zoom: fit_zoom }, - CirclePanScenario { name: "circle_small_zoomed", radius: 200.0, zoom: zoomed_in_c }, - CirclePanScenario { name: "circle_large_zoomed", radius: 2000.0, zoom: zoomed_in_c }, + CirclePanScenario { + name: "circle_small_fit", + radius: 200.0, + zoom: fit_zoom, + }, + CirclePanScenario { + name: "circle_large_fit", + radius: 2000.0, + zoom: fit_zoom, + }, + CirclePanScenario { + name: "circle_small_zoomed", + radius: 200.0, + zoom: zoomed_in_c, + }, + CirclePanScenario { + name: "circle_large_zoomed", + radius: 2000.0, + zoom: zoomed_in_c, + }, ]; for cs in &circle_scenarios { @@ -939,22 +1190,38 @@ fn run_scenarios( let zigzag_scenarios = vec![ // Fast zigzag: continuous diagonal sweeps, no pauses ZigzagScenario { - name: "zigzag_fast_fit", dx: 30.0, dy: 5.0, - segment_frames: 20, pause_frames: 0, zoom: fit_zoom, + name: "zigzag_fast_fit", + dx: 30.0, + dy: 5.0, + segment_frames: 20, + pause_frames: 0, + zoom: fit_zoom, }, ZigzagScenario { - name: "zigzag_fast_zoomed", dx: 30.0, dy: 5.0, - segment_frames: 20, pause_frames: 0, zoom: zoomed_in_z, + name: "zigzag_fast_zoomed", + dx: 30.0, + dy: 5.0, + segment_frames: 20, + pause_frames: 0, + zoom: zoomed_in_z, }, // Slow zigzag: zig, stop (settle fires), zag, stop (settle fires) // pause_frames=3 simulates ~3 settle frames during the "reading" pause ZigzagScenario { - name: "zigzag_slow_fit", dx: 10.0, dy: 3.0, - segment_frames: 15, pause_frames: 3, zoom: fit_zoom, + name: "zigzag_slow_fit", + dx: 10.0, + dy: 3.0, + segment_frames: 15, + pause_frames: 3, + zoom: fit_zoom, }, ZigzagScenario { - name: "zigzag_slow_zoomed", dx: 10.0, dy: 3.0, - segment_frames: 15, pause_frames: 3, zoom: zoomed_in_z, + name: "zigzag_slow_zoomed", + dx: 10.0, + dy: 3.0, + segment_frames: 15, + pause_frames: 3, + zoom: zoomed_in_z, }, ]; @@ -969,7 +1236,12 @@ fn run_scenarios( } let stats = run_zigzag_pan_pass( - renderer, frames, zz.dx, zz.dy, zz.segment_frames, zz.pause_frames, + renderer, + frames, + zz.dx, + zz.dy, + zz.segment_frames, + zz.pause_frames, ); results.push(ScenarioResult { name: zz.name.to_string(), @@ -1010,10 +1282,30 @@ fn run_scenarios( let zoomed_in_s = (fit_zoom * 4.0).min(10.0); let settle_scenarios = vec![ - SettlePanScenario { name: "pan_settle_slow_fit", dx: 2.0, zoom: fit_zoom, settle_interval: 12 }, - SettlePanScenario { name: "pan_settle_fast_fit", dx: 50.0, zoom: fit_zoom, settle_interval: 12 }, - SettlePanScenario { name: "pan_settle_slow_zoomed", dx: 2.0, zoom: zoomed_in_s, settle_interval: 12 }, - SettlePanScenario { name: "pan_settle_fast_zoomed", dx: 50.0, zoom: zoomed_in_s, settle_interval: 12 }, + SettlePanScenario { + name: "pan_settle_slow_fit", + dx: 2.0, + zoom: fit_zoom, + settle_interval: 12, + }, + SettlePanScenario { + name: "pan_settle_fast_fit", + dx: 50.0, + zoom: fit_zoom, + settle_interval: 12, + }, + SettlePanScenario { + name: "pan_settle_slow_zoomed", + dx: 2.0, + zoom: zoomed_in_s, + settle_interval: 12, + }, + SettlePanScenario { + name: "pan_settle_fast_zoomed", + dx: 50.0, + zoom: zoomed_in_s, + settle_interval: 12, + }, ]; for ss in &settle_scenarios { @@ -1055,20 +1347,36 @@ fn run_scenarios( let zoomed_in_rt = (fit_zoom * 4.0).min(10.0); let realtime_scenarios = vec![ RealtimeScenario { - name: "rt_pan_fast_fit", scroll_interval_ms: 8.0, - dx: 2.0, dy: 0.0, zoom: fit_zoom, duration_ms: 2000.0, + name: "rt_pan_fast_fit", + scroll_interval_ms: 8.0, + dx: 2.0, + dy: 0.0, + zoom: fit_zoom, + duration_ms: 2000.0, }, RealtimeScenario { - name: "rt_pan_slow_fit", scroll_interval_ms: 100.0, - dx: 5.0, dy: 0.0, zoom: fit_zoom, duration_ms: 2000.0, + name: "rt_pan_slow_fit", + scroll_interval_ms: 100.0, + dx: 5.0, + dy: 0.0, + zoom: fit_zoom, + duration_ms: 2000.0, }, RealtimeScenario { - name: "rt_pan_fast_zoomed", scroll_interval_ms: 8.0, - dx: 2.0, dy: 0.0, zoom: zoomed_in_rt, duration_ms: 2000.0, + name: "rt_pan_fast_zoomed", + scroll_interval_ms: 8.0, + dx: 2.0, + dy: 0.0, + zoom: zoomed_in_rt, + duration_ms: 2000.0, }, RealtimeScenario { - name: "rt_pan_slow_zoomed", scroll_interval_ms: 100.0, - dx: 5.0, dy: 0.0, zoom: zoomed_in_rt, duration_ms: 2000.0, + name: "rt_pan_slow_zoomed", + scroll_interval_ms: 100.0, + dx: 5.0, + dy: 0.0, + zoom: zoomed_in_rt, + duration_ms: 2000.0, }, ]; @@ -1079,8 +1387,12 @@ fn run_scenarios( warmup(renderer); let stats = run_realtime_pan_pass( - renderer, rt.scroll_interval_ms, - rt.dx, rt.dy, rt.duration_ms, 12, + renderer, + rt.scroll_interval_ms, + rt.dx, + rt.dy, + rt.duration_ms, + 12, ); results.push(ScenarioResult { name: rt.name.to_string(), @@ -1095,6 +1407,99 @@ fn run_scenarios( }); } + // FrameLoop-based pan scenarios: the real FrameLoop decision path. + // Unlike all other scenarios, these go through FrameLoop.poll() which + // decides Stable vs Unstable based on adaptive delay. This captures: + // - Pan image cache hit rate (GPU-only: unstable pan = cache blit) + // - Stable frame intrusion frequency (adaptive delay prevents these) + // - Compositor budget impact on stable frame cost + struct FrameLoopScenario { + name: &'static str, + scroll_interval_ms: f64, + dx: f32, + zoom: f32, + } + + // Sweep across a range of scroll intervals to find the jank threshold. + // Real trackpad scroll events range from ~8ms (fast flick) to ~200ms+ + // (very slow, deliberate single-finger scroll). + let frameloop_scenarios = vec![ + // Continuous fast pan — baseline, no stable frames should fire + FrameLoopScenario { + name: "fl_16ms", + scroll_interval_ms: 16.0, + dx: 5.0, + zoom: fit_zoom, + }, + // Moderate pan — gaps start approaching old 50ms debounce + FrameLoopScenario { + name: "fl_50ms", + scroll_interval_ms: 50.0, + dx: 3.0, + zoom: fit_zoom, + }, + // Slow pan — exceeds old 50ms debounce, adaptive should extend + FrameLoopScenario { + name: "fl_80ms", + scroll_interval_ms: 80.0, + dx: 3.0, + zoom: fit_zoom, + }, + // Slower — common slow trackpad scroll speed + FrameLoopScenario { + name: "fl_120ms", + scroll_interval_ms: 120.0, + dx: 2.0, + zoom: fit_zoom, + }, + // Very slow — deliberate, careful scrolling + FrameLoopScenario { + name: "fl_200ms", + scroll_interval_ms: 200.0, + dx: 1.0, + zoom: fit_zoom, + }, + // Ultra slow — near the edge of "interaction session" detection + FrameLoopScenario { + name: "fl_300ms", + scroll_interval_ms: 300.0, + dx: 1.0, + zoom: fit_zoom, + }, + // Discrete clicks — clearly separate events, stable should fire between + FrameLoopScenario { + name: "fl_500ms", + scroll_interval_ms: 500.0, + dx: 1.0, + zoom: fit_zoom, + }, + ]; + + for fl_s in &frameloop_scenarios { + renderer.camera.set_zoom(fl_s.zoom); + renderer.queue_stable(); + let _ = renderer.flush(); + warmup(renderer); + + let stats = run_frameloop_pan_pass( + renderer, + fl_s.scroll_interval_ms, + fl_s.dx, + 2000.0, // 2 second session + ); + results.push(ScenarioResult { + name: fl_s.name.to_string(), + kind: "frameloop".to_string(), + params: ScenarioParams { + speed: Some(fl_s.dx), + zoom: Some(fl_s.zoom), + zoom_min: None, + zoom_max: None, + }, + stats, + }); + } + results } @@ -1165,10 +1570,7 @@ fn build_benchmark_scene(grid: u32) -> Scene { // Single-scene bench (human-readable output) // --------------------------------------------------------------------------- -pub async fn run_bench( - args: BenchArgs, - load_scenes: impl AsyncSceneLoader, -) -> Result<()> { +pub async fn run_bench(args: BenchArgs, load_scenes: impl AsyncSceneLoader) -> Result<()> { let scenes = if let Some(ref path) = args.path { load_scenes.load(path).await? } else { @@ -1194,8 +1596,8 @@ pub async fn run_bench( let scene = scenes.into_iter().nth(args.scene_index).unwrap(); let node_count = scene.graph.node_count(); - let mut gpu = HeadlessGpu::new(args.width, args.height) - .map_err(|e| anyhow!("GPU init failed: {e}"))?; + let mut gpu = + HeadlessGpu::new(args.width, args.height).map_err(|e| anyhow!("GPU init failed: {e}"))?; gpu.print_gl_info(); let mut renderer = gpu.create_renderer(); @@ -1207,9 +1609,7 @@ pub async fn run_bench( println!("Loaded scene: {} nodes", node_count); println!( "Camera: zoom={:.4} viewport=({:.0}x{:.0})", - fit_zoom, - cam_rect.width, - cam_rect.height, + fit_zoom, cam_rect.width, cam_rect.height, ); println!( "Viewport: {}x{}, frames: {}\n", @@ -1321,7 +1721,10 @@ pub async fn run_bench_report( eprintln!( "bench-report: {} files, {} frames/pass, {}x{} viewport", - files.len(), args.frames, args.width, args.height + files.len(), + args.frames, + args.width, + args.height ); let mut results = Vec::new(); From 34532193fccb7a4b0e1e4dd704f9546b58e377bd Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 24 Mar 2026 15:38:17 +0900 Subject: [PATCH 02/31] docs: update SKILL.md to include `frameloop` benchmark details Added information about the new `frameloop` scenario type, which captures stable-frame jank during panning. Updated descriptions to clarify how `frameloop` scenarios differ from other benchmarks and their importance in analyzing panning smoothness and adaptive timing. Enhanced documentation on the implications of using `frameloop` for accurate performance measurements. --- .agents/skills/cg-perf/SKILL.md | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/.agents/skills/cg-perf/SKILL.md b/.agents/skills/cg-perf/SKILL.md index fafa832ada..d1c7719897 100644 --- a/.agents/skills/cg-perf/SKILL.md +++ b/.agents/skills/cg-perf/SKILL.md @@ -131,6 +131,7 @@ reports `min/p50/p95/p99/MAX` plus per-stage breakdown and settle cost. | `zoom` | slow/fast × around-fit/high | Zoom oscillation at different levels | | `pan_with_settle` | slow/fast × fit/zoomed | Pan with settle frames interleaved every 12 frames | | `realtime` | fast/slow × fit/zoomed | **Real-time event loop simulation** with sleep, 240Hz tick thread, and settle countdown matching the native viewer | +| `frameloop` | 16/50/80/120/200/300/500ms interval | **Real FrameLoop path** — the only bench that captures stable-frame jank during panning (see below) | | `resize` | alternating viewport sizes | `--resize` flag. Measures `resize()` + `redraw()` cost per cycle (layout rebuild + cache invalidation + repaint) | The `realtime` scenarios use actual `thread::sleep()` between frames @@ -138,6 +139,16 @@ and simulate the native viewer's 240Hz tick thread + settle countdown. These produce frame timings that match what users actually see, including settle-induced frame drops at their natural frequency. +The `frameloop` scenarios go through the actual `FrameLoop.poll()` / +`complete()` path — the same code path as `Application::frame()`. All +other pan/zoom scenarios bypass `FrameLoop` and call `queue_unstable()` +directly, which means they never produce stable frames mid-interaction. +The `frameloop` scenarios sweep scroll intervals from 16ms (fast flick) +to 500ms (discrete clicks) and reveal how `FrameLoop`'s stable-frame +decisions affect the frame time distribution at each speed. Use these +when investigating panning jank, adaptive timing, or pan/zoom image +cache behavior. + **Choosing scenes:** Use `--list-scenes` to see what's available. Pick scenes that stress the subsystem you're optimizing. For effects/caching work, look for scenes with high promoted-node counts. For culling work, @@ -174,6 +185,7 @@ of scenes, configs, and operations. The naming convention is | Does a config toggle actually help? | Both GPU benchmarks + Criterion | | Does it match what users see in the app? | `realtime` scenarios (sleep + settle simulation) | | Are there frame drops during gestures? | Check `p99` and `MAX` in scenario stats | +| Is slow panning janky (stable frame spikes)? | `frameloop` scenarios (real FrameLoop path) | | Is resize janky? | Single-scene GPU bench with `--resize` | --- @@ -447,9 +459,19 @@ Back-to-back frame benchmarks (no sleep between frames) can produce misleadingly fast numbers because they never trigger settle frames. The native viewer's 240Hz tick thread fires `queue_stable()` ~50ms after the last interaction, clearing image caches. Use the `realtime` -scenario type to simulate this timing and produce numbers that match -what users actually see. Always check `p99` and `MAX` — not just -`p50` — to catch settle-induced spikes. +or `frameloop` scenario types to produce numbers that match what users +actually see. Always check `p99` and `MAX` — not just `p50` — to +catch settle-induced spikes. + +### Most benchmarks bypass FrameLoop + +All pan/zoom/circle/zigzag scenarios call `queue_unstable()` directly +— they never go through `FrameLoop.poll()`. This means they never +produce stable frames mid-interaction and cannot capture the jank +pattern where a stable frame interrupts slow panning. Only the +`frameloop` scenarios use the real `FrameLoop` decision path. When +investigating panning smoothness or adaptive timing, always use the +`frameloop` scenarios. ### Stable frames must recapture caches From cc847e1a9952128ef44f254916450eeeabbab109 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 24 Mar 2026 15:44:21 +0900 Subject: [PATCH 03/31] perf(canvas): use FxHash for NodeId-keyed caches Replace SipHash (std HashMap) with a FxHash-style multiplicative hasher for all internal rendering caches keyed by NodeId (u64). These caches have trusted-input keys only, so DoS-resistant hashing is unnecessary. The fast hasher reduces per-lookup cost from ~25ns to ~3ns, yielding 5-15% improvement on pan/zoom operations (Criterion-verified). Affected caches: geometry, picture, vector_path, atlas, atlas_set, compositor, painter draw_order, and scene node_map. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/grida-canvas/src/cache/atlas/atlas.rs | 10 +-- .../grida-canvas/src/cache/atlas/atlas_set.rs | 6 +- .../src/cache/compositor/cache.rs | 6 +- crates/grida-canvas/src/cache/fast_hash.rs | 76 +++++++++++++++++++ crates/grida-canvas/src/cache/geometry.rs | 6 +- crates/grida-canvas/src/cache/mod.rs | 1 + crates/grida-canvas/src/cache/picture.rs | 10 +-- crates/grida-canvas/src/cache/vector_path.rs | 6 +- crates/grida-canvas/src/painter/painter.rs | 6 +- crates/grida-canvas/src/runtime/scene.rs | 6 +- .../grida-canvas/tests/compositor_effects.rs | 2 +- 11 files changed, 106 insertions(+), 29 deletions(-) create mode 100644 crates/grida-canvas/src/cache/fast_hash.rs diff --git a/crates/grida-canvas/src/cache/atlas/atlas.rs b/crates/grida-canvas/src/cache/atlas/atlas.rs index cbc5968cbd..21b2826978 100644 --- a/crates/grida-canvas/src/cache/atlas/atlas.rs +++ b/crates/grida-canvas/src/cache/atlas/atlas.rs @@ -12,7 +12,7 @@ use super::packing::{ShelfPacker, Slot, SlotId}; use crate::node::schema::NodeId; use skia_safe::{Canvas, Image, Rect, Surface}; -use std::collections::HashMap; +use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; /// A single atlas page. /// @@ -26,9 +26,9 @@ pub struct AtlasPage { /// Shelf packer managing slot allocation. packer: ShelfPacker, /// Map from slot ID to the node that occupies it. - slot_to_node: HashMap, + slot_to_node: NodeIdHashMap, /// Map from node ID to its allocated slot. - node_to_slot: HashMap, + node_to_slot: NodeIdHashMap, /// Whether the surface has been modified since the last snapshot. dirty: bool, /// Page index (for multi-page atlas sets). @@ -70,8 +70,8 @@ impl AtlasPage { surface, image: None, packer: ShelfPacker::new(w, h), - slot_to_node: HashMap::new(), - node_to_slot: HashMap::new(), + slot_to_node: new_node_id_map(), + node_to_slot: new_node_id_map(), dirty: false, page_index, } diff --git a/crates/grida-canvas/src/cache/atlas/atlas_set.rs b/crates/grida-canvas/src/cache/atlas/atlas_set.rs index 84ae1342e1..46b03579f0 100644 --- a/crates/grida-canvas/src/cache/atlas/atlas_set.rs +++ b/crates/grida-canvas/src/cache/atlas/atlas_set.rs @@ -7,7 +7,7 @@ use super::atlas::{AtlasAllocation, AtlasPage}; use crate::node::schema::NodeId; use skia_safe::{Image, Surface}; -use std::collections::HashMap; +use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; /// Configuration for an atlas set. #[derive(Debug, Clone, Copy)] @@ -54,7 +54,7 @@ pub struct AtlasSet { config: AtlasSetConfig, pages: Vec, /// Map from node ID to the page index it's allocated on. - node_page: HashMap, + node_page: NodeIdHashMap, } impl AtlasSet { @@ -65,7 +65,7 @@ impl AtlasSet { Self { config, pages: Vec::new(), - node_page: HashMap::new(), + node_page: new_node_id_map(), } } diff --git a/crates/grida-canvas/src/cache/compositor/cache.rs b/crates/grida-canvas/src/cache/compositor/cache.rs index a45578e20c..2cd83a2878 100644 --- a/crates/grida-canvas/src/cache/compositor/cache.rs +++ b/crates/grida-canvas/src/cache/compositor/cache.rs @@ -8,7 +8,7 @@ use crate::cg::prelude::LayerBlendMode; use crate::node::schema::NodeId; use math2::rect::Rectangle; use skia_safe::Image; -use std::collections::HashMap; +use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; use std::rc::Rc; /// Where a promoted node's cached pixels live. @@ -108,7 +108,7 @@ pub struct LayerImageCacheStats { #[derive(Debug, Clone)] pub struct LayerImageCache { /// Promoted node entries, keyed by node ID. - images: HashMap, + images: NodeIdHashMap, /// Maximum memory budget in bytes (default: 128 MB). /// Only individual (non-atlas) images count against this budget. memory_budget: usize, @@ -131,7 +131,7 @@ impl LayerImageCache { /// Create a new layer image cache with the given memory budget. pub fn new(memory_budget: usize) -> Self { Self { - images: HashMap::new(), + images: new_node_id_map(), memory_budget, memory_used: 0, frame_counter: 0, diff --git a/crates/grida-canvas/src/cache/fast_hash.rs b/crates/grida-canvas/src/cache/fast_hash.rs new file mode 100644 index 0000000000..4a61aa79cf --- /dev/null +++ b/crates/grida-canvas/src/cache/fast_hash.rs @@ -0,0 +1,76 @@ +//! Fast hasher for u64-keyed HashMaps in the rendering hot path. +//! +//! The default `HashMap` uses SipHash-1-3, which provides DoS resistance +//! at ~25ns per hash. For trusted-input rendering caches keyed by `NodeId` +//! (u64), we can use a much faster multiplicative hash (~3ns) since there +//! is no untrusted input to defend against. +//! +//! This is the same approach as `rustc-hash` (FxHash): multiply by a large +//! odd constant to scatter bits, then use the result directly as the hash. + +use std::collections::HashMap; +use std::hash::{BuildHasher, Hasher}; + +/// A fast hasher for integer keys. +/// +/// Uses a single multiply to distribute bits. Suitable for u64 keys +/// (NodeId) and (u64, u64) tuple keys used in the picture/geometry/ +/// compositor caches. +#[derive(Default)] +pub struct NodeIdHasher { + hash: u64, +} + +impl Hasher for NodeIdHasher { + #[inline] + fn write(&mut self, bytes: &[u8]) { + // For arbitrary byte sequences, use a simple FNV-like combine. + for &b in bytes { + self.hash = self.hash.wrapping_mul(0x100000001b3).wrapping_add(b as u64); + } + } + + #[inline] + fn write_u64(&mut self, i: u64) { + // FxHash: XOR-fold then multiply by a large odd constant. + // This is the primary fast path for NodeId (u64) keys. + self.hash = self.hash ^ i; + self.hash = self.hash.wrapping_mul(0x517cc1b727220a95); + } + + #[inline] + fn finish(&self) -> u64 { + self.hash + } +} + +/// BuildHasher that produces `NodeIdHasher` instances. +#[derive(Clone, Default)] +pub struct NodeIdBuildHasher; + +impl BuildHasher for NodeIdBuildHasher { + type Hasher = NodeIdHasher; + + #[inline] + fn build_hasher(&self) -> NodeIdHasher { + NodeIdHasher::default() + } +} + +/// A HashMap using the fast NodeId hasher. +/// +/// Use this for caches keyed by `NodeId` (u64) or `(NodeId, u64)` tuples +/// where keys come from trusted internal sources (no DoS risk). +pub type NodeIdHashMap = HashMap; + +/// Create a new empty NodeIdHashMap. +#[inline] +pub fn new_node_id_map() -> NodeIdHashMap { + HashMap::with_hasher(NodeIdBuildHasher) +} + +/// Create a new NodeIdHashMap with the specified capacity. +#[inline] +pub fn new_node_id_map_with_capacity(capacity: usize) -> NodeIdHashMap { + HashMap::with_capacity_and_hasher(capacity, NodeIdBuildHasher) +} diff --git a/crates/grida-canvas/src/cache/geometry.rs b/crates/grida-canvas/src/cache/geometry.rs index 30d1453aa8..1fc3b6962e 100644 --- a/crates/grida-canvas/src/cache/geometry.rs +++ b/crates/grida-canvas/src/cache/geometry.rs @@ -18,7 +18,7 @@ use crate::runtime::font_repository::FontRepository; use math2::rect; use math2::rect::Rectangle; use math2::transform::AffineTransform; -use std::collections::HashMap; +use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; /// Geometry data used for layout, culling, and rendering. /// @@ -52,13 +52,13 @@ struct GeometryBuildContext { #[derive(Debug, Clone)] pub struct GeometryCache { - entries: HashMap, + entries: NodeIdHashMap, } impl GeometryCache { pub fn new() -> Self { Self { - entries: HashMap::new(), + entries: new_node_id_map(), } } diff --git a/crates/grida-canvas/src/cache/mod.rs b/crates/grida-canvas/src/cache/mod.rs index c152874557..5b9a4257b8 100644 --- a/crates/grida-canvas/src/cache/mod.rs +++ b/crates/grida-canvas/src/cache/mod.rs @@ -1,5 +1,6 @@ pub mod atlas; pub mod compositor; +pub mod fast_hash; pub mod geometry; pub mod mipmap; pub mod paragraph; diff --git a/crates/grida-canvas/src/cache/picture.rs b/crates/grida-canvas/src/cache/picture.rs index 6384202690..23e1941084 100644 --- a/crates/grida-canvas/src/cache/picture.rs +++ b/crates/grida-canvas/src/cache/picture.rs @@ -1,6 +1,6 @@ +use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; use crate::node::schema::NodeId; use skia_safe::Picture; -use std::collections::HashMap; /// Configuration for how the scene should be cached. /// @@ -22,17 +22,17 @@ impl Default for PictureCacheStrategy { pub struct PictureCache { strategy: PictureCacheStrategy, /// Fast-path store for the default render variant (variant key = 0). - default_store: HashMap, + default_store: NodeIdHashMap, /// Store for non-default render variants (variant key != 0). - variant_store: HashMap<(NodeId, u64), Picture>, + variant_store: NodeIdHashMap<(NodeId, u64), Picture>, } impl PictureCache { pub fn new() -> Self { Self { strategy: PictureCacheStrategy::default(), - default_store: HashMap::new(), - variant_store: HashMap::new(), + default_store: new_node_id_map(), + variant_store: new_node_id_map(), } } diff --git a/crates/grida-canvas/src/cache/vector_path.rs b/crates/grida-canvas/src/cache/vector_path.rs index 1f09c49966..ff32b989c7 100644 --- a/crates/grida-canvas/src/cache/vector_path.rs +++ b/crates/grida-canvas/src/cache/vector_path.rs @@ -1,7 +1,7 @@ +use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; use crate::node::schema::NodeId; use skia_safe::Path; use std::collections::hash_map::DefaultHasher; -use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::rc::Rc; @@ -13,13 +13,13 @@ pub struct VectorPathCacheEntry { #[derive(Default, Clone, Debug)] pub struct VectorPathCache { - entries: HashMap, + entries: NodeIdHashMap, } impl VectorPathCache { pub fn new() -> Self { Self { - entries: HashMap::new(), + entries: new_node_id_map(), } } diff --git a/crates/grida-canvas/src/painter/painter.rs b/crates/grida-canvas/src/painter/painter.rs index a7c0e5a056..fc8134a28b 100644 --- a/crates/grida-canvas/src/painter/painter.rs +++ b/crates/grida-canvas/src/painter/painter.rs @@ -21,8 +21,8 @@ use skia_safe::{ canvas::SaveLayerRec, textlayout, Matrix, Paint as SkPaint, Path, PathBuilder, Point, Rect, Shader, }; +use crate::cache::fast_hash::NodeIdHashMap; use std::cell::{Cell, RefCell}; -use std::collections::HashMap; use std::rc::Rc; /// Pre-extracted blit data for a single promoted (compositor-cached) node. @@ -58,7 +58,7 @@ pub struct Painter<'a> { /// Pre-extracted blit data for promoted (compositor-cached) nodes. /// When present, promoted nodes are blitted inline at their correct /// z-position instead of being skipped. - promoted_blits: Option<&'a HashMap>, + promoted_blits: Option<&'a NodeIdHashMap>, } impl<'a> Painter<'a> { @@ -113,7 +113,7 @@ impl<'a> Painter<'a> { /// Set the promoted blit map. Nodes in this map will be blitted from /// their pre-extracted compositor cache data at the correct z-position /// in the render command tree, instead of being re-drawn live. - pub fn with_promoted_blits(mut self, blits: &'a HashMap) -> Self { + pub fn with_promoted_blits(mut self, blits: &'a NodeIdHashMap) -> Self { self.promoted_blits = Some(blits); self } diff --git a/crates/grida-canvas/src/runtime/scene.rs b/crates/grida-canvas/src/runtime/scene.rs index 85ca25ad4f..23288ac5af 100644 --- a/crates/grida-canvas/src/runtime/scene.rs +++ b/crates/grida-canvas/src/runtime/scene.rs @@ -402,10 +402,10 @@ impl Renderer { &mut self, plan: &FramePlan, ) -> ( - std::collections::HashMap, + crate::cache::fast_hash::NodeIdHashMap, usize, ) { - let mut blits = std::collections::HashMap::new(); + let mut blits = crate::cache::fast_hash::new_node_id_map(); let mut cache_hits = 0usize; for id in &plan.promoted { @@ -469,7 +469,7 @@ impl Renderer { &mut self, canvas: &Canvas, plan: &FramePlan, - promoted_blits: Option<&std::collections::HashMap>, + promoted_blits: Option<&crate::cache::fast_hash::NodeIdHashMap>, ) -> usize { // Select effect quality based on frame stability. // Unstable (interactive) frames use reduced effects for performance. diff --git a/crates/grida-canvas/tests/compositor_effects.rs b/crates/grida-canvas/tests/compositor_effects.rs index afa4516c31..871300eee4 100644 --- a/crates/grida-canvas/tests/compositor_effects.rs +++ b/crates/grida-canvas/tests/compositor_effects.rs @@ -615,7 +615,7 @@ fn z_order_promoted_child_visible_above_container() { let offscreen_image = offscreen.image_snapshot(); // Step 2: Build the promoted_blits map - let mut promoted_blits: HashMap = HashMap::new(); + let mut promoted_blits: cg::cache::fast_hash::NodeIdHashMap = cg::cache::fast_hash::new_node_id_map(); let src_rect = Rect::new( 0.0, 0.0, From c40122b497efdc934a13982d25853ec13f269152 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 24 Mar 2026 16:32:13 +0900 Subject: [PATCH 04/31] perf(cg): adaptive stable delay for smooth slow-pan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slow panning (80-120ms trackpad intervals) was laggier than fast panning because the fixed 50ms stable delay fired between every scroll event, nuking the pan image cache and forcing expensive full redraws. Two changes fix this: 1. FrameLoop tracks input cadence via EMA and extends the effective stable delay to max(base, cadence × 2.5). During 80ms scrolling the delay becomes ~200ms, preventing stable intrusions mid-interaction. Cadence resets on session breaks (>500ms gap). 2. Pan image cache is no longer invalidated on stable-only frames in both queue() and apply_changes(). The render path recaptures the cache from every full-quality draw, so the next unstable frame always has a fresh cache. Benchmark results (fl_80ms, 200 nodes): - Stable frame intrusions: 25 → 1 - p50 frame time: 3,025µs → 163µs (18.6× improvement) Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/grida-canvas/src/runtime/frame_loop.rs | 192 +++++++++++++++++- crates/grida-canvas/src/runtime/scene.rs | 17 +- 2 files changed, 196 insertions(+), 13 deletions(-) diff --git a/crates/grida-canvas/src/runtime/frame_loop.rs b/crates/grida-canvas/src/runtime/frame_loop.rs index dd2455f567..30305ca475 100644 --- a/crates/grida-canvas/src/runtime/frame_loop.rs +++ b/crates/grida-canvas/src/runtime/frame_loop.rs @@ -25,10 +25,19 @@ pub struct FrameLoop { next_frame: u64, /// Host-time (ms) of last invalidation. last_change_time: f64, - /// Debounce threshold in milliseconds before stable frame fires. + /// Base debounce threshold in milliseconds before stable frame fires. stable_delay_ms: f64, /// True after an unstable render, until a stable render completes. needs_stable: bool, + /// Smoothed input interval (EMA, milliseconds) for adaptive stable delay. + /// + /// During slow trackpad scrolling, events arrive at 60-120ms intervals — + /// longer than the base 50ms stable delay. Without adaptation, every gap + /// triggers a stable frame that nukes the pan cache and forces a full + /// redraw. By tracking the input cadence, `poll()` extends the effective + /// delay to `max(base, cadence * CADENCE_MULTIPLIER)`, ensuring stable + /// frames only fire when the user truly stops interacting. + input_cadence_ms: f64, } /// What quality of frame to render. @@ -50,6 +59,21 @@ impl FrameLoop { /// Default debounce delay (milliseconds) before a stable frame fires. pub const DEFAULT_STABLE_DELAY_MS: f64 = 50.0; + /// Multiplier applied to the smoothed input cadence. + /// + /// Effective stable delay = max(base_delay, cadence × multiplier). + /// At 2.5×, an 80ms trackpad cadence yields a 200ms effective delay, + /// comfortably above the 80ms inter-event gap. + const CADENCE_MULTIPLIER: f64 = 2.5; + + /// Intervals longer than this (ms) are treated as session breaks and + /// do not update the cadence EMA. + const CADENCE_MAX_INTERVAL: f64 = 500.0; + + /// EMA smoothing factor for input cadence (0 < α ≤ 1). + /// Lower values = more smoothing, slower to react. + const CADENCE_ALPHA: f64 = 0.3; + /// Create a new `FrameLoop` with the default stable delay. pub fn new() -> Self { Self::with_stable_delay(Self::DEFAULT_STABLE_DELAY_MS) @@ -60,20 +84,50 @@ impl FrameLoop { Self { prev_frame: 0, next_frame: 0, - last_change_time: 0.0, + last_change_time: f64::NAN, // NAN = no prior invalidation stable_delay_ms, needs_stable: false, + input_cadence_ms: 0.0, } } - /// Something changed. Bumps `next_frame`, records timestamp, sets - /// `needs_stable = true`. O(1), no plan building. + /// Something changed. Bumps `next_frame`, records timestamp, updates + /// the input cadence EMA, and sets `needs_stable = true`. O(1). pub fn invalidate(&mut self, now: f64) { + // Update cadence EMA from the interval since the last invalidation. + if self.last_change_time.is_finite() { + let interval = now - self.last_change_time; + if interval > 0.0 && interval < Self::CADENCE_MAX_INTERVAL { + if self.input_cadence_ms == 0.0 { + // First sample — seed the EMA. + self.input_cadence_ms = interval; + } else { + self.input_cadence_ms = self.input_cadence_ms * (1.0 - Self::CADENCE_ALPHA) + + interval * Self::CADENCE_ALPHA; + } + } else if interval >= Self::CADENCE_MAX_INTERVAL { + // Session break — reset cadence for the new interaction. + self.input_cadence_ms = 0.0; + } + } + self.next_frame = self.next_frame.wrapping_add(1); self.last_change_time = now; self.needs_stable = true; } + /// Effective stable delay, accounting for input cadence. + /// + /// Returns `max(base_delay, cadence × CADENCE_MULTIPLIER)`. + fn effective_stable_delay(&self) -> f64 { + if self.input_cadence_ms > 0.0 { + self.stable_delay_ms + .max(self.input_cadence_ms * Self::CADENCE_MULTIPLIER) + } else { + self.stable_delay_ms + } + } + /// Called once per host frame. Returns: /// - `None` — idle, nothing to render /// - `Some(Unstable)` — change within debounce window, render fast @@ -90,7 +144,7 @@ impl FrameLoop { // No new frame pending, but do we owe a stable frame? if self.needs_stable { let elapsed = now - self.last_change_time; - if elapsed >= self.stable_delay_ms { + if elapsed >= self.effective_stable_delay() { return Some(FrameQuality::Stable); } } @@ -98,7 +152,8 @@ impl FrameLoop { None } - /// Mark frame as rendered. If `Stable`, clears `needs_stable`. + /// Mark frame as rendered. If `Stable`, clears `needs_stable` and + /// resets the input cadence for the next interaction session. /// If `Unstable`, keeps `needs_stable = true` so `poll()` will /// return `Stable` once the debounce expires. pub fn complete(&mut self, quality: FrameQuality) { @@ -106,6 +161,10 @@ impl FrameLoop { match quality { FrameQuality::Stable => { self.needs_stable = false; + // Do NOT reset cadence or last_change_time here. + // The user may still be interacting (slow trackpad). + // Cadence resets naturally in invalidate() when the + // interval exceeds CADENCE_MAX_INTERVAL (session break). } FrameQuality::Unstable => { // needs_stable stays true — poll() will fire Stable later @@ -202,7 +261,8 @@ mod tests { // Waiting for stable at 150ms... assert_eq!(fl.poll(140.0), None); - // New invalidation at 145ms resets the debounce + // New invalidation at 145ms resets the debounce. + // Cadence becomes 45ms → effective delay = max(50, 45×2.5) = 112.5ms. fl.invalidate(145.0); assert_eq!(fl.poll(145.0), Some(FrameQuality::Unstable)); fl.complete(FrameQuality::Unstable); @@ -210,8 +270,11 @@ mod tests { // Old debounce (150ms) shouldn't fire stable assert_eq!(fl.poll(155.0), None); - // New debounce (145 + 50 = 195ms) fires stable - assert_eq!(fl.poll(195.0), Some(FrameQuality::Stable)); + // Base debounce (195ms) not enough — cadence pushes it to 257.5ms + assert_eq!(fl.poll(195.0), None); + + // Adaptive debounce: 145 + 112.5 = 257.5ms + assert_eq!(fl.poll(258.0), Some(FrameQuality::Stable)); fl.complete(FrameQuality::Stable); assert!(fl.is_idle()); @@ -262,4 +325,115 @@ mod tests { fl.invalidate(20.0); assert_eq!(fl.current_frame(), 3); } + + #[test] + fn adaptive_delay_slow_cadence() { + // Simulate slow trackpad scrolling at 80ms intervals. + // The adaptive delay should prevent stable frames from firing + // between scroll events. + let mut fl = FrameLoop::new(); + + // First scroll event + fl.invalidate(0.0); + assert_eq!(fl.poll(0.0), Some(FrameQuality::Unstable)); + fl.complete(FrameQuality::Unstable); + + // Second scroll event at 80ms — cadence seeds to 80ms. + // Effective delay = max(50, 80×2.5) = 200ms. + fl.invalidate(80.0); + assert_eq!(fl.poll(80.0), Some(FrameQuality::Unstable)); + fl.complete(FrameQuality::Unstable); + + // 50ms after last event — base delay expired but adaptive delay + // keeps us idle. No stable frame intrusion! + assert_eq!(fl.poll(130.0), None); + + // Third scroll event at 160ms — cadence stays ~80ms. + fl.invalidate(160.0); + assert_eq!(fl.poll(160.0), Some(FrameQuality::Unstable)); + fl.complete(FrameQuality::Unstable); + + // Still no stable frame between events + assert_eq!(fl.poll(210.0), None); + + // User stops scrolling. Stable fires after adaptive delay. + // Cadence ≈ 80ms, effective delay ≈ 200ms. + // Last event at 160ms → stable at ~360ms. + assert_eq!(fl.poll(350.0), None); + assert_eq!(fl.poll(361.0), Some(FrameQuality::Stable)); + fl.complete(FrameQuality::Stable); + + assert!(fl.is_idle()); + } + + #[test] + fn cadence_persists_across_stable() { + let mut fl = FrameLoop::new(); + + // Build up a slow cadence (80ms) + fl.invalidate(0.0); + fl.complete(FrameQuality::Unstable); + fl.invalidate(80.0); + fl.complete(FrameQuality::Unstable); + + // Let stable fire — cadence persists (interaction might continue). + assert_eq!(fl.poll(280.0), Some(FrameQuality::Stable)); + fl.complete(FrameQuality::Stable); + + // Interaction continues soon after. The interval 300-80=220ms + // blends with the old cadence 80ms → EMA ≈ 122ms. + // Effective delay = max(50, 122×2.5) = 305ms. + fl.invalidate(300.0); + assert_eq!(fl.poll(300.0), Some(FrameQuality::Unstable)); + fl.complete(FrameQuality::Unstable); + + // 50ms later: base delay expired but cadence keeps us idle + assert_eq!(fl.poll(350.0), None); + // 200ms later: still within 305ms effective delay + assert_eq!(fl.poll(500.0), None); + // After effective delay: 300 + 305 = 605ms + assert_eq!(fl.poll(606.0), Some(FrameQuality::Stable)); + fl.complete(FrameQuality::Stable); + } + + #[test] + fn cadence_resets_on_session_break() { + let mut fl = FrameLoop::new(); + + // Build up cadence + fl.invalidate(0.0); + fl.complete(FrameQuality::Unstable); + fl.invalidate(80.0); + fl.complete(FrameQuality::Unstable); + + // Let stable fire + assert_eq!(fl.poll(280.0), Some(FrameQuality::Stable)); + fl.complete(FrameQuality::Stable); + + // Long pause (>500ms) — session break resets cadence. + fl.invalidate(900.0); // 900 - 80 = 820ms > 500ms → cadence resets to 0 + assert_eq!(fl.poll(900.0), Some(FrameQuality::Unstable)); + fl.complete(FrameQuality::Unstable); + + // Now cadence=0, so base delay (50ms) applies. + assert_eq!(fl.poll(950.0), Some(FrameQuality::Stable)); + } + + #[test] + fn adaptive_delay_fast_cadence_uses_base() { + // Fast input (16ms, typical 60fps mouse) — cadence × 2.5 = 40ms < base 50ms. + // The base delay should be used. + let mut fl = FrameLoop::new(); + + fl.invalidate(0.0); + fl.complete(FrameQuality::Unstable); + fl.invalidate(16.0); + fl.complete(FrameQuality::Unstable); + fl.invalidate(32.0); + fl.complete(FrameQuality::Unstable); + + // Base delay applies: 32 + 50 = 82ms + assert_eq!(fl.poll(81.0), None); + assert_eq!(fl.poll(82.0), Some(FrameQuality::Stable)); + } } diff --git a/crates/grida-canvas/src/runtime/scene.rs b/crates/grida-canvas/src/runtime/scene.rs index 1449246499..68502afbb1 100644 --- a/crates/grida-canvas/src/runtime/scene.rs +++ b/crates/grida-canvas/src/runtime/scene.rs @@ -1407,10 +1407,13 @@ impl Renderer { self.scene_cache.compositor.mark_all_stale(); } - // Invalidate pan image cache when zoom changes or on stable frames. + // Invalidate pan image cache on zoom changes only. // Zoom changes alter the pixel content (different scale/density). - // Stable frames should produce a full-quality render, not a cached blit. - if camera_change.zoom_changed() || stable { + // Stable frames do NOT nuke the pan cache — the cache is valid for + // pan-only and no-change scenarios. The render path recaptures it + // from the full-quality draw anyway, so the next unstable frame + // always has a fresh cache to blit from. + if camera_change.zoom_changed() { self.pan_image_cache = None; } @@ -1619,7 +1622,13 @@ impl Renderer { // ----- Viewport snapshot caches (pan/zoom image caches) ----- // These are the ONLY caches that truly depend on viewport dimensions. - let invalidate_pan = has_data_changes || camera_change.zoom_changed() || stable; + // + // Pan cache: invalidate on data changes or zoom changes. Stable + // frames do NOT nuke the pan cache — during slow panning, a stable + // frame firing between scroll events would destroy the cache and + // force an expensive full redraw on the next unstable frame. The + // stable frame's render path recaptures the pan cache anyway. + let invalidate_pan = has_data_changes || camera_change.zoom_changed(); let invalidate_zoom = has_data_changes || stable; if invalidate_pan { From 2e89cdee3b9527696a56bad7a4f139ef4e5b76f4 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 24 Mar 2026 19:43:19 +0900 Subject: [PATCH 05/31] feat(scene): add scene_envelope method for bounding envelope retrieval Introduced a new method `scene_envelope` in `SceneCache` to return the bounding envelope of all scene content in the R-tree. This method provides an O(1) operation by reading the cached root node envelope and returns `None` if the scene is empty. This enhancement improves the efficiency of scene content management. --- crates/grida-canvas/src/cache/scene.rs | 11 ++++ docs/wg/feat-2d/optimization.md | 81 ++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/crates/grida-canvas/src/cache/scene.rs b/crates/grida-canvas/src/cache/scene.rs index c9bdda7cad..85c873cc4a 100644 --- a/crates/grida-canvas/src/cache/scene.rs +++ b/crates/grida-canvas/src/cache/scene.rs @@ -191,6 +191,17 @@ impl SceneCache { .collect() } + /// Return the bounding envelope of all scene content in the R-tree. + /// + /// O(1) — reads the cached root node envelope. Returns `None` when + /// the scene is empty (no layers indexed). + pub fn scene_envelope(&self) -> Option> { + if self.layer_index.size() == 0 { + return None; + } + Some(self.layer_index.root().envelope()) + } + /// Query painter layer indices whose bounds contain the given point. pub fn intersects_point(&self, point: Vector2) -> Vec { let env = AABB::from_point([point[0], point[1]]); diff --git a/docs/wg/feat-2d/optimization.md b/docs/wg/feat-2d/optimization.md index 4e4a573a93..84b841fbc4 100644 --- a/docs/wg/feat-2d/optimization.md +++ b/docs/wg/feat-2d/optimization.md @@ -983,6 +983,87 @@ Chromium uses 64 MB default with soft/hard limits. --- +## Slow-Pan Smoothness (FrameLoop) + +Slow trackpad panning (80-120ms between scroll events) was laggier than +fast panning — the fixed 50ms stable delay caused stable frames to fire +between every pair of scroll events, nuking the pan image cache and forcing +expensive full redraws. + +45. **Adaptive Stable Delay** ✅ IMPLEMENTED + + `FrameLoop` tracks input cadence via exponential moving average and + extends the effective stable delay to `max(base_delay, cadence × 2.5)`. + During 80ms trackpad scrolling, the delay becomes ~200ms. Stable frames + only fire when the user truly stops interacting. + + Cadence resets on session breaks (>500ms gap between events). Cadence + persists across stable frames (the user may still be scrolling slowly). + + **Measured impact (synthetic 200-node scene, `fl_80ms` scenario):** + + | Metric | Before | After | Improvement | + | ----------------- | -------- | ------ | ----------- | + | Stable intrusions | 25 | 1 | -96% | + | p50 frame time | 3,025 µs | 163 µs | 18.6× | + + Implementation: `FrameLoop` in `runtime/frame_loop.rs`. + +46. **Pan Cache Preservation on Stable Frames** ✅ IMPLEMENTED + + Stable frames no longer invalidate `pan_image_cache`. Previously, + `apply_changes()` and `queue()` nuked the pan cache when `stable=true`, + forcing the next unstable frame to do a full redraw. Since the stable + frame's render path recaptures the cache from every full-quality draw + anyway, the next unstable frame always has a fresh cache to blit from. + + Implementation: removed `|| stable` from `invalidate_pan` condition in + both `apply_changes()` and `queue()` in `runtime/scene.rs`. + +47. **Fully-Visible Stable Frame Fast Path** (reverted — correctness issues) + + When the viewport fully contains all scene content (`scene_envelope()` + containment check, O(1) via R-tree root node), the stable frame's full + draw path is redundant — the pan image cache already has the correct + pixels. The idea: blit from pan cache instead of doing an O(N) redraw. + + **Why it was reverted:** Three correctness bugs in succession: + 1. Blitting at `(0,0)` instead of the correct `(dx, dy)` offset caused + content to jump to the wrong position after panning stopped. + 2. Blitting at `(dx, dy)` clips content at viewport edges — the stable + frame never filled in the exposed strips, leaving permanent culling + artifacts at max zoom-out. + 3. Requiring `dx == dy == 0` and skipping the blit entirely (assuming + GPU surface persistence) caused stale back-buffer content to + accumulate — double-buffered GPU surfaces don't preserve content + across swaps. + + **The idea is valid but needs a different approach:** + - The `scene_envelope()` utility (O(1) R-tree root AABB) is kept in + `cache/scene.rs` for future use. + - A correct implementation would need either: (a) always blit the pan + cache at `(0,0)` after verifying the cache was captured at the + current camera position (not just any position), or (b) use a + `last_had_data_changes` flag that is reliably set in BOTH the + `frame()` and legacy `redraw()` code paths. + - The legacy `redraw()` path does not call `apply_changes()`, so any + flag set there is stale. Migrating all hosts to `frame()` would + eliminate this dual-path problem. + - The `queue()` stable promotion (non-camera events → stable quality) + interacts badly with clamped zoom at min/max zoom limits — the zoom + doesn't actually change, so `camera_change == None`, causing + unintended stable promotion that nukes the zoom cache and forces a + ~100ms full redraw. + + **Key files for future implementation:** + - `runtime/scene.rs` — `render_frame_with_plan_state()`, between the + pan-only cache check and the zoom cache check + - `cache/scene.rs` — `scene_envelope()` (already implemented) + - `runtime/scene.rs` — `apply_changes()` for `last_had_data_changes` + - `window/application.rs` — `frame()` vs `redraw()` dual-path issue + +--- + This list is designed to evolve the renderer from single-threaded mode to scalable, GPU-friendly real-time performance. Items are ordered roughly by implementation priority within each section. From 8b34c730966a3f2cdd7d82ac638ce04c209dc4af Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 24 Mar 2026 20:34:26 +0900 Subject: [PATCH 06/31] clean logs --- crates/grida-dev/src/platform/native_demo.rs | 26 ++++++++++++++++---- crates/grida-dev/src/platform/winit.rs | 15 ++++++++--- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/crates/grida-dev/src/platform/native_demo.rs b/crates/grida-dev/src/platform/native_demo.rs index a2584abd35..17b0a8047c 100644 --- a/crates/grida-dev/src/platform/native_demo.rs +++ b/crates/grida-dev/src/platform/native_demo.rs @@ -6,6 +6,7 @@ use cg::window::application::{ApplicationApi, HostEvent, HostEventCallback}; use futures::channel::mpsc; use std::path::PathBuf; use std::sync::Arc; +use std::time::Instant; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; #[allow(dead_code)] @@ -74,8 +75,9 @@ async fn run_demo_window_core_multi( { let width = 1080; let height = 1080; + let startup_started_at = Instant::now(); - println!("🚀 Starting demo window..."); + println!("[demo] starting demo window"); let (tx, rx) = mpsc::unbounded(); let (font_tx, font_rx) = mpsc::unbounded(); @@ -92,15 +94,23 @@ async fn run_demo_window_core_multi( file_drop_tx.is_some(), scenes_rx, ); + println!( + "[demo] native application initialized in {:?}", + startup_started_at.elapsed() + ); let proxy = el.create_proxy(); let surface_ptr = app.app.surface_mut_ptr(); app.app.set_renderer_backend(Backend::GL(surface_ptr)); - println!("📸 Initializing image loader..."); - println!("🔄 Starting to load scene images in background..."); + println!( + "[demo] initializing image loader at {:?}", + startup_started_at.elapsed() + ); + println!("[demo] loading scene images in background"); let scene_clone = scene.clone(); let tx_clone = tx.clone(); + let image_load_started_at = Instant::now(); let event_cb: HostEventCallback = { let proxy_clone = proxy.clone(); Arc::new(move |event: HostEvent| { @@ -115,7 +125,10 @@ async fn run_demo_window_core_multi( let event_cb = event_cb.clone(); futures::executor::block_on(async move { load_scene_images(&scene_clone, tx_clone, event_cb).await; - println!("✅ Scene images loading completed in background"); + println!( + "[demo] scene images loaded in {:?}", + image_load_started_at.elapsed() + ); }); }); @@ -140,7 +153,10 @@ async fn run_demo_window_core_multi( app.app.devtools_rendering_set_show_ruler(true); app.app.devtools_rendering_set_show_tiles(false); - println!("🎭 Starting event loop..."); + println!( + "[demo] entering event loop after {:?}", + startup_started_at.elapsed() + ); if let Err(e) = el.run_app(&mut app) { eprintln!("Event loop error: {:?}", e); } diff --git a/crates/grida-dev/src/platform/winit.rs b/crates/grida-dev/src/platform/winit.rs index 449cbc42b2..64cdde3c43 100644 --- a/crates/grida-dev/src/platform/winit.rs +++ b/crates/grida-dev/src/platform/winit.rs @@ -11,7 +11,7 @@ use glutin_winit::DisplayBuilder; #[allow(deprecated)] use raw_window_handle::HasRawWindowHandle; use skia_safe::gpu; -use std::{ffi::CString, num::NonZeroU32}; +use std::{ffi::CString, num::NonZeroU32, time::Instant}; use winit::{ dpi::LogicalSize, event_loop::EventLoop, @@ -28,7 +28,8 @@ pub(crate) struct WinitResult { } pub(crate) fn winit_window(width: i32, height: i32) -> WinitResult { - println!("🔄 Window process started with PID: {}", std::process::id()); + let setup_started_at = Instant::now(); + println!("[winit] process started (pid={})", std::process::id()); let el = EventLoop::::with_user_event().build().unwrap(); @@ -54,7 +55,11 @@ pub(crate) fn winit_window(width: i32, height: i32) -> WinitResult { best }) .expect("failed to build window"); - println!("Picked a config with {} samples", gl_config.num_samples()); + println!( + "[winit] picked GL config: samples={} (elapsed={:?})", + gl_config.num_samples(), + setup_started_at.elapsed() + ); let window = window.expect("Could not create window with OpenGL context"); #[allow(deprecated)] let raw_window_handle = window @@ -148,6 +153,10 @@ pub(crate) fn winit_window(width: i32, height: i32) -> WinitResult { .expect("Could not create skia surface"); let state = SurfaceState::from_parts(gr_context, fb_info, surface); + println!( + "[winit] GL/Skia surface initialized in {:?}", + setup_started_at.elapsed() + ); WinitResult { state, From 00e31dc5eb1fe819d3acad105854f8860cdca453 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 24 Mar 2026 20:53:13 +0900 Subject: [PATCH 07/31] refactor(renderer): enhance content caching and apply_changes logic Updated the `Renderer` to improve handling of cached content for overlay-only frames, allowing for faster rendering when neither scene data nor camera changes occur. Introduced a new method `blit_content_cache` to restore cached content efficiently. Modified `apply_changes` to return a boolean indicating whether content needs re-rendering, optimizing the rendering process during stable frames. Additionally, updated the `UnknownTargetApplication` to utilize these enhancements for better performance during frame rendering. --- crates/grida-canvas/src/runtime/scene.rs | 51 +++++++++++++++++-- crates/grida-canvas/src/window/application.rs | 28 +++++++++- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/crates/grida-canvas/src/runtime/scene.rs b/crates/grida-canvas/src/runtime/scene.rs index 1b0368534c..990eabcdef 100644 --- a/crates/grida-canvas/src/runtime/scene.rs +++ b/crates/grida-canvas/src/runtime/scene.rs @@ -475,7 +475,9 @@ impl Renderer { &mut self, canvas: &Canvas, plan: &FramePlan, - promoted_blits: Option<&crate::cache::fast_hash::NodeIdHashMap>, + promoted_blits: Option< + &crate::cache::fast_hash::NodeIdHashMap, + >, ) -> usize { // Select effect quality based on frame stability. // Unstable (interactive) frames use reduced effects for performance. @@ -910,7 +912,7 @@ impl Renderer { // // When the offset exceeds the viewport (no overlap with cached frame), // we fall through to a full redraw which captures a new snapshot. - if camera_change == CameraChangeKind::PanOnly && self.backend.is_gpu() { + if !stable && !camera_change.zoom_changed() && self.backend.is_gpu() { if let Some(ref cache) = self.pan_image_cache { let width = surface.width() as f32; let height = surface.height() as f32; @@ -1434,8 +1436,10 @@ impl Renderer { let can_defer = !stable && self.backend.is_gpu() && ( + // No content or camera change — overlay-only (marquee, hover) + (!camera_change.any_changed() && self.pan_image_cache.is_some()) // Pan cache will likely hit - (camera_change == CameraChangeKind::PanOnly && self.pan_image_cache.is_some()) + || (camera_change == CameraChangeKind::PanOnly && self.pan_image_cache.is_some()) // Zoom cache will likely hit || (camera_change.zoom_changed() && self.zoom_image_cache.is_some()) ); @@ -1511,6 +1515,31 @@ impl Renderer { Some(self.render_frame(plan)) } + /// Restore cached content for overlay-only frames. + /// + /// Blits the pan image cache at (0,0) onto the backend surface, + /// restoring the content layer without the previous overlay pixels. + /// Returns `true` if the blit succeeded, `false` if no cache exists. + /// + /// Used when neither scene data nor the camera changed — the content + /// is identical, so we skip the expensive frame-plan + draw and just + /// repaint the overlay on top of cached content. + pub fn blit_content_cache(&mut self) -> bool { + if !self.backend.is_gpu() { + return false; + } + let cache = match self.pan_image_cache.as_ref() { + Some(c) => c, + None => return false, + }; + let surface = unsafe { &mut *self.backend.get_surface() }; + let canvas = surface.canvas(); + // Draw the cached content snapshot at the same position (dx=0, dy=0). + canvas.draw_image(&cache.image, (0.0, 0.0), None); + Self::gpu_flush(surface); + true + } + /// Clear the cached scene picture. /// /// **Prefer [`mark_changed`] + [`apply_changes`]** for new code. @@ -1555,12 +1584,17 @@ impl Renderer { /// /// The `camera_change` parameter is folded in so that zoom/pan /// invalidation lives in the same dispatch table. - pub fn apply_changes(&mut self, camera_change: CameraChangeKind, stable: bool) { + /// Returns `true` when content actually needs re-rendering (data or + /// camera changed). Returns `false` for overlay-only frames (e.g. + /// marquee drag, hover highlight) where the caller can skip the + /// expensive frame-plan + draw and just blit the cached content. + pub fn apply_changes(&mut self, camera_change: CameraChangeKind, stable: bool) -> bool { let cs = self.changes.take(); let flags = cs.flags(); // Fast path: nothing changed (pure camera move handled below). let has_data_changes = !flags.is_empty(); + let content_changed = has_data_changes || camera_change.any_changed(); // ----- Layout ----- // Scene load handles its own layout in load_scene(); skip here. @@ -1629,7 +1663,12 @@ impl Renderer { // force an expensive full redraw on the next unstable frame. The // stable frame's render path recaptures the pan cache anyway. let invalidate_pan = has_data_changes || camera_change.zoom_changed(); - let invalidate_zoom = has_data_changes || stable; + // Zoom cache: invalidate when content changed OR when a stable frame + // follows a camera change (full-quality recapture needed). Overlay- + // only frames (no data + no camera change) must NOT nuke the zoom + // cache — the content is identical, and destroying the cache forces + // an expensive full draw on the next real zoom interaction. + let invalidate_zoom = has_data_changes || (stable && camera_change.any_changed()); if invalidate_pan { self.pan_image_cache = None; @@ -1637,6 +1676,8 @@ impl Renderer { if invalidate_zoom { self.zoom_image_cache = None; } + + content_changed } /// Check whether the current scene has layout that depends on viewport size. diff --git a/crates/grida-canvas/src/window/application.rs b/crates/grida-canvas/src/window/application.rs index 87f3b2214c..9172e32efa 100644 --- a/crates/grida-canvas/src/window/application.rs +++ b/crates/grida-canvas/src/window/application.rs @@ -557,11 +557,15 @@ impl ApplicationApi for UnknownTargetApplication { fn runtime_renderer_set_pixel_preview_scale(&mut self, scale: u8) { self.renderer.set_pixel_preview_scale(scale); + self.renderer + .mark_changed(crate::runtime::changes::ChangeFlags::CONFIG); self.queue(); } fn runtime_renderer_set_pixel_preview_stable(&mut self, stable: bool) { self.renderer.set_pixel_preview_strategy_stable(stable); + self.renderer + .mark_changed(crate::runtime::changes::ChangeFlags::CONFIG); self.queue(); } @@ -571,6 +575,8 @@ impl ApplicationApi for UnknownTargetApplication { ) { let policy = crate::runtime::render_policy::RenderPolicy::from_flags(flags); self.renderer.set_render_policy(policy); + self.renderer + .mark_changed(crate::runtime::changes::ChangeFlags::CONFIG); self.queue(); } @@ -1133,12 +1139,32 @@ impl UnknownTargetApplication { // (viewport resize, font/image loads, text edits, config changes) // and camera state in one place. This replaces the ad-hoc // invalidate_compositor_on_zoom() call and all per-site cache nuking. - self.renderer.apply_changes(camera_change, stable); + let content_changed = self.renderer.apply_changes(camera_change, stable); // Warm the camera cache once per frame so view_matrix(), rect(), and // screen_to_canvas_point() are essentially free for the rest of this frame. self.renderer.camera.warm_cache(); + // 5a. Overlay-only fast path + // + // When neither scene data nor the camera changed (e.g. marquee drag, + // hover highlight, selection change), the content layer is identical + // to the previous frame. Restore it from the pan image cache and + // skip the expensive frame-plan build + full draw. The overlay is + // still re-drawn below so marquee/selection visuals update correctly. + if !content_changed && self.renderer.blit_content_cache() { + // Consume the camera change (no-op here, but keeps the contract). + self.renderer.camera.consume_change(); + + // Draw devtools overlays on top of the restored content. + let _overlay_time = self.draw_and_flush_devtools_overlay(); + + // Complete frame in the loop. + self.frame_loop.complete(quality); + self.last_frame_time = __frame_start; + return true; + } + // Build frame plan lazily let rect = self.renderer.camera.rect(); let zoom = self.renderer.camera.get_zoom(); From cc9a6a12c946b7d4f75500fa611f8771e7c0893e Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 24 Mar 2026 21:08:47 +0900 Subject: [PATCH 08/31] feat(hittest): add intersects_topmost method for optimized hit testing Introduced the `intersects_topmost` method in `HitTester`, which returns the shallowest nodes whose bounding boxes intersect a given rectangle. This method improves performance by ensuring that only ancestor nodes are returned, avoiding unnecessary descendant checks. Updated the `handle_pointer_move` function in `SurfaceState` to utilize this new method for more efficient selection during pointer interactions. --- crates/grida-canvas/src/hittest/hit_tester.rs | 65 +++++++++++++++++++ crates/grida-canvas/src/surface/state.rs | 10 +-- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/crates/grida-canvas/src/hittest/hit_tester.rs b/crates/grida-canvas/src/hittest/hit_tester.rs index 3f28afa84d..3c0a0b2483 100644 --- a/crates/grida-canvas/src/hittest/hit_tester.rs +++ b/crates/grida-canvas/src/hittest/hit_tester.rs @@ -278,4 +278,69 @@ impl<'a> HitTester<'a> { } out } + + /// Returns the shallowest nodes whose bounding boxes intersect `rect`. + /// + /// Like [`intersects`], but when a node and one of its descendants + /// both match, only the ancestor is returned. The result contains no + /// node whose ancestor is also in the result. + /// + /// Layers are processed in ascending z-index order (parents before + /// children in the paint tree). Once a node is accepted, all of its + /// descendants are skipped with a single parent lookup each, making + /// the pruning effectively O(K) amortized. + pub fn intersects_topmost(&self, rect: &Rectangle) -> Vec { + let mut indices = self.cache.intersects(*rect); + if indices.is_empty() { + return Vec::new(); + } + indices.sort(); + + // Set of selected node IDs — used for O(1) ancestor lookups. + let mut selected_set = + std::collections::HashSet::with_capacity(indices.len().min(256)); + let mut out = Vec::with_capacity(indices.len().min(256)); + + let center_point = [rect.x + rect.width / 2.0, rect.y + rect.height / 2.0]; + + // Ascending z-index order: parents appear before their children. + for idx in indices { + let entry = &self.cache.layers.layers[idx]; + let id = &entry.id; + + // Quick check: does any ancestor already cover this node? + if self.has_selected_ancestor(id, &selected_set) { + continue; + } + + // Bounds + clip check (same as `intersects`) + if let Some(bounds) = self.cache.geometry.get_world_bounds(id) { + if rect::intersects(&bounds, rect) { + if self.is_point_within_parent_clip_bounds(id, center_point) { + selected_set.insert(*id); + out.push(*id); + } + } + } + } + out + } + + /// Walk up the parent chain and return `true` if any ancestor is in + /// `selected`. Stops as soon as a match is found — amortized O(1) + /// when parents are typically selected before their children. + fn has_selected_ancestor( + &self, + id: &NodeId, + selected: &std::collections::HashSet, + ) -> bool { + let mut current = self.cache.geometry.get_parent(id); + while let Some(parent) = current { + if selected.contains(&parent) { + return true; + } + current = self.cache.geometry.get_parent(&parent); + } + false + } } diff --git a/crates/grida-canvas/src/surface/state.rs b/crates/grida-canvas/src/surface/state.rs index 64e0705be9..d1e0b361ad 100644 --- a/crates/grida-canvas/src/surface/state.rs +++ b/crates/grida-canvas/src/surface/state.rs @@ -94,7 +94,7 @@ impl SurfaceState { SurfaceEvent::PointerMove { canvas_point, screen_point, - } => self.handle_pointer_move(canvas_point, screen_point, hit_tester, hierarchy, ui_hit_regions), + } => self.handle_pointer_move(canvas_point, screen_point, hit_tester, ui_hit_regions), SurfaceEvent::PointerDown { canvas_point, @@ -156,7 +156,6 @@ impl SurfaceState { canvas_point: math2::vector2::Vector2, screen_point: math2::vector2::Vector2, hit_tester: &HitTester, - hierarchy: &impl Hierarchy, ui_hit_regions: &HitRegions, ) -> SurfaceResponse { let mut response = SurfaceResponse::none(); @@ -206,11 +205,12 @@ impl SurfaceState { current_canvas: canvas_point, }; - // Compute tentative selection from marquee rectangle + // Compute tentative selection from marquee rectangle. + // Use intersects_topmost to get only the shallowest + // matching ancestors — avoids the separate O(K*D) prune. let rect = marquee_rect(anchor_canvas, canvas_point); - let hits = hit_tester.intersects(&rect); + let hits = hit_tester.intersects_topmost(&rect); self.selection.set(hits); - self.prune_selection(hierarchy); response.selection_changed = true; response.needs_redraw = true; From 4f2be30f2ff97369953cb553da4a0a9c0a6c9aa6 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 24 Mar 2026 21:37:37 +0900 Subject: [PATCH 09/31] refactor(image-loading): streamline raster image handling and URL extraction Refactored the image loading process to improve the handling of raster images. Introduced a new `load_raster` function that reads image bytes and registers them with the renderer, allowing for direct scene creation. Updated the `extract_image_urls` function to collect URLs from various node types more efficiently by utilizing a dedicated helper function for processing paints. This change enhances the overall performance and clarity of the image extraction logic. --- crates/grida-canvas/src/resources/mod.rs | 98 ++++++++++++++---- crates/grida-dev/src/main.rs | 125 +++++++++++++++++++---- 2 files changed, 183 insertions(+), 40 deletions(-) diff --git a/crates/grida-canvas/src/resources/mod.rs b/crates/grida-canvas/src/resources/mod.rs index 5d2162ae4b..0de99cd1ad 100644 --- a/crates/grida-canvas/src/resources/mod.rs +++ b/crates/grida-canvas/src/resources/mod.rs @@ -118,28 +118,90 @@ pub struct FontMessage { pub data: Vec, } -/// Extract all image URLs from a scene. -pub fn extract_image_urls(scene: &Scene) -> Vec { - // FIXME: this should either iterate the fills / strokes (all paints) rather then iterating the nodes. - the below implementation is legacy. +/// Collect the URL string from a [`ResourceRef`]. +fn resource_ref_url(r: &ResourceRef) -> &str { + match r { + ResourceRef::RID(s) | ResourceRef::HASH(s) => s, + } +} + +/// Push any image URLs found in a paint slice. +fn collect_image_urls_from_paints(paints: &[Paint], out: &mut Vec) { + for paint in paints { + if let Paint::Image(img) = paint { + let url = resource_ref_url(&img.image); + if !url.is_empty() { + out.push(url.to_owned()); + } + } + } +} + +/// Extract all image URLs from a scene by inspecting every node's fills, +/// strokes, and dedicated image references. +// TODO: consider a dedicated paints store or iterator so this doesn't need +// to match every node variant individually. +fn extract_image_urls(scene: &Scene) -> Vec { + use crate::node::schema::Node; let mut urls = Vec::new(); for (id, _) in scene.graph.iter() { - if let Ok(n) = scene.graph.get_node(id) { - if let crate::node::schema::Node::Rectangle(rect) = n { - for fill in &rect.fills { - if let Paint::Image(img) = fill { - match &img.image { - ResourceRef::RID(r) | ResourceRef::HASH(r) => urls.push(r.clone()), - } - } - } - for stroke in &rect.strokes { - if let Paint::Image(img) = stroke { - match &img.image { - ResourceRef::RID(r) | ResourceRef::HASH(r) => urls.push(r.clone()), - } - } + let Ok(node) = scene.graph.get_node(id) else { + continue; + }; + + match node { + Node::Image(n) => { + let url = resource_ref_url(&n.fill.image); + if !url.is_empty() { + urls.push(url.to_owned()); } } + Node::Rectangle(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::Ellipse(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::Container(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::Vector(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::Polygon(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::RegularPolygon(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::RegularStarPolygon(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::Path(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::BooleanOperation(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::TextSpan(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::Line(n) => { + // LineNodeRec has strokes only, no fills. + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + // Group, InitialContainer, and Error nodes have no paint data. + Node::Group(_) | Node::InitialContainer(_) | Node::Error(_) => {} } } urls diff --git a/crates/grida-dev/src/main.rs b/crates/grida-dev/src/main.rs index 6ee9dee2b5..b92b12daca 100644 --- a/crates/grida-dev/src/main.rs +++ b/crates/grida-dev/src/main.rs @@ -184,8 +184,10 @@ async fn load_scenes_from_source(source: &str) -> Result> { if let Some(ext) = path.extension().and_then(|e| e.to_str()) { match ext.to_ascii_lowercase().as_str() { "svg" => return scene_from_svg_path(path).map(|s| vec![s]), - "png" | "jpg" | "jpeg" | "webp" => { - return scene_from_raster_path(path).map(|s| vec![s]) + // Raster images should be loaded via load_raster() by the caller + // so bytes can be registered with the renderer. + ext if is_raster_ext(ext) => { + return load_raster(path).map(|r| vec![r.scene]) } _ => {} } @@ -230,8 +232,26 @@ fn build_empty_scene() -> Scene { } async fn run_interactive(file: Option) -> Result<()> { + // Load initial scene(s). For raster images we also capture the raw bytes + // so we can register them with the renderer once it's ready. + let mut initial_image: Option = None; let initial_scenes = if let Some(ref source) = file { - load_scenes_from_source(source).await? + let path = Path::new(source); + let ext = path + .extension() + .and_then(|e| e.to_str()) + .map(|s| s.to_ascii_lowercase()) + .unwrap_or_default(); + if !is_url(source) && is_raster_ext(&ext) { + let raster = load_raster(path)?; + initial_image = Some(ImageMessage { + src: raster.rid, + data: raster.bytes, + }); + vec![raster.scene] + } else { + load_scenes_from_source(source).await? + } } else { vec![build_empty_scene()] }; @@ -249,6 +269,12 @@ async fn run_interactive(file: Option) -> Result<()> { run_demo_window_with_drop( first, move |_renderer, tx, _font_tx, proxy| { + // Register initial raster image bytes if present. + if let Some(msg) = initial_image { + let _ = tx.unbounded_send(msg.clone()); + let _ = proxy.send_event(HostEvent::ImageLoaded(msg)); + } + let mut guard = drop_rx.lock().expect("drop rx mutex poisoned"); let drop_rx = guard.take().expect("drop receiver already taken"); start_master_drop_task(drop_rx, tx.clone(), proxy.clone(), scenes_tx); @@ -278,36 +304,60 @@ fn scene_from_svg_path(path: &Path) -> Result { }) } -fn scene_from_raster_path(path: &Path) -> Result { +/// Result of loading a raster image: the scene plus the raw bytes and RID +/// so the caller can register the image with the renderer directly. +struct RasterScene { + scene: Scene, + /// Raw image bytes read from disk. + bytes: Vec, + /// The `res://images/{hash}` RID used by the node's fill. + rid: String, +} + +fn load_raster(path: &Path) -> Result { use cg::cg::prelude::CGColor; - use cg::cg::types::ResourceRef; + use cg::cg::types::{Paints, ResourceRef}; use cg::node::factory::NodeFactory; use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::{Node, Size}; + use cg::resources::hash_bytes; + + let bytes = std::fs::read(path) + .with_context(|| format!("failed to read image file {}", path.display()))?; let (width, height) = image_dimensions(path) .with_context(|| format!("failed to read image dimensions {}", path.display()))?; - let mut graph = SceneGraph::new(); - let nf = NodeFactory::new(); - let mut image_node = nf.create_image_node(); - image_node.size = Size { - width: width as f32, - height: height as f32, - }; - image_node.image = ResourceRef::RID(path.to_string_lossy().into_owned()); + let rid = format!("res://images/{:016x}", hash_bytes(&bytes)); + let ref_ = ResourceRef::RID(rid.clone()); - graph.append_child(Node::Image(image_node), Parent::Root); + let nf = NodeFactory::new(); + let mut node = nf.create_image_node(); + node.size = Size { width: width as f32, height: height as f32 }; + node.image = ref_.clone(); + node.fill.image = ref_; + node.strokes = Paints::default(); - Ok(Scene { - name: path - .file_stem() - .map(|n| n.to_string_lossy().into_owned()) - .unwrap_or_else(|| "Image".to_string()), - graph, - background_color: Some(CGColor::from_u32(0xF8F8F8FF)), + let mut graph = SceneGraph::new(); + graph.append_child(Node::Image(node), Parent::Root); + + Ok(RasterScene { + scene: Scene { + name: path + .file_stem() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| "Image".to_string()), + graph, + background_color: Some(CGColor::from_u32(0xF8F8F8FF)), + }, + bytes, + rid, }) } +fn is_raster_ext(ext: &str) -> bool { + matches!(ext, "png" | "jpg" | "jpeg" | "webp") +} + async fn load_master_scenes_from_path(path: &Path) -> Result> { let ext = path .extension() @@ -318,7 +368,7 @@ async fn load_master_scenes_from_path(path: &Path) -> Result> { match ext.as_str() { "grida" | "grida1" => load_scenes_from_source(&path.to_string_lossy()).await, "svg" => scene_from_svg_path(path).map(|s| vec![s]), - "png" | "jpg" | "jpeg" | "webp" => scene_from_raster_path(path).map(|s| vec![s]), + // Raster images are handled separately in start_master_drop_task. other => Err(anyhow::anyhow!( "Unsupported dropped file type ({}): {}", other, @@ -335,6 +385,37 @@ fn start_master_drop_task( ) { tokio::spawn(async move { while let Some(path) = drop_rx.recv().await { + let ext = path + .extension() + .and_then(|e| e.to_str()) + .map(|s| s.to_ascii_lowercase()) + .unwrap_or_default(); + + // Raster images: read bytes, register image, then send scene. + // No need to go through load_scene_images / extract_image_urls. + if is_raster_ext(&ext) { + match load_raster(&path) { + Ok(raster) => { + // Send image bytes so the renderer registers them. + let msg = ImageMessage { + src: raster.rid, + data: raster.bytes, + }; + let _ = image_tx.unbounded_send(msg.clone()); + let _ = proxy.send_event(HostEvent::ImageLoaded(msg)); + + if scenes_tx.send(vec![raster.scene]).is_err() { + eprintln!("failed to send scene to window"); + } + } + Err(err) => { + eprintln!("Failed to load dropped image {}: {err}", path.display()) + } + } + continue; + } + + // Non-raster files: load scenes, then resolve any embedded image refs. match load_master_scenes_from_path(&path).await { Ok(scenes) => { let scenes_for_loader = scenes.clone(); From d8756251871484b4ee60c1e7dc9c371d17bd16cc Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 24 Mar 2026 21:38:16 +0900 Subject: [PATCH 10/31] feat(native-application): add exiting flag to manage event processing during application closure Introduced an `exiting` boolean flag in the `NativeApplication` struct to prevent event processing after a `CloseRequested` event. This change ensures that events are not processed while the application is in the process of shutting down, improving stability and preventing potential issues during termination. --- .../grida-dev/src/platform/native_application.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/grida-dev/src/platform/native_application.rs b/crates/grida-dev/src/platform/native_application.rs index fa7a6b62bd..bc5692855f 100644 --- a/crates/grida-dev/src/platform/native_application.rs +++ b/crates/grida-dev/src/platform/native_application.rs @@ -112,6 +112,10 @@ pub struct NativeApplication { pub(crate) modifiers: winit::keyboard::ModifiersState, file_drop_tx: Option>, fit_scene_on_load: bool, + /// Set to `true` after `CloseRequested` to prevent event processing on + /// a partially-torn-down application (the tick thread may still deliver + /// events between `event_loop.exit()` and actual termination). + exiting: bool, /// When >0, the next N ticks should request a redraw to produce a /// settle frame (showing "none" after a gesture ends). settle_countdown: u8, @@ -203,6 +207,7 @@ impl NativeApplication { modifiers: winit::keyboard::ModifiersState::default(), file_drop_tx, fit_scene_on_load, + exiting: false, settle_countdown: 0, scenes: Vec::new(), scene_index: 0, @@ -237,8 +242,13 @@ impl NativeApplicationHandler for NativeApplication { } if let WindowEvent::CloseRequested = &event { - self.app.renderer_mut().free(); + self.exiting = true; event_loop.exit(); + return; + } + + if self.exiting { + return; } if let WindowEvent::Resized(size) = &event { @@ -377,6 +387,9 @@ impl NativeApplicationHandler for NativeApplication { } fn user_event(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop, event: HostEvent) { + if self.exiting { + return; + } match event { HostEvent::Tick => { // Poll for new scenes from the drop task. From 0639600c10a3a658fbc8509b708b16b197b36139 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 24 Mar 2026 23:13:13 +0900 Subject: [PATCH 11/31] perf(layout): skip Taffy for non-flex containers and optimize hot paths (30x speedup) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Key optimizations: - Skip building Taffy nodes for LayoutMode::Normal containers — children use schema positions directly. This avoids O(n) Taffy tree construction and flex computation for ~90% of nodes in Figma-imported scenes. - Measure text nodes on-the-fly in extract_all_layouts when they lack explicit height (auto-height text under non-flex containers). - Skip PathOp::Difference for non-overlapping fill when node opacity is 1.0 — overlap artifacts are invisible at full opacity. - Use pre-sized NodeIdHashMap in geometry cache, layout cache, layout tree, and effect tree to reduce rehashing. - Avoid allocating children Vec in effect tree for nodes that don't need render surfaces; iterate slices instead. - Remove unnecessary .clone() on geometry cache entries — copy bounding box before insert to avoid cloning the full entry. Benchmark on yrr-main.grida fixture: skip_layout=false: 25.8s → 844ms (30.5x faster) skip_layout=true: 938ms → 878ms (1.07x faster) Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/grida-canvas/src/cache/geometry.rs | 25 +++-- crates/grida-canvas/src/layout/cache.rs | 6 +- crates/grida-canvas/src/layout/engine.rs | 104 +++++++++++++++--- crates/grida-canvas/src/layout/tree.rs | 5 +- crates/grida-canvas/src/painter/layer.rs | 29 +++-- .../grida-canvas/src/runtime/effect_tree.rs | 48 ++++---- crates/grida-canvas/src/runtime/scene.rs | 23 +++- crates/grida-canvas/src/window/application.rs | 2 +- 8 files changed, 179 insertions(+), 63 deletions(-) diff --git a/crates/grida-canvas/src/cache/geometry.rs b/crates/grida-canvas/src/cache/geometry.rs index 1fc3b6962e..5eb55a2dec 100644 --- a/crates/grida-canvas/src/cache/geometry.rs +++ b/crates/grida-canvas/src/cache/geometry.rs @@ -85,7 +85,11 @@ impl GeometryCache { layout_result: Option<&crate::layout::cache::LayoutResult>, viewport_size: crate::node::schema::Size, ) -> Self { - let mut cache = Self::new(); + let mut cache = Self { + entries: crate::cache::fast_hash::new_node_id_map_with_capacity( + scene.graph.node_count(), + ), + }; let root_world = AffineTransform::identity(); let context = GeometryBuildContext { viewport_size }; @@ -134,7 +138,7 @@ impl GeometryCache { ) -> Rectangle { let node = graph .get_node(id) - .expect(&format!("node not found in geometry cache {id:?}")); + .expect("node not found in geometry cache"); match node { Node::Group(n) => { @@ -198,8 +202,9 @@ impl GeometryCache { dirty_bounds: false, }; - cache.entries.insert(id.clone(), entry.clone()); - entry.absolute_bounding_box + let bounds = entry.absolute_bounding_box; + cache.entries.insert(*id, entry); + bounds } Node::InitialContainer(_n) => { // ICB fills viewport - size from context @@ -249,7 +254,7 @@ impl GeometryCache { dirty_bounds: false, }; - cache.entries.insert(id.clone(), entry); + cache.entries.insert(*id, entry); union_world_bounds } Node::BooleanOperation(n) => { @@ -311,8 +316,9 @@ impl GeometryCache { dirty_bounds: false, }; - cache.entries.insert(id.clone(), entry.clone()); - entry.absolute_bounding_box + let bounds = entry.absolute_bounding_box; + cache.entries.insert(*id, entry); + bounds } Node::Container(n) => { // All containers use computed layout (roots have position corrected by LayoutEngine) @@ -548,8 +554,9 @@ impl GeometryCache { dirty_bounds: false, }; - cache.entries.insert(id.clone(), entry.clone()); - entry.absolute_bounding_box + let bounds = entry.absolute_bounding_box; + cache.entries.insert(*id, entry); + bounds } } } diff --git a/crates/grida-canvas/src/layout/cache.rs b/crates/grida-canvas/src/layout/cache.rs index a78709fedf..a6356c9d2e 100644 --- a/crates/grida-canvas/src/layout/cache.rs +++ b/crates/grida-canvas/src/layout/cache.rs @@ -1,6 +1,6 @@ +use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; use crate::layout::ComputedLayout; use crate::node::schema::NodeId; -use std::collections::HashMap; /// Immutable layout computation result /// @@ -8,13 +8,13 @@ use std::collections::HashMap; /// computation phase. Cached between frames for performance and change detection. #[derive(Debug, Clone, PartialEq)] pub struct LayoutResult { - layouts: HashMap, + layouts: NodeIdHashMap, } impl LayoutResult { pub fn new() -> Self { Self { - layouts: HashMap::new(), + layouts: new_node_id_map(), } } diff --git a/crates/grida-canvas/src/layout/engine.rs b/crates/grida-canvas/src/layout/engine.rs index 35343aa5cb..fea7e2f129 100644 --- a/crates/grida-canvas/src/layout/engine.rs +++ b/crates/grida-canvas/src/layout/engine.rs @@ -143,7 +143,9 @@ impl LayoutEngine { // (Group, BoolOp) get manual layout results from schema data. // Children of non-Taffy parents get schema-position correction // via parent-type check (no extra bookkeeping needed). - self.extract_all_layouts(root_id, graph); + // Text nodes not in Taffy are measured on-the-fly if they lack + // explicit height in the schema. + self.extract_all_layouts(root_id, graph, &mut text_measure); } &self.result @@ -287,6 +289,25 @@ impl LayoutEngine { !matches!(node, Node::Group(_) | Node::BooleanOperation(_)) } + /// Check if a Container uses flex layout for its children. + /// + /// `LayoutMode::Normal` containers don't flow their children — all children + /// are positioned via schema coordinates. Their children should be treated + /// as independent subtrees (like Group children) rather than Taffy children. + /// + /// This is the critical optimization for Figma imports where 90%+ of + /// containers use `LayoutMode::Normal`. Skipping their children avoids + /// building O(n) Taffy nodes and running the flex algorithm unnecessarily. + fn is_flex_container(node: &Node) -> bool { + match node { + Node::Container(n) => { + n.layout_container.layout_mode == crate::cg::types::LayoutMode::Flex + } + Node::InitialContainer(_) => true, + _ => false, + } + } + /// Recursively build Taffy tree for a node and its descendants. /// /// Virtual grouping nodes (Group, BooleanOperation) are skipped from the @@ -336,7 +357,20 @@ impl LayoutEngine { if let Some(children) = children { if !children.is_empty() { - // Build children recursively, filtering out those that shouldn't participate + // For non-flex containers (LayoutMode::Normal), children are + // positioned via schema coordinates, not flex layout. Create + // this container as a Taffy leaf — it only needs its own size + // for its parent's flex computation. Children will be handled + // by extract_all_layouts using schema positions directly. + // + // This is the critical optimization for Figma imports: most + // containers use Normal mode, so we skip building Taffy nodes + // for their entire subtrees (~90%+ of nodes). + if !Self::is_flex_container(node) { + return self.tree.new_leaf(*node_id, style).ok(); + } + + // Flex containers: build children as Taffy children let taffy_children: Vec = children .iter() .filter_map(|child_id| { @@ -377,25 +411,37 @@ impl LayoutEngine { /// We override root positions with their schema positions so multiple /// artboards/nodes can be positioned anywhere in the viewport. /// - /// **Virtual grouping nodes** (Group, BooleanOperation) are not in the - /// Taffy tree — they get manual layout results from their schema. - /// Their children become independent Taffy subtree roots (computed at 0,0), - /// so we detect this via parent-type check and apply schema positions. - /// No extra bookkeeping is needed; this handles arbitrary nesting depth. - fn extract_all_layouts(&mut self, id: &NodeId, graph: &SceneGraph) { + /// **Non-layout nodes** (Group, BooleanOperation) are not in the Taffy + /// tree — they get manual layout results from their schema. + /// **Non-flex containers** (LayoutMode::Normal) ARE in Taffy (as leaves + /// for their parent's flex computation) but their children are independent + /// subtrees with schema positions. + /// In both cases, children become independent Taffy subtree roots + /// (computed at 0,0), so we detect this via parent-type check and apply + /// schema positions. No extra bookkeeping is needed. + fn extract_all_layouts( + &mut self, + id: &NodeId, + graph: &SceneGraph, + text_measure: &mut Option>, + ) { if let Some(layout) = self.tree.get_layout(id) { let mut computed = ComputedLayout::from(layout); - // Taffy roots are computed at (0,0). Two cases need schema-position + // Taffy roots are computed at (0,0). Three cases need schema-position // correction: // 1. Graph roots — top-level nodes on the infinite canvas - // 2. Children of non-layout parents (Group/BoolOp) — these are - // independent Taffy subtree roots, also computed at (0,0) + // 2. Children of non-layout parents (Group/BoolOp) — independent + // Taffy subtree roots, also computed at (0,0) + // 3. Children of non-flex containers (LayoutMode::Normal) — these + // are also extra_roots with schema positions let needs_schema_position = graph.is_root(id) || graph .get_parent(id) .and_then(|pid| graph.get_node(&pid).ok()) - .is_some_and(|parent| !Self::is_layout_node(parent)); + .is_some_and(|parent| { + !Self::is_layout_node(parent) || !Self::is_flex_container(parent) + }); if needs_schema_position { if let Ok(node) = graph.get_node(id) { @@ -407,10 +453,38 @@ impl LayoutEngine { self.result.insert(*id, computed); } else { - // Node not in Taffy tree (virtual grouping node) — use schema + // Node not in Taffy tree — use schema positions/sizes. + // For text nodes with missing dimensions, measure on-the-fly. if let Ok(node) = graph.get_node(id) { let (x, y) = Self::get_schema_position(node); - let (width, height) = Self::get_schema_size(node); + let (mut width, mut height) = Self::get_schema_size(node); + + // Text nodes under non-flex containers need measurement for + // missing width/height. The schema typically stores width but + // not height for auto-height text. + if let Node::TextSpan(n) = node { + if n.width.is_none() || n.height.is_none() { + if let Some(ref mut provider) = text_measure { + let width_constraint = n.width; + let measurements = provider.paragraph_cache.measure( + &n.text, + &n.text_style, + &n.text_align, + &n.max_lines, + &n.ellipsis, + width_constraint, + provider.fonts, + Some(id), + ); + if n.width.is_none() { + width = measurements.max_width; + } + if n.height.is_none() { + height = measurements.height; + } + } + } + } self.result.insert( *id, @@ -427,7 +501,7 @@ impl LayoutEngine { // Recurse for children if let Some(children) = graph.get_children(id) { for child_id in children { - self.extract_all_layouts(child_id, graph); + self.extract_all_layouts(child_id, graph, text_measure); } } } diff --git a/crates/grida-canvas/src/layout/tree.rs b/crates/grida-canvas/src/layout/tree.rs index a79bb839c2..05f5dd3ba3 100644 --- a/crates/grida-canvas/src/layout/tree.rs +++ b/crates/grida-canvas/src/layout/tree.rs @@ -1,3 +1,4 @@ +use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; use crate::cache::paragraph::ParagraphCache; use crate::cg::types::{TextAlign, TextStyleRec}; use crate::node::schema::NodeId; @@ -38,7 +39,7 @@ pub(crate) struct LayoutTree { /// Taffy tree for layout computation taffy: TaffyTree, /// Map from our SceneGraph NodeId to Taffy's NodeId - scene_to_taffy: HashMap, + scene_to_taffy: NodeIdHashMap, /// Reverse map from Taffy NodeId to SceneGraph NodeId taffy_to_scene: HashMap, } @@ -47,7 +48,7 @@ impl LayoutTree { pub(crate) fn new() -> Self { Self { taffy: TaffyTree::new(), - scene_to_taffy: HashMap::new(), + scene_to_taffy: new_node_id_map(), taffy_to_scene: HashMap::new(), } } diff --git a/crates/grida-canvas/src/painter/layer.rs b/crates/grida-canvas/src/painter/layer.rs index 8f12933b97..b1ddd76798 100644 --- a/crates/grida-canvas/src/painter/layer.rs +++ b/crates/grida-canvas/src/painter/layer.rs @@ -459,8 +459,17 @@ impl LayerList { stroke_overlaps_fill: bool, fills: &Paints, strokes: &Paints, + node_opacity: f32, ) -> Option { - if !stroke_overlaps_fill || fills.is_empty() || strokes.is_empty() { + // The non-overlapping fill path is only needed when the node's own + // opacity < 1.0. At full opacity, stroke/fill overlap is invisible — + // no compositing artifact, no need for the expensive PathOp::Difference. + // Parent opacity is handled at the parent level via save_layer_alpha. + if node_opacity >= 1.0 + || !stroke_overlaps_fill + || fills.is_empty() + || strokes.is_empty() + { return None; } stroke_path.and_then(|sp| { @@ -560,7 +569,7 @@ impl LayerList { let strokes = Self::filter_visible_paints(&n.strokes); let stroke_overlaps_fill = !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( - &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, + &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, n.opacity, ); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { @@ -670,7 +679,7 @@ impl LayerList { let strokes = Self::filter_visible_paints(&n.strokes); let stroke_overlaps_fill = !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( - &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, + &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, n.opacity, ); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { @@ -732,7 +741,7 @@ impl LayerList { let strokes = Self::filter_visible_paints(&n.strokes); let stroke_overlaps_fill = !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( - &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, + &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, n.opacity, ); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { @@ -788,7 +797,7 @@ impl LayerList { let strokes = Self::filter_visible_paints(&n.strokes); let stroke_overlaps_fill = !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( - &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, + &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, n.opacity, ); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { @@ -844,7 +853,7 @@ impl LayerList { let strokes = Self::filter_visible_paints(&n.strokes); let stroke_overlaps_fill = !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( - &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, + &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, n.opacity, ); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { @@ -900,7 +909,7 @@ impl LayerList { let strokes = Self::filter_visible_paints(&n.strokes); let stroke_overlaps_fill = !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( - &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, + &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, n.opacity, ); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { @@ -956,7 +965,7 @@ impl LayerList { let strokes = Self::filter_visible_paints(&n.strokes); let stroke_overlaps_fill = !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( - &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, + &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, n.opacity, ); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { @@ -1148,7 +1157,7 @@ impl LayerList { let strokes = Self::filter_visible_paints(&n.strokes); let stroke_overlaps_fill = !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( - &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, + &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, n.opacity, ); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { @@ -1246,7 +1255,7 @@ impl LayerList { let strokes = Self::filter_visible_paints(&n.strokes); let stroke_overlaps_fill = !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( - &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, + &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, n.opacity, ); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { diff --git a/crates/grida-canvas/src/runtime/effect_tree.rs b/crates/grida-canvas/src/runtime/effect_tree.rs index 3ea05b2195..f0a6cc5335 100644 --- a/crates/grida-canvas/src/runtime/effect_tree.rs +++ b/crates/grida-canvas/src/runtime/effect_tree.rs @@ -39,11 +39,11 @@ //! } //! ``` +use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; use crate::cg::types::LayerBlendMode; use crate::node::id::NodeId; use crate::node::scene_graph::SceneGraph; use crate::node::schema::{Node, NodeTrait}; -use std::collections::HashMap; /// Why a node needs a render surface. /// @@ -110,7 +110,7 @@ impl EffectNode { pub struct EffectTree { /// Map from NodeId to its EffectNode data. /// Only contains nodes that need render surfaces. - nodes: HashMap, + nodes: NodeIdHashMap, /// Total number of render surfaces (== nodes.len()). surface_count: usize, /// Summary statistics for diagnostics. @@ -143,7 +143,7 @@ impl EffectTree { /// Create an empty effect tree (no render surfaces). pub fn empty() -> Self { Self { - nodes: HashMap::new(), + nodes: new_node_id_map(), surface_count: 0, stats: EffectTreeStats::default(), } @@ -155,7 +155,7 @@ impl EffectTree { /// surfaces. This is a full rebuild — no incremental state is carried /// over. pub fn build(graph: &SceneGraph) -> Self { - let mut nodes = HashMap::new(); + let mut nodes = new_node_id_map(); let mut stats = EffectTreeStats::default(); for root_id in graph.roots() { @@ -176,7 +176,7 @@ impl EffectTree { fn visit( graph: &SceneGraph, id: &NodeId, - nodes: &mut HashMap, + nodes: &mut NodeIdHashMap, stats: &mut EffectTreeStats, ) { stats.nodes_visited += 1; @@ -191,14 +191,11 @@ impl EffectTree { return; } - let all_children: Vec = graph - .get_children(id) - .cloned() - .unwrap_or_default(); + let all_children = graph.get_children(id); + let all_children_slice = all_children.map(|c| c.as_slice()).unwrap_or(&[]); - // Filter to only active children — inactive nodes should not - // trigger render surface reasons (mask, shadow promotion, etc.). - let children: Vec = all_children + // Count visible children and check for masks without allocating a Vec. + let visible_child_count = all_children_slice .iter() .filter(|cid| { graph @@ -206,13 +203,10 @@ impl EffectTree { .map(|n| n.active()) .unwrap_or(false) }) - .copied() - .collect(); - - let visible_child_count = children.len(); + .count(); // Collect render surface reasons for this node. - let reasons = Self::classify(node, visible_child_count, &children, graph); + let reasons = Self::classify(node, visible_child_count, all_children_slice, graph); if !reasons.is_empty() { // Update per-reason stats. @@ -227,12 +221,24 @@ impl EffectTree { } } + // Only allocate the children Vec for nodes that actually need a surface. + let active_children: Vec = all_children_slice + .iter() + .filter(|cid| { + graph + .get_node(cid) + .map(|n| n.active()) + .unwrap_or(false) + }) + .copied() + .collect(); + nodes.insert( *id, EffectNode { id: *id, reasons, - children: children.clone(), + children: active_children, visible_child_count, }, ); @@ -240,7 +246,7 @@ impl EffectTree { // Recurse into all children (including inactive ones, which will // early-return in visit) so the full tree is traversed. - for child_id in &all_children { + for child_id in all_children_slice { Self::visit(graph, child_id, nodes, stats); } } @@ -311,12 +317,12 @@ impl EffectTree { reasons } - /// Check if any of the given children are mask nodes. + /// Check if any of the given children are active mask nodes. fn has_mask_children(children: &[NodeId], graph: &SceneGraph) -> bool { children.iter().any(|cid| { graph .get_node(cid) - .map(|n| n.mask().is_some()) + .map(|n| n.active() && n.mask().is_some()) .unwrap_or(false) }) } diff --git a/crates/grida-canvas/src/runtime/scene.rs b/crates/grida-canvas/src/runtime/scene.rs index 990eabcdef..3fea2bae83 100644 --- a/crates/grida-canvas/src/runtime/scene.rs +++ b/crates/grida-canvas/src/runtime/scene.rs @@ -1532,10 +1532,29 @@ impl Renderer { Some(c) => c, None => return false, }; + // The pan cache image was captured at (origin_tx, origin_ty). + // If the camera has since moved (e.g. pan-only fast-path frames + // that don't recapture), we must offset the blit — otherwise the + // settle frame "reverts" to the old camera position. + let vm = self.camera.view_matrix(); + let dx = vm.matrix[0][2] - cache.origin_tx; + let dy = vm.matrix[1][2] - cache.origin_ty; + let surface = unsafe { &mut *self.backend.get_surface() }; let canvas = surface.canvas(); - // Draw the cached content snapshot at the same position (dx=0, dy=0). - canvas.draw_image(&cache.image, (0.0, 0.0), None); + if dx != 0.0 || dy != 0.0 { + // Offset blit — need to clear first (exposed edges). + if let Some(scene) = self.scene.as_ref() { + if let Some(bg) = scene.background_color { + canvas.clear(skia_safe::Color::from(bg)); + } else { + canvas.clear(skia_safe::Color::TRANSPARENT); + } + } else { + canvas.clear(skia_safe::Color::TRANSPARENT); + } + } + canvas.draw_image(&cache.image, (dx, dy), None); Self::gpu_flush(surface); true } diff --git a/crates/grida-canvas/src/window/application.rs b/crates/grida-canvas/src/window/application.rs index 9172e32efa..ceff84cbc7 100644 --- a/crates/grida-canvas/src/window/application.rs +++ b/crates/grida-canvas/src/window/application.rs @@ -1152,7 +1152,7 @@ impl UnknownTargetApplication { // to the previous frame. Restore it from the pan image cache and // skip the expensive frame-plan build + full draw. The overlay is // still re-drawn below so marquee/selection visuals update correctly. - if !content_changed && self.renderer.blit_content_cache() { + if !stable && !content_changed && self.renderer.blit_content_cache() { // Consume the camera change (no-op here, but keeps the contract). self.renderer.camera.consume_change(); From 9054d36ff760b35d4fc365d9afd28bd49a0ee5fd Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 24 Mar 2026 23:23:09 +0900 Subject: [PATCH 12/31] clean --- crates/grida-canvas/src/resources/mod.rs | 3 +++ crates/grida-canvas/tests/compositor_effects.rs | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/grida-canvas/src/resources/mod.rs b/crates/grida-canvas/src/resources/mod.rs index 0de99cd1ad..d4cef7f031 100644 --- a/crates/grida-canvas/src/resources/mod.rs +++ b/crates/grida-canvas/src/resources/mod.rs @@ -119,6 +119,7 @@ pub struct FontMessage { } /// Collect the URL string from a [`ResourceRef`]. +#[allow(dead_code)] fn resource_ref_url(r: &ResourceRef) -> &str { match r { ResourceRef::RID(s) | ResourceRef::HASH(s) => s, @@ -126,6 +127,7 @@ fn resource_ref_url(r: &ResourceRef) -> &str { } /// Push any image URLs found in a paint slice. +#[allow(dead_code)] fn collect_image_urls_from_paints(paints: &[Paint], out: &mut Vec) { for paint in paints { if let Paint::Image(img) = paint { @@ -141,6 +143,7 @@ fn collect_image_urls_from_paints(paints: &[Paint], out: &mut Vec) { /// strokes, and dedicated image references. // TODO: consider a dedicated paints store or iterator so this doesn't need // to match every node variant individually. +#[allow(dead_code)] fn extract_image_urls(scene: &Scene) -> Vec { use crate::node::schema::Node; let mut urls = Vec::new(); diff --git a/crates/grida-canvas/tests/compositor_effects.rs b/crates/grida-canvas/tests/compositor_effects.rs index 871300eee4..9840f5ff3a 100644 --- a/crates/grida-canvas/tests/compositor_effects.rs +++ b/crates/grida-canvas/tests/compositor_effects.rs @@ -20,7 +20,6 @@ use cg::runtime::image_repository::ImageRepository; use cg::runtime::render_policy::RenderPolicy; use math2::rect::Rectangle; use skia_safe::{surfaces, Paint as SkPaint, Rect, Surface}; -use std::collections::HashMap; use std::rc::Rc; use std::sync::{Arc, Mutex}; From 255c874d7c576ad693f4b2145f83b84eb31f9110 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 25 Mar 2026 00:09:12 +0900 Subject: [PATCH 13/31] feat(emscripten): add new external functions for timing and main loop control Introduced several new external functions for Emscripten, including `emscripten_get_now`, `emscripten_random`, and `emscripten_get_device_pixel_ratio`. Added support for main loop management with functions like `emscripten_set_main_loop`, `emscripten_pause_main_loop`, and `emscripten_resume_main_loop`. This enhancement improves the integration with Emscripten's capabilities for timing and animation control. --- crates/grida-canvas/src/os/emscripten.rs | 53 ++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/crates/grida-canvas/src/os/emscripten.rs b/crates/grida-canvas/src/os/emscripten.rs index 8caba22b6e..dbf4564c55 100644 --- a/crates/grida-canvas/src/os/emscripten.rs +++ b/crates/grida-canvas/src/os/emscripten.rs @@ -1,4 +1,5 @@ #![cfg(target_os = "emscripten")] +#![allow(non_camel_case_types)] // // emscripten bindings @@ -6,6 +7,9 @@ // - https://github.com/ALEX11BR/emscripten-functions/tree/main/emscripten-functions-sys // +pub type em_callback_func = ::std::option::Option; +pub type em_arg_callback_func = + ::std::option::Option; unsafe extern "C" { pub fn emscripten_GetProcAddress( name: *const ::std::os::raw::c_char, @@ -25,6 +29,18 @@ unsafe extern "C" { pub fn emscripten_cancel_animation_frame(request_animation_frame_id: ::std::os::raw::c_int); } +extern "C" { + pub fn emscripten_get_now() -> f64; +} + +extern "C" { + pub fn emscripten_random() -> f32; +} + +extern "C" { + pub fn emscripten_get_device_pixel_ratio() -> f64; +} + unsafe extern "C" { pub fn emscripten_request_animation_frame_loop( cb: ::std::option::Option< @@ -33,3 +49,40 @@ unsafe extern "C" { user_data: *mut ::std::os::raw::c_void, ); } + +extern "C" { + pub fn emscripten_set_main_loop( + func: em_callback_func, + fps: ::std::os::raw::c_int, + simulate_infinite_loop: bool, + ); +} +extern "C" { + pub fn emscripten_set_main_loop_timing( + mode: ::std::os::raw::c_int, + value: ::std::os::raw::c_int, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn emscripten_get_main_loop_timing( + mode: *mut ::std::os::raw::c_int, + value: *mut ::std::os::raw::c_int, + ); +} +extern "C" { + pub fn emscripten_set_main_loop_arg( + func: em_arg_callback_func, + arg: *mut ::std::os::raw::c_void, + fps: ::std::os::raw::c_int, + simulate_infinite_loop: bool, + ); +} +extern "C" { + pub fn emscripten_pause_main_loop(); +} +extern "C" { + pub fn emscripten_resume_main_loop(); +} +extern "C" { + pub fn emscripten_cancel_main_loop(); +} From 8aebabcdc408c7d9bc88de0bd46d4fd87a56123d Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 25 Mar 2026 01:40:56 +0900 Subject: [PATCH 14/31] feat(perf): enhance WASM performance with DenseNodeMap and timing improvements - Introduced `DenseNodeMap` for efficient Vec-indexed storage, replacing `HashMap` in critical performance paths, particularly for WASM. - Updated various caches and data structures (e.g., `GeometryCache`, `ParagraphCache`, `SceneGraph`) to utilize `DenseNodeMap`, improving access speed and cache locality. - Added detailed performance measurement capabilities in the `load_scene` pipeline, allowing for better benchmarking and optimization insights. - Documented known WASM-specific performance issues and provided strategies for mitigation. This commit significantly optimizes performance for WASM targets, addressing overhead issues associated with `HashMap` and enhancing overall rendering efficiency. --- .agents/skills/cg-perf/SKILL.md | 74 ++++ .../lib/__test__/bench-load-scene.test.ts | 151 ++++++++ .../lib/modules/canvas-bindings.d.ts | 1 + .../grida-canvas-wasm/lib/modules/canvas.ts | 13 + crates/grida-canvas-wasm/lib/modules/ffi.ts | 2 + .../grida-canvas-wasm/src/wasm_application.rs | 16 + crates/grida-canvas/Cargo.toml | 1 + crates/grida-canvas/examples/tool_io_grida.rs | 14 +- crates/grida-canvas/src/cache/fast_hash.rs | 201 +++++++++++ crates/grida-canvas/src/cache/geometry.rs | 319 ++++++----------- crates/grida-canvas/src/cache/paragraph.rs | 47 ++- crates/grida-canvas/src/cache/scene.rs | 25 +- crates/grida-canvas/src/layout/cache.rs | 10 +- crates/grida-canvas/src/layout/tree.rs | 59 +-- crates/grida-canvas/src/node/repository.rs | 23 +- crates/grida-canvas/src/node/scene_graph.rs | 28 +- crates/grida-canvas/src/resources/mod.rs | 2 +- crates/grida-canvas/src/runtime/scene.rs | 27 ++ crates/grida-canvas/src/sys/mod.rs | 15 + crates/grida-canvas/src/window/application.rs | 11 +- .../src/window/application_emscripten.rs | 4 + crates/grida-dev/Cargo.toml | 1 + crates/grida-dev/src/bench/load_bench.rs | 12 +- docs/wg/feat-2d/wasm-benchmarking.md | 336 ++++++++++++++++++ .../feat-2d/wasm-load-scene-optimization.md | 155 ++++++++ 25 files changed, 1243 insertions(+), 304 deletions(-) create mode 100644 crates/grida-canvas-wasm/lib/__test__/bench-load-scene.test.ts create mode 100644 docs/wg/feat-2d/wasm-benchmarking.md create mode 100644 docs/wg/feat-2d/wasm-load-scene-optimization.md diff --git a/.agents/skills/cg-perf/SKILL.md b/.agents/skills/cg-perf/SKILL.md index d1c7719897..c5c791d540 100644 --- a/.agents/skills/cg-perf/SKILL.md +++ b/.agents/skills/cg-perf/SKILL.md @@ -495,3 +495,77 @@ absolute-positioned documents. thousands of cheap entries, the timing checks themselves can become significant. Use `elapsed()` checks at reasonable intervals, not every iteration. + +### `Instant::now()` is broken on emscripten + +Under emscripten, `Instant::now()` is effectively constant, so durations +collapse to zero. Use `crate::sys::perf_now()` for timing: it maps to +`emscripten_get_now()` (`performance.now()`) on WASM and `Instant` on native. + +### WASM/native ratios are stage-dependent + +WASM overhead is not a single multiplier. Roughly: simple compute is ~2-3x, +HashMap-heavy traversals can be 10-35x, and after Vec-indexing hot paths, +data-structure-bound stages drop to ~1-2x while compute-heavy stages stay +~5-15x+. Measure per stage. + +### Data structures matter much more in WASM + +Large `HashMap`s (100K+ entries) may be fine on native but can be extremely +slow in WASM due to linear memory and weaker cache behavior. Prefer dense +Vec-indexed storage (`DenseNodeMap`) for hot paths. See `cache/fast_hash.rs`. + +### Native profiles can mis-rank WASM bottlenecks + +Native profiling finds stage costs, but not WASM amplification. Example: +native highlighted layers, while WASM was dominated by geometry because +per-node `HashMap` costs were amplified. Confirm priorities with WASM data. + +--- + +## WASM Performance + +WASM is the primary shipping target. Native benchmarks show the algorithmic +ceiling; WASM benchmarks show delivered performance. + +See `docs/wg/feat-2d/wasm-benchmarking.md` for the full strategy and +lessons learned. Key points: + +### Measurement inside WASM + +`load_scene` emits per-stage timing via `eprintln!` + `sys::perf_now()`. +Read the `[load_scene]` line in browser console (stderr) for +fonts/layout/geometry/effects/layers. This is the primary `load_scene` +WASM measurement path today. + +### Three-layer benchmarking model + +1. **Native** (`load-bench`, Criterion): algorithmic ceiling + profiling +2. **WASM-on-Node**: real WASM in headless/CI — **implemented** +3. **Browser**: full pipeline (JS encode + WASM load + GPU render) + +WASM-on-Node benchmark: + +```sh +# Build WASM first +just --justfile crates/grida-canvas-wasm/justfile build + +# Run benchmark (requires fixtures/local/perf/local/yrr-main.grida for 136k test) +cd crates/grida-canvas-wasm && npx vitest run __test__/bench-load-scene.test.ts +``` + +WASM-on-Node results closely match browser WASM timings, confirming it as +a valid benchmarking layer for compute-heavy stages. + +### Known WASM-specific issues + +- **GPU-only paths** can fail only on WASM (native runs CPU backend). + `blit_content_cache` and overlay-only fast path both had WASM-only bugs. +- **Large enum access** is the dominant WASM bottleneck. The `Node` enum + (15 variants, each hundreds of bytes) causes cache-unfriendly memory access + that WASM amplifies to 30×+ native cost. Fix: Struct-of-Arrays (SoA) — + see `docs/wg/feat-2d/wasm-load-scene-optimization.md`. +- **Deep recursion** (`build_recursive`, `flatten_node`) is costlier in WASM + due to stack-frame overhead in linear memory. +- **JS↔WASM boundary** is small for bulk calls (`switch_scene`), but JS-side + FlatBuffers encoding is still ~10% of pipeline cost. diff --git a/crates/grida-canvas-wasm/lib/__test__/bench-load-scene.test.ts b/crates/grida-canvas-wasm/lib/__test__/bench-load-scene.test.ts new file mode 100644 index 0000000000..3ca202eca1 --- /dev/null +++ b/crates/grida-canvas-wasm/lib/__test__/bench-load-scene.test.ts @@ -0,0 +1,151 @@ +// @vitest-environment node +// +// WASM-on-Node benchmark for load_scene pipeline. +// +// Measures real WASM execution of the scene loading stages: +// 1. loadSceneGrida — FBS decode + SceneGraph construction +// 2. switchScene — layout + geometry + effects + layers +// +// When `perf` feature is enabled on the cg crate, the Rust side emits +// per-stage timing via eprintln! ([load_scene] line). +// This test measures JS-side wall time for comparison. +// +// Usage: +// pnpm test bench-load-scene +// pnpm vitest run bench-load-scene --reporter=verbose +// +// To benchmark a .grida file, place it in: +// lib/__test__/fixtures/local/ +// All .grida files in that directory will be auto-discovered. + +import { readFileSync, existsSync, readdirSync } from "node:fs"; +import { resolve } from "node:path"; +import { beforeAll, describe, expect, it } from "vitest"; +import { Scene } from "../modules/canvas"; + +/** Directory for local (gitignored) benchmark fixtures. */ +const LOCAL_FIXTURES_DIR = resolve(__dirname, "fixtures/local"); + +let module: any; + +beforeAll(async () => { + const pkg = require("../../dist/index.js") as { + default: (opts?: unknown) => Promise; + }; + const factory = await pkg.default(); + module = factory.module; +}, 30_000); + +function createRasterScene(width = 1000, height = 1000): Scene { + const appptr = module._init_with_backend( + 1, // BACKEND_ID.Raster + width, + height, + 1, // useEmbeddedFonts = true + 0 // configFlags + ); + return new Scene(module, appptr); +} + +/** + * Discover .grida files from the local fixtures directory. + */ +function discoverGridaFixtures(): { name: string; path: string }[] { + if (!existsSync(LOCAL_FIXTURES_DIR)) { + return []; + } + return readdirSync(LOCAL_FIXTURES_DIR) + .filter((f) => f.endsWith(".grida")) + .sort() + .map((f) => ({ name: f, path: resolve(LOCAL_FIXTURES_DIR, f) })); +} + +describe("bench: load_scene (WASM-on-Node)", () => { + it("grida1 JSON (rectangle)", async () => { + const scene = createRasterScene(); + const doc = readFileSync( + resolve(process.cwd(), "example/rectangle.grida1"), + "utf8" + ); + + const t0 = performance.now(); + scene.loadScene(doc); + const elapsed = performance.now() - t0; + + console.log(`[wasm-bench] rectangle.grida1: ${elapsed.toFixed(0)}ms`); + expect(elapsed).toBeLessThan(5_000); + scene.dispose(); + }); + + it("synthetic 100x100 grid (10k nodes)", async () => { + const scene = createRasterScene(); + + const t0 = performance.now(); + scene.loadBenchmarkScene(100, 100); + const elapsed = performance.now() - t0; + + console.log( + `[wasm-bench] synthetic 100x100: ${elapsed.toFixed(0)}ms (10k nodes)` + ); + expect(elapsed).toBeLessThan(30_000); + scene.dispose(); + }, 60_000); + + it("synthetic 200x200 grid (40k nodes)", async () => { + const scene = createRasterScene(); + + const t0 = performance.now(); + scene.loadBenchmarkScene(200, 200); + const elapsed = performance.now() - t0; + + console.log( + `[wasm-bench] synthetic 200x200: ${elapsed.toFixed(0)}ms (40k nodes)` + ); + expect(elapsed).toBeLessThan(60_000); + scene.dispose(); + }, 120_000); + + // Auto-discovered .grida fixtures from fixtures/local/ + const fixtures = discoverGridaFixtures(); + + for (const fx of fixtures) { + it(`grida binary: ${fx.name}`, async () => { + const data = new Uint8Array(readFileSync(fx.path)); + const scene = createRasterScene(); + + // Phase 1: FBS decode + const t0 = performance.now(); + scene.loadSceneGrida(data); + const tLoad = performance.now(); + + // Phase 2: switch to the first scene + const sceneIds = scene.loadedSceneIds(); + expect(sceneIds.length).toBeGreaterThan(0); + const firstSceneId = sceneIds[0]; + + scene.switchScene(firstSceneId); + const tSwitch = performance.now(); + + const loadMs = tLoad - t0; + const switchMs = tSwitch - tLoad; + const totalMs = tSwitch - t0; + + console.log( + `[wasm-bench] ${fx.name} (scene=${firstSceneId}): ` + + `load=${loadMs.toFixed(0)}ms switch=${switchMs.toFixed(0)}ms total=${totalMs.toFixed(0)}ms` + ); + + expect(totalMs).toBeLessThan(120_000); + scene.dispose(); + }, 120_000); + } + + if (fixtures.length === 0) { + it("no .grida fixtures found (skipped)", () => { + console.log( + "[wasm-bench] No .grida fixtures in lib/__test__/fixtures/local/. " + + "Place .grida files there to benchmark real scenes." + ); + }); + } +}); diff --git a/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts b/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts index 03cea3272d..1023d08417 100644 --- a/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts +++ b/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts @@ -51,6 +51,7 @@ declare namespace canvas { ptr: number, len: number ): void; + _loaded_scene_ids(state: GridaCanvasApplicationPtr): Ptr; _drain_missing_images(state: GridaCanvasApplicationPtr): Ptr; _resolve_image( state: GridaCanvasApplicationPtr, diff --git a/crates/grida-canvas-wasm/lib/modules/canvas.ts b/crates/grida-canvas-wasm/lib/modules/canvas.ts index 38a625b71a..2a66266076 100644 --- a/crates/grida-canvas-wasm/lib/modules/canvas.ts +++ b/crates/grida-canvas-wasm/lib/modules/canvas.ts @@ -195,6 +195,19 @@ export class Scene { this._free_string(ptr, len); } + /** + * Return the IDs of all scenes decoded by the last `loadSceneGrida` call. + */ + loadedSceneIds(): string[] { + this._assertAlive(); + const outptr = this.module._loaded_scene_ids(this.appptr); + if (outptr === 0) { + return []; + } + const str = ffi.readLenPrefixedString(this.module, outptr); + return JSON.parse(str) as string[]; + } + /** * Returns image refs that were needed during the last render but not found. * Only returns refs not yet reported in a previous call. diff --git a/crates/grida-canvas-wasm/lib/modules/ffi.ts b/crates/grida-canvas-wasm/lib/modules/ffi.ts index 987a188db4..b977d75d84 100644 --- a/crates/grida-canvas-wasm/lib/modules/ffi.ts +++ b/crates/grida-canvas-wasm/lib/modules/ffi.ts @@ -29,6 +29,8 @@ export namespace ffi { ): [ptr: number, len: number] { const len = bytes.length; const ptr = module._allocate(len); + // Re-read HEAPU8 after _allocate — if WASM memory grew during + // allocation, the old Uint8Array view is detached. module.HEAPU8.set(bytes, ptr); return [ptr, len]; } diff --git a/crates/grida-canvas-wasm/src/wasm_application.rs b/crates/grida-canvas-wasm/src/wasm_application.rs index 3b1a73fd6e..f5dd969f86 100644 --- a/crates/grida-canvas-wasm/src/wasm_application.rs +++ b/crates/grida-canvas-wasm/src/wasm_application.rs @@ -180,6 +180,22 @@ pub unsafe extern "C" fn switch_scene( } } +#[no_mangle] +/// js::_loaded_scene_ids +/// Returns a len-prefixed JSON array of scene ID strings, or null if empty. +pub unsafe extern "C" fn loaded_scene_ids(app: *mut UnknownTargetApplication) -> *const u8 { + if let Some(app) = app.as_ref() { + let ids = app.loaded_scene_ids(); + if ids.is_empty() { + return std::ptr::null(); + } + if let Ok(json) = serde_json::to_string(&ids) { + return alloc_len_prefixed(json.as_bytes()); + } + } + std::ptr::null() +} + #[no_mangle] /// js::_drain_missing_images /// Returns a len-prefixed JSON array of missing image ref strings, or null if empty. diff --git a/crates/grida-canvas/Cargo.toml b/crates/grida-canvas/Cargo.toml index 732ccf6d7b..9066a3b623 100644 --- a/crates/grida-canvas/Cargo.toml +++ b/crates/grida-canvas/Cargo.toml @@ -63,6 +63,7 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } [features] default = [] web = [] +perf = [] native-clock-tick = [] native-gl-context = ["dep:glutin", "dep:raw-window-handle"] diff --git a/crates/grida-canvas/examples/tool_io_grida.rs b/crates/grida-canvas/examples/tool_io_grida.rs index f2dc30aeec..7afe666f49 100644 --- a/crates/grida-canvas/examples/tool_io_grida.rs +++ b/crates/grida-canvas/examples/tool_io_grida.rs @@ -296,7 +296,7 @@ fn print_scene_stats(scene: &Scene, id_map: &HashMap, verbose: b // Node type breakdown let mut type_counts: BTreeMap<&str, usize> = BTreeMap::new(); for (node_id, node) in graph.nodes_iter() { - if !reachable.contains(node_id) { + if !reachable.contains(&node_id) { continue; } let label = classify_node(node); @@ -371,19 +371,19 @@ fn run_layout_check(scene: &Scene, id_map: &HashMap) -> bool { let mut missing: Vec<(NodeId, String, Vec)> = Vec::new(); for (node_id, node) in graph.nodes_iter() { - if !reachable.contains(node_id) { + if !reachable.contains(&node_id) { continue; } if let Node::Container(_) = node { - if layout_result.get(node_id).is_none() { + if layout_result.get(&node_id).is_none() { let string_id = id_map - .get(node_id) + .get(&node_id) .cloned() .unwrap_or_else(|| format!("{:?}", node_id)); - let mut ancestor_ids: Vec = graph.ancestors(node_id).unwrap_or_default(); + let mut ancestor_ids: Vec = graph.ancestors(&node_id).unwrap_or_default(); ancestor_ids.reverse(); - ancestor_ids.push(*node_id); + ancestor_ids.push(node_id); let path: Vec = ancestor_ids .iter() .map(|id| { @@ -394,7 +394,7 @@ fn run_layout_check(scene: &Scene, id_map: &HashMap) -> bool { }) .collect(); - missing.push((*node_id, string_id, path)); + missing.push((node_id, string_id, path)); } } } diff --git a/crates/grida-canvas/src/cache/fast_hash.rs b/crates/grida-canvas/src/cache/fast_hash.rs index 4a61aa79cf..d68c1061c1 100644 --- a/crates/grida-canvas/src/cache/fast_hash.rs +++ b/crates/grida-canvas/src/cache/fast_hash.rs @@ -74,3 +74,204 @@ pub fn new_node_id_map() -> NodeIdHashMap { pub fn new_node_id_map_with_capacity(capacity: usize) -> NodeIdHashMap { HashMap::with_capacity_and_hasher(capacity, NodeIdBuildHasher) } + +// --------------------------------------------------------------------------- +// DenseNodeMap — Vec-indexed storage for sequential NodeId keys +// --------------------------------------------------------------------------- + +use crate::node::id::NodeId; + +/// A dense, Vec-backed map keyed by `NodeId` (sequential u64 starting at 0). +/// +/// Replaces `HashMap` in hot paths where NodeIds are generated by +/// a counter. Provides O(1) access with zero hashing and excellent cache +/// locality — critical for WASM where HashMap overhead is 10–30× native. +#[derive(Debug, Clone)] +pub struct DenseNodeMap { + slots: Vec>, + len: usize, +} + +impl DenseNodeMap { + #[inline] + pub fn new() -> Self { + Self { + slots: Vec::new(), + len: 0, + } + } + + #[inline] + pub fn with_capacity(capacity: usize) -> Self { + let mut slots = Vec::with_capacity(capacity); + slots.resize_with(capacity, || None); + Self { slots, len: 0 } + } + + #[inline] + pub fn insert(&mut self, id: NodeId, value: V) -> Option { + let idx = id as usize; + if idx >= self.slots.len() { + self.slots.resize_with(idx + 1, || None); + } + let old = self.slots[idx].take(); + self.slots[idx] = Some(value); + if old.is_none() { + self.len += 1; + } + old + } + + #[inline] + pub fn get(&self, id: &NodeId) -> Option<&V> { + self.slots.get(*id as usize).and_then(|s| s.as_ref()) + } + + #[inline] + pub fn get_mut(&mut self, id: &NodeId) -> Option<&mut V> { + self.slots.get_mut(*id as usize).and_then(|s| s.as_mut()) + } + + #[inline] + pub fn remove(&mut self, id: &NodeId) -> Option { + let idx = *id as usize; + if idx < self.slots.len() { + let old = self.slots[idx].take(); + if old.is_some() { + self.len -= 1; + } + old + } else { + None + } + } + + #[inline] + pub fn contains_key(&self, id: &NodeId) -> bool { + self.get(id).is_some() + } + + #[inline] + pub fn len(&self) -> usize { + self.len + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + pub fn clear(&mut self) { + for slot in self.slots.iter_mut() { + *slot = None; + } + self.len = 0; + } + + /// Pre-allocate storage so that IDs up to `capacity - 1` can be inserted + /// without reallocation. + pub fn reserve(&mut self, capacity: usize) { + if capacity > self.slots.len() { + self.slots.resize_with(capacity, || None); + } + } + + /// Iterate over occupied entries as `(NodeId, &V)`. + pub fn iter(&self) -> DenseNodeMapIter<'_, V> { + DenseNodeMapIter { + inner: self.slots.iter().enumerate(), + } + } + + /// Iterate over occupied entries as `(NodeId, &mut V)`. + pub fn iter_mut(&mut self) -> DenseNodeMapIterMut<'_, V> { + DenseNodeMapIterMut { + inner: self.slots.iter_mut().enumerate(), + } + } + + /// Retain only entries satisfying a predicate. + pub fn retain bool>(&mut self, mut f: F) { + for (idx, slot) in self.slots.iter_mut().enumerate() { + if let Some(ref mut v) = slot { + if !f(idx as NodeId, v) { + *slot = None; + self.len -= 1; + } + } + } + } + + /// Get the allocated capacity (max NodeId + 1 that fits without realloc). + #[inline] + pub fn capacity(&self) -> usize { + self.slots.len() + } +} + +impl Default for DenseNodeMap { + fn default() -> Self { + Self::new() + } +} + +/// Iterator over `(NodeId, &V)` pairs in a `DenseNodeMap`. +pub struct DenseNodeMapIter<'a, V> { + inner: std::iter::Enumerate>>, +} + +impl<'a, V> Iterator for DenseNodeMapIter<'a, V> { + type Item = (NodeId, &'a V); + + #[inline] + fn next(&mut self) -> Option { + loop { + match self.inner.next() { + Some((idx, Some(v))) => return Some((idx as NodeId, v)), + Some((_, None)) => continue, + None => return None, + } + } + } +} + +/// Iterator over `(NodeId, &mut V)` pairs in a `DenseNodeMap`. +pub struct DenseNodeMapIterMut<'a, V> { + inner: std::iter::Enumerate>>, +} + +impl<'a, V> Iterator for DenseNodeMapIterMut<'a, V> { + type Item = (NodeId, &'a mut V); + + #[inline] + fn next(&mut self) -> Option { + loop { + match self.inner.next() { + Some((idx, Some(v))) => return Some((idx as NodeId, v)), + Some((_, None)) => continue, + None => return None, + } + } + } +} + +impl<'a, V> IntoIterator for &'a DenseNodeMap { + type Item = (NodeId, &'a V); + type IntoIter = DenseNodeMapIter<'a, V>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl FromIterator<(NodeId, V)> for DenseNodeMap { + fn from_iter>(iter: T) -> Self { + let iter = iter.into_iter(); + let (lower, _) = iter.size_hint(); + let mut map = Self::with_capacity(lower); + for (id, v) in iter { + map.insert(id, v); + } + map + } +} diff --git a/crates/grida-canvas/src/cache/geometry.rs b/crates/grida-canvas/src/cache/geometry.rs index 5eb55a2dec..b87e63d9d3 100644 --- a/crates/grida-canvas/src/cache/geometry.rs +++ b/crates/grida-canvas/src/cache/geometry.rs @@ -18,7 +18,7 @@ use crate::runtime::font_repository::FontRepository; use math2::rect; use math2::rect::Rectangle; use math2::transform::AffineTransform; -use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; +use crate::cache::fast_hash::DenseNodeMap; /// Geometry data used for layout, culling, and rendering. /// @@ -46,19 +46,52 @@ pub struct GeometryEntry { } /// Context passed during geometry building -struct GeometryBuildContext { +/// Bundles all immutable + mutable state for geometry build, reducing +/// recursive call parameter count from 9 to 4 (significant for WASM +/// where each function parameter adds call overhead). +struct GeometryBuildContext<'a> { + graph: &'a SceneGraph, + paragraph_cache: &'a mut ParagraphCache, + fonts: &'a FontRepository, + layout_result: Option<&'a crate::layout::cache::LayoutResult>, viewport_size: crate::node::schema::Size, + /// Pre-computed: which nodes are layout containers (Container or ICB). + is_layout_container: DenseNodeMap, +} + +impl<'a> GeometryBuildContext<'a> { + fn new( + graph: &'a SceneGraph, + paragraph_cache: &'a mut ParagraphCache, + fonts: &'a FontRepository, + layout_result: Option<&'a crate::layout::cache::LayoutResult>, + viewport_size: crate::node::schema::Size, + ) -> Self { + let mut is_layout_container = DenseNodeMap::with_capacity(graph.node_count()); + for (id, node) in graph.nodes_iter() { + let is_container = matches!(node, Node::Container(_) | Node::InitialContainer(_)); + is_layout_container.insert(id, is_container); + } + Self { + graph, + paragraph_cache, + fonts, + layout_result, + viewport_size, + is_layout_container, + } + } } #[derive(Debug, Clone)] pub struct GeometryCache { - entries: NodeIdHashMap, + entries: DenseNodeMap, } impl GeometryCache { pub fn new() -> Self { Self { - entries: new_node_id_map(), + entries: DenseNodeMap::new(), } } @@ -86,57 +119,31 @@ impl GeometryCache { viewport_size: crate::node::schema::Size, ) -> Self { let mut cache = Self { - entries: crate::cache::fast_hash::new_node_id_map_with_capacity( - scene.graph.node_count(), - ), + entries: DenseNodeMap::with_capacity(scene.graph.node_count()), }; let root_world = AffineTransform::identity(); - let context = GeometryBuildContext { viewport_size }; + let mut ctx = GeometryBuildContext::new( + &scene.graph, + paragraph_cache, + fonts, + layout_result, + viewport_size, + ); for child in scene.graph.roots() { - Self::build_recursive( - &child, - &scene.graph, - &root_world, - None, - &mut cache, - paragraph_cache, - fonts, - layout_result, - &context, - ); + Self::build_recursive(&child, &root_world, None, &mut cache, &mut ctx); } cache } - /// Check if a node's parent is a layout container (Container or ICB). - /// Only layout containers provide meaningful layout results; Group and - /// BooleanOperation parents produce synthetic fallbacks. - fn is_layout_container_parent( - parent_id: &Option, - graph: &SceneGraph, - ) -> bool { - parent_id - .as_ref() - .and_then(|pid| graph.get_node(pid).ok()) - .map(|parent_node| { - matches!(parent_node, Node::Container(_) | Node::InitialContainer(_)) - }) - .unwrap_or(false) - } - fn build_recursive( id: &NodeId, - graph: &SceneGraph, parent_world: &AffineTransform, parent_id: Option, cache: &mut GeometryCache, - paragraph_cache: &mut ParagraphCache, - fonts: &FontRepository, - layout_result: Option<&crate::layout::cache::LayoutResult>, - context: &GeometryBuildContext, + ctx: &mut GeometryBuildContext, ) -> Rectangle { - let node = graph + let node = ctx.graph .get_node(id) .expect("node not found in geometry cache"); @@ -145,18 +152,10 @@ impl GeometryCache { let world_transform = parent_world.compose(&n.transform.unwrap_or_default()); let mut union_bounds: Option = None; let mut union_render_bounds: Option = None; - if let Some(children) = graph.get_children(id) { + if let Some(children) = ctx.graph.get_children(id) { for child_id in children { let child_bounds = Self::build_recursive( - child_id, - graph, - &world_transform, - Some(id.clone()), - cache, - paragraph_cache, - fonts, - layout_result, - context, + child_id, &world_transform, Some(*id), cache, ctx, ); union_bounds = match union_bounds { Some(b) => Some(rect::union(&[b, child_bounds])), @@ -172,21 +171,13 @@ impl GeometryCache { } let world_bounds = union_bounds.unwrap_or_else(|| Rectangle { - x: 0.0, - y: 0.0, - width: 0.0, - height: 0.0, + x: 0.0, y: 0.0, width: 0.0, height: 0.0, }); let local_bounds = if let Some(inv) = world_transform.inverse() { transform_rect(&world_bounds, &inv) } else { - Rectangle { - x: 0.0, - y: 0.0, - width: 0.0, - height: 0.0, - } + Rectangle { x: 0.0, y: 0.0, width: 0.0, height: 0.0 } }; let render_bounds = union_render_bounds.unwrap_or(world_bounds); @@ -197,7 +188,7 @@ impl GeometryCache { bounding_box: local_bounds, absolute_bounding_box: world_bounds, absolute_render_bounds: render_bounds, - parent: parent_id.clone(), + parent: parent_id, dirty_transform: false, dirty_bounds: false, }; @@ -207,48 +198,31 @@ impl GeometryCache { bounds } Node::InitialContainer(_n) => { - // ICB fills viewport - size from context - // Layout was already computed by LayoutEngine - let size = context.viewport_size; - + let size = ctx.viewport_size; let local_transform = AffineTransform::identity(); let world_transform = parent_world.compose(&local_transform); let local_bounds = Rectangle { - x: 0.0, - y: 0.0, - width: size.width, - height: size.height, + x: 0.0, y: 0.0, width: size.width, height: size.height, }; - // Build children geometries (may use computed layouts from LayoutEngine) let mut union_world_bounds = transform_rect(&local_bounds, &world_transform); - if let Some(children) = graph.get_children(id) { + if let Some(children) = ctx.graph.get_children(id) { for child_id in children { let child_bounds = Self::build_recursive( - child_id, - graph, - &world_transform, - Some(id.clone()), - cache, - paragraph_cache, - fonts, - layout_result, - context, + child_id, &world_transform, Some(*id), cache, ctx, ); union_world_bounds = rect::union(&[union_world_bounds, child_bounds]); } } - let render_bounds = union_world_bounds; // ICB has no effects - let entry = GeometryEntry { transform: local_transform, absolute_transform: world_transform, bounding_box: local_bounds, absolute_bounding_box: union_world_bounds, - absolute_render_bounds: render_bounds, + absolute_render_bounds: union_world_bounds, parent: parent_id, dirty_transform: false, dirty_bounds: false, @@ -260,18 +234,10 @@ impl GeometryCache { Node::BooleanOperation(n) => { let world_transform = parent_world.compose(&n.transform.unwrap_or_default()); let mut union_bounds: Option = None; - if let Some(children) = graph.get_children(id) { + if let Some(children) = ctx.graph.get_children(id) { for child_id in children { let child_bounds = Self::build_recursive( - child_id, - graph, - &world_transform, - Some(id.clone()), - cache, - paragraph_cache, - fonts, - layout_result, - context, + child_id, &world_transform, Some(*id), cache, ctx, ); union_bounds = match union_bounds { Some(b) => Some(rect::union(&[b, child_bounds])), @@ -281,21 +247,13 @@ impl GeometryCache { } let world_bounds = union_bounds.unwrap_or_else(|| Rectangle { - x: 0.0, - y: 0.0, - width: 0.0, - height: 0.0, + x: 0.0, y: 0.0, width: 0.0, height: 0.0, }); let local_bounds = if let Some(inv) = world_transform.inverse() { transform_rect(&world_bounds, &inv) } else { - Rectangle { - x: 0.0, - y: 0.0, - width: 0.0, - height: 0.0, - } + Rectangle { x: 0.0, y: 0.0, width: 0.0, height: 0.0 } }; let render_bounds = compute_render_bounds_from_style( @@ -311,7 +269,7 @@ impl GeometryCache { bounding_box: local_bounds, absolute_bounding_box: world_bounds, absolute_render_bounds: render_bounds, - parent: parent_id.clone(), + parent: parent_id, dirty_transform: false, dirty_bounds: false, }; @@ -321,15 +279,12 @@ impl GeometryCache { bounds } Node::Container(n) => { - // All containers use computed layout (roots have position corrected by LayoutEngine) - let (x, y, width, height) = if let Some(result) = layout_result { - // Layout engine is active: use computed layout + let (x, y, width, height) = if let Some(result) = ctx.layout_result { let computed = result .get(id) .expect("Container must have layout result when layout engine is used"); (computed.x, computed.y, computed.width, computed.height) } else { - // No layout engine: use schema directly (backward compatibility) ( n.position.x().unwrap_or(0.0), n.position.y().unwrap_or(0.0), @@ -339,44 +294,26 @@ impl GeometryCache { }; let local_transform = AffineTransform::new(x, y, n.rotation); - let local_bounds = Rectangle { - x: 0.0, - y: 0.0, - width, - height, - }; + let local_bounds = Rectangle { x: 0.0, y: 0.0, width, height }; let world_transform = parent_world.compose(&local_transform); let world_bounds = transform_rect(&local_bounds, &world_transform); let mut union_world_bounds = world_bounds; let render_bounds = if let Some(rect_stroke) = n.rectangular_stroke_width() { compute_render_bounds_with_rectangular_stroke( - world_bounds, - &rect_stroke, - n.stroke_style.stroke_align, - &n.effects, + world_bounds, &rect_stroke, n.stroke_style.stroke_align, &n.effects, ) } else { compute_render_bounds_from_style( - world_bounds, - n.render_bounds_stroke_width(), - n.stroke_style.stroke_align, - &n.effects, + world_bounds, n.render_bounds_stroke_width(), + n.stroke_style.stroke_align, &n.effects, ) }; - if let Some(children) = graph.get_children(id) { + if let Some(children) = ctx.graph.get_children(id) { for child_id in children { let child_bounds = Self::build_recursive( - child_id, - graph, - &world_transform, - Some(id.clone()), - cache, - paragraph_cache, - fonts, - layout_result, - context, + child_id, &world_transform, Some(*id), cache, ctx, ); union_world_bounds = rect::union(&[union_world_bounds, child_bounds]); } @@ -388,67 +325,42 @@ impl GeometryCache { bounding_box: local_bounds, absolute_bounding_box: world_bounds, absolute_render_bounds: render_bounds, - parent: parent_id.clone(), + parent: parent_id, dirty_transform: false, dirty_bounds: false, }; - cache.entries.insert(id.clone(), entry.clone()); - + cache.entries.insert(*id, entry); union_world_bounds } Node::TextSpan(n) => { - // Resolve layout position/size (if available) and measure text consistently with layout width - let layout = layout_result.and_then(|r| r.get(id)); - let width_for_measure = layout.map(|l| l.width).or(n.width); - - let measurements = paragraph_cache.measure( - &n.text, - &n.text_style, - &n.text_align, - &n.max_lines, - &n.ellipsis, - width_for_measure, - fonts, - Some(id), - ); + let layout = ctx.layout_result.and_then(|r| r.get(id)); const MIN_SIZE_DIRTY_HACK: f32 = 1.0; - let parent_is_layout_container = - Self::is_layout_container_parent(&parent_id, graph); + let parent_is_layout_container = parent_id + .as_ref() + .and_then(|pid| ctx.is_layout_container.get(pid).copied()) + .unwrap_or(false); - let (local_transform, width, height) = if parent_is_layout_container { - let width = layout - .map(|l| l.width) - .unwrap_or_else(|| measurements.max_width) - .max(MIN_SIZE_DIRTY_HACK); - let height = layout - .map(|l| l.height) - .unwrap_or_else(|| n.height.unwrap_or(measurements.height)) - .max(MIN_SIZE_DIRTY_HACK); - let (x, y) = if let Some(l) = layout { - (l.x, l.y) + let (local_transform, width, height) = if let Some(l) = layout { + let width = l.width.max(MIN_SIZE_DIRTY_HACK); + let height = l.height.max(MIN_SIZE_DIRTY_HACK); + let transform = if parent_is_layout_container { + AffineTransform::new(l.x, l.y, n.transform.rotation()) } else { - (n.transform.x(), n.transform.y()) + n.transform }; - (AffineTransform::new(x, y, n.transform.rotation()), width, height) + (transform, width, height) } else { - let width = layout - .map(|l| l.width) - .unwrap_or_else(|| measurements.max_width) - .max(MIN_SIZE_DIRTY_HACK); - let height = layout - .map(|l| l.height) - .unwrap_or_else(|| n.height.unwrap_or(measurements.height)) - .max(MIN_SIZE_DIRTY_HACK); + let measurements = ctx.paragraph_cache.measure( + &n.text, &n.text_style, &n.text_align, &n.max_lines, + &n.ellipsis, n.width, ctx.fonts, Some(id), + ); + let width = measurements.max_width.max(MIN_SIZE_DIRTY_HACK); + let height = n.height.unwrap_or(measurements.height).max(MIN_SIZE_DIRTY_HACK); (n.transform, width, height) }; - let local_bounds = Rectangle { - x: 0.0, - y: 0.0, - width, - height, - }; + let local_bounds = Rectangle { x: 0.0, y: 0.0, width, height }; let world_transform = parent_world.compose(&local_transform); let world_bounds = transform_rect(&local_bounds, &world_transform); let render_bounds = compute_render_bounds(node, world_bounds); @@ -459,16 +371,15 @@ impl GeometryCache { bounding_box: local_bounds, absolute_bounding_box: world_bounds, absolute_render_bounds: render_bounds, - parent: parent_id.clone(), + parent: parent_id, dirty_transform: false, dirty_bounds: false, }; - cache.entries.insert(id.clone(), entry.clone()); - - local_bounds + let bounds = local_bounds; + cache.entries.insert(*id, entry); + bounds } _ => { - // Leaf nodes - check layout result first, fallback to schema transform let (rec_transform, schema_width, schema_height) = match node { Node::Rectangle(n) => (n.transform, n.size.width, n.size.height), Node::Ellipse(n) => (n.transform, n.size.width, n.size.height), @@ -489,55 +400,35 @@ impl GeometryCache { (n.transform, rect.width, rect.height) } Node::Error(n) => (n.transform, n.size.width, n.size.height), - // V2/special nodes handled above _ => unreachable!("Has dedicated case above"), }; - // Check if this node's parent is a layout container (Container - // or ICB). Only those parents provide meaningful layout results - // (computed flex/block positions). Nodes under Group or - let parent_is_layout_container = - Self::is_layout_container_parent(&parent_id, graph); + let parent_is_layout_container = parent_id + .as_ref() + .and_then(|pid| ctx.is_layout_container.get(pid).copied()) + .unwrap_or(false); let (local_transform, width, height) = if parent_is_layout_container { - // Parent is a layout container: use layout result for - // position/size (flex/absolute layout), with rotation - // from the schema transform. let (x, y, width, height) = - if let Some(result) = layout_result.and_then(|r| r.get(id)) { + if let Some(result) = ctx.layout_result.and_then(|r| r.get(id)) { (result.x, result.y, result.width, result.height) } else { - ( - rec_transform.x(), - rec_transform.y(), - schema_width, - schema_height, - ) + (rec_transform.x(), rec_transform.y(), schema_width, schema_height) }; (AffineTransform::new(x, y, rec_transform.rotation()), width, height) } else { - // Parent is NOT a layout container (Group, - // BooleanOperation, or root): use the full schema - // transform. This preserves scale, skew, and arbitrary - // matrix entries that don't fit the (x, y, rotation) - // decomposition. - let width = layout_result + let width = ctx.layout_result .and_then(|r| r.get(id)) .map(|l| l.width) .unwrap_or(schema_width); - let height = layout_result + let height = ctx.layout_result .and_then(|r| r.get(id)) .map(|l| l.height) .unwrap_or(schema_height); (rec_transform, width, height) }; - let local_bounds = Rectangle { - x: 0.0, - y: 0.0, - width, - height, - }; + let local_bounds = Rectangle { x: 0.0, y: 0.0, width, height }; let world_transform = parent_world.compose(&local_transform); let world_bounds = transform_rect(&local_bounds, &world_transform); @@ -549,7 +440,7 @@ impl GeometryCache { bounding_box: local_bounds, absolute_bounding_box: world_bounds, absolute_render_bounds: render_bounds, - parent: parent_id.clone(), + parent: parent_id, dirty_transform: false, dirty_bounds: false, }; diff --git a/crates/grida-canvas/src/cache/paragraph.rs b/crates/grida-canvas/src/cache/paragraph.rs index d2a4bf3874..bcbd1a450a 100644 --- a/crates/grida-canvas/src/cache/paragraph.rs +++ b/crates/grida-canvas/src/cache/paragraph.rs @@ -78,6 +78,7 @@ use crate::text::text_style::textstyle; use skia_safe::textlayout; use std::cell::RefCell; use std::collections::hash_map::DefaultHasher; +use crate::cache::fast_hash::DenseNodeMap; use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::rc::Rc; @@ -152,8 +153,9 @@ pub struct ParagraphCacheEntry { pub font_generation: usize, /// The cached Skia paragraph object pub paragraph: Rc>, - // TODO: Add width-based caching in the future - // For now, we just store the paragraph and compute measurements on demand + /// Cached measurements for the last layout width — avoids re-calling + /// Skia `paragraph.layout()` on every access when the width hasn't changed. + pub cached_measurements: Option<(Option, LayoutMeasurements)>, } /// Accumulated statistics from `measure()` calls — for benchmarking only. @@ -167,8 +169,8 @@ pub struct ParagraphMeasureStats { #[derive(Default, Debug, Clone)] pub struct ParagraphCache { - // ID-based cache for text nodes (primary usage) - entries_measurement_by_id: HashMap, + // ID-based cache for text nodes (primary usage) — Vec-backed for O(1) access + entries_measurement_by_id: DenseNodeMap, // Shape-key-based cache for flexible usage (not currently used) entries_measurement_by_shapekey_unstable: HashMap, /// Benchmark statistics — zero-cost when not read. @@ -181,7 +183,7 @@ pub struct ParagraphCache { impl ParagraphCache { pub fn new() -> Self { Self { - entries_measurement_by_id: HashMap::new(), + entries_measurement_by_id: DenseNodeMap::new(), entries_measurement_by_shapekey_unstable: HashMap::new(), stats: ParagraphMeasureStats::default(), skip_text_measure: false, @@ -236,23 +238,37 @@ impl ParagraphCache { // Check if we have a cached paragraph if let Some(node_id) = id { // Use ID-based cache - if let Some(entry) = self.entries_measurement_by_id.get(node_id) { + if let Some(entry) = self.entries_measurement_by_id.get_mut(node_id) { if entry.font_generation == fonts_gen { self.stats.cache_hits += 1; - // Use the cached paragraph and compute measurements + // Fast path: return cached measurements if width matches + if let Some((cached_w, ref measurements)) = entry.cached_measurements { + if cached_w == width { + return measurements.clone(); + } + } + // Width changed: re-layout and cache let paragraph_rc = entry.paragraph.clone(); - return Self::compute_measurements(paragraph_rc, width); + let m = Self::compute_measurements(paragraph_rc, width); + entry.cached_measurements = Some((width, m.clone())); + return m; } } } else { // Use shape-key-based cache let hash = Self::shape_key(text, style, align, max_lines); - if let Some(entry) = self.entries_measurement_by_shapekey_unstable.get(&hash) { + if let Some(entry) = self.entries_measurement_by_shapekey_unstable.get_mut(&hash) { if entry.font_generation == fonts_gen { self.stats.cache_hits += 1; - // Use the cached paragraph and compute measurements + if let Some((cached_w, ref measurements)) = entry.cached_measurements { + if cached_w == width { + return measurements.clone(); + } + } let paragraph_rc = entry.paragraph.clone(); - return Self::compute_measurements(paragraph_rc, width); + let m = Self::compute_measurements(paragraph_rc, width); + entry.cached_measurements = Some((width, m.clone())); + return m; } } } @@ -292,10 +308,14 @@ impl ParagraphCache { // Store the paragraph for future use let paragraph_rc = Rc::new(RefCell::new(paragraph)); + // Compute measurements and cache them with the entry + let measurements = Self::compute_measurements(paragraph_rc.clone(), width); + let entry = ParagraphCacheEntry { hash: Self::shape_key(text, style, align, max_lines), font_generation: fonts_gen, - paragraph: paragraph_rc.clone(), + paragraph: paragraph_rc, + cached_measurements: Some((width, measurements.clone())), }; // Store in the appropriate cache @@ -307,8 +327,7 @@ impl ParagraphCache { .insert(entry.hash, entry); } - // Compute and return the measurements - Self::compute_measurements(paragraph_rc, width) + measurements } /// Helper method to compute measurements for a given paragraph and width diff --git a/crates/grida-canvas/src/cache/scene.rs b/crates/grida-canvas/src/cache/scene.rs index 85c873cc4a..a4070d31d1 100644 --- a/crates/grida-canvas/src/cache/scene.rs +++ b/crates/grida-canvas/src/cache/scene.rs @@ -98,13 +98,24 @@ impl SceneCache { self.layers .layers .sort_by_key(|entry| entry.layer.z_index()); - self.layer_index = RTree::new(); - for (i, entry) in self.layers.layers.iter().enumerate() { - if let Some(rb) = self.geometry.get_render_bounds(&entry.id) { - let bounds = AABB::from_corners([rb.x, rb.y], [rb.x + rb.width, rb.y + rb.height]); - self.layer_index.insert(IndexedLayer { index: i, bounds }); - } - } + let items: Vec = self + .layers + .layers + .iter() + .enumerate() + .filter_map(|(i, entry)| { + self.geometry.get_render_bounds(&entry.id).map(|rb| { + IndexedLayer { + index: i, + bounds: AABB::from_corners( + [rb.x, rb.y], + [rb.x + rb.width, rb.y + rb.height], + ), + } + }) + }) + .collect(); + self.layer_index = RTree::bulk_load(items); } /// Access the geometry cache. diff --git a/crates/grida-canvas/src/layout/cache.rs b/crates/grida-canvas/src/layout/cache.rs index a6356c9d2e..e8e941b1c7 100644 --- a/crates/grida-canvas/src/layout/cache.rs +++ b/crates/grida-canvas/src/layout/cache.rs @@ -1,4 +1,4 @@ -use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; +use crate::cache::fast_hash::DenseNodeMap; use crate::layout::ComputedLayout; use crate::node::schema::NodeId; @@ -6,15 +6,15 @@ use crate::node::schema::NodeId; /// /// Maps NodeId to computed position/size. Represents the output of a layout /// computation phase. Cached between frames for performance and change detection. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct LayoutResult { - layouts: NodeIdHashMap, + layouts: DenseNodeMap, } impl LayoutResult { pub fn new() -> Self { Self { - layouts: new_node_id_map(), + layouts: DenseNodeMap::new(), } } @@ -38,7 +38,7 @@ impl LayoutResult { self.layouts.clear(); } - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { self.layouts.iter() } diff --git a/crates/grida-canvas/src/layout/tree.rs b/crates/grida-canvas/src/layout/tree.rs index 05f5dd3ba3..a2e8475e4c 100644 --- a/crates/grida-canvas/src/layout/tree.rs +++ b/crates/grida-canvas/src/layout/tree.rs @@ -1,8 +1,9 @@ -use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; +use crate::cache::fast_hash::DenseNodeMap; use crate::cache::paragraph::ParagraphCache; use crate::cg::types::{TextAlign, TextStyleRec}; use crate::node::schema::NodeId; use crate::runtime::font_repository::FontRepository; +#[cfg(test)] use std::collections::HashMap; use taffy::prelude::*; @@ -39,8 +40,9 @@ pub(crate) struct LayoutTree { /// Taffy tree for layout computation taffy: TaffyTree, /// Map from our SceneGraph NodeId to Taffy's NodeId - scene_to_taffy: NodeIdHashMap, - /// Reverse map from Taffy NodeId to SceneGraph NodeId + scene_to_taffy: DenseNodeMap, + /// Reverse map from Taffy NodeId to SceneGraph NodeId (test-only) + #[cfg(test)] taffy_to_scene: HashMap, } @@ -48,7 +50,8 @@ impl LayoutTree { pub(crate) fn new() -> Self { Self { taffy: TaffyTree::new(), - scene_to_taffy: new_node_id_map(), + scene_to_taffy: DenseNodeMap::new(), + #[cfg(test)] taffy_to_scene: HashMap::new(), } } @@ -65,6 +68,7 @@ impl LayoutTree { if self.scene_to_taffy.capacity() < capacity { self.taffy = TaffyTree::with_capacity(capacity); self.scene_to_taffy.reserve(capacity); + #[cfg(test)] self.taffy_to_scene.reserve(capacity); } } @@ -78,17 +82,17 @@ impl LayoutTree { style: Style, ) -> Result { let taffy_id = self.taffy.new_leaf(style)?; - - // Clean up any existing mapping for this scene_node_id - if let Some(old_taffy_id) = self.scene_to_taffy.insert(scene_node_id, taffy_id) { + #[cfg(test)] + if let Some(old_taffy_id) = self.scene_to_taffy.get(&scene_node_id).copied() { self.taffy_to_scene.remove(&old_taffy_id); } - - // Clean up any existing mapping for this taffy_id - if let Some(old_scene_id) = self.taffy_to_scene.insert(taffy_id, scene_node_id) { - self.scene_to_taffy.remove(&old_scene_id); + self.scene_to_taffy.insert(scene_node_id, taffy_id); + #[cfg(test)] + { + if let Some(old_scene_id) = self.taffy_to_scene.insert(taffy_id, scene_node_id) { + self.scene_to_taffy.remove(&old_scene_id); + } } - Ok(taffy_id) } @@ -102,15 +106,17 @@ impl LayoutTree { let taffy_id = self .taffy .new_leaf_with_context(style, LayoutNodeContext::Text(context))?; - - if let Some(old_taffy_id) = self.scene_to_taffy.insert(scene_node_id, taffy_id) { + #[cfg(test)] + if let Some(old_taffy_id) = self.scene_to_taffy.get(&scene_node_id).copied() { self.taffy_to_scene.remove(&old_taffy_id); } - - if let Some(old_scene_id) = self.taffy_to_scene.insert(taffy_id, scene_node_id) { - self.scene_to_taffy.remove(&old_scene_id); + self.scene_to_taffy.insert(scene_node_id, taffy_id); + #[cfg(test)] + { + if let Some(old_scene_id) = self.taffy_to_scene.insert(taffy_id, scene_node_id) { + self.scene_to_taffy.remove(&old_scene_id); + } } - Ok(taffy_id) } @@ -124,17 +130,17 @@ impl LayoutTree { children: &[taffy::NodeId], ) -> Result { let taffy_id = self.taffy.new_with_children(style, children)?; - - // Clean up any existing mapping for this scene_node_id - if let Some(old_taffy_id) = self.scene_to_taffy.insert(scene_node_id, taffy_id) { + #[cfg(test)] + if let Some(old_taffy_id) = self.scene_to_taffy.get(&scene_node_id).copied() { self.taffy_to_scene.remove(&old_taffy_id); } - - // Clean up any existing mapping for this taffy_id - if let Some(old_scene_id) = self.taffy_to_scene.insert(taffy_id, scene_node_id) { - self.scene_to_taffy.remove(&old_scene_id); + self.scene_to_taffy.insert(scene_node_id, taffy_id); + #[cfg(test)] + { + if let Some(old_scene_id) = self.taffy_to_scene.insert(taffy_id, scene_node_id) { + self.scene_to_taffy.remove(&old_scene_id); + } } - Ok(taffy_id) } @@ -214,6 +220,7 @@ impl LayoutTree { pub(crate) fn clear(&mut self) { self.taffy.clear(); self.scene_to_taffy.clear(); + #[cfg(test)] self.taffy_to_scene.clear(); } diff --git a/crates/grida-canvas/src/node/repository.rs b/crates/grida-canvas/src/node/repository.rs index 57f4c90abd..ffbcc202c3 100644 --- a/crates/grida-canvas/src/node/repository.rs +++ b/crates/grida-canvas/src/node/repository.rs @@ -1,11 +1,11 @@ +use crate::cache::fast_hash::DenseNodeMap; use crate::node::schema::{Node, NodeId}; -use std::collections::HashMap; /// A repository for managing nodes with automatic ID indexing. #[derive(Debug, Clone)] pub struct NodeRepository { /// The map of all nodes indexed by their IDs - nodes: HashMap, + nodes: DenseNodeMap, /// ID generator for auto-assigning IDs id_generator: crate::node::id::NodeIdGenerator, } @@ -14,7 +14,7 @@ impl NodeRepository { /// Creates a new empty node repository pub fn new() -> Self { Self { - nodes: HashMap::new(), + nodes: DenseNodeMap::new(), id_generator: crate::node::id::NodeIdGenerator::new(), } } @@ -49,7 +49,7 @@ impl NodeRepository { } /// Returns an iterator over all nodes in the repository - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { self.nodes.iter() } @@ -64,15 +64,16 @@ impl NodeRepository { } pub fn filter(&self, filter: impl Fn(&Node) -> bool) -> Self { - NodeRepository { - nodes: self - .nodes - .iter() - .filter(|(_, node)| filter(node)) - .map(|(k, v)| (k.clone(), v.clone())) - .collect(), + let mut new_repo = NodeRepository { + nodes: DenseNodeMap::new(), id_generator: self.id_generator.clone(), + }; + for (id, node) in self.nodes.iter() { + if filter(node) { + new_repo.nodes.insert(id, node.clone()); + } } + new_repo } } diff --git a/crates/grida-canvas/src/node/scene_graph.rs b/crates/grida-canvas/src/node/scene_graph.rs index 28d245e7c0..fabd0b7bb8 100644 --- a/crates/grida-canvas/src/node/scene_graph.rs +++ b/crates/grida-canvas/src/node/scene_graph.rs @@ -1,5 +1,6 @@ use super::repository::NodeRepository; use super::schema::{Node, NodeId}; +use crate::cache::fast_hash::DenseNodeMap; use std::collections::HashMap; /// Parent reference in the scene graph @@ -59,7 +60,7 @@ pub struct SceneGraph { /// Root node IDs - direct children of the scene roots: Vec, /// Parent to children adjacency list - links: HashMap>, + links: DenseNodeMap>, /// Node data repository nodes: NodeRepository, /// Optional display names for nodes (from the source file). @@ -71,7 +72,7 @@ impl SceneGraph { pub fn new() -> Self { Self { roots: Vec::new(), - links: HashMap::new(), + links: DenseNodeMap::new(), nodes: NodeRepository::new(), names: HashMap::new(), } @@ -98,8 +99,12 @@ impl SceneGraph { graph.nodes.insert_with_id(id, node); } - // Set up all links - graph.links = links; + // Convert HashMap links to DenseNodeMap + let mut dense_links = DenseNodeMap::new(); + for (id, children) in links { + dense_links.insert(id, children); + } + graph.links = dense_links; // Set roots graph.roots = roots; @@ -122,10 +127,11 @@ impl SceneGraph { self.roots.push(id.clone()); } Parent::NodeId(parent_id) => { - self.links - .entry(parent_id) - .or_insert_with(Vec::new) - .push(id.clone()); + if let Some(children) = self.links.get_mut(&parent_id) { + children.push(id.clone()); + } else { + self.links.insert(parent_id, vec![id.clone()]); + } } } @@ -201,7 +207,7 @@ impl SceneGraph { } /// Iterate over all parent->children pairs - pub fn iter(&self) -> impl Iterator)> { + pub fn iter(&self) -> impl Iterator)> { self.links.iter() } @@ -220,7 +226,7 @@ impl SceneGraph { pub fn get_parent(&self, id: &NodeId) -> Option { for (parent_id, children) in &self.links { if children.contains(id) { - return Some(*parent_id); + return Some(parent_id); } } None @@ -280,7 +286,7 @@ impl SceneGraph { /// /// The iteration order is not guaranteed; callers should use `roots()` + /// `get_children()` if they need tree order. - pub fn nodes_iter(&self) -> impl Iterator { + pub fn nodes_iter(&self) -> impl Iterator { self.nodes.iter() } diff --git a/crates/grida-canvas/src/resources/mod.rs b/crates/grida-canvas/src/resources/mod.rs index d4cef7f031..8247d53a07 100644 --- a/crates/grida-canvas/src/resources/mod.rs +++ b/crates/grida-canvas/src/resources/mod.rs @@ -148,7 +148,7 @@ fn extract_image_urls(scene: &Scene) -> Vec { use crate::node::schema::Node; let mut urls = Vec::new(); for (id, _) in scene.graph.iter() { - let Ok(node) = scene.graph.get_node(id) else { + let Ok(node) = scene.graph.get_node(&id) else { continue; }; diff --git a/crates/grida-canvas/src/runtime/scene.rs b/crates/grida-canvas/src/runtime/scene.rs index 3fea2bae83..fcbd50799d 100644 --- a/crates/grida-canvas/src/runtime/scene.rs +++ b/crates/grida-canvas/src/runtime/scene.rs @@ -1338,6 +1338,9 @@ impl Renderer { /// Load a scene into the renderer. Caching will be performed lazily during /// rendering based on the configured caching strategy. pub fn load_scene(&mut self, scene: Scene) { + #[cfg(feature = "perf")] + let _t0 = crate::sys::perf_now(); + self.scene = Some(scene); self.scene_cache = cache::scene::SceneCache::new(); @@ -1345,8 +1348,12 @@ impl Renderer { self.zoom_image_cache = None; self.images.clear_missing_tracking(); if let Some(scene) = self.scene.as_ref() { + #[cfg(feature = "perf")] + let _t_fonts_start = crate::sys::perf_now(); let requested = collect_scene_font_families(scene); self.fonts.set_requested_families(requested.into_iter()); + #[cfg(feature = "perf")] + let _t_fonts = crate::sys::perf_now(); let viewport_size = self.window_context.viewport_size; @@ -1367,6 +1374,8 @@ impl Renderer { }), ); } + #[cfg(feature = "perf")] + let _t_layout = crate::sys::perf_now(); // 2. Build geometry with layout results let layout_result = self.layout_engine.result(); @@ -1376,12 +1385,30 @@ impl Renderer { layout_result, viewport_size, ); + #[cfg(feature = "perf")] + let _t_geometry = crate::sys::perf_now(); // 3. Build effect tree (identifies render surface boundaries) self.scene_cache.update_effect_tree(scene); + #[cfg(feature = "perf")] + let _t_effects = crate::sys::perf_now(); // 4. Build layers self.scene_cache.update_layers(scene); + + #[cfg(feature = "perf")] + { + let _t_layers = crate::sys::perf_now(); + eprintln!( + "[load_scene] fonts={:.0}ms layout={:.0}ms geometry={:.0}ms effects={:.0}ms layers={:.0}ms total={:.0}ms", + _t_fonts - _t_fonts_start, + _t_layout - _t_fonts, + _t_geometry - _t_layout, + _t_effects - _t_geometry, + _t_layers - _t_effects, + _t_layers - _t0, + ); + } } // Record SCENE_LOAD so apply_changes() knows to clear picture/paragraph/ // path/compositor caches on the next frame. The scene_cache was already diff --git a/crates/grida-canvas/src/sys/mod.rs b/crates/grida-canvas/src/sys/mod.rs index b8789e03f6..3c67951d0f 100644 --- a/crates/grida-canvas/src/sys/mod.rs +++ b/crates/grida-canvas/src/sys/mod.rs @@ -1,2 +1,17 @@ pub mod clock; pub mod timer; + +/// High-resolution timestamp in milliseconds. +/// Uses `emscripten_get_now()` on WASM, `Instant` on native. +#[cfg(target_os = "emscripten")] +pub fn perf_now() -> f64 { + unsafe { crate::os::emscripten::emscripten_get_now() } +} + +#[cfg(not(target_os = "emscripten"))] +pub fn perf_now() -> f64 { + use std::time::Instant; + static START: std::sync::OnceLock = std::sync::OnceLock::new(); + let start = START.get_or_init(Instant::now); + start.elapsed().as_secs_f64() * 1000.0 +} diff --git a/crates/grida-canvas/src/window/application.rs b/crates/grida-canvas/src/window/application.rs index ceff84cbc7..101a0e9c98 100644 --- a/crates/grida-canvas/src/window/application.rs +++ b/crates/grida-canvas/src/window/application.rs @@ -132,6 +132,9 @@ pub trait ApplicationApi { /// Only works after `load_scene_grida` has decoded a multi-scene document. fn switch_scene(&mut self, scene_id: &str); + /// Return the IDs of all scenes decoded by the last `load_scene_grida` call. + fn loaded_scene_ids(&self) -> Vec; + /// Apply a batch of scene transactions represented as JSON Patch operations. fn apply_document_transactions( &mut self, @@ -626,8 +629,8 @@ impl ApplicationApi for UnknownTargetApplication { } fn load_scene_grida(&mut self, bytes: &[u8]) { - use crate::io::io_grida_fbs; - match io_grida_fbs::decode_with_id_map(bytes) { + use crate::io::io_grida_file; + match io_grida_file::decode_with_id_map(bytes) { Ok(result) => { // Build id mappings from the decode result let mut string_to_internal = std::collections::HashMap::new(); @@ -672,6 +675,10 @@ impl ApplicationApi for UnknownTargetApplication { } } + fn loaded_scene_ids(&self) -> Vec { + self.loaded_scenes.iter().map(|(id, _)| id.clone()).collect() + } + fn apply_document_transactions( &mut self, transactions: Vec>, diff --git a/crates/grida-canvas/src/window/application_emscripten.rs b/crates/grida-canvas/src/window/application_emscripten.rs index ca1f564574..237f0f1d6b 100644 --- a/crates/grida-canvas/src/window/application_emscripten.rs +++ b/crates/grida-canvas/src/window/application_emscripten.rs @@ -296,6 +296,10 @@ impl ApplicationApi for EmscriptenApplication { self.base.switch_scene(scene_id); } + fn loaded_scene_ids(&self) -> Vec { + self.base.loaded_scene_ids() + } + fn apply_document_transactions( &mut self, transactions: Vec>, diff --git a/crates/grida-dev/Cargo.toml b/crates/grida-dev/Cargo.toml index b456362939..97edadd8fa 100644 --- a/crates/grida-dev/Cargo.toml +++ b/crates/grida-dev/Cargo.toml @@ -9,6 +9,7 @@ description = "Native winit playground for grida-canvas scenes." [dependencies] cg = { path = "../grida-canvas", features = [ "web", + "perf", "native-clock-tick", "native-gl-context", ] } diff --git a/crates/grida-dev/src/bench/load_bench.rs b/crates/grida-dev/src/bench/load_bench.rs index c03f9a7560..9c6fea15c6 100644 --- a/crates/grida-dev/src/bench/load_bench.rs +++ b/crates/grida-dev/src/bench/load_bench.rs @@ -187,7 +187,7 @@ fn layout_diff(scene: &Scene, width: i32, height: i32, threshold: f32) { let mut diffs = Vec::new(); let eps = threshold; for (id, full) in result_full.iter() { - if let Some(schema) = result_schema.get(id) { + if let Some(schema) = result_schema.get(&id) { let dx = (full.x - schema.x).abs(); let dy = (full.y - schema.y).abs(); let dw = (full.width - schema.width).abs(); @@ -195,14 +195,14 @@ fn layout_diff(scene: &Scene, width: i32, height: i32, threshold: f32) { if dx > eps || dy > eps || dw > eps || dh > eps { let node_type = scene .graph - .get_node(id) + .get_node(&id) .map(|n| format!("{:?}", std::mem::discriminant(n))) .unwrap_or_else(|_| "?".to_string()); // Get node type name cleanly let type_name = scene .graph - .get_node(id) + .get_node(&id) .map(|n| match n { cg::node::schema::Node::Container(_) => "Container", cg::node::schema::Node::Rectangle(_) => "Rectangle", @@ -226,7 +226,7 @@ fn layout_diff(scene: &Scene, width: i32, height: i32, threshold: f32) { // Check if parent is a container (to understand context) let parent_info = scene .graph - .get_parent(id) + .get_parent(&id) .and_then(|pid| { scene.graph.get_node(&pid).ok().map(|p| match p { cg::node::schema::Node::Container(_) => "Container", @@ -239,7 +239,7 @@ fn layout_diff(scene: &Scene, width: i32, height: i32, threshold: f32) { .unwrap_or("root"); diffs.push(( - *id, + id, type_name, parent_info, *full, @@ -258,7 +258,7 @@ fn layout_diff(scene: &Scene, width: i32, height: i32, threshold: f32) { // Also check for nodes in schema but not in full for (id, _) in result_schema.iter() { - if result_full.get(id).is_none() { + if result_full.get(&id).is_none() { eprintln!( " WARN: node {:?} in schema result but missing from full result", id diff --git a/docs/wg/feat-2d/wasm-benchmarking.md b/docs/wg/feat-2d/wasm-benchmarking.md new file mode 100644 index 0000000000..55d3869a9d --- /dev/null +++ b/docs/wg/feat-2d/wasm-benchmarking.md @@ -0,0 +1,336 @@ +--- +title: "WASM Benchmarking Strategy" +format: md +tags: + - internal + - wg + - canvas + - performance + - wasm + - benchmarking +--- + +# WASM Benchmarking Strategy + +## Why Real WASM Benchmarking Matters + +When evaluating performance for a system that ships as WebAssembly, it is not +enough to benchmark only the native implementation. Native benchmarks are still +valuable, but they do not fully represent the runtime characteristics of WASM. + +WebAssembly introduces a different execution environment, different code +generation, different memory behavior, and often a different host integration +model. As a result, performance measured in native Rust cannot be assumed to +transfer directly to WASM. Even when the core algorithm is identical, the +compiled output and runtime constraints are not. + +Real WASM benchmarking is necessary whenever performance decisions are intended +to reflect the actual delivered runtime. + +## Why Native-Only Benchmarking Is Not Sufficient + +Native benchmarks are useful for understanding the upper bound of performance +and for comparing algorithmic choices in an idealized environment. They are +especially helpful for isolating pure compute costs and for detecting +regressions in the Rust implementation itself. + +However, native benchmarks do not capture several important WASM-specific +effects: + +- Code generation differences between native and WASM targets +- Linear memory behavior in WASM +- JS-WASM boundary costs +- Host runtime differences (V8/SpiderMonkey vs native CPU) +- Constraints specific to the WASM execution model (no SIMD by default, + limited stack, single-threaded) + +A native benchmark may correctly show that one version is faster than another, +while still failing to predict the actual magnitude of the improvement in WASM. + +## WASM-on-Node: Real WASM Without the Browser + +Although browser execution is the final target, benchmarking WASM does not +strictly require a browser in all cases. + +A WASM module can also run in Node.js. For compute-heavy workloads, this +provides a practical way to measure real WASM execution without introducing +browser automation, rendering setup, or UI-related noise. + +WASM-on-Node still measures: + +- The WASM target itself (same compiled `.wasm` binary) +- WASM code generation (same emscripten output) +- WASM memory behavior (same linear memory model) +- Much of the same runtime structure as deployed WASM (V8 WASM engine) + +It is not a native proxy pretending to be WASM. It is real WASM, just hosted +outside the browser. + +### What WASM-on-Node Is Good For + +WASM-on-Node is a strong option for benchmarks that are mostly internal to the +engine and do not depend heavily on browser APIs: + +- Scene loading and construction +- Layout computation +- Geometry processing +- Effect tree building +- Layer flattening and sorting +- Serialization and deserialization (FlatBuffers decode) +- Hit testing +- Pure compute kernels + +For these categories, WASM-on-Node provides meaningful performance +measurements and regression tracking. It is especially useful in CI, where +running a browser-based benchmark suite is heavier, more fragile, and harder +to keep deterministic. + +### What WASM-on-Node Cannot Fully Represent + +The browser environment adds behaviors and constraints that Node does not +reproduce exactly: + +- Browser event loop behavior and frame timing +- Worker scheduling characteristics +- Rendering pipeline costs (GPU flush, surface snapshot) +- DOM or canvas interaction +- Browser-specific memory pressure and GC behavior +- Browser-specific host API overhead + +WASM-on-Node should not be treated as a complete substitute for browser +benchmarking when the workload depends on browser integration. + +## Three-Layer Benchmarking Model + +A practical approach treats benchmarking as three separate layers: + +### Layer 1: Native benchmark + +Measures the implementation ceiling and isolates core algorithmic cost. + +Tools: `cargo run -p grida-dev --release -- load-bench`, Criterion benches +in `crates/grida-canvas/benches/`. + +Best for: Algorithmic comparisons, regression detection, profiling with +native tools (perf, Instruments, samply). + +### Layer 2: WASM-on-Node benchmark + +Measures real WASM execution in a simple, repeatable, automation-friendly +environment. + +Tools: Node.js script that loads the emscripten `.js` + `.wasm` module, +calls C-ABI functions, measures with `performance.now()`. + +Best for: Regression tracking, validating whether an optimization helps in +the WASM target, CI integration, comparing WASM/native ratios. + +### Layer 3: Browser benchmark + +The final reference for workloads that depend on browser-specific behavior +or APIs. + +Tools: Debug page at `/embed/v1/debug` with console timing, manual +stopwatch for end-to-end, browser DevTools profiling. + +Best for: Full pipeline validation (JS encode + WASM load + GPU render), +real-world user-facing latency measurement. + +## What We Can Safely Learn From WASM-on-Node + +WASM-on-Node is often very good for: + +- Tracking regressions over time +- Comparing alternative implementations +- Measuring scaling trends (how cost grows with node count) +- Validating whether an optimization helps in the WASM target at all + +Relative changes are more trustworthy than absolute numbers. If one +implementation is consistently 20% faster in WASM-on-Node, that is a +meaningful signal even if browser timings differ in absolute terms. + +## Caveats + +- Native Rust and WASM should not be assumed to have a fixed conversion + ratio. A workload that is close to native speed in one case may diverge + much more in another. +- WASM-on-Node and WASM-in-browser should not be assumed to be numerically + identical. They may show the same trend while differing in total runtime. +- Benchmarks that frequently cross the JS boundary may behave very + differently from benchmarks that remain almost entirely inside WASM. +- Any benchmark involving rendering, canvas, GPU, event loops, or browser + worker coordination should still be validated in the browser. + +## Lessons Learned (from load_scene optimization, 2026-03) + +### The WASM/native ratio is not constant across operations + +Different operations within the same codebase can have wildly different +WASM/native ratios. In the `load_scene` pipeline, we observed: + +- Simple compute (font collection, effect tree): ~2-3x WASM overhead +- HashMap-heavy traversal: 8-35x WASM overhead +- After replacing HashMap with Vec-indexed storage: overhead dropped to + 1-2x for data-structure-dominated stages, but stayed 5-30x for + compute-heavy stages + +The ratio depends on the nature of the work, not just the volume. Assuming +a single multiplier (e.g. "WASM is 3x slower") leads to incorrect +predictions. + +### Data structure choice matters far more in WASM than native + +HashMap with 136k entries showed acceptable performance on native (hidden +by hardware prefetch, out-of-order execution, and large caches). The same +HashMap in WASM was catastrophically slow because WASM's linear memory +model, smaller effective caches, and more in-order execution expose every +cache miss. + +The fix — replacing `HashMap` with `Vec>` indexed by +sequential `NodeId` — had a modest effect on native (~18% improvement) but +a dramatic effect on WASM, cutting some stages by 50% or more. This +confirms that data structure choices optimized for native may be poor +choices for WASM, and vice versa. + +### `std::time::Instant` does not work in emscripten WASM + +`Instant::now()` returns a constant value (effectively zero) under +emscripten. Any timing code that uses `Instant` will silently produce +meaningless results in WASM. + +The solution is `emscripten_get_now()` (bound as a C extern), which maps +to `performance.now()` and provides millisecond-resolution timing. We +wrapped this in `sys::perf_now()` which dispatches to `emscripten_get_now` +on WASM and `Instant`-based timing on native, so the same instrumentation +code works on both targets. + +### Native benchmarks can verify coverage but not WASM cost + +Before instrumenting WASM, we first confirmed that the native load-bench +covered the same code path as the WASM `switch_scene` C-ABI call. This +was important: if the benchmark had been missing a stage, the WASM +measurement would have been unexplainable. + +The native benchmark correctly identified all five stages (fonts, layout, +geometry, effects, layers) and their relative costs. What it could not +predict was which stages would blow up in WASM. The native profile showed +layers as the dominant cost (45%); in WASM, geometry was dominant (40%) +due to per-node HashMap amplification that native hardware masked. + +### Per-stage timing inside WASM is essential + +Without sub-stage timing, a 10-second WASM call is opaque. With +`emscripten_get_now()` instrumented around each stage, we immediately +identified that geometry and layout were the primary targets, not layers +(which native benchmarks had suggested). + +The pattern of adding `perf_now()` calls around major phases and +`eprintln!` for output (which appears in the browser console via stderr) +is lightweight and should be the first step in any WASM performance +investigation. + +### GPU-only code paths create WASM-specific bugs + +Two rendering bugs were found that only manifested in WASM because native +uses a CPU backend: + +1. `blit_content_cache()` drew a stale pan cache at (0,0) instead of the + correct offset — invisible on CPU backend because `is_gpu()` returned + false and the function short-circuited. +2. The overlay-only fast path intercepted stable frames, preventing + full-quality re-rendering — again only triggered on GPU backend. + +This reinforces that WASM testing is not just about performance. The GPU +backend (WebGL via emscripten) exercises code paths that native CPU +rendering never touches. + +### The JS-WASM boundary overhead is small for bulk operations + +For `load_scene`, the JS side contributes string allocation and a single +C-ABI call. The actual boundary cost (allocate string in WASM memory, +call `_switch_scene`, free string) is negligible compared to the work +inside WASM. For bulk operations, the boundary is not the bottleneck. + +However, the JS-side FlatBuffers encoding (serializing the scene graph +into a binary buffer before passing to WASM) is a non-trivial cost — +roughly 10% of the total pipeline. This work happens entirely in JS and +is invisible to Rust-side benchmarks. + +### Large enum access is pathologically slow in WASM + +After eliminating all data structure overhead (HashMap → DenseNodeMap), the +geometry DFS still showed 33× WASM/native ratio. The remaining bottleneck is +the `Node` enum itself — 15 variants, each a large struct. Accessing +`graph.get_node(id)` fetches a reference into `Vec>` where each +slot is the size of the largest variant (likely 500+ bytes). + +For 136k nodes, the DFS touches ~65MB of node data, most of which is +irrelevant to geometry (paints, text content, vector networks). Native +hardware mitigates this with prefetching and out-of-order execution. WASM's +linear memory model and bounds-checked loads make this 30× slower. + +The fix is a Struct-of-Arrays (SoA) approach: extract only the +geometry-relevant fields (transform, size, kind, ~48 bytes) into a compact +dense array, then run the DFS on that. This is documented in +`wasm-load-scene-optimization.md`. + +### Optimization priorities differ between native and WASM + +On native, the priority order for `load_scene` was: +layers > geometry > layout > fonts > effects. + +On WASM, after the same optimizations, the priority order was: +geometry > layout > layers > fonts > effects. + +An optimization strategy based purely on native profiling would have +targeted layers first. The actual highest-impact target in WASM was +geometry, due to HashMap amplification that native did not expose. + +## Implementation Plan: WASM-on-Node Benchmark Harness + +### Architecture + +The WASM-on-Node benchmark harness reuses the same emscripten-compiled +`.js` + `.wasm` module that ships to the browser. It loads the module in +Node.js, calls the C-ABI functions directly, and measures with +`performance.now()`. + +``` +Node.js script + |-- load grida-canvas-wasm.js (emscripten glue) + |-- instantiate grida_canvas_wasm.wasm + |-- call C-ABI: _create_app, _load_scene_grida, _switch_scene + |-- measure each call with performance.now() +``` + +### Scope + +The harness measures the `load_scene` pipeline — the same stages measured +by the native `load-bench`: + +1. FBS decode (`_load_scene_grida`) +2. Scene switch / layout+geometry+effects+layers (`_switch_scene`) + +GPU rendering is not available in Node (no WebGL context), so render-path +benchmarks are out of scope for this layer. + +### Key Differences from Browser + +- No GPU backend — the WASM module should be configured with a stub or + CPU-only backend for benchmarking load_scene (which does not render) +- No RAF loop — calls are synchronous +- No JS editor state — only the WASM module is exercised + +### Internal Timing + +The WASM module emits per-stage timing via `eprintln!` using +`sys::perf_now()` (which calls `emscripten_get_now()`). The Node harness +captures stderr and parses the `[load_scene]` line to extract per-stage +breakdowns without any additional instrumentation. + +### Location + +- WASM-on-Node bench: `crates/grida-canvas-wasm/lib/__test__/bench-load-scene.test.ts` +- Run: `cd crates/grida-canvas-wasm && npx vitest run __test__/bench-load-scene.test.ts` +- Reuses build artifacts from `crates/grida-canvas-wasm/lib/bin/` +- Requires fixture: `fixtures/local/perf/local/yrr-main.grida` (136k-node scene) diff --git a/docs/wg/feat-2d/wasm-load-scene-optimization.md b/docs/wg/feat-2d/wasm-load-scene-optimization.md new file mode 100644 index 0000000000..8536beeeae --- /dev/null +++ b/docs/wg/feat-2d/wasm-load-scene-optimization.md @@ -0,0 +1,155 @@ +# WASM `load_scene` Optimization Plan + +## Status: In Progress + +## Problem + +`Renderer::load_scene()` for a 136k-node Figma-imported scene (yrr-main.grida) takes ~10s in WASM vs ~800ms native — a 13× overhead far beyond the normal 2-3× WASM/native ratio. + +## Current Measurements (WASM-on-Node) + +| Stage | Native (ms) | WASM (ms) | Ratio | Notes | +| --------- | ----------- | ---------- | --------- | -------------------------------- | +| fonts | 5 | 7 | 1.4× | Healthy | +| layout | 239 | 4,272 | 17.9× | Taffy tree + flex compute | +| geometry | 121 | 4,017 | 33× | DFS transform/bounds propagation | +| effects | 3 | 5 | 1.7× | Healthy | +| layers | 427 | 2,182 | 5.1× | Flatten + RTree | +| **total** | **796** | **10,484** | **13.2×** | | + +Stages with healthy ratios (fonts, effects) confirm that simple per-node work runs at 1.5-2× in WASM. The pathological ratios in geometry/layout/layers indicate something structurally cache-unfriendly. + +## Root Cause Analysis + +### What we ruled out + +These were investigated and either fixed or confirmed not the bottleneck: + +1. **HashMap overhead** — Replaced with `DenseNodeMap` (Vec-backed). Fixed fonts/effects ratios but geometry/layout/layers unchanged. +2. **Function parameter count** — Reduced `build_recursive` from 9 params to 5 via context struct. No measurable change. +3. **Redundant text measurement** — Geometry was calling `paragraph_cache.measure()` for all 27k text spans even when layout results existed. Fixed (skip when layout provides dimensions). Helped native slightly, no WASM change. +4. **RTree sequential insert** — Replaced `RTree::new()` + N inserts with `RTree::bulk_load()`. Marginal improvement. +5. **`taffy_to_scene` HashMap** — Was written to on every node insert but only read in `#[cfg(test)]`. Gated behind `#[cfg(test)]`. No measurable change. + +### What IS the bottleneck + +The `Node` enum has 15 variants, each containing a full `*NodeRec` struct (transform, paints, effects, text content, vector networks, etc.). During the geometry DFS, every `graph.get_node(id)` fetches a reference into a `Vec>` where each slot is the size of the largest variant — likely 500+ bytes. + +For 136k nodes, the DFS touches ~65MB of node data, most of which is irrelevant to geometry (paints, text content, etc.). In WASM's linear memory model, this cache-unfriendly access pattern is amplified: + +- **Native**: L1/L2 cache prefetching partially hides latency → 0.9μs/node +- **WASM**: Linear memory accesses compile to bounds-checked loads, no hardware prefetch → 29μs/node (33×) + +The layout stage (17.9×) has a similar problem inside Taffy's `SlotMap` internals, plus the cost of building a parallel Taffy tree from our scene graph. + +The layers stage (5.1×) is a DFS that also accesses the full `Node` enum plus geometry cache per node, though it's less pathological since it reads from the already-built geometry cache (dense, cache-friendly). + +## Recommended Fix: Targeted SoA for Geometry Phase + +### Concept + +Extract a compact, geometry-only representation from the scene graph once (O(n) scan), then run the DFS on that instead of the full `Node` enum. + +```rust +/// Compact per-node data for geometry computation. +/// ~48 bytes vs hundreds for the full Node enum. +#[derive(Clone, Copy)] +struct GeoInput { + transform: AffineTransform, // 28 bytes (6 f32 + rotation) + width: f32, // from layout result or schema + height: f32, // from layout result or schema + kind: GeoNodeKind, // 1 byte enum + render_bounds_inflation: f32, // pre-computed from effects/stroke +} + +#[repr(u8)] +enum GeoNodeKind { + Group, + InitialContainer, + Container, + BooleanOperation, + TextSpan, + Leaf, +} +``` + +### Implementation Steps + +1. **Define `GeoInput` and `GeoNodeKind`** in `cache/geometry.rs` + +2. **Add extraction pass** in `from_scene_with_layout`: + + ```rust + // O(n) scan: extract geometry-relevant data from Node enum + let mut geo_inputs = DenseNodeMap::with_capacity(graph.node_count()); + for (id, node) in graph.nodes_iter() { + let layout = layout_result.and_then(|r| r.get(&id)); + geo_inputs.insert(id, GeoInput::from_node(node, layout, &ctx)); + } + ``` + +3. **Rewrite `build_recursive`** to operate on `&GeoInput` instead of `&Node`: + - `graph.get_node(id)` → `geo_inputs.get(id)` (44 bytes, cache-friendly) + - Match on `GeoNodeKind` (1-byte discriminant) instead of `Node` (large enum) + - Children still come from `graph.get_children(id)` (unchanged) + +4. **Handle text measurement**: For `GeoNodeKind::TextSpan` without layout results, store the measured (width, height) in `GeoInput` during the extraction pass. This moves text measurement out of the DFS entirely. + +5. **Handle render bounds**: Pre-compute the effect/stroke inflation in `GeoInput::from_node` so the DFS only needs `world_bounds.inflate(inflation)`. + +### Expected Impact + +- **Geometry**: The DFS now touches ~6.5MB (48 bytes × 136k) instead of ~65MB. Should bring WASM ratio from 33× closer to 3-5×. +- **Native**: Also benefits from better cache locality, potentially 2-3× faster. +- **Layers**: Can follow the same pattern later (extract a `LayerInput` struct). +- **Layout**: Harder — Taffy's internal data structures are the bottleneck. Consider profiling Taffy separately. + +### What this does NOT change + +- Taffy layout computation (still uses Taffy's own data structures) +- Layer flattening (still reads full Node enum for paint info — separate optimization) +- Scene graph structure (Node enum stays as-is for all other uses) + +### Risks + +- **Render bounds accuracy**: Pre-computing inflation requires careful handling of per-side stroke widths and multiple effect types. The extraction pass must match the current `compute_render_bounds_*` logic exactly. +- **Text measurement in extraction**: Moving text measurement before the DFS means we measure all text nodes, even inactive ones. Add an `active` check. +- **Maintenance**: Two representations of the same data. Document clearly that `GeoInput` is a cache, not a source of truth. + +## Benchmark Infrastructure + +WASM-on-Node benchmarks are in `crates/grida-canvas-wasm/lib/__test__/bench-load-scene.test.ts`. Run with: + +```sh +cd crates/grida-canvas-wasm +npx vitest run __test__/bench-load-scene.test.ts +``` + +Native benchmarks: + +```sh +cargo run -p grida-dev --release -- load-bench fixtures/local/perf/local/yrr-main.grida --iterations 3 +``` + +Build WASM (from repo root): + +```sh +just --justfile crates/grida-canvas-wasm/justfile build +``` + +## Files to Modify + +| File | Change | +| -------------------------------------------- | ------------------------------------------------------ | +| `crates/grida-canvas/src/cache/geometry.rs` | Add `GeoInput`, extraction pass, rewrite DFS | +| `crates/grida-canvas/src/cache/paragraph.rs` | No change (already optimized with measurement caching) | +| `crates/grida-canvas/src/cache/scene.rs` | No change | +| `crates/grida-canvas/src/layout/tree.rs` | No change | + +## Validation + +1. `cargo test -p cg` — all 330 tests must pass +2. `cargo check -p cg -p grida-canvas-wasm -p grida-dev` — all crates compile +3. Native benchmark: should not regress (target: <800ms) +4. WASM-on-Node benchmark: geometry stage should drop from ~4s to <1s +5. Visual: load yrr-main in browser debug embed, verify text renders correctly and pan/zoom/settle work From 2fac06ea525372f6f1a58d3ed012086107498d16 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 25 Mar 2026 01:58:02 +0900 Subject: [PATCH 15/31] refactor(embed): update RefigRenderConfig as now it is optimized agains layout - Modified the `RefigRenderConfig` interface to extend `iofigma.restful.factory.FactoryContext`, allowing for more flexible configuration options. - Removed the `prefer_fixed_text_sizing` property and changed `cg_skip_layout` to a boolean type for better clarity and future extensibility. - This refactor enhances the integration with the Figma API and prepares the code for upcoming features. --- editor/scaffolds/embed/use-refig-editor.ts | 26 +++++++--------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/editor/scaffolds/embed/use-refig-editor.ts b/editor/scaffolds/embed/use-refig-editor.ts index b889558dce..bc1b35028c 100644 --- a/editor/scaffolds/embed/use-refig-editor.ts +++ b/editor/scaffolds/embed/use-refig-editor.ts @@ -6,6 +6,7 @@ import { io } from "@grida/io"; import { editor } from "@/grida-canvas"; import { useEditor, useEditorState } from "@/grida-canvas-react"; import { distro } from "@/grida-canvas-hosted/distro"; +import type iofigma from "@grida/io-figma"; function validateExt(name: string) { const l = name.toLowerCase(); @@ -44,31 +45,20 @@ async function decompressGzip(buf: ArrayBuffer): Promise { * Renderer configuration for the refig embed canvas. * * These flags are applied to the WASM renderer after mount, before any - * document is loaded. Values are intentionally narrow literals today — - * widen to `boolean` once the feature graduates from "always-on". + * document is loaded. */ -interface RefigRenderConfig { +interface RefigRenderConfig extends Pick< + iofigma.restful.factory.FactoryContext, + "prefer_fixed_text_sizing" +> { /** - * Skip the Taffy flexbox layout engine during scene loading. - * - * Figma imports use absolute positioning — running layout on 100k+ - * nodes is the dominant cold-start cost (~25 s in WASM). Setting - * this to `true` derives layout from schema positions instead. + * Skip the flexbox layout engine during scene loading. */ - cg_skip_layout: true; - /** - * Bake Figma's absoluteBoundingBox dimensions into TEXT nodes instead - * of relying on layout-time text measurement. - * - * Paired with `cg_skip_layout` — without this, text nodes get 0×0 - * sizes because the layout engine (which would measure them) is skipped. - */ - prefer_fixed_text_sizing: true; + cg_skip_layout: boolean; } const REFIG_RENDER_CONFIG: RefigRenderConfig = { cg_skip_layout: true, - prefer_fixed_text_sizing: true, }; export function useRefigEditor() { From d02ab7c9eeb4ca40190b2900ebd2345702ea24b1 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 25 Mar 2026 02:03:54 +0900 Subject: [PATCH 16/31] fix, enable layout --- editor/scaffolds/embed/use-refig-editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/scaffolds/embed/use-refig-editor.ts b/editor/scaffolds/embed/use-refig-editor.ts index bc1b35028c..d7833f0ef3 100644 --- a/editor/scaffolds/embed/use-refig-editor.ts +++ b/editor/scaffolds/embed/use-refig-editor.ts @@ -58,7 +58,7 @@ interface RefigRenderConfig extends Pick< } const REFIG_RENDER_CONFIG: RefigRenderConfig = { - cg_skip_layout: true, + cg_skip_layout: false, }; export function useRefigEditor() { From ef781e778c058d16c074c16314d32ed53bb69e83 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 25 Mar 2026 03:15:37 +0900 Subject: [PATCH 17/31] fix docs build --- docs/wg/feat-2d/wasm-load-scene-optimization.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/wg/feat-2d/wasm-load-scene-optimization.md b/docs/wg/feat-2d/wasm-load-scene-optimization.md index 8536beeeae..134050723a 100644 --- a/docs/wg/feat-2d/wasm-load-scene-optimization.md +++ b/docs/wg/feat-2d/wasm-load-scene-optimization.md @@ -1,3 +1,7 @@ +--- +format: md +--- + # WASM `load_scene` Optimization Plan ## Status: In Progress @@ -150,6 +154,6 @@ just --justfile crates/grida-canvas-wasm/justfile build 1. `cargo test -p cg` — all 330 tests must pass 2. `cargo check -p cg -p grida-canvas-wasm -p grida-dev` — all crates compile -3. Native benchmark: should not regress (target: <800ms) -4. WASM-on-Node benchmark: geometry stage should drop from ~4s to <1s +3. Native benchmark: should not regress (target: `<800ms`) +4. WASM-on-Node benchmark: geometry stage should drop from ~4s to `<1s` 5. Visual: load yrr-main in browser debug embed, verify text renders correctly and pan/zoom/settle work From 8cba8c173a3628b879ec461143819a16f0f95431 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 25 Mar 2026 04:11:09 +0900 Subject: [PATCH 18/31] feat(io): add snapshot option to control JSON snapshot generation - Introduced a new `snapshot` option in the `pack` function to allow users to skip generating the `document.grida1` JSON snapshot, improving performance for pipelines that only require the FlatBuffers document. - Updated the `fig2grida-core` to utilize the new `snapshot` option, setting it to false by default for specific use cases. --- .../grida-canvas-io-figma/fig2grida-core.ts | 2 +- packages/grida-canvas-io/index.ts | 34 +++++++++++++------ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/packages/grida-canvas-io-figma/fig2grida-core.ts b/packages/grida-canvas-io-figma/fig2grida-core.ts index cb7137a2bc..2179a220ca 100644 --- a/packages/grida-canvas-io-figma/fig2grida-core.ts +++ b/packages/grida-canvas-io-figma/fig2grida-core.ts @@ -229,7 +229,7 @@ function packMergedDocument(merged: MergedDocument): Fig2GridaResult { merged.imageRecord, undefined, undefined, - { level: 0 } + { level: 0, snapshot: false } ); const nodeCount = Object.keys(merged.document.nodes).filter( (id) => merged.document.nodes[id]?.type !== "scene" diff --git a/packages/grida-canvas-io/index.ts b/packages/grida-canvas-io/index.ts index a38545446e..e6be7c1ebc 100644 --- a/packages/grida-canvas-io/index.ts +++ b/packages/grida-canvas-io/index.ts @@ -939,6 +939,14 @@ export namespace io { * immediately consumed (e.g. fig2grida → io.load round-trip). */ level?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; + /** + * When false, skip generating the `document.grida1` JSON snapshot. + * The snapshot is a migration/fallback format; pipelines that only + * need the FlatBuffers document (e.g. fig2grida) can disable it + * to avoid a costly `JSON.stringify` over the entire node tree. + * @default true + */ + snapshot?: boolean; } export function pack( @@ -963,15 +971,19 @@ export namespace io { ); // Generate document.grida1 (JSON snapshot) from document (for migration purposes) - const { - images: _images, - bitmaps: _bitmaps, - ...persistedDocument - } = document; - const snapshotJson = io.snapshot.stringify({ - version: schemaVersion, - document: persistedDocument, - }); + const includeSnapshot = options?.snapshot !== false; + let snapshotJson: string | undefined; + if (includeSnapshot) { + const { + images: _images, + bitmaps: _bitmaps, + ...persistedDocument + } = document; + snapshotJson = io.snapshot.stringify({ + version: schemaVersion, + document: persistedDocument, + }); + } const manifest: Manifest = { document_file: "document.grida", @@ -995,7 +1007,9 @@ export namespace io { const files: Record = { "manifest.json": strToU8(JSON.stringify(manifest)), "document.grida": fbBytes, - "document.grida1": strToU8(snapshotJson), + ...(snapshotJson && { + "document.grida1": strToU8(snapshotJson), + }), ...(images && Object.keys(images).length > 0 && { "images/": new Uint8Array() }), // Ensure folder exists }; From 17d6f87fa5556d3da143d2f8037f377a393483da Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 25 Mar 2026 04:17:25 +0900 Subject: [PATCH 19/31] feat(geometry): implement GeoInput struct and optimize geometry extraction - Added `GeoInput` struct, `GeoNodeKind` enum, and `RenderBoundsInfo` enum to improve geometry caching in `cache/geometry.rs`. - Introduced a two-phase geometry cache process: an O(n) extraction pass followed by a DFS pass, significantly reducing the time taken for depth-first searches in WASM. - Documented benchmark results showing a 94% reduction in geometry DFS time, although total geometry time remains unchanged due to extraction bottlenecks. - Proposed next steps for optimizing data layout at the scene graph level to enhance performance further. - Added a new research document on Chromium's node data layout strategies for rendering. --- .../feat-2d/wasm-load-scene-optimization.md | 102 ++++++ docs/wg/research/chromium/index.md | 2 +- docs/wg/research/chromium/node-data-layout.md | 338 ++++++++++++++++++ 3 files changed, 441 insertions(+), 1 deletion(-) create mode 100644 docs/wg/research/chromium/node-data-layout.md diff --git a/docs/wg/feat-2d/wasm-load-scene-optimization.md b/docs/wg/feat-2d/wasm-load-scene-optimization.md index 134050723a..db1a202aa0 100644 --- a/docs/wg/feat-2d/wasm-load-scene-optimization.md +++ b/docs/wg/feat-2d/wasm-load-scene-optimization.md @@ -157,3 +157,105 @@ just --justfile crates/grida-canvas-wasm/justfile build 3. Native benchmark: should not regress (target: `<800ms`) 4. WASM-on-Node benchmark: geometry stage should drop from ~4s to `<1s` 5. Visual: load yrr-main in browser debug embed, verify text renders correctly and pan/zoom/settle work + +--- + +## Results: GeoInput SoA Extraction (Implemented) + +### What was implemented + +`GeoInput` struct + `GeoNodeKind` enum + `RenderBoundsInfo` enum in +`cache/geometry.rs`. The geometry cache now runs in two phases: + +1. **O(n) extraction pass** — iterates `graph.nodes_iter()`, extracts + `GeoInput` (~56 bytes) per node into a `DenseNodeMap`. Text + measurement happens here. +2. **DFS pass** — operates on `DenseNodeMap` only. Never touches + the full `Node` enum. + +All 330 tests pass. No API changes. Single file modified. + +### Benchmark Results (yrr-main.grida, 136K nodes) + +**Native (release, 3-iteration average):** + +| Stage | Before (ms) | After (ms) | Delta | +| ---------------- | ----------- | ---------- | ----------- | +| geometry total | 121 | 130 | +7% (noise) | +| geometry extract | — | 121 | (new) | +| geometry DFS | ~121 | 7 | **-94%** | +| total | 796 | 706 | -11% | + +**WASM-on-Node (release):** + +| Stage | Before (ms) | After (ms) | Delta | +| ---------------- | ----------- | ---------- | ------------ | +| geometry total | 4,017 | 4,668 | +16% (noise) | +| geometry extract | — | 4,661 | (new) | +| geometry DFS | ~4,017 | **5** | **-99.9%** | +| total | 10,484 | 12,500 | — | + +### Analysis + +The DFS optimization worked exactly as designed — **DFS dropped from +~4,000ms to 5ms in WASM** (800x improvement, 0.7x native ratio). Once +data is compact, WASM operates at near-native speed. + +However, the total geometry time is unchanged because the **extraction +pass inherits the same bottleneck**: it iterates `graph.nodes_iter()` +which yields `&Node` references into `Vec>` where each slot +is 500+ bytes. The sequential scan still touches ~65 MB of cold data in +WASM linear memory. + +The cost shifted from DFS to extraction. The root cause is confirmed: +**any iteration over the full `Node` enum is fundamentally cache-unfriendly +in WASM**, regardless of whether it's a DFS or a sequential scan. + +### Conclusion + +SoA extraction within geometry.rs is a dead end for total geometry time. +The extraction pass itself is the bottleneck, and it must touch the `Node` +enum. To eliminate this cost, the split must happen **upstream** — at scene +graph construction time — so that geometry-relevant data is never stored +inside the monolithic `Node` enum in the first place. + +See [docs/wg/research/chromium/node-data-layout.md](../../research/chromium/node-data-layout.md) +for research on Chromium's property tree architecture, which solves +exactly this problem by storing properties in separate flat arrays indexed +by integer IDs. + +## Next Steps: Property Split at Scene Graph Level + +The GeoInput experiment proved the hypothesis: **compact data = fast WASM**. +The remaining question is where to split: + +### Option A: Split at SceneGraph construction + +Populate `DenseNodeMap`, `DenseNodeMap`, etc. +during FBS decode / JSON parse. The `Node` enum remains for painter and +export but hot loops (geometry, layers, effects) use the split maps. + +- Pro: Incremental migration, no format changes +- Con: Dual storage during transition + +### Option B: Reshape the FBS schema + +Store geometry-relevant fields in a separate FBS table. Decode directly +into split maps without materializing the full `Node`. + +- Pro: Minimal memory (no dual storage), aligned end-to-end +- Con: Format migration, breaks existing .grida files + +### Option C: Full ECS + +Replace `Node` enum with entity-component storage (e.g., archetype-based). + +- Pro: Maximum flexibility for future component shapes +- Con: Highest complexity, archetype migration overhead for common editing + operations, tree traversal requires indirection + +**Recommendation: Option A** (split at SceneGraph) as the incremental path, +with Option B as the long-term goal once the split maps stabilize. + +See [docs/wg/research/chromium/node-data-layout.md](../../research/chromium/node-data-layout.md) +for the full analysis including ECS tradeoffs and mutation considerations. diff --git a/docs/wg/research/chromium/index.md b/docs/wg/research/chromium/index.md index 8b98700f29..9015f5fa79 100644 --- a/docs/wg/research/chromium/index.md +++ b/docs/wg/research/chromium/index.md @@ -6,7 +6,6 @@ tags: - chromium - rendering - compositing - --- # Chromium Compositor Research @@ -36,6 +35,7 @@ material when designing rendering systems that face similar problems. | [resolution-scaling-during-interaction.md](./resolution-scaling-during-interaction.md) | Source-level: pinch-zoom raster scale, stale-tile reuse, CoverageIterator | | [pinch-zoom-deep-dive.md](./pinch-zoom-deep-dive.md) | Pinch-zoom: GPU tile stretching, anchor point, settle/refine, data flow | | [effect-optimizations.md](./effect-optimizations.md) | Effect optimization: filter demotion, render pass bypass, damage tracking | +| [node-data-layout.md](./node-data-layout.md) | Node data layout: DOM RareData, compositor property trees, ECS comparison | | [svg-pattern.md](./svg-pattern.md) | SVG `` paint server semantics, Chromium/resvg/Skia comparison | ## Source locations diff --git a/docs/wg/research/chromium/node-data-layout.md b/docs/wg/research/chromium/node-data-layout.md new file mode 100644 index 0000000000..8e3da6ac2a --- /dev/null +++ b/docs/wg/research/chromium/node-data-layout.md @@ -0,0 +1,338 @@ +--- +title: "Chromium Node Data Layout for Rendering" +format: md +tags: + - internal + - research + - chromium + - rendering + - performance + - data-layout +--- + +# Chromium Node Data Layout for Rendering + +How Chromium stores and accesses per-node data across its rendering +pipeline, with focus on data layout strategies that determine cache +locality and iteration cost during compositing and property propagation. + +See [property-trees.md](./property-trees.md) for the full property tree +structure. This document focuses on **why** the data is split that way +and how the pattern extends to DOM/SVG storage and mutation. + +--- + +## Three Storage Tiers + +Chromium uses three distinct tiers of per-node storage, each optimized +for different access patterns: + +### Tier 1: DOM Objects (Blink) — Monolithic with RareData + +DOM nodes (`Element`, `SVGElement`, `LayoutObject`) are heap-allocated +objects. Each contains all properties for that node type. To manage size, +Blink factors rarely-used properties into lazily-allocated `RareData` +objects: + +``` +SVGElement (always allocated): + class_name_: Member + svg_rare_data_: Member // null until needed + + inherited Element fields (~100+ bytes) + +SVGElementRareData (allocated on demand): + animated_sms_style_properties_ + presentation_attribute_style_ + ... +``` + +The same pattern appears in `LayoutObject` (`LayoutObjectRareData`) and +`LayerImpl` (`RareProperties`). + +Source: `third_party/blink/renderer/core/svg/svg_element.h`, +`third_party/blink/renderer/core/layout/layout_object.h` + +**Key insight:** Blink tolerates monolithic objects at the DOM layer +because DOM operations are infrequent relative to compositor-driven +rendering. The performance-critical path is in the compositor, which +uses a different layout. + +### Tier 2: Compositor Layers — Thin Index Carriers + +Each compositor layer (`LayerImpl`) stores minimal data plus four integer +indices into the property trees: + +``` +LayerImpl (~100 bytes hot data): + bounds_: gfx::Size // 8 bytes + offset_to_transform_parent_: gfx::Vector2dF // 8 bytes + transform_tree_index_: int // 4 bytes + effect_tree_index_: int // 4 bytes + clip_tree_index_: int // 4 bytes + scroll_tree_index_: int // 4 bytes + draw_properties_: DrawProperties // computed cache + element_id_: ElementId // 16 bytes + + bitfields (~4 bytes) + rare_properties_: unique_ptr // cold, heap-allocated +``` + +A layer does not own its transform, effect, or clip data. It references +shared property tree nodes. Multiple sibling layers with the same +transform parent share a single `TransformNode`. + +Source: `cc/layers/layer_impl.h` + +### Tier 3: Property Trees — SoA by Domain + +Properties are stored in four flat `std::vector` arrays, one per +domain: + +| Array | Element Type | Approx Size/Element | What Iterates It | +| ----------------------- | --------------- | ------------------- | ----------------------- | +| `TransformTree::nodes_` | `TransformNode` | ~200 bytes | `UpdateAllTransforms()` | +| `EffectTree::nodes_` | `EffectNode` | ~120 bytes | `ComputeEffects()` | +| `ClipTree::nodes_` | `ClipNode` | ~80 bytes | `ComputeClips()` | +| `ScrollTree::nodes_` | `ScrollNode` | ~60 bytes | Scroll handling | + +Plus a parallel cache vector for computed results: + +| Array | Element Type | Approx Size/Element | Purpose | +| ----------------------------- | ------------------------- | ------------------- | -------------------------- | +| `TransformTree::cached_data_` | `TransformCachedNodeData` | ~136 bytes | `to_screen`, `from_screen` | + +Each rendering pipeline step walks **one** property tree contiguously. +`UpdateAllTransforms()` reads `TransformNode.local`/`to_parent` and +writes `TransformCachedNodeData.to_screen` — both are sequential vector +accesses. This is cache-friendly: the working set is one input vector + +one output vector. + +Source: `cc/trees/property_tree.h`, `cc/trees/transform_node.h` + +--- + +## Why This Layout Works + +### Transform Propagation + +Before property trees, Chromium stored all properties on layers and walked +the layer tree to propagate transforms. The `CalculateDrawProperties()` +function was one of the largest performance bottlenecks because each layer +had 50+ fields but transform propagation only needed 3-4. + +After the property tree refactor: + +| Metric | Layer-Walk (old) | Property Tree (current) | +| ----------------------- | ---------------------------- | -------------------------- | +| Data per node | ~500 bytes (full layer) | ~200 bytes (TransformNode) | +| Working set (1K layers) | ~500 KB | ~200 KB | +| Cache lines touched | ~8 per node | ~3 per node | +| Other properties loaded | All (paints, clips, effects) | None | + +The key: **separation by access pattern**. Transform propagation never +touches effect data. Effect computation never touches clip data. Each +stage loads only what it needs. + +### Shared Nodes + +Property trees have **fewer nodes** than the layer tree. Common case: + +- 1000 layers might reference only 200 transform nodes (sibling groups + share parents) +- An opacity change on a container creates one `EffectNode` referenced by + all descendant layers, not N copies + +This sharing reduces both storage and propagation cost. + +--- + +## Mutation and Incremental Update + +Property trees are **persistent across frames** and support efficient +single-node mutation. + +### Mutation Flow + +1. **Mutate**: Write to the property node field + set dirty flag + + ``` + TransformNode: + needs_local_transform_update: bool // dirty flag + transform_changed_: bool // change tracking + damage_reasons_: DamageReasonSet // why it changed + ``` + +2. **Propagate**: Next frame, `UpdateAllTransforms()` walks the flat + vector top-down. For each node: + - If `needs_local_transform_update`: recompute `to_parent` from + `local`, `origin`, `scroll_offset`, `post_translation` + - Always recompute `to_screen = parent.to_screen * to_parent` (cached) + - If `transform_changed_`: propagate change flag to descendants for + damage tracking + +3. **Damage**: Changed flags feed into `DamageTracker` which determines + which render surfaces need redraw. + +### Compositor-Thread Animations + +For animated properties (transform, opacity), Chromium avoids the main +thread entirely: + +``` +Main Thread → commit → Pending Tree → activation → Active Tree + ↑ + MutatorHost drives + animations directly +``` + +The compositor thread mutates `TransformNode.local` and +`EffectNode.opacity` directly on the active tree. Scroll offsets are +similarly dual-tracked via `SyncedScrollOffsetMap` (main-thread value + +impl-thread value). + +### Single-Node Mutation Cost + +| Operation | Cost | Notes | +| ------------------------- | --------------- | --------------------------- | +| Set transform on one node | O(1) | Write field + set dirty bit | +| Propagate transforms | O(tree_size) | Sequential vector walk | +| Re-propagate only subtree | Not implemented | Chromium walks full tree | +| Add/remove property node | O(1) amortized | Vector push/pop | + +Chromium does not implement subtree-scoped propagation because web page +property trees are typically small (100-500 nodes). For scenes with +significantly larger property trees (tens of thousands of nodes), +subtree-scoped propagation would be a worthwhile extension. + +Source: `cc/trees/transform_node.h` (lines 26-183), +`cc/trees/property_tree.h` (`UpdateAllTransforms`) + +--- + +## Blink SVG: Where Monolithic Storage Hurts + +SVG elements store all properties on the DOM object. Each `SVGElement` +inherits from `Element` (which inherits from `Node`) and adds SVG-specific +data. An SVG `` carries: + +- Transform (presentation attribute or CSS) +- Geometry (`x`, `y`, `width`, `height`, `rx`, `ry`) +- Paint (fill, stroke, opacity) +- Effects (filter, clip-path, mask) +- Layout state + +During SVG rendering, Blink resolves styles and paints for each element, +touching all fields even when only a subset is needed. Blink mitigates +this via: + +1. **Style sharing**: Resolved styles are shared between elements with + identical computed values (`ComputedStyle` is reference-counted) +2. **Paint invalidation**: Only elements with changed properties are + re-painted (invalidation rect tracking) +3. **Hardware acceleration**: SVG elements with `will-change: transform` + or CSS animations are promoted to compositor layers, which then use + the property tree architecture + +For SVG without compositor promotion, Blink does pay the monolithic-object +cost. This is a known performance issue for complex SVG content. + +Source: `third_party/blink/renderer/core/svg/svg_element.h`, +`third_party/blink/renderer/core/layout/svg/` + +--- + +## Comparison: ECS vs Property Trees + +Game engines (Bevy, Unity DOTS) use Entity-Component-System (ECS) as an +alternative data layout strategy. Both ECS and property trees achieve +SoA-style access, but with different tradeoffs. + +| Aspect | Property Trees (Chromium) | ECS (Bevy) | +| --------------------- | --------------------------------------- | ------------------------------------------------- | +| Storage | Flat `Vec` per property domain | Archetype tables (SoA within archetype) | +| Access | Direct integer index into vector | Query over matching archetypes | +| Hierarchy | `parent_id` field in each node | `ChildOf` component + `Children` | +| Node sharing | Siblings share property nodes | No sharing; each entity owns components | +| Mutation | Write field + dirty flag | Write component (change detection) | +| Adding properties | Insert into the relevant vector | Archetype migration (entity moves between tables) | +| Removing properties | Remove from the relevant vector | Archetype migration | +| Transform propagation | Sequential top-down vector walk | Parallel DFS with work-stealing | +| Sparse data | Dense vector (unused slots waste space) | Sparse: only entities with component are stored | +| Complexity | Low (flat arrays + indices) | High (archetype bookkeeping, query resolution) | + +### ECS Archetype Migration + +When a component is added or removed from an entity in ECS, the entity +must migrate between archetype tables (because the storage layout +changes). This involves copying all component data to the new table. +For example, adding a drop shadow to a shape would trigger archetype +migration (moving the entity from `[Transform, Style, Geometry]` to +`[Transform, Style, Geometry, Effects]`). + +Property trees avoid this: adding an effect to a node creates an +`EffectNode` in the effect tree and sets `effect_tree_index_` on the +layer. No data movement for other properties. + +### Suitability + +Property trees are better suited for rendering engines with: + +- Stable component shapes (most nodes have the same set of properties) +- Tree-structured hierarchical propagation +- Frequent single-property mutations (animation, interaction) +- Need for property sharing between nodes + +ECS is better suited for: + +- Highly heterogeneous entities (wildly different component sets) +- Flat iteration over specific component combinations +- Dynamic component addition/removal as a core operation + +For scene graphs that resemble a design tool or document renderer — +stable node types, tree-structured transforms, frequent interactive +edits — the property tree model is a better fit. + +--- + +## Key Takeaways + +1. **Split by access pattern, not by identity.** Transform propagation + should only touch transform data. Effect computation should only touch + effect data. Storing all properties in one object forces every pipeline + stage to load irrelevant data. + +2. **Flat contiguous arrays.** Property trees store each domain in a + dense `std::vector` with O(1) index access. Sequential top-down + walks get full benefit of hardware prefetching. + +3. **Shared property nodes reduce tree size.** Sibling layers with the + same transform parent share a single `TransformNode`. The property + tree is often 5-10x smaller than the layer tree. + +4. **Persistent trees with dirty flags.** Trees are not rebuilt from + scratch each frame. Single-node mutation is O(1) (write + dirty bit), + propagation is O(tree_size) via sequential vector walk. + +5. **Monolithic objects are tolerated only where iteration is rare.** + Blink's DOM objects are monolithic because style resolution and paint + are per-element operations with invalidation. The compositor, which + must walk all layers every frame, uses split property trees. + +6. **RareData factoring is a partial mitigation.** Lazily-allocated + cold-data objects reduce the base object size but do not help with + iteration cost — the hot-data portion is still interleaved with + pointers and padding in the base object. + +--- + +## Source Files Referenced + +- `third_party/blink/renderer/core/svg/svg_element.h` +- `third_party/blink/renderer/core/layout/layout_object.h` +- `cc/layers/layer_impl.h` +- `cc/trees/property_tree.h` +- `cc/trees/transform_node.h` +- `cc/trees/effect_node.h` +- `cc/trees/clip_node.h` +- `cc/trees/scroll_node.h` +- `cc/trees/draw_property_utils.h` +- `cc/trees/draw_property_utils.cc` From d9032619c514d620318be72d2c38921be54670c5 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 25 Mar 2026 04:21:45 +0900 Subject: [PATCH 20/31] refactor(fig2grida): optimize document merging with shared buffers - Replaced per-page merging logic with a more efficient approach that utilizes shared buffers, reducing the need for Object.assign operations. - Updated the `MergedDocument` structure to use a Set for image references and added a node count property for better tracking. - Introduced a new `buildMergedDocument` function to streamline the merging process of canvases into a single document. - Enhanced the `FactoryContext` to support shared nodes, links, and image references, improving performance during document creation. - Added a new `mergeFigPages` function to handle merging results from .fig files, leveraging the shared buffer strategy for efficiency. --- .../grida-canvas-io-figma/fig2grida-core.ts | 340 +++++++++--------- packages/grida-canvas-io-figma/lib.ts | 41 ++- 2 files changed, 202 insertions(+), 179 deletions(-) diff --git a/packages/grida-canvas-io-figma/fig2grida-core.ts b/packages/grida-canvas-io-figma/fig2grida-core.ts index 2179a220ca..255dae2278 100644 --- a/packages/grida-canvas-io-figma/fig2grida-core.ts +++ b/packages/grida-canvas-io-figma/fig2grida-core.ts @@ -109,106 +109,105 @@ function tryReadRestArchiveZip( // Shared merge logic // --------------------------------------------------------------------------- -interface PageResult { - name: string; - result: iofigma.restful.factory.FigmaImportResult; -} - interface MergedDocument { document: grida.program.document.Document; imageRecord: Record; - imageRefsUsed: string[]; + imageRefsUsed: Set; pageNames: string[]; + /** Non-scene node count, accumulated during conversion. */ + nodeCount: number; } /** - * Merge per-page conversion results into a single multi-scene Document and - * collect referenced image bytes. + * Build a multi-scene Document by converting canvases directly into shared + * buffers, avoiding per-root / per-page Object.assign merge passes. */ -function mergePages( - pageResults: PageResult[], +function buildMergedDocument( + canvases: Array<{ + name: string; + children: Array>; + backgroundColor?: { r: number; g: number; b: number; a: number }; + }>, + context: iofigma.restful.factory.FactoryContext, imageProvider: (ref: string) => Uint8Array | undefined ): MergedDocument { - const allImageRefsUsed = new Set(); - const mergedNodes: Record = {}; - const mergedLinks: Record = {}; - const mergedImages: Record = {}; - const mergedBitmaps: Record = {}; - const mergedProperties: Record = {}; + // Single shared buffers — factory.document() writes directly here. + const sharedNodes: Record = {}; + const sharedLinks: Record = {}; + const sharedImageRefsUsed = new Set(); + const sharedFigmaIdMap = new Map(); + const scenesRef: string[] = []; + const pageNames: string[] = []; + let nodeCount = 0; + + const sharedContext: iofigma.restful.factory.FactoryContext = { + ...context, + _shared_nodes: sharedNodes, + _shared_links: sharedLinks, + _shared_image_refs_used: sharedImageRefsUsed, + _shared_figma_id_map: sharedFigmaIdMap, + }; - for (const { name, result } of pageResults) { - const packed = result.document; + for (const canvas of canvases) { + const background_color = canvas.backgroundColor + ? kolor.colorformats.newRGBA32F( + canvas.backgroundColor.r, + canvas.backgroundColor.g, + canvas.backgroundColor.b, + canvas.backgroundColor.a + ) + : undefined; + + const sceneId = makeIdGenerator("scene")(); + const childrenRefs: string[] = []; + + // Count nodes before this page so we can compute delta + const nodesBefore = Object.keys(sharedNodes).length; + + for (const rootNode of canvas.children) { + const result = iofigma.restful.factory.document( + rootNode as any, + {}, + sharedContext + ); + // factory.document() already wrote nodes/links/imageRefs into shared + // buffers. We only need the scene's children_refs from the result. + childrenRefs.push(...result.document.scene.children_refs); + } - const sceneId = - packed.scene.id === "tmp" ? makeIdGenerator("scene")() : packed.scene.id; + nodeCount += Object.keys(sharedNodes).length - nodesBefore; const sceneNode: grida.program.nodes.SceneNode = { type: "scene", id: sceneId, - name: name, + name: canvas.name, active: true, locked: false, - constraints: packed.scene.constraints, - guides: packed.scene.guides, - edges: packed.scene.edges, - background_color: packed.scene.background_color, + constraints: { children: "multiple" }, + guides: [], + edges: [], + background_color, }; - mergedNodes[sceneId] = sceneNode; - mergedLinks[sceneId] = packed.scene.children_refs; + (sharedNodes as any)[sceneId] = sceneNode; + sharedLinks[sceneId] = childrenRefs; scenesRef.push(sceneId); - - Object.assign(mergedNodes, packed.nodes); - for (const [key, value] of Object.entries(packed.links)) { - if (key !== sceneId) { - mergedLinks[key] = value; - } - } - Object.assign(mergedImages, packed.images); - Object.assign(mergedBitmaps, packed.bitmaps); - Object.assign(mergedProperties, packed.properties); - - for (const ref of result.imageRefsUsed) { - allImageRefsUsed.add(ref); - } - } - - // Prune orphan nodes - const reachable = new Set(scenesRef); - const queue = [...scenesRef]; - while (queue.length > 0) { - const id = queue.pop()!; - const children = mergedLinks[id]; - if (children) { - for (const childId of children) { - if (!reachable.has(childId)) { - reachable.add(childId); - queue.push(childId); - } - } - } - } - for (const id of Object.keys(mergedNodes)) { - if (!reachable.has(id)) { - delete mergedNodes[id]; - delete mergedLinks[id]; - } + pageNames.push(canvas.name); } const document: grida.program.document.Document = { - nodes: mergedNodes, - links: mergedLinks, - images: mergedImages, - bitmaps: mergedBitmaps, - properties: mergedProperties, + nodes: sharedNodes, + links: sharedLinks, + images: {}, + bitmaps: {}, + properties: {}, scenes_ref: scenesRef, entry_scene_id: scenesRef[0], }; - const imageRefsUsed = Array.from(allImageRefsUsed); const imageRecord: Record = {}; - for (const ref of imageRefsUsed) { + for (const ref of sharedImageRefsUsed) { const imageBytes = imageProvider(ref); if (imageBytes) { imageRecord[ref] = imageBytes; @@ -218,8 +217,9 @@ function mergePages( return { document, imageRecord, - imageRefsUsed, - pageNames: pageResults.map((p) => p.name), + imageRefsUsed: sharedImageRefsUsed, + pageNames, + nodeCount, }; } @@ -231,14 +231,11 @@ function packMergedDocument(merged: MergedDocument): Fig2GridaResult { undefined, { level: 0, snapshot: false } ); - const nodeCount = Object.keys(merged.document.nodes).filter( - (id) => merged.document.nodes[id]?.type !== "scene" - ).length; return { bytes: archiveBytes, pageNames: merged.pageNames, - nodeCount, + nodeCount: merged.nodeCount, imageCount: Object.keys(merged.imageRecord).length, }; } @@ -310,6 +307,88 @@ export function fig2grida( // .fig bytes path // --------------------------------------------------------------------------- +interface FigPageResult { + name: string; + result: iofigma.restful.factory.FigmaImportResult; +} + +/** + * Merge per-page results from the .fig/Kiwi path into a single Document. + * (The REST path uses buildMergedDocument with shared buffers instead.) + */ +function mergeFigPages( + pageResults: FigPageResult[], + imageProvider: (ref: string) => Uint8Array | undefined +): MergedDocument { + const allImageRefsUsed = new Set(); + const mergedNodes: Record = {}; + const mergedLinks: Record = {}; + const scenesRef: string[] = []; + let nodeCount = 0; + + for (const { name, result } of pageResults) { + const packed = result.document; + + const sceneId = + packed.scene.id === "tmp" ? makeIdGenerator("scene")() : packed.scene.id; + + const sceneNode: grida.program.nodes.SceneNode = { + type: "scene", + id: sceneId, + name: name, + active: true, + locked: false, + constraints: packed.scene.constraints, + guides: packed.scene.guides, + edges: packed.scene.edges, + background_color: packed.scene.background_color, + }; + + mergedNodes[sceneId] = sceneNode; + mergedLinks[sceneId] = packed.scene.children_refs; + scenesRef.push(sceneId); + + const pageSizeBefore = Object.keys(mergedNodes).length; + Object.assign(mergedNodes, packed.nodes); + for (const [key, value] of Object.entries(packed.links)) { + if (key !== sceneId) { + mergedLinks[key] = value; + } + } + nodeCount += Object.keys(mergedNodes).length - pageSizeBefore; + + for (const ref of result.imageRefsUsed) { + allImageRefsUsed.add(ref); + } + } + + const document: grida.program.document.Document = { + nodes: mergedNodes, + links: mergedLinks, + images: {}, + bitmaps: {}, + properties: {}, + scenes_ref: scenesRef, + entry_scene_id: scenesRef[0], + }; + + const imageRecord: Record = {}; + for (const ref of allImageRefsUsed) { + const imageBytes = imageProvider(ref); + if (imageBytes) { + imageRecord[ref] = imageBytes; + } + } + + return { + document, + imageRecord, + imageRefsUsed: allImageRefsUsed, + pageNames: pageResults.map((p) => p.name), + nodeCount, + }; +} + function fig2gridaFromFigBytes( input: Uint8Array, options?: Fig2GridaOptions @@ -327,7 +406,7 @@ function fig2gridaFromFigBytes( .map((i) => pages[i]); } - const pageResults: PageResult[] = []; + const pageResults: FigPageResult[] = []; for (const page of pages) { const placeholderForMissing = options?.placeholder_for_missing_images !== false; @@ -344,7 +423,7 @@ function fig2gridaFromFigBytes( } return packMergedDocument( - mergePages(pageResults, (ref) => extractedImages.get(ref)) + mergeFigPages(pageResults, (ref) => extractedImages.get(ref)) ); } @@ -452,7 +531,7 @@ function restJsonToMergedDocument( preserveFigmaIds?: boolean, preferFixedTextSizing?: boolean ): MergedDocument { - const canvases = extractCanvases(json); + const rawCanvases = extractCanvases(json); // A single shared context across all pages prevents ID collisions when // merging nodes from different canvases. @@ -472,17 +551,15 @@ function restJsonToMergedDocument( }), }; - const pageResults: PageResult[] = canvases.map((canvas) => ({ - name: canvas.name ?? "Page", - result: convertRootsToPackedScene( - canvas.name ?? "Page", - canvas.children ?? [], - canvas.backgroundColor, - context - ), + const canvases = rawCanvases.map((c) => ({ + name: c.name ?? "Page", + children: c.children ?? [], + backgroundColor: c.backgroundColor, })); - return mergePages(pageResults, (ref) => (images ? images[ref] : undefined)); + return buildMergedDocument(canvases, context, (ref) => + images ? images[ref] : undefined + ); } function fig2gridaFromRestJson( @@ -503,91 +580,6 @@ function fig2gridaFromRestJson( ); } -// --------------------------------------------------------------------------- -// Shared per-page conversion: root nodes → FigmaImportResult -// --------------------------------------------------------------------------- - -/** - * Convert an array of root nodes into a single packed scene document. - * Used by both the REST JSON path and the .fig Kiwi path (via - * `iofigma.kiwi.convertPageToScene`). - */ -function convertRootsToPackedScene( - name: string, - rootNodes: Array>, - backgroundColor: { r: number; g: number; b: number; a: number } | undefined, - context: iofigma.restful.factory.FactoryContext -): iofigma.restful.factory.FigmaImportResult { - const background_color = backgroundColor - ? kolor.colorformats.newRGBA32F( - backgroundColor.r, - backgroundColor.g, - backgroundColor.b, - backgroundColor.a - ) - : undefined; - - if (rootNodes.length === 0) { - return { - document: emptyPackedScene(name, background_color), - imageRefsUsed: [], - }; - } - - const individualResults = rootNodes.map((rootNode) => - iofigma.restful.factory.document(rootNode as any, {}, context) - ); - - const imageRefsUsed = new Set(); - for (const r of individualResults) { - for (const ref of r.imageRefsUsed) imageRefsUsed.add(ref); - } - - let packed: grida.program.document.IPackedSceneDocument; - if (individualResults.length === 1) { - packed = individualResults[0].document; - packed.scene.background_color = background_color; - } else { - packed = emptyPackedScene(name, background_color); - for (const { document: d } of individualResults) { - Object.assign(packed.nodes, d.nodes); - Object.assign(packed.links, d.links); - Object.assign(packed.images, d.images); - Object.assign(packed.bitmaps, d.bitmaps); - Object.assign(packed.properties, d.properties); - packed.scene.children_refs.push(...d.scene.children_refs); - } - } - - return { - document: packed, - imageRefsUsed: Array.from(imageRefsUsed), - }; -} - -function emptyPackedScene( - name: string, - background_color: ReturnType | undefined -): grida.program.document.IPackedSceneDocument { - return { - nodes: {}, - links: {}, - images: {}, - bitmaps: {}, - properties: {}, - scene: { - type: "scene", - id: "tmp", - name, - children_refs: [], - guides: [], - edges: [], - constraints: { children: "multiple" }, - background_color, - }, - }; -} - // --------------------------------------------------------------------------- // restJsonToGridaDocument — returns in-memory Document (no .grida packing) // --------------------------------------------------------------------------- @@ -635,6 +627,6 @@ export function restJsonToGridaDocument( return { document: merged.document, assets: merged.imageRecord, - imageRefsUsed: merged.imageRefsUsed, + imageRefsUsed: Array.from(merged.imageRefsUsed), }; } diff --git a/packages/grida-canvas-io-figma/lib.ts b/packages/grida-canvas-io-figma/lib.ts index 1fe363226c..67162a1934 100644 --- a/packages/grida-canvas-io-figma/lib.ts +++ b/packages/grida-canvas-io-figma/lib.ts @@ -473,6 +473,30 @@ export namespace iofigma { * @default false */ prefer_fixed_text_sizing?: boolean; + + // -- Shared buffers (performance) -- + // When provided, factory.document() writes directly into these + // pre-allocated collections instead of creating its own, eliminating + // the Object.assign merge passes in the caller. + + /** + * Shared nodes dictionary. When set, `factory.document()` inserts + * converted nodes here instead of allocating a local `nodes` object. + */ + _shared_nodes?: Record; + /** + * Shared links (parent→children) dictionary. + */ + _shared_links?: Record; + /** + * Shared mutable set for collecting image refs used across all roots. + */ + _shared_image_refs_used?: Set; + /** + * Shared Figma-ID → Grida-ID map. Avoids per-root Map allocations + * when converting many root nodes. + */ + _shared_figma_id_map?: Map; }; function toGradientPaint(paint: figrest.GradientPaint) { @@ -1022,12 +1046,15 @@ export namespace iofigma { images: { [key: string]: string }, context: FactoryContext ): FigmaImportResult { - const nodes: Record = {}; - const graph: Record = {}; - const imageRefsUsed = new Set(); + const nodes: Record = + context._shared_nodes ?? {}; + const graph: Record = context._shared_links ?? {}; + const imageRefsUsed: Set = + context._shared_image_refs_used ?? new Set(); // Map from Figma ID (ephemeral) to Grida ID (final) - const figma_id_to_grida_id = new Map(); + const figma_id_to_grida_id = + context._shared_figma_id_map ?? new Map(); // ID generator function - use provided generator or fallback let counter = 0; @@ -1465,7 +1492,11 @@ export namespace iofigma { return { document: packed, - imageRefsUsed: Array.from(imageRefsUsed), + // When shared buffers are in use, the caller reads imageRefsUsed + // from the shared Set directly — return empty to avoid copying. + imageRefsUsed: context._shared_image_refs_used + ? [] + : Array.from(imageRefsUsed), }; } From 58ca7917e1105a0ec7f3d3e7c6c81816085683b4 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 25 Mar 2026 04:59:42 +0900 Subject: [PATCH 21/31] feat(format): enhance FlatBuffer encoding with position encoding and sorting options - Removed the `generateNKeysBetween` function, replacing it with zero-padded integers for position encoding, improving performance and simplicity. - Introduced `ToFlatbufferOptions` interface to allow skipping deterministic sorting of node IDs, optimizing for write-once pipelines. - Updated `toFlatbuffer` function to accommodate new options and pre-size the FlatBuffers builder for efficiency. - Modified related functions to utilize the new position encoding strategy and sorting behavior. --- .../grida-canvas-io-figma/fig2grida-core.ts | 2 +- packages/grida-canvas-io/format.ts | 39 ++++++++++++++----- packages/grida-canvas-io/index.ts | 9 ++++- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/packages/grida-canvas-io-figma/fig2grida-core.ts b/packages/grida-canvas-io-figma/fig2grida-core.ts index 255dae2278..57f2060c64 100644 --- a/packages/grida-canvas-io-figma/fig2grida-core.ts +++ b/packages/grida-canvas-io-figma/fig2grida-core.ts @@ -229,7 +229,7 @@ function packMergedDocument(merged: MergedDocument): Fig2GridaResult { merged.imageRecord, undefined, undefined, - { level: 0, snapshot: false } + { level: 0, snapshot: false, skip_sort: true } ); return { diff --git a/packages/grida-canvas-io/format.ts b/packages/grida-canvas-io/format.ts index a4b0495cad..c243321cbf 100644 --- a/packages/grida-canvas-io/format.ts +++ b/packages/grida-canvas-io/format.ts @@ -5,7 +5,7 @@ import * as fbs from "@grida/format"; import { unionToPaint, unionToNode, unionToFeBlur } from "@grida/format"; import type { vn } from "@grida/schema"; import * as flatbuffers from "flatbuffers"; -import { generateNKeysBetween } from "@grida/sequence"; +// generateNKeysBetween removed — replaced by zero-padded integers for position encoding. type Builder = flatbuffers.Builder; @@ -4516,11 +4516,28 @@ export namespace format { * @param document - The TS IR document to encode * @returns Uint8Array containing the FlatBuffers binary data */ + export interface ToFlatbufferOptions { + /** + * When true, skip sorting node IDs for deterministic output. + * Safe for write-once pipelines (e.g. fig2grida) where the + * consumer does not depend on node order in the FlatBuffer vector. + * @default false + */ + skipSort?: boolean; + } + export function toFlatbuffer( document: grida.program.document.Document, - schemaVersion: string = grida.program.document.SCHEMA_VERSION + schemaVersion: string = grida.program.document.SCHEMA_VERSION, + options?: ToFlatbufferOptions ): Uint8Array { - const builder = new flatbuffers.Builder(1024); + // Pre-size the builder to reduce doubling + copy during growth. + // We avoid the full final size (which can be 80MB+ and slow to + // allocate in one shot) and instead start at roughly half the + // expected output so the builder only needs ~1 resize. + const nodeCount = Object.keys(document.nodes || {}).length; + const initialSize = Math.max(nodeCount * 256, 256 * 1024); + const builder = new flatbuffers.Builder(initialSize); // Build schema version const schemaVersionOffset = builder.createString(schemaVersion); @@ -4541,27 +4558,31 @@ export namespace format { } } - // Generate position strings for each parent's children + // Generate position strings for each parent's children. + // We use zero-padded integers ("000000", "000001", …) which are + // trivially lexicographically sortable and nearly free to generate, + // replacing the costly fractional-index algorithm. const nodeToParentRef = new Map< string, { parentId: string; position: string } >(); for (const [parentId, children] of parentToChildrenMap.entries()) { if (children.length === 0) continue; - // Generate position strings for all children - const positions = generateNKeysBetween(null, null, children.length); + const pad = Math.max(6, String(children.length - 1).length); for (let i = 0; i < children.length; i++) { nodeToParentRef.set(children[i]!, { parentId, - position: positions[i]!, + position: String(i).padStart(pad, "0"), }); } } // Encode nodes array (TS nodes map -> flat list) const nodeIds = Object.keys(document.nodes || {}); - // Deterministic ordering: sort by string id - nodeIds.sort(); + // Deterministic ordering: sort by string id (skippable for perf) + if (!options?.skipSort) { + nodeIds.sort(); + } const nodeSlotOffsets: flatbuffers.Offset[] = []; for (const nodeId of nodeIds) { diff --git a/packages/grida-canvas-io/index.ts b/packages/grida-canvas-io/index.ts index e6be7c1ebc..8435e077df 100644 --- a/packages/grida-canvas-io/index.ts +++ b/packages/grida-canvas-io/index.ts @@ -947,6 +947,12 @@ export namespace io { * @default true */ snapshot?: boolean; + /** + * When true, skip deterministic sorting of node IDs in the FlatBuffer. + * Safe for write-once pipelines where node order is irrelevant. + * @default false + */ + skip_sort?: boolean; } export function pack( @@ -967,7 +973,8 @@ export namespace io { images: {}, bitmaps: {}, }, - schemaVersion + schemaVersion, + { skipSort: options?.skip_sort } ); // Generate document.grida1 (JSON snapshot) from document (for migration purposes) From 7b5de6b741844e5c698461bbbf75dbeb67ac19b3 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 25 Mar 2026 05:20:11 +0900 Subject: [PATCH 22/31] refactor(format): optimize FlatBuffer encoding and transform handling - Replaced string creation with shared string creation for improved memory efficiency. - Updated rotation handling to compute transforms only when necessary, reducing unnecessary calculations. - Omitted empty dash array and VariableWidthProfile to streamline encoding, allowing the decoder to interpret null as absence. - Enhanced parent-child mapping logic for better performance during document processing. --- packages/grida-canvas-io/format.ts | 91 +++++++++++------------------- 1 file changed, 34 insertions(+), 57 deletions(-) diff --git a/packages/grida-canvas-io/format.ts b/packages/grida-canvas-io/format.ts index c243321cbf..17650b5692 100644 --- a/packages/grida-canvas-io/format.ts +++ b/packages/grida-canvas-io/format.ts @@ -439,7 +439,7 @@ export namespace format { builder: Builder, id: string ): flatbuffers.Offset { - const idOffset = builder.createString(id); + const idOffset = builder.createSharedString(id); return fbs.NodeIdentifier.createNodeIdentifier(builder, idOffset); } @@ -452,7 +452,7 @@ export namespace format { position: string ): flatbuffers.Offset { const parentIdOffset = structs.nodeIdentifier(builder, parentId); - const positionOffset = builder.createString(position); + const positionOffset = builder.createSharedString(position); fbs.ParentReference.startParentReference(builder); fbs.ParentReference.addParentId(builder, parentIdOffset); fbs.ParentReference.addPosition(builder, positionOffset); @@ -935,7 +935,7 @@ export namespace format { node: grida.program.nodes.Node ): flatbuffers.Offset { const idOffset = structs.nodeIdentifier(builder, node.id); - const nameOffset = builder.createString(node.name ?? ""); + const nameOffset = builder.createSharedString(node.name ?? ""); fbs.SystemNodeTrait.startSystemNodeTrait(builder); fbs.SystemNodeTrait.addId(builder, idOffset); @@ -1062,35 +1062,32 @@ export namespace format { } // 7. Post-layout transform (rotation as transform matrix) - // Convert rotation (degrees) to a rotation transform matrix + // Only compute trig and emit the transform when rotation is non-zero. const nodeWithRotation = node as grida.program.nodes.Node & Partial>; const rotationDegrees = nodeWithRotation.rotation ?? 0; - const rotationRad = (rotationDegrees * Math.PI) / 180; - const cos = Math.cos(rotationRad); - const sin = Math.sin(rotationRad); + if (rotationDegrees !== 0) { + const rotationRad = (rotationDegrees * Math.PI) / 180; + const cos = Math.cos(rotationRad); + const sin = Math.sin(rotationRad); - // Pure rotation matrix: [cos, -sin, 0], [sin, cos, 0] - const postLayoutTransformOffset = structs.cgTransform2D( - builder, - cos, // m00 - -sin, // m01 - 0, // m02 - sin, // m10 - cos, // m11 - 0 // m12 - ); - fbs.LayerTrait.addPostLayoutTransform( - builder, - postLayoutTransformOffset - ); + const postLayoutTransformOffset = structs.cgTransform2D( + builder, + cos, -sin, 0, + sin, cos, 0 + ); + fbs.LayerTrait.addPostLayoutTransform( + builder, + postLayoutTransformOffset + ); - // 8. Post-layout transform origin (default to center: 0, 0 in Alignment coordinates) - const transformOriginOffset = structs.alignment(builder, 0, 0); - fbs.LayerTrait.addPostLayoutTransformOrigin( - builder, - transformOriginOffset - ); + // 8. Post-layout transform origin (only needed when rotation is set) + const transformOriginOffset = structs.alignment(builder, 0, 0); + fbs.LayerTrait.addPostLayoutTransformOrigin( + builder, + transformOriginOffset + ); + } return fbs.LayerTrait.endLayerTrait(builder); } @@ -1332,10 +1329,6 @@ export namespace format { fbs.BasicShapeNodeType.Rectangle; // Helper to create StrokeStyle - const dashArrayOffset = fbs.StrokeStyle.createStrokeDashArrayVector( - builder, - [] - ); fbs.StrokeStyle.startStrokeStyle(builder); fbs.StrokeStyle.addStrokeCap( builder, @@ -1347,7 +1340,7 @@ export namespace format { ); fbs.StrokeStyle.addStrokeAlign(builder, fbs.StrokeAlign.Inside); fbs.StrokeStyle.addStrokeMiterLimit(builder, 4.0); - fbs.StrokeStyle.addStrokeDashArray(builder, dashArrayOffset); + // Skip empty dash array vector — decoder reads null as no dashes const strokeStyleOffset = fbs.StrokeStyle.endStrokeStyle(builder); // Encode paints as PaintStackItem arrays @@ -1364,15 +1357,8 @@ export namespace format { fbs.BasicShapeNode.createStrokePaintsVector ); - // Create VariableWidthProfile (empty for now - nodes don't have this in TS model) - const emptyStopsOffset = fbs.VariableWidthProfile.createStopsVector( - builder, - [] - ); - fbs.VariableWidthProfile.startVariableWidthProfile(builder); - fbs.VariableWidthProfile.addStops(builder, emptyStopsOffset); - const strokeWidthProfileOffset = - fbs.VariableWidthProfile.endVariableWidthProfile(builder); + // Skip VariableWidthProfile — nodes don't have this in TS model. + // Decoder reads null when the field is absent. // Encode corner_radius and rectangular properties // For rectangle, use rectangular_corner_radius; for others, use corner_radius @@ -1409,10 +1395,7 @@ export namespace format { builder, shapeNode.stroke_width ?? 0 ); - fbs.BasicShapeNode.addStrokeWidthProfile( - builder, - strokeWidthProfileOffset - ); + // strokeWidthProfile omitted — not in TS model, decoder reads null. // Create structs inline (must be done while table is being built) const rectangularCornerRadiusOffsetInline = shapeNode.type === "rectangle" @@ -2182,7 +2165,7 @@ export namespace format { ) => flatbuffers.Offset ): flatbuffers.Offset { if (!paints || paints.length === 0) { - return createVector(builder, []); + return 0; // No vector — decoder reads null/empty } const stackItemOffsets: flatbuffers.Offset[] = []; @@ -4531,29 +4514,24 @@ export namespace format { schemaVersion: string = grida.program.document.SCHEMA_VERSION, options?: ToFlatbufferOptions ): Uint8Array { + // Collect node IDs once — reused for pre-sizing and iteration. + const nodeIds = Object.keys(document.nodes || {}); + const nodeCount = nodeIds.length; + // Pre-size the builder to reduce doubling + copy during growth. - // We avoid the full final size (which can be 80MB+ and slow to - // allocate in one shot) and instead start at roughly half the - // expected output so the builder only needs ~1 resize. - const nodeCount = Object.keys(document.nodes || {}).length; const initialSize = Math.max(nodeCount * 256, 256 * 1024); const builder = new flatbuffers.Builder(initialSize); // Build schema version const schemaVersionOffset = builder.createString(schemaVersion); - // Build parent reference map: for each node, find its parent and generate position - // First, build a reverse map: childId -> parentId - const childToParentMap = new Map(); + // Build parent→children map from links for position generation. const parentToChildrenMap = new Map(); if (document.links) { for (const [parentId, children] of Object.entries(document.links)) { if (children && children.length > 0) { parentToChildrenMap.set(parentId, children); - for (const childId of children) { - childToParentMap.set(childId, parentId); - } } } } @@ -4578,7 +4556,6 @@ export namespace format { } // Encode nodes array (TS nodes map -> flat list) - const nodeIds = Object.keys(document.nodes || {}); // Deterministic ordering: sort by string id (skippable for perf) if (!options?.skipSort) { nodeIds.sort(); From 9c525f062b47f946dd24fcc10dfae948b7930cb7 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 25 Mar 2026 05:38:37 +0900 Subject: [PATCH 23/31] refactor(fig2grida): streamline benchmark tests and update encoding stages - Consolidated gzip decompression and JSON parsing into single lines for improved readability. - Renamed benchmark descriptions for clarity, changing "io.archive.pack sub-stages" to "encode sub-stages" and updating individual test names. - Removed redundant benchmarks for JSON.stringify and io.archive.pack, focusing on essential encoding and zipping tests. - Enhanced logging to reflect updated encoding sizes, improving performance tracking during benchmarks. --- .../__bench__/fig2grida.bench.ts | 118 +++++++++--------- 1 file changed, 58 insertions(+), 60 deletions(-) diff --git a/packages/grida-canvas-io-figma/__bench__/fig2grida.bench.ts b/packages/grida-canvas-io-figma/__bench__/fig2grida.bench.ts index 5b9a8494a9..348d564a5f 100644 --- a/packages/grida-canvas-io-figma/__bench__/fig2grida.bench.ts +++ b/packages/grida-canvas-io-figma/__bench__/fig2grida.bench.ts @@ -35,9 +35,7 @@ if (!HAS_FIXTURE) { const FIXTURE_GZ = HAS_FIXTURE ? readFileSync(FIXTURE_PATH) : null; -const decompressedBytes = FIXTURE_GZ - ? gunzipSync(FIXTURE_GZ) - : null; +const decompressedBytes = FIXTURE_GZ ? gunzipSync(FIXTURE_GZ) : null; const jsonString = decompressedBytes ? new TextDecoder().decode(decompressedBytes) : null; @@ -50,7 +48,6 @@ if (FIXTURE_GZ && decompressedBytes && jsonString && preConverted) { console.log( `[fixture] gz=${(FIXTURE_GZ.byteLength / 1024 / 1024).toFixed(1)}MB, ` + `decompressed=${(decompressedBytes.byteLength / 1024 / 1024).toFixed(1)}MB, ` + - `jsonString=${(jsonString.length / 1024 / 1024).toFixed(1)}MB, ` + `nodes=${nodeCount}` ); } @@ -60,22 +57,6 @@ if (FIXTURE_GZ && decompressedBytes && jsonString && preConverted) { // --------------------------------------------------------------------------- describe.skipIf(!HAS_FIXTURE)("fig2grida pipeline", () => { - bench( - "stage: gzip decompress", - () => { - gunzipSync(FIXTURE_GZ!); - }, - { iterations: 5, warmupIterations: 1 } - ); - - bench( - "stage: JSON.parse", - () => { - JSON.parse(jsonString!); - }, - { iterations: 5, warmupIterations: 1 } - ); - bench( "stage: restJsonToGridaDocument (convert + merge)", () => { @@ -92,14 +73,6 @@ describe.skipIf(!HAS_FIXTURE)("fig2grida pipeline", () => { { iterations: 3, warmupIterations: 1 } ); - bench( - "stage: io.archive.pack (level 6, default)", - () => { - io.archive.pack(preConverted!.document, preConverted!.assets); - }, - { iterations: 3, warmupIterations: 1 } - ); - bench( "stage: io.archive.pack (level 0, store)", () => { @@ -116,10 +89,10 @@ describe.skipIf(!HAS_FIXTURE)("fig2grida pipeline", () => { }); // --------------------------------------------------------------------------- -// Benchmarks — io.archive.pack sub-stages +// Benchmarks — encode sub-stages // --------------------------------------------------------------------------- -describe.skipIf(!HAS_FIXTURE)("io.archive.pack sub-stages", () => { +describe.skipIf(!HAS_FIXTURE)("encode sub-stages", () => { const docForFb = { ...preConverted!.document, images: {}, @@ -127,60 +100,85 @@ describe.skipIf(!HAS_FIXTURE)("io.archive.pack sub-stages", () => { }; bench( - "sub: toFlatbuffer", + "toFlatbuffer (default)", () => { format.document.encode.toFlatbuffer(docForFb); }, { iterations: 3, warmupIterations: 1 } ); - const { - images: _images, - bitmaps: _bitmaps, - ...persistedDocument - } = preConverted!.document; - const snapshotPayload = { - version: "1.0", - document: persistedDocument, - }; - bench( - "sub: JSON.stringify (snapshot)", + "toFlatbuffer (skipSort)", () => { - JSON.stringify(snapshotPayload); + format.document.encode.toFlatbuffer(docForFb, undefined, { + skipSort: true, + }); }, - { iterations: 5, warmupIterations: 1 } + { iterations: 3, warmupIterations: 1 } ); const fbBytes = format.document.encode.toFlatbuffer(docForFb); - const snapshotJson = JSON.stringify(snapshotPayload); - - const files: Record = { - "manifest.json": strToU8( - JSON.stringify({ document_file: "document.grida", version: "1.0" }) - ), - "document.grida": fbBytes, - "document.grida1": strToU8(snapshotJson), - }; bench( - "sub: zipSync (level 6)", + "zipSync (level 0)", () => { - zipSync(files); + zipSync( + { + "manifest.json": strToU8( + JSON.stringify({ + document_file: "document.grida", + version: "1.0", + }) + ), + "document.grida": fbBytes, + }, + { level: 0 } + ); }, { iterations: 5, warmupIterations: 1 } ); bench( - "sub: zipSync (level 0)", + "zipSync (level 6)", () => { - zipSync(files, { level: 0 }); + zipSync( + { + "manifest.json": strToU8( + JSON.stringify({ + document_file: "document.grida", + version: "1.0", + }) + ), + "document.grida": fbBytes, + }, + { level: 6 } + ); }, - { iterations: 5, warmupIterations: 1 } + { iterations: 3, warmupIterations: 1 } ); console.log( - `[pack sizes] flatbuffer=${(fbBytes.byteLength / 1024 / 1024).toFixed(1)}MB, ` + - `snapshot=${(snapshotJson.length / 1024 / 1024).toFixed(1)}MB` + `[encode sizes] flatbuffer=${(fbBytes.byteLength / 1024 / 1024).toFixed(1)}MB` + ); +}); + +// --------------------------------------------------------------------------- +// Benchmarks — decode (round-trip: encode once, then measure decode) +// --------------------------------------------------------------------------- + +describe.skipIf(!HAS_FIXTURE)("decode", () => { + const docForFb = { + ...preConverted!.document, + images: {}, + bitmaps: {}, + }; + const fbBytes = format.document.encode.toFlatbuffer(docForFb); + + bench( + "fromFlatbuffer", + () => { + format.document.decode.fromFlatbuffer(fbBytes); + }, + { iterations: 5, warmupIterations: 1 } ); }); From 2640c912f8eff1181fd8e58269bb44ae18529a95 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 25 Mar 2026 14:13:02 +0900 Subject: [PATCH 24/31] perf(cg): optimize WASM load_scene with property-split SoA and allocation reduction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduce syncDocument time for 136K-node scenes (yrr-main.grida) by applying the property-split pattern to layers, effects, and layout hot paths — same approach that cut geometry DFS from 4,000ms to 16ms. Key changes: 1. Faster hashers (rustc-hash): Replace std HashMap with FxHashMap in the FBS decode hot path for string_to_internal_id, children_by_parent, and position_map. ~5-10% decode speedup for 136K entries. 2. NodeLayerCore extraction: Add compact ~20-byte Copy struct with active/opacity/blend_mode/mask/clips_content/has_effects/node_type/ is_flex, stored in DenseNodeMap on SceneGraph. Effect tree and layers DFS read this instead of the full 500+ byte Node enum for visibility and dispatch checks. 3. Layout engine optimization: Use NodeLayerCore for is_layout_node and is_flex_container checks, NodeGeoData for schema position/size reads in extract_all_layouts. Eliminates full-Node reads for ~90% of nodes in the layout extraction phase. 4. SmallVec for Paints: Change Paints internal storage from Vec to SmallVec<[Paint; 1]>. Most nodes have 0-1 fills/strokes, avoiding ~272K heap allocations for a 136K-node scene. 5. Geometry resolve_layout robustness: Replace .expect() with graceful fallback to schema data for Container nodes missing layout results (orphan nodes not reachable from scene roots). WASM-on-Node benchmark (yrr-main.grida, 136K nodes): switch phase: 6,000ms → 2,668ms (-55%) total (load+switch): ~10,200ms → ~7,241ms (-29%) Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 5 + crates/grida-canvas/Cargo.toml | 4 + crates/grida-canvas/src/cache/geometry.rs | 811 +++++---- crates/grida-canvas/src/cg/types.rs | 35 +- crates/grida-canvas/src/io/io_grida_fbs.rs | 1505 +++++++++++------ crates/grida-canvas/src/layout/engine.rs | 149 +- crates/grida-canvas/src/node/scene_graph.rs | 420 ++++- crates/grida-canvas/src/node/schema.rs | 210 +++ crates/grida-canvas/src/painter/layer.rs | 12 +- .../grida-canvas/src/runtime/effect_tree.rs | 106 +- crates/grida-canvas/src/window/application.rs | 10 +- 11 files changed, 2146 insertions(+), 1121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 61edb15e4e..2ac168e199 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -495,10 +495,12 @@ dependencies = [ "rendiff", "reqwest", "rstar", + "rustc-hash", "seahash", "serde", "serde_json", "skia-safe", + "smallvec", "taffy", "tokio", "usvg", @@ -3949,6 +3951,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "smithay-client-toolkit" diff --git a/crates/grida-canvas/Cargo.toml b/crates/grida-canvas/Cargo.toml index 9066a3b623..bb62b98218 100644 --- a/crates/grida-canvas/Cargo.toml +++ b/crates/grida-canvas/Cargo.toml @@ -33,6 +33,10 @@ rstar = "0.12" # core resource hashing seahash = "4.1.0" +# fast non-cryptographic hasher for hot-path HashMaps +rustc-hash = "2" +# inline small collections (avoids heap alloc for common 0-1 element cases) +smallvec = { version = "1", features = ["serde"] } # layout engine taffy = "0.9.2" # svg parsing diff --git a/crates/grida-canvas/src/cache/geometry.rs b/crates/grida-canvas/src/cache/geometry.rs index b87e63d9d3..e1196ba601 100644 --- a/crates/grida-canvas/src/cache/geometry.rs +++ b/crates/grida-canvas/src/cache/geometry.rs @@ -9,16 +9,44 @@ //! - Consumes LayoutResult as immutable input from LayoutEngine //! - Missing layout for Inset nodes is a PANIC (LayoutEngine bug) //! - Missing geometry entry when accessed is a PANIC (GeometryCache bug) +//! +//! ## Property Split Optimization +//! +//! The `SceneGraph` stores compact `NodeGeoData` (~48 bytes/node) alongside +//! the full `Node` enum. This module resolves layout into a `GeoInput` struct +//! by reading only from `NodeGeoData` + `LayoutResult`, then runs the DFS on +//! that — never touching the full `Node` enum (~500+ bytes/node). +//! Working set for 136k nodes: ~7.6 MB instead of ~65 MB. +use crate::cache::fast_hash::DenseNodeMap; use crate::cache::paragraph::ParagraphCache; -use crate::cg::prelude::*; -use crate::node::scene_graph::SceneGraph; -use crate::node::schema::{LayerEffects, Node, NodeGeometryMixin, NodeId, NodeRectMixin, Scene}; +use crate::node::scene_graph::{GeoNodeKind, NodeGeoData, RenderBoundsInflation, SceneGraph}; +use crate::node::schema::{Node, NodeId, Scene}; use crate::runtime::font_repository::FontRepository; use math2::rect; use math2::rect::Rectangle; use math2::transform::AffineTransform; -use crate::cache::fast_hash::DenseNodeMap; + +// --------------------------------------------------------------------------- +// GeoInput — layout-resolved per-node geometry for the DFS +// --------------------------------------------------------------------------- + +/// Layout-resolved per-node data for the geometry DFS. +/// +/// Built from `NodeGeoData` (schema-level) + `LayoutResult` (layout-level). +/// The DFS reads only this — compact and `Copy`, ~48 bytes. +#[derive(Debug, Clone, Copy)] +struct GeoInput { + transform: AffineTransform, + width: f32, + height: f32, + kind: GeoNodeKind, + render_bounds_inflation: RenderBoundsInflation, +} + +// --------------------------------------------------------------------------- +// GeometryEntry — public output (unchanged) +// --------------------------------------------------------------------------- /// Geometry data used for layout, culling, and rendering. /// @@ -45,43 +73,9 @@ pub struct GeometryEntry { pub dirty_bounds: bool, } -/// Context passed during geometry building -/// Bundles all immutable + mutable state for geometry build, reducing -/// recursive call parameter count from 9 to 4 (significant for WASM -/// where each function parameter adds call overhead). -struct GeometryBuildContext<'a> { - graph: &'a SceneGraph, - paragraph_cache: &'a mut ParagraphCache, - fonts: &'a FontRepository, - layout_result: Option<&'a crate::layout::cache::LayoutResult>, - viewport_size: crate::node::schema::Size, - /// Pre-computed: which nodes are layout containers (Container or ICB). - is_layout_container: DenseNodeMap, -} - -impl<'a> GeometryBuildContext<'a> { - fn new( - graph: &'a SceneGraph, - paragraph_cache: &'a mut ParagraphCache, - fonts: &'a FontRepository, - layout_result: Option<&'a crate::layout::cache::LayoutResult>, - viewport_size: crate::node::schema::Size, - ) -> Self { - let mut is_layout_container = DenseNodeMap::with_capacity(graph.node_count()); - for (id, node) in graph.nodes_iter() { - let is_container = matches!(node, Node::Container(_) | Node::InitialContainer(_)); - is_layout_container.insert(id, is_container); - } - Self { - graph, - paragraph_cache, - fonts, - layout_result, - viewport_size, - is_layout_container, - } - } -} +// --------------------------------------------------------------------------- +// GeometryCache — public API (unchanged) +// --------------------------------------------------------------------------- #[derive(Debug, Clone)] pub struct GeometryCache { @@ -118,44 +112,111 @@ impl GeometryCache { layout_result: Option<&crate::layout::cache::LayoutResult>, viewport_size: crate::node::schema::Size, ) -> Self { + let graph = &scene.graph; + let schema_geo = graph.geo_data(); + + #[cfg(feature = "perf")] + let _t_resolve_start = crate::sys::perf_now(); + + // ── Layout resolution pass ── + // Resolve layout-dependent fields (position, size) from the compact + // NodeGeoData + LayoutResult. Text measurement for nodes without + // layout falls back to accessing the Node enum (rare path). + // + // This pass iterates over NodeGeoData (~48 bytes/node) instead of + // Node (~500+ bytes/node). + let mut is_layout_container = DenseNodeMap::with_capacity(graph.node_count()); + for (id, geo) in schema_geo.iter() { + let is_container = matches!( + geo.kind, + GeoNodeKind::Container | GeoNodeKind::InitialContainer + ); + is_layout_container.insert(id, is_container); + } + + // Build parent map from the graph's link structure. + let mut parent_map: DenseNodeMap = DenseNodeMap::with_capacity(graph.node_count()); + for (id, _) in schema_geo.iter() { + if let Some(children) = graph.get_children(&id) { + for child_id in children { + parent_map.insert(*child_id, id); + } + } + } + + let mut geo_inputs = DenseNodeMap::with_capacity(graph.node_count()); + + for (id, geo) in schema_geo.iter() { + let parent_id = parent_map.get(&id).copied(); + let resolved = resolve_layout( + &id, + geo, + parent_id, + layout_result, + &is_layout_container, + graph, + paragraph_cache, + fonts, + viewport_size, + ); + geo_inputs.insert(id, resolved); + } + + #[cfg(feature = "perf")] + let _t_resolve_end = crate::sys::perf_now(); + + // ── DFS pass ── let mut cache = Self { - entries: DenseNodeMap::with_capacity(scene.graph.node_count()), + entries: DenseNodeMap::with_capacity(graph.node_count()), }; let root_world = AffineTransform::identity(); - let mut ctx = GeometryBuildContext::new( - &scene.graph, - paragraph_cache, - fonts, - layout_result, - viewport_size, - ); - - for child in scene.graph.roots() { - Self::build_recursive(&child, &root_world, None, &mut cache, &mut ctx); + + for child in graph.roots() { + Self::build_recursive(child, &root_world, None, &mut cache, graph, &geo_inputs); } + + #[cfg(feature = "perf")] + { + let _t_dfs_end = crate::sys::perf_now(); + eprintln!( + "[geometry] resolve={:.0}ms dfs={:.0}ms total={:.0}ms", + _t_resolve_end - _t_resolve_start, + _t_dfs_end - _t_resolve_end, + _t_dfs_end - _t_resolve_start, + ); + } + cache } + /// DFS that operates on layout-resolved `GeoInput` data. fn build_recursive( id: &NodeId, parent_world: &AffineTransform, parent_id: Option, cache: &mut GeometryCache, - ctx: &mut GeometryBuildContext, + graph: &SceneGraph, + geo_inputs: &DenseNodeMap, ) -> Rectangle { - let node = ctx.graph - .get_node(id) - .expect("node not found in geometry cache"); + let geo = geo_inputs + .get(id) + .expect("GeoInput not found — resolve pass missed a node"); - match node { - Node::Group(n) => { - let world_transform = parent_world.compose(&n.transform.unwrap_or_default()); + match geo.kind { + GeoNodeKind::Group => { + let world_transform = parent_world.compose(&geo.transform); let mut union_bounds: Option = None; let mut union_render_bounds: Option = None; - if let Some(children) = ctx.graph.get_children(id) { + + if let Some(children) = graph.get_children(id) { for child_id in children { let child_bounds = Self::build_recursive( - child_id, &world_transform, Some(*id), cache, ctx, + child_id, + &world_transform, + Some(*id), + cache, + graph, + geo_inputs, ); union_bounds = match union_bounds { Some(b) => Some(rect::union(&[b, child_bounds])), @@ -170,20 +231,28 @@ impl GeometryCache { } } - let world_bounds = union_bounds.unwrap_or_else(|| Rectangle { - x: 0.0, y: 0.0, width: 0.0, height: 0.0, + let world_bounds = union_bounds.unwrap_or(Rectangle { + x: 0.0, + y: 0.0, + width: 0.0, + height: 0.0, }); let local_bounds = if let Some(inv) = world_transform.inverse() { transform_rect(&world_bounds, &inv) } else { - Rectangle { x: 0.0, y: 0.0, width: 0.0, height: 0.0 } + Rectangle { + x: 0.0, + y: 0.0, + width: 0.0, + height: 0.0, + } }; let render_bounds = union_render_bounds.unwrap_or(world_bounds); let entry = GeometryEntry { - transform: n.transform.unwrap_or_default(), + transform: geo.transform, absolute_transform: world_transform, bounding_box: local_bounds, absolute_bounding_box: world_bounds, @@ -197,28 +266,33 @@ impl GeometryCache { cache.entries.insert(*id, entry); bounds } - Node::InitialContainer(_n) => { - let size = ctx.viewport_size; - let local_transform = AffineTransform::identity(); - let world_transform = parent_world.compose(&local_transform); + GeoNodeKind::InitialContainer => { + let world_transform = parent_world.compose(&geo.transform); let local_bounds = Rectangle { - x: 0.0, y: 0.0, width: size.width, height: size.height, + x: 0.0, + y: 0.0, + width: geo.width, + height: geo.height, }; - let mut union_world_bounds = transform_rect(&local_bounds, &world_transform); - if let Some(children) = ctx.graph.get_children(id) { + if let Some(children) = graph.get_children(id) { for child_id in children { let child_bounds = Self::build_recursive( - child_id, &world_transform, Some(*id), cache, ctx, + child_id, + &world_transform, + Some(*id), + cache, + graph, + geo_inputs, ); union_world_bounds = rect::union(&[union_world_bounds, child_bounds]); } } let entry = GeometryEntry { - transform: local_transform, + transform: geo.transform, absolute_transform: world_transform, bounding_box: local_bounds, absolute_bounding_box: union_world_bounds, @@ -231,13 +305,20 @@ impl GeometryCache { cache.entries.insert(*id, entry); union_world_bounds } - Node::BooleanOperation(n) => { - let world_transform = parent_world.compose(&n.transform.unwrap_or_default()); + + GeoNodeKind::BooleanOperation => { + let world_transform = parent_world.compose(&geo.transform); let mut union_bounds: Option = None; - if let Some(children) = ctx.graph.get_children(id) { + + if let Some(children) = graph.get_children(id) { for child_id in children { let child_bounds = Self::build_recursive( - child_id, &world_transform, Some(*id), cache, ctx, + child_id, + &world_transform, + Some(*id), + cache, + graph, + geo_inputs, ); union_bounds = match union_bounds { Some(b) => Some(rect::union(&[b, child_bounds])), @@ -246,25 +327,27 @@ impl GeometryCache { } } - let world_bounds = union_bounds.unwrap_or_else(|| Rectangle { - x: 0.0, y: 0.0, width: 0.0, height: 0.0, + let world_bounds = union_bounds.unwrap_or(Rectangle { + x: 0.0, + y: 0.0, + width: 0.0, + height: 0.0, }); let local_bounds = if let Some(inv) = world_transform.inverse() { transform_rect(&world_bounds, &inv) } else { - Rectangle { x: 0.0, y: 0.0, width: 0.0, height: 0.0 } + Rectangle { + x: 0.0, + y: 0.0, + width: 0.0, + height: 0.0, + } }; - - let render_bounds = compute_render_bounds_from_style( - world_bounds, - n.stroke_width.value_or_zero(), - n.stroke_style.stroke_align, - &n.effects, - ); + let render_bounds = inflate_rect_sides(world_bounds, &geo.render_bounds_inflation); let entry = GeometryEntry { - transform: n.transform.unwrap_or_default(), + transform: geo.transform, absolute_transform: world_transform, bounding_box: local_bounds, absolute_bounding_box: world_bounds, @@ -278,49 +361,36 @@ impl GeometryCache { cache.entries.insert(*id, entry); bounds } - Node::Container(n) => { - let (x, y, width, height) = if let Some(result) = ctx.layout_result { - let computed = result - .get(id) - .expect("Container must have layout result when layout engine is used"); - (computed.x, computed.y, computed.width, computed.height) - } else { - ( - n.position.x().unwrap_or(0.0), - n.position.y().unwrap_or(0.0), - n.layout_dimensions.layout_target_width.unwrap_or(0.0), - n.layout_dimensions.layout_target_height.unwrap_or(0.0), - ) - }; - let local_transform = AffineTransform::new(x, y, n.rotation); - let local_bounds = Rectangle { x: 0.0, y: 0.0, width, height }; + GeoNodeKind::Container => { + let local_bounds = Rectangle { + x: 0.0, + y: 0.0, + width: geo.width, + height: geo.height, + }; - let world_transform = parent_world.compose(&local_transform); + let world_transform = parent_world.compose(&geo.transform); let world_bounds = transform_rect(&local_bounds, &world_transform); let mut union_world_bounds = world_bounds; - let render_bounds = if let Some(rect_stroke) = n.rectangular_stroke_width() { - compute_render_bounds_with_rectangular_stroke( - world_bounds, &rect_stroke, n.stroke_style.stroke_align, &n.effects, - ) - } else { - compute_render_bounds_from_style( - world_bounds, n.render_bounds_stroke_width(), - n.stroke_style.stroke_align, &n.effects, - ) - }; + let render_bounds = inflate_rect_sides(world_bounds, &geo.render_bounds_inflation); - if let Some(children) = ctx.graph.get_children(id) { + if let Some(children) = graph.get_children(id) { for child_id in children { let child_bounds = Self::build_recursive( - child_id, &world_transform, Some(*id), cache, ctx, + child_id, + &world_transform, + Some(*id), + cache, + graph, + geo_inputs, ); union_world_bounds = rect::union(&[union_world_bounds, child_bounds]); } } let entry = GeometryEntry { - transform: local_transform, + transform: geo.transform, absolute_transform: world_transform, bounding_box: local_bounds, absolute_bounding_box: world_bounds, @@ -332,41 +402,20 @@ impl GeometryCache { cache.entries.insert(*id, entry); union_world_bounds } - Node::TextSpan(n) => { - let layout = ctx.layout_result.and_then(|r| r.get(id)); - - const MIN_SIZE_DIRTY_HACK: f32 = 1.0; - let parent_is_layout_container = parent_id - .as_ref() - .and_then(|pid| ctx.is_layout_container.get(pid).copied()) - .unwrap_or(false); - - let (local_transform, width, height) = if let Some(l) = layout { - let width = l.width.max(MIN_SIZE_DIRTY_HACK); - let height = l.height.max(MIN_SIZE_DIRTY_HACK); - let transform = if parent_is_layout_container { - AffineTransform::new(l.x, l.y, n.transform.rotation()) - } else { - n.transform - }; - (transform, width, height) - } else { - let measurements = ctx.paragraph_cache.measure( - &n.text, &n.text_style, &n.text_align, &n.max_lines, - &n.ellipsis, n.width, ctx.fonts, Some(id), - ); - let width = measurements.max_width.max(MIN_SIZE_DIRTY_HACK); - let height = n.height.unwrap_or(measurements.height).max(MIN_SIZE_DIRTY_HACK); - (n.transform, width, height) + GeoNodeKind::TextSpan => { + let local_bounds = Rectangle { + x: 0.0, + y: 0.0, + width: geo.width, + height: geo.height, }; - let local_bounds = Rectangle { x: 0.0, y: 0.0, width, height }; - let world_transform = parent_world.compose(&local_transform); + let world_transform = parent_world.compose(&geo.transform); let world_bounds = transform_rect(&local_bounds, &world_transform); - let render_bounds = compute_render_bounds(node, world_bounds); + let render_bounds = inflate_rect_sides(world_bounds, &geo.render_bounds_inflation); let entry = GeometryEntry { - transform: local_transform, + transform: geo.transform, absolute_transform: world_transform, bounding_box: local_bounds, absolute_bounding_box: world_bounds, @@ -379,63 +428,21 @@ impl GeometryCache { cache.entries.insert(*id, entry); bounds } - _ => { - let (rec_transform, schema_width, schema_height) = match node { - Node::Rectangle(n) => (n.transform, n.size.width, n.size.height), - Node::Ellipse(n) => (n.transform, n.size.width, n.size.height), - Node::Image(n) => (n.transform, n.size.width, n.size.height), - Node::RegularPolygon(n) => (n.transform, n.size.width, n.size.height), - Node::RegularStarPolygon(n) => (n.transform, n.size.width, n.size.height), - Node::Line(n) => (n.transform, n.size.width, 0.0), - Node::Polygon(n) => { - let rect = n.rect(); - (n.transform, rect.width, rect.height) - } - Node::Path(n) => { - let rect = n.rect(); - (n.transform, rect.width, rect.height) - } - Node::Vector(n) => { - let rect = n.network.bounds(); - (n.transform, rect.width, rect.height) - } - Node::Error(n) => (n.transform, n.size.width, n.size.height), - _ => unreachable!("Has dedicated case above"), - }; - let parent_is_layout_container = parent_id - .as_ref() - .and_then(|pid| ctx.is_layout_container.get(pid).copied()) - .unwrap_or(false); - - let (local_transform, width, height) = if parent_is_layout_container { - let (x, y, width, height) = - if let Some(result) = ctx.layout_result.and_then(|r| r.get(id)) { - (result.x, result.y, result.width, result.height) - } else { - (rec_transform.x(), rec_transform.y(), schema_width, schema_height) - }; - (AffineTransform::new(x, y, rec_transform.rotation()), width, height) - } else { - let width = ctx.layout_result - .and_then(|r| r.get(id)) - .map(|l| l.width) - .unwrap_or(schema_width); - let height = ctx.layout_result - .and_then(|r| r.get(id)) - .map(|l| l.height) - .unwrap_or(schema_height); - (rec_transform, width, height) + GeoNodeKind::Leaf => { + let local_bounds = Rectangle { + x: 0.0, + y: 0.0, + width: geo.width, + height: geo.height, }; - let local_bounds = Rectangle { x: 0.0, y: 0.0, width, height }; - - let world_transform = parent_world.compose(&local_transform); + let world_transform = parent_world.compose(&geo.transform); let world_bounds = transform_rect(&local_bounds, &world_transform); - let render_bounds = compute_render_bounds(node, world_bounds); + let render_bounds = inflate_rect_sides(world_bounds, &geo.render_bounds_inflation); let entry = GeometryEntry { - transform: local_transform, + transform: geo.transform, absolute_transform: world_transform, bounding_box: local_bounds, absolute_bounding_box: world_bounds, @@ -464,14 +471,12 @@ impl GeometryCache { self.entries.get(id).map(|e| e.absolute_bounding_box) } - /// Return expanded render bounds for a node if available. pub fn get_render_bounds(&self, id: &NodeId) -> Option { self.entries.get(id).map(|e| e.absolute_render_bounds) } - /// Return the parent NodeId for a given node if available. pub fn get_parent(&self, id: &NodeId) -> Option { - self.entries.get(id).and_then(|e| e.parent.clone()) + self.entries.get(id).and_then(|e| e.parent) } pub fn len(&self) -> usize { @@ -482,258 +487,200 @@ impl GeometryCache { self.entries.contains_key(id) } - /// filter by node id and its entry data pub fn filter(&self, filter: impl Fn(&NodeId, &GeometryEntry) -> bool) -> Self { Self { entries: self .entries .iter() .filter(|(id, entry)| filter(id, entry)) - .map(|(id, entry)| (id.clone(), entry.clone())) + .map(|(id, entry)| (id, entry.clone())) .collect(), } } } -fn transform_rect(rect: &Rectangle, t: &AffineTransform) -> Rectangle { - rect::transform(*rect, t) -} - -fn inflate_rect(rect: Rectangle, delta: f32) -> Rectangle { - if delta <= 0.0 { - return rect; - } - Rectangle { - x: rect.x - delta, - y: rect.y - delta, - width: rect.width + 2.0 * delta, - height: rect.height + 2.0 * delta, - } -} - -fn stroke_outset(align: StrokeAlign, width: f32) -> f32 { - match align { - StrokeAlign::Inside => 0.0, - StrokeAlign::Center => width / 2.0, - StrokeAlign::Outside => width, - } -} - -fn compute_render_bounds_from_effects(bounds: Rectangle, effects: &LayerEffects) -> Rectangle { - let mut bounds = bounds; - if let Some(blur) = &effects.blur { - bounds = match &blur.blur { - FeBlur::Gaussian(gaussian) => { - // Use 3x sigma for 99.7% Gaussian coverage - inflate_rect(bounds, gaussian.radius * 3.0) - } - FeBlur::Progressive(progressive) => { - // Use the maximum of both radii for bounds calculation - // to handle both increasing and decreasing blur gradients - // Multiply by 3.0 for proper 3-sigma Gaussian coverage - let max_radius = progressive.radius.max(progressive.radius2); - inflate_rect(bounds, max_radius * 3.0) - } - }; - } - for shadow in &effects.shadows { - bounds = compute_render_bounds_from_effect(bounds, &shadow.clone().into()); - } - bounds -} +// --------------------------------------------------------------------------- +// Layout resolution — NodeGeoData + LayoutResult → GeoInput +// --------------------------------------------------------------------------- -fn compute_render_bounds_from_effect(bounds: Rectangle, effect: &FilterEffect) -> Rectangle { - match effect { - FilterEffect::LiquidGlass(glass) => inflate_rect(bounds, glass.blur_radius * 3.0), - FilterEffect::LayerBlur(blur) => match &blur.blur { - FeBlur::Gaussian(gaussian) => inflate_rect(bounds, gaussian.radius * 3.0), - FeBlur::Progressive(progressive) => { - let max_radius = progressive.radius.max(progressive.radius2); - inflate_rect(bounds, max_radius * 3.0) - } +/// Resolve layout-dependent fields from `NodeGeoData` + `LayoutResult`. +/// +/// For most nodes this is a lightweight copy from the pre-extracted data +/// with layout overrides for position/size. Only text spans without layout +/// results fall back to accessing the full `Node` for text measurement. +#[allow(clippy::too_many_arguments)] +fn resolve_layout( + id: &NodeId, + geo: &NodeGeoData, + parent_id: Option, + layout_result: Option<&crate::layout::cache::LayoutResult>, + is_layout_container: &DenseNodeMap, + graph: &SceneGraph, + paragraph_cache: &mut ParagraphCache, + fonts: &FontRepository, + viewport_size: crate::node::schema::Size, +) -> GeoInput { + match geo.kind { + GeoNodeKind::Group | GeoNodeKind::BooleanOperation => GeoInput { + transform: geo.schema_transform, + width: geo.schema_width, + height: geo.schema_height, + kind: geo.kind, + render_bounds_inflation: geo.render_bounds_inflation, }, - FilterEffect::BackdropBlur(blur) => match &blur.blur { - FeBlur::Gaussian(gaussian) => inflate_rect(bounds, gaussian.radius * 3.0), - FeBlur::Progressive(progressive) => { - let max_radius = progressive.radius.max(progressive.radius2); - inflate_rect(bounds, max_radius * 3.0) - } + GeoNodeKind::InitialContainer => GeoInput { + transform: geo.schema_transform, + width: viewport_size.width, + height: viewport_size.height, + kind: geo.kind, + render_bounds_inflation: geo.render_bounds_inflation, }, - FilterEffect::DropShadow(shadow) => { - // Apply spread by inflating the bounds, then offset and blur - let mut rect = if shadow.spread != 0.0 { - inflate_rect(bounds, shadow.spread) + GeoNodeKind::Container => { + let (x, y, width, height) = + if let Some(computed) = layout_result.and_then(|r| r.get(id)) { + (computed.x, computed.y, computed.width, computed.height) + } else { + // Fallback to schema data when layout result is missing. + // This happens for orphan nodes not reachable from scene roots, + // or when layout is skipped entirely (layout_result == None). + ( + geo.schema_transform.x(), + geo.schema_transform.y(), + geo.schema_width, + geo.schema_height, + ) + }; + GeoInput { + transform: AffineTransform::new(x, y, geo.rotation), + width, + height, + kind: geo.kind, + render_bounds_inflation: geo.render_bounds_inflation, + } + } + GeoNodeKind::TextSpan => { + let layout = layout_result.and_then(|r| r.get(id)); + const MIN_SIZE_DIRTY_HACK: f32 = 1.0; + + let parent_is_layout_container = parent_id + .as_ref() + .and_then(|pid| is_layout_container.get(pid).copied()) + .unwrap_or(false); + + let (local_transform, width, height) = if let Some(l) = layout { + let width = l.width.max(MIN_SIZE_DIRTY_HACK); + let height = l.height.max(MIN_SIZE_DIRTY_HACK); + let transform = if parent_is_layout_container { + AffineTransform::new(l.x, l.y, geo.schema_transform.rotation()) + } else { + geo.schema_transform + }; + (transform, width, height) } else { - bounds + // Fallback: text measurement via paragraph cache. + // This requires accessing the Node for text content. + // Only happens when layout_result is None (rare path). + if let Ok(Node::TextSpan(n)) = graph.get_node(id) { + let measurements = paragraph_cache.measure( + &n.text, + &n.text_style, + &n.text_align, + &n.max_lines, + &n.ellipsis, + n.width, + fonts, + Some(id), + ); + let width = measurements.max_width.max(MIN_SIZE_DIRTY_HACK); + let height = n + .height + .unwrap_or(measurements.height) + .max(MIN_SIZE_DIRTY_HACK); + (geo.schema_transform, width, height) + } else { + // Shouldn't happen; schema_width/height as fallback + ( + geo.schema_transform, + geo.schema_width.max(MIN_SIZE_DIRTY_HACK), + geo.schema_height.max(MIN_SIZE_DIRTY_HACK), + ) + } }; - rect.x += shadow.dx; - rect.y += shadow.dy; - // Use 3x sigma for proper Gaussian blur coverage - inflate_rect(rect, shadow.blur * 3.0) - } - // no inflation - FilterEffect::Noise(_) => bounds, - FilterEffect::InnerShadow(_) => bounds, - } -} - -fn compute_render_bounds_from_style( - world_bounds: Rectangle, - stroke_width: f32, - stroke_align: StrokeAlign, - effects: &LayerEffects, -) -> Rectangle { - let mut bounds = inflate_rect(world_bounds, stroke_outset(stroke_align, stroke_width)); - - bounds = compute_render_bounds_from_effects(bounds, effects); - - bounds -} -/// Computes render bounds for nodes with per-side stroke widths. -/// -/// Handles all three stroke alignments: -/// - **Center**: Inflate by half-widths (stroke extends inward and outward) -/// - **Inside**: No inflation (stroke is entirely inside node bounds) -/// - **Outside**: Inflate by full-widths (stroke extends entirely outward) -fn compute_render_bounds_with_rectangular_stroke( - world_bounds: Rectangle, - rect_stroke: &RectangularStrokeWidth, - stroke_align: StrokeAlign, - effects: &LayerEffects, -) -> Rectangle { - let mut bounds = world_bounds; - - // Inflate based on stroke alignment - match stroke_align { - StrokeAlign::Center => { - // Center: inflate by half the stroke width on each side - bounds = rect::inflate( - bounds, - rect::Sides { - top: rect_stroke.stroke_top_width / 2.0, - right: rect_stroke.stroke_right_width / 2.0, - bottom: rect_stroke.stroke_bottom_width / 2.0, - left: rect_stroke.stroke_left_width / 2.0, - }, - ); - } - StrokeAlign::Inside => { - // Inside: no inflation - stroke is entirely inside the node bounds - // bounds remain unchanged + GeoInput { + transform: local_transform, + width, + height, + kind: geo.kind, + render_bounds_inflation: geo.render_bounds_inflation, + } } - StrokeAlign::Outside => { - // Outside: inflate by full stroke width on each side - bounds = rect::inflate( - bounds, - rect::Sides { - top: rect_stroke.stroke_top_width, - right: rect_stroke.stroke_right_width, - bottom: rect_stroke.stroke_bottom_width, - left: rect_stroke.stroke_left_width, - }, - ); + GeoNodeKind::Leaf => { + let parent_is_layout_container = parent_id + .as_ref() + .and_then(|pid| is_layout_container.get(pid).copied()) + .unwrap_or(false); + + let (local_transform, width, height) = if parent_is_layout_container { + let (x, y, width, height) = + if let Some(result) = layout_result.and_then(|r| r.get(id)) { + (result.x, result.y, result.width, result.height) + } else { + ( + geo.schema_transform.x(), + geo.schema_transform.y(), + geo.schema_width, + geo.schema_height, + ) + }; + ( + AffineTransform::new(x, y, geo.schema_transform.rotation()), + width, + height, + ) + } else { + let width = layout_result + .and_then(|r| r.get(id)) + .map(|l| l.width) + .unwrap_or(geo.schema_width); + let height = layout_result + .and_then(|r| r.get(id)) + .map(|l| l.height) + .unwrap_or(geo.schema_height); + (geo.schema_transform, width, height) + }; + + GeoInput { + transform: local_transform, + width, + height, + kind: geo.kind, + render_bounds_inflation: geo.render_bounds_inflation, + } } } +} - bounds = compute_render_bounds_from_effects(bounds, effects); +// --------------------------------------------------------------------------- +// Helper functions +// --------------------------------------------------------------------------- - bounds +fn transform_rect(rect: &Rectangle, t: &AffineTransform) -> Rectangle { + rect::transform(*rect, t) } -fn compute_render_bounds(node: &Node, world_bounds: Rectangle) -> Rectangle { - match node { - Node::Rectangle(n) => { - // Check if this node has per-side stroke widths - if let Some(rect_stroke) = n.rectangular_stroke_width() { - compute_render_bounds_with_rectangular_stroke( - world_bounds, - &rect_stroke, - n.stroke_style.stroke_align, - &n.effects, - ) - } else { - compute_render_bounds_from_style( - world_bounds, - n.render_bounds_stroke_width(), - n.stroke_style.stroke_align, - &n.effects, - ) - } - } - Node::Ellipse(n) => compute_render_bounds_from_style( - world_bounds, - n.render_bounds_stroke_width(), - n.stroke_style.stroke_align, - &n.effects, - ), - Node::Polygon(n) => compute_render_bounds_from_style( - world_bounds, - n.render_bounds_stroke_width(), - n.stroke_style.stroke_align, - &n.effects, - ), - Node::RegularPolygon(n) => compute_render_bounds_from_style( - world_bounds, - n.render_bounds_stroke_width(), - n.stroke_style.stroke_align, - &n.effects, - ), - Node::RegularStarPolygon(n) => compute_render_bounds_from_style( - world_bounds, - n.render_bounds_stroke_width(), - n.stroke_style.stroke_align, - &n.effects, - ), - Node::Path(n) => compute_render_bounds_from_style( - world_bounds, - n.stroke_width.value_or_zero(), - n.stroke_style.stroke_align, - &n.effects, - ), - Node::Vector(n) => compute_render_bounds_from_style( - world_bounds, - n.stroke_width, - n.get_stroke_align(), - &n.effects, - ), - Node::Image(n) => compute_render_bounds_from_style( - world_bounds, - n.render_bounds_stroke_width(), - n.stroke_style.stroke_align, - &n.effects, - ), - Node::Line(n) => compute_render_bounds_from_style( - world_bounds, - n.stroke_width, - n.get_stroke_align(), - &n.effects, - ), - Node::TextSpan(n) => compute_render_bounds_from_style( - world_bounds, - n.stroke_width, - n.stroke_align, - &LayerEffects::default(), - ), - Node::Container(n) => { - // Check if this node has per-side stroke widths - if let Some(rect_stroke) = n.rectangular_stroke_width() { - compute_render_bounds_with_rectangular_stroke( - world_bounds, - &rect_stroke, - n.stroke_style.stroke_align, - &n.effects, - ) - } else { - compute_render_bounds_from_style( - world_bounds, - n.render_bounds_stroke_width(), - n.stroke_style.stroke_align, - &n.effects, - ) - } - } - Node::Error(_) => world_bounds, - Node::Group(_) | Node::BooleanOperation(_) | Node::InitialContainer(_) => world_bounds, +/// Inflate a rectangle by pre-computed per-side values. +fn inflate_rect_sides(rect: Rectangle, inf: &RenderBoundsInflation) -> Rectangle { + if inf.is_zero() { + return rect; } + rect::inflate( + rect, + rect::Sides { + top: inf.top, + right: inf.right, + bottom: inf.bottom, + left: inf.left, + }, + ) } diff --git a/crates/grida-canvas/src/cg/types.rs b/crates/grida-canvas/src/cg/types.rs index 6ae4b93426..6aff8a53fa 100644 --- a/crates/grida-canvas/src/cg/types.rs +++ b/crates/grida-canvas/src/cg/types.rs @@ -2,6 +2,7 @@ use super::prelude::*; use core::str; use math2::{box_fit::BoxFit, transform::AffineTransform}; use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; use std::hash::Hash; use super::alignment::Alignment; @@ -2007,9 +2008,14 @@ impl From for SolidPaint { /// The [`BlendMode`] assigned to each [`Paint`] applies to that specific entry /// while it is composited over the accumulated result. It never retroactively /// affects paints that were drawn earlier in the stack. +/// Inline capacity for the [`Paints`] collection. Most nodes have 0 or 1 fill/stroke. +/// `SmallVec<[Paint; 1]>` stores up to 1 paint inline without heap allocation, +/// saving ~272K allocs for a 136K-node scene (fills + strokes). +type PaintsVec = SmallVec<[Paint; 1]>; + #[derive(Debug, Clone, Default)] pub struct Paints { - paints: Vec, + paints: PaintsVec, } impl Paints { @@ -2053,7 +2059,7 @@ impl Paints { /// Consume the collection and return the underlying vector. pub fn into_vec(self) -> Vec { - self.paints + self.paints.into_vec() } /// Append a new paint to the top of the stack. @@ -2080,39 +2086,40 @@ impl From> for Paints { impl From for Vec { fn from(value: Paints) -> Self { - value.paints + value.paints.into_vec() } } // Custom trait to support both Vec and array literals in Paints::new() pub trait IntoPaints { - fn into_paints(self) -> Vec; + fn into_paints(self) -> PaintsVec; } impl IntoPaints for Vec { - fn into_paints(self) -> Vec { - self + fn into_paints(self) -> PaintsVec { + SmallVec::from_vec(self) } } impl IntoPaints for [Paint; N] { - fn into_paints(self) -> Vec { - self.to_vec() + fn into_paints(self) -> PaintsVec { + self.into_iter().collect() } } impl FromIterator for Paints { fn from_iter>(iter: I) -> Self { - Paints::new(iter.into_iter().collect::>()) + Paints { + paints: iter.into_iter().collect(), + } } } // Support for array literals - much more ergonomic than vec![] impl From<[Paint; N]> for Paints { fn from(value: [Paint; N]) -> Self { - // Most efficient: direct construction without intermediate allocations Paints { - paints: value.to_vec(), + paints: value.into_iter().collect(), } } } @@ -2120,16 +2127,16 @@ impl From<[Paint; N]> for Paints { // Support for single Paint conversion impl From for Paints { fn from(value: Paint) -> Self { - // More efficient: avoid the intermediate Vec allocation + // SmallVec stores 1 paint inline — zero heap allocation Paints { - paints: vec![value], + paints: smallvec::smallvec![value], } } } impl IntoIterator for Paints { type Item = Paint; - type IntoIter = std::vec::IntoIter; + type IntoIter = smallvec::IntoIter<[Paint; 1]>; fn into_iter(self) -> Self::IntoIter { self.paints.into_iter() diff --git a/crates/grida-canvas/src/io/io_grida_fbs.rs b/crates/grida-canvas/src/io/io_grida_fbs.rs index 4c68a73a8c..f16cfe005b 100644 --- a/crates/grida-canvas/src/io/io_grida_fbs.rs +++ b/crates/grida-canvas/src/io/io_grida_fbs.rs @@ -17,6 +17,7 @@ use std::collections::HashMap; use math2::{box_fit::BoxFit, transform::AffineTransform}; +use rustc_hash::FxHashMap; /// Schema version emitted by the Rust FlatBuffers writer. /// @@ -34,19 +35,18 @@ use crate::cg::{ stroke_dasharray::StrokeDashArray, stroke_width::{RectangularStrokeWidth, SingularStrokeWidth, StrokeWidth}, tilemode::TileMode, - varwidth, types::{ Axis, BlendMode, BooleanPathOperation, CGPoint, ContainerClipFlag, CornerSmoothing, CrossAxisAlignment, DiamondGradientPaint, EdgeInsets, FontFeature, FontOpticalSizing, FontVariation, FontWeight, GradientStop, ImageFilters, ImagePaint, ImagePaintFit, - ImageRepeat, ImageTile, - LayerBlendMode, LayerMaskType, LayoutGap, LayoutMode, LayoutPositioning, LayoutWrap, - LinearGradientPaint, MainAxisAlignment, Paint, Paints, RadialGradientPaint, - RectangularCornerRadius, ResourceRef, SolidPaint, StrokeAlign, StrokeCap, StrokeJoin, - StrokeMarkerPreset, StrokeMiterLimit, SweepGradientPaint, TextAlign, TextAlignVertical, - TextDecorationLine, TextDecorationRec, TextDecorationStyle, TextLetterSpacing, - TextLineHeight, TextStyleRec, TextTransform, TextWordSpacing, + ImageRepeat, ImageTile, LayerBlendMode, LayerMaskType, LayoutGap, LayoutMode, + LayoutPositioning, LayoutWrap, LinearGradientPaint, MainAxisAlignment, Paint, Paints, + RadialGradientPaint, RectangularCornerRadius, ResourceRef, SolidPaint, StrokeAlign, + StrokeCap, StrokeJoin, StrokeMarkerPreset, StrokeMiterLimit, SweepGradientPaint, TextAlign, + TextAlignVertical, TextDecorationLine, TextDecorationRec, TextDecorationStyle, + TextLetterSpacing, TextLineHeight, TextStyleRec, TextTransform, TextWordSpacing, }, + varwidth, }; use crate::node::{ id::NodeIdGenerator, @@ -54,9 +54,9 @@ use crate::node::{ schema::{ BooleanPathOperationNodeRec, ContainerNodeRec, EllipseNodeRec, GroupNodeRec, InitialContainerNodeRec, LayerEffects, LayoutChildStyle, LayoutContainerStyle, - LayoutDimensionStyle, LayoutPositioningBasis, LineNodeRec, Node, RectangleNodeRec, - RegularPolygonNodeRec, RegularStarPolygonNodeRec, Scene, Size, StrokeStyle, - PathNodeRec, TextSpanNodeRec, VectorNodeRec, + LayoutDimensionStyle, LayoutPositioningBasis, LineNodeRec, Node, PathNodeRec, + RectangleNodeRec, RegularPolygonNodeRec, RegularStarPolygonNodeRec, Scene, Size, + StrokeStyle, TextSpanNodeRec, VectorNodeRec, }, }; use crate::vectornetwork::{ @@ -146,14 +146,16 @@ fn decode_layer_common(sys: &fbs::SystemNodeTrait<'_>, layer: &fbs::LayerTrait<' fn decode_shape_layout(layer: &fbs::LayerTrait<'_>, cos_sin: (f32, f32)) -> ShapeLayout { let layout = layer.layout(); let (x, y) = layout.as_ref().map(decode_layout_xy).unwrap_or((0.0, 0.0)); - let (w, h) = layout.as_ref().map(decode_dimensions).unwrap_or((None, None)); + let (w, h) = layout + .as_ref() + .map(decode_dimensions) + .unwrap_or((None, None)); let size = Size { width: w.unwrap_or(0.0), height: h.unwrap_or(0.0), }; let (cos, sin) = cos_sin; - let transform = - AffineTransform::from_box_center_raw(x, y, size.width, size.height, cos, sin); + let transform = AffineTransform::from_box_center_raw(x, y, size.width, size.height, cos, sin); ShapeLayout { x, y, @@ -232,10 +234,18 @@ fn decode_all_inner(bytes: &[u8]) -> Result { } // ── 2. Decode all node slots ───────────────────────────────────────────── + // + // The decode pipeline is structured to minimize heap allocations: + // + // Phase 2a: Decode FBS → NodeEntry (one String per ID, no cloning) + // Phase 2b: Assign NodeId in the same loop → build string_to_internal_id + // Phase 3: Build children_by_parent using NodeId keys (no String cloning) + // Phase 4: Single-pass consume node_entries → node_pairs + node_names + position_map struct NodeEntry { - id: String, - parent: Option<(String, String)>, // (parent_id, fractional-index position) + internal_id: crate::node::id::NodeId, + parent_internal: Option, + position: Option, node: Node, name: Option, } @@ -247,24 +257,53 @@ fn decode_all_inner(bytes: &[u8]) -> Result { background_color: Option, } - let mut node_entries: Vec = Vec::new(); - let mut scene_metas: HashMap = HashMap::new(); + // Pre-allocate based on estimated node count. + let estimated_count = document.nodes().map(|v| v.len()).unwrap_or(0); + let mut string_to_internal_id: FxHashMap = + FxHashMap::with_capacity_and_hasher(estimated_count, Default::default()); + let mut id_generator = NodeIdGenerator::new(); + let mut node_entries: Vec = Vec::with_capacity(estimated_count); + let mut scene_metas: FxHashMap = + FxHashMap::default(); + + // Helper: get or assign an internal NodeId for a string ID. + // Uses the shared id_generator and string_to_internal_id map. + let mut get_or_assign_id = |s: String| -> crate::node::id::NodeId { + *string_to_internal_id + .entry(s) + .or_insert_with(|| id_generator.next()) + }; - /// Helper macro: every layer-bearing node type follows the same pattern of - /// extracting `sys`, `layer`, `id`, `parent` from the slot, then calling a - /// decoder. This macro eliminates that boilerplate and makes it impossible - /// to forget any step. + /// Helper macro: decode a layer-bearing node. Assigns internal IDs + /// immediately during the hot loop — no deferred string cloning needed. macro_rules! decode_layer_node { ($slot:expr, $accessor:ident, $decode_fn:expr) => { if let Some(typed) = $slot.$accessor() { let sys = typed.node(); let layer = typed.layer(); - let id = sys.id().id().to_owned(); + let string_id = sys.id().id().to_owned(); let name = sys.name().map(|s| s.to_owned()); - let parent = decode_parent_ref(&layer); + let (parent_internal, position) = { + let parent_ref = layer.parent(); + let parent_str = parent_ref.parent_id().id(); + if parent_str.is_empty() { + (None, None) + } else { + let parent_nid = get_or_assign_id(parent_str.to_owned()); + let pos = parent_ref.position().unwrap_or("").to_owned(); + (Some(parent_nid), Some(pos)) + } + }; + let internal_id = get_or_assign_id(string_id); let lc = decode_layer_common(&sys, &layer); let node = $decode_fn(&lc, &layer, &typed); - node_entries.push(NodeEntry { id, parent, node, name }); + node_entries.push(NodeEntry { + internal_id, + parent_internal, + position, + node, + name, + }); } }; } @@ -278,9 +317,8 @@ fn decode_all_inner(bytes: &[u8]) -> Result { let sys = sn.node(); let id = sys.id().id().to_owned(); let name = sys.name().unwrap_or("").to_owned(); - let bg = sn - .scene_background_color() - .map(decode_rgba32f_to_cg_color); + let bg = sn.scene_background_color().map(decode_rgba32f_to_cg_color); + get_or_assign_id(id.clone()); scene_metas.insert( id.clone(), SceneMeta { @@ -331,97 +369,120 @@ fn decode_all_inner(bytes: &[u8]) -> Result { } } - // ── 3. Build ID mapping (string → internal NodeId) ─────────────────────── - let mut string_to_internal_id: HashMap = HashMap::new(); - let mut id_generator = NodeIdGenerator::new(); - + // ── 3. Build children_by_parent using NodeId keys (no String cloning) ──── + // + // Previously this was HashMap> which cloned + // every node ID and position string. Now uses u64 NodeId keys directly. + let mut children_by_parent: FxHashMap< + crate::node::id::NodeId, + Vec<(crate::node::id::NodeId, &str)>, + > = FxHashMap::with_capacity_and_hasher(estimated_count / 4, Default::default()); for e in &node_entries { - string_to_internal_id - .entry(e.id.clone()) - .or_insert_with(|| id_generator.next()); - } - for (sid, _) in &scene_metas { - string_to_internal_id - .entry(sid.clone()) - .or_insert_with(|| id_generator.next()); - } - - let get_id = |s: &String| string_to_internal_id.get(s).copied(); - - // ── 4. Build children_by_parent (sorted by fractional index) ───────────── - let mut children_by_parent: HashMap> = HashMap::new(); - for e in &node_entries { - if let Some((parent_id, position)) = &e.parent { + if let Some(parent_nid) = e.parent_internal { + let pos = e.position.as_deref().unwrap_or(""); children_by_parent - .entry(parent_id.clone()) + .entry(parent_nid) .or_default() - .push((e.id.clone(), position.clone())); + .push((e.internal_id, pos)); } } for children in children_by_parent.values_mut() { children.sort_by(|a, b| a.1.cmp(&b.1)); } - let node_pairs: Vec<_> = node_entries - .iter() - .filter_map(|e| Some((get_id(&e.id)?, e.node.clone()))) - .collect(); - - let node_names: Vec<_> = node_entries - .iter() - .filter_map(|e| { - let id = get_id(&e.id)?; - let name = e.name.clone()?; - Some((id, name)) - }) - .collect(); - + // Build internal_links from children_by_parent BEFORE consuming + // node_entries, since children_by_parent borrows &str from node_entries. let internal_links: HashMap<_, Vec<_>> = children_by_parent - .iter() - .filter_map(|(parent_str, children)| { - let parent_internal = get_id(parent_str)?; - let child_internals: Vec<_> = children - .iter() - .filter_map(|(child_str, _)| get_id(child_str)) + .into_iter() + .map(|(parent_nid, children)| { + let child_ids: Vec<_> = children + .into_iter() + .map(|(child_nid, _)| child_nid) .collect(); - if child_internals.is_empty() { - None - } else { - Some((parent_internal, child_internals)) - } + (parent_nid, child_ids) }) .collect(); + // ── 4. Single-pass consume: node_pairs + node_names + position_map ─────── + // + // Moves nodes out of node_entries — no cloning of Node enums. + let mut node_pairs: Vec<(crate::node::id::NodeId, Node)> = + Vec::with_capacity(node_entries.len()); + let mut node_names: Vec<(crate::node::id::NodeId, String)> = Vec::new(); + let mut position_map: FxHashMap = + FxHashMap::with_capacity_and_hasher(node_entries.len(), Default::default()); + + for e in node_entries.into_iter() { + if let Some(pos) = e.position { + position_map.insert(e.internal_id, pos); + } + if let Some(name) = e.name { + node_names.push((e.internal_id, name)); + } + node_pairs.push((e.internal_id, e.node)); + } + // ── 5. Produce one Scene per listed scene id ────────────────────────────── + // + // For each scene we need a SceneGraph built from the full node set. + // The last scene consumes node_pairs/internal_links by move to avoid + // a deep clone of all Node enums. Earlier scenes (multi-scene files) + // must clone. Single-scene files (the common case) get zero clones. let mut scenes: Vec = Vec::new(); - let iter: Box> = if !scene_ids_ordered.is_empty() { - Box::new(scene_ids_ordered.iter()) + // Helper: resolve scene root NodeIds from the scene string ID. + let get_scene_roots = |scene_str: &str| -> Vec { + let scene_nid = string_to_internal_id.get(scene_str).copied(); + scene_nid + .and_then(|nid| internal_links.get(&nid)) + .cloned() + .unwrap_or_default() + }; + + let scene_id_strs: Vec<&String> = if !scene_ids_ordered.is_empty() { + scene_ids_ordered.iter().collect() } else { - Box::new(scene_metas.keys()) + scene_metas.keys().collect() }; + let scene_count = scene_id_strs.len(); + + // Handle all scenes except the last one (if multi-scene) with clones. + if scene_count > 1 { + for scene_id_str in &scene_id_strs[..scene_count - 1] { + let meta = scene_metas.get(*scene_id_str); + let name = meta.map(|m| m.name.clone()).unwrap_or_default(); + let background_color = meta.and_then(|m| m.background_color); + + let roots_internal = get_scene_roots(scene_id_str); - for scene_id_str in iter { - let meta = scene_metas.get(scene_id_str); + let mut graph = SceneGraph::new_from_snapshot( + node_pairs.clone(), + internal_links.clone(), + roots_internal, + ); + + for (id, name) in &node_names { + graph.set_name(*id, name.clone()); + } + + scenes.push(Scene { + name, + graph, + background_color, + }); + } + } + + // Handle the last (or only) scene by consuming node_pairs — zero clones. + if let Some(scene_id_str) = scene_id_strs.last() { + let meta = scene_metas.get(*scene_id_str); let name = meta.map(|m| m.name.clone()).unwrap_or_default(); let background_color = meta.and_then(|m| m.background_color); - let roots_strings = children_by_parent - .get(scene_id_str) - .cloned() - .unwrap_or_default(); - let roots_internal: Vec<_> = roots_strings - .iter() - .filter_map(|(child_str, _)| get_id(child_str)) - .collect(); + let roots_internal = get_scene_roots(scene_id_str); - let mut graph = SceneGraph::new_from_snapshot( - node_pairs.clone(), - internal_links.clone(), - roots_internal, - ); + let mut graph = SceneGraph::new_from_snapshot(node_pairs, internal_links, roots_internal); - // Preserve node display names for (id, name) in &node_names { graph.set_name(*id, name.clone()); } @@ -439,22 +500,11 @@ fn decode_all_inner(bytes: &[u8]) -> Result { .map(|(s, &nid)| (nid, s.clone())) .collect(); - // Build the position map (internal NodeId → original position string) - // so the encoder can preserve child ordering exactly. - let mut position_map: HashMap = HashMap::new(); - for e in &node_entries { - if let Some((_parent_id, position)) = &e.parent { - if let Some(&nid) = string_to_internal_id.get(&e.id) { - position_map.insert(nid, position.clone()); - } - } - } - Ok(DecodeResult { scenes, id_map, scene_ids: scene_ids_ordered, - position_map, + position_map: position_map.into_iter().collect(), }) } @@ -462,15 +512,8 @@ fn decode_all_inner(bytes: &[u8]) -> Result { // Hierarchy helpers // ───────────────────────────────────────────────────────────────────────────── -fn decode_parent_ref(layer: &fbs::LayerTrait<'_>) -> Option<(String, String)> { - let parent_ref = layer.parent(); - let parent_id = parent_ref.parent_id().id().to_owned(); - if parent_id.is_empty() { - return None; - } - let position = parent_ref.position().unwrap_or("").to_owned(); - Some((parent_id, position)) -} +// decode_parent_ref removed — parent decoding is now inline in the +// decode_layer_node! macro using get_or_assign_id for zero-clone IDs. // ───────────────────────────────────────────────────────────────────────────── // Color helpers @@ -665,15 +708,18 @@ fn decode_paint_item(item: &fbs::PaintStackItem<'_>) -> Option { fit, opacity: ip.opacity(), blend_mode: decode_blend_mode(ip.blend_mode()), - filters: ip.filters().map(|f| ImageFilters { - exposure: f.exposure(), - contrast: f.contrast(), - saturation: f.saturation(), - temperature: f.temperature(), - tint: f.tint(), - highlights: f.highlights(), - shadows: f.shadows(), - }).unwrap_or_default(), + filters: ip + .filters() + .map(|f| ImageFilters { + exposure: f.exposure(), + contrast: f.contrast(), + saturation: f.saturation(), + temperature: f.temperature(), + tint: f.tint(), + highlights: f.highlights(), + shadows: f.shadows(), + }) + .unwrap_or_default(), })) } _ => None, @@ -864,26 +910,62 @@ fn decode_fe_shadow(s: &fbs::FeShadow<'_>) -> FeShadow { dy: s.dy(), blur: s.blur(), spread: s.spread(), - color: s.color().map(decode_rgba32f_to_cg_color).unwrap_or(CGColor { r: 0, g: 0, b: 0, a: 64 }), + color: s + .color() + .map(decode_rgba32f_to_cg_color) + .unwrap_or(CGColor { + r: 0, + g: 0, + b: 0, + a: 64, + }), active: s.active(), } } fn decode_fe_noise(ne: &fbs::FeNoiseEffect<'_>) -> FeNoiseEffect { - const DEFAULT_MONO: CGColor = CGColor { r: 0, g: 0, b: 0, a: 64 }; - let default_mono = || NoiseEffectColors::Mono { color: DEFAULT_MONO }; + const DEFAULT_MONO: CGColor = CGColor { + r: 0, + g: 0, + b: 0, + a: 64, + }; + let default_mono = || NoiseEffectColors::Mono { + color: DEFAULT_MONO, + }; let coloring = ne .coloring() .map(|c| match c.kind() { fbs::NoiseEffectColorsKind::Mono => NoiseEffectColors::Mono { - color: c.mono_color().map(decode_rgba32f_to_cg_color).unwrap_or(DEFAULT_MONO), + color: c + .mono_color() + .map(decode_rgba32f_to_cg_color) + .unwrap_or(DEFAULT_MONO), }, fbs::NoiseEffectColorsKind::Duo => NoiseEffectColors::Duo { - color1: c.duo_color1().map(decode_rgba32f_to_cg_color).unwrap_or(CGColor { r: 0, g: 0, b: 0, a: 128 }), - color2: c.duo_color2().map(decode_rgba32f_to_cg_color).unwrap_or(CGColor { r: 255, g: 255, b: 255, a: 128 }), + color1: c + .duo_color1() + .map(decode_rgba32f_to_cg_color) + .unwrap_or(CGColor { + r: 0, + g: 0, + b: 0, + a: 128, + }), + color2: c + .duo_color2() + .map(decode_rgba32f_to_cg_color) + .unwrap_or(CGColor { + r: 255, + g: 255, + b: 255, + a: 128, + }), + }, + fbs::NoiseEffectColorsKind::Multi => NoiseEffectColors::Multi { + opacity: c.multi_opacity(), }, - fbs::NoiseEffectColorsKind::Multi => NoiseEffectColors::Multi { opacity: c.multi_opacity() }, _ => default_mono(), }) .unwrap_or_else(default_mono); @@ -1022,7 +1104,9 @@ fn decode_rectangular_stroke_geometry( // Corner radius / smoothing // ───────────────────────────────────────────────────────────────────────────── -fn decode_corner_radius(cr: Option>) -> RectangularCornerRadius { +fn decode_corner_radius( + cr: Option>, +) -> RectangularCornerRadius { use crate::cg::types::Radius; match cr.and_then(|t| t.rectangular_corner_radius()) { Some(rcr) => RectangularCornerRadius { @@ -1245,7 +1329,14 @@ fn decode_vector_network(vnd: Option>) -> VectorNetwo let vertices: Vec<(f32, f32)> = vnd .vertices() - .map(|v| (0..v.len()).map(|i| { let p = v.get(i); (p.x(), p.y()) }).collect()) + .map(|v| { + (0..v.len()) + .map(|i| { + let p = v.get(i); + (p.x(), p.y()) + }) + .collect() + }) .unwrap_or_default(); let segments: Vec = vnd @@ -1347,9 +1438,18 @@ fn decode_container_node( cn: &fbs::ContainerNode<'_>, ) -> Node { let layout = layer.layout(); - let position = layout.as_ref().map(decode_layout_position).unwrap_or_default(); - let layout_container = layout.as_ref().map(decode_layout_container_style).unwrap_or_default(); - let layout_dimensions = layout.as_ref().map(decode_layout_dimension_style).unwrap_or_default(); + let position = layout + .as_ref() + .map(decode_layout_position) + .unwrap_or_default(); + let layout_container = layout + .as_ref() + .map(decode_layout_container_style) + .unwrap_or_default(); + let layout_dimensions = layout + .as_ref() + .map(decode_layout_dimension_style) + .unwrap_or_default(); let (stroke_style, stroke_width) = decode_rectangular_stroke_geometry(cn.stroke_geometry()); Node::Container(ContainerNodeRec { @@ -1379,14 +1479,21 @@ fn decode_initial_container_node( _icn: &fbs::InitialContainerNode<'_>, ) -> Node { let layout = layer.layout(); - let lcs = layout.as_ref().map(decode_layout_container_style).unwrap_or_default(); + let lcs = layout + .as_ref() + .map(decode_layout_container_style) + .unwrap_or_default(); Node::InitialContainer(InitialContainerNodeRec { active: lc.active, layout_mode: lcs.layout_mode, layout_direction: lcs.layout_direction, layout_wrap: lcs.layout_wrap.unwrap_or(LayoutWrap::NoWrap), - layout_main_axis_alignment: lcs.layout_main_axis_alignment.unwrap_or(MainAxisAlignment::Start), - layout_cross_axis_alignment: lcs.layout_cross_axis_alignment.unwrap_or(CrossAxisAlignment::Start), + layout_main_axis_alignment: lcs + .layout_main_axis_alignment + .unwrap_or(MainAxisAlignment::Start), + layout_cross_axis_alignment: lcs + .layout_cross_axis_alignment + .unwrap_or(CrossAxisAlignment::Start), padding: lcs.layout_padding.unwrap_or_default(), layout_gap: lcs.layout_gap.unwrap_or(LayoutGap { main_axis_gap: 0.0, @@ -1418,7 +1525,12 @@ fn decode_basic_shape_node( } } else { let r = Radius::circular(bsn.corner_radius()); - RectangularCornerRadius { tl: r, tr: r, bl: r, br: r } + RectangularCornerRadius { + tl: r, + tr: r, + bl: r, + br: r, + } } }; let stroke_style = decode_stroke_style_from_fbs(bsn.stroke_style()); @@ -1444,11 +1556,17 @@ fn decode_basic_shape_node( let bottom = rsw.stroke_bottom_width(); let left = rsw.stroke_left_width(); if top == right && right == bottom && bottom == left { - if top == 0.0 { StrokeWidth::None } else { StrokeWidth::Uniform(top) } + if top == 0.0 { + StrokeWidth::None + } else { + StrokeWidth::Uniform(top) + } } else { StrokeWidth::Rectangular(RectangularStrokeWidth { - stroke_top_width: top, stroke_right_width: right, - stroke_bottom_width: bottom, stroke_left_width: left, + stroke_top_width: top, + stroke_right_width: right, + stroke_bottom_width: bottom, + stroke_left_width: left, }) } } else if stroke_width_f32 == 0.0 { @@ -1493,7 +1611,11 @@ fn decode_basic_shape_node( angle, corner_radius: { let cr = bsn.corner_radius(); - if cr == 0.0 { None } else { Some(cr) } + if cr == 0.0 { + None + } else { + Some(cr) + } }, effects: lc.effects.clone(), layout_child: lc.layout_child.clone(), @@ -1585,7 +1707,11 @@ fn decode_vector_node( mask: lc.mask, transform: sl.transform, network: decode_vector_network(vn.vector_network_data()), - corner_radius: vn.corner_radius().and_then(|cr| cr.corner_radius()).map(|r| r.rx()).unwrap_or(0.0), + corner_radius: vn + .corner_radius() + .and_then(|cr| cr.corner_radius()) + .map(|r| r.rx()) + .unwrap_or(0.0), fills: decode_paints_vec(vn.fill_paints()), strokes: decode_paints_vec(vn.stroke_paints()), stroke_width: stroke_width_f32, @@ -1602,11 +1728,7 @@ fn decode_vector_node( }) } -fn decode_path_node( - lc: &LayerCommon, - layer: &fbs::LayerTrait<'_>, - pn: &fbs::PathNode<'_>, -) -> Node { +fn decode_path_node(lc: &LayerCommon, layer: &fbs::LayerTrait<'_>, pn: &fbs::PathNode<'_>) -> Node { let sl = decode_shape_layout(layer, lc.rotation_cos_sin); let (stroke_style, stroke_width_f32, _stroke_width_profile) = decode_stroke_geometry_trait(pn.stroke_geometry()); @@ -1627,11 +1749,7 @@ fn decode_path_node( }) } -fn decode_line_node( - lc: &LayerCommon, - layer: &fbs::LayerTrait<'_>, - ln: &fbs::LineNode<'_>, -) -> Node { +fn decode_line_node(lc: &LayerCommon, layer: &fbs::LayerTrait<'_>, ln: &fbs::LineNode<'_>) -> Node { let sl = decode_shape_layout(layer, lc.rotation_cos_sin); let sg = ln.stroke_geometry(); let stroke_width = sg.as_ref().map(|s| s.stroke_width()).unwrap_or(0.0); @@ -1645,14 +1763,11 @@ fn decode_line_node( .and_then(|s| s.stroke_style()) .map(|ss| StrokeMiterLimit(ss.stroke_miter_limit())) .unwrap_or_default(); - let stroke_dash_array = sg - .as_ref() - .and_then(|s| s.stroke_style()) - .and_then(|ss| { - ss.stroke_dash_array() - .filter(|v| v.len() > 0) - .map(|v| StrokeDashArray((0..v.len()).map(|i| v.get(i)).collect())) - }); + let stroke_dash_array = sg.as_ref().and_then(|s| s.stroke_style()).and_then(|ss| { + ss.stroke_dash_array() + .filter(|v| v.len() > 0) + .map(|v| StrokeDashArray((0..v.len()).map(|i| v.get(i)).collect())) + }); // Lines use translation + rotation (no center-origin) and height=0. // Use raw cos/sin to avoid lossy degree conversion. @@ -1752,50 +1867,72 @@ fn decode_text_span_node( .unwrap_or(FontOpticalSizing::Auto); rec.text_decoration = ts.text_decoration().map(|td| TextDecorationRec { text_decoration_line: decode_text_decoration_line(td.text_decoration_line()), - text_decoration_color: td.text_decoration_color().map(|c| decode_rgba32f_to_cg_color(c)), - text_decoration_style: Some(decode_text_decoration_style(td.text_decoration_style())), + text_decoration_color: td + .text_decoration_color() + .map(|c| decode_rgba32f_to_cg_color(c)), + text_decoration_style: Some(decode_text_decoration_style( + td.text_decoration_style(), + )), text_decoration_skip_ink: Some(td.text_decoration_skip_ink()), text_decoration_thickness: { let t = td.text_decoration_thickness(); - if t == 0.0 { None } else { Some(t) } + if t == 0.0 { + None + } else { + Some(t) + } }, }); - rec.letter_spacing = ts.letter_spacing().map(|td| { - match td.kind() { - fbs::TextDimensionKind::Factor => TextLetterSpacing::Factor(td.value().unwrap_or(0.0)), + rec.letter_spacing = ts + .letter_spacing() + .map(|td| match td.kind() { + fbs::TextDimensionKind::Factor => { + TextLetterSpacing::Factor(td.value().unwrap_or(0.0)) + } _ => TextLetterSpacing::Fixed(td.value().unwrap_or(0.0)), - } - }).unwrap_or_default(); - rec.word_spacing = ts.word_spacing().map(|td| { - match td.kind() { - fbs::TextDimensionKind::Factor => TextWordSpacing::Factor(td.value().unwrap_or(0.0)), + }) + .unwrap_or_default(); + rec.word_spacing = ts + .word_spacing() + .map(|td| match td.kind() { + fbs::TextDimensionKind::Factor => { + TextWordSpacing::Factor(td.value().unwrap_or(0.0)) + } _ => TextWordSpacing::Fixed(td.value().unwrap_or(0.0)), - } - }).unwrap_or_default(); - rec.line_height = ts.line_height().map(|td| { - match td.kind() { + }) + .unwrap_or_default(); + rec.line_height = ts + .line_height() + .map(|td| match td.kind() { fbs::TextDimensionKind::Normal => TextLineHeight::Normal, - fbs::TextDimensionKind::Factor => TextLineHeight::Factor(td.value().unwrap_or(1.0)), + fbs::TextDimensionKind::Factor => { + TextLineHeight::Factor(td.value().unwrap_or(1.0)) + } _ => TextLineHeight::Fixed(td.value().unwrap_or(0.0)), - } - }).unwrap_or_default(); + }) + .unwrap_or_default(); rec.font_features = ts.font_features().map(|ff| { - (0..ff.len()).filter_map(|i| { - let f = ff.get(i); - f.open_type_feature_tag().map(|tag| FontFeature { - tag: String::from_utf8_lossy(&[tag.a(), tag.b(), tag.c(), tag.d()]).into_owned(), - value: f.open_type_feature_value(), + (0..ff.len()) + .filter_map(|i| { + let f = ff.get(i); + f.open_type_feature_tag().map(|tag| FontFeature { + tag: String::from_utf8_lossy(&[tag.a(), tag.b(), tag.c(), tag.d()]) + .into_owned(), + value: f.open_type_feature_value(), + }) }) - }).collect() + .collect() }); rec.font_variations = ts.font_variations().map(|fv| { - (0..fv.len()).map(|i| { - let v = fv.get(i); - FontVariation { - axis: v.variation_axis().to_owned(), - value: v.variation_value(), - } - }).collect() + (0..fv.len()) + .map(|i| { + let v = fv.get(i); + FontVariation { + axis: v.variation_axis().to_owned(), + value: v.variation_value(), + } + }) + .collect() }); rec }) @@ -1841,8 +1978,17 @@ fn decode_text_span_node( text_style, text_align, text_align_vertical, - max_lines: props.as_ref().map(|p| p.max_lines()).and_then(|v| if v == 0 { None } else { Some(v as usize) }), - ellipsis: props.as_ref().and_then(|p| p.ellipsis()).map(|s| s.to_owned()), + max_lines: props.as_ref().map(|p| p.max_lines()).and_then(|v| { + if v == 0 { + None + } else { + Some(v as usize) + } + }), + ellipsis: props + .as_ref() + .and_then(|p| p.ellipsis()) + .map(|s| s.to_owned()), fills: fill_paints, strokes: stroke_paints, stroke_width, @@ -1906,7 +2052,9 @@ pub fn encode( let scene_id_str = fbb.create_string(scene_id); let scene_nid = fbs::NodeIdentifier::create( &mut fbb, - &fbs::NodeIdentifierArgs { id: Some(scene_id_str) }, + &fbs::NodeIdentifierArgs { + id: Some(scene_id_str), + }, ); let scenes_vec = fbb.create_vector(&[scene_nid]); @@ -1924,7 +2072,9 @@ pub fn encode( // ── 5. Build GridaFile root ───────────────────────────────────────────── let root = fbs::GridaFile::create( &mut fbb, - &fbs::GridaFileArgs { document: Some(doc) }, + &fbs::GridaFileArgs { + document: Some(doc), + }, ); fbb.finish(root, Some("GRID")); @@ -1990,7 +2140,9 @@ pub fn encode_multi( ); let root = fbs::GridaFile::create( &mut fbb, - &fbs::GridaFileArgs { document: Some(doc) }, + &fbs::GridaFileArgs { + document: Some(doc), + }, ); fbb.finish(root, Some("GRID")); fbb.finished_data().to_vec() @@ -2085,11 +2237,14 @@ fn encode_scene_node<'a, A: flatbuffers::Allocator + 'a>( let bg = scene.background_color.map(|c| encode_color_to_rgba32f(&c)); - let sn = fbs::SceneNode::create(fbb, &fbs::SceneNodeArgs { - node: Some(sys), - scene_background_color: bg.as_ref(), - ..Default::default() - }); + let sn = fbs::SceneNode::create( + fbb, + &fbs::SceneNodeArgs { + node: Some(sys), + scene_background_color: bg.as_ref(), + ..Default::default() + }, + ); make_node_slot(fbb, fbs::Node::SceneNode, sn.as_union_value()) } @@ -2215,8 +2370,12 @@ fn encode_layer_trait<'a, A: flatbuffers::Allocator + 'a>( ) -> flatbuffers::WIPOffset> { // Parent reference let parent_id_str = fbb.create_string(input.parent_id); - let parent_nid = - fbs::NodeIdentifier::create(fbb, &fbs::NodeIdentifierArgs { id: Some(parent_id_str) }); + let parent_nid = fbs::NodeIdentifier::create( + fbb, + &fbs::NodeIdentifierArgs { + id: Some(parent_id_str), + }, + ); let pos_str = fbb.create_string(input.position); let parent_ref = fbs::ParentReference::create( fbb, @@ -2306,7 +2465,10 @@ fn encode_layer_blend_mode(lbm: LayerBlendMode) -> fbs::LayerBlendMode { fn encode_mask_type<'a, A: flatbuffers::Allocator + 'a>( fbb: &mut flatbuffers::FlatBufferBuilder<'a, A>, mask: Option, -) -> (fbs::LayerMaskType, Option>) { +) -> ( + fbs::LayerMaskType, + Option>, +) { match mask { None => (fbs::LayerMaskType::NONE, None), Some(LayerMaskType::Image(imt)) => { @@ -2326,10 +2488,7 @@ fn encode_mask_type<'a, A: flatbuffers::Allocator + 'a>( ) } Some(LayerMaskType::Geometry) => { - let table = fbs::LayerMaskTypeGeometry::create( - fbb, - &fbs::LayerMaskTypeGeometryArgs {}, - ); + let table = fbs::LayerMaskTypeGeometry::create(fbb, &fbs::LayerMaskTypeGeometryArgs {}); ( fbs::LayerMaskType::LayerMaskTypeGeometry, Some(table.as_union_value()), @@ -2345,7 +2504,11 @@ fn encode_mask_type<'a, A: flatbuffers::Allocator + 'a>( fn encode_paints<'a, A: flatbuffers::Allocator + 'a>( fbb: &mut flatbuffers::FlatBufferBuilder<'a, A>, paints: &Paints, -) -> Option>>>> { +) -> Option< + flatbuffers::WIPOffset< + flatbuffers::Vector<'a, flatbuffers::ForwardsUOffset>>, + >, +> { if paints.is_empty() { return None; } @@ -2364,118 +2527,166 @@ fn encode_paint_item<'a, A: flatbuffers::Allocator + 'a>( match paint { Paint::Solid(sp) => { let color = encode_color_to_rgba32f(&sp.color); - let solid = fbs::SolidPaint::create(fbb, &fbs::SolidPaintArgs { - active: sp.active, - color: Some(&color), - blend_mode: encode_blend_mode(sp.blend_mode), - }); - Some(fbs::PaintStackItem::create(fbb, &fbs::PaintStackItemArgs { - paint_type: fbs::Paint::SolidPaint, - paint: Some(solid.as_union_value()), - })) + let solid = fbs::SolidPaint::create( + fbb, + &fbs::SolidPaintArgs { + active: sp.active, + color: Some(&color), + blend_mode: encode_blend_mode(sp.blend_mode), + }, + ); + Some(fbs::PaintStackItem::create( + fbb, + &fbs::PaintStackItemArgs { + paint_type: fbs::Paint::SolidPaint, + paint: Some(solid.as_union_value()), + }, + )) } Paint::LinearGradient(lg) => { let stops = encode_gradient_stops(fbb, &lg.stops); let xy1 = fbs::Alignment::new(lg.xy1.0, lg.xy1.1); let xy2 = fbs::Alignment::new(lg.xy2.0, lg.xy2.1); let transform = encode_affine_to_cg_transform(&lg.transform); - let lgp = fbs::LinearGradientPaint::create(fbb, &fbs::LinearGradientPaintArgs { - active: lg.active, - xy1: Some(&xy1), - xy2: Some(&xy2), - stops: Some(stops), - opacity: lg.opacity, - blend_mode: encode_blend_mode(lg.blend_mode), - transform: Some(&transform), - tile_mode: encode_tile_mode(lg.tile_mode), - }); - Some(fbs::PaintStackItem::create(fbb, &fbs::PaintStackItemArgs { - paint_type: fbs::Paint::LinearGradientPaint, - paint: Some(lgp.as_union_value()), - })) + let lgp = fbs::LinearGradientPaint::create( + fbb, + &fbs::LinearGradientPaintArgs { + active: lg.active, + xy1: Some(&xy1), + xy2: Some(&xy2), + stops: Some(stops), + opacity: lg.opacity, + blend_mode: encode_blend_mode(lg.blend_mode), + transform: Some(&transform), + tile_mode: encode_tile_mode(lg.tile_mode), + }, + ); + Some(fbs::PaintStackItem::create( + fbb, + &fbs::PaintStackItemArgs { + paint_type: fbs::Paint::LinearGradientPaint, + paint: Some(lgp.as_union_value()), + }, + )) } Paint::RadialGradient(rg) => { let stops = encode_gradient_stops(fbb, &rg.stops); let transform = encode_affine_to_cg_transform(&rg.transform); - let rgp = fbs::RadialGradientPaint::create(fbb, &fbs::RadialGradientPaintArgs { - active: rg.active, - stops: Some(stops), - opacity: rg.opacity, - blend_mode: encode_blend_mode(rg.blend_mode), - transform: Some(&transform), - tile_mode: encode_tile_mode(rg.tile_mode), - }); - Some(fbs::PaintStackItem::create(fbb, &fbs::PaintStackItemArgs { - paint_type: fbs::Paint::RadialGradientPaint, - paint: Some(rgp.as_union_value()), - })) + let rgp = fbs::RadialGradientPaint::create( + fbb, + &fbs::RadialGradientPaintArgs { + active: rg.active, + stops: Some(stops), + opacity: rg.opacity, + blend_mode: encode_blend_mode(rg.blend_mode), + transform: Some(&transform), + tile_mode: encode_tile_mode(rg.tile_mode), + }, + ); + Some(fbs::PaintStackItem::create( + fbb, + &fbs::PaintStackItemArgs { + paint_type: fbs::Paint::RadialGradientPaint, + paint: Some(rgp.as_union_value()), + }, + )) } Paint::SweepGradient(sg) => { let stops = encode_gradient_stops(fbb, &sg.stops); let transform = encode_affine_to_cg_transform(&sg.transform); - let sgp = fbs::SweepGradientPaint::create(fbb, &fbs::SweepGradientPaintArgs { - active: sg.active, - stops: Some(stops), - opacity: sg.opacity, - blend_mode: encode_blend_mode(sg.blend_mode), - transform: Some(&transform), - }); - Some(fbs::PaintStackItem::create(fbb, &fbs::PaintStackItemArgs { - paint_type: fbs::Paint::SweepGradientPaint, - paint: Some(sgp.as_union_value()), - })) + let sgp = fbs::SweepGradientPaint::create( + fbb, + &fbs::SweepGradientPaintArgs { + active: sg.active, + stops: Some(stops), + opacity: sg.opacity, + blend_mode: encode_blend_mode(sg.blend_mode), + transform: Some(&transform), + }, + ); + Some(fbs::PaintStackItem::create( + fbb, + &fbs::PaintStackItemArgs { + paint_type: fbs::Paint::SweepGradientPaint, + paint: Some(sgp.as_union_value()), + }, + )) } Paint::Image(ip) => { let image_ref_offset = match &ip.image { ResourceRef::HASH(h) => { let hash_str = fbb.create_string(h); - let href = fbs::ResourceRefHASH::create(fbb, &fbs::ResourceRefHASHArgs { hash: Some(hash_str) }); + let href = fbs::ResourceRefHASH::create( + fbb, + &fbs::ResourceRefHASHArgs { + hash: Some(hash_str), + }, + ); (fbs::ResourceRef::ResourceRefHASH, href.as_union_value()) } ResourceRef::RID(r) => { let rid_str = fbb.create_string(r); - let rref = fbs::ResourceRefRID::create(fbb, &fbs::ResourceRefRIDArgs { rid: Some(rid_str) }); + let rref = fbs::ResourceRefRID::create( + fbb, + &fbs::ResourceRefRIDArgs { rid: Some(rid_str) }, + ); (fbs::ResourceRef::ResourceRefRID, rref.as_union_value()) } }; let alignment = fbs::Alignment::new(ip.alignement.0, ip.alignement.1); let (fit_type, fit_value) = encode_image_paint_fit(fbb, &ip.fit); let fbs_filters = fbs::ImageFilters::new( - ip.filters.exposure, ip.filters.contrast, ip.filters.saturation, - ip.filters.temperature, ip.filters.tint, ip.filters.highlights, + ip.filters.exposure, + ip.filters.contrast, + ip.filters.saturation, + ip.filters.temperature, + ip.filters.tint, + ip.filters.highlights, ip.filters.shadows, ); - let ip_offset = fbs::ImagePaint::create(fbb, &fbs::ImagePaintArgs { - active: ip.active, - image_type: image_ref_offset.0, - image: Some(image_ref_offset.1), - quarter_turns: ip.quarter_turns, - alignement: Some(&alignment), - fit_type, - fit: Some(fit_value), - opacity: ip.opacity, - blend_mode: encode_blend_mode(ip.blend_mode), - filters: Some(&fbs_filters), - }); - Some(fbs::PaintStackItem::create(fbb, &fbs::PaintStackItemArgs { - paint_type: fbs::Paint::ImagePaint, - paint: Some(ip_offset.as_union_value()), - })) + let ip_offset = fbs::ImagePaint::create( + fbb, + &fbs::ImagePaintArgs { + active: ip.active, + image_type: image_ref_offset.0, + image: Some(image_ref_offset.1), + quarter_turns: ip.quarter_turns, + alignement: Some(&alignment), + fit_type, + fit: Some(fit_value), + opacity: ip.opacity, + blend_mode: encode_blend_mode(ip.blend_mode), + filters: Some(&fbs_filters), + }, + ); + Some(fbs::PaintStackItem::create( + fbb, + &fbs::PaintStackItemArgs { + paint_type: fbs::Paint::ImagePaint, + paint: Some(ip_offset.as_union_value()), + }, + )) } Paint::DiamondGradient(dg) => { let stops = encode_gradient_stops(fbb, &dg.stops); let transform = encode_affine_to_cg_transform(&dg.transform); - let dgp = fbs::DiamondGradientPaint::create(fbb, &fbs::DiamondGradientPaintArgs { - active: dg.active, - stops: Some(stops), - opacity: dg.opacity, - blend_mode: encode_blend_mode(dg.blend_mode), - transform: Some(&transform), - }); - Some(fbs::PaintStackItem::create(fbb, &fbs::PaintStackItemArgs { - paint_type: fbs::Paint::DiamondGradientPaint, - paint: Some(dgp.as_union_value()), - })) + let dgp = fbs::DiamondGradientPaint::create( + fbb, + &fbs::DiamondGradientPaintArgs { + active: dg.active, + stops: Some(stops), + opacity: dg.opacity, + blend_mode: encode_blend_mode(dg.blend_mode), + transform: Some(&transform), + }, + ); + Some(fbs::PaintStackItem::create( + fbb, + &fbs::PaintStackItemArgs { + paint_type: fbs::Paint::DiamondGradientPaint, + paint: Some(dgp.as_union_value()), + }, + )) } } } @@ -2483,16 +2694,32 @@ fn encode_paint_item<'a, A: flatbuffers::Allocator + 'a>( fn encode_image_paint_fit<'a, A: flatbuffers::Allocator + 'a>( fbb: &mut flatbuffers::FlatBufferBuilder<'a, A>, fit: &ImagePaintFit, -) -> (fbs::ImagePaintFit, flatbuffers::WIPOffset) { +) -> ( + fbs::ImagePaintFit, + flatbuffers::WIPOffset, +) { match fit { ImagePaintFit::Fit(box_fit) => { - let f = fbs::ImagePaintFitFit::create(fbb, &fbs::ImagePaintFitFitArgs { box_fit: encode_box_fit(*box_fit) }); + let f = fbs::ImagePaintFitFit::create( + fbb, + &fbs::ImagePaintFitFitArgs { + box_fit: encode_box_fit(*box_fit), + }, + ); (fbs::ImagePaintFit::ImagePaintFitFit, f.as_union_value()) } ImagePaintFit::Transform(t) => { let ct = encode_affine_to_cg_transform(t); - let f = fbs::ImagePaintFitTransform::create(fbb, &fbs::ImagePaintFitTransformArgs { transform: Some(&ct) }); - (fbs::ImagePaintFit::ImagePaintFitTransform, f.as_union_value()) + let f = fbs::ImagePaintFitTransform::create( + fbb, + &fbs::ImagePaintFitTransformArgs { + transform: Some(&ct), + }, + ); + ( + fbs::ImagePaintFit::ImagePaintFitTransform, + f.as_union_value(), + ) } ImagePaintFit::Tile(tile) => { let fbs_repeat = match tile.repeat { @@ -2501,7 +2728,12 @@ fn encode_image_paint_fit<'a, A: flatbuffers::Allocator + 'a>( crate::cg::types::ImageRepeat::Repeat => fbs::ImageRepeat::Repeat, }; let fbs_tile = fbs::ImageTile::new(tile.scale, fbs_repeat); - let f = fbs::ImagePaintFitTile::create(fbb, &fbs::ImagePaintFitTileArgs { tile: Some(&fbs_tile) }); + let f = fbs::ImagePaintFitTile::create( + fbb, + &fbs::ImagePaintFitTileArgs { + tile: Some(&fbs_tile), + }, + ); (fbs::ImagePaintFit::ImagePaintFitTile, f.as_union_value()) } } @@ -2542,15 +2774,20 @@ fn encode_text_dimension_from_letter_spacing<'a, A: flatbuffers::Allocator + 'a> ) -> Option>> { let (kind, value) = match ls { TextLetterSpacing::Fixed(v) => { - if *v == 0.0 { return None; } + if *v == 0.0 { + return None; + } (fbs::TextDimensionKind::Fixed, *v) } TextLetterSpacing::Factor(v) => (fbs::TextDimensionKind::Factor, *v), }; - Some(fbs::TextDimension::create(fbb, &fbs::TextDimensionArgs { - kind, - value: Some(value), - })) + Some(fbs::TextDimension::create( + fbb, + &fbs::TextDimensionArgs { + kind, + value: Some(value), + }, + )) } fn encode_text_dimension_from_word_spacing<'a, A: flatbuffers::Allocator + 'a>( @@ -2559,15 +2796,20 @@ fn encode_text_dimension_from_word_spacing<'a, A: flatbuffers::Allocator + 'a>( ) -> Option>> { let (kind, value) = match ws { TextWordSpacing::Fixed(v) => { - if *v == 0.0 { return None; } + if *v == 0.0 { + return None; + } (fbs::TextDimensionKind::Fixed, *v) } TextWordSpacing::Factor(v) => (fbs::TextDimensionKind::Factor, *v), }; - Some(fbs::TextDimension::create(fbb, &fbs::TextDimensionArgs { - kind, - value: Some(value), - })) + Some(fbs::TextDimension::create( + fbb, + &fbs::TextDimensionArgs { + kind, + value: Some(value), + }, + )) } fn encode_text_dimension_from_line_height<'a, A: flatbuffers::Allocator + 'a>( @@ -2576,14 +2818,20 @@ fn encode_text_dimension_from_line_height<'a, A: flatbuffers::Allocator + 'a>( ) -> Option>> { match lh { TextLineHeight::Normal => None, - TextLineHeight::Fixed(v) => Some(fbs::TextDimension::create(fbb, &fbs::TextDimensionArgs { - kind: fbs::TextDimensionKind::Fixed, - value: Some(*v), - })), - TextLineHeight::Factor(v) => Some(fbs::TextDimension::create(fbb, &fbs::TextDimensionArgs { - kind: fbs::TextDimensionKind::Factor, - value: Some(*v), - })), + TextLineHeight::Fixed(v) => Some(fbs::TextDimension::create( + fbb, + &fbs::TextDimensionArgs { + kind: fbs::TextDimensionKind::Fixed, + value: Some(*v), + }, + )), + TextLineHeight::Factor(v) => Some(fbs::TextDimension::create( + fbb, + &fbs::TextDimensionArgs { + kind: fbs::TextDimensionKind::Factor, + value: Some(*v), + }, + )), } } @@ -2595,7 +2843,10 @@ fn encode_text_dimension_from_line_height<'a, A: flatbuffers::Allocator + 'a>( fn encode_fe_blur<'a, A: flatbuffers::Allocator + 'a>( fbb: &mut flatbuffers::FlatBufferBuilder<'a, A>, blur: &FeBlur, -) -> (fbs::FeBlur, flatbuffers::WIPOffset) { +) -> ( + fbs::FeBlur, + flatbuffers::WIPOffset, +) { match blur { FeBlur::Gaussian(g) => { let offset = @@ -2634,55 +2885,75 @@ fn encode_layer_effects<'a, A: flatbuffers::Allocator + 'a>( let blur_offset = effects.blur.as_ref().map(|lb| { let (blur_type, blur_union) = encode_fe_blur(fbb, &lb.blur); - fbs::FeLayerBlur::create(fbb, &fbs::FeLayerBlurArgs { - active: lb.active, - blur_type, - blur: Some(blur_union), - }) + fbs::FeLayerBlur::create( + fbb, + &fbs::FeLayerBlurArgs { + active: lb.active, + blur_type, + blur: Some(blur_union), + }, + ) }); let backdrop_blur_offset = effects.backdrop_blur.as_ref().map(|bb| { let (blur_type, blur_union) = encode_fe_blur(fbb, &bb.blur); - fbs::FeBackdropBlur::create(fbb, &fbs::FeBackdropBlurArgs { - active: bb.active, - blur_type, - blur: Some(blur_union), - }) + fbs::FeBackdropBlur::create( + fbb, + &fbs::FeBackdropBlurArgs { + active: bb.active, + blur_type, + blur: Some(blur_union), + }, + ) }); let shadows_offset = if effects.shadows.is_empty() { None } else { - let shadow_items: Vec<_> = effects.shadows.iter().map(|s| encode_filter_shadow_effect(fbb, s)).collect(); + let shadow_items: Vec<_> = effects + .shadows + .iter() + .map(|s| encode_filter_shadow_effect(fbb, s)) + .collect(); Some(fbb.create_vector(&shadow_items)) }; let glass_offset = effects.glass.as_ref().map(|lg| { - fbs::FeLiquidGlass::create(fbb, &fbs::FeLiquidGlassArgs { - active: lg.active, - light_intensity: lg.light_intensity, - light_angle: lg.light_angle, - refraction: lg.refraction, - depth: lg.depth, - dispersion: lg.dispersion, - blur_radius: lg.blur_radius, - }) + fbs::FeLiquidGlass::create( + fbb, + &fbs::FeLiquidGlassArgs { + active: lg.active, + light_intensity: lg.light_intensity, + light_angle: lg.light_angle, + refraction: lg.refraction, + depth: lg.depth, + dispersion: lg.dispersion, + blur_radius: lg.blur_radius, + }, + ) }); let noises_offset = if effects.noises.is_empty() { None } else { - let noise_items: Vec<_> = effects.noises.iter().map(|n| encode_fe_noise_effect(fbb, n)).collect(); + let noise_items: Vec<_> = effects + .noises + .iter() + .map(|n| encode_fe_noise_effect(fbb, n)) + .collect(); Some(fbb.create_vector(&noise_items)) }; - Some(fbs::LayerEffects::create(fbb, &fbs::LayerEffectsArgs { - fe_blur: blur_offset, - fe_backdrop_blur: backdrop_blur_offset, - fe_shadows: shadows_offset, - fe_glass: glass_offset, - fe_noises: noises_offset, - })) + Some(fbs::LayerEffects::create( + fbb, + &fbs::LayerEffectsArgs { + fe_blur: blur_offset, + fe_backdrop_blur: backdrop_blur_offset, + fe_shadows: shadows_offset, + fe_glass: glass_offset, + fe_noises: noises_offset, + }, + )) } fn encode_filter_shadow_effect<'a, A: flatbuffers::Allocator + 'a>( @@ -2694,13 +2965,24 @@ fn encode_filter_shadow_effect<'a, A: flatbuffers::Allocator + 'a>( FilterShadowEffect::InnerShadow(s) => (fbs::FilterShadowEffectKind::InnerShadow, s), }; let color = encode_color_to_rgba32f(&shadow.color); - let shadow_offset = fbs::FeShadow::create(fbb, &fbs::FeShadowArgs { - active: shadow.active, dx: shadow.dx, dy: shadow.dy, - blur: shadow.blur, spread: shadow.spread, color: Some(&color), - }); - fbs::FilterShadowEffect::create(fbb, &fbs::FilterShadowEffectArgs { - kind, shadow: Some(shadow_offset), - }) + let shadow_offset = fbs::FeShadow::create( + fbb, + &fbs::FeShadowArgs { + active: shadow.active, + dx: shadow.dx, + dy: shadow.dy, + blur: shadow.blur, + spread: shadow.spread, + color: Some(&color), + }, + ); + fbs::FilterShadowEffect::create( + fbb, + &fbs::FilterShadowEffectArgs { + kind, + shadow: Some(shadow_offset), + }, + ) } fn encode_fe_noise_effect<'a, A: flatbuffers::Allocator + 'a>( @@ -2708,11 +2990,18 @@ fn encode_fe_noise_effect<'a, A: flatbuffers::Allocator + 'a>( noise: &FeNoiseEffect, ) -> flatbuffers::WIPOffset> { let coloring = encode_noise_colors(fbb, &noise.coloring); - fbs::FeNoiseEffect::create(fbb, &fbs::FeNoiseEffectArgs { - active: noise.active, noise_size: noise.noise_size, density: noise.density, - num_octaves: noise.num_octaves, seed: noise.seed, coloring: Some(coloring), - blend_mode: encode_blend_mode(noise.blend_mode), - }) + fbs::FeNoiseEffect::create( + fbb, + &fbs::FeNoiseEffectArgs { + active: noise.active, + noise_size: noise.noise_size, + density: noise.density, + num_octaves: noise.num_octaves, + seed: noise.seed, + coloring: Some(coloring), + blend_mode: encode_blend_mode(noise.blend_mode), + }, + ) } fn encode_noise_colors<'a, A: flatbuffers::Allocator + 'a>( @@ -2722,22 +3011,36 @@ fn encode_noise_colors<'a, A: flatbuffers::Allocator + 'a>( match colors { NoiseEffectColors::Mono { color } => { let c = encode_color_to_rgba32f(color); - fbs::NoiseEffectColors::create(fbb, &fbs::NoiseEffectColorsArgs { - kind: fbs::NoiseEffectColorsKind::Mono, mono_color: Some(&c), ..Default::default() - }) + fbs::NoiseEffectColors::create( + fbb, + &fbs::NoiseEffectColorsArgs { + kind: fbs::NoiseEffectColorsKind::Mono, + mono_color: Some(&c), + ..Default::default() + }, + ) } NoiseEffectColors::Duo { color1, color2 } => { let c1 = encode_color_to_rgba32f(color1); let c2 = encode_color_to_rgba32f(color2); - fbs::NoiseEffectColors::create(fbb, &fbs::NoiseEffectColorsArgs { - kind: fbs::NoiseEffectColorsKind::Duo, duo_color1: Some(&c1), duo_color2: Some(&c2), ..Default::default() - }) - } - NoiseEffectColors::Multi { opacity } => { - fbs::NoiseEffectColors::create(fbb, &fbs::NoiseEffectColorsArgs { - kind: fbs::NoiseEffectColorsKind::Multi, multi_opacity: *opacity, ..Default::default() - }) + fbs::NoiseEffectColors::create( + fbb, + &fbs::NoiseEffectColorsArgs { + kind: fbs::NoiseEffectColorsKind::Duo, + duo_color1: Some(&c1), + duo_color2: Some(&c2), + ..Default::default() + }, + ) } + NoiseEffectColors::Multi { opacity } => fbs::NoiseEffectColors::create( + fbb, + &fbs::NoiseEffectColorsArgs { + kind: fbs::NoiseEffectColorsKind::Multi, + multi_opacity: *opacity, + ..Default::default() + }, + ), } } @@ -2933,9 +3236,10 @@ fn encode_container_layout<'a, A: flatbuffers::Allocator + 'a>( // Container style — use FBS `::None` sentinels for unset optional enum fields // so the decoder can distinguish "not set" from "explicitly set to default". - let padding = container_style.layout_padding.as_ref().map(|p| { - fbs::EdgeInsets::new(p.top, p.right, p.bottom, p.left) - }); + let padding = container_style + .layout_padding + .as_ref() + .map(|p| fbs::EdgeInsets::new(p.top, p.right, p.bottom, p.left)); let lc_fbs = Some(fbs::LayoutContainerStyle::create( fbb, &fbs::LayoutContainerStyleArgs { @@ -2976,7 +3280,12 @@ fn encode_container_layout<'a, A: flatbuffers::Allocator + 'a>( }, )); - let dims = encode_dimensions_with_aspect(fbb, dimensions.layout_target_width, dimensions.layout_target_height, dimensions.layout_target_aspect_ratio); + let dims = encode_dimensions_with_aspect( + fbb, + dimensions.layout_target_width, + dimensions.layout_target_height, + dimensions.layout_target_aspect_ratio, + ); let child = encode_layout_child_style(fbb, layout_child); fbs::LayoutStyle::create( fbb, @@ -3021,10 +3330,7 @@ fn encode_stroke_geometry<'a, A: flatbuffers::Allocator + 'a>( .stops .iter() .map(|s| { - fbs::VariableWidthStop::create( - fbb, - &fbs::VariableWidthStopArgs { u: s.u, r: s.r }, - ) + fbs::VariableWidthStop::create(fbb, &fbs::VariableWidthStopArgs { u: s.u, r: s.r }) }) .collect(); let stops_vec = fbb.create_vector(&stop_offsets); @@ -3103,12 +3409,18 @@ fn encode_container_node<'a, A: flatbuffers::Allocator + 'a>( let fill_offsets = encode_paints(fbb, &r.fills); let stroke_offsets = encode_paints(fbb, &r.strokes); - let cn = fbs::ContainerNode::create(fbb, &fbs::ContainerNodeArgs { - node: Some(sys), layer: Some(layer), - corner_radius: Some(cr_trait), stroke_geometry: Some(sg), - fill_paints: fill_offsets, stroke_paints: stroke_offsets, - clips_content: r.clip, - }); + let cn = fbs::ContainerNode::create( + fbb, + &fbs::ContainerNodeArgs { + node: Some(sys), + layer: Some(layer), + corner_radius: Some(cr_trait), + stroke_geometry: Some(sg), + fill_paints: fill_offsets, + stroke_paints: stroke_offsets, + clips_content: r.clip, + }, + ); make_node_slot(fbb, fbs::Node::ContainerNode, cn.as_union_value()) } @@ -3153,9 +3465,13 @@ fn encode_initial_container_node<'a, A: flatbuffers::Allocator + 'a>( }, ); - let icn = fbs::InitialContainerNode::create(fbb, &fbs::InitialContainerNodeArgs { - node: Some(sys), layer: Some(layer), - }); + let icn = fbs::InitialContainerNode::create( + fbb, + &fbs::InitialContainerNodeArgs { + node: Some(sys), + layer: Some(layer), + }, + ); make_node_slot(fbb, fbs::Node::InitialContainerNode, icn.as_union_value()) } @@ -3173,7 +3489,10 @@ fn encode_group_node<'a, A: flatbuffers::Allocator + 'a>( // Groups carry the full affine transform (scale, skew, rotation) — not // just rotation like shape nodes. Encode the full matrix so SVG-imported // transforms (e.g. scale(0.8, 1.2)) survive the roundtrip. - let plt = r.transform.as_ref().map(|t| encode_affine_to_cg_transform(t)); + let plt = r + .transform + .as_ref() + .map(|t| encode_affine_to_cg_transform(t)); let sys = encode_system_node_trait(fbb, node_id, "", r.active, false); let layout = encode_shape_layout(fbb, x, y, None, None, &None); @@ -3192,9 +3511,13 @@ fn encode_group_node<'a, A: flatbuffers::Allocator + 'a>( }, ); - let gn = fbs::GroupNode::create(fbb, &fbs::GroupNodeArgs { - node: Some(sys), layer: Some(layer), - }); + let gn = fbs::GroupNode::create( + fbb, + &fbs::GroupNodeArgs { + node: Some(sys), + layer: Some(layer), + }, + ); make_node_slot(fbb, fbs::Node::GroupNode, gn.as_union_value()) } @@ -3219,20 +3542,52 @@ fn encode_basic_shape_node<'a, A: flatbuffers::Allocator + 'a>( let (active, opacity, blend_mode, mask, effects, transform, size, fills, strokes, layout_child) = match &fields { BasicShapeFields::Rectangle(r) => ( - r.active, r.opacity, r.blend_mode, r.mask, &r.effects, &r.transform, &r.size, - &r.fills, &r.strokes, &r.layout_child, + r.active, + r.opacity, + r.blend_mode, + r.mask, + &r.effects, + &r.transform, + &r.size, + &r.fills, + &r.strokes, + &r.layout_child, ), BasicShapeFields::Ellipse(e) => ( - e.active, e.opacity, e.blend_mode, e.mask, &e.effects, &e.transform, &e.size, - &e.fills, &e.strokes, &e.layout_child, + e.active, + e.opacity, + e.blend_mode, + e.mask, + &e.effects, + &e.transform, + &e.size, + &e.fills, + &e.strokes, + &e.layout_child, ), BasicShapeFields::RegularPolygon(p) => ( - p.active, p.opacity, p.blend_mode, p.mask, &p.effects, &p.transform, &p.size, - &p.fills, &p.strokes, &p.layout_child, + p.active, + p.opacity, + p.blend_mode, + p.mask, + &p.effects, + &p.transform, + &p.size, + &p.fills, + &p.strokes, + &p.layout_child, ), BasicShapeFields::RegularStarPolygon(s) => ( - s.active, s.opacity, s.blend_mode, s.mask, &s.effects, &s.transform, &s.size, - &s.fills, &s.strokes, &s.layout_child, + s.active, + s.opacity, + s.blend_mode, + s.mask, + &s.effects, + &s.transform, + &s.size, + &s.fills, + &s.strokes, + &s.layout_child, ), }; @@ -3241,14 +3596,7 @@ fn encode_basic_shape_node<'a, A: flatbuffers::Allocator + 'a>( let plt = affine_to_rotation_transform(transform); let sys = encode_system_node_trait(fbb, node_id, "", active, false); - let layout = encode_shape_layout( - fbb, - x, - y, - Some(size.width), - Some(size.height), - layout_child, - ); + let layout = encode_shape_layout(fbb, x, y, Some(size.width), Some(size.height), layout_child); let layer = encode_layer_trait( fbb, &LayerTraitInput { @@ -3292,7 +3640,8 @@ fn encode_basic_shape_node<'a, A: flatbuffers::Allocator + 'a>( // Shape descriptor let shape_offset = match &fields { BasicShapeFields::Rectangle(_) => { - fbs::CanonicalShapeRectangular::create(fbb, &fbs::CanonicalShapeRectangularArgs {}).as_union_value() + fbs::CanonicalShapeRectangular::create(fbb, &fbs::CanonicalShapeRectangularArgs {}) + .as_union_value() } BasicShapeFields::Ellipse(e) => { let inner = e.inner_radius.unwrap_or(0.0); @@ -3315,17 +3664,21 @@ fn encode_basic_shape_node<'a, A: flatbuffers::Allocator + 'a>( ) .as_union_value() } - BasicShapeFields::RegularPolygon(p) => { - fbs::CanonicalShapeRegularPolygon::create(fbb, &fbs::CanonicalShapeRegularPolygonArgs { + BasicShapeFields::RegularPolygon(p) => fbs::CanonicalShapeRegularPolygon::create( + fbb, + &fbs::CanonicalShapeRegularPolygonArgs { point_count: p.point_count as u32, - }).as_union_value() - } - BasicShapeFields::RegularStarPolygon(s) => { - fbs::CanonicalShapeRegularStarPolygon::create(fbb, &fbs::CanonicalShapeRegularStarPolygonArgs { + }, + ) + .as_union_value(), + BasicShapeFields::RegularStarPolygon(s) => fbs::CanonicalShapeRegularStarPolygon::create( + fbb, + &fbs::CanonicalShapeRegularStarPolygonArgs { point_count: s.point_count as u32, inner_radius_ratio: s.inner_radius, - }).as_union_value() - } + }, + ) + .as_union_value(), }; // Corner smoothing (only meaningful for rectangles) @@ -3340,17 +3693,25 @@ fn encode_basic_shape_node<'a, A: flatbuffers::Allocator + 'a>( _ => None, }; - let bsn = fbs::BasicShapeNode::create(fbb, &fbs::BasicShapeNodeArgs { - node: Some(sys), layer: Some(layer), - type_: node_type, shape_type, shape: Some(shape_offset), - corner_radius: scalar_cr, fill_paints: fill_offsets, - stroke_style: Some(stroke_style_offset), stroke_width: stroke_width_f32, - rectangular_corner_radius: rect_cr.as_ref(), - stroke_paints: stroke_offsets, - corner_smoothing, - rectangular_stroke_width: rect_sw.as_ref(), - ..Default::default() - }); + let bsn = fbs::BasicShapeNode::create( + fbb, + &fbs::BasicShapeNodeArgs { + node: Some(sys), + layer: Some(layer), + type_: node_type, + shape_type, + shape: Some(shape_offset), + corner_radius: scalar_cr, + fill_paints: fill_offsets, + stroke_style: Some(stroke_style_offset), + stroke_width: stroke_width_f32, + rectangular_corner_radius: rect_cr.as_ref(), + stroke_paints: stroke_offsets, + corner_smoothing, + rectangular_stroke_width: rect_sw.as_ref(), + ..Default::default() + }, + ); make_node_slot(fbb, fbs::Node::BasicShapeNode, bsn.as_union_value()) } @@ -3383,22 +3744,32 @@ fn encode_line_node<'a, A: flatbuffers::Allocator + 'a>( }, ); - let sg = encode_stroke_geometry(fbb, &StrokeStyle { - stroke_align: r._data_stroke_align, - stroke_cap: r.stroke_cap, - stroke_join: StrokeJoin::Miter, - stroke_miter_limit: r.stroke_miter_limit, - stroke_dash_array: r.stroke_dash_array.clone(), - }, r.stroke_width, None); + let sg = encode_stroke_geometry( + fbb, + &StrokeStyle { + stroke_align: r._data_stroke_align, + stroke_cap: r.stroke_cap, + stroke_join: StrokeJoin::Miter, + stroke_miter_limit: r.stroke_miter_limit, + stroke_dash_array: r.stroke_dash_array.clone(), + }, + r.stroke_width, + None, + ); let stroke_offsets = encode_paints(fbb, &r.strokes); - let ln = fbs::LineNode::create(fbb, &fbs::LineNodeArgs { - node: Some(sys), layer: Some(layer), stroke_geometry: Some(sg), - stroke_paints: stroke_offsets, - marker_start_shape: encode_stroke_marker(r.marker_start_shape), - marker_end_shape: encode_stroke_marker(r.marker_end_shape), - }); + let ln = fbs::LineNode::create( + fbb, + &fbs::LineNodeArgs { + node: Some(sys), + layer: Some(layer), + stroke_geometry: Some(sg), + stroke_paints: stroke_offsets, + marker_start_shape: encode_stroke_marker(r.marker_start_shape), + marker_end_shape: encode_stroke_marker(r.marker_end_shape), + }, + ); make_node_slot(fbb, fbs::Node::LineNode, ln.as_union_value()) } @@ -3432,25 +3803,36 @@ fn encode_vector_node<'a, A: flatbuffers::Allocator + 'a>( // Vector network let vn = encode_vector_network(fbb, &r.network); - let sg = encode_stroke_geometry(fbb, &StrokeStyle { - stroke_align: r.stroke_align, - stroke_cap: r.stroke_cap, - stroke_join: r.stroke_join, - stroke_miter_limit: r.stroke_miter_limit, - stroke_dash_array: r.stroke_dash_array.clone(), - }, r.stroke_width, r.stroke_width_profile.as_ref()); + let sg = encode_stroke_geometry( + fbb, + &StrokeStyle { + stroke_align: r.stroke_align, + stroke_cap: r.stroke_cap, + stroke_join: r.stroke_join, + stroke_miter_limit: r.stroke_miter_limit, + stroke_dash_array: r.stroke_dash_array.clone(), + }, + r.stroke_width, + r.stroke_width_profile.as_ref(), + ); let fill_offsets = encode_paints(fbb, &r.fills); let stroke_offsets = encode_paints(fbb, &r.strokes); - let vn_node = fbs::VectorNode::create(fbb, &fbs::VectorNodeArgs { - node: Some(sys), layer: Some(layer), stroke_geometry: Some(sg), - stroke_paints: stroke_offsets, fill_paints: fill_offsets, - vector_network_data: vn, - marker_start_shape: encode_stroke_marker(r.marker_start_shape), - marker_end_shape: encode_stroke_marker(r.marker_end_shape), - ..Default::default() - }); + let vn_node = fbs::VectorNode::create( + fbb, + &fbs::VectorNodeArgs { + node: Some(sys), + layer: Some(layer), + stroke_geometry: Some(sg), + stroke_paints: stroke_offsets, + fill_paints: fill_offsets, + vector_network_data: vn, + marker_start_shape: encode_stroke_marker(r.marker_start_shape), + marker_end_shape: encode_stroke_marker(r.marker_end_shape), + ..Default::default() + }, + ); make_node_slot(fbb, fbs::Node::VectorNode, vn_node.as_union_value()) } @@ -3467,31 +3849,37 @@ fn encode_path_node<'a, A: flatbuffers::Allocator + 'a>( let sys = encode_system_node_trait(fbb, node_id, "", r.active, false); let layout = encode_shape_layout(fbb, x, y, None, None, &r.layout_child); - let layer = encode_layer_trait(fbb, &LayerTraitInput { - parent_id, - position, - opacity: r.opacity, - blend_mode: r.blend_mode, - mask: r.mask, - effects: &r.effects, - post_layout_transform: plt, - layout: Some(layout), - }); + let layer = encode_layer_trait( + fbb, + &LayerTraitInput { + parent_id, + position, + opacity: r.opacity, + blend_mode: r.blend_mode, + mask: r.mask, + effects: &r.effects, + post_layout_transform: plt, + layout: Some(layout), + }, + ); let sg = encode_stroke_geometry(fbb, &r.stroke_style, r.stroke_width.value_or_zero(), None); let fill_offsets = encode_paints(fbb, &r.fills); let stroke_offsets = encode_paints(fbb, &r.strokes); let data_offset = fbb.create_string(&r.data); - let pn = fbs::PathNode::create(fbb, &fbs::PathNodeArgs { - node: Some(sys), - layer: Some(layer), - stroke_geometry: Some(sg), - fill_paints: fill_offsets, - stroke_paints: stroke_offsets, - data: Some(data_offset), - fill_rule: fbs::FillRule::NonZero, - }); + let pn = fbs::PathNode::create( + fbb, + &fbs::PathNodeArgs { + node: Some(sys), + layer: Some(layer), + stroke_geometry: Some(sg), + fill_paints: fill_offsets, + stroke_paints: stroke_offsets, + data: Some(data_offset), + fill_rule: fbs::FillRule::NonZero, + }, + ); make_node_slot(fbb, fbs::Node::PathNode, pn.as_union_value()) } @@ -3527,92 +3915,136 @@ fn encode_text_span_node<'a, A: flatbuffers::Allocator + 'a>( let font_family_str = fbb.create_string(&ts.font_family); let font_weight = fbs::FontWeight::new(ts.font_weight.0); let fbs_font_optical_sizing = match ts.font_optical_sizing { - FontOpticalSizing::Auto => fbs::FontOpticalSizing::new(fbs::FontOpticalSizingKind::Auto, 0.0), - FontOpticalSizing::None => fbs::FontOpticalSizing::new(fbs::FontOpticalSizingKind::None, 0.0), - FontOpticalSizing::Fixed(v) => fbs::FontOpticalSizing::new(fbs::FontOpticalSizingKind::Fixed, v), + FontOpticalSizing::Auto => { + fbs::FontOpticalSizing::new(fbs::FontOpticalSizingKind::Auto, 0.0) + } + FontOpticalSizing::None => { + fbs::FontOpticalSizing::new(fbs::FontOpticalSizingKind::None, 0.0) + } + FontOpticalSizing::Fixed(v) => { + fbs::FontOpticalSizing::new(fbs::FontOpticalSizingKind::Fixed, v) + } }; let text_decoration_offset = ts.text_decoration.as_ref().map(|td| { - let color = td.text_decoration_color.as_ref().map(|c| encode_color_to_rgba32f(c)); - fbs::TextDecorationRec::create(fbb, &fbs::TextDecorationRecArgs { - text_decoration_line: encode_text_decoration_line(td.text_decoration_line), - text_decoration_style: encode_text_decoration_style(td.text_decoration_style.unwrap_or_default()), - text_decoration_skip_ink: td.text_decoration_skip_ink.unwrap_or(true), - text_decoration_thickness: td.text_decoration_thickness.unwrap_or(0.0), - text_decoration_color: color.as_ref(), - }) + let color = td + .text_decoration_color + .as_ref() + .map(|c| encode_color_to_rgba32f(c)); + fbs::TextDecorationRec::create( + fbb, + &fbs::TextDecorationRecArgs { + text_decoration_line: encode_text_decoration_line(td.text_decoration_line), + text_decoration_style: encode_text_decoration_style( + td.text_decoration_style.unwrap_or_default(), + ), + text_decoration_skip_ink: td.text_decoration_skip_ink.unwrap_or(true), + text_decoration_thickness: td.text_decoration_thickness.unwrap_or(0.0), + text_decoration_color: color.as_ref(), + }, + ) }); let letter_spacing_offset = encode_text_dimension_from_letter_spacing(fbb, &ts.letter_spacing); let word_spacing_offset = encode_text_dimension_from_word_spacing(fbb, &ts.word_spacing); let line_height_offset = encode_text_dimension_from_line_height(fbb, &ts.line_height); let font_features_offset = ts.font_features.as_ref().map(|features| { - let items: Vec<_> = features.iter().map(|f| { - let bytes = f.tag.as_bytes(); - let tag = fbs::OpenTypeFeatureTag::new( - *bytes.first().unwrap_or(&0), - *bytes.get(1).unwrap_or(&0), - *bytes.get(2).unwrap_or(&0), - *bytes.get(3).unwrap_or(&0), - ); - fbs::FontFeature::create(fbb, &fbs::FontFeatureArgs { - open_type_feature_tag: Some(&tag), - open_type_feature_value: f.value, + let items: Vec<_> = features + .iter() + .map(|f| { + let bytes = f.tag.as_bytes(); + let tag = fbs::OpenTypeFeatureTag::new( + *bytes.first().unwrap_or(&0), + *bytes.get(1).unwrap_or(&0), + *bytes.get(2).unwrap_or(&0), + *bytes.get(3).unwrap_or(&0), + ); + fbs::FontFeature::create( + fbb, + &fbs::FontFeatureArgs { + open_type_feature_tag: Some(&tag), + open_type_feature_value: f.value, + }, + ) }) - }).collect(); + .collect(); fbb.create_vector(&items) }); let font_variations_offset = ts.font_variations.as_ref().map(|variations| { - let items: Vec<_> = variations.iter().map(|v| { - let axis_str = fbb.create_string(&v.axis); - fbs::FontVariation::create(fbb, &fbs::FontVariationArgs { - variation_axis: Some(axis_str), - variation_value: v.value, + let items: Vec<_> = variations + .iter() + .map(|v| { + let axis_str = fbb.create_string(&v.axis); + fbs::FontVariation::create( + fbb, + &fbs::FontVariationArgs { + variation_axis: Some(axis_str), + variation_value: v.value, + }, + ) }) - }).collect(); + .collect(); fbb.create_vector(&items) }); - let text_style = fbs::TextStyleRec::create(fbb, &fbs::TextStyleRecArgs { - font_family: Some(font_family_str), - font_size: ts.font_size, - font_weight: Some(&font_weight), - font_style_italic: ts.font_style_italic, - font_kerning: ts.font_kerning, - font_width: ts.font_width.unwrap_or(0.0), - font_optical_sizing: Some(&fbs_font_optical_sizing), - text_transform: encode_text_transform(ts.text_transform), - text_decoration: text_decoration_offset, - letter_spacing: letter_spacing_offset, - word_spacing: word_spacing_offset, - line_height: line_height_offset, - font_features: font_features_offset, - font_variations: font_variations_offset, - }); + let text_style = fbs::TextStyleRec::create( + fbb, + &fbs::TextStyleRecArgs { + font_family: Some(font_family_str), + font_size: ts.font_size, + font_weight: Some(&font_weight), + font_style_italic: ts.font_style_italic, + font_kerning: ts.font_kerning, + font_width: ts.font_width.unwrap_or(0.0), + font_optical_sizing: Some(&fbs_font_optical_sizing), + text_transform: encode_text_transform(ts.text_transform), + text_decoration: text_decoration_offset, + letter_spacing: letter_spacing_offset, + word_spacing: word_spacing_offset, + line_height: line_height_offset, + font_features: font_features_offset, + font_variations: font_variations_offset, + }, + ); let text_str = fbb.create_string(&r.text); let fill_offsets = encode_paints(fbb, &r.fills); let stroke_offsets = encode_paints(fbb, &r.strokes); - let sg = encode_stroke_geometry(fbb, &StrokeStyle { - stroke_align: r.stroke_align, - stroke_cap: StrokeCap::Butt, - stroke_join: StrokeJoin::Miter, - stroke_miter_limit: StrokeMiterLimit::default(), - stroke_dash_array: None, - }, r.stroke_width, None); + let sg = encode_stroke_geometry( + fbb, + &StrokeStyle { + stroke_align: r.stroke_align, + stroke_cap: StrokeCap::Butt, + stroke_join: StrokeJoin::Miter, + stroke_miter_limit: StrokeMiterLimit::default(), + stroke_dash_array: None, + }, + r.stroke_width, + None, + ); let ellipsis_offset = r.ellipsis.as_ref().map(|s| fbb.create_string(s)); - let props = fbs::TextSpanNodeProperties::create(fbb, &fbs::TextSpanNodePropertiesArgs { - text: Some(text_str), text_style: Some(text_style), - text_align: encode_text_align(r.text_align), - text_align_vertical: encode_text_align_vertical(r.text_align_vertical), - fill_paints: fill_offsets, stroke_paints: stroke_offsets, - stroke_geometry: Some(sg), - max_lines: r.max_lines.map(|v| v as u32).unwrap_or(0), - ellipsis: ellipsis_offset, - }); + let props = fbs::TextSpanNodeProperties::create( + fbb, + &fbs::TextSpanNodePropertiesArgs { + text: Some(text_str), + text_style: Some(text_style), + text_align: encode_text_align(r.text_align), + text_align_vertical: encode_text_align_vertical(r.text_align_vertical), + fill_paints: fill_offsets, + stroke_paints: stroke_offsets, + stroke_geometry: Some(sg), + max_lines: r.max_lines.map(|v| v as u32).unwrap_or(0), + ellipsis: ellipsis_offset, + }, + ); - let tn = fbs::TextSpanNode::create(fbb, &fbs::TextSpanNodeArgs { - node: Some(sys), layer: Some(layer), properties: Some(props), - }); + let tn = fbs::TextSpanNode::create( + fbb, + &fbs::TextSpanNodeArgs { + node: Some(sys), + layer: Some(layer), + properties: Some(props), + }, + ); make_node_slot(fbb, fbs::Node::TextSpanNode, tn.as_union_value()) } @@ -3745,10 +4177,7 @@ fn encode_vector_network_region<'a, A: flatbuffers::Allocator + 'a>( crate::cg::types::FillRule::NonZero => fbs::FillRule::NonZero, }; - let fills_offset = region - .fills - .as_ref() - .and_then(|p| encode_paints(fbb, p)); + let fills_offset = region.fills.as_ref().and_then(|p| encode_paints(fbb, p)); fbs::VectorNetworkRegion::create( fbb, @@ -3764,9 +4193,7 @@ fn encode_vector_network_region<'a, A: flatbuffers::Allocator + 'a>( // Geometry helpers for encoding // ───────────────────────────────────────────────────────────────────────────── -fn encode_rectangular_corner_radius( - cr: &RectangularCornerRadius, -) -> fbs::RectangularCornerRadius { +fn encode_rectangular_corner_radius(cr: &RectangularCornerRadius) -> fbs::RectangularCornerRadius { let tl = fbs::CGRadius::new(cr.tl.rx, cr.tl.ry); let tr = fbs::CGRadius::new(cr.tr.rx, cr.tr.ry); let bl = fbs::CGRadius::new(cr.bl.rx, cr.bl.ry); diff --git a/crates/grida-canvas/src/layout/engine.rs b/crates/grida-canvas/src/layout/engine.rs index fea7e2f129..b2ccade15d 100644 --- a/crates/grida-canvas/src/layout/engine.rs +++ b/crates/grida-canvas/src/layout/engine.rs @@ -62,7 +62,7 @@ use crate::layout::cache::LayoutResult; use crate::layout::tree::{LayoutTree, TextMeasureContext, TextMeasureProvider}; use crate::layout::ComputedLayout; use crate::node::scene_graph::SceneGraph; -use crate::node::schema::{Node, NodeId, NodeRectMixin, Size}; +use crate::node::schema::{Node, NodeId, NodeRectMixin, NodeTypeTag, Size}; use taffy::prelude::*; /// Layout engine for the scene graph. @@ -174,21 +174,24 @@ impl LayoutEngine { } /// Recursively extract schema positions/sizes for a node and its children. + /// + /// Uses `NodeGeoData` (~48 bytes) instead of full `Node` (~500+ bytes) to + /// read schema positions and sizes — avoids 136K full-Node reads. fn extract_schema_only_recursive( &mut self, id: &NodeId, graph: &crate::node::scene_graph::SceneGraph, ) { - if let Ok(node) = graph.get_node(id) { - let (x, y) = Self::get_schema_position(node); - let (width, height) = Self::get_schema_size(node); + if let Some(geo) = graph.geo_data().get(id) { + let x = geo.schema_transform.x(); + let y = geo.schema_transform.y(); self.result.insert( *id, ComputedLayout { x, y, - width, - height, + width: geo.schema_width, + height: geo.schema_height, }, ); } @@ -280,33 +283,15 @@ impl LayoutEngine { } } - /// Check if a node type participates in Taffy layout. + /// Check if a node type participates in Taffy layout (using NodeTypeTag). /// /// Virtual grouping nodes (Group, BooleanOperation) are excluded — they /// have no intrinsic size, don't constrain children, and their bounds are /// derived from children. Their children form independent Taffy subtrees. - fn is_layout_node(node: &Node) -> bool { - !matches!(node, Node::Group(_) | Node::BooleanOperation(_)) + fn is_layout_node_tag(tag: NodeTypeTag) -> bool { + !matches!(tag, NodeTypeTag::Group | NodeTypeTag::BooleanOperation) } - /// Check if a Container uses flex layout for its children. - /// - /// `LayoutMode::Normal` containers don't flow their children — all children - /// are positioned via schema coordinates. Their children should be treated - /// as independent subtrees (like Group children) rather than Taffy children. - /// - /// This is the critical optimization for Figma imports where 90%+ of - /// containers use `LayoutMode::Normal`. Skipping their children avoids - /// building O(n) Taffy nodes and running the flex algorithm unnecessarily. - fn is_flex_container(node: &Node) -> bool { - match node { - Node::Container(n) => { - n.layout_container.layout_mode == crate::cg::types::LayoutMode::Flex - } - Node::InitialContainer(_) => true, - _ => false, - } - } /// Recursively build Taffy tree for a node and its descendants. /// @@ -321,11 +306,13 @@ impl LayoutEngine { viewport_size: Size, extra_roots: &mut Vec, ) -> Option { - let node = graph.get_node(node_id).ok()?; + // Fast-path: use compact layer_core (~16 bytes) for is_layout_node + // and is_flex_container checks before touching the full Node (~500+ bytes). + let lc = graph.get_layer_core(node_id)?; // Virtual grouping nodes don't participate in Taffy — skip them but // recurse into their children to discover Taffy-capable subtrees. - if !Self::is_layout_node(node) { + if !Self::is_layout_node_tag(lc.node_type) { if let Some(children) = graph.get_children(node_id) { for child_id in children { if let Some(taffy_id) = @@ -338,6 +325,9 @@ impl LayoutEngine { return None; } + // Only access full Node for nodes that participate in Taffy layout. + let node = graph.get_node(node_id).ok()?; + // Get style for this node (universal mapping) let mut style = crate::layout::into_taffy::node_to_taffy_style(node, graph, node_id); @@ -345,7 +335,7 @@ impl LayoutEngine { // extract_all_layouts() post-processes to apply schema positions // Special handling for root ICB nodes - use viewport size - if let Node::InitialContainer(_) = node { + if lc.node_type == NodeTypeTag::InitialContainer { style.size = taffy::Size { width: Dimension::length(viewport_size.width), height: Dimension::length(viewport_size.height), @@ -366,7 +356,7 @@ impl LayoutEngine { // This is the critical optimization for Figma imports: most // containers use Normal mode, so we skip building Taffy nodes // for their entire subtrees (~90%+ of nodes). - if !Self::is_flex_container(node) { + if !lc.is_flex { return self.tree.new_leaf(*node_id, style).ok(); } @@ -435,64 +425,81 @@ impl LayoutEngine { // Taffy subtree roots, also computed at (0,0) // 3. Children of non-flex containers (LayoutMode::Normal) — these // are also extra_roots with schema positions + // + // Uses layer_core (~16 bytes) instead of full Node (~500+ bytes) + // for parent type checks. let needs_schema_position = graph.is_root(id) || graph .get_parent(id) - .and_then(|pid| graph.get_node(&pid).ok()) - .is_some_and(|parent| { - !Self::is_layout_node(parent) || !Self::is_flex_container(parent) + .and_then(|pid| graph.get_layer_core(&pid)) + .is_some_and(|parent_lc| { + !Self::is_layout_node_tag(parent_lc.node_type) || !parent_lc.is_flex }); if needs_schema_position { - if let Ok(node) = graph.get_node(id) { - let (schema_x, schema_y) = Self::get_schema_position(node); - computed.x = schema_x; - computed.y = schema_y; + // Use geo_data (~48 bytes) for schema position instead of full Node. + if let Some(geo) = graph.geo_data().get(id) { + computed.x = geo.schema_transform.x(); + computed.y = geo.schema_transform.y(); } } self.result.insert(*id, computed); } else { - // Node not in Taffy tree — use schema positions/sizes. - // For text nodes with missing dimensions, measure on-the-fly. - if let Ok(node) = graph.get_node(id) { - let (x, y) = Self::get_schema_position(node); - let (mut width, mut height) = Self::get_schema_size(node); - - // Text nodes under non-flex containers need measurement for - // missing width/height. The schema typically stores width but - // not height for auto-height text. - if let Node::TextSpan(n) = node { - if n.width.is_none() || n.height.is_none() { - if let Some(ref mut provider) = text_measure { - let width_constraint = n.width; - let measurements = provider.paragraph_cache.measure( - &n.text, - &n.text_style, - &n.text_align, - &n.max_lines, - &n.ellipsis, - width_constraint, - provider.fonts, - Some(id), - ); - if n.width.is_none() { - width = measurements.max_width; - } - if n.height.is_none() { - height = measurements.height; + // Node not in Taffy tree — use schema positions/sizes from geo_data. + // For text nodes with missing dimensions, access full Node for measurement. + let lc = graph.get_layer_core(id); + let is_text = lc.map(|c| c.node_type == NodeTypeTag::TextSpan).unwrap_or(false); + + if is_text { + // TextSpan: may need on-the-fly measurement — access full Node. + if let Ok(node) = graph.get_node(id) { + let (x, y) = Self::get_schema_position(node); + let (mut width, mut height) = Self::get_schema_size(node); + + if let Node::TextSpan(n) = node { + if n.width.is_none() || n.height.is_none() { + if let Some(ref mut provider) = text_measure { + let width_constraint = n.width; + let measurements = provider.paragraph_cache.measure( + &n.text, + &n.text_style, + &n.text_align, + &n.max_lines, + &n.ellipsis, + width_constraint, + provider.fonts, + Some(id), + ); + if n.width.is_none() { + width = measurements.max_width; + } + if n.height.is_none() { + height = measurements.height; + } } } } - } + self.result.insert( + *id, + ComputedLayout { + x, + y, + width, + height, + }, + ); + } + } else if let Some(geo) = graph.geo_data().get(id) { + // Non-text: use geo_data (~48 bytes) instead of full Node (~500+ bytes). self.result.insert( *id, ComputedLayout { - x, - y, - width, - height, + x: geo.schema_transform.x(), + y: geo.schema_transform.y(), + width: geo.schema_width, + height: geo.schema_height, }, ); } diff --git a/crates/grida-canvas/src/node/scene_graph.rs b/crates/grida-canvas/src/node/scene_graph.rs index fabd0b7bb8..79750e180b 100644 --- a/crates/grida-canvas/src/node/scene_graph.rs +++ b/crates/grida-canvas/src/node/scene_graph.rs @@ -1,6 +1,8 @@ use super::repository::NodeRepository; -use super::schema::{Node, NodeId}; +use super::schema::{extract_layer_core, Node, NodeGeometryMixin, NodeId, NodeLayerCore, NodeRectMixin}; use crate::cache::fast_hash::DenseNodeMap; +use crate::cg::prelude::*; +use math2::transform::AffineTransform; use std::collections::HashMap; /// Parent reference in the scene graph @@ -46,12 +48,386 @@ impl std::error::Error for SceneGraphError {} pub type SceneGraphResult = Result; +// --------------------------------------------------------------------------- +// NodeGeoData — compact, schema-level geometry data per node +// --------------------------------------------------------------------------- + +/// Classifies how a node participates in geometry computation. +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(u8)] +pub enum GeoNodeKind { + Group, + InitialContainer, + Container, + BooleanOperation, + TextSpan, + Leaf, +} + +/// Pre-computed render bounds inflation. +/// +/// Stores per-side pixel expansion from stroke + effects, computed at +/// construction time. `Copy`, no heap allocation, 16 bytes total. +/// The geometry cache inflates world bounds by these values at DFS time. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct RenderBoundsInflation { + pub top: f32, + pub right: f32, + pub bottom: f32, + pub left: f32, +} + +impl RenderBoundsInflation { + pub const ZERO: Self = Self { + top: 0.0, + right: 0.0, + bottom: 0.0, + left: 0.0, + }; + + /// Uniform inflation on all sides. + pub fn uniform(delta: f32) -> Self { + Self { + top: delta, + right: delta, + bottom: delta, + left: delta, + } + } + + /// Expand this inflation to include another inflation (per-side max). + pub fn expand(&self, other: &Self) -> Self { + Self { + top: self.top.max(other.top), + right: self.right.max(other.right), + bottom: self.bottom.max(other.bottom), + left: self.left.max(other.left), + } + } + + /// Whether this inflation is all zeros. + pub fn is_zero(&self) -> bool { + self.top == 0.0 && self.right == 0.0 && self.bottom == 0.0 && self.left == 0.0 + } +} + +/// Compact, schema-level geometry data extracted from a `Node` at construction +/// time. Stored in a parallel `DenseNodeMap` on the `SceneGraph` so that the +/// geometry cache never needs to iterate over the full `Node` enum. +/// +/// Layout-dependent fields (final width/height/position) are resolved by the +/// geometry cache using this data + `LayoutResult`. +/// +/// This struct is `Copy` — no heap allocations, ~48 bytes total. +#[derive(Debug, Clone, Copy)] +pub struct NodeGeoData { + /// The node's transform as stored in the schema. + /// + /// For Container nodes, this is `AffineTransform::new(fallback_x, fallback_y, rotation)`. + /// The geometry cache may override x/y from layout results. + pub schema_transform: AffineTransform, + /// Schema width (from size, rect(), or network.bounds()). + pub schema_width: f32, + /// Schema height. + pub schema_height: f32, + /// What kind of node (determines DFS behavior). + pub kind: GeoNodeKind, + /// Pre-computed per-side render bounds inflation from stroke + effects. + pub render_bounds_inflation: RenderBoundsInflation, + /// Container rotation (needed to reconstruct transform from layout x/y). + /// Only meaningful for Container nodes; 0.0 for others. + pub rotation: f32, +} + +/// Extract schema-level geometry data from a `Node`. +/// +/// This is a pure function of the Node — no layout results, no font metrics, +/// no paragraph cache. Called once per node during SceneGraph construction. +/// All values are `Copy` — zero heap allocations. +pub fn extract_geo_data(node: &Node) -> NodeGeoData { + match node { + Node::Group(n) => NodeGeoData { + schema_transform: n.transform.unwrap_or_default(), + schema_width: 0.0, + schema_height: 0.0, + kind: GeoNodeKind::Group, + render_bounds_inflation: RenderBoundsInflation::ZERO, // union of children + rotation: 0.0, + }, + Node::InitialContainer(_) => NodeGeoData { + schema_transform: AffineTransform::identity(), + schema_width: 0.0, + schema_height: 0.0, + kind: GeoNodeKind::InitialContainer, + render_bounds_inflation: RenderBoundsInflation::ZERO, + rotation: 0.0, + }, + Node::BooleanOperation(n) => NodeGeoData { + schema_transform: n.transform.unwrap_or_default(), + schema_width: 0.0, + schema_height: 0.0, + kind: GeoNodeKind::BooleanOperation, + render_bounds_inflation: compute_inflation_uniform( + n.stroke_width.value_or_zero(), + n.stroke_style.stroke_align, + &n.effects, + ), + rotation: 0.0, + }, + Node::Container(n) => { + let fallback_x = n.position.x().unwrap_or(0.0); + let fallback_y = n.position.y().unwrap_or(0.0); + let schema_transform = AffineTransform::new(fallback_x, fallback_y, n.rotation); + + let render_bounds_inflation = if let Some(rect_stroke) = n.rectangular_stroke_width() { + compute_inflation_rectangular(&rect_stroke, n.stroke_style.stroke_align, &n.effects) + } else { + compute_inflation_uniform( + n.render_bounds_stroke_width(), + n.stroke_style.stroke_align, + &n.effects, + ) + }; + + NodeGeoData { + schema_transform, + schema_width: n.layout_dimensions.layout_target_width.unwrap_or(0.0), + schema_height: n.layout_dimensions.layout_target_height.unwrap_or(0.0), + kind: GeoNodeKind::Container, + render_bounds_inflation, + rotation: n.rotation, + } + } + Node::TextSpan(n) => NodeGeoData { + schema_transform: n.transform, + schema_width: n.width.unwrap_or(0.0), + schema_height: n.height.unwrap_or(0.0), + kind: GeoNodeKind::TextSpan, + render_bounds_inflation: compute_inflation_uniform( + n.stroke_width, + n.stroke_align, + &super::schema::LayerEffects::default(), + ), + rotation: 0.0, + }, + _ => { + // Leaf nodes: Rectangle, Ellipse, Image, RegularPolygon, + // RegularStarPolygon, Line, Polygon, Path, Vector, Error. + let (schema_transform, schema_width, schema_height) = match node { + Node::Rectangle(n) => (n.transform, n.size.width, n.size.height), + Node::Ellipse(n) => (n.transform, n.size.width, n.size.height), + Node::Image(n) => (n.transform, n.size.width, n.size.height), + Node::RegularPolygon(n) => (n.transform, n.size.width, n.size.height), + Node::RegularStarPolygon(n) => (n.transform, n.size.width, n.size.height), + Node::Line(n) => (n.transform, n.size.width, 0.0), + Node::Polygon(n) => { + let rect = n.rect(); + (n.transform, rect.width, rect.height) + } + Node::Path(n) => { + let rect = n.rect(); + (n.transform, rect.width, rect.height) + } + Node::Vector(n) => { + let rect = n.network.bounds(); + (n.transform, rect.width, rect.height) + } + Node::Error(n) => (n.transform, n.size.width, n.size.height), + _ => unreachable!("Non-leaf variants handled above"), + }; + + let render_bounds_inflation = extract_leaf_inflation(node); + + NodeGeoData { + schema_transform, + schema_width, + schema_height, + kind: GeoNodeKind::Leaf, + render_bounds_inflation, + rotation: 0.0, + } + } + } +} + +// --------------------------------------------------------------------------- +// Render bounds inflation computation — pure scalars, no heap allocations +// --------------------------------------------------------------------------- + +/// Stroke outset for a given alignment. +fn stroke_outset(align: StrokeAlign, width: f32) -> f32 { + match align { + StrokeAlign::Inside => 0.0, + StrokeAlign::Center => width / 2.0, + StrokeAlign::Outside => width, + } +} + +/// Compute per-side inflation from a uniform stroke + effects. +fn compute_inflation_uniform( + stroke_width: f32, + stroke_align: StrokeAlign, + effects: &super::schema::LayerEffects, +) -> RenderBoundsInflation { + let stroke_delta = stroke_outset(stroke_align, stroke_width); + let base = RenderBoundsInflation::uniform(stroke_delta); + expand_inflation_with_effects(base, effects) +} + +/// Compute per-side inflation from a rectangular (per-side) stroke + effects. +fn compute_inflation_rectangular( + rect_stroke: &RectangularStrokeWidth, + stroke_align: StrokeAlign, + effects: &super::schema::LayerEffects, +) -> RenderBoundsInflation { + let base = match stroke_align { + StrokeAlign::Center => RenderBoundsInflation { + top: rect_stroke.stroke_top_width / 2.0, + right: rect_stroke.stroke_right_width / 2.0, + bottom: rect_stroke.stroke_bottom_width / 2.0, + left: rect_stroke.stroke_left_width / 2.0, + }, + StrokeAlign::Inside => RenderBoundsInflation::ZERO, + StrokeAlign::Outside => RenderBoundsInflation { + top: rect_stroke.stroke_top_width, + right: rect_stroke.stroke_right_width, + bottom: rect_stroke.stroke_bottom_width, + left: rect_stroke.stroke_left_width, + }, + }; + expand_inflation_with_effects(base, effects) +} + +/// Expand a base inflation with the effect-induced expansion. +/// +/// Effects are additive: blur expands uniformly, drop shadows expand +/// asymmetrically (offset + blur + spread). +fn expand_inflation_with_effects( + base: RenderBoundsInflation, + effects: &super::schema::LayerEffects, +) -> RenderBoundsInflation { + let mut result = base; + + if let Some(blur_effect) = &effects.blur { + let radius = match &blur_effect.blur { + crate::cg::prelude::FeBlur::Gaussian(g) => g.radius * 3.0, + crate::cg::prelude::FeBlur::Progressive(p) => p.radius.max(p.radius2) * 3.0, + }; + result = result.expand(&RenderBoundsInflation::uniform(radius)); + } + + for shadow in &effects.shadows { + let effect: crate::cg::prelude::FilterEffect = shadow.clone().into(); + let shadow_inflation = compute_effect_inflation(&effect); + result = result.expand(&shadow_inflation); + } + + result +} + +/// Compute per-side inflation from a single filter effect. +fn compute_effect_inflation(effect: &crate::cg::prelude::FilterEffect) -> RenderBoundsInflation { + use crate::cg::prelude::FilterEffect; + match effect { + FilterEffect::LiquidGlass(glass) => RenderBoundsInflation::uniform(glass.blur_radius * 3.0), + FilterEffect::LayerBlur(blur) => { + let r = match &blur.blur { + crate::cg::prelude::FeBlur::Gaussian(g) => g.radius * 3.0, + crate::cg::prelude::FeBlur::Progressive(p) => p.radius.max(p.radius2) * 3.0, + }; + RenderBoundsInflation::uniform(r) + } + FilterEffect::BackdropBlur(blur) => { + let r = match &blur.blur { + crate::cg::prelude::FeBlur::Gaussian(g) => g.radius * 3.0, + crate::cg::prelude::FeBlur::Progressive(p) => p.radius.max(p.radius2) * 3.0, + }; + RenderBoundsInflation::uniform(r) + } + FilterEffect::DropShadow(shadow) => { + // Shadow creates a shifted, blurred copy of the shape. + // The per-side inflation from the original bounds is: + // side = max(0, blur*3 + spread ± offset) + // where the sign of the offset depends on direction. + let blur_r = shadow.blur * 3.0; + let spread = shadow.spread.max(0.0); + let base = blur_r + spread; + RenderBoundsInflation { + top: (base - shadow.dy).max(0.0), + right: (base + shadow.dx).max(0.0), + bottom: (base + shadow.dy).max(0.0), + left: (base - shadow.dx).max(0.0), + } + } + FilterEffect::Noise(_) | FilterEffect::InnerShadow(_) => RenderBoundsInflation::ZERO, + } +} + +/// Extract render bounds inflation for leaf nodes. +fn extract_leaf_inflation(node: &Node) -> RenderBoundsInflation { + match node { + Node::Rectangle(n) => { + if let Some(rect_stroke) = n.rectangular_stroke_width() { + compute_inflation_rectangular(&rect_stroke, n.stroke_style.stroke_align, &n.effects) + } else { + compute_inflation_uniform( + n.render_bounds_stroke_width(), + n.stroke_style.stroke_align, + &n.effects, + ) + } + } + Node::Ellipse(n) => compute_inflation_uniform( + n.render_bounds_stroke_width(), + n.stroke_style.stroke_align, + &n.effects, + ), + Node::Polygon(n) => compute_inflation_uniform( + n.render_bounds_stroke_width(), + n.stroke_style.stroke_align, + &n.effects, + ), + Node::RegularPolygon(n) => compute_inflation_uniform( + n.render_bounds_stroke_width(), + n.stroke_style.stroke_align, + &n.effects, + ), + Node::RegularStarPolygon(n) => compute_inflation_uniform( + n.render_bounds_stroke_width(), + n.stroke_style.stroke_align, + &n.effects, + ), + Node::Path(n) => compute_inflation_uniform( + n.stroke_width.value_or_zero(), + n.stroke_style.stroke_align, + &n.effects, + ), + Node::Vector(n) => { + compute_inflation_uniform(n.stroke_width, n.get_stroke_align(), &n.effects) + } + Node::Image(n) => compute_inflation_uniform( + n.render_bounds_stroke_width(), + n.stroke_style.stroke_align, + &n.effects, + ), + Node::Line(n) => { + compute_inflation_uniform(n.stroke_width, n.get_stroke_align(), &n.effects) + } + _ => RenderBoundsInflation::ZERO, + } +} + /// A scene graph that manages both the tree structure and node data. /// /// The SceneGraph maintains: /// - Root node IDs (direct children of the scene) /// - An adjacency list (parent->children) for the tree structure /// - A node repository for storing actual node data +/// - A parallel `geo_data` map with compact, schema-level geometry data +/// +/// The `geo_data` map is populated at construction time from the `Node` data. +/// It enables the geometry cache to compute transforms and bounds without +/// iterating over the full `Node` enum (critical for WASM performance). /// /// This provides a centralized, efficient way to manage scene hierarchy /// separate from node attributes. @@ -65,6 +441,17 @@ pub struct SceneGraph { nodes: NodeRepository, /// Optional display names for nodes (from the source file). names: HashMap, + /// Compact, schema-level geometry data per node. + /// + /// Populated at construction time from `Node` data. The geometry cache + /// reads this instead of iterating over the full `Node` enum. + geo_data: DenseNodeMap, + /// Compact, layer-relevant data per node. + /// + /// Populated at construction time from `Node` data. The effect tree and + /// layers DFS read this instead of iterating over the full `Node` enum + /// for visibility / opacity / blend mode checks. + layer_core: DenseNodeMap, } impl SceneGraph { @@ -75,6 +462,8 @@ impl SceneGraph { links: DenseNodeMap::new(), nodes: NodeRepository::new(), names: HashMap::new(), + geo_data: DenseNodeMap::new(), + layer_core: DenseNodeMap::new(), } } @@ -94,8 +483,11 @@ impl SceneGraph { ) -> Self { let mut graph = Self::new(); - // Add all nodes to the repository with their explicit IDs + // Add all nodes to the repository with their explicit IDs, + // extracting compact geo data and layer core at the same time. for (id, node) in node_pairs { + graph.geo_data.insert(id, extract_geo_data(&node)); + graph.layer_core.insert(id, extract_layer_core(&node)); graph.nodes.insert_with_id(id, node); } @@ -120,7 +512,11 @@ impl SceneGraph { /// /// Returns the node's ID. pub fn append_child(&mut self, node: Node, parent: Parent) -> NodeId { + let geo = extract_geo_data(&node); + let lc = extract_layer_core(&node); let id = self.nodes.insert(node); + self.geo_data.insert(id, geo); + self.layer_core.insert(id, lc); match parent { Parent::Root => { @@ -262,6 +658,7 @@ impl SceneGraph { /// Remove a node from the repository and return it pub fn remove_node(&mut self, id: &NodeId) -> SceneGraphResult { + self.geo_data.remove(id); self.nodes .remove(id) .ok_or_else(|| SceneGraphError::NodeNotFound(id.clone())) @@ -290,6 +687,25 @@ impl SceneGraph { self.nodes.iter() } + /// Access the compact, schema-level geometry data map. + /// + /// This map is populated at construction time and contains only the fields + /// needed for geometry computation (~48 bytes/node instead of ~500+). + /// The geometry cache reads this instead of iterating over the full Node enum. + pub fn geo_data(&self) -> &DenseNodeMap { + &self.geo_data + } + + /// Access the compact layer-core data map. + pub fn layer_core(&self) -> &DenseNodeMap { + &self.layer_core + } + + /// Get layer-core data for a single node. + pub fn get_layer_core(&self, id: &NodeId) -> Option<&NodeLayerCore> { + self.layer_core.get(id) + } + // ------------------------------------------------------------------------- // Tree Traversal Methods // ------------------------------------------------------------------------- diff --git a/crates/grida-canvas/src/node/schema.rs b/crates/grida-canvas/src/node/schema.rs index 78000f9ee5..325735922e 100644 --- a/crates/grida-canvas/src/node/schema.rs +++ b/crates/grida-canvas/src/node/schema.rs @@ -894,6 +894,216 @@ impl Default for LayoutDimensionStyle { } } +/// Discriminant tag for the [`Node`] enum — lets hot loops dispatch on node +/// type without touching the full 500+ byte `Node` variant. +/// +/// Used by [`NodeLayerCore`] and performance-critical DFS paths. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum NodeTypeTag { + InitialContainer, + Container, + Error, + Group, + Rectangle, + Ellipse, + Polygon, + RegularPolygon, + RegularStarPolygon, + Line, + TextSpan, + Path, + Vector, + BooleanOperation, + Image, +} + +/// Compact, layer-relevant data extracted from a `Node` at construction time. +/// +/// Stored in a parallel `DenseNodeMap` on `SceneGraph` so that the layers DFS +/// and effect tree never need to iterate over the full `Node` enum for basic +/// visibility / dispatch checks. +/// +/// This struct is `Copy` — no heap allocations, ~16 bytes total. +#[derive(Debug, Clone, Copy)] +pub struct NodeLayerCore { + /// Whether this node is visible. + pub active: bool, + /// Node opacity (0.0–1.0). + pub opacity: f32, + /// Blend mode. + pub blend_mode: LayerBlendMode, + /// Mask type (if any). + pub mask: Option, + /// Whether this container clips its descendants. + pub clips_content: bool, + /// Whether the node has any non-empty effects (quick check). + pub has_effects: bool, + /// Node type discriminant for dispatch. + pub node_type: NodeTypeTag, + /// Whether this node is a flex layout container (Container with LayoutMode::Flex + /// or InitialContainer). Used by the layout engine to skip Taffy for normal containers. + pub is_flex: bool, +} + +/// Extract compact layer-core data from a `Node`. +/// +/// Called once per node during `SceneGraph` construction. All values are `Copy`. +pub fn extract_layer_core(node: &Node) -> NodeLayerCore { + match node { + Node::InitialContainer(n) => NodeLayerCore { + active: n.active, + opacity: 1.0, + blend_mode: LayerBlendMode::PassThrough, + mask: None, + clips_content: false, + has_effects: false, + node_type: NodeTypeTag::InitialContainer, + is_flex: true, // ICB always uses flex layout + }, + Node::Container(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: n.clip, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::Container, + is_flex: n.layout_container.layout_mode == crate::cg::types::LayoutMode::Flex, + }, + Node::Error(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: LayerBlendMode::PassThrough, + mask: None, + clips_content: false, + has_effects: false, + node_type: NodeTypeTag::Error, + is_flex: false, + }, + Node::Group(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: false, + node_type: NodeTypeTag::Group, + is_flex: false, + }, + Node::Rectangle(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::Rectangle, + is_flex: false, + }, + Node::Ellipse(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::Ellipse, + is_flex: false, + }, + Node::Polygon(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::Polygon, + is_flex: false, + }, + Node::RegularPolygon(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::RegularPolygon, + is_flex: false, + }, + Node::RegularStarPolygon(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::RegularStarPolygon, + is_flex: false, + }, + Node::Line(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::Line, + is_flex: false, + }, + Node::TextSpan(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::TextSpan, + is_flex: false, + }, + Node::Path(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::Path, + is_flex: false, + }, + Node::Vector(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::Vector, + is_flex: false, + }, + Node::BooleanOperation(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::BooleanOperation, + is_flex: false, + }, + Node::Image(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::Image, + is_flex: false, + }, + } +} + #[derive(Debug, Clone)] pub enum Node { InitialContainer(InitialContainerNodeRec), diff --git a/crates/grida-canvas/src/painter/layer.rs b/crates/grida-canvas/src/painter/layer.rs index b1ddd76798..fc987645d7 100644 --- a/crates/grida-canvas/src/painter/layer.rs +++ b/crates/grida-canvas/src/painter/layer.rs @@ -484,14 +484,18 @@ impl LayerList { parent_opacity: f32, out: &mut Vec, ) -> FlattenResult { + // Fast-path: check active from compact layer_core (~16 bytes) + // before touching the full Node enum (~500+ bytes). + if let Some(lc) = graph.get_layer_core(id) { + if !lc.active { + return FlattenResult::default(); + } + } + let Ok(node) = graph.get_node(id) else { return FlattenResult::default(); }; - if !node.active() { - return FlattenResult::default(); - } - let transform = scene_cache .geometry() .get_world_transform(id) diff --git a/crates/grida-canvas/src/runtime/effect_tree.rs b/crates/grida-canvas/src/runtime/effect_tree.rs index f0a6cc5335..294349f3dd 100644 --- a/crates/grida-canvas/src/runtime/effect_tree.rs +++ b/crates/grida-canvas/src/runtime/effect_tree.rs @@ -43,7 +43,7 @@ use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; use crate::cg::types::LayerBlendMode; use crate::node::id::NodeId; use crate::node::scene_graph::SceneGraph; -use crate::node::schema::{Node, NodeTrait}; +use crate::node::schema::NodeLayerCore; /// Why a node needs a render surface. /// @@ -173,6 +173,10 @@ impl EffectTree { } /// Recursive visitor that checks each node for render surface triggers. + /// + /// Uses `layer_core` (compact ~16-byte struct) for all visibility, opacity, + /// blend mode, and mask checks — never touches the full 500+ byte `Node` + /// enum unless the node has effects that need detail inspection. fn visit( graph: &SceneGraph, id: &NodeId, @@ -181,35 +185,37 @@ impl EffectTree { ) { stats.nodes_visited += 1; - let node = match graph.get_node(id) { - Ok(n) => n, - Err(_) => return, + // Read from compact layer_core (~16 bytes) instead of full Node (~500 bytes). + let lc = match graph.get_layer_core(id) { + Some(lc) => *lc, + None => return, }; // Skip inactive nodes — they produce no visual output. - if !node.active() { + if !lc.active { return; } let all_children = graph.get_children(id); let all_children_slice = all_children.map(|c| c.as_slice()).unwrap_or(&[]); - // Count visible children and check for masks without allocating a Vec. + // Count visible children using layer_core (no full Node touch). + let layer_core_map = graph.layer_core(); let visible_child_count = all_children_slice .iter() .filter(|cid| { - graph - .get_node(cid) - .map(|n| n.active()) + layer_core_map + .get(cid) + .map(|c| c.active) .unwrap_or(false) }) .count(); - // Collect render surface reasons for this node. - let reasons = Self::classify(node, visible_child_count, all_children_slice, graph); + // Collect render surface reasons using layer_core for fast checks. + // Only access full Node when effects detail is needed. + let reasons = Self::classify_from_core(id, &lc, visible_child_count, all_children_slice, graph); if !reasons.is_empty() { - // Update per-reason stats. for reason in &reasons { match reason { RenderSurfaceReason::Opacity => stats.by_reason.opacity += 1, @@ -225,9 +231,9 @@ impl EffectTree { let active_children: Vec = all_children_slice .iter() .filter(|cid| { - graph - .get_node(cid) - .map(|n| n.active()) + layer_core_map + .get(cid) + .map(|c| c.active) .unwrap_or(false) }) .copied() @@ -251,11 +257,14 @@ impl EffectTree { } } - /// Classify a node: determine which render surface reasons apply. + /// Classify a node using `NodeLayerCore` for fast-path checks. /// - /// Returns an empty vec if the node doesn't need a render surface. - fn classify( - node: &Node, + /// Only accesses the full `Node` when the node has effects (blur/shadows) + /// that need detail inspection. For the majority of nodes (no effects), + /// this never touches the 500+ byte Node enum. + fn classify_from_core( + id: &NodeId, + lc: &NodeLayerCore, visible_child_count: usize, children: &[NodeId], graph: &SceneGraph, @@ -263,66 +272,51 @@ impl EffectTree { let mut reasons = Vec::new(); // --- Opacity isolation --- - // Only needed when a container-like node has opacity < 1.0 AND - // has 2+ visible children. A single child (or leaf) can have - // opacity applied directly without isolation. - if node.opacity() < 1.0 && visible_child_count >= 2 { + if lc.opacity < 1.0 && visible_child_count >= 2 { reasons.push(RenderSurfaceReason::Opacity); } // --- Blend mode isolation --- - // Non-PassThrough blend modes require the subtree to be drawn into - // an offscreen before blending with the backdrop. Only applies to - // container-like nodes with children (leaf blend modes are handled - // by the painter directly). - if node.blend_mode() != LayerBlendMode::PassThrough && visible_child_count >= 1 { + if lc.blend_mode != LayerBlendMode::PassThrough && visible_child_count >= 1 { reasons.push(RenderSurfaceReason::BlendMode); } // --- Effects that benefit from render surfaces --- - if let Some(effects) = node.effects() { - // Layer blur: applies to entire subtree content. - // Only creates a surface when there are children — leaf nodes - // have their blur applied directly by the painter. - if effects.blur.as_ref().is_some_and(|b| b.active) && visible_child_count >= 1 { - reasons.push(RenderSurfaceReason::LayerBlur); - } - - // Shadows: a render surface lets us compute the shadow filter - // once for the entire subtree instead of per-child. - // Only for container-like nodes — leaf shadows are per-node. - if !effects.shadows.is_empty() && visible_child_count >= 1 { - reasons.push(RenderSurfaceReason::Shadow); + // Only access full Node when has_effects is true (minority of nodes). + if lc.has_effects && visible_child_count >= 1 { + if let Ok(node) = graph.get_node(id) { + if let Some(effects) = node.effects() { + if effects.blur.as_ref().is_some_and(|b| b.active) { + reasons.push(RenderSurfaceReason::LayerBlur); + } + if !effects.shadows.is_empty() { + reasons.push(RenderSurfaceReason::Shadow); + } + } } - - // Note: backdrop_blur and liquid_glass are context-dependent - // (they read from content behind the node). They are handled - // per-node by the painter and do NOT create render surfaces. } // --- Clip --- - // Container clip=true requires clipping descendants to the - // container's shape. This is a render surface trigger. - if node.clips_content() && visible_child_count >= 1 { + if lc.clips_content && visible_child_count >= 1 { reasons.push(RenderSurfaceReason::Clip); } // --- Mask groups --- - // If any child is a mask node, this parent needs a render surface - // to implement the mask compositing (DstIn blend). - if Self::has_mask_children(children, graph) { + // Check children's mask from layer_core (no full Node needed). + if Self::has_mask_children_from_core(children, graph) { reasons.push(RenderSurfaceReason::Mask); } reasons } - /// Check if any of the given children are active mask nodes. - fn has_mask_children(children: &[NodeId], graph: &SceneGraph) -> bool { + /// Check if any of the given children are active mask nodes using layer_core. + fn has_mask_children_from_core(children: &[NodeId], graph: &SceneGraph) -> bool { + let layer_core_map = graph.layer_core(); children.iter().any(|cid| { - graph - .get_node(cid) - .map(|n| n.active() && n.mask().is_some()) + layer_core_map + .get(cid) + .map(|c| c.active && c.mask.is_some()) .unwrap_or(false) }) } diff --git a/crates/grida-canvas/src/window/application.rs b/crates/grida-canvas/src/window/application.rs index 101a0e9c98..1313f3a8d7 100644 --- a/crates/grida-canvas/src/window/application.rs +++ b/crates/grida-canvas/src/window/application.rs @@ -660,8 +660,9 @@ impl ApplicationApi for UnknownTargetApplication { } fn switch_scene(&mut self, scene_id: &str) { - if let Some((_, scene)) = self.loaded_scenes.iter().find(|(id, _)| id == scene_id) { - self.renderer.load_scene(scene.clone()); + if let Some(pos) = self.loaded_scenes.iter().position(|(id, _)| id == scene_id) { + let (_, scene) = self.loaded_scenes.swap_remove(pos); + self.renderer.load_scene(scene); self.queue(); } else { eprintln!( @@ -676,7 +677,10 @@ impl ApplicationApi for UnknownTargetApplication { } fn loaded_scene_ids(&self) -> Vec { - self.loaded_scenes.iter().map(|(id, _)| id.clone()).collect() + self.loaded_scenes + .iter() + .map(|(id, _)| id.clone()) + .collect() } fn apply_document_transactions( From 6783a3146cf6152b59577780d1985db1a785c77b Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 25 Mar 2026 14:33:13 +0900 Subject: [PATCH 25/31] fix(format): handle null id in NodeIdentifier creation and improve transform formatting - Updated NodeIdentifier creation to handle null id by using builder.createString(id) when id is not provided. - Reformatted the postLayoutTransform parameters for better readability, aligning the arguments vertically. --- packages/grida-canvas-io/format.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/grida-canvas-io/format.ts b/packages/grida-canvas-io/format.ts index 17650b5692..4695f3f21f 100644 --- a/packages/grida-canvas-io/format.ts +++ b/packages/grida-canvas-io/format.ts @@ -439,7 +439,9 @@ export namespace format { builder: Builder, id: string ): flatbuffers.Offset { - const idOffset = builder.createSharedString(id); + const idOffset = id + ? builder.createSharedString(id) + : builder.createString(id); return fbs.NodeIdentifier.createNodeIdentifier(builder, idOffset); } @@ -1073,8 +1075,12 @@ export namespace format { const postLayoutTransformOffset = structs.cgTransform2D( builder, - cos, -sin, 0, - sin, cos, 0 + cos, + -sin, + 0, + sin, + cos, + 0 ); fbs.LayerTrait.addPostLayoutTransform( builder, From b84ca70b0e60f09553589d8372162023506125cb Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 25 Mar 2026 14:33:54 +0900 Subject: [PATCH 26/31] perf logs --- editor/grida-canvas/editor.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/editor/grida-canvas/editor.ts b/editor/grida-canvas/editor.ts index 1627e2312b..ced9b4d101 100644 --- a/editor/grida-canvas/editor.ts +++ b/editor/grida-canvas/editor.ts @@ -3214,10 +3214,17 @@ export class Editor sceneId?: string ) => { const t0 = __DEV__ ? performance.now() : 0; + let tLoad = 0; + let encodeMs = 0; + let loadMs = 0; try { const bytes = io.GRID.encode(document); + const tEncode = __DEV__ ? performance.now() : 0; surface.loadSceneGrida(bytes); + tLoad = __DEV__ ? performance.now() : 0; + encodeMs = tEncode - t0; + loadMs = tLoad - tEncode; } catch { // Fallback to JSON if FlatBuffers encoding fails (e.g. unsupported node types) const p = JSON.stringify({ @@ -3225,6 +3232,9 @@ export class Editor document, }); surface.loadScene(p); + tLoad = __DEV__ ? performance.now() : 0; + encodeMs = 0; + loadMs = tLoad - t0; } // loadSceneGrida only decodes and stores scenes. @@ -3236,12 +3246,17 @@ export class Editor surface.switchScene(targetScene); } + const tSwitch = __DEV__ ? performance.now() : 0; + surface.redraw(); if (__DEV__) { + const tRedraw = performance.now(); console.log( `[syncDocument] ${Object.keys(document.nodes).length} nodes, ` + - `scene=${targetScene ?? "(none)"} in ${(performance.now() - t0).toFixed(0)}ms` + `scene=${targetScene ?? "(none)"} in ${(tRedraw - t0).toFixed(0)}ms` + + ` (encode=${encodeMs.toFixed(0)}ms load=${loadMs.toFixed(0)}ms` + + ` switch=${(tSwitch - tLoad).toFixed(0)}ms redraw=${(tRedraw - tSwitch).toFixed(0)}ms)` ); } }; From 930180d39b2557e707a741dfb9cfef034139a90e Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 25 Mar 2026 14:34:12 +0900 Subject: [PATCH 27/31] chore --- crates/grida-canvas/src/io/io_grida_fbs.rs | 6 ++++ crates/grida-canvas/src/node/scene_graph.rs | 39 ++++++++++++++++++++- crates/grida-canvas/src/runtime/scene.rs | 6 ++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/crates/grida-canvas/src/io/io_grida_fbs.rs b/crates/grida-canvas/src/io/io_grida_fbs.rs index f16cfe005b..b9119acb35 100644 --- a/crates/grida-canvas/src/io/io_grida_fbs.rs +++ b/crates/grida-canvas/src/io/io_grida_fbs.rs @@ -428,6 +428,12 @@ fn decode_all_inner(bytes: &[u8]) -> Result { // The last scene consumes node_pairs/internal_links by move to avoid // a deep clone of all Node enums. Earlier scenes (multi-scene files) // must clone. Single-scene files (the common case) get zero clones. + // + // TODO: Each SceneGraph currently receives ALL document nodes, not just + // the nodes reachable from that scene's roots. This causes unnecessary + // extraction (geo_data, layer_core) and storage for orphan nodes. + // Future: compute reachability per scene from `internal_links` + roots, + // then pass only reachable nodes to `new_from_snapshot`. let mut scenes: Vec = Vec::new(); // Helper: resolve scene root NodeIds from the scene string ID. diff --git a/crates/grida-canvas/src/node/scene_graph.rs b/crates/grida-canvas/src/node/scene_graph.rs index 79750e180b..afd5439284 100644 --- a/crates/grida-canvas/src/node/scene_graph.rs +++ b/crates/grida-canvas/src/node/scene_graph.rs @@ -452,6 +452,12 @@ pub struct SceneGraph { /// layers DFS read this instead of iterating over the full `Node` enum /// for visibility / opacity / blend mode checks. layer_core: DenseNodeMap, + /// Whether the scene contains any flex layout containers. + /// + /// When `false`, the layout engine can skip Taffy entirely and use + /// schema positions/sizes directly — saving ~1,500ms for 136K-node + /// Figma imports where all containers use `LayoutMode::Normal`. + has_flex: bool, } impl SceneGraph { @@ -464,6 +470,7 @@ impl SceneGraph { names: HashMap::new(), geo_data: DenseNodeMap::new(), layer_core: DenseNodeMap::new(), + has_flex: false, } } @@ -476,6 +483,18 @@ impl SceneGraph { /// * `nodes` - Iterator of nodes to add to the repository /// * `links` - HashMap of parent->children relationships /// * `roots` - Root node IDs (direct children of the scene) + // TODO: Currently `new_from_snapshot` receives ALL nodes from the + // document (across all scenes) but only one scene's roots. This means + // geo_data, layer_core, and the node repository contain orphan nodes + // not reachable from the current scene's roots. Downstream passes + // (geometry, layout, layers, effects) already scope their work to + // root-reachable nodes, but the extraction loop and storage still pay + // O(total_nodes) instead of O(scene_nodes). In a multi-scene document + // this is wasted work and memory. + // + // Future: either filter `node_pairs` to only scene-reachable nodes + // before calling this, or accept a reachability set so extraction and + // insertion can be bounded to the current scene. pub fn new_from_snapshot( node_pairs: impl IntoIterator, links: HashMap>, @@ -485,11 +504,18 @@ impl SceneGraph { // Add all nodes to the repository with their explicit IDs, // extracting compact geo data and layer core at the same time. + // Also detect whether any flex containers exist (for layout skip optimization). + let mut has_flex = false; for (id, node) in node_pairs { graph.geo_data.insert(id, extract_geo_data(&node)); - graph.layer_core.insert(id, extract_layer_core(&node)); + let lc = extract_layer_core(&node); + if lc.is_flex { + has_flex = true; + } + graph.layer_core.insert(id, lc); graph.nodes.insert_with_id(id, node); } + graph.has_flex = has_flex; // Convert HashMap links to DenseNodeMap let mut dense_links = DenseNodeMap::new(); @@ -514,6 +540,9 @@ impl SceneGraph { pub fn append_child(&mut self, node: Node, parent: Parent) -> NodeId { let geo = extract_geo_data(&node); let lc = extract_layer_core(&node); + if lc.is_flex { + self.has_flex = true; + } let id = self.nodes.insert(node); self.geo_data.insert(id, geo); self.layer_core.insert(id, lc); @@ -706,6 +735,14 @@ impl SceneGraph { self.layer_core.get(id) } + /// Whether the scene contains any flex layout containers. + /// + /// When `false`, all containers use `LayoutMode::Normal` (absolute + /// positioning) and the layout engine can skip Taffy entirely. + pub fn has_flex(&self) -> bool { + self.has_flex + } + // ------------------------------------------------------------------------- // Tree Traversal Methods // ------------------------------------------------------------------------- diff --git a/crates/grida-canvas/src/runtime/scene.rs b/crates/grida-canvas/src/runtime/scene.rs index fcbd50799d..4e097e6d7d 100644 --- a/crates/grida-canvas/src/runtime/scene.rs +++ b/crates/grida-canvas/src/runtime/scene.rs @@ -1358,6 +1358,12 @@ impl Renderer { let viewport_size = self.window_context.viewport_size; // 1. Compute layout phase + // + // NOTE: We cannot auto-skip Taffy based on has_flex() alone because + // compute_schema_only() skips text measurement — text nodes with + // height=None would get height=0 and become invisible. A future + // optimization could use a hybrid path that skips flex computation + // but still measures text via the paragraph cache. if self.config.skip_layout { // Fast path: derive layout directly from schema positions/sizes. // Skips Taffy tree construction, flexbox computation, and text From 9db0e99f7459a90aeaed22a90ca3cba755c89a2c Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 25 Mar 2026 14:42:11 +0900 Subject: [PATCH 28/31] wasm 0.91.0-canary.13 --- crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js | 2 +- crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm | 4 ++-- crates/grida-canvas-wasm/package.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js b/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js index 4094ee5487..05337f71df 100644 --- a/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js +++ b/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js @@ -1,2 +1,2 @@ -var createGridaCanvas=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else{}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.initialized)FS.init();TTY.init();wasmExports["Ng"]();FS.ignorePermissions=false}function preMain(){}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("grida_canvas_wasm.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var exceptionCaught=[];var uncaughtExceptionCount=0;var ___cxa_begin_catch=ptr=>{var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);return ___cxa_get_exception_ptr(ptr)};var exceptionLast=0;var ___cxa_end_catch=()=>{_setThrew(0,0);var info=exceptionCaught.pop();___cxa_decrement_exception_refcount(info.excPtr);exceptionLast=0};class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var setTempRet0=val=>__emscripten_tempret_set(val);var findMatchingCatch=args=>{var thrown=exceptionLast;if(!thrown){setTempRet0(0);return 0}var info=new ExceptionInfo(thrown);info.set_adjusted_ptr(thrown);var thrownType=info.get_type();if(!thrownType){setTempRet0(0);return thrown}for(var caughtType of args){if(caughtType===0||caughtType===thrownType){break}var adjusted_ptr_addr=info.ptr+16;if(___cxa_can_catch(caughtType,thrownType,adjusted_ptr_addr)){setTempRet0(caughtType);return thrown}}setTempRet0(thrownType);return thrown};var ___cxa_find_matching_catch_2=()=>findMatchingCatch([]);var ___cxa_find_matching_catch_3=arg0=>findMatchingCatch([arg0]);var ___cxa_find_matching_catch_4=(arg0,arg1)=>findMatchingCatch([arg0,arg1]);var ___cxa_rethrow=()=>{var info=exceptionCaught.pop();if(!info){abort("no exception to throw")}var ptr=info.excPtr;if(!info.get_rethrown()){exceptionCaught.push(info);info.set_rethrown(true);info.set_caught(false);uncaughtExceptionCount++}___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;throw exceptionLast};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var ___cxa_uncaught_exceptions=()=>uncaughtExceptionCount;var ___resumeException=ptr=>{if(!exceptionLast){exceptionLast=ptr}throw exceptionLast};var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\/]+|\/)\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(ENVIRONMENT_IS_NODE){var nodeCrypto=require("node:crypto");return view=>nodeCrypto.randomFillSync(view)}return view=>crypto.getRandomValues(view)};var randomFill=view=>{(randomFill=initRandomFill())(view)};var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(globalThis.window?.prompt){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var zeroMemory=(ptr,size)=>HEAPU8.fill(0,ptr,ptr+size);var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(ptr)zeroMemory(ptr,size);return ptr};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of["mode","atime","mtime","ctime"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){if(!MEMFS.doesNotExistError){MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack=""}throw MEMFS.doesNotExistError},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[".","..",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var asyncLoad=async url=>{var arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(...args)=>FS.createDataFile(...args);var getUniqueRunDependency=id=>id;var runDependencies=0;var dependenciesFulfilled=null;var removeRunDependency=id=>{runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}};var addRunDependency=id=>{runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)};var preloadPlugins=[];var FS_handledByPreloadPlugin=async(byteArray,fullname)=>{if(typeof Browser!="undefined")Browser.init();for(var plugin of preloadPlugins){if(plugin["canHandle"](fullname)){return plugin["handle"](byteArray,fullname)}}return byteArray};var FS_preloadFile=async(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);addRunDependency(dep);try{var byteArray=url;if(typeof url=="string"){byteArray=await asyncLoad(url)}byteArray=await FS_handledByPreloadPlugin(byteArray,fullname);preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}}finally{removeRunDependency(dep)}};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{FS_preloadFile(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish).then(onload).catch(onerror)};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,readFiles:{},ErrnoError:class{name="ErrnoError";constructor(errno){this.errno=errno}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+"/"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&(512|64)){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);setattr(arg,attr)},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}for(var mount of mounts){if(mount.type.syncfs){mount.type.syncfs(mount,populate,done)}else{done(null)}}},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);for(var[hash,current]of Object.entries(FS.nameTable)){while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}}node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name==="."||name===".."){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+="/";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS_modeStringToFlags(flags):flags;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path=="object"){node=path}else{isDirPath=path.endsWith("/");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}if(Module["logReadFiles"]&&!(flags&1)){if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){abort(`Invalid encoding type "${opts.encoding}"`)}var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){buf=UTF8ArrayToString(buf)}FS.close(stream);return buf},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){data=new Uint8Array(intArrayFromString(data,true))}if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{abort("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)abort("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)abort("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")abort("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(globalThis.XMLHttpRequest){if(!ENVIRONMENT_IS_WORKER)abort("Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc");var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};for(const[key,fn]of Object.entries(node.stream_ops)){stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}}function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var SYSCALLS={calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+"/"+path},writeStat(buf,stat){HEAPU32[buf>>2]=stat.dev;HEAPU32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAPU32[buf+12>>2]=stat.uid;HEAPU32[buf+16>>2]=stat.gid;HEAPU32[buf+20>>2]=stat.rdev;HEAP64[buf+24>>3]=BigInt(stat.size);HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAPU32[buf+4>>2]=stats.bsize;HEAPU32[buf+60>>2]=stats.bsize;HEAP64[buf+8>>3]=BigInt(stats.blocks);HEAP64[buf+16>>3]=BigInt(stats.bfree);HEAP64[buf+24>>3]=BigInt(stats.bavail);HEAP64[buf+32>>3]=BigInt(stats.files);HEAP64[buf+40>>3]=BigInt(stats.ffree);HEAPU32[buf+48>>2]=stats.fsid;HEAPU32[buf+64>>2]=stats.flags;HEAPU32[buf+56>>2]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{return SYSCALLS.writeStat(buf,FS.fstat(fd))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);function ___syscall_getcwd(buf,size){try{if(size===0)return-28;var cwd=FS.cwd();var cwdLengthInBytes=lengthBytesUTF8(cwd)+1;if(size>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21537:case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.lstat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.writeStat(buf,nofollow?FS.lstat(path):FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>abort("");var __emscripten_throw_longjmp=()=>{throw Infinity};var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function __gmtime_js(time,tmPtr){time=bigintToI53Checked(time);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getUTCSeconds();HEAP32[tmPtr+4>>2]=date.getUTCMinutes();HEAP32[tmPtr+8>>2]=date.getUTCHours();HEAP32[tmPtr+12>>2]=date.getUTCDate();HEAP32[tmPtr+16>>2]=date.getUTCMonth();HEAP32[tmPtr+20>>2]=date.getUTCFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getUTCDay();var start=Date.UTC(date.getUTCFullYear(),0,1,0,0,0,0);var yday=(date.getTime()-start)/(1e3*60*60*24)|0;HEAP32[tmPtr+28>>2]=yday}function __mmap_js(len,prot,flags,fd,offset,allocated,addr){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;HEAPU32[addr>>2]=ptr;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffsetperformance.now();var _emscripten_date_now=()=>Date.now();var nowIsMonotonic=1;var checkWasiClock=clock_id=>clock_id>=0&&clock_id<=3;function _clock_time_get(clk_id,ignored_precision,ptime){ignored_precision=bigintToI53Checked(ignored_precision);if(!checkWasiClock(clk_id)){return 28}var now;if(clk_id===0){now=_emscripten_date_now()}else if(nowIsMonotonic){now=_emscripten_get_now()}else{return 52}var nsec=Math.round(now*1e3*1e3);HEAP64[ptime>>3]=BigInt(nsec);return 0}var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var GLctx;var webgl_enable_ANGLE_instanced_arrays=ctx=>{var ext=ctx.getExtension("ANGLE_instanced_arrays");if(ext){ctx["vertexAttribDivisor"]=(index,divisor)=>ext["vertexAttribDivisorANGLE"](index,divisor);ctx["drawArraysInstanced"]=(mode,first,count,primcount)=>ext["drawArraysInstancedANGLE"](mode,first,count,primcount);ctx["drawElementsInstanced"]=(mode,count,type,indices,primcount)=>ext["drawElementsInstancedANGLE"](mode,count,type,indices,primcount);return 1}};var webgl_enable_OES_vertex_array_object=ctx=>{var ext=ctx.getExtension("OES_vertex_array_object");if(ext){ctx["createVertexArray"]=()=>ext["createVertexArrayOES"]();ctx["deleteVertexArray"]=vao=>ext["deleteVertexArrayOES"](vao);ctx["bindVertexArray"]=vao=>ext["bindVertexArrayOES"](vao);ctx["isVertexArray"]=vao=>ext["isVertexArrayOES"](vao);return 1}};var webgl_enable_WEBGL_draw_buffers=ctx=>{var ext=ctx.getExtension("WEBGL_draw_buffers");if(ext){ctx["drawBuffers"]=(n,bufs)=>ext["drawBuffersWEBGL"](n,bufs);return 1}};var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.dibvbi=ctx.getExtension("WEBGL_draw_instanced_base_vertex_base_instance"));var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.mdibvbi=ctx.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance"));var webgl_enable_EXT_polygon_offset_clamp=ctx=>!!(ctx.extPolygonOffsetClamp=ctx.getExtension("EXT_polygon_offset_clamp"));var webgl_enable_EXT_clip_control=ctx=>!!(ctx.extClipControl=ctx.getExtension("EXT_clip_control"));var webgl_enable_WEBGL_polygon_mode=ctx=>!!(ctx.webglPolygonMode=ctx.getExtension("WEBGL_polygon_mode"));var webgl_enable_WEBGL_multi_draw=ctx=>!!(ctx.multiDrawWebgl=ctx.getExtension("WEBGL_multi_draw"));var getEmscriptenSupportedExtensions=ctx=>{var supportedExtensions=["ANGLE_instanced_arrays","EXT_blend_minmax","EXT_disjoint_timer_query","EXT_frag_depth","EXT_shader_texture_lod","EXT_sRGB","OES_element_index_uint","OES_fbo_render_mipmap","OES_standard_derivatives","OES_texture_float","OES_texture_half_float","OES_texture_half_float_linear","OES_vertex_array_object","WEBGL_color_buffer_float","WEBGL_depth_texture","WEBGL_draw_buffers","EXT_color_buffer_float","EXT_conservative_depth","EXT_disjoint_timer_query_webgl2","EXT_texture_norm16","NV_shader_noperspective_interpolation","WEBGL_clip_cull_distance","EXT_clip_control","EXT_color_buffer_half_float","EXT_depth_clamp","EXT_float_blend","EXT_polygon_offset_clamp","EXT_texture_compression_bptc","EXT_texture_compression_rgtc","EXT_texture_filter_anisotropic","KHR_parallel_shader_compile","OES_texture_float_linear","WEBGL_blend_func_extended","WEBGL_compressed_texture_astc","WEBGL_compressed_texture_etc","WEBGL_compressed_texture_etc1","WEBGL_compressed_texture_s3tc","WEBGL_compressed_texture_s3tc_srgb","WEBGL_debug_renderer_info","WEBGL_debug_shaders","WEBGL_lose_context","WEBGL_multi_draw","WEBGL_polygon_mode"];return(ctx.getSupportedExtensions()||[]).filter(ext=>supportedExtensions.includes(ext))};var GL={counter:1,buffers:[],programs:[],framebuffers:[],renderbuffers:[],textures:[],shaders:[],vaos:[],contexts:[],offscreenCanvases:{},queries:[],samplers:[],transformFeedbacks:[],syncs:[],stringCache:{},stringiCache:{},unpackAlignment:4,unpackRowLength:0,recordError:errorCode=>{if(!GL.lastError){GL.lastError=errorCode}},getNewId:table=>{var ret=GL.counter++;for(var i=table.length;i{for(var i=0;i>2]=id}},getSource:(shader,count,string,length)=>{var source="";for(var i=0;i>2]:undefined;source+=UTF8ToString(HEAPU32[string+i*4>>2],len)}return source},createContext:(canvas,webGLContextAttributes)=>{if(!canvas.getContextSafariWebGL2Fixed){canvas.getContextSafariWebGL2Fixed=canvas.getContext;function fixedGetContext(ver,attrs){var gl=canvas.getContextSafariWebGL2Fixed(ver,attrs);return ver=="webgl"==gl instanceof WebGLRenderingContext?gl:null}canvas.getContext=fixedGetContext}var ctx=webGLContextAttributes.majorVersion>1?canvas.getContext("webgl2",webGLContextAttributes):canvas.getContext("webgl",webGLContextAttributes);if(!ctx)return 0;var handle=GL.registerContext(ctx,webGLContextAttributes);return handle},registerContext:(ctx,webGLContextAttributes)=>{var handle=GL.getNewId(GL.contexts);var context={handle,attributes:webGLContextAttributes,version:webGLContextAttributes.majorVersion,GLctx:ctx};if(ctx.canvas)ctx.canvas.GLctxObject=context;GL.contexts[handle]=context;if(typeof webGLContextAttributes.enableExtensionsByDefault=="undefined"||webGLContextAttributes.enableExtensionsByDefault){GL.initExtensions(context)}return handle},makeContextCurrent:contextHandle=>{GL.currentContext=GL.contexts[contextHandle];Module["ctx"]=GLctx=GL.currentContext?.GLctx;return!(contextHandle&&!GLctx)},getContext:contextHandle=>GL.contexts[contextHandle],deleteContext:contextHandle=>{if(GL.currentContext===GL.contexts[contextHandle]){GL.currentContext=null}if(typeof JSEvents=="object"){JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas)}if(GL.contexts[contextHandle]?.GLctx.canvas){GL.contexts[contextHandle].GLctx.canvas.GLctxObject=undefined}GL.contexts[contextHandle]=null},initExtensions:context=>{context||=GL.currentContext;if(context.initExtensionsDone)return;context.initExtensionsDone=true;var GLctx=context.GLctx;webgl_enable_WEBGL_multi_draw(GLctx);webgl_enable_EXT_polygon_offset_clamp(GLctx);webgl_enable_EXT_clip_control(GLctx);webgl_enable_WEBGL_polygon_mode(GLctx);webgl_enable_ANGLE_instanced_arrays(GLctx);webgl_enable_OES_vertex_array_object(GLctx);webgl_enable_WEBGL_draw_buffers(GLctx);webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);if(context.version>=2){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query_webgl2")}if(context.version<2||!GLctx.disjointTimerQueryExt){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query")}for(var ext of getEmscriptenSupportedExtensions(GLctx)){if(!ext.includes("lose_context")&&!ext.includes("debug")){GLctx.getExtension(ext)}}}};var _emscripten_glActiveTexture=x0=>GLctx.activeTexture(x0);var _emscripten_glAttachShader=(program,shader)=>{GLctx.attachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glBeginQuery=(target,id)=>{GLctx.beginQuery(target,GL.queries[id])};var _emscripten_glBeginQueryEXT=(target,id)=>{GLctx.disjointTimerQueryExt["beginQueryEXT"](target,GL.queries[id])};var _emscripten_glBeginTransformFeedback=x0=>GLctx.beginTransformFeedback(x0);var _emscripten_glBindAttribLocation=(program,index,name)=>{GLctx.bindAttribLocation(GL.programs[program],index,UTF8ToString(name))};var _emscripten_glBindBuffer=(target,buffer)=>{if(target==35051){GLctx.currentPixelPackBufferBinding=buffer}else if(target==35052){GLctx.currentPixelUnpackBufferBinding=buffer}GLctx.bindBuffer(target,GL.buffers[buffer])};var _emscripten_glBindBufferBase=(target,index,buffer)=>{GLctx.bindBufferBase(target,index,GL.buffers[buffer])};var _emscripten_glBindBufferRange=(target,index,buffer,offset,ptrsize)=>{GLctx.bindBufferRange(target,index,GL.buffers[buffer],offset,ptrsize)};var _emscripten_glBindFramebuffer=(target,framebuffer)=>{GLctx.bindFramebuffer(target,GL.framebuffers[framebuffer])};var _emscripten_glBindRenderbuffer=(target,renderbuffer)=>{GLctx.bindRenderbuffer(target,GL.renderbuffers[renderbuffer])};var _emscripten_glBindSampler=(unit,sampler)=>{GLctx.bindSampler(unit,GL.samplers[sampler])};var _emscripten_glBindTexture=(target,texture)=>{GLctx.bindTexture(target,GL.textures[texture])};var _emscripten_glBindTransformFeedback=(target,id)=>{GLctx.bindTransformFeedback(target,GL.transformFeedbacks[id])};var _emscripten_glBindVertexArray=vao=>{GLctx.bindVertexArray(GL.vaos[vao])};var _glBindVertexArray=_emscripten_glBindVertexArray;var _emscripten_glBindVertexArrayOES=_glBindVertexArray;var _emscripten_glBlendColor=(x0,x1,x2,x3)=>GLctx.blendColor(x0,x1,x2,x3);var _emscripten_glBlendEquation=x0=>GLctx.blendEquation(x0);var _emscripten_glBlendEquationSeparate=(x0,x1)=>GLctx.blendEquationSeparate(x0,x1);var _emscripten_glBlendFunc=(x0,x1)=>GLctx.blendFunc(x0,x1);var _emscripten_glBlendFuncSeparate=(x0,x1,x2,x3)=>GLctx.blendFuncSeparate(x0,x1,x2,x3);var _emscripten_glBlitFramebuffer=(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9)=>GLctx.blitFramebuffer(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9);var _emscripten_glBufferData=(target,size,data,usage)=>{if(GL.currentContext.version>=2){if(data&&size){GLctx.bufferData(target,HEAPU8,usage,data,size)}else{GLctx.bufferData(target,size,usage)}return}GLctx.bufferData(target,data?HEAPU8.subarray(data,data+size):size,usage)};var _emscripten_glBufferSubData=(target,offset,size,data)=>{if(GL.currentContext.version>=2){size&&GLctx.bufferSubData(target,offset,HEAPU8,data,size);return}GLctx.bufferSubData(target,offset,HEAPU8.subarray(data,data+size))};var _emscripten_glCheckFramebufferStatus=x0=>GLctx.checkFramebufferStatus(x0);var _emscripten_glClear=x0=>GLctx.clear(x0);var _emscripten_glClearBufferfi=(x0,x1,x2,x3)=>GLctx.clearBufferfi(x0,x1,x2,x3);var _emscripten_glClearBufferfv=(buffer,drawbuffer,value)=>{GLctx.clearBufferfv(buffer,drawbuffer,HEAPF32,value>>2)};var _emscripten_glClearBufferiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferiv(buffer,drawbuffer,HEAP32,value>>2)};var _emscripten_glClearBufferuiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferuiv(buffer,drawbuffer,HEAPU32,value>>2)};var _emscripten_glClearColor=(x0,x1,x2,x3)=>GLctx.clearColor(x0,x1,x2,x3);var _emscripten_glClearDepthf=x0=>GLctx.clearDepth(x0);var _emscripten_glClearStencil=x0=>GLctx.clearStencil(x0);var _emscripten_glClientWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);return GLctx.clientWaitSync(GL.syncs[sync],flags,timeout)};var _emscripten_glClipControlEXT=(origin,depth)=>{GLctx.extClipControl["clipControlEXT"](origin,depth)};var _emscripten_glColorMask=(red,green,blue,alpha)=>{GLctx.colorMask(!!red,!!green,!!blue,!!alpha)};var _emscripten_glCompileShader=shader=>{GLctx.compileShader(GL.shaders[shader])};var _emscripten_glCompressedTexImage2D=(target,level,internalFormat,width,height,border,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,imageSize,data);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8,data,imageSize);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexImage3D=(target,level,internalFormat,width,height,depth,border,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,imageSize,data)}else{GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,HEAPU8,data,imageSize)}};var _emscripten_glCompressedTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,imageSize,data);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8,data,imageSize);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)}else{GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,HEAPU8,data,imageSize)}};var _emscripten_glCopyBufferSubData=(x0,x1,x2,x3,x4)=>GLctx.copyBufferSubData(x0,x1,x2,x3,x4);var _emscripten_glCopyTexImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexSubImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage3D=(x0,x1,x2,x3,x4,x5,x6,x7,x8)=>GLctx.copyTexSubImage3D(x0,x1,x2,x3,x4,x5,x6,x7,x8);var _emscripten_glCreateProgram=()=>{var id=GL.getNewId(GL.programs);var program=GLctx.createProgram();program.name=id;program.maxUniformLength=program.maxAttributeLength=program.maxUniformBlockNameLength=0;program.uniformIdCounter=1;GL.programs[id]=program;return id};var _emscripten_glCreateShader=shaderType=>{var id=GL.getNewId(GL.shaders);GL.shaders[id]=GLctx.createShader(shaderType);return id};var _emscripten_glCullFace=x0=>GLctx.cullFace(x0);var _emscripten_glDeleteBuffers=(n,buffers)=>{for(var i=0;i>2];var buffer=GL.buffers[id];if(!buffer)continue;GLctx.deleteBuffer(buffer);buffer.name=0;GL.buffers[id]=null;if(id==GLctx.currentPixelPackBufferBinding)GLctx.currentPixelPackBufferBinding=0;if(id==GLctx.currentPixelUnpackBufferBinding)GLctx.currentPixelUnpackBufferBinding=0}};var _emscripten_glDeleteFramebuffers=(n,framebuffers)=>{for(var i=0;i>2];var framebuffer=GL.framebuffers[id];if(!framebuffer)continue;GLctx.deleteFramebuffer(framebuffer);framebuffer.name=0;GL.framebuffers[id]=null}};var _emscripten_glDeleteProgram=id=>{if(!id)return;var program=GL.programs[id];if(!program){GL.recordError(1281);return}GLctx.deleteProgram(program);program.name=0;GL.programs[id]=null};var _emscripten_glDeleteQueries=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.deleteQuery(query);GL.queries[id]=null}};var _emscripten_glDeleteQueriesEXT=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.disjointTimerQueryExt["deleteQueryEXT"](query);GL.queries[id]=null}};var _emscripten_glDeleteRenderbuffers=(n,renderbuffers)=>{for(var i=0;i>2];var renderbuffer=GL.renderbuffers[id];if(!renderbuffer)continue;GLctx.deleteRenderbuffer(renderbuffer);renderbuffer.name=0;GL.renderbuffers[id]=null}};var _emscripten_glDeleteSamplers=(n,samplers)=>{for(var i=0;i>2];var sampler=GL.samplers[id];if(!sampler)continue;GLctx.deleteSampler(sampler);sampler.name=0;GL.samplers[id]=null}};var _emscripten_glDeleteShader=id=>{if(!id)return;var shader=GL.shaders[id];if(!shader){GL.recordError(1281);return}GLctx.deleteShader(shader);GL.shaders[id]=null};var _emscripten_glDeleteSync=id=>{if(!id)return;var sync=GL.syncs[id];if(!sync){GL.recordError(1281);return}GLctx.deleteSync(sync);sync.name=0;GL.syncs[id]=null};var _emscripten_glDeleteTextures=(n,textures)=>{for(var i=0;i>2];var texture=GL.textures[id];if(!texture)continue;GLctx.deleteTexture(texture);texture.name=0;GL.textures[id]=null}};var _emscripten_glDeleteTransformFeedbacks=(n,ids)=>{for(var i=0;i>2];var transformFeedback=GL.transformFeedbacks[id];if(!transformFeedback)continue;GLctx.deleteTransformFeedback(transformFeedback);transformFeedback.name=0;GL.transformFeedbacks[id]=null}};var _emscripten_glDeleteVertexArrays=(n,vaos)=>{for(var i=0;i>2];GLctx.deleteVertexArray(GL.vaos[id]);GL.vaos[id]=null}};var _glDeleteVertexArrays=_emscripten_glDeleteVertexArrays;var _emscripten_glDeleteVertexArraysOES=_glDeleteVertexArrays;var _emscripten_glDepthFunc=x0=>GLctx.depthFunc(x0);var _emscripten_glDepthMask=flag=>{GLctx.depthMask(!!flag)};var _emscripten_glDepthRangef=(x0,x1)=>GLctx.depthRange(x0,x1);var _emscripten_glDetachShader=(program,shader)=>{GLctx.detachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glDisable=x0=>GLctx.disable(x0);var _emscripten_glDisableVertexAttribArray=index=>{GLctx.disableVertexAttribArray(index)};var _emscripten_glDrawArrays=(mode,first,count)=>{GLctx.drawArrays(mode,first,count)};var _emscripten_glDrawArraysInstanced=(mode,first,count,primcount)=>{GLctx.drawArraysInstanced(mode,first,count,primcount)};var _glDrawArraysInstanced=_emscripten_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedANGLE=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedARB=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedBaseInstanceWEBGL=(mode,first,count,instanceCount,baseInstance)=>{GLctx.dibvbi["drawArraysInstancedBaseInstanceWEBGL"](mode,first,count,instanceCount,baseInstance)};var _emscripten_glDrawArraysInstancedEXT=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedNV=_glDrawArraysInstanced;var tempFixedLengthArray=[];var _emscripten_glDrawBuffers=(n,bufs)=>{var bufArray=tempFixedLengthArray[n];for(var i=0;i>2]}GLctx.drawBuffers(bufArray)};var _glDrawBuffers=_emscripten_glDrawBuffers;var _emscripten_glDrawBuffersEXT=_glDrawBuffers;var _emscripten_glDrawBuffersWEBGL=_glDrawBuffers;var _emscripten_glDrawElements=(mode,count,type,indices)=>{GLctx.drawElements(mode,count,type,indices)};var _emscripten_glDrawElementsInstanced=(mode,count,type,indices,primcount)=>{GLctx.drawElementsInstanced(mode,count,type,indices,primcount)};var _glDrawElementsInstanced=_emscripten_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedANGLE=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedARB=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,count,type,offset,instanceCount,baseVertex,baseinstance)=>{GLctx.dibvbi["drawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,count,type,offset,instanceCount,baseVertex,baseinstance)};var _emscripten_glDrawElementsInstancedEXT=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedNV=_glDrawElementsInstanced;var _glDrawElements=_emscripten_glDrawElements;var _emscripten_glDrawRangeElements=(mode,start,end,count,type,indices)=>{_glDrawElements(mode,count,type,indices)};var _emscripten_glEnable=x0=>GLctx.enable(x0);var _emscripten_glEnableVertexAttribArray=index=>{GLctx.enableVertexAttribArray(index)};var _emscripten_glEndQuery=x0=>GLctx.endQuery(x0);var _emscripten_glEndQueryEXT=target=>{GLctx.disjointTimerQueryExt["endQueryEXT"](target)};var _emscripten_glEndTransformFeedback=()=>GLctx.endTransformFeedback();var _emscripten_glFenceSync=(condition,flags)=>{var sync=GLctx.fenceSync(condition,flags);if(sync){var id=GL.getNewId(GL.syncs);sync.name=id;GL.syncs[id]=sync;return id}return 0};var _emscripten_glFinish=()=>GLctx.finish();var _emscripten_glFlush=()=>GLctx.flush();var _emscripten_glFramebufferRenderbuffer=(target,attachment,renderbuffertarget,renderbuffer)=>{GLctx.framebufferRenderbuffer(target,attachment,renderbuffertarget,GL.renderbuffers[renderbuffer])};var _emscripten_glFramebufferTexture2D=(target,attachment,textarget,texture,level)=>{GLctx.framebufferTexture2D(target,attachment,textarget,GL.textures[texture],level)};var _emscripten_glFramebufferTextureLayer=(target,attachment,texture,level,layer)=>{GLctx.framebufferTextureLayer(target,attachment,GL.textures[texture],level,layer)};var _emscripten_glFrontFace=x0=>GLctx.frontFace(x0);var _emscripten_glGenBuffers=(n,buffers)=>{GL.genObject(n,buffers,"createBuffer",GL.buffers)};var _emscripten_glGenFramebuffers=(n,ids)=>{GL.genObject(n,ids,"createFramebuffer",GL.framebuffers)};var _emscripten_glGenQueries=(n,ids)=>{GL.genObject(n,ids,"createQuery",GL.queries)};var _emscripten_glGenQueriesEXT=(n,ids)=>{for(var i=0;i>2]=0;return}var id=GL.getNewId(GL.queries);query.name=id;GL.queries[id]=query;HEAP32[ids+i*4>>2]=id}};var _emscripten_glGenRenderbuffers=(n,renderbuffers)=>{GL.genObject(n,renderbuffers,"createRenderbuffer",GL.renderbuffers)};var _emscripten_glGenSamplers=(n,samplers)=>{GL.genObject(n,samplers,"createSampler",GL.samplers)};var _emscripten_glGenTextures=(n,textures)=>{GL.genObject(n,textures,"createTexture",GL.textures)};var _emscripten_glGenTransformFeedbacks=(n,ids)=>{GL.genObject(n,ids,"createTransformFeedback",GL.transformFeedbacks)};var _emscripten_glGenVertexArrays=(n,arrays)=>{GL.genObject(n,arrays,"createVertexArray",GL.vaos)};var _glGenVertexArrays=_emscripten_glGenVertexArrays;var _emscripten_glGenVertexArraysOES=_glGenVertexArrays;var _emscripten_glGenerateMipmap=x0=>GLctx.generateMipmap(x0);var __glGetActiveAttribOrUniform=(funcName,program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx[funcName](program,index);if(info){var numBytesWrittenExclNull=name&&stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull;if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type}};var _emscripten_glGetActiveAttrib=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveAttrib",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniform=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveUniform",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniformBlockName=(program,uniformBlockIndex,bufSize,length,uniformBlockName)=>{program=GL.programs[program];var result=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);if(!result)return;if(uniformBlockName&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(result,uniformBlockName,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}};var _emscripten_glGetActiveUniformBlockiv=(program,uniformBlockIndex,pname,params)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];if(pname==35393){var name=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);HEAP32[params>>2]=name.length+1;return}var result=GLctx.getActiveUniformBlockParameter(program,uniformBlockIndex,pname);if(result===null)return;if(pname==35395){for(var i=0;i>2]=result[i]}}else{HEAP32[params>>2]=result}};var _emscripten_glGetActiveUniformsiv=(program,uniformCount,uniformIndices,pname,params)=>{if(!params){GL.recordError(1281);return}if(uniformCount>0&&uniformIndices==0){GL.recordError(1281);return}program=GL.programs[program];var ids=[];for(var i=0;i>2])}var result=GLctx.getActiveUniforms(program,ids,pname);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var _emscripten_glGetAttachedShaders=(program,maxCount,count,shaders)=>{var result=GLctx.getAttachedShaders(GL.programs[program]);var len=result.length;if(len>maxCount){len=maxCount}HEAP32[count>>2]=len;for(var i=0;i>2]=id}};var _emscripten_glGetAttribLocation=(program,name)=>GLctx.getAttribLocation(GL.programs[program],UTF8ToString(name));var writeI53ToI64=(ptr,num)=>{HEAPU32[ptr>>2]=num;var lower=HEAPU32[ptr>>2];HEAPU32[ptr+4>>2]=(num-lower)/4294967296};var webglGetExtensions=()=>{var exts=getEmscriptenSupportedExtensions(GLctx);exts=exts.concat(exts.map(e=>"GL_"+e));return exts};var emscriptenWebGLGet=(name_,p,type)=>{if(!p){GL.recordError(1281);return}var ret=undefined;switch(name_){case 36346:ret=1;break;case 36344:if(type!=0&&type!=1){GL.recordError(1280)}return;case 34814:case 36345:ret=0;break;case 34466:var formats=GLctx.getParameter(34467);ret=formats?formats.length:0;break;case 33309:if(GL.currentContext.version<2){GL.recordError(1282);return}ret=webglGetExtensions().length;break;case 33307:case 33308:if(GL.currentContext.version<2){GL.recordError(1280);return}ret=name_==33307?3:0;break}if(ret===undefined){var result=GLctx.getParameter(name_);switch(typeof result){case"number":ret=result;break;case"boolean":ret=result?1:0;break;case"string":GL.recordError(1280);return;case"object":if(result===null){switch(name_){case 34964:case 35725:case 34965:case 36006:case 36007:case 32873:case 34229:case 36662:case 36663:case 35053:case 35055:case 36010:case 35097:case 35869:case 32874:case 36389:case 35983:case 35368:case 34068:{ret=0;break}default:{GL.recordError(1280);return}}}else if(result instanceof Float32Array||result instanceof Uint32Array||result instanceof Int32Array||result instanceof Array){for(var i=0;i>2]=result[i];break;case 2:HEAPF32[p+i*4>>2]=result[i];break;case 4:HEAP8[p+i]=result[i]?1:0;break}}return}else{try{ret=result.name|0}catch(e){GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`);return}}break;default:GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof result}!`);return}}switch(type){case 1:writeI53ToI64(p,ret);break;case 0:HEAP32[p>>2]=ret;break;case 2:HEAPF32[p>>2]=ret;break;case 4:HEAP8[p]=ret?1:0;break}};var _emscripten_glGetBooleanv=(name_,p)=>emscriptenWebGLGet(name_,p,4);var _emscripten_glGetBufferParameteri64v=(target,value,data)=>{if(!data){GL.recordError(1281);return}writeI53ToI64(data,GLctx.getBufferParameter(target,value))};var _emscripten_glGetBufferParameteriv=(target,value,data)=>{if(!data){GL.recordError(1281);return}HEAP32[data>>2]=GLctx.getBufferParameter(target,value)};var _emscripten_glGetError=()=>{var error=GLctx.getError()||GL.lastError;GL.lastError=0;return error};var _emscripten_glGetFloatv=(name_,p)=>emscriptenWebGLGet(name_,p,2);var _emscripten_glGetFragDataLocation=(program,name)=>GLctx.getFragDataLocation(GL.programs[program],UTF8ToString(name));var _emscripten_glGetFramebufferAttachmentParameteriv=(target,attachment,pname,params)=>{var result=GLctx.getFramebufferAttachmentParameter(target,attachment,pname);if(result instanceof WebGLRenderbuffer||result instanceof WebGLTexture){result=result.name|0}HEAP32[params>>2]=result};var emscriptenWebGLGetIndexed=(target,index,data,type)=>{if(!data){GL.recordError(1281);return}var result=GLctx.getIndexedParameter(target,index);var ret;switch(typeof result){case"boolean":ret=result?1:0;break;case"number":ret=result;break;case"object":if(result===null){switch(target){case 35983:case 35368:ret=0;break;default:{GL.recordError(1280);return}}}else if(result instanceof WebGLBuffer){ret=result.name|0}else{GL.recordError(1280);return}break;default:GL.recordError(1280);return}switch(type){case 1:writeI53ToI64(data,ret);break;case 0:HEAP32[data>>2]=ret;break;case 2:HEAPF32[data>>2]=ret;break;case 4:HEAP8[data]=ret?1:0;break;default:abort("internal emscriptenWebGLGetIndexed() error, bad type: "+type)}};var _emscripten_glGetInteger64i_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,1);var _emscripten_glGetInteger64v=(name_,p)=>{emscriptenWebGLGet(name_,p,1)};var _emscripten_glGetIntegeri_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,0);var _emscripten_glGetIntegerv=(name_,p)=>emscriptenWebGLGet(name_,p,0);var _emscripten_glGetInternalformativ=(target,internalformat,pname,bufSize,params)=>{if(bufSize<0){GL.recordError(1281);return}if(!params){GL.recordError(1281);return}var ret=GLctx.getInternalformatParameter(target,internalformat,pname);if(ret===null)return;for(var i=0;i>2]=ret[i]}};var _emscripten_glGetProgramBinary=(program,bufSize,length,binaryFormat,binary)=>{GL.recordError(1282)};var _emscripten_glGetProgramInfoLog=(program,maxLength,length,infoLog)=>{var log=GLctx.getProgramInfoLog(GL.programs[program]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetProgramiv=(program,pname,p)=>{if(!p){GL.recordError(1281);return}if(program>=GL.counter){GL.recordError(1281);return}program=GL.programs[program];if(pname==35716){var log=GLctx.getProgramInfoLog(program);if(log===null)log="(unknown error)";HEAP32[p>>2]=log.length+1}else if(pname==35719){if(!program.maxUniformLength){var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(var i=0;i>2]=program.maxUniformLength}else if(pname==35722){if(!program.maxAttributeLength){var numActiveAttributes=GLctx.getProgramParameter(program,35721);for(var i=0;i>2]=program.maxAttributeLength}else if(pname==35381){if(!program.maxUniformBlockNameLength){var numActiveUniformBlocks=GLctx.getProgramParameter(program,35382);for(var i=0;i>2]=program.maxUniformBlockNameLength}else{HEAP32[p>>2]=GLctx.getProgramParameter(program,pname)}};var _emscripten_glGetQueryObjecti64vEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param;if(GL.currentContext.version<2){param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname)}else{param=GLctx.getQueryParameter(query,pname)}var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}writeI53ToI64(params,ret)};var _emscripten_glGetQueryObjectivEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjecti64vEXT=_emscripten_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectui64vEXT=_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectuiv=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.getQueryParameter(query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjectivEXT=_emscripten_glGetQueryObjectivEXT;var _emscripten_glGetQueryObjectuivEXT=_glGetQueryObjectivEXT;var _emscripten_glGetQueryiv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getQuery(target,pname)};var _emscripten_glGetQueryivEXT=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.disjointTimerQueryExt["getQueryEXT"](target,pname)};var _emscripten_glGetRenderbufferParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getRenderbufferParameter(target,pname)};var _emscripten_glGetSamplerParameterfv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetSamplerParameteriv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetShaderInfoLog=(shader,maxLength,length,infoLog)=>{var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderPrecisionFormat=(shaderType,precisionType,range,precision)=>{var result=GLctx.getShaderPrecisionFormat(shaderType,precisionType);HEAP32[range>>2]=result.rangeMin;HEAP32[range+4>>2]=result.rangeMax;HEAP32[precision>>2]=result.precision};var _emscripten_glGetShaderSource=(shader,bufSize,length,source)=>{var result=GLctx.getShaderSource(GL.shaders[shader]);if(!result)return;var numBytesWrittenExclNull=bufSize>0&&source?stringToUTF8(result,source,bufSize):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderiv=(shader,pname,p)=>{if(!p){GL.recordError(1281);return}if(pname==35716){var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var logLength=log?log.length+1:0;HEAP32[p>>2]=logLength}else if(pname==35720){var source=GLctx.getShaderSource(GL.shaders[shader]);var sourceLength=source?source.length+1:0;HEAP32[p>>2]=sourceLength}else{HEAP32[p>>2]=GLctx.getShaderParameter(GL.shaders[shader],pname)}};var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var _emscripten_glGetString=name_=>{var ret=GL.stringCache[name_];if(!ret){switch(name_){case 7939:ret=stringToNewUTF8(webglGetExtensions().join(" "));break;case 7936:case 7937:case 37445:case 37446:var s=GLctx.getParameter(name_);if(!s){GL.recordError(1280)}ret=s?stringToNewUTF8(s):0;break;case 7938:var webGLVersion=GLctx.getParameter(7938);var glVersion=`OpenGL ES 2.0 (${webGLVersion})`;if(GL.currentContext.version>=2)glVersion=`OpenGL ES 3.0 (${webGLVersion})`;ret=stringToNewUTF8(glVersion);break;case 35724:var glslVersion=GLctx.getParameter(35724);var ver_re=/^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/;var ver_num=glslVersion.match(ver_re);if(ver_num!==null){if(ver_num[1].length==3)ver_num[1]=ver_num[1]+"0";glslVersion=`OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`}ret=stringToNewUTF8(glslVersion);break;default:GL.recordError(1280)}GL.stringCache[name_]=ret}return ret};var _emscripten_glGetStringi=(name,index)=>{if(GL.currentContext.version<2){GL.recordError(1282);return 0}var stringiCache=GL.stringiCache[name];if(stringiCache){if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index]}switch(name){case 7939:var exts=webglGetExtensions().map(stringToNewUTF8);stringiCache=GL.stringiCache[name]=exts;if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index];default:GL.recordError(1280);return 0}};var _emscripten_glGetSynciv=(sync,pname,bufSize,length,values)=>{if(bufSize<0){GL.recordError(1281);return}if(!values){GL.recordError(1281);return}var ret=GLctx.getSyncParameter(GL.syncs[sync],pname);if(ret!==null){HEAP32[values>>2]=ret;if(length)HEAP32[length>>2]=1}};var _emscripten_glGetTexParameterfv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTexParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTransformFeedbackVarying=(program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx.getTransformFeedbackVarying(program,index);if(!info)return;if(name&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type};var _emscripten_glGetUniformBlockIndex=(program,uniformBlockName)=>GLctx.getUniformBlockIndex(GL.programs[program],UTF8ToString(uniformBlockName));var _emscripten_glGetUniformIndices=(program,uniformCount,uniformNames,uniformIndices)=>{if(!uniformIndices){GL.recordError(1281);return}if(uniformCount>0&&(uniformNames==0||uniformIndices==0)){GL.recordError(1281);return}program=GL.programs[program];var names=[];for(var i=0;i>2]));var result=GLctx.getUniformIndices(program,names);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var jstoi_q=str=>parseInt(str);var webglGetLeftBracePos=name=>name.slice(-1)=="]"&&name.lastIndexOf("[");var webglPrepareUniformLocationsBeforeFirstUse=program=>{var uniformLocsById=program.uniformLocsById,uniformSizeAndIdsByName=program.uniformSizeAndIdsByName,i,j;if(!uniformLocsById){program.uniformLocsById=uniformLocsById={};program.uniformArrayNamesById={};var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(i=0;i0?nm.slice(0,lb):nm;var id=program.uniformIdCounter;program.uniformIdCounter+=sz;uniformSizeAndIdsByName[arrayName]=[sz,id];for(j=0;j{name=UTF8ToString(name);if(program=GL.programs[program]){webglPrepareUniformLocationsBeforeFirstUse(program);var uniformLocsById=program.uniformLocsById;var arrayIndex=0;var uniformBaseName=name;var leftBrace=webglGetLeftBracePos(name);if(leftBrace>0){arrayIndex=jstoi_q(name.slice(leftBrace+1))>>>0;uniformBaseName=name.slice(0,leftBrace)}var sizeAndId=program.uniformSizeAndIdsByName[uniformBaseName];if(sizeAndId&&arrayIndex{var p=GLctx.currentProgram;if(p){var webglLoc=p.uniformLocsById[location];if(typeof webglLoc=="number"){p.uniformLocsById[location]=webglLoc=GLctx.getUniformLocation(p,p.uniformArrayNamesById[location]+(webglLoc>0?`[${webglLoc}]`:""))}return webglLoc}else{GL.recordError(1282)}};var emscriptenWebGLGetUniform=(program,location,params,type)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];webglPrepareUniformLocationsBeforeFirstUse(program);var data=GLctx.getUniform(program,webglGetUniformLocation(location));if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break}}}};var _emscripten_glGetUniformfv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,2)};var _emscripten_glGetUniformiv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,0)};var _emscripten_glGetUniformuiv=(program,location,params)=>emscriptenWebGLGetUniform(program,location,params,0);var emscriptenWebGLGetVertexAttrib=(index,pname,params,type)=>{if(!params){GL.recordError(1281);return}var data=GLctx.getVertexAttrib(index,pname);if(pname==34975){HEAP32[params>>2]=data&&data["name"]}else if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break;case 5:HEAP32[params>>2]=Math.fround(data);break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break;case 5:HEAP32[params+i*4>>2]=Math.fround(data[i]);break}}}};var _emscripten_glGetVertexAttribIiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,0)};var _glGetVertexAttribIiv=_emscripten_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribIuiv=_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribPointerv=(index,pname,pointer)=>{if(!pointer){GL.recordError(1281);return}HEAP32[pointer>>2]=GLctx.getVertexAttribOffset(index,pname)};var _emscripten_glGetVertexAttribfv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,2)};var _emscripten_glGetVertexAttribiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,5)};var _emscripten_glHint=(x0,x1)=>GLctx.hint(x0,x1);var _emscripten_glInvalidateFramebuffer=(target,numAttachments,attachments)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateFramebuffer(target,list)};var _emscripten_glInvalidateSubFramebuffer=(target,numAttachments,attachments,x,y,width,height)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateSubFramebuffer(target,list,x,y,width,height)};var _emscripten_glIsBuffer=buffer=>{var b=GL.buffers[buffer];if(!b)return 0;return GLctx.isBuffer(b)};var _emscripten_glIsEnabled=x0=>GLctx.isEnabled(x0);var _emscripten_glIsFramebuffer=framebuffer=>{var fb=GL.framebuffers[framebuffer];if(!fb)return 0;return GLctx.isFramebuffer(fb)};var _emscripten_glIsProgram=program=>{program=GL.programs[program];if(!program)return 0;return GLctx.isProgram(program)};var _emscripten_glIsQuery=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.isQuery(query)};var _emscripten_glIsQueryEXT=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.disjointTimerQueryExt["isQueryEXT"](query)};var _emscripten_glIsRenderbuffer=renderbuffer=>{var rb=GL.renderbuffers[renderbuffer];if(!rb)return 0;return GLctx.isRenderbuffer(rb)};var _emscripten_glIsSampler=id=>{var sampler=GL.samplers[id];if(!sampler)return 0;return GLctx.isSampler(sampler)};var _emscripten_glIsShader=shader=>{var s=GL.shaders[shader];if(!s)return 0;return GLctx.isShader(s)};var _emscripten_glIsSync=sync=>GLctx.isSync(GL.syncs[sync]);var _emscripten_glIsTexture=id=>{var texture=GL.textures[id];if(!texture)return 0;return GLctx.isTexture(texture)};var _emscripten_glIsTransformFeedback=id=>GLctx.isTransformFeedback(GL.transformFeedbacks[id]);var _emscripten_glIsVertexArray=array=>{var vao=GL.vaos[array];if(!vao)return 0;return GLctx.isVertexArray(vao)};var _glIsVertexArray=_emscripten_glIsVertexArray;var _emscripten_glIsVertexArrayOES=_glIsVertexArray;var _emscripten_glLineWidth=x0=>GLctx.lineWidth(x0);var _emscripten_glLinkProgram=program=>{program=GL.programs[program];GLctx.linkProgram(program);program.uniformLocsById=0;program.uniformSizeAndIdsByName={}};var _emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL=(mode,firsts,counts,instanceCounts,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawArraysInstancedBaseInstanceWEBGL"](mode,HEAP32,firsts>>2,HEAP32,counts>>2,HEAP32,instanceCounts>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,counts,type,offsets,instanceCounts,baseVertices,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,HEAP32,counts>>2,type,HEAP32,offsets>>2,HEAP32,instanceCounts>>2,HEAP32,baseVertices>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glPauseTransformFeedback=()=>GLctx.pauseTransformFeedback();var _emscripten_glPixelStorei=(pname,param)=>{if(pname==3317){GL.unpackAlignment=param}else if(pname==3314){GL.unpackRowLength=param}GLctx.pixelStorei(pname,param)};var _emscripten_glPolygonModeWEBGL=(face,mode)=>{GLctx.webglPolygonMode["polygonModeWEBGL"](face,mode)};var _emscripten_glPolygonOffset=(x0,x1)=>GLctx.polygonOffset(x0,x1);var _emscripten_glPolygonOffsetClampEXT=(factor,units,clamp)=>{GLctx.extPolygonOffsetClamp["polygonOffsetClampEXT"](factor,units,clamp)};var _emscripten_glProgramBinary=(program,binaryFormat,binary,length)=>{GL.recordError(1280)};var _emscripten_glProgramParameteri=(program,pname,value)=>{GL.recordError(1280)};var _emscripten_glQueryCounterEXT=(id,target)=>{GLctx.disjointTimerQueryExt["queryCounterEXT"](GL.queries[id],target)};var _emscripten_glReadBuffer=x0=>GLctx.readBuffer(x0);var computeUnpackAlignedImageSize=(width,height,sizePerPixel)=>{function roundedToNextMultipleOf(x,y){return x+y-1&-y}var plainRowSize=(GL.unpackRowLength||width)*sizePerPixel;var alignedRowSize=roundedToNextMultipleOf(plainRowSize,GL.unpackAlignment);return height*alignedRowSize};var colorChannelsInGlTextureFormat=format=>{var colorChannels={5:3,6:4,8:2,29502:3,29504:4,26917:2,26918:2,29846:3,29847:4};return colorChannels[format-6402]||1};var heapObjectForWebGLType=type=>{type-=5120;if(type==0)return HEAP8;if(type==1)return HEAPU8;if(type==2)return HEAP16;if(type==4)return HEAP32;if(type==6)return HEAPF32;if(type==5||type==28922||type==28520||type==30779||type==30782)return HEAPU32;return HEAPU16};var toTypedArrayIndex=(pointer,heap)=>pointer>>>31-Math.clz32(heap.BYTES_PER_ELEMENT);var emscriptenWebGLGetTexPixelData=(type,format,width,height,pixels,internalFormat)=>{var heap=heapObjectForWebGLType(type);var sizePerPixel=colorChannelsInGlTextureFormat(format)*heap.BYTES_PER_ELEMENT;var bytes=computeUnpackAlignedImageSize(width,height,sizePerPixel);return heap.subarray(toTypedArrayIndex(pixels,heap),toTypedArrayIndex(pixels+bytes,heap))};var _emscripten_glReadPixels=(x,y,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelPackBufferBinding){GLctx.readPixels(x,y,width,height,format,type,pixels);return}var heap=heapObjectForWebGLType(type);var target=toTypedArrayIndex(pixels,heap);GLctx.readPixels(x,y,width,height,format,type,heap,target);return}var pixelData=emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,format);if(!pixelData){GL.recordError(1280);return}GLctx.readPixels(x,y,width,height,format,type,pixelData)};var _emscripten_glReleaseShaderCompiler=()=>{};var _emscripten_glRenderbufferStorage=(x0,x1,x2,x3)=>GLctx.renderbufferStorage(x0,x1,x2,x3);var _emscripten_glRenderbufferStorageMultisample=(x0,x1,x2,x3,x4)=>GLctx.renderbufferStorageMultisample(x0,x1,x2,x3,x4);var _emscripten_glResumeTransformFeedback=()=>GLctx.resumeTransformFeedback();var _emscripten_glSampleCoverage=(value,invert)=>{GLctx.sampleCoverage(value,!!invert)};var _emscripten_glSamplerParameterf=(sampler,pname,param)=>{GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameterfv=(sampler,pname,params)=>{var param=HEAPF32[params>>2];GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteri=(sampler,pname,param)=>{GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteriv=(sampler,pname,params)=>{var param=HEAP32[params>>2];GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glScissor=(x0,x1,x2,x3)=>GLctx.scissor(x0,x1,x2,x3);var _emscripten_glShaderBinary=(count,shaders,binaryformat,binary,length)=>{GL.recordError(1280)};var _emscripten_glShaderSource=(shader,count,string,length)=>{var source=GL.getSource(shader,count,string,length);GLctx.shaderSource(GL.shaders[shader],source)};var _emscripten_glStencilFunc=(x0,x1,x2)=>GLctx.stencilFunc(x0,x1,x2);var _emscripten_glStencilFuncSeparate=(x0,x1,x2,x3)=>GLctx.stencilFuncSeparate(x0,x1,x2,x3);var _emscripten_glStencilMask=x0=>GLctx.stencilMask(x0);var _emscripten_glStencilMaskSeparate=(x0,x1)=>GLctx.stencilMaskSeparate(x0,x1);var _emscripten_glStencilOp=(x0,x1,x2)=>GLctx.stencilOp(x0,x1,x2);var _emscripten_glStencilOpSeparate=(x0,x1,x2,x3)=>GLctx.stencilOpSeparate(x0,x1,x2,x3);var _emscripten_glTexImage2D=(target,level,internalFormat,width,height,border,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);var index=toTypedArrayIndex(pixels,heap);GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,heap,index);return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,internalFormat):null;GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixelData)};var _emscripten_glTexImage3D=(target,level,internalFormat,width,height,depth,border,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,null)}};var _emscripten_glTexParameterf=(x0,x1,x2)=>GLctx.texParameterf(x0,x1,x2);var _emscripten_glTexParameterfv=(target,pname,params)=>{var param=HEAPF32[params>>2];GLctx.texParameterf(target,pname,param)};var _emscripten_glTexParameteri=(x0,x1,x2)=>GLctx.texParameteri(x0,x1,x2);var _emscripten_glTexParameteriv=(target,pname,params)=>{var param=HEAP32[params>>2];GLctx.texParameteri(target,pname,param)};var _emscripten_glTexStorage2D=(x0,x1,x2,x3,x4)=>GLctx.texStorage2D(x0,x1,x2,x3,x4);var _emscripten_glTexStorage3D=(x0,x1,x2,x3,x4,x5)=>GLctx.texStorage3D(x0,x1,x2,x3,x4,x5);var _emscripten_glTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,heap,toTypedArrayIndex(pixels,heap));return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,0):null;GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixelData)};var _emscripten_glTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,null)}};var _emscripten_glTransformFeedbackVaryings=(program,count,varyings,bufferMode)=>{program=GL.programs[program];var vars=[];for(var i=0;i>2]));GLctx.transformFeedbackVaryings(program,vars,bufferMode)};var _emscripten_glUniform1f=(location,v0)=>{GLctx.uniform1f(webglGetUniformLocation(location),v0)};var miniTempWebGLFloatBuffers=[];var _emscripten_glUniform1fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1fv(webglGetUniformLocation(location),HEAPF32,value>>2,count);return}if(count<=288){var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1i=(location,v0)=>{GLctx.uniform1i(webglGetUniformLocation(location),v0)};var miniTempWebGLIntBuffers=[];var _emscripten_glUniform1iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1iv(webglGetUniformLocation(location),HEAP32,value>>2,count);return}if(count<=288){var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2]}}else{var view=HEAP32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1ui=(location,v0)=>{GLctx.uniform1ui(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1uiv=(location,count,value)=>{count&&GLctx.uniform1uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count)};var _emscripten_glUniform2f=(location,v0,v1)=>{GLctx.uniform2f(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2i=(location,v0,v1)=>{GLctx.uniform2i(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2iv(webglGetUniformLocation(location),HEAP32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2ui=(location,v0,v1)=>{GLctx.uniform2ui(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2uiv=(location,count,value)=>{count&&GLctx.uniform2uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*2)};var _emscripten_glUniform3f=(location,v0,v1,v2)=>{GLctx.uniform3f(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3i=(location,v0,v1,v2)=>{GLctx.uniform3i(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3iv(webglGetUniformLocation(location),HEAP32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3ui=(location,v0,v1,v2)=>{GLctx.uniform3ui(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3uiv=(location,count,value)=>{count&&GLctx.uniform3uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*3)};var _emscripten_glUniform4f=(location,v0,v1,v2,v3)=>{GLctx.uniform4f(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*4);return}if(count<=72){var view=miniTempWebGLFloatBuffers[4*count];var heap=HEAPF32;value=value>>2;count*=4;for(var i=0;i>2,value+count*16>>2)}GLctx.uniform4fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4i=(location,v0,v1,v2,v3)=>{GLctx.uniform4i(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4iv(webglGetUniformLocation(location),HEAP32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2];view[i+3]=HEAP32[value+(4*i+12)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*16>>2)}GLctx.uniform4iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4ui=(location,v0,v1,v2,v3)=>{GLctx.uniform4ui(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4uiv=(location,count,value)=>{count&&GLctx.uniform4uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*4)};var _emscripten_glUniformBlockBinding=(program,uniformBlockIndex,uniformBlockBinding)=>{program=GL.programs[program];GLctx.uniformBlockBinding(program,uniformBlockIndex,uniformBlockBinding)};var _emscripten_glUniformMatrix2fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*16>>2)}GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix2x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix2x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix3fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*9);return}if(count<=32){count*=9;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2];view[i+4]=HEAPF32[value+(4*i+16)>>2];view[i+5]=HEAPF32[value+(4*i+20)>>2];view[i+6]=HEAPF32[value+(4*i+24)>>2];view[i+7]=HEAPF32[value+(4*i+28)>>2];view[i+8]=HEAPF32[value+(4*i+32)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*36>>2)}GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix3x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix3x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUniformMatrix4fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*16);return}if(count<=18){var view=miniTempWebGLFloatBuffers[16*count];var heap=HEAPF32;value=value>>2;count*=16;for(var i=0;i>2,value+count*64>>2)}GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix4x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix4x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUseProgram=program=>{program=GL.programs[program];GLctx.useProgram(program);GLctx.currentProgram=program};var _emscripten_glValidateProgram=program=>{GLctx.validateProgram(GL.programs[program])};var _emscripten_glVertexAttrib1f=(x0,x1)=>GLctx.vertexAttrib1f(x0,x1);var _emscripten_glVertexAttrib1fv=(index,v)=>{GLctx.vertexAttrib1f(index,HEAPF32[v>>2])};var _emscripten_glVertexAttrib2f=(x0,x1,x2)=>GLctx.vertexAttrib2f(x0,x1,x2);var _emscripten_glVertexAttrib2fv=(index,v)=>{GLctx.vertexAttrib2f(index,HEAPF32[v>>2],HEAPF32[v+4>>2])};var _emscripten_glVertexAttrib3f=(x0,x1,x2,x3)=>GLctx.vertexAttrib3f(x0,x1,x2,x3);var _emscripten_glVertexAttrib3fv=(index,v)=>{GLctx.vertexAttrib3f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2])};var _emscripten_glVertexAttrib4f=(x0,x1,x2,x3,x4)=>GLctx.vertexAttrib4f(x0,x1,x2,x3,x4);var _emscripten_glVertexAttrib4fv=(index,v)=>{GLctx.vertexAttrib4f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2],HEAPF32[v+12>>2])};var _emscripten_glVertexAttribDivisor=(index,divisor)=>{GLctx.vertexAttribDivisor(index,divisor)};var _glVertexAttribDivisor=_emscripten_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorANGLE=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorARB=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorEXT=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorNV=_glVertexAttribDivisor;var _emscripten_glVertexAttribI4i=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4i(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4iv=(index,v)=>{GLctx.vertexAttribI4i(index,HEAP32[v>>2],HEAP32[v+4>>2],HEAP32[v+8>>2],HEAP32[v+12>>2])};var _emscripten_glVertexAttribI4ui=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4ui(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4uiv=(index,v)=>{GLctx.vertexAttribI4ui(index,HEAPU32[v>>2],HEAPU32[v+4>>2],HEAPU32[v+8>>2],HEAPU32[v+12>>2])};var _emscripten_glVertexAttribIPointer=(index,size,type,stride,ptr)=>{GLctx.vertexAttribIPointer(index,size,type,stride,ptr)};var _emscripten_glVertexAttribPointer=(index,size,type,normalized,stride,ptr)=>{GLctx.vertexAttribPointer(index,size,type,!!normalized,stride,ptr)};var _emscripten_glViewport=(x0,x1,x2,x3)=>GLctx.viewport(x0,x1,x2,x3);var _emscripten_glWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);GLctx.waitSync(GL.syncs[sync],flags,timeout)};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var _emscripten_request_animation_frame_loop=(cb,userData)=>{function tick(timeStamp){if(getWasmTableEntry(cb)(timeStamp,userData)){requestAnimationFrame(tick)}}return requestAnimationFrame(tick)};var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(globalThis.navigator?.language??"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var _environ_get=(__environ,environ_buf)=>{var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_read(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doReadv(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=BigInt(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _glGetIntegerv=_emscripten_glGetIntegerv;var _glGetString=_emscripten_glGetString;var _glGetStringi=_emscripten_glGetStringi;var _llvm_eh_typeid_for=type=>type;function _random_get(buffer,size){try{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};FS.createPreloadedFile=FS_createPreloadedFile;FS.preloadFile=FS_preloadFile;FS.staticInit();for(let i=0;i<32;++i)tempFixedLengthArray.push(new Array(i));var miniTempWebGLFloatBuffersStorage=new Float32Array(288);for(var i=0;i<=288;++i){miniTempWebGLFloatBuffers[i]=miniTempWebGLFloatBuffersStorage.subarray(0,i)}var miniTempWebGLIntBuffersStorage=new Int32Array(288);for(var i=0;i<=288;++i){miniTempWebGLIntBuffers[i]=miniTempWebGLIntBuffersStorage.subarray(0,i)}{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["preloadPlugins"])preloadPlugins=Module["preloadPlugins"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;Module["GL"]=GL;var _malloc,_add_font,_add_image,_add_image_with_rid,_allocate,_apply_scene_transactions,_command,_deallocate,_destroy,_devtools_rendering_set_show_fps_meter,_devtools_rendering_set_show_hit_testing,_devtools_rendering_set_show_ruler,_devtools_rendering_set_show_stats,_devtools_rendering_set_show_tiles,_drain_missing_images,_export_node_as,_get_default_fallback_fonts,_get_image_bytes,_get_image_size,_get_node_absolute_bounding_box,_get_node_id_from_point,_get_node_ids_from_envelope,_get_node_ids_from_point,_grida_fonts_analyze_family,_grida_fonts_free,_grida_fonts_parse_font,_grida_markdown_to_html,_grida_svg_optimize,_grida_svg_to_document,_has_missing_fonts,_highlight_strokes,_init,_init_with_backend,_list_available_fonts,_list_missing_fonts,_load_benchmark_scene,_load_dummy_scene,_load_scene_grida,_load_scene_grida1,_pointer_move,_redraw,_resize_surface,_resolve_image,_runtime_renderer_set_layer_compositing,_runtime_renderer_set_outline_mode,_runtime_renderer_set_pixel_preview_scale,_runtime_renderer_set_pixel_preview_stable,_runtime_renderer_set_render_policy_flags,_runtime_renderer_set_skip_layout,_set_debug,_set_default_fallback_fonts,_set_main_camera_transform,_set_surface_overlay_config,_set_verbose,_surface_get_cursor,_surface_get_hovered_node,_surface_get_selected_nodes,_surface_pointer_down,_surface_pointer_move,_surface_pointer_up,_surface_set_selection,_switch_scene,_text_edit_command,_text_edit_enter,_text_edit_exit,_text_edit_get_caret_rect,_text_edit_get_selected_html,_text_edit_get_selected_text,_text_edit_get_selection_rects,_text_edit_get_text,_text_edit_ime_cancel,_text_edit_ime_commit,_text_edit_ime_set_preedit,_text_edit_is_active,_text_edit_paste_html,_text_edit_paste_text,_text_edit_pointer_down,_text_edit_pointer_move,_text_edit_pointer_up,_text_edit_redo,_text_edit_set_color,_text_edit_set_font_family,_text_edit_set_font_size,_text_edit_tick,_text_edit_toggle_bold,_text_edit_toggle_italic,_text_edit_toggle_strikethrough,_text_edit_toggle_underline,_text_edit_undo,_tick,_to_vector_network,_toggle_debug,_main,_emscripten_builtin_memalign,_setThrew,__emscripten_tempret_set,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,___cxa_decrement_exception_refcount,___cxa_increment_exception_refcount,___cxa_can_catch,___cxa_get_exception_ptr,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){_malloc=wasmExports["Og"];_add_font=Module["_add_font"]=wasmExports["Qg"];_add_image=Module["_add_image"]=wasmExports["Rg"];_add_image_with_rid=Module["_add_image_with_rid"]=wasmExports["Sg"];_allocate=Module["_allocate"]=wasmExports["Tg"];_apply_scene_transactions=Module["_apply_scene_transactions"]=wasmExports["Ug"];_command=Module["_command"]=wasmExports["Vg"];_deallocate=Module["_deallocate"]=wasmExports["Wg"];_destroy=Module["_destroy"]=wasmExports["Xg"];_devtools_rendering_set_show_fps_meter=Module["_devtools_rendering_set_show_fps_meter"]=wasmExports["Yg"];_devtools_rendering_set_show_hit_testing=Module["_devtools_rendering_set_show_hit_testing"]=wasmExports["Zg"];_devtools_rendering_set_show_ruler=Module["_devtools_rendering_set_show_ruler"]=wasmExports["_g"];_devtools_rendering_set_show_stats=Module["_devtools_rendering_set_show_stats"]=wasmExports["$g"];_devtools_rendering_set_show_tiles=Module["_devtools_rendering_set_show_tiles"]=wasmExports["ah"];_drain_missing_images=Module["_drain_missing_images"]=wasmExports["bh"];_export_node_as=Module["_export_node_as"]=wasmExports["ch"];_get_default_fallback_fonts=Module["_get_default_fallback_fonts"]=wasmExports["dh"];_get_image_bytes=Module["_get_image_bytes"]=wasmExports["eh"];_get_image_size=Module["_get_image_size"]=wasmExports["fh"];_get_node_absolute_bounding_box=Module["_get_node_absolute_bounding_box"]=wasmExports["gh"];_get_node_id_from_point=Module["_get_node_id_from_point"]=wasmExports["hh"];_get_node_ids_from_envelope=Module["_get_node_ids_from_envelope"]=wasmExports["ih"];_get_node_ids_from_point=Module["_get_node_ids_from_point"]=wasmExports["jh"];_grida_fonts_analyze_family=Module["_grida_fonts_analyze_family"]=wasmExports["kh"];_grida_fonts_free=Module["_grida_fonts_free"]=wasmExports["lh"];_grida_fonts_parse_font=Module["_grida_fonts_parse_font"]=wasmExports["mh"];_grida_markdown_to_html=Module["_grida_markdown_to_html"]=wasmExports["nh"];_grida_svg_optimize=Module["_grida_svg_optimize"]=wasmExports["oh"];_grida_svg_to_document=Module["_grida_svg_to_document"]=wasmExports["ph"];_has_missing_fonts=Module["_has_missing_fonts"]=wasmExports["qh"];_highlight_strokes=Module["_highlight_strokes"]=wasmExports["rh"];_init=Module["_init"]=wasmExports["sh"];_init_with_backend=Module["_init_with_backend"]=wasmExports["th"];_list_available_fonts=Module["_list_available_fonts"]=wasmExports["uh"];_list_missing_fonts=Module["_list_missing_fonts"]=wasmExports["vh"];_load_benchmark_scene=Module["_load_benchmark_scene"]=wasmExports["wh"];_load_dummy_scene=Module["_load_dummy_scene"]=wasmExports["xh"];_load_scene_grida=Module["_load_scene_grida"]=wasmExports["yh"];_load_scene_grida1=Module["_load_scene_grida1"]=wasmExports["zh"];_pointer_move=Module["_pointer_move"]=wasmExports["Ah"];_redraw=Module["_redraw"]=wasmExports["Bh"];_resize_surface=Module["_resize_surface"]=wasmExports["Ch"];_resolve_image=Module["_resolve_image"]=wasmExports["Dh"];_runtime_renderer_set_layer_compositing=Module["_runtime_renderer_set_layer_compositing"]=wasmExports["Eh"];_runtime_renderer_set_outline_mode=Module["_runtime_renderer_set_outline_mode"]=wasmExports["Fh"];_runtime_renderer_set_pixel_preview_scale=Module["_runtime_renderer_set_pixel_preview_scale"]=wasmExports["Gh"];_runtime_renderer_set_pixel_preview_stable=Module["_runtime_renderer_set_pixel_preview_stable"]=wasmExports["Hh"];_runtime_renderer_set_render_policy_flags=Module["_runtime_renderer_set_render_policy_flags"]=wasmExports["Ih"];_runtime_renderer_set_skip_layout=Module["_runtime_renderer_set_skip_layout"]=wasmExports["Jh"];_set_debug=Module["_set_debug"]=wasmExports["Kh"];_set_default_fallback_fonts=Module["_set_default_fallback_fonts"]=wasmExports["Lh"];_set_main_camera_transform=Module["_set_main_camera_transform"]=wasmExports["Mh"];_set_surface_overlay_config=Module["_set_surface_overlay_config"]=wasmExports["Nh"];_set_verbose=Module["_set_verbose"]=wasmExports["Oh"];_surface_get_cursor=Module["_surface_get_cursor"]=wasmExports["Ph"];_surface_get_hovered_node=Module["_surface_get_hovered_node"]=wasmExports["Qh"];_surface_get_selected_nodes=Module["_surface_get_selected_nodes"]=wasmExports["Rh"];_surface_pointer_down=Module["_surface_pointer_down"]=wasmExports["Sh"];_surface_pointer_move=Module["_surface_pointer_move"]=wasmExports["Th"];_surface_pointer_up=Module["_surface_pointer_up"]=wasmExports["Uh"];_surface_set_selection=Module["_surface_set_selection"]=wasmExports["Vh"];_switch_scene=Module["_switch_scene"]=wasmExports["Wh"];_text_edit_command=Module["_text_edit_command"]=wasmExports["Xh"];_text_edit_enter=Module["_text_edit_enter"]=wasmExports["Yh"];_text_edit_exit=Module["_text_edit_exit"]=wasmExports["Zh"];_text_edit_get_caret_rect=Module["_text_edit_get_caret_rect"]=wasmExports["_h"];_text_edit_get_selected_html=Module["_text_edit_get_selected_html"]=wasmExports["$h"];_text_edit_get_selected_text=Module["_text_edit_get_selected_text"]=wasmExports["ai"];_text_edit_get_selection_rects=Module["_text_edit_get_selection_rects"]=wasmExports["bi"];_text_edit_get_text=Module["_text_edit_get_text"]=wasmExports["ci"];_text_edit_ime_cancel=Module["_text_edit_ime_cancel"]=wasmExports["di"];_text_edit_ime_commit=Module["_text_edit_ime_commit"]=wasmExports["ei"];_text_edit_ime_set_preedit=Module["_text_edit_ime_set_preedit"]=wasmExports["fi"];_text_edit_is_active=Module["_text_edit_is_active"]=wasmExports["gi"];_text_edit_paste_html=Module["_text_edit_paste_html"]=wasmExports["hi"];_text_edit_paste_text=Module["_text_edit_paste_text"]=wasmExports["ii"];_text_edit_pointer_down=Module["_text_edit_pointer_down"]=wasmExports["ji"];_text_edit_pointer_move=Module["_text_edit_pointer_move"]=wasmExports["ki"];_text_edit_pointer_up=Module["_text_edit_pointer_up"]=wasmExports["li"];_text_edit_redo=Module["_text_edit_redo"]=wasmExports["mi"];_text_edit_set_color=Module["_text_edit_set_color"]=wasmExports["ni"];_text_edit_set_font_family=Module["_text_edit_set_font_family"]=wasmExports["oi"];_text_edit_set_font_size=Module["_text_edit_set_font_size"]=wasmExports["pi"];_text_edit_tick=Module["_text_edit_tick"]=wasmExports["qi"];_text_edit_toggle_bold=Module["_text_edit_toggle_bold"]=wasmExports["ri"];_text_edit_toggle_italic=Module["_text_edit_toggle_italic"]=wasmExports["si"];_text_edit_toggle_strikethrough=Module["_text_edit_toggle_strikethrough"]=wasmExports["ti"];_text_edit_toggle_underline=Module["_text_edit_toggle_underline"]=wasmExports["ui"];_text_edit_undo=Module["_text_edit_undo"]=wasmExports["vi"];_tick=Module["_tick"]=wasmExports["wi"];_to_vector_network=Module["_to_vector_network"]=wasmExports["xi"];_toggle_debug=Module["_toggle_debug"]=wasmExports["yi"];_main=Module["_main"]=wasmExports["zi"];_emscripten_builtin_memalign=wasmExports["Ai"];_setThrew=wasmExports["Bi"];__emscripten_tempret_set=wasmExports["Ci"];__emscripten_stack_restore=wasmExports["Di"];__emscripten_stack_alloc=wasmExports["Ei"];_emscripten_stack_get_current=wasmExports["Fi"];___cxa_decrement_exception_refcount=wasmExports["Gi"];___cxa_increment_exception_refcount=wasmExports["Hi"];___cxa_can_catch=wasmExports["Ii"];___cxa_get_exception_ptr=wasmExports["Ji"];memory=wasmMemory=wasmExports["Mg"];__indirect_function_table=wasmTable=wasmExports["Pg"]}var wasmImports={F:___cxa_begin_catch,N:___cxa_end_catch,a:___cxa_find_matching_catch_2,o:___cxa_find_matching_catch_3,ga:___cxa_find_matching_catch_4,Ca:___cxa_rethrow,H:___cxa_throw,eb:___cxa_uncaught_exceptions,e:___resumeException,Fa:___syscall_fcntl64,vb:___syscall_fstat64,rb:___syscall_getcwd,wb:___syscall_ioctl,sb:___syscall_lstat64,tb:___syscall_newfstatat,Ga:___syscall_openat,ub:___syscall_stat64,zb:__abort_js,gb:__emscripten_throw_longjmp,mb:__gmtime_js,kb:__mmap_js,lb:__munmap_js,Ab:__tzset_js,yb:_clock_time_get,xb:_emscripten_date_now,ib:_emscripten_get_heap_max,Af:_emscripten_glActiveTexture,Bf:_emscripten_glAttachShader,de:_emscripten_glBeginQuery,Zd:_emscripten_glBeginQueryEXT,Ec:_emscripten_glBeginTransformFeedback,Cf:_emscripten_glBindAttribLocation,Df:_emscripten_glBindBuffer,Bc:_emscripten_glBindBufferBase,Cc:_emscripten_glBindBufferRange,Be:_emscripten_glBindFramebuffer,Ce:_emscripten_glBindRenderbuffer,je:_emscripten_glBindSampler,Ef:_emscripten_glBindTexture,Rb:_emscripten_glBindTransformFeedback,Xe:_emscripten_glBindVertexArray,_e:_emscripten_glBindVertexArrayOES,Ff:_emscripten_glBlendColor,Gf:_emscripten_glBlendEquation,Jd:_emscripten_glBlendEquationSeparate,Hf:_emscripten_glBlendFunc,Id:_emscripten_glBlendFuncSeparate,ve:_emscripten_glBlitFramebuffer,If:_emscripten_glBufferData,Jf:_emscripten_glBufferSubData,De:_emscripten_glCheckFramebufferStatus,Kf:_emscripten_glClear,fc:_emscripten_glClearBufferfi,gc:_emscripten_glClearBufferfv,ic:_emscripten_glClearBufferiv,hc:_emscripten_glClearBufferuiv,Lf:_emscripten_glClearColor,Hd:_emscripten_glClearDepthf,Mf:_emscripten_glClearStencil,se:_emscripten_glClientWaitSync,_c:_emscripten_glClipControlEXT,Nf:_emscripten_glColorMask,Of:_emscripten_glCompileShader,Qf:_emscripten_glCompressedTexImage2D,Rc:_emscripten_glCompressedTexImage3D,Rf:_emscripten_glCompressedTexSubImage2D,Qc:_emscripten_glCompressedTexSubImage3D,ue:_emscripten_glCopyBufferSubData,Gd:_emscripten_glCopyTexImage2D,Sf:_emscripten_glCopyTexSubImage2D,Sc:_emscripten_glCopyTexSubImage3D,Tf:_emscripten_glCreateProgram,Uf:_emscripten_glCreateShader,Vf:_emscripten_glCullFace,Wf:_emscripten_glDeleteBuffers,Ee:_emscripten_glDeleteFramebuffers,Xf:_emscripten_glDeleteProgram,ee:_emscripten_glDeleteQueries,_d:_emscripten_glDeleteQueriesEXT,Fe:_emscripten_glDeleteRenderbuffers,ke:_emscripten_glDeleteSamplers,Yf:_emscripten_glDeleteShader,te:_emscripten_glDeleteSync,Zf:_emscripten_glDeleteTextures,Qb:_emscripten_glDeleteTransformFeedbacks,Ye:_emscripten_glDeleteVertexArrays,$e:_emscripten_glDeleteVertexArraysOES,Fd:_emscripten_glDepthFunc,_f:_emscripten_glDepthMask,Ed:_emscripten_glDepthRangef,Dd:_emscripten_glDetachShader,$f:_emscripten_glDisable,ag:_emscripten_glDisableVertexAttribArray,bg:_emscripten_glDrawArrays,Ve:_emscripten_glDrawArraysInstanced,Md:_emscripten_glDrawArraysInstancedANGLE,Db:_emscripten_glDrawArraysInstancedARB,Se:_emscripten_glDrawArraysInstancedBaseInstanceWEBGL,Xc:_emscripten_glDrawArraysInstancedEXT,Eb:_emscripten_glDrawArraysInstancedNV,Qe:_emscripten_glDrawBuffers,Vc:_emscripten_glDrawBuffersEXT,Nd:_emscripten_glDrawBuffersWEBGL,cg:_emscripten_glDrawElements,We:_emscripten_glDrawElementsInstanced,Ld:_emscripten_glDrawElementsInstancedANGLE,Bb:_emscripten_glDrawElementsInstancedARB,Te:_emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Cb:_emscripten_glDrawElementsInstancedEXT,Wc:_emscripten_glDrawElementsInstancedNV,Ke:_emscripten_glDrawRangeElements,dg:_emscripten_glEnable,eg:_emscripten_glEnableVertexAttribArray,fe:_emscripten_glEndQuery,$d:_emscripten_glEndQueryEXT,Dc:_emscripten_glEndTransformFeedback,pe:_emscripten_glFenceSync,fg:_emscripten_glFinish,gg:_emscripten_glFlush,Ge:_emscripten_glFramebufferRenderbuffer,He:_emscripten_glFramebufferTexture2D,Hc:_emscripten_glFramebufferTextureLayer,hg:_emscripten_glFrontFace,ig:_emscripten_glGenBuffers,Ie:_emscripten_glGenFramebuffers,ge:_emscripten_glGenQueries,ae:_emscripten_glGenQueriesEXT,Je:_emscripten_glGenRenderbuffers,le:_emscripten_glGenSamplers,jg:_emscripten_glGenTextures,Pb:_emscripten_glGenTransformFeedbacks,Ue:_emscripten_glGenVertexArrays,af:_emscripten_glGenVertexArraysOES,xe:_emscripten_glGenerateMipmap,Cd:_emscripten_glGetActiveAttrib,Bd:_emscripten_glGetActiveUniform,ac:_emscripten_glGetActiveUniformBlockName,bc:_emscripten_glGetActiveUniformBlockiv,dc:_emscripten_glGetActiveUniformsiv,Ad:_emscripten_glGetAttachedShaders,zd:_emscripten_glGetAttribLocation,yd:_emscripten_glGetBooleanv,Xb:_emscripten_glGetBufferParameteri64v,kg:_emscripten_glGetBufferParameteriv,lg:_emscripten_glGetError,mg:_emscripten_glGetFloatv,rc:_emscripten_glGetFragDataLocation,ye:_emscripten_glGetFramebufferAttachmentParameteriv,Yb:_emscripten_glGetInteger64i_v,_b:_emscripten_glGetInteger64v,Fc:_emscripten_glGetIntegeri_v,ng:_emscripten_glGetIntegerv,Hb:_emscripten_glGetInternalformativ,Lb:_emscripten_glGetProgramBinary,og:_emscripten_glGetProgramInfoLog,pg:_emscripten_glGetProgramiv,Wd:_emscripten_glGetQueryObjecti64vEXT,Pd:_emscripten_glGetQueryObjectivEXT,Xd:_emscripten_glGetQueryObjectui64vEXT,he:_emscripten_glGetQueryObjectuiv,be:_emscripten_glGetQueryObjectuivEXT,ie:_emscripten_glGetQueryiv,ce:_emscripten_glGetQueryivEXT,ze:_emscripten_glGetRenderbufferParameteriv,Tb:_emscripten_glGetSamplerParameterfv,Ub:_emscripten_glGetSamplerParameteriv,qg:_emscripten_glGetShaderInfoLog,Td:_emscripten_glGetShaderPrecisionFormat,xd:_emscripten_glGetShaderSource,rg:_emscripten_glGetShaderiv,sg:_emscripten_glGetString,Ze:_emscripten_glGetStringi,Zb:_emscripten_glGetSynciv,wd:_emscripten_glGetTexParameterfv,vd:_emscripten_glGetTexParameteriv,zc:_emscripten_glGetTransformFeedbackVarying,cc:_emscripten_glGetUniformBlockIndex,ec:_emscripten_glGetUniformIndices,tg:_emscripten_glGetUniformLocation,ud:_emscripten_glGetUniformfv,td:_emscripten_glGetUniformiv,sc:_emscripten_glGetUniformuiv,yc:_emscripten_glGetVertexAttribIiv,xc:_emscripten_glGetVertexAttribIuiv,qd:_emscripten_glGetVertexAttribPointerv,sd:_emscripten_glGetVertexAttribfv,rd:_emscripten_glGetVertexAttribiv,pd:_emscripten_glHint,Ud:_emscripten_glInvalidateFramebuffer,Vd:_emscripten_glInvalidateSubFramebuffer,od:_emscripten_glIsBuffer,nd:_emscripten_glIsEnabled,md:_emscripten_glIsFramebuffer,ld:_emscripten_glIsProgram,Oc:_emscripten_glIsQuery,Qd:_emscripten_glIsQueryEXT,kd:_emscripten_glIsRenderbuffer,Wb:_emscripten_glIsSampler,jd:_emscripten_glIsShader,qe:_emscripten_glIsSync,ug:_emscripten_glIsTexture,Ob:_emscripten_glIsTransformFeedback,Gc:_emscripten_glIsVertexArray,Od:_emscripten_glIsVertexArrayOES,vg:_emscripten_glLineWidth,wg:_emscripten_glLinkProgram,Oe:_emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL,Pe:_emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Nb:_emscripten_glPauseTransformFeedback,xg:_emscripten_glPixelStorei,Zc:_emscripten_glPolygonModeWEBGL,id:_emscripten_glPolygonOffset,$c:_emscripten_glPolygonOffsetClampEXT,Kb:_emscripten_glProgramBinary,Jb:_emscripten_glProgramParameteri,Yd:_emscripten_glQueryCounterEXT,Re:_emscripten_glReadBuffer,yg:_emscripten_glReadPixels,hd:_emscripten_glReleaseShaderCompiler,Ae:_emscripten_glRenderbufferStorage,we:_emscripten_glRenderbufferStorageMultisample,Mb:_emscripten_glResumeTransformFeedback,gd:_emscripten_glSampleCoverage,me:_emscripten_glSamplerParameterf,Vb:_emscripten_glSamplerParameterfv,ne:_emscripten_glSamplerParameteri,oe:_emscripten_glSamplerParameteriv,zg:_emscripten_glScissor,fd:_emscripten_glShaderBinary,Ag:_emscripten_glShaderSource,Bg:_emscripten_glStencilFunc,Cg:_emscripten_glStencilFuncSeparate,Dg:_emscripten_glStencilMask,Eg:_emscripten_glStencilMaskSeparate,Fg:_emscripten_glStencilOp,Gg:_emscripten_glStencilOpSeparate,Hg:_emscripten_glTexImage2D,Uc:_emscripten_glTexImage3D,Ig:_emscripten_glTexParameterf,Jg:_emscripten_glTexParameterfv,Kg:_emscripten_glTexParameteri,Lg:_emscripten_glTexParameteriv,Le:_emscripten_glTexStorage2D,Ib:_emscripten_glTexStorage3D,Ma:_emscripten_glTexSubImage2D,Tc:_emscripten_glTexSubImage3D,Ac:_emscripten_glTransformFeedbackVaryings,Na:_emscripten_glUniform1f,Oa:_emscripten_glUniform1fv,wf:_emscripten_glUniform1i,xf:_emscripten_glUniform1iv,qc:_emscripten_glUniform1ui,mc:_emscripten_glUniform1uiv,yf:_emscripten_glUniform2f,zf:_emscripten_glUniform2fv,vf:_emscripten_glUniform2i,uf:_emscripten_glUniform2iv,pc:_emscripten_glUniform2ui,lc:_emscripten_glUniform2uiv,tf:_emscripten_glUniform3f,sf:_emscripten_glUniform3fv,rf:_emscripten_glUniform3i,qf:_emscripten_glUniform3iv,oc:_emscripten_glUniform3ui,kc:_emscripten_glUniform3uiv,pf:_emscripten_glUniform4f,of:_emscripten_glUniform4fv,bf:_emscripten_glUniform4i,cf:_emscripten_glUniform4iv,nc:_emscripten_glUniform4ui,jc:_emscripten_glUniform4uiv,$b:_emscripten_glUniformBlockBinding,df:_emscripten_glUniformMatrix2fv,Nc:_emscripten_glUniformMatrix2x3fv,Lc:_emscripten_glUniformMatrix2x4fv,ef:_emscripten_glUniformMatrix3fv,Mc:_emscripten_glUniformMatrix3x2fv,Jc:_emscripten_glUniformMatrix3x4fv,ff:_emscripten_glUniformMatrix4fv,Kc:_emscripten_glUniformMatrix4x2fv,Ic:_emscripten_glUniformMatrix4x3fv,gf:_emscripten_glUseProgram,ed:_emscripten_glValidateProgram,hf:_emscripten_glVertexAttrib1f,dd:_emscripten_glVertexAttrib1fv,cd:_emscripten_glVertexAttrib2f,jf:_emscripten_glVertexAttrib2fv,bd:_emscripten_glVertexAttrib3f,kf:_emscripten_glVertexAttrib3fv,ad:_emscripten_glVertexAttrib4f,lf:_emscripten_glVertexAttrib4fv,Me:_emscripten_glVertexAttribDivisor,Kd:_emscripten_glVertexAttribDivisorANGLE,Fb:_emscripten_glVertexAttribDivisorARB,Yc:_emscripten_glVertexAttribDivisorEXT,Gb:_emscripten_glVertexAttribDivisorNV,wc:_emscripten_glVertexAttribI4i,uc:_emscripten_glVertexAttribI4iv,vc:_emscripten_glVertexAttribI4ui,tc:_emscripten_glVertexAttribI4uiv,Ne:_emscripten_glVertexAttribIPointer,mf:_emscripten_glVertexAttribPointer,nf:_emscripten_glViewport,re:_emscripten_glWaitSync,Za:_emscripten_request_animation_frame_loop,hb:_emscripten_resize_heap,ob:_environ_get,pb:_environ_sizes_get,Ra:_exit,la:_fd_close,jb:_fd_pread,Ea:_fd_read,nb:_fd_seek,ka:_fd_write,Pa:_glGetIntegerv,pa:_glGetString,Qa:_glGetStringi,Rd:invoke_dd,Sd:invoke_dddd,Aa:invoke_diii,Ua:invoke_fdiiii,Ta:invoke_fdiiiii,Sa:invoke_fii,Ba:invoke_fiii,s:invoke_fiiidi,V:invoke_fiiif,t:invoke_fiiiidi,r:invoke_i,j:invoke_ii,G:invoke_iif,$a:invoke_iiffi,ra:invoke_iiffiii,f:invoke_iii,Ja:invoke_iiiffii,ta:invoke_iiifi,g:invoke_iiii,T:invoke_iiiiff,l:invoke_iiiii,db:invoke_iiiiid,A:invoke_iiiiii,y:invoke_iiiiiii,E:invoke_iiiiiiii,q:invoke_iiiiiiiii,qa:invoke_iiiiiiiiii,ca:invoke_iiiiiiiiiiii,oa:invoke_iiiiiiiiiiiifiii,R:invoke_iij,fb:invoke_j,ha:invoke_ji,_:invoke_jiii,da:invoke_jiiii,J:invoke_jjji,k:invoke_v,Pf:invoke_vff,b:invoke_vi,P:invoke_vid,S:invoke_vif,u:invoke_viff,D:invoke_viffff,Z:invoke_vifffff,Va:invoke_viffffff,C:invoke_viffi,ia:invoke_viffiiiiiii,c:invoke_vii,Ya:invoke_viidii,O:invoke_viif,v:invoke_viiff,$:invoke_viifi,va:invoke_viififii,x:invoke_viifiiifi,d:invoke_viii,I:invoke_viiif,xa:invoke_viiiff,B:invoke_viiiffi,K:invoke_viiiffiffii,L:invoke_viiififiiiiiiiiiiii,i:invoke_viiii,Xa:invoke_viiiidididii,ja:invoke_viiiif,ya:invoke_viiiiff,Da:invoke_viiiiffi,wa:invoke_viiiifi,h:invoke_viiiii,ab:invoke_viiiiif,Ia:invoke_viiiiiff,Wa:invoke_viiiiiffiii,cb:invoke_viiiiifi,m:invoke_viiiiii,p:invoke_viiiiiii,W:invoke_viiiiiiii,Y:invoke_viiiiiiiii,M:invoke_viiiiiiiiii,sa:invoke_viiiiiiiiiii,ba:invoke_viiiiiiiiiiiiiii,La:invoke_viiiiiji,Sb:invoke_viiiijjiiiiff,Q:invoke_viiij,z:invoke_viiijii,X:invoke_viij,n:invoke_viiji,ma:invoke_viijiffi,fa:invoke_viijii,bb:invoke_viijiii,aa:invoke_viijiiiif,Ka:invoke_viijiiiii,ua:invoke_viijj,U:invoke_viji,w:invoke_vijii,Ha:invoke_vijiifi,_a:invoke_vijiififi,za:invoke_vijiii,ea:invoke_vijjjj,Pc:invoke_vjii,na:_llvm_eh_typeid_for,qb:_random_get};function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ji(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiij(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vff(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viij(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiji(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiji(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vid(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viji(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiif(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiiifiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vif(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vjii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiffii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiffi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiijjiiiiff(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiffi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiifi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiififiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffiffii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiif(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffi(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viififii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiififi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iij(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijj(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jjji(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiifi(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viif(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viff(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifffff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiidi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viidii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiidididii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiidi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiffiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiijii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffffff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiif(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dddd(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dd(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijjjj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_j(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiid(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_fiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function callMain(args=[]){var entryFunction=_main;args.unshift(thisProgram);var argc=args.length;var argv=stackAlloc((argc+1)*4);var argv_ptr=argv;for(var arg of args){HEAPU32[argv_ptr>>2]=stringToUTF8OnStack(arg);argv_ptr+=4}HEAPU32[argv_ptr>>2]=0;try{var ret=entryFunction(argc,argv);exitJS(ret,true);return ret}catch(e){return handleException(e)}}function run(args=arguments_){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();var noInitialRun=Module["noInitialRun"]||false;if(!noInitialRun)callMain(args);postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} +var createGridaCanvas=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else{}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.initialized)FS.init();TTY.init();wasmExports["Og"]();FS.ignorePermissions=false}function preMain(){}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("grida_canvas_wasm.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var exceptionCaught=[];var uncaughtExceptionCount=0;var ___cxa_begin_catch=ptr=>{var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);return ___cxa_get_exception_ptr(ptr)};var exceptionLast=0;var ___cxa_end_catch=()=>{_setThrew(0,0);var info=exceptionCaught.pop();___cxa_decrement_exception_refcount(info.excPtr);exceptionLast=0};class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var setTempRet0=val=>__emscripten_tempret_set(val);var findMatchingCatch=args=>{var thrown=exceptionLast;if(!thrown){setTempRet0(0);return 0}var info=new ExceptionInfo(thrown);info.set_adjusted_ptr(thrown);var thrownType=info.get_type();if(!thrownType){setTempRet0(0);return thrown}for(var caughtType of args){if(caughtType===0||caughtType===thrownType){break}var adjusted_ptr_addr=info.ptr+16;if(___cxa_can_catch(caughtType,thrownType,adjusted_ptr_addr)){setTempRet0(caughtType);return thrown}}setTempRet0(thrownType);return thrown};var ___cxa_find_matching_catch_2=()=>findMatchingCatch([]);var ___cxa_find_matching_catch_3=arg0=>findMatchingCatch([arg0]);var ___cxa_find_matching_catch_4=(arg0,arg1)=>findMatchingCatch([arg0,arg1]);var ___cxa_rethrow=()=>{var info=exceptionCaught.pop();if(!info){abort("no exception to throw")}var ptr=info.excPtr;if(!info.get_rethrown()){exceptionCaught.push(info);info.set_rethrown(true);info.set_caught(false);uncaughtExceptionCount++}___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;throw exceptionLast};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var ___cxa_uncaught_exceptions=()=>uncaughtExceptionCount;var ___resumeException=ptr=>{if(!exceptionLast){exceptionLast=ptr}throw exceptionLast};var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\/]+|\/)\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(ENVIRONMENT_IS_NODE){var nodeCrypto=require("node:crypto");return view=>nodeCrypto.randomFillSync(view)}return view=>crypto.getRandomValues(view)};var randomFill=view=>{(randomFill=initRandomFill())(view)};var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(globalThis.window?.prompt){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var zeroMemory=(ptr,size)=>HEAPU8.fill(0,ptr,ptr+size);var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(ptr)zeroMemory(ptr,size);return ptr};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of["mode","atime","mtime","ctime"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){if(!MEMFS.doesNotExistError){MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack=""}throw MEMFS.doesNotExistError},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[".","..",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var asyncLoad=async url=>{var arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(...args)=>FS.createDataFile(...args);var getUniqueRunDependency=id=>id;var runDependencies=0;var dependenciesFulfilled=null;var removeRunDependency=id=>{runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}};var addRunDependency=id=>{runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)};var preloadPlugins=[];var FS_handledByPreloadPlugin=async(byteArray,fullname)=>{if(typeof Browser!="undefined")Browser.init();for(var plugin of preloadPlugins){if(plugin["canHandle"](fullname)){return plugin["handle"](byteArray,fullname)}}return byteArray};var FS_preloadFile=async(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);addRunDependency(dep);try{var byteArray=url;if(typeof url=="string"){byteArray=await asyncLoad(url)}byteArray=await FS_handledByPreloadPlugin(byteArray,fullname);preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}}finally{removeRunDependency(dep)}};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{FS_preloadFile(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish).then(onload).catch(onerror)};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,readFiles:{},ErrnoError:class{name="ErrnoError";constructor(errno){this.errno=errno}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+"/"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&(512|64)){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);setattr(arg,attr)},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}for(var mount of mounts){if(mount.type.syncfs){mount.type.syncfs(mount,populate,done)}else{done(null)}}},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);for(var[hash,current]of Object.entries(FS.nameTable)){while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}}node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name==="."||name===".."){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+="/";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS_modeStringToFlags(flags):flags;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path=="object"){node=path}else{isDirPath=path.endsWith("/");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}if(Module["logReadFiles"]&&!(flags&1)){if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){abort(`Invalid encoding type "${opts.encoding}"`)}var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){buf=UTF8ArrayToString(buf)}FS.close(stream);return buf},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){data=new Uint8Array(intArrayFromString(data,true))}if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{abort("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)abort("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)abort("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")abort("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(globalThis.XMLHttpRequest){if(!ENVIRONMENT_IS_WORKER)abort("Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc");var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};for(const[key,fn]of Object.entries(node.stream_ops)){stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}}function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var SYSCALLS={calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+"/"+path},writeStat(buf,stat){HEAPU32[buf>>2]=stat.dev;HEAPU32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAPU32[buf+12>>2]=stat.uid;HEAPU32[buf+16>>2]=stat.gid;HEAPU32[buf+20>>2]=stat.rdev;HEAP64[buf+24>>3]=BigInt(stat.size);HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAPU32[buf+4>>2]=stats.bsize;HEAPU32[buf+60>>2]=stats.bsize;HEAP64[buf+8>>3]=BigInt(stats.blocks);HEAP64[buf+16>>3]=BigInt(stats.bfree);HEAP64[buf+24>>3]=BigInt(stats.bavail);HEAP64[buf+32>>3]=BigInt(stats.files);HEAP64[buf+40>>3]=BigInt(stats.ffree);HEAPU32[buf+48>>2]=stats.fsid;HEAPU32[buf+64>>2]=stats.flags;HEAPU32[buf+56>>2]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{return SYSCALLS.writeStat(buf,FS.fstat(fd))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);function ___syscall_getcwd(buf,size){try{if(size===0)return-28;var cwd=FS.cwd();var cwdLengthInBytes=lengthBytesUTF8(cwd)+1;if(size>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21537:case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.lstat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.writeStat(buf,nofollow?FS.lstat(path):FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>abort("");var __emscripten_throw_longjmp=()=>{throw Infinity};var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function __gmtime_js(time,tmPtr){time=bigintToI53Checked(time);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getUTCSeconds();HEAP32[tmPtr+4>>2]=date.getUTCMinutes();HEAP32[tmPtr+8>>2]=date.getUTCHours();HEAP32[tmPtr+12>>2]=date.getUTCDate();HEAP32[tmPtr+16>>2]=date.getUTCMonth();HEAP32[tmPtr+20>>2]=date.getUTCFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getUTCDay();var start=Date.UTC(date.getUTCFullYear(),0,1,0,0,0,0);var yday=(date.getTime()-start)/(1e3*60*60*24)|0;HEAP32[tmPtr+28>>2]=yday}function __mmap_js(len,prot,flags,fd,offset,allocated,addr){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;HEAPU32[addr>>2]=ptr;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffsetperformance.now();var _emscripten_date_now=()=>Date.now();var nowIsMonotonic=1;var checkWasiClock=clock_id=>clock_id>=0&&clock_id<=3;function _clock_time_get(clk_id,ignored_precision,ptime){ignored_precision=bigintToI53Checked(ignored_precision);if(!checkWasiClock(clk_id)){return 28}var now;if(clk_id===0){now=_emscripten_date_now()}else if(nowIsMonotonic){now=_emscripten_get_now()}else{return 52}var nsec=Math.round(now*1e3*1e3);HEAP64[ptime>>3]=BigInt(nsec);return 0}var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var GLctx;var webgl_enable_ANGLE_instanced_arrays=ctx=>{var ext=ctx.getExtension("ANGLE_instanced_arrays");if(ext){ctx["vertexAttribDivisor"]=(index,divisor)=>ext["vertexAttribDivisorANGLE"](index,divisor);ctx["drawArraysInstanced"]=(mode,first,count,primcount)=>ext["drawArraysInstancedANGLE"](mode,first,count,primcount);ctx["drawElementsInstanced"]=(mode,count,type,indices,primcount)=>ext["drawElementsInstancedANGLE"](mode,count,type,indices,primcount);return 1}};var webgl_enable_OES_vertex_array_object=ctx=>{var ext=ctx.getExtension("OES_vertex_array_object");if(ext){ctx["createVertexArray"]=()=>ext["createVertexArrayOES"]();ctx["deleteVertexArray"]=vao=>ext["deleteVertexArrayOES"](vao);ctx["bindVertexArray"]=vao=>ext["bindVertexArrayOES"](vao);ctx["isVertexArray"]=vao=>ext["isVertexArrayOES"](vao);return 1}};var webgl_enable_WEBGL_draw_buffers=ctx=>{var ext=ctx.getExtension("WEBGL_draw_buffers");if(ext){ctx["drawBuffers"]=(n,bufs)=>ext["drawBuffersWEBGL"](n,bufs);return 1}};var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.dibvbi=ctx.getExtension("WEBGL_draw_instanced_base_vertex_base_instance"));var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.mdibvbi=ctx.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance"));var webgl_enable_EXT_polygon_offset_clamp=ctx=>!!(ctx.extPolygonOffsetClamp=ctx.getExtension("EXT_polygon_offset_clamp"));var webgl_enable_EXT_clip_control=ctx=>!!(ctx.extClipControl=ctx.getExtension("EXT_clip_control"));var webgl_enable_WEBGL_polygon_mode=ctx=>!!(ctx.webglPolygonMode=ctx.getExtension("WEBGL_polygon_mode"));var webgl_enable_WEBGL_multi_draw=ctx=>!!(ctx.multiDrawWebgl=ctx.getExtension("WEBGL_multi_draw"));var getEmscriptenSupportedExtensions=ctx=>{var supportedExtensions=["ANGLE_instanced_arrays","EXT_blend_minmax","EXT_disjoint_timer_query","EXT_frag_depth","EXT_shader_texture_lod","EXT_sRGB","OES_element_index_uint","OES_fbo_render_mipmap","OES_standard_derivatives","OES_texture_float","OES_texture_half_float","OES_texture_half_float_linear","OES_vertex_array_object","WEBGL_color_buffer_float","WEBGL_depth_texture","WEBGL_draw_buffers","EXT_color_buffer_float","EXT_conservative_depth","EXT_disjoint_timer_query_webgl2","EXT_texture_norm16","NV_shader_noperspective_interpolation","WEBGL_clip_cull_distance","EXT_clip_control","EXT_color_buffer_half_float","EXT_depth_clamp","EXT_float_blend","EXT_polygon_offset_clamp","EXT_texture_compression_bptc","EXT_texture_compression_rgtc","EXT_texture_filter_anisotropic","KHR_parallel_shader_compile","OES_texture_float_linear","WEBGL_blend_func_extended","WEBGL_compressed_texture_astc","WEBGL_compressed_texture_etc","WEBGL_compressed_texture_etc1","WEBGL_compressed_texture_s3tc","WEBGL_compressed_texture_s3tc_srgb","WEBGL_debug_renderer_info","WEBGL_debug_shaders","WEBGL_lose_context","WEBGL_multi_draw","WEBGL_polygon_mode"];return(ctx.getSupportedExtensions()||[]).filter(ext=>supportedExtensions.includes(ext))};var GL={counter:1,buffers:[],programs:[],framebuffers:[],renderbuffers:[],textures:[],shaders:[],vaos:[],contexts:[],offscreenCanvases:{},queries:[],samplers:[],transformFeedbacks:[],syncs:[],stringCache:{},stringiCache:{},unpackAlignment:4,unpackRowLength:0,recordError:errorCode=>{if(!GL.lastError){GL.lastError=errorCode}},getNewId:table=>{var ret=GL.counter++;for(var i=table.length;i{for(var i=0;i>2]=id}},getSource:(shader,count,string,length)=>{var source="";for(var i=0;i>2]:undefined;source+=UTF8ToString(HEAPU32[string+i*4>>2],len)}return source},createContext:(canvas,webGLContextAttributes)=>{if(!canvas.getContextSafariWebGL2Fixed){canvas.getContextSafariWebGL2Fixed=canvas.getContext;function fixedGetContext(ver,attrs){var gl=canvas.getContextSafariWebGL2Fixed(ver,attrs);return ver=="webgl"==gl instanceof WebGLRenderingContext?gl:null}canvas.getContext=fixedGetContext}var ctx=webGLContextAttributes.majorVersion>1?canvas.getContext("webgl2",webGLContextAttributes):canvas.getContext("webgl",webGLContextAttributes);if(!ctx)return 0;var handle=GL.registerContext(ctx,webGLContextAttributes);return handle},registerContext:(ctx,webGLContextAttributes)=>{var handle=GL.getNewId(GL.contexts);var context={handle,attributes:webGLContextAttributes,version:webGLContextAttributes.majorVersion,GLctx:ctx};if(ctx.canvas)ctx.canvas.GLctxObject=context;GL.contexts[handle]=context;if(typeof webGLContextAttributes.enableExtensionsByDefault=="undefined"||webGLContextAttributes.enableExtensionsByDefault){GL.initExtensions(context)}return handle},makeContextCurrent:contextHandle=>{GL.currentContext=GL.contexts[contextHandle];Module["ctx"]=GLctx=GL.currentContext?.GLctx;return!(contextHandle&&!GLctx)},getContext:contextHandle=>GL.contexts[contextHandle],deleteContext:contextHandle=>{if(GL.currentContext===GL.contexts[contextHandle]){GL.currentContext=null}if(typeof JSEvents=="object"){JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas)}if(GL.contexts[contextHandle]?.GLctx.canvas){GL.contexts[contextHandle].GLctx.canvas.GLctxObject=undefined}GL.contexts[contextHandle]=null},initExtensions:context=>{context||=GL.currentContext;if(context.initExtensionsDone)return;context.initExtensionsDone=true;var GLctx=context.GLctx;webgl_enable_WEBGL_multi_draw(GLctx);webgl_enable_EXT_polygon_offset_clamp(GLctx);webgl_enable_EXT_clip_control(GLctx);webgl_enable_WEBGL_polygon_mode(GLctx);webgl_enable_ANGLE_instanced_arrays(GLctx);webgl_enable_OES_vertex_array_object(GLctx);webgl_enable_WEBGL_draw_buffers(GLctx);webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);if(context.version>=2){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query_webgl2")}if(context.version<2||!GLctx.disjointTimerQueryExt){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query")}for(var ext of getEmscriptenSupportedExtensions(GLctx)){if(!ext.includes("lose_context")&&!ext.includes("debug")){GLctx.getExtension(ext)}}}};var _emscripten_glActiveTexture=x0=>GLctx.activeTexture(x0);var _emscripten_glAttachShader=(program,shader)=>{GLctx.attachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glBeginQuery=(target,id)=>{GLctx.beginQuery(target,GL.queries[id])};var _emscripten_glBeginQueryEXT=(target,id)=>{GLctx.disjointTimerQueryExt["beginQueryEXT"](target,GL.queries[id])};var _emscripten_glBeginTransformFeedback=x0=>GLctx.beginTransformFeedback(x0);var _emscripten_glBindAttribLocation=(program,index,name)=>{GLctx.bindAttribLocation(GL.programs[program],index,UTF8ToString(name))};var _emscripten_glBindBuffer=(target,buffer)=>{if(target==35051){GLctx.currentPixelPackBufferBinding=buffer}else if(target==35052){GLctx.currentPixelUnpackBufferBinding=buffer}GLctx.bindBuffer(target,GL.buffers[buffer])};var _emscripten_glBindBufferBase=(target,index,buffer)=>{GLctx.bindBufferBase(target,index,GL.buffers[buffer])};var _emscripten_glBindBufferRange=(target,index,buffer,offset,ptrsize)=>{GLctx.bindBufferRange(target,index,GL.buffers[buffer],offset,ptrsize)};var _emscripten_glBindFramebuffer=(target,framebuffer)=>{GLctx.bindFramebuffer(target,GL.framebuffers[framebuffer])};var _emscripten_glBindRenderbuffer=(target,renderbuffer)=>{GLctx.bindRenderbuffer(target,GL.renderbuffers[renderbuffer])};var _emscripten_glBindSampler=(unit,sampler)=>{GLctx.bindSampler(unit,GL.samplers[sampler])};var _emscripten_glBindTexture=(target,texture)=>{GLctx.bindTexture(target,GL.textures[texture])};var _emscripten_glBindTransformFeedback=(target,id)=>{GLctx.bindTransformFeedback(target,GL.transformFeedbacks[id])};var _emscripten_glBindVertexArray=vao=>{GLctx.bindVertexArray(GL.vaos[vao])};var _glBindVertexArray=_emscripten_glBindVertexArray;var _emscripten_glBindVertexArrayOES=_glBindVertexArray;var _emscripten_glBlendColor=(x0,x1,x2,x3)=>GLctx.blendColor(x0,x1,x2,x3);var _emscripten_glBlendEquation=x0=>GLctx.blendEquation(x0);var _emscripten_glBlendEquationSeparate=(x0,x1)=>GLctx.blendEquationSeparate(x0,x1);var _emscripten_glBlendFunc=(x0,x1)=>GLctx.blendFunc(x0,x1);var _emscripten_glBlendFuncSeparate=(x0,x1,x2,x3)=>GLctx.blendFuncSeparate(x0,x1,x2,x3);var _emscripten_glBlitFramebuffer=(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9)=>GLctx.blitFramebuffer(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9);var _emscripten_glBufferData=(target,size,data,usage)=>{if(GL.currentContext.version>=2){if(data&&size){GLctx.bufferData(target,HEAPU8,usage,data,size)}else{GLctx.bufferData(target,size,usage)}return}GLctx.bufferData(target,data?HEAPU8.subarray(data,data+size):size,usage)};var _emscripten_glBufferSubData=(target,offset,size,data)=>{if(GL.currentContext.version>=2){size&&GLctx.bufferSubData(target,offset,HEAPU8,data,size);return}GLctx.bufferSubData(target,offset,HEAPU8.subarray(data,data+size))};var _emscripten_glCheckFramebufferStatus=x0=>GLctx.checkFramebufferStatus(x0);var _emscripten_glClear=x0=>GLctx.clear(x0);var _emscripten_glClearBufferfi=(x0,x1,x2,x3)=>GLctx.clearBufferfi(x0,x1,x2,x3);var _emscripten_glClearBufferfv=(buffer,drawbuffer,value)=>{GLctx.clearBufferfv(buffer,drawbuffer,HEAPF32,value>>2)};var _emscripten_glClearBufferiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferiv(buffer,drawbuffer,HEAP32,value>>2)};var _emscripten_glClearBufferuiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferuiv(buffer,drawbuffer,HEAPU32,value>>2)};var _emscripten_glClearColor=(x0,x1,x2,x3)=>GLctx.clearColor(x0,x1,x2,x3);var _emscripten_glClearDepthf=x0=>GLctx.clearDepth(x0);var _emscripten_glClearStencil=x0=>GLctx.clearStencil(x0);var _emscripten_glClientWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);return GLctx.clientWaitSync(GL.syncs[sync],flags,timeout)};var _emscripten_glClipControlEXT=(origin,depth)=>{GLctx.extClipControl["clipControlEXT"](origin,depth)};var _emscripten_glColorMask=(red,green,blue,alpha)=>{GLctx.colorMask(!!red,!!green,!!blue,!!alpha)};var _emscripten_glCompileShader=shader=>{GLctx.compileShader(GL.shaders[shader])};var _emscripten_glCompressedTexImage2D=(target,level,internalFormat,width,height,border,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,imageSize,data);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8,data,imageSize);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexImage3D=(target,level,internalFormat,width,height,depth,border,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,imageSize,data)}else{GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,HEAPU8,data,imageSize)}};var _emscripten_glCompressedTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,imageSize,data);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8,data,imageSize);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)}else{GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,HEAPU8,data,imageSize)}};var _emscripten_glCopyBufferSubData=(x0,x1,x2,x3,x4)=>GLctx.copyBufferSubData(x0,x1,x2,x3,x4);var _emscripten_glCopyTexImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexSubImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage3D=(x0,x1,x2,x3,x4,x5,x6,x7,x8)=>GLctx.copyTexSubImage3D(x0,x1,x2,x3,x4,x5,x6,x7,x8);var _emscripten_glCreateProgram=()=>{var id=GL.getNewId(GL.programs);var program=GLctx.createProgram();program.name=id;program.maxUniformLength=program.maxAttributeLength=program.maxUniformBlockNameLength=0;program.uniformIdCounter=1;GL.programs[id]=program;return id};var _emscripten_glCreateShader=shaderType=>{var id=GL.getNewId(GL.shaders);GL.shaders[id]=GLctx.createShader(shaderType);return id};var _emscripten_glCullFace=x0=>GLctx.cullFace(x0);var _emscripten_glDeleteBuffers=(n,buffers)=>{for(var i=0;i>2];var buffer=GL.buffers[id];if(!buffer)continue;GLctx.deleteBuffer(buffer);buffer.name=0;GL.buffers[id]=null;if(id==GLctx.currentPixelPackBufferBinding)GLctx.currentPixelPackBufferBinding=0;if(id==GLctx.currentPixelUnpackBufferBinding)GLctx.currentPixelUnpackBufferBinding=0}};var _emscripten_glDeleteFramebuffers=(n,framebuffers)=>{for(var i=0;i>2];var framebuffer=GL.framebuffers[id];if(!framebuffer)continue;GLctx.deleteFramebuffer(framebuffer);framebuffer.name=0;GL.framebuffers[id]=null}};var _emscripten_glDeleteProgram=id=>{if(!id)return;var program=GL.programs[id];if(!program){GL.recordError(1281);return}GLctx.deleteProgram(program);program.name=0;GL.programs[id]=null};var _emscripten_glDeleteQueries=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.deleteQuery(query);GL.queries[id]=null}};var _emscripten_glDeleteQueriesEXT=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.disjointTimerQueryExt["deleteQueryEXT"](query);GL.queries[id]=null}};var _emscripten_glDeleteRenderbuffers=(n,renderbuffers)=>{for(var i=0;i>2];var renderbuffer=GL.renderbuffers[id];if(!renderbuffer)continue;GLctx.deleteRenderbuffer(renderbuffer);renderbuffer.name=0;GL.renderbuffers[id]=null}};var _emscripten_glDeleteSamplers=(n,samplers)=>{for(var i=0;i>2];var sampler=GL.samplers[id];if(!sampler)continue;GLctx.deleteSampler(sampler);sampler.name=0;GL.samplers[id]=null}};var _emscripten_glDeleteShader=id=>{if(!id)return;var shader=GL.shaders[id];if(!shader){GL.recordError(1281);return}GLctx.deleteShader(shader);GL.shaders[id]=null};var _emscripten_glDeleteSync=id=>{if(!id)return;var sync=GL.syncs[id];if(!sync){GL.recordError(1281);return}GLctx.deleteSync(sync);sync.name=0;GL.syncs[id]=null};var _emscripten_glDeleteTextures=(n,textures)=>{for(var i=0;i>2];var texture=GL.textures[id];if(!texture)continue;GLctx.deleteTexture(texture);texture.name=0;GL.textures[id]=null}};var _emscripten_glDeleteTransformFeedbacks=(n,ids)=>{for(var i=0;i>2];var transformFeedback=GL.transformFeedbacks[id];if(!transformFeedback)continue;GLctx.deleteTransformFeedback(transformFeedback);transformFeedback.name=0;GL.transformFeedbacks[id]=null}};var _emscripten_glDeleteVertexArrays=(n,vaos)=>{for(var i=0;i>2];GLctx.deleteVertexArray(GL.vaos[id]);GL.vaos[id]=null}};var _glDeleteVertexArrays=_emscripten_glDeleteVertexArrays;var _emscripten_glDeleteVertexArraysOES=_glDeleteVertexArrays;var _emscripten_glDepthFunc=x0=>GLctx.depthFunc(x0);var _emscripten_glDepthMask=flag=>{GLctx.depthMask(!!flag)};var _emscripten_glDepthRangef=(x0,x1)=>GLctx.depthRange(x0,x1);var _emscripten_glDetachShader=(program,shader)=>{GLctx.detachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glDisable=x0=>GLctx.disable(x0);var _emscripten_glDisableVertexAttribArray=index=>{GLctx.disableVertexAttribArray(index)};var _emscripten_glDrawArrays=(mode,first,count)=>{GLctx.drawArrays(mode,first,count)};var _emscripten_glDrawArraysInstanced=(mode,first,count,primcount)=>{GLctx.drawArraysInstanced(mode,first,count,primcount)};var _glDrawArraysInstanced=_emscripten_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedANGLE=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedARB=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedBaseInstanceWEBGL=(mode,first,count,instanceCount,baseInstance)=>{GLctx.dibvbi["drawArraysInstancedBaseInstanceWEBGL"](mode,first,count,instanceCount,baseInstance)};var _emscripten_glDrawArraysInstancedEXT=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedNV=_glDrawArraysInstanced;var tempFixedLengthArray=[];var _emscripten_glDrawBuffers=(n,bufs)=>{var bufArray=tempFixedLengthArray[n];for(var i=0;i>2]}GLctx.drawBuffers(bufArray)};var _glDrawBuffers=_emscripten_glDrawBuffers;var _emscripten_glDrawBuffersEXT=_glDrawBuffers;var _emscripten_glDrawBuffersWEBGL=_glDrawBuffers;var _emscripten_glDrawElements=(mode,count,type,indices)=>{GLctx.drawElements(mode,count,type,indices)};var _emscripten_glDrawElementsInstanced=(mode,count,type,indices,primcount)=>{GLctx.drawElementsInstanced(mode,count,type,indices,primcount)};var _glDrawElementsInstanced=_emscripten_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedANGLE=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedARB=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,count,type,offset,instanceCount,baseVertex,baseinstance)=>{GLctx.dibvbi["drawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,count,type,offset,instanceCount,baseVertex,baseinstance)};var _emscripten_glDrawElementsInstancedEXT=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedNV=_glDrawElementsInstanced;var _glDrawElements=_emscripten_glDrawElements;var _emscripten_glDrawRangeElements=(mode,start,end,count,type,indices)=>{_glDrawElements(mode,count,type,indices)};var _emscripten_glEnable=x0=>GLctx.enable(x0);var _emscripten_glEnableVertexAttribArray=index=>{GLctx.enableVertexAttribArray(index)};var _emscripten_glEndQuery=x0=>GLctx.endQuery(x0);var _emscripten_glEndQueryEXT=target=>{GLctx.disjointTimerQueryExt["endQueryEXT"](target)};var _emscripten_glEndTransformFeedback=()=>GLctx.endTransformFeedback();var _emscripten_glFenceSync=(condition,flags)=>{var sync=GLctx.fenceSync(condition,flags);if(sync){var id=GL.getNewId(GL.syncs);sync.name=id;GL.syncs[id]=sync;return id}return 0};var _emscripten_glFinish=()=>GLctx.finish();var _emscripten_glFlush=()=>GLctx.flush();var _emscripten_glFramebufferRenderbuffer=(target,attachment,renderbuffertarget,renderbuffer)=>{GLctx.framebufferRenderbuffer(target,attachment,renderbuffertarget,GL.renderbuffers[renderbuffer])};var _emscripten_glFramebufferTexture2D=(target,attachment,textarget,texture,level)=>{GLctx.framebufferTexture2D(target,attachment,textarget,GL.textures[texture],level)};var _emscripten_glFramebufferTextureLayer=(target,attachment,texture,level,layer)=>{GLctx.framebufferTextureLayer(target,attachment,GL.textures[texture],level,layer)};var _emscripten_glFrontFace=x0=>GLctx.frontFace(x0);var _emscripten_glGenBuffers=(n,buffers)=>{GL.genObject(n,buffers,"createBuffer",GL.buffers)};var _emscripten_glGenFramebuffers=(n,ids)=>{GL.genObject(n,ids,"createFramebuffer",GL.framebuffers)};var _emscripten_glGenQueries=(n,ids)=>{GL.genObject(n,ids,"createQuery",GL.queries)};var _emscripten_glGenQueriesEXT=(n,ids)=>{for(var i=0;i>2]=0;return}var id=GL.getNewId(GL.queries);query.name=id;GL.queries[id]=query;HEAP32[ids+i*4>>2]=id}};var _emscripten_glGenRenderbuffers=(n,renderbuffers)=>{GL.genObject(n,renderbuffers,"createRenderbuffer",GL.renderbuffers)};var _emscripten_glGenSamplers=(n,samplers)=>{GL.genObject(n,samplers,"createSampler",GL.samplers)};var _emscripten_glGenTextures=(n,textures)=>{GL.genObject(n,textures,"createTexture",GL.textures)};var _emscripten_glGenTransformFeedbacks=(n,ids)=>{GL.genObject(n,ids,"createTransformFeedback",GL.transformFeedbacks)};var _emscripten_glGenVertexArrays=(n,arrays)=>{GL.genObject(n,arrays,"createVertexArray",GL.vaos)};var _glGenVertexArrays=_emscripten_glGenVertexArrays;var _emscripten_glGenVertexArraysOES=_glGenVertexArrays;var _emscripten_glGenerateMipmap=x0=>GLctx.generateMipmap(x0);var __glGetActiveAttribOrUniform=(funcName,program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx[funcName](program,index);if(info){var numBytesWrittenExclNull=name&&stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull;if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type}};var _emscripten_glGetActiveAttrib=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveAttrib",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniform=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveUniform",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniformBlockName=(program,uniformBlockIndex,bufSize,length,uniformBlockName)=>{program=GL.programs[program];var result=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);if(!result)return;if(uniformBlockName&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(result,uniformBlockName,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}};var _emscripten_glGetActiveUniformBlockiv=(program,uniformBlockIndex,pname,params)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];if(pname==35393){var name=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);HEAP32[params>>2]=name.length+1;return}var result=GLctx.getActiveUniformBlockParameter(program,uniformBlockIndex,pname);if(result===null)return;if(pname==35395){for(var i=0;i>2]=result[i]}}else{HEAP32[params>>2]=result}};var _emscripten_glGetActiveUniformsiv=(program,uniformCount,uniformIndices,pname,params)=>{if(!params){GL.recordError(1281);return}if(uniformCount>0&&uniformIndices==0){GL.recordError(1281);return}program=GL.programs[program];var ids=[];for(var i=0;i>2])}var result=GLctx.getActiveUniforms(program,ids,pname);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var _emscripten_glGetAttachedShaders=(program,maxCount,count,shaders)=>{var result=GLctx.getAttachedShaders(GL.programs[program]);var len=result.length;if(len>maxCount){len=maxCount}HEAP32[count>>2]=len;for(var i=0;i>2]=id}};var _emscripten_glGetAttribLocation=(program,name)=>GLctx.getAttribLocation(GL.programs[program],UTF8ToString(name));var writeI53ToI64=(ptr,num)=>{HEAPU32[ptr>>2]=num;var lower=HEAPU32[ptr>>2];HEAPU32[ptr+4>>2]=(num-lower)/4294967296};var webglGetExtensions=()=>{var exts=getEmscriptenSupportedExtensions(GLctx);exts=exts.concat(exts.map(e=>"GL_"+e));return exts};var emscriptenWebGLGet=(name_,p,type)=>{if(!p){GL.recordError(1281);return}var ret=undefined;switch(name_){case 36346:ret=1;break;case 36344:if(type!=0&&type!=1){GL.recordError(1280)}return;case 34814:case 36345:ret=0;break;case 34466:var formats=GLctx.getParameter(34467);ret=formats?formats.length:0;break;case 33309:if(GL.currentContext.version<2){GL.recordError(1282);return}ret=webglGetExtensions().length;break;case 33307:case 33308:if(GL.currentContext.version<2){GL.recordError(1280);return}ret=name_==33307?3:0;break}if(ret===undefined){var result=GLctx.getParameter(name_);switch(typeof result){case"number":ret=result;break;case"boolean":ret=result?1:0;break;case"string":GL.recordError(1280);return;case"object":if(result===null){switch(name_){case 34964:case 35725:case 34965:case 36006:case 36007:case 32873:case 34229:case 36662:case 36663:case 35053:case 35055:case 36010:case 35097:case 35869:case 32874:case 36389:case 35983:case 35368:case 34068:{ret=0;break}default:{GL.recordError(1280);return}}}else if(result instanceof Float32Array||result instanceof Uint32Array||result instanceof Int32Array||result instanceof Array){for(var i=0;i>2]=result[i];break;case 2:HEAPF32[p+i*4>>2]=result[i];break;case 4:HEAP8[p+i]=result[i]?1:0;break}}return}else{try{ret=result.name|0}catch(e){GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`);return}}break;default:GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof result}!`);return}}switch(type){case 1:writeI53ToI64(p,ret);break;case 0:HEAP32[p>>2]=ret;break;case 2:HEAPF32[p>>2]=ret;break;case 4:HEAP8[p]=ret?1:0;break}};var _emscripten_glGetBooleanv=(name_,p)=>emscriptenWebGLGet(name_,p,4);var _emscripten_glGetBufferParameteri64v=(target,value,data)=>{if(!data){GL.recordError(1281);return}writeI53ToI64(data,GLctx.getBufferParameter(target,value))};var _emscripten_glGetBufferParameteriv=(target,value,data)=>{if(!data){GL.recordError(1281);return}HEAP32[data>>2]=GLctx.getBufferParameter(target,value)};var _emscripten_glGetError=()=>{var error=GLctx.getError()||GL.lastError;GL.lastError=0;return error};var _emscripten_glGetFloatv=(name_,p)=>emscriptenWebGLGet(name_,p,2);var _emscripten_glGetFragDataLocation=(program,name)=>GLctx.getFragDataLocation(GL.programs[program],UTF8ToString(name));var _emscripten_glGetFramebufferAttachmentParameteriv=(target,attachment,pname,params)=>{var result=GLctx.getFramebufferAttachmentParameter(target,attachment,pname);if(result instanceof WebGLRenderbuffer||result instanceof WebGLTexture){result=result.name|0}HEAP32[params>>2]=result};var emscriptenWebGLGetIndexed=(target,index,data,type)=>{if(!data){GL.recordError(1281);return}var result=GLctx.getIndexedParameter(target,index);var ret;switch(typeof result){case"boolean":ret=result?1:0;break;case"number":ret=result;break;case"object":if(result===null){switch(target){case 35983:case 35368:ret=0;break;default:{GL.recordError(1280);return}}}else if(result instanceof WebGLBuffer){ret=result.name|0}else{GL.recordError(1280);return}break;default:GL.recordError(1280);return}switch(type){case 1:writeI53ToI64(data,ret);break;case 0:HEAP32[data>>2]=ret;break;case 2:HEAPF32[data>>2]=ret;break;case 4:HEAP8[data]=ret?1:0;break;default:abort("internal emscriptenWebGLGetIndexed() error, bad type: "+type)}};var _emscripten_glGetInteger64i_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,1);var _emscripten_glGetInteger64v=(name_,p)=>{emscriptenWebGLGet(name_,p,1)};var _emscripten_glGetIntegeri_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,0);var _emscripten_glGetIntegerv=(name_,p)=>emscriptenWebGLGet(name_,p,0);var _emscripten_glGetInternalformativ=(target,internalformat,pname,bufSize,params)=>{if(bufSize<0){GL.recordError(1281);return}if(!params){GL.recordError(1281);return}var ret=GLctx.getInternalformatParameter(target,internalformat,pname);if(ret===null)return;for(var i=0;i>2]=ret[i]}};var _emscripten_glGetProgramBinary=(program,bufSize,length,binaryFormat,binary)=>{GL.recordError(1282)};var _emscripten_glGetProgramInfoLog=(program,maxLength,length,infoLog)=>{var log=GLctx.getProgramInfoLog(GL.programs[program]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetProgramiv=(program,pname,p)=>{if(!p){GL.recordError(1281);return}if(program>=GL.counter){GL.recordError(1281);return}program=GL.programs[program];if(pname==35716){var log=GLctx.getProgramInfoLog(program);if(log===null)log="(unknown error)";HEAP32[p>>2]=log.length+1}else if(pname==35719){if(!program.maxUniformLength){var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(var i=0;i>2]=program.maxUniformLength}else if(pname==35722){if(!program.maxAttributeLength){var numActiveAttributes=GLctx.getProgramParameter(program,35721);for(var i=0;i>2]=program.maxAttributeLength}else if(pname==35381){if(!program.maxUniformBlockNameLength){var numActiveUniformBlocks=GLctx.getProgramParameter(program,35382);for(var i=0;i>2]=program.maxUniformBlockNameLength}else{HEAP32[p>>2]=GLctx.getProgramParameter(program,pname)}};var _emscripten_glGetQueryObjecti64vEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param;if(GL.currentContext.version<2){param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname)}else{param=GLctx.getQueryParameter(query,pname)}var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}writeI53ToI64(params,ret)};var _emscripten_glGetQueryObjectivEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjecti64vEXT=_emscripten_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectui64vEXT=_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectuiv=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.getQueryParameter(query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjectivEXT=_emscripten_glGetQueryObjectivEXT;var _emscripten_glGetQueryObjectuivEXT=_glGetQueryObjectivEXT;var _emscripten_glGetQueryiv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getQuery(target,pname)};var _emscripten_glGetQueryivEXT=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.disjointTimerQueryExt["getQueryEXT"](target,pname)};var _emscripten_glGetRenderbufferParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getRenderbufferParameter(target,pname)};var _emscripten_glGetSamplerParameterfv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetSamplerParameteriv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetShaderInfoLog=(shader,maxLength,length,infoLog)=>{var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderPrecisionFormat=(shaderType,precisionType,range,precision)=>{var result=GLctx.getShaderPrecisionFormat(shaderType,precisionType);HEAP32[range>>2]=result.rangeMin;HEAP32[range+4>>2]=result.rangeMax;HEAP32[precision>>2]=result.precision};var _emscripten_glGetShaderSource=(shader,bufSize,length,source)=>{var result=GLctx.getShaderSource(GL.shaders[shader]);if(!result)return;var numBytesWrittenExclNull=bufSize>0&&source?stringToUTF8(result,source,bufSize):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderiv=(shader,pname,p)=>{if(!p){GL.recordError(1281);return}if(pname==35716){var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var logLength=log?log.length+1:0;HEAP32[p>>2]=logLength}else if(pname==35720){var source=GLctx.getShaderSource(GL.shaders[shader]);var sourceLength=source?source.length+1:0;HEAP32[p>>2]=sourceLength}else{HEAP32[p>>2]=GLctx.getShaderParameter(GL.shaders[shader],pname)}};var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var _emscripten_glGetString=name_=>{var ret=GL.stringCache[name_];if(!ret){switch(name_){case 7939:ret=stringToNewUTF8(webglGetExtensions().join(" "));break;case 7936:case 7937:case 37445:case 37446:var s=GLctx.getParameter(name_);if(!s){GL.recordError(1280)}ret=s?stringToNewUTF8(s):0;break;case 7938:var webGLVersion=GLctx.getParameter(7938);var glVersion=`OpenGL ES 2.0 (${webGLVersion})`;if(GL.currentContext.version>=2)glVersion=`OpenGL ES 3.0 (${webGLVersion})`;ret=stringToNewUTF8(glVersion);break;case 35724:var glslVersion=GLctx.getParameter(35724);var ver_re=/^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/;var ver_num=glslVersion.match(ver_re);if(ver_num!==null){if(ver_num[1].length==3)ver_num[1]=ver_num[1]+"0";glslVersion=`OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`}ret=stringToNewUTF8(glslVersion);break;default:GL.recordError(1280)}GL.stringCache[name_]=ret}return ret};var _emscripten_glGetStringi=(name,index)=>{if(GL.currentContext.version<2){GL.recordError(1282);return 0}var stringiCache=GL.stringiCache[name];if(stringiCache){if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index]}switch(name){case 7939:var exts=webglGetExtensions().map(stringToNewUTF8);stringiCache=GL.stringiCache[name]=exts;if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index];default:GL.recordError(1280);return 0}};var _emscripten_glGetSynciv=(sync,pname,bufSize,length,values)=>{if(bufSize<0){GL.recordError(1281);return}if(!values){GL.recordError(1281);return}var ret=GLctx.getSyncParameter(GL.syncs[sync],pname);if(ret!==null){HEAP32[values>>2]=ret;if(length)HEAP32[length>>2]=1}};var _emscripten_glGetTexParameterfv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTexParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTransformFeedbackVarying=(program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx.getTransformFeedbackVarying(program,index);if(!info)return;if(name&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type};var _emscripten_glGetUniformBlockIndex=(program,uniformBlockName)=>GLctx.getUniformBlockIndex(GL.programs[program],UTF8ToString(uniformBlockName));var _emscripten_glGetUniformIndices=(program,uniformCount,uniformNames,uniformIndices)=>{if(!uniformIndices){GL.recordError(1281);return}if(uniformCount>0&&(uniformNames==0||uniformIndices==0)){GL.recordError(1281);return}program=GL.programs[program];var names=[];for(var i=0;i>2]));var result=GLctx.getUniformIndices(program,names);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var jstoi_q=str=>parseInt(str);var webglGetLeftBracePos=name=>name.slice(-1)=="]"&&name.lastIndexOf("[");var webglPrepareUniformLocationsBeforeFirstUse=program=>{var uniformLocsById=program.uniformLocsById,uniformSizeAndIdsByName=program.uniformSizeAndIdsByName,i,j;if(!uniformLocsById){program.uniformLocsById=uniformLocsById={};program.uniformArrayNamesById={};var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(i=0;i0?nm.slice(0,lb):nm;var id=program.uniformIdCounter;program.uniformIdCounter+=sz;uniformSizeAndIdsByName[arrayName]=[sz,id];for(j=0;j{name=UTF8ToString(name);if(program=GL.programs[program]){webglPrepareUniformLocationsBeforeFirstUse(program);var uniformLocsById=program.uniformLocsById;var arrayIndex=0;var uniformBaseName=name;var leftBrace=webglGetLeftBracePos(name);if(leftBrace>0){arrayIndex=jstoi_q(name.slice(leftBrace+1))>>>0;uniformBaseName=name.slice(0,leftBrace)}var sizeAndId=program.uniformSizeAndIdsByName[uniformBaseName];if(sizeAndId&&arrayIndex{var p=GLctx.currentProgram;if(p){var webglLoc=p.uniformLocsById[location];if(typeof webglLoc=="number"){p.uniformLocsById[location]=webglLoc=GLctx.getUniformLocation(p,p.uniformArrayNamesById[location]+(webglLoc>0?`[${webglLoc}]`:""))}return webglLoc}else{GL.recordError(1282)}};var emscriptenWebGLGetUniform=(program,location,params,type)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];webglPrepareUniformLocationsBeforeFirstUse(program);var data=GLctx.getUniform(program,webglGetUniformLocation(location));if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break}}}};var _emscripten_glGetUniformfv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,2)};var _emscripten_glGetUniformiv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,0)};var _emscripten_glGetUniformuiv=(program,location,params)=>emscriptenWebGLGetUniform(program,location,params,0);var emscriptenWebGLGetVertexAttrib=(index,pname,params,type)=>{if(!params){GL.recordError(1281);return}var data=GLctx.getVertexAttrib(index,pname);if(pname==34975){HEAP32[params>>2]=data&&data["name"]}else if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break;case 5:HEAP32[params>>2]=Math.fround(data);break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break;case 5:HEAP32[params+i*4>>2]=Math.fround(data[i]);break}}}};var _emscripten_glGetVertexAttribIiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,0)};var _glGetVertexAttribIiv=_emscripten_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribIuiv=_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribPointerv=(index,pname,pointer)=>{if(!pointer){GL.recordError(1281);return}HEAP32[pointer>>2]=GLctx.getVertexAttribOffset(index,pname)};var _emscripten_glGetVertexAttribfv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,2)};var _emscripten_glGetVertexAttribiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,5)};var _emscripten_glHint=(x0,x1)=>GLctx.hint(x0,x1);var _emscripten_glInvalidateFramebuffer=(target,numAttachments,attachments)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateFramebuffer(target,list)};var _emscripten_glInvalidateSubFramebuffer=(target,numAttachments,attachments,x,y,width,height)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateSubFramebuffer(target,list,x,y,width,height)};var _emscripten_glIsBuffer=buffer=>{var b=GL.buffers[buffer];if(!b)return 0;return GLctx.isBuffer(b)};var _emscripten_glIsEnabled=x0=>GLctx.isEnabled(x0);var _emscripten_glIsFramebuffer=framebuffer=>{var fb=GL.framebuffers[framebuffer];if(!fb)return 0;return GLctx.isFramebuffer(fb)};var _emscripten_glIsProgram=program=>{program=GL.programs[program];if(!program)return 0;return GLctx.isProgram(program)};var _emscripten_glIsQuery=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.isQuery(query)};var _emscripten_glIsQueryEXT=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.disjointTimerQueryExt["isQueryEXT"](query)};var _emscripten_glIsRenderbuffer=renderbuffer=>{var rb=GL.renderbuffers[renderbuffer];if(!rb)return 0;return GLctx.isRenderbuffer(rb)};var _emscripten_glIsSampler=id=>{var sampler=GL.samplers[id];if(!sampler)return 0;return GLctx.isSampler(sampler)};var _emscripten_glIsShader=shader=>{var s=GL.shaders[shader];if(!s)return 0;return GLctx.isShader(s)};var _emscripten_glIsSync=sync=>GLctx.isSync(GL.syncs[sync]);var _emscripten_glIsTexture=id=>{var texture=GL.textures[id];if(!texture)return 0;return GLctx.isTexture(texture)};var _emscripten_glIsTransformFeedback=id=>GLctx.isTransformFeedback(GL.transformFeedbacks[id]);var _emscripten_glIsVertexArray=array=>{var vao=GL.vaos[array];if(!vao)return 0;return GLctx.isVertexArray(vao)};var _glIsVertexArray=_emscripten_glIsVertexArray;var _emscripten_glIsVertexArrayOES=_glIsVertexArray;var _emscripten_glLineWidth=x0=>GLctx.lineWidth(x0);var _emscripten_glLinkProgram=program=>{program=GL.programs[program];GLctx.linkProgram(program);program.uniformLocsById=0;program.uniformSizeAndIdsByName={}};var _emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL=(mode,firsts,counts,instanceCounts,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawArraysInstancedBaseInstanceWEBGL"](mode,HEAP32,firsts>>2,HEAP32,counts>>2,HEAP32,instanceCounts>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,counts,type,offsets,instanceCounts,baseVertices,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,HEAP32,counts>>2,type,HEAP32,offsets>>2,HEAP32,instanceCounts>>2,HEAP32,baseVertices>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glPauseTransformFeedback=()=>GLctx.pauseTransformFeedback();var _emscripten_glPixelStorei=(pname,param)=>{if(pname==3317){GL.unpackAlignment=param}else if(pname==3314){GL.unpackRowLength=param}GLctx.pixelStorei(pname,param)};var _emscripten_glPolygonModeWEBGL=(face,mode)=>{GLctx.webglPolygonMode["polygonModeWEBGL"](face,mode)};var _emscripten_glPolygonOffset=(x0,x1)=>GLctx.polygonOffset(x0,x1);var _emscripten_glPolygonOffsetClampEXT=(factor,units,clamp)=>{GLctx.extPolygonOffsetClamp["polygonOffsetClampEXT"](factor,units,clamp)};var _emscripten_glProgramBinary=(program,binaryFormat,binary,length)=>{GL.recordError(1280)};var _emscripten_glProgramParameteri=(program,pname,value)=>{GL.recordError(1280)};var _emscripten_glQueryCounterEXT=(id,target)=>{GLctx.disjointTimerQueryExt["queryCounterEXT"](GL.queries[id],target)};var _emscripten_glReadBuffer=x0=>GLctx.readBuffer(x0);var computeUnpackAlignedImageSize=(width,height,sizePerPixel)=>{function roundedToNextMultipleOf(x,y){return x+y-1&-y}var plainRowSize=(GL.unpackRowLength||width)*sizePerPixel;var alignedRowSize=roundedToNextMultipleOf(plainRowSize,GL.unpackAlignment);return height*alignedRowSize};var colorChannelsInGlTextureFormat=format=>{var colorChannels={5:3,6:4,8:2,29502:3,29504:4,26917:2,26918:2,29846:3,29847:4};return colorChannels[format-6402]||1};var heapObjectForWebGLType=type=>{type-=5120;if(type==0)return HEAP8;if(type==1)return HEAPU8;if(type==2)return HEAP16;if(type==4)return HEAP32;if(type==6)return HEAPF32;if(type==5||type==28922||type==28520||type==30779||type==30782)return HEAPU32;return HEAPU16};var toTypedArrayIndex=(pointer,heap)=>pointer>>>31-Math.clz32(heap.BYTES_PER_ELEMENT);var emscriptenWebGLGetTexPixelData=(type,format,width,height,pixels,internalFormat)=>{var heap=heapObjectForWebGLType(type);var sizePerPixel=colorChannelsInGlTextureFormat(format)*heap.BYTES_PER_ELEMENT;var bytes=computeUnpackAlignedImageSize(width,height,sizePerPixel);return heap.subarray(toTypedArrayIndex(pixels,heap),toTypedArrayIndex(pixels+bytes,heap))};var _emscripten_glReadPixels=(x,y,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelPackBufferBinding){GLctx.readPixels(x,y,width,height,format,type,pixels);return}var heap=heapObjectForWebGLType(type);var target=toTypedArrayIndex(pixels,heap);GLctx.readPixels(x,y,width,height,format,type,heap,target);return}var pixelData=emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,format);if(!pixelData){GL.recordError(1280);return}GLctx.readPixels(x,y,width,height,format,type,pixelData)};var _emscripten_glReleaseShaderCompiler=()=>{};var _emscripten_glRenderbufferStorage=(x0,x1,x2,x3)=>GLctx.renderbufferStorage(x0,x1,x2,x3);var _emscripten_glRenderbufferStorageMultisample=(x0,x1,x2,x3,x4)=>GLctx.renderbufferStorageMultisample(x0,x1,x2,x3,x4);var _emscripten_glResumeTransformFeedback=()=>GLctx.resumeTransformFeedback();var _emscripten_glSampleCoverage=(value,invert)=>{GLctx.sampleCoverage(value,!!invert)};var _emscripten_glSamplerParameterf=(sampler,pname,param)=>{GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameterfv=(sampler,pname,params)=>{var param=HEAPF32[params>>2];GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteri=(sampler,pname,param)=>{GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteriv=(sampler,pname,params)=>{var param=HEAP32[params>>2];GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glScissor=(x0,x1,x2,x3)=>GLctx.scissor(x0,x1,x2,x3);var _emscripten_glShaderBinary=(count,shaders,binaryformat,binary,length)=>{GL.recordError(1280)};var _emscripten_glShaderSource=(shader,count,string,length)=>{var source=GL.getSource(shader,count,string,length);GLctx.shaderSource(GL.shaders[shader],source)};var _emscripten_glStencilFunc=(x0,x1,x2)=>GLctx.stencilFunc(x0,x1,x2);var _emscripten_glStencilFuncSeparate=(x0,x1,x2,x3)=>GLctx.stencilFuncSeparate(x0,x1,x2,x3);var _emscripten_glStencilMask=x0=>GLctx.stencilMask(x0);var _emscripten_glStencilMaskSeparate=(x0,x1)=>GLctx.stencilMaskSeparate(x0,x1);var _emscripten_glStencilOp=(x0,x1,x2)=>GLctx.stencilOp(x0,x1,x2);var _emscripten_glStencilOpSeparate=(x0,x1,x2,x3)=>GLctx.stencilOpSeparate(x0,x1,x2,x3);var _emscripten_glTexImage2D=(target,level,internalFormat,width,height,border,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);var index=toTypedArrayIndex(pixels,heap);GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,heap,index);return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,internalFormat):null;GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixelData)};var _emscripten_glTexImage3D=(target,level,internalFormat,width,height,depth,border,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,null)}};var _emscripten_glTexParameterf=(x0,x1,x2)=>GLctx.texParameterf(x0,x1,x2);var _emscripten_glTexParameterfv=(target,pname,params)=>{var param=HEAPF32[params>>2];GLctx.texParameterf(target,pname,param)};var _emscripten_glTexParameteri=(x0,x1,x2)=>GLctx.texParameteri(x0,x1,x2);var _emscripten_glTexParameteriv=(target,pname,params)=>{var param=HEAP32[params>>2];GLctx.texParameteri(target,pname,param)};var _emscripten_glTexStorage2D=(x0,x1,x2,x3,x4)=>GLctx.texStorage2D(x0,x1,x2,x3,x4);var _emscripten_glTexStorage3D=(x0,x1,x2,x3,x4,x5)=>GLctx.texStorage3D(x0,x1,x2,x3,x4,x5);var _emscripten_glTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,heap,toTypedArrayIndex(pixels,heap));return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,0):null;GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixelData)};var _emscripten_glTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,null)}};var _emscripten_glTransformFeedbackVaryings=(program,count,varyings,bufferMode)=>{program=GL.programs[program];var vars=[];for(var i=0;i>2]));GLctx.transformFeedbackVaryings(program,vars,bufferMode)};var _emscripten_glUniform1f=(location,v0)=>{GLctx.uniform1f(webglGetUniformLocation(location),v0)};var miniTempWebGLFloatBuffers=[];var _emscripten_glUniform1fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1fv(webglGetUniformLocation(location),HEAPF32,value>>2,count);return}if(count<=288){var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1i=(location,v0)=>{GLctx.uniform1i(webglGetUniformLocation(location),v0)};var miniTempWebGLIntBuffers=[];var _emscripten_glUniform1iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1iv(webglGetUniformLocation(location),HEAP32,value>>2,count);return}if(count<=288){var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2]}}else{var view=HEAP32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1ui=(location,v0)=>{GLctx.uniform1ui(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1uiv=(location,count,value)=>{count&&GLctx.uniform1uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count)};var _emscripten_glUniform2f=(location,v0,v1)=>{GLctx.uniform2f(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2i=(location,v0,v1)=>{GLctx.uniform2i(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2iv(webglGetUniformLocation(location),HEAP32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2ui=(location,v0,v1)=>{GLctx.uniform2ui(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2uiv=(location,count,value)=>{count&&GLctx.uniform2uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*2)};var _emscripten_glUniform3f=(location,v0,v1,v2)=>{GLctx.uniform3f(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3i=(location,v0,v1,v2)=>{GLctx.uniform3i(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3iv(webglGetUniformLocation(location),HEAP32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3ui=(location,v0,v1,v2)=>{GLctx.uniform3ui(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3uiv=(location,count,value)=>{count&&GLctx.uniform3uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*3)};var _emscripten_glUniform4f=(location,v0,v1,v2,v3)=>{GLctx.uniform4f(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*4);return}if(count<=72){var view=miniTempWebGLFloatBuffers[4*count];var heap=HEAPF32;value=value>>2;count*=4;for(var i=0;i>2,value+count*16>>2)}GLctx.uniform4fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4i=(location,v0,v1,v2,v3)=>{GLctx.uniform4i(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4iv(webglGetUniformLocation(location),HEAP32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2];view[i+3]=HEAP32[value+(4*i+12)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*16>>2)}GLctx.uniform4iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4ui=(location,v0,v1,v2,v3)=>{GLctx.uniform4ui(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4uiv=(location,count,value)=>{count&&GLctx.uniform4uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*4)};var _emscripten_glUniformBlockBinding=(program,uniformBlockIndex,uniformBlockBinding)=>{program=GL.programs[program];GLctx.uniformBlockBinding(program,uniformBlockIndex,uniformBlockBinding)};var _emscripten_glUniformMatrix2fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*16>>2)}GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix2x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix2x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix3fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*9);return}if(count<=32){count*=9;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2];view[i+4]=HEAPF32[value+(4*i+16)>>2];view[i+5]=HEAPF32[value+(4*i+20)>>2];view[i+6]=HEAPF32[value+(4*i+24)>>2];view[i+7]=HEAPF32[value+(4*i+28)>>2];view[i+8]=HEAPF32[value+(4*i+32)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*36>>2)}GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix3x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix3x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUniformMatrix4fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*16);return}if(count<=18){var view=miniTempWebGLFloatBuffers[16*count];var heap=HEAPF32;value=value>>2;count*=16;for(var i=0;i>2,value+count*64>>2)}GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix4x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix4x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUseProgram=program=>{program=GL.programs[program];GLctx.useProgram(program);GLctx.currentProgram=program};var _emscripten_glValidateProgram=program=>{GLctx.validateProgram(GL.programs[program])};var _emscripten_glVertexAttrib1f=(x0,x1)=>GLctx.vertexAttrib1f(x0,x1);var _emscripten_glVertexAttrib1fv=(index,v)=>{GLctx.vertexAttrib1f(index,HEAPF32[v>>2])};var _emscripten_glVertexAttrib2f=(x0,x1,x2)=>GLctx.vertexAttrib2f(x0,x1,x2);var _emscripten_glVertexAttrib2fv=(index,v)=>{GLctx.vertexAttrib2f(index,HEAPF32[v>>2],HEAPF32[v+4>>2])};var _emscripten_glVertexAttrib3f=(x0,x1,x2,x3)=>GLctx.vertexAttrib3f(x0,x1,x2,x3);var _emscripten_glVertexAttrib3fv=(index,v)=>{GLctx.vertexAttrib3f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2])};var _emscripten_glVertexAttrib4f=(x0,x1,x2,x3,x4)=>GLctx.vertexAttrib4f(x0,x1,x2,x3,x4);var _emscripten_glVertexAttrib4fv=(index,v)=>{GLctx.vertexAttrib4f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2],HEAPF32[v+12>>2])};var _emscripten_glVertexAttribDivisor=(index,divisor)=>{GLctx.vertexAttribDivisor(index,divisor)};var _glVertexAttribDivisor=_emscripten_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorANGLE=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorARB=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorEXT=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorNV=_glVertexAttribDivisor;var _emscripten_glVertexAttribI4i=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4i(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4iv=(index,v)=>{GLctx.vertexAttribI4i(index,HEAP32[v>>2],HEAP32[v+4>>2],HEAP32[v+8>>2],HEAP32[v+12>>2])};var _emscripten_glVertexAttribI4ui=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4ui(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4uiv=(index,v)=>{GLctx.vertexAttribI4ui(index,HEAPU32[v>>2],HEAPU32[v+4>>2],HEAPU32[v+8>>2],HEAPU32[v+12>>2])};var _emscripten_glVertexAttribIPointer=(index,size,type,stride,ptr)=>{GLctx.vertexAttribIPointer(index,size,type,stride,ptr)};var _emscripten_glVertexAttribPointer=(index,size,type,normalized,stride,ptr)=>{GLctx.vertexAttribPointer(index,size,type,!!normalized,stride,ptr)};var _emscripten_glViewport=(x0,x1,x2,x3)=>GLctx.viewport(x0,x1,x2,x3);var _emscripten_glWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);GLctx.waitSync(GL.syncs[sync],flags,timeout)};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var _emscripten_request_animation_frame_loop=(cb,userData)=>{function tick(timeStamp){if(getWasmTableEntry(cb)(timeStamp,userData)){requestAnimationFrame(tick)}}return requestAnimationFrame(tick)};var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(globalThis.navigator?.language??"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var _environ_get=(__environ,environ_buf)=>{var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_read(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doReadv(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=BigInt(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _glGetIntegerv=_emscripten_glGetIntegerv;var _glGetString=_emscripten_glGetString;var _glGetStringi=_emscripten_glGetStringi;var _llvm_eh_typeid_for=type=>type;function _random_get(buffer,size){try{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};FS.createPreloadedFile=FS_createPreloadedFile;FS.preloadFile=FS_preloadFile;FS.staticInit();for(let i=0;i<32;++i)tempFixedLengthArray.push(new Array(i));var miniTempWebGLFloatBuffersStorage=new Float32Array(288);for(var i=0;i<=288;++i){miniTempWebGLFloatBuffers[i]=miniTempWebGLFloatBuffersStorage.subarray(0,i)}var miniTempWebGLIntBuffersStorage=new Int32Array(288);for(var i=0;i<=288;++i){miniTempWebGLIntBuffers[i]=miniTempWebGLIntBuffersStorage.subarray(0,i)}{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["preloadPlugins"])preloadPlugins=Module["preloadPlugins"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;Module["GL"]=GL;var _malloc,_add_font,_add_image,_add_image_with_rid,_allocate,_apply_scene_transactions,_command,_deallocate,_destroy,_devtools_rendering_set_show_fps_meter,_devtools_rendering_set_show_hit_testing,_devtools_rendering_set_show_ruler,_devtools_rendering_set_show_stats,_devtools_rendering_set_show_tiles,_drain_missing_images,_export_node_as,_get_default_fallback_fonts,_get_image_bytes,_get_image_size,_get_node_absolute_bounding_box,_get_node_id_from_point,_get_node_ids_from_envelope,_get_node_ids_from_point,_grida_fonts_analyze_family,_grida_fonts_free,_grida_fonts_parse_font,_grida_markdown_to_html,_grida_svg_optimize,_grida_svg_to_document,_has_missing_fonts,_highlight_strokes,_init,_init_with_backend,_list_available_fonts,_list_missing_fonts,_load_benchmark_scene,_load_dummy_scene,_load_scene_grida,_load_scene_grida1,_loaded_scene_ids,_pointer_move,_redraw,_resize_surface,_resolve_image,_runtime_renderer_set_layer_compositing,_runtime_renderer_set_outline_mode,_runtime_renderer_set_pixel_preview_scale,_runtime_renderer_set_pixel_preview_stable,_runtime_renderer_set_render_policy_flags,_runtime_renderer_set_skip_layout,_set_debug,_set_default_fallback_fonts,_set_main_camera_transform,_set_surface_overlay_config,_set_verbose,_surface_get_cursor,_surface_get_hovered_node,_surface_get_selected_nodes,_surface_pointer_down,_surface_pointer_move,_surface_pointer_up,_surface_set_selection,_switch_scene,_text_edit_command,_text_edit_enter,_text_edit_exit,_text_edit_get_caret_rect,_text_edit_get_selected_html,_text_edit_get_selected_text,_text_edit_get_selection_rects,_text_edit_get_text,_text_edit_ime_cancel,_text_edit_ime_commit,_text_edit_ime_set_preedit,_text_edit_is_active,_text_edit_paste_html,_text_edit_paste_text,_text_edit_pointer_down,_text_edit_pointer_move,_text_edit_pointer_up,_text_edit_redo,_text_edit_set_color,_text_edit_set_font_family,_text_edit_set_font_size,_text_edit_tick,_text_edit_toggle_bold,_text_edit_toggle_italic,_text_edit_toggle_strikethrough,_text_edit_toggle_underline,_text_edit_undo,_tick,_to_vector_network,_toggle_debug,_main,_emscripten_builtin_memalign,_setThrew,__emscripten_tempret_set,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,___cxa_decrement_exception_refcount,___cxa_increment_exception_refcount,___cxa_can_catch,___cxa_get_exception_ptr,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){_malloc=wasmExports["Pg"];_add_font=Module["_add_font"]=wasmExports["Rg"];_add_image=Module["_add_image"]=wasmExports["Sg"];_add_image_with_rid=Module["_add_image_with_rid"]=wasmExports["Tg"];_allocate=Module["_allocate"]=wasmExports["Ug"];_apply_scene_transactions=Module["_apply_scene_transactions"]=wasmExports["Vg"];_command=Module["_command"]=wasmExports["Wg"];_deallocate=Module["_deallocate"]=wasmExports["Xg"];_destroy=Module["_destroy"]=wasmExports["Yg"];_devtools_rendering_set_show_fps_meter=Module["_devtools_rendering_set_show_fps_meter"]=wasmExports["Zg"];_devtools_rendering_set_show_hit_testing=Module["_devtools_rendering_set_show_hit_testing"]=wasmExports["_g"];_devtools_rendering_set_show_ruler=Module["_devtools_rendering_set_show_ruler"]=wasmExports["$g"];_devtools_rendering_set_show_stats=Module["_devtools_rendering_set_show_stats"]=wasmExports["ah"];_devtools_rendering_set_show_tiles=Module["_devtools_rendering_set_show_tiles"]=wasmExports["bh"];_drain_missing_images=Module["_drain_missing_images"]=wasmExports["ch"];_export_node_as=Module["_export_node_as"]=wasmExports["dh"];_get_default_fallback_fonts=Module["_get_default_fallback_fonts"]=wasmExports["eh"];_get_image_bytes=Module["_get_image_bytes"]=wasmExports["fh"];_get_image_size=Module["_get_image_size"]=wasmExports["gh"];_get_node_absolute_bounding_box=Module["_get_node_absolute_bounding_box"]=wasmExports["hh"];_get_node_id_from_point=Module["_get_node_id_from_point"]=wasmExports["ih"];_get_node_ids_from_envelope=Module["_get_node_ids_from_envelope"]=wasmExports["jh"];_get_node_ids_from_point=Module["_get_node_ids_from_point"]=wasmExports["kh"];_grida_fonts_analyze_family=Module["_grida_fonts_analyze_family"]=wasmExports["lh"];_grida_fonts_free=Module["_grida_fonts_free"]=wasmExports["mh"];_grida_fonts_parse_font=Module["_grida_fonts_parse_font"]=wasmExports["nh"];_grida_markdown_to_html=Module["_grida_markdown_to_html"]=wasmExports["oh"];_grida_svg_optimize=Module["_grida_svg_optimize"]=wasmExports["ph"];_grida_svg_to_document=Module["_grida_svg_to_document"]=wasmExports["qh"];_has_missing_fonts=Module["_has_missing_fonts"]=wasmExports["rh"];_highlight_strokes=Module["_highlight_strokes"]=wasmExports["sh"];_init=Module["_init"]=wasmExports["th"];_init_with_backend=Module["_init_with_backend"]=wasmExports["uh"];_list_available_fonts=Module["_list_available_fonts"]=wasmExports["vh"];_list_missing_fonts=Module["_list_missing_fonts"]=wasmExports["wh"];_load_benchmark_scene=Module["_load_benchmark_scene"]=wasmExports["xh"];_load_dummy_scene=Module["_load_dummy_scene"]=wasmExports["yh"];_load_scene_grida=Module["_load_scene_grida"]=wasmExports["zh"];_load_scene_grida1=Module["_load_scene_grida1"]=wasmExports["Ah"];_loaded_scene_ids=Module["_loaded_scene_ids"]=wasmExports["Bh"];_pointer_move=Module["_pointer_move"]=wasmExports["Ch"];_redraw=Module["_redraw"]=wasmExports["Dh"];_resize_surface=Module["_resize_surface"]=wasmExports["Eh"];_resolve_image=Module["_resolve_image"]=wasmExports["Fh"];_runtime_renderer_set_layer_compositing=Module["_runtime_renderer_set_layer_compositing"]=wasmExports["Gh"];_runtime_renderer_set_outline_mode=Module["_runtime_renderer_set_outline_mode"]=wasmExports["Hh"];_runtime_renderer_set_pixel_preview_scale=Module["_runtime_renderer_set_pixel_preview_scale"]=wasmExports["Ih"];_runtime_renderer_set_pixel_preview_stable=Module["_runtime_renderer_set_pixel_preview_stable"]=wasmExports["Jh"];_runtime_renderer_set_render_policy_flags=Module["_runtime_renderer_set_render_policy_flags"]=wasmExports["Kh"];_runtime_renderer_set_skip_layout=Module["_runtime_renderer_set_skip_layout"]=wasmExports["Lh"];_set_debug=Module["_set_debug"]=wasmExports["Mh"];_set_default_fallback_fonts=Module["_set_default_fallback_fonts"]=wasmExports["Nh"];_set_main_camera_transform=Module["_set_main_camera_transform"]=wasmExports["Oh"];_set_surface_overlay_config=Module["_set_surface_overlay_config"]=wasmExports["Ph"];_set_verbose=Module["_set_verbose"]=wasmExports["Qh"];_surface_get_cursor=Module["_surface_get_cursor"]=wasmExports["Rh"];_surface_get_hovered_node=Module["_surface_get_hovered_node"]=wasmExports["Sh"];_surface_get_selected_nodes=Module["_surface_get_selected_nodes"]=wasmExports["Th"];_surface_pointer_down=Module["_surface_pointer_down"]=wasmExports["Uh"];_surface_pointer_move=Module["_surface_pointer_move"]=wasmExports["Vh"];_surface_pointer_up=Module["_surface_pointer_up"]=wasmExports["Wh"];_surface_set_selection=Module["_surface_set_selection"]=wasmExports["Xh"];_switch_scene=Module["_switch_scene"]=wasmExports["Yh"];_text_edit_command=Module["_text_edit_command"]=wasmExports["Zh"];_text_edit_enter=Module["_text_edit_enter"]=wasmExports["_h"];_text_edit_exit=Module["_text_edit_exit"]=wasmExports["$h"];_text_edit_get_caret_rect=Module["_text_edit_get_caret_rect"]=wasmExports["ai"];_text_edit_get_selected_html=Module["_text_edit_get_selected_html"]=wasmExports["bi"];_text_edit_get_selected_text=Module["_text_edit_get_selected_text"]=wasmExports["ci"];_text_edit_get_selection_rects=Module["_text_edit_get_selection_rects"]=wasmExports["di"];_text_edit_get_text=Module["_text_edit_get_text"]=wasmExports["ei"];_text_edit_ime_cancel=Module["_text_edit_ime_cancel"]=wasmExports["fi"];_text_edit_ime_commit=Module["_text_edit_ime_commit"]=wasmExports["gi"];_text_edit_ime_set_preedit=Module["_text_edit_ime_set_preedit"]=wasmExports["hi"];_text_edit_is_active=Module["_text_edit_is_active"]=wasmExports["ii"];_text_edit_paste_html=Module["_text_edit_paste_html"]=wasmExports["ji"];_text_edit_paste_text=Module["_text_edit_paste_text"]=wasmExports["ki"];_text_edit_pointer_down=Module["_text_edit_pointer_down"]=wasmExports["li"];_text_edit_pointer_move=Module["_text_edit_pointer_move"]=wasmExports["mi"];_text_edit_pointer_up=Module["_text_edit_pointer_up"]=wasmExports["ni"];_text_edit_redo=Module["_text_edit_redo"]=wasmExports["oi"];_text_edit_set_color=Module["_text_edit_set_color"]=wasmExports["pi"];_text_edit_set_font_family=Module["_text_edit_set_font_family"]=wasmExports["qi"];_text_edit_set_font_size=Module["_text_edit_set_font_size"]=wasmExports["ri"];_text_edit_tick=Module["_text_edit_tick"]=wasmExports["si"];_text_edit_toggle_bold=Module["_text_edit_toggle_bold"]=wasmExports["ti"];_text_edit_toggle_italic=Module["_text_edit_toggle_italic"]=wasmExports["ui"];_text_edit_toggle_strikethrough=Module["_text_edit_toggle_strikethrough"]=wasmExports["vi"];_text_edit_toggle_underline=Module["_text_edit_toggle_underline"]=wasmExports["wi"];_text_edit_undo=Module["_text_edit_undo"]=wasmExports["xi"];_tick=Module["_tick"]=wasmExports["yi"];_to_vector_network=Module["_to_vector_network"]=wasmExports["zi"];_toggle_debug=Module["_toggle_debug"]=wasmExports["Ai"];_main=Module["_main"]=wasmExports["Bi"];_emscripten_builtin_memalign=wasmExports["Ci"];_setThrew=wasmExports["Di"];__emscripten_tempret_set=wasmExports["Ei"];__emscripten_stack_restore=wasmExports["Fi"];__emscripten_stack_alloc=wasmExports["Gi"];_emscripten_stack_get_current=wasmExports["Hi"];___cxa_decrement_exception_refcount=wasmExports["Ii"];___cxa_increment_exception_refcount=wasmExports["Ji"];___cxa_can_catch=wasmExports["Ki"];___cxa_get_exception_ptr=wasmExports["Li"];memory=wasmMemory=wasmExports["Ng"];__indirect_function_table=wasmTable=wasmExports["Qg"]}var wasmImports={G:___cxa_begin_catch,O:___cxa_end_catch,a:___cxa_find_matching_catch_2,n:___cxa_find_matching_catch_3,ia:___cxa_find_matching_catch_4,Da:___cxa_rethrow,I:___cxa_throw,cb:___cxa_uncaught_exceptions,e:___resumeException,Ga:___syscall_fcntl64,ub:___syscall_fstat64,pb:___syscall_getcwd,vb:___syscall_ioctl,qb:___syscall_lstat64,rb:___syscall_newfstatat,Ha:___syscall_openat,sb:___syscall_stat64,yb:__abort_js,eb:__emscripten_throw_longjmp,kb:__gmtime_js,ib:__mmap_js,jb:__munmap_js,Ab:__tzset_js,xb:_clock_time_get,wb:_emscripten_date_now,gb:_emscripten_get_heap_max,zf:_emscripten_glActiveTexture,Af:_emscripten_glAttachShader,ce:_emscripten_glBeginQuery,Yd:_emscripten_glBeginQueryEXT,Dc:_emscripten_glBeginTransformFeedback,Bf:_emscripten_glBindAttribLocation,Cf:_emscripten_glBindBuffer,Ac:_emscripten_glBindBufferBase,Bc:_emscripten_glBindBufferRange,Ae:_emscripten_glBindFramebuffer,Be:_emscripten_glBindRenderbuffer,ie:_emscripten_glBindSampler,Df:_emscripten_glBindTexture,Rb:_emscripten_glBindTransformFeedback,We:_emscripten_glBindVertexArray,Ze:_emscripten_glBindVertexArrayOES,Ef:_emscripten_glBlendColor,Ff:_emscripten_glBlendEquation,Id:_emscripten_glBlendEquationSeparate,Gf:_emscripten_glBlendFunc,Hd:_emscripten_glBlendFuncSeparate,ue:_emscripten_glBlitFramebuffer,Hf:_emscripten_glBufferData,If:_emscripten_glBufferSubData,Ce:_emscripten_glCheckFramebufferStatus,Jf:_emscripten_glClear,ec:_emscripten_glClearBufferfi,fc:_emscripten_glClearBufferfv,hc:_emscripten_glClearBufferiv,gc:_emscripten_glClearBufferuiv,Kf:_emscripten_glClearColor,Gd:_emscripten_glClearDepthf,Lf:_emscripten_glClearStencil,re:_emscripten_glClientWaitSync,Zc:_emscripten_glClipControlEXT,Mf:_emscripten_glColorMask,Nf:_emscripten_glCompileShader,Of:_emscripten_glCompressedTexImage2D,Qc:_emscripten_glCompressedTexImage3D,Pf:_emscripten_glCompressedTexSubImage2D,Pc:_emscripten_glCompressedTexSubImage3D,te:_emscripten_glCopyBufferSubData,Fd:_emscripten_glCopyTexImage2D,Rf:_emscripten_glCopyTexSubImage2D,Rc:_emscripten_glCopyTexSubImage3D,Sf:_emscripten_glCreateProgram,Tf:_emscripten_glCreateShader,Uf:_emscripten_glCullFace,Vf:_emscripten_glDeleteBuffers,De:_emscripten_glDeleteFramebuffers,Wf:_emscripten_glDeleteProgram,de:_emscripten_glDeleteQueries,Zd:_emscripten_glDeleteQueriesEXT,Ee:_emscripten_glDeleteRenderbuffers,je:_emscripten_glDeleteSamplers,Xf:_emscripten_glDeleteShader,se:_emscripten_glDeleteSync,Yf:_emscripten_glDeleteTextures,Qb:_emscripten_glDeleteTransformFeedbacks,Xe:_emscripten_glDeleteVertexArrays,_e:_emscripten_glDeleteVertexArraysOES,Ed:_emscripten_glDepthFunc,Zf:_emscripten_glDepthMask,Dd:_emscripten_glDepthRangef,Cd:_emscripten_glDetachShader,_f:_emscripten_glDisable,$f:_emscripten_glDisableVertexAttribArray,ag:_emscripten_glDrawArrays,Ue:_emscripten_glDrawArraysInstanced,Ld:_emscripten_glDrawArraysInstancedANGLE,Db:_emscripten_glDrawArraysInstancedARB,Re:_emscripten_glDrawArraysInstancedBaseInstanceWEBGL,Wc:_emscripten_glDrawArraysInstancedEXT,Eb:_emscripten_glDrawArraysInstancedNV,Pe:_emscripten_glDrawBuffers,Uc:_emscripten_glDrawBuffersEXT,Md:_emscripten_glDrawBuffersWEBGL,bg:_emscripten_glDrawElements,Ve:_emscripten_glDrawElementsInstanced,Kd:_emscripten_glDrawElementsInstancedANGLE,Bb:_emscripten_glDrawElementsInstancedARB,Se:_emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Cb:_emscripten_glDrawElementsInstancedEXT,Vc:_emscripten_glDrawElementsInstancedNV,Je:_emscripten_glDrawRangeElements,cg:_emscripten_glEnable,dg:_emscripten_glEnableVertexAttribArray,ee:_emscripten_glEndQuery,_d:_emscripten_glEndQueryEXT,Cc:_emscripten_glEndTransformFeedback,oe:_emscripten_glFenceSync,eg:_emscripten_glFinish,fg:_emscripten_glFlush,Fe:_emscripten_glFramebufferRenderbuffer,Ge:_emscripten_glFramebufferTexture2D,Hc:_emscripten_glFramebufferTextureLayer,gg:_emscripten_glFrontFace,hg:_emscripten_glGenBuffers,He:_emscripten_glGenFramebuffers,fe:_emscripten_glGenQueries,$d:_emscripten_glGenQueriesEXT,Ie:_emscripten_glGenRenderbuffers,ke:_emscripten_glGenSamplers,ig:_emscripten_glGenTextures,Pb:_emscripten_glGenTransformFeedbacks,Te:_emscripten_glGenVertexArrays,$e:_emscripten_glGenVertexArraysOES,we:_emscripten_glGenerateMipmap,Bd:_emscripten_glGetActiveAttrib,Ad:_emscripten_glGetActiveUniform,$b:_emscripten_glGetActiveUniformBlockName,ac:_emscripten_glGetActiveUniformBlockiv,cc:_emscripten_glGetActiveUniformsiv,zd:_emscripten_glGetAttachedShaders,yd:_emscripten_glGetAttribLocation,xd:_emscripten_glGetBooleanv,Wb:_emscripten_glGetBufferParameteri64v,jg:_emscripten_glGetBufferParameteriv,kg:_emscripten_glGetError,lg:_emscripten_glGetFloatv,qc:_emscripten_glGetFragDataLocation,xe:_emscripten_glGetFramebufferAttachmentParameteriv,Xb:_emscripten_glGetInteger64i_v,Zb:_emscripten_glGetInteger64v,Ec:_emscripten_glGetIntegeri_v,mg:_emscripten_glGetIntegerv,Hb:_emscripten_glGetInternalformativ,Lb:_emscripten_glGetProgramBinary,ng:_emscripten_glGetProgramInfoLog,og:_emscripten_glGetProgramiv,Vd:_emscripten_glGetQueryObjecti64vEXT,Od:_emscripten_glGetQueryObjectivEXT,Wd:_emscripten_glGetQueryObjectui64vEXT,ge:_emscripten_glGetQueryObjectuiv,ae:_emscripten_glGetQueryObjectuivEXT,he:_emscripten_glGetQueryiv,be:_emscripten_glGetQueryivEXT,ye:_emscripten_glGetRenderbufferParameteriv,Sb:_emscripten_glGetSamplerParameterfv,Tb:_emscripten_glGetSamplerParameteriv,pg:_emscripten_glGetShaderInfoLog,Sd:_emscripten_glGetShaderPrecisionFormat,wd:_emscripten_glGetShaderSource,qg:_emscripten_glGetShaderiv,rg:_emscripten_glGetString,Ye:_emscripten_glGetStringi,Yb:_emscripten_glGetSynciv,vd:_emscripten_glGetTexParameterfv,ud:_emscripten_glGetTexParameteriv,yc:_emscripten_glGetTransformFeedbackVarying,bc:_emscripten_glGetUniformBlockIndex,dc:_emscripten_glGetUniformIndices,sg:_emscripten_glGetUniformLocation,td:_emscripten_glGetUniformfv,sd:_emscripten_glGetUniformiv,rc:_emscripten_glGetUniformuiv,xc:_emscripten_glGetVertexAttribIiv,wc:_emscripten_glGetVertexAttribIuiv,pd:_emscripten_glGetVertexAttribPointerv,rd:_emscripten_glGetVertexAttribfv,qd:_emscripten_glGetVertexAttribiv,od:_emscripten_glHint,Td:_emscripten_glInvalidateFramebuffer,Ud:_emscripten_glInvalidateSubFramebuffer,nd:_emscripten_glIsBuffer,md:_emscripten_glIsEnabled,ld:_emscripten_glIsFramebuffer,kd:_emscripten_glIsProgram,Oc:_emscripten_glIsQuery,Pd:_emscripten_glIsQueryEXT,jd:_emscripten_glIsRenderbuffer,Vb:_emscripten_glIsSampler,id:_emscripten_glIsShader,pe:_emscripten_glIsSync,tg:_emscripten_glIsTexture,Ob:_emscripten_glIsTransformFeedback,Fc:_emscripten_glIsVertexArray,Nd:_emscripten_glIsVertexArrayOES,ug:_emscripten_glLineWidth,vg:_emscripten_glLinkProgram,Ne:_emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL,Oe:_emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Nb:_emscripten_glPauseTransformFeedback,wg:_emscripten_glPixelStorei,Yc:_emscripten_glPolygonModeWEBGL,hd:_emscripten_glPolygonOffset,_c:_emscripten_glPolygonOffsetClampEXT,Kb:_emscripten_glProgramBinary,Jb:_emscripten_glProgramParameteri,Xd:_emscripten_glQueryCounterEXT,Qe:_emscripten_glReadBuffer,xg:_emscripten_glReadPixels,gd:_emscripten_glReleaseShaderCompiler,ze:_emscripten_glRenderbufferStorage,ve:_emscripten_glRenderbufferStorageMultisample,Mb:_emscripten_glResumeTransformFeedback,fd:_emscripten_glSampleCoverage,le:_emscripten_glSamplerParameterf,Ub:_emscripten_glSamplerParameterfv,me:_emscripten_glSamplerParameteri,ne:_emscripten_glSamplerParameteriv,yg:_emscripten_glScissor,ed:_emscripten_glShaderBinary,zg:_emscripten_glShaderSource,Ag:_emscripten_glStencilFunc,Bg:_emscripten_glStencilFuncSeparate,Cg:_emscripten_glStencilMask,Dg:_emscripten_glStencilMaskSeparate,Eg:_emscripten_glStencilOp,Fg:_emscripten_glStencilOpSeparate,Gg:_emscripten_glTexImage2D,Tc:_emscripten_glTexImage3D,Hg:_emscripten_glTexParameterf,Ig:_emscripten_glTexParameterfv,Jg:_emscripten_glTexParameteri,Kg:_emscripten_glTexParameteriv,Ke:_emscripten_glTexStorage2D,Ib:_emscripten_glTexStorage3D,Lg:_emscripten_glTexSubImage2D,Sc:_emscripten_glTexSubImage3D,zc:_emscripten_glTransformFeedbackVaryings,Mg:_emscripten_glUniform1f,Na:_emscripten_glUniform1fv,vf:_emscripten_glUniform1i,wf:_emscripten_glUniform1iv,pc:_emscripten_glUniform1ui,lc:_emscripten_glUniform1uiv,xf:_emscripten_glUniform2f,yf:_emscripten_glUniform2fv,uf:_emscripten_glUniform2i,tf:_emscripten_glUniform2iv,oc:_emscripten_glUniform2ui,kc:_emscripten_glUniform2uiv,sf:_emscripten_glUniform3f,rf:_emscripten_glUniform3fv,qf:_emscripten_glUniform3i,pf:_emscripten_glUniform3iv,nc:_emscripten_glUniform3ui,jc:_emscripten_glUniform3uiv,of:_emscripten_glUniform4f,nf:_emscripten_glUniform4fv,af:_emscripten_glUniform4i,bf:_emscripten_glUniform4iv,mc:_emscripten_glUniform4ui,ic:_emscripten_glUniform4uiv,_b:_emscripten_glUniformBlockBinding,cf:_emscripten_glUniformMatrix2fv,Nc:_emscripten_glUniformMatrix2x3fv,Lc:_emscripten_glUniformMatrix2x4fv,df:_emscripten_glUniformMatrix3fv,Mc:_emscripten_glUniformMatrix3x2fv,Jc:_emscripten_glUniformMatrix3x4fv,ef:_emscripten_glUniformMatrix4fv,Kc:_emscripten_glUniformMatrix4x2fv,Ic:_emscripten_glUniformMatrix4x3fv,ff:_emscripten_glUseProgram,dd:_emscripten_glValidateProgram,gf:_emscripten_glVertexAttrib1f,cd:_emscripten_glVertexAttrib1fv,bd:_emscripten_glVertexAttrib2f,hf:_emscripten_glVertexAttrib2fv,ad:_emscripten_glVertexAttrib3f,jf:_emscripten_glVertexAttrib3fv,$c:_emscripten_glVertexAttrib4f,kf:_emscripten_glVertexAttrib4fv,Le:_emscripten_glVertexAttribDivisor,Jd:_emscripten_glVertexAttribDivisorANGLE,Fb:_emscripten_glVertexAttribDivisorARB,Xc:_emscripten_glVertexAttribDivisorEXT,Gb:_emscripten_glVertexAttribDivisorNV,vc:_emscripten_glVertexAttribI4i,tc:_emscripten_glVertexAttribI4iv,uc:_emscripten_glVertexAttribI4ui,sc:_emscripten_glVertexAttribI4uiv,Me:_emscripten_glVertexAttribIPointer,lf:_emscripten_glVertexAttribPointer,mf:_emscripten_glViewport,qe:_emscripten_glWaitSync,Ya:_emscripten_request_animation_frame_loop,fb:_emscripten_resize_heap,mb:_environ_get,nb:_environ_sizes_get,Qa:_exit,na:_fd_close,hb:_fd_pread,Fa:_fd_read,lb:_fd_seek,ma:_fd_write,Oa:_glGetIntegerv,qa:_glGetString,Pa:_glGetStringi,Qd:invoke_dd,Rd:invoke_dddd,Ba:invoke_diii,Ta:invoke_fdiiii,Sa:invoke_fdiiiii,Ra:invoke_fii,Ca:invoke_fiii,t:invoke_fiiidi,U:invoke_fiiif,u:invoke_fiiiidi,s:invoke_i,j:invoke_ii,H:invoke_iif,$a:invoke_iiffi,sa:invoke_iiffiii,f:invoke_iii,ua:invoke_iiifi,g:invoke_iiii,T:invoke_iiiiff,l:invoke_iiiii,bb:invoke_iiiiid,z:invoke_iiiiii,A:invoke_iiiiiii,F:invoke_iiiiiiii,q:invoke_iiiiiiiii,ra:invoke_iiiiiiiiii,ea:invoke_iiiiiiiiiiii,pa:invoke_iiiiiiiiiiiifiii,W:invoke_iij,db:invoke_j,ja:invoke_ji,r:invoke_jiii,fa:invoke_jiiii,Z:invoke_jiijj,K:invoke_jjji,k:invoke_v,Qf:invoke_vff,b:invoke_vi,Q:invoke_vid,S:invoke_vif,v:invoke_viff,E:invoke_viffff,aa:invoke_vifffff,Ua:invoke_viffffff,D:invoke_viffi,ka:invoke_viffiiiiiii,c:invoke_vii,Xa:invoke_viidii,P:invoke_viif,x:invoke_viiff,Ka:invoke_viiffii,ba:invoke_viifi,va:invoke_viififii,y:invoke_viifiiifi,d:invoke_viii,J:invoke_viiif,xa:invoke_viiiff,C:invoke_viiiffi,L:invoke_viiiffiffii,M:invoke_viiififiiiiiiiiiiii,i:invoke_viiii,Wa:invoke_viiiidididii,X:invoke_viiiif,ya:invoke_viiiiff,Aa:invoke_viiiiffi,wa:invoke_viiiifi,h:invoke_viiiii,Va:invoke_viiiiiffiii,ab:invoke_viiiiifi,m:invoke_viiiiii,Ja:invoke_viiiiiiff,p:invoke_viiiiiii,V:invoke_viiiiiiii,$:invoke_viiiiiiiii,N:invoke_viiiiiiiiii,zb:invoke_viiiiiiiiiifii,ta:invoke_viiiiiiiiiii,da:invoke_viiiiiiiiiiiiiii,Ma:invoke_viiiiiiji,R:invoke_viiij,B:invoke_viiijii,_:invoke_viij,o:invoke_viiji,Ia:invoke_viijiffi,ha:invoke_viijii,ca:invoke_viijiiiif,La:invoke_viijiiiii,la:invoke_viijj,Y:invoke_viji,Za:invoke_vijififi,w:invoke_vijii,Ea:invoke_vijiifi,_a:invoke_vijiififi,za:invoke_vijiii,tb:invoke_vijijjiii,ga:invoke_vijjjj,Gc:invoke_vjii,oa:_llvm_eh_typeid_for,ob:_random_get};function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ji(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiij(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vff(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viij(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiji(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiijj(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiji(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vid(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viji(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiif(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiiifiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vif(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vjii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiffii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiff(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiffi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiifii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijijjiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiffi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiifi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiififiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffiffii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffi(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viififii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_vijiififi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijififi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iij(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijj(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jjji(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiifi(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viif(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viff(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifffff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiidi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viidii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiidididii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiidi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiffiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiijii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffffff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiif(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dddd(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dd(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijjjj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_j(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiid(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_fiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function callMain(args=[]){var entryFunction=_main;args.unshift(thisProgram);var argc=args.length;var argv=stackAlloc((argc+1)*4);var argv_ptr=argv;for(var arg of args){HEAPU32[argv_ptr>>2]=stringToUTF8OnStack(arg);argv_ptr+=4}HEAPU32[argv_ptr>>2]=0;try{var ret=entryFunction(argc,argv);exitJS(ret,true);return ret}catch(e){return handleException(e)}}function run(args=arguments_){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();var noInitialRun=Module["noInitialRun"]||false;if(!noInitialRun)callMain(args);postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} ;return moduleRtn}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=createGridaCanvas;module.exports.default=createGridaCanvas}else if(typeof define==="function"&&define["amd"])define([],()=>createGridaCanvas); diff --git a/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm b/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm index 33ce6f67f6..1cce264521 100755 --- a/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm +++ b/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64dc52d48723cf9eff216754c3cb024ea27ef810bd016d21d560e42cd82e951b -size 13197254 +oid sha256:fce3f7c0ac424db1867c19a5c58f76fc1c567b0dddcb9048aff2f8e653d74a2b +size 13245926 diff --git a/crates/grida-canvas-wasm/package.json b/crates/grida-canvas-wasm/package.json index db08cf0fff..908d9b0f49 100644 --- a/crates/grida-canvas-wasm/package.json +++ b/crates/grida-canvas-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@grida/canvas-wasm", - "version": "0.91.0-canary.12", + "version": "0.91.0-canary.13", "private": false, "description": "WASM bindings for Grida Canvas", "keywords": [ @@ -23,7 +23,7 @@ "build": "tsup", "dev": "tsup --watch", "prepack": "just build", - "prepublishOnly": "[ $(du -sk lib 2>/dev/null | cut -f1) -lt 15360 ]", + "prepublishOnly": "[ $(du -sk dist 2>/dev/null | cut -f1) -lt 15360 ]", "serve": "serve -p 4020", "test": "vitest run", "typecheck": "tsc --noEmit" From e955d0c4428ab45dbd614bdc062dc7eeea72a365 Mon Sep 17 00:00:00 2001 From: Universe Date: Wed, 25 Mar 2026 16:52:02 +0900 Subject: [PATCH 29/31] refactor(grida-canvas): remove SmallVec dependency and optimize Paints storage - Eliminated the SmallVec dependency from the Paints structure, replacing it with a standard Vec for internal storage. - Updated related methods to accommodate the new storage type, simplifying the code and reducing heap allocations. - Introduced a new method for creating Paints from pre-extracted data to enhance performance during scene graph construction. --- Cargo.lock | 4 --- crates/grida-canvas/Cargo.toml | 2 -- crates/grida-canvas/src/cg/types.rs | 33 +++++++---------- crates/grida-canvas/src/node/scene_graph.rs | 39 +++++++++++++++++++++ 4 files changed, 51 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ac168e199..808b7c0ca4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -500,7 +500,6 @@ dependencies = [ "serde", "serde_json", "skia-safe", - "smallvec", "taffy", "tokio", "usvg", @@ -3951,9 +3950,6 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = [ - "serde", -] [[package]] name = "smithay-client-toolkit" diff --git a/crates/grida-canvas/Cargo.toml b/crates/grida-canvas/Cargo.toml index bb62b98218..4fa3bdfbfb 100644 --- a/crates/grida-canvas/Cargo.toml +++ b/crates/grida-canvas/Cargo.toml @@ -35,8 +35,6 @@ rstar = "0.12" seahash = "4.1.0" # fast non-cryptographic hasher for hot-path HashMaps rustc-hash = "2" -# inline small collections (avoids heap alloc for common 0-1 element cases) -smallvec = { version = "1", features = ["serde"] } # layout engine taffy = "0.9.2" # svg parsing diff --git a/crates/grida-canvas/src/cg/types.rs b/crates/grida-canvas/src/cg/types.rs index 6aff8a53fa..f2f561eb95 100644 --- a/crates/grida-canvas/src/cg/types.rs +++ b/crates/grida-canvas/src/cg/types.rs @@ -2,7 +2,6 @@ use super::prelude::*; use core::str; use math2::{box_fit::BoxFit, transform::AffineTransform}; use serde::{Deserialize, Serialize}; -use smallvec::SmallVec; use std::hash::Hash; use super::alignment::Alignment; @@ -2008,14 +2007,9 @@ impl From for SolidPaint { /// The [`BlendMode`] assigned to each [`Paint`] applies to that specific entry /// while it is composited over the accumulated result. It never retroactively /// affects paints that were drawn earlier in the stack. -/// Inline capacity for the [`Paints`] collection. Most nodes have 0 or 1 fill/stroke. -/// `SmallVec<[Paint; 1]>` stores up to 1 paint inline without heap allocation, -/// saving ~272K allocs for a 136K-node scene (fills + strokes). -type PaintsVec = SmallVec<[Paint; 1]>; - #[derive(Debug, Clone, Default)] pub struct Paints { - paints: PaintsVec, + paints: Vec, } impl Paints { @@ -2059,7 +2053,7 @@ impl Paints { /// Consume the collection and return the underlying vector. pub fn into_vec(self) -> Vec { - self.paints.into_vec() + self.paints } /// Append a new paint to the top of the stack. @@ -2086,32 +2080,30 @@ impl From> for Paints { impl From for Vec { fn from(value: Paints) -> Self { - value.paints.into_vec() + value.paints } } // Custom trait to support both Vec and array literals in Paints::new() pub trait IntoPaints { - fn into_paints(self) -> PaintsVec; + fn into_paints(self) -> Vec; } impl IntoPaints for Vec { - fn into_paints(self) -> PaintsVec { - SmallVec::from_vec(self) + fn into_paints(self) -> Vec { + self } } impl IntoPaints for [Paint; N] { - fn into_paints(self) -> PaintsVec { - self.into_iter().collect() + fn into_paints(self) -> Vec { + self.to_vec() } } impl FromIterator for Paints { fn from_iter>(iter: I) -> Self { - Paints { - paints: iter.into_iter().collect(), - } + Paints::new(iter.into_iter().collect::>()) } } @@ -2119,7 +2111,7 @@ impl FromIterator for Paints { impl From<[Paint; N]> for Paints { fn from(value: [Paint; N]) -> Self { Paints { - paints: value.into_iter().collect(), + paints: value.to_vec(), } } } @@ -2127,16 +2119,15 @@ impl From<[Paint; N]> for Paints { // Support for single Paint conversion impl From for Paints { fn from(value: Paint) -> Self { - // SmallVec stores 1 paint inline — zero heap allocation Paints { - paints: smallvec::smallvec![value], + paints: vec![value], } } } impl IntoIterator for Paints { type Item = Paint; - type IntoIter = smallvec::IntoIter<[Paint; 1]>; + type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { self.paints.into_iter() diff --git a/crates/grida-canvas/src/node/scene_graph.rs b/crates/grida-canvas/src/node/scene_graph.rs index afd5439284..c45ad4483c 100644 --- a/crates/grida-canvas/src/node/scene_graph.rs +++ b/crates/grida-canvas/src/node/scene_graph.rs @@ -517,6 +517,45 @@ impl SceneGraph { } graph.has_flex = has_flex; + Self::finish_snapshot(graph, links, roots) + } + + /// Like [`new_from_snapshot`] but accepts pre-extracted `geo_data` and + /// `layer_core` maps, avoiding a second iteration over all nodes. + /// + /// Used by the FBS decoder which extracts compact data in its own + /// consume loop (Phase 4) while the Node fields are cache-hot. + pub fn new_from_snapshot_preextracted( + node_pairs: impl IntoIterator, + geo_data: impl IntoIterator, + layer_core: impl IntoIterator, + has_flex: bool, + links: HashMap>, + roots: Vec, + ) -> Self { + let mut graph = Self::new(); + + // Insert nodes without re-extracting — already done by caller. + for (id, node) in node_pairs { + graph.nodes.insert_with_id(id, node); + } + for (id, geo) in geo_data { + graph.geo_data.insert(id, geo); + } + for (id, lc) in layer_core { + graph.layer_core.insert(id, lc); + } + graph.has_flex = has_flex; + + Self::finish_snapshot(graph, links, roots) + } + + /// Shared tail for snapshot constructors: set links and roots. + fn finish_snapshot( + mut graph: Self, + links: HashMap>, + roots: Vec, + ) -> Self { // Convert HashMap links to DenseNodeMap let mut dense_links = DenseNodeMap::new(); for (id, children) in links { From edf4ee0e31e0fc03c52219a5a7fd214f083a4de7 Mon Sep 17 00:00:00 2001 From: Universe Date: Thu, 26 Mar 2026 03:13:23 +0900 Subject: [PATCH 30/31] feat(fig2grida): add support for Figma Deck (.deck) files - Introduced handling for .deck files in the Figma import process, allowing for the parsing and conversion of Figma Deck presentations. - Updated file validation and acceptance criteria to include .deck alongside .fig. - Enhanced documentation to reflect the new .deck format and its integration within the existing Figma file handling. - Implemented tests to ensure correct parsing and conversion of .deck files, verifying slide count and hierarchy preservation. - Updated relevant components to accommodate the new slide node types and their properties in the Grida document structure. --- .ref/figma/fig2kiwi.ts | 18 ++- docs/wg/feat-fig/index.md | 20 ++- editor/app/(embed)/embed/v1/debug/page.tsx | 2 +- .../starterkit-import/from-figma.tsx | 7 +- .../grida-canvas-react/use-data-transfer.ts | 7 +- editor/scaffolds/embed/use-refig-editor.ts | 1 + fixtures/.gitattributes | 3 +- fixtures/test-fig/deck/light.deck | 3 + .../__tests__/iofigma.kiwi.fig.test.ts | 80 +++++++++++ .../grida-canvas-io-figma/fig-kiwi/index.ts | 25 +++- packages/grida-canvas-io-figma/fig2grida.ts | 16 ++- packages/grida-canvas-io-figma/lib.ts | 134 +++++++++++++++++- packages/grida-canvas-sdk-render-figma/cli.ts | 8 +- 13 files changed, 294 insertions(+), 30 deletions(-) create mode 100644 fixtures/test-fig/deck/light.deck diff --git a/.ref/figma/fig2kiwi.ts b/.ref/figma/fig2kiwi.ts index fcd0f69cec..bec0275240 100755 --- a/.ref/figma/fig2kiwi.ts +++ b/.ref/figma/fig2kiwi.ts @@ -54,8 +54,12 @@ import { // --- Constants --- +// Kiwi archive preludes: the first 8 bytes of the file (see FigmaArchiveParser.parseArchive). +// Each variant uses a different fixed 8-byte ASCII magic string; FigJam's ends with a literal +// period — it is not a typo and must match bytes on disk. const FIG_KIWI_PRELUDE = "fig-kiwi"; const FIGJAM_KIWI_PRELUDE = "fig-jam."; +const FIGDECK_KIWI_PRELUDE = "fig-deck"; const ZIP_SIGNATURE = [0x50, 0x4b, 0x03, 0x04]; // --- Archive Parser (duplicated from main source) --- @@ -94,7 +98,11 @@ class FigmaArchiveParser { const preludeData = parser.read(FIG_KIWI_PRELUDE.length); const prelude = String.fromCharCode.apply(String, Array.from(preludeData)); - if (prelude !== FIG_KIWI_PRELUDE && prelude !== FIGJAM_KIWI_PRELUDE) { + if ( + prelude !== FIG_KIWI_PRELUDE && + prelude !== FIGJAM_KIWI_PRELUDE && + prelude !== FIGDECK_KIWI_PRELUDE + ) { throw new Error(`Unexpected prelude: "${prelude}"`); } @@ -138,8 +146,12 @@ function readFigFile(data: Uint8Array) { String, Array.from(fileData.slice(0, 8)) ); - return prelude === FIG_KIWI_PRELUDE || prelude === FIGJAM_KIWI_PRELUDE; - }) || keys.find((k) => k.endsWith(".fig")); + return ( + prelude === FIG_KIWI_PRELUDE || + prelude === FIGJAM_KIWI_PRELUDE || + prelude === FIGDECK_KIWI_PRELUDE + ); + }) || keys.find((k) => k.endsWith(".fig") || k.endsWith(".deck")); if (!mainFile) { throw new Error( diff --git a/docs/wg/feat-fig/index.md b/docs/wg/feat-fig/index.md index 7d30c3de84..1f7647e3c6 100644 --- a/docs/wg/feat-fig/index.md +++ b/docs/wg/feat-fig/index.md @@ -6,7 +6,6 @@ tags: - wg - figma - import - --- # .fig Format - `io-figma` @@ -53,7 +52,7 @@ The following capabilities are explicitly **not** in scope: The `.fig` file format uses the Kiwi binary encoding protocol. A typical `.fig` file consists of: -1. **Header** - File prelude (`"fig-kiwi"` or `"fig-jam."`) and version number +1. **Header** - An 8-byte ASCII prelude (magic string) identifying the container, then a little-endian version `u32`. Known preludes are all exactly eight bytes: `"fig-kiwi"` (Figma design files), `"fig-jam."` (FigJam — the trailing `.` is part of the format, not punctuation), and `"fig-deck"` (deck files). 2. **Chunk 1** - Compressed Kiwi schema definition 3. **Chunk 2+** - Compressed scene data encoded using the schema @@ -80,7 +79,6 @@ This is why we maintain the [fig.kiwi schema file](https://github.com/gridaco/gr ### Implementation Strategy 1. **For `.fig` file import:** - - Read schema from chunk 1 - Use embedded schema to decode remaining chunks - More robust to schema changes @@ -106,6 +104,22 @@ See [Kiwi Schema Glossary](./glossary/fig.kiwi.md) for detailed schema documenta - [Figma Inside — .fig file analysis by easylogic](https://easylogic.medium.com/7252bef141da) - [fig2json@GitHub / rust](https://github.com/kreako/fig2json) +## Figma Deck (Slides) Import + +Figma's REST API does **not** expose Deck/Slides node types. They exist only in the `.fig` Kiwi binary format with the `"fig-deck"` prelude. The Kiwi schema defines `SLIDE`, `SLIDE_GRID`, `SLIDE_ROW`, and `INTERACTIVE_SLIDE_ELEMENT` node types. + +Grida handles these through the `iofigma.__ir` intermediate representation: + +| Kiwi type | IR type | Grida output | +| ------------------------------------ | -------------- | ------------ | +| `SLIDE`, `INTERACTIVE_SLIDE_ELEMENT` | `X_SLIDE` | container | +| `SLIDE_GRID` | `X_SLIDE_GRID` | container | +| `SLIDE_ROW` | `X_SLIDE_ROW` | container | + +All three IR types are structurally equivalent to `figrest.FrameNode` (children, fills, clips, layout) and convert to Grida containers — the same output as frames. `X_SLIDE` carries an optional `slideMetadata` bag (speaker notes, skip flag, slide numbering) for future use. + +Full slide semantics (presenter mode, slide indices in UI, theme maps) are out of scope; this mapping ensures FigDeck files produce a complete, visually usable scene graph without dropping slide subtrees. + ## See Also - [WG:SVG](../feat-svg) diff --git a/editor/app/(embed)/embed/v1/debug/page.tsx b/editor/app/(embed)/embed/v1/debug/page.tsx index ead34b490f..479e968b74 100644 --- a/editor/app/(embed)/embed/v1/debug/page.tsx +++ b/editor/app/(embed)/embed/v1/debug/page.tsx @@ -210,7 +210,7 @@ export default function EmbedDebugPage() { { const f = e.target.files?.[0]; diff --git a/editor/grida-canvas-react-starter-kit/starterkit-import/from-figma.tsx b/editor/grida-canvas-react-starter-kit/starterkit-import/from-figma.tsx index ac0976e2ce..e22ffbe890 100644 --- a/editor/grida-canvas-react-starter-kit/starterkit-import/from-figma.tsx +++ b/editor/grida-canvas-react-starter-kit/starterkit-import/from-figma.tsx @@ -178,7 +178,8 @@ function FigFileImportTab({ const parseRunIdRef = useRef(0); const validateFile = (file: File) => { - return file.name.toLowerCase().endsWith(".fig"); + const name = file.name.toLowerCase(); + return name.endsWith(".fig") || name.endsWith(".deck"); }; const handleParse = useCallback(async (file: File, runId: number) => { @@ -258,7 +259,7 @@ function FigFileImportTab({ setParsed(result); setStep("confirm"); } catch (error) { - toast.error("Failed to parse .fig file"); + toast.error("Failed to parse file"); console.error(error); if (!isStale()) { // Mark failure to prevent repeated attempts for the same file @@ -340,7 +341,7 @@ function FigFileImportTab({ [Import Figma] to import .fig files"); continue; } diff --git a/editor/scaffolds/embed/use-refig-editor.ts b/editor/scaffolds/embed/use-refig-editor.ts index d7833f0ef3..a5c2f1e2f8 100644 --- a/editor/scaffolds/embed/use-refig-editor.ts +++ b/editor/scaffolds/embed/use-refig-editor.ts @@ -12,6 +12,7 @@ function validateExt(name: string) { const l = name.toLowerCase(); return ( l.endsWith(".fig") || + l.endsWith(".deck") || l.endsWith(".json") || l.endsWith(".json.gz") || l.endsWith(".zip") diff --git a/fixtures/.gitattributes b/fixtures/.gitattributes index 0eb23d95a0..b243b4a503 100644 --- a/fixtures/.gitattributes +++ b/fixtures/.gitattributes @@ -1 +1,2 @@ -*.fig filter=lfs diff=lfs merge=lfs -text \ No newline at end of file +*.fig filter=lfs diff=lfs merge=lfs -text +*.deck filter=lfs diff=lfs merge=lfs -text \ No newline at end of file diff --git a/fixtures/test-fig/deck/light.deck b/fixtures/test-fig/deck/light.deck new file mode 100644 index 0000000000..b29966bb65 --- /dev/null +++ b/fixtures/test-fig/deck/light.deck @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f230717e027ea0e0e4dd06a9dd4743457ccb42bf0005ebbaa5cfd1b6b1379414 +size 1191797 diff --git a/packages/grida-canvas-io-figma/__tests__/iofigma.kiwi.fig.test.ts b/packages/grida-canvas-io-figma/__tests__/iofigma.kiwi.fig.test.ts index 790ed656a5..cbcab80028 100644 --- a/packages/grida-canvas-io-figma/__tests__/iofigma.kiwi.fig.test.ts +++ b/packages/grida-canvas-io-figma/__tests__/iofigma.kiwi.fig.test.ts @@ -297,4 +297,84 @@ describe("FigImporter", () => { expect(sortedPages.length).toBe(figFile.pages.length); }); }); + + describe("FigDeck (.deck) import", () => { + const deckFixture = + __dirname + "/../../../fixtures/test-fig/deck/light.deck"; + + it("should parse deck file and produce 42 slides", () => { + const data = readFileSync(deckFixture); + const figFile = FigImporter.parseFile(data); + + expect(figFile.pages.length).toBe(1); + + // Count X_SLIDE nodes in the converted tree + function countByType(node: any, type: string): number { + let count = node.type === type ? 1 : 0; + if ("children" in node && Array.isArray(node.children)) { + node.children.forEach((child: any) => { + count += countByType(child, type); + }); + } + return count; + } + + const page = figFile.pages[0]; + let slideCount = 0; + page.rootNodes.forEach((root: any) => { + slideCount += countByType(root, "X_SLIDE"); + }); + + expect(slideCount).toBe(42); + }); + + it("should preserve complete slide hierarchy (no dropped subtrees)", () => { + const data = readFileSync(deckFixture); + const figData = readFigFile(data); + const nodeChanges = figData.message.nodeChanges || []; + + const canvas = nodeChanges.find( + (nc) => nc.type === "CANVAS" && !nc.internalOnly + ); + if (!canvas?.guid) return; + + const canvasGuidStr = iofigma.kiwi.guid(canvas.guid); + const rawCount = countKiwiDescendants(canvasGuidStr, nodeChanges); + + // Count nodes with types that are intentionally unsupported (e.g. SHAPE_WITH_TEXT) + const unsupportedTypes = new Set(["SHAPE_WITH_TEXT"]); + const unsupportedCount = nodeChanges.filter( + (nc) => nc.type && unsupportedTypes.has(nc.type) + ).length; + + const figFile = FigImporter.parseFile(data); + const page = figFile.pages[0]; + + let processedCount = 0; + page.rootNodes.forEach((rootNode: any) => { + processedCount += countNodes(rootNode); + }); + + // All supported nodes must be preserved — only unsupported FigJam-crossover types may be absent + expect(processedCount).toBe(rawCount - unsupportedCount); + }); + + it("should convert deck to Grida document without errors", () => { + const data = readFileSync(deckFixture); + const figFile = FigImporter.parseFile(data); + const { document: packedDoc } = FigImporter.convertPageToScene( + figFile.pages[0], + { gradient_id_generator: () => "test-id" } + ); + + expect(packedDoc.nodes).toBeDefined(); + expect(packedDoc.scene).toBeDefined(); + expect(packedDoc.scene.children_refs.length).toBeGreaterThan(0); + + // All root IDs must exist in nodes + packedDoc.scene.children_refs.forEach((rootId: string) => { + expect(packedDoc.nodes[rootId]).toBeDefined(); + }); + }); + }); }); diff --git a/packages/grida-canvas-io-figma/fig-kiwi/index.ts b/packages/grida-canvas-io-figma/fig-kiwi/index.ts index ec602cae54..44f79daa0c 100644 --- a/packages/grida-canvas-io-figma/fig-kiwi/index.ts +++ b/packages/grida-canvas-io-figma/fig-kiwi/index.ts @@ -54,10 +54,21 @@ export { compileSchema, prettyPrintSchema } from "kiwi-schema"; export type Header = { prelude: string; version: number }; +// First 8 bytes of a Kiwi .fig archive. Each product uses a fixed ASCII magic string; +// FigJam's is `fig-jam.` including the trailing dot (on-wire / on-disk format). const FIG_KIWI_PRELUDE = "fig-kiwi"; const FIGJAM_KIWI_PRELUDE = "fig-jam."; +const FIGDECK_KIWI_PRELUDE = "fig-deck"; const FIG_KIWI_VERSION = 15; +function isKiwiArchivePrelude(prelude: string): boolean { + return ( + prelude === FIG_KIWI_PRELUDE || + prelude === FIGJAM_KIWI_PRELUDE || + prelude === FIGDECK_KIWI_PRELUDE + ); +} + const HTML_MARKERS = { metaStart: "", @@ -128,7 +139,7 @@ export class FigmaArchiveParser { // @ts-ignore: charCode check const prelude = String.fromCharCode.apply(String, preludeData); - if (prelude !== FIG_KIWI_PRELUDE && prelude !== FIGJAM_KIWI_PRELUDE) { + if (!isKiwiArchivePrelude(prelude)) { throw new Error(`Unexpected prelude: "${prelude}"`); } @@ -273,8 +284,8 @@ export function readFigFile(data: Uint8Array): ParsedFigmaArchive { // Check prelude // @ts-ignore: charCode check const prelude = String.fromCharCode.apply(String, fileData.slice(0, 8)); - return prelude === FIG_KIWI_PRELUDE || prelude === FIGJAM_KIWI_PRELUDE; - }) || keys.find((k) => k.endsWith(".fig")); + return isKiwiArchivePrelude(prelude); + }) || keys.find((k) => k.endsWith(".fig") || k.endsWith(".deck")); if (!mainFile) { throw new Error( @@ -360,7 +371,9 @@ export async function readFigFileFromStream( } const total = buffer.reduce((s, c) => s + c.length, 0); if (total < 4) { - throw new Error("readFigFileFromStream: stream too short to detect format"); + throw new Error( + "readFigFileFromStream: stream too short to detect format" + ); } const merged = new Uint8Array(total); let off = 0; @@ -381,8 +394,8 @@ export async function readFigFileFromStream( String, Array.from(fileData.slice(0, 8)) ); - return prelude === FIG_KIWI_PRELUDE || prelude === FIGJAM_KIWI_PRELUDE; - }) || keys.find((k) => k.endsWith(".fig")); + return isKiwiArchivePrelude(prelude); + }) || keys.find((k) => k.endsWith(".fig") || k.endsWith(".deck")); if (!mainFile) { throw new Error( diff --git a/packages/grida-canvas-io-figma/fig2grida.ts b/packages/grida-canvas-io-figma/fig2grida.ts index 5d08f50197..0a639ef041 100644 --- a/packages/grida-canvas-io-figma/fig2grida.ts +++ b/packages/grida-canvas-io-figma/fig2grida.ts @@ -6,6 +6,7 @@ * * Supported input formats: * .fig Figma native binary (Kiwi/ZIP) + * .deck Figma Deck/Slides binary (same format as .fig) * .json Figma REST API JSON response * .json.gz Gzip-compressed REST API JSON * .zip REST API archive ZIP (contains document.json + images) @@ -33,7 +34,7 @@ function printHelp(): void { console.log(` fig2grida — Convert Figma files to .grida archives -Supported inputs: .fig, .json, .json.gz, .zip +Supported inputs: .fig, .deck, .json, .json.gz, .zip Usage: fig2grida [output.grida] @@ -50,6 +51,7 @@ Options: Examples: fig2grida design.fig + fig2grida presentation.deck fig2grida api-response.json.gz output.grida fig2grida design.fig --pages 0,2 --verbose fig2grida design.fig --info @@ -175,14 +177,16 @@ function main(): void { const inputPath = resolve(args.input); const lower = inputPath.toLowerCase(); - // Reject .fig-only flags for REST-format inputs - if (!lower.endsWith(".fig")) { + const isFigLike = lower.endsWith(".fig") || lower.endsWith(".deck"); + + // Reject .fig/.deck-only flags for REST-format inputs + if (!isFigLike) { if (args.info) { - console.error("--info is only supported for .fig input."); + console.error("--info is only supported for .fig/.deck input."); process.exit(1); } if (args.pages) { - console.error("--pages is currently only supported for .fig input."); + console.error("--pages is currently only supported for .fig/.deck input."); process.exit(1); } } @@ -195,7 +199,7 @@ function main(): void { // Strip known extensions to derive the default output name const stripExt = (name: string): string => { - for (const ext of [".json.gz", ".json", ".fig", ".zip"]) { + for (const ext of [".json.gz", ".json", ".fig", ".deck", ".zip"]) { if (name.toLowerCase().endsWith(ext)) { return name.slice(0, -ext.length); } diff --git a/packages/grida-canvas-io-figma/lib.ts b/packages/grida-canvas-io-figma/lib.ts index 67162a1934..014f338a41 100644 --- a/packages/grida-canvas-io-figma/lib.ts +++ b/packages/grida-canvas-io-figma/lib.ts @@ -195,6 +195,45 @@ export namespace iofigma { pointCount: number; innerRadius: number; }; + + /** + * Slide node from Figma Deck (.fig with fig-deck prelude). + * + * - rest-api-spec - Not supported (Figma REST API has no Deck/Slides types) + * - kiwi-spec - SLIDE, INTERACTIVE_SLIDE_ELEMENT + * + * Structurally equivalent to a FrameNode (children, fills, clips, layout). + * INTERACTIVE_SLIDE_ELEMENT is also mapped here since it behaves as a + * frame-like interactive element within a slide. + */ + export type SlideNodeIR = Omit & { + type: "X_SLIDE"; + slideMetadata?: { + speakerNotes?: string; + isSkipped?: boolean; + slideNumber?: string; + }; + }; + + /** + * Slide grid container from Figma Deck. + * + * - rest-api-spec - Not supported + * - kiwi-spec - SLIDE_GRID + */ + export type SlideGridNodeIR = Omit & { + type: "X_SLIDE_GRID"; + }; + + /** + * Slide row container from Figma Deck. + * + * - rest-api-spec - Not supported + * - kiwi-spec - SLIDE_ROW + */ + export type SlideRowNodeIR = Omit & { + type: "X_SLIDE_ROW"; + }; } export namespace restful { @@ -1032,14 +1071,20 @@ export namespace iofigma { | figrest.BooleanOperationNode | figrest.InstanceNode | figrest.FrameNode - | figrest.GroupNode; + | figrest.GroupNode + | __ir.SlideNodeIR + | __ir.SlideGridNodeIR + | __ir.SlideRowNodeIR; type InputNode = | (figrest.SubcanvasNode & Partial<__ir.HasLayoutTraitIR>) | __ir.VectorNodeRestInput | __ir.VectorNodeWithVectorNetworkDataPresent | __ir.StarNodeWithPointsDataPresent - | __ir.RegularPolygonNodeWithPointsDataPresent; + | __ir.RegularPolygonNodeWithPointsDataPresent + | __ir.SlideNodeIR + | __ir.SlideGridNodeIR + | __ir.SlideRowNodeIR; export function document( node: InputNode, @@ -1549,7 +1594,11 @@ export namespace iofigma { case "FRAME": // Fallback: treat COMPONENT_SET as FRAME for rendering. Grida does not yet // support component semantics; proper variant/swap support to be added later. - case "COMPONENT_SET": { + case "COMPONENT_SET": + // Slide IR types (Figma Deck) — structurally identical to frames + case "X_SLIDE": + case "X_SLIDE_GRID": + case "X_SLIDE_ROW": { return { id: gridaId, ...base_node_trait(node), @@ -2632,6 +2681,75 @@ export namespace iofigma { } satisfies figrest.FrameNode; } + /** + * Convert Kiwi SLIDE / INTERACTIVE_SLIDE_ELEMENT to X_SLIDE IR. + * Reuses the same trait pipeline as frame(). + */ + function slide( + nc: figkiwi.NodeChange + ): __ir.SlideNodeIR | undefined { + if (!nc.guid || !nc.name || !nc.size) return undefined; + return { + ...kiwi_is_layer_trait(nc, "FRAME"), + ...kiwi_blend_opacity_trait(nc), + ...kiwi_layout_trait(nc), + ...kiwi_geometry_trait(nc), + ...kiwi_corner_trait(nc), + ...kiwi_frame_clip_trait(nc), + ...kiwi_children_trait(), + ...kiwi_effects_trait(nc), + ...kiwi_has_export_settings_trait(nc), + type: "X_SLIDE", + slideMetadata: { + speakerNotes: nc.slideSpeakerNotes ?? undefined, + isSkipped: nc.isSkippedSlide ?? undefined, + slideNumber: nc.slideNumber ?? undefined, + }, + } as __ir.SlideNodeIR; + } + + /** + * Convert Kiwi SLIDE_GRID to X_SLIDE_GRID IR. + */ + function slideGrid( + nc: figkiwi.NodeChange + ): __ir.SlideGridNodeIR | undefined { + if (!nc.guid || !nc.name || !nc.size) return undefined; + return { + ...kiwi_is_layer_trait(nc, "FRAME"), + ...kiwi_blend_opacity_trait(nc), + ...kiwi_layout_trait(nc), + ...kiwi_geometry_trait(nc), + ...kiwi_corner_trait(nc), + ...kiwi_frame_clip_trait(nc), + ...kiwi_children_trait(), + ...kiwi_effects_trait(nc), + ...kiwi_has_export_settings_trait(nc), + type: "X_SLIDE_GRID", + } as __ir.SlideGridNodeIR; + } + + /** + * Convert Kiwi SLIDE_ROW to X_SLIDE_ROW IR. + */ + function slideRow( + nc: figkiwi.NodeChange + ): __ir.SlideRowNodeIR | undefined { + if (!nc.guid || !nc.name || !nc.size) return undefined; + return { + ...kiwi_is_layer_trait(nc, "FRAME"), + ...kiwi_blend_opacity_trait(nc), + ...kiwi_layout_trait(nc), + ...kiwi_geometry_trait(nc), + ...kiwi_corner_trait(nc), + ...kiwi_frame_clip_trait(nc), + ...kiwi_children_trait(), + ...kiwi_effects_trait(nc), + ...kiwi_has_export_settings_trait(nc), + type: "X_SLIDE_ROW", + } as __ir.SlideRowNodeIR; + } + /** * Convert NodeChange to SECTION node */ @@ -2995,6 +3113,9 @@ export namespace iofigma { | __ir.VectorNodeWithVectorNetworkDataPresent | __ir.StarNodeWithPointsDataPresent | __ir.RegularPolygonNodeWithPointsDataPresent + | __ir.SlideNodeIR + | __ir.SlideGridNodeIR + | __ir.SlideRowNodeIR | undefined { if (!nodeChange.type) return undefined; @@ -3026,6 +3147,13 @@ export namespace iofigma { return star(nodeChange); case "BOOLEAN_OPERATION": return booleanOperation(nodeChange); + case "SLIDE": + case "INTERACTIVE_SLIDE_ELEMENT": + return slide(nodeChange); + case "SLIDE_GRID": + return slideGrid(nodeChange); + case "SLIDE_ROW": + return slideRow(nodeChange); default: return undefined; } diff --git a/packages/grida-canvas-sdk-render-figma/cli.ts b/packages/grida-canvas-sdk-render-figma/cli.ts index f27f6b91ed..a984a93e37 100644 --- a/packages/grida-canvas-sdk-render-figma/cli.ts +++ b/packages/grida-canvas-sdk-render-figma/cli.ts @@ -207,7 +207,9 @@ async function runExportAll( fontsDir?: string, skipDefaultFonts?: boolean ): Promise { - const isFig = documentPath.toLowerCase().endsWith(".fig"); + const isFig = + documentPath.toLowerCase().endsWith(".fig") || + documentPath.toLowerCase().endsWith(".deck"); let document: FigmaDocument; let items: ExportItem[]; let rendererOptions: { @@ -319,7 +321,9 @@ async function runSingleNode( JSON.parse(readFileSync(documentPath, "utf8")) ); } else { - const isFig = documentPath.toLowerCase().endsWith(".fig"); + const isFig = + documentPath.toLowerCase().endsWith(".fig") || + documentPath.toLowerCase().endsWith(".deck"); const useStreaming = isFig && statSync(documentPath).size >= LARGE_FILE_THRESHOLD; if (useStreaming) { From 8d0885576e86fa57bdd3ef037dce99b3084979a6 Mon Sep 17 00:00:00 2001 From: Universe Date: Thu, 26 Mar 2026 03:35:08 +0900 Subject: [PATCH 31/31] fix(grida-canvas): fix 5 bugs found during PR #601 review - switch_scene: clone scene instead of swap_remove to preserve loaded scenes list - geometry cache: return world_bounds for TextSpan (consistent with Leaf branch) - frame loop: fix unreachable overlay-only fast path by checking camera_change directly - scene_graph: use actual TextSpan effects instead of hardcoded default - effect_tree: only allocate shadow surface when at least one shadow is active Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/grida-canvas/src/cache/geometry.rs | 2 +- crates/grida-canvas/src/node/scene_graph.rs | 6 ++- .../grida-canvas/src/runtime/effect_tree.rs | 39 ++++++------------- crates/grida-canvas/src/window/application.rs | 4 +- 4 files changed, 19 insertions(+), 32 deletions(-) diff --git a/crates/grida-canvas/src/cache/geometry.rs b/crates/grida-canvas/src/cache/geometry.rs index e1196ba601..9dabf8db63 100644 --- a/crates/grida-canvas/src/cache/geometry.rs +++ b/crates/grida-canvas/src/cache/geometry.rs @@ -424,7 +424,7 @@ impl GeometryCache { dirty_transform: false, dirty_bounds: false, }; - let bounds = local_bounds; + let bounds = world_bounds; cache.entries.insert(*id, entry); bounds } diff --git a/crates/grida-canvas/src/node/scene_graph.rs b/crates/grida-canvas/src/node/scene_graph.rs index c45ad4483c..38468c541f 100644 --- a/crates/grida-canvas/src/node/scene_graph.rs +++ b/crates/grida-canvas/src/node/scene_graph.rs @@ -1,5 +1,7 @@ use super::repository::NodeRepository; -use super::schema::{extract_layer_core, Node, NodeGeometryMixin, NodeId, NodeLayerCore, NodeRectMixin}; +use super::schema::{ + extract_layer_core, Node, NodeGeometryMixin, NodeId, NodeLayerCore, NodeRectMixin, +}; use crate::cache::fast_hash::DenseNodeMap; use crate::cg::prelude::*; use math2::transform::AffineTransform; @@ -206,7 +208,7 @@ pub fn extract_geo_data(node: &Node) -> NodeGeoData { render_bounds_inflation: compute_inflation_uniform( n.stroke_width, n.stroke_align, - &super::schema::LayerEffects::default(), + &n.effects, ), rotation: 0.0, }, diff --git a/crates/grida-canvas/src/runtime/effect_tree.rs b/crates/grida-canvas/src/runtime/effect_tree.rs index 294349f3dd..e73f9d98f7 100644 --- a/crates/grida-canvas/src/runtime/effect_tree.rs +++ b/crates/grida-canvas/src/runtime/effect_tree.rs @@ -203,17 +203,13 @@ impl EffectTree { let layer_core_map = graph.layer_core(); let visible_child_count = all_children_slice .iter() - .filter(|cid| { - layer_core_map - .get(cid) - .map(|c| c.active) - .unwrap_or(false) - }) + .filter(|cid| layer_core_map.get(cid).map(|c| c.active).unwrap_or(false)) .count(); // Collect render surface reasons using layer_core for fast checks. // Only access full Node when effects detail is needed. - let reasons = Self::classify_from_core(id, &lc, visible_child_count, all_children_slice, graph); + let reasons = + Self::classify_from_core(id, &lc, visible_child_count, all_children_slice, graph); if !reasons.is_empty() { for reason in &reasons { @@ -230,12 +226,7 @@ impl EffectTree { // Only allocate the children Vec for nodes that actually need a surface. let active_children: Vec = all_children_slice .iter() - .filter(|cid| { - layer_core_map - .get(cid) - .map(|c| c.active) - .unwrap_or(false) - }) + .filter(|cid| layer_core_map.get(cid).map(|c| c.active).unwrap_or(false)) .copied() .collect(); @@ -289,7 +280,7 @@ impl EffectTree { if effects.blur.as_ref().is_some_and(|b| b.active) { reasons.push(RenderSurfaceReason::LayerBlur); } - if !effects.shadows.is_empty() { + if effects.shadows.iter().any(|s| s.active()) { reasons.push(RenderSurfaceReason::Shadow); } } @@ -494,10 +485,7 @@ mod tests { crate::node::scene_graph::Parent::Root, ); for _ in 0..5 { - graph.append_child( - rect_node(), - crate::node::scene_graph::Parent::NodeId(root), - ); + graph.append_child(rect_node(), crate::node::scene_graph::Parent::NodeId(root)); } let tree = EffectTree::build(&graph); @@ -788,20 +776,17 @@ mod tests { ); // Two children under each - graph.append_child( - rect_node(), - crate::node::scene_graph::Parent::NodeId(root), - ); - graph.append_child( - rect_node(), - crate::node::scene_graph::Parent::NodeId(inner), - ); + graph.append_child(rect_node(), crate::node::scene_graph::Parent::NodeId(root)); + graph.append_child(rect_node(), crate::node::scene_graph::Parent::NodeId(inner)); let tree = EffectTree::build(&graph); // Root needs surface for opacity (2 visible children: inner + rect). assert!(tree.needs_surface(&root)); - assert!(tree.get(&root).unwrap().has_reason(RenderSurfaceReason::Opacity)); + assert!(tree + .get(&root) + .unwrap() + .has_reason(RenderSurfaceReason::Opacity)); // Inner needs surface for clip (1 visible child). assert!(tree.needs_surface(&inner)); diff --git a/crates/grida-canvas/src/window/application.rs b/crates/grida-canvas/src/window/application.rs index 1313f3a8d7..c6f99e6530 100644 --- a/crates/grida-canvas/src/window/application.rs +++ b/crates/grida-canvas/src/window/application.rs @@ -661,7 +661,7 @@ impl ApplicationApi for UnknownTargetApplication { fn switch_scene(&mut self, scene_id: &str) { if let Some(pos) = self.loaded_scenes.iter().position(|(id, _)| id == scene_id) { - let (_, scene) = self.loaded_scenes.swap_remove(pos); + let (_, scene) = self.loaded_scenes[pos].clone(); self.renderer.load_scene(scene); self.queue(); } else { @@ -1163,7 +1163,7 @@ impl UnknownTargetApplication { // to the previous frame. Restore it from the pan image cache and // skip the expensive frame-plan build + full draw. The overlay is // still re-drawn below so marquee/selection visuals update correctly. - if !stable && !content_changed && self.renderer.blit_content_cache() { + if !content_changed && !camera_change.any_changed() && self.renderer.blit_content_cache() { // Consume the camera change (no-op here, but keeps the contract). self.renderer.camera.consume_change();