mirror of
https://github.com/storytold/spark.git
synced 2026-10-09 00:09:53 +00:00
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:
@@ -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
@@ -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}`]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user