diff --git a/Sources/Rendering/WebGPU/BindGroup/index.js b/Sources/Rendering/WebGPU/BindGroup/index.js index 0aea0b73164..4f6eeac9645 100644 --- a/Sources/Rendering/WebGPU/BindGroup/index.js +++ b/Sources/Rendering/WebGPU/BindGroup/index.js @@ -1,5 +1,7 @@ import macro from 'vtk.js/Sources/macros'; +const { vtkErrorMacro } = macro; + // ---------------------------------------------------------------------------- // vtkWebGPUBindGroup methods // ---------------------------------------------------------------------------- @@ -30,6 +32,7 @@ function vtkWebGPUBindGroup(publicAPI, model) { publicAPI.getBindGroupLayout = (device) => { const entries = []; for (let i = 0; i < model.bindables.length; i++) { + model.bindables[i].setDevice?.(device); const entry = model.bindables[i].getBindGroupLayoutEntry(); entry.binding = i; entries.push(entry); @@ -38,13 +41,19 @@ function vtkWebGPUBindGroup(publicAPI, model) { }; publicAPI.getBindGroup = (device) => { + const deviceChanged = model.bindGroupDevice !== device; + + for (let i = 0; i < model.bindables.length; i++) { + model.bindables[i].setDevice?.(device); + } + // check mtime let mtime = publicAPI.getMTime(); for (let i = 0; i < model.bindables.length; i++) { const tm = model.bindables[i].getBindGroupTime().getMTime(); mtime = tm > mtime ? tm : mtime; } - if (mtime < model.bindGroupTime.getMTime()) { + if (!deviceChanged && mtime < model.bindGroupTime.getMTime()) { return model.bindGroup; } @@ -60,6 +69,7 @@ function vtkWebGPUBindGroup(publicAPI, model) { entries, label: model.label, }); + model.bindGroupDevice = device; model.bindGroupTime.modified(); return model.bindGroup; @@ -67,12 +77,25 @@ function vtkWebGPUBindGroup(publicAPI, model) { publicAPI.getShaderCode = (pipeline) => { const lines = []; - const bgroup = pipeline.getBindGroupLayoutCount(model.label); + const bgroup = pipeline.getBindGroupLayoutIndex(model.label); + if (bgroup < 0) { + vtkErrorMacro( + `vtkWebGPUBindGroup: bind group layout ${model.label} was not found in pipeline` + ); + return ''; + } for (let i = 0; i < model.bindables.length; i++) { lines.push(model.bindables[i].getShaderCode(i, bgroup)); } return lines.join('\n'); }; + + publicAPI.releaseGraphicsResources = () => { + model.bindGroup = null; + model.bindGroupDevice = null; + model.bindGroupTime.modified(); + publicAPI.modified(); + }; } // ---------------------------------------------------------------------------- @@ -82,6 +105,7 @@ function vtkWebGPUBindGroup(publicAPI, model) { const DEFAULT_VALUES = { device: null, handle: null, + bindGroupDevice: null, label: null, }; diff --git a/Sources/Rendering/WebGPU/ForwardPass/index.js b/Sources/Rendering/WebGPU/ForwardPass/index.js index fc4f25e0a47..8200efe0ce0 100644 --- a/Sources/Rendering/WebGPU/ForwardPass/index.js +++ b/Sources/Rendering/WebGPU/ForwardPass/index.js @@ -194,6 +194,20 @@ function vtkForwardPass(publicAPI, model) { publicAPI.addVolume = (volume) => { model.volumes.push(volume); }; + + publicAPI.releaseGraphicsResources = () => { + model.opaquePass?.releaseGraphicsResources?.(); + model.translucentPass?.releaseGraphicsResources?.(); + model.volumePass?.releaseGraphicsResources?.(); + + model.opaquePass = null; + model.translucentPass = null; + model.volumePass = null; + model._finalBlitEncoder = null; + model._finalBlitOutputTextureView = null; + model._fullScreenQuad = null; + model._fsqSampler = null; + }; } // ---------------------------------------------------------------------------- diff --git a/Sources/Rendering/WebGPU/IndexBuffer/index.js b/Sources/Rendering/WebGPU/IndexBuffer/index.js index b80707fef08..3dc31d0a2c7 100644 --- a/Sources/Rendering/WebGPU/IndexBuffer/index.js +++ b/Sources/Rendering/WebGPU/IndexBuffer/index.js @@ -34,7 +34,7 @@ class _LimitedMap { return true; } } - return undefined; + return false; } get(key) { @@ -300,7 +300,7 @@ function vtkWebGPUIndexBuffer(publicAPI, model) { } else { state.flatIdToPointId = new Uint32Array(numPts + state.extraPoints); } - if (numPts + state.extraPoints < 0x8fff) { + if (numPts + state.extraPoints <= 0x7fff) { state.pointIdToFlatId = new Int16Array(numPts); } else { state.pointIdToFlatId = new Int32Array(numPts); diff --git a/Sources/Rendering/WebGPU/OpaquePass/index.js b/Sources/Rendering/WebGPU/OpaquePass/index.js index a4d2f23a9a9..02d969fa22f 100644 --- a/Sources/Rendering/WebGPU/OpaquePass/index.js +++ b/Sources/Rendering/WebGPU/OpaquePass/index.js @@ -87,6 +87,12 @@ function vtkWebGPUOpaquePass(publicAPI, model) { // default settings are fine for this model.renderEncoder.setPipelineHash('op'); }; + + publicAPI.releaseGraphicsResources = () => { + model.renderEncoder = null; + model.colorTexture = null; + model.depthTexture = null; + }; } // ---------------------------------------------------------------------------- diff --git a/Sources/Rendering/WebGPU/OrderIndependentTranslucentPass/index.js b/Sources/Rendering/WebGPU/OrderIndependentTranslucentPass/index.js index 77d433337a2..309140fc6b8 100644 --- a/Sources/Rendering/WebGPU/OrderIndependentTranslucentPass/index.js +++ b/Sources/Rendering/WebGPU/OrderIndependentTranslucentPass/index.js @@ -252,6 +252,14 @@ function vtkWebGPUOrderIndependentTranslucentPass(publicAPI, model) { }, }); }; + + publicAPI.releaseGraphicsResources = () => { + model.translucentRenderEncoder = null; + model.translucentFinalEncoder = null; + model.translucentColorTexture = null; + model.translucentAccumulateTexture = null; + model.fullScreenQuad = null; + }; } // ---------------------------------------------------------------------------- diff --git a/Sources/Rendering/WebGPU/Pipeline/index.js b/Sources/Rendering/WebGPU/Pipeline/index.js index add1a564431..3867bf327bc 100644 --- a/Sources/Rendering/WebGPU/Pipeline/index.js +++ b/Sources/Rendering/WebGPU/Pipeline/index.js @@ -95,13 +95,13 @@ function vtkWebGPUPipeline(publicAPI, model) { publicAPI.getBindGroupLayout = (idx) => model.layouts[idx].layout; - publicAPI.getBindGroupLayoutCount = (llabel) => { + publicAPI.getBindGroupLayoutIndex = (llabel) => { for (let i = 0; i < model.layouts.length; i++) { if (model.layouts[i].label === llabel) { return i; } } - return 0; + return -1; // Not found }; publicAPI.bindVertexInput = (renderEncoder, vInput) => { diff --git a/Sources/Rendering/WebGPU/RenderEncoder/index.js b/Sources/Rendering/WebGPU/RenderEncoder/index.js index 92f47619d3d..51b478f6192 100644 --- a/Sources/Rendering/WebGPU/RenderEncoder/index.js +++ b/Sources/Rendering/WebGPU/RenderEncoder/index.js @@ -1,6 +1,8 @@ import * as macro from 'vtk.js/Sources/macros'; import vtkWebGPUShaderCache from 'vtk.js/Sources/Rendering/WebGPU/ShaderCache'; +const { vtkErrorMacro } = macro; + // methods we forward to the handle const forwarded = [ 'setBindGroup', @@ -70,7 +72,9 @@ function vtkWebGPURenderEncoder(publicAPI, model) { } // check depth buffer - if (!model.depthTextureView !== !('depthStencil' in pd)) { + const hasDepthAttachment = !!model.depthTextureView; + const pipelineUsesDepth = 'depthStencil' in pd; + if (hasDepthAttachment !== pipelineUsesDepth) { console.log('mismatched depth attachments'); console.trace(); } else if (model.depthTextureView) { @@ -98,7 +102,13 @@ function vtkWebGPURenderEncoder(publicAPI, model) { publicAPI.activateBindGroup = (bg) => { const device = model.boundPipeline.getDevice(); - const midx = model.boundPipeline.getBindGroupLayoutCount(bg.getLabel()); + const midx = model.boundPipeline.getBindGroupLayoutIndex(bg.getLabel()); + if (midx < 0) { + vtkErrorMacro( + `vtkWebGPURenderEncoder: could not find bind group layout ${bg.getLabel()}` + ); + return; + } model.handle.setBindGroup(midx, bg.getBindGroup(device)); // verify bind group layout matches const bgl1 = device.getBindGroupLayoutDescription( diff --git a/Sources/Rendering/WebGPU/RenderWindow/index.js b/Sources/Rendering/WebGPU/RenderWindow/index.js index 14644bcca02..b5e15e1540c 100644 --- a/Sources/Rendering/WebGPU/RenderWindow/index.js +++ b/Sources/Rendering/WebGPU/RenderWindow/index.js @@ -11,7 +11,7 @@ import vtkRenderPass from 'vtk.js/Sources/Rendering/SceneGraph/RenderPass'; import vtkRenderWindowViewNode from 'vtk.js/Sources/Rendering/SceneGraph/RenderWindowViewNode'; import HalfFloat from 'vtk.js/Sources/Common/Core/HalfFloat'; -const { vtkErrorMacro } = macro; +const { vtkErrorMacro, vtkWarningMacro } = macro; // const IS_CHROME = navigator.userAgent.indexOf('Chrome') !== -1; const SCREENSHOT_PLACEHOLDER = { position: 'absolute', @@ -31,6 +31,66 @@ function vtkWebGPURenderWindow(publicAPI, model) { publicAPI.getViewNodeFactory = () => model.myFactory; + function releaseViewNodeResources(viewNode, visited = new Set()) { + if (!viewNode || visited.has(viewNode)) { + return; + } + visited.add(viewNode); + + const children = viewNode.getChildren?.() || []; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + child?.releaseGraphicsResources?.(); + releaseViewNodeResources(child, visited); + } + } + + function queueRenderAfterInitialization() { + const subscription = publicAPI.onInitialized(() => { + subscription.unsubscribe(); + if (!model.deleted) { + publicAPI.traverseAllPasses(); + } + }); + } + + function handleDeviceLost(info, deviceGeneration) { + if ( + model.deleted || + deviceGeneration !== model.deviceGeneration || + model.handlingDeviceLost + ) { + return; + } + + model.handlingDeviceLost = true; + model.deviceLostInfo = info; + + const reason = info?.reason ?? 'unknown'; + const message = info?.message || 'WebGPU device was lost.'; + vtkWarningMacro(`WebGPU device lost (${reason}): ${message}`); + + publicAPI.releaseGraphicsResources(); + publicAPI.invokeDeviceLost({ + reason, + message, + recoverable: reason !== 'destroyed', + }); + + if (reason !== 'destroyed') { + queueRenderAfterInitialization(); + publicAPI.initialize(); + } + + model.handlingDeviceLost = false; + } + + function watchForDeviceLoss(deviceHandle, deviceGeneration) { + deviceHandle.lost.then((info) => { + handleDeviceLost(info, deviceGeneration); + }); + } + // Auto update style const previousSize = [0, 0]; function updateWindow() { @@ -128,6 +188,7 @@ function vtkWebGPURenderWindow(publicAPI, model) { publicAPI.initialize = () => { if (!model.initializing) { model.initializing = true; + model.deviceLostInfo = null; if (!navigator.gpu) { vtkErrorMacro('WebGPU is not enabled.'); return; @@ -213,20 +274,29 @@ function vtkWebGPURenderWindow(publicAPI, model) { model.device = null; return; } - // model.device.getHandle().lost.then((info) => { - // console.log(`${info.message}`); - // publicAPI.releaseGraphicsResources(); - // }); + model.deviceGeneration += 1; + watchForDeviceLoss(model.device.getHandle(), model.deviceGeneration); model.context = model.canvas.getContext('webgpu'); }; publicAPI.releaseGraphicsResources = () => { + if (model.renderPasses) { + for (let i = 0; i < model.renderPasses.length; i++) { + model.renderPasses[i]?.releaseGraphicsResources?.(); + } + } + releaseViewNodeResources(publicAPI); const rp = vtkRenderPass.newInstance(); rp.setCurrentOperation('Release'); rp.traverse(publicAPI, null); + if (model.context) { + model.context.unconfigure(); + } model.adapter = null; model.device = null; model.context = null; + model.commandEncoder = null; + model._configured = false; model.initialized = false; model.initializing = false; }; @@ -573,6 +643,10 @@ function vtkWebGPURenderWindow(publicAPI, model) { const DEFAULT_VALUES = { initialized: false, + initializing: false, + handlingDeviceLost: false, + deviceGeneration: 0, + deviceLostInfo: null, context: null, adapter: null, device: null, @@ -623,10 +697,12 @@ export function extend(publicAPI, model, initialValues = {}) { macro.event(publicAPI, model, 'imageReady'); macro.event(publicAPI, model, 'initialized'); + macro.event(publicAPI, model, 'deviceLost'); // Build VTK API macro.get(publicAPI, model, [ 'commandEncoder', + 'deviceLostInfo', 'device', 'presentationFormat', 'useBackgroundImage', diff --git a/Sources/Rendering/WebGPU/Renderer/index.js b/Sources/Rendering/WebGPU/Renderer/index.js index a04ad8bdb19..41429d569ef 100644 --- a/Sources/Rendering/WebGPU/Renderer/index.js +++ b/Sources/Rendering/WebGPU/Renderer/index.js @@ -527,6 +527,13 @@ function vtkWebGPURenderer(publicAPI, model) { if (model.selector !== null) { model.selector.releaseGraphicsResources(); } + model.clearFSQ?.releaseGraphicsResources?.(); + model.clearFSQ = null; + model.bindGroup.releaseGraphicsResources?.(); + model.UBO.releaseGraphicsResources?.(); + model.SSBO.releaseGraphicsResources?.(); + model.renderEncoder = null; + model.backgroundTexLoaded = false; }; } diff --git a/Sources/Rendering/WebGPU/SimpleMapper/index.js b/Sources/Rendering/WebGPU/SimpleMapper/index.js index 71c0a4fe02c..8b1826354f2 100644 --- a/Sources/Rendering/WebGPU/SimpleMapper/index.js +++ b/Sources/Rendering/WebGPU/SimpleMapper/index.js @@ -361,6 +361,16 @@ function vtkWebGPUSimpleMapper(publicAPI, model) { model.device.createPipeline(model.pipelineHash, model.pipeline); } }; + + publicAPI.releaseGraphicsResources = () => { + model.vertexInput?.releaseGraphicsResources?.(); + model.bindGroup?.releaseGraphicsResources?.(); + model.UBO?.releaseGraphicsResources?.(); + model.SSBO?.releaseGraphicsResources?.(); + model.pipeline = null; + model.renderEncoder = null; + model.textureViews.length = 0; + }; } // ---------------------------------------------------------------------------- diff --git a/Sources/Rendering/WebGPU/StorageBuffer/index.js b/Sources/Rendering/WebGPU/StorageBuffer/index.js index db41ceefc6e..03006026f6b 100644 --- a/Sources/Rendering/WebGPU/StorageBuffer/index.js +++ b/Sources/Rendering/WebGPU/StorageBuffer/index.js @@ -21,6 +21,21 @@ function vtkWebGPUStorageBuffer(publicAPI, model) { // Set our className model.classHierarchy.push('vtkWebGPUStorageBuffer'); + function ensureGPUBuffer(device) { + if (model._buffer) { + return; + } + + publicAPI.createView('Float32Array'); + const req = { + nativeArray: model.Float32Array, + usage: BufferUsage.Storage, + label: model.label, + }; + model._buffer = device.getBufferManager().getBuffer(req); + model.bindGroupTime.modified(); + } + publicAPI.addEntry = (name, type) => { if (model._bufferEntryNames.has(name)) { vtkErrorMacro(`entry named ${name} already exists`); @@ -39,14 +54,9 @@ function vtkWebGPUStorageBuffer(publicAPI, model) { }; publicAPI.send = (device) => { + model.device = device; if (!model._buffer) { - const req = { - nativeArray: model.Float32Array, - usage: BufferUsage.Storage, - label: model.label, - }; - model._buffer = device.getBufferManager().getBuffer(req); - model.bindGroupTime.modified(); + ensureGPUBuffer(device); model._sendTime.modified(); return; } @@ -183,6 +193,9 @@ struct ${model.label}Struct }; publicAPI.getBindGroupEntry = () => { + if (!model._buffer && model.device) { + ensureGPUBuffer(model.device); + } const foo = { resource: { buffer: model._buffer.getHandle(), @@ -200,6 +213,11 @@ struct ${model.label}Struct delete model.arrayBuffer; delete model.Float32Array; }; + + publicAPI.releaseGraphicsResources = () => { + model._buffer = null; + model.bindGroupTime.modified(); + }; } // ---------------------------------------------------------------------------- diff --git a/Sources/Rendering/WebGPU/UniformBuffer/index.js b/Sources/Rendering/WebGPU/UniformBuffer/index.js index 594f5e13b9e..706836e6b76 100644 --- a/Sources/Rendering/WebGPU/UniformBuffer/index.js +++ b/Sources/Rendering/WebGPU/UniformBuffer/index.js @@ -14,6 +14,24 @@ function vtkWebGPUUniformBuffer(publicAPI, model) { // Set our className model.classHierarchy.push('vtkWebGPUUniformBuffer'); + function ensureGPUBuffer(device) { + if (model.UBO) { + return; + } + + publicAPI.sortBufferEntries(); + publicAPI.createView('Float32Array'); + + const req = { + nativeArray: model.Float32Array, + usage: BufferUsage.UniformArray, + label: model.label, + }; + model.UBO = device.getBufferManager().getBuffer(req); + model.bindGroupTime.modified(); + model.sendDirty = false; + } + publicAPI.addEntry = (name, type) => { if (model._bufferEntryNames.has(name)) { vtkErrorMacro(`entry named ${name} already exists`); @@ -188,16 +206,8 @@ function vtkWebGPUUniformBuffer(publicAPI, model) { }; publicAPI.sendIfNeeded = (device) => { - if (!model.UBO) { - const req = { - nativeArray: model.Float32Array, - usage: BufferUsage.UniformArray, - label: model.label, - }; - model.UBO = device.getBufferManager().getBuffer(req); - model.bindGroupTime.modified(); - model.sendDirty = false; - } + model.device = device; + ensureGPUBuffer(device); // send data down if needed if (model.sendDirty) { @@ -268,6 +278,9 @@ function vtkWebGPUUniformBuffer(publicAPI, model) { }; publicAPI.getBindGroupEntry = () => { + if (!model.UBO && model.device) { + ensureGPUBuffer(model.device); + } const foo = { resource: { buffer: model.UBO.getHandle(), @@ -291,6 +304,12 @@ function vtkWebGPUUniformBuffer(publicAPI, model) { ); return lines.join('\n'); }; + + publicAPI.releaseGraphicsResources = () => { + model.UBO = null; + model.bindGroupTime.modified(); + model.sendDirty = true; + }; } // ---------------------------------------------------------------------------- diff --git a/Sources/Rendering/WebGPU/VertexInput/index.js b/Sources/Rendering/WebGPU/VertexInput/index.js index 635d745fb2c..66fb9421e36 100644 --- a/Sources/Rendering/WebGPU/VertexInput/index.js +++ b/Sources/Rendering/WebGPU/VertexInput/index.js @@ -6,8 +6,9 @@ function arraysEqual(a, b) { if (a == null || b == null) return false; if (a.length !== b.length) return false; + const values = new Set(b); for (let i = 0; i < a.length; ++i) { - if (!b.includes(a[i])) return false; + if (!values.has(a[i])) return false; } return true; } diff --git a/Sources/Rendering/WebGPU/VolumePass/index.js b/Sources/Rendering/WebGPU/VolumePass/index.js index e556bc0620f..a63caadcc73 100644 --- a/Sources/Rendering/WebGPU/VolumePass/index.js +++ b/Sources/Rendering/WebGPU/VolumePass/index.js @@ -696,6 +696,27 @@ function vtkWebGPUVolumePass(publicAPI, model) { } } }; + + publicAPI.releaseGraphicsResources = () => { + if (model._animationRateSubscription) { + model._animationRateSubscription.unsubscribe(); + model._animationRateSubscription = null; + } + + model._clearEncoder = null; + model._mergeEncoder = null; + model._copyEncoder = null; + model._depthRangeEncoder = null; + model._colorTexture = null; + model._colorTextureView = null; + model._depthRangeTexture = null; + model._depthRangeTexture2 = null; + model._volumeCopyQuad = null; + model.fullScreenQuad = null; + model._copyUBO = null; + model.colorTextureView = null; + model.depthTextureView = null; + }; } // ----------------------------------------------------------------------------