From: Svjatoslav Agejenko Date: Sun, 15 Mar 2026 02:43:56 +0000 (+0200) Subject: refactor(examples): convert GraphicsBenchmark to automated test runner X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;h=05bdc04e200c2f2679465154a3ac5883a0193fab;p=sixth-3d-demos.git refactor(examples): convert GraphicsBenchmark to automated test runner Change from interactive demo with keyboard controls to an automated benchmark that runs multiple tests sequentially for fixed durations and outputs reproducible results. --- diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/GraphicsBenchmark.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/GraphicsBenchmark.java index a861c4b..4c523c0 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/examples/GraphicsBenchmark.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/examples/GraphicsBenchmark.java @@ -11,7 +11,6 @@ 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.WorldNavigationUserInputTracker; import eu.svjatoslav.sixth.e3d.math.Vertex; import eu.svjatoslav.sixth.e3d.renderer.raster.Color; import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection; @@ -20,70 +19,180 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCom import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCube; import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture; -import java.awt.event.KeyEvent; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.Random; /** - * Fill-rate benchmark demo that tests the engine's rendering performance. - * Creates a 16x16x16 grid of cubes (4096 total) that orbit around the center point. - * The camera follows a wobbling orbital path while FPS statistics are printed to console. + * 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. * - *

Key controls: - *

+ *

The benchmark creates a 16x16x16 grid of cubes (4096 total) with the camera + * following a deterministic orbital path. Each test runs for 30 seconds.

* * @see SolidPolygonCube * @see TexturedPolygon */ -public class GraphicsBenchmark extends WorldNavigationUserInputTracker implements FrameListener { +public class GraphicsBenchmark implements FrameListener { - /** Number of cubes along each axis of the grid. */ + private static final int WINDOW_WIDTH = 1920; + private static final int WINDOW_HEIGHT = 1080; private static final int GRID_SIZE = 16; - /** Spacing between cube centers in world units. */ private static final double SPACING = 80; - /** Half-size of each cube. */ private static final double CUBE_SIZE = 25; - /** Distance of camera from center during orbit. */ private static final double ORBIT_DISTANCE = 1200; - /** Angular speed of camera orbit in radians per millisecond. */ private static final double ORBIT_SPEED = 0.0003; - /** Amplitude of vertical wobble during orbit. */ private static final double WOBBLE_AMPLITUDE = 800; - /** Number of unique textures to create for textured mode. */ private static final int TEXTURE_COUNT = 20; + private static final int TEST_DURATION_MS = 30000; + private static final long RANDOM_SEED = 42; - private final ViewFrame viewFrame; - private final ViewPanel viewPanel; - private final ShapeCollection shapes; - private final Random random = new Random(42); - private final Camera camera; + private ViewFrame viewFrame; + private ViewPanel viewPanel; + private ShapeCollection shapes; + private final Random random = new Random(RANDOM_SEED); + private Camera camera; - private final List cubes = new ArrayList<>(); private final Texture[] textures = new Texture[TEXTURE_COUNT]; - private final int[] cubeTextureIndices; + private final int[] cubeTextureIndices = new int[GRID_SIZE * GRID_SIZE * GRID_SIZE]; - private boolean texturedMode = false; private double orbitAngle = 0; + private long testStartTime; + private long frameCount; + private BenchmarkTest currentTest; + private boolean testFinished = false; + private final List results = new ArrayList<>(); + private final List tests = new ArrayList<>(); - private long frameCount = 0; - private long fpsStartTime = System.currentTimeMillis(); - private long totalFrameCount = 0; - private long appStartTime = System.currentTimeMillis(); + /** + * Interface for a single benchmark test. + * Implementations define how to set up and tear down the test scene. + */ + public interface BenchmarkTest { + String getName(); + void setup(ShapeCollection shapes, Texture[] textures, int[] cubeTextureIndices); + void teardown(ShapeCollection shapes); + } - /** Resets FPS statistics counters. */ - private void resetFpsStats() { - frameCount = 0; - fpsStartTime = System.currentTimeMillis(); - totalFrameCount = 0; - appStartTime = fpsStartTime; + /** + * Holds the results of a single benchmark test. + */ + public static class TestResult { + final String testName; + final long frameCount; + final double durationSeconds; + final double averageFps; + + TestResult(String testName, long frameCount, double durationSeconds) { + this.testName = testName; + this.frameCount = frameCount; + this.durationSeconds = durationSeconds; + this.averageFps = frameCount / durationSeconds; + } + } + + /** + * Benchmark test for solid-color cubes. + */ + public static class SolidCubesTest implements BenchmarkTest { + 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, Texture[] textures, int[] cubeTextureIndices) { + 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(); + } } /** - * Entry point for the graphics benchmark demo. + * Benchmark test for textured cubes. + */ + public static class TexturedCubesTest implements BenchmarkTest { + private final List cubes = new ArrayList<>(); + + @Override + public String getName() { + return "Textured Cubes"; + } + + @Override + public void setup(ShapeCollection shapes, Texture[] textures, int[] cubeTextureIndices) { + 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(); + } + } + + /** + * Entry point for the graphics benchmark. * @param args command line arguments (ignored) */ public static void main(String[] args) { @@ -91,31 +200,26 @@ public class GraphicsBenchmark extends WorldNavigationUserInputTracker implement } /** - * Constructs the graphics benchmark demo with a 1920x1080 window. - * Creates the cube grid and initializes camera orbit animation. + * Constructs and runs the graphics benchmark. */ public GraphicsBenchmark() { - viewFrame = new ViewFrame(1920, 1080); + initializeWindow(); + initializeTextures(); + initializeCubeTextureIndices(); + registerTests(); + startNextTest(); + } + + 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(); - - cubeTextureIndices = new int[GRID_SIZE * GRID_SIZE * GRID_SIZE]; - for (int i = 0; i < cubeTextureIndices.length; i++) { - cubeTextureIndices[i] = random.nextInt(TEXTURE_COUNT); - } - - createTextures(); - createCubes(false); } - /** Creates the pool of randomly colored glow textures. */ - private void createTextures() { + private void initializeTextures() { for (int i = 0; i < TEXTURE_COUNT; i++) { textures[i] = createGlowTexture( 50 + random.nextInt(200), @@ -125,13 +229,68 @@ public class GraphicsBenchmark extends WorldNavigationUserInputTracker implement } } - /** - * Creates a glow texture with the specified RGB color. - * @param r red component (0-255) - * @param g green component (0-255) - * @param b blue component (0-255) - * @return a 64x64 texture with glowing border effect - */ + private void initializeCubeTextureIndices() { + random.setSeed(RANDOM_SEED); + for (int i = 0; i < cubeTextureIndices.length; i++) { + cubeTextureIndices[i] = random.nextInt(TEXTURE_COUNT); + } + } + + private void registerTests() { + tests.add(new SolidCubesTest()); + tests.add(new TexturedCubesTest()); + } + + private void startNextTest() { + int nextIndex = results.size(); + if (nextIndex >= tests.size()) { + finishBenchmark(); + return; + } + + currentTest = tests.get(nextIndex); + testFinished = false; + orbitAngle = 0; + frameCount = 0; + testStartTime = System.currentTimeMillis(); + + currentTest.setup(shapes, textures, cubeTextureIndices); + } + + private void finishBenchmark() { + currentTest = null; + viewFrame.dispose(); + printResults(); + System.exit(0); + } + + private void printResults() { + String separator = "================================================================================"; + String thinSeparator = "--------------------------------------------------------------------------------"; + + 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.printf("%-28s %-11s %-9s %-10s%n", "Test", "Duration", "Frames", "Avg FPS"); + System.out.println(thinSeparator); + + for (TestResult result : results) { + System.out.printf("%-28s %-11s %-9d %-10.2f%n", + result.testName, + String.format("%.2fs", result.durationSeconds), + result.frameCount, + result.averageFps); + } + + System.out.println(separator); + } + private Texture createGlowTexture(int r, int g, int b) { int texSize = 64; Texture texture = new Texture(texSize, texSize, 2); @@ -158,70 +317,12 @@ public class GraphicsBenchmark extends WorldNavigationUserInputTracker implement return texture; } - /** Removes all cubes from the scene. */ - private void clearCubes() { - for (Object cube : cubes) { - shapes.getShapes().remove(cube); - } - cubes.clear(); - } - - /** - * Creates the grid of cubes in the scene. - * @param textured if true, creates textured cubes; if false, creates solid-colored cubes - */ - private void createCubes(boolean textured) { - clearCubes(); - random.setSeed(42); - - 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; - - if (textured) { - Texture tex = textures[cubeTextureIndices[idx]]; - TexturedCube cube = new TexturedCube( - new Point3D(px, py, pz), - CUBE_SIZE, - tex - ); - shapes.addShape(cube); - cubes.add(cube); - } else { - 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); - } - idx++; - } - } - } - } - - /** - * Called each frame to animate the camera orbit and track FPS. - * @param viewPanel the view panel rendering the scene - * @param millisecondsSinceLastFrame time elapsed since last frame - * @return true to continue rendering - */ @Override public boolean onFrame(ViewPanel viewPanel, int millisecondsSinceLastFrame) { + if (currentTest == null) { + return true; + } + orbitAngle += ORBIT_SPEED * millisecondsSinceLastFrame; double x = Math.sin(orbitAngle) * ORBIT_DISTANCE; @@ -232,60 +333,25 @@ public class GraphicsBenchmark extends WorldNavigationUserInputTracker implement camera.lookAt(new Point3D(0, 0, 0)); frameCount++; - totalFrameCount++; - long now = System.currentTimeMillis(); - long elapsed = now - fpsStartTime; - if (elapsed >= 1000) { - int fps = (int) (frameCount * 1000 / elapsed); - long totalElapsed = now - appStartTime; - double avgFps = totalFrameCount * 1000.0 / totalElapsed; - System.out.println("current: " + fps + " average: " + String.format("%.2f", avgFps)); - frameCount = 0; - fpsStartTime = now; - } - return true; - } + long elapsed = System.currentTimeMillis() - testStartTime; + if (elapsed >= TEST_DURATION_MS && !testFinished) { + testFinished = true; + double durationSeconds = elapsed / 1000.0; + results.add(new TestResult(currentTest.getName(), frameCount, durationSeconds)); - /** - * Handles keyboard input for switching between rendering modes. - * @param event the key event - * @param viewPanel the view panel - * @return true if the event was consumed - */ - @Override - public boolean keyPressed(KeyEvent event, ViewPanel viewPanel) { - switch (event.getKeyCode()) { - case KeyEvent.VK_1: - if (texturedMode) { - texturedMode = false; - createCubes(false); - resetFpsStats(); - } - return true; - case KeyEvent.VK_2: - if (!texturedMode) { - texturedMode = true; - createCubes(true); - resetFpsStats(); - } - return true; + currentTest.teardown(shapes); + startNextTest(); } - return super.keyPressed(event, viewPanel); + + return true; } /** * A cube composed of textured polygons. - * Each face is rendered as two triangles with UV coordinates for texture mapping. */ private static class TexturedCube extends AbstractCompositeShape { - /** - * Constructs a textured cube at the specified position. - * @param center the center position of the cube - * @param size half-size of the cube (total width is 2*size) - * @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); @@ -313,18 +379,6 @@ public class GraphicsBenchmark extends WorldNavigationUserInputTracker implement setBackfaceCulling(true); } - /** - * Adds a quad face as two textured triangles. - * @param p1 first corner position - * @param p2 second corner position - * @param p3 third corner position - * @param p4 fourth corner position - * @param t1 texture coordinate for p1 - * @param t2 texture coordinate for p2 - * @param t3 texture coordinate for p3 - * @param t4 texture coordinate for p4 - * @param texture the texture to apply - */ private void addTexturedFace(Point3D p1, Point3D p2, Point3D p3, Point3D p4, Point2D t1, Point2D t2, Point2D t3, Point2D t4, Texture texture) {