Search code examples
javamultithreadingimagelibgdxpixmap

Libgdx images from pixmaps are drawn solid black


For startes, i know libgdx is primarily used for gaming. I have just found out about it and wanted to try to give it an extra other (possible) purpose, for example a simple photo frame. The below code is just a part of a proof of concept and when it runs as it should evolve to a bigger app.

Below i have posted a very simple class at what i'm up to now. What it does is every [num] seconds, in a different thread, it loads an image from disk, put it in a pixmap and creates a texture from it on the GL thread (if i understand everything correctly).

I came to this code after a lot of trial and error, It took me an hour to found out that a texture should be created in the OpenGL thread. When the texture is created outside the thread the images where just big black boxes without the loaded texture.

Well, when i ran this class version with textures created on the thread I finally see the image showing, and nicely fading every [num] seconds.

But, after 15 executions the images are starting to appear as black boxes again as if the texture is created outside the GL thread. I'm not getting any exceptions printed in the console.

The application is running on a Raspberry Pi with memory split 128/128. The images are jpeg images in 1920*1080 (not progressive). Memory usage is as follows according to top:
VIRT: 249m
RES: 37m
SHR: 10m Command line is: java -Xmx128M -DPI=true -DLWJGJ_BACKEND=GLES -Djava.library.path=libs:/opt/vc/lib:. -classpath *:. org.pidome.raspberry.mirrorclient.BootStrapper

I see the RES rising when a new image is loaded but after loading it is back to 37.

The System.out.println("Swap counter: " +swapCounter); keeps giving me output when the thread is run.

Could one of you guys point me in the right direction into solving the issue that after 15 iterations the textures are not shown anymore and the images are solid black?

Here is my current code (name PhotosActor is misleading, a result of first trying it to be an Actor):

public class PhotosActor {

    List<Image> images = new ArrayList<>();

    private String imgDir = "appimages/photos/";
    List<String> fileSet = new ArrayList<>();

    private final ScheduledExecutorService changeExecutor = Executors.newSingleThreadScheduledExecutor();

    Stage stage;

    int swapCounter = 0;

    public PhotosActor(Stage stage) {
        this.stage = stage;
    }

    public final void preload(){
        loadFileSet();
        changeExecutor.scheduleAtFixedRate(switchimg(), 10, 10, TimeUnit.SECONDS);
    }

    private Runnable switchimg(){
        Runnable run = () -> {
            try {
                swapCounter++;
                FileInputStream input = new FileInputStream(fileSet.get(new Random().nextInt(fileSet.size())));
                Gdx2DPixmap gpm = new Gdx2DPixmap(input, Gdx2DPixmap.GDX2D_FORMAT_RGB888);
                input.close();
                Pixmap map = new Pixmap(gpm);
                Gdx.app.postRunnable(() -> {
                    System.out.println("Swap counter: " +swapCounter);
                    Texture tex = new Texture(map);
                    map.dispose();
                    Image newImg = new Image(tex);
                    newImg.addAction(Actions.sequence(Actions.alpha(0),Actions.fadeIn(1f),Actions.delay(5),Actions.run(() -> {
                        if(images.size()>1){
                            Image oldImg = images.remove(1);
                            oldImg.getActions().clear();
                            oldImg.remove();
                        }
                    })));
                    images.add(0,newImg);
                    stage.addActor(newImg);
                    newImg.toBack();
                    if(images.size()>1){ images.get(1).toBack(); }
                });
            } catch (Exception ex) {
                Logger.getLogger(PhotosActor.class.getName()).log(Level.SEVERE, null, ex);
            }
        };
        return run;
    }

    private void loadFileSet(){
        File[] files = new File(imgDir).listFiles();
        for (File file : files) {
            if (file.isFile()) {
                System.out.println("Loading: " + imgDir + file.getName());
                fileSet.add(imgDir + file.getName());
            }
        }
    }


}

Thanks in advance and cheers, John.


