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
|
||||
deadline, CultNet `realtime` A/V delivery, bounded queues, expiring/late-aware
|
||||
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
|
||||
for a short tail block. Canonical video and parity use CultNet's unreliable
|
||||
`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
|
||||
handoff queue independently, and CultMesh catalog publication runs outside the
|
||||
realtime media loop. The remaining work is the full direct/proxy impairment
|
||||
matrix, Opus FEC/PLC, and long-duration mixed soak.
|
||||
realtime media loop. Repair material becomes available when a video frame enters
|
||||
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
|
||||
`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
|
||||
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
|
||||
|
||||
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 MuninnReceiverKeyframeRequestCooldownMs = 500;
|
||||
constexpr uint64_t MuninnReceiverChunkRepairFirstWaitMs = 64;
|
||||
constexpr uint64_t MuninnReceiverChunkRepairRetryWaitMs = 32;
|
||||
constexpr uint32_t MuninnReceiverChunkRepairMaxRequests = 3;
|
||||
constexpr uint64_t MuninnVideoAssemblyExpiryScanMs = 10;
|
||||
constexpr size_t MuninnExpiredVideoFrameRememberCount = 4096;
|
||||
constexpr size_t MuninnCompletedVideoFrameRememberCount = 4096;
|
||||
|
|
@ -256,7 +258,9 @@ struct MuninnVideoFrameAssembly {
|
|||
uint16_t chunk_count = 0;
|
||||
uint16_t chunks_received = 0;
|
||||
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::vector<std::vector<uint8_t>> chunks;
|
||||
};
|
||||
|
|
@ -1650,6 +1654,15 @@ private:
|
|||
{
|
||||
expire_video_assemblies(std::chrono::steady_clock::now(), bridge_sequence);
|
||||
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) {
|
||||
return assemble_video_parity_payload(media, bridge_sequence);
|
||||
}
|
||||
|
|
@ -1658,8 +1671,6 @@ private:
|
|||
log_bridge_pressure("dropped invalid video chunk metadata");
|
||||
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)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
|
@ -1677,7 +1688,7 @@ private:
|
|||
assembly.chunk_count = media.chunk_count;
|
||||
assembly.chunks.resize(media.chunk_count);
|
||||
assembly.started_at = std::chrono::steady_clock::now();
|
||||
assembly.repair_requested = false;
|
||||
assembly.repair_request_count = 0;
|
||||
apply_cached_video_parity(key, assembly);
|
||||
}
|
||||
if (assembly.chunk_count != media.chunk_count ||
|
||||
|
|
@ -1999,11 +2010,18 @@ private:
|
|||
if (needs_decoder_recovery) {
|
||||
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);
|
||||
iterator = video_assemblies_.erase(iterator);
|
||||
++video_assemblies_expired_;
|
||||
log_bridge_pressure("expired incomplete video frame assembly");
|
||||
if (bootstrap_keyframe) {
|
||||
++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),
|
||||
missing_chunk_keys.size(),
|
||||
static_cast<long long>(age_ms),
|
||||
assembly.repair_requested ? "true" : "false",
|
||||
assembly.repair_request_count > 0 ? "true" : "false",
|
||||
assembly.fec_blocks.empty() ? "false" : "true",
|
||||
fec_state.str().c_str());
|
||||
}
|
||||
|
|
@ -2058,11 +2076,17 @@ private:
|
|||
if (assembly.chunks_received >= assembly.chunk_count) {
|
||||
return;
|
||||
}
|
||||
if (assembly.repair_requested) {
|
||||
if (assembly.repair_request_count >= MuninnReceiverChunkRepairMaxRequests) {
|
||||
return;
|
||||
}
|
||||
const auto repair_wait_ms = std::max<uint32_t>(parts_.gap_wait_ms, MuninnReceiverChunkRepairFirstWaitMs);
|
||||
if (now - assembly.started_at < std::chrono::milliseconds(repair_wait_ms)) {
|
||||
const auto first_wait_ms = std::max<uint32_t>(parts_.gap_wait_ms, MuninnReceiverChunkRepairFirstWaitMs);
|
||||
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;
|
||||
}
|
||||
const auto missing_chunk_keys = missing_video_chunk_keys(assembly);
|
||||
|
|
@ -2070,7 +2094,8 @@ private:
|
|||
return;
|
||||
}
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -342,13 +342,13 @@ The live Raven-to-Starfire program path is Muninn's typed CultNet media body:
|
|||
```mermaid
|
||||
flowchart TD
|
||||
A["Raven D3D11 desktop capture"] --> B["Long-lived NVENC encoder"]
|
||||
C["Raven WASAPI loopback"] --> D["10 ms float PCM packetizer"]
|
||||
B --> E["V4 8+8 block FEC and realtime video lane"]
|
||||
D --> F["4+2 audio FEC and realtime audio lane"]
|
||||
C["Raven WASAPI loopback"] --> D["AAC-LC / ADTS encoder"]
|
||||
B --> E["V4 8+4 block FEC and realtime video lane"]
|
||||
D --> F["Fixed 864-byte 4+2 audio FEC lane"]
|
||||
E --> G["Starfire OBS Muninn receiver udp/5200"]
|
||||
F --> G
|
||||
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"]
|
||||
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.
|
||||
|
||||
Invariant: video packets are useful only before their assembly deadline.
|
||||
Canonical video and V4 parity use unreliable `realtime`; block FEC, selective
|
||||
repair, and IDR recovery own loss. Canonical audio and 4+2 parity also use
|
||||
Canonical video and V4 parity use unreliable `realtime`; 8+4 block FEC,
|
||||
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
|
||||
video frames are tombstoned so late shards cannot
|
||||
become a second assembly owner. Raven uses direct LAN only and starts through a
|
||||
|
|
|
|||
|
|
@ -89,13 +89,13 @@ invariants:
|
|||
accepted_design:
|
||||
- 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
|
||||
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
|
||||
OBS receiver owns bounded block recovery before the assembly deadline,
|
||||
forwards only the canonical completed frame, and tombstones completed frame
|
||||
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
|
||||
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
|
||||
CultNet `realtime` without per-packet reliable queues. Repair is selective
|
||||
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 repeatable bundle includes the OBS experimental FFmpeg dependency
|
||||
snapshot. Raven now runs the bundle over direct LAN from a hidden scheduled
|
||||
task; WireGuard is not a runtime or administration path. Real impairment
|
||||
acceptance remains pending.
|
||||
task; WireGuard is not a runtime or administration path. Seeded 1% loss,
|
||||
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
|
||||
edges. Overflow rotates the controller epoch and rebases Sleipnir from the
|
||||
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.
|
||||
- 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`
|
||||
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
|
||||
concealment own continuity without a media ACK/retransmit 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
|
||||
owns normal playout. The FEC cache is not a second timeline. Native tests
|
||||
exhaust every one- and two-erasure permutation, and the production plugin
|
||||
|
|
|
|||
Loading…
Reference in a new issue