Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
12e37d8
fix(webgl): clearBuffer* take the buffer enum, bindTransformFeedback …
triniwiz Nov 7, 2026
4994da0
fix(webgpu): stop advertising subgroups, which naga cannot enable yet…
triniwiz Nov 7, 2026
e91cb7d
fix(canvas): ignore non-finite lineDashOffset, globalAlpha, miterLimi…
triniwiz Nov 7, 2026
f922bca
fix(polyfill): images that fail to decode fire error; window.name; bl…
triniwiz Nov 7, 2026
da8028a
fix(napi): Windows-only methods in their own impl blocks, so the modu…
triniwiz Nov 7, 2026
30bc244
fix(demo): phaser breakout constructs its game, three's compute parti…
triniwiz Nov 7, 2026
7498d4e
feat(linux): EGL contexts on GDK's display, pbuffers for offscreen su…
triniwiz Nov 7, 2026
3c18514
feat(linux): 2D, WebGL and transferred OffscreenCanvas surfaces
triniwiz Nov 7, 2026
a0db50e
feat(linux): WebGPU on Vulkan, frames shared with GL through exported…
triniwiz Nov 7, 2026
6366a5b
feat(linux): @nativescript/canvas on NativeScript Linux, a GTK 4 widg…
triniwiz Nov 7, 2026
cc868af
feat(linux): canvas-polyfill file and http, fetch of local files, win…
triniwiz Nov 7, 2026
d24a83d
fix(chartjs): replace Intl on Linux, where V8 has no ICU data
triniwiz Nov 7, 2026
8d69131
feat(linux): canvas-svg view on GTK 4
triniwiz Nov 7, 2026
3f6a2db
feat(linux): canvas-media video and audio on GStreamer
triniwiz Nov 7, 2026
f3ee2fb
feat(linux): audio-context through its Node-API module
triniwiz Nov 7, 2026
eb0967c
feat(linux): canvas-gamepad on evdev through gilrs, with hot-plug
triniwiz Nov 7, 2026
23f8415
feat(linux): run apps/demo on NativeScript Linux; apps/demo-linux smo…
triniwiz Nov 7, 2026
8f13bdb
chore(linux): x64 Node-API modules for canvas, canvas-svg, canvas-med…
triniwiz Nov 7, 2026
c9333e3
feat(linux): canvas-media video as GL textures shared with GDK and ca…
triniwiz Nov 7, 2026
694f2e3
feat(linux): WebGL and WebGPU take canvas-media frames on the GPU
triniwiz Nov 7, 2026
f693159
feat(linux): Video hands frames to WebGL and WebGPU natively
triniwiz Nov 7, 2026
46d19b2
chore(linux): x64 canvas and canvas-media modules with GPU video frames
triniwiz Nov 7, 2026
7d8237e
chore(linux): arm64 Node-API modules
triniwiz Nov 7, 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,6 @@ crates/canvas-napi/bench/results/
# Internal notes (local only)
crates/canvas-napi/PORTING.md
crates/vendor/napi-sys/PATCHED.md

# The packed @nativescript/linux runtime apps/demo-linux installs (copied in, not committed)
tools/linux/*.tgz
42 changes: 42 additions & 0 deletions apps/demo-linux/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# NativeScript
hooks/
node_modules/
platforms/

# NativeScript Template
*.js.map
*.js
!webpack.config.js

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# General
.DS_Store
.AppleDouble
.LSOverride
.idea
.cloud
.project
tmp/
typings/

# misc
npm-debug.log

# app
!*.d.ts
!src/assets/fontawesome.min.css
/report/
.nsbuildinfo
/temp/
/src/tns_modules/

# app uses platform specific scss which can inadvertently get renamed which will cause problems
app/app.scss

package-lock.json
10 changes: 10 additions & 0 deletions apps/demo-linux/nativescript.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { NativeScriptConfig } from '@nativescript/core';

export default {
id: 'org.nativescript.canvaslinuxdemo',
appPath: 'src',
appResourcesPath: '../../tools/assets/App_Resources',
cli: {
packageManager: 'npm',
},
} as NativeScriptConfig;
21 changes: 21 additions & 0 deletions apps/demo-linux/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "demo-linux",
"main": "./src/app.ts",
"description": "Canvas 2D, WebGL and Web Audio on NativeScript Linux",
"private": true,
"dependencies": {
"@nativescript/audio-context": "file:../../packages/audio-context",
"@nativescript/canvas": "file:../../packages/canvas",
"@nativescript/core": "https://pkg.pr.new/@nativescript/core@11620",
"@nativescript/font-manager": "file:../../node_modules/@nativescript/font-manager"
},
"devDependencies": {
"@nativescript/linux": "file:../../tools/linux/nativescript-linux-0.1.0-alpha.5.tgz",
"@nativescript/webpack": "https://pkg.pr.new/@nativescript/webpack@11620",
"nativescript": "9.1.2-dev.2026-11-07-42333414597",
"typescript": "~5.9.0"
},
"scripts": {
"start": "ns run linux --no-hmr"
}
}
1 change: 1 addition & 0 deletions apps/demo-linux/references.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference path="./node_modules/@nativescript/core/global-types.d.ts" />
162 changes: 162 additions & 0 deletions apps/demo-linux/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Application, Button, GridLayout, ItemSpec, Label } from '@nativescript/core';
import { Canvas } from '@nativescript/canvas';
import { AudioContext } from '@nativescript/audio-context';

function canvasIn(grid: GridLayout, row: number, col: number, onReady: (canvas: Canvas) => void) {
const canvas = new Canvas();
canvas.style.width = '100%' as any;
canvas.style.height = '100%' as any;
GridLayout.setRow(canvas, row);
GridLayout.setColumn(canvas, col);
grid.addChild(canvas);
// Usable right away; the drawing buffer follows the view's size once laid out.
onReady(canvas);
return canvas;
}

/** A gradient, shapes, text and an orbiting dot (requestAnimationFrame). */
function draw2D(canvas: Canvas) {
const ctx = canvas.getContext('2d') as any;
let logged = '';
const frame = (time: number) => {
const { width, height } = canvas;
if (logged !== `${width}x${height}`) {
logged = `${width}x${height}`;
console.log(`2d: ${logged}`);
}
const gradient = ctx.createLinearGradient(0, 0, width, height);
gradient.addColorStop(0, '#1e3c72');
gradient.addColorStop(1, '#2a5298');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);

ctx.fillStyle = 'rgba(255, 200, 0, 0.9)';
ctx.beginPath();
ctx.arc(width * 0.3, height * 0.5, Math.min(width, height) * 0.25, 0, Math.PI * 2);
ctx.fill();

ctx.strokeStyle = '#ff4f6d';
ctx.lineWidth = 8;
ctx.strokeRect(width * 0.55, height * 0.25, width * 0.3, height * 0.5);

const t = time / 1000;
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(width * 0.3 + Math.cos(t * 2) * width * 0.15, height * 0.5 + Math.sin(t * 2) * height * 0.3, 10, 0, Math.PI * 2);
ctx.fill();

ctx.font = `bold ${Math.round(width * 0.07)}px sans-serif`;
ctx.fillStyle = 'white';
ctx.fillText('Canvas 2D on GTK 4', 16, height * 0.15);
requestAnimationFrame(frame);
};
requestAnimationFrame(frame);
}

/** A clear colour and an RGB triangle. */
function drawWebGL(canvas: Canvas) {
const gl = canvas.getContext('webgl2') as WebGL2RenderingContext;
if (!gl) {
console.log('webgl2: unavailable');
return;
}
console.log(`webgl2: ${gl.getParameter(gl.VERSION)} / ${gl.getParameter(gl.RENDERER)}`);
const shader = (type: number, source: string) => {
const s = gl.createShader(type);
gl.shaderSource(s, source);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
console.log('shader:', gl.getShaderInfoLog(s));
}
return s;
};
const program = gl.createProgram();
gl.attachShader(
program,
shader(
gl.VERTEX_SHADER,
`#version 300 es
in vec2 position; in vec3 color; out vec3 vColor;
void main() { vColor = color; gl_Position = vec4(position, 0.0, 1.0); }`,
),
);
gl.attachShader(
program,
shader(
gl.FRAGMENT_SHADER,
`#version 300 es
precision mediump float; in vec3 vColor; out vec4 outColor;
void main() { outColor = vec4(vColor, 1.0); }`,
),
);
gl.linkProgram(program);
gl.useProgram(program);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0.8, 1, 0, 0, -0.8, -0.8, 0, 1, 0, 0.8, -0.8, 0, 0, 1]), gl.STATIC_DRAW);
const position = gl.getAttribLocation(program, 'position');
const color = gl.getAttribLocation(program, 'color');
gl.enableVertexAttribArray(position);
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 20, 0);
gl.enableVertexAttribArray(color);
gl.vertexAttribPointer(color, 3, gl.FLOAT, false, 20, 8);
const frame = (time: number) => {
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.clearColor(0.1, 0.1, 0.12 + 0.08 * Math.sin(time / 500), 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(frame);
};
requestAnimationFrame(frame);
}

let audio: AudioContext | null = null;

/** A 440 Hz tone, faded in and out over a second. */
function playTone(status: Label) {
audio ??= new AudioContext();
const now = audio.currentTime;
const oscillator = audio.createOscillator();
const gain = audio.createGain();
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(440, now);
gain.gain.setValueAtTime(0, now);
gain.gain.linearRampToValueAtTime(0.3, now + 0.05);
gain.gain.linearRampToValueAtTime(0, now + 1);
oscillator.connect(gain);
gain.connect(audio.destination);
oscillator.start(now);
oscillator.stop(now + 1);
status.text = `AudioContext: ${audio.state}, ${audio.sampleRate} Hz`;
console.log(`audio: ${audio.state} ${audio.sampleRate} Hz, base latency ${audio.baseLatency}`);
}

Application.run({
create: () => {
const grid = new GridLayout();
grid.addRow(new ItemSpec(1, 'star'));
grid.addRow(new ItemSpec(1, 'auto'));
grid.addColumn(new ItemSpec(1, 'star'));
grid.addColumn(new ItemSpec(1, 'star'));
canvasIn(grid, 0, 0, draw2D);
canvasIn(grid, 0, 1, drawWebGL);

const status = new Label();
status.text = 'AudioContext: not started';
status.margin = 12;
GridLayout.setRow(status, 1);
grid.addChild(status);

const play = new Button();
play.text = 'Play 440 Hz';
play.margin = 8;
play.on('tap', () => playTone(status));
GridLayout.setRow(play, 1);
GridLayout.setColumn(play, 1);
grid.addChild(play);

// Once on start, so a run without input still plays.
setTimeout(() => playTone(status), 1500);
return grid;
},
});
19 changes: 19 additions & 0 deletions apps/demo-linux/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"strict": false,
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ESNext", "DOM"],
"baseUrl": ".",
"paths": {
"~/*": ["src/*"]
},
"noEmit": true,
"skipLibCheck": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
},
"include": ["src/**/*.ts", "references.d.ts"]
}
16 changes: 16 additions & 0 deletions apps/demo-linux/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const webpack = require('@nativescript/webpack');
const { resolve } = require('path');

module.exports = (env) => {
webpack.init(env);
webpack.useConfig('typescript');

webpack.chainWebpack((config) => {
// packages/ ship every platform's sources; they were never type-checked together.
config.plugins.delete('ForkTsCheckerWebpackPlugin');
// The plugins are linked from packages/: keep one @nativescript/core, the app's.
config.resolve.alias.set('@nativescript/core', resolve(__dirname, 'node_modules', '@nativescript', 'core'));
});

return webpack.resolveConfig();
};
4 changes: 4 additions & 0 deletions apps/demo/nativescript.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export default {
markingMode: 'none',
discardUncaughtJsExceptions: false,
},
// Light like the Android app theme (Theme.AppCompat.Light), whatever the desktop uses
linux: {
appearance: 'light',
},
appPath: 'src',
ios: {
discardUncaughtJsExceptions: false,
Expand Down
6 changes: 4 additions & 2 deletions apps/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@nativescript/canvas-polyfill": "file:../../packages/canvas-polyfill",
"@nativescript/canvas-svg": "file:../../packages/canvas-svg",
"@nativescript/canvas-three": "file:../../packages/canvas-three",
"@nativescript/core": "https://pkg.pr.new/@nativescript/core@54125e1",
"@nativescript/core": "https://pkg.pr.new/@nativescript/core@11620",
"@nativescript/font-manager": "file:../../node_modules/@nativescript/font-manager",
"@pixi-essentials/svg": "file:../../node_modules/@pixi-essentials/svg",
"babylonjs": "file:../../node_modules/babylonjs",
Expand All @@ -37,9 +37,11 @@
"@nativescript/android": "9.1.1",
"@nativescript/devtools": "~0.0.1",
"@nativescript/ios": "9.0.4-next.2026-08-13-31742124335",
"@nativescript/linux": "file:../../tools/linux/nativescript-linux-0.1.0-alpha.5.tgz",
"@nativescript/visionos": "~9.0.1",
"@nativescript/webpack": "https://pkg.pr.new/@nativescript/webpack@54125e1",
"@nativescript/webpack": "https://pkg.pr.new/@nativescript/webpack@11620",
"@nativescript/windows": "beta",
"nativescript": "9.1.2-dev.2026-11-07-42333414597",
"typescript": "~5.6.0"
}
}
18 changes: 9 additions & 9 deletions apps/demo/src/launch-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
*
* echo {"demo":"canvas-spec","suite":"2d"} > %LOCALAPPDATA%\Packages\<package family>\LocalState\launch-args.json
*
* Linux: after the app folder, e.g. `platforms/linux/demo/bin/demo app --demo=canvas-spec`.
*
* Captured at launch rather than read in the page, because the first page
* navigates during bootstrap -- before Application.android.foregroundActivity
* is assigned.
*/
import { getFileAccess, knownFolders, path } from '@nativescript/core';

