Show 3D frames only once panels over them are painted

- A frame drawn into a 3D view's window waits until Swing has painted what lies behind the floating panels, so the ground behind the height slider no longer trails the view while it moves

Model used: Claude Opus 5.5
This commit is contained in:
piexel committed 2026-10-25 18:56:25 +01:00
1 parent 54213d4b62
commit 2acfac36fc
3 files changed
+58 -25

No files matched your search

@@ -77,7 +77,7 @@ final class GpuFrames {
if (!windows.get().drawInto(window)) { if (!windows.get().drawInto(window)) {
throw new IllegalStateException("GLX can't draw into the view's window"); throw new IllegalStateException("GLX can't draw into the view's window");
} }
return drawing.drawInto(new GpuWindowFrame(windows.get(), window, screen.seeThrough())); return drawing.drawInto(new GpuWindowFrame(screen.seeThrough()));
} catch (RuntimeException failure) { } catch (RuntimeException failure) {
gpu.stopDrawingIntoWindows(failure); gpu.stopDrawingIntoWindows(failure);
return Optional.empty(); return Optional.empty();
@@ -85,6 +85,31 @@ final class GpuFrames {
}); });
} }
/**
* Puts the last frame drawn into the view's window on screen. Another view may have drawn into its own window
* since, so the view's window is made current again first.
*
* @return false when the window didn't take it, and the caller should draw a picture instead
*/
static boolean showOnScreen(ScreenTarget screen) {
return onCard(gpu -> {
Optional<GlxDisplay> windows = gpu.windows();
long window = screen.canvas().window();
if (windows.isEmpty() || window == 0L) {
return Optional.<Boolean>empty();
}
try {
if (!windows.get().drawInto(window) || !windows.get().show(window)) {
throw new IllegalStateException("The window didn't take the frame");
}
return Optional.of(true);
} catch (RuntimeException failure) {
gpu.stopDrawingIntoWindows(failure);
return Optional.<Boolean>empty();
}
}).isPresent();
}
static void failOnError() { static void failOnError() {
int error = glGetError(); int error = glGetError();
if (error != GL_NO_ERROR) { if (error != GL_NO_ERROR) {
@@ -119,7 +144,7 @@ final class GpuFrames {
return offScreen; return offScreen;
} }
private static Optional<OrbitPicture> onCard(CardWork work) { private static <T> Optional<T> onCard(CardWork<T> work) {
if (failed) { if (failed) {
return Optional.empty(); return Optional.empty();
} }
@@ -145,7 +170,7 @@ final class GpuFrames {
} }
@FunctionalInterface @FunctionalInterface
private interface CardWork { private interface CardWork<T> {
Optional<OrbitPicture> drawOn(GpuContext gpu); Optional<T> drawOn(GpuContext gpu);
} }
} }
@@ -28,13 +28,12 @@ import static org.lwjgl.opengl.GL30.*;
/** /**
* A frame drawn straight into a view's window. Of what was drawn, only the patches Swing paints where panels float * A frame drawn straight into a view's window. Of what was drawn, only the patches Swing paints where panels float
* over the window are read back. Only for the GPU thread. * over the window are read back. The frame waits in the window's back buffer until {@link GpuFrames#showOnScreen},
* so it can go on screen together with its patches. Only for the GPU thread.
*/ */
final class GpuWindowFrame implements GpuFrameTarget { final class GpuWindowFrame implements GpuFrameTarget {
private static final int WINDOW_FRAMEBUFFER = 0; private static final int WINDOW_FRAMEBUFFER = 0;
private final GlxDisplay windows;
private final long window;
private final List<Rectangle> seeThrough; private final List<Rectangle> seeThrough;
private int width; private int width;
private int height; private int height;
@@ -42,9 +41,7 @@ final class GpuWindowFrame implements GpuFrameTarget {
/** /**
* @param seeThrough the parts of the view panels float over, which Swing paints instead of the window * @param seeThrough the parts of the view panels float over, which Swing paints instead of the window
*/ */
GpuWindowFrame(GlxDisplay windows, long window, List<Rectangle> seeThrough) { GpuWindowFrame(List<Rectangle> seeThrough) {
this.windows = windows;
this.window = window;
this.seeThrough = seeThrough; this.seeThrough = seeThrough;
} }
@@ -62,12 +59,7 @@ final class GpuWindowFrame implements GpuFrameTarget {
@Override @Override
public OrbitPicture finish(OrbitProjection projection, double depthRange) { public OrbitPicture finish(OrbitProjection projection, double depthRange) {
OrbitPicture picture = OrbitPicture.onScreen(width, height, readPatches(), return OrbitPicture.onScreen(width, height, readPatches(), GpuFrameTarget.depthInMiddle(projection, depthRange, width, height));
GpuFrameTarget.depthInMiddle(projection, depthRange, width, height));
if (!windows.show(window)) {
throw new IllegalStateException("The window didn't take the frame");
}
return picture;
} }
private List<PicturePatch> readPatches() { private List<PicturePatch> readPatches() {
@@ -31,6 +31,7 @@ import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration; import java.awt.GraphicsConfiguration;
import java.awt.Point; import java.awt.Point;
import java.awt.RenderingHints; import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter; import java.awt.event.ComponentAdapter;
@@ -338,7 +339,7 @@ final class OrbitViewport extends JComponent {
renderThread.execute(() -> { renderThread.execute(() -> {
OrbitPicture drawn = draw(frameScene, screen, frameCamera, width, height, covered); OrbitPicture drawn = draw(frameScene, screen, frameCamera, width, height, covered);
boolean missedScreen = screen != null && !drawn.onScreen(); boolean missedScreen = screen != null && !drawn.onScreen();
EventQueue.invokeLater(() -> finishFrame(frameScene, frameCamera, drawn, missedScreen, requestedAtNanos)); EventQueue.invokeLater(() -> finishFrame(frameScene, frameCamera, screen, drawn, missedScreen, requestedAtNanos));
}); });
} }
@@ -380,25 +381,40 @@ final class OrbitViewport extends JComponent {
/** /**
* Until the card is opened, nothing knows whether frames can go into the window; the first picture opens it. A * Until the card is opened, nothing knows whether frames can go into the window; the first picture opens it. A
* frame that should have gone into the window but couldn't isn't tried again right away. The next frame is asked * frame that should have gone into the window but couldn't isn't tried again right away. A frame for a scene
* for before this one's patches are painted, so the card draws while Swing paints. * replaced meanwhile is never shown.
*/ */
private void finishFrame(OrbitScene frameScene, OrbitCamera frameCamera, OrbitPicture drawn, boolean missedScreen, private void finishFrame(OrbitScene frameScene, OrbitCamera frameCamera, ScreenTarget screen, OrbitPicture drawn,
long requestedAtNanos) { boolean missedScreen, long requestedAtNanos) {
rendering = false; rendering = false;
measureFrameTime(System.nanoTime() - requestedAtNanos); measureFrameTime(System.nanoTime() - requestedAtNanos);
boolean shown = frameScene == scene; if (frameScene == scene) {
if (shown) {
takePicture(drawn, frameCamera); takePicture(drawn, frameCamera);
paintPicture(drawn);
showInWindow(screen, drawn);
} }
boolean canGoOnScreen = !missedScreen && canvas != null && !canvas.isVisible() && drawsOnScreen(); boolean canGoOnScreen = !missedScreen && canvas != null && !canvas.isVisible() && drawsOnScreen();
if (renderAgain || drawn.catchingUp() || canGoOnScreen) { if (renderAgain || drawn.catchingUp() || canGoOnScreen) {
renderAgain = false; renderAgain = false;
requestFrame(); requestFrame();
} }
if (shown) {
paintPicture(drawn);
} }
/**
* A frame in the window only goes on screen once Swing has painted its patches, or the panels floating over the
* window would show the frame before. Swing and the card reach the screen through separate connections, so
* Swing's is flushed first. The render thread shows the frame before it draws the next one into the window.
*/
private void showInWindow(ScreenTarget screen, OrbitPicture drawn) {
if (!drawn.onScreen()) {
return;
}
Toolkit.getDefaultToolkit().sync();
renderThread.execute(() -> {
if (!GpuFrames.showOnScreen(screen)) {
EventQueue.invokeLater(this::requestFrame);
}
});
} }
// A whole picture replaces the window until frames go into it again. // A whole picture replaces the window until frames go into it again.