Skip to content

Commit 0368453

Browse files
riccardoblCopilotgemini-code-assist[bot]
authored
Memory allocators overhaul (#2835)
* add memory management to ios and android allocators * default ios and android to saferalloc when available * make allocators fully thread safe * deprecate ReflectionAllocator * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * update jmeAndroidNatives * Harden native buffer allocators against allocation and cleanup failures * Update jme3-lwjgl3/src/main/java/com/jme3/util/LWJGLBufferAllocator.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update jme3-lwjgl3/src/main/java/com/jme3/util/LWJGLBufferAllocator.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * update to saferalloc 0.0.10 * Fix saferalloc native memory pressure accounting * Add saferalloc memory pressure guard tests * make screenshot tests use safer allocator --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 1c2822a commit 0368453

14 files changed

Lines changed: 400 additions & 125 deletions

File tree

gradle/libs.versions.toml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ checkstyle = "13.3.0"
66
jacoco = "0.8.12"
77
lwjgl3 = "3.4.1"
88
angle = "2026-05-09"
9-
libjglios = "0.4"
10-
saferalloc = "0.0.8"
9+
libjglios = "0.6"
10+
saferalloc = "0.0.10"
1111
nifty = "1.4.3"
1212
spotbugs = "4.9.8"
13-
jmeAndroidNatives = "3.10.0-xt16kb"
13+
jmeAndroidNatives = "3.10.0-xt16kb-alloc"
1414

1515
[libraries]
1616

@@ -73,9 +73,10 @@ saferalloc-natives-windows-aarch64 = { module = "org.ngengine:saferalloc-natives
7373
saferalloc-natives-macos-x8664 = { module = "org.ngengine:saferalloc-natives-macos-x86_64", version.ref = "saferalloc" }
7474
saferalloc-natives-macos-aarch64 = { module = "org.ngengine:saferalloc-natives-macos-aarch64", version.ref = "saferalloc" }
7575
saferalloc-natives-android = { module = "org.ngengine:saferalloc-natives-android", version.ref = "saferalloc" }
76+
saferalloc-natives-ios = { module = "org.ngengine:saferalloc-natives-ios", version.ref = "saferalloc" }
7677

7778
[bundles]
78-
saferalloc = ["saferalloc", "saferalloc-natives-linux-x8664", "saferalloc-natives-linux-aarch64", "saferalloc-natives-windows-x8664", "saferalloc-natives-windows-aarch64", "saferalloc-natives-macos-x8664", "saferalloc-natives-macos-aarch64", "saferalloc-natives-android"]
79+
saferalloc = ["saferalloc", "saferalloc-natives-linux-x8664", "saferalloc-natives-linux-aarch64", "saferalloc-natives-windows-x8664", "saferalloc-natives-windows-aarch64", "saferalloc-natives-macos-x8664", "saferalloc-natives-macos-aarch64", "saferalloc-natives-android", "saferalloc-natives-ios"]
7980

8081
[plugins]
8182
jacoco = { id = "jacoco", version.ref = "jacoco" }

jme3-android-examples/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ dependencies {
6262
implementation project(':jme3-plugins')
6363
implementation project(':jme3-plugins-json')
6464
implementation project(':jme3-plugins-json-gson')
65+
implementation project(':jme3-saferallocator')
6566
implementation project(':jme3-terrain')
6667
implementation files(examplesJar.flatMap { it.archiveFile })
6768
}

jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
*/
6767
public abstract class AndroidHarnessFragment extends Fragment implements SystemListener {
6868
private static final Logger logger = Logger.getLogger(AndroidHarnessFragment.class.getName());
69+
private static final String SAFER_BUFFER_ALLOCATOR_CLASS = "com.jme3.util.SaferBufferAllocator";
6970

7071
protected GLSurfaceView view;
7172
protected LegacyApplication app;
@@ -90,9 +91,12 @@ public void onCreate(Bundle savedInstanceState) {
9091
logger.fine("onCreate");
9192
super.onCreate(savedInstanceState);
9293

93-
System.setProperty(
94-
BufferAllocatorFactory.PROPERTY_BUFFER_ALLOCATOR_IMPLEMENTATION,
95-
AndroidNativeBufferAllocator.class.getName());
94+
if (System.getProperty(BufferAllocatorFactory.PROPERTY_BUFFER_ALLOCATOR_IMPLEMENTATION) == null) {
95+
String allocator = isClassPresent(SAFER_BUFFER_ALLOCATOR_CLASS)
96+
? SAFER_BUFFER_ALLOCATOR_CLASS
97+
: AndroidNativeBufferAllocator.class.getName();
98+
System.setProperty(BufferAllocatorFactory.PROPERTY_BUFFER_ALLOCATOR_IMPLEMENTATION, allocator);
99+
}
96100

97101
try {
98102
app = createApplication();
@@ -112,6 +116,15 @@ public void onCreate(Bundle savedInstanceState) {
112116
*/
113117
protected abstract LegacyApplication createApplication() throws Exception;
114118

119+
private static boolean isClassPresent(String className) {
120+
try {
121+
Class.forName(className, false, AndroidHarnessFragment.class.getClassLoader());
122+
return true;
123+
} catch (Throwable ignored) {
124+
return false;
125+
}
126+
}
127+
115128

116129
@Override
117130
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

jme3-android/src/main/java/com/jme3/util/AndroidNativeBufferAllocator.java

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,37 +31,104 @@
3131
*/
3232
package com.jme3.util;
3333

34+
import java.lang.ref.PhantomReference;
35+
import java.lang.ref.ReferenceQueue;
3436
import java.nio.Buffer;
3537
import java.nio.ByteBuffer;
38+
import java.util.Map;
39+
import java.util.concurrent.ConcurrentHashMap;
40+
import java.util.concurrent.atomic.AtomicBoolean;
41+
import java.util.logging.Level;
42+
import java.util.logging.Logger;
3643

3744
/**
3845
* Allocates and destroys direct byte buffers using native code.
3946
*
4047
* @author pavl_g.
4148
*/
4249
public final class AndroidNativeBufferAllocator implements BufferAllocator {
50+
private static final Logger LOGGER = Logger.getLogger(AndroidNativeBufferAllocator.class.getName());
51+
private static final ReferenceQueue<ByteBuffer> REFERENCE_QUEUE = new ReferenceQueue<>();
52+
private static final Map<Long, Deallocator> DEALLOCATORS = new ConcurrentHashMap<>();
53+
private static final Thread CLEAN_THREAD = new Thread(AndroidNativeBufferAllocator::freeCollectedBuffers);
4354

4455
static {
4556
System.loadLibrary("bufferallocatorjme");
57+
CLEAN_THREAD.setDaemon(true);
58+
CLEAN_THREAD.setName("Android Native Buffer Deallocator");
59+
CLEAN_THREAD.start();
4660
}
4761

4862
@Override
4963
public void destroyDirectBuffer(Buffer toBeDestroyed) {
50-
releaseDirectByteBuffer(toBeDestroyed);
64+
long address = directBufferAddress(toBeDestroyed);
65+
if (address == 0L) {
66+
LOGGER.log(Level.WARNING, "Not found address of the {0}", toBeDestroyed);
67+
return;
68+
}
69+
Deallocator deallocator = DEALLOCATORS.remove(address);
70+
if (deallocator == null) {
71+
LOGGER.log(Level.WARNING, "Not found a deallocator for address {0}", address);
72+
return;
73+
}
74+
deallocator.freeNow();
5175
}
5276

5377
@Override
5478
public ByteBuffer allocate(int size) {
55-
return createDirectByteBuffer(size);
79+
ByteBuffer buffer = createDirectByteBuffer(size);
80+
if (buffer == null) {
81+
throw new OutOfMemoryError("Could not allocate " + size + " bytes through Android native allocator");
82+
}
83+
long address = directBufferAddress(buffer);
84+
if (address != 0L) {
85+
DEALLOCATORS.put(address, new Deallocator(buffer, address));
86+
}
87+
return buffer;
88+
}
89+
90+
private static void freeCollectedBuffers() {
91+
for (;;) {
92+
try {
93+
Deallocator deallocator = (Deallocator) REFERENCE_QUEUE.remove();
94+
deallocator.freeNow();
95+
} catch (InterruptedException exception) {
96+
Thread.currentThread().interrupt();
97+
break;
98+
} catch (Throwable throwable) {
99+
LOGGER.log(Level.SEVERE, "Error deallocating direct buffer", throwable);
100+
}
101+
}
102+
}
103+
104+
private static final class Deallocator extends PhantomReference<ByteBuffer> {
105+
private final long address;
106+
private final AtomicBoolean freed = new AtomicBoolean(false);
107+
108+
private Deallocator(ByteBuffer referent, long address) {
109+
super(referent, REFERENCE_QUEUE);
110+
this.address = address;
111+
}
112+
113+
private void freeNow() {
114+
if (!freed.compareAndSet(false, true)) {
115+
return;
116+
}
117+
DEALLOCATORS.remove(address, this);
118+
clear();
119+
releaseDirectByteBufferAddress(address);
120+
}
56121
}
57122

58123
/**
59-
* Releases the memory of a direct buffer using a buffer object reference.
124+
* Releases the memory of a direct buffer using its native address.
60125
*
61-
* @param buffer the buffer reference to release its memory.
126+
* @param address the native address to release
62127
* @see AndroidNativeBufferAllocator#destroyDirectBuffer(Buffer)
63128
*/
64-
private native void releaseDirectByteBuffer(Buffer buffer);
129+
private static native void releaseDirectByteBufferAddress(long address);
130+
131+
private static native long directBufferAddress(Buffer buffer);
65132

66133
/**
67134
* Creates a new direct byte buffer explicitly with a specific size.

jme3-core/src/main/java/com/jme3/util/ReflectionAllocator.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141
/**
4242
* This class contains the reflection based way to remove DirectByteBuffers in
4343
* java, allocation is done via ByteBuffer.allocateDirect
44+
* @deprecated This class relies on internal APIs that are not accessible in Java 9+, and is not thread-safe for allocation bookkeeping. Use {@code com.jme3.util.SaferBufferAllocator} from the {@code jme3-saferallocator} module instead.
4445
*/
46+
@Deprecated
4547
public final class ReflectionAllocator implements BufferAllocator {
4648
private static Method cleanerMethod = null;
4749
private static Method cleanMethod = null;

jme3-ios-examples/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ sourceSets {
303303
dependencies {
304304
implementation project(':jme3-core')
305305
implementation project(':jme3-ios')
306+
implementation project(':jme3-saferallocator')
306307
implementation libs.libjglios.core.ios
307308
implementation libs.libjglios.gles.ios
308309
implementation libs.libjglios.sdl3.ios

jme3-ios/src/main/java/com/jme3/system/ios/IGLESContext.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public class IGLESContext implements JmeContext {
6868

6969
private static final String BLIT_MATERIAL = "Common/MatDefs/Blit/Blit.j3md";
7070
private static final Logger logger = Logger.getLogger(IGLESContext.class.getName());
71+
private static final String SAFER_BUFFER_ALLOCATOR_CLASS = "com.jme3.util.SaferBufferAllocator";
7172
protected final AtomicBoolean created = new AtomicBoolean(false);
7273
protected final AtomicBoolean renderable = new AtomicBoolean(false);
7374
protected final AtomicBoolean needClose = new AtomicBoolean(false);
@@ -102,7 +103,20 @@ public class IGLESContext implements JmeContext {
102103
final String implementation = BufferAllocatorFactory.PROPERTY_BUFFER_ALLOCATOR_IMPLEMENTATION;
103104

104105
if (System.getProperty(implementation) == null) {
105-
System.setProperty(implementation, LibJGLIOSNativeBufferAllocator.class.getName());
106+
if (isClassPresent(SAFER_BUFFER_ALLOCATOR_CLASS)) {
107+
System.setProperty(implementation, SAFER_BUFFER_ALLOCATOR_CLASS);
108+
} else {
109+
System.setProperty(implementation, LibJGLIOSNativeBufferAllocator.class.getName());
110+
}
111+
}
112+
}
113+
114+
private static boolean isClassPresent(String className) {
115+
try {
116+
Class.forName(className, false, IGLESContext.class.getClassLoader());
117+
return true;
118+
} catch (Throwable ignored) {
119+
return false;
106120
}
107121
}
108122

jme3-ios/src/main/java/com/jme3/util/LibJGLIOSNativeBufferAllocator.java

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,43 @@
44
*/
55
package com.jme3.util;
66

7+
import java.lang.ref.PhantomReference;
8+
import java.lang.ref.ReferenceQueue;
79
import java.nio.Buffer;
810
import java.nio.ByteBuffer;
11+
import java.util.Map;
12+
import java.util.concurrent.ConcurrentHashMap;
13+
import java.util.concurrent.atomic.AtomicBoolean;
14+
import java.util.logging.Level;
15+
import java.util.logging.Logger;
916
import org.ngengine.libjglios.core.LibJGLIOSBufferAllocator;
1017

1118

1219
public final class LibJGLIOSNativeBufferAllocator implements BufferAllocator {
20+
private static final Logger LOGGER = Logger.getLogger(LibJGLIOSNativeBufferAllocator.class.getName());
21+
private static final ReferenceQueue<ByteBuffer> REFERENCE_QUEUE = new ReferenceQueue<>();
22+
private static final Map<Long, Deallocator> DEALLOCATORS = new ConcurrentHashMap<>();
23+
private static final Thread CLEAN_THREAD = new Thread(LibJGLIOSNativeBufferAllocator::freeCollectedBuffers);
24+
25+
static {
26+
CLEAN_THREAD.setDaemon(true);
27+
CLEAN_THREAD.setName("libJGLIOS Native Buffer Deallocator");
28+
CLEAN_THREAD.start();
29+
}
1330

1431
@Override
1532
public void destroyDirectBuffer(Buffer toBeDestroyed) {
16-
LibJGLIOSBufferAllocator.free(toBeDestroyed);
33+
long address = LibJGLIOSBufferAllocator.baseAddress(toBeDestroyed);
34+
if (address == 0L) {
35+
LOGGER.log(Level.WARNING, "Not found address of the {0}", toBeDestroyed);
36+
return;
37+
}
38+
Deallocator deallocator = DEALLOCATORS.remove(address);
39+
if (deallocator == null) {
40+
LOGGER.log(Level.WARNING, "Not found a deallocator for address {0}", address);
41+
return;
42+
}
43+
deallocator.freeNow();
1744
}
1845

1946
@Override
@@ -22,6 +49,43 @@ public ByteBuffer allocate(int size) {
2249
if (buffer == null) {
2350
throw new OutOfMemoryError("Could not allocate " + size + " bytes through libJGLIOS");
2451
}
52+
long address = LibJGLIOSBufferAllocator.baseAddress(buffer);
53+
if (address != 0L) {
54+
DEALLOCATORS.put(address, new Deallocator(buffer, address));
55+
}
2556
return buffer;
2657
}
58+
59+
private static void freeCollectedBuffers() {
60+
for (;;) {
61+
try {
62+
Deallocator deallocator = (Deallocator) REFERENCE_QUEUE.remove();
63+
deallocator.freeNow();
64+
} catch (InterruptedException exception) {
65+
Thread.currentThread().interrupt();
66+
break;
67+
} catch (Throwable throwable) {
68+
LOGGER.log(Level.SEVERE, "Error deallocating direct buffer", throwable);
69+
}
70+
}
71+
}
72+
73+
private static final class Deallocator extends PhantomReference<ByteBuffer> {
74+
private final long address;
75+
private final AtomicBoolean freed = new AtomicBoolean(false);
76+
77+
private Deallocator(ByteBuffer referent, long address) {
78+
super(referent, REFERENCE_QUEUE);
79+
this.address = address;
80+
}
81+
82+
private void freeNow() {
83+
if (!freed.compareAndSet(false, true)) {
84+
return;
85+
}
86+
DEALLOCATORS.remove(address, this);
87+
clear();
88+
LibJGLIOSBufferAllocator.freeAddress(address);
89+
}
90+
}
2791
}

jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
import com.jme3.util.BufferAllocatorFactory;
5252
import com.jme3.util.BufferUtils;
5353
import com.jme3.util.LWJGLBufferAllocator;
54-
import com.jme3.util.LWJGLBufferAllocator.ConcurrentLWJGLBufferAllocator;
5554
import com.jme3.util.LWJGLSaferAllocMemoryAllocator;
5655

5756
import java.nio.IntBuffer;
@@ -83,6 +82,8 @@ public abstract class LwjglContext implements JmeContext {
8382
protected boolean useAngle = false;
8483

8584
private static final Logger logger = Logger.getLogger(LwjglContext.class.getName());
85+
private static final String CONCURRENT_LWJGL_BUFFER_ALLOCATOR_CLASS =
86+
"com.jme3.util.LWJGLBufferAllocator$ConcurrentLWJGLBufferAllocator";
8687

8788
static {
8889
final String implementation = BufferAllocatorFactory.PROPERTY_BUFFER_ALLOCATOR_IMPLEMENTATION;
@@ -95,8 +96,8 @@ public abstract class LwjglContext implements JmeContext {
9596
LWJGLSaferAllocMemoryAllocator.SAFER_BUFFER_ALLOCATOR_CLASS);
9697
}
9798
} else if (configuredImplementation == null) {
98-
if (Boolean.parseBoolean(System.getProperty(PROPERTY_CONCURRENT_BUFFER_ALLOCATOR, "true"))) {
99-
System.setProperty(implementation, ConcurrentLWJGLBufferAllocator.class.getName());
99+
if (Boolean.parseBoolean(System.getProperty(PROPERTY_CONCURRENT_BUFFER_ALLOCATOR, "false"))) {
100+
System.setProperty(implementation, CONCURRENT_LWJGL_BUFFER_ALLOCATOR_CLASS);
100101
} else {
101102
System.setProperty(implementation, LWJGLBufferAllocator.class.getName());
102103
}

0 commit comments

Comments
 (0)