Skip to content

Instantly share code, notes, and snippets.

@nomi-san
Last active September 5, 2026 06:01
Show Gist options
  • Select an option

  • Save nomi-san/f6e590f877881cce7f79ad3046370411 to your computer and use it in GitHub Desktop.

Select an option

Save nomi-san/f6e590f877881cce7f79ad3046370411 to your computer and use it in GitHub Desktop.
PlayStation Remote Play: Protocol Research

Takion: reliability, loss recovery, and congestion control

An analysis of how the PS4/PS5 Remote Play protocol (Takion) handles packet loss and rate adaptation, as implemented by this client, with a comparison against Parsec's BUD protocol and Sunshine/Moonlight.

Scope and method. Everything below was read from the client implementation in lib/. The console side is closed source, so where the protocol carries a value the console chooses, that is stated explicitly rather than guessed at. Line references are to the tree at the time of writing.

One-line summary. Takion is architecturally a Sunshine-class design: unreliable media protected by Reed-Solomon FEC, no NACK, no retransmission of video, and no congestion control of any kind on the client. It is not a Parsec-class design.

On the FEC ratio specifically: the client cannot set it, directly or indirectly by any supported means (§7). The console chooses k and m per frame, and captured traffic shows it choosing well — 33-40% redundancy is typical under load, with 50% observed (§2.4). The poor behaviour on lossy links is therefore not an under-provisioned FEC ratio. It is block FEC meeting burst loss with a block of five to ten packets emitted inside one millisecond, plus a rate loop that reports at 5 Hz into closed firmware (§9).


1. Layer map

Takion multiplexes every channel onto one UDP socket. A datagram is dispatched on the low nibble of byte 0 (lib/src/takion.c:62-71):

Type Channel Reliability
0 CONTROL — SCTP-shaped: INIT/INIT_ACK/COOKIE/COOKIE_ACK/DATA/DATA_ACK reliable, cumulative ACK + fixed RTO
1 FEEDBACK_HISTORY — discrete input events (button edges, touch) unreliable + repetition
2 VIDEO unreliable, FEC only
3 AUDIO unreliable, repetition only
4 HANDSHAKE setup
5 CONGESTION — client to console loss report unreliable, fire and forget
6 FEEDBACK_STATE — full analog controller snapshot unreliable, idempotent
8 CLIENT_INFO setup

Application-level control messages are nanopb-encoded protobuf (lib/protobuf/takion.proto) carried inside type 0.


2. Video: Reed-Solomon FEC, no acknowledgement

2.1 The code

lib/src/fec.c is Cauchy Reed-Solomon over GF(2^8) (CHIAKI_FEC_WORDSIZE 8) via jerasure. Each frame is split into k source units plus m parity units; any k of the k+m units reconstructs the frame by erasure decode.

k and m are chosen by the console, per frame. The client reads them straight out of the AV packet header and never negotiates or requests a ratio:

/* lib/src/frameprocessor.c:76-77 */
frame_processor->units_source_expected = packet->units_in_frame_total - packet->units_in_frame_fec;
frame_processor->units_fec_expected    = packet->units_in_frame_fec;

The frame is flushed as soon as enough units have arrived, rather than waiting for the tail (chiaki_frame_processor_flush_possible, lib/include/chiaki/frameprocessor.h:67):

return frame_processor->units_source_received + frame_processor->units_fec_received
    >= frame_processor->units_source_expected;

UNIT_SLOTS_MAX is 512, raised from 256 because high bitrates overflowed it.

2.2 There is no ACK and no retransmission for video

Nothing in the AV path acknowledges anything. There is no NACK, no selective repeat, no retransmit timer, no per-packet sequence tracking used for repair. Video sequence numbers exist only for reordering and loss accounting.

