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

@@ -3,6 +3,7 @@ package data
import (
"encoding/binary"
"fmt"
"image"
"io"
"io/ioutil"
"os"
@@ -40,6 +41,16 @@ type Sprite struct {
Data []byte
}
func (s *Sprite) ToImage() *image.Paletted {
return &image.Paletted{
Pix: s.Data,
Stride: int(s.Width),
Rect: image.Rect(0, 0, int(s.Width), int(s.Height)),
Palette: ColorPalette,
}
}
// dirEntry totals 8 bytes on disk
type dirEntry struct {
Offset uint32 // Offset of the sprite relative to the data block
Size uint32 // Size of the sprite in bytes, including any header
@@ -84,7 +95,7 @@ type Object struct {
Sprites []*Sprite
}
func LoadObject(filename string) (*Object, error) {
func LoadObjectLazily(filename string) (*Object, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
@@ -101,53 +112,85 @@ func LoadObject(filename string) (*Object, error) {
return nil, err
}
// Now load all sprites into memory
dir := make([]dirEntry, out.NumSprites)
if _, err := f.Seek(int64(out.DirOffset), io.SeekStart); err != nil {
return nil, fmt.Errorf("Seeking to sprite directory: %v", err)
}
if err := binary.Read(f, binary.LittleEndian, &dir); err != nil {
return nil, fmt.Errorf("Reading sprite directory: %v", err)
}
if _, err := f.Seek(int64(out.DataOffset), io.SeekStart); err != nil {
return nil, fmt.Errorf("Seeking to sprites: %v", err)
}
for i, dirEntry := range dir {
if err := dirEntry.Check(); err != nil {
return nil, err
}
if _, err := f.Seek(int64(out.DataOffset+dirEntry.Offset), io.SeekStart); err != nil {
return nil, fmt.Errorf("Seeking to sprite %v: %v", i, err)
}
sprite := &Sprite{}
if err := binary.Read(f, binary.LittleEndian, &sprite.SpriteHeader); err != nil {
return nil, fmt.Errorf("Reading sprite %v header: %v", i, err)
}
if err := sprite.Check(dirEntry.Size); err != nil {
return nil, err
}
buf := io.LimitReader(f, int64(sprite.PixelSize))
sprite.Data = make([]byte, int(sprite.Height)*int(sprite.Width))
// The pixel data is RLE-compressed. Uncompress it here.
if err := rle.Expand(buf, sprite.Data); err != nil {
return nil, err
}
out.Sprites = append(out.Sprites, sprite)
}
out.Sprites = make([]*Sprite, int(out.NumSprites))
return out, nil
}
func LoadObject(filename string) (*Object, error) {
obj, err := LoadObjectLazily(filename)
if err != nil {
return nil, err
}
if err := obj.LoadAllSprites(); err != nil {
return nil, err
}
return obj, nil
}
func (o *Object) LoadAllSprites() error {
for i := 0; i < int(o.NumSprites); i++ {
if err := o.LoadSprite(i); err != nil {
return err
}
}
return nil
}
func (o *Object) LoadSprite(idx int) error {
if idx < 0 || idx >= int(o.NumSprites) {
return fmt.Errorf("Asked for idx %v of %v", idx, o.NumSprites)
}
f, err := os.Open(o.Filename)
if err != nil {
return err
}
defer f.Close()
var entry dirEntry
if _, err := f.Seek(int64(o.DirOffset)+int64(idx*8), io.SeekStart); err != nil {
return fmt.Errorf("Seeking to sprite directory entry %v: %v", idx, err)
}
if err := binary.Read(f, binary.LittleEndian, &entry); err != nil {
return fmt.Errorf("Reading sprite directory entry %v: %v", idx, err)
}
if err := entry.Check(); err != nil {
return err
}
if _, err := f.Seek(int64(o.DataOffset+entry.Offset), io.SeekStart); err != nil {
return fmt.Errorf("Seeking to sprite %v: %v", idx, err)
}
sprite := &Sprite{}
if err := binary.Read(f, binary.LittleEndian, &sprite.SpriteHeader); err != nil {
return fmt.Errorf("Reading sprite %v header: %v", idx, err)
}
if err := sprite.Check(entry.Size); err != nil {
return err
}
buf := io.LimitReader(f, int64(sprite.PixelSize))
sprite.Data = make([]byte, int(sprite.Height)*int(sprite.Width))
// The pixel data is RLE-compressed. Uncompress it here.
if err := rle.Expand(buf, sprite.Data); err != nil {
return err
}
o.Sprites[idx] = sprite
return nil
}
func LoadObjects(dir string) (map[string]*Object, error) {
fis, err := ioutil.ReadDir(dir)
if err != nil {