From 848a8c682abdabe1a5cc16e45deacbbab9a10993 Mon Sep 17 00:00:00 2001 From: RtlZeroMemory <58250858+RtlZeroMemory@users.noreply.github.com> Date: Thu, 5 Mar 2026 23:22:35 +0400 Subject: [PATCH 1/5] Fix native worker-thread teardown stability --- packages/native/loader.cjs | 11 - packages/native/scripts/smoke-worker.mjs | 37 +- packages/native/scripts/smoke.mjs | 26 + packages/native/src/lib.rs | 4330 +++++++++-------- .../node/src/__tests__/config_guards.test.ts | 6 +- .../src/__tests__/worker_integration.test.ts | 6 +- packages/node/src/backend/nodeBackend.ts | 10 +- 7 files changed, 2359 insertions(+), 2067 deletions(-) diff --git a/packages/native/loader.cjs b/packages/native/loader.cjs index 6af8cb3c..01774033 100644 --- a/packages/native/loader.cjs +++ b/packages/native/loader.cjs @@ -1,15 +1,4 @@ const { readdirSync } = require("node:fs"); -const { isMainThread } = require("node:worker_threads"); - -if (!isMainThread && process.env.REZI_NATIVE_ALLOW_WORKER_THREADS !== "1") { - throw new Error( - [ - "@rezi-ui/native does not support worker_threads execution.", - 'Use `executionMode: "inline"` for native runtime, or provide `nativeShimModule` in worker test harnesses.', - "Set REZI_NATIVE_ALLOW_WORKER_THREADS=1 only for low-level debugging.", - ].join(" "), - ); -} let native = null; let lastErr = null; diff --git a/packages/native/scripts/smoke-worker.mjs b/packages/native/scripts/smoke-worker.mjs index c9126e39..69edc7b9 100644 --- a/packages/native/scripts/smoke-worker.mjs +++ b/packages/native/scripts/smoke-worker.mjs @@ -12,20 +12,25 @@ if (!parentPort) { throw new Error("smoke-worker: missing parentPort"); } -const res1 = enginePresent(engineId); -const res2 = enginePostUserEvent(engineId, 123, new Uint8Array([1, 2, 3])); -const res3 = engineSetConfig(engineId, { targetFps: 33 }); -const res4 = engineDebugDisable(engineId); -parentPort.postMessage({ - phase: "alive", - present: res1, - postUserEvent: res2, - setConfig: res3, - debugDisable: res4, -}); +if (workerData?.phase === "loadOnly") { + parentPort.postMessage({ phase: "loadOnly" }); + parentPort.close(); +} else { + const res1 = enginePresent(engineId); + const res2 = enginePostUserEvent(engineId, 123, new Uint8Array([1, 2, 3])); + const res3 = engineSetConfig(engineId, { targetFps: 33 }); + const res4 = engineDebugDisable(engineId); + parentPort.postMessage({ + phase: "alive", + present: res1, + postUserEvent: res2, + setConfig: res3, + debugDisable: res4, + }); -parentPort.on("message", (msg) => { - if (msg?.type !== "afterDestroy") return; - const res = enginePostUserEvent(engineId, 456, new Uint8Array([9])); - parentPort.postMessage({ phase: "destroyed", postUserEvent: res }); -}); + parentPort.on("message", (msg) => { + if (msg?.type !== "afterDestroy") return; + const res = enginePostUserEvent(engineId, 456, new Uint8Array([9])); + parentPort.postMessage({ phase: "destroyed", postUserEvent: res }); + }); +} diff --git a/packages/native/scripts/smoke.mjs b/packages/native/scripts/smoke.mjs index 8f9a9309..3a3f22d0 100644 --- a/packages/native/scripts/smoke.mjs +++ b/packages/native/scripts/smoke.mjs @@ -72,6 +72,32 @@ function readU64LE(bytes, offset) { return dv.getBigUint64(offset, true); } +async function assertWorkerLoadExitCleanly() { + const worker = new Worker(new URL("./smoke-worker.mjs", import.meta.url), { + workerData: { phase: "loadOnly" }, + type: "module", + }); + + const loadResult = await new Promise((resolve, reject) => { + const onExit = (code) => reject(new Error(`load-only worker exited with ${code}`)); + const onError = (err) => reject(err); + const onMessage = (msg) => { + worker.off("exit", onExit); + worker.off("error", onError); + resolve(msg); + }; + worker.once("exit", onExit); + worker.once("error", onError); + worker.once("message", onMessage); + }); + + assert(loadResult.phase === "loadOnly", "worker load-only phase must complete"); + const exitCode = await new Promise((resolve) => worker.once("exit", resolve)); + assert(exitCode === 0, `load-only worker must exit cleanly, got: ${String(exitCode)}`); +} + +await assertWorkerLoadExitCleanly(); + // Unknown / stale id behavior (result-returning functions). assert( enginePresent(0) === ZR_ERR_INVALID_ARGUMENT, diff --git a/packages/native/src/lib.rs b/packages/native/src/lib.rs index ccf946a4..ca520baa 100644 --- a/packages/native/src/lib.rs +++ b/packages/native/src/lib.rs @@ -2,1207 +2,1383 @@ use napi::bindgen_prelude::{BigInt, Error, Status, Uint8Array, ValueType}; use napi::{Env, JsObject, JsUnknown}; -use napi_derive::napi; +use napi_derive::{module_exports, napi}; use std::collections::HashMap; -use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; use std::sync::{Condvar, Mutex, OnceLock}; +use std::thread::ThreadId; type ParseResult = std::result::Result; #[allow(dead_code)] mod ffi { - pub type ZrResultT = i32; - - pub const ZR_OK: ZrResultT = 0; - pub const ZR_ERR_INVALID_ARGUMENT: ZrResultT = -1; - pub const ZR_ERR_LIMIT: ZrResultT = -3; - pub const ZR_ERR_PLATFORM: ZrResultT = -6; - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_limits_t { - pub arena_max_total_bytes: u32, - pub arena_initial_bytes: u32, - pub out_max_bytes_per_frame: u32, - pub dl_max_total_bytes: u32, - pub dl_max_cmds: u32, - pub dl_max_strings: u32, - pub dl_max_blobs: u32, - pub dl_max_clip_depth: u32, - pub dl_max_text_run_segments: u32, - pub diff_max_damage_rects: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct plat_config_t { - pub requested_color_mode: u8, - pub enable_mouse: u8, - pub enable_bracketed_paste: u8, - pub enable_focus_events: u8, - pub enable_osc52: u8, - pub _pad: [u8; 3], - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_engine_config_t { - pub requested_engine_abi_major: u32, - pub requested_engine_abi_minor: u32, - pub requested_engine_abi_patch: u32, - pub requested_drawlist_version: u32, - pub requested_event_batch_version: u32, - pub limits: zr_limits_t, - pub plat: plat_config_t, - pub tab_width: u32, - pub width_policy: u32, - pub target_fps: u32, - pub enable_scroll_optimizations: u8, - pub enable_debug_overlay: u8, - pub enable_replay_recording: u8, - pub wait_for_output_drain: u8, - pub cap_force_flags: u32, - pub cap_suppress_flags: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_engine_runtime_config_t { - pub limits: zr_limits_t, - pub plat: plat_config_t, - pub tab_width: u32, - pub width_policy: u32, - pub target_fps: u32, - pub enable_scroll_optimizations: u8, - pub enable_debug_overlay: u8, - pub enable_replay_recording: u8, - pub wait_for_output_drain: u8, - pub cap_force_flags: u32, - pub cap_suppress_flags: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_metrics_t { - pub struct_size: u32, - pub negotiated_engine_abi_major: u32, - pub negotiated_engine_abi_minor: u32, - pub negotiated_engine_abi_patch: u32, - pub negotiated_drawlist_version: u32, - pub negotiated_event_batch_version: u32, - pub frame_index: u64, - pub fps: u32, - pub _pad0: u32, - pub bytes_emitted_total: u64, - pub bytes_emitted_last_frame: u32, - pub _pad1: u32, - pub dirty_lines_last_frame: u32, - pub dirty_cols_last_frame: u32, - pub us_input_last_frame: u32, - pub us_drawlist_last_frame: u32, - pub us_diff_last_frame: u32, - pub us_write_last_frame: u32, - pub events_out_last_poll: u32, - pub events_dropped_total: u32, - pub arena_frame_high_water_bytes: u64, - pub arena_persistent_high_water_bytes: u64, - // v2 damage summary fields - pub damage_rects_last_frame: u32, - pub damage_cells_last_frame: u32, - pub damage_full_frame: u8, - pub _pad2: [u8; 3], - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_terminal_caps_t { - pub color_mode: u8, - pub supports_mouse: u8, - pub supports_bracketed_paste: u8, - pub supports_focus_events: u8, - pub supports_osc52: u8, - pub supports_sync_update: u8, - pub supports_scroll_region: u8, - pub supports_cursor_shape: u8, - pub supports_output_wait_writable: u8, - pub supports_underline_styles: u8, - pub supports_colored_underlines: u8, - pub supports_hyperlinks: u8, - pub sgr_attrs_supported: u32, - pub terminal_id: u32, - pub _pad1: [u8; 3], - pub cap_flags: u32, - pub cap_force_flags: u32, - pub cap_suppress_flags: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct plat_caps_t { - pub color_mode: u8, - pub supports_mouse: u8, - pub supports_bracketed_paste: u8, - pub supports_focus_events: u8, - pub supports_osc52: u8, - pub supports_sync_update: u8, - pub supports_scroll_region: u8, - pub supports_cursor_shape: u8, - pub supports_output_wait_writable: u8, - pub supports_underline_styles: u8, - pub supports_colored_underlines: u8, - pub supports_hyperlinks: u8, - pub sgr_attrs_supported: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_style_t { - pub fg_rgb: u32, - pub bg_rgb: u32, - pub attrs: u32, - pub reserved: u32, - pub underline_rgb: u32, - pub link_ref: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_cell_t { - pub glyph: [u8; 32], - pub glyph_len: u8, - pub width: u8, - pub _pad0: u16, - pub style: zr_style_t, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_rect_t { - pub x: i32, - pub y: i32, - pub w: i32, - pub h: i32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_fb_t { - pub cols: u32, - pub rows: u32, - pub cells: *mut zr_cell_t, - pub links: *mut zr_fb_link_t, - pub links_len: u32, - pub links_cap: u32, - pub link_bytes: *mut u8, - pub link_bytes_len: u32, - pub link_bytes_cap: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_fb_link_t { - pub uri_off: u32, - pub uri_len: u32, - pub id_off: u32, - pub id_len: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_fb_painter_t { - pub fb: *mut zr_fb_t, - pub clip_stack: *mut zr_rect_t, - pub clip_cap: u32, - pub clip_len: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_cursor_state_t { - pub x: i32, - pub y: i32, - pub shape: u8, - pub visible: u8, - pub blink: u8, - pub reserved0: u8, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_term_state_t { - pub cursor_x: u32, - pub cursor_y: u32, - pub cursor_visible: u8, - pub cursor_shape: u8, - pub cursor_blink: u8, - pub flags: u8, - pub style: zr_style_t, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_diff_stats_t { - pub dirty_lines: u32, - pub dirty_cells: u32, - pub damage_rects: u32, - pub damage_cells: u32, - pub damage_full_frame: u8, - pub path_sweep_used: u8, - pub path_damage_used: u8, - pub scroll_opt_attempted: u8, - pub scroll_opt_hit: u8, - pub collision_guard_hits: u32, - pub _pad0: u32, - pub bytes_emitted: usize, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_damage_rect_t { - pub x0: u32, - pub y0: u32, - pub x1: u32, - pub y1: u32, - } - - #[repr(C)] - pub struct zr_engine_t { - _private: [u8; 0], - } - - // Debug trace structures - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_debug_config_t { - pub enabled: u32, - pub ring_capacity: u32, - pub min_severity: u32, - pub category_mask: u32, - pub capture_raw_events: u32, - pub capture_drawlist_bytes: u32, - pub _pad0: u32, - pub _pad1: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_debug_query_t { - pub min_record_id: u64, - pub max_record_id: u64, - pub min_frame_id: u64, - pub max_frame_id: u64, - pub category_mask: u32, - pub min_severity: u32, - pub max_records: u32, - pub _pad0: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_debug_record_header_t { - pub record_id: u64, - pub timestamp_us: u64, - pub frame_id: u64, - pub category: u32, - pub severity: u32, - pub code: u32, - pub payload_size: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_debug_query_result_t { - pub records_returned: u32, - pub records_available: u32, - pub oldest_record_id: u64, - pub newest_record_id: u64, - pub records_dropped: u32, - pub _pad0: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_debug_stats_t { - pub total_records: u64, - pub total_dropped: u64, - pub error_count: u32, - pub warn_count: u32, - pub current_ring_usage: u32, - pub ring_capacity: u32, - } - - extern "C" { - pub fn zr_engine_config_default() -> zr_engine_config_t; - pub fn zr_fb_init(fb: *mut zr_fb_t, cols: u32, rows: u32) -> ZrResultT; - pub fn zr_fb_release(fb: *mut zr_fb_t); - pub fn zr_fb_cell(fb: *mut zr_fb_t, x: u32, y: u32) -> *mut zr_cell_t; - pub fn zr_fb_clear(fb: *mut zr_fb_t, style: *const zr_style_t) -> ZrResultT; - pub fn zr_fb_links_clone_from(dst: *mut zr_fb_t, src: *const zr_fb_t) -> ZrResultT; - pub fn zr_fb_link_intern( - fb: *mut zr_fb_t, - uri: *const u8, - uri_len: usize, - id: *const u8, - id_len: usize, - out_link_ref: *mut u32, - ) -> ZrResultT; - pub fn zr_fb_link_lookup( - fb: *const zr_fb_t, - link_ref: u32, - out_uri: *mut *const u8, - out_uri_len: *mut usize, - out_id: *mut *const u8, - out_id_len: *mut usize, - ) -> ZrResultT; - pub fn zr_fb_painter_begin( - p: *mut zr_fb_painter_t, - fb: *mut zr_fb_t, - clip_stack: *mut zr_rect_t, - clip_cap: u32, - ) -> ZrResultT; - pub fn zr_fb_clip_push(p: *mut zr_fb_painter_t, clip: zr_rect_t) -> ZrResultT; - pub fn zr_fb_clip_pop(p: *mut zr_fb_painter_t) -> ZrResultT; - pub fn zr_fb_put_grapheme( - p: *mut zr_fb_painter_t, - x: i32, - y: i32, - bytes: *const u8, - len: usize, - width: u8, - style: *const zr_style_t, - ) -> ZrResultT; - pub fn zr_diff_render( - prev: *const zr_fb_t, - next: *const zr_fb_t, - caps: *const plat_caps_t, - initial_term_state: *const zr_term_state_t, - desired_cursor_state: *const zr_cursor_state_t, - lim: *const zr_limits_t, - scratch_damage_rects: *mut zr_damage_rect_t, - scratch_damage_rect_cap: u32, - enable_scroll_optimizations: u8, - out_buf: *mut u8, - out_cap: usize, - out_len: *mut usize, - out_final_term_state: *mut zr_term_state_t, - out_stats: *mut zr_diff_stats_t, - ) -> ZrResultT; - - pub fn engine_create(out_engine: *mut *mut zr_engine_t, cfg: *const zr_engine_config_t) -> ZrResultT; - pub fn engine_destroy(e: *mut zr_engine_t); - - pub fn engine_poll_events(e: *mut zr_engine_t, timeout_ms: i32, out_buf: *mut u8, out_cap: i32) -> i32; - pub fn engine_post_user_event(e: *mut zr_engine_t, tag: u32, payload: *const u8, payload_len: i32) -> ZrResultT; - - pub fn engine_submit_drawlist(e: *mut zr_engine_t, bytes: *const u8, bytes_len: i32) -> ZrResultT; - pub fn engine_present(e: *mut zr_engine_t) -> ZrResultT; - - pub fn engine_get_metrics(e: *mut zr_engine_t, out_metrics: *mut zr_metrics_t) -> ZrResultT; - pub fn engine_get_caps(e: *mut zr_engine_t, out_caps: *mut zr_terminal_caps_t) -> ZrResultT; - pub fn engine_set_config(e: *mut zr_engine_t, cfg: *const zr_engine_runtime_config_t) -> ZrResultT; - - // Debug trace API - pub fn engine_debug_enable(e: *mut zr_engine_t, config: *const zr_debug_config_t) -> ZrResultT; - pub fn engine_debug_disable(e: *mut zr_engine_t); - pub fn engine_debug_query( - e: *mut zr_engine_t, - query: *const zr_debug_query_t, - out_headers: *mut zr_debug_record_header_t, - out_headers_cap: u32, - out_result: *mut zr_debug_query_result_t, - ) -> ZrResultT; - pub fn engine_debug_get_payload( - e: *mut zr_engine_t, - record_id: u64, - out_payload: *mut u8, - out_cap: u32, - out_size: *mut u32, - ) -> ZrResultT; - pub fn engine_debug_get_stats(e: *mut zr_engine_t, out_stats: *mut zr_debug_stats_t) -> ZrResultT; - pub fn engine_debug_export(e: *mut zr_engine_t, out_buf: *mut u8, out_cap: usize) -> i32; - pub fn engine_debug_reset(e: *mut zr_engine_t); - } + pub type ZrResultT = i32; + + pub const ZR_OK: ZrResultT = 0; + pub const ZR_ERR_INVALID_ARGUMENT: ZrResultT = -1; + pub const ZR_ERR_LIMIT: ZrResultT = -3; + pub const ZR_ERR_PLATFORM: ZrResultT = -6; + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_limits_t { + pub arena_max_total_bytes: u32, + pub arena_initial_bytes: u32, + pub out_max_bytes_per_frame: u32, + pub dl_max_total_bytes: u32, + pub dl_max_cmds: u32, + pub dl_max_strings: u32, + pub dl_max_blobs: u32, + pub dl_max_clip_depth: u32, + pub dl_max_text_run_segments: u32, + pub diff_max_damage_rects: u32, + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct plat_config_t { + pub requested_color_mode: u8, + pub enable_mouse: u8, + pub enable_bracketed_paste: u8, + pub enable_focus_events: u8, + pub enable_osc52: u8, + pub _pad: [u8; 3], + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_engine_config_t { + pub requested_engine_abi_major: u32, + pub requested_engine_abi_minor: u32, + pub requested_engine_abi_patch: u32, + pub requested_drawlist_version: u32, + pub requested_event_batch_version: u32, + pub limits: zr_limits_t, + pub plat: plat_config_t, + pub tab_width: u32, + pub width_policy: u32, + pub target_fps: u32, + pub enable_scroll_optimizations: u8, + pub enable_debug_overlay: u8, + pub enable_replay_recording: u8, + pub wait_for_output_drain: u8, + pub cap_force_flags: u32, + pub cap_suppress_flags: u32, + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_engine_runtime_config_t { + pub limits: zr_limits_t, + pub plat: plat_config_t, + pub tab_width: u32, + pub width_policy: u32, + pub target_fps: u32, + pub enable_scroll_optimizations: u8, + pub enable_debug_overlay: u8, + pub enable_replay_recording: u8, + pub wait_for_output_drain: u8, + pub cap_force_flags: u32, + pub cap_suppress_flags: u32, + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_metrics_t { + pub struct_size: u32, + pub negotiated_engine_abi_major: u32, + pub negotiated_engine_abi_minor: u32, + pub negotiated_engine_abi_patch: u32, + pub negotiated_drawlist_version: u32, + pub negotiated_event_batch_version: u32, + pub frame_index: u64, + pub fps: u32, + pub _pad0: u32, + pub bytes_emitted_total: u64, + pub bytes_emitted_last_frame: u32, + pub _pad1: u32, + pub dirty_lines_last_frame: u32, + pub dirty_cols_last_frame: u32, + pub us_input_last_frame: u32, + pub us_drawlist_last_frame: u32, + pub us_diff_last_frame: u32, + pub us_write_last_frame: u32, + pub events_out_last_poll: u32, + pub events_dropped_total: u32, + pub arena_frame_high_water_bytes: u64, + pub arena_persistent_high_water_bytes: u64, + // v2 damage summary fields + pub damage_rects_last_frame: u32, + pub damage_cells_last_frame: u32, + pub damage_full_frame: u8, + pub _pad2: [u8; 3], + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_terminal_caps_t { + pub color_mode: u8, + pub supports_mouse: u8, + pub supports_bracketed_paste: u8, + pub supports_focus_events: u8, + pub supports_osc52: u8, + pub supports_sync_update: u8, + pub supports_scroll_region: u8, + pub supports_cursor_shape: u8, + pub supports_output_wait_writable: u8, + pub supports_underline_styles: u8, + pub supports_colored_underlines: u8, + pub supports_hyperlinks: u8, + pub sgr_attrs_supported: u32, + pub terminal_id: u32, + pub _pad1: [u8; 3], + pub cap_flags: u32, + pub cap_force_flags: u32, + pub cap_suppress_flags: u32, + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct plat_caps_t { + pub color_mode: u8, + pub supports_mouse: u8, + pub supports_bracketed_paste: u8, + pub supports_focus_events: u8, + pub supports_osc52: u8, + pub supports_sync_update: u8, + pub supports_scroll_region: u8, + pub supports_cursor_shape: u8, + pub supports_output_wait_writable: u8, + pub supports_underline_styles: u8, + pub supports_colored_underlines: u8, + pub supports_hyperlinks: u8, + pub sgr_attrs_supported: u32, + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_style_t { + pub fg_rgb: u32, + pub bg_rgb: u32, + pub attrs: u32, + pub reserved: u32, + pub underline_rgb: u32, + pub link_ref: u32, + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_cell_t { + pub glyph: [u8; 32], + pub glyph_len: u8, + pub width: u8, + pub _pad0: u16, + pub style: zr_style_t, + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_rect_t { + pub x: i32, + pub y: i32, + pub w: i32, + pub h: i32, + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_fb_t { + pub cols: u32, + pub rows: u32, + pub cells: *mut zr_cell_t, + pub links: *mut zr_fb_link_t, + pub links_len: u32, + pub links_cap: u32, + pub link_bytes: *mut u8, + pub link_bytes_len: u32, + pub link_bytes_cap: u32, + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_fb_link_t { + pub uri_off: u32, + pub uri_len: u32, + pub id_off: u32, + pub id_len: u32, + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_fb_painter_t { + pub fb: *mut zr_fb_t, + pub clip_stack: *mut zr_rect_t, + pub clip_cap: u32, + pub clip_len: u32, + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_cursor_state_t { + pub x: i32, + pub y: i32, + pub shape: u8, + pub visible: u8, + pub blink: u8, + pub reserved0: u8, + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_term_state_t { + pub cursor_x: u32, + pub cursor_y: u32, + pub cursor_visible: u8, + pub cursor_shape: u8, + pub cursor_blink: u8, + pub flags: u8, + pub style: zr_style_t, + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_diff_stats_t { + pub dirty_lines: u32, + pub dirty_cells: u32, + pub damage_rects: u32, + pub damage_cells: u32, + pub damage_full_frame: u8, + pub path_sweep_used: u8, + pub path_damage_used: u8, + pub scroll_opt_attempted: u8, + pub scroll_opt_hit: u8, + pub collision_guard_hits: u32, + pub _pad0: u32, + pub bytes_emitted: usize, + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_damage_rect_t { + pub x0: u32, + pub y0: u32, + pub x1: u32, + pub y1: u32, + } + + #[repr(C)] + pub struct zr_engine_t { + _private: [u8; 0], + } + + // Debug trace structures + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_debug_config_t { + pub enabled: u32, + pub ring_capacity: u32, + pub min_severity: u32, + pub category_mask: u32, + pub capture_raw_events: u32, + pub capture_drawlist_bytes: u32, + pub _pad0: u32, + pub _pad1: u32, + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_debug_query_t { + pub min_record_id: u64, + pub max_record_id: u64, + pub min_frame_id: u64, + pub max_frame_id: u64, + pub category_mask: u32, + pub min_severity: u32, + pub max_records: u32, + pub _pad0: u32, + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_debug_record_header_t { + pub record_id: u64, + pub timestamp_us: u64, + pub frame_id: u64, + pub category: u32, + pub severity: u32, + pub code: u32, + pub payload_size: u32, + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_debug_query_result_t { + pub records_returned: u32, + pub records_available: u32, + pub oldest_record_id: u64, + pub newest_record_id: u64, + pub records_dropped: u32, + pub _pad0: u32, + } + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct zr_debug_stats_t { + pub total_records: u64, + pub total_dropped: u64, + pub error_count: u32, + pub warn_count: u32, + pub current_ring_usage: u32, + pub ring_capacity: u32, + } + + extern "C" { + pub fn zr_engine_config_default() -> zr_engine_config_t; + pub fn zr_fb_init(fb: *mut zr_fb_t, cols: u32, rows: u32) -> ZrResultT; + pub fn zr_fb_release(fb: *mut zr_fb_t); + pub fn zr_fb_cell(fb: *mut zr_fb_t, x: u32, y: u32) -> *mut zr_cell_t; + pub fn zr_fb_clear(fb: *mut zr_fb_t, style: *const zr_style_t) -> ZrResultT; + pub fn zr_fb_links_clone_from(dst: *mut zr_fb_t, src: *const zr_fb_t) -> ZrResultT; + pub fn zr_fb_link_intern( + fb: *mut zr_fb_t, + uri: *const u8, + uri_len: usize, + id: *const u8, + id_len: usize, + out_link_ref: *mut u32, + ) -> ZrResultT; + pub fn zr_fb_link_lookup( + fb: *const zr_fb_t, + link_ref: u32, + out_uri: *mut *const u8, + out_uri_len: *mut usize, + out_id: *mut *const u8, + out_id_len: *mut usize, + ) -> ZrResultT; + pub fn zr_fb_painter_begin( + p: *mut zr_fb_painter_t, + fb: *mut zr_fb_t, + clip_stack: *mut zr_rect_t, + clip_cap: u32, + ) -> ZrResultT; + pub fn zr_fb_clip_push(p: *mut zr_fb_painter_t, clip: zr_rect_t) -> ZrResultT; + pub fn zr_fb_clip_pop(p: *mut zr_fb_painter_t) -> ZrResultT; + pub fn zr_fb_put_grapheme( + p: *mut zr_fb_painter_t, + x: i32, + y: i32, + bytes: *const u8, + len: usize, + width: u8, + style: *const zr_style_t, + ) -> ZrResultT; + pub fn zr_diff_render( + prev: *const zr_fb_t, + next: *const zr_fb_t, + caps: *const plat_caps_t, + initial_term_state: *const zr_term_state_t, + desired_cursor_state: *const zr_cursor_state_t, + lim: *const zr_limits_t, + scratch_damage_rects: *mut zr_damage_rect_t, + scratch_damage_rect_cap: u32, + enable_scroll_optimizations: u8, + out_buf: *mut u8, + out_cap: usize, + out_len: *mut usize, + out_final_term_state: *mut zr_term_state_t, + out_stats: *mut zr_diff_stats_t, + ) -> ZrResultT; + + pub fn engine_create( + out_engine: *mut *mut zr_engine_t, + cfg: *const zr_engine_config_t, + ) -> ZrResultT; + pub fn engine_destroy(e: *mut zr_engine_t); + + pub fn engine_poll_events( + e: *mut zr_engine_t, + timeout_ms: i32, + out_buf: *mut u8, + out_cap: i32, + ) -> i32; + pub fn engine_post_user_event( + e: *mut zr_engine_t, + tag: u32, + payload: *const u8, + payload_len: i32, + ) -> ZrResultT; + + pub fn engine_submit_drawlist( + e: *mut zr_engine_t, + bytes: *const u8, + bytes_len: i32, + ) -> ZrResultT; + pub fn engine_present(e: *mut zr_engine_t) -> ZrResultT; + + pub fn engine_get_metrics(e: *mut zr_engine_t, out_metrics: *mut zr_metrics_t) + -> ZrResultT; + pub fn engine_get_caps(e: *mut zr_engine_t, out_caps: *mut zr_terminal_caps_t) + -> ZrResultT; + pub fn engine_set_config( + e: *mut zr_engine_t, + cfg: *const zr_engine_runtime_config_t, + ) -> ZrResultT; + + // Debug trace API + pub fn engine_debug_enable( + e: *mut zr_engine_t, + config: *const zr_debug_config_t, + ) -> ZrResultT; + pub fn engine_debug_disable(e: *mut zr_engine_t); + pub fn engine_debug_query( + e: *mut zr_engine_t, + query: *const zr_debug_query_t, + out_headers: *mut zr_debug_record_header_t, + out_headers_cap: u32, + out_result: *mut zr_debug_query_result_t, + ) -> ZrResultT; + pub fn engine_debug_get_payload( + e: *mut zr_engine_t, + record_id: u64, + out_payload: *mut u8, + out_cap: u32, + out_size: *mut u32, + ) -> ZrResultT; + pub fn engine_debug_get_stats( + e: *mut zr_engine_t, + out_stats: *mut zr_debug_stats_t, + ) -> ZrResultT; + pub fn engine_debug_export(e: *mut zr_engine_t, out_buf: *mut u8, out_cap: usize) -> i32; + pub fn engine_debug_reset(e: *mut zr_engine_t); + } } #[napi(object)] #[allow(non_snake_case)] pub struct EngineMetrics { - pub structSize: u32, + pub structSize: u32, - pub negotiatedEngineAbiMajor: u32, - pub negotiatedEngineAbiMinor: u32, - pub negotiatedEngineAbiPatch: u32, + pub negotiatedEngineAbiMajor: u32, + pub negotiatedEngineAbiMinor: u32, + pub negotiatedEngineAbiPatch: u32, - pub negotiatedDrawlistVersion: u32, - pub negotiatedEventBatchVersion: u32, + pub negotiatedDrawlistVersion: u32, + pub negotiatedEventBatchVersion: u32, - pub frameIndex: BigInt, - pub fps: u32, + pub frameIndex: BigInt, + pub fps: u32, - pub bytesEmittedTotal: BigInt, - pub bytesEmittedLastFrame: u32, + pub bytesEmittedTotal: BigInt, + pub bytesEmittedLastFrame: u32, - pub dirtyLinesLastFrame: u32, - pub dirtyColsLastFrame: u32, + pub dirtyLinesLastFrame: u32, + pub dirtyColsLastFrame: u32, - pub usInputLastFrame: u32, - pub usDrawlistLastFrame: u32, - pub usDiffLastFrame: u32, - pub usWriteLastFrame: u32, + pub usInputLastFrame: u32, + pub usDrawlistLastFrame: u32, + pub usDiffLastFrame: u32, + pub usWriteLastFrame: u32, - pub eventsOutLastPoll: u32, - pub eventsDroppedTotal: u32, + pub eventsOutLastPoll: u32, + pub eventsDroppedTotal: u32, - pub arenaFrameHighWaterBytes: BigInt, - pub arenaPersistentHighWaterBytes: BigInt, + pub arenaFrameHighWaterBytes: BigInt, + pub arenaPersistentHighWaterBytes: BigInt, - // v2 damage summary fields - pub damageRectsLastFrame: u32, - pub damageCellsLastFrame: u32, - pub damageFullFrame: bool, + // v2 damage summary fields + pub damageRectsLastFrame: u32, + pub damageCellsLastFrame: u32, + pub damageFullFrame: bool, } #[napi(object)] #[allow(non_snake_case)] pub struct TerminalCaps { - /// Color mode: 0=unknown, 1=16, 2=256, 3=rgb - pub colorMode: u32, - pub supportsMouse: bool, - pub supportsBracketedPaste: bool, - pub supportsFocusEvents: bool, - pub supportsOsc52: bool, - pub supportsSyncUpdate: bool, - pub supportsScrollRegion: bool, - pub supportsCursorShape: bool, - pub supportsOutputWaitWritable: bool, - pub supportsUnderlineStyles: bool, - pub supportsColoredUnderlines: bool, - pub supportsHyperlinks: bool, - /// Bitmask of supported SGR attributes - pub sgrAttrsSupported: u32, + /// Color mode: 0=unknown, 1=16, 2=256, 3=rgb + pub colorMode: u32, + pub supportsMouse: bool, + pub supportsBracketedPaste: bool, + pub supportsFocusEvents: bool, + pub supportsOsc52: bool, + pub supportsSyncUpdate: bool, + pub supportsScrollRegion: bool, + pub supportsCursorShape: bool, + pub supportsOutputWaitWritable: bool, + pub supportsUnderlineStyles: bool, + pub supportsColoredUnderlines: bool, + pub supportsHyperlinks: bool, + /// Bitmask of supported SGR attributes + pub sgrAttrsSupported: u32, } struct EngineSlot { - engine: *mut ffi::zr_engine_t, - owner_thread_id: u64, - active_calls: AtomicUsize, - active_calls_mu: Mutex<()>, - active_calls_cv: Condvar, - destroyed: AtomicBool, + engine: *mut ffi::zr_engine_t, + owner_thread_id: ThreadId, + active_calls: AtomicUsize, + active_calls_mu: Mutex<()>, + active_calls_cv: Condvar, + destroyed: AtomicBool, } unsafe impl Send for EngineSlot {} unsafe impl Sync for EngineSlot {} impl EngineSlot { - fn is_owner_thread(&self) -> bool { - self.owner_thread_id == current_thread_id_u64() - } + fn is_owner_thread(&self) -> bool { + self.owner_thread_id == current_thread_id() + } } struct EngineGuard { - slot: std::sync::Arc, + slot: std::sync::Arc, } impl Drop for EngineGuard { - fn drop(&mut self) { - let prev = self.slot.active_calls.fetch_sub(1, Ordering::Release); - if prev == 1 { - self.slot.active_calls_cv.notify_all(); + fn drop(&mut self) { + let prev = self.slot.active_calls.fetch_sub(1, Ordering::Release); + if prev == 1 { + self.slot.active_calls_cv.notify_all(); + } } - } } static REGISTRY: OnceLock>>> = OnceLock::new(); static NEXT_ENGINE_ID: AtomicU32 = AtomicU32::new(1); -static NEXT_THREAD_ID: AtomicU64 = AtomicU64::new(1); +// Keep the addon resident for process lifetime so worker-thread TLS cleanup +// cannot jump back into an already-unloaded Rust/N-API image. +static MODULE_PIN_STATE: OnceLock> = OnceLock::new(); fn registry() -> &'static Mutex>> { - REGISTRY.get_or_init(|| Mutex::new(HashMap::new())) + REGISTRY.get_or_init(|| Mutex::new(HashMap::new())) } -fn current_thread_id_u64() -> u64 { - thread_local! { - static THREAD_ID: u64 = NEXT_THREAD_ID.fetch_add(1, Ordering::Relaxed); - } - THREAD_ID.with(|id| *id) +fn current_thread_id() -> ThreadId { + std::thread::current().id() } -fn alloc_engine_id() -> Result { - loop { - let cur = NEXT_ENGINE_ID.load(Ordering::Relaxed); - if cur == 0 { - return Err(ffi::ZR_ERR_LIMIT); - } - if cur == u32::MAX { - if NEXT_ENGINE_ID - .compare_exchange(cur, 0, Ordering::SeqCst, Ordering::Relaxed) - .is_ok() - { - return Ok(cur); - } - continue; - } - let next = cur.wrapping_add(1); - if NEXT_ENGINE_ID - .compare_exchange(cur, next, Ordering::SeqCst, Ordering::Relaxed) - .is_ok() +#[cfg(any(target_os = "linux", target_os = "macos"))] +fn pin_current_module() -> Result { + use std::ffi::{c_char, c_int, c_void, CStr}; + + #[repr(C)] + struct DlInfo { + dli_fname: *const c_char, + dli_fbase: *mut c_void, + dli_sname: *const c_char, + dli_saddr: *mut c_void, + } + + unsafe extern "C" { + fn dladdr(addr: *const c_void, info: *mut DlInfo) -> c_int; + fn dlopen(filename: *const c_char, flags: c_int) -> *mut c_void; + fn dlerror() -> *const c_char; + } + + const RTLD_NOW: c_int = 0x2; + #[cfg(target_os = "linux")] + const RTLD_NODELETE: c_int = 0x1000; + + let mut info = DlInfo { + dli_fname: std::ptr::null(), + dli_fbase: std::ptr::null_mut(), + dli_sname: std::ptr::null(), + dli_saddr: std::ptr::null_mut(), + }; + let symbol = pin_current_module as *const (); + let lookup_ok = unsafe { dladdr(symbol.cast::(), &mut info as *mut _) }; + if lookup_ok == 0 || info.dli_fname.is_null() { + return Err("dladdr failed for native module address".to_owned()); + } + + let mut flags = RTLD_NOW; + #[cfg(target_os = "linux")] { - return Ok(cur); + flags |= RTLD_NODELETE; + } + + let handle = unsafe { dlopen(info.dli_fname, flags) }; + if handle.is_null() { + let detail = unsafe { dlerror() }; + if detail.is_null() { + return Err("dlopen returned null without dlerror detail".to_owned()); + } + let detail = unsafe { CStr::from_ptr(detail) } + .to_string_lossy() + .into_owned(); + return Err(format!("dlopen failed while pinning module: {detail}")); + } + + Ok(handle as usize) +} + +#[cfg(windows)] +fn pin_current_module() -> Result { + use std::ffi::c_void; + + type Hmodule = *mut c_void; + + unsafe extern "system" { + fn GetModuleHandleExW(flags: u32, module_name: *const u16, module: *mut Hmodule) -> i32; + } + + const GET_MODULE_HANDLE_EX_FLAG_PIN: u32 = 0x0000_0001; + const GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS: u32 = 0x0000_0004; + + let mut module: Hmodule = std::ptr::null_mut(); + let symbol = pin_current_module as *const (); + let ok = unsafe { + GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_PIN | GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + symbol.cast::(), + &mut module as *mut _, + ) + }; + if ok == 0 || module.is_null() { + return Err(std::io::Error::last_os_error().to_string()); + } + + Ok(module as usize) +} + +#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))] +fn pin_current_module() -> Result { + Ok(0) +} + +fn ensure_module_pinned() -> napi::Result<()> { + let state = MODULE_PIN_STATE.get_or_init(pin_current_module); + match state { + Ok(_) => Ok(()), + Err(detail) => Err(Error::new( + Status::GenericFailure, + format!("failed to pin @rezi-ui/native for worker_threads safety: {detail}"), + )), + } +} + +#[module_exports] +fn init_native_module(_exports: JsObject, _env: Env) -> napi::Result<()> { + ensure_module_pinned() +} + +fn alloc_engine_id() -> Result { + loop { + let cur = NEXT_ENGINE_ID.load(Ordering::Relaxed); + if cur == 0 { + return Err(ffi::ZR_ERR_LIMIT); + } + if cur == u32::MAX { + if NEXT_ENGINE_ID + .compare_exchange(cur, 0, Ordering::SeqCst, Ordering::Relaxed) + .is_ok() + { + return Ok(cur); + } + continue; + } + let next = cur.wrapping_add(1); + if NEXT_ENGINE_ID + .compare_exchange(cur, next, Ordering::SeqCst, Ordering::Relaxed) + .is_ok() + { + return Ok(cur); + } } - } } fn lock_registry(f: impl FnOnce(&mut HashMap>) -> T) -> T { - let mut guard = match registry().lock() { - Ok(g) => g, - Err(poison) => poison.into_inner(), - }; - f(&mut guard) + let mut guard = match registry().lock() { + Ok(g) => g, + Err(poison) => poison.into_inner(), + }; + f(&mut guard) } fn get_engine_guard(engine_id: u32) -> Result { - if engine_id == 0 { - return Err(ffi::ZR_ERR_INVALID_ARGUMENT); - } - - lock_registry(|map| { - let slot = match map.get(&engine_id) { - Some(s) => std::sync::Arc::clone(s), - None => return Err(ffi::ZR_ERR_INVALID_ARGUMENT), - }; - slot.active_calls.fetch_add(1, Ordering::Acquire); - Ok(EngineGuard { slot }) - }) + if engine_id == 0 { + return Err(ffi::ZR_ERR_INVALID_ARGUMENT); + } + + lock_registry(|map| { + let slot = match map.get(&engine_id) { + Some(s) => std::sync::Arc::clone(s), + None => return Err(ffi::ZR_ERR_INVALID_ARGUMENT), + }; + slot.active_calls.fetch_add(1, Ordering::Acquire); + Ok(EngineGuard { slot }) + }) } fn validate_known_keys(obj: &JsObject, allowed: &[(&str, &str)], ctx: &str) -> napi::Result<()> { - let names = obj.get_property_names()?; - let len = names.get_array_length()?; - - 'outer: for i in 0..len { - let unk = names.get_element::(i)?; - let s = unk.coerce_to_string()?; - let k = s.into_utf8()?.as_str()?.to_owned(); - for (primary, alias) in allowed { - if k == *primary || k == *alias { - continue 'outer; - } - } - return Err(Error::new(Status::InvalidArg, format!("{ctx}: unknown key: {k}"))); - } - Ok(()) + let names = obj.get_property_names()?; + let len = names.get_array_length()?; + + 'outer: for i in 0..len { + let unk = names.get_element::(i)?; + let s = unk.coerce_to_string()?; + let k = s.into_utf8()?.as_str()?.to_owned(); + for (primary, alias) in allowed { + if k == *primary || k == *alias { + continue 'outer; + } + } + return Err(Error::new( + Status::InvalidArg, + format!("{ctx}: unknown key: {k}"), + )); + } + Ok(()) } const LIMITS_KEYS: &[(&str, &str)] = &[ - ("arenaMaxTotalBytes", "arena_max_total_bytes"), - ("arenaInitialBytes", "arena_initial_bytes"), - ("outMaxBytesPerFrame", "out_max_bytes_per_frame"), - ("dlMaxTotalBytes", "dl_max_total_bytes"), - ("dlMaxCmds", "dl_max_cmds"), - ("dlMaxStrings", "dl_max_strings"), - ("dlMaxBlobs", "dl_max_blobs"), - ("dlMaxClipDepth", "dl_max_clip_depth"), - ("dlMaxTextRunSegments", "dl_max_text_run_segments"), - ("diffMaxDamageRects", "diff_max_damage_rects"), + ("arenaMaxTotalBytes", "arena_max_total_bytes"), + ("arenaInitialBytes", "arena_initial_bytes"), + ("outMaxBytesPerFrame", "out_max_bytes_per_frame"), + ("dlMaxTotalBytes", "dl_max_total_bytes"), + ("dlMaxCmds", "dl_max_cmds"), + ("dlMaxStrings", "dl_max_strings"), + ("dlMaxBlobs", "dl_max_blobs"), + ("dlMaxClipDepth", "dl_max_clip_depth"), + ("dlMaxTextRunSegments", "dl_max_text_run_segments"), + ("diffMaxDamageRects", "diff_max_damage_rects"), ]; const PLAT_KEYS: &[(&str, &str)] = &[ - ("requestedColorMode", "requested_color_mode"), - ("enableMouse", "enable_mouse"), - ("enableBracketedPaste", "enable_bracketed_paste"), - ("enableFocusEvents", "enable_focus_events"), - ("enableOsc52", "enable_osc52"), + ("requestedColorMode", "requested_color_mode"), + ("enableMouse", "enable_mouse"), + ("enableBracketedPaste", "enable_bracketed_paste"), + ("enableFocusEvents", "enable_focus_events"), + ("enableOsc52", "enable_osc52"), ]; const CREATE_CFG_KEYS: &[(&str, &str)] = &[ - ("requestedEngineAbiMajor", "requested_engine_abi_major"), - ("requestedEngineAbiMinor", "requested_engine_abi_minor"), - ("requestedEngineAbiPatch", "requested_engine_abi_patch"), - ("requestedDrawlistVersion", "requested_drawlist_version"), - ("requestedEventBatchVersion", "requested_event_batch_version"), - ("limits", "limits"), - ("plat", "plat"), - ("tabWidth", "tab_width"), - ("widthPolicy", "width_policy"), - ("targetFps", "target_fps"), - ("enableScrollOptimizations", "enable_scroll_optimizations"), - ("enableDebugOverlay", "enable_debug_overlay"), - ("enableReplayRecording", "enable_replay_recording"), - ("waitForOutputDrain", "wait_for_output_drain"), - ("capForceFlags", "cap_force_flags"), - ("capSuppressFlags", "cap_suppress_flags"), + ("requestedEngineAbiMajor", "requested_engine_abi_major"), + ("requestedEngineAbiMinor", "requested_engine_abi_minor"), + ("requestedEngineAbiPatch", "requested_engine_abi_patch"), + ("requestedDrawlistVersion", "requested_drawlist_version"), + ( + "requestedEventBatchVersion", + "requested_event_batch_version", + ), + ("limits", "limits"), + ("plat", "plat"), + ("tabWidth", "tab_width"), + ("widthPolicy", "width_policy"), + ("targetFps", "target_fps"), + ("enableScrollOptimizations", "enable_scroll_optimizations"), + ("enableDebugOverlay", "enable_debug_overlay"), + ("enableReplayRecording", "enable_replay_recording"), + ("waitForOutputDrain", "wait_for_output_drain"), + ("capForceFlags", "cap_force_flags"), + ("capSuppressFlags", "cap_suppress_flags"), ]; const RUNTIME_CFG_KEYS: &[(&str, &str)] = &[ - ("limits", "limits"), - ("plat", "plat"), - ("tabWidth", "tab_width"), - ("widthPolicy", "width_policy"), - ("targetFps", "target_fps"), - ("enableScrollOptimizations", "enable_scroll_optimizations"), - ("enableDebugOverlay", "enable_debug_overlay"), - ("enableReplayRecording", "enable_replay_recording"), - ("waitForOutputDrain", "wait_for_output_drain"), - ("capForceFlags", "cap_force_flags"), - ("capSuppressFlags", "cap_suppress_flags"), + ("limits", "limits"), + ("plat", "plat"), + ("tabWidth", "tab_width"), + ("widthPolicy", "width_policy"), + ("targetFps", "target_fps"), + ("enableScrollOptimizations", "enable_scroll_optimizations"), + ("enableDebugOverlay", "enable_debug_overlay"), + ("enableReplayRecording", "enable_replay_recording"), + ("waitForOutputDrain", "wait_for_output_drain"), + ("capForceFlags", "cap_force_flags"), + ("capSuppressFlags", "cap_suppress_flags"), ]; fn apply_create_cfg_strict(dst: &mut ffi::zr_engine_config_t, obj: &JsObject) -> napi::Result<()> { - validate_known_keys(obj, CREATE_CFG_KEYS, "engineCreate config")?; - if let Some(lim) = js_obj(obj, "limits", "limits") - .map_err(|_| Error::new(Status::InvalidArg, "engineCreate: limits must be an object"))? - { - validate_known_keys(&lim, LIMITS_KEYS, "engineCreate config.limits")?; - } - if let Some(plat) = js_obj(obj, "plat", "plat") - .map_err(|_| Error::new(Status::InvalidArg, "engineCreate: plat must be an object"))? - { - validate_known_keys(&plat, PLAT_KEYS, "engineCreate config.plat")?; - } - - apply_create_cfg(dst, obj).map_err(|_| Error::new(Status::InvalidArg, "engineCreate: invalid config value"))?; - Ok(()) -} + validate_known_keys(obj, CREATE_CFG_KEYS, "engineCreate config")?; + if let Some(lim) = js_obj(obj, "limits", "limits") + .map_err(|_| Error::new(Status::InvalidArg, "engineCreate: limits must be an object"))? + { + validate_known_keys(&lim, LIMITS_KEYS, "engineCreate config.limits")?; + } + if let Some(plat) = js_obj(obj, "plat", "plat") + .map_err(|_| Error::new(Status::InvalidArg, "engineCreate: plat must be an object"))? + { + validate_known_keys(&plat, PLAT_KEYS, "engineCreate config.plat")?; + } -fn apply_runtime_cfg_strict(dst: &mut ffi::zr_engine_runtime_config_t, obj: &JsObject) -> napi::Result<()> { - validate_known_keys(obj, RUNTIME_CFG_KEYS, "engineSetConfig config")?; - if let Some(lim) = js_obj(obj, "limits", "limits") - .map_err(|_| Error::new(Status::InvalidArg, "engineSetConfig: limits must be an object"))? - { - validate_known_keys(&lim, LIMITS_KEYS, "engineSetConfig config.limits")?; - } - if let Some(plat) = js_obj(obj, "plat", "plat") - .map_err(|_| Error::new(Status::InvalidArg, "engineSetConfig: plat must be an object"))? - { - validate_known_keys(&plat, PLAT_KEYS, "engineSetConfig config.plat")?; - } - - apply_runtime_cfg(dst, obj).map_err(|_| Error::new(Status::InvalidArg, "engineSetConfig: invalid config value"))?; - Ok(()) + apply_create_cfg(dst, obj) + .map_err(|_| Error::new(Status::InvalidArg, "engineCreate: invalid config value"))?; + Ok(()) } -fn js_u32(obj: &JsObject, primary: &str, alias: &str) -> ParseResult> { - for name in [primary, alias] { - let v = match obj.get_named_property::(name) { - Ok(v) => v, - Err(_) => continue, - }; - if v.get_type().map_err(|_| ())? == ValueType::Undefined { - continue; +fn apply_runtime_cfg_strict( + dst: &mut ffi::zr_engine_runtime_config_t, + obj: &JsObject, +) -> napi::Result<()> { + validate_known_keys(obj, RUNTIME_CFG_KEYS, "engineSetConfig config")?; + if let Some(lim) = js_obj(obj, "limits", "limits").map_err(|_| { + Error::new( + Status::InvalidArg, + "engineSetConfig: limits must be an object", + ) + })? { + validate_known_keys(&lim, LIMITS_KEYS, "engineSetConfig config.limits")?; } - let n = v.coerce_to_number().map_err(|_| ())?; - let f = n.get_double().map_err(|_| ())?; - if !f.is_finite() || f < 0.0 || f > (u32::MAX as f64) || f.fract() != 0.0 { - return Err(()); + if let Some(plat) = js_obj(obj, "plat", "plat").map_err(|_| { + Error::new( + Status::InvalidArg, + "engineSetConfig: plat must be an object", + ) + })? { + validate_known_keys(&plat, PLAT_KEYS, "engineSetConfig config.plat")?; } - return Ok(Some(f as u32)); - } - Ok(None) + + apply_runtime_cfg(dst, obj) + .map_err(|_| Error::new(Status::InvalidArg, "engineSetConfig: invalid config value"))?; + Ok(()) } -fn js_u8_bool(obj: &JsObject, primary: &str, alias: &str) -> ParseResult> { - for name in [primary, alias] { - let v = match obj.get_named_property::(name) { - Ok(v) => v, - Err(_) => continue, - }; - match v.get_type().map_err(|_| ())? { - ValueType::Undefined => continue, - ValueType::Boolean => { - let b = v.coerce_to_bool().map_err(|_| ())?; - return Ok(Some(if b.get_value().map_err(|_| ())? { 1 } else { 0 })); - } - ValueType::Number => { +fn js_u32(obj: &JsObject, primary: &str, alias: &str) -> ParseResult> { + for name in [primary, alias] { + let v = match obj.get_named_property::(name) { + Ok(v) => v, + Err(_) => continue, + }; + if v.get_type().map_err(|_| ())? == ValueType::Undefined { + continue; + } let n = v.coerce_to_number().map_err(|_| ())?; let f = n.get_double().map_err(|_| ())?; - if f == 0.0 { - return Ok(Some(0)); + if !f.is_finite() || f < 0.0 || f > (u32::MAX as f64) || f.fract() != 0.0 { + return Err(()); } - if f == 1.0 { - return Ok(Some(1)); + return Ok(Some(f as u32)); + } + Ok(None) +} + +fn js_u8_bool(obj: &JsObject, primary: &str, alias: &str) -> ParseResult> { + for name in [primary, alias] { + let v = match obj.get_named_property::(name) { + Ok(v) => v, + Err(_) => continue, + }; + match v.get_type().map_err(|_| ())? { + ValueType::Undefined => continue, + ValueType::Boolean => { + let b = v.coerce_to_bool().map_err(|_| ())?; + return Ok(Some(if b.get_value().map_err(|_| ())? { 1 } else { 0 })); + } + ValueType::Number => { + let n = v.coerce_to_number().map_err(|_| ())?; + let f = n.get_double().map_err(|_| ())?; + if f == 0.0 { + return Ok(Some(0)); + } + if f == 1.0 { + return Ok(Some(1)); + } + return Err(()); + } + _ => return Err(()), } - return Err(()); - } - _ => return Err(()), } - } - Ok(None) + Ok(None) } fn js_obj(obj: &JsObject, primary: &str, alias: &str) -> ParseResult> { - for name in [primary, alias] { - let v = match obj.get_named_property::(name) { - Ok(v) => v, - Err(_) => continue, - }; - if v.get_type().map_err(|_| ())? == ValueType::Undefined { - continue; + for name in [primary, alias] { + let v = match obj.get_named_property::(name) { + Ok(v) => v, + Err(_) => continue, + }; + if v.get_type().map_err(|_| ())? == ValueType::Undefined { + continue; + } + let o = v.coerce_to_object().map_err(|_| ())?; + return Ok(Some(o)); } - let o = v.coerce_to_object().map_err(|_| ())?; - return Ok(Some(o)); - } - Ok(None) + Ok(None) } fn apply_limits(dst: &mut ffi::zr_limits_t, obj: &JsObject) -> ParseResult<()> { - if let Some(v) = js_u32(obj, "arenaMaxTotalBytes", "arena_max_total_bytes")? { - dst.arena_max_total_bytes = v; - } - if let Some(v) = js_u32(obj, "arenaInitialBytes", "arena_initial_bytes")? { - dst.arena_initial_bytes = v; - } - if let Some(v) = js_u32(obj, "outMaxBytesPerFrame", "out_max_bytes_per_frame")? { - dst.out_max_bytes_per_frame = v; - } - if let Some(v) = js_u32(obj, "dlMaxTotalBytes", "dl_max_total_bytes")? { - dst.dl_max_total_bytes = v; - } - if let Some(v) = js_u32(obj, "dlMaxCmds", "dl_max_cmds")? { - dst.dl_max_cmds = v; - } - if let Some(v) = js_u32(obj, "dlMaxStrings", "dl_max_strings")? { - dst.dl_max_strings = v; - } - if let Some(v) = js_u32(obj, "dlMaxBlobs", "dl_max_blobs")? { - dst.dl_max_blobs = v; - } - if let Some(v) = js_u32(obj, "dlMaxClipDepth", "dl_max_clip_depth")? { - dst.dl_max_clip_depth = v; - } - if let Some(v) = js_u32(obj, "dlMaxTextRunSegments", "dl_max_text_run_segments")? { - dst.dl_max_text_run_segments = v; - } - if let Some(v) = js_u32(obj, "diffMaxDamageRects", "diff_max_damage_rects")? { - dst.diff_max_damage_rects = v; - } - Ok(()) + if let Some(v) = js_u32(obj, "arenaMaxTotalBytes", "arena_max_total_bytes")? { + dst.arena_max_total_bytes = v; + } + if let Some(v) = js_u32(obj, "arenaInitialBytes", "arena_initial_bytes")? { + dst.arena_initial_bytes = v; + } + if let Some(v) = js_u32(obj, "outMaxBytesPerFrame", "out_max_bytes_per_frame")? { + dst.out_max_bytes_per_frame = v; + } + if let Some(v) = js_u32(obj, "dlMaxTotalBytes", "dl_max_total_bytes")? { + dst.dl_max_total_bytes = v; + } + if let Some(v) = js_u32(obj, "dlMaxCmds", "dl_max_cmds")? { + dst.dl_max_cmds = v; + } + if let Some(v) = js_u32(obj, "dlMaxStrings", "dl_max_strings")? { + dst.dl_max_strings = v; + } + if let Some(v) = js_u32(obj, "dlMaxBlobs", "dl_max_blobs")? { + dst.dl_max_blobs = v; + } + if let Some(v) = js_u32(obj, "dlMaxClipDepth", "dl_max_clip_depth")? { + dst.dl_max_clip_depth = v; + } + if let Some(v) = js_u32(obj, "dlMaxTextRunSegments", "dl_max_text_run_segments")? { + dst.dl_max_text_run_segments = v; + } + if let Some(v) = js_u32(obj, "diffMaxDamageRects", "diff_max_damage_rects")? { + dst.diff_max_damage_rects = v; + } + Ok(()) } fn apply_plat(dst: &mut ffi::plat_config_t, obj: &JsObject) -> ParseResult<()> { - if let Some(v) = js_u32(obj, "requestedColorMode", "requested_color_mode")? { - dst.requested_color_mode = (v & 0xFF) as u8; - } - if let Some(v) = js_u8_bool(obj, "enableMouse", "enable_mouse")? { - dst.enable_mouse = v; - } - if let Some(v) = js_u8_bool(obj, "enableBracketedPaste", "enable_bracketed_paste")? { - dst.enable_bracketed_paste = v; - } - if let Some(v) = js_u8_bool(obj, "enableFocusEvents", "enable_focus_events")? { - dst.enable_focus_events = v; - } - if let Some(v) = js_u8_bool(obj, "enableOsc52", "enable_osc52")? { - dst.enable_osc52 = v; - } - dst._pad = [0, 0, 0]; - Ok(()) + if let Some(v) = js_u32(obj, "requestedColorMode", "requested_color_mode")? { + dst.requested_color_mode = (v & 0xFF) as u8; + } + if let Some(v) = js_u8_bool(obj, "enableMouse", "enable_mouse")? { + dst.enable_mouse = v; + } + if let Some(v) = js_u8_bool(obj, "enableBracketedPaste", "enable_bracketed_paste")? { + dst.enable_bracketed_paste = v; + } + if let Some(v) = js_u8_bool(obj, "enableFocusEvents", "enable_focus_events")? { + dst.enable_focus_events = v; + } + if let Some(v) = js_u8_bool(obj, "enableOsc52", "enable_osc52")? { + dst.enable_osc52 = v; + } + dst._pad = [0, 0, 0]; + Ok(()) } fn apply_create_cfg(dst: &mut ffi::zr_engine_config_t, obj: &JsObject) -> ParseResult<()> { - if let Some(v) = js_u32(obj, "requestedEngineAbiMajor", "requested_engine_abi_major")? { - dst.requested_engine_abi_major = v; - } - if let Some(v) = js_u32(obj, "requestedEngineAbiMinor", "requested_engine_abi_minor")? { - dst.requested_engine_abi_minor = v; - } - if let Some(v) = js_u32(obj, "requestedEngineAbiPatch", "requested_engine_abi_patch")? { - dst.requested_engine_abi_patch = v; - } - if let Some(v) = js_u32(obj, "requestedDrawlistVersion", "requested_drawlist_version")? { - dst.requested_drawlist_version = v; - } - if let Some(v) = js_u32(obj, "requestedEventBatchVersion", "requested_event_batch_version")? { - dst.requested_event_batch_version = v; - } - - if let Some(lim) = js_obj(obj, "limits", "limits")? { - apply_limits(&mut dst.limits, &lim)?; - } - if let Some(plat) = js_obj(obj, "plat", "plat")? { - apply_plat(&mut dst.plat, &plat)?; - } - - if let Some(v) = js_u32(obj, "tabWidth", "tab_width")? { - dst.tab_width = v; - } - if let Some(v) = js_u32(obj, "widthPolicy", "width_policy")? { - dst.width_policy = v; - } - if let Some(v) = js_u32(obj, "targetFps", "target_fps")? { - dst.target_fps = v; - } - - if let Some(v) = js_u8_bool(obj, "enableScrollOptimizations", "enable_scroll_optimizations")? { - dst.enable_scroll_optimizations = v; - } - if let Some(v) = js_u8_bool(obj, "enableDebugOverlay", "enable_debug_overlay")? { - dst.enable_debug_overlay = v; - } - if let Some(v) = js_u8_bool(obj, "enableReplayRecording", "enable_replay_recording")? { - dst.enable_replay_recording = v; - } - if let Some(v) = js_u8_bool(obj, "waitForOutputDrain", "wait_for_output_drain")? { - dst.wait_for_output_drain = v; - } - if let Some(v) = js_u32(obj, "capForceFlags", "cap_force_flags")? { - dst.cap_force_flags = v; - } - if let Some(v) = js_u32(obj, "capSuppressFlags", "cap_suppress_flags")? { - dst.cap_suppress_flags = v; - } - Ok(()) + if let Some(v) = js_u32(obj, "requestedEngineAbiMajor", "requested_engine_abi_major")? { + dst.requested_engine_abi_major = v; + } + if let Some(v) = js_u32(obj, "requestedEngineAbiMinor", "requested_engine_abi_minor")? { + dst.requested_engine_abi_minor = v; + } + if let Some(v) = js_u32(obj, "requestedEngineAbiPatch", "requested_engine_abi_patch")? { + dst.requested_engine_abi_patch = v; + } + if let Some(v) = js_u32( + obj, + "requestedDrawlistVersion", + "requested_drawlist_version", + )? { + dst.requested_drawlist_version = v; + } + if let Some(v) = js_u32( + obj, + "requestedEventBatchVersion", + "requested_event_batch_version", + )? { + dst.requested_event_batch_version = v; + } + + if let Some(lim) = js_obj(obj, "limits", "limits")? { + apply_limits(&mut dst.limits, &lim)?; + } + if let Some(plat) = js_obj(obj, "plat", "plat")? { + apply_plat(&mut dst.plat, &plat)?; + } + + if let Some(v) = js_u32(obj, "tabWidth", "tab_width")? { + dst.tab_width = v; + } + if let Some(v) = js_u32(obj, "widthPolicy", "width_policy")? { + dst.width_policy = v; + } + if let Some(v) = js_u32(obj, "targetFps", "target_fps")? { + dst.target_fps = v; + } + + if let Some(v) = js_u8_bool( + obj, + "enableScrollOptimizations", + "enable_scroll_optimizations", + )? { + dst.enable_scroll_optimizations = v; + } + if let Some(v) = js_u8_bool(obj, "enableDebugOverlay", "enable_debug_overlay")? { + dst.enable_debug_overlay = v; + } + if let Some(v) = js_u8_bool(obj, "enableReplayRecording", "enable_replay_recording")? { + dst.enable_replay_recording = v; + } + if let Some(v) = js_u8_bool(obj, "waitForOutputDrain", "wait_for_output_drain")? { + dst.wait_for_output_drain = v; + } + if let Some(v) = js_u32(obj, "capForceFlags", "cap_force_flags")? { + dst.cap_force_flags = v; + } + if let Some(v) = js_u32(obj, "capSuppressFlags", "cap_suppress_flags")? { + dst.cap_suppress_flags = v; + } + Ok(()) } fn create_default_runtime_cfg() -> ffi::zr_engine_runtime_config_t { - let base = unsafe { ffi::zr_engine_config_default() }; - ffi::zr_engine_runtime_config_t { - limits: base.limits, - plat: base.plat, - tab_width: base.tab_width, - width_policy: base.width_policy, - target_fps: base.target_fps, - enable_scroll_optimizations: base.enable_scroll_optimizations, - enable_debug_overlay: base.enable_debug_overlay, - enable_replay_recording: base.enable_replay_recording, - wait_for_output_drain: base.wait_for_output_drain, - cap_force_flags: base.cap_force_flags, - cap_suppress_flags: base.cap_suppress_flags, - } + let base = unsafe { ffi::zr_engine_config_default() }; + ffi::zr_engine_runtime_config_t { + limits: base.limits, + plat: base.plat, + tab_width: base.tab_width, + width_policy: base.width_policy, + target_fps: base.target_fps, + enable_scroll_optimizations: base.enable_scroll_optimizations, + enable_debug_overlay: base.enable_debug_overlay, + enable_replay_recording: base.enable_replay_recording, + wait_for_output_drain: base.wait_for_output_drain, + cap_force_flags: base.cap_force_flags, + cap_suppress_flags: base.cap_suppress_flags, + } } fn apply_runtime_cfg(dst: &mut ffi::zr_engine_runtime_config_t, obj: &JsObject) -> ParseResult<()> { - if let Some(lim) = js_obj(obj, "limits", "limits")? { - apply_limits(&mut dst.limits, &lim)?; - } - if let Some(plat) = js_obj(obj, "plat", "plat")? { - apply_plat(&mut dst.plat, &plat)?; - } - if let Some(v) = js_u32(obj, "tabWidth", "tab_width")? { - dst.tab_width = v; - } - if let Some(v) = js_u32(obj, "widthPolicy", "width_policy")? { - dst.width_policy = v; - } - if let Some(v) = js_u32(obj, "targetFps", "target_fps")? { - dst.target_fps = v; - } - if let Some(v) = js_u8_bool(obj, "enableScrollOptimizations", "enable_scroll_optimizations")? { - dst.enable_scroll_optimizations = v; - } - if let Some(v) = js_u8_bool(obj, "enableDebugOverlay", "enable_debug_overlay")? { - dst.enable_debug_overlay = v; - } - if let Some(v) = js_u8_bool(obj, "enableReplayRecording", "enable_replay_recording")? { - dst.enable_replay_recording = v; - } - if let Some(v) = js_u8_bool(obj, "waitForOutputDrain", "wait_for_output_drain")? { - dst.wait_for_output_drain = v; - } - if let Some(v) = js_u32(obj, "capForceFlags", "cap_force_flags")? { - dst.cap_force_flags = v; - } - if let Some(v) = js_u32(obj, "capSuppressFlags", "cap_suppress_flags")? { - dst.cap_suppress_flags = v; - } - Ok(()) + if let Some(lim) = js_obj(obj, "limits", "limits")? { + apply_limits(&mut dst.limits, &lim)?; + } + if let Some(plat) = js_obj(obj, "plat", "plat")? { + apply_plat(&mut dst.plat, &plat)?; + } + if let Some(v) = js_u32(obj, "tabWidth", "tab_width")? { + dst.tab_width = v; + } + if let Some(v) = js_u32(obj, "widthPolicy", "width_policy")? { + dst.width_policy = v; + } + if let Some(v) = js_u32(obj, "targetFps", "target_fps")? { + dst.target_fps = v; + } + if let Some(v) = js_u8_bool( + obj, + "enableScrollOptimizations", + "enable_scroll_optimizations", + )? { + dst.enable_scroll_optimizations = v; + } + if let Some(v) = js_u8_bool(obj, "enableDebugOverlay", "enable_debug_overlay")? { + dst.enable_debug_overlay = v; + } + if let Some(v) = js_u8_bool(obj, "enableReplayRecording", "enable_replay_recording")? { + dst.enable_replay_recording = v; + } + if let Some(v) = js_u8_bool(obj, "waitForOutputDrain", "wait_for_output_drain")? { + dst.wait_for_output_drain = v; + } + if let Some(v) = js_u32(obj, "capForceFlags", "cap_force_flags")? { + dst.cap_force_flags = v; + } + if let Some(v) = js_u32(obj, "capSuppressFlags", "cap_suppress_flags")? { + dst.cap_suppress_flags = v; + } + Ok(()) } #[napi(js_name = "engineCreate")] pub fn engine_create(_env: Env, config: Option) -> napi::Result { - let mut cfg = unsafe { ffi::zr_engine_config_default() }; - if let Some(obj) = config { - apply_create_cfg_strict(&mut cfg, &obj)?; - } - - let mut out_engine: *mut ffi::zr_engine_t = std::ptr::null_mut(); - let rc = unsafe { ffi::engine_create(&mut out_engine as *mut _, &cfg as *const _) }; - if rc != ffi::ZR_OK { - return Ok(rc as i64); - } - if out_engine.is_null() { - return Ok(ffi::ZR_ERR_PLATFORM as i64); - } - - let engine_id = match alloc_engine_id() { - Ok(id) => id, - Err(err) => { - unsafe { ffi::engine_destroy(out_engine) }; - return Ok(err as i64); - } - }; - - let slot = std::sync::Arc::new(EngineSlot { - engine: out_engine, - owner_thread_id: current_thread_id_u64(), - active_calls: AtomicUsize::new(0), - active_calls_mu: Mutex::new(()), - active_calls_cv: Condvar::new(), - destroyed: AtomicBool::new(false), - }); - - lock_registry(|map| { - map.insert(engine_id, slot); - }); - - Ok(engine_id as i64) + let mut cfg = unsafe { ffi::zr_engine_config_default() }; + if let Some(obj) = config { + apply_create_cfg_strict(&mut cfg, &obj)?; + } + + let mut out_engine: *mut ffi::zr_engine_t = std::ptr::null_mut(); + let rc = unsafe { ffi::engine_create(&mut out_engine as *mut _, &cfg as *const _) }; + if rc != ffi::ZR_OK { + return Ok(rc as i64); + } + if out_engine.is_null() { + return Ok(ffi::ZR_ERR_PLATFORM as i64); + } + + let engine_id = match alloc_engine_id() { + Ok(id) => id, + Err(err) => { + unsafe { ffi::engine_destroy(out_engine) }; + return Ok(err as i64); + } + }; + + let slot = std::sync::Arc::new(EngineSlot { + engine: out_engine, + owner_thread_id: current_thread_id(), + active_calls: AtomicUsize::new(0), + active_calls_mu: Mutex::new(()), + active_calls_cv: Condvar::new(), + destroyed: AtomicBool::new(false), + }); + + lock_registry(|map| { + map.insert(engine_id, slot); + }); + + Ok(engine_id as i64) } #[napi(js_name = "engineDestroy")] pub fn engine_destroy(engine_id: u32) { - if engine_id == 0 { - return; - } - - let slot = lock_registry(|map| { - let slot = match map.get(&engine_id) { - Some(s) => s, - None => return None, + if engine_id == 0 { + return; + } + + let slot = lock_registry(|map| { + let slot = match map.get(&engine_id) { + Some(s) => s, + None => return None, + }; + if slot.owner_thread_id != current_thread_id() { + return None; + } + map.remove(&engine_id) + }); + let Some(slot) = slot else { + return; + }; + + slot.destroyed.store(true, Ordering::Release); + let guard = match slot.active_calls_mu.lock() { + Ok(g) => g, + Err(poison) => poison.into_inner(), }; - if slot.owner_thread_id != current_thread_id_u64() { - return None; - } - map.remove(&engine_id) - }); - let Some(slot) = slot else { return; }; - - slot.destroyed.store(true, Ordering::Release); - let guard = match slot.active_calls_mu.lock() { - Ok(g) => g, - Err(poison) => poison.into_inner(), - }; - let _guard = match slot - .active_calls_cv - .wait_while(guard, |_| slot.active_calls.load(Ordering::Acquire) != 0) - { - Ok(g) => g, - Err(poison) => poison.into_inner(), - }; - unsafe { ffi::engine_destroy(slot.engine) }; + let _guard = match slot + .active_calls_cv + .wait_while(guard, |_| slot.active_calls.load(Ordering::Acquire) != 0) + { + Ok(g) => g, + Err(poison) => poison.into_inner(), + }; + unsafe { ffi::engine_destroy(slot.engine) }; } #[napi(js_name = "engineSubmitDrawlist")] pub fn engine_submit_drawlist(engine_id: u32, drawlist: Uint8Array) -> i32 { - let guard = match get_engine_guard(engine_id) { - Ok(g) => g, - Err(rc) => return rc, - }; - if !guard.slot.is_owner_thread() { - return ffi::ZR_ERR_INVALID_ARGUMENT; - } - - if drawlist.len() > (i32::MAX as usize) { - return ffi::ZR_ERR_LIMIT; - } - let bytes = drawlist.as_ref(); - unsafe { ffi::engine_submit_drawlist(guard.slot.engine, bytes.as_ptr(), bytes.len() as i32) } + let guard = match get_engine_guard(engine_id) { + Ok(g) => g, + Err(rc) => return rc, + }; + if !guard.slot.is_owner_thread() { + return ffi::ZR_ERR_INVALID_ARGUMENT; + } + + if drawlist.len() > (i32::MAX as usize) { + return ffi::ZR_ERR_LIMIT; + } + let bytes = drawlist.as_ref(); + unsafe { ffi::engine_submit_drawlist(guard.slot.engine, bytes.as_ptr(), bytes.len() as i32) } } #[napi(js_name = "enginePresent")] pub fn engine_present(engine_id: u32) -> i32 { - let guard = match get_engine_guard(engine_id) { - Ok(g) => g, - Err(rc) => return rc, - }; - if !guard.slot.is_owner_thread() { - return ffi::ZR_ERR_INVALID_ARGUMENT; - } - unsafe { ffi::engine_present(guard.slot.engine) } + let guard = match get_engine_guard(engine_id) { + Ok(g) => g, + Err(rc) => return rc, + }; + if !guard.slot.is_owner_thread() { + return ffi::ZR_ERR_INVALID_ARGUMENT; + } + unsafe { ffi::engine_present(guard.slot.engine) } } #[napi(js_name = "enginePollEvents")] pub fn engine_poll_events(engine_id: u32, timeout_ms: i32, mut out: Uint8Array) -> i32 { - let guard = match get_engine_guard(engine_id) { - Ok(g) => g, - Err(rc) => return rc, - }; - if !guard.slot.is_owner_thread() { - return ffi::ZR_ERR_INVALID_ARGUMENT; - } - if timeout_ms < 0 { - return ffi::ZR_ERR_INVALID_ARGUMENT; - } - if out.len() > (i32::MAX as usize) { - return ffi::ZR_ERR_LIMIT; - } - let out_buf = out.as_mut(); - unsafe { - ffi::engine_poll_events( - guard.slot.engine, - timeout_ms, - out_buf.as_mut_ptr(), - out_buf.len() as i32, - ) - } + let guard = match get_engine_guard(engine_id) { + Ok(g) => g, + Err(rc) => return rc, + }; + if !guard.slot.is_owner_thread() { + return ffi::ZR_ERR_INVALID_ARGUMENT; + } + if timeout_ms < 0 { + return ffi::ZR_ERR_INVALID_ARGUMENT; + } + if out.len() > (i32::MAX as usize) { + return ffi::ZR_ERR_LIMIT; + } + let out_buf = out.as_mut(); + unsafe { + ffi::engine_poll_events( + guard.slot.engine, + timeout_ms, + out_buf.as_mut_ptr(), + out_buf.len() as i32, + ) + } } #[napi(js_name = "enginePostUserEvent")] pub fn engine_post_user_event(engine_id: u32, tag: u32, payload: Uint8Array) -> i32 { - let guard = match get_engine_guard(engine_id) { - Ok(g) => g, - Err(rc) => return rc, - }; - - if payload.len() > (i32::MAX as usize) { - return ffi::ZR_ERR_LIMIT; - } - let bytes = payload.as_ref(); - let (ptr, len) = if bytes.is_empty() { - (std::ptr::null(), 0) - } else { - (bytes.as_ptr(), bytes.len() as i32) - }; - unsafe { ffi::engine_post_user_event(guard.slot.engine, tag, ptr, len) } + let guard = match get_engine_guard(engine_id) { + Ok(g) => g, + Err(rc) => return rc, + }; + + if payload.len() > (i32::MAX as usize) { + return ffi::ZR_ERR_LIMIT; + } + let bytes = payload.as_ref(); + let (ptr, len) = if bytes.is_empty() { + (std::ptr::null(), 0) + } else { + (bytes.as_ptr(), bytes.len() as i32) + }; + unsafe { ffi::engine_post_user_event(guard.slot.engine, tag, ptr, len) } } #[napi(js_name = "engineSetConfig")] pub fn engine_set_config(_env: Env, engine_id: u32, cfg: Option) -> napi::Result { - let guard = match get_engine_guard(engine_id) { - Ok(g) => g, - Err(rc) => return Ok(rc), - }; - if !guard.slot.is_owner_thread() { - return Ok(ffi::ZR_ERR_INVALID_ARGUMENT); - } - - let mut rcfg = create_default_runtime_cfg(); - if let Some(obj) = cfg { - apply_runtime_cfg_strict(&mut rcfg, &obj)?; - } else { - return Ok(ffi::ZR_ERR_INVALID_ARGUMENT); - } - - Ok(unsafe { ffi::engine_set_config(guard.slot.engine, &rcfg as *const _) }) + let guard = match get_engine_guard(engine_id) { + Ok(g) => g, + Err(rc) => return Ok(rc), + }; + if !guard.slot.is_owner_thread() { + return Ok(ffi::ZR_ERR_INVALID_ARGUMENT); + } + + let mut rcfg = create_default_runtime_cfg(); + if let Some(obj) = cfg { + apply_runtime_cfg_strict(&mut rcfg, &obj)?; + } else { + return Ok(ffi::ZR_ERR_INVALID_ARGUMENT); + } + + Ok(unsafe { ffi::engine_set_config(guard.slot.engine, &rcfg as *const _) }) } #[napi(js_name = "engineGetMetrics")] pub fn engine_get_metrics(engine_id: u32) -> napi::Result { - let guard = get_engine_guard(engine_id).map_err(|_| Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT"))?; - if !guard.slot.is_owner_thread() { - return Err(Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT")); - } - - let mut m = ffi::zr_metrics_t { - struct_size: std::mem::size_of::() as u32, - negotiated_engine_abi_major: 0, - negotiated_engine_abi_minor: 0, - negotiated_engine_abi_patch: 0, - negotiated_drawlist_version: 0, - negotiated_event_batch_version: 0, - frame_index: 0, - fps: 0, - _pad0: 0, - bytes_emitted_total: 0, - bytes_emitted_last_frame: 0, - _pad1: 0, - dirty_lines_last_frame: 0, - dirty_cols_last_frame: 0, - us_input_last_frame: 0, - us_drawlist_last_frame: 0, - us_diff_last_frame: 0, - us_write_last_frame: 0, - events_out_last_poll: 0, - events_dropped_total: 0, - arena_frame_high_water_bytes: 0, - arena_persistent_high_water_bytes: 0, - damage_rects_last_frame: 0, - damage_cells_last_frame: 0, - damage_full_frame: 0, - _pad2: [0, 0, 0], - }; - - let rc = unsafe { ffi::engine_get_metrics(guard.slot.engine, &mut m as *mut _) }; - if rc != ffi::ZR_OK { - return Err(Error::new(Status::GenericFailure, format!("engine_get_metrics failed: {rc}"))); - } - - Ok(EngineMetrics { - structSize: m.struct_size, - negotiatedEngineAbiMajor: m.negotiated_engine_abi_major, - negotiatedEngineAbiMinor: m.negotiated_engine_abi_minor, - negotiatedEngineAbiPatch: m.negotiated_engine_abi_patch, - negotiatedDrawlistVersion: m.negotiated_drawlist_version, - negotiatedEventBatchVersion: m.negotiated_event_batch_version, - frameIndex: BigInt { - sign_bit: false, - words: vec![m.frame_index], - }, - fps: m.fps, - bytesEmittedTotal: BigInt { - sign_bit: false, - words: vec![m.bytes_emitted_total], - }, - bytesEmittedLastFrame: m.bytes_emitted_last_frame, - dirtyLinesLastFrame: m.dirty_lines_last_frame, - dirtyColsLastFrame: m.dirty_cols_last_frame, - usInputLastFrame: m.us_input_last_frame, - usDrawlistLastFrame: m.us_drawlist_last_frame, - usDiffLastFrame: m.us_diff_last_frame, - usWriteLastFrame: m.us_write_last_frame, - eventsOutLastPoll: m.events_out_last_poll, - eventsDroppedTotal: m.events_dropped_total, - arenaFrameHighWaterBytes: BigInt { - sign_bit: false, - words: vec![m.arena_frame_high_water_bytes], - }, - arenaPersistentHighWaterBytes: BigInt { - sign_bit: false, - words: vec![m.arena_persistent_high_water_bytes], - }, - damageRectsLastFrame: m.damage_rects_last_frame, - damageCellsLastFrame: m.damage_cells_last_frame, - damageFullFrame: m.damage_full_frame != 0, - }) + let guard = get_engine_guard(engine_id) + .map_err(|_| Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT"))?; + if !guard.slot.is_owner_thread() { + return Err(Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT")); + } + + let mut m = ffi::zr_metrics_t { + struct_size: std::mem::size_of::() as u32, + negotiated_engine_abi_major: 0, + negotiated_engine_abi_minor: 0, + negotiated_engine_abi_patch: 0, + negotiated_drawlist_version: 0, + negotiated_event_batch_version: 0, + frame_index: 0, + fps: 0, + _pad0: 0, + bytes_emitted_total: 0, + bytes_emitted_last_frame: 0, + _pad1: 0, + dirty_lines_last_frame: 0, + dirty_cols_last_frame: 0, + us_input_last_frame: 0, + us_drawlist_last_frame: 0, + us_diff_last_frame: 0, + us_write_last_frame: 0, + events_out_last_poll: 0, + events_dropped_total: 0, + arena_frame_high_water_bytes: 0, + arena_persistent_high_water_bytes: 0, + damage_rects_last_frame: 0, + damage_cells_last_frame: 0, + damage_full_frame: 0, + _pad2: [0, 0, 0], + }; + + let rc = unsafe { ffi::engine_get_metrics(guard.slot.engine, &mut m as *mut _) }; + if rc != ffi::ZR_OK { + return Err(Error::new( + Status::GenericFailure, + format!("engine_get_metrics failed: {rc}"), + )); + } + + Ok(EngineMetrics { + structSize: m.struct_size, + negotiatedEngineAbiMajor: m.negotiated_engine_abi_major, + negotiatedEngineAbiMinor: m.negotiated_engine_abi_minor, + negotiatedEngineAbiPatch: m.negotiated_engine_abi_patch, + negotiatedDrawlistVersion: m.negotiated_drawlist_version, + negotiatedEventBatchVersion: m.negotiated_event_batch_version, + frameIndex: BigInt { + sign_bit: false, + words: vec![m.frame_index], + }, + fps: m.fps, + bytesEmittedTotal: BigInt { + sign_bit: false, + words: vec![m.bytes_emitted_total], + }, + bytesEmittedLastFrame: m.bytes_emitted_last_frame, + dirtyLinesLastFrame: m.dirty_lines_last_frame, + dirtyColsLastFrame: m.dirty_cols_last_frame, + usInputLastFrame: m.us_input_last_frame, + usDrawlistLastFrame: m.us_drawlist_last_frame, + usDiffLastFrame: m.us_diff_last_frame, + usWriteLastFrame: m.us_write_last_frame, + eventsOutLastPoll: m.events_out_last_poll, + eventsDroppedTotal: m.events_dropped_total, + arenaFrameHighWaterBytes: BigInt { + sign_bit: false, + words: vec![m.arena_frame_high_water_bytes], + }, + arenaPersistentHighWaterBytes: BigInt { + sign_bit: false, + words: vec![m.arena_persistent_high_water_bytes], + }, + damageRectsLastFrame: m.damage_rects_last_frame, + damageCellsLastFrame: m.damage_cells_last_frame, + damageFullFrame: m.damage_full_frame != 0, + }) } #[napi(js_name = "engineGetCaps")] pub fn engine_get_caps(engine_id: u32) -> napi::Result { - let guard = get_engine_guard(engine_id).map_err(|_| Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT"))?; - if !guard.slot.is_owner_thread() { - return Err(Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT")); - } - - let mut caps = ffi::zr_terminal_caps_t { - color_mode: 0, - supports_mouse: 0, - supports_bracketed_paste: 0, - supports_focus_events: 0, - supports_osc52: 0, - supports_sync_update: 0, - supports_scroll_region: 0, - supports_cursor_shape: 0, - supports_output_wait_writable: 0, - supports_underline_styles: 0, - supports_colored_underlines: 0, - supports_hyperlinks: 0, - sgr_attrs_supported: 0, - terminal_id: 0, - _pad1: [0, 0, 0], - cap_flags: 0, - cap_force_flags: 0, - cap_suppress_flags: 0, - }; - - let rc = unsafe { ffi::engine_get_caps(guard.slot.engine, &mut caps as *mut _) }; - if rc != ffi::ZR_OK { - return Err(Error::new(Status::GenericFailure, format!("engine_get_caps failed: {rc}"))); - } - - Ok(TerminalCaps { - colorMode: caps.color_mode as u32, - supportsMouse: caps.supports_mouse != 0, - supportsBracketedPaste: caps.supports_bracketed_paste != 0, - supportsFocusEvents: caps.supports_focus_events != 0, - supportsOsc52: caps.supports_osc52 != 0, - supportsSyncUpdate: caps.supports_sync_update != 0, - supportsScrollRegion: caps.supports_scroll_region != 0, - supportsCursorShape: caps.supports_cursor_shape != 0, - supportsOutputWaitWritable: caps.supports_output_wait_writable != 0, - supportsUnderlineStyles: caps.supports_underline_styles != 0, - supportsColoredUnderlines: caps.supports_colored_underlines != 0, - supportsHyperlinks: caps.supports_hyperlinks != 0, - sgrAttrsSupported: caps.sgr_attrs_supported, - }) + let guard = get_engine_guard(engine_id) + .map_err(|_| Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT"))?; + if !guard.slot.is_owner_thread() { + return Err(Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT")); + } + + let mut caps = ffi::zr_terminal_caps_t { + color_mode: 0, + supports_mouse: 0, + supports_bracketed_paste: 0, + supports_focus_events: 0, + supports_osc52: 0, + supports_sync_update: 0, + supports_scroll_region: 0, + supports_cursor_shape: 0, + supports_output_wait_writable: 0, + supports_underline_styles: 0, + supports_colored_underlines: 0, + supports_hyperlinks: 0, + sgr_attrs_supported: 0, + terminal_id: 0, + _pad1: [0, 0, 0], + cap_flags: 0, + cap_force_flags: 0, + cap_suppress_flags: 0, + }; + + let rc = unsafe { ffi::engine_get_caps(guard.slot.engine, &mut caps as *mut _) }; + if rc != ffi::ZR_OK { + return Err(Error::new( + Status::GenericFailure, + format!("engine_get_caps failed: {rc}"), + )); + } + + Ok(TerminalCaps { + colorMode: caps.color_mode as u32, + supportsMouse: caps.supports_mouse != 0, + supportsBracketedPaste: caps.supports_bracketed_paste != 0, + supportsFocusEvents: caps.supports_focus_events != 0, + supportsOsc52: caps.supports_osc52 != 0, + supportsSyncUpdate: caps.supports_sync_update != 0, + supportsScrollRegion: caps.supports_scroll_region != 0, + supportsCursorShape: caps.supports_cursor_shape != 0, + supportsOutputWaitWritable: caps.supports_output_wait_writable != 0, + supportsUnderlineStyles: caps.supports_underline_styles != 0, + supportsColoredUnderlines: caps.supports_colored_underlines != 0, + supportsHyperlinks: caps.supports_hyperlinks != 0, + sgrAttrsSupported: caps.sgr_attrs_supported, + }) } // ============================================================================= @@ -1212,1044 +1388,1142 @@ pub fn engine_get_caps(engine_id: u32) -> napi::Result { #[napi(object)] #[allow(non_snake_case)] pub struct DebugStats { - pub totalRecords: BigInt, - pub totalDropped: BigInt, - pub errorCount: u32, - pub warnCount: u32, - pub currentRingUsage: u32, - pub ringCapacity: u32, + pub totalRecords: BigInt, + pub totalDropped: BigInt, + pub errorCount: u32, + pub warnCount: u32, + pub currentRingUsage: u32, + pub ringCapacity: u32, } #[napi(object)] #[allow(non_snake_case)] pub struct DebugQueryResult { - pub recordsReturned: u32, - pub recordsAvailable: u32, - pub oldestRecordId: BigInt, - pub newestRecordId: BigInt, - pub recordsDropped: u32, + pub recordsReturned: u32, + pub recordsAvailable: u32, + pub oldestRecordId: BigInt, + pub newestRecordId: BigInt, + pub recordsDropped: u32, } const DEBUG_CFG_KEYS: &[(&str, &str)] = &[ - ("enabled", "enabled"), - ("ringCapacity", "ring_capacity"), - ("minSeverity", "min_severity"), - ("categoryMask", "category_mask"), - ("captureRawEvents", "capture_raw_events"), - ("captureDrawlistBytes", "capture_drawlist_bytes"), + ("enabled", "enabled"), + ("ringCapacity", "ring_capacity"), + ("minSeverity", "min_severity"), + ("categoryMask", "category_mask"), + ("captureRawEvents", "capture_raw_events"), + ("captureDrawlistBytes", "capture_drawlist_bytes"), ]; const DEBUG_QUERY_KEYS: &[(&str, &str)] = &[ - ("minRecordId", "min_record_id"), - ("maxRecordId", "max_record_id"), - ("minFrameId", "min_frame_id"), - ("maxFrameId", "max_frame_id"), - ("categoryMask", "category_mask"), - ("minSeverity", "min_severity"), - ("maxRecords", "max_records"), + ("minRecordId", "min_record_id"), + ("maxRecordId", "max_record_id"), + ("minFrameId", "min_frame_id"), + ("maxFrameId", "max_frame_id"), + ("categoryMask", "category_mask"), + ("minSeverity", "min_severity"), + ("maxRecords", "max_records"), ]; fn parse_debug_query_bigint_u64(sign_bit: bool, words: &[u64]) -> ParseResult { - // Reject negative values while still allowing canonical zero. - if sign_bit && words.iter().any(|w| *w != 0) { - return Err(()); - } - match words { - [] => Ok(0), - [value] => Ok(*value), - _ => Err(()), // More than 64 bits. - } + // Reject negative values while still allowing canonical zero. + if sign_bit && words.iter().any(|w| *w != 0) { + return Err(()); + } + match words { + [] => Ok(0), + [value] => Ok(*value), + _ => Err(()), // More than 64 bits. + } } fn js_u64(obj: &JsObject, primary: &str, alias: &str) -> ParseResult> { - for name in [primary, alias] { - let v = match obj.get_named_property::(name) { - Ok(v) => v, - Err(_) => continue, - }; - match v.get_type().map_err(|_| ())? { - ValueType::Undefined => continue, - ValueType::BigInt => { - let mut bi = unsafe { v.cast::() }; - let (sign_bit, words) = bi.get_words().map_err(|_| ())?; - let val = parse_debug_query_bigint_u64(sign_bit, &words)?; - return Ok(Some(val)); - } - ValueType::Number => { - let n = v.coerce_to_number().map_err(|_| ())?; - let f = n.get_double().map_err(|_| ())?; - if !f.is_finite() || f < 0.0 || f > (u64::MAX as f64) { - return Err(()); + for name in [primary, alias] { + let v = match obj.get_named_property::(name) { + Ok(v) => v, + Err(_) => continue, + }; + match v.get_type().map_err(|_| ())? { + ValueType::Undefined => continue, + ValueType::BigInt => { + let mut bi = unsafe { v.cast::() }; + let (sign_bit, words) = bi.get_words().map_err(|_| ())?; + let val = parse_debug_query_bigint_u64(sign_bit, &words)?; + return Ok(Some(val)); + } + ValueType::Number => { + let n = v.coerce_to_number().map_err(|_| ())?; + let f = n.get_double().map_err(|_| ())?; + if !f.is_finite() || f < 0.0 || f > (u64::MAX as f64) { + return Err(()); + } + return Ok(Some(f as u64)); + } + _ => return Err(()), } - return Ok(Some(f as u64)); - } - _ => return Err(()), } - } - Ok(None) + Ok(None) } fn apply_debug_cfg(dst: &mut ffi::zr_debug_config_t, obj: &JsObject) -> ParseResult<()> { - if let Some(v) = js_u8_bool(obj, "enabled", "enabled")? { - dst.enabled = v as u32; - } - if let Some(v) = js_u32(obj, "ringCapacity", "ring_capacity")? { - dst.ring_capacity = v; - } - if let Some(v) = js_u32(obj, "minSeverity", "min_severity")? { - dst.min_severity = v; - } - if let Some(v) = js_u32(obj, "categoryMask", "category_mask")? { - dst.category_mask = v; - } - if let Some(v) = js_u8_bool(obj, "captureRawEvents", "capture_raw_events")? { - dst.capture_raw_events = v as u32; - } - if let Some(v) = js_u8_bool(obj, "captureDrawlistBytes", "capture_drawlist_bytes")? { - dst.capture_drawlist_bytes = v as u32; - } - Ok(()) + if let Some(v) = js_u8_bool(obj, "enabled", "enabled")? { + dst.enabled = v as u32; + } + if let Some(v) = js_u32(obj, "ringCapacity", "ring_capacity")? { + dst.ring_capacity = v; + } + if let Some(v) = js_u32(obj, "minSeverity", "min_severity")? { + dst.min_severity = v; + } + if let Some(v) = js_u32(obj, "categoryMask", "category_mask")? { + dst.category_mask = v; + } + if let Some(v) = js_u8_bool(obj, "captureRawEvents", "capture_raw_events")? { + dst.capture_raw_events = v as u32; + } + if let Some(v) = js_u8_bool(obj, "captureDrawlistBytes", "capture_drawlist_bytes")? { + dst.capture_drawlist_bytes = v as u32; + } + Ok(()) } fn apply_debug_query(dst: &mut ffi::zr_debug_query_t, obj: &JsObject) -> ParseResult<()> { - if let Some(v) = js_u64(obj, "minRecordId", "min_record_id")? { - dst.min_record_id = v; - } - if let Some(v) = js_u64(obj, "maxRecordId", "max_record_id")? { - dst.max_record_id = v; - } - if let Some(v) = js_u64(obj, "minFrameId", "min_frame_id")? { - dst.min_frame_id = v; - } - if let Some(v) = js_u64(obj, "maxFrameId", "max_frame_id")? { - dst.max_frame_id = v; - } - if let Some(v) = js_u32(obj, "categoryMask", "category_mask")? { - dst.category_mask = v; - } - if let Some(v) = js_u32(obj, "minSeverity", "min_severity")? { - dst.min_severity = v; - } - if let Some(v) = js_u32(obj, "maxRecords", "max_records")? { - dst.max_records = v; - } - Ok(()) + if let Some(v) = js_u64(obj, "minRecordId", "min_record_id")? { + dst.min_record_id = v; + } + if let Some(v) = js_u64(obj, "maxRecordId", "max_record_id")? { + dst.max_record_id = v; + } + if let Some(v) = js_u64(obj, "minFrameId", "min_frame_id")? { + dst.min_frame_id = v; + } + if let Some(v) = js_u64(obj, "maxFrameId", "max_frame_id")? { + dst.max_frame_id = v; + } + if let Some(v) = js_u32(obj, "categoryMask", "category_mask")? { + dst.category_mask = v; + } + if let Some(v) = js_u32(obj, "minSeverity", "min_severity")? { + dst.min_severity = v; + } + if let Some(v) = js_u32(obj, "maxRecords", "max_records")? { + dst.max_records = v; + } + Ok(()) } #[napi(js_name = "engineDebugEnable")] -pub fn engine_debug_enable(_env: Env, engine_id: u32, config: Option) -> napi::Result { - let guard = match get_engine_guard(engine_id) { - Ok(g) => g, - Err(rc) => return Ok(rc), - }; - if !guard.slot.is_owner_thread() { - return Ok(ffi::ZR_ERR_INVALID_ARGUMENT); - } - - let mut cfg = ffi::zr_debug_config_t { - enabled: 1, - ring_capacity: 0, - min_severity: 0, - category_mask: 0xFFFFFFFF, // All categories - capture_raw_events: 0, - capture_drawlist_bytes: 0, - _pad0: 0, - _pad1: 0, - }; - - if let Some(obj) = config { - validate_known_keys(&obj, DEBUG_CFG_KEYS, "engineDebugEnable config")?; - apply_debug_cfg(&mut cfg, &obj).map_err(|_| Error::new(Status::InvalidArg, "engineDebugEnable: invalid config value"))?; - } - - Ok(unsafe { ffi::engine_debug_enable(guard.slot.engine, &cfg as *const _) }) +pub fn engine_debug_enable( + _env: Env, + engine_id: u32, + config: Option, +) -> napi::Result { + let guard = match get_engine_guard(engine_id) { + Ok(g) => g, + Err(rc) => return Ok(rc), + }; + if !guard.slot.is_owner_thread() { + return Ok(ffi::ZR_ERR_INVALID_ARGUMENT); + } + + let mut cfg = ffi::zr_debug_config_t { + enabled: 1, + ring_capacity: 0, + min_severity: 0, + category_mask: 0xFFFFFFFF, // All categories + capture_raw_events: 0, + capture_drawlist_bytes: 0, + _pad0: 0, + _pad1: 0, + }; + + if let Some(obj) = config { + validate_known_keys(&obj, DEBUG_CFG_KEYS, "engineDebugEnable config")?; + apply_debug_cfg(&mut cfg, &obj).map_err(|_| { + Error::new( + Status::InvalidArg, + "engineDebugEnable: invalid config value", + ) + })?; + } + + Ok(unsafe { ffi::engine_debug_enable(guard.slot.engine, &cfg as *const _) }) } #[napi(js_name = "engineDebugDisable")] pub fn engine_debug_disable(engine_id: u32) -> i32 { - let guard = match get_engine_guard(engine_id) { - Ok(g) => g, - Err(rc) => return rc, - }; - if !guard.slot.is_owner_thread() { - return ffi::ZR_ERR_INVALID_ARGUMENT; - } - - unsafe { ffi::engine_debug_disable(guard.slot.engine) }; - ffi::ZR_OK + let guard = match get_engine_guard(engine_id) { + Ok(g) => g, + Err(rc) => return rc, + }; + if !guard.slot.is_owner_thread() { + return ffi::ZR_ERR_INVALID_ARGUMENT; + } + + unsafe { ffi::engine_debug_disable(guard.slot.engine) }; + ffi::ZR_OK } #[napi(js_name = "engineDebugQuery")] pub fn engine_debug_query( - _env: Env, - engine_id: u32, - query: Option, - mut out_headers: Uint8Array, + _env: Env, + engine_id: u32, + query: Option, + mut out_headers: Uint8Array, ) -> napi::Result { - let guard = get_engine_guard(engine_id).map_err(|_| Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT"))?; - if !guard.slot.is_owner_thread() { - return Err(Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT")); - } - - let mut q = ffi::zr_debug_query_t { - min_record_id: 0, - max_record_id: 0, - min_frame_id: 0, - max_frame_id: 0, - category_mask: 0xFFFFFFFF, - min_severity: 0, - max_records: 0, - _pad0: 0, - }; - - if let Some(obj) = query { - validate_known_keys(&obj, DEBUG_QUERY_KEYS, "engineDebugQuery query")?; - apply_debug_query(&mut q, &obj).map_err(|_| Error::new(Status::InvalidArg, "engineDebugQuery: invalid query value"))?; - } - - let mut result = ffi::zr_debug_query_result_t { - records_returned: 0, - records_available: 0, - oldest_record_id: 0, - newest_record_id: 0, - records_dropped: 0, - _pad0: 0, - }; - - let out_headers_slice = out_headers.as_mut(); - let header_size = std::mem::size_of::(); - let header_align = std::mem::align_of::(); - let headers_cap = (out_headers_slice.len() / header_size) as u32; - - let headers_ptr: *mut ffi::zr_debug_record_header_t = if headers_cap == 0 { - std::ptr::null_mut() - } else { - let raw = out_headers_slice.as_mut_ptr(); - if (raw as usize) % header_align != 0 { - return Err(Error::new( - Status::InvalidArg, - "engineDebugQuery: outHeaders must be aligned for debug record headers", - )); - } - raw as *mut ffi::zr_debug_record_header_t - }; - - let rc = unsafe { - ffi::engine_debug_query( - guard.slot.engine, - &q as *const _, - headers_ptr, - headers_cap, - &mut result as *mut _, - ) - }; - - if rc != ffi::ZR_OK { - return Err(Error::new(Status::GenericFailure, format!("engine_debug_query failed: {rc}"))); - } - - Ok(DebugQueryResult { - recordsReturned: result.records_returned, - recordsAvailable: result.records_available, - oldestRecordId: BigInt { sign_bit: false, words: vec![result.oldest_record_id] }, - newestRecordId: BigInt { sign_bit: false, words: vec![result.newest_record_id] }, - recordsDropped: result.records_dropped, - }) + let guard = get_engine_guard(engine_id) + .map_err(|_| Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT"))?; + if !guard.slot.is_owner_thread() { + return Err(Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT")); + } + + let mut q = ffi::zr_debug_query_t { + min_record_id: 0, + max_record_id: 0, + min_frame_id: 0, + max_frame_id: 0, + category_mask: 0xFFFFFFFF, + min_severity: 0, + max_records: 0, + _pad0: 0, + }; + + if let Some(obj) = query { + validate_known_keys(&obj, DEBUG_QUERY_KEYS, "engineDebugQuery query")?; + apply_debug_query(&mut q, &obj) + .map_err(|_| Error::new(Status::InvalidArg, "engineDebugQuery: invalid query value"))?; + } + + let mut result = ffi::zr_debug_query_result_t { + records_returned: 0, + records_available: 0, + oldest_record_id: 0, + newest_record_id: 0, + records_dropped: 0, + _pad0: 0, + }; + + let out_headers_slice = out_headers.as_mut(); + let header_size = std::mem::size_of::(); + let header_align = std::mem::align_of::(); + let headers_cap = (out_headers_slice.len() / header_size) as u32; + + let headers_ptr: *mut ffi::zr_debug_record_header_t = if headers_cap == 0 { + std::ptr::null_mut() + } else { + let raw = out_headers_slice.as_mut_ptr(); + if (raw as usize) % header_align != 0 { + return Err(Error::new( + Status::InvalidArg, + "engineDebugQuery: outHeaders must be aligned for debug record headers", + )); + } + raw as *mut ffi::zr_debug_record_header_t + }; + + let rc = unsafe { + ffi::engine_debug_query( + guard.slot.engine, + &q as *const _, + headers_ptr, + headers_cap, + &mut result as *mut _, + ) + }; + + if rc != ffi::ZR_OK { + return Err(Error::new( + Status::GenericFailure, + format!("engine_debug_query failed: {rc}"), + )); + } + + Ok(DebugQueryResult { + recordsReturned: result.records_returned, + recordsAvailable: result.records_available, + oldestRecordId: BigInt { + sign_bit: false, + words: vec![result.oldest_record_id], + }, + newestRecordId: BigInt { + sign_bit: false, + words: vec![result.newest_record_id], + }, + recordsDropped: result.records_dropped, + }) } #[napi(js_name = "engineDebugGetPayload")] pub fn engine_debug_get_payload( - engine_id: u32, - record_id: BigInt, - mut out_payload: Uint8Array, + engine_id: u32, + record_id: BigInt, + mut out_payload: Uint8Array, ) -> napi::Result { - let guard = get_engine_guard(engine_id).map_err(|_| Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT"))?; - if !guard.slot.is_owner_thread() { - return Err(Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT")); - } - - let rid = parse_debug_query_bigint_u64(record_id.sign_bit, &record_id.words).map_err(|_| { - Error::new( - Status::InvalidArg, - "engineDebugGetPayload: recordId must be a non-negative u64", - ) - })?; - - let mut out_size: u32 = 0; - let out_cap = out_payload.len() as u32; - let out_ptr = out_payload.as_mut().as_mut_ptr(); - - let rc = unsafe { - ffi::engine_debug_get_payload( - guard.slot.engine, - rid, - out_ptr, - out_cap, - &mut out_size as *mut _, - ) - }; - - if rc != ffi::ZR_OK { - return Ok(rc); - } - - Ok(out_size as i32) + let guard = get_engine_guard(engine_id) + .map_err(|_| Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT"))?; + if !guard.slot.is_owner_thread() { + return Err(Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT")); + } + + let rid = parse_debug_query_bigint_u64(record_id.sign_bit, &record_id.words).map_err(|_| { + Error::new( + Status::InvalidArg, + "engineDebugGetPayload: recordId must be a non-negative u64", + ) + })?; + + let mut out_size: u32 = 0; + let out_cap = out_payload.len() as u32; + let out_ptr = out_payload.as_mut().as_mut_ptr(); + + let rc = unsafe { + ffi::engine_debug_get_payload( + guard.slot.engine, + rid, + out_ptr, + out_cap, + &mut out_size as *mut _, + ) + }; + + if rc != ffi::ZR_OK { + return Ok(rc); + } + + Ok(out_size as i32) } #[napi(js_name = "engineDebugGetStats")] pub fn engine_debug_get_stats(engine_id: u32) -> napi::Result { - let guard = get_engine_guard(engine_id).map_err(|_| Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT"))?; - if !guard.slot.is_owner_thread() { - return Err(Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT")); - } - - let mut stats = ffi::zr_debug_stats_t { - total_records: 0, - total_dropped: 0, - error_count: 0, - warn_count: 0, - current_ring_usage: 0, - ring_capacity: 0, - }; - - let rc = unsafe { ffi::engine_debug_get_stats(guard.slot.engine, &mut stats as *mut _) }; - if rc != ffi::ZR_OK { - return Err(Error::new(Status::GenericFailure, format!("engine_debug_get_stats failed: {rc}"))); - } - - Ok(DebugStats { - totalRecords: BigInt { sign_bit: false, words: vec![stats.total_records] }, - totalDropped: BigInt { sign_bit: false, words: vec![stats.total_dropped] }, - errorCount: stats.error_count, - warnCount: stats.warn_count, - currentRingUsage: stats.current_ring_usage, - ringCapacity: stats.ring_capacity, - }) + let guard = get_engine_guard(engine_id) + .map_err(|_| Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT"))?; + if !guard.slot.is_owner_thread() { + return Err(Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT")); + } + + let mut stats = ffi::zr_debug_stats_t { + total_records: 0, + total_dropped: 0, + error_count: 0, + warn_count: 0, + current_ring_usage: 0, + ring_capacity: 0, + }; + + let rc = unsafe { ffi::engine_debug_get_stats(guard.slot.engine, &mut stats as *mut _) }; + if rc != ffi::ZR_OK { + return Err(Error::new( + Status::GenericFailure, + format!("engine_debug_get_stats failed: {rc}"), + )); + } + + Ok(DebugStats { + totalRecords: BigInt { + sign_bit: false, + words: vec![stats.total_records], + }, + totalDropped: BigInt { + sign_bit: false, + words: vec![stats.total_dropped], + }, + errorCount: stats.error_count, + warnCount: stats.warn_count, + currentRingUsage: stats.current_ring_usage, + ringCapacity: stats.ring_capacity, + }) } #[napi(js_name = "engineDebugExport")] pub fn engine_debug_export(engine_id: u32, mut out_buf: Uint8Array) -> i32 { - let guard = match get_engine_guard(engine_id) { - Ok(g) => g, - Err(rc) => return rc, - }; - if !guard.slot.is_owner_thread() { - return ffi::ZR_ERR_INVALID_ARGUMENT; - } - - let out_cap = out_buf.len(); - let out_ptr = out_buf.as_mut().as_mut_ptr(); - - unsafe { ffi::engine_debug_export(guard.slot.engine, out_ptr, out_cap) } + let guard = match get_engine_guard(engine_id) { + Ok(g) => g, + Err(rc) => return rc, + }; + if !guard.slot.is_owner_thread() { + return ffi::ZR_ERR_INVALID_ARGUMENT; + } + + let out_cap = out_buf.len(); + let out_ptr = out_buf.as_mut().as_mut_ptr(); + + unsafe { ffi::engine_debug_export(guard.slot.engine, out_ptr, out_cap) } } #[napi(js_name = "engineDebugReset")] pub fn engine_debug_reset(engine_id: u32) -> i32 { - let guard = match get_engine_guard(engine_id) { - Ok(g) => g, - Err(rc) => return rc, - }; - if !guard.slot.is_owner_thread() { - return ffi::ZR_ERR_INVALID_ARGUMENT; - } - - unsafe { ffi::engine_debug_reset(guard.slot.engine) }; - ffi::ZR_OK + let guard = match get_engine_guard(engine_id) { + Ok(g) => g, + Err(rc) => return rc, + }; + if !guard.slot.is_owner_thread() { + return ffi::ZR_ERR_INVALID_ARGUMENT; + } + + unsafe { ffi::engine_debug_reset(guard.slot.engine) }; + ffi::ZR_OK } #[cfg(test)] mod tests { - use super::{ffi, parse_debug_query_bigint_u64}; - - const ATTR_BOLD: u32 = 1 << 0; - const ATTR_UNDERLINE: u32 = 1 << 2; - const ATTR_DIM: u32 = 1 << 4; - - fn contains_subsequence(haystack: &[u8], needle: &[u8]) -> bool { - if needle.is_empty() { - return true; - } - haystack.windows(needle.len()).any(|w| w == needle) - } - - fn style_with_attrs(attrs: u32) -> ffi::zr_style_t { - ffi::zr_style_t { - fg_rgb: 0, - bg_rgb: 0, - attrs, - reserved: 0, - underline_rgb: 0, - link_ref: 0, - } - } - - fn style_plain() -> ffi::zr_style_t { - ffi::zr_style_t { - fg_rgb: 0, - bg_rgb: 0, - attrs: 0, - reserved: 0, - underline_rgb: 0, - link_ref: 0, - } - } - - struct SingleCellFramebuffer { - raw: ffi::zr_fb_t, - } - - impl SingleCellFramebuffer { - fn with_attrs(attrs: u32) -> Self { - let mut raw = ffi::zr_fb_t { - cols: 0, - rows: 0, - cells: std::ptr::null_mut(), - links: std::ptr::null_mut(), - links_len: 0, - links_cap: 0, - link_bytes: std::ptr::null_mut(), - link_bytes_len: 0, - link_bytes_cap: 0, - }; - - let rc = unsafe { ffi::zr_fb_init(&mut raw as *mut _, 1, 1) }; - assert_eq!(rc, ffi::ZR_OK, "zr_fb_init must succeed for test framebuffer"); - - let cell = unsafe { ffi::zr_fb_cell(&mut raw as *mut _, 0, 0) }; - assert!(!cell.is_null(), "zr_fb_cell(0,0) must return a valid pointer"); - unsafe { - (*cell).glyph = [0; 32]; - (*cell).glyph[0] = b'X'; - (*cell).glyph_len = 1; - (*cell).width = 1; - (*cell)._pad0 = 0; - (*cell).style = style_with_attrs(attrs); - } - - Self { raw } - } - } - - impl Drop for SingleCellFramebuffer { - fn drop(&mut self) { - unsafe { ffi::zr_fb_release(&mut self.raw as *mut _) }; - } - } - - struct TestFramebuffer { - raw: ffi::zr_fb_t, - } - - impl TestFramebuffer { - fn new(cols: u32, rows: u32) -> Self { - let mut raw = ffi::zr_fb_t { - cols: 0, - rows: 0, - cells: std::ptr::null_mut(), - links: std::ptr::null_mut(), - links_len: 0, - links_cap: 0, - link_bytes: std::ptr::null_mut(), - link_bytes_len: 0, - link_bytes_cap: 0, - }; - let rc = unsafe { ffi::zr_fb_init(&mut raw as *mut _, cols, rows) }; - assert_eq!(rc, ffi::ZR_OK, "zr_fb_init must succeed for test framebuffer"); - let rc_clear = unsafe { ffi::zr_fb_clear(&mut raw as *mut _, &style_plain() as *const _) }; - assert_eq!(rc_clear, ffi::ZR_OK, "zr_fb_clear must succeed for test framebuffer"); - Self { raw } - } - - fn set_cell(&mut self, x: u32, y: u32, glyph: &[u8], width: u8, style: ffi::zr_style_t) { - assert!( - glyph.len() <= 32, - "glyph length must fit ZR_CELL_GLYPH_MAX (got {})", - glyph.len() - ); - let cell = unsafe { ffi::zr_fb_cell(&mut self.raw as *mut _, x, y) }; - assert!(!cell.is_null(), "zr_fb_cell({x},{y}) must return a valid pointer"); - unsafe { - (*cell).glyph = [0; 32]; - for (i, b) in glyph.iter().copied().enumerate() { - (*cell).glyph[i] = b; + use super::{ffi, parse_debug_query_bigint_u64}; + + const ATTR_BOLD: u32 = 1 << 0; + const ATTR_UNDERLINE: u32 = 1 << 2; + const ATTR_DIM: u32 = 1 << 4; + + fn contains_subsequence(haystack: &[u8], needle: &[u8]) -> bool { + if needle.is_empty() { + return true; } - (*cell).glyph_len = glyph.len() as u8; - (*cell).width = width; - (*cell)._pad0 = 0; - (*cell).style = style; - } + haystack.windows(needle.len()).any(|w| w == needle) } - fn set_cell_link_ref(&mut self, x: u32, y: u32, link_ref: u32) { - let cell = unsafe { ffi::zr_fb_cell(&mut self.raw as *mut _, x, y) }; - assert!(!cell.is_null(), "zr_fb_cell({x},{y}) must return a valid pointer"); - unsafe { - (*cell).style.link_ref = link_ref; - } + fn style_with_attrs(attrs: u32) -> ffi::zr_style_t { + ffi::zr_style_t { + fg_rgb: 0, + bg_rgb: 0, + attrs, + reserved: 0, + underline_rgb: 0, + link_ref: 0, + } } - fn cell_link_ref(&mut self, x: u32, y: u32) -> u32 { - let cell = unsafe { ffi::zr_fb_cell(&mut self.raw as *mut _, x, y) }; - assert!(!cell.is_null(), "zr_fb_cell({x},{y}) must return a valid pointer"); - unsafe { (*cell).style.link_ref } + fn style_plain() -> ffi::zr_style_t { + ffi::zr_style_t { + fg_rgb: 0, + bg_rgb: 0, + attrs: 0, + reserved: 0, + underline_rgb: 0, + link_ref: 0, + } } - } - impl Drop for TestFramebuffer { - fn drop(&mut self) { - unsafe { ffi::zr_fb_release(&mut self.raw as *mut _) }; - } - } - - fn render_diff_bytes( - prev: &ffi::zr_fb_t, - next: &ffi::zr_fb_t, - initial_style: ffi::zr_style_t, - ) -> Vec { - let caps = ffi::plat_caps_t { - color_mode: 3, - supports_mouse: 0, - supports_bracketed_paste: 0, - supports_focus_events: 0, - supports_osc52: 0, - supports_sync_update: 0, - supports_scroll_region: 0, - supports_cursor_shape: 1, - supports_output_wait_writable: 0, - supports_underline_styles: 0, - supports_colored_underlines: 0, - supports_hyperlinks: 0, - sgr_attrs_supported: u32::MAX, - }; - let limits = unsafe { ffi::zr_engine_config_default() }.limits; - let initial_term_state = ffi::zr_term_state_t { - cursor_x: 0, - cursor_y: 0, - cursor_visible: 1, - cursor_shape: 0, - cursor_blink: 0, - flags: 0, - style: initial_style, - }; - let desired_cursor_state = ffi::zr_cursor_state_t { - x: -1, - y: -1, - shape: 0, - visible: 1, - blink: 0, - reserved0: 0, - }; + struct SingleCellFramebuffer { + raw: ffi::zr_fb_t, + } - let mut scratch_damage_rects = vec![ - ffi::zr_damage_rect_t { - x0: 0, - y0: 0, - x1: 0, - y1: 0, - }; - limits.diff_max_damage_rects as usize - ]; - let mut out = [0u8; 1024]; - let mut out_len = 0usize; - let mut out_final_term_state: ffi::zr_term_state_t = unsafe { std::mem::zeroed() }; - let mut out_stats: ffi::zr_diff_stats_t = unsafe { std::mem::zeroed() }; + impl SingleCellFramebuffer { + fn with_attrs(attrs: u32) -> Self { + let mut raw = ffi::zr_fb_t { + cols: 0, + rows: 0, + cells: std::ptr::null_mut(), + links: std::ptr::null_mut(), + links_len: 0, + links_cap: 0, + link_bytes: std::ptr::null_mut(), + link_bytes_len: 0, + link_bytes_cap: 0, + }; + + let rc = unsafe { ffi::zr_fb_init(&mut raw as *mut _, 1, 1) }; + assert_eq!( + rc, + ffi::ZR_OK, + "zr_fb_init must succeed for test framebuffer" + ); + + let cell = unsafe { ffi::zr_fb_cell(&mut raw as *mut _, 0, 0) }; + assert!( + !cell.is_null(), + "zr_fb_cell(0,0) must return a valid pointer" + ); + unsafe { + (*cell).glyph = [0; 32]; + (*cell).glyph[0] = b'X'; + (*cell).glyph_len = 1; + (*cell).width = 1; + (*cell)._pad0 = 0; + (*cell).style = style_with_attrs(attrs); + } + + Self { raw } + } + } - let rc = unsafe { - ffi::zr_diff_render( - prev as *const _, - next as *const _, - &caps as *const _, - &initial_term_state as *const _, - &desired_cursor_state as *const _, - &limits as *const _, - scratch_damage_rects.as_mut_ptr(), - scratch_damage_rects.len() as u32, - 0, - out.as_mut_ptr(), - out.len(), - &mut out_len as *mut _, - &mut out_final_term_state as *mut _, - &mut out_stats as *mut _, - ) - }; - assert_eq!(rc, ffi::ZR_OK, "zr_diff_render must succeed"); - assert!(out_len > 0, "zr_diff_render must emit output"); - out[..out_len].to_vec() - } - - fn render_style_transition(current_attrs: u32, desired_attrs: u32) -> Vec { - let prev = SingleCellFramebuffer::with_attrs(current_attrs); - let next = SingleCellFramebuffer::with_attrs(desired_attrs); - render_diff_bytes(&prev.raw, &next.raw, style_with_attrs(current_attrs)) - } - - fn cell_snapshot(fb: &mut ffi::zr_fb_t, x: u32, y: u32) -> (u8, u8) { - let cell = unsafe { ffi::zr_fb_cell(fb as *mut _, x, y) }; - assert!(!cell.is_null(), "cell must exist at ({x},{y})"); - unsafe { ((*cell).glyph[0], (*cell).width) } - } - - #[test] - fn fb_links_clone_from_failure_has_no_partial_effects() { - let mut dst = TestFramebuffer::new(2, 1); - let uri = b"https://example.test/rezi"; - let mut link_ref = 0u32; - let intern_rc = unsafe { - ffi::zr_fb_link_intern( - &mut dst.raw as *mut _, - uri.as_ptr(), - uri.len(), - std::ptr::null(), - 0, - &mut link_ref as *mut _, - ) - }; - assert_eq!(intern_rc, ffi::ZR_OK, "zr_fb_link_intern must seed destination link state"); - assert_eq!(link_ref, 1u32); - - let before_links_ptr = dst.raw.links; - let before_links_len = dst.raw.links_len; - let before_links_cap = dst.raw.links_cap; - let before_link_bytes_ptr = dst.raw.link_bytes; - let before_link_bytes_len = dst.raw.link_bytes_len; - let before_link_bytes_cap = dst.raw.link_bytes_cap; - assert!(!before_links_ptr.is_null(), "seeded links pointer must be non-null"); - assert!(!before_link_bytes_ptr.is_null(), "seeded link-bytes pointer must be non-null"); - - let before_first_link = unsafe { *before_links_ptr }; - let before_link_bytes = - unsafe { std::slice::from_raw_parts(before_link_bytes_ptr, before_link_bytes_len as usize).to_vec() }; - - let invalid_src = ffi::zr_fb_t { - cols: dst.raw.cols, - rows: dst.raw.rows, - cells: dst.raw.cells, - links: std::ptr::null_mut(), - links_len: 1, - links_cap: 0, - link_bytes: std::ptr::null_mut(), - link_bytes_len: before_link_bytes_len, - link_bytes_cap: 0, - }; - let clone_rc = unsafe { ffi::zr_fb_links_clone_from(&mut dst.raw as *mut _, &invalid_src as *const _) }; - assert_eq!(clone_rc, ffi::ZR_ERR_INVALID_ARGUMENT); - - assert_eq!(dst.raw.links, before_links_ptr); - assert_eq!(dst.raw.links_len, before_links_len); - assert_eq!(dst.raw.links_cap, before_links_cap); - assert_eq!(dst.raw.link_bytes, before_link_bytes_ptr); - assert_eq!(dst.raw.link_bytes_len, before_link_bytes_len); - assert_eq!(dst.raw.link_bytes_cap, before_link_bytes_cap); - - let after_first_link = unsafe { *dst.raw.links }; - assert_eq!(after_first_link.uri_off, before_first_link.uri_off); - assert_eq!(after_first_link.uri_len, before_first_link.uri_len); - assert_eq!(after_first_link.id_off, before_first_link.id_off); - assert_eq!(after_first_link.id_len, before_first_link.id_len); - - let after_link_bytes = - unsafe { std::slice::from_raw_parts(dst.raw.link_bytes, dst.raw.link_bytes_len as usize) }; - assert_eq!(after_link_bytes, before_link_bytes.as_slice()); - } - - #[test] - fn fb_link_intern_compacts_stale_refs_and_bounds_growth() { - const LINK_ENTRY_MAX_BYTES: u32 = 2083 + 2083; - let mut fb = TestFramebuffer::new(2, 1); - let persistent_uri = b"https://example.test/persistent"; - - let mut persistent_ref = 0u32; - let seed_rc = unsafe { - ffi::zr_fb_link_intern( - &mut fb.raw as *mut _, - persistent_uri.as_ptr(), - persistent_uri.len(), - std::ptr::null(), - 0, - &mut persistent_ref as *mut _, - ) - }; - assert_eq!(seed_rc, ffi::ZR_OK); - assert_ne!(persistent_ref, 0); - fb.set_cell_link_ref(0, 0, persistent_ref); - - let mut peak_links_len = fb.raw.links_len; - let mut peak_link_bytes_len = fb.raw.link_bytes_len; - - for i in 0..64u32 { - let uri = format!("https://example.test/ephemeral/{i}"); - let mut ref_i = 0u32; - let rc = unsafe { - ffi::zr_fb_link_intern( - &mut fb.raw as *mut _, - uri.as_ptr(), - uri.len(), - std::ptr::null(), - 0, - &mut ref_i as *mut _, - ) - }; - assert_eq!(rc, ffi::ZR_OK, "zr_fb_link_intern failed at iteration {i}"); - assert!(ref_i >= 1 && ref_i <= fb.raw.links_len); - - fb.set_cell_link_ref(1, 0, ref_i); - - let live_ref0 = fb.cell_link_ref(0, 0); - let live_ref1 = fb.cell_link_ref(1, 0); - assert!(live_ref0 >= 1 && live_ref0 <= fb.raw.links_len, "cell(0,0) link_ref must remain valid"); - assert!(live_ref1 >= 1 && live_ref1 <= fb.raw.links_len, "cell(1,0) link_ref must remain valid"); - - peak_links_len = peak_links_len.max(fb.raw.links_len); - peak_link_bytes_len = peak_link_bytes_len.max(fb.raw.link_bytes_len); - } - - assert!( - peak_links_len <= 5, - "link table must stay bounded for 2-cell framebuffer (peak={peak_links_len})", - ); - assert!( - peak_link_bytes_len <= 5 * LINK_ENTRY_MAX_BYTES, - "link byte arena must stay bounded for 2-cell framebuffer (peak={peak_link_bytes_len})", - ); - - let mut uri_ptr: *const u8 = std::ptr::null(); - let mut uri_len: usize = 0; - let mut id_ptr: *const u8 = std::ptr::null(); - let mut id_len: usize = 0; - let persistent_cell_ref = fb.cell_link_ref(0, 0); - let lookup_rc = unsafe { - ffi::zr_fb_link_lookup( - &fb.raw as *const _, - persistent_cell_ref, - &mut uri_ptr as *mut _, - &mut uri_len as *mut _, - &mut id_ptr as *mut _, - &mut id_len as *mut _, - ) - }; - assert_eq!(lookup_rc, ffi::ZR_OK); - assert_eq!(id_len, 0); - assert!(id_ptr.is_null()); - assert!(!uri_ptr.is_null()); - - let resolved_uri = unsafe { std::slice::from_raw_parts(uri_ptr, uri_len) }; - assert_eq!(resolved_uri, persistent_uri); - } - - #[test] - fn ffi_layout_matches_vendored_headers() { - use std::mem::{align_of, size_of}; - use std::ptr::addr_of; - - assert_eq!(size_of::(), 24); - assert_eq!(align_of::(), 4); - assert_eq!(size_of::(), 60); - assert_eq!(size_of::(), 36); - assert_eq!(size_of::(), 16); - assert_eq!(align_of::(), 4); - - let caps = std::mem::MaybeUninit::::uninit(); - let base = caps.as_ptr(); - unsafe { - assert_eq!(addr_of!((*base).color_mode) as usize - base as usize, 0); - assert_eq!(addr_of!((*base).supports_output_wait_writable) as usize - base as usize, 8); - assert_eq!(addr_of!((*base).supports_underline_styles) as usize - base as usize, 9); - assert_eq!(addr_of!((*base).supports_colored_underlines) as usize - base as usize, 10); - assert_eq!(addr_of!((*base).supports_hyperlinks) as usize - base as usize, 11); - assert_eq!(addr_of!((*base).sgr_attrs_supported) as usize - base as usize, 12); - } - - if cfg!(target_pointer_width = "64") { - assert_eq!(size_of::(), 48); - assert_eq!(align_of::(), 8); - } else if cfg!(target_pointer_width = "32") { - assert_eq!(size_of::(), 36); - assert_eq!(align_of::(), 4); - } - } - - #[test] - fn clip_edge_write_over_continuation_cleans_lead_pair() { - let mut fb = ffi::zr_fb_t { - cols: 0, - rows: 0, - cells: std::ptr::null_mut(), - links: std::ptr::null_mut(), - links_len: 0, - links_cap: 0, - link_bytes: std::ptr::null_mut(), - link_bytes_len: 0, - link_bytes_cap: 0, - }; - let init_rc = unsafe { ffi::zr_fb_init(&mut fb as *mut _, 4, 1) }; - assert_eq!(init_rc, ffi::ZR_OK); - - let clear_rc = unsafe { ffi::zr_fb_clear(&mut fb as *mut _, &style_plain() as *const _) }; - assert_eq!(clear_rc, ffi::ZR_OK); - - let mut clip_stack = [ - ffi::zr_rect_t { - x: 0, - y: 0, - w: 4, - h: 1, - }, - ffi::zr_rect_t { - x: 0, - y: 0, - w: 0, - h: 0, - }, - ]; - let mut painter = ffi::zr_fb_painter_t { - fb: std::ptr::null_mut(), - clip_stack: std::ptr::null_mut(), - clip_cap: 0, - clip_len: 0, - }; - let begin_rc = unsafe { - ffi::zr_fb_painter_begin( - &mut painter as *mut _, - &mut fb as *mut _, - clip_stack.as_mut_ptr(), - clip_stack.len() as u32, - ) - }; - assert_eq!(begin_rc, ffi::ZR_OK); - - let wide_bytes = b"W"; - let write_wide_rc = unsafe { - ffi::zr_fb_put_grapheme( - &mut painter as *mut _, - 1, - 0, - wide_bytes.as_ptr(), - wide_bytes.len(), - 2, - &style_plain() as *const _, - ) - }; - assert_eq!(write_wide_rc, ffi::ZR_OK); - - let push_rc = unsafe { - ffi::zr_fb_clip_push( - &mut painter as *mut _, - ffi::zr_rect_t { - x: 2, - y: 0, - w: 1, - h: 1, - }, - ) - }; - assert_eq!(push_rc, ffi::ZR_OK); - - let a_bytes = b"A"; - let write_a_rc = unsafe { - ffi::zr_fb_put_grapheme( - &mut painter as *mut _, - 2, - 0, - a_bytes.as_ptr(), - a_bytes.len(), - 1, - &style_plain() as *const _, - ) - }; - assert_eq!(write_a_rc, ffi::ZR_OK); - - let pop_rc = unsafe { ffi::zr_fb_clip_pop(&mut painter as *mut _) }; - assert_eq!(pop_rc, ffi::ZR_OK); - - let (x1_ch, x1_w) = cell_snapshot(&mut fb, 1, 0); - let (x2_ch, x2_w) = cell_snapshot(&mut fb, 2, 0); - assert_eq!(x1_ch, b' '); - assert_eq!(x1_w, 1, "wide lead should be cleared when continuation is overwritten"); - assert_eq!(x2_ch, b'A'); - assert_eq!(x2_w, 1); - - unsafe { ffi::zr_fb_release(&mut fb as *mut _) }; - } - - #[test] - fn clip_edge_write_over_wide_lead_cleans_hidden_continuation() { - let mut fb = ffi::zr_fb_t { - cols: 0, - rows: 0, - cells: std::ptr::null_mut(), - links: std::ptr::null_mut(), - links_len: 0, - links_cap: 0, - link_bytes: std::ptr::null_mut(), - link_bytes_len: 0, - link_bytes_cap: 0, - }; - let init_rc = unsafe { ffi::zr_fb_init(&mut fb as *mut _, 4, 1) }; - assert_eq!(init_rc, ffi::ZR_OK); - - let clear_rc = unsafe { ffi::zr_fb_clear(&mut fb as *mut _, &style_plain() as *const _) }; - assert_eq!(clear_rc, ffi::ZR_OK); - - let mut clip_stack = [ - ffi::zr_rect_t { - x: 0, - y: 0, - w: 4, - h: 1, - }, - ffi::zr_rect_t { - x: 0, - y: 0, - w: 0, - h: 0, - }, - ]; - let mut painter = ffi::zr_fb_painter_t { - fb: std::ptr::null_mut(), - clip_stack: std::ptr::null_mut(), - clip_cap: 0, - clip_len: 0, - }; - let begin_rc = unsafe { - ffi::zr_fb_painter_begin( - &mut painter as *mut _, - &mut fb as *mut _, - clip_stack.as_mut_ptr(), - clip_stack.len() as u32, - ) - }; - assert_eq!(begin_rc, ffi::ZR_OK); - - let wide_bytes = b"W"; - let write_wide_rc = unsafe { - ffi::zr_fb_put_grapheme( - &mut painter as *mut _, - 1, - 0, - wide_bytes.as_ptr(), - wide_bytes.len(), - 2, - &style_plain() as *const _, - ) - }; - assert_eq!(write_wide_rc, ffi::ZR_OK); - - let push_rc = unsafe { - ffi::zr_fb_clip_push( - &mut painter as *mut _, - ffi::zr_rect_t { - x: 1, - y: 0, - w: 1, - h: 1, - }, - ) - }; - assert_eq!(push_rc, ffi::ZR_OK); - - let b_bytes = b"B"; - let write_b_rc = unsafe { - ffi::zr_fb_put_grapheme( - &mut painter as *mut _, - 1, - 0, - b_bytes.as_ptr(), - b_bytes.len(), - 1, - &style_plain() as *const _, - ) - }; - assert_eq!(write_b_rc, ffi::ZR_OK); - - let pop_rc = unsafe { ffi::zr_fb_clip_pop(&mut painter as *mut _) }; - assert_eq!(pop_rc, ffi::ZR_OK); - - let (x1_ch, x1_w) = cell_snapshot(&mut fb, 1, 0); - let (x2_ch, x2_w) = cell_snapshot(&mut fb, 2, 0); - assert_eq!(x1_ch, b'B'); - assert_eq!(x1_w, 1); - assert_eq!(x2_ch, b' '); - assert_eq!(x2_w, 1, "continuation outside clip should be cleaned"); - - unsafe { ffi::zr_fb_release(&mut fb as *mut _) }; - } - - #[test] - fn diff_reanchors_cursor_after_non_ascii_cell() { - let prev = TestFramebuffer::new(2, 1); - let mut next = TestFramebuffer::new(2, 1); - next.set_cell(0, 0, "✓".as_bytes(), 1, style_plain()); - next.set_cell(1, 0, b"A", 1, style_plain()); - - let out = render_diff_bytes(&prev.raw, &next.raw, style_plain()); - assert!( - contains_subsequence(&out, b"\x1b[1;2H"), - "expected explicit CUP for second cell after non-ascii glyph: {:?}", - String::from_utf8_lossy(&out), - ); - } - - #[test] - fn debug_query_bigint_u64_accepts_in_range_values() { - assert_eq!(parse_debug_query_bigint_u64(false, &[]), Ok(0)); - assert_eq!(parse_debug_query_bigint_u64(false, &[0]), Ok(0)); - assert_eq!(parse_debug_query_bigint_u64(false, &[123]), Ok(123)); - assert_eq!(parse_debug_query_bigint_u64(false, &[u64::MAX]), Ok(u64::MAX)); - } - - #[test] - fn debug_query_bigint_u64_rejects_negative_values() { - assert!(parse_debug_query_bigint_u64(true, &[1]).is_err()); - assert!(parse_debug_query_bigint_u64(true, &[u64::MAX]).is_err()); - } - - #[test] - fn debug_query_bigint_u64_rejects_overflow_values() { - assert!(parse_debug_query_bigint_u64(false, &[0, 1]).is_err()); - assert!(parse_debug_query_bigint_u64(false, &[u64::MAX, 1]).is_err()); - } - - #[test] - fn diff_emits_dim_and_normal_intensity_sequences() { - let to_dim = render_style_transition(0, ATTR_DIM); - assert!( - contains_subsequence(&to_dim, b"\x1b[0;2;"), - "expected dim SGR sequence in output: {:?}", - String::from_utf8_lossy(&to_dim), - ); - - let to_normal = render_style_transition(ATTR_DIM, 0); - assert!( - contains_subsequence(&to_normal, b"\x1b[0;38;"), - "expected normal-intensity SGR sequence in output: {:?}", - String::from_utf8_lossy(&to_normal), - ); - } - - #[test] - fn diff_reapplies_intensity_when_switching_bold_and_dim() { - let dim_to_bold = render_style_transition(ATTR_DIM, ATTR_BOLD); - assert!( - contains_subsequence(&dim_to_bold, b"\x1b[0;1;"), - "expected dim->bold transition to emit bold SGR: {:?}", - String::from_utf8_lossy(&dim_to_bold), - ); - - let bold_to_dim = render_style_transition(ATTR_BOLD, ATTR_DIM); - assert!( - contains_subsequence(&bold_to_dim, b"\x1b[0;2;"), - "expected bold->dim transition to emit dim SGR: {:?}", - String::from_utf8_lossy(&bold_to_dim), - ); - } - - #[test] - fn diff_preserves_non_intensity_attr_delta_path() { - let dim_to_dim_underline = render_style_transition(ATTR_DIM, ATTR_DIM | ATTR_UNDERLINE); - assert!( - contains_subsequence(&dim_to_dim_underline, b"\x1b[0;2;4;"), - "expected underline+dim sequence in output: {:?}", - String::from_utf8_lossy(&dim_to_dim_underline), - ); - } + impl Drop for SingleCellFramebuffer { + fn drop(&mut self) { + unsafe { ffi::zr_fb_release(&mut self.raw as *mut _) }; + } + } + + struct TestFramebuffer { + raw: ffi::zr_fb_t, + } + + impl TestFramebuffer { + fn new(cols: u32, rows: u32) -> Self { + let mut raw = ffi::zr_fb_t { + cols: 0, + rows: 0, + cells: std::ptr::null_mut(), + links: std::ptr::null_mut(), + links_len: 0, + links_cap: 0, + link_bytes: std::ptr::null_mut(), + link_bytes_len: 0, + link_bytes_cap: 0, + }; + let rc = unsafe { ffi::zr_fb_init(&mut raw as *mut _, cols, rows) }; + assert_eq!( + rc, + ffi::ZR_OK, + "zr_fb_init must succeed for test framebuffer" + ); + let rc_clear = + unsafe { ffi::zr_fb_clear(&mut raw as *mut _, &style_plain() as *const _) }; + assert_eq!( + rc_clear, + ffi::ZR_OK, + "zr_fb_clear must succeed for test framebuffer" + ); + Self { raw } + } + + fn set_cell(&mut self, x: u32, y: u32, glyph: &[u8], width: u8, style: ffi::zr_style_t) { + assert!( + glyph.len() <= 32, + "glyph length must fit ZR_CELL_GLYPH_MAX (got {})", + glyph.len() + ); + let cell = unsafe { ffi::zr_fb_cell(&mut self.raw as *mut _, x, y) }; + assert!( + !cell.is_null(), + "zr_fb_cell({x},{y}) must return a valid pointer" + ); + unsafe { + (*cell).glyph = [0; 32]; + for (i, b) in glyph.iter().copied().enumerate() { + (*cell).glyph[i] = b; + } + (*cell).glyph_len = glyph.len() as u8; + (*cell).width = width; + (*cell)._pad0 = 0; + (*cell).style = style; + } + } + + fn set_cell_link_ref(&mut self, x: u32, y: u32, link_ref: u32) { + let cell = unsafe { ffi::zr_fb_cell(&mut self.raw as *mut _, x, y) }; + assert!( + !cell.is_null(), + "zr_fb_cell({x},{y}) must return a valid pointer" + ); + unsafe { + (*cell).style.link_ref = link_ref; + } + } + + fn cell_link_ref(&mut self, x: u32, y: u32) -> u32 { + let cell = unsafe { ffi::zr_fb_cell(&mut self.raw as *mut _, x, y) }; + assert!( + !cell.is_null(), + "zr_fb_cell({x},{y}) must return a valid pointer" + ); + unsafe { (*cell).style.link_ref } + } + } + + impl Drop for TestFramebuffer { + fn drop(&mut self) { + unsafe { ffi::zr_fb_release(&mut self.raw as *mut _) }; + } + } + + fn render_diff_bytes( + prev: &ffi::zr_fb_t, + next: &ffi::zr_fb_t, + initial_style: ffi::zr_style_t, + ) -> Vec { + let caps = ffi::plat_caps_t { + color_mode: 3, + supports_mouse: 0, + supports_bracketed_paste: 0, + supports_focus_events: 0, + supports_osc52: 0, + supports_sync_update: 0, + supports_scroll_region: 0, + supports_cursor_shape: 1, + supports_output_wait_writable: 0, + supports_underline_styles: 0, + supports_colored_underlines: 0, + supports_hyperlinks: 0, + sgr_attrs_supported: u32::MAX, + }; + let limits = unsafe { ffi::zr_engine_config_default() }.limits; + let initial_term_state = ffi::zr_term_state_t { + cursor_x: 0, + cursor_y: 0, + cursor_visible: 1, + cursor_shape: 0, + cursor_blink: 0, + flags: 0, + style: initial_style, + }; + let desired_cursor_state = ffi::zr_cursor_state_t { + x: -1, + y: -1, + shape: 0, + visible: 1, + blink: 0, + reserved0: 0, + }; + + let mut scratch_damage_rects = vec![ + ffi::zr_damage_rect_t { + x0: 0, + y0: 0, + x1: 0, + y1: 0, + }; + limits.diff_max_damage_rects as usize + ]; + let mut out = [0u8; 1024]; + let mut out_len = 0usize; + let mut out_final_term_state: ffi::zr_term_state_t = unsafe { std::mem::zeroed() }; + let mut out_stats: ffi::zr_diff_stats_t = unsafe { std::mem::zeroed() }; + + let rc = unsafe { + ffi::zr_diff_render( + prev as *const _, + next as *const _, + &caps as *const _, + &initial_term_state as *const _, + &desired_cursor_state as *const _, + &limits as *const _, + scratch_damage_rects.as_mut_ptr(), + scratch_damage_rects.len() as u32, + 0, + out.as_mut_ptr(), + out.len(), + &mut out_len as *mut _, + &mut out_final_term_state as *mut _, + &mut out_stats as *mut _, + ) + }; + assert_eq!(rc, ffi::ZR_OK, "zr_diff_render must succeed"); + assert!(out_len > 0, "zr_diff_render must emit output"); + out[..out_len].to_vec() + } + + fn render_style_transition(current_attrs: u32, desired_attrs: u32) -> Vec { + let prev = SingleCellFramebuffer::with_attrs(current_attrs); + let next = SingleCellFramebuffer::with_attrs(desired_attrs); + render_diff_bytes(&prev.raw, &next.raw, style_with_attrs(current_attrs)) + } + + fn cell_snapshot(fb: &mut ffi::zr_fb_t, x: u32, y: u32) -> (u8, u8) { + let cell = unsafe { ffi::zr_fb_cell(fb as *mut _, x, y) }; + assert!(!cell.is_null(), "cell must exist at ({x},{y})"); + unsafe { ((*cell).glyph[0], (*cell).width) } + } + + #[test] + fn fb_links_clone_from_failure_has_no_partial_effects() { + let mut dst = TestFramebuffer::new(2, 1); + let uri = b"https://example.test/rezi"; + let mut link_ref = 0u32; + let intern_rc = unsafe { + ffi::zr_fb_link_intern( + &mut dst.raw as *mut _, + uri.as_ptr(), + uri.len(), + std::ptr::null(), + 0, + &mut link_ref as *mut _, + ) + }; + assert_eq!( + intern_rc, + ffi::ZR_OK, + "zr_fb_link_intern must seed destination link state" + ); + assert_eq!(link_ref, 1u32); + + let before_links_ptr = dst.raw.links; + let before_links_len = dst.raw.links_len; + let before_links_cap = dst.raw.links_cap; + let before_link_bytes_ptr = dst.raw.link_bytes; + let before_link_bytes_len = dst.raw.link_bytes_len; + let before_link_bytes_cap = dst.raw.link_bytes_cap; + assert!( + !before_links_ptr.is_null(), + "seeded links pointer must be non-null" + ); + assert!( + !before_link_bytes_ptr.is_null(), + "seeded link-bytes pointer must be non-null" + ); + + let before_first_link = unsafe { *before_links_ptr }; + let before_link_bytes = unsafe { + std::slice::from_raw_parts(before_link_bytes_ptr, before_link_bytes_len as usize) + .to_vec() + }; + + let invalid_src = ffi::zr_fb_t { + cols: dst.raw.cols, + rows: dst.raw.rows, + cells: dst.raw.cells, + links: std::ptr::null_mut(), + links_len: 1, + links_cap: 0, + link_bytes: std::ptr::null_mut(), + link_bytes_len: before_link_bytes_len, + link_bytes_cap: 0, + }; + let clone_rc = unsafe { + ffi::zr_fb_links_clone_from(&mut dst.raw as *mut _, &invalid_src as *const _) + }; + assert_eq!(clone_rc, ffi::ZR_ERR_INVALID_ARGUMENT); + + assert_eq!(dst.raw.links, before_links_ptr); + assert_eq!(dst.raw.links_len, before_links_len); + assert_eq!(dst.raw.links_cap, before_links_cap); + assert_eq!(dst.raw.link_bytes, before_link_bytes_ptr); + assert_eq!(dst.raw.link_bytes_len, before_link_bytes_len); + assert_eq!(dst.raw.link_bytes_cap, before_link_bytes_cap); + + let after_first_link = unsafe { *dst.raw.links }; + assert_eq!(after_first_link.uri_off, before_first_link.uri_off); + assert_eq!(after_first_link.uri_len, before_first_link.uri_len); + assert_eq!(after_first_link.id_off, before_first_link.id_off); + assert_eq!(after_first_link.id_len, before_first_link.id_len); + + let after_link_bytes = unsafe { + std::slice::from_raw_parts(dst.raw.link_bytes, dst.raw.link_bytes_len as usize) + }; + assert_eq!(after_link_bytes, before_link_bytes.as_slice()); + } + + #[test] + fn fb_link_intern_compacts_stale_refs_and_bounds_growth() { + const LINK_ENTRY_MAX_BYTES: u32 = 2083 + 2083; + let mut fb = TestFramebuffer::new(2, 1); + let persistent_uri = b"https://example.test/persistent"; + + let mut persistent_ref = 0u32; + let seed_rc = unsafe { + ffi::zr_fb_link_intern( + &mut fb.raw as *mut _, + persistent_uri.as_ptr(), + persistent_uri.len(), + std::ptr::null(), + 0, + &mut persistent_ref as *mut _, + ) + }; + assert_eq!(seed_rc, ffi::ZR_OK); + assert_ne!(persistent_ref, 0); + fb.set_cell_link_ref(0, 0, persistent_ref); + + let mut peak_links_len = fb.raw.links_len; + let mut peak_link_bytes_len = fb.raw.link_bytes_len; + + for i in 0..64u32 { + let uri = format!("https://example.test/ephemeral/{i}"); + let mut ref_i = 0u32; + let rc = unsafe { + ffi::zr_fb_link_intern( + &mut fb.raw as *mut _, + uri.as_ptr(), + uri.len(), + std::ptr::null(), + 0, + &mut ref_i as *mut _, + ) + }; + assert_eq!(rc, ffi::ZR_OK, "zr_fb_link_intern failed at iteration {i}"); + assert!(ref_i >= 1 && ref_i <= fb.raw.links_len); + + fb.set_cell_link_ref(1, 0, ref_i); + + let live_ref0 = fb.cell_link_ref(0, 0); + let live_ref1 = fb.cell_link_ref(1, 0); + assert!( + live_ref0 >= 1 && live_ref0 <= fb.raw.links_len, + "cell(0,0) link_ref must remain valid" + ); + assert!( + live_ref1 >= 1 && live_ref1 <= fb.raw.links_len, + "cell(1,0) link_ref must remain valid" + ); + + peak_links_len = peak_links_len.max(fb.raw.links_len); + peak_link_bytes_len = peak_link_bytes_len.max(fb.raw.link_bytes_len); + } + + assert!( + peak_links_len <= 5, + "link table must stay bounded for 2-cell framebuffer (peak={peak_links_len})", + ); + assert!( + peak_link_bytes_len <= 5 * LINK_ENTRY_MAX_BYTES, + "link byte arena must stay bounded for 2-cell framebuffer (peak={peak_link_bytes_len})", + ); + + let mut uri_ptr: *const u8 = std::ptr::null(); + let mut uri_len: usize = 0; + let mut id_ptr: *const u8 = std::ptr::null(); + let mut id_len: usize = 0; + let persistent_cell_ref = fb.cell_link_ref(0, 0); + let lookup_rc = unsafe { + ffi::zr_fb_link_lookup( + &fb.raw as *const _, + persistent_cell_ref, + &mut uri_ptr as *mut _, + &mut uri_len as *mut _, + &mut id_ptr as *mut _, + &mut id_len as *mut _, + ) + }; + assert_eq!(lookup_rc, ffi::ZR_OK); + assert_eq!(id_len, 0); + assert!(id_ptr.is_null()); + assert!(!uri_ptr.is_null()); + + let resolved_uri = unsafe { std::slice::from_raw_parts(uri_ptr, uri_len) }; + assert_eq!(resolved_uri, persistent_uri); + } + + #[test] + fn ffi_layout_matches_vendored_headers() { + use std::mem::{align_of, size_of}; + use std::ptr::addr_of; + + assert_eq!(size_of::(), 24); + assert_eq!(align_of::(), 4); + assert_eq!(size_of::(), 60); + assert_eq!(size_of::(), 36); + assert_eq!(size_of::(), 16); + assert_eq!(align_of::(), 4); + + let caps = std::mem::MaybeUninit::::uninit(); + let base = caps.as_ptr(); + unsafe { + assert_eq!(addr_of!((*base).color_mode) as usize - base as usize, 0); + assert_eq!( + addr_of!((*base).supports_output_wait_writable) as usize - base as usize, + 8 + ); + assert_eq!( + addr_of!((*base).supports_underline_styles) as usize - base as usize, + 9 + ); + assert_eq!( + addr_of!((*base).supports_colored_underlines) as usize - base as usize, + 10 + ); + assert_eq!( + addr_of!((*base).supports_hyperlinks) as usize - base as usize, + 11 + ); + assert_eq!( + addr_of!((*base).sgr_attrs_supported) as usize - base as usize, + 12 + ); + } + + if cfg!(target_pointer_width = "64") { + assert_eq!(size_of::(), 48); + assert_eq!(align_of::(), 8); + } else if cfg!(target_pointer_width = "32") { + assert_eq!(size_of::(), 36); + assert_eq!(align_of::(), 4); + } + } + + #[test] + fn clip_edge_write_over_continuation_cleans_lead_pair() { + let mut fb = ffi::zr_fb_t { + cols: 0, + rows: 0, + cells: std::ptr::null_mut(), + links: std::ptr::null_mut(), + links_len: 0, + links_cap: 0, + link_bytes: std::ptr::null_mut(), + link_bytes_len: 0, + link_bytes_cap: 0, + }; + let init_rc = unsafe { ffi::zr_fb_init(&mut fb as *mut _, 4, 1) }; + assert_eq!(init_rc, ffi::ZR_OK); + + let clear_rc = unsafe { ffi::zr_fb_clear(&mut fb as *mut _, &style_plain() as *const _) }; + assert_eq!(clear_rc, ffi::ZR_OK); + + let mut clip_stack = [ + ffi::zr_rect_t { + x: 0, + y: 0, + w: 4, + h: 1, + }, + ffi::zr_rect_t { + x: 0, + y: 0, + w: 0, + h: 0, + }, + ]; + let mut painter = ffi::zr_fb_painter_t { + fb: std::ptr::null_mut(), + clip_stack: std::ptr::null_mut(), + clip_cap: 0, + clip_len: 0, + }; + let begin_rc = unsafe { + ffi::zr_fb_painter_begin( + &mut painter as *mut _, + &mut fb as *mut _, + clip_stack.as_mut_ptr(), + clip_stack.len() as u32, + ) + }; + assert_eq!(begin_rc, ffi::ZR_OK); + + let wide_bytes = b"W"; + let write_wide_rc = unsafe { + ffi::zr_fb_put_grapheme( + &mut painter as *mut _, + 1, + 0, + wide_bytes.as_ptr(), + wide_bytes.len(), + 2, + &style_plain() as *const _, + ) + }; + assert_eq!(write_wide_rc, ffi::ZR_OK); + + let push_rc = unsafe { + ffi::zr_fb_clip_push( + &mut painter as *mut _, + ffi::zr_rect_t { + x: 2, + y: 0, + w: 1, + h: 1, + }, + ) + }; + assert_eq!(push_rc, ffi::ZR_OK); + + let a_bytes = b"A"; + let write_a_rc = unsafe { + ffi::zr_fb_put_grapheme( + &mut painter as *mut _, + 2, + 0, + a_bytes.as_ptr(), + a_bytes.len(), + 1, + &style_plain() as *const _, + ) + }; + assert_eq!(write_a_rc, ffi::ZR_OK); + + let pop_rc = unsafe { ffi::zr_fb_clip_pop(&mut painter as *mut _) }; + assert_eq!(pop_rc, ffi::ZR_OK); + + let (x1_ch, x1_w) = cell_snapshot(&mut fb, 1, 0); + let (x2_ch, x2_w) = cell_snapshot(&mut fb, 2, 0); + assert_eq!(x1_ch, b' '); + assert_eq!( + x1_w, 1, + "wide lead should be cleared when continuation is overwritten" + ); + assert_eq!(x2_ch, b'A'); + assert_eq!(x2_w, 1); + + unsafe { ffi::zr_fb_release(&mut fb as *mut _) }; + } + + #[test] + fn clip_edge_write_over_wide_lead_cleans_hidden_continuation() { + let mut fb = ffi::zr_fb_t { + cols: 0, + rows: 0, + cells: std::ptr::null_mut(), + links: std::ptr::null_mut(), + links_len: 0, + links_cap: 0, + link_bytes: std::ptr::null_mut(), + link_bytes_len: 0, + link_bytes_cap: 0, + }; + let init_rc = unsafe { ffi::zr_fb_init(&mut fb as *mut _, 4, 1) }; + assert_eq!(init_rc, ffi::ZR_OK); + + let clear_rc = unsafe { ffi::zr_fb_clear(&mut fb as *mut _, &style_plain() as *const _) }; + assert_eq!(clear_rc, ffi::ZR_OK); + + let mut clip_stack = [ + ffi::zr_rect_t { + x: 0, + y: 0, + w: 4, + h: 1, + }, + ffi::zr_rect_t { + x: 0, + y: 0, + w: 0, + h: 0, + }, + ]; + let mut painter = ffi::zr_fb_painter_t { + fb: std::ptr::null_mut(), + clip_stack: std::ptr::null_mut(), + clip_cap: 0, + clip_len: 0, + }; + let begin_rc = unsafe { + ffi::zr_fb_painter_begin( + &mut painter as *mut _, + &mut fb as *mut _, + clip_stack.as_mut_ptr(), + clip_stack.len() as u32, + ) + }; + assert_eq!(begin_rc, ffi::ZR_OK); + + let wide_bytes = b"W"; + let write_wide_rc = unsafe { + ffi::zr_fb_put_grapheme( + &mut painter as *mut _, + 1, + 0, + wide_bytes.as_ptr(), + wide_bytes.len(), + 2, + &style_plain() as *const _, + ) + }; + assert_eq!(write_wide_rc, ffi::ZR_OK); + + let push_rc = unsafe { + ffi::zr_fb_clip_push( + &mut painter as *mut _, + ffi::zr_rect_t { + x: 1, + y: 0, + w: 1, + h: 1, + }, + ) + }; + assert_eq!(push_rc, ffi::ZR_OK); + + let b_bytes = b"B"; + let write_b_rc = unsafe { + ffi::zr_fb_put_grapheme( + &mut painter as *mut _, + 1, + 0, + b_bytes.as_ptr(), + b_bytes.len(), + 1, + &style_plain() as *const _, + ) + }; + assert_eq!(write_b_rc, ffi::ZR_OK); + + let pop_rc = unsafe { ffi::zr_fb_clip_pop(&mut painter as *mut _) }; + assert_eq!(pop_rc, ffi::ZR_OK); + + let (x1_ch, x1_w) = cell_snapshot(&mut fb, 1, 0); + let (x2_ch, x2_w) = cell_snapshot(&mut fb, 2, 0); + assert_eq!(x1_ch, b'B'); + assert_eq!(x1_w, 1); + assert_eq!(x2_ch, b' '); + assert_eq!(x2_w, 1, "continuation outside clip should be cleaned"); + + unsafe { ffi::zr_fb_release(&mut fb as *mut _) }; + } + + #[test] + fn diff_reanchors_cursor_after_non_ascii_cell() { + let prev = TestFramebuffer::new(2, 1); + let mut next = TestFramebuffer::new(2, 1); + next.set_cell(0, 0, "✓".as_bytes(), 1, style_plain()); + next.set_cell(1, 0, b"A", 1, style_plain()); + + let out = render_diff_bytes(&prev.raw, &next.raw, style_plain()); + assert!( + contains_subsequence(&out, b"\x1b[1;2H"), + "expected explicit CUP for second cell after non-ascii glyph: {:?}", + String::from_utf8_lossy(&out), + ); + } + + #[test] + fn debug_query_bigint_u64_accepts_in_range_values() { + assert_eq!(parse_debug_query_bigint_u64(false, &[]), Ok(0)); + assert_eq!(parse_debug_query_bigint_u64(false, &[0]), Ok(0)); + assert_eq!(parse_debug_query_bigint_u64(false, &[123]), Ok(123)); + assert_eq!( + parse_debug_query_bigint_u64(false, &[u64::MAX]), + Ok(u64::MAX) + ); + } + + #[test] + fn debug_query_bigint_u64_rejects_negative_values() { + assert!(parse_debug_query_bigint_u64(true, &[1]).is_err()); + assert!(parse_debug_query_bigint_u64(true, &[u64::MAX]).is_err()); + } + + #[test] + fn debug_query_bigint_u64_rejects_overflow_values() { + assert!(parse_debug_query_bigint_u64(false, &[0, 1]).is_err()); + assert!(parse_debug_query_bigint_u64(false, &[u64::MAX, 1]).is_err()); + } + + #[test] + fn diff_emits_dim_and_normal_intensity_sequences() { + let to_dim = render_style_transition(0, ATTR_DIM); + assert!( + contains_subsequence(&to_dim, b"\x1b[0;2;"), + "expected dim SGR sequence in output: {:?}", + String::from_utf8_lossy(&to_dim), + ); + + let to_normal = render_style_transition(ATTR_DIM, 0); + assert!( + contains_subsequence(&to_normal, b"\x1b[0;38;"), + "expected normal-intensity SGR sequence in output: {:?}", + String::from_utf8_lossy(&to_normal), + ); + } + + #[test] + fn diff_reapplies_intensity_when_switching_bold_and_dim() { + let dim_to_bold = render_style_transition(ATTR_DIM, ATTR_BOLD); + assert!( + contains_subsequence(&dim_to_bold, b"\x1b[0;1;"), + "expected dim->bold transition to emit bold SGR: {:?}", + String::from_utf8_lossy(&dim_to_bold), + ); + + let bold_to_dim = render_style_transition(ATTR_BOLD, ATTR_DIM); + assert!( + contains_subsequence(&bold_to_dim, b"\x1b[0;2;"), + "expected bold->dim transition to emit dim SGR: {:?}", + String::from_utf8_lossy(&bold_to_dim), + ); + } + + #[test] + fn diff_preserves_non_intensity_attr_delta_path() { + let dim_to_dim_underline = render_style_transition(ATTR_DIM, ATTR_DIM | ATTR_UNDERLINE); + assert!( + contains_subsequence(&dim_to_dim_underline, b"\x1b[0;2;4;"), + "expected underline+dim sequence in output: {:?}", + String::from_utf8_lossy(&dim_to_dim_underline), + ); + } } diff --git a/packages/node/src/__tests__/config_guards.test.ts b/packages/node/src/__tests__/config_guards.test.ts index ec625097..420c4284 100644 --- a/packages/node/src/__tests__/config_guards.test.ts +++ b/packages/node/src/__tests__/config_guards.test.ts @@ -170,7 +170,7 @@ test("config guard: matching fpsCap/native target fps is accepted", () => { backend.dispose(); }); -test("config guard: native worker mode falls back to inline by default in TTY", () => { +test("config guard: native worker mode stays on worker in TTY", () => { const selection = selectNodeBackendExecutionMode({ requestedExecutionMode: "worker", fpsCap: 60, @@ -178,8 +178,8 @@ test("config guard: native worker mode falls back to inline by default in TTY", }); assert.deepEqual(selection, { resolvedExecutionMode: "worker", - selectedExecutionMode: "inline", - fallbackReason: "native-worker-unstable", + selectedExecutionMode: "worker", + fallbackReason: null, }); }); diff --git a/packages/node/src/__tests__/worker_integration.test.ts b/packages/node/src/__tests__/worker_integration.test.ts index 45fbbe89..8f3ecc23 100644 --- a/packages/node/src/__tests__/worker_integration.test.ts +++ b/packages/node/src/__tests__/worker_integration.test.ts @@ -118,7 +118,7 @@ async function shutdownAndWaitForExit(worker: Worker): Promise { await exitPromise; } -test("native loader: worker-thread load fails deterministically without native crash", async () => { +test("native loader: worker-thread load succeeds and exits cleanly", async () => { const loaderPath = fileURLToPath(new URL("../../../native/loader.cjs", import.meta.url)); const worker = new Worker( ` @@ -141,9 +141,9 @@ test("native loader: worker-thread load fails deterministically without native c Readonly<{ type?: unknown; ok?: unknown; message?: unknown }>, ]; assert.equal(msg.type, "loaderResult"); - assert.equal(msg.ok, false); + assert.equal(msg.ok, true); assert.equal(typeof msg.message, "string"); - assert.match(String(msg.message), /does not support worker_threads/); + assert.equal(String(msg.message), ""); const [code] = (await once(worker, "exit")) as [number]; assert.equal(code, 0); diff --git a/packages/node/src/backend/nodeBackend.ts b/packages/node/src/backend/nodeBackend.ts index bcfe4801..24e590f0 100644 --- a/packages/node/src/backend/nodeBackend.ts +++ b/packages/node/src/backend/nodeBackend.ts @@ -154,7 +154,7 @@ export type NodeBackendExecutionModeSelectionInput = Readonly<{ export type NodeBackendExecutionModeSelection = Readonly<{ resolvedExecutionMode: "worker" | "inline"; selectedExecutionMode: "worker" | "inline"; - fallbackReason: "native-worker-unstable" | null; + fallbackReason: string | null; }>; type Deferred = Readonly<{ @@ -353,7 +353,7 @@ function hasInteractiveTty(): boolean { export function selectNodeBackendExecutionMode( input: NodeBackendExecutionModeSelectionInput, ): NodeBackendExecutionModeSelection { - const { requestedExecutionMode, fpsCap, nativeShimModule, hasAnyTty } = input; + const { requestedExecutionMode, fpsCap } = input; const resolvedExecutionMode: "worker" | "inline" = requestedExecutionMode === "inline" ? "inline" @@ -362,12 +362,10 @@ export function selectNodeBackendExecutionMode( : fpsCap <= 30 ? "inline" : "worker"; - const shouldFallbackForNativeWorker = - resolvedExecutionMode === "worker" && nativeShimModule === undefined && hasAnyTty; return { resolvedExecutionMode, - selectedExecutionMode: shouldFallbackForNativeWorker ? "inline" : resolvedExecutionMode, - fallbackReason: shouldFallbackForNativeWorker ? "native-worker-unstable" : null, + selectedExecutionMode: resolvedExecutionMode, + fallbackReason: null, }; } From b71c1ec975eda8b4f83e5a3a6da38a75e9682d24 Mon Sep 17 00:00:00 2001 From: RtlZeroMemory <58250858+RtlZeroMemory@users.noreply.github.com> Date: Thu, 5 Mar 2026 23:27:45 +0400 Subject: [PATCH 2/5] refactor(native): split bridge modules --- packages/native/index.d.ts | 66 +- packages/native/src/config.rs | 350 +++++ packages/native/src/debug.rs | 366 +++++ packages/native/src/ffi.rs | 626 ++++++++ packages/native/src/lib.rs | 2453 ++++--------------------------- packages/native/src/registry.rs | 157 ++ packages/native/src/tests.rs | 759 ++++++++++ 7 files changed, 2566 insertions(+), 2211 deletions(-) create mode 100644 packages/native/src/config.rs create mode 100644 packages/native/src/debug.rs create mode 100644 packages/native/src/ffi.rs create mode 100644 packages/native/src/registry.rs create mode 100644 packages/native/src/tests.rs diff --git a/packages/native/index.d.ts b/packages/native/index.d.ts index 90d227b2..68e64e25 100644 --- a/packages/native/index.d.ts +++ b/packages/native/index.d.ts @@ -3,6 +3,39 @@ /* auto-generated by NAPI-RS */ +export interface DebugStats { + totalRecords: bigint; + totalDropped: bigint; + errorCount: number; + warnCount: number; + currentRingUsage: number; + ringCapacity: number; +} +export interface DebugQueryResult { + recordsReturned: number; + recordsAvailable: number; + oldestRecordId: bigint; + newestRecordId: bigint; + recordsDropped: number; +} +export declare function engineDebugEnable( + engineId: number, + config?: object | undefined | null, +): number; +export declare function engineDebugDisable(engineId: number): number; +export declare function engineDebugQuery( + engineId: number, + query: object | undefined | null, + outHeaders: Uint8Array, +): DebugQueryResult; +export declare function engineDebugGetPayload( + engineId: number, + recordId: bigint, + outPayload: Uint8Array, +): number; +export declare function engineDebugGetStats(engineId: number): DebugStats; +export declare function engineDebugExport(engineId: number, outBuf: Uint8Array): number; +export declare function engineDebugReset(engineId: number): number; export interface EngineMetrics { structSize: number; negotiatedEngineAbiMajor: number; @@ -62,36 +95,3 @@ export declare function enginePostUserEvent( export declare function engineSetConfig(engineId: number, cfg?: object | undefined | null): number; export declare function engineGetMetrics(engineId: number): EngineMetrics; export declare function engineGetCaps(engineId: number): TerminalCaps; -export interface DebugStats { - totalRecords: bigint; - totalDropped: bigint; - errorCount: number; - warnCount: number; - currentRingUsage: number; - ringCapacity: number; -} -export interface DebugQueryResult { - recordsReturned: number; - recordsAvailable: number; - oldestRecordId: bigint; - newestRecordId: bigint; - recordsDropped: number; -} -export declare function engineDebugEnable( - engineId: number, - config?: object | undefined | null, -): number; -export declare function engineDebugDisable(engineId: number): number; -export declare function engineDebugQuery( - engineId: number, - query: object | undefined | null, - outHeaders: Uint8Array, -): DebugQueryResult; -export declare function engineDebugGetPayload( - engineId: number, - recordId: bigint, - outPayload: Uint8Array, -): number; -export declare function engineDebugGetStats(engineId: number): DebugStats; -export declare function engineDebugExport(engineId: number, outBuf: Uint8Array): number; -export declare function engineDebugReset(engineId: number): number; diff --git a/packages/native/src/config.rs b/packages/native/src/config.rs new file mode 100644 index 00000000..e36eda00 --- /dev/null +++ b/packages/native/src/config.rs @@ -0,0 +1,350 @@ +use crate::ffi; +use napi::bindgen_prelude::{Error, Status, ValueType}; +use napi::{JsObject, JsUnknown}; + +pub(crate) type ParseResult = crate::ParseResult; + +const LIMITS_KEYS: &[(&str, &str)] = &[ + ("arenaMaxTotalBytes", "arena_max_total_bytes"), + ("arenaInitialBytes", "arena_initial_bytes"), + ("outMaxBytesPerFrame", "out_max_bytes_per_frame"), + ("dlMaxTotalBytes", "dl_max_total_bytes"), + ("dlMaxCmds", "dl_max_cmds"), + ("dlMaxStrings", "dl_max_strings"), + ("dlMaxBlobs", "dl_max_blobs"), + ("dlMaxClipDepth", "dl_max_clip_depth"), + ("dlMaxTextRunSegments", "dl_max_text_run_segments"), + ("diffMaxDamageRects", "diff_max_damage_rects"), +]; + +const PLAT_KEYS: &[(&str, &str)] = &[ + ("requestedColorMode", "requested_color_mode"), + ("enableMouse", "enable_mouse"), + ("enableBracketedPaste", "enable_bracketed_paste"), + ("enableFocusEvents", "enable_focus_events"), + ("enableOsc52", "enable_osc52"), +]; + +const CREATE_CFG_KEYS: &[(&str, &str)] = &[ + ("requestedEngineAbiMajor", "requested_engine_abi_major"), + ("requestedEngineAbiMinor", "requested_engine_abi_minor"), + ("requestedEngineAbiPatch", "requested_engine_abi_patch"), + ("requestedDrawlistVersion", "requested_drawlist_version"), + ("requestedEventBatchVersion", "requested_event_batch_version"), + ("limits", "limits"), + ("plat", "plat"), + ("tabWidth", "tab_width"), + ("widthPolicy", "width_policy"), + ("targetFps", "target_fps"), + ("enableScrollOptimizations", "enable_scroll_optimizations"), + ("enableDebugOverlay", "enable_debug_overlay"), + ("enableReplayRecording", "enable_replay_recording"), + ("waitForOutputDrain", "wait_for_output_drain"), + ("capForceFlags", "cap_force_flags"), + ("capSuppressFlags", "cap_suppress_flags"), +]; + +const RUNTIME_CFG_KEYS: &[(&str, &str)] = &[ + ("limits", "limits"), + ("plat", "plat"), + ("tabWidth", "tab_width"), + ("widthPolicy", "width_policy"), + ("targetFps", "target_fps"), + ("enableScrollOptimizations", "enable_scroll_optimizations"), + ("enableDebugOverlay", "enable_debug_overlay"), + ("enableReplayRecording", "enable_replay_recording"), + ("waitForOutputDrain", "wait_for_output_drain"), + ("capForceFlags", "cap_force_flags"), + ("capSuppressFlags", "cap_suppress_flags"), +]; + +pub(crate) fn validate_known_keys( + obj: &JsObject, + allowed: &[(&str, &str)], + ctx: &str, +) -> napi::Result<()> { + let names = obj.get_property_names()?; + let len = names.get_array_length()?; + + 'outer: for i in 0..len { + let unk = names.get_element::(i)?; + let s = unk.coerce_to_string()?; + let k = s.into_utf8()?.as_str()?.to_owned(); + for (primary, alias) in allowed { + if k == *primary || k == *alias { + continue 'outer; + } + } + return Err(Error::new(Status::InvalidArg, format!("{ctx}: unknown key: {k}"))); + } + Ok(()) +} + +pub(crate) fn apply_create_cfg_strict( + dst: &mut ffi::zr_engine_config_t, + obj: &JsObject, +) -> napi::Result<()> { + validate_known_keys(obj, CREATE_CFG_KEYS, "engineCreate config")?; + if let Some(lim) = js_obj(obj, "limits", "limits") + .map_err(|_| Error::new(Status::InvalidArg, "engineCreate: limits must be an object"))? + { + validate_known_keys(&lim, LIMITS_KEYS, "engineCreate config.limits")?; + } + if let Some(plat) = js_obj(obj, "plat", "plat") + .map_err(|_| Error::new(Status::InvalidArg, "engineCreate: plat must be an object"))? + { + validate_known_keys(&plat, PLAT_KEYS, "engineCreate config.plat")?; + } + + apply_create_cfg(dst, obj) + .map_err(|_| Error::new(Status::InvalidArg, "engineCreate: invalid config value"))?; + Ok(()) +} + +pub(crate) fn apply_runtime_cfg_strict( + dst: &mut ffi::zr_engine_runtime_config_t, + obj: &JsObject, +) -> napi::Result<()> { + validate_known_keys(obj, RUNTIME_CFG_KEYS, "engineSetConfig config")?; + if let Some(lim) = js_obj(obj, "limits", "limits") + .map_err(|_| Error::new(Status::InvalidArg, "engineSetConfig: limits must be an object"))? + { + validate_known_keys(&lim, LIMITS_KEYS, "engineSetConfig config.limits")?; + } + if let Some(plat) = js_obj(obj, "plat", "plat") + .map_err(|_| Error::new(Status::InvalidArg, "engineSetConfig: plat must be an object"))? + { + validate_known_keys(&plat, PLAT_KEYS, "engineSetConfig config.plat")?; + } + + apply_runtime_cfg(dst, obj) + .map_err(|_| Error::new(Status::InvalidArg, "engineSetConfig: invalid config value"))?; + Ok(()) +} + +pub(crate) fn js_u32(obj: &JsObject, primary: &str, alias: &str) -> ParseResult> { + for name in [primary, alias] { + let v = match obj.get_named_property::(name) { + Ok(v) => v, + Err(_) => continue, + }; + if v.get_type().map_err(|_| ())? == ValueType::Undefined { + continue; + } + let n = v.coerce_to_number().map_err(|_| ())?; + let f = n.get_double().map_err(|_| ())?; + if !f.is_finite() || f < 0.0 || f > (u32::MAX as f64) || f.fract() != 0.0 { + return Err(()); + } + return Ok(Some(f as u32)); + } + Ok(None) +} + +pub(crate) fn js_u8_bool( + obj: &JsObject, + primary: &str, + alias: &str, +) -> ParseResult> { + for name in [primary, alias] { + let v = match obj.get_named_property::(name) { + Ok(v) => v, + Err(_) => continue, + }; + match v.get_type().map_err(|_| ())? { + ValueType::Undefined => continue, + ValueType::Boolean => { + let b = v.coerce_to_bool().map_err(|_| ())?; + return Ok(Some(if b.get_value().map_err(|_| ())? { 1 } else { 0 })); + } + ValueType::Number => { + let n = v.coerce_to_number().map_err(|_| ())?; + let f = n.get_double().map_err(|_| ())?; + if f == 0.0 { + return Ok(Some(0)); + } + if f == 1.0 { + return Ok(Some(1)); + } + return Err(()); + } + _ => return Err(()), + } + } + Ok(None) +} + +fn js_obj(obj: &JsObject, primary: &str, alias: &str) -> ParseResult> { + for name in [primary, alias] { + let v = match obj.get_named_property::(name) { + Ok(v) => v, + Err(_) => continue, + }; + if v.get_type().map_err(|_| ())? == ValueType::Undefined { + continue; + } + let o = v.coerce_to_object().map_err(|_| ())?; + return Ok(Some(o)); + } + Ok(None) +} + +fn apply_limits(dst: &mut ffi::zr_limits_t, obj: &JsObject) -> ParseResult<()> { + if let Some(v) = js_u32(obj, "arenaMaxTotalBytes", "arena_max_total_bytes")? { + dst.arena_max_total_bytes = v; + } + if let Some(v) = js_u32(obj, "arenaInitialBytes", "arena_initial_bytes")? { + dst.arena_initial_bytes = v; + } + if let Some(v) = js_u32(obj, "outMaxBytesPerFrame", "out_max_bytes_per_frame")? { + dst.out_max_bytes_per_frame = v; + } + if let Some(v) = js_u32(obj, "dlMaxTotalBytes", "dl_max_total_bytes")? { + dst.dl_max_total_bytes = v; + } + if let Some(v) = js_u32(obj, "dlMaxCmds", "dl_max_cmds")? { + dst.dl_max_cmds = v; + } + if let Some(v) = js_u32(obj, "dlMaxStrings", "dl_max_strings")? { + dst.dl_max_strings = v; + } + if let Some(v) = js_u32(obj, "dlMaxBlobs", "dl_max_blobs")? { + dst.dl_max_blobs = v; + } + if let Some(v) = js_u32(obj, "dlMaxClipDepth", "dl_max_clip_depth")? { + dst.dl_max_clip_depth = v; + } + if let Some(v) = js_u32(obj, "dlMaxTextRunSegments", "dl_max_text_run_segments")? { + dst.dl_max_text_run_segments = v; + } + if let Some(v) = js_u32(obj, "diffMaxDamageRects", "diff_max_damage_rects")? { + dst.diff_max_damage_rects = v; + } + Ok(()) +} + +fn apply_plat(dst: &mut ffi::plat_config_t, obj: &JsObject) -> ParseResult<()> { + if let Some(v) = js_u32(obj, "requestedColorMode", "requested_color_mode")? { + dst.requested_color_mode = (v & 0xFF) as u8; + } + if let Some(v) = js_u8_bool(obj, "enableMouse", "enable_mouse")? { + dst.enable_mouse = v; + } + if let Some(v) = js_u8_bool(obj, "enableBracketedPaste", "enable_bracketed_paste")? { + dst.enable_bracketed_paste = v; + } + if let Some(v) = js_u8_bool(obj, "enableFocusEvents", "enable_focus_events")? { + dst.enable_focus_events = v; + } + if let Some(v) = js_u8_bool(obj, "enableOsc52", "enable_osc52")? { + dst.enable_osc52 = v; + } + dst._pad = [0, 0, 0]; + Ok(()) +} + +fn apply_create_cfg(dst: &mut ffi::zr_engine_config_t, obj: &JsObject) -> ParseResult<()> { + if let Some(v) = js_u32(obj, "requestedEngineAbiMajor", "requested_engine_abi_major")? { + dst.requested_engine_abi_major = v; + } + if let Some(v) = js_u32(obj, "requestedEngineAbiMinor", "requested_engine_abi_minor")? { + dst.requested_engine_abi_minor = v; + } + if let Some(v) = js_u32(obj, "requestedEngineAbiPatch", "requested_engine_abi_patch")? { + dst.requested_engine_abi_patch = v; + } + if let Some(v) = js_u32(obj, "requestedDrawlistVersion", "requested_drawlist_version")? { + dst.requested_drawlist_version = v; + } + if let Some(v) = js_u32(obj, "requestedEventBatchVersion", "requested_event_batch_version")? { + dst.requested_event_batch_version = v; + } + if let Some(lim) = js_obj(obj, "limits", "limits")? { + apply_limits(&mut dst.limits, &lim)?; + } + if let Some(plat) = js_obj(obj, "plat", "plat")? { + apply_plat(&mut dst.plat, &plat)?; + } + if let Some(v) = js_u32(obj, "tabWidth", "tab_width")? { + dst.tab_width = v; + } + if let Some(v) = js_u32(obj, "widthPolicy", "width_policy")? { + dst.width_policy = v; + } + if let Some(v) = js_u32(obj, "targetFps", "target_fps")? { + dst.target_fps = v; + } + if let Some(v) = js_u8_bool(obj, "enableScrollOptimizations", "enable_scroll_optimizations")? { + dst.enable_scroll_optimizations = v; + } + if let Some(v) = js_u8_bool(obj, "enableDebugOverlay", "enable_debug_overlay")? { + dst.enable_debug_overlay = v; + } + if let Some(v) = js_u8_bool(obj, "enableReplayRecording", "enable_replay_recording")? { + dst.enable_replay_recording = v; + } + if let Some(v) = js_u8_bool(obj, "waitForOutputDrain", "wait_for_output_drain")? { + dst.wait_for_output_drain = v; + } + if let Some(v) = js_u32(obj, "capForceFlags", "cap_force_flags")? { + dst.cap_force_flags = v; + } + if let Some(v) = js_u32(obj, "capSuppressFlags", "cap_suppress_flags")? { + dst.cap_suppress_flags = v; + } + Ok(()) +} + +pub(crate) fn create_default_runtime_cfg() -> ffi::zr_engine_runtime_config_t { + let base = unsafe { ffi::zr_engine_config_default() }; + ffi::zr_engine_runtime_config_t { + limits: base.limits, + plat: base.plat, + tab_width: base.tab_width, + width_policy: base.width_policy, + target_fps: base.target_fps, + enable_scroll_optimizations: base.enable_scroll_optimizations, + enable_debug_overlay: base.enable_debug_overlay, + enable_replay_recording: base.enable_replay_recording, + wait_for_output_drain: base.wait_for_output_drain, + cap_force_flags: base.cap_force_flags, + cap_suppress_flags: base.cap_suppress_flags, + } +} + +fn apply_runtime_cfg(dst: &mut ffi::zr_engine_runtime_config_t, obj: &JsObject) -> ParseResult<()> { + if let Some(lim) = js_obj(obj, "limits", "limits")? { + apply_limits(&mut dst.limits, &lim)?; + } + if let Some(plat) = js_obj(obj, "plat", "plat")? { + apply_plat(&mut dst.plat, &plat)?; + } + if let Some(v) = js_u32(obj, "tabWidth", "tab_width")? { + dst.tab_width = v; + } + if let Some(v) = js_u32(obj, "widthPolicy", "width_policy")? { + dst.width_policy = v; + } + if let Some(v) = js_u32(obj, "targetFps", "target_fps")? { + dst.target_fps = v; + } + if let Some(v) = js_u8_bool(obj, "enableScrollOptimizations", "enable_scroll_optimizations")? { + dst.enable_scroll_optimizations = v; + } + if let Some(v) = js_u8_bool(obj, "enableDebugOverlay", "enable_debug_overlay")? { + dst.enable_debug_overlay = v; + } + if let Some(v) = js_u8_bool(obj, "enableReplayRecording", "enable_replay_recording")? { + dst.enable_replay_recording = v; + } + if let Some(v) = js_u8_bool(obj, "waitForOutputDrain", "wait_for_output_drain")? { + dst.wait_for_output_drain = v; + } + if let Some(v) = js_u32(obj, "capForceFlags", "cap_force_flags")? { + dst.cap_force_flags = v; + } + if let Some(v) = js_u32(obj, "capSuppressFlags", "cap_suppress_flags")? { + dst.cap_suppress_flags = v; + } + Ok(()) +} diff --git a/packages/native/src/debug.rs b/packages/native/src/debug.rs new file mode 100644 index 00000000..fb97a0fa --- /dev/null +++ b/packages/native/src/debug.rs @@ -0,0 +1,366 @@ +use crate::config::{js_u32, js_u8_bool, validate_known_keys, ParseResult}; +use crate::ffi; +use crate::registry::get_engine_guard; +use crate::{bigint_from_u64, invalid_arg_error}; +use napi::bindgen_prelude::{BigInt, Error, Status, Uint8Array, ValueType}; +use napi::{Env, JsBigInt, JsObject, JsUnknown}; +use napi_derive::napi; + +#[napi(object)] +#[allow(non_snake_case)] +pub struct DebugStats { + pub totalRecords: BigInt, + pub totalDropped: BigInt, + pub errorCount: u32, + pub warnCount: u32, + pub currentRingUsage: u32, + pub ringCapacity: u32, +} + +#[napi(object)] +#[allow(non_snake_case)] +pub struct DebugQueryResult { + pub recordsReturned: u32, + pub recordsAvailable: u32, + pub oldestRecordId: BigInt, + pub newestRecordId: BigInt, + pub recordsDropped: u32, +} + +const DEBUG_CFG_KEYS: &[(&str, &str)] = &[ + ("enabled", "enabled"), + ("ringCapacity", "ring_capacity"), + ("minSeverity", "min_severity"), + ("categoryMask", "category_mask"), + ("captureRawEvents", "capture_raw_events"), + ("captureDrawlistBytes", "capture_drawlist_bytes"), +]; + +const DEBUG_QUERY_KEYS: &[(&str, &str)] = &[ + ("minRecordId", "min_record_id"), + ("maxRecordId", "max_record_id"), + ("minFrameId", "min_frame_id"), + ("maxFrameId", "max_frame_id"), + ("categoryMask", "category_mask"), + ("minSeverity", "min_severity"), + ("maxRecords", "max_records"), +]; + +pub(crate) fn parse_debug_query_bigint_u64(sign_bit: bool, words: &[u64]) -> ParseResult { + if sign_bit && words.iter().any(|word| *word != 0) { + return Err(()); + } + + match words { + [] => Ok(0), + [value] => Ok(*value), + _ => Err(()), + } +} + +fn js_u64(obj: &JsObject, primary: &str, alias: &str) -> ParseResult> { + for name in [primary, alias] { + let value = match obj.get_named_property::(name) { + Ok(value) => value, + Err(_) => continue, + }; + match value.get_type().map_err(|_| ())? { + ValueType::Undefined => continue, + ValueType::BigInt => { + let mut bigint = unsafe { value.cast::() }; + let (sign_bit, words) = bigint.get_words().map_err(|_| ())?; + return Ok(Some(parse_debug_query_bigint_u64(sign_bit, &words)?)); + } + ValueType::Number => { + let number = value.coerce_to_number().map_err(|_| ())?; + let float = number.get_double().map_err(|_| ())?; + if !float.is_finite() || float < 0.0 || float > (u64::MAX as f64) { + return Err(()); + } + return Ok(Some(float as u64)); + } + _ => return Err(()), + } + } + + Ok(None) +} + +fn apply_debug_cfg(dst: &mut ffi::zr_debug_config_t, obj: &JsObject) -> ParseResult<()> { + if let Some(value) = js_u8_bool(obj, "enabled", "enabled")? { + dst.enabled = value as u32; + } + if let Some(value) = js_u32(obj, "ringCapacity", "ring_capacity")? { + dst.ring_capacity = value; + } + if let Some(value) = js_u32(obj, "minSeverity", "min_severity")? { + dst.min_severity = value; + } + if let Some(value) = js_u32(obj, "categoryMask", "category_mask")? { + dst.category_mask = value; + } + if let Some(value) = js_u8_bool(obj, "captureRawEvents", "capture_raw_events")? { + dst.capture_raw_events = value as u32; + } + if let Some(value) = js_u8_bool(obj, "captureDrawlistBytes", "capture_drawlist_bytes")? { + dst.capture_drawlist_bytes = value as u32; + } + Ok(()) +} + +fn apply_debug_query(dst: &mut ffi::zr_debug_query_t, obj: &JsObject) -> ParseResult<()> { + if let Some(value) = js_u64(obj, "minRecordId", "min_record_id")? { + dst.min_record_id = value; + } + if let Some(value) = js_u64(obj, "maxRecordId", "max_record_id")? { + dst.max_record_id = value; + } + if let Some(value) = js_u64(obj, "minFrameId", "min_frame_id")? { + dst.min_frame_id = value; + } + if let Some(value) = js_u64(obj, "maxFrameId", "max_frame_id")? { + dst.max_frame_id = value; + } + if let Some(value) = js_u32(obj, "categoryMask", "category_mask")? { + dst.category_mask = value; + } + if let Some(value) = js_u32(obj, "minSeverity", "min_severity")? { + dst.min_severity = value; + } + if let Some(value) = js_u32(obj, "maxRecords", "max_records")? { + dst.max_records = value; + } + Ok(()) +} + +#[napi(js_name = "engineDebugEnable")] +pub fn engine_debug_enable( + _env: Env, + engine_id: u32, + config: Option, +) -> napi::Result { + let guard = match get_engine_guard(engine_id) { + Ok(guard) => guard, + Err(rc) => return Ok(rc), + }; + if !guard.slot.is_owner_thread() { + return Ok(ffi::ZR_ERR_INVALID_ARGUMENT); + } + + let mut cfg = ffi::zr_debug_config_t { + enabled: 1, + ring_capacity: 0, + min_severity: 0, + category_mask: 0xFFFF_FFFF, + capture_raw_events: 0, + capture_drawlist_bytes: 0, + _pad0: 0, + _pad1: 0, + }; + + if let Some(obj) = config { + validate_known_keys(&obj, DEBUG_CFG_KEYS, "engineDebugEnable config")?; + apply_debug_cfg(&mut cfg, &obj).map_err(|_| { + Error::new( + Status::InvalidArg, + "engineDebugEnable: invalid config value", + ) + })?; + } + + Ok(unsafe { ffi::engine_debug_enable(guard.slot.engine, &cfg as *const _) }) +} + +#[napi(js_name = "engineDebugDisable")] +pub fn engine_debug_disable(engine_id: u32) -> i32 { + let guard = match get_engine_guard(engine_id) { + Ok(guard) => guard, + Err(rc) => return rc, + }; + if !guard.slot.is_owner_thread() { + return ffi::ZR_ERR_INVALID_ARGUMENT; + } + + unsafe { ffi::engine_debug_disable(guard.slot.engine) }; + ffi::ZR_OK +} + +#[napi(js_name = "engineDebugQuery")] +pub fn engine_debug_query( + _env: Env, + engine_id: u32, + query: Option, + mut out_headers: Uint8Array, +) -> napi::Result { + let guard = get_engine_guard(engine_id).map_err(|_| invalid_arg_error())?; + if !guard.slot.is_owner_thread() { + return Err(invalid_arg_error()); + } + + let mut debug_query = ffi::zr_debug_query_t { + min_record_id: 0, + max_record_id: 0, + min_frame_id: 0, + max_frame_id: 0, + category_mask: 0xFFFF_FFFF, + min_severity: 0, + max_records: 0, + _pad0: 0, + }; + + if let Some(obj) = query { + validate_known_keys(&obj, DEBUG_QUERY_KEYS, "engineDebugQuery query")?; + apply_debug_query(&mut debug_query, &obj) + .map_err(|_| Error::new(Status::InvalidArg, "engineDebugQuery: invalid query value"))?; + } + + let mut result = ffi::zr_debug_query_result_t { + records_returned: 0, + records_available: 0, + oldest_record_id: 0, + newest_record_id: 0, + records_dropped: 0, + _pad0: 0, + }; + + let out_headers_slice = out_headers.as_mut(); + let header_size = std::mem::size_of::(); + let header_align = std::mem::align_of::(); + let headers_cap = (out_headers_slice.len() / header_size) as u32; + let headers_ptr = if headers_cap == 0 { + std::ptr::null_mut() + } else { + let raw = out_headers_slice.as_mut_ptr(); + if (raw as usize) % header_align != 0 { + return Err(Error::new( + Status::InvalidArg, + "engineDebugQuery: outHeaders must be aligned for debug record headers", + )); + } + raw as *mut ffi::zr_debug_record_header_t + }; + + let rc = unsafe { + ffi::engine_debug_query( + guard.slot.engine, + &debug_query as *const _, + headers_ptr, + headers_cap, + &mut result as *mut _, + ) + }; + if rc != ffi::ZR_OK { + return Err(Error::new( + Status::GenericFailure, + format!("engine_debug_query failed: {rc}"), + )); + } + + Ok(DebugQueryResult { + recordsReturned: result.records_returned, + recordsAvailable: result.records_available, + oldestRecordId: bigint_from_u64(result.oldest_record_id), + newestRecordId: bigint_from_u64(result.newest_record_id), + recordsDropped: result.records_dropped, + }) +} + +#[napi(js_name = "engineDebugGetPayload")] +pub fn engine_debug_get_payload( + engine_id: u32, + record_id: BigInt, + mut out_payload: Uint8Array, +) -> napi::Result { + let guard = get_engine_guard(engine_id).map_err(|_| invalid_arg_error())?; + if !guard.slot.is_owner_thread() { + return Err(invalid_arg_error()); + } + + let record_id = + parse_debug_query_bigint_u64(record_id.sign_bit, &record_id.words).map_err(|_| { + Error::new( + Status::InvalidArg, + "engineDebugGetPayload: recordId must be a non-negative u64", + ) + })?; + + let mut out_size = 0u32; + let out_cap = out_payload.len() as u32; + let out_ptr = out_payload.as_mut().as_mut_ptr(); + let rc = unsafe { + ffi::engine_debug_get_payload( + guard.slot.engine, + record_id, + out_ptr, + out_cap, + &mut out_size as *mut _, + ) + }; + if rc != ffi::ZR_OK { + return Ok(rc); + } + + Ok(out_size as i32) +} + +#[napi(js_name = "engineDebugGetStats")] +pub fn engine_debug_get_stats(engine_id: u32) -> napi::Result { + let guard = get_engine_guard(engine_id).map_err(|_| invalid_arg_error())?; + if !guard.slot.is_owner_thread() { + return Err(invalid_arg_error()); + } + + let mut stats = ffi::zr_debug_stats_t { + total_records: 0, + total_dropped: 0, + error_count: 0, + warn_count: 0, + current_ring_usage: 0, + ring_capacity: 0, + }; + let rc = unsafe { ffi::engine_debug_get_stats(guard.slot.engine, &mut stats as *mut _) }; + if rc != ffi::ZR_OK { + return Err(Error::new( + Status::GenericFailure, + format!("engine_debug_get_stats failed: {rc}"), + )); + } + + Ok(DebugStats { + totalRecords: bigint_from_u64(stats.total_records), + totalDropped: bigint_from_u64(stats.total_dropped), + errorCount: stats.error_count, + warnCount: stats.warn_count, + currentRingUsage: stats.current_ring_usage, + ringCapacity: stats.ring_capacity, + }) +} + +#[napi(js_name = "engineDebugExport")] +pub fn engine_debug_export(engine_id: u32, mut out_buf: Uint8Array) -> i32 { + let guard = match get_engine_guard(engine_id) { + Ok(guard) => guard, + Err(rc) => return rc, + }; + if !guard.slot.is_owner_thread() { + return ffi::ZR_ERR_INVALID_ARGUMENT; + } + + let out_cap = out_buf.len(); + let out_ptr = out_buf.as_mut().as_mut_ptr(); + unsafe { ffi::engine_debug_export(guard.slot.engine, out_ptr, out_cap) } +} + +#[napi(js_name = "engineDebugReset")] +pub fn engine_debug_reset(engine_id: u32) -> i32 { + let guard = match get_engine_guard(engine_id) { + Ok(guard) => guard, + Err(rc) => return rc, + }; + if !guard.slot.is_owner_thread() { + return ffi::ZR_ERR_INVALID_ARGUMENT; + } + + unsafe { ffi::engine_debug_reset(guard.slot.engine) }; + ffi::ZR_OK +} diff --git a/packages/native/src/ffi.rs b/packages/native/src/ffi.rs new file mode 100644 index 00000000..cf038918 --- /dev/null +++ b/packages/native/src/ffi.rs @@ -0,0 +1,626 @@ +#![allow(dead_code, non_camel_case_types)] + +pub(crate) type ZrResultT = i32; + +pub(crate) const ZR_OK: ZrResultT = 0; +pub(crate) const ZR_ERR_INVALID_ARGUMENT: ZrResultT = -1; +pub(crate) const ZR_ERR_LIMIT: ZrResultT = -3; +pub(crate) const ZR_ERR_PLATFORM: ZrResultT = -6; + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_limits_t { + pub(crate) arena_max_total_bytes: u32, + pub(crate) arena_initial_bytes: u32, + pub(crate) out_max_bytes_per_frame: u32, + pub(crate) dl_max_total_bytes: u32, + pub(crate) dl_max_cmds: u32, + pub(crate) dl_max_strings: u32, + pub(crate) dl_max_blobs: u32, + pub(crate) dl_max_clip_depth: u32, + pub(crate) dl_max_text_run_segments: u32, + pub(crate) diff_max_damage_rects: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct plat_config_t { + pub(crate) requested_color_mode: u8, + pub(crate) enable_mouse: u8, + pub(crate) enable_bracketed_paste: u8, + pub(crate) enable_focus_events: u8, + pub(crate) enable_osc52: u8, + pub(crate) _pad: [u8; 3], +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_engine_config_t { + pub(crate) requested_engine_abi_major: u32, + pub(crate) requested_engine_abi_minor: u32, + pub(crate) requested_engine_abi_patch: u32, + pub(crate) requested_drawlist_version: u32, + pub(crate) requested_event_batch_version: u32, + pub(crate) limits: zr_limits_t, + pub(crate) plat: plat_config_t, + pub(crate) tab_width: u32, + pub(crate) width_policy: u32, + pub(crate) target_fps: u32, + pub(crate) enable_scroll_optimizations: u8, + pub(crate) enable_debug_overlay: u8, + pub(crate) enable_replay_recording: u8, + pub(crate) wait_for_output_drain: u8, + pub(crate) cap_force_flags: u32, + pub(crate) cap_suppress_flags: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_engine_runtime_config_t { + pub(crate) limits: zr_limits_t, + pub(crate) plat: plat_config_t, + pub(crate) tab_width: u32, + pub(crate) width_policy: u32, + pub(crate) target_fps: u32, + pub(crate) enable_scroll_optimizations: u8, + pub(crate) enable_debug_overlay: u8, + pub(crate) enable_replay_recording: u8, + pub(crate) wait_for_output_drain: u8, + pub(crate) cap_force_flags: u32, + pub(crate) cap_suppress_flags: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_metrics_t { + pub(crate) struct_size: u32, + pub(crate) negotiated_engine_abi_major: u32, + pub(crate) negotiated_engine_abi_minor: u32, + pub(crate) negotiated_engine_abi_patch: u32, + pub(crate) negotiated_drawlist_version: u32, + pub(crate) negotiated_event_batch_version: u32, + pub(crate) frame_index: u64, + pub(crate) fps: u32, + pub(crate) _pad0: u32, + pub(crate) bytes_emitted_total: u64, + pub(crate) bytes_emitted_last_frame: u32, + pub(crate) _pad1: u32, + pub(crate) dirty_lines_last_frame: u32, + pub(crate) dirty_cols_last_frame: u32, + pub(crate) us_input_last_frame: u32, + pub(crate) us_drawlist_last_frame: u32, + pub(crate) us_diff_last_frame: u32, + pub(crate) us_write_last_frame: u32, + pub(crate) events_out_last_poll: u32, + pub(crate) events_dropped_total: u32, + pub(crate) arena_frame_high_water_bytes: u64, + pub(crate) arena_persistent_high_water_bytes: u64, + pub(crate) damage_rects_last_frame: u32, + pub(crate) damage_cells_last_frame: u32, + pub(crate) damage_full_frame: u8, + pub(crate) _pad2: [u8; 3], +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_terminal_caps_t { + pub(crate) color_mode: u8, + pub(crate) supports_mouse: u8, + pub(crate) supports_bracketed_paste: u8, + pub(crate) supports_focus_events: u8, + pub(crate) supports_osc52: u8, + pub(crate) supports_sync_update: u8, + pub(crate) supports_scroll_region: u8, + pub(crate) supports_cursor_shape: u8, + pub(crate) supports_output_wait_writable: u8, + pub(crate) supports_underline_styles: u8, + pub(crate) supports_colored_underlines: u8, + pub(crate) supports_hyperlinks: u8, + pub(crate) sgr_attrs_supported: u32, + pub(crate) terminal_id: u32, + pub(crate) _pad1: [u8; 3], + pub(crate) cap_flags: u32, + pub(crate) cap_force_flags: u32, + pub(crate) cap_suppress_flags: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct plat_caps_t { + pub(crate) color_mode: u8, + pub(crate) supports_mouse: u8, + pub(crate) supports_bracketed_paste: u8, + pub(crate) supports_focus_events: u8, + pub(crate) supports_osc52: u8, + pub(crate) supports_sync_update: u8, + pub(crate) supports_scroll_region: u8, + pub(crate) supports_cursor_shape: u8, + pub(crate) supports_output_wait_writable: u8, + pub(crate) supports_underline_styles: u8, + pub(crate) supports_colored_underlines: u8, + pub(crate) supports_hyperlinks: u8, + pub(crate) sgr_attrs_supported: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_style_t { + pub(crate) fg_rgb: u32, + pub(crate) bg_rgb: u32, + pub(crate) attrs: u32, + pub(crate) reserved: u32, + pub(crate) underline_rgb: u32, + pub(crate) link_ref: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_cell_t { + pub(crate) glyph: [u8; 32], + pub(crate) glyph_len: u8, + pub(crate) width: u8, + pub(crate) _pad0: u16, + pub(crate) style: zr_style_t, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_rect_t { + pub(crate) x: i32, + pub(crate) y: i32, + pub(crate) w: i32, + pub(crate) h: i32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_fb_t { + pub(crate) cols: u32, + pub(crate) rows: u32, + pub(crate) cells: *mut zr_cell_t, + pub(crate) links: *mut zr_fb_link_t, + pub(crate) links_len: u32, + pub(crate) links_cap: u32, + pub(crate) link_bytes: *mut u8, + pub(crate) link_bytes_len: u32, + pub(crate) link_bytes_cap: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_fb_link_t { + pub(crate) uri_off: u32, + pub(crate) uri_len: u32, + pub(crate) id_off: u32, + pub(crate) id_len: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_fb_painter_t { + pub(crate) fb: *mut zr_fb_t, + pub(crate) clip_stack: *mut zr_rect_t, + pub(crate) clip_cap: u32, + pub(crate) clip_len: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_cursor_state_t { + pub(crate) x: i32, + pub(crate) y: i32, + pub(crate) shape: u8, + pub(crate) visible: u8, + pub(crate) blink: u8, + pub(crate) reserved0: u8, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_term_state_t { + pub(crate) cursor_x: u32, + pub(crate) cursor_y: u32, + pub(crate) cursor_visible: u8, + pub(crate) cursor_shape: u8, + pub(crate) cursor_blink: u8, + pub(crate) flags: u8, + pub(crate) style: zr_style_t, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_diff_stats_t { + pub(crate) dirty_lines: u32, + pub(crate) dirty_cells: u32, + pub(crate) damage_rects: u32, + pub(crate) damage_cells: u32, + pub(crate) damage_full_frame: u8, + pub(crate) path_sweep_used: u8, + pub(crate) path_damage_used: u8, + pub(crate) scroll_opt_attempted: u8, + pub(crate) scroll_opt_hit: u8, + pub(crate) collision_guard_hits: u32, + pub(crate) _pad0: u32, + pub(crate) bytes_emitted: usize, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_damage_rect_t { + pub(crate) x0: u32, + pub(crate) y0: u32, + pub(crate) x1: u32, + pub(crate) y1: u32, +} + +#[repr(C)] +pub(crate) struct zr_engine_t { + _private: [u8; 0], +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_debug_config_t { + pub(crate) enabled: u32, + pub(crate) ring_capacity: u32, + pub(crate) min_severity: u32, + pub(crate) category_mask: u32, + pub(crate) capture_raw_events: u32, + pub(crate) capture_drawlist_bytes: u32, + pub(crate) _pad0: u32, + pub(crate) _pad1: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_debug_query_t { + pub(crate) min_record_id: u64, + pub(crate) max_record_id: u64, + pub(crate) min_frame_id: u64, + pub(crate) max_frame_id: u64, + pub(crate) category_mask: u32, + pub(crate) min_severity: u32, + pub(crate) max_records: u32, + pub(crate) _pad0: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_debug_record_header_t { + pub(crate) record_id: u64, + pub(crate) timestamp_us: u64, + pub(crate) frame_id: u64, + pub(crate) category: u32, + pub(crate) severity: u32, + pub(crate) code: u32, + pub(crate) payload_size: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_debug_query_result_t { + pub(crate) records_returned: u32, + pub(crate) records_available: u32, + pub(crate) oldest_record_id: u64, + pub(crate) newest_record_id: u64, + pub(crate) records_dropped: u32, + pub(crate) _pad0: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_debug_stats_t { + pub(crate) total_records: u64, + pub(crate) total_dropped: u64, + pub(crate) error_count: u32, + pub(crate) warn_count: u32, + pub(crate) current_ring_usage: u32, + pub(crate) ring_capacity: u32, +} + +unsafe extern "C" { + pub(crate) fn zr_engine_config_default() -> zr_engine_config_t; + pub(crate) fn zr_fb_init(fb: *mut zr_fb_t, cols: u32, rows: u32) -> ZrResultT; + pub(crate) fn zr_fb_release(fb: *mut zr_fb_t); + pub(crate) fn zr_fb_cell(fb: *mut zr_fb_t, x: u32, y: u32) -> *mut zr_cell_t; + pub(crate) fn zr_fb_clear(fb: *mut zr_fb_t, style: *const zr_style_t) -> ZrResultT; + pub(crate) fn zr_fb_links_clone_from(dst: *mut zr_fb_t, src: *const zr_fb_t) -> ZrResultT; + pub(crate) fn zr_fb_link_intern( + fb: *mut zr_fb_t, + uri: *const u8, + uri_len: usize, + id: *const u8, + id_len: usize, + out_link_ref: *mut u32, + ) -> ZrResultT; + pub(crate) fn zr_fb_link_lookup( + fb: *const zr_fb_t, + link_ref: u32, + out_uri: *mut *const u8, + out_uri_len: *mut usize, + out_id: *mut *const u8, + out_id_len: *mut usize, + ) -> ZrResultT; + pub(crate) fn zr_fb_painter_begin( + p: *mut zr_fb_painter_t, + fb: *mut zr_fb_t, + clip_stack: *mut zr_rect_t, + clip_cap: u32, + ) -> ZrResultT; + pub(crate) fn zr_fb_clip_push(p: *mut zr_fb_painter_t, clip: zr_rect_t) -> ZrResultT; + pub(crate) fn zr_fb_clip_pop(p: *mut zr_fb_painter_t) -> ZrResultT; + pub(crate) fn zr_fb_put_grapheme( + p: *mut zr_fb_painter_t, + x: i32, + y: i32, + bytes: *const u8, + len: usize, + width: u8, + style: *const zr_style_t, + ) -> ZrResultT; + pub(crate) fn zr_diff_render( + prev: *const zr_fb_t, + next: *const zr_fb_t, + caps: *const plat_caps_t, + initial_term_state: *const zr_term_state_t, + desired_cursor_state: *const zr_cursor_state_t, + lim: *const zr_limits_t, + scratch_damage_rects: *mut zr_damage_rect_t, + scratch_damage_rect_cap: u32, + enable_scroll_optimizations: u8, + out_buf: *mut u8, + out_cap: usize, + out_len: *mut usize, + out_final_term_state: *mut zr_term_state_t, + out_stats: *mut zr_diff_stats_t, + ) -> ZrResultT; + + pub(crate) fn engine_create( + out_engine: *mut *mut zr_engine_t, + cfg: *const zr_engine_config_t, + ) -> ZrResultT; + pub(crate) fn engine_destroy(e: *mut zr_engine_t); + + pub(crate) fn engine_poll_events( + e: *mut zr_engine_t, + timeout_ms: i32, + out_buf: *mut u8, + out_cap: i32, + ) -> i32; + pub(crate) fn engine_post_user_event( + e: *mut zr_engine_t, + tag: u32, + payload: *const u8, + payload_len: i32, + ) -> ZrResultT; + + pub(crate) fn engine_submit_drawlist( + e: *mut zr_engine_t, + bytes: *const u8, + bytes_len: i32, + ) -> ZrResultT; + pub(crate) fn engine_present(e: *mut zr_engine_t) -> ZrResultT; + + pub(crate) fn engine_get_metrics( + e: *mut zr_engine_t, + out_metrics: *mut zr_metrics_t, + ) -> ZrResultT; + pub(crate) fn engine_get_caps( + e: *mut zr_engine_t, + out_caps: *mut zr_terminal_caps_t, + ) -> ZrResultT; + pub(crate) fn engine_set_config( + e: *mut zr_engine_t, + cfg: *const zr_engine_runtime_config_t, + ) -> ZrResultT; + + pub(crate) fn engine_debug_enable( + e: *mut zr_engine_t, + config: *const zr_debug_config_t, + ) -> ZrResultT; + pub(crate) fn engine_debug_disable(e: *mut zr_engine_t); + pub(crate) fn engine_debug_query( + e: *mut zr_engine_t, + query: *const zr_debug_query_t, + out_headers: *mut zr_debug_record_header_t, + out_headers_cap: u32, + out_result: *mut zr_debug_query_result_t, + ) -> ZrResultT; + pub(crate) fn engine_debug_get_payload( + e: *mut zr_engine_t, + record_id: u64, + out_payload: *mut u8, + out_cap: u32, + out_size: *mut u32, + ) -> ZrResultT; + pub(crate) fn engine_debug_get_stats( + e: *mut zr_engine_t, + out_stats: *mut zr_debug_stats_t, + ) -> ZrResultT; + pub(crate) fn engine_debug_export(e: *mut zr_engine_t, out_buf: *mut u8, out_cap: usize) -> i32; + pub(crate) fn engine_debug_reset(e: *mut zr_engine_t); +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_debug_config_t { + pub(crate) enabled: u32, + pub(crate) ring_capacity: u32, + pub(crate) min_severity: u32, + pub(crate) category_mask: u32, + pub(crate) capture_raw_events: u32, + pub(crate) capture_drawlist_bytes: u32, + pub(crate) _pad0: u32, + pub(crate) _pad1: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_debug_query_t { + pub(crate) min_record_id: u64, + pub(crate) max_record_id: u64, + pub(crate) min_frame_id: u64, + pub(crate) max_frame_id: u64, + pub(crate) category_mask: u32, + pub(crate) min_severity: u32, + pub(crate) max_records: u32, + pub(crate) _pad0: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_debug_record_header_t { + pub(crate) record_id: u64, + pub(crate) timestamp_us: u64, + pub(crate) frame_id: u64, + pub(crate) category: u32, + pub(crate) severity: u32, + pub(crate) code: u32, + pub(crate) payload_size: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_debug_query_result_t { + pub(crate) records_returned: u32, + pub(crate) records_available: u32, + pub(crate) oldest_record_id: u64, + pub(crate) newest_record_id: u64, + pub(crate) records_dropped: u32, + pub(crate) _pad0: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct zr_debug_stats_t { + pub(crate) total_records: u64, + pub(crate) total_dropped: u64, + pub(crate) error_count: u32, + pub(crate) warn_count: u32, + pub(crate) current_ring_usage: u32, + pub(crate) ring_capacity: u32, +} + +extern "C" { + pub(crate) fn zr_engine_config_default() -> zr_engine_config_t; + pub(crate) fn zr_fb_init(fb: *mut zr_fb_t, cols: u32, rows: u32) -> ZrResultT; + pub(crate) fn zr_fb_release(fb: *mut zr_fb_t); + pub(crate) fn zr_fb_cell(fb: *mut zr_fb_t, x: u32, y: u32) -> *mut zr_cell_t; + pub(crate) fn zr_fb_clear(fb: *mut zr_fb_t, style: *const zr_style_t) -> ZrResultT; + pub(crate) fn zr_fb_links_clone_from(dst: *mut zr_fb_t, src: *const zr_fb_t) -> ZrResultT; + pub(crate) fn zr_fb_link_intern( + fb: *mut zr_fb_t, + uri: *const u8, + uri_len: usize, + id: *const u8, + id_len: usize, + out_link_ref: *mut u32, + ) -> ZrResultT; + pub(crate) fn zr_fb_link_lookup( + fb: *const zr_fb_t, + link_ref: u32, + out_uri: *mut *const u8, + out_uri_len: *mut usize, + out_id: *mut *const u8, + out_id_len: *mut usize, + ) -> ZrResultT; + pub(crate) fn zr_fb_painter_begin( + p: *mut zr_fb_painter_t, + fb: *mut zr_fb_t, + clip_stack: *mut zr_rect_t, + clip_cap: u32, + ) -> ZrResultT; + pub(crate) fn zr_fb_clip_push(p: *mut zr_fb_painter_t, clip: zr_rect_t) -> ZrResultT; + pub(crate) fn zr_fb_clip_pop(p: *mut zr_fb_painter_t) -> ZrResultT; + pub(crate) fn zr_fb_put_grapheme( + p: *mut zr_fb_painter_t, + x: i32, + y: i32, + bytes: *const u8, + len: usize, + width: u8, + style: *const zr_style_t, + ) -> ZrResultT; + pub(crate) fn zr_diff_render( + prev: *const zr_fb_t, + next: *const zr_fb_t, + caps: *const plat_caps_t, + initial_term_state: *const zr_term_state_t, + desired_cursor_state: *const zr_cursor_state_t, + lim: *const zr_limits_t, + scratch_damage_rects: *mut zr_damage_rect_t, + scratch_damage_rect_cap: u32, + enable_scroll_optimizations: u8, + out_buf: *mut u8, + out_cap: usize, + out_len: *mut usize, + out_final_term_state: *mut zr_term_state_t, + out_stats: *mut zr_diff_stats_t, + ) -> ZrResultT; + + pub(crate) fn engine_create( + out_engine: *mut *mut zr_engine_t, + cfg: *const zr_engine_config_t, + ) -> ZrResultT; + pub(crate) fn engine_destroy(e: *mut zr_engine_t); + + pub(crate) fn engine_poll_events( + e: *mut zr_engine_t, + timeout_ms: i32, + out_buf: *mut u8, + out_cap: i32, + ) -> i32; + pub(crate) fn engine_post_user_event( + e: *mut zr_engine_t, + tag: u32, + payload: *const u8, + payload_len: i32, + ) -> ZrResultT; + + pub(crate) fn engine_submit_drawlist( + e: *mut zr_engine_t, + bytes: *const u8, + bytes_len: i32, + ) -> ZrResultT; + pub(crate) fn engine_present(e: *mut zr_engine_t) -> ZrResultT; + + pub(crate) fn engine_get_metrics( + e: *mut zr_engine_t, + out_metrics: *mut zr_metrics_t, + ) -> ZrResultT; + pub(crate) fn engine_get_caps( + e: *mut zr_engine_t, + out_caps: *mut zr_terminal_caps_t, + ) -> ZrResultT; + pub(crate) fn engine_set_config( + e: *mut zr_engine_t, + cfg: *const zr_engine_runtime_config_t, + ) -> ZrResultT; + + pub(crate) fn engine_debug_enable( + e: *mut zr_engine_t, + config: *const zr_debug_config_t, + ) -> ZrResultT; + pub(crate) fn engine_debug_disable(e: *mut zr_engine_t); + pub(crate) fn engine_debug_query( + e: *mut zr_engine_t, + query: *const zr_debug_query_t, + out_headers: *mut zr_debug_record_header_t, + out_headers_cap: u32, + out_result: *mut zr_debug_query_result_t, + ) -> ZrResultT; + pub(crate) fn engine_debug_get_payload( + e: *mut zr_engine_t, + record_id: u64, + out_payload: *mut u8, + out_cap: u32, + out_size: *mut u32, + ) -> ZrResultT; + pub(crate) fn engine_debug_get_stats( + e: *mut zr_engine_t, + out_stats: *mut zr_debug_stats_t, + ) -> ZrResultT; + pub(crate) fn engine_debug_export(e: *mut zr_engine_t, out_buf: *mut u8, out_cap: usize) + -> i32; + pub(crate) fn engine_debug_reset(e: *mut zr_engine_t); +} diff --git a/packages/native/src/lib.rs b/packages/native/src/lib.rs index ccf946a4..be25b91e 100644 --- a/packages/native/src/lib.rs +++ b/packages/native/src/lib.rs @@ -1,2255 +1,352 @@ #![allow(non_snake_case)] -use napi::bindgen_prelude::{BigInt, Error, Status, Uint8Array, ValueType}; -use napi::{Env, JsObject, JsUnknown}; -use napi_derive::napi; -use std::collections::HashMap; -use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering}; -use std::sync::{Condvar, Mutex, OnceLock}; - -type ParseResult = std::result::Result; - -#[allow(dead_code)] -mod ffi { - pub type ZrResultT = i32; - - pub const ZR_OK: ZrResultT = 0; - pub const ZR_ERR_INVALID_ARGUMENT: ZrResultT = -1; - pub const ZR_ERR_LIMIT: ZrResultT = -3; - pub const ZR_ERR_PLATFORM: ZrResultT = -6; - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_limits_t { - pub arena_max_total_bytes: u32, - pub arena_initial_bytes: u32, - pub out_max_bytes_per_frame: u32, - pub dl_max_total_bytes: u32, - pub dl_max_cmds: u32, - pub dl_max_strings: u32, - pub dl_max_blobs: u32, - pub dl_max_clip_depth: u32, - pub dl_max_text_run_segments: u32, - pub diff_max_damage_rects: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct plat_config_t { - pub requested_color_mode: u8, - pub enable_mouse: u8, - pub enable_bracketed_paste: u8, - pub enable_focus_events: u8, - pub enable_osc52: u8, - pub _pad: [u8; 3], - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_engine_config_t { - pub requested_engine_abi_major: u32, - pub requested_engine_abi_minor: u32, - pub requested_engine_abi_patch: u32, - pub requested_drawlist_version: u32, - pub requested_event_batch_version: u32, - pub limits: zr_limits_t, - pub plat: plat_config_t, - pub tab_width: u32, - pub width_policy: u32, - pub target_fps: u32, - pub enable_scroll_optimizations: u8, - pub enable_debug_overlay: u8, - pub enable_replay_recording: u8, - pub wait_for_output_drain: u8, - pub cap_force_flags: u32, - pub cap_suppress_flags: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_engine_runtime_config_t { - pub limits: zr_limits_t, - pub plat: plat_config_t, - pub tab_width: u32, - pub width_policy: u32, - pub target_fps: u32, - pub enable_scroll_optimizations: u8, - pub enable_debug_overlay: u8, - pub enable_replay_recording: u8, - pub wait_for_output_drain: u8, - pub cap_force_flags: u32, - pub cap_suppress_flags: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_metrics_t { - pub struct_size: u32, - pub negotiated_engine_abi_major: u32, - pub negotiated_engine_abi_minor: u32, - pub negotiated_engine_abi_patch: u32, - pub negotiated_drawlist_version: u32, - pub negotiated_event_batch_version: u32, - pub frame_index: u64, - pub fps: u32, - pub _pad0: u32, - pub bytes_emitted_total: u64, - pub bytes_emitted_last_frame: u32, - pub _pad1: u32, - pub dirty_lines_last_frame: u32, - pub dirty_cols_last_frame: u32, - pub us_input_last_frame: u32, - pub us_drawlist_last_frame: u32, - pub us_diff_last_frame: u32, - pub us_write_last_frame: u32, - pub events_out_last_poll: u32, - pub events_dropped_total: u32, - pub arena_frame_high_water_bytes: u64, - pub arena_persistent_high_water_bytes: u64, - // v2 damage summary fields - pub damage_rects_last_frame: u32, - pub damage_cells_last_frame: u32, - pub damage_full_frame: u8, - pub _pad2: [u8; 3], - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_terminal_caps_t { - pub color_mode: u8, - pub supports_mouse: u8, - pub supports_bracketed_paste: u8, - pub supports_focus_events: u8, - pub supports_osc52: u8, - pub supports_sync_update: u8, - pub supports_scroll_region: u8, - pub supports_cursor_shape: u8, - pub supports_output_wait_writable: u8, - pub supports_underline_styles: u8, - pub supports_colored_underlines: u8, - pub supports_hyperlinks: u8, - pub sgr_attrs_supported: u32, - pub terminal_id: u32, - pub _pad1: [u8; 3], - pub cap_flags: u32, - pub cap_force_flags: u32, - pub cap_suppress_flags: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct plat_caps_t { - pub color_mode: u8, - pub supports_mouse: u8, - pub supports_bracketed_paste: u8, - pub supports_focus_events: u8, - pub supports_osc52: u8, - pub supports_sync_update: u8, - pub supports_scroll_region: u8, - pub supports_cursor_shape: u8, - pub supports_output_wait_writable: u8, - pub supports_underline_styles: u8, - pub supports_colored_underlines: u8, - pub supports_hyperlinks: u8, - pub sgr_attrs_supported: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_style_t { - pub fg_rgb: u32, - pub bg_rgb: u32, - pub attrs: u32, - pub reserved: u32, - pub underline_rgb: u32, - pub link_ref: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_cell_t { - pub glyph: [u8; 32], - pub glyph_len: u8, - pub width: u8, - pub _pad0: u16, - pub style: zr_style_t, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_rect_t { - pub x: i32, - pub y: i32, - pub w: i32, - pub h: i32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_fb_t { - pub cols: u32, - pub rows: u32, - pub cells: *mut zr_cell_t, - pub links: *mut zr_fb_link_t, - pub links_len: u32, - pub links_cap: u32, - pub link_bytes: *mut u8, - pub link_bytes_len: u32, - pub link_bytes_cap: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_fb_link_t { - pub uri_off: u32, - pub uri_len: u32, - pub id_off: u32, - pub id_len: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_fb_painter_t { - pub fb: *mut zr_fb_t, - pub clip_stack: *mut zr_rect_t, - pub clip_cap: u32, - pub clip_len: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_cursor_state_t { - pub x: i32, - pub y: i32, - pub shape: u8, - pub visible: u8, - pub blink: u8, - pub reserved0: u8, - } +mod config; +mod debug; +mod ffi; +mod registry; - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_term_state_t { - pub cursor_x: u32, - pub cursor_y: u32, - pub cursor_visible: u8, - pub cursor_shape: u8, - pub cursor_blink: u8, - pub flags: u8, - pub style: zr_style_t, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_diff_stats_t { - pub dirty_lines: u32, - pub dirty_cells: u32, - pub damage_rects: u32, - pub damage_cells: u32, - pub damage_full_frame: u8, - pub path_sweep_used: u8, - pub path_damage_used: u8, - pub scroll_opt_attempted: u8, - pub scroll_opt_hit: u8, - pub collision_guard_hits: u32, - pub _pad0: u32, - pub bytes_emitted: usize, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_damage_rect_t { - pub x0: u32, - pub y0: u32, - pub x1: u32, - pub y1: u32, - } - - #[repr(C)] - pub struct zr_engine_t { - _private: [u8; 0], - } - - // Debug trace structures - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_debug_config_t { - pub enabled: u32, - pub ring_capacity: u32, - pub min_severity: u32, - pub category_mask: u32, - pub capture_raw_events: u32, - pub capture_drawlist_bytes: u32, - pub _pad0: u32, - pub _pad1: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_debug_query_t { - pub min_record_id: u64, - pub max_record_id: u64, - pub min_frame_id: u64, - pub max_frame_id: u64, - pub category_mask: u32, - pub min_severity: u32, - pub max_records: u32, - pub _pad0: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_debug_record_header_t { - pub record_id: u64, - pub timestamp_us: u64, - pub frame_id: u64, - pub category: u32, - pub severity: u32, - pub code: u32, - pub payload_size: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_debug_query_result_t { - pub records_returned: u32, - pub records_available: u32, - pub oldest_record_id: u64, - pub newest_record_id: u64, - pub records_dropped: u32, - pub _pad0: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - pub struct zr_debug_stats_t { - pub total_records: u64, - pub total_dropped: u64, - pub error_count: u32, - pub warn_count: u32, - pub current_ring_usage: u32, - pub ring_capacity: u32, - } - - extern "C" { - pub fn zr_engine_config_default() -> zr_engine_config_t; - pub fn zr_fb_init(fb: *mut zr_fb_t, cols: u32, rows: u32) -> ZrResultT; - pub fn zr_fb_release(fb: *mut zr_fb_t); - pub fn zr_fb_cell(fb: *mut zr_fb_t, x: u32, y: u32) -> *mut zr_cell_t; - pub fn zr_fb_clear(fb: *mut zr_fb_t, style: *const zr_style_t) -> ZrResultT; - pub fn zr_fb_links_clone_from(dst: *mut zr_fb_t, src: *const zr_fb_t) -> ZrResultT; - pub fn zr_fb_link_intern( - fb: *mut zr_fb_t, - uri: *const u8, - uri_len: usize, - id: *const u8, - id_len: usize, - out_link_ref: *mut u32, - ) -> ZrResultT; - pub fn zr_fb_link_lookup( - fb: *const zr_fb_t, - link_ref: u32, - out_uri: *mut *const u8, - out_uri_len: *mut usize, - out_id: *mut *const u8, - out_id_len: *mut usize, - ) -> ZrResultT; - pub fn zr_fb_painter_begin( - p: *mut zr_fb_painter_t, - fb: *mut zr_fb_t, - clip_stack: *mut zr_rect_t, - clip_cap: u32, - ) -> ZrResultT; - pub fn zr_fb_clip_push(p: *mut zr_fb_painter_t, clip: zr_rect_t) -> ZrResultT; - pub fn zr_fb_clip_pop(p: *mut zr_fb_painter_t) -> ZrResultT; - pub fn zr_fb_put_grapheme( - p: *mut zr_fb_painter_t, - x: i32, - y: i32, - bytes: *const u8, - len: usize, - width: u8, - style: *const zr_style_t, - ) -> ZrResultT; - pub fn zr_diff_render( - prev: *const zr_fb_t, - next: *const zr_fb_t, - caps: *const plat_caps_t, - initial_term_state: *const zr_term_state_t, - desired_cursor_state: *const zr_cursor_state_t, - lim: *const zr_limits_t, - scratch_damage_rects: *mut zr_damage_rect_t, - scratch_damage_rect_cap: u32, - enable_scroll_optimizations: u8, - out_buf: *mut u8, - out_cap: usize, - out_len: *mut usize, - out_final_term_state: *mut zr_term_state_t, - out_stats: *mut zr_diff_stats_t, - ) -> ZrResultT; - - pub fn engine_create(out_engine: *mut *mut zr_engine_t, cfg: *const zr_engine_config_t) -> ZrResultT; - pub fn engine_destroy(e: *mut zr_engine_t); - - pub fn engine_poll_events(e: *mut zr_engine_t, timeout_ms: i32, out_buf: *mut u8, out_cap: i32) -> i32; - pub fn engine_post_user_event(e: *mut zr_engine_t, tag: u32, payload: *const u8, payload_len: i32) -> ZrResultT; - - pub fn engine_submit_drawlist(e: *mut zr_engine_t, bytes: *const u8, bytes_len: i32) -> ZrResultT; - pub fn engine_present(e: *mut zr_engine_t) -> ZrResultT; +#[cfg(test)] +mod tests; + +pub use crate::debug::{ + engine_debug_disable, engine_debug_enable, engine_debug_export, engine_debug_get_payload, + engine_debug_get_stats, engine_debug_query, engine_debug_reset, DebugQueryResult, DebugStats, +}; + +use crate::config::{ + apply_create_cfg_strict, apply_runtime_cfg_strict, create_default_runtime_cfg, +}; +use crate::registry::{get_engine_guard, register_engine, take_engine_for_owner}; +use napi::bindgen_prelude::{BigInt, Error, Status, Uint8Array}; +use napi::{Env, JsObject}; +use napi_derive::napi; - pub fn engine_get_metrics(e: *mut zr_engine_t, out_metrics: *mut zr_metrics_t) -> ZrResultT; - pub fn engine_get_caps(e: *mut zr_engine_t, out_caps: *mut zr_terminal_caps_t) -> ZrResultT; - pub fn engine_set_config(e: *mut zr_engine_t, cfg: *const zr_engine_runtime_config_t) -> ZrResultT; +pub(crate) fn bigint_from_u64(value: u64) -> BigInt { + BigInt { + sign_bit: false, + words: vec![value], + } +} - // Debug trace API - pub fn engine_debug_enable(e: *mut zr_engine_t, config: *const zr_debug_config_t) -> ZrResultT; - pub fn engine_debug_disable(e: *mut zr_engine_t); - pub fn engine_debug_query( - e: *mut zr_engine_t, - query: *const zr_debug_query_t, - out_headers: *mut zr_debug_record_header_t, - out_headers_cap: u32, - out_result: *mut zr_debug_query_result_t, - ) -> ZrResultT; - pub fn engine_debug_get_payload( - e: *mut zr_engine_t, - record_id: u64, - out_payload: *mut u8, - out_cap: u32, - out_size: *mut u32, - ) -> ZrResultT; - pub fn engine_debug_get_stats(e: *mut zr_engine_t, out_stats: *mut zr_debug_stats_t) -> ZrResultT; - pub fn engine_debug_export(e: *mut zr_engine_t, out_buf: *mut u8, out_cap: usize) -> i32; - pub fn engine_debug_reset(e: *mut zr_engine_t); - } +pub(crate) fn invalid_arg_error() -> Error { + Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT") } #[napi(object)] #[allow(non_snake_case)] pub struct EngineMetrics { - pub structSize: u32, - - pub negotiatedEngineAbiMajor: u32, - pub negotiatedEngineAbiMinor: u32, - pub negotiatedEngineAbiPatch: u32, - - pub negotiatedDrawlistVersion: u32, - pub negotiatedEventBatchVersion: u32, - - pub frameIndex: BigInt, - pub fps: u32, - - pub bytesEmittedTotal: BigInt, - pub bytesEmittedLastFrame: u32, - - pub dirtyLinesLastFrame: u32, - pub dirtyColsLastFrame: u32, - - pub usInputLastFrame: u32, - pub usDrawlistLastFrame: u32, - pub usDiffLastFrame: u32, - pub usWriteLastFrame: u32, - - pub eventsOutLastPoll: u32, - pub eventsDroppedTotal: u32, - - pub arenaFrameHighWaterBytes: BigInt, - pub arenaPersistentHighWaterBytes: BigInt, - - // v2 damage summary fields - pub damageRectsLastFrame: u32, - pub damageCellsLastFrame: u32, - pub damageFullFrame: bool, + pub structSize: u32, + pub negotiatedEngineAbiMajor: u32, + pub negotiatedEngineAbiMinor: u32, + pub negotiatedEngineAbiPatch: u32, + pub negotiatedDrawlistVersion: u32, + pub negotiatedEventBatchVersion: u32, + pub frameIndex: BigInt, + pub fps: u32, + pub bytesEmittedTotal: BigInt, + pub bytesEmittedLastFrame: u32, + pub dirtyLinesLastFrame: u32, + pub dirtyColsLastFrame: u32, + pub usInputLastFrame: u32, + pub usDrawlistLastFrame: u32, + pub usDiffLastFrame: u32, + pub usWriteLastFrame: u32, + pub eventsOutLastPoll: u32, + pub eventsDroppedTotal: u32, + pub arenaFrameHighWaterBytes: BigInt, + pub arenaPersistentHighWaterBytes: BigInt, + pub damageRectsLastFrame: u32, + pub damageCellsLastFrame: u32, + pub damageFullFrame: bool, } #[napi(object)] #[allow(non_snake_case)] pub struct TerminalCaps { - /// Color mode: 0=unknown, 1=16, 2=256, 3=rgb - pub colorMode: u32, - pub supportsMouse: bool, - pub supportsBracketedPaste: bool, - pub supportsFocusEvents: bool, - pub supportsOsc52: bool, - pub supportsSyncUpdate: bool, - pub supportsScrollRegion: bool, - pub supportsCursorShape: bool, - pub supportsOutputWaitWritable: bool, - pub supportsUnderlineStyles: bool, - pub supportsColoredUnderlines: bool, - pub supportsHyperlinks: bool, - /// Bitmask of supported SGR attributes - pub sgrAttrsSupported: u32, -} - -struct EngineSlot { - engine: *mut ffi::zr_engine_t, - owner_thread_id: u64, - active_calls: AtomicUsize, - active_calls_mu: Mutex<()>, - active_calls_cv: Condvar, - destroyed: AtomicBool, -} - -unsafe impl Send for EngineSlot {} -unsafe impl Sync for EngineSlot {} - -impl EngineSlot { - fn is_owner_thread(&self) -> bool { - self.owner_thread_id == current_thread_id_u64() - } -} - -struct EngineGuard { - slot: std::sync::Arc, -} - -impl Drop for EngineGuard { - fn drop(&mut self) { - let prev = self.slot.active_calls.fetch_sub(1, Ordering::Release); - if prev == 1 { - self.slot.active_calls_cv.notify_all(); + /// Color mode: 0=unknown, 1=16, 2=256, 3=rgb + pub colorMode: u32, + pub supportsMouse: bool, + pub supportsBracketedPaste: bool, + pub supportsFocusEvents: bool, + pub supportsOsc52: bool, + pub supportsSyncUpdate: bool, + pub supportsScrollRegion: bool, + pub supportsCursorShape: bool, + pub supportsOutputWaitWritable: bool, + pub supportsUnderlineStyles: bool, + pub supportsColoredUnderlines: bool, + pub supportsHyperlinks: bool, + /// Bitmask of supported SGR attributes + pub sgrAttrsSupported: u32, +} + +fn empty_metrics() -> ffi::zr_metrics_t { + ffi::zr_metrics_t { + struct_size: std::mem::size_of::() as u32, + negotiated_engine_abi_major: 0, + negotiated_engine_abi_minor: 0, + negotiated_engine_abi_patch: 0, + negotiated_drawlist_version: 0, + negotiated_event_batch_version: 0, + frame_index: 0, + fps: 0, + _pad0: 0, + bytes_emitted_total: 0, + bytes_emitted_last_frame: 0, + _pad1: 0, + dirty_lines_last_frame: 0, + dirty_cols_last_frame: 0, + us_input_last_frame: 0, + us_drawlist_last_frame: 0, + us_diff_last_frame: 0, + us_write_last_frame: 0, + events_out_last_poll: 0, + events_dropped_total: 0, + arena_frame_high_water_bytes: 0, + arena_persistent_high_water_bytes: 0, + damage_rects_last_frame: 0, + damage_cells_last_frame: 0, + damage_full_frame: 0, + _pad2: [0, 0, 0], } - } -} - -static REGISTRY: OnceLock>>> = OnceLock::new(); -static NEXT_ENGINE_ID: AtomicU32 = AtomicU32::new(1); -static NEXT_THREAD_ID: AtomicU64 = AtomicU64::new(1); - -fn registry() -> &'static Mutex>> { - REGISTRY.get_or_init(|| Mutex::new(HashMap::new())) -} - -fn current_thread_id_u64() -> u64 { - thread_local! { - static THREAD_ID: u64 = NEXT_THREAD_ID.fetch_add(1, Ordering::Relaxed); - } - THREAD_ID.with(|id| *id) -} - -fn alloc_engine_id() -> Result { - loop { - let cur = NEXT_ENGINE_ID.load(Ordering::Relaxed); - if cur == 0 { - return Err(ffi::ZR_ERR_LIMIT); - } - if cur == u32::MAX { - if NEXT_ENGINE_ID - .compare_exchange(cur, 0, Ordering::SeqCst, Ordering::Relaxed) - .is_ok() - { - return Ok(cur); - } - continue; - } - let next = cur.wrapping_add(1); - if NEXT_ENGINE_ID - .compare_exchange(cur, next, Ordering::SeqCst, Ordering::Relaxed) - .is_ok() - { - return Ok(cur); - } - } -} - -fn lock_registry(f: impl FnOnce(&mut HashMap>) -> T) -> T { - let mut guard = match registry().lock() { - Ok(g) => g, - Err(poison) => poison.into_inner(), - }; - f(&mut guard) -} - -fn get_engine_guard(engine_id: u32) -> Result { - if engine_id == 0 { - return Err(ffi::ZR_ERR_INVALID_ARGUMENT); - } - - lock_registry(|map| { - let slot = match map.get(&engine_id) { - Some(s) => std::sync::Arc::clone(s), - None => return Err(ffi::ZR_ERR_INVALID_ARGUMENT), - }; - slot.active_calls.fetch_add(1, Ordering::Acquire); - Ok(EngineGuard { slot }) - }) -} - -fn validate_known_keys(obj: &JsObject, allowed: &[(&str, &str)], ctx: &str) -> napi::Result<()> { - let names = obj.get_property_names()?; - let len = names.get_array_length()?; - - 'outer: for i in 0..len { - let unk = names.get_element::(i)?; - let s = unk.coerce_to_string()?; - let k = s.into_utf8()?.as_str()?.to_owned(); - for (primary, alias) in allowed { - if k == *primary || k == *alias { - continue 'outer; - } - } - return Err(Error::new(Status::InvalidArg, format!("{ctx}: unknown key: {k}"))); - } - Ok(()) -} - -const LIMITS_KEYS: &[(&str, &str)] = &[ - ("arenaMaxTotalBytes", "arena_max_total_bytes"), - ("arenaInitialBytes", "arena_initial_bytes"), - ("outMaxBytesPerFrame", "out_max_bytes_per_frame"), - ("dlMaxTotalBytes", "dl_max_total_bytes"), - ("dlMaxCmds", "dl_max_cmds"), - ("dlMaxStrings", "dl_max_strings"), - ("dlMaxBlobs", "dl_max_blobs"), - ("dlMaxClipDepth", "dl_max_clip_depth"), - ("dlMaxTextRunSegments", "dl_max_text_run_segments"), - ("diffMaxDamageRects", "diff_max_damage_rects"), -]; - -const PLAT_KEYS: &[(&str, &str)] = &[ - ("requestedColorMode", "requested_color_mode"), - ("enableMouse", "enable_mouse"), - ("enableBracketedPaste", "enable_bracketed_paste"), - ("enableFocusEvents", "enable_focus_events"), - ("enableOsc52", "enable_osc52"), -]; - -const CREATE_CFG_KEYS: &[(&str, &str)] = &[ - ("requestedEngineAbiMajor", "requested_engine_abi_major"), - ("requestedEngineAbiMinor", "requested_engine_abi_minor"), - ("requestedEngineAbiPatch", "requested_engine_abi_patch"), - ("requestedDrawlistVersion", "requested_drawlist_version"), - ("requestedEventBatchVersion", "requested_event_batch_version"), - ("limits", "limits"), - ("plat", "plat"), - ("tabWidth", "tab_width"), - ("widthPolicy", "width_policy"), - ("targetFps", "target_fps"), - ("enableScrollOptimizations", "enable_scroll_optimizations"), - ("enableDebugOverlay", "enable_debug_overlay"), - ("enableReplayRecording", "enable_replay_recording"), - ("waitForOutputDrain", "wait_for_output_drain"), - ("capForceFlags", "cap_force_flags"), - ("capSuppressFlags", "cap_suppress_flags"), -]; - -const RUNTIME_CFG_KEYS: &[(&str, &str)] = &[ - ("limits", "limits"), - ("plat", "plat"), - ("tabWidth", "tab_width"), - ("widthPolicy", "width_policy"), - ("targetFps", "target_fps"), - ("enableScrollOptimizations", "enable_scroll_optimizations"), - ("enableDebugOverlay", "enable_debug_overlay"), - ("enableReplayRecording", "enable_replay_recording"), - ("waitForOutputDrain", "wait_for_output_drain"), - ("capForceFlags", "cap_force_flags"), - ("capSuppressFlags", "cap_suppress_flags"), -]; - -fn apply_create_cfg_strict(dst: &mut ffi::zr_engine_config_t, obj: &JsObject) -> napi::Result<()> { - validate_known_keys(obj, CREATE_CFG_KEYS, "engineCreate config")?; - if let Some(lim) = js_obj(obj, "limits", "limits") - .map_err(|_| Error::new(Status::InvalidArg, "engineCreate: limits must be an object"))? - { - validate_known_keys(&lim, LIMITS_KEYS, "engineCreate config.limits")?; - } - if let Some(plat) = js_obj(obj, "plat", "plat") - .map_err(|_| Error::new(Status::InvalidArg, "engineCreate: plat must be an object"))? - { - validate_known_keys(&plat, PLAT_KEYS, "engineCreate config.plat")?; - } - - apply_create_cfg(dst, obj).map_err(|_| Error::new(Status::InvalidArg, "engineCreate: invalid config value"))?; - Ok(()) } -fn apply_runtime_cfg_strict(dst: &mut ffi::zr_engine_runtime_config_t, obj: &JsObject) -> napi::Result<()> { - validate_known_keys(obj, RUNTIME_CFG_KEYS, "engineSetConfig config")?; - if let Some(lim) = js_obj(obj, "limits", "limits") - .map_err(|_| Error::new(Status::InvalidArg, "engineSetConfig: limits must be an object"))? - { - validate_known_keys(&lim, LIMITS_KEYS, "engineSetConfig config.limits")?; - } - if let Some(plat) = js_obj(obj, "plat", "plat") - .map_err(|_| Error::new(Status::InvalidArg, "engineSetConfig: plat must be an object"))? - { - validate_known_keys(&plat, PLAT_KEYS, "engineSetConfig config.plat")?; - } - - apply_runtime_cfg(dst, obj).map_err(|_| Error::new(Status::InvalidArg, "engineSetConfig: invalid config value"))?; - Ok(()) -} - -fn js_u32(obj: &JsObject, primary: &str, alias: &str) -> ParseResult> { - for name in [primary, alias] { - let v = match obj.get_named_property::(name) { - Ok(v) => v, - Err(_) => continue, - }; - if v.get_type().map_err(|_| ())? == ValueType::Undefined { - continue; +fn empty_terminal_caps() -> ffi::zr_terminal_caps_t { + ffi::zr_terminal_caps_t { + color_mode: 0, + supports_mouse: 0, + supports_bracketed_paste: 0, + supports_focus_events: 0, + supports_osc52: 0, + supports_sync_update: 0, + supports_scroll_region: 0, + supports_cursor_shape: 0, + supports_output_wait_writable: 0, + supports_underline_styles: 0, + supports_colored_underlines: 0, + supports_hyperlinks: 0, + sgr_attrs_supported: 0, + terminal_id: 0, + _pad1: [0, 0, 0], + cap_flags: 0, + cap_force_flags: 0, + cap_suppress_flags: 0, } - let n = v.coerce_to_number().map_err(|_| ())?; - let f = n.get_double().map_err(|_| ())?; - if !f.is_finite() || f < 0.0 || f > (u32::MAX as f64) || f.fract() != 0.0 { - return Err(()); - } - return Ok(Some(f as u32)); - } - Ok(None) } -fn js_u8_bool(obj: &JsObject, primary: &str, alias: &str) -> ParseResult> { - for name in [primary, alias] { - let v = match obj.get_named_property::(name) { - Ok(v) => v, - Err(_) => continue, - }; - match v.get_type().map_err(|_| ())? { - ValueType::Undefined => continue, - ValueType::Boolean => { - let b = v.coerce_to_bool().map_err(|_| ())?; - return Ok(Some(if b.get_value().map_err(|_| ())? { 1 } else { 0 })); - } - ValueType::Number => { - let n = v.coerce_to_number().map_err(|_| ())?; - let f = n.get_double().map_err(|_| ())?; - if f == 0.0 { - return Ok(Some(0)); - } - if f == 1.0 { - return Ok(Some(1)); - } - return Err(()); - } - _ => return Err(()), +fn metrics_to_js(metrics: ffi::zr_metrics_t) -> EngineMetrics { + EngineMetrics { + structSize: metrics.struct_size, + negotiatedEngineAbiMajor: metrics.negotiated_engine_abi_major, + negotiatedEngineAbiMinor: metrics.negotiated_engine_abi_minor, + negotiatedEngineAbiPatch: metrics.negotiated_engine_abi_patch, + negotiatedDrawlistVersion: metrics.negotiated_drawlist_version, + negotiatedEventBatchVersion: metrics.negotiated_event_batch_version, + frameIndex: bigint_from_u64(metrics.frame_index), + fps: metrics.fps, + bytesEmittedTotal: bigint_from_u64(metrics.bytes_emitted_total), + bytesEmittedLastFrame: metrics.bytes_emitted_last_frame, + dirtyLinesLastFrame: metrics.dirty_lines_last_frame, + dirtyColsLastFrame: metrics.dirty_cols_last_frame, + usInputLastFrame: metrics.us_input_last_frame, + usDrawlistLastFrame: metrics.us_drawlist_last_frame, + usDiffLastFrame: metrics.us_diff_last_frame, + usWriteLastFrame: metrics.us_write_last_frame, + eventsOutLastPoll: metrics.events_out_last_poll, + eventsDroppedTotal: metrics.events_dropped_total, + arenaFrameHighWaterBytes: bigint_from_u64(metrics.arena_frame_high_water_bytes), + arenaPersistentHighWaterBytes: bigint_from_u64(metrics.arena_persistent_high_water_bytes), + damageRectsLastFrame: metrics.damage_rects_last_frame, + damageCellsLastFrame: metrics.damage_cells_last_frame, + damageFullFrame: metrics.damage_full_frame != 0, } - } - Ok(None) } -fn js_obj(obj: &JsObject, primary: &str, alias: &str) -> ParseResult> { - for name in [primary, alias] { - let v = match obj.get_named_property::(name) { - Ok(v) => v, - Err(_) => continue, - }; - if v.get_type().map_err(|_| ())? == ValueType::Undefined { - continue; +fn terminal_caps_to_js(caps: ffi::zr_terminal_caps_t) -> TerminalCaps { + TerminalCaps { + colorMode: caps.color_mode as u32, + supportsMouse: caps.supports_mouse != 0, + supportsBracketedPaste: caps.supports_bracketed_paste != 0, + supportsFocusEvents: caps.supports_focus_events != 0, + supportsOsc52: caps.supports_osc52 != 0, + supportsSyncUpdate: caps.supports_sync_update != 0, + supportsScrollRegion: caps.supports_scroll_region != 0, + supportsCursorShape: caps.supports_cursor_shape != 0, + supportsOutputWaitWritable: caps.supports_output_wait_writable != 0, + supportsUnderlineStyles: caps.supports_underline_styles != 0, + supportsColoredUnderlines: caps.supports_colored_underlines != 0, + supportsHyperlinks: caps.supports_hyperlinks != 0, + sgrAttrsSupported: caps.sgr_attrs_supported, } - let o = v.coerce_to_object().map_err(|_| ())?; - return Ok(Some(o)); - } - Ok(None) -} - -fn apply_limits(dst: &mut ffi::zr_limits_t, obj: &JsObject) -> ParseResult<()> { - if let Some(v) = js_u32(obj, "arenaMaxTotalBytes", "arena_max_total_bytes")? { - dst.arena_max_total_bytes = v; - } - if let Some(v) = js_u32(obj, "arenaInitialBytes", "arena_initial_bytes")? { - dst.arena_initial_bytes = v; - } - if let Some(v) = js_u32(obj, "outMaxBytesPerFrame", "out_max_bytes_per_frame")? { - dst.out_max_bytes_per_frame = v; - } - if let Some(v) = js_u32(obj, "dlMaxTotalBytes", "dl_max_total_bytes")? { - dst.dl_max_total_bytes = v; - } - if let Some(v) = js_u32(obj, "dlMaxCmds", "dl_max_cmds")? { - dst.dl_max_cmds = v; - } - if let Some(v) = js_u32(obj, "dlMaxStrings", "dl_max_strings")? { - dst.dl_max_strings = v; - } - if let Some(v) = js_u32(obj, "dlMaxBlobs", "dl_max_blobs")? { - dst.dl_max_blobs = v; - } - if let Some(v) = js_u32(obj, "dlMaxClipDepth", "dl_max_clip_depth")? { - dst.dl_max_clip_depth = v; - } - if let Some(v) = js_u32(obj, "dlMaxTextRunSegments", "dl_max_text_run_segments")? { - dst.dl_max_text_run_segments = v; - } - if let Some(v) = js_u32(obj, "diffMaxDamageRects", "diff_max_damage_rects")? { - dst.diff_max_damage_rects = v; - } - Ok(()) -} - -fn apply_plat(dst: &mut ffi::plat_config_t, obj: &JsObject) -> ParseResult<()> { - if let Some(v) = js_u32(obj, "requestedColorMode", "requested_color_mode")? { - dst.requested_color_mode = (v & 0xFF) as u8; - } - if let Some(v) = js_u8_bool(obj, "enableMouse", "enable_mouse")? { - dst.enable_mouse = v; - } - if let Some(v) = js_u8_bool(obj, "enableBracketedPaste", "enable_bracketed_paste")? { - dst.enable_bracketed_paste = v; - } - if let Some(v) = js_u8_bool(obj, "enableFocusEvents", "enable_focus_events")? { - dst.enable_focus_events = v; - } - if let Some(v) = js_u8_bool(obj, "enableOsc52", "enable_osc52")? { - dst.enable_osc52 = v; - } - dst._pad = [0, 0, 0]; - Ok(()) -} - -fn apply_create_cfg(dst: &mut ffi::zr_engine_config_t, obj: &JsObject) -> ParseResult<()> { - if let Some(v) = js_u32(obj, "requestedEngineAbiMajor", "requested_engine_abi_major")? { - dst.requested_engine_abi_major = v; - } - if let Some(v) = js_u32(obj, "requestedEngineAbiMinor", "requested_engine_abi_minor")? { - dst.requested_engine_abi_minor = v; - } - if let Some(v) = js_u32(obj, "requestedEngineAbiPatch", "requested_engine_abi_patch")? { - dst.requested_engine_abi_patch = v; - } - if let Some(v) = js_u32(obj, "requestedDrawlistVersion", "requested_drawlist_version")? { - dst.requested_drawlist_version = v; - } - if let Some(v) = js_u32(obj, "requestedEventBatchVersion", "requested_event_batch_version")? { - dst.requested_event_batch_version = v; - } - - if let Some(lim) = js_obj(obj, "limits", "limits")? { - apply_limits(&mut dst.limits, &lim)?; - } - if let Some(plat) = js_obj(obj, "plat", "plat")? { - apply_plat(&mut dst.plat, &plat)?; - } - - if let Some(v) = js_u32(obj, "tabWidth", "tab_width")? { - dst.tab_width = v; - } - if let Some(v) = js_u32(obj, "widthPolicy", "width_policy")? { - dst.width_policy = v; - } - if let Some(v) = js_u32(obj, "targetFps", "target_fps")? { - dst.target_fps = v; - } - - if let Some(v) = js_u8_bool(obj, "enableScrollOptimizations", "enable_scroll_optimizations")? { - dst.enable_scroll_optimizations = v; - } - if let Some(v) = js_u8_bool(obj, "enableDebugOverlay", "enable_debug_overlay")? { - dst.enable_debug_overlay = v; - } - if let Some(v) = js_u8_bool(obj, "enableReplayRecording", "enable_replay_recording")? { - dst.enable_replay_recording = v; - } - if let Some(v) = js_u8_bool(obj, "waitForOutputDrain", "wait_for_output_drain")? { - dst.wait_for_output_drain = v; - } - if let Some(v) = js_u32(obj, "capForceFlags", "cap_force_flags")? { - dst.cap_force_flags = v; - } - if let Some(v) = js_u32(obj, "capSuppressFlags", "cap_suppress_flags")? { - dst.cap_suppress_flags = v; - } - Ok(()) -} - -fn create_default_runtime_cfg() -> ffi::zr_engine_runtime_config_t { - let base = unsafe { ffi::zr_engine_config_default() }; - ffi::zr_engine_runtime_config_t { - limits: base.limits, - plat: base.plat, - tab_width: base.tab_width, - width_policy: base.width_policy, - target_fps: base.target_fps, - enable_scroll_optimizations: base.enable_scroll_optimizations, - enable_debug_overlay: base.enable_debug_overlay, - enable_replay_recording: base.enable_replay_recording, - wait_for_output_drain: base.wait_for_output_drain, - cap_force_flags: base.cap_force_flags, - cap_suppress_flags: base.cap_suppress_flags, - } -} - -fn apply_runtime_cfg(dst: &mut ffi::zr_engine_runtime_config_t, obj: &JsObject) -> ParseResult<()> { - if let Some(lim) = js_obj(obj, "limits", "limits")? { - apply_limits(&mut dst.limits, &lim)?; - } - if let Some(plat) = js_obj(obj, "plat", "plat")? { - apply_plat(&mut dst.plat, &plat)?; - } - if let Some(v) = js_u32(obj, "tabWidth", "tab_width")? { - dst.tab_width = v; - } - if let Some(v) = js_u32(obj, "widthPolicy", "width_policy")? { - dst.width_policy = v; - } - if let Some(v) = js_u32(obj, "targetFps", "target_fps")? { - dst.target_fps = v; - } - if let Some(v) = js_u8_bool(obj, "enableScrollOptimizations", "enable_scroll_optimizations")? { - dst.enable_scroll_optimizations = v; - } - if let Some(v) = js_u8_bool(obj, "enableDebugOverlay", "enable_debug_overlay")? { - dst.enable_debug_overlay = v; - } - if let Some(v) = js_u8_bool(obj, "enableReplayRecording", "enable_replay_recording")? { - dst.enable_replay_recording = v; - } - if let Some(v) = js_u8_bool(obj, "waitForOutputDrain", "wait_for_output_drain")? { - dst.wait_for_output_drain = v; - } - if let Some(v) = js_u32(obj, "capForceFlags", "cap_force_flags")? { - dst.cap_force_flags = v; - } - if let Some(v) = js_u32(obj, "capSuppressFlags", "cap_suppress_flags")? { - dst.cap_suppress_flags = v; - } - Ok(()) } #[napi(js_name = "engineCreate")] pub fn engine_create(_env: Env, config: Option) -> napi::Result { - let mut cfg = unsafe { ffi::zr_engine_config_default() }; - if let Some(obj) = config { - apply_create_cfg_strict(&mut cfg, &obj)?; - } - - let mut out_engine: *mut ffi::zr_engine_t = std::ptr::null_mut(); - let rc = unsafe { ffi::engine_create(&mut out_engine as *mut _, &cfg as *const _) }; - if rc != ffi::ZR_OK { - return Ok(rc as i64); - } - if out_engine.is_null() { - return Ok(ffi::ZR_ERR_PLATFORM as i64); - } - - let engine_id = match alloc_engine_id() { - Ok(id) => id, - Err(err) => { - unsafe { ffi::engine_destroy(out_engine) }; - return Ok(err as i64); + let mut cfg = unsafe { ffi::zr_engine_config_default() }; + if let Some(obj) = config { + apply_create_cfg_strict(&mut cfg, &obj)?; } - }; - - let slot = std::sync::Arc::new(EngineSlot { - engine: out_engine, - owner_thread_id: current_thread_id_u64(), - active_calls: AtomicUsize::new(0), - active_calls_mu: Mutex::new(()), - active_calls_cv: Condvar::new(), - destroyed: AtomicBool::new(false), - }); - lock_registry(|map| { - map.insert(engine_id, slot); - }); + let mut out_engine: *mut ffi::zr_engine_t = std::ptr::null_mut(); + let rc = unsafe { ffi::engine_create(&mut out_engine as *mut _, &cfg as *const _) }; + if rc != ffi::ZR_OK { + return Ok(rc as i64); + } + if out_engine.is_null() { + return Ok(ffi::ZR_ERR_PLATFORM as i64); + } - Ok(engine_id as i64) + match register_engine(out_engine) { + Ok(engine_id) => Ok(engine_id as i64), + Err(err) => { + unsafe { ffi::engine_destroy(out_engine) }; + Ok(err as i64) + } + } } #[napi(js_name = "engineDestroy")] pub fn engine_destroy(engine_id: u32) { - if engine_id == 0 { - return; - } - - let slot = lock_registry(|map| { - let slot = match map.get(&engine_id) { - Some(s) => s, - None => return None, + let Some(slot) = take_engine_for_owner(engine_id) else { + return; }; - if slot.owner_thread_id != current_thread_id_u64() { - return None; - } - map.remove(&engine_id) - }); - let Some(slot) = slot else { return; }; - slot.destroyed.store(true, Ordering::Release); - let guard = match slot.active_calls_mu.lock() { - Ok(g) => g, - Err(poison) => poison.into_inner(), - }; - let _guard = match slot - .active_calls_cv - .wait_while(guard, |_| slot.active_calls.load(Ordering::Acquire) != 0) - { - Ok(g) => g, - Err(poison) => poison.into_inner(), - }; - unsafe { ffi::engine_destroy(slot.engine) }; + slot.mark_destroyed(); + slot.wait_for_idle(); + unsafe { ffi::engine_destroy(slot.engine) }; } #[napi(js_name = "engineSubmitDrawlist")] pub fn engine_submit_drawlist(engine_id: u32, drawlist: Uint8Array) -> i32 { - let guard = match get_engine_guard(engine_id) { - Ok(g) => g, - Err(rc) => return rc, - }; - if !guard.slot.is_owner_thread() { - return ffi::ZR_ERR_INVALID_ARGUMENT; - } + let guard = match get_engine_guard(engine_id) { + Ok(guard) => guard, + Err(rc) => return rc, + }; + if !guard.slot.is_owner_thread() { + return ffi::ZR_ERR_INVALID_ARGUMENT; + } - if drawlist.len() > (i32::MAX as usize) { - return ffi::ZR_ERR_LIMIT; - } - let bytes = drawlist.as_ref(); - unsafe { ffi::engine_submit_drawlist(guard.slot.engine, bytes.as_ptr(), bytes.len() as i32) } + if drawlist.len() > (i32::MAX as usize) { + return ffi::ZR_ERR_LIMIT; + } + let bytes = drawlist.as_ref(); + unsafe { ffi::engine_submit_drawlist(guard.slot.engine, bytes.as_ptr(), bytes.len() as i32) } } #[napi(js_name = "enginePresent")] pub fn engine_present(engine_id: u32) -> i32 { - let guard = match get_engine_guard(engine_id) { - Ok(g) => g, - Err(rc) => return rc, - }; - if !guard.slot.is_owner_thread() { - return ffi::ZR_ERR_INVALID_ARGUMENT; - } - unsafe { ffi::engine_present(guard.slot.engine) } -} - -#[napi(js_name = "enginePollEvents")] -pub fn engine_poll_events(engine_id: u32, timeout_ms: i32, mut out: Uint8Array) -> i32 { - let guard = match get_engine_guard(engine_id) { - Ok(g) => g, - Err(rc) => return rc, - }; - if !guard.slot.is_owner_thread() { - return ffi::ZR_ERR_INVALID_ARGUMENT; - } - if timeout_ms < 0 { - return ffi::ZR_ERR_INVALID_ARGUMENT; - } - if out.len() > (i32::MAX as usize) { - return ffi::ZR_ERR_LIMIT; - } - let out_buf = out.as_mut(); - unsafe { - ffi::engine_poll_events( - guard.slot.engine, - timeout_ms, - out_buf.as_mut_ptr(), - out_buf.len() as i32, - ) - } -} - -#[napi(js_name = "enginePostUserEvent")] -pub fn engine_post_user_event(engine_id: u32, tag: u32, payload: Uint8Array) -> i32 { - let guard = match get_engine_guard(engine_id) { - Ok(g) => g, - Err(rc) => return rc, - }; - - if payload.len() > (i32::MAX as usize) { - return ffi::ZR_ERR_LIMIT; - } - let bytes = payload.as_ref(); - let (ptr, len) = if bytes.is_empty() { - (std::ptr::null(), 0) - } else { - (bytes.as_ptr(), bytes.len() as i32) - }; - unsafe { ffi::engine_post_user_event(guard.slot.engine, tag, ptr, len) } -} - -#[napi(js_name = "engineSetConfig")] -pub fn engine_set_config(_env: Env, engine_id: u32, cfg: Option) -> napi::Result { - let guard = match get_engine_guard(engine_id) { - Ok(g) => g, - Err(rc) => return Ok(rc), - }; - if !guard.slot.is_owner_thread() { - return Ok(ffi::ZR_ERR_INVALID_ARGUMENT); - } - - let mut rcfg = create_default_runtime_cfg(); - if let Some(obj) = cfg { - apply_runtime_cfg_strict(&mut rcfg, &obj)?; - } else { - return Ok(ffi::ZR_ERR_INVALID_ARGUMENT); - } - - Ok(unsafe { ffi::engine_set_config(guard.slot.engine, &rcfg as *const _) }) -} - -#[napi(js_name = "engineGetMetrics")] -pub fn engine_get_metrics(engine_id: u32) -> napi::Result { - let guard = get_engine_guard(engine_id).map_err(|_| Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT"))?; - if !guard.slot.is_owner_thread() { - return Err(Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT")); - } - - let mut m = ffi::zr_metrics_t { - struct_size: std::mem::size_of::() as u32, - negotiated_engine_abi_major: 0, - negotiated_engine_abi_minor: 0, - negotiated_engine_abi_patch: 0, - negotiated_drawlist_version: 0, - negotiated_event_batch_version: 0, - frame_index: 0, - fps: 0, - _pad0: 0, - bytes_emitted_total: 0, - bytes_emitted_last_frame: 0, - _pad1: 0, - dirty_lines_last_frame: 0, - dirty_cols_last_frame: 0, - us_input_last_frame: 0, - us_drawlist_last_frame: 0, - us_diff_last_frame: 0, - us_write_last_frame: 0, - events_out_last_poll: 0, - events_dropped_total: 0, - arena_frame_high_water_bytes: 0, - arena_persistent_high_water_bytes: 0, - damage_rects_last_frame: 0, - damage_cells_last_frame: 0, - damage_full_frame: 0, - _pad2: [0, 0, 0], - }; - - let rc = unsafe { ffi::engine_get_metrics(guard.slot.engine, &mut m as *mut _) }; - if rc != ffi::ZR_OK { - return Err(Error::new(Status::GenericFailure, format!("engine_get_metrics failed: {rc}"))); - } - - Ok(EngineMetrics { - structSize: m.struct_size, - negotiatedEngineAbiMajor: m.negotiated_engine_abi_major, - negotiatedEngineAbiMinor: m.negotiated_engine_abi_minor, - negotiatedEngineAbiPatch: m.negotiated_engine_abi_patch, - negotiatedDrawlistVersion: m.negotiated_drawlist_version, - negotiatedEventBatchVersion: m.negotiated_event_batch_version, - frameIndex: BigInt { - sign_bit: false, - words: vec![m.frame_index], - }, - fps: m.fps, - bytesEmittedTotal: BigInt { - sign_bit: false, - words: vec![m.bytes_emitted_total], - }, - bytesEmittedLastFrame: m.bytes_emitted_last_frame, - dirtyLinesLastFrame: m.dirty_lines_last_frame, - dirtyColsLastFrame: m.dirty_cols_last_frame, - usInputLastFrame: m.us_input_last_frame, - usDrawlistLastFrame: m.us_drawlist_last_frame, - usDiffLastFrame: m.us_diff_last_frame, - usWriteLastFrame: m.us_write_last_frame, - eventsOutLastPoll: m.events_out_last_poll, - eventsDroppedTotal: m.events_dropped_total, - arenaFrameHighWaterBytes: BigInt { - sign_bit: false, - words: vec![m.arena_frame_high_water_bytes], - }, - arenaPersistentHighWaterBytes: BigInt { - sign_bit: false, - words: vec![m.arena_persistent_high_water_bytes], - }, - damageRectsLastFrame: m.damage_rects_last_frame, - damageCellsLastFrame: m.damage_cells_last_frame, - damageFullFrame: m.damage_full_frame != 0, - }) -} - -#[napi(js_name = "engineGetCaps")] -pub fn engine_get_caps(engine_id: u32) -> napi::Result { - let guard = get_engine_guard(engine_id).map_err(|_| Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT"))?; - if !guard.slot.is_owner_thread() { - return Err(Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT")); - } - - let mut caps = ffi::zr_terminal_caps_t { - color_mode: 0, - supports_mouse: 0, - supports_bracketed_paste: 0, - supports_focus_events: 0, - supports_osc52: 0, - supports_sync_update: 0, - supports_scroll_region: 0, - supports_cursor_shape: 0, - supports_output_wait_writable: 0, - supports_underline_styles: 0, - supports_colored_underlines: 0, - supports_hyperlinks: 0, - sgr_attrs_supported: 0, - terminal_id: 0, - _pad1: [0, 0, 0], - cap_flags: 0, - cap_force_flags: 0, - cap_suppress_flags: 0, - }; - - let rc = unsafe { ffi::engine_get_caps(guard.slot.engine, &mut caps as *mut _) }; - if rc != ffi::ZR_OK { - return Err(Error::new(Status::GenericFailure, format!("engine_get_caps failed: {rc}"))); - } - - Ok(TerminalCaps { - colorMode: caps.color_mode as u32, - supportsMouse: caps.supports_mouse != 0, - supportsBracketedPaste: caps.supports_bracketed_paste != 0, - supportsFocusEvents: caps.supports_focus_events != 0, - supportsOsc52: caps.supports_osc52 != 0, - supportsSyncUpdate: caps.supports_sync_update != 0, - supportsScrollRegion: caps.supports_scroll_region != 0, - supportsCursorShape: caps.supports_cursor_shape != 0, - supportsOutputWaitWritable: caps.supports_output_wait_writable != 0, - supportsUnderlineStyles: caps.supports_underline_styles != 0, - supportsColoredUnderlines: caps.supports_colored_underlines != 0, - supportsHyperlinks: caps.supports_hyperlinks != 0, - sgrAttrsSupported: caps.sgr_attrs_supported, - }) -} - -// ============================================================================= -// Debug Trace API -// ============================================================================= - -#[napi(object)] -#[allow(non_snake_case)] -pub struct DebugStats { - pub totalRecords: BigInt, - pub totalDropped: BigInt, - pub errorCount: u32, - pub warnCount: u32, - pub currentRingUsage: u32, - pub ringCapacity: u32, -} - -#[napi(object)] -#[allow(non_snake_case)] -pub struct DebugQueryResult { - pub recordsReturned: u32, - pub recordsAvailable: u32, - pub oldestRecordId: BigInt, - pub newestRecordId: BigInt, - pub recordsDropped: u32, -} - -const DEBUG_CFG_KEYS: &[(&str, &str)] = &[ - ("enabled", "enabled"), - ("ringCapacity", "ring_capacity"), - ("minSeverity", "min_severity"), - ("categoryMask", "category_mask"), - ("captureRawEvents", "capture_raw_events"), - ("captureDrawlistBytes", "capture_drawlist_bytes"), -]; - -const DEBUG_QUERY_KEYS: &[(&str, &str)] = &[ - ("minRecordId", "min_record_id"), - ("maxRecordId", "max_record_id"), - ("minFrameId", "min_frame_id"), - ("maxFrameId", "max_frame_id"), - ("categoryMask", "category_mask"), - ("minSeverity", "min_severity"), - ("maxRecords", "max_records"), -]; - -fn parse_debug_query_bigint_u64(sign_bit: bool, words: &[u64]) -> ParseResult { - // Reject negative values while still allowing canonical zero. - if sign_bit && words.iter().any(|w| *w != 0) { - return Err(()); - } - match words { - [] => Ok(0), - [value] => Ok(*value), - _ => Err(()), // More than 64 bits. - } -} - -fn js_u64(obj: &JsObject, primary: &str, alias: &str) -> ParseResult> { - for name in [primary, alias] { - let v = match obj.get_named_property::(name) { - Ok(v) => v, - Err(_) => continue, + let guard = match get_engine_guard(engine_id) { + Ok(guard) => guard, + Err(rc) => return rc, }; - match v.get_type().map_err(|_| ())? { - ValueType::Undefined => continue, - ValueType::BigInt => { - let mut bi = unsafe { v.cast::() }; - let (sign_bit, words) = bi.get_words().map_err(|_| ())?; - let val = parse_debug_query_bigint_u64(sign_bit, &words)?; - return Ok(Some(val)); - } - ValueType::Number => { - let n = v.coerce_to_number().map_err(|_| ())?; - let f = n.get_double().map_err(|_| ())?; - if !f.is_finite() || f < 0.0 || f > (u64::MAX as f64) { - return Err(()); - } - return Ok(Some(f as u64)); - } - _ => return Err(()), + if !guard.slot.is_owner_thread() { + return ffi::ZR_ERR_INVALID_ARGUMENT; } - } - Ok(None) -} - -fn apply_debug_cfg(dst: &mut ffi::zr_debug_config_t, obj: &JsObject) -> ParseResult<()> { - if let Some(v) = js_u8_bool(obj, "enabled", "enabled")? { - dst.enabled = v as u32; - } - if let Some(v) = js_u32(obj, "ringCapacity", "ring_capacity")? { - dst.ring_capacity = v; - } - if let Some(v) = js_u32(obj, "minSeverity", "min_severity")? { - dst.min_severity = v; - } - if let Some(v) = js_u32(obj, "categoryMask", "category_mask")? { - dst.category_mask = v; - } - if let Some(v) = js_u8_bool(obj, "captureRawEvents", "capture_raw_events")? { - dst.capture_raw_events = v as u32; - } - if let Some(v) = js_u8_bool(obj, "captureDrawlistBytes", "capture_drawlist_bytes")? { - dst.capture_drawlist_bytes = v as u32; - } - Ok(()) -} - -fn apply_debug_query(dst: &mut ffi::zr_debug_query_t, obj: &JsObject) -> ParseResult<()> { - if let Some(v) = js_u64(obj, "minRecordId", "min_record_id")? { - dst.min_record_id = v; - } - if let Some(v) = js_u64(obj, "maxRecordId", "max_record_id")? { - dst.max_record_id = v; - } - if let Some(v) = js_u64(obj, "minFrameId", "min_frame_id")? { - dst.min_frame_id = v; - } - if let Some(v) = js_u64(obj, "maxFrameId", "max_frame_id")? { - dst.max_frame_id = v; - } - if let Some(v) = js_u32(obj, "categoryMask", "category_mask")? { - dst.category_mask = v; - } - if let Some(v) = js_u32(obj, "minSeverity", "min_severity")? { - dst.min_severity = v; - } - if let Some(v) = js_u32(obj, "maxRecords", "max_records")? { - dst.max_records = v; - } - Ok(()) -} - -#[napi(js_name = "engineDebugEnable")] -pub fn engine_debug_enable(_env: Env, engine_id: u32, config: Option) -> napi::Result { - let guard = match get_engine_guard(engine_id) { - Ok(g) => g, - Err(rc) => return Ok(rc), - }; - if !guard.slot.is_owner_thread() { - return Ok(ffi::ZR_ERR_INVALID_ARGUMENT); - } - - let mut cfg = ffi::zr_debug_config_t { - enabled: 1, - ring_capacity: 0, - min_severity: 0, - category_mask: 0xFFFFFFFF, // All categories - capture_raw_events: 0, - capture_drawlist_bytes: 0, - _pad0: 0, - _pad1: 0, - }; - - if let Some(obj) = config { - validate_known_keys(&obj, DEBUG_CFG_KEYS, "engineDebugEnable config")?; - apply_debug_cfg(&mut cfg, &obj).map_err(|_| Error::new(Status::InvalidArg, "engineDebugEnable: invalid config value"))?; - } - - Ok(unsafe { ffi::engine_debug_enable(guard.slot.engine, &cfg as *const _) }) -} - -#[napi(js_name = "engineDebugDisable")] -pub fn engine_debug_disable(engine_id: u32) -> i32 { - let guard = match get_engine_guard(engine_id) { - Ok(g) => g, - Err(rc) => return rc, - }; - if !guard.slot.is_owner_thread() { - return ffi::ZR_ERR_INVALID_ARGUMENT; - } - unsafe { ffi::engine_debug_disable(guard.slot.engine) }; - ffi::ZR_OK + unsafe { ffi::engine_present(guard.slot.engine) } } -#[napi(js_name = "engineDebugQuery")] -pub fn engine_debug_query( - _env: Env, - engine_id: u32, - query: Option, - mut out_headers: Uint8Array, -) -> napi::Result { - let guard = get_engine_guard(engine_id).map_err(|_| Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT"))?; - if !guard.slot.is_owner_thread() { - return Err(Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT")); - } - - let mut q = ffi::zr_debug_query_t { - min_record_id: 0, - max_record_id: 0, - min_frame_id: 0, - max_frame_id: 0, - category_mask: 0xFFFFFFFF, - min_severity: 0, - max_records: 0, - _pad0: 0, - }; - - if let Some(obj) = query { - validate_known_keys(&obj, DEBUG_QUERY_KEYS, "engineDebugQuery query")?; - apply_debug_query(&mut q, &obj).map_err(|_| Error::new(Status::InvalidArg, "engineDebugQuery: invalid query value"))?; - } - - let mut result = ffi::zr_debug_query_result_t { - records_returned: 0, - records_available: 0, - oldest_record_id: 0, - newest_record_id: 0, - records_dropped: 0, - _pad0: 0, - }; - - let out_headers_slice = out_headers.as_mut(); - let header_size = std::mem::size_of::(); - let header_align = std::mem::align_of::(); - let headers_cap = (out_headers_slice.len() / header_size) as u32; - - let headers_ptr: *mut ffi::zr_debug_record_header_t = if headers_cap == 0 { - std::ptr::null_mut() - } else { - let raw = out_headers_slice.as_mut_ptr(); - if (raw as usize) % header_align != 0 { - return Err(Error::new( - Status::InvalidArg, - "engineDebugQuery: outHeaders must be aligned for debug record headers", - )); +#[napi(js_name = "enginePollEvents")] +pub fn engine_poll_events(engine_id: u32, timeout_ms: i32, mut out: Uint8Array) -> i32 { + let guard = match get_engine_guard(engine_id) { + Ok(guard) => guard, + Err(rc) => return rc, + }; + if !guard.slot.is_owner_thread() { + return ffi::ZR_ERR_INVALID_ARGUMENT; } - raw as *mut ffi::zr_debug_record_header_t - }; - - let rc = unsafe { - ffi::engine_debug_query( - guard.slot.engine, - &q as *const _, - headers_ptr, - headers_cap, - &mut result as *mut _, - ) - }; - - if rc != ffi::ZR_OK { - return Err(Error::new(Status::GenericFailure, format!("engine_debug_query failed: {rc}"))); - } - - Ok(DebugQueryResult { - recordsReturned: result.records_returned, - recordsAvailable: result.records_available, - oldestRecordId: BigInt { sign_bit: false, words: vec![result.oldest_record_id] }, - newestRecordId: BigInt { sign_bit: false, words: vec![result.newest_record_id] }, - recordsDropped: result.records_dropped, - }) -} - -#[napi(js_name = "engineDebugGetPayload")] -pub fn engine_debug_get_payload( - engine_id: u32, - record_id: BigInt, - mut out_payload: Uint8Array, -) -> napi::Result { - let guard = get_engine_guard(engine_id).map_err(|_| Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT"))?; - if !guard.slot.is_owner_thread() { - return Err(Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT")); - } - - let rid = parse_debug_query_bigint_u64(record_id.sign_bit, &record_id.words).map_err(|_| { - Error::new( - Status::InvalidArg, - "engineDebugGetPayload: recordId must be a non-negative u64", - ) - })?; - - let mut out_size: u32 = 0; - let out_cap = out_payload.len() as u32; - let out_ptr = out_payload.as_mut().as_mut_ptr(); - - let rc = unsafe { - ffi::engine_debug_get_payload( - guard.slot.engine, - rid, - out_ptr, - out_cap, - &mut out_size as *mut _, - ) - }; - - if rc != ffi::ZR_OK { - return Ok(rc); - } - - Ok(out_size as i32) -} - -#[napi(js_name = "engineDebugGetStats")] -pub fn engine_debug_get_stats(engine_id: u32) -> napi::Result { - let guard = get_engine_guard(engine_id).map_err(|_| Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT"))?; - if !guard.slot.is_owner_thread() { - return Err(Error::new(Status::InvalidArg, "ZR_ERR_INVALID_ARGUMENT")); - } - - let mut stats = ffi::zr_debug_stats_t { - total_records: 0, - total_dropped: 0, - error_count: 0, - warn_count: 0, - current_ring_usage: 0, - ring_capacity: 0, - }; - - let rc = unsafe { ffi::engine_debug_get_stats(guard.slot.engine, &mut stats as *mut _) }; - if rc != ffi::ZR_OK { - return Err(Error::new(Status::GenericFailure, format!("engine_debug_get_stats failed: {rc}"))); - } - - Ok(DebugStats { - totalRecords: BigInt { sign_bit: false, words: vec![stats.total_records] }, - totalDropped: BigInt { sign_bit: false, words: vec![stats.total_dropped] }, - errorCount: stats.error_count, - warnCount: stats.warn_count, - currentRingUsage: stats.current_ring_usage, - ringCapacity: stats.ring_capacity, - }) -} - -#[napi(js_name = "engineDebugExport")] -pub fn engine_debug_export(engine_id: u32, mut out_buf: Uint8Array) -> i32 { - let guard = match get_engine_guard(engine_id) { - Ok(g) => g, - Err(rc) => return rc, - }; - if !guard.slot.is_owner_thread() { - return ffi::ZR_ERR_INVALID_ARGUMENT; - } - - let out_cap = out_buf.len(); - let out_ptr = out_buf.as_mut().as_mut_ptr(); - - unsafe { ffi::engine_debug_export(guard.slot.engine, out_ptr, out_cap) } -} - -#[napi(js_name = "engineDebugReset")] -pub fn engine_debug_reset(engine_id: u32) -> i32 { - let guard = match get_engine_guard(engine_id) { - Ok(g) => g, - Err(rc) => return rc, - }; - if !guard.slot.is_owner_thread() { - return ffi::ZR_ERR_INVALID_ARGUMENT; - } - - unsafe { ffi::engine_debug_reset(guard.slot.engine) }; - ffi::ZR_OK -} - -#[cfg(test)] -mod tests { - use super::{ffi, parse_debug_query_bigint_u64}; - - const ATTR_BOLD: u32 = 1 << 0; - const ATTR_UNDERLINE: u32 = 1 << 2; - const ATTR_DIM: u32 = 1 << 4; - - fn contains_subsequence(haystack: &[u8], needle: &[u8]) -> bool { - if needle.is_empty() { - return true; + if timeout_ms < 0 { + return ffi::ZR_ERR_INVALID_ARGUMENT; } - haystack.windows(needle.len()).any(|w| w == needle) - } - - fn style_with_attrs(attrs: u32) -> ffi::zr_style_t { - ffi::zr_style_t { - fg_rgb: 0, - bg_rgb: 0, - attrs, - reserved: 0, - underline_rgb: 0, - link_ref: 0, + if out.len() > (i32::MAX as usize) { + return ffi::ZR_ERR_LIMIT; } - } - fn style_plain() -> ffi::zr_style_t { - ffi::zr_style_t { - fg_rgb: 0, - bg_rgb: 0, - attrs: 0, - reserved: 0, - underline_rgb: 0, - link_ref: 0, + let out_buf = out.as_mut(); + unsafe { + ffi::engine_poll_events( + guard.slot.engine, + timeout_ms, + out_buf.as_mut_ptr(), + out_buf.len() as i32, + ) } - } - - struct SingleCellFramebuffer { - raw: ffi::zr_fb_t, - } - - impl SingleCellFramebuffer { - fn with_attrs(attrs: u32) -> Self { - let mut raw = ffi::zr_fb_t { - cols: 0, - rows: 0, - cells: std::ptr::null_mut(), - links: std::ptr::null_mut(), - links_len: 0, - links_cap: 0, - link_bytes: std::ptr::null_mut(), - link_bytes_len: 0, - link_bytes_cap: 0, - }; - - let rc = unsafe { ffi::zr_fb_init(&mut raw as *mut _, 1, 1) }; - assert_eq!(rc, ffi::ZR_OK, "zr_fb_init must succeed for test framebuffer"); - - let cell = unsafe { ffi::zr_fb_cell(&mut raw as *mut _, 0, 0) }; - assert!(!cell.is_null(), "zr_fb_cell(0,0) must return a valid pointer"); - unsafe { - (*cell).glyph = [0; 32]; - (*cell).glyph[0] = b'X'; - (*cell).glyph_len = 1; - (*cell).width = 1; - (*cell)._pad0 = 0; - (*cell).style = style_with_attrs(attrs); - } +} - Self { raw } - } - } +#[napi(js_name = "enginePostUserEvent")] +pub fn engine_post_user_event(engine_id: u32, tag: u32, payload: Uint8Array) -> i32 { + let guard = match get_engine_guard(engine_id) { + Ok(guard) => guard, + Err(rc) => return rc, + }; - impl Drop for SingleCellFramebuffer { - fn drop(&mut self) { - unsafe { ffi::zr_fb_release(&mut self.raw as *mut _) }; + if payload.len() > (i32::MAX as usize) { + return ffi::ZR_ERR_LIMIT; } - } + let bytes = payload.as_ref(); + let (ptr, len) = if bytes.is_empty() { + (std::ptr::null(), 0) + } else { + (bytes.as_ptr(), bytes.len() as i32) + }; - struct TestFramebuffer { - raw: ffi::zr_fb_t, - } + unsafe { ffi::engine_post_user_event(guard.slot.engine, tag, ptr, len) } +} - impl TestFramebuffer { - fn new(cols: u32, rows: u32) -> Self { - let mut raw = ffi::zr_fb_t { - cols: 0, - rows: 0, - cells: std::ptr::null_mut(), - links: std::ptr::null_mut(), - links_len: 0, - links_cap: 0, - link_bytes: std::ptr::null_mut(), - link_bytes_len: 0, - link_bytes_cap: 0, - }; - let rc = unsafe { ffi::zr_fb_init(&mut raw as *mut _, cols, rows) }; - assert_eq!(rc, ffi::ZR_OK, "zr_fb_init must succeed for test framebuffer"); - let rc_clear = unsafe { ffi::zr_fb_clear(&mut raw as *mut _, &style_plain() as *const _) }; - assert_eq!(rc_clear, ffi::ZR_OK, "zr_fb_clear must succeed for test framebuffer"); - Self { raw } +#[napi(js_name = "engineSetConfig")] +pub fn engine_set_config(_env: Env, engine_id: u32, cfg: Option) -> napi::Result { + let guard = match get_engine_guard(engine_id) { + Ok(guard) => guard, + Err(rc) => return Ok(rc), + }; + if !guard.slot.is_owner_thread() { + return Ok(ffi::ZR_ERR_INVALID_ARGUMENT); } - fn set_cell(&mut self, x: u32, y: u32, glyph: &[u8], width: u8, style: ffi::zr_style_t) { - assert!( - glyph.len() <= 32, - "glyph length must fit ZR_CELL_GLYPH_MAX (got {})", - glyph.len() - ); - let cell = unsafe { ffi::zr_fb_cell(&mut self.raw as *mut _, x, y) }; - assert!(!cell.is_null(), "zr_fb_cell({x},{y}) must return a valid pointer"); - unsafe { - (*cell).glyph = [0; 32]; - for (i, b) in glyph.iter().copied().enumerate() { - (*cell).glyph[i] = b; - } - (*cell).glyph_len = glyph.len() as u8; - (*cell).width = width; - (*cell)._pad0 = 0; - (*cell).style = style; - } + let mut runtime_cfg = create_default_runtime_cfg(); + if let Some(obj) = cfg { + apply_runtime_cfg_strict(&mut runtime_cfg, &obj)?; + } else { + return Ok(ffi::ZR_ERR_INVALID_ARGUMENT); } - fn set_cell_link_ref(&mut self, x: u32, y: u32, link_ref: u32) { - let cell = unsafe { ffi::zr_fb_cell(&mut self.raw as *mut _, x, y) }; - assert!(!cell.is_null(), "zr_fb_cell({x},{y}) must return a valid pointer"); - unsafe { - (*cell).style.link_ref = link_ref; - } - } + Ok(unsafe { ffi::engine_set_config(guard.slot.engine, &runtime_cfg as *const _) }) +} - fn cell_link_ref(&mut self, x: u32, y: u32) -> u32 { - let cell = unsafe { ffi::zr_fb_cell(&mut self.raw as *mut _, x, y) }; - assert!(!cell.is_null(), "zr_fb_cell({x},{y}) must return a valid pointer"); - unsafe { (*cell).style.link_ref } +#[napi(js_name = "engineGetMetrics")] +pub fn engine_get_metrics(engine_id: u32) -> napi::Result { + let guard = get_engine_guard(engine_id).map_err(|_| invalid_arg_error())?; + if !guard.slot.is_owner_thread() { + return Err(invalid_arg_error()); } - } - impl Drop for TestFramebuffer { - fn drop(&mut self) { - unsafe { ffi::zr_fb_release(&mut self.raw as *mut _) }; + let mut metrics = empty_metrics(); + let rc = unsafe { ffi::engine_get_metrics(guard.slot.engine, &mut metrics as *mut _) }; + if rc != ffi::ZR_OK { + return Err(Error::new( + Status::GenericFailure, + format!("engine_get_metrics failed: {rc}"), + )); } - } - - fn render_diff_bytes( - prev: &ffi::zr_fb_t, - next: &ffi::zr_fb_t, - initial_style: ffi::zr_style_t, - ) -> Vec { - let caps = ffi::plat_caps_t { - color_mode: 3, - supports_mouse: 0, - supports_bracketed_paste: 0, - supports_focus_events: 0, - supports_osc52: 0, - supports_sync_update: 0, - supports_scroll_region: 0, - supports_cursor_shape: 1, - supports_output_wait_writable: 0, - supports_underline_styles: 0, - supports_colored_underlines: 0, - supports_hyperlinks: 0, - sgr_attrs_supported: u32::MAX, - }; - let limits = unsafe { ffi::zr_engine_config_default() }.limits; - let initial_term_state = ffi::zr_term_state_t { - cursor_x: 0, - cursor_y: 0, - cursor_visible: 1, - cursor_shape: 0, - cursor_blink: 0, - flags: 0, - style: initial_style, - }; - let desired_cursor_state = ffi::zr_cursor_state_t { - x: -1, - y: -1, - shape: 0, - visible: 1, - blink: 0, - reserved0: 0, - }; - - let mut scratch_damage_rects = vec![ - ffi::zr_damage_rect_t { - x0: 0, - y0: 0, - x1: 0, - y1: 0, - }; - limits.diff_max_damage_rects as usize - ]; - let mut out = [0u8; 1024]; - let mut out_len = 0usize; - let mut out_final_term_state: ffi::zr_term_state_t = unsafe { std::mem::zeroed() }; - let mut out_stats: ffi::zr_diff_stats_t = unsafe { std::mem::zeroed() }; - - let rc = unsafe { - ffi::zr_diff_render( - prev as *const _, - next as *const _, - &caps as *const _, - &initial_term_state as *const _, - &desired_cursor_state as *const _, - &limits as *const _, - scratch_damage_rects.as_mut_ptr(), - scratch_damage_rects.len() as u32, - 0, - out.as_mut_ptr(), - out.len(), - &mut out_len as *mut _, - &mut out_final_term_state as *mut _, - &mut out_stats as *mut _, - ) - }; - assert_eq!(rc, ffi::ZR_OK, "zr_diff_render must succeed"); - assert!(out_len > 0, "zr_diff_render must emit output"); - out[..out_len].to_vec() - } - - fn render_style_transition(current_attrs: u32, desired_attrs: u32) -> Vec { - let prev = SingleCellFramebuffer::with_attrs(current_attrs); - let next = SingleCellFramebuffer::with_attrs(desired_attrs); - render_diff_bytes(&prev.raw, &next.raw, style_with_attrs(current_attrs)) - } - - fn cell_snapshot(fb: &mut ffi::zr_fb_t, x: u32, y: u32) -> (u8, u8) { - let cell = unsafe { ffi::zr_fb_cell(fb as *mut _, x, y) }; - assert!(!cell.is_null(), "cell must exist at ({x},{y})"); - unsafe { ((*cell).glyph[0], (*cell).width) } - } - - #[test] - fn fb_links_clone_from_failure_has_no_partial_effects() { - let mut dst = TestFramebuffer::new(2, 1); - let uri = b"https://example.test/rezi"; - let mut link_ref = 0u32; - let intern_rc = unsafe { - ffi::zr_fb_link_intern( - &mut dst.raw as *mut _, - uri.as_ptr(), - uri.len(), - std::ptr::null(), - 0, - &mut link_ref as *mut _, - ) - }; - assert_eq!(intern_rc, ffi::ZR_OK, "zr_fb_link_intern must seed destination link state"); - assert_eq!(link_ref, 1u32); - - let before_links_ptr = dst.raw.links; - let before_links_len = dst.raw.links_len; - let before_links_cap = dst.raw.links_cap; - let before_link_bytes_ptr = dst.raw.link_bytes; - let before_link_bytes_len = dst.raw.link_bytes_len; - let before_link_bytes_cap = dst.raw.link_bytes_cap; - assert!(!before_links_ptr.is_null(), "seeded links pointer must be non-null"); - assert!(!before_link_bytes_ptr.is_null(), "seeded link-bytes pointer must be non-null"); - - let before_first_link = unsafe { *before_links_ptr }; - let before_link_bytes = - unsafe { std::slice::from_raw_parts(before_link_bytes_ptr, before_link_bytes_len as usize).to_vec() }; - - let invalid_src = ffi::zr_fb_t { - cols: dst.raw.cols, - rows: dst.raw.rows, - cells: dst.raw.cells, - links: std::ptr::null_mut(), - links_len: 1, - links_cap: 0, - link_bytes: std::ptr::null_mut(), - link_bytes_len: before_link_bytes_len, - link_bytes_cap: 0, - }; - let clone_rc = unsafe { ffi::zr_fb_links_clone_from(&mut dst.raw as *mut _, &invalid_src as *const _) }; - assert_eq!(clone_rc, ffi::ZR_ERR_INVALID_ARGUMENT); - - assert_eq!(dst.raw.links, before_links_ptr); - assert_eq!(dst.raw.links_len, before_links_len); - assert_eq!(dst.raw.links_cap, before_links_cap); - assert_eq!(dst.raw.link_bytes, before_link_bytes_ptr); - assert_eq!(dst.raw.link_bytes_len, before_link_bytes_len); - assert_eq!(dst.raw.link_bytes_cap, before_link_bytes_cap); - - let after_first_link = unsafe { *dst.raw.links }; - assert_eq!(after_first_link.uri_off, before_first_link.uri_off); - assert_eq!(after_first_link.uri_len, before_first_link.uri_len); - assert_eq!(after_first_link.id_off, before_first_link.id_off); - assert_eq!(after_first_link.id_len, before_first_link.id_len); - - let after_link_bytes = - unsafe { std::slice::from_raw_parts(dst.raw.link_bytes, dst.raw.link_bytes_len as usize) }; - assert_eq!(after_link_bytes, before_link_bytes.as_slice()); - } - #[test] - fn fb_link_intern_compacts_stale_refs_and_bounds_growth() { - const LINK_ENTRY_MAX_BYTES: u32 = 2083 + 2083; - let mut fb = TestFramebuffer::new(2, 1); - let persistent_uri = b"https://example.test/persistent"; - - let mut persistent_ref = 0u32; - let seed_rc = unsafe { - ffi::zr_fb_link_intern( - &mut fb.raw as *mut _, - persistent_uri.as_ptr(), - persistent_uri.len(), - std::ptr::null(), - 0, - &mut persistent_ref as *mut _, - ) - }; - assert_eq!(seed_rc, ffi::ZR_OK); - assert_ne!(persistent_ref, 0); - fb.set_cell_link_ref(0, 0, persistent_ref); - - let mut peak_links_len = fb.raw.links_len; - let mut peak_link_bytes_len = fb.raw.link_bytes_len; - - for i in 0..64u32 { - let uri = format!("https://example.test/ephemeral/{i}"); - let mut ref_i = 0u32; - let rc = unsafe { - ffi::zr_fb_link_intern( - &mut fb.raw as *mut _, - uri.as_ptr(), - uri.len(), - std::ptr::null(), - 0, - &mut ref_i as *mut _, - ) - }; - assert_eq!(rc, ffi::ZR_OK, "zr_fb_link_intern failed at iteration {i}"); - assert!(ref_i >= 1 && ref_i <= fb.raw.links_len); - - fb.set_cell_link_ref(1, 0, ref_i); - - let live_ref0 = fb.cell_link_ref(0, 0); - let live_ref1 = fb.cell_link_ref(1, 0); - assert!(live_ref0 >= 1 && live_ref0 <= fb.raw.links_len, "cell(0,0) link_ref must remain valid"); - assert!(live_ref1 >= 1 && live_ref1 <= fb.raw.links_len, "cell(1,0) link_ref must remain valid"); + Ok(metrics_to_js(metrics)) +} - peak_links_len = peak_links_len.max(fb.raw.links_len); - peak_link_bytes_len = peak_link_bytes_len.max(fb.raw.link_bytes_len); +#[napi(js_name = "engineGetCaps")] +pub fn engine_get_caps(engine_id: u32) -> napi::Result { + let guard = get_engine_guard(engine_id).map_err(|_| invalid_arg_error())?; + if !guard.slot.is_owner_thread() { + return Err(invalid_arg_error()); } - assert!( - peak_links_len <= 5, - "link table must stay bounded for 2-cell framebuffer (peak={peak_links_len})", - ); - assert!( - peak_link_bytes_len <= 5 * LINK_ENTRY_MAX_BYTES, - "link byte arena must stay bounded for 2-cell framebuffer (peak={peak_link_bytes_len})", - ); - - let mut uri_ptr: *const u8 = std::ptr::null(); - let mut uri_len: usize = 0; - let mut id_ptr: *const u8 = std::ptr::null(); - let mut id_len: usize = 0; - let persistent_cell_ref = fb.cell_link_ref(0, 0); - let lookup_rc = unsafe { - ffi::zr_fb_link_lookup( - &fb.raw as *const _, - persistent_cell_ref, - &mut uri_ptr as *mut _, - &mut uri_len as *mut _, - &mut id_ptr as *mut _, - &mut id_len as *mut _, - ) - }; - assert_eq!(lookup_rc, ffi::ZR_OK); - assert_eq!(id_len, 0); - assert!(id_ptr.is_null()); - assert!(!uri_ptr.is_null()); - - let resolved_uri = unsafe { std::slice::from_raw_parts(uri_ptr, uri_len) }; - assert_eq!(resolved_uri, persistent_uri); - } - - #[test] - fn ffi_layout_matches_vendored_headers() { - use std::mem::{align_of, size_of}; - use std::ptr::addr_of; - - assert_eq!(size_of::(), 24); - assert_eq!(align_of::(), 4); - assert_eq!(size_of::(), 60); - assert_eq!(size_of::(), 36); - assert_eq!(size_of::(), 16); - assert_eq!(align_of::(), 4); - - let caps = std::mem::MaybeUninit::::uninit(); - let base = caps.as_ptr(); - unsafe { - assert_eq!(addr_of!((*base).color_mode) as usize - base as usize, 0); - assert_eq!(addr_of!((*base).supports_output_wait_writable) as usize - base as usize, 8); - assert_eq!(addr_of!((*base).supports_underline_styles) as usize - base as usize, 9); - assert_eq!(addr_of!((*base).supports_colored_underlines) as usize - base as usize, 10); - assert_eq!(addr_of!((*base).supports_hyperlinks) as usize - base as usize, 11); - assert_eq!(addr_of!((*base).sgr_attrs_supported) as usize - base as usize, 12); - } - - if cfg!(target_pointer_width = "64") { - assert_eq!(size_of::(), 48); - assert_eq!(align_of::(), 8); - } else if cfg!(target_pointer_width = "32") { - assert_eq!(size_of::(), 36); - assert_eq!(align_of::(), 4); + let mut caps = empty_terminal_caps(); + let rc = unsafe { ffi::engine_get_caps(guard.slot.engine, &mut caps as *mut _) }; + if rc != ffi::ZR_OK { + return Err(Error::new( + Status::GenericFailure, + format!("engine_get_caps failed: {rc}"), + )); } - } - - #[test] - fn clip_edge_write_over_continuation_cleans_lead_pair() { - let mut fb = ffi::zr_fb_t { - cols: 0, - rows: 0, - cells: std::ptr::null_mut(), - links: std::ptr::null_mut(), - links_len: 0, - links_cap: 0, - link_bytes: std::ptr::null_mut(), - link_bytes_len: 0, - link_bytes_cap: 0, - }; - let init_rc = unsafe { ffi::zr_fb_init(&mut fb as *mut _, 4, 1) }; - assert_eq!(init_rc, ffi::ZR_OK); - - let clear_rc = unsafe { ffi::zr_fb_clear(&mut fb as *mut _, &style_plain() as *const _) }; - assert_eq!(clear_rc, ffi::ZR_OK); - - let mut clip_stack = [ - ffi::zr_rect_t { - x: 0, - y: 0, - w: 4, - h: 1, - }, - ffi::zr_rect_t { - x: 0, - y: 0, - w: 0, - h: 0, - }, - ]; - let mut painter = ffi::zr_fb_painter_t { - fb: std::ptr::null_mut(), - clip_stack: std::ptr::null_mut(), - clip_cap: 0, - clip_len: 0, - }; - let begin_rc = unsafe { - ffi::zr_fb_painter_begin( - &mut painter as *mut _, - &mut fb as *mut _, - clip_stack.as_mut_ptr(), - clip_stack.len() as u32, - ) - }; - assert_eq!(begin_rc, ffi::ZR_OK); - - let wide_bytes = b"W"; - let write_wide_rc = unsafe { - ffi::zr_fb_put_grapheme( - &mut painter as *mut _, - 1, - 0, - wide_bytes.as_ptr(), - wide_bytes.len(), - 2, - &style_plain() as *const _, - ) - }; - assert_eq!(write_wide_rc, ffi::ZR_OK); - - let push_rc = unsafe { - ffi::zr_fb_clip_push( - &mut painter as *mut _, - ffi::zr_rect_t { - x: 2, - y: 0, - w: 1, - h: 1, - }, - ) - }; - assert_eq!(push_rc, ffi::ZR_OK); - - let a_bytes = b"A"; - let write_a_rc = unsafe { - ffi::zr_fb_put_grapheme( - &mut painter as *mut _, - 2, - 0, - a_bytes.as_ptr(), - a_bytes.len(), - 1, - &style_plain() as *const _, - ) - }; - assert_eq!(write_a_rc, ffi::ZR_OK); - - let pop_rc = unsafe { ffi::zr_fb_clip_pop(&mut painter as *mut _) }; - assert_eq!(pop_rc, ffi::ZR_OK); - - let (x1_ch, x1_w) = cell_snapshot(&mut fb, 1, 0); - let (x2_ch, x2_w) = cell_snapshot(&mut fb, 2, 0); - assert_eq!(x1_ch, b' '); - assert_eq!(x1_w, 1, "wide lead should be cleared when continuation is overwritten"); - assert_eq!(x2_ch, b'A'); - assert_eq!(x2_w, 1); - - unsafe { ffi::zr_fb_release(&mut fb as *mut _) }; - } - - #[test] - fn clip_edge_write_over_wide_lead_cleans_hidden_continuation() { - let mut fb = ffi::zr_fb_t { - cols: 0, - rows: 0, - cells: std::ptr::null_mut(), - links: std::ptr::null_mut(), - links_len: 0, - links_cap: 0, - link_bytes: std::ptr::null_mut(), - link_bytes_len: 0, - link_bytes_cap: 0, - }; - let init_rc = unsafe { ffi::zr_fb_init(&mut fb as *mut _, 4, 1) }; - assert_eq!(init_rc, ffi::ZR_OK); - - let clear_rc = unsafe { ffi::zr_fb_clear(&mut fb as *mut _, &style_plain() as *const _) }; - assert_eq!(clear_rc, ffi::ZR_OK); - - let mut clip_stack = [ - ffi::zr_rect_t { - x: 0, - y: 0, - w: 4, - h: 1, - }, - ffi::zr_rect_t { - x: 0, - y: 0, - w: 0, - h: 0, - }, - ]; - let mut painter = ffi::zr_fb_painter_t { - fb: std::ptr::null_mut(), - clip_stack: std::ptr::null_mut(), - clip_cap: 0, - clip_len: 0, - }; - let begin_rc = unsafe { - ffi::zr_fb_painter_begin( - &mut painter as *mut _, - &mut fb as *mut _, - clip_stack.as_mut_ptr(), - clip_stack.len() as u32, - ) - }; - assert_eq!(begin_rc, ffi::ZR_OK); - - let wide_bytes = b"W"; - let write_wide_rc = unsafe { - ffi::zr_fb_put_grapheme( - &mut painter as *mut _, - 1, - 0, - wide_bytes.as_ptr(), - wide_bytes.len(), - 2, - &style_plain() as *const _, - ) - }; - assert_eq!(write_wide_rc, ffi::ZR_OK); - - let push_rc = unsafe { - ffi::zr_fb_clip_push( - &mut painter as *mut _, - ffi::zr_rect_t { - x: 1, - y: 0, - w: 1, - h: 1, - }, - ) - }; - assert_eq!(push_rc, ffi::ZR_OK); - - let b_bytes = b"B"; - let write_b_rc = unsafe { - ffi::zr_fb_put_grapheme( - &mut painter as *mut _, - 1, - 0, - b_bytes.as_ptr(), - b_bytes.len(), - 1, - &style_plain() as *const _, - ) - }; - assert_eq!(write_b_rc, ffi::ZR_OK); - - let pop_rc = unsafe { ffi::zr_fb_clip_pop(&mut painter as *mut _) }; - assert_eq!(pop_rc, ffi::ZR_OK); - - let (x1_ch, x1_w) = cell_snapshot(&mut fb, 1, 0); - let (x2_ch, x2_w) = cell_snapshot(&mut fb, 2, 0); - assert_eq!(x1_ch, b'B'); - assert_eq!(x1_w, 1); - assert_eq!(x2_ch, b' '); - assert_eq!(x2_w, 1, "continuation outside clip should be cleaned"); - - unsafe { ffi::zr_fb_release(&mut fb as *mut _) }; - } - - #[test] - fn diff_reanchors_cursor_after_non_ascii_cell() { - let prev = TestFramebuffer::new(2, 1); - let mut next = TestFramebuffer::new(2, 1); - next.set_cell(0, 0, "✓".as_bytes(), 1, style_plain()); - next.set_cell(1, 0, b"A", 1, style_plain()); - - let out = render_diff_bytes(&prev.raw, &next.raw, style_plain()); - assert!( - contains_subsequence(&out, b"\x1b[1;2H"), - "expected explicit CUP for second cell after non-ascii glyph: {:?}", - String::from_utf8_lossy(&out), - ); - } - - #[test] - fn debug_query_bigint_u64_accepts_in_range_values() { - assert_eq!(parse_debug_query_bigint_u64(false, &[]), Ok(0)); - assert_eq!(parse_debug_query_bigint_u64(false, &[0]), Ok(0)); - assert_eq!(parse_debug_query_bigint_u64(false, &[123]), Ok(123)); - assert_eq!(parse_debug_query_bigint_u64(false, &[u64::MAX]), Ok(u64::MAX)); - } - - #[test] - fn debug_query_bigint_u64_rejects_negative_values() { - assert!(parse_debug_query_bigint_u64(true, &[1]).is_err()); - assert!(parse_debug_query_bigint_u64(true, &[u64::MAX]).is_err()); - } - - #[test] - fn debug_query_bigint_u64_rejects_overflow_values() { - assert!(parse_debug_query_bigint_u64(false, &[0, 1]).is_err()); - assert!(parse_debug_query_bigint_u64(false, &[u64::MAX, 1]).is_err()); - } - - #[test] - fn diff_emits_dim_and_normal_intensity_sequences() { - let to_dim = render_style_transition(0, ATTR_DIM); - assert!( - contains_subsequence(&to_dim, b"\x1b[0;2;"), - "expected dim SGR sequence in output: {:?}", - String::from_utf8_lossy(&to_dim), - ); - - let to_normal = render_style_transition(ATTR_DIM, 0); - assert!( - contains_subsequence(&to_normal, b"\x1b[0;38;"), - "expected normal-intensity SGR sequence in output: {:?}", - String::from_utf8_lossy(&to_normal), - ); - } - - #[test] - fn diff_reapplies_intensity_when_switching_bold_and_dim() { - let dim_to_bold = render_style_transition(ATTR_DIM, ATTR_BOLD); - assert!( - contains_subsequence(&dim_to_bold, b"\x1b[0;1;"), - "expected dim->bold transition to emit bold SGR: {:?}", - String::from_utf8_lossy(&dim_to_bold), - ); - - let bold_to_dim = render_style_transition(ATTR_BOLD, ATTR_DIM); - assert!( - contains_subsequence(&bold_to_dim, b"\x1b[0;2;"), - "expected bold->dim transition to emit dim SGR: {:?}", - String::from_utf8_lossy(&bold_to_dim), - ); - } - #[test] - fn diff_preserves_non_intensity_attr_delta_path() { - let dim_to_dim_underline = render_style_transition(ATTR_DIM, ATTR_DIM | ATTR_UNDERLINE); - assert!( - contains_subsequence(&dim_to_dim_underline, b"\x1b[0;2;4;"), - "expected underline+dim sequence in output: {:?}", - String::from_utf8_lossy(&dim_to_dim_underline), - ); - } + Ok(terminal_caps_to_js(caps)) } diff --git a/packages/native/src/registry.rs b/packages/native/src/registry.rs new file mode 100644 index 00000000..c9f6d153 --- /dev/null +++ b/packages/native/src/registry.rs @@ -0,0 +1,157 @@ +use crate::ffi; +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering}; +use std::sync::{Arc, Condvar, Mutex, OnceLock}; + +pub(crate) struct EngineSlot { + pub(crate) engine: *mut ffi::zr_engine_t, + owner_thread_id: u64, + active_calls: AtomicUsize, + active_calls_mu: Mutex<()>, + active_calls_cv: Condvar, + destroyed: AtomicBool, +} + +unsafe impl Send for EngineSlot {} +unsafe impl Sync for EngineSlot {} + +impl EngineSlot { + fn new(engine: *mut ffi::zr_engine_t) -> Self { + Self { + engine, + owner_thread_id: current_thread_id_u64(), + active_calls: AtomicUsize::new(0), + active_calls_mu: Mutex::new(()), + active_calls_cv: Condvar::new(), + destroyed: AtomicBool::new(false), + } + } + + pub(crate) fn is_owner_thread(&self) -> bool { + self.owner_thread_id == current_thread_id_u64() + } + + pub(crate) fn mark_destroyed(&self) { + self.destroyed.store(true, Ordering::Release); + } + + pub(crate) fn wait_for_idle(&self) { + let guard = match self.active_calls_mu.lock() { + Ok(guard) => guard, + Err(poison) => poison.into_inner(), + }; + let _guard = match self + .active_calls_cv + .wait_while(guard, |_| self.active_calls.load(Ordering::Acquire) != 0) + { + Ok(guard) => guard, + Err(poison) => poison.into_inner(), + }; + } +} + +pub(crate) struct EngineGuard { + pub(crate) slot: Arc, +} + +impl Drop for EngineGuard { + fn drop(&mut self) { + let prev = self.slot.active_calls.fetch_sub(1, Ordering::Release); + if prev == 1 { + self.slot.active_calls_cv.notify_all(); + } + } +} + +static REGISTRY: OnceLock>>> = OnceLock::new(); +static NEXT_ENGINE_ID: AtomicU32 = AtomicU32::new(1); +static NEXT_THREAD_ID: AtomicU64 = AtomicU64::new(1); + +fn registry() -> &'static Mutex>> { + REGISTRY.get_or_init(|| Mutex::new(HashMap::new())) +} + +fn current_thread_id_u64() -> u64 { + thread_local! { + static THREAD_ID: u64 = NEXT_THREAD_ID.fetch_add(1, Ordering::Relaxed); + } + + THREAD_ID.with(|id| *id) +} + +fn alloc_engine_id() -> Result { + loop { + let cur = NEXT_ENGINE_ID.load(Ordering::Relaxed); + if cur == 0 { + return Err(ffi::ZR_ERR_LIMIT); + } + if cur == u32::MAX { + if NEXT_ENGINE_ID + .compare_exchange(cur, 0, Ordering::SeqCst, Ordering::Relaxed) + .is_ok() + { + return Ok(cur); + } + continue; + } + + let next = cur.wrapping_add(1); + if NEXT_ENGINE_ID + .compare_exchange(cur, next, Ordering::SeqCst, Ordering::Relaxed) + .is_ok() + { + return Ok(cur); + } + } +} + +fn lock_registry(f: impl FnOnce(&mut HashMap>) -> T) -> T { + let mut guard = match registry().lock() { + Ok(guard) => guard, + Err(poison) => poison.into_inner(), + }; + f(&mut guard) +} + +pub(crate) fn register_engine(engine: *mut ffi::zr_engine_t) -> Result { + let engine_id = alloc_engine_id()?; + let slot = Arc::new(EngineSlot::new(engine)); + + lock_registry(|map| { + map.insert(engine_id, slot); + }); + + Ok(engine_id) +} + +pub(crate) fn take_engine_for_owner(engine_id: u32) -> Option> { + if engine_id == 0 { + return None; + } + + lock_registry(|map| { + let slot = match map.get(&engine_id) { + Some(slot) => slot, + None => return None, + }; + if !slot.is_owner_thread() { + return None; + } + map.remove(&engine_id) + }) +} + +pub(crate) fn get_engine_guard(engine_id: u32) -> Result { + if engine_id == 0 { + return Err(ffi::ZR_ERR_INVALID_ARGUMENT); + } + + lock_registry(|map| { + let slot = match map.get(&engine_id) { + Some(slot) => Arc::clone(slot), + None => return Err(ffi::ZR_ERR_INVALID_ARGUMENT), + }; + slot.active_calls.fetch_add(1, Ordering::Acquire); + Ok(EngineGuard { slot }) + }) +} diff --git a/packages/native/src/tests.rs b/packages/native/src/tests.rs new file mode 100644 index 00000000..d92bdc1d --- /dev/null +++ b/packages/native/src/tests.rs @@ -0,0 +1,759 @@ +use crate::debug::parse_debug_query_bigint_u64; +use crate::ffi; + +const ATTR_BOLD: u32 = 1 << 0; +const ATTR_UNDERLINE: u32 = 1 << 2; +const ATTR_DIM: u32 = 1 << 4; + +fn contains_subsequence(haystack: &[u8], needle: &[u8]) -> bool { + if needle.is_empty() { + return true; + } + haystack + .windows(needle.len()) + .any(|window| window == needle) +} + +fn style_with_attrs(attrs: u32) -> ffi::zr_style_t { + ffi::zr_style_t { + fg_rgb: 0, + bg_rgb: 0, + attrs, + reserved: 0, + underline_rgb: 0, + link_ref: 0, + } +} + +fn style_plain() -> ffi::zr_style_t { + ffi::zr_style_t { + fg_rgb: 0, + bg_rgb: 0, + attrs: 0, + reserved: 0, + underline_rgb: 0, + link_ref: 0, + } +} + +struct SingleCellFramebuffer { + raw: ffi::zr_fb_t, +} + +impl SingleCellFramebuffer { + fn with_attrs(attrs: u32) -> Self { + let mut raw = ffi::zr_fb_t { + cols: 0, + rows: 0, + cells: std::ptr::null_mut(), + links: std::ptr::null_mut(), + links_len: 0, + links_cap: 0, + link_bytes: std::ptr::null_mut(), + link_bytes_len: 0, + link_bytes_cap: 0, + }; + + let rc = unsafe { ffi::zr_fb_init(&mut raw as *mut _, 1, 1) }; + assert_eq!( + rc, + ffi::ZR_OK, + "zr_fb_init must succeed for test framebuffer" + ); + + let cell = unsafe { ffi::zr_fb_cell(&mut raw as *mut _, 0, 0) }; + assert!( + !cell.is_null(), + "zr_fb_cell(0,0) must return a valid pointer" + ); + unsafe { + (*cell).glyph = [0; 32]; + (*cell).glyph[0] = b'X'; + (*cell).glyph_len = 1; + (*cell).width = 1; + (*cell)._pad0 = 0; + (*cell).style = style_with_attrs(attrs); + } + + Self { raw } + } +} + +impl Drop for SingleCellFramebuffer { + fn drop(&mut self) { + unsafe { ffi::zr_fb_release(&mut self.raw as *mut _) }; + } +} + +struct TestFramebuffer { + raw: ffi::zr_fb_t, +} + +impl TestFramebuffer { + fn new(cols: u32, rows: u32) -> Self { + let mut raw = ffi::zr_fb_t { + cols: 0, + rows: 0, + cells: std::ptr::null_mut(), + links: std::ptr::null_mut(), + links_len: 0, + links_cap: 0, + link_bytes: std::ptr::null_mut(), + link_bytes_len: 0, + link_bytes_cap: 0, + }; + let rc = unsafe { ffi::zr_fb_init(&mut raw as *mut _, cols, rows) }; + assert_eq!( + rc, + ffi::ZR_OK, + "zr_fb_init must succeed for test framebuffer" + ); + let rc_clear = unsafe { ffi::zr_fb_clear(&mut raw as *mut _, &style_plain() as *const _) }; + assert_eq!( + rc_clear, + ffi::ZR_OK, + "zr_fb_clear must succeed for test framebuffer" + ); + Self { raw } + } + + fn set_cell(&mut self, x: u32, y: u32, glyph: &[u8], width: u8, style: ffi::zr_style_t) { + assert!( + glyph.len() <= 32, + "glyph length must fit ZR_CELL_GLYPH_MAX (got {})", + glyph.len() + ); + let cell = unsafe { ffi::zr_fb_cell(&mut self.raw as *mut _, x, y) }; + assert!( + !cell.is_null(), + "zr_fb_cell({x},{y}) must return a valid pointer" + ); + unsafe { + (*cell).glyph = [0; 32]; + for (i, byte) in glyph.iter().copied().enumerate() { + (*cell).glyph[i] = byte; + } + (*cell).glyph_len = glyph.len() as u8; + (*cell).width = width; + (*cell)._pad0 = 0; + (*cell).style = style; + } + } + + fn set_cell_link_ref(&mut self, x: u32, y: u32, link_ref: u32) { + let cell = unsafe { ffi::zr_fb_cell(&mut self.raw as *mut _, x, y) }; + assert!( + !cell.is_null(), + "zr_fb_cell({x},{y}) must return a valid pointer" + ); + unsafe { + (*cell).style.link_ref = link_ref; + } + } + + fn cell_link_ref(&mut self, x: u32, y: u32) -> u32 { + let cell = unsafe { ffi::zr_fb_cell(&mut self.raw as *mut _, x, y) }; + assert!( + !cell.is_null(), + "zr_fb_cell({x},{y}) must return a valid pointer" + ); + unsafe { (*cell).style.link_ref } + } +} + +impl Drop for TestFramebuffer { + fn drop(&mut self) { + unsafe { ffi::zr_fb_release(&mut self.raw as *mut _) }; + } +} + +fn render_diff_bytes( + prev: &ffi::zr_fb_t, + next: &ffi::zr_fb_t, + initial_style: ffi::zr_style_t, +) -> Vec { + let caps = ffi::plat_caps_t { + color_mode: 3, + supports_mouse: 0, + supports_bracketed_paste: 0, + supports_focus_events: 0, + supports_osc52: 0, + supports_sync_update: 0, + supports_scroll_region: 0, + supports_cursor_shape: 1, + supports_output_wait_writable: 0, + supports_underline_styles: 0, + supports_colored_underlines: 0, + supports_hyperlinks: 0, + sgr_attrs_supported: u32::MAX, + }; + let limits = unsafe { ffi::zr_engine_config_default() }.limits; + let initial_term_state = ffi::zr_term_state_t { + cursor_x: 0, + cursor_y: 0, + cursor_visible: 1, + cursor_shape: 0, + cursor_blink: 0, + flags: 0, + style: initial_style, + }; + let desired_cursor_state = ffi::zr_cursor_state_t { + x: -1, + y: -1, + shape: 0, + visible: 1, + blink: 0, + reserved0: 0, + }; + + let mut scratch_damage_rects = vec![ + ffi::zr_damage_rect_t { + x0: 0, + y0: 0, + x1: 0, + y1: 0, + }; + limits.diff_max_damage_rects as usize + ]; + let mut out = [0u8; 1024]; + let mut out_len = 0usize; + let mut out_final_term_state: ffi::zr_term_state_t = unsafe { std::mem::zeroed() }; + let mut out_stats: ffi::zr_diff_stats_t = unsafe { std::mem::zeroed() }; + + let rc = unsafe { + ffi::zr_diff_render( + prev as *const _, + next as *const _, + &caps as *const _, + &initial_term_state as *const _, + &desired_cursor_state as *const _, + &limits as *const _, + scratch_damage_rects.as_mut_ptr(), + scratch_damage_rects.len() as u32, + 0, + out.as_mut_ptr(), + out.len(), + &mut out_len as *mut _, + &mut out_final_term_state as *mut _, + &mut out_stats as *mut _, + ) + }; + assert_eq!(rc, ffi::ZR_OK, "zr_diff_render must succeed"); + assert!(out_len > 0, "zr_diff_render must emit output"); + out[..out_len].to_vec() +} + +fn render_style_transition(current_attrs: u32, desired_attrs: u32) -> Vec { + let prev = SingleCellFramebuffer::with_attrs(current_attrs); + let next = SingleCellFramebuffer::with_attrs(desired_attrs); + render_diff_bytes(&prev.raw, &next.raw, style_with_attrs(current_attrs)) +} + +fn cell_snapshot(fb: &mut ffi::zr_fb_t, x: u32, y: u32) -> (u8, u8) { + let cell = unsafe { ffi::zr_fb_cell(fb as *mut _, x, y) }; + assert!(!cell.is_null(), "cell must exist at ({x},{y})"); + unsafe { ((*cell).glyph[0], (*cell).width) } +} + +#[test] +fn fb_links_clone_from_failure_has_no_partial_effects() { + let mut dst = TestFramebuffer::new(2, 1); + let uri = b"https://example.test/rezi"; + let mut link_ref = 0u32; + let intern_rc = unsafe { + ffi::zr_fb_link_intern( + &mut dst.raw as *mut _, + uri.as_ptr(), + uri.len(), + std::ptr::null(), + 0, + &mut link_ref as *mut _, + ) + }; + assert_eq!( + intern_rc, + ffi::ZR_OK, + "zr_fb_link_intern must seed destination link state" + ); + assert_eq!(link_ref, 1u32); + + let before_links_ptr = dst.raw.links; + let before_links_len = dst.raw.links_len; + let before_links_cap = dst.raw.links_cap; + let before_link_bytes_ptr = dst.raw.link_bytes; + let before_link_bytes_len = dst.raw.link_bytes_len; + let before_link_bytes_cap = dst.raw.link_bytes_cap; + assert!( + !before_links_ptr.is_null(), + "seeded links pointer must be non-null" + ); + assert!( + !before_link_bytes_ptr.is_null(), + "seeded link-bytes pointer must be non-null" + ); + + let before_first_link = unsafe { *before_links_ptr }; + let before_link_bytes = unsafe { + std::slice::from_raw_parts(before_link_bytes_ptr, before_link_bytes_len as usize).to_vec() + }; + + let invalid_src = ffi::zr_fb_t { + cols: dst.raw.cols, + rows: dst.raw.rows, + cells: dst.raw.cells, + links: std::ptr::null_mut(), + links_len: 1, + links_cap: 0, + link_bytes: std::ptr::null_mut(), + link_bytes_len: before_link_bytes_len, + link_bytes_cap: 0, + }; + let clone_rc = + unsafe { ffi::zr_fb_links_clone_from(&mut dst.raw as *mut _, &invalid_src as *const _) }; + assert_eq!(clone_rc, ffi::ZR_ERR_INVALID_ARGUMENT); + + assert_eq!(dst.raw.links, before_links_ptr); + assert_eq!(dst.raw.links_len, before_links_len); + assert_eq!(dst.raw.links_cap, before_links_cap); + assert_eq!(dst.raw.link_bytes, before_link_bytes_ptr); + assert_eq!(dst.raw.link_bytes_len, before_link_bytes_len); + assert_eq!(dst.raw.link_bytes_cap, before_link_bytes_cap); + + let after_first_link = unsafe { *dst.raw.links }; + assert_eq!(after_first_link.uri_off, before_first_link.uri_off); + assert_eq!(after_first_link.uri_len, before_first_link.uri_len); + assert_eq!(after_first_link.id_off, before_first_link.id_off); + assert_eq!(after_first_link.id_len, before_first_link.id_len); + + let after_link_bytes = + unsafe { std::slice::from_raw_parts(dst.raw.link_bytes, dst.raw.link_bytes_len as usize) }; + assert_eq!(after_link_bytes, before_link_bytes.as_slice()); +} + +#[test] +fn fb_link_intern_compacts_stale_refs_and_bounds_growth() { + const LINK_ENTRY_MAX_BYTES: u32 = 2083 + 2083; + let mut fb = TestFramebuffer::new(2, 1); + let persistent_uri = b"https://example.test/persistent"; + + let mut persistent_ref = 0u32; + let seed_rc = unsafe { + ffi::zr_fb_link_intern( + &mut fb.raw as *mut _, + persistent_uri.as_ptr(), + persistent_uri.len(), + std::ptr::null(), + 0, + &mut persistent_ref as *mut _, + ) + }; + assert_eq!(seed_rc, ffi::ZR_OK); + assert_ne!(persistent_ref, 0); + fb.set_cell_link_ref(0, 0, persistent_ref); + + let mut peak_links_len = fb.raw.links_len; + let mut peak_link_bytes_len = fb.raw.link_bytes_len; + + for i in 0..64u32 { + let uri = format!("https://example.test/ephemeral/{i}"); + let mut ref_i = 0u32; + let rc = unsafe { + ffi::zr_fb_link_intern( + &mut fb.raw as *mut _, + uri.as_ptr(), + uri.len(), + std::ptr::null(), + 0, + &mut ref_i as *mut _, + ) + }; + assert_eq!(rc, ffi::ZR_OK, "zr_fb_link_intern failed at iteration {i}"); + assert!(ref_i >= 1 && ref_i <= fb.raw.links_len); + + fb.set_cell_link_ref(1, 0, ref_i); + + let live_ref0 = fb.cell_link_ref(0, 0); + let live_ref1 = fb.cell_link_ref(1, 0); + assert!( + live_ref0 >= 1 && live_ref0 <= fb.raw.links_len, + "cell(0,0) link_ref must remain valid" + ); + assert!( + live_ref1 >= 1 && live_ref1 <= fb.raw.links_len, + "cell(1,0) link_ref must remain valid" + ); + + peak_links_len = peak_links_len.max(fb.raw.links_len); + peak_link_bytes_len = peak_link_bytes_len.max(fb.raw.link_bytes_len); + } + + assert!( + peak_links_len <= 5, + "link table must stay bounded for 2-cell framebuffer (peak={peak_links_len})", + ); + assert!( + peak_link_bytes_len <= 5 * LINK_ENTRY_MAX_BYTES, + "link byte arena must stay bounded for 2-cell framebuffer (peak={peak_link_bytes_len})", + ); + + let mut uri_ptr: *const u8 = std::ptr::null(); + let mut uri_len: usize = 0; + let mut id_ptr: *const u8 = std::ptr::null(); + let mut id_len: usize = 0; + let persistent_cell_ref = fb.cell_link_ref(0, 0); + let lookup_rc = unsafe { + ffi::zr_fb_link_lookup( + &fb.raw as *const _, + persistent_cell_ref, + &mut uri_ptr as *mut _, + &mut uri_len as *mut _, + &mut id_ptr as *mut _, + &mut id_len as *mut _, + ) + }; + assert_eq!(lookup_rc, ffi::ZR_OK); + assert_eq!(id_len, 0); + assert!(id_ptr.is_null()); + assert!(!uri_ptr.is_null()); + + let resolved_uri = unsafe { std::slice::from_raw_parts(uri_ptr, uri_len) }; + assert_eq!(resolved_uri, persistent_uri); +} + +#[test] +fn ffi_layout_matches_vendored_headers() { + use std::mem::{align_of, size_of}; + use std::ptr::addr_of; + + assert_eq!(size_of::(), 24); + assert_eq!(align_of::(), 4); + assert_eq!(size_of::(), 60); + assert_eq!(size_of::(), 36); + assert_eq!(size_of::(), 16); + assert_eq!(align_of::(), 4); + + let caps = std::mem::MaybeUninit::::uninit(); + let base = caps.as_ptr(); + unsafe { + assert_eq!(addr_of!((*base).color_mode) as usize - base as usize, 0); + assert_eq!( + addr_of!((*base).supports_output_wait_writable) as usize - base as usize, + 8 + ); + assert_eq!( + addr_of!((*base).supports_underline_styles) as usize - base as usize, + 9 + ); + assert_eq!( + addr_of!((*base).supports_colored_underlines) as usize - base as usize, + 10 + ); + assert_eq!( + addr_of!((*base).supports_hyperlinks) as usize - base as usize, + 11 + ); + assert_eq!( + addr_of!((*base).sgr_attrs_supported) as usize - base as usize, + 12 + ); + } + + if cfg!(target_pointer_width = "64") { + assert_eq!(size_of::(), 48); + assert_eq!(align_of::(), 8); + } else if cfg!(target_pointer_width = "32") { + assert_eq!(size_of::(), 36); + assert_eq!(align_of::(), 4); + } +} + +#[test] +fn clip_edge_write_over_continuation_cleans_lead_pair() { + let mut fb = ffi::zr_fb_t { + cols: 0, + rows: 0, + cells: std::ptr::null_mut(), + links: std::ptr::null_mut(), + links_len: 0, + links_cap: 0, + link_bytes: std::ptr::null_mut(), + link_bytes_len: 0, + link_bytes_cap: 0, + }; + let init_rc = unsafe { ffi::zr_fb_init(&mut fb as *mut _, 4, 1) }; + assert_eq!(init_rc, ffi::ZR_OK); + + let clear_rc = unsafe { ffi::zr_fb_clear(&mut fb as *mut _, &style_plain() as *const _) }; + assert_eq!(clear_rc, ffi::ZR_OK); + + let mut clip_stack = [ + ffi::zr_rect_t { + x: 0, + y: 0, + w: 4, + h: 1, + }, + ffi::zr_rect_t { + x: 0, + y: 0, + w: 0, + h: 0, + }, + ]; + let mut painter = ffi::zr_fb_painter_t { + fb: std::ptr::null_mut(), + clip_stack: std::ptr::null_mut(), + clip_cap: 0, + clip_len: 0, + }; + let begin_rc = unsafe { + ffi::zr_fb_painter_begin( + &mut painter as *mut _, + &mut fb as *mut _, + clip_stack.as_mut_ptr(), + clip_stack.len() as u32, + ) + }; + assert_eq!(begin_rc, ffi::ZR_OK); + + let wide_bytes = b"W"; + let write_wide_rc = unsafe { + ffi::zr_fb_put_grapheme( + &mut painter as *mut _, + 1, + 0, + wide_bytes.as_ptr(), + wide_bytes.len(), + 2, + &style_plain() as *const _, + ) + }; + assert_eq!(write_wide_rc, ffi::ZR_OK); + + let push_rc = unsafe { + ffi::zr_fb_clip_push( + &mut painter as *mut _, + ffi::zr_rect_t { + x: 2, + y: 0, + w: 1, + h: 1, + }, + ) + }; + assert_eq!(push_rc, ffi::ZR_OK); + + let a_bytes = b"A"; + let write_a_rc = unsafe { + ffi::zr_fb_put_grapheme( + &mut painter as *mut _, + 2, + 0, + a_bytes.as_ptr(), + a_bytes.len(), + 1, + &style_plain() as *const _, + ) + }; + assert_eq!(write_a_rc, ffi::ZR_OK); + + let pop_rc = unsafe { ffi::zr_fb_clip_pop(&mut painter as *mut _) }; + assert_eq!(pop_rc, ffi::ZR_OK); + + let (x1_ch, x1_w) = cell_snapshot(&mut fb, 1, 0); + let (x2_ch, x2_w) = cell_snapshot(&mut fb, 2, 0); + assert_eq!(x1_ch, b' '); + assert_eq!( + x1_w, 1, + "wide lead should be cleared when continuation is overwritten" + ); + assert_eq!(x2_ch, b'A'); + assert_eq!(x2_w, 1); + + unsafe { ffi::zr_fb_release(&mut fb as *mut _) }; +} + +#[test] +fn clip_edge_write_over_wide_lead_cleans_hidden_continuation() { + let mut fb = ffi::zr_fb_t { + cols: 0, + rows: 0, + cells: std::ptr::null_mut(), + links: std::ptr::null_mut(), + links_len: 0, + links_cap: 0, + link_bytes: std::ptr::null_mut(), + link_bytes_len: 0, + link_bytes_cap: 0, + }; + let init_rc = unsafe { ffi::zr_fb_init(&mut fb as *mut _, 4, 1) }; + assert_eq!(init_rc, ffi::ZR_OK); + + let clear_rc = unsafe { ffi::zr_fb_clear(&mut fb as *mut _, &style_plain() as *const _) }; + assert_eq!(clear_rc, ffi::ZR_OK); + + let mut clip_stack = [ + ffi::zr_rect_t { + x: 0, + y: 0, + w: 4, + h: 1, + }, + ffi::zr_rect_t { + x: 0, + y: 0, + w: 0, + h: 0, + }, + ]; + let mut painter = ffi::zr_fb_painter_t { + fb: std::ptr::null_mut(), + clip_stack: std::ptr::null_mut(), + clip_cap: 0, + clip_len: 0, + }; + let begin_rc = unsafe { + ffi::zr_fb_painter_begin( + &mut painter as *mut _, + &mut fb as *mut _, + clip_stack.as_mut_ptr(), + clip_stack.len() as u32, + ) + }; + assert_eq!(begin_rc, ffi::ZR_OK); + + let wide_bytes = b"W"; + let write_wide_rc = unsafe { + ffi::zr_fb_put_grapheme( + &mut painter as *mut _, + 1, + 0, + wide_bytes.as_ptr(), + wide_bytes.len(), + 2, + &style_plain() as *const _, + ) + }; + assert_eq!(write_wide_rc, ffi::ZR_OK); + + let push_rc = unsafe { + ffi::zr_fb_clip_push( + &mut painter as *mut _, + ffi::zr_rect_t { + x: 1, + y: 0, + w: 1, + h: 1, + }, + ) + }; + assert_eq!(push_rc, ffi::ZR_OK); + + let b_bytes = b"B"; + let write_b_rc = unsafe { + ffi::zr_fb_put_grapheme( + &mut painter as *mut _, + 1, + 0, + b_bytes.as_ptr(), + b_bytes.len(), + 1, + &style_plain() as *const _, + ) + }; + assert_eq!(write_b_rc, ffi::ZR_OK); + + let pop_rc = unsafe { ffi::zr_fb_clip_pop(&mut painter as *mut _) }; + assert_eq!(pop_rc, ffi::ZR_OK); + + let (x1_ch, x1_w) = cell_snapshot(&mut fb, 1, 0); + let (x2_ch, x2_w) = cell_snapshot(&mut fb, 2, 0); + assert_eq!(x1_ch, b'B'); + assert_eq!(x1_w, 1); + assert_eq!(x2_ch, b' '); + assert_eq!(x2_w, 1, "continuation outside clip should be cleaned"); + + unsafe { ffi::zr_fb_release(&mut fb as *mut _) }; +} + +#[test] +fn diff_reanchors_cursor_after_non_ascii_cell() { + let prev = TestFramebuffer::new(2, 1); + let mut next = TestFramebuffer::new(2, 1); + next.set_cell(0, 0, "✓".as_bytes(), 1, style_plain()); + next.set_cell(1, 0, b"A", 1, style_plain()); + + let out = render_diff_bytes(&prev.raw, &next.raw, style_plain()); + assert!( + contains_subsequence(&out, b"\x1b[1;2H"), + "expected explicit CUP for second cell after non-ascii glyph: {:?}", + String::from_utf8_lossy(&out), + ); +} + +#[test] +fn debug_query_bigint_u64_accepts_in_range_values() { + assert_eq!(parse_debug_query_bigint_u64(false, &[]), Ok(0)); + assert_eq!(parse_debug_query_bigint_u64(false, &[0]), Ok(0)); + assert_eq!(parse_debug_query_bigint_u64(false, &[123]), Ok(123)); + assert_eq!( + parse_debug_query_bigint_u64(false, &[u64::MAX]), + Ok(u64::MAX) + ); +} + +#[test] +fn debug_query_bigint_u64_rejects_negative_values() { + assert!(parse_debug_query_bigint_u64(true, &[1]).is_err()); + assert!(parse_debug_query_bigint_u64(true, &[u64::MAX]).is_err()); +} + +#[test] +fn debug_query_bigint_u64_rejects_overflow_values() { + assert!(parse_debug_query_bigint_u64(false, &[0, 1]).is_err()); + assert!(parse_debug_query_bigint_u64(false, &[u64::MAX, 1]).is_err()); +} + +#[test] +fn diff_emits_dim_and_normal_intensity_sequences() { + let to_dim = render_style_transition(0, ATTR_DIM); + assert!( + contains_subsequence(&to_dim, b"\x1b[0;2;"), + "expected dim SGR sequence in output: {:?}", + String::from_utf8_lossy(&to_dim), + ); + + let to_normal = render_style_transition(ATTR_DIM, 0); + assert!( + contains_subsequence(&to_normal, b"\x1b[0;38;"), + "expected normal-intensity SGR sequence in output: {:?}", + String::from_utf8_lossy(&to_normal), + ); +} + +#[test] +fn diff_reapplies_intensity_when_switching_bold_and_dim() { + let dim_to_bold = render_style_transition(ATTR_DIM, ATTR_BOLD); + assert!( + contains_subsequence(&dim_to_bold, b"\x1b[0;1;"), + "expected dim->bold transition to emit bold SGR: {:?}", + String::from_utf8_lossy(&dim_to_bold), + ); + + let bold_to_dim = render_style_transition(ATTR_BOLD, ATTR_DIM); + assert!( + contains_subsequence(&bold_to_dim, b"\x1b[0;2;"), + "expected bold->dim transition to emit dim SGR: {:?}", + String::from_utf8_lossy(&bold_to_dim), + ); +} + +#[test] +fn diff_preserves_non_intensity_attr_delta_path() { + let dim_to_dim_underline = render_style_transition(ATTR_DIM, ATTR_DIM | ATTR_UNDERLINE); + assert!( + contains_subsequence(&dim_to_dim_underline, b"\x1b[0;2;4;"), + "expected underline+dim sequence in output: {:?}", + String::from_utf8_lossy(&dim_to_dim_underline), + ); +} From 301c4b876af4af91fcab8bdcfb3d4bf3a149d44c Mon Sep 17 00:00:00 2001 From: RtlZeroMemory <58250858+RtlZeroMemory@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:29:02 +0400 Subject: [PATCH 3/5] fix(native): align engine guard semantics --- packages/native/scripts/smoke.mjs | 9 +++++++-- packages/native/src/config.rs | 11 +++++++---- packages/native/src/lib.rs | 3 +++ packages/native/src/registry.rs | 6 +++++- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/native/scripts/smoke.mjs b/packages/native/scripts/smoke.mjs index 3a3f22d0..0b529d2f 100644 --- a/packages/native/scripts/smoke.mjs +++ b/packages/native/scripts/smoke.mjs @@ -167,6 +167,11 @@ assert( engineSetConfig(engineId, null) === ZR_ERR_INVALID_ARGUMENT, "engineSetConfig(null) must return ZR_ERR_INVALID_ARGUMENT", ); +assertThrows( + () => engineSetConfig(engineId, { plat: 1 }), + /plat must be an object/i, + "engineSetConfig must reject non-object plat values", +); assertThrows( () => engineSetConfig(engineId, { unknownKey: 1 }), /unknown key/i, @@ -292,8 +297,8 @@ assert( `wrong-thread enginePresent must return ZR_ERR_INVALID_ARGUMENT, got: ${alive.present}`, ); assert( - alive.postUserEvent === ZR_OK, - `enginePostUserEvent must succeed cross-thread while alive (ZR_OK), got: ${alive.postUserEvent}`, + alive.postUserEvent === ZR_ERR_INVALID_ARGUMENT, + `wrong-thread enginePostUserEvent must return ZR_ERR_INVALID_ARGUMENT, got: ${alive.postUserEvent}`, ); assert( alive.setConfig === ZR_ERR_INVALID_ARGUMENT, diff --git a/packages/native/src/config.rs b/packages/native/src/config.rs index ff3dacd2..bac4a6c2 100644 --- a/packages/native/src/config.rs +++ b/packages/native/src/config.rs @@ -188,11 +188,14 @@ fn js_obj(obj: &JsObject, primary: &str, alias: &str) -> ParseResult v, Err(_) => continue, }; - if v.get_type().map_err(|_| ())? == ValueType::Undefined { - continue; + match v.get_type().map_err(|_| ())? { + ValueType::Undefined => continue, + ValueType::Object => { + let o = v.coerce_to_object().map_err(|_| ())?; + return Ok(Some(o)); + } + _ => return Err(()), } - let o = v.coerce_to_object().map_err(|_| ())?; - return Ok(Some(o)); } Ok(None) } diff --git a/packages/native/src/lib.rs b/packages/native/src/lib.rs index 31e80db5..c284fe89 100644 --- a/packages/native/src/lib.rs +++ b/packages/native/src/lib.rs @@ -389,6 +389,9 @@ pub fn engine_post_user_event(engine_id: u32, tag: u32, payload: Uint8Array) -> Ok(guard) => guard, Err(rc) => return rc, }; + if !guard.slot.is_owner_thread() { + return ffi::ZR_ERR_INVALID_ARGUMENT; + } if payload.len() > (i32::MAX as usize) { return ffi::ZR_ERR_LIMIT; diff --git a/packages/native/src/registry.rs b/packages/native/src/registry.rs index ade92397..0fe1ff33 100644 --- a/packages/native/src/registry.rs +++ b/packages/native/src/registry.rs @@ -57,7 +57,11 @@ pub(crate) struct EngineGuard { impl Drop for EngineGuard { fn drop(&mut self) { - let prev = self.slot.active_calls.fetch_sub(1, Ordering::Release); + let _active_calls_guard = match self.slot.active_calls_mu.lock() { + Ok(guard) => guard, + Err(poison) => poison.into_inner(), + }; + let prev = self.slot.active_calls.fetch_sub(1, Ordering::AcqRel); if prev == 1 { self.slot.active_calls_cv.notify_all(); } From e7e1021cd8803ccd0c618494f665a313e1e359d3 Mon Sep 17 00:00:00 2001 From: RtlZeroMemory <58250858+RtlZeroMemory@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:41:14 +0400 Subject: [PATCH 4/5] test(node): gate prebuild native loader smoke --- .../src/__tests__/worker_integration.test.ts | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/packages/node/src/__tests__/worker_integration.test.ts b/packages/node/src/__tests__/worker_integration.test.ts index 8f3ecc23..2932aa0c 100644 --- a/packages/node/src/__tests__/worker_integration.test.ts +++ b/packages/node/src/__tests__/worker_integration.test.ts @@ -35,6 +35,13 @@ function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } +// The default JS test suite runs before CI builds the native addon. Keep the +// bare worker-thread loader smoke opt-in; packages/native/scripts/smoke.mjs +// covers the same path after an explicit native build. +const runNativeLoaderPrebuildSmoke = + (process.env as NodeJS.ProcessEnv & Readonly<{ REZI_RUN_NATIVE_LOADER_PREBUILD_SMOKE?: string }>) + .REZI_RUN_NATIVE_LOADER_PREBUILD_SMOKE === "1"; + function setIsTty( stream: NodeJS.ReadStream | NodeJS.WriteStream, value: boolean | undefined, @@ -118,10 +125,12 @@ async function shutdownAndWaitForExit(worker: Worker): Promise { await exitPromise; } -test("native loader: worker-thread load succeeds and exits cleanly", async () => { - const loaderPath = fileURLToPath(new URL("../../../native/loader.cjs", import.meta.url)); - const worker = new Worker( - ` +(runNativeLoaderPrebuildSmoke ? test : test.skip)( + "native loader: worker-thread load succeeds and exits cleanly", + async () => { + const loaderPath = fileURLToPath(new URL("../../../native/loader.cjs", import.meta.url)); + const worker = new Worker( + ` const { parentPort } = require("node:worker_threads"); try { require(${JSON.stringify(loaderPath)}); @@ -134,20 +143,21 @@ test("native loader: worker-thread load succeeds and exits cleanly", async () => parentPort.postMessage({ type: "loaderResult", ok: false, message }); } `, - { eval: true }, - ); + { eval: true }, + ); - const [msg] = (await once(worker, "message")) as [ - Readonly<{ type?: unknown; ok?: unknown; message?: unknown }>, - ]; - assert.equal(msg.type, "loaderResult"); - assert.equal(msg.ok, true); - assert.equal(typeof msg.message, "string"); - assert.equal(String(msg.message), ""); + const [msg] = (await once(worker, "message")) as [ + Readonly<{ type?: unknown; ok?: unknown; message?: unknown }>, + ]; + assert.equal(msg.type, "loaderResult"); + assert.equal(msg.ok, true); + assert.equal(typeof msg.message, "string"); + assert.equal(String(msg.message), ""); - const [code] = (await once(worker, "exit")) as [number]; - assert.equal(code, 0); -}); + const [code] = (await once(worker, "exit")) as [number]; + assert.equal(code, 0); + }, +); test("worker: init/ready + latest-wins transfer mailbox avoids stale fatal", async () => { const worker = makeWorker(); From 5eafe9c0273fd627fb82d331782cf6b108842427 Mon Sep 17 00:00:00 2001 From: RtlZeroMemory <58250858+RtlZeroMemory@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:16:38 +0400 Subject: [PATCH 5/5] fix(native): harden config and debug numeric parsing --- packages/native/src/config.rs | 9 ++++++++- packages/native/src/debug.rs | 19 +++++++++++++++---- packages/native/src/tests.rs | 28 +++++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/packages/native/src/config.rs b/packages/native/src/config.rs index d95b2ca6..1d793eea 100644 --- a/packages/native/src/config.rs +++ b/packages/native/src/config.rs @@ -155,6 +155,13 @@ pub(crate) fn js_u32(obj: &JsObject, primary: &str, alias: &str) -> ParseResult< Ok(None) } +pub(crate) fn checked_u8(value: u32) -> ParseResult { + if value > u8::MAX as u32 { + return Err(()); + } + Ok(value as u8) +} + pub(crate) fn js_u8_bool(obj: &JsObject, primary: &str, alias: &str) -> ParseResult> { for name in [primary, alias] { let v = match obj.get_named_property::(name) { @@ -238,7 +245,7 @@ fn apply_limits(dst: &mut ffi::zr_limits_t, obj: &JsObject) -> ParseResult<()> { fn apply_plat(dst: &mut ffi::plat_config_t, obj: &JsObject) -> ParseResult<()> { if let Some(v) = js_u32(obj, "requestedColorMode", "requested_color_mode")? { - dst.requested_color_mode = (v & 0xFF) as u8; + dst.requested_color_mode = checked_u8(v)?; } if let Some(v) = js_u8_bool(obj, "enableMouse", "enable_mouse")? { dst.enable_mouse = v; diff --git a/packages/native/src/debug.rs b/packages/native/src/debug.rs index fb97a0fa..e7d1f7da 100644 --- a/packages/native/src/debug.rs +++ b/packages/native/src/debug.rs @@ -46,6 +46,8 @@ const DEBUG_QUERY_KEYS: &[(&str, &str)] = &[ ("maxRecords", "max_records"), ]; +const MAX_SAFE_INTEGER_U64: u64 = 9_007_199_254_740_991; + pub(crate) fn parse_debug_query_bigint_u64(sign_bit: bool, words: &[u64]) -> ParseResult { if sign_bit && words.iter().any(|word| *word != 0) { return Err(()); @@ -58,6 +60,18 @@ pub(crate) fn parse_debug_query_bigint_u64(sign_bit: bool, words: &[u64]) -> Par } } +pub(crate) fn parse_debug_query_number_u64(float: f64) -> ParseResult { + if !float.is_finite() + || float < 0.0 + || float.fract() != 0.0 + || float > MAX_SAFE_INTEGER_U64 as f64 + { + return Err(()); + } + + Ok(float as u64) +} + fn js_u64(obj: &JsObject, primary: &str, alias: &str) -> ParseResult> { for name in [primary, alias] { let value = match obj.get_named_property::(name) { @@ -74,10 +88,7 @@ fn js_u64(obj: &JsObject, primary: &str, alias: &str) -> ParseResult ValueType::Number => { let number = value.coerce_to_number().map_err(|_| ())?; let float = number.get_double().map_err(|_| ())?; - if !float.is_finite() || float < 0.0 || float > (u64::MAX as f64) { - return Err(()); - } - return Ok(Some(float as u64)); + return Ok(Some(parse_debug_query_number_u64(float)?)); } _ => return Err(()), } diff --git a/packages/native/src/tests.rs b/packages/native/src/tests.rs index d92bdc1d..496ca21b 100644 --- a/packages/native/src/tests.rs +++ b/packages/native/src/tests.rs @@ -1,4 +1,5 @@ -use crate::debug::parse_debug_query_bigint_u64; +use crate::config::checked_u8; +use crate::debug::{parse_debug_query_bigint_u64, parse_debug_query_number_u64}; use crate::ffi; const ATTR_BOLD: u32 = 1 << 0; @@ -714,6 +715,31 @@ fn debug_query_bigint_u64_rejects_overflow_values() { assert!(parse_debug_query_bigint_u64(false, &[u64::MAX, 1]).is_err()); } +#[test] +fn debug_query_number_u64_accepts_safe_integers() { + assert_eq!(parse_debug_query_number_u64(0.0), Ok(0)); + assert_eq!(parse_debug_query_number_u64(42.0), Ok(42)); + assert_eq!( + parse_debug_query_number_u64(9_007_199_254_740_991.0), + Ok(9_007_199_254_740_991) + ); +} + +#[test] +fn debug_query_number_u64_rejects_fractional_or_unsafe_numbers() { + assert!(parse_debug_query_number_u64(-1.0).is_err()); + assert!(parse_debug_query_number_u64(1.5).is_err()); + assert!(parse_debug_query_number_u64(f64::INFINITY).is_err()); + assert!(parse_debug_query_number_u64(9_007_199_254_740_992.0).is_err()); +} + +#[test] +fn checked_u8_rejects_out_of_range_values() { + assert_eq!(checked_u8(0), Ok(0)); + assert_eq!(checked_u8(255), Ok(255)); + assert!(checked_u8(256).is_err()); +} + #[test] fn diff_emits_dim_and_normal_intensity_sequences() { let to_dim = render_style_transition(0, ATTR_DIM);