New Features
<media-surface>— the dynamic texture channel: a new element compositing textures produced outside the widget tree (video decoders, camera pipelines, external renderers like mpv) into the layout like any widget, with a stable Zig-tier producer API (runtime.acquireMediaSurfaceProducer) pushing RGBA8 frames from any thread — latest-wins, damage-tracked, paced by the compositor's presented-frame clock.- Pushes wake an idle compositor: a push staging new bytes requests one coalesced frame through the platform's thread-safe cross-thread frame request (the automation watcher's wake path), so video keeps playing in an idle demand-driven app and a late-starting producer is adopted promptly; damage-skipped and stale-handle pushes wake nothing, and teardown disarms the binding under the same fence the wake call holds, so an orphaned producer can never wake a dead host.
- Markup, both engines, and tooling:
surface="{binding}"binds the model-owned u64 surface id in the runtime-image-id grammar (binding-only, required, media-surface-scoped with teaching errors), wired through the validator, the compiled and interpreter engines,native check's model contract, LSP hover docs, and a docs component page plus a "Media Producers" recipe. - Deterministic by policy: texture contents are presentation chrome — goldens, reference screenshots, and session fingerprints see only the surface's id-derived placeholder, so a session recorded with a live producer replays fingerprint-identical with no producer attached; live GPU hosts composite the real texture through the existing image upload pipeline.
- Reserved id namespace: bit 63 of the ImageId space now belongs to media-surface textures;
registerCanvasImagerejects ids with it set (error.InvalidImageId) so producer textures and registered images can never collide. - Cover fit stays inside the frame: a
cover-fit media surface carries the image widget's rectangular clip around its texture draw, so the fit-expanded texture can never paint over siblings on hosts that only mask corner radii. - Adopted-texture memory is on-demand: each channel's texture buffer is one lazy frame-budget allocation from the new
Runtime.Options.allocatorat first adoption (freed by the newRuntime.deinit), so a runtime with no media producers carries zero media-texture bytes — an embedded pool would have put 32 MiB in every Runtime (measured on the docs wasm preview host: 169.5 MB → 137.5 MB per component tile). The allocator freezes into the runtime at init: mutatingoptions.allocatoron a live runtime never retargets ownership, so allocations and their frees always pair on one allocator. - Trackpad pinch reaches apps: pinch-to-zoom now flows from the macOS host (
magnifyWithEvent:) through phase-explicitpinch_begin/pinch_change/pinch_endinput events into a view-global app channel — Zig cores declareOptions.on_pinch, TypeScript cores exportpinchMsg(pinch)withPinchPhase/PinchEventin@native-sdk/core/events; each change carries a multiplicative delta (cumulative gesture scale is the product of1 + scale, applied memorylessly —zoom *= 1 + scale) and the pointer anchor rides view-localx/y(the pointer location during the gesture — zoom-at-cursor anchoring, not a between-the-fingers midpoint); every event names its source window and view (window_id/labelin Zig,windowId/labelin TypeScript), so multi-window apps tell pinches apart; a terminal Ended/Cancelled event that still measured a nonzero delta arrives as one last change before the end, so the product always matches what the OS reported. On macOS the delta is AppKit's raw per-eventNSEvent.magnification, forwarded untransformed: raw magnification IS the multiplicative per-event delta — the convention every browser engine ships — so the product of1 + scaleis the zoom users already experience for the same gesture in Safari and Chrome. The one guard is a per-event floor: a single event's magnification at or below -1 (a zoom inverting through zero scale — physically impossible, only a driver glitch could report it) clamps just above -1, so every emitted factor stays positive. Windows precision-touchpad and GTK gesture sources are staged follow-ups. widget-pinchautomation verb:native automate widget-pinch <view-label> <scale> [x y]drives the real pinch event stream without a trackpad (<scale>is the gesture's FINAL multiplicative zoom — one change carryingscale - 1, anchor point defaulting to the view center), journaled like every synthesized input so recorded sessions replay the identical zoom. Automation protocol bumps to v7.- Session journal v5: gpu-surface input records gain the pinch
scalefield and the pinch kinds; readers refuse v4 journals loudly in both directions, per the format's skew discipline — re-record sessions with this build.
Bug Fixes
- Registered CJK fonts render dense glyphs: glyph outline budgets are now sized from real CJK faces (Noto Sans JP/SC/TC/KR measured; 1024 points / 128 contours per glyph), so everyday dense kanji like 鬱 ink as real outlines instead of notdef blocks; the per-font registration size bound rose to 24 MiB so full CJK faces register.
- Glyph complexity validates at registration, not render: a face whose
maxpdeclares glyphs denser than the outline budgets — simple maxima and flattened-composite maxima (maxCompositePoints/maxCompositeContours) alike — is refused at registration with a teaching that names its numbers against the budgets (error.FontExceedsGlyphBudgets), instead of silently degrading individual glyphs to blocks at render time. - Font bytes are heap-allocated on demand: the registry copies each registered file into an exact-size allocation from the runtime's new
Options.allocator(freed by the newRuntime.deinit), so the 24 MiB bound is validation, not a storage reservation — a runtime with no registered fonts carries zero font bytes, where a reservation-shaped pool would have embedded 192 MiB in every Runtime (measured on the docs wasm preview host: 313.5 MB → 121.5 MB per component tile). EmbeddedApp.deinitcompletes the embed lifecycle: direct embedders end an embedded app withdefer embedded.deinit()(idempotent), which returns the runtime's heap-owned registrations — without it, a host creating and destroying apps in one process leaked the registered font storage per cycle. The wrapper hosts and the C ABI'snative_sdk_app_destroyroute their teardown through the same deinit, one lifecycle owner.- Glyph raster budgets are derived from the registration gate: the vector core's glyph-fill capacities (
GlyphRasterizer: 18,560 edges/crossings, flattening clamped at 16 segments per curve) are computed from the outline budgets registration admits, so a truthfully-declared budget-maximal glyph — a 1024-point zigzag contour included — rasterizes at any size instead of hittingVectorPathTooComplexand degrading to the block fallback the gate promises cannot happen; the clamp binds only above ~128-px ems and keeps the polyline within 0.2% of the em, so existing renders are byte-identical. - Single-line fields never hold or paint line breaks: pasting multi-line text into an input, text field, search field, or combobox now strips the line breaks at the edit seam (the HTML value-sanitization rule — lines join with nothing between them), covering clipboard paste from the shortcut and the context menu, typed and automation
text_input, and IME composition, with the app'son_inputhearing the same sanitized bytes the editor applied; a paste of only newlines inserts nothing. - Defensive render containment: a single-line value that still holds a
\n(a model-set value, an old journal) now paints as one line — breaks present as spaces — under a forced content-rect clip, so text can never escape the field's rounded border on any renderer.