Skip to content

feat: v3 - #152

Merged
triniwiz merged 75 commits into
masterfrom
v3-v8
Sep 28, 2026
Merged

triniwiz merged 75 commits into
masterfrom
v3-v8

Conversation

@triniwiz

Copy link
Copy Markdown
Member

No description provided.

triniwiz added 30 commits August 4, 2026 00:37
Nx's CopyAssetsHandler only reads the root .gitignore/.nxignore (see the
TODO in copy-assets-handler.js), so it never applies the nested
nativescript-v8/Headers/.gitignore when copying vendored V8 headers into
dist/.../NativeScriptV8/Headers -- that's what let v8.h etc. through in the
first place. But the .gitignore file itself matches the "**/*" glob and
gets copied into dist alongside the headers it was written to protect.
npm pack, run against that dist output, then reads the copied .gitignore
and re-excludes everything under include/* except the libffi negations,
silently stripping v8.h/v8config.h/etc. right back out of the published
tarball (confirmed against the published 3.0.0-alpha.3, which is missing
them).

Adding an explicit asset-glob "ignore" for the .gitignore file itself
(a feature CopyAssetsHandler already supports) keeps it out of dist, so
npm pack no longer has anything to re-exclude with.
Brings in #144-#149. Conflict resolution notes:

* #147 (V8 14 native bridge) overlapped almost entirely with the V8 14
  migration this branch already carries. Both sides fix the same API break;
  master does it with dual-version shims (canvas::GetAlignedPointer,
  canvas::Receiver, CANVAS_FAST_FUNCTION, #if V8_MAJOR_VERSION >= 14) so one
  tree can build against V8 10.3 and 14, while this branch targets a single
  vendored V8 14.9 (tools/scripts/download-v8.sh) and calls the native APIs
  directly. 118 of the 127 conflicting sources were that difference alone --
  after rewriting master's shims into this branch's idiom, 112 compared
  byte-identical -- so those keep this branch's spelling and the now-unused
  shim block is dropped from Common.h.

* Master's shims also compile fast API calls out on V8 >= 14
  (CANVAS_FAST_FUNCTION -> v8::CFunction{}, c_function -> nullptr). V8 14.9
  still declares both FunctionTemplate::New(..., const CFunction*) and
  NewWithCFunctionOverloads, and this branch's fast paths are built against
  it, so taking that would have silently disabled every fast call in the
  binding layer. Helpers.h keeps this branch's version; the four
  CANVAS_FAST_FUNCTION call sites that auto-merged into
  OES_vertex_array_objectImpl and WEBGL_draw_buffersImpl -- no conflict was
  raised for those -- are restored to v8::CFunction::Make.

* Master's *Array fast overloads are all guarded #if V8_MAJOR_VERSION < 14,
  so they are dead on 14 and equivalent to this branch having removed them.

* #149 (ImageData double free) applies unchanged. canvas_native_image_data_get_data
  borrows its argument and returns a U8Buffer holding a second refcounted
  handle to the same pixels, so ImageDataBuffer must release only the buffer --
  ~ImageDataImpl already releases the ImageData. The comment is reworded from
  master's, which described the buffer as owning a clone of the pixel storage;
  it is a shared handle, and that is what makes the JS data view live.

* Package versions stay on the 3.0.0-alpha line.
* canvas-release.aar keeps this branch's binary; it predates the Android
  render fixes in 638a265 and needs rebuilding from the merged sources.
SetFastMethodWithOverLoads took the overload set as `const v8::CFunction *`
and sized it with NUM(&method_overloads). That macro is
sizeof(a)/sizeof(*a) applied to a pointer-to-pointer, so it evaluated to 1
at every call site regardless of how many overloads the array held.

Across the 18 registration sites, 41 overloads are declared and 18 were
registered. The rest never reached V8, so those argument shapes always took
the slow FunctionCallback path:

  fill              1 of 4    clip, draw, isPointInPath   1 of 3
  bufferData        1 of 3    putImageData, stroke, roundRect,
  setTransform, isPointInStroke, bindBuffer, useProgram,
  bufferSubData, pixelStorei                              1 of 2

bindTexture, bindFramebuffer and bindRenderbuffer are the worst of these:
their null variant is listed first, so the registered overload is the one
for a null binding and the common non-null bind had no fast path.

Taking the array by reference deduces the extent at each call site, so no
call site changes. All 17 overload sets are declared as arrays in the same
translation unit, so N is available at every one.
Two problems kept `cargo check`/`cargo test` from running on the workspace,
which is why the existing unit tests had never been executed.

gpu/mod.rs compiled `metal` on every Apple target, but that module imports
objc2-metal, which is an optional dependency only enabled by the `mtl`
feature. Any Apple build without `mtl` failed to resolve the crate instead
of simply omitting Metal support, so gate the module on the feature too.

With that fixed, `--features mtl` then failed on its own: objc2-metal ships
one feature per generated header, and the set enabled here was missing
MTLBlitCommandEncoder, MTLCommandEncoder, MTLBuffer, MTLResource and
MTLTypes -- all of which gpu/metal.rs uses (blit encoding, buffer creation,
MTLOrigin/MTLRegion/MTLSize). Add them.

canvas-core still needs `2d` for skia-safe; the bare no-feature build fails
separately on that and is left alone here.
triniwiz/rust-skia d4c4011 -> 91bf15b, which is skia-safe 0.99.0 -> 0.101.0
and carries Skia m152. The fork's visionOS platform support is on that line
(d4c4011 is an ancestor of it) and picks up a small fix, so nothing is lost
by moving to the branch tip.

The only API break affecting this workspace is FontMgr::new_from_data, which
now takes an SkData rather than a byte slice; both call sites in
global_fonts.rs wrap their bytes in Data::new_copy, which is what the old
signature did internally.

wgpu is unchanged: triniwiz/wgpu's trunk is exactly the rev already pinned
here, so there is nothing newer to move to without first updating that fork.
… them in CI

Nothing in CI ran tests, and the four crates that already had #[test]
modules could not be built on the host at all, so none of them had ever
executed. With the canvas-core build fixed they do; `make test` and two new
CI jobs run them.

ImageData ownership (crates/canvas-c/src/c2d/image_data.rs)
  ImageData is a manually refcounted handle -- Clone copies the raw pixel
  pointer and bumps an Arc, and the storage is freed only when the last
  handle drops. canvas_native_image_data_get_data borrows and hands back a
  second handle to the same pixels, which is what makes the JS `data` view
  live and what #149 turned on. The tests pin all three parts: the buffer
  aliases the ImageData's pixels, releasing the buffer leaves the ImageData
  usable (the double free #149 fixed), and the buffer keeps the pixels alive
  when the ImageData handle is dropped first.

V8 bridge invariants (tools/tests/check-v8-bridge-invariants.py)
  Static checks over the bridge sources for things the compiler accepts but
  that break at runtime or silently cost performance: internal-field and
  external-pointer accessors that are missing their V8 14 tag (an untagged
  read returns null rather than failing to build), V8 14-removed APIs coming
  back, dual-version shims re-entering a branch that ships one vendored V8,
  and the overload helper regressing to a bare pointer -- which is exactly
  how 23 of the 41 fast-call overloads went unregistered. Both of the
  regressions this merge introduced by auto-merge would have been caught
  here.
…ffers work

Ten call sites built a std::vector with reserve(n), filled it through
data() (or operator[]), then passed data() together with size(). reserve
only sets capacity, so size() stayed 0 and every one of these handed the FFI
an empty array; writing through data()/operator[] past size() is also out of
bounds, which any hardened or debug STL would trap.

The affected entry points silently did nothing:

  ctx.setLineDash([...])                     slow path and fast path
  ctx.roundRect(x, y, w, h, [radii])         slow path and fast path
  path2d.roundRect(x, y, w, h, [radii])      slow path and fast path
  WEBGL_draw_buffers.drawBuffersWEBGL([...]) fast path
  gl2.drawBuffers([...])                     fast path
  gl2.invalidateFramebuffer(...)             fast path
  gl2.invalidateSubFramebuffer(...)          fast path

The 2D ones are the worst of these because the slow path is affected too, so
setLineDash and roundRect-with-radii never worked; the WebGL ones only break
once V8 tiers the call up, which makes them look intermittent.

resize(n) gives the elements the vector needs to report the right length.
Path2D::FastRoundRectArray additionally passed nullptr as the copy
destination instead of its own buffer, so it had nothing to send either way.

check-v8-bridge-invariants.py grows a check for the pattern; it flags a
reserve whose buffer is later passed as data(), size() with nothing having
grown it, and leaves the 27 legitimate reserve + push_back sites alone.
triniwiz/wgpu 7e0f39f -> 8bf3e5f, which is the v30.0.0 tag on the fork's
trunk: wgpu-core/hal/types 29.0.0 -> 30.0.0.

Not the branch tip (b421632). That merge brings in post-v30.0.0 upstream
trunk, where the id-based Global architecture is gone -- wgpu-core no longer
has the global, hub, identity or registry modules, and resources are reached
through Arc handles instead of typed ids. crates/canvas-c/src/webgpu is
built entirely on that API: ~110 sites bind a Global and call 101 distinct
methods on it across 29 files. Porting to the new model is a real piece of
work and wants to be its own change; v30.0.0 still has Global, so it gives
the version bump without it.

The five breaks at v30.0.0 are all mechanical:

  * Limits gained max_buffers_and_acceleration_structures_per_shader_stage,
    max_ray_dispatch_count and max_ray_recursion_depth. Canvas exposes no ray
    tracing -- the acceleration-structure limits beside these are already
    pinned to 0 -- so the ray limits stay 0; the combined limit is documented
    as the sum of the storage, uniform and vertex buffer limits and is
    derived rather than copying wgpu's default of 28.
  * SurfaceConfiguration gained color_space. Auto is the default and is
    defined to reproduce the pre-30 behaviour.
  * RenderPassDescriptor now takes depth_stencil_attachment and
    timestamp_writes by value rather than by reference.
  * render_bundle_encoder_finish borrows the encoder (&mut) instead of
    consuming the Box. The Box stays owned here and drops at the end of the
    function, so the encoder is still freed exactly once.
triniwiz/rust-skia 91bf15b -> 1eafe30 (skia-safe 0.101.0 -> 0.153.4). No
source changes needed: FontMgr::new_from_data, fixed up in the 0.101 bump,
is the only break either step introduced, and canvas-2d, canvas-core and
canvas-svg all build clean.

The fork's platform support grew tvOS alongside visionOS
(skia-bindings/build_support/platform/{tvos,visionos}.rs), which lines up
with the tvOS targets #144 added to the Makefile.
Brings in #150 and #151.

#151 (window timer / EventTarget aliases) is TypeScript only and merged
clean.

#150 (TextMetrics for empty measureText) conflicted on the one line this
branch spells differently: master reads the length through its
canvas::Utf8Length shim, this branch calls Utf8LengthV2 directly. Kept this
branch's call and took master's fix, which is the removal of the
`if (text_utf8_len == 0) return;` early exit.

The rest of MeasureText is safe at length 0: prefix_len is at least 3, so
the scratch buffer is never empty, WriteUtf8V2 with a zero length writes
nothing, and the empty string measures to a zero-width TextMetrics rather
than returning undefined.
AudioContextNative is Foundation + AVFoundation over libopus/libvorbis with
no UIKit and no AVAudioSession, so nothing in the sources needed a
TARGET_OS_TV guard -- this is all build configuration.

* build_opus_deps_ios.sh cross-compiles the third_party libs for appletvos
  and appletvsimulator. tvOS gets the same treatment as visionOS: a target
  triple rather than -m*-version-min (it spells device vs simulator
  unambiguously) and no bitcode. arm64 only, matching the scope canvas uses
  in canvas-ios/pre-build.sh -- an x86_64 simulator slice would need an
  x86_64 build of every dep here.

* The Xcode project gains appletvos/appletvsimulator in SUPPORTED_PLATFORMS,
  TVOS_DEPLOYMENT_TARGET, and device family 3. Nothing else was needed: the
  header and library search paths already key off
  $(PLATFORM_NAME)-$(CURRENT_ARCH), so they find the new prefixes on their own.

* build.sh builds both tvOS slices and adds them to the xcframework, which
  now carries six: ios, ios-sim, xros, xros-sim, tvos, tvos-sim.

Two things had to be fixed to get there, both of which were already broken
for visionOS:

* The scheme had buildImplicitDependencies = YES, so Xcode matched -lvorbis
  against the libvorbis macosx/Vorbis.xcodeproj bundled in third_party and
  built it as an implicit dependency. That project is macOS-oriented, cannot
  resolve its own ogg headers under a cross-SDK build, and is redundant --
  the framework has no target dependencies and links the prebuilt archives by
  path. Turning it off fixes iOS, visionOS and tvOS alike.

* The visionOS and tvOS *device* steps passed ARCHS=arm64
  ONLY_ACTIVE_ARCH=NO. Device SDKs are arm64-only already, so it was
  redundant there; the simulator steps keep it, since their third_party
  archives are arm64-only.

Verified with cleared DerivedData: all six SDKs build without errors, the
tvOS binaries report platform 3 (tvOS) and 8 (tvOS simulator) with the
decoder symbols exported, and -create-xcframework accepts all six slices.

build_opus_deps_ios.sh also now checks each target actually produced the
archives the framework links. Every configure/make in it is `|| true`, so a
dep that fails leaves a prefix that looks fine until the link fails -- which
is exactly what a stale libogg tree did here.
Pins wgpu at the fork's trunk tip (b421632), where wgpu-core has no global,
hub, identity or registry modules. DOES NOT COMPILE YET -- 100 errors left,
listed below. Branch is deliberately separate from v3-v8, which stays on the
v30.0.0 tag and stays green.

Done -- the three structural changes:

1. Resource handles. All 19 wrapper structs now hold Arc<T> instead of a
   typed id: Arc<Device>, Arc<Buffer>, Arc<Texture>, Arc<Surface> and so on.
   CanvasWebGPUInstance wraps Arc<Instance> (Instance::new already returns an
   Arc, so the extra Arc::new is gone) and exposes .instance() where it used
   to expose .global().

2. Error model. wgpu-core now implements the WebGPU error model itself --
   Device::push_error_scope / pop_error_scope / on_uncaptured_error -- and
   every infallible create_* reports into it. Keeping our own scope stack
   beside it would mean those creates never reach our stack, so the FFI
   entry points delegate: push/pop go straight to the device, and
   set_uncaptured_error_callback installs a handler on the device that
   forwards to the registered C callback. ErrorSinkRaw stays for the
   device-lost path, which wgpu handles separately.

   handle_error/handle_error_fatal lose their first parameter -- it was the
   Global and was already unused (`_context`) -- which is what let ~100 call
   sites stop binding one.

3. Drops. 15 Drop impls whose whole body was a *_drop call are deleted;
   lifetime is refcounting now. Two kept a real job and were rewritten:
   CanvasGPUDevice still polls to idle before release, and CanvasGPUTexture
   still hands an acquired-but-never-presented swapchain image back via
   Surface::discard.

Also converted: the device create_* family, render/compute pass encoders,
and the 11 command::bundle_ffi::wgpu_render_bundle_* free functions, which
are now RenderBundleEncoder methods. Buffer is complete, including
map_async returning Option<SubmissionIndex> rather than a Result.

Left, by file:

  gpu_render_pass_encoder   36   arguments are Arc<T> where ids were passed
  gpu_canvas_context        28   surface configure/acquire/present path
  gpu_device                19   remaining create_* argument types
  gpu_command_encoder       14   begin_render_pass now takes a
                                 ResolvedRenderPassDescriptor, so the colour
                                 and depth attachments have to be resolved to
                                 Arcs before the call -- this one is a real
                                 signature change, not a receiver swap
  gpu_render_bundle_encoder 10
  gpu_queue                  9   TexelCopyTextureInfo moved out of
                                 wgpu_core::command
  gpu_compute_pass_encoder   9
  the rest                  ~9

None of this is exercised by a test. The repo has no GPU test and WebGPU
needs a device, so compiling is the only signal available here -- treat the
whole branch as unverified until it runs on hardware.
Error path is settled: the infallible create_*/encode methods report into
wgpu's device sink, which push/pop_error_scope now read, so our duplicate
Err(cause) -> handle_error blocks are gone rather than being rewired.

