gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/gpu/internal/opengl/srgb.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package opengl 4 5 import ( 6 "errors" 7 "fmt" 8 "image" 9 "runtime" 10 "strings" 11 12 "gioui.org/internal/byteslice" 13 "gioui.org/internal/gl" 14 ) 15 16 // SRGBFBO implements an intermediate sRGB FBO 17 // for gamma-correct rendering on platforms without 18 // sRGB enabled native framebuffers. 19 type SRGBFBO struct { 20 c *gl.Functions 21 state *glState 22 viewport image.Point 23 fbo gl.Framebuffer 24 tex gl.Texture 25 blitted bool 26 quad gl.Buffer 27 prog gl.Program 28 format textureTriple 29 } 30 31 func NewSRGBFBO(f *gl.Functions, state *glState) (*SRGBFBO, error) { 32 glVer := f.GetString(gl.VERSION) 33 ver, _, err := gl.ParseGLVersion(glVer) 34 if err != nil { 35 return nil, err 36 } 37 exts := strings.Split(f.GetString(gl.EXTENSIONS), " ") 38 srgbTriple, err := srgbaTripleFor(ver, exts) 39 if err != nil { 40 // Fall back to the linear RGB colorspace, at the cost of color precision loss. 41 srgbTriple = textureTriple{gl.RGBA, gl.Enum(gl.RGBA), gl.Enum(gl.UNSIGNED_BYTE)} 42 } 43 s := &SRGBFBO{ 44 c: f, 45 state: state, 46 format: srgbTriple, 47 fbo: f.CreateFramebuffer(), 48 tex: f.CreateTexture(), 49 } 50 state.bindTexture(f, 0, s.tex) 51 f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) 52 f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) 53 f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) 54 f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) 55 return s, nil 56 } 57 58 func (s *SRGBFBO) Blit() { 59 if !s.blitted { 60 prog, err := gl.CreateProgram(s.c, blitVSrc, blitFSrc, []string{"pos", "uv"}) 61 if err != nil { 62 panic(err) 63 } 64 s.prog = prog 65 s.state.useProgram(s.c, prog) 66 s.c.Uniform1i(s.c.GetUniformLocation(prog, "tex"), 0) 67 s.quad = s.c.CreateBuffer() 68 s.state.bindBuffer(s.c, gl.ARRAY_BUFFER, s.quad) 69 coords := byteslice.Slice([]float32{ 70 -1, +1, 0, 1, 71 +1, +1, 1, 1, 72 -1, -1, 0, 0, 73 +1, -1, 1, 0, 74 }) 75 s.c.BufferData(gl.ARRAY_BUFFER, len(coords), gl.STATIC_DRAW, coords) 76 s.blitted = true 77 } 78 s.state.useProgram(s.c, s.prog) 79 s.state.bindTexture(s.c, 0, s.tex) 80 s.state.vertexAttribPointer(s.c, s.quad, 0 /* pos */, 2, gl.FLOAT, false, 4*4, 0) 81 s.state.vertexAttribPointer(s.c, s.quad, 1 /* uv */, 2, gl.FLOAT, false, 4*4, 4*2) 82 s.state.setVertexAttribArray(s.c, 0, true) 83 s.state.setVertexAttribArray(s.c, 1, true) 84 s.c.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) 85 s.state.bindFramebuffer(s.c, gl.FRAMEBUFFER, s.fbo) 86 s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0) 87 } 88 89 func (s *SRGBFBO) Framebuffer() gl.Framebuffer { 90 return s.fbo 91 } 92 93 func (s *SRGBFBO) Refresh(viewport image.Point) error { 94 if viewport.X == 0 || viewport.Y == 0 { 95 return errors.New("srgb: zero-sized framebuffer") 96 } 97 if s.viewport == viewport { 98 return nil 99 } 100 s.viewport = viewport 101 s.state.bindTexture(s.c, 0, s.tex) 102 s.c.TexImage2D(gl.TEXTURE_2D, 0, s.format.internalFormat, viewport.X, viewport.Y, s.format.format, s.format.typ) 103 s.state.bindFramebuffer(s.c, gl.FRAMEBUFFER, s.fbo) 104 s.c.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, s.tex, 0) 105 if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { 106 return fmt.Errorf("sRGB framebuffer incomplete (%dx%d), status: %#x error: %x", viewport.X, viewport.Y, st, s.c.GetError()) 107 } 108 109 if runtime.GOOS == "js" { 110 // With macOS Safari, rendering to and then reading from a SRGB8_ALPHA8 111 // texture result in twice gamma corrected colors. Using a plain RGBA 112 // texture seems to work. 113 s.state.setClearColor(s.c, .5, .5, .5, 1.0) 114 s.c.Clear(gl.COLOR_BUFFER_BIT) 115 var pixel [4]byte 116 s.c.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel[:]) 117 if pixel[0] == 128 { // Correct sRGB color value is ~188 118 s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, viewport.X, viewport.Y, gl.RGBA, gl.UNSIGNED_BYTE) 119 if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { 120 return fmt.Errorf("fallback RGBA framebuffer incomplete (%dx%d), status: %#x error: %x", viewport.X, viewport.Y, st, s.c.GetError()) 121 } 122 } 123 } 124 125 return nil 126 } 127 128 func (s *SRGBFBO) Release() { 129 s.state.deleteFramebuffer(s.c, s.fbo) 130 s.state.deleteTexture(s.c, s.tex) 131 if s.blitted { 132 s.state.deleteBuffer(s.c, s.quad) 133 s.state.deleteProgram(s.c, s.prog) 134 } 135 s.c = nil 136 } 137 138 const ( 139 blitVSrc = ` 140 #version 100 141 142 precision highp float; 143 144 attribute vec2 pos; 145 attribute vec2 uv; 146 147 varying vec2 vUV; 148 149 void main() { 150 gl_Position = vec4(pos, 0, 1); 151 vUV = uv; 152 } 153 ` 154 blitFSrc = ` 155 #version 100 156 157 precision mediump float; 158 159 uniform sampler2D tex; 160 varying vec2 vUV; 161 162 vec3 gamma(vec3 rgb) { 163 vec3 exp = vec3(1.055)*pow(rgb, vec3(0.41666)) - vec3(0.055); 164 vec3 lin = rgb * vec3(12.92); 165 bvec3 cut = lessThan(rgb, vec3(0.0031308)); 166 return vec3(cut.r ? lin.r : exp.r, cut.g ? lin.g : exp.g, cut.b ? lin.b : exp.b); 167 } 168 169 void main() { 170 vec4 col = texture2D(tex, vUV); 171 vec3 rgb = col.rgb; 172 rgb = gamma(rgb); 173 gl_FragColor = vec4(rgb, col.a); 174 } 175 ` 176 )