diff --git a/build-gpudemod-dll.ps1 b/build-gpudemod-dll.ps1 index 47f021c..7108a5c 100644 --- a/build-gpudemod-dll.ps1 +++ b/build-gpudemod-dll.ps1 @@ -17,7 +17,7 @@ if (!(Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir | Out-Nul Remove-Item $dll,$lib,$exp -Force -ErrorAction SilentlyContinue $cmd = @" -call "$vcvars" && "$nvcc" -shared "$src" -o "$dll" -Xcompiler "/MD" -arch=sm_75 -gencode arch=compute_75,code=sm_75 -gencode arch=compute_80,code=sm_80 -gencode arch=compute_86,code=sm_86 -gencode arch=compute_89,code=sm_89 -gencode arch=compute_90,code=sm_90 +call "$vcvars" && "$nvcc" -shared "$src" -o "$dll" -cudart=shared -Xcompiler "/MD" -arch=sm_75 -gencode arch=compute_75,code=sm_75 -gencode arch=compute_80,code=sm_80 -gencode arch=compute_86,code=sm_86 -gencode arch=compute_89,code=sm_89 -gencode arch=compute_90,code=sm_90 "@ Write-Host 'Building gpudemod CUDA DLL...' -ForegroundColor Cyan diff --git a/build-sdrplay.ps1 b/build-sdrplay.ps1 index a884df9..d20c89a 100644 --- a/build-sdrplay.ps1 +++ b/build-sdrplay.ps1 @@ -40,7 +40,11 @@ $dllDst = Join-Path $PSScriptRoot 'gpudemod_kernels.dll' $dllSrc = $dllCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1 if ($dllSrc) { if ((Resolve-Path $dllSrc).Path -ne (Resolve-Path (Split-Path $dllDst -Parent)).Path + '\gpudemod_kernels.dll') { - Copy-Item $dllSrc $dllDst -Force + try { + Copy-Item $dllSrc $dllDst -Force + } catch { + Write-Host "WARNING: could not refresh runtime DLL at $dllDst ($($_.Exception.Message))" -ForegroundColor Yellow + } } Write-Host "CUDA DLL ready at $dllDst" -ForegroundColor Green } else { diff --git a/cmd/sdrd/dsp_loop.go b/cmd/sdrd/dsp_loop.go index 76a5989..8d5e9ad 100644 --- a/cmd/sdrd/dsp_loop.go +++ b/cmd/sdrd/dsp_loop.go @@ -195,7 +195,7 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det * if rec != nil && len(finished) > 0 { evCopy := make([]detector.Event, len(finished)) copy(evCopy, finished) - go rec.OnEvents(evCopy) + rec.OnEvents(evCopy) } var debugInfo *SpectrumDebug if len(thresholds) > 0 || len(signals) > 0 || noiseFloor != 0 { diff --git a/internal/demod/gpudemod/gpudemod_windows.go b/internal/demod/gpudemod/gpudemod_windows.go index 5e17e0b..feda60d 100644 --- a/internal/demod/gpudemod/gpudemod_windows.go +++ b/internal/demod/gpudemod/gpudemod_windows.go @@ -405,7 +405,6 @@ func (e *Engine) Demod(iq []complex64, offsetHz float64, bw float64, mode DemodT return nil, 0, errors.New("sample count exceeds engine capacity") } - _ = fmt.Sprintf("%s:%0.3f", phaseStatus(), offsetHz) shifted, ok := e.tryCUDAFreqShift(iq, offsetHz) e.lastShiftUsedGPU = ok && ValidateFreqShift(iq, e.sampleRate, offsetHz, shifted, 1e-3) if !e.lastShiftUsedGPU { diff --git a/internal/recorder/recorder.go b/internal/recorder/recorder.go index 6b7b8d4..411f2f8 100644 --- a/internal/recorder/recorder.go +++ b/internal/recorder/recorder.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "strings" + "sync" "time" "sdr-visual-suite/internal/demod/gpudemod" @@ -29,6 +30,7 @@ type Policy struct { } type Manager struct { + mu sync.RWMutex policy Policy ring *Ring sampleRate int @@ -53,12 +55,14 @@ func New(sampleRate int, blockSize int, policy Policy, centerHz float64, decodeC } func (m *Manager) Update(sampleRate int, blockSize int, policy Policy, centerHz float64, decodeCommands map[string]string) { + m.mu.Lock() + defer m.mu.Unlock() m.policy = policy m.sampleRate = sampleRate m.blockSize = blockSize m.centerHz = centerHz m.decodeCommands = decodeCommands - m.initGPUDemod(sampleRate, blockSize) + m.initGPUDemodLocked(sampleRate, blockSize) if m.ring == nil { m.ring = NewRing(sampleRate, blockSize, policy.RingSeconds) return @@ -67,14 +71,26 @@ func (m *Manager) Update(sampleRate int, blockSize int, policy Policy, centerHz } func (m *Manager) Ingest(t0 time.Time, samples []complex64) { - if m == nil || m.ring == nil { + if m == nil { + return + } + m.mu.RLock() + ring := m.ring + m.mu.RUnlock() + if ring == nil { return } - m.ring.Push(t0, samples) + ring.Push(t0, samples) } func (m *Manager) OnEvents(events []detector.Event) { - if m == nil || !m.policy.Enabled || len(events) == 0 { + if m == nil || len(events) == 0 { + return + } + m.mu.RLock() + enabled := m.policy.Enabled + m.mu.RUnlock() + if !enabled { return } for _, ev := range events { @@ -93,6 +109,12 @@ func (m *Manager) worker() { } func (m *Manager) initGPUDemod(sampleRate int, blockSize int) { + m.mu.Lock() + defer m.mu.Unlock() + m.initGPUDemodLocked(sampleRate, blockSize) +} + +func (m *Manager) initGPUDemodLocked(sampleRate int, blockSize int) { if m.gpuDemod != nil { m.gpuDemod.Close() m.gpuDemod = nil @@ -108,22 +130,29 @@ func (m *Manager) initGPUDemod(sampleRate int, blockSize int) { } func (m *Manager) recordEvent(ev detector.Event) error { - if !m.policy.Enabled { + m.mu.RLock() + policy := m.policy + ring := m.ring + sampleRate := m.sampleRate + centerHz := m.centerHz + m.mu.RUnlock() + + if !policy.Enabled { return nil } - if ev.SNRDb < m.policy.MinSNRDb { + if ev.SNRDb < policy.MinSNRDb { return nil } dur := ev.End.Sub(ev.Start) - if m.policy.MinDuration > 0 && dur < m.policy.MinDuration { + if policy.MinDuration > 0 && dur < policy.MinDuration { return nil } - if m.policy.MaxDuration > 0 && dur > m.policy.MaxDuration { + if policy.MaxDuration > 0 && dur > policy.MaxDuration { return nil } - if len(m.policy.ClassFilter) > 0 && ev.Class != nil { + if len(policy.ClassFilter) > 0 && ev.Class != nil { match := false - for _, c := range m.policy.ClassFilter { + for _, c := range policy.ClassFilter { if strings.EqualFold(c, string(ev.Class.ModType)) { match = true break @@ -133,46 +162,49 @@ func (m *Manager) recordEvent(ev detector.Event) error { return nil } } - if !m.policy.RecordIQ && !m.policy.RecordAudio { + if !policy.RecordIQ && !policy.RecordAudio { return nil } - start := ev.Start.Add(-time.Duration(m.policy.PrerollMs) * time.Millisecond) + start := ev.Start.Add(-time.Duration(policy.PrerollMs) * time.Millisecond) end := ev.End if start.After(end) { return errors.New("invalid event window") } + if ring == nil { + return errors.New("no ring buffer") + } - segment := m.ring.Slice(start, end) + segment := ring.Slice(start, end) if len(segment) == 0 { return errors.New("no iq in ring") } - dir := filepath.Join(m.policy.OutputDir, fmt.Sprintf("%s_%0.fHz_evt%d", ev.Start.Format("2006-01-02T15-04-05"), ev.CenterHz, ev.ID)) + dir := filepath.Join(policy.OutputDir, fmt.Sprintf("%s_%0.fHz_evt%d", ev.Start.Format("2006-01-02T15-04-05"), ev.CenterHz, ev.ID)) if err := os.MkdirAll(dir, 0o755); err != nil { return err } files := map[string]any{} var iqPath string - if m.policy.RecordIQ { + if policy.RecordIQ { iqPath = filepath.Join(dir, "signal.cf32") if err := writeCF32(iqPath, segment); err != nil { return err } files["iq"] = "signal.cf32" files["iq_format"] = "cf32" - files["iq_sample_rate"] = m.sampleRate + files["iq_sample_rate"] = sampleRate } - // Optional demod + audio - if m.policy.RecordAudio && m.policy.AutoDemod && ev.Class != nil { + if policy.RecordAudio && policy.AutoDemod && ev.Class != nil { if err := m.demodAndWrite(dir, ev, segment, files); err != nil { return err } } - if m.policy.AutoDecode && iqPath != "" && ev.Class != nil { - m.runDecodeIfConfigured(string(ev.Class.ModType), iqPath, m.sampleRate, files, dir) + if policy.AutoDecode && iqPath != "" && ev.Class != nil { + m.runDecodeIfConfigured(string(ev.Class.ModType), iqPath, sampleRate, files, dir) } - return writeMeta(dir, ev, m.sampleRate, files) + _ = centerHz + return writeMeta(dir, ev, sampleRate, files) }