From db5760ee83f2d3e62a90f9a0b9f48e8c33fecb2b Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sat, 9 Apr 2022 14:41:24 -0700 Subject: [PATCH] Optimize memory by freeing up SDL2 textures * Added to the F3 Debug Overlay is a "Texture:" label that counts the number of textures currently loaded by the (SDL2) render engine. * Added Teardown() functions to Level, Doodad and the Chunker they both use to free up SDL2 textures for all their cached graphics. * The Canvas.Destroy() function now cleans up all textures that the Canvas is responsible for: calling the Teardown() of the Level or Doodad, calling Destroy() on all level actors, and cleaning up Wallpaper textures. * The Destroy() method of the game's various Scenes will properly Destroy() their canvases to clean up when transitioning to another scene. The MainScene, MenuScene, EditorScene and PlayScene. * Fix the sprites package to actually cache the ui.Image widgets. The game has very few sprites so no need to free them just yet. Some tricky places that were leaking textures have been cleaned up: * Canvas.InstallActors() destroys the canvases of existing actors before it reinitializes the list and installs the replacements. * The DraggableActor when the user is dragging an actor around their level cleans up the blueprint masked drag/drop actor before nulling it out. Misc changes: * The player character cheats during Play Mode will immediately swap out the player character on the current level. * Properly call the Close() function instead of Hide() to dismiss popup windows. The Close() function itself calls Hide() but also triggers WindowClose event handlers. The Doodad Dropper subscribes to its close event to free textures for all its doodad canvases. --- pkg/cheats.go | 13 +++++++++ pkg/doodads/doodad.go | 23 ++++++++++++++++ pkg/editor_scene.go | 4 +++ pkg/editor_ui.go | 11 ++++++-- pkg/editor_ui_doodad.go | 6 +++++ pkg/editor_ui_popups.go | 37 ++++++++++++------------- pkg/fps.go | 10 +++++++ pkg/level/chunk.go | 20 ++++++++++++++ pkg/level/chunker.go | 12 +++++++++ pkg/level/types.go | 20 ++++++++++++++ pkg/main_scene.go | 2 ++ pkg/menu_scene.go | 4 +++ pkg/modal/loadscreen/loadscreen.go | 1 + pkg/play_scene.go | 4 +++ pkg/sprites/sprites.go | 21 ++++++++++++--- pkg/uix/canvas.go | 34 +++++++++++++++++++---- pkg/uix/canvas_actors.go | 5 ++++ pkg/uix/canvas_editable.go | 6 ++--- pkg/wallpaper/texture.go | 43 ++++++++++++++++++++++++++++++ pkg/windows/doodad_dropper.go | 29 ++++++++++++++++++-- 20 files changed, 272 insertions(+), 33 deletions(-) diff --git a/pkg/cheats.go b/pkg/cheats.go index efdce7c..535be43 100644 --- a/pkg/cheats.go +++ b/pkg/cheats.go @@ -19,6 +19,9 @@ func (c Command) cheatCommand(d *Doodle) bool { // Some cheats only work in Play Mode. playScene, isPlay := d.Scene.(*PlayScene) + // If a character cheat is used during Play Mode, replace the player NOW. + var setPlayerCharacter bool + // Cheat codes switch c.Raw { case balance.CheatUncapFPS: @@ -117,22 +120,27 @@ func (c Command) cheatCommand(d *Doodle) bool { case balance.CheatPlayAsBird: balance.PlayerCharacterDoodad = "bird-red.doodad" + setPlayerCharacter = true d.Flash("Set default player character to Bird (red)") case balance.CheatPlayAsBoy: balance.PlayerCharacterDoodad = "boy.doodad" + setPlayerCharacter = true d.Flash("Set default player character to Boy") case balance.CheatPlayAsAzuBlue: balance.PlayerCharacterDoodad = "azu-blu.doodad" + setPlayerCharacter = true d.Flash("Set default player character to Blue Azulian") case balance.CheatPlayAsThief: balance.PlayerCharacterDoodad = "thief.doodad" + setPlayerCharacter = true d.Flash("Set default player character to Thief") case balance.CheatPlayAsAnvil: balance.PlayerCharacterDoodad = "anvil.doodad" + setPlayerCharacter = true d.Flash("Set default player character to the Anvil") case balance.CheatGodMode: @@ -173,5 +181,10 @@ func (c Command) cheatCommand(d *Doodle) bool { return false } + // If we're setting the player character and in Play Mode, do it. + if setPlayerCharacter && isPlay { + playScene.SetPlayerCharacter(balance.PlayerCharacterDoodad) + } + return true } diff --git a/pkg/doodads/doodad.go b/pkg/doodads/doodad.go index b25906c..6e3cb72 100644 --- a/pkg/doodads/doodad.go +++ b/pkg/doodads/doodad.go @@ -52,6 +52,29 @@ func New(size int) *Doodad { } } +// Teardown cleans up texture cache memory when the doodad is no longer needed by the game. +func (d *Doodad) Teardown() { + var ( + chunks int + textures int + ) + + for _, layer := range d.Layers { + for coord := range layer.Chunker.IterChunks() { + if chunk, ok := layer.Chunker.GetChunk(coord); ok { + freed := chunk.Teardown() + chunks++ + textures += freed + } + } + } + + // Debug log if any textures were actually freed. + if textures > 0 { + log.Debug("Teardown doodad (%s): Freed %d textures across %d chunks", d.Title, textures, chunks) + } +} + // Tag gets a value from the doodad's tags. func (d *Doodad) Tag(name string) string { if v, ok := d.Tags[name]; ok { diff --git a/pkg/editor_scene.go b/pkg/editor_scene.go index 19ba82a..dc75f2b 100644 --- a/pkg/editor_scene.go +++ b/pkg/editor_scene.go @@ -584,5 +584,9 @@ func (s *EditorScene) SaveDoodad(filename string) error { // Destroy the scene. func (s *EditorScene) Destroy() error { + // Free SDL2 textures. Note: if they are switching to the Editor, the chunks still have + // their bitmaps cached and will regen the textures as needed. + s.UI.Teardown() + return nil } diff --git a/pkg/editor_ui.go b/pkg/editor_ui.go index e33a543..4571ed5 100644 --- a/pkg/editor_ui.go +++ b/pkg/editor_ui.go @@ -147,6 +147,12 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI { return u } +// Teardown the UI manager and free all the SDL2 textures under its control. +func (u *EditorUI) Teardown() { + log.Debug("EditorUI.Teardown()") + u.Canvas.Destroy() +} + // FinishSetup runs the Setup tasks that must be postponed til the end, such // as rendering the Palette window so that it can accurately show the palette // loaded from a level. @@ -396,10 +402,10 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas { } // Handle the Canvas deleting our actors in edit mode. - drawing.OnDeleteActors = func(actors []*level.Actor) { + drawing.OnDeleteActors = func(actors []*uix.Actor) { if u.Scene.Level != nil { for _, actor := range actors { - u.Scene.Level.Actors.Remove(actor) + u.Scene.Level.Actors.Remove(actor.Actor) } u.Scene.Level.PruneLinks() drawing.InstallActors(u.Scene.Level.Actors) @@ -446,6 +452,7 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas { // The actor has been dropped so null it out. defer func() { + u.DraggableActor.Teardown() u.DraggableActor = nil }() diff --git a/pkg/editor_ui_doodad.go b/pkg/editor_ui_doodad.go index f3223da..8193c24 100644 --- a/pkg/editor_ui_doodad.go +++ b/pkg/editor_ui_doodad.go @@ -20,6 +20,12 @@ type DraggableActor struct { actor *level.Actor // if a level actor } +// Teardown the DraggableActor and free its textures. +func (da *DraggableActor) Teardown() { + log.Debug("Teardown DraggableActor") + da.canvas.Destroy() +} + // startDragActor begins the drag event for a Doodad onto a level. // actor may be nil (if you drag a new doodad from the palette) or otherwise // is an existing actor from the level. diff --git a/pkg/editor_ui_popups.go b/pkg/editor_ui_popups.go index 9744ba2..39a8b70 100644 --- a/pkg/editor_ui_popups.go +++ b/pkg/editor_ui_popups.go @@ -27,7 +27,7 @@ Functions to manage popup windows in the Editor Mode, such as: // Opens the "Layers" window (for editing doodads) func (u *EditorUI) OpenLayersWindow() { - u.layersWindow.Hide() + u.layersWindow.Close() u.layersWindow = nil u.SetupPopups(u.d) u.layersWindow.Show() @@ -36,7 +36,7 @@ func (u *EditorUI) OpenLayersWindow() { // OpenPaletteWindow opens the Palette Editor window. func (u *EditorUI) OpenPaletteWindow() { // TODO: recompute the window so the actual loaded level palette gets in - u.paletteEditor.Hide() + u.paletteEditor.Close() u.paletteEditor = nil u.SetupPopups(u.d) u.paletteEditor.Show() @@ -83,12 +83,12 @@ func (u *EditorUI) OpenPublishWindow() { u.d.Flash("Saved level: %s", u.Scene.filename) }, OnCancel: func() { - u.publishWindow.Hide() + u.publishWindow.Close() }, }) u.ConfigureWindow(u.d, u.publishWindow) - u.publishWindow.Hide() + u.publishWindow.Close() // u.publishWindow = nil u.SetupPopups(u.d) u.publishWindow.Show() @@ -96,7 +96,7 @@ func (u *EditorUI) OpenPublishWindow() { // OpenPublishWindow opens the FileSystem window. func (u *EditorUI) OpenFileSystemWindow() { - u.filesystemWindow.Hide() + u.filesystemWindow.Close() u.filesystemWindow = nil u.SetupPopups(u.d) u.filesystemWindow.Show() @@ -115,7 +115,7 @@ func (u *EditorUI) ConfigureWindow(d *Doodle, window *ui.Window) { Y: (d.height / 2) - (size.H / 2), }) - window.Hide() + window.Close() } // SetupPopups preloads popup windows like the DoodadDropper. @@ -126,19 +126,19 @@ func (u *EditorUI) SetupPopups(d *Doodle) { Supervisor: u.Supervisor, Engine: d.Engine, OnCancel: func() { - u.licenseWindow.Hide() + u.licenseWindow.Close() }, } cfg.OnLicensed = func() { // License status has changed, reload the window! if u.licenseWindow != nil { - u.licenseWindow.Hide() + u.licenseWindow.Close() } u.licenseWindow = windows.MakeLicenseWindow(d.width, d.height, cfg) } cfg.OnLicensed() - u.licenseWindow.Hide() + u.licenseWindow.Close() } // Doodad Dropper. @@ -149,7 +149,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) { OnStartDragActor: u.startDragActor, OnCancel: func() { - u.doodadWindow.Hide() + u.doodadWindow.Close() }, }) u.ConfigureWindow(d, u.doodadWindow) @@ -185,6 +185,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) { log.Info("OnChangePageTypeAndWallpaper called: %+v, %+v", pageType, wallpaper) scene.Level.PageType = pageType scene.Level.Wallpaper = wallpaper + u.Canvas.Destroy() // clean up old textures u.Canvas.LoadLevel(scene.Level) }, OnReload: func() { @@ -192,7 +193,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) { scene.Reset() }, OnCancel: func() { - u.levelSettingsWindow.Hide() + u.levelSettingsWindow.Close() }, }) u.ConfigureWindow(d, u.levelSettingsWindow) @@ -210,7 +211,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) { // Rebuild the window. TODO: hacky af. cfg.OnRefresh = func() { - u.doodadPropertiesWindow.Hide() + u.doodadPropertiesWindow.Close() u.doodadPropertiesWindow = nil u.SetupPopups(u.d) u.doodadPropertiesWindow.Show() @@ -262,7 +263,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) { return true }, OnCancel: func() { - u.filesystemWindow.Hide() + u.filesystemWindow.Close() }, }) u.ConfigureWindow(d, u.filesystemWindow) @@ -313,13 +314,13 @@ func (u *EditorUI) SetupPopups(d *Doodle) { log.Info("Added new palette color: %+v", sw) // Awkward but... reload this very same window. - u.paletteEditor.Hide() + u.paletteEditor.Close() u.paletteEditor = nil u.SetupPopups(d) u.paletteEditor.Show() }, OnCancel: func() { - u.paletteEditor.Hide() + u.paletteEditor.Close() }, }) u.ConfigureWindow(d, u.paletteEditor) @@ -347,7 +348,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) { // Awkward but... reload this very same window. // Otherwise, the window doesn't update to show the new // layer having been added. - u.layersWindow.Hide() + u.layersWindow.Close() u.layersWindow = nil u.SetupPopups(d) u.layersWindow.Show() @@ -364,7 +365,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) { // Awkward but... reload this very same window. // Otherwise, the window doesn't update to show the new // layer having been added. - u.layersWindow.Hide() + u.layersWindow.Close() u.layersWindow = nil u.SetupPopups(d) u.layersWindow.Show() @@ -380,7 +381,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) { u.Scene.ActiveLayer = index }, OnCancel: func() { - u.layersWindow.Hide() + u.layersWindow.Close() }, }) u.ConfigureWindow(d, u.layersWindow) diff --git a/pkg/fps.go b/pkg/fps.go index a62ce22..37f51cd 100644 --- a/pkg/fps.go +++ b/pkg/fps.go @@ -9,6 +9,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/drawtool" "git.kirsle.net/apps/doodle/pkg/uix" "git.kirsle.net/go/render" + "git.kirsle.net/go/render/sdl" "git.kirsle.net/go/ui" ) @@ -57,6 +58,13 @@ func (d *Doodle) DrawDebugOverlay() { framesSkipped = "uncapped" } + // Get the size of cached SDL2 textures at the render engine level. + var texCount = "n/a" + if sdl, ok := d.Engine.(*sdl.Renderer); ok { + gotex, sdltex := sdl.CountTextures() + texCount = fmt.Sprintf("%d img, %d sdl", gotex, sdltex) + } + var ( darken = balance.DebugStrokeDarken Yoffset = 20 // leave room for the menu bar @@ -65,11 +73,13 @@ func (d *Doodle) DrawDebugOverlay() { "FPS:", "Scene:", "Mouse:", + "Textures:", } values = []string{ fmt.Sprintf("%d %s", fpsCurrent, framesSkipped), d.Scene.Name(), fmt.Sprintf("%d,%d", d.event.CursorX, d.event.CursorY), + texCount, } ) diff --git a/pkg/level/chunk.go b/pkg/level/chunk.go index 22a22c6..799f3e6 100644 --- a/pkg/level/chunk.go +++ b/pkg/level/chunk.go @@ -215,6 +215,26 @@ func (c *Chunk) ToBitmap(mask render.Color) image.Image { return img } +// Teardown the chunk and free (SDL2) texture memory in ways Go can not by itself. +// Returns the number of textures freed. +func (c *Chunk) Teardown() int { + var freed int + + if c.texture != nil { + c.texture.Free() + c.texture = nil + freed++ + } + + if c.textureMasked != nil { + c.textureMasked.Free() + c.textureMasked = nil + freed++ + } + + return freed +} + // Set proxies to the accessor and flags the texture as dirty. func (c *Chunk) Set(p render.Point, sw *Swatch) error { c.dirty = true diff --git a/pkg/level/chunker.go b/pkg/level/chunker.go index 9408003..b4a8ec6 100644 --- a/pkg/level/chunker.go +++ b/pkg/level/chunker.go @@ -68,6 +68,18 @@ func (c *Chunker) IterViewport(viewport render.Rect) <-chan Pixel { return pipe } +// IterChunks returns a channel to iterate over all chunks in the drawing. +func (c *Chunker) IterChunks() <-chan render.Point { + pipe := make(chan render.Point) + go func() { + for point := range c.Chunks { + pipe <- point + } + close(pipe) + }() + return pipe +} + // IterViewportChunks returns a channel to iterate over the Chunk objects that // appear within the viewport rect, instead of the pixels in each chunk. func (c *Chunker) IterViewportChunks(viewport render.Rect) <-chan render.Point { diff --git a/pkg/level/types.go b/pkg/level/types.go index 72876f5..87a64c4 100644 --- a/pkg/level/types.go +++ b/pkg/level/types.go @@ -8,6 +8,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/drawtool" "git.kirsle.net/apps/doodle/pkg/enum" + "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/go/render" ) @@ -86,6 +87,25 @@ func New() *Level { } } +// Teardown the level when the game is done with it. This frees up SDL2 cached +// texture chunks and reclaims memory in ways the Go garbage collector can not. +func (m *Level) Teardown() { + var ( + chunks int + textures int + ) + + for coord := range m.Chunker.IterChunks() { + if chunk, ok := m.Chunker.GetChunk(coord); ok { + freed := chunk.Teardown() + chunks++ + textures += freed + } + } + + log.Debug("Teardown level (%s): Freed %d textures across %d level chunks", m.Title, textures, chunks) +} + // Pixel associates a coordinate with a palette index. type Pixel struct { X int `json:"x"` diff --git a/pkg/main_scene.go b/pkg/main_scene.go index 28173d0..a25b6f9 100644 --- a/pkg/main_scene.go +++ b/pkg/main_scene.go @@ -648,5 +648,7 @@ func (s *MainScene) Draw(d *Doodle) error { // Destroy the scene. func (s *MainScene) Destroy() error { + log.Debug("MainScene.Destroy(): clean up the demo level canvas") + s.canvas.Destroy() return nil } diff --git a/pkg/menu_scene.go b/pkg/menu_scene.go index 19d469f..75a5932 100644 --- a/pkg/menu_scene.go +++ b/pkg/menu_scene.go @@ -154,6 +154,7 @@ func (s *MenuScene) setupNewWindow(d *Doodle) error { Engine: d.Engine, OnChangePageTypeAndWallpaper: func(pageType level.PageType, wallpaper string) { log.Info("OnChangePageTypeAndWallpaper called: %+v, %+v", pageType, wallpaper) + s.canvas.Destroy() // clean up old textures s.configureCanvas(pageType, wallpaper) }, OnCreateNewLevel: func(lvl *level.Level) { @@ -238,5 +239,8 @@ func (s *MenuScene) Draw(d *Doodle) error { // Destroy the scene. func (s *MenuScene) Destroy() error { + // Free (wallpaper) textures. + s.canvas.Destroy() + return nil } diff --git a/pkg/modal/loadscreen/loadscreen.go b/pkg/modal/loadscreen/loadscreen.go index 4d534c0..7ac1676 100644 --- a/pkg/modal/loadscreen/loadscreen.go +++ b/pkg/modal/loadscreen/loadscreen.go @@ -88,6 +88,7 @@ func Resized() { // Hide the loading screen. func Hide() { + canvas.Destroy() // cleanup wallpaper textures visible = false } diff --git a/pkg/play_scene.go b/pkg/play_scene.go index f757a7e..33b851d 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -908,5 +908,9 @@ func (s *PlayScene) LoadLevel(filename string) error { // Destroy the scene. func (s *PlayScene) Destroy() error { + // Free SDL2 textures. Note: if they are switching to the Editor, the chunks still have + // their bitmaps cached and will regen the textures as needed. + s.drawing.Destroy() + return nil } diff --git a/pkg/sprites/sprites.go b/pkg/sprites/sprites.go index 28853c7..f51483e 100644 --- a/pkg/sprites/sprites.go +++ b/pkg/sprites/sprites.go @@ -47,7 +47,12 @@ func LoadImage(e render.Engine, filename string) (*ui.Image, error) { return nil, err } - return ui.ImageFromImage(img) + if image, err := ui.ImageFromImage(img); err == nil { + cache[filename] = image + return image, nil + } else { + return nil, err + } } // WASM: try the file over HTTP ajax request. @@ -62,7 +67,12 @@ func LoadImage(e render.Engine, filename string) (*ui.Image, error) { return nil, err } - return ui.ImageFromImage(img) + if image, err := ui.ImageFromImage(img); err == nil { + cache[filename] = image + return image, nil + } else { + return nil, err + } } // Then try the file system. @@ -79,7 +89,12 @@ func LoadImage(e render.Engine, filename string) (*ui.Image, error) { return nil, err } - return ui.ImageFromImage(img) + if image, err := ui.ImageFromImage(img); err == nil { + cache[filename] = image + return image, nil + } else { + return nil, err + } } return nil, errors.New("no such sprite found") diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index 2680699..8e61dfe 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -79,7 +79,7 @@ type Canvas struct { // When the Canvas wants to delete Actors, but ultimately it is upstream // that controls the actors. Upstream should delete them and then reinstall // the actor list from scratch. - OnDeleteActors func([]*level.Actor) + OnDeleteActors func([]*Actor) OnDragStart func(*level.Actor) // -- WHEN Canvas.Tool is "Link" -- @@ -150,10 +150,34 @@ func NewCanvas(size int, editable bool) *Canvas { return w } -// Destroy the canvas. -// -// TODO: Not implemented, here to satisfy ui.Widget, should free up textures tho. -func (w *Canvas) Destroy() {} +/* +Destroy the canvas. + +This function satisfies the ui.Widget interface but it also calls Teardown() methods +on the level or doodad as well as any level actors, which frees up SDL2 texture memory. + +Note: the rest of the data can be garbage collected by Go normally, the textures are +able to regenerate themselves again if needed. +*/ +func (w *Canvas) Destroy() { + if w.level != nil { + w.level.Teardown() + } + + if w.doodad != nil { + w.doodad.Teardown() + } + + for _, actor := range w.actors { + actor.Canvas.Destroy() + } + + if w.wallpaper.WP != nil { + if freed := w.wallpaper.WP.Free(); freed > 0 { + log.Debug("%s.Destroy(): freed %d wallpaper textures", w, freed) + } + } +} // Load initializes the Canvas using an existing Palette and Grid. func (w *Canvas) Load(p *level.Palette, g *level.Chunker) { diff --git a/pkg/uix/canvas_actors.go b/pkg/uix/canvas_actors.go index 8b4d87a..a0ed890 100644 --- a/pkg/uix/canvas_actors.go +++ b/pkg/uix/canvas_actors.go @@ -27,6 +27,11 @@ func (w *Canvas) InstallActors(actors level.ActorMap) error { } sort.Strings(actorIDs) + // In case we are replacing the actors, free up all their textures first! + for _, actor := range w.actors { + actor.Canvas.Destroy() + } + w.actors = make([]*Actor, 0) for _, id := range actorIDs { var actor = actors[id] diff --git a/pkg/uix/canvas_editable.go b/pkg/uix/canvas_editable.go index cb7d006..c08309b 100644 --- a/pkg/uix/canvas_editable.go +++ b/pkg/uix/canvas_editable.go @@ -460,7 +460,7 @@ func (w *Canvas) loopEditable(ev *event.State) error { // log.Debug("ActorTool, cursor=%s WP=%s zoom=%d P=%s", cursor, WP, w.Zoom, ui.AbsolutePosition(w)) - var deleteActors = []*level.Actor{} + var deleteActors = []*Actor{} for _, actor := range w.actors { // Compute the bounding box on screen where this doodad @@ -493,13 +493,13 @@ func (w *Canvas) loopEditable(ev *event.State) error { if keybind.LeftClick(ev) { // Pop this canvas out for the drag/drop. if w.OnDragStart != nil { - deleteActors = append(deleteActors, actor.Actor) + deleteActors = append(deleteActors, actor) w.OnDragStart(actor.Actor) } break } else if ev.Button3 { // Right click to delete an actor. - deleteActors = append(deleteActors, actor.Actor) + deleteActors = append(deleteActors, actor) } } else { actor.Canvas.SetBorderSize(0) diff --git a/pkg/wallpaper/texture.go b/pkg/wallpaper/texture.go index 32706a4..e41471c 100644 --- a/pkg/wallpaper/texture.go +++ b/pkg/wallpaper/texture.go @@ -11,6 +11,49 @@ import ( "git.kirsle.net/go/render" ) +/* +Free all SDL2 textures from memory. + +The Canvas widget will free wallpaper textures in its Destroy method. Note that +if the wallpaper was still somehow in use, the textures will be regenerated the +next time a method like CornerTexture() asks for one. + +Returns the number of textures freed (up to 4) or -1 if wallpaper was not ready. +*/ +func (wp *Wallpaper) Free() int { + if !wp.ready { + return -1 + } + + var freed int + + if wp.tex.corner != nil { + wp.tex.corner.Free() + wp.tex.corner = nil + freed++ + } + + if wp.tex.top != nil { + wp.tex.top.Free() + wp.tex.top = nil + freed++ + } + + if wp.tex.left != nil { + wp.tex.left.Free() + wp.tex.left = nil + freed++ + } + + if wp.tex.repeat != nil { + wp.tex.repeat.Free() + wp.tex.repeat = nil + freed++ + } + + return freed +} + // CornerTexture returns the Texture. func (wp *Wallpaper) CornerTexture(e render.Engine) (render.Texturer, error) { if !wp.ready { diff --git a/pkg/windows/doodad_dropper.go b/pkg/windows/doodad_dropper.go index 21b9e05..4b3502b 100644 --- a/pkg/windows/doodad_dropper.go +++ b/pkg/windows/doodad_dropper.go @@ -41,6 +41,11 @@ func NewDoodadDropper(config DoodadDropper) *ui.Window { // size of the doodad window width = buttonSize * columns height = (buttonSize * rows) + 64 // account for button borders :( + + // Collect the Canvas widgets that all the doodads will be drawn in. + // When the doodad window is closed or torn down, we can free up + // the SDL2 textures and avoid a slow memory leak. + canvases = []*uix.Canvas{} ) // Get all the doodads. @@ -79,6 +84,17 @@ func NewDoodadDropper(config DoodadDropper) *ui.Window { Background: render.Grey, }) + // When the window is closed, clear canvas textures. Note: we still cache + // bitmap images in memory, those would be garbage collected by Go and SDL2 + // textures can always be regenerated. + window.Handle(ui.CloseWindow, func(ed ui.EventData) error { + log.Debug("Doodad Dropper: window closed, free %d canvas textures", len(canvases)) + for _, can := range canvases { + can.Destroy() + } + return nil + }) + tabFrame := ui.NewTabFrame("Category Tabs") window.Pack(tabFrame, ui.Pack{ Side: ui.N, @@ -103,7 +119,8 @@ func NewDoodadDropper(config DoodadDropper) *ui.Window { Text: category.Name, Font: balance.TabFont, })) - makeDoodadTab(config, tab1, render.NewRect(width-4, height-60), category.ID, items) + cans := makeDoodadTab(config, tab1, render.NewRect(width-4, height-60), category.ID, items) + canvases = append(canvases, cans...) } tabFrame.Supervise(config.Supervisor) @@ -113,7 +130,7 @@ func NewDoodadDropper(config DoodadDropper) *ui.Window { } // Function to generate the TabFrame frame of the Doodads window. -func makeDoodadTab(config DoodadDropper, frame *ui.Frame, size render.Rect, category string, available []*doodads.Doodad) { +func makeDoodadTab(config DoodadDropper, frame *ui.Frame, size render.Rect, category string, available []*doodads.Doodad) []*uix.Canvas { var ( buttonSize = balance.DoodadButtonSize columns = balance.DoodadDropperCols @@ -128,6 +145,9 @@ func makeDoodadTab(config DoodadDropper, frame *ui.Frame, size render.Rect, cate pages int perPage = 20 maxPageButtons = 10 + + // Collect the created Canvas widgets so we can free SDL2 textures later. + canvases = []*uix.Canvas{} ) frame.Resize(size) @@ -238,6 +258,9 @@ func makeDoodadTab(config DoodadDropper, frame *ui.Frame, size render.Rect, cate can.SetBackground(balance.DoodadButtonBackground) can.LoadDoodad(doodad) + // Keep the canvas to free textures later. + canvases = append(canvases, can) + btn := ui.NewButton(doodad.Title, can) btn.Resize(render.NewRect( buttonSize-2, // TODO: without the -2 the button border @@ -404,4 +427,6 @@ func makeDoodadTab(config DoodadDropper, frame *ui.Frame, size render.Rect, cate }) } } + + return canvases }