1

I'm using Java 8 with JavaFX from Oracle, and I want to create a desktop remote app where the client continuously sends images as byte[] over sockets to the server. The server receives these images and displays them as an image in an ImageView. However, after a few seconds, the program starts crashing, and I get this exception: java.lang.NullPointerException at com.sun.prism.d3d.D3DTexture.getContext(D3DTexture.java:84) at com.sun.prism.d3d.D3DTexture.update(D3DTexture.java:207) at com.sun.prism.d3d.D3DTexture.update(D3DTexture.java:151)...

After a lot of googling, I found out that by enabling "-Dprism.verbose=true," I can get more information about the problem. I discovered that the console keeps outputting: Growing pool D3D Vram Pool target to 524,167,168 Growing pool D3D Vram Pool target to 524,167,168 Growing pool D3D Vram Pool target to 535,226,368...

always with a larger number. This output becomes even faster if, for example, I resize the window in which the ImageView is located, even if the ImageView itself doesn't change in size, which doesn't make sense to me.

After some testing, I added a "System.gc();" call after each "setImage();" method call of the ImageView, and suddenly it works without any issues. There is no more "growing pool" log, and the program no longer crashes with the aforementioned NullPointerException. However, continuously calling "System.gc();" is not a solution for me as it severely impacts the performance of the program, and it's generally recommended to avoid it. Besides, Java should handle this automatically.

Another thing that worked was setting "-Dprism.order=sw -Xmx1024m" in the VM options. However, this caused the entire UI to be relatively buggy, so it's not a solution for me either.

To trigger the garbage collector, I also tried calling "setImage(null);" every time, hoping that the garbage collector would "clean up." Unfortunately, this didn't help at all.

I have created a code example that indirectly reproduces the same issue.

Code-Example:

package example;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;

public class Main extends Application {

    private ImageView imageView;
    private Button button;


    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setTitle("ImageView Example");

        button = new Button("Start");
        button.setOnAction(e -> start());

        imageView = new ImageView();
        imageView.setFitWidth(720);
        imageView.setFitHeight(480);

        VBox root = new VBox();
        root.setAlignment(Pos.CENTER);
        root.getChildren().addAll(imageView, button);

