From 2063a99b09908732230ae4baf3900425f1075b9f Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Tue, 22 Oct 2024 20:19:34 +0200 Subject: [PATCH 1/2] Enable Multivalue again With LLVM 19 they explicitly require using the `experimental-mv` ABI in addition to the `multivalue` LLVM feature (the latter is enabled by default now). Enabling the ABI is non-trivial and requires a full on custom Rust target. This adds `wasm32-multivalue.json` which is identical to `wasm32-unknown-unknown` except it sets the `experimental-mv` ABI. The target JSON format is very experimental by itself, so we may need to keep this up to date from time to time. --- buildCore.js | 57 ++++++++++++++++++++++++----------------- wasm32-multivalue.json | 58 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 24 deletions(-) create mode 100644 wasm32-multivalue.json diff --git a/buildCore.js b/buildCore.js index de543294..02c8078a 100644 --- a/buildCore.js +++ b/buildCore.js @@ -2,18 +2,21 @@ import { execSync } from "child_process"; import fs from "fs"; let toolchain = ""; -let targetFolder = "debug"; +let profile = "debug"; let cargoFlags = ""; -let rustFlags = "-C target-feature=+bulk-memory,+mutable-globals,+nontrapping-fptoint,+sign-ext,+simd128,+extended-const"; +let rustFlags = + "-C target-feature=+bulk-memory,+mutable-globals,+nontrapping-fptoint,+sign-ext,+simd128,+extended-const,+multivalue"; let wasmBindgenFlags = ""; +let target = "wasm32-unknown-unknown"; +let targetFolder = target; if (process.argv.some((v) => v === "--max-opt")) { // Do a fully optimized build ready for deployment. - targetFolder = "max-opt"; + profile = "max-opt"; cargoFlags = "--profile max-opt"; } else if (process.argv.some((v) => v === "--release")) { // Do an optimized build. - targetFolder = "release"; + profile = "release"; cargoFlags = "--release"; } else { // Do a debug build. @@ -23,19 +26,28 @@ if (process.argv.some((v) => v === "--max-opt")) { // Use WASM features that may not be supported by all the browsers. if (process.argv.some((v) => v === "--unstable")) { // Relaxed SIMD is not supported by Firefox and Safari yet. - // Tail calls are not supported by Safari and wasm-bindgen yet. - rustFlags += ",+relaxed-simd"; //,+tail-call"; + rustFlags += ",+relaxed-simd"; + + // Tail calls are not supported by Safari yet. + rustFlags += ",+tail-call"; + + // Reference types are broken in webpack (or rather its underlying webassemblyjs): + // https://github.com/LiveSplit/LiveSplitOne/issues/630 + // wasmBindgenFlags += " --reference-types"; } // Use the nightly toolchain, which enables some more optimizations. if (process.argv.some((v) => v === "--nightly")) { toolchain = "+nightly"; - cargoFlags += " -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort"; - rustFlags += ",+multivalue -Z wasm-c-abi=spec"; + cargoFlags += + " -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort"; + rustFlags += " -Z wasm-c-abi=spec"; + target = "../wasm32-multivalue.json"; + targetFolder = "wasm32-multivalue"; // Virtual function elimination requires LTO, so we can only do it for // max-opt builds. - if (targetFolder == "max-opt") { + if (profile == "max-opt") { // Seems like cargo itself calls rustc to check for file name patterns, // but it forgets to pass the LTO flag that we specified in the // Cargo.toml, so the virtual-function-elimination complains that it's @@ -45,33 +57,30 @@ if (process.argv.some((v) => v === "--nightly")) { } } -execSync( - `cargo ${toolchain} run`, - { - cwd: "livesplit-core/capi/bind_gen", - stdio: "inherit", - }, -); +execSync(`cargo ${toolchain} run`, { + cwd: "livesplit-core/capi/bind_gen", + stdio: "inherit", +}); execSync( - `cargo ${toolchain} rustc -p livesplit-core-capi --crate-type cdylib --features wasm-web,web-rendering --target wasm32-unknown-unknown ${cargoFlags}`, + `cargo ${toolchain} rustc -p livesplit-core-capi --crate-type cdylib --features wasm-web,web-rendering --target ${target} ${cargoFlags}`, { cwd: "livesplit-core", stdio: "inherit", env: { ...process.env, - 'RUSTFLAGS': rustFlags, + RUSTFLAGS: rustFlags, }, - }, + } ); execSync( - `wasm-bindgen ${wasmBindgenFlags} livesplit-core/target/wasm32-unknown-unknown/${targetFolder}/livesplit_core.wasm --out-dir src/livesplit-core`, + `wasm-bindgen ${wasmBindgenFlags} livesplit-core/target/${targetFolder}/${profile}/livesplit_core.wasm --out-dir src/livesplit-core`, { stdio: "inherit", - }, + } ); -fs - .createReadStream("livesplit-core/capi/bindings/wasm_bindgen/index.ts") - .pipe(fs.createWriteStream("src/livesplit-core/index.ts")); +fs.createReadStream("livesplit-core/capi/bindings/wasm_bindgen/index.ts").pipe( + fs.createWriteStream("src/livesplit-core/index.ts") +); diff --git a/wasm32-multivalue.json b/wasm32-multivalue.json new file mode 100644 index 00000000..f074344a --- /dev/null +++ b/wasm32-multivalue.json @@ -0,0 +1,58 @@ +{ + "arch": "wasm32", + "crt-objects-fallback": "true", + "data-layout": "e-m:e-p:32:32-p10:8:8-p20:8:8-i64:64-n32:64-S128-ni:1:10:20", + "dll-prefix": "", + "dll-suffix": ".wasm", + "dynamic-linking": true, + "eh-frame-header": false, + "emit-debug-gdb-scripts": false, + "exe-suffix": ".wasm", + "generate-arange-section": false, + "has-thread-local": true, + "is-builtin": false, + "is-like-wasm": true, + "limit-rdylib-exports": false, + "linker": "rust-lld", + "linker-flavor": "wasm-lld", + "linker-is-gnu": false, + "lld-flavor": "wasm", + "llvm-abiname": "experimental-mv", + "llvm-target": "wasm32-unknown-unknown", + "max-atomic-width": 64, + "metadata": { + "description": "WebAssembly", + "host_tools": false, + "std": true, + "tier": 2 + }, + "only-cdylib": true, + "os": "unknown", + "panic-strategy": "abort", + "pre-link-args": { + "wasm-lld": [ + "-z", + "stack-size=1048576", + "--stack-first", + "--allow-undefined", + "--no-demangle", + "--no-entry" + ], + "wasm-lld-cc": [ + "-Wl,-z", + "-Wl,stack-size=1048576", + "-Wl,--stack-first", + "-Wl,--allow-undefined", + "-Wl,--no-demangle", + "--target=wasm32-unknown-unknown", + "-Wl,--no-entry" + ] + }, + "relocation-model": "static", + "singlethread": true, + "target-family": [ + "wasm" + ], + "target-pointer-width": "32", + "tls-model": "local-exec" +} From da58c85e3a2983407ed97b638c855133ad3d16d3 Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Tue, 22 Oct 2024 20:38:31 +0200 Subject: [PATCH 2/2] Fix tests --- test/rendering-test.js | 164 ++++++++++++++++++++++++++++++----------- 1 file changed, 119 insertions(+), 45 deletions(-) diff --git a/test/rendering-test.js b/test/rendering-test.js index 5854c4fe..0b3d1300 100644 --- a/test/rendering-test.js +++ b/test/rendering-test.js @@ -1,13 +1,9 @@ import fs from "fs"; import path from "path"; -import { createServer } from 'http-server'; -import { - Builder, - By, - until, -} from "selenium-webdriver"; +import { createServer } from "http-server"; +import { Builder, By, until } from "selenium-webdriver"; import chrome from "selenium-webdriver/chrome.js"; -import { } from "chromedriver"; +import {} from "chromedriver"; import imghash from "imghash"; import hex64 from "hex64"; import leven from "leven"; @@ -39,30 +35,37 @@ describe("Layout Rendering Tests", function () { const clickElement = async (selector) => { const element = await findElement(selector); await element.click(); - } + }; const loadFile = async (filePath) => { const inputElement = await findElement(By.id("file-input")); await inputElement.sendKeys(path.resolve(filePath)); await driver.sleep(500); - } + }; const hashToBinary = (hash) => { const buffer = Buffer.from(hash, "base64"); const values = Array.from(buffer.values()); - return values.map((value) => value.toString(2).padStart(8, "0")).join(""); + return values + .map((value) => value.toString(2).padStart(8, "0")) + .join(""); }; before(async () => { - console.log('Starting server...'); + console.log("Starting server..."); await startServer(); - console.log('Server started!'); - console.log('Preparing WebDriver for tests...'); + console.log("Server started!"); + console.log("Preparing WebDriver for tests..."); - const options = new chrome.Options().windowSize({ width: 1200, height: 2400 }).addArguments("--headless"); - driver = await new Builder().forBrowser("chrome").setChromeOptions(options).build(); + const options = new chrome.Options() + .windowSize({ width: 1200, height: 2400 }) + .addArguments("--headless"); + driver = await new Builder() + .forBrowser("chrome") + .setChromeOptions(options) + .build(); await driver.get("http://localhost:8081"); @@ -75,29 +78,41 @@ describe("Layout Rendering Tests", function () { this.style.display = "none"; this.id = "file-input"; document.body.appendChild(this); - } + }; }); if (!fs.existsSync(SCREENSHOTS_FOLDER)) { fs.mkdirSync(SCREENSHOTS_FOLDER); } - console.log('Ready to run tests!'); + console.log("Ready to run tests!"); }); const testRendering = (layoutName, splitsName, expectedHash) => { it(`Renders the ${layoutName} layout with the ${splitsName} splits correctly`, async function () { this.timeout(10000); - await clickElement(By.xpath(".//button[contains(text(), 'Layout')]")); - await clickElement(By.xpath(".//button[contains(text(), 'Import')]")); + await clickElement( + By.xpath(".//button[contains(text(), 'Layout')]") + ); + await clickElement( + By.xpath(".//button[contains(text(), 'Import')]") + ); await loadFile(`${LAYOUTS_FOLDER}/${layoutName}.ls1l`); await clickElement(By.xpath(".//button[contains(text(), 'Back')]")); - await clickElement(By.xpath(".//button[contains(text(), 'Splits')]")); - await clickElement(By.xpath(".//button[contains(text(), 'Import')]")); + await clickElement( + By.xpath(".//button[contains(text(), 'Splits')]") + ); + await clickElement( + By.xpath(".//button[contains(text(), 'Import')]") + ); await loadFile(`${SPLITS_FOLDER}/${splitsName}.lss`); - await clickElement(By.xpath("(.//button[contains(@aria-label, 'Open Splits')])[last()]")); + await clickElement( + By.xpath( + "(.//button[contains(@aria-label, 'Open Splits')])[last()]" + ) + ); await clickElement(By.xpath(".//button[contains(text(), 'Back')]")); const layoutElement = await findElement(By.className("layout")); @@ -113,8 +128,14 @@ describe("Layout Rendering Tests", function () { const actualHashHex = await imghash.hash(tempFilePath, 24); const actualHash = hex64.encode(actualHashHex); - const actualScreenshotPath = `${SCREENSHOTS_FOLDER}/${layoutName}_${splitsName}_${actualHash.replace("/", "$")}.png`; - const expectedScreenshotPath = `${SCREENSHOTS_FOLDER}/${layoutName}_${splitsName}_${expectedHash.replace("/", "$")}.png`; + const actualScreenshotPath = `${SCREENSHOTS_FOLDER}/${layoutName}_${splitsName}_${actualHash.replace( + "/", + "$" + )}.png`; + const expectedScreenshotPath = `${SCREENSHOTS_FOLDER}/${layoutName}_${splitsName}_${expectedHash.replace( + "/", + "$" + )}.png`; fs.renameSync(tempFilePath, actualScreenshotPath); @@ -126,42 +147,95 @@ describe("Layout Rendering Tests", function () { let showWarning = false; try { if (fs.existsSync(expectedScreenshotPath)) { - const actualImage = PNG.sync.read(fs.readFileSync(actualScreenshotPath)); - const expectedImage = PNG.sync.read(fs.readFileSync(expectedScreenshotPath)); + const actualImage = PNG.sync.read( + fs.readFileSync(actualScreenshotPath) + ); + const expectedImage = PNG.sync.read( + fs.readFileSync(expectedScreenshotPath) + ); const { width, height } = actualImage; const diff = new PNG({ width, height }); - const numPixelsDifferent = pixelmatch(actualImage.data, expectedImage.data, diff.data, width, height, { threshold: 0.2 }); + const numPixelsDifferent = pixelmatch( + actualImage.data, + expectedImage.data, + diff.data, + width, + height, + { threshold: 0.2 } + ); if (numPixelsDifferent === 0) { showWarning = true; } - fs.writeFileSync(`${SCREENSHOTS_FOLDER}/${layoutName}_${splitsName}_diff.png`, PNG.sync.write(diff)); + fs.writeFileSync( + `${SCREENSHOTS_FOLDER}/${layoutName}_${splitsName}_diff.png`, + PNG.sync.write(diff) + ); } - } - finally { + } finally { if (showWarning) { - console.warn(`Render match despite mismatching hashes (${layoutName} layout with ${splitsName} splits)! ` + - `Expected hash: ${expectedHash}, actual hash: ${actualHash}`); + console.warn( + `Render match despite mismatching hashes (${layoutName} layout with ${splitsName} splits)! ` + + `Expected hash: ${expectedHash}, actual hash: ${actualHash}` + ); } else { - throw Error(`Render mismatch (${layoutName} layout with ${splitsName} splits)! ` + - `Expected hash: ${expectedHash}, actual hash: ${actualHash}`) + throw Error( + `Render mismatch (${layoutName} layout with ${splitsName} splits)! ` + + `Expected hash: ${expectedHash}, actual hash: ${actualHash}` + ); } } } }); - } - - testRendering("all_components", "default", "PwD-XAAAAAA2AAAO____________________AAAAAAAAAAAA____AAAA____AAAA____AAAAf_AAd_AAAAD-Nz___34HbwAG"); - testRendering("all_components", "pmw3", "PwD-XAAAAAA2AAAO_________sAPf8AAf4AC3_AH_-AOX-APXfAGX-AP18AOf-APXeAOX-AOD7AOQIAIAAD-b_j_____LwBs"); - testRendering("default", "default", "________8AADVVVVAAAAAAAAAAAA____________AAAAAAAAAAAA____VVVVAAAA____AAAAVVVVAAAAAADgAAD_________"); - testRendering("default", "pmw3", "b_gAb9-HV8AG__AHV_AG7kAGX-AGV-AOX8APX_gOX-AP28AO_-APX-APXeAOUQAOX-AOX_gP_-AfAAAAAADgAAD_q97_____"); - testRendering("splits_two_rows", "celeste", "b4AAYAAH________bwAA4AAHb-AA4AAHb-AAYAAHbwAA____bwAAYAAH_9VV____b_AAYAAH__wA____b_AAYAAPb8AAYAAP"); - testRendering("splits_with_labels", "celeste", "AAAAAAA8AAA3AAA3AAA3________WAAHfwAHfwAHTwAHVVVVAAAA0gAH_wAH_wAH3wAH_________IAP34AP34AP38APAAAA"); - testRendering("title_centered_no_game_icon", "celeste", "________MzMzADAAADwAADwA________VX1VADwAAAAAAAAA____ABkAAP8DAP8DAP8DAP8D________V_1XAGAAAAAAAAAA"); - testRendering("title_centered_with_game_icon", "celeste", "________AiIiQDAAYDwAYDwA________YDwAYDwAYAAAYAAAd393YBkAYL8DYP8DYP8DYP8D________d_1XYGAAAAAAAAAA"); - testRendering("title_left_no_attempt_count", "celeste", "________MzMz4AAA8AAA-AAA_________VVV-AAAAAAAAAAA____GQAA_wAA_wAA_wAA_wAA________f_1XIAAAAAAAAAAA"); + }; + + testRendering( + "all_components", + "default", + "fwB-WgAAAAA2AAAO____________________AAAAAAAAAAAA____AAAA____AAAA____AAAAf_AAf_AAAAD-Mj___34HbwAG" + ); + testRendering( + "all_components", + "pmw3", + "fwB-WgAAAAA2AAAO_________8AGf8AAf4AC3_AH_-APX-APXfAGX-APV8AOX-APX-APX-AOL_AOQIAIAAD-b_j_____LgAs" + ); + testRendering( + "default", + "default", + "________8AADVVVVAAAAAAAAAAAA____________AAAAAAAAAAAA____VVVVAAAA____AAAAVVVVAAAAAADgAAD_________" + ); + testRendering( + "default", + "pmw3", + "b_gAb_-GV8AG3_AHV_AGXuAGX-AGV-AOX-AOX_AOX-AP38AO3-APX-APXeAOWWAOXeAOX_gO_-AfAAAAAADgAAD_q97_____" + ); + testRendering( + "splits_two_rows", + "celeste", + "b4AAYAAH________bwAA4AAHb-AA4AAHb-AAYAAHbwAA____bwAAYAAH_9VV____b_AAYAAH__wA____b_AAYAAPb8AAYAAP" + ); + testRendering( + "splits_with_labels", + "celeste", + "AAAAAAA8AAA_AAA3AAAn________WAAHfwAHfwAHXwAHVVVVAAAA2wAH_wAH_wAH3wAH________9IAP_4AP34AP34APAAAA" + ); + testRendering( + "title_centered_no_game_icon", + "celeste", + "________MzMzADwAADwAADwA________VX1VADwAAAAAAAAA____ABkDAP8DAP8DAP8DAP8D________V_1XACEAAAAAAAAA" + ); + testRendering( + "title_centered_with_game_icon", + "celeste", + "________AjIiQDwAYDwAYDwA________YDwAYDwAYAAAYAAAczszYBkDYP8DYP8DYP8DYP8D________d_1XYCEAAAAAAAAA" + ); + testRendering( + "title_left_no_attempt_count", + "celeste", + "________IiIi8AAA-AAA-AAA_________VVV-AAAAAAAAAAA____GQAA_wAA_wAA_wAA_wAA________f_1XKQAAAAAAAAAA" + ); after(async () => { await driver.quit();