mabe02 / lanterna

Java library for creating text-based GUIs
GNU Lesser General Public License v3.0
2.3k stars 243 forks source link

frame time shoots up by 500% when calling screen.clear() #618

Closed epic-64 closed 4 weeks ago

epic-64 commented 1 month ago

Hey there,

Issue: If I call screen.clear() on every frame, it increases my frame time by ~500%. The behavior is similar in an actual terminal as well as the JFrame (although JFrame has a slower baseline).

E.g. without screen.clear() my frames are processed within 1ms, with screen.clear it takes 5ms. If I set my laptop to "power save mode" then both are 3x slower (3ms vs 15ms).

Workaround: Since this is a huge penalty I decided not to call screen.clear() on each frame, but rather only once every few seconds. This often leaves the screen in a corrupted state with some characters lingering around.

Considerations: I'm not sure whether it's the clearing that causes the spike, or the subsequent refresh(), which then can no longer apply its diff logic.

Any ideas how I (or the library) could tackle this issue of keeping the screen integrous without the huge perfromance sacrifice?

Note, the game's update() and draw() loops are supposed to run with or without user input, so it's not as simple as just waiting for user input before redrawing

epic-64 commented 1 month ago

Here is the content of my draw() method which I call every frame (I capped it to max 30 times a second). Is this loop fundamentally flawed somehow? Excuse the Scala syntax.

def draw(block: RenderBlock, graphics: TextGraphics): Unit =
  // screen.clear() makes frame time 5x longer than not calling it. But not calling it results in artifacts
  screen.clear()

  block.draw(graphics) // block contains all my game objects and makes calls to graphics.putString() for each one
  screen.setCursorPosition(null) // hide cursor
  screen.refresh()
end draw
avl42 commented 1 month ago

There may be a mismatch between what lanterna does, and what you expect from it.

I gather that you print out a set of (probably changing) strings to particular locations in the screen, then refresh() it to the user.

What happens: in refresh() lanterna attempts to determine, if your changes warrant for a full refresh, or for a delta-refresh. Depending on the percentage how many characters typically change in the screen, you might get better performance by forcing full refreshes by passing it the argument: RefreshType.COMPLETE .

If the screen contents are really scrolled (e.g. you have some array of data that you display each in a line, and only a range of that array is displayed on screen at a time) then it might further improve performance to use scrollLines(...) as this will tell the terminal to scroll, and then only update what changed apart from scrolling (usually: the new last line, or first line if scrolling down). If scrolling is your pattern, and you go to use scrollLines, then do NOT use the RefreshType.COMPLETE mentioned above.

On Thu, Oct 24, 2024 at 1:16 PM William Raendchen @.***> wrote:

Here is the content of my draw() method which I call every frame. Excuse the Scala syntax.

def draw(block: RenderBlock, graphics: TextGraphics): Unit = // screen.clear() makes frame time 5x longer than not calling it. But not calling it results in artifacts screen.clear()

block.draw(graphics) // calls graphics.putString() for a ton of strings (all my game objects) screen.setCursorPosition(null) // hide cursor screen.refresh() end draw

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you are subscribed to this thread.Message ID: @.***>

epic-64 commented 1 month ago

I'm not sure I understand if this applies to me, but I wanna clarify what exactly I'm doing and how it corrupts my screen if I don't call screen.clear().

I have a bunch of game objects that mostly stay in the same place, but they change slightly. As long as the element always takes up as much space as previously, there is no problem, because the entire space is overridden in the new frame, and refresh() draws it to the screen.

But for example if I spend most of my gold and it goes from 1234 to 3, then the old string and new strings are (internally) gold: 1234 gold: 3

using refresh() with the diff method, this results in the following being drawn: gold: 3124

My naive solution to this was to always clear the screen, which is similar to always doing a full refresh

Some more options come to my mind:

In summary, I think it's fair to say, calling screen.clear or a complete refresh on every frame is not a good idea as it's expected to be very slow and to be used sparingly

avl42 commented 1 month ago

I wrote it in reply to your question, so at least I do believe it applies to you.

just add the parameter to the refresh call, and see if it helps.

another option would be to write the texts "blank-padded": then "3 " (rather than "3") would completely overwrite "1234".

William Raendchen @.***> schrieb am Do., 24. Okt. 2024, 19:46:

I'm not sure I understand if this applies to me, but I wanna clarify what exactly I'm doing and how it corrupts my screen if I don't call screen.clear().

I have a bunch of game objects that mostly stay in the same place, but they change slightly. As long as the element always takes up as much space as previously, there is no problem, because the entire space is overridden in the new frame, and refresh() draws it to the screen.

But for example if I spend most of my gold and it goes from 1234 to 3, then the old string and new strings are (internally) gold: 1234 gold: 3

using refresh() with the diff method, this results in the following being drawn: gold: 3124

My naive solution to this was to always clear the screen, which is similar to always doing a full refresh

Some more options come to my mind:

  • only clearing the screen once every few seconds (can cause temporary glitches but they'll fix themselves soon enough)
  • force clear the screen on certain actions, e.g. after spending coins, removing an element from the screen, or when changing scenes
  • additionally, I could make sure my strings are always the same length, e.g. drawing gold: 3 instead of gold: 3 to make sure it's entire "display" is wiped out whenever the number changes.

In summary, I think it's fair to say, calling screen.clear or a complete refresh on every frame is not a good idea as it's expected to be very slow and to be used sparingly

— Reply to this email directly, view it on GitHub https://github.com/mabe02/lanterna/issues/618#issuecomment-2435970183, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABIDBMV5JOE2RDQF3I23XALZ5EW7VAVCNFSM6AAAAABQPXTW3CVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDIMZVHE3TAMJYGM . You are receiving this because you commented.Message ID: @.***>

epic-64 commented 4 weeks ago

Calling screen.refresh(RefreshType.COMPLETE) on every frame leads to similar behavior as screen.clear(), i.e. much longer frame times.

So a combination of the above mentioned techniques has to do the trick