From 2a7bf8528abdd45802a2ce98e411b2937b0cc8c6 Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Fri, 29 May 2026 15:10:30 +0300 Subject: [PATCH] Cache SampleRateOverlay to a bitmap instead of redrawing per frame The span-selector panel was rendered directly onto the spectrum canvas every frame via DrawSelf, doing ~30 TextWidth + ~11 TextOut Canvas calls. On Cocoa these text ops are very expensive (~10.6 ms/frame, ~30% of a core) and dominated DrawSpectrum. Render the panel once into a per-pixel-alpha cache bitmap and only composite it over the spectrum each frame; rebuild only when state changes (rate, hover, hidden, bounds). The cache is built with the two-background method (render over black and white, derive straight alpha + color) so layered semi-transparent button fills and antialiased text composite over the live spectrum exactly as before. Cuts the overlay phase from ~10.6 ms to ~0.2 ms per frame; DrawSpectrum drops ~13.8 ms -> ~2.7 ms. Mirrors the caching VfoOverlay already uses. Co-Authored-By: Claude Opus 4.8 --- SampleRateOverlay.pas | 184 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 183 insertions(+), 1 deletion(-) diff --git a/SampleRateOverlay.pas b/SampleRateOverlay.pas index 73dfd7b..5a0174b 100644 --- a/SampleRateOverlay.pas +++ b/SampleRateOverlay.pas @@ -36,11 +36,30 @@ type FHitCount: Integer; FHotIdx: Integer; + // Кэш отрисовки: панель кнопок (статичный текст) рендерится в bitmap с + // попиксельной альфой и пересобирается только при смене состояния, а каждый + // кадр лишь композитится поверх спектра. Без этого ~10 мс/кадр на Cocoa + // уходило на TextOut/TextWidth (по результатам профилирования отрисовки). + FCache: TBitmap; + FCacheW: Integer; + FCacheDirty: Boolean; + FKeyRate: Integer; + FKeyHot: Integer; + FKeyHidden: Boolean; + FKeyLeft: Integer; + FKeyTop: Integer; + FKeyW: Integer; + FKeyH: Integer; + procedure DrawSelf(Target: TBitmap; C: TCanvas; OriginX, OriginY, W, H: Integer); procedure RegisterHit(const HR: TRect; Value: Integer); procedure RequestInvalidate; + function ComputeContentW(C: TCanvas; AvailW: Integer): Integer; + procedure RebuildCache(C: TCanvas; W, H: Integer); + procedure CompositeCache(Target: TBitmap); public constructor Create(AOwner: TComponent); override; + destructor Destroy; override; procedure SetBounds(ALeft, ATop, AWidth, AHeight: Integer); procedure DrawOverlay(Target: TBitmap; C: TCanvas; W, H: Integer); procedure DrawOverlayBitmap(Target: TBitmap; W, H: Integer); @@ -133,6 +152,17 @@ begin FHotIdx := -1; FWidth := 320; FHeight := SPAN_H + PAD_Y * 2; + FCache := TBitmap.Create; + FCache.PixelFormat := pf32bit; + FCacheW := 0; + FCacheDirty := True; + FKeyRate := -1; FKeyHot := -2; FKeyLeft := MaxInt; FKeyTop := MaxInt; +end; + +destructor TSampleRateOverlay.Destroy; +begin + FCache.Free; + inherited Destroy; end; procedure TSampleRateOverlay.RequestInvalidate; @@ -148,6 +178,7 @@ begin FTop := ATop; FWidth := AWidth; FHeight := AHeight; + FCacheDirty := True; RequestInvalidate; end; @@ -242,11 +273,162 @@ begin end; end; +function TSampleRateOverlay.ComputeContentW(C: TCanvas; AvailW: Integer): Integer; +var I, CW: Integer; +begin + C.Font.Name := 'Courier New'; C.Font.Size := 7; C.Font.Style := []; + CW := PAD_X + HIDE_W + BTN_GAP; + for I := 0 to High(SPAN_NAMES) do + begin + Inc(CW, SpanBtnWidth(SPAN_NAMES[I], C)); + if I < High(SPAN_NAMES) then Inc(CW, BTN_GAP); + end; + Inc(CW, PAD_X); + if CW > AvailW then CW := AvailW; + Result := CW; +end; + +// Перерисовывает панель в FCache с попиксельной альфой. Метод двух фонов: +// рендерим оверлей на чёрном (B0) и белом (B1) фоне; для пикселя +// alpha = 255 - (B1 - B0), color = B0 / alpha +// что точно восстанавливает результат любого числа src-over слоёв (включая +// сглаженный текст). Затем композит FCache поверх спектра даёт тот же вид. +procedure TSampleRateOverlay.RebuildCache(C: TCanvas; W, H: Integer); +var + availW, availH, ContentW, X, Y, I, d, A: Integer; + B0, B1: TBitmap; + R0, R1, RC: PByte; +begin + availW := Min(FWidth, W - FLeft); + availH := Min(FHeight, H - FTop); + FCacheW := 0; + if (availW <= 0) or (availH <= 0) then begin FCacheDirty := False; Exit; end; + + B0 := TBitmap.Create; + B1 := TBitmap.Create; + try + B0.PixelFormat := pf32bit; B0.SetSize(8, 8); + ContentW := ComputeContentW(B0.Canvas, availW); + if ContentW <= 0 then begin FCacheDirty := False; Exit; end; + + B0.SetSize(ContentW, availH); + B1.PixelFormat := pf32bit; B1.SetSize(ContentW, availH); + B0.Canvas.Brush.Style := bsSolid; B0.Canvas.Brush.Color := clBlack; + B0.Canvas.FillRect(Rect(0, 0, ContentW, availH)); + B1.Canvas.Brush.Style := bsSolid; B1.Canvas.Brush.Color := clWhite; + B1.Canvas.FillRect(Rect(0, 0, ContentW, availH)); + + DrawSelf(B0, B0.Canvas, 0, 0, ContentW, availH); + DrawSelf(B1, B1.Canvas, 0, 0, ContentW, availH); // итоговые hit-rect отсюда + for I := 0 to FHitCount - 1 do + OffsetRect(FHitRects[I], FLeft, FTop); + + FCache.SetSize(ContentW, availH); + B0.BeginUpdate(False); B1.BeginUpdate(False); FCache.BeginUpdate(False); + try + for Y := 0 to availH - 1 do + begin + R0 := PByte(B0.ScanLine[Y]); + R1 := PByte(B1.ScanLine[Y]); + RC := PByte(FCache.ScanLine[Y]); + if (R0 = nil) or (R1 = nil) or (RC = nil) then Continue; + for X := 0 to ContentW - 1 do + begin +{$IFDEF DARWIN} + d := (EnsureRange(R1[1]-R0[1],0,255) + EnsureRange(R1[2]-R0[2],0,255) + + EnsureRange(R1[3]-R0[3],0,255)) div 3; + A := 255 - d; + if A <= 0 then begin RC[0]:=0; RC[1]:=0; RC[2]:=0; RC[3]:=0; end + else begin + RC[0] := A; + RC[1] := EnsureRange(R0[1]*255 div A, 0, 255); + RC[2] := EnsureRange(R0[2]*255 div A, 0, 255); + RC[3] := EnsureRange(R0[3]*255 div A, 0, 255); + end; +{$ELSE} + d := (EnsureRange(R1[0]-R0[0],0,255) + EnsureRange(R1[1]-R0[1],0,255) + + EnsureRange(R1[2]-R0[2],0,255)) div 3; + A := 255 - d; + if A <= 0 then begin RC[0]:=0; RC[1]:=0; RC[2]:=0; RC[3]:=0; end + else begin + RC[0] := EnsureRange(R0[0]*255 div A, 0, 255); + RC[1] := EnsureRange(R0[1]*255 div A, 0, 255); + RC[2] := EnsureRange(R0[2]*255 div A, 0, 255); + RC[3] := A; + end; +{$ENDIF} + Inc(R0, 4); Inc(R1, 4); Inc(RC, 4); + end; + end; + finally + FCache.EndUpdate(False); B1.EndUpdate(False); B0.EndUpdate(False); + end; + FCacheW := ContentW; + finally + B0.Free; B1.Free; + end; + + FKeyRate := FCurrentRate; FKeyHot := FHotIdx; FKeyHidden := FPanelHidden; + FKeyLeft := FLeft; FKeyTop := FTop; FKeyW := W; FKeyH := H; + FCacheDirty := False; +end; + +// Альфа-композит FCache поверх Target в точке (FLeft, FTop) — один проход/кадр. +procedure TSampleRateOverlay.CompositeCache(Target: TBitmap); +var X, Y, A, InvA, h: Integer; RS, RD: PByte; +begin + if (Target = nil) or (FCacheW <= 0) then Exit; + h := FCache.Height; + Target.BeginUpdate(False); + FCache.BeginUpdate(False); + try + for Y := 0 to h - 1 do + begin + if (FTop + Y < 0) or (FTop + Y >= Target.Height) then Continue; + RS := PByte(FCache.ScanLine[Y]); + RD := PByte(Target.ScanLine[FTop + Y]); + if (RS = nil) or (RD = nil) then Continue; + Inc(RD, FLeft * 4); + for X := 0 to FCacheW - 1 do + begin + if (FLeft + X >= 0) and (FLeft + X < Target.Width) then + begin +{$IFDEF DARWIN} + A := RS[0]; InvA := 255 - A; + if A > 0 then + begin + RD[1] := Byte((RS[1]*A + RD[1]*InvA) div 255); + RD[2] := Byte((RS[2]*A + RD[2]*InvA) div 255); + RD[3] := Byte((RS[3]*A + RD[3]*InvA) div 255); + end; +{$ELSE} + A := RS[3]; InvA := 255 - A; + if A > 0 then + begin + RD[0] := Byte((RS[0]*A + RD[0]*InvA) div 255); + RD[1] := Byte((RS[1]*A + RD[1]*InvA) div 255); + RD[2] := Byte((RS[2]*A + RD[2]*InvA) div 255); + end; +{$ENDIF} + end; + Inc(RS, 4); Inc(RD, 4); + end; + end; + finally + FCache.EndUpdate(False); + Target.EndUpdate(False); + end; +end; + procedure TSampleRateOverlay.DrawOverlay(Target: TBitmap; C: TCanvas; W, H: Integer); begin if (FWidth <= 0) or (FHeight <= 0) then Exit; if (FLeft >= W) or (FTop >= H) then Exit; - DrawSelf(Target, C, FLeft, FTop, Min(FWidth, W - FLeft), Min(FHeight, H - FTop)); + if FCacheDirty or (FKeyRate <> FCurrentRate) or (FKeyHot <> FHotIdx) or + (FKeyHidden <> FPanelHidden) or (FKeyLeft <> FLeft) or (FKeyTop <> FTop) or + (FKeyW <> W) or (FKeyH <> H) then + RebuildCache(C, W, H); + CompositeCache(Target); end; procedure TSampleRateOverlay.DrawOverlayBitmap(Target: TBitmap; W, H: Integer);