typegpu 0.12.4 → 0.12.6
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.
- package/bin.mjs +39 -67
- package/common/index.d.ts +1 -0
- package/common/index.js +1 -0
- package/common/writeChannels.d.ts +31 -0
- package/common/writeChannels.js +32 -0
- package/core/commandEncoder/computePass.d.ts +13 -0
- package/core/commandEncoder/computePass.js +5 -2
- package/core/commandEncoder/renderPass.d.ts +18 -4
- package/core/commandEncoder/renderPass.js +6 -2
- package/core/declare/tgpuDeclare.d.ts +11 -0
- package/core/declare/tgpuDeclare.js +1 -1
- package/core/immediate/immediateVar.d.ts +52 -0
- package/core/immediate/immediateVar.js +149 -0
- package/core/pipeline/computePipeline.d.ts +23 -0
- package/core/pipeline/computePipeline.js +26 -9
- package/core/pipeline/drawState.d.ts +8 -4
- package/core/pipeline/drawState.js +41 -51
- package/core/pipeline/priors.js +1 -0
- package/core/pipeline/renderPipeline.d.ts +24 -1
- package/core/pipeline/renderPipeline.js +18 -1
- package/core/root/init.js +3 -0
- package/core/root/rootTypes.d.ts +11 -0
- package/core/slot/slotTypes.d.ts +2 -1
- package/core/texture/texture.d.ts +20 -14
- package/core/texture/texture.js +70 -65
- package/core/texture/textureUtils.d.ts +3 -6
- package/core/texture/textureUtils.js +142 -165
- package/core/texture/textureWrite.d.ts +61 -0
- package/core/texture/textureWrite.js +59 -0
- package/data/dataIO.d.ts +2 -2
- package/data/generalizeFn.d.ts +17 -1
- package/data/generalizeFn.js +39 -2
- package/data/snippet.d.ts +5 -1
- package/data/snippet.js +1 -0
- package/data/wgslTypes.d.ts +1 -1
- package/errors.d.ts +7 -0
- package/errors.js +15 -0
- package/indexNamedExports.d.ts +5 -2
- package/indexNamedExports.js +2 -1
- package/internal.d.ts +1 -1
- package/internal.js +1 -1
- package/package.json +2 -2
- package/resolutionCtx.d.ts +5 -0
- package/resolutionCtx.js +11 -0
- package/serial/restore.d.ts +3 -1
- package/serial/restore.js +2 -0
- package/shared/meta.js +1 -1
- package/shared/symbols.js +1 -1
- package/shared/tseynit.d.ts +1 -0
- package/shared/tseynit.js +39 -7
- package/shared/utilityTypes.d.ts +1 -1
- package/std/atomic.d.ts +13 -0
- package/std/atomic.js +63 -29
- package/std/bitcast.js +4 -0
- package/std/boolean.d.ts +1 -1
- package/std/boolean.js +35 -21
- package/std/derivative.js +17 -12
- package/std/index.d.ts +4 -4
- package/std/index.js +5 -5
- package/std/numeric.d.ts +5 -1
- package/std/numeric.js +75 -8
- package/std/operators.d.ts +2 -2
- package/std/operators.js +29 -17
- package/std/packing.d.ts +61 -1
- package/std/packing.js +214 -25
- package/std/subgroup.d.ts +4 -0
- package/std/subgroup.js +34 -0
- package/std/texture.d.ts +13 -0
- package/std/texture.js +48 -0
- package/tgpuUnstable.d.ts +1 -0
- package/tgpuUnstable.js +1 -0
- package/tgsl/conversion.d.ts +2 -0
- package/tgsl/conversion.js +3 -0
- package/tgsl/shaderGenerator.d.ts +1 -1
- package/tgsl/wgslGenerator.d.ts +5 -0
- package/tgsl/wgslGenerator.js +218 -50
- package/types.d.ts +9 -1
- package/types.js +5 -0
package/core/texture/texture.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
for (
|
|
147
|
-
this.#clearMipLevel(
|
|
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
|
-
|
|
151
|
-
|
|
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,
|
|
166
|
+
write(source, options = {}) {
|
|
169
167
|
if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) {
|
|
170
|
-
this.#writeBufferData(source, typeof
|
|
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
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
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.`);
|
|
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.`);
|
|
186
180
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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,
|
|
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
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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 (
|
|
237
|
-
(source.props.size[
|
|
238
|
-
|
|
239
|
-
|
|
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()
|
|
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() {
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import type
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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,
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
|
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
|
-
|
|
162
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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 {};
|