Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dropping frames: Trying to use optimizations. Can't determine my issue #1130

Closed
jrcichra opened this issue Apr 4, 2020 · 13 comments
Closed

Comments

@jrcichra
Copy link

jrcichra commented Apr 4, 2020

Hi @hajimehoshi!

I'm in the process of porting my 2D isometric game to ebiten from pixel. I'm struggling with performance. I'm dropping frames on Linux and much worse on Windows.

I used the SubImage feature on a single ebiten.NewImageFromImage (using that one source) for all my sprites.

My code (at the time of writing): https://github.com/jrcichra/gollercoaster/tree/d5288f1812a16655b995b44e8eb9668d18bba789

This is my render() function, which comes at the end of update()

func (g *Game) render(screen *ebiten.Image) {
	op := &ebiten.DrawImageOptions{}
	for x := 0; x <= g.currentLevel.GetWidth()-1; x++ {
		for y := 0; y <= g.currentLevel.GetHeight()-1; y++ {
			xi, yi := g.cartesianToIso(float64(x), float64(y))
			op.GeoM.Reset()
			//Translate for isometric
			op.GeoM.Translate(float64(xi), float64(yi))
			//Translate for camera position
			op.GeoM.Translate(-g.CamPosX, g.CamPosY)
			//Scale for camera zoom
			op.GeoM.Scale(g.CamZoom, g.CamZoom)
			//Translate for center of screen offset
			op.GeoM.Translate(float64(g.windowWidth/2.0), float64(g.windowHeight/2.0))
			t, err := g.currentLevel.GetTile(x, y)
			if err != nil {
				fmt.Println(err)
			} else {
				t.Draw(screen, op)
			}
		}
	}
}

t.Draw() is here:

func (t *Tile) Draw(screen *ebiten.Image, options *ebiten.DrawImageOptions) {
	//To draw a tile, you need to render the sprites in order from furthest to closest
	//Here I'm assuming 0 is furthest and N is closest
	for _, s := range t.sprites {
		screen.DrawImage(s.Sprite, options)
	}

	//Pop off for every temp push
	for i := 0; i < t.temppushes; i++ {
		t.Pop()
		t.temppushes--
	}

}

How I load textures:

func (t *TextureLoader) Open(path string) error {
	var err error
	t.file, err = ebitenutil.OpenFile(path)
	if err != nil {
		return err
	}
	img, _, err := image.Decode(t.file)
	if err != nil {
		return err
	}
	t.picture, err = ebiten.NewImageFromImage(img, ebiten.FilterDefault)
	return err
}

//GetTexture - returns a sprite from the coordinates on the texture
func (t *TextureLoader) GetTexture(x, y int) *sprite.Sprite {
	img := t.picture.SubImage(image.Rect(x*64, ((y + 1) * 64), (x+1)*64, (y * 64))).(*ebiten.Image)
	s := &sprite.Sprite{}
	s.Sprite = img
	return s
}

I am using pointers so I am not sure what the issue is.

Thanks for your help and an awesome Go 2D library!

EDIT: Attached profiler chart:
file-1

@hajimehoshi
Copy link
Owner

hajimehoshi commented Apr 5, 2020

Hi, thank you for using Ebiten!

I'd like to know what kind of machine you are using (Linux and Windows).

I tried your game on my MacBook Pro 2018, and saw it kept 60FPS.

I also tried -tags=ebitendebug, and the number of graphics operations seems pretty regular. I feel like more operations should be integrated, so I'll take a look more:

