computeShader.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. import { __decorate } from "../tslib.es6.js";
  2. import { serialize } from "../Misc/decorators.js";
  3. import { SerializationHelper } from "../Misc/decorators.serialization.js";
  4. import { RegisterClass } from "../Misc/typeStore.js";
  5. import { ComputeBindingType } from "../Engines/Extensions/engine.computeShader.js";
  6. import { Texture } from "../Materials/Textures/texture.js";
  7. import { UniqueIdGenerator } from "../Misc/uniqueIdGenerator.js";
  8. import { Logger } from "../Misc/logger.js";
  9. import { TextureSampler } from "../Materials/Textures/textureSampler.js";
  10. import { WebGPUPerfCounter } from "../Engines/WebGPU/webgpuPerfCounter.js";
  11. /**
  12. * The ComputeShader object lets you execute a compute shader on your GPU (if supported by the engine)
  13. */
  14. export class ComputeShader {
  15. /**
  16. * The options used to create the shader
  17. */
  18. get options() {
  19. return this._options;
  20. }
  21. /**
  22. * The shaderPath used to create the shader
  23. */
  24. get shaderPath() {
  25. return this._shaderPath;
  26. }
  27. /**
  28. * Instantiates a new compute shader.
  29. * @param name Defines the name of the compute shader in the scene
  30. * @param engine Defines the engine the compute shader belongs to
  31. * @param shaderPath Defines the route to the shader code in one of three ways:
  32. * * object: \{ compute: "custom" \}, used with ShaderStore.ShadersStoreWGSL["customComputeShader"]
  33. * * object: \{ computeElement: "HTMLElementId" \}, used with shader code in script tags
  34. * * object: \{ computeSource: "compute shader code string" \}, where the string contains the shader code
  35. * * string: try first to find the code in ShaderStore.ShadersStoreWGSL[shaderPath + "ComputeShader"]. If not, assumes it is a file with name shaderPath.compute.fx in index.html folder.
  36. * @param options Define the options used to create the shader
  37. */
  38. constructor(name, engine, shaderPath, options = {}) {
  39. this._bindings = {};
  40. this._samplers = {};
  41. this._contextIsDirty = false;
  42. /**
  43. * When set to true, dispatch won't call isReady anymore and won't check if the underlying GPU resources should be (re)created because of a change in the inputs (texture, uniform buffer, etc.)
  44. * If you know that your inputs did not change since last time dispatch was called and that isReady() returns true, set this flag to true to improve performance
  45. */
  46. this.fastMode = false;
  47. /**
  48. * Callback triggered when the shader is compiled
  49. */
  50. this.onCompiled = null;
  51. /**
  52. * Callback triggered when an error occurs
  53. */
  54. this.onError = null;
  55. this.name = name;
  56. this._engine = engine;
  57. this.uniqueId = UniqueIdGenerator.UniqueId;
  58. if (engine.enableGPUTimingMeasurements) {
  59. this.gpuTimeInFrame = new WebGPUPerfCounter();
  60. }
  61. if (!this._engine.getCaps().supportComputeShaders) {
  62. Logger.Error("This engine does not support compute shaders!");
  63. return;
  64. }
  65. if (!options.bindingsMapping) {
  66. Logger.Error("You must provide the binding mappings as browsers don't support reflection for wgsl shaders yet!");
  67. return;
  68. }
  69. this._context = engine.createComputeContext();
  70. this._shaderPath = shaderPath;
  71. this._options = {
  72. bindingsMapping: {},
  73. defines: [],
  74. ...options,
  75. };
  76. }
  77. /**
  78. * Gets the current class name of the material e.g. "ComputeShader"
  79. * Mainly use in serialization.
  80. * @returns the class name
  81. */
  82. getClassName() {
  83. return "ComputeShader";
  84. }
  85. /**
  86. * Binds a texture to the shader
  87. * @param name Binding name of the texture
  88. * @param texture Texture to bind
  89. * @param bindSampler Bind the sampler corresponding to the texture (default: true). The sampler will be bound just before the binding index of the texture
  90. */
  91. setTexture(name, texture, bindSampler = true) {
  92. const current = this._bindings[name];
  93. this._bindings[name] = {
  94. type: bindSampler ? ComputeBindingType.Texture : ComputeBindingType.TextureWithoutSampler,
  95. object: texture,
  96. indexInGroupEntries: current?.indexInGroupEntries,
  97. };
  98. this._contextIsDirty || (this._contextIsDirty = !current || current.object !== texture || current.type !== this._bindings[name].type);
  99. }
  100. /**
  101. * Binds a storage texture to the shader
  102. * @param name Binding name of the texture
  103. * @param texture Texture to bind
  104. */
  105. setStorageTexture(name, texture) {
  106. const current = this._bindings[name];
  107. this._contextIsDirty || (this._contextIsDirty = !current || current.object !== texture);
  108. this._bindings[name] = {
  109. type: ComputeBindingType.StorageTexture,
  110. object: texture,
  111. indexInGroupEntries: current?.indexInGroupEntries,
  112. };
  113. }
  114. /**
  115. * Binds an external texture to the shader
  116. * @param name Binding name of the texture
  117. * @param texture Texture to bind
  118. */
  119. setExternalTexture(name, texture) {
  120. const current = this._bindings[name];
  121. this._contextIsDirty || (this._contextIsDirty = !current || current.object !== texture);
  122. this._bindings[name] = {
  123. type: ComputeBindingType.ExternalTexture,
  124. object: texture,
  125. indexInGroupEntries: current?.indexInGroupEntries,
  126. };
  127. }
  128. /**
  129. * Binds a video texture to the shader (by binding the external texture attached to this video)
  130. * @param name Binding name of the texture
  131. * @param texture Texture to bind
  132. * @returns true if the video texture was successfully bound, else false. false will be returned if the current engine does not support external textures
  133. */
  134. setVideoTexture(name, texture) {
  135. if (texture.externalTexture) {
  136. this.setExternalTexture(name, texture.externalTexture);
  137. return true;
  138. }
  139. return false;
  140. }
  141. /**
  142. * Binds a uniform buffer to the shader
  143. * @param name Binding name of the buffer
  144. * @param buffer Buffer to bind
  145. */
  146. setUniformBuffer(name, buffer) {
  147. const current = this._bindings[name];
  148. this._contextIsDirty || (this._contextIsDirty = !current || current.object !== buffer);
  149. this._bindings[name] = {
  150. type: ComputeShader._BufferIsDataBuffer(buffer) ? ComputeBindingType.DataBuffer : ComputeBindingType.UniformBuffer,
  151. object: buffer,
  152. indexInGroupEntries: current?.indexInGroupEntries,
  153. };
  154. }
  155. /**
  156. * Binds a storage buffer to the shader
  157. * @param name Binding name of the buffer
  158. * @param buffer Buffer to bind
  159. */
  160. setStorageBuffer(name, buffer) {
  161. const current = this._bindings[name];
  162. this._contextIsDirty || (this._contextIsDirty = !current || current.object !== buffer);
  163. this._bindings[name] = {
  164. type: ComputeShader._BufferIsDataBuffer(buffer) ? ComputeBindingType.DataBuffer : ComputeBindingType.StorageBuffer,
  165. object: buffer,
  166. indexInGroupEntries: current?.indexInGroupEntries,
  167. };
  168. }
  169. /**
  170. * Binds a texture sampler to the shader
  171. * @param name Binding name of the sampler
  172. * @param sampler Sampler to bind
  173. */
  174. setTextureSampler(name, sampler) {
  175. const current = this._bindings[name];
  176. this._contextIsDirty || (this._contextIsDirty = !current || !sampler.compareSampler(current.object));
  177. this._bindings[name] = {
  178. type: ComputeBindingType.Sampler,
  179. object: sampler,
  180. indexInGroupEntries: current?.indexInGroupEntries,
  181. };
  182. }
  183. /**
  184. * Specifies that the compute shader is ready to be executed (the compute effect and all the resources are ready)
  185. * @returns true if the compute shader is ready to be executed
  186. */
  187. isReady() {
  188. let effect = this._effect;
  189. for (const key in this._bindings) {
  190. const binding = this._bindings[key], type = binding.type, object = binding.object;
  191. switch (type) {
  192. case ComputeBindingType.Texture:
  193. case ComputeBindingType.TextureWithoutSampler:
  194. case ComputeBindingType.StorageTexture: {
  195. const texture = object;
  196. if (!texture.isReady()) {
  197. return false;
  198. }
  199. break;
  200. }
  201. case ComputeBindingType.ExternalTexture: {
  202. const texture = object;
  203. if (!texture.isReady()) {
  204. return false;
  205. }
  206. break;
  207. }
  208. }
  209. }
  210. const defines = [];
  211. const shaderName = this._shaderPath;
  212. if (this._options.defines) {
  213. for (let index = 0; index < this._options.defines.length; index++) {
  214. defines.push(this._options.defines[index]);
  215. }
  216. }
  217. const join = defines.join("\n");
  218. if (this._cachedDefines !== join) {
  219. this._cachedDefines = join;
  220. effect = this._engine.createComputeEffect(shaderName, {
  221. defines: join,
  222. entryPoint: this._options.entryPoint,
  223. onCompiled: this.onCompiled,
  224. onError: this.onError,
  225. });
  226. this._effect = effect;
  227. }
  228. if (!effect.isReady()) {
  229. return false;
  230. }
  231. return true;
  232. }
  233. /**
  234. * Dispatches (executes) the compute shader
  235. * @param x Number of workgroups to execute on the X dimension
  236. * @param y Number of workgroups to execute on the Y dimension (default: 1)
  237. * @param z Number of workgroups to execute on the Z dimension (default: 1)
  238. * @returns True if the dispatch could be done, else false (meaning either the compute effect or at least one of the bound resources was not ready)
  239. */
  240. dispatch(x, y, z) {
  241. if (!this.fastMode && !this._checkContext()) {
  242. return false;
  243. }
  244. this._engine.computeDispatch(this._effect, this._context, this._bindings, x, y, z, this._options.bindingsMapping, this.gpuTimeInFrame);
  245. return true;
  246. }
  247. /**
  248. * Dispatches (executes) the compute shader.
  249. * @param buffer Buffer containing the number of workgroups to execute on the X, Y and Z dimensions
  250. * @param offset Offset in the buffer where the workgroup counts are stored (default: 0)
  251. * @returns True if the dispatch could be done, else false (meaning either the compute effect or at least one of the bound resources was not ready)
  252. */
  253. dispatchIndirect(buffer, offset = 0) {
  254. if (!this.fastMode && !this._checkContext()) {
  255. return false;
  256. }
  257. const dataBuffer = ComputeShader._BufferIsDataBuffer(buffer) ? buffer : buffer.getBuffer();
  258. this._engine.computeDispatchIndirect(this._effect, this._context, this._bindings, dataBuffer, offset, this._options.bindingsMapping, this.gpuTimeInFrame);
  259. return true;
  260. }
  261. _checkContext() {
  262. if (!this.isReady()) {
  263. return false;
  264. }
  265. // If the sampling parameters of a texture bound to the shader have changed, we must clear the compute context so that it is recreated with the updated values
  266. // Also, if the actual (gpu) buffer used by a uniform buffer has changed, we must clear the compute context so that it is recreated with the updated value
  267. for (const key in this._bindings) {
  268. const binding = this._bindings[key];
  269. if (!this._options.bindingsMapping[key]) {
  270. throw new Error("ComputeShader ('" + this.name + "'): No binding mapping has been provided for the property '" + key + "'");
  271. }
  272. switch (binding.type) {
  273. case ComputeBindingType.Texture: {
  274. const sampler = this._samplers[key];
  275. const texture = binding.object;
  276. if (!sampler || !texture._texture || !sampler.compareSampler(texture._texture)) {
  277. this._samplers[key] = new TextureSampler().setParameters(texture.wrapU, texture.wrapV, texture.wrapR, texture.anisotropicFilteringLevel, texture._texture.samplingMode, texture._texture?._comparisonFunction);
  278. this._contextIsDirty = true;
  279. }
  280. break;
  281. }
  282. case ComputeBindingType.ExternalTexture: {
  283. // we must recreate the bind groups each time if there's an external texture, because device.importExternalTexture must be called each frame
  284. this._contextIsDirty = true;
  285. break;
  286. }
  287. case ComputeBindingType.UniformBuffer: {
  288. const ubo = binding.object;
  289. if (ubo.getBuffer() !== binding.buffer) {
  290. binding.buffer = ubo.getBuffer();
  291. this._contextIsDirty = true;
  292. }
  293. break;
  294. }
  295. }
  296. }
  297. if (this._contextIsDirty) {
  298. this._contextIsDirty = false;
  299. this._context.clear();
  300. }
  301. return true;
  302. }
  303. /**
  304. * Waits for the compute shader to be ready and executes it
  305. * @param x Number of workgroups to execute on the X dimension
  306. * @param y Number of workgroups to execute on the Y dimension (default: 1)
  307. * @param z Number of workgroups to execute on the Z dimension (default: 1)
  308. * @param delay Delay between the retries while the shader is not ready (in milliseconds - 10 by default)
  309. * @returns A promise that is resolved once the shader has been sent to the GPU. Note that it does not mean that the shader execution itself is finished!
  310. */
  311. dispatchWhenReady(x, y, z, delay = 10) {
  312. return new Promise((resolve) => {
  313. const check = () => {
  314. if (!this.dispatch(x, y, z)) {
  315. setTimeout(check, delay);
  316. }
  317. else {
  318. resolve();
  319. }
  320. };
  321. check();
  322. });
  323. }
  324. /**
  325. * Serializes this compute shader in a JSON representation
  326. * @returns the serialized compute shader object
  327. */
  328. serialize() {
  329. const serializationObject = SerializationHelper.Serialize(this);
  330. serializationObject.options = this._options;
  331. serializationObject.shaderPath = this._shaderPath;
  332. serializationObject.bindings = {};
  333. serializationObject.textures = {};
  334. for (const key in this._bindings) {
  335. const binding = this._bindings[key];
  336. const object = binding.object;
  337. switch (binding.type) {
  338. case ComputeBindingType.Texture:
  339. case ComputeBindingType.TextureWithoutSampler:
  340. case ComputeBindingType.StorageTexture: {
  341. const serializedData = object.serialize();
  342. if (serializedData) {
  343. serializationObject.textures[key] = serializedData;
  344. serializationObject.bindings[key] = {
  345. type: binding.type,
  346. };
  347. }
  348. break;
  349. }
  350. case ComputeBindingType.UniformBuffer: {
  351. break;
  352. }
  353. }
  354. }
  355. return serializationObject;
  356. }
  357. /**
  358. * Creates a compute shader from parsed compute shader data
  359. * @param source defines the JSON representation of the compute shader
  360. * @param scene defines the hosting scene
  361. * @param rootUrl defines the root URL to use to load textures and relative dependencies
  362. * @returns a new compute shader
  363. */
  364. static Parse(source, scene, rootUrl) {
  365. const compute = SerializationHelper.Parse(() => new ComputeShader(source.name, scene.getEngine(), source.shaderPath, source.options), source, scene, rootUrl);
  366. for (const key in source.textures) {
  367. const binding = source.bindings[key];
  368. const texture = Texture.Parse(source.textures[key], scene, rootUrl);
  369. if (binding.type === ComputeBindingType.Texture) {
  370. compute.setTexture(key, texture);
  371. }
  372. else if (binding.type === ComputeBindingType.TextureWithoutSampler) {
  373. compute.setTexture(key, texture, false);
  374. }
  375. else {
  376. compute.setStorageTexture(key, texture);
  377. }
  378. }
  379. return compute;
  380. }
  381. static _BufferIsDataBuffer(buffer) {
  382. return buffer.underlyingResource !== undefined;
  383. }
  384. }
  385. __decorate([
  386. serialize()
  387. ], ComputeShader.prototype, "name", void 0);
  388. __decorate([
  389. serialize()
  390. ], ComputeShader.prototype, "fastMode", void 0);
  391. RegisterClass("BABYLON.ComputeShader", ComputeShader);
  392. //# sourceMappingURL=computeShader.js.map