From: Svjatoslav Agejenko Date: Tue, 12 May 2026 19:17:34 +0000 (+0300) Subject: feat(gui): make render thread count configurable at runtime X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;h=5f3ebb4bf39883ef4228dc7915fd76659e445ec4;p=sixth-3d.git feat(gui): make render thread count configurable at runtime Replace the hardcoded NUM_RENDER_SEGMENTS constant with a dynamic numRenderSegments field on RenderingContext. ViewPanel now exposes getNumRenderThreads() and setNumRenderThreads() so callers (e.g. the benchmark) can vary parallelism at runtime. The render executor is recreated lazily when the thread count changes, and the rendering context is rebuilt when the segment count no longer matches. DeveloperToolsPanel displays the active thread count alongside the available core count. Also move Sixth 3D Demos TODO items into TODO.org. --- diff --git a/TODO.org b/TODO.org index dbdd8cc..9df8441 100644 --- a/TODO.org +++ b/TODO.org @@ -135,7 +135,7 @@ Add formula textbox display on top of 3D graph. - Consider integrating with FriCAS or similar CAS software so that formula parsing and computation happens there. -* Study and apply where applicable ++ Study and apply where applicable :PROPERTIES: :CUSTOM_ID: study-and-apply :END: @@ -143,3 +143,22 @@ Add formula textbox display on top of 3D graph. http://blog.rogach.org/2015/08/how-to-create-your-own-simple-3d-render.html + Improve triangulation. Read: https://ianthehenry.com/posts/delaunay/ + +* Sixth 3D Demos + +** Text editors demo + ++ Improve focus handling: + + Perhaps add shortcut to navigate world without exiting entire + stack of focus. + + Possibility to retain and reuse recently focused elements. + + Store user location in the world and view direction with the + focused window. So that when returning focus to far away object, + user is redirected also to proper location in the world. ++ Possibility to store recently visited locations in the world and + return to them. + +** Math graphs demo + ++ Instead of projecting 2D visualizations onto 3D space, visualize + some formula using all 3 dimensions available. diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/DeveloperToolsPanel.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/DeveloperToolsPanel.java index 68949b5..6902e73 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/DeveloperToolsPanel.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/DeveloperToolsPanel.java @@ -80,6 +80,10 @@ public class DeveloperToolsPanel extends JFrame { * The label showing tessellated polygon count. */ private final JLabel tessellationPolygonCountLabel; + /** + * The label showing current render thread count. + */ + private final JLabel renderThreadsLabel; /** * Timer for periodic updates. */ @@ -125,12 +129,17 @@ public class DeveloperToolsPanel extends JFrame { tessellationPolygonCountLabel = new JLabel("0"); tessellationPolygonCountLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + // Initialize render threads label + renderThreadsLabel = new JLabel(String.valueOf(viewPanel.getNumRenderThreads())); + renderThreadsLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + final JPanel topPanel = new JPanel(); topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS)); topPanel.add(createSettingsPanel()); topPanel.add(createCameraPanel()); topPanel.add(createCullingPanel()); topPanel.add(createTessellationPanel()); + topPanel.add(createRenderThreadsPanel()); add(topPanel, BorderLayout.NORTH); logArea = new JTextArea(15, 60); @@ -274,6 +283,25 @@ public class DeveloperToolsPanel extends JFrame { return panel; } + private JPanel createRenderThreadsPanel() { + final JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(0, 8, 8, 8), + BorderFactory.createTitledBorder("Render Threads") + )); + + final JPanel statsRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 2)); + statsRow.add(new JLabel("Active:")); + statsRow.add(renderThreadsLabel); + statsRow.add(new JLabel(" Available cores:")); + statsRow.add(new JLabel(String.valueOf(Runtime.getRuntime().availableProcessors()))); + + panel.add(statsRow); + + return panel; + } + private JPanel createButtonPanel() { final JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); @@ -300,6 +328,7 @@ public class DeveloperToolsPanel extends JFrame { updateCameraLabel(); updateCullingStatistics(); updateTessellationStatistics(); + updateRenderThreadsLabel(); updateLogDisplay(); } finally { updating = false; @@ -345,6 +374,13 @@ public class DeveloperToolsPanel extends JFrame { tessellationPolygonCountLabel.setText(String.valueOf(controller.getLastPolygonCount())); } + private void updateRenderThreadsLabel() { + if (viewPanel == null) { + return; + } + renderThreadsLabel.setText(String.valueOf(viewPanel.getNumRenderThreads())); + } + private void updateLogDisplay() { final List entries = debugLogBuffer.getEntries(); final StringBuilder sb = new StringBuilder(); diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java index 4ca886e..c9e4aff 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java @@ -48,8 +48,9 @@ public class RenderingContext { /** * Number of horizontal segments for parallel rendering. * Each segment is rendered by a separate thread. + * Configurable at runtime via {@link ViewPanel#setNumRenderThreads(int)}. */ - public static final int NUM_RENDER_SEGMENTS = 8; + public final int numRenderSegments; /** * Java2D graphics context for drawing text, anti-aliased shapes, and other @@ -165,13 +166,14 @@ public class RenderingContext { /** * Creates a new rendering context for full-screen rendering. * - *

