JTDX_CONTEST · CE3TSKTested during WW Digi 2026
v3.0.0-rc05 · derivative work of JTDX by UA3DJY/ES1JA and WSJT-X by K1JT
What classic JTDX does. What v3.0.0 adds. What each is worth.
Classic JTDX already tries many readings of one signal — realigning it, re-reading it at three coherent block lengths, decoding it with parts of the answer pinned. JTDX_contest v3.0.0 adds three more approaches on top: an ensemble that re-decodes the same audio through fixed perturbations, an alternate pass that re-attacks the residual with the opposite recipe, and a hint memory four periods deep instead of one. This page separates what was already there from what is new, and ends with the measured benefit of each.
classic JTDX marks stock behaviour · new in v3.0.0 marks the fork's own work
Part Ialready in classic JTDX
Stock JTDX already builds an ensemble of readings inside a single candidate: it realigns the signal, demodulates it at three coherent block lengths, and re-decodes it with known bits pinned. Everything in Part I ships in unmodified JTDX — it is the baseline the fork's new work is measured against.
An FT8 transmission is 79 symbols of 8-tone FSK at 6.25 baud — 12.64 seconds. Twenty-one of those symbols are sync: three Costas arrays of 7, at the start, middle and end. The other 58 carry a 174-bit LDPC codeword, which unpacks to 77 message bits plus a 14-bit CRC.
The decoder's job is to turn that waveform into 174 log-likelihood ratios —
one soft value per bit, signed by which way the bit leans and scaled by how sure the
evidence is — and hand them to an LDPC belief-propagation decoder. If BP fails, an
ordered-statistics decoder (osd174) gets a turn. The CRC is the referee: a
codeword either checks out or it doesn't, so the decoder can afford to guess repeatedly
and cheaply verify.
That last point is what makes an ensemble worth building. There is no penalty for a wrong attempt beyond the CPU it burns, and there is no way to know in advance which reading of a given signal is the right one. So the decoder computes several and tries them all.
Perturb the candidate's arrival time and frequency, rescore the Costas sync, keep the best.
The coarse candidate search hands ft8b a rough time and frequency
(xdt0, f10). Both are wrong — the time by up to a fraction
of a symbol, the frequency by a couple of hertz from drift, Doppler and dial error. Every
downstream metric is computed on symbol boundaries, so an alignment error smears energy
from each symbol into its neighbours and quietly destroys the bit metrics before the LDPC
decoder ever sees them.
So before demodulating, the decoder walks a small grid around the candidate and rescores the sync at each point — a two-stage search, coarse then fine:
Sync score across the alignment plane
Costas sync score for a simulated FT8 signal at −15 dB SNR, evaluated on a 49×49 grid of time and frequency offsets. The stronger the match, the stronger the amber. Crosshair marks the true offset; the ring marks where the search actually peaked.
The peak is narrow. That is the whole justification for the search: a candidate landing half a symbol late or three hertz high sits on the floor of that surface, not the ridge, and its bit metrics would be noise. The two slices on the right show the ridge profile through the true offset in each axis — note how much faster the score falls off in time than in frequency.
The code does this in two stages rather than one fine sweep, because a fine sweep over the whole plane is far too expensive to run on every candidate in a 2.5 kHz passband:
ft8b.f90:186) walks time first —
do idt=i0-8,i0+8, commented "Search over +/- one quarter symbol"
— then re-centres and walks frequency, do ifr=-5,5 at 0.5 Hz steps
for ±2.5 Hz, tightened to 0.25 Hz steps for ±1.25 Hz on a
signal already identified as the QSO partner.ft4_decode.f90:163) makes the two stages explicit as
do isync=1,2: pass one strides coarsely (idfstp=3,
ibstp=4), pass two re-searches at step 1 in a small window around whatever
pass one found. Its time search is additionally split across three segments
(do iseg=1,3).The refined frequency is then applied for real — twkfreq1 mixes
cd0 by -delfbest, so everything downstream works on a signal that
has been physically recentred, not merely measured.
Demodulate one symbol at a time, then two at a time, then three (FT8) or four (FT4) — and keep all three answers.
With the signal aligned, the decoder builds bit metrics. The obvious way is symbol by
symbol: for each of the 8 tones take the magnitude of that tone's bin, and for each bit
take the best hypothesis where the bit is 1 minus the best where it is 0. That is
nsym=1, and it is non-coherent — it throws away phase.
But FT8's tones sit at exact multiples of the baud rate, so the phase advance across a symbol is a whole number of cycles. Consecutive symbols are therefore not just continuous but phase-aligned, and you may legally add their complex bins before taking a magnitude. Doing so over two or three symbols at once buys coherent processing gain:
Why all three block lengths are computed, not just the longest
Hard-decision bit error rate against SNR in 2500 Hz, from a Monte Carlo
of the metric arithmetic in ft8b.f90:778–812. Left: a clean channel.
Right: the same signal with random-walk phase noise. Lower is better.
On the clean channel the ranking is what theory predicts: longer coherent blocks win, and at −18 dB the three-symbol metric is several times better than the one-symbol metric. If that were the whole story the decoder would simply always use the longest block.
Add phase noise and the ranking inverts. Coherent combining assumes the phase reference holds across the whole block; when it doesn't, summing bins before taking the magnitude adds noise vectors that no longer align, and the longer the block the worse the damage. At the right-hand end of the drifting panel the one-symbol metric is the best of the three by a wide margin — exactly the case where the clean panel says it is the worst.
Neither metric dominates, and the decoder cannot tell in advance which channel it is
looking at — ionospheric spread, drifting transmitters and rapid QSB all move a signal
between the two panels within a single 12-second transmission. So it computes all of them,
normalises each with normalizebmet, scales by 2.83, and stores
them side by side:
bmeta / bmetb / bmetc for
nsym = 1, 2, 3, plus a fourth — bmetd, the one-symbol
metric divided by the larger of its two competing hypotheses. That ratio is a
confidence-normalised variant rather than a new block length, and it is what
isubp2=4 reaches for.get_ft4_bitmetrics.f90:72 is blunter about it —
do nseq=1,3 !Try coherent sequences of 1, 2, and 4 symbols. Four rather than
three, because FT4 is 4-tone at 23.4 baud, so a four-symbol block spans a comparable
slice of time.These become llra, llrb, llrc, llrd
— four complete, independent readings of the same 174 bits.
Overwrite part of the codeword with saturated likelihoods taken from the QSO state, and let the decoder solve for the rest.
The first two mechanisms extract more from the signal. This one adds information that was never in the signal at all.
Mid-QSO, you know a great deal about what the other station is about to send. If you called
CE3TSK and they answered, their next transmission almost certainly begins with
your callsign and theirs. Those bits are not a guess to be recovered from a noisy waveform
— they are known. So the decoder writes them in directly, as log-likelihoods at
apmag = maxval(abs(llra))*1.01: fractionally louder than the loudest genuine
evidence, which pins them without letting them overflow the arithmetic.
How much of the message each AP hypothesis supplies
Of the 77 payload bits, the number pinned a priori for each
iaptype — from the table at ft8b.f90:857. The remainder is
what the LDPC decoder still has to recover from the signal.
iaptype of 4, 5 or 6 pins the entire payload: the decoder is no
longer reading a message, it is testing a single specific hypothesis — "is this
MyCall DxCall RR73?" — against the received energy, and the CRC answers yes or
no. That is the cheapest decode in the program and the one that pulls the deepest signals
out, which is why a QSO in progress decodes several dB below a cold CQ from a stranger.Which hypotheses get tried is not fixed: naptypes(nQSOProgress, ...) is a
lookup indexed by where you are in the exchange. Sitting on Tx6 calling CQ, the decoder
tries CQ-shaped and MyCall-shaped hypotheses. Once you are at Tx3 sending a signal report,
it starts testing for RRR, 73 and RR73 — the
replies that are actually plausible next.
The cost is a real one and worth naming: AP decoding can manufacture a decode that was never transmitted, because a pinned codeword plus enough noise can pass CRC by luck. The 14-bit CRC gives roughly a 1-in-16,000 false-accept rate per attempt, and the ensemble makes thousands of attempts per interval. That is the tension the whole design lives with — which is also why JTDX gates the more speculative AP passes behind its own thresholds rather than running every hypothesis on every candidate.
Stacked, these produce a lot of attempts per candidate. The structure in
ft8_decode.f90 and ft8b.f90 is three nested tiers:
npass is set from the user's FT8 cycles setting (1→3, 2→6,
3→9). Between passes the decoder subtracts signals it has already decoded
from the waveform, so a weak station sitting under a strong one becomes reachable on a
later pass. Subtraction is disabled on the final pass, where there is nothing left to
gain from it.
Selects which version of the downsampled signal the metrics are built from: the
straight one, the reversed-tone one (cs=csr), or a combination with a
stored copy of the same station from the previous interval (csold) —
averaging a signal against its own past. The number of subpasses depends on what the
candidate is: 5 for a CQ, 8 for a call to you, 11 for your active QSO partner.
1–4 are the plain decodes, one per metric: llra, llrb,
llrc, llrd. 5–31 are the AP passes, and each one picks a
metric too — the ensemble of block lengths is re-tried underneath every AP
hypothesis rather than chosen once.
Each attempt runs bpdecode174, falling back to
osd174. The CRC decides. The first attempt that passes wins, and everything
else is discarded — which is why the decoder can be this profligate without being
wrong: it is not combining opinions, it is running a search whose answers are individually
verifiable.
That is the sense in which it is an ensemble. Not a vote, and not a committee — a set of deliberately different readings of the same twelve seconds of audio, generated because the channel will not tell you which reading is correct, and cheap to test because the code carries its own proof.
Part IInew in JTDX_contest v3.0.0
Part I is all inside one candidate and inside one period: better
alignment, better metrics, better priors. That leaves two whole classes of failure untouched
— the ones decided before ft8b ever runs, by which candidates the sync
search kept and in what order they were subtracted; and the ones where the evidence needed
isn't in this period at all. The ensemble and the alternate pass attack the
first. The hint memory attacks the second.
Decode the same audio several times through fixed, reproducible perturbations — a delay, a dither, a tone shift — and keep the union.
The observation that started it: the old decoder's non-deterministic runs reached "over 100" messages on a test capture not because any single run was better, but because every run was a different sample of a decoder whose result flips on threshold decisions. The union of many samples climbs toward the real content of the band.
The FT8 chain is a sequence of hard decisions taken on soft data, and every one of them is a place where an input change smaller than the noise flips the outcome:
sync8 normalises the sync surface by its 40th percentile and keeps
only bins above syncmin (1.1–1.5). A candidate a hair under the line is
not a weak decode — it does not exist.ft8b rejects candidates on dozens of nsyncscore /
scoreratio rules before LDPC or OSD ever runs.Measured on one capture: a dither at 1 % of RMS moves a single run between 77 and 86 messages; a delay of one sample gives 83; a 0.2 Hz shift gives 79. Each is a fair sample, and different samples fail on different signals.
The insight is that this can be had deterministically. Instead of hoping a race condition reshuffles the decisions, perturb the input on purpose, by a fixed amount, with a seeded generator — then print only what is new. Same audio, same machine or another, same answer every time.
All three live in ft8ensemble.f90 (ens_perturb), and all three are
exactly reproducible on any machine:
dd8(1+n:)=dd8p(1:np-n), zeros in front. 128 samples is
10.7 ms, below the 40 ms DT step. The member's reported DT is corrected by
-n/12000 so the shift never leaks into avexdt.xorshift64* generator with a fixed seed and a fixed
Box–Muller step. Deliberately not Fortran's random_number,
whose sequence is compiler-defined and would break cross-machine determinism. The sweet
spot is 3–5 % of RMS (−30 to −26 dB); 30 % destroys the
weak tier.r2c over 180,000 samples, zero the negative half, c2r),
multiplied by exp(i2πΔf t), real part. About 10 ms.
Reported frequencies are corrected by -Δf.Members run as extra passes inside one period's decode, starting from the pristine
band rather than the merged one — a different subtraction history is the entire point.
They deliberately never become "the previous period": feeding perturbed copies into
ft8sd averaging produced 95 messages in testing but manufactured one that was
not on the air, because averaging near-identical copies is coherent gain that does not exist
in reality. That path was closed on purpose.
What each member adds — FT8, 12 threads
Cumulative unique messages as members are added to the exhaustive recipe, against a 104-message validated reference set for the capture. Members are ordered by measured gain per second, so a tighter time budget keeps the best ones.
Two things are worth reading off that pair. The gain is monotone and front-loaded — the first two members are cheap SWL-5 recipes and take 85 to 95 for 2.4 s — and the cost is close to linear, so the member count is a genuine time dial rather than a cliff. The three-member setting (98 messages, 7.6 s) is the contest target; five is for SWL and monitoring.
The safety result matters more than the count: all 16 messages gained across 100+ perturbed runs are in the validated reference set. Zero false decodes. On a sparse second capture nothing is lost and two plausible CQs are added.
Why those particular messages were missing is instructive. Three callers at 797–809 Hz appear only on the frequency-shifted members — they sit on a bin edge of the 3.125 Hz sync grid. Two pairs 6–20 Hz apart decode only in one subtraction order. None of that is reachable by tuning thresholds; it needs a different sample.
Once the normal passes have subtracted what they can, re-decode the residual with the opposite recipe.
JTDX's two decoding families are good at different things. The SWL-cycle family works on averaged data with a wide DT window; the plain-cycles + OSD family works on raw data and searches deeper. Classic JTDX makes you pick one and run it everywhere.
The alternate pass runs the other one afterwards. It is a third kind of pass in
decoder.f90's pass list — SWL off, 7 plain cycles, OSD order 2, on
the main grid, operating on the band with every earlier pass's subtractions merged in and the
duplicate arrays carried over. Parameter lft8altpass, file mode -X,
GUI Decode → FT8 decoding → alternate-approach pass.
Two results came out of the sweep that settled its shape. The pass wants 7 or 9 cycles — at 5 it gains nothing. And the older half-slice offset pass actively hurts once the alternate pass exists: its extra subtractions and duplicates take away exactly what the alternate pass would otherwise have found. Adding a good idea meant removing one that had been earning its place until then.
Alternate-pass recipe sweep — FT8, 12 threads, deterministic
Every row is 3-of-3 identical across runs and entirely inside the reference set. "Weakest" counts how many of the capture's 17 hardest signals were recovered.
Keep four same-parity message lists instead of one, so a single missed period no longer breaks a station's chain.
This one extends the mechanism from section 03 across time. AP
decoding pins bits you know from the QSO you are in; the ^ hint decoder pins
bits you know because you already decoded that station, at that frequency and DT, in
an earlier period. It is the same 77-bit a-priori machinery, sourced from memory rather than
from the sequencer.
Classic JTDX keeps exactly one list per parity — evencopy and
oddcopy — and will try a stored message only if it was decoded in the
immediately previous same-parity period, 30 seconds back. That is a brittle
chain. One missed period from a stronger neighbour's residue, from QSB, or from a subtraction
artefact — all routine at −24 dB — and the station drops out of memory
entirely, then stays lost until some blind pass happens to catch it cold again.
The case that prompted it: CT7AUT decoded cleanly at 03:10:00 and 03:10:30,
was missed once, and then went unheard for five minutes — while still calling
the whole time.
v3.0.0 keeps four lists per parity, reaching two minutes back. The older lists are
searched only when the newer ones hold nothing for a candidate, and a hint decode retires its
entry in the list it came from, so a station cannot be re-hinted from stale memory once it has
been heard. The lastrxmsg fallback looks back the same way. Default is
NHINTDEFAULT in ft8_mod1.f90; JTDX_HINT_DEPTH=1..8
overrides it, and 1 is exactly JTDX's behaviour. No params-block change —
jtdxjt9 carries it alone.
Depth sweep — one-hour on-air set, replayed at every depth
Gain over depth 1 (JTDX's behaviour) for two recipes. Zero suspicious texts at every depth in both. The light preset has no depth-6 run.
Where the gained messages come from, by how many same-parity periods back the station was last decoded (classical recipe, depth 6). The yield falls steadily but has not reached zero at eight periods — depth 4 is a deliberate stop, not a ceiling.
Two findings decided the default. First, 86–89 % of the gained messages are chains continuing through a missed slot — the station had been decoded one to three same-parity periods earlier. And only 15–45 % of the gains are messages that some depth-1 preset decoded cold in the same period; the rest are signals no blind pass reaches, which is what a 77-bit-known correlation buys you.
Second, the cost is a real one and it is why the memory stops at four rather than eight. The
failure mode is the stale hint decode: a candidate turns up on a re-used frequency at
the old DT, the old message shares most of its bits with the new one, the correlation passes,
and the wrong text prints — EA5KB KS4OT EM83 ^ where the air carried
EA4EQ KS4OT EM83. That runs 1–2 an hour at depth 4 and 3 an hour at depth
8. Against +320–425 real decodes it is about 0.05 % of the output, but it grows
with depth while the yield shrinks, and eight periods was judged the wrong side of that
trade.
One consequence worth noting: with a two-minute memory the lists must not outlive the dial. A band or mode change now empties the hint lists at every depth, along with the call and DT lists — JTDX kept those across the change, but they are keyed by audio frequency and DT and mean nothing under another dial.
FT4 had no hint pass at all in JTDX. It now has the same four-period memory in
ft4_mod1.f90: a message decoded in one of the last four same-parity
7.5 second periods at a candidate's frequency and DT gets one extra pass with all 77
message bits fixed, through the RR73 a-priori type-6 mechanism — so the acceptance test
is JTDX's own — after every ordinary pass has failed in all three DT segments. On the
240-period hour it is the single largest contributor in the whole stack: +185 messages,
+11.9 %, with 196 of the 1 737 decodes carrying the ^ marker.
Depth 8 was measured there too and rejected for the same reason: +25 but −9, starting
to resurrect stale messages.
It also compounds with the ensemble, which is the nicest interaction in the release: a member's decode is stored in the hint lists and re-tried in later periods, so hinted decodes rise from 196 to 251 once the ensemble is running.
The ensemble was transplanted whole into FT4, where ens_perturb runs on the
aligned candidate rather than the whole band: member 1 is dither, members 2–3 shift
±0.6 Hz, 4–5 delay by ±1 sample, 6 draws different noise. FT4 is
threaded on the same anchored 417 Hz grid as FT8 — each slice subtracting into its
own copy of the audio, four inherited data races fixed on the way — so the same audio
gives the same message set at any thread count of two or more, and the whole ensemble fits
inside FT4’s 1.36 s reply deadline where it used to overrun it. Measured over a
240-period on-air hour at 12 threads on an idle machine:
What came after the ensemble is FT4’s own: a TX background switched on exactly as FT8’s, which re-works the retained period while you transmit — the members the reply phase did not run, deep OSD, the alternate pass, a residual pass, lower thresholds — with its own effort, sensitivity and clock, until 0.5 s before the window ends; six presets in FT8’s tiers, each label carrying its measured cost; budget auto for the max effort tier, which learns what a member costs on this band and admits as many as fit inside 1.3 s; and the virtual candidate at the QSO frequency, worth about +1 dB on the one reply a QSO is waiting for. On the night hour the default recipe decodes 1 737 messages, recommended 1 881 with a 0.10 s reply time, max effort 1 888 at 0.59 s — against 1 525 for JTDX’s own FT4 decoder with its ALLCALL7.TXT lookup off on the same audio, +24 % (1 356 and +39 % as shipped).
FT4 ensemble scaling — 240 on-air periods, 12 threads
Unique messages against member count, with the alternate pass added on top at the right. Baseline is the fork's default, which already carries the depth-4 hint memory.
Which perturbation kind carried the +111 gained messages at 6 members in the 2026-08-30 run (94 gained on the 2026-09-05 rerun). The frequency shifts do over half the work — but note what this is not: a finer coarse frequency step in the sync search gains nothing at all. The candidates were already being found; what a member changes is the demodulation of one the decoder had given up on.
Three fixed choices that mapped to 3, 6 or 9 internal passes become a direct setting of 3 to 9.
Everything in Parts I and II is spent inside a pass, and the number of
passes was the one dial the operator could not really turn. Classic JTDX offers
actionDecFT8cycles1..3 — three menu entries — and
ft8_decode.f90 translates them:
if(nft8cycles.eq.1) then; npass=3
else if(nft8cycles.eq.2) then; npass=6
else if(nft8cycles.eq.3) then; npass=9
In the fork the indirection is gone — the setting is the pass count, and the
menu carries actionDecFT8cycles3 through 9:
if(nft8cycles.ge.3 .and. nft8cycles.le.9) then; npass=nft8cycles
The same change applies to the SWL cycle count (nft8swlcycles), and file mode
exposes it as -C. Why the old steps were multiples of three is visible in the
pass loop: when low-threshold or SWL mode is on, the sync threshold walks a
three-pass cycle — 1.225, then 1.5, then 1.1 — and repeats. Classic JTDX
only let you buy whole sweeps of that cycle.
Where you are allowed to stop
The nine passes with the sync threshold each one runs at under low-threshold or SWL mode. Classic JTDX could stop only at the end of a complete three-pass sweep.
This one is a control change rather than a new decoding idea, but it is what makes the rest of Part II usable: with the ensemble, the alternate pass and a deeper hint memory all competing for the same seconds, a pass count that moves in steps of three is far too coarse a budget. Every preset in the release — and every row in the benchmark tables above — is built on being able to ask for 5 or 7 passes rather than 6 or 9.
A note on credit: the fork's own README lists the finer-grained cycle system among the upstream JTDX updates it inherited (September 9, 2025 — "cycle values three times the previous ones, with intermediate steps"). It is new relative to the JTDX v2.2.159 baseline this page compares against throughout, which is why it is in Part II — but unlike the ensemble, the alternate pass and the hint memory, it is not the fork's own work.
Part IIInew in JTDX_contest v3.0.0
Everything in Part II costs seconds, and a contest decoder has very few to spend. Two things had to change before any of it could ship: the decoder had to become fast and deterministic, and it had to stop throwing away four fifths of every period.
Fifteen data races fixed — nine inherited in FT8, four in FT4, two of the fork’s own — the hot loops rewritten, and the same answer on every thread count.
The determinism work is not housekeeping here — it is what makes the ensemble a coherent idea at all. "Decode it several times and take the union" is only an instrument if the decoder returns the same answer when you do not change the input. On a racing decoder it is just re-rolling dice, which is precisely what the old non-deterministic runs were doing when they reached "over 100" by accident.
Nine races were found and fixed in the stock FT8 decoder (four more in FT4, and two the fork introduced in its own pipeline and fixed). The worst was the shared audio buffer
dd8: threads were subtracting decoded signals out of each other's audio,
so what any one thread saw depended on who got there first. It is now threadprivate with
copyin. The others were the ft8_downsample C++ cache, the
osd174_91 first_osd initialisation, the four2a FFTW
plan-cache publish, the gen_ft8wave tables, and the decoded-message store's
counter.
On top of that the fixed 12-slice band grid was decoupled from the thread count, with
threads picking slices up dynamically and per-slice state indexed by slice. The result is the
property the rest of the release is built on: the same message set at every thread count
of 2 or more. A 2-core and a 16-core machine decode identically; -j changes
only the time.
Then the speed work, which bought the seconds Part II spends:
sync8 window-sum tabulation. The 17-bin window sum was being
recomputed for every DT lag although it depends only on the bin and the symbol. Tabulated
once, using the identical expression — bit for bit exact — and the lag loops
halved.sync8; the old cap
was silently dropping real signals on a crowded band.Split the decoder in two: a fast phase that makes the reply deadline, and a background phase that uses the time the process used to spend idle.
No other FT8 program does this. WSJT-X and JTDX decode once per period and then sleep until the next one; MSHV’s three-stage decoding splits the work inside the receive period, not past its end. In all three the time after the decode is simply lost.
Two facts set the shape of this. The decoder process idled about 12 of every 15
seconds — in TX and RX periods alike. And the timing is tight at exactly the wrong
moment: the decode does not start until 14.1 s into the period
(m_hsymStop=49), because that is when the audio it needs has finally arrived, and
the auto-sequencer wants its reply decision roughly one second later — just as
the next period, the one you transmit in, begins. Anything slower than that was already too
late to change what you send, however good it was.
So the decode is now two phases. The RX phase is deliberately cheap — SWL-5,
78 messages in 1.2 s — and prints <DecodeFinished> exactly as
before, so the sequencer sees what it needs on time. The TX background then keeps
working on the retained band while you transmit, running units the RX phase could never
have afforded:
Each unit is preceded by a check of the GUI's .lock file — removed means
the next decode is wanted, and the unit stops within a pass rather than finishing
— and, in auto mode, of the remaining time budget. Only new messages print, carrying
the period's UTC, and a <BackgroundFinished> line closes with the count.
Background decodes are marked | in the output, and a hint made in the background
┼.
Where a 15-second FT8 period goes
Drawn to scale on a shared clock. The decode fires at half-symbol 49 — 14.1 s into the period whose audio it decodes — so the burst straddles the boundary and finishes inside the next period, the one you transmit in. The window shown is the tail of that previous period plus the full period after it.
One instrumented result is worth repeating because it redirected the design. The RX phase's
LDPC failures are almost entirely noise — 2438 of 2475 of them fail the sync gates
again when retried. So the members are not rescuing candidates that ft8b
fumbled; their gains come from the candidate and subtraction stage, which is why the residual
pass turned out to be the unit that finds what the members miss.
The three parts of Part II and the pipeline are not independent. They compound, and the thing that couples them is the hint memory from section 06.
Background decodes feed the next period's priors
Each period's decodes become the next period's hypotheses; the four-period memory keeps the chain alive across a missed slot.
Reading it around: the TX background decodes stations the RX phase never reached, those decodes join the period's message set, the period's message set becomes the hint memory, and the next RX phase — the cheap 1.2-second one that has to make the reply deadline — starts with a larger set of 77-bit hypotheses it can test almost for free. A station the background found while you were transmitting can be decoded by the fast phase in the next period, in time to answer it.
The measured evidence for the loop closing is on the FT4 side, where hinted decodes rise from 196 to 251 once the ensemble members run — the members' extra decodes are stored and re-tried in later periods. On air over 1,323 common periods the pipeline build printed 9,201 decodes against the old decoder's 8,540, and 212 of its 722 unique finds came from the TX background.
One part of the state is deliberately kept out of the loop. The stored even/odd
signals used for ft8sd averaging, the CQ/MyCall signal stores and the DT
statistics are exported from pass 1 only (lsecondpass in
ft8_decode.f90). A perturbed or background-derived signal must never become "the
previous period" — averaging near-identical copies is coherent gain that does not exist
on the air, and when it was tried it manufactured a decode that was never transmitted. The
messages feed forward; the waveforms do not.
The benefit
Everything below is measured on recorded audio with a script that re-runs it (the measurement harness is not part of the published source; it will be available for download) — not modelled, and not a single lucky run. The engine table is FT8 and every headline figure carries its mode; the cards after them label each measurement FT8 or FT4, because the two modes were measured on different captures at different thread counts and their percentages are not comparable.
The headline FT8 comparison is 240 consecutive on-air 15-second periods (2026-08-27, 20 m, 100–3100 Hz, 12 threads of a Ryzen 7 5800H), the same recorded audio put through every engine at its own best settings:
The recorded hour, the FT4 hour and the wideband benchmark capture are published as 16-bit WAV, with a script that replays them through the standalone decoder and reprints this table on any machine: ce3tsk.com/download/wav.
And every approach separated out, each against the classic JTDX behaviour it replaces or adds to, per mode:
Every approach against the classic JTDX recipe, by mode
What classic JTDX does, what v3.0.0 does instead, and the measured delta with its cost. Ordered by how much each contributes to the total.
The ensemble and the alternate pass are complementary rather than competing, which is the case for shipping both. The alternate pass changes how the residual is decoded once; the ensemble changes what the decoder sees and does it several times. On FT4 they stack almost additively — 6 members give +94, and the alternate pass on top takes that to +105.
Neither is free, and neither runs at reply time everywhere: the ensemble is gated by thread count, and in FT4 the presets put the members and the alternate pass into the TX background, where they cost nothing at reply time. The whole design of the pipeline — a fast RX phase that decides the reply, then a TX background that keeps working through your own transmission — now exists in both modes, so that the extra seconds come out of CPU that used to sit idle rather than out of the reply deadline.