use alloc::vec;
use alloc::vec::Vec;
use num_complex::Complex32;
use super::super::engine::llr::{
compute_llr_fast, compute_llr_partial, descramble_info, symbol_spectra, sync_quality,
};
use super::super::engine::pipeline::{DecodeResult, osd_escalation_gates};
use super::super::engine::protocol::{BpPooledFec, FecOpts, MessageCodec, Protocol};
use super::super::engine::sync::SyncCandidate;
pub struct RungMajorCandidate {
pub cand: SyncCandidate,
pub cd0: Vec<Complex32>,
pub refined_freq_hz: f32,
pub i0: i32,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Schedule {
RungMajor,
PhaseSplit,
}
pub fn decode_rung_major<P>(
candidates: &[RungMajorCandidate],
skip_llrc: bool,
) -> Vec<Option<DecodeResult>>
where
P: Protocol,
P::Fec: BpPooledFec,
{
decode_rung_major_timed::<P>(candidates, skip_llrc, false, &[0], None, 12_000.0).0
}
pub fn decode_rung_major_timed<P>(
candidates: &[RungMajorCandidate],
skip_llrc: bool,
skip_osd: bool,
offsets: &[i32],
clock: Option<fn() -> i64>,
sample_rate_hz: f32,
) -> (Vec<Option<DecodeResult>>, Option<Vec<Vec<i64>>>)
where
P: Protocol,
P::Fec: BpPooledFec,
{
decode_scheduled::<P>(
candidates,
skip_llrc,
skip_osd,
offsets,
clock,
Schedule::RungMajor,
None,
sample_rate_hz,
)
}
pub fn decode_phase_split_timed<P>(
candidates: &[RungMajorCandidate],
skip_llrc: bool,
skip_osd: bool,
offsets: &[i32],
clock: Option<fn() -> i64>,
budget_ok: Option<fn() -> bool>,
sample_rate_hz: f32,
) -> (Vec<Option<DecodeResult>>, Option<Vec<Vec<i64>>>)
where
P: Protocol,
P::Fec: BpPooledFec,
{
decode_scheduled::<P>(
candidates,
skip_llrc,
skip_osd,
offsets,
clock,
Schedule::PhaseSplit,
budget_ok,
sample_rate_hz,
)
}
fn decode_scheduled<P>(
candidates: &[RungMajorCandidate],
skip_llrc: bool,
skip_osd: bool,
offsets: &[i32],
clock: Option<fn() -> i64>,
schedule: Schedule,
budget_ok: Option<fn() -> bool>,
sample_rate_hz: f32,
) -> (Vec<Option<DecodeResult>>, Option<Vec<Vec<i64>>>)
where
P: Protocol,
P::Fec: BpPooledFec,
{
assert!(
!offsets.is_empty(),
"decode_rung_major_timed: offsets must be non-empty"
);
let nsym_mid = P::LLR_NSYM_MID
.expect("decode_rung_major is FST4-specific: P::LLR_NSYM_MID must be set (see module doc)")
as usize;
let nsym_max = P::LLR_NSYM_MAX as usize;
let ds_rate = sample_rate_hz / P::NDOWN as f32;
let tx_start = P::TX_START_OFFSET_S;
let (osd_attempt_min, osd_depth3_min) = osd_escalation_gates::<P>();
let verify_info = Some(<P::Msg as MessageCodec>::verify_info as fn(&[u8]) -> bool);
const N_SUBSTAGES: usize = 5;
let n_stages = offsets.len() * N_SUBSTAGES;
#[derive(Default)]
struct OffsetState {
computed: bool,
cs: Vec<crate::engine::scalar::Cmplx<f32>>,
nsync: u32,
llra: Vec<f32>,
llrb: Vec<f32>,
llre: Vec<f32>,
llrc: Vec<f32>,
}
struct CandState<'a> {
input: &'a RungMajorCandidate,
cd0: Vec<Complex32>,
offsets: Vec<OffsetState>,
decoded: Option<DecodeResult>,
}
fn build_result<P: Protocol>(
mut r: crate::engine::protocol::FecResult,
input: &RungMajorCandidate,
i0: i32,
ds_rate: f32,
tx_start: f32,
pass: u8,
) -> DecodeResult {
descramble_info::<P>(&mut r.info);
DecodeResult {
info: r.info.into_boxed_slice(),
freq_hz: input.refined_freq_hz,
dt_sec: (i0 as f32) / ds_rate - tx_start,
hard_errors: r.hard_errors,
sync_score: input.cand.score,
pass,
sync_cv: 0.0,
snr_db: crate::fst4::baseline::fst4_ddc_snr_db::<P>(&input.cd0, ds_rate)
.unwrap_or(f32::NAN),
}
}
const SYNC_Q_MIN: u32 = 16;
let mut states: Vec<CandState> = candidates
.iter()
.map(|input| {
let df_hz = input.refined_freq_hz - input.cand.freq_hz;
let cd0 = super::super::engine::sync2d::freq_shift_cd0(&input.cd0, df_hz, ds_rate);
CandState {
input,
cd0,
offsets: (0..offsets.len()).map(|_| OffsetState::default()).collect(),
decoded: None,
}
})
.collect();
let bp_opts = |osd_depth: u32| -> FecOpts<'static> {
FecOpts {
bp_max_iter: 30,
osd_depth,
ap_mask: None,
verify_info,
..FecOpts::default()
}
};
let mut per_stage_us: Vec<Vec<i64>> = vec![vec![0i64; n_stages]; candidates.len()];
let run_stage = |st: &mut CandState,
idx: usize,
offset_idx: usize,
substage: usize,
per_stage: &mut [Vec<i64>]| {
if substage == 3 && skip_llrc {
return;
}
if st.decoded.is_some() {
return;
}
let stage = offset_idx * N_SUBSTAGES + substage;
let ioffset = offsets[offset_idx];
let t0 = clock.map(|c| c());
let off = &mut st.offsets[offset_idx];
if !off.computed {
let i0 = st.input.i0 + ioffset;
off.cs = symbol_spectra::<P>(&st.cd0, i0);
off.nsync = sync_quality::<P>(&off.cs);
off.computed = true;
}
if off.nsync <= SYNC_Q_MIN {
if let (Some(clk), Some(t0)) = (clock, t0) {
per_stage[idx][stage] = clk() - t0;
}
return;
}
let fec = P::Fec::default();
let mut bp_scratch = <P::Fec as BpPooledFec>::Scratch::default();
let i0 = st.input.i0 + ioffset;
match substage {
0 => {
off.llra = compute_llr_fast::<P, f32>(&off.cs).llra;
if let Some(r) = fec.decode_soft_pooled(&off.llra, &bp_opts(0), &mut bp_scratch) {
st.decoded = Some(build_result::<P>(r, st.input, i0, ds_rate, tx_start, 0));
}
}
1 => {
off.llrb = compute_llr_partial::<P, f32, f32>(&off.cs, 2);
if let Some(r) = fec.decode_soft_pooled(&off.llrb, &bp_opts(0), &mut bp_scratch) {
st.decoded = Some(build_result::<P>(r, st.input, i0, ds_rate, tx_start, 1));
}
}
2 => {
off.llre = compute_llr_partial::<P, f32, f32>(&off.cs, nsym_mid);
if let Some(r) = fec.decode_soft_pooled(&off.llre, &bp_opts(0), &mut bp_scratch) {
st.decoded = Some(build_result::<P>(r, st.input, i0, ds_rate, tx_start, 6));
}
}
3 => {
off.llrc = compute_llr_partial::<P, f32, f32>(&off.cs, nsym_max);
if let Some(r) = fec.decode_soft_pooled(&off.llrc, &bp_opts(0), &mut bp_scratch) {
st.decoded = Some(build_result::<P>(r, st.input, i0, ds_rate, tx_start, 2));
}
}
4 => {
if skip_osd || off.nsync < osd_attempt_min {
if let (Some(clk), Some(t0)) = (clock, t0) {
per_stage[idx][stage] = clk() - t0;
}
return;
}
let osd_depth: u32 = if off.nsync >= osd_depth3_min { 3 } else { 2 };
let mut variants: Vec<&Vec<f32>> = vec![&off.llra, &off.llrb, &off.llre];
if !skip_llrc {
variants.push(&off.llrc);
}
let mut hit: Option<crate::engine::protocol::FecResult> = None;
for llr in variants {
if let Some(r) =
fec.decode_soft_pooled(llr, &bp_opts(osd_depth), &mut bp_scratch)
{
hit = Some(r);
break;
}
}
if let Some(r) = hit {
let pass = if skip_llrc { 4 } else { 5 };
st.decoded = Some(build_result::<P>(r, st.input, i0, ds_rate, tx_start, pass));
}
}
_ => unreachable!(),
}
if let (Some(clk), Some(t0)) = (clock, t0) {
per_stage[idx][stage] = clk() - t0;
}
};
match schedule {
Schedule::RungMajor => {
for stage in 0..n_stages {
let offset_idx = stage / N_SUBSTAGES;
let substage = stage % N_SUBSTAGES;
for (idx, st) in states.iter_mut().enumerate() {
run_stage(st, idx, offset_idx, substage, &mut per_stage_us);
}
}
}
Schedule::PhaseSplit => {
for (idx, st) in states.iter_mut().enumerate() {
run_stage(st, idx, 0, 0, &mut per_stage_us);
}
let mut order: Vec<usize> = (0..states.len()).collect();
order.sort_by(|&a, &b| states[b].offsets[0].nsync.cmp(&states[a].offsets[0].nsync));
'budget: for offset_idx in 0..offsets.len() {
for &idx in &order {
for substage in 0..N_SUBSTAGES {
if offset_idx == 0 && substage == 0 {
continue; }
if budget_ok.is_some_and(|ok| !ok()) {
break 'budget;
}
let st = &mut states[idx];
run_stage(st, idx, offset_idx, substage, &mut per_stage_us);
if st.decoded.is_some() {
break;
}
}
}
}
}
}
let results = states.into_iter().map(|st| st.decoded).collect();
(results, clock.map(|_| per_stage_us))
}