Shader compilation messages move with it: create_shader_module no longer
returns an error carrying them, so getCompilationInfo now reads
ShaderModule::compilation_info. The new SourceLocation counts UTF-8 bytes
where GPUCompilationMessage is specified in UTF-16 code units, so
from_compilation_message keeps the conversion the error-based path did.

Also: Limits gained per-stage storage buffer/texture caps, DeviceDescriptor
gained default_queue, TextureViewDescriptor gained swizzle, index/vertex
buffer sizes are Option<u64> rather than Option<NonZeroU64>, and
Buffer::map_async signals 'queued, still needs polling' as Some(index)
rather than Ok.

100 -> 74 errors. gpu_canvas_context (27) and gpu_command_encoder (14) are
what is left of substance.
… wgpu

gpu_canvas_context was the last file: the surface is an Arc<Surface> now, so
create/configure/get_current_texture/present/discard/get_capabilities are all
methods on it, and surface_drop is gone -- assigning a new Arc over the old
one is the release. Surface-backed textures compare by Arc::ptr_eq rather
than by id equality.

Whole crate is at 0 errors.
The Metal backend's reported alpha modes changed between wgpu v30.0.0 and
trunk: v30.0.0 advertises [Opaque, PostMultiplied], trunk advertises
[Opaque, PreMultiplied]. The demo hardcoded PostMultiplied, so on trunk
surface configuration fails validation and nothing renders.

