Lazily load sprite image data

This cuts memory use significantly, since many sprites in an object are
never used. We can get savings over time by evicting sprites when they
go out of scope, but that's, well, out of scope.

To achieve this, I introduce an assetstore package that is in charge of
loading things from the filesystem. This also allows some lingering
case-sensitivity issues to be handled cleanly.

I'd hoped that creating fewer ebiten.Image instances would help CPU
usage, but that doesn't seem to be the case.
This commit is contained in:
2020-03-19 22:24:21 +00:00
parent 34d12edc2a
commit 5fccf97f4b
8 changed files with 440 additions and 152 deletions

View File

@@ -2,32 +2,25 @@ package main
import (
"flag"
"fmt"
"image"
"log"
"math"
"os"
"path/filepath"
"github.com/hajimehoshi/ebiten"
"code.ur.gs/lupine/ordoor/internal/conv"
"code.ur.gs/lupine/ordoor/internal/data"
"code.ur.gs/lupine/ordoor/internal/maps"
"code.ur.gs/lupine/ordoor/internal/sets"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/ui"
)
var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
mapFile = flag.String("map", "", "Prefix path to a .map file, e.g. ./orig/Maps/Chapter01.MAP")
txtFile = flag.String("txt", "", "Prefix path to a .txt file, e.g. ./orig/Maps/Chapter01.txt")
gameMap = flag.String("map", "", "Name of a map, e.g., Chapter01")
)
type env struct {
gameMap *maps.GameMap
set *sets.MapSet
objects map[string]*conv.Object
assets *assetstore.AssetStore
area *assetstore.Map
step int
state state
@@ -43,39 +36,19 @@ type state struct {
func main() {
flag.Parse()
if *gamePath == "" || *mapFile == "" || *txtFile == "" {
if *gamePath == "" || *gameMap == "" {
flag.Usage()
os.Exit(1)
}
gameMap, err := maps.LoadGameMapByFiles(*mapFile, *txtFile)
assets, err := assetstore.New(*gamePath)
if err != nil {
log.Fatalf("Couldn't load map file: %v", err)
log.Fatalf("Failed to scan root directory %v: %v", *gamePath, err)
}
setFile := filepath.Join(*gamePath, "Sets", gameMap.MapSetFilename())
log.Println(setFile)
mapSet, err := sets.LoadSet(setFile)
area, err := assets.Map(*gameMap)
if err != nil {
log.Fatalf("Couldn't load set file %s: %v", setFile, err)
}
objects := make([]*conv.Object, 0, len(mapSet.Palette))
for _, name := range mapSet.Palette {
objFile := filepath.Join(*gamePath, "Obj", name+".obj")
rawObj, err := data.LoadObject(objFile)
if err != nil {
log.Fatalf("Failed to load %s: %v", name, err)
}
rawObj.Name = name
obj, err := conv.ConvertObject(rawObj, name)
if err != nil {
log.Fatal(err)
}
objects = append(objects, obj)
log.Fatalf("Failed to load map %v: %v", *gameMap, err)
}
state := state{
@@ -83,14 +56,13 @@ func main() {
origin: image.Point{0, 3000}, // FIXME: haxxx
}
env := &env{
gameMap: gameMap,
set: mapSet,
objects: conv.MapByName(objects),
area: area,
assets: assets,
state: state,
lastState: state,
}
win, err := ui.NewWindow("View Map " + *mapFile)
win, err := ui.NewWindow("View Map " + *gameMap)
if err != nil {
log.Fatal("Couldn't create window: %v", err)
}
@@ -123,30 +95,6 @@ func (e *env) Update() error {
return nil
}
func (e *env) getSprite(palette []string, ref maps.ObjRef) (*conv.Sprite, error) {
// There seems to be an active bit that hides many sins
if !ref.IsActive() {
return nil, nil
}
if ref.Index() >= len(palette) {
return nil, fmt.Errorf("Palette too small: %v requested", ref.Index())
}
name := palette[ref.Index()]
obj := e.objects[name]
if obj == nil {
return nil, fmt.Errorf("Failed to find surface sprite %#v -> %q", ref, name)
}
if ref.Sprite() >= len(obj.Sprites) {
return nil, fmt.Errorf("Out-of-index sprite %v requested for %v", ref.Sprite(), name)
}
return obj.Sprites[ref.Sprite()], nil
}
func (e *env) Draw(screen *ebiten.Image) error {
// Bounds clipping
// http://www.java-gaming.org/index.php?topic=24922.0
@@ -177,8 +125,7 @@ func (e *env) Draw(screen *ebiten.Image) error {
x := (a + b) / 2
y := (a - b) / 2
if x < int(e.gameMap.MinWidth) || x >= int(e.gameMap.MaxWidth) ||
y < int(e.gameMap.MinLength) || y >= int(e.gameMap.MaxLength) {
if !image.Pt(x, y).In(e.area.Rect) {
continue
}
@@ -192,31 +139,9 @@ func (e *env) Draw(screen *ebiten.Image) error {
}
func (e *env) renderCell(x, y, z int, screen *ebiten.Image) error {
var sprites []*conv.Sprite
cell := e.gameMap.Cells.At(x, y, z)
if spr, err := e.getSprite(e.set.Palette, cell.Surface); err != nil {
log.Printf("%v %v %v surface: %v", x, y, z, err)
} else if spr != nil {
sprites = append(sprites, spr)
}
if spr, err := e.getSprite(e.set.Palette, cell.Center); err != nil {
log.Printf("%v %v %v center: %v", x, y, z, err)
} else if spr != nil {
sprites = append(sprites, spr)
}
if spr, err := e.getSprite(e.set.Palette, cell.Left); err != nil {
log.Printf("%v %v %v left: %v", x, y, z, err)
} else if spr != nil {
sprites = append(sprites, spr)
}
if spr, err := e.getSprite(e.set.Palette, cell.Right); err != nil {
log.Printf("%v %v %v right: %v", x, y, z, err)
} else if spr != nil {
sprites = append(sprites, spr)
images, err := e.area.ImagesForCell(x, y, z)
if err != nil {
return err
}
iso := ebiten.GeoM{}
@@ -231,8 +156,8 @@ func (e *env) renderCell(x, y, z int, screen *ebiten.Image) error {
// TODO: iso.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor
for _, sprite := range sprites {
if err := screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: iso}); err != nil {
for _, img := range images {
if err := screen.DrawImage(img, &ebiten.DrawImageOptions{GeoM: iso}); err != nil {
return err
}
}