85 lines
3 KiB
Markdown
85 lines
3 KiB
Markdown
# libgdx-0001: Model.loadNode — O(N²) nested for-loop string-ID lookup during model load
|
||
|
||
**Severity:** HIGH
|
||
**File:** gdx/src/com/badlogic/gdx/graphics/g3d/Model.java
|
||
**Line:** 190–210
|
||
**Status:** PATCHED
|
||
|
||
## Description
|
||
|
||
`Model.loadNode()` is called for every node when loading a 3D model. For each
|
||
`ModelNodePart` of each node, it scans the full `meshParts` array to find a matching
|
||
`meshPartId` (string comparison) and then scans the full `materials` array to find a
|
||
matching `materialId` (string comparison).
|
||
|
||
When a model has P node-parts, M mesh-parts, and T materials, the total cost is
|
||
**O(P × (M + T))** — quadratic in total element count.
|
||
|
||
The libGDX developers have already identified this: a `// FIXME create temporary maps for
|
||
faster lookup?` comment appears on line 188, directly above the offending code.
|
||
|
||
## Root Cause
|
||
|
||
```java
|
||
// Model.java:188-210
|
||
// FIXME create temporary maps for faster lookup?
|
||
if (modelNode.parts != null) {
|
||
for (ModelNodePart modelNodePart : modelNode.parts) {
|
||
MeshPart meshPart = null;
|
||
Material meshMaterial = null;
|
||
|
||
if (modelNodePart.meshPartId != null) {
|
||
for (MeshPart part : meshParts) { // O(M) per node-part
|
||
if (modelNodePart.meshPartId.equals(part.id)) {
|
||
meshPart = part;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (modelNodePart.materialId != null) {
|
||
for (Material material : materials) { // O(T) per node-part
|
||
if (modelNodePart.materialId.equals(material.id)) {
|
||
meshMaterial = material;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
`meshParts` and `materials` are `Array<T>` (libGDX's dynamic array) — O(N) linear scan.
|
||
No lookup maps are built before processing nodes.
|
||
|
||
## Fix
|
||
|
||
Build `HashMap<String, MeshPart>` and `HashMap<String, Material>` once before iterating
|
||
nodes, then use O(1) map lookups inside the loop.
|
||
|
||
```java
|
||
// Build lookup maps once before loadNodes loop
|
||
Map<String, MeshPart> meshPartById = new HashMap<>();
|
||
for (MeshPart part : meshParts) meshPartById.put(part.id, part);
|
||
|
||
Map<String, Material> materialById = new HashMap<>();
|
||
for (Material mat : materials) materialById.put(mat.id, mat);
|
||
|
||
// Inside loadNode:
|
||
if (modelNodePart.meshPartId != null)
|
||
meshPart = meshPartById.get(modelNodePart.meshPartId); // O(1)
|
||
|
||
if (modelNodePart.materialId != null)
|
||
meshMaterial = materialById.get(modelNodePart.materialId); // O(1)
|
||
```
|
||
|
||
## Speedup
|
||
|
||
| Node-parts | MeshParts + Materials | Before | After |
|
||
|-----------:|----------------------:|-------:|------:|
|
||
| 100 | 50 | 5 000 comparisons | ~100 lookups |
|
||
| 1 000 | 200 | 200 000 comparisons | ~1 000 lookups |
|
||
| 5 000 | 500 | 2 500 000 comparisons | ~5 000 lookups |
|
||
|
||
Estimated **50–500x** speedup for complex models (character models, level geometry with
|
||
many meshes and material variants). Converts model load time from O(N²) to O(N).
|