Source code for GLES2 portions of SusuJava
./susuwu/sdl_gles2_jni.c:
/* JNI native implementation for `susuwu.SdlGles2`. Build: see `build.sh`. */
/* Usage: compile as shared library, then `System.loadLibrary("sdl_gles2_jni")` loads this. */
#include <jni.h> /* JNI types + macros */
#include <SDL2/SDL.h> /* SDL_Init, SDL_CreateWindow, SDL_GL_CreateContext, SDL_PollEvent, SDL_GL_SwapWindow, SDL_Quit */
#include <GLES2/gl2.h> /* glClear, glClearColor, glCreateShader, glCreateProgram, glDrawArrays, ... */
#include <stdio.h> /* fprintf, stderr */
#include <string.h> /* NULL */
static SDL_Window *g_window = NULL;
static SDL_GLContext g_context = NULL;
static GLuint g_program = 0;
static GLint g_posAttrib = -1;
static GLint g_colorUniform = -1;
static GLint g_resolutionUniform = -1;
static int g_width = 0, g_height = 0;
/* Minimal vertex shader: converts pixel coords to clip space, flips Y so (0,0) is top-left (matches JavaFX canvas). */
static const char *VERT_SRC =
"attribute vec2 a_position;\n"
"uniform vec2 u_resolution;\n"
"void main() {\n"
" vec2 zeroToOne = a_position / u_resolution;\n"
" vec2 clipSpace = zeroToOne * 2.0 - 1.0;\n"
" gl_Position = vec4(clipSpace * vec2(1.0, -1.0), 0.0, 1.0);\n"
"}\n";
/* Minimal fragment shader: outputs a uniform solid color. */
static const char *FRAG_SRC =
"precision mediump float;\n"
"uniform vec4 u_color;\n"
"void main() {\n"
" gl_FragColor = u_color;\n"
"}\n";
static GLuint compile_shader(GLenum type, const char *src) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &src, NULL);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if(!compiled) {
char log[512];
glGetShaderInfoLog(shader, sizeof(log), NULL, log);
fprintf(stderr, "sdl_gles2_jni: shader compile error: %s\n", log);
glDeleteShader(shader);
return 0;
}
return shader;
}
/* Java_susuwu_SdlGles2_init: creates SDL2 window + GLES2 context + compiles shaders. */
JNIEXPORT jboolean JNICALL Java_susuwu_SdlGles2_init(JNIEnv *env, jclass cls, jint width, jint height, jstring jtitle) {
if(SDL_Init(SDL_INIT_VIDEO) < 0) {
fprintf(stderr, "sdl_gles2_jni: SDL_Init failed: %s\n", SDL_GetError());
return JNI_FALSE;
}
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
const char *title = (*env)->GetStringUTFChars(env, jtitle, NULL);
g_window = SDL_CreateWindow(title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
(int)width, (int)height, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN);
(*env)->ReleaseStringUTFChars(env, jtitle, title);
if(!g_window) {
fprintf(stderr, "sdl_gles2_jni: SDL_CreateWindow failed: %s\n", SDL_GetError());
SDL_Quit();
return JNI_FALSE;
}
g_context = SDL_GL_CreateContext(g_window);
if(!g_context) {
fprintf(stderr, "sdl_gles2_jni: SDL_GL_CreateContext failed: %s\n", SDL_GetError());
SDL_DestroyWindow(g_window);
g_window = NULL;
SDL_Quit();
return JNI_FALSE;
}
g_width = (int)width;
g_height = (int)height;
GLuint vert = compile_shader(GL_VERTEX_SHADER, VERT_SRC);
GLuint frag = compile_shader(GL_FRAGMENT_SHADER, FRAG_SRC);
if(!vert || !frag) {
if(vert) glDeleteShader(vert);
if(frag) glDeleteShader(frag);
SDL_GL_DeleteContext(g_context); g_context = NULL;
SDL_DestroyWindow(g_window); g_window = NULL;
SDL_Quit();
return JNI_FALSE;
}
g_program = glCreateProgram();
glAttachShader(g_program, vert);
glAttachShader(g_program, frag);
glLinkProgram(g_program);
glDeleteShader(vert);
glDeleteShader(frag);
GLint linked = 0;
glGetProgramiv(g_program, GL_LINK_STATUS, &linked);
if(!linked) {
char log[512];
glGetProgramInfoLog(g_program, sizeof(log), NULL, log);
fprintf(stderr, "sdl_gles2_jni: program link error: %s\n", log);
glDeleteProgram(g_program); g_program = 0;
SDL_GL_DeleteContext(g_context); g_context = NULL;
SDL_DestroyWindow(g_window); g_window = NULL;
SDL_Quit();
return JNI_FALSE;
}
g_posAttrib = glGetAttribLocation(g_program, "a_position");
g_colorUniform = glGetUniformLocation(g_program, "u_color");
g_resolutionUniform = glGetUniformLocation(g_program, "u_resolution");
glUseProgram(g_program);
glUniform2f(g_resolutionUniform, (float)g_width, (float)g_height);
glViewport(0, 0, g_width, g_height);
return JNI_TRUE;
}
/* Java_susuwu_SdlGles2_destroy: tears down GLES2 + SDL2. */
JNIEXPORT void JNICALL Java_susuwu_SdlGles2_destroy(JNIEnv *env, jclass cls) {
if(g_program) { glDeleteProgram(g_program); g_program = 0; }
if(g_context) { SDL_GL_DeleteContext(g_context); g_context = NULL; }
if(g_window) { SDL_DestroyWindow(g_window); g_window = NULL; }
SDL_Quit();
}
/* Java_susuwu_SdlGles2_pollQuit: drains SDL event queue; returns JNI_TRUE if app should exit. */
JNIEXPORT jboolean JNICALL Java_susuwu_SdlGles2_pollQuit(JNIEnv *env, jclass cls) {
SDL_Event event;
while(SDL_PollEvent(&event)) {
if(SDL_QUIT == event.type) {
return JNI_TRUE;
}
if(SDL_KEYDOWN == event.type && SDLK_ESCAPE == event.key.keysym.sym) {
return JNI_TRUE;
}
}
return JNI_FALSE;
}
JNIEXPORT void JNICALL Java_susuwu_SdlGles2_glClearColor(JNIEnv *env, jclass cls, jfloat r, jfloat g, jfloat b, jfloat a) {
glClearColor(r, g, b, a);
}
JNIEXPORT void JNICALL Java_susuwu_SdlGles2_glClear(JNIEnv *env, jclass cls, jint mask) {
glClear((GLbitfield)mask);
}
JNIEXPORT void JNICALL Java_susuwu_SdlGles2_swapWindow(JNIEnv *env, jclass cls) {
SDL_GL_SwapWindow(g_window);
}
JNIEXPORT void JNICALL Java_susuwu_SdlGles2_setWindowTitle(JNIEnv *env, jclass cls, jstring jtitle) {
if(!g_window) { return; }
const char *title = (*env)->GetStringUTFChars(env, jtitle, NULL);
SDL_SetWindowTitle(g_window, title);
(*env)->ReleaseStringUTFChars(env, jtitle, title);
}
/* Java_susuwu_SdlGles2_drawFilledPolygon: draws pre-triangulated vertices (multiples of 3) in screen coords with solid color. */
JNIEXPORT void JNICALL Java_susuwu_SdlGles2_drawFilledPolygon(JNIEnv *env, jclass cls, jfloatArray jverts, jfloat r, jfloat g, jfloat b, jfloat a) {
jsize len = (*env)->GetArrayLength(env, jverts);
jfloat *verts = (*env)->GetFloatArrayElements(env, jverts, NULL);
glUseProgram(g_program);
glUniform4f(g_colorUniform, r, g, b, a);
glVertexAttribPointer(g_posAttrib, 2, GL_FLOAT, GL_FALSE, 0, verts);
glEnableVertexAttribArray(g_posAttrib);
glDrawArrays(GL_TRIANGLES, 0, (GLsizei)(len / 2));
glDisableVertexAttribArray(g_posAttrib);
(*env)->ReleaseFloatArrayElements(env, jverts, verts, JNI_ABORT);
}
./susuwu/SdlGles2.java:
package susuwu; /* Usage: `import susuwu.SdlGles2;` */
/**
* {@code class SdlGles2} is a thin JNI bridge to SDL2 and OpenGL ES 2.0.
* Replaces JavaFX {@code Stage}, {@code Scene}, {@code Canvas}, {@code GraphicsContext}, {@code AnimationTimer}, {@code Timeline}.
* Usage: {@code SdlGles2.init(width, height, title);} then render loop, then {@code SdlGles2.destroy();}
*/
public class SdlGles2 {
static {
System.loadLibrary("sdl_gles2_jni"); /* Loads `libsdl_gles2_jni.so` (or `.dll`/`.dylib`). Build: see `build.sh`. */
}
public static final int GL_COLOR_BUFFER_BIT = 0x00004000; /* Matches `GL_COLOR_BUFFER_BIT` from `<GLES2/gl2.h>` */
/** Creates the SDL2 window + OpenGL ES 2.0 context. Returns {@code true} on success. Replaces {@code Stage}, {@code Scene}. */
public static native boolean init(int width, int height, String title);
/** Destroys the SDL2 window + context. Calls {@code SDL_Quit()}. Replaces {@code stage.close()}. */
public static native void destroy();
/** Polls SDL events; returns {@code true} if SDL_QUIT or Escape was received (main loop should exit). Replaces {@code AnimationTimer}/{@code Timeline} termination. */
public static native boolean pollQuit();
/** Sets the GLES2 clear color. Replaces {@code Scene} background color. */
public static native void glClearColor(float r, float g, float b, float a);
/** Clears the GLES2 framebuffer. {@code mask} should be {@link #GL_COLOR_BUFFER_BIT}. Replaces {@code gc.clearRect()}. */
public static native void glClear(int mask);
/** Swaps front/back buffers (presents the rendered frame). Replaces implicit JavaFX frame commit. */
public static native void swapWindow();
/** Sets the window title. Used for FPS text display. Replaces {@code javafx.scene.text.Text}. */
public static native void setWindowTitle(String title);
/**
* Draws a filled polygon as triangles in screen space.
* {@code vertices}: interleaved {@code [x0,y0, x1,y1, ...]} in pixels (already transformed to screen coords).
* Vertex count must be a multiple of 3 (pre-triangulated input). Replaces {@code gc.beginPath/moveTo/lineTo/fill}.
*/
public static native void drawFilledPolygon(float[] vertices, float r, float g, float b, float a);
}
./build.sh:
#!/bin/sh
#
# /* This is the new build script for `./susuwu/FishSim.java`. */
PATH_TO_CLASS="susuwu/FishSim"
PATH_TO_SOURCE="${PATH_TO_CLASS}.java"
PATH_TO_NATIVE="susuwu/sdl_gles2_jni.c"
PATH_TO_NATIVE_LIB="susuwu/libsdl_gles2_jni.so" # /* `.so` on Linux/Android, `.dll` on Windows, `.dylib` on macOS */
JAVA_FLAGS="${JAVA_FLAGS} -enableassertions" # /* Notice: remove `-enableassertions` so performance improves */
JAVA_FLAGS="${JAVA_FLAGS} -Djava.library.path=susuwu" # /* Allows JNI to find `libsdl_gles2_jni.so` */
export JAVA_BUILD_TEST_FLAGS="-verbose"
export JAVA_TEST_FLAGS="-verbose:module"
if command -v sudo >/dev/null; then
APTITUDE="sudo apt -y install "
else
APTITUDE="apt -y install " # /* Fixes "The program sudo is not installed." on platforms such as smartphones */
fi
command -v java >/dev/null || ${APTITUDE} openjdk-21-jdk-headless || ${APTITUDE} default-jdk-headless
if ! dpkg -l libsdl2-dev >/dev/null 2>&1; then # /* Install SDL2 + GLES2 dev headers (replaces `openjfx`) */
${APTITUDE} libsdl2-dev libgles2-mesa-dev || true
fi
# /* Compile the JNI native library: `libsdl_gles2_jni.so` (replaces `--module-path`/`--add-modules javafx.*`) */
JAVA_HOME="${JAVA_HOME:-$(java -XshowSettings:properties -version 2>&1 | grep 'java.home' | sed 's/.*= //')}"
JNI_INCLUDES="-I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux" # /* Linux; macOS uses `include/darwin`, Android uses NDK paths */
#shellcheck disable=SC2086 # /* Quotes cause errors with pkg-config output */
cc -shared -fPIC "${PATH_TO_NATIVE}" -o "${PATH_TO_NATIVE_LIB}" ${JNI_INCLUDES} $(pkg-config --cflags --libs sdl2) -lGLESv2 || exit $?
if [ -n "${GITHUB_ACTIONS}" ]; then
#shellcheck disable=SC2086 # /* Quotes cause "Unrecognized option:" */
javac ${JAVA_BUILD_TEST_FLAGS} susuwu/SdlGles2.java susuwu/SimUsages.java ${PATH_TO_SOURCE} # /* Gives "Missing JavaFX application class susuwu/FishSim" unless `cd $(dirname ${PATH_TO_CLASS})` is used. */
else
#shellcheck disable=SC2086 # /* Quotes cause "Unrecognized option:" */
java ${JAVA_FLAGS} --source 16 ${PATH_TO_SOURCE} # /* `--source` is workaround for "error: cannot find symbol\n...\n symbol: {class Force, variable Utils}" when not compiling all sources together */
fi
exit $? #Status required so [*CodeQL*](https://docs.github.com/en/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning-with-codeql) passes.
./susuwu/SimUsages.java:
package susuwu; /* Usage: `import susuwu.SimUsages;` */
/**
* {@code class SimUsages} shows {@code FpsTextMode} statistics such as {@code fps} or {@code ms}.
* Requirements: some render loop (for measurements). Is not specific to the renderer used.
* Was produced for {@code susuwu.FishSim}, so the text (plus comments) assume the organisms are {@code class Fish}, but {@code SimUsages} is not specific to {@code class Fish}
* Some {@code assert}s follow, thus document which arguments to use with this (without {@code -enableassertions}, thus are not enforced).
* Some "Usage:" comments follow, which document how to use this.
* Usage: {@code SimUsages usages = new SimUsages(); usages.show(); usages.fpsTextMode = FpsTextMode.fps.value | FpsTextMode.ms.value;}
* Text is rendered via {@link SdlGles2#setWindowTitle} (replaces {@code javafx.scene.text.Text}).
*/
public class SimUsages {
/* `public` members */
public long fpsTextMode = FpsTextMode.allUsages.value; // Usage `usages.fpsTextModeFps = FpsTextMode.fps.value;` to just show `fps`
public double secondsPerFpsTextRefresh = 1.0; // Usage: `usages.secondsPerFpsTextRefresh = 0.2;` to give more current values, or `= 2.0;` to give more smooth values. In `postRefresh()`: if `secondsPerFpsTextRefresh` elapses, `lastTime = System.nanoTime(); fpsTextRefresh();``
public double renderMs = Double.NaN; // Usage: `functionWhichUsesRenderMs(usages.renderMs);`. Stores average **ms** from `startRender()` to `postRender()` (`renderNs / renderCounter / 1_000_000.0`)
public double physicsMs = Double.NaN; // Usage: `functionWhichUsesPhysicsMs(usages.physicsMs);`. Stores average **ms** from `startPhysics()` to `postPhysics()` (`physicsNs / physicsCounter / 1_000_000.0`)
public double fps = Double.NaN; // Usage: `functionWhichUsesFps(usages.fps);`. Stores `renderCounter / (System.nanoTime() - lastTime) / 1_000_000_000.0`
/* `private` or almost-`private` members */
public long lastTime = System.nanoTime(); // Stores `System.nanoTime()` when `renderCounter = 0`.
public int refreshCounter = 0; // Sum of `postRefresh()` uses since `SimUsages(Pane)`.
public int renderCounter = 0; // Sum of `postRender()` uses since `lastTime = System.nanoTime()`.
public int physicsCounter = 0; // Sum of `postPhysics()` uses since `lastTime = System.nanoTime()`.
public long physicsNs = -1; // Sum of `nanoTime()` at `postRender()` minus `nanoTime()` at `startRender()` since `lastTime = System.nanoTime()`.
public long renderNs = -1; // Sum of `nanoTime()` at `postRender()` minus `nanoTime()` at `startRender()` since `lastTime = System.nanoTime()`.
private String fpsText = "0 FPS";
public SimUsages() {
/* No Pane or display object needed: text is shown via SdlGles2.setWindowTitle(). */
}
static public enum FpsTextMode { // `FpsTextMode` says which resources `fpsText` will show.
none (0 ), // `fpsText = "";`
fps (1 << 0), // `fpsText` += `fps` "FPS";
ms (2 << 1), // `fpsText` += `ms` "ms"; /* Notice: `ms = 1000 / fps;`, so includes idle CPU */
msSpec (1 << 2), // `fpsText` += `renderMs` "renderMs," `physicsMs` "physicsMs"; /* Notice: uses `System.nanoTime()`, does not include idle CPU */
msFish (1 << 3), // `fpsText` += `renderMs / fishShown` "renderMs / Fish shown," `physicsMs / fishListSize` "physicsMs / Fish";
fish (1 << 4), // `fpsText` += `fishListSize` "Fish";
fishShown (1 << 5), // `fpsText` += `fishShown` "Fish shown";
allUsages (FpsTextMode.fps.value | FpsTextMode.ms.value | FpsTextMode.msSpec.value | FpsTextMode.msFish.value | FpsTextMode.fish.value | FpsTextMode.fishShown.value); // shows as all supported usages
long value; // Stores bitwise-or of those.
FpsTextMode(long value) { this.value = value; }
}; // TODO: replace manual bitshifts with `java.util.EnumSet<E>`?
public void show() {
SdlGles2.setWindowTitle("Fish Simulation (Boids) - " + fpsText); /* Replaces `Text.setX/Y/setFill(Color.WHITE)`: text is in window title. */
}
/* Measurement funtions
* Usage: ```
* refreshLoop() {
* usages.startRefresh();
* usages.startPhysics(); physics(fishList); usages.postPhysics();
* usages.startRender(); render(visibleFish); usages.postRender();
* usages.postRefresh(System.nanoTime(), visibleFish.size(), fishList.size());
* }```
* "Refresh loop" is the loop which invokes the render loop plus the physics loop. `class SimUsages` does not assume that the physics loop is invoked as often as the render loop is. If the renderer has its own separate loop, the render loop is the refresh loop.
*/
public void startRefresh() { // Usage: `startRender();` at start of refresh loop.
}
public void postRefresh(long now, long fishShown, long fishListSize) { // Usage: `postRefresh(System.nanoTime(), visibleObjects.size(), physisObjects.size());`
double elapsed = (now - lastTime) / 1_000_000_000.0;
if(elapsed >= secondsPerFpsTextRefresh) {
lastTime = now;
fps = renderCounter / elapsed;
renderMs = renderNs / renderCounter / 1_000_000.0;
physicsMs = physicsNs / physicsCounter / 1_000_000.0;
fpsTextRefresh(fishShown, fishListSize); /* Replaces `Platform.runLater(...)`: SDL2 has no UI thread restriction, so call directly. */
renderCounter = 1;
renderNs = -1;
physicsCounter = 1;
physicsNs = -1;
}
refreshCounter++;
}
private long renderNsStart, physicsNsStart;
public void startRender() { // Usage: `startRender();` at start of render loop
renderNsStart = System.nanoTime();
}
public void postRender() { // Usage: `startRender();` at closure of render loop
renderNs += System.nanoTime() - renderNsStart;
renderCounter++;
}
public void startPhysics() { // Usage: `startPhysics();` at start of physics loop
physicsNsStart = System.nanoTime();
}
public void postPhysics() { // Usage: `startPhysics();` at closure of physics loop
physicsNs += System.nanoTime() - physicsNsStart;
physicsCounter++;
}
private void fpsTextRefresh(long fishShown, long fishListSize) { /* Usage: `usages.fpsTextModeFps(visibleFish.size(), fishList.size())` */
/* omitted due to 32,000 characters maximum */
fpsText = fpsTextStr.substring(0, fpsTextStr.length() - strSep.length());
SdlGles2.setWindowTitle("Fish Simulation (Boids) - " + fpsText); /* Replaces `Text.setText(...)`: update window title with FPS stats. */
}
};
./susuwu/FishSim.java: much of the source code is not included (due to 32,000 characters maximum)
package susuwu; /* Usage: `import susuwu.FishSim;` */
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import susuwu.SimUsages; /* `class SimUsages`, `enum FpsTextMode` */
import susuwu.SdlGles2; /* `class SdlGles2`: JNI bridge to SDL2 + GLES2 */
import susuwu.Calculus; /* `Calculus.pow2()` */
import susuwu.Forces; /* `class Forces implements java.lang.Cloneable` */
import susuwu.ImmutablePosBounds; /* `enum PosBoundsMode`: which stores how sims enforce bounds. */
import susuwu.PosBounds; /* `class PosBounds : extends ImmutablePosBounds`, `PosBounds.set*(PosBounds*)` */
import susuwu.ImmutablePos; /* `class ImmutablePos implements java.util.RandomAccess` */
import susuwu.Pos; /* `class Pos extends ImmutablePos` */
import susuwu.Pos2; /* `class Pos extends Pos` */
public class FishSim {
private static int[] resolution = {1280, 720};
private static Pos2 resolutionf = new Pos2(resolution[0], resolution[1]);
private static Pos resolutionfSlash2 = resolutionf.slashScalar(2); /* Improves execution of inner loops which use this. Notice: `setResolution(newResolution)` invalidates stored references to `resolutionfSlash2` */
private static int resVolume = (int)Math.round(resolutionf.volume()); /* Notice: uses `Pos::volume()` since simple source code is less bug prone. `Math.round` ensures 24-bit mantissas give accurate values */
private static double fishVolume = 200; // Uses resolution of `Fish::render()`.
private static double fishLengthsSep = 62; // Average `Fish`-lengths distance from `Fish` to `Fish`.
private static double fishPerVolume = 1 / fishVolume / fishLengthsSep; // `Fish` per volume (for 2D, volume is resolution).
private static int fishCount = (int)(posBounds.getBoundsVolume() * fishPerVolume);
private static int gridResolution = 100; // Notice: set this to `Colllections.max({forces*.distance})` (which should equal what most sims call "view distance"), so that all relevent `Fish` are processed.
private static int positionInterval = 2; // The `refreshCounter` per `Fish::applyFlockingRulesUpdate()`
public static double monitorRefreshHertz = 60.0; // The `SimUsages.fps` to wish for // Notice: since this limits `SimUsages.fps` to `monitorRefreshHertz`, this prevents benchmarks which use `FpsTextMode.fps` (or `FpsTextMode.ms`). Benchmarks can still use `FpsTextMode.msSpec` (or `FpsTextMode.msFish`).
public static double physicsRefreshHertz = monitorRefreshHertz / positionInterval; // The `1 / SimUsages.physicsMs` to wish for // Notice: unknown what `javafx.animation.Timeline` does if `physicsRefreshHertz > (1 / SimUsages.physicsMs)`, but guess thus stalls or consumes multiple executors
private List<Fish> fishList = new ArrayList<>();
private int fishShown = 0;
private List<Fish>[][] grid; /* `listToPartitions(List<>[][] grid, List<> list)` uses this */
private Random random = new Random();
SimUsages simUsages = new SimUsages(); /* Replaces `new SimUsages(root)`: no Pane needed for SDL2 text (shown via window title). */
// simUsages.fpsTextMode = FpsTextMode.allUsages.value; // TODO: "error: <identifier> expected" solution
private volatile boolean quit = false; /* Set to `true` by `stop()` to signal the main SDL loop to exit. */
private ExecutorService executor = Executors.newSingleThreadExecutor();
private ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); /* Replaces `javafx.animation.Timeline` for `separateFps` physics. */
public static void main(String[] args) {
new FishSim().run(args); /* Replaces `launch(args)`: instantiate directly since there is no JavaFX Application lifecycle. */
}
/** Initializes SDL2+GLES2, populates fish, starts physics loops, then runs the render loop until quit. Replaces {@code start(Stage primaryStage)}. */
public void run(String[] args) { /* `args` preserved for future CLI configuration (e.g., `--resolution`, `--physics-mode`); currently unused. */
if(!SdlGles2.init(resolution[0], resolution[1], "Fish Simulation (Boids)")) {
System.err.println("FishSim.run: SdlGles2.init failed; aborting.");
return;
}
SdlGles2.glClearColor( /* Light-blue background (replaces `Color.LIGHTBLUE` passed to `new Scene(...)`) */
(float)Color.LIGHTBLUE.getRed(), (float)Color.LIGHTBLUE.getGreen(), (float)Color.LIGHTBLUE.getBlue(), 1.0f);
// Initialize fish
for(int i = 0; i < fishCount; i++) {
Pos2 pos = new Pos2(random.nextDouble() * posBounds.getBounds(0), random.nextDouble() * posBounds.getBounds(1));
Pos2 dpos = new Pos2((random.nextDouble() * 2 - 1) * Fish.dposMax, (random.nextDouble() * 2 - 1) * Fish.dposMax);
fishList.add(new Fish(pos, dpos, Color.color(random.nextDouble(), random.nextDouble(), random.nextDouble())));
}
posBounds.setGridResolution(gridResolution);
grid = new ArrayList[posBounds.getGridSize(0)][posBounds.getGridSize(1)]; /* `listToPartitions(List<>[][] grid, List<> list)` uses this */
for(int i = 0; i < grid.length; i++) {
for(int j = 0; j < grid[i].length; j++) {
grid[i][j] = new ArrayList<>();
}
}
simUsages.show();
// Start separate physics loop for `separateUnbound` / `separateFps` modes (replaces `AnimationTimer` / `Timeline`):
switch(physicsMode) { // `PhysicsMode.` is omitted from all `case`s, to support old `java --source` versions
case separateUnbound:
executor.submit(() -> { while(!quit) { updateFish(); } });
break;
case separateFps:
long physicsIntervalMs = Math.max(1L, (long)(1_000.0 / physicsRefreshHertz)); /* Use milliseconds for scheduler precision (avoids nanosecond scheduler overhead). */
scheduledExecutor.scheduleAtFixedRate(() -> updateFish(), 0, physicsIntervalMs, TimeUnit.MILLISECONDS);
break;
default:
break; /* synchronous* / asynchronous* modes: handled inside `refreshLoop()` */
}
// Main SDL render loop (replaces `AnimationTimer` / `Timeline` for `monitorRefreshMode`):
long renderIntervalNs = (long)(1_000_000_000.0 / monitorRefreshHertz);
long lastRenderTime = System.nanoTime();
while(!quit && !SdlGles2.pollQuit()) {
long now = System.nanoTime();
boolean shouldRender;
switch(monitorRefreshMode) { // `PhysicsMode.` is omitted from all `case`s, to support old `java --source` versions
case separateUnbound:
shouldRender = true;
break;
case separateFps: /* fall-through */
default:
shouldRender = ((now - lastRenderTime) >= renderIntervalNs);
break;
}
if(shouldRender) {
refreshLoop(now);
lastRenderTime = now;
} else {
try { Thread.sleep(1); } catch(InterruptedException e) { Thread.currentThread().interrupt(); break; } /* 1ms sleep avoids busy-waiting while still responding within 1 frame at 60fps (~16ms). */
}
}
stop();
}
private void refreshLoop(long now) {
simUsages.startRefresh();
switch(physicsMode) { // `PhysicsMode.` is omitted from all `case`s, to support old `java --source` versions
case synchronousHomo:
updateFish();
break;
case synchronousInterval:
if(simUsages.refreshCounter % positionInterval == 0) {
updateFish();
}
break;
case asynchronousHomo:
executor.submit(() -> updateFish());
break;
case asynchronousInterval:
if(simUsages.refreshCounter % positionInterval == 0) {
executor.submit(() -> updateFish());
}
break;
case separateUnbound:
case separateFps:
break; // no-op for both, since `run()` processes thus
default:
throw new IllegalArgumentException("Unsupported `PhysicsMode physicsMode`: " + physicsMode);
}
renderFish();
simUsages.postRefresh(now, fishShown, fishList.size());
}
private void outOfBounds(String function, Fish fish) {
/* Notice: `outOfBounds()` has numerous sensible actions other than to print to `stderr`: `fish.die()`, `fish.stop()`, `fish.reverse()`, `fish.wrapAround()` */
System.err.println(function + ": " + posBounds.posOutOfBoundsStr(fish.pos, "Fish.pos"));
}
/* Spatial partitioning (simple grid system). TODO: generic version of this (accept all `class`s with `#isInBounds` plus `#pos`). */
private void listToPartitions(List<Fish>[][] grid, List<Fish> list) {
assert grid.length == (int)Math.ceil(posBounds.getBounds(0) / gridResolution);
assert grid[0].length == (int)Math.ceil(posBounds.getBounds(1) / gridResolution);
for(int i = 0; i < grid.length; i++) {
for(int j = 0; j < grid[i].length; j++) {
grid[i][j].clear();
}
}
for(Fish fish : list) { /* Assign list members to grid sections */
if(fish.isInBounds) {
int[] gridPos = {(int) (fish.pos.pos[0] / gridResolution), (int) (fish.pos.pos[1] / gridResolution)};
grid[gridPos[0]][gridPos[1]].add(fish); // if `gridPos` is not in bounds, this will `throw new IndexOutOfBoundsException()`. But `Fish.setPos()` uses `PosBounds::posBounds.posBound()` which, which ensures `Fish.pos` bounds to `FishSim.resolution`, so this will not `throw`.
}
}
}
private void updateFish() {
updateFishLock.lock();
simUsages.startPhysics();
listToPartitions(grid, fishList);
// Update each fish
for(Fish fish : fishList) {
fish.applyFlockingRules(fishList, grid);
fish.update();
}
simUsages.postPhysics();
updateFishLock.unlock();
}
private void renderFish() {
renderFishLock.lock();
simUsages.startRender();
fishShown = 0;
SdlGles2.glClear(SdlGles2.GL_COLOR_BUFFER_BIT); /* Replaces `gc.clearRect(0, 0, resolution[0], resolution[1])` */
for(Fish fish : fishList) {
if(fish.isVisible) { // For `Fish` not shown, this condition improves `SimUsages.fps` (lowers `SimUsages.renderNs`).
fishShown++;
fish.render();
}
}
SdlGles2.swapWindow(); /* Presents the rendered frame (replaces implicit JavaFX frame commit). */
simUsages.postRender();
renderFishLock.unlock();
}
/** Signals the main loop to exit, shuts down executor threads, and calls {@code SDL_Quit()} via {@link SdlGles2#destroy()}. Replaces {@code @Override stop()}. */
public void stop() {
quit = true;
scheduledExecutor.shutdownNow();
executor.shutdown();
SdlGles2.destroy(); /* Replaces implicit JavaFX window teardown. */
}
public class Fish { /* snip... can just include 32,000 chars of source code */ }
/**
* Renders this fish using GLES2 via {@link SdlGles2#drawFilledPolygon}.
* Replicates the JavaFX {@code gc.save/translate/rotate/setFill/beginPath/moveTo/lineTo/closePath/fill/restore} sequence.
* Fish shape vertices (local space): {@code {0,-10}, {-5,10}, {-2,0}, {2,0}, {5,10}}.
* Triangulated (triangle fan from vertex 0): triangles {@code {0,1,2}, {0,2,3}, {0,3,4}}.
*/
public synchronized void render() {
/* Replicate `gc.translate(tx,ty); gc.rotate(angleDeg+90)` as a 2-D rotation matrix. */
double angle = Math.atan2(dpos.pos[1], dpos.pos[0]) + Math.PI / 2.0; /* equiv. to Math.toRadians(Math.toDegrees(atan2) + 90) */
double cosA = Math.cos(angle);
double sinA = Math.sin(angle);
double tx = pos.pos[0];
double ty = pos.pos[1];
/* Fish shape local vertices: same coordinates as original JavaFX path */
final double[][] lv = {{0,-10}, {-5,10}, {-2,0}, {2,0}, {5,10}};
/* Triangulate polygon as fan from vertex 0: {0,1,2}, {0,2,3}, {0,3,4} -> 3 triangles, 9 verts, 18 floats */
final int[][] tris = {{0,1,2}, {0,2,3}, {0,3,4}};
float[] verts = new float[18];
int vi = 0;
for(int[] tri : tris) {
for(int idx : tri) {
double lx = lv[idx][0], ly = lv[idx][1];
verts[vi++] = (float)(lx * cosA - ly * sinA + tx); /* x' = x*cos - y*sin + tx */
verts[vi++] = (float)(lx * sinA + ly * cosA + ty); /* y' = x*sin + y*cos + ty */
}
}
SdlGles2.drawFilledPolygon(verts, (float)color.getRed(), (float)color.getGreen(), (float)color.getBlue(), 1.0f);
}
};
};