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,
"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 {

View File

@ -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")

View File

@ -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.

View File

@ -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{

View File

@ -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 {

View File

@ -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

View File

@ -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")

View File

@ -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,

View File

@ -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)
}

View File

@ -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
}

View File

@ -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