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;
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.
*
- * <p>Key controls:
- * <ul>
- * <li>Press 1 - Switch to solid color cubes (faster rendering)</li>
- * <li>Press 2 - Switch to textured cubes (slower rendering, tests texture fill-rate)</li>
- * </ul>
+ * <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.</p>
*
* @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<Object> 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<TestResult> results = new ArrayList<>();
+ private final List<BenchmarkTest> 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<Object> 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<Object> 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) {
}
/**
- * 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),
}
}
- /**
- * 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);
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;
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);
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) {