doodle/pkg/scripting/vm.go
Noah Petherbridge c5353df211 LoadUnloadChunk for Memory Optimization
Instead of the loadscreen eager-loading ALL level chunks to Go Images, only
load the chunks within the "LoadingViewport" - which is the on-screen
Viewport plus a margin of chunks off the screen edges.

During gameplay, every few ticks, reevaluate which chunks are inside or
outside the LoadingViewport; for chunks outside, free their SDL2 textures
and free their cached bitmaps to keep overall memory usage down. The
AzulianTag-Forest level now stays under 200 Textures at any given time
and the loadscreen goes faster as it doesn't have to load every chunk's
images up front.

The LoadUnloadChunk feature can be turned on/off with feature flags. If
disabled the old behavior is restored: loadscreen loads all images and
the LoadUnloadChunks function is not run.

Other changes:

* loadscreen: do not free textures in the Hide() function as this runs on
  a different goroutine and may break. The 4 wallpaper textures are OK
  to keep in memory anyway, the loadscreen is reused often!
* Free more leaked textures: on the Inventory frame and when an actor
  calls Self.Destroy()
* Stop leaking goroutines in the PubSub feature of the doodad script
  engine; scripting.Supervisor.Teardown() sends a stop signal to all
  scripts to clean up neatly. Canvas.Destroy() tears down its scripting
  supervisor automatically.
2022-04-10 12:40:25 -07:00

99 lines
2.2 KiB
Go

package scripting
import (
"errors"
"fmt"
"sync"
"git.kirsle.net/apps/doodle/pkg/log"
"github.com/dop251/goja"
)
// VM manages a single isolated JavaScript VM.
type VM struct {
Name string
// Globals available to the scripts.
Events *Events
Self interface{}
// Channels for inbound and outbound PubSub messages.
// Each VM has a single Inbound channel that watches for received messages
// and invokes the Message.Subscribe() handlers for relevant ones.
// Each VM also has an array of Outbound channels which map to the Inbound
// channel of the VMs it is linked to, for pushing out Message.Publish()
// messages.
Inbound chan Message
Outbound []chan Message
stop chan bool
subscribe map[string][]goja.Value // Subscribed message handlers by name.
muSubscribe sync.RWMutex
vm *goja.Runtime
// setTimeout and setInterval variables.
timerLastID int // becomes 1 when first timer is set
timers map[int]*Timer
}
// NewVM creates a new JavaScript VM.
func NewVM(name string) *VM {
vm := &VM{
Name: name,
vm: goja.New(),
timers: map[int]*Timer{},
// Pub/sub structs.
Inbound: make(chan Message),
Outbound: []chan Message{},
stop: make(chan bool, 1),
subscribe: map[string][]goja.Value{},
}
vm.Events = NewEvents(vm.vm)
return vm
}
// Run code in the VM.
func (vm *VM) Run(src string) (goja.Value, error) {
v, err := vm.vm.RunString(src)
return v, err
}
// Set a value in the VM.
func (vm *VM) Set(name string, v interface{}) error {
return vm.vm.Set(name, v)
}
// RegisterLevelHooks registers accessors to the level hooks
// and Doodad API for Play Mode.
func (vm *VM) RegisterLevelHooks() error {
bindings := NewJSProxy(vm)
for name, v := range bindings {
err := vm.vm.Set(name, v)
if err != nil {
return fmt.Errorf("RegisterLevelHooks(%s): %s",
name, err,
)
}
}
return nil
}
// Main calls the main function of the script.
func (vm *VM) Main() error {
function, ok := goja.AssertFunction(vm.vm.Get("main"))
if !ok {
return errors.New("didn't find function main()")
}
// Catch panics.
defer func() {
if err := recover(); err != nil {
log.Error("Panic caught in JavaScript VM: %s", err)
}
}()
_, err := function(goja.Undefined())
return err
}