srgb.go 4.8 KB

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