505 lines
12 KiB
Go
505 lines
12 KiB
Go
package render
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"math"
|
|
|
|
"aego/core/errs"
|
|
"aego/core/log"
|
|
"aego/core/mathx"
|
|
"aego/core/trace"
|
|
"aego/gpu"
|
|
"aego/render/debug"
|
|
"aego/render/gpuprof"
|
|
"aego/render/mesh"
|
|
"aego/render/sprite"
|
|
"aego/render/text"
|
|
"aego/render/texture"
|
|
"aego/render/view"
|
|
"aego/vfs"
|
|
)
|
|
|
|
const (
|
|
spriteStride = sprite.InstanceStride
|
|
sprite3DStride = sprite.Instance3DStride
|
|
uniformSize = 64
|
|
drawUniformSize = 96
|
|
defaultSprite = 8192
|
|
defaultSprite3D = 4096
|
|
defaultMeshes = 4096
|
|
defaultDebug = 8192
|
|
defaultBudget = 4 << 20
|
|
blendModes = 3
|
|
)
|
|
|
|
type Options struct {
|
|
MaxSprites int
|
|
MaxSprites3D int
|
|
MaxMeshes int
|
|
MaxDebugVerts int
|
|
UploadBudget int
|
|
}
|
|
|
|
func (o Options) withDefaults() Options {
|
|
if o.MaxSprites <= 0 {
|
|
o.MaxSprites = defaultSprite
|
|
}
|
|
if o.MaxSprites3D <= 0 {
|
|
o.MaxSprites3D = defaultSprite3D
|
|
}
|
|
if o.MaxMeshes <= 0 {
|
|
o.MaxMeshes = defaultMeshes
|
|
}
|
|
if o.MaxDebugVerts <= 0 {
|
|
o.MaxDebugVerts = defaultDebug
|
|
}
|
|
if o.UploadBudget <= 0 {
|
|
o.UploadBudget = defaultBudget
|
|
}
|
|
return o
|
|
}
|
|
|
|
type Stats struct {
|
|
Submitted uint32
|
|
Visible uint32
|
|
Culled uint32
|
|
Dropped uint32
|
|
ZClamped uint32
|
|
Batches uint32
|
|
DrawCalls uint32
|
|
Instances uint32
|
|
TexSwitches uint32
|
|
DebugVerts uint32
|
|
InstanceBytes uint32
|
|
MeshDraws uint32
|
|
MeshCulled uint32
|
|
Sprites3D uint32
|
|
Sprite3DCulled uint32
|
|
CPURenderNS uint64
|
|
GPUNS uint64
|
|
GPUValid bool
|
|
Views uint32
|
|
}
|
|
|
|
type Renderer struct {
|
|
dev gpu.Device
|
|
caps gpu.Capabilities
|
|
log *log.ChannelLogger
|
|
files vfs.FS
|
|
opt Options
|
|
up *texture.Uploader
|
|
samplers map[gpu.SamplerDesc]gpu.SamplerHandle
|
|
batcher *sprite.Batcher
|
|
sprites3d *sprite.Batcher3D
|
|
meshes *mesh.Batcher
|
|
dbg *debug.Draw
|
|
font *text.BitmapFont
|
|
white texture.Region
|
|
spriteShader gpu.ShaderHandle
|
|
sprite3dShader gpu.ShaderHandle
|
|
meshShader gpu.ShaderHandle
|
|
debugShader gpu.ShaderHandle
|
|
spritePipes [blendModes]gpu.PipelineHandle
|
|
sprite3dPipes [blendModes]gpu.PipelineHandle
|
|
meshPipes [blendModes][2]gpu.PipelineHandle
|
|
debugPipe gpu.PipelineHandle
|
|
debugDepthPipe gpu.PipelineHandle
|
|
ub gpu.BufferHandle
|
|
ubDraw gpu.BufferHandle
|
|
ubBuf []byte
|
|
drawBuf []byte
|
|
prof *gpuprof.Profiler
|
|
passes []PassDesc
|
|
order []PassDesc
|
|
frozen bool
|
|
frame gpu.Frame
|
|
fbW, fbH int
|
|
fbScale float32
|
|
vf ViewFrame
|
|
stats Stats
|
|
textBuf []sprite.Draw
|
|
passCtx PassContext
|
|
defSampler gpu.SamplerHandle
|
|
warned bool
|
|
uiFont *text.Font
|
|
depthSeen []gpu.TextureHandle
|
|
tracer *trace.Tracer
|
|
passZone []trace.ZoneID
|
|
}
|
|
|
|
func New(dev gpu.Device, caps gpu.Capabilities, files vfs.FS, lg *log.Logger, opt Options) (*Renderer, error) {
|
|
if lg == nil {
|
|
lg = log.Nop()
|
|
}
|
|
opt = opt.withDefaults()
|
|
r := &Renderer{
|
|
dev: dev,
|
|
caps: caps,
|
|
log: lg.With(log.Render),
|
|
files: files,
|
|
opt: opt,
|
|
up: texture.NewUploader(dev),
|
|
samplers: make(map[gpu.SamplerDesc]gpu.SamplerHandle, 8),
|
|
ubBuf: make([]byte, uniformSize),
|
|
drawBuf: make([]byte, drawUniformSize),
|
|
textBuf: make([]sprite.Draw, 0, 256),
|
|
meshes: mesh.NewBatcher(opt.MaxMeshes),
|
|
}
|
|
if err := r.initPipelines(); err != nil {
|
|
r.Close()
|
|
return nil, err
|
|
}
|
|
b, err := sprite.NewBatcher(dev, opt.MaxSprites)
|
|
if err != nil {
|
|
r.Close()
|
|
return nil, err
|
|
}
|
|
r.batcher = b
|
|
b3, err := sprite.NewBatcher3D(dev, opt.MaxSprites3D)
|
|
if err != nil {
|
|
r.Close()
|
|
return nil, err
|
|
}
|
|
r.sprites3d = b3
|
|
d, err := debug.New(dev, opt.MaxDebugVerts)
|
|
if err != nil {
|
|
r.Close()
|
|
return nil, err
|
|
}
|
|
r.dbg = d
|
|
r.defSampler = r.Sampler(gpu.SamplerDesc{})
|
|
whiteTex, err := r.up.Upload(texture.Solid(2, 2, mathx.Color{R: 1, G: 1, B: 1, A: 1}), texture.UploadOptions{Label: "render.white"})
|
|
if err != nil {
|
|
r.Close()
|
|
return nil, err
|
|
}
|
|
r.white = texture.Whole(whiteTex, 2, 2).Sub(0, 0, 1, 1)
|
|
if f, err := text.LoadBitmap(files, r.up, "engine://fonts/debug.png", "engine://fonts/debug.json"); err != nil {
|
|
r.log.Warn("debug font is unavailable, text rendering disabled", log.F("err", err.Error()))
|
|
} else {
|
|
r.font = f
|
|
}
|
|
r.prof = gpuprof.New(dev, caps)
|
|
r.passes = []PassDesc{
|
|
{ID: "mesh_opaque", Pass: meshOpaquePass{}},
|
|
{ID: "mesh_blend", After: []string{"mesh_opaque"}, Pass: meshBlendPass{}},
|
|
{ID: "sprites3d", After: []string{"mesh_blend"}, Pass: sprite3DPass{}},
|
|
{ID: "sprites", After: []string{"sprites3d"}, Pass: spritePass{}},
|
|
{ID: "debug", After: []string{"sprites"}, Pass: debugPass{}},
|
|
}
|
|
r.vf.r = r
|
|
return r, nil
|
|
}
|
|
|
|
const (
|
|
shaderDir = "engine://shaders/"
|
|
shaderDirES = "engine://shaders/es/"
|
|
)
|
|
|
|
func (r *Renderer) loadShader(label, vert, frag string) (gpu.ShaderHandle, error) {
|
|
dir := shaderDir
|
|
if r.caps.ESSL {
|
|
dir = shaderDirES
|
|
}
|
|
vsrc, err := r.files.ReadFile(dir + vert)
|
|
if err != nil {
|
|
return gpu.ShaderHandle{}, err
|
|
}
|
|
fsrc, err := r.files.ReadFile(dir + frag)
|
|
if err != nil {
|
|
return gpu.ShaderHandle{}, err
|
|
}
|
|
return r.dev.CreateShader(gpu.ShaderDesc{
|
|
Label: label,
|
|
Sources: []gpu.ShaderSource{
|
|
{Stage: gpu.StageVertex, Source: string(vsrc)},
|
|
{Stage: gpu.StageFragment, Source: string(fsrc)},
|
|
},
|
|
})
|
|
}
|
|
|
|
func (r *Renderer) initPipelines() error {
|
|
var err error
|
|
if r.spriteShader, err = r.loadShader("sprite", "sprite.vert", "sprite.frag"); err != nil {
|
|
return err
|
|
}
|
|
if r.sprite3dShader, err = r.loadShader("sprite3d", "sprite3d.vert", "sprite.frag"); err != nil {
|
|
return err
|
|
}
|
|
if r.meshShader, err = r.loadShader("mesh", "mesh.vert", "mesh.frag"); err != nil {
|
|
return err
|
|
}
|
|
if r.debugShader, err = r.loadShader("debug", "debug.vert", "debug.frag"); err != nil {
|
|
return err
|
|
}
|
|
for m := sprite.BlendMode(0); m < blendModes; m++ {
|
|
if r.spritePipes[m], err = r.dev.CreatePipeline(gpu.PipelineDesc{
|
|
Shader: r.spriteShader,
|
|
Layout: sprite.Layout(),
|
|
Blend: sprite.BlendState(m),
|
|
Primitive: gpu.PrimTriangleStrip,
|
|
Label: "sprite",
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if r.sprite3dPipes[m], err = r.dev.CreatePipeline(gpu.PipelineDesc{
|
|
Shader: r.sprite3dShader,
|
|
Layout: sprite.Layout3D(),
|
|
Blend: sprite.BlendState(m),
|
|
Depth: gpu.DepthState{Test: true, Compare: gpu.CompareLessEqual},
|
|
Primitive: gpu.PrimTriangleStrip,
|
|
Label: "sprite3d",
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
for ds := 0; ds < 2; ds++ {
|
|
cull := gpu.CullBack
|
|
if ds == 1 {
|
|
cull = gpu.CullNone
|
|
}
|
|
if r.meshPipes[m][ds], err = r.dev.CreatePipeline(gpu.PipelineDesc{
|
|
Shader: r.meshShader,
|
|
Layout: mesh.Layout(),
|
|
Blend: sprite.BlendState(m),
|
|
Depth: gpu.DepthState{Test: true, Write: m == sprite.BlendOpaque, Compare: gpu.CompareLess},
|
|
Cull: cull,
|
|
Primitive: gpu.PrimTriangles,
|
|
Label: "mesh",
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if r.debugPipe, err = r.dev.CreatePipeline(gpu.PipelineDesc{
|
|
Shader: r.debugShader,
|
|
Layout: debug.Layout(),
|
|
Blend: gpu.BlendPremultiplied(),
|
|
Primitive: gpu.PrimLines,
|
|
Label: "debug",
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if r.debugDepthPipe, err = r.dev.CreatePipeline(gpu.PipelineDesc{
|
|
Shader: r.debugShader,
|
|
Layout: debug.Layout(),
|
|
Blend: gpu.BlendPremultiplied(),
|
|
Depth: gpu.DepthState{Test: true, Compare: gpu.CompareLessEqual},
|
|
Primitive: gpu.PrimLines,
|
|
Label: "debug.depth",
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if r.ub, err = r.dev.CreateBuffer(gpu.BufferDesc{Usage: gpu.BufferUniform, Size: uniformSize, Dynamic: true, Label: "render.viewproj"}); err != nil {
|
|
return err
|
|
}
|
|
r.ubDraw, err = r.dev.CreateBuffer(gpu.BufferDesc{Usage: gpu.BufferUniform, Size: drawUniformSize, Dynamic: true, Label: "render.draw"})
|
|
return err
|
|
}
|
|
|
|
func (r *Renderer) UIFont() *text.Font {
|
|
if r.uiFont == nil && r.font != nil {
|
|
r.uiFont = r.font.Font()
|
|
}
|
|
return r.uiFont
|
|
}
|
|
|
|
func putF32(b []byte, v float32) {
|
|
binary.LittleEndian.PutUint32(b, math.Float32bits(v))
|
|
}
|
|
|
|
func (r *Renderer) setUniform(p gpu.Pass, m mathx.Mat4) {
|
|
for i, v := range m {
|
|
putF32(r.ubBuf[i*4:], v)
|
|
}
|
|
r.dev.UpdateBuffer(r.ub, 0, r.ubBuf)
|
|
p.SetUniformBuffer(0, r.ub)
|
|
}
|
|
|
|
func (r *Renderer) Textures() *texture.Uploader {
|
|
return r.up
|
|
}
|
|
|
|
func (r *Renderer) Device() gpu.Device {
|
|
return r.dev
|
|
}
|
|
|
|
func (r *Renderer) White() texture.Region {
|
|
if r.font != nil && !r.font.White.IsZero() {
|
|
return r.font.White
|
|
}
|
|
return r.white
|
|
}
|
|
|
|
func (r *Renderer) DebugFont() *text.BitmapFont {
|
|
return r.font
|
|
}
|
|
|
|
func (r *Renderer) Caps() gpu.Capabilities {
|
|
return r.caps
|
|
}
|
|
|
|
func (r *Renderer) Sampler(d gpu.SamplerDesc) gpu.SamplerHandle {
|
|
if h, ok := r.samplers[d]; ok {
|
|
return h
|
|
}
|
|
h, err := r.dev.CreateSampler(d)
|
|
if err != nil {
|
|
r.log.Error("sampler creation failed", log.F("err", err.Error()))
|
|
return gpu.SamplerHandle{}
|
|
}
|
|
r.samplers[d] = h
|
|
return h
|
|
}
|
|
|
|
func (r *Renderer) SetTracer(t *trace.Tracer) {
|
|
r.tracer = t
|
|
}
|
|
|
|
func (r *Renderer) AddPass(d PassDesc) error {
|
|
if r.frozen {
|
|
return errs.New(errs.EngineFrozen, "render passes are frozen after the first frame", log.F("pass", d.ID))
|
|
}
|
|
r.passes = append(r.passes, d)
|
|
return nil
|
|
}
|
|
|
|
func (r *Renderer) BeginFrame(f gpu.Frame, pixelW, pixelH int, pixelScale float32) error {
|
|
if !r.frozen {
|
|
order, err := sortPasses(r.passes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.order = order
|
|
r.passZone = make([]trace.ZoneID, len(order))
|
|
for i, d := range order {
|
|
r.passZone[i] = r.tracer.Zone("render." + d.ID)
|
|
}
|
|
r.frozen = true
|
|
}
|
|
r.frame = f
|
|
r.fbW, r.fbH = pixelW, pixelH
|
|
r.fbScale = pixelScale
|
|
r.stats = Stats{}
|
|
r.depthSeen = r.depthSeen[:0]
|
|
r.up.Drain(r.opt.UploadBudget)
|
|
if f != nil {
|
|
r.prof.Collect(f)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Renderer) firstDepthUse(t gpu.TextureHandle) bool {
|
|
for _, s := range r.depthSeen {
|
|
if s == t {
|
|
return false
|
|
}
|
|
}
|
|
r.depthSeen = append(r.depthSeen, t)
|
|
return true
|
|
}
|
|
|
|
func (r *Renderer) framebuffer() (int, int) {
|
|
return r.fbW, r.fbH
|
|
}
|
|
|
|
func (r *Renderer) View(v view.View) *ViewFrame {
|
|
f := &r.vf
|
|
f.reset(v)
|
|
r.stats.Views++
|
|
if r.frame == nil {
|
|
return f
|
|
}
|
|
fv := &f.view
|
|
depth := gpu.LoadKeep
|
|
if fv.UsesDepth() {
|
|
target := fv.Target
|
|
if !fv.DepthTarget.IsZero() {
|
|
target = fv.DepthTarget
|
|
}
|
|
if r.firstDepthUse(target) || fv.DoClear {
|
|
depth = gpu.LoadClear
|
|
}
|
|
}
|
|
f.pass = r.frame.BeginPass(gpu.PassDesc{
|
|
Target: fv.Target,
|
|
DepthTarget: fv.DepthTarget,
|
|
ClearColor: fv.Clear,
|
|
ClearDepth: 1,
|
|
Color: clearAction(fv.DoClear),
|
|
Depth: depth,
|
|
FlipWinding: fv.Flipped(r.caps),
|
|
Label: fv.Label,
|
|
})
|
|
f.pass.SetViewport(fv.LetterboxViewport())
|
|
if r.stats.Views == 1 {
|
|
r.prof.Begin(f.pass)
|
|
}
|
|
return f
|
|
}
|
|
|
|
func clearAction(do bool) gpu.LoadAction {
|
|
if do {
|
|
return gpu.LoadClear
|
|
}
|
|
return gpu.LoadKeep
|
|
}
|
|
|
|
func (r *Renderer) EndFrame() Stats {
|
|
if r.frame != nil {
|
|
fs := r.frame.Stats()
|
|
r.stats.DrawCalls = fs.DrawCalls
|
|
r.stats.TexSwitches = fs.TexSwitches
|
|
}
|
|
if ns, ok := r.prof.LastNS(); ok {
|
|
r.stats.GPUNS = ns
|
|
r.stats.GPUValid = true
|
|
}
|
|
r.frame = nil
|
|
return r.stats
|
|
}
|
|
|
|
func (r *Renderer) destroyPipe(p gpu.PipelineHandle) {
|
|
if !p.IsZero() {
|
|
r.dev.DestroyPipeline(p)
|
|
}
|
|
}
|
|
|
|
func (r *Renderer) destroyShader(s gpu.ShaderHandle) {
|
|
if !s.IsZero() {
|
|
r.dev.DestroyShader(s)
|
|
}
|
|
}
|
|
|
|
func (r *Renderer) Close() {
|
|
if r.prof != nil {
|
|
r.prof.Close()
|
|
}
|
|
if r.dbg != nil {
|
|
r.dbg.Close()
|
|
}
|
|
if r.batcher != nil {
|
|
r.batcher.Close()
|
|
}
|
|
if r.sprites3d != nil {
|
|
r.sprites3d.Close()
|
|
}
|
|
if !r.ub.IsZero() {
|
|
r.dev.DestroyBuffer(r.ub)
|
|
}
|
|
if !r.ubDraw.IsZero() {
|
|
r.dev.DestroyBuffer(r.ubDraw)
|
|
}
|
|
for m := range r.spritePipes {
|
|
r.destroyPipe(r.spritePipes[m])
|
|
r.destroyPipe(r.sprite3dPipes[m])
|
|
r.destroyPipe(r.meshPipes[m][0])
|
|
r.destroyPipe(r.meshPipes[m][1])
|
|
}
|
|
r.destroyPipe(r.debugPipe)
|
|
r.destroyPipe(r.debugDepthPipe)
|
|
r.destroyShader(r.spriteShader)
|
|
r.destroyShader(r.sprite3dShader)
|
|
r.destroyShader(r.meshShader)
|
|
r.destroyShader(r.debugShader)
|
|
}
|