feat(benchmark): show results in dialog with copy-to-clipboard master
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sun, 15 Mar 2026 22:51:56 +0000 (00:51 +0200)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sun, 15 Mar 2026 22:51:56 +0000 (00:51 +0200)
- Add intro dialog with instructions before benchmark starts

- Display results in scrollable dialog instead of console

- Add Copy to Clipboard button for easy sharing

- Show system info and FPS results in formatted text

src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/GraphicsBenchmark.java

index 96a50dc..1f9728a 100644 (file)
@@ -13,7 +13,9 @@ 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 javax.swing.*;
+import java.awt.*;
+import java.awt.datatransfer.StringSelection;
 import java.awt.event.KeyEvent;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
@@ -22,8 +24,8 @@ 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.
+ * Runs multiple tests sequentially, each for a fixed duration, and displays
+ * results in a dialog with copy-to-clipboard functionality.
  * 
  * <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>
@@ -68,15 +70,43 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
      * @param args command line arguments (ignored)
      */
     public static void main(String[] args) {
-        new GraphicsBenchmark();
+        SwingUtilities.invokeLater(() -> {
+            if (showIntroDialog()) {
+                new GraphicsBenchmark();
+            }
+        });
     }
 
-/**
+    private static boolean showIntroDialog() {
+        String message =
+                "<html><div style='width:400px; font-family: sans-serif;'>" +
+                "<h2>Graphics Benchmark</h2>" +
+                "<p>This will run a series of performance tests.</p>" +
+                "<ul>" +
+                "<li>Each test runs for 30 seconds</li>" +
+                "<li>Press <b>SPACE</b> to skip any test<br>" +
+                "<span style='color: gray;'>(skipping reduces measurement precision)</span></li>" +
+                "<li>A summary will be shown at the end</li>" +
+                "</ul>" +
+                "</div></html>";
+
+        int choice = JOptionPane.showConfirmDialog(
+                null,
+                message,
+                "Graphics Benchmark",
+                JOptionPane.OK_CANCEL_OPTION,
+                JOptionPane.INFORMATION_MESSAGE
+        );
+
+        return choice == JOptionPane.OK_OPTION;
+    }
+
+    /**
      * 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
+        registerTests();
+        initializeWindow();
     }
 
     private void initializeWindow() {
@@ -87,7 +117,6 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
         viewPanel.addFrameListener(this);
         viewPanel.getKeyboardFocusStack().pushFocusOwner(this);
         camera = viewPanel.getCamera();
-        // Now explicitly start the render thread after all listeners are registered
         viewPanel.ensureRenderThreadStarted();
     }
 
@@ -124,7 +153,6 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
         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;
     }
 
@@ -146,39 +174,89 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
         viewPanel.removeFrameListener(this);
         viewPanel.stop();
         viewFrame.dispose();
-        printResults();
+
+        showResultsDialog();
     }
 
-    private void printResults() {
+    private String formatResults() {
+        StringBuilder sb = new StringBuilder();
         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);
+        sb.append(separator).append("\n");
+        sb.append("                         GRAPHICS BENCHMARK RESULTS\n");
+        sb.append(separator).append("\n");
+        sb.append("Date:        ").append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).append("\n");
+        sb.append("Resolution:  ").append(WINDOW_WIDTH).append("x").append(WINDOW_HEIGHT).append("\n");
+        sb.append("Cubes:       ").append(GRID_SIZE * GRID_SIZE * GRID_SIZE)
+                .append(" (").append(GRID_SIZE).append("x").append(GRID_SIZE).append("x").append(GRID_SIZE).append(" grid)\n");
+        sb.append("Duration:    ").append(TEST_DURATION_MS / 1000).append(" seconds per test\n");
+        sb.append("\n");
+        sb.append(thinSeparator).append("\n");
+        sb.append("SYSTEM INFORMATION\n");
+        sb.append(thinSeparator).append("\n");
+        sb.append("CPU Name:    ").append(getCpuName()).append("\n");
+        sb.append("Arch:        ").append(System.getProperty("os.arch")).append("\n");
+        sb.append("Cores:       ").append(runtime.availableProcessors()).append("\n");
+        sb.append("\n");
+        sb.append(thinSeparator).append("\n");
+        sb.append(String.format("%-28s %s%n", "Test", "Avg FPS"));
+        sb.append(thinSeparator).append("\n");
 
         for (TestResult result : results) {
-            System.out.printf("%-28s %.2f%n", result.testName, result.averageFps);
+            sb.append(String.format("%-28s %.2f%n", result.testName, result.averageFps));
         }
 
-        System.out.println(separator);
+        sb.append(separator).append("\n");
+
+        return sb.toString();
+    }
+
+    private void showResultsDialog() {
+        String resultsText = formatResults();
+
+        JTextArea textArea = new JTextArea(resultsText);
+        textArea.setEditable(false);
+        textArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
+        textArea.setCaretPosition(0);
+
+        JScrollPane scrollPane = new JScrollPane(textArea);
+        scrollPane.setPreferredSize(new Dimension(600, 400));
+
+        JButton copyButton = new JButton("Copy to Clipboard");
+        JButton closeButton = new JButton("Close");
+
+        copyButton.addActionListener(e -> {
+            StringSelection selection = new StringSelection(resultsText);
+            Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, null);
+            copyButton.setText("Copied!");
+            copyButton.setEnabled(false);
+        });
+
+        closeButton.addActionListener(e -> {
+            Window window = SwingUtilities.getWindowAncestor(closeButton);
+            if (window != null) {
+                window.dispose();
+            }
+        });
+
+        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
+        buttonPanel.add(copyButton);
+        buttonPanel.add(closeButton);
+
+        JPanel panel = new JPanel(new BorderLayout(0, 10));
+        panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+        panel.add(scrollPane, BorderLayout.CENTER);
+        panel.add(buttonPanel, BorderLayout.SOUTH);
+
+        JOptionPane.showMessageDialog(
+                null,
+                panel,
+                "Benchmark Results",
+                JOptionPane.PLAIN_MESSAGE
+        );
     }
 
     private String getCpuName() {
@@ -199,13 +277,11 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
 
     @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();
         }