Skip to content

Commit 179f6df

Browse files
committed
2.14.0 - major memory leak patches.
1 parent 13d9c7d commit 179f6df

16 files changed

Lines changed: 377 additions & 84 deletions

File tree

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ plugins {
1111
}
1212

1313
group = "gg.generations"
14-
version = "2.13.2"
14+
version = "2.14.0"
1515

1616
java.toolchain.languageVersion.set(JavaLanguageVersion.of(21))
1717

src/library/java/gg/generations/rarecandy/pokeutils/Pair.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ public void b(B b) {
2727
this.b = b;
2828
}
2929

30+
public Pair<A, B> set(A a, B b) {
31+
a(a);
32+
b(b);
33+
return this;
34+
}
35+
3036
@Override
3137
public int hashCode() {
3238
return Objects.hash(a, b);

src/library/java/gg/generations/rarecandy/renderer/animation/Animation.java

Lines changed: 126 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import gg.generations.rarecandy.pokeutils.ModelNode;
44
import gg.generations.rarecandy.pokeutils.SkeletalTransform;
55
import org.joml.Matrix4f;
6+
import org.joml.Matrix4fStack;
67
import org.joml.Quaternionf;
78
import org.joml.Vector3f;
89

@@ -11,7 +12,6 @@
1112
import java.util.function.BiConsumer;
1213

1314
public class Animation {
14-
private final static Matrix4f IDENTITY = new Matrix4f();
1515

1616
public static final int FPS_60 = 1000;
1717
public static final int FPS_24 = 400;
@@ -21,6 +21,20 @@ public class Animation {
2121
public static Vector3f TRANSLATE = new Vector3f();
2222
protected static Vector3f SCALE = new Vector3f(1, 1, 1);
2323
protected static Vector3f TRANSLATION = new Vector3f();
24+
25+
private static final Matrix4f[] MATRIX = new Matrix4f[220];
26+
private static final Matrix4fStack GLOBAL = new Matrix4fStack(220);
27+
private final static Matrix4f IDENTITY = new Matrix4f();
28+
private static final Matrix4f TEMP_GLOBAL_TRANSFORM = new Matrix4f();
29+
private static final Matrix4f TEMP_BONE_RESULT = new Matrix4f();
30+
private static final Vector3f TEMP_ORIGIN_VECTOR = new Vector3f();
31+
32+
static {
33+
for (int i = 0; i < MATRIX.length; i++) {
34+
MATRIX[i] = new Matrix4f();
35+
}
36+
}
37+
2438
public final String name;
2539
public final double animationDuration;
2640
protected final Skeleton skeleton;
@@ -29,6 +43,9 @@ public class Animation {
2943
private final AnimationNode[] animationNodes;
3044
public Map<String, Offset> offsets;
3145

46+
private Matrix4f[] cachedBoneTransforms;
47+
private final Matrix4f cachedIdentity = new Matrix4f().identity();
48+
3249
public float ticksPerSecond;
3350
public boolean loops;
3451
public boolean ignoreInstancedTime = false;
@@ -96,41 +113,111 @@ public float getAnimationTime(double secondsPassed) {
96113
}
97114

98115
public Matrix4f[] getFrameTransform(AnimationInstance instance) {
99-
var boneTransforms = new Matrix4f[this.skeleton.jointMap.size()];
100-
readNodeHierarchy(instance.getCurrentTime(), skeleton.rootNode, new Matrix4f().identity(), boneTransforms, false);
101-
for (int i = 0; i < boneTransforms.length; i++) {
102-
if(boneTransforms[i] == null) boneTransforms[i] = new Matrix4f();
116+
117+
if (cachedBoneTransforms == null || cachedBoneTransforms.length != skeleton.jointMap.size()) {
118+
cachedBoneTransforms = new Matrix4f[skeleton.jointMap.size()];
119+
for (int i = 0; i < cachedBoneTransforms.length; i++) {
120+
cachedBoneTransforms[i] = new Matrix4f();
121+
}
103122
}
104123

105-
return boneTransforms;
124+
// Reset all transforms to identity before populating
125+
for (Matrix4f mat : cachedBoneTransforms) {
126+
mat.identity();
127+
}
128+
129+
GLOBAL.identity();
130+
131+
readNodeHierarchyPooled(instance.getCurrentTime(), skeleton.rootNode, cachedBoneTransforms, false, 0);
132+
return cachedBoneTransforms;
106133
}
107134

108135
public void getFrameOffset(AnimationInstance instance) {
109-
this.offsets.forEach((k, v) -> {
136+
for (Map.Entry<String, Offset> entry : this.offsets.entrySet()) {
137+
String k = entry.getKey();
138+
Offset v = entry.getValue();
110139
var offsetInstance = instance.offsets.computeIfAbsent(k, a -> new Transform());
111140
offsetInstance.offset().zero();
112141
offsetInstance.scale().set(1, 1);
113142

114143
offsets.get(k).calcOffset(instance.getCurrentTime(), offsetInstance);
115-
});
144+
}
116145
}
117146

118147
public Matrix4f[] getFrameTransform(double secondsPassed) {
119-
var boneTransforms = new Matrix4f[this.skeleton.jointMap.size()];
120-
readNodeHierarchy(getAnimationTime(secondsPassed), skeleton.rootNode, new Matrix4f().identity(), boneTransforms, false);
148+
if (cachedBoneTransforms == null || cachedBoneTransforms.length != skeleton.jointMap.size()) {
149+
cachedBoneTransforms = new Matrix4f[skeleton.jointMap.size()];
150+
for (int i = 0; i < cachedBoneTransforms.length; i++) {
151+
cachedBoneTransforms[i] = new Matrix4f();
152+
}
153+
}
121154

122-
for (int i = 0; i < boneTransforms.length; i++) {
123-
if(boneTransforms[i] == null) boneTransforms[i] = new Matrix4f();
155+
for (Matrix4f mat : cachedBoneTransforms) {
156+
mat.identity();
124157
}
125158

126-
return boneTransforms;
159+
readNodeHierarchyPooled(getAnimationTime(secondsPassed), skeleton.rootNode, cachedBoneTransforms, false, 0);
160+
return cachedBoneTransforms;
127161
}
128162

129-
private static final Matrix4f matrix = new Matrix4f();
163+
public void readNodeHierarchyPooled(float animTime, ModelNode node, Matrix4f[] boneTransforms, boolean offsetUsed, int depth) {
164+
165+
166+
var name = node.name;
167+
var nodeTransform = MATRIX[depth].set(node.transform); // Reuses existing static 'matrix' field
168+
169+
var animationNodeId = skeleton.boneIdMap.getOrDefault(name, -1);
170+
var bone = skeleton.get(name);
171+
172+
if (animationNodeId != -1) {
173+
var animNode = animationNodes[animationNodeId];
174+
175+
if (animNode != null) {
176+
var scale = ignoreScaling ? SCALE : AnimationMath.calcInterpolatedScaling(animTime, animNode);
177+
var rotation = AnimationMath.calcInterpolatedRotation(animTime, animNode);
178+
179+
// Reuse pooled Vector3f for "origin" case
180+
Vector3f translation;
181+
if (name.equalsIgnoreCase("origin")) {
182+
translation = TEMP_ORIGIN_VECTOR;
183+
} else {
184+
translation = AnimationMath.calcInterpolatedPosition(animTime, animNode);
185+
}
186+
187+
if (!offsetUsed) {
188+
offsetUsed = true;
189+
translation.add(rootOffset.position());
190+
rotation.mul(rootOffset.rotation());
191+
}
192+
193+
if(!isIdentityTransform(translation, scale, rotation, 1e-5f)) nodeTransform.identity().translationRotateScale(translation, rotation, scale);
194+
}
195+
}
196+
197+
198+
199+
// Reuse pooled Matrix4f for globalTransform
200+
TEMP_GLOBAL_TRANSFORM.set(GLOBAL).mul(nodeTransform);
201+
202+
if (bone != null && animationNodeId >= 0 && animationNodeId < boneTransforms.length) {
203+
// Write directly into pre-allocated array slot
204+
TEMP_GLOBAL_TRANSFORM.mul(bone.inverseBindMatrix, boneTransforms[animationNodeId]);
205+
}
206+
207+
var nextDepth = depth + 1;
208+
209+
GLOBAL.pushMatrix();
210+
GLOBAL.set(TEMP_GLOBAL_TRANSFORM);
211+
212+
for (var child : node.children) {
213+
readNodeHierarchyPooled(animTime, child, boneTransforms, offsetUsed, nextDepth);
214+
}
215+
GLOBAL.popMatrix();
216+
}
130217

131218
public void readNodeHierarchy(float animTime, ModelNode node, Matrix4f parentTransform, Matrix4f[] boneTransforms, boolean offsetUsed) {
132219
var name = node.name;
133-
var nodeTransform = matrix.set(node.transform);
220+
var nodeTransform = MATRIX[0].set(node.transform);
134221

135222
var animationNodeId = skeleton.boneIdMap.getOrDefault(name, -1);
136223
var bone = skeleton.get(name);
@@ -245,6 +332,30 @@ public void calcOffset(float animTime, Transform instance) {
245332
instance.scale().set(uScale, vScale);
246333
}
247334
}
335+
336+
public static boolean isIdentityTransform(
337+
Vector3f translation,
338+
Vector3f scale,
339+
Quaternionf rotation,
340+
float eps
341+
) {
342+
boolean tX = translation.x == 0.0f;
343+
boolean tY = translation.y == 0.0f;
344+
boolean tZ = translation.z == 0.0f;
345+
346+
boolean sX = scale.x == 0.0f;
347+
boolean sY = scale.y == 0.0f;
348+
boolean sZ = scale.z == 0.0f;
349+
350+
boolean rX = rotation.x == 0.0f;
351+
boolean rY = rotation.y == 0.0f;
352+
boolean rZ = rotation.z == 0.0f;
353+
boolean rW = rotation.w == 1.0f;
354+
355+
return tX && tY && tZ
356+
&& sX && sY && sZ
357+
&& rX && rY && rZ && rW;
358+
}
248359
}
249360

250361

src/library/java/gg/generations/rarecandy/renderer/animation/AnimationController.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public class AnimationController {
1616
Arrays.fill(NO_ANIMATION, identity);
1717
}
1818

19-
public final List<AnimationInstance> playingInstances = new ArrayList<>();
19+
public final Set<AnimationInstance> playingInstances = new LinkedHashSet<>();
2020
public final Map<Animation, Matrix4f[]> instanceIgnoringAnimTransforms = new HashMap<>();
2121

2222
public void render(double globalSecondsPassed) {
@@ -44,6 +44,8 @@ public void render(double globalSecondsPassed) {
4444
playingInstance.animation.getFrameOffset(playingInstance);
4545
}
4646

47-
playingInstances.removeAll(instancesToRemove);
47+
for (AnimationInstance animationInstance : instancesToRemove) {
48+
playingInstances.remove(animationInstance);
49+
}
4850
}
4951
}

src/library/java/gg/generations/rarecandy/renderer/animation/AnimationInstance.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public class AnimationInstance {
1313
public double startTime = -1;
1414
public Matrix4f[] matrixTransforms;
1515
public final Map<String, Transform> offsets = new HashMap<>();
16-
16+
private boolean registered = false;
1717

1818
protected Animation animation;
1919
protected float currentTime;
@@ -67,6 +67,7 @@ public boolean isPaused() {
6767

6868
public void destroy() {
6969
this.unused = true;
70+
this.registered = false;
7071
}
7172

7273
public boolean shouldDestroy() {
@@ -82,4 +83,15 @@ public Transform getOffset(String name) {
8283

8384
return offset;
8485
}
86+
87+
/**
88+
* Ensures this instance is registered with the controller exactly once.
89+
* O(1) check instead of O(n) contains().
90+
*/
91+
public void ensureRegistered(AnimationController controller) {
92+
if (!registered && !unused) {
93+
controller.playingInstances.add(this);
94+
registered = true;
95+
}
96+
}
8597
}

src/library/java/gg/generations/rarecandy/renderer/animation/AnimationMath.java

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,22 @@
55
import org.joml.Vector3f;
66

77
public class AnimationMath {
8+
private static final Pair<TransformStorage.TimeKey<Quaternionf>, TransformStorage.TimeKey<Quaternionf>> QUATERIONF_KEYPAIR = new Pair<>(null, null);
9+
private static final Pair<TransformStorage.TimeKey<Vector3f>, TransformStorage.TimeKey<Vector3f>> VECTOR3F_KEYPAIR = new Pair<>(null, null);
10+
11+
private static final Vector3f DEST_SCALE = new Vector3f();
12+
private static final Vector3f DEST_TRANSLATION = new Vector3f();
13+
private static final Quaternionf DEST_ROTATION = new Quaternionf();
814

915
public static Vector3f calcInterpolatedPosition(float animTime, Animation.AnimationNode node) {
1016
if (node.positionKeys.size() == 1) return node.getDefaultPosition().value();
1117

1218
var positions = findPositions(animTime, node);
1319
float factor = (float) ((animTime - (float) positions.a().time()) / (positions.b().time() - positions.a().time()));
1420

15-
var start = new Vector3f(positions.a().value());
16-
var end = new Vector3f(positions.b().value());
17-
var delta = new Vector3f(end.sub(start));
18-
return new Vector3f(start.add(delta.mul(factor)));
21+
var start = positions.a().value();
22+
var end = positions.b().value();
23+
return DEST_TRANSLATION.set(end).sub(start).mul(factor).add(start);
1924
}
2025

2126
public static Quaternionf calcInterpolatedRotation(float animTime, Animation.AnimationNode node) {
@@ -24,48 +29,46 @@ public static Quaternionf calcInterpolatedRotation(float animTime, Animation.Ani
2429
var rotations = findRotations(animTime, node);
2530
var deltaTime = (float) (rotations.b().time() - rotations.a().time());
2631
var factor = (animTime - (float) rotations.a().time()) / deltaTime;
27-
var start = new Quaternionf(rotations.a().value());
28-
var end = new Quaternionf(rotations.b().value());
29-
return new Quaternionf(start.slerp(end, factor));
32+
var start = rotations.a().value();
33+
var end = rotations.b().value();
34+
return DEST_ROTATION.set(start).slerp(end, factor);
3035
}
3136

3237
public static Vector3f calcInterpolatedScaling(float animTime, Animation.AnimationNode node) {
3338
if (node.scaleKeys.size() == 1) return node.getDefaultScale().value();
3439

35-
var out = new Vector3f();
3640
var scalings = findScalings(animTime, node);
3741
var deltaTime = (float) (scalings.b().time() - scalings.a().time());
3842
var factor = (animTime - (float) scalings.a().time()) / deltaTime;
39-
var start = new Vector3f(scalings.a().value());
40-
var end = new Vector3f(scalings.b().value());
41-
var delta = new Vector3f(end.sub(start));
42-
return out.add(start.add(delta.mul(factor)));
43+
var start = scalings.a().value();
44+
var end = scalings.b().value();
45+
return DEST_SCALE.set(end).sub(start).mul(factor).add(start);
4346
}
4447

4548
public static Pair<TransformStorage.TimeKey<Vector3f>, TransformStorage.TimeKey<Vector3f>> findPositions(float animTime, Animation.AnimationNode node) {
4649
for (var key : node.positionKeys) {
4750
if (animTime < key.time())
48-
return new Pair<>(node.positionKeys.getBefore(key), key);
51+
return VECTOR3F_KEYPAIR.set(node.positionKeys.getBefore(key), key);
4952
}
5053

51-
return new Pair<>(node.positionKeys.get(0), node.positionKeys.get(1));
54+
return VECTOR3F_KEYPAIR.set(node.positionKeys.get(0), node.positionKeys.get(1));
5255
}
5356

5457
public static Pair<TransformStorage.TimeKey<Quaternionf>, TransformStorage.TimeKey<Quaternionf>> findRotations(float animTime, Animation.AnimationNode node) {
5558
for (var key : node.rotationKeys) {
5659
if (animTime < key.time())
57-
return new Pair<>(node.rotationKeys.getBefore(key), key);
60+
return QUATERIONF_KEYPAIR.set(node.rotationKeys.getBefore(key), key);
5861
}
5962

60-
return new Pair<>(node.rotationKeys.get(0), node.rotationKeys.get(1));
63+
return QUATERIONF_KEYPAIR.set(node.rotationKeys.get(0), node.rotationKeys.get(1));
6164
}
6265

6366
public static Pair<TransformStorage.TimeKey<Vector3f>, TransformStorage.TimeKey<Vector3f>> findScalings(float animTime, Animation.AnimationNode node) {
6467
for (var key : node.scaleKeys) {
6568
if (animTime < key.time())
66-
return new Pair<>(node.scaleKeys.getBefore(key), key);
69+
return VECTOR3F_KEYPAIR.set(node.scaleKeys.getBefore(key), key);
6770
}
6871

69-
return new Pair<>(node.scaleKeys.get(0), node.scaleKeys.get(1));
72+
return VECTOR3F_KEYPAIR.set(node.scaleKeys.get(0), node.scaleKeys.get(1));
7073
}
7174
}

src/library/java/gg/generations/rarecandy/renderer/pipeline/Pipeline.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,26 @@ public void unbind(Material material) {
3131
}
3232

3333
public void updateOtherUniforms(ObjectInstance instance, RenderObject renderObject) {
34+
var context = UniformUploadContext.INSTANCE.with(renderObject, instance);
35+
3436
for (var name : uniforms.keySet()) {
3537
var uniform = uniforms.get(name);
3638
if (!uniformSuppliers.containsKey(name))
3739
RareCandy.fatal("No handler for uniform with name \"" + name + "\"");
3840
if (uniform.type != GL20C.GL_SAMPLER_2D)
39-
uniformSuppliers.get(name).accept(new UniformUploadContext(renderObject, instance, uniform));
41+
uniformSuppliers.get(name).accept(context.with(uniform));
4042
}
4143
}
4244

4345
public void updateTexUniforms(ObjectInstance instance, RenderObject renderObject) {
46+
var context = UniformUploadContext.INSTANCE.with(renderObject, instance);
47+
4448
for (var name : uniforms.keySet()) {
4549
var uniform = uniforms.get(name);
4650
if (!uniformSuppliers.containsKey(name))
4751
RareCandy.fatal("No handler for uniform with name \"" + name + "\"");
4852
if (uniform.type == GL20C.GL_SAMPLER_2D)
49-
uniformSuppliers.get(name).accept(new UniformUploadContext(renderObject, instance, uniform));
53+
uniformSuppliers.get(name).accept(UniformUploadContext.INSTANCE.with(uniform));
5054
}
5155
}
5256

0 commit comments

Comments
 (0)