Files
2026-09-26 16:32:40 +03:00

419 lines
9.4 KiB
Go

package loader
import (
"bytes"
"aego/asset"
"aego/core/errs"
"aego/core/ids"
"aego/core/log"
"aego/gpu"
"aego/render/texture"
"aego/vfs"
)
const ServiceID = "asset.loader"
type pageEntry struct {
handle gpu.TextureHandle
width int
height int
filter gpu.FilterMode
}
type regionEntry struct {
region texture.Region
page ids.AssetID
}
type Loader struct {
dev gpu.Device
up *texture.Uploader
log *log.ChannelLogger
index *asset.Index
files vfs.FS
pages map[ids.AssetID]pageEntry
region map[ids.AssetID]regionEntry
blobs map[ids.AssetID][]byte
failed map[ids.AssetID]bool
subs []subscriber
nextID uint64
gen uint32
}
type subscriber struct {
id uint64
fn func(asset.Reloaded)
}
func New(dev gpu.Device, up *texture.Uploader, lg *log.Logger) *Loader {
if lg == nil {
lg = log.Nop()
}
return &Loader{
dev: dev,
up: up,
log: lg.With(log.Asset),
pages: map[ids.AssetID]pageEntry{},
region: map[ids.AssetID]regionEntry{},
blobs: map[ids.AssetID][]byte{},
failed: map[ids.AssetID]bool{},
gen: 1,
}
}
func (l *Loader) Mount(idx *asset.Index, files vfs.FS) error {
if idx == nil || files == nil {
return errs.New(errs.AssetNotMounted, "loader needs both an index and an artifact file system")
}
l.index = idx
l.files = files
l.gen++
return nil
}
func (l *Loader) Generation() uint32 {
return l.gen
}
func (l *Loader) Resident() int {
return len(l.pages) + len(l.blobs)
}
func (l *Loader) Subscribe(fn func(asset.Reloaded)) func() {
l.nextID++
id := l.nextID
l.subs = append(l.subs, subscriber{id: id, fn: fn})
return func() {
for i := range l.subs {
if l.subs[i].id == id {
l.subs = append(l.subs[:i], l.subs[i+1:]...)
return
}
}
}
}
func (l *Loader) notify(r asset.Reloaded) {
for i := range l.subs {
l.subs[i].fn(r)
}
}
func (l *Loader) entry(id ids.AssetID) (*asset.Entry, bool) {
if l.index == nil {
return nil, false
}
e, ok := l.index.Lookup(id)
if !ok || e.Missing {
return nil, false
}
return e, true
}
func (l *Loader) read(a asset.Artifact) ([]byte, error) {
data, err := l.files.ReadFile(a.Path)
if err != nil {
return nil, err
}
return data, nil
}
func (l *Loader) Texture(id ids.AssetID) (texture.Region, bool) {
if r, ok := l.region[id]; ok {
return r.region, true
}
if l.failed[id] {
return texture.Region{}, false
}
e, ok := l.entry(id)
if !ok {
return texture.Region{}, false
}
switch e.Kind {
case asset.ArtSpriteRegion:
return l.loadRegion(id, e)
case asset.ArtTexture, asset.ArtAtlasPage:
page, ok := l.loadPage(id, e)
if !ok {
return texture.Region{}, false
}
reg := texture.Whole(page.handle, page.width, page.height)
l.region[id] = regionEntry{region: reg, page: id}
return reg, true
}
return texture.Region{}, false
}
func (l *Loader) loadRegion(id ids.AssetID, e *asset.Entry) (texture.Region, bool) {
art, ok := e.Artifact(asset.ArtSpriteRegion)
if !ok {
l.fail(id, "sprite region artifact is missing", nil)
return texture.Region{}, false
}
data, err := l.read(art)
if err != nil {
l.fail(id, "cannot read the sprite region artifact", err)
return texture.Region{}, false
}
ra, err := asset.DecodeRegion(data)
if err != nil {
l.fail(id, "malformed sprite region artifact", err)
return texture.Region{}, false
}
pageEntryValue, ok := l.pages[ra.Page]
if !ok {
pe, pok := l.entry(ra.Page)
if !pok {
l.fail(id, "sprite region points at a missing page", nil)
return texture.Region{}, false
}
pageEntryValue, ok = l.loadPage(ra.Page, pe)
if !ok {
return texture.Region{}, false
}
}
whole := texture.Whole(pageEntryValue.handle, pageEntryValue.width, pageEntryValue.height)
reg := whole.Sub(ra.X, ra.Y, ra.W, ra.H)
l.region[id] = regionEntry{region: reg, page: ra.Page}
return reg, true
}
func (l *Loader) loadPage(id ids.AssetID, e *asset.Entry) (pageEntry, bool) {
if p, ok := l.pages[id]; ok {
return p, true
}
kind := asset.ArtTexture
if e.Kind == asset.ArtAtlasPage {
kind = asset.ArtAtlasPage
}
art, ok := e.Artifact(kind)
if !ok {
l.fail(id, "texture artifact is missing", nil)
return pageEntry{}, false
}
data, err := l.read(art)
if err != nil {
l.fail(id, "cannot read the texture artifact", err)
return pageEntry{}, false
}
p, err := l.uploadTexture(data)
if err != nil {
l.fail(id, "cannot upload the texture", err)
return pageEntry{}, false
}
l.pages[id] = p
return p, true
}
func (l *Loader) uploadTexture(data []byte) (pageEntry, error) {
ta, err := asset.DecodeTexture(data)
if err != nil {
return pageEntry{}, err
}
d := &texture.Data{
Width: ta.Width,
Height: ta.Height,
Format: ta.Format,
Pixels: ta.Pixels,
Premultiplied: true,
}
h, err := l.up.Upload(d, texture.UploadOptions{Mips: ta.Mips, Label: "asset"})
if err != nil {
return pageEntry{}, err
}
return pageEntry{handle: h, width: ta.Width, height: ta.Height, filter: ta.Filter}, nil
}
func (l *Loader) fail(id ids.AssetID, msg string, cause error) {
l.failed[id] = true
fields := []log.Field{log.F("asset", id.String())}
if cause != nil {
fields = append(fields, log.F("err", cause.Error()))
}
l.log.Error(msg, fields...)
}
func (l *Loader) blob(id ids.AssetID, kind asset.ArtifactKind) ([]byte, bool) {
e, ok := l.entry(id)
if !ok || e.Kind != kind {
return nil, false
}
if b, ok := l.blobs[id]; ok {
return b, true
}
if l.failed[id] {
return nil, false
}
art, ok := e.Artifact(kind)
if !ok {
l.fail(id, "artifact is missing", nil)
return nil, false
}
data, err := l.read(art)
if err != nil {
l.fail(id, "cannot read the artifact", err)
return nil, false
}
l.blobs[id] = data
return data, true
}
func (l *Loader) Scene(id ids.AssetID) ([]byte, bool) {
return l.blob(id, asset.ArtScene)
}
func (l *Loader) Prefab(id ids.AssetID) ([]byte, bool) {
return l.blob(id, asset.ArtPrefab)
}
func (l *Loader) Preload(list ...ids.AssetID) error {
for _, id := range list {
e, ok := l.entry(id)
if !ok {
return errs.New(errs.AssetNotFound, "asset is not in the index", log.F("asset", id.String()))
}
switch e.Kind {
case asset.ArtTexture, asset.ArtAtlasPage, asset.ArtSpriteRegion:
if _, ok := l.Texture(id); !ok {
return errs.New(errs.AssetLoadFailed, "texture failed to load", log.F("asset", id.String()))
}
case asset.ArtScene:
if _, ok := l.Scene(id); !ok {
return errs.New(errs.AssetLoadFailed, "scene failed to load", log.F("asset", id.String()))
}
case asset.ArtPrefab:
if _, ok := l.Prefab(id); !ok {
return errs.New(errs.AssetLoadFailed, "prefab failed to load", log.F("asset", id.String()))
}
}
}
return nil
}
type staged struct {
id ids.AssetID
kind asset.ArtifactKind
page pageEntry
blob []byte
isTex bool
unchanged bool
}
func (l *Loader) Swap(entries []asset.Entry, files vfs.FS) error {
if files != nil {
l.files = files
}
if l.index == nil {
return errs.New(errs.AssetNotMounted, "loader has no index mounted")
}
prepared := make([]staged, 0, len(entries))
for i := range entries {
e := entries[i]
switch e.Kind {
case asset.ArtTexture, asset.ArtAtlasPage:
art, ok := e.Artifact(e.Kind)
if !ok {
return errs.New(errs.AssetBadArtifact, "entry has no artifact of its own kind",
log.F("asset", e.ID.String()))
}
data, err := l.files.ReadFile(art.Path)
if err != nil {
l.rollback(prepared)
return err
}
p, err := l.uploadTexture(data)
if err != nil {
l.rollback(prepared)
return err
}
prepared = append(prepared, staged{id: e.ID, kind: e.Kind, page: p, isTex: true})
case asset.ArtSpriteRegion, asset.ArtScene, asset.ArtPrefab:
art, ok := e.Artifact(e.Kind)
if !ok {
return errs.New(errs.AssetBadArtifact, "entry has no artifact of its own kind",
log.F("asset", e.ID.String()))
}
data, err := l.files.ReadFile(art.Path)
if err != nil {
l.rollback(prepared)
return err
}
prepared = append(prepared, staged{id: e.ID, kind: e.Kind, blob: data})
}
}
for i := range entries {
l.index.Put(entries[i])
}
for i := range prepared {
s := &prepared[i]
delete(l.failed, s.id)
switch {
case s.isTex:
if old, ok := l.pages[s.id]; ok {
l.dev.DestroyTexture(old.handle)
}
l.pages[s.id] = s.page
l.invalidateRegionsOf(s.id)
case s.kind == asset.ArtSpriteRegion:
delete(l.region, s.id)
default:
if old, ok := l.blobs[s.id]; ok && bytes.Equal(old, s.blob) {
s.unchanged = true
continue
}
l.blobs[s.id] = s.blob
}
}
l.gen++
for i := range prepared {
if prepared[i].unchanged {
continue
}
l.notify(asset.Reloaded{ID: prepared[i].id, Kind: prepared[i].kind})
}
return nil
}
func (l *Loader) invalidateRegionsOf(page ids.AssetID) {
for id, r := range l.region {
if r.page == page {
delete(l.region, id)
}
}
}
func (l *Loader) rollback(prepared []staged) {
for _, s := range prepared {
if s.isTex {
l.dev.DestroyTexture(s.page.handle)
}
}
}
func (l *Loader) Drop(id ids.AssetID) {
if p, ok := l.pages[id]; ok {
l.dev.DestroyTexture(p.handle)
delete(l.pages, id)
l.invalidateRegionsOf(id)
}
delete(l.region, id)
delete(l.blobs, id)
delete(l.failed, id)
l.gen++
}
func (l *Loader) Close() {
for id, p := range l.pages {
l.dev.DestroyTexture(p.handle)
delete(l.pages, id)
}
l.region = map[ids.AssetID]regionEntry{}
l.blobs = map[ids.AssetID][]byte{}
l.failed = map[ids.AssetID]bool{}
l.subs = nil
}
var _ asset.Resolver = (*Loader)(nil)