Skip to content

Commit ff76f0d

Browse files
committed
feat(arrow): integrate color conversion
1 parent c907cd2 commit ff76f0d

15 files changed

Lines changed: 491 additions & 286 deletions

File tree

modules/arrow-layers/src/layers/arrow-layer-input.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@
33
// Copyright (c) vis.gl contributors
44

55
import {
6+
canConvertColors,
7+
convertArrowColors,
8+
convertColors,
69
getArrowPaths,
710
getArrowRecordBatchAsyncIterator,
11+
readArrowGPUVectorAsync,
12+
type ArrowColorType,
813
type ArrowRecordBatchSource
914
} from '@luma.gl/arrow';
15+
import type {Device} from '@luma.gl/core';
1016
import {GPUVector, type GPUVectorFormat} from '@luma.gl/tables';
1117
import {RecordBatch, Table, type DataType, type Vector} from 'apache-arrow';
1218

@@ -56,6 +62,59 @@ export function assertArrowLayerGPUVector(
5662
}
5763
}
5864

65+
/** Validates a caller-owned color vector that is normalized already or convertible by luma.gl. */
66+
export function assertArrowLayerColorGPUVector(
67+
ownerName: string,
68+
vector: GPUVector,
69+
expectedLength?: number
70+
): void {
71+
if (vector.format === 'vertex-list<unorm8x4>') {
72+
assertArrowLayerGPUVector(ownerName, vector, ['vertex-list<unorm8x4>'], expectedLength);
73+
return;
74+
}
75+
if (!canConvertColors(vector)) {
76+
throw new Error(
77+
`${ownerName} GPUVector.format "${vector.format ?? 'undefined'}" must be a convertible fixed-width RGB/RGBA format or vertex-list<unorm8x4>`
78+
);
79+
}
80+
assertArrowLayerGPUVector(ownerName, vector, [vector.format!], expectedLength);
81+
}
82+
83+
/** Returns normalized RGBA8 colors, preserving caller ownership when conversion is unnecessary. */
84+
export async function convertArrowLayerColorGPUVector(
85+
device: Device,
86+
vector: GPUVector,
87+
name: string
88+
): Promise<{
89+
vector: GPUVector<'unorm8x4' | 'vertex-list<unorm8x4>'>;
90+
converted: boolean;
91+
}> {
92+
if (vector.format === 'unorm8x4' || vector.format === 'vertex-list<unorm8x4>') {
93+
return {
94+
vector: vector as GPUVector<'unorm8x4' | 'vertex-list<unorm8x4>'>,
95+
converted: false
96+
};
97+
}
98+
return {vector: await convertColors(device, vector, {name}), converted: true};
99+
}
100+
101+
/** Normalizes one supported fixed-width Arrow RGB/RGBA vector to Uint8 RGBA rows. */
102+
export async function convertArrowLayerColorVector(
103+
device: Device,
104+
vector: Vector,
105+
name: string
106+
): Promise<Vector> {
107+
if (!canConvertColors(vector)) {
108+
throw new Error(`Arrow color vector type ${vector.type} is not convertible to Uint8 RGBA`);
109+
}
110+
const converted = await convertArrowColors(device, vector as Vector<ArrowColorType>, {name});
111+
try {
112+
return await readArrowGPUVectorAsync(converted);
113+
} finally {
114+
converted.destroy();
115+
}
116+
}
117+
59118
function hasNullRows(nullBitmap: Uint8Array, length: number): boolean {
60119
for (let rowIndex = 0; rowIndex < length; rowIndex++) {
61120
if ((nullBitmap[rowIndex >> 3] & (1 << (rowIndex & 7))) === 0) {

modules/arrow-layers/src/layers/arrow-path-layer.ts

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
getArrowRecordBatchAsyncIterator,
1818
readArrowGPUVectorAsync,
1919
resolveArrowPathSourceVectors,
20+
type ArrowColorType,
2021
type ArrowPathSourceVectors,
2122
type ArrowPathSourceVectorSelectors,
2223
type ArrowRecordBatchSource,
@@ -35,7 +36,7 @@ import {
3536
GPUVector,
3637
PathAttributeModel,
3738
PathStorageModel,
38-
type VertexList
39+
type GPUVectorFormat
3940
} from '@luma.gl/tables';
4041
import {
4142
DataType,
@@ -57,7 +58,10 @@ import {
5758
type ArrowLayerPickingInfo
5859
} from './arrow-layer-types';
5960
import {
61+
assertArrowLayerColorGPUVector,
6062
assertArrowLayerGPUVector,
63+
convertArrowLayerColorGPUVector,
64+
convertArrowLayerColorVector,
6165
getArrowLayerInputNullValue,
6266
getArrowLayerInputSource,
6367
hasArrowLayerColumn,
@@ -69,11 +73,10 @@ import {
6973
} from './arrow-layer-input';
7074

7175
type ArrowPathColor = [number, number, number, number];
72-
type ArrowPathRowColorType = FixedSizeList<Uint8>;
73-
type ArrowPathVertexColorType = List<ArrowPathRowColorType>;
76+
type ArrowPathRowColorType = ArrowColorType;
77+
type ArrowPathVertexColorType = List<FixedSizeList<Uint8>>;
7478
type ArrowPathColorSource = ArrowLayerColumnSource<
75-
ArrowPathRowColorType | ArrowPathVertexColorType,
76-
'unorm8x4' | VertexList<'unorm8x4'>
79+
ArrowPathRowColorType | ArrowPathVertexColorType
7780
>;
7881
type ArrowPathWidthSource = ArrowLayerColumnSource<Float32, 'float32'>;
7982

@@ -424,6 +427,7 @@ type ArrowPathLayerBatch = {
424427
rowIndexOffset: number;
425428
rowCount: number;
426429
temporalBuffer: Buffer | null;
430+
convertedColorVector: GPUVector | null;
427431
};
428432

429433
/** Deck-facing props for an Arrow-backed path layer. */
@@ -640,10 +644,7 @@ export class ArrowPathLayer extends Layer<ArrowPathLayerProps> {
640644
widthNullValue
641645
);
642646
if (isArrowLayerGPUVector(colorSource)) {
643-
assertArrowLayerGPUVector('ArrowPathLayer color', colorSource, [
644-
'unorm8x4',
645-
'vertex-list<unorm8x4>'
646-
]);
647+
assertArrowLayerColorGPUVector('ArrowPathLayer color', colorSource);
647648
}
648649
if (isArrowLayerGPUVector(widthSource)) {
649650
assertArrowLayerGPUVector('ArrowPathLayer width', widthSource, ['float32']);
@@ -656,18 +657,28 @@ export class ArrowPathLayer extends Layer<ArrowPathLayerProps> {
656657
model === 'storage' && isArrowLayerGPUVector(widthSource)
657658
? undefined
658659
: await this.resolvePathStyleSource(widthSource, rowIndexOffset, recordBatch?.numRows);
659-
const sourceVectors = fillNullablePathStyleVectors(
660+
let sourceVectors = fillNullablePathStyleVectors(
660661
resolveArrowPathSourceVectors(model === 'storage' ? PathStorageModel : PathAttributeModel, {
661662
data: recordBatch,
662663
selectors: {
663664
paths: props.paths,
664-
colors: colorSelector ?? null,
665+
colors: (colorSelector ?? null) as ArrowPathSourceVectorSelectors['colors'],
665666
widths: widthSelector ?? null
666667
}
667668
}),
668669
colorNullValue,
669670
widthNullValue
670671
);
672+
if (sourceVectors.colors && !DataType.isList(sourceVectors.colors.type)) {
673+
sourceVectors = {
674+
...sourceVectors,
675+
colors: (await convertArrowLayerColorVector(
676+
this.context.device,
677+
sourceVectors.colors,
678+
`${props.id}-batch-${batchIndex}-arrow-colors`
679+
)) as NonNullable<ArrowPathSourceVectors['colors']>
680+
};
681+
}
671682
const prepared = await ArrowPathRenderer.convertToGPUVectors(
672683
this.context.device,
673684
sourceVectors,
@@ -682,10 +693,32 @@ export class ArrowPathLayer extends Layer<ArrowPathLayerProps> {
682693
return;
683694
}
684695

685-
const preparedColorColumn =
686-
model === 'storage' && isArrowLayerGPUVector(colorSource)
687-
? getPathStorageGPUVectorBatch(colorSource, batchIndex, sourceVectors.paths.length)
688-
: prepared.colors;
696+
let convertedColorVector: GPUVector | null = null;
697+
let preparedColorColumn = prepared.colors;
698+
if (model === 'storage' && isArrowLayerGPUVector(colorSource)) {
699+
try {
700+
const sourceColorBatch = getPathStorageGPUVectorBatch(
701+
colorSource,
702+
batchIndex,
703+
sourceVectors.paths.length
704+
);
705+
const normalized = await convertArrowLayerColorGPUVector(
706+
this.context.device,
707+
sourceColorBatch,
708+
`${props.id}-batch-${batchIndex}-colors`
709+
);
710+
preparedColorColumn = normalized.vector;
711+
convertedColorVector = normalized.converted ? normalized.vector : null;
712+
} catch (error) {
713+
prepared.destroy();
714+
throw error;
715+
}
716+
if (!this.isActiveLoad(loadVersion)) {
717+
convertedColorVector?.destroy();
718+
prepared.destroy();
719+
return;
720+
}
721+
}
689722
const preparedWidthColumn =
690723
model === 'storage' && isArrowLayerGPUVector(widthSource)
691724
? getPathStorageGPUVectorBatch(widthSource, batchIndex, sourceVectors.paths.length)
@@ -748,12 +781,14 @@ export class ArrowPathLayer extends Layer<ArrowPathLayerProps> {
748781
}
749782
} catch (error) {
750783
destroyBuffers(constantStyle.buffers);
784+
convertedColorVector?.destroy();
751785
prepared.destroy();
752786
throw error;
753787
}
754788
if (!this.isActiveLoad(loadVersion)) {
755789
renderModel.destroy();
756790
destroyBuffers(constantStyle.buffers);
791+
convertedColorVector?.destroy();
757792
prepared.destroy();
758793
return;
759794
}
@@ -766,7 +801,8 @@ export class ArrowPathLayer extends Layer<ArrowPathLayerProps> {
766801
batchIndex,
767802
rowIndexOffset,
768803
rowCount,
769-
temporalBuffer: constantStyle.temporalBuffer
804+
temporalBuffer: constantStyle.temporalBuffer,
805+
convertedColorVector
770806
};
771807
const batches = [...this.getLayerState().batches, batch];
772808
this.setState({batches, loadVersion, sourceInitialized: true});
@@ -807,7 +843,20 @@ export class ArrowPathLayer extends Layer<ArrowPathLayerProps> {
807843
const cache = this.getLayerState().gpuVectorSourceCache;
808844
let arrowVectorPromise = cache.get(source);
809845
if (!arrowVectorPromise) {
810-
arrowVectorPromise = readArrowGPUVectorAsync(source);
846+
arrowVectorPromise = (async () => {
847+
const normalized = await convertArrowLayerColorGPUVector(
848+
this.context.device,
849+
source,
850+
`${this.id}-colors`
851+
);
852+
try {
853+
return await readArrowGPUVectorAsync(normalized.vector);
854+
} finally {
855+
if (normalized.converted) {
856+
normalized.vector.destroy();
857+
}
858+
}
859+
})();
811860
cache.set(source, arrowVectorPromise);
812861
}
813862
const arrowVector = await arrowVectorPromise;
@@ -905,9 +954,11 @@ function hasArrowDataNulls(data: Data): boolean {
905954
return data.nullCount > 0 || data.children.some(hasArrowDataNulls);
906955
}
907956

908-
function getPathStorageGPUVectorBatch<
909-
Format extends 'unorm8x4' | VertexList<'unorm8x4'> | 'float32'
910-
>(vector: GPUVector<Format>, batchIndex: number, expectedRowCount: number): GPUVector<Format> {
957+
function getPathStorageGPUVectorBatch<Format extends GPUVectorFormat>(
958+
vector: GPUVector<Format>,
959+
batchIndex: number,
960+
expectedRowCount: number
961+
): GPUVector<Format> {
911962
const useWholeVector =
912963
vector.data.length === 1 && batchIndex === 0 && vector.length === expectedRowCount;
913964
const data = useWholeVector ? vector.data[0] : vector.data[batchIndex];
@@ -1085,6 +1136,7 @@ function destroyPathBatches(batches: ArrowPathLayerBatch[]): void {
10851136
for (const batch of batches) {
10861137
batch.model.destroy();
10871138
destroyBuffers(batch.constantBuffers);
1139+
batch.convertedColorVector?.destroy();
10881140
batch.prepared.destroy();
10891141
}
10901142
}

modules/arrow-layers/src/layers/arrow-polygon-layer.ts

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import {
1616
ArrowPolygonRenderer,
1717
readArrowGPUVectorAsync,
18+
type ArrowColorType,
1819
type ArrowPolygonRendererDataBatchUpdate,
1920
type ArrowPolygonRendererProps
2021
} from '@luma.gl/arrow';
@@ -31,12 +32,13 @@ import type {ArrowLayerPickingInfo} from './arrow-layer-types';
3132
import {
3233
GPUVector,
3334
POLYGON_STORAGE_SHADER_LAYOUT,
34-
type GPURecordBatchSourceInfo,
35-
type VertexList
35+
type GPURecordBatchSourceInfo
3636
} from '@luma.gl/tables';
37-
import type {Vector} from 'apache-arrow';
37+
import {Vector} from 'apache-arrow';
3838
import {
39-
assertArrowLayerGPUVector,
39+
assertArrowLayerColorGPUVector,
40+
convertArrowLayerColorGPUVector,
41+
convertArrowLayerColorVector,
4042
getArrowLayerInputNullValue,
4143
getArrowLayerInputSource,
4244
inspectArrowLayerColumn,
@@ -48,7 +50,8 @@ import {
4850
type ArrowPolygonColor = [number, number, number, number];
4951
type ArrowPolygonColorSource =
5052
| Exclude<ArrowPolygonRendererProps['colors'], null | undefined>
51-
| GPUVector<'unorm8x4' | VertexList<'unorm8x4'>>;
53+
| Vector<ArrowColorType>
54+
| GPUVector;
5255
type ArrowPolygonResolvedColorSource = {
5356
value: Exclude<ArrowPolygonColorSource, GPUVector> | null;
5457
};
@@ -299,13 +302,12 @@ export class ArrowPolygonLayer extends Layer<ArrowPolygonLayerProps> {
299302
...this.getRendererModelProps(props),
300303
data: props.data,
301304
polygons: props.polygons,
302-
colors:
303-
resolvedColorSource?.value ??
305+
colors: (resolvedColorSource?.value ??
304306
(resolvedColorSource
305307
? null
306308
: isArrowLayerGPUVector(colorSource)
307309
? null
308-
: (colorSource ?? null)),
310+
: (colorSource ?? null))) as ArrowPolygonRendererProps['colors'],
309311
tessellated: props.tessellated,
310312
color: getArrowLayerInputNullValue(props.color, DEFAULT_POLYGON_COLOR, isArrowLayerColor),
311313
center: props.center,
@@ -330,6 +332,29 @@ export class ArrowPolygonLayer extends Layer<ArrowPolygonLayerProps> {
330332
const state = this.getLayerState();
331333
const colorResolveVersion = state.colorResolveVersion + 1;
332334
state.colorResolveVersion = colorResolveVersion;
335+
if (colorSource instanceof Vector) {
336+
renderer.setProps(this.getRendererProps(props, {value: null}));
337+
void convertArrowLayerColorVector(this.context.device, colorSource, `${this.id}-colors`)
338+
.then(colorVector => {
339+
if (
340+
this.getRendererOrNull() === renderer &&
341+
this.getLayerState().colorResolveVersion === colorResolveVersion
342+
) {
343+
renderer.setProps(
344+
this.getRendererProps(props, {value: colorVector as Vector as never})
345+
);
346+
this.watchRendererPipeline(renderer);
347+
this.setNeedsRedraw();
348+
}
349+
})
350+
.catch(error => {
351+
if (this.getLayerState().colorResolveVersion === colorResolveVersion) {
352+
if (props.onDataError) props.onDataError(error);
353+
else this.raiseError(toError(error), `resolving Arrow polygon color in ${this}`);
354+
}
355+
});
356+
return;
357+
}
333358
if (!isArrowLayerGPUVector(colorSource)) {
334359
if (typeof colorSource === 'string') {
335360
if (!props.data) {
@@ -380,14 +405,22 @@ export class ArrowPolygonLayer extends Layer<ArrowPolygonLayerProps> {
380405
}
381406
const polygonRowCount =
382407
props.polygons && typeof props.polygons !== 'string' ? props.polygons.length : undefined;
383-
assertArrowLayerGPUVector(
384-
'ArrowPolygonLayer color',
385-
colorSource,
386-
['unorm8x4', 'vertex-list<unorm8x4>'],
387-
polygonRowCount
388-
);
408+
assertArrowLayerColorGPUVector('ArrowPolygonLayer color', colorSource, polygonRowCount);
389409
renderer.setProps({data: null});
390-
void readArrowGPUVectorAsync(colorSource)
410+
void (async () => {
411+
const normalized = await convertArrowLayerColorGPUVector(
412+
this.context.device,
413+
colorSource,
414+
`${this.id}-colors`
415+
);
416+
try {
417+
return await readArrowGPUVectorAsync(normalized.vector);
418+
} finally {
419+
if (normalized.converted) {
420+
normalized.vector.destroy();
421+
}
422+
}
423+
})()
391424
.then(colorVector => {
392425
if (
393426
this.getRendererOrNull() === renderer &&

0 commit comments

Comments
 (0)