diff --git a/.gitignore b/.gitignore index f35e0efe..50148fd3 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,9 @@ compile_commands.json todo/ susuwu/*.class susuwu/FishSim_local.log +susuwu/*.so +susuwu/*.dll +susuwu/*.dylib # `remark-cli` temp package.json diff --git a/build.sh b/build.sh index 64957e9a..c19b813d 100755 --- a/build.sh +++ b/build.sh @@ -6,14 +6,12 @@ # * If *this attribution* is shown, *this source code* allows all uses. *This attribution* constitutes the most permissive which is compatible with [*GPLv2*](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) + [*Apache 2*](https://www.apache.org/licenses/LICENSE-2.0.html), which is suitable for personal use (also suitable for school use). # * If *this attribution* is not professional enough for business use: businesses can use *this source code* through included versions of [*GPLv2*](./LICENSE_GPLv2), [*Apache 2*](./LICENSE), or through both of those. */ -PATH_TO_FX="${PATH_TO_FX:-"/usr/share/openjfx/lib/"}" # /* Notice: prefix " "s in path with slashes, such as "\ ". */ PATH_TO_CLASS="susuwu/FishSim" PATH_TO_SOURCE="${PATH_TO_CLASS}.java" -JAVA_MODULES="${JAVA_MODULES} --module-path ${PATH_TO_FX} --add-modules javafx.controls,javafx.fxml" # /* Notice: quotes around `${PATH_TO_FX}` give errors, so ensure to escape the path in `PATH_TO_FX=...` */ -#JAVA_MODULES="${JAVA_MODULES} --module-path \"${PATH_TO_FX}\" --add-modules javafx.controls,javafx.fxml" # /* Notice: those quotes (around `${PATH_TO_FX}`) give "java.lang.module.FindException: Module javafx.controls not found" */ -JAVA_FLAGS="${JAVA_FLAGS} --enable-native-access=javafx.graphics" -#JAVA_FLAGS="${JAVA_FLAGS} -XX:+HeapDumpOnOutOfMemoryError " # /* Notice: if "Exception java.lang.OutOfMemoryError occurred dispatching signal SIGINT to handler- the VM may need to be forcibly terminated" then uncomment this to use `jhat java_pid*.hprof` */ +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 @@ -21,22 +19,26 @@ if command -v sudo >/dev/null; then else APTITUDE="apt -y install " # /* Fixes "The program sudo is not installed." on platforms such as smartphones */ fi -if ! test -d "${PATH_TO_FX}"; then # /* TODO: search for this if default (**Ubuntu**'s) path is not found */ - ${APTITUDE} openjfx || ${APTITUDE} libopenjfx-java - if ! test -d "${PATH_TO_FX}"; then - echo "$0: '${PATH_TO_FX}' dir not found. Use \`${APTITUDE} install openjfx\`, then set '\${PATH_TO_FX}' to the actual libs." - fi -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} ${JAVA_MODULES} ${PATH_TO_SOURCE} # /* Gives "Missing JavaFX application class susuwu/FishSim" unless `cd $(dirname ${PATH_TO_CLASS})` is used. */ -# java ${JAVA_TEST_FLAGS} ${JAVA_FLAGS} ${JAVA_MODULES} ${PATH_TO_CLASS} # /* Assumes `main()` will `return` (but, `FishSim.java`'s does not) */ -# java ${JAVA_TEST_FLAGS} ${JAVA_FLAGS} --source 16 ${JAVA_MODULES} ${PATH_TO_SOURCE} # /* Assumes `main()` will `return` (but, `FishSim.java`'s does not) */ + 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. */ +# java ${JAVA_TEST_FLAGS} ${JAVA_FLAGS} ${PATH_TO_CLASS} # /* Assumes `main()` will `return` (but, `FishSim.java`'s does not) */ +# java ${JAVA_TEST_FLAGS} ${JAVA_FLAGS} --source 16 ${PATH_TO_SOURCE} # /* Assumes `main()` will `return` (but, `FishSim.java`'s does not) */ else #shellcheck disable=SC2086 # /* Quotes cause "Unrecognized option:" */ - java ${JAVA_FLAGS} --source 16 ${JAVA_MODULES} ${PATH_TO_SOURCE} # /* `---source` is workaround for "Missing JavaFX application class susuwu/FishSim" */ + 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. diff --git a/susuwu/FishSim.java b/susuwu/FishSim.java index aa722086..2af0053c 100644 --- a/susuwu/FishSim.java +++ b/susuwu/FishSim.java @@ -6,26 +6,17 @@ package susuwu; /* Usage: `import susuwu.FishSim;` */ -import javafx.animation.Animation; -import javafx.animation.AnimationTimer; -import javafx.animation.KeyFrame; -import javafx.animation.Timeline; -import javafx.application.Application; -import javafx.scene.Scene; -import javafx.scene.canvas.Canvas; -import javafx.scene.canvas.GraphicsContext; -import javafx.scene.layout.Pane; -import javafx.scene.paint.Color; -import javafx.stage.Stage; -import javafx.util.Duration; 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. */ @@ -37,18 +28,29 @@ // TODO: test how much of `java`'s [static `Array` overhead](https://github.com/SwuduSusuwu/SusuPosts/blob/preview/posts/Physics_sims_which_structures_to_use.md#separate-variables-versus-dim-lists) `java`'s toolkit optimizes for you. If performance is a problem, choose a new approach to use. /** - * Simple [*JavaFX*](https://github.com/openjdk/jfx) fish sim, which includes reusable {@code public class}s (for new sims to use). Most of the reusable {@code public class}s are in other {@code .java} sources for {@code package susuwu} + * Simple SDL2+GLES2 fish sim (via JNI), which includes reusable {@code public class}s (for new sims to use). Most of the reusable {@code public class}s are in other {@code .java} sources for {@code package susuwu} * This ([`./susuwu/FishSim.java`](./FishSim.java)) uses pseudo-*Markdown* for comments, but [`./posts/FishSim.md`](../posts/FishSim.md) is the actual [*Markdown*](https://github.github.com/gfm/) document for this. * Notice: replaced most of [*Solar-Pro-2*'s original `FishSim.java`](https://github.com/SwuduSusuwu/SusuJava/blob/solarPro2FishSim/susuwu/FishSim.java), as [`./posts/FishSim.md#intro`](../posts/FishSim.md#intro) documents (plus [*GitHub*'s `/compare/` tool shows](https://github.com/SwuduSusuwu/SusuJava/compare/solarPro2FishSim..susuFishSim#diff-8c440bb92bc6939e1450542897e0bbb1a8737b93808ea63ed32784edfacef4b4). */ -public class FishSim extends Application { +public class FishSim { + /** Minimal {@code Color} replacement (replaces {@code javafx.scene.paint.Color}). Supports {@code getRed()}, {@code getGreen()}, {@code getBlue()} for {@code isSimilarTo()} comparisons. */ + public static class Color { + private final double red, green, blue; + private Color(double r, double g, double b) { this.red = r; this.green = g; this.blue = b; } + public static Color color(double r, double g, double b) { return new Color(r, g, b); } + public double getRed() { return red; } + public double getGreen() { return green; } + public double getBlue() { return blue; } + public static final Color LIGHTBLUE = new Color(0.678, 0.847, 0.902); /* Light blue background (replaces `Color.LIGHTBLUE` from JavaFX) */ + } + public enum PhysicsMode { // `PhysicsMode` says how to execute `updateFish()` synchronousHomo, // `updateFish()` once per `refreshLoop()`. synchronousInterval, // `updateFish()` per `positionInterval` `refreshLoop()`s. asynchronousHomo, // `executor.submit(() -> updateFish());` once per `refreshLoop()`. asynchronousInterval, // `executor.submit(() -> updateFish());` per `positionInterval` `refreshLoop()`s. - separateUnbound, // `new AnimationTimer() { public void handle(long now) { updateFish(); }` - separateFps, // `Timeline timeline = new Timeline( new KeyFrame(Duration.millis(1000.0 / physicsRefreshHertz), event -> { updateFish(); })` + separateUnbound, // `updateFish()` runs in a background thread continuously (replaces `AnimationTimer`). Notice: with `GLES2` this has an implicit bound to the monitor refresh (Virtual Synchronization, which `SimUsages` does not count towards "drawMS"). + separateFps, // `updateFish()` runs via `ScheduledExecutorService` at `physicsRefreshHertz` (replaces `Timeline/KeyFrame`). } private static PhysicsMode monitorRefreshMode = PhysicsMode.separateFps; // `monitorRefreshMode` must use `.separateUnbound` or `.separateFps`. private static PhysicsMode physicsMode = PhysicsMode.separateFps; // Notice: if `PhysicsMode.*Interval`, must set `positionInterval`. if `PhysicsMode.separateFps`, must set `physicsRefreshHertz`. @@ -67,10 +69,9 @@ public static boolean setResolution(int[] newResolution) { /* Notice: invalidate resolutionfSlash2 = resolutionf.slashScalar(2); /* Notice: invalidates references which store the old address to `resolutionfSlash2`. */ 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 */ posBounds.setBounds(resolutionf.starScalar(boundsResolutionFactor).pos); // TODO: if sure that no functions store references to the original instance, replace the above row with this (since simple source code is less bug prone) - // canvas = new Canvas(resolution[0], resolution[1]); // TODO: replace with `canvas.setWidth(resolution[0]); canvas.setHeight(resolution[1]);`? - // gc = canvas.getGraphicsContext2D(); - // scene = new Scene(root, resolution[0], resolution[1], Color.LIGHTBLUE); // replace with `scene.widthProperty().bind(primaryStage.widthProperty());`? - // stage.setScene(scene); + // (Resize SDL window here if needed: SDL_SetWindowSize(window, resolution[0], resolution[1])) + // (Resize GLES2 viewport here if needed: glViewport(0, 0, resolution[0], resolution[1])) + // (Rebuild GLES2 u_resolution uniform: SdlGles2.glClearColor/etc. if resolution changes) renderFishLock.unlock(); updateFishLock.unlock(); return true; @@ -79,7 +80,7 @@ public static boolean setResolution(int[] newResolution) { /* Notice: invalidate 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 boundsResolutionFactor = 2; // `resolution[dim] * 2` gives best results (sufficient room for natural ocean, small enough for most CPUs to process). Notice: Powers of 2 give improved versions of most formulas for computers, but for now this allows all values + private static double boundsResolutionFactor = (1_000_000 > resVolume ? 2.0 : 1.2); // Ocean is `resolution[dim] * boundsResolutionFactor`. `2.0` gives more room for natural oceans, but old laptops with huge resolutions (such as `{2200, 1200}`) must use `1.2` so the load is low enough for old CPUs to process). TODO: include short benchmark (on startup) to set `boundsResolutionFactor` to optimal value, or reduce CPU use for unshown `Fish` (`if(!fish.isVisible)`, then execute `updateFish` just once per second (1 hertz), with larger steps). private static PosBounds posBounds = new PosBounds(PosBounds.PosBoundsMode.wrapAroundResolution, resolutionf.starScalar(boundsResolutionFactor)); // for simple sims, use `resolutionf.clone()` private static double fishVolume = 200; // Uses resolution of `Fish::render()`. private static double fishLengthsSep = 62; // Average `Fish`-lengths distance from `Fish` to `Fish`. @@ -94,21 +95,26 @@ public static boolean setResolution(int[] newResolution) { /* Notice: invalidate private int fishShown = 0; private List[][] grid; /* `listToPartitions(List<>[][] grid, List<> list)` uses this */ private Random random = new Random(); - private Pane root = new Pane(); - private Canvas canvas = new Canvas(resolution[0], resolution[1]); - private GraphicsContext gc = canvas.getGraphicsContext2D(); - private Stage stage; - SimUsages simUsages = new SimUsages(root); + 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: 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) { - launch(args); + new FishSim().run(args); /* Replaces `launch(args)`: instantiate directly since there is no JavaFX Application lifecycle. */ } - @Override - public void start(Stage primaryStage) { + /** 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)); @@ -116,16 +122,6 @@ public void start(Stage primaryStage) { fishList.add(new Fish(pos, dpos, Color.color(random.nextDouble(), random.nextDouble(), random.nextDouble()))); } - root.getChildren().add(canvas); - - Scene scene = new Scene(root, resolution[0], resolution[1], Color.LIGHTBLUE); - stage = primaryStage; - primaryStage.setScene(scene); - primaryStage.setTitle("Fish Simulation (Boids)"); - primaryStage.setResizable(false); - primaryStage.show(); - simUsages.show(); - 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++) { @@ -134,39 +130,44 @@ public void start(Stage primaryStage) { } } - // Start animation loop - switch(monitorRefreshMode) { // `PhysicsMode.` is omitted from all `case`s, to support old `java --source` versions + 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: - new AnimationTimer() { - @Override - public void handle(long now) { refreshLoop(now); } - }.start(); // Notice: replace `Timeline` with this for benchmarks which use `FpsTextMode.fps` (or `FpsTextMode.ms`). + executor.submit(() -> { while(!quit) { updateFish(); } }); break; case separateFps: - Timeline timeline = new Timeline( - new KeyFrame(Duration.millis(1000.0 / monitorRefreshHertz), event -> { refreshLoop(System.nanoTime()); }) - ); // 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`). - timeline.setCycleCount(Animation.INDEFINITE); - timeline.play(); + 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: - throw new IllegalArgumentException("Unsupported `PhysicsMode monitorRefreshMode`: " + monitorRefreshMode); + break; /* synchronous* / asynchronous* modes: handled inside `refreshLoop()` */ } - switch(physicsMode) { // `PhysicsMode.` is omitted from all `case`s, to support old `java --source` versions - case separateUnbound: - new AnimationTimer() { - @Override - public void handle(long now) { updateFish(); } - }.start(); - break; - case separateFps: - Timeline loopPerSecond = new Timeline( - new KeyFrame(Duration.millis(1000.0 / physicsRefreshHertz), event -> { updateFish(); }) - ); - loopPerSecond.setCycleCount(Animation.INDEFINITE); - loopPerSecond.play(); - break; + + // 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: // Notice: uses Vertical Synchronization, which `SimUsages` subtracts from `renderNs` (does not count towards resource usage). + 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) { @@ -190,7 +191,7 @@ private void refreshLoop(long now) { break; case separateUnbound: case separateFps: - break; // no-op for both, since `start(Stage primaryStage)` processes thus + break; // no-op for both, since `run()` processes thus default: throw new IllegalArgumentException("Unsupported `PhysicsMode physicsMode`: " + physicsMode); } @@ -241,20 +242,26 @@ private void renderFish() { renderFishLock.lock(); simUsages.startRender(); fishShown = 0; - gc.clearRect(0, 0, resolution[0], resolution[1]); + simUsages.preSynchro(); // Notice: alternatives: use `SDL_GL_SetSwapInterval(0);`, or move `simUsages.startRender()` below the first use of `SdlGles2` + SdlGles2.glClear(SdlGles2.GL_COLOR_BUFFER_BIT); // Replaces `gc.clearRect(0, 0, resolution[0], resolution[1])` + simUsages.postSynchro(); for(Fish fish : fishList) { if(fish.isVisible) { // For `Fish` not shown, this condition improves `SimUsages.fps` (lowers `SimUsages.renderNs`). fishShown++; - fish.render(gc); + fish.render(); } } + SdlGles2.swapWindow(); // Presents the rendered frame (replaces implicit JavaFX frame commit). simUsages.postRender(); renderFishLock.unlock(); } - @Override + /** 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 { /* `static Fish` causes "{posBounds,posBounds.posBound()} cannot be referenced from a static context" (unless those are set to `static`, which prevents `FishSim` from use of separate values with multiple windows) */ @@ -271,7 +278,7 @@ public class Fish { /* `static Fish` causes "{posBounds,posBounds.posBound()} ca private Pos pos; // Position private Pos dpos; // Motion (derivative of position) - private Color color; + private Color color; /* Replaces `javafx.scene.paint.Color`: uses `FishSim.Color` which supports `getRed()`, `getGreen()`, `getBlue()`. */ public boolean isInBounds; public boolean isVisible = false; // Just stores `0 <= pos[0] && resolution[0] > pos[0] && 0 <= pos[1] && resolution[1] > pos[1]` for now. @@ -441,20 +448,33 @@ public synchronized void update() { setPos(pos.plus(dpos)); } - public synchronized void render(GraphicsContext gc) { - gc.save(); - gc.translate(pos.pos[0], pos.pos[1]); - gc.rotate(Math.toDegrees(Math.atan2(dpos.pos[1], dpos.pos[0])) + 90); - gc.setFill(color); - gc.beginPath(); - gc.moveTo(0, -10); - gc.lineTo(-5, 10); - gc.lineTo(-2, 0); - gc.lineTo(2, 0); - gc.lineTo(5, 10); - gc.closePath(); - gc.fill(); - gc.restore(); + /** + * 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); } }; }; diff --git a/susuwu/SdlGles2.java b/susuwu/SdlGles2.java new file mode 100644 index 00000000..28e2807f --- /dev/null +++ b/susuwu/SdlGles2.java @@ -0,0 +1,45 @@ +/* Attribution (henceforth "*this attribution*", whose syntax is *Markdown*): 2024 [Swudu Susuwu](https://swudususuwu.substack.com) + * has the newest version of `./susuwu/SdlGles2.java` (henceforth "*this source code*"). + * If *this attribution* is shown, *this source code* allows all uses. *This attribution* constitutes the most permissive which is compatible with [*GPLv2*](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) + [*Apache 2*](https://www.apache.org/licenses/LICENSE-2.0.html), which is suitable for personal use (also suitable for school use). + * If *this attribution* is not professional enough for business use: businesses can use *this source code* through included versions of [*GPLv2*](./LICENSE_GPLv2), [*Apache 2*](./LICENSE), or through both of those. + */ + +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 `` */ + + /** 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); +} diff --git a/susuwu/SimUsages.java b/susuwu/SimUsages.java index 2dbc754a..575e7d4a 100644 --- a/susuwu/SimUsages.java +++ b/susuwu/SimUsages.java @@ -6,18 +6,14 @@ package susuwu; /* Usage: `import susuwu.SimUsages;` */ -import javafx.application.Platform; /* `Platform.runLater(...)` */ -import javafx.scene.layout.Pane; /* `Pane pane` */ -import javafx.scene.paint.Color; /* `Color.WHITE` */ -import javafx.scene.text.Text; /* `Text fpsText` */ - /** * {@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 = SimUsages(Pane); usages.show(); usages.fpsTextMode = FpsTextMode.fps.value | FpsTextMode.ms.value;} + * 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 */ @@ -34,10 +30,9 @@ public class SimUsages { 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 Pane root; - public SimUsages(Pane pane) { - this.root = pane; - this.root.getChildren().add(fpsText); + 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 = "";` @@ -52,11 +47,8 @@ static public enum FpsTextMode { // `FpsTextMode` says which resources `fpsText` FpsTextMode(long value) { this.value = value; } }; // TODO: replace manual bitshifts with `java.util.EnumSet`? - private Text fpsText = new Text("0 FPS"); public void show() { - fpsText.setX(10); - fpsText.setY(30); - fpsText.setFill(Color.WHITE); + SdlGles2.setWindowTitle("Fish Simulation (Boids) - " + fpsText); /* Replaces `Text.setX/Y/setFill(Color.WHITE)`: text is in window title. */ } /* Measurement funtions @@ -78,7 +70,7 @@ public void show() { fps = renderCounter / elapsed; renderMs = renderNs / renderCounter / 1_000_000.0; physicsMs = physicsNs / physicsCounter / 1_000_000.0; - Platform.runLater(() -> fpsTextRefresh(fishShown, fishListSize)); + fpsTextRefresh(fishShown, fishListSize); /* Replaces `Platform.runLater(...)`: SDL2 has no UI thread restriction, so call directly. */ renderCounter = 1; renderNs = -1; physicsCounter = 1; @@ -90,6 +82,12 @@ public void show() { public void startRender() { // Usage: `startRender();` at start of render loop renderNsStart = System.nanoTime(); } + public void preSynchro() { // Usage: `startRender(); ... startSynchro(); SdlGles2.glClear(...); postSynchro();` + renderNsStart += System.nanoTime(); + } // TODO: reduce `preSynchro()` to no-op, improve `postSynchro()` to subtract actual Virtual Synchronization time from `renderNsStat` (so the time to clear buffers is included). + public void postSynchro() { // Usage: `startRender(); ... startSynchro(); SdlGles2.glClear(...); postSynchro();` + renderNsStart -= System.nanoTime(); // Purpose: subtracts vertical synchronization time from "drawMS` (which is computed from `renderNs`, which is computed from `renderNsStart`). Problem: `glClear()` does not just wait for Virtical Synchronization, but also clears the buffer, which this will also subtract from `renderNs`. + } public void postRender() { // Usage: `startRender();` at closure of render loop renderNs += System.nanoTime() - renderNsStart; renderCounter++; @@ -141,6 +139,7 @@ private void fpsTextRefresh(long fishShown, long fishListSize) { /* Usage: `usag fpsTextStr += String.format(fpsTextModeFish ? "%4d shown)" : "%4d Fish shown", fishShown); fpsTextStr += strSep; } - fpsText.setText(fpsTextStr.substring(0, fpsTextStr.length() - strSep.length())); + fpsText = fpsTextStr.substring(0, fpsTextStr.length() - strSep.length()); + SdlGles2.setWindowTitle("Fish Simulation (Boids) - " + fpsText); /* Replaces `Text.setText(...)`: update window title with FPS stats. */ } }; diff --git a/susuwu/build.sh b/susuwu/build.sh index 0b9c9738..b47398a2 100755 --- a/susuwu/build.sh +++ b/susuwu/build.sh @@ -6,14 +6,12 @@ # * If *this attribution* is shown, *this source code* allows all uses. *This attribution* constitutes the most permissive which is compatible with [*GPLv2*](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) + [*Apache 2*](https://www.apache.org/licenses/LICENSE-2.0.html), which is suitable for personal use (also suitable for school use). # * If *this attribution* is not professional enough for business use: businesses can use *this source code* through included versions of [*GPLv2*](./LICENSE_GPLv2), [*Apache 2*](./LICENSE), or through both of those. */ -PATH_TO_FX="${PATH_TO_FX:-"/usr/share/openjfx/lib/"}" # /* Notice: prefix " "s in path with slashes, such as "\ ". */ PATH_TO_CLASS="FishSim" PATH_TO_SOURCE="${PATH_TO_CLASS}.java" -JAVA_MODULES="${JAVA_MODULES} --module-path ${PATH_TO_FX} --add-modules javafx.controls,javafx.fxml" # /* Notice: quotes around `${PATH_TO_FX}` give errors, so ensure to escape the path in `PATH_TO_FX=...` */ -#JAVA_MODULES="${JAVA_MODULES} --module-path \"${PATH_TO_FX}\" --add-modules javafx.controls,javafx.fxml" # /* Notice: those quotes (around `${PATH_TO_FX}`) give "error: module not found: javafx.{fxml, controls}", so this version is not used. */ -JAVA_FLAGS="${JAVA_FLAGS} --enable-native-access=javafx.graphics" -#JAVA_FLAGS="${JAVA_FLAGS} -XX:+HeapDumpOnOutOfMemoryError " # /* Notice: if "Exception java.lang.OutOfMemoryError occurred dispatching signal SIGINT to handler- the VM may need to be forcibly terminated" then uncomment this to use `jhat java_pid*.hprof` */ +PATH_TO_NATIVE="sdl_gles2_jni.c" +PATH_TO_NATIVE_LIB="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=." # /* Allows JNI to find `libsdl_gles2_jni.so` in the current directory */ export JAVA_BUILD_TEST_FLAGS="-verbose" export JAVA_TEST_FLAGS="-verbose:module" if command -v sudo >/dev/null; then @@ -21,24 +19,24 @@ if command -v sudo >/dev/null; then else APTITUDE="apt -y install " # /* Fixes "The program sudo is not installed." on platforms such as smartphones */ fi -if ! test -d "${PATH_TO_FX}"; then # /* TODO: search for this if default (**Ubuntu**'s) path is not found */ - ${APTITUDE} openjfx || ${APTITUDE} libopenjfx-java - if ! test -d "${PATH_TO_FX}"; then - echo "$0: '${PATH_TO_FX}' dir not found. Use \`${APTITUDE} install openjfx\`, then set '\${PATH_TO_FX}' to the actual libs." - fi -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} ${JAVA_MODULES} ${PATH_TO_SOURCE} # /* Gives "error: cannot find symbol\n...\n symbol: class Force" */ -# java ${JAVA_TEST_FLAGS} ${JAVA_FLAGS} ${JAVA_MODULES} ${PATH_TO_CLASS} # /* Assumes `main()` will `return` (but, `FishSim.java`'s does not) */ -# java ${JAVA_TEST_FLAGS} ${JAVA_FLAGS} --source 16 ${JAVA_MODULES} ${PATH_TO_SOURCE} # /* Assumes `main()` will `return` (but, `FishSim.java`'s does not) */ + javac ${JAVA_BUILD_TEST_FLAGS} SdlGles2.java SimUsages.java ${PATH_TO_SOURCE} else -# javac ${JAVA_MODULES} ${PATH_TO_SOURCE} # /* Gives "error: cannot find symbol\n...\n symbol: class Force" */ -# java ${JAVA_FLAGS} ${JAVA_MODULES} ${PATH_TO_CLASS} #shellcheck disable=SC2086 # /* Quotes cause "Unrecognized option:" */ - java ${JAVA_FLAGS} --source 16 ${JAVA_MODULES} ${PATH_TO_SOURCE} # /* `---source` is workaround for "error: cannot find symbol\n...\n symbol: {class Force,variable Utils}" */ + java ${JAVA_FLAGS} --source 16 ${PATH_TO_SOURCE} # /* `--source` is workaround for "error: cannot find symbol\n...\n symbol: {class Force,variable Utils}" */ 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. diff --git a/susuwu/sdl_gles2_jni.c b/susuwu/sdl_gles2_jni.c new file mode 100644 index 00000000..853be096 --- /dev/null +++ b/susuwu/sdl_gles2_jni.c @@ -0,0 +1,188 @@ +/* Attribution (henceforth "*this attribution*", whose syntax is *Markdown*): 2024 [Swudu Susuwu](https://swudususuwu.substack.com) + * has the newest version of `./susuwu/sdl_gles2_jni.c` (henceforth "*this source code*"). + * If *this attribution* is shown, *this source code* allows all uses. *This attribution* constitutes the most permissive which is compatible with [*GPLv2*](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) + [*Apache 2*](https://www.apache.org/licenses/LICENSE-2.0.html), which is suitable for personal use (also suitable for school use). + * If *this attribution* is not professional enough for business use: businesses can use *this source code* through included versions of [*GPLv2*](./LICENSE_GPLv2), [*Apache 2*](./LICENSE), or through both of those. + */ + +/* 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 types + macros */ +#include /* SDL_Init, SDL_CreateWindow, SDL_GL_CreateContext, SDL_PollEvent, SDL_GL_SwapWindow, SDL_Quit */ +#include /* glClear, glClearColor, glCreateShader, glCreateProgram, glDrawArrays, ... */ +#include /* fprintf, stderr */ +#include /* 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); +}