All files / core/src/RenderingEngine/helpers renderBackendRegistry.ts

6.06% Statements 4/66
0% Branches 0/40
0% Functions 0/11
6.06% Lines 4/66

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360                                                                                                                                                                        128x 128x 128x 128x                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                
import RenderBackends, {
  registerRenderBackendsConstant,
} from '../../enums/RenderBackends';
import { ActorRenderMode } from '../../types/IActor';
import type {
  EffectiveRenderBackend,
  RenderBackend as RenderBackendWire,
  RenderBackendConstants,
} from '../../types/RenderBackendRegistry';
import type { RenderPathDefinition } from '../GenericViewport/ViewportArchitectureTypes';
 
/**
 * The composited canvas a render mode draws to inside a planar
 * GenericViewport. The viewport owns one canvas per surface and toggles their
 * visibility when the active render mode changes; custom backends draw to one
 * of the existing surfaces.
 */
export type RenderSurface = 'vtk' | 'cpu';
 
/** The dataset shapes a planar render backend can mount. */
export type RenderBackendDataKind = 'image' | 'volume';
 
export interface RenderBackendRenderMode {
  /** Wire id of the render mode, e.g. 'myOrg:webgpuImage'. */
  id: string;
  /**
   * Planar render path definition implementing this render mode. Called once
   * per viewport (each PlanarViewport owns its resolver), so it must return a
   * fresh definition instance on every call. Core backends omit it: their
   * render paths ship in createDefaultPlanarRenderPaths().
   */
  createDefinition?: () => RenderPathDefinition;
}
 
export interface RenderBackendRenderModes {
  /** Render mode selected for image-stack (non-volume-backed) mounts. */
  image: RenderBackendRenderMode;
  /**
   * Render mode selected for volume-backed mounts. Omit when the backend
   * cannot render volume-backed datasets; selecting the backend for such a
   * dataset then fails with a descriptive error.
   */
  volume?: RenderBackendRenderMode;
}
 
export interface RenderBackendDefinition {
  /** Wire id of the backend, e.g. 'gpu' or 'myOrg:webgpu'. 'auto' is reserved. */
  backend: string;
  /**
   * Render modes this backend resolves to, by dataset kind. Each mode
   * co-locates its wire id with the render path definition implementing it.
   */
  renderModes: RenderBackendRenderModes;
  /** Composited canvas the backend's render modes draw to. Default 'vtk'. */
  surface?: RenderSurface;
}
 
type RegisterRenderBackendNamedOptions<
  Name extends keyof RenderBackendConstants,
> = RenderBackendDefinition & {
  /** Constant name added on `Enums.RenderBackends`, e.g. 'WEBGPU'. */
  name: Name;
  backend: RenderBackendConstants[Name];
};
 
type RegisterRenderBackendUnnamedOptions = RenderBackendDefinition & {
  backend: RenderBackendWire | string;
  name?: never;
};
 
export type RegisterRenderBackendOptions =
  | RegisterRenderBackendNamedOptions<keyof RenderBackendConstants>
  | RegisterRenderBackendUnnamedOptions;
 
interface RenderModeEntry {
  backend: EffectiveRenderBackend;
  kind: RenderBackendDataKind;
  surface: RenderSurface;
}
 
type InternalRenderBackendDefinition = RenderBackendDefinition & {
  surface: RenderSurface;
};
 
const backendDefinitions = new Map<string, InternalRenderBackendDefinition>();
const renderModeIndex = new Map<string, RenderModeEntry>();
const registeredConstantNames: Array<keyof RenderBackendConstants> = [];
let hasRegisteredCoreRenderBackends = false;
 
function registerCoreRenderBackends() {
  if (hasRegisteredCoreRenderBackends) {
    return;
  }
  hasRegisteredCoreRenderBackends = true;
 
  // Core render modes carry no createDefinition: their planar render paths
  // are part of createDefaultPlanarRenderPaths() and are always present.
  registerRenderBackend({
    backend: RenderBackends.GPU,
    renderModes: {
      image: { id: ActorRenderMode.VTK_IMAGE },
      volume: { id: ActorRenderMode.VTK_VOLUME_SLICE },
    },
    surface: 'vtk',
  });
  registerRenderBackend({
    backend: RenderBackends.CPU,
    renderModes: {
      image: { id: ActorRenderMode.CPU_IMAGE },
      volume: { id: ActorRenderMode.CPU_VOLUME },
    },
    surface: 'cpu',
  });
}
 
