diff --git a/bun.lock b/bun.lock index 32525e0d3..9bfe8b026 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,8 @@ "version": "0.1.7", "dependencies": { "jimp": "1.6.0", + "typegpu": "^0.7.1", + "unplugin-typegpu": "^0.2.2", "yoga-layout": "3.2.1", }, "devDependencies": { @@ -123,6 +125,8 @@ "@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="], + "@babel/standalone": ["@babel/standalone@7.28.3", "", {}, "sha512-VHmaaU23OkxShTtkwXlte7/uHDK8v55J9YLMqlucjnYujeB9YgrYCHU6LREqUegTVq+/KlLgjoUu8lbeI3XQPA=="], + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], "@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="], @@ -191,6 +195,8 @@ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], @@ -199,6 +205,18 @@ "@opentui/core": ["@opentui/core@workspace:packages/core"], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LdEcuzG9I9jrz2snybqh4WAaEJY3oRbv1eh4fLaiWfr/P9YiSbsN1d30xJY0WUAe0vKvcj1yp/3SlyiyFqqzOg=="], + + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-YuSpHznKbNJAbh3C6ghI/B+w3vNcbJWsgAeI+9BZ2bQfb4U4ThWfj2Wzfayek8VDYkkKJdpjqKEMFT5RD8Gosw=="], + + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-djjd8RnDeKKyFIf1Sig7e0JnKq3qwRIDC1KFKQiR49spOGgCe9eeXxOWlNmx062WEslUk0Ic9FRPkpC8+04Kzw=="], + + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.7", "", { "os": "linux", "cpu": "x64" }, "sha512-8e8x3Lu/vcgIvWKqxWlV9qirkO1krNdIoct1doNNZNVOG2R/OzR/07zNf5H0gTkpjpL4R/TozLDC9jPTSEuLVw=="], + + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-2Vnrj2RPH7m6wI2VwBKO/bGEEcnnBQ/agQeHubKMoY3Wd5NOzXsBs/NAfFBpFunwNF+v6zgfnEjDQ9XU+zmbrQ=="], + + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.7", "", { "os": "win32", "cpu": "x64" }, "sha512-M9Xw6F3jmXF1+V7DHOPgJRypwp9Gb2ToZHg27/mN7sWkBDEDu9x2m4vdaQIoIEY26PqtcYjPOB5Tm0ESpEzZwQ=="], + "@opentui/react": ["@opentui/react@workspace:packages/react"], "@opentui/solid": ["@opentui/solid@workspace:packages/solid"], @@ -217,6 +235,8 @@ "@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], "@types/react": ["@types/react@19.1.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg=="], @@ -233,6 +253,8 @@ "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "any-base": ["any-base@1.1.0", "", {}, "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="], "await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="], @@ -277,12 +299,16 @@ "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + "electron-to-chromium": ["electron-to-chromium@1.5.203", "", {}, "sha512-uz4i0vLhfm6dLZWbz/iH88KNDV+ivj5+2SA+utpgjKaj9Q0iDLuwk6Idhe9BTxciHudyx6IvTvijhkPvFGUQ0g=="], "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], @@ -331,6 +357,10 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], + + "magic-string-ast": ["magic-string-ast@1.0.2", "", { "dependencies": { "magic-string": "^0.30.17" } }, "sha512-8ngQgLhcT0t3YBdn9CGkZqCYlvwW9pm7aWJwd7AxseVWf1RU8ZHCQvG1mt3N5vvUme+pXTcHB8G/7fE666U8Vw=="], + "meshoptimizer": ["meshoptimizer@0.18.1", "", {}, "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw=="], "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], @@ -367,10 +397,14 @@ "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="], "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], @@ -423,18 +457,32 @@ "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], + "tinyest": ["tinyest@0.1.1", "", {}, "sha512-YNHlB8BOXgW6RPzrfqqAkgyY9xj33sjXJcJlOl3MwY0BXXx26m3JUqf5yV8iBdwJPNe51DmxypR9Zbbd266biQ=="], + + "tinyest-for-wgsl": ["tinyest-for-wgsl@0.1.2", "", { "dependencies": { "tinyest": "~0.1.1" } }, "sha512-yJ49SoJIpEi4ADsBVNE54GVJ5JZMIAKNkRueeNpYhIiq0z1Nn9THJNMNP1b9HI0VQt7LzCrxT0ZP29muiUtcRg=="], + "token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="], + "typed-binary": ["typed-binary@4.3.2", "", {}, "sha512-HT3pIBM2njCZUmeczDaQUUErGiM6GXFCqMsHegE12HCoBtvHCkfR10JJni0TeGOTnLilTd6YFyj+YhflqQDrDQ=="], + + "typegpu": ["typegpu@0.7.1", "", { "dependencies": { "tinyest": "~0.1.1", "typed-binary": "^4.3.1" } }, "sha512-PyP/ZRsX8lCX27fNzBE5TjwxC8Vj88RcLlCv4VXg5sVd6gBuiPZTJQoC5HR15hjTJoRAfayT/TXQ4phWOkA/aA=="], + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "unplugin": ["unplugin@2.3.6", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-+/MdXl8bLTXI2lJF22gUBeCFqZruEpL/oM9f8wxCuKh9+Mw9qeul3gTqgbKpMeOFlusCzc0s7x2Kax2xKW+FQg=="], + + "unplugin-typegpu": ["unplugin-typegpu@0.2.2", "", { "dependencies": { "@babel/standalone": "^7.27.0", "defu": "^6.1.4", "estree-walker": "^3.0.3", "magic-string-ast": "^1.0.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "tinyest": "~0.1.1", "tinyest-for-wgsl": "~0.1.2", "unplugin": "^2.3.5" }, "peerDependencies": { "typegpu": "^0.7.0" } }, "sha512-5pbwv0cTMRMxRCQXEPAMUsUoAKMQsz2B4zlhpRRHNy3qtMk+rP3Uxe/5zZUtr2yAASCVO2iciblH5HqcUoijjQ=="], + "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], "utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="], "validate-html-nesting": ["validate-html-nesting@1.2.3", "", {}, "sha512-kdkWdCl6eCeLlRShJKbjVOU2kFKxMF8Ghu50n+crEoyx+VKm3FxAxF9z4DCy6+bbTOqNW0+jcIYRnjoIRzigRw=="], + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + "xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="], "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], diff --git a/packages/core/bunfig.toml b/packages/core/bunfig.toml new file mode 100644 index 000000000..7d029e77c --- /dev/null +++ b/packages/core/bunfig.toml @@ -0,0 +1 @@ +preload = ["./preload.ts"] diff --git a/packages/core/package.json b/packages/core/package.json index 599795e51..361133f7f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -37,7 +37,9 @@ }, "dependencies": { "jimp": "1.6.0", - "yoga-layout": "3.2.1" + "yoga-layout": "3.2.1", + "typegpu": "^0.7.1", + "unplugin-typegpu": "^0.2.2" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", diff --git a/packages/core/preload.ts b/packages/core/preload.ts new file mode 100644 index 000000000..45bde100d --- /dev/null +++ b/packages/core/preload.ts @@ -0,0 +1,4 @@ +import { plugin } from "bun" +import typegpu from "unplugin-typegpu/bun" + +plugin(typegpu({ include: /\.ts$/ })) diff --git a/packages/core/src/3d/canvas.ts b/packages/core/src/3d/canvas.ts index 72ebc2b1e..1fab8cdea 100644 --- a/packages/core/src/3d/canvas.ts +++ b/packages/core/src/3d/canvas.ts @@ -1,15 +1,14 @@ import { GPUCanvasContextMock } from "bun-webgpu" +import { tgpu, type TgpuBuffer, type TgpuComputePipeline, type TgpuRoot, type UniformFlag } from "typegpu" import { RGBA } from "../types" import { SuperSampleType } from "./WGPURenderer" import type { OptimizedBuffer } from "../buffer" +import { createSuperSamplingComputeShader, layout as superSamplingLayout, SuperSamplingParams } from './shaders/supersampling' import { toArrayBuffer } from "bun:ffi" import { Jimp } from "jimp" -// @ts-ignore -import shaderTemplate from "./shaders/supersampling.wgsl" with { type: "text" } - const WORKGROUP_SIZE = 4 -const SUPERSAMPLING_COMPUTE_SHADER = shaderTemplate.replace(/\${WORKGROUP_SIZE}/g, WORKGROUP_SIZE.toString()) +const SUPERSAMPLING_COMPUTE_SHADER = createSuperSamplingComputeShader(WORKGROUP_SIZE) export enum SuperSampleAlgorithm { STANDARD = 0, @@ -18,6 +17,7 @@ export enum SuperSampleAlgorithm { export class CLICanvas { private device: GPUDevice + private root: TgpuRoot private readbackBuffer: GPUBuffer | null = null private width: number private height: number @@ -28,10 +28,9 @@ export class CLICanvas { public superSample: SuperSampleType = SuperSampleType.GPU // Compute shader super sampling - private computePipeline: GPUComputePipeline | null = null - private computeBindGroupLayout: GPUBindGroupLayout | null = null + private computePipeline: TgpuComputePipeline | null = null private computeOutputBuffer: GPUBuffer | null = null - private computeParamsBuffer: GPUBuffer | null = null + private computeParamsBuffer: TgpuBuffer & UniformFlag | null = null private computeReadbackBuffer: GPUBuffer | null = null private updateScheduled: boolean = false private screenshotGPUBuffer: GPUBuffer | null = null @@ -45,6 +44,7 @@ export class CLICanvas { sampleAlgo: SuperSampleAlgorithm = SuperSampleAlgorithm.STANDARD, ) { this.device = device + this.root = tgpu.initFromDevice({ device }) this.width = width this.height = height this.superSample = superSample @@ -163,52 +163,14 @@ export class CLICanvas { private async initComputePipeline(): Promise { if (this.computePipeline) return - const shaderModule = this.device.createShaderModule({ - label: "SuperSampling Compute Shader", - code: SUPERSAMPLING_COMPUTE_SHADER, - }) - - this.computeBindGroupLayout = this.device.createBindGroupLayout({ - label: "SuperSampling Bind Group Layout", - entries: [ - { - binding: 0, - visibility: GPUShaderStage.COMPUTE, - texture: { sampleType: "float", viewDimension: "2d" }, - }, - { - binding: 1, - visibility: GPUShaderStage.COMPUTE, - buffer: { type: "storage" }, - }, - { - binding: 2, - visibility: GPUShaderStage.COMPUTE, - buffer: { type: "uniform" }, - }, - ], - }) + this.computePipeline = this.root['~unstable'] + .withCompute(SUPERSAMPLING_COMPUTE_SHADER) + .createPipeline() - const pipelineLayout = this.device.createPipelineLayout({ - label: "SuperSampling Pipeline Layout", - bindGroupLayouts: [this.computeBindGroupLayout], - }) - - this.computePipeline = this.device.createComputePipeline({ - label: "SuperSampling Compute Pipeline", - layout: pipelineLayout, - compute: { - module: shaderModule, - entryPoint: "main", - }, - }) - - // Create uniform buffer for parameters (8 bytes - 2 u32s: width, height) - this.computeParamsBuffer = this.device.createBuffer({ - label: "SuperSampling Params Buffer", - size: 16, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }) + // Create uniform buffer for parameters + this.computeParamsBuffer = this.root.createBuffer(SuperSamplingParams) + .$usage('uniform') + .$name("SuperSampling Params Buffer") this.updateComputeParams() } @@ -218,14 +180,11 @@ export class CLICanvas { // Update uniform buffer with parameters // Note: this.width/height are render dimensions (2x terminal size for super sampling) - const paramsData = new ArrayBuffer(16) - const uint32View = new Uint32Array(paramsData) - - uint32View[0] = this.width - uint32View[1] = this.height - uint32View[2] = this.superSampleAlgorithm - - this.device.queue.writeBuffer(this.computeParamsBuffer, 0, paramsData) + this.computeParamsBuffer.write({ + width: this.width, + height: this.height, + sampleAlgo: this.superSampleAlgorithm, + }) } private scheduleUpdateComputeBuffers(): void { @@ -278,7 +237,6 @@ export class CLICanvas { if ( !this.computePipeline || - !this.computeBindGroupLayout || !this.computeOutputBuffer || !this.computeParamsBuffer ) { @@ -290,30 +248,24 @@ export class CLICanvas { label: "SuperSampling Input Texture View", }) - const bindGroup = this.device.createBindGroup({ - label: "SuperSampling Bind Group", - layout: this.computeBindGroupLayout, - entries: [ - { binding: 0, resource: textureView }, - { binding: 1, resource: { buffer: this.computeOutputBuffer } }, - { binding: 2, resource: { buffer: this.computeParamsBuffer } }, - ], + const bindGroup = this.root.createBindGroup(superSamplingLayout, { + inputTexture: textureView, + output: this.computeOutputBuffer, + params: this.computeParamsBuffer, }) - const commandEncoder = this.device.createCommandEncoder({ label: "SuperSampling Command Encoder" }) - const computePass = commandEncoder.beginComputePass({ label: "SuperSampling Compute Pass" }) - computePass.setPipeline(this.computePipeline) - computePass.setBindGroup(0, bindGroup) - // Must match WGSL calculation exactly: (params.width + 1u) / 2u const terminalWidthCells = Math.floor((this.width + 1) / 2) const terminalHeightCells = Math.floor((this.height + 1) / 2) const dispatchX = Math.ceil(terminalWidthCells / WORKGROUP_SIZE) const dispatchY = Math.ceil(terminalHeightCells / WORKGROUP_SIZE) - computePass.dispatchWorkgroups(dispatchX, dispatchY, 1) - computePass.end() + this.computePipeline + .with(superSamplingLayout, bindGroup) + .dispatchWorkgroups(dispatchX, dispatchY) + this.root["~unstable"].flush() + const commandEncoder = this.device.createCommandEncoder({ label: "SuperSampling Command Encoder" }) commandEncoder.copyBufferToBuffer( this.computeOutputBuffer, 0, diff --git a/packages/core/src/3d/shaders/supersampling.ts b/packages/core/src/3d/shaders/supersampling.ts new file mode 100644 index 000000000..2c3c7a479 --- /dev/null +++ b/packages/core/src/3d/shaders/supersampling.ts @@ -0,0 +1,228 @@ +import tgpu from "typegpu" +import * as d from "typegpu/data" +import { add, dot, select, textureLoad } from "typegpu/std" + +export const SuperSamplingParams = d.struct({ + /** Canvas width in pixels */ + width: d.u32, + /** Canvas height in pixels */ + height: d.u32, + /** 0 = standard 2x2, 1 = pre-squeezed horizontal blend */ + sampleAlgo: d.size(8, d.u32), + // ^ Padding for 16-byte alignment +}) + +const CellResult = d.struct({ + /** Background RGBA (16 bytes) */ + bg: d.vec4f, + /** Foreground RGBA (16 bytes) */ + fg: d.vec4f, + /** Unicode character code (4 bytes) */ + char: d.size(16, d.u32), + // ^ Padding so that the total size is 48 +}) + +const CellBuffer = (n: number) => + d.struct({ + cells: d.arrayOf(CellResult, n), + }) + +export const layout = tgpu.bindGroupLayout({ + inputTexture: { texture: "float", viewDimension: "2d" }, + output: { storage: CellBuffer, access: "mutable" }, + params: { uniform: SuperSamplingParams }, +}) + +const colorDistance = tgpu.fn( + [d.vec4f, d.vec4f], + d.f32, +)((a, b) => { + const diff = a.sub(b).xyz + return dot(diff, diff) +}) + +const luminance = tgpu.fn([d.vec4f], d.f32)((color) => 0.2126 * color.x + 0.7152 * color.y + 0.0722 * color.z) + +const closestColorIndex = tgpu.fn( + [d.vec4f, d.vec4f, d.vec4f], + d.u32, +)((pixel, candA, candB) => { + return select(d.u32(1), d.u32(0), colorDistance(pixel, candA) <= colorDistance(pixel, candB)) +}) + +const getPixelColor = tgpu.fn( + [d.u32, d.u32], + d.vec4f, +)((pixelX, pixelY) => { + if (pixelX >= layout.$.params.width || pixelY >= layout.$.params.height) { + return d.vec4f(0, 0, 0, 1) // Black for out-of-bounds + } + + // textureLoad automatically handles format conversion to RGBA + return textureLoad(layout.$.inputTexture, d.vec2i(pixelX, pixelY), 0) +}) + +const blendColors = tgpu.fn( + [d.vec4f, d.vec4f], + d.vec4f, +)((color1, color2) => { + const a1 = color1.w + const a2 = color2.w + + if (a1 === 0 && a2 === 0) { + return d.vec4f() + } + + const outAlpha = a1 + a2 - a1 * a2 + if (outAlpha === 0) { + return d.vec4f() + } + + const rgb = add(color1.xyz.mul(a1), color2.xyz.mul(a2 * (1 - a1))).div(outAlpha) + + return d.vec4f(rgb, outAlpha) +}) + +const averageColorsWithAlpha = tgpu.fn( + [d.arrayOf(d.vec4f, 4)], + d.vec4f, +)((pixels) => { + const blend1 = blendColors(pixels[0], pixels[1]) + const blend2 = blendColors(pixels[2], pixels[3]) + + return blendColors(blend1, blend2) +}) + +// Quadrant character lookup table (same as Zig implementation) +const quadrantChars = tgpu["~unstable"].const(d.arrayOf(d.u32, 16), [ + 32, // ' ' - 0000 + 0x2597, // ▗ - 0001 BR + 0x2596, // ▖ - 0010 BL + 0x2584, // ▄ - 0011 Lower Half Block + 0x259d, // ▝ - 0100 TR + 0x2590, // ▐ - 0101 Right Half Block + 0x259e, // ▞ - 0110 TR+BL + 0x259f, // ▟ - 0111 TR+BL+BR + 0x2598, // ▘ - 1000 TL + 0x259a, // ▚ - 1001 TL+BR + 0x258c, // ▌ - 1010 Left Half Block + 0x2599, // ▙ - 1011 TL+BL+BR + 0x2580, // ▀ - 1100 Upper Half Block + 0x259c, // ▜ - 1101 TL+TR+BR + 0x259b, // ▛ - 1110 TL+TR+BL + 0x2588, // █ - 1111 Full Block +]) + +const renderQuadrantBlock = tgpu.fn( + [d.arrayOf(d.vec4f, 4)], + CellResult, +)((pixels) => { + let maxDist = colorDistance(pixels[0], pixels[1]) + let pIdxA = d.u32(0) + let pIdxB = d.u32(1) + + for (let i = d.u32(0); i < 4; i++) { + for (let j = d.u32(i + 1); j < 4; j++) { + const dist = colorDistance(pixels[i], pixels[j]) + if (dist > maxDist) { + pIdxA = i + pIdxB = j + maxDist = dist + } + } + } + + const pCandA = pixels[pIdxA] + const pCandB = pixels[pIdxB] + + let chosenDarkColor = d.vec4f() + let chosenLightColor = d.vec4f() + + if (luminance(pCandA) <= luminance(pCandB)) { + chosenDarkColor = pCandA + chosenLightColor = pCandB + } else { + chosenDarkColor = pCandB + chosenLightColor = pCandA + } + + let quadrantBits = d.u32(0) + const bitValues = [d.u32(8), d.u32(4), d.u32(2), d.u32(1)] // TL, TR, BL, BR + + for (let i = d.u32(0); i < 4; i++) { + if (closestColorIndex(pixels[i], chosenDarkColor, chosenLightColor) === 0) { + quadrantBits |= bitValues[i] + } + } + + // Construct result + const result = CellResult() + + if (quadrantBits === 0) { + // All light + result.char = 32 // Space character + result.fg = chosenDarkColor + result.bg = averageColorsWithAlpha(pixels) + } else if (quadrantBits === 15) { + // All dark + result.char = quadrantChars.$[15] // Full block + result.fg = averageColorsWithAlpha(pixels) + result.bg = chosenLightColor + } else { + // Mixed pattern + result.char = quadrantChars.$[quadrantBits] + result.fg = chosenDarkColor + result.bg = chosenLightColor + } + + return result +}) + +export const createSuperSamplingComputeShader = (WORKGROUP_SIZE: number) => { + const main = tgpu["~unstable"].computeFn({ + workgroupSize: [WORKGROUP_SIZE, WORKGROUP_SIZE], + in: { id: d.builtin.globalInvocationId }, + })((input) => { + const cellX = input.id.x + const cellY = input.id.y + const bufferWidthCells = d.u32((layout.$.params.width + 1) / 2) + const bufferHeightCells = d.u32((layout.$.params.height + 1) / 2) + + if (cellX >= bufferWidthCells || cellY >= bufferHeightCells) { + return + } + + const renderX = cellX * 2 + const renderY = cellY * 2 + + const pixelsRgba = [d.vec4f(), d.vec4f(), d.vec4f(), d.vec4f()] + + if (layout.$.params.sampleAlgo === 1) { + const topColor = getPixelColor(renderX, renderY) + const topColor2 = getPixelColor(renderX + 1, renderY) + + const blendedTop = blendColors(topColor, topColor2) + + const bottomColor = getPixelColor(renderX, renderY + 1) + const bottomColor2 = getPixelColor(renderX + 1, renderY + 1) + const blendedBottom = blendColors(bottomColor, bottomColor2) + + pixelsRgba[0] = blendedTop // TL + pixelsRgba[1] = blendedTop // TR + pixelsRgba[2] = blendedBottom // BL + pixelsRgba[3] = blendedBottom // BR + } else { + pixelsRgba[0] = getPixelColor(renderX, renderY) // TL + pixelsRgba[1] = getPixelColor(renderX + 1, renderY) // TR + pixelsRgba[2] = getPixelColor(renderX, renderY + 1) // BL + pixelsRgba[3] = getPixelColor(renderX + 1, renderY + 1) // BR + } + + const cellResult = renderQuadrantBlock(pixelsRgba) + + const outputIndex = cellY * bufferWidthCells + cellX + layout.$.output.cells[outputIndex] = cellResult + }) + + return main +} diff --git a/packages/core/src/3d/shaders/supersampling.wgsl b/packages/core/src/3d/shaders/supersampling.wgsl deleted file mode 100644 index 514428653..000000000 --- a/packages/core/src/3d/shaders/supersampling.wgsl +++ /dev/null @@ -1,201 +0,0 @@ -struct CellResult { - bg: vec4, // Background RGBA (16 bytes) - fg: vec4, // Foreground RGBA (16 bytes) - char: u32, // Unicode character code (4 bytes) - _padding1: u32, // Padding (4 bytes) - _padding2: u32, // Extra padding (4 bytes) - _padding3: u32, // Extra padding (4 bytes) - total now 48 bytes (16-byte aligned) -}; - -struct CellBuffer { - cells: array -}; - -struct SuperSamplingParams { - width: u32, // Canvas width in pixels - height: u32, // Canvas height in pixels - sampleAlgo: u32, // 0 = standard 2x2, 1 = pre-squeezed horizontal blend - _padding: u32, // Padding for 16-byte alignment -}; - -@group(0) @binding(0) var inputTexture: texture_2d; -@group(0) @binding(1) var output: CellBuffer; -@group(0) @binding(2) var params: SuperSamplingParams; - -// Quadrant character lookup table (same as Zig implementation) -const quadrantChars = array( - 32u, // ' ' - 0000 - 0x2597u, // ▗ - 0001 BR - 0x2596u, // ▖ - 0010 BL - 0x2584u, // ▄ - 0011 Lower Half Block - 0x259Du, // ▝ - 0100 TR - 0x2590u, // ▐ - 0101 Right Half Block - 0x259Eu, // ▞ - 0110 TR+BL - 0x259Fu, // ▟ - 0111 TR+BL+BR - 0x2598u, // ▘ - 1000 TL - 0x259Au, // ▚ - 1001 TL+BR - 0x258Cu, // ▌ - 1010 Left Half Block - 0x2599u, // ▙ - 1011 TL+BL+BR - 0x2580u, // ▀ - 1100 Upper Half Block - 0x259Cu, // ▜ - 1101 TL+TR+BR - 0x259Bu, // ▛ - 1110 TL+TR+BL - 0x2588u // █ - 1111 Full Block -); - -const inv_255: f32 = 1.0 / 255.0; - -fn getPixelColor(pixelX: u32, pixelY: u32) -> vec4 { - if (pixelX >= params.width || pixelY >= params.height) { - return vec4(0.0, 0.0, 0.0, 1.0); // Black for out-of-bounds - } - - // textureLoad automatically handles format conversion to RGBA - return textureLoad(inputTexture, vec2(i32(pixelX), i32(pixelY)), 0); -} - -fn colorDistance(a: vec4, b: vec4) -> f32 { - let diff = a.rgb - b.rgb; - return dot(diff, diff); -} - -fn luminance(color: vec4) -> f32 { - return 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b; -} - -fn closestColorIndex(pixel: vec4, candA: vec4, candB: vec4) -> u32 { - return select(1u, 0u, colorDistance(pixel, candA) <= colorDistance(pixel, candB)); -} - -fn averageColor(pixels: array, 4>) -> vec4 { - return (pixels[0] + pixels[1] + pixels[2] + pixels[3]) * 0.25; -} - -fn blendColors(color1: vec4, color2: vec4) -> vec4 { - let a1 = color1.a; - let a2 = color2.a; - - if (a1 == 0.0 && a2 == 0.0) { - return vec4(0.0, 0.0, 0.0, 0.0); - } - - let outAlpha = a1 + a2 - a1 * a2; - if (outAlpha == 0.0) { - return vec4(0.0, 0.0, 0.0, 0.0); - } - - let rgb = (color1.rgb * a1 + color2.rgb * a2 * (1.0 - a1)) / outAlpha; - - return vec4(rgb, outAlpha); -} - -fn averageColorsWithAlpha(pixels: array, 4>) -> vec4 { - let blend1 = blendColors(pixels[0], pixels[1]); - let blend2 = blendColors(pixels[2], pixels[3]); - - return blendColors(blend1, blend2); -} - -fn renderQuadrantBlock(pixels: array, 4>) -> CellResult { - var maxDist: f32 = colorDistance(pixels[0], pixels[1]); - var pIdxA: u32 = 0u; - var pIdxB: u32 = 1u; - - for (var i: u32 = 0u; i < 4u; i++) { - for (var j: u32 = i + 1u; j < 4u; j++) { - let dist = colorDistance(pixels[i], pixels[j]); - if (dist > maxDist) { - pIdxA = i; - pIdxB = j; - maxDist = dist; - } - } - } - - let pCandA = pixels[pIdxA]; - let pCandB = pixels[pIdxB]; - - var chosenDarkColor: vec4; - var chosenLightColor: vec4; - - if (luminance(pCandA) <= luminance(pCandB)) { - chosenDarkColor = pCandA; - chosenLightColor = pCandB; - } else { - chosenDarkColor = pCandB; - chosenLightColor = pCandA; - } - - var quadrantBits: u32 = 0u; - let bitValues = array(8u, 4u, 2u, 1u); // TL, TR, BL, BR - - for (var i: u32 = 0u; i < 4u; i++) { - if (closestColorIndex(pixels[i], chosenDarkColor, chosenLightColor) == 0u) { - quadrantBits |= bitValues[i]; - } - } - - // Construct result - var result: CellResult; - - if (quadrantBits == 0u) { // All light - result.char = 32u; // Space character - result.fg = chosenDarkColor; - result.bg = averageColorsWithAlpha(pixels); - } else if (quadrantBits == 15u) { // All dark - result.char = quadrantChars[15]; // Full block - result.fg = averageColorsWithAlpha(pixels); - result.bg = chosenLightColor; - } else { // Mixed pattern - result.char = quadrantChars[quadrantBits]; - result.fg = chosenDarkColor; - result.bg = chosenLightColor; - } - result._padding1 = 0u; - result._padding2 = 0u; - result._padding3 = 0u; - - return result; -} - -@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE}, 1) -fn main(@builtin(global_invocation_id) id: vec3) { - let cellX = id.x; - let cellY = id.y; - let bufferWidthCells = (params.width + 1u) / 2u; - let bufferHeightCells = (params.height + 1u) / 2u; - - if (cellX >= bufferWidthCells || cellY >= bufferHeightCells) { - return; - } - - let renderX = cellX * 2u; - let renderY = cellY * 2u; - - var pixelsRgba: array, 4>; - - if (params.sampleAlgo == 1u) { - let topColor = getPixelColor(renderX, renderY); - let topColor2 = getPixelColor(renderX + 1u, renderY); - - let blendedTop = blendColors(topColor, topColor2); - - let bottomColor = getPixelColor(renderX, renderY + 1u); - let bottomColor2 = getPixelColor(renderX + 1u, renderY + 1u); - let blendedBottom = blendColors(bottomColor, bottomColor2); - - pixelsRgba[0] = blendedTop; // TL - pixelsRgba[1] = blendedTop; // TR - pixelsRgba[2] = blendedBottom; // BL - pixelsRgba[3] = blendedBottom; // BR - } else { - pixelsRgba[0] = getPixelColor(renderX, renderY); // TL - pixelsRgba[1] = getPixelColor(renderX + 1u, renderY); // TR - pixelsRgba[2] = getPixelColor(renderX, renderY + 1u); // BL - pixelsRgba[3] = getPixelColor(renderX + 1u, renderY + 1u); // BR - } - - let cellResult = renderQuadrantBlock(pixelsRgba); - - let outputIndex = cellY * bufferWidthCells + cellX; - output.cells[outputIndex] = cellResult; -} \ No newline at end of file