Files
ordoor/internal/data/object.go
Nick Thomas 5fccf97f4b 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.
2020-03-19 22:24:21 +00:00

222 lines
4.9 KiB
Go

package data
import (
"encoding/binary"
"fmt"
"image"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"code.ur.gs/lupine/ordoor/internal/data/rle"
)
type SpriteHeader struct {
Unknown0 uint32
Width uint16
Height uint16
Padding1 uint32 // I don't think this is used. Could be wrong.
PixelSize uint32 // Size of PixelData, excluding this sprite header
Padding2 uint64 // I don't think this is used either. Could be wrong.
}
func (s SpriteHeader) Check(expectedSize uint32) error {
if s.Padding1 != 0 || s.Padding2 != 0 {
return fmt.Errorf("Sprite header padding contains unknown values: %d %d", s.Padding1, s.Padding2)
}
// Remove 24 bytes from passed-in size to account for the header
if s.PixelSize != expectedSize-24 {
return fmt.Errorf("Advertised pixel size: %d differs from expected: %v", s.PixelSize, expectedSize-24)
}
return nil
}
type Sprite struct {
SpriteHeader
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
}
func (d dirEntry) Check() error {
if d.Size < 24 {
return fmt.Errorf("Unexpected sprite size: %d (expected >= 24)", d.Size)
}
return nil
}
// ObjectHeader totals 24 bytes on disk
type ObjectHeader struct {
NumSprites uint32 // How many sprites does this object have?
DirOffset uint32 // Offset of the directory block
DirSize uint32 // Size of the directory block. 8 * NumSprites
DataOffset uint32 // Offset of the sprite data block
DataSize uint32 // Size of the sprite data block
}
func (h ObjectHeader) ExpectedDirSize() uint32 {
return h.NumSprites * 8
}
func (h ObjectHeader) Check() error {
// TODO: check for overlaps
if h.ExpectedDirSize() != h.DirSize {
return fmt.Errorf("Unexpected sprite directory size: %d (expected %d)", h.DirSize, h.ExpectedDirSize())
}
return nil
}
type Object struct {
ObjectHeader
Filename string
Name string // left blank for use by you
Sprites []*Sprite
}
func LoadObjectLazily(filename string) (*Object, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
out := &Object{Filename: filename}
if err := binary.Read(f, binary.LittleEndian, &out.ObjectHeader); err != nil {
return nil, fmt.Errorf("Reading object header: %v", err)
}
if err := out.ObjectHeader.Check(); err != nil {
return nil, err
}
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 {
return nil, err
}
out := make(map[string]*Object, len(fis))
for _, fi := range fis {
filename := filepath.Join(dir, fi.Name())
basename := filepath.Base(filename)
extname := filepath.Ext(filename)
// Don't try to load non-.obj files
if !strings.EqualFold(extname, ".obj") {
continue
}
obj, err := LoadObject(filename)
if err != nil {
return nil, fmt.Errorf("%s: %v", filename, err)
}
out[basename] = obj
}
return out, nil
}