2.3 The loss-recovery ladder

  1. FEC decode. Covers up to m lost units per frame.
  2. Reference-frame remap. If a P-slice names a reference frame that was never successfully decoded, the client rewrites ref_idx in the bitstream to point at the nearest surviving reference (lib/src/videoreceiver.c:271-290, via chiaki_bitstream_slice_set_reference_frame). This is decoder-side concealment added by chiaki-ng, not part of the protocol.
  3. Give up on the frame. Send CORRUPTFRAME(start, end) on the reliable control channel (lib/src/streamconnection.c:1263), drop the frame, and count it lost.
  4. Optionally send IDRREQUEST (lib/src/streamconnection.c:1285) and skip P-frames until an I-slice arrives.

Step 4 is opt-in and off by default. enable_idr_on_fec_failure defaults to false (gui/include/settings.h:302). Redundant requests are further suppressed by a waiting_for_idr latch. This is a deliberately conservative default and the right one for a lossy link: at 60 fps, a client-driven IDR per failed frame is worse than the artifact it repairs.

Step 3 is unconditional, and it is not free. CORRUPTFRAME is always sent, both on FEC failure and on a detected frame-index gap (lib/src/videoreceiver.c:165). What the console does with it is console-side policy — very likely a keyframe. So IDR-driven flicker can still occur with enable_idr_on_fec_failure off; the client simply is not the one asking. There is no client-side way to suppress CORRUPTFRAME.

2.4 What the console actually chooses: observed k and m

The FEC decode test vectors in test/fec_test_cases.inl are 64 real captured frames. Their (k, m) distribution is direct evidence of the console's protection policy:

k,m Count Redundancy m/(k+m)
3,2 31 40.0%
4,2 20 33.3%
5,2 4 28.6%
8,1 2 11.1%
7,2 1 22.2%
6,4 1 40.0%
6,2 1 25.0%
6,1 1 14.3%
5,3 1 37.5%
4,4 1 50.0%
4,3 1 42.9%

Plus one live-parsed packet in test/takion.c:44-45 at k=7, m=1 (12.5%).

The console does adapt m, and it goes high — the mode is 40% redundancy and the ceiling observed is 50%. This is at or above what Sunshine users configure by hand. Any claim that Remote Play performs poorly on lossy links because it under-provisions FEC is not supported by this data.

Caveat on the sample. These are FEC decode vectors, captured precisely at moments when units were missing. They are biased toward the lossy regime — which is the regime of interest here — and are not representative of steady state on a clean link, where m is presumably lower. Note also that k is small throughout (3 to 8), which matters a great deal for burst loss; see §9.

2.5 Reordering

Video and audio each get a reorder queue with a 16 ms head-of-line timeout (TAKION_AV_REORDER_TIMEOUT_US 16000, ~1 frame at 60 fps, lib/src/takion.c:942-1025). On timeout it skips directly to the first buffered packet, so a loss burst pays one timeout rather than one per missing packet. This absorbs Wi-Fi jitter without stalling on genuine loss.


3. Audio and input: repetition, not erasure coding

Audio is not Reed-Solomon. An audio AV packet carries source_units_count fresh Opus frames followed by fec_units_count copies of previous frames (lib/src/audioreceiver.c:119-134):

frame_index = packet->frame_index - fec_units_count + fec_index;

Plain repetition with an offset. Losing one packet is covered by the next. Note the packed header: units_in_frame_fec holds unit size in the high byte, FEC count in bits 4-7, source count in bits 0-3 (lib/include/chiaki/takion.h:55-57).

Input uses two complementary schemes:

  • FEEDBACK_STATE (type 6): a full analog snapshot sent every 8-200 ms on change (FEEDBACK_STATE_TIMEOUT_MIN_MS 8 / MAX_MS 200). Unreliable and never retransmitted — state is idempotent, so a lost packet self-heals on the next one.
  • FEEDBACK_HISTORY (type 1): discrete events, which are not idempotent. Each packet re-includes the last 4 events (FEEDBACK_HISTORY_RESEND_EVENT_COUNT 0x4, lib/src/feedbacksender.c:189), so four consecutive losses are needed to drop a button press.

4. The reliable channel exists, is weak, and never carries media

DATA_ACK is SCTP-shaped: cumulative TSN, a_rwnd, gap_ack_blocks_count, dup_tsns_count.

