From 51b18c8594402f41a0c56b0b53bb363d5bc19297 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Thu, 24 Sep 2026 19:34:35 -0700 Subject: [PATCH 01/11] create `ThreeInterface` --- src/data_manager/data_manager.ts | 11 +---- src/data_manager/device_interface.ts | 68 ++++++++++++++++++++++++++++ src/data_manager/types.ts | 15 ------ 3 files changed, 70 insertions(+), 24 deletions(-) create mode 100644 src/data_manager/device_interface.ts diff --git a/src/data_manager/data_manager.ts b/src/data_manager/data_manager.ts index 5ce886aa..d6610ccb 100644 --- a/src/data_manager/data_manager.ts +++ b/src/data_manager/data_manager.ts @@ -1,12 +1,4 @@ -import type { - ChunkId, - ChunkPriority, - ChunkEntry, - DataManagerLimits, - LocalChunkId, - Chunk, - DeviceInterface, -} from "./types.js"; +import type { ChunkId, ChunkPriority, ChunkEntry, DataManagerLimits, LocalChunkId, Chunk } from "./types.js"; import { chunkIdToString, ChunkState, @@ -23,6 +15,7 @@ import { import type { NumberType, TypedArray } from "../types.js"; import PriorityQueue from "./priority_queue.js"; import { VolumeDims } from "../VolumeDims.js"; +import type { DeviceInterface } from "./device_interface.js"; const SUBSCRIBER_ID = Symbol("DataManager.subscriberId"); diff --git a/src/data_manager/device_interface.ts b/src/data_manager/device_interface.ts new file mode 100644 index 00000000..a1dee99b --- /dev/null +++ b/src/data_manager/device_interface.ts @@ -0,0 +1,68 @@ +import { + ByteType, + Data3DTexture, + FloatType, + IntType, + type PixelFormat, + PixelFormatGPU, + RedFormat, + RedIntegerFormat, + ShortType, + type TextureDataType, + UnsignedByteType, + UnsignedIntType, + UnsignedShortType, +} from "three"; + +import type { NumberType, TypedArray } from "../types.js"; + +/** Abstraction over the graphics API or library used by `DataManager` to move chunks to/from the GPU. */ +export interface DeviceInterface { + /** + * Type predicate for this interface's *device handle*, for APIs (like WebGPU) that require explicit access to a + * shared resource to perform GPU operations. + */ + isDeviceHandle(val: unknown): val is Dev; + /** Create a texture with the given `data`, `dtype`, and `size` using `deviceHandle`. */ + createTexture(data: TypedArray, dtype: NumberType, size: [number, number, number], deviceHandle: Dev): Tex; + /** Destroy a texture and clean up its GPU resources. */ + destroyTexture(tex: Tex): void; + /** Called when `DataManager` is done with an update cycle, in case the interface has state to clean up. */ + finishUpdate(): void; +} + +const dataTypeToTextureProperties: { + [T in Exclude]: [TextureDataType, PixelFormat, PixelFormatGPU]; +} = { + int8: [ByteType, RedIntegerFormat, "R8I"], + int16: [ShortType, RedIntegerFormat, "R16I"], + int32: [IntType, RedIntegerFormat, "R32I"], + uint8: [UnsignedByteType, RedIntegerFormat, "R8UI"], + uint16: [UnsignedShortType, RedIntegerFormat, "R16UI"], + uint32: [UnsignedIntType, RedIntegerFormat, "R32UI"], + float32: [FloatType, RedFormat, "R32F"], +}; + +export class ThreeInterface implements DeviceInterface { + isDeviceHandle(_val: unknown): _val is void { + return true; + } + + createTexture(data: TypedArray, dtype: NumberType, [x, y, z]: [number, number, number]) { + const [texType, texFormat, texInternalFormat] = dataTypeToTextureProperties[dtype]; + const texture = new Data3DTexture(data, x, y, z); + texture.type = texType; + texture.format = texFormat; + texture.internalFormat = texInternalFormat; + texture.needsUpdate = true; + return texture; + } + + destroyTexture(tex: Data3DTexture): void { + tex.dispose(); + } + + finishUpdate() { + /* noop */ + } +} diff --git a/src/data_manager/types.ts b/src/data_manager/types.ts index 39125fcd..a20dfb7e 100644 --- a/src/data_manager/types.ts +++ b/src/data_manager/types.ts @@ -46,21 +46,6 @@ export const validateDataManagerLimits = (limits: DataManagerLimits): DataManage concurrentPrefetches: Math.min(limits.concurrentRequests, limits.concurrentPrefetches), }); -/** Abstraction over the graphics API or library used by `DataManager` to move chunks to/from the GPU. */ -export interface DeviceInterface { - /** - * Type predicate for this interface's *device handle*, for APIs (like WebGPU) that require explicit access to a - * shared resource to perform GPU operations. - */ - isDeviceHandle(val: unknown): val is Dev; - /** Create a texture with the given `data`, `dtype`, and `size` using `deviceHandle`. */ - createTexture(data: TypedArray, dtype: NumberType, size: [number, number, number], deviceHandle: Dev): Tex; - /** Destroy a texture and clean up its GPU resources. */ - destroyTexture(tex: Tex): void; - /** Called when `DataManager` is done with an update cycle, in case the interface has state to clean up. */ - finishUpdate(): void; -} - /** Reasons that a chunk is tracked by `DataManager`, in increasing order of priority. */ export const enum ChunkPriorityLevel { /** From 98fd1e14d0eb3835c8703adadecaf321ae3c0eaf Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Thu, 24 Sep 2026 20:03:10 -0700 Subject: [PATCH 02/11] add `WebGPU` interface --- src/data_manager/data_manager.ts | 1 + src/data_manager/device_interface.ts | 81 ++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/src/data_manager/data_manager.ts b/src/data_manager/data_manager.ts index d6610ccb..67b34886 100644 --- a/src/data_manager/data_manager.ts +++ b/src/data_manager/data_manager.ts @@ -53,6 +53,7 @@ const swapRemove = (arr: T[], index: number) => { } }; +// TODO also used in `device_interface`; consolidate to utils module or something const dataTypeToByteLength: { [T in NumberType]: number } = { int8: 1, int16: 2, diff --git a/src/data_manager/device_interface.ts b/src/data_manager/device_interface.ts index a1dee99b..3b089049 100644 --- a/src/data_manager/device_interface.ts +++ b/src/data_manager/device_interface.ts @@ -31,7 +31,9 @@ export interface DeviceInterface { finishUpdate(): void; } -const dataTypeToTextureProperties: { +// MARK: Three + +const dataTypeToThreeTextureProperties: { [T in Exclude]: [TextureDataType, PixelFormat, PixelFormatGPU]; } = { int8: [ByteType, RedIntegerFormat, "R8I"], @@ -49,7 +51,7 @@ export class ThreeInterface implements DeviceInterface { } createTexture(data: TypedArray, dtype: NumberType, [x, y, z]: [number, number, number]) { - const [texType, texFormat, texInternalFormat] = dataTypeToTextureProperties[dtype]; + const [texType, texFormat, texInternalFormat] = dataTypeToThreeTextureProperties[dtype]; const texture = new Data3DTexture(data, x, y, z); texture.type = texType; texture.format = texFormat; @@ -63,6 +65,79 @@ export class ThreeInterface implements DeviceInterface { } finishUpdate() { - /* noop */ + // noop + } +} + +// MARK: WebGPU + +export type GPUDeviceHandle = { + device: GPUDevice; + queue: GPUQueue; +}; + +const dataTypeToFormat: { [T in Exclude]: GPUTextureFormat } = { + int8: "r8snorm", + int16: "r16snorm", + uint8: "r8unorm", + uint16: "r16unorm", + float32: "r32float", +}; + +const dataTypeToSize: { [T in NumberType]: number } = { + int8: 1, + int16: 2, + int32: 4, + uint8: 1, + uint16: 2, + uint32: 4, + float32: 4, + float64: 8, +}; + +export class WebGPUInterface implements DeviceInterface { + isDeviceHandle(val: unknown): val is GPUDeviceHandle { + return Array.isArray(val) && val.length === 2 && val[0] instanceof GPUDevice && val[1] instanceof GPUQueue; + } + + createTexture( + data: TypedArray, + dtype: NumberType, + size: [number, number, number], + { device, queue }: GPUDeviceHandle + ): GPUTexture { + const format = dataTypeToFormat[dtype]; + const texelSize = dataTypeToSize[dtype]; + const texture = device.createTexture({ + format, + size, + mipLevelCount: 1, + sampleCount: 1, + dimension: "3d", + // 2 === GPUTextureUsage.COPY_DST; 4 === GPUTextureUsage.TEXTURE_BINDING + usage: 2 | 4, + }); + const copyInfo: GPUTexelCopyTextureInfo = { + texture, + mipLevel: 0, + origin: [0, 0, 0], + aspect: "all", + }; + const layout: GPUTexelCopyBufferLayout = { + offset: 0, + bytesPerRow: texelSize * texture.width, + rowsPerImage: texture.height, + }; + queue.writeTexture(copyInfo, data, layout, size); + return texture; + } + + destroyTexture(texture: GPUTexture) { + texture.destroy(); + } + + finishUpdate(): void { + // noop for now, but this class could be made stateful to e.g. reuse same-sized textures on eviction and use + // this method to definitively destroy unused textures } } From 7375bb3c5d3f2655c55e05cfcda482b503c561ee Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 30 Sep 2026 14:03:25 -0700 Subject: [PATCH 03/11] tweaks to device interfaces --- src/data_manager/device_interface.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/data_manager/device_interface.ts b/src/data_manager/device_interface.ts index 3b089049..adb9d072 100644 --- a/src/data_manager/device_interface.ts +++ b/src/data_manager/device_interface.ts @@ -12,7 +12,9 @@ import { UnsignedByteType, UnsignedIntType, UnsignedShortType, + type WebGLRenderer, } from "three"; +import type { Renderer } from "three/webgpu"; import type { NumberType, TypedArray } from "../types.js"; @@ -45,18 +47,24 @@ const dataTypeToThreeTextureProperties: { float32: [FloatType, RedFormat, "R32F"], }; -export class ThreeInterface implements DeviceInterface { - isDeviceHandle(_val: unknown): _val is void { - return true; +export class ThreeInterface implements DeviceInterface { + isDeviceHandle(val: unknown): val is Renderer | WebGLRenderer { + return typeof (val as Renderer | WebGLRenderer).initTexture === "function"; } - createTexture(data: TypedArray, dtype: NumberType, [x, y, z]: [number, number, number]) { + createTexture( + data: TypedArray, + dtype: NumberType, + [x, y, z]: [number, number, number], + renderer: Renderer | WebGLRenderer + ) { const [texType, texFormat, texInternalFormat] = dataTypeToThreeTextureProperties[dtype]; const texture = new Data3DTexture(data, x, y, z); texture.type = texType; texture.format = texFormat; texture.internalFormat = texInternalFormat; texture.needsUpdate = true; + renderer.initTexture(texture); return texture; } @@ -97,7 +105,7 @@ const dataTypeToSize: { [T in NumberType]: number } = { export class WebGPUInterface implements DeviceInterface { isDeviceHandle(val: unknown): val is GPUDeviceHandle { - return Array.isArray(val) && val.length === 2 && val[0] instanceof GPUDevice && val[1] instanceof GPUQueue; + return (val as GPUDeviceHandle).device instanceof GPUDevice && (val as GPUDeviceHandle).queue instanceof GPUQueue; } createTexture( From 253a83af686fc2acd57ba20bf059fac475a92b46 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 30 Sep 2026 16:01:34 -0700 Subject: [PATCH 04/11] starting on some unit tests --- src/test/DataManager.test.ts | 102 +++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/test/DataManager.test.ts diff --git a/src/test/DataManager.test.ts b/src/test/DataManager.test.ts new file mode 100644 index 00000000..efa69c32 --- /dev/null +++ b/src/test/DataManager.test.ts @@ -0,0 +1,102 @@ +import type { IChunkSource, IDataSubscriber } from "../data_manager/DataManager.js"; +import DataManager from "../data_manager/DataManager.js"; +import type { DeviceInterface } from "../data_manager/device_interface.js"; +import { type ChunkPriority, ChunkPriorityLevel } from "../data_manager/types.js"; +import type { TypedArray, NumberType } from "../types.js"; + +const mockSource = () => { + return { + getDims: vi.fn(() => [ + { + shape: [7, 7, 7, 7, 7], + spacing: [1, 1, 1, 1, 1], + chunkShape: [1, 1, 2, 2, 2], + dataType: "uint8", + spaceUnit: "micrometer", + timeUnit: "second", + }, + ]), + getChunk: vi.fn(async ({ tczyx }) => { + const length = tczyx.slice(2).reduce((len, coord) => (coord >= 3 ? len : len * 2), 1); + const data = new Uint8Array(length).fill(0); + data.set(tczyx); + return { data, dtype: "uint8" }; + }), + } satisfies IChunkSource; +}; + +type MockTex = { + data: TypedArray; + dtype: NumberType; + size: [number, number, number]; + id: number; +}; + +let mockTexCount = 0; + +const mockInterface = (): DeviceInterface => ({ + isDeviceHandle: (_val): _val is void => true, + createTexture: vi.fn((data, dtype, size) => { + const result = { data, dtype, size, id: mockTexCount }; + mockTexCount += 1; + return result; + }), + destroyTexture: vi.fn(), + finishUpdate: vi.fn(), +}); + +const mockSubscriber = (): IDataSubscriber => ({ + onChunkLoaded: vi.fn(), + onChunkOnGpu: vi.fn(), +}); + +describe("request priority", () => { + it("orders requests by priority", () => { + const manager = new DataManager(mockInterface()); + const sourceMock = mockSource(); + const source = manager.addSource(sourceMock); + const subscriber = mockSubscriber(); + + const queue = (x: number, score: number) => + manager.queueChunkRequest( + subscriber, + { source, multiscale: 0, tczyx: [0, 0, 0, 0, x] }, + { level: ChunkPriorityLevel.VISIBLE, score }, + false + ); + queue(0, 1); + queue(1, 0); + queue(2, 2); + manager.update(); + + const getCalls = sourceMock.getChunk.mock.calls; + expect(getCalls[0][0].tczyx[4]).toEqual(2); + expect(getCalls[1][0].tczyx[4]).toEqual(0); + expect(getCalls[2][0].tczyx[4]).toEqual(1); + }); + + it("orders `VISIBLE` requests ahead of `PREFETCH` ones", () => { + const manager = new DataManager(mockInterface()); + const sourceMock = mockSource(); + const source = manager.addSource(sourceMock); + const subscriber = mockSubscriber(); + + const queue = (x: number, priority: ChunkPriority) => + manager.queueChunkRequest(subscriber, { source, multiscale: 0, tczyx: [0, 0, 0, 0, x] }, priority, false); + queue(0, { level: ChunkPriorityLevel.PREFETCH, score: 1000 }); + queue(1, { level: ChunkPriorityLevel.VISIBLE, score: 0 }); + manager.update(); + + const getCalls = sourceMock.getChunk.mock.calls; + expect(getCalls[0][0].tczyx[4]).toEqual(1); + expect(getCalls[1][0].tczyx[4]).toEqual(0); + }); + + it("resolves chunk priorities across all subscribers", () => { + // TODO + }); + + it("handles memory and device priorities separately", () => { + // TODO + }); +}); From bb120dbcde5f0423e613b61ba479acbd289f4df0 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 6 Oct 2026 16:00:59 -0700 Subject: [PATCH 05/11] rename test module with underscores --- src/test/{DataManager.test.ts => data_manager.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/test/{DataManager.test.ts => data_manager.test.ts} (100%) diff --git a/src/test/DataManager.test.ts b/src/test/data_manager.test.ts similarity index 100% rename from src/test/DataManager.test.ts rename to src/test/data_manager.test.ts From 34ed8f2414833eec6f8c54cda6877abb9653e526 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 6 Oct 2026 16:12:11 -0700 Subject: [PATCH 06/11] fix import --- src/test/data_manager.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/data_manager.test.ts b/src/test/data_manager.test.ts index efa69c32..8950ae38 100644 --- a/src/test/data_manager.test.ts +++ b/src/test/data_manager.test.ts @@ -1,5 +1,5 @@ -import type { IChunkSource, IDataSubscriber } from "../data_manager/DataManager.js"; -import DataManager from "../data_manager/DataManager.js"; +import type { IChunkSource, IDataSubscriber } from "../data_manager/data_manager.js"; +import DataManager from "../data_manager/data_manager.js"; import type { DeviceInterface } from "../data_manager/device_interface.js"; import { type ChunkPriority, ChunkPriorityLevel } from "../data_manager/types.js"; import type { TypedArray, NumberType } from "../types.js"; From c06b110d3d4c534fad2ab861a1a95bceb0d56de6 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 7 Oct 2026 13:59:30 -0700 Subject: [PATCH 07/11] fix method name in tests --- src/test/data_manager.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/data_manager.test.ts b/src/test/data_manager.test.ts index 8950ae38..47b4fb52 100644 --- a/src/test/data_manager.test.ts +++ b/src/test/data_manager.test.ts @@ -58,7 +58,7 @@ describe("request priority", () => { const subscriber = mockSubscriber(); const queue = (x: number, score: number) => - manager.queueChunkRequest( + manager.addChunkRequest( subscriber, { source, multiscale: 0, tczyx: [0, 0, 0, 0, x] }, { level: ChunkPriorityLevel.VISIBLE, score }, @@ -82,7 +82,7 @@ describe("request priority", () => { const subscriber = mockSubscriber(); const queue = (x: number, priority: ChunkPriority) => - manager.queueChunkRequest(subscriber, { source, multiscale: 0, tczyx: [0, 0, 0, 0, x] }, priority, false); + manager.addChunkRequest(subscriber, { source, multiscale: 0, tczyx: [0, 0, 0, 0, x] }, priority, false); queue(0, { level: ChunkPriorityLevel.PREFETCH, score: 1000 }); queue(1, { level: ChunkPriorityLevel.VISIBLE, score: 0 }); manager.update(); From 0abfae0009f80b190623ecd84c1c286268a150bf Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Thu, 15 Oct 2026 14:43:35 -0700 Subject: [PATCH 08/11] fix `ThreeInterface` --- src/data_manager/device_interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data_manager/device_interface.ts b/src/data_manager/device_interface.ts index adb9d072..33da8a0c 100644 --- a/src/data_manager/device_interface.ts +++ b/src/data_manager/device_interface.ts @@ -49,7 +49,7 @@ const dataTypeToThreeTextureProperties: { export class ThreeInterface implements DeviceInterface { isDeviceHandle(val: unknown): val is Renderer | WebGLRenderer { - return typeof (val as Renderer | WebGLRenderer).initTexture === "function"; + return typeof val === "object" && typeof (val as Renderer | WebGLRenderer).initTexture === "function"; } createTexture( From 62d20cb470b76df3b3eb40b69604eefcad45b9b9 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Thu, 15 Oct 2026 16:29:21 -0700 Subject: [PATCH 09/11] move `dataTypeToSize` into utils --- src/data_manager/data_manager.ts | 15 ++------------- src/data_manager/device_interface.ts | 12 +----------- src/data_manager/utils.ts | 12 ++++++++++++ 3 files changed, 15 insertions(+), 24 deletions(-) create mode 100644 src/data_manager/utils.ts diff --git a/src/data_manager/data_manager.ts b/src/data_manager/data_manager.ts index 67b34886..c8badbca 100644 --- a/src/data_manager/data_manager.ts +++ b/src/data_manager/data_manager.ts @@ -16,6 +16,7 @@ import type { NumberType, TypedArray } from "../types.js"; import PriorityQueue from "./priority_queue.js"; import { VolumeDims } from "../VolumeDims.js"; import type { DeviceInterface } from "./device_interface.js"; +import { dataTypeToSize } from "./utils.js"; const SUBSCRIBER_ID = Symbol("DataManager.subscriberId"); @@ -53,18 +54,6 @@ const swapRemove = (arr: T[], index: number) => { } }; -// TODO also used in `device_interface`; consolidate to utils module or something -const dataTypeToByteLength: { [T in NumberType]: number } = { - int8: 1, - int16: 2, - int32: 4, - uint8: 1, - uint16: 2, - uint32: 4, - float32: 4, - float64: 8, -}; - type ChunkQueue = PriorityQueue; class ChunkQueues { @@ -202,7 +191,7 @@ export default class DataManager { return 0; } const { x, y, z, dataType } = dims; - return x * y * z * dataTypeToByteLength[dataType]; + return x * y * z * dataTypeToSize[dataType]; } /** diff --git a/src/data_manager/device_interface.ts b/src/data_manager/device_interface.ts index 33da8a0c..a4de14f9 100644 --- a/src/data_manager/device_interface.ts +++ b/src/data_manager/device_interface.ts @@ -17,6 +17,7 @@ import { import type { Renderer } from "three/webgpu"; import type { NumberType, TypedArray } from "../types.js"; +import { dataTypeToSize } from "./utils.js"; /** Abstraction over the graphics API or library used by `DataManager` to move chunks to/from the GPU. */ export interface DeviceInterface { @@ -92,17 +93,6 @@ const dataTypeToFormat: { [T in Exclude { isDeviceHandle(val: unknown): val is GPUDeviceHandle { return (val as GPUDeviceHandle).device instanceof GPUDevice && (val as GPUDeviceHandle).queue instanceof GPUQueue; diff --git a/src/data_manager/utils.ts b/src/data_manager/utils.ts new file mode 100644 index 00000000..122931ae --- /dev/null +++ b/src/data_manager/utils.ts @@ -0,0 +1,12 @@ +import type { NumberType } from "../types.js"; + +export const dataTypeToSize: { [T in NumberType]: number } = { + int8: 1, + int16: 2, + int32: 4, + uint8: 1, + uint16: 2, + uint32: 4, + float32: 4, + float64: 8, +}; From 68914779032a7194dddc9c8f6818699ba06e9662 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Thu, 15 Oct 2026 16:41:10 -0700 Subject: [PATCH 10/11] use native `BYTES_PER_ELEMENT` for size --- src/data_manager/device_interface.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/data_manager/device_interface.ts b/src/data_manager/device_interface.ts index a4de14f9..8efafc42 100644 --- a/src/data_manager/device_interface.ts +++ b/src/data_manager/device_interface.ts @@ -17,7 +17,6 @@ import { import type { Renderer } from "three/webgpu"; import type { NumberType, TypedArray } from "../types.js"; -import { dataTypeToSize } from "./utils.js"; /** Abstraction over the graphics API or library used by `DataManager` to move chunks to/from the GPU. */ export interface DeviceInterface { @@ -105,7 +104,6 @@ export class WebGPUInterface implements DeviceInterface Date: Thu, 15 Oct 2026 16:41:39 -0700 Subject: [PATCH 11/11] bit more of a fix for `ThreeInterface` --- src/data_manager/device_interface.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/data_manager/device_interface.ts b/src/data_manager/device_interface.ts index 8efafc42..901df9ac 100644 --- a/src/data_manager/device_interface.ts +++ b/src/data_manager/device_interface.ts @@ -49,7 +49,9 @@ const dataTypeToThreeTextureProperties: { export class ThreeInterface implements DeviceInterface { isDeviceHandle(val: unknown): val is Renderer | WebGLRenderer { - return typeof val === "object" && typeof (val as Renderer | WebGLRenderer).initTexture === "function"; + return ( + typeof val === "object" && val !== null && typeof (val as Renderer | WebGLRenderer).initTexture === "function" + ); } createTexture(