├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .prettierrc.json ├── .travis.yml ├── LICENSE ├── README.md ├── example.png ├── package-lock.json ├── package.json ├── serve-benchmark ├── .env ├── .gitignore ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ └── mobileconsole.js ├── src │ ├── App.tsx │ ├── index.css │ ├── index.tsx │ └── react-app-env.d.ts └── tsconfig.json ├── src ├── benchmarks │ └── generate.bench.ts ├── bitonic.ts ├── frag.d.ts ├── index.ts ├── initialize.ts ├── limiter.ts ├── parameters.ts ├── radix.ts ├── shaders │ ├── 00_transform32.frag │ ├── 00_transform64.frag │ ├── 01_sort32.frag │ ├── 01_sort64.frag │ ├── 02_untransform32.frag │ └── 02_untransform64.frag ├── sort.ts ├── tests │ ├── bitonic.test.ts │ ├── index.test.ts │ ├── initialize.test.ts │ └── limiter.test.ts └── uniforms.ts ├── tsconfig.json ├── vscode.code-workspace └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/typescript", ["@babel/env", { "modules": "commonjs" }]], 3 | "plugins": [ 4 | [ 5 | "babel-plugin-static-fs", 6 | { 7 | "target": "browser", 8 | "dynamic": false 9 | } 10 | ], 11 | "add-module-exports", 12 | "@babel/proposal-class-properties", 13 | "@babel/proposal-object-rest-spread" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | end_of_line = lf 3 | 4 | [*.{js,jsx,ts,tsx,html,sass}] 5 | indent_style = tab 6 | indent_size = tab 7 | tab_width = 2 8 | 9 | [*.{glsl,frag,vert,go}] 10 | indent_style = tab 11 | indent_size = tab 12 | tab_width = 4 13 | 14 | [*.{glsl,frag,vert}] 15 | indent_style = space 16 | indent_size = 4 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"], 4 | "plugins": ["@typescript-eslint"], 5 | "parserOptions": { 6 | "ecmaVersion": 11, 7 | "sourceType": "module" 8 | }, 9 | "rules": { 10 | "indent": ["error", 2, { "SwitchCase": 1 }], 11 | "linebreak-style": ["error", "unix"], 12 | "semi": ["error", "always"], 13 | "no-var": 0, 14 | "prefer-const": "off", 15 | "quotes": "off", 16 | "@typescript-eslint/no-explicit-any": 0, 17 | "@typescript-eslint/no-var-requires": 0, 18 | "@typescript-eslint/no-use-before-define": 0, 19 | "@typescript-eslint/explicit-function-return-type": 0, 20 | "@typescript-eslint/camelcase": 0, 21 | "@typescript-eslint/class-name-casing": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Vendored sources 5 | serve-benchmark/public/mobileconsole.js linguist-vendored 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /lib 3 | /coverage 4 | /dist 5 | /bench 6 | /test 7 | serve-benchmark/public/generate.bundle.js 8 | .DS_Store -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "singleQuote": false, 5 | "semi": true, 6 | "useTabs": false 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | os: osx 5 | after_success: cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js --verbose 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alex Demskie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gpu-sort 2 | 3 | ## GPU accelerated asynchronous sorting 4 | 5 | [![Build Status](https://travis-ci.org/demskie/gpu-sort.svg?branch=master)](https://travis-ci.org/demskie/gpu-sort) [![Coverage Status](https://coveralls.io/repos/github/demskie/gpu-sort/badge.svg?branch=master)](https://coveralls.io/github/demskie/gpu-sort?branch=master) 6 | [![Dependency Status](https://david-dm.org/demskie/gpu-sort/status.svg)](https://david-dm.org/demskie/gpu-sort#info=dependencies&view=table) [![npm version](https://badge.fury.io/js/gpu-sort.svg)](https://badge.fury.io/js/gpu-sort) 7 | 8 | ## Why? 9 | 10 | ``` 11 | WebGL seems underutilized and sorting asynchronously is really handy. 12 | ``` 13 | 14 | ## Installation 15 | 16 | ```bash 17 | npm install gpu-sort 18 | ``` 19 | 20 | ## Example 21 | 22 | ```js 23 | import * as gpu from "gpu-sort"; 24 | 25 | let numbers = new Float64Array([5, 4, 3, 2, 1, 0]); 26 | 27 | // sort in place 28 | gpu.sort(numbers); 29 | 30 | // sort in place asynchronously 31 | gpu.sortAsync(numbers).then(() => console.log("sorted!")); 32 | ``` 33 | 34 | ## NodeJS support 35 | 36 | ```js 37 | import * as gpu from "gpu-sort"; 38 | 39 | // For NodeJS an emulation library must be provided as it doesn't support WebGL 40 | gpu.setWebGLContext(require("gl")(1, 1)); 41 | 42 | // sort using webgl emulated context 43 | gpu.sort(foo); 44 | ``` 45 | 46 | ## Benchmarks 47 | 48 | ## https://demskie.github.io/gpu-sort 49 | 50 | 51 | 52 | ## Limitations 53 | 54 | 1. Only TypedArrays are supported. 55 | 2. When sorting 8bit and 16bit numbers GPU acceleration is disabled (as it seems rather pointless). 56 | 3. The maximum number of elements that can be sorted is constrained by the max texture width squared. For example `4096 ^ 2 = 16,777,216 (32bit)` or `4096 ^ 2 / 2 = 8,388,608 (64bit)`. This can be increased in the future by up to 8 times by multiplexing the data across multiple framebuffers. 57 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demskie/gpu-sort/2773ff4a0ab40725fc01d3d088faebd282e250bb/example.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gpu-sort", 3 | "version": "1.0.3", 4 | "description": "GPU accelerated asynchronous sorting", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "tsc --noEmit && jest --runInBand --coverage", 8 | "build": "trash lib && make-dir lib/shaders && cpy src/shaders/** lib/shaders/ && tsc && jest --runInBand --coverage", 9 | "bundle": "webpack --progress --config-name dist", 10 | "bench-browser": "webpack --progress --config-name bench-generate && npm run debug --prefix serve-benchmark", 11 | "gh-pages-deploy": "webpack --progress --config-name bench-generate && npm run build --prefix serve-benchmark && gh-pages -d serve-benchmark/build" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/demskie/gpu-sort.git" 16 | }, 17 | "files": [ 18 | "lib/**/*" 19 | ], 20 | "keywords": [ 21 | "sort", 22 | "bitonic", 23 | "radix", 24 | "gpu", 25 | "gpgpu", 26 | "compute", 27 | "webgl", 28 | "glsl", 29 | "shader", 30 | "async", 31 | "promises" 32 | ], 33 | "author": "Alex Demskie", 34 | "license": "MIT", 35 | "homepage": "https://demskie.github.io/gpu-sort", 36 | "bugs": { 37 | "url": "https://github.com/demskie/gpu-sort/issues" 38 | }, 39 | "typings": "lib/index.d.ts", 40 | "browserslist": [ 41 | "node 6.5" 42 | ], 43 | "jest": { 44 | "roots": [ 45 | "/src" 46 | ], 47 | "transform": { 48 | "^.+\\.(ts|tsx)$": "ts-jest" 49 | }, 50 | "testRegex": [ 51 | ".*.test\\.ts" 52 | ], 53 | "moduleFileExtensions": [ 54 | "ts", 55 | "js", 56 | "json", 57 | "node" 58 | ], 59 | "globals": { 60 | "ts-jest": { 61 | "tsConfig": "tsconfig.json", 62 | "diagnostics": false 63 | } 64 | }, 65 | "testPathIgnorePatterns": [ 66 | "/node_modules/", 67 | "/config/", 68 | "/build/" 69 | ], 70 | "collectCoverage": false, 71 | "collectCoverageFrom": [ 72 | "!**/node_modules/**", 73 | "!**/vendor/**", 74 | "**/*.ts" 75 | ] 76 | }, 77 | "devDependencies": { 78 | "@babel/cli": "^7.8.4", 79 | "@babel/core": "^7.8.4", 80 | "@babel/plugin-proposal-class-properties": "^7.8.3", 81 | "@babel/plugin-proposal-object-rest-spread": "^7.8.3", 82 | "@babel/plugin-transform-runtime": "^7.8.3", 83 | "@babel/preset-env": "^7.8.4", 84 | "@babel/preset-typescript": "^7.8.3", 85 | "@types/jest": "^25.1.3", 86 | "@types/node": "^13.7.4", 87 | "@typescript-eslint/eslint-plugin": "^2.20.0", 88 | "@typescript-eslint/parser": "^2.20.0", 89 | "babel-loader": "^8.0.6", 90 | "babel-plugin-add-module-exports": "^1.0.2", 91 | "babel-plugin-static-fs": "^3.0.0", 92 | "babel-runtime": "^6.26.0", 93 | "coveralls": "^3.0.9", 94 | "cpy-cli": "^3.1.0", 95 | "eslint": "^6.8.0", 96 | "eslint-config-prettier": "^6.10.0", 97 | "eslint-plugin-prettier": "^3.1.2", 98 | "gh-pages": "^2.2.0", 99 | "gl": "^4.4.1", 100 | "jest": "^25.1.0", 101 | "make-dir-cli": "^2.0.0", 102 | "prettier": "^1.19.1", 103 | "trash-cli": "^3.0.0", 104 | "ts-jest": "^25.2.1", 105 | "ts-node": "^8.6.2", 106 | "typescript": "^3.8.2", 107 | "webpack": "^4.41.6", 108 | "webpack-cli": "^3.3.11" 109 | }, 110 | "dependencies": { 111 | "gpu-compute": "^1.0.1" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /serve-benchmark/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | -------------------------------------------------------------------------------- /serve-benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /serve-benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serve-benchmark", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/jest": "24.0.18", 7 | "@types/node": "12.7.4", 8 | "@types/react": "16.9.2", 9 | "@types/react-dom": "16.9.0", 10 | "@types/stats.js": "^0.17.0", 11 | "@types/ua-parser-js": "^0.7.33", 12 | "react": "^16.9.0", 13 | "react-dom": "^16.9.0", 14 | "react-loading": "^2.0.3", 15 | "react-scripts": "^3.3.1", 16 | "serve": "^11.1.0", 17 | "stats.js": "^0.17.0", 18 | "typescript": "3.6.2", 19 | "ua-parser-js": "^0.7.21" 20 | }, 21 | "scripts": { 22 | "debug": "react-scripts start", 23 | "build": "react-scripts build", 24 | "bench": "react-scripts build && npx serve -s build" 25 | }, 26 | "eslintConfig": { 27 | "extends": "react-app" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /serve-benchmark/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demskie/gpu-sort/2773ff4a0ab40725fc01d3d088faebd282e250bb/serve-benchmark/public/favicon.ico -------------------------------------------------------------------------------- /serve-benchmark/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 26 | gpu-sort 27 | 28 | 29 | 30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /serve-benchmark/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [], 5 | "start_url": ".", 6 | "display": "standalone", 7 | "theme_color": "#000000", 8 | "background_color": "#ffffff" 9 | } 10 | -------------------------------------------------------------------------------- /serve-benchmark/public/mobileconsole.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * hnl.mobileConsole - javascript mobile console - v1.3.5 - 19/4/2018 3 | * Adds html console to webpage. Especially useful for debugging JS on mobile devices. 4 | * Supports 'log', 'trace', 'info', 'warn', 'error', 'group', 'groupEnd', 'table', 'assert', 'clear' 5 | * Inspired by code by jakub fiala (https://gist.github.com/jakubfiala/8fe3461ab6508f46003d) 6 | * Licensed under the MIT license 7 | * 8 | * Original author: @hnldesign 9 | * Further changes, comments: @hnldesign 10 | * Copyright (c) 2014-2016 HN Leussink 11 | * Dual licensed under the MIT and GPL licenses. 12 | * 13 | * Info: http://www.hnldesign.nl/work/code/javascript-mobile-console/ 14 | * Demo: http://code.hnldesign.nl/demo/hnl.MobileConsole.html 15 | */ 16 | 17 | //Polyfills 18 | 19 | //Date.now polyfill 20 | if (!Date.now) { 21 | Date.now = function now() { 22 | return new Date().getTime(); 23 | }; 24 | } 25 | //Array.isArray polyfill 26 | if (typeof Array.isArray === "undefined") { 27 | Array.isArray = function(obj) { 28 | return Object.prototype.toString.call(obj) === "[object Array]"; 29 | }; 30 | } 31 | //Array.filter polyfill 32 | if (!Array.prototype.filter) { 33 | Array.prototype.filter = function(fun /*, thisArg*/) { 34 | if (this === void 0 || this === null) { 35 | throw new TypeError(); 36 | } 37 | var t = Object(this); 38 | var len = t.length >>> 0; 39 | if (typeof fun !== "function") { 40 | throw new TypeError(); 41 | } 42 | var res = []; 43 | var thisArg = arguments.length >= 2 ? arguments[1] : void 0; 44 | for (var i = 0; i < len; i++) { 45 | if (i in t) { 46 | var val = t[i]; 47 | if (fun.call(thisArg, val, i, t)) { 48 | res.push(val); 49 | } 50 | } 51 | } 52 | 53 | return res; 54 | }; 55 | } 56 | //Function.bind polyfill 57 | if (!Function.prototype.bind) { 58 | Function.prototype.bind = function(oThis) { 59 | if (typeof this !== "function") { 60 | // closest thing possible to the ECMAScript 5 61 | // internal IsCallable function 62 | throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); 63 | } 64 | var aArgs = Array.prototype.slice.call(arguments, 1), 65 | fToBind = this, 66 | fNOP = function() {}, 67 | fBound = function() { 68 | return fToBind.apply(this instanceof fNOP ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments))); 69 | }; 70 | if (this.prototype) { 71 | // Function.prototype doesn't have a prototype property 72 | fNOP.prototype = this.prototype; 73 | } 74 | fBound.prototype = new fNOP(); 75 | return fBound; 76 | }; 77 | } 78 | //Array.prototype.indexOf polyfill 79 | // Production steps of ECMA-262, Edition 5, 15.4.4.14 80 | // Referentie: http://es5.github.io/#x15.4.4.14 81 | if (!Array.prototype.indexOf) { 82 | Array.prototype.indexOf = function(searchElement, fromIndex) { 83 | var k; 84 | if (this == null) { 85 | throw new TypeError('"this" is null or not defined'); 86 | } 87 | var o = Object(this); 88 | var len = o.length >>> 0; 89 | if (len === 0) { 90 | return -1; 91 | } 92 | var n = +fromIndex || 0; 93 | if (Math.abs(n) === Infinity) { 94 | n = 0; 95 | } 96 | if (n >= len) { 97 | return -1; 98 | } 99 | k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); 100 | while (k < len) { 101 | if (k in o && o[k] === searchElement) { 102 | return k; 103 | } 104 | k++; 105 | } 106 | return -1; 107 | }; 108 | } 109 | //String.prototype.trim polyfill 110 | if (!String.prototype.trim) { 111 | String.prototype.trim = function() { 112 | return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ""); 113 | }; 114 | } 115 | //Array.prototype.map polyfill 116 | // Production steps of ECMA-262, Edition 5, 15.4.4.19 117 | // Reference: http://es5.github.io/#x15.4.4.19 118 | if (!Array.prototype.map) { 119 | Array.prototype.map = function(callback /*, thisArg*/) { 120 | var T, A, k; 121 | if (this == null) { 122 | throw new TypeError("this is null or not defined"); 123 | } 124 | var O = Object(this); 125 | var len = O.length >>> 0; 126 | if (typeof callback !== "function") { 127 | throw new TypeError(callback + " is not a function"); 128 | } 129 | if (arguments.length > 1) { 130 | T = arguments[1]; 131 | } 132 | A = new Array(len); 133 | k = 0; 134 | while (k < len) { 135 | var kValue, mappedValue; 136 | if (k in O) { 137 | kValue = O[k]; 138 | mappedValue = callback.call(T, kValue, k, O); 139 | A[k] = mappedValue; 140 | } 141 | k++; 142 | } 143 | return A; 144 | }; 145 | } 146 | 147 | // DocReady - Fires supplied function when document is ready 148 | if (typeof "docReady" !== "function") { 149 | (function(funcName, baseObj) { 150 | // The public function name defaults to window.docReady 151 | // but you can pass in your own object and own function name and those will be used 152 | // if you want to put them in a different namespace 153 | funcName = funcName || "docReady"; 154 | baseObj = baseObj || window; 155 | var i, 156 | len, 157 | readyList = [], 158 | readyFired = false, 159 | readyEventHandlersInstalled = false; 160 | 161 | // call this when the document is ready 162 | // this function protects itself against being called more than once 163 | function ready() { 164 | if (!readyFired) { 165 | // this must be set to true before we start calling callbacks 166 | readyFired = true; 167 | for (i = 0, len = readyList.length; i < len; i = i + 1) { 168 | // if a callback here happens to add new ready handlers, 169 | // the docReady() function will see that it already fired 170 | // and will schedule the callback to run right after 171 | // this event loop finishes so all handlers will still execute 172 | // in order and no new ones will be added to the readyList 173 | // while we are processing the list 174 | readyList[i].fn.call(window, readyList[i].ctx); 175 | } 176 | // allow any closures held by these functions to free 177 | readyList = []; 178 | } 179 | } 180 | 181 | function readyStateChange() { 182 | if (document.readyState === "complete") { 183 | ready(); 184 | } 185 | } 186 | 187 | // This is the one public interface 188 | // docReady(fn, context); 189 | // the context argument is optional - if present, it will be passed 190 | // as an argument to the callback 191 | baseObj[funcName] = function(callback, context) { 192 | // if ready has already fired, then just schedule the callback 193 | // to fire asynchronously, but right away 194 | if (readyFired) { 195 | setTimeout(function() { 196 | callback(context); 197 | }, 1); 198 | return; 199 | } 200 | // add the function and context to the list 201 | readyList.push({ fn: callback, ctx: context }); 202 | // if document already ready to go, schedule the ready function to run 203 | if (document.readyState === "complete") { 204 | setTimeout(ready, 1); 205 | } else if (!readyEventHandlersInstalled) { 206 | // otherwise if we don't have event handlers installed, install them 207 | if (document.addEventListener) { 208 | // first choice is DOMContentLoaded event 209 | document.addEventListener("DOMContentLoaded", ready, false); 210 | // backup is window load event 211 | window.addEventListener("load", ready, false); 212 | } else { 213 | // must be IE 214 | document.attachEvent("onreadystatechange", readyStateChange); 215 | window.attachEvent("onload", ready); 216 | } 217 | readyEventHandlersInstalled = true; 218 | } 219 | }; 220 | })("docReady", window); 221 | } 222 | 223 | //define console variable 224 | var console = window.console; 225 | 226 | var mobileConsole = (function() { 227 | "use strict"; 228 | 229 | //options and other variable containers 230 | var options = { 231 | overrideAutorun: false, 232 | version: "1.3.5", 233 | baseClass: "mobileConsole_", 234 | animParams: "all 200ms ease", 235 | browserinfo: { 236 | isMobile: (function(a) { 237 | return ( 238 | /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( 239 | a 240 | ) || 241 | /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( 242 | a.substr(0, 4) 243 | ) 244 | ); 245 | })(navigator.userAgent || navigator.vendor || window.opera), 246 | browserChrome: /chrome/.test(navigator.userAgent.toLowerCase()), 247 | ffox: /firefox/.test(navigator.userAgent.toLowerCase()) && !/chrome/.test(navigator.userAgent.toLowerCase()), 248 | safari: /safari/.test(navigator.userAgent.toLowerCase()) && !/chrome/.test(navigator.userAgent.toLowerCase()), 249 | trident: /trident/.test(navigator.userAgent.toLowerCase()), 250 | evtLstn: typeof window.addEventListener === "function" 251 | }, 252 | methods: [ 253 | "log", 254 | "trace", 255 | "info", 256 | "warn", 257 | "error", 258 | "group", 259 | "groupCollapsed", 260 | "groupEnd", 261 | "table", 262 | "assert", 263 | "time", 264 | "timeEnd", 265 | "clear" 266 | ], 267 | hideButtons: ["group", "groupCollapsed", "groupEnd", "table", "assert", "time", "timeEnd"], 268 | ratio: 0.4, 269 | paddingLeft: 0, 270 | groupDepth: 0 271 | }, 272 | messages = { 273 | clear: "Console was cleared", 274 | empty: "(Empty string)" 275 | }, 276 | status = { 277 | initialized: false, 278 | acActive: false, 279 | acHovered: false, 280 | acInput: "", 281 | timers: {} 282 | }, 283 | history = { 284 | output: { 285 | prevMsg: "", 286 | prevMethod: "", 287 | counter: 0 288 | }, 289 | input: { 290 | commands: window.sessionStorage 291 | ? sessionStorage.getItem("mobileConsoleCommandHistory") 292 | ? JSON.parse(sessionStorage.getItem("mobileConsoleCommandHistory")) 293 | : [] 294 | : [], 295 | commandIdx: window.sessionStorage 296 | ? sessionStorage.getItem("mobileConsoleCommandHistory") 297 | ? JSON.parse(sessionStorage.getItem("mobileConsoleCommandHistory")).length 298 | : 0 299 | : 0, 300 | acIdx: 0, 301 | acHovered: false 302 | } 303 | }, 304 | //'backup' original console for reference & internal debugging 305 | missingMethod = function() { 306 | return true; 307 | }, //method is not supported on this device's original console, return dummy 308 | originalConsole = { 309 | log: console && typeof console.log === "function" ? console.log.bind(console) : missingMethod, 310 | info: console && typeof console.info === "function" ? console.info.bind(console) : missingMethod, 311 | dir: console && typeof console.dir === "function" ? console.dir.bind(console) : missingMethod, 312 | group: console && typeof console.group === "function" ? console.group.bind(console) : missingMethod, 313 | groupEnd: console && typeof console.groupEnd === "function" ? console.groupEnd.bind(console) : missingMethod, 314 | warn: console && typeof console.warn === "function" ? console.warn.bind(console) : missingMethod, 315 | error: console && typeof console.error === "function" ? console.error.bind(console) : missingMethod, 316 | trace: console && typeof console.trace === "function" ? console.trace.bind(console) : missingMethod, 317 | clear: console && typeof console.clear === "function" ? console.clear.bind(console) : missingMethod 318 | }, 319 | // reference variables 320 | mobileConsole, 321 | consoleElement, 322 | commandLine; 323 | 324 | //helpers for all sub functions 325 | function setCSS(el, css) { 326 | var i; 327 | for (i in css) { 328 | if (css.hasOwnProperty(i)) { 329 | el.style[i] = css[i]; 330 | } 331 | } 332 | return el; 333 | } 334 | function htmlToString(html) { 335 | var string; 336 | try { 337 | string = String(html); 338 | } catch (e) { 339 | string = JSON.stringify(html); 340 | } //this should be done differently, but works for now 341 | return string 342 | .replace(/&/g, "&") 343 | .replace(//g, ">") 345 | .replace(/"/g, """) 346 | .replace(/ /g, "\u00a0") 347 | .replace(/(?:\r\n|\r|\n)/g, "
") 348 | .trim(); 349 | } 350 | function createElem(type, className, css) { 351 | if (!type || typeof setCSS !== "function") { 352 | return; 353 | } 354 | var element = setCSS(document.createElement(type), css); 355 | if (className) { 356 | element.className = options.baseClass + className; 357 | } 358 | return setCSS(element, css); 359 | } 360 | function storeCommand(command) { 361 | if (history) { 362 | history.input.commands.push(encodeURI(command.trim())); 363 | history.input.commandIdx = history.input.commands.length; 364 | if (window.sessionStorage) { 365 | sessionStorage.setItem("mobileConsoleCommandHistory", JSON.stringify(history.input.commands)); 366 | } 367 | } 368 | } 369 | function valBetween(val, min, max) { 370 | return Math.min(max, Math.max(min, val)); 371 | } 372 | function getMaxHeight() { 373 | return valBetween( 374 | Math.floor((window.innerHeight || document.documentElement.clientHeight) * options.ratio), 375 | 55, 376 | 300 377 | ); 378 | } 379 | function getClass(item) { 380 | var returnVal = ""; 381 | if (item && item.constructor) { 382 | returnVal = item.constructor.name; 383 | } else { 384 | returnVal = Object.prototype.toString.call(item); 385 | } 386 | return String(returnVal); 387 | } 388 | 389 | // elements 390 | var elements = { 391 | lines: [], 392 | acItems: [], 393 | base: createElem("div", "base", { 394 | boxSizing: "border-box", 395 | position: "fixed", 396 | resize: "none", 397 | fontSize: "12px", 398 | lineHeight: "14px", 399 | bottom: 0, 400 | top: "auto", 401 | right: 0, 402 | width: "100%", 403 | zIndex: 10000, 404 | padding: 0, 405 | paddingBottom: options.browserinfo.isMobile ? "35px" : "25px", 406 | margin: 0, 407 | border: "0 none", 408 | borderTop: "1px solid #808080", 409 | backgroundColor: "#ffffff" 410 | }), 411 | topbar: createElem("div", "topbar", { 412 | boxSizing: "border-box", 413 | position: "absolute", 414 | height: "28px", 415 | left: 0, 416 | right: 0, 417 | display: "block", 418 | padding: "0 2px", 419 | overflow: "hidden", 420 | webkitOverflowScrolling: "touch", 421 | color: "#444444", 422 | backgroundColor: "#f3f3f3", 423 | border: "0 none", 424 | borderTop: "1px solid #a3a3a3", 425 | borderBottom: "1px solid #a3a3a3", 426 | whiteSpace: "nowrap", 427 | overflowX: "auto" 428 | }), 429 | scrollcontainer: createElem("div", "scroller", { 430 | boxSizing: "border-box", 431 | border: "0 none", 432 | fontFamily: "Consolas, monaco, monospace", 433 | position: "relative", 434 | display: "block", 435 | height: getMaxHeight() + "px", 436 | overflow: "auto", 437 | webkitOverflowScrolling: "touch", 438 | "-webkit-transition": options.animParams, 439 | "-moz-transition": options.animParams, 440 | "-o-transition": options.animParams, 441 | transition: options.animParams 442 | }), 443 | table: createElem("table", "table", { 444 | border: "0 none", 445 | margin: 0, 446 | position: "relative", 447 | tableLayout: "auto", 448 | width: "100%", 449 | borderCollapse: "collapse" 450 | }), 451 | stackTraceTable: createElem("table", "stackTraceTable", { 452 | border: "0 none", 453 | margin: 0, 454 | display: "none", 455 | marginLeft: "10px", 456 | marginTop: options.browserinfo.isMobile ? "8px" : "4px", 457 | tableLayout: "auto", 458 | maxWidth: "100%", 459 | color: "#333333" 460 | }), 461 | tr: createElem("tr", "table_row", { 462 | verticalAlign: "top" 463 | }), 464 | td: createElem("td", "table_row", { 465 | border: "0 none", 466 | padding: "2px 4px", 467 | verticalAlign: "top" 468 | }), 469 | msgContainer: createElem("span", "msgContainer", { 470 | border: "0 none", 471 | margin: 0, 472 | display: "inline", 473 | overflow: "hidden" 474 | }), 475 | tdLeft: createElem("td", "table_row_data", { 476 | border: "0 none", 477 | textAlign: "left", 478 | padding: options.browserinfo.isMobile ? "8px 12px" : "4px 8px" 479 | }), 480 | tdRight: createElem("td", "table_row_data", { 481 | border: "0 none", 482 | textAlign: "left", 483 | padding: options.browserinfo.isMobile ? "8px 12px" : "4px 8px", 484 | whiteSpace: "nowrap", 485 | overflow: "hidden" 486 | }), 487 | link: createElem("a", "link", { 488 | color: "#1155cc", 489 | textDecoration: "underline" 490 | }), 491 | dot: createElem("div", "table_row_data_dot", { 492 | display: "inline", 493 | borderRadius: "50%", 494 | fontSize: "80%", 495 | fontWeight: "bold", 496 | padding: "2px 5px", 497 | textAlign: "center", 498 | marginRight: "5px", 499 | backgroundColor: "#333333", 500 | color: "#ffffff" 501 | }), 502 | button: createElem("button", "button", { 503 | display: "inline-block", 504 | fontFamily: '"Helvetica Neue",Helvetica,Arial,sans-serif', 505 | fontWeight: "normal", 506 | textTransform: "capitalize", 507 | fontSize: "12px", 508 | lineHeight: "26px", 509 | height: "26px", 510 | padding: "0 8px", 511 | margin: 0, 512 | textAlign: "center", 513 | marginRight: "5px", 514 | border: "0 none", 515 | backgroundColor: "transparent", 516 | color: "inherit", 517 | cursor: "pointer" 518 | }), 519 | buttons: {}, 520 | input: createElem("div", "input", { 521 | boxSizing: "border-box", 522 | height: options.browserinfo.isMobile ? "35px" : "29px", 523 | fontFamily: "Consolas, monaco, monospace", 524 | position: "absolute", 525 | bottom: 0, 526 | left: 0, 527 | right: 0, 528 | margin: 0, 529 | border: "0 none", 530 | borderTop: "1px solid #EEEEEE" 531 | }), 532 | gt: createElem("DIV", "gt", { 533 | position: "absolute", 534 | bottom: 0, 535 | width: "25px", 536 | lineHeight: options.browserinfo.isMobile ? "34px" : "28px", 537 | height: options.browserinfo.isMobile ? "34px" : "28px", 538 | textAlign: "center", 539 | fontSize: "16px", 540 | fontFamily: "Consolas, monaco, monospace", 541 | fontWeight: "bold", 542 | color: "#3577B1", 543 | zIndex: 2 544 | }), 545 | consoleinput: createElem("input", "consoleinput", { 546 | boxSizing: "border-box", 547 | position: "absolute", 548 | bottom: 0, 549 | width: "100%", 550 | fontSize: options.browserinfo.isMobile ? "16px" : "inherit", //prevents ios safari's zoom on focus 551 | fontFamily: "Consolas, monaco, monospace", 552 | paddingLeft: "25px", 553 | margin: 0, 554 | height: options.browserinfo.isMobile ? "35px" : "25px", 555 | border: "0 none", 556 | outline: "none", 557 | outlineWidth: 0, 558 | boxShadow: "none", 559 | "-moz-appearance": "none", 560 | "-webkit-appearance": "none", 561 | backgroundColor: "transparent", 562 | color: "#000000", 563 | zIndex: 1 564 | }), 565 | autocomplete: createElem("div", "autocomplete", { 566 | display: "none", 567 | position: "absolute", 568 | bottom: options.browserinfo.isMobile ? "35px" : "28px", 569 | left: 0, 570 | boxShadow: "1px 2px 5px rgba(0,0,0,0.1)", 571 | color: "#000000", 572 | backgroundColor: "#FFFFFF", 573 | border: "1px solid #b5b5b5" 574 | }), 575 | autocompleteItem: createElem("a", "autocompleteitem", { 576 | display: "block", 577 | textDecoration: "none", 578 | fontSize: options.browserinfo.isMobile ? "16px" : "inherit", 579 | padding: "5px 8px", 580 | wordWrap: "break-word", 581 | whiteSpace: "nowrap" 582 | }), 583 | arrowUp: 584 | '', 585 | arrowDown: 586 | '', 587 | arrowRight: 588 | '' 589 | }; 590 | 591 | //shared functions 592 | 593 | var setLineStyle = (function() { 594 | var lineStyles = function(style) { 595 | switch (style) { 596 | case "log": 597 | return { 598 | text: { 599 | borderBottom: "1px solid #DDDDDD", 600 | color: "#000000" 601 | }, 602 | dot: { 603 | color: "#FFFFFF", 604 | backgroundColor: "#8097bd" 605 | } 606 | }; 607 | case "info": 608 | return { 609 | text: { 610 | borderBottom: "1px solid #DDDDDD", 611 | color: "#1f3dc4" 612 | }, 613 | dot: { 614 | color: "#FFFFFF", 615 | backgroundColor: "#367AB4" 616 | } 617 | }; 618 | case "warn": 619 | return { 620 | text: { 621 | borderBottom: "1px solid #DDDDDD", 622 | color: "#CE8724", 623 | backgroundColor: "#fff6e0" 624 | }, 625 | dot: { 626 | color: "#FFFFFF", 627 | backgroundColor: "#e8a400" 628 | } 629 | }; 630 | case "error": 631 | case "table": 632 | return { 633 | text: { 634 | borderBottom: "1px solid #DDDDDD", 635 | color: "#FF0000", 636 | backgroundColor: "#ffe5e5" 637 | }, 638 | dot: { 639 | color: "#FFFFFF", 640 | backgroundColor: "#FF0000" 641 | } 642 | }; 643 | case "assert": 644 | return { 645 | text: { 646 | borderBottom: "1px solid #DDDDDD", 647 | color: "#FF0000", 648 | backgroundColor: "#ffe5e5" 649 | }, 650 | dot: { 651 | color: "#FFFFFF", 652 | backgroundColor: "#FF0000" 653 | } 654 | }; 655 | case "trace": 656 | return { 657 | text: { 658 | borderBottom: "1px solid #DDDDDD", 659 | color: "#000000" 660 | }, 661 | dot: { 662 | //will not happen 663 | } 664 | }; 665 | case "time": 666 | case "timeEnd": 667 | return { 668 | text: { 669 | borderBottom: "1px solid #DDDDDD", 670 | color: "#0000ff" 671 | }, 672 | dot: { 673 | color: "#FFFFFF", 674 | backgroundColor: "#0000ff" 675 | } 676 | }; 677 | default: 678 | return { 679 | text: { 680 | borderBottom: "1px solid #DDDDDD", 681 | color: "#000000" 682 | }, 683 | dot: { 684 | color: "#FFFFFF", 685 | backgroundColor: "#8097bd" 686 | } 687 | }; 688 | } 689 | }; 690 | var color, dot; 691 | 692 | return function(element, type, msg) { 693 | if (status.initialized) { 694 | color = 695 | typeof msg === "undefined" || msg === htmlToString(messages.empty) 696 | ? { color: "#808080" } 697 | : msg === htmlToString(messages.clear) 698 | ? { color: "#808080", fontStyle: "italic" } 699 | : lineStyles(type) !== undefined 700 | ? lineStyles(type).text 701 | : lineStyles.log.text; 702 | dot = typeof lineStyles(type) !== "undefined" ? lineStyles(type).dot : lineStyles.log.dot; 703 | setCSS(element, color); 704 | //has dot? 705 | if (element.childNodes[0].childNodes[0].className.indexOf("dot") !== -1) { 706 | setCSS(element.childNodes[0].childNodes[0], lineStyles(type).dot); 707 | } 708 | } 709 | }; 710 | })(), 711 | getLink = function(href, textString) { 712 | var HTMLurl = elements.link.cloneNode(false); 713 | if (href) { 714 | HTMLurl.setAttribute("href", href); 715 | HTMLurl.setAttribute("target", "_blank"); 716 | } 717 | HTMLurl.innerHTML = 718 | textString || 719 | href 720 | .split("\\") 721 | .pop() 722 | .split("/") 723 | .filter(Boolean) 724 | .pop(); 725 | return HTMLurl; 726 | }, 727 | toggleHeight = function() { 728 | if (status.initialized) { 729 | var existingPadding = 730 | parseInt(document.body.style.paddingBottom, 10) - 731 | Math.abs(elements.base.offsetHeight + elements.topbar.offsetHeight); 732 | var newHeight = elements.base.minimized ? getMaxHeight() + "px" : "0px"; 733 | setCSS(elements.scrollcontainer, { 734 | height: newHeight 735 | }); 736 | setCSS(document.body, { 737 | paddingBottom: existingPadding + Math.abs(parseInt(newHeight, 10) + elements.topbar.offsetHeight) + "px" 738 | }); 739 | elements.buttons.toggler.innerHTML = elements.base.minimized ? elements.arrowDown : elements.arrowUp; 740 | elements.buttons.toggler.setAttribute( 741 | "title", 742 | elements.base.minimized ? "Minimize console" : "Maximize console" 743 | ); 744 | elements.base.minimized = !elements.base.minimized; 745 | return elements.base.minimized; 746 | } 747 | return "Not built!"; 748 | }, 749 | about = (function() { 750 | return function() { 751 | console.info( 752 | "--==## Mobile Console " + 753 | (status.initialized ? "active" : "inactive") + 754 | " ##==--" + 755 | "\n" + 756 | "--===============================--" + 757 | "\n" + 758 | "MobileConsole v" + 759 | options.version + 760 | ", running on " + 761 | navigator.userAgent.toLowerCase() 762 | ); 763 | }; 764 | })(); 765 | 766 | // --==** sub functions start here **==-- 767 | 768 | //initializes the console HTML element 769 | function initConsoleElement() { 770 | //reference 771 | var ref; 772 | //core 773 | function toggleScroll() { 774 | elements.scrollcontainer.scrollTop = elements.scrollcontainer.scrollHeight; 775 | elements.scrollcontainer.scrollLeft = 0; 776 | } 777 | function destroyConsole() { 778 | //conan the destroyer. Very basic; just removes the console element. mobileConsole will still 'pipe' console logging 779 | //don't see any reason for now to reverse that. 780 | elements.base.parentNode.removeChild(elements.base); 781 | status.initialized = false; 782 | console.warn( 783 | "--==## Mobile Console DESTROYED ##==--" + 784 | "\n" + 785 | "To enable again: reload the page. Tip: use the minimize button instead of closing." 786 | ); 787 | } 788 | function assemble() { 789 | var i = options.methods.length, 790 | key; 791 | 792 | //add buttons 793 | while (i--) { 794 | elements.buttons[options.methods[i]] = elements.button.cloneNode(false); 795 | elements.buttons[options.methods[i]].innerHTML = 796 | options.methods[i].charAt(0).toUpperCase() + options.methods[i].slice(1); 797 | elements.buttons[options.methods[i]].setAttribute( 798 | "title", 799 | options.methods[i] !== "clear" 800 | ? "Toggle the display of " + options.methods[i] + " messages" 801 | : "Clear the console" 802 | ); 803 | } 804 | //add min/maximize button 805 | elements.buttons.toggler = elements.button.cloneNode(false); 806 | elements.buttons.toggler.innerHTML = elements.arrowDown; 807 | elements.buttons.toggler.setAttribute("title", "Minimize console"); 808 | //add close button 809 | elements.buttons.closer = elements.button.cloneNode(false); 810 | elements.buttons.closer.innerHTML = "✕"; 811 | elements.buttons.closer.setAttribute("title", "Close (destroy) console"); 812 | setCSS(elements.buttons.closer, { float: "right", margin: "0" }); 813 | 814 | //assemble everything 815 | for (key in elements.buttons) { 816 | if (elements.buttons.hasOwnProperty(key)) { 817 | elements.topbar.insertBefore(elements.buttons[key], elements.topbar.firstChild); 818 | } 819 | } 820 | elements.scrollcontainer.appendChild(elements.table); 821 | 822 | elements.base.appendChild(elements.topbar); 823 | elements.base.appendChild(elements.scrollcontainer); 824 | 825 | status.initialized = true; 826 | return elements.base; 827 | } 828 | function attach(console) { 829 | document.body.appendChild(console); 830 | setCSS(elements.topbar, { 831 | top: -Math.abs(elements.topbar.offsetHeight) + "px" 832 | }); 833 | var existingPadding = isNaN(parseInt(document.body.style.paddingBottom, 10)) 834 | ? 0 835 | : parseInt(document.body.style.paddingBottom, 10); 836 | setCSS(document.body, { 837 | paddingBottom: existingPadding + Math.abs(console.offsetHeight + elements.topbar.offsetHeight) + "px" 838 | }); 839 | elements.scrollcontainer.scrollTop = elements.scrollcontainer.scrollHeight; 840 | 841 | return elements.base; 842 | } 843 | function toggleLogType(e) { 844 | var button = e.currentTarget || e.srcElement; 845 | var logType = button.innerHTML.toLowerCase(); 846 | var elems = elements.lines[logType], 847 | i = elems.length; 848 | button.toggled = typeof button.toggled === "undefined" ? true : !button.toggled; 849 | setCSS(button, { opacity: button.toggled ? "0.5" : "" }); 850 | while (i--) { 851 | setCSS(elems[i], { display: button.toggled ? "none" : "" }); 852 | } 853 | toggleScroll(); 854 | button.blur(); 855 | return button; 856 | } 857 | function setBinds() { 858 | var methods = options.methods, 859 | i = methods.length; 860 | while (i--) { 861 | if (methods[i] !== "clear") { 862 | if (options.browserinfo.evtLstn) { 863 | elements.buttons[methods[i]].addEventListener("click", toggleLogType, false); 864 | } else { 865 | elements.buttons[methods[i]].attachEvent("onclick", toggleLogType); 866 | } 867 | } 868 | if (options.hideButtons.indexOf(methods[i]) !== -1) { 869 | //hide buttons that we don't want 870 | setCSS(elements.buttons[methods[i]], { display: "none" }); 871 | } 872 | } 873 | if (options.browserinfo.evtLstn) { 874 | elements.buttons.toggler.addEventListener("click", toggleHeight, false); 875 | elements.buttons.closer.addEventListener("click", destroyConsole, false); 876 | elements.buttons.clear.addEventListener("click", console.clear, false); 877 | } else { 878 | elements.buttons.toggler.attachEvent("onclick", toggleHeight); 879 | elements.buttons.closer.attachEvent("onclick", destroyConsole); 880 | elements.buttons.clear.attachEvent("onclick", console.clear); 881 | } 882 | } 883 | //init 884 | function init() { 885 | var element = assemble(); 886 | docReady(function() { 887 | setBinds(); 888 | attach(element); 889 | }); 890 | //expose Public methods and variables 891 | return { 892 | toggleHeight: toggleHeight, 893 | toggleScroll: toggleScroll, 894 | destroy: destroyConsole 895 | }; 896 | } 897 | if (!ref) { 898 | ref = init(); 899 | } 900 | return ref; 901 | } 902 | 903 | //initializes the new console logger 904 | function initConsole() { 905 | //reference 906 | var ref; 907 | //sub helpers 908 | function isEmpty(obj) { 909 | for (var prop in obj) { 910 | if (obj.hasOwnProperty(prop)) return false; 911 | } 912 | return JSON.stringify(obj) === JSON.stringify({}); 913 | } 914 | function isElement(o) { 915 | return typeof HTMLElement === "object" 916 | ? o instanceof HTMLElement //DOM2 917 | : o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName === "string"; 918 | } 919 | function objectToString(object) { 920 | var prop, 921 | output = ""; 922 | if (!isElement(object)) { 923 | for (prop in object) { 924 | if (!object.hasOwnProperty(prop)) { 925 | continue; 926 | } else if (typeof object[prop] === "object") { 927 | output += prop + (Array.isArray(object[prop]) ? ": Array(" + object[prop].length + ")" : ": {…}"); 928 | } else if (typeof object[prop] === "function") { 929 | output += "func: f"; 930 | } else { 931 | output += prop + ': "' + object[prop] + '"'; 932 | } 933 | output += ", "; 934 | } 935 | return "{" + output.slice(0, -2) + "}"; // returns cleaned up JSON 936 | } 937 | return htmlToString(object.outerHTML); 938 | } 939 | function urlFromString(string) { 940 | string = String(string); 941 | //searches for url in string, returns url as string 942 | var match, 943 | uriPattern = /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/gi; 944 | try { 945 | match = string.match(uriPattern)[0]; 946 | return match; 947 | } catch (e) { 948 | return ""; 949 | } 950 | } 951 | function filterOut(array, match) { 952 | return array.filter(function(item) { 953 | return typeof item === "string" && item.indexOf(match) === -1; 954 | }); 955 | } 956 | function preFilterTrace(array) { 957 | var newArray = array.split("\n").filter(Boolean), //filter cleans out empty values 958 | isCommandLine = false, 959 | stealthThese, 960 | i; 961 | if (newArray[0].indexOf("http") === -1) { 962 | newArray.shift(); 963 | } //remove first line if contains no 'http' (Chrome starts with 'Error', Firefox doesn't..) 964 | if (newArray[0].indexOf("console.") !== -1 || newArray[0].indexOf("console[method]") !== -1) { 965 | newArray.shift(); 966 | } 967 | if (newArray.length > 0) { 968 | isCommandLine = newArray[newArray.length - 1].indexOf("keydown") !== -1; 969 | newArray = newArray.filter(function(item) { 970 | return item !== ""; 971 | }); 972 | 973 | if (isCommandLine) { 974 | stealthThese = ["submitCommand", "eval", "setBinds", "interceptConsole", "newConsole"]; 975 | newArray.pop(); //remove last index, as it is the keydown event. 976 | i = stealthThese.length; 977 | while (i--) { 978 | newArray = filterOut(newArray, stealthThese[i]); 979 | } 980 | } 981 | } 982 | if (isCommandLine || newArray.length === 0) { 983 | newArray.push("(anonymous function) console:1:1"); 984 | } 985 | return newArray; 986 | } 987 | //core 988 | function formatStackTrace(trace, origtrace) { 989 | var callStack = []; 990 | //original stack is hidden inside trace object, if specified 991 | var stackTraceOrig = typeof trace !== "undefined" && typeof trace[4] !== "undefined" ? trace[4].stack : undefined; 992 | //if the first line contains this, skip it. Meant for browsers that begin the stack with the error message itself (already captured before formatStackTrace) 993 | var traceToProcess = origtrace && origtrace !== "" ? origtrace : stackTraceOrig, 994 | i, 995 | lines, 996 | url, 997 | txt, 998 | thisLine, 999 | lineAndColumn, 1000 | caller, 1001 | separator = options.browserinfo.ffox ? "@" : "()"; 1002 | 1003 | //stop if no source trace can be determined 1004 | if (!traceToProcess) { 1005 | return; 1006 | } 1007 | 1008 | lines = preFilterTrace(traceToProcess); //pre filters all lines by filtering out all mobileConsole's own methods so mobileConsole runs Stealth and unobtrusive 1009 | i = lines.length; 1010 | while (i--) { 1011 | thisLine = lines[i].trim(); 1012 | lineAndColumn = thisLine.match(/(?::)(\d+)(?::)(\d+)/); 1013 | url = 1014 | urlFromString(thisLine) 1015 | .replace(lineAndColumn[0], "") 1016 | .split("#")[0] || ""; 1017 | caller = htmlToString( 1018 | thisLine 1019 | .replace(urlFromString(thisLine), "") 1020 | .replace(separator, "") 1021 | .replace("at ", "") 1022 | .trim() 1023 | ); 1024 | if (caller === "" || caller === lineAndColumn[0]) { 1025 | continue; 1026 | } 1027 | if (url[url.length - 1] === "/") { 1028 | txt = "(index)"; 1029 | } else { 1030 | txt = 1031 | url 1032 | .split("\\") 1033 | .pop() 1034 | .split("/") 1035 | .filter(Boolean) 1036 | .pop() || caller; 1037 | } 1038 | callStack.push({ 1039 | caller: caller, 1040 | url: url ? url.split(":")[0] + ":" + url.split(":")[1] : caller, 1041 | linkText: txt + lineAndColumn[0], 1042 | line: lineAndColumn[1], 1043 | col: lineAndColumn[2], 1044 | originalLine: thisLine 1045 | }); 1046 | } 1047 | return callStack; 1048 | } 1049 | function traceToTable(table, trace) { 1050 | var i, tdLeft, tdRight, tr; 1051 | if (typeof trace === "undefined") { 1052 | return; 1053 | } 1054 | trace.reverse(); //reverse order of trace, just like in a browser's console 1055 | i = trace.length; 1056 | while (i--) { 1057 | tdLeft = elements.td.cloneNode(false); 1058 | tdRight = elements.td.cloneNode(false); 1059 | tr = elements.tr.cloneNode(false); 1060 | tdLeft.innerHTML = trace[i].caller; 1061 | tdRight.innerHTML = " @ "; 1062 | tdRight.appendChild(getLink(trace[i].url || "", trace[i].linkText)); 1063 | tr.appendChild(tdLeft); 1064 | tr.appendChild(tdRight); 1065 | table.insertBefore(tr, table.firstChild); 1066 | } 1067 | return table; 1068 | } 1069 | function colorizeData(key, value) { 1070 | var valueColor = "#3c53da", 1071 | keyColor = "#ae33b7", 1072 | classname = getClass(value); 1073 | if (value && classname.indexOf("HTML") !== -1) { 1074 | value = htmlToString(value.outerHTML); 1075 | valueColor = "#ad8200"; 1076 | } else if (key === "innerHTML" || key === "outerHTML") { 1077 | value = htmlToString(value); 1078 | valueColor = "#ad8200"; 1079 | } 1080 | if (typeof value === "string") { 1081 | valueColor = "#c54300"; 1082 | //HARD limit, for speed/mem issues with consecutive logging of large strings 1083 | if (value.length > 400) { 1084 | value = 1085 | '"' + 1086 | String(value).substring(0, 400) + 1087 | '" [...]
Note: string was truncated to 400 chars'; 1088 | } else { 1089 | value = '"' + value + '"'; 1090 | } 1091 | } else if (value === null) { 1092 | valueColor = "#808080"; 1093 | value = "null"; 1094 | } else if (typeof value === "undefined" || value === undefined) { 1095 | valueColor = "#808080"; 1096 | value = "undefined"; 1097 | } else if (typeof value === "object") { 1098 | if (isEmpty(value)) { 1099 | value = "{}"; 1100 | } else { 1101 | valueColor = ""; 1102 | //iterate over object to create another table inside 1103 | var tempTable = createElem("table", "stackTraceSubTable", { 1104 | border: "0 none", 1105 | margin: 0, 1106 | display: "none", 1107 | marginLeft: "10px", 1108 | marginTop: options.browserinfo.isMobile ? "8px" : "4px", 1109 | tableLayout: "auto", 1110 | maxWidth: "100%", 1111 | color: "#333333" 1112 | }), 1113 | wrap = document.createElement("div"); 1114 | wrap.appendChild(objectToTable(tempTable, value).cloneNode(true)); 1115 | if (Array.isArray(value)) { 1116 | value = "Array(" + value.length + ")" + wrap.innerHTML; 1117 | } else { 1118 | value = wrap.innerHTML; 1119 | } 1120 | } 1121 | } 1122 | 1123 | return ( 1124 | '' + 1127 | key + 1128 | ': ' + 1131 | value + 1132 | "" 1133 | ); 1134 | } 1135 | function objectToTable(table, object) { 1136 | var i, tdLeft, tr; 1137 | if (isElement(object)) { 1138 | tdLeft = elements.td.cloneNode(false); 1139 | tr = elements.tr.cloneNode(false); 1140 | tdLeft.innerHTML = htmlToString(object.outerHTML); 1141 | tr.appendChild(tdLeft); 1142 | table.appendChild(tr); 1143 | } else { 1144 | for (i in object) { 1145 | if (object.hasOwnProperty(i)) { 1146 | tdLeft = elements.td.cloneNode(false); 1147 | tr = elements.tr.cloneNode(false); 1148 | tdLeft.innerHTML = colorizeData(i, object[i]); 1149 | tr.appendChild(tdLeft); 1150 | table.appendChild(tr); 1151 | } 1152 | } 1153 | } 1154 | return table; 1155 | } 1156 | function toggleDetails(e) { 1157 | var button = e.currentTarget || e.srcElement, 1158 | i, 1159 | hidden; 1160 | if (button.getAttribute("toggles") === "table") { 1161 | var tables = button.parentElement.getElementsByTagName("table"); 1162 | i = tables.length; 1163 | while (i--) { 1164 | hidden = 1165 | (tables[i].currentStyle 1166 | ? tables[i].currentStyle.display 1167 | : window.getComputedStyle(tables[i], null).display) === "none"; 1168 | button.innerHTML = button.innerHTML.replace( 1169 | hidden ? elements.arrowRight : elements.arrowDown, 1170 | hidden ? elements.arrowDown : elements.arrowRight 1171 | ); 1172 | setCSS(tables[i], { display: hidden ? "table" : "none" }); 1173 | } 1174 | } 1175 | } 1176 | function isRepeat(message, method) { 1177 | return ( 1178 | history.output.prevMsg === message && 1179 | history.output.prevMethod === method && 1180 | typeof message !== "object" && 1181 | method !== "trace" && 1182 | method !== "group" && 1183 | method !== "groupCollapsed" && 1184 | method !== "groupEnd" 1185 | ); 1186 | } 1187 | function newConsole() { 1188 | try { 1189 | //get arguments, set vars 1190 | var method = arguments[0], 1191 | className, 1192 | isHTMLElement, 1193 | message = typeof arguments[1].newMessage !== "undefined" ? arguments[1].newMessage : undefined, 1194 | stackTrace = typeof arguments[1].newStackTrace !== "undefined" ? arguments[1].newStackTrace : undefined; 1195 | 1196 | //if message emtpy or undefined, show empty message-message 1197 | if (message === "" || typeof message === "undefined" || message === undefined) { 1198 | message = messages.empty; 1199 | } 1200 | 1201 | if (isRepeat(message, method) && method.indexOf("time") === -1) { 1202 | // up the counter and add the dot 1203 | history.output.counter = history.output.counter + 1; 1204 | elements.table.lastChild.countDot = elements.table.lastChild.countDot || elements.dot.cloneNode(false); 1205 | elements.table.lastChild.firstChild.insertBefore( 1206 | elements.table.lastChild.countDot, 1207 | elements.table.lastChild.firstChild.firstChild 1208 | ).innerHTML = history.output.counter; 1209 | setLineStyle(elements.table.lastChild, method, message); 1210 | } else { 1211 | history.output.prevMsg = message; 1212 | history.output.prevMethod = method; 1213 | history.output.counter = 1; 1214 | 1215 | //an object requires some more handling 1216 | if (typeof message === "object" && method !== "assert" && method !== "timeEnd") { 1217 | message = isElement(message) 1218 | ? htmlToString(message.outerHTML.match(/<(.*?)>/g)[0] + "..." + message.outerHTML.match(/<(.*?)>/g).pop()) //gets e.g.
...
1219 | : objectToString(message); 1220 | } else if (method !== "assert" && method.indexOf("time") === -1) { 1221 | message = htmlToString(message); 1222 | } 1223 | 1224 | var detailTable, 1225 | stackTable, 1226 | msgContainer = elements.msgContainer.cloneNode(false), 1227 | lineContainer = elements.tr.cloneNode(false), 1228 | leftContainer = elements.tdLeft.cloneNode(true), 1229 | rightContainer = elements.tdRight.cloneNode(false), 1230 | arrows = stackTrace ? elements.arrowRight + " " : ""; 1231 | 1232 | switch (method) { 1233 | case "assert": 1234 | if (message[0] === false) { 1235 | msgContainer.innerHTML = arrows + "Assertion failed: " + message[1]; 1236 | } 1237 | stackTable = traceToTable(elements.stackTraceTable.cloneNode(false), stackTrace); 1238 | method = "error"; //groups it under 'error' and is thus toggleable in view 1239 | break; 1240 | case "log": 1241 | case "debug": 1242 | case "info": 1243 | case "warn": 1244 | if (typeof arguments[1].newMessage === "object") { 1245 | detailTable = objectToTable(elements.stackTraceTable.cloneNode(false), arguments[1].newMessage); 1246 | msgContainer.innerHTML = elements.arrowRight + " " + message; 1247 | } else { 1248 | msgContainer.innerHTML = message; 1249 | } 1250 | break; 1251 | case "error": 1252 | case "trace": 1253 | case "dir": 1254 | case "table": 1255 | //left side 1256 | if (method === "table" || typeof arguments[1].newMessage === "object") { 1257 | detailTable = objectToTable(elements.stackTraceTable.cloneNode(false), arguments[1].newMessage); 1258 | msgContainer.innerHTML = elements.arrowRight + " " + message; 1259 | } else if (method === "trace") { 1260 | message = "console.trace()"; 1261 | msgContainer.innerHTML = arrows + message; 1262 | } else { 1263 | msgContainer.innerHTML = arrows + message; 1264 | } 1265 | stackTable = traceToTable(elements.stackTraceTable.cloneNode(false), stackTrace); 1266 | break; 1267 | case "group": 1268 | case "groupCollapsed": 1269 | case "groupEnd": 1270 | if (method !== "groupEnd") { 1271 | options.groupDepth = options.groupDepth + 1; 1272 | msgContainer.innerHTML = "" + message + ""; 1273 | msgContainer.setAttribute("toggles", "group_" + options.groupDepth); 1274 | } else { 1275 | options.groupDepth = valBetween(options.groupDepth - 1, 0, 99); 1276 | history.output.prevMsg = ""; 1277 | } 1278 | if (options.groupDepth > 0) { 1279 | options.paddingLeft = options.groupDepth * 23 + "px"; 1280 | } else { 1281 | options.paddingLeft = 0; 1282 | } 1283 | break; 1284 | case "time": 1285 | case "timeEnd": 1286 | var timerName = arguments[1].newMessage || "default", 1287 | now, 1288 | passed; 1289 | if (method === "time") { 1290 | status.timers[timerName] = Date.now(); 1291 | if (typeof arguments[1].original === "function") { 1292 | arguments[1].original.apply(console, arguments[1].originalArguments); //make sure we still call the original console.time to start the browser's console timer 1293 | } 1294 | return; 1295 | } 1296 | now = Date.now(); 1297 | if (!status.timers[timerName]) { 1298 | console.warn('Timer "' + timerName + '" does not exist.'); 1299 | return; 1300 | } 1301 | passed = now - (status.timers[timerName] || 0); 1302 | message = timerName + ": " + passed + "ms"; 1303 | msgContainer.innerHTML = message; 1304 | delete status.timers[timerName]; 1305 | break; 1306 | default: 1307 | msgContainer.innerHTML = message; 1308 | } 1309 | 1310 | if (!msgContainer.innerHTML) { 1311 | return; 1312 | } 1313 | leftContainer.appendChild(msgContainer); 1314 | 1315 | if (detailTable || stackTable) { 1316 | setCSS(msgContainer, { cursor: "pointer" }); 1317 | leftContainer.appendChild(detailTable || stackTable); 1318 | msgContainer.setAttribute("toggles", "table"); 1319 | } 1320 | 1321 | //populate right side 1322 | if (stackTrace && typeof stackTrace[stackTrace.length - 1] !== "undefined") { 1323 | rightContainer.appendChild( 1324 | setCSS(getLink(stackTrace[0].url, stackTrace[0].linkText), { color: "#808080" }) 1325 | ); 1326 | } 1327 | 1328 | //add to line 1329 | lineContainer.appendChild(leftContainer); 1330 | lineContainer.appendChild(rightContainer); 1331 | 1332 | //set colors 1333 | setCSS(lineContainer, { display: elements.buttons[method].toggled ? "none" : "" }); 1334 | setLineStyle(lineContainer, method, message); 1335 | 1336 | //set binds 1337 | if (options.browserinfo.evtLstn) { 1338 | msgContainer.addEventListener("click", toggleDetails, false); 1339 | } else { 1340 | msgContainer.attachEvent("onclick", toggleDetails); 1341 | } 1342 | 1343 | //store the lines in the object corresponding to the method used 1344 | elements.lines[method].push(lineContainer); 1345 | 1346 | //handle grouping (group and groupEnd 1347 | if (options.paddingLeft !== 0) { 1348 | setCSS(leftContainer, { paddingLeft: options.paddingLeft }); 1349 | setCSS(msgContainer, { borderLeft: "1px solid #808080", paddingLeft: "5px" }); 1350 | } 1351 | 1352 | //add the line to the table 1353 | elements.table.appendChild(lineContainer); 1354 | } 1355 | //scroll 1356 | consoleElement.toggleScroll(); 1357 | //========================================================== 1358 | //make sure we still call the original method, if applicable (not window.onerror) 1359 | if (typeof arguments[1].original === "function") { 1360 | arguments[1].original.apply(console, arguments[1].originalArguments); 1361 | } 1362 | } catch (e) { 1363 | //not logging. why? throw error 1364 | if (options.browserinfo.isMobile) { 1365 | alert(e); 1366 | } 1367 | originalConsole.error("mobileConsole generated an error logging this event! (type: " + typeof message + ")"); 1368 | originalConsole.error(arguments); 1369 | originalConsole.error(e); 1370 | //try to re-log it as an error 1371 | newConsole("error", e); 1372 | } 1373 | } 1374 | function interceptConsole(method) { 1375 | var original = console ? console[method] : missingMethod(), 1376 | i, 1377 | stackTraceOrig; 1378 | if (!console) { 1379 | console = {}; 1380 | } //create empty console if we have no console (IE?) 1381 | console[method] = function() { 1382 | var args = Array.prototype.slice.call(arguments); 1383 | args.original = original; 1384 | args.originalArguments = arguments; 1385 | args.newMessage = method === "assert" ? [args[0], args[1]] : args[0]; 1386 | //create an Error and get its stack trace and format it 1387 | try { 1388 | throw new Error(); 1389 | } catch (e) { 1390 | stackTraceOrig = e.stack; 1391 | } 1392 | args.newStackTrace = formatStackTrace(args.newStackTrace, stackTraceOrig); 1393 | if (method === "clear") { 1394 | try { 1395 | elements.table.innerHTML = ""; 1396 | } catch (e) { 1397 | console.log("This browser does not allow clearing tables, the console cannot be cleared."); 1398 | return; 1399 | } 1400 | history.output.prevMethod = ""; 1401 | i = options.methods.length; 1402 | while (i--) { 1403 | elements.lines[options.methods[i]] = []; 1404 | } 1405 | options.groupDepth = 0; 1406 | options.paddingLeft = 0; 1407 | console.log(messages.clear); 1408 | originalConsole.clear(); 1409 | return; 1410 | } 1411 | //Handle the new console logging 1412 | newConsole(method, args); 1413 | }; 1414 | } 1415 | //init 1416 | function init() { 1417 | //Intercept all original console methods including trace. Register the event type as a line type. 1418 | var i = options.methods.length; 1419 | while (i--) { 1420 | elements.lines[options.methods[i]] = []; 1421 | interceptConsole(options.methods[i]); 1422 | } 1423 | //Bind to window.onerror 1424 | window.onerror = function() { 1425 | var args = Array.prototype.slice.call(arguments); 1426 | args.newMessage = args[0]; 1427 | args.newStackTrace = formatStackTrace(arguments); 1428 | newConsole("error", args); 1429 | }; 1430 | 1431 | return { 1432 | //nothing to expose 1433 | }; 1434 | } 1435 | //return 1436 | if (!ref) { 1437 | ref = init(); 1438 | } 1439 | return ref; 1440 | } 1441 | 1442 | //initialize the console commandline 1443 | function initCommandLine() { 1444 | //reference 1445 | var ref; 1446 | //sub helpers 1447 | function getFromArrayById(id) { 1448 | var pos = elements.acItems 1449 | .map(function(x) { 1450 | return x.id; 1451 | }) 1452 | .indexOf(id); 1453 | return { 1454 | position: pos, 1455 | element: pos !== -1 ? elements.acItems[pos] : undefined 1456 | }; 1457 | } 1458 | function findInArray(array, match) { 1459 | return array.filter(function(item, index, self) { 1460 | return typeof item === "string" && item.indexOf(match) > -1 && index === self.indexOf(item); 1461 | }); 1462 | } 1463 | //core 1464 | function assemble() { 1465 | elements.consoleinput.setAttribute("type", "text"); 1466 | elements.consoleinput.setAttribute("autocapitalize", "off"); 1467 | elements.consoleinput.setAttribute("autocorrect", "off"); 1468 | elements.autocompleteItem.setAttribute("href", "#"); 1469 | elements.gt.innerHTML = ">"; 1470 | elements.input.appendChild(elements.gt); 1471 | elements.input.appendChild(elements.consoleinput); 1472 | elements.input.appendChild(elements.autocomplete); 1473 | elements.base.appendChild(elements.input); 1474 | 1475 | return elements.base; 1476 | } 1477 | function submitCommand(command) { 1478 | if (command !== "") { 1479 | storeCommand(command); 1480 | var result; 1481 | try { 1482 | result = eval.call(window, command.trim()); 1483 | console.log.call(window, result); 1484 | } catch (e) { 1485 | console.error(e.message); 1486 | } finally { 1487 | elements.consoleinput.value = ""; 1488 | } 1489 | } 1490 | } 1491 | function hoverAutoComplete(e) { 1492 | if (typeof e === "undefined") { 1493 | return; 1494 | } 1495 | //unset any already hovered elements 1496 | var hovered = getFromArrayById("hover").element, 1497 | target = e.target || e.srcElement, 1498 | over; 1499 | if (typeof hovered !== "undefined") { 1500 | setCSS(hovered, { 1501 | color: "", 1502 | backgroundColor: "" 1503 | }).id = ""; 1504 | } 1505 | if (e.type === "mouseover") { 1506 | status.acHovered = true; 1507 | over = true; 1508 | } else { 1509 | over = false; 1510 | } 1511 | setCSS(target, { 1512 | color: over ? "#FFFFFF" : "", 1513 | backgroundColor: over ? "rgb(66,139,202)" : "" 1514 | }).id = over ? "hover" : ""; 1515 | } 1516 | function toggleAutoComplete(show) { 1517 | var hidden = 1518 | (elements.autocomplete.currentStyle 1519 | ? elements.autocomplete.currentStyle.display 1520 | : window.getComputedStyle(elements.autocomplete, null).display) === "none"; 1521 | show = typeof show === "undefined" ? hidden : show; 1522 | setCSS(elements.autocomplete, { display: show ? "inherit" : "none" }); 1523 | status.acActive = show; 1524 | if (!show) { 1525 | status.acHovered = false; 1526 | } 1527 | } 1528 | function clickAutoComplete(e) { 1529 | if (e.preventDefault) { 1530 | e.preventDefault(); 1531 | } else { 1532 | e.returnValue = false; 1533 | } 1534 | var tgt = e.target || e.srcElement; 1535 | elements.consoleinput.value = tgt.innerHTML; 1536 | elements.consoleinput.focus(); 1537 | toggleAutoComplete(); 1538 | } 1539 | function autoComplete(command) { 1540 | if (command.length < 1) { 1541 | toggleAutoComplete(false); 1542 | return; 1543 | } 1544 | var searchString = encodeURI(command), 1545 | matches, 1546 | match, 1547 | row, 1548 | i, 1549 | maxAmount = options.browserinfo.isMobile ? 3 : 5; 1550 | elements.autocomplete.innerHTML = ""; 1551 | elements.acItems = []; 1552 | matches = findInArray(history.input.commands, searchString); 1553 | matches = matches.slice(Math.max(matches.length - maxAmount, 0)); 1554 | i = matches.length; 1555 | while (i--) { 1556 | match = decodeURI(matches[i]); 1557 | row = elements.autocompleteItem.cloneNode(false); 1558 | row.innerHTML = match; 1559 | row.onmouseover = hoverAutoComplete; 1560 | elements.autocomplete.insertBefore(row, elements.autocomplete.firstChild); 1561 | elements.acItems.unshift(row); 1562 | } 1563 | toggleAutoComplete(matches.length > 0); 1564 | } 1565 | function setBinds() { 1566 | if (options.browserinfo.evtLstn) { 1567 | elements.autocomplete.addEventListener("click", clickAutoComplete, false); 1568 | } else { 1569 | elements.autocomplete.attachEvent("onclick", clickAutoComplete); 1570 | } 1571 | document.onkeydown = function(e) { 1572 | e = e || window.event; 1573 | var tgt = e.target || e.srcElement; 1574 | if (tgt === elements.consoleinput) { 1575 | if (e.key === "Enter" || e.keyCode === 13) { 1576 | //enter 1577 | if (e.preventDefault) { 1578 | e.preventDefault(); 1579 | } else { 1580 | e.returnValue = false; 1581 | } 1582 | if (!status.acHovered) { 1583 | submitCommand(elements.consoleinput.value); 1584 | } else { 1585 | elements.consoleinput.value = getFromArrayById("hover").element.innerHTML; 1586 | elements.consoleinput.focus(); 1587 | } 1588 | toggleAutoComplete(false); 1589 | status.acInput = ""; 1590 | } else if (e.keyCode === 38 || e.keyCode === 40) { 1591 | //up and down arrows for history browsing 1592 | if (e.preventDefault) { 1593 | e.preventDefault(); 1594 | } else { 1595 | e.returnValue = false; 1596 | } 1597 | var up = e.keyCode === 40; 1598 | if (status.acActive) { 1599 | //autocomplete window is opened 1600 | //get id of currently hovered element 1601 | var hovered = getFromArrayById("hover").position; 1602 | var counter = hovered === -1 ? elements.acItems.length : hovered; 1603 | //hover new (in- or decreased number) one 1604 | counter = valBetween((counter += up ? 1 : -1), 0, elements.acItems.length - 1); 1605 | hoverAutoComplete({ target: elements.acItems[counter], type: "mouseover" }); 1606 | } else { 1607 | //autocompete window not opened 1608 | var hist = history.input.commands; 1609 | history.input.commandIdx += up ? 1 : -1; 1610 | history.input.commandIdx = valBetween(history.input.commandIdx, 0, hist.length); 1611 | elements.consoleinput.value = 1612 | typeof hist[history.input.commandIdx] === "undefined" ? "" : decodeURI(hist[history.input.commandIdx]); 1613 | } 1614 | } 1615 | } 1616 | if (e.keyCode === 27 && status.acActive) { 1617 | toggleAutoComplete(false); 1618 | } 1619 | }; 1620 | document.onkeyup = function(e) { 1621 | e = e || window.event; 1622 | var tgt = e.target || e.srcElement; 1623 | if ( 1624 | tgt === elements.consoleinput && 1625 | status.acInput !== elements.consoleinput.value && 1626 | (e.keyCode !== 38 && e.keyCode !== 40 && e.keyCode !== 27 && e.key !== "Enter" && e.keyCode !== 13) 1627 | ) { 1628 | status.acInput = elements.consoleinput.value.trim(); 1629 | autoComplete(elements.consoleinput.value); 1630 | } 1631 | }; 1632 | } 1633 | //init 1634 | function init() { 1635 | assemble(); 1636 | setBinds(); 1637 | return { 1638 | //nothing to expose 1639 | }; 1640 | } 1641 | //return 1642 | if (!ref) { 1643 | ref = init(); 1644 | } 1645 | return ref; 1646 | } 1647 | 1648 | function init() { 1649 | if (!status.initialized) { 1650 | if (consoleElement && mobileConsole) { 1651 | console.error( 1652 | "Mobile Console cannot be reconstructed! Reload the page to enable Mobile Console again." + 1653 | "\n" + 1654 | "Tip: use the minimize button instead of closing." 1655 | ); 1656 | return; 1657 | } else { 1658 | status.initialized = true; 1659 | //populate references 1660 | if (!mobileConsole) { 1661 | //taps into native console and adds new functionality 1662 | mobileConsole = initConsole(); 1663 | } 1664 | if (!consoleElement && mobileConsole) { 1665 | //creates the new HTML console element and attaches it to document 1666 | consoleElement = initConsoleElement(); 1667 | } 1668 | if (!commandLine && consoleElement && mobileConsole) { 1669 | //creates an HTML commandline and attaches it to existing console element 1670 | commandLine = initCommandLine(); 1671 | } 1672 | } 1673 | } 1674 | //log a 'welcome' message 1675 | console.info( 1676 | "--==## Mobile Console v" + options.version + " " + (status.initialized ? "active" : "inactive") + " ##==--" 1677 | ); 1678 | } 1679 | 1680 | //autorun if mobile 1681 | if (options.browserinfo.isMobile || options.overrideAutorun) { 1682 | init(); 1683 | } 1684 | 1685 | //expose the mobileConsole's methods 1686 | return { 1687 | init: init, 1688 | about: about, 1689 | toggle: toggleHeight, 1690 | status: status, 1691 | options: options 1692 | }; 1693 | })(); 1694 | -------------------------------------------------------------------------------- /serve-benchmark/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactLoading from "react-loading"; 3 | import { default as Stats } from "stats.js"; 4 | import { UAParser } from "ua-parser-js"; 5 | import { getWebGLContext } from "gpu-compute"; 6 | 7 | const parser = new UAParser(); 8 | var result = parser.getResult(); 9 | 10 | const startBenchmarking = (window as any).gpuSortGenerate.startBenchmarking as () => void; 11 | const getBenchmarkText = (window as any).gpuSortGenerate.getBenchmarkText as () => string; 12 | const isBenchmarking = (window as any).gpuSortGenerate.isBenchmarking as () => boolean; 13 | 14 | interface AppState { 15 | output: string; 16 | benchmarking: boolean; 17 | } 18 | 19 | export default class App extends React.Component<{}, AppState> { 20 | state = { output: getBenchmarkText() } as AppState; 21 | 22 | componentDidMount = () => { 23 | const stats = new Stats(); 24 | stats.showPanel(0); 25 | document.body.appendChild(stats.dom); 26 | const animate = () => { 27 | stats.begin(); 28 | stats.end(); 29 | requestAnimationFrame(animate); 30 | }; 31 | requestAnimationFrame(animate); 32 | setInterval(() => this.setState({ output: getBenchmarkText(), benchmarking: isBenchmarking() }), 100); 33 | }; 34 | 35 | getWebGLRenderer() { 36 | const gl = getWebGLContext(); 37 | const debugInfo = gl.getExtension("WEBGL_debug_renderer_info"); 38 | if (!debugInfo) return `${debugInfo}`; 39 | return `${gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)}`; 40 | } 41 | 42 | render() { 43 | return ( 44 |
45 |
56 | {(() => { 57 | if (!this.state.benchmarking) { 58 | return ( 59 | 74 | ); 75 | } else { 76 | return ; 77 | } 78 | })()} 79 |
80 | {`${result.browser.name} ${result.browser.major}, ` + 81 | `${result.os.name} ${result.os.version}, ` + 82 | `${this.getWebGLRenderer()}`} 83 |
84 |
92 | {this.state.output} 93 |
94 |
95 |
96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /serve-benchmark/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /serve-benchmark/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | ReactDOM.render(, document.getElementById("root")); 7 | -------------------------------------------------------------------------------- /serve-benchmark/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /serve-benchmark/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react" 17 | }, 18 | "include": ["src"] 19 | } 20 | -------------------------------------------------------------------------------- /src/benchmarks/generate.bench.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | import * as index from "../index"; 4 | 5 | var benchmarking = false; 6 | const widths = [256, 512, 1024, 2048, 4096]; 7 | const results = new Array(widths.length * 3).fill("pending"); 8 | 9 | export function isBenchmarking() { 10 | return benchmarking; 11 | } 12 | 13 | function insertRandomNumbers(i: number, array: Float64Array, callback: () => void) { 14 | for (let j = 0; j < 1e5; j++) array[i++] = Math.random() - 0.5; 15 | if (i >= array.length) return callback(); 16 | requestAnimationFrame(() => insertRandomNumbers(i, array, callback)); 17 | } 18 | 19 | function checkSorting(array: ArrayLike) { 20 | if (false) { 21 | for (let i = 0; i < array.length - 1; i++) { 22 | if (array[i] > array[i + 1]) { 23 | console.error(array); 24 | throw new Error(`index:${i} (${array[i]} > ${array[i + 1]})`); 25 | } 26 | } 27 | } 28 | } 29 | 30 | function gpuFloat64Array(width: number) { 31 | const array = new Float64Array(Array.from(Array((width * width) / 2), () => Math.random() - 0.5)); 32 | let start = performance.now(); 33 | index.sort(array); 34 | checkSorting(array); 35 | return performance.now() - start; 36 | } 37 | 38 | function gpuFloat64ArrayAsync(width: number): Promise { 39 | return new Promise(resolve => { 40 | const array = new Float64Array((width * width) / 2); 41 | insertRandomNumbers(0, array, () => { 42 | let start = performance.now(); 43 | index.sortAsync(array).then(() => { 44 | checkSorting(array); 45 | resolve(performance.now() - start); 46 | }); 47 | }); 48 | }); 49 | } 50 | 51 | function cpuFloat64Array(width: number) { 52 | const array = new Float64Array(Array.from(Array((width * width) / 2), () => Math.random() - 0.5)); 53 | let start = performance.now(); 54 | array.sort((a, b) => a - b); 55 | return performance.now() - start; 56 | } 57 | 58 | async function prebenchWarmup() { 59 | index.initializeShaders(); 60 | gpuFloat64Array(widths[0]); 61 | await gpuFloat64ArrayAsync(widths[0]); 62 | cpuFloat64Array(widths[0]); 63 | } 64 | 65 | function integerWithCommas(x: number) { 66 | return Math.round(x) 67 | .toString() 68 | .replace(/\B(?=(\d{3})+(?!\d))/g, ","); 69 | } 70 | 71 | export async function startBenchmarking() { 72 | if (!benchmarking) { 73 | benchmarking = true; 74 | results.fill("pending"); 75 | if (true) { 76 | await prebenchWarmup(); 77 | } 78 | if (true) { 79 | for (let i = 0; i < widths.length; i++) { 80 | const n = gpuFloat64Array(widths[i]); 81 | results[i + 0 * widths.length] = `${integerWithCommas(n)}ms`; 82 | await new Promise(fn => setTimeout(fn, 500)); 83 | } 84 | } 85 | if (true) { 86 | for (let i = 0; i < widths.length; i++) { 87 | const n = await gpuFloat64ArrayAsync(widths[i]); 88 | results[i + 1 * widths.length] = `${integerWithCommas(n)}ms`; 89 | await new Promise(fn => setTimeout(fn, 500)); 90 | } 91 | } 92 | if (true) { 93 | for (let i = 0; i < widths.length; i++) { 94 | const n = cpuFloat64Array(widths[i]); 95 | results[i + 2 * widths.length] = `${integerWithCommas(n)}ms`; 96 | await new Promise(fn => setTimeout(fn, 500)); 97 | } 98 | } 99 | benchmarking = false; 100 | } 101 | } 102 | 103 | export function getBenchmarkText() { 104 | return `‎‎‎ 105 | 32,768 106 | gpu.sort ${results[0]} 107 | gpu.sortAsync ${results[5]} 108 | Float64Array.sort ${results[10]} 109 | 110 | 131,072 111 | gpu.sort ${results[1]} 112 | gpu.sortAsync ${results[6]} 113 | Float64Array.sort ${results[11]} 114 | 115 | 524,288 116 | gpu.sort ${results[2]} 117 | gpu.sortAsync ${results[7]} 118 | Float64Array.sort ${results[12]} 119 | 120 | 2,097,152 121 | gpu.sort ${results[3]} 122 | gpu.sortAsync ${results[8]} 123 | Float64Array.sort ${results[13]} 124 | 125 | 8,388,608 126 | gpu.sort ${results[4]} 127 | gpu.sortAsync ${results[9]} 128 | Float64Array.sort ${results[14]} 129 | `.trim(); 130 | } 131 | 132 | export default { 133 | startBenchmarking, 134 | getBenchmarkText, 135 | isBenchmarking: () => benchmarking 136 | }; 137 | -------------------------------------------------------------------------------- /src/bitonic.ts: -------------------------------------------------------------------------------- 1 | import * as gpu from "gpu-compute"; 2 | import { Limiter } from "./limiter"; 3 | import { BitonicUniformGenerator } from "./uniforms"; 4 | 5 | export function bitonicSort( 6 | array: Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array, 7 | kind: "Int32Array" | "Uint32Array" | "Float32Array" | "Float64Array" | "BigInt64Array" | "BigUint64Array" 8 | ) { 9 | const bytes = new Uint8Array(array.buffer); 10 | const target = getRenderTarget(bytes); 11 | target.pushTextureData(bytes); 12 | const emptyTexelCount = target.width * target.width - bytes.length / 4; 13 | const generator = new BitonicUniformGenerator(target, kind, emptyTexelCount); 14 | for (let x of generator.generate()) target.compute(x.shader, x.uniforms); 15 | pullPixels(target, emptyTexelCount, bytes); 16 | target.delete(); 17 | return array; 18 | } 19 | 20 | export function bitonicSortAsync( 21 | array: Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array, 22 | kind: "Int32Array" | "Uint32Array" | "Float32Array" | "Float64Array" | "BigInt64Array" | "BigUint64Array" 23 | ): Promise { 24 | return new Promise((resolve, reject) => { 25 | const bytes = new Uint8Array(array.buffer); 26 | const target = getRenderTarget(bytes); 27 | target 28 | .pushTextureDataAsync(bytes) 29 | .then(() => { 30 | const limiter = new Limiter(target.width); 31 | const emptyTexelCount = target.width * target.width - bytes.length / 4; 32 | const generator = new BitonicUniformGenerator(target, kind, emptyTexelCount); 33 | for (let x of generator.generate()) limiter.addWork(() => target.compute(x.shader, x.uniforms)); 34 | limiter.onceFinished(() => { 35 | pullPixelsAsync(target, emptyTexelCount, bytes) 36 | .then(() => resolve()) 37 | .catch(err => reject(err)) 38 | .finally(() => (target.delete(), limiter.stop())); 39 | }); 40 | }) 41 | .catch(err => (reject(err), target.delete())); 42 | }); 43 | } 44 | 45 | export function getRenderTarget(bytes: Uint8Array) { 46 | const gl = gpu.getWebGLContext(); 47 | const framebufferLimit = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE); 48 | for (let width = 1; width <= framebufferLimit; width *= 2) { 49 | if (width * width * 4 >= bytes.length) { 50 | return new gpu.RenderTarget(width); 51 | } 52 | } 53 | throw new Error(`data overflows ${framebufferLimit}x${framebufferLimit} framebuffer`); 54 | } 55 | 56 | export function pullPixels(target: gpu.RenderTarget, e: number, bytes: Uint8Array) { 57 | const w = target.width; 58 | const h = Math.floor(e / w); 59 | if (e % w > 0) { 60 | target.readSomePixels(e % w, h, w - (e % w), 1, bytes.subarray(0, 4 * (w - (e % w)))); 61 | target.readSomePixels(0, h + 1, w, w - h - 1, bytes.subarray(4 * (w - (e % w)))); 62 | } else { 63 | target.readSomePixels(0, h, w, w - h, bytes); 64 | } 65 | } 66 | 67 | export async function pullPixelsAsync(target: gpu.RenderTarget, e: number, bytes: Uint8Array) { 68 | const w = target.width; 69 | const h = Math.floor(e / w); 70 | if (e % w > 0) { 71 | await target.readSomePixelsAsync(e % w, h, w - (e % w), 1, bytes.subarray(0, 4 * (w - (e % w)))); 72 | return target.readSomePixelsAsync(0, h + 1, w, w - h - 1, bytes.subarray(4 * (w - (e % w)))); 73 | } else { 74 | return target.readSomePixelsAsync(0, h, w, w - h, bytes); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/frag.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.frag" { 2 | const value: string; 3 | export default value; 4 | } 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { initializeShaders } from "./initialize"; 2 | import { setWebGLContext } from "gpu-compute"; 3 | import { sort, sortAsync } from "./sort"; 4 | 5 | module.exports = { 6 | initializeShaders, 7 | setWebGLContext, 8 | sort, 9 | sortAsync 10 | }; 11 | 12 | export { initializeShaders } from "./initialize"; 13 | export { setWebGLContext } from "gpu-compute"; 14 | export { sort, sortAsync } from "./sort"; 15 | -------------------------------------------------------------------------------- /src/initialize.ts: -------------------------------------------------------------------------------- 1 | import * as gpu from "gpu-compute"; 2 | import { readFileSync } from "fs"; 3 | 4 | export const searchAndReplace = { 5 | "float round(float);": gpu.functionStrings.round, 6 | "float floatEquals(float, float);": gpu.functionStrings.floatEquals, 7 | "float floatNotEquals(float, float);": gpu.functionStrings.floatNotEquals, 8 | "float floatLessThan(float, float);": gpu.functionStrings.floatLessThan, 9 | "float floatGreaterThan(float, float);": gpu.functionStrings.floatGreaterThan, 10 | "float floatLessThanOrEqual(float, float);": gpu.functionStrings.floatLessThanOrEqual, 11 | "float floatGreaterThanOrEqual(float, float);": gpu.functionStrings.floatGreaterThanOrEqual, 12 | "float vec2ToUint16(vec2);": gpu.functionStrings.vec2ToUint16, 13 | "vec2 uint16ToVec2(float);": gpu.functionStrings.uint16ToVec2 14 | }; 15 | 16 | const transform64 = readFileSync(require.resolve("./shaders/00_transform64.frag"), "utf8"); 17 | const sort64 = readFileSync(require.resolve("./shaders/01_sort64.frag"), "utf8"); 18 | const untransform64 = readFileSync(require.resolve("./shaders/02_untransform64.frag"), "utf8"); 19 | 20 | var transform64Shader: gpu.ComputeShader | undefined; 21 | var sort64Shader: gpu.ComputeShader | undefined; 22 | var untransform64Shader: gpu.ComputeShader | undefined; 23 | 24 | const transform32 = readFileSync(require.resolve("./shaders/00_transform32.frag"), "utf8"); 25 | const sort32 = readFileSync(require.resolve("./shaders/01_sort32.frag"), "utf8"); 26 | const untransform32 = readFileSync(require.resolve("./shaders/02_untransform32.frag"), "utf8"); 27 | 28 | var transform32Shader: gpu.ComputeShader | undefined; 29 | var sort32Shader: gpu.ComputeShader | undefined; 30 | var untransform32Shader: gpu.ComputeShader | undefined; 31 | 32 | export function getTransform64Shader() { 33 | if (!transform64Shader) transform64Shader = new gpu.ComputeShader(transform64, searchAndReplace); 34 | return transform64Shader; 35 | } 36 | 37 | export function getSort64Shader() { 38 | if (!sort64Shader) sort64Shader = new gpu.ComputeShader(sort64, searchAndReplace); 39 | return sort64Shader; 40 | } 41 | 42 | export function getUntransform64Shader() { 43 | if (!untransform64Shader) untransform64Shader = new gpu.ComputeShader(untransform64, searchAndReplace); 44 | return untransform64Shader; 45 | } 46 | 47 | export function getTransform32Shader() { 48 | if (!transform32Shader) transform32Shader = new gpu.ComputeShader(transform32, searchAndReplace); 49 | return transform32Shader; 50 | } 51 | 52 | export function getSort32Shader() { 53 | if (!sort32Shader) sort32Shader = new gpu.ComputeShader(sort32, searchAndReplace); 54 | return sort32Shader; 55 | } 56 | 57 | export function getUntransform32Shader() { 58 | if (!untransform32Shader) untransform32Shader = new gpu.ComputeShader(untransform32, searchAndReplace); 59 | return untransform32Shader; 60 | } 61 | 62 | export function initializeShaders() { 63 | getTransform64Shader(); 64 | getSort64Shader(); 65 | getUntransform64Shader(); 66 | 67 | getTransform32Shader(); 68 | getSort32Shader(); 69 | getUntransform32Shader(); 70 | } 71 | -------------------------------------------------------------------------------- /src/limiter.ts: -------------------------------------------------------------------------------- 1 | import { getWebGLContext } from "gpu-compute"; 2 | 3 | export class Limiter { 4 | private stopped = false; 5 | private tally = 0; 6 | private readonly rate: number; 7 | private readonly work = [] as (() => void)[]; 8 | 9 | constructor(width: number) { 10 | if (width >= 4096) { 11 | this.rate = 1; 12 | } else if (width === 2048) { 13 | this.rate = 4; 14 | } else if (width === 1024) { 15 | this.rate = 16; 16 | } else { 17 | this.rate = 128; 18 | } 19 | this.executeWork(); 20 | } 21 | 22 | public stop() { 23 | this.stopped = true; 24 | } 25 | 26 | private executeWork() { 27 | this.tally += this.rate; 28 | if (this.work.length === 0) { 29 | this.tally = Math.max(this.tally, 1); 30 | if (!this.stopped) requestAnimationFrame(() => this.executeWork()); 31 | return; 32 | } 33 | if (this.tally >= 1) { 34 | const limit = Math.floor(Math.min(this.work.length, Math.max(this.rate, 1))); 35 | for (let i = 0; i < limit; i++) this.work[i](); 36 | this.work.splice(0, limit); 37 | this.tally -= limit; 38 | } 39 | getWebGLContext().flush(); 40 | requestAnimationFrame(() => this.executeWork()); 41 | } 42 | 43 | public addWork(work: () => void) { 44 | if (this.stopped) throw new Error("limiter has been stopped"); 45 | this.work.push(work); 46 | } 47 | 48 | public onceFinished(callback: () => void) { 49 | const check = () => { 50 | if (!this.work.length) { 51 | callback(); 52 | } else { 53 | requestAnimationFrame(() => check()); 54 | } 55 | }; 56 | check(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/parameters.ts: -------------------------------------------------------------------------------- 1 | import * as gpu from "gpu-compute"; 2 | import * as init from "./initialize"; 3 | 4 | export enum TRANSFORM_MODE { 5 | PASSTHROUGH = 0, 6 | INTEGER = 1, 7 | FLOAT = 2 8 | } 9 | 10 | export enum CONSTRUCTOR_MODE { 11 | Int32Array = 0, 12 | Uint32Array = 1, 13 | Float32Array = 2, 14 | Float64Array = 3, 15 | BigInt64Array = 4, 16 | BigUint64Array = 5 17 | } 18 | 19 | export interface bitonicParameters { 20 | mode: TRANSFORM_MODE; 21 | constructor: CONSTRUCTOR_MODE; 22 | transformShader: gpu.ComputeShader; 23 | sortShader: gpu.ComputeShader; 24 | untransformShader: gpu.ComputeShader; 25 | } 26 | 27 | export function getBitonicParameters(constructorName: string): bitonicParameters { 28 | switch (constructorName) { 29 | case "Int32Array": 30 | return { 31 | mode: TRANSFORM_MODE.INTEGER, 32 | constructor: CONSTRUCTOR_MODE.Int32Array, 33 | transformShader: init.getTransform32Shader(), 34 | sortShader: init.getSort32Shader(), 35 | untransformShader: init.getUntransform32Shader() 36 | }; 37 | case "Uint32Array": 38 | return { 39 | mode: TRANSFORM_MODE.PASSTHROUGH, 40 | constructor: CONSTRUCTOR_MODE.Uint32Array, 41 | transformShader: init.getTransform32Shader(), 42 | sortShader: init.getSort32Shader(), 43 | untransformShader: init.getUntransform32Shader() 44 | }; 45 | case "Float32Array": 46 | return { 47 | mode: TRANSFORM_MODE.FLOAT, 48 | constructor: CONSTRUCTOR_MODE.Float32Array, 49 | transformShader: init.getTransform32Shader(), 50 | sortShader: init.getSort32Shader(), 51 | untransformShader: init.getUntransform32Shader() 52 | }; 53 | case "Float64Array": 54 | return { 55 | mode: TRANSFORM_MODE.FLOAT, 56 | constructor: CONSTRUCTOR_MODE.Float64Array, 57 | transformShader: init.getTransform64Shader(), 58 | sortShader: init.getSort64Shader(), 59 | untransformShader: init.getUntransform64Shader() 60 | }; 61 | case "BigInt64Array": 62 | return { 63 | mode: TRANSFORM_MODE.INTEGER, 64 | constructor: CONSTRUCTOR_MODE.BigInt64Array, 65 | transformShader: init.getTransform64Shader(), 66 | sortShader: init.getSort64Shader(), 67 | untransformShader: init.getUntransform64Shader() 68 | }; 69 | case "BigUint64Array": 70 | return { 71 | mode: TRANSFORM_MODE.PASSTHROUGH, 72 | constructor: CONSTRUCTOR_MODE.BigUint64Array, 73 | transformShader: init.getTransform64Shader(), 74 | sortShader: init.getSort64Shader(), 75 | untransformShader: init.getUntransform64Shader() 76 | }; 77 | } 78 | throw new Error(`unsupported constructor.name: ${constructorName}`); 79 | } 80 | -------------------------------------------------------------------------------- /src/radix.ts: -------------------------------------------------------------------------------- 1 | function countAsync(rdx: number[], arr: Uint8Array | Uint16Array, i: number, callback: () => void) { 2 | const start = Date.now(); 3 | while (i < arr.length) { 4 | rdx[arr[i++]]++; 5 | if (!(i % 65536) && Date.now() - start > 1000 / 90) { 6 | requestAnimationFrame(() => countAsync(rdx, arr, i, callback)); 7 | return; 8 | } 9 | } 10 | callback(); 11 | } 12 | 13 | function fillAsync(rdx: number[], arr: Uint8Array | Uint16Array, i: number, n: number, callback: (i: number) => void) { 14 | const start = Date.now(); 15 | while (i < arr.length && n < rdx.length) { 16 | arr.fill(n, i, (i += rdx[n++])); 17 | if (Date.now() - start > 1000 / 90) { 18 | requestAnimationFrame(() => fillAsync(rdx, arr, i, n, callback)); 19 | return; 20 | } 21 | } 22 | callback(i); 23 | } 24 | 25 | export function radixSortAsync(arr: Uint8Array | Uint16Array, radix: 256 | 65536) { 26 | return new Promise(resolve => { 27 | const rdx = new Array(radix).fill(0); 28 | countAsync(rdx, arr, 0, () => { 29 | fillAsync(rdx, arr, 0, 0, () => { 30 | resolve(); 31 | }); 32 | }); 33 | }); 34 | } 35 | 36 | export function radixSortSignedAsync(arr: Uint8Array | Uint16Array, radix: 256 | 65536) { 37 | return new Promise(resolve => { 38 | const rdx = new Array(radix).fill(0); 39 | countAsync(rdx, arr, 0, () => { 40 | fillAsync(rdx, arr, 0, radix / 2, i => { 41 | fillAsync(rdx, arr, i, 0, () => { 42 | resolve(); 43 | }); 44 | }); 45 | }); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/shaders/00_transform32.frag: -------------------------------------------------------------------------------- 1 | #ifdef GL_ES 2 | precision mediump float; 3 | precision mediump int; 4 | precision mediump sampler2D; 5 | #endif 6 | 7 | uniform sampler2D u_bytes; 8 | uniform float u_width; 9 | uniform float u_mode; 10 | uniform float u_endianness; 11 | uniform float u_dataDelimX; 12 | uniform float u_dataDelimY; 13 | 14 | const float PASSTHROUGH = 0.0; 15 | const float INTEGER = 1.0; 16 | const float FLOAT = 2.0; 17 | 18 | const float LITTLE_ENDIAN = 0.0; 19 | const float BIG_ENDIAN = 1.0; 20 | 21 | float round(float); 22 | float floatEquals(float, float); 23 | float floatLessThan(float, float); 24 | float floatGreaterThan(float, float); 25 | float floatLessThanOrEqual(float, float); 26 | float floatGreaterThanOrEqual(float, float); 27 | 28 | void main() { 29 | // get current texel 30 | vec4 texel = texture2D(u_bytes, vec2(gl_FragCoord.xy) / u_width); 31 | 32 | // reorder if endianness if not little endian 33 | vec4 reordered = floatEquals(u_endianness, LITTLE_ENDIAN) * texel.rgba 34 | + floatEquals(u_endianness, BIG_ENDIAN) * texel.abgr; 35 | 36 | // denormalize texel data 37 | texel = 255.0 * reordered; 38 | 39 | // initialize flipped texel 40 | vec4 flipped = floatEquals(u_mode, PASSTHROUGH) * texel.rgba; 41 | 42 | // determine if we should flip the bits or not 43 | float signBitIsSet = floatGreaterThanOrEqual(floor(texel.a), 128.0); 44 | 45 | // for integers just flip the sign bit 46 | flipped.r += floatEquals(u_mode, INTEGER) * texel.r; 47 | flipped.g += floatEquals(u_mode, INTEGER) * texel.g; 48 | flipped.b += floatEquals(u_mode, INTEGER) * texel.b; 49 | flipped.a += floatEquals(u_mode, INTEGER) * floatEquals(signBitIsSet, 0.0) * (texel.a + 128.0) 50 | + floatEquals(u_mode, INTEGER) * floatEquals(signBitIsSet, 1.0) * (texel.a - 128.0); 51 | 52 | // for floats flip only sign bit if the sign bit WAS NOT already set - otherwise flip all of the bits 53 | flipped.r += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * texel.r 54 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * (255.0 - texel.r); 55 | flipped.g += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * texel.g 56 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * (255.0 - texel.g); 57 | flipped.b += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * texel.b 58 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * (255.0 - texel.b); 59 | flipped.a += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * (texel.a + 128.0) 60 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * (255.0 - texel.a); 61 | 62 | // output denormalized bytes 63 | gl_FragColor = flipped.rgba / 255.0; 64 | 65 | // zeroize all out of bounds data 66 | gl_FragColor *= max(floatLessThan(floor(gl_FragCoord.x), u_dataDelimX), 67 | floatLessThan(floor(gl_FragCoord.y), u_dataDelimY)); 68 | gl_FragColor *= floatLessThanOrEqual(floor(gl_FragCoord.y), u_dataDelimY); 69 | } 70 | -------------------------------------------------------------------------------- /src/shaders/00_transform64.frag: -------------------------------------------------------------------------------- 1 | #ifdef GL_ES 2 | precision mediump float; 3 | precision mediump int; 4 | precision mediump sampler2D; 5 | #endif 6 | 7 | uniform sampler2D u_bytes; 8 | uniform float u_width; 9 | uniform float u_mode; 10 | uniform float u_endianness; 11 | uniform float u_dataDelimX; 12 | uniform float u_dataDelimY; 13 | 14 | const float PASSTHROUGH = 0.0; 15 | const float INTEGER = 1.0; 16 | const float FLOAT = 2.0; 17 | 18 | const float LITTLE_ENDIAN = 0.0; 19 | const float BIG_ENDIAN = 1.0; 20 | 21 | float round(float); 22 | float floatEquals(float, float); 23 | float floatLessThan(float, float); 24 | float floatGreaterThan(float, float); 25 | float floatLessThanOrEqual(float, float); 26 | float floatGreaterThanOrEqual(float, float); 27 | 28 | void main() { 29 | // get current texel 30 | vec2 baseFragCoord = vec2(floor(gl_FragCoord.x) - mod(floor(gl_FragCoord.x), 2.0), floor(gl_FragCoord.y)); 31 | vec4 texelOne = texture2D(u_bytes, vec2(baseFragCoord.xy + vec2(0.5, 0.5)) / u_width); 32 | vec4 texelTwo = texture2D(u_bytes, vec2(baseFragCoord.xy + vec2(1.5, 0.5)) / u_width); 33 | 34 | // reorder if endianness if not little endian 35 | vec4 reorderedOne = floatEquals(u_endianness, LITTLE_ENDIAN) * texelOne.rgba 36 | + floatEquals(u_endianness, BIG_ENDIAN) * texelTwo.abgr; 37 | vec4 reorderedTwo = floatEquals(u_endianness, LITTLE_ENDIAN) * texelTwo.rgba 38 | + floatEquals(u_endianness, BIG_ENDIAN) * texelOne.abgr; 39 | 40 | // denormalize texel data 41 | texelOne = 255.0 * reorderedOne; 42 | texelTwo = 255.0 * reorderedTwo; 43 | 44 | // initialize flipped texel 45 | vec4 flippedOne = floatEquals(u_mode, PASSTHROUGH) * texelOne.rgba; 46 | vec4 flippedTwo = floatEquals(u_mode, PASSTHROUGH) * texelTwo.rgba; 47 | 48 | // determine if we should flip the bits or not 49 | float signBitIsSet = floatGreaterThanOrEqual(floor(texelTwo.a), 128.0); 50 | 51 | // for integers just flip the sign bit 52 | flippedOne.r += floatEquals(u_mode, INTEGER) * texelOne.r; 53 | flippedOne.g += floatEquals(u_mode, INTEGER) * texelOne.g; 54 | flippedOne.b += floatEquals(u_mode, INTEGER) * texelOne.b; 55 | flippedOne.a += floatEquals(u_mode, INTEGER) * texelOne.a; 56 | flippedTwo.r += floatEquals(u_mode, INTEGER) * texelTwo.r; 57 | flippedTwo.g += floatEquals(u_mode, INTEGER) * texelTwo.g; 58 | flippedTwo.b += floatEquals(u_mode, INTEGER) * texelTwo.b; 59 | flippedTwo.a += floatEquals(u_mode, INTEGER) * floatEquals(signBitIsSet, 0.0) * (texelTwo.a + 128.0) 60 | + floatEquals(u_mode, INTEGER) * floatEquals(signBitIsSet, 1.0) * (texelTwo.a - 128.0); 61 | 62 | // for floats flip only sign bit if the sign bit WAS NOT not already set - otherwise flip all of the bits 63 | flippedOne.r += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * texelOne.r 64 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * (255.0 - texelOne.r); 65 | flippedOne.g += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * texelOne.g 66 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * (255.0 - texelOne.g); 67 | flippedOne.b += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * texelOne.b 68 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * (255.0 - texelOne.b); 69 | flippedOne.a += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * texelOne.a 70 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * (255.0 - texelOne.a); 71 | flippedTwo.r += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * texelTwo.r 72 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * (255.0 - texelTwo.r); 73 | flippedTwo.g += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * texelTwo.g 74 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * (255.0 - texelTwo.g); 75 | flippedTwo.b += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * texelTwo.b 76 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * (255.0 - texelTwo.b); 77 | flippedTwo.a += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * (texelTwo.a + 128.0) 78 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * (255.0 - texelTwo.a); 79 | 80 | // output denormalized bytes 81 | gl_FragColor = floatEquals(floor(mod(floor(gl_FragCoord.x), 2.0)), 0.0) * (flippedOne.rgba / 255.0) 82 | + floatEquals(floor(mod(floor(gl_FragCoord.x), 2.0)), 1.0) * (flippedTwo.rgba / 255.0); 83 | 84 | // zeroize all out of bounds data 85 | gl_FragColor *= max(floatLessThan(floor(gl_FragCoord.x), u_dataDelimX), 86 | floatLessThan(floor(gl_FragCoord.y), u_dataDelimY)); 87 | gl_FragColor *= floatLessThanOrEqual(floor(gl_FragCoord.y), u_dataDelimY); 88 | } 89 | -------------------------------------------------------------------------------- /src/shaders/01_sort32.frag: -------------------------------------------------------------------------------- 1 | #ifdef GL_ES 2 | precision mediump float; 3 | precision mediump int; 4 | precision mediump sampler2D; 5 | #endif 6 | 7 | uniform sampler2D u_bytes; 8 | uniform float u_width; 9 | uniform float u_blockSizeX; 10 | uniform float u_blockSizeY; 11 | uniform float u_regionSizeX; 12 | uniform float u_regionSizeY; 13 | 14 | float round(float); 15 | float floatEquals(float, float); 16 | float floatLessThan(float, float); 17 | float floatGreaterThan(float, float); 18 | float floatLessThanOrEqual(float, float); 19 | float floatGreaterThanOrEqual(float, float); 20 | 21 | void main() { 22 | // calculate starting coordinates for this block 23 | vec2 blockStartCoord = vec2(gl_FragCoord.x - floor(mod(floor(gl_FragCoord.x), u_blockSizeX)), 24 | gl_FragCoord.y - floor(mod(floor(gl_FragCoord.y), u_blockSizeY))); 25 | vec2 blockOffset = vec2(floatEquals(u_blockSizeY, 1.0) * u_blockSizeX, floatGreaterThan(u_blockSizeY, 1.0) * u_blockSizeY); 26 | vec2 halfBlockOffset = vec2(floatEquals(blockOffset.y, 0.0) * blockOffset.x / 2.0 + 27 | floatEquals(blockOffset.y, 1.0) * floatEquals(blockOffset.x, 0.0) * u_width / 2.0, 28 | floor(blockOffset.y / 2.0)); 29 | vec2 blockMiddleCoord = vec2(blockStartCoord.xy) + halfBlockOffset; 30 | 31 | // calculate starting coordinates for each sorting region 32 | vec2 ascendingStartCoord = vec2(gl_FragCoord.x - floor(mod(floor(gl_FragCoord.x), u_regionSizeX)), 33 | gl_FragCoord.y - floor(mod(floor(gl_FragCoord.y), u_regionSizeY))); 34 | vec2 regionOffset = vec2(floatEquals(u_regionSizeY, 1.0) * u_regionSizeX, floatGreaterThan(u_regionSizeY, 1.0) * u_regionSizeY); 35 | vec2 halfRegionOffset = vec2(floatEquals(regionOffset.y, 0.0) * regionOffset.x / 2.0 + 36 | floatEquals(regionOffset.y, 1.0) * floatEquals(regionOffset.x, 0.0) * u_width / 2.0, 37 | floor(regionOffset.y / 2.0)); 38 | vec2 descendingStartCoord = vec2(ascendingStartCoord.xy) + halfRegionOffset; 39 | 40 | // get booleans for determining relative position and sorting order 41 | float ascendingGroupBool = floatLessThan(floor(gl_FragCoord.y), floor(descendingStartCoord.y)); 42 | ascendingGroupBool += floatEquals(floor(gl_FragCoord.y), floor(descendingStartCoord.y)) * 43 | floatLessThan(floor(gl_FragCoord.x), floor(descendingStartCoord.x)); 44 | float firstTexelBool = floatLessThan(floor(gl_FragCoord.y), floor(blockMiddleCoord.y)); 45 | firstTexelBool += floatEquals(floor(gl_FragCoord.y), floor(blockMiddleCoord.y)) * 46 | floatLessThan(floor(gl_FragCoord.x), floor(blockMiddleCoord.x)); 47 | 48 | // get current data 49 | vec4 localData = texture2D(u_bytes, vec2(gl_FragCoord.xy) / u_width); 50 | 51 | // get peer data 52 | vec2 peerFragCoord = floatEquals(firstTexelBool, 1.0) * (gl_FragCoord.xy + halfBlockOffset) 53 | + floatEquals(firstTexelBool, 0.0) * (gl_FragCoord.xy - halfBlockOffset); 54 | vec4 peerData = texture2D(u_bytes, vec2(peerFragCoord.xy) / u_width); 55 | 56 | // create alpha and bravo texels where alpha is expected to be less than bravo 57 | vec4 alphaData = floatEquals(firstTexelBool, 1.0) * floatEquals(ascendingGroupBool, 1.0) * localData.rgba 58 | + floatEquals(firstTexelBool, 1.0) * floatEquals(ascendingGroupBool, 0.0) * peerData.rgba 59 | + floatEquals(firstTexelBool, 0.0) * floatEquals(ascendingGroupBool, 1.0) * peerData.rgba 60 | + floatEquals(firstTexelBool, 0.0) * floatEquals(ascendingGroupBool, 0.0) * localData.rgba; 61 | vec4 bravoData = floatEquals(firstTexelBool, 1.0) * floatEquals(ascendingGroupBool, 1.0) * peerData.rgba 62 | + floatEquals(firstTexelBool, 1.0) * floatEquals(ascendingGroupBool, 0.0) * localData.rgba 63 | + floatEquals(firstTexelBool, 0.0) * floatEquals(ascendingGroupBool, 1.0) * localData.rgba 64 | + floatEquals(firstTexelBool, 0.0) * floatEquals(ascendingGroupBool, 0.0) * peerData.rgba; 65 | 66 | // denormalize data 67 | alphaData *= 255.0; 68 | bravoData *= 255.0; 69 | 70 | // initializing booleans to false 71 | float swapBool = 0.0; 72 | float notSwapBool = 0.0; 73 | 74 | // compare each byte in order to determine if swap is necessary 75 | swapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatGreaterThan(round(alphaData.a), round(bravoData.a)); 76 | notSwapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatLessThan(round(alphaData.a), round(bravoData.a)); 77 | swapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatGreaterThan(round(alphaData.b), round(bravoData.b)); 78 | notSwapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatLessThan(round(alphaData.b), round(bravoData.b)); 79 | swapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatGreaterThan(round(alphaData.g), round(bravoData.g)); 80 | notSwapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatLessThan(round(alphaData.g), round(bravoData.g)); 81 | swapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatGreaterThan(round(alphaData.r), round(bravoData.r)); 82 | notSwapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatLessThan(round(alphaData.r), round(bravoData.r)); 83 | 84 | // handle edge case where bytes are identical and thus both booleans are false 85 | notSwapBool += floatEquals(notSwapBool, 0.0) * floatEquals(swapBool, 0.0); 86 | 87 | // use booleans to render the correct texel 88 | gl_FragColor = floatEquals(notSwapBool, 1.0) * localData.rgba 89 | + floatEquals(notSwapBool, 0.0) * peerData.rgba; 90 | } 91 | -------------------------------------------------------------------------------- /src/shaders/01_sort64.frag: -------------------------------------------------------------------------------- 1 | #ifdef GL_ES 2 | precision mediump float; 3 | precision mediump int; 4 | precision mediump sampler2D; 5 | #endif 6 | 7 | uniform sampler2D u_bytes; 8 | uniform float u_width; 9 | uniform float u_blockSizeX; 10 | uniform float u_blockSizeY; 11 | uniform float u_regionSizeX; 12 | uniform float u_regionSizeY; 13 | 14 | float round(float); 15 | float floatEquals(float, float); 16 | float floatLessThan(float, float); 17 | float floatGreaterThan(float, float); 18 | float floatLessThanOrEqual(float, float); 19 | float floatGreaterThanOrEqual(float, float); 20 | 21 | void main() { 22 | // double the blockSize area 23 | float dblBlockSizeY = u_blockSizeY + u_blockSizeY * floatEquals(u_blockSizeX, u_width); 24 | float dblBlockSizeX = u_blockSizeX + u_blockSizeX * floatLessThan(u_blockSizeX, u_width); 25 | 26 | // calculate starting coordinates for this block 27 | vec2 blockStartCoord = vec2(gl_FragCoord.x - floor(mod(floor(gl_FragCoord.x), dblBlockSizeX)), 28 | gl_FragCoord.y - floor(mod(floor(gl_FragCoord.y), dblBlockSizeY))); 29 | vec2 blockOffset = vec2(floatEquals(dblBlockSizeY, 1.0) * dblBlockSizeX, floatGreaterThan(dblBlockSizeY, 1.0) * dblBlockSizeY); 30 | vec2 halfBlockOffset = vec2(floatEquals(blockOffset.y, 0.0) * blockOffset.x / 2.0 + 31 | floatEquals(blockOffset.y, 1.0) * floatEquals(blockOffset.x, 0.0) * u_width / 2.0, 32 | floor(blockOffset.y / 2.0)); 33 | vec2 blockMiddleCoord = vec2(blockStartCoord.xy) + halfBlockOffset; 34 | 35 | // double the regionSize area 36 | float dblRegionSizeY = u_regionSizeY + u_regionSizeY * floatEquals(u_regionSizeX, u_width); 37 | float dblRegionSizeX = u_regionSizeX + u_regionSizeX * floatLessThan(u_regionSizeX, u_width); 38 | 39 | // calculate starting coordinates for each sorting region 40 | vec2 ascendingStartCoord = vec2(gl_FragCoord.x - floor(mod(floor(gl_FragCoord.x), dblRegionSizeX)), 41 | gl_FragCoord.y - floor(mod(floor(gl_FragCoord.y), dblRegionSizeY))); 42 | vec2 regionOffset = vec2(floatEquals(dblRegionSizeY, 1.0) * dblRegionSizeX, floatGreaterThan(dblRegionSizeY, 1.0) * dblRegionSizeY); 43 | vec2 halfRegionOffset = vec2(floatEquals(regionOffset.y, 0.0) * regionOffset.x / 2.0 + 44 | floatEquals(regionOffset.y, 1.0) * floatEquals(regionOffset.x, 0.0) * u_width / 2.0, 45 | floor(regionOffset.y / 2.0)); 46 | vec2 descendingStartCoord = vec2(ascendingStartCoord.xy) + halfRegionOffset; 47 | 48 | // get booleans for determining relative position and sorting order 49 | float ascendingGroupBool = floatLessThan(floor(gl_FragCoord.y), floor(descendingStartCoord.y)); 50 | ascendingGroupBool += floatEquals(floor(gl_FragCoord.y), floor(descendingStartCoord.y)) * 51 | floatLessThan(floor(gl_FragCoord.x), floor(descendingStartCoord.x)); 52 | float firstTexelBool = floatLessThan(floor(gl_FragCoord.y), floor(blockMiddleCoord.y)); 53 | firstTexelBool += floatEquals(floor(gl_FragCoord.y), floor(blockMiddleCoord.y)) * 54 | floatLessThan(floor(gl_FragCoord.x), floor(blockMiddleCoord.x)); 55 | 56 | // get current data 57 | vec2 localDataCoord = vec2(floor(gl_FragCoord.x) - mod(floor(gl_FragCoord.x), 2.0), gl_FragCoord.y); 58 | vec4 localDataOne = texture2D(u_bytes, vec2(localDataCoord.xy + vec2(0.5, 0.0)) / u_width); 59 | vec4 localDataTwo = texture2D(u_bytes, vec2(localDataCoord.xy + vec2(1.5, 0.0)) / u_width); 60 | 61 | // get peer data 62 | vec2 peerFragCoord = floatEquals(firstTexelBool, 1.0) * (localDataCoord.xy + halfBlockOffset) 63 | + floatEquals(firstTexelBool, 0.0) * (localDataCoord.xy - halfBlockOffset); 64 | vec4 peerDataOne = texture2D(u_bytes, vec2(peerFragCoord.xy + vec2(0.5, 0.0)) / u_width); 65 | vec4 peerDataTwo = texture2D(u_bytes, vec2(peerFragCoord.xy + vec2(1.5, 0.0)) / u_width); 66 | 67 | // create alpha and bravo texels where alpha is expected to be less than bravo 68 | vec4 alphaDataOne = floatEquals(firstTexelBool, 1.0) * floatEquals(ascendingGroupBool, 1.0) * localDataOne.rgba 69 | + floatEquals(firstTexelBool, 1.0) * floatEquals(ascendingGroupBool, 0.0) * peerDataOne.rgba 70 | + floatEquals(firstTexelBool, 0.0) * floatEquals(ascendingGroupBool, 1.0) * peerDataOne.rgba 71 | + floatEquals(firstTexelBool, 0.0) * floatEquals(ascendingGroupBool, 0.0) * localDataOne.rgba; 72 | vec4 alphaDataTwo = floatEquals(firstTexelBool, 1.0) * floatEquals(ascendingGroupBool, 1.0) * localDataTwo.rgba 73 | + floatEquals(firstTexelBool, 1.0) * floatEquals(ascendingGroupBool, 0.0) * peerDataTwo.rgba 74 | + floatEquals(firstTexelBool, 0.0) * floatEquals(ascendingGroupBool, 1.0) * peerDataTwo.rgba 75 | + floatEquals(firstTexelBool, 0.0) * floatEquals(ascendingGroupBool, 0.0) * localDataTwo.rgba; 76 | vec4 bravoDataOne = floatEquals(firstTexelBool, 1.0) * floatEquals(ascendingGroupBool, 1.0) * peerDataOne.rgba 77 | + floatEquals(firstTexelBool, 1.0) * floatEquals(ascendingGroupBool, 0.0) * localDataOne.rgba 78 | + floatEquals(firstTexelBool, 0.0) * floatEquals(ascendingGroupBool, 1.0) * localDataOne.rgba 79 | + floatEquals(firstTexelBool, 0.0) * floatEquals(ascendingGroupBool, 0.0) * peerDataOne.rgba; 80 | vec4 bravoDataTwo = floatEquals(firstTexelBool, 1.0) * floatEquals(ascendingGroupBool, 1.0) * peerDataTwo.rgba 81 | + floatEquals(firstTexelBool, 1.0) * floatEquals(ascendingGroupBool, 0.0) * localDataTwo.rgba 82 | + floatEquals(firstTexelBool, 0.0) * floatEquals(ascendingGroupBool, 1.0) * localDataTwo.rgba 83 | + floatEquals(firstTexelBool, 0.0) * floatEquals(ascendingGroupBool, 0.0) * peerDataTwo.rgba; 84 | 85 | // denormalize data 86 | alphaDataOne *= 255.0; 87 | alphaDataTwo *= 255.0; 88 | bravoDataOne *= 255.0; 89 | bravoDataTwo *= 255.0; 90 | 91 | // initializing booleans to false 92 | float swapBool = 0.0; 93 | float notSwapBool = 0.0; 94 | 95 | // compare each byte in order to determine if swap is necessary 96 | swapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatGreaterThan(round(alphaDataTwo.a), round(bravoDataTwo.a)); 97 | notSwapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatLessThan(round(alphaDataTwo.a), round(bravoDataTwo.a)); 98 | swapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatGreaterThan(round(alphaDataTwo.b), round(bravoDataTwo.b)); 99 | notSwapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatLessThan(round(alphaDataTwo.b), round(bravoDataTwo.b)); 100 | swapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatGreaterThan(round(alphaDataTwo.g), round(bravoDataTwo.g)); 101 | notSwapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatLessThan(round(alphaDataTwo.g), round(bravoDataTwo.g)); 102 | swapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatGreaterThan(round(alphaDataTwo.r), round(bravoDataTwo.r)); 103 | notSwapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatLessThan(round(alphaDataTwo.r), round(bravoDataTwo.r)); 104 | swapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatGreaterThan(round(alphaDataOne.a), round(bravoDataOne.a)); 105 | notSwapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatLessThan(round(alphaDataOne.a), round(bravoDataOne.a)); 106 | swapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatGreaterThan(round(alphaDataOne.b), round(bravoDataOne.b)); 107 | notSwapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatLessThan(round(alphaDataOne.b), round(bravoDataOne.b)); 108 | swapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatGreaterThan(round(alphaDataOne.g), round(bravoDataOne.g)); 109 | notSwapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatLessThan(round(alphaDataOne.g), round(bravoDataOne.g)); 110 | swapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatGreaterThan(round(alphaDataOne.r), round(bravoDataOne.r)); 111 | notSwapBool += floatEquals(swapBool, 0.0) * floatEquals(notSwapBool, 0.0) * floatLessThan(round(alphaDataOne.r), round(bravoDataOne.r)); 112 | 113 | // handle edge case where bytes are identical and thus both booleans are false 114 | notSwapBool += floatEquals(notSwapBool, 0.0) * floatEquals(swapBool, 0.0); 115 | 116 | // use booleans to render the correct texel 117 | gl_FragColor = floatEquals(notSwapBool, 1.0) * floatEquals(mod(floor(gl_FragCoord.x), 2.0), 0.0) * localDataOne.rgba 118 | + floatEquals(notSwapBool, 1.0) * floatEquals(mod(floor(gl_FragCoord.x), 2.0), 1.0) * localDataTwo.rgba 119 | + floatEquals(notSwapBool, 0.0) * floatEquals(mod(floor(gl_FragCoord.x), 2.0), 0.0) * peerDataOne.rgba 120 | + floatEquals(notSwapBool, 0.0) * floatEquals(mod(floor(gl_FragCoord.x), 2.0), 1.0) * peerDataTwo.rgba; 121 | } 122 | -------------------------------------------------------------------------------- /src/shaders/02_untransform32.frag: -------------------------------------------------------------------------------- 1 | #ifdef GL_ES 2 | precision mediump float; 3 | precision mediump int; 4 | precision mediump sampler2D; 5 | #endif 6 | 7 | uniform sampler2D u_bytes; 8 | uniform float u_width; 9 | uniform float u_mode; 10 | uniform float u_endianness; 11 | 12 | const float PASSTHROUGH = 0.0; 13 | const float INTEGER = 1.0; 14 | const float FLOAT = 2.0; 15 | 16 | const float LITTLE_ENDIAN = 0.0; 17 | const float BIG_ENDIAN = 1.0; 18 | 19 | float round(float); 20 | float floatEquals(float, float); 21 | float floatLessThan(float, float); 22 | float floatGreaterThan(float, float); 23 | float floatLessThanOrEqual(float, float); 24 | float floatGreaterThanOrEqual(float, float); 25 | 26 | void main() { 27 | // get current texel 28 | vec4 texel = texture2D(u_bytes, vec2(gl_FragCoord.xy) / u_width); 29 | 30 | // reorder if endianness if not little endian 31 | vec4 reordered = floatEquals(u_endianness, LITTLE_ENDIAN) * texel.rgba 32 | + floatEquals(u_endianness, BIG_ENDIAN) * texel.abgr; 33 | 34 | // denormalize texel data 35 | texel = 255.0 * reordered; 36 | 37 | // initialize flipped texel 38 | vec4 flipped = floatEquals(u_mode, PASSTHROUGH) * texel.rgba; 39 | 40 | // determine if we should flip the bits or not 41 | float signBitIsSet = floatGreaterThanOrEqual(floor(texel.a), 128.0); 42 | 43 | // for integers just flip the sign bit 44 | flipped.r += floatEquals(u_mode, INTEGER) * texel.r; 45 | flipped.g += floatEquals(u_mode, INTEGER) * texel.g; 46 | flipped.b += floatEquals(u_mode, INTEGER) * texel.b; 47 | flipped.a += floatEquals(u_mode, INTEGER) * floatEquals(signBitIsSet, 0.0) * (texel.a + 128.0) 48 | + floatEquals(u_mode, INTEGER) * floatEquals(signBitIsSet, 1.0) * (texel.a - 128.0); 49 | 50 | // NOTE: to untransform floats we must invert the logic seen in `transform.frag` 51 | // for floats flip only sign bit if the sign bit WAS already set - otherwise flip all of the bits 52 | flipped.r += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * texel.r 53 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * (255.0 - texel.r); 54 | flipped.g += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * texel.g 55 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * (255.0 - texel.g); 56 | flipped.b += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * texel.b 57 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * (255.0 - texel.b); 58 | flipped.a += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * (texel.a - 128.0) 59 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * (255.0 - texel.a); 60 | 61 | // output denormalized bytes 62 | gl_FragColor = flipped.rgba / 255.0; 63 | } 64 | -------------------------------------------------------------------------------- /src/shaders/02_untransform64.frag: -------------------------------------------------------------------------------- 1 | #ifdef GL_ES 2 | precision mediump float; 3 | precision mediump int; 4 | precision mediump sampler2D; 5 | #endif 6 | 7 | uniform sampler2D u_bytes; 8 | uniform float u_width; 9 | uniform float u_mode; 10 | uniform float u_endianness; 11 | 12 | const float PASSTHROUGH = 0.0; 13 | const float INTEGER = 1.0; 14 | const float FLOAT = 2.0; 15 | 16 | const float LITTLE_ENDIAN = 0.0; 17 | const float BIG_ENDIAN = 1.0; 18 | 19 | float round(float); 20 | float floatEquals(float, float); 21 | float floatLessThan(float, float); 22 | float floatGreaterThan(float, float); 23 | float floatLessThanOrEqual(float, float); 24 | float floatGreaterThanOrEqual(float, float); 25 | 26 | void main() { 27 | // get current texel 28 | vec2 baseFragCoord = vec2(floor(gl_FragCoord.x) - mod(floor(gl_FragCoord.x), 2.0), floor(gl_FragCoord.y)); 29 | vec4 texelOne = texture2D(u_bytes, vec2(baseFragCoord.xy + vec2(0.5, 0.5)) / u_width); 30 | vec4 texelTwo = texture2D(u_bytes, vec2(baseFragCoord.xy + vec2(1.5, 0.5)) / u_width); 31 | 32 | // reorder if endianness if not little endian 33 | vec4 reorderedOne = floatEquals(u_endianness, LITTLE_ENDIAN) * texelOne.rgba 34 | + floatEquals(u_endianness, BIG_ENDIAN) * texelTwo.abgr; 35 | vec4 reorderedTwo = floatEquals(u_endianness, LITTLE_ENDIAN) * texelTwo.rgba 36 | + floatEquals(u_endianness, BIG_ENDIAN) * texelOne.abgr; 37 | 38 | // denormalize texel data 39 | texelOne = 255.0 * reorderedOne; 40 | texelTwo = 255.0 * reorderedTwo; 41 | 42 | // initialize flipped texel 43 | vec4 flippedOne = floatEquals(u_mode, PASSTHROUGH) * texelOne.rgba; 44 | vec4 flippedTwo = floatEquals(u_mode, PASSTHROUGH) * texelTwo.rgba; 45 | 46 | // determine if we should flip the bits or not 47 | float signBitIsSet = floatGreaterThanOrEqual(floor(texelTwo.a), 128.0); 48 | 49 | // for integers just flip the sign bit 50 | flippedOne.r += floatEquals(u_mode, INTEGER) * texelOne.r; 51 | flippedOne.g += floatEquals(u_mode, INTEGER) * texelOne.g; 52 | flippedOne.b += floatEquals(u_mode, INTEGER) * texelOne.b; 53 | flippedOne.a += floatEquals(u_mode, INTEGER) * texelOne.a; 54 | flippedTwo.r += floatEquals(u_mode, INTEGER) * texelTwo.r; 55 | flippedTwo.g += floatEquals(u_mode, INTEGER) * texelTwo.g; 56 | flippedTwo.b += floatEquals(u_mode, INTEGER) * texelTwo.b; 57 | flippedTwo.a += floatEquals(u_mode, INTEGER) * floatEquals(signBitIsSet, 0.0) * (texelTwo.a + 128.0) 58 | + floatEquals(u_mode, INTEGER) * floatEquals(signBitIsSet, 1.0) * (texelTwo.a - 128.0); 59 | 60 | // NOTE: to untransform floats we must invert the logic seen in `transform.frag` 61 | // for floats flip only sign bit if the sign bit WAS already set - otherwise flip all of the bits 62 | flippedOne.r += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * texelOne.r 63 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * (255.0 - texelOne.r); 64 | flippedOne.g += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * texelOne.g 65 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * (255.0 - texelOne.g); 66 | flippedOne.b += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * texelOne.b 67 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * (255.0 - texelOne.b); 68 | flippedOne.a += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * texelOne.a 69 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * (255.0 - texelOne.a); 70 | flippedTwo.r += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * texelTwo.r 71 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * (255.0 - texelTwo.r); 72 | flippedTwo.g += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * texelTwo.g 73 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * (255.0 - texelTwo.g); 74 | flippedTwo.b += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * texelTwo.b 75 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * (255.0 - texelTwo.b); 76 | flippedTwo.a += floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 1.0) * (texelTwo.a - 128.0) 77 | + floatEquals(u_mode, FLOAT) * floatEquals(signBitIsSet, 0.0) * (255.0 - texelTwo.a); 78 | 79 | // output denormalized bytes 80 | gl_FragColor = floatEquals(floor(mod(floor(gl_FragCoord.x), 2.0)), 0.0) * (flippedOne.rgba / 255.0) 81 | + floatEquals(floor(mod(floor(gl_FragCoord.x), 2.0)), 1.0) * (flippedTwo.rgba / 255.0); 82 | } 83 | -------------------------------------------------------------------------------- /src/sort.ts: -------------------------------------------------------------------------------- 1 | import { bitonicSort, bitonicSortAsync } from "./bitonic"; 2 | import { radixSortAsync, radixSortSignedAsync } from "./radix"; 3 | 4 | export function sort( 5 | array: 6 | | Int8Array 7 | | Uint8Array 8 | | Uint8ClampedArray 9 | | Int16Array 10 | | Uint16Array 11 | | Int32Array 12 | | Uint32Array 13 | | Float32Array 14 | | Float64Array 15 | | BigInt64Array 16 | | BigUint64Array 17 | ) { 18 | if (!array.length) return array; 19 | switch (Object.prototype.toString.call(array)) { 20 | case "[object Int8Array]": 21 | return (array as Int8Array).sort((a: number, b: number) => a - b); 22 | case "[object Uint8Array]": 23 | return (array as Uint8Array).sort((a: number, b: number) => a - b); 24 | case "[object Uint8ClampedArray]": 25 | return (array as Uint8ClampedArray).sort((a: number, b: number) => a - b); 26 | case "[object Int16Array]": 27 | return (array as Int16Array).sort((a: number, b: number) => a - b); 28 | case "[object Uint16Array]": 29 | return (array as Uint16Array).sort((a: number, b: number) => a - b); 30 | case "[object Int32Array]": 31 | return bitonicSort(array as Int32Array, "Int32Array"); 32 | case "[object Uint32Array]": 33 | return bitonicSort(array as Uint32Array, "Uint32Array"); 34 | case "[object Float32Array]": 35 | return bitonicSort(array as Float32Array, "Float32Array"); 36 | case "[object Float64Array]": 37 | return bitonicSort(array as Float64Array, "Float64Array"); 38 | case "[object BigInt64Array]": 39 | return bitonicSort(array as BigInt64Array, "BigInt64Array"); 40 | case "[object BigUint64Array]": 41 | return bitonicSort(array as BigUint64Array, "BigUint64Array"); 42 | } 43 | throw new Error(`${Object.prototype.toString.call(array)} is unsupported`); 44 | } 45 | 46 | export function sortAsync( 47 | array: 48 | | Int8Array 49 | | Uint8Array 50 | | Uint8ClampedArray 51 | | Int16Array 52 | | Uint16Array 53 | | Int32Array 54 | | Uint32Array 55 | | Float32Array 56 | | Float64Array 57 | | BigInt64Array 58 | | BigUint64Array 59 | ): Promise< 60 | | Int8Array 61 | | Uint8Array 62 | | Uint8ClampedArray 63 | | Int16Array 64 | | Uint16Array 65 | | Int32Array 66 | | Uint32Array 67 | | Float32Array 68 | | Float64Array 69 | | BigInt64Array 70 | | BigUint64Array 71 | > { 72 | return new Promise((resolve, reject) => { 73 | if (!array.length) return resolve(); 74 | switch (Object.prototype.toString.call(array)) { 75 | case "[object Int8Array]": 76 | return radixSortSignedAsync(new Uint8Array((array as Int8Array).buffer), 256) 77 | .then(() => resolve(array)) 78 | .catch(err => reject(err)); 79 | case "[object Uint8Array]": 80 | return radixSortAsync(new Uint8Array((array as Uint8Array).buffer), 256) 81 | .then(() => resolve(array)) 82 | .catch(err => reject(err)); 83 | case "[object Uint8ClampedArray]": 84 | return radixSortAsync(new Uint8Array((array as Uint8ClampedArray).buffer), 256) 85 | .then(() => resolve(array)) 86 | .catch(err => reject(err)); 87 | case "[object Int16Array]": 88 | return radixSortSignedAsync(new Uint16Array((array as Int16Array).buffer), 65536) 89 | .then(() => resolve(array)) 90 | .catch(err => reject(err)); 91 | case "[object Uint16Array]": 92 | return radixSortAsync(array as Uint16Array, 65536) 93 | .then(() => resolve(array)) 94 | .catch(err => reject(err)); 95 | case "[object Int32Array]": 96 | return bitonicSortAsync(array as Int32Array, "Int32Array") 97 | .then(() => resolve(array)) 98 | .catch(err => reject(err)); 99 | case "[object Uint32Array]": 100 | return bitonicSortAsync(array as Uint32Array, "Uint32Array") 101 | .then(() => resolve(array)) 102 | .catch(err => reject(err)); 103 | case "[object Float32Array]": 104 | return bitonicSortAsync(array as Float32Array, "Float32Array") 105 | .then(() => resolve(array)) 106 | .catch(err => reject(err)); 107 | case "[object Float64Array]": 108 | return bitonicSortAsync(array as Float64Array, "Float64Array") 109 | .then(() => resolve(array)) 110 | .catch(err => reject(err)); 111 | case "[object BigInt64Array]": 112 | return bitonicSortAsync(array as BigInt64Array, "BigInt64Array") 113 | .then(() => resolve(array)) 114 | .catch(err => reject(err)); 115 | case "[object BigUint64Array]": 116 | return bitonicSortAsync(array as BigUint64Array, "BigUint64Array") 117 | .then(() => resolve(array)) 118 | .catch(err => reject(err)); 119 | } 120 | reject(new Error(`${Object.prototype.toString.call(array)} is unsupported`)); 121 | }); 122 | } 123 | -------------------------------------------------------------------------------- /src/tests/bitonic.test.ts: -------------------------------------------------------------------------------- 1 | import { setWebGLContext } from "../index"; 2 | import * as bitonic from "../bitonic"; 3 | import * as gpu from "gpu-compute"; 4 | 5 | beforeAll(() => { 6 | setWebGLContext(require("gl")(1, 1)); 7 | }); 8 | 9 | test("getRenderTarget overflow", () => { 10 | const array = new Float64Array(Math.pow(2, 28)); 11 | try { 12 | bitonic.getRenderTarget(new Uint8Array(array.buffer)); 13 | } catch (err) { 14 | return; 15 | } 16 | throw new Error("did not see an error"); 17 | }); 18 | 19 | test("pullPixels", () => { 20 | const bytes = new Uint8Array(128 * 128 * 4); 21 | const target = new gpu.RenderTarget(128); 22 | bitonic.pullPixels(target, 0, bytes); 23 | }); 24 | 25 | test("pullPixels minus one", () => { 26 | const bytes = new Uint8Array(128 * 128 * 4 - 4); 27 | const target = new gpu.RenderTarget(128); 28 | bitonic.pullPixels(target, 1, bytes); 29 | }); 30 | 31 | test("pullPixelsAsync", async function() { 32 | const bytes = new Uint8Array(128 * 128 * 4); 33 | const target = new gpu.RenderTarget(128); 34 | await bitonic.pullPixelsAsync(target, 0, bytes); 35 | }); 36 | 37 | test("pullPixelsAsync minus one", async function() { 38 | const bytes = new Uint8Array(128 * 128 * 4 - 4); 39 | const target = new gpu.RenderTarget(128); 40 | await bitonic.pullPixelsAsync(target, 1, bytes); 41 | }); 42 | -------------------------------------------------------------------------------- /src/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as index from "../index"; 2 | 3 | beforeAll(() => { 4 | index.setWebGLContext(require("gl")(1, 1)); 5 | }); 6 | 7 | test("testArray", () => { 8 | let n = 123 * 456; 9 | let alpha = Array.from(Array(n), () => (Math.random() - 0.5) * 256); 10 | try { 11 | index.sort(alpha as any); 12 | } catch (err) { 13 | if (err.toString() !== "Error: [object Array] is unsupported") { 14 | throw new Error("something unexpected happened"); 15 | } 16 | } 17 | }); 18 | 19 | test("testArrayAsync", async function() { 20 | let n = 123 * 456; 21 | let alpha = Array.from(Array(n), () => (Math.random() - 0.5) * 256); 22 | try { 23 | await index.sortAsync(alpha as any); 24 | } catch (err) { 25 | if (err.toString() !== "Error: [object Array] is unsupported") { 26 | throw new Error("something unexpected happened"); 27 | } 28 | } 29 | }); 30 | 31 | test("testInt8Empty", () => { 32 | let n = 0; 33 | let alpha = new Int8Array(Array.from(Array(n), () => (Math.random() - 0.5) * 256)); 34 | let bravo = Int8Array.from(alpha).sort((a, b) => a - b); 35 | index.sort(alpha); 36 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 37 | }); 38 | 39 | test("testInt8EmptyAsync", async function() { 40 | let n = 0; 41 | let alpha = new Int8Array(Array.from(Array(n), () => (Math.random() - 0.5) * 256)); 42 | let bravo = Int8Array.from(alpha).sort((a, b) => a - b); 43 | await index.sortAsync(alpha); 44 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 45 | }); 46 | 47 | test("testInt8", () => { 48 | let n = 123 * 456; 49 | let alpha = new Int8Array(Array.from(Array(n), () => (Math.random() - 0.5) * 256)); 50 | let bravo = Int8Array.from(alpha).sort((a, b) => a - b); 51 | index.sort(alpha); 52 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 53 | }); 54 | 55 | test("testInt8Async", async function() { 56 | let n = 123 * 456; 57 | let alpha = new Int8Array(Array.from(Array(n), () => (Math.random() - 0.5) * 256)); 58 | let bravo = Int8Array.from(alpha).sort((a, b) => a - b); 59 | await index.sortAsync(alpha).catch(err => { 60 | throw new Error(err); 61 | }); 62 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 63 | }); 64 | 65 | test("testUint8", () => { 66 | let n = 123 * 456; 67 | let alpha = new Uint8Array(Array.from(Array(n), () => Math.random() * 256)); 68 | let bravo = Uint8Array.from(alpha).sort((a, b) => a - b); 69 | index.sort(alpha); 70 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 71 | }); 72 | 73 | test("testUint8Async", async function() { 74 | let n = 123 * 456; 75 | let alpha = new Uint8Array(Array.from(Array(n), () => Math.random() * 256)); 76 | let bravo = Uint8Array.from(alpha).sort((a, b) => a - b); 77 | await index.sortAsync(alpha); 78 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 79 | }); 80 | 81 | test("testUint8Clamped", () => { 82 | let n = 123 * 456; 83 | let alpha = new Uint8ClampedArray(Array.from(Array(n), () => Math.random() * 256)); 84 | let bravo = Uint8ClampedArray.from(alpha).sort((a, b) => a - b); 85 | index.sort(alpha); 86 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 87 | }); 88 | 89 | test("testUint8ClampedAsync", async function() { 90 | let n = 123 * 456; 91 | let alpha = new Uint8ClampedArray(Array.from(Array(n), () => Math.random() * 256)); 92 | let bravo = Uint8ClampedArray.from(alpha).sort((a, b) => a - b); 93 | await index.sortAsync(alpha); 94 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 95 | }); 96 | 97 | test("testInt16", () => { 98 | let n = 123 * 456; 99 | let alpha = new Int16Array(Array.from(Array(n), () => (Math.random() - 0.5) * 65536)); 100 | let bravo = Int16Array.from(alpha).sort((a, b) => a - b); 101 | index.sort(alpha); 102 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 103 | }); 104 | 105 | test("testInt16Async", async function() { 106 | let n = 123 * 456; 107 | let alpha = new Int16Array(Array.from(Array(n), () => (Math.random() - 0.5) * 65536)); 108 | let bravo = Int16Array.from(alpha).sort((a, b) => a - b); 109 | await index.sortAsync(alpha); 110 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 111 | }); 112 | 113 | test("testUint16", () => { 114 | let n = 123 * 456; 115 | let alpha = new Uint16Array(Array.from(Array(n), () => Math.random() * 65536)); 116 | let bravo = Uint16Array.from(alpha).sort((a, b) => a - b); 117 | index.sort(alpha); 118 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 119 | }); 120 | 121 | test("testUint16Async", async function() { 122 | let n = 123 * 456; 123 | let alpha = new Uint16Array(Array.from(Array(n), () => Math.random() * 65536)); 124 | let bravo = Uint16Array.from(alpha).sort((a, b) => a - b); 125 | await index.sortAsync(alpha); 126 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 127 | }); 128 | 129 | test("testFloat32", () => { 130 | let n = 12 * 34; 131 | let alpha = new Float32Array(Array.from(Array(n), () => Math.random() - 0.5)); 132 | let bravo = Float32Array.from(alpha).sort((a, b) => a - b); 133 | index.sort(alpha); 134 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 135 | }); 136 | 137 | test("testFloat32Async", async function() { 138 | let n = 12 * 34; 139 | let alpha = new Float32Array(Array.from(Array(n), () => Math.random() - 0.5)); 140 | let bravo = Float32Array.from(alpha).sort((a, b) => a - b); 141 | await index.sortAsync(alpha); 142 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 143 | }); 144 | 145 | test("testInt32", () => { 146 | let n = 12 * 34; 147 | let alpha = new Int32Array(Array.from(Array(n), () => (Math.random() - 0.5) * 1e6)); 148 | let bravo = Int32Array.from(alpha).sort((a, b) => a - b); 149 | index.sort(alpha); 150 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 151 | }); 152 | 153 | test("testInt32Async", async function() { 154 | let n = 12 * 34; 155 | let alpha = new Int32Array(Array.from(Array(n), () => (Math.random() - 0.5) * 1e6)); 156 | let bravo = Int32Array.from(alpha).sort((a, b) => a - b); 157 | await index.sortAsync(alpha); 158 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 159 | }); 160 | 161 | test("testUint32", () => { 162 | let n = 12 * 34; 163 | let alpha = new Uint32Array(Array.from(Array(n), () => Math.random() * 1e6)); 164 | let bravo = Uint32Array.from(alpha).sort((a, b) => a - b); 165 | index.sort(alpha); 166 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 167 | }); 168 | 169 | test("testUint32Async", async function() { 170 | let n = 12 * 34; 171 | let alpha = new Uint32Array(Array.from(Array(n), () => Math.random() * 1e6)); 172 | let bravo = Uint32Array.from(alpha).sort((a, b) => a - b); 173 | await index.sortAsync(alpha); 174 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 175 | }); 176 | 177 | test("testFloat64", () => { 178 | let n = 12 * 34; 179 | let alpha = new Float64Array(Array.from(Array(n), () => Math.random() - 0.5)); 180 | let bravo = Float64Array.from(alpha).sort((a, b) => a - b); 181 | index.sort(alpha); 182 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 183 | }); 184 | 185 | test("testFloat64Async", async function() { 186 | let n = 12 * 34; 187 | let alpha = new Float64Array(Array.from(Array(n), () => Math.random() - 0.5)); 188 | let bravo = Float64Array.from(alpha).sort((a, b) => a - b); 189 | await index.sortAsync(alpha); 190 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 191 | }); 192 | 193 | test("testBigInt64Array", () => { 194 | let n = 12 * 34; 195 | let alpha = new BigInt64Array(Array.from(Array(n), () => BigInt(Math.floor((Math.random() - 0.5) * 1e6)))); 196 | let bravo = BigInt64Array.from(alpha).sort((a: bigint, b: bigint) => Number(a - b)); 197 | index.sort(alpha); 198 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 199 | }); 200 | 201 | test("testBigInt64ArrayAsync", async function() { 202 | let n = 12 * 34; 203 | let alpha = new BigInt64Array(Array.from(Array(n), () => BigInt(Math.floor((Math.random() - 0.5) * 1e6)))); 204 | let bravo = BigInt64Array.from(alpha).sort((a: bigint, b: bigint) => Number(a - b)); 205 | await index.sortAsync(alpha); 206 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 207 | }); 208 | 209 | test("testBigUint64Array", () => { 210 | let n = 12 * 34; 211 | let alpha = new BigUint64Array(Array.from(Array(n), () => BigInt(Math.floor(Math.random() * 1e6)))); 212 | let bravo = BigUint64Array.from(alpha).sort((a: bigint, b: bigint) => Number(a - b)); 213 | index.sort(alpha); 214 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 215 | }); 216 | 217 | test("testBigUint64ArrayAsync", async function() { 218 | let n = 12 * 34; 219 | let alpha = new BigUint64Array(Array.from(Array(n), () => BigInt(Math.floor(Math.random() * 1e6)))); 220 | let bravo = BigUint64Array.from(alpha).sort((a: bigint, b: bigint) => Number(a - b)); 221 | await index.sortAsync(alpha); 222 | if (alpha.toString() !== bravo.toString()) throw new Error("did not sort properly"); 223 | }); 224 | -------------------------------------------------------------------------------- /src/tests/initialize.test.ts: -------------------------------------------------------------------------------- 1 | import { setWebGLContext } from "../index"; 2 | import * as initialize from "../initialize"; 3 | 4 | beforeAll(() => { 5 | setWebGLContext(require("gl")(1, 1)); 6 | }); 7 | 8 | test("initializeShaders", () => { 9 | initialize.initializeShaders(); 10 | }); 11 | -------------------------------------------------------------------------------- /src/tests/limiter.test.ts: -------------------------------------------------------------------------------- 1 | import { setWebGLContext } from "../index"; 2 | import { Limiter } from "../limiter"; 3 | 4 | beforeAll(() => { 5 | setWebGLContext(require("gl")(1, 1)); 6 | }); 7 | 8 | test("Limiter width 512", done => { 9 | const limiter = new Limiter(512); 10 | limiter.addWork(() => {}); 11 | limiter.onceFinished(() => (limiter.stop(), done())); 12 | }); 13 | 14 | test("Limiter width 1024", done => { 15 | const limiter = new Limiter(1024); 16 | limiter.addWork(() => {}); 17 | limiter.onceFinished(() => (limiter.stop(), done())); 18 | }); 19 | 20 | test("Limiter width 2048", done => { 21 | const limiter = new Limiter(2048); 22 | limiter.addWork(() => {}); 23 | limiter.onceFinished(() => (limiter.stop(), done())); 24 | }); 25 | 26 | test("Limiter width 4096", done => { 27 | const limiter = new Limiter(4096); 28 | limiter.addWork(() => {}); 29 | limiter.onceFinished(() => (limiter.stop(), done())); 30 | }); 31 | 32 | test("Limiter stopped", () => { 33 | const limiter = new Limiter(4096); 34 | limiter.stop(); 35 | try { 36 | limiter.addWork(() => {}); 37 | } catch (err) { 38 | if (err.toString() === "Error: limiter has been stopped") return; 39 | } 40 | throw new Error("something unexpected happened"); 41 | }); 42 | -------------------------------------------------------------------------------- /src/uniforms.ts: -------------------------------------------------------------------------------- 1 | import * as gpu from "gpu-compute"; 2 | import { Uniforms } from "gpu-compute/lib/renderTarget"; 3 | import { getBitonicParameters, bitonicParameters, TRANSFORM_MODE } from "./parameters"; 4 | 5 | export const isBigEndian = new Uint8Array(new Uint32Array([0x12345678]).buffer)[0] === 0x12; 6 | export const isLittleEndian = new Uint8Array(new Uint32Array([0x12345678]).buffer)[0] === 0x78; 7 | 8 | export class BitonicUniformGenerator { 9 | private readonly target: gpu.RenderTarget; 10 | private readonly parameters: bitonicParameters; 11 | private readonly emptyTexelCount: number; 12 | 13 | constructor(target: gpu.RenderTarget, kind: string, emptyTexelCount: number) { 14 | this.target = target; 15 | this.parameters = getBitonicParameters(kind); 16 | this.emptyTexelCount = emptyTexelCount; 17 | } 18 | 19 | *generate(): IterableIterator { 20 | for (let uniforms of this.generateTransformerUniforms()) 21 | yield { shader: this.parameters.transformShader, uniforms: uniforms }; 22 | for (let uniforms of this.generateSorterUniforms()) 23 | yield { shader: this.parameters.sortShader, uniforms: uniforms }; 24 | for (let uniforms of this.generateUntransformerUniforms()) 25 | yield { shader: this.parameters.untransformShader, uniforms: uniforms }; 26 | } 27 | 28 | private *generateTransformerUniforms(): IterableIterator { 29 | if (!isLittleEndian || this.parameters.mode !== TRANSFORM_MODE.PASSTHROUGH) { 30 | const w = this.target.width; 31 | const x = w * w - this.emptyTexelCount; 32 | yield { 33 | u_bytes: this.target, 34 | u_width: w, 35 | u_mode: this.parameters.mode, 36 | u_endianness: isLittleEndian ? 0 : 1, 37 | u_dataDelimX: Math.floor(x % w), 38 | u_dataDelimY: Math.floor(x / w) 39 | }; 40 | } 41 | } 42 | 43 | private *generateSorterUniforms(): IterableIterator { 44 | const w = this.target.width; 45 | for (let rs = 4; rs <= 2 * w * w; rs *= 2) { 46 | for (let bs = rs / 2; bs > 1; bs /= 2) { 47 | yield { 48 | u_bytes: this.target, 49 | u_width: w, 50 | u_blockSizeX: Math.floor(bs % w) > 0 ? Math.floor(bs % w) : w, 51 | u_blockSizeY: Math.max(Math.floor(bs / w), 1), 52 | u_regionSizeX: Math.floor(rs % w) > 0 ? Math.floor(rs % w) : w, 53 | u_regionSizeY: Math.max(Math.floor(rs / w), 1) 54 | }; 55 | } 56 | } 57 | } 58 | 59 | private *generateUntransformerUniforms(): IterableIterator { 60 | if (!isLittleEndian || this.parameters.mode !== TRANSFORM_MODE.PASSTHROUGH) { 61 | yield { 62 | u_bytes: this.target, 63 | u_width: this.target.width, 64 | u_mode: this.parameters.mode, 65 | u_endianness: isLittleEndian ? 0 : 1 66 | }; 67 | } 68 | } 69 | } 70 | 71 | export interface bitonicTransformerUniforms extends Uniforms { 72 | u_bytes: gpu.RenderTarget; 73 | u_width: number; 74 | u_mode: number; 75 | u_endianness: number; 76 | u_dataDelimX: number; 77 | u_dataDelimY: number; 78 | } 79 | 80 | export interface bitonicSorterUniforms extends Uniforms { 81 | u_bytes: gpu.RenderTarget; 82 | u_width: number; 83 | u_blockSizeX: number; 84 | u_blockSizeY: number; 85 | u_regionSizeX: number; 86 | u_regionSizeY: number; 87 | } 88 | 89 | export interface bitonicUntransformerUniforms extends Uniforms { 90 | u_bytes: gpu.RenderTarget; 91 | u_width: number; 92 | u_mode: number; 93 | u_endianness: number; 94 | } 95 | 96 | export interface bitonicShaderAndUniforms { 97 | shader: gpu.ComputeShader; 98 | uniforms: bitonicTransformerUniforms | bitonicSorterUniforms | bitonicUntransformerUniforms; 99 | } 100 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "sourceMap": true, 5 | "target": "es2015", 6 | "module": "commonjs", 7 | "lib": ["dom", "es2015", "scripthost"], 8 | "declaration": true, 9 | "outDir": "./lib", 10 | "removeComments": true, 11 | "downlevelIteration": true, 12 | "strict": true, 13 | "newLine": "LF", 14 | "esModuleInterop": true, 15 | "resolveJsonModule": true, 16 | "typeRoots": ["node_modules/@types"] 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "src/benchmarks", "src/tests"] 20 | } 21 | -------------------------------------------------------------------------------- /vscode.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "prettier.printWidth": 119, 9 | "prettier.useTabs": true, 10 | "editor.detectIndentation": false, 11 | "editor.insertSpaces": true, 12 | "editor.tabSize": 4, 13 | "editor.formatOnSave": true, 14 | "[javascript]": { "editor.tabSize": 2 }, 15 | "[typescript]": { "editor.tabSize": 2 }, 16 | "[typescriptreact]": { "editor.tabSize": 2 }, 17 | "[json]": { "editor.tabSize": 2 }, 18 | "[jsonc]": { "editor.tabSize": 2 }, 19 | "[html]": { "editor.tabSize": 2 }, 20 | "[css]": { "editor.tabSize": 2 }, 21 | "[frag]": { "editor.tabSize": 4 }, 22 | "[vert]": { "editor.tabSize": 4 }, 23 | "[go]": { "editor.tabSize": 4 }, 24 | "files.eol": "\n", 25 | "jest.runAllTestsFirst": true, 26 | "jest.debugMode": true, 27 | "jest.pathToConfig": "package.json", 28 | "coverage-gutters.showGutterCoverage": false, 29 | "coverage-gutters.showLineCoverage": true, 30 | "coverage-gutters.showRulerCoverage": false, 31 | "coverage-gutters.coverageFileNames": ["coverage/lcov.info"], 32 | "coverage-gutters.highlightdark": "rgba(45, 121, 10, 0.2)" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | const path = require("path"); 4 | 5 | const distConfig = { 6 | name: "dist", 7 | target: "web", 8 | mode: "production", 9 | performance: { hints: false }, 10 | entry: "./lib/index", 11 | output: { 12 | path: path.resolve(__dirname, "dist"), 13 | filename: "dist.bundle.js" 14 | }, 15 | externals: [], 16 | resolve: { 17 | extensions: [".ts", ".js", ".json"] 18 | }, 19 | node: { 20 | fs: "empty" 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.(ts|js)x?$/, 26 | exclude: /node_modules/, 27 | loader: "babel-loader" 28 | } 29 | ] 30 | } 31 | }; 32 | 33 | const benchGenerateConfig = { 34 | name: "bench-generate", 35 | target: "web", 36 | mode: "production", 37 | performance: { hints: false }, 38 | entry: path.resolve(__dirname, "src/benchmarks/generate.bench.ts"), 39 | output: { 40 | path: path.resolve(__dirname, "serve-benchmark/public"), 41 | filename: "generate.bundle.js", 42 | library: "gpuSortGenerate", 43 | libraryTarget: "window" 44 | }, 45 | externals: [], 46 | resolve: { 47 | extensions: [".ts", ".js", ".json"] 48 | }, 49 | node: { 50 | fs: "empty" 51 | }, 52 | module: { 53 | rules: [ 54 | { 55 | test: /\.(ts|js)x?$/, 56 | exclude: /node_modules/, 57 | loader: "babel-loader" 58 | } 59 | ] 60 | } 61 | }; 62 | 63 | module.exports = [distConfig, benchGenerateConfig]; 64 | --------------------------------------------------------------------------------