From: Svjatoslav Agejenko Date: Fri, 20 Mar 2026 21:03:16 +0000 (+0200) Subject: feat: add graphics benchmark framework X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;h=fa1fb679a4bad997e91a4c1db38d036714471ba6;p=sixth-3d-demos.git feat: add graphics benchmark framework - 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 --- diff --git a/AGENTS.md b/AGENTS.md index 8b6057f..899d46f 100644 --- 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. diff --git a/doc/index.org b/doc/index.org index 6ca38c4..92d3d10 100644 --- a/doc/index.org +++ b/doc/index.org @@ -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: diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/GraphDemo.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/GraphDemo.java index ae7ff23..5aa5e3f 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/examples/GraphDemo.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/GraphDemo.java @@ -197,6 +197,9 @@ public class GraphDemo { addWobblySurface(geometryCollection, -200); setCameraLocation(viewFrame); + + // Ensure the render thread is started + viewFrame.getViewPanel().ensureRenderThreadStarted(); } /** diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/OctreeDemo.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/OctreeDemo.java index 23a5282..6b3accc 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/examples/OctreeDemo.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/OctreeDemo.java @@ -161,6 +161,9 @@ public class OctreeDemo extends WorldNavigationUserInputTracker { viewPanel.getKeyboardFocusStack().pushFocusOwner(this); viewPanel.repaintDuringNextViewUpdate(); + + // Ensure the render thread is started + viewPanel.ensureRenderThreadStarted(); } /** diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/RainingNumbersDemo.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/RainingNumbersDemo.java index 7f97540..b3c9d3c 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/examples/RainingNumbersDemo.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/RainingNumbersDemo.java @@ -94,5 +94,8 @@ public class RainingNumbersDemo implements FrameListener { } viewFrame.getViewPanel().addFrameListener(this); + + // Ensure the render thread is started + viewFrame.getViewPanel().ensureRenderThreadStarted(); } } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/RandomPolygonsDemo.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/RandomPolygonsDemo.java index 8754044..30c4ef9 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/examples/RandomPolygonsDemo.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/RandomPolygonsDemo.java @@ -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(); + } } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/ShadedShapesDemo.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/ShadedShapesDemo.java index c0cbd50..7d0b27d 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/examples/ShadedShapesDemo.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/ShadedShapesDemo.java @@ -103,6 +103,9 @@ public class ShadedShapesDemo { viewPanel.addFrameListener(animator); viewPanel.repaintDuringNextViewUpdate(); + + // Ensure the render thread is started + viewPanel.ensureRenderThreadStarted(); } /** diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/TextEditorDemo.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/TextEditorDemo.java index ec5c6b6..c9292a5 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/examples/TextEditorDemo.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/TextEditorDemo.java @@ -44,6 +44,9 @@ public class TextEditorDemo { addGrid(shapeCollection); addTextEditors(viewPanel, shapeCollection); + + // Ensure the render thread is started + viewFrame.getViewPanel().ensureRenderThreadStarted(); } /** diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/TextEditorDemo2.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/TextEditorDemo2.java index 887fb30..936aed6 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/examples/TextEditorDemo2.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/TextEditorDemo2.java @@ -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 index 0000000..d81426f --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/WindingOrderDemo.java @@ -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. + *

+ * Creates one triangle with CCW winding (front face) following the docs: + *

+ *

+ * 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 index 0000000..dec1514 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/BenchmarkTest.java @@ -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 index 0000000..96a50dc --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/GraphicsBenchmark.java @@ -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. + * + *

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.

+ * + *

Press Space to skip to the next test immediately.

+ * + *

Available tests:

+ * + */ +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 results = new ArrayList<>(); + private final List 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 index 0000000..d9e9773 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/SolidCubesTest.java @@ -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 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 index 0000000..967a621 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/StarGridTest.java @@ -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 stars = new ArrayList<>(); + private List 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 index 0000000..9c7eef2 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/TestResult.java @@ -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 index 0000000..ecb883e --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/TexturedCube.java @@ -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 index 0000000..c8f4798 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/TexturedCubesTest.java @@ -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 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 index 0000000..c625627 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/WireframeCubesTest.java @@ -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 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 index 0000000..f95f2b4 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/package-info.java @@ -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. + * + *

The benchmark suite includes:

+ *
    + *
  • {@link eu.svjatoslav.sixth.e3d.examples.benchmark.GraphicsBenchmark} - Main benchmark runner
  • + *
  • {@link eu.svjatoslav.sixth.e3d.examples.benchmark.BenchmarkTest} - Interface for test implementations
  • + *
  • {@link eu.svjatoslav.sixth.e3d.examples.benchmark.SolidCubesTest} - Solid-color cube rendering test
  • + *
  • {@link eu.svjatoslav.sixth.e3d.examples.benchmark.TexturedCubesTest} - Textured cube rendering test
  • + *
  • {@link eu.svjatoslav.sixth.e3d.examples.benchmark.WireframeCubesTest} - Wireframe cube rendering test
  • + *
  • {@link eu.svjatoslav.sixth.e3d.examples.benchmark.StarGridTest} - Billboard (glowing point) rendering test
  • + *
+ */ + +package eu.svjatoslav.sixth.e3d.examples.benchmark; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/galaxy_demo/PointCloudDemo.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/galaxy_demo/PointCloudDemo.java index bc9f227..02d3c3e 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/examples/galaxy_demo/PointCloudDemo.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/galaxy_demo/PointCloudDemo.java @@ -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(); + } } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/launcher/ApplicationListPanel.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/launcher/ApplicationListPanel.java index 424406d..ea856e6 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/examples/launcher/ApplicationListPanel.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/launcher/ApplicationListPanel.java @@ -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); } } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/life_demo/Main.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/life_demo/Main.java index 86ac7f1..fd8398b 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/examples/life_demo/Main.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/life_demo/Main.java @@ -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(); } /**