How slow is 'GetDrawingSurface' ? (and other performance-related questions)

Started by Monsieur OUXX, Thu 16/04/2015 10:49:42

Previous topic - Next topic

Monsieur OUXX

I'm trying to find the best way to perform the following process:
- have a very large room (like 5000x5000) in a 320x200 game.
- at game's startup, I save the orginal background to a dynamicSprite
- then at every game cycle, I'll manually draw stuff into the view port. In order to do that, I need to restore the bit of background that's visible in the Viewport. So I just need to redraw a 320x200 area.

I've yet to find a fast way to do that. It's horribly slow, as if the performance dropped because the room is very large. Yet, it can't be the room's fault, because if I just let the game run normally (without doing any of my custom drawing operations), it runs at normal speed.

So I'm trying to fnd the culprit. Could it be GetDrawingSurface? I believe that behind the scenes it just sets a pointer to the memory location where the RGB values of the sprite are stored -- I don't think (I hope!!!) that it duplicates the whole sprite in memory. But I could be wrong.

Here is the code I run in repeatedly_execute:
Code: ags

DynamicSprite* bgWithGrid; //Global variable. It's a backup of the original room's background.

  ...

void repeatedly_execute()
{
  DrawingSurface* ds = Room. GetDrawingSurfaceFromBAckground();
  DrawingSurface* source = bgWithGrid.GetDrawingSurface();
  DynamicSprite* s = DynamicSprite.CreateFromDrawingSurface(source,  GetViewportX(),  GetViewportY(),  System.ScreenWidth,  System.ScreenHeight);
  ds.DrawImage(GetViewportX(), GetViewportY(), s.Graphic); //Let's redraw the original background
  
  source.Release();
  s.Delete();
  ds.Release();
}





 

Crimson Wizard

GetDrawingSurface is not slow, and it does not duplicate anything. It simply creates a reference to existing bitmap and wraps it in a "script pointer".

What is slow is that you clone the part of "source" image every game tick, then draw newly created pic, only to then dispose it and repeat the process again next tick. This is simply terrible.

In my strong opinion, AGS Script lacks the drawing function that would let to draw only part of image without cropping it first. I think I suggested to add such function several years ago, when CJ was still actively working on program. For some reason this was never done.
Hint: add this to issue tracker now...

What you could do, though, is draw "source" with negative coordinates. I can't make a proper test right now, but IIRC AGS should detect which parts of the bitmap are outside of the destination surface and skip them. You are lucky in a way that the picture should cover whole game screen.
Try doing this:
Code: ags

void repeatedly_execute()
{
  DrawingSurface* ds = Room. GetDrawingSurfaceFromBackground();
  ds.DrawImage(-GetViewportX(), -GetViewportY(), bgWithGrid.Graphic); //Let's redraw the original background
  ds.Release();
}




E: By the way, try NOT to using System.ScreenWidth and ScreenHeight, like EVER. These are not game size, these are game size + black borders, and you may get incorrect results on pre-3.4.0 engines. In AGS 3.4 these are usually equal to game size, but that was done to keep backwards script compatibility.
Use System.ViewportWidth and System.ViewportHeight instead.

Monsieur OUXX

Quote from: Crimson Wizard on Thu 16/04/2015 11:32:20
E: By the way, try NOT to using System.ScreenWidth and ScreenHeight, like EVER. These are not game size, these are game size + black borders. Use System.ViewportWidth and System.ViewportHeight instead.

Woops! Thanks. For the rest of what you wrote, I'll have a look asap.

Quote from: Crimson Wizard on Thu 16/04/2015 11:32:20
Code: ags

  ds.DrawImage(..., ..., bgWithGrid.Graphic); //Let's redraw the original background

Don't you think it will be even slower to redraw the entire giant background, like this?


Tracker: http://www.adventuregamestudio.co.uk/forums/index.php?issue=664.0
 

Calin Leafshade

probably not because the areas outside the viewport will be culled and not rasterized.

Crimson Wizard

Quote from: Monsieur OUXX on Thu 16/04/2015 11:45:27
Don't you think it will be even slower to redraw the entire giant background, like this?
Quote from: Calin Leafshade on Thu 16/04/2015 12:07:00
probably not because the areas outside the viewport will be culled and not rasterized.
Yes, in drawing function it calculates the sensible area to draw. It must not draw outside these limits anyway, because it will overwrite the memory beyond bitmap (= program screwed up).



