├── .eslintignore ├── .gitattributes ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .npmignore ├── README.md ├── common ├── config │ └── rush │ │ ├── .npmrc │ │ ├── .npmrc-publish │ │ ├── command-line.json │ │ ├── common-versions.json │ │ ├── experiments.json │ │ ├── pnpm-lock.yaml │ │ └── version-policies.json ├── git-hooks │ └── commit-msg.sample └── scripts │ ├── install-run-rush-pnpm.js │ ├── install-run-rush.js │ ├── install-run-rushx.js │ └── install-run.js ├── package.json ├── packages ├── batch-renderer │ ├── .jsdoc.conf.json │ ├── .npmignore │ ├── CHANGELOG.json │ ├── CHANGELOG.md │ ├── README.md │ ├── api-extractor.json │ ├── package.json │ ├── src │ │ ├── AggregateUniformsBatch.ts │ │ ├── AggregateUniformsBatchFactory.ts │ │ ├── BatchDrawer.ts │ │ ├── BatchGeometryFactory.ts │ │ ├── BatchRenderer.ts │ │ ├── BatchRendererPluginFactory.ts │ │ ├── BatchShaderFactory.ts │ │ ├── StdBatch.ts │ │ ├── StdBatchFactory.ts │ │ ├── index.ts │ │ ├── redirects │ │ │ ├── AttributeRedirect.ts │ │ │ ├── Redirect.ts │ │ │ ├── UniformRedirect.ts │ │ │ └── index.ts │ │ ├── resolve │ │ │ ├── index.ts │ │ │ ├── resolveConstantOrProperty.ts │ │ │ └── resolveFunctionOrProperty.ts │ │ └── utils │ │ │ ├── BufferPool.ts │ │ │ └── resolveProperty.ts │ ├── test │ │ ├── BufferPool.js │ │ └── index.js │ └── tsconfig.json └── diffy │ ├── .eslintrc │ ├── .npmignore │ ├── CHANGELOG.json │ ├── CHANGELOG.md │ ├── README.md │ ├── api-extractor.json │ ├── package.json │ ├── src │ ├── DiffBuffer.ts │ ├── DiffDrawer.ts │ ├── DiffGeometry.ts │ ├── DiffGeometryFactory.ts │ ├── index.ts │ └── utils │ │ ├── BufferInvalidation.ts │ │ ├── BufferInvalidationQueue.ts │ │ └── uploadBuffer.ts │ ├── test │ ├── BufferInvalidationQueue.js │ └── index.js │ └── tsconfig.json ├── rush.json └── tools └── unit-tests ├── index.js ├── package.json └── test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | /lib 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Don't allow people to merge changes to these generated files, because the result 2 | # may be invalid. You need to run "rush update" again. 3 | pnpm-lock.yaml merge=binary 4 | shrinkwrap.yaml merge=binary 5 | npm-shrinkwrap.json merge=binary 6 | yarn.lock merge=binary 7 | 8 | # Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic 9 | # syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor 10 | # may also require a special configuration to allow comments in JSON. 11 | # 12 | # For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088 13 | # 14 | *.json linguist-language=JSON-with-Comments 15 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - uses: pnpm/action-setup@v1.2.1 28 | with: 29 | version: 5.17.2 30 | - run: pnpm install -g @microsoft/rush 31 | - run: rush install 32 | - run: rush rebuild 33 | - uses: GabrielBB/xvfb-action@v1.0 34 | with: 35 | run: rush unit-test 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | example/node_modules 3 | docs 4 | .DS_Store 5 | compile 6 | lib 7 | dist 8 | index.d.ts 9 | 10 | # Logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Runtime data 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # next.js build output 65 | .next 66 | 67 | # OS X temporary files 68 | .DS_Store 69 | 70 | # Rush temporary files 71 | common/deploy/ 72 | common/temp/ 73 | common/autoinstallers/*/.npmrc 74 | **/.rush/temp/ 75 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | example 3 | docs 4 | src 5 | compile 6 | 7 | # Build Tools 8 | .eslintrc.json 9 | .eslintignore 10 | .jsdoc.conf.json 11 | rollup.config.js 12 | tsconfig.json 13 | README.md 14 | api-extractor.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PixiJS Batch Rendering Kit 2 | 3 |

4 | 5 |