The gap ack blocks are parsed only to validate the packet length, then discarded. Compare lib/src/takion.c:1430, which reads gap_ack_blocks_count, with :1447, which passes only cumulative_seq_num to the send buffer. The result is cumulative-only acknowledgement: no SACK, no fast retransmit, no duplicate-ACK signal.

Retransmission (lib/src/takionsendbuffer.c:12-15):

Constant Value
TAKION_DATA_RESEND_TIMEOUT_MS 200, flat
TAKION_DATA_RESEND_TRIES_MAX 25 (~5 s, then the packet is faked as acked and dropped)
TAKION_SEND_BUFFER_SIZE 16 slots

No RTT estimate, no exponential backoff, no window management. a_rwnd is the constant 0x19000 and is also used verbatim as the socket SO_RCVBUF.

This channel carries protobuf control messages, rumble, trigger effects, and pad info — never audio or video. It rarely bites. But CORRUPTFRAME and IDRREQUEST ride it, which is exactly the path under stress at the moment the link is already dropping packets.


5. Congestion control: there is none on the client

lib/src/congestioncontrol.c is 81 lines and this is the entire algorithm:

every 200 ms (CONGESTION_CONTROL_INTERVAL_MS):
    (received, lost) = chiaki_packet_stats_get(reset = true)
    loss = lost / (received + lost)
    if loss > packet_loss_max:
        clamp:  lost = total * packet_loss_max;  received = total - lost
    send CONGESTION { word_0, received:u16, lost:u16 }

No congestion window. No send rate. No AIMD. No pacing. No in-session RTT measurement. A grep of lib/ for cwnd, AIMD, pacing, BBR, GCC, or rate control returns nothing.

The control loop lives entirely inside closed console firmware. The client is a passive reporter with a 5 Hz update cadence and a two-uint16 signal.

5.1 Loss accounting is at least exact

lib/src/packetstats.c merges two sources:

  • Video: generation-based. Per frame, expected = k + m and received is the actual count, so loss is measured, not inferred (chiaki_frame_processor_report_packet_stats).
  • Audio: sequence-based. seq_max - seq_min - seq_received.

Both are summed into one ratio (lib/src/packetstats.c:70-79).

5.2 What comes back, and what the client does with it

The console sends CONNECTIONQUALITY, carrying target_bitrate, upstream_bitrate, upstream_loss, disable_upstream_audio, rtt, and loss. The client logs it at verbose level and nothing else (lib/src/streamconnection.c:693-706); the only side effect is snapshotting measured bitrate for the on-screen overlay.

The PACKETLOSS protobuf message type is defined and never sent.

5.3 What the console actually adapts

  • Resolution. "adaptiveStreamMode": "resize" in the launch spec (lib/src/launchspec.c:77), plus adaptive_stream_index on every AV packet selecting among the profiles advertised in STREAMINFO. The client just follows the switch and re-emits the matching video header.
  • FEC ratio, per frame, as covered in §2.1.
  • Bitrate, presumably, reported back as target_bitrate.

All three are console-side decisions driven by the 5 Hz loss report.

5.4 The packet_loss_max knob

packet_loss_max caps how much loss the client is willing to admit to. Set it low and the client under-reports, so the console keeps quality high and you eat the artifacts; set it to 1.0 and the console sees the truth and downshifts sooner.

Upstream chiaki-ng defaults to 0.05. Commit 79a8e611 ("honest congestion control") changed the default in this tree to 1.0. That commit is a tacit acknowledgement that the console's loop is the only loop, and that misreporting is the only client-side lever over it.

Exposed in the GUI as an integer percentage (QmlSettings::setPacketLossReportedMax).


6. Path measurement is one-shot

Senkusha runs before the stream starts (lib/src/senkusha.c:252-272) and never again:

  1. RTT — 10 pings, arithmetic mean (SENKUSHA_PING_COUNT_DEFAULT 10).
  2. MTU — binary search 576-1454, inbound then outbound, 3 retries, timeout 5 * rtt clamped to 5-500 ms.

That is the whole of it. No re-probing on path change, Wi-Fi roam, or congestion.

