typegpu 0.12.5 → 0.12.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/common/index.d.ts +1 -0
  2. package/common/index.js +1 -0
  3. package/common/writeChannels.d.ts +31 -0
  4. package/common/writeChannels.js +32 -0
  5. package/common/writeSoA.js +16 -0
  6. package/core/buffer/buffer.js +47 -11
  7. package/core/commandEncoder/attachments.js +5 -2
  8. package/core/commandEncoder/computePass.d.ts +13 -0
  9. package/core/commandEncoder/computePass.js +5 -2
  10. package/core/commandEncoder/renderPass.d.ts +18 -4
  11. package/core/commandEncoder/renderPass.js +6 -2
  12. package/core/declare/tgpuDeclare.d.ts +11 -0
  13. package/core/declare/tgpuDeclare.js +1 -1
  14. package/core/function/autoIO.js +4 -2
  15. package/core/function/ioSchema.d.ts +7 -2
  16. package/core/function/ioSchema.js +41 -15
  17. package/core/function/tgpuFragmentFn.d.ts +1 -1
  18. package/core/function/tgpuFragmentFn.js +2 -1
  19. package/core/function/tgpuVertexFn.js +2 -1
  20. package/core/immediate/immediateVar.d.ts +52 -0
  21. package/core/immediate/immediateVar.js +149 -0
  22. package/core/pipeline/computePipeline.d.ts +17 -0
  23. package/core/pipeline/computePipeline.js +23 -9
  24. package/core/pipeline/connectAttachmentToShader.d.ts +2 -2
  25. package/core/pipeline/connectAttachmentToShader.js +16 -21
  26. package/core/pipeline/connectTargetsToShader.d.ts +5 -1
  27. package/core/pipeline/connectTargetsToShader.js +23 -18
  28. package/core/pipeline/drawState.d.ts +8 -4
  29. package/core/pipeline/drawState.js +41 -51
  30. package/core/pipeline/priors.js +1 -0
  31. package/core/pipeline/renderPipeline.d.ts +22 -4
  32. package/core/pipeline/renderPipeline.js +25 -13
  33. package/core/resolve/namespace.js +2 -2
  34. package/core/root/init.js +3 -0
  35. package/core/root/rootTypes.d.ts +17 -2
  36. package/core/slot/accessor.d.ts +1 -1
  37. package/core/slot/accessor.js +72 -5
  38. package/core/slot/slotTypes.d.ts +2 -1
  39. package/core/texture/texture.d.ts +20 -14
  40. package/core/texture/texture.js +78 -69
  41. package/core/texture/textureUtils.d.ts +3 -6
  42. package/core/texture/textureUtils.js +142 -165
  43. package/core/texture/textureWrite.d.ts +61 -0
  44. package/core/texture/textureWrite.js +59 -0
  45. package/data/autoStruct.d.ts +1 -1
  46. package/data/autoStruct.js +8 -4
  47. package/data/dataIO.d.ts +2 -2
  48. package/data/deepEqual.js +19 -9
  49. package/data/matrix.js +10 -3
  50. package/data/partialIO.d.ts +8 -1
  51. package/data/partialIO.js +36 -19
  52. package/data/ref.js +2 -2
  53. package/data/snippet.d.ts +11 -5
  54. package/data/snippet.js +9 -4
  55. package/data/wgslTypes.d.ts +4 -1
  56. package/errors.d.ts +7 -0
  57. package/errors.js +15 -0
  58. package/indexNamedExports.d.ts +5 -2
  59. package/indexNamedExports.js +2 -1
  60. package/internal.d.ts +2 -1
  61. package/internal.js +1 -1
  62. package/nameUtils.d.ts +7 -0
  63. package/nameUtils.js +60 -0
  64. package/package.json +2 -2
  65. package/resolutionCtx.d.ts +5 -0
  66. package/resolutionCtx.js +11 -0
  67. package/serial/restore.d.ts +3 -1
  68. package/serial/restore.js +2 -0
  69. package/shared/meta.js +1 -1
  70. package/shared/symbols.js +1 -1
  71. package/shared/tseynit.d.ts +1 -0
  72. package/shared/tseynit.js +80 -44
  73. package/std/atomic.d.ts +13 -0
  74. package/std/atomic.js +63 -29
  75. package/std/boolean.js +9 -10
  76. package/std/derivative.js +14 -9
  77. package/std/index.d.ts +4 -4
  78. package/std/index.js +5 -5
  79. package/std/numeric.d.ts +5 -1
  80. package/std/numeric.js +3 -3
  81. package/std/operators.js +9 -4
  82. package/std/packing.d.ts +61 -1
  83. package/std/packing.js +214 -25
  84. package/std/subgroup.d.ts +4 -0
  85. package/std/subgroup.js +34 -0
  86. package/std/texture.d.ts +15 -2
  87. package/std/texture.js +48 -0
  88. package/tgpuUnstable.d.ts +1 -0
  89. package/tgpuUnstable.js +1 -0
  90. package/tgsl/accessIndex.js +19 -23
  91. package/tgsl/conversion.d.ts +2 -0
  92. package/tgsl/conversion.js +3 -0
  93. package/tgsl/forOfUtils.js +6 -6
  94. package/tgsl/generationHelpers.js +2 -2
  95. package/tgsl/shaderGenerator.d.ts +1 -1
  96. package/tgsl/wgslGenerator.d.ts +5 -0
  97. package/tgsl/wgslGenerator.js +288 -58
  98. package/types.d.ts +9 -1
  99. package/types.js +5 -0