--
draw-triangles: dst: 4 <- src: 1, colorm: <nil>, mode copy, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 4 <- src: 2, colorm: <nil>, mode source-over, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 4 <- src: 2, colorm: <nil>, mode source-over, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 4 <- src: 2, colorm: <nil>, mode source-over, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 4 <- src: 2, colorm: <nil>, mode source-over, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 4 <- src: 2, colorm: <nil>, mode source-over, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 5 (screen) <- src: 1, colorm: <nil>, mode clear, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 5 (screen) <- src: 4, colorm: <nil>, mode copy, filter: screen, address: clamp_to_zero
--
draw-triangles: dst: 4 <- src: 1, colorm: <nil>, mode copy, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 4 <- src: 2, colorm: <nil>, mode source-over, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 4 <- src: 2, colorm: <nil>, mode source-over, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 4 <- src: 2, colorm: <nil>, mode source-over, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 4 <- src: 2, colorm: <nil>, mode source-over, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 4 <- src: 2, colorm: <nil>, mode source-over, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 5 (screen) <- src: 1, colorm: <nil>, mode clear, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 5 (screen) <- src: 4, colorm: <nil>, mode copy, filter: screen, address: clamp_to_zero
--
draw-triangles: dst: 4 <- src: 1, colorm: <nil>, mode copy, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 4 <- src: 2, colorm: <nil>, mode source-over, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 4 <- src: 2, colorm: <nil>, mode source-over, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 4 <- src: 2, colorm: <nil>, mode source-over, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 4 <- src: 2, colorm: <nil>, mode source-over, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 4 <- src: 2, colorm: <nil>, mode source-over, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 5 (screen) <- src: 1, colorm: <nil>, mode clear, filter: nearest, address: clamp_to_zero
draw-triangles: dst: 5 (screen) <- src: 4, colorm: <nil>, mode copy, filter: screen, address: clamp_to_zero
...

@hajimehoshi
Copy link
Owner

hajimehoshi commented Apr 5, 2020

OK I understood the reason why the app was slow on you machine: your application is drawing too many objects per one frame: over about 300K vertices (= 75K rectangles). I don't think your game needs to draw all of them at the same time. Would it be possible to reduce the number by rendering objects only visible in the current region?

@jrcichra
Copy link
Author

jrcichra commented Apr 5, 2020

@hajimehoshi it certainly would help! An optimization I made in the pixel game engine was to only redraw when I knew something changed, like camera movement: https://github.com/jrcichra/gollercoaster/blob/62570d8957c033fe121c111bc883e7df919eb24a/game/game.go#L83

The game would play at a high framerate, even with camera movement. Only when I placed blocks did the framerate dip significantly.

I'm running a new Ryzen 9 3900X with an AMD WX2100 for my host OS (Linux, Ubuntu 19.04 w/ latest Mesa Drivers). I also do IOMMU passthrough with Linux and pass all cores to a Windows 10 VM. That Windows VM has a dedicated RX580 in it. It should be near native performance. I also tried on a Windows 10 laptop with intel integrated graphics and had the same slowness. I have not tried on a Mac yet. Windows (may) be locking the FPS to 30fps, with a TPS of 60. I need to do more testing.

The game at the current state makes a single core go to 100%, even on a 3900X, if I'm focused on the game engine with my mouse. I can limit the number of draw calls I make at my game.go level but I was hoping I could leverage some optimizations done in ebiten to write less complicated "when-to-draw" logic.

If you want to try the difference between ebiten and pixel with my code, here are the builds from Github Actions with the change #'s associated there:

Pixel: https://github.com/jrcichra/gollercoaster/releases/tag/45

Ebiten: https://github.com/jrcichra/gollercoaster/releases/tag/47

I think you'll find Pixel is faster with those optimizations.

FYI when you place a tile in Pixel, I rerender only the tiles Tx-1,Ty-1 to the front to satisfy the painter's method in a simple way. Placing a block closer to the front is faster than placing a block near 0,0. I could do something similar for Ebiten.

@hajimehoshi
Copy link
Owner

Thank you for the info!

but I was hoping I could leverage some optimizations done in ebiten to write less complicated "when-to-draw" logic.

So the issue is that Ebiten doesn't have a clear notion of 'render-only-when-necessary', whereas Pixels has one. In the Ebiten version of your code, all the tiles are rendered every frame, right?

I'd use an offscreen rendering. You can create an Ebiten image and use it as a render target. The offscreen should be updated only when necessary. Does this make sense?

@hajimehoshi
Copy link
Owner

FYI when you place a tile in Pixel, I rerender only the tiles Tx-1,Ty-1 to the front to satisfy the painter's method in a simple way. Placing a block closer to the front is faster than placing a block near 0,0. I could do something similar for Ebiten.

This sounds the similar way to what I suggested at #1130 (comment). Both (this way and the offscreen way) are fine :-)

@jrcichra
Copy link
Author

jrcichra commented Apr 5, 2020

Correct, I'm rendering every tile every frame, where every tile has a variable N sprites/images on it.

