unit WaterfallViewOpengl; { OpenGL renderer for the waterfall pane. The CPU path rebuilds a full bitmap after scrolling rows in memory. This renderer keeps one GL texture and uploads only the newest waterfall row. } {$IFDEF FPC} {$MODE Delphi} {$ENDIF} interface uses Classes, SysUtils, Graphics, Controls, Math, OpenGLContext, GL, AppTheme, WaterfallView; type TWaterfallViewOpenGL = class(TWaterfallView) private FGLControl: TOpenGLControl; FTexture: GLuint; FTexW: Integer; FTexH: Integer; FLatestRow: Integer; FRowPixels: array of Byte; FMarkerTex: GLuint; FMarkerTexW: Integer; FMarkerTexH: Integer; FMarkerText: string; procedure DeleteTexture(var Tex: GLuint); procedure EnsureTexture(W, H: Integer); procedure UploadCurrentRow(W: Integer); procedure DrawTextureWrapped(W, H: Integer); procedure DrawMarker(W, H: Integer); procedure UploadMarkerText(const S: string); procedure UpdateLevels; function FormatFreqGL(Hz: Double): string; function ColorForDB(ValueDB, LowDB, HighDB: Double): LongWord; public constructor Create; destructor Destroy; override; procedure AttachControl(C: TOpenGLControl); procedure SetWaterfallBitmapSize(W, H: Integer); override; procedure DrawWaterfall; override; procedure PaintWaterfall(Sender: TObject); override; procedure SetTheme(const T: TAppTheme); override; procedure ResetWfBuf; override; procedure ResetWfAvgBuf; override; end; implementation function EnsureByte(V: Integer): Byte; begin if V < 0 then Result := 0 else if V > 255 then Result := 255 else Result := V; end; function TWaterfallViewOpenGL.FormatFreqGL(Hz: Double): string; var Mhz: Int64; KHz, Rest: Integer; begin Mhz := Trunc(Hz / 1000000); KHz := Trunc((Hz - Mhz * 1000000) / 1000); Rest := Trunc(Hz) mod 1000; Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]); end; constructor TWaterfallViewOpenGL.Create; begin inherited Create; FTexture := 0; FMarkerTex := 0; FTexW := 0; FTexH := 0; FLatestRow := 0; end; destructor TWaterfallViewOpenGL.Destroy; begin if Assigned(FGLControl) and FGLControl.MakeCurrent then begin DeleteTexture(FTexture); DeleteTexture(FMarkerTex); end; inherited Destroy; end; procedure TWaterfallViewOpenGL.AttachControl(C: TOpenGLControl); begin FGLControl := C; end; procedure TWaterfallViewOpenGL.DeleteTexture(var Tex: GLuint); begin if Tex <> 0 then glDeleteTextures(1, @Tex); Tex := 0; end; function TWaterfallViewOpenGL.ColorForDB(ValueDB, LowDB, HighDB: Double): LongWord; const NSTOPS = 8; SR: array[0..NSTOPS-1] of Integer = (224, 160, 20, 0, 0, 220, 255, 255); SG: array[0..NSTOPS-1] of Integer = (230, 185, 80, 185, 200, 210, 90, 20); SB: array[0..NSTOPS-1] of Integer = (232, 210, 200, 210, 80, 0, 0, 20); var Overall, Local, T: Double; Seg, R, G, B: Integer; begin if FLightTheme then begin if ValueDB <= LowDB then begin Result := $FFE0E6E8; Exit; end; if ValueDB >= HighDB then begin Result := ($FF shl 24) or (SR[NSTOPS-1] shl 16) or (SG[NSTOPS-1] shl 8) or SB[NSTOPS-1]; Exit; end; T := (ValueDB - LowDB) / Max(1E-9, HighDB - LowDB) * (NSTOPS - 1); Seg := Min(NSTOPS - 2, Trunc(T)); T := T - Seg; R := EnsureRange(Round(SR[Seg] * (1.0 - T) + SR[Seg+1] * T), 0, 255); G := EnsureRange(Round(SG[Seg] * (1.0 - T) + SG[Seg+1] * T), 0, 255); B := EnsureRange(Round(SB[Seg] * (1.0 - T) + SB[Seg+1] * T), 0, 255); Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B; Exit; end; if ValueDB <= LowDB then begin Result := $FF000000; Exit; end; if ValueDB >= HighDB then begin Result := ($FF shl 24) or (255 shl 16) or (124 shl 8) or 192; Exit; end; Overall := (ValueDB - LowDB) / Max(1E-9, HighDB - LowDB); if Overall < (2.0 / 9.0) then begin Local := Overall / (2.0/9.0); R := 0; G := 0; B := Round(Local * 255.0); end else if Overall < (3.0 / 9.0) then begin Local := (Overall - 2.0/9.0) / (1.0/9.0); R := 0; G := Round(Local*255.0); B := 255; end else if Overall < (4.0 / 9.0) then begin Local := (Overall - 3.0/9.0) / (1.0/9.0); R := 0; G := 255; B := Round((1.0-Local)*255.0); end else if Overall < (5.0 / 9.0) then begin Local := (Overall - 4.0/9.0) / (1.0/9.0); R := Round(Local*255.0); G := 255; B := 0; end else if Overall < (7.0 / 9.0) then begin Local := (Overall - 5.0/9.0) / (2.0/9.0); R := 255; G := Round((1.0-Local)*255.0); B := 0; end else if Overall < (8.0 / 9.0) then begin Local := (Overall - 7.0/9.0) / (1.0/9.0); R := 255; G := 0; B := Round(Local*255.0); end else begin Local := (Overall - 8.0/9.0) / (1.0/9.0); R := Round((0.75 + 0.25 * (1.0 - Local)) * 255.0); G := Round(Local * 255.0 * 0.5); B := 255; end; Result := ($FF shl 24) or (EnsureRange(R, 0, 255) shl 16) or (EnsureRange(G, 0, 255) shl 8) or EnsureRange(B, 0, 255); end; procedure TWaterfallViewOpenGL.UpdateLevels; var X, SrcCount, LowTargetCount, HighTargetCount, CumCount, HistIdx: Integer; Hist: array[0..191] of Integer; HistMinDB, HistMaxDB, HistStepDB, NoiseFloorDB, SignalTopDB: Double; TargetLow, TargetHigh: Double; const ALPHA_HIGH = 0.10; ALPHA_LOW = 0.08; WF_AUTO_OFFSET = -4.0; WF_MIN_RANGE = 48.0; WF_MAX_RANGE = 62.0; begin SrcCount := EnsureRange(FWaterfallBufCount, 2, 1024); FillChar(Hist, SizeOf(Hist), 0); HistMinDB := -170.0; HistMaxDB := 22.0; HistStepDB := (HistMaxDB - HistMinDB) / Length(Hist); for X := 0 to SrcCount - 1 do begin HistIdx := EnsureRange(Trunc((FWaterfallBuf[X] - HistMinDB) / HistStepDB), 0, High(Hist)); Inc(Hist[HistIdx]); end; LowTargetCount := Round(SrcCount * 0.30); HighTargetCount := Round(SrcCount * 0.98); CumCount := 0; NoiseFloorDB := FWfLow; SignalTopDB := FWfHigh; for HistIdx := 0 to High(Hist) do begin Inc(CumCount, Hist[HistIdx]); if CumCount >= LowTargetCount then begin NoiseFloorDB := HistMinDB + (HistIdx + 0.5) * HistStepDB; Break; end; end; CumCount := 0; for HistIdx := 0 to High(Hist) do begin Inc(CumCount, Hist[HistIdx]); if CumCount >= HighTargetCount then begin SignalTopDB := HistMinDB + (HistIdx + 0.5) * HistStepDB; Break; end; end; if FWfAGCEnabled then begin TargetLow := NoiseFloorDB + WF_AUTO_OFFSET + FWfAGCOffset; if FWfNFEnabled then TargetHigh := Max(TargetLow + WF_MIN_RANGE, SignalTopDB + 6.0) else TargetHigh := TargetLow + 52.0; if TargetHigh > TargetLow + WF_MAX_RANGE then TargetHigh := TargetLow + WF_MAX_RANGE; FWfLow := FWfLow + ALPHA_LOW * (TargetLow - FWfLow); FWfHigh := FWfHigh + ALPHA_HIGH * (TargetHigh - FWfHigh); end; end; procedure TWaterfallViewOpenGL.EnsureTexture(W, H: Integer); var Bg: LongWord; Pixels: array of Byte; I: Integer; begin if (W <= 0) or (H <= 0) then Exit; if (FTexture <> 0) and (FTexW = W) and (FTexH = H) then Exit; DeleteTexture(FTexture); FTexW := W; FTexH := H; FLatestRow := 0; SetLength(FRowPixels, W * 4); SetLength(Pixels, W * H * 4); Bg := ColorForDB(-130.0, FWfManualLow, FWfManualHigh); for I := 0 to W * H - 1 do begin Pixels[I * 4] := Byte(Bg shr 16); Pixels[I * 4 + 1] := Byte(Bg shr 8); Pixels[I * 4 + 2] := Byte(Bg); Pixels[I * 4 + 3] := $FF; end; glGenTextures(1, @FTexture); glBindTexture(GL_TEXTURE_2D, FTexture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, W, H, 0, GL_RGBA, GL_UNSIGNED_BYTE, @Pixels[0]); glBindTexture(GL_TEXTURE_2D, 0); end; procedure TWaterfallViewOpenGL.UploadCurrentRow(W: Integer); var X, SrcCount, S0, S1, Off: Integer; SrcF, Step, Frac, DBV, WfHigh, WfLow: Double; Pal: LongWord; begin if (FTexture = 0) or (W <= 0) then Exit; UpdateLevels; WfHigh := FWfHigh; WfLow := FWfLow; if not FWfAGCEnabled then begin WfHigh := FWfManualHigh; WfLow := FWfManualLow; end; if WfHigh < WfLow + 40.0 then WfHigh := WfLow + 40.0; if WfHigh > 0.0 then WfHigh := 0.0; if WfLow < -160 then WfLow := -160; SrcCount := EnsureRange(FWaterfallBufCount, 2, 1024); Step := (SrcCount - 1.0) / Max(1, W - 1); SrcF := 0.0; for X := 0 to W - 1 do begin if FTXMode and (FTXSpanHz > 0) and (FSpanHz > 0) then begin DBV := FCenterFreq - FSpanHz * 0.5 + X * FSpanHz / Max(1, W - 1) - FTXFreq; if (DBV < -FTXSpanHz * 0.5) or (DBV > FTXSpanHz * 0.5) then DBV := WfLow else begin SrcF := (DBV + FTXSpanHz * 0.5) / FTXSpanHz * (SrcCount - 1); S0 := Min(Trunc(SrcF), SrcCount - 2); S1 := S0 + 1; Frac := SrcF - S0; DBV := FWaterfallBuf[S0] * (1.0 - Frac) + FWaterfallBuf[S1] * Frac; end; end else begin S0 := Min(Trunc(SrcF), SrcCount - 2); S1 := S0 + 1; Frac := SrcF - S0; DBV := FWaterfallBuf[S0] * (1.0 - Frac) + FWaterfallBuf[S1] * Frac; end; Pal := ColorForDB(DBV, WfLow, WfHigh); Off := X * 4; FRowPixels[Off] := Byte(Pal shr 16); FRowPixels[Off + 1] := Byte(Pal shr 8); FRowPixels[Off + 2] := Byte(Pal); FRowPixels[Off + 3] := $FF; SrcF := SrcF + Step; end; Dec(FLatestRow); if FLatestRow < 0 then FLatestRow := FTexH - 1; glBindTexture(GL_TEXTURE_2D, FTexture); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, FLatestRow, W, 1, GL_RGBA, GL_UNSIGNED_BYTE, @FRowPixels[0]); glBindTexture(GL_TEXTURE_2D, 0); end; procedure TWaterfallViewOpenGL.DrawTextureWrapped(W, H: Integer); var FirstRows, SecondRows: Integer; V0, V1: GLFloat; begin if FTexture = 0 then Exit; glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, FTexture); glColor4f(1, 1, 1, 1); FirstRows := FTexH - FLatestRow; V0 := FLatestRow / FTexH; V1 := 1.0; glBegin(GL_QUADS); glTexCoord2f(0, V0); glVertex2f(0, 0); glTexCoord2f(1, V0); glVertex2f(W, 0); glTexCoord2f(1, V1); glVertex2f(W, FirstRows); glTexCoord2f(0, V1); glVertex2f(0, FirstRows); glEnd; SecondRows := FLatestRow; if SecondRows > 0 then begin V0 := 0.0; V1 := FLatestRow / FTexH; glBegin(GL_QUADS); glTexCoord2f(0, V0); glVertex2f(0, FirstRows); glTexCoord2f(1, V0); glVertex2f(W, FirstRows); glTexCoord2f(1, V1); glVertex2f(W, H); glTexCoord2f(0, V1); glVertex2f(0, H); glEnd; end; glBindTexture(GL_TEXTURE_2D, 0); glDisable(GL_TEXTURE_2D); end; procedure TWaterfallViewOpenGL.UploadMarkerText(const S: string); var B: TBitmap; X, Y, I: Integer; Src: PByte; Buf: array of Byte; A: Byte; begin if S = '' then Exit; B := TBitmap.Create; try B.PixelFormat := pf32bit; B.SetSize(8, 8); B.Canvas.Font.Name := 'Courier New'; B.Canvas.Font.Size := 7; FMarkerTexW := B.Canvas.TextWidth(S) + 4; FMarkerTexH := B.Canvas.TextHeight(S) + 2; B.SetSize(FMarkerTexW, FMarkerTexH); B.Canvas.Brush.Color := clBlack; B.Canvas.FillRect(0, 0, FMarkerTexW, FMarkerTexH); B.Canvas.Font.Name := 'Courier New'; B.Canvas.Font.Size := 7; B.Canvas.Font.Color := clWhite; B.Canvas.Brush.Style := bsClear; B.Canvas.TextOut(2, 1, S); SetLength(Buf, FMarkerTexW * FMarkerTexH * 4); I := 0; B.BeginUpdate(False); try for Y := 0 to FMarkerTexH - 1 do begin Src := PByte(B.ScanLine[Y]); for X := 0 to FMarkerTexW - 1 do begin A := Max(Src[0], Max(Src[1], Src[2])); Buf[I] := $FF; Buf[I + 1] := $44; Buf[I + 2] := $44; Buf[I + 3] := A; Inc(Src, 4); Inc(I, 4); end; end; finally B.EndUpdate(False); end; if FMarkerTex = 0 then glGenTextures(1, @FMarkerTex); glBindTexture(GL_TEXTURE_2D, FMarkerTex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, FMarkerTexW, FMarkerTexH, 0, GL_RGBA, GL_UNSIGNED_BYTE, @Buf[0]); glBindTexture(GL_TEXTURE_2D, 0); FMarkerText := S; finally B.Free; end; end; procedure TWaterfallViewOpenGL.DrawMarker(W, H: Integer); var MX, TX: Integer; MarkerFreq: Double; S: string; begin if not FMarkerActive then Exit; MX := Round(FMarkerX / 1000.0 * W); if (MX < 0) or (MX >= W) then Exit; glColor4f(1.0, $44 / 255.0, $44 / 255.0, 1); glBegin(GL_LINES); glVertex2f(MX + 0.5, 0); glVertex2f(MX + 0.5, H); glEnd; MarkerFreq := (FCenterFreq - FSpanHz / 2) + FMarkerX / 1000.0 * FSpanHz; S := FormatFreqGL(Round(MarkerFreq)); if (S <> FMarkerText) or (FMarkerTex = 0) then UploadMarkerText(S); if FMarkerTex = 0 then Exit; if MX + 4 + FMarkerTexW < W then TX := MX + 4 else TX := MX - 4 - FMarkerTexW; glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, FMarkerTex); glColor4f(1, 1, 1, 1); glBegin(GL_QUADS); glTexCoord2f(0, 0); glVertex2f(TX, 4); glTexCoord2f(1, 0); glVertex2f(TX + FMarkerTexW, 4); glTexCoord2f(1, 1); glVertex2f(TX + FMarkerTexW, 4 + FMarkerTexH); glTexCoord2f(0, 1); glVertex2f(TX, 4 + FMarkerTexH); glEnd; glBindTexture(GL_TEXTURE_2D, 0); glDisable(GL_TEXTURE_2D); end; procedure TWaterfallViewOpenGL.SetWaterfallBitmapSize(W, H: Integer); begin if (W <= 0) or (H <= 0) then Exit; if Assigned(FGLControl) and FGLControl.MakeCurrent then EnsureTexture(W, H); FWaterfallDirty := True; end; procedure TWaterfallViewOpenGL.DrawWaterfall; begin if Assigned(FGLControl) then FGLControl.Invalidate; end; procedure TWaterfallViewOpenGL.PaintWaterfall(Sender: TObject); var W, H: Integer; R, G, B: GLFloat; begin if Sender is TOpenGLControl then FGLControl := TOpenGLControl(Sender); if (FGLControl = nil) or (not FGLControl.MakeCurrent) then Exit; W := FGLControl.Width; H := FGLControl.Height; if (W <= 0) or (H <= 0) then Exit; glViewport(0, 0, W, H); glMatrixMode(GL_PROJECTION); glLoadIdentity; glOrtho(0, W, H, 0, -1, 1); glMatrixMode(GL_MODELVIEW); glLoadIdentity; glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); R := (FTheme.Panel and $FF) / 255.0; G := ((FTheme.Panel shr 8) and $FF) / 255.0; B := ((FTheme.Panel shr 16) and $FF) / 255.0; glClearColor(R, G, B, 1); glClear(GL_COLOR_BUFFER_BIT); EnsureTexture(W, H); if FWaterfallDirty then begin UploadCurrentRow(W); FWaterfallDirty := False; end; DrawTextureWrapped(W, H); DrawMarker(W, H); FGLControl.SwapBuffers; end; procedure TWaterfallViewOpenGL.SetTheme(const T: TAppTheme); begin inherited SetTheme(T); if Assigned(FGLControl) and FGLControl.MakeCurrent then DeleteTexture(FTexture); FTexW := 0; FTexH := 0; end; procedure TWaterfallViewOpenGL.ResetWfBuf; begin inherited ResetWfBuf; if Assigned(FGLControl) and FGLControl.MakeCurrent then DeleteTexture(FTexture); FTexW := 0; FTexH := 0; end; procedure TWaterfallViewOpenGL.ResetWfAvgBuf; begin inherited ResetWfAvgBuf; end; end.