Solution

  • I was able to resolve this myself, A couple of minutes ago it struck me that i have to dispose the texture. I was in the believe that removing the image also removed the texture. Which it clearly did not (or i have to update to more recent version).

    So what i did was create a new class extending the image class:

    public class PhotoImage extends Image {
    
        Texture tex;
    
        public PhotoImage(Texture tex){
            super(tex);
            this.tex = tex;
        }
    
        public void dispose(){
            try {
                this.tex.dispose();
            } catch(Exception ex){
                System.out.println(ex.getMessage());
            }
        }
    }
    

    On all the location i was refering to the image class i changed it to this PhotoImage class. The class modified some now looks like:

    public class PhotosActor {
    
        List<PhotoImage> images = new ArrayList<>();
    
        private String imgDir = "appimages/photos/";
        List<String> fileSet = new ArrayList<>();
    
        private final ScheduledExecutorService changeExecutor = Executors.newSingleThreadScheduledExecutor();
    
        Stage stage;
    
        int swapCounter = 0;
    
        public PhotosActor(Stage stage) {
            this.stage = stage;
        }
    
        public final void preload(){
            loadFileSet();
            changeExecutor.scheduleAtFixedRate(switchimg(), 10, 10, TimeUnit.SECONDS);
        }
    
        private Runnable switchimg(){
            Runnable run = () -> {
                try {
                    swapCounter++;
                    byte[] byteResult = readLocalRandomFile();
                    Pixmap map = new Pixmap(byteResult, 0, byteResult.length);
                    Gdx.app.postRunnable(() -> {
                        System.out.println("Swap counter: " +swapCounter);
                        Texture tex = new Texture(map);
                        map.dispose();
                        PhotoImage newImg = new PhotoImage(tex);
                        images.add(0,newImg);
                        stage.addActor(newImg);
                        addTransform(newImg);
                    });
                } catch (Exception ex) {
                    Logger.getLogger(PhotosActor.class.getName()).log(Level.SEVERE, null, ex);
                }
            };
            return run;
        }
    
        public void addTransform(Image img){
            switch(new Random().nextInt(3)){
                case 0:
                    img.toBack();
                    if(images.size()>1){ images.get(1).toBack(); }
                    img.addAction(Actions.sequence(Actions.alpha(0),Actions.fadeIn(1f),Actions.delay(5),Actions.run(() -> {
                        removeOldImg();
                    })));
                break;
                case 1:
                    img.toBack();
                    if(images.size()>1){ images.get(1).toBack(); }
                    img.setPosition(1920f, 1080f);
                    img.addAction(Actions.sequence(Actions.moveTo(0f, 0f, 5f),Actions.run(() -> {
                        removeOldImg();
                    })));
                break;
                case 2:
                    img.toBack();
                    if(images.size()>1){ images.get(1).toBack(); }
                    img.setScale(0f, 0f);
                    img.setPosition(960f, 540f);
                    img.addAction(Actions.sequence(Actions.parallel(Actions.scaleTo(1f, 1f, 5f), Actions.moveTo(0f, 0f, 5f)),Actions.run(() -> {
                        removeOldImg();
                    })));
                break;
            }
        }
    
        private void removeOldImg(){
            if(images.size()>1){
                PhotoImage oldImg = images.remove(1);
                oldImg.remove();
                oldImg.getActions().clear();
                oldImg.dispose();
            }
            System.out.println("Amount of images: " + images.size());
        }
    
        private byte[] readLocalRandomFile() throws Exception{
            FileInputStream input = null;
            try {
                input = new FileInputStream(fileSet.get(new Random().nextInt(fileSet.size())));
                ByteArrayOutputStream out;
                try (InputStream in = new BufferedInputStream(input)) {
                    out = new ByteArrayOutputStream();
                    byte[] buf = new byte[1024];
                    int n = 0;
                    while (-1 != (n = in.read(buf))) {
                        out.write(buf, 0, n);
                    }   
                    out.close();
                    return out.toByteArray();
                } catch (IOException ex) {
                    Logger.getLogger(PhotosActor.class.getName()).log(Level.SEVERE, null, ex);
                }
            } catch (FileNotFoundException ex) {
                Logger.getLogger(PhotosActor.class.getName()).log(Level.SEVERE, null, ex);
            }
            throw new Exception("No data");
        }
    
        private void loadFileSet(){
            File[] files = new File(imgDir).listFiles();
            for (File file : files) {
                if (file.isFile()) {
                    System.out.println("Loading: " + imgDir + file.getName());
                    fileSet.add(imgDir + file.getName());
                }
            }
        }
    
    
    }
    

    In the remove function i now have added

    oldImg.dispose();
    

    to get rid of the texture. Image transitions are now happy running on 50+ fps on the Raspberry Pi and the image rotation counter is on: 88 now. If there where people thinking thanks for your time!