UPD: Umm.... wait.... the destination bitmap is a big scrollable room background too.......... hmmmmm. :undecided:
I might have given a bad advice here. Need to think more about this.

Calin Leafshade

in that case just make a new bitmap that is screen size, attach it to a GUI and draw on that.

Snarky

Doesn't work so well if you have characters, objects, etc. on screen.

Would it be worth checking if this is faster?

Code: AGS

DrawingSurface* roomBgSurf;

void initRoomBgSurf()
{
  DrawingSurface* surface = Room.GetDrawingSurfaceForBackground();
  roomBgSurf = surface.CreateCopy();
  surface.Release();
}

function repeatedly_execute()
{
  DrawingSurface* surface = Room.GetDrawingSurfaceForBackground();
  surface.DrawSurface(roomBgSurf);
  surface.Release();
}


No, wait, I have a better idea. Keep a viewport-sized dynamicSprite in memory, draw the original bg to that, and then draw the sprite to the bg. It's an extra draw operation, but you're only working on the viewport size, not the room size.

Monsieur OUXX

In the meantime I've tried out a workaround relying on splitting the giant background into tiles and then drawing only the required tiles depending on the viewport position. Even with 100 tiles (32x20 pixels) fitting in the viewport, that's also very slow. Dammit!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 

Crimson Wizard

Do you repaint all the time? Try to make "need_redraw" flag that is set to true only when viewport is moved and check it before drawing.

Frankly, I am not sure drawing 320x200 amount of pixels would slow things so much. Maybe there's something else?

Snarky

So, to write out what I was suggesting above:

Code: ags

DynamicSprite* plainBackground;
DynamicSprite* backgroundViewPort;
  ...

void initDynamicSprites()
{
  plainBackground = DynamicSprite.CreateFromBackground();
  backgroundViewPort = DynamicSprite.Create(System.ViewportWidth, System.ViewportHeight);
}

void repeatedly_execute()
{
  DrawingSurface* ds = Room.GetDrawingSurfaceFromBackground();
  DrawingSurface* bgvp = backgroundViewPort.GetDrawingSurface();

  bgvp.DrawImage(-GetViewportX(), -GetViewPortY(), plainBackground.Graphic);
  bgvp.Release();

  ds.DrawImage(GetViewportX(), GetViewPortY(), backgroundViewPort.Graphic);
  ds.Release();
}

Monsieur OUXX

Quote from: Snarky on Thu 16/04/2015 13:20:37
Keep a viewport-sized dynamicSprite in memory, draw the original bg to that, and then draw the sprite to the bg.
Quote from: Calin Leafshade on Thu 16/04/2015 12:22:01
just make a new bitmap that is screen size, attach it to a GUI and draw on that.
You sggested the same thing , combined with CW's idea of using negative coordinates. Calin's idea requires one less DrawImage (it's the engine that draws the GUI's image automatically).
I'll try it and let you know. EDIT: wrote this message while Snarky was writing an implementation. I'm trying asap.

 


Monsieur OUXX

For the record here is the implementation using the tiles :
(I still have no idea why it's so slow)
Code: ags


DynamicSprite* bits_array[]; //because of AGS low performance, we'll actually save the background as an array of tiles.
struct BgBits {
  //DynamicSprite* bits_array[];  //this array cannot be placed here because of AGS language limitations 
  int bits_w;
  int bits_h;
  
  int nbBits_horiz; //how many tiles columns
  int nbBits_vertic; //how many tiles rows.  
                    // nbBits_horiz*nbBits_vertic give the total number of tiles making up the background
};
BgBits bgBits;







void DrawGrid(DrawingSurface* ds)
{
  //draw some stuff onto ds, that we'll want to paste onto the original background
}

//Restores the original background (into the viewport only) using a tiles-based system
void ClearBackground(DrawingSurface* ds)
{

  int i1 = GetViewportX()/bgBits.bits_w;
  int ofs_x = (i1+1)*bgBits.bits_w - GetViewportX();
  int j1 = GetViewportY()/bgBits.bits_h;
  int ofs_y = (j1+1)*bgBits.bits_h - GetViewportY();
  
  int i2 = i1+ System.ViewportWidth/bgBits.bits_w;
  int j2 = j1+System.ViewportHeight/bgBits.bits_h;
  
  
  int i = i1; int j;
  int i_s,  j_s;
  while(i<i2) {
    j=j1;
    j_s = 0;
    while (j<j2) {
      int index = j*bgBits.nbBits_horiz+i;
      ds.DrawImage(GetViewportX()+ofs_x+ i_s * bgBits.bits_w, GetViewportY()+ofs_y+j_s*bgBits.bits_h, bits_array[index].Graphic);
      j++;
      j_s++;
    }
    i++;
    i_s++;
  }

}



bool BackgroundInitialized; //flag to save the room's background and perform all associated computing (drawing grid, etc.) only once


void InitBackground()
{
  if (!BackgroundInitialized)
  {
    SetViewport(25, 13); //DEBUG
  
    DynamicSprite* backgroundBackup = DynamicSprite.CreateFromBackground(); //Create a copy of the room's background
    
    //create a new empty sprite where we draw the grid
    DynamicSprite* g = DynamicSprite.Create(Room.Width,  Room.Height, false);
    DrawingSurface* gS = g.GetDrawingSurface();
    gS.Clear(COLOR_TRANSPARENT);
    DrawGrid(gS);
    gS.Release();
    
    //create a second copy of the room's bg, then we render the sprite with the grid onto that one (with transparency)
    DynamicSprite* bgWithGrid = DynamicSprite.CreateFromExistingSprite(backgroundBackup.Graphic, false);
    DrawingSurface* s2 = bgWithGrid.GetDrawingSurface();
    s2.DrawImage(0, 0,  g.Graphic,  100-grid.opacity);
    
    //delete the grid sprite, we needed it only for transparency drawing
    g.Delete();
    
    //and now... CHOP IT INTO BITS!
    bgBits.bits_w = System.ViewportWidth /10;
    bgBits.bits_h = System.ViewportHeight /10;
    
    bgBits.nbBits_horiz = Room.Width / bgBits.bits_w;
    bgBits.nbBits_vertic = Room.Height / bgBits.bits_h;
    
    bits_array = new DynamicSprite [bgBits.nbBits_horiz * bgBits.nbBits_vertic];
    int i=0; int j = 0;
    while (j<bgBits.nbBits_vertic) {
      i=0;
      while (i<bgBits.nbBits_horiz) {
        bits_array[j*bgBits.nbBits_horiz+i] = DynamicSprite.CreateFromDrawingSurface(s2, i*bgBits.bits_w,  j*bgBits.bits_h, bgBits.bits_w,  bgBits.bits_h);
        i++;
      }
      j++;
    }
    
    s2.Release();
    
    backgroundBackup.Delete();
    bgWithGrid.Delete();
    
    BackgroundInitialized = true;

  }
}


void Render()
{
  InitBackground(); //This will be done only once, at first game loop
  

    DrawingSurface* ds = Room.GetDrawingSurfaceForBackground();
    ClearBackground(ds);
    ds.Release();
    
}
void repeatedly_execute()
{
  Render();
}




void game_start()
{

}
 

Monsieur OUXX

The solution(s) with the negative coordinates works very well. The thing with AGS built-in functions is that I never know which ones have cropping-checking or which ones will throw exceptions with the wrong coordinates.

 

Snarky

Quote from: Monsieur OUXX on Thu 16/04/2015 14:02:59
You sggested the same thing , combined with CW's idea of using negative coordinates. Calin's idea requires one less DrawImage (it's the engine that draws the GUI's image automatically).

Saving that DrawImage() call may or may not make a difference, since the engine still needs to draw it.

In any case, the drawback with using a GUI is that it will cover up any characters or objects. If that's not an issue for you, go right ahead. But if you don't have anything on the screen, why draw directly on the background in the first place? You could keep what you draw on a separate "layer" (as a full-screen GUI), and simply clear it between updates. That way you won't need a backup copy of the background, and don't have to copy it into your drawing surface.

It also reminds me: The third alternative is to use a full-screen object with baseline 0 (so it appears behind all the other objects or characters). However, if you have any walkbehinds, it won't be drawn in those regions. Also, you need to move it when the background scrolls to stay on-screen, and unless you've locked the viewport and you're controlling it in the script, there will be jittering. So probably not the best idea.

SMF spam blocked by CleanTalk