feat(cef): popup rendering, OnPaint multi-thread safety, render pipeline improvements
- Add popup overlay rendering (OnPopupShow, OnPopupSize, handlePopupPaint) - Multi-threaded OnPaint: copy buffer before GTK dispatch when using CEF's multi-threaded message loop - Add paint sequence tracking for frame ordering - Enable LocalStorage in BrowserSettings - Remove video diagnostic JS injection (no longer needed) - Add render pipeline tests
- commit
- 3aee6b3e0ab698929647debea099681ad1f04eb7
- author
- brice · · 2026-03-24 05:43 UTC
- committer
- brice · · 2026-03-28 04:38 UTC
- parent
- 4749d51
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +378 -48
M internal/infrastructure/cef/content_injector.go +1 -5
| @@ -13,10 +13,6 @@ import ( | |||
| 13 | 13 | // Compile-time interface check. | |
| 14 | 14 | var _ port.ContentInjector = (*contentInjector)(nil) | |
| 15 | 15 | ||
| 16 | - | // internalSchemePrefix is used to detect internal pages that receive dark mode | |
| 17 | - | // and message bridge scripts. Must match the engine's InternalSchemePath prefix. | |
| 18 | - | const internalSchemePrefix = "dumb://" | |
| 19 | - | ||
| 20 | 16 | // scrollbarCSS styles the scrollbar with auto-hide behavior: invisible by | |
| 21 | 17 | // default, fades in on scroll, widens on hover, fades out after 1s idle. | |
| 22 | 18 | // Uses --primary theme color for the thumb. | |
| @@ -247,7 +243,7 @@ func (ci *contentInjector) RefreshScripts(ctx context.Context, wv port.WebView) | |||
| 247 | 243 | // It injects the appropriate scripts based on whether the page is internal. | |
| 248 | 244 | func (ci *contentInjector) onLoadEnd(wv *WebView) { | |
| 249 | 245 | uri := wv.URI() | |
| 250 | - | isInternal := strings.HasPrefix(uri, internalSchemePrefix) | |
| 246 | + | isInternal := isConceptualInternalURL(uri) || isActualInternalURL(uri) | |
| 251 | 247 | ||
| 252 | 248 | ci.mu.RLock() | |
| 253 | 249 | themeCSS := ci.themeCSS | |
| @@ -13,10 +13,6 @@ import ( | |||||
| 13 | // Compile-time interface check. | 13 | // Compile-time interface check. | ||
| 14 | var _ port.ContentInjector = (*contentInjector)(nil) | 14 | var _ port.ContentInjector = (*contentInjector)(nil) | ||
| 15 | 15 | ||||
| 16 | - | // internalSchemePrefix is used to detect internal pages that receive dark mode | |||
| 17 | - | // and message bridge scripts. Must match the engine's InternalSchemePath prefix. | |||
| 18 | - | const internalSchemePrefix = "dumb://" | |||
| 19 | - | ||||
| 20 | // scrollbarCSS styles the scrollbar with auto-hide behavior: invisible by | 16 | // scrollbarCSS styles the scrollbar with auto-hide behavior: invisible by | ||
| 21 | // default, fades in on scroll, widens on hover, fades out after 1s idle. | 17 | // default, fades in on scroll, widens on hover, fades out after 1s idle. | ||
| 22 | // Uses --primary theme color for the thumb. | 18 | // Uses --primary theme color for the thumb. | ||
| @@ -247,7 +243,7 @@ func (ci *contentInjector) RefreshScripts(ctx context.Context, wv port.WebView) | |||||
| 247 | // It injects the appropriate scripts based on whether the page is internal. | 243 | // It injects the appropriate scripts based on whether the page is internal. | ||
| 248 | func (ci *contentInjector) onLoadEnd(wv *WebView) { | 244 | func (ci *contentInjector) onLoadEnd(wv *WebView) { | ||
| 249 | uri := wv.URI() | 245 | uri := wv.URI() | ||
| 250 | - | isInternal := strings.HasPrefix(uri, internalSchemePrefix) | 246 | + | isInternal := isConceptualInternalURL(uri) || isActualInternalURL(uri) |
| 251 | 247 | ||||
| 252 | ci.mu.RLock() | 248 | ci.mu.RLock() | ||
| 253 | themeCSS := ci.themeCSS | 249 | themeCSS := ci.themeCSS | ||
M internal/infrastructure/cef/factory.go +1 -0
| @@ -111,6 +111,7 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) { | |||
| 111 | 111 | // Configure BrowserSettings. | |
| 112 | 112 | settings := purecef.DefaultBrowserSettings() | |
| 113 | 113 | settings.WindowlessFrameRate = f.windowlessFrameRate | |
| 114 | + | settings.LocalStorage = 1 // CEF_STATE_ENABLED | |
| 114 | 115 | if bg := f.bgColor.Load(); bg != 0 { | |
| 115 | 116 | settings.BackgroundColor = bg | |
| 116 | 117 | } | |
| @@ -111,6 +111,7 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) { | |||||
| 111 | // Configure BrowserSettings. | 111 | // Configure BrowserSettings. | ||
| 112 | settings := purecef.DefaultBrowserSettings() | 112 | settings := purecef.DefaultBrowserSettings() | ||
| 113 | settings.WindowlessFrameRate = f.windowlessFrameRate | 113 | settings.WindowlessFrameRate = f.windowlessFrameRate | ||
| 114 | + | settings.LocalStorage = 1 // CEF_STATE_ENABLED | |||
| 114 | if bg := f.bgColor.Load(); bg != 0 { | 115 | if bg := f.bgColor.Load(); bg != 0 { | ||
| 115 | settings.BackgroundColor = bg | 116 | settings.BackgroundColor = bg | ||
| 116 | } | 117 | } | ||
M internal/infrastructure/cef/render_pipeline.go +298 -36
| @@ -29,12 +29,13 @@ type renderPipeline struct { | |||
| 29 | 29 | glArea *gtk.GLArea | |
| 30 | 30 | ||
| 31 | 31 | // GL resources (created on first render). | |
| 32 | - | texture uint32 | |
| 33 | - | pbo [2]uint32 | |
| 34 | - | pboIndex int | |
| 35 | - | program uint32 | |
| 36 | - | vao uint32 | |
| 37 | - | vbo uint32 | |
| 32 | + | texture uint32 | |
| 33 | + | popupTexture uint32 | |
| 34 | + | pbo [2]uint32 | |
| 35 | + | pboIndex int | |
| 36 | + | program uint32 | |
| 37 | + | vao uint32 | |
| 38 | + | vbo uint32 | |
| 38 | 39 | ||
| 39 | 40 | // Surface dimensions (in device pixels, i.e. scaled). | |
| 40 | 41 | width int32 | |
| @@ -52,6 +53,15 @@ type renderPipeline struct { | |||
| 52 | 53 | needsUpload bool | |
| 53 | 54 | sizeChanged bool | |
| 54 | 55 | ||
| 56 | + | // Popup staging for native CEF widgets like <select> menus. | |
| 57 | + | popupVisible bool | |
| 58 | + | popupRect rect | |
| 59 | + | popupStaging []byte | |
| 60 | + | popupWidth int32 | |
| 61 | + | popupHeight int32 | |
| 62 | + | popupNeedsUpload bool | |
| 63 | + | popupSizeChanged bool | |
| 64 | + | ||
| 55 | 65 | // GL initialized flag. | |
| 56 | 66 | glReady bool | |
| 57 | 67 | ||
| @@ -179,12 +189,16 @@ func (rp *renderPipeline) handlePaint(buffer []byte, width, height int32, rects | |||
| 179 | 189 | } | |
| 180 | 190 | rp.mu.Lock() | |
| 181 | 191 | ||
| 182 | - | bufSize := int(int64(width) * int64(height) * 4) | |
| 192 | + | bufSize, ok := bgraBufferSize(width, height) | |
| 193 | + | if !ok { | |
| 194 | + | rp.mu.Unlock() | |
| 195 | + | return | |
| 196 | + | } | |
| 183 | 197 | copiedBytes := uint64(0) | |
| 184 | 198 | sizeChanged := false | |
| 185 | 199 | ||
| 186 | 200 | // Detect size change, or first paint (staging not yet allocated). | |
| 187 | - | if width != rp.width || height != rp.height || rp.staging == nil { | |
| 201 | + | if stagingNeedsReset(rp.staging, rp.width, rp.height, width, height, bufSize) { | |
| 188 | 202 | rp.width = width | |
| 189 | 203 | rp.height = height | |
| 190 | 204 | rp.widthAtomic.Store(width) | |
| @@ -193,27 +207,20 @@ func (rp *renderPipeline) handlePaint(buffer []byte, width, height int32, rects | |||
| 193 | 207 | rp.sizeChanged = true | |
| 194 | 208 | sizeChanged = true | |
| 195 | 209 | // On size change, copy the entire buffer. | |
| 196 | - | copy(rp.staging, buffer) | |
| 197 | - | copiedBytes = uint64(bufSize) | |
| 210 | + | copiedBytes = uint64(copy(rp.staging, buffer)) | |
| 198 | 211 | } else { | |
| 199 | 212 | // Copy only dirty rect rows. | |
| 200 | - | stride := int(width) * 4 | |
| 201 | - | sanitized := make([]rect, 0, len(rects)) | |
| 202 | - | for _, rawRect := range rects { | |
| 203 | - | r, ok := clampDirtyRect(rawRect, width, height) | |
| 204 | - | if !ok { | |
| 205 | - | continue | |
| 206 | - | } | |
| 207 | - | sanitized = append(sanitized, r) | |
| 208 | - | for row := r.Y; row < r.Y+r.Height; row++ { | |
| 209 | - | srcOff := int(row)*stride + int(r.X)*4 | |
| 210 | - | dstOff := srcOff | |
| 211 | - | rowBytes := int(r.Width) * 4 | |
| 212 | - | copy(rp.staging[dstOff:dstOff+rowBytes], buffer[srcOff:srcOff+rowBytes]) | |
| 213 | - | copiedBytes += uint64(rowBytes) | |
| 214 | - | } | |
| 213 | + | var truncated bool | |
| 214 | + | copiedBytes, rects, truncated = copyDirtyRectsIntoStaging(rp.staging, buffer, width, height, rects) | |
| 215 | + | if truncated && rp.ctx != nil { | |
| 216 | + | logging.FromContext(rp.ctx).Warn(). | |
| 217 | + | Uint64("paint_seq", paintSeq). | |
| 218 | + | Int32("width", width). | |
| 219 | + | Int32("height", height). | |
| 220 | + | Int("buffer_len", len(buffer)). | |
| 221 | + | Int("expected_len", bufSize). | |
| 222 | + | Msg("cef: truncated paint buffer while copying dirty rects") | |
| 215 | 223 | } | |
| 216 | - | rects = sanitized | |
| 217 | 224 | } | |
| 218 | 225 | ||
| 219 | 226 | // Accumulate dirty rects across multiple OnPaint calls between renders. | |
| @@ -237,6 +244,15 @@ func (rp *renderPipeline) handlePaint(buffer []byte, width, height int32, rects | |||
| 237 | 244 | ||
| 238 | 245 | rp.glArea.QueueRender() | |
| 239 | 246 | if rp.ctx != nil { | |
| 247 | + | if sizeChanged && copiedBytes < uint64(bufSize) { | |
| 248 | + | logging.FromContext(rp.ctx).Warn(). | |
| 249 | + | Uint64("paint_seq", paintSeq). | |
| 250 | + | Int32("width", width). | |
| 251 | + | Int32("height", height). | |
| 252 | + | Int("buffer_len", len(buffer)). | |
| 253 | + | Int("expected_len", bufSize). | |
| 254 | + | Msg("cef: truncated full paint buffer on resize") | |
| 255 | + | } | |
| 240 | 256 | logging.FromContext(rp.ctx).Debug(). | |
| 241 | 257 | Uint64("paint_seq", paintSeq). | |
| 242 | 258 | Uint64("copied_bytes", copiedBytes). | |
| @@ -306,16 +322,16 @@ func (rp *renderPipeline) onGLRender() bool { | |||
| 306 | 322 | } | |
| 307 | 323 | } | |
| 308 | 324 | ||
| 309 | - | // Draw fullscreen quad. | |
| 325 | + | // Draw the main view texture. | |
| 326 | + | w, h, _ := rp.viewRectSize() | |
| 327 | + | gl.viewport(0, 0, w, h) | |
| 310 | 328 | gl.clearColor(0, 0, 0, 1) | |
| 311 | 329 | gl.clear(glColorBufferBit) | |
| 312 | - | gl.useProgram(rp.program) | |
| 313 | - | gl.bindTexture(glTexture2D, rp.texture) | |
| 314 | - | gl.bindVertexArray(rp.vao) | |
| 315 | - | gl.drawArrays(glTriangleStrip, 0, 4) | |
| 316 | - | gl.bindVertexArray(0) | |
| 317 | - | gl.bindTexture(glTexture2D, 0) | |
| 318 | - | gl.useProgram(0) | |
| 330 | + | rp.drawTexture(rp.texture) | |
| 331 | + | ||
| 332 | + | if rp.uploadPopupTexture(renderSeq, paintSeq) { | |
| 333 | + | rp.drawPopupOverlay(w, h) | |
| 334 | + | } | |
| 319 | 335 | ||
| 320 | 336 | rp.glRenderCount++ | |
| 321 | 337 | renderDuration := time.Since(renderStartedAt) | |
| @@ -337,6 +353,20 @@ func (rp *renderPipeline) onGLRender() bool { | |||
| 337 | 353 | return true | |
| 338 | 354 | } | |
| 339 | 355 | ||
| 356 | + | func (rp *renderPipeline) drawTexture(texture uint32) { | |
| 357 | + | if texture == 0 { | |
| 358 | + | return | |
| 359 | + | } | |
| 360 | + | gl := rp.gl | |
| 361 | + | gl.useProgram(rp.program) | |
| 362 | + | gl.bindTexture(glTexture2D, texture) | |
| 363 | + | gl.bindVertexArray(rp.vao) | |
| 364 | + | gl.drawArrays(glTriangleStrip, 0, 4) | |
| 365 | + | gl.bindVertexArray(0) | |
| 366 | + | gl.bindTexture(glTexture2D, 0) | |
| 367 | + | gl.useProgram(0) | |
| 368 | + | } | |
| 369 | + | ||
| 340 | 370 | // uploadToPBO uploads dirty regions from the staging buffer into a PBO, then | |
| 341 | 371 | // transfers to the GL texture via async DMA. | |
| 342 | 372 | func (rp *renderPipeline) uploadToPBO(dirtyRects []rect, fullUpload bool, renderSeq, paintSeq uint64) uint64 { | |
| @@ -373,8 +403,11 @@ func (rp *renderPipeline) uploadToPBO(dirtyRects []rect, fullUpload bool, render | |||
| 373 | 403 | ||
| 374 | 404 | rp.mu.Lock() | |
| 375 | 405 | if fullUpload { | |
| 376 | - | copy(mappedSlice, rp.staging) | |
| 377 | - | uploadedBytes = uint64(bufSize) | |
| 406 | + | copied := copy(mappedSlice, rp.staging) | |
| 407 | + | if copied < len(mappedSlice) { | |
| 408 | + | clear(mappedSlice[copied:]) | |
| 409 | + | } | |
| 410 | + | uploadedBytes = uint64(copied) | |
| 378 | 411 | } else { | |
| 379 | 412 | for _, rawRect := range dirtyRects { | |
| 380 | 413 | r, ok := clampDirtyRect(rawRect, w, h) | |
| @@ -440,6 +473,84 @@ func (rp *renderPipeline) uploadToPBO(dirtyRects []rect, fullUpload bool, render | |||
| 440 | 473 | return uploadedBytes | |
| 441 | 474 | } | |
| 442 | 475 | ||
| 476 | + | func (rp *renderPipeline) uploadPopupTexture(renderSeq, paintSeq uint64) bool { | |
| 477 | + | rp.mu.Lock() | |
| 478 | + | defer rp.mu.Unlock() | |
| 479 | + | ||
| 480 | + | if !rp.popupVisible || rp.popupWidth <= 0 || rp.popupHeight <= 0 || len(rp.popupStaging) == 0 { | |
| 481 | + | return false | |
| 482 | + | } | |
| 483 | + | ||
| 484 | + | if !rp.popupNeedsUpload && !rp.popupSizeChanged && rp.popupTexture != 0 { | |
| 485 | + | return true | |
| 486 | + | } | |
| 487 | + | ||
| 488 | + | gl := rp.gl | |
| 489 | + | ||
| 490 | + | if rp.popupTexture == 0 { | |
| 491 | + | gl.genTextures(1, &rp.popupTexture) | |
| 492 | + | gl.bindTexture(glTexture2D, rp.popupTexture) | |
| 493 | + | gl.texParameteri(glTexture2D, glTextureMinFilter, int32(glLinear)) | |
| 494 | + | gl.texParameteri(glTexture2D, glTextureMagFilter, int32(glLinear)) | |
| 495 | + | gl.texParameteri(glTexture2D, glTextureWrapS, int32(glClampToEdge)) | |
| 496 | + | gl.texParameteri(glTexture2D, glTextureWrapT, int32(glClampToEdge)) | |
| 497 | + | } else { | |
| 498 | + | gl.bindTexture(glTexture2D, rp.popupTexture) | |
| 499 | + | } | |
| 500 | + | ||
| 501 | + | pixels := unsafe.Pointer(&rp.popupStaging[0]) | |
| 502 | + | if rp.popupSizeChanged { | |
| 503 | + | gl.texImage2D(glTexture2D, 0, int32(glRGBA), rp.popupWidth, rp.popupHeight, 0, glBGRA, glUnsignedByte, pixels) | |
| 504 | + | } else { | |
| 505 | + | gl.texSubImage2D(glTexture2D, 0, 0, 0, rp.popupWidth, rp.popupHeight, glBGRA, glUnsignedByte, pixels) | |
| 506 | + | } | |
| 507 | + | gl.bindTexture(glTexture2D, 0) | |
| 508 | + | ||
| 509 | + | rp.popupNeedsUpload = false | |
| 510 | + | rp.popupSizeChanged = false | |
| 511 | + | ||
| 512 | + | if rp.ctx != nil { | |
| 513 | + | logging.FromContext(rp.ctx).Debug(). | |
| 514 | + | Uint64("render_seq", renderSeq). | |
| 515 | + | Uint64("paint_seq", paintSeq). | |
| 516 | + | Int32("popup_width", rp.popupWidth). | |
| 517 | + | Int32("popup_height", rp.popupHeight). | |
| 518 | + | Msg("cef: popup texture uploaded") | |
| 519 | + | } | |
| 520 | + | ||
| 521 | + | return true | |
| 522 | + | } | |
| 523 | + | ||
| 524 | + | func (rp *renderPipeline) drawPopupOverlay(viewWidth, viewHeight int32) { | |
| 525 | + | rp.mu.Lock() | |
| 526 | + | popupVisible := rp.popupVisible | |
| 527 | + | popupRect := rp.popupRect | |
| 528 | + | popupWidth := rp.popupWidth | |
| 529 | + | popupHeight := rp.popupHeight | |
| 530 | + | texture := rp.popupTexture | |
| 531 | + | rp.mu.Unlock() | |
| 532 | + | ||
| 533 | + | if !popupVisible || texture == 0 || popupWidth <= 0 || popupHeight <= 0 || viewWidth <= 0 || viewHeight <= 0 { | |
| 534 | + | return | |
| 535 | + | } | |
| 536 | + | ||
| 537 | + | popupRect.Width = popupWidth | |
| 538 | + | popupRect.Height = popupHeight | |
| 539 | + | popupRect, ok := clampDirtyRect(popupRect, viewWidth, viewHeight) | |
| 540 | + | if !ok { | |
| 541 | + | return | |
| 542 | + | } | |
| 543 | + | ||
| 544 | + | viewportY := viewHeight - popupRect.Y - popupRect.Height | |
| 545 | + | if viewportY < 0 { | |
| 546 | + | viewportY = 0 | |
| 547 | + | } | |
| 548 | + | ||
| 549 | + | rp.gl.viewport(popupRect.X, viewportY, popupRect.Width, popupRect.Height) | |
| 550 | + | rp.drawTexture(texture) | |
| 551 | + | rp.gl.viewport(0, 0, viewWidth, viewHeight) | |
| 552 | + | } | |
| 553 | + | ||
| 443 | 554 | // initGL creates or recreates all GL resources for the current surface size. | |
| 444 | 555 | func (rp *renderPipeline) initGL() { | |
| 445 | 556 | rp.mu.Lock() | |
| @@ -662,6 +773,70 @@ func (rp *renderPipeline) onResize(width, height int32) { | |||
| 662 | 773 | } | |
| 663 | 774 | } | |
| 664 | 775 | ||
| 776 | + | func (rp *renderPipeline) setPopupVisible(show bool) { | |
| 777 | + | rp.mu.Lock() | |
| 778 | + | rp.popupVisible = show | |
| 779 | + | if !show { | |
| 780 | + | rp.popupStaging = nil | |
| 781 | + | rp.popupWidth = 0 | |
| 782 | + | rp.popupHeight = 0 | |
| 783 | + | rp.popupNeedsUpload = false | |
| 784 | + | rp.popupSizeChanged = false | |
| 785 | + | } | |
| 786 | + | rp.mu.Unlock() | |
| 787 | + | rp.glArea.QueueRender() | |
| 788 | + | } | |
| 789 | + | ||
| 790 | + | func (rp *renderPipeline) setPopupRect(popup rect) { | |
| 791 | + | rp.mu.Lock() | |
| 792 | + | rp.popupRect = rect{ | |
| 793 | + | X: popup.X * rp.scale, | |
| 794 | + | Y: popup.Y * rp.scale, | |
| 795 | + | Width: popup.Width * rp.scale, | |
| 796 | + | Height: popup.Height * rp.scale, | |
| 797 | + | } | |
| 798 | + | rp.mu.Unlock() | |
| 799 | + | rp.glArea.QueueRender() | |
| 800 | + | } | |
| 801 | + | ||
| 802 | + | func (rp *renderPipeline) handlePopupPaint(buffer []byte, width, height int32, paintSeq uint64) { | |
| 803 | + | if len(buffer) == 0 || width <= 0 || height <= 0 { | |
| 804 | + | return | |
| 805 | + | } | |
| 806 | + | ||
| 807 | + | bufSize, ok := bgraBufferSize(width, height) | |
| 808 | + | if !ok { | |
| 809 | + | return | |
| 810 | + | } | |
| 811 | + | ||
| 812 | + | rp.mu.Lock() | |
| 813 | + | if len(rp.popupStaging) != bufSize { | |
| 814 | + | rp.popupStaging = make([]byte, bufSize) | |
| 815 | + | rp.popupSizeChanged = true | |
| 816 | + | } else if rp.popupWidth != width || rp.popupHeight != height { | |
| 817 | + | rp.popupSizeChanged = true | |
| 818 | + | } | |
| 819 | + | copied := copy(rp.popupStaging, buffer) | |
| 820 | + | if copied < len(rp.popupStaging) { | |
| 821 | + | clear(rp.popupStaging[copied:]) | |
| 822 | + | } | |
| 823 | + | rp.popupWidth = width | |
| 824 | + | rp.popupHeight = height | |
| 825 | + | rp.popupNeedsUpload = true | |
| 826 | + | rp.popupVisible = true | |
| 827 | + | rp.mu.Unlock() | |
| 828 | + | ||
| 829 | + | if rp.ctx != nil { | |
| 830 | + | logging.FromContext(rp.ctx).Debug(). | |
| 831 | + | Uint64("paint_seq", paintSeq). | |
| 832 | + | Int32("popup_width", width). | |
| 833 | + | Int32("popup_height", height). | |
| 834 | + | Msg("cef: handlePopupPaint") | |
| 835 | + | } | |
| 836 | + | ||
| 837 | + | rp.glArea.QueueRender() | |
| 838 | + | } | |
| 839 | + | ||
| 665 | 840 | func (rp *renderPipeline) viewRectSize() (width, height, scale int32) { | |
| 666 | 841 | width = rp.widthAtomic.Load() | |
| 667 | 842 | height = rp.heightAtomic.Load() | |
| @@ -728,6 +903,10 @@ func (rp *renderPipeline) deleteGLResources() { | |||
| 728 | 903 | gl.deleteTextures(1, &rp.texture) | |
| 729 | 904 | rp.texture = 0 | |
| 730 | 905 | } | |
| 906 | + | if rp.popupTexture != 0 { | |
| 907 | + | gl.deleteTextures(1, &rp.popupTexture) | |
| 908 | + | rp.popupTexture = 0 | |
| 909 | + | } | |
| 731 | 910 | for i := 0; i < 2; i++ { | |
| 732 | 911 | if rp.pbo[i] != 0 { | |
| 733 | 912 | gl.deleteBuffers(1, &rp.pbo[i]) | |
| @@ -750,6 +929,89 @@ func (rp *renderPipeline) deleteGLResources() { | |||
| 750 | 929 | } | |
| 751 | 930 | } | |
| 752 | 931 | ||
| 932 | + | func bgraBufferSize(width, height int32) (int, bool) { | |
| 933 | + | if width <= 0 || height <= 0 { | |
| 934 | + | return 0, false | |
| 935 | + | } | |
| 936 | + | size64 := int64(width) * int64(height) * 4 | |
| 937 | + | if size64 <= 0 { | |
| 938 | + | return 0, false | |
| 939 | + | } | |
| 940 | + | return int(size64), true | |
| 941 | + | } | |
| 942 | + | ||
| 943 | + | func stagingNeedsReset(staging []byte, currentWidth, currentHeight, nextWidth, nextHeight int32, expectedBytes int) bool { | |
| 944 | + | return staging == nil || | |
| 945 | + | len(staging) != expectedBytes || | |
| 946 | + | currentWidth != nextWidth || | |
| 947 | + | currentHeight != nextHeight | |
| 948 | + | } | |
| 949 | + | ||
| 950 | + | func copyDirtyRectsIntoStaging(dst, src []byte, width, height int32, rects []rect) (uint64, []rect, bool) { | |
| 951 | + | if len(dst) == 0 || len(src) == 0 || width <= 0 || height <= 0 { | |
| 952 | + | return 0, nil, false | |
| 953 | + | } | |
| 954 | + | ||
| 955 | + | stride := int(width) * 4 | |
| 956 | + | copiedBytes := uint64(0) | |
| 957 | + | truncated := false | |
| 958 | + | sanitized := make([]rect, 0, len(rects)) | |
| 959 | + | ||
| 960 | + | for _, rawRect := range rects { | |
| 961 | + | r, ok := clampDirtyRect(rawRect, width, height) | |
| 962 | + | if !ok { | |
| 963 | + | continue | |
| 964 | + | } | |
| 965 | + | sanitized = append(sanitized, r) | |
| 966 | + | ||
| 967 | + | for row := r.Y; row < r.Y+r.Height; row++ { | |
| 968 | + | dstOff := int(row)*stride + int(r.X)*4 | |
| 969 | + | if dstOff < 0 || dstOff >= len(dst) { | |
| 970 | + | truncated = true | |
| 971 | + | break | |
| 972 | + | } | |
| 973 | + | ||
| 974 | + | rowBytes := int(r.Width) * 4 | |
| 975 | + | dstEnd := dstOff + rowBytes | |
| 976 | + | if dstEnd > len(dst) { | |
| 977 | + | dstEnd = len(dst) | |
| 978 | + | truncated = true | |
| 979 | + | } | |
| 980 | + | ||
| 981 | + | srcOff := int(row)*stride + int(r.X)*4 | |
| 982 | + | if srcOff < 0 || srcOff >= len(src) { | |
| 983 | + | clear(dst[dstOff:dstEnd]) | |
| 984 | + | truncated = true | |
| 985 | + | continue | |
| 986 | + | } | |
| 987 | + | ||
| 988 | + | srcEnd := srcOff + rowBytes | |
| 989 | + | if srcEnd > len(src) { | |
| 990 | + | srcEnd = len(src) | |
| 991 | + | truncated = true | |
| 992 | + | } | |
| 993 | + | ||
| 994 | + | copyLen := srcEnd - srcOff | |
| 995 | + | maxDstLen := dstEnd - dstOff | |
| 996 | + | if copyLen > maxDstLen { | |
| 997 | + | copyLen = maxDstLen | |
| 998 | + | truncated = true | |
| 999 | + | } | |
| 1000 | + | ||
| 1001 | + | if copyLen > 0 { | |
| 1002 | + | copy(dst[dstOff:dstOff+copyLen], src[srcOff:srcOff+copyLen]) | |
| 1003 | + | copiedBytes += uint64(copyLen) | |
| 1004 | + | } | |
| 1005 | + | ||
| 1006 | + | if dstOff+copyLen < dstEnd { | |
| 1007 | + | clear(dst[dstOff+copyLen : dstEnd]) | |
| 1008 | + | } | |
| 1009 | + | } | |
| 1010 | + | } | |
| 1011 | + | ||
| 1012 | + | return copiedBytes, sanitized, truncated | |
| 1013 | + | } | |
| 1014 | + | ||
| 753 | 1015 | // pboOffset converts a byte offset into an unsafe.Pointer for use with | |
| 754 | 1016 | // glTexSubImage2D when a PBO is bound. OpenGL interprets the pointer as a | |
| 755 | 1017 | // byte offset into the bound buffer rather than a real memory address. | |
| @@ -29,12 +29,13 @@ type renderPipeline struct { | |||||
| 29 | glArea *gtk.GLArea | 29 | glArea *gtk.GLArea | ||
| 30 | 30 | ||||
| 31 | // GL resources (created on first render). | 31 | // GL resources (created on first render). | ||
| 32 | - | texture uint32 | 32 | + | texture uint32 |
| 33 | - | pbo [2]uint32 | 33 | + | popupTexture uint32 |
| 34 | - | pboIndex int | 34 | + | pbo [2]uint32 |
| 35 | - | program uint32 | 35 | + | pboIndex int |
| 36 | - | vao uint32 | 36 | + | program uint32 |
| 37 | - | vbo uint32 | 37 | + | vao uint32 |
| 38 | + | vbo uint32 | |||
| 38 | 39 | ||||
| 39 | // Surface dimensions (in device pixels, i.e. scaled). | 40 | // Surface dimensions (in device pixels, i.e. scaled). | ||
| 40 | width int32 | 41 | width int32 | ||
| @@ -52,6 +53,15 @@ type renderPipeline struct { | |||||
| 52 | needsUpload bool | 53 | needsUpload bool | ||
| 53 | sizeChanged bool | 54 | sizeChanged bool | ||
| 54 | 55 | ||||
| 56 | + | // Popup staging for native CEF widgets like <select> menus. | |||
| 57 | + | popupVisible bool | |||
| 58 | + | popupRect rect | |||
| 59 | + | popupStaging []byte | |||
| 60 | + | popupWidth int32 | |||
| 61 | + | popupHeight int32 | |||
| 62 | + | popupNeedsUpload bool | |||
| 63 | + | popupSizeChanged bool | |||
| 64 | + | ||||
| 55 | // GL initialized flag. | 65 | // GL initialized flag. | ||
| 56 | glReady bool | 66 | glReady bool | ||
| 57 | 67 | ||||
| @@ -179,12 +189,16 @@ func (rp *renderPipeline) handlePaint(buffer []byte, width, height int32, rects | |||||
| 179 | } | 189 | } | ||
| 180 | rp.mu.Lock() | 190 | rp.mu.Lock() | ||
| 181 | 191 | ||||
| 182 | - | bufSize := int(int64(width) * int64(height) * 4) | 192 | + | bufSize, ok := bgraBufferSize(width, height) |
| 193 | + | if !ok { | |||
| 194 | + | rp.mu.Unlock() | |||
| 195 | + | return | |||
| 196 | + | } | |||
| 183 | copiedBytes := uint64(0) | 197 | copiedBytes := uint64(0) | ||
| 184 | sizeChanged := false | 198 | sizeChanged := false | ||
| 185 | 199 | ||||
| 186 | // Detect size change, or first paint (staging not yet allocated). | 200 | // Detect size change, or first paint (staging not yet allocated). | ||
| 187 | - | if width != rp.width || height != rp.height || rp.staging == nil { | 201 | + | if stagingNeedsReset(rp.staging, rp.width, rp.height, width, height, bufSize) { |
| 188 | rp.width = width | 202 | rp.width = width | ||
| 189 | rp.height = height | 203 | rp.height = height | ||
| 190 | rp.widthAtomic.Store(width) | 204 | rp.widthAtomic.Store(width) | ||
| @@ -193,27 +207,20 @@ func (rp *renderPipeline) handlePaint(buffer []byte, width, height int32, rects | |||||
| 193 | rp.sizeChanged = true | 207 | rp.sizeChanged = true | ||
| 194 | sizeChanged = true | 208 | sizeChanged = true | ||
| 195 | // On size change, copy the entire buffer. | 209 | // On size change, copy the entire buffer. | ||
| 196 | - | copy(rp.staging, buffer) | 210 | + | copiedBytes = uint64(copy(rp.staging, buffer)) |
| 197 | - | copiedBytes = uint64(bufSize) | |||
| 198 | } else { | 211 | } else { | ||
| 199 | // Copy only dirty rect rows. | 212 | // Copy only dirty rect rows. | ||
| 200 | - | stride := int(width) * 4 | 213 | + | var truncated bool |
| 201 | - | sanitized := make([]rect, 0, len(rects)) | 214 | + | copiedBytes, rects, truncated = copyDirtyRectsIntoStaging(rp.staging, buffer, width, height, rects) |
| 202 | - | for _, rawRect := range rects { | 215 | + | if truncated && rp.ctx != nil { |
| 203 | - | r, ok := clampDirtyRect(rawRect, width, height) | 216 | + | logging.FromContext(rp.ctx).Warn(). |
| 204 | - | if !ok { | 217 | + | Uint64("paint_seq", paintSeq). |
| 205 | - | continue | 218 | + | Int32("width", width). |
| 206 | - | } | 219 | + | Int32("height", height). |
| 207 | - | sanitized = append(sanitized, r) | 220 | + | Int("buffer_len", len(buffer)). |
| 208 | - | for row := r.Y; row < r.Y+r.Height; row++ { | 221 | + | Int("expected_len", bufSize). |
| 209 | - | srcOff := int(row)*stride + int(r.X)*4 | 222 | + | Msg("cef: truncated paint buffer while copying dirty rects") |
| 210 | - | dstOff := srcOff | |||
| 211 | - | rowBytes := int(r.Width) * 4 | |||
| 212 | - | copy(rp.staging[dstOff:dstOff+rowBytes], buffer[srcOff:srcOff+rowBytes]) | |||
| 213 | - | copiedBytes += uint64(rowBytes) | |||
| 214 | - | } | |||
| 215 | } | 223 | } | ||
| 216 | - | rects = sanitized | |||
| 217 | } | 224 | } | ||
| 218 | 225 | ||||
| 219 | // Accumulate dirty rects across multiple OnPaint calls between renders. | 226 | // Accumulate dirty rects across multiple OnPaint calls between renders. | ||
| @@ -237,6 +244,15 @@ func (rp *renderPipeline) handlePaint(buffer []byte, width, height int32, rects | |||||
| 237 | 244 | ||||
| 238 | rp.glArea.QueueRender() | 245 | rp.glArea.QueueRender() | ||
| 239 | if rp.ctx != nil { | 246 | if rp.ctx != nil { | ||
| 247 | + | if sizeChanged && copiedBytes < uint64(bufSize) { | |||
| 248 | + | logging.FromContext(rp.ctx).Warn(). | |||
| 249 | + | Uint64("paint_seq", paintSeq). | |||
| 250 | + | Int32("width", width). | |||
| 251 | + | Int32("height", height). | |||
| 252 | + | Int("buffer_len", len(buffer)). | |||
| 253 | + | Int("expected_len", bufSize). | |||
| 254 | + | Msg("cef: truncated full paint buffer on resize") | |||
| 255 | + | } | |||
| 240 | logging.FromContext(rp.ctx).Debug(). | 256 | logging.FromContext(rp.ctx).Debug(). | ||
| 241 | Uint64("paint_seq", paintSeq). | 257 | Uint64("paint_seq", paintSeq). | ||
| 242 | Uint64("copied_bytes", copiedBytes). | 258 | Uint64("copied_bytes", copiedBytes). | ||
| @@ -306,16 +322,16 @@ func (rp *renderPipeline) onGLRender() bool { | |||||
| 306 | } | 322 | } | ||
| 307 | } | 323 | } | ||
| 308 | 324 | ||||
| 309 | - | // Draw fullscreen quad. | 325 | + | // Draw the main view texture. |
| 326 | + | w, h, _ := rp.viewRectSize() | |||
| 327 | + | gl.viewport(0, 0, w, h) | |||
| 310 | gl.clearColor(0, 0, 0, 1) | 328 | gl.clearColor(0, 0, 0, 1) | ||
| 311 | gl.clear(glColorBufferBit) | 329 | gl.clear(glColorBufferBit) | ||
| 312 | - | gl.useProgram(rp.program) | 330 | + | rp.drawTexture(rp.texture) |
| 313 | - | gl.bindTexture(glTexture2D, rp.texture) | 331 | + | |
| 314 | - | gl.bindVertexArray(rp.vao) | 332 | + | if rp.uploadPopupTexture(renderSeq, paintSeq) { |
| 315 | - | gl.drawArrays(glTriangleStrip, 0, 4) | 333 | + | rp.drawPopupOverlay(w, h) |
| 316 | - | gl.bindVertexArray(0) | 334 | + | } |
| 317 | - | gl.bindTexture(glTexture2D, 0) | |||
| 318 | - | gl.useProgram(0) | |||
| 319 | 335 | ||||
| 320 | rp.glRenderCount++ | 336 | rp.glRenderCount++ | ||
| 321 | renderDuration := time.Since(renderStartedAt) | 337 | renderDuration := time.Since(renderStartedAt) | ||
| @@ -337,6 +353,20 @@ func (rp *renderPipeline) onGLRender() bool { | |||||
| 337 | return true | 353 | return true | ||
| 338 | } | 354 | } | ||
| 339 | 355 | ||||
| 356 | + | func (rp *renderPipeline) drawTexture(texture uint32) { | |||
| 357 | + | if texture == 0 { | |||
| 358 | + | return | |||
| 359 | + | } | |||
| 360 | + | gl := rp.gl | |||
| 361 | + | gl.useProgram(rp.program) | |||
| 362 | + | gl.bindTexture(glTexture2D, texture) | |||
| 363 | + | gl.bindVertexArray(rp.vao) | |||
| 364 | + | gl.drawArrays(glTriangleStrip, 0, 4) | |||
| 365 | + | gl.bindVertexArray(0) | |||
| 366 | + | gl.bindTexture(glTexture2D, 0) | |||
| 367 | + | gl.useProgram(0) | |||
| 368 | + | } | |||
| 369 | + | ||||
| 340 | // uploadToPBO uploads dirty regions from the staging buffer into a PBO, then | 370 | // uploadToPBO uploads dirty regions from the staging buffer into a PBO, then | ||
| 341 | // transfers to the GL texture via async DMA. | 371 | // transfers to the GL texture via async DMA. | ||
| 342 | func (rp *renderPipeline) uploadToPBO(dirtyRects []rect, fullUpload bool, renderSeq, paintSeq uint64) uint64 { | 372 | func (rp *renderPipeline) uploadToPBO(dirtyRects []rect, fullUpload bool, renderSeq, paintSeq uint64) uint64 { | ||
| @@ -373,8 +403,11 @@ func (rp *renderPipeline) uploadToPBO(dirtyRects []rect, fullUpload bool, render | |||||
| 373 | 403 | ||||
| 374 | rp.mu.Lock() | 404 | rp.mu.Lock() | ||
| 375 | if fullUpload { | 405 | if fullUpload { | ||
| 376 | - | copy(mappedSlice, rp.staging) | 406 | + | copied := copy(mappedSlice, rp.staging) |
| 377 | - | uploadedBytes = uint64(bufSize) | 407 | + | if copied < len(mappedSlice) { |
| 408 | + | clear(mappedSlice[copied:]) | |||
| 409 | + | } | |||
| 410 | + | uploadedBytes = uint64(copied) | |||
| 378 | } else { | 411 | } else { | ||
| 379 | for _, rawRect := range dirtyRects { | 412 | for _, rawRect := range dirtyRects { | ||
| 380 | r, ok := clampDirtyRect(rawRect, w, h) | 413 | r, ok := clampDirtyRect(rawRect, w, h) | ||
| @@ -440,6 +473,84 @@ func (rp *renderPipeline) uploadToPBO(dirtyRects []rect, fullUpload bool, render | |||||
| 440 | return uploadedBytes | 473 | return uploadedBytes | ||
| 441 | } | 474 | } | ||
| 442 | 475 | ||||
| 476 | + | func (rp *renderPipeline) uploadPopupTexture(renderSeq, paintSeq uint64) bool { | |||
| 477 | + | rp.mu.Lock() | |||
| 478 | + | defer rp.mu.Unlock() | |||
| 479 | + | ||||
| 480 | + | if !rp.popupVisible || rp.popupWidth <= 0 || rp.popupHeight <= 0 || len(rp.popupStaging) == 0 { | |||
| 481 | + | return false | |||
| 482 | + | } | |||
| 483 | + | ||||
| 484 | + | if !rp.popupNeedsUpload && !rp.popupSizeChanged && rp.popupTexture != 0 { | |||
| 485 | + | return true | |||
| 486 | + | } | |||
| 487 | + | ||||
| 488 | + | gl := rp.gl | |||
| 489 | + | ||||
| 490 | + | if rp.popupTexture == 0 { | |||
| 491 | + | gl.genTextures(1, &rp.popupTexture) | |||
| 492 | + | gl.bindTexture(glTexture2D, rp.popupTexture) | |||
| 493 | + | gl.texParameteri(glTexture2D, glTextureMinFilter, int32(glLinear)) | |||
| 494 | + | gl.texParameteri(glTexture2D, glTextureMagFilter, int32(glLinear)) | |||
| 495 | + | gl.texParameteri(glTexture2D, glTextureWrapS, int32(glClampToEdge)) | |||
| 496 | + | gl.texParameteri(glTexture2D, glTextureWrapT, int32(glClampToEdge)) | |||
| 497 | + | } else { | |||
| 498 | + | gl.bindTexture(glTexture2D, rp.popupTexture) | |||
| 499 | + | } | |||
| 500 | + | ||||
| 501 | + | pixels := unsafe.Pointer(&rp.popupStaging[0]) | |||
| 502 | + | if rp.popupSizeChanged { | |||
| 503 | + | gl.texImage2D(glTexture2D, 0, int32(glRGBA), rp.popupWidth, rp.popupHeight, 0, glBGRA, glUnsignedByte, pixels) | |||
| 504 | + | } else { | |||
| 505 | + | gl.texSubImage2D(glTexture2D, 0, 0, 0, rp.popupWidth, rp.popupHeight, glBGRA, glUnsignedByte, pixels) | |||
| 506 | + | } | |||
| 507 | + | gl.bindTexture(glTexture2D, 0) | |||
| 508 | + | ||||
| 509 | + | rp.popupNeedsUpload = false | |||
| 510 | + | rp.popupSizeChanged = false | |||
| 511 | + | ||||
| 512 | + | if rp.ctx != nil { | |||
| 513 | + | logging.FromContext(rp.ctx).Debug(). | |||
| 514 | + | Uint64("render_seq", renderSeq). | |||
| 515 | + | Uint64("paint_seq", paintSeq). | |||
| 516 | + | Int32("popup_width", rp.popupWidth). | |||
| 517 | + | Int32("popup_height", rp.popupHeight). | |||
| 518 | + | Msg("cef: popup texture uploaded") | |||
| 519 | + | } | |||
| 520 | + | ||||
| 521 | + | return true | |||
| 522 | + | } | |||
| 523 | + | ||||
| 524 | + | func (rp *renderPipeline) drawPopupOverlay(viewWidth, viewHeight int32) { | |||
| 525 | + | rp.mu.Lock() | |||
| 526 | + | popupVisible := rp.popupVisible | |||
| 527 | + | popupRect := rp.popupRect | |||
| 528 | + | popupWidth := rp.popupWidth | |||
| 529 | + | popupHeight := rp.popupHeight | |||
| 530 | + | texture := rp.popupTexture | |||
| 531 | + | rp.mu.Unlock() | |||
| 532 | + | ||||
| 533 | + | if !popupVisible || texture == 0 || popupWidth <= 0 || popupHeight <= 0 || viewWidth <= 0 || viewHeight <= 0 { | |||
| 534 | + | return | |||
| 535 | + | } | |||
| 536 | + | ||||
| 537 | + | popupRect.Width = popupWidth | |||
| 538 | + | popupRect.Height = popupHeight | |||
| 539 | + | popupRect, ok := clampDirtyRect(popupRect, viewWidth, viewHeight) | |||
| 540 | + | if !ok { | |||
| 541 | + | return | |||
| 542 | + | } | |||
| 543 | + | ||||
| 544 | + | viewportY := viewHeight - popupRect.Y - popupRect.Height | |||
| 545 | + | if viewportY < 0 { | |||
| 546 | + | viewportY = 0 | |||
| 547 | + | } | |||
| 548 | + | ||||
| 549 | + | rp.gl.viewport(popupRect.X, viewportY, popupRect.Width, popupRect.Height) | |||
| 550 | + | rp.drawTexture(texture) | |||
| 551 | + | rp.gl.viewport(0, 0, viewWidth, viewHeight) | |||
| 552 | + | } | |||
| 553 | + | ||||
| 443 | // initGL creates or recreates all GL resources for the current surface size. | 554 | // initGL creates or recreates all GL resources for the current surface size. | ||
| 444 | func (rp *renderPipeline) initGL() { | 555 | func (rp *renderPipeline) initGL() { | ||
| 445 | rp.mu.Lock() | 556 | rp.mu.Lock() | ||
| @@ -662,6 +773,70 @@ func (rp *renderPipeline) onResize(width, height int32) { | |||||
| 662 | } | 773 | } | ||
| 663 | } | 774 | } | ||
| 664 | 775 | ||||
| 776 | + | func (rp *renderPipeline) setPopupVisible(show bool) { | |||
| 777 | + | rp.mu.Lock() | |||
| 778 | + | rp.popupVisible = show | |||
| 779 | + | if !show { | |||
| 780 | + | rp.popupStaging = nil | |||
| 781 | + | rp.popupWidth = 0 | |||
| 782 | + | rp.popupHeight = 0 | |||
| 783 | + | rp.popupNeedsUpload = false | |||
| 784 | + | rp.popupSizeChanged = false | |||
| 785 | + | } | |||
| 786 | + | rp.mu.Unlock() | |||
| 787 | + | rp.glArea.QueueRender() | |||
| 788 | + | } | |||
| 789 | + | ||||
| 790 | + | func (rp *renderPipeline) setPopupRect(popup rect) { | |||
| 791 | + | rp.mu.Lock() | |||
| 792 | + | rp.popupRect = rect{ | |||
| 793 | + | X: popup.X * rp.scale, | |||
| 794 | + | Y: popup.Y * rp.scale, | |||
| 795 | + | Width: popup.Width * rp.scale, | |||
| 796 | + | Height: popup.Height * rp.scale, | |||
| 797 | + | } | |||
| 798 | + | rp.mu.Unlock() | |||
| 799 | + | rp.glArea.QueueRender() | |||
| 800 | + | } | |||
| 801 | + | ||||
| 802 | + | func (rp *renderPipeline) handlePopupPaint(buffer []byte, width, height int32, paintSeq uint64) { | |||
| 803 | + | if len(buffer) == 0 || width <= 0 || height <= 0 { | |||
| 804 | + | return | |||
| 805 | + | } | |||
| 806 | + | ||||
| 807 | + | bufSize, ok := bgraBufferSize(width, height) | |||
| 808 | + | if !ok { | |||
| 809 | + | return | |||
| 810 | + | } | |||
| 811 | + | ||||
| 812 | + | rp.mu.Lock() | |||
| 813 | + | if len(rp.popupStaging) != bufSize { | |||
| 814 | + | rp.popupStaging = make([]byte, bufSize) | |||
| 815 | + | rp.popupSizeChanged = true | |||
| 816 | + | } else if rp.popupWidth != width || rp.popupHeight != height { | |||
| 817 | + | rp.popupSizeChanged = true | |||
| 818 | + | } | |||
| 819 | + | copied := copy(rp.popupStaging, buffer) | |||
| 820 | + | if copied < len(rp.popupStaging) { | |||
| 821 | + | clear(rp.popupStaging[copied:]) | |||
| 822 | + | } | |||
| 823 | + | rp.popupWidth = width | |||
| 824 | + | rp.popupHeight = height | |||
| 825 | + | rp.popupNeedsUpload = true | |||
| 826 | + | rp.popupVisible = true | |||
| 827 | + | rp.mu.Unlock() | |||
| 828 | + | ||||
| 829 | + | if rp.ctx != nil { | |||
| 830 | + | logging.FromContext(rp.ctx).Debug(). | |||
| 831 | + | Uint64("paint_seq", paintSeq). | |||
| 832 | + | Int32("popup_width", width). | |||
| 833 | + | Int32("popup_height", height). | |||
| 834 | + | Msg("cef: handlePopupPaint") | |||
| 835 | + | } | |||
| 836 | + | ||||
| 837 | + | rp.glArea.QueueRender() | |||
| 838 | + | } | |||
| 839 | + | ||||
| 665 | func (rp *renderPipeline) viewRectSize() (width, height, scale int32) { | 840 | func (rp *renderPipeline) viewRectSize() (width, height, scale int32) { | ||
| 666 | width = rp.widthAtomic.Load() | 841 | width = rp.widthAtomic.Load() | ||
| 667 | height = rp.heightAtomic.Load() | 842 | height = rp.heightAtomic.Load() | ||
| @@ -728,6 +903,10 @@ func (rp *renderPipeline) deleteGLResources() { | |||||
| 728 | gl.deleteTextures(1, &rp.texture) | 903 | gl.deleteTextures(1, &rp.texture) | ||
| 729 | rp.texture = 0 | 904 | rp.texture = 0 | ||
| 730 | } | 905 | } | ||
| 906 | + | if rp.popupTexture != 0 { | |||
| 907 | + | gl.deleteTextures(1, &rp.popupTexture) | |||
| 908 | + | rp.popupTexture = 0 | |||
| 909 | + | } | |||
| 731 | for i := 0; i < 2; i++ { | 910 | for i := 0; i < 2; i++ { | ||
| 732 | if rp.pbo[i] != 0 { | 911 | if rp.pbo[i] != 0 { | ||
| 733 | gl.deleteBuffers(1, &rp.pbo[i]) | 912 | gl.deleteBuffers(1, &rp.pbo[i]) | ||
| @@ -750,6 +929,89 @@ func (rp *renderPipeline) deleteGLResources() { | |||||
| 750 | } | 929 | } | ||
| 751 | } | 930 | } | ||
| 752 | 931 | ||||
| 932 | + | func bgraBufferSize(width, height int32) (int, bool) { | |||
| 933 | + | if width <= 0 || height <= 0 { | |||
| 934 | + | return 0, false | |||
| 935 | + | } | |||
| 936 | + | size64 := int64(width) * int64(height) * 4 | |||
| 937 | + | if size64 <= 0 { | |||
| 938 | + | return 0, false | |||
| 939 | + | } | |||
| 940 | + | return int(size64), true | |||
| 941 | + | } | |||
| 942 | + | ||||
| 943 | + | func stagingNeedsReset(staging []byte, currentWidth, currentHeight, nextWidth, nextHeight int32, expectedBytes int) bool { | |||
| 944 | + | return staging == nil || | |||
| 945 | + | len(staging) != expectedBytes || | |||
| 946 | + | currentWidth != nextWidth || | |||
| 947 | + | currentHeight != nextHeight | |||
| 948 | + | } | |||
| 949 | + | ||||
| 950 | + | func copyDirtyRectsIntoStaging(dst, src []byte, width, height int32, rects []rect) (uint64, []rect, bool) { | |||
| 951 | + | if len(dst) == 0 || len(src) == 0 || width <= 0 || height <= 0 { | |||
| 952 | + | return 0, nil, false | |||
| 953 | + | } | |||
| 954 | + | ||||
| 955 | + | stride := int(width) * 4 | |||
| 956 | + | copiedBytes := uint64(0) | |||
| 957 | + | truncated := false | |||
| 958 | + | sanitized := make([]rect, 0, len(rects)) | |||
| 959 | + | ||||
| 960 | + | for _, rawRect := range rects { | |||
| 961 | + | r, ok := clampDirtyRect(rawRect, width, height) | |||
| 962 | + | if !ok { | |||
| 963 | + | continue | |||
| 964 | + | } | |||
| 965 | + | sanitized = append(sanitized, r) | |||
| 966 | + | ||||
| 967 | + | for row := r.Y; row < r.Y+r.Height; row++ { | |||
| 968 | + | dstOff := int(row)*stride + int(r.X)*4 | |||
| 969 | + | if dstOff < 0 || dstOff >= len(dst) { | |||
| 970 | + | truncated = true | |||
| 971 | + | break | |||
| 972 | + | } | |||
| 973 | + | ||||
| 974 | + | rowBytes := int(r.Width) * 4 | |||
| 975 | + | dstEnd := dstOff + rowBytes | |||
| 976 | + | if dstEnd > len(dst) { | |||
| 977 | + | dstEnd = len(dst) | |||
| 978 | + | truncated = true | |||
| 979 | + | } | |||
| 980 | + | ||||
| 981 | + | srcOff := int(row)*stride + int(r.X)*4 | |||
| 982 | + | if srcOff < 0 || srcOff >= len(src) { | |||
| 983 | + | clear(dst[dstOff:dstEnd]) | |||
| 984 | + | truncated = true | |||
| 985 | + | continue | |||
| 986 | + | } | |||
| 987 | + | ||||
| 988 | + | srcEnd := srcOff + rowBytes | |||
| 989 | + | if srcEnd > len(src) { | |||
| 990 | + | srcEnd = len(src) | |||
| 991 | + | truncated = true | |||
| 992 | + | } | |||
| 993 | + | ||||
| 994 | + | copyLen := srcEnd - srcOff | |||
| 995 | + | maxDstLen := dstEnd - dstOff | |||
| 996 | + | if copyLen > maxDstLen { | |||
| 997 | + | copyLen = maxDstLen | |||
| 998 | + | truncated = true | |||
| 999 | + | } | |||
| 1000 | + | ||||
| 1001 | + | if copyLen > 0 { | |||
| 1002 | + | copy(dst[dstOff:dstOff+copyLen], src[srcOff:srcOff+copyLen]) | |||
| 1003 | + | copiedBytes += uint64(copyLen) | |||
| 1004 | + | } | |||
| 1005 | + | ||||
| 1006 | + | if dstOff+copyLen < dstEnd { | |||
| 1007 | + | clear(dst[dstOff+copyLen : dstEnd]) | |||
| 1008 | + | } | |||
| 1009 | + | } | |||
| 1010 | + | } | |||
| 1011 | + | ||||
| 1012 | + | return copiedBytes, sanitized, truncated | |||
| 1013 | + | } | |||
| 1014 | + | ||||
| 753 | // pboOffset converts a byte offset into an unsafe.Pointer for use with | 1015 | // pboOffset converts a byte offset into an unsafe.Pointer for use with | ||
| 754 | // glTexSubImage2D when a PBO is bound. OpenGL interprets the pointer as a | 1016 | // glTexSubImage2D when a PBO is bound. OpenGL interprets the pointer as a | ||
| 755 | // byte offset into the bound buffer rather than a real memory address. | 1017 | // byte offset into the bound buffer rather than a real memory address. | ||
A internal/infrastructure/cef/render_pipeline_test.go +41 -0
| @@ -0,0 +1,41 @@ | |||
| 1 | + | package cef | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/stretchr/testify/require" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func TestStagingNeedsResetWhenBufferSizeMismatches(t *testing.T) { | |
| 11 | + | staging := make([]byte, 8) | |
| 12 | + | ||
| 13 | + | require.True(t, stagingNeedsReset(staging, 2, 2, 2, 2, 16)) | |
| 14 | + | require.False(t, stagingNeedsReset(make([]byte, 16), 2, 2, 2, 2, 16)) | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | func TestCopyDirtyRectsIntoStagingClampsTruncatedBuffer(t *testing.T) { | |
| 18 | + | const ( | |
| 19 | + | width = int32(4) | |
| 20 | + | height = int32(2) | |
| 21 | + | ) | |
| 22 | + | ||
| 23 | + | dst := bytes.Repeat([]byte{0xaa}, 32) | |
| 24 | + | src := make([]byte, 28) | |
| 25 | + | for i := range src { | |
| 26 | + | src[i] = byte(i + 1) | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | copied, sanitized, truncated := copyDirtyRectsIntoStaging(dst, src, width, height, []rect{{ | |
| 30 | + | X: 0, | |
| 31 | + | Y: 0, | |
| 32 | + | Width: width, | |
| 33 | + | Height: height, | |
| 34 | + | }}) | |
| 35 | + | ||
| 36 | + | require.True(t, truncated) | |
| 37 | + | require.Equal(t, uint64(len(src)), copied) | |
| 38 | + | require.Len(t, sanitized, 1) | |
| 39 | + | require.Equal(t, src, dst[:len(src)]) | |
| 40 | + | require.Equal(t, []byte{0, 0, 0, 0}, dst[len(src):]) | |
| 41 | + | } | |
| @@ -0,0 +1,41 @@ | |||||
| 1 | + | package cef | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/stretchr/testify/require" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func TestStagingNeedsResetWhenBufferSizeMismatches(t *testing.T) { | |||
| 11 | + | staging := make([]byte, 8) | |||
| 12 | + | ||||
| 13 | + | require.True(t, stagingNeedsReset(staging, 2, 2, 2, 2, 16)) | |||
| 14 | + | require.False(t, stagingNeedsReset(make([]byte, 16), 2, 2, 2, 2, 16)) | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | func TestCopyDirtyRectsIntoStagingClampsTruncatedBuffer(t *testing.T) { | |||
| 18 | + | const ( | |||
| 19 | + | width = int32(4) | |||
| 20 | + | height = int32(2) | |||
| 21 | + | ) | |||
| 22 | + | ||||
| 23 | + | dst := bytes.Repeat([]byte{0xaa}, 32) | |||
| 24 | + | src := make([]byte, 28) | |||
| 25 | + | for i := range src { | |||
| 26 | + | src[i] = byte(i + 1) | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | copied, sanitized, truncated := copyDirtyRectsIntoStaging(dst, src, width, height, []rect{{ | |||
| 30 | + | X: 0, | |||
| 31 | + | Y: 0, | |||
| 32 | + | Width: width, | |||
| 33 | + | Height: height, | |||
| 34 | + | }}) | |||
| 35 | + | ||||
| 36 | + | require.True(t, truncated) | |||
| 37 | + | require.Equal(t, uint64(len(src)), copied) | |||
| 38 | + | require.Len(t, sanitized, 1) | |||
| 39 | + | require.Equal(t, src, dst[:len(src)]) | |||
| 40 | + | require.Equal(t, []byte{0, 0, 0, 0}, dst[len(src):]) | |||
| 41 | + | } | |||
M internal/infrastructure/cef/webview.go +6 -4
| @@ -116,17 +116,18 @@ func (wv *WebView) LoadURI(_ context.Context, uri string) error { | |||
| 116 | 116 | if wv.destroyed.Load() { | |
| 117 | 117 | return errDestroyed | |
| 118 | 118 | } | |
| 119 | + | actualURI := toActualInternalURL(uri) | |
| 119 | 120 | wv.mu.Lock() | |
| 120 | 121 | browser := wv.browser | |
| 121 | 122 | if browser == nil { | |
| 122 | 123 | // Browser not yet created — queue the URI for OnAfterCreated. | |
| 123 | - | wv.pendingURI = uri | |
| 124 | + | wv.pendingURI = actualURI | |
| 124 | 125 | wv.mu.Unlock() | |
| 125 | 126 | return nil | |
| 126 | 127 | } | |
| 127 | 128 | wv.mu.Unlock() | |
| 128 | 129 | if frame := browser.GetMainFrame(); frame != nil { | |
| 129 | - | frame.LoadURL(uri) | |
| 130 | + | frame.LoadURL(actualURI) | |
| 130 | 131 | } | |
| 131 | 132 | return nil | |
| 132 | 133 | } | |
| @@ -444,7 +445,7 @@ const colorScale = 255 | |||
| 444 | 445 | // SetBackgroundColor sets the background via JS injection (CEF has no runtime API). | |
| 445 | 446 | func (wv *WebView) SetBackgroundColor(r, g, b, a float64) { | |
| 446 | 447 | script := fmt.Sprintf( | |
| 447 | - | `document.documentElement.style.backgroundColor='rgba(%d,%d,%d,%.2f)'`, | |
| 448 | + | `(function(){ if (document.documentElement) { document.documentElement.style.backgroundColor='rgba(%d,%d,%d,%.2f)'; } })()`, | |
| 448 | 449 | int(r*colorScale), int(g*colorScale), int(b*colorScale), a, | |
| 449 | 450 | ) | |
| 450 | 451 | wv.RunJavaScript(context.Background(), script) | |
| @@ -453,7 +454,7 @@ func (wv *WebView) SetBackgroundColor(r, g, b, a float64) { | |||
| 453 | 454 | // ResetBackgroundToDefault clears the injected background color. | |
| 454 | 455 | func (wv *WebView) ResetBackgroundToDefault() { | |
| 455 | 456 | wv.RunJavaScript(context.Background(), | |
| 456 | - | `document.documentElement.style.backgroundColor=''`) | |
| 457 | + | `(function(){ if (document.documentElement) { document.documentElement.style.backgroundColor=''; } })()`) | |
| 457 | 458 | } | |
| 458 | 459 | ||
| 459 | 460 | // --------------------------------------------------------------------------- | |
| @@ -499,6 +500,7 @@ func (wv *WebView) NativeWidget() uintptr { | |||
| 499 | 500 | // --------------------------------------------------------------------------- | |
| 500 | 501 | ||
| 501 | 502 | func (wv *WebView) updateURI(uri string) { | |
| 503 | + | uri = toConceptualInternalURL(uri) | |
| 502 | 504 | wv.mu.Lock() | |
| 503 | 505 | wv.uri = uri | |
| 504 | 506 | cb := wv.callbacks | |
| @@ -116,17 +116,18 @@ func (wv *WebView) LoadURI(_ context.Context, uri string) error { | |||||
| 116 | if wv.destroyed.Load() { | 116 | if wv.destroyed.Load() { | ||
| 117 | return errDestroyed | 117 | return errDestroyed | ||
| 118 | } | 118 | } | ||
| 119 | + | actualURI := toActualInternalURL(uri) | |||
| 119 | wv.mu.Lock() | 120 | wv.mu.Lock() | ||
| 120 | browser := wv.browser | 121 | browser := wv.browser | ||
| 121 | if browser == nil { | 122 | if browser == nil { | ||
| 122 | // Browser not yet created — queue the URI for OnAfterCreated. | 123 | // Browser not yet created — queue the URI for OnAfterCreated. | ||
| 123 | - | wv.pendingURI = uri | 124 | + | wv.pendingURI = actualURI |
| 124 | wv.mu.Unlock() | 125 | wv.mu.Unlock() | ||
| 125 | return nil | 126 | return nil | ||
| 126 | } | 127 | } | ||
| 127 | wv.mu.Unlock() | 128 | wv.mu.Unlock() | ||
| 128 | if frame := browser.GetMainFrame(); frame != nil { | 129 | if frame := browser.GetMainFrame(); frame != nil { | ||
| 129 | - | frame.LoadURL(uri) | 130 | + | frame.LoadURL(actualURI) |
| 130 | } | 131 | } | ||
| 131 | return nil | 132 | return nil | ||
| 132 | } | 133 | } | ||
| @@ -444,7 +445,7 @@ const colorScale = 255 | |||||
| 444 | // SetBackgroundColor sets the background via JS injection (CEF has no runtime API). | 445 | // SetBackgroundColor sets the background via JS injection (CEF has no runtime API). | ||
| 445 | func (wv *WebView) SetBackgroundColor(r, g, b, a float64) { | 446 | func (wv *WebView) SetBackgroundColor(r, g, b, a float64) { | ||
| 446 | script := fmt.Sprintf( | 447 | script := fmt.Sprintf( | ||
| 447 | - | `document.documentElement.style.backgroundColor='rgba(%d,%d,%d,%.2f)'`, | 448 | + | `(function(){ if (document.documentElement) { document.documentElement.style.backgroundColor='rgba(%d,%d,%d,%.2f)'; } })()`, |
| 448 | int(r*colorScale), int(g*colorScale), int(b*colorScale), a, | 449 | int(r*colorScale), int(g*colorScale), int(b*colorScale), a, | ||
| 449 | ) | 450 | ) | ||
| 450 | wv.RunJavaScript(context.Background(), script) | 451 | wv.RunJavaScript(context.Background(), script) | ||
| @@ -453,7 +454,7 @@ func (wv *WebView) SetBackgroundColor(r, g, b, a float64) { | |||||
| 453 | // ResetBackgroundToDefault clears the injected background color. | 454 | // ResetBackgroundToDefault clears the injected background color. | ||
| 454 | func (wv *WebView) ResetBackgroundToDefault() { | 455 | func (wv *WebView) ResetBackgroundToDefault() { | ||
| 455 | wv.RunJavaScript(context.Background(), | 456 | wv.RunJavaScript(context.Background(), | ||
| 456 | - | `document.documentElement.style.backgroundColor=''`) | 457 | + | `(function(){ if (document.documentElement) { document.documentElement.style.backgroundColor=''; } })()`) |
| 457 | } | 458 | } | ||
| 458 | 459 | ||||
| 459 | // --------------------------------------------------------------------------- | 460 | // --------------------------------------------------------------------------- | ||
| @@ -499,6 +500,7 @@ func (wv *WebView) NativeWidget() uintptr { | |||||
| 499 | // --------------------------------------------------------------------------- | 500 | // --------------------------------------------------------------------------- | ||
| 500 | 501 | ||||
| 501 | func (wv *WebView) updateURI(uri string) { | 502 | func (wv *WebView) updateURI(uri string) { | ||
| 503 | + | uri = toConceptualInternalURL(uri) | |||
| 502 | wv.mu.Lock() | 504 | wv.mu.Lock() | ||
| 503 | wv.uri = uri | 505 | wv.uri = uri | ||
| 504 | cb := wv.callbacks | 506 | cb := wv.callbacks | ||
M internal/infrastructure/cef/webview_handlers.go +31 -3
| @@ -147,15 +147,35 @@ func (h *handlerSet) GetScreenInfo(_ purecef.Browser, info *purecef.ScreenInfo) | |||
| 147 | 147 | return 1 | |
| 148 | 148 | } | |
| 149 | 149 | ||
| 150 | - | func (h *handlerSet) OnPopupShow(_ purecef.Browser, _ int32) {} | |
| 150 | + | func (h *handlerSet) OnPopupShow(_ purecef.Browser, show int32) { | |
| 151 | + | if h.wv == nil || h.wv.pipeline == nil { | |
| 152 | + | return | |
| 153 | + | } | |
| 154 | + | h.wv.runOnGTK(func() { | |
| 155 | + | h.wv.pipeline.setPopupVisible(show != 0) | |
| 156 | + | }) | |
| 157 | + | } | |
| 151 | 158 | ||
| 152 | - | func (h *handlerSet) OnPopupSize(_ purecef.Browser, _ *purecef.Rect) {} | |
| 159 | + | func (h *handlerSet) OnPopupSize(_ purecef.Browser, popupRect *purecef.Rect) { | |
| 160 | + | if h.wv == nil || h.wv.pipeline == nil || popupRect == nil { | |
| 161 | + | return | |
| 162 | + | } | |
| 163 | + | popup := rect{ | |
| 164 | + | X: popupRect.X, | |
| 165 | + | Y: popupRect.Y, | |
| 166 | + | Width: popupRect.Width, | |
| 167 | + | Height: popupRect.Height, | |
| 168 | + | } | |
| 169 | + | h.wv.runOnGTK(func() { | |
| 170 | + | h.wv.pipeline.setPopupRect(popup) | |
| 171 | + | }) | |
| 172 | + | } | |
| 153 | 173 | ||
| 154 | 174 | // OnPaint receives the BGRA pixel buffer from CEF and forwards dirty rects | |
| 155 | 175 | // to the render pipeline for GPU upload. With a multi-threaded CEF UI loop we | |
| 156 | 176 | // must first copy the transient CEF buffer before hopping back to GTK. | |
| 157 | 177 | func (h *handlerSet) OnPaint( | |
| 158 | - | _ purecef.Browser, _ purecef.PaintElementType, | |
| 178 | + | _ purecef.Browser, elementType purecef.PaintElementType, | |
| 159 | 179 | dirtyRects []purecef.Rect, buffer []byte, width, height int32, | |
| 160 | 180 | ) { | |
| 161 | 181 | if len(buffer) == 0 || width <= 0 || height <= 0 { | |
| @@ -179,6 +199,10 @@ func (h *handlerSet) OnPaint( | |||
| 179 | 199 | pixels := make([]byte, len(buffer)) | |
| 180 | 200 | copy(pixels, buffer) | |
| 181 | 201 | h.wv.runOnGTK(func() { | |
| 202 | + | if elementType == purecef.PaintElementTypePetPopup { | |
| 203 | + | h.wv.pipeline.handlePopupPaint(pixels, width, height, paintSeq) | |
| 204 | + | return | |
| 205 | + | } | |
| 182 | 206 | h.wv.pipeline.handlePaint(pixels, width, height, rects, paintSeq) | |
| 183 | 207 | }) | |
| 184 | 208 | if h.wv != nil && h.wv.ctx != nil { | |
| @@ -188,6 +212,10 @@ func (h *handlerSet) OnPaint( | |||
| 188 | 212 | } | |
| 189 | 213 | return | |
| 190 | 214 | } | |
| 215 | + | if elementType == purecef.PaintElementTypePetPopup { | |
| 216 | + | h.wv.pipeline.handlePopupPaint(buffer, width, height, paintSeq) | |
| 217 | + | return | |
| 218 | + | } | |
| 191 | 219 | h.wv.pipeline.handlePaint(buffer, width, height, rects, paintSeq) | |
| 192 | 220 | if h.wv != nil && h.wv.ctx != nil { | |
| 193 | 221 | logging.FromContext(h.wv.ctx).Debug(). | |
| @@ -147,15 +147,35 @@ func (h *handlerSet) GetScreenInfo(_ purecef.Browser, info *purecef.ScreenInfo) | |||||
| 147 | return 1 | 147 | return 1 | ||
| 148 | } | 148 | } | ||
| 149 | 149 | ||||
| 150 | - | func (h *handlerSet) OnPopupShow(_ purecef.Browser, _ int32) {} | 150 | + | func (h *handlerSet) OnPopupShow(_ purecef.Browser, show int32) { |
| 151 | + | if h.wv == nil || h.wv.pipeline == nil { | |||
| 152 | + | return | |||
| 153 | + | } | |||
| 154 | + | h.wv.runOnGTK(func() { | |||
| 155 | + | h.wv.pipeline.setPopupVisible(show != 0) | |||
| 156 | + | }) | |||
| 157 | + | } | |||
| 151 | 158 | ||||
| 152 | - | func (h *handlerSet) OnPopupSize(_ purecef.Browser, _ *purecef.Rect) {} | 159 | + | func (h *handlerSet) OnPopupSize(_ purecef.Browser, popupRect *purecef.Rect) { |
| 160 | + | if h.wv == nil || h.wv.pipeline == nil || popupRect == nil { | |||
| 161 | + | return | |||
| 162 | + | } | |||
| 163 | + | popup := rect{ | |||
| 164 | + | X: popupRect.X, | |||
| 165 | + | Y: popupRect.Y, | |||
| 166 | + | Width: popupRect.Width, | |||
| 167 | + | Height: popupRect.Height, | |||
| 168 | + | } | |||
| 169 | + | h.wv.runOnGTK(func() { | |||
| 170 | + | h.wv.pipeline.setPopupRect(popup) | |||
| 171 | + | }) | |||
| 172 | + | } | |||
| 153 | 173 | ||||
| 154 | // OnPaint receives the BGRA pixel buffer from CEF and forwards dirty rects | 174 | // OnPaint receives the BGRA pixel buffer from CEF and forwards dirty rects | ||
| 155 | // to the render pipeline for GPU upload. With a multi-threaded CEF UI loop we | 175 | // to the render pipeline for GPU upload. With a multi-threaded CEF UI loop we | ||
| 156 | // must first copy the transient CEF buffer before hopping back to GTK. | 176 | // must first copy the transient CEF buffer before hopping back to GTK. | ||
| 157 | func (h *handlerSet) OnPaint( | 177 | func (h *handlerSet) OnPaint( | ||
| 158 | - | _ purecef.Browser, _ purecef.PaintElementType, | 178 | + | _ purecef.Browser, elementType purecef.PaintElementType, |
| 159 | dirtyRects []purecef.Rect, buffer []byte, width, height int32, | 179 | dirtyRects []purecef.Rect, buffer []byte, width, height int32, | ||
| 160 | ) { | 180 | ) { | ||
| 161 | if len(buffer) == 0 || width <= 0 || height <= 0 { | 181 | if len(buffer) == 0 || width <= 0 || height <= 0 { | ||
| @@ -179,6 +199,10 @@ func (h *handlerSet) OnPaint( | |||||
| 179 | pixels := make([]byte, len(buffer)) | 199 | pixels := make([]byte, len(buffer)) | ||
| 180 | copy(pixels, buffer) | 200 | copy(pixels, buffer) | ||
| 181 | h.wv.runOnGTK(func() { | 201 | h.wv.runOnGTK(func() { | ||
| 202 | + | if elementType == purecef.PaintElementTypePetPopup { | |||
| 203 | + | h.wv.pipeline.handlePopupPaint(pixels, width, height, paintSeq) | |||
| 204 | + | return | |||
| 205 | + | } | |||
| 182 | h.wv.pipeline.handlePaint(pixels, width, height, rects, paintSeq) | 206 | h.wv.pipeline.handlePaint(pixels, width, height, rects, paintSeq) | ||
| 183 | }) | 207 | }) | ||
| 184 | if h.wv != nil && h.wv.ctx != nil { | 208 | if h.wv != nil && h.wv.ctx != nil { | ||
| @@ -188,6 +212,10 @@ func (h *handlerSet) OnPaint( | |||||
| 188 | } | 212 | } | ||
| 189 | return | 213 | return | ||
| 190 | } | 214 | } | ||
| 215 | + | if elementType == purecef.PaintElementTypePetPopup { | |||
| 216 | + | h.wv.pipeline.handlePopupPaint(buffer, width, height, paintSeq) | |||
| 217 | + | return | |||
| 218 | + | } | |||
| 191 | h.wv.pipeline.handlePaint(buffer, width, height, rects, paintSeq) | 219 | h.wv.pipeline.handlePaint(buffer, width, height, rects, paintSeq) | ||
| 192 | if h.wv != nil && h.wv.ctx != nil { | 220 | if h.wv != nil && h.wv.ctx != nil { | ||
| 193 | logging.FromContext(h.wv.ctx).Debug(). | 221 | logging.FromContext(h.wv.ctx).Debug(). | ||