faiface / pixel

A hand-crafted 2D game library in Go
MIT License
4.46k stars 245 forks source link

Added an opportunity to disable caching for sprites #297

Closed zergon321 closed 2 years ago

zergon321 commented 3 years ago

Recently I attempted to create a video player in Go. I decoded video playback with Reisen and tried to play each frame using sprite.Set() and then sprite.Draw() but I encountered an issue with RAM consumption. Pixel saves each picture the sprite receives through sprite.Set(). When we play a video, there gonna be a lot of different pictures going to the sprite. But there's no need for them after they were played on the screen.

That's why I think there's no need to make all the sprites able to cache incoming pictures. I implemented Cached parameter for Drawer and SetCached() method for Sprite so the user can disable picture caching for the sprite for a period of time or forever.

Here's a snippet of code that produces random images and passes them through a channel to the renderer for them to be displayed on the screen. Launch the terminal, execute top command, then run the code and see how the application process depletes your RAM.

Source code ```go package main import ( "crypto/rand" "fmt" "image" "image/color" "runtime" "time" "github.com/faiface/pixel" "github.com/faiface/pixel/pixelgl" colors "golang.org/x/image/colornames" ) const ( width = 1280 height = 720 frameBufferSize = 128 ) func pixToPictureData(pixels []byte, width, height int) *pixel.PictureData { picData := pixel.MakePictureData(pixel. R(0, 0, float64(width), float64(height))) for y := height - 1; y >= 0; y-- { for x := 0; x < width; x++ { picData.Pix[(height-y-1)*width+x].R = pixels[y*width*4+x*4+0] picData.Pix[(height-y-1)*width+x].G = pixels[y*width*4+x*4+1] picData.Pix[(height-y-1)*width+x].B = pixels[y*width*4+x*4+2] picData.Pix[(height-y-1)*width+x].A = pixels[y*width*4+x*4+3] } } return picData } func readVideoFrames( filename string, ) ( <-chan *pixel.PictureData, chan error, error, ) { frameBuffer := make(chan *pixel.PictureData, frameBufferSize) errs := make(chan error) go func(frameBuffer chan *pixel.PictureData, errs chan error) { for { upLeft := image.Point{0, 0} lowRight := image.Point{width, height} img := image.NewNRGBA(image. Rectangle{upLeft, lowRight}) for i := 0; i < width; i++ { for j := 0; j < height; j++ { buf := make([]byte, 4) _, err := rand.Read(buf) if err != nil { go func(err error) { errs <- err }(err) } pixelColor := color.RGBA{ R: buf[0], G: buf[1], B: buf[2], A: buf[3], } img.Set(i, j, pixelColor) } } pic := pixToPictureData(img.Pix, width, height) frameBuffer <- pic } }(frameBuffer, errs) return frameBuffer, errs, nil } func run() { fname := "demo.mp4" // Create a new window. cfg := pixelgl.WindowConfig{ Title: "Video demo", Bounds: pixel.R(0, 0, width, height), } win, err := pixelgl.NewWindow(cfg) handleError(err) videoFPS := 30 handleError(err) spf := 1.0 / float64(videoFPS) frameDuration, err := time. ParseDuration(fmt.Sprintf("%fs", spf)) handleError(err) frameBuffer, errs, err := readVideoFrames( fname) handleError(err) ticker := time.Tick(frameDuration) tr := pixel.IM.Moved(pixel.V(width/2, height/2)) videoSprite := pixel.NewSprite(nil, pixel.R(0, 0, width, height)) // Setup metrics. last := time.Now() fps := 0 perSecond := time.Tick(time.Second) i := 0 k := 0 var ram runtime.MemStats for !win.Closed() { deltaTime := time.Since(last).Seconds() last = time.Now() select { case err, ok := <-errs: if ok { fmt.Println( "error occurred while reading video frames:", err) } default: } select { case <-ticker: frame, ok := <-frameBuffer if ok { videoSprite.Set(frame, frame.Rect) i++ k++ } default: } win.Clear(colors.White) videoSprite.Draw(win, tr) win.Update() fps++ select { case <-perSecond: runtime.ReadMemStats(&ram) win.SetTitle(fmt.Sprintf("%s | FPS: %d | dt: %f | Frames: %d | Video FPS: %d | RAM: %d", cfg.Title, fps, deltaTime, i, k, ram.Alloc)) fps = 0 k = 0 default: } } } func main() { pixelgl.Run(run) } func handleError(err error) { if err != nil { panic(err) } } ```

But if you add a videoSprite.SetCached(false) line right after the sprite creation fragment, the RAM consumption disappears and the amount of memory taken by the application stay approximately the same. This addition is really useful.

bencarrr commented 2 years ago

LGTM :+1:

dusk125 commented 2 years ago

Yeah looks good, looks like everything is still cached by default and can be disabled if need-be; merging!