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.
This commit is contained in:
Noah 2021-07-19 17:14:00 -07:00
parent 2d1b926e4f
commit 215ed5c847
11 changed files with 58 additions and 78 deletions

View File

@ -101,7 +101,7 @@ func (s *EditorScene) setupAsync(d *Doodle) error {
"Opening: "+s.Level.Title, "Opening: "+s.Level.Title,
"by "+s.Level.Author, "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) s.UI.Canvas.InstallActors(s.Level.Actors)
} else if s.filename != "" && s.OpenFile { } else if s.filename != "" && s.OpenFile {
log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename) 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") log.Debug("EditorScene.Setup: initializing a new Level")
s.Level = level.New() s.Level = level.New()
s.Level.Palette = level.DefaultPalette() 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.ScrollTo(render.Origin)
s.UI.Canvas.Scrollable = true s.UI.Canvas.Scrollable = true
} }
@ -360,7 +360,7 @@ func (s *EditorScene) LoadLevel(filename string) error {
s.DrawingType = enum.LevelDrawing s.DrawingType = enum.LevelDrawing
s.Level = level 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)) log.Info("Installing %d actors into the drawing", len(level.Actors))
if err := s.UI.Canvas.InstallActors(level.Actors); err != nil { if err := s.UI.Canvas.InstallActors(level.Actors); err != nil {

View File

@ -131,7 +131,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) {
log.Info("OnChangePageTypeAndWallpaper called: %+v, %+v", pageType, wallpaper) log.Info("OnChangePageTypeAndWallpaper called: %+v, %+v", pageType, wallpaper)
scene.Level.PageType = pageType scene.Level.PageType = pageType
scene.Level.Wallpaper = wallpaper scene.Level.Wallpaper = wallpaper
u.Canvas.LoadLevel(d.Engine, scene.Level) u.Canvas.LoadLevel(scene.Level)
}, },
OnCancel: func() { OnCancel: func() {
u.levelSettingsWindow.Hide() u.levelSettingsWindow.Hide()
@ -261,7 +261,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) {
// Reload the level. // Reload the level.
if scene.Level != nil { if scene.Level != nil {
log.Warn("RELOAD LEVEL") log.Warn("RELOAD LEVEL")
u.Canvas.LoadLevel(d.Engine, scene.Level) u.Canvas.LoadLevel(scene.Level)
scene.Level.Chunker.Redraw() scene.Level.Chunker.Redraw()
} else if scene.Doodad != nil { } else if scene.Doodad != nil {
log.Warn("RELOAD DOODAD") log.Warn("RELOAD DOODAD")

View File

@ -244,7 +244,7 @@ func (s *MainScene) SetupDemoLevel(d *Doodle) error {
// Title screen level to load. // Title screen level to load.
if lvl, err := level.LoadFile(balance.DemoLevelName); err == nil { if lvl, err := level.LoadFile(balance.DemoLevelName); err == nil {
s.canvas.LoadLevel(d.Engine, lvl) s.canvas.LoadLevel(lvl)
s.canvas.InstallActors(lvl.Actors) s.canvas.InstallActors(lvl.Actors)
// Load all actor scripts. // Load all actor scripts.

View File

@ -96,7 +96,7 @@ func (s *MenuScene) Setup(d *Doodle) error {
W: d.width, W: d.width,
H: d.height, H: d.height,
}) })
s.canvas.LoadLevel(d.Engine, &level.Level{ s.canvas.LoadLevel(&level.Level{
Chunker: level.NewChunker(100), Chunker: level.NewChunker(100),
Palette: level.NewPalette(), Palette: level.NewPalette(),
PageType: level.Bounded, 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 // configureCanvas updates the settings of the background canvas, so a live
// preview of the wallpaper and wrapping type can be shown. // preview of the wallpaper and wrapping type can be shown.
func (s *MenuScene) configureCanvas(e render.Engine, pageType level.PageType, wallpaper string) { func (s *MenuScene) configureCanvas(pageType level.PageType, wallpaper string) {
s.canvas.LoadLevel(e, &level.Level{ s.canvas.LoadLevel(&level.Level{
Chunker: level.NewChunker(100), Chunker: level.NewChunker(100),
Palette: level.NewPalette(), Palette: level.NewPalette(),
PageType: pageType, PageType: pageType,
@ -151,7 +151,7 @@ func (s *MenuScene) setupNewWindow(d *Doodle) error {
Engine: d.Engine, Engine: d.Engine,
OnChangePageTypeAndWallpaper: func(pageType level.PageType, wallpaper string) { OnChangePageTypeAndWallpaper: func(pageType level.PageType, wallpaper string) {
log.Info("OnChangePageTypeAndWallpaper called: %+v, %+v", pageType, wallpaper) log.Info("OnChangePageTypeAndWallpaper called: %+v, %+v", pageType, wallpaper)
s.configureCanvas(d.Engine, pageType, wallpaper) s.configureCanvas(pageType, wallpaper)
}, },
OnCreateNewLevel: func(lvl *level.Level) { OnCreateNewLevel: func(lvl *level.Level) {
d.Goto(&EditorScene{ d.Goto(&EditorScene{

View File

@ -7,6 +7,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/level" "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/shmem"
"git.kirsle.net/apps/doodle/pkg/uix" "git.kirsle.net/apps/doodle/pkg/uix"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
@ -108,7 +109,7 @@ func setup() {
// Create the parent container that will stretch full screen. // Create the parent container that will stretch full screen.
window = ui.NewFrame("Loadscreen Window") 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. // "Loading" text.
label := ui.NewLabel(ui.Label{ label := ui.NewLabel(ui.Label{
@ -167,7 +168,7 @@ func Loop(windowSize render.Rect, e render.Engine) {
// Initialize the wallpaper canvas? // Initialize the wallpaper canvas?
if canvas == nil { if canvas == nil {
canvas = uix.NewCanvas(128, false) canvas = uix.NewCanvas(128, false)
canvas.LoadLevel(e, &level.Level{ canvas.LoadLevel(&level.Level{
Chunker: level.NewChunker(100), Chunker: level.NewChunker(100),
Palette: level.NewPalette(), Palette: level.NewPalette(),
PageType: level.Bounded, 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. // of chunks vs. chunks remaining to pre-cache bitmaps from.
func PreloadAllChunkBitmaps(chunker *level.Chunker) { func PreloadAllChunkBitmaps(chunker *level.Chunker) {
loadChunksTarget := len(chunker.Chunks) loadChunksTarget := len(chunker.Chunks)
// if loadChunksTarget == 0 {
// return
// }
for { for {
remaining := chunker.PrerenderN(10) remaining := chunker.PrerenderN(10)
log.Info("Remain: %d", remaining)
// Set the load screen progress % based on number of chunks to render. // Set the load screen progress % based on number of chunks to render.
if loadChunksTarget > 0 { if loadChunksTarget > 0 {

View File

@ -171,7 +171,7 @@ func (s *PlayScene) setupAsync(d *Doodle) error {
// Given a filename or map data to play? // Given a filename or map data to play?
if s.Level != nil { if s.Level != nil {
log.Debug("PlayScene.Setup: received level from scene caller") 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) s.drawing.InstallActors(s.Level.Actors)
} else if s.Filename != "" { } else if s.Filename != "" {
loadscreen.SetSubtitle("Opening: " + s.Filename) loadscreen.SetSubtitle("Opening: " + s.Filename)
@ -183,7 +183,7 @@ func (s *PlayScene) setupAsync(d *Doodle) error {
if s.Level == nil { if s.Level == nil {
log.Debug("PlayScene.Setup: no grid given, initializing empty grid") log.Debug("PlayScene.Setup: no grid given, initializing empty grid")
s.Level = level.New() s.Level = level.New()
s.drawing.LoadLevel(d.Engine, s.Level) s.drawing.LoadLevel(s.Level)
s.drawing.InstallActors(s.Level.Actors) s.drawing.InstallActors(s.Level.Actors)
} }
@ -611,7 +611,7 @@ func (s *PlayScene) LoadLevel(filename string) error {
} }
s.Level = level s.Level = level
s.drawing.LoadLevel(s.d.Engine, s.Level) s.drawing.LoadLevel(s.Level)
s.drawing.InstallActors(s.Level.Actors) s.drawing.InstallActors(s.Level.Actors)
return nil return nil

View File

@ -29,12 +29,7 @@ func LoadImage(e render.Engine, filename string) (*ui.Image, error) {
return nil, err return nil, err
} }
tex, err := e.StoreTexture(filename, img) return ui.ImageFromImage(img)
if err != nil {
return nil, err
}
return ui.ImageFromTexture(tex), nil
} }
// WASM: try the file over HTTP ajax request. // 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 return nil, err
} }
tex, err := e.StoreTexture(filename, img) return ui.ImageFromImage(img)
if err != nil {
return nil, err
}
return ui.ImageFromTexture(tex), nil
} }
// Then try the file system. // Then try the file system.
@ -71,12 +61,7 @@ func LoadImage(e render.Engine, filename string) (*ui.Image, error) {
return nil, err return nil, err
} }
tex, err := e.StoreTexture(filename, img) return ui.ImageFromImage(img)
if err != nil {
return nil, err
}
return ui.ImageFromTexture(tex), nil
} }
return nil, errors.New("no such sprite found") return nil, errors.New("no such sprite found")

View File

@ -48,7 +48,7 @@ func (s *StoryScene) Setup(d *Doodle) error {
// Set up the background wallpaper canvas. // Set up the background wallpaper canvas.
s.canvas = uix.NewCanvas(100, false) s.canvas = uix.NewCanvas(100, false)
s.canvas.Resize(render.NewRect(d.width, d.height)) 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), Chunker: level.NewChunker(100),
Palette: level.NewPalette(), Palette: level.NewPalette(),
PageType: level.Bounded, PageType: level.Bounded,

View File

@ -145,7 +145,7 @@ func (w *Canvas) Load(p *level.Palette, g *level.Chunker) {
} }
// LoadLevel initializes a Canvas from a Level object. // 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.level = level
w.Load(level.Palette, level.Chunker) 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 { if err != nil {
log.Error("wallpaper FromFile(%s): %s", filename, err) log.Error("wallpaper FromFile(%s): %s", filename, err)
} }
w.wallpaper.maxWidth = level.MaxWidth w.wallpaper.maxWidth = level.MaxWidth
w.wallpaper.maxHeight = level.MaxHeight w.wallpaper.maxHeight = level.MaxHeight
err = w.wallpaper.Load(e, level.PageType, wp) err = w.wallpaper.Load(level.PageType, wp)
if err != nil { if err != nil {
log.Error("wallpaper Load: %s", err) log.Error("wallpaper Load: %s", err)
} }

View File

@ -11,16 +11,15 @@ type Wallpaper struct {
pageType level.PageType pageType level.PageType
maxWidth int64 maxWidth int64
maxHeight int64 maxHeight int64
corner render.Texturer
top render.Texturer // Pointer to the Wallpaper datum.
left render.Texturer WP *wallpaper.Wallpaper
repeat render.Texturer
} }
// Valid returns whether the Wallpaper is configured. Only Levels should // Valid returns whether the Wallpaper is configured. Only Levels should
// have wallpapers and Doodads will have nil ones. // have wallpapers and Doodads will have nil ones.
func (wp *Wallpaper) Valid() bool { 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 // 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 ( var (
wp = w.wallpaper wp = w.wallpaper
S = w.Size() S = w.Size()
size = wp.corner.Size() size = wp.WP.QuarterRect()
sizeOrig = wp.corner.Size() sizeOrig = wp.WP.QuarterRect()
// Get the relative viewport of world coordinates looked at by the canvas. // Get the relative viewport of world coordinates looked at by the canvas.
// The X,Y values are the negative Scroll value // 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 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)) 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. // 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)) 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. // 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)) 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 return nil
} }
// Load the wallpaper settings from a level. // 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 wp.pageType = pageType
if tex, err := v.CornerTexture(e); err == nil { wp.WP = v
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
}
return nil return nil
} }

View File

@ -48,19 +48,19 @@ type Wallpaper struct {
// FromImage creates a Wallpaper from an image.Image. // FromImage creates a Wallpaper from an image.Image.
// If the renger.Engine is nil it will compute images but not pre-cache any // If the renger.Engine is nil it will compute images but not pre-cache any
// textures yet. // 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{ wp := &Wallpaper{
Name: name, Name: name,
Image: img, Image: img,
} }
wp.cache(e) wp.cache()
return wp, nil return wp, nil
} }
// FromFile creates a Wallpaper from a file on disk. // FromFile creates a Wallpaper from a file on disk.
// If the renger.Engine is nil it will compute images but not pre-cache any // If the renger.Engine is nil it will compute images but not pre-cache any
// textures yet. // 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. // Default object to return on errors.
var defaultWP = &Wallpaper{ var defaultWP = &Wallpaper{
Name: strings.Split(filepath.Base(filename), ".")[0], Name: strings.Split(filepath.Base(filename), ".")[0],
@ -118,12 +118,12 @@ func FromFile(e render.Engine, filename string, embeddable filesystem.Embeddable
Image: rgba, Image: rgba,
ready: true, ready: true,
} }
wp.cache(e) wp.cache()
return wp, nil return wp, nil
} }
// cache the bitmap images. // 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 // Zero-bound the rect cuz an image.Rect doesn't necessarily contain 0,0
var rect = wp.Image.Bounds() var rect = wp.Image.Bounds()
if rect.Min.X < 0 { if rect.Min.X < 0 {
@ -164,6 +164,11 @@ func (wp *Wallpaper) QuarterSize() (int, int) {
return wp.quarterWidth, wp.quarterHeight 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. // Corner returns the top left corner image.
func (wp *Wallpaper) Corner() *image.RGBA { func (wp *Wallpaper) Corner() *image.RGBA {
return wp.corner return wp.corner