Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion packages/deck.gl-raster/src/mesh-layer/mesh-layer.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type {
DefaultProps,
LayerContext,
TextureSource,
UpdateParameters,
} from "@deck.gl/core";
import type { SimpleMeshLayerProps } from "@deck.gl/mesh-layers";
import { SimpleMeshLayer } from "@deck.gl/mesh-layers";
import type { Texture } from "@luma.gl/core";
import type { Texture, VertexArray } from "@luma.gl/core";
import type { ShaderModule } from "@luma.gl/shadertools";
import { CreateTexture } from "../gpu-modules/create-texture.js";
import type { RasterModule } from "../gpu-modules/types.js";
Expand Down Expand Up @@ -60,6 +61,19 @@ const defaultProps: DefaultProps<
getPosition: { type: "accessor", value: [0, 0, 0] },
};

/**
* Destroys the vertex array of a model that has been destroyed.
*
* luma.gl's `Model.destroy()` doesn't destroy the model's `vertexArray` (as of
* `@luma.gl/engine` 9.4.2), which leaks one WebGL vertex array object per
* model. Remove this once luma.gl destroys it itself.
*/
function destroyVertexArray(model: { vertexArray: VertexArray }): void {
if (!model.vertexArray.destroyed) {
model.vertexArray.destroy();
}
}

/**
* A specialized raster-rendering layer, spiritually based on deck.gl's
* `SimpleMeshLayer` but with a narrower purpose: it draws **one** texture-mapped
Expand Down Expand Up @@ -115,6 +129,10 @@ export class MeshTextureLayer extends SimpleMeshLayer<
return [...imageModule, ...(renderPipeline ?? [])];
}

/**
* Rebuilds the model when the render pipeline changes, and destroys the
* vertex array of any model that {@link SimpleMeshLayer} replaces.
*/
override updateState(params: UpdateParameters<this>): void {
// Ensure the SimpleMeshLayer rebuilds the model when the renderPipeline has
// changed.
Expand All @@ -124,7 +142,21 @@ export class MeshTextureLayer extends SimpleMeshLayer<
params.changeFlags.extensionsChanged = true;
}

const oldModel = this.state.model;
super.updateState(params);
// SimpleMeshLayer destroys the model it replaces
if (oldModel && oldModel !== this.state.model) {
destroyVertexArray(oldModel);
}
}

/** Destroys this layer's models along with their vertex arrays. */
override finalizeState(context: LayerContext): void {
const models = this.getModels();
super.finalizeState(context);
for (const model of models) {
destroyVertexArray(model);
}
}

/** Returns true if the render pipeline has changed between the old and new props. */
Expand Down
116 changes: 116 additions & 0 deletions packages/deck.gl-raster/tests/mesh-layer/mesh-layer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { LayerManager } from "@deck.gl/core";
import type { Device } from "@luma.gl/core";
import type { ShaderModule } from "@luma.gl/shadertools";
import { describe, expect, it, vi } from "vitest";
import type { MeshTextureLayerProps } from "../../src/mesh-layer/mesh-layer.js";
import { MeshTextureLayer } from "../../src/mesh-layer/mesh-layer.js";

/** A luma.gl `VertexArray` stand-in that records being destroyed. */
function makeVertexArray() {
const vertexArray = {
destroyed: false,
destroy: vi.fn(() => {
vertexArray.destroyed = true;
}),
};
return vertexArray;
}

/**
* A luma.gl `Model` stand-in. Like luma.gl 9.4.2's, its `destroy()` leaves its
* vertex array alive.
*/
function makeModel() {
return {
vertexArray: makeVertexArray(),
destroy: () => {},
// Called by deck.gl as it updates the layer
setTopology: () => {},
setAttributes: () => {},
setConstantAttributes: () => {},
shaderInputs: { setProps: () => {} },
};
}

type FakeModel = ReturnType<typeof makeModel>;

/** {@link MeshTextureLayer} with stand-in models, which need no GPU. */
class FakeModelMeshTextureLayer extends MeshTextureLayer {
static override layerName = "FakeModelMeshTextureLayer";

protected override getModel(): any {
return makeModel();
}
}

const MESH = {
attributes: { positions: { value: new Float32Array(9), size: 3 } },
};

/**
* Render {@link MeshTextureLayer} through deck.gl's {@link LayerManager}. Each
* call of `render` renders a new layer instance with the given props, as apps
* do on every render, and returns the layer's model. `remove` renders no
* layers.
*/
function makeRenderer() {
// The GPU resources deck.gl creates for the layer besides its model
const device = {
createTexture: () => ({ delete: () => {} }),
createBuffer: () => ({ write: () => {}, delete: () => {} }),
} as unknown as Device;
const layerManager = new LayerManager(device, {});
// Fail the test on layer lifecycle errors, which deck.gl catches
layerManager.setProps({
onError: (error: Error) => {
throw error;
},
});

const render = (
props: Partial<Pick<MeshTextureLayerProps, "mesh" | "renderPipeline">> = {},
): FakeModel => {
const layer = new FakeModelMeshTextureLayer({
id: "mesh",
mesh: MESH,
renderPipeline: [],
...props,
});
layerManager.setLayers([layer]);
return layer.getModels()[0] as unknown as FakeModel;
};
const remove = () => layerManager.setLayers([]);
return { render, remove };
}

describe("MeshTextureLayer vertex arrays", () => {
it("destroys its model's vertex array when the layer is removed", () => {
const { render, remove } = makeRenderer();
const model = render();
remove();
expect(model.vertexArray.destroyed).toBe(true);
});

it.each([
["the mesh changes", { mesh: { ...MESH } }],
[
"the render pipeline changes",
{ renderPipeline: [{ module: { name: "filter" } as ShaderModule }] },
],
])("destroys the old model's vertex array when %s", (_, newProps) => {
const { render } = makeRenderer();
const oldModel = render();
const newModel = render(newProps);
expect(oldModel.vertexArray.destroyed).toBe(true);
expect(newModel.vertexArray.destroyed).toBe(false);
});

it("destroys a vertex array only once when Model.destroy() also destroys it", () => {
const { render, remove } = makeRenderer();
const model = render();
// As luma.gl's Model.destroy() will once it destroys its vertex array
model.destroy = () => model.vertexArray.destroy();
remove();
expect(model.vertexArray.destroy).toHaveBeenCalledTimes(1);
});
});
Loading