// SPDX-License-Identifier: Unlicense OR MIT // OpenGL (WebGL2) backend for the Device interface. // WASM-only: exclusive context, gles=true, glver=[3,0], no vertex arrays. package gio import ( "bytes" "errors" "fmt" "image" "math/bits" "unsafe" ) // Backend implements Device. type Backend struct { funcs *Functions clear bool glstate glState state backendState feats Caps floatTriple textureTriple alphaTriple textureTriple srgbaTriple textureTriple storage [storageBindings]*oglBuffer outputFBO GLFramebuffer sRGBFBO *SRGBFBO } type glState struct { drawFBO Framebuffer readFBO Framebuffer vertAttribs [5]struct { obj GLBuffer enabled bool size int typ Enum normalized bool stride int offset int } prog GLProgram texUnits struct { active Enum binds [2]GLTexture } arrayBuf GLBuffer elemBuf GLBuffer uniBuf GLBuffer uniBufs [2]GLBuffer srgb bool blend struct { enable bool srcRGB, dstRGB Enum srcA, dstA Enum } clearColor [4]float32 viewport [4]int unpack_row_length int pack_row_length int } // glState stores GL-level framebuffer handles - distinct from Texture (driver interface). // The Framebuffer alias here refers to GLFramebuffer. type Framebuffer = GLFramebuffer type backendState struct { pipeline *oglPipeline buffer oglBufferBinding } type oglBufferBinding struct { obj GLBuffer offset int } type ogltimer struct{} type oglTexture struct { backend *Backend obj GLTexture fbo GLFramebuffer hasFBO bool triple textureTriple width int height int mipmap bool bindings BufferBinding foreign bool } type oglPipeline struct { prog *oglProgram inputs []ShaderInputLocation layout VertexLayout blend BlendDesc topology Topology } type oglBuffer struct { backend *Backend hasBuffer bool obj GLBuffer typ BufferBinding size int immutable bool data []byte } type oglShader struct { backend *Backend obj GLShader src ShaderSources } type oglProgram struct { backend *Backend obj GLProgram vertUniforms oglUniforms fragUniforms oglUniforms } type oglUniforms struct { locs []oglUniformLocation size int } type oglUniformLocation struct { uniform GLUniform offset int typ ShaderDataType size int } type textureTriple struct { internalFormat Enum format Enum typ Enum } const storageBindings = 32 func newOpenGLDevice(api OpenGL) (Device, error) { f, err := NewFunctions(api.Context) if err != nil { return nil, err } ver := [2]int{3, 0} floatTriple, ffboErr := floatTripleFor(f, ver) srgbaTriple := textureTriple{SRGB8_ALPHA8, Enum(GL_RGBA), Enum(UNSIGNED_BYTE)} b := &Backend{ funcs: f, floatTriple: floatTriple, alphaTriple: alphaTripleFor(), srgbaTriple: srgbaTriple, } b.feats.BottomLeftOrigin = true b.feats.Features |= FeatureSRGB if ffboErr == nil { b.feats.Features |= FeatureFloatRenderTargets } b.feats.MaxTextureSize = f.GetInteger(MAX_TEXTURE_SIZE) return b, nil } func (b *Backend) BeginFrame(target RenderTarget, clear bool, viewport image.Point) Texture { b.clear = clear b.state = backendState{} var renderFBO GLFramebuffer if target != nil { switch t := target.(type) { case OpenGLRenderTarget: renderFBO = GLFramebuffer(t) case *oglTexture: renderFBO = t.ensureFBO() default: panic(fmt.Errorf("opengl: invalid render target type: %T", target)) } } b.outputFBO = renderFBO b.glstate.bindFramebuffer(b.funcs, FRAMEBUFFER, renderFBO) // WebGL2 canvas is always linear; always use SRGBFBO for gamma correctness. if viewport != (image.Point{}) { if b.sRGBFBO == nil { sfbo, err := NewSRGBFBO(b.funcs, &b.glstate) if err != nil { panic(err) } b.sRGBFBO = sfbo } if err := b.sRGBFBO.Refresh(viewport); err != nil { panic(err) } renderFBO = b.sRGBFBO.fbo } else if b.sRGBFBO != nil { b.sRGBFBO.Release() b.sRGBFBO = nil } b.glstate.bindFramebuffer(b.funcs, FRAMEBUFFER, renderFBO) if b.sRGBFBO != nil && !clear { b.clearOutput(0, 0, 0, 0) } return &oglTexture{backend: b, fbo: renderFBO, hasFBO: true, foreign: true} } func (b *Backend) EndFrame() { if b.sRGBFBO != nil { b.glstate.bindFramebuffer(b.funcs, FRAMEBUFFER, b.outputFBO) if b.clear { b.SetBlend(false) } else { b.BlendFunc(BlendFactorOne, BlendFactorOneMinusSrcAlpha) b.SetBlend(true) } b.sRGBFBO.Blit() } } func (b *Backend) Caps() Caps { return b.feats } func (b *Backend) NewTimer() Timer { return &ogltimer{} } func (b *Backend) IsTimeContinuous() bool { return false } func (t *ogltimer) Begin() {} func (t *ogltimer) End() {} func (t *ogltimer) Release() {} func (t *oglTexture) ensureFBO() GLFramebuffer { if t.hasFBO { return t.fbo } b := t.backend oldFBO := b.glstate.drawFBO defer func() { b.glstate.bindFramebuffer(b.funcs, FRAMEBUFFER, oldFBO) }() fb := b.funcs.CreateFramebuffer() b.glstate.bindFramebuffer(b.funcs, FRAMEBUFFER, fb) b.funcs.FramebufferTexture2D(FRAMEBUFFER, COLOR_ATTACHMENT0, TEXTURE_2D, t.obj, 0) if st := b.funcs.CheckFramebufferStatus(FRAMEBUFFER); st != FRAMEBUFFER_COMPLETE { b.funcs.DeleteFramebuffer(fb) panic(fmt.Errorf("incomplete framebuffer, status = 0x%x", st)) } t.fbo = fb t.hasFBO = true return fb } func (b *Backend) NewTexture(format TextureFormat, width, height int, minFilter, magFilter TextureFilter, binding BufferBinding) (Texture, error) { tex := &oglTexture{backend: b, obj: b.funcs.CreateTexture(), width: width, height: height, bindings: binding} switch format { case TextureFormatFloat: tex.triple = b.floatTriple case TextureFormatSRGBA: tex.triple = b.srgbaTriple case TextureFormatRGBA8: tex.triple = textureTriple{RGBA8, GL_RGBA, UNSIGNED_BYTE} default: return nil, errors.New("unsupported texture format") } b.BindTexture(0, tex) min, mipmap := toTexFilter(minFilter) mag, _ := toTexFilter(magFilter) tex.mipmap = mipmap b.funcs.TexParameteri(TEXTURE_2D, TEXTURE_MAG_FILTER, mag) b.funcs.TexParameteri(TEXTURE_2D, TEXTURE_MIN_FILTER, min) b.funcs.TexParameteri(TEXTURE_2D, TEXTURE_WRAP_S, int(CLAMP_TO_EDGE)) b.funcs.TexParameteri(TEXTURE_2D, TEXTURE_WRAP_T, int(CLAMP_TO_EDGE)) if mipmap { dim := width if height > dim { dim = height } log2 := 32 - bits.LeadingZeros32(uint32(dim)) - 1 nmipmaps := log2 + 1 b.funcs.TexStorage2D(TEXTURE_2D, nmipmaps, tex.triple.internalFormat, width, height) } else { b.funcs.TexImage2D(TEXTURE_2D, 0, tex.triple.internalFormat, width, height, tex.triple.format, tex.triple.typ) } return tex, nil } func (b *Backend) NewBuffer(typ BufferBinding, size int) (Buffer, error) { buf := &oglBuffer{backend: b, typ: typ, size: size} if typ&BufferBindingUniforms != 0 { if typ != BufferBindingUniforms { return nil, errors.New("uniforms buffers cannot be bound as anything else") } buf.data = []byte{:size} } if typ&^BufferBindingUniforms != 0 { buf.hasBuffer = true buf.obj = b.funcs.CreateBuffer() firstBinding := firstBufferType(typ) b.glstate.bindBuffer(b.funcs, firstBinding, buf.obj) b.funcs.BufferData(firstBinding, size, DYNAMIC_DRAW, nil) } return buf, nil } func (b *Backend) NewImmutableBuffer(typ BufferBinding, data []byte) (Buffer, error) { obj := b.funcs.CreateBuffer() buf := &oglBuffer{backend: b, obj: obj, typ: typ, size: len(data), hasBuffer: true} firstBinding := firstBufferType(typ) b.glstate.bindBuffer(b.funcs, firstBinding, buf.obj) b.funcs.BufferData(firstBinding, len(data), STATIC_DRAW, data) buf.immutable = true return buf, nil } func (b *Backend) Release() { if b.sRGBFBO != nil { b.sRGBFBO.Release() } *b = Backend{} } func (b *Backend) DispatchCompute(x, y, z int) { panic("opengl: compute not supported in WebGL2") } func (b *Backend) BindImageTexture(unit int, tex Texture) { panic("opengl: compute not supported in WebGL2") } func (b *Backend) BlendFunc(sfactor, dfactor BlendFactor) { src, dst := toGLBlendFactor(sfactor), toGLBlendFactor(dfactor) b.glstate.setBlendFuncSeparate(b.funcs, src, dst, src, dst) } func toGLBlendFactor(f BlendFactor) Enum { switch f { case BlendFactorOne: return ONE case BlendFactorOneMinusSrcAlpha: return ONE_MINUS_SRC_ALPHA case BlendFactorZero: return ZERO case BlendFactorDstColor: return DST_COLOR default: panic("unsupported blend factor") } } func (b *Backend) SetBlend(enable bool) { b.glstate.set(b.funcs, BLEND, enable) } func (b *Backend) DrawElements(off, count int) { b.prepareDraw() byteOff := off * 2 b.funcs.DrawElements(toGLDrawMode(b.state.pipeline.topology), count, UNSIGNED_SHORT, byteOff) } func (b *Backend) DrawArrays(off, count int) { b.prepareDraw() b.funcs.DrawArrays(toGLDrawMode(b.state.pipeline.topology), off, count) } func (b *Backend) prepareDraw() { if b.state.pipeline == nil { return } b.setupVertexArrays() } func toGLDrawMode(mode Topology) Enum { switch mode { case TopologyTriangleStrip: return TRIANGLE_STRIP case TopologyTriangles: return TRIANGLES default: panic("unsupported draw mode") } } func (b *Backend) Viewport(x, y, width, height int) { b.glstate.setViewport(b.funcs, x, y, width, height) } func (b *Backend) clearOutput(colR, colG, colB, colA float32) { b.glstate.setClearColor(b.funcs, colR, colG, colB, colA) b.funcs.Clear(COLOR_BUFFER_BIT) } func (b *Backend) NewComputeProgram(src ShaderSources) (ShaderProgram, error) { return nil, errors.New("opengl: compute not supported") } func (b *Backend) NewVertexShader(src ShaderSources) (VertexShader, error) { sh, err := GLCreateShader(b.funcs, VERTEX_SHADER, src.GLSL100ES) return &oglShader{backend: b, obj: sh, src: src}, err } func (b *Backend) NewFragmentShader(src ShaderSources) (FragmentShader, error) { sh, err := GLCreateShader(b.funcs, FRAGMENT_SHADER, src.GLSL100ES) return &oglShader{backend: b, obj: sh, src: src}, err } func (b *Backend) NewPipeline(desc PipelineDesc) (Pipeline, error) { p, err := b.newProgram(desc) if err != nil { return nil, err } layout := desc.VertexLayout vsrc := desc.VertexShader.(*oglShader).src if len(vsrc.Inputs) != len(layout.Inputs) { return nil, fmt.Errorf("opengl: got %d inputs, expected %d", len(layout.Inputs), len(vsrc.Inputs)) } for i, inp := range vsrc.Inputs { if exp, got := inp.Size, layout.Inputs[i].Size; exp != got { return nil, fmt.Errorf("opengl: data size mismatch for %q: got %d expected %d", inp.Name, got, exp) } } return &oglPipeline{ prog: p, inputs: vsrc.Inputs, layout: layout, blend: desc.BlendDesc, topology: desc.Topology, }, nil } func (b *Backend) newProgram(desc PipelineDesc) (*oglProgram, error) { p := b.funcs.CreateProgram() if !p.Valid() { return nil, errors.New("opengl: glCreateProgram failed") } vsh := desc.VertexShader.(*oglShader) fsh := desc.FragmentShader.(*oglShader) b.funcs.AttachShader(p, vsh.obj) b.funcs.AttachShader(p, fsh.obj) for _, inp := range vsh.src.Inputs { b.funcs.BindAttribLocation(p, GLAttrib(inp.Location), inp.Name) } b.funcs.LinkProgram(p) if b.funcs.GetProgrami(p, LINK_STATUS) == 0 { log := b.funcs.GetProgramInfoLog(p) b.funcs.DeleteProgram(p) return nil, fmt.Errorf("opengl: program link failed: %s", bytes.TrimSpace(log)) } prog := &oglProgram{backend: b, obj: p} b.glstate.useProgram(b.funcs, p) for _, tex := range vsh.src.Textures { u := b.funcs.GetUniformLocation(p, tex.Name) if u.Valid() { b.funcs.Uniform1i(u, tex.Binding) } } for _, tex := range fsh.src.Textures { u := b.funcs.GetUniformLocation(p, tex.Name) if u.Valid() { b.funcs.Uniform1i(u, tex.Binding) } } prog.vertUniforms.setup(b.funcs, p, vsh.src.Uniforms.Size, vsh.src.Uniforms.Locations) prog.fragUniforms.setup(b.funcs, p, fsh.src.Uniforms.Size, fsh.src.Uniforms.Locations) return prog, nil } func (b *Backend) BindStorageBuffer(binding int, buf Buffer) { panic("opengl: compute not supported in WebGL2") } func (b *Backend) BindUniforms(buf Buffer) { bf := buf.(*oglBuffer) if bf.typ&BufferBindingUniforms == 0 { panic("not a uniform buffer") } b.state.pipeline.prog.vertUniforms.update(b.funcs, bf) b.state.pipeline.prog.fragUniforms.update(b.funcs, bf) } func (b *Backend) BindProgram(prog ShaderProgram) { p := prog.(*oglProgram) b.glstate.useProgram(b.funcs, p.obj) } func (s *oglShader) Release() { s.backend.funcs.DeleteShader(s.obj) } func (p *oglProgram) Release() { p.backend.glstate.deleteProgram(p.backend.funcs, p.obj) } func (u *oglUniforms) setup(funcs *Functions, p GLProgram, uniformSize int, uniforms []ShaderUniformLocation) { u.locs = []oglUniformLocation{:len(uniforms)} for i, uniform := range uniforms { loc := funcs.GetUniformLocation(p, uniform.Name) u.locs[i] = oglUniformLocation{uniform: loc, offset: uniform.Offset, typ: uniform.Type, size: uniform.Size} } u.size = uniformSize } func (u *oglUniforms) update(funcs *Functions, buf *oglBuffer) { if buf.size < u.size { panic(fmt.Errorf("uniform buffer too small, got %d need %d", buf.size, u.size)) } data := buf.data for _, ul := range u.locs { if !ul.uniform.Valid() { continue } d := data[ul.offset:] switch { case ul.typ == ShaderDataTypeFloat && ul.size == 1: v := *(*[1]float32)(unsafe.Pointer(&d[0])) funcs.Uniform1f(ul.uniform, v[0]) case ul.typ == ShaderDataTypeFloat && ul.size == 2: v := *(*[2]float32)(unsafe.Pointer(&d[0])) funcs.Uniform2f(ul.uniform, v[0], v[1]) case ul.typ == ShaderDataTypeFloat && ul.size == 3: v := *(*[3]float32)(unsafe.Pointer(&d[0])) funcs.Uniform3f(ul.uniform, v[0], v[1], v[2]) case ul.typ == ShaderDataTypeFloat && ul.size == 4: v := *(*[4]float32)(unsafe.Pointer(&d[0])) funcs.Uniform4f(ul.uniform, v[0], v[1], v[2], v[3]) default: panic("unsupported uniform data type or size") } } } func (buf *oglBuffer) Upload(data []byte) { if buf.immutable { panic("immutable buffer") } if len(data) > buf.size { panic("buffer size overflow") } copy(buf.data, data) if buf.hasBuffer { firstBinding := firstBufferType(buf.typ) buf.backend.glstate.bindBuffer(buf.backend.funcs, firstBinding, buf.obj) if len(data) == buf.size { buf.backend.funcs.BufferData(firstBinding, buf.size, DYNAMIC_DRAW, data) } else { buf.backend.funcs.BufferSubData(firstBinding, 0, data) } } } func (buf *oglBuffer) Download(data []byte) error { if len(data) > buf.size { panic("buffer size overflow") } if !buf.hasBuffer { copy(data, buf.data) return nil } return errors.New("opengl: buffer download not supported on WebGL2") } func (buf *oglBuffer) Release() { if buf.hasBuffer { buf.backend.glstate.deleteBuffer(buf.backend.funcs, buf.obj) buf.hasBuffer = false } } func (b *Backend) BindVertexBuffer(buf Buffer, offset int) { gbuf := buf.(*oglBuffer) if gbuf.typ&BufferBindingVertices == 0 { panic("not a vertex buffer") } b.state.buffer = oglBufferBinding{obj: gbuf.obj, offset: offset} } func (b *Backend) setupVertexArrays() { p := b.state.pipeline inputs := p.inputs if len(inputs) == 0 { return } layout := p.layout const maxAttribs = 5 var enabled [maxAttribs]bool buf := b.state.buffer for i, inp := range inputs { l := layout.Inputs[i] var gltyp Enum switch l.Type { case ShaderDataTypeFloat: gltyp = FLOAT case ShaderDataTypeShort: gltyp = SHORT default: panic("unsupported data type") } if inp.Location < maxAttribs { enabled[inp.Location] = true } b.glstate.vertexAttribPointer(b.funcs, buf.obj, inp.Location, l.Size, gltyp, false, p.layout.Stride, buf.offset+l.Offset) } for i := 0; i < maxAttribs; i++ { b.glstate.setVertexAttribArray(b.funcs, i, enabled[i]) } } func (b *Backend) BindIndexBuffer(buf Buffer) { gbuf := buf.(*oglBuffer) if gbuf.typ&BufferBindingIndices == 0 { panic("not an index buffer") } b.glstate.bindBuffer(b.funcs, ELEMENT_ARRAY_BUFFER, gbuf.obj) } func (b *Backend) CopyTexture(dst Texture, dstOrigin image.Point, src Texture, srcRect image.Rectangle) { const unit = 0 oldTex := b.glstate.texUnits.binds[unit] defer func() { b.glstate.bindGLTexture(b.funcs, unit, oldTex) }() b.glstate.bindGLTexture(b.funcs, unit, dst.(*oglTexture).obj) b.glstate.bindFramebuffer(b.funcs, FRAMEBUFFER, src.(*oglTexture).ensureFBO()) sz := srcRect.Size() b.funcs.CopyTexSubImage2D(TEXTURE_2D, 0, dstOrigin.X, dstOrigin.Y, srcRect.Min.X, srcRect.Min.Y, sz.X, sz.Y) } func (t *oglTexture) ReadPixels(src image.Rectangle, pixels []byte, stride int) error { t.backend.glstate.bindFramebuffer(t.backend.funcs, FRAMEBUFFER, t.ensureFBO()) w, h := src.Dx(), src.Dy() if len(pixels) < w*h*4 { return errors.New("unexpected RGBA size") } rowLen := 0 if n := stride / 4; n != w { rowLen = n } if rowLen == 0 { t.backend.glstate.pixelStorei(t.backend.funcs, PACK_ROW_LENGTH, rowLen) t.backend.funcs.ReadPixels(src.Min.X, src.Min.Y, w, h, GL_RGBA, UNSIGNED_BYTE, pixels) } else { tmp := []byte{:w * h * 4} t.backend.funcs.ReadPixels(src.Min.X, src.Min.Y, w, h, GL_RGBA, UNSIGNED_BYTE, tmp) for y := 0; y < h; y++ { copy(pixels[y*stride:], tmp[y*w*4:]) } } return nil } func (b *Backend) BindPipeline(pl Pipeline) { p := pl.(*oglPipeline) b.state.pipeline = p b.glstate.useProgram(b.funcs, p.prog.obj) b.SetBlend(p.blend.Enable) b.BlendFunc(p.blend.SrcFactor, p.blend.DstFactor) } func (b *Backend) BeginCompute() { panic("opengl: compute not supported") } func (b *Backend) EndCompute() {} func (b *Backend) BeginRenderPass(tex Texture, desc LoadDesc) { fbo := tex.(*oglTexture).ensureFBO() b.glstate.bindFramebuffer(b.funcs, FRAMEBUFFER, fbo) switch desc.Action { case LoadActionClear: c := desc.ClearColor b.clearOutput(c.R, c.G, c.B, c.A) case LoadActionInvalidate: b.funcs.InvalidateFramebuffer(FRAMEBUFFER, COLOR_ATTACHMENT0) } } func (b *Backend) EndRenderPass() {} func (t *oglTexture) ImplementsRenderTarget() {} func (p *oglPipeline) Release() { p.prog.Release() *p = oglPipeline{} } func toTexFilter(f TextureFilter) (int, bool) { switch f { case FilterNearest: return int(NEAREST), false case FilterLinear: return int(LINEAR), false case FilterLinearMipmapLinear: return int(LINEAR_MIPMAP_LINEAR), true default: panic("unsupported texture filter") } } func (b *Backend) PrepareTexture(tex Texture) {} func (b *Backend) BindTexture(unit int, t Texture) { b.glstate.bindGLTexture(b.funcs, unit, t.(*oglTexture).obj) } func (t *oglTexture) Release() { if t.foreign { panic("texture not created by NewTexture") } if t.hasFBO { t.backend.glstate.deleteFramebuffer(t.backend.funcs, t.fbo) } t.backend.glstate.deleteTexture(t.backend.funcs, t.obj) } func (t *oglTexture) Upload(offset, size image.Point, pixels []byte, stride int) { if min := size.X * size.Y * 4; min > len(pixels) { panic(fmt.Errorf("size %d larger than data %d", min, len(pixels))) } t.backend.BindTexture(0, t) rowLen := 0 if n := stride / 4; n != size.X { rowLen = n } t.backend.glstate.pixelStorei(t.backend.funcs, UNPACK_ROW_LENGTH, rowLen) t.backend.funcs.TexSubImage2D(TEXTURE_2D, 0, offset.X, offset.Y, size.X, size.Y, t.triple.format, t.triple.typ, pixels) if t.mipmap { t.backend.funcs.GenerateMipmap(TEXTURE_2D) } } // glState methods func (s *glState) setVertexAttribArray(f *Functions, idx int, enabled bool) { a := &s.vertAttribs[idx] if enabled != a.enabled { if enabled { f.EnableVertexAttribArray(GLAttrib(idx)) } else { f.DisableVertexAttribArray(GLAttrib(idx)) } a.enabled = enabled } } func (s *glState) vertexAttribPointer(f *Functions, buf GLBuffer, idx, size int, typ Enum, normalized bool, stride, offset int) { s.bindBuffer(f, ARRAY_BUFFER, buf) a := &s.vertAttribs[idx] a.obj = buf a.size = size a.typ = typ a.normalized = normalized a.stride = stride a.offset = offset f.VertexAttribPointer(GLAttrib(idx), a.size, a.typ, a.normalized, a.stride, int(a.offset)) } func (s *glState) activeTexture(f *Functions, unit Enum) { if unit != s.texUnits.active { f.ActiveTexture(unit) s.texUnits.active = unit } } func (s *glState) bindGLTexture(f *Functions, unit int, t GLTexture) { s.activeTexture(f, TEXTURE0+Enum(unit)) if !t.Equal(s.texUnits.binds[unit]) { f.BindTexture(TEXTURE_2D, t) s.texUnits.binds[unit] = t } } func (s *glState) deleteFramebuffer(f *Functions, fbo GLFramebuffer) { f.DeleteFramebuffer(fbo) if fbo.Equal(s.drawFBO) { s.drawFBO = GLFramebuffer{} } if fbo.Equal(s.readFBO) { s.readFBO = GLFramebuffer{} } } func (s *glState) deleteBuffer(f *Functions, b GLBuffer) { f.DeleteBuffer(b) if b.Equal(s.arrayBuf) { s.arrayBuf = GLBuffer{} } if b.Equal(s.elemBuf) { s.elemBuf = GLBuffer{} } if b.Equal(s.uniBuf) { s.uniBuf = GLBuffer{} } for i, b2 := range s.uniBufs { if b.Equal(b2) { s.uniBufs[i] = GLBuffer{} } } } func (s *glState) deleteProgram(f *Functions, p GLProgram) { f.DeleteProgram(p) if p.Equal(s.prog) { s.prog = GLProgram{} } } func (s *glState) deleteTexture(f *Functions, t GLTexture) { f.DeleteTexture(t) binds := &s.texUnits.binds for i, obj := range binds { if t.Equal(obj) { binds[i] = GLTexture{} } } } func (s *glState) useProgram(f *Functions, p GLProgram) { if !p.Equal(s.prog) { f.UseProgram(p) s.prog = p } } func (s *glState) bindFramebuffer(f *Functions, target Enum, fbo GLFramebuffer) { switch target { case FRAMEBUFFER: if fbo.Equal(s.drawFBO) && fbo.Equal(s.readFBO) { return } s.drawFBO = fbo s.readFBO = fbo case READ_FRAMEBUFFER: if fbo.Equal(s.readFBO) { return } s.readFBO = fbo case DRAW_FRAMEBUFFER: if fbo.Equal(s.drawFBO) { return } s.drawFBO = fbo default: panic("unknown framebuffer target") } f.BindFramebuffer(target, fbo) } func (s *glState) bindBuffer(f *Functions, target Enum, buf GLBuffer) { switch target { case ARRAY_BUFFER: if buf.Equal(s.arrayBuf) { return } s.arrayBuf = buf case ELEMENT_ARRAY_BUFFER: if buf.Equal(s.elemBuf) { return } s.elemBuf = buf case UNIFORM_BUFFER: if buf.Equal(s.uniBuf) { return } s.uniBuf = buf default: panic("unknown buffer target") } f.BindBuffer(target, buf) } func (s *glState) pixelStorei(f *Functions, pname Enum, val int) { switch pname { case UNPACK_ROW_LENGTH: if val == s.unpack_row_length { return } s.unpack_row_length = val case PACK_ROW_LENGTH: if val == s.pack_row_length { return } s.pack_row_length = val default: panic("unsupported PixelStorei pname") } f.PixelStorei(pname, val) } func (s *glState) setClearColor(f *Functions, r, g, b, a float32) { col := [4]float32{r, g, b, a} if col != s.clearColor { f.ClearColor(r, g, b, a) s.clearColor = col } } func (s *glState) setViewport(f *Functions, x, y, width, height int) { view := [4]int{x, y, width, height} if view != s.viewport { f.Viewport(x, y, width, height) s.viewport = view } } func (s *glState) setBlendFuncSeparate(f *Functions, srcRGB, dstRGB, srcA, dstA Enum) { if srcRGB != s.blend.srcRGB || dstRGB != s.blend.dstRGB || srcA != s.blend.srcA || dstA != s.blend.dstA { s.blend.srcRGB = srcRGB s.blend.dstRGB = dstRGB s.blend.srcA = srcA s.blend.dstA = dstA f.BlendFuncSeparate(srcRGB, dstRGB, srcA, dstA) } } func (s *glState) set(f *Functions, target Enum, enable bool) { switch target { case BLEND: if enable == s.blend.enable { return } s.blend.enable = enable default: panic("unknown enable target") } if enable { f.Enable(target) } else { f.Disable(target) } } func floatTripleFor(f *Functions, ver [2]int) (textureTriple, error) { triples := []textureTriple{ {R16F, Enum(RED), Enum(HALF_FLOAT)}, // WebGL2 + EXT_color_buffer_half_float {RGBA16F, Enum(GL_RGBA), Enum(HALF_FLOAT)}, // RGBA half-float, broader compat {RGBA32F, Enum(GL_RGBA), Enum(FLOAT)}, // RGBA full-float {RGBA8, Enum(GL_RGBA), Enum(UNSIGNED_BYTE)}, // fallback: correct for simple paths } tex := f.CreateTexture() defer f.DeleteTexture(tex) f.BindTexture(TEXTURE_2D, tex) f.TexParameteri(TEXTURE_2D, TEXTURE_WRAP_S, int(CLAMP_TO_EDGE)) f.TexParameteri(TEXTURE_2D, TEXTURE_WRAP_T, int(CLAMP_TO_EDGE)) f.TexParameteri(TEXTURE_2D, TEXTURE_MAG_FILTER, int(NEAREST)) f.TexParameteri(TEXTURE_2D, TEXTURE_MIN_FILTER, int(NEAREST)) fbo := f.CreateFramebuffer() defer f.DeleteFramebuffer(fbo) f.BindFramebuffer(FRAMEBUFFER, fbo) defer f.BindFramebuffer(FRAMEBUFFER, GLFramebuffer{}) for _, tt := range triples { const size = 256 f.TexImage2D(TEXTURE_2D, 0, tt.internalFormat, size, size, tt.format, tt.typ) f.FramebufferTexture2D(FRAMEBUFFER, COLOR_ATTACHMENT0, TEXTURE_2D, tex, 0) if f.CheckFramebufferStatus(FRAMEBUFFER) == FRAMEBUFFER_COMPLETE { return tt, nil } } return textureTriple{}, errors.New("floating point FBOs not supported") } func alphaTripleFor() textureTriple { return textureTriple{R8, Enum(RED), UNSIGNED_BYTE} } func firstBufferType(typ BufferBinding) Enum { switch { case typ&BufferBindingIndices != 0: return ELEMENT_ARRAY_BUFFER case typ&BufferBindingVertices != 0: return ARRAY_BUFFER case typ&BufferBindingUniforms != 0: return UNIFORM_BUFFER default: panic("unsupported buffer type") } } // SRGBFBO provides an intermediate sRGB framebuffer for gamma-correct rendering. type SRGBFBO struct { c *Functions state *glState viewport image.Point fbo GLFramebuffer tex GLTexture blitted bool quad GLBuffer prog GLProgram format textureTriple } func NewSRGBFBO(f *Functions, state *glState) (*SRGBFBO, error) { srgbTriple := textureTriple{SRGB8_ALPHA8, Enum(GL_RGBA), Enum(UNSIGNED_BYTE)} s := &SRGBFBO{ c: f, state: state, format: srgbTriple, fbo: f.CreateFramebuffer(), tex: f.CreateTexture(), } state.bindGLTexture(f, 0, s.tex) f.TexParameteri(TEXTURE_2D, TEXTURE_WRAP_S, int(CLAMP_TO_EDGE)) f.TexParameteri(TEXTURE_2D, TEXTURE_WRAP_T, int(CLAMP_TO_EDGE)) f.TexParameteri(TEXTURE_2D, TEXTURE_MAG_FILTER, int(NEAREST)) f.TexParameteri(TEXTURE_2D, TEXTURE_MIN_FILTER, int(NEAREST)) return s, nil } func (s *SRGBFBO) Blit() { if !s.blitted { prog, err := GLCreateProgram(s.c, blitVSrc, blitFSrc, []string{"pos", "uv"}) if err != nil { panic(err) } s.prog = prog s.state.useProgram(s.c, prog) s.c.Uniform1i(s.c.GetUniformLocation(prog, "tex"), 0) s.quad = s.c.CreateBuffer() s.state.bindBuffer(s.c, ARRAY_BUFFER, s.quad) coords := Float32sToBytes([]float32{ -1, +1, 0, 1, +1, +1, 1, 1, -1, -1, 0, 0, +1, -1, 1, 0, }) s.c.BufferData(ARRAY_BUFFER, len(coords), STATIC_DRAW, coords) s.blitted = true } s.state.useProgram(s.c, s.prog) s.state.bindGLTexture(s.c, 0, s.tex) s.state.vertexAttribPointer(s.c, s.quad, 0, 2, FLOAT, false, 4*4, 0) s.state.vertexAttribPointer(s.c, s.quad, 1, 2, FLOAT, false, 4*4, 4*2) s.state.setVertexAttribArray(s.c, 0, true) s.state.setVertexAttribArray(s.c, 1, true) s.c.DrawArrays(TRIANGLE_STRIP, 0, 4) s.state.bindFramebuffer(s.c, FRAMEBUFFER, s.fbo) s.c.InvalidateFramebuffer(FRAMEBUFFER, COLOR_ATTACHMENT0) } func (s *SRGBFBO) Refresh(viewport image.Point) error { if viewport.X == 0 || viewport.Y == 0 { return errors.New("srgb: zero-sized framebuffer") } if s.viewport == viewport { return nil } s.viewport = viewport s.state.bindGLTexture(s.c, 0, s.tex) s.c.TexImage2D(TEXTURE_2D, 0, s.format.internalFormat, viewport.X, viewport.Y, s.format.format, s.format.typ) s.state.bindFramebuffer(s.c, FRAMEBUFFER, s.fbo) s.c.FramebufferTexture2D(FRAMEBUFFER, COLOR_ATTACHMENT0, TEXTURE_2D, s.tex, 0) if st := s.c.CheckFramebufferStatus(FRAMEBUFFER); st != FRAMEBUFFER_COMPLETE { return fmt.Errorf("sRGB framebuffer incomplete (%dx%d), status: %#x", viewport.X, viewport.Y, st) } // WebGL2/Safari: verify sRGB is working correctly. s.state.setClearColor(s.c, .5, .5, .5, 1.0) s.c.Clear(COLOR_BUFFER_BIT) var pixel [4]byte s.c.ReadPixels(0, 0, 1, 1, GL_RGBA, UNSIGNED_BYTE, pixel[:]) if pixel[0] == 128 { // sRGB not working, fall back to linear RGBA. s.c.TexImage2D(TEXTURE_2D, 0, GL_RGBA, viewport.X, viewport.Y, GL_RGBA, UNSIGNED_BYTE) if st := s.c.CheckFramebufferStatus(FRAMEBUFFER); st != FRAMEBUFFER_COMPLETE { return fmt.Errorf("fallback RGBA framebuffer incomplete (%dx%d), status: %#x", viewport.X, viewport.Y, st) } } return nil } func (s *SRGBFBO) Release() { s.state.deleteFramebuffer(s.c, s.fbo) s.state.deleteTexture(s.c, s.tex) if s.blitted { s.state.deleteBuffer(s.c, s.quad) s.state.deleteProgram(s.c, s.prog) } s.c = nil } const ( blitVSrc = ` #version 100 precision highp float; attribute vec2 pos; attribute vec2 uv; varying vec2 vUV; void main() { gl_Position = vec4(pos, 0, 1); vUV = uv; } ` blitFSrc = ` #version 100 precision mediump float; uniform sampler2D tex; varying vec2 vUV; vec3 gamma(vec3 rgb) { vec3 exp = vec3(1.055)*pow(rgb, vec3(0.41666)) - vec3(0.055); vec3 lin = rgb * vec3(12.92); bvec3 cut = lessThan(rgb, vec3(0.0031308)); return vec3(cut.r ? lin.r : exp.r, cut.g ? lin.g : exp.g, cut.b ? lin.b : exp.b); } void main() { vec4 col = texture2D(tex, vUV); gl_FragColor = vec4(gamma(col.rgb), col.a); } ` )