Skip to content

Commit cc8a76e

Browse files
dakerfinetjul
authored andcommitted
feat(WebGPU): implement subimage texture upload
1 parent 7fbc87a commit cc8a76e

3 files changed

Lines changed: 414 additions & 70 deletions

File tree

Sources/Rendering/WebGPU/Texture/index.js

Lines changed: 200 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import vtkWebGPUTextureView from 'vtk.js/Sources/Rendering/WebGPU/TextureView';
44
import vtkWebGPUTypes from 'vtk.js/Sources/Rendering/WebGPU/Types';
55
import vtkTexture from 'vtk.js/Sources/Rendering/Core/Texture';
66

7+
const { vtkErrorMacro } = macro;
8+
79
// ----------------------------------------------------------------------------
810
// Global methods
911
// ----------------------------------------------------------------------------
@@ -16,6 +18,142 @@ function vtkWebGPUTexture(publicAPI, model) {
1618
// Set our className
1719
model.classHierarchy.push('vtkWebGPUTexture');
1820

21+
const getUploadArrayType = (tDetails, fallbackType) => {
22+
if (tDetails.elementSize === 2 && tDetails.sampleType === 'float') {
23+
return 'Uint16Array';
24+
}
25+
26+
if (tDetails.sampleType === 'sint') {
27+
if (tDetails.elementSize === 1) return 'Int8Array';
28+
if (tDetails.elementSize === 2) return 'Int16Array';
29+
if (tDetails.elementSize === 4) return 'Int32Array';
30+
} else if (tDetails.sampleType === 'unfilterable-float') {
31+
if (tDetails.elementSize === 4) return 'Float32Array';
32+
} else {
33+
if (tDetails.elementSize === 1) return 'Uint8Array';
34+
if (tDetails.elementSize === 2) return 'Uint16Array';
35+
if (tDetails.elementSize === 4) return 'Uint32Array';
36+
}
37+
38+
return fallbackType;
39+
};
40+
41+
const prepareTextureUploadData = (arr, width, height, depth) => {
42+
const tDetails = vtkWebGPUTypes.getDetailsFromTextureFormat(model.format);
43+
const expectedRowElements = width * tDetails.numComponents;
44+
const expectedElementCount = expectedRowElements * height * depth;
45+
const halfFloat =
46+
tDetails.elementSize === 2 && tDetails.sampleType === 'float';
47+
48+
if (!arr?.length && expectedElementCount > 0) {
49+
vtkErrorMacro('Texture upload failed: missing nativeArray data.');
50+
return null;
51+
}
52+
53+
if (arr.length < expectedElementCount) {
54+
vtkErrorMacro(
55+
`Texture upload failed: expected ${expectedElementCount} values but received ${arr.length}.`
56+
);
57+
return null;
58+
}
59+
60+
const inputArray =
61+
arr.length > expectedElementCount
62+
? arr.subarray(0, expectedElementCount)
63+
: arr;
64+
65+
const sourceBytesPerElement =
66+
inputArray.BYTES_PER_ELEMENT || tDetails.elementSize;
67+
const expectedBytesPerRow = width * tDetails.stride;
68+
const alignedBytesPerRow =
69+
256 * Math.floor((expectedBytesPerRow + 255) / 256);
70+
const outputArrayType = getUploadArrayType(
71+
tDetails,
72+
inputArray.constructor.name
73+
);
74+
const outputBytesPerElement = halfFloat
75+
? 2
76+
: macro.newTypedArray(outputArrayType, 0).BYTES_PER_ELEMENT;
77+
const alignedRowElements = alignedBytesPerRow / outputBytesPerElement;
78+
const inputRowBytes = expectedRowElements * sourceBytesPerElement;
79+
const requiresRepack =
80+
halfFloat ||
81+
inputArray.constructor.name !== outputArrayType ||
82+
inputRowBytes !== alignedBytesPerRow;
83+
84+
// No changes needed if not half float and already aligned
85+
if (!requiresRepack) {
86+
return {
87+
data: inputArray,
88+
bytesPerRow: alignedBytesPerRow,
89+
};
90+
}
91+
92+
// Create the output array
93+
const totalRows = height * depth;
94+
const outArray = macro.newTypedArray(
95+
outputArrayType,
96+
alignedRowElements * totalRows
97+
);
98+
99+
// Copy and convert data when needed
100+
if (halfFloat) {
101+
for (let row = 0; row < totalRows; row++) {
102+
const inOffset = row * expectedRowElements;
103+
const outOffset = row * alignedRowElements;
104+
for (let i = 0; i < expectedRowElements; i++) {
105+
outArray[outOffset + i] = HalfFloat.toHalf(inputArray[inOffset + i]);
106+
}
107+
}
108+
} else if (alignedRowElements === expectedRowElements) {
109+
// If the output width is the same as input, just copy
110+
outArray.set(inputArray);
111+
} else {
112+
for (let row = 0; row < totalRows; row++) {
113+
outArray.set(
114+
inputArray.subarray(
115+
row * expectedRowElements,
116+
(row + 1) * expectedRowElements
117+
),
118+
row * alignedRowElements
119+
);
120+
}
121+
}
122+
123+
return {
124+
data: outArray,
125+
bytesPerRow: alignedBytesPerRow,
126+
};
127+
};
128+
129+
const validateTextureWriteBounds = (x, y, z, width, height, depth) => {
130+
if (x < 0 || y < 0 || z < 0 || width <= 0 || height <= 0 || depth <= 0) {
131+
vtkErrorMacro(
132+
`Texture upload failed: invalid write region ` +
133+
`origin=(${x}, ${y}, ${z}) ` +
134+
`size=(${width}, ${height}, ${depth}).`
135+
);
136+
return false;
137+
}
138+
139+
if (
140+
x + width > model.width ||
141+
y + height > model.height ||
142+
z + depth > model.depth
143+
) {
144+
vtkErrorMacro(
145+
`Texture upload failed: write region ` +
146+
`origin=(${x}, ${y}, ${z}) ` +
147+
`size=(${width}, ${height}, ${depth}) ` +
148+
`exceeds texture extent=(${model.width}, ` +
149+
`${model.height}, ${model.depth}).`
150+
);
151+
return false;
152+
}
153+
154+
return true;
155+
};
156+
19157
publicAPI.create = (device, options) => {
20158
model.device = device;
21159
model.width = options.width;
@@ -121,84 +259,21 @@ function vtkWebGPUTexture(publicAPI, model) {
121259
return;
122260
}
123261

124-
const tDetails = vtkWebGPUTypes.getDetailsFromTextureFormat(model.format);
125-
let bufferBytesPerRow = model.width * tDetails.stride;
126-
127-
/**
128-
* Align texture data to ensure bytesPerRow is a multiple of 256.
129-
* This is necessary for WebGPU texture uploads, especially for half-float formats.
130-
* It also handles half-float conversion if the texture format requires it.
131-
* @param {*} arr - The input array containing texture data.
132-
* @param {*} height - The height of the texture.
133-
* @param {*} depth - The depth of the texture (1 for 2D textures).
134-
* @returns
135-
*/
136-
const alignTextureData = (arr, height, depth) => {
137-
// bytesPerRow must be a multiple of 256 so we might need to rebuild
138-
// the data here before passing to the buffer. e.g. if it is unorm8x4 then
139-
// we need to have width be a multiple of 64
140-
// Check if the texture is half float
141-
const halfFloat =
142-
tDetails.elementSize === 2 && tDetails.sampleType === 'float';
143-
144-
const bytesPerElement = arr.BYTES_PER_ELEMENT;
145-
const inWidthInBytes = (arr.length / (height * depth)) * bytesPerElement;
146-
147-
// No changes needed if not half float and already aligned
148-
if (!halfFloat && inWidthInBytes % 256 === 0) {
149-
return [arr, inWidthInBytes];
150-
}
151-
152-
// Calculate dimensions for the new buffer
153-
const inWidth = inWidthInBytes / bytesPerElement;
154-
const outBytesPerElement = tDetails.elementSize;
155-
const outWidthInBytes =
156-
256 * Math.floor((inWidth * outBytesPerElement + 255) / 256);
157-
const outWidth = outWidthInBytes / outBytesPerElement;
158-
159-
// Create the output array
160-
const outArray = macro.newTypedArray(
161-
halfFloat ? 'Uint16Array' : arr.constructor.name,
162-
outWidth * height * depth
163-
);
164-
165-
// Copy and convert data when needed
166-
const totalRows = height * depth;
167-
if (halfFloat) {
168-
for (let v = 0; v < totalRows; v++) {
169-
const inOffset = v * inWidth;
170-
const outOffset = v * outWidth;
171-
for (let i = 0; i < inWidth; i++) {
172-
outArray[outOffset + i] = HalfFloat.toHalf(arr[inOffset + i]);
173-
}
174-
}
175-
} else if (outWidth === inWidth) {
176-
// If the output width is the same as input, just copy
177-
outArray.set(arr);
178-
} else {
179-
for (let v = 0; v < totalRows; v++) {
180-
outArray.set(
181-
arr.subarray(v * inWidth, (v + 1) * inWidth),
182-
v * outWidth
183-
);
184-
}
185-
}
186-
187-
return [outArray, outWidthInBytes];
188-
};
189-
190262
if (req.nativeArray) {
191263
nativeArray = req.nativeArray;
192264
}
193265

194266
const is3D = publicAPI.getDimensionality() === 3;
195-
const alignedTextureData = alignTextureData(
267+
const preparedData = prepareTextureUploadData(
196268
nativeArray,
269+
model.width,
197270
model.height,
198271
is3D ? model.depth : 1
199272
);
200-
bufferBytesPerRow = alignedTextureData[1];
201-
const data = alignedTextureData[0];
273+
if (!preparedData) {
274+
return;
275+
}
276+
const data = preparedData.data;
202277

203278
model.device.getHandle().queue.writeTexture(
204279
{
@@ -209,7 +284,7 @@ function vtkWebGPUTexture(publicAPI, model) {
209284
data,
210285
{
211286
offset: 0,
212-
bytesPerRow: bufferBytesPerRow,
287+
bytesPerRow: preparedData.bytesPerRow,
213288
rowsPerImage: model.height,
214289
},
215290
{
@@ -229,6 +304,61 @@ function vtkWebGPUTexture(publicAPI, model) {
229304
model.ready = true;
230305
};
231306

307+
publicAPI.writeSubImageData = (req) => {
308+
const x = req.x ?? 0;
309+
const y = req.y ?? 0;
310+
const z = req.z ?? 0;
311+
const width = req.width ?? model.width - x;
312+
const height = req.height ?? model.height - y;
313+
const depth = req.depth ?? model.depth - z;
314+
const nativeArray = req.nativeArray || [];
315+
if (!validateTextureWriteBounds(x, y, z, width, height, depth)) {
316+
return;
317+
}
318+
const preparedData = prepareTextureUploadData(
319+
nativeArray,
320+
width,
321+
height,
322+
depth
323+
);
324+
if (!preparedData) {
325+
return;
326+
}
327+
328+
model.device.getHandle().queue.writeTexture(
329+
{
330+
texture: model.handle,
331+
mipLevel: 0,
332+
origin: {
333+
x,
334+
y,
335+
z,
336+
},
337+
},
338+
preparedData.data,
339+
{
340+
offset: 0,
341+
bytesPerRow: preparedData.bytesPerRow,
342+
rowsPerImage: height,
343+
},
344+
{
345+
width,
346+
height,
347+
depthOrArrayLayers: depth,
348+
}
349+
);
350+
351+
if (publicAPI.getDimensionality() !== 3 && model.mipLevel > 0) {
352+
vtkTexture.generateMipmaps(
353+
model.device.getHandle(),
354+
model.handle,
355+
model.mipLevel + 1
356+
);
357+
}
358+
359+
model.ready = true;
360+
};
361+
232362
// when data is pulled out of this texture what scale must be applied to
233363
// get back to the original source data. For formats such as r8unorm we
234364
// have to multiply by 255.0, for formats such as r16float it is 1.0

0 commit comments

Comments
 (0)