There is no bandwidth probe. SenkushaBandwidthCommand is defined in the protobuf (lib/protobuf/takion.proto:444) and the client never sends it. The bwKbpsSent field in the launch spec is simply the user's configured bitrate (lib/src/streamconnection.c:1009, from the per-preset defaults at lib/src/session.c:100-120) — a static declaration, not a measurement.


7. What the client can and cannot control about FEC

7.1 Direct control: none

There is no protocol message for requesting a FEC ratio, and no client-side knob that sets one. k and m arrive in the header of every AV packet (units_in_frame_total, units_in_frame_fec) and are strictly read-only inputs to the decoder. chiaki_fec_encode() exists in lib/src/fec.c but has no caller anywhere in the library — it is there for symmetry and for the test suite. The console decides, per frame, and the client complies.

7.2 Indirect levers, ranked by leverage

Lever Where Current value Assessment
bwLoss in the launch spec lib/src/launchspec.c:25 hardcoded 0.001000 The client's declared expected path loss, sent once at session setup. It is a string literal in the format template — ChiakiLaunchSpec has no field for it, and nothing measured ever reaches it. Most promising untested lever.
packet_loss_max lib/src/congestioncontrol.c:28 1.0 in this tree, 0.05 upstream Caps reported loss. Raising reported loss should push the console toward more parity and lower rate. Mediated by console policy, updated at 5 Hz.
bwKbpsSent (bitrate) lib/src/streamconnection.c:1009 user setting Fewer bytes per frame means smaller k. For a given m that is higher fractional redundancy. Crude, but it is the one lever with a reliable mechanism behind it.
mtu Senkusha probe measured, 576-1454 Sets unit size, hence k for a given frame size. Not directly settable; only reachable by making the probe fail.
rtt Senkusha probe measured Reported once. Effect on FEC policy unknown.
minFps, minBandwidth, score lib/src/launchspec.c:20,32-33 hardcoded 30, 0, 10 Constrain the console's adaptation envelope. Untested.

bwLoss deserves a specific experiment. The client currently tells every console, on every session, that the path loses 0.1% of packets — on a link that may be losing 10%. If the console seeds its initial FEC ratio from this field, then raising it is a one-line change with a directly observable outcome: log units_in_frame_fec on the first hundred frames of a session at 0.001000 and again at, say, 0.050000, and compare. This is a hypothesis, not a finding: the console is closed source and the field's actual effect is unverified.

7.3 Two quirks worth knowing

m = 0 is silently rewritten to m = 1 (lib/src/frameprocessor.c:77-79):

frame_processor->units_fec_expected = packet->units_in_frame_fec;
if(frame_processor->units_fec_expected < 1)
    frame_processor->units_fec_expected = 1;

Frame assembly is unaffected — the flush loop iterates only over units_source_expected, and flush_possible still fires at k units. But chiaki_frame_processor_report_packet_stats() computes expected = units_source_expected + units_fec_expected, so a frame the console sent with no parity is accounted as one unit short. If the console ever sends m = 0, the client reports a phantom loss on every such frame — at k = 7 that is a fabricated 12.5% loss floor fed straight into the congestion report, which would drive the console to degrade a link that is in fact clean. Whether m = 0 ever occurs on the wire is unverified; every observed capture has m >= 1.

congestion_control_interval from STREAMINFO is ignored. The console can specify the reporting interval (lib/protobuf/takion.proto:270), and stream_connection_takion_data_expect_streaminfo() decodes only audio_header and resolution from that message. The field is never read. The client hardcodes CONGESTION_CONTROL_INTERVAL_MS 200 regardless of what the console asked for.


8. Comparison

