Skip to content

fix(windows): stop presents outrunning the display, cap Skia's cache, fix deleteShader - #160

Merged
triniwiz merged 3 commits into
v3-v8from
fix/windows-present-frame-latency
Sep 28, 2026
Merged

triniwiz merged 3 commits into
v3-v8from
fix/windows-present-frame-latency

Conversation

@triniwiz

Copy link
Copy Markdown
Member

Three Windows fixes found while building a demo app on 3.0.0-alpha.15 (2D, WebGL, WebGPU, three.js, PixiJS).

Don't present faster than the display

Swapchains are created with DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT, which stops Present from throttling, but nothing waited on the object. requestAnimationFrame runs at ~66–70 fps here against a 59 Hz display, so frames and the GPU work each holds queued without bound: an opaque canvas running a busy 2D demo grew by ~100 MB/s, reached 21 GB and went black.

CompositionSwapChain::acquire_frame() checks the waitable without blocking. When the display hasn't taken the queued frames, the 2D (present_d3d) and WebGL (egl.rs) presents are skipped. The drawing stays in the canvas's own surface or texture. The frame scheduler keeps that context dirty on a retry list and flushes it again from requestAnimationFrame, so nothing blocks the UI thread and no frame rate is capped. After 250 ms of skipped presents, one goes through anyway, so a canvas can't stop updating for good.

No JS or C ABI changes. The skip reaches the scheduler through a thread-local in dxgi.rs, since flushes are synchronous on one thread.

A blocking wait was tried first. It capped fps at the display rate and made an intermittent startup stall of the XAML dispatcher much more likely (3 of 4 launches, against ~1 in 8 without the wait).

Cap and purge the shared Skia resource cache

All D3D canvases on a thread share 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 (the allocator's 32 + 64 + 128 + 256 MB blocks). The budget is now 64 MB, and resources unused for 5 s are freed, checked at most once a second while presenting.

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, so neither started.

Testing

Windows 11, x64, Intel Iris Xe, 59 Hz, release-napi build of this branch in the app:

Before After
Busy 2D canvas, 40 s +~100 MB/s flat at ~700 MB, 67–70 fps
Demo home screen (several canvases) ~770 MB ~385 MB, flat over 3 min
Launches with a demo starting immediately – 9 of 9 without the dispatcher stall
three.js / PixiJS demos throw on start start and run

Still open, not addressed here: creating and destroying three.js and PixiJS canvases retains roughly 15 MB per cycle. 2D, WebGL shader and WebGPU canvases return to baseline.

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.
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.
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).
@coderabbitai

coderabbitai Bot commented Sep 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: f13892ff-4591-4307-9969-b242333fb171

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@triniwiz
triniwiz merged commit e1f18cc into v3-v8 Sep 28, 2026
10 of 17 checks passed
triniwiz added a commit that referenced this pull request Sep 28, 2026
* 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
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.

1 participant