Worth flagging beyond the demo: this is a behaviour change any app hits.
Anything asking for PostMultiplied on Metal has to switch, or query
getCapabilities and pick a supported mode instead of hardcoding one.
Backends do not agree on how they name premultiplied compositing, and the
name changes between wgpu releases for the same physical behaviour: Metal
reported PostMultiplied up to v30.0.0 and reports PreMultiplied after it. A
caller that hardcodes either one is one wgpu bump away from a surface that
fails validation and never presents -- which is exactly what the playground
demo hit.

configure now matches the requested mode against the surface's capabilities
instead of passing it through. An unsupported Pre/PostMultiplied falls back
to the other spelling (same intent, different name), then to Opaque, then to
whatever the surface does support, with a warning. Auto is left alone since
wgpu accepts it everywhere.

The JS layer already did this in GPUCanvasContext.configure, so apps going
through the JS API were never exposed; direct FFI callers were. Doing it in
the native layer too means both paths behave the same and neither depends on
which names the backend happens to report.

CanvasGPUDevice now keeps its Arc<Adapter> to make that query possible --
wgpu-core's Device::adapter is pub(crate), so there is no route back to the
adapter from a device otherwise.

The demo goes back to requesting PostMultiplied, which now works on trunk.
The cube covers render pipelines, render passes, surface acquire/present and
queue submit. Compute, render bundles, copyExternalImageToTexture and
toDataURL readback were left compile-verified only after the Global removal,
which is the weakest part of that port to leave unexecuted.

webgpu_smoke runs each once against the live device and checks a result
rather than just a non-null pointer:

  compute pass      dispatches values[i] = i*2+1 over a storage buffer and
                    reads the numbers back
  render bundle     records a fullscreen triangle, replays it into an
                    offscreen pass over a red clear, checks the pixel is the
                    bundle's green
  external image    uploads known magenta and reads the texture back
  toDataURL         checks the PNG data URL prefix and length

It also surfaced that the demo only rendered on WindowEvent::Resized --
request_redraw was commented out -- so "it ran for 25s" previously meant an
idle window, not frames. RedrawRequested now renders and re-arms, which is
also what gives the acquire/present and Arc release paths real repetition.
Exercises compute passes, render bundles, copyExternalImageToTexture and
toDataURL readback once against the live device, checking results rather
than just non-null pointers. These are the paths the spinning-cube demo
never reaches, so they had no executable coverage at all.

Also fixes the demo only rendering on WindowEvent::Resized: request_redraw
was commented out, so the window sat idle after the first frame.
RedrawRequested now renders and re-arms.

Same suite passes identically here and on the wgpu Arc-handle migration
branch, which is what makes it useful as a before/after oracle.
…153)

* feat(svg): real-DOM SVG with SMIL, GPU rendering and shared sources

- canvas-svg crate: live SvgDocument DOM (nodes, attrs, frame scheduling)
  with its own SMIL engine, plus a C ABI crate (canvas-svg-c)
- GPU backends: GL/Vulkan on Android (TextureView/SurfaceView hosts),
  Metal on iOS/tvOS/visionOS, with context-loss recovery and CPU fallback
- Real element classes in @nativescript/canvas-svg backed by native nodes;
  same-src views share one document, clock and SkPicture
- JSI module install on both platforms; tSpan + CharacterData in polyfill
- canvas-core: Vulkan present/rebuild no longer panics on surface loss,
  drop leaked autorelease pools in Metal context
- bump skia fork, packages to 3.0.0-alpha.12

* chore(svg): fix stale demo comment on CSS animations

* feat(svg): SVG views and markup as canvas image sources

- drawImage/createPattern/drawAtlas, texImage2D/texSubImage2D/texImage3D,
  createImageBitmap and copyExternalImageToTexture accept an Svg view
  (or polyfill <svg> element): the current frame, rasterized at the size
  it lands at for drawImage, reused until the document changes
- ImageAsset.loadSvg/loadSvgSync for markup, paths, URLs and views, with
  width/height/scale/time options; <img> SVG sources use the same loader
- canvas-svg registers the provider on import, so canvas keeps no
  dependency on it
- ImageAsset.loadFromBytesSync takes a premultiplied flag; SVG rasters
  were being premultiplied twice (canvas aar rebuilt for the JSI change)
- drop the dead _svg branches that called the removed getBitmap()

* fix(svg): rebuild stale Android binaries, stop stale bitmap under GPU frames

- the Android aar and jniLibs were built before setText and the current
  frame/SMIL/GPU code, so text mutations threw on Android; rebuilt all
  four ABIs from current source (JSI lib now ships for every ABI) and
  regenerated the cbindgen header
- NSCSVG.onDraw skipped the bitmap only for the single-threaded context,
  and nothing invalidated once a GPU context came up, so Android kept
  replaying an earlier bitmap frame under the GPU one (doubled shapes,
  removed nodes still visible)
- exclude libNativeScript.so from the aar, as canvas does

* feat(svg): shared render thread, threaded by default; fix canvas GL context use

- one process-wide render thread serves every threaded Svg view (was a
  thread and wakeup per view); threaded is now the default on both
  platforms, as browsers raster off the main thread
- canvas-2d binds its GL context before every draw and before texture
  promotion: Skia uploads a pattern's image while recording the draw, so
  with two GL canvases it landed in the other context (black or wrong
  pattern on Android)
- GL resize keeps its Skia context instead of creating a second one on
  the same EGL context
- SVG snapshots bypass the views' shared frame cache, which only holds
  four sizes
- make exports the SDKROOT and deployment targets Xcode uses: cargo
  tracks them, so every make/Xcode switch rebuilt all Apple slices from
  scratch (~52 min for canvas); a no-change round is now seconds
- canvas and canvas-svg binaries rebuilt for all Android ABIs and Apple
GPUDevice.importExternalTexture wraps the current video frame as a
single-plane GPUExternalTexture, so shaders can sample it through
texture_external. Metal only: wgpu has no Vulkan support yet, so
Android throws NotSupportedError.

- canvas-c: import entry point, external texture bind group resource
  and layout entry; EXTERNAL_TEXTURE is enabled wherever the adapter
  supports it
- V8 bindings: GPUExternalTextureImpl, device method, bind group parsing
- canvas-media (iOS): play() and the playing event now wait for a
  decoded frame, a pause() before then rejects play() with AbortError,
  and the loadeddata check no longer consumes the first frame
- demo: videoUploading uses the video element directly
- rebuilt CanvasNative.xcframework and canvas-release.aar
SvgDocument.add_stylesheet (addStylesheet in JS) applies @Keyframes rules from
CSS supplied separately from the SVG, e.g. a CodePen CSS panel, and restarts
the animation clock if nothing was running. Only #id selectors apply.

Rebuilt CanvasSVG.xcframework (including the x86_64 simulator slice, which
had been stale) and canvassvg-release.aar.
* feat(windows): D3D12 and ANGLE backends in canvas-core, patched napi-sys

canvas-core gains the two Windows GPU backends the Node-API port needs:

- gpu::d3d: a per-thread Direct3D 12 device + queue shared by every 2D
  canvas (Skia Ganesh D3D), with WARP (CANVAS_FORCE_WARP) and debug-layer
  (CANVAS_D3D_DEBUG) switches.
- gpu::gl on Windows: a GLContext over a dynamically loaded ANGLE
  (libEGL/libGLESv2 next to the binary, or CANVAS_ANGLE_DIR) on D3D11,
  with the same API as the Android EGL context plus D3D11 client-texture
  surfaces for presenting into a composition swapchain.

Examples d3d_offscreen and angle_offscreen render and read back on both.

