// 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")
	}
}
