From e9e44a656dc4bd74d201756be12b44773cbefafe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Mandado=20Almajano?= <50873010+atomicsulfate@users.noreply.github.com> Date: Fri, 6 Nov 2020 09:24:41 +0100 Subject: [PATCH] HARP-12700: Disposal of POI resources by reference counting. (#1946) * HARP-12700: Disposal of POI resources by reference counting. - Share material and texture across pois with same image even if they have different render order. - Do reference counting on resources to dispose of them when they're not used any longer. * HARP-12700: Address review comment and fix unit tests. --- .../lib/TechniqueParams.ts | 6 +- @here/harp-mapview/lib/MapView.ts | 5 +- @here/harp-mapview/lib/Tile.ts | 3 + @here/harp-mapview/lib/poi/BoxBuffer.ts | 228 +++---- @here/harp-mapview/lib/poi/PoiRenderer.ts | 557 +++++++++--------- @here/harp-mapview/lib/text/TextElement.ts | 18 +- @here/harp-mapview/test/PlacementTest.ts | 5 +- .../harp-mapview/test/PoiBatchRegistryTest.ts | 174 ++++++ @here/harp-mapview/test/PoiInfoBuilder.ts | 51 +- @here/harp-mapview/test/PoiRendererTest.ts | 8 +- @here/harp-mapview/test/TileTest.ts | 13 + 11 files changed, 661 insertions(+), 407 deletions(-) create mode 100644 @here/harp-mapview/test/PoiBatchRegistryTest.ts diff --git a/@here/harp-datasource-protocol/lib/TechniqueParams.ts b/@here/harp-datasource-protocol/lib/TechniqueParams.ts index f9213e5c70..863507302c 100644 --- a/@here/harp-datasource-protocol/lib/TechniqueParams.ts +++ b/@here/harp-datasource-protocol/lib/TechniqueParams.ts @@ -707,11 +707,13 @@ export interface MarkerTechniqueParams extends BaseTechniqueParams { */ poiNameField?: string; /** - * Name of [[ImageTexture]] definition to use. + * The name of either the {@link ImageTexture} in {@link Theme.imageTextures} or the user image + * cached in {@link @here/harp-mapview#userImageCache} to be rendered as marker. */ imageTexture?: DynamicProperty; /** - * Field name to extract imageTexture content from. + * Field name to extract imageTexture content from, if imageTexture refers to an + * [[ImageTexture]] definition. */ imageTextureField?: string; /** diff --git a/@here/harp-mapview/lib/MapView.ts b/@here/harp-mapview/lib/MapView.ts index 7d6432359b..85bddba901 100644 --- a/@here/harp-mapview/lib/MapView.ts +++ b/@here/harp-mapview/lib/MapView.ts @@ -1885,6 +1885,7 @@ export class MapView extends EventDispatcher { } /** + * @internal * Get the {@link ImageCache} that belongs to this `MapView`. * * Images stored in this cache are primarily used for POIs (icons) and they are used with the @@ -1900,8 +1901,7 @@ export class MapView extends EventDispatcher { /** * Get the {@link ImageCache} for user images that belongs to this `MapView`. * - * Images added to this cache can be removed if no longer required. If images with identical - * names are stored in imageCache and userImageCache, the userImageCache will take precedence. + * Images added to this cache can be removed if no longer required. */ get userImageCache(): MapViewImageCache { return this.m_userImageCache; @@ -1909,6 +1909,7 @@ export class MapView extends EventDispatcher { /** * @hidden + * @internal * Get the {@link PoiManager} that belongs to this `MapView`. */ get poiManager(): PoiManager { diff --git a/@here/harp-mapview/lib/Tile.ts b/@here/harp-mapview/lib/Tile.ts index e5547f7952..25c52419b8 100644 --- a/@here/harp-mapview/lib/Tile.ts +++ b/@here/harp-mapview/lib/Tile.ts @@ -966,6 +966,9 @@ export class Tile implements CachedResource { } this.textElementsChanged = true; this.m_pathBlockingElements.splice(0); + this.textElementGroups.forEach((element: TextElement) => { + element.dispose(); + }); this.textElementGroups.clear(); } diff --git a/@here/harp-mapview/lib/poi/BoxBuffer.ts b/@here/harp-mapview/lib/poi/BoxBuffer.ts index 43025f1978..b2aa21bf60 100644 --- a/@here/harp-mapview/lib/poi/BoxBuffer.ts +++ b/@here/harp-mapview/lib/poi/BoxBuffer.ts @@ -104,53 +104,53 @@ export class BoxBuffer { /** * {@link @here/harp-datasource-protocol#BufferAttribute} holding the `BoxBuffer` position data. */ - protected positionAttribute?: THREE.BufferAttribute; + private m_positionAttribute?: THREE.BufferAttribute; /** * {@link @here/harp-datasource-protocol#BufferAttribute} holding the `BoxBuffer` color data. */ - protected colorAttribute?: THREE.BufferAttribute; + private m_colorAttribute?: THREE.BufferAttribute; /** * {@link @here/harp-datasource-protocol#BufferAttribute} holding the `BoxBuffer` uv data. */ - protected uvAttribute?: THREE.BufferAttribute; + private m_uvAttribute?: THREE.BufferAttribute; /** * {@link @here/harp-datasource-protocol#BufferAttribute} holding the `BoxBuffer` index data. */ - protected indexAttribute?: THREE.BufferAttribute; - protected pickInfos: Array; + private m_indexAttribute?: THREE.BufferAttribute; + private readonly m_pickInfos: Array; /** * [[BufferGeometry]] holding all the different * {@link @here/harp-datasource-protocol#BufferAttribute}s. */ - protected geometry: THREE.BufferGeometry | undefined; + private m_geometry: THREE.BufferGeometry | undefined; /** * [[Mesh]] used for rendering. */ - protected internalMesh: BoxBufferMesh | undefined; + private m_mesh: BoxBufferMesh | undefined; private m_size: number = 0; /** * Creates a new `BoxBuffer`. * - * @param material - Material to be used for [[Mesh]] of this `BoxBuffer`. - * @param renderOrder - Optional renderOrder of this buffer. + * @param m_material - Material to be used for [[Mesh]] of this `BoxBuffer`. + * @param m_renderOrder - Optional renderOrder of this buffer. * @param startElementCount - Initial number of elements this `BoxBuffer` can hold. - * @param maxElementCount - Maximum number of elements this `BoxBuffer` can hold. + * @param m_maxElementCount - Maximum number of elements this `BoxBuffer` can hold. */ constructor( - readonly material: THREE.Material | THREE.Material[], - readonly renderOrder: number = 0, - readonly startElementCount = START_BOX_BUFFER_SIZE, - readonly maxElementCount = MAX_BOX_BUFFER_SIZE + private readonly m_material: THREE.Material | THREE.Material[], + private readonly m_renderOrder: number = 0, + startElementCount = START_BOX_BUFFER_SIZE, + private readonly m_maxElementCount = MAX_BOX_BUFFER_SIZE ) { this.resizeBuffer(startElementCount); - this.pickInfos = new Array(); + this.m_pickInfos = new Array(); } /** @@ -159,18 +159,18 @@ export class BoxBuffer { * @returns A clone of this `BoxBuffer`. */ clone(): BoxBuffer { - return new BoxBuffer(this.material, this.renderOrder); + return new BoxBuffer(this.m_material, this.m_renderOrder); } /** * Dispose of the geometry. */ dispose() { - if (this.geometry !== undefined) { - this.geometry.dispose(); - this.geometry = undefined; + if (this.m_geometry !== undefined) { + this.m_geometry.dispose(); + this.m_geometry = undefined; } - this.internalMesh = undefined; + this.m_mesh = undefined; } /** @@ -184,12 +184,12 @@ export class BoxBuffer { * Clear's the `BoxBuffer` attribute buffers. */ reset() { - if (this.positionAttribute !== undefined) { - this.positionAttribute.count = 0; - this.colorAttribute!.count = 0; - this.uvAttribute!.count = 0; - this.indexAttribute!.count = 0; - this.pickInfos!.length = 0; + if (this.m_positionAttribute !== undefined) { + this.m_positionAttribute.count = 0; + this.m_colorAttribute!.count = 0; + this.m_uvAttribute!.count = 0; + this.m_indexAttribute!.count = 0; + this.m_pickInfos!.length = 0; } } @@ -202,17 +202,17 @@ export class BoxBuffer { * @returns `true` if the element (box or glyph) can be added to the buffer, `false` otherwise. */ canAddElements(glyphCount = 1): boolean { - const indexAttribute = this.indexAttribute!; + const indexAttribute = this.m_indexAttribute!; if ( indexAttribute.count + glyphCount * NUM_INDICES_PER_ELEMENT >= indexAttribute.array.length ) { // Too many elements for the current buffer, check if we can resize the buffer. - if (indexAttribute.array.length >= this.maxElementCount * NUM_INDICES_PER_ELEMENT) { + if (indexAttribute.array.length >= this.m_maxElementCount * NUM_INDICES_PER_ELEMENT) { return false; } - const newSize = Math.min(this.maxElementCount, this.size === 0 ? 256 : this.size * 2); + const newSize = Math.min(this.m_maxElementCount, this.size === 0 ? 256 : this.size * 2); this.resize(newSize); } return true; @@ -223,11 +223,11 @@ export class BoxBuffer { */ saveState(): State { const state: State = { - positionAttributeCount: this.positionAttribute!.count, - colorAttributeCount: this.colorAttribute!.count, - uvAttributeCount: this.uvAttribute!.count, - indexAttributeCount: this.indexAttribute!.count, - pickInfoCount: this.pickInfos!.length + positionAttributeCount: this.m_positionAttribute!.count, + colorAttributeCount: this.m_colorAttribute!.count, + uvAttributeCount: this.m_uvAttribute!.count, + indexAttributeCount: this.m_indexAttribute!.count, + pickInfoCount: this.m_pickInfos!.length }; return state; } @@ -238,11 +238,11 @@ export class BoxBuffer { * @param state - [[State]] struct describing a previous attribute state. */ restoreState(state: State) { - this.positionAttribute!.count = state.positionAttributeCount; - this.colorAttribute!.count = state.colorAttributeCount; - this.uvAttribute!.count = state.uvAttributeCount; - this.indexAttribute!.count = state.indexAttributeCount; - this.pickInfos!.length = state.pickInfoCount; + this.m_positionAttribute!.count = state.positionAttributeCount; + this.m_colorAttribute!.count = state.colorAttributeCount; + this.m_uvAttribute!.count = state.uvAttributeCount; + this.m_indexAttribute!.count = state.indexAttributeCount; + this.m_pickInfos!.length = state.pickInfoCount; } /** @@ -276,10 +276,10 @@ export class BoxBuffer { const b = Math.round(color.b * opacity * 255); const a = Math.round(opacity * 255); - const positionAttribute = this.positionAttribute!; - const colorAttribute = this.colorAttribute!; - const uvAttribute = this.uvAttribute!; - const indexAttribute = this.indexAttribute!; + const positionAttribute = this.m_positionAttribute!; + const colorAttribute = this.m_colorAttribute!; + const uvAttribute = this.m_uvAttribute!; + const indexAttribute = this.m_indexAttribute!; const baseVertex = positionAttribute.count; const baseIndex = indexAttribute.count; @@ -311,7 +311,7 @@ export class BoxBuffer { uvAttribute.count += NUM_VERTICES_PER_ELEMENT; indexAttribute.count += NUM_INDICES_PER_ELEMENT; - this.pickInfos.push(pickInfo); + this.m_pickInfos.push(pickInfo); return true; } @@ -321,10 +321,10 @@ export class BoxBuffer { * data. */ updateBufferGeometry() { - const positionAttribute = this.positionAttribute!; - const colorAttribute = this.colorAttribute!; - const uvAttribute = this.uvAttribute!; - const indexAttribute = this.indexAttribute!; + const positionAttribute = this.m_positionAttribute!; + const colorAttribute = this.m_colorAttribute!; + const uvAttribute = this.m_uvAttribute!; + const indexAttribute = this.m_indexAttribute!; if (positionAttribute.count > 0) { positionAttribute.needsUpdate = true; @@ -351,9 +351,9 @@ export class BoxBuffer { indexAttribute.updateRange.count = indexAttribute.count; } - if (this.geometry !== undefined) { - this.geometry.clearGroups(); - this.geometry.addGroup(0, this.indexAttribute!.count); + if (this.m_geometry !== undefined) { + this.m_geometry.clearGroups(); + this.m_geometry.addGroup(0, this.m_indexAttribute!.count); } } @@ -363,7 +363,7 @@ export class BoxBuffer { */ cleanUp() { // If there is nothing in this buffer, resize it, it may never be used again. - if (this.indexAttribute!.count === 0 && this.size > START_BOX_BUFFER_SIZE) { + if (this.m_indexAttribute!.count === 0 && this.size > START_BOX_BUFFER_SIZE) { this.clearAttributes(); } } @@ -372,7 +372,7 @@ export class BoxBuffer { * Determine if the mesh is empty. */ get isEmpty(): boolean { - return this.internalMesh!.isEmpty; + return this.m_mesh!.isEmpty; } /** @@ -380,10 +380,10 @@ export class BoxBuffer { * resized. The mesh, once created, will not change, so it can always be added to the scene. */ get mesh(): BoxBufferMesh { - if (this.internalMesh === undefined) { + if (this.m_mesh === undefined) { this.resize(); } - return this.internalMesh!; + return this.m_mesh!; } /** @@ -399,9 +399,9 @@ export class BoxBuffer { pickCallback: (pickData: any | undefined) => void, imageData?: ImageBitmap | ImageData ) { - const n = this.pickInfos.length; - const pickInfos = this.pickInfos; - const positions = this.positionAttribute!; + const n = this.m_pickInfos.length; + const pickInfos = this.m_pickInfos; + const positions = this.m_positionAttribute!; const screenX = screenPosition.x; const screenY = screenPosition.y; @@ -463,29 +463,29 @@ export class BoxBuffer { * @param forceResize - Optional flag to force a resize even if new size is smaller than before. */ resize(newSize?: number, forceResize?: boolean): BoxBufferMesh { - if (this.geometry !== undefined) { - this.geometry.dispose(); + if (this.m_geometry !== undefined) { + this.m_geometry.dispose(); } - this.geometry = new THREE.BufferGeometry(); + this.m_geometry = new THREE.BufferGeometry(); if (newSize !== undefined && (forceResize === true || newSize > this.size)) { this.resizeBuffer(newSize); } - this.geometry.setAttribute("position", this.positionAttribute!); - this.geometry.setAttribute("color", this.colorAttribute!); - this.geometry.setAttribute("uv", this.uvAttribute!); - this.geometry.setIndex(this.indexAttribute!); - this.geometry.addGroup(0, this.indexAttribute!.count); + this.m_geometry.setAttribute("position", this.m_positionAttribute!); + this.m_geometry.setAttribute("color", this.m_colorAttribute!); + this.m_geometry.setAttribute("uv", this.m_uvAttribute!); + this.m_geometry.setIndex(this.m_indexAttribute!); + this.m_geometry.addGroup(0, this.m_indexAttribute!.count); - if (this.internalMesh === undefined) { - this.internalMesh = new BoxBufferMesh(this.geometry, this.material); - this.internalMesh.renderOrder = this.renderOrder; + if (this.m_mesh === undefined) { + this.m_mesh = new BoxBufferMesh(this.m_geometry, this.m_material); + this.m_mesh.renderOrder = this.m_renderOrder; } else { - this.internalMesh.geometry = this.geometry; + this.m_mesh.geometry = this.m_geometry; } - return this.internalMesh; + return this.m_mesh; } /** @@ -495,10 +495,10 @@ export class BoxBuffer { */ updateMemoryUsage(info: MemoryUsage) { const numBytes = - this.positionAttribute!.count * NUM_POSITION_VALUES_PER_VERTEX * NUM_BYTES_PER_FLOAT + - this.colorAttribute!.count * NUM_COLOR_VALUES_PER_VERTEX + - this.uvAttribute!.count * NUM_UV_VALUES_PER_VERTEX * NUM_BYTES_PER_FLOAT + - this.indexAttribute!.count * NUM_BYTES_PER_INT32; // May be UInt16, so we overestimate + this.m_positionAttribute!.count * NUM_POSITION_VALUES_PER_VERTEX * NUM_BYTES_PER_FLOAT + + this.m_colorAttribute!.count * NUM_COLOR_VALUES_PER_VERTEX + + this.m_uvAttribute!.count * NUM_UV_VALUES_PER_VERTEX * NUM_BYTES_PER_FLOAT + + this.m_indexAttribute!.count * NUM_BYTES_PER_INT32; // May be UInt16, so we overestimate info.heapSize += numBytes; info.gpuSize += numBytes; @@ -515,7 +515,7 @@ export class BoxBuffer { * @param canvas - Canvas element that will be used to draw the image, in case the imageData is * an `ImageBitmap` */ - protected isPixelTransparent( + private isPixelTransparent( imageData: ImageBitmap | ImageData, xScreenPos: number, yScreenPos: number, @@ -543,11 +543,11 @@ export class BoxBuffer { /** * Remove current attributes and arrays. Minimizes memory footprint. */ - protected clearAttributes() { - this.positionAttribute = undefined; - this.colorAttribute = undefined; - this.uvAttribute = undefined; - this.indexAttribute = undefined; + private clearAttributes() { + this.m_positionAttribute = undefined; + this.m_colorAttribute = undefined; + this.m_uvAttribute = undefined; + this.m_indexAttribute = undefined; this.resize(START_BOX_BUFFER_SIZE, true); } @@ -557,57 +557,57 @@ export class BoxBuffer { * @param newSize - New number of elements in the buffer. Number has to be larger than the * previous size. */ - protected resizeBuffer(newSize: number) { + private resizeBuffer(newSize: number) { const newPositionArray = new Float32Array( newSize * NUM_VERTICES_PER_ELEMENT * NUM_POSITION_VALUES_PER_VERTEX ); - if (this.positionAttribute !== undefined && this.positionAttribute.array.length > 0) { - const positionAttributeCount = this.positionAttribute.count; - newPositionArray.set(this.positionAttribute.array); - this.positionAttribute.array = newPositionArray; - this.positionAttribute.count = positionAttributeCount; + if (this.m_positionAttribute !== undefined && this.m_positionAttribute.array.length > 0) { + const positionAttributeCount = this.m_positionAttribute.count; + newPositionArray.set(this.m_positionAttribute.array); + this.m_positionAttribute.array = newPositionArray; + this.m_positionAttribute.count = positionAttributeCount; } else { - this.positionAttribute = new THREE.BufferAttribute( + this.m_positionAttribute = new THREE.BufferAttribute( newPositionArray, NUM_POSITION_VALUES_PER_VERTEX ); - this.positionAttribute.count = 0; - this.positionAttribute.setUsage(THREE.DynamicDrawUsage); + this.m_positionAttribute.count = 0; + this.m_positionAttribute.setUsage(THREE.DynamicDrawUsage); } const newColorArray = new Uint8Array( newSize * NUM_VERTICES_PER_ELEMENT * NUM_COLOR_VALUES_PER_VERTEX ); - if (this.colorAttribute !== undefined) { - const colorAttributeCount = this.colorAttribute.count; - newColorArray.set(this.colorAttribute.array); - this.colorAttribute.array = newColorArray; - this.colorAttribute.count = colorAttributeCount; + if (this.m_colorAttribute !== undefined) { + const colorAttributeCount = this.m_colorAttribute.count; + newColorArray.set(this.m_colorAttribute.array); + this.m_colorAttribute.array = newColorArray; + this.m_colorAttribute.count = colorAttributeCount; } else { - this.colorAttribute = new THREE.BufferAttribute( + this.m_colorAttribute = new THREE.BufferAttribute( newColorArray, NUM_COLOR_VALUES_PER_VERTEX, true ); - this.colorAttribute.count = 0; - this.colorAttribute.setUsage(THREE.DynamicDrawUsage); + this.m_colorAttribute.count = 0; + this.m_colorAttribute.setUsage(THREE.DynamicDrawUsage); } const newUvArray = new Float32Array( newSize * NUM_VERTICES_PER_ELEMENT * NUM_UV_VALUES_PER_VERTEX ); - if (this.uvAttribute !== undefined) { - const uvAttributeCount = this.uvAttribute.count; - newUvArray.set(this.uvAttribute.array); - this.uvAttribute.array = newUvArray; - this.uvAttribute.count = uvAttributeCount; + if (this.m_uvAttribute !== undefined) { + const uvAttributeCount = this.m_uvAttribute.count; + newUvArray.set(this.m_uvAttribute.array); + this.m_uvAttribute.array = newUvArray; + this.m_uvAttribute.count = uvAttributeCount; } else { - this.uvAttribute = new THREE.BufferAttribute(newUvArray, NUM_UV_VALUES_PER_VERTEX); - this.uvAttribute.count = 0; - this.uvAttribute.setUsage(THREE.DynamicDrawUsage); + this.m_uvAttribute = new THREE.BufferAttribute(newUvArray, NUM_UV_VALUES_PER_VERTEX); + this.m_uvAttribute.count = 0; + this.m_uvAttribute.setUsage(THREE.DynamicDrawUsage); } const numIndexValues = newSize * NUM_INDICES_PER_ELEMENT * NUM_INDEX_VALUES_PER_VERTEX; @@ -617,18 +617,18 @@ export class BoxBuffer { ? new Uint32Array(numIndexValues) : new Uint16Array(numIndexValues); - if (this.indexAttribute !== undefined) { - const indexAttributeCount = this.indexAttribute.count; - newIndexArray.set(this.indexAttribute.array); - this.indexAttribute.array = newIndexArray; - this.indexAttribute.count = indexAttributeCount; + if (this.m_indexAttribute !== undefined) { + const indexAttributeCount = this.m_indexAttribute.count; + newIndexArray.set(this.m_indexAttribute.array); + this.m_indexAttribute.array = newIndexArray; + this.m_indexAttribute.count = indexAttributeCount; } else { - this.indexAttribute = new THREE.BufferAttribute( + this.m_indexAttribute = new THREE.BufferAttribute( newIndexArray, NUM_INDEX_VALUES_PER_VERTEX ); - this.indexAttribute.count = 0; - this.indexAttribute.setUsage(THREE.DynamicDrawUsage); + this.m_indexAttribute.count = 0; + this.m_indexAttribute.setUsage(THREE.DynamicDrawUsage); } this.m_size = newSize; diff --git a/@here/harp-mapview/lib/poi/PoiRenderer.ts b/@here/harp-mapview/lib/poi/PoiRenderer.ts index 4d60e5d298..bfc09d102b 100644 --- a/@here/harp-mapview/lib/poi/PoiRenderer.ts +++ b/@here/harp-mapview/lib/poi/PoiRenderer.ts @@ -5,7 +5,7 @@ */ import { Env, getPropertyValue, ImageTexture } from "@here/harp-datasource-protocol"; import { IconMaterial } from "@here/harp-materials"; -import { MemoryUsage, TextCanvas } from "@here/harp-text-canvas"; +import { MemoryUsage, TextCanvas, TextCanvasLayer } from "@here/harp-text-canvas"; import { assert, LoggerManager, Math2D } from "@here/harp-utils"; import * as THREE from "three"; @@ -19,9 +19,6 @@ import { BoxBuffer } from "./BoxBuffer"; const logger = LoggerManager.instance.create("PoiRenderer"); -const INVALID_RENDER_BATCH = -1; -const tempPos = new THREE.Vector3(0); - /** * Neutral color used as `vColor` attribute of [[IconMaterial]] if no `iconColor` color was * specified. @@ -34,91 +31,86 @@ const neutralColor = new THREE.Color(1, 1, 1); const tmpIconColor = new THREE.Color(); /** - * The `PoiRenderBufferBatch` contains the geometry and the material for all POIs that share the - * same icon image ({@link @here/harp-datasource-protocol#ImageTexture}). - * - * @remarks - * If the image is the same, all the objects in this batch can - * share the same material, which makes them renderable in the same draw call, whatever the number - * of actual objects (WebGL limits apply!). - * - * There is a `PoiRenderBufferBatch` for every icon in a texture atlas, since the size of the icon - * in the atlas as well as the texture coordinates are specified in the `PoiRenderBufferBatch`. + * @internal + * Buffer for POIs sharing same material and render order, renderable in a single draw call + * (WebGL limits apply, see {@link BoxBuffer}). */ -class PoiRenderBufferBatch { - // Enable trilinear filtering to reduce flickering due to distance scaling - static trilinear: boolean = true; - - boxBuffer: BoxBuffer | undefined; - - private m_material?: THREE.Material | THREE.Material[]; +export class PoiBuffer { + private m_refCount: number = 0; /** - * Create the `PoiRenderBufferBatch`. - * - * @param mapView - The {@link MapView} instance. - * @param scene - The three.js scene to add the POIs to. - * @param imageItem - The icon that will have his material shared. - * @param renderOrder - RenderOrder of the batch geometry's [[Mesh]]. + * Creates a `PoiBuffer` + * @param buffer - + * @param layer - The {@link TextCanvas} layer used to render the POIs. */ constructor( - readonly mapView: MapView, - readonly scene: THREE.Scene, - readonly imageItem: ImageItem, - readonly renderOrder: number + readonly buffer: BoxBuffer, + readonly layer: TextCanvasLayer, + private readonly m_onDispose: () => void ) {} /** - * Initialize with the {@link @here/harp-datasource-protocol#ImageTexture}. - * - * @remarks - * Loads the image and sets up the icon size, the texture - * coordinates and material of the batch. Since image loading is done asynchronously, this - * batch cannot be rendered right away. MapView#update is being triggered if it loaded - * successfully. + * Increases this `PoiBuffer`'s reference count. + * @returns this `PoiBuffer`. */ - init() { - if (this.boxBuffer === undefined) { - this.setup(); - } + increaseRefCount(): PoiBuffer { + ++this.m_refCount; + return this; } /** - * Clean the `PoiRenderBufferBatch`, remove all icon boxes. Called before starting a new frame. + * Decreases this `PoiBuffer`'s reference count. All resources will be disposed when the + * reference count reaches 0. + * @returns this `PoiBuffer`. */ - reset(): void { - if (this.boxBuffer === undefined) { - this.init(); - } - this.boxBuffer!.reset(); - } + decreaseRefCount(): PoiBuffer { + assert(this.m_refCount > 0); - /** - * Update the geometry with all the added boxes during the frame. - */ - update(): void { - if (this.boxBuffer === undefined) { - this.init(); + if (--this.m_refCount === 0) { + this.dispose(); } - this.boxBuffer!.updateBufferGeometry(); + return this; } - /** - * Update the info with the memory footprint caused by objects owned by the - * `PoiRenderBufferBatch`. - * - * @param info - The info object to increment with the values from this `PoiRenderBufferBatch`. - */ - updateMemoryUsage(info: MemoryUsage) { - if (this.boxBuffer !== undefined) { - this.boxBuffer.updateMemoryUsage(info); - } + private dispose() { + this.layer.storage.scene.remove(this.buffer.mesh); + this.buffer.dispose(); + this.m_onDispose(); } +} + +/** + * @internal + * + * The `PoiBatch` contains the geometry and the material for all POIs that share the same icon image + * ({@link @here/harp-datasource-protocol#ImageTexture}). + * + * There is a `PoiBatch` for every icon in a texture atlas, since the size of the icon in the atlas + * as well as the texture coordinates are specified in the `PoiBatch`. + */ +class PoiBatch { + // Enable trilinear filtering to reduce flickering due to distance scaling + static readonly trilinear: boolean = true; + + // Map of buffers and their corresponding canvas layers, with render order as key. + private readonly m_poiBuffers: Map; + + private readonly m_material: IconMaterial; /** - * Setup texture and material for the batch. + * Create the `PoiBatch`. + * + * @param mapView - The {@link MapView} instance. + * @param textCanvas - The {@link TextCanvas} used for rendering. + * @param imageItem - The icon that will have his material shared. + * @param m_onDispose - Callback executed when the `PoiBatch` is disposed. */ - private setup() { + constructor( + readonly mapView: MapView, + readonly textCanvas: TextCanvas, + readonly imageItem: ImageItem, + private readonly m_onDispose: () => void + ) { // Texture images should be generated with premultiplied alpha const premultipliedAlpha = true; @@ -127,11 +119,11 @@ class PoiRenderBufferBatch { THREE.UVMapping, undefined, undefined, - PoiRenderBufferBatch.trilinear ? THREE.LinearFilter : THREE.LinearFilter, - PoiRenderBufferBatch.trilinear ? THREE.LinearMipMapLinearFilter : THREE.LinearFilter, + PoiBatch.trilinear ? THREE.LinearFilter : THREE.LinearFilter, + PoiBatch.trilinear ? THREE.LinearMipMapLinearFilter : THREE.LinearFilter, THREE.RGBAFormat ); - if (PoiRenderBufferBatch.trilinear && this.imageItem.mipMaps) { + if (PoiBatch.trilinear && this.imageItem.mipMaps) { // Generate mipmaps for distance scaling of icon texture.mipmaps = this.imageItem.mipMaps; texture.image = texture.mipmaps[0]; @@ -145,30 +137,115 @@ class PoiRenderBufferBatch { map: texture }); - this.boxBuffer = new BoxBuffer(this.m_material, this.renderOrder); - - const mesh = this.boxBuffer.mesh; + this.m_poiBuffers = new Map(); + } + /** + * Gets the {@link PoiBuffer} for a given render order, creating it if necessary. + * @returns The {@link PoiBuffer}. + */ + getBuffer(renderOrder: number): PoiBuffer { + let poiBuffer = this.m_poiBuffers.get(renderOrder); + if (poiBuffer) { + return poiBuffer.increaseRefCount(); + } + const boxBuffer = new BoxBuffer(this.m_material, renderOrder); + const mesh = boxBuffer.mesh; mesh.frustumCulled = false; - this.scene.add(mesh); + const layer = this.textCanvas.addLayer(renderOrder); + layer.storage.scene.add(mesh); + + poiBuffer = new PoiBuffer(boxBuffer, layer, () => { + this.disposeBuffer(renderOrder); + }); + this.m_poiBuffers.set(renderOrder, poiBuffer); this.mapView.update(); + return poiBuffer.increaseRefCount(); + } + + /** + * Clean the `PoiBatch`, remove all icon boxes. Called before starting a new frame. + */ + reset(): void { + for (const poiBuffer of this.m_poiBuffers.values()) { + poiBuffer.buffer.reset(); + } + } + + /** + * Update the geometry with all the added boxes during the frame. + */ + update(): void { + for (const poiBuffer of this.m_poiBuffers.values()) { + poiBuffer.buffer.updateBufferGeometry(); + } + } + + /** + * Fill the picking results for the pixel with the given screen coordinate. If multiple + * boxes are found, the order of the results is unspecified. + * + * @param screenPosition - Screen coordinate of picking position. + * @param pickCallback - Callback to be called for every picked element. + * @param imageData - Image data to test if the pixel is transparent + */ + pickBoxes( + screenPosition: THREE.Vector2, + pickCallback: (pickData: any | undefined) => void, + imageData?: ImageBitmap | ImageData + ) { + for (const poiBuffer of this.m_poiBuffers.values()) { + poiBuffer.buffer.pickBoxes(screenPosition, pickCallback, imageData); + } + } + + /** + * Update the info with the memory footprint caused by objects owned by the `PoiBatch`. + * + * @param info - The info object to increment with the values from this `PoiBatch`. + */ + updateMemoryUsage(info: MemoryUsage) { + if (this.imageItem.imageData !== undefined) { + const imageBytes = this.imageItem.imageData.width * this.imageItem.imageData.height * 4; + info.heapSize += imageBytes; + info.gpuSize += imageBytes; + } + for (const poiBuffer of this.m_poiBuffers.values()) { + poiBuffer.buffer.updateMemoryUsage(info); + } + } + + private dispose() { + this.m_poiBuffers.clear(); + this.m_material.map.dispose(); + this.m_material.dispose(); + this.m_onDispose(); + } + + private disposeBuffer(renderOrder: number) { + assert(this.m_poiBuffers.size > 0); + + this.m_poiBuffers.delete(renderOrder); + if (this.m_poiBuffers.size === 0) { + this.dispose(); + } } } /** - * Contains all [[PoiRenderBufferBatch]]es. Selects (and initializes) the correct batch for a POI. + * @internal + * Contains all [[PoiBatch]]es. Selects (and initializes) the correct batch for a POI. */ -class PoiRenderBuffer { - readonly batches: PoiRenderBufferBatch[] = []; - private readonly m_batchMap: Map> = new Map(); +export class PoiBatchRegistry { + private readonly m_batchMap: Map = new Map(); /** - * Create the `PoiRenderBuffer`. + * Create the `PoiBatchRegistry`. * * @param mapView - The {@link MapView} to be rendered to. - * @param textCanvas - The [[TextCanvas]] to which scenes this `PoiRenderBuffer` + * @param textCanvas - The [[TextCanvas]] to which scenes this `PoiBatchRegistry` * adds geometry to. * The actual scene a {@link TextElement} is added to is specified by the renderOrder of the * {@link TextElement}. @@ -176,57 +253,33 @@ class PoiRenderBuffer { constructor(readonly mapView: MapView, readonly textCanvas: TextCanvas) {} /** - * Register the POI and prepare the [[PoiRenderBufferBatch]] for the POI at first usage. + * Register the POI and prepare the [[PoiBatch]] for the POI at first usage. * * @param poiInfo - Describes the POI icon. */ - registerPoi(poiInfo: PoiInfo): number { - const { imageItem, imageTexture, imageTextureName } = poiInfo; - - if ( - imageItem === undefined || - imageTextureName === undefined || - imageTexture === undefined - ) { + registerPoi(poiInfo: PoiInfo): PoiBuffer | undefined { + const { imageItem, imageTexture } = poiInfo; + + if (!imageItem) { // No image -> invisible -> ignore - return INVALID_RENDER_BATCH; + poiInfo.isValid = false; + return undefined; } - const renderOrder = poiInfo.renderOrder!; - // There is a batch for every ImageDefinition, which could be a texture atlas with many - // ImageTextures in it. - const batchKey = imageTexture.image; - let batchSet = this.m_batchMap.get(batchKey); - let mappedIndex: number | undefined; - - if (batchSet === undefined) { - batchSet = new Map(); - this.m_batchMap.set(batchKey, batchSet); + // ImageTextures in it. If the imageTexture is not set, imageTextureName has the actual + // image name. + const batchKey = imageTexture?.image ?? poiInfo.imageTextureName; + let batch = this.m_batchMap.get(batchKey); + + if (batch === undefined) { + batch = new PoiBatch(this.mapView, this.textCanvas, imageItem, () => { + this.deleteBatch(batchKey); + }); + this.m_batchMap.set(batchKey, batch); } - mappedIndex = batchSet.get(renderOrder); - if (mappedIndex !== undefined) { - return mappedIndex; - } - mappedIndex = this.batches.length; - - let layer = this.textCanvas.getLayer(renderOrder); - if (layer === undefined) { - this.textCanvas.addText("", tempPos, { layer: renderOrder }); - layer = this.textCanvas.getLayer(renderOrder); - } - - const bufferBatch = new PoiRenderBufferBatch( - this.mapView, - layer!.storage.scene, - imageItem, - renderOrder - ); - bufferBatch.init(); - batchSet.set(renderOrder, mappedIndex); - this.batches.push(bufferBatch); - return mappedIndex; + return batch.getBuffer(poiInfo.renderOrder!); } /** @@ -237,20 +290,15 @@ class PoiRenderBuffer { * @param viewDistance - Box's distance to camera. * @param opacity - Opacity of icon to allow fade in/out. */ - addPoi(poiInfo: PoiInfo, screenBox: Math2D.Box, viewDistance: number, opacity: number): number { - const poiRegistered = - poiInfo.poiRenderBatch !== undefined && poiInfo.poiRenderBatch !== INVALID_RENDER_BATCH; - const batchIndex = poiRegistered ? poiInfo.poiRenderBatch! : this.registerPoi(poiInfo); - if (batchIndex === INVALID_RENDER_BATCH) { - return INVALID_RENDER_BATCH; + addPoi(poiInfo: PoiInfo, screenBox: Math2D.Box, viewDistance: number, opacity: number) { + if (poiInfo.isValid === false) { + return; } - assert(batchIndex >= 0); - assert(batchIndex < this.batches.length); - assert(poiInfo.uvBox !== undefined); - - if (this.batches[batchIndex].boxBuffer === undefined) { - this.batches[batchIndex].init(); + const poiBuffer = poiInfo.buffer ?? this.registerPoi(poiInfo); + if (!poiBuffer) { + return; } + assert(poiInfo.uvBox !== undefined); let color: THREE.Color; if (poiInfo.iconBrightness !== undefined) { @@ -263,7 +311,7 @@ class PoiRenderBuffer { } else { color = neutralColor; } - this.batches[batchIndex].boxBuffer!.addBox( + poiBuffer.buffer.addBox( screenBox, poiInfo.uvBox!, color, @@ -271,40 +319,23 @@ class PoiRenderBuffer { viewDistance, poiInfo.textElement ); - - return batchIndex; } /** - * Retrieve the [[PoiRenderBufferBatch]] from the array at the specified index. May be invalid - * if the imageTexture could not be found - * - * @param index - Index into batch array. - */ - getBatch(index: number): PoiRenderBufferBatch | undefined { - if (index >= 0) { - assert(index < this.batches.length); - return this.batches[index]; - } - // may be invalid if the imageTexture could not be found - return undefined; - } - - /** - * Reset all batches, removing all content from the [[PoiRenderBufferBatch]]es. Called at the + * Reset all batches, removing all content from the [[PoiBatch]]es. Called at the * beginning of a frame before the POIs are placed. */ reset(): void { - for (const batch of this.batches) { + for (const batch of this.m_batchMap.values()) { batch.reset(); } } /** - * Update the geometry of all [[PoiRenderBufferBatch]]es. Called before rendering. + * Update the geometry of all [[PoiBatch]]es. Called before rendering. */ update(): void { - for (const batch of this.batches) { + for (const batch of this.m_batchMap.values()) { batch.update(); } } @@ -320,36 +351,30 @@ class PoiRenderBuffer { screenPosition: THREE.Vector2, pickCallback: (pickData: any | undefined) => void ) { - for (const batch of this.batches) { - if (batch.boxBuffer === undefined) { - batch.init(); - } - batch.boxBuffer!.pickBoxes(screenPosition, pickCallback, batch.imageItem.imageData); + for (const batch of this.m_batchMap.values()) { + batch.pickBoxes(screenPosition, pickCallback, batch.imageItem.imageData); } } /** - * Update the info with the memory footprint caused by objects owned by the `PoiRenderBuffer`. + * Update the info with the memory footprint caused by objects owned by the `PoiBatchRegistry`. * - * @param info - The info object to increment with the values from this `PoiRenderBuffer`. + * @param info - The info object to increment with the values from this `PoiBatchRegistry`. */ updateMemoryUsage(info: MemoryUsage) { - for (const batch of this.batches) { - if (batch.imageItem.imageData !== undefined) { - const imageBytes = - batch.imageItem.imageData.width * batch.imageItem.imageData.height * 4; - info.heapSize += imageBytes; - info.gpuSize += imageBytes; - } - if (batch.boxBuffer !== undefined) { - batch.boxBuffer.updateMemoryUsage(info); - } + for (const batch of this.m_batchMap.values()) { + batch.updateMemoryUsage(info); } } + + private deleteBatch(batchKey: string) { + this.m_batchMap.delete(batchKey); + } } /** - * Manage POI rendering. Uses a [[PoiRenderBuffer]] to actually create the geometry that is being + * @internal + * Manage POI rendering. Uses a [[PoiBatchRegistry]] to actually create the geometry that is being * rendered. */ export class PoiRenderer { @@ -371,8 +396,7 @@ export class PoiRenderer { env: Env, /* out */ screenBox: Math2D.Box = new Math2D.Box() ): Math2D.Box { - assert(poiInfo.poiRenderBatch !== undefined); - assert(poiInfo.poiRenderBatch !== INVALID_RENDER_BATCH); + assert(poiInfo.buffer !== undefined); const width = poiInfo.computedWidth! * scale; const height = poiInfo.computedHeight! * scale; @@ -397,7 +421,7 @@ export class PoiRenderer { private static readonly m_missingTextureName: Map = new Map(); // the render buffer containing all batches, one batch per texture/material. - private readonly m_renderBuffer: PoiRenderBuffer; + private readonly m_poiBatchRegistry: PoiBatchRegistry; // temporary variable to save allocations private readonly m_tempScreenBox = new Math2D.Box(); @@ -410,12 +434,12 @@ export class PoiRenderer { * the different layers of this [[TextCanvas]] based on renderOrder. */ constructor(readonly mapView: MapView, readonly textCanvas: TextCanvas) { - this.m_renderBuffer = new PoiRenderBuffer(mapView, textCanvas); + this.m_poiBatchRegistry = new PoiBatchRegistry(mapView, textCanvas); } /** - * Prepare the POI for rendering, and determine which `poiRenderBatch` should be used. If a - * `poiRenderBatch` is assigned, the POI is ready to be rendered. + * Prepare the POI for rendering, and determine which {@link PoiBuffer} should be used. If a + * {@link PoiBuffer} is assigned, the POI is ready to be rendered. * * @param pointLabel - TextElement with PoiInfo for rendering the POI icon. * @param env - TODO! The current zoomLevel level of {@link MapView} @@ -427,18 +451,18 @@ export class PoiRenderer { if (poiInfo === undefined) { return false; } - if (poiInfo.poiRenderBatch === undefined) { + if (poiInfo.buffer === undefined) { this.preparePoi(pointLabel, env); } - return poiInfo.poiRenderBatch !== undefined; + return poiInfo.buffer !== undefined; } /** - * Reset all batches, removing all content from the [[PoiRenderBuffer]]es. Called at the + * Reset all batches, removing all content from the [[PoiBatchRegistry]]. Called at the * beginning of a frame before the POIs are placed. */ reset(): void { - this.m_renderBuffer.reset(); + this.m_poiBatchRegistry.reset(); } /** @@ -463,8 +487,8 @@ export class PoiRenderer { allocateScreenSpace: boolean, opacity: number, env: Env - ): boolean { - assert(poiInfo.poiRenderBatch !== undefined); + ): void { + assert(poiInfo.buffer !== undefined); PoiRenderer.computeIconScreenBox(poiInfo, screenPosition, scale, env, this.m_tempScreenBox); @@ -473,17 +497,15 @@ export class PoiRenderer { } if (opacity > 0) { - this.m_renderBuffer.addPoi(poiInfo, this.m_tempScreenBox, viewDistance, opacity); - return true; + this.m_poiBatchRegistry.addPoi(poiInfo, this.m_tempScreenBox, viewDistance, opacity); } - return false; } /** - * Update the geometry of all [[PoiRenderBuffer]]es. Called before rendering. + * Update the geometry of all [[PoiBatch]]es. Called before rendering. */ update(): void { - this.m_renderBuffer.update(); + this.m_poiBatchRegistry.update(); } /** @@ -497,7 +519,7 @@ export class PoiRenderer { screenPosition: THREE.Vector2, pickCallback: (pickData: any | undefined) => void ) { - this.m_renderBuffer.pickTextElements(screenPosition, pickCallback); + this.m_poiBatchRegistry.pickTextElements(screenPosition, pickCallback); } /** @@ -506,12 +528,12 @@ export class PoiRenderer { * @param info - The info object to increment with the values from this `PoiRenderer`. */ getMemoryUsage(info: MemoryUsage) { - this.m_renderBuffer.updateMemoryUsage(info); + this.m_poiBatchRegistry.updateMemoryUsage(info); } /** - * Register the POI at the [[PoiRenderBuffer]] which may require some setup, for example loading - * of the actual image. + * Register the POI at the [[PoiBatchRegistry]] which may require some setup, for example + * loading of the actual image. */ private preparePoi(pointLabel: TextElement, env: Env): void { const poiInfo = pointLabel.poiInfo; @@ -519,7 +541,7 @@ export class PoiRenderer { return; } - if (poiInfo.poiRenderBatch !== undefined || poiInfo.isValid === false) { + if (poiInfo.buffer !== undefined || poiInfo.isValid === false) { // Already set up, nothing to be done here. return; } @@ -539,62 +561,64 @@ export class PoiRenderer { const imageTextureName = poiInfo.imageTextureName; const imageTexture = this.mapView.poiManager.getImageTexture(imageTextureName); - if (imageTexture === undefined) { - // Warn about a missing texture, but only once. - if (PoiRenderer.m_missingTextureName.get(imageTextureName) === undefined) { - PoiRenderer.m_missingTextureName.set(imageTextureName, true); - logger.error(`preparePoi: No imageTexture with name '${imageTextureName}' found`); - } - poiInfo.isValid = false; - return; - } + let imageItem: ImageItem; + let imageCache: MapViewImageCache; + if (imageTexture) { + const imageDefinition = imageTexture.image; - const imageDefinition = imageTexture.image; + const image = this.mapView.imageCache.findImageByName(imageDefinition); - // Check user image cache first. - let imageItem = this.mapView.userImageCache.findImageByName(imageDefinition); - let imageCache: MapViewImageCache | undefined; - if (imageItem === undefined) { - // Then check default image cache. - imageItem = this.mapView.imageCache.findImageByName(imageDefinition); - if (imageItem === undefined) { + if (!image) { logger.error(`init: No imageItem found with name '${imageDefinition}'`); poiInfo.isValid = false; return; - } else { - imageCache = this.mapView.imageCache; } + imageItem = image; + imageCache = this.mapView.imageCache; } else { + // No image texture found. Either this is a user image or it's missing. + const image = this.mapView.userImageCache.findImageByName(imageTextureName); + + if (!image) { + // Warn about a missing texture, but only once. + if (PoiRenderer.m_missingTextureName.get(imageTextureName) === undefined) { + PoiRenderer.m_missingTextureName.set(imageTextureName, true); + logger.error( + `preparePoi: No imageTexture with name '${imageTextureName}' found` + ); + } + poiInfo.isValid = false; + return; + } + imageItem = image; imageCache = this.mapView.userImageCache; } - if (!imageItem.loaded) { - if (imageItem.loadingPromise !== undefined) { - // already being loaded, will be rendered once available - return; - } - const imageUrl = imageItem.url; - const loading = imageCache.loadImage(imageItem); - if (loading instanceof Promise) { - loading - .then(loadedImageItem => { - if (loadedImageItem === undefined) { - logger.error(`preparePoi: Failed to load imageItem: '${imageUrl}`); - return; - } - this.setupPoiInfo(poiInfo, imageTexture, loadedImageItem, env); - }) - .catch(error => { - logger.error(`preparePoi: Failed to load imageItem: '${imageUrl}`, error); - poiInfo.isValid = false; - }); - return; - } else { - imageItem = loading; - } + if (imageItem.loaded) { + this.setupPoiInfo(poiInfo, imageItem, env, imageTexture); + return; } - this.setupPoiInfo(poiInfo, imageTexture, imageItem, env); + if (imageItem.loadingPromise) { + // already being loaded, will be rendered once available + return; + } + + const result = imageCache.loadImage(imageItem); + assert(result instanceof Promise); + const loadPromise = result as Promise; + loadPromise + .then(loadedImageItem => { + if (loadedImageItem === undefined) { + logger.error(`preparePoi: Failed to load imageItem: '${imageItem.url}`); + return; + } + this.setupPoiInfo(poiInfo, loadedImageItem, env, imageTexture); + }) + .catch(error => { + logger.error(`preparePoi: Failed to load imageItem: '${imageItem.url}`, error); + poiInfo.isValid = false; + }); } /** @@ -608,16 +632,14 @@ export class PoiRenderer { */ private setupPoiInfo( poiInfo: PoiInfo, - imageTexture: ImageTexture, imageItem: ImageItem, - env: Env + env: Env, + imageTexture?: ImageTexture ) { assert(poiInfo.uvBox === undefined); if (imageItem === undefined || imageItem.imageData === undefined) { logger.error("setupPoiInfo: No imageItem/imageData found"); - // invalid render batch number - poiInfo.poiRenderBatch = INVALID_RENDER_BATCH; poiInfo.isValid = false; return; } @@ -627,43 +649,26 @@ export class PoiRenderer { const imageWidth = imageItem.imageData.width; const imageHeight = imageItem.imageData.height; const paddedSize = MipMapGenerator.getPaddedSize(imageWidth, imageHeight); - const trilinearFiltering = PoiRenderBufferBatch.trilinear && imageItem.mipMaps; + const trilinearFiltering = PoiBatch.trilinear && imageItem.mipMaps; const paddedImageWidth = trilinearFiltering ? paddedSize.width : imageWidth; const paddedImageHeight = trilinearFiltering ? paddedSize.height : imageHeight; - const iconWidth = imageTexture.width !== undefined ? imageTexture.width : imageWidth; - const iconHeight = imageTexture.height !== undefined ? imageTexture.height : imageHeight; + const iconWidth = imageTexture?.width !== undefined ? imageTexture.width : imageWidth; + const iconHeight = imageTexture?.height !== undefined ? imageTexture.height : imageHeight; - let minS = 0; - let maxS = 1; - let minT = 0; - let maxT = 1; + const width = imageTexture?.width !== undefined ? imageTexture.width : imageWidth; + const height = imageTexture?.height !== undefined ? imageTexture.height : imageHeight; + const xOffset = imageTexture?.xOffset !== undefined ? imageTexture.xOffset : 0; + const yOffset = imageTexture?.yOffset !== undefined ? imageTexture.yOffset : 0; + + const minS = xOffset / paddedImageWidth; + const maxS = (xOffset + width) / paddedImageWidth; + const minT = yOffset / paddedImageHeight; + const maxT = (yOffset + height) / paddedImageHeight; let iconScaleH = technique.iconScale !== undefined ? technique.iconScale : 1; let iconScaleV = technique.iconScale !== undefined ? technique.iconScale : 1; - const width = imageTexture.width !== undefined ? imageTexture.width : imageWidth; - const height = imageTexture.height !== undefined ? imageTexture.height : imageHeight; - const xOffset = imageTexture.xOffset !== undefined ? imageTexture.xOffset : 0; - const yOffset = imageTexture.yOffset !== undefined ? imageTexture.yOffset : 0; - - minS = xOffset / paddedImageWidth; - maxS = (xOffset + width) / paddedImageWidth; - - const flipY = false; - if (flipY) { - minT = (imageHeight - yOffset) / paddedImageHeight; - maxT = (imageHeight - yOffset - height) / paddedImageHeight; - } else { - minT = yOffset / paddedImageHeight; - maxT = (yOffset + height) / paddedImageHeight; - } - - // minS += 0.5 / imageWidth; - // maxS += 0.5 / imageWidth; - // minT += 0.5 / imageHeight; - // maxT += 0.5 / imageHeight; - // By default, iconScaleV should be equal to iconScaleH, whatever is set in the style. const screenWidth = getPropertyValue(technique.screenWidth, env); if (screenWidth !== undefined && screenWidth !== null) { @@ -689,9 +694,7 @@ export class PoiRenderer { }; poiInfo.imageItem = imageItem; poiInfo.imageTexture = imageTexture; - poiInfo.poiRenderBatch = this.m_renderBuffer.registerPoi(poiInfo); + poiInfo.buffer = this.m_poiBatchRegistry.registerPoi(poiInfo); poiInfo.isValid = true; - - assert(poiInfo.poiRenderBatch !== undefined); } } diff --git a/@here/harp-mapview/lib/text/TextElement.ts b/@here/harp-mapview/lib/text/TextElement.ts index a042d1eb0e..8cebdb7860 100644 --- a/@here/harp-mapview/lib/text/TextElement.ts +++ b/@here/harp-mapview/lib/text/TextElement.ts @@ -25,6 +25,7 @@ import * as THREE from "three"; import { ImageItem } from "../image/Image"; import { PickResult } from "../PickHandler"; +import { PoiBuffer } from "../poi/PoiRenderer"; import { TextElementType } from "./TextElementType"; /** @@ -37,7 +38,8 @@ export interface PoiInfo { technique: PoiTechnique | LineMarkerTechnique; /** - * Name of the {@link @here/harp-datasource-protocol#ImageTexture}. + * Name of the {@link @here/harp-datasource-protocol#ImageTexture} or image in + * {@link @here/harp-mapview#userImageCache}. */ imageTextureName: string; @@ -158,7 +160,7 @@ export interface PoiInfo { * @hidden * Internal reference to a render batch, made up of all icons that use the same Material. */ - poiRenderBatch?: number; + buffer?: PoiBuffer; /** * @hidden @@ -193,7 +195,7 @@ export interface PoiInfo { * @internal */ export function poiIsRenderable(poiInfo: PoiInfo): boolean { - return poiInfo.poiRenderBatch !== undefined; + return poiInfo.buffer !== undefined; } export interface TextPickResult extends PickResult { @@ -529,4 +531,14 @@ export class TextElement { } } } + + /** + * Disposes of any allocated resources. + */ + dispose() { + const poiBuffer = this.poiInfo?.buffer; + if (poiBuffer) { + poiBuffer.decreaseRefCount(); + } + } } diff --git a/@here/harp-mapview/test/PlacementTest.ts b/@here/harp-mapview/test/PlacementTest.ts index 224e766752..cf20d5f047 100644 --- a/@here/harp-mapview/test/PlacementTest.ts +++ b/@here/harp-mapview/test/PlacementTest.ts @@ -37,6 +37,7 @@ import * as path from "path"; import * as sinon from "sinon"; import * as THREE from "three"; +import { PoiBuffer } from "../lib/poi/PoiRenderer"; import { ScreenCollisions } from "../lib/ScreenCollisions"; import { placeIcon, PlacementResult, placePointLabel } from "../lib/text/Placement"; import { RenderState } from "../lib/text/RenderState"; @@ -1287,7 +1288,7 @@ describe("Placement", function() { computedWidth: 32, computedHeight: 32, mayOverlap: false, - poiRenderBatch: 1, + buffer: {} as PoiBuffer, technique: {} }; @@ -1311,7 +1312,7 @@ describe("Placement", function() { computedWidth: 32, computedHeight: 32, mayOverlap: false, - poiRenderBatch: 1, + buffer: {} as PoiBuffer, technique: { iconYOffset: -16 } diff --git a/@here/harp-mapview/test/PoiBatchRegistryTest.ts b/@here/harp-mapview/test/PoiBatchRegistryTest.ts new file mode 100644 index 0000000000..659420221f --- /dev/null +++ b/@here/harp-mapview/test/PoiBatchRegistryTest.ts @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2020 HERE Europe B.V. + * Licensed under Apache 2.0, see full license in LICENSE + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IconMaterial } from "@here/harp-materials"; +import { TextCanvas } from "@here/harp-text-canvas"; +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as THREE from "three"; + +import { MapView } from "../lib/MapView"; +import { PoiBatchRegistry } from "../lib/poi/PoiRenderer"; +import { TextElement } from "../lib/text/TextElement"; +import { PoiInfoBuilder } from "./PoiInfoBuilder"; +import { stubFontCatalog } from "./stubFontCatalog"; + +describe("PoiBatchRegistry", () => { + const renderer = { capabilities: { isWebGL2: false } } as THREE.WebGLRenderer; + const mapView = { update: () => {}, renderer } as MapView; + let textCanvas: TextCanvas; + let registry: PoiBatchRegistry; + const textElement = {} as TextElement; + const sandbox = sinon.createSandbox(); + + beforeEach(() => { + textCanvas = new TextCanvas({ + renderer, + fontCatalog: stubFontCatalog(sandbox), + minGlyphCount: 1, + maxGlyphCount: 1 + }); + registry = new PoiBatchRegistry(mapView, textCanvas); + }); + describe("registerPoi", function() { + it("marks PoiInfo as invalid if it has no image item", () => { + const poiInfo = new PoiInfoBuilder().build(textElement); + const buffer = registry.registerPoi(poiInfo); + + expect(buffer).to.be.undefined; + expect((poiInfo as any).isValid).to.be.false; + }); + + it("uses PoiInfo's imageTexture's image by default as batch key", () => { + const poiInfo1 = new PoiInfoBuilder() + .withImageTexture({ name: "tex1", image: "image1" }) + .withImageItem() + .build(textElement); + + const buffer1 = registry.registerPoi(poiInfo1); + const buffer2 = registry.registerPoi( + new PoiInfoBuilder() + .withImageTexture({ name: "tex2", image: poiInfo1.imageTexture!.image }) + .withImageItem() + .build(textElement) + ); + + expect(buffer1).equals(buffer2).and.not.undefined; + }); + + it("uses PoiInfo's imageTextureName as batch key if imageTexture is undefined", () => { + const poiInfo1 = new PoiInfoBuilder() + .withImageTextureName("image1") + .withImageItem() + .build(textElement); + + const buffer1 = registry.registerPoi(poiInfo1); + const buffer2 = registry.registerPoi( + new PoiInfoBuilder() + .withImageTextureName(poiInfo1.imageTextureName) + .withImageItem() + .build(textElement) + ); + expect(buffer1).equals(buffer2).and.not.undefined; + }); + + it("POIs with same image but diff. render order share only material (and texture)", () => { + const poiInfo1 = new PoiInfoBuilder() + .withImageTextureName("image1") + .withImageItem() + .withRenderOrder(0) + .build(textElement); + + const buffer1 = registry.registerPoi(poiInfo1); + const buffer2 = registry.registerPoi( + new PoiInfoBuilder() + .withImageTextureName(poiInfo1.imageTextureName) + .withImageItem() + .withRenderOrder(1) + .build(textElement) + ); + expect(buffer1).not.undefined; + expect(buffer2).not.undefined; + expect(buffer1).not.equals(buffer2); + + const mesh1 = buffer1!.buffer.mesh; + const mesh2 = buffer2!.buffer.mesh; + + expect(buffer1!.buffer).not.equals(buffer2!.buffer); + expect(mesh1).not.equals(mesh2); + expect(mesh1.material).equals(mesh2.material); + }); + + it("POIs with same image and render order share buffer", () => { + const poiInfo1 = new PoiInfoBuilder() + .withImageTextureName("image1") + .withImageItem() + .build(textElement); + + const buffer1 = registry.registerPoi(poiInfo1); + const buffer2 = registry.registerPoi( + new PoiInfoBuilder() + .withImageTextureName(poiInfo1.imageTextureName) + .withImageItem() + .build(textElement) + ); + + expect(buffer1).equals(buffer2).and.not.undefined; + expect(buffer1!.buffer).equals(buffer2!.buffer); + }); + + it("POIs with different image do not share resources", () => { + const buffer1 = registry.registerPoi( + new PoiInfoBuilder() + .withImageTextureName("image1") + .withImageItem() + .build(textElement) + ); + const buffer2 = registry.registerPoi( + new PoiInfoBuilder() + .withImageTextureName("image2") + .withImageItem() + .build(textElement) + ); + expect(buffer1).not.undefined; + expect(buffer2).not.undefined; + expect(buffer1).not.equals(buffer2); + + expect(buffer1!.buffer).not.equals(buffer2!.buffer); + const mesh1 = buffer1!.buffer.mesh; + const mesh2 = buffer2!.buffer.mesh; + expect(mesh1).not.equals(mesh2); + expect(mesh1.material).not.equals(mesh2.material); + expect(mesh1.material).instanceOf(IconMaterial); + expect(mesh2.material).instanceOf(IconMaterial); + const material1 = mesh1.material as IconMaterial; + const material2 = mesh2.material as IconMaterial; + expect(material1.map).not.equals(material2.map); + }); + + it("resources are disposed when unused", () => { + const result = registry.registerPoi( + new PoiInfoBuilder().withImageItem().build(textElement) + ); + expect(result).not.undefined; + const poiBuffer = result!; + const material = poiBuffer.buffer.mesh.material as IconMaterial; + + const disposalSpies: sinon.SinonSpy[] = [ + sinon.spy(poiBuffer.buffer, "dispose"), + sinon.spy(poiBuffer.layer.storage.scene, "remove"), + sinon.spy(material, "dispose"), + sinon.spy(material.map, "dispose") + ]; + + poiBuffer.decreaseRefCount(); + + for (const spy of disposalSpies) { + expect(spy.calledOnce).is.true; + } + }); + }); +}); diff --git a/@here/harp-mapview/test/PoiInfoBuilder.ts b/@here/harp-mapview/test/PoiInfoBuilder.ts index 22280620f2..246f58cb8d 100644 --- a/@here/harp-mapview/test/PoiInfoBuilder.ts +++ b/@here/harp-mapview/test/PoiInfoBuilder.ts @@ -4,8 +4,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { LineMarkerTechnique, PoiTechnique } from "@here/harp-datasource-protocol"; +import { ImageTexture, LineMarkerTechnique, PoiTechnique } from "@here/harp-datasource-protocol"; +import { TextCanvasLayer } from "@here/harp-text-canvas"; +import * as THREE from "three"; +import { ImageItem } from "../lib/image/Image"; +import { BoxBuffer } from "../lib/poi/BoxBuffer"; +import { PoiBuffer } from "../lib/poi/PoiRenderer"; import { PoiInfo, TextElement } from "../lib/text/TextElement"; export class PoiInfoBuilder { @@ -29,6 +34,7 @@ export class PoiInfoBuilder { }; static readonly DEF_TECHNIQUE = PoiInfoBuilder.POI_TECHNIQUE; + static readonly DEF_IMAGE_TEXTURE_NAME = "dummy"; private readonly m_iconMinZl: number = PoiInfoBuilder.DEF_ICON_TEXT_MIN_ZL; private readonly m_iconMaxZl: number = PoiInfoBuilder.DEF_ICON_TEXT_MAX_ZL; @@ -43,6 +49,10 @@ export class PoiInfoBuilder { private readonly m_width: number = PoiInfoBuilder.DEF_WIDTH_HEIGHT; private readonly m_height: number = PoiInfoBuilder.DEF_WIDTH_HEIGHT; private m_technique: PoiTechnique | LineMarkerTechnique = PoiInfoBuilder.DEF_TECHNIQUE; + private m_imageTextureName: string = PoiInfoBuilder.DEF_IMAGE_TEXTURE_NAME; + private m_imageTexture?: ImageTexture; + private m_renderOrder: number = 0; + private m_imageItem?: ImageItem; withPoiTechnique(): PoiInfoBuilder { this.m_technique = { ...PoiInfoBuilder.POI_TECHNIQUE }; @@ -80,10 +90,40 @@ export class PoiInfoBuilder { return this; } + withImageTextureName(name: string): PoiInfoBuilder { + this.m_imageTextureName = name; + return this; + } + + withImageTexture(imageTexture: ImageTexture): PoiInfoBuilder { + this.m_imageTexture = imageTexture; + return this; + } + + withRenderOrder(renderOrder: number): PoiInfoBuilder { + this.m_renderOrder = renderOrder; + return this; + } + + withImageItem(): PoiInfoBuilder { + this.m_imageItem = { + url: "dummy", + imageData: { + height: this.m_height, + width: this.m_width, + data: new Uint8ClampedArray(this.m_height * this.m_width * 4) + }, + loaded: true + }; + return this; + } + build(textElement: TextElement): PoiInfo { return { technique: this.m_technique, - imageTextureName: "", + imageTextureName: this.m_imageTextureName, + imageTexture: this.m_imageTexture, + imageItem: this.m_imageItem, iconMinZoomLevel: this.m_iconMinZl, iconMaxZoomLevel: this.m_iconMaxZl, textMinZoomLevel: this.m_textMinZl, @@ -97,7 +137,12 @@ export class PoiInfoBuilder { computedWidth: this.m_width, computedHeight: this.m_height, textElement, - poiRenderBatch: 0 + renderOrder: this.m_renderOrder, + buffer: new PoiBuffer( + new BoxBuffer(new THREE.Material()), + {} as TextCanvasLayer, + () => {} + ) }; } } diff --git a/@here/harp-mapview/test/PoiRendererTest.ts b/@here/harp-mapview/test/PoiRendererTest.ts index 2262f93a96..c6e2e6f3b8 100644 --- a/@here/harp-mapview/test/PoiRendererTest.ts +++ b/@here/harp-mapview/test/PoiRendererTest.ts @@ -15,7 +15,7 @@ import { Math2D } from "@here/harp-utils"; import { expect } from "chai"; import * as THREE from "three"; -import { PoiRenderer } from "../lib/poi/PoiRenderer"; +import { PoiBuffer, PoiRenderer } from "../lib/poi/PoiRenderer"; describe("PoiRenderer", function() { describe("computeIconScreenBox", function() { @@ -24,7 +24,7 @@ describe("PoiRenderer", function() { computedWidth: 32, computedHeight: 32, mayOverlap: false, - poiRenderBatch: 1, + buffer: {} as PoiBuffer, technique: {} }; const env = new Env(); @@ -48,7 +48,7 @@ describe("PoiRenderer", function() { computedWidth: 32, computedHeight: 32, mayOverlap: false, - poiRenderBatch: 1, + buffer: {} as PoiBuffer, technique: { iconXOffset: 16, iconYOffset: -16 @@ -75,7 +75,7 @@ describe("PoiRenderer", function() { computedWidth: 32, computedHeight: 32, mayOverlap: false, - poiRenderBatch: 1, + buffer: {} as PoiBuffer, technique: { iconXOffset: 16, iconYOffset: -16 diff --git a/@here/harp-mapview/test/TileTest.ts b/@here/harp-mapview/test/TileTest.ts index 7b5774f659..4d1de5c861 100644 --- a/@here/harp-mapview/test/TileTest.ts +++ b/@here/harp-mapview/test/TileTest.ts @@ -205,6 +205,19 @@ describe("Tile", function() { expect(tile.textElementGroups.count()).to.equal(0); assert.isTrue(tile.textElementsChanged); }); + + it("dispose diposes of text elements", function() { + const tile = new Tile(stubDataSource, tileKey); + const textElement = createFakeTextElement(); + const disposeStub = sinon.stub(textElement, "dispose"); + tile.addTextElement(textElement); + expect(tile.textElementGroups.count()).to.equal(1); + + tile.dispose(); + + disposeStub.called; + expect(tile.textElementGroups.count()).to.equal(0); + }); }); it("setting skipping will cause willRender to return false", function() {