Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 46 additions & 53 deletions Sources/Rendering/WebGPU/Texture/test/testWriteSubImageData.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,61 +10,56 @@ it.skipIf(!__VTK_TEST_WEBGPU__)(
const device = await testUtils.createWebGPUTestDevice();
const texture = vtkWebGPUTexture.newInstance({ label: 'subImageTexture' });

try {
texture.create(device, {
width: 4,
height: 4,
format: 'rgba8unorm',
usage:
/* eslint-disable no-undef */
/* eslint-disable no-bitwise */
GPUTextureUsage.TEXTURE_BINDING |
/* eslint-disable no-undef */
/* eslint-disable no-bitwise */
GPUTextureUsage.COPY_DST |
/* eslint-disable no-undef */
/* eslint-disable no-bitwise */
GPUTextureUsage.COPY_SRC,
});
texture.create(device, {
width: 4,
height: 4,
format: 'rgba8unorm',
usage:
/* eslint-disable no-undef */
/* eslint-disable no-bitwise */
GPUTextureUsage.TEXTURE_BINDING |
/* eslint-disable no-undef */
/* eslint-disable no-bitwise */
GPUTextureUsage.COPY_DST |
/* eslint-disable no-undef */
/* eslint-disable no-bitwise */
GPUTextureUsage.COPY_SRC,
});

const basePixels = new Uint8Array(4 * 4 * 4);
for (let i = 0; i < 16; i++) {
basePixels[i * 4 + 3] = 255;
}
texture.writeImageData({ nativeArray: basePixels });
const basePixels = new Uint8Array(4 * 4 * 4);
for (let i = 0; i < 16; i++) {
basePixels[i * 4 + 3] = 255;
}
texture.writeImageData({ nativeArray: basePixels });

const patch = new Uint8Array([
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255,
]);
texture.writeSubImageData({
x: 1,
y: 1,
width: 2,
height: 2,
nativeArray: patch,
});

const patch = new Uint8Array([
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255,
]);
texture.writeSubImageData({
x: 1,
y: 1,
width: 2,
height: 2,
nativeArray: patch,
});
const actual = await testUtils.readWebGPUTexture2D(device, texture, 4, 4);

const actual = await testUtils.readWebGPUTexture2D(device, texture, 4, 4);
const expected = new Uint8Array(basePixels);
const setPixel = (x, y, rgba) => {
expected.set(rgba, (y * 4 + x) * 4);
};
setPixel(1, 1, [255, 0, 0, 255]);
setPixel(2, 1, [0, 255, 0, 255]);
setPixel(1, 2, [0, 0, 255, 255]);
setPixel(2, 2, [255, 255, 0, 255]);

const expected = new Uint8Array(basePixels);
const setPixel = (x, y, rgba) => {
expected.set(rgba, (y * 4 + x) * 4);
};
setPixel(1, 1, [255, 0, 0, 255]);
setPixel(2, 1, [0, 255, 0, 255]);
setPixel(1, 2, [0, 0, 255, 255]);
setPixel(2, 2, [255, 255, 0, 255]);

expect(actual.length, 'readback size matches texture size').toBe(
expected.length
);

for (let i = 0; i < expected.length; i++) {
expect(actual[i], `byte ${i} matches expected value`).toBe(expected[i]);
}
} finally {
texture.getHandle().destroy();
device.getHandle().destroy();
expect(actual.length, 'readback size matches texture size').toBe(
expected.length
);

for (let i = 0; i < expected.length; i++) {
expect(actual[i], `byte ${i} matches expected value`).toBe(expected[i]);
}
}
);
Expand Down Expand Up @@ -131,8 +126,6 @@ it.skipIf(!__VTK_TEST_WEBGPU__)(
).toEqual(Array.from(basePixels));
} finally {
macro.setLoggerFunction('error', previousErrorLogger);
texture.getHandle().destroy();
device.getHandle().destroy();
}
}
);
8 changes: 8 additions & 0 deletions Sources/Rendering/WebGPU/TextureManager/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,14 @@ function vtkWebGPUTextureManager(publicAPI, model) {
}
// fill out the req time and format based on imageData/image
_fillRequest(treq);
// _fillRequest derives the cache hash from the scalar array mtime alone,
// discarding the texture mtime seeded above. A texture rebuild must also
// be triggered by srcTexture.modified() or imageData.modified() alone, so
// hash on the newest of the texture, imageData, and scalar array mtimes.
treq.time = Math.max(treq.time, srcTexture.getMTime());
if (treq.imageData) {
treq.time = Math.max(treq.time, treq.imageData.getMTime());
}
treq.mipLevel = srcTexture.getMipLevel();
treq.hash = treq.time + treq.format + treq.mipLevel;
return model.device.getTextureManager().getTexture(treq);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,47 +9,48 @@ it.skipIf(!__VTK_TEST_WEBGPU__)(
async () => {
const device = await testUtils.createWebGPUTestDevice();

try {
const values = new Uint8Array(4 * 4).fill(10);
const scalars = vtkDataArray.newInstance({
name: 'Scalars',
values,
numberOfComponents: 1,
});
const imageData = vtkImageData.newInstance();
imageData.setDimensions(4, 4, 1);
imageData.getPointData().setScalars(scalars);
const values = new Uint8Array(4 * 4).fill(10);
const scalars = vtkDataArray.newInstance({
name: 'Scalars',
values,
numberOfComponents: 1,
});
const imageData = vtkImageData.newInstance();
imageData.setDimensions(4, 4, 1);
imageData.getPointData().setScalars(scalars);

const textureManager = device.getTextureManager();
const textureManager = device.getTextureManager();

const texture = textureManager.getTextureForImageData(imageData);
expect(
textureManager.getTextureForImageData(imageData),
'an unchanged imageData reuses the cached texture'
).toBe(texture);
// Identities are compared as booleans: passing the textures themselves
// to expect() makes a failure serialize them through
// toJSON()/getState(), which recurses the circular device–texture
// graph and masks the assertion message with a stack overflow.
const texture = textureManager.getTextureForImageData(imageData);
expect(
textureManager.getTextureForImageData(imageData) === texture,
'an unchanged imageData reuses the cached texture'
).toBe(true);

// Write the scalars in place and signal the change through
// imageData.modified() alone — the pattern used by consumers that
// stream new frames into an existing array (the OpenGL backend keys
// its texture rebuilds on the imageData mtime, so this must also
// refresh the WebGPU texture cache).
values.fill(200);
imageData.modified();
const textureAfterImageDataModified =
textureManager.getTextureForImageData(imageData);
expect(
textureAfterImageDataModified,
'imageData.modified() alone must invalidate the cached texture'
).not.toBe(texture);
// Write the scalars in place and signal the change through
// imageData.modified() alone — the pattern used by consumers that
// stream new frames into an existing array (the OpenGL backend keys
// its texture rebuilds on the imageData mtime, so this must also
// refresh the WebGPU texture cache).
values.fill(200);
imageData.modified();
const textureAfterImageDataModified =
textureManager.getTextureForImageData(imageData);
expect(
textureAfterImageDataModified === texture,
'imageData.modified() alone must invalidate the cached texture'
).toBe(false);

// Direct scalar-array modification keeps invalidating as before.
scalars.modified();
expect(
textureManager.getTextureForImageData(imageData),
'scalars.modified() must invalidate the cached texture'
).not.toBe(textureAfterImageDataModified);
} finally {
device.getHandle().destroy();
}
// Direct scalar-array modification keeps invalidating as before.
scalars.modified();
expect(
textureManager.getTextureForImageData(imageData) ===
textureAfterImageDataModified,
'scalars.modified() must invalidate the cached texture'
).toBe(false);
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { it, expect } from 'vitest';

import testUtils from 'vtk.js/Sources/Testing/testUtils';
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData';
import vtkTexture from 'vtk.js/Sources/Rendering/Core/Texture';

it.skipIf(!__VTK_TEST_WEBGPU__)(
'Test vtkWebGPUTextureManager invalidates cached vtkTexture textures',
async () => {
const device = await testUtils.createWebGPUTestDevice();

const values = new Uint8Array(4 * 4).fill(10);
const scalars = vtkDataArray.newInstance({
name: 'Scalars',
values,
numberOfComponents: 1,
});
const imageData = vtkImageData.newInstance();
imageData.setDimensions(4, 4, 1);
imageData.getPointData().setScalars(scalars);

const srcTexture = vtkTexture.newInstance();
srcTexture.setInputData(imageData);

const textureManager = device.getTextureManager();

const texture = textureManager.getTextureForVTKTexture(srcTexture);
expect(
textureManager.getTextureForVTKTexture(srcTexture) === texture,
'an unchanged vtkTexture reuses the cached texture'
).toBe(true);

// Write the scalars in place and signal the change through
// imageData.modified() alone — the pattern used by consumers that
// stream new frames into an existing array.
values.fill(200);
imageData.modified();
const afterImageDataModified =
textureManager.getTextureForVTKTexture(srcTexture);
expect(
afterImageDataModified === texture,
'imageData.modified() alone must invalidate the cached texture'
).toBe(false);

// The mtime seeded from the source texture must also keep counting:
// srcTexture.modified() alone refreshes the cache entry.
srcTexture.modified();
const afterTextureModified =
textureManager.getTextureForVTKTexture(srcTexture);
expect(
afterTextureModified === afterImageDataModified,
'srcTexture.modified() alone must invalidate the cached texture'
).toBe(false);

// Direct scalar-array modification keeps invalidating as before.
scalars.modified();
expect(
textureManager.getTextureForVTKTexture(srcTexture) ===
afterTextureModified,
'scalars.modified() must invalidate the cached texture'
).toBe(false);
}
);
3 changes: 2 additions & 1 deletion Sources/Testing/testUtils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect } from 'vitest';
import { expect, onTestFinished } from 'vitest';
import pixelmatch from 'pixelmatch';
import vtkRTAnalyticSource from 'vtk.js/Sources/Filters/Sources/RTAnalyticSource';
import vtkWebGPUDevice from 'vtk.js/Sources/Rendering/WebGPU/Device';
Expand Down Expand Up @@ -203,6 +203,7 @@ async function createWebGPUTestDevice() {
}

const handle = await adapter.requestDevice();
onTestFinished(() => handle.destroy());
const device = vtkWebGPUDevice.newInstance();
device.initialize(handle);
return device;
Expand Down
Loading