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 }