@@ -5,7 +5,8 @@ import { getName, setName } from "../../shared/meta.js";
5
5
  import { getTextureFormatInfo, } from "./textureFormats.js";
6
6
  import { $gpuValueOf, $internal, $ownSnippet, $repr, $resolve, $soul, } from "../../shared/symbols.js";
7
7
  import { valueProxyHandler } from "../valueProxyUtils.js";
8
- import { generateTextureMipmaps, getImageSourceDimensions, resampleImage } from "./textureUtils.js";
8
+ import { clearTextureWithColor, generateTextureMipmaps, writeImages } from "./textureUtils.js";
9
+ import { mipLevelSize, origin3d, resolveImageWrite, writeRegionSize, } from "./textureWrite.js";
9
10
  import { logger } from "../../tgpuLogger.js";
10
11
  function getDescriptorForProps(props) {
11
12
  return {
@@ -128,28 +129,25 @@ class TgpuTextureImpl {
128
129
  return new TgpuFixedTextureViewImpl(schema ?? textureDescriptorToSchema(getDescriptorForProps(this.props)), this, viewDescriptor);
129
130
  }
130
131
  #clearMipLevel(mip = 0) {
131
- const scale = 2 ** mip;
132
- const [width, height, depth] = [
133
- Math.max(1, Math.floor((this.props.size[0] ?? 1) / scale)),
134
- Math.max(1, Math.floor((this.props.size[1] ?? 1) / scale)),
135
- Math.max(1, Math.floor((this.props.size[2] ?? 1) / scale)),
136
- ];
132
+ const [width, height, depth] = mipLevelSize(this.props, mip);
137
133
  const texelSize = this.#formatInfo.texelSize;
138
134
  if (texelSize === 'non-copyable') {
139
135
  throw new Error(`Cannot clear texture with format '${this.props.format}': this format does not support copy operations.`);
140
136
  }
141
137
  this[$soul].device.queue.writeTexture({ texture: this[$internal].materialize(), mipLevel: mip }, new Uint8Array(width * height * depth * texelSize), { bytesPerRow: texelSize * width, rowsPerImage: height }, [width, height, depth]);
142
138
  }
143
- clear(mipLevel = 'all') {
144
- if (mipLevel === 'all') {
145
- const mipLevels = this.props.mipLevelCount ?? 1;
146
- for (let i = 0; i < mipLevels; i++) {
147
- this.#clearMipLevel(i);
139
+ clear(colorOrMipLevel = 'all', mipLevel = 'all') {
140
+ const mipLevels = (level) => level === 'all' ? [...Array(this.props.mipLevelCount ?? 1).keys()] : [level];
141
+ if (typeof colorOrMipLevel !== 'object') {
142
+ for (const mip of mipLevels(colorOrMipLevel)) {
143
+ this.#clearMipLevel(mip);
148
144
  }
145
+ return;
149
146
  }
150
- else {
151
- this.#clearMipLevel(mipLevel);
147
+ if (!this.usableAsRender) {
148
+ throw new Error("texture.clear(color) requires 'render' usage. Add it via the $usage('render') method.");
152
149
  }
150
+ clearTextureWithColor(this[$soul].device, this[$internal].materialize(), colorOrMipLevel, mipLevels(mipLevel));
153
151
  }
154
152
  generateMipmaps(baseMipLevel = 0, mipLevels) {
155
153
  if (!this.usableAsRender) {
@@ -165,81 +163,88 @@ class TgpuTextureImpl {
165
163
  }
166
164
  generateTextureMipmaps(this[$soul].device, this[$internal].materialize(), baseMipLevel, actualMipLevels);
167
165
  }
168
- write(source, optionsOrMipLevel = 0) {
166
+ write(source, options = {}) {
169
167
  if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) {
170
- this.#writeBufferData(source, typeof optionsOrMipLevel === 'number' ? optionsOrMipLevel : 0);
168
+ this.#writeBufferData(source, typeof options === 'number' ? { mipLevel: options } : options);
171
169
  return;
172
170
  }
173
171
  if (!this.usableAsRender) {
174
172
  throw new Error("texture.write(...) with image sources requires 'render' usage. Add it via the $usage('render') method.");
175
173
  }
176
- const options = typeof optionsOrMipLevel === 'number' ? undefined : optionsOrMipLevel;
177
- const dimension = this.props.dimension ?? '2d';
178
- const isArray = Array.isArray(source);
179
- if (!isArray) {
180
- this.#writeSingleLayer(source, dimension === '3d' ? 0 : undefined, options);
181
- return;
174
+ const imageOptions = options;
175
+ const sources = Array.isArray(source) ? source : [source];
176
+ const baseLayer = imageOptions.origin?.[2] ?? 0;
177
+ const layerCount = mipLevelSize(this.props, imageOptions.mipLevel ?? 0)[2];
178
+ if (baseLayer + sources.length > layerCount) {
179
+ logger.warn('suspicious', `Too many image sources provided. Texture has ${layerCount} layers, got ${sources.length} sources starting at layer ${baseLayer}. Extra sources will be ignored.`);
182
180
  }
183
- const layerCount = this.props.size[2] ?? 1;
184
- if (source.length > layerCount) {
185
- logger.warn('suspicious', `Too many image sources provided. Expected ${layerCount} layers, got ${source.length}. Extra sources will be ignored.`);
186
- }
187
- for (let layer = 0; layer < Math.min(source.length, layerCount); layer++) {
188
- const bitmap = source[layer];
189
- if (bitmap) {
190
- this.#writeSingleLayer(bitmap, layer, options);
181
+ writeImages(this[$soul].device, this[$internal].materialize(), sources
182
+ .slice(0, layerCount - baseLayer)
183
+ .map((image, i) => resolveImageWrite(this.props, image, imageOptions, baseLayer + i)));
184
+ }
185
+ async writeAsync(source, options = {}) {
186
+ const [resizeWidth, resizeHeight] = writeRegionSize(this.props, options);
187
+ const bitmap = await createImageBitmap(source, options.fit === 'stretch'
188
+ ? {
189
+ resizeWidth,
190
+ resizeHeight,
191
+ resizeQuality: options.filter === 'nearest' ? 'pixelated' : 'high',
191
192
  }
193
+ : undefined);
194
+ try {
195
+ this.write(bitmap, options);
196
+ }
197
+ finally {
198
+ bitmap.close();
192
199
  }
193
200
  }
194
- #writeBufferData(source, mipLevel) {
195
- const mipWidth = Math.max(1, this.props.size[0] >> mipLevel);
196
- const mipHeight = Math.max(1, (this.props.size[1] ?? 1) >> mipLevel);
197
- const mipDepth = Math.max(1, (this.props.size[2] ?? 1) >> mipLevel);
201
+ #writeBufferData(source, options) {
198
202
  const texelSize = this.#formatInfo.texelSize;
199
203
  if (texelSize === 'non-copyable') {
200
204
  throw new Error(`Cannot write to texture with format '${this.props.format}': this format does not support copy operations.`);
201
205
  }
202
- const expectedSize = mipWidth * mipHeight * mipDepth * texelSize;
203
- const actualSize = source.byteLength ?? source.byteLength;
204
- if (actualSize !== expectedSize) {
205
- throw new Error(`Buffer size mismatch. Expected ${expectedSize} bytes for mip level ${mipLevel}, got ${actualSize} bytes.`);
206
+ const mipLevel = options.mipLevel ?? 0;
207
+ const [mipWidth, mipHeight, mipDepth] = mipLevelSize(this.props, mipLevel);
208
+ const origin = origin3d(options.origin);
209
+ const width = options.size?.[0] ?? mipWidth - origin.x;
210
+ const height = options.size?.[1] ?? mipHeight - origin.y;
211
+ const depth = options.size?.[2] ?? mipDepth - origin.z;
212
+ const expectedSize = width * height * depth * texelSize;
213
+ if (source.byteLength !== expectedSize) {
214
+ throw new Error(`Buffer size mismatch. Expected ${expectedSize} bytes for mip level ${mipLevel}, got ${source.byteLength} bytes.`);
206
215
  }
207
216
  this[$soul].device.queue.writeTexture({
208
217
  texture: this[$internal].materialize(),
209
218
  mipLevel,
210
- }, 'buffer' in source ? source.buffer : source, {
211
- bytesPerRow: texelSize * mipWidth,
212
- rowsPerImage: mipHeight,
213
- }, [mipWidth, mipHeight, mipDepth]);
214
- }
215
- #writeSingleLayer(source, layer, options) {
216
- const targetWidth = this.props.size[0];
217
- const targetHeight = this.props.size[1] ?? 1;
218
- const { width: sourceWidth, height: sourceHeight } = getImageSourceDimensions(source);
219
- const needsResampling = sourceWidth !== targetWidth || sourceHeight !== targetHeight;
220
- if (needsResampling) {
221
- if (options?.fit !== 'stretch') {
222
- throw new Error(`Texture write source size ${sourceWidth}x${sourceHeight} does not match target size ${targetWidth}x${targetHeight}. Pass fit: 'stretch' to resize explicitly.`);
223
- }
224
- resampleImage(this[$soul].device, this[$internal].materialize(), source, layer);
225
- return;
226
- }
227
- this[$soul].device.queue.copyExternalImageToTexture({ source }, {
228
- texture: this[$internal].materialize(),
229
- ...(layer !== undefined && { origin: { x: 0, y: 0, z: layer } }),
230
- }, layer !== undefined ? [targetWidth, targetHeight, 1] : this.props.size);
219
+ origin,
220
+ }, source, {
221
+ bytesPerRow: texelSize * width,
222
+ rowsPerImage: height,
223
+ }, [width, height, depth]);
231
224
  }
232
- copyFrom(source) {
225
+ copyFrom(source, options) {
233
226
  if (source.props.format !== this.props.format) {
234
227
  throw new Error(`Texture format mismatch. Source texture has format ${source.props.format}, target texture has format ${this.props.format}`);
235
228
  }
236
- if (source.props.size[0] !== this.props.size[0] ||
237
- (source.props.size[1] ?? 1) !== (this.props.size[1] ?? 1) ||
238
- (source.props.size[2] ?? 1) !== (this.props.size[2] ?? 1)) {
239
- throw new Error(`Texture size mismatch. Source texture has size ${source.props.size.join('x')}, target texture has size ${this.props.size.join('x')}`);
229
+ if (!options &&
230
+ (source.props.size[0] !== this.props.size[0] ||
231
+ (source.props.size[1] ?? 1) !== (this.props.size[1] ?? 1) ||
232
+ (source.props.size[2] ?? 1) !== (this.props.size[2] ?? 1))) {
233
+ throw new Error(`Texture size mismatch. Source texture has size ${source.props.size.join('x')}, target texture has size ${this.props.size.join('x')}. Pass copy options to copy a region.`);
240
234
  }
235
+ const sourceMipLevel = options?.sourceMipLevel ?? 0;
236
+ const sourceOrigin = origin3d(options?.sourceOrigin);
237
+ const [sourceWidth, sourceHeight, sourceDepth] = mipLevelSize(source.props, sourceMipLevel);
241
238
  const commandEncoder = this[$soul].device.createCommandEncoder();
242
- commandEncoder.copyTextureToTexture({ texture: source[$internal].materialize() }, { texture: this[$internal].materialize() }, source.props.size);
239
+ commandEncoder.copyTextureToTexture({ texture: source[$internal].materialize(), mipLevel: sourceMipLevel, origin: sourceOrigin }, {
240
+ texture: this[$internal].materialize(),
241
+ mipLevel: options?.mipLevel ?? 0,
242
+ origin: origin3d(options?.origin),
243
+ }, [
244
+ options?.size?.[0] ?? sourceWidth - sourceOrigin.x,
245
+ options?.size?.[1] ?? sourceHeight - sourceOrigin.y,
246
+ options?.size?.[2] ?? sourceDepth - sourceOrigin.z,
247
+ ]);
243
248
  this[$soul].device.queue.submit([commandEncoder.finish()]);
244
249
  }
245
250
  toString() {
@@ -400,10 +405,14 @@ export class TgpuTextureRenderViewImpl {
400
405
  };
401
406
  this[$internal] = {
402
407
  unwrap: () => {
403
- return baseTexture[$internal].materialize().createView({
404
- label: getName(this) ?? '<unnamed>',
405
- ...this.descriptor,
406
- });
408
+ const soul = this[$soul];
409
+ if (!soul.raw) {
410
+ soul.raw = soul.texture[$internal].materialize().createView({
411
+ label: getName(this) ?? '<unnamed>',
412
+ ...soul.descriptor,
413
+ });
414
+ }
415
+ return soul.raw;
407
416
  },
408
417
  format: descriptor.format ?? baseTexture.props.format,
409
418
  aspect: descriptor.aspect,
@@ -1,8 +1,5 @@
1
- import type { ExternalImageSource } from './texture.ts';
2
- export declare function getImageSourceDimensions(source: ExternalImageSource): {
3
- width: number;
4
- height: number;
5
- };
1
+ import { type ImageWrite } from './textureWrite.ts';
6
2
  export declare function clearTextureUtilsCache(device: GPUDevice): void;
7
3
  export declare function generateTextureMipmaps(device: GPUDevice, texture: GPUTexture, baseMipLevel?: number, mipLevels?: number): void;
8
- export declare function resampleImage(device: GPUDevice, targetTexture: GPUTexture, image: ExternalImageSource, layer?: number): void;
4
+ export declare function clearTextureWithColor(device: GPUDevice, texture: GPUTexture, color: readonly [number, number, number, number], mipLevels: readonly number[]): void;
5
+ export declare function writeImages(device: GPUDevice, texture: GPUTexture, writes: readonly ImageWrite[]): void;
@@ -1,23 +1,5 @@
1
1
  import { getEffectiveSampleTypes, getTextureFormatInfo } from "./textureFormats.js";
2
- export function getImageSourceDimensions(source) {
3
- const { videoWidth, videoHeight } = source;
4
- if (videoWidth && videoHeight) {
5
- return { width: videoWidth, height: videoHeight };
6
- }
7
- const { naturalWidth, naturalHeight } = source;
8
- if (naturalWidth && naturalHeight) {
9
- return { width: naturalWidth, height: naturalHeight };
10
- }
11
- const { codedWidth, codedHeight } = source;
12
- if (codedWidth && codedHeight) {
13
- return { width: codedWidth, height: codedHeight };
14
- }
15
- const { width, height } = source;
16
- if (width && height) {
17
- return { width, height };
18
- }
19
- throw new Error('Cannot determine dimensions of the provided image source.');
20
- }
2
+ import { sameSize } from "./textureWrite.js";
21
3
  const FULLSCREEN_VERTEX_SHADER = `
22
4
  struct VertexOutput {
23
5
  @builtin(position) pos: vec4f,
@@ -30,13 +12,13 @@ fn vs_main(@builtin(vertex_index) i: u32) -> VertexOutput {
30
12
  const uv = array(vec2f(0, 1), vec2f(2, 1), vec2f(0, -1));
31
13
  return VertexOutput(vec4f(pos[i], 0, 1), uv[i]);
32
14
  }`;
33
- const SAMPLE_FRAGMENT_SHADER = `
15
+ const sampleFragmentShader = (channel) => `
34
16
  @group(0) @binding(0) var src: texture_2d<f32>;
35
17
  @group(0) @binding(1) var samp: sampler;
36
18
 
37
19
  @fragment
38
20
  fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
39
- return textureSample(src, samp, uv);
21
+ return textureSample(src, samp, uv)${channel ? `.${channel.repeat(4)}` : ''};
40
22
  }`;
41
23
  const GATHER_FRAGMENT_SHADER = `
42
24
  @group(0) @binding(0) var src: texture_2d<f32>;
@@ -50,181 +32,176 @@ fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
50
32
  let a = textureGather(3, src, samp, uv);
51
33
  return vec4f(dot(r, vec4f(0.25)), dot(g, vec4f(0.25)), dot(b, vec4f(0.25)), dot(a, vec4f(0.25)));
52
34
  }`;
53
- const blitCache = new WeakMap();
54
- function getOrCreateDeviceCache(device) {
55
- let cache = blitCache.get(device);
35
+ const CHANNEL_WRITE_MASKS = { r: 0x1, g: 0x2, b: 0x4, a: 0x8 };
36
+ const deviceCaches = new WeakMap();
37
+ function cached(device, key, create) {
38
+ let cache = deviceCaches.get(device);
56
39
  if (!cache) {
57
- cache = {
58
- vertexModule: device.createShaderModule({
59
- code: FULLSCREEN_VERTEX_SHADER,
60
- }),
61
- filterableResources: new Map(),
62
- layoutResources: new Map(),
63
- };
64
- blitCache.set(device, cache);
65
- }
66
- return cache;
67
- }
68
- function getBlitResources(device, filterable, sampleType) {
69
- const cache = getOrCreateDeviceCache(device);
70
- let filterableRes = cache.filterableResources.get(filterable);
71
- if (!filterableRes) {
72
- filterableRes = {
73
- fragmentModule: device.createShaderModule({
74
- code: filterable ? SAMPLE_FRAGMENT_SHADER : GATHER_FRAGMENT_SHADER,
75
- }),
76
- sampler: device.createSampler(filterable ? { magFilter: 'linear', minFilter: 'linear' } : {}),
77
- };
78
- cache.filterableResources.set(filterable, filterableRes);
40
+ cache = new Map();
41
+ deviceCaches.set(device, cache);
79
42
  }
80
- const layoutKey = `${filterable}:${sampleType}`;
81
- let layoutRes = cache.layoutResources.get(layoutKey);
82
- if (!layoutRes) {
83
- const bindGroupLayout = device.createBindGroupLayout({
84
- entries: [
85
- {
86
- binding: 0,
87
- visibility: GPUShaderStage.FRAGMENT,
88
- texture: { sampleType },
89
- },
90
- {
91
- binding: 1,
92
- visibility: GPUShaderStage.FRAGMENT,
93
- sampler: { type: filterable ? 'filtering' : 'non-filtering' },
94
- },
95
- ],
96
- });
97
- layoutRes = {
98
- bindGroupLayout,
99
- pipelineLayout: device.createPipelineLayout({
100
- bindGroupLayouts: [bindGroupLayout],
101
- }),
102
- };
103
- cache.layoutResources.set(layoutKey, layoutRes);
43
+ if (!cache.has(key)) {
44
+ cache.set(key, create());
104
45
  }
105
- return {
106
- vertexModule: cache.vertexModule,
107
- ...filterableRes,
108
- ...layoutRes,
109
- };
46
+ return cache.get(key);
110
47
  }
111
- function blit(options) {
112
- const { device, source, destination, format, filterable, sampleType } = options;
113
- const resources = getBlitResources(device, filterable, sampleType);
114
- const pipeline = device.createRenderPipeline({
115
- layout: resources.pipelineLayout,
116
- vertex: { module: resources.vertexModule },
117
- fragment: { module: resources.fragmentModule, targets: [{ format }] },
118
- primitive: { topology: 'triangle-list' },
119
- });
120
- const bindGroup = device.createBindGroup({
121
- layout: resources.bindGroupLayout,
48
+ export function clearTextureUtilsCache(device) {
49
+ deviceCaches.delete(device);
50
+ }
51
+ function blit(device, encoder, { format, filterable, filter = 'linear', channel }, source, attachment, viewport) {
52
+ const sampleType = filterable ? 'float' : 'unfilterable-float';
53
+ const bindGroupLayout = cached(device, `layout:${sampleType}`, () => device.createBindGroupLayout({
122
54
  entries: [
123
- { binding: 0, resource: source },
124
- { binding: 1, resource: resources.sampler },
125
- ],
126
- });
127
- const ownEncoder = !options.encoder;
128
- const encoder = options.encoder ?? device.createCommandEncoder();
129
- const pass = encoder.beginRenderPass({
130
- colorAttachments: [
55
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType } },
131
56
  {
132
- view: destination,
133
- loadOp: 'clear',
134
- storeOp: 'store',
57
+ binding: 1,
58
+ visibility: GPUShaderStage.FRAGMENT,
59
+ sampler: { type: filterable ? 'filtering' : 'non-filtering' },
135
60
  },
136
61
  ],
62
+ }));
63
+ const channelKey = channel ? `${channel.from}${channel.to}` : '';
64
+ const pipeline = cached(device, `pipeline:${format}:${sampleType}:${channelKey}`, () => device.createRenderPipeline({
65
+ layout: cached(device, `pipelineLayout:${sampleType}`, () => device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] })),
66
+ vertex: {
67
+ module: cached(device, 'vertex', () => device.createShaderModule({ code: FULLSCREEN_VERTEX_SHADER })),
68
+ },
69
+ fragment: {
70
+ module: cached(device, `fragment:${sampleType}:${channel?.from ?? ''}`, () => device.createShaderModule({
71
+ code: filterable ? sampleFragmentShader(channel?.from) : GATHER_FRAGMENT_SHADER,
72
+ })),
73
+ targets: [channel ? { format, writeMask: CHANNEL_WRITE_MASKS[channel.to] } : { format }],
74
+ },
75
+ }));
76
+ const sampler = cached(device, `sampler:${filterable && filter}`, () => device.createSampler(filterable ? { magFilter: filter, minFilter: filter } : {}));
77
+ const bindGroup = device.createBindGroup({
78
+ layout: bindGroupLayout,
79
+ entries: [
80
+ { binding: 0, resource: source },
81
+ { binding: 1, resource: sampler },
82
+ ],
137
83
  });
84
+ const pass = encoder.beginRenderPass({ colorAttachments: [attachment] });
85
+ if (viewport) {
86
+ pass.setViewport(...viewport, 0, 1);
87
+ pass.setScissorRect(...viewport);
88
+ }
138
89
  pass.setPipeline(pipeline);
139
90
  pass.setBindGroup(0, bindGroup);
140
91
  pass.draw(3);
141
92
  pass.end();
142
- if (ownEncoder) {
143
- device.queue.submit([encoder.finish()]);
144
- }
145
93
  }
146
- export function clearTextureUtilsCache(device) {
147
- blitCache.delete(device);
94
+ function layerView(texture, mipLevel, layer) {
95
+ const is3d = texture.dimension === '3d';
96
+ return texture.createView({
97
+ dimension: is3d ? '3d' : '2d',
98
+ baseMipLevel: mipLevel,
99
+ mipLevelCount: 1,
100
+ baseArrayLayer: is3d ? 0 : layer,
101
+ arrayLayerCount: 1,
102
+ });
148
103
  }
149
- function validateBlitFormat(device, format, operation) {
150
- const info = getTextureFormatInfo(format);
151
- const effectiveSampleTypes = getEffectiveSampleTypes(device, format);
152
- const isFloat = effectiveSampleTypes.includes('float');
153
- const isUnfilterableFloat = effectiveSampleTypes.includes('unfilterable-float');
154
- if (!isFloat && !isUnfilterableFloat) {
155
- throw new Error(`Cannot ${operation} for format '${format}': only float formats are supported.`);
156
- }
157
- if (!info.canRenderAttachment) {
158
- throw new Error(`Cannot ${operation} for format '${format}': format does not support render attachments.`);
159
- }
104
+ function layerAttachment(texture, mipLevel, layer, loadOp) {
160
105
  return {
161
- filterable: isFloat,
162
- sampleType: isFloat ? 'float' : 'unfilterable-float',
106
+ view: layerView(texture, mipLevel, layer),
107
+ ...(texture.dimension === '3d' && { depthSlice: layer }),
108
+ loadOp,
109
+ storeOp: 'store',
163
110
  };
164
111
  }
165
112
  export function generateTextureMipmaps(device, texture, baseMipLevel = 0, mipLevels) {
166
113
  if (texture.dimension !== '2d') {
167
114
  throw new Error('Mipmap generation only supports 2D textures.');
168
115
  }
169
- const { filterable, sampleType } = validateBlitFormat(device, texture.format, 'generate mipmaps');
116
+ const { format } = texture;
117
+ const sampleTypes = getEffectiveSampleTypes(device, format);
118
+ const filterable = sampleTypes.includes('float');
119
+ if (!filterable && !sampleTypes.includes('unfilterable-float')) {
120
+ throw new Error(`Cannot generate mipmaps for format '${format}': only float formats are supported.`);
121
+ }
122
+ if (!getTextureFormatInfo(format).canRenderAttachment) {
123
+ throw new Error(`Cannot generate mipmaps for format '${format}': format does not support render attachments.`);
124
+ }
170
125
  const levels = mipLevels ?? texture.mipLevelCount - baseMipLevel;
126
+ const encoder = device.createCommandEncoder();
171
127
  for (let layer = 0; layer < texture.depthOrArrayLayers; layer++) {
172
128
  for (let mip = baseMipLevel; mip < baseMipLevel + levels - 1; mip++) {
173
- const viewOptions = (level) => ({
174
- dimension: '2d',
175
- baseMipLevel: level,
176
- mipLevelCount: 1,
177
- baseArrayLayer: layer,
178
- arrayLayerCount: 1,
179
- });
180
- blit({
181
- device,
182
- source: texture.createView(viewOptions(mip)),
183
- destination: texture.createView(viewOptions(mip + 1)),
184
- format: texture.format,
185
- filterable,
186
- sampleType,
187
- });
129
+ blit(device, encoder, { format, filterable }, layerView(texture, mip, layer), layerAttachment(texture, mip + 1, layer, 'clear'));
188
130
  }
189
131
  }
132
+ device.queue.submit([encoder.finish()]);
190
133
  }
191
- export function resampleImage(device, targetTexture, image, layer = 0) {
192
- if (targetTexture.dimension !== '2d') {
193
- throw new Error('Resampling only supports 2D textures.');
134
+ export function clearTextureWithColor(device, texture, color, mipLevels) {
135
+ const encoder = device.createCommandEncoder();
136
+ for (const mipLevel of mipLevels) {
137
+ const layers = texture.dimension === '3d'
138
+ ? Math.max(1, texture.depthOrArrayLayers >> mipLevel)
139
+ : texture.depthOrArrayLayers;
140
+ for (let layer = 0; layer < layers; layer++) {
141
+ encoder
142
+ .beginRenderPass({
143
+ colorAttachments: [
144
+ { ...layerAttachment(texture, mipLevel, layer, 'clear'), clearValue: [...color] },
145
+ ],
146
+ })
147
+ .end();
148
+ }
194
149
  }
195
- const { filterable } = validateBlitFormat(device, targetTexture.format, 'resample');
196
- const { width, height } = getImageSourceDimensions(image);
197
- const inputTexture = device.createTexture({
198
- size: [width, height],
199
- format: 'rgba8unorm',
150
+ device.queue.submit([encoder.finish()]);
151
+ }
152
+ function copyImage(device, write, destination, size) {
153
+ const { flipY = false, premultipliedAlpha = false, colorSpace = 'srgb' } = write.options;
154
+ device.queue.copyExternalImageToTexture({ source: write.source, origin: write.sourceOrigin, flipY }, { ...destination, premultipliedAlpha, colorSpace }, size);
155
+ }
156
+ function stageImage(device, write, format) {
157
+ const texture = device.createTexture({
158
+ size: write.sourceSize,
159
+ format,
200
160
  usage: GPUTextureUsage.TEXTURE_BINDING |
161
+ GPUTextureUsage.COPY_SRC |
201
162
  GPUTextureUsage.COPY_DST |
202
163
  GPUTextureUsage.RENDER_ATTACHMENT,
203
164
  });
204
- device.queue.copyExternalImageToTexture({ source: image }, {
205
- texture: inputTexture,
206
- }, [width, height]);
207
- const renderTexture = device.createTexture({
208
- size: [targetTexture.width, targetTexture.height],
209
- format: targetTexture.format,
210
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
211
- });
212
- const encoder = device.createCommandEncoder();
213
- blit({
214
- device,
215
- source: inputTexture.createView(),
216
- destination: renderTexture.createView(),
217
- format: targetTexture.format,
218
- filterable,
219
- sampleType: 'float', // Input is always rgba8unorm which is filterable
220
- encoder,
221
- });
222
- encoder.copyTextureToTexture({ texture: renderTexture }, { texture: targetTexture, origin: { x: 0, y: 0, z: layer } }, {
223
- width: targetTexture.width,
224
- height: targetTexture.height,
225
- depthOrArrayLayers: 1,
226
- });
227
- device.queue.submit([encoder.finish()]);
228
- inputTexture.destroy();
229
- renderTexture.destroy();
165
+ copyImage(device, write, { texture }, write.sourceSize);
166
+ return texture;
167
+ }
168
+ function stagingFormat(format) {
169
+ if (format.endsWith('-srgb')) {
170
+ return format;
171
+ }
172
+ return format.endsWith('8unorm') ? 'rgba8unorm' : 'rgba16float';
173
+ }
174
+ export function writeImages(device, texture, writes) {
175
+ const isCopy = (write) => !write.channel && sameSize(write.sourceSize, write.size);
176
+ const bySource = new Map();
177
+ for (const write of writes) {
178
+ if (isCopy(write) && texture.dimension === '2d') {
179
+ copyImage(device, write, { texture, mipLevel: write.mipLevel, origin: write.origin }, write.size);
180
+ }
181
+ else {
182
+ bySource.set(write.source, [...(bySource.get(write.source) ?? []), write]);
183
+ }
184
+ }
185
+ for (const group of bySource.values()) {
186
+ const encoder = device.createCommandEncoder();
187
+ let staged;
188
+ for (const write of group) {
189
+ if (isCopy(write)) {
190
+ // copyExternalImageToTexture only accepts 2d destinations, so 3d writes stage through a 2d texture
191
+ staged ??= stageImage(device, write, texture.format);
192
+ encoder.copyTextureToTexture({ texture: staged }, { texture, mipLevel: write.mipLevel, origin: write.origin }, [...write.size, 1]);
193
+ }
194
+ else {
195
+ staged ??= stageImage(device, write, stagingFormat(texture.format));
196
+ blit(device, encoder, {
197
+ format: texture.format,
198
+ filterable: true,
199
+ filter: write.options.filter,
200
+ channel: write.channel,
201
+ }, staged.createView(), layerAttachment(texture, write.mipLevel, write.origin.z, 'load'), [write.origin.x, write.origin.y, ...write.size]);
202
+ }
203
+ }
204
+ device.queue.submit([encoder.finish()]);
205
+ staged?.destroy();
206
+ }
230
207
  }
@@ -0,0 +1,61 @@
1
+ import type { TextureProps } from './textureProps.ts';
2
+ export type TextureWriteFit = 'stretch' | 'clip';
3
+ export type TextureWriteOptions = {
4
+ mipLevel?: GPUIntegerCoordinate;
5
+ origin?: readonly [x: number, y: number, z?: number];
6
+ size?: readonly [width: number, height: number];
7
+ sourceOrigin?: readonly [x: number, y: number];
8
+ sourceSize?: readonly [width: number, height: number];
9
+ fit?: TextureWriteFit;
10
+ filter?: GPUFilterMode;
11
+ flipY?: boolean;
12
+ premultipliedAlpha?: boolean;
13
+ colorSpace?: PredefinedColorSpace;
14
+ };
15
+ export type TextureBlobWriteOptions = Omit<TextureWriteOptions, 'sourceOrigin' | 'sourceSize'>;
16
+ export type TextureChannel = 'r' | 'g' | 'b' | 'a';
17
+ export type TextureRawWriteOptions = {
18
+ mipLevel?: GPUIntegerCoordinate;
19
+ origin?: readonly [x: number, y: number, z?: number];
20
+ size?: readonly [width: number, height: number, depthOrArrayLayers?: number];
21
+ };
22
+ export type TextureCopyOptions = {
23
+ sourceMipLevel?: GPUIntegerCoordinate;
24
+ mipLevel?: GPUIntegerCoordinate;
25
+ sourceOrigin?: readonly [x: number, y: number, z?: number];
26
+ origin?: readonly [x: number, y: number, z?: number];
27
+ size?: readonly [width: number, height: number, depthOrArrayLayers?: number];
28
+ };
29
+ /** An image write resolved against a texture, with `fit` already applied */
30
+ export type ImageWrite = {
31
+ source: GPUCopyExternalImageSource;
32
+ sourceOrigin: {
33
+ x: number;
34
+ y: number;
35
+ };
36
+ sourceSize: [width: number, height: number];
37
+ origin: {
38
+ x: number;
39
+ y: number;
40
+ z: number;
41
+ };
42
+ size: [width: number, height: number];
43
+ mipLevel: number;
44
+ options: TextureWriteOptions;
45
+ channel?: {
46
+ from: TextureChannel;
47
+ to: TextureChannel;
48
+ };
49
+ };
50
+ type TextureShape = Pick<TextureProps, 'size' | 'dimension'>;
51
+ export declare function origin3d([x, y, z]?: readonly (number | undefined)[]): {
52
+ x: number;
53
+ y: number;
54
+ z: number;
55
+ };
56
+ export declare function mipLevelSize({ size, dimension }: TextureShape, mipLevel: number): [number, number, number];
57
+ /** The size of the written region: `size`, or the rest of the mip level past `origin` */
58
+ export declare function writeRegionSize(texture: TextureShape, options: TextureWriteOptions): [number, number];
59
+ export declare function sameSize(a: readonly number[], b: readonly number[]): boolean;
60
+ export declare function resolveImageWrite(texture: TextureShape, source: GPUCopyExternalImageSource, options: TextureWriteOptions, layer?: number): ImageWrite;
61
+ export {};