srgb.go raw

   1  // SPDX-License-Identifier: Unlicense OR MIT
   2  
   3  package srgb
   4  
   5  import (
   6  	"fmt"
   7  	"runtime"
   8  	"strings"
   9  
  10  	"github.com/p9c/p9/pkg/gel/gio/internal/byteslice"
  11  	"github.com/p9c/p9/pkg/gel/gio/internal/gl"
  12  )
  13  
  14  // FBO implements an intermediate sRGB FBO
  15  // for gamma-correct rendering on platforms without
  16  // sRGB enabled native framebuffers.
  17  type FBO struct {
  18  	c             *gl.Functions
  19  	width, height int
  20  	frameBuffer   gl.Framebuffer
  21  	depthBuffer   gl.Renderbuffer
  22  	colorTex      gl.Texture
  23  	blitted       bool
  24  	quad          gl.Buffer
  25  	prog          gl.Program
  26  	gl3           bool
  27  }
  28  
  29  func New(ctx gl.Context) (*FBO, error) {
  30  	f, err := gl.NewFunctions(ctx)
  31  	if err != nil {
  32  		return nil, err
  33  	}
  34  	var gl3 bool
  35  	glVer := f.GetString(gl.VERSION)
  36  	ver, _, err := gl.ParseGLVersion(glVer)
  37  	if err != nil {
  38  		return nil, err
  39  	}
  40  	if ver[0] >= 3 {
  41  		gl3 = true
  42  	} else {
  43  		exts := f.GetString(gl.EXTENSIONS)
  44  		if !strings.Contains(exts, "EXT_sRGB") {
  45  			return nil, fmt.Errorf("no support for OpenGL ES 3 nor EXT_sRGB")
  46  		}
  47  	}
  48  	s := &FBO{
  49  		c:           f,
  50  		gl3:         gl3,
  51  		frameBuffer: f.CreateFramebuffer(),
  52  		colorTex:    f.CreateTexture(),
  53  		depthBuffer: f.CreateRenderbuffer(),
  54  	}
  55  	f.BindTexture(gl.TEXTURE_2D, s.colorTex)
  56  	f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
  57  	f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
  58  	f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
  59  	f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
  60  	return s, nil
  61  }
  62  
  63  func (s *FBO) Blit() {
  64  	if !s.blitted {
  65  		prog, err := gl.CreateProgram(s.c, blitVSrc, blitFSrc, []string{"pos", "uv"})
  66  		if err != nil {
  67  			panic(err)
  68  		}
  69  		s.prog = prog
  70  		s.c.UseProgram(prog)
  71  		s.c.Uniform1i(s.c.GetUniformLocation(prog, "tex"), 0)
  72  		s.quad = s.c.CreateBuffer()
  73  		s.c.BindBuffer(gl.ARRAY_BUFFER, s.quad)
  74  		coords := byteslice.Slice([]float32{
  75  			-1, +1, 0, 1,
  76  			+1, +1, 1, 1,
  77  			-1, -1, 0, 0,
  78  			+1, -1, 1, 0,
  79  		})
  80  		s.c.BufferData(gl.ARRAY_BUFFER, len(coords), gl.STATIC_DRAW)
  81  		s.c.BufferSubData(gl.ARRAY_BUFFER, 0, coords)
  82  		s.blitted = true
  83  	}
  84  	s.c.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{})
  85  	s.c.UseProgram(s.prog)
  86  	s.c.BindTexture(gl.TEXTURE_2D, s.colorTex)
  87  	s.c.BindBuffer(gl.ARRAY_BUFFER, s.quad)
  88  	s.c.VertexAttribPointer(0 /* pos */, 2, gl.FLOAT, false, 4*4, 0)
  89  	s.c.VertexAttribPointer(1 /* uv */, 2, gl.FLOAT, false, 4*4, 4*2)
  90  	s.c.EnableVertexAttribArray(0)
  91  	s.c.EnableVertexAttribArray(1)
  92  	s.c.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
  93  	s.c.BindTexture(gl.TEXTURE_2D, gl.Texture{})
  94  	s.c.DisableVertexAttribArray(0)
  95  	s.c.DisableVertexAttribArray(1)
  96  	s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer)
  97  	s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0)
  98  	s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT)
  99  	// The Android emulator requires framebuffer 0 bound at eglSwapBuffer time.
 100  	// Bind the sRGB framebuffer again in afterPresent.
 101  	s.c.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{})
 102  }
 103  
 104  func (s *FBO) AfterPresent() {
 105  	s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer)
 106  }
 107  
 108  func (s *FBO) Refresh(w, h int) error {
 109  	s.width, s.height = w, h
 110  	if w == 0 || h == 0 {
 111  		return nil
 112  	}
 113  	s.c.BindTexture(gl.TEXTURE_2D, s.colorTex)
 114  	if s.gl3 {
 115  		s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.SRGB8_ALPHA8, w, h, gl.RGBA, gl.UNSIGNED_BYTE)
 116  	} else /* EXT_sRGB */ {
 117  		s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.SRGB_ALPHA_EXT, w, h, gl.SRGB_ALPHA_EXT, gl.UNSIGNED_BYTE)
 118  	}
 119  	currentRB := gl.Renderbuffer(s.c.GetBinding(gl.RENDERBUFFER_BINDING))
 120  	s.c.BindRenderbuffer(gl.RENDERBUFFER, s.depthBuffer)
 121  	s.c.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h)
 122  	s.c.BindRenderbuffer(gl.RENDERBUFFER, currentRB)
 123  	s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer)
 124  	s.c.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, s.colorTex, 0)
 125  	s.c.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, s.depthBuffer)
 126  	if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
 127  		return fmt.Errorf("sRGB framebuffer incomplete (%dx%d), status: %#x error: %x", s.width, s.height, st, s.c.GetError())
 128  	}
 129  
 130  	if runtime.GOOS == "js" {
 131  		// With macOS Safari, rendering to and then reading from a SRGB8_ALPHA8
 132  		// texture result in twice gamma corrected colors. Using a plain RGBA
 133  		// texture seems to work.
 134  		s.c.ClearColor(.5, .5, .5, 1.0)
 135  		s.c.Clear(gl.COLOR_BUFFER_BIT)
 136  		var pixel [4]byte
 137  		s.c.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel[:])
 138  		if pixel[0] == 128 { // Correct sRGB color value is ~188
 139  			s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, gl.RGBA, gl.UNSIGNED_BYTE)
 140  			if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
 141  				return fmt.Errorf("fallback RGBA framebuffer incomplete (%dx%d), status: %#x error: %x", s.width, s.height, st, s.c.GetError())
 142  			}
 143  		}
 144  	}
 145  
 146  	return nil
 147  }
 148  
 149  func (s *FBO) Release() {
 150  	s.c.DeleteFramebuffer(s.frameBuffer)
 151  	s.c.DeleteTexture(s.colorTex)
 152  	s.c.DeleteRenderbuffer(s.depthBuffer)
 153  	if s.blitted {
 154  		s.c.DeleteBuffer(s.quad)
 155  		s.c.DeleteProgram(s.prog)
 156  	}
 157  	s.c = nil
 158  }
 159  
 160  const (
 161  	blitVSrc = `
 162  #version 100
 163  
 164  precision highp float;
 165  
 166  attribute vec2 pos;
 167  attribute vec2 uv;
 168  
 169  varying vec2 vUV;
 170  
 171  void main() {
 172      gl_Position = vec4(pos, 0, 1);
 173      vUV = uv;
 174  }
 175  `
 176  	blitFSrc = `
 177  #version 100
 178  
 179  precision mediump float;
 180  
 181  uniform sampler2D tex;
 182  varying vec2 vUV;
 183  
 184  vec3 gamma(vec3 rgb) {
 185  	vec3 exp = vec3(1.055)*pow(rgb, vec3(0.41666)) - vec3(0.055);
 186  	vec3 lin = rgb * vec3(12.92);
 187  	bvec3 cut = lessThan(rgb, vec3(0.0031308));
 188  	return vec3(cut.r ? lin.r : exp.r, cut.g ? lin.g : exp.g, cut.b ? lin.b : exp.b);
 189  }
 190  
 191  void main() {
 192      vec4 col = texture2D(tex, vUV);
 193  	vec3 rgb = col.rgb;
 194  	rgb = gamma(rgb);
 195  	gl_FragColor = vec4(rgb, col.a);
 196  }
 197  `
 198  )
 199