If I'm understanding, I'll make a global buffer := &ebiten.Image{} and have my game loop re-render to that buffer only when necessary (movement, clicking, etc).

Could I then always draw that buffer to the screen in update()? Is that a cheap operation? I'm new to graphics coding, been doing backend my whole life :-)

@hajimehoshi
Copy link
Owner

If I'm understanding, I'll make a global buffer := &ebiten.Image{} and have my game loop re-render to that buffer only when necessary (movement, clicking, etc).

You should use ebiten.NewImage to create an offscreen image. And yes, you have to update the offscreen buffer only when necessary.

Could I then always draw that buffer to the screen in update()? Is that a cheap operation? I'm new to graphics coding, been doing backend my whole life :-)

Yes, that's correct! This is generally cheap compared to the current way, since the number of draw calls would be much smaller.

@jrcichra
Copy link
Author

jrcichra commented Apr 6, 2020

Thanks @hajimehoshi ! I'm working on a prototype change and I'm already seeing vast performance increases. Once I have a few more optimizations in I'll post a diff and close this issue. Thanks!

@jrcichra
Copy link
Author

jrcichra commented Apr 6, 2020

Hmm, maybe I spoke too soon. While my optimizations are helping my framerate, I'm seeing frameskipping, even with no draw calls. I wrote this "sanity check program"

package main

import (
	"fmt"
	"time"

	"github.com/hajimehoshi/ebiten"
)

var frame int

func update(screen *ebiten.Image) error {
	if ebiten.IsDrawingSkipped() {
		// When the game is running slowly, the rendering result
		// will not be adopted.
		fmt.Printf("%s - WARNING: We skipped frame %d\n", time.Now().Format("2006-01-02 15:04:05"), frame)
		return nil
	}
	frame++
	return nil
}

func main() {
	if err := ebiten.Run(update, 1280, 720, 1, "Draw Nothing"); err != nil {
		panic(err)
	}
}

And get these results:

2020-04-06 17:45:47 - WARNING: We skipped frame 32
2020-04-06 17:45:47 - WARNING: We skipped frame 69
2020-04-06 17:45:48 - WARNING: We skipped frame 108
2020-04-06 17:45:49 - WARNING: We skipped frame 147
2020-04-06 17:45:49 - WARNING: We skipped frame 182
2020-04-06 17:45:50 - WARNING: We skipped frame 218
2020-04-06 17:45:50 - WARNING: We skipped frame 251
2020-04-06 17:45:51 - WARNING: We skipped frame 297
2020-04-06 17:45:52 - WARNING: We skipped frame 338
2020-04-06 17:45:53 - WARNING: We skipped frame 383
2020-04-06 17:45:53 - WARNING: We skipped frame 423
2020-04-06 17:45:54 - WARNING: We skipped frame 461
2020-04-06 17:45:55 - WARNING: We skipped frame 502
2020-04-06 17:45:55 - WARNING: We skipped frame 545
2020-04-06 17:45:56 - WARNING: We skipped frame 590
2020-04-06 17:45:57 - WARNING: We skipped frame 628
2020-04-06 17:45:57 - WARNING: We skipped frame 663
2020-04-06 17:45:58 - WARNING: We skipped frame 700

I also attached a profiler again:
file-1

This happens on the latest stable release of ebiten at time of writing, 1.10.5

@hajimehoshi
Copy link
Owner

This (about one skipping per second) is usual. Rendering happens based on the display's refresh rate, while updating happens based on the OS's timer, and there is a slight difference between them.

@silbinarywolf
Copy link
Sponsor Contributor

silbinarywolf commented Apr 8, 2020

@jrcichra I've noticed similar problems too. As Hajimehoshi mentioned, its a problem with the refresh rate / OS timer de-syncing. Here are some articles that might give you ideas on how to reduce stutter with heuristics.

https://medium.com/@alen.ladavac/the-elusive-frame-timing-168f899aec92
https://medium.com/@tglaiel/how-to-make-your-game-run-at-60fps-24c61210fe75

@jrcichra
Copy link
Author

jrcichra commented Apr 9, 2020

@silbinarywolf Wow, I didn't take frame timing into as much consideration as I should have. These are great resources. Thanks!

@hajimehoshi
Copy link
Owner

I think the cause and the solutions are clear now. Let me close this. Feel free to reopen when you find any other issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants