Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2b91bc9
NativeDawn
CedricGuillemet Jul 16, 2026
f01c83e
fix cmake script
CedricGuillemet Jul 16, 2026
c5db0cb
submodules recurse init
CedricGuillemet Jul 16, 2026
5780773
fix ubuntu
CedricGuillemet Jul 16, 2026
9f12ae3
dawn desktop gl off
CedricGuillemet Jul 17, 2026
4d23ee0
more generic dawn plugin
CedricGuillemet Jul 17, 2026
c35a939
bootstrap
CedricGuillemet Jul 17, 2026
c431bf7
fix pg cmake
CedricGuillemet Jul 17, 2026
5b12b9d
iteration
CedricGuillemet Jul 17, 2026
da4388a
vis tests
CedricGuillemet Jul 17, 2026
295f7a0
fixes for 15scenes
CedricGuillemet Jul 17, 2026
1e12267
bundles rendering
CedricGuillemet Jul 21, 2026
ef841d6
blob decoder
CedricGuillemet Jul 23, 2026
286faaf
more tests
CedricGuillemet Jul 24, 2026
d091581
exclusion apis
CedricGuillemet Jul 24, 2026
bd5ea89
fix PG pollution
CedricGuillemet Jul 24, 2026
84cda40
tests
CedricGuillemet Jul 24, 2026
a61358e
more fixes
CedricGuillemet Jul 24, 2026
8dd981a
up rendercount
CedricGuillemet Jul 27, 2026
025b538
disable test
CedricGuillemet Jul 27, 2026
838fa2b
warp tests
CedricGuillemet Jul 27, 2026
f22f7cf
CI: NativeDawn-only + Babylon-Lite scene1 bundle smoke test
CedricGuillemet Jul 28, 2026
5061e1b
NativeDawn: broaden software-adapter detection for WARP MSAA workaround
CedricGuillemet Jul 28, 2026
f3cbc11
config: exclude 'FrameGraph image processing' on WebGPU (CI WARP MSAA…
CedricGuillemet Jul 28, 2026
2729c00
NativeDawn: drain V8 N-API finalizer queue to stop GPU-resource leak
CedricGuillemet Jul 28, 2026
32761cf
[TEMP] CI: isolation-run representative NativeDawn failures for diagn…
CedricGuillemet Jul 28, 2026
f459eda
NativeDawn: port to the WebGPU C API (#11)
bkaradzic-microsoft Aug 24, 2026
ff71460
Test NativeDawn render canvas lookup
CedricGuillemet Aug 24, 2026
a3e734b
Restore the JsRuntimeHost pin lost in the master merge (#12)
bkaradzic-microsoft Aug 25, 2026
ddf1d62
Fix NativeDawn render canvas lookup
CedricGuillemet Aug 25, 2026
5e7e7b6
Fix NativeDawn canvas DOM behavior
CedricGuillemet Aug 26, 2026
aee3e03
Re-enable full CI matrix
CedricGuillemet Aug 26, 2026
7bfe7d2
Restore NativeDawn validation and unit tests
CedricGuillemet Aug 26, 2026
8124e26
Fix UWP diagnostics and Linux line validation
CedricGuillemet Aug 26, 2026
e3f579f
Allow deterministic line raster differences
CedricGuillemet Aug 26, 2026
f620421
Validate NativeDawn across CI adapters
CedricGuillemet Aug 26, 2026
cbb2f4e
Remove superseded NativeDawn shims and isolate Playground host policy
CedricGuillemet Oct 5, 2026
bcc0590
Complete NativeDawn Canvas parity and validation tooling
CedricGuillemet Oct 7, 2026
9312c4d
Merge master into NativeDawn WebGPU plugin
CedricGuillemet Oct 7, 2026
2222ce8
Merge latest master into NativeDawn branch
CedricGuillemet Oct 12, 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
67 changes: 67 additions & 0 deletions .github/scripts/bundle-lite-parity.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { createRequire } from "node:module";
import { existsSync } from "node:fs";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { pathToFileURL } from "node:url";

const clone = resolve(process.argv[2] ?? "build\\lite-parity");
const output = resolve(process.argv[3] ?? "build\\dawn\\Apps\\Playground\\Scripts\\lite-parity");
const require = createRequire(resolve(clone, "package.json"));
const { build } = await import(pathToFileURL(require.resolve("esbuild")));
const catalog = JSON.parse(await readFile(resolve(clone, "scene-config.json"), "utf8"));
if (!Array.isArray(catalog) || new Set(catalog.map(scene => scene.id)).size !== catalog.length) {
throw new Error("Expected a unique scene array in scene-config.json");
}
const library = resolve(clone, "packages\\babylon-lite\\build\\lib");
if (!existsSync(resolve(library, "index.js"))) {
throw new Error("Run Babylon-Lite build:lib before bundling parity scenes");
}
await mkdir(output, { recursive: true });
const manifest = [];
for (const scene of catalog) {
const entry = resolve(clone, `lab\\lite\\src\\lite\\scene${scene.id}.ts`);
const file = resolve(output, `scene${scene.id}.js`);
try {
const result = await build({
entryPoints: [entry],
bundle: true,
splitting: false,
format: "iife",
platform: "browser",
target: "esnext",
minify: false,
keepNames: true,
legalComments: "none",
sourcemap: "inline",
outfile: file,
metafile: true,
alias: { "babylon-lite": library },
define: { "import.meta.url": "globalThis.__liteParityModuleUrl" },
loader: { ".wgsl": "text", ".glsl": "text", ".wasm": "file", ".ttf": "file" },
assetNames: "assets/[name]-[hash]",
plugins: [{
name: "vite-asset-queries",
setup(builder) {
builder.onResolve({ filter: /\?(raw|url)$/ }, args => ({
path: resolve(args.resolveDir, args.path.replace(/\?(raw|url)$/, "")),
namespace: args.path.endsWith("?raw") ? "raw-asset" : "file",
}));
builder.onLoad({ filter: /.*/, namespace: "raw-asset" }, async args => ({
contents: await readFile(args.path, "utf8"),
loader: "text",
}));
},
}],
logLevel: "silent",
});
await writeFile(resolve(output, `scene${scene.id}.meta.json`), JSON.stringify(result.metafile));
manifest.push({ ...scene, entry, bundle: file, build: "PASS" });
} catch (error) {
const message = error.errors?.map(item => `${item.location?.file ?? entry}: ${item.text}`).join("\n") ?? String(error);
await writeFile(resolve(output, `scene${scene.id}.build.log`), message);
manifest.push({ ...scene, entry, bundle: file, build: "FAIL", buildError: message });
}
await writeFile(resolve(output, "manifest.json"), JSON.stringify(manifest, null, 2));
console.log(`scene${scene.id}: ${manifest.at(-1).build}`);
}
console.log(`Bundled ${manifest.filter(scene => scene.build === "PASS").length}/${catalog.length} scenes into ${dirname(resolve(output, "manifest.json"))}`);
76 changes: 76 additions & 0 deletions .github/scripts/bundle-lite-scene.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Bundle a single Babylon-Lite lab scene into one self-contained IIFE "classic"
// script that a standalone V8 JS engine (the BabylonNative Playground on the
// NativeDawn/WebGPU backend) can load with a plain XHR + eval -- no bundler, no
// HTTP server, no ESM/code-splitting.
//
// The official Babylon-Lite build (`pnpm build:bundle-scenes`) emits ESM with
// code-splitting, which the Playground's classic loader can't consume. This
// script re-bundles the same scene entry (e.g. lab/lite/src/lite/scene1.ts)
// against the prebuilt library (packages/babylon-lite/build/lib) as a single
// minified IIFE, inlining every dynamic import.
//
// Prerequisites (run in the Babylon-Lite clone first):
// pnpm install
// pnpm build:lib # produces packages/babylon-lite/build/lib
//
// Usage (cwd = Babylon-Lite clone, so `esbuild` resolves from its node_modules):
// pnpm exec node <path>/bundle-lite-scene.mjs <cloneDir> <scene> <outFile>
// e.g. pnpm exec node .../bundle-lite-scene.mjs . scene1 ./scene1.playground.js
import { resolve } from "node:path";
import { existsSync } from "node:fs";
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";

const cloneDir = resolve(process.argv[2] || process.cwd());
const scene = process.argv[3] || "scene1";
const outFile = resolve(process.argv[4] || resolve(cloneDir, `${scene}.playground.js`));

// Resolve esbuild from the Babylon-Lite clone's node_modules (this script lives
// in the BabylonNative repo, so a bare `import "esbuild"` would resolve against
// the wrong tree). Babylon-Lite already declares esbuild as a direct dependency.
const requireFromClone = createRequire(resolve(cloneDir, "package.json"));
const esbuildEntry = requireFromClone.resolve("esbuild");
const { build } = await import(pathToFileURL(esbuildEntry));

const entryCandidates = [
resolve(cloneDir, `lab/lite/src/lite/${scene}.ts`),
resolve(cloneDir, `lab/src/lite/${scene}.ts`),
];
const entry = entryCandidates.find(existsSync);
if (!entry) {
console.error("[bundle-lite-scene] scene entry not found. Tried:\n " + entryCandidates.join("\n "));
process.exit(1);
}

const libDir = resolve(cloneDir, "packages/babylon-lite/build/lib");
if (!existsSync(libDir)) {
console.error("[bundle-lite-scene] prebuilt lib missing: " + libDir + "\n Run `pnpm build:lib` first.");
process.exit(1);
}

console.log("[bundle-lite-scene] entry : " + entry);
console.log("[bundle-lite-scene] lib : " + libDir);
console.log("[bundle-lite-scene] out : " + outFile);

try {
await build({
entryPoints: [entry],
bundle: true,
splitting: false, // single file: inline all dynamic imports
format: "iife", // classic script (no import/export)
platform: "browser",
target: "esnext",
minify: true,
keepNames: true, // Babylon-Lite relies on Function.name
legalComments: "none",
sourcemap: false,
outfile: outFile,
alias: { "babylon-lite": libDir },
loader: { ".wgsl": "text", ".glsl": "text" },
logLevel: "info",
});
console.log("[bundle-lite-scene] wrote " + outFile);
} catch (err) {
console.error("[bundle-lite-scene] build failed:", err);
process.exit(1);
}
67 changes: 67 additions & 0 deletions .github/scripts/capture-dawn-provenance.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";

const build = resolve(process.argv[2] ?? "build\\dawn");
const output = resolve(process.argv[3] ?? "build\\lite-parity-postfix-results\\provenance.json");
const sha256 = bytes => createHash("sha256").update(bytes).digest("hex");
const cache = Object.fromEntries((await readFile(resolve(build, "CMakeCache.txt"), "utf8"))
.split(/\r?\n/).flatMap(line => {
const match = /^([^#/:]+):[^=]+=(.*)$/.exec(line);
return match ? [[match[1], match[2]]] : [];
}));

function dependencySource(override, source) {
const directory = cache[override] || cache[source];
if (!directory) {
throw new Error(`Missing dependency source in CMake cache: ${override} / ${source}`);
}
return resolve(directory);
}

async function identity(name, directory, excludeReports = false) {
const git = (...args) => execFileSync("git", ["-C", directory, ...args], { maxBuffer: 20 * 1024 * 1024 });
const exclusions = excludeReports ? [":!*.md"] : [];
const patch = git("diff", "--binary", "--no-ext-diff", "--no-textconv", "HEAD", "--", ".", ...exclusions);
const files = git("ls-files", "--others", "--exclude-standard", "-z").toString("utf8")
.split("\0").filter(file => file && (!excludeReports || !file.endsWith(".md"))).sort();
const newFiles = await Promise.all(files.map(async file => ({
file, sha256: sha256(await readFile(resolve(directory, file))),
})));
const manifest = Buffer.from(newFiles.map(({ file, sha256 }) => `${file}\t${sha256}\n`).join(""));
const archive = resolve(dirname(output), "source-patches", name);
await mkdir(archive, { recursive: true });
await writeFile(resolve(archive, "tracked.patch"), patch);
await writeFile(resolve(archive, "new-files.sha256"), manifest);
for (const file of files) {
const destination = resolve(archive, "new-files", file);
await mkdir(dirname(destination), { recursive: true });
await writeFile(destination, await readFile(resolve(directory, file)));
}
return {
directory, head: git("rev-parse", "HEAD").toString("utf8").trim(),
mergeHead: git("rev-parse", "--revs-only", "MERGE_HEAD").toString("utf8").trim() || null,
patchSHA256: sha256(Buffer.concat([patch, manifest])),
trackedPatchSHA256: sha256(patch), newFileManifestSHA256: sha256(manifest),
newFiles, excludedReports: excludeReports, archive,
};
}

const executablePath = resolve(process.argv[4] ?? resolve(build, "Apps", "Playground", "Playground.exe"));
const repositories = {
BabylonNative: await identity("BabylonNative", resolve("."), true),
JsRuntimeHost: await identity("JsRuntimeHost", dependencySource("FETCHCONTENT_SOURCE_DIR_JSRUNTIMEHOST", "JsRuntimeHost_SOURCE_DIR")),
UrlLib: await identity("UrlLib", dependencySource("FETCHCONTENT_SOURCE_DIR_URLLIB", "UrlLib_SOURCE_DIR")),
BabylonLite: await identity("BabylonLite", resolve("build\\lite-parity")),
};
await writeFile(output, JSON.stringify({
capturedAt: new Date().toISOString(),
repositories,
executable: { path: executablePath, sha256: sha256(await readFile(executablePath)) },
build,
cache: Object.fromEntries(Object.entries(cache).filter(([name]) =>
/^(CMAKE_(BUILD_TYPE|GENERATOR|CXX_COMPILER)|NAPI_JAVASCRIPT_ENGINE|BABYLON_NATIVE_PLUGIN_NATIVEDAWN|JSRUNTIMEHOST_POLYFILL_COMPRESSION|FETCHCONTENT_SOURCE_DIR_(URLLIB|JSRUNTIMEHOST))$/.test(name))),
fingerprintDefinition: "SHA256(git diff --binary --no-ext-diff --no-textconv HEAD bytes || UTF-8 manifest with ordinal-sorted new file name, tab, lowercase SHA256, LF)",
}, null, 2));
console.log(`Recorded source and executable provenance at ${output}`);
108 changes: 108 additions & 0 deletions .github/scripts/complete-lite-parity.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { mkdir, readFile, appendFile } from "node:fs/promises";
import { resolve, join, dirname } from "node:path";
import { createNativeHost } from "./lite-parity-page.mjs";

const clone = resolve(process.argv[2] ?? "build\\lite-parity");
const artifacts = resolve(process.argv[3] ?? "build\\lite-parity-results");
const playground = resolve(process.argv[4] ?? "build\\dawn\\Apps\\Playground\\Playground.exe");
const requested = process.argv[5]?.split(",").map(Number);
const catalog = JSON.parse(await readFile(join(clone, "scene-config.json"), "utf8"));
if (requested && (!/^\d+(,\d+)*$/.test(process.argv[5])
|| requested.some(id => !catalog.some(scene => scene.id === id)))) {
throw new Error("Requested coverage IDs must be a comma-separated list of catalog scene numbers");
}
if (requested?.includes(164)) {
console.log("Scene164requires the original pre-loss/capture/recovery spec; no generic readiness retry.");
}
const cases = (await readFile(join(artifacts, "cases.jsonl"), "utf8"))
.trim().split("\n").filter(Boolean).map(line => JSON.parse(line));
const attempted = new Set(cases.flatMap(test => test.history ?? [])
.filter(item => item.backend === "native")
.map(item => Number(/\/scene(\d+)\.html/.exec(item.url)?.[1])));
const queries = {
seekTime: {
5: 2, 7: 2, 11: 1.91, 12: .5, 20: 0, 23: 0, 26: 3, 34: 0, 39: 5,
58: .72, 59: .72, 115: 100 / 60, 150: 1, 151: .5, 152: 1.91, 153: 1,
154: .75, 155: 1, 156: 1.25, 157: 1.2, 158: 1.2, 169: 1.25, 211: .5,
218: 1, 219: 1, 231: .5, 240: .5, 241: 2, 242: 1, 243: 1, 244: 1,
245: 1, 246: 1, 250: 5, 251: .5, 253: 1, 254: 2, 255: 1, 302: 2,
},
captureFrame: {
40: 120, 41: 10, 42: 300, 43: 300, 46: 10, 47: 1, 100: 120, 101: 150,
102: 5, 103: 5, 104: 35, 105: 55, 106: 20, 290: 180,
},
captureAfter: { 44: 5, 45: 3 },
capture: { 48: 1, 49: 1 },
freeze: { 64: 1, 66: 1, 140: 1, 171: 1, 172: 1, 173: 1, 174: 1, 175: 1 },
msaa: { 51: 4 },
nocam: { 224: 1 },
};
await mkdir(join(artifacts, "coverage"), { recursive: true });
const host = await createNativeHost({
playground, liteDirectory: clone, assetBase: "http://127.0.0.1:5179",
bundleDirectory: join(dirname(playground), "Scripts", "lite-parity"),
artifactsDirectory: join(artifacts, "native-coverage"), timeoutMs: 180000,
});
try {
for (const scene of catalog.filter(scene => scene.id !== 164
&& (requested ? requested.includes(scene.id) : !attempted.has(scene.id)))) {
const page = await host.createPage();
const record = { id: scene.id, slug: scene.slug, status: "FAIL", history: page.history };
let sceneError;
page.on("console", message => {
if (message.type() === "error") {
sceneError = new Error(message.text());
}
});
page.on("pageerror", error => { sceneError = error; });
try {
const params = new URLSearchParams();
for (const [key, values] of Object.entries(queries)) {
if (scene.id in values) {
params.set(key, String(values[scene.id]));
}
}
const flags = ["ready"];
if (params.has("seekTime") || [283,284].includes(scene.id)) {
flags.push("animationFrozen");
}
if (params.has("captureFrame") || params.has("captureAfter") || params.has("capture")) {
flags.push("captureReady");
}
if (scene.id === 272) { flags.push("swapped"); }
if (scene.id === 273) { flags.push("added"); }
await page.goto(`http://127.0.0.1:5179/scene${scene.id}.html?${params}`);
const deadline = Date.now() + 180000;
while (Date.now() < deadline) {
if (sceneError) {
throw sceneError;
}
const dataset = await page.evaluate(() => ({ ...document.querySelector("canvas").dataset }));
record.dataset = dataset;
if (dataset.error) {
throw new Error(dataset.error);
}
if (flags.every(flag => dataset[flag] === "true")) {
record.status = "EXECUTION PASS";
break;
}
await page.waitForTimeout(50);
}
if (record.status !== "EXECUTION PASS") {
throw new Error("Native coverage readiness timeout after 180s; required " + flags.join(", "));
}
await page.waitForTimeout(500);
record.screenshot = join(artifacts, "coverage", `scene${scene.id}.png`);
await page.locator("canvas").screenshot({ path: record.screenshot });
} catch (error) {
record.status = /timeout/i.test(error.message) ? "TIMEOUT" : "FAIL";
record.error = { message: error.message, stack: error.stack };
} finally {
await page.close();
await appendFile(join(artifacts, "coverage.jsonl"), JSON.stringify(record) + "\n");
console.log(`Coverage scene${scene.id}: ${record.status}${record.error ? " — " + record.error.message.split("\n")[0] : ""}`);
}
}
} finally {
await host.close();
}
55 changes: 55 additions & 0 deletions .github/scripts/diagnose-lite-parity.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { readFile, writeFile } from "node:fs/promises";
import { dirname, resolve, join } from "node:path";
import { fileURLToPath } from "node:url";
import { spawn } from "node:child_process";

const clone = resolve(process.argv[2] ?? "build\\lite-parity");
const nativeArtifacts = resolve(process.argv[3] ?? "build\\lite-parity-results");
const browserArtifacts = resolve(process.argv[4] ?? "build\\lite-parity-browser-results");
const playground = resolve(process.argv[5] ?? "build\\dawn\\Apps\\Playground\\Playground.exe");
const scripts = dirname(fileURLToPath(import.meta.url));
function run(file, args, options = {}) {
return new Promise((resolveRun, reject) => {
const child = spawn(process.execPath, [file, ...args], { stdio: "inherit", ...options });
child.once("error", reject);
child.once("exit", (code, signal) => {
if (signal) {
reject(new Error(`Diagnostic process terminated by ${signal}`));
} else {
resolveRun(code);
}
});
});
}
const records = (await readFile(join(nativeArtifacts, "cases.jsonl"), "utf8"))
.trim().split("\n").filter(Boolean).map(line => JSON.parse(line));
const files = Array.from(new Set(records
.filter(test => test.status !== "passed" && /[\\/]scenes[\\/]/.test(test.location.file)
&& test.history.some(item => item.backend === "native"))
.map(test => test.location.file)));
if (!files.length) {
console.log("No failed native scene cases require browser-Lite diagnostic replay.");
process.exit(0);
}
const preparation = await run(join(scripts, "prepare-lite-parity.mjs"),
[clone, playground, browserArtifacts, "60000"]);
if (preparation !== 0) {
throw new Error("Cannot prepare browser diagnostic suite: exit " + preparation);
}
await writeFile(join(browserArtifacts, "diagnostic-files.json"), JSON.stringify(files, null, 2));
const filter = files.map(file => file.split(/[\\/]/).at(-1).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
const result = await run(join(clone, "node_modules", "@playwright", "test", "cli.js"),
["test", "--config", ".native-parity\\playwright.config.mjs", filter], {
cwd: clone,
env: {
...process.env, NATIVE_PARITY_BROWSER: "1", NATIVE_PARITY_REPORT: "0",
REUSE_BROWSER: "false",
},
});
const report = await run(join(scripts, "report-lite-parity.mjs"), [
clone, nativeArtifacts, resolve("Dawn-Lite-Parity.md"), browserArtifacts,
], { cwd: resolve(".") });
if (report !== 0) {
throw new Error("Cannot refresh the parity report: exit " + report);
}
process.exitCode = result;
Loading
Loading