mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
feat(waterfall): палитры Inferno/Turbo + gamma, разрешение до 4096, HiDPI
- SPECTRUM_PIXELS 1024→4096 + настройка display_pixels (лимит точек анализатора, SetDisplayResLimit); web-зеркало остаётся 1024 — max-децимация в SnapshotPixels - палитры водопада Classic/Inferno/Turbo + регулятор gamma (в LUT), настройки wf_palette/wf_gamma, страница Waterfall → группа Rendering - GL-водопад переведён на общий LUT/AGC-уровни базового класса (убраны дубли ColorForDB/UpdateLevels) - явные glViewport в GL-вьюхах умножаются на PixelScale (HiDPI), текстура водопада — в физической ширине Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -587,6 +587,8 @@ type
|
||||
SpecAvgTimeMS: Double);
|
||||
procedure ApplyWaterfallParams(WfDet, WfAvgMode: Integer;
|
||||
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double);
|
||||
procedure ApplyWfRenderParams(Palette: Integer; Gamma: Double;
|
||||
ResLimit: Integer);
|
||||
procedure ApplyWfAGCNF(WfAGC, WfNF: Boolean);
|
||||
procedure ApplyADCSettings(Dither, Random: Boolean);
|
||||
procedure ApplyWebSettings(Enabled: Boolean; Port: Integer;
|
||||
@@ -1028,6 +1030,8 @@ begin
|
||||
FSpecView.WfManualHigh := FController.FWfManualHigh;
|
||||
FSpecView.WfManualLow := FController.FWfManualLow;
|
||||
FSpecView.WfAGCOffset := FController.FWfAGCOffset;
|
||||
FSpecView.WfPalette := FController.FWfPalette;
|
||||
FSpecView.WfGamma := FController.FWfGamma;
|
||||
FSpecView.SpecRefLevel := FController.FSpecRefLevel;
|
||||
FSpecView.SpecRange := FController.FSpecRange;
|
||||
FSpecView.SpecGridStep := FController.FSpecGridStep;
|
||||
@@ -1469,6 +1473,8 @@ begin
|
||||
FSpecView.WfManualHigh := FController.FWfManualHigh;
|
||||
FSpecView.WfManualLow := FController.FWfManualLow;
|
||||
FSpecView.WfAGCOffset := FController.FWfAGCOffset;
|
||||
FSpecView.WfPalette := FController.FWfPalette;
|
||||
FSpecView.WfGamma := FController.FWfGamma;
|
||||
FSpecView.ResetWfAvgBuf;
|
||||
if Assigned(FWidebandView) then
|
||||
FWidebandView.SetFillSpectrum(FController.FWidebandFill);
|
||||
@@ -6435,6 +6441,20 @@ begin
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ApplyWfRenderParams(Palette: Integer; Gamma: Double;
|
||||
ResLimit: Integer);
|
||||
begin
|
||||
FController.FWfPalette := Palette;
|
||||
FController.FWfGamma := Gamma;
|
||||
FController.FDisplayPixels := ResLimit;
|
||||
FSpecView.WfPalette := Palette;
|
||||
FSpecView.WfGamma := Gamma;
|
||||
if FController.FWDSPReady then
|
||||
FController.FDSPEngine.SetDisplayResLimit(ResLimit);
|
||||
FWaterfallDirty := True;
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ApplyGridParams(RefLevel, Range, GridStep: Double);
|
||||
begin
|
||||
FController.FSpecRefLevel := RefLevel;
|
||||
@@ -6602,6 +6622,7 @@ begin
|
||||
FSettingsForm := SF;
|
||||
SF.OnDisplayChange := ApplyDisplayParams;
|
||||
SF.OnWaterfallChange := ApplyWaterfallParams;
|
||||
SF.OnWfRenderChange := ApplyWfRenderParams;
|
||||
SF.OnGridChange := ApplyGridParams;
|
||||
SF.OnAudioDevChange := ApplyAudioDevice;
|
||||
SF.OnAudioInDevChange := ApplyAudioInputDevice;
|
||||
@@ -6646,6 +6667,8 @@ begin
|
||||
SF.LoadVisibility(FController.FShowSpectrum, FController.FShowWaterfall, FController.FShowWideband, FController.FWidebandFill,
|
||||
FController.FSpectrumFill);
|
||||
SF.LoadWfAGCNF(FController.FWfAGCEnabled, FController.FWfNFEnabled);
|
||||
SF.LoadWfRender(FController.FWfPalette, FController.FWfGamma,
|
||||
FController.FDisplayPixels);
|
||||
SF.LoadADCSettings(FController.FDitherEnabled, FController.FRandomEnabled);
|
||||
SF.LoadWebSettings(FWebEnabled, FWebPort, FWebBindAddr, FWebUser, FWebPass);
|
||||
SF.LoadFPS(FDisplayFPS);
|
||||
|
||||
+42
-14
@@ -281,6 +281,9 @@ type
|
||||
FWfManualHigh: Double;
|
||||
FWfManualLow: Double;
|
||||
FWfAGCOffset: Double;
|
||||
FWfPalette: Integer; // 0=Classic, 1=Inferno, 2=Turbo
|
||||
FWfGamma: Double; // контраст палитры водопада
|
||||
FDisplayPixels: Integer; // потолок точек анализатора
|
||||
FSpecRefLevel: Double;
|
||||
FSpecRange: Double;
|
||||
FSpecGridStep: Double;
|
||||
@@ -688,6 +691,7 @@ begin
|
||||
FPAMaxPower := 100.0;
|
||||
FPlutoTxMaxAttDb := -10.0; // ADI-дефолт: линейный режим выходных каскадов
|
||||
FWfAGCEnabled := True; FDitherEnabled := True; FRandomEnabled := True;
|
||||
FWfPalette := 0; FWfGamma := 1.0; FDisplayPixels := 4096;
|
||||
FFMStepOn := True;
|
||||
FLocalAudio := True; // GUI: звук на локальную карту; демон выключит явно
|
||||
FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1;
|
||||
@@ -709,6 +713,7 @@ begin
|
||||
// Роутинг аудио слайсов на их звуковухи — внутренний, вешаем сразу (не
|
||||
// зависит от фронтенда, в отличие от OnAudio/OnSpectrum).
|
||||
FDSPEngine.OnSliceAudio := OnSliceAudioReady;
|
||||
FDSPEngine.SetDisplayResLimit(FDisplayPixels);
|
||||
|
||||
// QO-100 beacon lock: эталон частоты = декодер (см. ServiceBeaconLock).
|
||||
FBeaconRefHz := 10489750000.0; // центральный BPSK-маяк NB-транспондера (downlink)
|
||||
@@ -1256,18 +1261,40 @@ begin
|
||||
if Result then S := FSlices[idx];
|
||||
end;
|
||||
|
||||
// Снимок кадра для web-зеркала: протокол фиксирован на 1024 точки, поэтому
|
||||
// при большем разрешении анализатора децимируем max-детектором (не обрезаем).
|
||||
procedure SnapshotPixels(const Pixels: array of Single; Count: Integer;
|
||||
var Dst: array of Single; out DstCount: Integer);
|
||||
var
|
||||
i, j, S0, S1, N: Integer;
|
||||
V: Single;
|
||||
begin
|
||||
N := Length(Dst);
|
||||
if Count > Length(Pixels) then Count := Length(Pixels);
|
||||
if Count <= N then
|
||||
begin
|
||||
for i := 0 to Count - 1 do Dst[i] := Pixels[i];
|
||||
DstCount := Count;
|
||||
Exit;
|
||||
end;
|
||||
for i := 0 to N - 1 do
|
||||
begin
|
||||
S0 := Int64(i) * Count div N;
|
||||
S1 := (Int64(i) + 1) * Count div N - 1;
|
||||
V := Pixels[S0];
|
||||
for j := S0 + 1 to S1 do
|
||||
if Pixels[j] > V then V := Pixels[j];
|
||||
Dst[i] := V;
|
||||
end;
|
||||
DstCount := N;
|
||||
end;
|
||||
|
||||
procedure TRadioController.OnSpectrumReady(const Pixels: array of Single;
|
||||
Count: Integer);
|
||||
// DSP-поток: сохраняем последний кадр для web-зеркала и отдаём сырьё наружу
|
||||
// (UI рисует FSpecView). Децимации у спектра нет.
|
||||
var
|
||||
i, N: Integer;
|
||||
begin
|
||||
N := Count;
|
||||
if N > Length(FSpectrumBuf) then N := Length(FSpectrumBuf);
|
||||
for i := 0 to N - 1 do
|
||||
FSpectrumBuf[i] := Pixels[i];
|
||||
FSpectrumBufCount := N;
|
||||
SnapshotPixels(Pixels, Count, FSpectrumBuf, FSpectrumBufCount);
|
||||
if Assigned(FOnSpectrumData) then FOnSpectrumData(Pixels, Count);
|
||||
end;
|
||||
|
||||
@@ -1275,14 +1302,8 @@ procedure TRadioController.OnWaterfallReady(const Pixels: array of Single;
|
||||
Count: Integer);
|
||||
// DSP-поток: web-зеркало получает КАЖДЫЙ кадр; децимация рендера водопада —
|
||||
// забота фронтенда (OnWaterfallData).
|
||||
var
|
||||
i, N: Integer;
|
||||
begin
|
||||
N := Count;
|
||||
if N > Length(FWaterfallBuf) then N := Length(FWaterfallBuf);
|
||||
for i := 0 to N - 1 do
|
||||
FWaterfallBuf[i] := Pixels[i];
|
||||
FWaterfallBufCount := N;
|
||||
SnapshotPixels(Pixels, Count, FWaterfallBuf, FWaterfallBufCount);
|
||||
if Assigned(FOnWaterfallData) then FOnWaterfallData(Pixels, Count);
|
||||
end;
|
||||
|
||||
@@ -1892,6 +1913,9 @@ begin
|
||||
Result.WfManualHigh := FWfManualHigh;
|
||||
Result.WfManualLow := FWfManualLow;
|
||||
Result.WfAGCOffset := FWfAGCOffset;
|
||||
Result.WfPalette := FWfPalette;
|
||||
Result.WfGamma := FWfGamma;
|
||||
Result.DisplayPixels := FDisplayPixels;
|
||||
Result.SpecRefLevel := FSpecRefLevel;
|
||||
Result.SpecRange := FSpecRange;
|
||||
Result.SpecGridStep := FSpecGridStep;
|
||||
@@ -3478,6 +3502,10 @@ begin
|
||||
FWfManualHigh := FLoadedGlobal.WfManualHigh;
|
||||
FWfManualLow := FLoadedGlobal.WfManualLow;
|
||||
FWfAGCOffset := FLoadedGlobal.WfAGCOffset;
|
||||
FWfPalette := EnsureRange(FLoadedGlobal.WfPalette, 0, 2);
|
||||
FWfGamma := EnsureRange(FLoadedGlobal.WfGamma, 0.3, 3.0);
|
||||
FDisplayPixels := EnsureRange(FLoadedGlobal.DisplayPixels, 64, 4096);
|
||||
if FWDSPReady then FDSPEngine.SetDisplayResLimit(FDisplayPixels);
|
||||
FShowSpectrum := FLoadedGlobal.ShowSpectrum;
|
||||
FShowWaterfall := FLoadedGlobal.ShowWaterfall;
|
||||
FShowWideband := FLoadedGlobal.ShowWideband;
|
||||
|
||||
@@ -250,6 +250,9 @@ type
|
||||
WfManualHigh: Double; // manual waterfall high threshold
|
||||
WfManualLow: Double; // manual waterfall low threshold
|
||||
WfAGCOffset: Double; // AGC offset in dB
|
||||
WfPalette: Integer; // 0=Classic, 1=Inferno, 2=Turbo
|
||||
WfGamma: Double; // контраст палитры (0.3..3.0, 1.0 = линейно)
|
||||
DisplayPixels: Integer; // потолок точек анализатора (1024/2048/4096)
|
||||
SpecRefLevel: Double; // dBm reference (top of scale), e.g. -20
|
||||
SpecRange: Double; // dB range, e.g. 110 (bottom = ref - range)
|
||||
SpecGridStep: Double; // grid step in dB, e.g. 10
|
||||
@@ -443,6 +446,9 @@ begin
|
||||
G.WfManualHigh := -80.0;
|
||||
G.WfManualLow := -130.0;
|
||||
G.WfAGCOffset := 0.0;
|
||||
G.WfPalette := 0; // Classic
|
||||
G.WfGamma := 1.0;
|
||||
G.DisplayPixels := 4096; // 1:1 до 4K-ширины
|
||||
G.SpecRefLevel := -20.0;
|
||||
G.SpecRange := 110.0;
|
||||
G.SpecGridStep := 10.0;
|
||||
@@ -666,6 +672,9 @@ begin
|
||||
G.WfManualHigh := JD(GObj,'wf_manual_high',-80.0);
|
||||
G.WfManualLow := JD(GObj,'wf_manual_low',-130.0);
|
||||
G.WfAGCOffset := JD(GObj,'wf_agc_offset',0.0);
|
||||
G.WfPalette := JI(GObj,'wf_palette',0);
|
||||
G.WfGamma := JD(GObj,'wf_gamma',1.0);
|
||||
G.DisplayPixels := JI(GObj,'display_pixels',4096);
|
||||
G.SpecRefLevel := JD(GObj,'spec_ref_level',-20.0);
|
||||
G.SpecRange := JD(GObj,'spec_range',110.0);
|
||||
G.SpecGridStep := JD(GObj,'spec_grid_step',10.0);
|
||||
@@ -771,6 +780,9 @@ begin
|
||||
JW(O,'wf_manual_high',G.WfManualHigh);
|
||||
JW(O,'wf_manual_low',G.WfManualLow);
|
||||
JW(O,'wf_agc_offset',G.WfAGCOffset);
|
||||
JW(O,'wf_palette',G.WfPalette);
|
||||
JW(O,'wf_gamma',G.WfGamma);
|
||||
JW(O,'display_pixels',G.DisplayPixels);
|
||||
JW(O,'spec_ref_level',G.SpecRefLevel);
|
||||
JW(O,'spec_range',G.SpecRange);
|
||||
JW(O,'spec_grid_step',G.SpecGridStep);
|
||||
|
||||
@@ -47,6 +47,8 @@ type
|
||||
SpecAvgMode: Integer; SpecAvgTimeMS: Double) of object;
|
||||
TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode: Integer;
|
||||
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double) of object;
|
||||
TOnWfRenderChange = procedure(Palette: Integer; Gamma: Double;
|
||||
ResLimit: Integer) of object;
|
||||
TOnGridParamChange = procedure(RefLevel, Range, GridStep: Double) of object;
|
||||
TOnAudioDevChange = procedure(DevIndex: Integer; const DevName: string) of object;
|
||||
TOnAudioInDevChange = procedure(DevIndex: Integer; const DevName: string) of object;
|
||||
@@ -119,6 +121,9 @@ type
|
||||
FWfAGCOffset: TFlatFloatSpinEdit;
|
||||
FChkWfAGC: TFlatCheckBox;
|
||||
FChkWfNF: TFlatCheckBox;
|
||||
FCmbWfPalette: TFlatComboBox;
|
||||
FWfGamma: TFlatFloatSpinEdit;
|
||||
FCmbWfRes: TFlatComboBox;
|
||||
FEdRefLevel: TFlatEdit;
|
||||
FEdRange: TFlatEdit;
|
||||
FCmbGridStep: TFlatComboBox;
|
||||
@@ -289,6 +294,7 @@ type
|
||||
FOnWebSettingsChange: TOnWebSettingsChange;
|
||||
FOnDisplayChange: TOnDisplayParamChange;
|
||||
FOnWaterfallChange: TOnWaterfallParamChange;
|
||||
FOnWfRenderChange: TOnWfRenderChange;
|
||||
FOnGridChange: TOnGridParamChange;
|
||||
FOnAudioDevChange: TOnAudioDevChange;
|
||||
FOnAudioInDevChange: TOnAudioInDevChange;
|
||||
@@ -378,6 +384,8 @@ type
|
||||
|
||||
procedure FireDisplayChange;
|
||||
procedure FireWaterfallChange;
|
||||
procedure FireWfRenderChange;
|
||||
procedure OnWfRenderCtlChange(Sender: TObject);
|
||||
procedure FireGridChange;
|
||||
|
||||
function GetSpecAvgTimeMS: Double;
|
||||
@@ -423,6 +431,7 @@ type
|
||||
procedure LoadAlexSettings(const A: TAlexSettings; BoardType: Integer);
|
||||
procedure LoadXvtrSettings(const X: TXvtrSettings);
|
||||
procedure LoadWfAGCNF(AGC, NF: Boolean);
|
||||
procedure LoadWfRender(Palette: Integer; Gamma: Double; ResLimit: Integer);
|
||||
procedure LoadADCSettings(Dither, Random: Boolean);
|
||||
procedure LoadWebSettings(Enabled: Boolean; Port: Integer;
|
||||
const BindAddr, User, Pass: string);
|
||||
@@ -435,6 +444,7 @@ type
|
||||
property OnVHFCalChange: TOnVHFCalChange read FOnVHFCalChange write FOnVHFCalChange;
|
||||
property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange;
|
||||
property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange;
|
||||
property OnWfRenderChange: TOnWfRenderChange read FOnWfRenderChange write FOnWfRenderChange;
|
||||
property OnGridChange: TOnGridParamChange read FOnGridChange write FOnGridChange;
|
||||
property OnAudioDevChange: TOnAudioDevChange read FOnAudioDevChange write FOnAudioDevChange;
|
||||
property OnAudioInDevChange: TOnAudioInDevChange read FOnAudioInDevChange write FOnAudioInDevChange;
|
||||
@@ -492,6 +502,10 @@ const
|
||||
GRID_STEPS: array[0..4] of Double = (5.0, 10.0, 20.0, 30.0, 40.0);
|
||||
GRID_STEP_NAMES: array[0..4] of string = ('5 dB','10 dB','20 dB','30 dB','40 dB');
|
||||
|
||||
WF_PALETTE_NAMES: array[0..2] of string = ('Classic', 'Inferno', 'Turbo');
|
||||
DISPLAY_RES_VALUES: array[0..2] of Integer = (1024, 2048, 4096);
|
||||
DISPLAY_RES_NAMES: array[0..2] of string = ('1024 px', '2048 px', '4096 px');
|
||||
|
||||
FPS_VALUES: array[0..12] of Integer = (5, 10, 15, 20, 25, 30, 40, 50, 60, 75, 80, 90, 100);
|
||||
FPS_NAMES: array[0..12] of string = ('5 fps','10 fps','15 fps','20 fps','25 fps','30 fps',
|
||||
'40 fps','50 fps','60 fps','75 fps','80 fps','90 fps','100 fps');
|
||||
@@ -1093,6 +1107,19 @@ begin
|
||||
FChkWfNF.Font.Name := UI_FONT;
|
||||
FChkWfNF.Font.Size := 9;
|
||||
FChkWfNF.OnChange := OnWfAGCNFChkChange;
|
||||
|
||||
Grp := MakeGroupPanel(FPageWaterfall, 'Rendering', MARGIN, 614, 650, 170);
|
||||
MakeLbl(Grp, 'Palette:', PAD, R1 + 5, LW);
|
||||
FCmbWfPalette := MakeCombo(Grp, CX, R1, CMB_W, OnWfRenderCtlChange);
|
||||
for i := 0 to High(WF_PALETTE_NAMES) do FCmbWfPalette.Items.Add(WF_PALETTE_NAMES[i]);
|
||||
FCmbWfPalette.ItemIndex := 0;
|
||||
MakeLbl(Grp, 'Gamma:', PAD, R1 + STEP + 5, LW);
|
||||
FWfGamma := MkFloat(CX, R1 + STEP, SPW, 1.0, 0.3, 3.0, 0.05, OnWfRenderCtlChange);
|
||||
FWfGamma.DecimalPlaces := 2;
|
||||
MakeLbl(Grp, 'Resolution:', PAD, R1 + 2 * STEP + 5, LW);
|
||||
FCmbWfRes := MakeCombo(Grp, CX, R1 + 2 * STEP, 130, OnWfRenderCtlChange);
|
||||
for i := 0 to High(DISPLAY_RES_NAMES) do FCmbWfRes.Items.Add(DISPLAY_RES_NAMES[i]);
|
||||
FCmbWfRes.ItemIndex := High(DISPLAY_RES_NAMES);
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -2310,6 +2337,41 @@ begin
|
||||
GetWfAGCOffset);
|
||||
end;
|
||||
|
||||
procedure TSettingsForm.FireWfRenderChange;
|
||||
var
|
||||
ResV: Integer;
|
||||
begin
|
||||
if FLoading then Exit;
|
||||
if not Assigned(FOnWfRenderChange) then Exit;
|
||||
if (FCmbWfRes.ItemIndex >= 0) and (FCmbWfRes.ItemIndex <= High(DISPLAY_RES_VALUES)) then
|
||||
ResV := DISPLAY_RES_VALUES[FCmbWfRes.ItemIndex]
|
||||
else
|
||||
ResV := DISPLAY_RES_VALUES[High(DISPLAY_RES_VALUES)];
|
||||
FOnWfRenderChange(Max(0, FCmbWfPalette.ItemIndex), FWfGamma.Value, ResV);
|
||||
end;
|
||||
|
||||
procedure TSettingsForm.OnWfRenderCtlChange(Sender: TObject);
|
||||
begin FireWfRenderChange; end;
|
||||
|
||||
procedure TSettingsForm.LoadWfRender(Palette: Integer; Gamma: Double; ResLimit: Integer);
|
||||
var i: Integer;
|
||||
begin
|
||||
FLoading := True;
|
||||
try
|
||||
if (Palette >= 0) and (Palette < FCmbWfPalette.Items.Count) then
|
||||
FCmbWfPalette.ItemIndex := Palette
|
||||
else
|
||||
FCmbWfPalette.ItemIndex := 0;
|
||||
FWfGamma.Value := EnsureRange(Gamma, 0.3, 3.0);
|
||||
FCmbWfRes.ItemIndex := High(DISPLAY_RES_VALUES);
|
||||
for i := 0 to High(DISPLAY_RES_VALUES) do
|
||||
if DISPLAY_RES_VALUES[i] = ResLimit then
|
||||
begin FCmbWfRes.ItemIndex := i; Break; end;
|
||||
finally
|
||||
FLoading := False;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TSettingsForm.FireGridChange;
|
||||
begin
|
||||
if FLoading then Exit;
|
||||
|
||||
+15
-5
@@ -100,7 +100,7 @@ type
|
||||
FBeaconDecFreq: Double;
|
||||
FBeaconDecHalf: Double; // полуширина фильтра, Гц
|
||||
// ── Spectrum buffer ───────────────────────────────────────────────────────
|
||||
FSpectrumBuf: array[0..1023] of Single;
|
||||
FSpectrumBuf: array[0..WF_MAX_PIXELS-1] of Single;
|
||||
FSpectrumBufCount: Integer;
|
||||
FSpectrumDirty: Boolean;
|
||||
// ── Sub-views ─────────────────────────────────────────────────────────────
|
||||
@@ -172,6 +172,10 @@ type
|
||||
procedure SetWfAGCOffset(V: Double);
|
||||
function GetWfFrameInterval: Integer;
|
||||
procedure SetWfFrameInterval(V: Integer);
|
||||
function GetWfPalette: Integer;
|
||||
procedure SetWfPalette(V: Integer);
|
||||
function GetWfGamma: Double;
|
||||
procedure SetWfGamma(V: Double);
|
||||
procedure SetPbWaterfall(V: TControl);
|
||||
// SMeter sub-view delegating accessors
|
||||
function GetLastSMeter: Double;
|
||||
@@ -254,6 +258,8 @@ type
|
||||
property FillSpectrum: Boolean read FFillSpectrum write SetFillSpectrum;
|
||||
property WaterfallDirty: Boolean read GetWaterfallDirty write SetWaterfallDirty;
|
||||
property WfFrameInterval: Integer read GetWfFrameInterval write SetWfFrameInterval;
|
||||
property WfPalette: Integer read GetWfPalette write SetWfPalette;
|
||||
property WfGamma: Double read GetWfGamma write SetWfGamma;
|
||||
|
||||
// ── Ссылки на PaintBox ────────────────────────────────────────────────────
|
||||
property PbWaterfall: TControl write SetPbWaterfall;
|
||||
@@ -528,6 +534,10 @@ function TSpectrumView.GetWfAGCOffset: Double; begin Result := FWaterfall.
|
||||
procedure TSpectrumView.SetWfAGCOffset(V: Double); begin FWaterfall.WfAGCOffset := V; end;
|
||||
function TSpectrumView.GetWfFrameInterval: Integer; begin Result := FWaterfall.WfFrameInterval; end;
|
||||
procedure TSpectrumView.SetWfFrameInterval(V: Integer); begin FWaterfall.WfFrameInterval := V; end;
|
||||
function TSpectrumView.GetWfPalette: Integer; begin Result := FWaterfall.WfPalette; end;
|
||||
procedure TSpectrumView.SetWfPalette(V: Integer); begin FWaterfall.WfPalette := V; end;
|
||||
function TSpectrumView.GetWfGamma: Double; begin Result := FWaterfall.WfGamma; end;
|
||||
procedure TSpectrumView.SetWfGamma(V: Double); begin FWaterfall.WfGamma := V; end;
|
||||
procedure TSpectrumView.SetPbWaterfall(V: TControl); begin FWaterfall.PbWaterfall := V; end;
|
||||
|
||||
function TSpectrumView.GetLastSMeter: Double; begin Result := FSMeter.LastSMeter; end;
|
||||
@@ -1148,7 +1158,7 @@ end;
|
||||
procedure TSpectrumView.SetSpectrumData(const Pixels: array of Single; Count: Integer);
|
||||
var i, N: Integer;
|
||||
begin
|
||||
N := Min(Count, 1024);
|
||||
N := Min(Count, WF_MAX_PIXELS);
|
||||
for i := 0 to N - 1 do FSpectrumBuf[i] := Pixels[i];
|
||||
FSpectrumBufCount := N;
|
||||
FSpectrumDirty := True;
|
||||
@@ -1228,7 +1238,7 @@ end;
|
||||
procedure TSpectrumView.ResetSpectrumBuf;
|
||||
var i: Integer;
|
||||
begin
|
||||
for i := 0 to 1023 do FSpectrumBuf[i] := -130.0;
|
||||
for i := 0 to High(FSpectrumBuf) do FSpectrumBuf[i] := -130.0;
|
||||
FWaterfall.ResetWfBuf;
|
||||
end;
|
||||
|
||||
@@ -1249,7 +1259,7 @@ begin
|
||||
Noise := -110 + (Random - 0.5) * 6;
|
||||
if Abs(FreqOff) < 2000 then Sig := -50 - Abs(FreqOff) / 200
|
||||
else Sig := -999;
|
||||
FSpectrumBuf[i mod 1024] := Max(Noise, Sig);
|
||||
FSpectrumBuf[i mod WF_MAX_PIXELS] := Max(Noise, Sig);
|
||||
end;
|
||||
end;
|
||||
|
||||
@@ -1394,7 +1404,7 @@ begin
|
||||
// Точки кривой спектра
|
||||
if FSpPtsLen <> W + 2 then
|
||||
begin SetLength(FSpPts, W + 2); FSpPtsLen := W + 2; end;
|
||||
SrcCount := EnsureRange(FSpectrumBufCount, 2, 1024);
|
||||
SrcCount := EnsureRange(FSpectrumBufCount, 2, WF_MAX_PIXELS);
|
||||
if FTXMode and (FTXSpanHz > 0) and (FSpanHz > 0) then
|
||||
begin
|
||||
for i := 0 to W - 1 do
|
||||
|
||||
@@ -17,7 +17,8 @@ interface
|
||||
uses
|
||||
Classes, SysUtils, Graphics, Controls, Math, Types,
|
||||
OpenGLContextEx, GL,
|
||||
AppTheme, SpectrumView, VfoOverlay, BandPlanOverlay, WaterfallViewOpengl;
|
||||
AppTheme, SpectrumView, VfoOverlay, BandPlanOverlay,
|
||||
WaterfallView, WaterfallViewOpengl;
|
||||
|
||||
type
|
||||
TGLTextureCache = record
|
||||
@@ -663,7 +664,7 @@ var
|
||||
R, G, B: GLFloat;
|
||||
begin
|
||||
if Length(FSpY) <> W then SetLength(FSpY, W);
|
||||
SrcCount := ClampI(FSpectrumBufCount, 2, 1024);
|
||||
SrcCount := ClampI(FSpectrumBufCount, 2, WF_MAX_PIXELS);
|
||||
for I := 0 to W - 1 do
|
||||
begin
|
||||
if FTXMode and (FTXSpanHz > 0) and (FSpanHz > 0) then
|
||||
@@ -944,7 +945,9 @@ begin
|
||||
H := FGLControl.Height;
|
||||
if (W <= 0) or (H <= 0) then Exit;
|
||||
|
||||
glViewport(0, 0, W, H);
|
||||
// HiDPI: viewport в физических пикселях FBO, координаты — логические
|
||||
glViewport(0, 0, Round(W * FGLControl.PixelScale),
|
||||
Round(H * FGLControl.PixelScale));
|
||||
glMatrixMode(GL_PROJECTION);
|
||||
glLoadIdentity;
|
||||
glOrtho(0, W, H, 0, -1, 1);
|
||||
|
||||
+17
-3
@@ -103,7 +103,7 @@ const
|
||||
RX_DISP_ID = 0; // analyzer для RX-спектра/водопада
|
||||
TX_DISP_ID = 1; // analyzer для TX-спектра/водопада
|
||||
DSP_BUFSIZE = 1024;
|
||||
SPECTRUM_PIXELS = 1024;
|
||||
SPECTRUM_PIXELS = 4096; // потолок точек дисплея; синхронно с WaterfallView.WF_MAX_PIXELS
|
||||
DISPLAY_BLOCK_SIZE = 64;
|
||||
|
||||
// Очередь IQ пакетов между сетевым и DSP потоком
|
||||
@@ -338,6 +338,7 @@ type
|
||||
FAGCHangLevel: Double; // читается из WDSP (GetRXAAGCHangLevel)
|
||||
FAGCThresh: Double; // читается из WDSP (GetRXAAGCThresh)
|
||||
FLastSpectrumW: Integer; // последняя известная ширина спектра (пикс)
|
||||
FDisplayResLimit: Integer; // потолок точек анализатора (настройка display_pixels)
|
||||
FShiftHz: Double; // NCO shift для CTUN
|
||||
FNRMode: Integer;
|
||||
FNBMode: Integer;
|
||||
@@ -442,6 +443,7 @@ type
|
||||
procedure SetAGCHangThreshold(Threshold: Integer);
|
||||
procedure UpdateAGCLines(SpectrumW: Integer); // читает hang/thresh из WDSP для линии на спектре
|
||||
procedure SetSpectrumWidth(W: Integer); // обновляет FLastSpectrumW для корректных AGC линий
|
||||
procedure SetDisplayResLimit(V: Integer); // потолок точек анализатора (1024/2048/4096)
|
||||
procedure SetShift(ShiftHz: Double); // CTUN NCO сдвиг
|
||||
procedure SetNRMode(Mode: Integer);
|
||||
procedure SetNR(Enable: Boolean);
|
||||
@@ -851,6 +853,7 @@ begin
|
||||
FDisplayFPS := 60;
|
||||
FEffectiveDisplayFPS := 60.0;
|
||||
FDisplayPixelCount := SPECTRUM_PIXELS;
|
||||
FDisplayResLimit := SPECTRUM_PIXELS;
|
||||
FSpecDetector := 0; // Peak
|
||||
FSpecAvgMode := 3; // Log Recursive
|
||||
FSpecAvgTimeMS := 30.0;
|
||||
@@ -1017,7 +1020,8 @@ begin
|
||||
MaxW := AnalyzerFFT + Round(Min(KEEP_TIME_SEC * FSampleRate,
|
||||
KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100)));
|
||||
CalcAnalyzerTiming(AnalyzerFFT, Overlap, FEffectiveDisplayFPS);
|
||||
DisplayPixels := EnsureRange(FLastSpectrumW, 64, SPECTRUM_PIXELS);
|
||||
DisplayPixels := EnsureRange(FLastSpectrumW, 64,
|
||||
EnsureRange(FDisplayResLimit, 64, SPECTRUM_PIXELS));
|
||||
FDisplayPixelCount := DisplayPixels;
|
||||
|
||||
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
|
||||
@@ -1071,7 +1075,8 @@ begin
|
||||
MaxW := AnalyzerFFT + Round(Min(KEEP_TIME_SEC * FTXSampleRate,
|
||||
KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100)));
|
||||
CalcAnalyzerTiming(AnalyzerFFT, Overlap, EffectiveFPS);
|
||||
DisplayPixels := EnsureRange(FLastSpectrumW, 64, SPECTRUM_PIXELS);
|
||||
DisplayPixels := EnsureRange(FLastSpectrumW, 64,
|
||||
EnsureRange(FDisplayResLimit, 64, SPECTRUM_PIXELS));
|
||||
|
||||
AverageTimeToParams(FTXSpecAvgMode, FTXSpecAvgTimeMS, AvgCount, Backmult);
|
||||
AverageTimeToParams(FTXWfAvgMode, FTXWfAvgTimeMS, WfAvgCount, WfBackmult);
|
||||
@@ -1909,6 +1914,15 @@ begin
|
||||
ApplyAnalyzerSettings;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetDisplayResLimit(V: Integer);
|
||||
begin
|
||||
V := EnsureRange(V, 64, SPECTRUM_PIXELS);
|
||||
if FDisplayResLimit = V then Exit;
|
||||
FDisplayResLimit := V;
|
||||
if FAnalyzerOpen then
|
||||
ApplyAnalyzerSettings;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.UpdateAGCLines(SpectrumW: Integer);
|
||||
begin
|
||||
if not FInitialized then Exit;
|
||||
|
||||
+194
-95
@@ -15,6 +15,16 @@ uses
|
||||
Classes, SysUtils, Graphics, ExtCtrls, Controls, Math,
|
||||
LCLIntf, LCLType, AppTheme;
|
||||
|
||||
const
|
||||
// Максимум точек строки водопада; держать в синхроне с
|
||||
// WDSPEngine.SPECTRUM_PIXELS.
|
||||
WF_MAX_PIXELS = 4096;
|
||||
|
||||
// Палитры водопада (индексы совпадают с настройкой wf_palette)
|
||||
WF_PAL_CLASSIC = 0; // Thetis enhanced (тёмная) / пастельная (светлая тема)
|
||||
WF_PAL_INFERNO = 1;
|
||||
WF_PAL_TURBO = 2;
|
||||
|
||||
type
|
||||
TWaterfallView = class
|
||||
protected
|
||||
@@ -24,11 +34,16 @@ type
|
||||
FWfBitmapH: Integer;
|
||||
FWfRowBuf: array of LongWord; // одна строка (новейшая), нативный порядок
|
||||
FWfHead: Integer; // физ. строка bitmap с новейшей линией
|
||||
FWfLut: array[0..511] of LongWord;
|
||||
FWfLut: array[0..511] of LongWord; // нативный порядок байт (CPU bitmap)
|
||||
FWfLutARGB: array[0..511] of LongWord; // логический $AARRGGBB (GL-путь)
|
||||
FWfLutLow: Double;
|
||||
FWfLutHigh: Double;
|
||||
FWfLutLight: Boolean;
|
||||
FWaterfallBuf: array[0..1023] of Single;
|
||||
FWfLutPalette: Integer;
|
||||
FWfLutGamma: Double;
|
||||
FWfPalette: Integer;
|
||||
FWfGamma: Double;
|
||||
FWaterfallBuf: array[0..WF_MAX_PIXELS-1] of Single;
|
||||
FWaterfallBufCount: Integer;
|
||||
FWaterfallDirty: Boolean;
|
||||
FWfFrameCounter: Integer;
|
||||
@@ -53,6 +68,12 @@ type
|
||||
|
||||
procedure DrawMarkerLine(C: TCanvas; W, H: Integer);
|
||||
procedure BuildWfLut(WfLow, WfHigh: Double);
|
||||
function LutNeedsRebuild(WfLow, WfHigh: Double): Boolean;
|
||||
function PaletteColor(T: Double): LongWord; // t∈[0..1] → $AARRGGBB
|
||||
procedure SetWfPalette(V: Integer);
|
||||
procedure SetWfGamma(V: Double);
|
||||
procedure UpdateAutoLevels; // гистограммный AGC → FWfLow/FWfHigh
|
||||
procedure GetEffectiveLevels(out Lo, Hi: Double);
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
@@ -72,6 +93,8 @@ type
|
||||
property MarkerActive: Boolean read FMarkerActive write FMarkerActive;
|
||||
property MarkerX: Integer read FMarkerX write FMarkerX;
|
||||
property PbWaterfall: TControl write FPbWaterfall;
|
||||
property WfPalette: Integer read FWfPalette write SetWfPalette;
|
||||
property WfGamma: Double read FWfGamma write SetWfGamma;
|
||||
|
||||
procedure SetWaterfallData(const Pixels: array of Single; Count: Integer); virtual;
|
||||
procedure DrawWaterfall; virtual;
|
||||
@@ -97,29 +120,29 @@ begin
|
||||
Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
|
||||
end;
|
||||
|
||||
function WaterfallEnhancedColorThetis(ValueDB, LowDB, HighDB: Double): LongWord;
|
||||
// Классическая тёмная палитра (Thetis enhanced), T∈[0..1]
|
||||
function ClassicDarkColor(T: Double): LongWord;
|
||||
var
|
||||
Overall, Local: Double;
|
||||
Local: Double;
|
||||
R, G, B: Integer;
|
||||
begin
|
||||
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
|
||||
if T <= 0.0 then begin Result := $FF000000; Exit; end;
|
||||
if T >= 1.0 then begin Result := ($FF shl 24) or (255 shl 16) or (124 shl 8) or 192; Exit; end;
|
||||
if T < (2.0 / 9.0) then
|
||||
begin Local := T / (2.0/9.0); R := 0; G := 0; B := Round(Local * 255.0); end
|
||||
else if T < (3.0 / 9.0) then
|
||||
begin Local := (T - 2.0/9.0) / (1.0/9.0); R := 0; G := Round(Local*255.0); B := 255; end
|
||||
else if T < (4.0 / 9.0) then
|
||||
begin Local := (T - 3.0/9.0) / (1.0/9.0); R := 0; G := 255; B := Round((1.0-Local)*255.0); end
|
||||
else if T < (5.0 / 9.0) then
|
||||
begin Local := (T - 4.0/9.0) / (1.0/9.0); R := Round(Local*255.0); G := 255; B := 0; end
|
||||
else if T < (7.0 / 9.0) then
|
||||
begin Local := (T - 5.0/9.0) / (2.0/9.0); R := 255; G := Round((1.0-Local)*255.0); B := 0; end
|
||||
else if T < (8.0 / 9.0) then
|
||||
begin Local := (T - 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);
|
||||
Local := (T - 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;
|
||||
@@ -130,28 +153,48 @@ begin
|
||||
Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B;
|
||||
end;
|
||||
|
||||
function WaterfallLightTheme(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);
|
||||
// Линейная интерполяция по таблице стопов, T∈[0..1]
|
||||
function GradientColor(const SR, SG, SB: array of Integer; T: Double): LongWord;
|
||||
var
|
||||
T: Double;
|
||||
Seg: Integer;
|
||||
R, G, B: Integer;
|
||||
F: Double;
|
||||
Seg, N, R, G, B: Integer;
|
||||
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);
|
||||
N := Length(SR);
|
||||
if T <= 0.0 then
|
||||
begin
|
||||
Result := ($FF shl 24) or (SR[0] shl 16) or (SG[0] shl 8) or SB[0];
|
||||
Exit;
|
||||
end;
|
||||
if T >= 1.0 then
|
||||
begin
|
||||
Result := ($FF shl 24) or (SR[N-1] shl 16) or (SG[N-1] shl 8) or SB[N-1];
|
||||
Exit;
|
||||
end;
|
||||
F := T * (N - 1);
|
||||
Seg := Min(N - 2, Trunc(F));
|
||||
F := F - Seg;
|
||||
R := EnsureRange(Round(SR[Seg] * (1.0 - F) + SR[Seg+1] * F), 0, 255);
|
||||
G := EnsureRange(Round(SG[Seg] * (1.0 - F) + SG[Seg+1] * F), 0, 255);
|
||||
B := EnsureRange(Round(SB[Seg] * (1.0 - F) + SB[Seg+1] * F), 0, 255);
|
||||
Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B;
|
||||
end;
|
||||
|
||||
const
|
||||
// Классическая палитра светлой темы (пастель, фон E0E6E8)
|
||||
LIGHT_R: array[0..7] of Integer = (224, 160, 20, 0, 0, 220, 255, 255);
|
||||
LIGHT_G: array[0..7] of Integer = (230, 185, 80, 185, 200, 210, 90, 20);
|
||||
LIGHT_B: array[0..7] of Integer = (232, 210, 200, 210, 80, 0, 0, 20);
|
||||
|
||||
// Inferno (matplotlib), перцептуально-равномерная, 11 опорных точек
|
||||
INF_R: array[0..10] of Integer = ( 0, 22, 66, 106, 147, 188, 221, 243, 252, 246, 252);
|
||||
INF_G: array[0..10] of Integer = ( 0, 11, 10, 23, 38, 55, 81, 120, 165, 215, 255);
|
||||
INF_B: array[0..10] of Integer = ( 4, 57, 104, 110, 103, 84, 58, 25, 10, 70, 164);
|
||||
|
||||
// Turbo (Google), улучшенный jet без артефактов яркости, 9 опорных точек
|
||||
TRB_R: array[0..8] of Integer = ( 48, 70, 24, 97, 164, 249, 247, 202, 122);
|
||||
TRB_G: array[0..8] of Integer = ( 18, 118, 221, 252, 252, 163, 94, 33, 4);
|
||||
TRB_B: array[0..8] of Integer = ( 59, 232, 194, 108, 60, 63, 33, 6, 3);
|
||||
|
||||
// Преобразует логический $AARRGGBB в нативный для ScanLine порядок байт.
|
||||
// DARWIN (Cocoa): в памяти [A,R,G,B]; прочие (Qt6/Win32): [B,G,R,A].
|
||||
// Платформы little-endian, поэтому байт 0 — младший байт LongWord.
|
||||
@@ -195,9 +238,13 @@ begin
|
||||
FTheme := DarkTheme;
|
||||
FLightTheme := False;
|
||||
FSpanHz := 192000;
|
||||
FWfPalette := WF_PAL_CLASSIC;
|
||||
FWfGamma := 1.0;
|
||||
FWfLutLow := 1.0; // невалидно → LUT перестроится при первом кадре
|
||||
FWfLutHigh := 0.0;
|
||||
FWfLutLight := False;
|
||||
FWfLutPalette := -1;
|
||||
FWfLutGamma := 0.0;
|
||||
ResetWfBuf;
|
||||
end;
|
||||
|
||||
@@ -232,16 +279,34 @@ end;
|
||||
procedure TWaterfallView.ResetWfBuf;
|
||||
var i: Integer;
|
||||
begin
|
||||
for i := 0 to 1023 do FWaterfallBuf[i] := -130.0;
|
||||
for i := 0 to High(FWaterfallBuf) do FWaterfallBuf[i] := -130.0;
|
||||
FWfHigh := FWfManualHigh;
|
||||
FWfLow := FWfManualLow;
|
||||
FWaterfallDirty := True;
|
||||
end;
|
||||
|
||||
procedure TWaterfallView.SetWfPalette(V: Integer);
|
||||
begin
|
||||
V := EnsureRange(V, WF_PAL_CLASSIC, WF_PAL_TURBO);
|
||||
if V = FWfPalette then Exit;
|
||||
FWfPalette := V;
|
||||
FWfLutPalette := -1; // LUT перестроится на следующем кадре
|
||||
FWaterfallDirty := True;
|
||||
end;
|
||||
|
||||
procedure TWaterfallView.SetWfGamma(V: Double);
|
||||
begin
|
||||
V := EnsureRange(V, 0.3, 3.0);
|
||||
if Abs(V - FWfGamma) < 1E-6 then Exit;
|
||||
FWfGamma := V;
|
||||
FWfLutGamma := 0.0; // LUT перестроится на следующем кадре
|
||||
FWaterfallDirty := True;
|
||||
end;
|
||||
|
||||
procedure TWaterfallView.SetWaterfallData(const Pixels: array of Single; Count: Integer);
|
||||
var i, N: Integer;
|
||||
begin
|
||||
N := Min(Count, 1024);
|
||||
N := Min(Count, WF_MAX_PIXELS);
|
||||
for i := 0 to N - 1 do FWaterfallBuf[i] := Pixels[i];
|
||||
FWaterfallBufCount := N;
|
||||
Inc(FWfFrameCounter);
|
||||
@@ -279,58 +344,71 @@ begin
|
||||
C.TextOut(MX - 4 - C.TextWidth(MarkerLbl), 4, MarkerLbl);
|
||||
end;
|
||||
|
||||
// Цвет палитры для нормированного уровня T∈[0..1] (без гаммы — она в LUT)
|
||||
function TWaterfallView.PaletteColor(T: Double): LongWord;
|
||||
begin
|
||||
case FWfPalette of
|
||||
WF_PAL_INFERNO: Result := GradientColor(INF_R, INF_G, INF_B, T);
|
||||
WF_PAL_TURBO: Result := GradientColor(TRB_R, TRB_G, TRB_B, T);
|
||||
else
|
||||
if FLightTheme then
|
||||
Result := GradientColor(LIGHT_R, LIGHT_G, LIGHT_B, T)
|
||||
else
|
||||
Result := ClassicDarkColor(T);
|
||||
end;
|
||||
end;
|
||||
|
||||
function TWaterfallView.LutNeedsRebuild(WfLow, WfHigh: Double): Boolean;
|
||||
begin
|
||||
Result := (FWfLutLight <> FLightTheme) or
|
||||
(FWfLutPalette <> FWfPalette) or
|
||||
(Abs(FWfLutGamma - FWfGamma) > 1E-6) or
|
||||
(Abs(WfLow - FWfLutLow) > 0.05) or
|
||||
(Abs(WfHigh - FWfLutHigh) > 0.05);
|
||||
end;
|
||||
|
||||
// Перестраивает палитру dB→цвет (512 точек) один раз на кадр вместо
|
||||
// вычисления цвета с плавающей точкой на каждый пиксель.
|
||||
procedure TWaterfallView.BuildWfLut(WfLow, WfHigh: Double);
|
||||
var
|
||||
i: Integer;
|
||||
dB, Range: Double;
|
||||
T: Double;
|
||||
C: LongWord;
|
||||
begin
|
||||
Range := WfHigh - WfLow;
|
||||
if Range < 1E-6 then Range := 1E-6;
|
||||
for i := 0 to High(FWfLut) do
|
||||
begin
|
||||
dB := WfLow + (i / High(FWfLut)) * Range;
|
||||
if FLightTheme then
|
||||
C := WaterfallLightTheme(dB, WfLow, WfHigh)
|
||||
else
|
||||
C := WaterfallEnhancedColorThetis(dB, WfLow, WfHigh);
|
||||
FWfLut[i] := NativePixel(C);
|
||||
T := i / High(FWfLut);
|
||||
// Гамма <1 вытягивает слабые сигналы из шума, >1 притемняет
|
||||
if Abs(FWfGamma - 1.0) > 1E-6 then T := Power(T, FWfGamma);
|
||||
C := PaletteColor(T);
|
||||
FWfLutARGB[i] := C;
|
||||
FWfLut[i] := NativePixel(C);
|
||||
end;
|
||||
FWfLutLow := WfLow;
|
||||
FWfLutHigh := WfHigh;
|
||||
FWfLutLight := FLightTheme;
|
||||
FWfLutLow := WfLow;
|
||||
FWfLutHigh := WfHigh;
|
||||
FWfLutLight := FLightTheme;
|
||||
FWfLutPalette := FWfPalette;
|
||||
FWfLutGamma := FWfGamma;
|
||||
end;
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// DrawWaterfall
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
procedure TWaterfallView.DrawWaterfall;
|
||||
// Гистограммный AGC: оценивает шумовой пол (30-й перцентиль) и верх сигналов
|
||||
// (98-й), плавно подтягивает FWfLow/FWfHigh. Общий для CPU- и GL-пути.
|
||||
procedure TWaterfallView.UpdateAutoLevels;
|
||||
var
|
||||
W, H, X: Integer;
|
||||
dB, frac, WatSrcF: Double;
|
||||
WatS0, WatS1: Integer;
|
||||
X, SrcCount, LowTargetCount, HighTargetCount, CumCount, HistIdx: Integer;
|
||||
Hist: array[0..191] of Integer;
|
||||
HistMinDB, HistStepDB, NoiseFloorDB, SignalTopDB: Double;
|
||||
TargetLow, TargetHigh: Double;
|
||||
HistMinDB, HistMaxDB, HistStepDB: Double;
|
||||
NoiseFloorDB, SignalTopDB: Double;
|
||||
CumCount, LowTargetCount, HighTargetCount, HistIdx: Integer;
|
||||
WfHigh, WfLow, LutScale, Step: Double;
|
||||
Row, Idx: Integer; RowPtr: PByte;
|
||||
Pal: LongWord; Hist: array[0..191] of Integer; SrcCount: Integer;
|
||||
const
|
||||
ALPHA_HIGH = 0.10; ALPHA_LOW = 0.08;
|
||||
WF_AUTO_OFFSET = -4.0; WF_MIN_RANGE = 48.0; WF_MAX_RANGE = 62.0;
|
||||
HIST_MAX_DB = 22.0;
|
||||
begin
|
||||
if FWaterfallBitmap = nil then Exit;
|
||||
W := FWaterfallBitmap.Width; H := FWaterfallBitmap.Height;
|
||||
if (W <= 0) or (H <= 0) then Exit;
|
||||
SrcCount := EnsureRange(FWaterfallBufCount, 2, 1024);
|
||||
|
||||
if not FWfAGCEnabled then Exit;
|
||||
SrcCount := EnsureRange(FWaterfallBufCount, 2, WF_MAX_PIXELS);
|
||||
FillChar(Hist, SizeOf(Hist), 0);
|
||||
HistMinDB := -170.0; HistMaxDB := 22.0;
|
||||
HistStepDB := (HistMaxDB - HistMinDB) / Length(Hist);
|
||||
HistMinDB := -170.0;
|
||||
HistStepDB := (HIST_MAX_DB - HistMinDB) / Length(Hist);
|
||||
for X := 0 to SrcCount - 1 do
|
||||
begin
|
||||
HistIdx := EnsureRange(Trunc((FWaterfallBuf[X] - HistMinDB) / HistStepDB), 0, High(Hist));
|
||||
@@ -342,41 +420,62 @@ begin
|
||||
CumCount := 0; NoiseFloorDB := FWfLow; SignalTopDB := FWfHigh;
|
||||
for HistIdx := 0 to High(Hist) do
|
||||
begin
|
||||
CumCount := CumCount + Hist[HistIdx];
|
||||
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
|
||||
CumCount := CumCount + Hist[HistIdx];
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
|
||||
procedure TWaterfallView.GetEffectiveLevels(out Lo, Hi: Double);
|
||||
begin
|
||||
if FWfAGCEnabled then begin Lo := FWfLow; Hi := FWfHigh; end
|
||||
else begin Lo := FWfManualLow; Hi := FWfManualHigh; end;
|
||||
if Hi < Lo + 40.0 then Hi := Lo + 40.0;
|
||||
if Hi > 0.0 then Hi := 0.0;
|
||||
if Lo < -160 then Lo := -160;
|
||||
end;
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// DrawWaterfall
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
procedure TWaterfallView.DrawWaterfall;
|
||||
var
|
||||
W, H, X: Integer;
|
||||
dB, frac, WatSrcF: Double;
|
||||
WatS0, WatS1: Integer;
|
||||
WfHigh, WfLow, LutScale, Step: Double;
|
||||
Row, Idx: Integer; RowPtr: PByte;
|
||||
Pal: LongWord; SrcCount: Integer;
|
||||
begin
|
||||
if FWaterfallBitmap = nil then Exit;
|
||||
W := FWaterfallBitmap.Width; H := FWaterfallBitmap.Height;
|
||||
if (W <= 0) or (H <= 0) then Exit;
|
||||
SrcCount := EnsureRange(FWaterfallBufCount, 2, WF_MAX_PIXELS);
|
||||
|
||||
UpdateAutoLevels;
|
||||
GetEffectiveLevels(WfLow, WfHigh);
|
||||
LutScale := High(FWfLut) / (WfHigh - WfLow);
|
||||
|
||||
// Палитра зависит только от WfLow/WfHigh/темы — перестраиваем лишь при
|
||||
// ощутимом изменении (AGC сдвигает границы медленно).
|
||||
if (FWfLutLight <> FLightTheme) or
|
||||
(Abs(WfLow - FWfLutLow) > 0.05) or
|
||||
(Abs(WfHigh - FWfLutHigh) > 0.05) then
|
||||
// Палитра зависит только от WfLow/WfHigh/темы/палитры/гаммы — перестраиваем
|
||||
// лишь при ощутимом изменении (AGC сдвигает границы медленно).
|
||||
if LutNeedsRebuild(WfLow, WfHigh) then
|
||||
BuildWfLut(WfLow, WfHigh);
|
||||
|
||||
if (FWfBitmapW <> W) or (FWfBitmapH <> H) then
|
||||
|
||||
+24
-142
@@ -38,9 +38,7 @@ type
|
||||
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;
|
||||
@@ -102,125 +100,6 @@ begin
|
||||
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;
|
||||
@@ -235,7 +114,7 @@ begin
|
||||
FLatestRow := 0;
|
||||
SetLength(FRowPixels, W * 4);
|
||||
SetLength(Pixels, W * H * 4);
|
||||
Bg := ColorForDB(-130.0, FWfManualLow, FWfManualHigh);
|
||||
Bg := PaletteColor(0.0); // «холодный» конец палитры = фон
|
||||
for I := 0 to W * H - 1 do
|
||||
begin
|
||||
Pixels[I * 4] := Byte(Bg shr 16);
|
||||
@@ -256,24 +135,18 @@ end;
|
||||
|
||||
procedure TWaterfallViewOpenGL.UploadCurrentRow(W: Integer);
|
||||
var
|
||||
X, SrcCount, S0, S1, Off: Integer;
|
||||
SrcF, Step, Frac, DBV, WfHigh, WfLow: Double;
|
||||
X, SrcCount, S0, S1, Off, Idx: Integer;
|
||||
SrcF, Step, Frac, DBV, WfHigh, WfLow, LutScale: 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;
|
||||
UpdateAutoLevels;
|
||||
GetEffectiveLevels(WfLow, WfHigh);
|
||||
LutScale := High(FWfLut) / (WfHigh - WfLow);
|
||||
if LutNeedsRebuild(WfLow, WfHigh) then
|
||||
BuildWfLut(WfLow, WfHigh);
|
||||
|
||||
SrcCount := EnsureRange(FWaterfallBufCount, 2, 1024);
|
||||
SrcCount := EnsureRange(FWaterfallBufCount, 2, WF_MAX_PIXELS);
|
||||
Step := (SrcCount - 1.0) / Max(1, W - 1);
|
||||
SrcF := 0.0;
|
||||
for X := 0 to W - 1 do
|
||||
@@ -299,7 +172,9 @@ begin
|
||||
Frac := SrcF - S0;
|
||||
DBV := FWaterfallBuf[S0] * (1.0 - Frac) + FWaterfallBuf[S1] * Frac;
|
||||
end;
|
||||
Pal := ColorForDB(DBV, WfLow, WfHigh);
|
||||
Idx := Round((DBV - WfLow) * LutScale);
|
||||
if Idx < 0 then Idx := 0 else if Idx > High(FWfLut) then Idx := High(FWfLut);
|
||||
Pal := FWfLutARGB[Idx];
|
||||
Off := X * 4;
|
||||
FRowPixels[Off] := Byte(Pal shr 16);
|
||||
FRowPixels[Off + 1] := Byte(Pal shr 8);
|
||||
@@ -447,8 +322,10 @@ end;
|
||||
procedure TWaterfallViewOpenGL.SetWaterfallBitmapSize(W, H: Integer);
|
||||
begin
|
||||
if (W <= 0) or (H <= 0) then Exit;
|
||||
// Ширина текстуры — в физических пикселях (HiDPI): строка водопада должна
|
||||
// ложиться 1:1 на пиксели фреймбуфера, иначе GL размажет её при увеличении.
|
||||
if Assigned(FGLControl) and FGLControl.MakeCurrent then
|
||||
EnsureTexture(W, H);
|
||||
EnsureTexture(Round(W * FGLControl.PixelScale), H);
|
||||
FWaterfallDirty := True;
|
||||
end;
|
||||
|
||||
@@ -459,7 +336,8 @@ end;
|
||||
|
||||
procedure TWaterfallViewOpenGL.PaintWaterfall(Sender: TObject);
|
||||
var
|
||||
W, H: Integer;
|
||||
W, H, TexW: Integer;
|
||||
Scale: Double;
|
||||
R, G, B: GLFloat;
|
||||
begin
|
||||
if Sender is TOpenGLControl then FGLControl := TOpenGLControl(Sender);
|
||||
@@ -467,8 +345,12 @@ begin
|
||||
W := FGLControl.Width;
|
||||
H := FGLControl.Height;
|
||||
if (W <= 0) or (H <= 0) then Exit;
|
||||
// HiDPI: viewport покрывает весь FBO (физические пиксели), координаты
|
||||
// остаются логическими; строка водопада строится в физической ширине.
|
||||
Scale := FGLControl.PixelScale;
|
||||
TexW := Round(W * Scale);
|
||||
|
||||
glViewport(0, 0, W, H);
|
||||
glViewport(0, 0, TexW, Round(H * Scale));
|
||||
glMatrixMode(GL_PROJECTION);
|
||||
glLoadIdentity;
|
||||
glOrtho(0, W, H, 0, -1, 1);
|
||||
@@ -483,10 +365,10 @@ begin
|
||||
glClearColor(R, G, B, 1);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
EnsureTexture(W, H);
|
||||
EnsureTexture(TexW, H);
|
||||
if FWaterfallDirty then
|
||||
begin
|
||||
UploadCurrentRow(W);
|
||||
UploadCurrentRow(TexW);
|
||||
FWaterfallDirty := False;
|
||||
end;
|
||||
DrawTextureWrapped(W, H);
|
||||
|
||||
+2
-1
@@ -1077,7 +1077,8 @@ begin
|
||||
UploadLabelTextureToGL;
|
||||
FGLBackgroundDirty := False;
|
||||
end;
|
||||
glViewport(0, 0, W, H);
|
||||
// HiDPI: viewport в физических пикселях FBO, координаты — логические
|
||||
glViewport(0, 0, Round(W * C.PixelScale), Round(H * C.PixelScale));
|
||||
glMatrixMode(GL_PROJECTION);
|
||||
glLoadIdentity;
|
||||
glOrtho(0, W, 0, H, -1, 1);
|
||||
|
||||
Reference in New Issue
Block a user