/**
 * Registers a render backend for planar GenericViewports, following the same
 * extensible-enum model as `registerViewportType`: the backend id becomes a
 * valid value for `setRenderBackend()`, the global
 * `rendering.planar.renderBackend` configuration and per-mount `renderBackend`
 * options, and — when `name` is given — a constant on `Enums.RenderBackends`.
 *
 * The definition carries the semantic wiring the viewport needs:
 * - `renderModes` tells the planar render-path decision which render mode to
 *   select for image-stack vs volume-backed datasets. Each mode co-locates
 *   its wire id with the `createDefinition` factory that injects the render
 *   path implementing it into every planar viewport's render path resolver.
 * - `surface` tells the viewport which composited canvas to show while one of
 *   the backend's modes is active.
 *
 * For compile-time typing, augment the `RenderBackendRegistry` (wire strings)
 * and `RenderBackendConstants` (constant names) interfaces in your extension's
 * `.d.ts`.
 *
 * @experimental Custom backends are not fully functional yet, and this
 * registration API is intentionally incomplete: the definition is intended to
 * grow additional parameters describing the backend-specific changes and
 * behaviours being registered (participation in the 'auto' capability
 * resolution, backend-owned surface/canvas creation, per-backend degradation
 * handling). Expect `RegisterRenderBackendOptions` to change.
 *
 * @example
 * ```ts
 * registerRenderBackend({
 *   name: 'WEBGPU',
 *   backend: 'myOrg:webgpu',
 *   renderModes: {
 *     image: {
 *       id: 'myOrg:webgpuImage',
 *       createDefinition: () => new WebGPUImageSlicePath(),
 *     },
 *     volume: {
 *       id: 'myOrg:webgpuVolume',
 *       createDefinition: () => new WebGPUVolumeSlicePath(),
 *     },
 *   },
 * });
 * setRenderBackend(RenderBackends.WEBGPU);
 * ```
 */
export function registerRenderBackend<
  Name extends keyof RenderBackendConstants,
>(options: RegisterRenderBackendNamedOptions<Name>): void;
export function registerRenderBackend(
  options: RegisterRenderBackendUnnamedOptions
): void;
export function registerRenderBackend({
  backend,
  name,
  renderModes,
  surface = 'vtk',
}: RegisterRenderBackendOptions): void {
  registerCoreRenderBackends();
 
  if (backend === RenderBackends.Auto) {
    throw new Error(
      `Render backend id "${RenderBackends.Auto}" is reserved for the automatic backend resolution`
    );
  }
 
  if (backendDefinitions.has(backend)) {
    throw new Error(`Render backend "${backend}" is already registered`);
  }
 
  if (!renderModes?.image?.id) {
    throw new Error(
      `Render backend "${backend}" must declare renderModes.image`
    );
  }
 
  if (renderModes.volume && renderModes.volume.id === renderModes.image.id) {
    throw new Error(
      `Render backend "${backend}" must use distinct render modes for image and volume`
    );
  }
 
  const modeEntries: Array<[string, RenderBackendDataKind]> = [
    [renderModes.image.id, 'image'],
  ];
 
  if (renderModes.volume) {
    modeEntries.push([renderModes.volume.id, 'volume']);
  }
 
  for (const [renderMode] of modeEntries) {
    const existing = renderModeIndex.get(renderMode);
    if (existing && existing.backend !== backend) {
      throw new Error(
        `Render mode "${renderMode}" is already provided by render backend "${existing.backend}"`
      );
    }
  }
 
  if (name && Object.prototype.hasOwnProperty.call(RenderBackends, name)) {
    throw new Error(`Render backend constant "${String(name)}" already exists`);
  }
 
  backendDefinitions.set(backend, {
    backend,
    renderModes,
    surface,
  });
 
  for (const [renderMode, kind] of modeEntries) {
    renderModeIndex.set(renderMode, {
      backend: backend as EffectiveRenderBackend,
      kind,
      surface,
    });
  }
 
  if (name) {
    registerRenderBackendsConstant(
      name,
      backend as RenderBackendConstants[typeof name]
    );
    registeredConstantNames.push(name);
  }
}
 
