Skip to content

Commit 4666c28

Browse files
committed
fix(ui): double-buffer preview frames to eliminate concurrent access
- Previous single-buffer approach still races: UI goroutine holds pointer from previous send while render loop overwrites that buffer on next tick - Alternate between two buffers so producer always fills the one the UI is not reading, eliminating the race with zero additional per-frame cost Signed-off-by: Martin Wimpress <code@wimpress.io>
1 parent 5942348 commit 4666c28

1 file changed

Lines changed: 16 additions & 7 deletions

File tree

cmd/jivefire/main.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -416,13 +416,19 @@ func runPass2(p *tea.Program, profile *audio.Profile, cfg pass2Config) {
416416
rearrangedHeights := make([]float64, config.NumBars)
417417
barHeightsCopy := make([]float64, config.NumBars) // For UI updates
418418

419-
// Reusable private RGBA buffer for the preview. The render loop reuses the
420-
// frame's internal image every iteration, so the UI goroutine must read a
421-
// copy rather than the live buffer the next Draw will overwrite. Allocated
422-
// once here, only when preview is enabled, to keep it off the hot path.
423-
var previewImg *image.RGBA
419+
// Double-buffered private RGBA images for the preview. The render loop reuses
420+
// the frame's internal image every iteration, so the UI goroutine must read a
421+
// copy rather than the live buffer the next Draw will overwrite. A single copy
422+
// is not enough: the UI still holds the pointer from the previous send while
423+
// the render loop overwrites that same buffer on the next tick. Ping-pong
424+
// between two buffers so the producer always fills the one the UI is not
425+
// reading. Allocated once here, only when preview is enabled, to keep it off
426+
// the hot path.
427+
var previewImgs [2]*image.RGBA
428+
previewIdx := 0
424429
if !cfg.noPreview {
425-
previewImg = image.NewRGBA(image.Rect(0, 0, config.Width, config.Height))
430+
previewImgs[0] = image.NewRGBA(image.Rect(0, 0, config.Width, config.Height))
431+
previewImgs[1] = image.NewRGBA(image.Rect(0, 0, config.Width, config.Height))
426432
}
427433

428434
sensitivity := 1.0
@@ -590,9 +596,12 @@ func runPass2(p *tea.Program, profile *audio.Profile, cfg pass2Config) {
590596

591597
var frameData *image.RGBA
592598
if !cfg.noPreview {
593-
// Copy into the private buffer; the next frame.Draw mutates img.
599+
// Copy into the buffer the UI is not reading; the next frame.Draw
600+
// mutates img and the next send reuses the other buffer.
601+
previewImg := previewImgs[previewIdx]
594602
copy(previewImg.Pix, img.Pix)
595603
frameData = previewImg
604+
previewIdx ^= 1
596605
}
597606

598607
p.Send(ui.RenderProgress{

0 commit comments

Comments
 (0)