Bound Muninn video FEC decode time

This commit is contained in:
Meta Vens 2026-07-17 01:46:28 +01:00
commit e9e7eaae30
4 changed files with 84 additions and 36 deletions

View file

@ -9,7 +9,7 @@
namespace muninn_video_fec {
inline uint8_t multiply(uint8_t a, uint8_t b)
inline uint8_t raw_multiply(uint8_t a, uint8_t b)
{
uint8_t product = 0;
while (b != 0) {
@ -24,12 +24,39 @@ inline uint8_t multiply(uint8_t a, uint8_t b)
return product;
}
struct Tables {
uint8_t products[256][256] = {};
uint8_t inverses[256] = {};
Tables()
{
for (size_t a = 0; a < 256; ++a) {
for (size_t b = 0; b < 256; ++b)
products[a][b] = raw_multiply(static_cast<uint8_t>(a), static_cast<uint8_t>(b));
}
for (size_t value = 1; value < 256; ++value) {
uint8_t result = 1;
for (size_t index = 0; index < 254; ++index)
result = raw_multiply(result, static_cast<uint8_t>(value));
inverses[value] = result;
}
}
};
inline const Tables &tables()
{
static const Tables value;
return value;
}
inline uint8_t multiply(uint8_t a, uint8_t b)
{
return tables().products[a][b];
}
inline uint8_t inverse(uint8_t value)
{
uint8_t result = 1;
for (size_t index = 0; index < 254; ++index)
result = multiply(result, value);
return result;
return tables().inverses[value];
}
inline uint8_t coefficient(uint16_t parity_index, uint16_t parity_count, uint16_t data_index)
@ -82,41 +109,48 @@ inline bool recover(std::vector<std::vector<uint8_t>> &chunks,
}
const size_t count = missing.size();
for (size_t offset = 0; offset < shard_bytes; ++offset) {
std::vector<std::vector<uint8_t>> matrix(count, std::vector<uint8_t>(count + 1, 0));
for (size_t row = 0; row < count; ++row) {
const uint16_t parity_index = rows[row].first;
uint8_t rhs = (*rows[row].second)[offset];
for (uint16_t data_index = 0; data_index < chunks.size(); ++data_index) {
if (!chunks[data_index].empty() && offset < chunks[data_index].size())
rhs ^= multiply(coefficient(parity_index, parity_count, data_index),
chunks[data_index][offset]);
}
for (size_t column = 0; column < count; ++column)
matrix[row][column] = coefficient(parity_index, parity_count, missing[column]);
matrix[row][count] = rhs;
}
for (size_t column = 0; column < count; ++column) {
std::vector<std::vector<uint8_t>> inverse_matrix(count, std::vector<uint8_t>(count * 2, 0));
for (size_t row = 0; row < count; ++row) {
for (size_t column = 0; column < count; ++column)
inverse_matrix[row][column] = coefficient(rows[row].first, parity_count, missing[column]);
inverse_matrix[row][count + row] = 1;
}
for (size_t column = 0; column < count; ++column) {
size_t pivot = column;
while (pivot < count && matrix[pivot][column] == 0)
while (pivot < count && inverse_matrix[pivot][column] == 0)
++pivot;
if (pivot == count)
return false;
std::swap(matrix[column], matrix[pivot]);
const uint8_t scale = inverse(matrix[column][column]);
for (size_t cell = column; cell <= count; ++cell)
matrix[column][cell] = multiply(matrix[column][cell], scale);
std::swap(inverse_matrix[column], inverse_matrix[pivot]);
const uint8_t scale = inverse(inverse_matrix[column][column]);
for (size_t cell = 0; cell < count * 2; ++cell)
inverse_matrix[column][cell] = multiply(inverse_matrix[column][cell], scale);
for (size_t row = 0; row < count; ++row) {
if (row == column)
continue;
const uint8_t factor = matrix[row][column];
for (size_t cell = column; cell <= count; ++cell)
matrix[row][cell] ^= multiply(factor, matrix[column][cell]);
const uint8_t factor = inverse_matrix[row][column];
for (size_t cell = 0; cell < count * 2; ++cell)
inverse_matrix[row][cell] ^= multiply(factor, inverse_matrix[column][cell]);
}
}
std::vector<uint8_t> rhs(count, 0);
for (size_t offset = 0; offset < shard_bytes; ++offset) {
for (size_t row = 0; row < count; ++row) {
const uint16_t parity_index = rows[row].first;
rhs[row] = (*rows[row].second)[offset];
for (uint16_t data_index = 0; data_index < chunks.size(); ++data_index) {
if (!chunks[data_index].empty() && offset < chunks[data_index].size())
rhs[row] ^= multiply(coefficient(parity_index, parity_count, data_index),
chunks[data_index][offset]);
}
}
for (size_t index = 0; index < count; ++index) {
uint8_t value = 0;
for (size_t row = 0; row < count; ++row)
value ^= multiply(inverse_matrix[index][count + row], rhs[row]);
if (offset < recovered[index].size())
recovered[index][offset] = matrix[index][count];
recovered[index][offset] = value;
}
}
for (size_t index = 0; index < count; ++index)

View file

@ -1,15 +1,22 @@
#include "muninn_video_fec.h"
#include <cassert>
#include <chrono>
#include <cstdlib>
#include <cstdint>
#include <map>
#include <vector>
void require(bool condition)
{
if (!condition)
std::abort();
}
int main()
{
constexpr uint16_t data_count = 64;
constexpr uint16_t parity_count = 16;
constexpr size_t shard_bytes = 97;
constexpr size_t shard_bytes = 1400;
std::vector<std::vector<uint8_t>> original(data_count, std::vector<uint8_t>(shard_bytes));
for (uint16_t data = 0; data < data_count; ++data) {
for (size_t offset = 0; offset < shard_bytes; ++offset)
@ -30,12 +37,14 @@ int main()
auto damaged = original;
for (uint16_t index = 0; index < burst; ++index)
damaged[20 + index].clear();
assert(muninn_video_fec::recover(damaged, lengths, parity_count, parity));
assert(damaged == original);
const auto started = std::chrono::steady_clock::now();
require(muninn_video_fec::recover(damaged, lengths, parity_count, parity));
require(std::chrono::steady_clock::now() - started < std::chrono::milliseconds(50));
require(damaged == original);
}
auto excessive = original;
for (uint16_t index = 0; index <= parity_count; ++index)
excessive[index].clear();
assert(!muninn_video_fec::recover(excessive, lengths, parity_count, parity));
require(!muninn_video_fec::recover(excessive, lengths, parity_count, parity));
return 0;
}

View file

@ -195,3 +195,4 @@
{"ts": "2026-07-16T21:35:47+01:00", "type": "verification", "status": "ok", "note": "Muninn receiver pressure now drives bounded AIMD bitrate on the long-lived NVENC context. Live 2560x1440 D3D11 proof reconfigured 12 Mbps to 6 Mbps at frame 35, emitted an IDR at frame 35, and decoded cleanly; 176 Muninn tests pass. Cross-host field acceptance remains missing because Raven SSH/TCP 22 timed out again."}
{"ts": "2026-07-16T21:38:23+01:00", "type": "blocker", "status": "pending", "note": "Field Moonlight acceptance cannot run while Raven is offline: Starfire's documented SSH/ICMP/TCP route to 10.77.0.4 times out, and Yggdrasil reports Raven's last WireGuard handshake at 2026-07-15T06:04:12Z versus Starfire's live handshake at 2026-07-16T20:36:32Z. No Raven task or host state was mutated."}
{"ts": "2026-07-17T01:37:22+01:00", "type": "verification", "status": "partial", "note": "Muninn video parity v3 uses 16 Cauchy GF(256) shards; native receiver burst tests 1..16 and Odin 180-test suite pass. Direct Raven LAN at 250 ms recovery deadline ran over one minute with no video assembly expiry or A/V failure; full 10-minute and impairment matrix remain open."}
{"ts": "2026-07-17T01:46:28+01:00", "type": "scar", "status": "fixed", "note": "Per-byte Gaussian elimination for production-sized video FEC can stall the single OBS receive owner and cascade socket loss. Receiver now inverts coefficients once per frame and uses table-driven GF(256); active Release assertions prove every 1..16 consecutive erasure case on 64x1400-byte shards under 50 ms."}

View file

@ -93,8 +93,12 @@ accepted_design:
Mimir's OBS receiver owns the matching bounded matrix recovery before the
assembly deadline. V2 stripe parity is never reinterpreted as V3. Native
receiver tests prove consecutive bursts from one through sixteen shards,
Odin's 180 Muninn tests pass, and the first direct Raven LAN field minute
at a 250 ms recovery deadline had no video assembly expiry or A/V failure.
including production-sized 64 by 1400-byte frames under a 50 ms per-frame
recovery bound. Odin's 180 Muninn tests pass. A per-byte elimination draft
was rejected after one recovery stalled the receiver thread; production
inverts the coefficient matrix once per frame and uses table-driven field
arithmetic. The fresh direct Raven LAN session has no video assembly expiry
or A/V failure in its initial minute at a 250 ms recovery deadline.
Normal frames are forwarded immediately; the recovery deadline is not a
playout buffer. The ten-minute and impaired acceptance gates remain open.
- Muninn now closes receiver congestion pressure into the live NVENC context.