/**
 * Whether `backend` names a registered concrete render backend. The 'auto'
 * preference is not a concrete backend and returns false; validate it
 * separately where a preference (rather than a resolved backend) is accepted.
 */
export function isRegisteredRenderBackend(backend: string): boolean {
  registerCoreRenderBackends();
  return backendDefinitions.has(backend);
}
 
export function getRenderBackendDefinition(
  backend: string
): Readonly<RenderBackendDefinition> | undefined {
  registerCoreRenderBackends();
  return backendDefinitions.get(backend);
}
 
/**
 * Render mode a backend resolves to for the given dataset kind. Throws a
 * descriptive error when the backend is unknown or does not support the kind
 * (e.g. an image-only backend asked to mount a volume-backed dataset).
 */
export function getRenderModeForBackend(
  backend: string,
  kind: RenderBackendDataKind
): string {
  const definition = getRenderBackendDefinition(backend);
 
  if (!definition) {
    throw new Error(
      `Unknown render backend "${backend}". Register it with registerRenderBackend() first.`
    );
  }
 
  const renderMode = definition.renderModes[kind];
 
  if (!renderMode) {
    throw new Error(
      `Render backend "${backend}" does not support ${kind}-backed rendering`
    );
  }
 
  return renderMode.id;
}
 
/** Backend that provides `renderMode`, or undefined for unknown modes. */
export function getRenderBackendForRenderMode(
  renderMode: string | undefined
): EffectiveRenderBackend | undefined {
  registerCoreRenderBackends();
  return renderMode ? renderModeIndex.get(renderMode)?.backend : undefined;
}
 
/** Whether `renderMode` is a registered backend's volume render mode. */
export function isVolumeRenderMode(renderMode: string | undefined): boolean {
  registerCoreRenderBackends();
  return renderMode
    ? renderModeIndex.get(renderMode)?.kind === 'volume'
    : false;
}
 
/** Whether `renderMode` is a registered backend's image render mode. */
export function isImageRenderMode(renderMode: string | undefined): boolean {
  registerCoreRenderBackends();
  return renderMode ? renderModeIndex.get(renderMode)?.kind === 'image' : false;
}
 
/**
 * Composited canvas the given render mode draws to. Unknown modes default to
 * the 'vtk' surface.
 */
export function getRenderSurfaceForRenderMode(
  renderMode: string | undefined
): RenderSurface {
  registerCoreRenderBackends();
  return (renderMode && renderModeIndex.get(renderMode)?.surface) || 'vtk';
}
 
/**
 * Instantiates the planar render path definitions of every registered
 * extension backend's render modes (core backends ship their paths in
 * `createDefaultPlanarRenderPaths`). Called once per planar viewport.
 */
export function createRegisteredPlanarRenderPaths(): RenderPathDefinition[] {
  registerCoreRenderBackends();
 
  const paths: RenderPathDefinition[] = [];
 
  for (const definition of backendDefinitions.values()) {
    const { image, volume } = definition.renderModes;
    for (const mode of [image, volume]) {
      if (mode?.createDefinition) {
        paths.push(mode.createDefinition());
      }
    }
  }
 
  return paths;
}
 
/**
 * Test-only: wipes all backend registrations (including the lazily registered
 * core backends) and removes the `Enums.RenderBackends` constants added
 * through `registerRenderBackend()`. Not part of the public API — import it
 * from this module directly in test setup/teardown.
 * @internal
 */
export function __resetRenderBackendRegistry(): void {
  backendDefinitions.clear();
  renderModeIndex.clear();
  hasRegisteredCoreRenderBackends = false;
 
  for (const name of registeredConstantNames) {
    delete (RenderBackends as Record<keyof RenderBackendConstants, string>)[
      name
    ];
  }
  registeredConstantNames.length = 0;
}