gate Muninn decode epochs and retry repair
This commit is contained in:
parent
42d6d173c3
commit
78ca783276
5 changed files with 89 additions and 28 deletions
|
|
@ -452,15 +452,18 @@ The first cut landed in Odin on 2026-07-16: experimental CultLib snapshot
|
||||||
`8965f3c0`, epoch/sequence/edge-ack HID delivery, 100 ms default LAN media
|
`8965f3c0`, epoch/sequence/edge-ack HID delivery, 100 ms default LAN media
|
||||||
deadline, CultNet `realtime` A/V delivery, bounded queues, expiring/late-aware
|
deadline, CultNet `realtime` A/V delivery, bounded queues, expiring/late-aware
|
||||||
repair, and decode-chain-owned keyframe pressure. The production video path now
|
repair, and decode-chain-owned keyframe pressure. The production video path now
|
||||||
uses typed V4 Cauchy GF(256) FEC in independent 8-data/8-parity blocks. Each
|
uses typed V4 Cauchy GF(256) FEC in independent 8-data/4-parity blocks. Each
|
||||||
block schedules data and parity as separate lanes, including fixed protection
|
block schedules data and parity as separate lanes, including fixed protection
|
||||||
for a short tail block. Canonical video and parity use CultNet's unreliable
|
for a short tail block. Canonical video and parity use CultNet's unreliable
|
||||||
`realtime` lane; selective repair and IDR recovery remain deadline-bound.
|
`realtime` lane; selective repair and IDR recovery remain deadline-bound.
|
||||||
Canonical audio and its parity also use `realtime`; 4+2 FEC, reorder, and
|
Canonical AAC ADTS audio and its fixed 864-byte parity shards also use
|
||||||
|
`realtime`; 4+2 FEC, 120 ms reorder, and
|
||||||
concealment own continuity without an ACK/retransmit window. Sender access units enter the
|
concealment own continuity without an ACK/retransmit window. Sender access units enter the
|
||||||
handoff queue independently, and CultMesh catalog publication runs outside the
|
handoff queue independently, and CultMesh catalog publication runs outside the
|
||||||
realtime media loop. The remaining work is the full direct/proxy impairment
|
realtime media loop. Repair material becomes available when a video frame enters
|
||||||
matrix, Opus FEC/PLC, and long-duration mixed soak.
|
the queue, and the receiver may issue bounded repair requests at 64/96/128 ms.
|
||||||
|
The remaining work is duplicate/reconnect acceptance, longer iid runs, and the
|
||||||
|
long-duration mixed soak.
|
||||||
|
|
||||||
The socket harness and bounded PCM loss recovery have now landed. Odin's
|
The socket harness and bounded PCM loss recovery have now landed. Odin's
|
||||||
`cultnet-impair` proxy supplies deterministic seeded loss/burst/reorder/
|
`cultnet-impair` proxy supplies deterministic seeded loss/burst/reorder/
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,37 @@ datagram or move fragmentation above the erasure-code boundary.
|
||||||
scheduler sees it. Capacity-eviction counters proved zero during the clean
|
scheduler sees it. Capacity-eviction counters proved zero during the clean
|
||||||
acceptance run.
|
acceptance run.
|
||||||
|
|
||||||
|
## 2026-07-17 bounded-media field cut
|
||||||
|
|
||||||
|
The live path now keeps every protected shard in one datagram, sends AAC ADTS
|
||||||
|
in fixed 864-byte 4+2 audio blocks, uses 768-byte video shards with 8+4 block
|
||||||
|
FEC, and begins each decoder epoch at a complete IDR. The sender admits video
|
||||||
|
to its repair cache when the frame enters the queue, accepts the production
|
||||||
|
CultNet `realtime` channel, and can service bounded repair requests at
|
||||||
|
64/96/128 ms in batches of up to eight chunks.
|
||||||
|
|
||||||
|
Seeded Raven-to-Starfire evidence:
|
||||||
|
|
||||||
|
- iid 1%: 802 of 78,406 datagrams dropped; zero decode drop, video expiry,
|
||||||
|
audio failure, or audio skip; 1,800 video frames recovered;
|
||||||
|
- burst-4: 1,436 of 71,910 datagrams dropped; zero expiry or audio skip across
|
||||||
|
1,800 forwarded frames;
|
||||||
|
- reorder: 2,872 reorder events; zero expiry or audio skip across 900 frames;
|
||||||
|
- iid 3%: 2,564 of 85,731 datagrams dropped; three of 5,400 frames expired,
|
||||||
|
zero audio skip (99.94 percent video delivery);
|
||||||
|
- burst-8: 190 chunks repaired; one of 1,800 frames expired, zero audio skip;
|
||||||
|
- 250 ms stall: 420 datagrams withheld, one frame expired, 68 chunks repaired,
|
||||||
|
an IDR was requested, and another 900 frames arrived without a stuck decoder
|
||||||
|
or audio skip.
|
||||||
|
|
||||||
|
Delay/jitter produced one expiry in 900 frames. Duplicate, reconnect, longer
|
||||||
|
iid runs, and the 60-minute mixed soak remain open acceptance rows.
|
||||||
|
|
||||||
|
Field scar: production video uses the `realtime` channel, but the repair cache
|
||||||
|
previously admitted only legacy `media` payloads. Synthetic tests built the
|
||||||
|
legacy shape and certified a repair path that was empty in production. The
|
||||||
|
production-channel regression test now owns this boundary.
|
||||||
|
|
||||||
## Completion rule
|
## Completion rule
|
||||||
|
|
||||||
Moonlight-level performance is reasonably expected only after all local rows
|
Moonlight-level performance is reasonably expected only after all local rows
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,8 @@ constexpr uint64_t MuninnMediaStaleMs = 1500;
|
||||||
constexpr uint64_t MuninnMediaRestartCooldownMs = 5000;
|
constexpr uint64_t MuninnMediaRestartCooldownMs = 5000;
|
||||||
constexpr uint64_t MuninnReceiverKeyframeRequestCooldownMs = 500;
|
constexpr uint64_t MuninnReceiverKeyframeRequestCooldownMs = 500;
|
||||||
constexpr uint64_t MuninnReceiverChunkRepairFirstWaitMs = 64;
|
constexpr uint64_t MuninnReceiverChunkRepairFirstWaitMs = 64;
|
||||||
|
constexpr uint64_t MuninnReceiverChunkRepairRetryWaitMs = 32;
|
||||||
|
constexpr uint32_t MuninnReceiverChunkRepairMaxRequests = 3;
|
||||||
constexpr uint64_t MuninnVideoAssemblyExpiryScanMs = 10;
|
constexpr uint64_t MuninnVideoAssemblyExpiryScanMs = 10;
|
||||||
constexpr size_t MuninnExpiredVideoFrameRememberCount = 4096;
|
constexpr size_t MuninnExpiredVideoFrameRememberCount = 4096;
|
||||||
constexpr size_t MuninnCompletedVideoFrameRememberCount = 4096;
|
constexpr size_t MuninnCompletedVideoFrameRememberCount = 4096;
|
||||||
|
|
@ -256,7 +258,9 @@ struct MuninnVideoFrameAssembly {
|
||||||
uint16_t chunk_count = 0;
|
uint16_t chunk_count = 0;
|
||||||
uint16_t chunks_received = 0;
|
uint16_t chunks_received = 0;
|
||||||
std::chrono::steady_clock::time_point started_at;
|
std::chrono::steady_clock::time_point started_at;
|
||||||
bool repair_requested = false;
|
uint32_t repair_request_count = 0;
|
||||||
|
std::chrono::steady_clock::time_point last_repair_requested_at =
|
||||||
|
std::chrono::steady_clock::time_point::min();
|
||||||
std::map<uint16_t, MuninnVideoFecBlockAssembly> fec_blocks;
|
std::map<uint16_t, MuninnVideoFecBlockAssembly> fec_blocks;
|
||||||
std::vector<std::vector<uint8_t>> chunks;
|
std::vector<std::vector<uint8_t>> chunks;
|
||||||
};
|
};
|
||||||
|
|
@ -1650,6 +1654,15 @@ private:
|
||||||
{
|
{
|
||||||
expire_video_assemblies(std::chrono::steady_clock::now(), bridge_sequence);
|
expire_video_assemblies(std::chrono::steady_clock::now(), bridge_sequence);
|
||||||
expire_video_parity_cache(std::chrono::steady_clock::now());
|
expire_video_parity_cache(std::chrono::steady_clock::now());
|
||||||
|
const std::string key =
|
||||||
|
media.stream_id + ":" + media.session_id + ":" + std::to_string(media.frame_id);
|
||||||
|
if (waiting_for_video_keyframe_ && !media.keyframe) {
|
||||||
|
video_assemblies_.erase(key);
|
||||||
|
video_parity_cache_.erase(key);
|
||||||
|
remember_completed_video_frame(key);
|
||||||
|
++video_recovery_dropped_;
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
if (media.kind == MuninnMediaPayload::Kind::VideoParity) {
|
if (media.kind == MuninnMediaPayload::Kind::VideoParity) {
|
||||||
return assemble_video_parity_payload(media, bridge_sequence);
|
return assemble_video_parity_payload(media, bridge_sequence);
|
||||||
}
|
}
|
||||||
|
|
@ -1658,8 +1671,6 @@ private:
|
||||||
log_bridge_pressure("dropped invalid video chunk metadata");
|
log_bridge_pressure("dropped invalid video chunk metadata");
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
const std::string key =
|
|
||||||
media.stream_id + ":" + media.session_id + ":" + std::to_string(media.frame_id);
|
|
||||||
if (video_frame_is_settled(key)) {
|
if (video_frame_is_settled(key)) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
@ -1677,7 +1688,7 @@ private:
|
||||||
assembly.chunk_count = media.chunk_count;
|
assembly.chunk_count = media.chunk_count;
|
||||||
assembly.chunks.resize(media.chunk_count);
|
assembly.chunks.resize(media.chunk_count);
|
||||||
assembly.started_at = std::chrono::steady_clock::now();
|
assembly.started_at = std::chrono::steady_clock::now();
|
||||||
assembly.repair_requested = false;
|
assembly.repair_request_count = 0;
|
||||||
apply_cached_video_parity(key, assembly);
|
apply_cached_video_parity(key, assembly);
|
||||||
}
|
}
|
||||||
if (assembly.chunk_count != media.chunk_count ||
|
if (assembly.chunk_count != media.chunk_count ||
|
||||||
|
|
@ -1999,11 +2010,18 @@ private:
|
||||||
if (needs_decoder_recovery) {
|
if (needs_decoder_recovery) {
|
||||||
waiting_for_video_keyframe_ = true;
|
waiting_for_video_keyframe_ = true;
|
||||||
}
|
}
|
||||||
log_video_assembly_expiry(iterator->second, missing_chunk_keys, now);
|
const bool bootstrap_keyframe = waiting_for_video_keyframe_ && iterator->second.keyframe;
|
||||||
|
if (!bootstrap_keyframe) {
|
||||||
|
log_video_assembly_expiry(iterator->second, missing_chunk_keys, now);
|
||||||
|
}
|
||||||
remember_expired_video_frame(iterator->first);
|
remember_expired_video_frame(iterator->first);
|
||||||
iterator = video_assemblies_.erase(iterator);
|
iterator = video_assemblies_.erase(iterator);
|
||||||
++video_assemblies_expired_;
|
if (bootstrap_keyframe) {
|
||||||
log_bridge_pressure("expired incomplete video frame assembly");
|
++video_recovery_dropped_;
|
||||||
|
} else {
|
||||||
|
++video_assemblies_expired_;
|
||||||
|
log_bridge_pressure("expired incomplete video frame assembly");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2046,7 +2064,7 @@ private:
|
||||||
static_cast<unsigned int>(assembly.chunk_count),
|
static_cast<unsigned int>(assembly.chunk_count),
|
||||||
missing_chunk_keys.size(),
|
missing_chunk_keys.size(),
|
||||||
static_cast<long long>(age_ms),
|
static_cast<long long>(age_ms),
|
||||||
assembly.repair_requested ? "true" : "false",
|
assembly.repair_request_count > 0 ? "true" : "false",
|
||||||
assembly.fec_blocks.empty() ? "false" : "true",
|
assembly.fec_blocks.empty() ? "false" : "true",
|
||||||
fec_state.str().c_str());
|
fec_state.str().c_str());
|
||||||
}
|
}
|
||||||
|
|
@ -2058,11 +2076,17 @@ private:
|
||||||
if (assembly.chunks_received >= assembly.chunk_count) {
|
if (assembly.chunks_received >= assembly.chunk_count) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (assembly.repair_requested) {
|
if (assembly.repair_request_count >= MuninnReceiverChunkRepairMaxRequests) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto repair_wait_ms = std::max<uint32_t>(parts_.gap_wait_ms, MuninnReceiverChunkRepairFirstWaitMs);
|
const auto first_wait_ms = std::max<uint32_t>(parts_.gap_wait_ms, MuninnReceiverChunkRepairFirstWaitMs);
|
||||||
if (now - assembly.started_at < std::chrono::milliseconds(repair_wait_ms)) {
|
if (assembly.repair_request_count == 0 &&
|
||||||
|
now - assembly.started_at < std::chrono::milliseconds(first_wait_ms)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (assembly.repair_request_count > 0 &&
|
||||||
|
now - assembly.last_repair_requested_at <
|
||||||
|
std::chrono::milliseconds(MuninnReceiverChunkRepairRetryWaitMs)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto missing_chunk_keys = missing_video_chunk_keys(assembly);
|
const auto missing_chunk_keys = missing_video_chunk_keys(assembly);
|
||||||
|
|
@ -2070,7 +2094,8 @@ private:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
send_receiver_feedback_if_due(assembly, missing_chunk_keys, false, false, bridge_sequence);
|
send_receiver_feedback_if_due(assembly, missing_chunk_keys, false, false, bridge_sequence);
|
||||||
assembly.repair_requested = true;
|
assembly.last_repair_requested_at = now;
|
||||||
|
++assembly.repair_request_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
void expire_rudp_packet_fragments(std::chrono::steady_clock::time_point now)
|
void expire_rudp_packet_fragments(std::chrono::steady_clock::time_point now)
|
||||||
|
|
|
||||||
|
|
@ -342,13 +342,13 @@ The live Raven-to-Starfire program path is Muninn's typed CultNet media body:
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
A["Raven D3D11 desktop capture"] --> B["Long-lived NVENC encoder"]
|
A["Raven D3D11 desktop capture"] --> B["Long-lived NVENC encoder"]
|
||||||
C["Raven WASAPI loopback"] --> D["10 ms float PCM packetizer"]
|
C["Raven WASAPI loopback"] --> D["AAC-LC / ADTS encoder"]
|
||||||
B --> E["V4 8+8 block FEC and realtime video lane"]
|
B --> E["V4 8+4 block FEC and realtime video lane"]
|
||||||
D --> F["4+2 audio FEC and realtime audio lane"]
|
D --> F["Fixed 864-byte 4+2 audio FEC lane"]
|
||||||
E --> G["Starfire OBS Muninn receiver udp/5200"]
|
E --> G["Starfire OBS Muninn receiver udp/5200"]
|
||||||
F --> G
|
F --> G
|
||||||
G --> H["Frame assembly / FEC / IDR feedback"]
|
G --> H["Frame assembly / FEC / IDR feedback"]
|
||||||
G --> I["40 ms audio reorder / concealment"]
|
G --> I["120 ms audio reorder / concealment"]
|
||||||
H --> J["OBS-local video decoder socket"]
|
H --> J["OBS-local video decoder socket"]
|
||||||
I --> K["OBS-local audio decoder socket"]
|
I --> K["OBS-local audio decoder socket"]
|
||||||
```
|
```
|
||||||
|
|
@ -361,8 +361,9 @@ the provider surface; neither discovery nor the compatibility decoder sockets
|
||||||
own hot media truth.
|
own hot media truth.
|
||||||
|
|
||||||
Invariant: video packets are useful only before their assembly deadline.
|
Invariant: video packets are useful only before their assembly deadline.
|
||||||
Canonical video and V4 parity use unreliable `realtime`; block FEC, selective
|
Canonical video and V4 parity use unreliable `realtime`; 8+4 block FEC,
|
||||||
repair, and IDR recovery own loss. Canonical audio and 4+2 parity also use
|
queue-admitted selective repair, and IDR recovery own loss. Canonical AAC audio
|
||||||
|
and fixed 4+2 parity also use
|
||||||
`realtime`; reorder and bounded concealment own playout continuity. Completed
|
`realtime`; reorder and bounded concealment own playout continuity. Completed
|
||||||
video frames are tombstoned so late shards cannot
|
video frames are tombstoned so late shards cannot
|
||||||
become a second assembly owner. Raven uses direct LAN only and starts through a
|
become a second assembly owner. Raven uses direct LAN only and starts through a
|
||||||
|
|
|
||||||
|
|
@ -89,13 +89,13 @@ invariants:
|
||||||
accepted_design:
|
accepted_design:
|
||||||
- Muninn video FEC is typed as `muninn.media_video_parity_shard.v4`.
|
- Muninn video FEC is typed as `muninn.media_video_parity_shard.v4`.
|
||||||
Odin divides each access unit into independent blocks of at most eight data
|
Odin divides each access unit into independent blocks of at most eight data
|
||||||
shards and emits eight Cauchy GF(256) parity rows for every block, including
|
shards and emits four Cauchy GF(256) parity rows for every block, including
|
||||||
a short tail. Data/parity lanes are round-robin scheduled per block. Mimir's
|
a short tail. Data/parity lanes are round-robin scheduled per block. Mimir's
|
||||||
OBS receiver owns bounded block recovery before the assembly deadline,
|
OBS receiver owns bounded block recovery before the assembly deadline,
|
||||||
forwards only the canonical completed frame, and tombstones completed frame
|
forwards only the canonical completed frame, and tombstones completed frame
|
||||||
keys so late shards cannot resurrect ghost assemblies. V2/V3 remain legacy
|
keys so late shards cannot resurrect ghost assemblies. V2/V3 remain legacy
|
||||||
decode shapes and never own V4 behavior. Rust tests exhaust every burst of
|
decode shapes and never own V4 behavior. Rust tests exhaust every burst of
|
||||||
one through eight packets at every alignment for chunk counts 2 through 80;
|
one through four packets at every alignment, plus tail-block burst coverage;
|
||||||
native tests prove the matching receiver. Canonical video and parity now use
|
native tests prove the matching receiver. Canonical video and parity now use
|
||||||
CultNet `realtime` without per-packet reliable queues. Repair is selective
|
CultNet `realtime` without per-packet reliable queues. Repair is selective
|
||||||
and behind fresh media; unrecoverable dependency damage requests a live
|
and behind fresh media; unrecoverable dependency damage requests a live
|
||||||
|
|
@ -122,8 +122,9 @@ accepted_design:
|
||||||
The normal quarter-second scheduled IDR remains independent fallback.
|
The normal quarter-second scheduled IDR remains independent fallback.
|
||||||
The repeatable bundle includes the OBS experimental FFmpeg dependency
|
The repeatable bundle includes the OBS experimental FFmpeg dependency
|
||||||
snapshot. Raven now runs the bundle over direct LAN from a hidden scheduled
|
snapshot. Raven now runs the bundle over direct LAN from a hidden scheduled
|
||||||
task; WireGuard is not a runtime or administration path. Real impairment
|
task; WireGuard is not a runtime or administration path. Seeded 1% loss,
|
||||||
acceptance remains pending.
|
burst-4, reorder, and bounded stall recovery now pass; longer matrix and
|
||||||
|
soak acceptance remain pending.
|
||||||
- Muninn input now bounds disconnected unacknowledged button history at 256
|
- Muninn input now bounds disconnected unacknowledged button history at 256
|
||||||
edges. Overflow rotates the controller epoch and rebases Sleipnir from the
|
edges. Overflow rotates the controller epoch and rebases Sleipnir from the
|
||||||
next complete latest-state frame, rather than dropping an arbitrary press
|
next complete latest-state frame, rather than dropping an arbitrary press
|
||||||
|
|
@ -131,11 +132,11 @@ accepted_design:
|
||||||
256. Full Muninn (174) and Sleipnir (26) suites pass.
|
256. Full Muninn (174) and Sleipnir (26) suites pass.
|
||||||
- The 2026-07-16 loss-recovery cut is now live on both sides of the Muninn
|
- The 2026-07-16 loss-recovery cut is now live on both sides of the Muninn
|
||||||
audio boundary. Odin emits typed `muninn.media_audio_parity_shard.v1`
|
audio boundary. Odin emits typed `muninn.media_audio_parity_shard.v1`
|
||||||
records using fixed 4+2 GF(256) parity for constant-size 10 ms PCM packets.
|
records using fixed 4+2 GF(256) parity for padded AAC ADTS packets.
|
||||||
Canonical audio and parity use `realtime`; 4+2 FEC, reorder, and bounded
|
Canonical audio and parity use `realtime`; 4+2 FEC, reorder, and bounded
|
||||||
concealment own continuity without a media ACK/retransmit window.
|
concealment own continuity without a media ACK/retransmit window.
|
||||||
Mimir's production OBS receiver retains a bounded recent data window,
|
Mimir's production OBS receiver retains a bounded recent data window,
|
||||||
reconstructs up to two erased packets per block before the 40 ms playout
|
reconstructs up to two erased packets per block before the 120 ms playout
|
||||||
deadline, and then hands recovered packets to the same reorder queue that
|
deadline, and then hands recovered packets to the same reorder queue that
|
||||||
owns normal playout. The FEC cache is not a second timeline. Native tests
|
owns normal playout. The FEC cache is not a second timeline. Native tests
|
||||||
exhaust every one- and two-erasure permutation, and the production plugin
|
exhaust every one- and two-erasure permutation, and the production plugin
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue