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