From 5ebd70a31f57d27150ffe8dedaf65fb048c9675b Mon Sep 17 00:00:00 2001 From: Nicolas Cannasse Date: Fri, 9 Oct 2026 17:49:22 +0200 Subject: [PATCH 001/191] switch hide js to electron --- .github/workflows/main.yml | 19 +- .gitignore | 4 +- .vscode/launch.json | 93 +++-- .vscode/tasks.json | 26 ++ CLAUDE.md | 18 + README.md | 14 +- bin/app.html | 30 +- bin/cdb.cmd | 3 + bin/hide.cmd | 3 +- bin/package-cdb.json | 13 - bin/package.json | 24 +- bin/prompt.html | 42 ++ bin/test-preload.js | 9 + hide.hxml | 14 +- hide/Plugin.hx | 1 + hide_js/hide/ElectronMain.hx | 473 +++++++++++++++++++++++ hide_js/hide/Ide.hx | 233 ++++------- hide_js/hide/comp/ColorPicker.hx | 6 +- hide_js/hide/comp/cdb/Editor.hx | 8 +- hide_js/hide/tools/DragAndDrop.hx | 2 +- hide_js/hide/tools/FileManager.hx | 21 +- hide_js/hide/tools/ThumbnailGenerator.hx | 8 +- hide_js/hide/ui/Menu.hx | 14 +- hide_js/hide/ui/View.hx | 6 +- hide_js/hide/view/DevTools.hx | 18 +- hide_js/hide/view/FileView.hx | 2 +- hide_js/hide/view/Gym.hx | 9 +- hide_js/hide/view/Model.hx | 2 +- hide_js/libs/electron/App.hx | 74 ++++ hide_js/libs/electron/Clipboard.hx | 38 ++ hide_js/libs/electron/Dialog.hx | 33 ++ hide_js/libs/electron/Ipc.hx | 63 +++ hide_js/libs/electron/Menu.hx | 21 + hide_js/libs/electron/MenuItem.hx | 57 +++ hide_js/libs/electron/Screen.hx | 30 ++ hide_js/libs/electron/Window.hx | 152 ++++++++ hide_js/libs/electron/main/Electron.hx | 123 ++++++ hide_js/libs/nw/App.hx | 11 - hide_js/libs/nw/Clipboard.hx | 25 -- hide_js/libs/nw/Menu.hx | 14 - hide_js/libs/nw/MenuItem.hx | 18 - hide_js/libs/nw/Screen.hx | 32 -- hide_js/libs/nw/Window.hx | 39 -- 43 files changed, 1401 insertions(+), 444 deletions(-) create mode 100644 CLAUDE.md create mode 100644 bin/cdb.cmd delete mode 100644 bin/package-cdb.json create mode 100644 bin/prompt.html create mode 100644 bin/test-preload.js create mode 100644 hide_js/hide/ElectronMain.hx create mode 100644 hide_js/libs/electron/App.hx create mode 100644 hide_js/libs/electron/Clipboard.hx create mode 100644 hide_js/libs/electron/Dialog.hx create mode 100644 hide_js/libs/electron/Ipc.hx create mode 100644 hide_js/libs/electron/Menu.hx create mode 100644 hide_js/libs/electron/MenuItem.hx create mode 100644 hide_js/libs/electron/Screen.hx create mode 100644 hide_js/libs/electron/Window.hx create mode 100644 hide_js/libs/electron/main/Electron.hx delete mode 100644 hide_js/libs/nw/App.hx delete mode 100644 hide_js/libs/nw/Clipboard.hx delete mode 100644 hide_js/libs/nw/Menu.hx delete mode 100644 hide_js/libs/nw/MenuItem.hx delete mode 100644 hide_js/libs/nw/Screen.hx delete mode 100644 hide_js/libs/nw/Window.hx diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ef3758b26..bcf3a0167 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -61,7 +61,7 @@ jobs: needs: build runs-on: ubuntu-latest env: - nwjs-version: v0.101.2 + electron-version: v44.7.0 steps: - uses: actions/checkout@v6 - name: Download js @@ -73,33 +73,30 @@ jobs: run: sudo apt-get install p7zip-full - name: Windows run: | - wget -O nwjs-win.zip https://dl.nwjs.io/${{env.nwjs-version}}/nwjs-sdk-${{env.nwjs-version}}-win-x64.zip - unzip nwjs-win.zip + wget -O electron-win.zip https://github.com/electron/electron/releases/download/${{env.electron-version}}/electron-${{env.electron-version}}-win32-x64.zip mkdir -p zips cp -r bin zips/windows - cp -r nwjs-sdk-${{env.nwjs-version}}-win-x64 zips/windows/nwjs + unzip -q electron-win.zip -d zips/windows/electron cd zips/windows 7z a -mm=Deflate -mx=9 ../windows.zip * cd ../.. rm -rf zips/windows - name: Linux run: | - wget -O nwjs-linux.tar.gz https://dl.nwjs.io/${{env.nwjs-version}}/nwjs-sdk-${{env.nwjs-version}}-linux-x64.tar.gz - tar -xvzf nwjs-linux.tar.gz + wget -O electron-linux.zip https://github.com/electron/electron/releases/download/${{env.electron-version}}/electron-${{env.electron-version}}-linux-x64.zip mkdir -p zips cp -r bin zips/linux - cp -r nwjs-sdk-${{env.nwjs-version}}-linux-x64 zips/linux/nwjs + unzip -q electron-linux.zip -d zips/linux/electron cd zips/linux 7z a -mm=Deflate -mx=9 ../linux.zip * cd ../.. rm -rf zips/linux - name: Mac run: | - wget -O nwjs-mac.zip https://dl.nwjs.io/${{env.nwjs-version}}/nwjs-sdk-${{env.nwjs-version}}-osx-x64.zip - unzip nwjs-mac.zip + wget -O electron-mac.zip https://github.com/electron/electron/releases/download/${{env.electron-version}}/electron-${{env.electron-version}}-darwin-x64.zip mkdir -p zips/mac - cp -r nwjs-sdk-${{env.nwjs-version}}-osx-x64/nwjs.app zips/mac/nwjs.app - cp -r bin zips/mac/nwjs.app/Contents/Resources/app.nw + unzip -q electron-mac.zip -d zips/mac + cp -r bin zips/mac/Electron.app/Contents/Resources/app cd zips/mac 7z a -mm=Deflate -mx=9 ../mac.zip * cd ../.. diff --git a/.gitignore b/.gitignore index 759763ab6..f3be2297f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,6 @@ shaders/ api.xml bin_hl/ -*.dump \ No newline at end of file +*.dump +/bin/electron +/bin/main.js diff --git a/.vscode/launch.json b/.vscode/launch.json index ccaf00871..cca5f6f4b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,54 +4,56 @@ // Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + // Electron main process (ElectronMain.hx), used by the "Launch" compounds below { - "type": "nwjs", + "type": "node", "request": "launch", - "name": "Launch HIDE", - "nwjsVersion": "any", - "webRoot": "${workspaceFolder}/bin", - "env": {"HIDE_DEBUG":"1"}, - "preLaunchTask": "Build", - "reloadAfterAttached": false - }, - { - "type": "nwjs", - "request": "launch", - "name": "Launch HIDE (Enable logging)", - "nwjsVersion": "any", - "webRoot": "${workspaceFolder}/bin", - "env": {"HIDE_DEBUG":"1"}, - "runtimeArgs": ["--enable-logging", "--log-file=${workspaceFolder}/chrome_debug.log"], - "preLaunchTask": "Build", - "reloadAfterAttached": false + "name": "HIDE main process", + "cwd": "${workspaceFolder}/bin", + "runtimeExecutable": "${workspaceFolder}/bin/electron/electron.exe", + "args": [".", "--remote-debugging-port=9222"], + "env": {"HIDE_DEBUG":"1", "ELECTRON_RUN_AS_NODE": null}, + "outputCapture": "std", + "sourceMaps": true, + "presentation": { "hidden": true } }, { - "type": "nwjs", + "type": "node", "request": "launch", - "name": "Launch CDB", - "nwjsVersion": "any", - "webRoot": "${workspaceFolder}/bin", - "env": {"HIDE_DEBUG":"1","HIDE_START_CDB":"1"}, - "preLaunchTask": "Build" + "name": "CDB main process", + "cwd": "${workspaceFolder}/bin", + "runtimeExecutable": "${workspaceFolder}/bin/electron/electron.exe", + "args": [".", "--remote-debugging-port=9222", "--cdb"], + "env": {"HIDE_DEBUG":"1", "ELECTRON_RUN_AS_NODE": null}, + "outputCapture": "std", + "sourceMaps": true, + "presentation": { "hidden": true } }, + // Editor (hide.js), waits for the window started by the main process { "type": "chrome", "request": "attach", - "name": "Attach to HIDE", + "name": "HIDE editor", "address": "127.0.0.1", "port": 9222, + "timeout": 60000, + "urlFilter": "*app.html", + "sourceMaps": true, "webRoot": "${workspaceFolder}/bin", - "preLaunchTask": "Build" + "presentation": { "hidden": true } }, + // Attach to an already running HIDE (started with hide.cmd) { "type": "chrome", "request": "attach", - "name": "Attach to HIDE (chrome)", + "name": "Attach to HIDE", "address": "127.0.0.1", "port": 9222, - "preLaunchTask": "Build", - "sourceMaps": true, - "webRoot": "${workspaceFolder}/bin", + "urlFilter": "*app.html", + "sourceMaps": true, + "webRoot": "${workspaceFolder}/bin", + "preLaunchTask": "Build hide", + "presentation": { "group": "1_hide", "order": 3 } }, { "name": "hide_hl", @@ -59,10 +61,8 @@ "type": "hl", "cwd": "${workspaceFolder}", "port": 7779, - "preLaunchTask": { - "type": "haxe", - "args": "active configuration" - } + "preLaunchTask": "Build hide_hl", + "presentation": { "group": "2_hl", "order": 1 } }, { "name": "hide_hl --autoprofile", @@ -71,10 +71,25 @@ "cwd": "${workspaceFolder}", "args": ["--autoprofile"], "port": 7779, - "preLaunchTask": { - "type": "haxe", - "args": "active configuration" - } + "preLaunchTask": "Build hide_hl", + "presentation": { "group": "2_hl", "order": 2 } + } + ], + // build first, then start Electron and debug both the main process and the editor + "compounds": [ + { + "name": "Launch HIDE", + "configurations": ["HIDE main process", "HIDE editor"], + "preLaunchTask": "Build hide", + "stopAll": true, + "presentation": { "group": "1_hide", "order": 1 } + }, + { + "name": "Launch CDB", + "configurations": ["CDB main process", "HIDE editor"], + "preLaunchTask": "Build hide", + "stopAll": true, + "presentation": { "group": "1_hide", "order": 2 } } ] -} \ No newline at end of file +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 15cc7eba8..ec779db0d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -11,6 +11,32 @@ "isDefault": true }, "label": "Build" + }, + { + // always builds the Electron editor (hide.js + main.js), whatever the active configuration + "type": "hxml", + "file": "hide.hxml", + "problemMatcher": [ + "$haxe-absolute", + "$haxe", + "$haxe-error", + "$haxe-trace" + ], + "group": "build", + "label": "Build hide" + }, + { + // always builds the HashLink editor, whatever the active configuration + "type": "hxml", + "file": "hide_hl.hxml", + "problemMatcher": [ + "$haxe-absolute", + "$haxe", + "$haxe-error", + "$haxe-trace" + ], + "group": "build", + "label": "Build hide_hl" } ] } diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..11d6d9fc1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,18 @@ +# HIDE + +## Architecture + +- HIDE runs on Electron. The main process is Haxe code (`hide_js/hide/ElectronMain.hx`, built into `bin/main.js` by the second part of `hide.hxml` (after `--next`)); the renderer is `bin/app.html` + `bin/hide.js`. +- Renderer code talks to the main process only through the wrappers in `hide_js/libs/electron/` (`electron.Ipc`, `electron.Window`, `electron.Clipboard`, ...). Main process externs are in `hide_js/libs/electron/main/`. +- Keep main process code in Haxe, not hand-written JS. +- `haxe hide.hxml` builds both `bin/hide.js` and `bin/main.js`. The Electron runtime is unpacked in `bin/electron/`. + +## Running the editor for tests + +- When launching HIDE yourself to test something, ALWAYS set `HIDE_TEST=1`. In this mode: + - `alert` / `confirm` / `prompt` popups are replaced by console logs (`bin/test-preload.js`), so no blocking exception popup is shown to the user; + - windows are shown without activation and never focused / maximized, so they stay in the background behind the user's windows. +- Unset `ELECTRON_RUN_AS_NODE` (set by VSCode), and use `--enable-logging=stderr` to get the renderer console in the output: + `cd bin && env -u ELECTRON_RUN_AS_NODE HIDE_TEST=1 ./electron/electron.exe --enable-logging=stderr --remote-debugging-port=9223 . > log.txt 2>&1` +- With `--remote-debugging-port`, expressions can be evaluated in the page through the DevTools protocol (`http://127.0.0.1:9223/json/list` then `Runtime.evaluate`), e.g. `hide.Ide.inst.views.length`. +- Kill leftover instances (`taskkill /F /IM electron.exe`) before relaunching: the app is single instance. diff --git a/README.md b/README.md index f2c0e8108..330e387f3 100644 --- a/README.md +++ b/README.md @@ -37,18 +37,18 @@ haxelib git hashlink https://github.com/HaxeFoundation/hashlink master other/hax #### 2. Build Heaps IDE - Clone this repo - Run `haxe hide.hxml` -- This will create hide.js in the bin folder +- This will create hide.js (editor) and main.js (Electron main process) in the bin folder - If there are errors when compiling, make sure you are using the latest libraries from git -#### 3. Install NWJS -- Download and copy [NWJS](https://nwjs.io/) SDK into the `/bin/nwjs` directory -- On OSX, copy all files from `bin` into the `bin/nwjs.app/Contents/Resources/app.nw` folder +#### 3. Install Electron +- Download the [Electron](https://github.com/electron/electron/releases) release zip for your platform (`electron-vXX--x64.zip`) and unzip it into the `/bin/electron` directory +- On OSX, copy all files from `bin` into the `Electron.app/Contents/Resources/app` folder ## Running -- Windows: Run `hide.cmd` -- Linux: Run `nwjs/nw .` (don't miss the trailing space and dot) -- OSX: Open the NWJS application +- Windows: Run `hide.cmd` (or `cdb.cmd` for CastleDB) +- Linux: Run `electron/electron .` from the `bin` directory (add `--cdb` for CastleDB) +- OSX: Open the Electron application ## Configuration diff --git a/bin/app.html b/bin/app.html index 57c78af82..d72ab6be0 100644 --- a/bin/app.html +++ b/bin/app.html @@ -13,6 +13,14 @@ + @@ -35,16 +43,6 @@ } var thumbnail = getQueryVariable("thumbnail"); - if (!thumbnail) { - var win = nw.Window.get(); - win.on('close', function() { - chrome.app.window.getAll().forEach(win => { - win.close(true); - }); - nw.App.closeAllWindows(); - nw.App.quit(); - }); - } @@ -57,7 +55,15 @@ var nodeRequire = require; monaco = {}; + + + + diff --git a/bin/test-preload.js b/bin/test-preload.js new file mode 100644 index 000000000..953677e50 --- /dev/null +++ b/bin/test-preload.js @@ -0,0 +1,9 @@ +// Loaded by main.js when HIDE_TEST=1 : replace blocking popups by console logs +// so that automated test runs never get stuck on a modal dialog +window.alert = function( msg ) { + console.warn('[alert] ' + msg); +}; +window.confirm = function( msg ) { + console.warn('[confirm] ' + msg); + return true; +}; diff --git a/hide.hxml b/hide.hxml index 86c283b25..5765a8ad8 100644 --- a/hide.hxml +++ b/hide.hxml @@ -23,4 +23,16 @@ cdb.ConstLoader -debug -#-D shader_debug_dump \ No newline at end of file +#-D shader_debug_dump + +--next + +# Electron main process +-cp hide_js +-cp hide_js/libs +-lib hxnodejs +-main hide.ElectronMain +-js bin/main.js +-dce full +-D js-es=6 +-debug diff --git a/hide/Plugin.hx b/hide/Plugin.hx index d8138d38a..c3d0a9403 100644 --- a/hide/Plugin.hx +++ b/hide/Plugin.hx @@ -33,6 +33,7 @@ class Plugin { "cdb", "format", "domkit", + "electron", "HxOverrides", "Math", diff --git a/hide_js/hide/ElectronMain.hx b/hide_js/hide/ElectronMain.hx new file mode 100644 index 000000000..0e02a34cc --- /dev/null +++ b/hide_js/hide/ElectronMain.hx @@ -0,0 +1,473 @@ +package hide; + +import electron.main.Electron; + +typedef WindowState = { + var interceptClose : Bool; + var forceClose : Bool; + var opener : Null; + var menu : Menu; +} + +typedef CreateWindowOptions = { + ?show : Bool, + ?title : String, + ?width : Int, + ?height : Int, + ?opener : Int, +} + +/** + Electron main process for HIDE / CDB, compiled to bin/main.js by hide.hxml (after --next). + + Renderer windows run hide.js with full Node integration and talk to this process + through the "hide-sync" / "hide-async" IPC channels (see electron.Ipc). +**/ +class ElectronMain { + + static var MIMES = ["text" => "text/plain", "html" => "text/html", "rtf" => "text/rtf", "png" => "image/png", "jpeg" => "image/jpeg"]; + + static var isCDB : Bool; + static var appName : String; + static var appTitle : String; + // HIDE_TEST=1 (automated testing) : replace alert/confirm popups by console logs and + // never activate / focus / maximize windows so they stay in the background + static var testMode : Bool; + + static var mainWindow : BrowserWindow; + static var windowStates : Map = new Map(); + static var sharedRefs : Map = new Map(); + static var sharedRefId = 0; + // window id => subscriber webContents id => events + static var subscriptions : Map>> = new Map(); + // webContents id => accelerators + static var shortcuts : Map> = new Map(); + static var handlers : Map = new Map(); + + static function main() { + var argv : Array = js.Node.process.argv; + var env = js.Node.process.env; + isCDB = argv.indexOf("--cdb") >= 0 || env.get("HIDE_START_CDB") == "1"; + testMode = env.get("HIDE_TEST") == "1"; + appName = isCDB ? "CDB" : "hide"; + appTitle = isCDB ? "CDB" : "HIDE"; + + App.setName(appName); + App.setPath("userData", js.node.Path.join(App.getPath("appData"), appName)); + App.commandLine.appendSwitch("js-flags", "--expose-gc --no-efficiency-mode"); + App.commandLine.appendSwitch("disable-features", "AllowSoftwareGLFallbackDueToCrashes"); + + if( !App.requestSingleInstanceLock() ) { + App.quit(); + return; + } + + initHandlers(); + + IpcMain.on("hide-sync", function(e : Dynamic, cmd : String, args : haxe.extern.Rest) { + function error( err : Dynamic ) { + e.returnValue = { error : Std.string(err != null && err.stack != null ? err.stack : err) }; + } + try { + var r : Dynamic = runCommand(e.sender, cmd, args.toArray()); + if( Std.isOfType(r, js.lib.Promise) ) + (r : js.lib.Promise).then((v) -> e.returnValue = { value : v }, error); + else + e.returnValue = { value : r }; + } catch( err : Dynamic ) { + error(err); + } + }); + IpcMain.handle("hide-async", function(e : Dynamic, cmd : String, args : haxe.extern.Rest) { + return runCommand(e.sender, cmd, args.toArray()); + }); + + App.on("second-instance", function(e, argv : Array, cwd : String) { + if( mainWindow == null || mainWindow.isDestroyed() ) return; + if( mainWindow.isMinimized() ) mainWindow.restore(); + if( !testMode ) mainWindow.focus(); + send(mainWindow.webContents, "hide-app-open", filterArgv(argv, cwd)); + }); + App.on("open-file", function(e : Dynamic, file : String) { + e.preventDefault(); + if( mainWindow != null ) send(mainWindow.webContents, "hide-app-open", [file]); + }); + App.on("open-url", function(e : Dynamic, uri : String) { + e.preventDefault(); + if( mainWindow != null ) send(mainWindow.webContents, "hide-app-open", [uri]); + }); + App.on("before-quit", function() { + for( s in windowStates ) s.forceClose = true; + }); + App.on("window-all-closed", () -> App.quit()); + App.on("will-quit", () -> GlobalShortcut.unregisterAll()); + + App.whenReady().then(function(_) { + Menu.setApplicationMenu(null); + try Session.defaultSession.setSpellCheckerLanguages(["en-US", "fr-FR"]) catch( e : Dynamic ) {} + mainWindow = createWindow("app.html", {}); + mainWindow.on("closed", function() { + mainWindow = null; + App.quit(); + }); + }); + } + + // ---------------- helpers + + static function filterArgv( argv : Array, ?cwd : String ) { + var appPath = js.node.Path.resolve(App.getAppPath()); + var mainFile = js.node.Path.resolve(js.Node.__filename); + if( cwd == null ) cwd = js.Node.process.cwd(); + var out = []; + for( a in argv.slice(1) ) { + if( StringTools.startsWith(a, "--") ) continue; + var abs = js.node.Path.resolve(cwd, a); + if( abs == appPath || abs == mainFile ) continue; + out.push(sys.FileSystem.exists(abs) ? abs : a); + } + return out; + } + + static function send( wc : WebContents, channel : String, args : haxe.extern.Rest ) { + if( wc != null && !wc.isDestroyed() ) wc.send(channel, ...args); + } + + static function getWin( wc : WebContents, id : Null ) { + var win = id == null ? BrowserWindow.fromWebContents(wc) : BrowserWindow.fromId(id); + if( win == null || win.isDestroyed() ) throw "Window not found " + id; + return win; + } + + // ---------------- windows + + static function dispatchWinEvent( winId : Int, event : String ) { + var subs = subscriptions.get(winId); + if( subs == null ) return; + for( wcId => events in subs ) + if( events.indexOf(event) >= 0 ) + send(WebContents.fromId(wcId), "hide-win-event", winId, event); + } + + static function createWindow( file : String, opts : CreateWindowOptions ) { + var show = opts.show != false; + var win = new BrowserWindow({ + width : opts.width == null ? 800 : opts.width, + height : opts.height == null ? 600 : opts.height, + show : show && !testMode, + title : opts.title == null ? appTitle : opts.title, + icon : js.node.Path.join(js.Node.__dirname, "res", isCDB ? "cdb.png" : "hide.png"), + backgroundColor : "#222222", + webPreferences : { + nodeIntegration : true, + nodeIntegrationInWorker : true, + contextIsolation : false, + sandbox : false, + webviewTag : true, + webSecurity : false, + backgroundThrottling : false, + spellcheck : true, + preload : testMode ? js.node.Path.join(js.Node.__dirname, "test-preload.js") : null, + }, + }); + var id = win.id; + var wcId = win.webContents.id; + var state : WindowState = { interceptClose : false, forceClose : false, opener : opts.opener, menu : null }; + windowStates.set(id, state); + win.setMenu(null); + // title is driven by hide, not by the document + win.on("page-title-updated", (e : Dynamic) -> e.preventDefault()); + + win.on("close", function(e : Dynamic) { + if( state.forceClose || !state.interceptClose || win.webContents.isDestroyed() || win.webContents.isCrashed() ) { + dispatchWinEvent(id, "close"); + return; + } + e.preventDefault(); + send(win.webContents, "hide-win-event", id, "close"); + }); + win.on("closed", function() { + var subs = subscriptions.get(id); + if( subs != null ) + for( wcId in subs.keys() ) + send(WebContents.fromId(wcId), "hide-win-event", id, "closed"); + subscriptions.remove(id); + windowStates.remove(id); + }); + for( ev in ["maximize", "minimize", "move", "resize", "focus", "blur", "show", "hide"] ) + win.on(ev, () -> dispatchWinEvent(id, ev)); + // nwjs "restore" is sent both on unmaximize and when restoring from minimize + win.on("unmaximize", () -> dispatchWinEvent(id, "restore")); + win.on("restore", () -> dispatchWinEvent(id, "restore")); + win.on("enter-full-screen", () -> dispatchWinEvent(id, "enter-fullscreen")); + win.on("leave-full-screen", () -> dispatchWinEvent(id, "leave-fullscreen")); + + win.webContents.on("destroyed", () -> cleanupWebContents(wcId)); + // page reload : forget what the previous page had registered + win.webContents.on("did-start-navigation", function(e : Dynamic) { + if( !e.isMainFrame || e.isSameDocument ) return; + cleanupWebContents(wcId); + state.interceptClose = false; + }); + if( testMode && show ) + win.showInactive(); + + var parts = file.split("?"); + var fileUrl : String = js.Lib.require("url").pathToFileURL(js.node.Path.join(js.Node.__dirname, parts[0])).href; + win.loadURL(parts[1] == null ? fileUrl : fileUrl + "?" + parts[1]); + return win; + } + + static function cleanupWebContents( wcId : Int ) { + for( subs in subscriptions ) + subs.remove(wcId); + var keys = shortcuts.get(wcId); + if( keys != null ) { + for( k in keys ) + GlobalShortcut.unregister(k); + shortcuts.remove(wcId); + } + } + + static function menuFromTemplate( wc : WebContents, items : Array ) : Array { + return [for( it in items ) { + if( it.type == "separator" ) { type : "separator" } else { + var m : Dynamic = { + id : it.id, + label : it.label, + type : it.submenu != null ? "submenu" : (it.type == "checkbox" ? "checkbox" : "normal"), + enabled : it.enabled != false, + }; + if( it.type == "checkbox" ) m.checked = it.checked == true; + if( it.submenu != null ) + m.submenu = menuFromTemplate(wc, it.submenu); + else { + var itemId : String = it.id; + m.click = () -> send(wc, "hide-menu-click", itemId); + } + m; + } + }]; + } + + static function dialogFilters( exts : Array ) : Dynamic { + return exts == null ? null : [{ name : "Files", extensions : exts }, { name : "All Files", extensions : ["*"] }]; + } + + // ---------------- commands + + static function runCommand( wc : WebContents, cmd : String, args : Array ) : Dynamic { + var h = handlers.get(cmd); + if( h == null ) throw "Unknown command " + cmd; + return Reflect.callMethod(null, h, [(wc : Dynamic)].concat(args)); + } + + static function initHandlers() { + var h = handlers; + + h.set("hello", function(wc : WebContents) { + // called when a page (re)loads : reset everything it had registered + cleanupWebContents(wc.id); + var win = BrowserWindow.fromWebContents(wc); + if( win != null ) windowStates.get(win.id).interceptClose = false; + return { + argv : filterArgv(js.Node.process.argv), + dataPath : App.getPath("userData"), + name : appName, + windowId : win == null ? null : win.id, + version : js.Node.process.versions.get("electron"), + }; + }); + + // app + h.set("app.quit", (wc) -> App.quit()); + h.set("app.clearCache", (wc : WebContents) -> wc.session.clearCache()); + h.set("shortcut.register", function(wc : WebContents, accel : String) { + var ok = GlobalShortcut.register(accel, () -> send(wc, "hide-shortcut", accel)); + if( ok ) { + var keys = shortcuts.get(wc.id); + if( keys == null ) shortcuts.set(wc.id, keys = []); + keys.push(accel); + } + return ok; + }); + h.set("shortcut.unregister", function(wc : WebContents, accel : String) { + GlobalShortcut.unregister(accel); + var keys = shortcuts.get(wc.id); + if( keys != null ) keys.remove(accel); + }); + + // screens + h.set("screen.all", (wc) -> [for( d in Screen.getAllDisplays() ) { + id : d.id, + bounds : d.bounds, + work_area : d.workArea, + scaleFactor : d.scaleFactor, + isBuiltIn : d.internal, + rotation : d.rotation, + touchSupport : d.touchSupport, + }]); + + // clipboard (async in Electron, exposed as sync to the renderer) + h.set("clipboard.get", function(wc, type : String) : js.lib.Promise { + if( type == null || type == "text" ) + return Clipboard.readText(); + var mime = MIMES.exists(type) ? MIMES.get(type) : type; + return Clipboard.read().then(function(items) { + for( it in items ) + if( it.types.indexOf(mime) >= 0 ) + return it.getType(mime).then((b : Dynamic) -> (b.text() : js.lib.Promise)); + return js.lib.Promise.resolve(""); + }); + }); + h.set("clipboard.set", function(wc, datas : Array<{ data : String, type : String }>) { + var item : haxe.DynamicAccess = {}; + for( d in datas ) { + var type = d.type == null ? "text" : d.type; + item.set(MIMES.exists(type) ? MIMES.get(type) : type, d.data); + } + return Clipboard.write([new ClipboardItem(cast item)]); + }); + h.set("clipboard.clear", (wc) -> Clipboard.clear()); + + // windows + h.set("win.open", function(wc : WebContents, file : String, opts : CreateWindowOptions) { + if( opts == null ) opts = {}; + opts.opener = wc.id; + return createWindow(file, opts).id; + }); + h.set("win.list", (wc) -> [for( w in BrowserWindow.getAllWindows() ) { id : w.id, title : w.getTitle() }]); + h.set("win.subscribe", function(wc : WebContents, id : Null, events : Array) { + var win = getWin(wc, id); + var subs = subscriptions.get(win.id); + if( subs == null ) subscriptions.set(win.id, subs = new Map()); + var evs = subs.get(wc.id); + if( evs == null ) subs.set(wc.id, evs = []); + for( e in events ) { + if( evs.indexOf(e) < 0 ) evs.push(e); + if( e == "close" && win.webContents == wc ) windowStates.get(win.id).interceptClose = true; + } + }); + h.set("win.getBounds", (wc, id) -> getWin(wc, id).getBounds()); + h.set("win.getTitle", (wc, id) -> getWin(wc, id).getTitle()); + h.set("win.setTitle", (wc, id, title : String) -> getWin(wc, id).setTitle(title)); + h.set("win.isMaximized", (wc, id) -> getWin(wc, id).isMaximized()); + h.set("win.moveTo", (wc, id, x : Float, y : Float) -> getWin(wc, id).setPosition(Math.round(x), Math.round(y))); + h.set("win.resizeTo", (wc, id, w : Float, h : Float) -> getWin(wc, id).setSize(Math.round(w), Math.round(h))); + h.set("win.resizeBy", function(wc, id, dw : Float, dh : Float) { + var win = getWin(wc, id); + var size = win.getSize(); + win.setSize(Math.round(size[0] + dw), Math.round(size[1] + dh)); + }); + h.set("win.maximize", (wc, id) -> if( !testMode ) getWin(wc, id).maximize()); + h.set("win.minimize", (wc, id) -> getWin(wc, id).minimize()); + h.set("win.restore", function(wc, id) { + var win = getWin(wc, id); + if( win.isMaximized() ) win.unmaximize() else win.restore(); + }); + h.set("win.setFullScreen", (wc, id, b : Bool) -> if( !testMode ) getWin(wc, id).setFullScreen(b)); + h.set("win.show", function(wc, id, b : Bool) { + var win = getWin(wc, id); + if( b == false ) win.hide() else if( testMode ) win.showInactive() else win.show(); + }); + h.set("win.focus", (wc, id) -> if( !testMode ) getWin(wc, id).focus()); + h.set("win.blur", (wc, id) -> getWin(wc, id).blur()); + h.set("win.close", function(wc : WebContents, id : Null, force : Bool) { + var win = id == null ? BrowserWindow.fromWebContents(wc) : BrowserWindow.fromId(id); + if( win == null || win.isDestroyed() ) return; + if( force ) windowStates.get(win.id).forceClose = true; + win.close(); + }); + h.set("win.openDevTools", (wc, id) -> getWin(wc, id).webContents.openDevTools()); + h.set("win.setMenu", function(wc : WebContents, items : Array) { + var win = getWin(wc, null); + var template = items == null ? null : menuFromTemplate(wc, items); + // without an Edit menu, copy/paste shortcuts are not handled on OSX + if( template != null && Sys.systemName() == "Mac" ) + template = ([{ role : "appMenu" }, { role : "editMenu" }] : Array).concat(template); + var menu = template == null ? null : Menu.buildFromTemplate(template); + if( Sys.systemName() == "Mac" ) { + if( win == mainWindow ) Menu.setApplicationMenu(menu); + } else + win.setMenu(menu); + windowStates.get(win.id).menu = menu; + }); + h.set("win.updateMenuItem", function(wc, itemId : String, props : haxe.DynamicAccess) { + var menu = windowStates.get(getWin(wc, null).id).menu; + var item = menu == null ? null : menu.getMenuItemById(itemId); + if( item == null ) return; + for( k => v in props ) Reflect.setField(item, k, v); + }); + h.set("win.callParent", function(wc, name : String, param : Dynamic) { + var opener = windowStates.get(getWin(wc, null).id).opener; + if( opener != null ) send(WebContents.fromId(opener), "hide-parent-call", name, param); + }); + + // values shared between windows (must be serializable) + h.set("shared.set", function(wc, value : Dynamic) { + var id = sharedRefId++; + sharedRefs.set(id, value); + return id; + }); + h.set("shared.get", (wc, id : Int) -> sharedRefs.get(id)); + + // dialogs + h.set("dialog.open", function(wc, o : Dynamic) { + var props = [o.directory ? "openDirectory" : "openFile", "createDirectory"]; + if( o.multiple ) props.push("multiSelections"); + return Dialog.showOpenDialog(getWin(wc, null), { defaultPath : o.defaultPath, properties : props, filters : dialogFilters(o.exts) }) + .then((r) -> r.canceled || r.filePaths.length == 0 ? null : r.filePaths); + }); + h.set("dialog.save", function(wc, o : Dynamic) { + return Dialog.showSaveDialog(getWin(wc, null), { defaultPath : o.defaultPath, filters : dialogFilters(o.exts) }) + .then((r) -> r.canceled || r.filePath == null || r.filePath == "" ? null : [r.filePath]); + }); + // window.prompt() is not supported by Electron + h.set("dialog.prompt", (wc, text : String, value : String) -> new js.lib.Promise(function(resolve, _) { + if( testMode ) { + js.Browser.console.log("[prompt] " + text); + resolve(value == null ? "" : value); + return; + } + var parent = BrowserWindow.fromWebContents(wc); + var w = new BrowserWindow({ + parent : parent, + modal : true, + width : 450, + height : 150, + useContentSize : true, + resizable : false, + minimizable : false, + maximizable : false, + show : false, + title : parent == null ? appTitle : parent.getTitle(), + backgroundColor : "#222222", + webPreferences : { nodeIntegration : true, contextIsolation : false, sandbox : false }, + }); + w.setMenu(null); + var result : String = null; + var promptWc = w.webContents; + function onAnswer( e : Dynamic, v : String ) { + if( e.sender != promptWc ) return; + result = v; + w.close(); + } + IpcMain.on("hide-prompt-answer", onAnswer); + w.on("closed", function() { + IpcMain.off("hide-prompt-answer", onAnswer); + resolve(result); + }); + w.once("ready-to-show", () -> w.show()); + w.loadFile(js.node.Path.join(js.Node.__dirname, "prompt.html"), { query : { text : Std.string(text), value : value == null ? "" : Std.string(value) } }); + })); + + // devtools embedded into a + h.set("devtools.attach", function(wc, targetId : Int, devtoolsId : Int) { + var target = WebContents.fromId(targetId); + target.setDevToolsWebContents(WebContents.fromId(devtoolsId)); + target.openDevTools(); + }); + h.set("devtools.open", (wc, wcId : Int) -> WebContents.fromId(wcId).openDevTools({ mode : "detach" })); + } + +} diff --git a/hide_js/hide/Ide.hx b/hide_js/hide/Ide.hx index f9bf9d508..da9077f4f 100644 --- a/hide_js/hide/Ide.hx +++ b/hide_js/hide/Ide.hx @@ -29,8 +29,7 @@ class Ide extends hide.tools.IdeData { public var gamePad(default,null) : hxd.Pad; public var localStorage(get,never) : js.html.Storage; - var window : nw.Window; - var saveMenu : nw.Menu; + var window : electron.Window; var layout : golden.Layout; var currentLayout : { name : String, state : HideProjectConfig.LayoutState }; @@ -42,7 +41,7 @@ class Ide extends hide.tools.IdeData { var lastClosedTabStates : Array = []; var renderers : Array; - var subView : { component : String, state : Dynamic, events : {} }; + var subView : { component : String, state : Dynamic }; var scripts : MapVoid>> = new Map(); var hasReloaded = false; public var thumbnailMode : Bool = false; @@ -54,21 +53,19 @@ class Ide extends hide.tools.IdeData { var goldenContainer : hide.Element; var statusIcons : hide.Element; - var breakShortcut : Dynamic; - public var show3DIcons = true; public var show3DIconsCategory : Map = new Map(); static var firstInit = true; - var customMenus : Array = []; - - var filePickerElement : hide.Element; + var customMenus : Array = []; function new() { super(); initPad(); - isCDB = Sys.getEnv("HIDE_START_CDB") == "1" || nw.App.manifest.name == "CDB"; + hxd.System.getClipboardText = () -> electron.Clipboard.get(Text); + hxd.System.setClipboardText = (text) -> { electron.Clipboard.set(text, Text); return true; }; + isCDB = Sys.getEnv("HIDE_START_CDB") == "1" || electron.App.name == "CDB"; isDebugger = Sys.getEnv("HIDE_DEBUG") == "1"; var thumb = StringTools.contains(js.Browser.window.location.href, "thumbnail"); @@ -91,7 +88,7 @@ class Ide extends hide.tools.IdeData { } override function getAppDataPath() { - return nw.App.dataPath; + return electron.App.dataPath; } function initPad() { @@ -105,12 +102,10 @@ class Ide extends hide.tools.IdeData { function startup() { inst = this; - window = nw.Window.get(); + window = electron.Window.get(); var cwd = Sys.getCwd(); initConfig(cwd); var current = ideConfig.currentProject; - if( StringTools.endsWith(cwd,"package.nw") && sys.FileSystem.exists(cwd.substr(0,-10)+"res") ) - cwd = cwd.substr(0,-11); if( current == "" ) cwd; var args = js.Browser.document.URL.split("?")[1]; @@ -123,15 +118,14 @@ class Ide extends hide.tools.IdeData { } var sub = vars.get("subView"); if( sub != null ) { - var obj = untyped global.sharedRefs.get(Std.parseInt(vars.get("sid"))); - subView = { component : sub, state : obj.state, events : obj.events }; + var state = electron.App.getShared(Std.parseInt(vars.get("sid"))); + subView = { component : sub, state : state }; } } - nw.Screen.Init(); var xMax = 1; var yMax = 1; - for( s in nw.Screen.screens ) { + for( s in electron.Screen.screens ) { if( s.work_area.x + s.work_area.width > xMax ) xMax = s.work_area.x + s.work_area.width; if( s.work_area.y + s.work_area.height > yMax ) @@ -141,7 +135,7 @@ class Ide extends hide.tools.IdeData { var wp = ideConfig.windowPos; if( wp != null ) { if( wp.w > 400 && wp.h > 300 ) - window.resizeBy(wp.w - Std.int(window.window.outerWidth), wp.h - Std.int(window.window.outerHeight)); + window.resizeTo(wp.w, wp.h); if( wp.x >= 0 && wp.y >= 0 && wp.x < xMax && wp.y < yMax) window.moveTo(wp.x, wp.y); if( wp.max ) { @@ -171,11 +165,11 @@ class Ide extends hide.tools.IdeData { return; } - window.window.document.addEventListener("mousedown", function(e) { + js.Browser.document.addEventListener("mousedown", function(e) { mouseX = e.x; mouseY = e.y; }); - window.window.document.addEventListener("mousemove", function(e) { + js.Browser.document.addEventListener("mousemove", function(e) { mouseX = e.x; mouseY = e.y; }); @@ -205,45 +199,19 @@ class Ide extends hide.tools.IdeData { window.on("blur", function() { if( h3d.Engine.getCurrent() != null && !hasReloaded ) hxd.Key.initialize(); }); // handle commandline parameters - function onOpen(cmd: String) { - var dyncmd: Dynamic = cmd; - if (Type.typeof(dyncmd).match(TClass(Array))) { - var arr: Array = dyncmd; - cmd = arr[arr.length - 1]; - } - var protocols = ["hide://", "cdb://"]; - for (p in protocols) { - var uriIndex = cmd.indexOf(p); - if (uriIndex >= 0) { - var uri = cmd.substr(uriIndex); - if (!StringTools.contains(uri, " ")) { - hide.view.RemoteConsoleView.onOpenUri(uri); - } + electron.App.onOpen(function(args) { + if( hasReloaded ) return; + for( arg in args ) { + if( StringTools.startsWith(arg, "hide://") || StringTools.startsWith(arg, "cdb://") ) { + hide.view.RemoteConsoleView.onOpenUri(arg); + continue; } + if( sys.FileSystem.exists(arg) ) openFile(arg); } - haxe.Timer.delay(() -> trace("on open", cmd), 500); - nw.App.on("open", onOpen); - if( hasReloaded ) return; - ~/"([^"]+)"/g.map(cmd, function(r) { - var file = r.matched(1); - if( sys.FileSystem.exists(file) ) openFile(file); - return ""; - }); - } - nw.App.on("open", onOpen); + }); - var body = window.window.document.body; + var body = js.Browser.document.body; window.on("focus", function() { - // handle cancel on type=file - - if (filePickerElement != null && filePickerElement.data("allownull") != null) { - haxe.Timer.delay(() -> { - if (filePickerElement != null) { - filePickerElement.change(); - } - }, 100); - } - if(fileExists(databaseFile) && getFileText(databaseFile) != lastDBContent) { if(js.Browser.window.confirm(databaseFile + " has changed outside of Hide. Do you want to reload?")) { loadDatabase(true); @@ -277,7 +245,7 @@ class Ide extends hide.tools.IdeData { body.ondrop = function(e:js.html.DragEvent) { if(!dragFunc(true, e)) { for( f in e.dataTransfer.files ) - openFile(Reflect.field(f,"path")); + openFile(electron.App.getPathForFile(f)); e.preventDefault(); } return false; @@ -302,8 +270,6 @@ class Ide extends hide.tools.IdeData { hrt.impl.EditorTools.setupIconCategories(); - untyped chrome.settingsPrivate.setPref('spellcheck.dictionaries', ["en-US","fr-FR"], "null", ()->{}); - refreshFont(); } @@ -360,8 +326,8 @@ class Ide extends hide.tools.IdeData { if( !maximized ) { ideConfig.windowPos.x = window.x; ideConfig.windowPos.y = window.y; - ideConfig.windowPos.w = Std.int(window.window.outerWidth); - ideConfig.windowPos.h = Std.int(window.window.outerHeight); + ideConfig.windowPos.w = window.width; + ideConfig.windowPos.h = window.height; } if( subView == null ) config.global.save(); @@ -558,17 +524,7 @@ class Ide extends hide.tools.IdeData { // register a global shortcut that break in the debugger // on Alt+F1. Usefull to debug UI elements that are temporary // Note : the debugger window must be open for this to work - { - var option = { - key: "Alt+F1", - active: () -> { - js.Lib.debug(); - } - }; - - breakShortcut = js.Syntax.construct("nw.Shortcut", option); - untyped nw.App.registerGlobalHotKey(breakShortcut); - } + electron.App.registerGlobalShortcut("Alt+F1", () -> js.Lib.debug()); var waitCount = 0; function waitInit() { @@ -608,7 +564,7 @@ class Ide extends hide.tools.IdeData { } if( firstInit ) { firstInit = false; - for( file in nw.App.argv ) { + for( file in electron.App.argv ) { if( !sys.FileSystem.exists(file) ) continue; openFile(file); } @@ -632,14 +588,11 @@ class Ide extends hide.tools.IdeData { if (b) { fullscreen = true; window.maximize(); - saveMenu = window.menu; - window.menu = null; window.enterFullscreen(); } else { - window.menu = saveMenu; window.leaveFullscreen(); - // NWJS bug: changing fullscreen triggers spurious "restore" events + // changing fullscreen triggers spurious "restore" events haxe.Timer.delay(function() { fullscreen = false; if(maximized) @@ -671,16 +624,16 @@ class Ide extends hide.tools.IdeData { } - public function setClipboard( data : String, type: nw.Clipboard.ClipboardType = Text ) { - nw.Clipboard.get().set([{data: data, type: type }]); + public function setClipboard( data : String, type: electron.Clipboard.ClipboardType = Text ) { + electron.Clipboard.set(data, type); } - public function setClipboardMultiple( datas: Array ) { - nw.Clipboard.get().set(datas); + public function setClipboardMultiple( datas: Array ) { + electron.Clipboard.setMultiple(datas); } - public function getClipboard(type: nw.Clipboard.ClipboardType = Text) { - return nw.Clipboard.get().get(type); + public function getClipboard(type: electron.Clipboard.ClipboardType = Text) { + return electron.Clipboard.get(type); } @@ -939,7 +892,7 @@ class Ide extends hide.tools.IdeData { public function reload() { hasReloaded = true; fileWatcher.dispose(); - untyped nw.App.unregisterGlobalHotKey(breakShortcut); + electron.App.unregisterGlobalShortcut("Alt+F1"); hide.tools.FileManager.onBeforeReload(); hide.view.RemoteConsoleView.onBeforeReload(); js.Browser.location.reload(); @@ -986,62 +939,34 @@ class Ide extends hide.tools.IdeData { } = null) { options = options ?? {}; - function callback() { - if (filePickerElement != null) - filePickerElement.remove(); - - var args : Array = []; - - if (options.allowNull == true) - args.push("data-allownull='true'"); - if (options.saveAs != null) - args.push('nwsaveas="${options.saveAs}"'); - if (options.exts != null) - args.push('accept="${[for( e in options.exts ) "."+e].join(",")}"'); - if (options.onlyDirectory == true) - args.push("nwdirectory"); - if (options.multiple == true) - args.push('multiple="multiple"'); - if (options.workingDir != null && options.workingDir != "#MISSING") { - var pathArray = getPath(options.workingDir).split("/"); - var c = isWindows ? "\\" : "/"; - var workingDirPath = pathArray.join(c); - args.push('nwworkingdir="$workingDirPath"'); + var defaultPath = null; + if (options.workingDir != null && options.workingDir != "#MISSING") + defaultPath = getPath(options.workingDir); + if (options.saveAs != null) + defaultPath = defaultPath == null ? options.saveAs : defaultPath + "/" + options.saveAs; + if (defaultPath != null && isWindows) + defaultPath = defaultPath.split("/").join("\\"); + + function onResult(files: Array) { + if (files == null) { + if (options.allowNull) onSelect(null); + return; } - - var argsString = args.join(" "); - - var buildString = ''; - - filePickerElement = new Element(buildString).appendTo(window.window.document.body); - - filePickerElement.change(function(_) { - var file = filePickerElement.val(); - filePickerElement.remove(); - filePickerElement = null; - if( file == "" && !options.allowNull ) return; - if (file == "") { - onSelect(null); - } else { - var files = file.split(";"); - if (options.isAbsolute != true) { - for (i => file in files) { - files[i] = makeRelative(file); - } - } - onSelect(files); - } - }); - - filePickerElement.click(); + if (options.isAbsolute != true) + files = [for (f in files) makeRelative(f)]; + onSelect(files); } - if (options.allowNull) { - haxe.Timer.delay(callback, 100); - } - else { - callback(); - } + var dialogOptions : electron.Dialog.FileDialogOptions = { + defaultPath: defaultPath, + exts: options.exts, + directory: options.onlyDirectory == true, + multiple: options.multiple == true, + }; + if (options.saveAs != null) + electron.Dialog.saveFile(dialogOptions, (file) -> onResult(file == null ? null : [file])); + else + electron.Dialog.openFiles(dialogOptions, onResult); } /** @@ -1442,7 +1367,7 @@ class Ide extends hide.tools.IdeData { js.node.ChildProcess.exec(c, function(e:js.node.ChildProcess.ChildProcessExecError,_,_) callb(e == null ? null : e.message)); } - public function addCustomMenu(item: nw.MenuItem) { + public function addCustomMenu(item: electron.MenuItem) { customMenus.push(item); } @@ -1483,13 +1408,13 @@ class Ide extends hide.tools.IdeData { initMenu(); }); menu.find(".project .exit").click(function(_) { - Sys.exit(0); + electron.App.quit(); }); menu.find(".project .clear-local").click(function(_) { js.Browser.window.localStorage.clear(); - nw.App.clearCache(); + electron.App.clearCache(); try sys.FileSystem.deleteFile(Ide.inst.appPath + "/props.json") catch( e : Dynamic ) {}; - untyped chrome.runtime.reload(); + js.Browser.location.reload(); }); menu.find(".build-files").click(function(_) { hrt.impl.BuildTools.buildAllFiles(resourceDir + "/", function(percent, currentFile) { @@ -1842,20 +1767,26 @@ class Ide extends hide.tools.IdeData { } public function openSubView( component : Class>, state : T, events : {} ) { - var sharedRefs : Map = untyped global.sharedRefs; - if( sharedRefs == null ) { - sharedRefs = new Map(); - untyped global.sharedRefs = sharedRefs; - } - var id = 0; - while( sharedRefs.exists(id) ) id++; - sharedRefs.set(id,{ state : state, events : events }); + // state is sent to the sub window, events are called back through IPC + if( subViewEvents == null ) { + subViewEvents = []; + electron.Window.onOpenerCall(function(name, param) { + for( events in subViewEvents ) { + var f = Reflect.field(events, name); + if( f != null ) Reflect.callMethod(events, f, [param]); + } + }); + } + if( subViewEvents.indexOf(events) < 0 ) subViewEvents.push(events); + var id = electron.App.setShared(state); var compName = Type.getClassName(component); - nw.Window.open("app.html?subView="+compName+"&sid="+id,{ id : compName }); + electron.Window.open("app.html?subView="+compName+"&sid="+id); } + var subViewEvents : Array<{}>; + public function callParentView( name : String, param : Dynamic ) { - if( subView != null ) Reflect.callMethod(subView.events,Reflect.field(subView.events,name),[param]); + if( subView != null ) electron.Window.callOpener(name, param); } public function closeInspector() { @@ -2004,7 +1935,7 @@ class Ide extends hide.tools.IdeData { } public function ask( text : String, ?defaultValue = "" ) { - return js.Browser.window.prompt(text, defaultValue); + return electron.Dialog.prompt(text, defaultValue); } var delayedSvnStatusCallbacks : Array<(files : Array) -> Void> = null; diff --git a/hide_js/hide/comp/ColorPicker.hx b/hide_js/hide/comp/ColorPicker.hx index 15a161023..2dcb2d030 100644 --- a/hide_js/hide/comp/ColorPicker.hx +++ b/hide_js/hide/comp/ColorPicker.hx @@ -4,7 +4,7 @@ import hrt.impl.ColorSpace; import h2d.Slider; import js.html.InputElement; import js.jquery.Event; -import nw.Clipboard; +import electron.Clipboard; import js.html.PointerEvent; import h3d.Vector4; import js.Browser; @@ -243,7 +243,7 @@ class ColorPicker extends Popup { copyButton = new Element("
"); copyButton.append(new Element("
")); copyButton.on("click", function(e) { - Clipboard.get().set(colorCode.val()); + Clipboard.set(colorCode.val()); }); group.append(copyButton); @@ -251,7 +251,7 @@ class ColorPicker extends Popup { pasteButton = new Element("
"); pasteButton.append(new Element("
")); pasteButton.on("click", function(e) { - var value = Clipboard.get().get(); + var value = Clipboard.get(); colorCode.val(value).change(); }); group.append(pasteButton); diff --git a/hide_js/hide/comp/cdb/Editor.hx b/hide_js/hide/comp/cdb/Editor.hx index 7350482d5..78fe7b68c 100644 --- a/hide_js/hide/comp/cdb/Editor.hx +++ b/hide_js/hide/comp/cdb/Editor.hx @@ -676,8 +676,8 @@ class Editor extends Component { } ide.setClipboardMultiple([ - { type: nw.Clipboard.ClipboardType.Text, data: plainText }, - { type: nw.Clipboard.ClipboardType.Rtf, data: rtfText } + { type: electron.Clipboard.ClipboardType.Text, data: plainText }, + { type: electron.Clipboard.ClipboardType.Rtf, data: rtfText } ]); } @@ -685,7 +685,7 @@ class Editor extends Component { if (this.cursor.table == null) return; - var cdbDataText = ide.getClipboard(nw.Clipboard.ClipboardType.Rtf); + var cdbDataText = ide.getClipboard(electron.Clipboard.ClipboardType.Rtf); if (cdbDataText.indexOf(CLIPBOARD_PREFIX) >= 0) cdbDataText = StringTools.replace(cdbDataText, CLIPBOARD_PREFIX, ""); else @@ -789,7 +789,7 @@ class Editor extends Component { return null; } - var plainText = ide.getClipboard(nw.Clipboard.ClipboardType.Text); + var plainText = ide.getClipboard(electron.Clipboard.ClipboardType.Text); beginChanges(); for (c in targetCells) { diff --git a/hide_js/hide/tools/DragAndDrop.hx b/hide_js/hide/tools/DragAndDrop.hx index 7ab8e9931..2909c19a1 100644 --- a/hide_js/hide/tools/DragAndDrop.hx +++ b/hide_js/hide/tools/DragAndDrop.hx @@ -139,7 +139,7 @@ class DragAndDrop { tmpDrag.data = []; var list = []; for (file in e.dataTransfer.files) { - var fe = FileManager.inst.getFileEntry(untyped file.path); + var fe = FileManager.inst.getFileEntry(electron.App.getPathForFile(file)); if (fe != null) { list.push(fe); } diff --git a/hide_js/hide/tools/FileManager.hx b/hide_js/hide/tools/FileManager.hx index ec2212c45..5ce282ec8 100644 --- a/hide_js/hide/tools/FileManager.hx +++ b/hide_js/hide/tools/FileManager.hx @@ -398,11 +398,11 @@ class FileManager { windowManager = null; - untyped nw.Window.getAll((win:nw.Window) -> { + for (win in electron.Window.getAll()) { if (win.title == "HideThumbnailGenerator") { win.close(true); } - }); + } } function init() { @@ -849,7 +849,7 @@ enum RenderWindowState { @:access(hide.tools.FileManager) class RenderWindowManager { var state : RenderWindowState = Pending; - var generatorWindow : nw.Window; + var generatorWindow : electron.Window; function new() { state = Pending; @@ -857,15 +857,12 @@ class RenderWindowManager { // the generator socket is properly initialised untyped js.Browser.window.requestIdleCallback(() -> { state = Ready; - nw.Window.open('app.html?thumbnail=true', cast { - new_instance: true, - show: false, - title: "HideThumbnailGenerator" - }, (win: nw.Window) -> { - generatorWindow = win; - win.on("close", () -> { - hide.Tools.FileManager.cleanupGenerator(); - }); + generatorWindow = electron.Window.open('app.html?thumbnail=true', { + show: false, + title: "HideThumbnailGenerator" + }); + generatorWindow.on("closed", () -> { + FileManager.inst.cleanupGenerator(); }); }, {timeout: 1000}); } diff --git a/hide_js/hide/tools/ThumbnailGenerator.hx b/hide_js/hide/tools/ThumbnailGenerator.hx index 3111ef605..5a8769a2e 100644 --- a/hide_js/hide/tools/ThumbnailGenerator.hx +++ b/hide_js/hide/tools/ThumbnailGenerator.hx @@ -87,11 +87,11 @@ class ThumbnailGenerator { function new() { if (Ide.inst.ideConfig.filebrowserDebugShowWindow) { - nw.Window.get().show(true); + electron.Window.get().show(true); } else { - untyped nw.Window.get().hide(); + electron.Window.get().hide(); } - nw.Window.get().resizeTo(128,128); + electron.Window.get().resizeTo(128,128); bufferedData = haxe.io.Bytes.alloc(maxBufferSize); @@ -100,7 +100,7 @@ class ThumbnailGenerator { // Destroy the generator if any error occurs socket.onError = (msg) -> { - nw.Window.get().close(true); + electron.Window.get().close(true); } var handler = new MessageHandler(socket, handleCommand); diff --git a/hide_js/hide/ui/Menu.hx b/hide_js/hide/ui/Menu.hx index 516c838b8..ec39326b9 100644 --- a/hide_js/hide/ui/Menu.hx +++ b/hide_js/hide/ui/Menu.hx @@ -2,14 +2,14 @@ package hide.ui; class Menu { - public var root : nw.Menu; + public var root : electron.Menu; public function new( menu : Element) { - root = new nw.Menu({type: Menubar}); + root = new electron.Menu(); buildMenuRec(root,"",menu); } - function buildMenuRec( menu : nw.Menu, path : String, e : Element ) { + function buildMenuRec( menu : electron.Menu, path : String, e : Element ) { var cl = e.attr("class"); if( cl != null ) { if( path == "" ) path = cl else path = path + "." + cl; @@ -19,18 +19,18 @@ class Menu { case "MENU": var submenu = null; if( elt.firstElementChild != null ) { - submenu = new nw.Menu({type:ContextMenu}); + submenu = new electron.Menu(); for( e in e.children().elements() ) buildMenuRec(submenu, path, e); } - var type : nw.MenuItem.MenuItemType = switch( e.attr("type") ) { + var type : electron.MenuItem.MenuItemType = switch( e.attr("type") ) { case "checkbox": Checkbox; default: Normal; } var label = e.attr("label"); if( label == null ) label = "???"; var checked = e.prop("checked") || e.attr("checked") == "checked"; - var m = new nw.MenuItem(submenu == null ? { label : label, type : type } : { label : label, type : type, submenu : submenu }); + var m = new electron.MenuItem(submenu == null ? { label : label, type : type } : { label : label, type : type, submenu : submenu }); if( type == Checkbox ) m.checked = checked; if( e.attr("disabled") == "disabled" ) @@ -45,7 +45,7 @@ class Menu { }; menu.append(m); case "SEPARATOR": - menu.append(new nw.MenuItem({ label : null, type : Separator })); + menu.append(new electron.MenuItem({ label : null, type : Separator })); default: for( e in e.children().elements() ) buildMenuRec(menu, path, e); diff --git a/hide_js/hide/ui/View.hx b/hide_js/hide/ui/View.hx index 1d17b39d5..43666b52e 100644 --- a/hide_js/hide/ui/View.hx +++ b/hide_js/hide/ui/View.hx @@ -113,18 +113,18 @@ class View extends hide.comp.Component { #if !hl public function setClipboard( v : Dynamic, ?type : String, ?opts : {} ) { - nw.Clipboard.get().set(ide.toJSON({ type : type == null ? viewClass : type, value : v, opts : opts })); + electron.Clipboard.set(ide.toJSON({ type : type == null ? viewClass : type, value : v, opts : opts })); } public function hasClipboard( ?type : String ) { if( type == null ) type = viewClass; - var v : Dynamic = try haxe.Json.parse(nw.Clipboard.get().get()) catch( e : Dynamic ) null; + var v : Dynamic = try haxe.Json.parse(electron.Clipboard.get()) catch( e : Dynamic ) null; return v != null && v.type == type; } public function getClipboard( ?type : String, ?opts : { ref : Dynamic } ) : Dynamic { if( type == null ) type = viewClass; - var v : Dynamic = try haxe.Json.parse(nw.Clipboard.get().get()) catch( e : Dynamic ) null; + var v : Dynamic = try haxe.Json.parse(electron.Clipboard.get()) catch( e : Dynamic ) null; if( v != null && opts != null ) opts.ref = v.opts; return v == null || v.type != type ? null : v.value; } diff --git a/hide_js/hide/view/DevTools.hx b/hide_js/hide/view/DevTools.hx index a858dbcab..e8865b7ed 100644 --- a/hide_js/hide/view/DevTools.hx +++ b/hide_js/hide/view/DevTools.hx @@ -22,15 +22,15 @@ class DevTools extends hide.ui.View<{ profileFilePath : String }> { if( openCalled || !blankLoaded || !devtoolsLoaded ) return; openCalled = true; - showDevTools(blank, true, devtools); + attachDevTools(blank, devtools); // wait for devTools ready haxe.Timer.delay(() -> openProfile(), 500); } - blank.addEventListener("contentload", function() { + blank.addEventListener("dom-ready", function() { blankLoaded = true; tryOpen(); }); - devtools.addEventListener("contentload", function() { + devtools.addEventListener("dom-ready", function() { devtoolsLoaded = true; tryOpen(); }); @@ -42,7 +42,7 @@ class DevTools extends hide.ui.View<{ profileFilePath : String }> { var menu = super.buildTabMenu(); menu.push({isSeparator: true}); menu.push({label: "Debug", click: () -> { - showDevTools(devtools, true); + electron.Ipc.call("devtools.open", getWebContentsId(devtools)); }}); return menu; } @@ -71,12 +71,16 @@ setTimeout(function() { } - inline function showDevTools( webview : Element.HTMLElement, show : Bool, ?container : Element.HTMLElement ) { - js.Syntax.code("{0}.showDevTools({1}, {2});", webview, show, container); + inline function getWebContentsId( webview : Element.HTMLElement ) : Int { + return (cast webview).getWebContentsId(); + } + + function attachDevTools( webview : Element.HTMLElement, container : Element.HTMLElement ) { + electron.Ipc.call("devtools.attach", getWebContentsId(webview), getWebContentsId(container)); } inline function unsafeExecuteScript( script : String ) { - js.Syntax.code("{0}.executeScript({ code: {1}, mainWorld: false });", devtools, script); + (cast devtools).executeJavaScript(script); } override function getTitle() { diff --git a/hide_js/hide/view/FileView.hx b/hide_js/hide/view/FileView.hx index 72d9006fe..f0afffd55 100644 --- a/hide_js/hide/view/FileView.hx +++ b/hide_js/hide/view/FileView.hx @@ -114,7 +114,7 @@ class FileView extends hide.ui.View<{ path : String }> { keys.register("redo", function() undo.redo()); keys.register("save", function() save()); keys.register("view.refresh", function() rebuild()); - keys.register("view.refreshApp", function() untyped chrome.runtime.reload()); + keys.register("view.refreshApp", function() js.Browser.location.reload()); } public function canSave() { diff --git a/hide_js/hide/view/Gym.hx b/hide_js/hide/view/Gym.hx index a2ba5a21f..27c62f90c 100644 --- a/hide_js/hide/view/Gym.hx +++ b/hide_js/hide/view/Gym.hx @@ -282,11 +282,10 @@ class Gym extends hide.ui.View<{}> { } }); - nw.Window.open('app.html?thumbnail=true', {new_instance: true}, (win: nw.Window) -> { - win.on("close", () -> { - sock.close(); - sock = null; - }); + var win = electron.Window.open('app.html?thumbnail=true'); + win.on("closed", () -> { + sock.close(); + sock = null; }); } diff --git a/hide_js/hide/view/Model.hx b/hide_js/hide/view/Model.hx index 53101c5dd..aba409831 100644 --- a/hide_js/hide/view/Model.hx +++ b/hide_js/hide/view/Model.hx @@ -433,7 +433,7 @@ class Model extends FileView { sceneEditor.view.keys.register("view.refresh", function() rebuild()); - sceneEditor.view.keys.register("view.refreshApp", function() untyped chrome.runtime.reload()); + sceneEditor.view.keys.register("view.refreshApp", function() js.Browser.location.reload()); sceneEditor.view.keys.register("sceneeditor.radialViewModes", {name: "Radial view modes", category: "Scene"}, function() { var renderer = Std.downcast(@:privateAccess scene.s3d.renderer, h3d.scene.pbr.Renderer); diff --git a/hide_js/libs/electron/App.hx b/hide_js/libs/electron/App.hx new file mode 100644 index 000000000..f1f7c6cad --- /dev/null +++ b/hide_js/libs/electron/App.hx @@ -0,0 +1,74 @@ +package electron; + +class App { + + /** command line arguments, without the executable / app path and switches **/ + public static var argv(get, never) : Array; + /** directory where per-user data is stored **/ + public static var dataPath(get, never) : String; + /** "hide" or "CDB" **/ + public static var name(get, never) : String; + + static var openListeners : Array -> Void>; + static var shortcuts : Map Void>; + + static function get_argv() return Ipc.info.argv; + static function get_dataPath() return Ipc.info.dataPath; + static function get_name() return Ipc.info.name; + + /** + Called with the command line arguments when the application is started again + while already running, or when a file / url is opened from the OS. + **/ + public static function onOpen( callb : Array -> Void ) { + if( openListeners == null ) { + openListeners = []; + Ipc.on("hide-app-open", function(_, args : Array) { + for( f in openListeners.copy() ) f(args); + }); + } + openListeners.push(callb); + } + + public static function quit() { + Ipc.call("app.quit"); + } + + public static function clearCache() { + Ipc.call("app.clearCache"); + } + + public static function registerGlobalShortcut( accelerator : String, callb : Void -> Void ) { + if( shortcuts == null ) { + shortcuts = new Map(); + Ipc.on("hide-shortcut", function(_, accel : String) { + var f = shortcuts.get(accel); + if( f != null ) f(); + }); + } + shortcuts.set(accelerator, callb); + return (Ipc.call("shortcut.register", accelerator) : Bool); + } + + public static function unregisterGlobalShortcut( accelerator : String ) { + if( shortcuts == null || !shortcuts.exists(accelerator) ) return; + shortcuts.remove(accelerator); + Ipc.call("shortcut.unregister", accelerator); + } + + /** + Returns the full path of a File coming from a drag and drop or an input + **/ + public static function getPathForFile( file : js.html.File ) : String { + return Ipc.WebUtils.getPathForFile(file); + } + + public static function setShared( value : Dynamic ) : Int { + return Ipc.call("shared.set", value); + } + + public static function getShared( id : Int ) : Dynamic { + return Ipc.call("shared.get", id); + } + +} diff --git a/hide_js/libs/electron/Clipboard.hx b/hide_js/libs/electron/Clipboard.hx new file mode 100644 index 000000000..330d6bb8b --- /dev/null +++ b/hide_js/libs/electron/Clipboard.hx @@ -0,0 +1,38 @@ +package electron; + +enum abstract ClipboardType(String) { + var Text = "text"; + var Png = "png"; + var Jpeg = "jpeg"; + var Html = "html"; + var Rtf = "rtf"; +} + +typedef ClipboardData = { + data : String, + ?type : ClipboardType, +} + +class Clipboard { + + public static function get( ?type : ClipboardType ) : String { + var v : String = Ipc.call("clipboard.get", type); + return v == null ? "" : v; + } + + public static function set( data : String, ?type : ClipboardType ) { + setMultiple([{ data : data, type : type == null ? Text : type }]); + } + + /** + Set several formats at once in the clipboard + **/ + public static function setMultiple( datas : Array ) { + Ipc.call("clipboard.set", datas); + } + + public static function clear() { + Ipc.call("clipboard.clear"); + } + +} diff --git a/hide_js/libs/electron/Dialog.hx b/hide_js/libs/electron/Dialog.hx new file mode 100644 index 000000000..1675c868d --- /dev/null +++ b/hide_js/libs/electron/Dialog.hx @@ -0,0 +1,33 @@ +package electron; + +typedef FileDialogOptions = { + ?defaultPath : String, + ?exts : Array, + ?directory : Bool, + ?multiple : Bool, +} + +class Dialog { + + /** + Show a file open dialog, callb receives null if cancelled + **/ + public static function openFiles( options : FileDialogOptions, callb : Null> -> Void ) { + Ipc.callAsync("dialog.open", options).then((files) -> callb(files)); + } + + /** + Show a file save dialog, callb receives null if cancelled + **/ + public static function saveFile( options : FileDialogOptions, callb : Null -> Void ) { + Ipc.callAsync("dialog.save", options).then((files : Array) -> callb(files == null ? null : files[0])); + } + + /** + Synchronous replacement for window.prompt() which is not supported in Electron + **/ + public static function prompt( text : String, ?defaultValue : String ) : Null { + return Ipc.call("dialog.prompt", text, defaultValue); + } + +} diff --git a/hide_js/libs/electron/Ipc.hx b/hide_js/libs/electron/Ipc.hx new file mode 100644 index 000000000..2609cc1ec --- /dev/null +++ b/hide_js/libs/electron/Ipc.hx @@ -0,0 +1,63 @@ +package electron; + +@:jsRequire("electron", "ipcRenderer") +private extern class IpcRenderer { + static function sendSync( channel : String, cmd : String, args : haxe.extern.Rest ) : Dynamic; + static function send( channel : String, cmd : String, args : haxe.extern.Rest ) : Void; + static function invoke( channel : String, cmd : String, args : haxe.extern.Rest ) : js.lib.Promise; + static function on( channel : String, listener : haxe.Constraints.Function ) : Void; +} + +@:jsRequire("electron", "webUtils") +extern class WebUtils { + static function getPathForFile( file : js.html.File ) : String; +} + +typedef AppInfo = { + var argv : Array; + var dataPath : String; + var name : String; + var windowId : Int; + var version : String; +} + +/** + Communication with the Electron main process (bin/main.js) +**/ +class Ipc { + + public static var info(get, null) : AppInfo; + + static function get_info() { + if( info == null ) + info = send("hello"); + return info; + } + + static function send( cmd : String, args : haxe.extern.Rest ) : Dynamic { + var r : Dynamic = IpcRenderer.sendSync("hide-sync", cmd, ...args); + if( r == null ) + throw "IPC failure on " + cmd; + if( r.error != null ) + throw r.error; + return r.value; + } + + /** + Synchronous call to the main process, will block until the result is available. + **/ + public static function call( cmd : String, args : haxe.extern.Rest ) : Dynamic { + if( info == null ) get_info(); + return send(cmd, ...args); + } + + public static function callAsync( cmd : String, args : haxe.extern.Rest ) : js.lib.Promise { + if( info == null ) get_info(); + return IpcRenderer.invoke("hide-async", cmd, ...args); + } + + public static function on( channel : String, listener : haxe.Constraints.Function ) { + IpcRenderer.on(channel, listener); + } + +} diff --git a/hide_js/libs/electron/Menu.hx b/hide_js/libs/electron/Menu.hx new file mode 100644 index 000000000..3e4ab7232 --- /dev/null +++ b/hide_js/libs/electron/Menu.hx @@ -0,0 +1,21 @@ +package electron; + +/** + A native menu, attached to a window by setting `Window.menu` +**/ +class Menu { + + public var items(default, null) : Array = []; + + public function new() { + } + + public function append( m : MenuItem ) { + items.push(m); + } + + function toTemplate( all : Map ) : Array { + return [for( m in items ) @:privateAccess m.toTemplate(all)]; + } + +} diff --git a/hide_js/libs/electron/MenuItem.hx b/hide_js/libs/electron/MenuItem.hx new file mode 100644 index 000000000..fe8127a9c --- /dev/null +++ b/hide_js/libs/electron/MenuItem.hx @@ -0,0 +1,57 @@ +package electron; + +enum abstract MenuItemType(String) { + var Normal = "normal"; + var Checkbox = "checkbox"; + var Separator = "separator"; +} + +typedef MenuItemOptions = { label : String, ?type : MenuItemType, ?submenu : Menu }; + +class MenuItem { + + static var UID = 0; + + public var id(default, null) : String; + public var label(default, null) : String; + public var type(default, null) : MenuItemType; + public var submenu(default, null) : Menu; + public var checked(default, set) : Bool = false; + public var enabled(default, set) : Bool = true; + + var attached = false; + + public function new( options : MenuItemOptions ) { + id = "m" + (UID++); + label = options.label; + type = options.type == null ? Normal : options.type; + submenu = options.submenu; + } + + public dynamic function click() { + } + + function set_checked( b : Bool ) { + if( attached && b != checked ) Ipc.call("win.updateMenuItem", id, { checked : b }); + return checked = b; + } + + function set_enabled( b : Bool ) { + if( attached && b != enabled ) Ipc.call("win.updateMenuItem", id, { enabled : b }); + return enabled = b; + } + + function toTemplate( all : Map ) : Dynamic { + attached = true; + all.set(id, this); + return { + id : id, + label : label, + type : type, + checked : checked, + enabled : enabled, + submenu : submenu == null ? null : @:privateAccess submenu.toTemplate(all), + }; + } + +} diff --git a/hide_js/libs/electron/Screen.hx b/hide_js/libs/electron/Screen.hx new file mode 100644 index 000000000..bec472e0a --- /dev/null +++ b/hide_js/libs/electron/Screen.hx @@ -0,0 +1,30 @@ +package electron; + +typedef Rect = { + x : Int, + y : Int, + width : Int, + height : Int, +} + +typedef IndividualScreen = { + var id : Int; + // screen bounds, can be negative depending on screen arrangement + var bounds : Rect; + // useable area within the screen bounds + var work_area : Rect; + var scaleFactor : Float; + var isBuiltIn : Bool; + var rotation : Int; + var touchSupport : String; +} + +class Screen { + + public static var screens(get, never) : Array; + + static function get_screens() : Array { + return Ipc.call("screen.all"); + } + +} diff --git a/hide_js/libs/electron/Window.hx b/hide_js/libs/electron/Window.hx new file mode 100644 index 000000000..4a44e35c1 --- /dev/null +++ b/hide_js/libs/electron/Window.hx @@ -0,0 +1,152 @@ +package electron; + +typedef WindowOptions = { + ?show : Bool, + ?title : String, + ?width : Int, + ?height : Int, +} + +/** + Handle on an Electron BrowserWindow. + + Events : close, closed, maximize, minimize, restore, move, resize, focus, blur, show, hide, + enter-fullscreen, leave-fullscreen. + + Listening to "close" on the current window prevents it from closing : `close(true)` + must then be called to actually close it. +**/ +class Window { + + static var windows : Map; + static var current : Window; + + public var id(default, null) : Int; + public var x(get, never) : Int; + public var y(get, never) : Int; + public var width(get, never) : Int; + public var height(get, never) : Int; + public var title(get, set) : String; + public var isMaximized(get, never) : Bool; + /** only supported on the current window **/ + public var menu(default, set) : Menu; + + var listeners : Map Void>> = new Map(); + var menuItems : Map = new Map(); + + function new( id : Int ) { + this.id = id; + } + + function get_x() return getBounds().x; + function get_y() return getBounds().y; + function get_width() return getBounds().width; + function get_height() return getBounds().height; + function get_isMaximized() : Bool return Ipc.call("win.isMaximized", id); + function get_title() : String return Ipc.call("win.getTitle", id); + function set_title( t : String ) { + Ipc.call("win.setTitle", id, t); + return t; + } + + public function getBounds() : Screen.Rect { + return Ipc.call("win.getBounds", id); + } + + function set_menu( m : Menu ) { + if( this != get() ) throw "Can only set menu on current window"; + for( i in menuItems ) @:privateAccess i.attached = false; + menuItems = new Map(); + Ipc.call("win.setMenu", m == null ? null : @:privateAccess m.toTemplate(menuItems)); + return menu = m; + } + + public function on( event : String, callb : Void -> Void ) { + var l = listeners.get(event); + if( l == null ) { + l = []; + listeners.set(event, l); + Ipc.call("win.subscribe", id, [event]); + } + l.push(callb); + } + + function dispatch( event : String ) { + var l = listeners.get(event); + if( l == null ) return; + for( f in l.copy() ) f(); + } + + public function showDevTools() Ipc.call("win.openDevTools", id); + public function moveTo( x : Int, y : Int ) Ipc.call("win.moveTo", id, x, y); + public function resizeTo( w : Int, h : Int ) Ipc.call("win.resizeTo", id, w, h); + public function resizeBy( dw : Int, dh : Int ) Ipc.call("win.resizeBy", id, dw, dh); + public function maximize() Ipc.call("win.maximize", id); + public function minimize() Ipc.call("win.minimize", id); + public function restore() Ipc.call("win.restore", id); + public function enterFullscreen() Ipc.call("win.setFullScreen", id, true); + public function leaveFullscreen() Ipc.call("win.setFullScreen", id, false); + public function focus() Ipc.call("win.focus", id); + public function blur() Ipc.call("win.blur", id); + public function show( b = true ) Ipc.call("win.show", id, b); + public function hide() Ipc.call("win.show", id, false); + public function close( force = false ) Ipc.call("win.close", id, force); + + static function init() { + if( windows != null ) return; + windows = new Map(); + Ipc.on("hide-win-event", function(_, id : Int, event : String) { + var w = windows.get(id); + if( w == null ) return; + w.dispatch(event); + if( event == "closed" ) windows.remove(id); + }); + Ipc.on("hide-menu-click", function(_, itemId : String) { + var item = current == null ? null : current.menuItems.get(itemId); + if( item != null ) item.click(); + }); + } + + static function getById( id : Int ) { + init(); + var w = windows.get(id); + if( w == null ) { + w = new Window(id); + windows.set(id, w); + } + return w; + } + + /** + The window of the current page + **/ + public static function get() : Window { + if( current == null ) current = getById(Ipc.info.windowId); + return current; + } + + public static function getAll() : Array { + var all : Array<{ id : Int }> = Ipc.call("win.list"); + return [for( w in all ) getById(w.id)]; + } + + /** + Open a new window, url is relative to the application directory + **/ + public static function open( url : String, ?options : WindowOptions ) : Window { + var id : Int = Ipc.call("win.open", url, options); + return getById(id); + } + + /** + Call a function on the page that opened this window + **/ + public static function callOpener( name : String, param : Dynamic ) { + Ipc.call("win.callParent", name, param); + } + + public static function onOpenerCall( callb : String -> Dynamic -> Void ) { + Ipc.on("hide-parent-call", function(_, name : String, param : Dynamic) callb(name, param)); + } + +} diff --git a/hide_js/libs/electron/main/Electron.hx b/hide_js/libs/electron/main/Electron.hx new file mode 100644 index 000000000..be2bdf8c0 --- /dev/null +++ b/hide_js/libs/electron/main/Electron.hx @@ -0,0 +1,123 @@ +package electron.main; + +/** + Minimal externs for the Electron main process APIs used by hide.ElectronMain +**/ + +typedef Rectangle = { x : Int, y : Int, width : Int, height : Int }; + +@:jsRequire("electron", "app") +extern class App { + static var commandLine : { function appendSwitch( name : String, ?value : String ) : Void; }; + static var isPackaged : Bool; + static function setName( name : String ) : Void; + static function getPath( name : String ) : String; + static function setPath( name : String, path : String ) : Void; + static function getAppPath() : String; + static function requestSingleInstanceLock() : Bool; + static function whenReady() : js.lib.Promise; + static function quit() : Void; + static function on( event : String, callb : haxe.Constraints.Function ) : Void; +} + +@:jsRequire("electron", "BrowserWindow") +extern class BrowserWindow { + var id(default, null) : Int; + var webContents(default, null) : WebContents; + function new( options : Dynamic ) : Void; + function on( event : String, callb : haxe.Constraints.Function ) : Void; + function once( event : String, callb : haxe.Constraints.Function ) : Void; + function loadURL( url : String ) : js.lib.Promise; + function loadFile( file : String, ?options : Dynamic ) : js.lib.Promise; + function isDestroyed() : Bool; + function getBounds() : Rectangle; + function getSize() : Array; + function setSize( w : Int, h : Int ) : Void; + function setPosition( x : Int, y : Int ) : Void; + function getTitle() : String; + function setTitle( t : String ) : Void; + function isMaximized() : Bool; + function isMinimized() : Bool; + function maximize() : Void; + function unmaximize() : Void; + function minimize() : Void; + function restore() : Void; + function setFullScreen( b : Bool ) : Void; + function show() : Void; + function showInactive() : Void; + function hide() : Void; + function focus() : Void; + function blur() : Void; + function close() : Void; + function setMenu( menu : Menu ) : Void; + static function fromWebContents( wc : WebContents ) : Null; + static function fromId( id : Int ) : Null; + static function getAllWindows() : Array; +} + +@:jsRequire("electron", "webContents") +extern class WebContents { + var id(default, null) : Int; + var session(default, null) : Session; + function on( event : String, callb : haxe.Constraints.Function ) : Void; + function send( channel : String, args : haxe.extern.Rest ) : Void; + function isDestroyed() : Bool; + function isCrashed() : Bool; + function openDevTools( ?options : Dynamic ) : Void; + function setDevToolsWebContents( wc : WebContents ) : Void; + static function fromId( id : Int ) : Null; +} + +@:jsRequire("electron", "Menu") +extern class Menu { + function getMenuItemById( id : String ) : Null; + static function buildFromTemplate( template : Array ) : Menu; + static function setApplicationMenu( menu : Null ) : Void; +} + +@:jsRequire("electron", "ipcMain") +extern class IpcMain { + static function on( channel : String, callb : haxe.Constraints.Function ) : Void; + static function off( channel : String, callb : haxe.Constraints.Function ) : Void; + static function handle( channel : String, callb : haxe.Constraints.Function ) : Void; +} + +@:jsRequire("electron", "dialog") +extern class Dialog { + static function showOpenDialog( win : BrowserWindow, options : Dynamic ) : js.lib.Promise<{ canceled : Bool, filePaths : Array }>; + static function showSaveDialog( win : BrowserWindow, options : Dynamic ) : js.lib.Promise<{ canceled : Bool, ?filePath : String }>; +} + +@:jsRequire("electron", "screen") +extern class Screen { + static function getAllDisplays() : Array; +} + +@:jsRequire("electron", "clipboard") +extern class Clipboard { + static function readText() : js.lib.Promise; + static function read() : js.lib.Promise>; + static function write( items : Array ) : js.lib.Promise; + static function clear() : Void; +} + +@:jsRequire("electron", "ClipboardItem") +extern class ClipboardItem { + var types(default, null) : Array; + function new( items : Dynamic ) : Void; + function getType( type : String ) : js.lib.Promise; +} + +@:jsRequire("electron", "globalShortcut") +extern class GlobalShortcut { + static function register( accelerator : String, callb : Void -> Void ) : Bool; + static function unregister( accelerator : String ) : Void; + static function unregisterAll() : Void; +} + +@:jsRequire("electron", "session") +extern class Session { + static var defaultSession(default, null) : Session; + function clearCache() : js.lib.Promise; + function setSpellCheckerLanguages( langs : Array ) : Void; +} diff --git a/hide_js/libs/nw/App.hx b/hide_js/libs/nw/App.hx deleted file mode 100644 index 648f0aaa1..000000000 --- a/hide_js/libs/nw/App.hx +++ /dev/null @@ -1,11 +0,0 @@ -package nw; - -extern class App { - - public static var argv(default, never) : Array; - public static var manifest(default, never) : Dynamic; - public static var dataPath(default, never) : String; - public static function on( event : String, callb : String -> Void ) : Void; - public static function clearCache( ) : Void; - -} \ No newline at end of file diff --git a/hide_js/libs/nw/Clipboard.hx b/hide_js/libs/nw/Clipboard.hx deleted file mode 100644 index 37ae95df5..000000000 --- a/hide_js/libs/nw/Clipboard.hx +++ /dev/null @@ -1,25 +0,0 @@ -package nw; - -enum abstract ClipboardType(String) { - var Text = "text"; - var Png = "png"; - var Jpeg = "jpeg"; - var Html = "html"; - var Rtf = "rtf"; -} - -typedef ClipboardData = { - data: String, - ?type: ClipboardType, - ?raw: Bool -} -extern class Clipboard { - - function set( clipboardData : Dynamic ) : Void; - function get( ?type : ClipboardType, ?raw : Bool ) : String; - function readAvailableTypes() : Array; - function clear() : Void; - - static function get() : Clipboard; - -} \ No newline at end of file diff --git a/hide_js/libs/nw/Menu.hx b/hide_js/libs/nw/Menu.hx deleted file mode 100644 index ec3a1663d..000000000 --- a/hide_js/libs/nw/Menu.hx +++ /dev/null @@ -1,14 +0,0 @@ -package nw; - -enum abstract MenuType(String) { - var Menubar = "menubar"; - var ContextMenu = "contextmenu"; -} - -extern class Menu { - - public function new( ?options : { ?label : String, ?type : MenuType } ) : Void; - public function append( m : MenuItem ) : Void; - public function popup( x : Int, y : Int ) : Void; - -} \ No newline at end of file diff --git a/hide_js/libs/nw/MenuItem.hx b/hide_js/libs/nw/MenuItem.hx deleted file mode 100644 index ad05ee054..000000000 --- a/hide_js/libs/nw/MenuItem.hx +++ /dev/null @@ -1,18 +0,0 @@ -package nw; - -enum abstract MenuItemType(String) { - var Normal = "normal"; - var Checkbox = "checkbox"; - var Separator = "separator"; -} - -typedef MenuItemOptions = { label : String, ?icon : String, ?type : MenuItemType, ?submenu : Menu, ?key : String, ?modifiers : String }; - -extern class MenuItem { - - public var checked : Bool; - public var enabled : Bool; - - public function new( options : MenuItemOptions ) : Void; - public dynamic function click() : Void; -} \ No newline at end of file diff --git a/hide_js/libs/nw/Screen.hx b/hide_js/libs/nw/Screen.hx deleted file mode 100644 index 7f41445ca..000000000 --- a/hide_js/libs/nw/Screen.hx +++ /dev/null @@ -1,32 +0,0 @@ -package nw; - -typedef IndividualScreen = { - var id : Int; - - // physical screen resolution, can be negative, not necessarily start from 0,depending on screen arrangement - var bounds : { - x : Int, - y : Int, - width : Int, - height : Int, - }; - // useable area within the screen bound - var work_area : { - x : Int, - y : Int, - width : Int, - height : Int, - }; - var scaleFactor : Float; - var isBuiltIn : Bool; - var rotation : Int; - var touchSupport : Int; -} - -extern class Screen { - public static var screens(default, never) : Array; - public static function Init() : Void; - public static function on( event : String, callb : IndividualScreen -> Void ) : Void; - public static function chooseDesktopMedia(sources : Array, callb : Dynamic -> Void) : Void; - -} \ No newline at end of file diff --git a/hide_js/libs/nw/Window.hx b/hide_js/libs/nw/Window.hx deleted file mode 100644 index 19453494e..000000000 --- a/hide_js/libs/nw/Window.hx +++ /dev/null @@ -1,39 +0,0 @@ -package nw; - -extern class Window { - - public var x : Int; - public var y : Int; - public var width : Int; - public var height : Int; - public var zoomLevel : Float; - - public var window : js.html.Window; - - public var menu : Menu; - public var title : String; - - public function showDevTools() : Void; - - public function moveTo( x : Int, y : Int ) : Void; - public function moveBy( dx : Int, dy : Int ) : Void; - public function resizeTo( w : Int, h : Int ) : Void; - public function resizeBy( dw : Int, dh : Int ) : Void; - - public function maximize() : Void; - public function minimize() : Void; - public function restore() : Void; - public function enterFullscreen() : Void; - public function leaveFullscreen() : Void; - public function on( event : String, callb : Void -> Void ) : Void; - - public function focus() : Void; - public function blur() : Void; - public function show( ?b : Bool ) : Void; - - public function close( ?force : Bool ) : Void; - - public static function get() : Window; - public static function open( url : String, ?params : {?new_instance:Bool,?inject_js_start:String,?inject_js_end:String,?id:String}, ?callb:Window->Void ) : Void; - -} \ No newline at end of file From c037a1e402eb822053e8acf86526e31c53472b09 Mon Sep 17 00:00:00 2001 From: Nicolas Cannasse Date: Fri, 9 Oct 2026 18:33:06 +0200 Subject: [PATCH 002/191] added jsdx12 --- .gitignore | 3 + CLAUDE.md | 1 + bin/app.html | 5 + hide_dx12.hxml | 5 + hide_js/hide/ElectronMain.hx | 3 + hide_js/libs/Single.hx | 5 + hide_js/libs/dx/Display.hx | 104 ++ hide_js/libs/dx/Dx12.hx | 1905 +++++++++++++++++++++++ hide_js/libs/dx/Dx12Macros.hx | 21 + hide_js/libs/dx/Window.hx | 36 + hide_js/libs/hlemu/Abstract.hx | 4 + hide_js/libs/hlemu/Api.hx | 10 + hide_js/libs/hlemu/Bytes.hx | 170 ++ hide_js/libs/hlemu/BytesAccess.hx | 8 + hide_js/libs/hlemu/CArray.hx | 40 + hide_js/libs/hlemu/CArrayData.hx | 19 + hide_js/libs/hlemu/F32.hx | 3 + hide_js/libs/hlemu/F64.hx | 3 + hide_js/libs/hlemu/I64.hx | 30 + hide_js/libs/hlemu/Macros.hx | 722 +++++++++ hide_js/libs/hlemu/Native.hx | 31 + hide_js/libs/hlemu/NativeArray.hx | 47 + hide_js/libs/hlemu/Ref.hx | 19 + hide_js/libs/hlemu/StringExt.hx | 14 + hide_js/libs/hlemu/Struct.hx | 72 + hide_js/libs/hlemu/UI16.hx | 4 + hide_js/libs/hlemu/UI8.hx | 4 + hide_js/libs/jsdx/native/build.bat | 31 + hide_js/libs/jsdx/native/dx12js.cpp | 334 ++++ hide_js/libs/jsdx/native/gen-dx12-hx.js | 179 +++ hide_js/libs/jsdx/native/hl.h | 168 ++ hide_js/libs/jsdx/native/napi_prim.h | 129 ++ hide_js/libs/jsdx/package-lock.json | 20 + hide_js/libs/jsdx/package.json | 12 + 34 files changed, 4161 insertions(+) create mode 100644 hide_dx12.hxml create mode 100644 hide_js/libs/Single.hx create mode 100644 hide_js/libs/dx/Display.hx create mode 100644 hide_js/libs/dx/Dx12.hx create mode 100644 hide_js/libs/dx/Dx12Macros.hx create mode 100644 hide_js/libs/dx/Window.hx create mode 100644 hide_js/libs/hlemu/Abstract.hx create mode 100644 hide_js/libs/hlemu/Api.hx create mode 100644 hide_js/libs/hlemu/Bytes.hx create mode 100644 hide_js/libs/hlemu/BytesAccess.hx create mode 100644 hide_js/libs/hlemu/CArray.hx create mode 100644 hide_js/libs/hlemu/CArrayData.hx create mode 100644 hide_js/libs/hlemu/F32.hx create mode 100644 hide_js/libs/hlemu/F64.hx create mode 100644 hide_js/libs/hlemu/I64.hx create mode 100644 hide_js/libs/hlemu/Macros.hx create mode 100644 hide_js/libs/hlemu/Native.hx create mode 100644 hide_js/libs/hlemu/NativeArray.hx create mode 100644 hide_js/libs/hlemu/Ref.hx create mode 100644 hide_js/libs/hlemu/StringExt.hx create mode 100644 hide_js/libs/hlemu/Struct.hx create mode 100644 hide_js/libs/hlemu/UI16.hx create mode 100644 hide_js/libs/hlemu/UI8.hx create mode 100644 hide_js/libs/jsdx/native/build.bat create mode 100644 hide_js/libs/jsdx/native/dx12js.cpp create mode 100644 hide_js/libs/jsdx/native/gen-dx12-hx.js create mode 100644 hide_js/libs/jsdx/native/hl.h create mode 100644 hide_js/libs/jsdx/native/napi_prim.h create mode 100644 hide_js/libs/jsdx/package-lock.json create mode 100644 hide_js/libs/jsdx/package.json diff --git a/.gitignore b/.gitignore index f3be2297f..07cff31f8 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ bin_hl/ *.dump /bin/electron /bin/main.js +/bin/jsdx +/hide_js/libs/jsdx/node_modules +/hide_js/libs/jsdx/build diff --git a/CLAUDE.md b/CLAUDE.md index 11d6d9fc1..48b542264 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,7 @@ - Renderer code talks to the main process only through the wrappers in `hide_js/libs/electron/` (`electron.Ipc`, `electron.Window`, `electron.Clipboard`, ...). Main process externs are in `hide_js/libs/electron/main/`. - Keep main process code in Haxe, not hand-written JS. - `haxe hide.hxml` builds both `bin/hide.js` and `bin/main.js`. The Electron runtime is unpacked in `bin/electron/`. +- DirectX 12 (jsdx) : `haxe hide_dx12.hxml` builds `bin/hide.js` with the heaps DX12Driver (heaps branch `js-dx12`). The HL emulation (`hlemu`) and `dx` API are in `hide_js/libs`; the N-API addon `bin/jsdx/dx12.node` is built by `hide_js/libs/jsdx/native/build.bat` (needs `HASHLINK_SRC` and `npm install` in `hide_js/libs/jsdx`). `dx/Dx12.hx` is generated by `hide_js/libs/jsdx/native/gen-dx12-hx.js`, do not edit it. ## Running the editor for tests diff --git a/bin/app.html b/bin/app.html index d72ab6be0..6cd541b9c 100644 --- a/bin/app.html +++ b/bin/app.html @@ -17,6 +17,11 @@ // Electron defines a global "module" : hide it so jQuery, goldenlayout and monaco // register themselves as browser globals instead of CommonJS / node modules var nodeModule = window.module; + // DirectX 12 addon used by hide.js when built with -D dx12 (hide_dx12.hxml, see hide_js/libs/jsdx/native/build.bat) + (function() { + var dx12Path = require("path").join(__dirname, "jsdx", "dx12.node"); + if( require("fs").existsSync(dx12Path) ) globalThis.__dx12 = require(dx12Path); + })(); window.module = undefined; // hide.js overrides the global Reflect with the Haxe one var nativeReflect = Reflect; diff --git a/hide_dx12.hxml b/hide_dx12.hxml new file mode 100644 index 000000000..9d1d5a985 --- /dev/null +++ b/hide_dx12.hxml @@ -0,0 +1,5 @@ +# HIDE with the DirectX 12 driver (jsdx), requires bin/jsdx/dx12.node (see hide_js/libs/jsdx/native/build.bat) +-D dx12 +--remap hl:hlemu +--macro hlemu.Macros.init(["dx", "h3d.impl"]) +hide.hxml diff --git a/hide_js/hide/ElectronMain.hx b/hide_js/hide/ElectronMain.hx index 0e02a34cc..f286f4128 100644 --- a/hide_js/hide/ElectronMain.hx +++ b/hide_js/hide/ElectronMain.hx @@ -56,6 +56,9 @@ class ElectronMain { App.setPath("userData", js.node.Path.join(App.getPath("appData"), appName)); App.commandLine.appendSwitch("js-flags", "--expose-gc --no-efficiency-mode"); App.commandLine.appendSwitch("disable-features", "AllowSoftwareGLFallbackDueToCrashes"); + // DirectX 12 (jsdx) : Chromium must use the same GPU as the dx12 addon, shared textures can not cross adapters + App.commandLine.appendSwitch("force_high_performance_gpu"); + App.commandLine.appendSwitch("ignore-gpu-blocklist"); if( !App.requestSingleInstanceLock() ) { App.quit(); diff --git a/hide_js/libs/Single.hx b/hide_js/libs/Single.hx new file mode 100644 index 000000000..34e1a4396 --- /dev/null +++ b/hide_js/libs/Single.hx @@ -0,0 +1,5 @@ +/** + Single-precision float, as in HL (StdTypes only defines it for java, hl and cpp). + A JS number at runtime, 4 bytes in emulated structs. +**/ +@:notNull abstract Single(Float) from Float to Float {} diff --git a/hide_js/libs/dx/Display.hx b/hide_js/libs/dx/Display.hx new file mode 100644 index 000000000..a14cf4439 --- /dev/null +++ b/hide_js/libs/dx/Display.hx @@ -0,0 +1,104 @@ +package dx; + +/** + Displays the emulated swapchain in the page : the back buffers are shared textures + created by the addon, imported once with Electron's sharedTexture module and drawn + into the window canvas with WebGPU at each present. +**/ +class Display { + + static var window : Window; + static var textures : Array = []; + static var device : Dynamic; + static var context : Dynamic; + static var pipeline : Dynamic; + static var sampler : Dynamic; + static var format : String; + static var pending = false; + + public static function init( win : Window, tries = 5 ) { + window = win; + var gpu : Dynamic = js.Syntax.code("navigator.gpu"); + if( gpu == null ) { + js.Browser.console.error("DX12 display : no WebGPU in this page, nothing is shown"); + return; + } + // the GPU of the addon (Dx12.create) : the shared textures do not cross adapters + gpu.requestAdapter({ powerPreference : "high-performance" }).then(function( adapter : Dynamic ) { + // none while the GPU process (re)starts, after a reload of the page : asked again + if( adapter == null ) { + if( tries > 1 ) js.Browser.window.setTimeout(() -> init(win, tries - 1), 200); + else js.Browser.console.error("DX12 display : no WebGPU adapter, nothing is shown"); + return null; + } + return adapter.requestDevice(); + }).then(function( dev : Dynamic ) { + if( dev == null ) return; + device = dev; + context = window.canvas.getContext("webgpu"); + format = gpu.getPreferredCanvasFormat(); + context.configure({ device : device, format : format, alphaMode : "opaque" }); + var module = device.createShaderModule({ code : " + struct V { @builtin(position) pos : vec4f, @location(0) uv : vec2f }; + @vertex fn vs( @builtin(vertex_index) i : u32 ) -> V { + let p = vec2f(f32((i << 1u) & 2u), f32(i & 2u)); + return V(vec4f(p * 2.0 - 1.0, 0.0, 1.0), vec2f(p.x, 1.0 - p.y)); + } + @group(0) @binding(0) var s : sampler; + @group(0) @binding(1) var t : texture_external; + @fragment fn fs( v : V ) -> @location(0) vec4f { return textureSampleBaseClampToEdge(t, s, v.uv); } + " }); + pipeline = device.createRenderPipeline({ + layout : "auto", + vertex : { module : module, entryPoint : "vs" }, + fragment : { module : module, entryPoint : "fs", targets : [{ format : format }] }, + }); + sampler = device.createSampler({ magFilter : "nearest", minFilter : "nearest" }); + }); + } + + /** called after the native resize : (re)imports the back buffers **/ + public static function resize( width : Int, height : Int ) { + for( t in textures ) t.release(); + textures = []; + var sharedTexture : Dynamic = js.Syntax.code("require('electron').sharedTexture"); + var count : Int = js.Syntax.code("__dx12.js_back_buffer_count()"); + for( i in 0...count ) { + var handle : Float = js.Syntax.code("__dx12.js_back_buffer_handle({0})", i); + var buf = new js.lib.DataView(new js.lib.ArrayBuffer(8)); + hlemu.Struct.setNum64(buf, 0, handle); + var nodeBuf = js.Syntax.code("Buffer.from({0})", buf.buffer); + textures.push(sharedTexture.subtle.importSharedTexture({ + pixelFormat : "rgba", + codedSize : { width : width, height : height }, + handle : { ntHandle : nodeBuf }, + })); + } + window.canvas.width = width; + window.canvas.height = height; + } + + /** called after the native present : draws the presented buffer into the canvas **/ + public static function present() { + if( device == null ) return; + var index : Int = js.Syntax.code("__dx12.js_presented_buffer()"); + var tex = textures[index]; + if( tex == null ) return; + var frame : Dynamic = tex.getVideoFrame(); + var ext = device.importExternalTexture({ source : frame }); + var bind = device.createBindGroup({ layout : pipeline.getBindGroupLayout(0), entries : [ + { binding : 0, resource : sampler }, + { binding : 1, resource : ext }, + ] }); + var enc = device.createCommandEncoder(); + var pass = enc.beginRenderPass({ colorAttachments : [{ + view : context.getCurrentTexture().createView(), loadOp : "clear", storeOp : "store", clearValue : [0, 0, 0, 1], + }] }); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bind); + pass.draw(3); + pass.end(); + device.queue.submit([enc.finish()]); + frame.close(); + } +} diff --git a/hide_js/libs/dx/Dx12.hx b/hide_js/libs/dx/Dx12.hx new file mode 100644 index 000000000..dd858e12b --- /dev/null +++ b/hide_js/libs/dx/Dx12.hx @@ -0,0 +1,1905 @@ +package dx; + +// GENERATED by jsdx/native/gen-dx12-hx.js from hldx's dx/Dx12.hx : do not edit. +// JS version of the hldx DirectX 12 API, implemented by the dx12 Node-API addon. + +#if !macro + +import haxe.Int64; + +typedef DriverInstance = hlemu.Abstract<"dx_driver">; + +typedef Device = hlemu.Abstract<"dx_device">; + +typedef Adapter = hlemu.Abstract<"dx_adapter">; + +typedef Factory = hlemu.Abstract<"dx_factory">; + +typedef SwapChain = hlemu.Abstract<"dx_swapchain">; + +enum DriverInitFlag { + DEBUG; + GPU_BASED_VALIDATION; + BREAK_ON_ERROR; +} + +enum abstract Constant(Int) to Int { + public var TEXTURE_DATA_PITCH_ALIGNMENT; + public var TEXTURE_DATA_PLACEMENT_ALIGNMENT; + public var DESCRIPTOR_RANGE_OFFSET_APPEND; + public var RESOURCE_BARRIER_ALL_SUBRESOURCES; +} + +enum abstract CommandListType(Int) { + public var DIRECT = 0; + public var BUNDLE = 1; + public var COMPUTE = 2; + public var COPY = 3; + public var VIDEO_DECODE = 4; + public var VIDEO_PROCESS = 5; + public var VIDEO_ENCODE = 6; +} + +typedef DriverInitFlags = haxe.EnumFlags; + +@:hlNative("dx12","resource_") +abstract Resource(hlemu.Abstract<"dx_resource">) { + public function release() {} + public inline function setName( name : String ) { + var b = hl.Bytes.ofUcs2(name); + set_name(b); + b.free(); + } + function set_name( name : hl.Bytes ) {} +} + +@:struct class Range { + public var begin : Int64; + public var end : Int64; + public function new() { + } +} + +@:hlNative("dx12","resource_") @:forward(release, setName) +abstract GpuResource(Resource) { + @:hlNative("dx12","resource_get_gpu_virtual_address") + public function getGpuVirtualAddress() : Int64 { return 0; } + @:hlNative("dx12","get_required_intermediate_size") + public function getRequiredIntermediateSize( subRes : Int, resCount : Int ) : Int64 { return 0; } + public function map( subResource : Int, range : Range ) : hl.Bytes { return null; } + public function unmap( subResource : Int, writtenRange : Range ) {} + @:to inline function to() : Resource { return cast this; } +} + +abstract PipelineState(Resource) { +} + +@:forward(release) +abstract CommandSignature(Resource) { +} + +@:hlNative("dx12","command_queue_") +abstract CommandQueue(Resource) { + public function new(type) { + this = create(type); + } + public function executeCommandList( commandList : CommandList ) {} + public function executeCommandLists( commandLists : hl.CArray, count : Int ) {} + public function signal( fence : Fence, value : Int64 ) {} + public function wait( fence : Fence, value : Int64 ) {} + public function present( vsync : Bool ) { + present_native(vsync); + dx.Display.present(); + } + @:hlNative("dx12","command_queue_present") + function present_native( vsync : Bool ) {} + public function suspend() {} + public function resume() {} + @:hlNative("dx12","command_queue_get_timestamp_frequency") + public function getTimestampFrequency() : Int64 { return 0; } + static function create( type : CommandListType ) : Resource { return null; } +} + +@:hlNative("dx12","command_allocator_") +abstract CommandAllocator(Resource) { + public function new(type) { + this = create(type); + } + public function reset() {} + static function create( type : CommandListType ) : Resource { return null; } +} + +enum abstract ClearFlags(Int) { + var DEPTH = 1; + var STENCIL = 2; + var BOTH = 3; +} + +enum abstract PrimitiveTopology(Int) { + var UNDEFINED = 0; + var POINTLIST = 1; + var LINELIST = 2; + var LINESTRIP = 3; + var TRIANGLELIST = 4; + var TRIANGLESTRIP = 5; + var LINELIST_ADJ = 10; + var LINESTRIP_ADJ = 11; + var TRIANGLELIST_ADJ = 12; + var TRIANGLESTRIP_ADJ = 13; + public static function controlPointPatchList( count : Int ) : PrimitiveTopology { + return cast (32 + count); + } +} + +@:struct class VertexBufferView { + public var bufferLocation : Address; + public var sizeInBytes : Int; + public var strideInBytes : Int; + public function new() { + } +} + +@:struct class IndexBufferView { + public var bufferLocation : Address; + public var sizeInBytes : Int; + public var format : DxgiFormat; + public function new() { + } +} + +@:struct class Viewport { + public var topLeftX : Single; + public var topLeftY : Single; + public var width : Single; + public var height : Single; + public var minDepth : Single; + public var maxDepth : Single; + public function new() { + } +} + +@:struct class Rect { + public var left : Int; + public var top : Int; + public var right : Int; + public var bottom : Int; + public function new() { + } +} + +@:struct class Box { + public var left : Int; + public var top : Int; + public var front : Int; + public var right : Int; + public var bottom : Int; + public var back : Int; + public function new() { + } +} + +enum abstract TextureCopyType(Int) { + var SUBRESOURCE_INDEX = 0; + var PLACED_FOOTPRINT = 1; +} + +@:struct class SubresourceFootprint { + public var format : DxgiFormat; + public var width : Int; + public var height : Int; + public var depth : Int; + public var rowPitch : Int; + public function new() { + } +} + +@:struct class PlacedSubresourceFootprint { + public var offset : Int64; + @:packed public var footprint(default,null) : SubresourceFootprint; + public function new() { + } +} + +@:struct class TextureCopyLocation { + public var res : GpuResource; + public var type : TextureCopyType; + var __unionPadding : Int; + + public var subResourceIndex(get,set) : Int; + inline function get_subResourceIndex() : Int return placedFootprint.offset.low & 0xFF; + inline function set_subResourceIndex(v: Int) { placedFootprint.offset = v; return v; } + + @:packed public var placedFootprint(default,null) : PlacedSubresourceFootprint; + public function new() { + } +} + +@:hlNative("dx12","command_list_") +abstract CommandList(Resource) { + public function new(type,alloc,state) { + this = create(type,alloc,state); + } + + public function close() {} + public function clearRenderTargetView( rtv : Address, color : ClearColor ) {} + public function clearDepthStencilView( rtv : Address, flags : ClearFlags, depth : Single, stencil : Int ) {} + public function reset( alloc : CommandAllocator, state : PipelineState ) {} + public function resourceBarrier( b : ResourceBarrier ) {} + public function resourceBarriers( b : hl.CArray, barrierCount : Int ) {} + public function setPipelineState( state : PipelineState ) {} + public function setDescriptorHeaps( heaps : hl.NativeArray ) {} + public function copyBufferRegion( dst : GpuResource, dstOffset : Int64, src : GpuResource, srcOffset : Int64, size : Int64 ) {} + public function copyTextureRegion( dst : TextureCopyLocation, dstX : Int, dstY : Int, dstZ : Int, src : TextureCopyLocation, srcBox : Box ) {} + + public function setGraphicsRootSignature( sign : RootSignature ) {} + public function setGraphicsRoot32BitConstants( index : Int, numValues : Int, data : hl.Bytes, dstOffset : Int ) {} + public function setGraphicsRootConstantBufferView( index : Int, address : Address ) {} + public function setGraphicsRootDescriptorTable( index : Int, address : Address ) {} + public function setGraphicsRootShaderResourceView( index : Int, address : Address ) {} + public function setGraphicsRootUnorderedAccessView( index : Int, address : Address ) {} + + public function iaSetPrimitiveTopology( top : PrimitiveTopology ) {} + public function iaSetVertexBuffers( startSlot : Int, numViews : Int, views : VertexBufferView /* hl.CArray */ ) {} + public function iaSetIndexBuffer( view : IndexBufferView ) {} + + public function drawInstanced( vertexCountPerInstance : Int, instanceCount : Int, startVertexLocation : Int, startInstanceLocation : Int ) {} + public function drawIndexedInstanced( indexCountPerInstance : Int, instanceCount : Int, startIndexLocation : Int, baseVertexLocation : Int, startInstanceLocation : Int ) {} + public function executeIndirect( sign : CommandSignature, maxCommandCount : Int, args : Resource, argsOffset : Int64, count : Resource, countOffset : Int64 ) {} + + public function omSetRenderTargets( count : Int, handles : hl.BytesAccess
, flag : Bool32, depthStencils : hl.BytesAccess
) {} + public function omSetStencilRef( value : Int ) {} + + public function rsSetViewports( count : Int, viewports : Viewport ) {} + public function rsSetScissorRects( count : Int, rects : Rect ) {} + + @:hlNative("?dx12debug","command_list_pix_begin_event") + public function pixBeginEvent( color : haxe.Int64, formatString : hl.Bytes ) {} + @:hlNative("?dx12debug","command_list_pix_end_event") + public function pixEndEvent() {} + + public function beginQuery( heap : QueryHeap, type : QueryType, index : Int ) {} + public function endQuery( heap : QueryHeap, type : QueryType, index : Int ) {} + public function resolveQueryData( heap : QueryHeap, type : QueryType, index : Int, count : Int, dest : Resource, offset : Int64 ) {} + + public function setPredication( res : Resource, offset : Int64, op : PredicationOp ) {} + + public function setComputeRootSignature( sign : RootSignature ) {} + public function setComputeRoot32BitConstants( index : Int, numValues : Int, data : hl.Bytes, dstOffset : Int ) {} + public function setComputeRootConstantBufferView( index : Int, address : Address ) {} + public function setComputeRootDescriptorTable( index : Int, address : Address ) {} + public function setComputeRootShaderResourceView( index : Int, address : Address ) {} + public function setComputeRootUnorderedAccessView( index : Int, address : Address ) {} + public function dispatch( x : Int, y : Int, z : Int ) {} + + static function create( type : CommandListType, alloc : CommandAllocator, state : PipelineState ) : Resource { return null; } +} + +enum abstract FenceFlags(Int) { + var NONE = 0; + var SHARED = 1; + var SHARED_CROSS_ADAPTER = 2; + var NON_MONITORED = 4; +} + +@:hlNative("dx12","fence_") +abstract Fence(Resource) { + public function new(value,flags) { + this = create(value, flags); + } + @:hlNative("dx12","fence_get_completed_value") + public function getValue() : Int64 { return 0; } + public function setEvent( value : Int64, event : WaitEvent ) {} + static function create( value : Int64, flags : FenceFlags ) : Resource { return null; } +} + +@:hlNative("dx12","waitevent_") +abstract WaitEvent(hlemu.Abstract<"dx_event">) { + + public function new(state) { + this = cast create(state); + } + + public function wait( time : Int ) : Bool { return false; } + static function create( state : Bool ) : WaitEvent { return null; } +} + +enum abstract DescriptorHeapType(Int) { + var CBV_SRV_UAV = 0; + var SAMPLER = 1; + var RTV = 2; + var DSV = 3; +} + +enum abstract DescriptorHeapFlags(Int) { + var NONE = 0; + var SHADER_VISIBLE = 1; +} + +@:struct class DescriptorHeapDesc { + public var type : DescriptorHeapType; + public var numDescriptors : Int; + public var flags : DescriptorHeapFlags; + public var nodeMask : Int; + public function new() { + } +} + +abstract Address(hl.I64) { + + public var value(get,never) : Int64; + + public inline function new( v : Int64 ) { + this = hl.I64.ofInt64(v); + } + + inline function get_value() return hl.I64.toInt64(this); + + public inline function offset( delta : Int ) : Address { + return cast ((this : Float) + delta); + } + + @:from static inline function fromInt64( v : Int64 ) : Address { + return new Address(v); + } +} + +@:hlNative("dx12","compiler_") +abstract ShaderCompiler(hlemu.Abstract<"dx_compiler">) { + public function new() { + this = cast create(); + } + public function compile( source : String, profile : String, args : Array ) : haxe.io.Bytes { + var outLen = new hl.Ref(0); + var nargs = new hl.NativeArray(args.length); + for( i in 0...args.length ) + nargs[i] = hl.Bytes.ofUcs2(args[i]); + var src = hl.Bytes.ofUcs2(source), prof = hl.Bytes.ofUcs2(profile); + function free() { + src.free(); + prof.free(); + for( b in nargs ) b.free(); + } + /* + Compiling source can trigger a validation error if DXCOMPILER.DLL is missing + */ + var bytes = try do_compile(cast this, src, prof, nargs, outLen) catch( e : Dynamic ) { free(); throw e; } + free(); + var out = bytes.toBytes(outLen.get()); + bytes.free(); + return out; + } + @:hlNative("dx12","compiler_compile") + static function do_compile( comp : ShaderCompiler, source : hl.Bytes, profile : hl.Bytes, args : hl.NativeArray, out : hl.Ref ) : hl.Bytes { + return null; + } + static function create() : ShaderCompiler { return null; } +} + +@:hlNative("dx12","descriptor_heap_") +abstract DescriptorHeap(Resource) to Resource { + public function new(desc) { + this = create(desc); + } + + @:hlNative("dx12","descriptor_heap_get_handle") + public function getHandle( gpu : Bool ) : Address { return cast null; } + static function create( desc : DescriptorHeapDesc ) : Resource { return null; } +} + +enum abstract ResourceBarrierType(Int) { + var TRANSITION = 0; + var ALIASING = 1; + var UAV = 2; +} + +enum abstract ResourceBarrierFlags(Int) { + var NONE = 0; + var BEGIN_ONLY = 1; + var END_ONLY = 2; +} + +enum abstract ResourceState(Int) { + public var COMMON = 0; + public var VERTEX_AND_CONSTANT_BUFFER = 0x1; + public var INDEX_BUFFER = 0x2; + public var RENDER_TARGET = 0x4; + public var UNORDERED_ACCESS = 0x8; + public var DEPTH_WRITE = 0x10; + public var DEPTH_READ = 0x20; + public var NON_PIXEL_SHADER_RESOURCE = 0x40; + public var PIXEL_SHADER_RESOURCE = 0x80; + public var STREAM_OUT = 0x100; + public var INDIRECT_ARGUMENT = 0x200; + public var COPY_DEST = 0x400; + public var COPY_SOURCE = 0x800; + public var RESOLVE_DESC = 0x1000; + public var RESOLVE_SOURCE = 0x2000; + public var RAYTRACING_ACCELERATION_STRUCTURE = 0x400000; + public var SHADING_RATE_SOURCE = 0x1000000; + public var GENERIC_READ = 0x1 | 0x2 | 0x40 | 0x80 | 0x200 | 0x800; + public var ALL_SHADER_RESOURCE = 0x40 | 0x80; + public var PRESENT = #if ( console && !xbogdk ) 0x100000 #else 0 #end; + public var PREDICATION = 0x200; + public var VIDE_DECODE_READ = 0x10000; + public var VIDE_DECODE_WRITE = 0x20000; + public var VIDE_PROCESS_READ = 0x40000; + public var VIDE_PROCESS_WRITE = 0x80000; + public var VIDE_ENCODE_READ = 0x200000; + public var VIDE_ENCODE_WRITE = 0x800000; + @:op(a|b) function or(r:ResourceState):ResourceState; + @:op(a&b) function and(r:ResourceState):ResourceState; +} + +@:struct class Color { + public var r : Single; + public var g : Single; + public var b : Single; + public var a : Single; + public function new() { + } +} + +typedef ClearColor = Color; + +@:struct class ResourceBarrier { + var type : ResourceBarrierType; + public var flags : ResourceBarrierFlags; + public var resource : Resource; + public var subResource : Int; + public var stateBefore : ResourceState; + public var stateAfter : ResourceState; + public function new() { type = TRANSITION; } +} + +enum abstract DsvDimension(Int) { + var UNKNOWN = 0; + var TEXTURE1D = 1; + var TEXTURE1DARRAY = 2; + var TEXTURE2D = 3; + var TEXTURE2DARRAY = 4; + var TEXTURE2DMS = 5; + var TEXTURE2DMSARRAY = 6; +} + +enum abstract RtvDimension(Int) { + var UNKNOWN = 0; + var BUFFER = 1; + var TEXTURE1D = 2; + var TEXTURE1DARRAY = 3; + var TEXTURE2D = 4; + var TEXTURE2DARRAY = 5; + var TEXTURE2DMS = 6; + var TEXTURE2DMSARRAY = 7; + var TEXTURE3D = 8; +} + +@:struct class RenderTargetViewDesc { + public var format : DxgiFormat; + public var viewDimension : RtvDimension; + + var int0 : Int; + var int1 : Int; + var int2 : Int; + var int3 : Int; + + public var mipSlice(get,set) : Int; + + public var firstElement(get,set) : Int; + public var numElements(get,set) : Int; + + public var firstArraySlice(get,set) : Int; + public var arraySize(get,set) : Int; + public var planeSlice(get,set) : Int; + + inline function get_firstElement() return int0; + inline function set_firstElement(v) return int0 = v; + inline function get_numElements() return int1; + inline function set_numElements(v) return int1 = v; + inline function get_mipSlice() return int0; + inline function set_mipSlice(v) return int0 = v; + + inline function get_firstArraySlice() return switch( viewDimension ) { case TEXTURE2DMSARRAY: int0; default: int1; }; + inline function set_firstArraySlice(v) return switch( viewDimension ) { case TEXTURE2DMSARRAY: int0 = v; default: int1 = v; }; + + inline function get_arraySize() return switch( viewDimension ) { case TEXTURE2DMSARRAY: int1; default: int2; }; + inline function set_arraySize(v) return switch( viewDimension ) { case TEXTURE2DMSARRAY: int1 = v; default: int2 = v; }; + + inline function get_planeSlice() return switch( viewDimension ) { case TEXTURE2D: int1; default: int3; }; + inline function set_planeSlice(v) return switch( viewDimension ) { case TEXTURE2D: int1 = v; default: int3 = v; }; + + function new() { + } +} + + +enum DsvFlags { + READ_ONLY_DEPTH; + READ_ONLY_STENCIL; +} + +@:struct class DepthStencilViewDesc { + public var format : DxgiFormat; + public var viewDimension : DsvDimension; + public var flags : haxe.EnumFlags; + public var mipSlice : Int; + public var firstArraySlice : Int; + public var arraySize : Int; + public function new() { + } +} + +@:struct class ConstantBufferViewDesc { + public var bufferLocation : Address; + public var sizeInBytes : Int; + public function new() { + } +} + +enum RootSignatureFlag { + ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT; + DENY_VERTEX_SHADER_ROOT_ACCESS; + DENY_HULL_SHADER_ROOT_ACCESS; + DENY_DOMAIN_SHADER_ROOT_ACCESS; + DENY_GEOMETRY_SHADER_ROOT_ACCESS; + DENY_PIXEL_SHADER_ROOT_ACCESS; + ALLOW_STREAM_OUTPUT; + LOCAL_ROOT_SIGNATURE; + DENY_AMPLIFICATION_SHADER_ROOT_ACCESS; + DENY_MESH_SHADER_ROOT_ACCESS; + CBV_SRV_UAV_HEAP_DIRECTLY_INDEXED; + SAMPLER_HEAP_DIRECTLY_INDEXED; +} + +enum abstract RootParameterType(Int) { + var DESCRIPTOR_TABLE = 0; + var CONSTANTS = 1; + var CBV = 2; + var SRV = 3; + var UAV = 4; +} + +enum abstract DescriptorRangeType(Int) { + var SRV = 0; + var UAV = 1; + var CBV = 2; + var SAMPLER = 3; +} + +@:struct class RootParameter { + public var parameterType : RootParameterType; + var __paddingBeforeUnion : Int; +} + +@:struct class DescriptorRange { + public var rangeType : DescriptorRangeType; + public var numDescriptors : Int; + public var baseShaderRegister : Int; + public var registerSpace : Int; + public var offsetInDescriptorsFromTableStart : Int; + public function new() { + } +} + +@:struct class RootParameterDescriptorTable extends RootParameter { + public var numDescriptorRanges : Int; + public var __padding : Int; + public var descriptorRanges : hl.CArray; + public var shaderVisibility : ShaderVisibility; + public function new() { + parameterType = DESCRIPTOR_TABLE; + } +} + +@:struct class RootParameterConstants extends RootParameter { + public var shaderRegister : Int; + public var registerSpace : Int; + public var num32BitValues : Int; + var __unused : Int; + public var shaderVisibility : ShaderVisibility; + public function new() { + parameterType = CONSTANTS; + } +} + +@:struct class RootParameterDescriptor extends RootParameter { + public var shaderRegister : Int; + public var registerSpace : Int; + var __unused : Int; + var __unused2 : Int; + public var shaderVisibility : ShaderVisibility; + public function new(t) { + parameterType = t; + } +} + +enum abstract AddressMode(Int) { + var WRAP = 1; + var MIRROR = 2; + var CLAMP = 3; + var BORDER = 4; + var ONCE = 5; +} + +enum abstract Filter(Int) { + var MIN_MAG_MIP_POINT = 0; + var MIN_MAG_POINT_MIP_LINEAR = 0x1; + var MIN_POINT_MAG_LINEAR_MIP_POINT = 0x4; + var MIN_POINT_MAG_MIP_LINEAR = 0x5; + var MIN_LINEAR_MAG_MIP_POINT = 0x10; + var MIN_LINEAR_MAG_POINT_MIP_LINEAR = 0x11; + var MIN_MAG_LINEAR_MIP_POINT = 0x14; + var MIN_MAG_MIP_LINEAR = 0x15; + var ANISOTROPIC = 0x55; + var COMPARISON_MIN_MAG_MIP_POINT = 0x80; + var COMPARISON_MIN_MAG_POINT_MIP_LINEAR = 0x81; + var COMPARISON_MIN_POINT_MAG_LINEAR_MIP_POINT = 0x84; + var COMPARISON_MIN_POINT_MAG_MIP_LINEAR = 0x85; + var COMPARISON_MIN_LINEAR_MAG_MIP_POINT = 0x90; + var COMPARISON_MIN_LINEAR_MAG_POINT_MIP_LINEAR = 0x91; + var COMPARISON_MIN_MAG_LINEAR_MIP_POINT = 0x94; + var COMPARISON_MIN_MAG_MIP_LINEAR = 0x95; + var COMPARISON_ANISOTROPIC = 0xd5; + var MINIMUM_MIN_MAG_MIP_POINT = 0x100; + var MINIMUM_MIN_MAG_POINT_MIP_LINEAR = 0x101; + var MINIMUM_MIN_POINT_MAG_LINEAR_MIP_POINT = 0x104; + var MINIMUM_MIN_POINT_MAG_MIP_LINEAR = 0x105; + var MINIMUM_MIN_LINEAR_MAG_MIP_POINT = 0x110; + var MINIMUM_MIN_LINEAR_MAG_POINT_MIP_LINEAR = 0x111; + var MINIMUM_MIN_MAG_LINEAR_MIP_POINT = 0x114; + var MINIMUM_MIN_MAG_MIP_LINEAR = 0x115; + var MINIMUM_ANISOTROPIC = 0x155; + var MAXIMUM_MIN_MAG_MIP_POINT = 0x180; + var MAXIMUM_MIN_MAG_POINT_MIP_LINEAR = 0x181; + var MAXIMUM_MIN_POINT_MAG_LINEAR_MIP_POINT = 0x184; + var MAXIMUM_MIN_POINT_MAG_MIP_LINEAR = 0x185; + var MAXIMUM_MIN_LINEAR_MAG_MIP_POINT = 0x190; + var MAXIMUM_MIN_LINEAR_MAG_POINT_MIP_LINEAR = 0x191; + var MAXIMUM_MIN_MAG_LINEAR_MIP_POINT = 0x194; + var MAXIMUM_MIN_MAG_MIP_LINEAR = 0x195; + var MAXIMUM_ANISOTROPIC = 0x1d5; +} + +enum abstract ComparisonFunc(Int) { + var NEVER = 1; + var LESS = 2; + var EQUAL = 3; + var LESS_EQUAL = 4; + var GREATER = 5; + var NOT_EQUAL = 6; + var GREATER_EQUAL = 7; + var ALWAYS = 8; +} + +enum abstract StaticBorderColor(Int) { + var TRANSPARENT_BLACK = 0; + var OPAQUE_BLACK = 1; + var OPAQUE_WHITE = 2; +} + +enum abstract ShaderVisibility(Int) { + var ALL = 0; + var VERTEX = 1; + var HULL = 2; + var DOMAIN = 3; + var GEOMETRY = 4; + var PIXEL = 5; + var AMPLIFICATION = 6; + var MESH = 7; +} + +@:struct class StaticSamplerDesc { + public var filter : Filter; + public var addressU : AddressMode; + public var addressV : AddressMode; + public var addressW : AddressMode; + public var mipLODBias : Single; + public var maxAnisotropy : Int; + public var comparisonFunc : ComparisonFunc; + public var borderColor : StaticBorderColor; + public var minLOD : Single; + public var maxLOD : Single; + public var shaderRegister : Int; + public var registerSpace : Int; + public var shaderVisibility : ShaderVisibility; + public function new() { + } +} + +@:struct class RootSignatureDesc { + public var numParameters : Int; + public var parameters : hl.CArray; + public var numStaticSamplers : Int; + public var staticSamplers : hl.CArray; + public var flags : haxe.EnumFlags; + public function new() { + } +} + +@:hlNative("dx12","rootsignature_") +abstract RootSignature(Resource) { + public function new( bytes : hl.Bytes, len : Int ) { + this = create(bytes,len); + } + static function create(bytes:hl.Bytes,len:Int) : Resource { return null; } +} + +abstract GraphicsPipelineState(Resource) { + @:to inline function toPS():PipelineState { + return cast this; + } +} + +abstract ComputePipelineState(Resource) { + @:to inline function toPS():PipelineState { + return cast this; + } +} + +abstract Bool32(Int) { + @:to inline function toBool() return this != 1; + @:from static inline function fromBool(b:Bool) : Bool32 { return cast b?1:0; }; +} + +enum abstract DxgiFormat(Int) { + var UNKNOWN = 0; + var R32G32B32A32_TYPELESS = 1; + var R32G32B32A32_FLOAT = 2; + var R32G32B32A32_UINT = 3; + var R32G32B32A32_SINT = 4; + var R32G32B32_TYPELESS = 5; + var R32G32B32_FLOAT = 6; + var R32G32B32_UINT = 7; + var R32G32B32_SINT = 8; + var R16G16B16A16_TYPELESS = 9; + var R16G16B16A16_FLOAT = 10; + var R16G16B16A16_UNORM = 11; + var R16G16B16A16_UINT = 12; + var R16G16B16A16_SNORM = 13; + var R16G16B16A16_SINT = 14; + var R32G32_TYPELESS = 15; + var R32G32_FLOAT = 16; + var R32G32_UINT = 17; + var R32G32_SINT = 18; + var R32G8X24_TYPELESS = 19; + var D32_FLOAT_S8X24_UINT = 20; + var R32_FLOAT_X8X24_TYPELESS = 21; + var X32_TYPELESS_G8X24_UINT = 22; + var R10G10B10A2_TYPELESS = 23; + var R10G10B10A2_UNORM = 24; + var R10G10B10A2_UINT = 25; + var R11G11B10_FLOAT = 26; + var R8G8B8A8_TYPELESS = 27; + var R8G8B8A8_UNORM = 28; + var R8G8B8A8_UNORM_SRGB = 29; + var R8G8B8A8_UINT = 30; + var R8G8B8A8_SNORM = 31; + var R8G8B8A8_SINT = 32; + var R16G16_TYPELESS = 33; + var R16G16_FLOAT = 34; + var R16G16_UNORM = 35; + var R16G16_UINT = 36; + var R16G16_SNORM = 37; + var R16G16_SINT = 38; + var R32_TYPELESS = 39; + var D32_FLOAT = 40; + var R32_FLOAT = 41; + var R32_UINT = 42; + var R32_SINT = 43; + var R24G8_TYPELESS = 44; + var D24_UNORM_S8_UINT = 45; + var R24_UNORM_X8_TYPELESS = 46; + var X24_TYPELESS_G8_UINT = 47; + var R8G8_TYPELESS = 48; + var R8G8_UNORM = 49; + var R8G8_UINT = 50; + var R8G8_SNORM = 51; + var R8G8_SINT = 52; + var R16_TYPELESS = 53; + var R16_FLOAT = 54; + var D16_UNORM = 55; + var R16_UNORM = 56; + var R16_UINT = 57; + var R16_SNORM = 58; + var R16_SINT = 59; + var R8_TYPELESS = 60; + var R8_UNORM = 61; + var R8_UINT = 62; + var R8_SNORM = 63; + var R8_SINT = 64; + var A8_UNORM = 65; + var R1_UNORM = 66; + var R9G9B9E5_SHAREDEXP = 67; + var R8G8_B8G8_UNORM = 68; + var G8R8_G8B8_UNORM = 69; + var BC1_TYPELESS = 70; + var BC1_UNORM = 71; + var BC1_UNORM_SRGB = 72; + var BC2_TYPELESS = 73; + var BC2_UNORM = 74; + var BC2_UNORM_SRGB = 75; + var BC3_TYPELESS = 76; + var BC3_UNORM = 77; + var BC3_UNORM_SRGB = 78; + var BC4_TYPELESS = 79; + var BC4_UNORM = 80; + var BC4_SNORM = 81; + var BC5_TYPELESS = 82; + var BC5_UNORM = 83; + var BC5_SNORM = 84; + var B5G6R5_UNORM = 85; + var B5G5R5A1_UNORM = 86; + var B8G8R8A8_UNORM = 87; + var B8G8R8X8_UNORM = 88; + var R10G10B10_XR_BIAS_A2_UNORM = 89; + var B8G8R8A8_TYPELESS = 90; + var B8G8R8A8_UNORM_SRGB = 91; + var B8G8R8X8_TYPELESS = 92; + var B8G8R8X8_UNORM_SRGB = 93; + var BC6H_TYPELESS = 94; + var BC6H_UF16 = 95; + var BC6H_SF16 = 96; + var BC7_TYPELESS = 97; + var BC7_UNORM = 98; + var BC7_UNORM_SRGB = 99; + var AYUV = 100; + var Y410 = 101; + var Y416 = 102; + var NV12 = 103; + var P010 = 104; + var P016 = 105; + var _420_OPAQUE = 106; + var YUY2 = 107; + var Y210 = 108; + var Y216 = 109; + var NV11 = 110; + var AI44 = 111; + var IA44 = 112; + var P8 = 113; + var A8P8 = 114; + var B4G4R4A4_UNORM = 115; + var P208 = 130; + var V208 = 131; + var V408 = 132; + public inline function toInt() return this; +} + +@:struct class ShaderBytecode { + public var shaderBytecode : hl.Bytes; + public var bytecodeLength : hl.I64; +} + +@:struct class SoDeclarationEntry { + public var stream : Int; + public var semanticName : hl.Bytes; + public var semanticIndex : Int; + public var startComponent : hl.UI8; + public var componentCount : hl.UI8; + public var outputSlot : hl.UI8; + public function new() { + } +} + +@:struct class StreamOutputDesc { + public var soDeclaration : hl.CArray; + public var numEntries : Int; + public var bufferStrides : hl.Bytes; + public var numStrides : Int; + public var rasterizedStream : Int; +} + +abstract GraphicsRTVFormats(GraphicsPipelineStateDesc) { + public inline function new(g) { + this = g; + } + @:arrayAccess inline function get( index : Int ) { + return @:privateAccess switch( index ) { + case 0: this.rtvFormat0; + case 1: this.rtvFormat1; + case 2: this.rtvFormat2; + case 3: this.rtvFormat3; + case 4: this.rtvFormat4; + case 5: this.rtvFormat5; + case 6: this.rtvFormat6; + case 7: this.rtvFormat7; + default: throw "assert"; + } + } + @:arrayAccess inline function set( index : Int, v : DxgiFormat ) { + @:privateAccess switch( index ) { + case 0: this.rtvFormat0 = v; + case 1: this.rtvFormat1 = v; + case 2: this.rtvFormat2 = v; + case 3: this.rtvFormat3 = v; + case 4: this.rtvFormat4 = v; + case 5: this.rtvFormat5 = v; + case 6: this.rtvFormat6 = v; + case 7: this.rtvFormat7 = v; + default: throw "assert"; + } + } +} + +abstract BlendDescRenderTargets(BlendDesc) { + public inline function new(d) { + this = d; + } + @:arrayAccess inline function get( index : Int ) { + return @:privateAccess switch( index ) { + case 0: this.renderTarget0; + case 1: this.renderTarget1; + case 2: this.renderTarget2; + case 3: this.renderTarget3; + case 4: this.renderTarget4; + case 5: this.renderTarget5; + case 6: this.renderTarget6; + case 7: this.renderTarget7; + default: throw "assert"; + } + } +} + +enum abstract Blend(Int) { + var ZERO = 1; + var ONE = 2; + var SRC_COLOR = 3; + var INV_SRC_COLOR = 4; + var SRC_ALPHA = 5; + var INV_SRC_ALPHA = 6; + var DEST_ALPHA = 7; + var INV_DEST_ALPHA = 8; + var DEST_COLOR = 9; + var INV_DEST_COLOR = 10; + var SRC_ALPHA_SAT = 11; + var BLEND_FACTOR = 14; + var INV_BLEND_FACTOR = 15; + var SRC1_COLOR = 16; + var INV_SRC1_COLOR = 17; + var SRC1_ALPHA = 18; + var INV_SRC1_ALPHA = 19; +} + +enum abstract BlendOp(Int) { + var ADD = 1; + var SUBTRACT = 2; + var REV_SUBTRACT = 3; + var MIN = 4; + var MAX = 5; +} + +enum abstract LogicOp(Int) { + var CLEAR; + var SET; + var COPY; + var COPY_INVERTED; + var NOOP; + var INVERT; + var AND; + var NAND; + var OR; + var NOR; + var XOR; + var EQUIV; + var AND_REVERSE; + var AND_INVERTED; + var OR_REVERSE; + var OR_INVERTED; +} + +@:struct class RenderTargetBlendDesc { + public var blendEnable : Bool32; + public var logicOpEnable : Bool32; + public var srcBlend : Blend; + public var dstBlend : Blend; + public var blendOp : BlendOp; + public var srcBlendAlpha : Blend; + public var dstBlendAlpha : Blend; + public var blendOpAlpha : BlendOp; + public var logicOp : LogicOp; + public var renderTargetWriteMask : hl.UI8; + public function new() { + } +} + +@:struct class BlendDesc { + public var alphaToCoverage : Bool32; + public var independentBlendEnable : Bool32; + public var renderTargets(get,never) : BlendDescRenderTargets; + inline function get_renderTargets() return new BlendDescRenderTargets(this); + @:packed var renderTarget0 : RenderTargetBlendDesc; + @:packed var renderTarget1 : RenderTargetBlendDesc; + @:packed var renderTarget2 : RenderTargetBlendDesc; + @:packed var renderTarget3 : RenderTargetBlendDesc; + @:packed var renderTarget4 : RenderTargetBlendDesc; + @:packed var renderTarget5 : RenderTargetBlendDesc; + @:packed var renderTarget6 : RenderTargetBlendDesc; + @:packed var renderTarget7 : RenderTargetBlendDesc; + public function new() { + } +} + +enum abstract FillMode(Int) { + var WIREFRAME = 2; + var SOLID = 3; +} + +enum abstract CullMode(Int) { + var NONE = 1; + var FRONT = 2; + var BACK = 3; +} + +enum abstract ConservativeRasterMode(Int) { + var OFF = 0; + var ON = 1; +} + +@:struct class RasterizerDesc { + public var fillMode : FillMode; + public var cullMode : CullMode; + public var frontCounterClockwise : Bool32; + public var depthBias : Int; + public var depthBiasClamp : Single; + public var slopeScaledDepthBias : Single; + public var depthClipEnable : Bool32; + public var multisampleEnable : Bool32; + public var antialiasedLineEnable : Bool32; + public var forcedSampleCount : Int; + public var conservativeRaster : ConservativeRasterMode; + public function new() { + } +} + +enum abstract DepthWriteMask(Int) { + var ZERO = 0; + var ALL = 1; +} + +enum abstract StencilOp(Int) { + var KEEP = 1; + var ZERO = 2; + var REPLACE = 3; + var INCR_SAT = 4; + var DECR_SAT = 5; + var INVERT = 6; + var INCR = 7; + var DECR = 8; +} + +@:struct class DepthStencilOpDesc { + public var stencilFailOp : StencilOp; + public var stencilDepthFailOp : StencilOp; + public var stencilPassOp : StencilOp; + public var stencilFunc : ComparisonFunc; + public function new() { + } +} + +@:struct class DepthStencilDesc { + public var depthEnable : Bool32; + public var depthWriteMask : DepthWriteMask; + public var depthFunc : ComparisonFunc; + public var stencilEnable : Bool32; + public var stencilReadMask : hl.UI8; + public var stencilWriteMask : hl.UI8; + @:packed public var frontFace(default,null) : DepthStencilOpDesc; + @:packed public var backFace(default,null) : DepthStencilOpDesc; + public function new() { + } +} + +enum abstract InputClassification(Int) { + var PER_VERTEX_DATA = 0; + var PER_INSTANCE_DATA = 1; +} + +@:struct class InputElementDesc { + public var semanticName : hl.Bytes; + public var semanticIndex : Int; + public var format : DxgiFormat; + public var inputSlot : Int; + public var alignedByteOffset : Int; + public var inputSlotClass : InputClassification; + public var instanceDataStepRate : Int; + public function new() { + } +} + +@:struct class InputLayoutDesc { + public var inputElementDescs : hl.CArray; + public var numElements : Int; + public var __padding : Int; // largest element + public function new() { + } +} + +enum abstract IndexBufferStripCutValue(Int) { + var DISABLED = 0; + var _0xFFFF = 1; + var _0xFFFFFFFF = 2; +} + +enum abstract PrimitiveTopologyType(Int) { + var UNDEFINED = 0; + var POINT = 1; + var LINE = 2; + var TRIANGLE = 3; + var PATCH = 4; +} + +@:struct class DxgiSampleDesc { + public var count : Int; + public var quality : Int; + public function new() { + } +} + +@:struct class CachedPipelineState { + public var cachedBlob : hl.Bytes; + public var cachedBlobSizeInBytes : hl.I64; + public function new() { + } +} + +enum abstract PipelineStateFlags(Int) { + var NONE = 0; + var TOOL_DEBUG = 1; +} + +@:struct class GraphicsPipelineStateDesc { + public var rootSignature : RootSignature; + @:packed public var vs(default,null) : ShaderBytecode; + @:packed public var ps(default,null) : ShaderBytecode; + @:packed public var ds(default,null) : ShaderBytecode; + @:packed public var hs(default,null) : ShaderBytecode; + @:packed public var gs(default,null) : ShaderBytecode; + @:packed public var streamOutput(default,null) : StreamOutputDesc; + @:packed public var blendState(default,null) : BlendDesc; + public var sampleMask : Int; + @:packed public var rasterizerState(default,null) : RasterizerDesc; + @:packed public var depthStencilDesc(default,null) : DepthStencilDesc; + @:packed public var inputLayout(default,null) : InputLayoutDesc; + public var ibStripCutValue : IndexBufferStripCutValue; + public var primitiveTopologyType : PrimitiveTopologyType; + public var numRenderTargets : Int; + public var rtvFormats(get,never) : GraphicsRTVFormats; + inline function get_rtvFormats() return new GraphicsRTVFormats(this); + var rtvFormat0 : DxgiFormat; + var rtvFormat1 : DxgiFormat; + var rtvFormat2 : DxgiFormat; + var rtvFormat3 : DxgiFormat; + var rtvFormat4 : DxgiFormat; + var rtvFormat5 : DxgiFormat; + var rtvFormat6 : DxgiFormat; + var rtvFormat7 : DxgiFormat; + public var dsvFormat : DxgiFormat; + @:packed public var sampleDesc(default,null) : DxgiSampleDesc; + public var nodeMask : Int; + @:packed public var cachedPSO(default,null) : CachedPipelineState; + public var flags : PipelineStateFlags; + public function new() { + } +} + +@:struct class ComputePipelineStateDesc { + public var rootSignature : RootSignature; + @:packed public var cs(default,null) : ShaderBytecode; + public var nodeMask : Int; + @:packed public var cachedPSO(default,null) : CachedPipelineState; + public var flags : PipelineStateFlags; + public function new() { + } +} + +enum abstract HeapType(Int) { + var DEFAULT = 1; + var UPLOAD = 2; + var READBACK = 3; + var CUSTOM = 4; +} + +enum abstract CpuPageProperty(Int) { + var UNKNOWN = 0; + var NOT_AVAILABLE = 1; + var WRITE_COMBINE = 2; + var WRITE_BACK = 3; +} + +enum abstract MemoryPool(Int) { + var UNKNOWN = 0; + var L0 = 1; + var L1 = 2; +} + +@:struct class HeapProperties { + public var type : HeapType; + public var cpuPageProperty : CpuPageProperty; + public var memoryPoolReference : MemoryPool; + public var creationNodeMask : Int; + public var visibleNodeMask : Int; + public function new() { + } +} + +enum HeapFlag { + SHARED; + __UNUSED; + DENY_BUFFERS; + ALLOW_DISPLAY; + __UNUSED2; + SHARED_CROSS_ADAPTER; + DENY_RT_DS_TEXTURES; + DENY_NON_RT_DS_TEXTURES; + HARDWARE_PROTECTED; + ALLOW_WRITE_WATCH; + ALLOW_SHADER_ATOMICS; + CREATE_NOT_RESIDENT; + CREATE_NOT_ZEROED; +} + +enum abstract ResourceDimension(Int) { + var UNKNOWN = 0; + var BUFFER = 1; + var TEXTURE1D = 2; + var TEXTURE2D = 3; + var TEXTURE3D = 4; +} + +enum abstract TextureLayout(Int) { + var UNKNOWN = 0; + var ROW_MAJOR = 1; + var _64KB_UNDEFINED_SWIZZLE = 2; + var _64KB_STANDARD_SWIZZLE = 3; +} + +enum ResourceFlag { + ALLOW_RENDER_TARGET; + ALLOW_DEPTH_STENCIL; + ALLOW_UNORDERED_ACCESS; + DENY_SHADER_RESOURCE; + ALLOW_CROSS_ADAPTER; + ALLOW_SIMULTANEOUS_ACCESS; + VIDEO_DECODE_REFERENCE_ONLY; + VIDEO_ENCODE_REFERENCE_ONLY; +} + +@:struct class ResourceDesc { + public var dimension : ResourceDimension; + public var alignment : Int64; + public var width : Int64; + public var height : Int; + public var depthOrArraySize : hl.UI16; + public var mipLevels : hl.UI16; + public var format : DxgiFormat; + @:packed public var sampleDesc(default,null) : DxgiSampleDesc; + public var layout : TextureLayout; + public var flags : haxe.EnumFlags; + public function new() { + } +} + +@:struct class ClearValue { + public var format : DxgiFormat; + @:packed public var color(default,never) : Color; + public var depth(get,set) : Float; + public var stencil(get,set) : Int; + inline function get_depth() return color.r; + inline function set_depth(v) return color.r = v; + function get_stencil() return haxe.io.FPHelper.floatToI32(color.g); + function set_stencil(v) { + color.g = haxe.io.FPHelper.i32ToFloat(v); + return v; + } +} + +enum abstract SrvDimension(Int) { + var UNKNOWN = 0; + var BUFFER = 1; + var TEXTURE1D = 2; + var TEXTURE1DARRAY = 3; + var TEXTURE2D = 4; + var TEXTURE2DARRAY = 5; + var TEXTURE2DMS = 6; + var TEXTURE2DMSARRAY = 7; + var TEXTURE3D = 8; + var TEXTURECUBE = 9; + var TEXTURECUBEARRAY = 10; + var RAYTRACING_ACCELERATION_STRUCTURE = 11; +} + +enum abstract ShaderComponentValue(Int) { + var R = 0; + var G = 1; + var B = 2; + var A = 3; + var ZERO = 4; + var ONE = 5; +} + +abstract ShaderComponentMapping(Int) { + public var red(get,set) : ShaderComponentValue; + public var green(get,set) : ShaderComponentValue; + public var blue(get,set) : ShaderComponentValue; + public var alpha(get,set) : ShaderComponentValue; + public function new() { + this = 1 << 12; + } + inline function get_red() : ShaderComponentValue { return cast (this & 7); } + inline function get_green() : ShaderComponentValue { return cast ((this >> 3) & 7); } + inline function get_blue() : ShaderComponentValue { return cast ((this >> 6) & 7); } + inline function get_alpha() : ShaderComponentValue { return cast ((this >> 9) & 7); } + inline function set_red(v : ShaderComponentValue) { this = (this & ~(3<<0)) | ((cast v : Int) << 0); return v; } + inline function set_green(v : ShaderComponentValue) { this = (this & ~(3<<3)) | ((cast v : Int) << 3); return v; } + inline function set_blue(v : ShaderComponentValue) { this = (this & ~(3<<6)) | ((cast v : Int) << 6); return v; } + inline function set_alpha(v : ShaderComponentValue) { this = (this & ~(3<<9)) | ((cast v : Int) << 9); return v; } + + public static inline var DEFAULT : ShaderComponentMapping = cast 0x1688; +} + +@:struct class ShaderResourceViewDesc { + public var format : DxgiFormat; + public var dimension : SrvDimension; + public var shader4ComponentMapping : ShaderComponentMapping; + var __unionPadding : Int; +} + +enum abstract BufferSRVFlags(Int) { + var NONE = 0; + var RAW = 1; +} + +@:struct class BufferSRV extends ShaderResourceViewDesc { + public var firstElement : Int64; + public var numElements : Int; + public var structureByteStride : Int; + public var flags : BufferSRVFlags; + var unused : Int; + public function new() { + dimension = BUFFER; + } +} + +@:struct class Tex1DSRV extends ShaderResourceViewDesc { + public var mostDetailedMip : Int; + public var mipLevels : Int; + public var resourceMinLODClamp : Single; + var unused1 : Int; + var unused2 : Int; + var unused3 : Int; + public function new() { + dimension = TEXTURE1D; + } +} + +@:struct class Tex1DArraySRV extends ShaderResourceViewDesc { + public var mostDetailedMip : Int; + public var mipLevels : Int; + public var firstArraySlice : Int; + public var arraySize : Int; + public var resourceMinLODClamp : Single; + var unused1 : Int; + public function new() { + dimension = TEXTURE1DARRAY; + } +} + +@:struct class Tex2DSRV extends ShaderResourceViewDesc { + public var mostDetailedMip : Int; + public var mipLevels : Int; + public var planeSlice : Int; + public var resourceMinLODClamp : Single; + var unused1 : Int; + var unused2 : Int; + public function new() { + dimension = TEXTURE2D; + } +} + +@:struct class Tex2DArraySRV extends ShaderResourceViewDesc { + public var mostDetailedMip : Int; + public var mipLevels : Int; + public var firstArraySlice : Int; + public var arraySize : Int; + public var planeSlice : Int; + public var resourceMinLODClamp : Single; + public function new() { + dimension = TEXTURE2DARRAY; + } +} + +@:struct class Tex3DSRV extends ShaderResourceViewDesc { + public var mostDetailedMip : Int; + public var mipLevels : Int; + public var resourceMinLODClamp : Single; + var unused1 : Int; + var unused2 : Int; + var unused3 : Int; + public function new() { + dimension = TEXTURE3D; + } +} + +@:struct class TexCubeSRV extends ShaderResourceViewDesc { + public var mostDetailedMip : Int; + public var mipLevels : Int; + public var resourceMinLODClamp : Single; + var unused1 : Int; + var unused2 : Int; + var unused3 : Int; + public function new() { + dimension = TEXTURECUBE; + } +} + +@:struct class TexCubeArraySRV extends ShaderResourceViewDesc { + public var mostDetailedMip : Int; + public var mipLevels : Int; + public var first2DArrayFace : Int; + public var numCubes : Int; + public var resourceMinLODClamp : Single; + var unused1 : Int; + public function new() { + dimension = TEXTURECUBEARRAY; + } +} + +@:struct class SamplerDesc { + public var filter : Filter; + public var addressU : AddressMode; + public var addressV : AddressMode; + public var addressW : AddressMode; + public var mipLODBias : Single; + public var maxAnisotropy : Int; + public var comparisonFunc : ComparisonFunc; + @:packed public var borderColor(default,never) : Color; + public var minLod : Single; + public var maxLod : Single; + public function new() { + } +} + +@:struct class SubResourceData { + public var data : hl.Bytes; + public var rowPitch : Int64; + public var slicePitch : Int64; + public function new() { + } +} + +enum abstract IndirectArgumentType(Int) { + var DRAW = 0; + var DRAW_INDEXED = 1; + var DISPATCH = 2; + var VERTEX_BUFFER_VIEW = 3; + var INDEX_BUFFER_VIEW = 4; + var CONSTANT = 5; + var CONSTANT_BUFFER_VIEW = 6; + var SHADER_RESOURCE_VIEW = 7; + var UNORDERED_ACCESS_VIEW = 8; + var DISPATCH_RAYS = 9; + var DISPATCH_MESH = 10; +} + +@:struct class IndirectArgumentDesc { + public var type : IndirectArgumentType; + public var rootParameterIndex : Int; + public var destOffsetIn32BitValues : Int; + public var num32BitValuesToSet : Int; + public var slot(get,set) : Int; + inline function get_slot() return rootParameterIndex; + inline function set_slot(v) return rootParameterIndex = v; + public function new() { + } +} + +@:struct class CommandSignatureDesc { + public var byteStride :Int; + public var numArgumentDescs : Int; + public var argumentDescs : hl.CArray; + public var nodeMask : Int; + public function new() { + } +} + +enum abstract UAVDimension(Int) { + public var UNKNOWN = 0; + public var BUFFER = 1; + public var TEXTURE1D = 2; + public var TEXTURE1DARRAY = 3; + public var TEXTURE2D = 4; + public var TEXTURE2DARRAY = 5; + public var TEXTURE3D = 8; +} + +@:struct class UnorderedAccessViewDesc { + public var format : DxgiFormat; + public var viewDimension : UAVDimension; +} + +enum UAVBufferFlags { + RAW; +} + +@:struct class UAVBufferViewDesc extends UnorderedAccessViewDesc { + public var firstElement : hl.I64; + public var numElements : Int; + public var structureSizeInBytes : Int; + public var counterOffsetInBytes : hl.I64; + public var flags : haxe.EnumFlags; + public function new() { + viewDimension = BUFFER; + } +} + +@:struct class UAVTextureViewDesc extends UnorderedAccessViewDesc { + var int0 : Int; + var int1 : Int; + var int2 : Int; + var int3 : Int; + var padding1 : hl.I64; + var padding2 : Int; + public function new(dim) { + viewDimension = dim; + } + public var mipSlice(get,set) : Int; + public var firstArraySlice(get,set) : Int; + public var firstWSlice(get,set) : Int; + public var arraySize(get,set) : Int; + public var planeSlice(get,set) : Int; + public var wSlice(get,set) : Int; + inline function get_mipSlice() return int0; + inline function set_mipSlice(v) return int0 = v; + + inline function get_planeSlice() return switch( viewDimension ) { case TEXTURE2DARRAY: int3; default: int1; } + inline function set_planeSlice(v) return switch( viewDimension ) { case TEXTURE2DARRAY: int3 = v; default: int1 = v; } + + inline function get_firstArraySlice() return int1; + inline function set_firstArraySlice(v) return int1 = v; + inline function get_arraySize() return int2; + inline function set_arraySize(v) return int2 = v; + + inline function get_firstWSlice() return int1; + inline function set_firstWSlice(v) return int1 = v; + inline function get_wSlice() return int2; + inline function set_wSlice(v) return int2 = v; +} + +enum abstract QueryType(Int) { + var OCCLUSION = 0; + var BINARY_OCCLUSION = 1; + var TIMESTAMP = 2; + var PIPELINE_STATISTICS = 3; + var SO_STATISTICS_STREAM0 = 4; + var SO_STATISTICS_STREAM1 = 5; + var SO_STATISTICS_STREAM2 = 6; + var SO_STATISTICS_STREAM3 = 7; + var VIDEO_DECODE_STATISTICS = 8; +} + +enum abstract QueryHeapType(Int) { + var OCCLUSION = 0; + var TIMESTAMP = 1; + var PIPELINE_STATISTICS = 2; + var SO_STATISTICS = 3; + var VIDEO_DECODE_STATISTICS = 4; + var COPY_QUEUE_TIMESTAMP = 5; +} + +@:struct class QueryHeapDesc { + public var type : QueryHeapType; + public var count : Int; + public var nodeMask : Int; + public function new() { + } +} + +@:forward(release) +abstract QueryHeap(Resource) { +} + +@:forward(release, setName) +abstract Heap(Resource) { +} + +@:struct class HeapDesc { + public var sizeInBytes : Int64; + @:packed public var properties(default,null) : HeapProperties; + public var alignment : Int64; + public var flags : haxe.EnumFlags; + public function new() { + } +} + +@:struct class ResourceAllocationInfo { + public var sizeInBytes : Int64; + public var alignment : Int64; + public function new() { + } +} + +enum abstract PredicationOp(Int) { + var EQUAL_ZERO = 0; + var NOT_EQUAL_ZERO = 1; +} + +enum abstract Dx12Feature(Int) { + var D3D12_OPTIONS = 0; + var ARCHITECTURE = 1; + var FEATURE_LEVELS = 2; + var FORMAT_SUPPORT = 3; + var MULTISAMPLE_QUALITY_LEVELS = 4; + var FORMAT_INFO = 5; + var GPU_VIRTUAL_ADDRESS_SUPPORT = 6; + var SHADER_MODEL = 7; + var D3D12_OPTIONS1 = 8; + var PROTECTED_RESOURCE_SESSION_SUPPORT = 10; + var ROOT_SIGNATURE = 12; + var ARCHITECTURE1 = 16; + var D3D12_OPTIONS2 = 18; + var SHADER_CACHE = 19; + var COMMAND_QUEUE_PRIORITY = 20; + var D3D12_OPTIONS3 = 21; + var EXISTING_HEAPS = 22; + var D3D12_OPTIONS4 = 23; + var SERIALIZATION = 24; + var CROSS_NODE = 25; + var D3D12_OPTIONS5 = 27; + var DISPLAYABLE; + var D3D12_OPTIONS6 = 30; + var QUERY_META_COMMAND = 31; + var D3D12_OPTIONS7 = 32; + var PROTECTED_RESOURCE_SESSION_TYPE_COUNT = 33; + var PROTECTED_RESOURCE_SESSION_TYPES = 34; + var D3D12_OPTIONS8 = 36; + var D3D12_OPTIONS9 = 37; + var D3D12_OPTIONS10; + var D3D12_OPTIONS11; + var D3D12_OPTIONS12; + var D3D12_OPTIONS13; + var D3D12_OPTIONS14; + var D3D12_OPTIONS15; + var D3D12_OPTIONS16; + var D3D12_OPTIONS17; + var D3D12_OPTIONS18; + var D3D12_OPTIONS19; + var D3D12_OPTIONS20; + var PREDICATION; + var PLACED_RESOURCE_SUPPORT_INFO; + var HARDWARE_COPY; + var D3D12_OPTIONS21; + var APPLICATION_SPECIFIC_DRIVER_STATE; + var BYTECODE_BYPASS_HASH_SUPPORTED; + var SHADER_CACHE_ABI_SUPPORT; +} + +enum abstract ShaderModel(Int) to Int { + var SHADER_MODEL_NONE; + var SHADER_MODEL_5_1 = 0x51; + var SHADER_MODEL_6_0 = 0x60; + var SHADER_MODEL_6_1 = 0x61; + var SHADER_MODEL_6_2 = 0x62; + var SHADER_MODEL_6_3 = 0x63; + var SHADER_MODEL_6_4 = 0x64; + var SHADER_MODEL_6_5 = 0x65; + var SHADER_MODEL_6_6 = 0x66; + var SHADER_MODEL_6_7 = 0x67; + var SHADER_MODEL_6_8; + var SHADER_MODEL_6_9; + var HIGHEST_SHADER_MODEL; +} + +@:struct class QueryVideoMemoryInfo { + public var budget : Int64; + public var currentUsage : Int64; + public var availableForReservation : Int64; + public var currentReservation : Int64; + public function new() {} +} + +@:struct class InfoQueueFilterDesc { + public var numCategories : Int; + public var categoryList : hl.Bytes; + public var numSeverities : Int; + public var severityList : hl.Bytes; + public var numIDs : Int; + public var idList : hl.Bytes; + public function new() {} +} + +@:struct class InfoQueueFilter { + @:packed public var allowList(default, null) : InfoQueueFilterDesc; + @:packed public var denyList(default, null) : InfoQueueFilterDesc; + public function new() {} +} + +@:hlNative("dx12") +class Dx12 { + + public static function create( win : Window, flags : DriverInitFlags, ?deviceName : String ) { + // no name : the high performance GPU, as Chromium takes it (force_high_performance_gpu) to share the back buffers + // (an addon older than the package, a client not updated yet, has no js_high_performance_device : the default GPU) + if( deviceName == null && js.Syntax.code("typeof __dx12.js_high_performance_device == 'function'") ) { + var index : Int = js.Syntax.code("__dx12.js_high_performance_device()"); + if( index >= 0 ) deviceName = listDevices()[index]; + } + var name = hl.Bytes.ofUcs2(deviceName); + var d = dxCreate(null, flags, name); + if( name != null ) name.free(); + dx.Display.init(win); + return d; + } + + public static function getDevice() : Device { + return null; + } + + public static function getFactory() : Factory { + return null; + } + + public static function getAdapter() : Adapter { + return null; + } + + public static function setDevice(device : Device) { + } + + public static function setFactory(factory : Factory) { + } + + public static function setSwapChain(swapChain : SwapChain) { + } + + public static function flushMessages() { + } + + public static function suppressDebugMessages( filter : InfoQueueFilter ) : Void { + } + + public static function getDescriptorHandleIncrementSize( type : DescriptorHeapType ) : Int { + return 0; + } + + public static function createGraphicsPipelineState( desc : GraphicsPipelineStateDesc ) : GraphicsPipelineState { + return null; + } + + public static function createComputePipelineState( desc : ComputePipelineStateDesc ) : ComputePipelineState { + return null; + } + + public static macro function serializeRootSignature( desc, version, size ) { + return dx.Dx12Macros.serializeRootSignature(desc, version, size); + } + + @:hlNative("dx12", "serialize_root_signature") + static function serializeRootSignatureRef( desc : RootSignatureDesc, version : Int, size : hl.Ref ) : hl.Bytes { + return null; + } + + public static function getBackBuffer( index : Int ) : GpuResource { + return null; + } + + public static function getCurrentBackBufferIndex() : Int { + return 0; + } + + public static function createRenderTargetView( buffer : Resource, desc : RenderTargetViewDesc, target : Address ) { + } + + public static function createDepthStencilView( buffer : Resource, desc : DepthStencilViewDesc, target : Address ) { + } + + public static function createConstantBufferView( desc : ConstantBufferViewDesc, target : Address ) { + } + + public static function createUnorderedAccessView( res : Resource, counter : Resource, desc : UnorderedAccessViewDesc, target : Address ) { + } + + public static function createShaderResourceView( resource : Resource, desc : ShaderResourceViewDesc, target : Address ) { + } + + public static function createQueryHeap( desc : QueryHeapDesc ) : QueryHeap { + return null; + } + + public static function getCopyableFootprints( srcDesc : ResourceDesc, firstSubResource : Int, numSubResources : Int, baseOffset : Int64, layouts : PlacedSubresourceFootprint, numRows : hl.BytesAccess, rowSizeInBytes : hl.BytesAccess, totalBytes : hl.BytesAccess ) : Void { + } + + public static function createSampler( desc : SamplerDesc, target : Address ) { + } + + public static function createCommittedResource( heapProperties : HeapProperties, heapFlags : haxe.EnumFlags, desc : ResourceDesc, initialState : ResourceState, clearValue : ClearValue ) : GpuResource { + return null; + } + + public static function createHeap( desc : HeapDesc ) : Heap { + return null; + } + + public static function createPlacedResource( heap : Heap, offset : Int64, desc : ResourceDesc, initialState : ResourceState, clearValue : ClearValue ) : GpuResource { + return null; + } + + public static function getResourceAllocationInfo( desc : ResourceDesc, info : ResourceAllocationInfo ) { + } + + public static function createCommandSignature( desc : CommandSignatureDesc, root : RootSignature ) : CommandSignature { + return null; + } + + public static function resize( directQueue : CommandQueue, width : Int, height : Int, bufferCount : Int, format : DxgiFormat ) { + resizeNative(directQueue, width, height, bufferCount, format); + dx.Display.resize(width, height); + } + + @:hlNative("dx12", "resize") + static function resizeNative( directQueue : CommandQueue, width : Int, height : Int, bufferCount : Int, format : DxgiFormat ) { + } + + public static function updateSubResource( commandList : CommandList, dst : GpuResource, src : GpuResource, srcOffset : Int64, first : Int, count : Int, data : SubResourceData ) : Bool { + return false; + } + + public static function getConstant( index : Int ) : Int { + return 0; + } + + public static function copyDescriptorsSimple( numDescriptors : Int, dstCpuAddress : Address, srcCpuAddress : Address, heapType : DescriptorHeapType ) { + } + + public static function checkFeatureSupport( feature : Dx12Feature, data : hl.Bytes, dataSize : Int ) { + } + + public static function getDeviceName() { + return dxGetDeviceName().toUcs2(); + } + + public static function listDevices() { + var arr = dxListDevices(); + var out = []; + for( i in 0...arr.length ) { + if( arr[i] == null ) break; + out.push(arr[i].toUcs2()); + } + return out; + } + + @:hlNative("dx12", "list_devices") + static function dxListDevices() : hl.NativeArray { + return null; + } + + @:hlNative("dx12", "get_device_name") + static function dxGetDeviceName() : hl.Bytes { + return null; + } + + @:hlNative("dx12", "get_driver_version") + public static function getDriverVersion() : Int64 { + return 0; + } + + @:hlNative("dx12", "query_video_memory_info") + public static function queryVideoMemoryInfo( group : Int, infos : QueryVideoMemoryInfo ) : Void { + } + + @:hlNative("dx12", "create") + static function dxCreate( win : hlemu.Abstract<"dx_window">, flags : DriverInitFlags, deviceName : hl.Bytes ) : DriverInstance { + return null; + } + + public static function setGpuCrashHandler( f : (name : hl.Bytes, bytes : hl.Bytes, size : Int, lastFile : Bool) -> Void ) { + return; + } +} + +#else + +class Dx12 { + public static macro function serializeRootSignature( desc, version, size ) { + return dx.Dx12Macros.serializeRootSignature(desc, version, size); + } +} + +#end diff --git a/hide_js/libs/dx/Dx12Macros.hx b/hide_js/libs/dx/Dx12Macros.hx new file mode 100644 index 000000000..dc2ae8fed --- /dev/null +++ b/hide_js/libs/dx/Dx12Macros.hx @@ -0,0 +1,21 @@ +package dx; + +#if macro +import haxe.macro.Expr; + +class Dx12Macros { + + /** + HL passes the address of the caller's `size` local variable : write the result + back to it after the call. + **/ + public static function serializeRootSignature( desc : Expr, version : Expr, size : Expr ) : Expr { + return macro { + var __size = new hl.Ref(0); + var __bytes = @:privateAccess dx.Dx12.serializeRootSignatureRef($desc, $version, __size); + $size = __size.get(); + __bytes; + }; + } +} +#end diff --git a/hide_js/libs/dx/Window.hx b/hide_js/libs/dx/Window.hx new file mode 100644 index 000000000..801719438 --- /dev/null +++ b/hide_js/libs/dx/Window.hx @@ -0,0 +1,36 @@ +package dx; + +/** + JS replacement of hldx's dx.Window, as used by the DX12 driver : the window is the + page canvas (#webgl, like hxd.Window), where the back buffers are displayed. +**/ +class Window { + + static var windows(get, null) : Array; + + public var canvas(default, null) : js.html.CanvasElement; + public var width(get, never) : Int; + public var height(get, never) : Int; + public var vsync : Bool = true; + + public function new( canvas : js.html.CanvasElement ) { + this.canvas = canvas; + } + + function get_width() { + return Math.round(canvas.getBoundingClientRect().width * js.Browser.window.devicePixelRatio); + } + + function get_height() { + return Math.round(canvas.getBoundingClientRect().height * js.Browser.window.devicePixelRatio); + } + + static function get_windows() { + if( windows == null ) { + var canvas : js.html.CanvasElement = cast js.Browser.document.getElementById("webgl"); + if( canvas == null ) throw "Missing canvas #webgl"; + windows = [new Window(canvas)]; + } + return windows; + } +} diff --git a/hide_js/libs/hlemu/Abstract.hx b/hide_js/libs/hlemu/Abstract.hx new file mode 100644 index 000000000..172329b65 --- /dev/null +++ b/hide_js/libs/hlemu/Abstract.hx @@ -0,0 +1,4 @@ +package hlemu; + +/** JS emulation of `hl.Abstract` : a native pointer, stored as a JS number at runtime **/ +extern class Abstract {} diff --git a/hide_js/libs/hlemu/Api.hx b/hide_js/libs/hlemu/Api.hx new file mode 100644 index 000000000..0fa72a7be --- /dev/null +++ b/hide_js/libs/hlemu/Api.hx @@ -0,0 +1,10 @@ +package hlemu; + +class Api { + public static inline function unsafeCast( v : From ) : To { + return cast v; + } + public static inline function rethrow( v : Dynamic ) : Void { + throw v; + } +} diff --git a/hide_js/libs/hlemu/Bytes.hx b/hide_js/libs/hlemu/Bytes.hx new file mode 100644 index 000000000..bcd00dfe4 --- /dev/null +++ b/hide_js/libs/hlemu/Bytes.hx @@ -0,0 +1,170 @@ +package hlemu; + +#if !macro + +import hlemu.Native as N; + +/** + JS emulation of `hl.Bytes` : a raw native address. + + It can point to native memory (allocated by the addon, or mapped GPU memory) or to + the data of a JS ArrayBuffer : V8 never moves a backing store, so its address stays + valid as long as the buffer is alive. Electron does not allow JS to see native memory + directly, so every access goes through the addon (one call per blit). +**/ +abstract Bytes(Float) { + + /** native allocation. Like small GC allocations in HL, it is never freed. **/ + public inline function new( size : Int ) { + this = N.mem_alloc(size); + } + + public inline function offset( delta : Int ) : Bytes { + return cast (this + delta); + } + + public inline function subtract( other : Bytes ) : Int { + return Std.int(this - (cast other : Float)); + } + + public inline function blit( pos : Int, src : Bytes, srcPos : Int, len : Int ) : Void { + N.mem_copy(this + pos, (cast src : Float) + srcPos, len); + } + + public inline function fill( pos : Int, size : Int, v : Int ) : Void { + N.mem_fill(this + pos, v, size); + } + + @:arrayAccess public inline function getUI8( pos : Int ) : Int { + return N.mem_get_ui8(this + pos); + } + + @:arrayAccess public inline function setUI8( pos : Int, value : Int ) : Int { + N.mem_set_ui8(this + pos, value); + return value; + } + + public inline function getUI16( pos : Int ) : Int { + return N.mem_get_ui16(this + pos); + } + + public inline function setUI16( pos : Int, v : Int ) : Void { + N.mem_set_ui16(this + pos, v); + } + + public inline function getI32( pos : Int ) : Int { + return N.mem_get_i32(this + pos); + } + + public inline function setI32( pos : Int, value : Int ) : Void { + N.mem_set_i32(this + pos, value); + } + + public inline function getF32( pos : Int ) : Single { + return N.mem_get_f32(this + pos); + } + + public inline function setF32( pos : Int, value : Single ) : Void { + N.mem_set_f32(this + pos, value); + } + + public inline function getF64( pos : Int ) : Float { + return N.mem_get_f64(this + pos); + } + + public inline function setF64( pos : Int, value : Float ) : Void { + N.mem_set_f64(this + pos, value); + } + + public inline function address() : haxe.Int64 { + return I64.toInt64(cast this); + } + + public static inline function fromAddress( h : haxe.Int64 ) : Bytes { + return cast I64.ofInt64(h); + } + + /** copies `size` bytes into a new haxe.io.Bytes (JS can't share native memory) **/ + public function sub( pos : Int, size : Int ) : haxe.io.Bytes { + var out = haxe.io.Bytes.alloc(size); + N.mem_copy(N.mem_address(@:privateAccess out.b), this + pos, size); + return out; + } + + public inline function toBytes( len : Int ) : haxe.io.Bytes { + return sub(0, len); + } + + @:from public static inline function fromBytes( bytes : haxe.io.Bytes ) : Bytes { + return bytes == null ? null : convert(bytes); + } + + static var lastConverted : haxe.io.Bytes; + static var lastAddress : Float = 0; + + static function convert( bytes : haxe.io.Bytes ) : Bytes { + var a = N.mem_address(@:privateAccess bytes.b); + lastConverted = bytes; + lastAddress = a; + return cast a; + } + + /** + The haxe.io.Bytes the given address was just converted from, if any. Used by the + struct fields to keep the JS object alive, like the HL GC does. + **/ + @:noCompletion public static inline function convertedFrom( b : Bytes ) : haxe.io.Bytes { + return b != null && (cast b : Float) == lastAddress ? lastConverted : null; + } + + /** + Address of the data of an array of basic types, like `hl.Bytes.getArray`. + Typed arrays are not copied; plain JS arrays (16 bits indexes) are copied into a + temporary buffer, valid until the next call. + **/ + public static macro function getArray( a : haxe.macro.Expr ) : haxe.macro.Expr { + return hlemu.Macros.getArray(a); + } + + // ---- JS helpers (not in hl.Bytes) + + /** null-terminated UTF-16 copy of `s`, in native memory (to free with `free`) **/ + public static function ofUcs2( s : String ) : Bytes { + if( s == null ) return null; + var b = new Bytes((s.length + 1) << 1); + for( i in 0...s.length ) + b.setUI16(i << 1, StringTools.fastCodeAt(s, i)); + b.setUI16(s.length << 1, 0); + return b; + } + + /** reads a null-terminated UTF-16 string **/ + public function toUcs2() : String { + var len = N.mem_ustrlen(this); + var buf = new StringBuf(); + for( i in 0...len ) + buf.addChar(getUI16(i << 1)); + return buf.toString(); + } + + public inline function free() : Void { + N.mem_free(this); + } + + public inline function isNull() : Bool { + return this == null || this == 0; + } + + @:noCompletion public static var tmpArray : Dynamic; +} + +#else + +// the type is loaded in the macro context to run getArray +abstract Bytes(Float) { + public static macro function getArray( a : haxe.macro.Expr ) : haxe.macro.Expr { + return hlemu.Macros.getArray(a); + } +} + +#end diff --git a/hide_js/libs/hlemu/BytesAccess.hx b/hide_js/libs/hlemu/BytesAccess.hx new file mode 100644 index 000000000..35966d13a --- /dev/null +++ b/hide_js/libs/hlemu/BytesAccess.hx @@ -0,0 +1,8 @@ +package hlemu; + +/** + JS emulation of `hl.BytesAccess` : typed access to an hl.Bytes. Each element type + generates its own abstract over hlemu.Bytes (see hlemu.Macros.bytesAccess). +**/ +@:genericBuild(hlemu.Macros.bytesAccess()) +class BytesAccess {} diff --git a/hide_js/libs/hlemu/CArray.hx b/hide_js/libs/hlemu/CArray.hx new file mode 100644 index 000000000..64626b7a6 --- /dev/null +++ b/hide_js/libs/hlemu/CArray.hx @@ -0,0 +1,40 @@ +package hlemu; + + +/** + JS emulation of `hl.CArray` : structs stored contiguously in one ArrayBuffer. + The element views are created on demand and cached. +**/ +abstract CArray(CArrayData) { + + @:arrayAccess inline function get( index : Int ) : T { + var it = this.items[index]; + if( it == null ) { + var o = index * this.stride; + it = this.items[index] = this.cl.__view(this.view, o, this.__a + o); + } + return it; + } + + public static function alloc( cl : Class, size : Int ) : CArray { + var d = new CArrayData(); + d.cl = cl; + d.stride = (cast cl).__SIZE; + d.count = size; + var buf = new js.lib.ArrayBuffer(d.stride * size > 0 ? d.stride * size : 1); + d.view = new js.lib.DataView(buf); + d.__a = Native.mem_address(buf); + d.items = []; + return cast d; + } + + public function toBytes( cl : Class, count : Int ) : haxe.io.Bytes { + if( this == null ) return null; + return (cast this.__a : Bytes).sub(0, this.stride * count); + } + + public inline function blit( cl : Class, pos : Int, src : CArray, srcPos : Int, srcLen : Int ) : Void { + var s : CArrayData = cast src; + Native.mem_copy(this.__a + pos * this.stride, s.__a + srcPos * this.stride, srcLen * this.stride); + } +} diff --git a/hide_js/libs/hlemu/CArrayData.hx b/hide_js/libs/hlemu/CArrayData.hx new file mode 100644 index 000000000..37e6f59c9 --- /dev/null +++ b/hide_js/libs/hlemu/CArrayData.hx @@ -0,0 +1,19 @@ +package hlemu; + +/** the data of a CArray (its own module : with Haxe 5 preview.1, the type of a macro is resolved by its path) **/ +@:noCompletion class CArrayData { + public var cl : Dynamic; + public var stride : Int; + public var count : Int; + public var view : js.lib.DataView; + /** native address, same name as the structs field (see Struct.addr) **/ + public var __a : Float; + public var items : Array; + public function new() { + } + public function __sync() { + if( !cl.__NEEDSYNC ) return; + for( it in items ) + if( it != null ) it.__sync(); + } +} diff --git a/hide_js/libs/hlemu/F32.hx b/hide_js/libs/hlemu/F32.hx new file mode 100644 index 000000000..ccb720178 --- /dev/null +++ b/hide_js/libs/hlemu/F32.hx @@ -0,0 +1,3 @@ +package hlemu; + +typedef F32 = Single; diff --git a/hide_js/libs/hlemu/F64.hx b/hide_js/libs/hlemu/F64.hx new file mode 100644 index 000000000..0b82dec7e --- /dev/null +++ b/hide_js/libs/hlemu/F64.hx @@ -0,0 +1,3 @@ +package hlemu; + +typedef F64 = Float; diff --git a/hide_js/libs/hlemu/I64.hx b/hide_js/libs/hlemu/I64.hx new file mode 100644 index 000000000..e46899185 --- /dev/null +++ b/hide_js/libs/hlemu/I64.hx @@ -0,0 +1,30 @@ +package hlemu; + +/** + JS emulation of `hl.I64` : a 64 bits integer stored as a JS number (exact below 2^53, + enough for sizes, offsets, fence values and addresses). + + Also hosts the conversions between JS numbers and haxe.Int64. +**/ +@:notNull abstract I64(Float) from Int from Float to Float { + + public inline function toInt() : Int { + return Std.int(this); + } + + @:to inline function implicitToInt() : Int { + return toInt(); + } + + // the arguments of js.Syntax.code are pasted without parentheses once inlined : without them, + // ofInt64(toInt64(v)) gave "x | 0 >>> 0" (">>>" first : a signed low word, addresses 4 GB off) + + public static inline function toInt64( v : Float ) : haxe.Int64 { + var high = Math.floor(v / 4294967296.); + return haxe.Int64.make(Std.int(high), js.Syntax.code("((({0}) >>> 0) | 0)", v - high * 4294967296.)); + } + + public static inline function ofInt64( v : haxe.Int64 ) : Float { + return v.high * 4294967296. + js.Syntax.code("(({0}) >>> 0)", v.low); + } +} diff --git a/hide_js/libs/hlemu/Macros.hx b/hide_js/libs/hlemu/Macros.hx new file mode 100644 index 000000000..14c832259 --- /dev/null +++ b/hide_js/libs/hlemu/Macros.hx @@ -0,0 +1,722 @@ +package hlemu; + +#if macro +import haxe.macro.Context; +import haxe.macro.Expr; +import haxe.macro.Type; +using haxe.macro.Tools; +#end + +/** + Compile time support of the HL emulation for JS. + + - `init()` : call with `--macro hlemu.Macros.init(["dx", ...])` : remaps nothing by + itself (use `--remap hl:hlemu`), but adds `build()` to the given packages. + - `@:struct` classes get the exact C layout of HashLink : fields are stored in an + ArrayBuffer (accessed through a DataView), whose native address is known, so the + struct can be passed to the addon like in HL. + - `@:hlNative` methods call the addon (global `__dx12`), with the same naming rules + as the HL compiler. +**/ +class Macros { + + #if macro + + public static function init( packs : Array ) { + for( p in packs ) + haxe.macro.Compiler.addGlobalMetadata(p, "@:build(hlemu.Macros.build())", true, true, false); + } + + public static function build() : Array { + var fields = Context.getBuildFields(); + var changed = rewriteStringMethods(fields); + if( rewriteRemappedTypes(fields) ) changed = true; + if( addDefaultValues(fields) ) changed = true; + var t = Context.getLocalType(); + var res = switch( t ) { + case TInst(c, _): + var c = c.get(); + if( c.meta.has(":struct") ) + buildStruct(c, fields); + else + // abstracts are built through their implementation class + buildNatives(c.meta.extract(":hlNative"), c.kind.match(KAbstractImpl(_)), fields); + case TAbstract(a, _): + buildNatives(a.get().meta.extract(":hlNative"), true, fields); + default: + null; + } + return res != null ? res : changed ? fields : null; + } + + /** + HL initializes basic typed fields (Int, Float, Bool...) to 0 / false, JS leaves them + undefined : add the HL default values. Struct fields are in zeroed memory already. + **/ + static function addDefaultValues( fields : Array ) : Bool { + var changed = false; + var cl = Context.getLocalClass(); + if( cl == null ) return false; + var c = cl.get(); + if( c.meta.has(":struct") || c.isInterface || c.isExtern || !c.kind.match(KNormal) ) return false; + // classes generated by hxsl macros (shaders, globals...) are left alone + for( m in c.meta.extract(":build") ) + if( m.params.length > 0 && StringTools.startsWith(haxe.macro.ExprTools.toString(m.params[0]), "hxsl.") ) return false; + var p = c; + while( p != null ) { + if( p.meta.has(":autoBuild") ) return false; + p = p.superClass == null ? null : p.superClass.t.get(); + } + for( f in fields ) { + switch( f.kind ) { + case FVar(t, null) if( t != null ): + var e = defaultValue(t, f.pos); + if( e != null ) { + f.kind = FVar(t, e); + changed = true; + } + case FProp(get, set, t, null) if( t != null && (get == "default" || get == "null") && (set == "default" || set == "null" || set == "never") ): + var e = defaultValue(t, f.pos); + if( e != null ) { + f.kind = FProp(get, set, t, e); + changed = true; + } + default: + } + } + return changed; + } + + static function defaultValue( ct : ComplexType, pos : Position ) : Expr { + switch( ct ) { + case TPath({ name : "Null", pack : [] }): return null; + default: + } + var t = try Context.resolveType(ct, pos) catch( _ : Dynamic ) return null; + function loop( t : Type ) : Expr { + return switch( t ) { + case TType(_): loop(Context.follow(t, true)); + case TLazy(f): loop(f()); + case TAbstract(a, _): + var a = a.get(); + switch( a.pack.concat([a.name]).join(".") ) { + case "Int", "Float", "Single", "hlemu.UI8", "hlemu.UI16", "hlemu.I64": macro cast 0; + case "Bool": macro false; + case "haxe.Int64", "haxe._Int64.Int64": macro haxe.Int64.make(0, 0); + case "Null": null; + default: + if( a.meta.has(":coreType") ) null; + // enum abstracts over Int, EnumFlags... + else switch( Context.follow(a.type) ) { + case TAbstract(b, _) if( b.get().name == "Int" && b.get().pack.length == 0 ): macro cast 0; + default: null; + } + } + default: null; + } + } + return loop(t); + } + + /** + `--remap hl:hlemu` loses the type parameters of @:genericBuild types and constant + type parameters (Haxe 5 preview) : these types are rewritten to hlemu directly. + **/ + static var REMAP_DIRECT = ["BytesAccess", "Abstract"]; + + static var remapChanged = false; + + static function rewriteRemappedTypes( fields : Array ) : Bool { + remapChanged = false; + for( f in fields ) mapField(f); + return remapChanged; + } + + static function mapType( t : ComplexType ) : ComplexType { + if( t == null ) return null; + return switch( t ) { + case TPath(p): + TPath(mapPath(p)); + case TFunction(args, ret): + TFunction([for( a in args ) mapType(a)], mapType(ret)); + case TAnonymous(fl): + TAnonymous([for( f in fl ) mapField(f)]); + case TParent(t): + TParent(mapType(t)); + case TOptional(t): + TOptional(mapType(t)); + case TNamed(n, t): + TNamed(n, mapType(t)); + default: + t; + } + } + static function mapPath( p : TypePath ) : TypePath { + var pack = p.pack; + if( pack.length == 1 && pack[0] == "hl" && REMAP_DIRECT.indexOf(p.name) >= 0 ) { + pack = ["hlemu"]; + remapChanged = true; + } + return { + pack : pack, + name : p.name, + sub : p.sub, + params : p.params == null ? null : [for( tp in p.params ) switch( tp ) { case TPType(t): TPType(mapType(t)); default: tp; }], + }; + } + static function mapFun( f : Function ) { + for( a in f.args ) a.type = mapType(a.type); + f.ret = mapType(f.ret); + if( f.expr != null ) f.expr = mapExpr(f.expr); + } + static function mapExpr( e : Expr ) : Expr { + if( e == null ) return null; + switch( e.expr ) { + case EVars(vl): + for( v in vl ) { + v.type = mapType(v.type); + v.expr = mapExpr(v.expr); + } + return e; + case ECast(v, t): + return { expr : ECast(mapExpr(v), mapType(t)), pos : e.pos }; + case ECheckType(v, t): + return { expr : ECheckType(mapExpr(v), mapType(t)), pos : e.pos }; + case ENew(p, args): + return { expr : ENew(mapPath(p), [for( a in args ) mapExpr(a)]), pos : e.pos }; + case EFunction(_, f): + mapFun(f); + return e; + default: + return haxe.macro.ExprTools.map(e, mapExpr); + } + } + static function mapField( f : Field ) : Field { + switch( f.kind ) { + case FVar(t, e): f.kind = FVar(mapType(t), mapExpr(e)); + case FProp(g, s, t, e): f.kind = FProp(g, s, mapType(t), mapExpr(e)); + case FFun(fn): mapFun(fn); + } + return f; + } + + + /** + HL strings have toUtf8() : x.toUtf8() calls are redirected to hlemu.StringExt + (syntactically : the types are not known yet). + **/ + static function rewriteStringMethods( fields : Array ) : Bool { + var changed = false; + function map( e : Expr ) : Expr { + return switch( e.expr ) { + case ECall({ expr : EField(obj, "toUtf8") }, []): + changed = true; + var o = map(obj); + macro hlemu.StringExt.toUtf8($o); + default: + haxe.macro.ExprTools.map(e, map); + } + } + for( f in fields ) + switch( f.kind ) { + case FFun(fn) if( fn.expr != null ): fn.expr = map(fn.expr); + default: + } + return changed; + } + + // ---- types + + public static function classify( t : Type, packed : Bool ) : Kind { + switch( t ) { + case TType(_): + return classify(Context.follow(t, true), packed); + case TLazy(f): + return classify(f(), packed); + case TAbstract(a, params): + var a = a.get(); + var path = a.pack.concat([a.name]).join("."); + switch( path ) { + case "Int": return KI32; + case "Float": return KF64; + case "Single": return KF32; + case "Bool": return KBool; + case "haxe.Int64", "haxe._Int64.Int64": return KInt64; + case "hlemu.UI8": return KU8; + case "hlemu.UI16": return KU16; + case "hlemu.I64": return KNum64; + case "hlemu.Bytes", "hlemu.Abstract": return KPtr; + case "hlemu.CArray": + return KPtrRef(switch( Context.follow(params[0]) ) { case TInst(c, _) if( c.get().meta.has(":struct") ): c.get(); default: null; }); + case "hlemu.NativeArray": return KArray; + case "haxe.EnumFlags": return KFlags; + case "Null": return KObject; + default: + } + if( a.pack.join(".") == "hlemu" && StringTools.startsWith(a.name, "BytesAccess_") ) + return KPtr; + if( a.meta.has(":coreType") ) + return KObject; + return classify(a.type, packed); + case TInst(c, _): + var c = c.get(); + if( c.meta.has(":struct") ) + return packed ? KStruct(c) : KPtrRef(c); + if( c.pack.join(".") == "hlemu" && c.name == "Abstract" ) + return KPtr; + return KObject; + default: + return KObject; + } + } + + static function kindSize( k : Kind ) : { size : Int, align : Int } { + return switch( k ) { + case KBool, KU8: { size : 1, align : 1 }; + case KU16: { size : 2, align : 2 }; + case KI32, KF32, KFlags: { size : 4, align : 4 }; + case KF64, KInt64, KNum64, KPtr, KPtrRef(_): { size : 8, align : 8 }; + case KStruct(c): getLayout(c); + case KArray, KObject, KRef: { size : 0, align : 1 }; + } + } + + static function getLayout( c : ClassType ) : { size : Int, align : Int, end : Int, sync : Bool } { + var m = c.meta.extract(":hlemuLayout"); + if( m.length == 0 ) { + // force the build of the struct + c.fields.get(); + m = c.meta.extract(":hlemuLayout"); + if( m.length == 0 ) Context.error("Struct layout of " + c.name + " is not known yet", c.pos); + } + var p = m[0].params; + inline function int(e:Expr) return switch( e.expr ) { case EConst(CInt(v)): Std.parseInt(v); default: throw "assert"; } + return { size : int(p[0]), align : int(p[1]), end : int(p[2]), sync : int(p[3]) != 0 }; + } + + // ---- structs + + static function buildStruct( c : ClassType, fields : Array ) : Array { + var pos = Context.currentPos(); + var offset = 0, align = 1; + var isRoot = true; + // EnumFlags fields are normal JS fields (so that flags.set() works), copied to the + // memory by __sync() before the struct is passed to a native function + var syncs : Array = []; + var needSync = false; + if( c.superClass != null && c.superClass.t.get().meta.has(":struct") ) { + var l = getLayout(c.superClass.t.get()); + offset = l.end; + align = l.align; + needSync = l.sync; + isRoot = false; + } + var out : Array = []; + var ctPath = c.pack.concat([c.name]); + var self = TPath({ pack : c.pack, name : c.module.split(".").pop(), sub : c.name == c.module.split(".").pop() ? null : c.name }); + + for( f in fields ) { + if( f.access.indexOf(AStatic) >= 0 ) { + out.push(f); + continue; + } + var ct, isReadOnly = false; + switch( f.kind ) { + case FVar(t, _): + ct = t; + case FProp("default", set, t, _) if( set == "null" || set == "never" || set == "default" ): + ct = t; + isReadOnly = set != "default"; + default: + out.push(f); + continue; + } + if( ct == null ) Context.error("Struct field requires a type", f.pos); + var packed = f.meta != null && Lambda.exists(f.meta, m -> m.name == ":packed"); + var kind = classify(Context.resolveType(ct, f.pos), packed); + switch( kind ) { + case KObject, KArray, KRef: + // not representable in memory : kept as a normal JS field + out.push(f); + continue; + default: + } + var sz = kindSize(kind); + offset = alignTo(offset, sz.align); + if( sz.align > align ) align = sz.align; + var off = offset; + offset += sz.size; + + var name = f.name; + var access = f.access.copy(); + var getName = "get_" + name, setName = "set_" + name; + var o = macro this.__o + $v{off}; + var getter : Expr, setter : Expr = null; + var extra : Array = []; + switch( kind ) { + case KFlags: + syncs.push(macro this.__v.setInt32($o, cast this.$name, true)); + needSync = true; + out.push(f); + continue; + case KBool: + getter = macro return this.__v.getUint8($o) != 0; + setter = macro { this.__v.setUint8($o, v ? 1 : 0); return v; }; + case KU8: + getter = macro return cast this.__v.getUint8($o); + setter = macro { this.__v.setUint8($o, cast v); return v; }; + case KU16: + getter = macro return cast this.__v.getUint16($o, true); + setter = macro { this.__v.setUint16($o, cast v, true); return v; }; + case KI32: + getter = macro return cast this.__v.getInt32($o, true); + setter = macro { this.__v.setInt32($o, cast v, true); return v; }; + case KF32: + getter = macro return this.__v.getFloat32($o, true); + setter = macro { this.__v.setFloat32($o, v, true); return v; }; + case KF64: + getter = macro return this.__v.getFloat64($o, true); + setter = macro { this.__v.setFloat64($o, v, true); return v; }; + case KInt64: + getter = macro return haxe.Int64.make(this.__v.getInt32($o + 4, true), this.__v.getInt32($o, true)); + setter = macro { var x : haxe.Int64 = v; this.__v.setInt32($o, x.low, true); this.__v.setInt32($o + 4, x.high, true); return v; }; + case KNum64: + getter = macro return cast hlemu.Struct.getNum64(this.__v, $o); + setter = macro { hlemu.Struct.setNum64(this.__v, $o, cast v); return v; }; + case KPtr if( isBytesType(ct, f.pos) ): + // In HL the GC follows the pointer : keep the haxe.io.Bytes it was converted from + // alive (shader bytecode kept in a pipeline description, for instance) + var ref = "__r_" + name; + extra.push({ name : ref, kind : FVar(macro : haxe.io.Bytes), pos : f.pos, access : [APublic], meta : [{ name : ":noCompletion", pos : f.pos }] }); + getter = macro { var n = hlemu.Struct.getNum64(this.__v, $o); return n == 0 ? null : cast n; }; + setter = macro { this.$ref = hlemu.Bytes.convertedFrom(v); hlemu.Struct.setNum64(this.__v, $o, v == null ? 0 : cast v); return v; }; + case KPtr: + getter = macro { var n = hlemu.Struct.getNum64(this.__v, $o); return n == 0 ? null : cast n; }; + setter = macro { hlemu.Struct.setNum64(this.__v, $o, v == null ? 0 : cast v); return v; }; + case KPtrRef(target): + var ref = "__r_" + name; + if( target == null || getLayout(target).sync ) { + syncs.push(macro if( this.$ref != null ) (cast this.$ref).__sync()); + needSync = true; + } + extra.push({ name : ref, kind : FVar(ct), pos : f.pos, access : [APublic], meta : [{ name : ":noCompletion", pos : f.pos }] }); + getter = macro return this.$ref; + setter = macro { this.$ref = v; hlemu.Struct.setNum64(this.__v, $o, hlemu.Struct.addr(v)); return v; }; + case KStruct(sc): + var cache = "__p_" + name; + extra.push({ name : cache, kind : FVar(ct), pos : f.pos, access : [], meta : [{ name : ":noCompletion", pos : f.pos }] }); + var scPath = sc.pack.concat([sc.module.split(".").pop()]); + if( sc.name != sc.module.split(".").pop() ) scPath.push(sc.name); + if( getLayout(sc).sync ) { + syncs.push(macro { var s = this.$cache; if( s != null ) s.__sync(); }); + needSync = true; + } + getter = macro { + var s = this.$cache; + if( s == null ) this.$cache = s = $p{scPath}.__view(this.__v, $o, this.__a + $v{off}); + return s; + }; + isReadOnly = true; + case KObject, KArray, KRef: + throw "assert"; + } + out.push({ name : name, access : access, kind : FProp("get", isReadOnly ? "never" : "set", ct), pos : f.pos, meta : f.meta, doc : f.doc }); + out.push({ name : getName, access : [AInline], kind : FFun({ args : [], ret : ct, expr : getter }), pos : f.pos, meta : [{ name : ":noCompletion", pos : f.pos }] }); + if( !isReadOnly ) + out.push({ name : setName, access : [AInline], kind : FFun({ args : [{ name : "v", type : ct }], ret : ct, expr : setter }), pos : f.pos, meta : [{ name : ":noCompletion", pos : f.pos }] }); + for( e in extra ) out.push(e); + } + + var end = offset; + var size = alignTo(offset, align); + c.meta.add(":hlemuLayout", [macro $v{size}, macro $v{align}, macro $v{end}, macro $v{needSync ? 1 : 0}], pos); + + if( isRoot ) { + var noComp = [{ name : ":noCompletion", pos : pos }]; + out.push({ name : "__v", access : [APublic], kind : FVar(macro : js.lib.DataView), pos : pos, meta : noComp }); + out.push({ name : "__o", access : [APublic], kind : FVar(macro : Int), pos : pos, meta : noComp }); + out.push({ name : "__a", access : [APublic], kind : FVar(macro : Float), pos : pos, meta : noComp }); + } + var keep = [{ name : ":keep", pos : pos }, { name : ":noCompletion", pos : pos }]; + if( isRoot ) + out.push({ name : "__sync", access : [APublic], pos : pos, meta : keep, kind : FFun({ args : [], ret : macro : Void, expr : macro $b{syncs} }) }); + else if( syncs.length > 0 ) + out.push({ name : "__sync", access : [APublic, AOverride], pos : pos, meta : keep, kind : FFun({ args : [], ret : macro : Void, expr : macro { super.__sync(); $b{syncs}; } }) }); + out.push({ name : "__NEEDSYNC", access : [APublic, AStatic], kind : FVar(macro : Bool, macro $v{needSync}), pos : pos, meta : keep }); + var cl = macro $p{ctPath}; + out.push({ name : "__SIZE", access : [APublic, AStatic], kind : FVar(macro : Int, macro $v{size}), pos : pos, meta : keep }); + out.push({ name : "__view", access : [APublic, AStatic], pos : pos, meta : keep, kind : FFun({ + args : [{ name : "v", type : macro : js.lib.DataView }, { name : "o", type : macro : Int }, { name : "a", type : macro : Float }], + ret : self, + expr : macro { + var s : $self = js.Syntax.code("Object.create({0}.prototype)", $cl); + s.__v = v; + s.__o = o; + s.__a = a; + return s; + } + })}); + // memory allocation at construction + for( f in out ) + if( f.name == "new" ) + switch( f.kind ) { + case FFun(fn): + fn.expr = macro { hlemu.Struct.alloc(this, $v{size}); ${fn.expr}; }; + default: + } + return out; + } + + static function isBytesType( ct : ComplexType, pos : Position ) { + return switch( Context.follow(Context.resolveType(ct, pos)) ) { + case TAbstract(a, _): a.get().pack.join(".") == "hlemu" && a.get().name == "Bytes"; + default: false; + } + } + + static inline function alignTo( v : Int, a : Int ) { + return (v + a - 1) & ~(a - 1); + } + + // ---- natives + + static function nativeName( prefix : String, name : String ) { + return prefix + ~/[A-Z]+/g.map(name, r -> "_" + r.matched(0)).toLowerCase(); + } + + static function isEmptyBody( e : Expr ) { + if( e == null ) return true; + return switch( e.expr ) { + case EBlock([]): true; + case EBlock([{ expr : EReturn({ expr : EConst(_) }) }]): true; + default: false; + } + } + + static function buildNatives( classMeta : Array, isAbstract : Bool, fields : Array ) : Array { + var lib = null, prefix = ""; + for( m in classMeta ) { + lib = getString(m.params[0]); + if( m.params.length > 1 ) prefix = getString(m.params[1]); + } + var changed = false; + for( f in fields ) { + switch( f.kind ) { + case FFun(fn): + if( f.access.indexOf(AInline) >= 0 || f.access.indexOf(ADynamic) >= 0 || f.access.indexOf(AMacro) >= 0 ) continue; + var native = null, flib = null; + for( m in f.meta ) + if( m.name == ":hlNative" ) { + flib = getString(m.params[0]); + native = m.params.length > 1 ? getString(m.params[1]) : f.name; + } + if( native == null ) { + if( lib == null || !isEmptyBody(fn.expr) || f.name == "new" ) continue; + // class level : static methods only (and all methods of abstracts) + if( !isAbstract && f.access.indexOf(AStatic) < 0 ) continue; + flib = lib; + native = nativeName(prefix, f.name); + } + var isStatic = f.access.indexOf(AStatic) >= 0; + fn.expr = makeNativeCall(flib, native, fn, isAbstract && !isStatic, f.pos); + changed = true; + default: + } + } + return changed ? fields : null; + } + + static function getString( e : Expr ) { + return switch( e.expr ) { + case EConst(CString(s)): s; + default: Context.error("String expected", e.pos); + } + } + + static function makeNativeCall( lib : String, name : String, fn : Function, withThis : Bool, pos : Position ) : Expr { + if( lib != "dx12" ) { + // optional libraries (?dx12debug) are not available in JS + if( StringTools.startsWith(lib, "?") ) return macro {}; + Context.error("Native library " + lib + " is not available in JS", pos); + } + var args : Array = [], pre : Array = [], post : Array = []; + if( withThis ) args.push(macro this); + for( a in fn.args ) { + var e = macro $i{a.name}; + var kind = a.type == null ? KObject : classifyArg(a.type, pos); + // HL accepts any BytesAccess for a BytesAccess argument + if( kind == KPtr && isBytesAccess(a.type) ) a.type = macro : hlemu.Bytes; + switch( kind ) { + case KInt64: + args.push(macro hlemu.I64.ofInt64($e)); + case KPtrRef(_): + args.push(macro hlemu.Struct.addrSync($e)); + case KArray: + var tmp = "__arr_" + a.name; + pre.push(macro var $tmp = hlemu.Struct.toVarray(cast $e)); + post.push(macro hlemu.Native.mem_free($i{tmp})); + args.push(macro $i{tmp}); + case KRef: + var tmp = "__ref_" + a.name; + pre.push(macro var $tmp = hlemu.Native.mem_alloc(8)); + post.push(macro { $e.set(cast hlemu.Native.mem_get_i32($i{tmp})); hlemu.Native.mem_free($i{tmp}); }); + args.push(macro $i{tmp}); + case KObject: + Context.error("Unsupported native argument type " + a.type.toString(), pos); + default: + args.push(e); + } + } + var code = macro $v{"__dx12." + name + "(" + [for( i in 0...args.length ) "{" + i + "}"].join(",") + ")"}; + var call = { expr : ECall(macro js.Syntax.code, [code].concat(args)), pos : pos }; + var retKind = fn.ret == null ? KObject : (switch( fn.ret ) { case TPath({ name : "Void" }): null; default: classifyArg(fn.ret, pos); }); + var body : Array = pre.copy(); + if( retKind == null ) { + body.push(call); + for( e in post ) body.push(e); + } else { + var conv = switch( retKind ) { + case KInt64: macro hlemu.I64.toInt64(__r); + case KArray: macro cast hlemu.Struct.fromVarray(__r); + default: macro cast __r; + }; + body.push(macro var __r : Dynamic = $call); + for( e in post ) body.push(e); + body.push(macro return $conv); + } + return macro $b{body}; + } + + static function isBytesAccess( ct : ComplexType ) { + return switch( ct ) { + case TPath({ name : "BytesAccess" }): true; + default: false; + } + } + + static function classifyArg( ct : ComplexType, pos : Position ) : Kind { + var t = Context.resolveType(ct, pos); + switch( Context.follow(t) ) { + case TInst(c, _) if( c.get().pack.join(".") == "hlemu" && c.get().name == "Ref" ): + return KRef; + default: + } + return classify(t, false); + } + + // ---- hl.Bytes.getArray + + public static function getArray( e : Expr ) : Expr { + // x.toData() : HL vectors, typed arrays in JS (h3d.shader.ShaderBufferData) + switch( e.expr ) { + case ECall({ expr : EField(x, "toData") }, []): + switch( Context.followWithAbstracts(Context.typeof(x)) ) { + case TInst(c, _) if( c.get().pack.join(".") == "js.lib" ): + e = x; + default: + } + default: + } + var t = Context.typeof(e); + switch( Context.followWithAbstracts(t) ) { + case TInst(c, [elt]) if( c.get().name == "Array" && c.get().pack.length == 0 ): + // JS arrays have no native representation : copy (16 bits indexes) + return macro hlemu.Struct.tmpUI16($e); + case TInst(c, _) if( c.get().pack.join(".") == "js.lib" ): + return macro (cast hlemu.Native.mem_address($e) : hlemu.Bytes); + case TAnonymous(a) if( Lambda.exists(a.get().fields, f -> f.name == "array") ): + // hxd.FloatBuffer on JS + return macro (cast hlemu.Native.mem_address((cast $e).array) : hlemu.Bytes); + default: + Context.error("Unsupported getArray type " + t.toString(), e.pos); + return null; + } + } + + // ---- hl.BytesAccess + + public static function bytesAccess() : ComplexType { + var pos = Context.currentPos(); + switch( Context.getLocalType() ) { + case TInst(_, [t]): + switch( t ) { + case TInst(c, _) if( c.get().kind.match(KTypeParameter(_)) ): + // not specialized (declaration itself) + return macro : hlemu.Bytes; + default: + } + var kind = classify(t, false); + var ct = t.toComplexType(); + var name = "BytesAccess_" + switch( kind ) { + case KPtr, KPtrRef(_), KNum64: "N64_" + StringTools.replace(t.toString(), ".", "_").split("<")[0]; + default: Std.string(kind); + }; + try { + Context.getType("hlemu." + name); + } catch( _ : Dynamic ) { + var size, get, set; + var a = macro (cast this : Float) + (i << $v{0}); + switch( kind ) { + case KI32: size = 2; get = macro cast hlemu.Native.mem_get_i32(p); set = macro hlemu.Native.mem_set_i32(p, cast v); + case KU8: size = 0; get = macro cast hlemu.Native.mem_get_ui8(p); set = macro hlemu.Native.mem_set_ui8(p, cast v); + case KU16: size = 1; get = macro cast hlemu.Native.mem_get_ui16(p); set = macro hlemu.Native.mem_set_ui16(p, cast v); + case KF32: size = 2; get = macro hlemu.Native.mem_get_f32(p); set = macro hlemu.Native.mem_set_f32(p, v); + case KF64: size = 3; get = macro hlemu.Native.mem_get_f64(p); set = macro hlemu.Native.mem_set_f64(p, v); + case KInt64: size = 3; get = macro hlemu.I64.toInt64(hlemu.Native.mem_get_i64(p)); set = macro hlemu.Native.mem_set_i64(p, hlemu.I64.ofInt64(v)); + case KNum64, KPtr: size = 3; get = macro cast hlemu.Native.mem_get_i64(p); set = macro hlemu.Native.mem_set_i64(p, cast v); + default: Context.error("Unsupported BytesAccess type " + t.toString(), pos); + } + var sizeBits = size; + var def = macro class $name { + public var sizeBits(get, never) : Int; + inline function get_sizeBits() return $v{sizeBits}; + @:arrayAccess public inline function get( i : Int ) : $ct { + var p = (cast this : Float) + (i << $v{sizeBits}); + return $get; + } + @:arrayAccess public inline function set( i : Int, v : $ct ) : $ct { + var p = (cast this : Float) + (i << $v{sizeBits}); + $set; + return v; + } + public inline function blit( pos : Int, src : hlemu.Bytes, srcPos : Int, len : Int ) : Void { + (cast this : hlemu.Bytes).blit(pos << $v{sizeBits}, src, srcPos << $v{sizeBits}, len << $v{sizeBits}); + } + }; + def.pack = ["hlemu"]; + def.kind = TDAbstract(macro : hlemu.Bytes, [AbFrom(macro : hlemu.Bytes), AbTo(macro : hlemu.Bytes)]); + Context.defineType(def); + } + return TPath({ pack : ["hlemu"], name : name }); + default: + throw "assert"; + } + } + + #end +} + +#if macro +enum Kind { + KBool; + KU8; + KU16; + KI32; + KF32; + KF64; + /** haxe.Int64 **/ + KInt64; + /** 64 bits integer stored as a JS number **/ + KNum64; + /** pointer stored as a JS number, null when 0 **/ + KPtr; + /** pointer to a struct or CArray (of the given struct) : the JS object is kept alive by the field **/ + KPtrRef( target : Null ); + /** haxe.EnumFlags : a normal JS field, synchronized before native calls **/ + KFlags; + KStruct( c : ClassType ); + KArray; + KRef; + KObject; +} +#end diff --git a/hide_js/libs/hlemu/Native.hx b/hide_js/libs/hlemu/Native.hx new file mode 100644 index 000000000..a9b160816 --- /dev/null +++ b/hide_js/libs/hlemu/Native.hx @@ -0,0 +1,31 @@ +package hlemu; + +/** + The dx12 Node-API addon (jsdx/native), loaded by the page as the global `__dx12` + before the Haxe code starts. + + Only the memory primitives are declared here: the DirectX primitives are called by + name from the code generated by `hlemu.NativeBind`. +**/ +@:native("__dx12") +extern class Native { + static function mem_alloc( size : Int ) : Float; + static function mem_free( p : Float ) : Void; + static function mem_copy( dst : Float, src : Float, size : Int ) : Void; + static function mem_fill( dst : Float, value : Int, size : Int ) : Void; + static function mem_get_ui8( p : Float ) : Int; + static function mem_set_ui8( p : Float, v : Int ) : Void; + static function mem_get_ui16( p : Float ) : Int; + static function mem_set_ui16( p : Float, v : Int ) : Void; + static function mem_get_i32( p : Float ) : Int; + static function mem_set_i32( p : Float, v : Int ) : Void; + static function mem_get_f32( p : Float ) : Float; + static function mem_set_f32( p : Float, v : Float ) : Void; + static function mem_get_f64( p : Float ) : Float; + static function mem_set_f64( p : Float, v : Float ) : Void; + static function mem_get_i64( p : Float ) : Float; + static function mem_set_i64( p : Float, v : Float ) : Void; + static function mem_ustrlen( p : Float ) : Int; + /** native address of the data of an ArrayBuffer, TypedArray or DataView **/ + static function mem_address( buf : Dynamic ) : Float; +} diff --git a/hide_js/libs/hlemu/NativeArray.hx b/hide_js/libs/hlemu/NativeArray.hx new file mode 100644 index 000000000..9c76b8f5d --- /dev/null +++ b/hide_js/libs/hlemu/NativeArray.hx @@ -0,0 +1,47 @@ +package hlemu; + +/** + JS emulation of `hl.NativeArray` : a plain JS array. Native bindings taking a + NativeArray convert it to an HL varray in native memory. +**/ +abstract NativeArray(Array) { + public var length(get, never) : Int; + + public inline function new( length : Int ) { + this = []; + if( length > 0 ) this[length - 1] = cast null; + } + + inline function get_length() : Int { + return this.length; + } + + @:arrayAccess inline function get( pos : Int ) : T { + return this[pos]; + } + + @:arrayAccess inline function set( pos : Int, value : T ) : T { + return this[pos] = value; + } + + public inline function sub( pos : Int, len : Int ) : NativeArray { + return cast this.slice(pos, pos + len); + } + + public inline function blit( pos : Int, src : NativeArray, srcPos : Int, srcLen : Int ) : Void { + var s : Array = cast src; + if( s == this && srcPos < pos ) { + var i = srcLen; + while( i-- > 0 ) this[pos + i] = s[srcPos + i]; + } else + for( i in 0...srcLen ) this[pos + i] = s[srcPos + i]; + } + + public inline function toArray() : Array { + return this; + } + + public function iterator() { + return this.iterator(); + } +} diff --git a/hide_js/libs/hlemu/Ref.hx b/hide_js/libs/hlemu/Ref.hx new file mode 100644 index 000000000..53adffd20 --- /dev/null +++ b/hide_js/libs/hlemu/Ref.hx @@ -0,0 +1,19 @@ +package hlemu; + +/** + JS emulation of `hl.Ref`. JS can't take the address of a local variable: native + bindings taking a Ref are generated as macros that write the result back to the + argument (see hlemu.NativeBind). This box is only used when a Ref is stored. +**/ +class Ref { + var v : T; + public function new( v : T ) { + this.v = v; + } + public inline function get() : T { + return v; + } + public inline function set( v : T ) : Void { + this.v = v; + } +} diff --git a/hide_js/libs/hlemu/StringExt.hx b/hide_js/libs/hlemu/StringExt.hx new file mode 100644 index 000000000..3001d57c6 --- /dev/null +++ b/hide_js/libs/hlemu/StringExt.hx @@ -0,0 +1,14 @@ +package hlemu; + +/** HL String methods used by native code (calls are redirected here by hlemu.Macros) **/ +class StringExt { + /** null-terminated UTF-8 copy in native memory. Like small GC allocations in HL, it is never freed. **/ + public static function toUtf8( s : Dynamic ) : Dynamic { + if( js.Syntax.typeof(s) != "string" ) return s.toUtf8(); + var b = haxe.io.Bytes.ofString(s); + var out = new Bytes(b.length + 1); + out.blit(0, b, 0, b.length); + out.setUI8(b.length, 0); + return out; + } +} diff --git a/hide_js/libs/hlemu/Struct.hx b/hide_js/libs/hlemu/Struct.hx new file mode 100644 index 000000000..93601b43a --- /dev/null +++ b/hide_js/libs/hlemu/Struct.hx @@ -0,0 +1,72 @@ +package hlemu; + +import hlemu.Native as N; + +/** + Runtime helpers for the code generated by hlemu.Macros. +**/ +class Struct { + + /** allocates the memory of a new struct : a zeroed ArrayBuffer with a known address **/ + public static function alloc( s : Dynamic, size : Int ) : Void { + var buf = new js.lib.ArrayBuffer(size > 0 ? size : 1); + s.__v = new js.lib.DataView(buf); + s.__o = 0; + s.__a = N.mem_address(buf); + } + + public static inline function getNum64( v : js.lib.DataView, o : Int ) : Float { + return v.getUint32(o, true) + v.getInt32(o + 4, true) * 4294967296.; + } + + public static inline function setNum64( v : js.lib.DataView, o : Int, x : Float ) : Void { + var high = Math.floor(x / 4294967296.); + v.setUint32(o, js.Syntax.code("(({0}) >>> 0)", x - high * 4294967296.), true); + v.setInt32(o + 4, Std.int(high), true); + } + + /** native address of a struct or CArray, 0 for null **/ + public static inline function addr( v : Dynamic ) : Float { + return v == null ? 0 : v.__a; + } + + /** native address of a struct or CArray passed to a native function : copies the pending fields to memory first **/ + public static inline function addrSync( v : Dynamic ) : Float { + if( v == null ) return 0; + v.__sync(); + return v.__a; + } + + /** builds an HL varray of pointers / numbers in native memory, to free after the call **/ + public static function toVarray( a : Array ) : Float { + if( a == null ) return 0; + var p = N.mem_alloc(24 + a.length * 8); + N.mem_set_i32(p + 16, a.length); + for( i in 0...a.length ) { + var v : Dynamic = a[i]; + N.mem_set_i64(p + 24 + i * 8, v == null ? 0 : v); + } + return p; + } + + /** reads an HL varray of pointers returned by the addon, and frees it **/ + public static function fromVarray( p : Float ) : Array { + if( p == null || p == 0 ) return null; + var len = N.mem_get_i32(p + 16); + var out = []; + for( i in 0...len ) { + var v = N.mem_get_i64(p + 24 + i * 8); + out.push(v == 0 ? null : v); + } + N.mem_free(p); + return out; + } + + static var tmp : js.lib.Uint16Array; + + /** copies a JS array of 16 bits values into a temporary buffer, valid until the next call **/ + public static function tmpUI16( a : Array ) : hlemu.Bytes { + tmp = new js.lib.Uint16Array(a); + return cast N.mem_address(tmp); + } +} diff --git a/hide_js/libs/hlemu/UI16.hx b/hide_js/libs/hlemu/UI16.hx new file mode 100644 index 000000000..61f3af63f --- /dev/null +++ b/hide_js/libs/hlemu/UI16.hx @@ -0,0 +1,4 @@ +package hlemu; + +/** JS emulation of `hl.UI16` (2 bytes in structs) **/ +@:notNull abstract UI16(Int) to Int from Int {} diff --git a/hide_js/libs/hlemu/UI8.hx b/hide_js/libs/hlemu/UI8.hx new file mode 100644 index 000000000..77522d73c --- /dev/null +++ b/hide_js/libs/hlemu/UI8.hx @@ -0,0 +1,4 @@ +package hlemu; + +/** JS emulation of `hl.UI8` (1 byte in structs) **/ +@:notNull abstract UI8(Int) to Int from Int {} diff --git a/hide_js/libs/jsdx/native/build.bat b/hide_js/libs/jsdx/native/build.bat new file mode 100644 index 000000000..de07b4d43 --- /dev/null +++ b/hide_js/libs/jsdx/native/build.bat @@ -0,0 +1,31 @@ +@echo off +rem Builds bin\jsdx\dx12.node from hldx's dx12.cpp (unmodified) and the hl.h shim. +rem Uses HASHLINK_SRC (the hashlink "src" directory) for the hldx sources (libs\directx) +rem and the shader compiler DLLs (dxcompiler.dll, dxil.dll in the hashlink directory). +rem Requires Visual Studio 2026 and "npm install" in hide_js\libs\jsdx (node-api-headers). +setlocal +set ROOT=%~dp0.. +set OUT=%~dp0..\..\..\..\bin\jsdx +set NODE_API=%~dp0..\node_modules\node-api-headers +if "%HASHLINK_SRC%"=="" ( + echo HASHLINK_SRC is not set : it must point to the hashlink "src" directory + exit /b 1 +) +set HLDX=%HASHLINK_SRC%\libs\directx +set HLBIN=%HASHLINK_SRC%\.. +if not exist "%NODE_API%" ( + echo node-api-headers not found : run "npm install" in %ROOT% + exit /b 1 +) +call "C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvars64.bat" >nul 2>nul || exit /b 1 +cd /d "%ROOT%" +if not exist "%OUT%" mkdir "%OUT%" +if not exist build\obj mkdir build\obj +lib /nologo /def:"%NODE_API%\def\node_api.def" /name:node.exe /machine:x64 /out:build\obj\node.lib || exit /b 1 +rem /Zi /DEBUG : dx12.pdb next to the addon, to read the minidumps of a crash +cl /nologo /std:c++17 /O2 /Zi /EHsc /MT /LD /W1 /Inative /I"%NODE_API%\include" /I"%HLDX%" native\dx12js.cpp /Fobuild\obj\ /Fdbuild\obj\ /Fe:"%OUT%\dx12.node" ^ + /link /DEBUG /OPT:REF /OPT:ICF /IMPLIB:build\obj\dx12.lib build\obj\node.lib d3d12.lib dxgi.lib dxguid.lib dxcompiler.lib delayimp.lib /DELAYLOAD:node.exe || exit /b 1 +rem the shader compiler DLLs are loaded from the addon directory +copy /y "%HLBIN%\dxcompiler.dll" "%OUT%\" >nul +copy /y "%HLBIN%\dxil.dll" "%OUT%\" >nul +echo OK diff --git a/hide_js/libs/jsdx/native/dx12js.cpp b/hide_js/libs/jsdx/native/dx12js.cpp new file mode 100644 index 000000000..6ac428a73 --- /dev/null +++ b/hide_js/libs/jsdx/native/dx12js.cpp @@ -0,0 +1,334 @@ +// Node-API build of hldx's dx12.cpp, compiled unmodified against the hl.h shim. +// +// On top of the original primitives: +// - the swapchain is emulated with shared textures, displayed by the page +// (resize, get_back_buffer, get_current_back_buffer_index, command_queue_present) +// - js_* : access to the shared back buffers for the page +// - mem_* : raw memory access, used by hlemu.Bytes / structs on the JS side + +#include "dx12.cpp" +#include + +#include + +hl_type hlt_bytes, hlt_i32, hlt_bool, hlt_dyn, hlt_abstract; + +// Electron exports Node-API from its own executable: redirect "node.exe" imports there +static FARPROC WINAPI loadExeHook( unsigned int event, DelayLoadInfo *info ) { + if( event != dliNotePreLoadLibrary || _stricmp(info->szDll, "node.exe") != 0 ) return nullptr; + return (FARPROC)GetModuleHandle(nullptr); +} +decltype(__pfnDliNotifyHook2) __pfnDliNotifyHook2 = loadExeHook; + +// ---- swapchain emulation + +#define JS_MAX_BUFFERS 8 + +static struct { + int count; + int index; + int presented; + int presentCount; + ID3D12Resource *buffers[JS_MAX_BUFFERS]; + HANDLE handles[JS_MAX_BUFFERS]; + ID3D12Fence *fence; + UINT64 fenceValue; + HANDLE event; +} js_swap = {}; + +static void js_release_buffers() { + for( int i = 0; i < js_swap.count; i++ ) { + if( js_swap.buffers[i] ) js_swap.buffers[i]->Release(); + if( js_swap.handles[i] ) CloseHandle(js_swap.handles[i]); + js_swap.buffers[i] = NULL; + js_swap.handles[i] = NULL; + } + js_swap.count = 0; +} + +static void js_resize( ID3D12CommandQueue *directQueue, int width, int height, int buffer_count, DXGI_FORMAT format ) { + dx_driver *drv = static_driver; + if( buffer_count > JS_MAX_BUFFERS ) hl_error("too many back buffers"); + js_release_buffers(); + if( !js_swap.fence ) { + CHKERR(drv->device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&js_swap.fence))); + js_swap.event = CreateEvent(NULL, FALSE, FALSE, NULL); + } + D3D12_HEAP_PROPERTIES heap = {}; + heap.Type = D3D12_HEAP_TYPE_DEFAULT; + D3D12_RESOURCE_DESC desc = {}; + desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + desc.Width = width; + desc.Height = height; + desc.DepthOrArraySize = 1; + desc.MipLevels = 1; + desc.Format = format; + desc.SampleDesc.Count = 1; + // simultaneous access: the texture stays in the COMMON layout (== PRESENT), readable + // by Chromium's D3D11 device without a cross-API barrier + desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET | D3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESS; + for( int i = 0; i < buffer_count; i++ ) { + CHKERR(drv->device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_SHARED, &desc, D3D12_RESOURCE_STATE_COMMON, NULL, IID_PPV_ARGS(&js_swap.buffers[i]))); + CHKERR(drv->device->CreateSharedHandle(js_swap.buffers[i], NULL, GENERIC_ALL, NULL, &js_swap.handles[i])); + js_swap.buffers[i]->SetName(L"JS_BACKBUFFER"); + } + js_swap.count = buffer_count; + js_swap.index = 0; + js_swap.presented = -1; +} + +// like IDXGISwapChain::GetBuffer, the caller owns a reference +static ID3D12Resource *js_get_back_buffer( int index ) { + if( index < 0 || index >= js_swap.count ) return NULL; + js_swap.buffers[index]->AddRef(); + return js_swap.buffers[index]; +} + +static int js_get_current_back_buffer_index() { + return js_swap.index; +} + +// No shared fence with Chromium: wait for the GPU so that the page can display the +// buffer as soon as present returns. +static void js_command_queue_present( ID3D12CommandQueue *q, bool vsync ) { + CHKERR(q->Signal(js_swap.fence, ++js_swap.fenceValue)); + if( js_swap.fence->GetCompletedValue() < js_swap.fenceValue ) { + js_swap.fence->SetEventOnCompletion(js_swap.fenceValue, js_swap.event); + WaitForSingleObject(js_swap.event, INFINITE); + } + js_swap.presented = js_swap.index; + js_swap.presentCount++; + js_swap.index = (js_swap.index + 1) % js_swap.count; +} + +static hl_shim::Register js_reg_resize("resize", hl_shim::wrap<&js_resize>()); +static hl_shim::Register js_reg_get_back_buffer("get_back_buffer", hl_shim::wrap<&js_get_back_buffer>()); +static hl_shim::Register js_reg_get_current_back_buffer_index("get_current_back_buffer_index", hl_shim::wrap<&js_get_current_back_buffer_index>()); +static hl_shim::Register js_reg_command_queue_present("command_queue_present", hl_shim::wrap<&js_command_queue_present>()); + +// ---- page access to the back buffers + +static int js_back_buffer_count() { return js_swap.count; } +static HANDLE js_back_buffer_handle( int index ) { return index >= 0 && index < js_swap.count ? js_swap.handles[index] : NULL; } +static int js_presented_buffer() { return js_swap.presented; } +static int js_present_count() { return js_swap.presentCount; } + +// The high performance GPU (as Chromium takes it with force_high_performance_gpu) : its index in +// list_devices (the hardware adapters in the order of DXGI), -1 when it is unknown. The page passes +// its name to the driver, so that the addon and Chromium share their textures on the same GPU. +static int js_high_performance_device() { + IDXGIFactory6 *factory = NULL; + if( FAILED(CreateDXGIFactory2(0, IID_PPV_ARGS(&factory))) ) return -1; + IDXGIAdapter1 *adapter = NULL; + int found = -1; + if( SUCCEEDED(factory->EnumAdapterByGpuPreference(0, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(&adapter))) ) { + DXGI_ADAPTER_DESC1 best; + adapter->GetDesc1(&best); + adapter->Release(); + UINT index = 0; + int write = 0; + while( found < 0 && factory->EnumAdapters1(index++, &adapter) != DXGI_ERROR_NOT_FOUND ) { + DXGI_ADAPTER_DESC1 desc; + adapter->GetDesc1(&desc); + if( (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) == 0 ) { + if( desc.AdapterLuid.LowPart == best.AdapterLuid.LowPart && desc.AdapterLuid.HighPart == best.AdapterLuid.HighPart ) found = write; + write++; + } + adapter->Release(); + } + } + factory->Release(); + return found; +} + +static hl_shim::Register js_reg_high_performance_device("js_high_performance_device", hl_shim::wrap<&js_high_performance_device>()); +static hl_shim::Register js_reg_back_buffer_count("js_back_buffer_count", hl_shim::wrap<&js_back_buffer_count>()); +static hl_shim::Register js_reg_back_buffer_handle("js_back_buffer_handle", hl_shim::wrap<&js_back_buffer_handle>()); +static hl_shim::Register js_reg_presented_buffer("js_presented_buffer", hl_shim::wrap<&js_presented_buffer>()); +static hl_shim::Register js_reg_present_count("js_present_count", hl_shim::wrap<&js_present_count>()); + +// ---- raw memory + +static void *mem_alloc( int size ) { return calloc(1, size > 0 ? size : 1); } +static void mem_free( void *p ) { free(p); } +static void mem_copy( vbyte *dst, vbyte *src, int size ) { memmove(dst, src, size); } +static void mem_fill( vbyte *dst, int value, int size ) { memset(dst, value, size); } +static int mem_get_ui8( vbyte *p ) { return *p; } +static void mem_set_ui8( vbyte *p, int v ) { *p = (vbyte)v; } +static int mem_get_ui16( vbyte *p ) { return *(unsigned short*)p; } +static void mem_set_ui16( vbyte *p, int v ) { *(unsigned short*)p = (unsigned short)v; } +static int mem_get_i32( vbyte *p ) { return *(int*)p; } +static void mem_set_i32( vbyte *p, int v ) { *(int*)p = v; } +static double mem_get_f32( vbyte *p ) { return *(float*)p; } +static void mem_set_f32( vbyte *p, double v ) { *(float*)p = (float)v; } +static double mem_get_f64( vbyte *p ) { return *(double*)p; } +static void mem_set_f64( vbyte *p, double v ) { *(double*)p = v; } +static int64 mem_get_i64( vbyte *p ) { return *(int64*)p; } +static void mem_set_i64( vbyte *p, int64 v ) { *(int64*)p = v; } +static int mem_ustrlen( uchar *p ) { return ustrlen(p); } + +#define MEM_PRIM(name) static hl_shim::Register mem_reg_##name(#name, hl_shim::wrap<&name>()); +MEM_PRIM(mem_alloc) +MEM_PRIM(mem_free) +MEM_PRIM(mem_copy) +MEM_PRIM(mem_fill) +MEM_PRIM(mem_get_ui8) +MEM_PRIM(mem_set_ui8) +MEM_PRIM(mem_get_ui16) +MEM_PRIM(mem_set_ui16) +MEM_PRIM(mem_get_i32) +MEM_PRIM(mem_set_i32) +MEM_PRIM(mem_get_f32) +MEM_PRIM(mem_set_f32) +MEM_PRIM(mem_get_f64) +MEM_PRIM(mem_set_f64) +MEM_PRIM(mem_get_i64) +MEM_PRIM(mem_set_i64) +MEM_PRIM(mem_ustrlen) + +// mem_address(ArrayBuffer | TypedArray | DataView) : native address of the data. +// V8 never moves an ArrayBuffer backing store, the address stays valid while the +// buffer is alive. +static napi_value mem_address( napi_env env, napi_callback_info info ) { + size_t argc = 1; + napi_value argv[1]; + napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); + void *data = NULL; + bool is; + if( napi_is_arraybuffer(env, argv[0], &is) == napi_ok && is ) { + size_t len; + napi_get_arraybuffer_info(env, argv[0], &data, &len); + } else if( napi_is_typedarray(env, argv[0], &is) == napi_ok && is ) { + napi_typedarray_type type; + size_t len, offset; + napi_value ab; + napi_get_typedarray_info(env, argv[0], &type, &len, &data, &ab, &offset); + } else if( napi_is_dataview(env, argv[0], &is) == napi_ok && is ) { + size_t len, offset; + napi_value ab; + napi_get_dataview_info(env, argv[0], &len, &data, &ab, &offset); + } else { + napi_throw_type_error(env, nullptr, "mem_address : ArrayBuffer, TypedArray or DataView expected"); + return nullptr; + } + return hl_shim::toJs(env, data); +} +static hl_shim::Register mem_reg_address("mem_address", &mem_address); + +// ---- descriptor copies checked against the live heaps +// +// A copy from a released heap (or past the end of one) crashes in the GPU driver and takes the +// tool down. The live heaps are kept with their CPU range : a bad copy throws a JS exception +// instead, and is logged with its JS stack in %TEMP%\hide-dx12.log. + +struct js_heap_range { uintptr_t start; size_t bytes; }; +static std::map js_heaps; +static std::map js_heap_starts; + +static ID3D12DescriptorHeap *js_descriptor_heap_create( D3D12_DESCRIPTOR_HEAP_DESC *desc ) { + ID3D12DescriptorHeap *heap = HL_NAME(descriptor_heap_create)(desc); + if( heap ) { + uintptr_t start = heap->GetCPUDescriptorHandleForHeapStart().ptr; + size_t bytes = (size_t)desc->NumDescriptors * static_driver->device->GetDescriptorHandleIncrementSize(desc->Type); + js_heaps[heap] = { start, bytes }; + js_heap_starts[start] = bytes; + } + return heap; +} + +// a JS error, logged with its stack in %TEMP%\hide-dx12.log +static napi_value js_log( napi_env env, const char *msg ) { + napi_value text, err, stack; + napi_create_string_utf8(env, msg, NAPI_AUTO_LENGTH, &text); + napi_create_error(env, nullptr, text, &err); + char trace[12288] = ""; + size_t len; + if( napi_get_named_property(env, err, "stack", &stack) == napi_ok ) + napi_get_value_string_utf8(env, stack, trace, sizeof(trace), &len); + char file[MAX_PATH]; + DWORD n = GetTempPathA(MAX_PATH, file); + FILE *f = NULL; + if( n > 0 && n < MAX_PATH - 32 && strcat_s(file, "hide-dx12.log") == 0 && fopen_s(&f, file, "a") == 0 && f ) { + SYSTEMTIME t; + GetLocalTime(&t); + fprintf(f, "%04d-%02d-%02d %02d:%02d:%02d.%03d %s\n\n", t.wYear, t.wMonth, t.wDay, t.wHour, t.wMinute, t.wSecond, t.wMilliseconds, trace); + fclose(f); + } + return err; +} + +static napi_value js_resource_release( napi_env env, napi_callback_info info ) { + size_t argc = 1; + napi_value argv[1]; + napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); + IUnknown *res = hl_shim::fromJs(env, argv[0]); + if( res == NULL ) return nullptr; + auto it = js_heaps.find((ID3D12DescriptorHeap*)res); + if( res->Release() == 0 && it != js_heaps.end() ) { + if( getenv("GS_DX12_TRACE") ) { + char msg[160]; + snprintf(msg, sizeof(msg), "heap released 0x%llx (%zu bytes)", (unsigned long long)it->second.start, it->second.bytes); + js_log(env, msg); + } + js_heap_starts.erase(it->second.start); + js_heaps.erase(it); + } + return nullptr; +} + +static bool js_heap_contains( uintptr_t p, size_t bytes ) { + auto it = js_heap_starts.upper_bound(p); + if( it == js_heap_starts.begin() ) return false; + --it; + return p + bytes <= it->first + it->second; +} + +static napi_value js_copy_descriptors_simple( napi_env env, napi_callback_info info ) { + size_t argc = 4; + napi_value argv[4]; + napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); + int count = hl_shim::fromJs(env, argv[0]); + D3D12_CPU_DESCRIPTOR_HANDLE dst = hl_shim::fromJs(env, argv[1]); + D3D12_CPU_DESCRIPTOR_HANDLE src = hl_shim::fromJs(env, argv[2]); + D3D12_DESCRIPTOR_HEAP_TYPE type = hl_shim::fromJs(env, argv[3]); + size_t bytes = (size_t)count * static_driver->device->GetDescriptorHandleIncrementSize(type); + const char *bad = !js_heap_contains(src.ptr, bytes) ? "source" : !js_heap_contains(dst.ptr, bytes) ? "destination" : NULL; + if( bad == NULL ) { + static_driver->device->CopyDescriptorsSimple(count, dst, src, type); + return nullptr; + } + char msg[2048]; + int pos = snprintf(msg, sizeof(msg), "copyDescriptorsSimple : the %s is not in a live descriptor heap (%d descriptors of type %d, src 0x%llx, dst 0x%llx)", bad, count, (int)type, (unsigned long long)src.ptr, (unsigned long long)dst.ptr); + if( getenv("GS_DX12_TRACE") ) + for( auto &h : js_heap_starts ) + if( pos < (int)sizeof(msg) - 64 ) pos += snprintf(msg + pos, sizeof(msg) - pos, "\n live 0x%llx +%zu", (unsigned long long)h.first, h.second); + napi_throw(env, js_log(env, msg)); + return nullptr; +} + +static hl_shim::Register js_reg_descriptor_heap_create("descriptor_heap_create", hl_shim::wrap<&js_descriptor_heap_create>()); +static hl_shim::Register js_reg_resource_release("resource_release", &js_resource_release); +static hl_shim::Register js_reg_copy_descriptors_simple("copy_descriptors_simple", &js_copy_descriptors_simple); + +// dxcompiler.dll loads dxil.dll (shader signing) by name, from the executable directory: +// preload it from the addon directory, otherwise unsigned shaders make PSO creation fail +static void js_preload_dxil() { + HMODULE self = NULL; + GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (LPCWSTR)&js_preload_dxil, &self); + wchar_t path[MAX_PATH]; + DWORD len = GetModuleFileNameW(self, path, MAX_PATH); + while( len > 0 && path[len - 1] != L'\\' && path[len - 1] != L'/' ) len--; + path[len] = 0; + wcscat_s(path, L"dxil.dll"); + LoadLibraryW(path); +} + +NAPI_MODULE_INIT() { + js_preload_dxil(); + for( auto &p : hl_shim::registry() ) { + napi_value f; + napi_create_function(env, p.first.c_str(), NAPI_AUTO_LENGTH, p.second, nullptr, &f); + napi_set_named_property(env, exports, p.first.c_str(), f); + } + return exports; +} diff --git a/hide_js/libs/jsdx/native/gen-dx12-hx.js b/hide_js/libs/jsdx/native/gen-dx12-hx.js new file mode 100644 index 000000000..1fd4252e3 --- /dev/null +++ b/hide_js/libs/jsdx/native/gen-dx12-hx.js @@ -0,0 +1,179 @@ +// Generates lib/dx/Dx12.hx (JS) from hldx's dx/Dx12.hx (HL). +// +// The API stays identical: only the bodies that use HashLink internals (String.bytes, +// haxe.io.Bytes internals, window handles) are replaced. Every replacement must match +// exactly, so that a change in hldx is noticed. +// +// usage: node native/gen-dx12-hx.js [path/to/hldx] (default : $HASHLINK_SRC/libs/directx) +const fs = require('fs'); +const path = require('path'); + +if( !process.argv[2] && !process.env.HASHLINK_SRC ) throw new Error("HASHLINK_SRC is not set : it must point to the hashlink src directory"); +const hldx = process.argv[2] || path.join(process.env.HASHLINK_SRC, 'libs/directx'); +const src = path.join(hldx, 'dx/Dx12.hx'); +const dst = path.join(__dirname, '../../dx/Dx12.hx'); +let s = fs.readFileSync(src, 'utf8').replace(/\r\n/g, '\n'); + +function rep(from, to) { + const n = s.split(from).length - 1; + if (n !== 1) throw new Error(`expected exactly one match (found ${n}) for:\n${from}`); + s = s.replace(from, () => to); +} + +// String -> UTF-16 native copy +rep(` public inline function setName( name : String ) { + set_name(@:privateAccess name.bytes); + }`, ` public inline function setName( name : String ) { + var b = hl.Bytes.ofUcs2(name); + set_name(b); + b.free(); + }`); + +// display the emulated swapchain after present +rep(` public function present( vsync : Bool ) {}`, ` public function present( vsync : Bool ) { + present_native(vsync); + dx.Display.present(); + } + @:hlNative("dx12","command_queue_present") + function present_native( vsync : Bool ) {}`); + +// GPU addresses are JS numbers (exact below 2^53) +rep(`abstract Address(Int64) from Int64 { + + public var value(get,never) : Int64; + + public inline function new( v : Int64 ) { + this = v; + } + + inline function get_value() return this; + + public inline function offset( delta : Int ) : Address { + return cast this + delta; + } +}`, `abstract Address(hl.I64) { + + public var value(get,never) : Int64; + + public inline function new( v : Int64 ) { + this = hl.I64.ofInt64(v); + } + + inline function get_value() return hl.I64.toInt64(this); + + public inline function offset( delta : Int ) : Address { + return cast ((this : Float) + delta); + } + + @:from static inline function fromInt64( v : Int64 ) : Address { + return new Address(v); + } +}`); + +rep(` public function compile( source : String, profile : String, args : Array ) : haxe.io.Bytes { + var outLen = 0; + var nargs = new hl.NativeArray(args.length); + for( i in 0...args.length ) + nargs[i] = @:privateAccess args[i].bytes; + /* + Compiling source can trigger a validation error if DXCOMPILER.DLL is missing + */ + var bytes = do_compile(cast this, @:privateAccess source.bytes, @:privateAccess profile.bytes, nargs, outLen); + return @:privateAccess new haxe.io.Bytes(bytes, outLen); + }`, ` public function compile( source : String, profile : String, args : Array ) : haxe.io.Bytes { + var outLen = new hl.Ref(0); + var nargs = new hl.NativeArray(args.length); + for( i in 0...args.length ) + nargs[i] = hl.Bytes.ofUcs2(args[i]); + var src = hl.Bytes.ofUcs2(source), prof = hl.Bytes.ofUcs2(profile); + function free() { + src.free(); + prof.free(); + for( b in nargs ) b.free(); + } + /* + Compiling source can trigger a validation error if DXCOMPILER.DLL is missing + */ + var bytes = try do_compile(cast this, src, prof, nargs, outLen) catch( e : Dynamic ) { free(); throw e; } + free(); + var out = bytes.toBytes(outLen.get()); + bytes.free(); + return out; + }`); + +rep(` public static function create( win : Window, flags : DriverInitFlags, ?deviceName : String ) { + return dxCreate(@:privateAccess win.win, flags, deviceName == null ? null : @:privateAccess deviceName.bytes); + }`, ` public static function create( win : Window, flags : DriverInitFlags, ?deviceName : String ) { + // no name : the high performance GPU, as Chromium takes it (force_high_performance_gpu) to share the back buffers + // (an addon older than the package, a client not updated yet, has no js_high_performance_device : the default GPU) + if( deviceName == null && js.Syntax.code("typeof __dx12.js_high_performance_device == 'function'") ) { + var index : Int = js.Syntax.code("__dx12.js_high_performance_device()"); + if( index >= 0 ) deviceName = listDevices()[index]; + } + var name = hl.Bytes.ofUcs2(deviceName); + var d = dxCreate(null, flags, name); + if( name != null ) name.free(); + dx.Display.init(win); + return d; + }`); + +rep(` public static function resize( directQueue : CommandQueue, width : Int, height : Int, bufferCount : Int, format : DxgiFormat ) { + }`, ` public static function resize( directQueue : CommandQueue, width : Int, height : Int, bufferCount : Int, format : DxgiFormat ) { + resizeNative(directQueue, width, height, bufferCount, format); + dx.Display.resize(width, height); + } + + @:hlNative("dx12", "resize") + static function resizeNative( directQueue : CommandQueue, width : Int, height : Int, bufferCount : Int, format : DxgiFormat ) { + }`); + +// see Dx12Macros : JS can't take the address of the size local variable +rep(` public static function serializeRootSignature( desc : RootSignatureDesc, version : Int, size : hl.Ref ) : hl.Bytes { + return null; + }`, ` public static macro function serializeRootSignature( desc, version, size ) { + return dx.Dx12Macros.serializeRootSignature(desc, version, size); + } + + @:hlNative("dx12", "serialize_root_signature") + static function serializeRootSignatureRef( desc : RootSignatureDesc, version : Int, size : hl.Ref ) : hl.Bytes { + return null; + }`); + +// device names are malloc'ed UTF-16 strings +rep(` return @:privateAccess String.fromUCS2(dxGetDeviceName());`, ` return dxGetDeviceName().toUcs2();`); +rep(` out.push(@:privateAccess String.fromUCS2(arr[i]));`, ` out.push(arr[i].toUcs2());`); + +// GPU crash dumps call back into Haxe : not supported in JS +rep(` @:hlNative("dx12", "set_gpu_crash_handler") + public static function setGpuCrashHandler( f : (name : hl.Bytes, bytes : hl.Bytes, size : Int, lastFile : Bool) -> Void ) { + }`, ` public static function setGpuCrashHandler( f : (name : hl.Bytes, bytes : hl.Bytes, size : Int, lastFile : Bool) -> Void ) { + return; + }`); + +// the class body is typed in the macro context too (for serializeRootSignature) +rep(`package dx; +`, `package dx; + +// GENERATED by jsdx/native/gen-dx12-hx.js from hldx's dx/Dx12.hx : do not edit. +// JS version of the hldx DirectX 12 API, implemented by the dx12 Node-API addon. + +#if !macro +`); +s = s.replace(/\n*$/, ` + +#else + +class Dx12 { + public static macro function serializeRootSignature( desc, version, size ) { + return dx.Dx12Macros.serializeRootSignature(desc, version, size); + } +} + +#end +`); + +// --remap hl:hlemu loses constant type parameters (Haxe 5 preview) : use hlemu directly +s = s.split('hl.Abstract<').join('hlemu.Abstract<'); + +fs.writeFileSync(dst, s); +console.log('written', dst); diff --git a/hide_js/libs/jsdx/native/hl.h b/hide_js/libs/jsdx/native/hl.h new file mode 100644 index 000000000..b358684fa --- /dev/null +++ b/hide_js/libs/jsdx/native/hl.h @@ -0,0 +1,168 @@ +// Minimal replacement of HashLink's hl.h, so that hldx's dx12.cpp compiles unmodified +// as a Node-API addon. Only what dx12.cpp uses is provided. +// +// Every DEFINE_PRIM registers its C function in a table; napi_prim.h generates the +// JS wrapper from the C signature: numbers for integers, floats, enums, int64 and +// pointers (all addresses are passed as JS numbers, exact below 2^53). +#ifndef HL_SHIM_H +#define HL_SHIM_H + +#define HL_WIN +#define HL_WIN_DESKTOP +#define HL_64 +#define HL_VS + +#define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#include +#include +#include +#include + +typedef unsigned char vbyte; +typedef wchar_t uchar; +typedef long long int64; +typedef unsigned long long uint64; + +#define USTR(str) L##str + +// Haxe types are never inspected by dx12.cpp: they only have to exist +typedef struct hl_type { int kind; } hl_type; +extern hl_type hlt_bytes, hlt_i32, hlt_bool, hlt_dyn, hlt_abstract; + +typedef struct vdynamic { + hl_type *t; + union { + bool b; + int i; + int64 i64; + double d; + void *ptr; + } v; +} vdynamic; + +// same layout as HL: the data follows the header +typedef struct varray { + hl_type *t; + hl_type *at; + int size; + int __pad; +} varray; + +#define hl_aptr(a,t) ((t*)(((varray*)(a))+1)) + +typedef struct vclosure { + hl_type *t; + void *fun; + int hasValue; + void *value; +} vclosure; + +#define HL_PRIM +#define HL_API +#define EXPORT + +// ---- memory: dx12.cpp results that HL would GC are malloc'ed here. +// The JS side copies what it needs and frees them (see hlemu.Bytes). + +static inline void *hl_gc_alloc_raw( int size ) { return calloc(1, size); } + +static inline vbyte *hl_copy_bytes( const vbyte *b, int size ) { + vbyte *out = (vbyte*)malloc(size ? size : 1); + memcpy(out, b, size); + return out; +} + +static inline varray *hl_alloc_array( hl_type *t, int size ) { + varray *a = (varray*)calloc(1, sizeof(varray) + sizeof(void*) * size); + a->t = &hlt_dyn; + a->at = t; + a->size = size; + return a; +} + +static inline int ustrlen( const uchar *s ) { return (int)wcslen(s); } + +static inline uchar *ustrdup( const uchar *s ) { + int len = ustrlen(s) + 1; + uchar *out = (uchar*)malloc(len * sizeof(uchar)); + memcpy(out, s, len * sizeof(uchar)); + return out; +} + +static inline uchar *hl_to_utf16( const char *str ) { + int len = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + uchar *out = (uchar*)malloc(len * sizeof(uchar)); + MultiByteToWideChar(CP_UTF8, 0, str, -1, out, len); + return out; +} + +// ---- errors: thrown as C++ exceptions, turned into JS exceptions by the wrapper + +struct hl_shim_error { + char msg[8192]; +}; + +// dx12.cpp prints the D3D12 debug layer messages with printf : the renderer process has +// no console, they are kept and appended to the next error +static char hl_shim_log[6144]; +static size_t hl_shim_log_len = 0; + +static inline int hl_shim_printf( const char *fmt, ... ) { + char tmp[1024]; + va_list args; + va_start(args, fmt); + int n = vsnprintf(tmp, sizeof(tmp), fmt, args); + va_end(args); + size_t len = strlen(tmp); + if( hl_shim_log_len + len >= sizeof(hl_shim_log) ) hl_shim_log_len = 0; + memcpy(hl_shim_log + hl_shim_log_len, tmp, len + 1); + hl_shim_log_len += len; + return n; +} + +// HL formats errors as UTF-16, "%s" arguments are uchar* (MSVC wide printf semantics) +static inline void hl_shim_throw( const char *fmt, ... ) { + wchar_t wfmt[256], wmsg[1024]; + MultiByteToWideChar(CP_UTF8, 0, fmt, -1, wfmt, 256); + va_list args; + va_start(args, fmt); + _vsnwprintf_s(wmsg, 1024, _TRUNCATE, wfmt, args); + va_end(args); + hl_shim_error e; + int len = WideCharToMultiByte(CP_UTF8, 0, wmsg, -1, e.msg, sizeof(e.msg), NULL, NULL); + if( hl_shim_log_len > 0 && len > 0 ) { + snprintf(e.msg + len - 1, sizeof(e.msg) - len, "\n%s", hl_shim_log); + hl_shim_log_len = 0; + } + throw e; +} + +#define printf hl_shim_printf + +#define hl_error(msg, ...) hl_shim_throw(msg, ##__VA_ARGS__) + +// ---- threads / GC: nothing to do, JS calls are on one thread + +static inline void hl_blocking( bool b ) {} +static inline void hl_add_root( void *r ) {} +static inline void *hl_get_thread() { return (void*)1; } +static inline void hl_register_thread( void *ctx ) {} +static inline void hl_unregister_thread() {} +// GPU crash dumps (Aftermath) call back into Haxe: not supported in JS +static inline vdynamic *hl_dyn_call( vclosure *c, vdynamic **args, int nargs ) { return NULL; } + +// ---- primitives registration + +#include "napi_prim.h" + +#define DEFINE_PRIM(t, name, args) static hl_shim::Register HL_SHIM_CAT(__prim_, name)(#name, hl_shim::wrap<&HL_NAME(name)>()); +#define HL_SHIM_CAT2(a,b) a##b +#define HL_SHIM_CAT(a,b) HL_SHIM_CAT2(a,b) + +#endif diff --git a/hide_js/libs/jsdx/native/napi_prim.h b/hide_js/libs/jsdx/native/napi_prim.h new file mode 100644 index 000000000..18fb00472 --- /dev/null +++ b/hide_js/libs/jsdx/native/napi_prim.h @@ -0,0 +1,129 @@ +// Generates Node-API wrappers from the C signature of HashLink primitives. +// +// JS -> C : bool, integers and enums (int32 semantics, uint32 wraps), int64 and +// pointers (JS numbers; null/undefined -> 0), float/double, and 8-byte +// structs passed by value (descriptor handles, _I64 in HL). +// C -> JS : the same, null pointers become null. +#ifndef HL_SHIM_NAPI_PRIM_H +#define HL_SHIM_NAPI_PRIM_H + +#define NAPI_VERSION 8 +#include +#include +#include +#include +#include +#include + +namespace hl_shim { + +typedef napi_value (*Callback)( napi_env, napi_callback_info ); + +inline std::map ®istry() { + static std::map r; + return r; +} + +// a later registration under the same name replaces the previous one +struct Register { + Register( const char *name, Callback cb ) { registry()[name] = cb; } +}; + +template inline T fromJs( napi_env env, napi_value v ) { + if constexpr( std::is_same_v ) { + bool b = false; + if( napi_get_value_bool(env, v, &b) != napi_ok ) { + double d = 0; + napi_get_value_double(env, v, &d); + b = d != 0; + } + return b; + } else if constexpr( std::is_floating_point_v ) { + double d = 0; + napi_get_value_double(env, v, &d); + return (T)d; + } else if constexpr( std::is_pointer_v ) { + double d = 0; + napi_get_value_double(env, v, &d); + return reinterpret_cast((uintptr_t)(uint64_t)d); + } else if constexpr( std::is_enum_v || std::is_integral_v ) { + if constexpr( sizeof(T) == 8 ) { + double d = 0; + napi_get_value_double(env, v, &d); + return (T)(int64_t)d; + } else { + int32_t i = 0; + napi_get_value_int32(env, v, &i); + return (T)i; + } + } else if constexpr( std::is_class_v && std::is_trivially_copyable_v && sizeof(T) == 8 ) { + double d = 0; + napi_get_value_double(env, v, &d); + int64_t i = (int64_t)d; + T out; + memcpy(&out, &i, 8); + return out; + } else { + static_assert(sizeof(T) == 0, "unsupported primitive argument type"); + } +} + +template inline napi_value toJs( napi_env env, T v ) { + napi_value r; + if constexpr( std::is_same_v ) + napi_get_boolean(env, v, &r); + else if constexpr( std::is_floating_point_v ) + napi_create_double(env, (double)v, &r); + else if constexpr( std::is_pointer_v ) { + if( v == nullptr ) + napi_get_null(env, &r); + else + napi_create_double(env, (double)(uintptr_t)v, &r); + } else if constexpr( std::is_class_v ) { + static_assert(std::is_trivially_copyable_v && sizeof(T) == 8, "unsupported primitive return type"); + int64_t i; + memcpy(&i, &v, 8); + napi_create_double(env, (double)i, &r); + } else if constexpr( sizeof(T) == 8 ) + napi_create_double(env, (double)(int64_t)v, &r); + else if constexpr( std::is_unsigned_v ) + napi_create_uint32(env, (uint32_t)v, &r); + else + napi_create_int32(env, (int32_t)v, &r); + return r; +} + +template struct Prim; + +template struct Prim { + template static napi_value invoke( napi_env env, napi_value *argv, std::index_sequence ) { + if constexpr( std::is_void_v ) { + F(fromJs(env, argv[I])...); + return nullptr; + } else + return toJs(env, F(fromJs(env, argv[I])...)); + } + static napi_value call( napi_env env, napi_callback_info info ) { + constexpr size_t N = sizeof...(A); + size_t argc = N; + napi_value argv[N > 0 ? N : 1]; + napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); + if( argc < N ) { + napi_value undef; + napi_get_undefined(env, &undef); + for( size_t i = argc; i < N; i++ ) argv[i] = undef; + } + try { + return invoke(env, argv, std::index_sequence_for{}); + } catch( hl_shim_error &e ) { + napi_throw_error(env, nullptr, e.msg); + return nullptr; + } + } +}; + +template constexpr Callback wrap() { return &Prim::call; } + +} + +#endif diff --git a/hide_js/libs/jsdx/package-lock.json b/hide_js/libs/jsdx/package-lock.json new file mode 100644 index 000000000..9a9e1690c --- /dev/null +++ b/hide_js/libs/jsdx/package-lock.json @@ -0,0 +1,20 @@ +{ + "name": "hide-jsdx", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hide-jsdx", + "devDependencies": { + "node-api-headers": "^1.5.0" + } + }, + "node_modules/node-api-headers": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.9.0.tgz", + "integrity": "sha512-2oNILP4jXwRB4ywnYKjVk1YyJ96n2D4EOVJO6S3oYZ5PtbJrw3Yt9TpAuX3nBLMuzn74rnfGQrv13pS9vC+YiA==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/hide_js/libs/jsdx/package.json b/hide_js/libs/jsdx/package.json new file mode 100644 index 000000000..a8fd1b6f7 --- /dev/null +++ b/hide_js/libs/jsdx/package.json @@ -0,0 +1,12 @@ +{ + "name": "hide-jsdx", + "private": true, + "description": "DirectX 12 for HIDE on Electron : N-API addon built from hldx dx12.cpp", + "scripts": { + "build": "native\\build.bat", + "gen": "node native/gen-dx12-hx.js" + }, + "devDependencies": { + "node-api-headers": "^1.5.0" + } +} From 99dd49393a15a336b3e0ddeaa8ea5e2749578e93 Mon Sep 17 00:00:00 2001 From: ncannasse Date: Sat, 10 Oct 2026 13:28:01 +0200 Subject: [PATCH 003/191] finalised dx12 driver integration --- CLAUDE.md | 5 +- common-hl.hxml | 11 --- common.hxml | 7 -- hide.hxml | 27 ++---- hide/Plugin.hx | 26 +++--- hide_common.hxml | 19 ++++ hide_dx12.hxml | 5 -- hide_hl.hxml | 27 +----- hide_js/hide/ElectronMain.hx | 11 ++- hide_js/hide/comp/Scene.hx | 15 ++++ hide_js/hide/comp/SceneEditor.hx | 2 +- hide_js/libs/dx/Display.hx | 32 ++++--- hide_js/libs/dx/Dx12.hx | 2 +- hide_js/libs/dx/Window.hx | 25 +++--- hide_js/libs/jsdx/native/dx12js.cpp | 110 ++++++++++++++++-------- hide_js/libs/jsdx/native/gen-dx12-hx.js | 2 +- hide_jsdx.hxml | 4 + 17 files changed, 176 insertions(+), 154 deletions(-) delete mode 100644 common-hl.hxml create mode 100644 hide_common.hxml delete mode 100644 hide_dx12.hxml create mode 100644 hide_jsdx.hxml diff --git a/CLAUDE.md b/CLAUDE.md index 48b542264..d706408b3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,8 +5,9 @@ - HIDE runs on Electron. The main process is Haxe code (`hide_js/hide/ElectronMain.hx`, built into `bin/main.js` by the second part of `hide.hxml` (after `--next`)); the renderer is `bin/app.html` + `bin/hide.js`. - Renderer code talks to the main process only through the wrappers in `hide_js/libs/electron/` (`electron.Ipc`, `electron.Window`, `electron.Clipboard`, ...). Main process externs are in `hide_js/libs/electron/main/`. - Keep main process code in Haxe, not hand-written JS. -- `haxe hide.hxml` builds both `bin/hide.js` and `bin/main.js`. The Electron runtime is unpacked in `bin/electron/`. -- DirectX 12 (jsdx) : `haxe hide_dx12.hxml` builds `bin/hide.js` with the heaps DX12Driver (heaps branch `js-dx12`). The HL emulation (`hlemu`) and `dx` API are in `hide_js/libs`; the N-API addon `bin/jsdx/dx12.node` is built by `hide_js/libs/jsdx/native/build.bat` (needs `HASHLINK_SRC` and `npm install` in `hide_js/libs/jsdx`). `dx/Dx12.hx` is generated by `hide_js/libs/jsdx/native/gen-dx12-hx.js`, do not edit it. +- `haxe hide.hxml` builds both `bin/hide.js` (WebGL) and `bin/main.js`; `haxe hide_jsdx.hxml` the same with DX12; `haxe hide_hl.hxml` the HashLink editor. The shared libraries / defines / includes are in `hide_common.hxml`. The Electron runtime is unpacked in `bin/electron/`. +- DirectX 12 (jsdx) : `haxe hide_jsdx.hxml` builds `bin/hide.js` with the heaps DX12Driver (heaps branch `js-dx12`). The HL emulation (`hlemu`) and `dx` API are in `hide_js/libs`; the N-API addon `bin/jsdx/dx12.node` is built by `hide_js/libs/jsdx/native/build.bat` (needs `HASHLINK_SRC` and `npm install` in `hide_js/libs/jsdx`). `dx/Dx12.hx` is generated by `hide_js/libs/jsdx/native/gen-dx12-hx.js`, do not edit it. +- DX12 with several scenes : one `h3d.Engine` per scene (as with WebGL, `multidriver`), all sharing a single `DX12Driver` (`DX12Driver.inst`, never disposed : compiled shaders / pipelines are kept). The driver has a single emulated swapchain : `Scene.doSync` resizes it to the engine size (nothing if unchanged) before rendering, then presents it into the canvas of the current `hxd.Window` (`dx.Window`, `dx.Display`). `DX12Driver` itself is unchanged. The addon has one swapchain per command queue and shares its D3D12 device between the pages of the renderer process (main window, thumbnail generator). ## Running the editor for tests diff --git a/common-hl.hxml b/common-hl.hxml deleted file mode 100644 index b21e74802..000000000 --- a/common-hl.hxml +++ /dev/null @@ -1,11 +0,0 @@ --lib heaps --lib hscript --lib castle --lib hx3compat --lib domkit --lib hashlink -# -D editor -#-D no-deprecation-warnings --D manual_sync_pad --cp hide_hl -# -cp hide_js/libs diff --git a/common.hxml b/common.hxml index 85ca13bb3..547dbba3b 100644 --- a/common.hxml +++ b/common.hxml @@ -1,16 +1,9 @@ --lib hxnodejs -lib heaps -lib hscript -lib castle -lib hx3compat -lib domkit -lib hashlink --D js-classic --D js-unflatten -D hscriptPos --D multidriver --D editor -D no-deprecation-warnings -D manual_sync_pad --cp hide_js --cp hide_js/libs diff --git a/hide.hxml b/hide.hxml index 5765a8ad8..b3ffacd27 100644 --- a/hide.hxml +++ b/hide.hxml @@ -1,27 +1,16 @@ -common.hxml --D hide +hide_common.hxml +-lib hxnodejs +-D js-classic +-D js-unflatten +-D editor +-D multidriver +-cp hide_js +-cp hide_js/libs -js bin/hide.js -main hide.Ide --macro hide.tools.Macros.includeShaderSources() ---macro include("h3d.shader") ---macro include("h3d.anim") ---macro include("h3d.prim") ---macro include("h3d.scene") ---macro include("h3d.pass") ---macro include("h3d.mat") ---macro include("h2d.col") ---macro include("hxd.poly2tri") ---macro include("hxd.clipper") ---macro include("hxd.earcut") ---macro include("hide.view") ---macro include("hide.prefab") ---macro include("hide.tools") ---macro include("hrt") cdb.ConstLoader --dce no --debug - #-D shader_debug_dump diff --git a/hide/Plugin.hx b/hide/Plugin.hx index c3d0a9403..2d7418f11 100644 --- a/hide/Plugin.hx +++ b/hide/Plugin.hx @@ -77,16 +77,10 @@ class Plugin { var hidePath = getLibraryPath("hide"); - var path = if (Context.defined("js")) { - hidePath+"/common.hxml"; - } else if (Context.defined("hl")) { - hidePath+"/common-hl.hxml"; - } - else { + if( !Context.defined("js") && !Context.defined("hl") ) Context.fatalError("Hide plugin only support js or hl targets", Context.currentPos()); - } - for( f in sys.io.File.getContent(path).split("\n") ) { + for( f in sys.io.File.getContent(hidePath+"/common.hxml").split("\n") ) { var f = StringTools.trim(f); if( f == "" ) continue; var pl = f.split(" "); @@ -94,11 +88,6 @@ class Plugin { switch( pl[0] ) { case "-lib": if( value == "heaps" ) continue; - if( value == "hxnodejs" ) { - // should be set with -cp or will conflict with macro code - if( !Context.defined("hxnodejs") ) Context.error("Please add -lib hxnodejs", Context.currentPos()); - continue; - } if ( value == "domkit" ) { // should be set with -lib or will conflict with macro code if (!Context.defined("domkit")) { @@ -118,6 +107,17 @@ class Plugin { default: } } + + if( Context.defined("js") ) { + // should be set with -cp or will conflict with macro code + if( !Context.defined("hxnodejs") ) Context.error("Please add -lib hxnodejs", Context.currentPos()); + for( d in ["js-classic", "js-unflatten", "editor", "multidriver"] ) + C.define(d, "1"); + C.addClassPath(hidePath+"/hide_js"); + C.addClassPath(hidePath+"/hide_js/libs"); + } else + C.addClassPath(hidePath+"/hide_hl"); + for( e in EXCLUDES ) C.exclude(e); } diff --git a/hide_common.hxml b/hide_common.hxml new file mode 100644 index 000000000..0a557df58 --- /dev/null +++ b/hide_common.hxml @@ -0,0 +1,19 @@ +common.hxml +-D hide + +--macro include("h3d.shader") +--macro include("h3d.anim") +--macro include("h3d.prim") +--macro include("h3d.scene") +--macro include("h3d.pass") +--macro include("h3d.mat") +--macro include("h2d.col") +--macro include("hxd.poly2tri") +--macro include("hxd.clipper") +--macro include("hxd.earcut") +--macro include("hide.view") +--macro include("hide.prefab") +--macro include("hide.tools") +--macro include("hrt") +-dce no +-debug diff --git a/hide_dx12.hxml b/hide_dx12.hxml deleted file mode 100644 index 9d1d5a985..000000000 --- a/hide_dx12.hxml +++ /dev/null @@ -1,5 +0,0 @@ -# HIDE with the DirectX 12 driver (jsdx), requires bin/jsdx/dx12.node (see hide_js/libs/jsdx/native/build.bat) --D dx12 ---remap hl:hlemu ---macro hlemu.Macros.init(["dx", "h3d.impl"]) -hide.hxml diff --git a/hide_hl.hxml b/hide_hl.hxml index f4d6b5e5a..1b65d9a0b 100644 --- a/hide_hl.hxml +++ b/hide_hl.hxml @@ -1,16 +1,7 @@ --lib heaps --lib hscript --lib castle --lib hx3compat --lib domkit --lib hashlink +hide_common.hxml -lib hlphysics --D hscriptPos # -D editor # Disabled at the moment because most of the #if editor code of hide depends on js and we don't have it in the hl target --D no-deprecation-warnings --D manual_sync_pad --D hide -D hui -D dx12 -D hl-profile @@ -31,23 +22,7 @@ --macro hide.HlMacros.init() --macro include("hide.ui.comp") --macro include("hide.ui") ---macro include("h3d.shader") ---macro include("h3d.anim") ---macro include("h3d.prim") ---macro include("h3d.scene") ---macro include("h3d.pass") ---macro include("h3d.mat") ---macro include("h2d.col") ---macro include("hxd.poly2tri") ---macro include("hxd.clipper") ---macro include("hxd.earcut") ---macro include("hide.view") ---macro include("hide.prefab") ---macro include("hide.tools") ---macro include("hrt") --macro include("hrt.ui") --dce no --debug -D hl_ver=2.0.0 diff --git a/hide_js/hide/ElectronMain.hx b/hide_js/hide/ElectronMain.hx index f286f4128..abda07f01 100644 --- a/hide_js/hide/ElectronMain.hx +++ b/hide_js/hide/ElectronMain.hx @@ -253,8 +253,11 @@ class ElectronMain { }]; } - static function dialogFilters( exts : Array ) : Dynamic { - return exts == null ? null : [{ name : "Files", extensions : exts }, { name : "All Files", extensions : ["*"] }]; + // Electron rejects null options + static function dialogOptions( o : Dynamic, opts : Dynamic ) : Dynamic { + if( o.defaultPath != null ) opts.defaultPath = o.defaultPath; + if( o.exts != null ) opts.filters = [{ name : "Files", extensions : o.exts }, { name : "All Files", extensions : ["*"] }]; + return opts; } // ---------------- commands @@ -418,11 +421,11 @@ class ElectronMain { h.set("dialog.open", function(wc, o : Dynamic) { var props = [o.directory ? "openDirectory" : "openFile", "createDirectory"]; if( o.multiple ) props.push("multiSelections"); - return Dialog.showOpenDialog(getWin(wc, null), { defaultPath : o.defaultPath, properties : props, filters : dialogFilters(o.exts) }) + return Dialog.showOpenDialog(getWin(wc, null), dialogOptions(o, { properties : props })) .then((r) -> r.canceled || r.filePaths.length == 0 ? null : r.filePaths); }); h.set("dialog.save", function(wc, o : Dynamic) { - return Dialog.showSaveDialog(getWin(wc, null), { defaultPath : o.defaultPath, filters : dialogFilters(o.exts) }) + return Dialog.showSaveDialog(getWin(wc, null), dialogOptions(o, {})) .then((r) -> r.canceled || r.filePath == null || r.filePath == "" ? null : [r.filePath]); }); // window.prompt() is not supported by Electron diff --git a/hide_js/hide/comp/Scene.hx b/hide_js/hide/comp/Scene.hx index 244c9170c..8e040f79e 100644 --- a/hide_js/hide/comp/Scene.hx +++ b/hide_js/hide/comp/Scene.hx @@ -245,7 +245,15 @@ class Scene extends hide.comp.Component implements h3d.IDrawable { for( f in listeners ) f(dt); onUpdate(dt); + #if dx12 + // the driver is shared by the scenes : resized and presented for each one + engine.driver.resize(engine.width, engine.height); engine.render(this); + engine.driver.present(); + @:privateAccess hxd.System.lastPresentTime = @:privateAccess engine.lastTime; + #else + engine.render(this); + #end } function sync() { @@ -326,8 +334,15 @@ class Scene extends hide.comp.Component implements h3d.IDrawable { setCurrent(); var bmp : js.html.ImageElement = cast img[0]; t.resize(bmp.width, bmp.height); + #if dx12 + var data = new hxd.BitmapData(bmp.width, bmp.height); + @:privateAccess data.ctx.drawImage(bmp, 0, 0); + engine.driver.uploadTextureBitmap(t, data, 0, 0); + data.dispose(); + #else untyped bmp.ctx = { getImageData : function(_) return bmp, canvas : { width : 0, height : 0 } }; engine.driver.uploadTextureBitmap(t, cast bmp, 0, 0); + #end t.realloc = onLoaded; t.flags.unset(Loading); @:privateAccess if( t.waitLoads != null ) { diff --git a/hide_js/hide/comp/SceneEditor.hx b/hide_js/hide/comp/SceneEditor.hx index 9ade48f98..6793ca099 100644 --- a/hide_js/hide/comp/SceneEditor.hx +++ b/hide_js/hide/comp/SceneEditor.hx @@ -3539,7 +3539,7 @@ class SceneEditor { } public function setWireframe(val = true) { - var engine = h3d.Engine.getCurrent(); + var engine = scene.engine; if( engine.driver.hasFeature(Wireframe) ) { for( m in scene.s3d.getMaterials() ) { if ( m.name == "$collider" ) diff --git a/hide_js/libs/dx/Display.hx b/hide_js/libs/dx/Display.hx index a14cf4439..8413a0bba 100644 --- a/hide_js/libs/dx/Display.hx +++ b/hide_js/libs/dx/Display.hx @@ -3,21 +3,19 @@ package dx; /** Displays the emulated swapchain in the page : the back buffers are shared textures created by the addon, imported once with Electron's sharedTexture module and drawn - into the window canvas with WebGPU at each present. + with WebGPU at each present, into the canvas of the engine rendered (see dx.Window). **/ class Display { - static var window : Window; static var textures : Array = []; + static var width = 0; + static var height = 0; static var device : Dynamic; - static var context : Dynamic; static var pipeline : Dynamic; static var sampler : Dynamic; static var format : String; - static var pending = false; - public static function init( win : Window, tries = 5 ) { - window = win; + public static function init( tries = 5 ) { var gpu : Dynamic = js.Syntax.code("navigator.gpu"); if( gpu == null ) { js.Browser.console.error("DX12 display : no WebGPU in this page, nothing is shown"); @@ -27,7 +25,7 @@ class Display { gpu.requestAdapter({ powerPreference : "high-performance" }).then(function( adapter : Dynamic ) { // none while the GPU process (re)starts, after a reload of the page : asked again if( adapter == null ) { - if( tries > 1 ) js.Browser.window.setTimeout(() -> init(win, tries - 1), 200); + if( tries > 1 ) js.Browser.window.setTimeout(() -> init(tries - 1), 200); else js.Browser.console.error("DX12 display : no WebGPU adapter, nothing is shown"); return null; } @@ -35,9 +33,7 @@ class Display { }).then(function( dev : Dynamic ) { if( dev == null ) return; device = dev; - context = window.canvas.getContext("webgpu"); format = gpu.getPreferredCanvasFormat(); - context.configure({ device : device, format : format, alphaMode : "opaque" }); var module = device.createShaderModule({ code : " struct V { @builtin(position) pos : vec4f, @location(0) uv : vec2f }; @vertex fn vs( @builtin(vertex_index) i : u32 ) -> V { @@ -74,8 +70,18 @@ class Display { handle : { ntHandle : nodeBuf }, })); } - window.canvas.width = width; - window.canvas.height = height; + Display.width = width; + Display.height = height; + } + + static function getContext( canvas : js.html.CanvasElement ) : Dynamic { + var context : Dynamic = (cast canvas).__dxContext; + if( context == null ) { + context = canvas.getContext("webgpu"); + context.configure({ device : device, format : format, alphaMode : "opaque" }); + (cast canvas).__dxContext = context; + } + return context; } /** called after the native present : draws the presented buffer into the canvas **/ @@ -84,6 +90,10 @@ class Display { var index : Int = js.Syntax.code("__dx12.js_presented_buffer()"); var tex = textures[index]; if( tex == null ) return; + var canvas = @:privateAccess Window.windows[0].canvas; + if( canvas.width != width ) canvas.width = width; + if( canvas.height != height ) canvas.height = height; + var context = getContext(canvas); var frame : Dynamic = tex.getVideoFrame(); var ext = device.importExternalTexture({ source : frame }); var bind = device.createBindGroup({ layout : pipeline.getBindGroupLayout(0), entries : [ diff --git a/hide_js/libs/dx/Dx12.hx b/hide_js/libs/dx/Dx12.hx index dd858e12b..97ebcd654 100644 --- a/hide_js/libs/dx/Dx12.hx +++ b/hide_js/libs/dx/Dx12.hx @@ -1724,7 +1724,7 @@ class Dx12 { var name = hl.Bytes.ofUcs2(deviceName); var d = dxCreate(null, flags, name); if( name != null ) name.free(); - dx.Display.init(win); + dx.Display.init(); return d; } diff --git a/hide_js/libs/dx/Window.hx b/hide_js/libs/dx/Window.hx index 801719438..edd29a931 100644 --- a/hide_js/libs/dx/Window.hx +++ b/hide_js/libs/dx/Window.hx @@ -1,20 +1,24 @@ package dx; /** - JS replacement of hldx's dx.Window, as used by the DX12 driver : the window is the - page canvas (#webgl, like hxd.Window), where the back buffers are displayed. + JS replacement of hldx's dx.Window, as used by the DX12 driver : the window is the canvas of + the current hxd.Window, where the back buffers are displayed. The driver is shared by the engines + of all the canvas (see h3d.Engine) : it is resized and presented for the engine rendered. **/ class Window { - static var windows(get, null) : Array; + static var windows : Array = [new Window()]; - public var canvas(default, null) : js.html.CanvasElement; + public var canvas(get, never) : js.html.CanvasElement; public var width(get, never) : Int; public var height(get, never) : Int; public var vsync : Bool = true; - public function new( canvas : js.html.CanvasElement ) { - this.canvas = canvas; + function new() { + } + + function get_canvas() : js.html.CanvasElement { + return @:privateAccess hxd.Window.getInstance().canvas; } function get_width() { @@ -24,13 +28,4 @@ class Window { function get_height() { return Math.round(canvas.getBoundingClientRect().height * js.Browser.window.devicePixelRatio); } - - static function get_windows() { - if( windows == null ) { - var canvas : js.html.CanvasElement = cast js.Browser.document.getElementById("webgl"); - if( canvas == null ) throw "Missing canvas #webgl"; - windows = [new Window(canvas)]; - } - return windows; - } } diff --git a/hide_js/libs/jsdx/native/dx12js.cpp b/hide_js/libs/jsdx/native/dx12js.cpp index 6ac428a73..259b7daf7 100644 --- a/hide_js/libs/jsdx/native/dx12js.cpp +++ b/hide_js/libs/jsdx/native/dx12js.cpp @@ -20,11 +20,22 @@ static FARPROC WINAPI loadExeHook( unsigned int event, DelayLoadInfo *info ) { } decltype(__pfnDliNotifyHook2) __pfnDliNotifyHook2 = loadExeHook; -// ---- swapchain emulation +// the pages of the renderer process share the addon : they use the same device + +static dx_driver *js_create( HWND window, DriverInitFlag flags, uchar *dev_desc ) { + if( static_driver && static_driver->device && static_driver->device->GetDeviceRemovedReason() == S_OK ) + return static_driver; + return HL_NAME(create)(window, flags, dev_desc); +} + +static hl_shim::Register js_reg_create("create", hl_shim::wrap<&js_create>()); + +// ---- swapchain emulation : one per command queue (driver), selected by resize / present #define JS_MAX_BUFFERS 8 -static struct { +struct js_swapchain { + ID3D12Device *device; // a queue address can be reused int count; int index; int presented; @@ -33,27 +44,46 @@ static struct { HANDLE handles[JS_MAX_BUFFERS]; ID3D12Fence *fence; UINT64 fenceValue; - HANDLE event; -} js_swap = {}; - -static void js_release_buffers() { - for( int i = 0; i < js_swap.count; i++ ) { - if( js_swap.buffers[i] ) js_swap.buffers[i]->Release(); - if( js_swap.handles[i] ) CloseHandle(js_swap.handles[i]); - js_swap.buffers[i] = NULL; - js_swap.handles[i] = NULL; +}; + +static std::map js_swaps; +static js_swapchain *js_cur = NULL; +static HANDLE js_event = NULL; + +static void js_release_buffers( js_swapchain *sc ) { + for( int i = 0; i < sc->count; i++ ) { + if( sc->buffers[i] ) sc->buffers[i]->Release(); + if( sc->handles[i] ) CloseHandle(sc->handles[i]); + sc->buffers[i] = NULL; + sc->handles[i] = NULL; } - js_swap.count = 0; + sc->count = 0; +} + +static js_swapchain *js_select( ID3D12CommandQueue *q ) { + js_swapchain *sc = &js_swaps[q]; + if( sc->device != static_driver->device ) { + js_release_buffers(sc); + if( sc->fence ) sc->fence->Release(); + *sc = {}; + sc->device = static_driver->device; + sc->presented = -1; + CHKERR(sc->device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&sc->fence))); + } + js_cur = sc; + return sc; +} + +static js_swapchain *js_current() { + if( js_cur == NULL ) hl_error("no swapchain"); + return js_cur; } static void js_resize( ID3D12CommandQueue *directQueue, int width, int height, int buffer_count, DXGI_FORMAT format ) { dx_driver *drv = static_driver; + js_swapchain *sc = js_select(directQueue); if( buffer_count > JS_MAX_BUFFERS ) hl_error("too many back buffers"); - js_release_buffers(); - if( !js_swap.fence ) { - CHKERR(drv->device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&js_swap.fence))); - js_swap.event = CreateEvent(NULL, FALSE, FALSE, NULL); - } + js_release_buffers(sc); D3D12_HEAP_PROPERTIES heap = {}; heap.Type = D3D12_HEAP_TYPE_DEFAULT; D3D12_RESOURCE_DESC desc = {}; @@ -68,37 +98,41 @@ static void js_resize( ID3D12CommandQueue *directQueue, int width, int height, i // by Chromium's D3D11 device without a cross-API barrier desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET | D3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESS; for( int i = 0; i < buffer_count; i++ ) { - CHKERR(drv->device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_SHARED, &desc, D3D12_RESOURCE_STATE_COMMON, NULL, IID_PPV_ARGS(&js_swap.buffers[i]))); - CHKERR(drv->device->CreateSharedHandle(js_swap.buffers[i], NULL, GENERIC_ALL, NULL, &js_swap.handles[i])); - js_swap.buffers[i]->SetName(L"JS_BACKBUFFER"); + CHKERR(drv->device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_SHARED, &desc, D3D12_RESOURCE_STATE_COMMON, NULL, IID_PPV_ARGS(&sc->buffers[i]))); + CHKERR(drv->device->CreateSharedHandle(sc->buffers[i], NULL, GENERIC_ALL, NULL, &sc->handles[i])); + sc->buffers[i]->SetName(L"JS_BACKBUFFER"); } - js_swap.count = buffer_count; - js_swap.index = 0; - js_swap.presented = -1; + sc->count = buffer_count; + sc->index = 0; + sc->presented = -1; } // like IDXGISwapChain::GetBuffer, the caller owns a reference static ID3D12Resource *js_get_back_buffer( int index ) { - if( index < 0 || index >= js_swap.count ) return NULL; - js_swap.buffers[index]->AddRef(); - return js_swap.buffers[index]; + js_swapchain *sc = js_current(); + if( index < 0 || index >= sc->count ) return NULL; + sc->buffers[index]->AddRef(); + return sc->buffers[index]; } static int js_get_current_back_buffer_index() { - return js_swap.index; + return js_current()->index; } // No shared fence with Chromium: wait for the GPU so that the page can display the // buffer as soon as present returns. static void js_command_queue_present( ID3D12CommandQueue *q, bool vsync ) { - CHKERR(q->Signal(js_swap.fence, ++js_swap.fenceValue)); - if( js_swap.fence->GetCompletedValue() < js_swap.fenceValue ) { - js_swap.fence->SetEventOnCompletion(js_swap.fenceValue, js_swap.event); - WaitForSingleObject(js_swap.event, INFINITE); + js_swapchain *sc = js_select(q); + if( !js_event ) js_event = CreateEvent(NULL, FALSE, FALSE, NULL); + CHKERR(q->Signal(sc->fence, ++sc->fenceValue)); + if( sc->fence->GetCompletedValue() < sc->fenceValue ) { + sc->fence->SetEventOnCompletion(sc->fenceValue, js_event); + WaitForSingleObject(js_event, INFINITE); } - js_swap.presented = js_swap.index; - js_swap.presentCount++; - js_swap.index = (js_swap.index + 1) % js_swap.count; + if( sc->count == 0 ) return; + sc->presented = sc->index; + sc->presentCount++; + sc->index = (sc->index + 1) % sc->count; } static hl_shim::Register js_reg_resize("resize", hl_shim::wrap<&js_resize>()); @@ -108,10 +142,10 @@ static hl_shim::Register js_reg_command_queue_present("command_queue_present", h // ---- page access to the back buffers -static int js_back_buffer_count() { return js_swap.count; } -static HANDLE js_back_buffer_handle( int index ) { return index >= 0 && index < js_swap.count ? js_swap.handles[index] : NULL; } -static int js_presented_buffer() { return js_swap.presented; } -static int js_present_count() { return js_swap.presentCount; } +static int js_back_buffer_count() { return js_current()->count; } +static HANDLE js_back_buffer_handle( int index ) { js_swapchain *sc = js_current(); return index >= 0 && index < sc->count ? sc->handles[index] : NULL; } +static int js_presented_buffer() { return js_current()->presented; } +static int js_present_count() { return js_current()->presentCount; } // The high performance GPU (as Chromium takes it with force_high_performance_gpu) : its index in // list_devices (the hardware adapters in the order of DXGI), -1 when it is unknown. The page passes diff --git a/hide_js/libs/jsdx/native/gen-dx12-hx.js b/hide_js/libs/jsdx/native/gen-dx12-hx.js index 1fd4252e3..afe9587ae 100644 --- a/hide_js/libs/jsdx/native/gen-dx12-hx.js +++ b/hide_js/libs/jsdx/native/gen-dx12-hx.js @@ -113,7 +113,7 @@ rep(` public static function create( win : Window, flags : DriverInitFlags, ?dev var name = hl.Bytes.ofUcs2(deviceName); var d = dxCreate(null, flags, name); if( name != null ) name.free(); - dx.Display.init(win); + dx.Display.init(); return d; }`); diff --git a/hide_jsdx.hxml b/hide_jsdx.hxml new file mode 100644 index 000000000..99d932644 --- /dev/null +++ b/hide_jsdx.hxml @@ -0,0 +1,4 @@ +-D dx12 +--remap hl:hlemu +--macro hlemu.Macros.init(["dx", "h3d.impl"]) +hide.hxml From adf3a853bea964128e31467f0d208f431b7c00d5 Mon Sep 17 00:00:00 2001 From: ncannasse Date: Sat, 10 Oct 2026 14:13:37 +0200 Subject: [PATCH 004/191] added meshtools --- CLAUDE.md | 2 +- hide_js/hide/ElectronMain.hx | 7 +- hide_js/hide/Ide.hx | 1 + hide_js/hide/tools/NativeMeshTools.hx | 99 +++++++++++++++++++++++++++ hide_js/libs/jsdx/native/build.bat | 12 +++- hide_js/libs/jsdx/native/dx12js.cpp | 10 +++ 6 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 hide_js/hide/tools/NativeMeshTools.hx diff --git a/CLAUDE.md b/CLAUDE.md index d706408b3..4f63a9164 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ - Renderer code talks to the main process only through the wrappers in `hide_js/libs/electron/` (`electron.Ipc`, `electron.Window`, `electron.Clipboard`, ...). Main process externs are in `hide_js/libs/electron/main/`. - Keep main process code in Haxe, not hand-written JS. - `haxe hide.hxml` builds both `bin/hide.js` (WebGL) and `bin/main.js`; `haxe hide_jsdx.hxml` the same with DX12; `haxe hide_hl.hxml` the HashLink editor. The shared libraries / defines / includes are in `hide_common.hxml`. The Electron runtime is unpacked in `bin/electron/`. -- DirectX 12 (jsdx) : `haxe hide_jsdx.hxml` builds `bin/hide.js` with the heaps DX12Driver (heaps branch `js-dx12`). The HL emulation (`hlemu`) and `dx` API are in `hide_js/libs`; the N-API addon `bin/jsdx/dx12.node` is built by `hide_js/libs/jsdx/native/build.bat` (needs `HASHLINK_SRC` and `npm install` in `hide_js/libs/jsdx`). `dx/Dx12.hx` is generated by `hide_js/libs/jsdx/native/gen-dx12-hx.js`, do not edit it. +- DirectX 12 (jsdx) : `haxe hide_jsdx.hxml` builds `bin/hide.js` with the heaps DX12Driver (heaps branch `js-dx12`). The HL emulation (`hlemu`) and `dx` API are in `hide_js/libs`; the N-API addon `bin/jsdx/dx12.node` is built by `npm run build` in `hide_js/libs/jsdx` (`native/build.bat`, needs `HASHLINK_SRC` and `npm install`). It also has the heaps natives of HashLink (`libs/heaps` : mikktspace, meshoptimizer, V-HACD, compiled unmodified), used by `hide.tools.NativeMeshTools` (the HL code of `hxd.tools.MeshTools`) with DX12 instead of the meshTools executable. When loaded, the addon disables the power throttling (EcoQoS) of the page thread, which Windows otherwise puts on the efficiency cores when HIDE is not in the foreground. `dx/Dx12.hx` is generated by `hide_js/libs/jsdx/native/gen-dx12-hx.js`, do not edit it. - DX12 with several scenes : one `h3d.Engine` per scene (as with WebGL, `multidriver`), all sharing a single `DX12Driver` (`DX12Driver.inst`, never disposed : compiled shaders / pipelines are kept). The driver has a single emulated swapchain : `Scene.doSync` resizes it to the engine size (nothing if unchanged) before rendering, then presents it into the canvas of the current `hxd.Window` (`dx.Window`, `dx.Display`). `DX12Driver` itself is unchanged. The addon has one swapchain per command queue and shares its D3D12 device between the pages of the renderer process (main window, thumbnail generator). ## Running the editor for tests diff --git a/hide_js/hide/ElectronMain.hx b/hide_js/hide/ElectronMain.hx index abda07f01..488c1045c 100644 --- a/hide_js/hide/ElectronMain.hx +++ b/hide_js/hide/ElectronMain.hx @@ -55,7 +55,12 @@ class ElectronMain { App.setName(appName); App.setPath("userData", js.node.Path.join(App.getPath("appData"), appName)); App.commandLine.appendSwitch("js-flags", "--expose-gc --no-efficiency-mode"); - App.commandLine.appendSwitch("disable-features", "AllowSoftwareGLFallbackDueToCrashes"); + // test mode : the windows stay behind the user's ones, they must still render (scroll, animation frames, screenshots) + App.commandLine.appendSwitch("disable-features", "AllowSoftwareGLFallbackDueToCrashes" + (testMode ? ",CalculateNativeWinOcclusion" : "")); + if( testMode ) { + App.commandLine.appendSwitch("disable-renderer-backgrounding"); + App.commandLine.appendSwitch("disable-backgrounding-occluded-windows"); + } // DirectX 12 (jsdx) : Chromium must use the same GPU as the dx12 addon, shared textures can not cross adapters App.commandLine.appendSwitch("force_high_performance_gpu"); App.commandLine.appendSwitch("ignore-gpu-blocklist"); diff --git a/hide_js/hide/Ide.hx b/hide_js/hide/Ide.hx index da9077f4f..b840a7eca 100644 --- a/hide_js/hide/Ide.hx +++ b/hide_js/hide/Ide.hx @@ -65,6 +65,7 @@ class Ide extends hide.tools.IdeData { initPad(); hxd.System.getClipboardText = () -> electron.Clipboard.get(Text); hxd.System.setClipboardText = (text) -> { electron.Clipboard.set(text, Text); return true; }; + #if dx12 hide.tools.NativeMeshTools.init(); #end isCDB = Sys.getEnv("HIDE_START_CDB") == "1" || electron.App.name == "CDB"; isDebugger = Sys.getEnv("HIDE_DEBUG") == "1"; diff --git a/hide_js/hide/tools/NativeMeshTools.hx b/hide_js/hide/tools/NativeMeshTools.hx new file mode 100644 index 000000000..05c527b6c --- /dev/null +++ b/hide_js/hide/tools/NativeMeshTools.hx @@ -0,0 +1,99 @@ +package hide.tools; + +#if dx12 +import hlemu.Native in M; + +/** + The mesh processing of the FBX to HMD conversion with the heaps natives of HashLink (libs/heaps, compiled + unmodified in bin/jsdx/dx12.node) instead of the meshTools executable : the HL code of hxd.tools.MeshTools, + with the memory of hlemu.Native. +**/ +class NativeMeshTools { + + static var prims : Dynamic; + + public static function init() { + prims = js.Syntax.code("globalThis.__dx12"); + if( prims == null || prims.compute_mikkt_tangents == null ) return; + hxd.tools.MeshTools.mikktspace = mikktspace; + hxd.tools.MeshTools.optimize = optimize; + hxd.tools.MeshTools.convexHulls = convexHulls; + } + + static inline function addr( b : haxe.io.Bytes ) { + return M.mem_address(b.getData()); + } + + static function mikktspace( vertices : haxe.io.Bytes, count : Int, stride : Int, xPos : Int, normalPos : Int, uvPos : Int, threshold = 180. ) { + var tangents = haxe.io.Bytes.alloc(count << 4); + for( i in 0...count ) + tangents.setFloat(i << 4, 1); + var indexes = haxe.io.Bytes.alloc(count << 2); + for( i in 0...count ) + indexes.setInt32(i << 2, i); + // user_info of mikkt.c + var m = haxe.io.Bytes.alloc(64); + M.mem_set_i64(addr(m) + 8, addr(vertices)); + m.setInt32(16, stride); + m.setInt32(20, xPos); + m.setInt32(24, normalPos); + m.setInt32(28, uvPos); + M.mem_set_i64(addr(m) + 32, addr(tangents)); + m.setInt32(40, 4); + m.setInt32(44, 0); + M.mem_set_i64(addr(m) + 48, addr(indexes)); + m.setInt32(56, count); + if( !prims.compute_mikkt_tangents(addr(m), threshold) ) + throw "assert"; + return tangents; + } + + static function optimize( vertices : haxe.io.Bytes, vertexCount : Int, vertexSize : Int, indexes : haxe.io.Bytes, indexCount : Int, targetIndexCount = -1, targetError = 0. ) { + var v = addr(vertices); + var idx = addr(indexes); + var remap = haxe.io.Bytes.alloc(vertexCount << 2); + var uniqueVertexCount : Int = prims.generate_vertex_remap(addr(remap), idx, indexCount, v, vertexCount, vertexSize); + prims.remap_index_buffer(idx, idx, indexCount, addr(remap)); + prims.remap_vertex_buffer(v, v, vertexCount, vertexSize, addr(remap)); + vertexCount = uniqueVertexCount; + if( targetIndexCount >= 0 ) + indexCount = prims.simplify(idx, idx, indexCount, v, vertexCount, vertexSize, targetIndexCount, targetError, 1 | 8 /* LockBorder | Prune */, null); + prims.optimize_vertex_cache(idx, idx, indexCount, vertexCount); + prims.optimize_overdraw(idx, idx, indexCount, v, vertexCount, vertexSize, 1.05); + vertexCount = prims.optimize_vertex_fetch(v, idx, indexCount, v, vertexCount, vertexSize); + return { vertexCount : vertexCount, indexCount : indexCount }; + } + + static function convexHulls( vertices : haxe.io.Bytes, vertexCount : Int, indexes : haxe.io.Bytes, triangleCount : Int, maxConvexHulls : Int, resolution : Int ) : Array { + // VHACD::IVHACD::Parameters, with the values of hxd.tools.VHACD.Parameters + var p = haxe.io.Bytes.alloc(72); + p.setInt32(24, maxConvexHulls); + p.setInt32(28, resolution); + p.setDouble(32, 1); + p.setInt32(40, 10); + p.set(44, 1); + p.setInt32(48, 0); + p.setInt32(52, 64); + p.set(56, 0); + p.setInt32(60, 2); + p.set(64, 0); + var vhacd = prims.create_vhacd(); + prims.vhacd_compute(vhacd, addr(vertices), vertexCount, addr(indexes), triangleCount, addr(p)); + var count : Int = prims.vhacd_get_n_convex_hulls(vhacd); + var out = count == 0 ? null : []; + // convex_hull of vhacd.cpp + var hull = haxe.io.Bytes.alloc(112); + for( i in 0...count ) { + prims.vhacd_get_convex_hull(vhacd, i, addr(hull)); + var points = new js.lib.Float64Array(hull.getInt32(16) * 3); + M.mem_copy(M.mem_address(points), M.mem_get_i64(addr(hull)), points.byteLength); + var triangles = new js.lib.Int32Array(hull.getInt32(20) * 3); + M.mem_copy(M.mem_address(triangles), M.mem_get_i64(addr(hull) + 8), triangles.byteLength); + out.push({ vertices : [for( f in points ) f], indexes : [for( i in triangles ) i] }); + } + prims.vhacd_release(vhacd); + return out; + } + +} +#end diff --git a/hide_js/libs/jsdx/native/build.bat b/hide_js/libs/jsdx/native/build.bat index de07b4d43..05e9bc2aa 100644 --- a/hide_js/libs/jsdx/native/build.bat +++ b/hide_js/libs/jsdx/native/build.bat @@ -1,6 +1,7 @@ @echo off -rem Builds bin\jsdx\dx12.node from hldx's dx12.cpp (unmodified) and the hl.h shim. -rem Uses HASHLINK_SRC (the hashlink "src" directory) for the hldx sources (libs\directx) +rem Builds bin\jsdx\dx12.node from hldx's dx12.cpp and the heaps natives (mikktspace, meshoptimizer, V-HACD), +rem unmodified, with the hl.h shim. +rem Uses HASHLINK_SRC (the hashlink "src" directory) for the sources (libs\directx, libs\heaps, include) rem and the shader compiler DLLs (dxcompiler.dll, dxil.dll in the hashlink directory). rem Requires Visual Studio 2026 and "npm install" in hide_js\libs\jsdx (node-api-headers). setlocal @@ -12,6 +13,8 @@ if "%HASHLINK_SRC%"=="" ( exit /b 1 ) set HLDX=%HASHLINK_SRC%\libs\directx +set HLHEAPS=%HASHLINK_SRC%\libs\heaps +set HLINC=%HASHLINK_SRC%\include set HLBIN=%HASHLINK_SRC%\.. if not exist "%NODE_API%" ( echo node-api-headers not found : run "npm install" in %ROOT% @@ -23,7 +26,10 @@ if not exist "%OUT%" mkdir "%OUT%" if not exist build\obj mkdir build\obj lib /nologo /def:"%NODE_API%\def\node_api.def" /name:node.exe /machine:x64 /out:build\obj\node.lib || exit /b 1 rem /Zi /DEBUG : dx12.pdb next to the addon, to read the minidumps of a crash -cl /nologo /std:c++17 /O2 /Zi /EHsc /MT /LD /W1 /Inative /I"%NODE_API%\include" /I"%HLDX%" native\dx12js.cpp /Fobuild\obj\ /Fdbuild\obj\ /Fe:"%OUT%\dx12.node" ^ +rem mikkt.c includes the C++ shim : compiled as C++ (/Tp) +cl /nologo /std:c++17 /O2 /Zi /EHsc /MT /LD /W1 /Inative /I"%NODE_API%\include" /I"%HLDX%" /I"%HLINC%\mikktspace" /I"%HLINC%\meshoptimizer" /I"%HLINC%\vhacd" ^ + native\dx12js.cpp /Tp"%HLHEAPS%\mikkt.c" "%HLHEAPS%\meshoptimizer.cpp" "%HLHEAPS%\vhacd.cpp" "%HLINC%\mikktspace\mikktspace.c" "%HLINC%\meshoptimizer\*.cpp" ^ + /Fobuild\obj\ /Fdbuild\obj\ /Fe:"%OUT%\dx12.node" ^ /link /DEBUG /OPT:REF /OPT:ICF /IMPLIB:build\obj\dx12.lib build\obj\node.lib d3d12.lib dxgi.lib dxguid.lib dxcompiler.lib delayimp.lib /DELAYLOAD:node.exe || exit /b 1 rem the shader compiler DLLs are loaded from the addon directory copy /y "%HLBIN%\dxcompiler.dll" "%OUT%\" >nul diff --git a/hide_js/libs/jsdx/native/dx12js.cpp b/hide_js/libs/jsdx/native/dx12js.cpp index 259b7daf7..491b9bae9 100644 --- a/hide_js/libs/jsdx/native/dx12js.cpp +++ b/hide_js/libs/jsdx/native/dx12js.cpp @@ -357,8 +357,18 @@ static void js_preload_dxil() { LoadLibraryW(path); } +// Windows runs the page thread on the efficiency cores when the window is not in the foreground +// (FBX conversions, shader compilations... more than 1.5x slower) : full speed on the thread loading the addon +static void js_high_qos() { + THREAD_POWER_THROTTLING_STATE s = {}; + s.Version = THREAD_POWER_THROTTLING_CURRENT_VERSION; + s.ControlMask = THREAD_POWER_THROTTLING_EXECUTION_SPEED; + SetThreadInformation(GetCurrentThread(), ThreadPowerThrottling, &s, sizeof(s)); +} + NAPI_MODULE_INIT() { js_preload_dxil(); + js_high_qos(); for( auto &p : hl_shim::registry() ) { napi_value f; napi_create_function(env, p.first.c_str(), NAPI_AUTO_LENGTH, p.second, nullptr, &f); From 34549f635d0e3bc0e53248b1b7741ef55e0007e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Espeute?= Date: Mon, 12 Oct 2026 15:15:55 +0200 Subject: [PATCH 005/191] [js] Fix missing dev tools --- hide_js/hide/ElectronMain.hx | 5 +++++ hide_js/libs/electron/main/Electron.hx | 1 + 2 files changed, 6 insertions(+) diff --git a/hide_js/hide/ElectronMain.hx b/hide_js/hide/ElectronMain.hx index 488c1045c..e3fa50c89 100644 --- a/hide_js/hide/ElectronMain.hx +++ b/hide_js/hide/ElectronMain.hx @@ -211,6 +211,11 @@ class ElectronMain { win.on("leave-full-screen", () -> dispatchWinEvent(id, "leave-fullscreen")); win.webContents.on("destroyed", () -> cleanupWebContents(wcId)); + // not prevented : the page also gets the key (F12 is used by DomkitEditor) + win.webContents.on("before-input-event", function(e : Dynamic, input : Dynamic) { + if( input.type == "keyDown" && (input.key == "F12" || (input.control && input.shift && input.code == "KeyI")) ) + win.webContents.toggleDevTools(); + }); // page reload : forget what the previous page had registered win.webContents.on("did-start-navigation", function(e : Dynamic) { if( !e.isMainFrame || e.isSameDocument ) return; diff --git a/hide_js/libs/electron/main/Electron.hx b/hide_js/libs/electron/main/Electron.hx index be2bdf8c0..1abc5776d 100644 --- a/hide_js/libs/electron/main/Electron.hx +++ b/hide_js/libs/electron/main/Electron.hx @@ -64,6 +64,7 @@ extern class WebContents { function isDestroyed() : Bool; function isCrashed() : Bool; function openDevTools( ?options : Dynamic ) : Void; + function toggleDevTools() : Void; function setDevToolsWebContents( wc : WebContents ) : Void; static function fromId( id : Int ) : Null; } From 168075e4b7ab964b5fc1427e595cf78d9d8daac1 Mon Sep 17 00:00:00 2001 From: trethaller Date: Mon, 12 Oct 2026 15:17:53 +0200 Subject: [PATCH 006/191] emitter-spec.md --- emitter-spec.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 emitter-spec.md diff --git a/emitter-spec.md b/emitter-spec.md new file mode 100644 index 000000000..93db92230 --- /dev/null +++ b/emitter-spec.md @@ -0,0 +1,87 @@ +# Emitter params — spec + +Scope: `Emitter` and `Emitter2D`. Editing through the new inspector only (edit2 / HideKit). Colors (particle shader color curves, emitter random color / gradient) keep their current model. + +## Terms + +- **Particle curve**: a curve over the life of a particle, time in [0, 1]. Edited inline in the inspector, never shown in the timeline. +- **Animation curve**: a curve over the FX time. Shown and edited in the timeline. + +## Param groups + +| Group | Params | Modes | Timeline | +|---|---|---|---| +| Emitter | emission settings (rate, burst count, angle, shape dimensions...) | Constant | animatable ones take an animation curve | +| Spawn | lifetime, start speed, start world speed | Constant, Range | `value` / `min` / `max` take an animation curve, read when the particle spawns | +| Particle | speed, world speed, acceleration, world acceleration, orbit speed, dampen, max velocity, scale, stretch, stretch velocity, rotation, pivot, offset; particle shader params | all | — | +| Multipliers | scale multiplier, orbit speed multiplier | Constant | take an animation curve, read every frame | + +Vector params have one mode per component. + +## Modes + +| Mode | Label | Fields | Value | +|---|---|---|---| +| unset | **+** | — | default value of the param | +| `Constant` | Constant | `value` | `value` | +| `Range` | Range | `min`, `max` | `lerp(min, max, u)` | +| `Curve` | Curve | `shape`, `from`, `to` | `lerp(from, to, s(t))` | +| `RandomCurve` | Random Curve | `shape`, `from: {min, max}`, `to: {min, max}` | `lerp(lerp(from.min, from.max, u), lerp(to.min, to.max, u), s(t))` | +| `CurveRange` | Curve Range | `a`, `b`, each `{shape, from, to}` | `lerp(a(t), b(t), u)` | + +- `t`: particle life in [0, 1]. `s(t)`: the shape evaluated at `t`. +- `u`: a random in [0, 1], one draw per particle and per param component, keyed by the param. Every random field of a mode uses the same draw. +- `from` / `to` default to 0 / 1. +- Unset params show a **+** button; Constant offers a reset to the default value. + +## Shapes + +A shape is either: + +- a **curve**: a reference, by path, to a curve prefab in the hierarchy. A curve keeps its own options: blend between two curves by an FX parameter, time remap (by an FX parameter or by another curve), loop. +- an **easing**: an enum, `Linear` and `{Sine, Quad, Cubic, Quart, Quint, Expo, Circ, Back, Elastic, Bounce} × {In, Out, InOut}`. + +The range (`from` / `to`) belongs to the param; a reference shares the shape only. + +## Animation curves + +- They set the animated value directly. +- They hold plain keys: no range, no random. Constants of the old data are baked into the keys. +- Targets: transforms, shader params outside emitters, Emitter params, Spawn `value` / `min` / `max`, Multipliers. + +## Storage + +- Params are stored under their plain name (`scale`, `startSpeed`...), each component with its mode and fields. +- Old files are upgraded on load and saved upgraded. + +## Upgrade + +Rule: runtime-equivalent whenever possible, while simplifying. + +`k`: base value, `a`: random amount (`_rand`), `c`: curve. + +| Old | New | +|---|---| +| `k` | Constant `k` | +| `k ± a` | Range `[k−a, k+a]` | +| `k·c` | Curve `c`, from 0, to `k` | +| `(k ± a)·c` | RandomCurve `c`, from `{0, 0}`, to `{k−a, k+a}` | +| `k ± a + c` (rotation, offset, pivot) | RandomCurve `c`, from `{k−a, k+a}`, to `{k−a+1, k+a+1}` | +| random between two curves | CurveRange, both curves with from 0, to 1 | +| curve blend / time remap / loop | kept on the curve | +| lifetime + lifetime random | Spawn, Range | +| start speed curves (FX time) | animation curves on its Spawn fields, baked | +| scale / orbit speed over time | Multipliers, Constant or animation curve | +| emitter param `k·c` | animation curve, `k` baked | +| emitter object transform curves | animation curves, re-baked to override the transform | +| particle shader float `c·k` | Curve `c`, from 0, to `k` | +| particle shader vector, some components animated | animated components: Curve; the others: Constant 0 | + +Validation: a batch run over the games' .fx, outside the repo, that prints each param as a canonical formula before and after the upgrade and compares them (constants rounded, randoms compared by structure). + +## UI + +- Each param (or component) has a mode dropdown, restricted to the modes of its group. +- Curve modes show their particle curves in an inline curve editor. Random Curve: `from` fields at the bottom, `to` fields at the top, the shape at mid range as a solid line, the two extremes dashed. Keys are edited in display space. +- Curve Range: two stacked curve editors. +- In the FX editor, animated properties are marked in the inspector, Blender-like. From e04676b00181f89b677e697c49591200bcf862ee Mon Sep 17 00:00:00 2001 From: trethaller Date: Mon, 12 Oct 2026 16:07:09 +0200 Subject: [PATCH 007/191] Values: VLerp and VEasing, easing functions vLerp folds into the existing flat values when it matches one of them (the usual cases evaluate exactly as before), and falls back to VLerp, which getFast evaluates, instead of a non flat VAdd/VMult. Co-Authored-By: Claude Opus 5.5 --- hrt/prefab/fx/Easing.hx | 125 +++++++++++++++++++++++++++++++++++++ hrt/prefab/fx/Evaluator.hx | 40 ++++++++++++ hrt/prefab/fx/Value.hx | 4 ++ 3 files changed, 169 insertions(+) create mode 100644 hrt/prefab/fx/Easing.hx diff --git a/hrt/prefab/fx/Easing.hx b/hrt/prefab/fx/Easing.hx new file mode 100644 index 000000000..a2f47e44c --- /dev/null +++ b/hrt/prefab/fx/Easing.hx @@ -0,0 +1,125 @@ +package hrt.prefab.fx; + +/** + Easing functions, mapping [0, 1] to a curve from 0 to 1 (`Back` and + `Elastic` overshoot). Used as the shape of an emitter param over the + particle life, see `EmitterParam.ParamShape`. +**/ +enum abstract Easing(String) from String to String { + var Linear; + var SineIn; + var SineOut; + var SineInOut; + var QuadIn; + var QuadOut; + var QuadInOut; + var CubicIn; + var CubicOut; + var CubicInOut; + var QuartIn; + var QuartOut; + var QuartInOut; + var QuintIn; + var QuintOut; + var QuintInOut; + var ExpoIn; + var ExpoOut; + var ExpoInOut; + var CircIn; + var CircOut; + var CircInOut; + var BackIn; + var BackOut; + var BackInOut; + var ElasticIn; + var ElasticOut; + var ElasticInOut; + var BounceIn; + var BounceOut; + var BounceInOut; + + public static var ALL : Array = [ + Linear, + SineIn, SineOut, SineInOut, + QuadIn, QuadOut, QuadInOut, + CubicIn, CubicOut, CubicInOut, + QuartIn, QuartOut, QuartInOut, + QuintIn, QuintOut, QuintInOut, + ExpoIn, ExpoOut, ExpoInOut, + CircIn, CircOut, CircInOut, + BackIn, BackOut, BackInOut, + ElasticIn, ElasticOut, ElasticInOut, + BounceIn, BounceOut, BounceInOut, + ]; + + public static function isValid( name : String ) { + return ALL.indexOf(name) >= 0; + } + + /** The easing at `t`, clamped to [0, 1]. **/ + public function eval( t : Float ) : Float { + if( t <= 0 ) t = 0; + if( t >= 1 ) t = 1; + inline function pow( v : Float, n : Float ) return Math.pow(v, n); + inline function inOut( t : Float, n : Int ) return t < 0.5 ? pow(2, n - 1) * pow(t, n) : 1 - pow(-2 * t + 2, n) / 2; + final PI = Math.PI; + final c1 = 1.70158; + final c2 = c1 * 1.525; + final c3 = c1 + 1; + final c4 = (2 * PI) / 3; + final c5 = (2 * PI) / 4.5; + return switch( (cast this : Easing) ) { + case Linear: t; + case SineIn: 1 - Math.cos(t * PI / 2); + case SineOut: Math.sin(t * PI / 2); + case SineInOut: -(Math.cos(PI * t) - 1) / 2; + case QuadIn: t * t; + case QuadOut: 1 - (1 - t) * (1 - t); + case QuadInOut: inOut(t, 2); + case CubicIn: t * t * t; + case CubicOut: 1 - pow(1 - t, 3); + case CubicInOut: inOut(t, 3); + case QuartIn: pow(t, 4); + case QuartOut: 1 - pow(1 - t, 4); + case QuartInOut: inOut(t, 4); + case QuintIn: pow(t, 5); + case QuintOut: 1 - pow(1 - t, 5); + case QuintInOut: inOut(t, 5); + case ExpoIn: t == 0 ? 0 : pow(2, 10 * t - 10); + case ExpoOut: t == 1 ? 1 : 1 - pow(2, -10 * t); + case ExpoInOut: t == 0 ? 0 : t == 1 ? 1 : t < 0.5 ? pow(2, 20 * t - 10) / 2 : (2 - pow(2, -20 * t + 10)) / 2; + case CircIn: 1 - Math.sqrt(1 - t * t); + case CircOut: Math.sqrt(1 - (t - 1) * (t - 1)); + case CircInOut: t < 0.5 ? (1 - Math.sqrt(1 - 4 * t * t)) / 2 : (Math.sqrt(1 - pow(-2 * t + 2, 2)) + 1) / 2; + case BackIn: c3 * t * t * t - c1 * t * t; + case BackOut: 1 + c3 * pow(t - 1, 3) + c1 * pow(t - 1, 2); + case BackInOut: t < 0.5 ? (pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2 : (pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2; + case ElasticIn: t == 0 ? 0 : t == 1 ? 1 : -pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4); + case ElasticOut: t == 0 ? 0 : t == 1 ? 1 : pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1; + case ElasticInOut: t == 0 ? 0 : t == 1 ? 1 : t < 0.5 + ? -(pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + : (pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + 1; + case BounceIn: 1 - bounceOut(1 - t); + case BounceOut: bounceOut(t); + case BounceInOut: t < 0.5 ? (1 - bounceOut(1 - 2 * t)) / 2 : (1 + bounceOut(2 * t - 1)) / 2; + default: t; + } + } + + static function bounceOut( t : Float ) : Float { + final n1 = 7.5625; + final d1 = 2.75; + if( t < 1 / d1 ) + return n1 * t * t; + if( t < 2 / d1 ) { + t -= 1.5 / d1; + return n1 * t * t + 0.75; + } + if( t < 2.5 / d1 ) { + t -= 2.25 / d1; + return n1 * t * t + 0.9375; + } + t -= 2.625 / d1; + return n1 * t * t + 0.984375; + } +} diff --git a/hrt/prefab/fx/Evaluator.hx b/hrt/prefab/fx/Evaluator.hx index d85eda351..8d2644aa4 100644 --- a/hrt/prefab/fx/Evaluator.hx +++ b/hrt/prefab/fx/Evaluator.hx @@ -69,6 +69,37 @@ class Evaluator { return r != null ? r : VAdd(a, b); } + /** Whether `getFast` can evaluate the value. **/ + public static function isFlat(v: Value) : Bool { + return switch(v) { + case VAdd(_), VMult(_), VVector(_), VHsl(_): false; + case VParamRemap(a, _): isFlat(a); + case VValueRemap(a, remap): isFlat(a) && isFlat(remap); + case VLerp(a, b, t): isFlat(a) && isFlat(b) && isFlat(t); + default: true; + } + } + + /** + lerp(a, b, t), folded into the flat values above when it matches one of them, + so the usual cases evaluate exactly like before. + **/ + public static function vLerp(a: Value, b: Value, t: Value) : Value { + inline function const(v: Value) : Null return switch(v) { case VZero: 0.0; case VConst(f): f; default: null; }; + var ca = const(a), cb = const(b); + var r : Null = switch [a, b, t] { + case [_, _, VZero]: a; + case [_, _, VConst(1.0)]: b; + case [VZero, _, _]: vMult(b, t); + case [_, _, _] if(ca != null && cb != null): vAdd(a, vMult(VConst(cb - ca), t)); + // b = a + 1: the additive params (rotation, offset, pivot) with a random base + case [VRandom(ra, sa, aa), VRandom(rb, sb, ab), _] if(ra == rb && sa == sb && ab - aa == 1.0): vAdd(a, t); + case [VCurve(curveA), VCurve(curveB), VRandom(rid, 0.5, 0.5)]: VRandomBetweenCurves(rid, curveA, curveB); + default: null; + } + return r != null && isFlat(r) ? r : VLerp(a, b, t); + } + inline function getRandom(pidx: Int, ridx: Int) { var i = pidx * stride + ridx; return randValues[i]; @@ -108,6 +139,12 @@ class Evaluator { var va = a.getVal(time); var vb = b.getVal(time); va + (vb - va) * ((getRandom(pidx, rid) + 1) * 0.5); + case VLerp(a, b, t): + var va = getFast(pidx, a, time); + var vb = getFast(pidx, b, time); + va + (vb - va) * getFast(pidx, t, time); + case VEasing(e): + e.eval(time); case VParamRemap(a, param): getFast(pidx, a, parameters[param] ?? 0.0); case VValueRemap(a, remap): @@ -129,6 +166,9 @@ class Evaluator { return getFloat(pidx, a, time) + getFloat(pidx, b, time); case VMult(a, b): return getFloat(pidx, a, time) * getFloat(pidx, b, time); + case VLerp(a, b, t): + var va = getFloat(pidx, a, time); + return va + (getFloat(pidx, b, time) - va) * getFloat(pidx, t, time); default: return getFast(pidx, val, time); } diff --git a/hrt/prefab/fx/Value.hx b/hrt/prefab/fx/Value.hx index 6a507a798..303676c56 100644 --- a/hrt/prefab/fx/Value.hx +++ b/hrt/prefab/fx/Value.hx @@ -19,6 +19,10 @@ enum Value { VRandomBlendCurves(rid: Int, scale: Float, add: Float, a: Curve, b: Curve, blendVar: String); // lerp(a.getVal(t), b.getVal(t), (random(rid)+1)/2) VRandomBetweenCurves(rid: Int, a: Curve, b: Curve); + // lerp(a, b, t), each of them a flat value + VLerp(a: Value, b: Value, t: Value); + // an easing of t + VEasing(e: Easing); // rare complex values that we cannot currently optimize VParamRemap(a: Value, param: String); From 80df4db164a92821c846d8688423a5cec7d3822a Mon Sep 17 00:00:00 2001 From: trethaller Date: Mon, 12 Oct 2026 16:07:09 +0200 Subject: [PATCH 008/191] Emitter: params with modes, upgrade of the old format on load Params are stored under their plain name (scale, startSpeed...), one mode per component: Constant, Range, Curve, RandomCurve, CurveRange or unset, the shapes being a curve referenced by path or an easing (EmitterParam). Particle shaders under an emitter get the same model (Shader.particleParams), colors keep their curves. Old files are upgraded on load into equivalent modes (EmitterUpgrade) and saved in the new format (props.paramsVersion). Upgraded emitters keep the random slot layout of the old code (skippedRandomSlots), so they draw the same randoms. The animation curves of the spawn and multiplier params, and of the emitter settings, keep multiplying the value by name. Co-Authored-By: Claude Opus 5.5 --- hrt/prefab/Curve.hx | 25 +- hrt/prefab/Shader.hx | 79 +++++++ hrt/prefab/fx/BaseFX.hx | 86 +++++-- hrt/prefab/fx/Emitter.hx | 374 +++++++++++++++++------------- hrt/prefab/fx/EmitterMacros.hx | 82 +------ hrt/prefab/fx/EmitterParam.hx | 396 ++++++++++++++++++++++++++++++++ hrt/prefab/fx/EmitterUpgrade.hx | 276 ++++++++++++++++++++++ 7 files changed, 1069 insertions(+), 249 deletions(-) create mode 100644 hrt/prefab/fx/EmitterParam.hx create mode 100644 hrt/prefab/fx/EmitterUpgrade.hx diff --git a/hrt/prefab/Curve.hx b/hrt/prefab/Curve.hx index 3332f8c29..c3dd4d29d 100644 --- a/hrt/prefab/Curve.hx +++ b/hrt/prefab/Curve.hx @@ -110,16 +110,20 @@ class Curve extends Prefab { addKey(0.0, 0.0); addKey(1.0, 1.0); } + name = loadedName(name, Std.downcast(parent, Curve)?.name); + #if !editor + bakeValues(); + #end + } + + /** The name a curve named `name` gets on load, under a curve named `parentCurve` if any. **/ + public static function loadedName(name: String, parentCurve: Null) : String { name = StringTools.replace(name, ".", ":"); #if editor - if (Std.downcast(parent, Curve) != null) { - if (StringTools.startsWith(name, parent.name)) { - name = StringTools.replace(name, parent.name + ":", ""); - } - } - #else - bakeValues(); + if (parentCurve != null && StringTools.startsWith(name, parentCurve)) + name = StringTools.replace(name, parentCurve + ":", ""); #end + return name; } override function copy(o:Prefab) { @@ -759,7 +763,8 @@ class Curve extends Prefab { return false; } - static inline function checkName(a: String, b: String) { + /** Curves are found by name, case insensitive **/ + public static inline function sameName(a: String, b: String) { return a == b || a.toLowerCase() == b.toLowerCase(); } @@ -769,7 +774,7 @@ class Curve extends Prefab { #if editor if (parent.shared.editor?.isHidden(c)) continue; #end - if(!checkName(c.name, name)) continue; + if(!sameName(c.name, name)) continue; var curve = c.to(Curve); if(curve == null) continue; return curve; @@ -786,7 +791,7 @@ class Curve extends Prefab { #end var idx = c.name.indexOf(":"); var curvePrefix = (idx >= 0) ? c.name.substr(0, idx) : c.name; - if(!checkName(curvePrefix, prefix)) + if(!sameName(curvePrefix, prefix)) continue; var curve = c.to(Curve); if(curve == null) continue; diff --git a/hrt/prefab/Shader.hx b/hrt/prefab/Shader.hx index b3208b657..0def5f343 100644 --- a/hrt/prefab/Shader.hx +++ b/hrt/prefab/Shader.hx @@ -19,11 +19,90 @@ class Shader extends Prefab { public var shader : hxsl.Shader; + /** + Under an emitter: the params evaluated over the particle life, by shader param name, with + their modes (see `hrt.prefab.fx.EmitterParam`). Colors keep their curves instead. + **/ + public var particleParams : Map = null; + function new(parent, sh: ContextShared) { super(parent, sh); props = {}; } + override function load(obj: Dynamic) { + super.load(obj); + particleParams = null; + var data : Dynamic = obj.particleParams; + if( data != null ) + for( name in Reflect.fields(data) ) { + var v : Dynamic = Reflect.field(data, name); + var value = hrt.prefab.fx.EmitterParam.parse(v, Std.isOfType(v, Array) ? (v : Array).length : 1); + if( value != null ) + setParticleParam(name, value); + } + var emitter = getParticleEmitter(); + if( emitter != null && @:privateAccess emitter.loadedLegacy ) + for( name => value in hrt.prefab.fx.EmitterUpgrade.upgradeShader(this, props, obj.children) ) + setParticleParam(name, value); + } + + override function save() : Dynamic { + var obj = super.save(); + if( particleParams != null ) { + var data = {}; + for( name => value in particleParams ) + Reflect.setField(data, name, hrt.prefab.fx.EmitterParam.save(value)); + obj.particleParams = data; + } + return obj; + } + + override function copy(data: Prefab) { + super.copy(data); + var from : Shader = cast data; + particleParams = from.particleParams == null ? null : [for( k => v in from.particleParams ) k => v.copy()]; + } + + /** The emitter whose particles the shader applies to, if any **/ + public function getParticleEmitter() : Null { + var p = parent; + while( p != null ) { + if( Std.isOfType(p, hrt.prefab.fx.Emitter) ) + return cast p; + if( Std.isOfType(p, hrt.prefab.l3d.Trails) ) + return null; + p = p.parent; + } + return null; + } + + /** The modes of a param over the particle life, the name is case insensitive like the old curves. **/ + public function getParticleParam(name: String) : Null { + if( particleParams == null ) + return null; + var v = particleParams.get(name); + if( v != null ) + return v; + for( k => v in particleParams ) + if( hrt.prefab.Curve.sameName(k, name) ) + return v; + return null; + } + + /** Sets the modes of a param over the particle life, null to unset it. **/ + public function setParticleParam(name: String, value: Null) { + if( hrt.prefab.fx.EmitterParam.isUnset(value) ) { + particleParams?.remove(name); + if( particleParams != null && !particleParams.iterator().hasNext() ) + particleParams = null; + return; + } + if( particleParams == null ) + particleParams = []; + particleParams.set(name, value); + } + public function makeShader() : hxsl.Shader { return null; } diff --git a/hrt/prefab/fx/BaseFX.hx b/hrt/prefab/fx/BaseFX.hx index e9b3544a1..c4c099309 100644 --- a/hrt/prefab/fx/BaseFX.hx +++ b/hrt/prefab/fx/BaseFX.hx @@ -28,6 +28,8 @@ class CustomAnimation extends Evaluator { /** This animation will need to keep being updated if setTime was called once **/ public var finishSync : Bool = false; public var startedPlaying : Bool = false; + /** Under an emitter: the particle whose randoms the values use **/ + public var particle : Int = 0; public function setTime(time: Float) { throw "This method should overrided!"; @@ -94,16 +96,16 @@ class ShaderAnimation extends CustomAnimation { var val : Dynamic; switch(v.type) { case TFloat: - var v = getFloat(param.value, time); + var v = getFloat(particle, param.value, time); shader.setParamIndexFloatValue(param.idx, v); continue; - case TInt: val = hxd.Math.round(getFloat(param.value, time)); - case TBool: val = getFloat(param.value, time) >= 0.5; + case TInt: val = hxd.Math.round(getFloat(particle, param.value, time)); + case TBool: val = getFloat(particle, param.value, time) >= 0.5; case TVec(4, VFloat): - getVector(param.value, time, vector4); + getVector(particle, param.value, time, vector4); val = vector4; case TVec(_, VFloat): - getVector(param.value, time, vector4); + getVector(particle, param.value, time, vector4); vector.set(vector4.x, vector4.y, vector4.z); val = vector; default: @@ -122,13 +124,13 @@ class ShaderDynAnimation extends ShaderAnimation { var v = param.def; switch(v.type) { case TFloat: - var val = getFloat(param.value, time); + var val = getFloat(particle, param.value, time); shader.setParamFloatValue(v, val); case TInt: - var val = hxd.Math.round(getFloat(param.value, time)); + var val = hxd.Math.round(getFloat(particle, param.value, time)); shader.setParamValue(v, val); case TBool: - var val = getFloat(param.value, time) >= 0.5; + var val = getFloat(particle, param.value, time) >= 0.5; shader.setParamValue(v, val); case TVec(4, VFloat): var index = shader.getParamIndex(v); @@ -137,7 +139,7 @@ class ShaderDynAnimation extends ShaderAnimation { vector = new h3d.Vector4(); shader.setParamValue(v, vector); } - getVector(param.value, time, vector); + getVector(particle, param.value, time, vector); case TVec(_, VFloat): var index = shader.getParamIndex(v); var vector = shader.getParamValue(index); @@ -145,7 +147,7 @@ class ShaderDynAnimation extends ShaderAnimation { vector = new h3d.Vector(); shader.setParamValue(v, vector); } - getVector(param.value, time, tmpVector4); + getVector(particle, param.value, time, tmpVector4); vector.set(tmpVector4.x, tmpVector4.y, tmpVector4.z); default: } @@ -238,6 +240,60 @@ class BaseFXTools { return ret; } + /** + The params of a shader under an emitter, evaluated over the particle life: the modes of + `Shader.particleParams` (see `EmitterParam`), colors excepted, which keep their curves. + **/ + public static function makeParticleShaderParams(shader: hrt.prefab.Shader, shaderDef: hxsl.SharedShader, params: hrt.prefab.fx.EmitterParam.ParamBuilder) : ShaderParams { + if(shaderDef == null) + return null; + var ret : ShaderParams = null; + var paramCount = 0; + for(v in shaderDef.data.vars) { + if(v.kind != Param) + continue; + paramCount++; + var val = switch(v.type) { + case TVec(_, VFloat) if(v.name.toLowerCase().indexOf("color") >= 0): + var curves = Curve.getCurves(shader, v.name); + curves == null || curves.length == 0 ? null : Curve.getColorValue(curves); + case TVec(n, VFloat): makeParticleShaderValue(shader, v.name, n, params); + case TFloat, TInt, TBool: makeParticleShaderValue(shader, v.name, 1, params); + default: null; + } + if(val == null) + continue; + if(ret == null) ret = []; + ret.push({ + idx: paramCount - 1, + def: v, + value: val, + }); + } + return ret; + } + + /** + The value of the param `name` of a shader under an emitter, `dim` its number of components + (1 for a float, a vector has at least 3, w is 1 when not given). Null when it is not animated. + An unset component takes the value of the shader prop. + **/ + public static function makeParticleShaderValue(shader: hrt.prefab.Shader, name: String, dim: Int, params: hrt.prefab.fx.EmitterParam.ParamBuilder) : Null { + var value = shader.getParticleParam(name); + if(value == null) + return null; + var prop : Dynamic = Reflect.field(shader.props, name); + inline function base(i: Int) : Float { + var v : Dynamic = dim == 1 ? prop : (Std.isOfType(prop, Array) ? (prop : Array)[i] : null); + return Std.isOfType(v, Float) || Std.isOfType(v, Int) ? v : 0.0; + } + var key = shader.getAbsPath(true) + "." + name; + if(dim == 1) + return params.comp(key, value[0], base(0), shader); + var c = [for(i in 0...dim) params.comp(key + "." + hrt.prefab.fx.EmitterParam.COMPONENTS[i], value[i], base(i), shader)]; + return VVector(c[0], c[1], dim > 2 ? c[2] : VZero, dim > 3 ? c[3] : null); + } + public static function makeRendererFXParams(rfxElt: hrt.prefab.rfx.RendererFX) { var classRfx = Type.getClass(cast rfxElt); var originalProps : Array = @:privateAccess Prefab.getSerializablePropsForClass(classRfx); @@ -267,17 +323,21 @@ class BaseFXTools { } static var emptyParams : Array = []; - public static function getCustomAnimations(elt: PrefabElement, anims: Array, ?batch: h3d.scene.MeshBatch) { + /** + `particleParams`: the shaders are under an emitter, their params are evaluated over the + particle life and allocate their random slots there. + **/ + public static function getCustomAnimations(elt: PrefabElement, anims: Array, ?batch: h3d.scene.MeshBatch, ?particleParams: hrt.prefab.fx.EmitterParam.ParamBuilder) { // Init all animations recursively except Emitter ones (called in Emitter) if(Std.downcast(elt, hrt.prefab.fx.Emitter) == null) { for(c in elt.children) { - getCustomAnimations(c, anims, batch); + getCustomAnimations(c, anims, batch, particleParams); } } var shader = elt.to(hrt.prefab.Shader); if (shader != null && shader.shader != null && !Std.isOfType(shader.shader, hrt.prefab.fx.gpuemitter.ComputeUtils) ) { - var params = makeShaderParams(shader, shader.getShaderDefinition()); + var params = particleParams != null ? makeParticleShaderParams(shader, shader.getShaderDefinition(), particleParams) : makeShaderParams(shader, shader.getShaderDefinition()); var shader = shader.shader; if(useAutoPerInstance && batch != null) @:privateAccess { diff --git a/hrt/prefab/fx/Emitter.hx b/hrt/prefab/fx/Emitter.hx index 28d30d0f0..9d92c98cb 100644 --- a/hrt/prefab/fx/Emitter.hx +++ b/hrt/prefab/fx/Emitter.hx @@ -4,7 +4,7 @@ import hrt.prefab.l3d.Polygon; import hrt.prefab.Curve; import hrt.prefab.fx.Value; import hrt.prefab.fx.Evaluator; -import hrt.prefab.fx.EmitterMacros.param; +import hrt.prefab.fx.EmitterParam; #if editor import hide.prefab.HideProps; @@ -92,6 +92,24 @@ class InstanceDef { public function new() { } } +/** The values an `EmitterObject` is built with, see `Emitter.makeValues`. **/ +@:publicFields +class EmitterValues { + var instDef : InstanceDef; + var startSpeed : Value; + var startWorldSpeed : Value; + var emitRate : Value; + var emitRateMin : Value; + var emitRateMax : Value; + var burstParticleCount : Value; + var emitAngle : Value; + var lifeTime : Float; + var lifeTimeRand : Float; + /** Holds the random slots allocated by the values, the particle shaders add theirs **/ + var builder : ParamBuilder; + function new() { } +} + typedef PartArray = #if (hl_ver >= version("1.14.0")) hl.CArray #else Array #end; typedef Single = #if (hl_ver >= version("1.13.0")) hl.F32 #else Float #end; @@ -619,6 +637,8 @@ class EmitterObject extends h3d.scene.Object { var meshTransform = new h3d.Matrix(); var randomValues : Array; var randSlots : Int; + /** The param component drawn by each random slot **/ + var randomKeys : Array; public function new(?parent) { super(parent); @@ -658,9 +678,8 @@ class EmitterObject extends h3d.scene.Object { scene = getScene(); } - function init(randSlots: Int, prefab: Emitter) { + function init(params: ParamBuilder, prefab: Emitter) { this.emitterPrefab = prefab; - this.randSlots = randSlots; updateParentNonEmitter(); @@ -752,7 +771,7 @@ class EmitterObject extends h3d.scene.Object { var shaders = findRec(emitterPrefab, hrt.prefab.Shader); for( shader in shaders ) { makeShaderInstance(shader); - hrt.prefab.fx.BaseFX.BaseFXTools.getCustomAnimations(shader, customAnims, batch); + hrt.prefab.fx.BaseFX.BaseFXTools.getCustomAnimations(shader, customAnims, batch, params); } // Animated textures animations @@ -794,8 +813,16 @@ class EmitterObject extends h3d.scene.Object { particles = #if (hl_ver >= version("1.14.0")) hl.CArray.alloc(ParticleInstance, maxCount) #else [for(i in 0...maxCount) new ParticleInstance()] #end; particlesCount = maxCount; + randomKeys = params.slots; + randSlots = randomKeys.length; randomValues = [for(i in 0...(maxCount * randSlots)) 0]; evaluator = new Evaluator(randomValues, randSlots); + // the particle shaders draw their randoms from the emitter + if( customAnims != null ) + for( anim in customAnims ) @:privateAccess { + anim.randValues = randomValues; + anim.stride = randSlots; + } var anim = getParentFX(); if(anim != null) @@ -849,6 +876,8 @@ class EmitterObject extends h3d.scene.Object { emitTarget = 0; listHead = null; + // Slot after slot, in the order the values were built (see ParamBuilder). Keyed draws would + // hash the particle and randomKeys[slot] instead, so that editing a param cannot shift the others. if(randomValues != null) { for(i in 0...randomValues.length) randomValues[i] = random.srand(); @@ -1423,8 +1452,9 @@ class EmitterObject extends h3d.scene.Object { return; inline tmpMat.load(p.absPos.toMatrix()); batch.worldPosition = tmpMat; - for( anim in customAnims ) { + for( anim in customAnims ) { var t = hxd.Math.clamp(p.life / p.lifeTime, 0.0, 1.0); + anim.particle = p.idx; anim.setTime(t); } if( animatedTextureShader != null ){ @@ -1679,10 +1709,6 @@ class EmitterObject extends h3d.scene.Object { class Emitter extends Object3D { // PROPERTIES - @:param({ t: PFloat(0, 10), group: "Properties" }) - public var lifeTime : Float = 1.0; - @:param({ t: PFloat(0, 1), group: "Properties" }) - public var lifeTimeRand : Float = 0.0; @:param({ t: PEnum(SubEmitterKind), disp: "Sub Kind", group: "Properties" }) public var subEmitterKind : SubEmitterKind = SubEmitterKind.SpawnOnDeath; @:param({ t: PFloat(0, 1), disp: "Speed Factor", group: "Properties" }) @@ -1788,44 +1814,57 @@ class Emitter extends Object3D { @:param({ t: PBool, disp: "Show Debug", group: "Debug" }) public var viewDebug : Bool = false; - // INSTANCES - @:instParam({ t: PVec(3, -10, 10), def: [0., 0., 0.], disp: "World Acceleration", group: "Particle Movement" }) - public var instWorldAcceleration : Array; - @:instParam({ t: PVec(3, -10, 10), def: [0., 0., 0.], disp: "Fixed Speed", group: "Particle Movement" }) - public var instSpeed : Array; - @:instParam({ t: PVec(3, -10, 10), def: [0., 0., 0.], disp: "Fixed World Speed", group: "Particle Movement" }) - public var instWorldSpeed : Array; - - // In instance param to avoid refactoring the param editor more, but this is no longer linked to the instances (hence the instance: false in the declaration) - @:instParam({ t: PVec(3, -10, 10), def: [0., 0., 0.], disp: "Start Speed", group: "Particle Movement", instance: false }) - public var instStartSpeed : Array; - @:instParam({ t: PVec(3, -10, 10), def: [0., 0., 0.], disp: "Start World Speed", group: "Particle Movement", instance: false }) - public var instStartWorldSpeed : Array; - - @:instParam({ t: PVec(3, -10, 10), def: [0., 0., 0.], disp: "Orbit Speed", group: "Particle Movement" }) - public var instOrbitSpeed : Array; - @:instParam({ t: PFloat(0, 2.0), def: 1., disp: "Orbit Speed over time", group: "Particle Movement" }) - public var instOrbitSpeedOverTime : Null; - @:instParam({ t: PVec(3, -10, 10), def: [0., 0., 0.], disp: "Acceleration", group: "Particle Movement" }) - public var instAcceleration : Array; - @:instParam({ t: PFloat(0, 10.0), def: 0., disp: "Max Velocity", group: "Limit Velocity" }) - public var instMaxVelocity : Null; - @:instParam({ t: PFloat(0, 10.0), def: 0., disp: "Dampen", group: "Limit Velocity" }) - public var instDampen : Null; - @:instParam({ t: PFloat(0, 2.0), def: 1., disp: "Scale", group: "Particle Transform" }) - public var instScale : Null; - @:instParam({ t: PFloat(0, 2.0), def: 1., disp: "Scale over time", group: "Particle Transform" }) - public var instScaleOverTime : Null; - @:instParam({ t: PVec(3, 0.0, 2.0), def: [1., 1., 1.], disp: "Stretch", group: "Particle Transform" }) - public var instStretch : Array; - @:instParam({ t: PFloat(0.0, 2.0), def: 0.0, disp: "Stretch Vel.", group: "Particle Transform" }) - public var instStretchVelocity : Null; - @:instParam({ t: PVec(3, 0, 360), def: [0., 0., 0.], disp: "Rotation", group: "Particle Transform" }) - public var instRotation : Array; - @:instParam({ t: PVec(3, -1, 1), def: [0., 0., 0.], disp: "Pivot", group: "Particle Transform" }) - public var instPivot : Array; - @:instParam({ t: PVec(3, -10, 10), def: [0., 0., 0.], disp: "Offset", group: "Particle Transform" }) - public var instOffset : Array; + /** Version of the param format, the older one is upgraded on load (see `EmitterUpgrade`) **/ + public static inline var PARAMS_VERSION = 2; + + /** The params with modes, see `EmitterParam`. The other ones are the fields above. **/ + public static var MODE_PARAMS : Array = [ + { name: "lifeTime", disp: "Life Time", group: Spawn, def: [1.], min: 0, max: 10, category: "Properties" }, + { name: "worldAcceleration", disp: "World Acceleration", group: Particle, def: [0., 0., 0.], min: -10, max: 10, category: "Particle Movement" }, + { name: "speed", disp: "Fixed Speed", group: Particle, def: [0., 0., 0.], min: -10, max: 10, category: "Particle Movement" }, + { name: "worldSpeed", disp: "Fixed World Speed", group: Particle, def: [0., 0., 0.], min: -10, max: 10, category: "Particle Movement" }, + { name: "startSpeed", disp: "Start Speed", group: Spawn, def: [0., 0., 0.], min: -10, max: 10, category: "Particle Movement", track: "instStartSpeed" }, + { name: "startWorldSpeed", disp: "Start World Speed", group: Spawn, def: [0., 0., 0.], min: -10, max: 10, category: "Particle Movement", track: "instStartWorldSpeed" }, + { name: "orbitSpeed", disp: "Orbit Speed", group: Particle, def: [0., 0., 0.], min: -10, max: 10, category: "Particle Movement" }, + { name: "orbitSpeedMultiplier", disp: "Orbit Speed Multiplier", group: Multiplier, def: [1.], min: 0, max: 2, category: "Particle Movement", track: "instOrbitSpeedOverTime" }, + { name: "acceleration", disp: "Acceleration", group: Particle, def: [0., 0., 0.], min: -10, max: 10, category: "Particle Movement" }, + { name: "maxVelocity", disp: "Max Velocity", group: Particle, def: [0.], min: 0, max: 10, category: "Limit Velocity" }, + { name: "dampen", disp: "Dampen", group: Particle, def: [0.], min: 0, max: 10, category: "Limit Velocity" }, + { name: "scale", disp: "Scale", group: Particle, def: [1.], min: 0, max: 2, category: "Particle Transform" }, + { name: "scaleMultiplier", disp: "Scale Multiplier", group: Multiplier, def: [1.], min: 0, max: 2, category: "Particle Transform", track: "instScaleOverTime" }, + { name: "stretch", disp: "Stretch", group: Particle, def: [1., 1., 1.], min: 0, max: 2, category: "Particle Transform" }, + { name: "stretchVelocity", disp: "Stretch Velocity", group: Particle, def: [0.], min: 0, max: 2, category: "Particle Transform" }, + { name: "rotation", disp: "Rotation", group: Particle, def: [0., 0., 0.], min: 0, max: 360, category: "Particle Transform" }, + { name: "pivot", disp: "Pivot", group: Particle, def: [0., 0., 0.], min: -1, max: 1, category: "Particle Transform" }, + { name: "offset", disp: "Offset", group: Particle, def: [0., 0., 0.], min: -10, max: 10, category: "Particle Transform" }, + ]; + public static var MODE_PARAMS_BY_NAME = [for( p in MODE_PARAMS ) p.name => p]; + + /** The animation curves of the spawn and multiplier params, as tracks of the FX timeline **/ + public static var instanceParams : Array = [for( p in MODE_PARAMS ) if( p.track != null ) ({ + name: p.track, + disp: p.disp, + t: p.def.length > 1 ? PVec(p.def.length, p.min, p.max) : PFloat(p.min, p.max), + def: p.def.length > 1 ? (p.def : Dynamic) : (p.def[0] : Dynamic), + groupName: p.category, + animate: true, + instance: false, + } : hrt.prefab.fx.EmitterHelper.ParamDef)]; + + /** The old params added to their curve instead of being multiplied by it **/ + static var LEGACY_ADDITIVE = ["Rotation", "Offset", "Pivot"]; + + /** The params with modes by name (see `MODE_PARAMS`), missing when all their components are unset **/ + public var params : Map = []; + + /** + Components that leave one random slot unused before their own. Emitters upgraded from the + old format keep its slot layout this way, so they draw the very same randoms. + **/ + public var skippedRandomSlots : Array = null; + + /** Loaded from the old format: the particle shaders under it are upgraded too **/ + @:allow(hrt.prefab.Shader) var loadedLegacy = false; public function new(parent, shared: ContextShared) { super(parent, shared); @@ -1834,14 +1873,38 @@ class Emitter extends Object3D { override function save() { var data = super.save(); - data.props = saveParams(); + var props : Dynamic = saveParams(); + props.paramsVersion = PARAMS_VERSION; + for( p in MODE_PARAMS ) { + var v = EmitterParam.save(params.get(p.name)); + if( v != null ) + Reflect.setField(props, p.name, v); + } + if( skippedRandomSlots != null ) + props.skippedRandomSlots = skippedRandomSlots.copy(); + data.props = props; return data; } override function load( obj : Dynamic ) { super.load(obj); - loadParams(obj.props); - props = null; + var props : Dynamic = obj.props ?? {}; + loadParams(props); + params = []; + for( p in MODE_PARAMS ) { + var v = EmitterParam.parse(Reflect.field(props, p.name), p.def.length); + if( v != null ) + params.set(p.name, v); + } + var skips : Array = props.skippedRandomSlots; + skippedRandomSlots = skips == null ? null : [for( s in skips ) Std.string(s)]; + loadedLegacy = props.paramsVersion == null; + if( loadedLegacy || EmitterUpgrade.hasLegacyFields(props, MODE_PARAMS) ) { + var skips = EmitterUpgrade.upgradeEmitter(this, props, obj.children, MODE_PARAMS, LEGACY_ADDITIVE, loadedLegacy, params); + if( skips != null ) + skippedRandomSlots = (skippedRandomSlots ?? []).concat(skips); + } + this.props = null; // Negative warmup time is an old version of delay if( warmUpTime < 0 ) { delay = -warmUpTime; @@ -1853,7 +1916,64 @@ class Emitter extends Object3D { override function copy(obj:Prefab) { super.copy(obj); - copyParams(cast obj); + var from : Emitter = cast obj; + copyParams(from); + params = [for( k => v in from.params ) k => v.copy()]; + skippedRandomSlots = from.skippedRandomSlots?.copy(); + } + + public function getParam( name : String ) : Null { + return params.get(name); + } + + /** Sets the modes of a param, null to unset all its components. **/ + public function setParam( name : String, v : Null ) { + if( EmitterParam.isUnset(v) ) + params.remove(name); + else + params.set(name, v); + } + + /** + The values the emitter object is built with. They are built in the order of the old + code, which allocated the random slots in that order. + **/ + public function makeValues() : EmitterValues { + var b = new ParamBuilder(this, skippedRandomSlots); + inline function param( name : String ) return b.param(MODE_PARAMS_BY_NAME.get(name), params.get(name)); + inline function animated( name : String, v : Float ) return b.animated(Evaluator.vVal(v), name, name); + var d = new InstanceDef(); + d.localSpeed = param("speed"); + d.worldSpeed = param("worldSpeed"); + d.orbitSpeed = param("orbitSpeed"); + d.orbitSpeedOverTime = param("orbitSpeedMultiplier"); + d.acceleration = param("acceleration"); + d.worldAcceleration = param("worldAcceleration"); + d.localOffset = param("offset"); + d.scale = param("scale"); + d.scaleOverTime = param("scaleMultiplier"); + d.dampen = param("dampen"); + d.maxVelocity = param("maxVelocity"); + d.stretch = param("stretch"); + d.stretchVelocity = param("stretchVelocity"); + d.rotation = param("rotation"); + d.pivot = param("pivot"); + var v = new EmitterValues(); + v.instDef = d; + v.burstParticleCount = animated("burstParticleCount", burstParticleCount); + v.emitRate = animated("emitRate", emitRate); + v.emitRateMin = animated("emitRateMin", emitRateMin); + v.emitRateMax = animated("emitRateMax", emitRateMax); + v.emitAngle = animated("emitAngle", emitAngle); + v.startSpeed = param("startSpeed"); + v.startWorldSpeed = param("startWorldSpeed"); + // drawn on the emitter random stream when the particle spawns + var life = params.get("lifeTime"); + var span = EmitterParam.span(life == null ? null : life[0], 1.0); + v.lifeTime = (span.min + span.max) * 0.5; + v.lifeTimeRand = (span.max - span.min) * 0.5; + v.builder = b; + return v; } override function getPreloadFiles():Array { @@ -1872,7 +1992,6 @@ class Emitter extends Object3D { return; } - var randIdx = 0; var template : Object3D = cast children.find( c -> EmitterObject.checkEnabled(c) && (c.name == null || c.name.indexOf("collision") == -1) && @@ -1881,67 +2000,8 @@ class Emitter extends Object3D { c.to(Emitter) == null && c.to(hrt.prefab.l3d.Trails) == null); - var getCurve = hrt.prefab.Curve.getCurve.bind(this); - - function makeParam(base: Float, randProp: Null, pname: String, suffix: String) : Value { - var xVal = Evaluator.vVal(base); - var randCurve = getCurve(pname + suffix + ":rand"); - var randVal : Value = VZero; - if(randCurve != null) - randVal = Evaluator.vMult(VRandom(randIdx++, 1.0, 0.0), Evaluator.vMult(randCurve.makeVal(), VConst(randProp != null ? randProp : 1.0))); - else if(randProp != null && randProp != 0.0) - randVal = VRandom(randIdx++, randProp, 0.0); - - var xCurve = getCurve(pname + suffix); - if (xCurve != null) { - if (xCurve.blendMode == CurveBlendMode.RandomBlend) { - var ca = Std.downcast(xCurve.children[0], Curve); - var cb = Std.downcast(xCurve.children[1], Curve); - return VRandomBetweenCurves(randIdx++, ca, cb); - } - else { - if (pname.indexOf("Rotation") >= 0 || pname.indexOf("Offset") >= 0 || pname.indexOf("Pivot") >= 0) - return Evaluator.vAdd(Evaluator.vAdd(xVal, randVal), xCurve.makeVal()); - else - return Evaluator.vMult(Evaluator.vAdd(xVal, randVal), xCurve.makeVal()); - } - } - else - return Evaluator.vAdd(xVal, randVal); - } - - function fparam(p: hrt.prefab.fx.EmitterHelper.ParamDef, base: Null, rand: Null) : Value { - return makeParam(base != null ? base : p.def, rand, p.name, ""); - } - - function vparam(p: hrt.prefab.fx.EmitterHelper.ParamDef, base: Array, rand: Array) : Value { - var b : Array = base != null ? base : p.def; - inline function comp(idx, suffix) return makeParam(b[idx], rand != null ? rand[idx] : null, p.name, suffix); - var v : Value = VVector(comp(0, ":x"), comp(1, ":y"), comp(2, ":z")); - if(v.match(VVector(VZero, VZero, VZero))) - v = VZero; - else if(v.match(VVector(VConst(1.0), VConst(1.0), VConst(1.0)))) - v = VConst(1.0); - return v; - } - - var d = new InstanceDef(); - d.localSpeed = param(instSpeed); - d.worldSpeed = param(instWorldSpeed); - d.orbitSpeed = param(instOrbitSpeed); - d.orbitSpeedOverTime = param(instOrbitSpeedOverTime); - d.acceleration = param(instAcceleration); - d.worldAcceleration = param(instWorldAcceleration); - d.localOffset = param(instOffset); - d.scale = param(instScale); - d.scaleOverTime = param(instScaleOverTime); - d.dampen = param(instDampen); - d.maxVelocity = param(instMaxVelocity); - d.stretch = param(instStretch); - d.stretchVelocity = param(instStretchVelocity); - d.rotation = param(instRotation); - d.pivot = param(instPivot); - emitterObj.instDef = d; + var values = makeValues(); + emitterObj.instDef = values.instDef; emitterObj.particleTemplate = template; // SUB-EMITTER @@ -1956,8 +2016,8 @@ class Emitter extends Object3D { emitterObj.seedGroup = seedGroup; emitterObj.subEmitterKind = subEmitterKind; // LIFE - emitterObj.lifeTime = lifeTime; - emitterObj.lifeTimeRand = lifeTimeRand; + emitterObj.lifeTime = values.lifeTime; + emitterObj.lifeTimeRand = values.lifeTimeRand; emitterObj.speedFactor = speedFactor; emitterObj.warmUpTime = warmUpTime; emitterObj.delay = delay; @@ -1965,22 +2025,22 @@ class Emitter extends Object3D { emitterObj.emitType = emitType; emitterObj.burstCount = Std.int(burstCount); emitterObj.burstDelay = burstDelay; - emitterObj.burstParticleCount = param(burstParticleCount); + emitterObj.burstParticleCount = values.burstParticleCount; emitterObj.emitDuration = emitDuration; emitterObj.simulationSpace = simulationSpace; emitterObj.particleScaling = particleScaling; emitterObj.emitOrientation = emitOrientation; emitterObj.maxCount = maxCount; emitterObj.sortMode = enableSort ? sortMode : None; - emitterObj.emitRate = param(emitRate); - emitterObj.emitRateMin = param(emitRateMin); - emitterObj.emitRateMax = param(emitRateMax); + emitterObj.emitRate = values.emitRate; + emitterObj.emitRateMin = values.emitRateMin; + emitterObj.emitRateMax = values.emitRateMax; emitterObj.emitRateChangeDelay = emitRateChangeDelay; emitterObj.emitShape = emitShape; emitterObj.followRotation = followRotation; emitterObj.applyMeshTransform = applyMeshTransform; // EMIT SHAPE - emitterObj.emitAngle = param(emitAngle); + emitterObj.emitAngle = values.emitAngle; emitterObj.emitRad1 = emitRad1; emitterObj.emitRad2 = emitRad2; emitterObj.emitSurface = emitSurface; @@ -2009,15 +2069,15 @@ class Emitter extends Object3D { emitterObj.randomGradient = randomGradient; // PARTICLE MOVEMENT - emitterObj.startSpeed = param(instStartSpeed); - emitterObj.startWorldSpeed = param(instStartWorldSpeed); + emitterObj.startSpeed = values.startSpeed; + emitterObj.startWorldSpeed = values.startWorldSpeed; // DEBUG #if editor emitterObj.debugGraphics.visible = viewDebug; #end - emitterObj.init(randIdx, this); + emitterObj.init(values.builder, this); #if editor if(propName == null || ["emitShape", "emitAngle", "emitRad1", "emitRad2"].indexOf(propName) >= 0) @@ -2184,41 +2244,45 @@ class Emitter extends Object3D { for (p in getVisibleParams()) addParam(p); - // Instance params are optional (null means default), "+" sets them and "-" removes them - function addOptional(cat: hide.kit.Category, name: String, label: String, t: hrt.prefab.Props.PropType, init: () -> Dynamic) { - var line = new hide.kit.Line(cat, name); - line.label = label; - var set = Reflect.field(this, name) != null; - if (set) { - switch (t) { - case PVec(n, min, max): - addVecSliders(line, name, n, min, max); - case PFloat(min, max): - makeSlider(line, name, "", Reflect.field(this, name), min, max, (v) -> Reflect.setField(this, name, v)); - default: - throw "Unsupported instance param type " + t; + // Params with modes: only the constant components are edited here, "+" sets a component + // to its default and "-" unsets the param + for (p in MODE_PARAMS) { + var line = new hide.kit.Line(getCategory(p.category), p.name); + line.label = p.disp; + var value = params.get(p.name); + var dim = p.def.length; + for (i in 0...dim) { + var id = p.name + "." + i; + var label = dim == 1 ? "" : ["X", "Y", "Z"][i]; + function set(m: ParamMode) { + var v : ParamValue = params.get(p.name)?.copy() ?? [for (_ in 0...dim) null]; + v[i] = m; + setParam(p.name, v); + } + switch (value == null ? null : value[i]) { + case null: + var btn = new hide.kit.Button(line, id, label + " +"); + btn.onClick = () -> { + set(MConstant(p.def[i])); + ctx.rebuildPrefab(this); + ctx.rebuildInspector(); + } + case MConstant(v): + makeSlider(line, id, label, v, p.min, p.max, (v) -> set(MConstant(v))); + case m: + new hide.kit.Text(line, id, label + " " + EmitterParam.kind(m)); } } - var btn = new hide.kit.Button(line, set ? "remove" : "add", set ? "-" : "+"); - btn.width = 1; - btn.onClick = () -> { - Reflect.setField(this, name, set ? null : init()); - ctx.rebuildPrefab(this); - ctx.rebuildInspector(); + if (value != null) { + var btn = new hide.kit.Button(line, "remove", "-"); + btn.width = 1; + btn.onClick = () -> { + setParam(p.name, null); + ctx.rebuildPrefab(this); + ctx.rebuildInspector(); + } } } - - for (p in instanceParams) { - var cat = getCategory(p.groupName ?? "Particles"); - addOptional(cat, p.name, p.disp ?? hide.kit.Macros.camelToSpaceCase(p.name), p.t, function() : Dynamic return switch (p.t) { - case PVec(_): ((p.def : Array).copy() : Dynamic); - default: p.def; - }); - addOptional(cat, hrt.prefab.fx.EmitterHelper.randProp(p.name), "~", p.t, function() : Dynamic return switch (p.t) { - case PVec(n): ([for (_ in 0...n) 0.0] : Dynamic); - default: (0.0 : Dynamic); - }); - } } #if editor @@ -2257,7 +2321,7 @@ class Emitter extends Object3D { } var params = getVisibleParams(); - EmitterHelper.generateEdit(params, instanceParams, this, ctx.properties, onChange, refresh, true); + EmitterHelper.generateEdit(params, [], this, ctx.properties, onChange, refresh, true); } override function setSelected(b : Bool ) { diff --git a/hrt/prefab/fx/EmitterMacros.hx b/hrt/prefab/fx/EmitterMacros.hx index 31efb8119..c7f8b0064 100644 --- a/hrt/prefab/fx/EmitterMacros.hx +++ b/hrt/prefab/fx/EmitterMacros.hx @@ -16,28 +16,11 @@ typedef Param = { var kind : ParamKind; var def : Expr; var pos : Position; - var inst : Bool; - var rand : Bool; } #end class EmitterMacros { - public static macro function param( e : haxe.macro.Expr ) : haxe.macro.Expr { - var name = switch( e.expr ) { - case EConst(CIdent(n)): n; - default: haxe.macro.Context.error("param() expects an emitter param name", e.pos); - } - var decl = macro @:pos(e.pos) $i{"P_" + name}; - var randName = name + "_rand"; - var hasRand = Lambda.exists(haxe.macro.Context.getLocalClass().get().fields.get(), f -> f.name == randName); - var rand = hasRand ? macro @:pos(e.pos) $i{randName} : macro @:pos(e.pos) null; - return switch( haxe.macro.Context.followWithAbstracts(haxe.macro.Context.typeof(e)) ) { - case TInst(_.get().name => "Array", _): macro @:pos(e.pos) vparam($decl, $e, $rand); - default: macro @:pos(e.pos) fparam($decl, $e, $rand); - } - } - #if macro static function metaObj( f : Field, name : String ) : Null> { @@ -75,34 +58,21 @@ class EmitterMacros { } } - static function zeroDef( k : ParamKind, pos : Position ) : Expr { - return switch( k ) { - case KVec(n): { expr: EArrayDecl([for( i in 0...n ) macro @:pos(pos) 0.0]), pos: pos }; - default: macro @:pos(pos) 0.0; - } - } - public static function build() : Array { var fields = Context.getBuildFields(); var pos = Context.currentPos(); var params : Array = []; var emitterDecls : Array = []; - var instDecls : Array = []; var extra : Array = []; for( f in fields ) { - var inst = false; var meta = metaObj(f, ":param"); - if( meta == null ) { - meta = metaObj(f, ":instParam"); - inst = meta != null; - } if( meta == null ) continue; - var ft = null, fdef = null; + var fdef = null; switch( f.kind ) { - case FVar(t, e): ft = t; fdef = e; + case FVar(_, e): fdef = e; default: Context.error("Emitter param must be a variable", f.pos); } @@ -110,14 +80,7 @@ class EmitterMacros { if( t == null ) Context.error("Missing param type", f.pos); var kind = kindOf(t); - var def = inst ? field(meta, "def") : fdef; - if( def == null ) - def = macro @:pos(f.pos) null; - if( inst ) { - if( fdef != null ) - Context.error("Instance param default belongs in the metadata", f.pos); - f.kind = FVar(macro : Null<$ft>, macro @:pos(f.pos) null); - } + var def = fdef ?? macro @:pos(f.pos) null; var decl : Array = [ { field: "name", expr: macro @:pos(f.pos) $v{f.name} }, @@ -128,8 +91,7 @@ class EmitterMacros { if( disp != null ) decl.push({ field: "disp", expr: disp }); var group = field(meta, "group"); if( group != null ) decl.push({ field: "groupName", expr: group }); - decl.push({ field: "animate", expr: inst ? macro true : (field(meta, "animate") ?? macro false) }); - if( inst ) decl.push({ field: "instance", expr: field(meta, "instance") ?? macro true }); + decl.push({ field: "animate", expr: field(meta, "animate") ?? macro false }); var declExpr : Expr = { expr: EObjectDecl(decl), pos: f.pos }; var declName = "P_" + f.name; @@ -139,19 +101,8 @@ class EmitterMacros { kind: FVar(macro : hrt.prefab.fx.EmitterHelper.ParamDef, declExpr), pos: f.pos, }); - (inst ? instDecls : emitterDecls).push(macro @:pos(f.pos) $i{declName}); - - params.push({ name: f.name, kind: kind, def: def, pos: f.pos, inst: inst, rand: false }); - - if( inst ) { - extra.push({ - name: f.name + "_rand", - access: [APublic], - kind: FVar(macro : Null<$ft>, macro null), - pos: f.pos, - }); - params.push({ name: f.name + "_rand", kind: kind, def: zeroDef(kind, f.pos), pos: f.pos, inst: true, rand: true }); - } + emitterDecls.push(macro @:pos(f.pos) $i{declName}); + params.push({ name: f.name, kind: kind, def: def, pos: f.pos }); } var load : Array = []; @@ -161,26 +112,22 @@ class EmitterMacros { for( p in params ) { var n = p.name; var def = p.def; - var nullable = p.inst; var pos = p.pos; var src = macro @:pos(pos) props.$n; load.push(switch( p.kind ) { - case KEnum(e) if( !nullable ): + case KEnum(e): macro @:pos(pos) this.$n = hrt.prefab.Macros.enumOrNullByName($e, $src, null, $def); case KGradient: macro @:pos(pos) this.$n = $src; case KVec(_): - nullable ? macro @:pos(pos) this.$n = hrt.prefab.fx.EmitterHelper.toArray($src) - : macro @:pos(pos) { var v = $src; this.$n = v == null ? $def : hrt.prefab.fx.EmitterHelper.toArray(v); }; + macro @:pos(pos) { var v = $src; this.$n = v == null ? $def : hrt.prefab.fx.EmitterHelper.toArray(v); }; default: - nullable ? macro @:pos(pos) this.$n = $src - : macro @:pos(pos) { var v = $src; this.$n = v == null ? $def : v; }; + macro @:pos(pos) { var v = $src; this.$n = v == null ? $def : v; }; }); var set = macro @:pos(pos) Reflect.setField(props, $v{n}, this.$n); - // a set rand keeps its slot even at its neutral value, the editor shows it as set - var changed = if( p.rand ) macro @:pos(pos) true else switch( p.kind ) { + var changed = switch( p.kind ) { case KVec(_): macro @:pos(pos) !hrt.prefab.fx.EmitterHelper.sameArray(this.$n, $def); case KGradient: macro @:pos(pos) true; default: macro @:pos(pos) this.$n != $def; @@ -189,7 +136,7 @@ class EmitterMacros { case KEnum(_): set = macro @:pos(pos) Reflect.setField(props, $v{n}, Type.enumConstructor(this.$n)); default: } - save.push(nullable || p.kind.match(KGradient) + save.push(p.kind.match(KGradient) ? macro @:pos(pos) if( this.$n != null && $changed ) $set : macro @:pos(pos) if( $changed ) $set); @@ -207,19 +154,12 @@ class EmitterMacros { kind: FVar(macro : Array, { expr: EArrayDecl(emitterDecls), pos: pos }), pos: pos, }); - extra.push({ - name: "instanceParams", - access: [APublic, AStatic], - kind: FVar(macro : Array, { expr: EArrayDecl(instDecls), pos: pos }), - pos: pos, - }); extra.push({ name: "PARAMS", access: [APublic, AStatic], kind: FVar(macro : Map, macro { var m = new Map(); for( p in emitterParams ) m.set(p.name, p); - for( p in instanceParams ) m.set(p.name, p); m; }), pos: pos, diff --git a/hrt/prefab/fx/EmitterParam.hx b/hrt/prefab/fx/EmitterParam.hx new file mode 100644 index 000000000..041bdac3e --- /dev/null +++ b/hrt/prefab/fx/EmitterParam.hx @@ -0,0 +1,396 @@ +package hrt.prefab.fx; + +import hrt.prefab.Curve; +import hrt.prefab.fx.Value; +import hrt.prefab.fx.Evaluator; + +/** + Which modes a param can take, and over which time it is read. + The emitter settings (rate, angle, shape dimensions...) are plain values, not part of this. +**/ +enum ParamGroup { + /** Constant or Range, read when the particle spawns. The value takes an animation curve over the FX time. **/ + Spawn; + /** All the modes, over the particle life. **/ + Particle; + /** Constant, read every frame. The value takes an animation curve over the FX time. **/ + Multiplier; +} + +/** What a curve mode follows over the particle life, from 0 (`from`) to 1 (`to`). **/ +enum ParamShape { + /** A curve prefab, by path from the prefab that owns the param (see `EmitterParam.resolveCurve`). Its blend, remap and loop options apply. **/ + SCurve(path : String); + SEasing(e : Easing); +} + +/** + The mode of one component of a param, with its fields. A null mode is an unset + component, which takes the default value of the param. + + `u` is a random in [0, 1], one per particle and component, shared by all the random + fields of the mode. `s(t)` is the shape at the particle life `t`. +**/ +enum ParamMode { + /** `value` **/ + MConstant( value : Float ); + /** `lerp(min, max, u)` **/ + MRange( min : Float, max : Float ); + /** `lerp(from, to, s(t))` **/ + MCurve( shape : ParamShape, from : Float, to : Float ); + /** `lerp(lerp(fromMin, fromMax, u), lerp(toMin, toMax, u), s(t))` **/ + MRandomCurve( shape : ParamShape, fromMin : Float, fromMax : Float, toMin : Float, toMax : Float ); + /** `lerp(lerp(aFrom, aTo, a(t)), lerp(bFrom, bTo, b(t)), u)` **/ + MCurveRange( a : ParamShape, aFrom : Float, aTo : Float, b : ParamShape, bFrom : Float, bTo : Float ); +} + +/** The name of a mode, as serialized **/ +enum abstract ParamModeKind(String) from String to String { + var KConstant = "Constant"; + var KRange = "Range"; + var KCurve = "Curve"; + var KRandomCurve = "RandomCurve"; + var KCurveRange = "CurveRange"; +} + +/** One mode per component (a scalar param has one), null when unset. A null `ParamValue` is a param with every component unset. **/ +typedef ParamValue = Array>; + +typedef ModeParamDef = { + var name : String; + var disp : String; + var group : ParamGroup; + /** Default value of each component, a scalar param has one **/ + var def : Array; + /** Slider range in the inspector **/ + var min : Float; + var max : Float; + /** Inspector category **/ + var category : String; + /** + Spawn and multiplier params: name of the animation curves over the FX time that multiply + the value, found by name among the children of the emitter (`:x/y/z` per component). + **/ + var ?track : String; +} + +class EmitterParam { + + public static var COMPONENTS = ["x", "y", "z", "w"]; + + public static function kind( m : ParamMode ) : ParamModeKind { + return switch( m ) { + case MConstant(_): KConstant; + case MRange(_): KRange; + case MCurve(_): KCurve; + case MRandomCurve(_): KRandomCurve; + case MCurveRange(_): KCurveRange; + } + } + + public static function modes( g : ParamGroup ) : Array { + return switch( g ) { + case Spawn: [KConstant, KRange]; + case Particle: [KConstant, KRange, KCurve, KRandomCurve, KCurveRange]; + case Multiplier: [KConstant]; + } + } + + /** Whether the mode draws a random per particle. **/ + public static function isRandom( m : Null ) { + return switch( m ) { + case null, MConstant(_), MCurve(_): false; + default: true; + } + } + + /** + The mode `kind` closest to `m` (an unset `m` is the constant `def`): the values at + the start / end of the curve and the low / high bounds of the ranges are kept, and + a missing shape is `Linear`. + **/ + public static function convert( m : Null, kind : ParamModeKind, def : Float ) : ParamMode { + var fromLo = def, fromHi = def, toLo = def, toHi = def; + var sa : ParamShape = null, sb : ParamShape = null; + switch( m ) { + case null: + case MConstant(v): fromLo = fromHi = toLo = toHi = v; + case MRange(lo, hi): fromLo = toLo = lo; fromHi = toHi = hi; + case MCurve(s, f, t): sa = sb = s; fromLo = fromHi = f; toLo = toHi = t; + case MRandomCurve(s, f0, f1, t0, t1): sa = sb = s; fromLo = f0; fromHi = f1; toLo = t0; toHi = t1; + case MCurveRange(a, af, at, b, bf, bt): sa = a; sb = b; fromLo = af; toLo = at; fromHi = bf; toHi = bt; + } + sa = sa ?? SEasing(Linear); + sb = sb ?? sa; + return switch( kind ) { + case KConstant: MConstant(toLo); + case KRange: MRange(toLo, toHi); + case KCurve: MCurve(sa, fromLo, toLo); + case KRandomCurve: MRandomCurve(sa, fromLo, fromHi, toLo, toHi); + case KCurveRange: MCurveRange(sa, fromLo, toLo, sb, fromHi, toHi); + default: MConstant(toLo); + } + } + + /** The bounds of a spawn mode (constant or range), an unset one is `def`. **/ + public static function span( m : Null, def : Float ) : { min : Float, max : Float } { + return switch( m ) { + case null: { min: def, max: def }; + case MConstant(v): { min: v, max: v }; + case MRange(min, max): { min: min, max: max }; + default: span(convert(m, KRange, def), def); + } + } + + // Serialization : a constant is a plain number, the other modes an object with a `mode` field. + + public static function parse( v : Dynamic, dim : Int ) : Null { + if( v == null ) + return null; + var out : ParamValue = []; + if( dim == 1 && !Std.isOfType(v, Array) ) + out.push(parseMode(v)); + else { + var a : Array = Std.isOfType(v, Array) ? v : [v]; + for( i in 0...dim ) + out.push(parseMode(a[i])); + } + return isUnset(out) ? null : out; + } + + public static function save( v : Null ) : Dynamic { + if( v == null || isUnset(v) ) + return null; + if( v.length == 1 ) + return saveMode(v[0]); + return [for( m in v ) saveMode(m)]; + } + + public static function isUnset( v : Null ) { + if( v != null ) + for( m in v ) + if( m != null ) + return false; + return true; + } + + public static function parseMode( v : Dynamic ) : Null { + if( v == null ) + return null; + if( Std.isOfType(v, Float) || Std.isOfType(v, Int) ) + return MConstant(v); + inline function num( v : Dynamic, def : Float ) : Float return v == null ? def : v; + inline function shape( o : Dynamic ) return parseShape(o); + var from : Dynamic = v.from, to : Dynamic = v.to; + return switch( (v.mode : ParamModeKind) ) { + case KConstant: MConstant(num(v.value, 0)); + case KRange: MRange(num(v.min, 0), num(v.max, 0)); + case KCurve: MCurve(shape(v), num(from, 0), num(to, 1)); + case KRandomCurve: MRandomCurve(shape(v), num(from?.min, 0), num(from?.max, 0), num(to?.min, 1), num(to?.max, 1)); + case KCurveRange: + var a : Dynamic = v.a ?? {}, b : Dynamic = v.b ?? {}; + MCurveRange(shape(a), num(a.from, 0), num(a.to, 1), shape(b), num(b.from, 0), num(b.to, 1)); + default: null; + } + } + + public static function saveMode( m : Null ) : Dynamic { + inline function setShape( o : Dynamic, s : ParamShape ) { + switch( s ) { + case SCurve(path): o.curve = path; + case SEasing(e): o.ease = (e : String); + } + } + inline function setSpan( o : Dynamic, from : Float, to : Float ) { + if( from != 0 ) o.from = from; + if( to != 1 ) o.to = to; + } + return switch( m ) { + case null: null; + case MConstant(v): v; + case MRange(min, max): { mode: (KRange : String), min: min, max: max }; + case MCurve(s, from, to): + var o : Dynamic = { mode: (KCurve : String) }; + setShape(o, s); + setSpan(o, from, to); + o; + case MRandomCurve(s, f0, f1, t0, t1): + var o : Dynamic = { mode: (KRandomCurve : String) }; + setShape(o, s); + if( f0 != 0 || f1 != 0 ) o.from = { min: f0, max: f1 }; + if( t0 != 1 || t1 != 1 ) o.to = { min: t0, max: t1 }; + o; + case MCurveRange(a, af, at, b, bf, bt): + var oa : Dynamic = {}, ob : Dynamic = {}; + setShape(oa, a); + setSpan(oa, af, at); + setShape(ob, b); + setSpan(ob, bf, bt); + { mode: (KCurveRange : String), a: oa, b: ob }; + } + } + + static function parseShape( o : Dynamic ) : ParamShape { + var curve : String = o.curve; + if( curve != null ) + return SCurve(curve); + var ease : String = o.ease; + return SEasing(ease != null && Easing.isValid(ease) ? ease : Linear); + } + + // Curve references + + /** + The curve at `path` under `owner`: names separated by dots, a name taken by several + siblings told apart with `name-N` like `Prefab.getUniqueName`. Null if it does not + exist or is disabled. + **/ + public static function resolveCurve( owner : Prefab, path : String ) : Null { + if( path == null ) + return null; + var p = owner; + var dashed = ~/^(.*)-([0-9]+)$/; + for( part in path.split(".") ) { + var found = null; + for( c in p.children ) + if( c.name == part ) { + found = c; + break; + } + if( found == null && dashed.match(part) ) { + var name = dashed.matched(1), n = Std.parseInt(dashed.matched(2)); + for( c in p.children ) + if( c.name == name && n-- == 0 ) { + found = c; + break; + } + } + if( found == null || !found.enabled ) + return null; + p = found; + } + var curve = p.to(Curve); + #if editor + if( curve != null && owner.shared.editor?.isHidden(curve) ) + return null; + #end + return curve; + } + + /** The path of `curve` from `owner`, for `SCurve`. Null if it is not under `owner`. **/ + public static function curvePath( owner : Prefab, curve : Curve ) : Null { + var parts = []; + var p : Prefab = curve; + while( p != owner ) { + if( p == null ) + return null; + parts.unshift(p.getUniqueName()); + p = p.parent; + } + return parts.join("."); + } +} + +/** + Builds the `Value` of the params of an emitter, and allocates their random slots: one per + random component, in build order. A slot is one random per particle, drawn by the emitter + (see `EmitterObject.reset`). +**/ +class ParamBuilder { + + var owner : Prefab; + var skips : Array; + + /** The component keys of the random slots, in slot order. **/ + public var slots(default, null) : Array = []; + + /** + `skips` lists the components that leave one slot unused before their own, which + keeps the slot layout of the emitters upgraded from the old format (see + `Emitter.skippedRandomSlots`). + **/ + public function new( owner : Prefab, ?skips : Array ) { + this.owner = owner; + this.skips = skips; + } + + function slot( key : String ) { + slots.push(key); + return slots.length - 1; + } + + /** A random in [lo, hi] on slot `rid`. **/ + static inline function range( rid : Int, lo : Float, hi : Float ) : Value { + return lo == hi ? Evaluator.vVal(lo) : VRandom(rid, (hi - lo) * 0.5, (hi + lo) * 0.5); + } + + /** `scope`: the prefab the curve paths start from, the owner by default. **/ + public function shape( s : ParamShape, ?scope : Prefab ) : Value { + return switch( s ) { + case SCurve(path): + // a missing curve leaves the value at `to` + var c = EmitterParam.resolveCurve(scope ?? owner, path); + c != null ? c.makeVal() : VConst(1.0); + case SEasing(e): VEasing(e); + } + } + + /** + The value of one component, `key` naming it for its random slot. `scope`: the prefab the + curve paths start from, the owner by default. + **/ + public function comp( key : String, m : Null, def : Float, ?scope : Prefab ) : Value { + if( skips != null && skips.indexOf(key) >= 0 ) + slot(key + "~"); + inline function v( f : Float ) return Evaluator.vVal(f); + return switch( m ) { + case null: v(def); + case MConstant(c): v(c); + case MRange(min, max): VRandom(slot(key), (max - min) * 0.5, (max + min) * 0.5); + case MCurve(s, from, to): Evaluator.vLerp(v(from), v(to), shape(s, scope)); + case MRandomCurve(s, f0, f1, t0, t1): + var rid = slot(key); + Evaluator.vLerp(range(rid, f0, f1), range(rid, t0, t1), shape(s, scope)); + case MCurveRange(a, af, at, b, bf, bt): + var rid = slot(key); + Evaluator.vLerp(Evaluator.vLerp(v(af), v(at), shape(a, scope)), Evaluator.vLerp(v(bf), v(bt), shape(b, scope)), VRandom(rid, 0.5, 0.5)); + } + } + + /** + `base` multiplied by the animation curve named `name` (over the FX time), the first + enabled child of the emitter with that name, case insensitive. + **/ + public function animated( base : Value, key : String, name : String ) : Value { + var c = Curve.getCurve(owner, name); + if( c == null ) + return base; + // random between the two child curves, the base value is not used + if( c.blendMode == RandomBlend ) + return VRandomBetweenCurves(slot(key), Std.downcast(c.children[0], Curve), Std.downcast(c.children[1], Curve)); + return Evaluator.vMult(base, c.makeVal()); + } + + /** + The value of a param. A vector is a `VVector` of 3 components (the third is 0 for a 2D + one), `VZero` when all are 0 and `VConst(1)` when all are 1. + **/ + public function param( def : ModeParamDef, value : Null ) : Value { + var dim = def.def.length; + inline function one( key : String, i : Int ) { + var c = comp(key, value == null ? null : value[i], def.def[i]); + if( def.track != null ) + c = animated(c, key, dim == 1 ? def.track : def.track + ":" + EmitterParam.COMPONENTS[i]); + return c; + } + if( dim == 1 ) + return one(def.name, 0); + var c = [for( i in 0...dim ) one(def.name + "." + EmitterParam.COMPONENTS[i], i)]; + var v : Value = VVector(c[0], c[1], c[2] ?? VZero); + if( v.match(VVector(VZero, VZero, VZero)) ) + v = VZero; + else if( v.match(VVector(VConst(1.0), VConst(1.0), VConst(1.0))) ) + v = VConst(1.0); + return v; + } +} diff --git a/hrt/prefab/fx/EmitterUpgrade.hx b/hrt/prefab/fx/EmitterUpgrade.hx new file mode 100644 index 000000000..793975a5d --- /dev/null +++ b/hrt/prefab/fx/EmitterUpgrade.hx @@ -0,0 +1,276 @@ +package hrt.prefab.fx; + +import hrt.prefab.Curve; +import hrt.prefab.fx.EmitterParam; + +/** + Reads the emitter params of the old format (emitters saved before `Emitter.PARAMS_VERSION`) + into the param modes, keeping the values they evaluate to. + + Old format, per param: a base value `k` (`instScale`), a random amount `a` (`instScale_rand`, + the value is `k + a * random(-1, 1)`), and curves found by name among the children + (`instScale`, `instStretch:x`, `instScale:rand`, ...), first enabled match, case insensitive. + A curve multiplies the value, or adds to it for the rotation, offset and pivot; a curve in + RandomBlend mode replaces it by a random between its two child curves. + + The curves stay where they are and the modes reference them by path. The animation curves + of the spawn and multiplier params (over the FX time) are left as they are: they keep + multiplying the value (see `ModeParamDef.track`). + + The upgrade reads the raw data of the children, it runs from `load`, before they exist. +**/ +class EmitterUpgrade { + + /** Called for old data the upgrade can only approximate. **/ + public static dynamic function onWarning( owner : Prefab, message : String ) { + } + + /** New param name => old field name **/ + public static var LEGACY_NAMES = [ + "lifeTime" => "lifeTime", + "startSpeed" => "instStartSpeed", + "startWorldSpeed" => "instStartWorldSpeed", + "speed" => "instSpeed", + "worldSpeed" => "instWorldSpeed", + "acceleration" => "instAcceleration", + "worldAcceleration" => "instWorldAcceleration", + "orbitSpeed" => "instOrbitSpeed", + "orbitSpeedMultiplier" => "instOrbitSpeedOverTime", + "maxVelocity" => "instMaxVelocity", + "dampen" => "instDampen", + "scale" => "instScale", + "scaleMultiplier" => "instScaleOverTime", + "stretch" => "instStretch", + "stretchVelocity" => "instStretchVelocity", + "rotation" => "instRotation", + "pivot" => "instPivot", + "offset" => "instOffset", + ]; + + static function legacyRand( name : String ) { + return name == "lifeTime" ? "lifeTimeRand" : name + "_rand"; + } + + /** Whether `props` holds params in the old format (an override applied over upgraded data can bring some back). **/ + public static function hasLegacyFields( props : Dynamic, defs : Array ) { + for( d in defs ) { + var old = LEGACY_NAMES.get(d.name); + if( old != d.name && Reflect.field(props, old) != null ) + return true; + if( Reflect.field(props, legacyRand(old)) != null ) + return true; + } + return false; + } + + // Raw children + + static function isCurve( data : Dynamic ) { + var info = @:privateAccess Prefab.getPrefabInfoByName(data.type); + return info != null && Prefab.isOfType(info.prefabClass, Curve); + } + + /** The names the children get once loaded (curves are renamed by `Curve.load`). **/ + static function loadedNames( children : Array, parentCurve : Null ) : Array { + return [for( c in children ) { + var name : String = c.name ?? ""; + isCurve(c) ? Curve.loadedName(name, parentCurve) : name; + }]; + } + + /** Path of child `i`, unique among its siblings like `Prefab.getUniqueName`. **/ + static function uniqueName( names : Array, i : Int ) { + var n = 0; + for( j in 0...i ) + if( names[j] == names[i] ) + n++; + return n > 0 ? names[i] + "-" + n : names[i]; + } + + /** Mirrors `Curve.getCurve`: the first enabled curve named `name`, case insensitive. **/ + static function findCurve( children : Array, names : Array, name : String ) : Null { + for( i => c in children ) { + if( c.enabled == false || !Curve.sameName(names[i], name) || !isCurve(c) ) + continue; + return i; + } + return null; + } + + static function blendMode( c : Dynamic ) : Int { + var b : Dynamic = c.blendMode; + if( b == null ) return 0; + return Std.isOfType(b, Int) ? b : Std.parseInt(b); + } + + static function num( v : Dynamic ) : Float { + return v == null || !(Std.isOfType(v, Float) || Std.isOfType(v, Int)) ? 0.0 : v; + } + + /** + Upgrades the old params of an emitter. `legacy` is set when the whole emitter is in the + old format: params with no base or random field are upgraded from their curves alone. + Otherwise only the params with old fields are (and replace the new ones). + Fills `params` and returns the components that need an unused random slot before their + own (see `Emitter.skippedRandomSlots`). + **/ + public static function upgradeEmitter( owner : Prefab, props : Dynamic, rawChildren : Array, defs : Array, + additive : Array, legacy : Bool, params : Map ) : Array { + var children : Array = rawChildren ?? []; + var names = loadedNames(children, null); + var skips = []; + + for( d in defs ) { + var old = LEGACY_NAMES.get(d.name); + var baseField : Dynamic = old != d.name ? Reflect.field(props, old) : null; + var randField : Dynamic = Reflect.field(props, legacyRand(old)); + if( baseField == null && randField == null && !legacy ) + continue; + var dim = d.def.length; + var cur = params.get(d.name); + var out : ParamValue = [for( i in 0...dim ) cur == null ? null : cur[i]]; + var changed = false; + + for( i in 0...dim ) { + var suffix = dim == 1 ? "" : ":" + EmitterParam.COMPONENTS[i]; + var key = dim == 1 ? d.name : d.name + "." + EmitterParam.COMPONENTS[i]; + // the old code reads the arrays as Float, a missing element is 0 + inline function comp( v : Dynamic ) : Null { + return v == null ? null : dim == 1 ? (Std.isOfType(v, Array) ? null : num(v)) : (Std.isOfType(v, Array) ? num((v : Array)[i]) : null); + } + var base = comp(baseField); + // lifeTime was already a plain value, read as a constant + if( d.name == "lifeTime" && out[i] != null ) + base = switch( out[i] ) { case MConstant(v): v; default: base; }; + var rand = comp(randField); + var hasRand = rand != null && rand != 0; + var k = base ?? d.def[i]; + + var ci = findCurve(children, names, old + suffix); + var ri = findCurve(children, names, old + suffix + ":rand"); + var isBlend = ci != null && blendMode(children[ci]) == 2; + // random slots the old code took: one for the random amount, one for a RandomBlend curve + var oldSlots = (hasRand || ri != null ? 1 : 0) + (isBlend ? 1 : 0); + var mode : Null = null; + if( d.group != Particle ) { + // the curves of these params are animation curves over the FX time, they are left as they are + // (a RandomBlend one still takes its slot, see `ParamBuilder.animated`) + if( isBlend ) oldSlots--; + if( ri != null ) + onWarning(owner, '${old + suffix}:rand: a random curve on a ${d.group} param has no equivalent, ignored'); + if( hasRand && d.group == Spawn ) + mode = MRange(k - rand, k + rand); + else { + if( hasRand ) + onWarning(owner, '${d.name}: random amount $rand ignored, a multiplier is a constant'); + if( base != null || hasRand ) + mode = MConstant(k); + } + } else { + var isAdditive = Lambda.exists(additive, a -> old.indexOf(a) >= 0); + var curve = ci == null ? null : SCurve(uniqueName(names, ci)); + if( ri != null && ci == null ) { + // the random amount follows a curve: k + a * random * curve(t) + var a = rand ?? 1.0; + mode = MRandomCurve(SCurve(uniqueName(names, ri)), k, k, k - a, k + a); + } else { + if( ri != null ) + onWarning(owner, '${old + suffix}:rand: a random curve next to a curve has no equivalent, ignored'); + if( isBlend ) { + // lerp(curve0(t), curve1(t), random), the base and random amount are not used + var sub : Array = children[ci].children ?? []; + var subNames = loadedNames(sub, names[ci]); + if( sub.length < 2 || !isCurve(sub[0]) || !isCurve(sub[1]) ) + onWarning(owner, '${old + suffix}: random blend without its two child curves'); + else if( sub[0].enabled == false || sub[1].enabled == false ) + onWarning(owner, '${old + suffix}: disabled child curve of a random blend'); + inline function path( j : Int ) return SCurve(uniqueName(names, ci) + "." + (j < sub.length ? uniqueName(subNames, j) : "0")); + mode = MCurveRange(path(0), 0, 1, path(1), 0, 1); + } else if( curve != null ) { + if( isAdditive ) + mode = hasRand ? MRandomCurve(curve, k - rand, k + rand, k - rand + 1, k + rand + 1) : MCurve(curve, k, k + 1); + else + mode = hasRand ? MRandomCurve(curve, 0, 0, k - rand, k + rand) : MCurve(curve, 0, k); + } else if( hasRand ) + mode = MRange(k - rand, k + rand); + else if( base != null ) + mode = MConstant(k); + } + } + if( oldSlots > (EmitterParam.isRandom(mode) ? 1 : 0) ) + skips.push(key); + if( mode != null || baseField != null || randField != null ) { + out[i] = mode; + changed = true; + } + } + if( changed ) { + if( EmitterParam.isUnset(out) ) + params.remove(d.name); + else + params.set(d.name, out); + } + } + return skips.length > 0 ? skips : null; + } + + /** + Upgrades the curves of a shader under an emitter, evaluated over the particle life. + The shader definition is not known at load, the kind of a param comes from its stored + value: a float was `curve * value`, a vector came from its component curves alone, + 0 for the others (1 for w). Colors keep their curves, they are not upgraded. + **/ + public static function upgradeShader( owner : Prefab, props : Dynamic, rawChildren : Array ) : Map { + var children : Array = rawChildren ?? []; + var names = loadedNames(children, null); + var out = new Map(); + var seen = new Map(); + for( i => c in children ) { + if( c.enabled == false || !isCurve(c) ) + continue; + var prefix = names[i].split(":")[0]; + var low = prefix.toLowerCase(); + if( seen.exists(low) ) + continue; + seen.set(low, true); + + // the runtime finds the curves from the shader var name, case insensitive + var name = prefix; + var prop : Dynamic = null; + if( props != null ) + for( f in Reflect.fields(props) ) + if( f.toLowerCase() == low ) { + name = f; + prop = Reflect.field(props, f); + } + var group = [for( j => c in children ) if( c.enabled != false && isCurve(c) && Curve.sameName(names[j].split(":")[0], name) ) j]; + var components = Lambda.exists(group, j -> names[j].indexOf(":") >= 0); + var isVector = Std.isOfType(prop, Array) || (prop == null && components); + if( isVector && low.indexOf("color") >= 0 ) + continue; + if( isVector ) { + var dim = Std.isOfType(prop, Array) ? hxd.Math.imin((prop : Array).length, 4) : 4; + var v : ParamValue = []; + for( k in 0...dim ) { + var suffix = ":" + EmitterParam.COMPONENTS[k]; + var j = Lambda.find(group, j -> StringTools.endsWith(names[j].toLowerCase(), suffix)); + v.push(j != null ? MCurve(SCurve(uniqueName(names, j)), 0, 1) : MConstant(k == 3 ? 1 : 0)); + } + out.set(name, v); + } else { + var j = findCurve(children, names, name); + // component curves on a float are not read + if( j == null ) + continue; + // the old code multiplied by the value when it is a number, by 1 otherwise, and a missing + // value is the default of the param: 0 for a float + var isNumber = Std.isOfType(prop, Float) || Std.isOfType(prop, Int); + var base = isNumber ? num(prop) : prop != null ? 1.0 : 0.0; + if( prop == null ) + onWarning(owner, '$name: no stored value, taken as a float of default 0'); + out.set(name, [MCurve(SCurve(uniqueName(names, j)), 0, base)]); + } + } + return out; + } +} From 5fefc278154dc77782f717d125b108f6b644e6ca Mon Sep 17 00:00:00 2001 From: trethaller Date: Mon, 12 Oct 2026 16:07:09 +0200 Subject: [PATCH 009/191] Emitter2D: params with modes, upgrade of the old format on load Co-Authored-By: Claude Opus 5.5 --- hrt/prefab/fx/Emitter2D.hx | 295 +++++++++++++++++++++++-------------- 1 file changed, 181 insertions(+), 114 deletions(-) diff --git a/hrt/prefab/fx/Emitter2D.hx b/hrt/prefab/fx/Emitter2D.hx index 266a6389c..001c42350 100644 --- a/hrt/prefab/fx/Emitter2D.hx +++ b/hrt/prefab/fx/Emitter2D.hx @@ -1,6 +1,7 @@ package hrt.prefab.fx; import hrt.prefab.fx.Emitter; +import hrt.prefab.fx.EmitterParam; import hrt.impl.Gradient; import hrt.prefab.Curve; @@ -241,6 +242,28 @@ class Particle2DInstance extends h2d.SpriteBatch.BatchElement { } } +/** The values an `Emitter2DObject` is built with, see `Emitter2D.makeValues`. **/ +@:publicFields +class Emitter2DValues { + var instDef : InstanceDef; + var startSpeed : Value; + var startWorldSpeed : Value; + var emitRate : Value; + var emitRateMin : Value; + var emitRateMax : Value; + var burstParticleCount : Value; + var emitRadius : Value; + var emitAngle1 : Value; + var emitAngle2 : Value; + var emitWidth : Value; + var emitHeight : Value; + var lifeTime : Float; + var lifeTimeRand : Float; + /** Holds the random slots allocated by the values **/ + var builder : ParamBuilder; + function new() { } +} + class Emitter2DObject extends h2d.Object { public var particles : Array = []; public var batch : h2d.SpriteBatch; @@ -493,8 +516,6 @@ class Emitter2D extends Object2D { { name: "emitSurface", t: PBool, def: false, disp: "Surface", groupName : "Shape Emission" }, // PARTICLE - { name: "lifeTime", t: PFloat(0, 10), def: 1.0, groupName : "Particle" }, - { name: "lifeTimeRand", t: PFloat(0, 1), def: 0.0, groupName : "Particle" }, { name: "spriteType", t: PEnum(SpriteType), def: SpriteType.Color, groupName : "Particle" }, { name: "color", t: PVec(4), def: [0,0,0,1], groupName : "Particle" }, { name: "texture", t: PTexture, def: "", groupName : "Particle" }, @@ -513,40 +534,60 @@ class Emitter2D extends Object2D { { name: "animationBlendBetweenFrames", t: PBool, def: true, groupName : "Particle", disp: "Blend frames" }, ]; - public static var instanceParams : Array = [ - // PARTICLE TRANSFORM - { name: "instScale", t: PFloat(0, 2.0), def: 1., disp: "Scale", groupName: "Particle Transform"}, - { name: "instScaleOverTime", t: PFloat(0, 2.0), def: 1., disp: "Scale over time", groupName: "Particle Transform"}, - { name: "instStretch", t: PVec(2, 0.0, 2.0), def: [1.,1.], disp: "Stretch", groupName: "Particle Transform"}, - { name: "instRotation", t: PFloat(0, 360), def: 0., disp: "Rotation", groupName: "Particle Transform"}, - { name: "instOffset", t: PVec(2, -10, 10), def: [0.,0.], disp: "Offset", groupName: "Particle Transform"}, + /** Version of the param format, the older one is upgraded on load (see `EmitterUpgrade`) **/ + public static inline var PARAMS_VERSION = 2; + /** The params with modes, see `EmitterParam`. The other ones are `emitterParams`. **/ + public static var MODE_PARAMS : Array = [ + { name: "lifeTime", disp: "Life Time", group: Spawn, def: [1.], min: 0, max: 10, category: "Particle" }, + // PARTICLE TRANSFORM + { name: "scale", disp: "Scale", group: Particle, def: [1.], min: 0, max: 2, category: "Particle Transform" }, + { name: "scaleMultiplier", disp: "Scale Multiplier", group: Multiplier, def: [1.], min: 0, max: 2, category: "Particle Transform", track: "instScaleOverTime" }, + { name: "stretch", disp: "Stretch", group: Particle, def: [1., 1.], min: 0, max: 2, category: "Particle Transform" }, + { name: "rotation", disp: "Rotation", group: Particle, def: [0.], min: 0, max: 360, category: "Particle Transform" }, + { name: "offset", disp: "Offset", group: Particle, def: [0., 0.], min: -10, max: 10, category: "Particle Transform" }, // PARTICLE MOVEMENT - { name: "instAcceleration", t: PVec(2, -10, 10), def: [0.,0.], disp: "Acceleration", groupName: "Particle Movement"}, - { name: "instWorldAcceleration", t: PVec(2, -10, 10), def: [0.,0.], disp: "World Acceleration", groupName: "Particle Movement"}, - { name: "instSpeed", t: PVec(2, -10, 10), def: [0.,0.], disp: "Fixed Speed", groupName: "Particle Movement" }, - { name: "instWorldSpeed", t: PVec(2, -10, 10), def: [0.,0.], disp: "Fixed World Speed", groupName: "Particle Movement"}, - // In instance param to avoid refactoring the param editor more, but this is no longer linked to the instances (hence the instance: false in the declaration) - { name: "instStartSpeed", t: PVec(2, -10, 10), def: [0.,0.], disp: "Start Speed", groupName: "Particle Movement", instance: false}, - { name: "instStartWorldSpeed", t: PVec(2, -10, 10), def: [0.,0.], disp: "Start World Speed", groupName: "Particle Movement", instance: false}, - { name: "instOrbitSpeed", t: PFloat(-10., 10.), def: 0., disp: "Orbit Speed", groupName: "Particle Movement"}, - { name: "instOrbitSpeedOverTime", t: PFloat(0, 2.0), def: 1., disp: "Orbit Speed over time", groupName: "Particle Movement"}, - + { name: "acceleration", disp: "Acceleration", group: Particle, def: [0., 0.], min: -10, max: 10, category: "Particle Movement" }, + { name: "worldAcceleration", disp: "World Acceleration", group: Particle, def: [0., 0.], min: -10, max: 10, category: "Particle Movement" }, + { name: "speed", disp: "Fixed Speed", group: Particle, def: [0., 0.], min: -10, max: 10, category: "Particle Movement" }, + { name: "worldSpeed", disp: "Fixed World Speed", group: Particle, def: [0., 0.], min: -10, max: 10, category: "Particle Movement" }, + { name: "startSpeed", disp: "Start Speed", group: Spawn, def: [0., 0.], min: -10, max: 10, category: "Particle Movement", track: "instStartSpeed" }, + { name: "startWorldSpeed", disp: "Start World Speed", group: Spawn, def: [0., 0.], min: -10, max: 10, category: "Particle Movement", track: "instStartWorldSpeed" }, + { name: "orbitSpeed", disp: "Orbit Speed", group: Particle, def: [0.], min: -10, max: 10, category: "Particle Movement" }, + { name: "orbitSpeedMultiplier", disp: "Orbit Speed Multiplier", group: Multiplier, def: [1.], min: 0, max: 2, category: "Particle Movement", track: "instOrbitSpeedOverTime" }, // PARTICLE LIMIT VELOCITY - { name: "instMaxVelocity", t: PFloat(0, 10.0), def: 0., disp: "Max Velocity", groupName: "Particle Limit Velocity"}, - { name: "instDampen", t: PFloat(0, 10.0), def: 0., disp: "Dampen", groupName: "Particle Limit Velocity"}, + { name: "maxVelocity", disp: "Max Velocity", group: Particle, def: [0.], min: 0, max: 10, category: "Particle Limit Velocity" }, + { name: "dampen", disp: "Dampen", group: Particle, def: [0.], min: 0, max: 10, category: "Particle Limit Velocity" }, ]; + public static var MODE_PARAMS_BY_NAME = [for( p in MODE_PARAMS ) p.name => p]; + + /** The animation curves of the spawn and multiplier params, as tracks of the FX timeline **/ + public static var instanceParams : Array = [for( p in MODE_PARAMS ) if( p.track != null ) ({ + name: p.track, + disp: p.disp, + t: p.def.length > 1 ? PVec(p.def.length, p.min, p.max) : PFloat(p.min, p.max), + def: p.def.length > 1 ? (p.def : Dynamic) : (p.def[0] : Dynamic), + groupName: p.category, + animate: true, + instance: false, + } : hrt.prefab.fx.EmitterHelper.ParamDef)]; + + /** The old params added to their curve instead of being multiplied by it **/ + static var LEGACY_ADDITIVE = ["Rotation", "Offset"]; + + /** The params with modes by name (see `MODE_PARAMS`), missing when all their components are unset **/ + public var params : Map = []; + + /** See `Emitter.skippedRandomSlots` **/ + public var skippedRandomSlots : Array = null; public static var PARAMS : Map = { var map = new Map(); for(e in emitterParams) { map.set(e.name, e); } - for(i in instanceParams) { - i.instance = i.instance ?? true; - i.animate = true; + for(i in instanceParams) map.set(i.name, i); - } map; }; @@ -582,11 +623,22 @@ class Emitter2D extends Object2D { Reflect.deleteField(data.props, param.name); } } + var props : Dynamic = data.props; + props.paramsVersion = PARAMS_VERSION; + for( p in MODE_PARAMS ) { + var v = EmitterParam.save(params.get(p.name)); + if( v != null ) + Reflect.setField(props, p.name, v); + } + if( skippedRandomSlots != null ) + props.skippedRandomSlots = skippedRandomSlots.copy(); return data; } override function load( obj : Dynamic ) { super.load(obj); + // props is the loaded data itself, which is not ours to change + props = Reflect.copy(props) ?? {}; for(param in emitterParams) { if(Reflect.hasField(obj.props, param.name)) { var val : Dynamic = Reflect.field(obj.props, param.name); @@ -610,6 +662,31 @@ class Emitter2D extends Object2D { else if (param.name == "randomGradient") (props:Dynamic).randomGradient = Gradient.getDefaultGradientData(); } + + var data : Dynamic = obj.props ?? {}; + params = []; + for( p in MODE_PARAMS ) { + var v = EmitterParam.parse(Reflect.field(data, p.name), p.def.length); + if( v != null ) + params.set(p.name, v); + } + var skips : Array = data.skippedRandomSlots; + skippedRandomSlots = skips == null ? null : [for( s in skips ) Std.string(s)]; + var legacy = data.paramsVersion == null; + if( legacy || EmitterUpgrade.hasLegacyFields(data, MODE_PARAMS) ) { + var skips = EmitterUpgrade.upgradeEmitter(this, data, obj.children, MODE_PARAMS, LEGACY_ADDITIVE, legacy, params); + if( skips != null ) + skippedRandomSlots = (skippedRandomSlots ?? []).concat(skips); + } + // the params with modes are not kept in props, nor their old fields + var props : Dynamic = props; + for( p in MODE_PARAMS ) { + var old = EmitterUpgrade.LEGACY_NAMES.get(p.name); + for( f in [p.name, old, old + "_rand", p.name + "Rand"] ) + Reflect.deleteField(props, f); + } + Reflect.deleteField(props, "paramsVersion"); + Reflect.deleteField(props, "skippedRandomSlots"); } override function copy(obj:Prefab) { @@ -627,6 +704,68 @@ class Emitter2D extends Object2D { else if(param.def != null) EmitterHelper.resetParam(props, param); } + var from : Emitter2D = cast obj; + params = [for( k => v in from.params ) k => v.copy()]; + skippedRandomSlots = from.skippedRandomSlots?.copy(); + } + + public function getParam( name : String ) : Null { + return params.get(name); + } + + /** Sets the modes of a param, null to unset all its components. **/ + public function setParam( name : String, v : Null ) { + if( EmitterParam.isUnset(v) ) + params.remove(name); + else + params.set(name, v); + } + + /** + The values the emitter object is built with. They are built in the order of the old + code, which allocated the random slots in that order. + **/ + public function makeValues() : Emitter2DValues { + var b = new ParamBuilder(this, skippedRandomSlots); + inline function param( name : String ) return b.param(MODE_PARAMS_BY_NAME.get(name), params.get(name)); + inline function animated( name : String ) { + var v : Float = EmitterHelper.getParamVal(PARAMS, props, name); + return b.animated(Evaluator.vVal(v), name, name); + } + var d = new InstanceDef(); + d.localSpeed = param("speed"); + d.worldSpeed = param("worldSpeed"); + d.orbitSpeed = param("orbitSpeed"); + d.orbitSpeedOverTime = param("orbitSpeedMultiplier"); + d.acceleration = param("acceleration"); + d.worldAcceleration = param("worldAcceleration"); + d.localOffset = param("offset"); + d.scale = param("scale"); + d.scaleOverTime = param("scaleMultiplier"); + d.dampen = param("dampen"); + d.maxVelocity = param("maxVelocity"); + d.stretch = param("stretch"); + d.rotation = param("rotation"); + var v = new Emitter2DValues(); + v.instDef = d; + v.burstParticleCount = animated("burstParticleCount"); + v.emitRate = animated("emitRate"); + v.emitRateMin = animated("emitRateMin"); + v.emitRateMax = animated("emitRateMax"); + v.emitRadius = animated("emitRadius"); + v.emitAngle1 = animated("emitAngle1"); + v.emitAngle2 = animated("emitAngle2"); + v.emitWidth = animated("emitWidth"); + v.emitHeight = animated("emitHeight"); + v.startSpeed = param("startSpeed"); + v.startWorldSpeed = param("startWorldSpeed"); + // drawn on the emitter random stream when the particle spawns + var life = params.get("lifeTime"); + var span = EmitterParam.span(life == null ? null : life[0], 1.0); + v.lifeTime = (span.min + span.max) * 0.5; + v.lifeTimeRand = (span.max - span.min) * 0.5; + v.builder = b; + return v; } override function makeObject(parent2d : h2d.Object) { @@ -637,108 +776,36 @@ class Emitter2D extends Object2D { super.updateInstance(propName); var emitterObj = Std.downcast(local2d, Emitter2DObject); - var randIdx = 0; - - function makeParam(scope: Prefab, name: String): Value { - var getCurve = hrt.prefab.Curve.getCurve.bind(scope); - - function makeCompVal(baseProp: Null, defVal: Float, randProp: Null, pname: String, suffix: String) : Value { - var xVal = Evaluator.vVal(baseProp != null ? baseProp : defVal); - var randCurve = getCurve(pname + suffix + ":rand"); - var randVal : Value = VZero; - if(randCurve != null) - randVal = Evaluator.vMult(VRandom(randIdx++, 1.0, 0.0), Evaluator.vMult(randCurve.makeVal(), VConst(randProp != null ? randProp : 1.0))); - else if(randProp != null && randProp != 0.0) - randVal = VRandom(randIdx++, randProp, 0.0); - - var xCurve = getCurve(pname + suffix); - if (xCurve != null) { - if (xCurve.blendMode == CurveBlendMode.RandomBlend) { - var ca = Std.downcast(xCurve.children[0], Curve); - var cb = Std.downcast(xCurve.children[1], Curve); - return VRandomBetweenCurves(randIdx++, ca, cb); - } - else { - if (pname.indexOf("Rotation") >= 0 || pname.indexOf("Offset") >= 0) - return Evaluator.vAdd(Evaluator.vAdd(xVal, randVal), xCurve.makeVal()); - else - return Evaluator.vMult(Evaluator.vAdd(xVal, randVal), xCurve.makeVal()); - } - } - else - return Evaluator.vAdd(xVal, randVal); - } - - var baseProp: Dynamic = Reflect.field(props, name); - var randProp: Dynamic = Reflect.field(props, EmitterHelper.randProp(name)); - var param = PARAMS.get(name); - switch(param.t) { - case PVec(_): - inline function makeComp(idx, suffix) { - return makeCompVal( - baseProp != null ? (baseProp[idx] : Float) : null, - param.def != null ? param.def[idx] : 0.0, - randProp != null ? (randProp[idx] : Float) : null, - param.name, suffix); - } - var v : Value = VVector( - makeComp(0, ":x"), - makeComp(1, ":y"), - makeComp(2, ":z")); - if(v.match(VVector(VZero, VZero, VZero))) - v = VZero; - else if(v.match(VVector(VConst(1.0), VConst(1.0), VConst(1.0)))) - v = VConst(1.0); - return v; - - default: - return makeCompVal(baseProp, param.def != null ? param.def : 0.0, randProp, param.name, ""); - } - } - - var d = new InstanceDef(); - d.localSpeed = makeParam(this, "instSpeed"); - d.worldSpeed = makeParam(this, "instWorldSpeed"); - d.orbitSpeed = makeParam(this, "instOrbitSpeed"); - d.orbitSpeedOverTime = makeParam(this, "instOrbitSpeedOverTime"); - d.acceleration = makeParam(this, "instAcceleration"); - d.worldAcceleration = makeParam(this, "instWorldAcceleration"); - d.localOffset = makeParam(this, "instOffset"); - d.scale = makeParam(this, "instScale"); - d.scaleOverTime = makeParam(this, "instScaleOverTime"); - d.dampen = makeParam(this, "instDampen"); - d.maxVelocity = makeParam(this, "instMaxVelocity"); - d.stretch = makeParam(this, "instStretch"); - d.rotation = makeParam(this, "instRotation"); - emitterObj.instDef = d; + var values = makeValues(); + emitterObj.instDef = values.instDef; emitterObj.blendMode = this.blendMode; emitterObj.batch.blendMode = this.blendMode; emitterObj.spriteType = EmitterHelper.getParamVal(PARAMS, props, "spriteType"); emitterObj.texture = EmitterHelper.getParamVal(PARAMS, props, "texture"); emitterObj.color = EmitterHelper.getParamVal(PARAMS, props, "color"); emitterObj.seed = EmitterHelper.getParamVal(PARAMS, props, "seed"); - emitterObj.lifeTime = EmitterHelper.getParamVal(PARAMS, props, "lifeTime"); - emitterObj.lifeTimeRand = EmitterHelper.getParamVal(PARAMS, props, "lifeTimeRand"); + emitterObj.lifeTime = values.lifeTime; + emitterObj.lifeTimeRand = values.lifeTimeRand; emitterObj.speedFactor = EmitterHelper.getParamVal(PARAMS, props, "speedFactor"); emitterObj.warmUpTime = EmitterHelper.getParamVal(PARAMS, props, "warmUpTime"); emitterObj.delay = EmitterHelper.getParamVal(PARAMS, props, "delay"); emitterObj.emitType = EmitterHelper.getParamVal(PARAMS, props, "emitType"); emitterObj.burstCount = EmitterHelper.getParamVal(PARAMS, props, "burstCount"); emitterObj.burstDelay = EmitterHelper.getParamVal(PARAMS, props, "burstDelay"); - emitterObj.burstParticleCount = makeParam(this, "burstParticleCount"); + emitterObj.burstParticleCount = values.burstParticleCount; emitterObj.emitDuration = EmitterHelper.getParamVal(PARAMS, props, "emitDuration"); emitterObj.emitOrientation = EmitterHelper.getParamVal(PARAMS, props, "emitOrientation"); emitterObj.maxCount = EmitterHelper.getParamVal(PARAMS, props, "maxCount"); - emitterObj.emitRate = makeParam(this, "emitRate"); - emitterObj.emitRateMin = makeParam(this, "emitRateMin"); - emitterObj.emitRateMax = makeParam(this, "emitRateMax"); + emitterObj.emitRate = values.emitRate; + emitterObj.emitRateMin = values.emitRateMin; + emitterObj.emitRateMax = values.emitRateMax; emitterObj.emitRateChangeDelay = EmitterHelper.getParamVal(PARAMS, props, "emitRateChangeDelay"); emitterObj.emitShape = EmitterHelper.getParamVal(PARAMS, props, "emitShape"); - emitterObj.emitRadius = makeParam(this, "emitRadius"); - emitterObj.emitAngle1 = makeParam(this, "emitAngle1"); - emitterObj.emitAngle2 = makeParam(this, "emitAngle2"); - emitterObj.emitWidth = makeParam(this, "emitWidth"); - emitterObj.emitHeight = makeParam(this, "emitHeight"); + emitterObj.emitRadius = values.emitRadius; + emitterObj.emitAngle1 = values.emitAngle1; + emitterObj.emitAngle2 = values.emitAngle2; + emitterObj.emitWidth = values.emitWidth; + emitterObj.emitHeight = values.emitHeight; emitterObj.emitSurface = EmitterHelper.getParamVal(PARAMS, props, "emitSurface"); emitterObj.spriteSheet = EmitterHelper.getParamVal(PARAMS, props, "spriteSheet"); emitterObj.frameCount = EmitterHelper.getParamVal(PARAMS, props, "frameCount"); @@ -753,10 +820,10 @@ class Emitter2D extends Object2D { emitterObj.randomColor1 = EmitterHelper.getParamVal(PARAMS, props, "randomColor1"); emitterObj.randomColor2 = EmitterHelper.getParamVal(PARAMS, props, "randomColor2"); emitterObj.randomGradient = EmitterHelper.getParamVal(PARAMS, props, "randomGradient"); - emitterObj.startSpeed = makeParam(this, "instStartSpeed"); - emitterObj.startWorldSpeed = makeParam(this, "instStartWorldSpeed"); + emitterObj.startSpeed = values.startSpeed; + emitterObj.startWorldSpeed = values.startWorldSpeed; - emitterObj.init(randIdx, this); + emitterObj.init(values.builder.slots.length, this); #if editor if(propName == null || ["emitShape", "emitRadius", "emitAngle1", "emitAngle2", "emitWidth", "emitHeight"].indexOf(propName) >= 0) @@ -895,7 +962,7 @@ class Emitter2D extends Object2D { removeParam("emitRateChangeDelay"); } - EmitterHelper.generateEdit(params, instanceParams, props, ctx.properties, onChange, refresh); + EmitterHelper.generateEdit(params, [], props, ctx.properties, onChange, refresh); } override function getHideProps() : hide.prefab.HideProps { From 6ab4543ee97b14c332751576225e5f6c7352aa6b Mon Sep 17 00:00:00 2001 From: trethaller Date: Mon, 12 Oct 2026 16:35:44 +0200 Subject: [PATCH 010/191] emitter-spec: params reference their animation curves by path Co-Authored-By: Claude Opus 5.5 --- emitter-spec.md | 1 + 1 file changed, 1 insertion(+) diff --git a/emitter-spec.md b/emitter-spec.md index 93db92230..9cc744b17 100644 --- a/emitter-spec.md +++ b/emitter-spec.md @@ -48,6 +48,7 @@ The range (`from` / `to`) belongs to the param; a reference shares the shape onl - They set the animated value directly. - They hold plain keys: no range, no random. Constants of the old data are baked into the keys. - Targets: transforms, shader params outside emitters, Emitter params, Spawn `value` / `min` / `max`, Multipliers. +- A param references its animation curves by path, like particle curves; curve names carry no meaning. The upgrade binds each param to its existing curve, under its old name. ## Storage From a223ac4b2c294c622f21a6b8f98bafd785f49861 Mon Sep 17 00:00:00 2001 From: borisrp Date: Mon, 12 Oct 2026 17:00:20 +0200 Subject: [PATCH 011/191] Ensure hide jsdx when running with vscode launch --- .vscode/tasks.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ec779db0d..6e6296cf0 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -15,7 +15,7 @@ { // always builds the Electron editor (hide.js + main.js), whatever the active configuration "type": "hxml", - "file": "hide.hxml", + "file": "hide_jsdx.hxml", "problemMatcher": [ "$haxe-absolute", "$haxe", From f86d48076bd3b27c5f7ebf05e43dcc36fd288351 Mon Sep 17 00:00:00 2001 From: borisrp Date: Mon, 12 Oct 2026 17:00:46 +0200 Subject: [PATCH 012/191] Make GPU emitter working with hide_jsdx --- hrt/prefab/fx/gpuemitter/GPUEmitter.hx | 6 +----- hrt/prefab/fx/gpuemitter/GPUEmitterObject.hx | 4 ---- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/hrt/prefab/fx/gpuemitter/GPUEmitter.hx b/hrt/prefab/fx/gpuemitter/GPUEmitter.hx index e3861ca24..c81c80b2e 100644 --- a/hrt/prefab/fx/gpuemitter/GPUEmitter.hx +++ b/hrt/prefab/fx/gpuemitter/GPUEmitter.hx @@ -155,7 +155,7 @@ class GPUEmitter extends Object3D { if (templates != null) { for (t in templates) { for (e in t.emitters) - e.remove(); + e.remove(); } templates = null; } @@ -165,10 +165,6 @@ class GPUEmitter extends Object3D { } function getTemplates() : Array