feat: add graphics benchmark framework
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Fri, 20 Mar 2026 21:03:16 +0000 (23:03 +0200)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Fri, 20 Mar 2026 21:03:16 +0000 (23:03 +0200)
- Convert GraphicsBenchmark to automated test runner
- Add wireframe and textured cubes tests
- Add StarGridTest benchmark for billboard rendering
- Add system info to benchmark output
- Add keyboard shortcut to skip tests
- Add WindingOrderDemo to test backface culling
- Fix render thread startup and benchmark timing

22 files changed:
AGENTS.md
doc/index.org
src/main/java/eu/svjatoslav/sixth/e3d/examples/GraphDemo.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/OctreeDemo.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/RainingNumbersDemo.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/RandomPolygonsDemo.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/ShadedShapesDemo.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/TextEditorDemo.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/TextEditorDemo2.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/WindingOrderDemo.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/BenchmarkTest.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/GraphicsBenchmark.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/SolidCubesTest.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/StarGridTest.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/TestResult.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/TexturedCube.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/TexturedCubesTest.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/WireframeCubesTest.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/package-info.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/examples/galaxy_demo/PointCloudDemo.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/launcher/ApplicationListPanel.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/life_demo/Main.java

index 8b6057f..899d46f 100644 (file)
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -40,6 +40,9 @@ java -cp target/sixth-3d-demos.jar eu.svjatoslav.sixth.e3d.examples.life_demo.Ma
 
 # Run Point Cloud Galaxy demo
 java -cp target/sixth-3d-demos.jar eu.svjatoslav.sixth.e3d.examples.galaxy_demo.PointCloudDemo
+
+# Run Winding Order demo (tests backface culling)
+java -cp target/sixth-3d-demos.jar eu.svjatoslav.sixth.e3d.examples.WindingOrderDemo
 ```
 
 ### Testing
@@ -67,6 +70,7 @@ src/main/java/eu/svjatoslav/sixth/e3d/examples/
 ├── TextEditorDemo.java
 ├── TextEditorDemo2.java
 ├── RainingNumbersDemo.java
+├── WindingOrderDemo.java    - Tests winding order & backface culling
 └── package-info.java
 ```
 
