From 215ed5c84714517adc636e3d1ead7f8c47e33d33 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Mon, 19 Jul 2021 17:14:00 -0700 Subject: [PATCH] Stabilize Load Screen by Deferring SDL2 Calls * The loading screen for Edit and Play modes is stable and the risk of game crash is removed. The root cause was the setupAsync() functions running on a background goroutine, and running SDL2 draw functions while NOT on the main thread, which causes problems. * The fix is all SDL2 Texture draws become lazy loaded: when the main thread is presenting, any Wallpaper or ui.Image that has no texture yet gets one created at that time from the cached image.Image. * All internal game logic then uses image.Image types, to cache bitmaps of Level Chunks, Wallpaper images, Sprite icons, etc. and the game is free to prepare these asynchronously; only the main thread ever Presents and the SDL2 textures initialize on first appearance. * Several functions had arguments cleaned up: Canvas.LoadLevel() does not need the render.Engine as (e.g. wallpaper) textures don't render at that stage. --- pkg/editor_scene.go | 6 ++-- pkg/editor_ui_popups.go | 4 +-- pkg/main_scene.go | 2 +- pkg/menu_scene.go | 8 ++--- pkg/modal/loadscreen/loadscreen.go | 10 ++++-- pkg/play_scene.go | 6 ++-- pkg/sprites/sprites.go | 21 ++--------- pkg/story_scene.go | 2 +- pkg/uix/canvas.go | 6 ++-- pkg/uix/canvas_wallpaper.go | 56 +++++++++++------------------- pkg/wallpaper/wallpaper.go | 15 +++++--- 11 files changed, 58 insertions(+), 78 deletions(-) diff --git a/pkg/editor_scene.go b/pkg/editor_scene.go index cd06bd3..480de4f 100644 --- a/pkg/editor_scene.go +++ b/pkg/editor_scene.go @@ -101,7 +101,7 @@ func (s *EditorScene) setupAsync(d *Doodle) error { "Opening: "+s.Level.Title, "by "+s.Level.Author, ) - s.UI.Canvas.LoadLevel(d.Engine, s.Level) + s.UI.Canvas.LoadLevel(s.Level) s.UI.Canvas.InstallActors(s.Level.Actors) } else if s.filename != "" && s.OpenFile { log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename) @@ -132,7 +132,7 @@ func (s *EditorScene) setupAsync(d *Doodle) error { log.Debug("EditorScene.Setup: initializing a new Level") s.Level = level.New() s.Level.Palette = level.DefaultPalette() - s.UI.Canvas.LoadLevel(d.Engine, s.Level) + s.UI.Canvas.LoadLevel(s.Level) s.UI.Canvas.ScrollTo(render.Origin) s.UI.Canvas.Scrollable = true } @@ -360,7 +360,7 @@ func (s *EditorScene) LoadLevel(filename string) error { s.DrawingType = enum.LevelDrawing s.Level = level - s.UI.Canvas.LoadLevel(s.d.Engine, s.Level) + s.UI.Canvas.LoadLevel(s.Level) log.Info("Installing %d actors into the drawing", len(level.Actors)) if err := s.UI.Canvas.InstallActors(level.Actors); err != nil { diff --git a/pkg/editor_ui_popups.go b/pkg/editor_ui_popups.go index 0e18dc6..0e39b88 100644 --- a/pkg/editor_ui_popups.go +++ b/pkg/editor_ui_popups.go @@ -131,7 +131,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.LoadLevel(d.Engine, scene.Level) + u.Canvas.LoadLevel(scene.Level) }, OnCancel: func() { u.levelSettingsWindow.Hide() @@ -261,7 +261,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) { // Reload the level. if scene.Level != nil { log.Warn("RELOAD LEVEL") - u.Canvas.LoadLevel(d.Engine, scene.Level) + u.Canvas.LoadLevel(scene.Level) scene.Level.Chunker.Redraw() } else if scene.Doodad != nil { log.Warn("RELOAD DOODAD") diff --git a/pkg/main_scene.go b/pkg/main_scene.go index fca4bc3..b536042 100644 --- a/pkg/main_scene.go +++ b/pkg/main_scene.go @@ -244,7 +244,7 @@ func (s *MainScene) SetupDemoLevel(d *Doodle) error { // Title screen level to load. if lvl, err := level.LoadFile(balance.DemoLevelName); err == nil { - s.canvas.LoadLevel(d.Engine, lvl) + s.canvas.LoadLevel(lvl) s.canvas.InstallActors(lvl.Actors) // Load all actor scripts. diff --git a/pkg/menu_scene.go b/pkg/menu_scene.go index 43a159c..b2d8516 100644 --- a/pkg/menu_scene.go +++ b/pkg/menu_scene.go @@ -96,7 +96,7 @@ func (s *MenuScene) Setup(d *Doodle) error { W: d.width, H: d.height, }) - s.canvas.LoadLevel(d.Engine, &level.Level{ + s.canvas.LoadLevel(&level.Level{ Chunker: level.NewChunker(100), Palette: level.NewPalette(), PageType: level.Bounded, @@ -135,8 +135,8 @@ func (s *MenuScene) Setup(d *Doodle) error { // configureCanvas updates the settings of the background canvas, so a live // preview of the wallpaper and wrapping type can be shown. -func (s *MenuScene) configureCanvas(e render.Engine, pageType level.PageType, wallpaper string) { - s.canvas.LoadLevel(e, &level.Level{ +func (s *MenuScene) configureCanvas(pageType level.PageType, wallpaper string) { + s.canvas.LoadLevel(&level.Level{ Chunker: level.NewChunker(100), Palette: level.NewPalette(), PageType: pageType, @@ -151,7 +151,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.configureCanvas(d.Engine, pageType, wallpaper) + s.configureCanvas(pageType, wallpaper) }, OnCreateNewLevel: func(lvl *level.Level) { d.Goto(&EditorScene{ diff --git a/pkg/modal/loadscreen/loadscreen.go b/pkg/modal/loadscreen/loadscreen.go index 6a700f3..9ee6f3d 100644 --- a/pkg/modal/loadscreen/loadscreen.go +++ b/pkg/modal/loadscreen/loadscreen.go @@ -7,6 +7,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/level" + "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/shmem" "git.kirsle.net/apps/doodle/pkg/uix" "git.kirsle.net/go/render" @@ -108,7 +109,7 @@ func setup() { // Create the parent container that will stretch full screen. window = ui.NewFrame("Loadscreen Window") - window.SetBackground(render.RGBA(0, 0, 1, 40)) + window.SetBackground(render.RGBA(0, 0, 1, 40)) // makes the wallpaper darker? :/ // "Loading" text. label := ui.NewLabel(ui.Label{ @@ -167,7 +168,7 @@ func Loop(windowSize render.Rect, e render.Engine) { // Initialize the wallpaper canvas? if canvas == nil { canvas = uix.NewCanvas(128, false) - canvas.LoadLevel(e, &level.Level{ + canvas.LoadLevel(&level.Level{ Chunker: level.NewChunker(100), Palette: level.NewPalette(), PageType: level.Bounded, @@ -215,8 +216,13 @@ func Loop(windowSize render.Rect, e render.Engine) { // of chunks vs. chunks remaining to pre-cache bitmaps from. func PreloadAllChunkBitmaps(chunker *level.Chunker) { loadChunksTarget := len(chunker.Chunks) + // if loadChunksTarget == 0 { + // return + // } + for { remaining := chunker.PrerenderN(10) + log.Info("Remain: %d", remaining) // Set the load screen progress % based on number of chunks to render. if loadChunksTarget > 0 { diff --git a/pkg/play_scene.go b/pkg/play_scene.go index f787bbd..64ed4eb 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -171,7 +171,7 @@ func (s *PlayScene) setupAsync(d *Doodle) error { // Given a filename or map data to play? if s.Level != nil { log.Debug("PlayScene.Setup: received level from scene caller") - s.drawing.LoadLevel(d.Engine, s.Level) + s.drawing.LoadLevel(s.Level) s.drawing.InstallActors(s.Level.Actors) } else if s.Filename != "" { loadscreen.SetSubtitle("Opening: " + s.Filename) @@ -183,7 +183,7 @@ func (s *PlayScene) setupAsync(d *Doodle) error { if s.Level == nil { log.Debug("PlayScene.Setup: no grid given, initializing empty grid") s.Level = level.New() - s.drawing.LoadLevel(d.Engine, s.Level) + s.drawing.LoadLevel(s.Level) s.drawing.InstallActors(s.Level.Actors) } @@ -611,7 +611,7 @@ func (s *PlayScene) LoadLevel(filename string) error { } s.Level = level - s.drawing.LoadLevel(s.d.Engine, s.Level) + s.drawing.LoadLevel(s.Level) s.drawing.InstallActors(s.Level.Actors) return nil diff --git a/pkg/sprites/sprites.go b/pkg/sprites/sprites.go index 5ce9d66..8b5b6ee 100644 --- a/pkg/sprites/sprites.go +++ b/pkg/sprites/sprites.go @@ -29,12 +29,7 @@ func LoadImage(e render.Engine, filename string) (*ui.Image, error) { return nil, err } - tex, err := e.StoreTexture(filename, img) - if err != nil { - return nil, err - } - - return ui.ImageFromTexture(tex), nil + return ui.ImageFromImage(img) } // WASM: try the file over HTTP ajax request. @@ -49,12 +44,7 @@ func LoadImage(e render.Engine, filename string) (*ui.Image, error) { return nil, err } - tex, err := e.StoreTexture(filename, img) - if err != nil { - return nil, err - } - - return ui.ImageFromTexture(tex), nil + return ui.ImageFromImage(img) } // Then try the file system. @@ -71,12 +61,7 @@ func LoadImage(e render.Engine, filename string) (*ui.Image, error) { return nil, err } - tex, err := e.StoreTexture(filename, img) - if err != nil { - return nil, err - } - - return ui.ImageFromTexture(tex), nil + return ui.ImageFromImage(img) } return nil, errors.New("no such sprite found") diff --git a/pkg/story_scene.go b/pkg/story_scene.go index 35631a2..3fbede0 100644 --- a/pkg/story_scene.go +++ b/pkg/story_scene.go @@ -48,7 +48,7 @@ func (s *StoryScene) Setup(d *Doodle) error { // Set up the background wallpaper canvas. s.canvas = uix.NewCanvas(100, false) s.canvas.Resize(render.NewRect(d.width, d.height)) - s.canvas.LoadLevel(d.Engine, &level.Level{ + s.canvas.LoadLevel(&level.Level{ Chunker: level.NewChunker(100), Palette: level.NewPalette(), PageType: level.Bounded, diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index ec47ebf..2926336 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -145,7 +145,7 @@ func (w *Canvas) Load(p *level.Palette, g *level.Chunker) { } // LoadLevel initializes a Canvas from a Level object. -func (w *Canvas) LoadLevel(e render.Engine, level *level.Level) { +func (w *Canvas) LoadLevel(level *level.Level) { w.level = level w.Load(level.Palette, level.Chunker) @@ -159,14 +159,14 @@ func (w *Canvas) LoadLevel(e render.Engine, level *level.Level) { } } - wp, err := wallpaper.FromFile(e, filename, level) + wp, err := wallpaper.FromFile(filename, level) if err != nil { log.Error("wallpaper FromFile(%s): %s", filename, err) } w.wallpaper.maxWidth = level.MaxWidth w.wallpaper.maxHeight = level.MaxHeight - err = w.wallpaper.Load(e, level.PageType, wp) + err = w.wallpaper.Load(level.PageType, wp) if err != nil { log.Error("wallpaper Load: %s", err) } diff --git a/pkg/uix/canvas_wallpaper.go b/pkg/uix/canvas_wallpaper.go index 0115dde..174b751 100644 --- a/pkg/uix/canvas_wallpaper.go +++ b/pkg/uix/canvas_wallpaper.go @@ -11,16 +11,15 @@ type Wallpaper struct { pageType level.PageType maxWidth int64 maxHeight int64 - corner render.Texturer - top render.Texturer - left render.Texturer - repeat render.Texturer + + // Pointer to the Wallpaper datum. + WP *wallpaper.Wallpaper } // Valid returns whether the Wallpaper is configured. Only Levels should // have wallpapers and Doodads will have nil ones. func (wp *Wallpaper) Valid() bool { - return wp.repeat != nil + return wp.WP != nil && wp.WP.Repeat() != nil } // Canvas Loop() task that keeps mobile actors constrained inside the borders @@ -76,8 +75,8 @@ func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error { var ( wp = w.wallpaper S = w.Size() - size = wp.corner.Size() - sizeOrig = wp.corner.Size() + size = wp.WP.QuarterRect() + sizeOrig = wp.WP.QuarterRect() // Get the relative viewport of world coordinates looked at by the canvas. // The X,Y values are the negative Scroll value @@ -202,7 +201,9 @@ func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error { src.H = sizeOrig.H } - e.Copy(wp.repeat, src, dst) + if tex, err := wp.WP.RepeatTexture(e); err == nil { + e.Copy(tex, src, dst) + } } } @@ -227,7 +228,9 @@ func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error { } render.TrimBox(&src, &dst, p, S, w.BoxThickness(1)) - e.Copy(wp.left, src, dst) + if tex, err := wp.WP.LeftTexture(e); err == nil { + e.Copy(tex, src, dst) + } } // The top edge tiled along the top edge. @@ -250,7 +253,9 @@ func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error { } render.TrimBox(&src, &dst, p, S, w.BoxThickness(1)) - e.Copy(wp.top, src, dst) + if tex, err := wp.WP.TopTexture(e); err == nil { + e.Copy(tex, src, dst) + } } // The top left corner for all page types except Unbounded. @@ -273,38 +278,17 @@ func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error { } render.TrimBox(&src, &dst, p, S, w.BoxThickness(1)) - e.Copy(wp.corner, src, dst) + if tex, err := wp.WP.CornerTexture(e); err == nil { + e.Copy(tex, src, dst) + } } } return nil } // Load the wallpaper settings from a level. -func (wp *Wallpaper) Load(e render.Engine, pageType level.PageType, v *wallpaper.Wallpaper) error { +func (wp *Wallpaper) Load(pageType level.PageType, v *wallpaper.Wallpaper) error { wp.pageType = pageType - if tex, err := v.CornerTexture(e); err == nil { - wp.corner = tex - } else { - return err - } - - if tex, err := v.TopTexture(e); err == nil { - wp.top = tex - } else { - return err - } - - if tex, err := v.LeftTexture(e); err == nil { - wp.left = tex - } else { - return err - } - - if tex, err := v.RepeatTexture(e); err == nil { - wp.repeat = tex - } else { - return err - } - + wp.WP = v return nil } diff --git a/pkg/wallpaper/wallpaper.go b/pkg/wallpaper/wallpaper.go index e737be6..eb58c15 100644 --- a/pkg/wallpaper/wallpaper.go +++ b/pkg/wallpaper/wallpaper.go @@ -48,19 +48,19 @@ type Wallpaper struct { // FromImage creates a Wallpaper from an image.Image. // If the renger.Engine is nil it will compute images but not pre-cache any // textures yet. -func FromImage(e render.Engine, img *image.RGBA, name string) (*Wallpaper, error) { +func FromImage(img *image.RGBA, name string) (*Wallpaper, error) { wp := &Wallpaper{ Name: name, Image: img, } - wp.cache(e) + wp.cache() return wp, nil } // FromFile creates a Wallpaper from a file on disk. // If the renger.Engine is nil it will compute images but not pre-cache any // textures yet. -func FromFile(e render.Engine, filename string, embeddable filesystem.Embeddable) (*Wallpaper, error) { +func FromFile(filename string, embeddable filesystem.Embeddable) (*Wallpaper, error) { // Default object to return on errors. var defaultWP = &Wallpaper{ Name: strings.Split(filepath.Base(filename), ".")[0], @@ -118,12 +118,12 @@ func FromFile(e render.Engine, filename string, embeddable filesystem.Embeddable Image: rgba, ready: true, } - wp.cache(e) + wp.cache() return wp, nil } // cache the bitmap images. -func (wp *Wallpaper) cache(e render.Engine) { +func (wp *Wallpaper) cache() { // Zero-bound the rect cuz an image.Rect doesn't necessarily contain 0,0 var rect = wp.Image.Bounds() if rect.Min.X < 0 { @@ -164,6 +164,11 @@ func (wp *Wallpaper) QuarterSize() (int, int) { return wp.quarterWidth, wp.quarterHeight } +// QuarterRect returns a Rect of the size of the quarter images. +func (wp *Wallpaper) QuarterRect() render.Rect { + return render.NewRect(wp.QuarterSize()) +} + // Corner returns the top left corner image. func (wp *Wallpaper) Corner() *image.RGBA { return wp.corner