Skip to content

Commit d0817bf

Browse files
committed
feat(WebGPU): implement device.lost handler
1 parent cc8a76e commit d0817bf

14 files changed

Lines changed: 245 additions & 31 deletions

File tree

Sources/Rendering/WebGPU/BindGroup/index.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import macro from 'vtk.js/Sources/macros';
22

3+
const { vtkErrorMacro } = macro;
4+
35
// ----------------------------------------------------------------------------
46
// vtkWebGPUBindGroup methods
57
// ----------------------------------------------------------------------------
@@ -30,6 +32,7 @@ function vtkWebGPUBindGroup(publicAPI, model) {
3032
publicAPI.getBindGroupLayout = (device) => {
3133
const entries = [];
3234
for (let i = 0; i < model.bindables.length; i++) {
35+
model.bindables[i].setDevice?.(device);
3336
const entry = model.bindables[i].getBindGroupLayoutEntry();
3437
entry.binding = i;
3538
entries.push(entry);
@@ -38,13 +41,19 @@ function vtkWebGPUBindGroup(publicAPI, model) {
3841
};
3942

4043
publicAPI.getBindGroup = (device) => {
44+
const deviceChanged = model.bindGroupDevice !== device;
45+
46+
for (let i = 0; i < model.bindables.length; i++) {
47+
model.bindables[i].setDevice?.(device);
48+
}
49+
4150
// check mtime
4251
let mtime = publicAPI.getMTime();
4352
for (let i = 0; i < model.bindables.length; i++) {
4453
const tm = model.bindables[i].getBindGroupTime().getMTime();
4554
mtime = tm > mtime ? tm : mtime;
4655
}
47-
if (mtime < model.bindGroupTime.getMTime()) {
56+
if (!deviceChanged && mtime < model.bindGroupTime.getMTime()) {
4857
return model.bindGroup;
4958
}
5059

@@ -60,19 +69,33 @@ function vtkWebGPUBindGroup(publicAPI, model) {
6069
entries,
6170
label: model.label,
6271
});
72+
model.bindGroupDevice = device;
6373
model.bindGroupTime.modified();
6474

6575
return model.bindGroup;
6676
};
6777

6878
publicAPI.getShaderCode = (pipeline) => {
6979
const lines = [];
70-
const bgroup = pipeline.getBindGroupLayoutCount(model.label);
80+
const bgroup = pipeline.getBindGroupLayoutIndex(model.label);
81+
if (bgroup < 0) {
82+
vtkErrorMacro(
83+
`vtkWebGPUBindGroup: bind group layout ${model.label} was not found in pipeline`
84+
);
85+
return '';
86+
}
7187
for (let i = 0; i < model.bindables.length; i++) {
7288
lines.push(model.bindables[i].getShaderCode(i, bgroup));
7389
}
7490
return lines.join('\n');
7591
};
92+
93+
publicAPI.releaseGraphicsResources = () => {
94+
model.bindGroup = null;
95+
model.bindGroupDevice = null;
96+
model.bindGroupTime.modified();
97+
publicAPI.modified();
98+
};
7699
}
77100

78101
// ----------------------------------------------------------------------------
@@ -82,6 +105,7 @@ function vtkWebGPUBindGroup(publicAPI, model) {
82105
const DEFAULT_VALUES = {
83106
device: null,
84107
handle: null,
108+
bindGroupDevice: null,
85109
label: null,
86110
};
87111

Sources/Rendering/WebGPU/ForwardPass/index.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,20 @@ function vtkForwardPass(publicAPI, model) {
194194
publicAPI.addVolume = (volume) => {
195195
model.volumes.push(volume);
196196
};
197+
198+
publicAPI.releaseGraphicsResources = () => {
199+
model.opaquePass?.releaseGraphicsResources?.();
200+
model.translucentPass?.releaseGraphicsResources?.();
201+
model.volumePass?.releaseGraphicsResources?.();
202+
203+
model.opaquePass = null;
204+
model.translucentPass = null;
205+
model.volumePass = null;
206+
model._finalBlitEncoder = null;
207+
model._finalBlitOutputTextureView = null;
208+
model._fullScreenQuad = null;
209+
model._fsqSampler = null;
210+
};
197211
}
198212

199213
// ----------------------------------------------------------------------------

Sources/Rendering/WebGPU/IndexBuffer/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class _LimitedMap {
3434
return true;
3535
}
3636
}
37-
return undefined;
37+
return false;
3838
}
3939

4040
get(key) {
@@ -300,7 +300,7 @@ function vtkWebGPUIndexBuffer(publicAPI, model) {
300300
} else {
301301
state.flatIdToPointId = new Uint32Array(numPts + state.extraPoints);
302302
}
303-
if (numPts + state.extraPoints < 0x8fff) {
303+
if (numPts + state.extraPoints <= 0x7fff) {
304304
state.pointIdToFlatId = new Int16Array(numPts);
305305
} else {
306306
state.pointIdToFlatId = new Int32Array(numPts);

Sources/Rendering/WebGPU/OpaquePass/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ function vtkWebGPUOpaquePass(publicAPI, model) {
8787
// default settings are fine for this
8888
model.renderEncoder.setPipelineHash('op');
8989
};
90+
91+
publicAPI.releaseGraphicsResources = () => {
92+
model.renderEncoder = null;
93+
model.colorTexture = null;
94+
model.depthTexture = null;
95+
};
9096
}
9197

9298
// ----------------------------------------------------------------------------

Sources/Rendering/WebGPU/OrderIndependentTranslucentPass/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,14 @@ function vtkWebGPUOrderIndependentTranslucentPass(publicAPI, model) {
252252
},
253253
});
254254
};
255+
256+
publicAPI.releaseGraphicsResources = () => {
257+
model.translucentRenderEncoder = null;
258+
model.translucentFinalEncoder = null;
259+
model.translucentColorTexture = null;
260+
model.translucentAccumulateTexture = null;
261+
model.fullScreenQuad = null;
262+
};
255263
}
256264

257265
// ----------------------------------------------------------------------------

Sources/Rendering/WebGPU/Pipeline/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,13 @@ function vtkWebGPUPipeline(publicAPI, model) {
9595

9696
publicAPI.getBindGroupLayout = (idx) => model.layouts[idx].layout;
9797

98-
publicAPI.getBindGroupLayoutCount = (llabel) => {
98+
publicAPI.getBindGroupLayoutIndex = (llabel) => {
9999
for (let i = 0; i < model.layouts.length; i++) {
100100
if (model.layouts[i].label === llabel) {
101101
return i;
102102
}
103103
}
104-
return 0;
104+
return -1; // Not found
105105
};
106106

107107
publicAPI.bindVertexInput = (renderEncoder, vInput) => {

Sources/Rendering/WebGPU/RenderEncoder/index.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as macro from 'vtk.js/Sources/macros';
22
import vtkWebGPUShaderCache from 'vtk.js/Sources/Rendering/WebGPU/ShaderCache';
33

4+
const { vtkErrorMacro } = macro;
5+
46
// methods we forward to the handle
57
const forwarded = [
68
'setBindGroup',
@@ -70,7 +72,9 @@ function vtkWebGPURenderEncoder(publicAPI, model) {
7072
}
7173

7274
// check depth buffer
73-
if (!model.depthTextureView !== !('depthStencil' in pd)) {
75+
const hasDepthAttachment = !!model.depthTextureView;
76+
const pipelineUsesDepth = 'depthStencil' in pd;
77+
if (hasDepthAttachment !== pipelineUsesDepth) {
7478
console.log('mismatched depth attachments');
7579
console.trace();
7680
} else if (model.depthTextureView) {
@@ -98,7 +102,13 @@ function vtkWebGPURenderEncoder(publicAPI, model) {
98102

99103
publicAPI.activateBindGroup = (bg) => {
100104
const device = model.boundPipeline.getDevice();
101-
const midx = model.boundPipeline.getBindGroupLayoutCount(bg.getLabel());
105+
const midx = model.boundPipeline.getBindGroupLayoutIndex(bg.getLabel());
106+
if (midx < 0) {
107+
vtkErrorMacro(
108+
`vtkWebGPURenderEncoder: could not find bind group layout ${bg.getLabel()}`
109+
);
110+
return;
111+
}
102112
model.handle.setBindGroup(midx, bg.getBindGroup(device));
103113
// verify bind group layout matches
104114
const bgl1 = device.getBindGroupLayoutDescription(

Sources/Rendering/WebGPU/RenderWindow/index.js

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import vtkRenderPass from 'vtk.js/Sources/Rendering/SceneGraph/RenderPass';
1111
import vtkRenderWindowViewNode from 'vtk.js/Sources/Rendering/SceneGraph/RenderWindowViewNode';
1212
import HalfFloat from 'vtk.js/Sources/Common/Core/HalfFloat';
1313

14-
const { vtkErrorMacro } = macro;
14+
const { vtkErrorMacro, vtkWarningMacro } = macro;
1515
// const IS_CHROME = navigator.userAgent.indexOf('Chrome') !== -1;
1616
const SCREENSHOT_PLACEHOLDER = {
1717
position: 'absolute',
@@ -31,6 +31,66 @@ function vtkWebGPURenderWindow(publicAPI, model) {
3131

3232
publicAPI.getViewNodeFactory = () => model.myFactory;
3333

34+
function releaseViewNodeResources(viewNode, visited = new Set()) {
35+
if (!viewNode || visited.has(viewNode)) {
36+
return;
37+
}
38+
visited.add(viewNode);
39+
40+
const children = viewNode.getChildren?.() || [];
41+
for (let i = 0; i < children.length; i++) {
42+
const child = children[i];
43+
child?.releaseGraphicsResources?.();
44+
releaseViewNodeResources(child, visited);
45+
}
46+
}
47+
48+
function queueRenderAfterInitialization() {
49+
const subscription = publicAPI.onInitialized(() => {
50+
subscription.unsubscribe();
51+
if (!model.deleted) {
52+
publicAPI.traverseAllPasses();
53+
}
54+
});
55+
}
56+
57+
function handleDeviceLost(info, deviceGeneration) {
58+
if (
59+
model.deleted ||
60+
deviceGeneration !== model.deviceGeneration ||
61+
model.handlingDeviceLost
62+
) {
63+
return;
64+
}
65+
66+
model.handlingDeviceLost = true;
67+
model.deviceLostInfo = info;
68+
69+
const reason = info?.reason ?? 'unknown';
70+
const message = info?.message || 'WebGPU device was lost.';
71+
vtkWarningMacro(`WebGPU device lost (${reason}): ${message}`);
72+
73+
publicAPI.releaseGraphicsResources();
74+
publicAPI.invokeDeviceLost({
75+
reason,
76+
message,
77+
recoverable: reason !== 'destroyed',
78+
});
79+
80+
if (reason !== 'destroyed') {
81+
queueRenderAfterInitialization();
82+
publicAPI.initialize();
83+
}
84+
85+
model.handlingDeviceLost = false;
86+
}
87+
88+
function watchForDeviceLoss(deviceHandle, deviceGeneration) {
89+
deviceHandle.lost.then((info) => {
90+
handleDeviceLost(info, deviceGeneration);
91+
});
92+
}
93+
3494
// Auto update style
3595
const previousSize = [0, 0];
3696
function updateWindow() {
@@ -128,6 +188,7 @@ function vtkWebGPURenderWindow(publicAPI, model) {
128188
publicAPI.initialize = () => {
129189
if (!model.initializing) {
130190
model.initializing = true;
191+
model.deviceLostInfo = null;
131192
if (!navigator.gpu) {
132193
vtkErrorMacro('WebGPU is not enabled.');
133194
return;
@@ -213,20 +274,29 @@ function vtkWebGPURenderWindow(publicAPI, model) {
213274
model.device = null;
214275
return;
215276
}
216-
// model.device.getHandle().lost.then((info) => {
217-
// console.log(`${info.message}`);
218-
// publicAPI.releaseGraphicsResources();
219-
// });
277+
model.deviceGeneration += 1;
278+
watchForDeviceLoss(model.device.getHandle(), model.deviceGeneration);
220279
model.context = model.canvas.getContext('webgpu');
221280
};
222281

223282
publicAPI.releaseGraphicsResources = () => {
283+
if (model.renderPasses) {
284+
for (let i = 0; i < model.renderPasses.length; i++) {
285+
model.renderPasses[i]?.releaseGraphicsResources?.();
286+
}
287+
}
288+
releaseViewNodeResources(publicAPI);
224289
const rp = vtkRenderPass.newInstance();
225290
rp.setCurrentOperation('Release');
226291
rp.traverse(publicAPI, null);
292+
if (model.context) {
293+
model.context.unconfigure();
294+
}
227295
model.adapter = null;
228296
model.device = null;
229297
model.context = null;
298+
model.commandEncoder = null;
299+
model._configured = false;
230300
model.initialized = false;
231301
model.initializing = false;
232302
};
@@ -573,6 +643,10 @@ function vtkWebGPURenderWindow(publicAPI, model) {
573643

574644
const DEFAULT_VALUES = {
575645
initialized: false,
646+
initializing: false,
647+
handlingDeviceLost: false,
648+
deviceGeneration: 0,
649+
deviceLostInfo: null,
576650
context: null,
577651
adapter: null,
578652
device: null,
@@ -623,10 +697,12 @@ export function extend(publicAPI, model, initialValues = {}) {
623697

624698
macro.event(publicAPI, model, 'imageReady');
625699
macro.event(publicAPI, model, 'initialized');
700+
macro.event(publicAPI, model, 'deviceLost');
626701

627702
// Build VTK API
628703
macro.get(publicAPI, model, [
629704
'commandEncoder',
705+
'deviceLostInfo',
630706
'device',
631707
'presentationFormat',
632708
'useBackgroundImage',

Sources/Rendering/WebGPU/Renderer/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,13 @@ function vtkWebGPURenderer(publicAPI, model) {
527527
if (model.selector !== null) {
528528
model.selector.releaseGraphicsResources();
529529
}
530+
model.clearFSQ?.releaseGraphicsResources?.();
531+
model.clearFSQ = null;
532+
model.bindGroup.releaseGraphicsResources?.();
533+
model.UBO.releaseGraphicsResources?.();
534+
model.SSBO.releaseGraphicsResources?.();
535+
model.renderEncoder = null;
536+
model.backgroundTexLoaded = false;
530537
};
531538
}
532539

Sources/Rendering/WebGPU/SimpleMapper/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,16 @@ function vtkWebGPUSimpleMapper(publicAPI, model) {
361361
model.device.createPipeline(model.pipelineHash, model.pipeline);
362362
}
363363
};
364+
365+
publicAPI.releaseGraphicsResources = () => {
366+
model.vertexInput?.releaseGraphicsResources?.();
367+
model.bindGroup?.releaseGraphicsResources?.();
368+
model.UBO?.releaseGraphicsResources?.();
369+
model.SSBO?.releaseGraphicsResources?.();
370+
model.pipeline = null;
371+
model.renderEncoder = null;
372+
model.textureViews.length = 0;
373+
};
364374
}
365375

366376
// ----------------------------------------------------------------------------

0 commit comments

Comments
 (0)