153 lines
5.2 KiB
Go
153 lines
5.2 KiB
Go
// Package gpupreview presents immutable Art Core snapshots with Aego's existing
|
|
// runtime renderer, lighting and CPU particles. It owns no authoring state,
|
|
// commands, history or file-system access.
|
|
package gpupreview
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"fmt"
|
|
|
|
"aego/art"
|
|
"aego/asset"
|
|
"aego/core/ids"
|
|
"aego/gpu"
|
|
"aego/render/texture"
|
|
"aego/vfs"
|
|
"aego/vfs/memfs"
|
|
)
|
|
|
|
const MaxSnapshotBytes = 64 << 20
|
|
const maxPreparedBytes = 64 << 20
|
|
|
|
type prepared struct {
|
|
info art.Info
|
|
entries []asset.Entry
|
|
files vfs.FS
|
|
frames map[art.ID]map[art.Channel]ids.AssetID
|
|
}
|
|
|
|
func previewID(document string, frame art.ID, channel art.Channel, kind string) ids.AssetID {
|
|
h := sha256.Sum256([]byte("aego.art.preview.v1\x00" + document + "\x00" + string(frame) + "\x00" + string(channel) + "\x00" + kind))
|
|
return ids.AssetID{Hi: binary.BigEndian.Uint64(h[:8]), Lo: binary.BigEndian.Uint64(h[8:16])}
|
|
}
|
|
|
|
// surfaceChannels indexes cels once, preserving the renderer's channel order and
|
|
// counting each frame/channel only once even when several layers contain it.
|
|
// Info comes from a validated Art document; enforce the existing expanded-pixel
|
|
// budget before rendering or allocating any frame-sized surface.
|
|
func surfaceChannels(i art.Info) (map[art.ID][]art.Channel, error) {
|
|
const (
|
|
normal = 1 << iota
|
|
height
|
|
emission
|
|
)
|
|
present := make(map[art.ID]uint8, len(i.Frames))
|
|
for _, c := range i.Cels {
|
|
switch c.Channel {
|
|
case art.Normal:
|
|
present[c.Frame] |= normal
|
|
case art.Height:
|
|
present[c.Frame] |= height
|
|
case art.Emission:
|
|
present[c.Frame] |= emission
|
|
}
|
|
}
|
|
channels := make(map[art.ID][]art.Channel, len(i.Frames))
|
|
surfaces := 0
|
|
for _, f := range i.Frames {
|
|
channels[f.ID] = []art.Channel{art.Albedo}
|
|
for _, ch := range []struct {
|
|
channel art.Channel
|
|
mask uint8
|
|
}{{art.Normal, normal}, {art.Height, height}, {art.Emission, emission}} {
|
|
if present[f.ID]&ch.mask != 0 {
|
|
channels[f.ID] = append(channels[f.ID], ch.channel)
|
|
}
|
|
}
|
|
surfaces += len(channels[f.ID])
|
|
}
|
|
bytes := int64(i.Canvas.Width) * int64(i.Canvas.Height) * 4 * int64(surfaces)
|
|
if bytes > maxPreparedBytes {
|
|
return nil, fmt.Errorf("preview: prepared surface pixels exceed the 64 MiB budget")
|
|
}
|
|
return channels, nil
|
|
}
|
|
|
|
func prepare(data []byte) (*prepared, error) {
|
|
if len(data) == 0 || len(data) > MaxSnapshotBytes {
|
|
return nil, fmt.Errorf("preview: document snapshot exceeds the 64 MiB limit")
|
|
}
|
|
d, err := art.Decode(data, art.DefaultLimits())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Rendered textures and Info are detached copies. Once preparation ends,
|
|
// the decoded authoring document and its tiled pixels are no longer needed.
|
|
defer d.Close()
|
|
i := d.Info()
|
|
channels, err := surfaceChannels(i)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p := &prepared{info: i, frames: make(map[art.ID]map[art.Channel]ids.AssetID)}
|
|
files := make(map[string][]byte)
|
|
put := func(id ids.AssetID, kind asset.ArtifactKind, data []byte, deps []ids.AssetID) {
|
|
h := sha256.Sum256(data)
|
|
name := id.String() + "." + string(kind)
|
|
files[name] = data
|
|
p.entries = append(p.entries, asset.Entry{ID: id, Kind: kind, Name: name, Deps: deps,
|
|
Artifacts: []asset.Artifact{{Kind: kind, Path: "res://" + name, Hash: hex.EncodeToString(h[:]), Size: int64(len(data))}}})
|
|
}
|
|
for _, f := range i.Frames {
|
|
p.frames[f.ID] = make(map[art.Channel]ids.AssetID)
|
|
for _, ch := range channels[f.ID] {
|
|
p.frames[f.ID][ch] = previewID(i.ID, f.ID, ch, "region")
|
|
}
|
|
}
|
|
for _, f := range i.Frames {
|
|
for _, ch := range channels[f.ID] {
|
|
im, e := d.Render(f.ID, art.RenderOptions{Channel: ch})
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
format, semantic := gpu.TexSRGBA8, asset.TextureColor
|
|
switch ch {
|
|
case art.Normal:
|
|
format, semantic = gpu.TexRGBA8, asset.TextureNormal
|
|
case art.Height:
|
|
format, semantic = gpu.TexRGBA8, asset.TextureHeight
|
|
case art.Emission:
|
|
semantic = asset.TextureEmissionRGB
|
|
}
|
|
pixels := &texture.Data{Width: i.Canvas.Width, Height: i.Canvas.Height, Format: format, Pixels: im.Pix, NonColor: semantic != asset.TextureColor}
|
|
// Identical color preparation to the PNG importer and runtime loader.
|
|
pixels.Premultiply()
|
|
page := previewID(i.ID, f.ID, ch, "texture")
|
|
put(page, asset.ArtTexture, asset.EncodeTexture(&asset.TextureArtifact{Width: pixels.Width, Height: pixels.Height,
|
|
Format: format, Filter: gpu.FilterNearest, Semantic: semantic, Pixels: pixels.Pixels}), nil)
|
|
r := asset.RegionArtifact{Page: page, W: pixels.Width, H: pixels.Height, PageW: pixels.Width, PageH: pixels.Height, Filter: gpu.FilterNearest}
|
|
deps := []ids.AssetID{page}
|
|
if pivot, ok := i.FramePivot(f.ID); ok {
|
|
r.HasPivot = true
|
|
r.Pivot = [2]float32{float32(pivot.X) / float32(pixels.Width), float32(pivot.Y) / float32(pixels.Height)}
|
|
}
|
|
if ch == art.Albedo {
|
|
r.Normal, r.Emission = p.frames[f.ID][art.Normal], p.frames[f.ID][art.Emission]
|
|
for _, id := range []ids.AssetID{r.Normal, r.Emission} {
|
|
if !id.IsZero() {
|
|
deps = append(deps, id)
|
|
}
|
|
}
|
|
}
|
|
put(p.frames[f.ID][ch], asset.ArtSpriteRegion, asset.EncodeRegion(&r), deps)
|
|
}
|
|
}
|
|
p.files, err = vfs.New(vfs.Mount{Scheme: "res", Source: memfs.New(files), ReadOnly: true})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p, nil
|
|
}
|