Image() is slow, are there any alternatives?

I am rendering about 1024 images, they have to be scaled up, I store the scaled-up version so it is a bit faster. these images are covering the whole screen, and I would like to get at least 60 fps when rendering this. Is there any faster way, because as soon as I remove the image() from my nested for loops, it runs 60 fps, so this is because of image().

I am rendering all these tiles, and it is slowing me down. (yes I am only rendering the ones on the screen)

2 Likes

You could render small chunks of your map to PGraphics objects and display those instead of all images.

Take a look at the reference.

I wrote a small example program that you should just be able to run. Doesnā€™t seem to make that much of a difference after all, but see for yourself:

int chunkSizePixels;
PImage source;
PGraphics[][] chunks;

void setup() {
  size(1000, 700);

  source = loadImage("https://opengameart.org/sites/default/files/images/grass_test_2.png");
  source.resize(32, 32);
  int imgWidth = source.width, imgHeight = source.height;

  chunkSizePixels = int(width / float(4));
  chunks = new PGraphics[ceil(width / float(chunkSizePixels))][ceil(height / float(chunkSizePixels))];

  for (int i = 0; i < chunks.length; i++)
    for (int j = 0; j < chunks[i].length; j++) {
      PGraphics chunk = chunks[i][j] = createGraphics(chunkSizePixels, chunkSizePixels);
      chunk.beginDraw();

      for (int k = 0; k < ceil(chunkSizePixels / float(imgWidth)); k++)
        for (int m = 0; m  < ceil(chunkSizePixels / float(imgHeight)); m++)
          chunk.image(source, k * imgWidth, m * imgHeight);

      chunk.endDraw();
    }
}

void draw() {
  background(0); 

  long start = millis(); 

  for (int i = 0; i < ceil(width / float(source.width)); i++)
    for (int j = 0; j < ceil(height / float(source.height)); j++)
      image(source, i * source.width, j * source.height); 

  // for (int i = 0; i < chunks.length; i++)
  // for (int j = 0; j < chunks[i].length; j++)
  //    image(chunks[i][j], i * chunkSizePixels, j * chunkSizePixels);

  println(millis() - start);
}
3 Likes

Any idea why making chunks out of images makes it faster?

Another problem with this is I have seen that if any blocks change, it might not work. Also, the map is quite big, so updating these would probably not be efficient. Also, it seems that if I make the tiles get called from a class it slows down furthermore.

The Way I tried doing it was like this:

  for (int i = 0; i < MapWidth; i++) {
    for (int j = 0; j < MapHeight; j++) {
        for (int k = 0; k < blocks.size(); k++) {
          blocks.get(k).display((i*Width)+X, (j*Height)+Y, tile, extraMapData[i][j]);
          //blocks.get(k).special((i*Width)+X, (j*Height)+Y, Width, Height, tile, extraMapData[i][j]);
        }
    }
}

class Block {

  //Note: all hitbox related things are done in the Player class.

  int id;
  boolean cammo;
  boolean solid;
  PImage img;
  PImage[] animatedLooks;
  ArrayList<String> mods = new ArrayList<String>();
  ArrayList<PImage> modimgs = new ArrayList<PImage>();

  Block(PImage looks, int id) {
    img = looks;
    this.id = id;
  }
  Block(PImage[] looks, int id) {
    animatedLooks = looks;
    this.id = id;
  }

  Block(boolean invisible, int id) {
    this.cammo = invisible;
    this.id = id;
  }

  void display(int x, int y, int id, String mods) {

    if (id == this.id) {
      if (!cammo) {
        if (img != null) {
          image(img, x, y);
        } else {
          image(animateOffset(animatedLooks, x, y), x, y, BlockSize, BlockSize);
        }
      } else {
        //grab neighboring block
      }
    } else {
      for (int i = 0; i < blocks.size(); i++) {
        if (blocks.get(i).getId() == id) {
          blocks.get(i).display(x, y, id, mods);
        }
      }
    }
  }


  void addMod(String str, PImage img) {
    mods.add(str);
    modimgs.add(img);
  }
  void setSolid(boolean s) {
    solid =  s;
  }

  int getId() {
    return id;
  }
}