declare const __WINDOWS__: boolean, NSWinRT: any, Windows: any;
declare const __WINDOWS__: boolean, __LINUX__: boolean, NSWinRT: any, Windows: any;

export const launchArgs: { demo?: string; profile?: string; frames?: number; suite?: string; surface?: string } = {};

Expand All @@ -40,7 +42,10 @@ export function captureLaunchArgs(androidIntent?: any) {
if (__ANDROID__) {
captureFromIntent(androidIntent);
} else if (__APPLE__) {
captureFromProcessArguments();
const args = NSProcessInfo.processInfo.arguments;
captureFromArguments(args ? Array.from({ length: args.count }, (_, i) => String(args.objectAtIndex(i))) : []);
} else if (typeof __LINUX__ !== 'undefined' && __LINUX__) {
captureFromArguments((global as any).process?.argv ?? []);
} else if (typeof __WINDOWS__ !== 'undefined' && __WINDOWS__) {
captureFromLaunchFile();
}
Expand Down Expand Up @@ -69,13 +74,8 @@ function captureFromIntent(intent: any) {
}

/** `--demo=canvas-spec`, as simctl and Xcode's scheme arguments pass them. */
function captureFromProcessArguments() {
const args = NSProcessInfo.processInfo.arguments;
if (!args) {
return;
}
for (let i = 0; i < args.count; i++) {
const arg = String(args.objectAtIndex(i));
function captureFromArguments(args: string[]) {
for (const arg of args) {
const eq = arg.indexOf('=');
if (arg.indexOf('--') !== 0 || eq === -1) {
continue;
Expand Down
5 changes: 5 additions & 0 deletions apps/demo/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ module.exports = (env) => {

// shared demo code
config.resolve.alias.set('@demo/shared', resolve(__dirname, '..', '..', 'tools', 'demo'));

if (webpack.Utils.platform.getPlatformName() === 'linux') {
// The plugins are linked from packages/: keep one @nativescript/core, the app's.
config.resolve.alias.set('@nativescript/core', resolve(__dirname, 'node_modules', '@nativescript', 'core'));
}
//config.resolve.alias.set('three', 'three/webgpu');
// config.resolve.alias.set('three/tsl', 'three/tsl');

Expand Down
2 changes: 1 addition & 1 deletion crates/audio-context-napi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "audio-context-napi"
version = "0.1.0"
edition = "2021"
publish = false
description = "Node-API binding for @nativescript/audio-context: a Web Audio graph rendered by the web-audio-api crate and played through WASAPI (cpal), for Node-API hosts (NativeScript Windows, Node)."
description = "Node-API binding for @nativescript/audio-context: a Web Audio graph rendered by the web-audio-api crate and played through cpal (WASAPI on Windows, ALSA on Linux), for Node-API hosts (NativeScript Windows and Linux, Node)."

[lib]
crate-type = ["cdylib"]
Expand Down
Loading
Loading