Skip to content

Commit f4b50af

Browse files
committed
Add graalvm basic example
1 parent 5c969ca commit f4b50af

8 files changed

Lines changed: 294 additions & 1 deletion

File tree

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
- `./gradlew :examples:basic:platforms:desktop:teavm-c:builder:basic_desktop_c_generate`
2828
- `./gradlew :examples:basic:platforms:desktop:teavm-c:builder:basic_desktop_c_debug_build`
2929
- `./gradlew :examples:basic:platforms:desktop:teavm-c:builder:basic_desktop_c_debug_run`
30+
- GraalVM Native Image basic example:
31+
- `./gradlew :examples:basic:platforms:desktop:graalvm:basic_desktop_graalvm_build`
32+
- `./gradlew :examples:basic:platforms:desktop:graalvm:basic_desktop_graalvm_run`
3033
- Gradle plugin basic example:
3134
- `./gradlew :examples:basic:platforms:web:plugin:gdx_teavm_web_js_run`
3235
- `./gradlew :examples:basic:platforms:web:plugin:gdx_teavm_web_wasm_run`

examples/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ Only leaf projects under `platforms` contain launchers and runnable Gradle tasks
99
|-- core/ # Portable application code
1010
`-- platforms/
1111
|-- desktop/
12-
| |-- lwjgl3/
12+
| |-- lwjgl3/ # JVM/LWJGL3
13+
| |-- graalvm/ # GraalVM Native Image/LWJGL3
1314
| `-- teavm-c/
1415
| |-- builder/ # Manual TeaBuilder API
1516
| `-- plugin/ # gdx-teavm Gradle plugin

examples/basic/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Portable test applications live in `core`, shared runtime assets live in `assets
55
| Platform | Gradle project | Representative task |
66
| --- | --- | --- |
77
| Desktop LWJGL3 | `:examples:basic:platforms:desktop:lwjgl3` | `basic_desktop_run` |
8+
| Desktop GraalVM Native Image | `:examples:basic:platforms:desktop:graalvm` | `basic_desktop_graalvm_run` |
89
| Desktop TeaVM C builder | `:examples:basic:platforms:desktop:teavm-c:builder` | `basic_desktop_c_debug_build` |
910
| Desktop TeaVM C plugin | `:examples:basic:platforms:desktop:teavm-c:plugin` | `gdx_teavm_glfw_build` |
1011
| Web builder | `:examples:basic:platforms:web:builder` | `basic_web_run` |
@@ -14,4 +15,27 @@ Portable test applications live in `core`, shared runtime assets live in `assets
1415

1516
See the [manual TeaVM C guide](platforms/desktop/teavm-c/builder/README.md) for native toolchain requirements.
1617

18+
## GraalVM speed comparison
19+
20+
The GraalVM launcher runs the same `SpriteBatchTest` as the regular LWJGL3 and TeaVM C launchers. Each launcher disables VSync and removes the foreground FPS cap, while `SpriteBatchTest` prints a one-second FPS sample to the console.
21+
22+
Point `JAVA_HOME` and `GRAALVM_HOME` at a GraalVM JDK that includes Native Image, then build or run the optimized native executable:
23+
24+
```bash
25+
./gradlew :examples:basic:platforms:desktop:graalvm:basic_desktop_graalvm_build
26+
./gradlew :examples:basic:platforms:desktop:graalvm:basic_desktop_graalvm_run
27+
```
28+
29+
The executable and copied assets are written to `platforms/desktop/graalvm/build/native/nativeCompile`. For a JVM-side launcher check, or to compare the GraalVM JIT when Gradle itself runs on GraalVM, use `basic_desktop_graalvm_jvm_run`.
30+
31+
For a quick side-by-side run of the same workload, compare the one-second FPS samples printed by these tasks after allowing each runtime to warm up:
32+
33+
```bash
34+
./gradlew :examples:basic:platforms:desktop:lwjgl3:basic_desktop_run
35+
./gradlew :examples:basic:platforms:desktop:graalvm:basic_desktop_graalvm_run
36+
./gradlew :examples:basic:platforms:desktop:teavm-c:plugin:gdx_teavm_glfw_release_aggressive_run
37+
```
38+
39+
Native Image builds for the current operating system only. This comparison build also uses `-march=native`, so its executable targets the build machine's CPU rather than a broadly compatible distribution target. On Windows, run Gradle from a Visual Studio x64 Native Tools environment if Native Image cannot find the C++ toolchain.
40+
1741
The basic web plugin build also demonstrates shared `webDefaults {}` plus named JS and Wasm release variants. Their build tasks are `gdx_teavm_web_js_release_build` and `gdx_teavm_web_wasm_release_build`. The TeaVM C plugin build demonstrates `nativeDefaults {}` and a named GLFW release variant with `gdx_teavm_glfw_release_generate`, `gdx_teavm_glfw_release_build`, and `gdx_teavm_glfw_release_run`.
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import java.util.Locale
2+
3+
plugins {
4+
id("application")
5+
}
6+
7+
val mainClassName = "Main"
8+
val nativeImageName = "basic-desktop-graalvm"
9+
val assetsDir = file("../../../assets")
10+
val nativeOutputDir = layout.buildDirectory.dir("native/nativeCompile")
11+
val isWindows = System.getProperty("os.name").startsWith("Windows", ignoreCase = true)
12+
val nativeExecutableName = if(isWindows) "$nativeImageName.exe" else nativeImageName
13+
val nativeExecutable = nativeOutputDir.map { it.file(nativeExecutableName) }
14+
val nativeImageOutputBase = nativeOutputDir.map { it.file(nativeImageName) }
15+
val nativeImageArgsFile = layout.buildDirectory.file("tmp/nativeCompile/native-image.args")
16+
val exampleExitAfterSeconds = providers.gradleProperty("exampleExitAfterSeconds")
17+
val nativeImageThreads = providers.gradleProperty("nativeImageThreads").orElse("2")
18+
val nativeImageBuilderMaxHeap = providers.gradleProperty("nativeImageBuilderMaxHeap").orElse("4g")
19+
20+
fun lwjglNativesClassifier(): String {
21+
val osName = System.getProperty("os.name").lowercase(Locale.ROOT)
22+
val architecture = System.getProperty("os.arch").lowercase(Locale.ROOT)
23+
val isX64 = architecture == "amd64" || architecture == "x86_64"
24+
val isX86 = architecture == "x86" || architecture == "i386"
25+
val isArm64 = architecture == "aarch64" || architecture == "arm64"
26+
val isArm32 = architecture.startsWith("arm") && !isArm64
27+
28+
return when {
29+
osName.startsWith("windows") && isX64 -> "natives-windows"
30+
osName.startsWith("windows") && isX86 -> "natives-windows-x86"
31+
osName.startsWith("mac") && isX64 -> "natives-macos"
32+
osName.startsWith("mac") && isArm64 -> "natives-macos-arm64"
33+
osName.startsWith("linux") && isX64 -> "natives-linux"
34+
osName.startsWith("linux") && isArm64 -> "natives-linux-arm64"
35+
osName.startsWith("linux") && isArm32 -> "natives-linux-arm32"
36+
else -> throw GradleException(
37+
"Unsupported LWJGL native platform: ${System.getProperty("os.name")} $architecture"
38+
)
39+
}
40+
}
41+
42+
val lwjglNatives = lwjglNativesClassifier()
43+
44+
application {
45+
mainClass.set(mainClassName)
46+
}
47+
48+
dependencies {
49+
implementation(variantOf(libs.gdxPlatform) { classifier("natives-desktop") })
50+
implementation(libs.gdxBackendLwjgl3)
51+
implementation(variantOf(libs.gdxBox2dPlatform) { classifier("natives-desktop") })
52+
implementation(variantOf(libs.gdxFreetypePlatform) { classifier("natives-desktop") })
53+
implementation(project(":examples:basic:core"))
54+
55+
runtimeOnly(variantOf(libs.lwjglCore) { classifier(lwjglNatives) })
56+
runtimeOnly(variantOf(libs.lwjglGlfw) { classifier(lwjglNatives) })
57+
runtimeOnly(variantOf(libs.lwjglJemalloc) { classifier(lwjglNatives) })
58+
runtimeOnly(variantOf(libs.lwjglOpenal) { classifier(lwjglNatives) })
59+
runtimeOnly(variantOf(libs.lwjglOpengl) { classifier(lwjglNatives) })
60+
runtimeOnly(variantOf(libs.lwjglStb) { classifier(lwjglNatives) })
61+
}
62+
63+
tasks.register<JavaExec>("basic_desktop_graalvm_jvm_run") {
64+
dependsOn("classes")
65+
group = "example-desktop-graalvm"
66+
description = "Run the GraalVM module's SpriteBatchTest launcher on the current JVM"
67+
mainClass.set(mainClassName)
68+
classpath = sourceSets["main"].runtimeClasspath
69+
workingDir = assetsDir
70+
jvmArgs("--enable-native-access=ALL-UNNAMED")
71+
72+
if(System.getProperty("os.name").startsWith("Mac", ignoreCase = true)) {
73+
jvmArgs("-XstartOnFirstThread")
74+
}
75+
doFirst {
76+
if(exampleExitAfterSeconds.isPresent) {
77+
args("--exit-after-seconds=${exampleExitAfterSeconds.get()}")
78+
}
79+
}
80+
}
81+
82+
tasks.named<JavaExec>("run") {
83+
workingDir = assetsDir
84+
jvmArgs("--enable-native-access=ALL-UNNAMED")
85+
if(System.getProperty("os.name").startsWith("Mac", ignoreCase = true)) {
86+
jvmArgs("-XstartOnFirstThread")
87+
}
88+
}
89+
90+
val nativeCompile = tasks.register<Exec>("nativeCompile") {
91+
dependsOn("classes")
92+
group = "build"
93+
description = "Compile the GraalVM Native Image SpriteBatchTest executable"
94+
inputs.files(sourceSets["main"].runtimeClasspath)
95+
inputs.property("nativeImageThreads", nativeImageThreads)
96+
inputs.property("nativeImageBuilderMaxHeap", nativeImageBuilderMaxHeap)
97+
outputs.file(nativeExecutable)
98+
99+
doFirst {
100+
val graalvmHome = providers.environmentVariable("GRAALVM_HOME")
101+
.orElse(providers.environmentVariable("JAVA_HOME"))
102+
.orElse(providers.systemProperty("java.home"))
103+
.get()
104+
val nativeImageCommand = file("$graalvmHome/bin/${if(isWindows) "native-image.cmd" else "native-image"}")
105+
if(!nativeImageCommand.isFile) {
106+
throw GradleException(
107+
"Native Image was not found at ${nativeImageCommand.absolutePath}. " +
108+
"Set GRAALVM_HOME or JAVA_HOME to a GraalVM JDK that includes Native Image."
109+
)
110+
}
111+
112+
nativeOutputDir.get().asFile.mkdirs()
113+
val nativeImageArgs = listOf(
114+
"--no-fallback",
115+
"--parallelism=${nativeImageThreads.get()}",
116+
"-J-Xmx${nativeImageBuilderMaxHeap.get()}",
117+
"-O3",
118+
"-march=native",
119+
"--enable-native-access=ALL-UNNAMED",
120+
"-cp",
121+
sourceSets["main"].runtimeClasspath.asPath,
122+
"-o",
123+
nativeImageOutputBase.get().asFile.absolutePath,
124+
mainClassName
125+
)
126+
val argsFile = nativeImageArgsFile.get().asFile
127+
argsFile.parentFile.mkdirs()
128+
argsFile.writeText(nativeImageArgs.joinToString(System.lineSeparator()) { argument ->
129+
"\"${argument.replace("\\", "\\\\").replace("\"", "\\\"")}\""
130+
})
131+
commandLine(nativeImageCommand.absolutePath, "@${argsFile.absolutePath}")
132+
}
133+
}
134+
135+
val copyBasicAssetsToGraalVM = tasks.register<Copy>("copyBasicAssetsToGraalVM") {
136+
dependsOn(nativeCompile)
137+
from(assetsDir)
138+
into(nativeOutputDir)
139+
}
140+
141+
tasks.register("basic_desktop_graalvm_build") {
142+
dependsOn(copyBasicAssetsToGraalVM)
143+
group = "example-desktop-graalvm"
144+
description = "Build the optimized GraalVM Native Image SpriteBatchTest executable and copy its assets"
145+
}
146+
147+
tasks.register<Exec>("basic_desktop_graalvm_run") {
148+
dependsOn(copyBasicAssetsToGraalVM)
149+
group = "example-desktop-graalvm"
150+
description = "Build and run SpriteBatchTest as an optimized GraalVM native executable"
151+
executable = nativeExecutable.get().asFile.absolutePath
152+
workingDir = nativeOutputDir.get().asFile
153+
154+
doFirst {
155+
if(exampleExitAfterSeconds.isPresent) {
156+
args("--exit-after-seconds=${exampleExitAfterSeconds.get()}")
157+
}
158+
}
159+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import com.badlogic.gdx.ApplicationListener;
2+
import com.badlogic.gdx.Gdx;
3+
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application;
4+
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration;
5+
import com.github.xpenatan.gdx.teavm.examples.basic.tests.webgl.SpriteBatchTest;
6+
7+
public class Main {
8+
9+
private static final String LWJGL_JNI_FUNCTION_COUNT = "org.lwjgl.system.JNINativeInterfaceSize";
10+
// LWJGL 3.3.3 predates GraalVM/JDK 25, whose JNI table currently has 232 callable entries.
11+
private static final String GRAALVM_JNI_FUNCTION_COUNT = "232";
12+
private static final String EXIT_AFTER_SECONDS_ARGUMENT = "--exit-after-seconds=";
13+
14+
public static void main(String[] args) {
15+
if(System.getProperty(LWJGL_JNI_FUNCTION_COUNT) == null) {
16+
System.setProperty(LWJGL_JNI_FUNCTION_COUNT, GRAALVM_JNI_FUNCTION_COUNT);
17+
}
18+
19+
Lwjgl3ApplicationConfiguration config = new Lwjgl3ApplicationConfiguration();
20+
config.useVsync(false);
21+
config.setForegroundFPS(0);
22+
23+
ApplicationListener listener = new SpriteBatchTest();
24+
float exitAfterSeconds = getExitAfterSeconds(args);
25+
if(exitAfterSeconds > 0f) {
26+
listener = new AutoExitListener(listener, exitAfterSeconds);
27+
}
28+
29+
new Lwjgl3Application(listener, config);
30+
}
31+
32+
private static float getExitAfterSeconds(String[] args) {
33+
for(String arg : args) {
34+
if(arg.startsWith(EXIT_AFTER_SECONDS_ARGUMENT)) {
35+
return Float.parseFloat(arg.substring(EXIT_AFTER_SECONDS_ARGUMENT.length()));
36+
}
37+
}
38+
return 0f;
39+
}
40+
41+
private static class AutoExitListener implements ApplicationListener {
42+
private final ApplicationListener listener;
43+
private final float exitAfterSeconds;
44+
private float elapsedSeconds;
45+
46+
private AutoExitListener(ApplicationListener listener, float exitAfterSeconds) {
47+
this.listener = listener;
48+
this.exitAfterSeconds = exitAfterSeconds;
49+
}
50+
51+
@Override
52+
public void create() {
53+
listener.create();
54+
}
55+
56+
@Override
57+
public void resize(int width, int height) {
58+
listener.resize(width, height);
59+
}
60+
61+
@Override
62+
public void render() {
63+
listener.render();
64+
elapsedSeconds += Gdx.graphics.getDeltaTime();
65+
if(elapsedSeconds >= exitAfterSeconds) {
66+
Gdx.app.exit();
67+
}
68+
}
69+
70+
@Override
71+
public void pause() {
72+
listener.pause();
73+
}
74+
75+
@Override
76+
public void resume() {
77+
listener.resume();
78+
}
79+
80+
@Override
81+
public void dispose() {
82+
listener.dispose();
83+
}
84+
}
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"resources": {
3+
"includes": [
4+
{
5+
"pattern": ".*\\.(dll|so|dylib)$"
6+
},
7+
{
8+
"pattern": "META-INF/.*\\.(sha1|git)$"
9+
}
10+
]
11+
}
12+
}

gradle/libs.versions.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ junit = "4.13.2"
1717
fleks = "2.12"
1818
visUi = "1.5.8"
1919
artemisOdb = "2.3.0"
20+
lwjgl = "3.3.3"
2021
easyPublishing = "0.1.2"
2122
androidGradlePlugin = "9.1.1"
2223

@@ -54,6 +55,13 @@ fleks = { module = "io.github.quillraven.fleks:Fleks", version.ref = "fleks" }
5455
visUi = { module = "com.kotcrab.vis:vis-ui", version.ref = "visUi" }
5556
artemisOdb = { module = "net.onedaybeard.artemis:artemis-odb", version.ref = "artemisOdb" }
5657

58+
lwjglCore = { module = "org.lwjgl:lwjgl", version.ref = "lwjgl" }
59+
lwjglGlfw = { module = "org.lwjgl:lwjgl-glfw", version.ref = "lwjgl" }
60+
lwjglJemalloc = { module = "org.lwjgl:lwjgl-jemalloc", version.ref = "lwjgl" }
61+
lwjglOpenal = { module = "org.lwjgl:lwjgl-openal", version.ref = "lwjgl" }
62+
lwjglOpengl = { module = "org.lwjgl:lwjgl-opengl", version.ref = "lwjgl" }
63+
lwjglStb = { module = "org.lwjgl:lwjgl-stb", version.ref = "lwjgl" }
64+
5765
[bundles]
5866
teavm = ["teavmTooling", "teavmCore", "teavmClasslib", "teavmExtensionSpi", "teavmJsoCore", "teavmJsoApis", "teavmJsoImpl"]
5967
jetty = ["jettyServer", "jettyWebapp"]

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ include(":extensions:web:gdx-freetype-web")
6161
include(":examples:shared")
6262
include(":examples:basic:core")
6363
include(":examples:basic:platforms:desktop:lwjgl3")
64+
include(":examples:basic:platforms:desktop:graalvm")
6465
include(":examples:basic:platforms:desktop:teavm-c:builder")
6566
include(":examples:basic:platforms:desktop:teavm-c:plugin")
6667
include(":examples:basic:platforms:web:builder")

0 commit comments

Comments
 (0)