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
3 changes: 2 additions & 1 deletion examples/naip-mosaic/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -433,11 +433,12 @@ export default function App() {
colormapReversed: colormapChoice.reversed,
}),
signal,
maxCacheSize: 10,
});
},
// Smaller cache for MosaicLayer cache, since it caches full COGLayer
// instances
maxCacheSize: 5,
maxCacheSize: 0,
// @ts-expect-error beforeId is injected by @deck.gl/mapbox; LayerProps
// doesn't know about it.
beforeId: "boundary_country_outline",
Expand Down
40 changes: 39 additions & 1 deletion packages/deck.gl-raster/src/mesh-layer/mesh-layer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { DefaultProps, TextureSource } from "@deck.gl/core";
import type {
DefaultProps,
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";
Expand Down Expand Up @@ -55,6 +59,40 @@ export class MeshTextureLayer extends SimpleMeshLayer<
return [...imageModule, ...(renderPipeline ?? [])];
}

override updateState(params: UpdateParameters<this>): void {
// Ensure the SimpleMeshLayer rebuilds the model when the renderPipeline has
// changed.
if (this.hasRenderPipelineChanged(params)) {
// Setting extensionsChanged to true causes recompiling the shader
// https://github.com/visgl/deck.gl/blob/70adde2f1fcdf5e99195df81512e6d01ee7a5edc/modules/mesh-layers/src/simple-mesh-layer/simple-mesh-layer.ts#L284-L297
params.changeFlags.extensionsChanged = true;
}

super.updateState(params);
}

/** Returns true if the render pipeline has changed between the old and new props. */
private hasRenderPipelineChanged(params: UpdateParameters<this>): boolean {
const { oldProps, props: newProps } = params;
if (Boolean(oldProps.image) !== Boolean(newProps.image)) {
return true;
}

const oldPipeline = oldProps.renderPipeline ?? [];
const newPipeline = newProps.renderPipeline ?? [];
if (oldPipeline.length !== newPipeline.length) {
return true;
}

for (let i = 0; i < oldPipeline.length; i++) {
if (oldPipeline[i]?.module.name !== newPipeline[i]?.module.name) {
return true;
}
}

return false;
}

