UNPKG

@cornerstonejs/core

Version:
421 lines (420 loc) • 19 kB
import BaseRenderingEngine, { VIEWPORT_MIN_SIZE } from './BaseRenderingEngine.js'; import WebGLContextPool from './WebGLContextPool.js'; import { getConfiguration } from '../init.js'; import Events from '../enums/Events.js'; import eventTarget from '../eventTarget.js'; import triggerEvent from '../utilities/triggerEvent.js'; import ViewportType from '../enums/ViewportType.js'; import viewportTypeUsesCustomRenderingPipeline, { viewportUsesCustomRenderingPipeline, } from './helpers/viewportTypeUsesCustomRenderingPipeline.js'; import getOrCreateCanvas, { updateCanvasSizeAndAspectRatio, } from './helpers/getOrCreateCanvas.js'; import { setElementTouchActionNone } from './helpers/elementTouchAction.js'; import { getViewportClassForInput } from './helpers/viewportTypeToViewportClass.js'; import { isGenericViewport } from '../utilities/viewportCapabilities.js'; class ContextPoolRenderingEngine extends BaseRenderingEngine { constructor(id) { super(id); this._renderFlaggedViewports = () => { this._throwIfDestroyed(); const viewports = this._getViewportsAsArray(); const viewportsToRender = viewports.filter((vp) => this._needsRender.has(vp.id)); if (viewportsToRender.length === 0) { this._animationFrameSet = false; this._animationFrameHandle = null; return; } const eventDetails = viewportsToRender.map((viewport) => { const eventDetail = this.renderViewportUsingCustomOrVtkPipeline(viewport); viewport.setRendered(); this._needsRender.delete(viewport.id); return eventDetail; }); this._animationFrameSet = false; this._animationFrameHandle = null; eventDetails.forEach((eventDetail) => { if (eventDetail?.element) { triggerEvent(eventDetail.element, Events.IMAGE_RENDERED, eventDetail); } }); }; if (!this.useCPURendering) { this.ensureContextPool(); } } ensureContextPool() { if (!this.contextPool) { const { rendering } = getConfiguration(); const { webGlContextCount } = rendering; this.contextPool = new WebGLContextPool(webGlContextCount, this.id); } return this.contextPool; } enableVTKjsDrivenViewport(viewportInputEntry) { const viewports = this._getViewportsAsArray(); const viewportsDrivenByVtkJs = viewports.filter((vp) => viewportTypeUsesCustomRenderingPipeline(vp.type) === false); const canvasesDrivenByVtkJs = viewportsDrivenByVtkJs.map((vp) => vp.canvas); const canvas = getOrCreateCanvas(viewportInputEntry.element); updateCanvasSizeAndAspectRatio(canvas); canvasesDrivenByVtkJs.push(canvas); const internalViewportEntry = { ...viewportInputEntry, canvas }; this.addVtkjsDrivenViewport(internalViewportEntry); } addVtkjsDrivenViewport(viewportInputEntry) { const contextPool = this.ensureContextPool(); const { element, canvas, viewportId, type, defaultOptions } = viewportInputEntry; element.tabIndex = -1; setElementTouchActionNone(element); let contextIndex = 0; if (type === ViewportType.STACK) { const contexts = contextPool.getAllContexts(); contextIndex = this._viewports.size % contexts.length; } contextPool.assignViewportToContext(viewportId, contextIndex); contextPool.updateViewportSize(viewportId, canvas.width, canvas.height); const contextData = contextPool.getContextByIndex(contextIndex); const { context: offscreenMultiRenderWindow, container } = contextData; const maxSize = contextPool.getMaxSizeForContext(contextIndex); container.width = maxSize.width; container.height = maxSize.height; offscreenMultiRenderWindow.resize(); offscreenMultiRenderWindow.addRenderer({ viewport: [0, 0, 1, 1], id: viewportId, background: defaultOptions.background ? defaultOptions.background : [0, 0, 0], }); const viewportInput = { id: viewportId, element, renderingEngineId: this.id, type, canvas, sx: 0, sy: 0, sWidth: canvas.width, sHeight: canvas.height, defaultOptions: defaultOptions || {}, }; const ViewportClass = getViewportClassForInput(viewportInputEntry); if (!ViewportClass) { throw new Error(`Viewport Type ${type} is not supported`); } const viewport = new ViewportClass(viewportInput); viewport.requestedType = viewportInputEntry.requestedType ?? type; this._viewports.set(viewportId, viewport); const eventDetail = { element, viewportId, renderingEngineId: this.id, }; if (!viewport.suppressEvents) { triggerEvent(eventTarget, Events.ELEMENT_ENABLED, eventDetail); } } setVtkjsDrivenViewports(viewportInputEntries) { if (viewportInputEntries.length) { const vtkDrivenCanvases = viewportInputEntries.map((vp) => getOrCreateCanvas(vp.element)); vtkDrivenCanvases.forEach((canvas) => updateCanvasSizeAndAspectRatio(canvas)); for (let i = 0; i < viewportInputEntries.length; i++) { const vtkDrivenViewportInputEntry = viewportInputEntries[i]; const canvas = vtkDrivenCanvases[i]; const internalViewportEntry = { ...vtkDrivenViewportInputEntry, canvas, }; this.addVtkjsDrivenViewport(internalViewportEntry); } } } _getDisplayedSize(vp) { const devicePixelRatio = window.devicePixelRatio || 1; const sizeSource = isGenericViewport(vp) ? vp.element : getOrCreateCanvas(vp.element); return { displayedWidth: Math.round(sizeSource.clientWidth * devicePixelRatio), displayedHeight: Math.round(sizeSource.clientHeight * devicePixelRatio), }; } _resizeVTKViewports(vtkDrivenViewports, keepCamera = true, immediate = true) { const viewportsNeedingResize = []; for (const vp of vtkDrivenViewports) { const canvas = getOrCreateCanvas(vp.element); const { displayedWidth, displayedHeight } = this._getDisplayedSize(vp); if (displayedWidth === 0 || displayedHeight === 0) { continue; } const renderedWidth = canvas.width; const renderedHeight = canvas.height; if (displayedWidth === renderedWidth && displayedHeight === renderedHeight) { continue; } viewportsNeedingResize.push(vp); } if (viewportsNeedingResize.length === 0) { return; } for (const vp of viewportsNeedingResize) { const { displayedWidth, displayedHeight } = this._getDisplayedSize(vp); const targetWidth = Math.max(VIEWPORT_MIN_SIZE, displayedWidth); const targetHeight = Math.max(VIEWPORT_MIN_SIZE, displayedHeight); vp.sWidth = targetWidth; vp.sHeight = targetHeight; } if (this._animationFrameSet) { return; } if (vtkDrivenViewports.length) { this._resize(vtkDrivenViewports); } vtkDrivenViewports.forEach((vp) => { vp.resize?.(); if (isGenericViewport(vp)) { return; } const prevCamera = vp.getCamera(); const rotation = vp.getRotation?.() ?? 0; const { flipHorizontal } = prevCamera; const resizeResetViewport = vp; if (resizeResetViewport.resetViewStateForResize) { resizeResetViewport.resetViewStateForResize(); } else { resizeResetViewport.resetCameraForResize?.(); } const displayArea = vp.getDisplayArea(); if (keepCamera) { if (displayArea) { if (flipHorizontal) { vp.setCamera({ flipHorizontal }); } if (rotation) { vp.setViewPresentation({ rotation }); } } else { vp.setCamera(prevCamera); } } }); if (immediate) { this.render(); } } renderViewportUsingCustomOrVtkPipeline(viewport) { if (viewportUsesCustomRenderingPipeline(viewport)) { const eventDetail = viewport.customRenderViewportToCanvas(); return eventDetail; } if (!this.contextPool) { throw new Error('GPU not available, and using a viewport with no custom render pipeline.'); } const assignedContextIndex = this.contextPool.getContextIndexForViewport(viewport.id); const contextData = this.contextPool.getContextByIndex(assignedContextIndex); const { context, container } = contextData; const eventDetail = this._renderViewportWithContext(viewport, context, container); return eventDetail; } _renderViewportWithContext(viewport, offscreenMultiRenderWindow, offScreenCanvasContainer) { if (viewport.canvas.clientWidth === 0 || viewport.canvas.clientHeight === 0) { return; } if (viewport.sWidth < VIEWPORT_MIN_SIZE || viewport.sHeight < VIEWPORT_MIN_SIZE) { console.warn('Viewport is too small', viewport.sWidth, viewport.sHeight); return; } if (viewportUsesCustomRenderingPipeline(viewport)) { return viewport.customRenderViewportToCanvas(); } if (!this.contextPool) { throw new Error('GPU not available, and using a viewport with no custom render pipeline.'); } if (!offscreenMultiRenderWindow.getRenderer(viewport.id)) { offscreenMultiRenderWindow.addRenderer({ viewport: [0, 0, 1, 1], id: viewport.id, background: viewport.defaultOptions?.background || [0, 0, 0], }); } const renderWindow = offscreenMultiRenderWindow.getRenderWindow(); const view = renderWindow.getViews()[0]; const originalRenderPasses = view.getRenderPasses(); const viewportRenderPasses = this.getViewportRenderPasses(viewport.id); if (viewportRenderPasses) { view.setRenderPasses(viewportRenderPasses); } this._resizeOffScreenCanvasForViewport(viewport, offScreenCanvasContainer, offscreenMultiRenderWindow); const renderer = offscreenMultiRenderWindow.getRenderer(viewport.id); const contextIndex = this.contextPool.getContextIndexForViewport(viewport.id); const maxSize = this.contextPool.getMaxSizeForContext(contextIndex); const viewportWidth = viewport.sWidth; const viewportHeight = viewport.sHeight; const xEnd = Math.min(1, viewportWidth / maxSize.width); const yEnd = Math.min(1, viewportHeight / maxSize.height); renderer.setViewport(0, 0, xEnd, yEnd); const allRenderers = offscreenMultiRenderWindow.getRenderers(); allRenderers.forEach(({ renderer: r, id }) => { const baseId = id.split('::')[0]; r.setDraw(baseId === viewport.id); }); const widgetRenderers = this.getWidgetRenderers(); widgetRenderers.forEach((viewportId, renderer) => { renderer.setDraw(viewportId === viewport.id); }); renderWindow.render(); allRenderers.forEach(({ renderer: r }) => r.setDraw(false)); widgetRenderers.forEach((_, renderer) => { renderer.setDraw(false); }); if (originalRenderPasses) { view.setRenderPasses(originalRenderPasses); } const openGLRenderWindow = offscreenMultiRenderWindow.getOpenGLRenderWindow(); const context = openGLRenderWindow.get3DContext(); const offScreenCanvas = context.canvas; const eventDetail = this._copyToOnscreenCanvas(viewport, offScreenCanvas); return eventDetail; } _renderViewportFromVtkCanvasToOnscreenCanvas(viewport, offScreenCanvas) { return this._copyToOnscreenCanvas(viewport, offScreenCanvas); } _resizeOffScreenCanvasForViewport(viewport, offScreenCanvasContainer, offscreenMultiRenderWindow) { const contextIndex = this.contextPool.getContextIndexForViewport(viewport.id); if (contextIndex === undefined) { return; } const maxSizeChanged = this.contextPool.updateViewportSize(viewport.id, viewport.sWidth, viewport.sHeight); if (!maxSizeChanged) { return; } const maxSize = this.contextPool.getMaxSizeForContext(contextIndex); if (offScreenCanvasContainer.width === maxSize.width && offScreenCanvasContainer.height === maxSize.height) { return; } offScreenCanvasContainer.width = maxSize.width; offScreenCanvasContainer.height = maxSize.height; offscreenMultiRenderWindow.resize(); } _copyToOnscreenCanvas(viewport, offScreenCanvas) { const { element, canvas, id: viewportId, renderingEngineId, suppressEvents, } = viewport; const dWidth = viewport.sWidth; const dHeight = viewport.sHeight; updateCanvasSizeAndAspectRatio(canvas, { width: dWidth, height: dHeight }); const onScreenContext = canvas.getContext('2d'); const contextIndex = this.contextPool.getContextIndexForViewport(viewportId); const maxSize = this.contextPool.getMaxSizeForContext(contextIndex); const sourceY = maxSize.height - dHeight; onScreenContext.drawImage(offScreenCanvas, 0, sourceY, dWidth, dHeight, 0, 0, dWidth, dHeight); return { element, suppressEvents, viewportId, renderingEngineId, viewportStatus: viewport.viewportStatus, }; } _resize(viewportsDrivenByVtkJs) { const contextsToResize = new Set(); for (const viewport of viewportsDrivenByVtkJs) { viewport.sx = 0; viewport.sy = 0; const contextIndex = this.contextPool.getContextIndexForViewport(viewport.id); const maxSizeChanged = this.contextPool.updateViewportSize(viewport.id, viewport.sWidth, viewport.sHeight); if (maxSizeChanged) { contextsToResize.add(contextIndex); } const contextData = this.contextPool.getContextByIndex(contextIndex); const { context: offscreenMultiRenderWindow } = contextData; const renderer = offscreenMultiRenderWindow.getRenderer(viewport.id); const maxSize = this.contextPool.getMaxSizeForContext(contextIndex); const xEnd = Math.min(1, viewport.sWidth / maxSize.width); const yEnd = Math.min(1, viewport.sHeight / maxSize.height); renderer.setViewport(0, 0, xEnd, yEnd); } contextsToResize.forEach((contextIndex) => { const contextData = this.contextPool.getContextByIndex(contextIndex); if (contextData) { const { context: offscreenMultiRenderWindow, container } = contextData; const maxSize = this.contextPool.getMaxSizeForContext(contextIndex); container.width = maxSize.width; container.height = maxSize.height; offscreenMultiRenderWindow.resize(); } }); } getWidgetRenderers() { const allViewports = this._getViewportsAsArray(); const widgetRenderers = new Map(); allViewports.forEach((vp) => { const widgets = vp.getWidgets ? vp.getWidgets() : []; widgets.forEach((widget) => { const renderer = widget.getRenderer ? widget.getRenderer() : null; if (renderer) { widgetRenderers.set(renderer, vp.id); } }); }); return widgetRenderers; } getViewportRenderPasses(viewportId) { const viewport = this.getViewport(viewportId); return viewport?.getRenderPasses ? viewport.getRenderPasses() : null; } getRenderer(viewportId) { const contextIndex = this.contextPool?.getContextIndexForViewport(viewportId); if (contextIndex === undefined || !this.contextPool) { return; } const contextData = this.contextPool.getContextByIndex(contextIndex); if (!contextData) { return; } const { context: offscreenMultiRenderWindow } = contextData; return offscreenMultiRenderWindow.getRenderer(viewportId); } disableElement(viewportId) { const viewport = this.getViewport(viewportId); if (!viewport) { return; } super.disableElement(viewportId); if (!viewportTypeUsesCustomRenderingPipeline(viewport.type) && this.contextPool) { const contextIndex = this.contextPool.getContextIndexForViewport(viewportId); if (contextIndex !== undefined) { const contextData = this.contextPool.getContextByIndex(contextIndex); if (contextData) { const { context: offscreenMultiRenderWindow } = contextData; offscreenMultiRenderWindow.removeRenderer(viewportId); } } this.contextPool.removeViewport(viewportId); } } destroy() { super.destroy(); if (this.contextPool) { this.contextPool.destroy(); } } getOffscreenMultiRenderWindow(viewportId) { if (!this.contextPool) { throw new Error('Offscreen multi render window is not available.'); } const contextIndex = this.contextPool.getContextIndexForViewport(viewportId); if (contextIndex === undefined) { throw new Error(`Offscreen multi render window is not available for viewport ${viewportId}.`); } const contextData = this.contextPool.getContextByIndex(contextIndex); if (!contextData) { throw new Error(`Offscreen multi render window is not available for viewport ${viewportId}.`); } return contextData.context; } } export default ContextPoolRenderingEngine;