Files
aego/gpu/webgl2/device.go
T

476 lines
12 KiB
Go

//go:build js
package webgl2
import (
"math/bits"
"reflect"
"strings"
"syscall/js"
"aego/core/errs"
"aego/core/handle"
"aego/core/log"
"aego/gpu"
"aego/gpu/internal/glstate"
)
const (
scratchUnit = glstate.MaxTextureUnits - 1
minScratch = 64 << 10
timerExt = "EXT_disjoint_timer_query_webgl2"
floatExt = "EXT_color_buffer_float"
infoExt = "WEBGL_debug_renderer_info"
loseExt = "WEBGL_lose_context"
)
type Surface interface {
WebGL2() js.Value
}
type Options struct {
Debug bool
Log *log.Logger
}
type fns struct {
ActiveTexture, AttachShader, BeginQuery, BindBuffer, BindBufferBase, BindFramebuffer, BindRenderbuffer js.Value
BindSampler, BindTexture, BindVertexArray, BlendFuncSeparate, BufferData, BufferSubData js.Value
CheckFramebufferStatus, Clear, ClearColor, ClearDepth, CompileShader js.Value
CreateBuffer, CreateFramebuffer, CreateProgram, CreateQuery, CreateRenderbuffer js.Value
CreateSampler, CreateShader, CreateTexture, CreateVertexArray, CullFace js.Value
DeleteBuffer, DeleteFramebuffer, DeleteProgram, DeleteQuery, DeleteRenderbuffer js.Value
DeleteSampler, DeleteShader, DeleteTexture, DeleteVertexArray, DepthFunc, DepthMask, DetachShader js.Value
Disable, DrawArrays, DrawArraysInstanced, DrawBuffers, DrawElements, DrawElementsInstanced, Enable js.Value
EnableVertexAttribArray, EndQuery, FramebufferRenderbuffer, FramebufferTexture2D, FrontFace js.Value
GenerateMipmap, GetActiveUniform, GetActiveUniformBlockName, GetError, GetExtension, GetParameter js.Value
GetProgramInfoLog, GetProgramParameter, GetQueryParameter, GetShaderInfoLog, GetShaderParameter js.Value
GetUniformLocation, IsContextLost, LinkProgram, PixelStorei, ReadBuffer, ReadPixels js.Value
RenderbufferStorage, SamplerParameteri, Scissor, ShaderSource, TexImage2D, TexParameteri js.Value
TexSubImage2D, Uniform1i, UniformBlockBinding, UseProgram, VertexAttribDivisor js.Value
VertexAttribIPointer, VertexAttribPointer, Viewport js.Value
}
func bindAll(ctx js.Value) (fns, error) {
var f fns
v := reflect.ValueOf(&f).Elem()
t := v.Type()
for i := 0; i < t.NumField(); i++ {
n := t.Field(i).Name
name := strings.ToLower(n[:1]) + n[1:]
m := ctx.Get(name)
if m.Type() != js.TypeFunction {
return f, errs.New(errs.GPUInit, "the WebGL2 context has no "+name)
}
v.Field(i).Set(reflect.ValueOf(m.Call("bind", ctx)))
}
return f, nil
}
type objects struct {
vals []js.Value
free []uint32
}
func (o *objects) put(v js.Value) uint32 {
if n := len(o.free); n > 0 {
id := o.free[n-1]
o.free = o.free[:n-1]
o.vals[id] = v
return id
}
if len(o.vals) == 0 {
o.vals = append(o.vals, js.Null())
}
o.vals = append(o.vals, v)
return uint32(len(o.vals) - 1)
}
func (o *objects) get(id uint32) js.Value {
if id == 0 || int(id) >= len(o.vals) {
return js.Null()
}
return o.vals[id]
}
func (o *objects) reuse(id uint32, v js.Value) uint32 {
if id == 0 {
return o.put(v)
}
o.vals[id] = v
return id
}
func (o *objects) drop(id uint32) js.Value {
v := o.get(id)
if id != 0 && int(id) < len(o.vals) {
o.vals[id] = js.Null()
o.free = append(o.free, id)
}
return v
}
type glBuffer struct {
id uint32
usage gpu.BufferUsage
size int
index gpu.IndexFormat
dynamic bool
shadow []byte
}
type glTexture struct {
id uint32
w, h int
info texInfo
mips bool
shadow []byte
}
type glSampler struct {
id uint32
desc gpu.SamplerDesc
}
type glShader struct {
program uint32
desc gpu.ShaderDesc
}
type glPipeline struct {
vao uint32
program uint32
prim gpu.Primitive
mode uint32
attrs []glstate.Attrib
divisor []uint32
used [gpu.MaxVertexSlots]bool
bind glstate.VertexBinding
blend glstate.Blend
depth glstate.Depth
cull glstate.Cull
}
type resourceKind uint8
const (
kindBuffer resourceKind = iota
kindTexture
kindSampler
kindShader
kindPipeline
kindQuery
)
type pendingDestroy struct {
kind resourceKind
index, gen uint32
}
type Device struct {
log *log.ChannelLogger
debug bool
caps gpu.Capabilities
ctx js.Value
canvas js.Value
f fns
obj objects
cache *glstate.Cache
buffers *handle.Arena[glBuffer]
textures *handle.Arena[glTexture]
samplers *handle.Arena[glSampler]
shaders *handle.Arena[glShader]
pipelines *handle.Arena[glPipeline]
queries *handle.Arena[glQuery]
fbos map[fboKey]uint32
frame frame
pass pass
inFrame bool
window bool
activeQuery uint32
pending []pendingDestroy
fbW, fbH int
bb backbuffer
out output
u8, f32 js.Value
buf js.Value
bufN int
drawColor js.Value
drawNone js.Value
loseCtx js.Value
lost bool
restores int
calls uint64
mark uint64
lastCalls uint64
onLost js.Func
onRestored js.Func
watching bool
}
func newArena[T any](l *log.Logger, kind string, capacity int) *handle.Arena[T] {
a := handle.NewArena[T](capacity)
cl := l.With(log.GPU)
a.OnGrow = func(from, to int) {
cl.Warn("resource arena grows", log.F("kind", kind), log.F("from", from), log.F("to", to))
}
return a
}
func New(s Surface, opts Options) (*Device, error) {
ctx := s.WebGL2()
if !ctx.Truthy() {
return nil, errs.New(errs.GPUInit, "the window has no WebGL2 context")
}
f, err := bindAll(ctx)
if err != nil {
return nil, err
}
logger := opts.Log
if logger == nil {
logger = log.Nop()
}
g := js.Global()
d := &Device{
log: logger.With(log.GPU),
debug: opts.Debug,
ctx: ctx,
canvas: ctx.Get("canvas"),
f: f,
buffers: newArena[glBuffer](logger, "buffer", 256),
textures: newArena[glTexture](logger, "texture", 128),
samplers: newArena[glSampler](logger, "sampler", 16),
shaders: newArena[glShader](logger, "shader", 32),
pipelines: newArena[glPipeline](logger, "pipeline", 32),
queries: newArena[glQuery](logger, "query", 16),
fbos: make(map[fboKey]uint32, 16),
pending: make([]pendingDestroy, 0, 64),
u8: g.Get("Uint8Array"),
f32: g.Get("Float32Array"),
drawColor: js.ValueOf([]any{glColorAttachment0}),
drawNone: js.ValueOf([]any{glNone}),
}
d.frame.d = d
d.pass.d = d
d.cache = glstate.New(api{d: d})
d.queryCaps()
d.initState()
if err := d.initOutput(); err != nil {
return nil, err
}
d.watchLoss()
return d, nil
}
func num(v js.Value) int {
if v.Type() == js.TypeNumber {
return v.Int()
}
return 0
}
func (d *Device) call(f js.Value, args ...any) js.Value {
d.calls++
return f.Invoke(args...)
}
func (d *Device) param(p int) js.Value {
return d.call(d.f.GetParameter, p)
}
func (d *Device) scratch(n int) js.Value {
if n > d.bufN {
c := 1 << bits.Len(uint(max(n, minScratch)-1))
d.buf = d.u8.New(c)
d.bufN = c
}
return d.buf
}
func (d *Device) bytes(b []byte) js.Value {
v := d.scratch(len(b))
js.CopyBytesToJS(v, b)
return v
}
func (d *Device) view(i texInfo, data []byte) js.Value {
u := d.bytes(data)
if i.xtype == glFloat {
return d.f32.New(u.Get("buffer"), 0, len(data)/4)
}
return u
}
func (d *Device) queryCaps() {
c := &d.caps
c.Vendor = d.param(glVendor).String()
c.Renderer = d.param(glRenderer).String()
if d.call(d.f.GetExtension, infoExt).Truthy() {
if v := d.param(glUnmaskedVendor); v.Type() == js.TypeString {
c.Vendor = v.String()
}
if v := d.param(glUnmaskedRenderer); v.Type() == js.TypeString {
c.Renderer = v.String()
}
}
c.Version = d.param(glVersion).String()
c.GLSL = d.param(glShadingLanguageVersion).String()
c.MaxTextureSize = num(d.param(glMaxTextureSize))
c.MaxTextureUnits = min(num(d.param(glMaxCombinedTextureImageUnits)), scratchUnit)
c.MaxSamples = num(d.param(glMaxSamples))
c.Instancing = true
c.TextureArrays = true
c.SRGB = true
c.ESSL = true
c.OriginBottomLeft = true
c.TimerQueries = d.call(d.f.GetExtension, timerExt).Truthy()
d.call(d.f.GetExtension, floatExt)
d.loseCtx = d.call(d.f.GetExtension, loseExt)
}
func (d *Device) initState() {
d.call(d.f.PixelStorei, glUnpackAlignment, 1)
d.call(d.f.PixelStorei, glPackAlignment, 1)
}
func (d *Device) checkError(op, label string) error {
if !d.debug {
return nil
}
var codes []string
for i := 0; i < 8; i++ {
e := uint32(num(d.call(d.f.GetError)))
if e == glNoError {
break
}
codes = append(codes, glErrorName(e))
}
if len(codes) == 0 {
return nil
}
return errs.New(errs.GPUResource, op+" failed", log.F("label", label), log.F("gl", strings.Join(codes, ",")))
}
func (d *Device) Caps() gpu.Capabilities {
return d.caps
}
func (d *Device) Calls() uint64 {
return d.lastCalls
}
func (d *Device) Lost() bool {
return d.lost
}
func (d *Device) Restores() int {
return d.restores
}
func (d *Device) SimulateLoss(lose bool) bool {
if !d.loseCtx.Truthy() {
return false
}
if lose {
d.loseCtx.Call("loseContext")
} else {
d.loseCtx.Call("restoreContext")
}
return true
}
func (d *Device) watchLoss() {
if !d.canvas.Truthy() {
return
}
d.onLost = js.FuncOf(func(_ js.Value, a []js.Value) any {
if len(a) > 0 {
a[0].Call("preventDefault")
}
d.lost = true
d.log.Warn("WebGL context lost, rendering stops until the browser restores it")
return nil
})
d.onRestored = js.FuncOf(func(js.Value, []js.Value) any {
d.restore()
return nil
})
d.canvas.Call("addEventListener", "webglcontextlost", d.onLost)
d.canvas.Call("addEventListener", "webglcontextrestored", d.onRestored)
d.watching = true
}
func (d *Device) bindScratch(id uint32) {
d.call(d.f.ActiveTexture, glTexture0+scratchUnit)
d.cache.BindTexture(scratchUnit, id)
}
func (d *Device) BeginFrame(pixelW, pixelH int) gpu.Frame {
d.fbW, d.fbH = pixelW, pixelH
d.frame.stats = gpu.FrameStats{}
d.inFrame = true
d.window = false
d.mark = d.calls
return &d.frame
}
func (d *Device) EndFrame() {
if d.pass.active {
d.log.Warn("render pass was not ended before EndFrame")
d.pass.active = false
}
if d.window && !d.lost {
d.present()
}
d.inFrame = false
for _, p := range d.pending {
d.destroyNow(p.kind, p.index, p.gen)
}
d.pending = d.pending[:0]
d.lastCalls = d.calls - d.mark
}
func (d *Device) Close() {
d.pending = d.pending[:0]
d.inFrame = false
d.cache.BindVertexArray(0)
d.cache.UseProgram(0)
d.cache.BindFramebuffer(0)
d.deleteFramebuffers()
d.freeBackbuffer()
d.pipelines.Each(func(_ handle.Handle[glPipeline], p *glPipeline) {
d.call(d.f.DeleteVertexArray, d.obj.drop(p.vao))
})
d.shaders.Each(func(_ handle.Handle[glShader], s *glShader) {
d.call(d.f.DeleteProgram, d.obj.drop(s.program))
})
d.samplers.Each(func(_ handle.Handle[glSampler], s *glSampler) {
d.call(d.f.DeleteSampler, d.obj.drop(s.id))
})
d.textures.Each(func(_ handle.Handle[glTexture], t *glTexture) {
d.call(d.f.DeleteTexture, d.obj.drop(t.id))
})
d.buffers.Each(func(_ handle.Handle[glBuffer], b *glBuffer) {
d.call(d.f.DeleteBuffer, d.obj.drop(b.id))
})
d.queries.Each(func(_ handle.Handle[glQuery], q *glQuery) {
d.call(d.f.DeleteQuery, d.obj.drop(q.id))
})
d.freeOutput()
d.pipelines.Clear()
d.shaders.Clear()
d.samplers.Clear()
d.textures.Clear()
d.buffers.Clear()
d.queries.Clear()
d.activeQuery = 0
d.cache.Invalidate()
if d.watching {
d.canvas.Call("removeEventListener", "webglcontextlost", d.onLost)
d.canvas.Call("removeEventListener", "webglcontextrestored", d.onRestored)
d.onLost.Release()
d.onRestored.Release()
d.watching = false
}
}