Takion (Remote Play) BUD (Parsec) Sunshine / Moonlight
Video transport unreliable + RS FEC fully reliable ordered ring unreliable + RS FEC
ACK for video none group ack, 10 ms / gap floors none
NACK none yes (0x10), fast retransmit with per-fragment latch no
Video retransmission none yes, clamp(2*(retries+1)*srtt, 50-1000 ms) + 30 ms no
Loss recovery FEC, then ref-frame remap, then IDR request retransmit until delivered, furthest-slot stall escape FEC, then IDR request
Reliable channel cumulative ACK only, flat 200 ms RTO, 16 slots per-fragment RTO, NACK, 100-fragment cap control stream
Congestion control none on client; 5 Hz loss report to closed firmware local AIMD, MD x0.7, AI +step*0.15 per 30 clean ticks, actuates encoder bitrate none in protocol
Controller cadence 200 ms timer per encoded frame (~16.7 ms at 60 fps) n/a
Congestion signal loss ratio only stale-fragment ratio, partly delay-derived (srtt*1.1 + 20 ms) n/a
RTT tracking once, pre-stream continuous EWMA (0.9 / 0.1) stats only
Bandwidth probe defined in protocol, never used throughput measured per tick, feeds peak no
MTU one binary search, then fixed continuous path probing fixed

BUD reference: 01-protocol.md §5.2, §9, §10, §13.


9. Behaviour at 5-10% loss: why retransmission wins

This section explains the empirically observed ordering on lossy links — Parsec good, Sunshine and Remote Play poor — from the mechanisms above.

9.1 Block FEC under independent loss

A frame of k source and m parity units survives if and only if at most m of the n = k+m units are lost. Under independent per-packet loss p:

P_fail = 1 - sum(i=0..m) C(n,i) * p^i * (1-p)^(n-i)

For the configurations actually observed on the wire (§2.4), plus two larger blocks for contrast:

k,m n Redundancy p=2% p=5% p=10% Failed frames/s at 60 fps, p=10%
3,2 5 40.0% 0.01% 0.12% 0.86% 0.5
4,2 6 33.3% 0.02% 0.22% 1.58% 1.0
5,2 7 28.6% 0.03% 0.38% 2.57% 1.5
7,2 9 22.2% 0.06% 0.84% 5.30% 3.2
6,1 7 14.3% 0.79% 4.44% 14.97% 9.0
8,1 9 11.1% 1.31% 7.12% 22.52% 13.5
12,4 16 25.0% 0.00% 0.09% 1.70% 1.0
6,4 10 40.0% 0.00% 0.01% 0.16% 0.1

Two things follow. First, m = 1 is catastrophic at these loss rates — 9 to 13.5 failed frames per second — which is why the console raising m under load is the single most important thing it does. Second, at the console's typical 33-40% redundancy, independent loss is not the problem. 0.12% to 1.58% frame failure at 5-10% loss would be a perfectly usable stream.

The observed behaviour is much worse than that. So independent loss is the wrong model.

9.2 Burst loss, which is what actually happens

Real loss on a congested path is bursty: a tail-drop event discards a run of consecutive packets. Two properties of this codebase make block FEC nearly defenceless against it.

A frame's units are emitted back to back. They are not paced and not interleaved with anything else. If the bottleneck queue overflows during that emission, the losses are not merely correlated — they are guaranteed consecutive within one FEC block. The independence assumption that the table in §9.1 rests on is violated in the worst available way.

k is small. Every observed capture has k between 3 and 8, so n is 5 to 10. A frame is five to ten packets sent in a burst lasting well under a millisecond. Under burst loss the block size stops mattering and only m does:

A burst of m+1 consecutive lost packets destroys the frame, regardless of k.

At the modal k=3, m=2, a three-packet drop — entirely routine on a congested link — is fatal. No increase in redundancy percentage helps, because the redundancy is spent inside the same sub-millisecond window that the burst covers. Larger blocks would average bursts out; frames this small cannot produce them. There is no interleaving across frames either: each frame is its own independent FEC block, so a burst spanning a frame boundary takes the tail of one and the head of the next, killing both.

This is the mechanism behind the "20-30% FEC and it still flickers" experience in Sunshine, and it applies to Remote Play identically. You pay the bandwidth and still fail, because the failure mode is burst-shaped and the block is too small and too brief to average over it.

9.3 Why BUD does not have this failure mode

