@@ -4,6 +4,8 @@ import vtkWebGPUTextureView from 'vtk.js/Sources/Rendering/WebGPU/TextureView';
44import vtkWebGPUTypes from 'vtk.js/Sources/Rendering/WebGPU/Types' ;
55import 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