Files
ewsdr/WaterfallViewOpengl.pas
2026-05-25 20:41:06 +03:00

521 lines
15 KiB
ObjectPascal

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.