mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
feat(display): скорость водопада с max-hold накоплением + MSAA спектра
- настройка Speed (wf_speed_div 1/2/4/8, Waterfall→Rendering): замедление накапливает кадры max-hold'ом в SetWaterfallData — короткие посылки (CW) не выпадают из строки; двойная децимация MainForm убрана (EffectiveWfInterval, CPU-путь не быстрее полукадровой) - настройка Anti-aliasing (spec_msaa 1/2/4/8, дефолт 4x, Spectrum→Rendering): MultiSampling на GL-контроле спектра; смена в рантайме пересоздаёт контекст → ResetGLCache обнуляет текстурные кэши без glDelete Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+44
-28
@@ -163,9 +163,6 @@ type
|
|||||||
// переехало в TRadioController (нужно headless-load в DoConnectDevice).
|
// переехало в TRadioController (нужно headless-load в DoConnectDevice).
|
||||||
// Снимок последнего кадра спектра/водопада для web переехал в TRadioController
|
// Снимок последнего кадра спектра/водопада для web переехал в TRadioController
|
||||||
// (FSpectrumBuf/FWaterfallBuf) — нужен headless-демону.
|
// (FSpectrumBuf/FWaterfallBuf) — нужен headless-демону.
|
||||||
// Waterfall frame timing
|
|
||||||
FWaterfallFrameInterval: Integer;
|
|
||||||
FWaterfallFrameCounter: Integer;
|
|
||||||
FAgcLineCounter: Integer;
|
FAgcLineCounter: Integer;
|
||||||
// ---- Splitter ----
|
// ---- Splitter ----
|
||||||
FSplitterDrag: Boolean;
|
FSplitterDrag: Boolean;
|
||||||
@@ -588,7 +585,9 @@ type
|
|||||||
procedure ApplyWaterfallParams(WfDet, WfAvgMode: Integer;
|
procedure ApplyWaterfallParams(WfDet, WfAvgMode: Integer;
|
||||||
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double);
|
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double);
|
||||||
procedure ApplyWfRenderParams(Palette: Integer; Gamma: Double;
|
procedure ApplyWfRenderParams(Palette: Integer; Gamma: Double;
|
||||||
ResLimit: Integer);
|
ResLimit, SpeedDiv: Integer);
|
||||||
|
procedure ApplySpecMSAA(Samples: Integer);
|
||||||
|
function EffectiveWfInterval: Integer;
|
||||||
procedure ApplyWfAGCNF(WfAGC, WfNF: Boolean);
|
procedure ApplyWfAGCNF(WfAGC, WfNF: Boolean);
|
||||||
procedure ApplyADCSettings(Dither, Random: Boolean);
|
procedure ApplyADCSettings(Dither, Random: Boolean);
|
||||||
procedure ApplyWebSettings(Enabled: Boolean; Port: Integer;
|
procedure ApplyWebSettings(Enabled: Boolean; Port: Integer;
|
||||||
@@ -845,8 +844,6 @@ begin
|
|||||||
FController.FFMRptOffsetHz := RPT_DEFAULT_2M;
|
FController.FFMRptOffsetHz := RPT_DEFAULT_2M;
|
||||||
FController.FFMRptAutoActive := False;
|
FController.FFMRptAutoActive := False;
|
||||||
FController.FFMCTCSSAutoActive := False;
|
FController.FFMCTCSSAutoActive := False;
|
||||||
FWaterfallFrameInterval := 2;
|
|
||||||
FWaterfallFrameCounter := 0;
|
|
||||||
FController.FDevConnected := False;
|
FController.FDevConnected := False;
|
||||||
// Spectrum grid settings defaults
|
// Spectrum grid settings defaults
|
||||||
FController.FSpecRefLevel := -20.0;
|
FController.FSpecRefLevel := -20.0;
|
||||||
@@ -1010,8 +1007,6 @@ begin
|
|||||||
// OpenGLContextEx (QOpenGLWidget-бэкенд) работает и в нативном Wayland,
|
// OpenGLContextEx (QOpenGLWidget-бэкенд) работает и в нативном Wayland,
|
||||||
// старая X11-заглушка больше не нужна
|
// старая X11-заглушка больше не нужна
|
||||||
FUseOpenGLSpectrum := HasOpenGLSpectrumSwitch;
|
FUseOpenGLSpectrum := HasOpenGLSpectrumSwitch;
|
||||||
if FUseOpenGLSpectrum then
|
|
||||||
FWaterfallFrameInterval := 1;
|
|
||||||
if FUseOpenGLSpectrum then
|
if FUseOpenGLSpectrum then
|
||||||
FSpecView := TSpectrumViewOpenGL.Create
|
FSpecView := TSpectrumViewOpenGL.Create
|
||||||
else
|
else
|
||||||
@@ -1037,7 +1032,7 @@ begin
|
|||||||
FSpecView.SpecGridStep := FController.FSpecGridStep;
|
FSpecView.SpecGridStep := FController.FSpecGridStep;
|
||||||
// TX overlay: ширина окна = TX_SAMPLE_RATE из движка (фикс 192k)
|
// TX overlay: ширина окна = TX_SAMPLE_RATE из движка (фикс 192k)
|
||||||
FSpecView.TXSpanHz := WDSPEngine.TX_SAMPLE_RATE;
|
FSpecView.TXSpanHz := WDSPEngine.TX_SAMPLE_RATE;
|
||||||
FSpecView.WfFrameInterval := FWaterfallFrameInterval;
|
FSpecView.WfFrameInterval := EffectiveWfInterval;
|
||||||
FSpecView.PAMaxPower := FController.FPAMaxPower;
|
FSpecView.PAMaxPower := FController.FPAMaxPower;
|
||||||
FWidebandView := TWidebandView.Create;
|
FWidebandView := TWidebandView.Create;
|
||||||
|
|
||||||
@@ -1475,6 +1470,8 @@ begin
|
|||||||
FSpecView.WfAGCOffset := FController.FWfAGCOffset;
|
FSpecView.WfAGCOffset := FController.FWfAGCOffset;
|
||||||
FSpecView.WfPalette := FController.FWfPalette;
|
FSpecView.WfPalette := FController.FWfPalette;
|
||||||
FSpecView.WfGamma := FController.FWfGamma;
|
FSpecView.WfGamma := FController.FWfGamma;
|
||||||
|
FSpecView.WfFrameInterval := EffectiveWfInterval;
|
||||||
|
ApplySpecMSAA(FController.FSpecMSAA);
|
||||||
FSpecView.ResetWfAvgBuf;
|
FSpecView.ResetWfAvgBuf;
|
||||||
if Assigned(FWidebandView) then
|
if Assigned(FWidebandView) then
|
||||||
FWidebandView.SetFillSpectrum(FController.FWidebandFill);
|
FWidebandView.SetFillSpectrum(FController.FWidebandFill);
|
||||||
@@ -2211,6 +2208,7 @@ begin
|
|||||||
PbSpectrum := TOpenGLControl.Create(Self);
|
PbSpectrum := TOpenGLControl.Create(Self);
|
||||||
TOpenGLControl(PbSpectrum).AutoResizeViewport := False;
|
TOpenGLControl(PbSpectrum).AutoResizeViewport := False;
|
||||||
TOpenGLControl(PbSpectrum).DoubleBuffered := True;
|
TOpenGLControl(PbSpectrum).DoubleBuffered := True;
|
||||||
|
TOpenGLControl(PbSpectrum).MultiSampling := Max(1, FController.FSpecMSAA);
|
||||||
TOpenGLControl(PbSpectrum).OnPaint := FSpecView.PaintSpectrum;
|
TOpenGLControl(PbSpectrum).OnPaint := FSpecView.PaintSpectrum;
|
||||||
TOpenGLControl(PbSpectrum).OnMouseDown := PbSpectrumMouseDown;
|
TOpenGLControl(PbSpectrum).OnMouseDown := PbSpectrumMouseDown;
|
||||||
TOpenGLControl(PbSpectrum).OnDblClick := PbSpectrumDblClick;
|
TOpenGLControl(PbSpectrum).OnDblClick := PbSpectrumDblClick;
|
||||||
@@ -6400,16 +6398,12 @@ end;
|
|||||||
|
|
||||||
procedure TMainForm.OnWaterfallReady(const Pixels: array of Single;
|
procedure TMainForm.OnWaterfallReady(const Pixels: array of Single;
|
||||||
Count: Integer);
|
Count: Integer);
|
||||||
// Подписчик на FController.OnWaterfallData — рендер с децимацией скорости
|
// Подписчик на FController.OnWaterfallData. Каждый кадр уходит во view —
|
||||||
// прокрутки (FWaterfallFrameInterval). Web получает каждый кадр (в контроллере).
|
// замедление/накопление (max-hold) делает сам TWaterfallView по
|
||||||
|
// WfFrameInterval; рендер дёргаем только когда view собрал строку.
|
||||||
begin
|
begin
|
||||||
Inc(FWaterfallFrameCounter);
|
FSpecView.SetWaterfallData(Pixels, Count);
|
||||||
if FWaterfallFrameCounter >= Max(1, FWaterfallFrameInterval) then
|
if FSpecView.WaterfallDirty then FWaterfallDirty := True;
|
||||||
begin
|
|
||||||
FWaterfallFrameCounter := 0;
|
|
||||||
FSpecView.SetWaterfallData(Pixels, Count);
|
|
||||||
FWaterfallDirty := True;
|
|
||||||
end;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -6442,19 +6436,37 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.ApplyWfRenderParams(Palette: Integer; Gamma: Double;
|
procedure TMainForm.ApplyWfRenderParams(Palette: Integer; Gamma: Double;
|
||||||
ResLimit: Integer);
|
ResLimit, SpeedDiv: Integer);
|
||||||
begin
|
begin
|
||||||
FController.FWfPalette := Palette;
|
FController.FWfPalette := Palette;
|
||||||
FController.FWfGamma := Gamma;
|
FController.FWfGamma := Gamma;
|
||||||
FController.FDisplayPixels := ResLimit;
|
FController.FDisplayPixels := ResLimit;
|
||||||
|
FController.FWfSpeedDiv := SpeedDiv;
|
||||||
FSpecView.WfPalette := Palette;
|
FSpecView.WfPalette := Palette;
|
||||||
FSpecView.WfGamma := Gamma;
|
FSpecView.WfGamma := Gamma;
|
||||||
|
FSpecView.WfFrameInterval := EffectiveWfInterval;
|
||||||
if FController.FWDSPReady then
|
if FController.FWDSPReady then
|
||||||
FController.FDSPEngine.SetDisplayResLimit(ResLimit);
|
FController.FDSPEngine.SetDisplayResLimit(ResLimit);
|
||||||
FWaterfallDirty := True;
|
FWaterfallDirty := True;
|
||||||
FSpectrumDirty := True;
|
FSpectrumDirty := True;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.ApplySpecMSAA(Samples: Integer);
|
||||||
|
begin
|
||||||
|
Samples := EnsureRange(Samples, 1, 8);
|
||||||
|
FController.FSpecMSAA := Samples;
|
||||||
|
if FUseOpenGLSpectrum and (PbSpectrum is TOpenGLControl) and
|
||||||
|
(TOpenGLControl(PbSpectrum).MultiSampling <> Cardinal(Samples)) then
|
||||||
|
begin
|
||||||
|
// Смена MSAA пересоздаёт хэндл → старый GL-контекст (и все текстуры)
|
||||||
|
// умирает; кэши view нужно обнулить БЕЗ glDelete.
|
||||||
|
TOpenGLControl(PbSpectrum).MultiSampling := Samples;
|
||||||
|
if FSpecView is TSpectrumViewOpenGL then
|
||||||
|
TSpectrumViewOpenGL(FSpecView).ResetGLCache;
|
||||||
|
FSpectrumDirty := True;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TMainForm.ApplyGridParams(RefLevel, Range, GridStep: Double);
|
procedure TMainForm.ApplyGridParams(RefLevel, Range, GridStep: Double);
|
||||||
begin
|
begin
|
||||||
FController.FSpecRefLevel := RefLevel;
|
FController.FSpecRefLevel := RefLevel;
|
||||||
@@ -6565,19 +6577,21 @@ begin
|
|||||||
FDisplayFPS := FPS;
|
FDisplayFPS := FPS;
|
||||||
FController.FLoadedGlobal.DisplayFPS := FPS; // зеркало в persist-буфер контроллера
|
FController.FLoadedGlobal.DisplayFPS := FPS; // зеркало в persist-буфер контроллера
|
||||||
FSpectrumTimer.Interval := 1000 div FPS;
|
FSpectrumTimer.Interval := 1000 div FPS;
|
||||||
// CPU path keeps the historical every-other-frame waterfall update.
|
|
||||||
// OpenGL uploads only one texture row per frame, so it can match display FPS.
|
|
||||||
if FUseOpenGLSpectrum then
|
|
||||||
FWaterfallFrameInterval := 1
|
|
||||||
else
|
|
||||||
FWaterfallFrameInterval := 2;
|
|
||||||
FWaterfallFrameCounter := 0;
|
|
||||||
FWaterfallDirty := True;
|
FWaterfallDirty := True;
|
||||||
FSpecView.WfFrameInterval := FWaterfallFrameInterval;
|
FSpecView.WfFrameInterval := EffectiveWfInterval;
|
||||||
if FController.FWDSPReady then
|
if FController.FWDSPReady then
|
||||||
FController.FDSPEngine.SetDisplayFPS(FPS);
|
FController.FDSPEngine.SetDisplayFPS(FPS);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TMainForm.EffectiveWfInterval: Integer;
|
||||||
|
// Делитель скорости водопада: пользовательский Speed (max-hold накопление
|
||||||
|
// в TWaterfallView); CPU-путь дополнительно не быстрее полукадровой частоты
|
||||||
|
// (исторически — экономия на полной перерисовке bitmap).
|
||||||
|
begin
|
||||||
|
Result := Max(1, FController.FWfSpeedDiv);
|
||||||
|
if not FUseOpenGLSpectrum then Result := Max(Result, 2);
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TMainForm.ApplyFreqMhzDigits(Digits: Integer);
|
procedure TMainForm.ApplyFreqMhzDigits(Digits: Integer);
|
||||||
begin
|
begin
|
||||||
Digits := EnsureRange(Digits, 3, 5);
|
Digits := EnsureRange(Digits, 3, 5);
|
||||||
@@ -6623,6 +6637,7 @@ begin
|
|||||||
SF.OnDisplayChange := ApplyDisplayParams;
|
SF.OnDisplayChange := ApplyDisplayParams;
|
||||||
SF.OnWaterfallChange := ApplyWaterfallParams;
|
SF.OnWaterfallChange := ApplyWaterfallParams;
|
||||||
SF.OnWfRenderChange := ApplyWfRenderParams;
|
SF.OnWfRenderChange := ApplyWfRenderParams;
|
||||||
|
SF.OnSpecMSAAChange := ApplySpecMSAA;
|
||||||
SF.OnGridChange := ApplyGridParams;
|
SF.OnGridChange := ApplyGridParams;
|
||||||
SF.OnAudioDevChange := ApplyAudioDevice;
|
SF.OnAudioDevChange := ApplyAudioDevice;
|
||||||
SF.OnAudioInDevChange := ApplyAudioInputDevice;
|
SF.OnAudioInDevChange := ApplyAudioInputDevice;
|
||||||
@@ -6668,7 +6683,8 @@ begin
|
|||||||
FController.FSpectrumFill);
|
FController.FSpectrumFill);
|
||||||
SF.LoadWfAGCNF(FController.FWfAGCEnabled, FController.FWfNFEnabled);
|
SF.LoadWfAGCNF(FController.FWfAGCEnabled, FController.FWfNFEnabled);
|
||||||
SF.LoadWfRender(FController.FWfPalette, FController.FWfGamma,
|
SF.LoadWfRender(FController.FWfPalette, FController.FWfGamma,
|
||||||
FController.FDisplayPixels);
|
FController.FDisplayPixels, FController.FWfSpeedDiv);
|
||||||
|
SF.LoadSpecMSAA(FController.FSpecMSAA);
|
||||||
SF.LoadADCSettings(FController.FDitherEnabled, FController.FRandomEnabled);
|
SF.LoadADCSettings(FController.FDitherEnabled, FController.FRandomEnabled);
|
||||||
SF.LoadWebSettings(FWebEnabled, FWebPort, FWebBindAddr, FWebUser, FWebPass);
|
SF.LoadWebSettings(FWebEnabled, FWebPort, FWebBindAddr, FWebUser, FWebPass);
|
||||||
SF.LoadFPS(FDisplayFPS);
|
SF.LoadFPS(FDisplayFPS);
|
||||||
|
|||||||
@@ -284,6 +284,8 @@ type
|
|||||||
FWfPalette: Integer; // 0=Classic, 1=Inferno, 2=Turbo
|
FWfPalette: Integer; // 0=Classic, 1=Inferno, 2=Turbo
|
||||||
FWfGamma: Double; // контраст палитры водопада
|
FWfGamma: Double; // контраст палитры водопада
|
||||||
FDisplayPixels: Integer; // потолок точек анализатора
|
FDisplayPixels: Integer; // потолок точек анализатора
|
||||||
|
FWfSpeedDiv: Integer; // делитель скорости водопада (1/2/4/8, max-hold)
|
||||||
|
FSpecMSAA: Integer; // MSAA GL-спектра (1=off, 2, 4, 8)
|
||||||
FSpecRefLevel: Double;
|
FSpecRefLevel: Double;
|
||||||
FSpecRange: Double;
|
FSpecRange: Double;
|
||||||
FSpecGridStep: Double;
|
FSpecGridStep: Double;
|
||||||
@@ -692,6 +694,7 @@ begin
|
|||||||
FPlutoTxMaxAttDb := -10.0; // ADI-дефолт: линейный режим выходных каскадов
|
FPlutoTxMaxAttDb := -10.0; // ADI-дефолт: линейный режим выходных каскадов
|
||||||
FWfAGCEnabled := True; FDitherEnabled := True; FRandomEnabled := True;
|
FWfAGCEnabled := True; FDitherEnabled := True; FRandomEnabled := True;
|
||||||
FWfPalette := 0; FWfGamma := 1.0; FDisplayPixels := 4096;
|
FWfPalette := 0; FWfGamma := 1.0; FDisplayPixels := 4096;
|
||||||
|
FWfSpeedDiv := 1; FSpecMSAA := 4;
|
||||||
FFMStepOn := True;
|
FFMStepOn := True;
|
||||||
FLocalAudio := True; // GUI: звук на локальную карту; демон выключит явно
|
FLocalAudio := True; // GUI: звук на локальную карту; демон выключит явно
|
||||||
FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1;
|
FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1;
|
||||||
@@ -1916,6 +1919,8 @@ begin
|
|||||||
Result.WfPalette := FWfPalette;
|
Result.WfPalette := FWfPalette;
|
||||||
Result.WfGamma := FWfGamma;
|
Result.WfGamma := FWfGamma;
|
||||||
Result.DisplayPixels := FDisplayPixels;
|
Result.DisplayPixels := FDisplayPixels;
|
||||||
|
Result.WfSpeedDiv := FWfSpeedDiv;
|
||||||
|
Result.SpecMSAA := FSpecMSAA;
|
||||||
Result.SpecRefLevel := FSpecRefLevel;
|
Result.SpecRefLevel := FSpecRefLevel;
|
||||||
Result.SpecRange := FSpecRange;
|
Result.SpecRange := FSpecRange;
|
||||||
Result.SpecGridStep := FSpecGridStep;
|
Result.SpecGridStep := FSpecGridStep;
|
||||||
@@ -3505,6 +3510,8 @@ begin
|
|||||||
FWfPalette := EnsureRange(FLoadedGlobal.WfPalette, 0, 2);
|
FWfPalette := EnsureRange(FLoadedGlobal.WfPalette, 0, 2);
|
||||||
FWfGamma := EnsureRange(FLoadedGlobal.WfGamma, 0.3, 3.0);
|
FWfGamma := EnsureRange(FLoadedGlobal.WfGamma, 0.3, 3.0);
|
||||||
FDisplayPixels := EnsureRange(FLoadedGlobal.DisplayPixels, 64, 4096);
|
FDisplayPixels := EnsureRange(FLoadedGlobal.DisplayPixels, 64, 4096);
|
||||||
|
FWfSpeedDiv := EnsureRange(FLoadedGlobal.WfSpeedDiv, 1, 8);
|
||||||
|
FSpecMSAA := EnsureRange(FLoadedGlobal.SpecMSAA, 1, 8);
|
||||||
if FWDSPReady then FDSPEngine.SetDisplayResLimit(FDisplayPixels);
|
if FWDSPReady then FDSPEngine.SetDisplayResLimit(FDisplayPixels);
|
||||||
FShowSpectrum := FLoadedGlobal.ShowSpectrum;
|
FShowSpectrum := FLoadedGlobal.ShowSpectrum;
|
||||||
FShowWaterfall := FLoadedGlobal.ShowWaterfall;
|
FShowWaterfall := FLoadedGlobal.ShowWaterfall;
|
||||||
|
|||||||
@@ -253,6 +253,8 @@ type
|
|||||||
WfPalette: Integer; // 0=Classic, 1=Inferno, 2=Turbo
|
WfPalette: Integer; // 0=Classic, 1=Inferno, 2=Turbo
|
||||||
WfGamma: Double; // контраст палитры (0.3..3.0, 1.0 = линейно)
|
WfGamma: Double; // контраст палитры (0.3..3.0, 1.0 = линейно)
|
||||||
DisplayPixels: Integer; // потолок точек анализатора (1024/2048/4096)
|
DisplayPixels: Integer; // потолок точек анализатора (1024/2048/4096)
|
||||||
|
WfSpeedDiv: Integer; // делитель скорости водопада (1/2/4/8, max-hold)
|
||||||
|
SpecMSAA: Integer; // MSAA GL-спектра (1=off, 2, 4, 8)
|
||||||
SpecRefLevel: Double; // dBm reference (top of scale), e.g. -20
|
SpecRefLevel: Double; // dBm reference (top of scale), e.g. -20
|
||||||
SpecRange: Double; // dB range, e.g. 110 (bottom = ref - range)
|
SpecRange: Double; // dB range, e.g. 110 (bottom = ref - range)
|
||||||
SpecGridStep: Double; // grid step in dB, e.g. 10
|
SpecGridStep: Double; // grid step in dB, e.g. 10
|
||||||
@@ -449,6 +451,8 @@ begin
|
|||||||
G.WfPalette := 0; // Classic
|
G.WfPalette := 0; // Classic
|
||||||
G.WfGamma := 1.0;
|
G.WfGamma := 1.0;
|
||||||
G.DisplayPixels := 4096; // 1:1 до 4K-ширины
|
G.DisplayPixels := 4096; // 1:1 до 4K-ширины
|
||||||
|
G.WfSpeedDiv := 1; // полная скорость
|
||||||
|
G.SpecMSAA := 4; // сглаживание кривой спектра
|
||||||
G.SpecRefLevel := -20.0;
|
G.SpecRefLevel := -20.0;
|
||||||
G.SpecRange := 110.0;
|
G.SpecRange := 110.0;
|
||||||
G.SpecGridStep := 10.0;
|
G.SpecGridStep := 10.0;
|
||||||
@@ -675,6 +679,8 @@ begin
|
|||||||
G.WfPalette := JI(GObj,'wf_palette',0);
|
G.WfPalette := JI(GObj,'wf_palette',0);
|
||||||
G.WfGamma := JD(GObj,'wf_gamma',1.0);
|
G.WfGamma := JD(GObj,'wf_gamma',1.0);
|
||||||
G.DisplayPixels := JI(GObj,'display_pixels',4096);
|
G.DisplayPixels := JI(GObj,'display_pixels',4096);
|
||||||
|
G.WfSpeedDiv := JI(GObj,'wf_speed_div',1);
|
||||||
|
G.SpecMSAA := JI(GObj,'spec_msaa',4);
|
||||||
G.SpecRefLevel := JD(GObj,'spec_ref_level',-20.0);
|
G.SpecRefLevel := JD(GObj,'spec_ref_level',-20.0);
|
||||||
G.SpecRange := JD(GObj,'spec_range',110.0);
|
G.SpecRange := JD(GObj,'spec_range',110.0);
|
||||||
G.SpecGridStep := JD(GObj,'spec_grid_step',10.0);
|
G.SpecGridStep := JD(GObj,'spec_grid_step',10.0);
|
||||||
@@ -783,6 +789,8 @@ begin
|
|||||||
JW(O,'wf_palette',G.WfPalette);
|
JW(O,'wf_palette',G.WfPalette);
|
||||||
JW(O,'wf_gamma',G.WfGamma);
|
JW(O,'wf_gamma',G.WfGamma);
|
||||||
JW(O,'display_pixels',G.DisplayPixels);
|
JW(O,'display_pixels',G.DisplayPixels);
|
||||||
|
JW(O,'wf_speed_div',G.WfSpeedDiv);
|
||||||
|
JW(O,'spec_msaa',G.SpecMSAA);
|
||||||
JW(O,'spec_ref_level',G.SpecRefLevel);
|
JW(O,'spec_ref_level',G.SpecRefLevel);
|
||||||
JW(O,'spec_range',G.SpecRange);
|
JW(O,'spec_range',G.SpecRange);
|
||||||
JW(O,'spec_grid_step',G.SpecGridStep);
|
JW(O,'spec_grid_step',G.SpecGridStep);
|
||||||
|
|||||||
+60
-6
@@ -48,7 +48,8 @@ type
|
|||||||
TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode: Integer;
|
TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode: Integer;
|
||||||
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double) of object;
|
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double) of object;
|
||||||
TOnWfRenderChange = procedure(Palette: Integer; Gamma: Double;
|
TOnWfRenderChange = procedure(Palette: Integer; Gamma: Double;
|
||||||
ResLimit: Integer) of object;
|
ResLimit, SpeedDiv: Integer) of object;
|
||||||
|
TOnSpecMSAAChange = procedure(Samples: Integer) of object;
|
||||||
TOnGridParamChange = procedure(RefLevel, Range, GridStep: Double) of object;
|
TOnGridParamChange = procedure(RefLevel, Range, GridStep: Double) of object;
|
||||||
TOnAudioDevChange = procedure(DevIndex: Integer; const DevName: string) of object;
|
TOnAudioDevChange = procedure(DevIndex: Integer; const DevName: string) of object;
|
||||||
TOnAudioInDevChange = procedure(DevIndex: Integer; const DevName: string) of object;
|
TOnAudioInDevChange = procedure(DevIndex: Integer; const DevName: string) of object;
|
||||||
@@ -124,6 +125,8 @@ type
|
|||||||
FCmbWfPalette: TFlatComboBox;
|
FCmbWfPalette: TFlatComboBox;
|
||||||
FWfGamma: TFlatFloatSpinEdit;
|
FWfGamma: TFlatFloatSpinEdit;
|
||||||
FCmbWfRes: TFlatComboBox;
|
FCmbWfRes: TFlatComboBox;
|
||||||
|
FCmbWfSpeed: TFlatComboBox;
|
||||||
|
FCmbSpecMSAA: TFlatComboBox;
|
||||||
FEdRefLevel: TFlatEdit;
|
FEdRefLevel: TFlatEdit;
|
||||||
FEdRange: TFlatEdit;
|
FEdRange: TFlatEdit;
|
||||||
FCmbGridStep: TFlatComboBox;
|
FCmbGridStep: TFlatComboBox;
|
||||||
@@ -295,6 +298,7 @@ type
|
|||||||
FOnDisplayChange: TOnDisplayParamChange;
|
FOnDisplayChange: TOnDisplayParamChange;
|
||||||
FOnWaterfallChange: TOnWaterfallParamChange;
|
FOnWaterfallChange: TOnWaterfallParamChange;
|
||||||
FOnWfRenderChange: TOnWfRenderChange;
|
FOnWfRenderChange: TOnWfRenderChange;
|
||||||
|
FOnSpecMSAAChange: TOnSpecMSAAChange;
|
||||||
FOnGridChange: TOnGridParamChange;
|
FOnGridChange: TOnGridParamChange;
|
||||||
FOnAudioDevChange: TOnAudioDevChange;
|
FOnAudioDevChange: TOnAudioDevChange;
|
||||||
FOnAudioInDevChange: TOnAudioInDevChange;
|
FOnAudioInDevChange: TOnAudioInDevChange;
|
||||||
@@ -386,6 +390,7 @@ type
|
|||||||
procedure FireWaterfallChange;
|
procedure FireWaterfallChange;
|
||||||
procedure FireWfRenderChange;
|
procedure FireWfRenderChange;
|
||||||
procedure OnWfRenderCtlChange(Sender: TObject);
|
procedure OnWfRenderCtlChange(Sender: TObject);
|
||||||
|
procedure OnSpecMSAACtlChange(Sender: TObject);
|
||||||
procedure FireGridChange;
|
procedure FireGridChange;
|
||||||
|
|
||||||
function GetSpecAvgTimeMS: Double;
|
function GetSpecAvgTimeMS: Double;
|
||||||
@@ -431,7 +436,9 @@ type
|
|||||||
procedure LoadAlexSettings(const A: TAlexSettings; BoardType: Integer);
|
procedure LoadAlexSettings(const A: TAlexSettings; BoardType: Integer);
|
||||||
procedure LoadXvtrSettings(const X: TXvtrSettings);
|
procedure LoadXvtrSettings(const X: TXvtrSettings);
|
||||||
procedure LoadWfAGCNF(AGC, NF: Boolean);
|
procedure LoadWfAGCNF(AGC, NF: Boolean);
|
||||||
procedure LoadWfRender(Palette: Integer; Gamma: Double; ResLimit: Integer);
|
procedure LoadWfRender(Palette: Integer; Gamma: Double;
|
||||||
|
ResLimit, SpeedDiv: Integer);
|
||||||
|
procedure LoadSpecMSAA(Samples: Integer);
|
||||||
procedure LoadADCSettings(Dither, Random: Boolean);
|
procedure LoadADCSettings(Dither, Random: Boolean);
|
||||||
procedure LoadWebSettings(Enabled: Boolean; Port: Integer;
|
procedure LoadWebSettings(Enabled: Boolean; Port: Integer;
|
||||||
const BindAddr, User, Pass: string);
|
const BindAddr, User, Pass: string);
|
||||||
@@ -445,6 +452,7 @@ type
|
|||||||
property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange;
|
property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange;
|
||||||
property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange;
|
property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange;
|
||||||
property OnWfRenderChange: TOnWfRenderChange read FOnWfRenderChange write FOnWfRenderChange;
|
property OnWfRenderChange: TOnWfRenderChange read FOnWfRenderChange write FOnWfRenderChange;
|
||||||
|
property OnSpecMSAAChange: TOnSpecMSAAChange read FOnSpecMSAAChange write FOnSpecMSAAChange;
|
||||||
property OnGridChange: TOnGridParamChange read FOnGridChange write FOnGridChange;
|
property OnGridChange: TOnGridParamChange read FOnGridChange write FOnGridChange;
|
||||||
property OnAudioDevChange: TOnAudioDevChange read FOnAudioDevChange write FOnAudioDevChange;
|
property OnAudioDevChange: TOnAudioDevChange read FOnAudioDevChange write FOnAudioDevChange;
|
||||||
property OnAudioInDevChange: TOnAudioInDevChange read FOnAudioInDevChange write FOnAudioInDevChange;
|
property OnAudioInDevChange: TOnAudioInDevChange read FOnAudioInDevChange write FOnAudioInDevChange;
|
||||||
@@ -505,6 +513,10 @@ const
|
|||||||
WF_PALETTE_NAMES: array[0..2] of string = ('Classic', 'Inferno', 'Turbo');
|
WF_PALETTE_NAMES: array[0..2] of string = ('Classic', 'Inferno', 'Turbo');
|
||||||
DISPLAY_RES_VALUES: array[0..2] of Integer = (1024, 2048, 4096);
|
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');
|
DISPLAY_RES_NAMES: array[0..2] of string = ('1024 px', '2048 px', '4096 px');
|
||||||
|
WF_SPEED_VALUES: array[0..3] of Integer = (1, 2, 4, 8);
|
||||||
|
WF_SPEED_NAMES: array[0..3] of string = ('Full rate', '1/2', '1/4', '1/8');
|
||||||
|
MSAA_VALUES: array[0..3] of Integer = (1, 2, 4, 8);
|
||||||
|
MSAA_NAMES: array[0..3] of string = ('Off', '2x', '4x', '8x');
|
||||||
|
|
||||||
FPS_VALUES: array[0..12] of Integer = (5, 10, 15, 20, 25, 30, 40, 50, 60, 75, 80, 90, 100);
|
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',
|
FPS_NAMES: array[0..12] of string = ('5 fps','10 fps','15 fps','20 fps','25 fps','30 fps',
|
||||||
@@ -1039,6 +1051,13 @@ begin
|
|||||||
MakeLbl(Grp, 'Avg Time (ms):', PAD, R1 + 2 * STEP + 5, LW);
|
MakeLbl(Grp, 'Avg Time (ms):', PAD, R1 + 2 * STEP + 5, LW);
|
||||||
FSpecAvgTime := MkFloat(CX, R1 + 2 * STEP, SPW, 30.0, 0.0, 5000.0, 10.0, OnSpecAvgTimeChange);
|
FSpecAvgTime := MkFloat(CX, R1 + 2 * STEP, SPW, 30.0, 0.0, 5000.0, 10.0, OnSpecAvgTimeChange);
|
||||||
|
|
||||||
|
Grp := MakeGroupPanel(FPageSpectrum, 'Rendering', MARGIN, 602, 650, 128);
|
||||||
|
MakeLbl(Grp, 'Anti-aliasing:', PAD, R1 + 5, LW);
|
||||||
|
FCmbSpecMSAA := MakeCombo(Grp, CX, R1, 130, OnSpecMSAACtlChange);
|
||||||
|
for i := 0 to High(MSAA_NAMES) do FCmbSpecMSAA.Items.Add(MSAA_NAMES[i]);
|
||||||
|
FCmbSpecMSAA.ItemIndex := 2; // 4x
|
||||||
|
MakeLbl(Grp, '(GL spectrum curve smoothing, MSAA)', CX + 140, R1 + 5, 300);
|
||||||
|
|
||||||
Grp := MakeGroupPanel(FPageSpectrum, 'Grid', MARGIN, 416, 650, 170);
|
Grp := MakeGroupPanel(FPageSpectrum, 'Grid', MARGIN, 416, 650, 170);
|
||||||
MakeLbl(Grp, 'Reference level:', PAD, R1 + 5, LW);
|
MakeLbl(Grp, 'Reference level:', PAD, R1 + 5, LW);
|
||||||
FEdRefLevel := MkEdit(CX, R1, EDT_W, '-20', OnRefLevelChange);
|
FEdRefLevel := MkEdit(CX, R1, EDT_W, '-20', OnRefLevelChange);
|
||||||
@@ -1108,7 +1127,7 @@ begin
|
|||||||
FChkWfNF.Font.Size := 9;
|
FChkWfNF.Font.Size := 9;
|
||||||
FChkWfNF.OnChange := OnWfAGCNFChkChange;
|
FChkWfNF.OnChange := OnWfAGCNFChkChange;
|
||||||
|
|
||||||
Grp := MakeGroupPanel(FPageWaterfall, 'Rendering', MARGIN, 614, 650, 170);
|
Grp := MakeGroupPanel(FPageWaterfall, 'Rendering', MARGIN, 614, 650, 212);
|
||||||
MakeLbl(Grp, 'Palette:', PAD, R1 + 5, LW);
|
MakeLbl(Grp, 'Palette:', PAD, R1 + 5, LW);
|
||||||
FCmbWfPalette := MakeCombo(Grp, CX, R1, CMB_W, OnWfRenderCtlChange);
|
FCmbWfPalette := MakeCombo(Grp, CX, R1, CMB_W, OnWfRenderCtlChange);
|
||||||
for i := 0 to High(WF_PALETTE_NAMES) do FCmbWfPalette.Items.Add(WF_PALETTE_NAMES[i]);
|
for i := 0 to High(WF_PALETTE_NAMES) do FCmbWfPalette.Items.Add(WF_PALETTE_NAMES[i]);
|
||||||
@@ -1120,6 +1139,10 @@ begin
|
|||||||
FCmbWfRes := MakeCombo(Grp, CX, R1 + 2 * STEP, 130, OnWfRenderCtlChange);
|
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]);
|
for i := 0 to High(DISPLAY_RES_NAMES) do FCmbWfRes.Items.Add(DISPLAY_RES_NAMES[i]);
|
||||||
FCmbWfRes.ItemIndex := High(DISPLAY_RES_NAMES);
|
FCmbWfRes.ItemIndex := High(DISPLAY_RES_NAMES);
|
||||||
|
MakeLbl(Grp, 'Speed:', PAD, R1 + 3 * STEP + 5, LW);
|
||||||
|
FCmbWfSpeed := MakeCombo(Grp, CX, R1 + 3 * STEP, 130, OnWfRenderCtlChange);
|
||||||
|
for i := 0 to High(WF_SPEED_NAMES) do FCmbWfSpeed.Items.Add(WF_SPEED_NAMES[i]);
|
||||||
|
FCmbWfSpeed.ItemIndex := 0;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -2339,7 +2362,7 @@ end;
|
|||||||
|
|
||||||
procedure TSettingsForm.FireWfRenderChange;
|
procedure TSettingsForm.FireWfRenderChange;
|
||||||
var
|
var
|
||||||
ResV: Integer;
|
ResV, SpeedV: Integer;
|
||||||
begin
|
begin
|
||||||
if FLoading then Exit;
|
if FLoading then Exit;
|
||||||
if not Assigned(FOnWfRenderChange) then Exit;
|
if not Assigned(FOnWfRenderChange) then Exit;
|
||||||
@@ -2347,13 +2370,26 @@ begin
|
|||||||
ResV := DISPLAY_RES_VALUES[FCmbWfRes.ItemIndex]
|
ResV := DISPLAY_RES_VALUES[FCmbWfRes.ItemIndex]
|
||||||
else
|
else
|
||||||
ResV := DISPLAY_RES_VALUES[High(DISPLAY_RES_VALUES)];
|
ResV := DISPLAY_RES_VALUES[High(DISPLAY_RES_VALUES)];
|
||||||
FOnWfRenderChange(Max(0, FCmbWfPalette.ItemIndex), FWfGamma.Value, ResV);
|
if (FCmbWfSpeed.ItemIndex >= 0) and (FCmbWfSpeed.ItemIndex <= High(WF_SPEED_VALUES)) then
|
||||||
|
SpeedV := WF_SPEED_VALUES[FCmbWfSpeed.ItemIndex]
|
||||||
|
else
|
||||||
|
SpeedV := 1;
|
||||||
|
FOnWfRenderChange(Max(0, FCmbWfPalette.ItemIndex), FWfGamma.Value, ResV, SpeedV);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TSettingsForm.OnWfRenderCtlChange(Sender: TObject);
|
procedure TSettingsForm.OnWfRenderCtlChange(Sender: TObject);
|
||||||
begin FireWfRenderChange; end;
|
begin FireWfRenderChange; end;
|
||||||
|
|
||||||
procedure TSettingsForm.LoadWfRender(Palette: Integer; Gamma: Double; ResLimit: Integer);
|
procedure TSettingsForm.OnSpecMSAACtlChange(Sender: TObject);
|
||||||
|
begin
|
||||||
|
if FLoading then Exit;
|
||||||
|
if not Assigned(FOnSpecMSAAChange) then Exit;
|
||||||
|
if (FCmbSpecMSAA.ItemIndex >= 0) and (FCmbSpecMSAA.ItemIndex <= High(MSAA_VALUES)) then
|
||||||
|
FOnSpecMSAAChange(MSAA_VALUES[FCmbSpecMSAA.ItemIndex]);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TSettingsForm.LoadWfRender(Palette: Integer; Gamma: Double;
|
||||||
|
ResLimit, SpeedDiv: Integer);
|
||||||
var i: Integer;
|
var i: Integer;
|
||||||
begin
|
begin
|
||||||
FLoading := True;
|
FLoading := True;
|
||||||
@@ -2367,6 +2403,24 @@ begin
|
|||||||
for i := 0 to High(DISPLAY_RES_VALUES) do
|
for i := 0 to High(DISPLAY_RES_VALUES) do
|
||||||
if DISPLAY_RES_VALUES[i] = ResLimit then
|
if DISPLAY_RES_VALUES[i] = ResLimit then
|
||||||
begin FCmbWfRes.ItemIndex := i; Break; end;
|
begin FCmbWfRes.ItemIndex := i; Break; end;
|
||||||
|
FCmbWfSpeed.ItemIndex := 0;
|
||||||
|
for i := 0 to High(WF_SPEED_VALUES) do
|
||||||
|
if WF_SPEED_VALUES[i] = SpeedDiv then
|
||||||
|
begin FCmbWfSpeed.ItemIndex := i; Break; end;
|
||||||
|
finally
|
||||||
|
FLoading := False;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TSettingsForm.LoadSpecMSAA(Samples: Integer);
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
FLoading := True;
|
||||||
|
try
|
||||||
|
FCmbSpecMSAA.ItemIndex := 0;
|
||||||
|
for i := 0 to High(MSAA_VALUES) do
|
||||||
|
if MSAA_VALUES[i] = Samples then
|
||||||
|
begin FCmbSpecMSAA.ItemIndex := i; Break; end;
|
||||||
finally
|
finally
|
||||||
FLoading := False;
|
FLoading := False;
|
||||||
end;
|
end;
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ type
|
|||||||
procedure InvalidateGridCache; override;
|
procedure InvalidateGridCache; override;
|
||||||
procedure InvalidateOverlayCache; override;
|
procedure InvalidateOverlayCache; override;
|
||||||
procedure InvalidateVfoOverlay; override;
|
procedure InvalidateVfoOverlay; override;
|
||||||
|
procedure ResetGLCache;
|
||||||
procedure SetTheme(const T: TAppTheme); override;
|
procedure SetTheme(const T: TAppTheme); override;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -424,6 +425,40 @@ begin
|
|||||||
FOverlayDirty := True;
|
FOverlayDirty := True;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// Контекст пересоздан (смена MSAA → RecreateWnd): все GL-имена текстур
|
||||||
|
// стали недействительны. Обнуляем кэши БЕЗ glDelete — старого контекста
|
||||||
|
// уже нет, а в новом эти id могут достаться другим текстурам.
|
||||||
|
procedure TSpectrumViewOpenGL.ResetGLCache;
|
||||||
|
|
||||||
|
procedure Zap(var T: TGLTextureCache);
|
||||||
|
begin
|
||||||
|
T.Tex := 0; T.W := 0; T.H := 0; T.Dirty := True;
|
||||||
|
end;
|
||||||
|
|
||||||
|
var
|
||||||
|
i: Integer;
|
||||||
|
begin
|
||||||
|
Zap(FGridLabelTex);
|
||||||
|
Zap(FAGCLabelTex);
|
||||||
|
Zap(FAGCHangLabelTex);
|
||||||
|
Zap(FMarkerLabelTex);
|
||||||
|
Zap(FADCOverlayTex);
|
||||||
|
Zap(FSampleOverlayTex);
|
||||||
|
Zap(FVfoOverlayTex);
|
||||||
|
Zap(FBandOverlayTex);
|
||||||
|
for i := 0 to High(FSliceTex) do Zap(FSliceTex[i].Tex);
|
||||||
|
for i := 0 to High(FLetterTex) do Zap(FLetterTex[i]);
|
||||||
|
FLastAGCText := '';
|
||||||
|
FLastMarkerText := '';
|
||||||
|
FGridDirty := True;
|
||||||
|
FOverlayDirty := True;
|
||||||
|
FSampleOverlayDirty := True;
|
||||||
|
FVfoOverlayDirty := True;
|
||||||
|
FBandOverlayDirty := True;
|
||||||
|
FGLSpectrumW := 0;
|
||||||
|
FGLSpectrumH := 0;
|
||||||
|
end;
|
||||||
|
|
||||||
function TSpectrumViewOpenGL.FormatFreqGL(Hz: Double): string;
|
function TSpectrumViewOpenGL.FormatFreqGL(Hz: Double): string;
|
||||||
var Mhz: Int64; KHz, Rest: Integer;
|
var Mhz: Int64; KHz, Rest: Integer;
|
||||||
begin
|
begin
|
||||||
|
|||||||
+10
-1
@@ -307,7 +307,13 @@ procedure TWaterfallView.SetWaterfallData(const Pixels: array of Single; Count:
|
|||||||
var i, N: Integer;
|
var i, N: Integer;
|
||||||
begin
|
begin
|
||||||
N := Min(Count, WF_MAX_PIXELS);
|
N := Min(Count, WF_MAX_PIXELS);
|
||||||
for i := 0 to N - 1 do FWaterfallBuf[i] := Pixels[i];
|
// Замедление (WfFrameInterval>1) накапливает кадры max-hold'ом, а не
|
||||||
|
// выбрасывает их — короткие посылки (CW) не выпадают из строки.
|
||||||
|
if (FWfFrameCounter = 0) or (N <> FWaterfallBufCount) then
|
||||||
|
for i := 0 to N - 1 do FWaterfallBuf[i] := Pixels[i]
|
||||||
|
else
|
||||||
|
for i := 0 to N - 1 do
|
||||||
|
if Pixels[i] > FWaterfallBuf[i] then FWaterfallBuf[i] := Pixels[i];
|
||||||
FWaterfallBufCount := N;
|
FWaterfallBufCount := N;
|
||||||
Inc(FWfFrameCounter);
|
Inc(FWfFrameCounter);
|
||||||
if FWfFrameCounter >= Max(1, FWfFrameInterval) then
|
if FWfFrameCounter >= Max(1, FWfFrameInterval) then
|
||||||
@@ -548,6 +554,9 @@ begin
|
|||||||
finally
|
finally
|
||||||
FWfBitmap.EndUpdate(False);
|
FWfBitmap.EndUpdate(False);
|
||||||
end;
|
end;
|
||||||
|
// Строка потреблена — иначе фронтенд (OnWaterfallReady) считал бы кадр
|
||||||
|
// «несобранным» и дёргал перерисовку на каждый кадр анализатора.
|
||||||
|
FWaterfallDirty := False;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user