Files
aego/render/postfx/plan.go
T
sanspie 5eb1832907
ci-platforms / darwin amd64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 15m28s
ci / release (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 7m47s
Complete Stage 12 for v0.12.0
2026-11-05 11:46:53 +03:00

257 lines
6.9 KiB
Go

package postfx
import (
"aego/core/ids"
"aego/gpu"
"aego/render/material"
"aego/render/shader"
"fmt"
"strings"
)
type Profile struct {
ReducedMotion bool
LinearOutput bool
Transparent bool
}
type State uint8
const (
Active State = iota
Disabled
Neutral
Unsupported
Unknown
Invalid
)
type Status struct {
Phase Phase
passIndex int
TypeID string
InstanceID string
State State
Reason string
}
type Pass struct {
LUTID, MaterialID ids.AssetID
LUT gpu.TextureHandle
LUTSize int
Material *material.Material
Descriptor *Descriptor
InstanceID string
Params [4][4]float32
}
// MaterialSource reads a prepared material, including its declared colour phase.
type MaterialSource interface {
Material(ids.AssetID) (*material.Material, bool)
}
func (d *Descriptor) materialFor(e *Instance, src MaterialSource) *material.Material {
if !d.UsesMaterial || src == nil || e.Material.ID.IsZero() {
return nil
}
m, _ := src.Material(e.Material.ID)
return m
}
// Phase returns the phase declared by a Custom material. Older postfx shaders
// and newly added, unassigned Custom instances use the display-linear phase.
func (p Pass) Phase() Phase {
if p.Descriptor.UsesMaterial && p.Material != nil {
s := p.Material.Shader()
if s != nil && s.Domain == shader.DomainPostFX && s.Features&shader.FeaturePostSceneLinear != 0 {
return SceneLinear
}
}
return p.Descriptor.Phase
}
func (d *Descriptor) PhaseFor(e *Instance, src MaterialSource) Phase {
return (Pass{Descriptor: d, Material: d.materialFor(e, src)}).Phase()
}
type Plan struct {
Passes []Pass
States []Status
}
// Build reuses dst's storage. A rejected stack has no executable passes.
func (r *Registry) Build(dst *Plan, enabled bool, items []Instance, profile Profile) error {
return r.BuildWithMaterials(dst, enabled, items, profile, nil)
}
func (r *Registry) BuildWithMaterials(dst *Plan, enabled bool, items []Instance, profile Profile, src MaterialSource) error {
// Backing arrays are reused, but deleted effects must release material and string references.
clear(dst.Passes)
clear(dst.States)
dst.Passes = dst.Passes[:0]
dst.States = dst.States[:0]
var first error
var prevPhase Phase
for i := range items {
e := &items[i]
s := Status{InstanceID: e.InstanceID, TypeID: e.TypeID}
d, known := r.Lookup(e.TypeID)
var mat *material.Material
if known {
mat = d.materialFor(e, src)
s.Phase = (Pass{Descriptor: d, Material: mat}).Phase()
}
var why error
switch {
case strings.TrimSpace(e.InstanceID) == "" || strings.TrimSpace(e.TypeID) == "":
why = fmt.Errorf("postfx: missing type or instance ID at index %d", i)
case hasID(items[:i], e.InstanceID):
why = fmt.Errorf("postfx: duplicate instance ID %q", e.InstanceID)
case !enabled || !e.Enabled:
s.State, s.Reason = Disabled, "disabled"
if !known {
s.State, s.Reason = Unknown, "effect type unavailable (disabled)"
}
case !known:
why = fmt.Errorf("postfx: %s requires unavailable effect %q", e.InstanceID, e.TypeID)
case d.Version != e.Version:
why = fmt.Errorf("postfx: %s (%s): schema version %d, expected %d", e.InstanceID, d.ID, e.Version, d.Version)
default:
var p Pass
p, why = d.compile(e)
p.Material = mat
switch {
case why != nil:
why = fmt.Errorf("postfx: %s (%s): %w", e.InstanceID, e.TypeID, why)
case s.Phase < prevPhase:
why = fmt.Errorf("postfx: %s (%s) crosses the display-linear phase boundary", e.InstanceID, e.TypeID)
default:
prevPhase = s.Phase
switch {
case profile.ReducedMotion && d.MotionBypass:
s.State, s.Reason = Disabled, "reduced motion"
case p.neutral():
s.State, s.Reason = Neutral, "neutral parameters"
case profile.LinearOutput && s.Phase == DisplayLinear:
s.State, s.Reason = Unsupported, "display effect requires a display output"
case profile.Transparent && d.Alpha == OpaqueOnly:
s.State, s.Reason = Unsupported, "effect requires an opaque output"
default:
s.passIndex = len(dst.Passes)
dst.Passes = append(dst.Passes, p)
}
}
}
if why != nil {
s.State, s.Reason = Invalid, why.Error()
if first == nil {
first = why
}
}
dst.States = append(dst.States, s)
}
if first != nil {
clear(dst.Passes)
dst.Passes = dst.Passes[:0]
}
return first
}
func (r *Registry) Validate(items []Instance) error {
var plan Plan
return r.Build(&plan, true, items, Profile{})
}
// ValidateForOutput is the strict counterpart of preview's per-effect bypass.
func (r *Registry) ValidateForOutput(enabled bool, items []Instance, profile Profile) error {
var plan Plan
if err := r.Build(&plan, enabled, items, profile); err != nil {
return err
}
return plan.CheckOutput()
}
func (p *Plan) CheckOutput() error {
for _, s := range p.States {
if s.State == Unsupported {
return fmt.Errorf("postfx: %s (%s): %s", s.InstanceID, s.TypeID, s.Reason)
}
}
return nil
}
func (p Pass) neutral() bool {
d := p.Descriptor
return d.Neutral != nil && d.Neutral(p.Params) && (!d.LUT || p.LUTID.IsZero() || p.Params[0][2] == 0)
}
// UpdateParameters updates the uniforms of a structurally unchanged effect.
// False requests full validation for a changed shape, invalid value or bypass transition.
func (p *Plan) UpdateParameters(i int, d *Descriptor, e *Instance) bool {
s := p.States[i]
if s.State == Unknown || s.State == Disabled && !e.Enabled {
return true
}
if s.State != Active && s.State != Neutral {
return false
}
pass, err := d.compile(e)
if err != nil || pass.neutral() != (s.State == Neutral) {
return false
}
if s.State == Active {
p.Passes[s.passIndex].Params = pass.Params
}
return true
}
func hasID(items []Instance, id string) bool {
for i := range items {
if items[i].InstanceID == id {
return true
}
}
return false
}
func (d *Descriptor) compile(e *Instance) (Pass, error) {
p := Pass{Descriptor: d, InstanceID: e.InstanceID, LUTID: e.LUT.ID, MaterialID: e.Material.ID}
if !e.LUT.ID.IsZero() && !d.LUT || !e.Material.ID.IsZero() && !d.UsesMaterial {
return Pass{}, fmt.Errorf("unexpected effect asset reference")
}
if d.UsesMaterial && e.Material.ID.IsZero() {
return Pass{}, fmt.Errorf("assign a postfx material")
}
write := func(f *Field, v [4]float32) { copy(p.Params[f.Slot][f.Lane:], v[:f.Kind.Lanes()]) }
for i := range d.Fields {
f := &d.Fields[i]
v, _ := f.pack(f.Default)
write(f, v)
}
var used uint32
for _, param := range e.Parameters {
index := -1
for i := range d.Fields {
if d.Fields[i].Name == param.Name {
index = i
break
}
}
if index < 0 {
return Pass{}, fmt.Errorf("unknown parameter %q", param.Name)
}
bit := uint32(1 << index)
if used&bit != 0 {
return Pass{}, fmt.Errorf("duplicate parameter %q", param.Name)
}
used |= bit
f := &d.Fields[index]
v, err := f.pack(param)
if err != nil {
return Pass{}, err
}
write(f, v)
}
return p, nil
}