blocks is an ArrayList of these block types. The array list is only as long as the number of block types in the game. Some reason this is way slower than directly for looping it and using an if statement, and then displaying it like this:

  for (int i = 0; i < MapWidth; i++) {
    for (int j = 0; j < MapHeight; j++) {
        switch(tile) {
        case 1:
            image(grass, (i*Width)+X, (j*Height)+Y);
          break;
        ....
    } 
}

I want to use the other method so its easier to add blocks to the game, but for some reason, it slows it down a lot.

1 Like

Just theorizing, but there is probably some overhead with image(). Given that you can pass image anything from a png to a jpeg to an svg, there are probably checks.

If youā€™re really curious, you might try digging through the source:

1 Like

Iā€™m also pretty sure it has something to do with the overhead of image(...).

If you have certain blocks that change very often you could either keep those as seperate images or draw over the existing chunks. Itā€™s not like you would have to redraw the entire chunk, but rather only the tile that changed.

You have an extra loop in there. For each tile in your map, you are drawing every block in your list, meaning every one is drawn MapWidth * MapHeight times.

Do it like this instead:

for (int i = 0; i < MapWidth; i++) {
  for (int j = 0; j < MapHeight; j++) {
     // This logic of course wouldn't be needed if you stored the images in a 2D array
    Block block = blocks.get(i * MapHeight + j);
    block.display((i * Width) + X, (j * Height) + Y, tile, extraMapData[i][j]);
  }
}
2 Likes

I might add that you should use image() command only with three parameters, never with 5.

5 is resizing the image but that should be done beforehand in setup(). Same is for all loadImage() (or other making of an image).

Chrisir

3 Likes

Hi @Chrisir,

you should use image() command only with three parameters, never with 5

Has this been tested across renderers? In PGraphics, image with three parameters defers to imageImpl which may be overridden and implemented differently by sub-classes. The Java AWT rendererā€™s implementation here overrides its parentā€™s implementation. Iā€™m guessing that imageā€™s default arguments are such that expensive operations are not triggered (perhaps here?) . Far as I can tell, the OpenGL renderer doesnā€™t override the parent.

I believe that hypothetically you could override the AWT implementation in a sub-class, but I havenā€™t done enough research to see if thereā€™s another way to approach dynamic tinting and resizing an image. EDIT: or if youā€™d have access to all the fields that JAVA2D accesses in that function.

Best,
Jeremy

PS, take a guess how Processing handles default font rendering with textMode MODEL.

3 Likes

Thanks for leading me in the right direction, all I needed to do was remove the the k for loop, and replace blocks.get(k) with blocks.get(0), because in the block class it loops through them. It appears I was drawing everything about 8 times.

3 Likes

I believe so. The basic idea here is that the last two arguments in the 5-arg image are for scaling. Scaling every frame is incredibly wasteful if you can instead scale once at setup and then repeatedly display with no scaling (3-arg) ā€“ regardless of the implementation. Without magic caching, scaling will always be worse than not.

Hereā€™s a simple example if you want to play with testing different renderers. Press space to switch between 5-arg and 3-arg image(). On my 2016 laptop, that switches the simple sketch between 60fps and ~25 fps.

PImage img;
PImage img2;
int w = 20;
int h = 20;

boolean mode;

void setup() {
  size(640, 640);
  img = loadImage("https://upload.wikimedia.org/wikipedia/commons/2/2e/Processing_3_logo.png");
  img2 = img.copy();
  img2.resize(width/w, height/h);
}
void draw() {
  for (int r=0; r<h; r++) {  
    for (int c=0; c<w; c++) {
      int x = r*height/h;
      int y = c*width/w;
      if(!mode){
        image(img2, x, y);
      } else {
        image(img, x, y, width/w, height/h);
      }
    }
  }
  println(frameRate);
}

void keyReleased() {
  mode = !mode;
}
3 Likes

Very much depends on the renderer. Using P2D or P3D itā€™s basically free. The OP really would be better with P2D here.

And for tile maps, you really want the nine argument version of image()!

1 Like

Hi @jeremydouglass,

Odd, I get the opposite results: with P2D there is no significant dip in frame rate. But Iā€™m not an expert in profiling, and there are a lot of differences between machines which could account for that. Maybe the caching is done elsewhere in the OpenGL renderer code.

regardless of the implementation

I agree that a general approach is to do as many operations on PImage in setup as possible regardless of renderer. But I think thereā€™s room for nuance here, within the AWT implementation thereā€™s plenty of commented out code where the devs have tried different ways to do the magic. Anyways, I was curious because Iā€™ve been tinkering with a library that subclasses a few renderers. Sounds like a rabbit hole Iā€™m not inclined to wander down, though.

Thanks!
Jeremy

1 Like

There is no need for caching in OpenGL renderer code, because of the way OpenGL works - textures are scaled on the GPU (effectively for free). If anything caching is likely to cause a problem. AWT can theoretically do hardware accelerated scaling of images too, but this is unlikely to kick in with the way Processing uses it, and is also somewhat OS dependent.

3 Likes

Thanks for the insights, @neilcsmith.

That is what Iā€™m seeing on that test sketch when I plug in P2D.

100000 tiles on a 2000x2000 image:

  1. 60fps ā€“ P2D ā€“ img(f, x, y)
  2. 60fps ā€“ P2D ā€“ img(f, x, y, w, h) ā† !!!
  3. 15fps ā€“ JAVA2D ā€“ img(f, x, y)
  4. 2fps ā€“ JAVA2D ā€“ img(f, x, y, w, h)

So the avoid-5-arg advice is actually quite Java2D specific. I should not have discounted ā€œmagic caching.ā€ Of course, this simple test is for a single image at exactly size ā€“ probably the best possible case scenario. Results might vary if you have many different tiles ā€“ I just havenā€™t tried. But the overall picture seems pretty clear.

To work with tiles you should normally use one image with all the tiles in it and the 9 argument variant of image(..) to draw with -

image(source, xPosition, yPosition, width, height, srcX1, srcY1, srcX2, srcY2);

Switching images (textures) is slower, at least with OpenGL.

Unfortunately, Java2D is using BufferedImage as the target by the look of it (sure it used BufferStrategy or VolatileImage at one point), which means that drawing is never GPU accelerated. Also try with smooth(0) if you havenā€™t, which should improve performance, but not quality(!), of Java2D.

1 Like

what is the nine argument version?

Why is that when I put P2D in the size(), it is 200% faster, but everything looks awful. The text is blurry, buttons are blurry, even some images look blurry. Some of the tiles look the same like this, is there any way I could apply P2D rendering on only these images?

I have tried
buffer = createGraphics(x, y, P2D);
this does not seem like itā€™s giving me more frames.