mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 15:45:38 +00:00
30ecf75a0f
Most Gio programs have exlusive access to their OpenGL contexts, where saving and restoring of the GL state is not required. This change applies to all OpenGL platforms, but matters most for WASM where syscalls are slow. Signed-off-by: Elias Naur <mail@eliasnaur.com>
1358 lines
37 KiB
Go
1358 lines
37 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package opengl
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"strings"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"gioui.org/gpu/internal/driver"
|
|
"gioui.org/internal/gl"
|
|
"gioui.org/shader"
|
|
)
|
|
|
|
// Backend implements driver.Device.
|
|
type Backend struct {
|
|
funcs *gl.Functions
|
|
|
|
clear bool
|
|
glstate glState
|
|
state state
|
|
savedState glState
|
|
sharedCtx bool
|
|
|
|
glver [2]int
|
|
gles bool
|
|
feats driver.Caps
|
|
// floatTriple holds the settings for floating point
|
|
// textures.
|
|
floatTriple textureTriple
|
|
// Single channel alpha textures.
|
|
alphaTriple textureTriple
|
|
srgbaTriple textureTriple
|
|
storage [storageBindings]*buffer
|
|
|
|
outputFBO gl.Framebuffer
|
|
sRGBFBO *SRGBFBO
|
|
|
|
// vertArray is bound during a frame. We don't need it, but
|
|
// core desktop OpenGL profile 3.3 requires some array bound.
|
|
vertArray gl.VertexArray
|
|
}
|
|
|
|
// State tracking.
|
|
type glState struct {
|
|
drawFBO gl.Framebuffer
|
|
readFBO gl.Framebuffer
|
|
renderBuf gl.Renderbuffer
|
|
vertAttribs [5]struct {
|
|
obj gl.Buffer
|
|
enabled bool
|
|
size int
|
|
typ gl.Enum
|
|
normalized bool
|
|
stride int
|
|
offset uintptr
|
|
}
|
|
prog gl.Program
|
|
texUnits struct {
|
|
active gl.Enum
|
|
binds [2]gl.Texture
|
|
}
|
|
arrayBuf gl.Buffer
|
|
elemBuf gl.Buffer
|
|
uniBuf gl.Buffer
|
|
uniBufs [2]gl.Buffer
|
|
storeBuf gl.Buffer
|
|
storeBufs [4]gl.Buffer
|
|
vertArray gl.VertexArray
|
|
srgb bool
|
|
blend struct {
|
|
enable bool
|
|
srcRGB, dstRGB gl.Enum
|
|
srcA, dstA gl.Enum
|
|
}
|
|
clearColor [4]float32
|
|
viewport [4]int
|
|
unpack_row_length int
|
|
pack_row_length int
|
|
}
|
|
|
|
type state struct {
|
|
pipeline *pipeline
|
|
buffer bufferBinding
|
|
}
|
|
|
|
type bufferBinding struct {
|
|
obj gl.Buffer
|
|
offset int
|
|
}
|
|
|
|
type timer struct {
|
|
funcs *gl.Functions
|
|
obj gl.Query
|
|
}
|
|
|
|
type texture struct {
|
|
backend *Backend
|
|
obj gl.Texture
|
|
fbo gl.Framebuffer
|
|
hasFBO bool
|
|
triple textureTriple
|
|
width int
|
|
height int
|
|
bindings driver.BufferBinding
|
|
foreign bool
|
|
}
|
|
|
|
type pipeline struct {
|
|
prog *program
|
|
inputs []shader.InputLocation
|
|
layout driver.VertexLayout
|
|
blend driver.BlendDesc
|
|
topology driver.Topology
|
|
}
|
|
|
|
type buffer struct {
|
|
backend *Backend
|
|
hasBuffer bool
|
|
obj gl.Buffer
|
|
typ driver.BufferBinding
|
|
size int
|
|
immutable bool
|
|
// For emulation of uniform buffers.
|
|
data []byte
|
|
}
|
|
|
|
type glshader struct {
|
|
backend *Backend
|
|
obj gl.Shader
|
|
src shader.Sources
|
|
}
|
|
|
|
type program struct {
|
|
backend *Backend
|
|
obj gl.Program
|
|
vertUniforms uniforms
|
|
fragUniforms uniforms
|
|
}
|
|
|
|
type uniforms struct {
|
|
locs []uniformLocation
|
|
size int
|
|
}
|
|
|
|
type uniformLocation struct {
|
|
uniform gl.Uniform
|
|
offset int
|
|
typ shader.DataType
|
|
size int
|
|
}
|
|
|
|
type inputLayout struct {
|
|
inputs []shader.InputLocation
|
|
layout []driver.InputDesc
|
|
}
|
|
|
|
// textureTriple holds the type settings for
|
|
// a TexImage2D call.
|
|
type textureTriple struct {
|
|
internalFormat gl.Enum
|
|
format gl.Enum
|
|
typ gl.Enum
|
|
}
|
|
|
|
const (
|
|
storageBindings = 32
|
|
)
|
|
|
|
func init() {
|
|
driver.NewOpenGLDevice = newOpenGLDevice
|
|
}
|
|
|
|
// Supporting compute programs is theoretically possible with OpenGL ES 3.1. In
|
|
// practice, there are too many driver issues, especially on Android (e.g.
|
|
// Google Pixel, Samsung J2 are both broken i different ways). Disable support
|
|
// and rely on Vulkan for devices that support it, and the CPU fallback for
|
|
// devices that don't.
|
|
const brokenGLES31 = true
|
|
|
|
func newOpenGLDevice(api driver.OpenGL) (driver.Device, error) {
|
|
f, err := gl.NewFunctions(api.Context, api.ES)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
exts := strings.Split(f.GetString(gl.EXTENSIONS), " ")
|
|
glVer := f.GetString(gl.VERSION)
|
|
ver, gles, err := gl.ParseGLVersion(glVer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
floatTriple, ffboErr := floatTripleFor(f, ver, exts)
|
|
srgbaTriple, srgbErr := srgbaTripleFor(ver, exts)
|
|
gles31 := gles && (ver[0] > 3 || (ver[0] == 3 && ver[1] >= 1))
|
|
b := &Backend{
|
|
glver: ver,
|
|
gles: gles,
|
|
funcs: f,
|
|
floatTriple: floatTriple,
|
|
alphaTriple: alphaTripleFor(ver),
|
|
srgbaTriple: srgbaTriple,
|
|
sharedCtx: api.Shared,
|
|
}
|
|
b.feats.BottomLeftOrigin = true
|
|
if srgbErr == nil {
|
|
b.feats.Features |= driver.FeatureSRGB
|
|
}
|
|
if ffboErr == nil {
|
|
b.feats.Features |= driver.FeatureFloatRenderTargets
|
|
}
|
|
if gles31 && !brokenGLES31 {
|
|
b.feats.Features |= driver.FeatureCompute
|
|
}
|
|
if hasExtension(exts, "GL_EXT_disjoint_timer_query_webgl2") || hasExtension(exts, "GL_EXT_disjoint_timer_query") {
|
|
b.feats.Features |= driver.FeatureTimers
|
|
}
|
|
b.feats.MaxTextureSize = f.GetInteger(gl.MAX_TEXTURE_SIZE)
|
|
if !b.sharedCtx {
|
|
// We have exclusive access to the context, so query the GL state once
|
|
// instead of at each frame.
|
|
b.glstate = b.queryState()
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
func (b *Backend) BeginFrame(target driver.RenderTarget, clear bool, viewport image.Point) driver.Texture {
|
|
b.clear = clear
|
|
if b.sharedCtx {
|
|
b.glstate = b.queryState()
|
|
b.savedState = b.glstate
|
|
}
|
|
b.state = state{}
|
|
var renderFBO gl.Framebuffer
|
|
if target != nil {
|
|
switch t := target.(type) {
|
|
case driver.OpenGLRenderTarget:
|
|
renderFBO = gl.Framebuffer(t)
|
|
case *texture:
|
|
renderFBO = t.ensureFBO()
|
|
default:
|
|
panic(fmt.Errorf("opengl: invalid render target type: %T", target))
|
|
}
|
|
}
|
|
b.outputFBO = renderFBO
|
|
b.glstate.bindFramebuffer(b.funcs, gl.FRAMEBUFFER, renderFBO)
|
|
if b.gles {
|
|
// If the output framebuffer is not in the sRGB colorspace already, emulate it.
|
|
var fbEncoding int
|
|
if !renderFBO.Valid() {
|
|
fbEncoding = b.funcs.GetFramebufferAttachmentParameteri(gl.FRAMEBUFFER, gl.BACK, gl.FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING)
|
|
} else {
|
|
fbEncoding = b.funcs.GetFramebufferAttachmentParameteri(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING)
|
|
}
|
|
if fbEncoding == gl.LINEAR && 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.Framebuffer()
|
|
} else if b.sRGBFBO != nil {
|
|
b.sRGBFBO.Release()
|
|
b.sRGBFBO = nil
|
|
}
|
|
} else {
|
|
b.glstate.set(b.funcs, gl.FRAMEBUFFER_SRGB, true)
|
|
if !b.vertArray.Valid() {
|
|
b.vertArray = b.funcs.CreateVertexArray()
|
|
}
|
|
b.glstate.bindVertexArray(b.funcs, b.vertArray)
|
|
}
|
|
b.glstate.bindFramebuffer(b.funcs, gl.FRAMEBUFFER, renderFBO)
|
|
if b.sRGBFBO != nil && !clear {
|
|
b.clearOutput(0, 0, 0, 0)
|
|
}
|
|
return &texture{backend: b, fbo: renderFBO, hasFBO: true, foreign: true}
|
|
}
|
|
|
|
func (b *Backend) EndFrame() {
|
|
if b.sRGBFBO != nil {
|
|
b.glstate.bindFramebuffer(b.funcs, gl.FRAMEBUFFER, b.outputFBO)
|
|
if b.clear {
|
|
b.SetBlend(false)
|
|
} else {
|
|
b.BlendFunc(driver.BlendFactorOne, driver.BlendFactorOneMinusSrcAlpha)
|
|
b.SetBlend(true)
|
|
}
|
|
b.sRGBFBO.Blit()
|
|
}
|
|
if b.sharedCtx {
|
|
b.restoreState(b.savedState)
|
|
}
|
|
}
|
|
|
|
func (b *Backend) queryState() glState {
|
|
s := glState{
|
|
prog: gl.Program(b.funcs.GetBinding(gl.CURRENT_PROGRAM)),
|
|
arrayBuf: gl.Buffer(b.funcs.GetBinding(gl.ARRAY_BUFFER_BINDING)),
|
|
elemBuf: gl.Buffer(b.funcs.GetBinding(gl.ELEMENT_ARRAY_BUFFER_BINDING)),
|
|
drawFBO: gl.Framebuffer(b.funcs.GetBinding(gl.FRAMEBUFFER_BINDING)),
|
|
clearColor: b.funcs.GetFloat4(gl.COLOR_CLEAR_VALUE),
|
|
viewport: b.funcs.GetInteger4(gl.VIEWPORT),
|
|
unpack_row_length: b.funcs.GetInteger(gl.UNPACK_ROW_LENGTH),
|
|
pack_row_length: b.funcs.GetInteger(gl.PACK_ROW_LENGTH),
|
|
}
|
|
s.blend.enable = b.funcs.IsEnabled(gl.BLEND)
|
|
s.blend.srcRGB = gl.Enum(b.funcs.GetInteger(gl.BLEND_SRC_RGB))
|
|
s.blend.dstRGB = gl.Enum(b.funcs.GetInteger(gl.BLEND_DST_RGB))
|
|
s.blend.srcA = gl.Enum(b.funcs.GetInteger(gl.BLEND_SRC_ALPHA))
|
|
s.blend.dstA = gl.Enum(b.funcs.GetInteger(gl.BLEND_DST_ALPHA))
|
|
s.texUnits.active = gl.Enum(b.funcs.GetInteger(gl.ACTIVE_TEXTURE))
|
|
if !b.gles {
|
|
s.srgb = b.funcs.IsEnabled(gl.FRAMEBUFFER_SRGB)
|
|
}
|
|
if !b.gles || b.glver[0] >= 3 {
|
|
s.vertArray = gl.VertexArray(b.funcs.GetBinding(gl.VERTEX_ARRAY_BINDING))
|
|
s.readFBO = gl.Framebuffer(b.funcs.GetBinding(gl.READ_FRAMEBUFFER_BINDING))
|
|
s.uniBuf = gl.Buffer(b.funcs.GetBinding(gl.UNIFORM_BUFFER_BINDING))
|
|
for i := range s.uniBufs {
|
|
s.uniBufs[i] = gl.Buffer(b.funcs.GetBindingi(gl.UNIFORM_BUFFER_BINDING, i))
|
|
}
|
|
}
|
|
if b.gles && (b.glver[0] > 3 || (b.glver[0] == 3 && b.glver[1] >= 1)) {
|
|
s.storeBuf = gl.Buffer(b.funcs.GetBinding(gl.SHADER_STORAGE_BUFFER_BINDING))
|
|
for i := range s.storeBufs {
|
|
s.storeBufs[i] = gl.Buffer(b.funcs.GetBindingi(gl.SHADER_STORAGE_BUFFER_BINDING, i))
|
|
}
|
|
}
|
|
for i := range s.texUnits.binds {
|
|
s.activeTexture(b.funcs, gl.TEXTURE0+gl.Enum(i))
|
|
s.texUnits.binds[i] = gl.Texture(b.funcs.GetBinding(gl.TEXTURE_BINDING_2D))
|
|
}
|
|
for i := range s.vertAttribs {
|
|
a := &s.vertAttribs[i]
|
|
a.enabled = b.funcs.GetVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_ENABLED) != gl.FALSE
|
|
a.obj = gl.Buffer(b.funcs.GetVertexAttribBinding(i, gl.VERTEX_ATTRIB_ARRAY_ENABLED))
|
|
a.size = b.funcs.GetVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_SIZE)
|
|
a.typ = gl.Enum(b.funcs.GetVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_TYPE))
|
|
a.normalized = b.funcs.GetVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_NORMALIZED) != gl.FALSE
|
|
a.stride = b.funcs.GetVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_STRIDE)
|
|
a.offset = b.funcs.GetVertexAttribPointer(i, gl.VERTEX_ATTRIB_ARRAY_POINTER)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (b *Backend) restoreState(dst glState) {
|
|
src := b.glstate
|
|
f := b.funcs
|
|
for i, unit := range dst.texUnits.binds {
|
|
src.bindTexture(f, i, unit)
|
|
}
|
|
src.activeTexture(f, dst.texUnits.active)
|
|
src.bindFramebuffer(f, gl.FRAMEBUFFER, dst.drawFBO)
|
|
src.bindFramebuffer(f, gl.READ_FRAMEBUFFER, dst.readFBO)
|
|
src.set(f, gl.BLEND, dst.blend.enable)
|
|
bf := dst.blend
|
|
src.setBlendFuncSeparate(f, bf.srcRGB, bf.dstRGB, bf.srcA, bf.dstA)
|
|
src.set(f, gl.FRAMEBUFFER_SRGB, dst.srgb)
|
|
src.bindVertexArray(f, dst.vertArray)
|
|
src.useProgram(f, dst.prog)
|
|
src.bindBuffer(f, gl.ELEMENT_ARRAY_BUFFER, dst.elemBuf)
|
|
for i, b := range dst.uniBufs {
|
|
src.bindBufferBase(f, gl.UNIFORM_BUFFER, i, b)
|
|
}
|
|
src.bindBuffer(f, gl.UNIFORM_BUFFER, dst.uniBuf)
|
|
for i, b := range dst.storeBufs {
|
|
src.bindBufferBase(f, gl.SHADER_STORAGE_BUFFER, i, b)
|
|
}
|
|
src.bindBuffer(f, gl.SHADER_STORAGE_BUFFER, dst.storeBuf)
|
|
col := dst.clearColor
|
|
src.setClearColor(f, col[0], col[1], col[2], col[3])
|
|
for i, attr := range dst.vertAttribs {
|
|
src.setVertexAttribArray(f, i, attr.enabled)
|
|
src.vertexAttribPointer(f, attr.obj, i, attr.size, attr.typ, attr.normalized, attr.stride, int(attr.offset))
|
|
}
|
|
src.bindBuffer(f, gl.ARRAY_BUFFER, dst.arrayBuf)
|
|
v := dst.viewport
|
|
src.setViewport(f, v[0], v[1], v[2], v[3])
|
|
src.pixelStorei(f, gl.UNPACK_ROW_LENGTH, dst.unpack_row_length)
|
|
src.pixelStorei(f, gl.PACK_ROW_LENGTH, dst.pack_row_length)
|
|
}
|
|
|
|
func (s *glState) setVertexAttribArray(f *gl.Functions, idx int, enabled bool) {
|
|
a := &s.vertAttribs[idx]
|
|
if enabled != a.enabled {
|
|
if enabled {
|
|
f.EnableVertexAttribArray(gl.Attrib(idx))
|
|
} else {
|
|
f.DisableVertexAttribArray(gl.Attrib(idx))
|
|
}
|
|
a.enabled = enabled
|
|
}
|
|
}
|
|
|
|
func (s *glState) vertexAttribPointer(f *gl.Functions, buf gl.Buffer, idx, size int, typ gl.Enum, normalized bool, stride, offset int) {
|
|
s.bindBuffer(f, gl.ARRAY_BUFFER, buf)
|
|
a := &s.vertAttribs[idx]
|
|
a.obj = buf
|
|
a.size = size
|
|
a.typ = typ
|
|
a.normalized = normalized
|
|
a.stride = stride
|
|
a.offset = uintptr(offset)
|
|
f.VertexAttribPointer(gl.Attrib(idx), a.size, a.typ, a.normalized, a.stride, int(a.offset))
|
|
}
|
|
|
|
func (s *glState) activeTexture(f *gl.Functions, unit gl.Enum) {
|
|
if unit != s.texUnits.active {
|
|
f.ActiveTexture(unit)
|
|
s.texUnits.active = unit
|
|
}
|
|
}
|
|
|
|
func (s *glState) bindRenderbuffer(f *gl.Functions, target gl.Enum, r gl.Renderbuffer) {
|
|
if !r.Equal(s.renderBuf) {
|
|
f.BindRenderbuffer(gl.RENDERBUFFER, r)
|
|
s.renderBuf = r
|
|
}
|
|
}
|
|
|
|
func (s *glState) bindTexture(f *gl.Functions, unit int, t gl.Texture) {
|
|
s.activeTexture(f, gl.TEXTURE0+gl.Enum(unit))
|
|
if !t.Equal(s.texUnits.binds[unit]) {
|
|
f.BindTexture(gl.TEXTURE_2D, t)
|
|
s.texUnits.binds[unit] = t
|
|
}
|
|
}
|
|
|
|
func (s *glState) bindVertexArray(f *gl.Functions, a gl.VertexArray) {
|
|
if !a.Equal(s.vertArray) {
|
|
f.BindVertexArray(a)
|
|
s.vertArray = a
|
|
}
|
|
}
|
|
|
|
func (s *glState) deleteRenderbuffer(f *gl.Functions, r gl.Renderbuffer) {
|
|
f.DeleteRenderbuffer(r)
|
|
if r.Equal(s.renderBuf) {
|
|
s.renderBuf = gl.Renderbuffer{}
|
|
}
|
|
}
|
|
|
|
func (s *glState) deleteFramebuffer(f *gl.Functions, fbo gl.Framebuffer) {
|
|
f.DeleteFramebuffer(fbo)
|
|
if fbo.Equal(s.drawFBO) {
|
|
s.drawFBO = gl.Framebuffer{}
|
|
}
|
|
if fbo.Equal(s.readFBO) {
|
|
s.readFBO = gl.Framebuffer{}
|
|
}
|
|
}
|
|
|
|
func (s *glState) deleteBuffer(f *gl.Functions, b gl.Buffer) {
|
|
f.DeleteBuffer(b)
|
|
if b.Equal(s.arrayBuf) {
|
|
s.arrayBuf = gl.Buffer{}
|
|
}
|
|
if b.Equal(s.elemBuf) {
|
|
s.elemBuf = gl.Buffer{}
|
|
}
|
|
if b.Equal(s.uniBuf) {
|
|
s.uniBuf = gl.Buffer{}
|
|
}
|
|
if b.Equal(s.storeBuf) {
|
|
s.uniBuf = gl.Buffer{}
|
|
}
|
|
for i, b2 := range s.storeBufs {
|
|
if b.Equal(b2) {
|
|
s.storeBufs[i] = gl.Buffer{}
|
|
}
|
|
}
|
|
for i, b2 := range s.uniBufs {
|
|
if b.Equal(b2) {
|
|
s.uniBufs[i] = gl.Buffer{}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *glState) deleteProgram(f *gl.Functions, p gl.Program) {
|
|
f.DeleteProgram(p)
|
|
if p.Equal(s.prog) {
|
|
s.prog = gl.Program{}
|
|
}
|
|
}
|
|
|
|
func (s *glState) deleteVertexArray(f *gl.Functions, a gl.VertexArray) {
|
|
f.DeleteVertexArray(a)
|
|
if a.Equal(s.vertArray) {
|
|
s.vertArray = gl.VertexArray{}
|
|
}
|
|
}
|
|
|
|
func (s *glState) deleteTexture(f *gl.Functions, t gl.Texture) {
|
|
f.DeleteTexture(t)
|
|
binds := &s.texUnits.binds
|
|
for i, obj := range binds {
|
|
if t.Equal(obj) {
|
|
binds[i] = gl.Texture{}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *glState) useProgram(f *gl.Functions, p gl.Program) {
|
|
if !p.Equal(s.prog) {
|
|
f.UseProgram(p)
|
|
s.prog = p
|
|
}
|
|
}
|
|
|
|
func (s *glState) bindFramebuffer(f *gl.Functions, target gl.Enum, fbo gl.Framebuffer) {
|
|
switch target {
|
|
case gl.FRAMEBUFFER:
|
|
if fbo.Equal(s.drawFBO) && fbo.Equal(s.readFBO) {
|
|
return
|
|
}
|
|
s.drawFBO = fbo
|
|
s.readFBO = fbo
|
|
case gl.READ_FRAMEBUFFER:
|
|
if fbo.Equal(s.readFBO) {
|
|
return
|
|
}
|
|
s.readFBO = fbo
|
|
case gl.DRAW_FRAMEBUFFER:
|
|
if fbo.Equal(s.drawFBO) {
|
|
return
|
|
}
|
|
s.drawFBO = fbo
|
|
default:
|
|
panic("unknown target")
|
|
}
|
|
f.BindFramebuffer(target, fbo)
|
|
}
|
|
|
|
func (s *glState) bindBufferBase(f *gl.Functions, target gl.Enum, idx int, buf gl.Buffer) {
|
|
switch target {
|
|
case gl.UNIFORM_BUFFER:
|
|
if buf.Equal(s.uniBuf) && buf.Equal(s.uniBufs[idx]) {
|
|
return
|
|
}
|
|
s.uniBuf = buf
|
|
s.uniBufs[idx] = buf
|
|
case gl.SHADER_STORAGE_BUFFER:
|
|
if buf.Equal(s.storeBuf) && buf.Equal(s.storeBufs[idx]) {
|
|
return
|
|
}
|
|
s.storeBuf = buf
|
|
s.storeBufs[idx] = buf
|
|
default:
|
|
panic("unknown buffer target")
|
|
}
|
|
f.BindBufferBase(target, idx, buf)
|
|
}
|
|
|
|
func (s *glState) bindBuffer(f *gl.Functions, target gl.Enum, buf gl.Buffer) {
|
|
switch target {
|
|
case gl.ARRAY_BUFFER:
|
|
if buf.Equal(s.arrayBuf) {
|
|
return
|
|
}
|
|
s.arrayBuf = buf
|
|
case gl.ELEMENT_ARRAY_BUFFER:
|
|
if buf.Equal(s.elemBuf) {
|
|
return
|
|
}
|
|
s.elemBuf = buf
|
|
case gl.UNIFORM_BUFFER:
|
|
if buf.Equal(s.uniBuf) {
|
|
return
|
|
}
|
|
s.uniBuf = buf
|
|
case gl.SHADER_STORAGE_BUFFER:
|
|
if buf.Equal(s.storeBuf) {
|
|
return
|
|
}
|
|
s.storeBuf = buf
|
|
default:
|
|
panic("unknown buffer target")
|
|
}
|
|
f.BindBuffer(target, buf)
|
|
}
|
|
|
|
func (s *glState) pixelStorei(f *gl.Functions, pname gl.Enum, val int) {
|
|
switch pname {
|
|
case gl.UNPACK_ROW_LENGTH:
|
|
if val == s.unpack_row_length {
|
|
return
|
|
}
|
|
s.unpack_row_length = val
|
|
case gl.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 *gl.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 *gl.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 *gl.Functions, srcRGB, dstRGB, srcA, dstA gl.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(srcA, dstA, srcA, dstA)
|
|
}
|
|
}
|
|
|
|
func (s *glState) set(f *gl.Functions, target gl.Enum, enable bool) {
|
|
switch target {
|
|
case gl.FRAMEBUFFER_SRGB:
|
|
if s.srgb == enable {
|
|
return
|
|
}
|
|
s.srgb = enable
|
|
case gl.BLEND:
|
|
if enable == s.blend.enable {
|
|
return
|
|
}
|
|
s.blend.enable = enable
|
|
default:
|
|
panic("unknown enable")
|
|
}
|
|
if enable {
|
|
f.Enable(target)
|
|
} else {
|
|
f.Disable(target)
|
|
}
|
|
}
|
|
|
|
func (b *Backend) Caps() driver.Caps {
|
|
return b.feats
|
|
}
|
|
|
|
func (b *Backend) NewTimer() driver.Timer {
|
|
return &timer{
|
|
funcs: b.funcs,
|
|
obj: b.funcs.CreateQuery(),
|
|
}
|
|
}
|
|
|
|
func (b *Backend) IsTimeContinuous() bool {
|
|
return b.funcs.GetInteger(gl.GPU_DISJOINT_EXT) == gl.FALSE
|
|
}
|
|
|
|
func (t *texture) ensureFBO() gl.Framebuffer {
|
|
if t.hasFBO {
|
|
return t.fbo
|
|
}
|
|
b := t.backend
|
|
oldFBO := b.glstate.drawFBO
|
|
defer func() {
|
|
b.glstate.bindFramebuffer(b.funcs, gl.FRAMEBUFFER, oldFBO)
|
|
}()
|
|
glErr(b.funcs)
|
|
fb := b.funcs.CreateFramebuffer()
|
|
b.glstate.bindFramebuffer(b.funcs, gl.FRAMEBUFFER, fb)
|
|
if err := glErr(b.funcs); err != nil {
|
|
b.funcs.DeleteFramebuffer(fb)
|
|
panic(err)
|
|
}
|
|
b.funcs.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, t.obj, 0)
|
|
if st := b.funcs.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
|
|
b.funcs.DeleteFramebuffer(fb)
|
|
panic(fmt.Errorf("incomplete framebuffer, status = 0x%x, err = %d", st, b.funcs.GetError()))
|
|
}
|
|
t.fbo = fb
|
|
t.hasFBO = true
|
|
return fb
|
|
}
|
|
|
|
func (b *Backend) NewTexture(format driver.TextureFormat, width, height int, minFilter, magFilter driver.TextureFilter, binding driver.BufferBinding) (driver.Texture, error) {
|
|
glErr(b.funcs)
|
|
tex := &texture{backend: b, obj: b.funcs.CreateTexture(), width: width, height: height, bindings: binding}
|
|
switch format {
|
|
case driver.TextureFormatFloat:
|
|
tex.triple = b.floatTriple
|
|
case driver.TextureFormatSRGBA:
|
|
tex.triple = b.srgbaTriple
|
|
case driver.TextureFormatRGBA8:
|
|
tex.triple = textureTriple{gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE}
|
|
default:
|
|
return nil, errors.New("unsupported texture format")
|
|
}
|
|
b.BindTexture(0, tex)
|
|
b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, toTexFilter(magFilter))
|
|
b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, toTexFilter(minFilter))
|
|
b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
|
b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
|
if b.gles && b.glver[0] >= 3 {
|
|
// Immutable textures are required for BindImageTexture, and can't hurt otherwise.
|
|
b.funcs.TexStorage2D(gl.TEXTURE_2D, 1, tex.triple.internalFormat, width, height)
|
|
} else {
|
|
b.funcs.TexImage2D(gl.TEXTURE_2D, 0, tex.triple.internalFormat, width, height, tex.triple.format, tex.triple.typ)
|
|
}
|
|
if err := glErr(b.funcs); err != nil {
|
|
tex.Release()
|
|
return nil, err
|
|
}
|
|
return tex, nil
|
|
}
|
|
|
|
func (b *Backend) NewBuffer(typ driver.BufferBinding, size int) (driver.Buffer, error) {
|
|
glErr(b.funcs)
|
|
buf := &buffer{backend: b, typ: typ, size: size}
|
|
if typ&driver.BufferBindingUniforms != 0 {
|
|
if typ != driver.BufferBindingUniforms {
|
|
return nil, errors.New("uniforms buffers cannot be bound as anything else")
|
|
}
|
|
buf.data = make([]byte, size)
|
|
}
|
|
if typ&^driver.BufferBindingUniforms != 0 {
|
|
buf.hasBuffer = true
|
|
buf.obj = b.funcs.CreateBuffer()
|
|
if err := glErr(b.funcs); err != nil {
|
|
buf.Release()
|
|
return nil, err
|
|
}
|
|
firstBinding := firstBufferType(typ)
|
|
b.glstate.bindBuffer(b.funcs, firstBinding, buf.obj)
|
|
b.funcs.BufferData(firstBinding, size, gl.DYNAMIC_DRAW, nil)
|
|
}
|
|
return buf, nil
|
|
}
|
|
|
|
func (b *Backend) NewImmutableBuffer(typ driver.BufferBinding, data []byte) (driver.Buffer, error) {
|
|
glErr(b.funcs)
|
|
obj := b.funcs.CreateBuffer()
|
|
buf := &buffer{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), gl.STATIC_DRAW, data)
|
|
buf.immutable = true
|
|
if err := glErr(b.funcs); err != nil {
|
|
buf.Release()
|
|
return nil, err
|
|
}
|
|
return buf, nil
|
|
}
|
|
|
|
func glErr(f *gl.Functions) error {
|
|
if st := f.GetError(); st != gl.NO_ERROR {
|
|
return fmt.Errorf("glGetError: %#x", st)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *Backend) Release() {
|
|
if b.sRGBFBO != nil {
|
|
b.sRGBFBO.Release()
|
|
}
|
|
if b.vertArray.Valid() {
|
|
b.glstate.deleteVertexArray(b.funcs, b.vertArray)
|
|
}
|
|
*b = Backend{}
|
|
}
|
|
|
|
func (b *Backend) DispatchCompute(x, y, z int) {
|
|
for binding, buf := range b.storage {
|
|
if buf != nil {
|
|
b.glstate.bindBufferBase(b.funcs, gl.SHADER_STORAGE_BUFFER, binding, buf.obj)
|
|
}
|
|
}
|
|
b.funcs.DispatchCompute(x, y, z)
|
|
b.funcs.MemoryBarrier(gl.ALL_BARRIER_BITS)
|
|
}
|
|
|
|
func (b *Backend) BindImageTexture(unit int, tex driver.Texture) {
|
|
t := tex.(*texture)
|
|
var acc gl.Enum
|
|
switch t.bindings & (driver.BufferBindingShaderStorageRead | driver.BufferBindingShaderStorageWrite) {
|
|
case driver.BufferBindingShaderStorageRead:
|
|
acc = gl.READ_ONLY
|
|
case driver.BufferBindingShaderStorageWrite:
|
|
acc = gl.WRITE_ONLY
|
|
case driver.BufferBindingShaderStorageRead | driver.BufferBindingShaderStorageWrite:
|
|
acc = gl.READ_WRITE
|
|
default:
|
|
panic("unsupported access bits")
|
|
}
|
|
b.funcs.BindImageTexture(unit, t.obj, 0, false, 0, acc, t.triple.internalFormat)
|
|
}
|
|
|
|
func (b *Backend) BlendFunc(sfactor, dfactor driver.BlendFactor) {
|
|
src, dst := toGLBlendFactor(sfactor), toGLBlendFactor(dfactor)
|
|
b.glstate.setBlendFuncSeparate(b.funcs, src, dst, src, dst)
|
|
}
|
|
|
|
func toGLBlendFactor(f driver.BlendFactor) gl.Enum {
|
|
switch f {
|
|
case driver.BlendFactorOne:
|
|
return gl.ONE
|
|
case driver.BlendFactorOneMinusSrcAlpha:
|
|
return gl.ONE_MINUS_SRC_ALPHA
|
|
case driver.BlendFactorZero:
|
|
return gl.ZERO
|
|
case driver.BlendFactorDstColor:
|
|
return gl.DST_COLOR
|
|
default:
|
|
panic("unsupported blend factor")
|
|
}
|
|
}
|
|
|
|
func (b *Backend) SetBlend(enable bool) {
|
|
b.glstate.set(b.funcs, gl.BLEND, enable)
|
|
}
|
|
|
|
func (b *Backend) DrawElements(off, count int) {
|
|
b.prepareDraw()
|
|
// off is in 16-bit indices, but DrawElements take a byte offset.
|
|
byteOff := off * 2
|
|
b.funcs.DrawElements(toGLDrawMode(b.state.pipeline.topology), count, gl.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() {
|
|
p := b.state.pipeline
|
|
if p == nil {
|
|
return
|
|
}
|
|
b.setupVertexArrays()
|
|
}
|
|
|
|
func toGLDrawMode(mode driver.Topology) gl.Enum {
|
|
switch mode {
|
|
case driver.TopologyTriangleStrip:
|
|
return gl.TRIANGLE_STRIP
|
|
case driver.TopologyTriangles:
|
|
return gl.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(gl.COLOR_BUFFER_BIT)
|
|
}
|
|
|
|
func (b *Backend) NewComputeProgram(src shader.Sources) (driver.Program, error) {
|
|
// We don't support ES 3.1 compute, see brokenGLES31 above.
|
|
const GLES31Source = ""
|
|
p, err := gl.CreateComputeProgram(b.funcs, GLES31Source)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %v", src.Name, err)
|
|
}
|
|
return &program{
|
|
backend: b,
|
|
obj: p,
|
|
}, nil
|
|
}
|
|
|
|
func (b *Backend) NewVertexShader(src shader.Sources) (driver.VertexShader, error) {
|
|
glslSrc := b.glslFor(src)
|
|
sh, err := gl.CreateShader(b.funcs, gl.VERTEX_SHADER, glslSrc)
|
|
return &glshader{backend: b, obj: sh, src: src}, err
|
|
}
|
|
|
|
func (b *Backend) NewFragmentShader(src shader.Sources) (driver.FragmentShader, error) {
|
|
glslSrc := b.glslFor(src)
|
|
sh, err := gl.CreateShader(b.funcs, gl.FRAGMENT_SHADER, glslSrc)
|
|
return &glshader{backend: b, obj: sh, src: src}, err
|
|
}
|
|
|
|
func (b *Backend) glslFor(src shader.Sources) string {
|
|
if b.gles {
|
|
return src.GLSL100ES
|
|
} else {
|
|
return src.GLSL150
|
|
}
|
|
}
|
|
|
|
func (b *Backend) NewPipeline(desc driver.PipelineDesc) (driver.Pipeline, error) {
|
|
p, err := b.newProgram(desc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
layout := desc.VertexLayout
|
|
vsrc := desc.VertexShader.(*glshader).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 &pipeline{
|
|
prog: p,
|
|
inputs: vsrc.Inputs,
|
|
layout: layout,
|
|
blend: desc.BlendDesc,
|
|
topology: desc.Topology,
|
|
}, nil
|
|
}
|
|
|
|
func (b *Backend) newProgram(desc driver.PipelineDesc) (*program, error) {
|
|
p := b.funcs.CreateProgram()
|
|
if !p.Valid() {
|
|
return nil, errors.New("opengl: glCreateProgram failed")
|
|
}
|
|
vsh, fsh := desc.VertexShader.(*glshader), desc.FragmentShader.(*glshader)
|
|
b.funcs.AttachShader(p, vsh.obj)
|
|
b.funcs.AttachShader(p, fsh.obj)
|
|
for _, inp := range vsh.src.Inputs {
|
|
b.funcs.BindAttribLocation(p, gl.Attrib(inp.Location), inp.Name)
|
|
}
|
|
b.funcs.LinkProgram(p)
|
|
if b.funcs.GetProgrami(p, gl.LINK_STATUS) == 0 {
|
|
log := b.funcs.GetProgramInfoLog(p)
|
|
b.funcs.DeleteProgram(p)
|
|
return nil, fmt.Errorf("opengl: program link failed: %s", strings.TrimSpace(log))
|
|
}
|
|
prog := &program{
|
|
backend: b,
|
|
obj: p,
|
|
}
|
|
b.glstate.useProgram(b.funcs, p)
|
|
// Bind texture uniforms.
|
|
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 driver.Buffer) {
|
|
bf := buf.(*buffer)
|
|
if bf.typ&(driver.BufferBindingShaderStorageRead|driver.BufferBindingShaderStorageWrite) == 0 {
|
|
panic("not a shader storage buffer")
|
|
}
|
|
b.storage[binding] = bf
|
|
}
|
|
|
|
func (b *Backend) BindUniforms(buf driver.Buffer) {
|
|
bf := buf.(*buffer)
|
|
if bf.typ&driver.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 driver.Program) {
|
|
p := prog.(*program)
|
|
b.glstate.useProgram(b.funcs, p.obj)
|
|
}
|
|
|
|
func (s *glshader) Release() {
|
|
s.backend.funcs.DeleteShader(s.obj)
|
|
}
|
|
|
|
func (p *program) Release() {
|
|
p.backend.glstate.deleteProgram(p.backend.funcs, p.obj)
|
|
}
|
|
|
|
func (u *uniforms) setup(funcs *gl.Functions, p gl.Program, uniformSize int, uniforms []shader.UniformLocation) {
|
|
u.locs = make([]uniformLocation, len(uniforms))
|
|
for i, uniform := range uniforms {
|
|
loc := funcs.GetUniformLocation(p, uniform.Name)
|
|
u.locs[i] = uniformLocation{uniform: loc, offset: uniform.Offset, typ: uniform.Type, size: uniform.Size}
|
|
}
|
|
u.size = uniformSize
|
|
}
|
|
|
|
func (p *uniforms) update(funcs *gl.Functions, buf *buffer) {
|
|
if buf.size < p.size {
|
|
panic(fmt.Errorf("uniform buffer too small, got %d need %d", buf.size, p.size))
|
|
}
|
|
data := buf.data
|
|
for _, u := range p.locs {
|
|
if !u.uniform.Valid() {
|
|
continue
|
|
}
|
|
data := data[u.offset:]
|
|
switch {
|
|
case u.typ == shader.DataTypeFloat && u.size == 1:
|
|
data := data[:4]
|
|
v := *(*[1]float32)(unsafe.Pointer(&data[0]))
|
|
funcs.Uniform1f(u.uniform, v[0])
|
|
case u.typ == shader.DataTypeFloat && u.size == 2:
|
|
data := data[:8]
|
|
v := *(*[2]float32)(unsafe.Pointer(&data[0]))
|
|
funcs.Uniform2f(u.uniform, v[0], v[1])
|
|
case u.typ == shader.DataTypeFloat && u.size == 3:
|
|
data := data[:12]
|
|
v := *(*[3]float32)(unsafe.Pointer(&data[0]))
|
|
funcs.Uniform3f(u.uniform, v[0], v[1], v[2])
|
|
case u.typ == shader.DataTypeFloat && u.size == 4:
|
|
data := data[:16]
|
|
v := *(*[4]float32)(unsafe.Pointer(&data[0]))
|
|
funcs.Uniform4f(u.uniform, v[0], v[1], v[2], v[3])
|
|
default:
|
|
panic("unsupported uniform data type or size")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (b *buffer) Upload(data []byte) {
|
|
if b.immutable {
|
|
panic("immutable buffer")
|
|
}
|
|
if len(data) > b.size {
|
|
panic("buffer size overflow")
|
|
}
|
|
copy(b.data, data)
|
|
if b.hasBuffer {
|
|
firstBinding := firstBufferType(b.typ)
|
|
b.backend.glstate.bindBuffer(b.backend.funcs, firstBinding, b.obj)
|
|
if len(data) == b.size {
|
|
// the iOS GL implementation doesn't recognize when BufferSubData
|
|
// clears the entire buffer. Tell it and avoid GPU stalls.
|
|
// See also https://github.com/godotengine/godot/issues/23956.
|
|
b.backend.funcs.BufferData(firstBinding, b.size, gl.DYNAMIC_DRAW, data)
|
|
} else {
|
|
b.backend.funcs.BufferSubData(firstBinding, 0, data)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (b *buffer) Download(data []byte) error {
|
|
if len(data) > b.size {
|
|
panic("buffer size overflow")
|
|
}
|
|
if !b.hasBuffer {
|
|
copy(data, b.data)
|
|
return nil
|
|
}
|
|
firstBinding := firstBufferType(b.typ)
|
|
b.backend.glstate.bindBuffer(b.backend.funcs, firstBinding, b.obj)
|
|
bufferMap := b.backend.funcs.MapBufferRange(firstBinding, 0, len(data), gl.MAP_READ_BIT)
|
|
if bufferMap == nil {
|
|
return fmt.Errorf("MapBufferRange: error %#x", b.backend.funcs.GetError())
|
|
}
|
|
copy(data, bufferMap)
|
|
if !b.backend.funcs.UnmapBuffer(firstBinding) {
|
|
return driver.ErrContentLost
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *buffer) Release() {
|
|
if b.hasBuffer {
|
|
b.backend.glstate.deleteBuffer(b.backend.funcs, b.obj)
|
|
b.hasBuffer = false
|
|
}
|
|
}
|
|
|
|
func (b *Backend) BindVertexBuffer(buf driver.Buffer, offset int) {
|
|
gbuf := buf.(*buffer)
|
|
if gbuf.typ&driver.BufferBindingVertices == 0 {
|
|
panic("not a vertex buffer")
|
|
}
|
|
b.state.buffer = bufferBinding{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 max = len(b.glstate.vertAttribs)
|
|
var enabled [max]bool
|
|
buf := b.state.buffer
|
|
for i, inp := range inputs {
|
|
l := layout.Inputs[i]
|
|
var gltyp gl.Enum
|
|
switch l.Type {
|
|
case shader.DataTypeFloat:
|
|
gltyp = gl.FLOAT
|
|
case shader.DataTypeShort:
|
|
gltyp = gl.SHORT
|
|
default:
|
|
panic("unsupported data type")
|
|
}
|
|
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 < max; i++ {
|
|
b.glstate.setVertexAttribArray(b.funcs, i, enabled[i])
|
|
}
|
|
}
|
|
|
|
func (b *Backend) BindIndexBuffer(buf driver.Buffer) {
|
|
gbuf := buf.(*buffer)
|
|
if gbuf.typ&driver.BufferBindingIndices == 0 {
|
|
panic("not an index buffer")
|
|
}
|
|
b.glstate.bindBuffer(b.funcs, gl.ELEMENT_ARRAY_BUFFER, gbuf.obj)
|
|
}
|
|
|
|
func (b *Backend) CopyTexture(dst driver.Texture, dstOrigin image.Point, src driver.Texture, srcRect image.Rectangle) {
|
|
const unit = 0
|
|
oldTex := b.glstate.texUnits.binds[unit]
|
|
defer func() {
|
|
b.glstate.bindTexture(b.funcs, unit, oldTex)
|
|
}()
|
|
b.glstate.bindTexture(b.funcs, unit, dst.(*texture).obj)
|
|
b.glstate.bindFramebuffer(b.funcs, gl.FRAMEBUFFER, src.(*texture).ensureFBO())
|
|
sz := srcRect.Size()
|
|
b.funcs.CopyTexSubImage2D(gl.TEXTURE_2D, 0, dstOrigin.X, dstOrigin.Y, srcRect.Min.X, srcRect.Min.Y, sz.X, sz.Y)
|
|
}
|
|
|
|
func (t *texture) ReadPixels(src image.Rectangle, pixels []byte, stride int) error {
|
|
glErr(t.backend.funcs)
|
|
t.backend.glstate.bindFramebuffer(t.backend.funcs, gl.FRAMEBUFFER, t.ensureFBO())
|
|
if len(pixels) < src.Dx()*src.Dy()*4 {
|
|
return errors.New("unexpected RGBA size")
|
|
}
|
|
w, h := src.Dx(), src.Dy()
|
|
// WebGL 1 doesn't support PACK_ROW_LENGTH != 0. Avoid it if possible.
|
|
rowLen := 0
|
|
if n := stride / 4; n != w {
|
|
rowLen = n
|
|
}
|
|
t.backend.glstate.pixelStorei(t.backend.funcs, gl.PACK_ROW_LENGTH, rowLen)
|
|
t.backend.funcs.ReadPixels(src.Min.X, src.Min.Y, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels)
|
|
return glErr(t.backend.funcs)
|
|
}
|
|
|
|
func (b *Backend) BindPipeline(pl driver.Pipeline) {
|
|
p := pl.(*pipeline)
|
|
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() {
|
|
b.funcs.MemoryBarrier(gl.ALL_BARRIER_BITS)
|
|
}
|
|
|
|
func (b *Backend) EndCompute() {
|
|
}
|
|
|
|
func (b *Backend) BeginRenderPass(tex driver.Texture, desc driver.LoadDesc) {
|
|
fbo := tex.(*texture).ensureFBO()
|
|
b.glstate.bindFramebuffer(b.funcs, gl.FRAMEBUFFER, fbo)
|
|
switch desc.Action {
|
|
case driver.LoadActionClear:
|
|
c := desc.ClearColor
|
|
b.clearOutput(c.R, c.G, c.B, c.A)
|
|
case driver.LoadActionInvalidate:
|
|
b.funcs.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0)
|
|
}
|
|
}
|
|
|
|
func (b *Backend) EndRenderPass() {
|
|
}
|
|
|
|
func (f *texture) ImplementsRenderTarget() {}
|
|
|
|
func (p *pipeline) Release() {
|
|
p.prog.Release()
|
|
*p = pipeline{}
|
|
}
|
|
|
|
func toTexFilter(f driver.TextureFilter) int {
|
|
switch f {
|
|
case driver.FilterNearest:
|
|
return gl.NEAREST
|
|
case driver.FilterLinear:
|
|
return gl.LINEAR
|
|
default:
|
|
panic("unsupported texture filter")
|
|
}
|
|
}
|
|
|
|
func (b *Backend) PrepareTexture(tex driver.Texture) {}
|
|
|
|
func (b *Backend) BindTexture(unit int, t driver.Texture) {
|
|
b.glstate.bindTexture(b.funcs, unit, t.(*texture).obj)
|
|
}
|
|
|
|
func (t *texture) 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 *texture) 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)
|
|
// WebGL 1 doesn't support UNPACK_ROW_LENGTH != 0. Avoid it if possible.
|
|
rowLen := 0
|
|
if n := stride / 4; n != size.X {
|
|
rowLen = n
|
|
}
|
|
t.backend.glstate.pixelStorei(t.backend.funcs, gl.UNPACK_ROW_LENGTH, rowLen)
|
|
t.backend.funcs.TexSubImage2D(gl.TEXTURE_2D, 0, offset.X, offset.Y, size.X, size.Y, t.triple.format, t.triple.typ, pixels)
|
|
}
|
|
|
|
func (t *timer) Begin() {
|
|
t.funcs.BeginQuery(gl.TIME_ELAPSED_EXT, t.obj)
|
|
}
|
|
|
|
func (t *timer) End() {
|
|
t.funcs.EndQuery(gl.TIME_ELAPSED_EXT)
|
|
}
|
|
|
|
func (t *timer) ready() bool {
|
|
return t.funcs.GetQueryObjectuiv(t.obj, gl.QUERY_RESULT_AVAILABLE) == gl.TRUE
|
|
}
|
|
|
|
func (t *timer) Release() {
|
|
t.funcs.DeleteQuery(t.obj)
|
|
}
|
|
|
|
func (t *timer) Duration() (time.Duration, bool) {
|
|
if !t.ready() {
|
|
return 0, false
|
|
}
|
|
nanos := t.funcs.GetQueryObjectuiv(t.obj, gl.QUERY_RESULT)
|
|
return time.Duration(nanos), true
|
|
}
|
|
|
|
// floatTripleFor determines the best texture triple for floating point FBOs.
|
|
func floatTripleFor(f *gl.Functions, ver [2]int, exts []string) (textureTriple, error) {
|
|
var triples []textureTriple
|
|
if ver[0] >= 3 {
|
|
triples = append(triples, textureTriple{gl.R16F, gl.Enum(gl.RED), gl.Enum(gl.HALF_FLOAT)})
|
|
}
|
|
// According to the OES_texture_half_float specification, EXT_color_buffer_half_float is needed to
|
|
// render to FBOs. However, the Safari WebGL1 implementation does support half-float FBOs but does not
|
|
// report EXT_color_buffer_half_float support. The triples are verified below, so it doesn't matter if we're
|
|
// wrong.
|
|
if hasExtension(exts, "GL_OES_texture_half_float") || hasExtension(exts, "GL_EXT_color_buffer_half_float") {
|
|
// Try single channel.
|
|
triples = append(triples, textureTriple{gl.LUMINANCE, gl.Enum(gl.LUMINANCE), gl.Enum(gl.HALF_FLOAT_OES)})
|
|
// Fallback to 4 channels.
|
|
triples = append(triples, textureTriple{gl.RGBA, gl.Enum(gl.RGBA), gl.Enum(gl.HALF_FLOAT_OES)})
|
|
}
|
|
if hasExtension(exts, "GL_OES_texture_float") || hasExtension(exts, "GL_EXT_color_buffer_float") {
|
|
triples = append(triples, textureTriple{gl.RGBA, gl.Enum(gl.RGBA), gl.Enum(gl.FLOAT)})
|
|
}
|
|
tex := f.CreateTexture()
|
|
defer f.DeleteTexture(tex)
|
|
defTex := gl.Texture(f.GetBinding(gl.TEXTURE_BINDING_2D))
|
|
defer f.BindTexture(gl.TEXTURE_2D, defTex)
|
|
f.BindTexture(gl.TEXTURE_2D, tex)
|
|
f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
|
f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
|
f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
|
|
f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
|
|
fbo := f.CreateFramebuffer()
|
|
defer f.DeleteFramebuffer(fbo)
|
|
defFBO := gl.Framebuffer(f.GetBinding(gl.FRAMEBUFFER_BINDING))
|
|
f.BindFramebuffer(gl.FRAMEBUFFER, fbo)
|
|
defer f.BindFramebuffer(gl.FRAMEBUFFER, defFBO)
|
|
var attempts []string
|
|
for _, tt := range triples {
|
|
const size = 256
|
|
f.TexImage2D(gl.TEXTURE_2D, 0, tt.internalFormat, size, size, tt.format, tt.typ)
|
|
f.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0)
|
|
st := f.CheckFramebufferStatus(gl.FRAMEBUFFER)
|
|
if st == gl.FRAMEBUFFER_COMPLETE {
|
|
return tt, nil
|
|
}
|
|
attempts = append(attempts, fmt.Sprintf("(0x%x, 0x%x, 0x%x): 0x%x", tt.internalFormat, tt.format, tt.typ, st))
|
|
}
|
|
return textureTriple{}, fmt.Errorf("floating point fbos not supported (attempted %s)", attempts)
|
|
}
|
|
|
|
func srgbaTripleFor(ver [2]int, exts []string) (textureTriple, error) {
|
|
switch {
|
|
case ver[0] >= 3:
|
|
return textureTriple{gl.SRGB8_ALPHA8, gl.Enum(gl.RGBA), gl.Enum(gl.UNSIGNED_BYTE)}, nil
|
|
case hasExtension(exts, "GL_EXT_sRGB"):
|
|
return textureTriple{gl.SRGB_ALPHA_EXT, gl.Enum(gl.SRGB_ALPHA_EXT), gl.Enum(gl.UNSIGNED_BYTE)}, nil
|
|
default:
|
|
return textureTriple{}, errors.New("no sRGB texture formats found")
|
|
}
|
|
}
|
|
|
|
func alphaTripleFor(ver [2]int) textureTriple {
|
|
intf, f := gl.Enum(gl.R8), gl.Enum(gl.RED)
|
|
if ver[0] < 3 {
|
|
// R8, RED not supported on OpenGL ES 2.0.
|
|
intf, f = gl.LUMINANCE, gl.Enum(gl.LUMINANCE)
|
|
}
|
|
return textureTriple{intf, f, gl.UNSIGNED_BYTE}
|
|
}
|
|
|
|
func hasExtension(exts []string, ext string) bool {
|
|
for _, e := range exts {
|
|
if ext == e {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func firstBufferType(typ driver.BufferBinding) gl.Enum {
|
|
switch {
|
|
case typ&driver.BufferBindingIndices != 0:
|
|
return gl.ELEMENT_ARRAY_BUFFER
|
|
case typ&driver.BufferBindingVertices != 0:
|
|
return gl.ARRAY_BUFFER
|
|
case typ&driver.BufferBindingUniforms != 0:
|
|
return gl.UNIFORM_BUFFER
|
|
case typ&(driver.BufferBindingShaderStorageRead|driver.BufferBindingShaderStorageWrite) != 0:
|
|
return gl.SHADER_STORAGE_BUFFER
|
|
default:
|
|
panic("unsupported buffer type")
|
|
}
|
|
}
|