Bound Muninn video FEC decode time
This commit is contained in:
parent
afce0245b1
commit
e9e7eaae30
4 changed files with 84 additions and 36 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue