@cornerstonejs/core
Version:
Cornerstone3D Core
421 lines (420 loc) • 19 kB
JavaScript
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;