Also vendors napi-sys 3.3.2 with a host-module lookup patch (Node-API
hosts that export napi_* from a DLL, e.g. NativeScript Windows' runtime),
and adds a release-napi profile that unwinds panics into JS exceptions.

* fix(windows): build canvas-2d, canvas-webgl and canvas-c for Windows

Platform branches that only knew Apple and Android:

- Pixel readback formats and the default WebGPU surface format now treat
  Android as the RGBA exception rather than listing Apple as BGRA, so
  Windows (and later Linux) default to BGRA surfaces / RGBA readback.
- The view-based WebGL/2D-GL constructors are Apple-only; Windows
  renders GL offscreen on ANGLE and the host presents it.
- Skia's GL interface is loaded through ANGLE's eglGetProcAddress on
  Windows instead of the native (WGL) interface.

* feat(canvas-napi): Node-API rewrite on napi-rs 3

canvas-napi becomes the Node-API build of the canvas native module: it
installs the same global.CanvasModule the V8 bindings do, so
packages/canvas can drive it on Node-API hosts (NativeScript Windows
first, later macOS/Linux). The standalone macOS Node/Deno host layer
and the napi-rs 2 sources are removed.

Classes are raw Node-API callbacks (util::class macros) that coerce
arguments like the V8 bindings/WebIDL and dispatch on a wrapped-object
header (util::native) instead of Node-API type tags. napi-rs 3.13
provides registration; util::task (pool + threadsafe functions) and
util::frame (dirty tracking, frame-end flush) replace the platform
run loops and display links the V8 bindings use.

Path2D and the shared types for 2D, images and WebGL are in place; the
remaining classes are ported against PORTING.md.

* feat(canvas-napi): restore the napi-rs bindings, migrate to napi-rs 3

Brings back the existing napi-rs binding code (c2d, WebGL, WebGL2,
WebGPU, images, text) in place of the raw-callback rewrite, migrated
to napi-rs 3.13 and the current canvas-c:

- napi-rs 3 idioms: class arguments by reference, classes returned by
  value, Unknown/Object with lifetimes, ThreadsafeFunction parameters,
  zero-copy typed arrays; crate::js::ToJs for argument-dependent
  returns and AnyArrayBuffer for ArrayBuffers inside Either.
- canvas-c drift: colour spaces, has_current_texture, texture view
  usage, pipeline constants, optional depth-stencil fields, wgpu 30
  limits, new vertex/texture formats.
- Fixes: lineCap round/butt were swapped; ImageData wrapped a Box with
  Arc::from_raw; fillStyle/strokeStyle getters leaked the style;
  ImageAsset loaded encoded bytes as raw pixels and vice versa;
  createImageBitmap decoded ImageData pixels as an encoded image and
  ignored the source rect; drawImage(gpu canvas) panicked; GPU_INSTANCE
  was a const (a new wgpu instance per use); getMappedRange buffers
  dangled after unmap.
- Apple-only view/layer factories removed; surfaces come from the
  desktop host.

The 2D context now matches what packages/canvas calls: int fill rules,
textBaseline and globalCompositeOperation; __toDataURL/__getPointer/
__makeDirty/__startRaf/__stopRaf/__resize/__createPatternWithNative;
drawPaint/drawPoint(s)/drawAtlas/fillOval; drawImage/createPattern take
the native source objects; create2DContext/create2DContextWithPointer
(the latter non-owning). Drawing marks the context dirty for the
desktop frame flush (crate::frame, CanvasModule.__flushAll). Exports are
installed as globalThis.CanvasModule.

* feat(canvas-napi): images, text, DOMMatrix and module functions match packages/canvas

Aligns the rest of the CanvasModule surface with what packages/canvas
calls and the V8 bindings register:

- DOMMatrix: a-f/m11-m44 accessors; translate/scaleNonUniform/rotate/
  skewX/skewY (from the matrix passed last, or this) and their *Self
  forms; multiplySelf/premultiplySelf take the native matrix or its
  wrapper (MatrixArg).
- Path2D.addPath(path, transform?) applies the matrix (self-add copies
  first); roundRect radii optional. CanvasPattern.setTransform(DOMMatrix).
- ImageAsset: fromUrlSync/fromUrlCb/fromFileCb/fromBytesCb/
  fromEncodedBytesCb, premultiplied fromBytesSync, __addr/__getRef.
  Callbacks run on a worker pool and return through threadsafe
  functions; byte arguments are read in place and kept alive meanwhile.
- ImageBitmap: fromAsset (shares the image), __addr/__getRef, 0x0 after
  close(); options read field by field like the C++ HandleOptions.
- TextEncoder/TextDecoder: lowercase encoding; decode reads any buffer or
  view in place; decodeAsync.
- Module functions (module.rs): createImageBitmap in all four argument
  forms, readFile, getMime, __addFontFamily, __addFontData, __base64*.

Fixed rather than copied from the C++: the crop rect is applied to
encoded bytes; failed decodes report an error; zero-size crop checks
fire; views read only their own byte window; rotateSelf without a centre
no longer yields NaN; decode keeps embedded NULs.

__test__/images.test.mjs covers the above (30 node tests pass).

* feat(windows): 2D on Direct3D 12, presented in a WinUI SwapChainPanel

- canvas-core gpu::dxgi: CompositionSwapChain, a flip-model BGRA
  composition swapchain (2 buffers, frame-latency waitable,
  premultiplied or ignored alpha) bound to a panel through WinUI 3's
  ISwapChainPanelNative; SetMatrixTransform maps physical pixels back to
  the panel's DIPs.
- canvas-2d surface_d3d (feature d3d): canvases draw into a persistent
  Skia render target on the thread's shared D3D12 device and
  DirectContext; presenting blits it into the current back buffer.
  Resizing clears the canvas and resizes the swapchain.
- canvas-c: Engine::D3D, canvas_native_context_create_d3d (null without
  a usable device), _attach_swap_chain_panel and
  _set_swap_chain_transform; render() presents and resize() goes through
  the D3D path.
- canvas-napi host::windows: CanvasModule.NSCCanvas, the native side of
  the canvas view. It takes the panel's pointer key (offscreen without
  one), owns the context like the iOS view, applies the composition
  scale, and falls back to a CPU context when D3D12 is unavailable.

__test__/windows-host.test.mjs renders, reads back and resizes offscreen
on the hardware adapter and on WARP.

* feat(canvas-napi): frame scheduling and view layout for the Windows host

- frame: a default scheduler, installed at module init, so drawing
  presents without the host calling __flushAll. The first context
  dirtied in a JS turn queues one microtask that flushes every dirty
  context, so a turn (a rAF batch, an event handler) presents once,
  after all of its drawing. Works on any Node-API host.
- canvas_core::fit: the iOS/Android fit modes (none, fill, fitX, fitY,
  scaleDown) as one neutral buffer-to-view transform, per-axis density.
