Improve .ply parsing speed using compiled parser function (#150)

* Compile a parse function per element type in PlyReader

* Parse f_rest properties as array for faster .ply file parsing
This commit is contained in:
Noeri Huisman
2025-08-07 19:21:23 +02:00
committed by GitHub
parent b454b3652f
commit bafff1f531
2 changed files with 238 additions and 65 deletions
+5
View File
@@ -36,3 +36,8 @@ export const SPLAT_TEX_DEPTH_MASK = SPLAT_TEX_DEPTH - 1;
// time between pure JS and WASM are minimal and don't make a big difference. // time between pure JS and WASM are minimal and don't make a big difference.
export const WASM_SPLAT_SORT = true; export const WASM_SPLAT_SORT = true;
// Enable/disable compiling a dedicated parse function per element type
// in the plyReader.
export const USE_COMPILED_PARSER_FUNCTION = true;
+233 -65
View File
@@ -1,14 +1,18 @@
// PLY file format reader // PLY file format reader
export type PlyPropertyType = import { USE_COMPILED_PARSER_FUNCTION } from "./defines";
| "char"
| "uchar" const PLY_PROPERTY_TYPES = [
| "short" "char",
| "ushort" "uchar",
| "int" "short",
| "uint" "ushort",
| "float" "int",
| "double"; "uint",
"float",
"double",
] as const;
export type PlyPropertyType = (typeof PLY_PROPERTY_TYPES)[number];
export type PlyElement = { export type PlyElement = {
name: string; name: string;
@@ -199,49 +203,14 @@ export class PlyReader {
for (const elementName in this.elements) { for (const elementName in this.elements) {
const element = this.elements[elementName]; const element = this.elements[elementName];
const { count, properties } = element; const { count, properties } = element;
const item: Record<string, number | number[]> = {}; const item = createEmptyItem(properties);
// Construct an array of parser function to parse each property in an item // Construct a parse function
const parsers = []; const parseFn = createParseFn(properties, this.littleEndian);
for (const [propertyName, property] of Object.entries(properties)) {
if (!property.isList) {
item[propertyName] = 0;
parsers.push(() => {
item[propertyName] = PARSE_FIELD[property.type](
data,
offset,
this.littleEndian,
);
offset += FIELD_BYTES[property.type];
});
} else {
// Property is a list, so parse the count first
item[propertyName] = [];
parsers.push(() => {
const list = item[propertyName] as number[];
list.length = PARSE_FIELD[property.countType as PlyPropertyType](
data,
offset,
this.littleEndian,
);
offset += FIELD_BYTES[property.countType as PlyPropertyType];
for (let i = 0; i < list.length; i++) {
list[i] = PARSE_FIELD[property.type](
data,
offset,
this.littleEndian,
);
offset += FIELD_BYTES[property.type];
}
});
}
}
// Parse all the items in the element // Parse all the items in the element
const callback = elementCallback(element) ?? (() => {}); const callback = elementCallback(element) ?? (() => {});
for (let index = 0; index < count; index++) { for (let index = 0; index < count; index++) {
for (let parserIndex = 0; parserIndex < parsers.length; parserIndex++) { offset = parseFn(data, offset, item);
parsers[parserIndex]();
}
callback(index, item); callback(index, item);
} }
} }
@@ -258,9 +227,9 @@ export class PlyReader {
const ssChunks: SSChunk[] = []; const ssChunks: SSChunk[] = [];
let numSh = 0; let numSh = 0;
let sh1Props: string[] = []; let sh1Props: number[] = [];
let sh2Props: string[] = []; let sh2Props: number[] = [];
let sh3Props: string[] = []; let sh3Props: number[] = [];
let sh1: Float32Array | undefined = undefined; let sh1: Float32Array | undefined = undefined;
let sh2: Float32Array | undefined = undefined; let sh2: Float32Array | undefined = undefined;
let sh3: Float32Array | undefined = undefined; let sh3: Float32Array | undefined = undefined;
@@ -270,18 +239,16 @@ export class PlyReader {
const num_f_rest = NUM_SH_TO_NUM_F_REST[numSh]; const num_f_rest = NUM_SH_TO_NUM_F_REST[numSh];
sh1Props = new Array(3) sh1Props = new Array(3)
.fill(null) .fill(null)
.flatMap((_, k) => .flatMap((_, k) => [0, 1, 2].map((_, d) => k + (d * num_f_rest) / 3));
[0, 1, 2].map((_, d) => `f_rest_${k + (d * num_f_rest) / 3}`),
);
sh2Props = new Array(5) sh2Props = new Array(5)
.fill(null) .fill(null)
.flatMap((_, k) => .flatMap((_, k) =>
[0, 1, 2].map((_, d) => `f_rest_${3 + k + (d * num_f_rest) / 3}`), [0, 1, 2].map((_, d) => 3 + k + (d * num_f_rest) / 3),
); );
sh3Props = new Array(7) sh3Props = new Array(7)
.fill(null) .fill(null)
.flatMap((_, k) => .flatMap((_, k) =>
[0, 1, 2].map((_, d) => `f_rest_${8 + k + (d * num_f_rest) / 3}`), [0, 1, 2].map((_, d) => 8 + k + (d * num_f_rest) / 3),
); );
sh1 = numSh >= 1 ? new Float32Array(3 * 3) : undefined; sh1 = numSh >= 1 ? new Float32Array(3 * 3) : undefined;
sh2 = numSh >= 2 ? new Float32Array(5 * 3) : undefined; sh2 = numSh >= 2 ? new Float32Array(5 * 3) : undefined;
@@ -296,17 +263,19 @@ export class PlyReader {
if (!sh1) { if (!sh1) {
throw new Error("Missing sh1"); throw new Error("Missing sh1");
} }
const sh = item.f_rest as number[];
for (let i = 0; i < sh1Props.length; i++) { for (let i = 0; i < sh1Props.length; i++) {
sh1[i] = ((item[sh1Props[i]] as number) * 8) / 255 - 4; sh1[i] = (sh[sh1Props[i]] * 8) / 255 - 4;
} }
if (sh2) { if (sh2) {
for (let i = 0; i < sh2Props.length; i++) { for (let i = 0; i < sh2Props.length; i++) {
sh2[i] = ((item[sh2Props[i]] as number) * 8) / 255 - 4; sh2[i] = (sh[sh2Props[i]] * 8) / 255 - 4;
} }
} }
if (sh3) { if (sh3) {
for (let i = 0; i < sh3Props.length; i++) { for (let i = 0; i < sh3Props.length; i++) {
sh3[i] = ((item[sh3Props[i]] as number) * 8) / 255 - 4; sh3[i] = (sh[sh3Props[i]] * 8) / 255 - 4;
} }
} }
shCallback?.(index, sh1, sh2, sh3); shCallback?.(index, sh1, sh2, sh3);
@@ -623,19 +592,20 @@ export class PlyReader {
); );
if (shCallback && sh1) { if (shCallback && sh1) {
const sh = item.f_rest as number[];
if (sh1) { if (sh1) {
for (const [i, key] of sh1Props.entries()) { for (let i = 0; i < sh1Props.length; i++) {
sh1[i] = item[key] as number; sh1[i] = sh[sh1Props[i]];
} }
} }
if (sh2) { if (sh2) {
for (const [i, key] of sh2Props.entries()) { for (let i = 0; i < sh2Props.length; i++) {
sh2[i] = item[key] as number; sh2[i] = sh[sh2Props[i]];
} }
} }
if (sh3) { if (sh3) {
for (const [i, key] of sh3Props.entries()) { for (let i = 0; i < sh3Props.length; i++) {
sh3[i] = item[key] as number; sh3[i] = sh[sh3Props[i]];
} }
} }
shCallback(index, sh1, sh2, sh3); shCallback(index, sh1, sh2, sh3);
@@ -887,6 +857,204 @@ const NUM_SH_TO_NUM_F_REST: Record<number, number> = {
3: 45, 3: 45,
}; };
const F_REST_REGEX = /^f_rest_([0-9]{1,2})$/;
function createEmptyItem(
properties: Record<string, PlyProperty>,
): Record<string, number | number[]> {
const item: Record<string, number | number[]> = {};
for (const [propertyName, property] of Object.entries(properties)) {
// Treat f_rest properties as a single array for performance
if (F_REST_REGEX.test(propertyName)) {
item.f_rest = new Array(getNumSh(properties));
} else {
item[propertyName] = property.isList ? [] : 0;
}
}
return item;
}
function createParseFn(
properties: Record<string, PlyProperty>,
littleEndian: boolean,
) {
if (USE_COMPILED_PARSER_FUNCTION && safeToCompile(properties)) {
return createCompiledParserFn(properties, littleEndian);
}
return createDynamicParserFn(properties, littleEndian);
}
// Detect if unsafe eval is allowed in the current execution context
const UNSAFE_EVAL_ALLOWED = (() => {
try {
new Function("return 42;");
} catch (e) {
return false;
}
return true;
})();
const PROPERTY_NAME_REGEX = /^[a-zA-Z0-9_]+$/;
function safeToCompile(properties: Record<string, PlyProperty>) {
if (!UNSAFE_EVAL_ALLOWED) {
return false;
}
for (const [propertyName, property] of Object.entries(properties)) {
if (!PROPERTY_NAME_REGEX.test(propertyName)) {
return false;
}
if (
property.isList &&
!PLY_PROPERTY_TYPES.includes(property.countType as PlyPropertyType)
) {
return false;
}
if (!PLY_PROPERTY_TYPES.includes(property.type)) {
return false;
}
}
return true;
}
function createCompiledParserFn(
properties: Record<string, PlyProperty>,
littleEndian: boolean,
) {
// Construct the parser function source.
const parserSrc: string[] = ["let list;"];
for (const [propertyName, property] of Object.entries(properties)) {
const fRestMatch = propertyName.match(F_REST_REGEX);
if (fRestMatch) {
const fRestIndex = +fRestMatch[1];
parserSrc.push(/*js*/ `
item.f_rest[${fRestIndex}] = PARSE_FIELD['${property.type}'](data, offset, ${littleEndian});
offset += ${FIELD_BYTES[property.type]};
`);
} else if (!property.isList) {
parserSrc.push(/*js*/ `
item['${propertyName}'] = PARSE_FIELD['${property.type}'](data, offset, ${littleEndian});
offset += ${FIELD_BYTES[property.type]};
`);
} else {
// Property is a list, so parse the count first
parserSrc.push(/*js*/ `
list = item['${propertyName}'];
list.length = PARSE_FIELD['${property.countType}'](data, offset, ${littleEndian});
offset += ${FIELD_BYTES[property.countType as PlyPropertyType]};
for (let i = 0; i < list.length; i++) {
list[i] = PARSE_FIELD['${property.type}'](data, offset, ${littleEndian});
offset += ${FIELD_BYTES[property.type]};
}
`);
}
}
parserSrc.push("return offset;");
const fn = new Function(
"data",
"offset",
"item",
"PARSE_FIELD",
parserSrc.join("\n"),
);
return (
data: DataView,
offset: number,
item: Record<string, number | number[]>,
) => fn(data, offset, item, PARSE_FIELD);
}
function createDynamicParserFn(
properties: Record<string, PlyProperty>,
littleEndian: boolean,
) {
// Construct an array of parser function to parse each property in an item
const parsers: Array<
(
data: DataView,
offset: number,
item: Record<string, number | number[]>,
) => number
> = [];
for (const [propertyName, property] of Object.entries(properties)) {
const fRestMatch = propertyName.match(F_REST_REGEX);
if (fRestMatch) {
const fRestIndex = +fRestMatch[1];
parsers.push(
(
data: DataView,
offset: number,
item: Record<string, number | number[]>,
) => {
(item.f_rest as number[])[fRestIndex] = PARSE_FIELD[property.type](
data,
offset,
littleEndian,
);
return offset + FIELD_BYTES[property.type];
},
);
} else if (!property.isList) {
parsers.push(
(
data: DataView,
offset: number,
item: Record<string, number | number[]>,
) => {
item[propertyName] = PARSE_FIELD[property.type](
data,
offset,
littleEndian,
);
return offset + FIELD_BYTES[property.type];
},
);
} else {
// Property is a list, so parse the count first
parsers.push(
(
data: DataView,
offset: number,
item: Record<string, number | number[]>,
) => {
const list = item[propertyName] as number[];
list.length = PARSE_FIELD[property.countType as PlyPropertyType](
data,
offset,
littleEndian,
);
let currentOffset =
offset + FIELD_BYTES[property.countType as PlyPropertyType];
for (let i = 0; i < list.length; i++) {
list[i] = PARSE_FIELD[property.type](
data,
currentOffset,
littleEndian,
);
currentOffset += FIELD_BYTES[property.type];
}
return currentOffset;
},
);
}
}
return (
data: DataView,
offset: number,
item: Record<string, number | number[]>,
) => {
let currentOffset = offset;
for (let parserIndex = 0; parserIndex < parsers.length; parserIndex++) {
currentOffset = parsers[parserIndex](data, currentOffset, item);
}
return currentOffset;
};
}
function getNumSh(properties: Record<string, PlyProperty>) { function getNumSh(properties: Record<string, PlyProperty>) {
let num_f_rest = 0; let num_f_rest = 0;
while (properties[`f_rest_${num_f_rest}`]) { while (properties[`f_rest_${num_f_rest}`]) {