        Scene scene = new Scene(root, 720, 510);

        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void start() {
        button.setDisable(true);
        new Thread(() -> {
            while (true) {
                Platform.runLater(() -> {
                    try {
                        imageView.setImage(new Image(new ByteArrayInputStream(robotScreenshot("jpg"))));
//System.gc(); <- would fix the "bug" but is not a option for me
                    } catch (Exception e) {
                        e.printStackTrace();
                        System.exit(-1);
                    }
                });

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.exit(-1);
                }
            }
        }).start();
    }

    private static final GraphicsDevice graphicDevice = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()[0];

    public static byte[] robotScreenshot(String format) throws Exception {
        Rectangle rectangle = graphicDevice.getDefaultConfiguration().getBounds();
        BufferedImage bufferedImage = new Robot().createScreenCapture(rectangle);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ImageIO.write(bufferedImage, format.toLowerCase(), byteArrayOutputStream);
        return byteArrayOutputStream.toByteArray();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

I tried various approaches to resolve the issue with the growing D3D VRAM pool in JavaFX. Initially, I enabled the "-Dprism.verbose=true" option to gather more information about the problem. My expectation was to identify the cause of the crash and find a solution. However, I discovered that the console continuously displayed messages indicating the growth of the VRAM pool with increasingly larger numbers.

To address the issue, I experimented with using "System.gc();" after each "setImage();" method call in the ImageView. Surprisingly, this temporarily resolved the problem. However, I realized that constantly triggering the garbage collector negatively impacted the program's performance, which was not a viable long-term solution.

I also attempted setting the VM options to "-Dprism.order=sw -Xmx1024m," but this led to UI bugs, making it unsuitable as a resolution.

Additionally, I tried calling "setImage(null);" instead of "System.gc();" to trigger the garbage collector, but it had no effect on the issue.

In summary, despite my attempts to mitigate the growing VRAM pool problem, I have yet to find a satisfactory solution that maintains the program's performance while avoiding the NullPointerException and excessive memory usage.

Slaw
  • 37,820
  • 8
  • 53
  • 80
Finn
  • 11
  • 2
  • For context, this is a follow-up question to [JavaFX exception on setImage in a loop problem](https://stackoverflow.com/questions/76629736/javafx-exception-on-setimage-in-a-loop-problem). – jewelsea Jul 12 '23 at 21:14
  • *"after a few seconds, the program starts crashing"* -> I couldn't replicate this. I tried running the example code with OpenJDK 20.0.1 and JavaFX 20.0.1 on OS X 13.4.1 on a 2019 MacBook Pro. The application was still running fine, with no exception, after ten minutes. It could be that the issue may be resolved for you by upgrading to a modern version of the JRE and JavaFX. The optimizations mentioned in [DuncG's answer](https://stackoverflow.com/users/4712734/duncg), and perhaps some others, are probably good ideas to implement as well. – jewelsea Jul 12 '23 at 21:27
  • *"by enabling `-Dprism.verbose=true`, I can get more information about the problem. I discovered that the console keeps outputting: `Growing pool D3D Vram Pool target to 524,167,168`"* -> With that setting, I do not receive such console log messages on my setup (there are no logs in my console related to the "D3D Vram Pool"), which makes sense because OS X doesn't use D3D. It is possible that your issue is Windows system related, though I currently don't have a test system available to try that. – jewelsea Jul 13 '23 at 01:39
  • That's very interesting, yes as I said when I enabled the "-Dprism.order=sw" option, and thus also use a different (rendering?), I also had no problems with it, very strange, I also changed the computer in the meantime to test, but even if it should be a windows problem, why does the System.gc(); call then fix the whole thing – Finn Jul 13 '23 at 06:49
  • `-Dprism.order=sw` is software rendering. It uses the CPU, not the GPU and the Direct3D system. So there is no hardware rendering and there is no texture memory limit on a graphics card involved, only a limit on the actual main memory available to the system (which can be paged to disk). But software rendering on a modern system, is not optimal, it is slower and numerous rendering features such as effects and 3D are disabled in that mode, which is why it is not the standard option. – jewelsea Jul 13 '23 at 07:21
  • *"even if it should be a windows problem, why does the System.gc(); call then fix the whole thing"* -> Could be an issue with the obsolete version of JavaFX you are using which may be fixed in later versions. Could be an issue specific to Direct3D resource management in JavaFX. Could be an issue with your graphics card driver. Could be numerous things. You would need to debug it on a platform that exhibits the issue to determine the root cause. Better to use modern software and write code that avoids the issue IMO. – jewelsea Jul 13 '23 at 11:18
  • I can't replicate your exception. I found an older Windows Laptop, which uses `Intel(R) UHD Graphics 620`, which is low-end integrated graphics on the CPU. I downloaded an old Oracle JDK 8 edition `1.8.0_371-b11`. I enabled prism debug. I added benchmarking to each runlater call. The awt robot and ImageIO you use is quite slow, taking about half a second per frame. So the limiter with the CountDownLatch recommended by DuncG is absolutely necessary or you will flood the JavaFX runLater queue and the AWT robot and eventually kill the app. – jewelsea Jul 15 '23 at 06:24
  • But even still, with or without DuncG's fix, it won't crash quickly (within a few minutes, I didn't try longer) for me. Without the latch limiter it will update very slowly as it is starts trying to take lots of simultaneous screenshots, but it doesn't die. With the latch limiter, it will update each half second, the prism debug will output that it grows the Vram (which as I noted before, I believe is a normal thing for it to do), eventually the Vram on my system gets up to about 535K, then stops growing, and the app continues running. For clean shutdown I made the thread a daemon thread. – jewelsea Jul 15 '23 at 06:26

1 Answers1

2

It's not possible for me to test this, but the code you have is flawed because this line puts a task into the Platform JavaFXUI thread every 100ms, but you have no idea whether the Platform task finished before you queue up another task:

Platform.runLater(() -> { ... });
Thread.sleep(100);

If the action takes over 100ms you will start to get a lot of backed up tasks in the UI thread, and then JavaFX will not have a chance to repaint the image properly after each new setImage making the use of more calls to Platform.runLater pointless because you never see the newer image on screen before your code tries to replace it. The app will become jerky / unresponsive when this starts to occur, and you will get memory issues with a large backlog.

A simple way to fix this is to change the while loop to wait for each item in the runLater call, one way would be with CountDownLatch:

while (true) {
    CountDownLatch done = new CountDownLatch(1);
    Platform.runLater(() -> {
        try {
            imageView.setImage(new Image(new ByteArrayInputStream(robotScreenshot("jpg"))));
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(-1);
        }
        done.countDown();
    });

    try {
        done.await();
        Thread.sleep(100);
    } catch (InterruptedException e) {
        e.printStackTrace();
        System.exit(-1);
    }
}

In addition, it is better to have as much processing outside of the JavaFX UI thread so try moving the screen grab out of the Platform.runLater call and diff byte[] with previous and only submit new task for setImage when the new image is changed.

DuncG
  • 12,137
  • 2
  • 21
  • 33