- NSCCanvas: 300x150 until sized (as on the web), surfaceWidth/Height
  setters, fit, and setViewSize (the panel's DIPs). With the
  composition scale these feed the swapchain's matrix transform.

* feat(windows): the canvas view on NativeScript Windows

The TS side of 2D on Windows, verified in a NativeScript Windows app
(core, webpack and the classic runtime with Node-API addons): 2D draws
into a SwapChainPanel at the display's refresh rate at 100% and 125%.

- platform.ts: capability flags (NAPI_HOST, POINTER_CONTEXT_HOST) that
  call sites branch on instead of platform lists. __WINDOWS__ is read
  through typeof, so bundlers that do not define it get false.
- helpers.ts: Node-API hosts load system_lib://canvasnative.node, which
  installs globalThis.CanvasModule.
- Canvas/napi-host.ts: the view shared by Node-API hosts, driving
  CanvasModule.NSCCanvas like the iOS view drives its native view:
  width/height coalescing, context creation, fit from the CSS size,
  rAF pause on unload, toDataURL, parent DOM shims. Hosts with native
  layout report the view's size and scale; a `%` size then sizes the
  buffer to the view's pixels (NativeScript never measures the view).
- Canvas/index.windows.ts: the SwapChainPanel, its SizeChanged and
  CompositionScaleChanged, and pointer/wheel events in the shape
  _handleEvents takes (which now also accepts objects).
- WebGL/WebGL2/WebGPU context pointer lookups use POINTER_CONTEXT_HOST.
- Packaging: platforms/windows plugin.props/targets copy
  <arch>/canvasnative.node next to the app's executable;
  tools/scripts/build-napi.sh and `make windows` build it;
  package.json declares the windows platform.

* feat(windows): canvas-polyfill on Windows; svg, media and audio import cleanly

- canvas-polyfill: file.windows (reads through CanvasModule.readFile,
  writes through core's file access, deletes through WinRT promises) and
  http.windows on Windows.Web.Http: one shared HttpClient, headers and
  content headers, string/binary/JSON bodies, onHeaders/onProgress,
  timeout and cancel through the WinRT operation, the body read into an
  ArrayBuffer without a copy, and the same text/JSON/bytes result rules
  as iOS.
- XMLHttpRequest takes ArrayBuffer response content first for every
  responseType (its non-Apple branches assumed Android's NSData /
  ByteBuffer and threw on Windows). Local files: readFile's result is
  { buffer, mime }, so decode and return `buffer` itself; text, json,
  document and arraybuffer responses were handed the wrapper object on
  every platform.
- canvas-svg, canvas-media, audio-context have no Windows backend yet.
  canvas-svg imports cleanly (canvas-polyfill's DOM references it at
  startup) and throws a clear error from Helpers.initialize() when an
  SVG is used; media and audio throw on import, so canvas-polyfill's
  probes leave AudioContext absent and fall back to its VideoFrame stub.

* feat(demo): run apps/demo on NativeScript Windows

- Windows-capable @nativescript/core and @nativescript/webpack from the
  core PR (pkg.pr.new 11272) and @nativescript/windows. typescript is
  pinned for ts-loader (unpinned, fork-ts-checker's peer resolution
  installed TypeScript 7, which has no JS compiler API); the type checker
  it would switch on stays off, as the demo has never type-checked.
- @nativescript/font-manager is linked into the demo like the other root
  packages, so the CLI stages its Windows plugin (as a dependency of the
  file:-linked canvas package it was never installed in the app).
- Asset names with parentheses or commas are not copied on Windows,
  where MSIX packaging cannot handle them.
- Launch arguments on Windows: launch-args.json in the app's LocalState,
  read (asynchronously) and removed at launch; the main page waits for
  them before opening the requested demo.
- App_Resources/Windows (manifest, PerMonitorV2 app.manifest, assets).

* test(canvas-napi): contract check against the V8 bindings

tools/tests/check-napi-contract.mjs scrapes the classes, methods,
accessors, Fast API methods and module members the V8 bindings register
(packages/canvas/platforms/ios/src/cpp/**, following inherited
Set{Methods,Props,Constants}) and checks them on the Node-API module.
Constants are reported separately (packages/canvas defines its own);
--strict fails on anything else missing, --json for tooling.

* feat(windows): WebGL and WebGL 2 on ANGLE in a SwapChainPanel

- canvas-core: GLContext::create_texture_context renders the default
  framebuffer into a BGRA D3D11 texture on ANGLE's device; present()
  flushes, copies it into a D3D11 composition swapchain bound to the
  panel and presents. A copy leaves ANGLE's cached D3D11 state alone
  (a draw would not); GL's bottom-up rows are flipped by a negative Y
  scale in the swapchain's matrix transform. ANGLE cannot wrap a
  multisampled texture, so these contexts report antialias: false.
  dxgi::CompositionSwapChain gains a D3D11 constructor (and builds with
  the gl feature).
- canvas-webgl / canvas-c: Windows attach / present / transform / resize,
  canvas_native_webgl_create_d3d, and canvas_native_webgl_present (every
  platform: present where on screen, else swap).
- canvas-napi: NSCCanvas.initContext + nativeContext (the iOS view's
  contract), one context kind per view; createWebGLContext /
  createWebGL2Context wrap the view's state (non-owning) or create an
  offscreen one from the options object. WebGL contexts are presented at
  frame end through crate::frame and gain __toDataURL, __flipY,
  __getSupportedExtensions, __resized, __startRaf/__stopRaf and
  continuousRenderMode.
- Fixes (Node-API, all platforms): shader sources were rewritten to
  desktop GLSL (#version 120 / 330 core), now macOS only; getUniform
  freed a result the into_* accessor had already freed (heap
  corruption); texImage2D gains ImageData/ImageBitmap sources and takes
  pixels from any ArrayBuffer or view; texSubImage2D(ImageData) passed
  RGBA as the type; bind* / isFramebuffer / useProgram / VAO calls
  accept null, undefined and 0 (what packages/canvas passes) via
  GLObject; WebGL 2 getParameter answers the WebGL 2 pnames (and stops
  leaking its result); getFragDataLocation returns -1 for unknown names.
- Canvas.createCustomView() on Node-API hosts (offscreen canvases).
- Packaging: tools/scripts/download-angle.sh (pinned, checksummed ANGLE
  build); build-napi.sh ships libEGL/libGLESv2 next to the module;
  THIRD_PARTY_NOTICES.txt.

apps/demo canvas-spec on Windows: 2d 180/180, webgl 74/74.

* feat(windows): WebGPU on wgpu DX12 in a SwapChainPanel

- canvas-core: dxgi::PanelSurfaceTarget, a COM stand-in for the panel's
  ISwapChainPanelNative that wgpu binds its swapchain through. It
  forwards to the panel and remembers the swapchain, so the DPI / fit
  matrix transform stays ours across wgpu's reconfigurations.
- canvas-c: wgpu-core/hal with dx12 on Windows;
  canvas_native_webgpu_context_create_swap_chain_panel,
  _set_swap_chain_transform and _resize_swap_chain_panel (reconfigures
  in place, keeping the page's configuration, and resizes the toDataURL
  read-back texture). onSubmittedWorkDone now polls the device, so it
  fires without a frame loop.
- canvas-napi host: NSCCanvas.initWebGPUContext(gpuPointer); WebGPU
  joins the one-context-per-view rule, transforms and resizes.
- canvas-napi gpu: rewritten against the packages/canvas WebGPU contract
  (objects.rs's strict descriptors replaced by lenient parsing like the
  V8 bindings'): callback-style requestAdapter / requestDevice / async
  pipelines, GPUSupportedLimits, error scopes and uncaptured errors,
  device.lost, mapAsync rejecting on every error type, destroy() /
  __releaseHandle on the transient objects, bounds-checked writeBuffer
  and setBindGroup offsets (canvas-c would panic / assert). The canvas
  context presents at frame end unless presentSurface() already did.
  Uncaptured-error callbacks are held weakly (the device was never
  collected); raw threadsafe functions are not released twice at
  teardown (segfault at exit in about half the runs).
- TS: bgra8unorm preferred and the capability checks on Node-API hosts;
  GPUDevice's event target reads WeakRef via deref() or get();
  getCurrentTexture releases the previous frame's wrappers when the host
  presented at frame end (they accumulated forever);
  GPURenderBundleEncoder.drawIndexed passes baseVertex (firstInstance
  landed in its slot).
- Demo: WEBGPU_SAMPLES / runWebGPUSample, and canvas-perf's
  `webgpu:<sample>` suite.

canvas-napi node:test 66/66 (webgpu 21/21 on D3D12). apps/demo on
Windows: canvas-spec webgpu 17/19 (the two webgpu.video tests need
canvas-media video, not ported yet); occlusionQuery animates through
auto-present, survives resizes, memory flat over 30s. Samples that load
their WGSL with File.readText() fail on Windows: @nativescript/core's
Windows PathIO calls get the app path joined with '/' and its sync
readers return before the async read completes.

* feat(windows): TS completeness, WebGL extensions on ANGLE, GPU device loss

TS through the platform/ helpers (Node-API hosts):
- ImageSource / XAML images in drawImage, createPattern, drawAtlas,
  createImageBitmap, WebGL texImage2D / texImage3D / texSubImage3D and
  ImageAsset.loadFromNative: the encoded bytes an ImageSource keeps, or
  the file behind its UriSource, decoded once per source.
- Fonts registered from font-manager's loadingdone; Dom's container is a
  XAML Grid; key events (key / code / repeat) from the panel; snapshot()
  as a PNG ImageSource.

WebGL on ANGLE (canvas-webgl angle.rs): ANGLE's WebGL-compatibility mode
exposes a GL extension only once requested, so getExtension enabled
nothing. Extensions are now mapped to the GL extensions ANGLE needs,
requested on getExtension (glRequestExtensionANGLE), offered only on the
WebGL versions they exist in, and getSupportedExtensions lists WebGL
names (not GL_*). WebGL 1 contexts are ES 2, so the extension objects
call the suffixed entry points (GL_ANGLE_instanced_arrays added to
gl-bindings). canvas-napi extension objects gain ext_name (packages/canvas
switches on it: every getExtension returned null), the constants the TS
reads and the web method names (drawArraysInstancedANGLE,
createVertexArrayOES, COLOR_ATTACHMENT0_EXT, ...; napi-rs camel-cased
them), a WebGL 2 EXT_color_buffer_float; OES_texture_half_float was
unreachable (a typo), EXT_disjoint_timer_query's TIMESTAMP_EXT was wrong.
TS: EXT_disjoint_timer_query called *EXT methods the bindings name *Ext
(every platform), getQueryEXT's null check was inverted, and
getExtension('EXT_color_buffer_float') had no case.

Canvas as an image source (every GPU engine):
- Context::get_image copied GPU snapshots to the CPU for GL, Vulkan and
  Metal only: D3D snapshots were unreadable, so createImageBitmap(canvas),
  the bitmap renderer, texImage2D(2d canvas) and copyExternalImageToTexture
  got nothing.
- A WebGL canvas as a source is read as an image: RGBA8, top row first,
  from its default framebuffer, bindings restored (read with format RGBA as
  the type before: GL error, white).
- copyExternalImageToTexture: the row pitch was width x the padded pitch
  (write_texture overran), flipY was ignored, and a 2D canvas copied
  smaller than itself mismatched its layout.

GPU device loss (Windows):
- 2D: a removed D3D12 device is detected at present; restoreContext()
  moves the canvas to a new device (the thread's shared device and Skia
  context are recreated), cleared, and back into its panel. TS fires
  contextlost, then contextrestored unless prevented, retrying while the
  driver comes back.
- WebGL: ANGLE contexts ask for reset notification; a reset makes
  isContextLost() true and fires webglcontextlost (no restore yet).
- __setContextLostListener / NSCCanvas.isContextLost / restoreContext;
  __simulateD3DDeviceRemoval and __createHeadlessPanel (a stand-in
  SwapChainPanel) for tests.
- Native log records (canvas-c, wgpu) now reach the host's console (and
  OutputDebugString); on Windows they went nowhere. CANVAS_LOG sets the
  level.

Also: ImageAsset.saveSync / saveCb (PNG, JPG; the C function the bindings
declare had gone); canvas-polyfill's devicePixelRatio / innerWidth read the
window when asked (the Windows window does not exist at import: 1x,
1920x1080); canvas-chartjs survives Intl polyfill probes that throw.

canvas-napi node:test 78/78; contract check: only GPUDevice's Apple-only
__getMetalDevicePointer missing. apps/demo canvas-spec on Windows 319/321
(webgpu.video needs canvas-media video); suite "contextlost" passes run on
its own. Known: removing the device in a packaged app leaves the process
unable to make hardware D3D12 devices for other APIs (WebGPU) afterwards;
the NativeScript Windows V8 engine loads no ICU data (Intl without
locales).

* feat(windows): canvas-svg on Node-API; canvas-polyfill gaps on Windows

canvas-svg:
- crates/canvas-svg-napi (canvassvg.node): the global.SVGModule the V8
  bindings install (SVGDocument, SVGNode, createSVGDocument, createElement,
  createTextNode), over canvas-svg-c, so NativeNode.ts runs unchanged.
  renderToBuffer takes an optional `bgra`.
- canvas-svg-c: canvas_native_svg_document_render_to_buffer_ordered renders
  premultiplied RGBA or BGRA whatever the platform's N32 order (BGRA on
  Windows: canvas-image loads the buffer as RGBA).
- Windows view: the document rasterized on the CPU straight into a
  WriteableBitmap's pixels (NSWinRT.interop.arrayBufferFromBuffer), shown
  by an Image sized to the content in DIPs; SvgData holds the markup and
  its natural size. Helpers.initialize loads system_lib://canvassvg.node.
- Packaging: platforms/windows (plugin.targets copies canvassvg.node);
  build-napi.sh takes the crate (`make windows-svg`).

canvas-polyfill on Windows: HTMLElement's WeakRef read deref() only on
Apple and get() only on Android (undefined elsewhere); lang from
Device.language; getBoundingClientRect from the view's window location and
size for non-canvas elements; data: images decode in memory where there is
no native base64-to-file helper; SVG lengths in in/cm/mm use the CSS inch
(96) instead of NaN.

canvas-svg-napi node:test 6/6. apps/demo on Windows: canvas-svg renders
DOM-built SVGs, SMIL animations and drawImage(svg) into a canvas (1.2 ms
for two per frame); canvas-spec 319/321. SVGs loaded from `~/` files fail
in @nativescript/core (PR build): knownFolders.currentApp() is the package
root, not <root>\app, and path.join leaves '/' inside the joined part,
which Windows PathIO rejects.

* fix(windows): canvases take their style size; WebGL buffer calls take any view

- The canvas overrode width/height setNative to size its drawing buffer
  without chaining to the platform view, which on Windows is what sets the
  XAML element's Width / Height: every canvas stretched to fill its cell
  (the canvas-spec page's 64x64 canvas was a full-width band). The
  overrides now chain. Core watches SizeChanged itself for % sizes and an
  event takes one delegate, so the canvas also hears size changes through
  _onSizeChanged and only wires SizeChanged when core has not.
- bufferData / bufferSubData / compressedTexImage2D /
  compressedTexSubImage2D / texSubImage2D took Uint8Array (or a few fixed
  view types) only: Float32Array vertex updates, Uint16Array indices,
  Uint8ClampedArray pixels threw. They take any ArrayBuffer or view now,
  read in place.
- Demo: canvas-perf `alpha` / `alpha-webgl` suites (a cleared canvas over a
  magenta parent).

Transparency: a cleared canvas still shows black on Windows. WinUI 3's
SwapChainPanel is external content and cannot blend with XAML behind it
(documented; microsoft-ui-xaml #6893), whatever the swapchain's alpha mode.

* feat(windows): transparent canvases blend with the page

WinUI 3's SwapChainPanel is external content: nothing in XAML shows
through it, whatever the swapchain's alpha mode (microsoft-ui-xaml #6893),
so a cleared canvas showed the window's black/white fill. A canvas with
alpha (the default) now presents into a XAML SurfaceImageSource shown by an
Image inside its panel (a panel with no swapchain stays see-through);
alpha: false keeps the zero-copy swapchain.

- canvas-core: WinUI 3's ISurfaceImageSourceNative and XamlSurface
  (BeginDraw / copy / EndDraw); D3D12Context::d3d11_on_12, a D3D11On12
  device on the canvas queue. ANGLE now runs on a D3D11 device of ours made
  with BGRA support (EGL_ANGLE_device_creation), which SurfaceImageSources
  require; ANGLE's own device is the fallback.
- canvas-2d: a transparent D3D12 canvas draws its frame into a texture (as
  into a swapchain buffer) that D3D11On12 copies into the image on the
  same queue. Device loss re-attaches the same surface.
- WebGL: the ANGLE texture is copied into the image; the view flips it (GL
  rows are bottom-up).
- canvas-c / canvas-napi: *_attach_xaml_surface,
  NSCCanvas.attachSurfaceImageSource and surfaceTransform (where the
  drawing buffer sits in the view, for placing the Image).
- TS: napi-host's _prepareSurface / _layoutSurface hooks; on Windows a
  SurfaceImageSource of the drawing buffer's size (a new one per size),
  placed with surfaceTransform, flipped for WebGL. The Image stays
  hit-testable, so pointer events bubble to the panel. % sizes are passed
  to core as `auto`: the panel stretches over its cell (core sizes % views
  against the whole parent).

WebGPU canvases still present through the panel (wgpu owns the swapchain):
their transparent areas show the window background.

apps/demo on Windows: canvas-perf alpha / alpha-webgl show the page through
the canvas (WebGL upright); canvas-svg's canvases draw over the page;
canvas-spec 319/321 (webgpu.video: canvas-media).

* perf(canvas-napi): raw fast members for the hottest calls; benchmark harness

napi-rs 3's generated methods and accessors create a reference to `this`
and register a native borrow on every call: ~300 ns, against ~75 ns for a
plain module function and a few times the work of a call like
translate(). src/fast.rs installs raw Node-API callbacks over the napi-rs
members on the class prototypes; they unwrap `this` checking napi-rs's
per-class type tag (a foreign receiver still throws "Illegal invocation")
and never call back into JS, which is what makes skipping the borrow
bookkeeping sound. They mirror the members they replace (canvas-c calls,
dirty marking):

- 2D: save/restore, resetTransform, translate/rotate/scale, beginPath,
  closePath, moveTo/lineTo/bezierCurveTo/quadraticCurveTo/arcTo/arc/rect,
  fillRect/strokeRect/clearRect, lineWidth and globalAlpha, and
  fillStyle/strokeStyle: colour strings read on the stack, the last parsed
  colour per style reused (no CSS parse when set again), colours
  serialized directly by the getter, gradients and patterns through the
  napi-rs accessors.
- WebGL / WebGL 2: uniform{1,2,3,4}f, uniform1i, uniformMatrix{2,3,4}fv,
  viewport, clear, clearColor, enable/disable, activeTexture,
  enableVertexAttribArray, vertexAttribPointer, bindBuffer, bindTexture,
  useProgram, drawArrays, drawElements. A null uniform location is a
  no-op, as in WebGL (the napi-rs members threw); numbers are coerced as
  WebIDL does (the napi-rs members threw on "5").

CANVAS_NAPI_FAST=0 turns them off. Release build, per call (median, this
laptop; unchanged calls vary +-30% run to run): translate 477 -> 180-230
ns, lineTo 349 -> 166, lineWidth 332 -> 171-191, fillStyle (same colour)
557 -> 301, uniformMatrix4fv 1171 -> 438-504, uniform4f 849 -> 417-453,
drawArrays 1552 -> 475-1186.

bench/run.mjs (the demo's `native` scenarios on the offscreen host:
median and p95, --save to bench/results/<commit>[-label].json, which is
ignored) and bench/compare.mjs (per-scenario change, exits 1 past a
threshold). PORTING.md documents both.

canvas-napi node:test 83/83; apps/demo canvas-spec on Windows 319/321.

* fix(windows): a lost 2D context is restored onto its GPU, not WARP

D3D12 devices are per-adapter singletons: while anything holds a removed
device, D3D12CreateDevice on that adapter fails. Restoring one lost canvas
while the others still held the device fell through to the next adapter,
usually WARP, for the rest of the process (and failed outright where WARP
was the only adapter, as on GPU-less CI runners).

- canvas-2d keeps a thread registry of D3D canvases (registered by
  canvas-c once boxed). Before a new device is made after a loss, every
  canvas on the removed one lets go of it: Skia context and surfaces,
  swapchain (unbound from its panel), XAML surface. A lost canvas draws
  into a raster stand-in until restored; resizing it while lost sizes the
  restore.
- XAML SurfaceImageSources keep the device they were given, and what they
  last drew with it, until they draw again (SetDevice(null) does not
  release it): a lost one is given a small D3D11 WARP stand-in device and
  drawn blank once.
- For 10 s after losing a GPU device, a new device has to be on a GPU
  again (restores fail and packages/canvas retries) rather than settle for
  WARP; after that WARP will do.
- CanvasModule.__d3dAdapterInfo() (tests): the device-lost suites assert
  the restore comes back on the same adapter.

* ci(windows): build the Node-API modules for x64 and arm64

build-native.yml gains canvas-windows and canvas-svg-windows (cached on
its sources, like the other SVG jobs), each for x64 and arm64
(cross-compiled). The x64 jobs run the Node-API suites on the shipped
modules, on WARP. .github/actions/setup-windows-native uses the image's
LLVM and ninja (installing LLVM over it with choco fails). The npm job
places the Windows modules; make windows / windows-svg build arm64 too.

Internal notes (canvas-napi PORTING.md, napi-sys PATCHED.md) stay local
(gitignored).

* build(windows): commit the x64 and arm64 Node-API modules

Release builds (release-napi) of canvasnative.node (+ ANGLE libEGL/libGLESv2)
and canvassvg.node, so npm_release.yml ships them. Temporary: the
canvas-windows / canvas-svg-windows CI jobs will provide them instead.

* feat(windows): audio-context on Node-API (#155)

* fix(canvas-napi): device.lost settles on the JS thread; the flush test keeps its host

- destroy() (and reading `lost` on a destroyed device) settled the weak lost promise through
  its threadsafe function. An unref'd function does not keep Node's loop alive, so an await on
  `lost` right after could see the loop end first (Node 22, CI). It now settles in place, and
  destroy() takes the pending promise before canvas-c's own lost callback can.
- windows-host: the flush test held only the pointer-wrapped context; the NSCCanvas that owns
  it could be collected across the awaits, freeing the context under it (transparent reads or
  an access violation on Node 22).

* feat(windows): audio-context on Node-API (web-audio-api, WASAPI)

- crates/audio-context-napi (audiocontext.node): AudioContext / OfflineAudioContext, every node
  in index.d.ts, AudioParam automation, AudioBuffer, PeriodicWave and the listener over the
  web-audio-api crate, played through WASAPI (cpal). web-audio-api panics on spec violations;
  each call turns the panic into a JS error ("InvalidStateError: ...") instead of aborting the
  host. Decoding (bytes, base64, files) and offline rendering run as async work; ended and
  statechange arrive through weak threadsafe functions.
- index.windows.ts: the public classes over it, sharing common.ts with iOS and Android.
  canvas-polyfill's probe now finds AudioContext on Windows. Not yet: MediaElementAudioSourceNode
  (canvas-media has no Windows backend); a WaveShaperNode takes one curve.
- build-napi.sh audio-context-napi, `make windows-audio`, the MSBuild copy targets, a CI job
  (x64 and arm64; the x64 job runs the Node-API suite offline and on the 'none' sink) and the
  PR workflow's artifact placement. THIRD_PARTY_NOTICES.txt for web-audio-api, Symphonia
  (MPL-2.0) and cpal.
- demo: an `audio` spec suite on Windows (offline graphs, decoding, realtime state and ended
  events on the default device).

* build(windows): commit the x64 and arm64 audiocontext modules

* build(windows): rebuild the x64 and arm64 canvasnative modules with the device.lost fix
Comment thread .github/workflows/build-native.yml Fixed
Comment thread .github/workflows/build-native.yml Fixed
Comment thread .github/workflows/build-native.yml Fixed
Comment thread .github/workflows/build-native.yml Fixed
Comment thread .github/workflows/build-native.yml Fixed
Comment thread .github/workflows/build-native.yml Fixed
Comment thread .github/workflows/build-native.yml Fixed
Comment thread .github/workflows/build-native.yml Fixed
Comment thread .github/workflows/build-native.yml Fixed
Comment thread .github/workflows/build-native.yml Fixed
…caching of untrusted files'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Comment thread .github/workflows/build-native.yml Fixed
Comment thread .github/workflows/build-native.yml Fixed
Comment thread .github/workflows/build-native.yml Fixed
triniwiz and others added 2 commits September 26, 2026 22:05
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
triniwiz and others added 5 commits September 27, 2026 18:20
…oSourceNode (#157)

* fix(canvas): fillText's maxWidth is optional; deleteShader takes a WebGLShader

- CanvasRenderingContext2D.fillText(text, x, y, maxWidth?): maxWidth is optional, as on the web
  (strokeText already was).
- WebGLRenderingContext.deleteShader(shader: WebGLShader | null) was declared as taking a
  WebGLRenderbuffer; null is a no-op, as in WebGL (it threw on `shader.native`).

* feat(windows): canvas-media on Media Foundation

canvas-media's Video, Audio and VideoFrame on NativeScript Windows, over a WinRT MediaPlayer.

- crates/canvas-media-napi (canvasmedia.node): NSCMediaPlayerBridge subscribes the player's events
  natively (Media Foundation raises them on its own threads, where the runtime cannot run JS
  delegates) and delivers them through threadsafe functions. Video players run in frame-server
  mode: each frame is copied (CopyFrameToVideoSurface) into a ring of BGRA textures on the module's
  D3D11 device, presented into the view's XAML SurfaceImageSource and read back (RGBA) on demand.
  Frames are also shareable with WebGPU (gpuFrame(): NT handles plus ready / release fences) and
  the audio can be tapped (createAudioTap(): an IBasicAudioEffect the module serves as an in-process
  WinRT class, registered in the app manifest by plugin.targets).
- Exit safety: a MediaPlayer released during process exit (after ExitProcess has killed Media
  Foundation's threads, e.g. from a thread-local destructor) spins forever in the graphics driver
  and leaves an unkillable process. An env cleanup hook closes every live player while its threads
  run, nothing releasable lives in TLS, and no frame copy starts after close().
- player-windows.ts: HTMLMediaElement state and events over the player (readyState, play()
  promises with AbortError / NotSupportedError, timeupdate while playing, seeking / seeked,
  volumechange, <Source> fallback in order, ms-appx: / file / http sources, canPlayType).
- Video: frames shown by an Image; 2D drawImage, WebGL texImage2D / texImage3D / texSubImage3D,
  WebGPU copyExternalImageToTexture (ImageAsset) and VideoFrame read the current frame once per
  decoded frame. `controls`: WinUI's transport controls over the frames (the stock
  MediaPlayerElement template with its presenter kept for layout but invisible: in frame-server
  mode it only paints black), so canvases keep getting frames.
- Audio: WinUI's compact transport controls.
- Packaging: platforms/windows plugin.props / plugin.targets, build-napi.sh canvas-media-napi,
  `make windows-media`, the PR workflow's artifact placement.
- CI: canvas-svg, audio-context and canvas-media share one napi-windows matrix job (one entry per
  module), with the earlier canvas-svg fixes applied to all: no cache on manual runs, the default
  branch checked out on them, a source key from git tree hashes (Cargo.lock is untracked), and a
  timeout on the Node-API tests.
- demo: a `media` spec suite on Windows (events, drawImage, texImage2D, VideoFrame,
  copyExternalImageToTexture, importExternalTexture, seeking, errors, audio,
  createMediaElementSource).

* feat(windows): video frames reach WebGPU without the CPU

wgpu's DX12 backend supports external textures, so importExternalTexture now works on Windows and
copyExternalImageToTexture(video) stays on the GPU, over the frames canvas-media shares.

- canvas-c gpu_shared_frame: a CanvasD3DSharedFrame (`nativeTexture`) names a shareable BGRA
  texture and two shared fences by NT handle. The texture is opened on wgpu's D3D12 device
  (OpenSharedHandle, texture_from_raw; cached by id), only ever sampled so it stays in COMMON
  between submits, and the fork's add_wait_fence / add_signal_fence bracket the submit that reads
  it: wait for the producer's copy, signal when the read is done so the producer may reuse it. A
  frame from another adapter (hybrid GPUs) is refused, and the callers take the CPU path.
- copyExternalImageToTexture: the existing blit, factored into blit_texture (any source and
  destination texture).
- importExternalTexture: the frame is drawn at once into a plane of our own, so the external
  texture's later uses never read the producer's texture.
- canvas-napi: GPUDevice.__getAdapterLuid(); canvas-c
  canvas_native_webgpu_device_get_adapter_luid.
- packages/canvas: GPUDevice.__frameDevice (the MTLDevice on Apple, the adapter LUID on Windows)
  replaces __metalDevice where videos are asked for GPU frames.

* feat(windows): MediaElementAudioSourceNode

createMediaElementSource(element) on Windows, over the tap canvas-media puts on the element's
MediaPlayer (as MTAudioProcessingTap does on iOS and an ExoPlayer AudioProcessor on Android).

- canvas-media installs the tap (an audio effect in the player's pipeline) before any source is set,
  passing the audio through; createMediaElementSource routes it: the element goes silent and its
  decoded audio, at the element's volume (silent when muted), plays through the graph. It works
  for <audio> and <video>, and for canvas-polyfill's elements. disposeMediaElementSource gives the
  element its output back.
- audiocontext.node: createMediaElementSourceFromTap(address) reads the tap through the
  AudioTapSource canvas-media's module exposes (retained for as long as the node lives), as a
  MediaStreamTrack source: pulled on the render thread, silence when the element is paused.
  Realtime contexts only, as on the web.

* build(windows): commit the x64 and arm64 canvasnative, canvasmedia and audiocontext modules

Release builds (release-napi) with the shared video frames, the audio tap and the exit-safe player teardown, so npm_release.yml ships them until the napi-windows / canvas-windows CI jobs provide them.

* chore: keep only the comments the code cannot say

Drops comments that restate the next lines or a member's name, keeping the whys: threading, the exit-time release hazard, D3D12 resource states, cross-module struct layouts, the frame/tap contracts and platform quirks.

* chore: trim a redundant comment
…tive context (#156)

* fix(canvas): keep JS contexts safe after their canvas releases the native context

The Canvas view frees the native 2D/WebGL/WebGPU context in disposeNativeView while app code may still hold the JS wrapper (frame loops, or a view tree rebuilt under HMR). Calls through a stale wrapper read freed memory, and on Android a destroyed SurfaceView surface leaves a WebGPU swapchain that getCurrentTexture blocks on.

- GPUCanvasContext: guard every native entry point once detached, refuse to configure a surface that reports no capabilities, pause acquire/present while the surface is gone (resuming once the native re-attach answers a capabilities probe), and stop the wrapper's per-frame RAF before the context can be released.
- 2D and WebGL contexts: swap the native object for an inert stand-in on detach, keeping the original referenced so finalization timing is unchanged.
- Canvas views detach their contexts before releasing, and forward surfaceDestroyed/Created/Resize to the WebGPU context.
- Native: the wrapper adopting the view's context pointer now takes its own strong count (canvas_native_webgpu_context_reference), since both the view and the wrapper's finalizer release one.

* fix(canvas): contexts outlive their view natively

The view and the JS wrapper shared one raw context pointer with no
ownership between them, so the view's release (Android
releaseNativeContext, iOS deinit) left the wrapper on freed memory,
and the WebGPU wrapper released an Arc count it never took.

- 2D contexts and WebGLState are refcounted (WebGLState's live-set is
  gone); the C++ wrappers take their own reference when they wrap the
  view's pointer, and the view only drops its own.
- Before letting go, the Android view moves the context off its
  surface: WebGPU renders into an offscreen texture (no acquire on a
  dead surface, so no hang) until a resize attaches a new one, 2D GL
  moves to a pbuffer, Vulkan 2D to an offscreen render target. The
  same happens on surfaceDestroyed.
- iOS deinit also releases WebGPU contexts.

A context that outlives its canvas keeps working offscreen, like a
detached <canvas>.

* refactor(canvas): drop the JS context detach now that native handles it

The context wrappers no longer swap in inert stand-ins, track
detach/surface-lost state or pause the native RAF: a context keeps a
reference of its own and renders offscreen once its view lets go, so
every call stays valid without JS bookkeeping (and without breaking
code that keeps drawing across a Vite HMR rebuild).

* fix(windows): napi context wrappers hold their own reference

disposeNativeView drops the host, whose finalizer released the
context the 2D/WebGL/WebGPU wrappers were still borrowing. The
wrappers now take a reference of their own, like the V8 bindings.

* test(canvas): contexts keep working after their canvas leaves the page

* build(windows): rebuild the x64 and arm64 canvasnative modules with the context refcounts

---------

Co-authored-by: Osei Fortune <fortune.osei@yahoo.com>
* feat(canvas): contexts attach to the view's surface whenever it appears

A context made before the Android view had a surface stayed offscreen:
the first surface only fired contextReady, and a recreated TextureView
surface was ignored. Every new surface now attaches the context
(resize()), carrying 2D pixels over when the size is unchanged.

WebGPU contexts can be created before there is a window: the context's
wgpu surface is optional, None until the view has one and again once it
lets go of it, with frames rendering offscreen meanwhile.

* feat(canvas): deprecate the Canvas ready event

getContext works as soon as the canvas exists, as on the web. The event still fires; listening for it warns once.

* chore(demo): start the demos from loaded instead of ready

* build(windows): rebuild the x64 and arm64 canvasnative modules

* feat(android): show a WebGL or WebGPU frame drawn before the surface existed

A frame drawn before the view had a surface went to the pbuffer
(WebGL) or the offscreen texture (WebGPU) and was dropped when the
window surface attached, so a canvas drawn once in `loaded` stayed
blank. The attach now carries that frame over and presents it:
WebGL blits it through a renderbuffer across the EGL surface switch,
WebGPU copies it into the first swapchain texture.

* fix(android): pass xr_compatible when a 2d context detaches its surface

nativeDetach2DSurface passed 12 of ContextAttributes::new's 13 arguments,
so canvas-android did not compile.

* fix(webgl): keep the pbuffer when a resize does not change its size

Layout resized the pbuffer to the size it already had before the surface
appeared, which recreated it blank and lost the frame the surface was
meant to show.

* fix(canvas2d): free a context's GL objects on its own GL context

GrDirectContext deletes its GL names on whichever context is current. When
another canvas was current, collecting a 2d context deleted that canvas's
objects and its later draws did nothing.

* fix(ios): stop bundling the Node-API loader on Android and Apple

NAPI_HOST cannot be folded at build time, so its __non_webpack_require__
survived into the iOS bundle and imported node:module, which the iOS runtime
does not have. The app failed to start.

* build: rebuild the Android AAR and the Apple xcframework
… fix deleteShader (#160)

* fix(windows): don't present faster than the display

Swapchains are created with a frame-latency waitable object, so Present no longer throttles, and nothing waited on the object. Presenting faster than the display queued frames (and the GPU work and memory each holds) without bound: a busy canvas grew by ~100 MB/s.

A present now checks the waitable without blocking and is skipped when the display hasn't taken the queued frames. The frame scheduler keeps that context dirty and flushes it again on the next requestAnimationFrame, so no frame rate is capped and nothing blocks the UI thread. After 250 ms of skipped presents, one goes through anyway.

* fix(windows): cap and purge the shared Skia resource cache

Every D3D canvas on a thread shares one DirectContext, which kept Skia's default 256 MB resource cache and never purged it. Its buffers live in D3D12 upload heaps, which grew to 480 MB in the demo app. The budget is now 64 MB, and resources unused for 5 s are freed (checked at most once a second while presenting): the demo app's home screen went from ~770 MB to ~385 MB.

* fix(webgl): deleteShader takes a WebGLShader

The binding declared a WebGLRenderbuffer, so every deleteShader(shader) threw 'Value is not an instance of class WebGLRenderbuffer' (three.js and PixiJS both call it after linking).
* feat(gamepad): Gamepad API for iOS, Android and Windows

New @nativescript/canvas-gamepad package: navigator.getGamepads() plus
gamepadconnected/gamepaddisconnected, standard mapping, up to 4 pads.

Native code writes controller state into one shared Float32Array as input
arrives; getGamepads() reads it in TS, so polling makes no native calls on
iOS/Android and one per frame on Windows.

- iOS/tvOS/visionOS: GameController GCExtendedGamepad (ObjC)
- Android: KeyEvent/MotionEvent taken at the activity's Window.Callback
- Windows: Windows.Gaming.Input poller (crates/canvas-gamepad-napi)
- canvas-polyfill: getGamepads() and window events use the package when
  installed; events go through the iOS runtime's own EventTarget

* build(gamepad): Windows canvasgamepad.node for x64 and arm64

* build(windows): rebuild canvasnative.node with #160

* feat(gamepad): name Bluetooth Xbox pads "Xbox Wireless Controller" like Chrome

* chore(demo): use core and webpack from NativeScript 54125e1

* chore: 3.0.0-alpha.16
@triniwiz
triniwiz merged commit 1cfa2a3 into master Sep 28, 2026
15 of 22 checks passed

This branch had an error being deployed

1 failed deployment
npm-publish — ea56330e Deployed Sep 28, 2026 by triniwiz via publish #39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants