Skip to content

Commit 5fe394e

Browse files
committed
Optimize LightList hot path copies
1 parent 5c81b07 commit 5fe394e

2 files changed

Lines changed: 187 additions & 35 deletions

File tree

jme3-core/src/main/java/com/jme3/light/LightList.java

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@
4949
public final class LightList implements Iterable<Light>, Savable, Cloneable, JmeCloneable {
5050

5151
private Light[] list, tlist;
52-
private float[] distToOwner;
5352
private int listSize;
53+
private int tlistSize;
5454
private Spatial owner;
5555

5656
private static final int DEFAULT_SIZE = 1;
@@ -84,8 +84,6 @@ protected LightList() {
8484
public LightList(Spatial owner) {
8585
listSize = 0;
8686
list = new Light[DEFAULT_SIZE];
87-
distToOwner = new float[DEFAULT_SIZE];
88-
Arrays.fill(distToOwner, Float.NEGATIVE_INFINITY);
8987
this.owner = owner;
9088
}
9189

@@ -100,11 +98,8 @@ public void setOwner(Spatial owner) {
10098

10199
private void doubleSize() {
102100
Light[] temp = new Light[list.length * 2];
103-
float[] temp2 = new float[list.length * 2];
104101
System.arraycopy(list, 0, temp, 0, list.length);
105-
System.arraycopy(distToOwner, 0, temp2, 0, list.length);
106102
list = temp;
107-
distToOwner = temp2;
108103
}
109104

110105
/**
@@ -117,8 +112,7 @@ public void add(Light l) {
117112
if (listSize == list.length) {
118113
doubleSize();
119114
}
120-
list[listSize] = l;
121-
distToOwner[listSize++] = Float.NEGATIVE_INFINITY;
115+
list[listSize++] = l;
122116
}
123117

124118
/**
@@ -136,8 +130,9 @@ public void remove(int index) {
136130
return;
137131
}
138132

139-
for (int i = index; i < listSize; i++) {
140-
list[i] = list[i+1];
133+
int copyLength = listSize - index;
134+
if (copyLength > 0) {
135+
System.arraycopy(list, index + 1, list, index, copyLength);
141136
}
142137
list[listSize] = null;
143138
}
@@ -182,11 +177,11 @@ public void clear() {
182177
if (listSize == 0)
183178
return;
184179

185-
for (int i = 0; i < listSize; i++)
186-
list[i] = null;
180+
Arrays.fill(list, 0, listSize, null);
187181

188182
if (tlist != null)
189-
Arrays.fill(tlist, null);
183+
Arrays.fill(tlist, 0, tlistSize, null);
184+
tlistSize = 0;
190185

191186
listSize = 0;
192187
}
@@ -205,11 +200,15 @@ public void clear() {
205200
public void sort(boolean transformChanged) {
206201
if (listSize > 1) {
207202
// resize or populate our temporary array as necessary
208-
if (tlist == null || tlist.length != list.length) {
209-
tlist = list.clone();
203+
if (tlist == null || tlist.length < listSize) {
204+
tlist = new Light[listSize];
210205
} else {
211-
System.arraycopy(list, 0, tlist, 0, list.length);
206+
if (tlistSize > listSize) {
207+
Arrays.fill(tlist, listSize, tlistSize, null);
208+
}
212209
}
210+
System.arraycopy(list, 0, tlist, 0, listSize);
211+
tlistSize = listSize;
213212

214213
if (transformChanged) {
215214
// check distance of each light
@@ -249,31 +248,33 @@ public void update(LightList local, LightList parent, Predicate<Light> filter) {
249248
// using the arguments
250249
clear();
251250

252-
while (list.length <= local.listSize) {
251+
int requiredSize = local.listSize + (parent == null ? 0 : parent.listSize);
252+
while (list.length < requiredSize) {
253253
doubleSize();
254254
}
255255

256-
int localListSize = 0;
257-
for(int i=0;i<local.listSize;i++){
258-
Light l = local.list[i];
259-
if (filter != null && !filter.test(l)) continue;
260-
list[localListSize] = l;
261-
distToOwner[localListSize] = Float.NEGATIVE_INFINITY;
262-
localListSize++;
256+
int localListSize;
257+
if (filter == null) {
258+
localListSize = local.listSize;
259+
System.arraycopy(local.list, 0, list, 0, localListSize);
260+
} else {
261+
localListSize = 0;
262+
for(int i=0;i<local.listSize;i++){
263+
Light l = local.list[i];
264+
if (!filter.test(l)) continue;
265+
list[localListSize] = l;
266+
localListSize++;
267+
}
263268
}
264269

265270
// if the spatial has a parent node, add the lights
266271
// from the parent list as well
267272
if (parent != null) {
268273
int sz = localListSize + parent.listSize;
269-
while (list.length <= sz)
274+
while (list.length < sz)
270275
doubleSize();
271276

272-
for (int i = 0; i < parent.listSize; i++) {
273-
int p = i + localListSize;
274-
list[p] = parent.list[i];
275-
distToOwner[p] = Float.NEGATIVE_INFINITY;
276-
}
277+
System.arraycopy(parent.list, 0, list, localListSize, parent.listSize);
277278

278279
listSize = localListSize + parent.listSize;
279280
} else {
@@ -319,7 +320,6 @@ public LightList clone() {
319320

320321
clone.owner = null;
321322
clone.list = list.clone();
322-
clone.distToOwner = distToOwner.clone();
323323
clone.tlist = null; // list used for sorting only
324324

325325
return clone;
@@ -343,7 +343,6 @@ public LightList jmeClone() {
343343
public void cloneFields(Cloner cloner, Object original) {
344344
this.owner = cloner.clone(owner);
345345
this.list = cloner.clone(list);
346-
this.distToOwner = cloner.clone(distToOwner);
347346
}
348347

349348
@Override
@@ -370,13 +369,10 @@ public void read(JmeImporter im) throws IOException {
370369
// NOTE: make sure the array has a length of at least 1
371370
int arraySize = Math.max(DEFAULT_SIZE, listSize);
372371
list = new Light[arraySize];
373-
distToOwner = new float[arraySize];
374372

375373
for (int i = 0; i < listSize; i++) {
376374
list[i] = lights.get(i);
377375
}
378-
379-
Arrays.fill(distToOwner, Float.NEGATIVE_INFINITY);
380376
}
381377

382378
@Override
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright (c) 2026 jMonkeyEngine
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are
7+
* met:
8+
*
9+
* * Redistributions of source code must retain the above copyright
10+
* notice, this list of conditions and the following disclaimer.
11+
*
12+
* * Redistributions in binary form must reproduce the above copyright
13+
* notice, this list of conditions and the following disclaimer in the
14+
* documentation and/or other materials provided with the distribution.
15+
*
16+
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17+
* may be used to endorse or promote products derived from this software
18+
* without specific prior written permission.
19+
*
20+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22+
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23+
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24+
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25+
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26+
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27+
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28+
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29+
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30+
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31+
*/
32+
package com.jme3.light;
33+
34+
import com.jme3.math.Vector3f;
35+
import com.jme3.scene.Geometry;
36+
import com.jme3.scene.Mesh;
37+
import org.junit.jupiter.api.Assertions;
38+
import org.junit.jupiter.api.Test;
39+
40+
/**
41+
* Test cases for LightList.
42+
*/
43+
public class LightListTest {
44+
45+
/**
46+
* Verify sort() remains correct after the list previously held more lights.
47+
*/
48+
@Test
49+
public void testSortAfterRetainedCapacity() {
50+
Geometry owner = new Geometry("owner", new Mesh());
51+
LightList list = new LightList(owner);
52+
53+
for (int i = 0; i < 32; i++) {
54+
list.add(new PointLight(new Vector3f(100f + i, 0f, 0f)));
55+
}
56+
list.sort(true);
57+
list.clear();
58+
59+
PointLight far = new PointLight(new Vector3f(10f, 0f, 0f));
60+
PointLight near = new PointLight(new Vector3f(1f, 0f, 0f));
61+
PointLight middle = new PointLight(new Vector3f(5f, 0f, 0f));
62+
63+
list.add(far);
64+
list.add(near);
65+
list.add(middle);
66+
list.sort(true);
67+
68+
Assertions.assertSame(near, list.get(0));
69+
Assertions.assertSame(middle, list.get(1));
70+
Assertions.assertSame(far, list.get(2));
71+
}
72+
73+
/**
74+
* Verify indexed removal preserves order for front, middle, and tail removals.
75+
*/
76+
@Test
77+
public void testRemovePreservesOrder() {
78+
LightList list = new LightList(new Geometry("owner", new Mesh()));
79+
AmbientLight first = new AmbientLight();
80+
AmbientLight second = new AmbientLight();
81+
AmbientLight third = new AmbientLight();
82+
AmbientLight fourth = new AmbientLight();
83+
84+
list.add(first);
85+
list.add(second);
86+
list.add(third);
87+
list.add(fourth);
88+
89+
list.remove(0);
90+
Assertions.assertEquals(3, list.size());
91+
Assertions.assertSame(second, list.get(0));
92+
Assertions.assertSame(third, list.get(1));
93+
Assertions.assertSame(fourth, list.get(2));
94+
95+
list.remove(1);
96+
Assertions.assertEquals(2, list.size());
97+
Assertions.assertSame(second, list.get(0));
98+
Assertions.assertSame(fourth, list.get(1));
99+
100+
list.remove(1);
101+
Assertions.assertEquals(1, list.size());
102+
Assertions.assertSame(second, list.get(0));
103+
}
104+
105+
/**
106+
* Verify unfiltered world-list updates preserve local-then-parent ordering.
107+
*/
108+
@Test
109+
public void testUpdateCopiesLocalAndParentLights() {
110+
LightList local = new LightList(new Geometry("local", new Mesh()));
111+
LightList parent = new LightList(new Geometry("parent", new Mesh()));
112+
LightList world = new LightList(new Geometry("world", new Mesh()));
113+
114+
AmbientLight localFirst = new AmbientLight();
115+
AmbientLight localSecond = new AmbientLight();
116+
AmbientLight parentFirst = new AmbientLight();
117+
AmbientLight parentSecond = new AmbientLight();
118+
119+
local.add(localFirst);
120+
local.add(localSecond);
121+
parent.add(parentFirst);
122+
parent.add(parentSecond);
123+
124+
world.update(local, parent);
125+
126+
Assertions.assertEquals(4, world.size());
127+
Assertions.assertSame(localFirst, world.get(0));
128+
Assertions.assertSame(localSecond, world.get(1));
129+
Assertions.assertSame(parentFirst, world.get(2));
130+
Assertions.assertSame(parentSecond, world.get(3));
131+
}
132+
133+
/**
134+
* Verify filtered world-list updates keep only accepted local lights.
135+
*/
136+
@Test
137+
public void testUpdateFiltersLocalLights() {
138+
LightList local = new LightList(new Geometry("local", new Mesh()));
139+
LightList parent = new LightList(new Geometry("parent", new Mesh()));
140+
LightList world = new LightList(new Geometry("world", new Mesh()));
141+
142+
AmbientLight keep = new AmbientLight();
143+
AmbientLight discard = new AmbientLight();
144+
AmbientLight parentLight = new AmbientLight();
145+
146+
local.add(keep);
147+
local.add(discard);
148+
parent.add(parentLight);
149+
150+
world.update(local, parent, light -> light != discard);
151+
152+
Assertions.assertEquals(2, world.size());
153+
Assertions.assertSame(keep, world.get(0));
154+
Assertions.assertSame(parentLight, world.get(1));
155+
}
156+
}

0 commit comments

Comments
 (0)