import eu.svjatoslav.aukio.e3d.geometry.Box;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
+import eu.svjatoslav.aukio.e3d.gui.humaninput.KeyboardFocusStack;
import eu.svjatoslav.aukio.e3d.gui.humaninput.KeyboardHelper;
import eu.svjatoslav.aukio.e3d.gui.humaninput.KeyboardInputHandler;
import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseEvent;
@Override
public boolean mouseClicked(int button) {
+ // Direct-call path (no dispatch, no focus stack supplied). Only
+ // works for components constructed with a live panel; headless-built
+ // components (null panel) ignore the click instead of throwing.
+ if (viewPanel == null)
+ return false;
+ return mouseClicked(button, Double.NaN, Double.NaN,
+ viewPanel.getKeyboardFocusStack());
+ }
+
+ @Override
+ public boolean mouseClicked(final int button, final double textureU,
+ final double textureV,
+ final KeyboardFocusStack focusStack) {
if (button == MouseEvent.BUTTON_MIDDLE) {
// middle click releases keyboard focus, like ESC
- viewPanel.getKeyboardFocusStack().popFocusOwner();
+ focusStack.popFocusOwner();
return true;
}
- return viewPanel.getKeyboardFocusStack().pushFocusOwner(this);
+ return focusStack.pushFocusOwner(this);
}
@Override
// but that must not kill the render loop with an NPE here.
if (mouseInThisEye && pending.segmentContexts != null) {
combineMouseResults(pending.segmentContexts, pending.context);
- viewRepaintNeeded = pending.context.handlePossibleComponentMouseEvent();
+ viewRepaintNeeded = pending.context.handlePossibleComponentMouseEvent(
+ getKeyboardFocusStack());
}
if (developerTools.showSegmentBoundaries) {
deviceHotplug.stop();
deviceHotplug = null;
}
+ if (globalIllumination != null) {
+ // Stop the GI sweep with the view: the worker threads trace
+ // rays against THIS view's scene, so after close they are pure
+ // CPU burn (observed 2026-09-20: 4 leaked threads at ~70% duty
+ // minutes after the demo window was gone).
+ globalIllumination.stop();
+ globalIllumination = null;
+ }
renderThreadRunning = false;
presentThreadRunning = false;
presentSignal.release();
if (dropped != null) {
dropped.gate.countDown();
}
- pendingPaints.clear();
- if (transformExecutor != null) {
- transformExecutor.shutdownNow();
- }
if (renderThread != null) {
+ // Interrupt BEFORE any executor teardown: the render thread may
+ // be parked in flushPendingPaint's paintLatch.await(), and
+ // shutdownNow() cancels queued paint tasks — a cancelled task
+ // never runs its finally countDown(), so the latch never fires
+ // and an uninterrupted join here hangs forever (observed
+ // 2026-09-20: close button dead, EDT stuck in this join).
+ renderThread.interrupt();
try {
renderThread.join();
} catch (InterruptedException e) {
}
renderThread = null;
}
+ // Tear down the pipeline only with the render thread dead: clearing
+ // the deque and cancelling queued tasks while it was still flushing
+ // raced its peek/poll and orphaned the latches it awaited.
+ pendingPaints.clear();
+ if (transformExecutor != null) {
+ transformExecutor.shutdownNow();
+ }
}
/**
public void stop() {
running = false;
+ // Close first: the reader thread is parked in a native read that no
+ // interrupt can wake; closing the fd fails the read instantly.
+ device.close();
try {
readerThread.join(500);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
- device.close();
}
private void readLoop() {
public void stop() {
running = false;
+ // Wake the 2 s poll sleep; without the interrupt the join below
+ // burns its full 1 s timeout on the EDT at every window close.
+ thread.interrupt();
try {
thread.join(1000);
} catch (final InterruptedException e) {
return mouseClicked(button);
}
+ /**
+ * Called when mouse is clicked on component, additionally carrying the
+ * keyboard focus stack of the dispatching view.
+ *
+ * <p>Components that take keyboard focus on click should override THIS
+ * method rather than reaching for a stored ViewPanel reference: scenes
+ * built headlessly (golden-image harness) construct components with a
+ * null panel, but the dispatching view always has a focus stack.</p>
+ *
+ * <p>The default implementation ignores the focus stack and delegates
+ * to {@link #mouseClicked(int, double, double)}.</p>
+ *
+ * @param button the mouse button that was clicked (1 = left, 2 = middle, 3 = right)
+ * @param textureU texture-space X of the clicked point, or {@link Double#NaN}
+ * @param textureV texture-space Y of the clicked point, or {@link Double#NaN}
+ * @param focusStack keyboard focus stack of the dispatching view
+ * @return {@code true} if view update is needed as a consequence of this mouse click
+ */
+ default boolean mouseClicked(final int button, final double textureU,
+ final double textureV,
+ final KeyboardFocusStack focusStack) {
+ return mouseClicked(button, textureU, textureV);
+ }
+
/**
* Called when the mouse wheel is turned while this component has
* keyboard focus. Both axes are reported: horizontal wheel input
public void stop() {
running = false;
+ // Wake the 2 s poll sleep; without the interrupt the join below
+ // burns its full 1 s timeout on the EDT at every window close.
+ thread.interrupt();
try {
thread.join(1000);
} catch (final InterruptedException e) {
int close(int fd);
int read(int fd, Memory buffer, int count);
+
+ /** poll(2) on a single-element struct pollfd (8 bytes: fd, events, revents). */
+ int poll(Memory fds, int nfds, int timeout);
}
private final File deviceNode;
}
private void readLoop() {
+ // Poll before reading: the device is SILENT at rest, so a plain
+ // blocking read never returns when idle — and close() from stop()
+ // does not wake a blocked read on Linux, which made every window
+ // close wait out the full 1 s join timeout.
+ final Memory pollfd = new Memory(8);
while (running) {
+ pollfd.clear();
+ pollfd.setInt(0, fd);
+ pollfd.setShort(4, (short) 0x1); // events = POLLIN
+ final int ready = CLib.INSTANCE.poll(pollfd, 1, 250);
+ if (ready == 0)
+ continue; // idle device: re-check running
+ if (ready < 0) {
+ if (Native.getLastError() == 4) // EINTR
+ continue;
+ broken = true;
+ return;
+ }
+ final short revents = pollfd.getShort(6);
+ if ((revents & 0x1) == 0) { // no POLLIN: ERR/HUP/NVAL = unplugged
+ broken = true;
+ return;
+ }
final int n = CLib.INSTANCE.read(fd, readBuffer, 64);
if (n < 0) {
broken = true; // unplugged
public void stop() {
running = false;
- if (fd >= 0) {
- CLib.INSTANCE.close(fd);
- fd = -1;
- }
+ // Join first: the poll loop notices the flag within 250 ms. Closing
+ // the fd before the reader is dead risks the fd number being reused
+ // under a concurrent open while the reader is mid-read.
if (reader != null) {
try {
reader.join(1000);
}
reader = null;
}
+ if (fd >= 0) {
+ CLib.INSTANCE.close(fd);
+ fd = -1;
+ }
}
}
import eu.svjatoslav.aukio.e3d.gui.DeveloperTools;
import eu.svjatoslav.aukio.e3d.geometry.Point2D;
+import eu.svjatoslav.aukio.e3d.gui.humaninput.KeyboardFocusStack;
import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseEvent;
import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController;
import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightingManager;
/**
* Handles mouse events for components and returns whether a view repaint is needed.
*
+ * @param focusStack keyboard focus stack of the dispatching view, handed
+ * to clicked components so they can acquire focus
+ * without holding a ViewPanel reference
* @return {@code true} if view update is needed as a consequence of this mouse event
*/
- public boolean handlePossibleComponentMouseEvent() {
+ public boolean handlePossibleComponentMouseEvent(final KeyboardFocusStack focusStack) {
if (mouseEvent == null) return false;
boolean viewRepaintNeeded = false;
if (mouseEvent.button != 0 && currentObjectUnderMouseCursor != null) {
// Mouse button was clicked on some component.
viewRepaintNeeded |= currentObjectUnderMouseCursor.mouseClicked(
- mouseEvent.button, currentMouseTextureU, currentMouseTextureV);
+ mouseEvent.button, currentMouseTextureU, currentMouseTextureV,
+ focusStack);
} else if (currentObjectUnderMouseCursor != null)
// hover: let the component track the pointer position
viewRepaintNeeded |= currentObjectUnderMouseCursor.mouseHover(
/** Debug statistics with {@code -De3d.gi.debug}. */
private static final boolean DEBUG = Boolean.getBoolean("e3d.gi.debug");
+ /** Albedo for snapshot entries that carry no flat color (textured triangles). */
+ private static final Color FALLBACK_ALBEDO = new Color(128, 128, 128);
+
/**
* EMA policy for the inner per-sample indirect blend: "fixed" (default,
* 0.15) keeps every ray hit equally intensive forever — fading toward
private static Color colorOf(final TriangleBvh.Entry entry) {
if (entry.lightmap != null)
return entry.lightmap.baseColor;
- return ((SolidPolygon) entry.polygon).getColor();
+ if (entry.polygon instanceof SolidPolygon)
+ return ((SolidPolygon) entry.polygon).getColor();
+ // Snapshot triangles are not all SolidPolygons (e.g. textured
+ // triangles from a TextCanvas in a GI scene carry no flat color) —
+ // a neutral gray albedo keeps bounce light plausible instead of
+ // throwing ClassCastException into the worker loop.
+ return FALLBACK_ALBEDO;
}
// ------------------------------------------------------------------