Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
133 commits
Select commit Hold shift + click to select a range
3a60d74
fix(export): remove legacy vflip — native compositor readback is alre…
AIEraDev Aug 25, 2026
6874815
fix(evaluation): persist same-track zIndex independent of clip array …
AIEraDev Aug 25, 2026
1ec786a
feat(history): single undoable path for all track property toggles
AIEraDev Aug 25, 2026
54e6b69
feat(render): NativeRasterBridge unifies raster asset production acro…
AIEraDev Aug 25, 2026
1a0d40f
feat(compounds): multi-track compound clips preserve each child's ori…
AIEraDev Aug 25, 2026
11ddde1
test(native): validate layer caps and raise track density target to 2…
AIEraDev Aug 25, 2026
8b8a4e1
refactor(types): promote role, zIndex, and evaluationPriority to firs…
AIEraDev Aug 25, 2026
c78f6a1
feat(compositor): canonical ordering function shared by evaluator and…
AIEraDev Aug 25, 2026
6bea8e2
test(ordering): update expectations to reflect track-authority stacki…
AIEraDev Aug 25, 2026
f21a8f3
test(audit): regression coverage for audio/visibility edge cases
AIEraDev Aug 25, 2026
a2e7267
fix(decoder): normalize RGB still-image metadata to SDR NV12 after co…
AIEraDev Aug 25, 2026
c441392
fix(timeline): preserve authored still-image duration through project…
AIEraDev Aug 25, 2026
f0c28da
style(NativeProgramPreview): reformat long import statements to multi…
AIEraDev Aug 25, 2026
bacd3a5
feat(native): decode_image_rgba IPC command for alpha-preserving stil…
AIEraDev Aug 25, 2026
96302b2
feat(tauri): decodeNativeRgbaFrame IPC binding for still-image alpha …
AIEraDev Aug 25, 2026
f260d72
feat(serialization): persist and restore mainVideoTrackId in project …
AIEraDev Aug 25, 2026
e99da68
fix(store): infer main video track from clip/asset kind; persist main…
AIEraDev Aug 25, 2026
8dcd379
feat(rust/app): wire mainVideoTrackId through Rust model and App load…
AIEraDev Aug 25, 2026
bc01bf8
feat(rasterBridge): rasterizeImages path decodes still images as cach…
AIEraDev Aug 25, 2026
2a3b1c5
feat(nativeVideoPreview): route still images through raster path; gat…
AIEraDev Aug 25, 2026
9a0dced
fix(NativeProgramPreview): include native-image assets in device-reco…
AIEraDev Aug 25, 2026
f1e0a03
feat(properties-panel): 44px collapsed rail with expand button and sh…
AIEraDev Aug 26, 2026
d9eb1b5
feat(topbar): add PanelLeft/PanelRight toggle buttons for sidebar and…
AIEraDev Aug 26, 2026
024582b
feat(shortcuts): Cmd+B toggles Media Library; Alt+P / Cmd+Shift+P tog…
AIEraDev Aug 26, 2026
fcecb08
fix(sidebar): flex-1 min-w-0 when no explicit width is supplied
AIEraDev Aug 26, 2026
1bb0681
feat(native): in-GPU image decode path; transport pitch preservation
AIEraDev Aug 26, 2026
107f0d1
fix(preview): preserve pitch across transport runtimes
AIEraDev Aug 26, 2026
5def97a
fix(preview): align image raster identity on seek
AIEraDev Aug 26, 2026
22fa97e
feat(native-core): SDF text foundation — font registry, glyph cache, …
AIEraDev Aug 26, 2026
13b9acc
feat(compositor): text effect pipeline, interpreter, and GPU layer cache
AIEraDev Aug 26, 2026
2b19457
feat(native-preview): wire TextLayerSnapshot through contract, compos…
AIEraDev Aug 26, 2026
808bd96
feat(crates): expose font/glyph/SDF and TextLayerSnapshot from native…
AIEraDev Aug 26, 2026
f6d0b66
feat(ts): NativeTextLayerSnapshot type and text-layer native frame re…
AIEraDev Aug 26, 2026
7a49532
docs: add text-effects-architecture.md covering SDF pipeline and effe…
AIEraDev Aug 26, 2026
195b5cd
test(native-core): SDF accuracy, glyph cache eviction, and text shapi…
AIEraDev Aug 26, 2026
15379e2
feat(telemetry): zero-PII performance telemetry collector and HTTP cl…
AIEraDev Aug 26, 2026
69a70ef
feat(uiStore): openSettingsModal(tab) action with initial tab routing
AIEraDev Aug 26, 2026
57f4473
feat(services): export telemetryCollector and performanceClient from …
AIEraDev Aug 26, 2026
f21217d
fix(sdf-test): use odd-dimension grid for exact radial symmetry; tigh…
AIEraDev Aug 26, 2026
945c922
fix(compositor): expose target_format; reject unknown primitive varia…
AIEraDev Aug 26, 2026
c1c479c
docs: add performance telemetry spec and update README with telemetry…
AIEraDev Aug 26, 2026
9380ca2
refactor(uiStore): remove phantom performance tab; reformat long unio…
AIEraDev Aug 26, 2026
4e4883d
feat(native-core): glyph cache stats, MAX_TEXT_CANVAS_DIMENSION, font…
AIEraDev Aug 26, 2026
e36ef4f
feat(contracts/compositor): TemplateDefinitionSnapshot and multi-pass…
AIEraDev Aug 26, 2026
54c9afc
refactor(text-templates): rewrite TemplateDefinition types for Phase …
AIEraDev Aug 26, 2026
e7f4a03
fix(inspector): 8192px canvas budget truncation warning in text style…
AIEraDev Aug 26, 2026
77ddede
feat(text-templates): instantiateTemplate — compound clip reuse (Phas…
AIEraDev Aug 26, 2026
5af59d4
feat(export): preflight dependency gate — blocks silent text-effect d…
AIEraDev Aug 26, 2026
cc65f78
feat(timeline): template insertion via compound clip reuse in useAddT…
AIEraDev Aug 26, 2026
cebc894
style(playback/audio): remove tracePlayback imports; reformat long lines
AIEraDev Aug 26, 2026
34fda96
refactor(rasterBridge): text layers handled natively by wgpu; remove …
AIEraDev Aug 26, 2026
bd72c70
feat(preview): native text layers built in buildNativeVideoProjectReq…
AIEraDev Aug 26, 2026
def63a4
fix(native-text): compute_text_layer_placement — center text inside b…
AIEraDev Aug 26, 2026
21c66e8
fix(transform): apply cursor style inline so resize/move cursors work…
AIEraDev Aug 26, 2026
0cd532c
fix(text-templates): make TemplateDefinition fields optional for lega…
AIEraDev Aug 26, 2026
3603c40
feat(contracts): bump to v2; add font variants, alignment, background…
AIEraDev Aug 26, 2026
88b2267
feat(font-registry): add require_font — strict lookup that rejects mi…
AIEraDev Aug 26, 2026
b3297ff
feat(crate): re-export new v2 contract types from clypra-native-core
AIEraDev Aug 26, 2026
1f7ed3c
feat(compositor): strict font lookup, expanded cache key, vertical al…
AIEraDev Aug 26, 2026
762305d
feat(commands): register_native_font and list_native_fonts IPC; verti…
AIEraDev Aug 26, 2026
d3831fd
feat(tauri): register register_native_font and list_native_fonts in i…
AIEraDev Aug 26, 2026
dc4aba7
fix(playback): reference NATIVE_CORE_CONTRACT_VERSION instead of hard…
AIEraDev Aug 26, 2026
b6e065b
fix(service): reference NATIVE_CORE_CONTRACT_VERSION in FrameRequest …
AIEraDev Aug 26, 2026
0271b83
fix(session): pin PlaybackSession contract_version to NATIVE_CORE_CON…
AIEraDev Aug 26, 2026
837b513
fix(native_playback): reference NATIVE_CORE_CONTRACT_VERSION in test …
AIEraDev Aug 26, 2026
1a23afe
feat(nativeCore): bump TS contract to v2; extend NativeTextLayerSnaps…
AIEraDev Aug 26, 2026
9d68266
feat(tauri): add registerNativeFont and listNativeFonts IPC bindings
AIEraDev Aug 26, 2026
d380e87
feat(types): add runs field to EvaluatedTextLayer for karaoke/caption…
AIEraDev Aug 26, 2026
97996e7
feat(evaluator): compute karaoke runs from clip words at evaluation time
AIEraDev Aug 26, 2026
26b6da3
refactor(rasterBridge): remove browser text rasterization — text is n…
AIEraDev Aug 26, 2026
3b467db
feat(nativeVideoPreview): populate v2 text snapshot fields; add effec…
AIEraDev Aug 26, 2026
77c69f5
fix(NativeProgramPreview): native surface owns all states; disable RG…
AIEraDev Aug 26, 2026
2f14b55
fix(audio): use NATIVE_CORE_CONTRACT_VERSION in configureNativePlayba…
AIEraDev Aug 26, 2026
91b1b9d
refactor(thumbnails): migrate to buildNativeFrameRequest / renderNati…
AIEraDev Aug 26, 2026
e7ce493
refactor(exportFrame): migrate to buildNativeFrameRequest / renderNat…
AIEraDev Aug 26, 2026
1cdba65
refactor(exportSequence): migrate to buildNativeFrameRequest / render…
AIEraDev Aug 26, 2026
b49fb73
refactor(videoExport): migrate to buildNativeFrameRequest / renderNat…
AIEraDev Aug 26, 2026
0d81098
feat(ExportDialog): blocked-missing-effects phase with explicit fallb…
AIEraDev Aug 26, 2026
38351a5
test(nativeContract): update contract version assertion to 2
AIEraDev Aug 26, 2026
a0f4c41
test(nativeVideoPreview): update contractVersion to 2; add v2 text sn…
AIEraDev Aug 26, 2026
32a8bda
test(nativePreviewScheduler): update makeRequest fixture contractVers…
AIEraDev Aug 26, 2026
e09fa10
docs(native-architecture): record contract v2 milestone; expand text …
AIEraDev Aug 26, 2026
b841f37
docs(native-only-preview): move gradient/shader backgrounds and Lotti…
AIEraDev Aug 26, 2026
f47054e
docs(legacy-retirement): check off native surface state ownership and…
AIEraDev Aug 26, 2026
079ad9b
docs(text-effects): correct diagram label; add desktop contract note …
AIEraDev Aug 26, 2026
04995e6
feat(nativeVideoPreview): add 'prefetch' to buildNativeFrameRequest m…
AIEraDev Aug 26, 2026
dfab3e3
fix(native-preview): keep prefetch frames outside the visible generat…
AIEraDev Aug 26, 2026
9207553
fix(NativeProgramPreview): connect prefetch to desktop playback — war…
AIEraDev Aug 26, 2026
eaf9b41
fix(evaluator): preserve empty text — use nullish coalescing instead …
AIEraDev Aug 26, 2026
5bb25c6
fix(TextStyleSection): use nullish coalescing when computing text con…
AIEraDev Aug 26, 2026
f9fd812
test(textEvaluation): regression — explicitly empty text clip produce…
AIEraDev Aug 26, 2026
1fb4823
feat(native-core): add woff2-patched dependency for WOFF2-to-TTF conv…
AIEraDev Aug 26, 2026
676aae0
feat(font-registry): accept WOFF2 fonts — auto-convert to TTF at regi…
AIEraDev Aug 26, 2026
3486b59
feat(tauri): add woff2-patched dependency for WOFF2 font decoding in …
AIEraDev Aug 26, 2026
67f8662
chore(Cargo.lock): update lockfile for woff2-patched and transitive deps
AIEraDev Aug 26, 2026
6c40a00
feat(native-preview): register_native_font_bytes command; diagnostic …
AIEraDev Aug 26, 2026
bcbc525
feat(tauri): register register_native_font_bytes in invoke_handler
AIEraDev Aug 26, 2026
ccc3157
fix(compositor): diagnostic eprintln for text-font-missing and text-c…
AIEraDev Aug 26, 2026
99b6ffa
feat(tauri): add registerNativeFontBytes IPC binding
AIEraDev Aug 26, 2026
cf3e6a8
feat(fonts): nativeFontRegistry — auto-register all bundled editor fo…
AIEraDev Aug 26, 2026
0b60c21
feat(NativeProgramPreview): register fonts before native frames; pers…
AIEraDev Aug 26, 2026
6ae13ff
refactor(audio): remove audibilityCheckHandle field and getNativeAudi…
AIEraDev Aug 26, 2026
0730274
chore: remove periodic console.log and console.table output from prod…
AIEraDev Aug 26, 2026
3dd456b
fix(TransformOverlay): RAF-throttle mousemove; expand corner handle h…
AIEraDev Aug 26, 2026
ac044c5
test(TransformOverlay): assert diagonal cursor direction for each cor…
AIEraDev Aug 26, 2026
a289f54
fix(historyStore): schedule auto-save after every command, undo, redo…
AIEraDev Aug 26, 2026
a3017ef
fix(NativeProgramPreview): event-driven render loop while paused; wak…
AIEraDev Aug 26, 2026
31ffd9f
fix(css): propagate transform drag cursors through all descendants wi…
AIEraDev Aug 26, 2026
1f70faf
fix(TextStyleSection): preserve empty text when applying a template —…
AIEraDev Aug 26, 2026
764131b
fix(Clip): include text, kind, and name in arePropsEqual so memoized …
AIEraDev Aug 26, 2026
21774cc
feat(TextStyleSection): native-aware font picker — show only register…
AIEraDev Aug 26, 2026
559d798
fix(Track): compare clip.text, clip.kind, and clip.name in arePropsEqual
AIEraDev Aug 26, 2026
a32da57
fix(NativeProgramPreview): replace native-only blocker overlay with d…
AIEraDev Aug 26, 2026
011731d
chore(deps): add @fontsource/noto-emoji 5.3.0 for native emoji fallba…
AIEraDev Aug 26, 2026
3469e62
feat(nativeFontRegistry): register Noto Emoji as internal native fall…
AIEraDev Aug 26, 2026
a174d0f
feat(font-registry): export EMOJI_FALLBACK_FONT_ID constant for wgpu …
AIEraDev Aug 26, 2026
cb560da
feat(glyph-cache): render_text_sdf_aligned_with_fallback — per-glyph …
AIEraDev Aug 26, 2026
43edd33
feat(compositor): pass Noto Emoji fallback to render_text_sdf_aligned…
AIEraDev Aug 26, 2026
4ab75ce
fix(TransformOverlay): layer selection passes through selected video'…
AIEraDev Aug 26, 2026
70e370a
fix(TextStyleSection): resolve font picker value against all aliases …
AIEraDev Aug 26, 2026
3a01c81
feat(native-text): synthesize font-weight and italic via bitmap trans…
AIEraDev Aug 26, 2026
9be0b1a
fix(textClip): pass fontStyle and lineHeight to measureTextInk; fix m…
AIEraDev Aug 26, 2026
899497c
fix(presetStore): retire built-in 'Neon Glow' preset with migration g…
AIEraDev Aug 26, 2026
43dac45
fix(native-preview): scale-to-fit text texture inside bounding box be…
AIEraDev Aug 26, 2026
ef0a7c9
fix(TextStyleSection): constrain gradient picker trigger and reformat…
AIEraDev Aug 26, 2026
ed1ef5d
refactor(TextAnimationControls): unwrap from PropertySection — caller…
AIEraDev Aug 26, 2026
c68073f
refactor(properties): delete TextModeSelector component and remove it…
AIEraDev Aug 26, 2026
8c21e73
chore(i18n): remove 'Colors & Effects' key from zh-TW and zh-CN local…
AIEraDev Aug 26, 2026
e75f651
fix(textClip): multiline height, fontStyle passthrough, empty text fa…
AIEraDev Aug 26, 2026
48de501
style(TransformSection): reformat long lines to multi-line Prettier s…
AIEraDev Aug 26, 2026
d38ffcb
chore(pnpm-lock): update lockfile
AIEraDev Aug 26, 2026
859435b
perf(prefetch): reduce look-ahead to 1 job within 3s horizon; block w…
AIEraDev Aug 26, 2026
628a8e0
release: v1.4.5
AIEraDev Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,64 @@ All notable changes to Clypra will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [1.4.5] - 2026-08-27

### 🔤 Native GPU SDF Text Engine & Font Registry
- **Native Signed Distance Field (SDF) text rendering** — migrated text rendering from browser canvas to GPU-accelerated wgpu/WGSL SDF shaders for razor-sharp typography at any preview scale.
- **Native font registry & WOFF2 support** — added `register_native_font` / `register_native_font_bytes` IPC commands; native core automatically decodes bundled WOFF2 and TTF fonts via `woff2-patched`.
- **Per-glyph font fallback & Noto Emoji** — `render_text_sdf_aligned_with_fallback` seamlessly resolves missing glyphs to the bundled `@fontsource/noto-emoji` fallback font.
- **Synthetic weight & italic transforms** — pre-SDF bitmap dilation and skewing synthesize bold and italic styles for single-weight fonts.
- **Contract v2 text snapshots** — extended native contract to v2, introducing text runs for timed karaoke/captions, vertical alignment, and multi-pass text shaders.
- **Native text templates & compound clip reuse** — text templates instantiate natively with full styling and shader passes preserved across export and preview.
- **Retired browser text rasterization** — desktop preview and export render text layers directly via wgpu, removing canvas raster roundtrips and DOM bottlenecks.

### 🔊 Professional Audio Engine & Timeline Controls
- **Extended native audio mixer** — Rust mixer supports fade-in / fade-out curves, keyframe interpolation, pan, channel routing, and pitch preservation.
- **Interactive audio envelope editor** — CapCut-style volume rubber bands and fade handle knobs directly on clips with cubic Bézier curves.
- **J/L-cut audio unlinking** — added `UnlinkAudioCommand` and `RelinkAudioCommand` for independent audio/video trimming and J/L cut workflows.
- **Atomic audio graph updates** — `replaceNativeAudioClips` IPC synchronizes audio edits in-place without restarting playback decoders.
- **Desktop Web Audio engine** — browser desktop audio path honors clip-level `preservePitch` with smooth time-stretching.
- **Waveform fade curves & solo button** — waveforms render overlaid fade curves with source-scoped peak caching and per-track solo controls.

### 🔄 Session-Safe Deferred Updates & Auto-Updater Pipeline
- **Two-stage update lifecycle** — separated update download from installation via `AutoUpdateManager` singleton; active editing sessions are never interrupted.
- **Save-before-update verification** — transport playback is paused and project state is atomically saved and verified before update restart; save failures abort installation safely.
- **Unified updater state** — synchronized update notifications, downloading status, and restart prompts across SettingsModal and UpdateBanner.

### 💾 Project Persistence Reliability & Crash Recovery
- **Atomic save pipeline** — verified candidate write (`.tmp`), generation rotation (`.bak`), and atomic file replacement with save receipt verification.
- **Transactional project hydration** — `validateAndMigrateProjectPayload` safely migrates legacy project fields; hydration failures roll back to the previously active project.
- **Failure-isolated recent projects** — corrupt or unreadable projects no longer hide valid projects and offer one-click recovery from verified backup files.
- **Crash recovery snapshot v2** — IndexedDB snapshot persists timeline gaps, markers, and schema version.
- **Cross-platform save hardening** — resolved Windows backup rename races and mobile Capacitor overwrite/rename paths.

### 🖥️ Dual-Monitor Workspace & Preview Layouts
- **Dual-monitor preview workspace** — added `PreviewMonitorWorkspace` supporting configurable side-by-side row and stacked column orientations for Source and Program monitors.
- **Independent preview panels** — `PreviewPanel` supports an explicit `mode` prop ("program" vs "source") regardless of global preview state.
- **Interaction-deferred transport context** — `SourcePreview` defers transport claiming until user interaction, preventing accidental playback hijacking on mount.

### 🎨 Design System, Semantic Tokens & Clip Palettes
- **Per-theme clip palettes** — clip colors derive from dynamic semantic design tokens (`--clypra-clip-*`) mapped 1-to-1 with UI themes.
- **Full palette fidelity** — eliminated heavy CSS saturation and opacity filters that dulled theme palette colors.
- **Live theme preview swatches** — Settings theme swatches preview actual built-in clip palette color schemes.

### 📐 Timeline Viewport Precision & Interactions
- **Sub-pixel alignment & 20px clip-start offset** — consistent timeline coordinates across Ruler, Clips, Playhead, Gaps, and mouse/wheel seek anchors via `TIMELINE_CLIP_START_OFFSET_PX`.
- **Refined visual hierarchy** — distinct A-roll (primary) and B-roll visual roles with dedicated track heights.
- **Visual track ordering guards** — visual tracks are prevented from being placed below the main video track, enforced across drag/drop and history undo/redo.
- **CapCut-style ruler** — clean ticks with compact `MM:SS` formatting under one hour and `HH:MM:SS` timecodes for longer projects.
- **Spring-animated zoom & canonical overview** — buttery-smooth anchored wheel zoom with spring physics; projects reliably open at standard minimum zoom.

### ⚡ Performance, Telemetry & Lookahead Pre-fetching
- **Zero-PII performance telemetry** — lightweight client and collector reporting stage timings and dropped frames with adaptive sampling.
- **Bounded lookahead pre-fetching** — predictive decoding warms upcoming clip assets within a 3-second horizon without saturating GPU presentation.
- **Event-driven paused render loop** — native preview pauses RAF loops while stopped, waking instantaneously on state and transform changes.
- **24-track compositor density** — validated multi-track layer pooling and composition stability up to 24 concurrent tracks.

### 🛠️ UI Polish & Workflow Shortcuts
- **Collapsible properties panel** — 44px collapsed rail with expand button and shortcut icons; `Alt+P` / `Cmd+Shift+P` toggle shortcut.
- **Media library shortcut** — `Cmd+B` toggles the media library drawer.
- **Transform overlay polish** — RAF-throttled gizmo tracking, enlarged hit targets, and diagonal resize cursors.

## [1.4.4] - 2026-08-25

Expand Down
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ A modern video editor built on Tauri v2, React 19, and Rust with hardware-accele

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) [![GitHub issues](https://img.shields.io/github/issues/AIEraDev/clypra)](https://github.com/AIEraDev/clypra/issues) [![GitHub stars](https://img.shields.io/github/stars/AIEraDev/clypra)](https://github.com/AIEraDev/clypra/stargazers) [![GitHub Sponsors](https://img.shields.io/github/sponsors/AIEraDev?label=Sponsors&logo=githubsponsors&color=EA4AAA)](SPONSORS.md)

[Features](#features) • [Architecture](#architecture) • [Installation](#installation) • [Development](#development) • [Contributing](CONTRIBUTING.md)
[Features](#key-features) • [Architecture](#architecture) • [⚡ Performance Telemetry](#-production-performance-telemetry--privacy) • [Installation](#installation) • [Development](#development) • [Contributing](CONTRIBUTING.md)

</div>

Expand All @@ -34,6 +34,20 @@ Clypra is a **free, open-source video editor** (MIT License) with professional-g
- 🎵 **High-Fidelity Audio & Waveforms** — Real-time peak + RMS waveform rendering, frame-accurate AV sync, and clip volume controls.
- 🎨 **Text & Visual Compositing** — Custom text styling, animated overlays, filters, and real-time canvas previews.
- 📦 **Multi-Format Import & Export** — Support for MP4, MOV, WebM, MKV, MP3, WAV, PNG, and high-quality ProRes/H.264 export.
- 🛡️ **Zero-Overhead Performance Telemetry** — Production-only frame timing analysis to optimize driver compatibility and cross-OS rendering with zero PII.

---

## ⚡ Production Performance Telemetry & Privacy

To ensure stutter-free 60 FPS playback and diagnose edge-case GPU driver regressions across diverse hardware configurations, Clypra includes a lightweight, non-blocking telemetry collector:

- **What We Collect**: High-level numerical timings (decode $\mu$s, compose $\mu$s, P95 seek latency, dropped frame counts, OS family, and GPU vendor/model).
- **Strict Privacy (Zero PII)**: Zero video frames, media assets, project titles, file paths, or personal user identities are ever accessed or sent.
- **Adaptive Sampling**: Smooth 60 FPS playback is sampled at 1%, while dropped frames ($>5\%$) and driver fallbacks are captured at 100% to isolate regressions without impacting playback frame pacing.
- **Analysis Hub**: Aggregated performance metrics are analyzed by engineers in the [Clypra Studio Admin Console](https://github.com/AIEraDev/clypra-studio) (`/studio/admin`).

For technical details, see the [Performance Telemetry Specification](docs/performance-telemetry.md).

---

Expand Down Expand Up @@ -65,7 +79,8 @@ Clypra is built with a native Rust/Tauri v2 core for hardware video operations,
```

For performance benchmarks and technical deep-dives, see the
[Native Performance Contract](docs/performance-contract.md) and the
[Native Performance Contract](docs/performance-contract.md), the
[Performance Telemetry Specification](docs/performance-telemetry.md), and the
[Program Preview Performance Runbook](docs/program-preview-performance-runbook.md).

---
Expand Down
3 changes: 3 additions & 0 deletions crates/clypra-native-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ license = "MIT"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
fontdue = "0.9"
parking_lot = "0.12"
woff2 = { package = "woff2-patched", version = "0.4" }
208 changes: 208 additions & 0 deletions crates/clypra-native-core/src/font_registry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
//! Font Registry — In-memory, thread-safe TrueType / OpenType font repository.
//!
//! Maps font identifiers (e.g. "inter-regular", "roboto-bold") to parsed `fontdue::Font`
//! instances and content hashes. The compatibility lookup retains a bundled default;
//! authoritative rendering uses `require_font` and fails on missing registrations.

use fontdue::{Font, FontSettings};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::io::Cursor;
use std::sync::{Arc, OnceLock};

/// Bundled compatibility font bytes (Inconsolata Regular).
pub const DEFAULT_FONT_BYTES: &[u8] = include_bytes!("../tests/test_font.ttf");
pub const DEFAULT_FONT_ID: &str = "default";
/// Internal face used for glyphs missing from the selected editor font.
pub const EMOJI_FALLBACK_FONT_ID: &str = "__clypra_noto_emoji";

/// Thread-safe registry of parsed TrueType / OpenType fonts.
pub struct FontRegistry {
fonts: RwLock<HashMap<String, (Arc<Font>, u64)>>,
missing_warnings: RwLock<Vec<String>>,
}

impl FontRegistry {
/// Creates a new font registry initialized with the bundled default font.
pub fn new() -> Self {
let registry = Self {
fonts: RwLock::new(HashMap::new()),
missing_warnings: RwLock::new(Vec::new()),
};

// Register default fallback font
let _ = registry.register_font(DEFAULT_FONT_ID, DEFAULT_FONT_BYTES);
registry
}

/// Register a font from raw TTF/OTF bytes.
/// Returns the 64-bit content hash of the registered font.
pub fn register_font(&self, font_id: &str, font_bytes: &[u8]) -> Result<u64, String> {
if font_id.trim().is_empty() {
return Err("Native font id must not be empty".to_string());
}

// The web editor bundles fonts as WOFF2. Convert at the native
// boundary so the same deterministic registration API accepts both
// editor assets and user-provided TTF/OTF files.
let decoded_woff2;
let parse_bytes = if woff2::decode::is_woff2(font_bytes) {
decoded_woff2 = woff2::convert_woff2_to_ttf(&mut Cursor::new(font_bytes))
.map_err(|error| format!("Failed to decode WOFF2 font '{font_id}': {error}"))?;
decoded_woff2.as_slice()
} else {
font_bytes
};
let font = Font::from_bytes(parse_bytes, FontSettings::default())
.map_err(|e| format!("Failed to parse font '{font_id}': {e}"))?;

// Compute 64-bit content hash of font bytes
let mut hasher = std::collections::hash_map::DefaultHasher::new();
font_bytes.hash(&mut hasher);
font_id.hash(&mut hasher);
let font_hash = hasher.finish();

let mut write = self.fonts.write();
write.insert(font_id.to_lowercase(), (Arc::new(font), font_hash));
Ok(font_hash)
}

/// Retrieve a font by identifier. If the requested font is not found,
/// falls back to the bundled default font and records a diagnostic warning.
pub fn get_font(&self, font_id: &str) -> (Arc<Font>, u64) {
let (font, hash, _) = self.get_font_with_status(font_id);
(font, hash)
}

/// Retrieve a font by identifier, returning `(font, font_hash, is_fallback)`.
/// `is_fallback` is `true` when the requested font was missing and fell back to default.
pub fn get_font_with_status(&self, font_id: &str) -> (Arc<Font>, u64, bool) {
let key = font_id.to_lowercase();
let read = self.fonts.read();
if let Some(entry) = read.get(&key) {
return (Arc::clone(&entry.0), entry.1, false);
}

// Record missing font warning
drop(read);
{
let mut warnings = self.missing_warnings.write();
let msg = format!("Requested font '{font_id}' is not installed; fell back to '{DEFAULT_FONT_ID}'");
if !warnings.contains(&msg) {
warnings.push(msg);
}
}

let read = self.fonts.read();
// Fallback to default font
if let Some(default_entry) = read.get(DEFAULT_FONT_ID) {
return (Arc::clone(&default_entry.0), default_entry.1, true);
}

drop(read);
// If even default was missing, re-register and return it
let _ = self.register_font(DEFAULT_FONT_ID, DEFAULT_FONT_BYTES);
let read = self.fonts.read();
let entry = read.get(DEFAULT_FONT_ID).expect("Default font must be registered");
(Arc::clone(&entry.0), entry.1, true)
}

/// Strict lookup used by authoritative desktop rendering. Browser-style
/// fallback is intentionally kept only for compatibility/diagnostic APIs.
pub fn require_font(&self, font_id: &str) -> Result<(Arc<Font>, u64), String> {
let key = font_id.to_lowercase();
self.fonts
.read()
.get(&key)
.map(|entry| (Arc::clone(&entry.0), entry.1))
.ok_or_else(|| format!("Native font '{font_id}' is not registered"))
}

/// Retrieve all missing font warnings accumulated during font lookups.
pub fn get_missing_font_warnings(&self) -> Vec<String> {
self.missing_warnings.read().clone()
}

/// Clear accumulated missing font warnings.
pub fn clear_missing_font_warnings(&self) {
self.missing_warnings.write().clear();
}

/// Checks whether a specific font is registered.
pub fn has_font(&self, font_id: &str) -> bool {
let key = font_id.to_lowercase();
self.fonts.read().contains_key(&key)
}

/// Returns list of all registered font IDs.
pub fn list_fonts(&self) -> Vec<String> {
self.fonts.read().keys().cloned().collect()
}
}

impl Default for FontRegistry {
fn default() -> Self {
Self::new()
}
}

static GLOBAL_FONT_REGISTRY: OnceLock<FontRegistry> = OnceLock::new();

/// Process-global font registry singleton.
pub fn global_font_registry() -> &'static FontRegistry {
GLOBAL_FONT_REGISTRY.get_or_init(FontRegistry::new)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn default_font_is_always_registered() {
let reg = FontRegistry::new();
assert!(reg.has_font("default"));
let (font, hash, is_fallback) = reg.get_font_with_status("default");
assert!(hash > 0);
assert!(!is_fallback);
assert!(font.glyph_count() > 0);
}

#[test]
fn unknown_font_falls_back_to_default_and_records_warning() {
let reg = FontRegistry::new();
let (default_font, default_hash) = reg.get_font("default");
let (fallback_font, fallback_hash, is_fallback) = reg.get_font_with_status("non-existent-font-1234");
assert_eq!(default_hash, fallback_hash);
assert!(is_fallback);
assert_eq!(default_font.glyph_count(), fallback_font.glyph_count());

let warnings = reg.get_missing_font_warnings();
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("non-existent-font-1234"));
}

#[test]
fn register_custom_font() {
let reg = FontRegistry::new();
let hash = reg.register_font("inconsolata-custom", DEFAULT_FONT_BYTES).unwrap();
assert!(reg.has_font("inconsolata-custom"));
let (_, fetched_hash, is_fallback) = reg.get_font_with_status("inconsolata-custom");
assert_eq!(hash, fetched_hash);
assert!(!is_fallback);
}

#[test]
fn strict_lookup_rejects_unregistered_font() {
let reg = FontRegistry::new();
let error = reg.require_font("missing-desktop-font").unwrap_err();
assert!(error.contains("not registered"));
}

#[test]
fn global_font_registry_is_singleton() {
let r1 = global_font_registry() as *const _;
let r2 = global_font_registry() as *const _;
assert_eq!(r1, r2);
}
}
Loading
Loading