Retransmission is indifferent to the loss pattern. A lost fragment is detected by a gap in the cumulative acknowledgement and resent; whether it was lost alone or as part of a run of twelve changes the amount of retransmission, not whether recovery is possible. Concretely:

  • Cost scales linearly with loss rate, not with burst structure. 5% loss costs ~5% extra traffic. There is no cliff and no exponent.
  • Repair latency is bounded and small. NACK-driven fast retransmit fires without waiting for the timer (§5.2 of BUD), and the RTO floor is 50 ms + 30 ms grace. The affected frame is late; it is never absent.
  • Nothing becomes undecodable, so nothing requests a keyframe. No IDR, no flicker. This is the decisive difference at 60 fps — the visible symptom of FEC failure is not the dropped frame, it is the keyframe that follows it.
  • The rate loop closes locally, per frame. BUD's AIMD sees the congestion that is causing the burst loss and reduces the encoder bitrate within a frame or two, which reduces the queue overflow that produced the bursts. Takion reports loss at 5 Hz to a closed controller and waits.

The trade is real and worth stating plainly: on a clean path, FEC repairs in zero added latency where retransmission costs at least one RTT. BUD is paying a latency premium that Remote Play and Sunshine do not. That premium is a good deal at 5-10% loss and a bad one at 0.1%.

9.4 Where Remote Play sits relative to Sunshine

Better than Sunshine in two respects:

  • The FEC ratio adapts. Sunshine's is a static config value; the console moves m between 1 and 4 and reaches 40-50% redundancy under load (§2.4). It is slow — 5 Hz, closed loop — but it is not fixed.
  • There is a concealment step Sunshine lacks. The reference-frame remap (§2.3 step 2) rewrites a P-slice's ref_idx to the nearest surviving reference instead of giving up, and enable_idr_on_fec_failure defaults to false. Some failures that would be a keyframe elsewhere become a brief artifact here.

Worse than Sunshine in one respect that matters:

  • You cannot set the ratio. In Sunshine, 20-30% FEC is a number you type. Here the console decides from a 5 Hz loss report and a bwLoss field hardcoded to 0.1% (§7.2). If its policy is wrong for your path, there is no override.

9.5 Practical mitigations, in order of expected effect

  1. Lower the bitrate. Fewer bytes per frame means smaller k, and for a given m that is strictly higher fractional redundancy. It also reduces the queue pressure producing the bursts in the first place. This is the only lever with a mechanism that is certain.
  2. Keep packet_loss_max at 1.0 so the console sees the real loss rate and raises m. The upstream default of 0.05 actively suppresses the adaptation you want here.
  3. Leave enable_idr_on_fec_failure off (the default). Client-driven IDR per failed frame at 60 fps trades one bad frame for a visible flash. Note this does not eliminate keyframes — CORRUPTFRAME is still sent unconditionally and the console may answer it with one (§2.3).
  4. Experiment with bwLoss (§7.2). One-line change, directly measurable by logging units_in_frame_fec, and currently untested by anyone as far as this tree shows.
  5. Reduce the resolution rather than only the bitrate. adaptiveStreamMode: "resize" means the console will do this itself, but starting lower avoids the transient.

None of these change the architecture. At sustained 5-10% burst loss, a retransmitting protocol is the right tool and a block-FEC protocol is not, and no amount of tuning closes that gap.


10. Assessment: how good is this over the internet

Where it is genuinely good

  • FEC is the right primitive for 60 fps interactive video. A retransmission costs at least one RTT; a parity unit costs bandwidth and zero latency. On a 35 ms path, FEC repairs in zero frames what a retransmit-based scheme needs 80 ms+ — three to five frames — to fix. On this axis Takion beats BUD.
  • The FEC ratio is per frame, so the console can adapt protection within a frame time rather than on a control-message round trip.
  • Loss accounting is exact for video, not inferred from sequence gaps.
  • Resolution downshift is a better degradation axis than bitrate alone for a fixed-profile hardware encoder.
  • The 16 ms AV reorder window absorbs Wi-Fi microbursts without stalling on real loss.

