Skip to content

Commit dc8cca4

Browse files
authored
feat: eagerly upload chunk textures to the GPU (#698)
Summary: this pr changes when chunk textures are sent to the gpu. instead of uploading a chunk lazily the first time it is drawn, each chunk's texture is now uploaded as soon as its data finishes loading and that gpu texture is owned centrally for the chunk's whole lifetime rather than by individual layers. once a chunk is on the gpu its cpu copy is released to save memory, and its texture is freed when the chunk is no longer needed. the image, label, and volume layers now share these textures instead of each building and tracking their own. there's a minor temporary regression: because a chunk's texture is freed the moment it is no longer needed scrubbing through time can briefly show gaps where the previous frame used to stay on screen while the next one loaded. that smoothness returns once eviction is deferred under a budget, keeping recently used textures resident. this does not affect volume rendering.
1 parent 5380ddd commit dc8cca4

18 files changed

Lines changed: 159 additions & 196 deletions

src/core/renderable_object.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,19 @@ export abstract class RenderableObject extends Node {
2020
private cullFaceMode_: CullingMode = "none";
2121

2222
public setTexture(index: number, texture: Texture) {
23-
const oldTexture = this.textures_[index];
24-
if (oldTexture !== undefined) {
25-
this.staleTextures_.push(oldTexture);
26-
}
2723
this.textures_[index] = texture;
2824
}
2925

26+
protected clearTextures() {
27+
this.textures_.length = 0;
28+
}
29+
30+
protected markStaleTexture(texture: Texture | undefined) {
31+
if (texture !== undefined) {
32+
this.staleTextures_.push(texture);
33+
}
34+
}
35+
3036
public popStaleTextures() {
3137
const stale = this.staleTextures_;
3238
this.staleTextures_ = [];

src/data/chunk.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { TextureUnpackRowAlignment } from "../objects/textures/texture";
1+
import {
2+
type Texture,
3+
TextureUnpackRowAlignment,
4+
} from "../objects/textures/texture";
25
import { Logger } from "../utilities/logger";
36

47
const chunkDataTypes = [
@@ -34,6 +37,7 @@ export type ChunkViewState = {
3437

3538
export type Chunk = {
3639
data?: ChunkData;
40+
texture?: Texture;
3741
state: "unloaded" | "queued" | "loading" | "loaded";
3842
lod: number;
3943
shape: {

src/data/chunk_manager.ts

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,34 @@
1-
import { ChunkSource } from "./chunk";
2-
import { ChunkQueue } from "./chunk_queue";
3-
import { chunkMemoryStats } from "./chunk_memory";
1+
import { Chunk, ChunkSource } from "./chunk";
2+
import { ChunkQueue, comparePriority } from "./chunk_queue";
3+
import { chunkMemoryStats, clearChunkData } from "./chunk_memory";
4+
import { ChunkStore } from "./chunk_store";
5+
import { ChunkStoreView } from "./chunk_store_view";
6+
import { ImageSourcePolicy } from "../core/image_source_policy";
7+
import { Texture } from "../objects/textures/texture";
8+
import { Texture3D } from "../objects/textures/texture_3d";
49

510
export type QueueStats = {
611
pending: number;
712
running: number;
813
};
9-
import { ChunkStore } from "./chunk_store";
10-
import { ChunkStoreView } from "./chunk_store_view";
11-
import { ImageSourcePolicy } from "../core/image_source_policy";
14+
15+
const MAX_UPLOADS_PER_STORE_PER_UPDATE = 4;
1216

1317
export class ChunkManager {
1418
private readonly stores_: { source: ChunkSource; store: ChunkStore }[] = [];
1519
private readonly queue_ = new ChunkQueue();
1620

21+
private readonly uploadTexture_?: (texture: Texture) => void;
22+
private readonly disposeTexture_?: (texture: Texture) => void;
23+
24+
constructor(
25+
uploadTexture?: (texture: Texture) => void,
26+
disposeTexture?: (texture: Texture) => void
27+
) {
28+
this.uploadTexture_ = uploadTexture;
29+
this.disposeTexture_ = disposeTexture;
30+
}
31+
1732
public get queueStats(): QueueStats {
1833
return {
1934
pending: this.queue_.pendingCount,
@@ -44,12 +59,16 @@ export class ChunkManager {
4459
for (const chunk of updatedChunks) {
4560
if (chunk.priority === null) {
4661
this.queue_.cancel(chunk);
62+
this.disposeChunkTexture(chunk);
63+
clearChunkData(chunk);
4764
} else if (chunk.state === "queued") {
4865
this.queue_.enqueue(chunk, (signal) =>
4966
source.loader.loadChunkData(chunk, signal)
5067
);
5168
}
5269
}
70+
71+
this.uploadLoadedChunks(updatedChunks);
5372
}
5473

5574
this.queue_.flush();
@@ -60,4 +79,38 @@ export class ChunkManager {
6079
}
6180
}
6281
}
82+
83+
private uploadLoadedChunks(chunks: Set<Chunk>) {
84+
if (!this.uploadTexture_) return;
85+
86+
const pending: Chunk[] = [];
87+
88+
for (const chunk of chunks) {
89+
if (chunk.state === "loaded" && chunk.texture === undefined) {
90+
pending.push(chunk);
91+
}
92+
}
93+
94+
if (pending.length === 0) return;
95+
96+
pending.sort(comparePriority);
97+
98+
const limit = Math.min(pending.length, MAX_UPLOADS_PER_STORE_PER_UPDATE);
99+
100+
for (let i = 0; i < limit; i++) {
101+
const chunk = pending[i];
102+
const texture = Texture3D.createWithChunk(chunk);
103+
this.uploadTexture_(texture);
104+
chunk.texture = texture;
105+
clearChunkData(chunk);
106+
}
107+
}
108+
109+
private disposeChunkTexture(chunk: Chunk) {
110+
if (!this.disposeTexture_) return;
111+
112+
if (chunk.texture === undefined) return;
113+
this.disposeTexture_(chunk.texture);
114+
chunk.texture = undefined;
115+
}
63116
}

src/data/chunk_memory.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export function clearChunkData(chunk: Chunk): void {
2626
cpuChunkCount -= 1;
2727
}
2828
chunk.data = undefined;
29+
chunk.texture?.releaseCpuData();
2930
}
3031

3132
export function chunkMemoryStats() {

src/data/chunk_queue.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ type LoaderFn = (signal: AbortSignal) => Promise<void>;
77

88
type PendingItem = { chunk: Chunk; fn: LoaderFn };
99

10+
export function comparePriority(a: Chunk, b: Chunk) {
11+
const priorityA = a.priority ?? Number.MAX_SAFE_INTEGER;
12+
const priorityB = b.priority ?? Number.MAX_SAFE_INTEGER;
13+
14+
if (priorityA === priorityB) {
15+
return (
16+
(a.orderKey ?? Number.MAX_SAFE_INTEGER) -
17+
(b.orderKey ?? Number.MAX_SAFE_INTEGER)
18+
);
19+
}
20+
21+
return priorityA - priorityB;
22+
}
23+
1024
export class ChunkQueue {
1125
private readonly maxConcurrent_: number;
1226
private readonly pending_: PendingItem[] = [];
@@ -61,19 +75,7 @@ export class ChunkQueue {
6175
return;
6276
}
6377

64-
this.pending_.sort((a, b) => {
65-
const priorityA = a.chunk.priority ?? Number.MAX_SAFE_INTEGER;
66-
const priorityB = b.chunk.priority ?? Number.MAX_SAFE_INTEGER;
67-
68-
if (priorityA === priorityB) {
69-
return (
70-
(a.chunk.orderKey ?? Number.MAX_SAFE_INTEGER) -
71-
(b.chunk.orderKey ?? Number.MAX_SAFE_INTEGER)
72-
);
73-
}
74-
75-
return priorityA - priorityB;
76-
});
78+
this.pending_.sort((a, b) => comparePriority(a.chunk, b.chunk));
7779

7880
while (
7981
this.running_.size < this.maxConcurrent_ &&

src/data/chunk_store.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Chunk, SourceDimensionMap } from "./chunk";
2-
import { clearChunkData } from "./chunk_memory";
32
import { almostEqual } from "../utilities/almost_equal";
43
import { Logger } from "../utilities/logger";
54
import { ChunkStoreView } from "./chunk_store_view";
@@ -218,7 +217,6 @@ export class ChunkStore {
218217
);
219218
}
220219

221-
clearChunkData(chunk);
222220
chunk.state = "unloaded";
223221
chunk.orderKey = null;
224222
Logger.debug(

src/data/chunk_store_view.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ export class ChunkStoreView {
7575
const lowResChunks: Chunk[] = [];
7676

7777
for (const [chunk, state] of this.chunkViewStates_) {
78-
if (!state.visible || chunk.state !== "loaded") continue;
78+
if (!state.visible || chunk.state !== "loaded" || !chunk.texture)
79+
continue;
7980
if (chunk.lod === currentLOD) {
8081
currentLODChunks.push(chunk);
8182
} else if (chunk.lod === fallbackLOD && currentLOD !== fallbackLOD) {
@@ -113,7 +114,7 @@ export class ChunkStoreView {
113114
"ChunkStoreView",
114115
"updateChunkViewStates called with no chunks initialized"
115116
);
116-
this.chunkViewStates_.clear();
117+
this.chunkViewStates_.forEach(resetChunkViewState);
117118
return;
118119
}
119120

@@ -202,7 +203,7 @@ export class ChunkStoreView {
202203
"ChunkStoreView",
203204
"updateChunksForVolume called with no chunks initialized"
204205
);
205-
this.chunkViewStates_.clear();
206+
this.chunkViewStates_.forEach(resetChunkViewState);
206207
return;
207208
}
208209

src/idetik.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,10 @@ export class Idetik {
113113
this.canvas = params.canvas;
114114

115115
this.renderer_ = renderer ?? new WebGLRenderer(this.canvas);
116-
this.chunkManager_ = new ChunkManager();
116+
this.chunkManager_ = new ChunkManager(
117+
(texture) => this.renderer_.uploadTexture(texture),
118+
(texture) => this.renderer_.disposeTexture(texture)
119+
);
117120
this.context_ = {
118121
chunkManager: this.chunkManager_,
119122
};

src/layers/image_layer.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ import {
1111
validateChannelPropsCount,
1212
} from "../core/channel";
1313
import { ImageRenderable } from "../objects/renderable/image_renderable";
14-
import { Texture3D } from "../objects/textures/texture_3d";
1514
import { Color } from "../math/color";
1615
import { EventContext } from "../core/event_dispatcher";
1716
import { vec2, vec3 } from "gl-matrix";
1817
import { handlePointPickingEvent, PointPickingResult } from "./point_picking";
1918
import { clamp } from "../utilities/clamp";
2019
import { RenderablePool } from "../utilities/renderable_pool";
20+
import { Texture } from "../objects/textures/texture";
2121

2222
export type ImageLayerProps = LayerOptions & {
2323
source: ChunkSource;
@@ -128,8 +128,13 @@ export class ImageLayer extends Layer implements ChannelsEnabled {
128128
if (!this.chunkStoreView_) return;
129129
if (this.state !== "ready") this.setState("ready");
130130

131+
const visibleChunksResident = Array.from(this.visibleChunks_.keys()).every(
132+
(chunk) => chunk.texture !== undefined
133+
);
134+
131135
if (
132136
this.visibleChunks_.size > 0 &&
137+
visibleChunksResident &&
133138
!this.chunkStoreView_.allVisibleFallbackLODLoaded() &&
134139
!this.isPresentationStale()
135140
) {
@@ -147,8 +152,7 @@ export class ImageLayer extends Layer implements ChannelsEnabled {
147152

148153
this.clearObjects();
149154
for (const chunk of orderedByLOD) {
150-
if (chunk.state !== "loaded") continue;
151-
const image = this.getImageForChunk(chunk);
155+
const image = this.getImageForChunk(chunk, chunk.texture!);
152156
this.visibleChunks_.set(chunk, image);
153157
this.addObject(image);
154158
}
@@ -209,35 +213,32 @@ export class ImageLayer extends Layer implements ChannelsEnabled {
209213
}
210214
}
211215

212-
private getImageForChunk(chunk: Chunk) {
216+
private getImageForChunk(chunk: Chunk, texture: Texture) {
213217
const existing = this.visibleChunks_.get(chunk);
214218
if (existing) return existing;
215219

216220
const pooled = this.pool_.acquire(poolKeyForImageRenderable(chunk));
217221
if (pooled) {
218-
const texture = pooled.textures[0] as Texture3D;
219-
texture.updateWithChunk(chunk);
220-
222+
pooled.setTexture(0, texture);
221223
pooled.zTexCoord = this.zTexCoordForChunk(chunk);
222224
pooled.setChannelProps(this.getChannelPropsForChunk(chunk));
223225
this.updateImageChunk(pooled, chunk);
224-
225226
return pooled;
226227
}
227228

228-
return this.createImage(chunk);
229+
return this.createImage(chunk, texture);
229230
}
230231

231232
private getChannelPropsForChunk(chunk: Chunk): ChannelProps[] {
232233
if (!this.channelProps_) return [{}];
233234
return [this.channelProps_[chunk.chunkIndex.c] ?? {}];
234235
}
235236

236-
private createImage(chunk: Chunk) {
237+
private createImage(chunk: Chunk, texture: Texture) {
237238
const image = new ImageRenderable(
238239
chunk.shape.x,
239240
chunk.shape.y,
240-
Texture3D.createWithChunk(chunk),
241+
texture,
241242
this.getChannelPropsForChunk(chunk)
242243
);
243244
image.zTexCoord = this.zTexCoordForChunk(chunk);

src/layers/label_layer.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
LabelColorMap,
1111
LabelColorMapProps,
1212
} from "../objects/renderable/label_color_map";
13-
import { Texture3D } from "../objects/textures/texture_3d";
13+
import { Texture } from "../objects/textures/texture";
1414
import { EventContext } from "../core/event_dispatcher";
1515
import { vec2, vec3 } from "gl-matrix";
1616
import { handlePointPickingEvent, PointPickingResult } from "./point_picking";
@@ -114,8 +114,13 @@ export class LabelLayer extends Layer {
114114
if (!this.chunkStoreView_) return;
115115
if (this.state !== "ready") this.setState("ready");
116116

117+
const visibleChunksResident = Array.from(this.visibleChunks_.keys()).every(
118+
(chunk) => chunk.texture !== undefined
119+
);
120+
117121
if (
118122
this.visibleChunks_.size > 0 &&
123+
visibleChunksResident &&
119124
!this.chunkStoreView_.allVisibleFallbackLODLoaded() &&
120125
!this.isPresentationStale()
121126
) {
@@ -133,8 +138,7 @@ export class LabelLayer extends Layer {
133138

134139
this.clearObjects();
135140
for (const chunk of orderedByLOD) {
136-
if (chunk.state !== "loaded") continue;
137-
const label = this.getLabelForChunk(chunk);
141+
const label = this.getLabelForChunk(chunk, chunk.texture!);
138142
this.visibleChunks_.set(chunk, label);
139143
this.addObject(label);
140144
}
@@ -248,15 +252,13 @@ export class LabelLayer extends Layer {
248252
return (await label.textures[0].readTexel?.(x, y, z)) ?? null;
249253
}
250254

251-
private getLabelForChunk(chunk: Chunk) {
255+
private getLabelForChunk(chunk: Chunk, texture: Texture) {
252256
const existing = this.visibleChunks_.get(chunk);
253257
if (existing) return existing;
254258

255259
const pooled = this.pool_.acquire(poolKeyForImageRenderable(chunk));
256260
if (pooled) {
257-
const texture = pooled.textures[0] as Texture3D;
258-
texture.updateWithChunk(chunk);
259-
261+
pooled.setTexture(0, texture);
260262
pooled.zTexCoord = this.zTexCoordForChunk(chunk);
261263
pooled.setColorMap(this.colorMap_);
262264
pooled.setSelectedValue(this.selectedValue_);
@@ -265,14 +267,14 @@ export class LabelLayer extends Layer {
265267
return pooled;
266268
}
267269

268-
return this.createLabel(chunk);
270+
return this.createLabel(chunk, texture);
269271
}
270272

271-
private createLabel(chunk: Chunk) {
273+
private createLabel(chunk: Chunk, texture: Texture) {
272274
const label = new LabelImageRenderable({
273275
width: chunk.shape.x,
274276
height: chunk.shape.y,
275-
imageData: Texture3D.createWithChunk(chunk),
277+
imageData: texture,
276278
colorMap: this.colorMap_,
277279
outlineSelected: this.outlineSelected_,
278280
selectedValue: this.selectedValue_,

0 commit comments

Comments
 (0)