override getShaders() {
const upstreamShaders = super.getShaders();

Expand Down
39 changes: 18 additions & 21 deletions packages/deck.gl-raster/src/raster-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,19 @@ export class RasterLayer extends CompositeLayer<RasterLayerProps> {

declare state: {
reprojector?: RasterReprojector;
/**
* Mesh in the exact shape SimpleMeshLayer expects.
*
* It's important for this to be passed to MeshTextureLayer as a stable
* reference so `props.mesh` equality holds across renders. This avoids
* unnecessarily recreating the model.
*/
mesh?: {
positions: Float32Array;
indices: Uint32Array;
texCoords: Float32Array;
indices: { value: Uint32Array; size: number };
attributes: {
POSITION: { value: Float32Array; size: number };
TEXCOORD_0: { value: Float32Array; size: number };
};
};
};

Expand Down Expand Up @@ -199,9 +208,11 @@ export class RasterLayer extends CompositeLayer<RasterLayerProps> {
this.setState({
reprojector,
mesh: {
positions,
indices,
texCoords,
indices: { value: indices, size: 1 },
attributes: {
POSITION: { value: positions, size: 3 },
TEXCOORD_0: { value: texCoords, size: 2 },
},
},
});
}
Expand Down Expand Up @@ -275,8 +286,6 @@ export class RasterLayer extends CompositeLayer<RasterLayerProps> {
return null;
}

const { indices, positions, texCoords } = mesh;

const meshLayer = new MeshTextureLayer(
this.getSubLayerProps({
id: "raster",
Expand All @@ -285,19 +294,7 @@ export class RasterLayer extends CompositeLayer<RasterLayerProps> {
// Dummy data because we're only rendering _one_ instance of this mesh
// https://github.com/visgl/deck.gl/blob/93111b667b919148da06ff1918410cf66381904f/modules/geo-layers/src/terrain-layer/terrain-layer.ts#L241
data: [1],
mesh: {
indices: { value: indices, size: 1 },
attributes: {
POSITION: {
value: positions,
size: 3,
},
TEXCOORD_0: {
value: texCoords,
size: 2,
},
},
},
mesh,
// We're only rendering a single mesh, without instancing
// https://github.com/visgl/deck.gl/blob/93111b667b919148da06ff1918410cf66381904f/modules/geo-layers/src/terrain-layer/terrain-layer.ts#L244
_instanced: false,
Expand Down
86 changes: 86 additions & 0 deletions packages/deck.gl-raster/tests/raster-layer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { ReprojectionFns } from "@developmentseed/raster-reproject";
import { describe, expect, it, vi } from "vitest";

vi.mock("../src/mesh-layer/mesh-layer.js", () => ({
MeshTextureLayer: class CapturingMeshTextureLayer {
public props: Record<string, unknown>;
constructor(props: Record<string, unknown>) {
this.props = props;
}
},
}));

const { RasterLayer } = await import("../src/raster-layer.js");

const identity = (x: number, y: number): [number, number] => [x, y];
const REPROJECTION_FNS: ReprojectionFns = {
forwardTransform: identity,
inverseTransform: identity,
forwardReproject: identity,
inverseReproject: identity,
};

/**
* Build a {@link RasterLayer} ready for direct lifecycle invocation: bypasses
* deck.gl's `LayerManager` by replacing `state` and `setState` with a plain
* object + assign, and short-circuits `getSubLayerProps` so MeshTextureLayer
* receives the exact prop shape we hand it.
*/
function makeBareLayer() {
const layer = new RasterLayer({
id: "test",
width: 4,
height: 4,
reprojectionFns: REPROJECTION_FNS,
image: {} as never,
});
const internalState: Record<string, unknown> = {};
Object.assign(layer as object, { state: internalState });
Object.assign(layer as object, {
setState: (updates: Record<string, unknown>) =>
Object.assign(internalState, updates),
getSubLayerProps: <T>(props: T) => props,
});
return { layer, internalState };
}

type WrappedMesh = {
indices: { value: Uint32Array; size: number };
attributes: {
POSITION: { value: Float32Array; size: number };
TEXCOORD_0: { value: Float32Array; size: number };
};
};

describe("RasterLayer.state.mesh", () => {
it("stores the mesh in SimpleMeshLayer's expected wrapper shape", () => {
const { layer, internalState } = makeBareLayer();

(layer as unknown as { _generateMesh: () => void })._generateMesh();

const mesh = internalState.mesh as WrappedMesh;
expect(mesh.indices.value).toBeInstanceOf(Uint32Array);
expect(mesh.indices.size).toBe(1);
expect(mesh.attributes.POSITION.value).toBeInstanceOf(Float32Array);
expect(mesh.attributes.POSITION.size).toBe(3);
expect(mesh.attributes.TEXCOORD_0.value).toBeInstanceOf(Float32Array);
expect(mesh.attributes.TEXCOORD_0.size).toBe(2);
});

it("passes state.mesh to MeshTextureLayer by reference across renders", () => {
const { layer, internalState } = makeBareLayer();

(layer as unknown as { _generateMesh: () => void })._generateMesh();
const meshRef = internalState.mesh;

const renderOnce = layer as unknown as {
renderLayers: () => { props: { mesh: unknown } }[];
};
const layers1 = renderOnce.renderLayers();
const layers2 = renderOnce.renderLayers();

expect(layers1[0]!.props.mesh).toBe(meshRef);
expect(layers2[0]!.props.mesh).toBe(meshRef);
expect(layers1[0]!.props.mesh).toBe(layers2[0]!.props.mesh);
});
});