Where it fails, specifically

  1. Loss-only congestion signal. The congestion packet carries two uint16s. No one-way delay, no queueing delay, no jitter. On a bufferbloated last mile — most consumer internet — loss appears only after the queue has already added 100-300 ms of latency. A loss-only controller structurally cannot see that arriving. BUD's staleness classifier is partly delay-derived and reacts to RTT growth; Takion does not measure RTT during the session at all.
  2. 5 Hz control loop. BUD ticks its controller once per encoded frame and acknowledges on a 10 ms floor. A congestion episode is 200 ms stale before the console hears about it.
  3. Open loop. The controller cannot be inspected, tuned, or fixed. With BUD the constants are in the spec and can be changed.
  4. No bandwidth probe. The initial rate is a user guess. Configure 30 Mbps on a 15 Mbps link and the session self-congests until the console's loop walks it down on its own schedule.
  5. Burst loss defeats block FEC, and this is the dominant failure mode on a lossy path. Units of one frame go out back to back with no cross-frame interleaving, and k is only 3 to 8, so a tail-drop burst removes consecutive units of one small generation. Under bursts only m matters: m+1 consecutive losses are fatal regardless of redundancy percentage. Quantified in §9.1-9.2. Sunshine shares this exactly; BUD is immune because it retransmits.
  6. FEC failure is a cliff, not a slope. Below k units the frame is gone: report corrupt, and wait for whatever keyframe the console decides to send over a link that has just demonstrated it is lossy. There is no intermediate degradation between "fine" and "frozen". The reference-frame remap softens some cases; it does not change the shape of the curve.
  7. The reliable channel would be bad if it mattered. A flat 200 ms RTO with no SACK on a 150 ms path costs 200-400 ms per lost control message, and IDRREQUEST is a control message.

Verdict

On a wired, low-jitter path with real headroom — under ~40 ms, fiber or off-peak cable — Remote Play is genuinely good, and its FEC-not-retransmit choice actually beats BUD on latency, because the console's rate loop rarely has to act and the weak parts never engage.

On a path with variable capacity or bufferbloat — LTE, congested evening cable, a distant Wi-Fi AP — it degrades as a cliff rather than a ramp: FEC failures, freezes, keyframe flicker, where BUD's AIMD would have walked the bitrate down smoothly a second earlier.

At sustained 5-10% loss the gap is structural and not closable by tuning. Two mechanisms compound: block FEC over a five-to-ten-packet block emitted in under a millisecond has no defence against burst loss (§9.2), and a 5 Hz loss-only report into closed firmware cannot back the rate off fast enough to stop producing the bursts (§5). A retransmitting protocol is simply the right tool in that regime, and Remote Play is not one.

Available client-side levers: the bitrate slider, packet_loss_max, and enable_idr_on_fec_failure — plus bwLoss, if someone patches it (§7.2). That is all of them.


11. File index

File Role
lib/src/fec.c Cauchy Reed-Solomon encode/decode via jerasure
lib/src/frameprocessor.c Frame reassembly from units, erasure list construction, FEC invocation
lib/src/videoreceiver.c Frame sequencing, corrupt-frame reporting, IDR request, reference-frame remap
lib/src/audioreceiver.c Audio repetition redundancy extraction
lib/src/congestioncontrol.c The entire congestion "control": a 200 ms loss reporter
lib/src/packetstats.c Generation-based and sequence-based loss accounting
lib/src/takion.c Transport, packet dispatch, DATA_ACK handling, AV reorder queues
lib/src/takionsendbuffer.c Reliable-channel retransmission, flat 200 ms RTO
lib/src/feedbacksender.c Input: idempotent state plus 4-event repetition history
lib/src/senkusha.c Pre-stream RTT and MTU probing
lib/src/streamconnection.c Protobuf control messages, CONNECTIONQUALITY handling
lib/src/launchspec.c Launch spec JSON, including the static bwKbpsSent
lib/protobuf/takion.proto Full control-message schema, including unused capabilities
test/fec_test_cases.inl 64 captured frames; the (k, m) evidence in §2.4
test/takion.c Real captured AV packet parse, k=7, m=1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment