gioui.org/ui@v0.0.0-20190926171558-ce74bc0cbaea/app/internal/gpu/path.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package gpu 4 5 // GPU accelerated path drawing using the algorithms from 6 // Pathfinder (https://github.com/pcwalton/pathfinder). 7 8 import ( 9 "image" 10 "unsafe" 11 12 "gioui.org/ui/app/internal/gl" 13 "gioui.org/ui/f32" 14 "gioui.org/ui/internal/path" 15 ) 16 17 type pather struct { 18 ctx *context 19 20 viewport image.Point 21 22 stenciler *stenciler 23 coverer *coverer 24 } 25 26 type coverer struct { 27 ctx *context 28 prog [2]gl.Program 29 vars [2]struct { 30 z gl.Uniform 31 uScale, uOffset gl.Uniform 32 uUVScale, uUVOffset gl.Uniform 33 uCoverUVScale, uCoverUVOffset gl.Uniform 34 uColor gl.Uniform 35 } 36 } 37 38 type stenciler struct { 39 ctx *context 40 defFBO gl.Framebuffer 41 indexBufQuads int 42 prog gl.Program 43 iprog gl.Program 44 fbos fboSet 45 intersections fboSet 46 uScale, uOffset gl.Uniform 47 uPathOffset gl.Uniform 48 uIntersectUVOffset gl.Uniform 49 uIntersectUVScale gl.Uniform 50 indexBuf gl.Buffer 51 } 52 53 type fboSet struct { 54 fbos []stencilFBO 55 } 56 57 type stencilFBO struct { 58 size image.Point 59 fbo gl.Framebuffer 60 tex gl.Texture 61 } 62 63 type pathData struct { 64 ncurves int 65 data gl.Buffer 66 } 67 68 var ( 69 pathAttribs = []string{"corner", "maxy", "from", "ctrl", "to"} 70 attribPathCorner gl.Attrib = 0 71 attribPathMaxY gl.Attrib = 1 72 attribPathFrom gl.Attrib = 2 73 attribPathCtrl gl.Attrib = 3 74 attribPathTo gl.Attrib = 4 75 76 intersectAttribs = []string{"pos", "uv"} 77 ) 78 79 func newPather(ctx *context) *pather { 80 return &pather{ 81 ctx: ctx, 82 stenciler: newStenciler(ctx), 83 coverer: newCoverer(ctx), 84 } 85 } 86 87 func newCoverer(ctx *context) *coverer { 88 prog, err := createColorPrograms(ctx, coverVSrc, coverFSrc) 89 if err != nil { 90 panic(err) 91 } 92 c := &coverer{ 93 ctx: ctx, 94 prog: prog, 95 } 96 for i, prog := range prog { 97 ctx.UseProgram(prog) 98 switch materialType(i) { 99 case materialTexture: 100 uTex := gl.GetUniformLocation(ctx.Functions, prog, "tex") 101 ctx.Uniform1i(uTex, 0) 102 c.vars[i].uUVScale = gl.GetUniformLocation(ctx.Functions, prog, "uvScale") 103 c.vars[i].uUVOffset = gl.GetUniformLocation(ctx.Functions, prog, "uvOffset") 104 case materialColor: 105 c.vars[i].uColor = gl.GetUniformLocation(ctx.Functions, prog, "color") 106 } 107 uCover := gl.GetUniformLocation(ctx.Functions, prog, "cover") 108 ctx.Uniform1i(uCover, 1) 109 c.vars[i].z = gl.GetUniformLocation(ctx.Functions, prog, "z") 110 c.vars[i].uScale = gl.GetUniformLocation(ctx.Functions, prog, "scale") 111 c.vars[i].uOffset = gl.GetUniformLocation(ctx.Functions, prog, "offset") 112 c.vars[i].uCoverUVScale = gl.GetUniformLocation(ctx.Functions, prog, "uvCoverScale") 113 c.vars[i].uCoverUVOffset = gl.GetUniformLocation(ctx.Functions, prog, "uvCoverOffset") 114 } 115 return c 116 } 117 118 func newStenciler(ctx *context) *stenciler { 119 defFBO := gl.Framebuffer(ctx.GetBinding(gl.FRAMEBUFFER_BINDING)) 120 prog, err := gl.CreateProgram(ctx.Functions, stencilVSrc, stencilFSrc, pathAttribs) 121 if err != nil { 122 panic(err) 123 } 124 ctx.UseProgram(prog) 125 iprog, err := gl.CreateProgram(ctx.Functions, intersectVSrc, intersectFSrc, intersectAttribs) 126 if err != nil { 127 panic(err) 128 } 129 coverLoc := gl.GetUniformLocation(ctx.Functions, iprog, "cover") 130 ctx.UseProgram(iprog) 131 ctx.Uniform1i(coverLoc, 0) 132 return &stenciler{ 133 ctx: ctx, 134 defFBO: defFBO, 135 prog: prog, 136 iprog: iprog, 137 uScale: gl.GetUniformLocation(ctx.Functions, prog, "scale"), 138 uOffset: gl.GetUniformLocation(ctx.Functions, prog, "offset"), 139 uPathOffset: gl.GetUniformLocation(ctx.Functions, prog, "pathOffset"), 140 uIntersectUVScale: gl.GetUniformLocation(ctx.Functions, iprog, "uvScale"), 141 uIntersectUVOffset: gl.GetUniformLocation(ctx.Functions, iprog, "uvOffset"), 142 indexBuf: ctx.CreateBuffer(), 143 } 144 } 145 146 func (s *fboSet) resize(ctx *context, sizes []image.Point) { 147 // Add fbos. 148 for i := len(s.fbos); i < len(sizes); i++ { 149 tex := ctx.CreateTexture() 150 ctx.BindTexture(gl.TEXTURE_2D, tex) 151 ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) 152 ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) 153 ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) 154 ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) 155 fbo := ctx.CreateFramebuffer() 156 s.fbos = append(s.fbos, stencilFBO{ 157 fbo: fbo, 158 tex: tex, 159 }) 160 } 161 // Resize fbos. 162 for i, sz := range sizes { 163 f := &s.fbos[i] 164 // Resizing or recreating FBOs can introduce rendering stalls. 165 // Avoid if the space waste is not too high. 166 resize := sz.X > f.size.X || sz.Y > f.size.Y 167 waste := float32(sz.X*sz.Y) / float32(f.size.X*f.size.Y) 168 resize = resize || waste > 1.2 169 if resize { 170 f.size = sz 171 ctx.BindTexture(gl.TEXTURE_2D, f.tex) 172 tt := ctx.caps.floatTriple 173 ctx.TexImage2D(gl.TEXTURE_2D, 0, tt.internalFormat, sz.X, sz.Y, tt.format, tt.typ, nil) 174 ctx.BindFramebuffer(gl.FRAMEBUFFER, f.fbo) 175 ctx.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, f.tex, 0) 176 } 177 } 178 // Delete extra fbos. 179 s.delete(ctx, len(sizes)) 180 } 181 182 func (s *fboSet) invalidate(ctx *context) { 183 for _, f := range s.fbos { 184 ctx.BindFramebuffer(gl.FRAMEBUFFER, f.fbo) 185 ctx.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0) 186 } 187 } 188 189 func (s *fboSet) delete(ctx *context, idx int) { 190 for i := idx; i < len(s.fbos); i++ { 191 f := s.fbos[i] 192 ctx.DeleteFramebuffer(f.fbo) 193 ctx.DeleteTexture(f.tex) 194 } 195 s.fbos = s.fbos[:idx] 196 } 197 198 func (s *stenciler) release() { 199 s.fbos.delete(s.ctx, 0) 200 s.ctx.DeleteProgram(s.prog) 201 s.ctx.DeleteBuffer(s.indexBuf) 202 } 203 204 func (p *pather) release() { 205 p.stenciler.release() 206 p.coverer.release() 207 } 208 209 func (c *coverer) release() { 210 for _, p := range c.prog { 211 c.ctx.DeleteProgram(p) 212 } 213 } 214 215 func buildPath(ctx *context, p []byte) *pathData { 216 buf := ctx.CreateBuffer() 217 ctx.BindBuffer(gl.ARRAY_BUFFER, buf) 218 ctx.BufferData(gl.ARRAY_BUFFER, p, gl.STATIC_DRAW) 219 return &pathData{ 220 ncurves: len(p) / path.VertStride, 221 data: buf, 222 } 223 } 224 225 func (p *pathData) release(ctx *context) { 226 ctx.DeleteBuffer(p.data) 227 } 228 229 func (p *pather) begin(sizes []image.Point) { 230 p.stenciler.begin(sizes) 231 } 232 233 func (p *pather) end() { 234 p.stenciler.end() 235 } 236 237 func (p *pather) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data *pathData) { 238 p.stenciler.stencilPath(bounds, offset, uv, data) 239 } 240 241 func (s *stenciler) beginIntersect(sizes []image.Point) { 242 s.ctx.ActiveTexture(gl.TEXTURE1) 243 s.ctx.BindTexture(gl.TEXTURE_2D, gl.Texture{}) 244 s.ctx.ActiveTexture(gl.TEXTURE0) 245 s.ctx.BlendFunc(gl.DST_COLOR, gl.ZERO) 246 // 8 bit coverage is enough, but OpenGL ES only supports single channel 247 // floating point formats. Replace with GL_RGB+GL_UNSIGNED_BYTE if 248 // no floating point support is available. 249 s.intersections.resize(s.ctx, sizes) 250 s.ctx.ClearColor(1.0, 0.0, 0.0, 0.0) 251 s.ctx.UseProgram(s.iprog) 252 } 253 254 func (s *stenciler) endIntersect() { 255 s.ctx.BindFramebuffer(gl.FRAMEBUFFER, s.defFBO) 256 } 257 258 func (s *stenciler) invalidateFBO() { 259 s.intersections.invalidate(s.ctx) 260 s.fbos.invalidate(s.ctx) 261 s.ctx.BindFramebuffer(gl.FRAMEBUFFER, s.defFBO) 262 } 263 264 func (s *stenciler) cover(idx int) stencilFBO { 265 return s.fbos.fbos[idx] 266 } 267 268 func (s *stenciler) begin(sizes []image.Point) { 269 s.ctx.ActiveTexture(gl.TEXTURE1) 270 s.ctx.BindTexture(gl.TEXTURE_2D, gl.Texture{}) 271 s.ctx.ActiveTexture(gl.TEXTURE0) 272 s.ctx.BlendFunc(gl.ONE, gl.ONE) 273 s.fbos.resize(s.ctx, sizes) 274 s.ctx.ClearColor(0.0, 0.0, 0.0, 0.0) 275 s.ctx.UseProgram(s.prog) 276 s.ctx.EnableVertexAttribArray(attribPathCorner) 277 s.ctx.EnableVertexAttribArray(attribPathMaxY) 278 s.ctx.EnableVertexAttribArray(attribPathFrom) 279 s.ctx.EnableVertexAttribArray(attribPathCtrl) 280 s.ctx.EnableVertexAttribArray(attribPathTo) 281 s.ctx.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, s.indexBuf) 282 } 283 284 func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data *pathData) { 285 s.ctx.BindBuffer(gl.ARRAY_BUFFER, data.data) 286 s.ctx.Viewport(uv.X, uv.Y, bounds.Dx(), bounds.Dy()) 287 // Transform UI coordinates to OpenGL coordinates. 288 texSize := f32.Point{X: float32(bounds.Dx()), Y: float32(bounds.Dy())} 289 scale := f32.Point{X: 2 / texSize.X, Y: 2 / texSize.Y} 290 orig := f32.Point{X: -1 - float32(bounds.Min.X)*2/texSize.X, Y: -1 - float32(bounds.Min.Y)*2/texSize.Y} 291 s.ctx.Uniform2f(s.uScale, scale.X, scale.Y) 292 s.ctx.Uniform2f(s.uOffset, orig.X, orig.Y) 293 s.ctx.Uniform2f(s.uPathOffset, offset.X, offset.Y) 294 // Draw in batches that fit in uint16 indices. 295 start := 0 296 nquads := data.ncurves / 4 297 for start < nquads { 298 batch := nquads - start 299 if max := int(^uint16(0)) / 6; batch > max { 300 batch = max 301 } 302 // Enlarge VBO if necessary. 303 if batch > s.indexBufQuads { 304 indices := make([]uint16, batch*6) 305 for i := 0; i < batch; i++ { 306 i := uint16(i) 307 indices[i*6+0] = i*4 + 0 308 indices[i*6+1] = i*4 + 1 309 indices[i*6+2] = i*4 + 2 310 indices[i*6+3] = i*4 + 2 311 indices[i*6+4] = i*4 + 1 312 indices[i*6+5] = i*4 + 3 313 } 314 s.ctx.BufferData(gl.ELEMENT_ARRAY_BUFFER, gl.BytesView(indices), gl.STATIC_DRAW) 315 s.indexBufQuads = batch 316 } 317 off := path.VertStride * start * 4 318 s.ctx.VertexAttribPointer(attribPathCorner, 2, gl.SHORT, false, path.VertStride, off+int(unsafe.Offsetof((*(*path.Vertex)(nil)).CornerX))) 319 s.ctx.VertexAttribPointer(attribPathMaxY, 1, gl.FLOAT, false, path.VertStride, off+int(unsafe.Offsetof((*(*path.Vertex)(nil)).MaxY))) 320 s.ctx.VertexAttribPointer(attribPathFrom, 2, gl.FLOAT, false, path.VertStride, off+int(unsafe.Offsetof((*(*path.Vertex)(nil)).FromX))) 321 s.ctx.VertexAttribPointer(attribPathCtrl, 2, gl.FLOAT, false, path.VertStride, off+int(unsafe.Offsetof((*(*path.Vertex)(nil)).CtrlX))) 322 s.ctx.VertexAttribPointer(attribPathTo, 2, gl.FLOAT, false, path.VertStride, off+int(unsafe.Offsetof((*(*path.Vertex)(nil)).ToX))) 323 s.ctx.DrawElements(gl.TRIANGLES, batch*6, gl.UNSIGNED_SHORT, 0) 324 start += batch 325 } 326 } 327 328 func (s *stenciler) end() { 329 s.ctx.DisableVertexAttribArray(attribPathCorner) 330 s.ctx.DisableVertexAttribArray(attribPathMaxY) 331 s.ctx.DisableVertexAttribArray(attribPathFrom) 332 s.ctx.DisableVertexAttribArray(attribPathCtrl) 333 s.ctx.DisableVertexAttribArray(attribPathTo) 334 s.ctx.BindFramebuffer(gl.FRAMEBUFFER, s.defFBO) 335 } 336 337 func (p *pather) cover(z float32, mat materialType, col [4]float32, scale, off, uvScale, uvOff, coverScale, coverOff f32.Point) { 338 p.coverer.cover(z, mat, col, scale, off, uvScale, uvOff, coverScale, coverOff) 339 } 340 341 func (c *coverer) cover(z float32, mat materialType, col [4]float32, scale, off, uvScale, uvOff, coverScale, coverOff f32.Point) { 342 c.ctx.UseProgram(c.prog[mat]) 343 switch mat { 344 case materialColor: 345 c.ctx.Uniform4f(c.vars[mat].uColor, col[0], col[1], col[2], col[3]) 346 case materialTexture: 347 c.ctx.Uniform2f(c.vars[mat].uUVScale, uvScale.X, uvScale.Y) 348 c.ctx.Uniform2f(c.vars[mat].uUVOffset, uvOff.X, uvOff.Y) 349 } 350 c.ctx.Uniform1f(c.vars[mat].z, z) 351 c.ctx.Uniform2f(c.vars[mat].uScale, scale.X, scale.Y) 352 c.ctx.Uniform2f(c.vars[mat].uOffset, off.X, off.Y) 353 c.ctx.Uniform2f(c.vars[mat].uCoverUVScale, coverScale.X, coverScale.Y) 354 c.ctx.Uniform2f(c.vars[mat].uCoverUVOffset, coverOff.X, coverOff.Y) 355 c.ctx.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) 356 } 357 358 const stencilVSrc = ` 359 #version 100 360 361 precision highp float; 362 363 uniform vec2 scale; 364 uniform vec2 offset; 365 uniform vec2 pathOffset; 366 367 attribute vec2 corner; 368 attribute float maxy; 369 attribute vec2 from; 370 attribute vec2 ctrl; 371 attribute vec2 to; 372 373 varying vec2 vFrom; 374 varying vec2 vCtrl; 375 varying vec2 vTo; 376 377 void main() { 378 // Add a one pixel overlap so curve quads cover their 379 // entire curves. Could use conservative rasterization 380 // if available. 381 vec2 from = from + pathOffset; 382 vec2 ctrl = ctrl + pathOffset; 383 vec2 to = to + pathOffset; 384 float maxy = maxy + pathOffset.y; 385 vec2 pos; 386 if (corner.x > 0.0) { 387 // East. 388 pos.x = max(max(from.x, ctrl.x), to.x)+1.0; 389 } else { 390 // West. 391 pos.x = min(min(from.x, ctrl.x), to.x)-1.0; 392 } 393 if (corner.y > 0.0) { 394 // North. 395 pos.y = maxy + 1.0; 396 } else { 397 // South. 398 pos.y = min(min(from.y, ctrl.y), to.y) - 1.0; 399 } 400 vFrom = from-pos; 401 vCtrl = ctrl-pos; 402 vTo = to-pos; 403 pos *= scale; 404 pos += offset; 405 gl_Position = vec4(pos, 1, 1); 406 } 407 ` 408 409 const stencilFSrc = ` 410 #version 100 411 412 precision mediump float; 413 414 varying vec2 vFrom; 415 varying vec2 vCtrl; 416 varying vec2 vTo; 417 418 uniform sampler2D areaLUT; 419 420 void main() { 421 float dx = vTo.x - vFrom.x; 422 // Sort from and to in increasing order so the root below 423 // is always the positive square root, if any. 424 // We need the direction of the curve below, so this can't be 425 // done from the vertex shader. 426 bool increasing = vTo.x >= vFrom.x; 427 vec2 left = increasing ? vFrom : vTo; 428 vec2 right = increasing ? vTo : vFrom; 429 430 // The signed horizontal extent of the fragment. 431 vec2 extent = clamp(vec2(vFrom.x, vTo.x), -0.5, 0.5); 432 // Find the t where the curve crosses the middle of the 433 // extent, x₀. 434 // Given the Bézier curve with x coordinates P₀, P₁, P₂ 435 // where P₀ is at the origin, its x coordinate in t 436 // is given by: 437 // 438 // x(t) = 2(1-t)tP₁ + t²P₂ 439 // 440 // Rearranging: 441 // 442 // x(t) = (P₂ - 2P₁)t² + 2P₁t 443 // 444 // Setting x(t) = x₀ and using Muller's quadratic formula ("Citardauq") 445 // for robustnesss, 446 // 447 // t = 2x₀/(2P₁±√(4P₁²+4(P₂-2P₁)x₀)) 448 // 449 // which simplifies to 450 // 451 // t = x₀/(P₁±√(P₁²+(P₂-2P₁)x₀)) 452 // 453 // Setting v = P₂-P₁, 454 // 455 // t = x₀/(P₁±√(P₁²+(v-P₁)x₀)) 456 // 457 // t lie in [0; 1]; P₂ ≥ P₁ and P₁ ≥ 0 since we split curves where 458 // the control point lies before the start point or after the end point. 459 // It can then be shown that only the positive square root is valid. 460 float midx = mix(extent.x, extent.y, 0.5); 461 float x0 = midx - left.x; 462 vec2 p1 = vCtrl - left; 463 vec2 v = right - vCtrl; 464 float t = x0/(p1.x+sqrt(p1.x*p1.x+(v.x-p1.x)*x0)); 465 // Find y(t) on the curve. 466 float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t); 467 // And the slope. 468 vec2 d_half = mix(p1, v, t); 469 float dy = d_half.y/d_half.x; 470 // Together, y and dy form a line approximation. 471 472 // Compute the fragment area above the line. 473 // The area is symmetric around dy = 0. Scale slope with extent width. 474 float width = extent.y - extent.x; 475 dy = abs(dy*width); 476 477 vec4 sides = vec4(dy*+0.5 + y, dy*-0.5 + y, (+0.5-y)/dy, (-0.5-y)/dy); 478 sides = clamp(sides+0.5, 0.0, 1.0); 479 480 float area = 0.5*(sides.z - sides.z*sides.y + 1.0 - sides.x+sides.x*sides.w); 481 area *= width; 482 483 // Work around issue #13. 484 if (width == 0.0) 485 area = 0.0; 486 487 gl_FragColor.r = area; 488 } 489 ` 490 491 const coverVSrc = ` 492 #version 100 493 494 precision highp float; 495 496 uniform float z; 497 uniform vec2 scale; 498 uniform vec2 offset; 499 uniform vec2 uvScale; 500 uniform vec2 uvOffset; 501 uniform vec2 uvCoverScale; 502 uniform vec2 uvCoverOffset; 503 504 attribute vec2 pos; 505 506 varying vec2 vCoverUV; 507 508 attribute vec2 uv; 509 varying vec2 vUV; 510 511 void main() { 512 gl_Position = vec4(pos*scale + offset, z, 1); 513 vUV = uv*uvScale + uvOffset; 514 vCoverUV = uv*uvCoverScale+uvCoverOffset; 515 } 516 ` 517 518 const coverFSrc = ` 519 #version 100 520 521 precision mediump float; 522 523 // Use high precision to be pixel accurate for 524 // large cover atlases. 525 varying highp vec2 vCoverUV; 526 uniform sampler2D cover; 527 varying vec2 vUV; 528 529 HEADER 530 531 void main() { 532 gl_FragColor = GET_COLOR; 533 float cover = abs(texture2D(cover, vCoverUV).r); 534 gl_FragColor *= cover; 535 } 536 ` 537 538 const intersectVSrc = ` 539 #version 100 540 541 precision highp float; 542 543 attribute vec2 pos; 544 attribute vec2 uv; 545 546 uniform vec2 uvScale; 547 uniform vec2 uvOffset; 548 549 varying vec2 vUV; 550 551 void main() { 552 vec2 p = pos; 553 p.y = -p.y; 554 gl_Position = vec4(p, 0, 1); 555 vUV = uv*uvScale + uvOffset; 556 } 557 ` 558 559 const intersectFSrc = ` 560 #version 100 561 562 precision mediump float; 563 564 // Use high precision to be pixel accurate for 565 // large cover atlases. 566 varying highp vec2 vUV; 567 uniform sampler2D cover; 568 569 void main() { 570 float cover = abs(texture2D(cover, vUV).r); 571 gl_FragColor.r = cover; 572 } 573 `