Equivalent to {@code RenderingContext(width, height, 0, height)}.

+ *

Equivalent to {@code RenderingContext(width, height, 0, height, numRenderSegments)}.

* - * @param width the rendering area width in pixels - * @param height the rendering area height in pixels + * @param width the rendering area width in pixels + * @param height the rendering area height in pixels + * @param numRenderSegments number of parallel render segments (threads) */ - public RenderingContext(final int width, final int height) { - this(width, height, 0, height); + public RenderingContext(final int width, final int height, final int numRenderSegments) { + this(width, height, 0, height, numRenderSegments); } /** @@ -180,17 +182,20 @@ public class RenderingContext { *

Initializes the offscreen image buffer, extracts the raw pixel byte array, * and configures anti-aliasing on the Graphics2D context.

* - * @param width the rendering area width in pixels - * @param height the rendering area height in pixels - * @param renderMinY minimum Y coordinate (inclusive) to render - * @param renderMaxY maximum Y coordinate (exclusive) to render + * @param width the rendering area width in pixels + * @param height the rendering area height in pixels + * @param renderMinY minimum Y coordinate (inclusive) to render + * @param renderMaxY maximum Y coordinate (exclusive) to render + * @param numRenderSegments number of parallel render segments (threads) */ public RenderingContext(final int width, final int height, - final int renderMinY, final int renderMaxY) { + final int renderMinY, final int renderMaxY, + final int numRenderSegments) { this.width = width; this.height = height; this.renderMinY = renderMinY; this.renderMaxY = renderMaxY; + this.numRenderSegments = numRenderSegments; this.centerCoordinate = new Point2D(width / 2d, height / 2d); this.projectionScale = width / 3d; @@ -221,6 +226,7 @@ public class RenderingContext { this.height = parent.height; this.renderMinY = renderMinY; this.renderMaxY = renderMaxY; + this.numRenderSegments = parent.numRenderSegments; this.centerCoordinate = parent.centerCoordinate; this.projectionScale = parent.projectionScale; this.bufferedImage = parent.bufferedImage; @@ -248,12 +254,12 @@ public class RenderingContext { * @return array of Graphics2D objects, one per segment */ private Graphics2D[] createSegmentGraphics() { - final Graphics2D[] contexts = new Graphics2D[NUM_RENDER_SEGMENTS]; - final int segmentHeight = height / NUM_RENDER_SEGMENTS; + final Graphics2D[] contexts = new Graphics2D[numRenderSegments]; + final int segmentHeight = height / numRenderSegments; - for (int i = 0; i < NUM_RENDER_SEGMENTS; i++) { + for (int i = 0; i < numRenderSegments; i++) { final int minY = i * segmentHeight; - final int maxY = (i == NUM_RENDER_SEGMENTS - 1) ? height : (i + 1) * segmentHeight; + final int maxY = (i == numRenderSegments - 1) ? height : (i + 1) * segmentHeight; final Graphics2D g = bufferedImage.createGraphics(); g.setClip(0, minY, width, maxY - minY); @@ -269,7 +275,7 @@ public class RenderingContext { * Returns the Graphics2D context for a specific render segment. * Each segment's Graphics2D is pre-clipped to its Y bounds. * - * @param segmentIndex the segment index (0 to NUM_RENDER_SEGMENTS-1) + * @param segmentIndex the segment index (0 to numRenderSegments-1) * @return the Graphics2D for that segment * @throws NullPointerException if called on a segment view (not the main context) */ diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/SegmentRenderingContext.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/SegmentRenderingContext.java index f01b2a8..5ab063e 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/SegmentRenderingContext.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/SegmentRenderingContext.java @@ -34,7 +34,7 @@ public class SegmentRenderingContext extends RenderingContext { * @param parent the parent rendering context to delegate to * @param renderMinY minimum Y coordinate (inclusive) for this segment * @param renderMaxY maximum Y coordinate (exclusive) for this segment - * @param segmentIndex the index of this segment (0 to NUM_RENDER_SEGMENTS-1) + * @param segmentIndex the index of this segment (0 to numRenderSegments-1) */ public SegmentRenderingContext(final RenderingContext parent, final int renderMinY, final int renderMaxY, diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java index fe77551..90319af 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java @@ -90,7 +90,9 @@ public class ViewPanel extends Canvas { /** The set of frame listeners notified before each frame. */ private final Set frameListeners = ConcurrentHashMap.newKeySet(); /** The executor service for parallel rendering. */ - private final ExecutorService renderExecutor = Executors.newFixedThreadPool(RenderingContext.NUM_RENDER_SEGMENTS); + private ExecutorService renderExecutor = Executors.newFixedThreadPool(Math.min(Runtime.getRuntime().availableProcessors(), 4)); + /** Number of render threads. Can be changed at runtime via {@link #setNumRenderThreads(int)}. */ + private volatile int numRenderThreads = Math.min(Runtime.getRuntime().availableProcessors(), 4); /** The background color of the view. */ public Color backgroundColor = Color.BLACK; @@ -371,6 +373,7 @@ public class ViewPanel extends Canvas { private void renderFrame() { ensureBufferStrategy(); + ensureExecutorMatchesThreadCount(); if (bufferStrategy == null || renderingContext == null) { debugLogBuffer.log("[VIEWPANEL] renderFrame ABORT: bufferStrategy=" + bufferStrategy + ", renderingContext=" + renderingContext); @@ -387,15 +390,16 @@ public class ViewPanel extends Canvas { rootShapeCollection.sortShapes(); // Phase 4: Clear and paint segments in parallel + final int segments = renderingContext.numRenderSegments; final int height = renderingContext.height; - final int segmentHeight = height / RenderingContext.NUM_RENDER_SEGMENTS; - final SegmentRenderingContext[] segmentContexts = new SegmentRenderingContext[RenderingContext.NUM_RENDER_SEGMENTS]; - final CountDownLatch latch = new CountDownLatch(RenderingContext.NUM_RENDER_SEGMENTS); + final int segmentHeight = height / segments; + final SegmentRenderingContext[] segmentContexts = new SegmentRenderingContext[segments]; + final CountDownLatch latch = new CountDownLatch(segments); - for (int i = 0; i < RenderingContext.NUM_RENDER_SEGMENTS; i++) { + for (int i = 0; i < segments; i++) { final int segmentIndex = i; final int minY = i * segmentHeight; - final int maxY = (i == RenderingContext.NUM_RENDER_SEGMENTS - 1) ? height : (i + 1) * segmentHeight; + final int maxY = (i == segments - 1) ? height : (i + 1) * segmentHeight; segmentContexts[i] = new SegmentRenderingContext(renderingContext, minY, maxY, segmentIndex); @@ -432,7 +436,7 @@ public class ViewPanel extends Canvas { final int[] pixels = renderingContext.pixels; final int width = renderingContext.width; final int red = (255 << 16); // Red in RGB format: R=255, G=0, B=0 - for (int i = 1; i < RenderingContext.NUM_RENDER_SEGMENTS; i++) { + for (int i = 1; i < segments; i++) { final int offset = i * segmentHeight * width; Arrays.fill(pixels, offset, offset + width, red); } @@ -519,6 +523,46 @@ public class ViewPanel extends Canvas { targetFPS = frameRate; } + /** + * Returns the current number of render threads. + * + * @return the number of render threads + */ + public int getNumRenderThreads() { + return numRenderThreads; + } + + /** + * Sets the number of render threads. Takes effect on the next frame. + * The executor service is recreated lazily when the render loop detects the change. + * + * @param count number of render threads (must be at least 1) + */ + public void setNumRenderThreads(final int count) { + if (count < 1) + throw new IllegalArgumentException("Render thread count must be at least 1, got: " + count); + numRenderThreads = count; + viewRepaintNeeded = true; + } + + /** + * Recreates the executor service if the thread count has changed since last creation. + * Called from the render thread only (no synchronization needed). + */ + private void ensureExecutorMatchesThreadCount() { + if (renderExecutor == null || renderExecutor.isShutdown()) { + renderExecutor = Executors.newFixedThreadPool(numRenderThreads); + return; + } + if (renderExecutor instanceof java.util.concurrent.ThreadPoolExecutor) { + final java.util.concurrent.ThreadPoolExecutor tpe = (java.util.concurrent.ThreadPoolExecutor) renderExecutor; + if (tpe.getCorePoolSize() != numRenderThreads) { + tpe.shutdown(); + renderExecutor = Executors.newFixedThreadPool(numRenderThreads); + } + } + } + /** * Stops rendering of this view. */ @@ -633,14 +677,15 @@ public class ViewPanel extends Canvas { return; } - // create new rendering context if window size has changed + // create new rendering context if window size has changed OR thread count has changed if ((renderingContext == null) || (renderingContext.width != panelWidth) - || (renderingContext.height != panelHeight)) { + || (renderingContext.height != panelHeight) + || (renderingContext.numRenderSegments != numRenderThreads)) { if (renderingContext != null) { renderingContext.dispose(); } - renderingContext = new RenderingContext(panelWidth, panelHeight); + renderingContext = new RenderingContext(panelWidth, panelHeight, numRenderThreads); renderingContext.developerTools = developerTools; renderingContext.debugLogBuffer = debugLogBuffer; renderingContext.lightingManager = lightingManager;