6 | 7 | [![Node.js CI](https://github.com/pixijs/batch/actions/workflows/node.js.yml/badge.svg)](https://github.com/pixijs/batch/actions/workflows/node.js.yml) 8 | 9 | This project has been converted into a monorepo. See the [pixi-batch-renderer package](https://github.com/pixijs/pixi-batch-renderer/tree/master/packages/pixi-batch-renderer) for the original documentation. 10 | -------------------------------------------------------------------------------- /common/config/rush/.npmrc: -------------------------------------------------------------------------------- 1 | # Rush uses this file to configure the NPM package registry during installation. It is applicable 2 | # to PNPM, NPM, and Yarn package managers. It is used by operations such as "rush install", 3 | # "rush update", and the "install-run.js" scripts. 4 | # 5 | # NOTE: The "rush publish" command uses .npmrc-publish instead. 6 | # 7 | # Before invoking the package manager, Rush will copy this file to the folder where installation 8 | # is performed. The copied file will omit any config lines that reference environment variables 9 | # that are undefined in that session; this avoids problems that would otherwise result due to 10 | # a missing variable being replaced by an empty string. 11 | # 12 | # * * * SECURITY WARNING * * * 13 | # 14 | # It is NOT recommended to store authentication tokens in a text file on a lab machine, because 15 | # other unrelated processes may be able to read the file. Also, the file may persist indefinitely, 16 | # for example if the machine loses power. A safer practice is to pass the token via an 17 | # environment variable, which can be referenced from .npmrc using ${} expansion. For example: 18 | # 19 | # //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} 20 | # 21 | registry=https://registry.npmjs.org/ 22 | always-auth=false 23 | -------------------------------------------------------------------------------- /common/config/rush/.npmrc-publish: -------------------------------------------------------------------------------- 1 | # This config file is very similar to common/config/rush/.npmrc, except that .npmrc-publish 2 | # is used by the "rush publish" command, as publishing often involves different credentials 3 | # and registries than other operations. 4 | # 5 | # Before invoking the package manager, Rush will copy this file to "common/temp/publish-home/.npmrc" 6 | # and then temporarily map that folder as the "home directory" for the current user account. 7 | # This enables the same settings to apply for each project folder that gets published. The copied file 8 | # will omit any config lines that reference environment variables that are undefined in that session; 9 | # this avoids problems that would otherwise result due to a missing variable being replaced by 10 | # an empty string. 11 | # 12 | # * * * SECURITY WARNING * * * 13 | # 14 | # It is NOT recommended to store authentication tokens in a text file on a lab machine, because 15 | # other unrelated processes may be able to read the file. Also, the file may persist indefinitely, 16 | # for example if the machine loses power. A safer practice is to pass the token via an 17 | # environment variable, which can be referenced from .npmrc using ${} expansion. For example: 18 | # 19 | # //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} 20 | # 21 | -------------------------------------------------------------------------------- /common/config/rush/command-line.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json", 3 | "commands": [ 4 | { 5 | "commandKind": "global", 6 | "name": "unit-test", 7 | "summary": "Unit testing", 8 | "description": "Unit test each project", 9 | "shellCommand": "pnpm test" 10 | }, 11 | { 12 | "commandKind": "bulk", 13 | "name": "build", 14 | "summary": "Build", 15 | "description": "Bundle all flavors & generate typings declaration files", 16 | "enableParallelism": true, 17 | "safeForSimultaneousRushProcesses": false 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /common/config/rush/common-versions.json: -------------------------------------------------------------------------------- 1 | /** 2 | * This configuration file specifies NPM dependency version selections that affect all projects 3 | * in a Rush repo. For full documentation, please see https://rushjs.io 4 | */ 5 | { 6 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json", 7 | 8 | /** 9 | * A table that specifies a "preferred version" for a given NPM package. This feature is typically used 10 | * to hold back an indirect dependency to a specific older version, or to reduce duplication of indirect dependencies. 11 | * 12 | * The "preferredVersions" value can be any SemVer range specifier (e.g. "~1.2.3"). Rush injects these values into 13 | * the "dependencies" field of the top-level common/temp/package.json, which influences how the package manager 14 | * will calculate versions. The specific effect depends on your package manager. Generally it will have no 15 | * effect on an incompatible or already constrained SemVer range. If you are using PNPM, similar effects can be 16 | * achieved using the pnpmfile.js hook. See the Rush documentation for more details. 17 | * 18 | * After modifying this field, it's recommended to run "rush update --full" so that the package manager 19 | * will recalculate all version selections. 20 | */ 21 | "preferredVersions": { 22 | /** 23 | * When someone asks for "^1.0.0" make sure they get "1.2.3" when working in this repo, 24 | * instead of the latest version. 25 | */ 26 | // "some-library": "1.2.3" 27 | }, 28 | 29 | /** 30 | * When set to true, for all projects in the repo, all dependencies will be automatically added as preferredVersions, 31 | * except in cases where different projects specify different version ranges for a given dependency. For older 32 | * package managers, this tended to reduce duplication of indirect dependencies. However, it can sometimes cause 33 | * trouble for indirect dependencies with incompatible peerDependencies ranges. 34 | * 35 | * The default value is true. If you're encountering installation errors related to peer dependencies, 36 | * it's recommended to set this to false. 37 | * 38 | * After modifying this field, it's recommended to run "rush update --full" so that the package manager 39 | * will recalculate all version selections. 40 | */ 41 | // "implicitlyPreferredVersions": false, 42 | 43 | /** 44 | * The "rush check" command can be used to enforce that every project in the repo must specify 45 | * the same SemVer range for a given dependency. However, sometimes exceptions are needed. 46 | * The allowedAlternativeVersions table allows you to list other SemVer ranges that will be 47 | * accepted by "rush check" for a given dependency. 48 | * 49 | * IMPORTANT: THIS TABLE IS FOR *ADDITIONAL* VERSION RANGES THAT ARE ALTERNATIVES TO THE 50 | * USUAL VERSION (WHICH IS INFERRED BY LOOKING AT ALL PROJECTS IN THE REPO). 51 | * This design avoids unnecessary churn in this file. 52 | */ 53 | "allowedAlternativeVersions": { 54 | /** 55 | * For example, allow some projects to use an older TypeScript compiler 56 | * (in addition to whatever "usual" version is being used by other projects in the repo): 57 | */ 58 | // "typescript": [ 59 | // "~2.4.0" 60 | // ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /common/config/rush/experiments.json: -------------------------------------------------------------------------------- 1 | /** 2 | * This configuration file allows repo maintainers to enable and disable experimental 3 | * Rush features. For full documentation, please see https://rushjs.io 4 | */ 5 | { 6 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/experiments.schema.json", 7 | 8 | /** 9 | * Rush 5.14.0 improved incremental builds to ignore spurious changes in the pnpm-lock.json file. 10 | * This optimization is enabled by default. If you encounter a problem where "rush build" is neglecting 11 | * to build some projects, please open a GitHub issue. As a workaround you can uncomment this line 12 | * to temporarily restore the old behavior where everything must be rebuilt whenever pnpm-lock.json 13 | * is modified. 14 | */ 15 | // "legacyIncrementalBuildDependencyDetection": true, 16 | 17 | /** 18 | * By default, rush passes --no-prefer-frozen-lockfile to 'pnpm install'. 19 | * Set this option to true to pass '--frozen-lockfile' instead. 20 | */ 21 | // "usePnpmFrozenLockfileForRushInstall": true, 22 | 23 | /** 24 | * If true, the chmod field in temporary project tar headers will not be normalized. 25 | * This normalization can help ensure consistent tarball integrity across platforms. 26 | */ 27 | // "noChmodFieldInTarHeaderNormalization": true 28 | } 29 | -------------------------------------------------------------------------------- /common/config/rush/version-policies.json: -------------------------------------------------------------------------------- 1 | /** 2 | * This is configuration file is used for advanced publishing configurations with Rush. 3 | * For full documentation, please see https://rushjs.io 4 | */ 5 | 6 | /** 7 | * A list of version policy definitions. A "version policy" is a custom package versioning 8 | * strategy that affects "rush change", "rush version", and "rush publish". The strategy applies 9 | * to a set of projects that are specified using the "versionPolicyName" field in rush.json. 10 | */ 11 | [ 12 | // { 13 | // /** 14 | // * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion"). 15 | // * 16 | // * The "lockStepVersion" mode specifies that the projects will use "lock-step versioning". This 17 | // * strategy is appropriate for a set of packages that act as selectable components of a 18 | // * unified product. The entire set of packages are always published together, and always share 19 | // * the same NPM version number. When the packages depend on other packages in the set, the 20 | // * SemVer range is usually restricted to a single version. 21 | // */ 22 | // "definitionName": "lockStepVersion", 23 | // 24 | // /** 25 | // * (Required) The name that will be used for the "versionPolicyName" field in rush.json. 26 | // * This name is also used command-line parameters such as "--version-policy" 27 | // * and "--to-version-policy". 28 | // */ 29 | // "policyName": "MyBigFramework", 30 | // 31 | // /** 32 | // * (Required) The current version. All packages belonging to the set should have this version 33 | // * in the current branch. When bumping versions, Rush uses this to determine the next version. 34 | // * (The "version" field in package.json is NOT considered.) 35 | // */ 36 | // "version": "1.0.0", 37 | // 38 | // /** 39 | // * (Required) The type of bump that will be performed when publishing the next release. 40 | // * When creating a release branch in Git, this field should be updated according to the 41 | // * type of release. 42 | // * 43 | // * Valid values are: "prerelease", "release", "minor", "patch", "major" 44 | // */ 45 | // "nextBump": "prerelease", 46 | // 47 | // /** 48 | // * (Optional) If specified, all packages in the set share a common CHANGELOG.md file. 49 | // * This file is stored with the specified "main" project, which must be a member of the set. 50 | // * 51 | // * If this field is omitted, then a separate CHANGELOG.md file will be maintained for each 52 | // * package in the set. 53 | // */ 54 | // "mainProject": "my-app" 55 | // }, 56 | // 57 | // { 58 | // /** 59 | // * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion"). 60 | // * 61 | // * The "individualVersion" mode specifies that the projects will use "individual versioning". 62 | // * This is the typical NPM model where each package has an independent version number 63 | // * and CHANGELOG.md file. Although a single CI definition is responsible for publishing the 64 | // * packages, they otherwise don't have any special relationship. The version bumping will 65 | // * depend on how developers answer the "rush change" questions for each package that 66 | // * is changed. 67 | // */ 68 | // "definitionName": "individualVersion", 69 | // 70 | // "policyName": "MyRandomLibraries", 71 | // 72 | // /** 73 | // * (Optional) This can be used to enforce that all packages in the set must share a common 74 | // * major version number, e.g. because they are from the same major release branch. 75 | // * It can also be used to discourage people from accidentally making "MAJOR" SemVer changes 76 | // * inappropriately. The minor/patch version parts will be bumped independently according 77 | // * to the types of changes made to each project, according to the "rush change" command. 78 | // */ 79 | // "lockedMajor": 3, 80 | // 81 | // /** 82 | // * (Optional) When publishing is managed by Rush, by default the "rush change" command will 83 | // * request changes for any projects that are modified by a pull request. These change entries 84 | // * will produce a CHANGELOG.md file. If you author your CHANGELOG.md manually or announce updates 85 | // * in some other way, set "exemptFromRushChange" to true to tell "rush change" to ignore the projects 86 | // * belonging to this version policy. 87 | // */ 88 | // "exemptFromRushChange": false 89 | // } 90 | ] 91 | -------------------------------------------------------------------------------- /common/git-hooks/commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # This is an example Git hook for use with Rush. To enable this hook, rename this file 4 | # to "commit-msg" and then run "rush install", which will copy it from common/git-hooks 5 | # to the .git/hooks folder. 6 | # 7 | # TO LEARN MORE ABOUT GIT HOOKS 8 | # 9 | # The Git documentation is here: https://git-scm.com/docs/githooks 10 | # Some helpful resources: https://githooks.com 11 | # 12 | # ABOUT THIS EXAMPLE 13 | # 14 | # The commit-msg hook is called by "git commit" with one argument, the name of the file 15 | # that has the commit message. The hook should exit with non-zero status after issuing 16 | # an appropriate message if it wants to stop the commit. The hook is allowed to edit 17 | # the commit message file. 18 | 19 | # This example enforces that commit message should contain a minimum amount of 20 | # description text. 21 | if [ `cat $1 | wc -w` -lt 3 ]; then 22 | echo "" 23 | echo "Invalid commit message: The message must contain at least 3 words." 24 | exit 1 25 | fi 26 | -------------------------------------------------------------------------------- /common/scripts/install-run-rush-pnpm.js: -------------------------------------------------------------------------------- 1 | // THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. 2 | // 3 | // This script is intended for usage in an automated build environment where the Rush command may not have 4 | // been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush 5 | // specified in the rush.json configuration file (if not already installed), and then pass a command-line to the 6 | // rush-pnpm command. 7 | // 8 | // An example usage would be: 9 | // 10 | // node common/scripts/install-run-rush-pnpm.js pnpm-command 11 | // 12 | // For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ 13 | 14 | /******/ (() => { // webpackBootstrap 15 | /******/ "use strict"; 16 | var __webpack_exports__ = {}; 17 | /*!*****************************************************!*\ 18 | !*** ./lib-esnext/scripts/install-run-rush-pnpm.js ***! 19 | \*****************************************************/ 20 | 21 | // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 22 | // See the @microsoft/rush package's LICENSE file for license information. 23 | require('./install-run-rush'); 24 | //# sourceMappingURL=install-run-rush-pnpm.js.map 25 | module.exports = __webpack_exports__; 26 | /******/ })() 27 | ; 28 | //# sourceMappingURL=install-run-rush-pnpm.js.map -------------------------------------------------------------------------------- /common/scripts/install-run-rush.js: -------------------------------------------------------------------------------- 1 | // THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. 2 | // 3 | // This script is intended for usage in an automated build environment where the Rush command may not have 4 | // been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush 5 | // specified in the rush.json configuration file (if not already installed), and then pass a command-line to it. 6 | // An example usage would be: 7 | // 8 | // node common/scripts/install-run-rush.js install 9 | // 10 | // For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ 11 | 12 | /******/ (() => { // webpackBootstrap 13 | /******/ "use strict"; 14 | /******/ var __webpack_modules__ = ({ 15 | 16 | /***/ 657147: 17 | /*!*********************!*\ 18 | !*** external "fs" ***! 19 | \*********************/ 20 | /***/ ((module) => { 21 | 22 | module.exports = require("fs"); 23 | 24 | /***/ }), 25 | 26 | /***/ 371017: 27 | /*!***********************!*\ 28 | !*** external "path" ***! 29 | \***********************/ 30 | /***/ ((module) => { 31 | 32 | module.exports = require("path"); 33 | 34 | /***/ }) 35 | 36 | /******/ }); 37 | /************************************************************************/ 38 | /******/ // The module cache 39 | /******/ var __webpack_module_cache__ = {}; 40 | /******/ 41 | /******/ // The require function 42 | /******/ function __webpack_require__(moduleId) { 43 | /******/ // Check if module is in cache 44 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 45 | /******/ if (cachedModule !== undefined) { 46 | /******/ return cachedModule.exports; 47 | /******/ } 48 | /******/ // Create a new module (and put it into the cache) 49 | /******/ var module = __webpack_module_cache__[moduleId] = { 50 | /******/ // no module.id needed 51 | /******/ // no module.loaded needed 52 | /******/ exports: {} 53 | /******/ }; 54 | /******/ 55 | /******/ // Execute the module function 56 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 57 | /******/ 58 | /******/ // Return the exports of the module 59 | /******/ return module.exports; 60 | /******/ } 61 | /******/ 62 | /************************************************************************/ 63 | /******/ /* webpack/runtime/compat get default export */ 64 | /******/ (() => { 65 | /******/ // getDefaultExport function for compatibility with non-harmony modules 66 | /******/ __webpack_require__.n = (module) => { 67 | /******/ var getter = module && module.__esModule ? 68 | /******/ () => (module['default']) : 69 | /******/ () => (module); 70 | /******/ __webpack_require__.d(getter, { a: getter }); 71 | /******/ return getter; 72 | /******/ }; 73 | /******/ })(); 74 | /******/ 75 | /******/ /* webpack/runtime/define property getters */ 76 | /******/ (() => { 77 | /******/ // define getter functions for harmony exports 78 | /******/ __webpack_require__.d = (exports, definition) => { 79 | /******/ for(var key in definition) { 80 | /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { 81 | /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); 82 | /******/ } 83 | /******/ } 84 | /******/ }; 85 | /******/ })(); 86 | /******/ 87 | /******/ /* webpack/runtime/hasOwnProperty shorthand */ 88 | /******/ (() => { 89 | /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) 90 | /******/ })(); 91 | /******/ 92 | /******/ /* webpack/runtime/make namespace object */ 93 | /******/ (() => { 94 | /******/ // define __esModule on exports 95 | /******/ __webpack_require__.r = (exports) => { 96 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 97 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 98 | /******/ } 99 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 100 | /******/ }; 101 | /******/ })(); 102 | /******/ 103 | /************************************************************************/ 104 | var __webpack_exports__ = {}; 105 | // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. 106 | (() => { 107 | /*!************************************************!*\ 108 | !*** ./lib-esnext/scripts/install-run-rush.js ***! 109 | \************************************************/ 110 | __webpack_require__.r(__webpack_exports__); 111 | /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! path */ 371017); 112 | /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); 113 | /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 657147); 114 | /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); 115 | // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 116 | // See the @microsoft/rush package's LICENSE file for license information. 117 | 118 | 119 | const { installAndRun, findRushJsonFolder, RUSH_JSON_FILENAME, runWithErrorAndStatusCode } = require('./install-run'); 120 | const PACKAGE_NAME = '@microsoft/rush'; 121 | const RUSH_PREVIEW_VERSION = 'RUSH_PREVIEW_VERSION'; 122 | const INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_RUSH_LOCKFILE_PATH'; 123 | function _getRushVersion(logger) { 124 | const rushPreviewVersion = process.env[RUSH_PREVIEW_VERSION]; 125 | if (rushPreviewVersion !== undefined) { 126 | logger.info(`Using Rush version from environment variable ${RUSH_PREVIEW_VERSION}=${rushPreviewVersion}`); 127 | return rushPreviewVersion; 128 | } 129 | const rushJsonFolder = findRushJsonFolder(); 130 | const rushJsonPath = path__WEBPACK_IMPORTED_MODULE_0__.join(rushJsonFolder, RUSH_JSON_FILENAME); 131 | try { 132 | const rushJsonContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(rushJsonPath, 'utf-8'); 133 | // Use a regular expression to parse out the rushVersion value because rush.json supports comments, 134 | // but JSON.parse does not and we don't want to pull in more dependencies than we need to in this script. 135 | const rushJsonMatches = rushJsonContents.match(/\"rushVersion\"\s*\:\s*\"([0-9a-zA-Z.+\-]+)\"/); 136 | return rushJsonMatches[1]; 137 | } 138 | catch (e) { 139 | throw new Error(`Unable to determine the required version of Rush from rush.json (${rushJsonFolder}). ` + 140 | "The 'rushVersion' field is either not assigned in rush.json or was specified " + 141 | 'using an unexpected syntax.'); 142 | } 143 | } 144 | function _getBin(scriptName) { 145 | switch (scriptName.toLowerCase()) { 146 | case 'install-run-rush-pnpm.js': 147 | return 'rush-pnpm'; 148 | case 'install-run-rushx.js': 149 | return 'rushx'; 150 | default: 151 | return 'rush'; 152 | } 153 | } 154 | function _run() { 155 | const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, ...packageBinArgs /* [build, --to, myproject] */] = process.argv; 156 | // Detect if this script was directly invoked, or if the install-run-rushx script was invokved to select the 157 | // appropriate binary inside the rush package to run 158 | const scriptName = path__WEBPACK_IMPORTED_MODULE_0__.basename(scriptPath); 159 | const bin = _getBin(scriptName); 160 | if (!nodePath || !scriptPath) { 161 | throw new Error('Unexpected exception: could not detect node path or script path'); 162 | } 163 | let commandFound = false; 164 | let logger = { info: console.log, error: console.error }; 165 | for (const arg of packageBinArgs) { 166 | if (arg === '-q' || arg === '--quiet') { 167 | // The -q/--quiet flag is supported by both `rush` and `rushx`, and will suppress 168 | // any normal informational/diagnostic information printed during startup. 169 | // 170 | // To maintain the same user experience, the install-run* scripts pass along this 171 | // flag but also use it to suppress any diagnostic information normally printed 172 | // to stdout. 173 | logger = { 174 | info: () => { }, 175 | error: console.error 176 | }; 177 | } 178 | else if (!arg.startsWith('-') || arg === '-h' || arg === '--help') { 179 | // We either found something that looks like a command (i.e. - doesn't start with a "-"), 180 | // or we found the -h/--help flag, which can be run without a command 181 | commandFound = true; 182 | } 183 | } 184 | if (!commandFound) { 185 | console.log(`Usage: ${scriptName} [args...]`); 186 | if (scriptName === 'install-run-rush-pnpm.js') { 187 | console.log(`Example: ${scriptName} pnpm-command`); 188 | } 189 | else if (scriptName === 'install-run-rush.js') { 190 | console.log(`Example: ${scriptName} build --to myproject`); 191 | } 192 | else { 193 | console.log(`Example: ${scriptName} custom-command`); 194 | } 195 | process.exit(1); 196 | } 197 | runWithErrorAndStatusCode(logger, () => { 198 | const version = _getRushVersion(logger); 199 | logger.info(`The rush.json configuration requests Rush version ${version}`); 200 | const lockFilePath = process.env[INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE]; 201 | if (lockFilePath) { 202 | logger.info(`Found ${INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE}="${lockFilePath}", installing with lockfile.`); 203 | } 204 | return installAndRun(logger, PACKAGE_NAME, version, bin, packageBinArgs, lockFilePath); 205 | }); 206 | } 207 | _run(); 208 | //# sourceMappingURL=install-run-rush.js.map 209 | })(); 210 | 211 | module.exports = __webpack_exports__; 212 | /******/ })() 213 | ; 214 | //# sourceMappingURL=install-run-rush.js.map -------------------------------------------------------------------------------- /common/scripts/install-run-rushx.js: -------------------------------------------------------------------------------- 1 | // THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. 2 | // 3 | // This script is intended for usage in an automated build environment where the Rush command may not have 4 | // been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush 5 | // specified in the rush.json configuration file (if not already installed), and then pass a command-line to the 6 | // rushx command. 7 | // 8 | // An example usage would be: 9 | // 10 | // node common/scripts/install-run-rushx.js custom-command 11 | // 12 | // For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ 13 | 14 | /******/ (() => { // webpackBootstrap 15 | /******/ "use strict"; 16 | var __webpack_exports__ = {}; 17 | /*!*************************************************!*\ 18 | !*** ./lib-esnext/scripts/install-run-rushx.js ***! 19 | \*************************************************/ 20 | 21 | // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 22 | // See the @microsoft/rush package's LICENSE file for license information. 23 | require('./install-run-rush'); 24 | //# sourceMappingURL=install-run-rushx.js.map 25 | module.exports = __webpack_exports__; 26 | /******/ })() 27 | ; 28 | //# sourceMappingURL=install-run-rushx.js.map -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixi-pbr-monorepo", 3 | "version": "1.0.0", 4 | "description": "PixiJS Batch Rendering Library", 5 | "private": true, 6 | "scripts": { 7 | "test": "node ./tools/unit-tests/index.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/SukantPal/pixi-batch-renderer.git" 12 | }, 13 | "keywords": [ 14 | "pixijs", 15 | "batch-renderer", 16 | "optimizations", 17 | "webgl" 18 | ], 19 | "author": "Shukant K. Pal ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/SukantPal/pixi-batch-renderer/issues" 23 | }, 24 | "homepage": "https://github.com/SukantPal/pixi-batch-renderer#readme" 25 | } -------------------------------------------------------------------------------- /packages/batch-renderer/.jsdoc.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": false 4 | }, 5 | "source": { 6 | "include": [ 7 | "lib/pixi-batch-renderer.es.js" 8 | ], 9 | "includePattern": ".+\\.es.js(doc)?$" 10 | }, 11 | "plugins": [ 12 | "plugins/markdown" 13 | ], 14 | "templates": { 15 | "cleverLinks": false, 16 | "monospaceLinks": false, 17 | "default": { 18 | "outputSourceFiles": true 19 | }, 20 | "applicationName": "pixi-batch-renderer", 21 | "footer": "Made with ♥ by Shukant Pal", 22 | "copyright": "Copyright © 2019-2020 Shukant Pal", 23 | "disqus": "", 24 | "openGraph": { 25 | "title": "", 26 | "type": "website", 27 | "image": "", 28 | "site_name": "", 29 | "url": "" 30 | }, 31 | "meta": { 32 | "title": "PixiJS Batch Rendering Library API Documentation", 33 | "description": "Documentation for pixi-batch-renderer library", 34 | "keyword": "docs, documentation, pixi, pixijs, renderer, html5, javascript, jsdoc" 35 | }, 36 | "linenums": true, 37 | "sort": false 38 | }, 39 | "markdown": { 40 | "parser": "gfm", 41 | "hardwrap": false 42 | }, 43 | "opts": { 44 | "encoding": "utf8", 45 | "recurse": true, 46 | "lenient": true, 47 | "destination": "./docs", 48 | "template": "./node_modules/@pixi/jsdoc-template", 49 | "sort": false 50 | } 51 | } -------------------------------------------------------------------------------- /packages/batch-renderer/.npmignore: -------------------------------------------------------------------------------- 1 | compile 2 | api-extractor.json 3 | *.log 4 | tsconfig.json 5 | src 6 | *.json 7 | CHANGELOG.md 8 | docs -------------------------------------------------------------------------------- /packages/batch-renderer/CHANGELOG.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixi-batch-renderer", 3 | "entries": [ 4 | { 5 | "version": "2.5.3", 6 | "tag": "pixi-batch-renderer_v2.5.3", 7 | "date": "Tue, 06 Apr 2021 13:59:04 GMT", 8 | "comments": { 9 | "patch": [ 10 | { 11 | "comment": "Fix texture-lookup attribute not being filled correctly for multi-texture geometres (i.e. texturesPerObject > 1)" 12 | } 13 | ] 14 | } 15 | }, 16 | { 17 | "version": "2.5.1", 18 | "tag": "pixi-batch-renderer_v2.5.1", 19 | "date": "Sun, 28 Mar 2021 19:51:30 GMT", 20 | "comments": { 21 | "patch": [ 22 | { 23 | "comment": "Fix vertexSizeFor crashing pixi-batch-renderer due to dangling reference to PIXI." 24 | } 25 | ] 26 | } 27 | }, 28 | { 29 | "version": "2.5.0", 30 | "tag": "pixi-batch-renderer_v2.5.0", 31 | "date": "Sun, 28 Mar 2021 19:20:49 GMT", 32 | "comments": { 33 | "minor": [ 34 | { 35 | "comment": "Upgrade to PixiJS 6" 36 | } 37 | ] 38 | } 39 | }, 40 | { 41 | "version": "2.4.2", 42 | "tag": "pixi-batch-renderer_v2.4.2", 43 | "date": "Mon, 19 Oct 2020 00:38:23 GMT", 44 | "comments": { 45 | "patch": [ 46 | { 47 | "comment": "Fixes the incorrect resolution of vertex count in geometry merging" 48 | } 49 | ] 50 | } 51 | }, 52 | { 53 | "version": "2.4.1", 54 | "tag": "pixi-batch-renderer_v2.4.1", 55 | "date": "Mon, 19 Oct 2020 00:28:47 GMT", 56 | "comments": { 57 | "patch": [ 58 | { 59 | "comment": "Fixes the resolution of the vertex count if vertexCountProperty was not passed." 60 | } 61 | ] 62 | } 63 | }, 64 | { 65 | "version": "2.4.0", 66 | "tag": "pixi-batch-renderer_v2.4.0", 67 | "date": "Mon, 19 Oct 2020 00:17:18 GMT", 68 | "comments": { 69 | "minor": [ 70 | { 71 | "comment": "Support manual resolution of the number of indices to be batched." 72 | } 73 | ] 74 | } 75 | }, 76 | { 77 | "version": "2.3.7", 78 | "tag": "pixi-batch-renderer_v2.3.7", 79 | "date": "Sun, 18 Oct 2020 23:46:38 GMT", 80 | "comments": { 81 | "patch": [ 82 | { 83 | "comment": "Support passing vertexCountProperty as a functor." 84 | } 85 | ] 86 | } 87 | }, 88 | { 89 | "version": "2.3.6", 90 | "tag": "pixi-batch-renderer_v2.3.6", 91 | "date": "Sun, 30 Aug 2020 15:11:17 GMT", 92 | "comments": { 93 | "patch": [ 94 | { 95 | "comment": "Added unit-test for BufferPool, fixed crash when the an unallocated buffer was released." 96 | } 97 | ] 98 | } 99 | }, 100 | { 101 | "version": "2.3.5", 102 | "tag": "pixi-batch-renderer_v2.3.5", 103 | "date": "Sat, 29 Aug 2020 19:06:52 GMT", 104 | "comments": { 105 | "patch": [ 106 | { 107 | "comment": "Don't upload docs, compile to npm" 108 | } 109 | ] 110 | } 111 | }, 112 | { 113 | "version": "2.3.4", 114 | "tag": "pixi-batch-renderer_v2.3.4", 115 | "date": "Sat, 29 Aug 2020 19:02:11 GMT", 116 | "comments": { 117 | "patch": [ 118 | { 119 | "comment": ".cjs -> .js, .mjs -> .es.js for bundles" 120 | }, 121 | { 122 | "comment": "BufferPool API!" 123 | } 124 | ] 125 | } 126 | } 127 | ] 128 | } 129 | -------------------------------------------------------------------------------- /packages/batch-renderer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - pixi-batch-renderer 2 | 3 | This log was last generated on Tue, 06 Apr 2021 13:59:04 GMT and should not be manually modified. 4 | 5 | ## 2.5.3 6 | Tue, 06 Apr 2021 13:59:04 GMT 7 | 8 | ### Patches 9 | 10 | - Fix texture-lookup attribute not being filled correctly for multi-texture geometres (i.e. texturesPerObject > 1) 11 | 12 | ## 2.5.1 13 | Sun, 28 Mar 2021 19:51:30 GMT 14 | 15 | ### Patches 16 | 17 | - Fix vertexSizeFor crashing pixi-batch-renderer due to dangling reference to PIXI. 18 | 19 | ## 2.5.0 20 | Sun, 28 Mar 2021 19:20:49 GMT 21 | 22 | ### Minor changes 23 | 24 | - Upgrade to PixiJS 6 25 | 26 | ## 2.4.2 27 | Mon, 19 Oct 2020 00:38:23 GMT 28 | 29 | ### Patches 30 | 31 | - Fixes the incorrect resolution of vertex count in geometry merging 32 | 33 | ## 2.4.1 34 | Mon, 19 Oct 2020 00:28:47 GMT 35 | 36 | ### Patches 37 | 38 | - Fixes the resolution of the vertex count if vertexCountProperty was not passed. 39 | 40 | ## 2.4.0 41 | Mon, 19 Oct 2020 00:17:18 GMT 42 | 43 | ### Minor changes 44 | 45 | - Support manual resolution of the number of indices to be batched. 46 | 47 | ## 2.3.7 48 | Sun, 18 Oct 2020 23:46:38 GMT 49 | 50 | ### Patches 51 | 52 | - Support passing vertexCountProperty as a functor. 53 | 54 | ## 2.3.6 55 | Sun, 30 Aug 2020 15:11:17 GMT 56 | 57 | ### Patches 58 | 59 | - Added unit-test for BufferPool, fixed crash when the an unallocated buffer was released. 60 | 61 | ## 2.3.5 62 | Sat, 29 Aug 2020 19:06:52 GMT 63 | 64 | ### Patches 65 | 66 | - Don't upload docs, compile to npm 67 | 68 | ## 2.3.4 69 | Sat, 29 Aug 2020 19:02:11 GMT 70 | 71 | ### Patches 72 | 73 | - .cjs -> .js, .mjs -> .es.js for bundles 74 | - BufferPool API! 75 | 76 | -------------------------------------------------------------------------------- /packages/batch-renderer/README.md: -------------------------------------------------------------------------------- 1 | # pixi-batch-renderer 2 | 3 | ![Node.js CI](https://github.com/pixijs/pixi-batch-renderer/workflows/Node.js%20CI/badge.svg) 4 | [![install size](https://packagephobia.now.sh/badge?p=pixi-batch-renderer)](https://packagephobia.now.sh/result?p=pixi-batch-renderer) 5 | ![](https://img.shields.io/bundlephobia/min/pixi-batch-renderer) 6 | ![](https://img.shields.io/npm/v/pixi-batch-renderer/latest) 7 | [![](https://data.jsdelivr.com/v1/package/npm/pixi-batch-renderer-alpha/badge)](https://www.jsdelivr.com/package/npm/pixi-batch-renderer-alpha) 8 | ![dependencies](https://david-dm.org/sukantpal/pixi-batch-renderer.svg) 9 | 10 | `pixi-batch-renderer` is a PixiJS plugin that allows you to add batch rendering to your custom display objects. I have documented each class in the `PIXI.brend` namespace. 11 | 12 | ## Usage 13 | 14 | In browser: 15 | ```js 16 | PIXI.brend.BatchRenderer 17 | ``` 18 | 19 | Using ES: 20 | ```js 21 | import { BatchRenderer } from 'pixi-batch-renderer'; 22 | ``` 23 | 24 | ## Concepts 25 | 26 | [Batch rendering](https://medium.com/swlh/inside-pixijs-batch-rendering-system-fad1b466c420) objects involves aggregating them into groups/batches and rendering them together with one WebGL draw call. PixiJS supports batch rendering its internal display objects - `PIXI.Sprite`, `PIXI.Graphics`, and `PIXI.Mesh`. However, it is difficult to extend that to custom-built display objects; it wasn't designed as an exposable API. 27 | 28 | This library builds upon the same concept and is designed for maximum flexibility. It still uses PixiJS's batch system - a stub that enables objects to be rendered asynchronously, without loosing the order of rendering. To understand how it works, understanding these things are helpful: 29 | 30 | * **Attribute Redirects**: An attribute redirect is a data-object that tells `pixi-batch-renderer` how it will transform your object into a set of shader attributes. 31 | 32 | * **Index Property**: If you use indices, this will be property on your display object that holds those indices. It could also be a constant array, rather than a property on each object. 33 | 34 | * **State Function**: This function/property tells the batch renderer what WebGL state is required to render a display-object. It is optional if you're display objects use the default state (`PIXI.State.for2d()`). 35 | 36 | * **Shader Function**: This function generates the shader to render whole batches. It takes one argument - the batch renderer 37 | that will use the shader. You can use the `BatchShaderFactory#derive()` API for create one. 38 | 39 | ### New features 40 | 41 | * **Shader Templates**: The `BatchShaderFactory` allows you to write shader "templates" containing `%macros%`. At runtime, you 42 | can replace these macros based with another expression. For example, the (built-in) `%texturesPerBatch%` macro is set to the 43 | no. of textures units in the GPU. 44 | 45 | * **Custom uniforms**: [Experimental] You can also use uniforms in the batch shader; however, this might reduce the batching 46 | efficiency if most batchable display-objects have different values for uniforms (because then they can't be batched together). 47 | 48 | * **Modular architecture**: With the modular architecture of this library, you change the behaviour of any component. The 49 | geometry composition, batch generation, and drawing stages are componentized and can be modified by providing a custom 50 | implementation to `BatchRendererPluginFactory.from`. 51 | 52 | ### Caveat with filtered/masked objects 53 | 54 | Before rendering itself, a `PIXI.Container` with filters or a mask will flush the batch renderer and will not batch itself. This 55 | is because the PixiJS batch renderer cannot batch filtered and masked objects. Although this does not break pixi-batch-renderer, 56 | it does reduce batching-efficiency. If you want to create a batch renderer that will batch filtered and masked objects too, your display-object must override `render` (**however, you will have to derive your own batch renderer class for that**): 57 | 58 | ``` 59 | render(renderer: PIXI.Renderer): void 60 | { 61 | // If you registered the batch renderer as a plugin "pluginName", then replace with 62 | // renderer.plugins.pluginName 63 | renderer.setObjectRenderer(); 64 | .render(this); 65 | 66 | for (let i = 0, j = this.children.length; i < j; i++) 67 | { 68 | this._children.render(renderer); 69 | } 70 | } 71 | ``` 72 | 73 | # Usage 74 | 75 | ### Standard Pipeline 76 | 77 | For most use cases, `PIXI.brend.BatchRendererPluginFactory` is all you'll need from this library. You need to do these three things: 78 | 79 | 1. **Generate the plugin class using `PIXI.brend.BatchRendererPluginFactory.from`** 80 | 81 | 2. **Register the plugin with PixiJS's WebGL renderer** 82 | 83 | 3. **Make your custom display object defer its rendering to your plugin** 84 | 85 | An example implementation would look like: 86 | 87 | ```js 88 | import * as PIXI from 'pixi.js'; 89 | import { AttributeRedirect, BatchRendererPluginFactory, BatchShaderFactory } from 'pixi-batch-renderer'; 90 | 91 | // ExampleFigure has two attributes: aVertex and aTextureCoord. They come from the 92 | // vertices and uvs properties in this object. The indices are in the indices property. 93 | class ExampleFigure extends PIXI.Container 94 | { 95 | _render(renderer) 96 | { 97 | this.vertices = [x0,y0, x1,y1, x2,y2, ..., xn,yn];// variable number of vertices 98 | this.uvs = [u0,v0, u1,v1, u2, v2, ..., un,yn];// however, all other attributes must have equal length 99 | this.texture = PIXI.Texture.from("url:example"); 100 | 101 | this.indices = [0, 1, 2, ..., n];// we could also tell our batch renderer to not use indices too :) 102 | 103 | renderer.setObjectRenderer(renderer.plugins["ExampleRenderer"]); 104 | renderer.plugins["ExampleRenderer"].render(this); 105 | } 106 | } 107 | 108 | // Define the geometry of ExampleFigure. 109 | const attribSet = [ 110 | new AttributeRedirect({ 111 | source: "vertices", 112 | attrib: "aVertex", 113 | type: 'float32', 114 | size: 2, 115 | glType: PIXI.TYPES.FLOAT, 116 | glSize: 2 117 | }), 118 | new AttributeRedirect({ 119 | source: "uvs", 120 | attrib: "aTextureCoord", 121 | type: 'float32', 122 | size: 2, 123 | glType: PIXI.TYPES.FLOAT, 124 | size: 2 125 | }), 126 | ]; 127 | 128 | // Create a shader function from a shader template! 129 | const shaderFunction = new BatchShaderFactory( 130 | // Vertex Shader 131 | ` 132 | attribute vec2 aVertex; 133 | attribute vec2 aTextureCoord; 134 | attribute float aTextureId; 135 | 136 | varying float vTextureId; 137 | varying vec2 vTextureCoord; 138 | 139 | uniform mat3 projectionMatrix; 140 | 141 | void main() 142 | { 143 | gl_Position = vec4((projectionMatrix * vec3(aVertex.xy, 1)), 0, 1); 144 | vTextureId = aTextureId; 145 | vTextureCoord = aTextureCoord; 146 | } 147 | `, 148 | 149 | // Fragment Shader 150 | ` 151 | uniform uSamplers[%texturesPerBatch%];/* %texturesPerBatch% is a macro and will become a number */\ 152 | varying float vTextureId; 153 | varying vec2 vTextureCoord; 154 | 155 | void main(void){ 156 | vec4 color; 157 | 158 | /* get color, which is the pixel in texture uSamplers[vTextureId] @ vTextureCoord */ 159 | for (int k = 0; k < %texturesPerBatch%; ++k) 160 | { 161 | if (int(vTextureId) == k) 162 | color = texture2D(uSamplers[k], vTextureCoord); 163 | 164 | } 165 | 166 | gl_FragColor = color; 167 | } 168 | `, 169 | {}).derive(); 170 | 171 | // Create batch renderer class 172 | const ExampleRenderer = BatchRendererPluginFactory.from({ 173 | attribSet, 174 | indexProperty: "indices", 175 | textureProperty: "texture", 176 | texIDAttrib: "aTextureId", // this will be used to locate the texture in the fragment shader later 177 | shaderFunction 178 | }); 179 | 180 | // Remember to do this before instantiating a PIXI.Application or PIXI.Renderer! 181 | PIXI.Renderer.registerPlugin("ExampleRenderer", ExampleRenderer); 182 | ``` 183 | 184 | ### Uniforms Pipeline [Experimental] 185 | 186 | You can take advantage of shader uniforms in batching too! pixi-batch-renderer supports this out of the box 187 | with the `AggregateUniformsBatchFactory`. There are two modes for uniforms: 188 | 189 | * **Aggregation mode**: Uniforms of each display-object are aggregated into an array. Then an uniform-ID attribute 190 | is uploaded (say `aUniformId`) that tells the shader which uniform to pick out of an array. The attribute is passed 191 | to the plugin factory via the `uniformIDAttrib` option. 192 | 193 | * **No-aggregation mode**: Uniforms are uploaded in one-element arrays. Display-objects with different uniform values 194 | are not batched together. This is useful if your uniform values don't differ a lot and will avoid another attribute. To 195 | use this mode, simply don't pass an `uniformIDAttrib`. 196 | 197 | 1. Aggregation Mode (Example) 198 | 199 | ```js 200 | const { UniformRedirect, AggregateUniformsBatchFactory } = require('pixi-batch-renderer'); 201 | 202 | const shaderFunction = new BatchShaderFactory( 203 | // Vertex Shader 204 | ` 205 | attribute vec2 aVertex; 206 | attribute vec2 aTextureCoord; 207 | attribute float aTextureId; 208 | attribute float aUniformId; 209 | 210 | varying float vTextureId; 211 | varying vec2 vTextureCoord; 212 | varying float vUniformId; 213 | 214 | uniform mat3 projectionMatrix; 215 | 216 | void main() 217 | { 218 | gl_Position = vec4((projectionMatrix * vec3(aVertex.xy, 1)), 0, 1); 219 | vTextureId = aTextureId; 220 | vTextureCoord = aTextureCoord; 221 | 222 | vUniformId = aUniformId; 223 | } 224 | `, 225 | 226 | // Fragment Shader 227 | ` 228 | // You can also use this in the vertex shader. 229 | uniform shaderType[%uniformsPerBatch%]; 230 | varying float vUniformId; 231 | 232 | uniform uSamplers[%texturesPerBatch%];/* %texturesPerBatch% is a macro and will become a number */\ 233 | varying float vTextureId; 234 | varying vec2 vTextureCoord; 235 | 236 | void main(void){ 237 | vec4 color; 238 | float type; 239 | 240 | /* get color & shaderType */ 241 | for (int k = 0; k < int(max(%texturesPerBatch%., %uniformsPerBatch%.)); ++k) 242 | { 243 | if (int(vTextureId) == k) { 244 | color = texture2D(uSamplers[k], vTextureCoord); 245 | } 246 | if (int(vUniformId) == k) { 247 | type = shaderType[vUniformId]; 248 | } 249 | } 250 | 251 | 252 | if (type == 1) 253 | { 254 | gl_FragColor = color; 255 | } 256 | else 257 | { 258 | gl_FragColor = vec4(color.rgb * vTextureCoord.x, vTextureCoord.x); 259 | } 260 | } 261 | `, 262 | {}).derive(); 263 | 264 | const uniformSet = [ 265 | new UniformRedirect({ source: "type", uniform: "shadingType" }); 266 | ]; 267 | 268 | const ExampleRenderer = BatchRendererPluginFactory.from({ 269 | uniformSet, 270 | uniformIDAttrib: "aUniformId", 271 | 272 | // Previous example's stuff 273 | attribSet, 274 | indexProperty: "indices", 275 | textureProperty: "texture", 276 | texIDAttrib: "aTextureId", 277 | shaderFunction, 278 | 279 | BatchFactoryClass: AggregateUniformsBatchFactory 280 | }) 281 | ``` 282 | 283 | 2. No Aggregation Mode (Example) 284 | 285 | 286 | ```js 287 | const { UniformRedirect, AggregateUniformsBatchFactory } = require('pixi-batch-renderer'); 288 | 289 | const shaderFunction = new BatchShaderFactory( 290 | // Vertex Shader (no changes to standard pipeline example!) 291 | ` 292 | attribute vec2 aVertex; 293 | attribute vec2 aTextureCoord; 294 | attribute float aTextureId; 295 | varying float vTextureId; 296 | varying vec2 vTextureCoord; 297 | 298 | uniform mat3 projectionMatrix; 299 | 300 | void main() 301 | { 302 | gl_Position = vec4((projectionMatrix * vec3(aVertex.xy, 1)), 0, 1); 303 | vTextureId = aTextureId; 304 | vTextureCoord = aTextureCoord; 305 | } 306 | `, 307 | 308 | // Fragment Shader 309 | ` 310 | // Look only one-element instead of %uniformsPerBatch% 311 | uniform shaderType[1]; 312 | varying float vUniformId; 313 | 314 | uniform uSamplers[%texturesPerBatch%];/* %texturesPerBatch% is a macro and will become a number */\ 315 | varying float vTextureId; 316 | varying vec2 vTextureCoord; 317 | 318 | void main(void){ 319 | vec4 color; 320 | float type = shaderType; 321 | 322 | /* get color & shaderType */ 323 | for (int k = 0; k < %texturesPerBatch%; ++k) 324 | { 325 | if (int(vTextureId) == k) { 326 | color = texture2D(uSamplers[k], vTextureCoord); 327 | } 328 | } 329 | 330 | if (type == 1) 331 | { 332 | gl_FragColor = color; 333 | } 334 | else 335 | { 336 | gl_FragColor = vec4(color.rgb * vTextureCoord.x, vTextureCoord.x); 337 | } 338 | } 339 | `, 340 | {}).derive(); 341 | 342 | const uniformSet = [ 343 | new UniformRedirect({ source: "type", uniform: "shaderType" }); 344 | ]; 345 | 346 | const ExampleRenderer = BatchRendererPluginFactory.from({ 347 | uniformSet, 348 | 349 | attribSet, 350 | indexProperty: "indices", 351 | textureProperty: "texture", 352 | texIDAttrib: "aTextureId", 353 | shaderFunction, 354 | 355 | BatchFactoryClass: AggregateUniformsBatchFactory 356 | }); 357 | 358 | PIXI.Renderer.registerPlugin("erend", ExampleRenderer); 359 | 360 | // Try using with a Sprite! 361 | const sprite = PIXI.Sprite.from(); 362 | 363 | sprite.pluginName = "erend"; 364 | sprite.type = 0;// will fade out horizontally in shader 365 | ``` 366 | 367 | ### Advanced/Customized Batch Generation 368 | 369 | The `BatchRendererPluginFactory.from` method also accepts these (optional) options that can be used to extend the 370 | behaviour of built-in components: 371 | 372 | * `BatchFactoryClass`: Child class of [StdBatchFactory]{@link https://pixijs.io/pixi-batch-renderer/PIXI.brend.StdBatchFactory.html} 373 | 374 | * `BatchGeometryClass`: Child class of [BatchGeometry]{@link https://pixijs.io/pixi-batch-renderer/PIXI.brend.BatchGeometryFactory.html} 375 | 376 | * `BatchDrawerClass`: Child class of [BatchDrawer]{@link https://pixijs.io/pixi-batch-renderer/PIXI.brend.BatchDrawer.html} 377 | 378 | * `BatchRendererClass`: If overriding a component does not meet your requirements, you can derive your own batch renderer by 379 | providing a child class of [BatchRenderer]{@link https://pixijs.io/pixi-batch-renderer/PIXI.brend.BatchRenderer.html} 380 | -------------------------------------------------------------------------------- /packages/batch-renderer/api-extractor.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 3 | "mainEntryPointFilePath": "/compile/index.d.ts", 4 | "bundledPackages": [ 5 | "@pixi-essentials/pixi-object-pool" 6 | ], 7 | "compiler": { 8 | "tsconfigFilePath": "/tsconfig.json" 9 | }, 10 | "apiReport": { 11 | "enabled": false 12 | }, 13 | "docModel": { 14 | "enabled": false 15 | }, 16 | "dtsRollup": { 17 | "enabled": true, 18 | "untrimmedFilePath": "/index.d.ts", 19 | }, 20 | "tsdocMetadata": { 21 | "enabled": false 22 | }, 23 | "messages": { 24 | "compilerMessageReporting": { 25 | "default": { 26 | "logLevel": "none" 27 | } 28 | }, 29 | "extractorMessageReporting": { 30 | "default": { 31 | "logLevel": "none" 32 | } 33 | }, 34 | "tsdocMessageReporting": { 35 | "default": { 36 | "logLevel": "none" 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /packages/batch-renderer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixi-batch-renderer", 3 | "version": "3.0.0", 4 | "description": "Batch rendering library for PixiJS applications", 5 | "main": "lib/pixi-batch-renderer.js", 6 | "module": "lib/pixi-batch-renderer.es.js", 7 | "bundle": "dist/pixi-batch-renderer.js", 8 | "namespace": "PIXI.brend", 9 | "types": "index.d.ts", 10 | "scripts": { 11 | "build": "npm run compile && pnpm compile:types", 12 | "compile": "rollup -c node_modules/@pixi-build-tools/rollup-configurator/index.js --silent", 13 | "compile:types": "rm -rf compile && tsc; api-extractor run; echo Done", 14 | "prepublishOnly": "npm run build" 15 | }, 16 | "author": "Shukant Pal ", 17 | "license": "MIT", 18 | "files": [ 19 | "lib", 20 | "dist", 21 | "index.d.ts" 22 | ], 23 | "keywords": [ 24 | "pixi", 25 | "pixijs", 26 | "webgl", 27 | "rendering", 28 | "canvas", 29 | "batching" 30 | ], 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/SukantPal/pixi-batch-renderer" 34 | }, 35 | "peerDependencies": { 36 | "@pixi/constants": "^7.0.0", 37 | "@pixi/core": "^7.0.0", 38 | "@pixi/display": "^7.0.0", 39 | "@pixi/math": "^7.0.0", 40 | "@pixi/settings": "^7.0.0", 41 | "@pixi/utils": "^7.0.0" 42 | }, 43 | "devDependencies": { 44 | "@microsoft/api-extractor": "^7.7.13", 45 | "@pixi/eslint-config": "^1.0.1", 46 | "@pixi-build-tools/rollup-configurator": "~1.0.11", 47 | "eslint": "^6.8.0", 48 | "eslint-plugin-jsdoc": "^15.12.2", 49 | "floss": "^5.0.0", 50 | "electron": "^12.0.0", 51 | "nyc": "15.1.0", 52 | "gh-pages": "^2.2.0", 53 | "rollup": "^2.3.3", 54 | "tslib": "^2.0.0", 55 | "typescript": "~4.9.5", 56 | "@types/node": "^12.12.2" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/AggregateUniformsBatch.ts: -------------------------------------------------------------------------------- 1 | import { StdBatch } from './StdBatch'; 2 | 3 | import type { BatchRenderer } from './BatchRenderer'; 4 | import type { Renderer, UniformGroup } from '@pixi/core'; 5 | 6 | /** 7 | * Allows usage of uniforms when rendering display-objects in batches. It expects you to 8 | * aggregate each display-object's uniforms in an array and that the shader will pick 9 | * the appropriate uniform at runtime (an index into the uniforms array will be passed). 10 | * 11 | * **Usage in shader:** 12 | * ``` 13 | * // Your display-objects' affine transforms are aggregated into this array. 14 | * uniform mat3d affineTransform[]; 15 | * 16 | * // For WebGL1+ machines, your uniforms may be fetched by the uniform-ID attrib (float). 17 | * varying float vUniformID; 18 | * 19 | * // For WebGL-2 only, to prevent interpolation overhead, you may use the flat in variables. You 20 | * // can configure this in AggregateUniformShaderFactory. 21 | * flat in int uniformID; 22 | * ``` 23 | * 24 | * # No Aggregation Mode 25 | * 26 | * Aggregating uniforms into arrays requries a uniform-ID attribute to be uploaded as well. This 27 | * may cost a lot of memory if your uniforms don't really change a lot. For these cases, you can 28 | * disable uniform aggregation by not passing a `uniformIDAttrib`. This will make batches **only** 29 | * have one value for each uniform. The uniforms will still be uploaded as 1-element arrays, however. 30 | */ 31 | export class AggregateUniformsBatch extends StdBatch 32 | { 33 | renderer: BatchRenderer; 34 | 35 | uniformBuffer: { [id: string]: Array }; 36 | uniformMap: Array; 37 | uniformLength: number; 38 | 39 | constructor(renderer: BatchRenderer, geometryOffset?: number) 40 | { 41 | super(geometryOffset); 42 | 43 | /** 44 | * Renderer holding the uniform redirects 45 | */ 46 | this.renderer = renderer; 47 | 48 | /** 49 | * The buffer of uniform arrays of the display-objects 50 | * @member {Object>} 51 | */ 52 | this.uniformBuffer = null; 53 | 54 | /** 55 | * Array mapping the in-batch ID to the uniform ID. 56 | * @member {Array} 57 | */ 58 | this.uniformMap = null; 59 | 60 | /** 61 | * No. of uniforms buffered (per uniform name) 62 | * @member {number} 63 | */ 64 | this.uniformLength = 0; 65 | } 66 | 67 | upload(renderer: Renderer): void 68 | { 69 | super.upload(renderer); 70 | 71 | const { _uniformRedirects: uniformRedirects, _shader: shader } = this.renderer; 72 | 73 | for (let i = 0, j = uniformRedirects.length; i < j; i++) 74 | { 75 | const glslIdentifer = uniformRedirects[i].glslIdentifer; 76 | 77 | shader.uniforms[glslIdentifer] = this.uniformBuffer[glslIdentifer]; 78 | } 79 | 80 | // shader.uniformGroup.update(); 81 | } 82 | 83 | /** 84 | * @override 85 | */ 86 | reset(): void 87 | { 88 | super.reset(); 89 | 90 | for (const uniformName in this.uniformBuffer) 91 | { 92 | this.uniformBuffer[uniformName].length = 0; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/AggregateUniformsBatchFactory.ts: -------------------------------------------------------------------------------- 1 | import { AggregateUniformsBatch } from './AggregateUniformsBatch'; 2 | import { BaseTexture } from '@pixi/core'; 3 | import { BatchRenderer } from './BatchRenderer'; 4 | import { Matrix, Point } from '@pixi/math'; 5 | import { StdBatchFactory } from './StdBatchFactory'; 6 | 7 | import type { DisplayObject } from '@pixi/display'; 8 | import type { UniformGroup } from '@pixi/core'; 9 | 10 | /** 11 | * Factory for producing aggregate-uniforms batches. This is useful for shaders that 12 | * **must** use uniforms. 13 | */ 14 | export class AggregateUniformsBatchFactory extends StdBatchFactory 15 | { 16 | MAX_UNIFORMS: number; 17 | 18 | protected uniformBuffer: { [id: string]: Array }; 19 | protected uniformMap: Array; 20 | protected uniformLength: number; 21 | 22 | constructor(renderer: BatchRenderer) 23 | { 24 | super(renderer); 25 | 26 | /** 27 | * The max. uniforms until the batch is filled 28 | * @member {number} 29 | * @readonly 30 | */ 31 | // Max. no. of uniforms that can be passed to the batch shader. We divide by four because 32 | // mat4d/vec4 count as four uniforms. 33 | this.MAX_UNIFORMS = Math.floor( 34 | Math.min( 35 | renderer.renderer.gl.getParameter(renderer.renderer.gl.MAX_VERTEX_UNIFORM_VECTORS), 36 | renderer.renderer.gl.getParameter(renderer.renderer.gl.MAX_FRAGMENT_UNIFORM_VECTORS)) 37 | / (4 * renderer._uniformRedirects.length)); 38 | 39 | this.uniformBuffer = this._createUniformBuffer(); 40 | this.uniformMap = []; 41 | this.uniformLength = 0; 42 | } 43 | 44 | /** 45 | * @returns {AggregateUniformsBatch} 46 | */ 47 | _newBatch(): AggregateUniformsBatch 48 | { 49 | const batch = new AggregateUniformsBatch(this._renderer); 50 | 51 | // All pooled batches will have a buffer 52 | batch.uniformBuffer = this._createUniformBuffer(); 53 | batch.uniformMap = []; 54 | 55 | return batch; 56 | } 57 | 58 | /** 59 | * Stores uniforms in the current batch, if possible. 60 | * 61 | * If you want to override this, be sure to return beforehand if `super._put` returns 62 | * false: 63 | * ``` 64 | * _put(displayObject: DisplayObject): boolean 65 | * { 66 | * if (!super._put(displayObject)) 67 | * { 68 | * return false; 69 | * } 70 | * 71 | * // Your logic ... 72 | * } 73 | * ``` 74 | * 75 | * @protected 76 | * @returns Whether uniforms can be buffered 77 | */ 78 | protected _put(displayObject: DisplayObject): boolean 79 | { 80 | if (!this._renderer._uniformIDAttrib) 81 | { 82 | // No aggregation mode! If uniforms already buffered, they **must** match or batch will break. 83 | if (this.uniformLength >= 0) 84 | { 85 | const id = this._matchUniforms(displayObject); 86 | 87 | if (id >= 0) 88 | { 89 | return true; 90 | } 91 | 92 | return false; 93 | } 94 | } 95 | 96 | if (this.uniformLength + 1 >= this.MAX_UNIFORMS) 97 | { 98 | return false; 99 | } 100 | 101 | if (this._renderer._uniformIDAttrib) 102 | { 103 | const id = this._matchUniforms(displayObject); 104 | 105 | if (id >= 0) 106 | { 107 | this.uniformMap.push(id); 108 | 109 | return true; 110 | } 111 | } 112 | 113 | // Push each uniform into the buffer 114 | for (let i = 0, j = this._renderer._uniformRedirects.length; i < j; i++) 115 | { 116 | const uniformRedirect = this._renderer._uniformRedirects[i]; 117 | const { source, glslIdentifer } = uniformRedirect; 118 | 119 | const value = typeof source === 'string' 120 | ? (displayObject as any)[source] 121 | : source(displayObject, this._renderer); 122 | 123 | if (Array.isArray(value)) 124 | { 125 | this.uniformBuffer[glslIdentifer].push(...value); 126 | } 127 | else 128 | { 129 | this.uniformBuffer[glslIdentifer].push(value); 130 | } 131 | } 132 | 133 | this.uniformMap.push(this.uniformLength); 134 | ++this.uniformLength; 135 | 136 | return true; 137 | } 138 | 139 | /** 140 | * @protected 141 | * @param {AggregateUniformBatch} batch 142 | */ 143 | _buildBatch(batch: any): void 144 | { 145 | super._buildBatch(batch); 146 | 147 | const buffer = batch.uniformBuffer; 148 | const map = batch.uniformMap; 149 | 150 | batch.uniformBuffer = this.uniformBuffer; 151 | batch.uniformMap = this.uniformMap; 152 | batch.uniformLength = this.uniformLength; 153 | 154 | // Swap & reset instead of new allocation 155 | this.uniformBuffer = buffer; 156 | this.uniformMap = map; 157 | this.uniformLength = 0; 158 | this._resetUniformBuffer(this.uniformBuffer); 159 | this.uniformMap.length = 0; 160 | } 161 | 162 | /** 163 | * Creates an array for each uniform-name in an object. 164 | * 165 | * @returns - the object created (the uniform buffer) 166 | */ 167 | private _createUniformBuffer(): { [id: string]: Array } 168 | { 169 | const buffer: { [id: string]: Array } = {}; 170 | 171 | for (let i = 0, j = this._renderer._uniformRedirects.length; i < j; i++) 172 | { 173 | const uniformRedirect = this._renderer._uniformRedirects[i]; 174 | 175 | buffer[uniformRedirect.glslIdentifer] = []; 176 | } 177 | 178 | return buffer; 179 | } 180 | 181 | /** 182 | * Resets each array in the uniform buffer 183 | * @param {object} buffer 184 | */ 185 | private _resetUniformBuffer(buffer: { [id: string]: Array }): void 186 | { 187 | for (let i = 0, j = this._renderer._uniformRedirects.length; i < j; i++) 188 | { 189 | const uniformRedirect = this._renderer._uniformRedirects[i]; 190 | 191 | buffer[uniformRedirect.glslIdentifer].length = 0; 192 | } 193 | } 194 | 195 | /** 196 | * Finds a matching set of uniforms in the buffer. 197 | */ 198 | private _matchUniforms(displayObject: any): number 199 | { 200 | const uniforms = this._renderer._uniformRedirects; 201 | 202 | for (let i = this.uniformLength - 1; i >= 0; i--) 203 | { 204 | let isMatch = true; 205 | 206 | for (let k = 0, n = uniforms.length; k < n; k++) 207 | { 208 | const { glslIdentifer, source } = uniforms[k]; 209 | 210 | const value = typeof source === 'string' 211 | ? displayObject[source] 212 | : source(displayObject as DisplayObject, this._renderer); 213 | 214 | if (!this._compareUniforms(value, this.uniformBuffer[glslIdentifer][i])) 215 | { 216 | isMatch = false; 217 | break; 218 | } 219 | } 220 | 221 | if (isMatch) 222 | { 223 | return i; 224 | } 225 | } 226 | 227 | return -1; 228 | } 229 | 230 | // Compares two uniforms u1 & u2 for equality. 231 | private _compareUniforms(u1: any, u2: any): boolean 232 | { 233 | if (u1 === u2) 234 | { 235 | return true; 236 | } 237 | 238 | // UniformGroups must have referential equality 239 | if (u1.group || u2.group) 240 | { 241 | return false; 242 | } 243 | 244 | // Allow equals() method for custom stuff. 245 | if (u1.equals) 246 | { 247 | return u1.equals(u2); 248 | } 249 | 250 | // Test one-depth equality for arrays 251 | if (Array.isArray(u1) && Array.isArray(u2)) 252 | { 253 | if (u1.length !== u2.length) 254 | { 255 | return false; 256 | } 257 | 258 | for (let i = 0, j = u1.length; i < j; i++) 259 | { 260 | // Referential equality for array elements 261 | if (u1[i] !== u2[i]) 262 | { 263 | return false; 264 | } 265 | } 266 | 267 | return true; 268 | } 269 | 270 | if (u1 instanceof Point && u2 instanceof Point) 271 | { 272 | return u1.x === u2.x && u1.y === u2.y; 273 | } 274 | if (u1 instanceof Matrix && u2 instanceof Matrix) 275 | { 276 | return u1.a === u2.a && u1.b === u2.b 277 | && u1.c === u2.c && u1.d === u2.d 278 | && u1.tx === u2.tx && u1.ty === u2.ty; 279 | } 280 | 281 | // Unlikely for batch rendering 282 | if (u1 instanceof BaseTexture && u2 instanceof BaseTexture) 283 | { 284 | return u1.uid === u2.uid; 285 | } 286 | 287 | return false; 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/BatchDrawer.ts: -------------------------------------------------------------------------------- 1 | import { BatchRenderer } from './BatchRenderer'; 2 | 3 | /** 4 | * Executes the final stage of batch rendering - drawing. The drawer can assume that 5 | * all display-objects have been into the batch-factory and the batch-geometry factory. 6 | */ 7 | export class BatchDrawer 8 | { 9 | /** The batch renderer */ 10 | renderer: BatchRenderer; 11 | 12 | constructor(renderer: BatchRenderer) 13 | { 14 | this.renderer = renderer; 15 | } 16 | 17 | /** 18 | * This method will be called after all display-object have been fed into the 19 | * batch and batch-geometry factories. 20 | * 21 | * **Hint**: You will call some form of `BatchGeometryFactory#build`; be sure to release 22 | * that geometry for reuse in next render pass via `BatchGeometryFactory#release(geom)`. 23 | */ 24 | draw(): void 25 | { 26 | const { 27 | renderer, 28 | _batchFactory: batchFactory, 29 | _geometryFactory: geometryFactory, 30 | _indexProperty: indexProperty, 31 | } = this.renderer; 32 | 33 | const batchList = batchFactory.access(); 34 | const batchCount = batchFactory.size(); 35 | const geom = geometryFactory.build(); 36 | const { gl } = renderer; 37 | 38 | // PixiJS bugs - the shader can't be bound before uploading because uniform sync caching 39 | // and geometry requires the shader to be bound. 40 | batchList[0].upload(renderer); 41 | renderer.shader.bind(this.renderer._shader, false); 42 | renderer.geometry.bind(geom); 43 | 44 | for (let i = 0; i < batchCount; i++) 45 | { 46 | const batch = batchList[i]; 47 | 48 | batch.upload(renderer); 49 | renderer.shader.bind(this.renderer._shader, false); 50 | 51 | if (indexProperty) 52 | { 53 | // TODO: Get rid of the $indexCount black magic! 54 | gl.drawElements(gl.TRIANGLES, batch.$indexCount, gl.UNSIGNED_SHORT, batch.geometryOffset * 2); 55 | } 56 | else 57 | { 58 | // TODO: Get rid of the $vertexCount black magic! 59 | gl.drawArrays(gl.TRIANGLES, batch.geometryOffset, batch.$vertexCount); 60 | } 61 | 62 | batch.reset(); 63 | } 64 | 65 | geometryFactory.release(geom); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/BatchGeometryFactory.ts: -------------------------------------------------------------------------------- 1 | import { AttributeRedirect } from './redirects/AttributeRedirect'; 2 | import Redirect from './redirects/Redirect'; 3 | import BatchRenderer from './BatchRenderer'; 4 | import { Buffer, Geometry, ViewableBuffer } from '@pixi/core'; 5 | import { StdBatch } from './StdBatch'; 6 | import { TYPES } from '@pixi/constants'; 7 | import { AggregateUniformsBatch } from './AggregateUniformsBatch'; 8 | import { nextPow2, log2 } from '@pixi/utils'; 9 | 10 | import type { DisplayObject } from '@pixi/display'; 11 | import type { Resolvable } from './utils/resolveProperty'; 12 | 13 | // BatchGeometryFactory uses this class internally to setup the attributes of 14 | // the batches. 15 | // 16 | // Supports Uniforms+Standard Pipeline's in-batch/uniform ID. 17 | export class BatchGeometry extends Geometry 18 | { 19 | // Interleaved attribute data buffer 20 | attribBuffer: Buffer; 21 | 22 | // Batched indicies 23 | indexBuffer: Buffer; 24 | 25 | constructor(attributeRedirects: AttributeRedirect[], 26 | hasIndex: boolean, 27 | texIDAttrib: string, 28 | texturesPerObject: number, 29 | inBatchIDAttrib: string, 30 | uniformIDAttrib: string, 31 | masterIDAttrib: string, 32 | attributeBuffer?: Buffer, 33 | indexBuffer?: Buffer 34 | ) 35 | { 36 | super(); 37 | 38 | attributeBuffer = attributeBuffer || new Buffer(null, false, false); 39 | indexBuffer = indexBuffer || (hasIndex ? new Buffer(null, false, true) : null); 40 | 41 | attributeRedirects.forEach((redirect) => 42 | { 43 | const { glslIdentifer, glType, glSize, normalize } = redirect; 44 | 45 | this.addAttribute(glslIdentifer, attributeBuffer, glSize, normalize, glType); 46 | }); 47 | 48 | if (!masterIDAttrib) 49 | { 50 | if (texIDAttrib && texturesPerObject > 0) 51 | { 52 | this.addAttribute(texIDAttrib, attributeBuffer, texturesPerObject, true, TYPES.FLOAT); 53 | } 54 | if (inBatchIDAttrib) 55 | { 56 | this.addAttribute(inBatchIDAttrib, attributeBuffer, 1, false, TYPES.FLOAT); 57 | } 58 | if (uniformIDAttrib) 59 | { 60 | this.addAttribute(uniformIDAttrib, attributeBuffer, 1, false, TYPES.FLOAT); 61 | } 62 | } 63 | else 64 | { 65 | this.addAttribute(masterIDAttrib, attributeBuffer, 1, false, TYPES.FLOAT); 66 | } 67 | 68 | if (hasIndex) 69 | { 70 | this.addIndex(indexBuffer); 71 | } 72 | 73 | this.attribBuffer = attributeBuffer; 74 | this.indexBuffer = indexBuffer; 75 | } 76 | } 77 | 78 | // To define the constructor shape, this is defined as an abstract class but documented 79 | // as an interface. 80 | export abstract class IBatchGeometryFactory 81 | { 82 | protected _renderer: BatchRenderer; 83 | 84 | // eslint-disable-next-line @typescript-eslint/no-useless-constructor, @typescript-eslint/no-unused-vars 85 | constructor(renderer: BatchRenderer) 86 | { 87 | // Implementation 88 | this._renderer = renderer; 89 | } 90 | 91 | abstract init(verticesBatched: number, indiciesBatched: number): void; 92 | abstract append(displayObject: DisplayObject, batch: any): void; 93 | abstract build(): Geometry; 94 | abstract release(geom: Geometry): void; 95 | } 96 | 97 | /** 98 | * This interface defines the methods you need to implement to creating your own batch 99 | * geometry factory. 100 | * 101 | * The constructor of an implementation should take only one argument - the batch renderer. 102 | * 103 | * @interface IBatchGeometryFactory 104 | */ 105 | 106 | /** 107 | * Called before the batch renderer starts feeding the display-objects. This can be used 108 | * to pre-allocated space for the batch geometry. 109 | * 110 | * @memberof IBatchGeometryFactory# 111 | * @method init 112 | * @param {number} verticesBatched 113 | * @param {number}[indiciesBatched] - optional when display-object's don't use a index buffer 114 | */ 115 | 116 | /** 117 | * Adds the display-object to the batch geometry. 118 | * 119 | * If the display-object's shader also uses textures (in `uSamplers` uniform), then it will 120 | * be given a texture-ID to get the texture from the `uSamplers` array. If it uses multiple 121 | * textures, then the texture-ID is an array of indices into `uSamplers`. The texture-attrib 122 | * passed to the batch renderer sets the name of the texture-ID attribute (defualt is `aTextureId`). 123 | * 124 | * @memberof IBatchGeometryFactory# 125 | * @method append 126 | * @param {PIXI.DisplayObject} displayObject 127 | * @param {object} batch - the batch 128 | */ 129 | 130 | /** 131 | * This should wrap up the batch geometry in a `PIXI.Geometry` object. 132 | * 133 | * @memberof IBatchGeometryFactory# 134 | * @method build 135 | * @returns {PIXI.Geometry} batch geometry 136 | */ 137 | 138 | /** 139 | * This is used to return a batch geometry so it can be pooled and reused in a future `build()` 140 | * call. 141 | * 142 | * @memberof IBatchGeometryFactory# 143 | * @method release 144 | * @param {PIXI.Geometry} geom 145 | */ 146 | 147 | /** 148 | * Factory class that generates the geometry for a whole batch by feeding on 149 | * the individual display-object geometries. This factory is reusable, i.e. you 150 | * can build another geometry after a {@link build} call. 151 | * 152 | * **Optimizations:** To speed up geometry generation, this compiles an optimized 153 | * packing function that pushes attributes without looping through the attribute 154 | * redirects. 155 | * 156 | * **Default Format:** If you are not using a custom draw-call issuer, then 157 | * the batch geometry must have an interleaved attribute data buffer and one 158 | * index buffer. 159 | * 160 | * **Customization:** If you want to customize the batch geometry, then you must 161 | * also define your draw call issuer. 162 | * 163 | * **inBatchID Support**: If you specified an `inBatchID` attribute in the batch-renderer, 164 | * then this will support it automatically. The aggregate-uniforms pipeline doesn't need a custom 165 | * geometry factory. 166 | */ 167 | export class BatchGeometryFactory extends IBatchGeometryFactory 168 | { 169 | _targetCompositeAttributeBuffer: ViewableBuffer; 170 | _targetCompositeIndexBuffer: Uint16Array; 171 | _aIndex: number; 172 | _iIndex: number; 173 | 174 | // These properties are not protected because GeometryMerger uses them! 175 | 176 | // Standard Pipeline 177 | _attribRedirects: AttributeRedirect[]; 178 | _indexProperty: string; 179 | _vertexCountProperty: string | number | ((object: DisplayObject) => number); 180 | _vertexSize: number; 181 | _texturesPerObject: number; 182 | _textureProperty: string; 183 | _texIDAttrib: string; 184 | _inBatchIDAttrib: string; 185 | _inBatchID: number; 186 | 187 | // Uniform+Standard Pipeline 188 | _uniformIDAttrib: string; 189 | _uniformID: number; 190 | 191 | // Master-ID attribute feature 192 | _masterIDAttrib: string; 193 | 194 | /* Set to the indicies of the display-object's textures in uSamplers uniform before 195 | invoking geometryMerger(). */ 196 | protected _texID: number | number[]; 197 | 198 | protected _aBuffers: ViewableBuffer[]; 199 | protected _iBuffers: Uint16Array[]; 200 | 201 | protected _geometryPool: Array; 202 | 203 | _geometryMerger: (displayObject: DisplayObject, factory: BatchGeometryFactory) => void; 204 | 205 | /** 206 | * @param renderer 207 | */ 208 | constructor(renderer: BatchRenderer) 209 | { 210 | super(renderer); 211 | 212 | this._targetCompositeAttributeBuffer = null; 213 | this._targetCompositeIndexBuffer = null; 214 | this._aIndex = 0; 215 | this._iIndex = 0; 216 | 217 | this._attribRedirects = renderer._attribRedirects; 218 | this._indexProperty = renderer._indexProperty; 219 | this._vertexCountProperty = renderer._vertexCountProperty; 220 | this._vertexSize = AttributeRedirect.vertexSizeFor(this._attribRedirects); 221 | this._texturesPerObject = renderer._texturesPerObject; 222 | this._textureProperty = renderer._textureProperty; 223 | this._texIDAttrib = renderer._texIDAttrib; 224 | 225 | this._inBatchIDAttrib = renderer._inBatchIDAttrib; 226 | this._uniformIDAttrib = renderer._uniformIDAttrib; 227 | 228 | this._masterIDAttrib = renderer._masterIDAttrib; 229 | 230 | if (!this._masterIDAttrib) 231 | { 232 | this._vertexSize += this._texturesPerObject * 4;// texture indices are also passed 233 | 234 | if (this._inBatchIDAttrib) 235 | { 236 | this._vertexSize += 4; 237 | } 238 | if (this._uniformIDAttrib) 239 | { 240 | this._vertexSize += 4; 241 | } 242 | } 243 | else 244 | { 245 | this._vertexSize += 4; 246 | } 247 | 248 | if (this._texturesPerObject === 1) 249 | { 250 | this._texID = 0; 251 | } 252 | else if (this._texturesPerObject > 1) 253 | { 254 | this._texID = new Array(this._texturesPerObject); 255 | } 256 | 257 | this._aBuffers = [];// @see _getAttributeBuffer 258 | this._iBuffers = [];// @see _getIndexBuffer 259 | 260 | /** 261 | * Batch geometries that can be reused. 262 | * 263 | * @member {PIXI.Geometry} 264 | * @protected 265 | * @see IBatchGeometryFactory#release 266 | */ 267 | this._geometryPool = []; 268 | } 269 | 270 | /** 271 | * Ensures this factory has enough space to buffer the given number of vertices 272 | * and indices. This should be called before feeding display-objects from the 273 | * batch. 274 | * 275 | * @param {number} verticesBatched 276 | * @param {number} indiciesBatched 277 | */ 278 | init(verticesBatched: number, indiciesBatched?: number): void 279 | { 280 | this._targetCompositeAttributeBuffer = this.getAttributeBuffer(verticesBatched); 281 | 282 | if (this._indexProperty) 283 | { 284 | this._targetCompositeIndexBuffer = this.getIndexBuffer(indiciesBatched); 285 | } 286 | 287 | this._aIndex = this._iIndex = 0; 288 | } 289 | 290 | /** 291 | * Append's the display-object geometry to this batch's geometry. You must override 292 | * this you need to "modify" the geometry of the display-object before merging into 293 | * the composite geometry (for example, adding an ID to a special uniform) 294 | */ 295 | append(targetObject: DisplayObject, batch_: any): void 296 | { 297 | const batch: StdBatch = batch_ as StdBatch; 298 | const tex = (targetObject as any)[this._textureProperty]; 299 | 300 | // GeometryMerger uses _texID for texIDAttrib 301 | if (this._texturesPerObject === 1) 302 | { 303 | const texUID = tex.baseTexture ? tex.baseTexture.uid : tex.uid; 304 | 305 | this._texID = batch.uidMap[texUID]; 306 | } 307 | else if (this._texturesPerObject > 1) 308 | { 309 | let _tex; 310 | 311 | for (let k = 0; k < tex.length; k++) 312 | { 313 | _tex = tex[k]; 314 | 315 | const texUID = _tex.baseTexture ? _tex.baseTexture.uid : _tex.uid; 316 | 317 | (this._texID as number[])[k] = batch.uidMap[texUID]; 318 | } 319 | } 320 | 321 | // GeometryMerger uses this 322 | if (this._inBatchIDAttrib || this._uniformIDAttrib) 323 | { 324 | this._inBatchID = batch.batchBuffer.indexOf(targetObject); 325 | } 326 | if (this._uniformIDAttrib) 327 | { 328 | this._uniformID = (batch as AggregateUniformsBatch).uniformMap[this._inBatchID]; 329 | } 330 | 331 | // If _masterIDAttrib, then it is expected you override this function. 332 | 333 | this.geometryMerger(targetObject, this); 334 | } 335 | 336 | /** 337 | * @override 338 | * @returns {PIXI.Geometry} the generated batch geometry 339 | * @example 340 | * build(): PIXI.Geometry 341 | * { 342 | * // Make sure you're not allocating new geometries if _geometryPool has some 343 | * // already. (Otherwise, a memory leak will result!) 344 | * const geom: ExampleGeometry = (this._geometryPool.pop() || new ExampleGeometry( 345 | * // ...your arguments... //)) as ExampleGeometry; 346 | * 347 | * // Put data into geometry's buffer 348 | * 349 | * return geom; 350 | * } 351 | */ 352 | build(): Geometry 353 | { 354 | const geom: BatchGeometry = (this._geometryPool.pop() || new BatchGeometry( 355 | this._attribRedirects, 356 | true, 357 | this._texIDAttrib, 358 | this._texturesPerObject, 359 | this._inBatchIDAttrib, 360 | this._uniformIDAttrib, 361 | this._masterIDAttrib, 362 | )) as BatchGeometry; 363 | 364 | // We don't really have to remove the buffers because BatchRenderer won't reuse 365 | // the data in these buffers after the next build() call. 366 | geom.attribBuffer.update(this._targetCompositeAttributeBuffer.float32View); 367 | geom.indexBuffer.update(this._targetCompositeIndexBuffer); 368 | 369 | return geom; 370 | } 371 | 372 | /** 373 | * @param {PIXI.Geometry} geom - releases back the geometry to be reused. It is expected 374 | * that it is not used externally again. 375 | * @override 376 | */ 377 | release(geom: Geometry): void 378 | { 379 | this._geometryPool.push(geom); 380 | } 381 | 382 | /** 383 | * This lazy getter returns the geometry-merger function. This function 384 | * takes one argument - the display-object to be appended to the batch - 385 | * and pushes its geometry to the batch geometry. 386 | * 387 | * You can overwrite this property with a custom geometry-merger function 388 | * if customizing `BatchGeometryFactory`. 389 | * 390 | * @member {IGeometryMerger} 391 | */ 392 | protected get geometryMerger(): (displayObject: DisplayObject, factory: BatchGeometryFactory) => void 393 | { 394 | if (!this._geometryMerger) 395 | { 396 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 397 | this._geometryMerger = new GeometryMergerFactory(this).compile(); 398 | } 399 | 400 | return this._geometryMerger; 401 | } 402 | // eslint-disable-next-line require-jsdoc 403 | protected set geometryMerger(func: (displayObject: DisplayObject, factory: BatchGeometryFactory) => void) 404 | { 405 | this._geometryMerger = func; 406 | } 407 | 408 | /** 409 | * @protected 410 | */ 411 | get _indexCountProperty(): Resolvable 412 | { 413 | return this._renderer._indexCountProperty; 414 | } 415 | 416 | /** 417 | * Allocates an attribute buffer with sufficient capacity to hold `size` elements. 418 | * 419 | * @param {number} size 420 | * @protected 421 | */ 422 | protected getAttributeBuffer(size: number): ViewableBuffer 423 | { 424 | const roundedP2 = nextPow2(size); 425 | const roundedSizeIndex = log2(roundedP2); 426 | const roundedSize = roundedP2; 427 | 428 | if (this._aBuffers.length <= roundedSizeIndex) 429 | { 430 | this._aBuffers.length = roundedSizeIndex + 1; 431 | } 432 | 433 | let buffer = this._aBuffers[roundedSizeIndex]; 434 | 435 | if (!buffer) 436 | { 437 | this._aBuffers[roundedSizeIndex] = buffer = new ViewableBuffer(roundedSize * this._vertexSize); 438 | } 439 | 440 | return buffer; 441 | } 442 | 443 | /** 444 | * Allocates an index buffer (`Uint16Array`) with sufficient capacity to hold `size` indices. 445 | * 446 | * @param size 447 | * @protected 448 | */ 449 | protected getIndexBuffer(size: number): Uint16Array 450 | { 451 | // 12 indices is enough for 2 quads 452 | const roundedP2 = nextPow2(Math.ceil(size / 12)); 453 | const roundedSizeIndex = log2(roundedP2); 454 | const roundedSize = roundedP2 * 12; 455 | 456 | if (this._iBuffers.length <= roundedSizeIndex) 457 | { 458 | this._iBuffers.length = roundedSizeIndex + 1; 459 | } 460 | 461 | let buffer = this._iBuffers[roundedSizeIndex]; 462 | 463 | if (!buffer) 464 | { 465 | this._iBuffers[roundedSizeIndex] = buffer = new Uint16Array(roundedSize); 466 | } 467 | 468 | return buffer; 469 | } 470 | } 471 | 472 | // GeometryMergerFactory uses these variable names. 473 | const CompilerConstants = { 474 | INDICES_OFFSET: '__offset_indices_', 475 | FUNC_SOURCE_BUFFER: 'getSourceBuffer', 476 | 477 | // Argument names for the geometryMerger() function. 478 | packerArguments: [ 479 | 'targetObject', 480 | 'factory', 481 | ], 482 | }; 483 | 484 | // This was intended to be an inner class of BatchGeometryFactory; however, due to 485 | // a bug in JSDoc, it was placed outside. 486 | // https://github.com/jsdoc/jsdoc/issues/1673 487 | 488 | // Factory for generating a geometry-merger function (which appends the geometry of 489 | // a display-object to the batch geometry). 490 | const GeometryMergerFactory = class 491 | { 492 | packer: BatchGeometryFactory; 493 | 494 | // We need the BatchGeometryFactory for attribute redirect information. 495 | constructor(packer: BatchGeometryFactory) 496 | { 497 | this.packer = packer; 498 | } 499 | 500 | compile(): (displayObject: DisplayObject, factory: BatchGeometryFactory) => void 501 | { 502 | const packer = this.packer; 503 | 504 | // The function's body/code is placed here. 505 | let packerBody = ` 506 | const compositeAttributes = factory._targetCompositeAttributeBuffer; 507 | const compositeIndices = factory._targetCompositeIndexBuffer; 508 | let aIndex = factory._aIndex; 509 | let iIndex = factory._iIndex; 510 | const textureId = factory._texID; 511 | const attributeRedirects = factory._attribRedirects; 512 | `; 513 | 514 | // Define __offset_${i}, the offset of each attribute in the display-object's 515 | // geometry, __buffer_${i} the source buffer of the attribute data. 516 | packer._attribRedirects.forEach((redirect, i) => 517 | { 518 | packerBody += ` 519 | let __offset_${i} = 0; 520 | const __buffer_${i} = ( 521 | ${this.generateSourceBufferExpr(redirect, i)}); 522 | `; 523 | }); 524 | 525 | // This loops through each vertex in the display-object's geometry and appends 526 | // them (attributes are interleaved, so each attribute element is pushed per vertex) 527 | packerBody += ` 528 | const { 529 | int8View, 530 | uint8View, 531 | int16View, 532 | uint16View, 533 | int32View, 534 | uint32View, 535 | float32View, 536 | } = compositeAttributes; 537 | 538 | const vertexCount = ${this.generateVertexCountExpr()}; 539 | 540 | let adjustedAIndex = 0; 541 | 542 | for (let vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) 543 | { 544 | `; 545 | 546 | // Eliminate offset conversion when adjacent attributes 547 | // have similar source-types. 548 | let skipByteIndexConversion = false; 549 | 550 | // Appends a vertice's attributes (inside the for-loop above). 551 | for (let i = 0; i < packer._attribRedirects.length; i++) 552 | { 553 | const redirect = packer._attribRedirects[i]; 554 | 555 | // Initialize adjustedAIndex in terms of source type. 556 | if (!skipByteIndexConversion) 557 | { 558 | packerBody += ` 559 | adjustedAIndex = aIndex / ${this._sizeOf(i)}; 560 | `; 561 | } 562 | 563 | if (typeof redirect.size === 'number') 564 | { 565 | for (let j = 0; j < redirect.size; j++) 566 | { 567 | packerBody += ` 568 | ${redirect.type}View[adjustedAIndex++] = __buffer_${i}[__offset_${i}++]; 569 | `; 570 | } 571 | } 572 | else 573 | { 574 | packerBody += ` 575 | ${redirect.type}View[adjustedAIndex++] = __buffer_${i}; 576 | `; 577 | } 578 | 579 | if (packer._attribRedirects[i + 1] && (this._sizeOf(i + 1) !== this._sizeOf(i))) 580 | { 581 | packerBody += ` 582 | aIndex = adjustedAIndex * ${this._sizeOf(i)}; 583 | `; 584 | } 585 | else 586 | { 587 | skipByteIndexConversion = true; 588 | } 589 | } 590 | 591 | if (skipByteIndexConversion) 592 | { 593 | if (this._sizeOf(packer._attribRedirects.length - 1) !== 4) 594 | { 595 | packerBody += ` 596 | aIndex = adjustedAIndex * ${this._sizeOf(packer._attribRedirects.length - 1)} 597 | `; 598 | skipByteIndexConversion = false; 599 | } 600 | } 601 | 602 | if (!packer._masterIDAttrib) 603 | { 604 | if (packer._texturesPerObject > 0) 605 | { 606 | if (packer._texturesPerObject > 1) 607 | { 608 | if (!skipByteIndexConversion) 609 | { 610 | packerBody += ` 611 | adjustedAIndex = aIndex / 4; 612 | `; 613 | } 614 | 615 | for (let k = 0; k < packer._texturesPerObject; k++) 616 | { 617 | packerBody += ` 618 | float32View[adjustedAIndex++] = textureId[${k}]; 619 | `; 620 | } 621 | 622 | packerBody += ` 623 | aIndex = adjustedAIndex * 4; 624 | `; 625 | } 626 | else if (!skipByteIndexConversion) 627 | { 628 | packerBody += ` 629 | float32View[aIndex / 4] = textureId; 630 | `; 631 | } 632 | else 633 | { 634 | packerBody += ` 635 | float32View[adjustedAIndex++] = textureId; 636 | aIndex = adjustedAIndex * 4; 637 | `; 638 | } 639 | } 640 | if (packer._inBatchIDAttrib) 641 | { 642 | packerBody += ` 643 | float32View[adjustedAIndex++] = factory._inBatchID; 644 | aIndex = adjustedAIndex * 4; 645 | `; 646 | } 647 | if (packer._uniformIDAttrib) 648 | { 649 | packerBody += ` 650 | float32View[adjustedAIndex++] = factory._uniformID; 651 | aIndex = adjustedAIndex * 4; 652 | `; 653 | } 654 | } 655 | else 656 | { 657 | if (!skipByteIndexConversion) 658 | { 659 | packerBody += ` 660 | adjustedAIndex = aIndex / 4; 661 | `; 662 | } 663 | 664 | packerBody += ` 665 | float32View[adjustedAIndex++] = factory._masterID; 666 | aIndex = adjustedAIndex * 4; 667 | `; 668 | } 669 | 670 | /* Close the packing for-loop. */ 671 | packerBody += `} 672 | ${this.packer._indexProperty 673 | ? `const oldAIndex = this._aIndex;` 674 | : ''} 675 | this._aIndex = aIndex; 676 | `; 677 | 678 | if (this.packer._indexProperty) 679 | { 680 | packerBody += ` 681 | const verticesBefore = oldAIndex / ${this.packer._vertexSize}; 682 | const indexCount = ${this.generateIndexCountExpr()}; 683 | 684 | for (let j = 0; j < indexCount; j++) 685 | { 686 | compositeIndices[iIndex++] = verticesBefore + targetObject['${this.packer._indexProperty}'][j]; 687 | } 688 | 689 | this._iIndex = iIndex; 690 | `; 691 | } 692 | 693 | // eslint-disable-next-line no-new-func 694 | return new Function( 695 | ...CompilerConstants.packerArguments, 696 | packerBody) as 697 | (displayObject: DisplayObject, factory: BatchGeometryFactory) => void; 698 | } 699 | 700 | // Returns an expression that fetches the attribute data source from 701 | // targetObject (DisplayObject). 702 | generateSourceBufferExpr(redirect: Redirect, i: number): string 703 | { 704 | return (typeof redirect.source === 'string') 705 | ? `targetObject['${redirect.source}']` 706 | : `attributeRedirects[${i}].source(targetObject, factory._renderer)`; 707 | } 708 | 709 | generateVertexCountExpr(): string 710 | { 711 | if (!this.packer._vertexCountProperty) 712 | { 713 | // auto-calculate based on primary attribute 714 | return `__buffer_0.length / ${this.packer._attribRedirects[0].size}`; 715 | } 716 | 717 | if (typeof this.packer._vertexCountProperty === 'function') 718 | { 719 | return `factory._vertexCountProperty(targetObject)`; 720 | } 721 | 722 | return ( 723 | (typeof this.packer._vertexCountProperty === 'string') 724 | ? `targetObject.${this.packer._vertexCountProperty}` 725 | : `${this.packer._vertexCountProperty}` 726 | ); 727 | } 728 | 729 | generateIndexCountExpr(): string 730 | { 731 | const idxCountProp = this.packer._indexCountProperty; 732 | const idxProp = this.packer._indexProperty; 733 | 734 | if (!idxCountProp) 735 | { 736 | return `targetObject['${idxProp}'].length`; 737 | } 738 | 739 | if (typeof idxCountProp === 'function') 740 | { 741 | return `factory._indexCountProperty(targetObject)`; 742 | } 743 | else if (typeof idxCountProp === 'string') 744 | { 745 | return `targetObject['${idxCountProp}']`; 746 | } 747 | 748 | return `${idxCountProp}`; 749 | } 750 | 751 | _sizeOf(i: number): number 752 | { 753 | return ViewableBuffer.sizeOf( 754 | this.packer._attribRedirects[i].type); 755 | } 756 | }; 757 | 758 | export default BatchGeometryFactory; 759 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/BatchRenderer.ts: -------------------------------------------------------------------------------- 1 | import { StdBatchFactory } from './StdBatchFactory'; 2 | import { BatchGeometryFactory } from './BatchGeometryFactory'; 3 | import { AttributeRedirect } from './redirects/AttributeRedirect'; 4 | import { BatchDrawer } from './BatchDrawer'; 5 | import { ENV } from '@pixi/constants'; 6 | import { ObjectRenderer, State } from '@pixi/core'; 7 | import { UniformRedirect } from './redirects/UniformRedirect'; 8 | import { resolve } from './utils/resolveProperty'; 9 | import { resolveFunctionOrProperty } from './resolve'; 10 | import { settings } from '@pixi/settings'; 11 | 12 | import type { 13 | Renderer, 14 | Shader 15 | } from '@pixi/core'; 16 | import type { DisplayObject } from '@pixi/display'; 17 | 18 | export interface IBatchRendererOptions 19 | { 20 | // Standard pipeline 21 | attribSet: AttributeRedirect[]; 22 | indexProperty: string; 23 | indexCountProperty?: string | number | ((object: DisplayObject) => number); 24 | vertexCountProperty?: string | number | ((object: DisplayObject) => number); 25 | textureProperty: string; 26 | texturesPerObject?: number; 27 | texIDAttrib: string; 28 | inBatchIDAttrib?: string; 29 | masterIDAttrib?: string; 30 | stateFunction?: (renderer: DisplayObject) => State; 31 | shaderFunction: (renderer: BatchRenderer) => Shader; 32 | 33 | // Components 34 | BatchFactoryClass?: typeof StdBatchFactory; 35 | BatchGeometryFactoryClass?: typeof BatchGeometryFactory; 36 | BatchDrawerClass?: typeof BatchDrawer; 37 | 38 | // Uniforms+Standard Pipeline 39 | uniformSet?: UniformRedirect[]; 40 | uniformIDAttrib?: string; 41 | } 42 | 43 | /** 44 | * This object renderer renders multiple display-objects in batches. It can greatly 45 | * reduce the number of draw calls issued per frame. 46 | * 47 | * ## Batch Rendering Pipeline 48 | * 49 | * The batch rendering pipeline consists of the following stages: 50 | * 51 | * * **Display-Object Buffering**: Each display-object is kept in a buffer until it fills up or a 52 | * flush is required. 53 | * 54 | * * **Batch Generation**: In a sliding window, display-object batches are generated based off of certain 55 | * constraints like GPU texture units and the uniforms used in each display-object. This is done using an 56 | * instance of {@link BatchFactory}. 57 | * 58 | * * **Geometry Composition**: The geometries of all display-objects are merged together in a 59 | * composite geometry. This is done using an instance of {@link BatchGeometryFactory}. 60 | * 61 | * * **Drawing**: Each batch is rendered in-order using `gl.draw*`. The textures and 62 | * uniforms of each display-object are uploaded as arrays. This is done using an instance of 63 | * {@link BatchDrawer}. 64 | * 65 | * Each stage in this pipeline can be configured by overriding the appropriate component and passing that 66 | * class to `BatchRendererPluginFactory.from*`. 67 | * 68 | * ## Shaders 69 | * 70 | * ### Shader templates 71 | * 72 | * Since the max. display-object count per batch is not known until the WebGL context is created, 73 | * shaders are generated at runtime by processing shader templates. A shader templates has "%macros%" 74 | * that are replaced by constants at runtime. 75 | * 76 | * To use shader templates, simply use {@link BatchShaderFactory#derive}. This will generate a 77 | * function that derives a shader from your template at runtime. 78 | * 79 | * ### Textures 80 | * 81 | * The batch renderer uploads textures in the `uniform sampler2D uSamplers[%texturesPerBatch%];`. The 82 | * `varying float vTextureId` defines the index into this array that holds the current display-object's 83 | * texture. 84 | * 85 | * ### Uniforms 86 | * 87 | * This renderer currently does not support customized uniforms for display-objects. This is a 88 | * work-in-progress feature. 89 | * 90 | * ## Learn more 91 | * This batch renderer uses the PixiJS object-renderer API to hook itself: 92 | * 93 | * 1. [PIXI.ObjectRenderer]{@link http://pixijs.download/release/docs/PIXI.ObjectRenderer.html} 94 | * 95 | * 2. [PIXI.AbstractBatchRenderer]{@link http://pixijs.download/release/docs/PIXI.AbstractBatchRenderer.html} 96 | * 97 | * @example 98 | * import * as PIXI from 'pixi.js'; 99 | * import { BatchRendererPluginFactory } from 'pixi-batch-renderer'; 100 | * 101 | * // Define the geometry of your display-object and create a BatchRenderer using 102 | * // BatchRendererPluginFactory. Register it as a plugin with PIXI.Renderer. 103 | * PIXI.Renderer.registerPlugin('ExampleBatchRenderer', BatchRendererPluginFactory.from(...)); 104 | * 105 | * class ExampleObject extends PIXI.Container 106 | * { 107 | * _render(renderer: PIXI.Renderer): void 108 | * { 109 | * // BatchRenderer will handle the whole rendering process for you! 110 | * renderer.batch.setObjectRenderer(renderer.plugins['ExampleBatchRenderer']); 111 | * renderer.plugins['ExampleBatchRenderer'].render(this); 112 | * } 113 | * } 114 | */ 115 | export class BatchRenderer extends ObjectRenderer 116 | { 117 | /** @protected */ 118 | public renderer: Renderer; 119 | 120 | // Standard pipeline 121 | /** 122 | * Attribute redirects 123 | * 124 | * @access protected 125 | */ 126 | readonly _attribRedirects: AttributeRedirect[]; 127 | 128 | /** 129 | * Indices property 130 | * 131 | * @access protected 132 | */ 133 | readonly _indexProperty: string; 134 | 135 | /** 136 | * A manual resolution of the number of indicies in a display object's geometry. This is ignored if the 137 | * index buffer is not used (see _indexProperty). If not provided, the index buffer's entire length 138 | * is used. 139 | * 140 | * @access protected 141 | */ 142 | readonly _indexCountProperty: string | number | ((object: DisplayObject) => number); 143 | 144 | /** 145 | * A manual resolution of the number of vertices in a display object's geometry. If not provided, this is 146 | * calculated as the number of element in the first attribute's buffer. 147 | * 148 | * @access protected 149 | */ 150 | readonly _vertexCountProperty: string | number | ((object: DisplayObject) => number); 151 | readonly _textureProperty: string; 152 | readonly _texturesPerObject: number; 153 | readonly _texIDAttrib: string; 154 | readonly _inBatchIDAttrib: string; 155 | readonly _stateFunction: Function; 156 | readonly _shaderFunction: Function; 157 | 158 | // Uniforms+Standard Pipeline 159 | readonly _uniformRedirects: UniformRedirect[]; 160 | readonly _uniformIDAttrib: string; 161 | 162 | // Master-ID optimization 163 | readonly _masterIDAttrib: string; 164 | 165 | // API Visiblity Note: These properties are used by component/factories and must be public; 166 | // however, they are prefixed with an underscore because they are not for exposure to the end-user. 167 | 168 | // Components 169 | _batchFactory: StdBatchFactory; 170 | _geometryFactory: BatchGeometryFactory; 171 | _drawer: BatchDrawer; 172 | 173 | // Display-object buffering 174 | _objectBuffer: DisplayObject[]; 175 | _bufferedVertices: number; 176 | _bufferedIndices: number; 177 | 178 | // Drawer 179 | _shader: Shader; 180 | 181 | // WebGL Context config 182 | MAX_TEXTURES: number; 183 | 184 | // Component ctors 185 | protected readonly _BatchFactoryClass: typeof StdBatchFactory; 186 | protected readonly _BatchGeometryFactoryClass: typeof BatchGeometryFactory; 187 | protected readonly _BatchDrawerClass: typeof BatchDrawer; 188 | 189 | // Additional args 190 | protected readonly options: any; 191 | 192 | /** 193 | * Creates a batch renderer the renders display-objects with the described geometry. 194 | * 195 | * To register a batch-renderer plugin, you must use the API provided by `BatchRendererPluginFactory`. 196 | * 197 | * @param {PIXI.Renderer} renderer - renderer to attach to 198 | * @param {object} options 199 | * @param {AttributeRedirect[]} options.attribSet 200 | * @param {string | null} options.indexProperty 201 | * @param {string | number} [options.vertexCountProperty] 202 | * @param {string | null} options.textureProperty 203 | * @param {number} [options.texturesPerObject=1] 204 | * @param {string} options.texIDAttrib - name of texture-id attribute variable 205 | * @param {Function}[options.stateFunction=PIXI.State.for2d()] - returns a `PIXI.State` for an object 206 | * @param {Function} options.shaderFunction - generates a shader given this instance 207 | * @param {Class} [options.BatchGeometryFactory=BatchGeometry] 208 | * @param {Class} [options.BatchFactoryClass=StdBatchFactory] 209 | * @param {Class} [options.BatchDrawer=BatchDrawer] 210 | * @see BatchShaderFactory 211 | * @see StdBatchFactory 212 | * @see BatchGeometryFactory 213 | * @see BatchDrawer 214 | */ 215 | constructor(renderer: Renderer, options: IBatchRendererOptions) 216 | { 217 | super(renderer); 218 | 219 | this._attribRedirects = options.attribSet; 220 | this._indexProperty = options.indexProperty; 221 | this._indexCountProperty = options.indexCountProperty; 222 | this._vertexCountProperty = options.vertexCountProperty; 223 | 224 | /** 225 | * Texture(s) property 226 | * @member {string} 227 | * @protected 228 | * @readonly 229 | */ 230 | this._textureProperty = options.textureProperty; 231 | 232 | /** 233 | * Textures per display-object 234 | * @member {number} 235 | * @protected 236 | * @readonly 237 | * @default 1 238 | */ 239 | this._texturesPerObject = typeof options.texturesPerObject !== 'undefined' ? options.texturesPerObject : 1; 240 | 241 | /** 242 | * Texture ID attribute 243 | * @member {string} 244 | * @protected 245 | * @readonly 246 | */ 247 | this._texIDAttrib = options.texIDAttrib; 248 | 249 | /** 250 | * Indexes the display-object in the batch. 251 | * @member {string} 252 | * @protected 253 | * @readonly 254 | */ 255 | this._inBatchIDAttrib = options.inBatchIDAttrib; 256 | 257 | /** 258 | * State generating function (takes a display-object) 259 | * @member {Function} 260 | * @default () => PIXI.State.for2d() 261 | * @protected 262 | * @readonly 263 | */ 264 | this._stateFunction = options.stateFunction || ((): State => State.for2d()); 265 | 266 | /** 267 | * Shader generating function (takes the batch renderer) 268 | * @member {Function} 269 | * @protected 270 | * @see BatchShaderFactory 271 | * @readonly 272 | */ 273 | this._shaderFunction = options.shaderFunction; 274 | 275 | /** 276 | * Batch-factory class. 277 | * @member {Class} 278 | * @protected 279 | * @default StdBatchFactory 280 | * @readonly 281 | */ 282 | this._BatchFactoryClass = options.BatchFactoryClass || StdBatchFactory; 283 | 284 | /** 285 | * Batch-geometry factory class. Its constructor takes one argument - this batch renderer. 286 | * @member {Class} 287 | * @protected 288 | * @default BatchGeometryFactory 289 | * @readonly 290 | */ 291 | this._BatchGeometryFactoryClass = options.BatchGeometryFactoryClass || BatchGeometryFactory; 292 | 293 | /** 294 | * Batch drawer class. Its constructor takes one argument - this batch renderer. 295 | * @member {Class} 296 | * @protected 297 | * @default BatchDrawer 298 | * @readonly 299 | */ 300 | this._BatchDrawerClass = options.BatchDrawerClass || BatchDrawer; 301 | 302 | /** 303 | * Uniform redirects. If you use uniforms in your shader, be sure to use one the compatible 304 | * batch factories (like {@link AggregateUniformsBatchFactory}). 305 | * @member {UniformRedirect[]} 306 | * @protected 307 | * @default null 308 | * @readonly 309 | */ 310 | this._uniformRedirects = options.uniformSet || null; 311 | 312 | /** 313 | * Indexes the uniforms of the display-object in the uniform arrays. This is not equal to the 314 | * in-batch ID because equal uniforms are not uploaded twice. 315 | * @member {string} 316 | * @protected 317 | * @readonly 318 | */ 319 | this._uniformIDAttrib = options.uniformIDAttrib; 320 | 321 | /** 322 | * This is an advanced feature that allows you to pack the {@code _texIDAttrib}, {@code _uniformIDAttrib}, 323 | * {@code _inBatchIDAttrib}, and other information into one 32-bit float attribute. You can then unpack 324 | * them in the vertex shader and pass varyings to the fragment shader (because {@code int} varyings are not 325 | * supported). 326 | * 327 | * To use it, you must provide your own {@link BatchGeometryFactory} that overrides 328 | * {@link BatchGeometryFactory#append} and sets the {@code _masterIDAttrib}. 329 | */ 330 | this._masterIDAttrib = options.masterIDAttrib; 331 | 332 | /** 333 | * The options used to create this batch renderer. 334 | * @readonly {object} 335 | * @protected 336 | * @readonly 337 | */ 338 | this.options = options; 339 | 340 | if (options.masterIDAttrib) 341 | { 342 | this._texIDAttrib = this._masterIDAttrib; 343 | this._uniformIDAttrib = this._masterIDAttrib; 344 | this._inBatchIDAttrib = this._masterIDAttrib; 345 | } 346 | 347 | // Although the runners property is not a public API, it is required to 348 | // handle contextChange events. 349 | this.renderer.runners.contextChange.add(this); 350 | 351 | // If the WebGL context has already been created, initialization requires a 352 | // syntheic call to contextChange. 353 | if (this.renderer.gl) 354 | { 355 | this.contextChange(); 356 | } 357 | 358 | this._objectBuffer = []; 359 | this._bufferedVertices = 0; 360 | this._bufferedIndices = 0; 361 | this._shader = null; 362 | } 363 | 364 | /** 365 | * Internal method that is called whenever the renderer's WebGL context changes. 366 | */ 367 | contextChange(): void 368 | { 369 | const gl = this.renderer.gl; 370 | 371 | if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) 372 | { 373 | this.MAX_TEXTURES = 1; 374 | } 375 | else 376 | { 377 | this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); 378 | } 379 | 380 | /** 381 | * @member {_BatchFactoryClass} 382 | * @readonly 383 | * @protected 384 | */ 385 | this._batchFactory = new this._BatchFactoryClass(this); 386 | 387 | /** 388 | * @member {_BatchGeometryFactoryClass} 389 | * @readonly 390 | * @protected 391 | */ 392 | this._geometryFactory = new this._BatchGeometryFactoryClass(this); 393 | 394 | /** 395 | * @member {_BatchDrawerClass} 396 | * @readonly 397 | * @protected 398 | */ 399 | this._drawer = new this._BatchDrawerClass(this); 400 | } 401 | 402 | /** 403 | * This is an internal method. It ensures that the batch renderer is ready to start buffering display-objects. 404 | * This is automatically invoked by the renderer's batch system. 405 | * 406 | * @override 407 | */ 408 | start(): void 409 | { 410 | this._objectBuffer.length = 0; 411 | this._bufferedVertices = 0; 412 | this._bufferedIndices = 0; 413 | 414 | this._shader = this._shaderFunction(this); 415 | } 416 | 417 | /** 418 | * Adds the display-object to be rendered in a batch. Your display-object's render/_render method should call 419 | * this as follows: 420 | * 421 | * ```js 422 | * renderer.setObjectRenderer(); 423 | * .render(this); 424 | * ``` 425 | * 426 | * @override 427 | */ 428 | render(displayObject: DisplayObject): void 429 | { 430 | this._objectBuffer.push(displayObject); 431 | 432 | this._bufferedVertices += this.calculateVertexCount(displayObject); 433 | 434 | if (this._indexProperty) 435 | { 436 | this._bufferedIndices += this.calculateIndexCount(displayObject); 437 | } 438 | } 439 | 440 | /** 441 | * Forces buffered display-objects to be rendered immediately. This should not be called unless absolutely 442 | * necessary like the following scenarios: 443 | * 444 | * * before directly rendering your display-object, to preserve render-order. 445 | * 446 | * * to do a nested render pass (calling `Renderer#render` inside a `render` method) 447 | * because the PixiJS renderer is not re-entrant. 448 | * 449 | * @override 450 | */ 451 | flush(): void 452 | { 453 | const { _batchFactory: batchFactory, _geometryFactory: geometryFactory, _stateFunction: stateFunction } = this; 454 | const buffer = this._objectBuffer; 455 | const bufferLength = buffer.length; 456 | 457 | // Reset components 458 | batchFactory.reset(); 459 | geometryFactory.init(this._bufferedVertices, this._bufferedIndices); 460 | 461 | let batchStart = 0; 462 | 463 | // Loop through display-objects and create batches 464 | for (let objectIndex = 0; objectIndex < bufferLength;) 465 | { 466 | const target = buffer[objectIndex]; 467 | const wasPut = batchFactory.put(target, resolveFunctionOrProperty(target, stateFunction)); 468 | 469 | if (!wasPut) 470 | { 471 | batchFactory.build(batchStart); 472 | batchStart = objectIndex; 473 | } 474 | else 475 | { 476 | ++objectIndex; 477 | } 478 | } 479 | 480 | // Generate the last batch, if required. 481 | if (!batchFactory.ready()) 482 | { 483 | batchFactory.build(batchStart); 484 | } 485 | 486 | const batchList = batchFactory.access(); 487 | const batchCount = batchFactory.size(); 488 | let indices = 0; 489 | 490 | // Loop through batches and their display-object list to compose geometry 491 | for (let i = 0; i < batchCount; i++)// loop-per(batch) 492 | { 493 | const batch = batchList[i]; 494 | const batchBuffer = batch.batchBuffer; 495 | const batchLength = batchBuffer.length; 496 | 497 | let vertexCount = 0; 498 | let indexCount = 0; 499 | 500 | batch.geometryOffset = indices; 501 | 502 | for (let j = 0; j < batchLength; j++)// loop-per(targetObject) 503 | { 504 | const targetObject = batchBuffer[j]; 505 | 506 | if (this._indexProperty) 507 | { 508 | indexCount += this.calculateIndexCount(targetObject); 509 | } 510 | else 511 | { 512 | vertexCount += this.calculateVertexCount(targetObject); 513 | } 514 | 515 | geometryFactory.append(targetObject, batch); 516 | } 517 | 518 | // externally-defined properties for draw calls 519 | batch.$vertexCount = vertexCount; 520 | batch.$indexCount = indexCount; 521 | 522 | indices += batch.$indexCount; 523 | } 524 | 525 | // BatchDrawer handles the rest! 526 | this._drawer.draw(); 527 | } 528 | 529 | /** 530 | * Internal method that stops buffering of display-objects and flushes any existing buffers. 531 | * 532 | * @override 533 | */ 534 | stop(): void 535 | { 536 | if (this._bufferedVertices) 537 | { 538 | this.flush(); 539 | } 540 | } 541 | 542 | /** 543 | * Calculates the number of vertices in the display object's geometry. 544 | * 545 | * @param object 546 | */ 547 | protected calculateVertexCount(object: DisplayObject): number 548 | { 549 | return resolve( 550 | object, 551 | this._vertexCountProperty, 552 | resolve>(object, this._attribRedirects[0].source).length / (this._attribRedirects[0].size as number) 553 | ); 554 | } 555 | 556 | /** 557 | * Calculates the number of indices in the display object's geometry. 558 | * 559 | * @param object 560 | */ 561 | protected calculateIndexCount(object: DisplayObject): number 562 | { 563 | if (!this._indexProperty) 564 | { 565 | return 0; 566 | } 567 | 568 | return resolve( 569 | object, 570 | this._indexCountProperty, 571 | resolve(object, this._indexProperty).length, 572 | ); 573 | } 574 | } 575 | 576 | export default BatchRenderer; 577 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/BatchRendererPluginFactory.ts: -------------------------------------------------------------------------------- 1 | import { BatchRenderer } from './BatchRenderer'; 2 | import { AttributeRedirect } from './redirects/AttributeRedirect'; 3 | import BatchGeometryFactory from './BatchGeometryFactory'; 4 | import StdBatchFactory from './StdBatchFactory'; 5 | 6 | import type { BatchDrawer } from './BatchDrawer'; 7 | import type { DisplayObject } from '@pixi/display'; 8 | import type { Renderer } from '@pixi/core'; 9 | import type { UniformRedirect } from './redirects'; 10 | 11 | // (Uniforms?)+Geometry+Textures is the standard pipeline in Pixi's AbstractBatchRenderer. 12 | export interface IBatchRendererStdOptions 13 | { 14 | attribSet: AttributeRedirect[]; 15 | vertexCountProperty?: string | number | ((object: DisplayObject) => number); 16 | indexCountProperty?: string | number | ((object: DisplayObject) => number); 17 | indexProperty: string; 18 | textureProperty: string; 19 | texturesPerObject?: number; 20 | texIDAttrib: string; 21 | inBatchIDAttrib?: string; 22 | styleIDAttrib?: string; 23 | stateFunction?: (brend: DisplayObject) => any; 24 | shaderFunction: (brend: BatchRenderer) => any; 25 | BatchFactoryClass?: typeof StdBatchFactory; 26 | BatchRendererClass?: typeof BatchRenderer; 27 | BatchGeometryFactoryClass?: typeof BatchGeometryFactory; 28 | BatchDrawerClass?: typeof BatchDrawer; 29 | 30 | uniformSet?: UniformRedirect[]; 31 | uniformIDAttrib?: string; 32 | } 33 | 34 | /** 35 | * Factory class for creating a batch-renderer. 36 | * 37 | * @example 38 | * import * as PIXI from 'pixi.js'; 39 | * import { AttributeRedirect, BatchShaderFactory, BatchRendererPluginFactory } from 'pixi-batch-renderer'; 40 | * 41 | * // Define the geometry of Sprite. 42 | * const attribSet = [ 43 | * // Sprite vertexData contains global coordinates of the corners 44 | * new AttributeRedirect({ 45 | * source: 'vertexData', 46 | * attrib: 'aVertex', 47 | * type: 'float32', 48 | * size: 2, 49 | * glType: PIXI.TYPES.FLOAT, 50 | * glSize: 2, 51 | * }), 52 | * // Sprite uvs contains the normalized texture coordinates for each corner/vertex 53 | * new AttributeRedirect({ 54 | * source: 'uvs', 55 | * attrib: 'aTextureCoord', 56 | * type: 'float32', 57 | * size: 2, 58 | * glType: PIXI.TYPES.FLOAT, 59 | * glSize: 2, 60 | * }), 61 | * ]; 62 | * 63 | * const shaderFunction = new BatchShaderFactory(// 1. vertexShader 64 | * ` 65 | * attribute vec2 aVertex; 66 | * attribute vec2 aTextureCoord; 67 | * attribute float aTextureId; 68 | * 69 | * varying float vTextureId; 70 | * varying vec2 vTextureCoord; 71 | * 72 | * uniform mat3 projectionMatrix; 73 | * 74 | * void main() { 75 | * gl_Position = vec4((projectionMatrix * vec3(aVertex, 1)).xy, 0, 1); 76 | * vTextureId = aTextureId; 77 | * vTextureCoord = aTextureCoord; 78 | * } 79 | * `, 80 | * ` 81 | * uniform sampler2D uSamplers[%texturesPerBatch%]; 82 | * varying float vTextureId; 83 | * varying vec2 vTextureCoord; 84 | * 85 | * void main(void){ 86 | * vec4 color; 87 | * 88 | * // get color, which is the pixel in texture uSamplers[vTextureId] at vTextureCoord 89 | * for (int k = 0; k < %texturesPerBatch%; ++k) { 90 | * if (int(vTextureId) == k) { 91 | * color = texture2D(uSamplers[k], vTextureCoord); 92 | * break; 93 | * } 94 | * } 95 | * 96 | * gl_FragColor = color; 97 | * } 98 | * `, 99 | * {// we don't use any uniforms except uSamplers, which is handled by default! 100 | * }, 101 | * // no custom template injectors 102 | * // disable vertex shader macros by default 103 | * ).derive(); 104 | * 105 | * // Produce the SpriteBatchRenderer class! 106 | * const SpriteBatchRenderer = BatchRendererPluginFactory.from({ 107 | * attribSet, 108 | * indexProperty: 'indices', 109 | * textureProperty: 'texture', 110 | * texturesPerObject: 1, // default 111 | * texIDAttrib: 'aTextureId', 112 | * stateFunction: () => PIXI.State.for2d(), // default 113 | * shaderFunction 114 | * }); 115 | * 116 | * PIXI.Renderer.registerPlugin('customBatch', SpriteBatchRenderer); 117 | * 118 | * // Sprite will render using SpriteBatchRenderer instead of default PixiJS 119 | * // batch renderer. Instead of targetting PIXI.Sprite, you can write a batch 120 | * // renderer for a custom display-object too! (See main page for that example!) 121 | * const exampleSprite = PIXI.Sprite.from('./asset/example.png'); 122 | * exampleSprite.pluginName = 'customBatch'; 123 | * exampleSprite.width = 128; 124 | * exampleSprite.height = 128; 125 | */ 126 | export class BatchRendererPluginFactory 127 | { 128 | /** 129 | * Generates a fully customized `BatchRenderer` that aggregates primitives and textures. This is useful 130 | * for non-uniform based display-objects. 131 | * 132 | * @param {object} options 133 | * @param {AttributeRedirect[]} options.attribSet - set of geometry attributes 134 | * @param {string | Array} options.indexProperty - no. of indices on display-object 135 | * @param {string | number | function(DisplayObject): number}[options.vertexCountProperty] - no. of vertices on display-object 136 | * @param {string | number | function(DisplayObject): number}[options.indexCountProperty] - no. of indicies on display object 137 | * @param {string} options.textureProperty - textures used in display-object 138 | * @param {number} options.texturePerObject - no. of textures used per display-object 139 | * @param {string} options.texIDAttrib - used to find texture for each display-object (index into array) 140 | * @param {string} options.uniformIDAttrib - used to find the uniform data for each display-object (index into array) 141 | * @param {string} options.inBatchIDAttrib - used get the index of the object in the batch 142 | * @param {string} options.masterIDAttrib - used to combine texture-ID, batch-ID, uniform-ID and other 143 | * information into one attribute. This is an advanced optimization. It is expected you override 144 | * {@code BatchGeometryFactory#append} and supply the `_masterID` property. 145 | * @param {string | Function}[options.stateFunction= ()=>PIXI.State.for2d()] - callback that finds the WebGL 146 | * state required for display-object shader 147 | * @param {Function} options.shaderFunction - shader generator function 148 | * @param {Class}[options.BatchGeometryFactoryClass] - custom batch geometry factory class 149 | * @param {Class} [options.BatchFactoryClass] - custom batch factory class 150 | * @param {Class} [options.BatchRendererClass] - custom batch renderer class 151 | * @param {Class} [options.BatchDrawerClass] - custom batch drawer class 152 | * @static 153 | */ 154 | static from(options: IBatchRendererStdOptions): typeof BatchRenderer 155 | { 156 | // This class wraps around BatchRendererClass's constructor and passes the options from the outer scope. 157 | return class extends (options.BatchRendererClass || BatchRenderer) 158 | { 159 | constructor(renderer: Renderer) 160 | { 161 | super(renderer, options); 162 | } 163 | }; 164 | } 165 | } 166 | 167 | export default BatchRendererPluginFactory; 168 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/BatchShaderFactory.ts: -------------------------------------------------------------------------------- 1 | import { AggregateUniformsBatchFactory } from './AggregateUniformsBatchFactory'; 2 | import { Shader } from '@pixi/core'; 3 | 4 | import type { BatchRenderer } from './BatchRenderer'; 5 | 6 | // This file might need a cleanup :) 7 | 8 | // JavaScript is stupid enough not to have a replaceAll in String. This is a temporary 9 | // solution and we should depend on an actually polyfill. 10 | function _replaceAll(target: string, search: string, replacement: string): string 11 | { 12 | return target.replace(new RegExp(search, 'g'), replacement); 13 | } 14 | 15 | function injectTexturesPerBatch(batchRenderer: BatchRenderer): string 16 | { 17 | return `${batchRenderer.MAX_TEXTURES}`; 18 | } 19 | 20 | const INJECTORS = { 21 | uniformsPerBatch(renderer: BatchRenderer): string 22 | { 23 | return `${(renderer._batchFactory as AggregateUniformsBatchFactory).MAX_UNIFORMS}`; 24 | }, 25 | }; 26 | 27 | /** 28 | * Exposes an easy-to-use API for generating shader-functions to use in 29 | * the batch renderer! 30 | * 31 | * You are required to provide an injector map, which maps macros to functions 32 | * that return a string value for those macros given a renderer. By default, only one 33 | * injector is used - the textures per batch `%texturesPerBatch%` macro. This is replaced by 34 | * the number of textures passed to the `uSamplers` textures uniform. 35 | * 36 | * **Built-in Injectors**: 37 | * 38 | * * `%texturesPerBatch%`: replaced by the max. textures allowed by WebGL context 39 | * 40 | * * `%uniformsPerBatch%`: replaced by the (aggregate-uniforms) batch factory's `MAX_UNIFORMS` property. 41 | */ 42 | export class BatchShaderFactory 43 | { 44 | protected _vertexShaderTemplate: string; 45 | protected _fragmentShaderTemplate: string; 46 | protected _uniforms: any; 47 | protected _templateInjectors: any; 48 | 49 | protected disableVertexShaderTemplate: boolean; 50 | 51 | protected _cache: any; 52 | protected _cState: any; 53 | 54 | /** 55 | * WARNING: Do not pass `uSamplers` in your uniforms. They 56 | * will be added to your shader instance directly. 57 | * 58 | * @param {string} vertexShaderTemplate 59 | * @param {string} fragmentShaderTemplate 60 | * @param {UniformGroup | Map} uniforms 61 | * @param {Object.} [templateInjectors] 62 | * @param {boolean} [disableVertexShaderTemplate=true] - turn off (true) 63 | * if you aren't using macros in the vertex shader 64 | */ 65 | constructor( 66 | vertexShaderTemplate: string, 67 | fragmentShaderTemplate: string, 68 | uniforms = {}, 69 | templateInjectors: any = {}, 70 | disableVertexShaderTemplate = true, 71 | ) 72 | { 73 | if (!templateInjectors['%texturesPerBatch%']) 74 | { 75 | templateInjectors['%texturesPerBatch%'] = injectTexturesPerBatch; 76 | } 77 | if (!templateInjectors['%uniformsPerBatch%']) 78 | { 79 | templateInjectors['%uniformsPerBatch%'] = INJECTORS.uniformsPerBatch; 80 | } 81 | 82 | this._vertexShaderTemplate = vertexShaderTemplate; 83 | this._fragmentShaderTemplate = fragmentShaderTemplate; 84 | this._uniforms = uniforms; 85 | this._templateInjectors = templateInjectors; 86 | 87 | /** 88 | * Disable vertex shader templates to speed up shader 89 | * generation. 90 | * 91 | * @member {Boolean} 92 | */ 93 | this.disableVertexShaderTemplate = disableVertexShaderTemplate; 94 | 95 | /** 96 | * Maps the stringifed state of the batch renderer to the 97 | * generated shader. 98 | * 99 | * @private 100 | * @member {Object.} 101 | */ 102 | this._cache = {}; 103 | 104 | /** 105 | * Unstringifed current state of the batch renderer. 106 | * 107 | * @private 108 | * @member {Object.} 109 | * @see {ShaderGenerator#_generateInjectorBasedState} 110 | */ 111 | this._cState = null; 112 | } 113 | 114 | /** 115 | * This essentially returns a function for generating the shader for a batch 116 | * renderer. 117 | * 118 | * @return shader function that can be given to the batch renderer 119 | */ 120 | derive(): (brend: BatchRenderer) => Shader 121 | { 122 | return (batchRenderer: BatchRenderer): Shader => 123 | { 124 | const stringState = this._generateInjectorBasedState(batchRenderer); 125 | const cachedShader = this._cache[stringState]; 126 | 127 | if (cachedShader) 128 | { 129 | return cachedShader; 130 | } 131 | 132 | return this._generateShader(stringState, batchRenderer); 133 | }; 134 | } 135 | 136 | protected _generateInjectorBasedState(batchRenderer: BatchRenderer): string 137 | { 138 | let state = ''; 139 | const cState: any = this._cState = {}; 140 | 141 | for (const injectorMacro in this._templateInjectors) 142 | { 143 | const val = this._templateInjectors[injectorMacro](batchRenderer); 144 | 145 | state += val; 146 | cState[injectorMacro] = val; 147 | } 148 | 149 | return state; 150 | } 151 | 152 | protected _generateShader(stringState: string, renderer: BatchRenderer): Shader 153 | { 154 | let vertexShaderTemplate = this._vertexShaderTemplate.slice(0); 155 | 156 | let fragmentShaderTemplate = this._fragmentShaderTemplate.slice(0); 157 | 158 | for (const injectorTemplate in this._cState) 159 | { 160 | if (!this.disableVertexShaderTemplate) 161 | { 162 | vertexShaderTemplate = _replaceAll(vertexShaderTemplate, 163 | injectorTemplate, this._cState[injectorTemplate]); 164 | } 165 | 166 | fragmentShaderTemplate = _replaceAll(fragmentShaderTemplate, 167 | injectorTemplate, this._cState[injectorTemplate]); 168 | } 169 | 170 | const shader = Shader.from(vertexShaderTemplate, 171 | fragmentShaderTemplate, this._uniforms); 172 | 173 | const textures = new Array(renderer.MAX_TEXTURES); 174 | 175 | for (let i = 0; i < textures.length; i++) 176 | { 177 | textures[i] = i; 178 | } 179 | shader.uniforms.uSamplers = textures; 180 | 181 | this._cache[stringState] = shader; 182 | 183 | return shader; 184 | } 185 | } 186 | 187 | export default BatchShaderFactory; 188 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/StdBatch.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BaseTexture, 3 | Renderer, 4 | State, 5 | } from '@pixi/core'; 6 | import type { DisplayObject } from '@pixi/display'; 7 | 8 | /** 9 | * Resources that need to be uploaded to WebGL to render one batch. 10 | * 11 | * To customize batches, you must create your own batch factory by extending the 12 | * {@link StdBatchFactory} class. 13 | */ 14 | export class StdBatch 15 | { 16 | geometryOffset: number; 17 | uidMap: any; 18 | state: State; 19 | 20 | batchBuffer: Array; 21 | textureBuffer: Array; 22 | 23 | constructor(geometryOffset?: number) 24 | { 25 | /** 26 | * Index of the first vertex of this batch's geometry in the uploaded geometry. 27 | * 28 | * @member {number} 29 | */ 30 | this.geometryOffset = geometryOffset; 31 | 32 | /** 33 | * Textures that are used by the display-object's in this batch. 34 | * 35 | * @member {Array} 36 | */ 37 | this.textureBuffer = null; 38 | 39 | /** 40 | * Map of base-texture UIDs to texture indices into `uSamplers`. 41 | * 42 | * @member {Map} 43 | */ 44 | this.uidMap = null; 45 | 46 | /** 47 | * State required to render this batch. 48 | * 49 | * @member {PIXI.State} 50 | */ 51 | this.state = null; 52 | } 53 | 54 | /** 55 | * Uploads the resources required before rendering this batch. If you override 56 | * this, you must call `super.upload`. 57 | * 58 | * @param {PIXI.Renderer} renderer 59 | */ 60 | upload(renderer: Renderer): void 61 | { 62 | this.textureBuffer.forEach((tex, i) => 63 | { 64 | renderer.texture.bind(tex, i); 65 | }); 66 | 67 | renderer.state.set(this.state); 68 | } 69 | 70 | /** 71 | * Reset this batch to become "fresh"! 72 | */ 73 | reset(): void 74 | { 75 | this.textureBuffer = this.uidMap = this.state = null; 76 | 77 | if (this.batchBuffer) 78 | { 79 | this.batchBuffer.length = 0; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/StdBatchFactory.ts: -------------------------------------------------------------------------------- 1 | import { StdBatch } from './StdBatch'; 2 | import BatchRenderer from './BatchRenderer'; 3 | 4 | import type { BaseTexture, State, Texture } from '@pixi/core'; 5 | import type { DisplayObject } from '@pixi/display'; 6 | 7 | /** 8 | * Factory for producing "standard" (based on state, geometry, & textures) batches of 9 | * display-objects. 10 | * 11 | * **NOTE:** Instead of "building" batches, this factory actually keeps the batches in 12 | * a buffer so they can be accessed together at the end. 13 | * 14 | * **Shared Textures**: If display-objects in the same batch use the same base-texture, 15 | * then that base-texture is not uploaded twice. This allows for more better batch density 16 | * when you use texture atlases (textures with same base-texture). This is one reason why 17 | * textures are treated as "special" uniforms. 18 | */ 19 | export class StdBatchFactory 20 | { 21 | protected _renderer: BatchRenderer; 22 | 23 | protected _textureCount: number; 24 | protected _textureLimit: number; 25 | protected _textureProperty: string; 26 | 27 | /** @internal */ 28 | public _batchBuffer: Array; 29 | protected _state: State; 30 | 31 | protected _textureBuffer: any; 32 | protected _textureBufferLength: number; 33 | protected _textureIndexedBuffer: Array; 34 | protected _textureIndexMap: any; 35 | 36 | protected _batchPool: any[]; 37 | protected _batchCount: number; 38 | 39 | // _putTexture is optimized for the one texture/display-object case. 40 | protected _putTexture: any; 41 | 42 | /** 43 | * @param renderer 44 | */ 45 | constructor(renderer: BatchRenderer) 46 | { 47 | /** 48 | * @protected 49 | */ 50 | this._renderer = renderer; 51 | this._state = null; 52 | 53 | /** 54 | * Textures per display-object 55 | * @member {number} 56 | */ 57 | this._textureCount = renderer._texturesPerObject; 58 | 59 | /** 60 | * Property in which textures are kept of display-objects 61 | * @member {string} 62 | */ 63 | this._textureProperty = renderer._textureProperty; 64 | 65 | /** 66 | * Max. no of textures per batch (should be <= texture units of GPU) 67 | * @member {number} 68 | */ 69 | this._textureLimit = renderer.MAX_TEXTURES; 70 | 71 | /** 72 | * @member {object} 73 | */ 74 | this._textureBuffer = {}; // uid : texture map 75 | this._textureBufferLength = 0; 76 | this._textureIndexedBuffer = []; // array of textures 77 | this._textureIndexMap = {}; // uid : index in above 78 | 79 | /** 80 | * Display-objects in current batch 81 | * @protected 82 | */ 83 | this._batchBuffer = []; 84 | 85 | /** 86 | * Pool to batch objects into which data is fed. 87 | * @member {any[]} 88 | * @protected 89 | */ 90 | this._batchPool = []; 91 | 92 | /** 93 | * Number of batches created since last reset. 94 | * @member {number} 95 | * @protected 96 | */ 97 | this._batchCount = 0; 98 | 99 | if (this._textureCount === 1) 100 | { 101 | this._putTexture = this._putSingleTexture; 102 | } 103 | else 104 | { 105 | this._putTexture = this._putAllTextures; 106 | } 107 | } 108 | 109 | /** 110 | * Puts the display-object into the current batch, if possible. 111 | * 112 | * @param targetObject - object to add 113 | * @param state - state required by that object 114 | * @return whether the object was added to the batch. If it wasn't, you should "build" it. 115 | */ 116 | put(targetObject: DisplayObject, state: State): boolean 117 | { 118 | // State compat 119 | if (!this._state) 120 | { 121 | this._state = state; 122 | } 123 | else if (this._state.data !== state.data) 124 | { 125 | return false; 126 | } 127 | 128 | // Customized compat 129 | if (!this._put(targetObject)) 130 | { 131 | return false; 132 | } 133 | 134 | // Texture compat 135 | if (this._textureCount > 0 && !this._putTexture((targetObject as any)[this._textureProperty])) 136 | { 137 | return false; 138 | } 139 | 140 | this._batchBuffer.push(targetObject); 141 | 142 | return true; 143 | } 144 | 145 | /** 146 | * Creates the batch object and pushes it into the pool This also resets any state 147 | * so that a new batch can be started again. 148 | * 149 | * @param batch 150 | */ 151 | build(geometryOffset: number): void 152 | { 153 | const batch = this._nextBatch() as StdBatch; 154 | 155 | batch.geometryOffset = geometryOffset; 156 | this._buildBatch(batch); 157 | 158 | this._state = null; 159 | this._batchBuffer = []; 160 | this._textureBuffer = {}; 161 | this._textureIndexMap = {}; 162 | this._textureBufferLength = 0; 163 | this._textureIndexedBuffer = []; 164 | } 165 | 166 | /** 167 | * @returns {boolean} - whether this factory is ready to start a new batch from 168 | * "start". If not, then the current batch must be built before starting a new one. 169 | */ 170 | ready(): boolean 171 | { 172 | return this._batchBuffer.length === 0; 173 | } 174 | 175 | /** 176 | * Clears the batch pool. 177 | */ 178 | reset(): void 179 | { 180 | this._batchCount = 0; 181 | } 182 | 183 | /** 184 | * Returns the built batch pool. The array returned may be larger than the pool 185 | * itself. 186 | * 187 | * @returns {Array} 188 | */ 189 | access(): any[] 190 | { 191 | return this._batchPool; 192 | } 193 | 194 | /** 195 | * Size of the batch pool built since last reset. 196 | */ 197 | size(): number 198 | { 199 | return this._batchCount; 200 | } 201 | 202 | /** 203 | * Should store any information from the display-object to be put into 204 | * the batch. 205 | * 206 | * @param displayObject 207 | * @returns {boolean} - whether the display-object was "compatible" with 208 | * other display-objects in the batch. If not, it should not have been 209 | * added. 210 | */ 211 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 212 | protected _put(_displayObject: DisplayObject): boolean 213 | { 214 | // Override this 215 | return true; 216 | } 217 | 218 | /** 219 | * @returns {object} a new batch 220 | * @protected 221 | * @example 222 | * _newBatch(): CustomBatch 223 | * { 224 | * return new CustomBatch(); 225 | * } 226 | */ 227 | protected _newBatch(): any 228 | { 229 | return new StdBatch(); 230 | } 231 | 232 | /** 233 | * @param {number} geometryOffset 234 | */ 235 | protected _nextBatch(geometryOffset?: number): any 236 | { 237 | if (this._batchCount === this._batchPool.length) 238 | { 239 | this._batchPool.push(this._newBatch()); 240 | } 241 | 242 | const batch = this._batchPool[this._batchCount++]; 243 | 244 | batch.reset(); 245 | batch.geometryOffset = geometryOffset; 246 | 247 | return batch; 248 | } 249 | 250 | /** 251 | * Should add any information required to render the batch. If you override this, 252 | * you must call `super._buildBatch` and clear any state. 253 | * @param {object} batch 254 | * @protected 255 | * @example 256 | * _buildBatch(batch: any): void 257 | * { 258 | * super._buildBatch(batch); 259 | * batch.depth = this.generateDepth(); 260 | * 261 | * // if applicable 262 | * this.resetDepthGenerator(); 263 | * } 264 | */ 265 | protected _buildBatch(batch: any): void 266 | { 267 | batch.batchBuffer = this._batchBuffer; 268 | batch.textureBuffer = this._textureIndexedBuffer; 269 | batch.uidMap = this._textureIndexMap; 270 | batch.state = this._state; 271 | } 272 | 273 | // Optimized _putTexture case. 274 | private _putSingleTexture(texture: BaseTexture | Texture): boolean 275 | { 276 | if ('baseTexture' in texture) 277 | { 278 | texture = texture.baseTexture; 279 | } 280 | 281 | const baseTexture: BaseTexture = texture as BaseTexture; 282 | 283 | if (this._textureBuffer[baseTexture.uid]) 284 | { 285 | return true; 286 | } 287 | else if (this._textureBufferLength + 1 <= this._textureLimit) 288 | { 289 | this._textureBuffer[baseTexture.uid] = texture; 290 | this._textureBufferLength += 1; 291 | 292 | const newLength = this._textureIndexedBuffer.push(baseTexture); 293 | const index = newLength - 1; 294 | 295 | this._textureIndexMap[baseTexture.uid] = index; 296 | 297 | return true; 298 | } 299 | 300 | return false; 301 | } 302 | 303 | private _putAllTextures(textureArray: Array): boolean 304 | { 305 | let deltaBufferLength = 0; 306 | 307 | for (let i = 0; i < textureArray.length; i++) 308 | { 309 | const texture: BaseTexture = ('baseTexture' in textureArray[i] 310 | ? (textureArray[i] as Texture).baseTexture 311 | : textureArray[i]) as BaseTexture; 312 | 313 | if (!this._textureBuffer[texture.uid]) 314 | { 315 | ++deltaBufferLength; 316 | } 317 | } 318 | 319 | if (deltaBufferLength + this._textureBufferLength > this._textureLimit) 320 | { 321 | return false; 322 | } 323 | 324 | for (let i = 0; i < textureArray.length; i++) 325 | { 326 | const texture: BaseTexture = 'baseTexture' in textureArray[i] 327 | ? (textureArray[i] as Texture).baseTexture 328 | : (textureArray[i] as BaseTexture); 329 | 330 | if (!this._textureBuffer[texture.uid]) 331 | { 332 | this._textureBuffer[texture.uid] = texture; 333 | this._textureBufferLength += 1; 334 | 335 | const newLength = this._textureIndexedBuffer.push(texture); 336 | const index = newLength - 1; 337 | 338 | this._textureIndexMap[texture.uid] = index; 339 | } 340 | } 341 | 342 | return true; 343 | } 344 | } 345 | 346 | export default StdBatchFactory; 347 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './redirects'; 2 | export { StdBatchFactory as BatchGenerator } from './StdBatchFactory'; 3 | export { BatchRenderer } from './BatchRenderer'; 4 | export { BatchRendererPluginFactory } from './BatchRendererPluginFactory'; 5 | export { BatchGeometryFactory, BatchGeometry } from './BatchGeometryFactory'; 6 | export { Redirect } from './redirects/Redirect'; 7 | export { BatchShaderFactory } from './BatchShaderFactory'; 8 | export { StdBatch as Batch } from './StdBatch'; 9 | export { BatchDrawer } from './BatchDrawer'; 10 | 11 | export { BufferPool } from './utils/BufferPool'; 12 | 13 | export type { IBatchRendererOptions } from './BatchRenderer'; 14 | export type { IBatchRendererStdOptions } from './BatchRendererPluginFactory'; 15 | export type { IBatchGeometryFactory } from './BatchGeometryFactory'; 16 | export type { Resolvable } from './utils/resolveProperty'; 17 | 18 | export * from './AggregateUniformsBatch'; 19 | export * from './AggregateUniformsBatchFactory'; 20 | 21 | /** 22 | * Used by `BatchGeometryFactory` to merge the geometry of a 23 | * display-object into the whole batch's geometry. 24 | * 25 | * @function IGeometryMerger 26 | * @param {PIXI.DisplayObject} displayObject 27 | * @param {BatchGeometryFactory} factory 28 | * @see BatchGeometryFactory#geometryMerger 29 | */ 30 | 31 | /** 32 | * @function 33 | * @name InjectorFunction 34 | * 35 | * @param {BatchRenderer} batchRenderer 36 | * @return {string} value of the macro for this renderer 37 | */ 38 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/redirects/AttributeRedirect.ts: -------------------------------------------------------------------------------- 1 | import { Redirect } from './Redirect'; 2 | import { ViewableBuffer } from '@pixi/core'; 3 | 4 | import type { DisplayObject } from '@pixi/display'; 5 | import type { TYPES } from '@pixi/constants'; 6 | 7 | export interface IAttributeRedirectOptions 8 | { 9 | source: string | ((db: DisplayObject) => any); 10 | attrib: string; 11 | type: string; 12 | size?: number | '%notarray%'; 13 | glType: number; 14 | glSize: number; 15 | normalize?: boolean; 16 | } 17 | 18 | /** 19 | * This redirect defines an attribute of a display-object's geometry. The attribute 20 | * data is expected to be stored in a `PIXI.ViewableBuffer`, in an array, or (if 21 | * just one element) as the property itself. 22 | * 23 | * @example 24 | * // This attribute redirect calculates the tint used on top of a texture. Since the 25 | * // tintMode can change anytime, it is better to use a derived source (function). 26 | * // 27 | * // Furthermore, the color is uploaded as four bytes (`attribute vec4 aTint`) while the 28 | * // source returns an integer. This is done by splitting the 32-bit integer into four 29 | * // 8-bit bytes. 30 | * new AttributeRedirect({ 31 | * source: (tgt: ExampleDisplay) => (tgt.alpha < 1.0 && tgt.tintMode === PREMULTIPLY) 32 | * ? premultiplyTint(tgt.rgb, tgt.alpha) 33 | * : tgt.rgb + (tgt.alpha << 24); 34 | * attrib: 'aTint', 35 | * type: 'int32', 36 | * size: '%notarray%', // optional/default 37 | * glType: PIXI.TYPES.UNSIGNED_BYTE, 38 | * glSize: 4, 39 | * normalize: true // We are using [0, 255] range for RGBA here. Must normalize to [0, 1]. 40 | * }); 41 | */ 42 | export class AttributeRedirect extends Redirect 43 | { 44 | /** 45 | * The type of data stored in the source buffer. This can be any of: `int8`, `uint8`, 46 | * `int16`, `uint16`, `int32`, `uint32`, or (by default) `float32`. 47 | * 48 | * @member {string} 49 | * @see [PIXI.ViewableBuffer#view]{@link https://pixijs.download/dev/docs/PIXI.ViewableBuffer.html} 50 | * @default 'float32' 51 | */ 52 | public type: string; 53 | 54 | /** 55 | * Number of elements to extract out of `source` with 56 | * the given view type, for one vertex. 57 | * 58 | * If source isn't an array (only one element), then 59 | * you can set this to `'%notarray%'`. 60 | * 61 | * @member {number | '%notarray%'} 62 | */ 63 | public size: number | '%notarray%'; 64 | 65 | /** 66 | * Type of attribute, when uploading. 67 | * 68 | * Normally, you would use the corresponding type for 69 | * the view on source. However, to speed up uploads 70 | * you can aggregate attribute values in larger data 71 | * types. For example, an RGBA vec4 (byte-sized channels) 72 | * can be represented as one `Uint32`, while having 73 | * a `glType` of `UNSIGNED_BYTE`. 74 | */ 75 | public glType: TYPES; 76 | 77 | /** 78 | * Size of attribute in terms of `glType`. 79 | * 80 | * Note that `glSize * glType <= size * type` 81 | * 82 | * @readonly 83 | */ 84 | public glSize: number; 85 | 86 | /** 87 | * Whether to normalize the attribute values. 88 | * 89 | * @readonly 90 | */ 91 | public normalize: boolean; 92 | 93 | /** This is equal to `size` or 1 if size is `%notarray%`. */ 94 | public properSize: number; 95 | 96 | /** 97 | * @param {object} options 98 | * @param {string | Function} options.source - redirect source 99 | * @param {string} options.attrib - shader attribute variable 100 | * @param {string}[options.type='float32'] - the type of data stored in the source 101 | * @param {number | '%notarray%'}[options.size=0] - size of the source array ('%notarray' if not an array & just one element) 102 | * @param {PIXI.TYPES}[options.glType=PIXI.TYPES.FLOAT] - data format to be uploaded in 103 | * @param {number} options.glSize - number of elements to be uploaded as (size of source and upload must match) 104 | * @param {boolean}[options.normalize=false] - whether to normalize the data before uploading 105 | */ 106 | constructor(options: IAttributeRedirectOptions) 107 | { 108 | super(options.source, options.attrib); 109 | 110 | this.type = options.type; 111 | this.size = options.size; 112 | this.properSize = (options.size === '%notarray%' || options.size === undefined) ? 1 : options.size; 113 | this.glType = options.glType; 114 | this.glSize = options.glSize; 115 | this.normalize = !!options.normalize; 116 | } 117 | 118 | static vertexSizeFor(attributeRedirects: Array): number 119 | { 120 | return attributeRedirects.reduce( 121 | (acc, redirect) => 122 | (ViewableBuffer.sizeOf(redirect.type) 123 | * redirect.properSize) 124 | + acc, 125 | 0); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/redirects/Redirect.ts: -------------------------------------------------------------------------------- 1 | import type { BatchRenderer } from '../BatchRenderer'; 2 | import type { DisplayObject } from '@pixi/display'; 3 | 4 | /** 5 | * Redirects are used to aggregate the resources needed by the WebGL pipeline to render 6 | * a display-object. This includes the base primitives (geometry), uniforms, and 7 | * textures (which are handled as "special" uniforms). 8 | * 9 | * @see AttributeRedirect 10 | */ 11 | export abstract class Redirect 12 | { 13 | /** 14 | * The property on the display-object that holds the resource. 15 | * 16 | * Instead of a property, you can provide a callback that generates the resource 17 | * on invokation. 18 | */ 19 | public source: string | ((displayObject: DisplayObject, renderer: BatchRenderer) => any); 20 | 21 | /** The shader variable that references the resource, e.g. attribute or uniform name. */ 22 | public glslIdentifer: string; 23 | 24 | constructor(source: string | ((displayObject: DisplayObject) => any), glslIdentifer: string) 25 | { 26 | this.source = source; 27 | this.glslIdentifer = glslIdentifer; 28 | } 29 | } 30 | 31 | export default Redirect; 32 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/redirects/UniformRedirect.ts: -------------------------------------------------------------------------------- 1 | import { Redirect } from './Redirect'; 2 | 3 | import type { DisplayObject } from '@pixi/display'; 4 | 5 | export interface IUniformRedirectOptions 6 | { 7 | source: string | ((displayObject: DisplayObject) => any); 8 | uniform: string; 9 | } 10 | 11 | /** 12 | * This redirect is used to aggregate & upload uniforms required for shading the 13 | * display-object. 14 | * 15 | * @example 16 | * // The data-type of this uniform is defined in your shader. 17 | * new UniformRedirect({ 18 | * source: (dob: PIXI.DisplayObject) => dob.transform.worldTransform, 19 | * uniform: "transform" 20 | * }); 21 | */ 22 | export class UniformRedirect extends Redirect 23 | { 24 | constructor(options: IUniformRedirectOptions) 25 | { 26 | super(options.source, options.uniform); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/redirects/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AttributeRedirect'; 2 | export * from './Redirect'; 3 | export * from './UniformRedirect'; 4 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/resolve/index.ts: -------------------------------------------------------------------------------- 1 | export { resolveConstantOrProperty } from './resolveConstantOrProperty'; 2 | export { resolveFunctionOrProperty } from './resolveFunctionOrProperty'; 3 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/resolve/resolveConstantOrProperty.ts: -------------------------------------------------------------------------------- 1 | import type { DisplayObject } from '@pixi/display'; 2 | 3 | export function resolveConstantOrProperty(targetObject: DisplayObject, property: string | number): any 4 | { 5 | return (typeof property === 'string') 6 | ? (targetObject as Record)[property] 7 | : property; 8 | } 9 | 10 | export default resolveConstantOrProperty; 11 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/resolve/resolveFunctionOrProperty.ts: -------------------------------------------------------------------------------- 1 | import type { DisplayObject } from '@pixi/display'; 2 | 3 | export function resolveFunctionOrProperty(targetObject: DisplayObject, property: Function | string): any 4 | { 5 | return (typeof property === 'string') 6 | ? (targetObject as Record)[property] 7 | : property(targetObject); 8 | } 9 | 10 | export default resolveFunctionOrProperty; 11 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/utils/BufferPool.ts: -------------------------------------------------------------------------------- 1 | import { nextPow2, log2 } from '@pixi/utils'; 2 | 3 | /** 4 | * Pool for any array-like type. 5 | */ 6 | export class BufferPool> 7 | { 8 | private _bufferPools: T[][]; 9 | private _bufferType: { new(size: number): ArrayLike }; 10 | 11 | constructor(bufferType: { new(size: number): ArrayLike }) 12 | { 13 | this._bufferPools = []; 14 | this._bufferType = bufferType; 15 | } 16 | 17 | allocateBuffer(size: number): T 18 | { 19 | const roundedP2 = nextPow2(size); 20 | const roundedSizeIndex = log2(roundedP2); 21 | const roundedSize = roundedP2; 22 | 23 | if (this._bufferPools.length <= roundedSizeIndex) 24 | { 25 | this._bufferPools.length = roundedSizeIndex + 1; 26 | } 27 | 28 | let bufferPool = this._bufferPools[roundedSizeIndex]; 29 | 30 | if (!bufferPool) 31 | { 32 | this._bufferPools[roundedSizeIndex] = bufferPool = []; 33 | } 34 | 35 | return bufferPool.pop() || (new (this._bufferType)(roundedSize) as T); 36 | } 37 | 38 | releaseBuffer(buffer: T): void 39 | { 40 | const roundedP2 = nextPow2(buffer.length); 41 | const roundedSizeIndex = log2(roundedP2); 42 | 43 | if (!this._bufferPools[roundedSizeIndex]) 44 | { 45 | this._bufferPools[roundedSizeIndex] = []; 46 | } 47 | 48 | this._bufferPools[roundedSizeIndex].push(buffer); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/batch-renderer/src/utils/resolveProperty.ts: -------------------------------------------------------------------------------- 1 | import type { DisplayObject } from '@pixi/display'; 2 | import type { BatchRenderer } from '../BatchRenderer'; 3 | 4 | /** 5 | * A resolvable configures specific settings of how a display-object is rendered by a batch renderer. It 6 | * is resolved to a number, for a given display object, using {@link resolveProperty}. 7 | * 8 | * @ignore 9 | */ 10 | export type Resolvable = string | T | ((object: DisplayObject, renderer: BatchRenderer) => T); 11 | 12 | /** 13 | * Resolves a resolvable for the passed {@link DisplayObject}. It is expected to evaluate to a 14 | * number. If the passed {@code prop} is a string, it is dereferenced from the object. If the passed 15 | * {@code prop} is a function, then it is invoked by passing the object. 16 | * 17 | * @ignore 18 | * @param object - The object for which the parameter property is to be resolved. 19 | * @param prop - The property that is to be resolved to a numeric value. 20 | * @param def - The value that will be resolved if {@code prop} is undefined. 21 | * @return The resolved value of the {@code prop}. 22 | */ 23 | export function resolve( 24 | object: DisplayObject, 25 | prop: Resolvable, 26 | def?: T 27 | ): T 28 | { 29 | switch(typeof prop) 30 | { 31 | case 'string': 32 | return (object as any)[prop]; 33 | case 'function': 34 | return (prop as ((object: DisplayObject) => T))(object); 35 | case 'undefined': 36 | return def; 37 | } 38 | 39 | return prop; 40 | } -------------------------------------------------------------------------------- /packages/batch-renderer/test/BufferPool.js: -------------------------------------------------------------------------------- 1 | const { BufferPool } = require('../'); 2 | 3 | describe('PIXI.brend.BufferPool', function() 4 | { 5 | it('should return the released buffer', function() 6 | { 7 | const bufferPool = new BufferPool(Float32Array); 8 | const firstBuffer = bufferPool.allocateBuffer(1500); 9 | 10 | bufferPool.releaseBuffer(firstBuffer); 11 | 12 | const secondBuffer = bufferPool.allocateBuffer(1501); 13 | 14 | expect(firstBuffer).to.equal(secondBuffer); 15 | }) 16 | }) -------------------------------------------------------------------------------- /packages/batch-renderer/test/index.js: -------------------------------------------------------------------------------- 1 | require('./BufferPool'); -------------------------------------------------------------------------------- /packages/batch-renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "allowJs": true, 5 | "sourceMap": true, 6 | "removeComments": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "strictNullChecks": false, 13 | "esModuleInterop": true, 14 | "moduleResolution": "node", 15 | "baseUrl": "./", 16 | "outDir": "compile", 17 | "declaration": true 18 | }, 19 | "include": [ 20 | "src/**/*.ts" 21 | ], 22 | "exclude": [ 23 | "node_modules", 24 | "**/dist/**", 25 | "**/lib/**", 26 | "**/test/**", 27 | "**/scripts/**", 28 | "**/types/**", 29 | "bundles/**/*.d.ts", 30 | "rollup.config.js" 31 | ] 32 | } -------------------------------------------------------------------------------- /packages/diffy/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "browser": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "globals": { 9 | "global": false 10 | }, 11 | "parserOptions": { 12 | "ecmaVersion": 8, 13 | "sourceType": "module" 14 | }, 15 | "extends": [ 16 | "@pixi/eslint-config" 17 | ], 18 | "rules": { 19 | "@typescript-eslint/triple-slash-reference": [ 20 | "off" 21 | ], 22 | "spaced-comment": [ 23 | "off" 24 | ], 25 | "no-duplicate-imports": [ 26 | "off" 27 | ] 28 | } 29 | } -------------------------------------------------------------------------------- /packages/diffy/.npmignore: -------------------------------------------------------------------------------- 1 | # Build-related 2 | compile 3 | docs 4 | src 5 | test 6 | 7 | # Tooling configuration 8 | *.log 9 | *.md 10 | .rush 11 | .eslintrc 12 | CHANGELOG.json 13 | CHANGELOG.md 14 | api-extractor.json 15 | jsdoc.conf.json 16 | webdoc.conf.json 17 | tsconfig.json 18 | *.config.js -------------------------------------------------------------------------------- /packages/diffy/CHANGELOG.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pixi-pbr/diffy", 3 | "entries": [ 4 | { 5 | "version": "1.1.0", 6 | "tag": "@pixi-pbr/diffy_v1.1.0", 7 | "date": "Sun, 28 Mar 2021 19:20:49 GMT", 8 | "comments": { 9 | "minor": [ 10 | { 11 | "comment": "Upgrade to PixiJS 6" 12 | } 13 | ], 14 | "dependency": [ 15 | { 16 | "comment": "Updating dependency \"pixi-batch-renderer\" from `~2.4.2` to `~2.5.0`" 17 | }, 18 | { 19 | "comment": "Updating dependency \"pixi-batch-renderer\" from `~2.4.2` to `~2.5.0`" 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "version": "1.0.9", 26 | "tag": "@pixi-pbr/diffy_v1.0.9", 27 | "date": "Mon, 19 Oct 2020 00:17:18 GMT", 28 | "comments": { 29 | "dependency": [ 30 | { 31 | "comment": "Updating dependency \"pixi-batch-renderer\" from `~2.3.7` to `~2.4.0`" 32 | }, 33 | { 34 | "comment": "Updating dependency \"pixi-batch-renderer\" from `~2.3.7` to `~2.4.0`" 35 | } 36 | ] 37 | } 38 | }, 39 | { 40 | "version": "1.0.8", 41 | "tag": "@pixi-pbr/diffy_v1.0.8", 42 | "date": "Tue, 13 Oct 2020 23:57:21 GMT", 43 | "comments": { 44 | "patch": [ 45 | { 46 | "comment": "Reduce BufferInvalidation allocations by merging insignificant ones preliminarily." 47 | } 48 | ] 49 | } 50 | }, 51 | { 52 | "version": "1.0.7", 53 | "tag": "@pixi-pbr/diffy_v1.0.7", 54 | "date": "Fri, 09 Oct 2020 22:54:02 GMT", 55 | "comments": { 56 | "patch": [ 57 | { 58 | "comment": "Clear indices properly to prevent garbage from causing errors" 59 | } 60 | ] 61 | } 62 | }, 63 | { 64 | "version": "1.0.6", 65 | "tag": "@pixi-pbr/diffy_v1.0.6", 66 | "date": "Fri, 09 Oct 2020 22:28:52 GMT", 67 | "comments": { 68 | "patch": [ 69 | { 70 | "comment": "Fix critical bug in partitioning algorithm" 71 | } 72 | ] 73 | } 74 | }, 75 | { 76 | "version": "1.0.5", 77 | "tag": "@pixi-pbr/diffy_v1.0.5", 78 | "date": "Sun, 30 Aug 2020 19:00:04 GMT", 79 | "comments": { 80 | "patch": [ 81 | { 82 | "comment": "Fix crash when last node is coalesced in BufferInvalidationQueue#partition" 83 | } 84 | ] 85 | } 86 | }, 87 | { 88 | "version": "1.0.4", 89 | "tag": "@pixi-pbr/diffy_v1.0.4", 90 | "date": "Sun, 30 Aug 2020 18:46:13 GMT", 91 | "comments": { 92 | "patch": [ 93 | { 94 | "comment": "Fix calculation of buffer length" 95 | } 96 | ] 97 | } 98 | }, 99 | { 100 | "version": "1.0.3", 101 | "tag": "@pixi-pbr/diffy_v1.0.3", 102 | "date": "Sun, 30 Aug 2020 16:04:06 GMT", 103 | "comments": { 104 | "patch": [ 105 | { 106 | "comment": "Use Uint32Array comparision to avoid NaN, and diff only up till aIndex to prevent upload unused geometry" 107 | } 108 | ] 109 | } 110 | }, 111 | { 112 | "version": "1.0.2", 113 | "tag": "@pixi-pbr/diffy_v1.0.2", 114 | "date": "Sun, 30 Aug 2020 15:11:17 GMT", 115 | "comments": { 116 | "patch": [ 117 | { 118 | "comment": "Fix memory leak in DiffGeometryFactory (specifically hackViewableBuffer was not recycling the ViewableBuffer, and created a new Float32Array buffer each time)" 119 | } 120 | ], 121 | "dependency": [ 122 | { 123 | "comment": "Updating dependency \"pixi-batch-renderer\" from `~2.3.5` to `~2.3.6`" 124 | }, 125 | { 126 | "comment": "Updating dependency \"pixi-batch-renderer\" from `~2.3.5` to `~2.3.6`" 127 | } 128 | ] 129 | } 130 | }, 131 | { 132 | "version": "1.0.1", 133 | "tag": "@pixi-pbr/diffy_v1.0.1", 134 | "date": "Sat, 29 Aug 2020 19:02:11 GMT", 135 | "comments": { 136 | "patch": [ 137 | { 138 | "comment": "First release, fully optimized for WebGL 2!" 139 | } 140 | ], 141 | "dependency": [ 142 | { 143 | "comment": "Updating dependency \"pixi-batch-renderer\" from `~2.3.3` to `~2.3.4`" 144 | } 145 | ] 146 | } 147 | } 148 | ] 149 | } 150 | -------------------------------------------------------------------------------- /packages/diffy/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - @pixi-pbr/diffy 2 | 3 | This log was last generated on Sun, 28 Mar 2021 19:20:49 GMT and should not be manually modified. 4 | 5 | ## 1.1.0 6 | Sun, 28 Mar 2021 19:20:49 GMT 7 | 8 | ### Minor changes 9 | 10 | - Upgrade to PixiJS 6 11 | 12 | ## 1.0.9 13 | Mon, 19 Oct 2020 00:17:18 GMT 14 | 15 | *Version update only* 16 | 17 | ## 1.0.8 18 | Tue, 13 Oct 2020 23:57:21 GMT 19 | 20 | ### Patches 21 | 22 | - Reduce BufferInvalidation allocations by merging insignificant ones preliminarily. 23 | 24 | ## 1.0.7 25 | Fri, 09 Oct 2020 22:54:02 GMT 26 | 27 | ### Patches 28 | 29 | - Clear indices properly to prevent garbage from causing errors 30 | 31 | ## 1.0.6 32 | Fri, 09 Oct 2020 22:28:52 GMT 33 | 34 | ### Patches 35 | 36 | - Fix critical bug in partitioning algorithm 37 | 38 | ## 1.0.5 39 | Sun, 30 Aug 2020 19:00:04 GMT 40 | 41 | ### Patches 42 | 43 | - Fix crash when last node is coalesced in BufferInvalidationQueue#partition 44 | 45 | ## 1.0.4 46 | Sun, 30 Aug 2020 18:46:13 GMT 47 | 48 | ### Patches 49 | 50 | - Fix calculation of buffer length 51 | 52 | ## 1.0.3 53 | Sun, 30 Aug 2020 16:04:06 GMT 54 | 55 | ### Patches 56 | 57 | - Use Uint32Array comparision to avoid NaN, and diff only up till aIndex to prevent upload unused geometry 58 | 59 | ## 1.0.2 60 | Sun, 30 Aug 2020 15:11:17 GMT 61 | 62 | ### Patches 63 | 64 | - Fix memory leak in DiffGeometryFactory (specifically hackViewableBuffer was not recycling the ViewableBuffer, and created a new Float32Array buffer each time) 65 | 66 | ## 1.0.1 67 | Sat, 29 Aug 2020 19:02:11 GMT 68 | 69 | ### Patches 70 | 71 | - First release, fully optimized for WebGL 2! 72 | 73 | -------------------------------------------------------------------------------- /packages/diffy/README.md: -------------------------------------------------------------------------------- 1 | # @pixi-pbr/diffy 2 | 3 | This package can be used to minimize buffer upload overhead in vertex-bound applications. It exploits 4 | `bufferSubData` to upload only modified parts of the batched geometry. This trades off graphics memory 5 | for reducing data transfers. 6 | 7 | ## Installation :package: 8 | 9 | ```bash 10 | npm install @pixi-pbr/diffy 11 | ``` 12 | 13 | ## Notes 14 | 15 | * **WebGL 1**: WebGL 1 does not the required overload of `gl.bufferSubData` to do partial uploads. This 16 | in turns only allows optimizations in the case where the geometry has not changed at all. 17 | * **Projection Matrix**: If your application uses a viewport (like `pixi-viewport` by David Fignater), then 18 | you can optimize it use a projection-matrix so that your object's coordinate do not change when modifying 19 | the viewport itself. This has the effect of the geometry not changing at all when panning/zooming so buffer uploads can be fully optimized away! 20 | 21 | ## Usage 22 | 23 | To enable this optimization, you can use the exported geometry-factory and drawer. 24 | 25 | ```js 26 | import { BatchRendererPluginFactory } from 'pixi-batch-renderer'; 27 | import { DiffGeometryFactory, DiffDrawer } from '@pixi-pbr/diffy'; 28 | 29 | // YourRenderer will have the diffy optimization! 30 | const YourRenderer = BatchRendererPluginFactory.from({ 31 | ...yourOptions, 32 | BatchGeometryFactoryClass: DiffGeometryFactory, 33 | BatchDrawerClass: DiffDrawer 34 | }); 35 | ``` 36 | 37 | ## Collaboration 38 | 39 | I'd like to thank [Strytegy](https://www.strytegy.com) for funding the initial development of this package. 40 | 41 | 42 | -------------------------------------------------------------------------------- /packages/diffy/api-extractor.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 3 | "mainEntryPointFilePath": "/compile/index.d.ts", 4 | "bundledPackages": [], 5 | "compiler": { 6 | "tsconfigFilePath": "/tsconfig.json" 7 | }, 8 | "apiReport": { 9 | "enabled": false 10 | }, 11 | "docModel": { 12 | "enabled": false 13 | }, 14 | "dtsRollup": { 15 | "enabled": true, 16 | "untrimmedFilePath": "/index.d.ts" 17 | }, 18 | "tsdocMetadata": { 19 | "enabled": false 20 | }, 21 | "messages": { 22 | "compilerMessageReporting": { 23 | "default": { 24 | "logLevel": "none" 25 | } 26 | }, 27 | "extractorMessageReporting": { 28 | "default": { 29 | "logLevel": "none" 30 | } 31 | }, 32 | "tsdocMessageReporting": { 33 | "default": { 34 | "logLevel": "none" 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /packages/diffy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pixi-pbr/diffy", 3 | "version": "2.0.1", 4 | "description": "Geometry factory that minimizes buffer uploads by tracking & updating only modified regions of data", 5 | "main": "lib/diffy.js", 6 | "module": "lib/diffy.es.js", 7 | "bundle": "dist/diffy.js", 8 | "namespace": "PIXI.brend", 9 | "types": "index.d.ts", 10 | "scripts": { 11 | "build": "pnpm compile && pnpm compile:types", 12 | "compile": "rollup -c node_modules/@pixi-build-tools/rollup-configurator/index.js --silent", 13 | "compile:types": "rm -rf compile && tsc; api-extractor run; echo Done", 14 | "prepublishOnly": "pnpm compile && pnpm compile:types" 15 | }, 16 | "files": [ 17 | "lib", 18 | "dist", 19 | "index.d.ts" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/pixijs/pixi-batch-renderer.git" 24 | }, 25 | "keywords": [ 26 | "pixijs", 27 | "batch", 28 | "diff", 29 | "geometry", 30 | "bufferSubData", 31 | "pixi-pbr" 32 | ], 33 | "author": "Shukant K. Pal ", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/pixijs/pixi-batch-renderer/issues" 37 | }, 38 | "homepage": "https://github.com/pixijs/pixi-batch-renderer#readme", 39 | "publishConfig": { 40 | "access": "public" 41 | }, 42 | "peerDependencies": { 43 | "pixi-batch-renderer": "~2.5.3", 44 | "@pixi/utils": "^7.0.0", 45 | "@pixi/core": "^7.0.0", 46 | "@pixi-essentials/object-pool": "^1.0.0" 47 | }, 48 | "devDependencies": { 49 | "@pixi/eslint-config": "~1.0.1", 50 | "eslint": "~7.7.0", 51 | "@typescript-eslint/eslint-plugin": "~3.10.1", 52 | "@typescript-eslint/parser": "~3.10.1", 53 | "@pixi-essentials/object-pool": "^0.1.0", 54 | "rollup": "~2.26.7", 55 | "@pixi-build-tools/rollup-configurator": "~1.0.4", 56 | "pixi-batch-renderer": "~3.0.0", 57 | "typescript": "~4.9.5" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/diffy/src/DiffBuffer.ts: -------------------------------------------------------------------------------- 1 | import { Buffer, IArrayBuffer } from '@pixi/core'; 2 | import { BufferInvalidationQueue } from './utils/BufferInvalidationQueue'; 3 | 4 | export class DiffBuffer extends Buffer 5 | { 6 | public updateQueue: BufferInvalidationQueue; 7 | 8 | constructor(data?: IArrayBuffer, _static = false, index = false) 9 | { 10 | super(data, _static, index); 11 | 12 | this.updateQueue = new BufferInvalidationQueue(); 13 | } 14 | 15 | update(data?: IArrayBuffer): void 16 | { 17 | super.update(data); 18 | this.updateQueue.clear(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/diffy/src/DiffDrawer.ts: -------------------------------------------------------------------------------- 1 | import { BatchDrawer } from 'pixi-batch-renderer'; 2 | import { uploadBuffer } from './utils/uploadBuffer'; 3 | 4 | import type { DiffBuffer } from './DiffBuffer'; 5 | 6 | export class DiffDrawer extends BatchDrawer 7 | { 8 | draw(): void 9 | { 10 | const { 11 | renderer, 12 | _batchFactory: batchFactory, 13 | _geometryFactory: geometryFactory, 14 | _indexProperty: indexProperty, 15 | } = this.renderer; 16 | 17 | const batchList = batchFactory.access(); 18 | const batchCount = batchFactory.size(); 19 | const geom = geometryFactory.build(); 20 | const { gl } = renderer; 21 | 22 | // PixiJS bugs - the shader can't be bound before uploading because uniform sync caching 23 | // and geometry requires the shader to be bound. 24 | batchList[0].upload(renderer); 25 | renderer.shader.bind(this.renderer._shader, false); 26 | 27 | // Clear update-queue if buffers are fully invalidated 28 | for (let i = 0; i < (geom as any).buffers.length; i++) 29 | { 30 | const buffer = (geom as any).buffers[i]; 31 | const glBuffer = buffer._glBuffers[(renderer as any).CONTEXT_UID]; 32 | 33 | if (glBuffer && buffer._updateID !== glBuffer.updateID) 34 | { 35 | (buffer as DiffBuffer).updateQueue.clear(); 36 | } 37 | } 38 | 39 | renderer.geometry.bind(geom); 40 | 41 | for (let i = 0; i < (geom as any).buffers.length; i++) 42 | { 43 | const buffer = (geom as any).buffers[i]; 44 | 45 | uploadBuffer(renderer.geometry, buffer); 46 | } 47 | 48 | for (let i = 0; i < batchCount; i++) 49 | { 50 | const batch = batchList[i]; 51 | 52 | batch.upload(renderer); 53 | renderer.shader.bind(this.renderer._shader, false); 54 | 55 | if (indexProperty) 56 | { 57 | // TODO: Get rid of the $indexCount black magic! 58 | gl.drawElements(gl.TRIANGLES, batch.$indexCount, gl.UNSIGNED_SHORT, batch.geometryOffset * 2); 59 | } 60 | else 61 | { 62 | // TODO: Get rid of the $vertexCount black magic! 63 | gl.drawArrays(gl.TRIANGLES, batch.geometryOffset, batch.$vertexCount); 64 | } 65 | 66 | batch.reset(); 67 | } 68 | 69 | geometryFactory.release(geom); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/diffy/src/DiffGeometry.ts: -------------------------------------------------------------------------------- 1 | import { BatchGeometry } from 'pixi-batch-renderer'; 2 | import { DiffBuffer } from './DiffBuffer'; 3 | 4 | import type { AttributeRedirect } from 'pixi-batch-renderer'; 5 | 6 | /** 7 | * The geometry used by {@link DiffGeometryFactory} 8 | */ 9 | export class DiffGeometry extends BatchGeometry 10 | { 11 | constructor(attributeRedirects: AttributeRedirect[], 12 | hasIndex: boolean, 13 | texIDAttrib: string, 14 | texturesPerObject: number, 15 | inBatchIDAttrib: string, 16 | uniformIDAttrib: string, 17 | masterIDAttrib: string) 18 | { 19 | super(attributeRedirects, 20 | hasIndex, 21 | texIDAttrib, 22 | texturesPerObject, 23 | inBatchIDAttrib, 24 | uniformIDAttrib, 25 | masterIDAttrib, 26 | new DiffBuffer(null, false, false), 27 | new DiffBuffer(null, false, true)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/diffy/src/DiffGeometryFactory.ts: -------------------------------------------------------------------------------- 1 | import { BatchGeometryFactory, BatchGeometry, BufferPool } from 'pixi-batch-renderer'; 2 | import { BufferInvalidationQueue } from './utils/BufferInvalidationQueue'; 3 | import { DiffBuffer } from './DiffBuffer'; 4 | import { DiffGeometry } from './DiffGeometry'; 5 | import { ViewableBuffer } from '@pixi/core'; 6 | 7 | import type { BatchRenderer } from 'pixi-batch-renderer'; 8 | 9 | declare interface HackedUint32Array extends Uint32Array { 10 | viewableBuffer: ViewableBuffer; 11 | } 12 | 13 | // ViewableBuffer.constructor(ArrayBuffer) is introduced in the Compressed Textures PR in pixi.js, will be released ion 5.4.0 14 | // Cringe hack till then: 15 | function hackViewableBuffer(buffer: Uint32Array): ViewableBuffer 16 | { 17 | const hackedBuffer = buffer as HackedUint32Array; 18 | 19 | if (hackedBuffer.viewableBuffer) 20 | { 21 | return hackedBuffer.viewableBuffer; 22 | } 23 | 24 | const viewableBuffer = new ViewableBuffer(0); 25 | 26 | viewableBuffer.rawBinaryData = buffer.buffer; 27 | viewableBuffer.uint32View = buffer; 28 | viewableBuffer.float32View = new Float32Array(buffer.buffer); 29 | 30 | hackedBuffer.viewableBuffer = viewableBuffer; 31 | 32 | return viewableBuffer; 33 | } 34 | 35 | export class DiffGeometryFactory extends BatchGeometryFactory 36 | { 37 | // typings from BGF 38 | _vertexSize: number; 39 | 40 | protected _geometryCache: DiffGeometry[]; 41 | protected _geometryPipeline: DiffGeometry[]; 42 | 43 | attribPool: BufferPool; 44 | indexPool: BufferPool; 45 | 46 | constructor(renderer: BatchRenderer) 47 | { 48 | super(renderer); 49 | 50 | /** 51 | * Cache of the geometries drawn in the last frame. 52 | */ 53 | this._geometryCache = []; 54 | 55 | /** 56 | * The geometries already drawn this frame. 57 | */ 58 | this._geometryPipeline = []; 59 | 60 | this.attribPool = new BufferPool(Uint32Array); 61 | this.indexPool = new BufferPool(Uint16Array); 62 | 63 | renderer.renderer.on('postrender', this.postrender, this); 64 | } 65 | 66 | build(): DiffGeometry 67 | { 68 | const cache: DiffGeometry = this._geometryCache.shift(); 69 | 70 | const geom = cache || new DiffGeometry( 71 | this._attribRedirects, 72 | true, 73 | this._texIDAttrib, 74 | this._texturesPerObject, 75 | this._inBatchIDAttrib, 76 | this._uniformIDAttrib, 77 | this._masterIDAttrib, 78 | ); 79 | 80 | if (!cache) 81 | { 82 | geom.attribBuffer.update(this._targetCompositeAttributeBuffer.uint32View); 83 | geom.indexBuffer.update(this._targetCompositeIndexBuffer); 84 | } 85 | else 86 | { 87 | this.updateCache(cache, 'attribBuffer', this._targetCompositeAttributeBuffer.uint32View); 88 | this.updateCache(cache, 'indexBuffer', this._targetCompositeIndexBuffer); 89 | } 90 | 91 | return geom; 92 | } 93 | 94 | /** 95 | * {@code DiffGeometryFactory} 96 | * 97 | * @override 98 | * @param geom 99 | */ 100 | release(geom: DiffGeometry): void 101 | { 102 | this._geometryPipeline.push(geom); 103 | } 104 | 105 | protected updateCache( 106 | geometry: BatchGeometry, 107 | type: 'attribBuffer' | 'indexBuffer', 108 | data: Uint32Array | Uint16Array, 109 | ): void 110 | { 111 | const cachedBuffer = geometry[type].data as ArrayLike; 112 | const buffer = geometry[type] as DiffBuffer; 113 | const bufferPool = this[type === 'attribBuffer' ? 'attribPool' : 'indexPool']; 114 | const bufferLength = type === 'attribBuffer' ? this._aIndex / 4 : this._iIndex; 115 | 116 | if (cachedBuffer.length < data.length) 117 | { 118 | buffer.update(data); 119 | bufferPool.releaseBuffer(cachedBuffer as any); 120 | 121 | return; 122 | } 123 | 124 | this.diffCache(buffer.data as ArrayLike, data, bufferLength, buffer.updateQueue); 125 | bufferPool.releaseBuffer(data as any); 126 | } 127 | 128 | /** 129 | * Calculates the regions different in the cached & updated versions of a buffer. The cache is expected to not be smaller 130 | * than the updated data. 131 | * 132 | * @param cache 133 | * @param data 134 | */ 135 | protected diffCache( 136 | cache: ArrayLike, 137 | data: ArrayLike, 138 | length: number, 139 | diffQueue: BufferInvalidationQueue): void 140 | { 141 | diffQueue.clear(); 142 | 143 | // Flags whether the loop is inside an invalidated interval 144 | let inDiff = false; 145 | 146 | // Stores the offset of the current invalidated interval, if inDiff 147 | let diffOffset = 0; 148 | 149 | // Fill diffQueue 150 | for (let i = 0; i < length; i++) 151 | { 152 | const cachedElement = cache[i]; 153 | const dataElement = data[i]; 154 | 155 | if (cachedElement !== dataElement) 156 | { 157 | (cache as number[])[i] = dataElement; 158 | } 159 | 160 | if (cachedElement !== dataElement && !inDiff) 161 | { 162 | inDiff = true; 163 | diffOffset = i; 164 | } 165 | else if (cachedElement === dataElement && inDiff) 166 | { 167 | inDiff = false; 168 | 169 | diffQueue.append(diffOffset, i - diffOffset); 170 | } 171 | } 172 | 173 | if (inDiff) 174 | { 175 | diffQueue.append(diffOffset, length - diffOffset); 176 | } 177 | } 178 | 179 | /** 180 | * Release the geometry cache 181 | */ 182 | protected releaseCache(): void 183 | { 184 | this._geometryPool.push(...this._geometryCache); 185 | 186 | // Swap geometryCache & geometryPipeline arrays. 187 | const lastCache = this._geometryCache; 188 | 189 | this._geometryCache = this._geometryPipeline; 190 | this._geometryPipeline = lastCache; 191 | this._geometryPipeline.length = 0; 192 | } 193 | 194 | postrender(): void 195 | { 196 | this.releaseCache(); 197 | } 198 | 199 | getAttributeBuffer(size: number): ViewableBuffer 200 | { 201 | const buffer = this.attribPool.allocateBuffer(size * this._vertexSize); 202 | const vbuf = hackViewableBuffer(buffer); 203 | 204 | return vbuf; 205 | } 206 | 207 | getIndexBuffer(size: number): Uint16Array 208 | { 209 | return this.indexPool.allocateBuffer(size); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /packages/diffy/src/index.ts: -------------------------------------------------------------------------------- 1 | export { DiffBuffer } from './DiffBuffer'; 2 | export { DiffDrawer } from './DiffDrawer'; 3 | export { DiffGeometry } from './DiffGeometry'; 4 | export { DiffGeometryFactory } from './DiffGeometryFactory'; 5 | 6 | export { BufferInvalidation } from './utils/BufferInvalidation'; 7 | export { BufferInvalidationQueue } from './utils/BufferInvalidationQueue'; 8 | -------------------------------------------------------------------------------- /packages/diffy/src/utils/BufferInvalidation.ts: -------------------------------------------------------------------------------- 1 | import { ObjectPoolFactory } from '@pixi-essentials/object-pool'; 2 | 3 | import type { ObjectPool } from '@pixi-essentials/object-pool'; 4 | 5 | /* eslint-disable-next-line prefer-const */ 6 | export let BufferInvalidationPool: ObjectPool; 7 | 8 | /** 9 | * A buffer invalidation records a region of data that has changed across frames. 10 | * 11 | * @ignore 12 | * @internal 13 | */ 14 | export class BufferInvalidation 15 | { 16 | offset: number; 17 | size: number; 18 | next: BufferInvalidation; 19 | 20 | constructor() 21 | { 22 | this.offset = 0; 23 | this.size = 0; 24 | this.next = null; 25 | } 26 | 27 | /** 28 | * Initialize the invalidation tracking data. 29 | */ 30 | init(srcStart: number, size: number): this 31 | { 32 | this.offset = srcStart; 33 | this.size = size; 34 | 35 | return this; 36 | } 37 | 38 | /** 39 | * Clone this object, used for debugging only 40 | */ 41 | clone(): BufferInvalidation 42 | { 43 | return BufferInvalidationPool.allocate() 44 | .init(this.offset, this.size); 45 | } 46 | } 47 | 48 | BufferInvalidationPool = ObjectPoolFactory.build(BufferInvalidation); 49 | -------------------------------------------------------------------------------- /packages/diffy/src/utils/BufferInvalidationQueue.ts: -------------------------------------------------------------------------------- 1 | import { BufferInvalidationPool } from './BufferInvalidation'; 2 | import { BufferPool } from 'pixi-batch-renderer'; 3 | 4 | import type { BufferInvalidation } from './BufferInvalidation'; 5 | 6 | const arrayPool: BufferPool> = new BufferPool(Array); 7 | 8 | /** 9 | * The minimum length of a "significant" invalidation. Two significant invalidations can be separated 10 | * by at most 20% of their lengths, unless the number of regions is below {@link MAX_INVALIDATIONS}. 11 | * 12 | * This is also the maximum length of an "insignificant" invalidation. Two insignifant invalidations 13 | * should always be merged if they are less away than 20% of {@code SIG_REGION_LEN}. 14 | * 15 | * @ignore 16 | */ 17 | export const SIG_REGION_LEN = 128; 18 | 19 | /** 20 | * 20% of {@link SIG_REGION_LEN} 21 | * 22 | * @ignore 23 | */ 24 | export const INSIG_REGION_DIST = Math.ceil(0.2 * SIG_REGION_LEN); 25 | 26 | /** 27 | * The maximum number of invalidations allowed per buffer. 28 | * 29 | * @ignore 30 | */ 31 | const MAX_INVALIDATIONS = 8; 32 | 33 | /** 34 | * The buffer invalidation queue manages a singly-linked list of buffer invalidations. 35 | * 36 | * @ignore 37 | */ 38 | export class BufferInvalidationQueue 39 | { 40 | start: BufferInvalidation; 41 | end: BufferInvalidation; 42 | size: number; 43 | 44 | constructor() 45 | { 46 | this.start = null; 47 | this.end = null; 48 | this.size = 0; 49 | } 50 | 51 | /** 52 | * @returns whether the queue is empty 53 | */ 54 | isEmpty(): boolean 55 | { 56 | return !this.start; 57 | } 58 | /** 59 | * Appends the invalidation-node to this queue 60 | * 61 | * @param node 62 | */ 63 | append(node: BufferInvalidation): void; 64 | 65 | /** 66 | * Appends an invalidation from offset with the given size. 67 | * 68 | * @param offset 69 | * @param size 70 | */ 71 | append(offset: number, size: number): void; 72 | 73 | append(offset: BufferInvalidation | number, size?: number): void 74 | { 75 | // Merge nearby insignificant invalidations preliminarily to conserve memory usage. 76 | if (typeof offset === 'number' && this.end) 77 | { 78 | const lastNode = this.end; 79 | const lastEnd = lastNode.offset + lastNode.size; 80 | 81 | if (lastNode.size < SIG_REGION_LEN && lastNode.offset - lastEnd < INSIG_REGION_DIST) 82 | { 83 | lastNode.size = offset + size - lastNode.offset; 84 | 85 | return; 86 | } 87 | } 88 | 89 | const node = typeof offset === 'number' 90 | ? BufferInvalidationPool 91 | .allocate() 92 | .init(offset, size) 93 | : offset; 94 | 95 | ++this.size; 96 | 97 | if (!this.start) 98 | { 99 | this.start = node; 100 | this.end = node; 101 | 102 | node.next = null; 103 | 104 | return; 105 | } 106 | 107 | this.end.next = node; 108 | node.next = null; 109 | this.end = node; 110 | } 111 | 112 | /** 113 | * This will repartition the invalidated buffer indices into fewer, larger segments. This is mainly used to avoid 114 | * issuing too many `bufferSubData` when sparse, small changes occur in the geometry. 115 | */ 116 | // threshold parameter for testing only! 117 | partition(threshold: number = MAX_INVALIDATIONS): void 118 | { 119 | // NOTE: This algorithm doesn't have a solid statistical basis and improving it is welcome! 120 | 121 | let node = this.start; 122 | let lastNode = null; 123 | let lastNodeInsig = false; 124 | 125 | // Coalesce as many insignificant invalidations as possible 126 | while (node) 127 | { 128 | const nodeInsig = node.size < SIG_REGION_LEN; 129 | 130 | if ((lastNodeInsig || nodeInsig) && lastNode 131 | && (node.offset - (lastNode.offset + lastNode.size) < INSIG_REGION_DIST)) 132 | { 133 | node = this.coalesce(lastNode); 134 | } 135 | 136 | lastNode = node; 137 | lastNodeInsig = nodeInsig; 138 | node = node.next; 139 | } 140 | 141 | if (this.size <= threshold) 142 | { 143 | // Phew! No need to coalesce significant invalidations as well! 144 | return; 145 | } 146 | 147 | // Distance of each node from its successor 148 | // NOTE: This is kind of expensive since we _have_ to allocate three arrays 149 | const size = this.size; 150 | const dists = arrayPool.allocateBuffer(size - 1) as Array; 151 | const indices = arrayPool.allocateBuffer(size - 1) as Array; 152 | const nodes = arrayPool.allocateBuffer(size - 1) as Array;// needed for random access 153 | 154 | node = this.start; 155 | 156 | for (let i = 0; i < size - 1; i++) 157 | { 158 | const next = node.next; 159 | 160 | dists[i] = next.offset - (node.offset + node.size); 161 | indices[i] = i; 162 | nodes[i] = node; 163 | 164 | node = next; 165 | } 166 | // Clear rest of indices so those are not sorted back before size - 1 167 | for (let i = size - 1; i < indices.length; i++) 168 | { 169 | indices[i] = undefined; 170 | } 171 | 172 | // Sort indicies of nodes based on the distances to their successor 173 | indices.sort((i, j) => dists[i] - dists[j]); 174 | 175 | // Coalesce nodes with the least distance 176 | for (let i = 0; i < size - threshold; i++) 177 | { 178 | const idx = indices[i]; 179 | const node = nodes[idx]; 180 | const nextNode = nodes[idx + 1]; 181 | 182 | // Merge modes 183 | this.coalesce(node); 184 | 185 | // Replace all references to nextNode with node. 186 | for (let j = idx + 1; j < size && nodes[j] === nextNode; j++) 187 | { 188 | nodes[j] = node; 189 | } 190 | } 191 | 192 | arrayPool.releaseBuffer(dists); 193 | arrayPool.releaseBuffer(indices); 194 | arrayPool.releaseBuffer(nodes); 195 | } 196 | 197 | /** 198 | * Clears this queue, and returns the nodes to {@code BufferInvalidationPool}. 199 | */ 200 | clear(): this 201 | { 202 | // Release nodes into BufferInvalidationPool 203 | for (let node = this.start; (!!node) as boolean;) 204 | { 205 | const next = node.next; 206 | 207 | BufferInvalidationPool.release(node); 208 | node = next; 209 | } 210 | 211 | // Reset queue 212 | this.start = null; 213 | this.end = null; 214 | this.size = 0; 215 | 216 | return this; 217 | } 218 | 219 | /** 220 | * Coalesces {@code node} and its successor {@code node.next} into one. The successor is released from the 221 | * queue. 222 | * 223 | * @param node - the node with coalesce 224 | * @returns the coalesced node 225 | */ 226 | protected coalesce(node: BufferInvalidation): BufferInvalidation 227 | { 228 | const successor = node.next; 229 | 230 | const offset = node.offset; 231 | const size = successor.offset + successor.size - offset; 232 | 233 | node.size = size; 234 | 235 | // Delete successor from the queue 236 | node.next = successor.next; 237 | successor.next = null; 238 | 239 | // Update end, if needed 240 | if (this.end === successor) 241 | { 242 | this.end = node; 243 | } 244 | 245 | // Update queue's size 246 | --this.size; 247 | 248 | BufferInvalidationPool.release(successor); 249 | 250 | return node; 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /packages/diffy/src/utils/uploadBuffer.ts: -------------------------------------------------------------------------------- 1 | import type { BufferInvalidationQueue } from './BufferInvalidationQueue'; 2 | import type { GeometrySystem } from '@pixi/core'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 5 | export function uploadBuffer(geometrySystem: GeometrySystem, buffer: any): void 6 | { 7 | // eslint-disable-next-line dot-notation 8 | const renderer = geometrySystem['renderer']; 9 | const gl = renderer.gl; 10 | const glBuffer = buffer._glBuffers[(renderer as any).CONTEXT_UID]; 11 | 12 | const type = buffer.index ? gl.ELEMENT_ARRAY_BUFFER : gl.ARRAY_BUFFER; 13 | 14 | if (buffer._updateID === glBuffer.updateID && buffer.updateQueue.length === 0) 15 | { 16 | return; 17 | } 18 | 19 | gl.bindBuffer(type, glBuffer.buffer); 20 | 21 | (geometrySystem as any)._boundBuffer = glBuffer; 22 | 23 | if (buffer._updateID !== glBuffer.updateID || renderer.context.webGLVersion === 1) 24 | { 25 | if (glBuffer.byteLength >= buffer.data.byteLength) 26 | { 27 | // offset is always zero for now! 28 | gl.bufferSubData(type, 0, buffer.data); 29 | } 30 | else 31 | { 32 | const drawType = buffer.static ? gl.STATIC_DRAW : gl.DYNAMIC_DRAW; 33 | 34 | glBuffer.byteLength = buffer.data.byteLength; 35 | gl.bufferData(type, buffer.data, drawType); 36 | } 37 | 38 | glBuffer.updateID = buffer._updateID; 39 | } 40 | else if (buffer.updateQueue.size > 0) 41 | { 42 | const queue: BufferInvalidationQueue = buffer.updateQueue; 43 | const data = buffer.data; 44 | 45 | queue.partition(); 46 | 47 | for (let node = queue.start; node; node = node.next) 48 | { 49 | const { offset, size } = node; 50 | 51 | (gl as WebGL2RenderingContext).bufferSubData( 52 | type, 53 | offset * data.BYTES_PER_ELEMENT, 54 | data, 55 | offset, 56 | size, 57 | ); 58 | } 59 | } 60 | 61 | if (buffer.updateQueue.size > 0) 62 | { 63 | buffer.updateQueue.clear(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/diffy/test/BufferInvalidationQueue.js: -------------------------------------------------------------------------------- 1 | const { BufferInvalidation, BufferInvalidationQueue } = require('../'); 2 | 3 | /** 4 | * @param {BufferInvalidation} result 5 | * @param {BufferInvalidation} expected 6 | */ 7 | function expectNode(result, expected) 8 | { 9 | expect(result.offset).to.equal(expected.offset); 10 | expect(result.size).to.equal(expected.size); 11 | } 12 | 13 | /** 14 | * @param {BufferInvalidation[]} nodes 15 | * @returns {BufferInvalidationQueue} 16 | */ 17 | function buildQueue(nodes) 18 | { 19 | const queue = new BufferInvalidationQueue(); 20 | 21 | for (let i = 0, j = nodes.length; i < j; i++) 22 | { 23 | queue.append(nodes[i]); 24 | } 25 | 26 | return queue; 27 | } 28 | 29 | /** 30 | * @param {BufferInvalidationQueue} result 31 | * @param {BufferInvalidationQueue} expected 32 | */ 33 | function expectQueue(result, expected) 34 | { 35 | expect(result.size).to.equal(expected.size); 36 | 37 | let resultNode = result.start; 38 | let expectedNode = expected.start; 39 | 40 | while (resultNode || expectedNode) 41 | { 42 | expectNode(resultNode, expectedNode); 43 | 44 | resultNode = resultNode.next; 45 | expectedNode = expectedNode.next; 46 | } 47 | } 48 | 49 | describe('PIXI.brend.BufferInvalidationQueue', function () 50 | { 51 | it('should coalesce significant invalidations in the right order', function () 52 | { 53 | const queue = buildQueue([ 54 | new BufferInvalidation().init(0, 200), 55 | new BufferInvalidation().init(220, 200), 56 | 57 | new BufferInvalidation().init(600, 200), 58 | new BufferInvalidation().init(820, 200), 59 | new BufferInvalidation().init(1050, 200), 60 | ]); 61 | 62 | queue.partition(2); 63 | 64 | const expected = buildQueue([ 65 | new BufferInvalidation().init(0, 420), 66 | new BufferInvalidation().init(600, 650), 67 | ]); 68 | 69 | expectQueue(queue, expected); 70 | }); 71 | 72 | it('should coalesce a series of insignificant invalidations', function () 73 | { 74 | const queue = buildQueue([ 75 | new BufferInvalidation().init(0, 16), 76 | new BufferInvalidation().init(32, 16), 77 | new BufferInvalidation().init(48, 16), 78 | new BufferInvalidation().init(64, 16), 79 | new BufferInvalidation().init(80, 16), 80 | new BufferInvalidation().init(96, 16), 81 | 82 | new BufferInvalidation().init(160, 16), 83 | ]); 84 | 85 | queue.partition(); 86 | 87 | const expected = buildQueue([ 88 | new BufferInvalidation().init(0, 112), 89 | new BufferInvalidation().init(160, 16), 90 | ]); 91 | 92 | expectQueue(queue, expected); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /packages/diffy/test/index.js: -------------------------------------------------------------------------------- 1 | require('./BufferInvalidationQueue'); 2 | -------------------------------------------------------------------------------- /packages/diffy/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "allowJs": true, 5 | "sourceMap": true, 6 | "removeComments": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "strictNullChecks": false, 13 | "esModuleInterop": true, 14 | "moduleResolution": "node", 15 | "baseUrl": "./", 16 | "outDir": "compile", 17 | "declaration": true 18 | }, 19 | "include": [ 20 | "src/**/*.ts" 21 | ], 22 | "exclude": [ 23 | "node_modules", 24 | "**/dist/**", 25 | "**/lib/**", 26 | "**/test/**", 27 | "**/scripts/**", 28 | "**/types/**", 29 | "bundles/**/*.d.ts", 30 | "rollup.config.js" 31 | ] 32 | } -------------------------------------------------------------------------------- /rush.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json", 3 | "rushVersion": "5.90.1", 4 | "pnpmVersion": "7.25.0", 5 | "pnpmOptions": { 6 | "pnpmStore": "global", 7 | "strictPeerDependencies": false, 8 | "resolutionStrategy": "fast" 9 | }, 10 | "nodeSupportedVersionRange": ">=10.0.0 <20.0.0", 11 | "projectFolderMinDepth": 2, 12 | "projectFolderMaxDepth": 2, 13 | "repository": { 14 | "url": "https://github.com/pixijs/pixi-batch-renderer", 15 | "defaultBranch": "master", 16 | "defaultRemote": "origin" 17 | }, 18 | "eventHooks": { 19 | "preRushInstall": [], 20 | "postRushInstall": [], 21 | "preRushBuild": [], 22 | "postRushBuild": [] 23 | }, 24 | "telemetryEnabled": false, 25 | "projects": [ 26 | { 27 | "packageName": "@pixi-pbr/diffy", 28 | "projectFolder": "packages/diffy", 29 | "reviewCategory": "production", 30 | "shouldPublish": true 31 | }, 32 | { 33 | "packageName": "pixi-batch-renderer", 34 | "projectFolder": "packages/batch-renderer", 35 | "reviewCategory": "production", 36 | "shouldPublish": true 37 | }, 38 | { 39 | "packageName": "@pixi-pbr/unit-tests", 40 | "projectFolder": "tools/unit-tests", 41 | "reviewCategory": "development", 42 | "shouldPublish": false 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /tools/unit-tests/index.js: -------------------------------------------------------------------------------- 1 | const { floss } = require('floss'); 2 | const path = require('path'); 3 | 4 | function done() 5 | { 6 | /* eslint-disable-next-line no-console */ 7 | console.log('Done?'); 8 | } 9 | 10 | floss({ 11 | path: path.join(__dirname, './test.js'), 12 | quiet: false, 13 | }, done); -------------------------------------------------------------------------------- /tools/unit-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pixi-pbr/unit-tests", 3 | "version": "1.0.0", 4 | "description": "Unit testing for PixiJS Batch Rendering Library", 5 | "main": "index.js", 6 | "private": true, 7 | "scripts": { 8 | "build": "echo @pixi-essentials/unit-tests does not build!", 9 | "test": "floss --path node_modules/@pixi-build-tools/floss-rush-monorepo --debug" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/pixijs/pixi-batch-renderer.git" 14 | }, 15 | "keywords": [ 16 | "pixijs", 17 | "unit-tests", 18 | "pixi-batch-renderer" 19 | ], 20 | "author": "Shukant K. Pal ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/pixijs/pixi-batch-renderer/issues" 24 | }, 25 | "homepage": "https://github.com/pixijs/pixi-batch-renderer#readme", 26 | "dependencies": { 27 | "floss": "^5.0.0", 28 | "@pixi-build-tools/floss-rush-monorepo": "~1.0.1", 29 | "electron": "^12.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tools/unit-tests/test.js: -------------------------------------------------------------------------------- 1 | require('@pixi-build-tools/floss-rush-monorepo'); --------------------------------------------------------------------------------