@@ -116,6 +120,13 @@ public class MyShape extends AbstractCompositeShape {
 
 Implement `MouseInteractionController` for mouse events, or extend input tracker classes for keyboard input.
 
+### Polygon Winding Order
+
+When creating triangles with backface culling enabled, use CCW winding in screen space:
+- Vertex order: top → lower-left → lower-right (as seen from camera)
+- `signedArea < 0` = front-facing = visible
+- See `WindingOrderDemo.java` for a minimal example
+
 ## Documentation
 
 Always make sure that documentation in`doc/index.org` stays up to date.
index 6ca38c4..92d3d10 100644 (file)
@@ -193,28 +193,52 @@ partitioned [[https://en.wikipedia.org/wiki/Octree][octree]] is used to compress
 the scene to raytrace current view through compressed voxel
 datastructure.
 
-** Fill-rate test
+** Graphics Benchmark
 :PROPERTIES:
-:CUSTOM_ID: fill-rate-test
+:CUSTOM_ID: graphics-benchmark
 :ID:       e5f6a7b8-c9d0-1234-ef01-345678901234
 :END:
 
-A benchmark for polygon rasterization performance testing. This demo
-tests pixel fill throughput by rendering 40 large screen-filling quads
-(600x600) layered at different Z depths.
+An automated graphics benchmark that measures the engine's rendering
+performance across different rendering modes.
+
+The benchmark creates a 16x16x16 grid of cubes (4096 total) and runs
+three tests sequentially, each for 30 seconds:
+
+- *Solid Cubes* - Tests solid-color polygon rasterization
+- *Textured Cubes* - Tests textured polygon rendering with texture sampling
+- *Wireframe Cubes* - Tests line rendering performance
+
+The camera follows a deterministic orbital path around the scene,
+ensuring reproducible results across runs.
+
+Example benchmark results:
+#+begin_example
+================================================================================
+                         GRAPHICS BENCHMARK RESULTS
+================================================================================
+Date:        2026-03-15 20:16:01
+Resolution:  1920x1080
+Cubes:       4096 (16x16x16 grid)
+Duration:    30 seconds per test
+
+--------------------------------------------------------------------------------
+SYSTEM INFORMATION
+--------------------------------------------------------------------------------
+CPU Name:    AMD Ryzen AI 9 HX 370 w/ Radeon 890M
+Arch:        amd64
+Cores:       24
+
+--------------------------------------------------------------------------------
+Test                         Avg FPS
+--------------------------------------------------------------------------------
+Solid Cubes                  35.65
+Textured Cubes               26.08
+Wireframe Cubes              33.67
+Star Grid                    256.88
+================================================================================
+#+end_example
 
-The low vertex count (4 vertices per quad) combined with high overdraw
-isolates the pixel fill cost from vertex processing overhead, allowing
-you to measure pure rasterization performance.
-
-Controls:
-| key | result              |
-|-----+---------------------|
-| 1   | Solid colored quads |
-| 2   | Textured quads      |
-
-Compare FPS between solid and textured modes to see exactly what
-texture sampling costs on your hardware.
 
 * Source code
 :PROPERTIES:
index ae7ff23..5aa5e3f 100755 (executable)
@@ -197,6 +197,9 @@ public class GraphDemo {
         addWobblySurface(geometryCollection, -200);
 
         setCameraLocation(viewFrame);
+        
+        // Ensure the render thread is started
+        viewFrame.getViewPanel().ensureRenderThreadStarted();
     }
 
     /**
index 23a5282..6b3accc 100755 (executable)
@@ -161,6 +161,9 @@ public class OctreeDemo extends WorldNavigationUserInputTracker {
 
         viewPanel.getKeyboardFocusStack().pushFocusOwner(this);
         viewPanel.repaintDuringNextViewUpdate();
+        
+        // Ensure the render thread is started
+        viewPanel.ensureRenderThreadStarted();
     }
 
     /**
index 7f97540..b3c9d3c 100644 (file)
@@ -94,5 +94,8 @@ public class RainingNumbersDemo implements FrameListener {
         }
 
         viewFrame.getViewPanel().addFrameListener(this);
+        
+        // Ensure the render thread is started
+        viewFrame.getViewPanel().ensureRenderThreadStarted();
     }
 }
index 8754044..30c4ef9 100755 (executable)
@@ -96,5 +96,8 @@ public class RandomPolygonsDemo {
         for (int i = 0; i < POLYGON_COUNT; i++)
             addRandomPolygon(shapeCollection);
 
+        // Ensure the render thread is started
+        viewFrame.getViewPanel().ensureRenderThreadStarted();
+
     }
 }
index c0cbd50..7d0b27d 100644 (file)
@@ -103,6 +103,9 @@ public class ShadedShapesDemo {
         viewPanel.addFrameListener(animator);
 
         viewPanel.repaintDuringNextViewUpdate();
+        
+        // Ensure the render thread is started
+        viewPanel.ensureRenderThreadStarted();
     }
 
     /**
index ec5c6b6..c9292a5 100644 (file)
@@ -44,6 +44,9 @@ public class TextEditorDemo {
         addGrid(shapeCollection);
 
         addTextEditors(viewPanel, shapeCollection);
+        
+        // Ensure the render thread is started
+        viewFrame.getViewPanel().ensureRenderThreadStarted();
     }
 
     /**
index 887fb30..936aed6 100644 (file)
@@ -66,6 +66,9 @@ public class TextEditorDemo2 {
         addGrid(shapeCollection);
 
         addCity(viewPanel, shapeCollection);
+        
+        // Ensure the render thread is started
+        viewFrame.getViewPanel().ensureRenderThreadStarted();
     }
 
     /**
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/WindingOrderDemo.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/WindingOrderDemo.java
new file mode 100644 (file)
index 0000000..d81426f
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ *
+ */
+package eu.svjatoslav.sixth.e3d.examples;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.ViewFrame;
+import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
+
+/**
+ * Demo to test winding order and backface culling documentation.
+ * <p>
+ * Creates one triangle with CCW winding (front face) following the docs:
+ * <ul>
+ *   <li>upper-center → lower-left → lower-right</li>
+ *   <li>Backface culling enabled</li>
+ * </ul>
+ * <p>
+ * Expected: green triangle visible (CCW = front face).
+ */
+public class WindingOrderDemo {
+
+    public static void main(String[] args) {
+        ViewFrame viewFrame = new ViewFrame();
+        ViewPanel viewPanel = viewFrame.getViewPanel();
+        ShapeCollection shapes = viewPanel.getRootShapeCollection();
+
+        double size = 150;
+
+        Point3D upperCenter = new Point3D(0, -size, 0);
+        Point3D lowerLeft = new Point3D(-size, +size, 0);
+        Point3D lowerRight = new Point3D(+size, +size, 0);
+
+        SolidPolygon triangle = new SolidPolygon(upperCenter, lowerLeft, lowerRight, Color.GREEN);
+        triangle.setBackfaceCulling(true);
+
+        shapes.addShape(triangle);
+
+        viewPanel.getCamera().getTransform().setTranslation(new Point3D(0, 0, -500));
+
+        viewPanel.ensureRenderThreadStarted();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/BenchmarkTest.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/BenchmarkTest.java
new file mode 100644 (file)
index 0000000..dec1514
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+package eu.svjatoslav.sixth.e3d.examples.benchmark;
+
+import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
+
+/**
+ * Interface for a single benchmark test.
+ * Implementations define how to set up and tear down the test scene.
+ */
+public interface BenchmarkTest {
+
+    /**
+     * Returns the display name for this benchmark test.
+     * @return the test name
+     */
+    String getName();
+
+    /**
+     * Sets up the test scene by adding shapes to the collection.
+     * @param shapes the shape collection to populate
+     */
+    void setup(ShapeCollection shapes);
+
+    /**
+     * Tears down the test scene by removing all shapes added during setup.
+     * @param shapes the shape collection to clean up
+     */
+    void teardown(ShapeCollection shapes);
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/GraphicsBenchmark.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/GraphicsBenchmark.java
new file mode 100644 (file)
index 0000000..96a50dc
--- /dev/null
@@ -0,0 +1,263 @@
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+package eu.svjatoslav.sixth.e3d.examples.benchmark;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.Camera;
+import eu.svjatoslav.sixth.e3d.gui.FrameListener;
+import eu.svjatoslav.sixth.e3d.gui.ViewFrame;
+import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
+import eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardInputHandler;
+import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
+
+import javax.swing.SwingUtilities;
+import java.awt.event.KeyEvent;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Automated graphics benchmark that tests the engine's rendering performance.
+ * Runs multiple tests sequentially, each for a fixed duration, and outputs
+ * reproducible benchmark results to standard output.
+ * 
+ * <p>The benchmark creates a 16x16x16 grid of cubes (4096 total) with the camera
+ * following a deterministic orbital path. Each test runs for 30 seconds by default.</p>
+ * 
+ * <p>Press <b>Space</b> to skip to the next test immediately.</p>
+ * 
+ * <p>Available tests:</p>
+ * <ul>
+ *   <li>{@link SolidCubesTest} - Solid-color polygon rendering</li>
+ *   <li>{@link TexturedCubesTest} - Textured polygon rendering</li>
+ *   <li>{@link WireframeCubesTest} - Line rendering</li>
+ *   <li>{@link StarGridTest} - Billboard (glowing point) rendering</li>
+ * </ul>
+ */
+public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
+
+    private static final int WINDOW_WIDTH = 1920;
+    private static final int WINDOW_HEIGHT = 1080;
+    private static final int GRID_SIZE = 16;
+    private static final double ORBIT_DISTANCE = 1200;
+    private static final double ORBIT_SPEED = 0.0003;
+    private static final double WOBBLE_AMPLITUDE = 800;
+    private static final int TEST_DURATION_MS = 30000;
+
+    private ViewFrame viewFrame;
+    private ViewPanel viewPanel;
+    private ShapeCollection shapes;
+    private Camera camera;
+
+    private double orbitAngle = 0;
+    private long testStartTime;
+    private long frameCount;
+    private BenchmarkTest currentTest;
+    private boolean testFinished = false;
+    private boolean benchmarkFinished = false;
+    private boolean pendingTestTransition = false;
+    private final List<TestResult> results = new ArrayList<>();
+    private final List<BenchmarkTest> tests = new ArrayList<>();
+
+    /**
+     * Entry point for the graphics benchmark.
+     * @param args command line arguments (ignored)
+     */
+    public static void main(String[] args) {
+        new GraphicsBenchmark();
+    }
+
+/**
+     * Constructs and runs the graphics benchmark.
+     */
+    public GraphicsBenchmark() {
+        registerTests();        // populate tests FIRST, before render thread starts
+        initializeWindow();     // now onFrame can safely access the tests list
+    }
+
+    private void initializeWindow() {
+        viewFrame = new ViewFrame(WINDOW_WIDTH, WINDOW_HEIGHT);
+        viewPanel = viewFrame.getViewPanel();
+        viewPanel.setFrameRate(0);
+        shapes = viewPanel.getRootShapeCollection();
+        viewPanel.addFrameListener(this);
+        viewPanel.getKeyboardFocusStack().pushFocusOwner(this);
+        camera = viewPanel.getCamera();
+        // Now explicitly start the render thread after all listeners are registered
+        viewPanel.ensureRenderThreadStarted();
+    }
+
+    private void registerTests() {
+        tests.add(new SolidCubesTest());
+        tests.add(new TexturedCubesTest());
+        tests.add(new WireframeCubesTest());
+        tests.add(new StarGridTest());
+    }
+
+    private void startNextTest() {
+        int nextIndex = results.size();
+        if (nextIndex >= tests.size()) {
+            scheduleBenchmarkFinish();
+            return;
+        }
+
+        currentTest = tests.get(nextIndex);
+        testFinished = false;
+        orbitAngle = 0;
+        frameCount = 0;
+        testStartTime = System.currentTimeMillis();
+
+        currentTest.setup(shapes);
+    }
+
+    private void finishCurrentTest() {
+        if (currentTest == null || testFinished) {
+            return;
+        }
+
+        testFinished = true;
+        long elapsed = System.currentTimeMillis() - testStartTime;
+        double durationSeconds = elapsed / 1000.0;
+        results.add(new TestResult(currentTest.getName(), frameCount, durationSeconds));
+
+        // Defer test transition to safe point (beginning of next frame)
+        pendingTestTransition = true;
+    }
+
+    private void performTestTransition() {
+        if (currentTest != null) {
+            currentTest.teardown(shapes);
+        }
+        startNextTest();
+    }
+
+    private void scheduleBenchmarkFinish() {
+        benchmarkFinished = true;
+        SwingUtilities.invokeLater(this::finishBenchmark);
+    }
+
+    private void finishBenchmark() {
+        currentTest = null;
+        viewPanel.getKeyboardFocusStack().popFocusOwner();
+        viewPanel.removeFrameListener(this);
+        viewPanel.stop();
+        viewFrame.dispose();
+        printResults();
+    }
+
+    private void printResults() {
+        String separator = "================================================================================";
+        String thinSeparator = "--------------------------------------------------------------------------------";
+
+        Runtime runtime = Runtime.getRuntime();
+
+        System.out.println(separator);
+        System.out.println("                         GRAPHICS BENCHMARK RESULTS");
+        System.out.println(separator);
+        System.out.println("Date:        " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+        System.out.println("Resolution:  " + WINDOW_WIDTH + "x" + WINDOW_HEIGHT);
+        System.out.println("Cubes:       " + (GRID_SIZE * GRID_SIZE * GRID_SIZE) + " (" + GRID_SIZE + "x" + GRID_SIZE + "x" + GRID_SIZE + " grid)");
+        System.out.println("Duration:    " + (TEST_DURATION_MS / 1000) + " seconds per test");
+        System.out.println();
+        System.out.println(thinSeparator);
+        System.out.println("SYSTEM INFORMATION");
+        System.out.println(thinSeparator);
+        System.out.println("CPU Name:    " + getCpuName());
+        System.out.println("Arch:        " + System.getProperty("os.arch"));
+        System.out.println("Cores:       " + runtime.availableProcessors());
+        System.out.println();
+        System.out.println(thinSeparator);
+        System.out.printf("%-28s %s%n", "Test", "Avg FPS");
+        System.out.println(thinSeparator);
+
+        for (TestResult result : results) {
+            System.out.printf("%-28s %.2f%n", result.testName, result.averageFps);
+        }
+
+        System.out.println(separator);
+    }
+
+    private String getCpuName() {
+        try {
+            java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader("/proc/cpuinfo"));
+            String line;
+            while ((line = reader.readLine()) != null) {
+                if (line.startsWith("model name")) {
+                    reader.close();
+                    return line.substring(line.indexOf(':') + 1).trim();
+                }
+            }
+            reader.close();
+        } catch (Exception ignored) {
+        }
+        return System.getProperty("java.vm.name", "Unknown");
+    }
+
+    @Override
+    public boolean onFrame(ViewPanel viewPanel, int millisecondsSinceLastFrame) {
+        // Perform deferred test transition at safe point (before render cycle starts)
+        if (pendingTestTransition) {
+            pendingTestTransition = false;
+            performTestTransition();
+        }
+
+        // Deferred first-test start: runs on the render thread, before renderFrame()
+        if (currentTest == null && !benchmarkFinished && results.isEmpty()) {
+            startNextTest();
+        }
+
+        if (benchmarkFinished) {
+            return false;
+        }
+
+        if (currentTest == null) {
+            return false;
+        }
+
+        orbitAngle += ORBIT_SPEED * millisecondsSinceLastFrame;
+
+        double x = Math.sin(orbitAngle) * ORBIT_DISTANCE;
+        double z = Math.cos(orbitAngle) * ORBIT_DISTANCE;
+        double y = Math.sin(orbitAngle * 1.8934) * WOBBLE_AMPLITUDE;
+
+        camera.getTransform().setTranslation(new Point3D(x, y, z));
+        camera.lookAt(new Point3D(0, 0, 0));
+
+        frameCount++;
+
+        long elapsed = System.currentTimeMillis() - testStartTime;
+        if (elapsed >= TEST_DURATION_MS && !testFinished) {
+            finishCurrentTest();
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean keyPressed(KeyEvent event, ViewPanel viewPanel) {
+        if (event.getKeyChar() == ' ') {
+            finishCurrentTest();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean keyReleased(KeyEvent event, ViewPanel viewPanel) {
+        return false;
+    }
+
+    @Override
+    public boolean focusLost(ViewPanel viewPanel) {
+        return false;
+    }
+
+    @Override
+    public boolean focusReceived(ViewPanel viewPanel) {
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/SolidCubesTest.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/SolidCubesTest.java
new file mode 100644 (file)
index 0000000..d9e9773
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+package eu.svjatoslav.sixth.e3d.examples.benchmark;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCube;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Benchmark test for solid-color cubes.
+ * Renders a grid of cubes with random semi-transparent colors to test
+ * solid polygon rasterization performance.
+ */
+public class SolidCubesTest implements BenchmarkTest {
+
+    private static final int GRID_SIZE = 16;
+    private static final double SPACING = 80;
+    private static final double CUBE_SIZE = 25;
+    private static final long RANDOM_SEED = 42;
+
+    private final Random random = new Random(RANDOM_SEED);
+    private final List<Object> cubes = new ArrayList<>();
+
+    @Override
+    public String getName() {
+        return "Solid Cubes";
+    }
+
+    @Override
+    public void setup(ShapeCollection shapes) {
+        random.setSeed(RANDOM_SEED);
+        double offset = -(GRID_SIZE - 1) * SPACING / 2;
+
+        for (int x = 0; x < GRID_SIZE; x++) {
+            for (int y = 0; y < GRID_SIZE; y++) {
+                for (int z = 0; z < GRID_SIZE; z++) {
+                    double px = offset + x * SPACING;
+                    double py = offset + y * SPACING;
+                    double pz = offset + z * SPACING;
+
+                    Color color = new Color(
+                            50 + random.nextInt(150),
+                            100 + random.nextInt(155),
+                            50 + random.nextInt(150),
+                            40
+                    );
+                    SolidPolygonCube cube = new SolidPolygonCube(
+                            new Point3D(px, py, pz),
+                            CUBE_SIZE,
+                            color
+                    );
+                    shapes.addShape(cube);
+                    cubes.add(cube);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void teardown(ShapeCollection shapes) {
+        for (Object cube : cubes) {
+            shapes.getShapes().remove(cube);
+        }
+        cubes.clear();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/StarGridTest.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/StarGridTest.java
new file mode 100644 (file)
index 0000000..967a621
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+package eu.svjatoslav.sixth.e3d.examples.benchmark;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.GlowingPoint;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Benchmark test for glowing point (star) billboards.
+ * Renders a grid of glowing points arranged in a cube formation to test
+ * billboard rendering and texture blending performance.
+ */
+public class StarGridTest implements BenchmarkTest {
+
+    private static final int GRID_SIZE = 16;
+    private static final double SPACING = 80;
+    private static final double STAR_SIZE = 20;
+    private static final int UNIQUE_COLORS_COUNT = 30;
+    private static final long RANDOM_SEED = 42;
+
+    private final Random random = new Random(RANDOM_SEED);
+    private final List<Object> stars = new ArrayList<>();
+    private List<Color> colors;
+
+    @Override
+    public String getName() {
+        return "Star Grid";
+    }
+
+    @Override
+    public void setup(ShapeCollection shapes) {
+        initializeColors();
+        random.setSeed(RANDOM_SEED);
+        double offset = -(GRID_SIZE - 1) * SPACING / 2;
+
+        for (int x = 0; x < GRID_SIZE; x++) {
+            for (int y = 0; y < GRID_SIZE; y++) {
+                for (int z = 0; z < GRID_SIZE; z++) {
+                    double px = offset + x * SPACING;
+                    double py = offset + y * SPACING;
+                    double pz = offset + z * SPACING;
+
+                    Color color = colors.get(random.nextInt(colors.size()));
+                    GlowingPoint star = new GlowingPoint(
+                            new Point3D(px, py, pz),
+                            STAR_SIZE,
+                            color
+                    );
+                    shapes.addShape(star);
+                    stars.add(star);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void teardown(ShapeCollection shapes) {
+        for (Object star : stars) {
+            shapes.getShapes().remove(star);
+        }
+        stars.clear();
+    }
+
+    private void initializeColors() {
+        colors = new ArrayList<>();
+        random.setSeed(RANDOM_SEED);
+        for (int i = 0; i < UNIQUE_COLORS_COUNT; i++) {
+            colors.add(new Color(
+                    random.nextDouble() + 0.5,
+                    random.nextDouble() + 0.5,
+                    random.nextDouble() + 0.5,
+                    255
+            ));
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/TestResult.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/TestResult.java
new file mode 100644 (file)
index 0000000..9c7eef2
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+package eu.svjatoslav.sixth.e3d.examples.benchmark;
+
+/**
+ * Holds the results of a single benchmark test.
+ */
+public class TestResult {
+
+    /** The name of the benchmark test. */
+    public final String testName;
+
+    /** Total number of frames rendered during the test. */
+    public final long frameCount;
+
+    /** Duration of the test in seconds. */
+    public final double durationSeconds;
+
+    /** Average frames per second achieved during the test. */
+    public final double averageFps;
+
+    /**
+     * Creates a test result record.
+     * @param testName the name of the test
+     * @param frameCount total frames rendered
+     * @param durationSeconds test duration in seconds
+     */
+    public TestResult(String testName, long frameCount, double durationSeconds) {
+        this.testName = testName;
+        this.frameCount = frameCount;
+        this.durationSeconds = durationSeconds;
+        this.averageFps = frameCount / durationSeconds;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/TexturedCube.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/TexturedCube.java
new file mode 100644 (file)
index 0000000..ecb883e
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+package eu.svjatoslav.sixth.e3d.examples.benchmark;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point2D;
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.math.Vertex;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
+import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture;
+
+/**
+ * A cube composed of textured polygons.
+ * Used by the {@link TexturedCubesTest} benchmark to measure texture rendering performance.
+ */
+public class TexturedCube extends AbstractCompositeShape {
+
+    /**
+     * Creates a textured cube centered at the given position.
+     * @param center the center position of the cube
+     * @param size half the edge length of the cube
+     * @param texture the texture to apply to all faces
+     */
+    public TexturedCube(Point3D center, double size, Texture texture) {
+        double s = size;
+        Point3D p1 = new Point3D(center.x - s, center.y - s, center.z - s);
+        Point3D p7 = new Point3D(center.x + s, center.y + s, center.z + s);
+
+        Point3D p2 = new Point3D(p7.x, p1.y, p1.z);
+        Point3D p3 = new Point3D(p7.x, p1.y, p7.z);
+        Point3D p4 = new Point3D(p1.x, p1.y, p7.z);
+        Point3D p5 = new Point3D(p1.x, p7.y, p1.z);
+        Point3D p6 = new Point3D(p7.x, p7.y, p1.z);
+        Point3D p8 = new Point3D(p1.x, p7.y, p7.z);
+
+        Point2D t00 = new Point2D(0, 0);
+        Point2D t10 = new Point2D(64, 0);
+        Point2D t01 = new Point2D(0, 64);
+        Point2D t11 = new Point2D(64, 64);
+
+        addTexturedFace(p1, p2, p3, p4, t00, t10, t11, t01, texture);
+        addTexturedFace(p5, p8, p7, p6, t00, t01, t11, t10, texture);
+        addTexturedFace(p1, p5, p6, p2, t00, t01, t11, t10, texture);
+        addTexturedFace(p3, p7, p8, p4, t00, t10, t11, t01, texture);
+        addTexturedFace(p1, p4, p8, p5, t00, t10, t11, t01, texture);
+        addTexturedFace(p2, p6, p7, p3, t00, t01, t11, t10, texture);
+
+        setBackfaceCulling(true);
+    }
+
+    private void addTexturedFace(Point3D p1, Point3D p2, Point3D p3, Point3D p4,
+                                 Point2D t1, Point2D t2, Point2D t3, Point2D t4,
+                                 Texture texture) {
+        TexturedPolygon tri1 = new TexturedPolygon(
+                new Vertex(p1, t1),
+                new Vertex(p2, t2),
+                new Vertex(p3, t3),
+                texture
+        );
+        tri1.setBackfaceCulling(true);
+
+        TexturedPolygon tri2 = new TexturedPolygon(
+                new Vertex(p1, t1),
+                new Vertex(p3, t3),
+                new Vertex(p4, t4),
+                texture
+        );
+        tri2.setBackfaceCulling(true);
+
+        addShape(tri1);
+        addShape(tri2);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/TexturedCubesTest.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/TexturedCubesTest.java
new file mode 100644 (file)
index 0000000..c8f4798
--- /dev/null
@@ -0,0 +1,120 @@
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+package eu.svjatoslav.sixth.e3d.examples.benchmark;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
+import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Benchmark test for textured cubes.
+ * Renders a grid of cubes with textures to test textured polygon rendering
+ * and texture sampling performance.
+ */
+public class TexturedCubesTest implements BenchmarkTest {
+
+    private static final int GRID_SIZE = 16;
+    private static final double SPACING = 80;
+    private static final double CUBE_SIZE = 25;
+    private static final int TEXTURE_COUNT = 20;
+    private static final long RANDOM_SEED = 42;
+
+    private final Random random = new Random(RANDOM_SEED);
+    private final List<Object> cubes = new ArrayList<>();
+    private Texture[] textures;
+    private int[] cubeTextureIndices;
+
+    @Override
+    public String getName() {
+        return "Textured Cubes";
+    }
+
+    @Override
+    public void setup(ShapeCollection shapes) {
+        initializeTextures();
+        initializeCubeTextureIndices();
+
+        double offset = -(GRID_SIZE - 1) * SPACING / 2;
+        int idx = 0;
+
+        for (int x = 0; x < GRID_SIZE; x++) {
+            for (int y = 0; y < GRID_SIZE; y++) {
+                for (int z = 0; z < GRID_SIZE; z++) {
+                    double px = offset + x * SPACING;
+                    double py = offset + y * SPACING;
+                    double pz = offset + z * SPACING;
+
+                    Texture tex = textures[cubeTextureIndices[idx]];
+                    TexturedCube cube = new TexturedCube(
+                            new Point3D(px, py, pz),
+                            CUBE_SIZE,
+                            tex
+                    );
+                    shapes.addShape(cube);
+                    cubes.add(cube);
+                    idx++;
+                }
+            }
+        }
+    }
+
+    @Override
+    public void teardown(ShapeCollection shapes) {
+        for (Object cube : cubes) {
+            shapes.getShapes().remove(cube);
+        }
+        cubes.clear();
+    }
+
+    private void initializeTextures() {
+        textures = new Texture[TEXTURE_COUNT];
+        for (int i = 0; i < TEXTURE_COUNT; i++) {
+            textures[i] = createGlowTexture(
+                    50 + random.nextInt(200),
+                    50 + random.nextInt(200),
+                    50 + random.nextInt(200)
+            );
+        }
+    }
+
+    private void initializeCubeTextureIndices() {
+        random.setSeed(RANDOM_SEED);
+        cubeTextureIndices = new int[GRID_SIZE * GRID_SIZE * GRID_SIZE];
+        for (int i = 0; i < cubeTextureIndices.length; i++) {
+            cubeTextureIndices[i] = random.nextInt(TEXTURE_COUNT);
+        }
+    }
+
+    private Texture createGlowTexture(int r, int g, int b) {
+        int texSize = 64;
+        Texture texture = new Texture(texSize, texSize, 2);
+
+        java.awt.Graphics2D gr = texture.graphics;
+        gr.setBackground(new java.awt.Color(r, g, b, 80));
+        gr.clearRect(0, 0, texSize, texSize);
+
+        int glowWidth = 6;
+        for (int i = 0; i < glowWidth; i++) {
+            int intensity = (int) (255.0 * (glowWidth - i) / glowWidth);
+            java.awt.Color glowColor = new java.awt.Color(
+                    Math.min(255, r + intensity),
+                    Math.min(255, g + intensity),
+                    Math.min(255, b + intensity),
+                    200 - i * 30
+            );
+            gr.setColor(glowColor);
+            gr.drawRect(i, i, texSize - 1 - 2 * i, texSize - 1 - 2 * i);
+        }
+
+        gr.dispose();
+        texture.resetResampledBitmapCache();
+        return texture;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/WireframeCubesTest.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/WireframeCubesTest.java
new file mode 100644 (file)
index 0000000..c625627
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+package eu.svjatoslav.sixth.e3d.examples.benchmark;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe.WireframeBox;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Benchmark test for wireframe cubes.
+ * Renders a grid of wireframe cube outlines to test line rendering performance.
+ */
+public class WireframeCubesTest implements BenchmarkTest {
+
+    private static final int GRID_SIZE = 16;
+    private static final double SPACING = 80;
+    private static final double CUBE_SIZE = 25;
+    private static final long RANDOM_SEED = 42;
+
+    private final Random random = new Random(RANDOM_SEED);
+    private final List<Object> cubes = new ArrayList<>();
+
+    @Override
+    public String getName() {
+        return "Wireframe Cubes";
+    }
+
+    @Override
+    public void setup(ShapeCollection shapes) {
+        random.setSeed(RANDOM_SEED);
+        double offset = -(GRID_SIZE - 1) * SPACING / 2;
+
+        for (int x = 0; x < GRID_SIZE; x++) {
+            for (int y = 0; y < GRID_SIZE; y++) {
+                for (int z = 0; z < GRID_SIZE; z++) {
+                    double px = offset + x * SPACING;
+                    double py = offset + y * SPACING;
+                    double pz = offset + z * SPACING;
+
+                    Color color = new Color(
+                            100 + random.nextInt(155),
+                            100 + random.nextInt(155),
+                            100 + random.nextInt(155)
+                    );
+                    LineAppearance appearance = new LineAppearance(5.5, color);
+
+                    Point3D center = new Point3D(px, py, pz);
+                    Point3D p1 = new Point3D(center.x - CUBE_SIZE, center.y - CUBE_SIZE, center.z - CUBE_SIZE);
+                    Point3D p2 = new Point3D(center.x + CUBE_SIZE, center.y + CUBE_SIZE, center.z + CUBE_SIZE);
+
+                    WireframeBox cube = new WireframeBox(p1, p2, appearance);
+                    shapes.addShape(cube);
+                    cubes.add(cube);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void teardown(ShapeCollection shapes) {
+        for (Object cube : cubes) {
+            shapes.getShapes().remove(cube);
+        }
+        cubes.clear();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/package-info.java
new file mode 100644 (file)
index 0000000..f95f2b4
--- /dev/null
@@ -0,0 +1,20 @@
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+/**
+ * Graphics benchmark components for measuring the Sixth 3D engine's rendering performance.
+ * 
+ * <p>The benchmark suite includes:</p>
+ * <ul>
+ *   <li>{@link eu.svjatoslav.sixth.e3d.examples.benchmark.GraphicsBenchmark} - Main benchmark runner</li>
+ *   <li>{@link eu.svjatoslav.sixth.e3d.examples.benchmark.BenchmarkTest} - Interface for test implementations</li>
+ *   <li>{@link eu.svjatoslav.sixth.e3d.examples.benchmark.SolidCubesTest} - Solid-color cube rendering test</li>
+ *   <li>{@link eu.svjatoslav.sixth.e3d.examples.benchmark.TexturedCubesTest} - Textured cube rendering test</li>
+ *   <li>{@link eu.svjatoslav.sixth.e3d.examples.benchmark.WireframeCubesTest} - Wireframe cube rendering test</li>
+ *   <li>{@link eu.svjatoslav.sixth.e3d.examples.benchmark.StarGridTest} - Billboard (glowing point) rendering test</li>
+ * </ul>
+ */
+
+package eu.svjatoslav.sixth.e3d.examples.benchmark;
\ No newline at end of file
index bc9f227..02d3c3e 100644 (file)
@@ -33,5 +33,8 @@ public class PointCloudDemo {
         // add galaxy
         geometryCollection.addShape(new Galaxy(500, 3, 10000, transform));
 
+        // Ensure the render thread is started
+        viewFrame.getViewPanel().ensureRenderThreadStarted();
+
     }
 }
index 424406d..ea856e6 100644 (file)
@@ -6,9 +6,15 @@
 
 package eu.svjatoslav.sixth.e3d.examples.launcher;
 
-import eu.svjatoslav.sixth.e3d.examples.*;
-import eu.svjatoslav.sixth.e3d.examples.galaxy_demo.PointCloudDemo;
+import eu.svjatoslav.sixth.e3d.examples.GraphDemo;
+import eu.svjatoslav.sixth.e3d.examples.OctreeDemo;
+import eu.svjatoslav.sixth.e3d.examples.RandomPolygonsDemo;
+import eu.svjatoslav.sixth.e3d.examples.RainingNumbersDemo;
 import eu.svjatoslav.sixth.e3d.examples.ShadedShapesDemo;
+import eu.svjatoslav.sixth.e3d.examples.TextEditorDemo;
+import eu.svjatoslav.sixth.e3d.examples.TextEditorDemo2;
+import eu.svjatoslav.sixth.e3d.examples.benchmark.GraphicsBenchmark;
+import eu.svjatoslav.sixth.e3d.examples.galaxy_demo.PointCloudDemo;
 
 import javax.swing.*;
 import java.awt.event.ActionEvent;
@@ -34,7 +40,7 @@ class ApplicationListPanel extends JPanel {
         sequentialGroup.addComponent(new JButton(new ShowGameOfLife()));
         sequentialGroup.addComponent(new JButton(new ShowRandomPolygons()));
         sequentialGroup.addComponent(new JButton(new ShowShadedShapes()));
-        sequentialGroup.addComponent(new JButton(new ShowFillRateTest()));
+        sequentialGroup.addComponent(new JButton(new ShowGraphicsBenchmark()));
     }
 
     /** Action to launch the TextEditorDemo. */
@@ -146,15 +152,15 @@ class ApplicationListPanel extends JPanel {
         }
     }
 
-    /** Action to launch the FillRateTest benchmark. */
-    private static class ShowFillRateTest extends AbstractAction {
-        ShowFillRateTest() {
-            putValue(NAME, "Fill-rate Test");
+    /** Action to launch the GraphicsBenchmark. */
+    private static class ShowGraphicsBenchmark extends AbstractAction {
+        ShowGraphicsBenchmark() {
+            putValue(NAME, "Graphics Benchmark");
         }
 
         @Override
         public void actionPerformed(final ActionEvent e) {
-            FillRateTest.main(null);
+            GraphicsBenchmark.main(null);
         }
     }
 
index 86ac7f1..fd8398b 100644 (file)
@@ -87,6 +87,9 @@ public class Main extends WorldNavigationUserInputTracker {
 
         // Done! World is built. So ensure screen is updated too.
         viewPanel.repaintDuringNextViewUpdate();
+        
+        // Ensure the render thread is started
+        viewPanel.ensureRenderThreadStarted();
     }
 
     /**