From fe495011fa86188087df147ba26fcd418dad5731 Mon Sep 17 00:00:00 2001 From: joseph calderon Date: Sat, 13 Jun 2026 22:45:23 -0700 Subject: [PATCH 1/3] basic rateless codec support --- examples/normRatelessTest.cpp | 88 +++++++++++++++++++ include/normApi.h | 25 ++++++ include/normEncoder.h | 15 ++++ include/normMessage.h | 119 +++++++++++++++++++++++-- include/normSegment.h | 6 +- include/normSession.h | 37 +++++++- makefiles/Makefile.common | 10 ++- src/common/normApi.cpp | 30 +++++++ src/common/normNode.cpp | 159 ++++++++++++++++++++-------------- src/common/normObject.cpp | 57 ++++++++++-- src/common/normSegment.cpp | 30 ++++++- src/common/normSession.cpp | 61 +++++++++++-- 12 files changed, 545 insertions(+), 92 deletions(-) create mode 100644 examples/normRatelessTest.cpp diff --git a/examples/normRatelessTest.cpp b/examples/normRatelessTest.cpp new file mode 100644 index 0000000..ba4b8a8 --- /dev/null +++ b/examples/normRatelessTest.cpp @@ -0,0 +1,88 @@ +#include +#include +#include +#include +#include + +class MockRatelessEncoder : public NormEncoder { +public: + virtual bool Init(unsigned int numData, unsigned int numParity, UINT16 vectorSize) { + ndata = numData; + vec_size = vectorSize; + return true; + } + virtual void Destroy() {} + virtual void Encode(unsigned int segmentId, const char* dataVector, char** parityVectorList) {} + virtual bool IsRateless() const { return true; } + +private: + unsigned int ndata; + UINT16 vec_size; +}; + +class MockRatelessDecoder : public NormDecoder { +public: + virtual bool Init(unsigned int numData, unsigned int numParity, UINT16 vectorSize) { + ndata = numData; + vec_size = vectorSize; + return true; + } + virtual void Destroy() {} + virtual int Decode(char** vectorList, unsigned int numData, unsigned int erasureCount, unsigned int* erasureLocs) { + return erasureCount; + } + +private: + unsigned int ndata; + UINT16 vec_size; +}; + +extern "C" { + NormEncoder* CreateMockEncoder() { return new MockRatelessEncoder(); } + NormDecoder* CreateMockDecoder() { return new MockRatelessDecoder(); } +} + +int main(int argc, char* argv[]) { + NormInstanceHandle instance = NormCreateInstance(); + if (!instance) return -1; + + NormSessionHandle session = NormCreateSession(instance, "127.0.0.1", 6003, 1); + + // Register Rateless Coder (FEC ID 131) + if (!NormRegisterFecCoder(session, 131, CreateMockEncoder, CreateMockDecoder, true)) { + fprintf(stderr, "Failed to register custom FEC codec.\n"); + return -1; + } + + NormSetRxPortReuse(session, true); + NormSetMulticastLoopback(session, true); + + // Start Sender using FEC 131 + if (!NormStartSender(session, 1, 1024*1024, 1024, 64, 0, 131)) { + fprintf(stderr, "Failed to start sender.\n"); + return -1; + } + + printf("Sender started with mock rateless codec. Transmitting...\n"); + + NormObjectHandle obj = NormDataEnqueue(session, "testdata", 8); + + if (obj) { + bool running = true; + while (running) { + NormEvent event; + if (NormGetNextEvent(instance, &event)) { + if (event.type == NORM_TX_FLUSH_COMPLETED) { + printf("Transmission flushed completely.\n"); + running = false; + } + } + } + } + + NormStopSender(session); + NormDestroyInstance(instance); + + printf("Success.\n"); + return 0; +} diff --git a/include/normApi.h b/include/normApi.h index 0caa5c3..e29b8b2 100755 --- a/include/normApi.h +++ b/include/normApi.h @@ -851,6 +851,31 @@ NORM_API_LINKAGE bool NormNodeDenySender(NormNodeId senderId); #ifdef __cplusplus +struct NormFecLayout +{ + UINT16 payloadIdLength; + UINT32 blockMask; + void (*packPayloadId)(UINT32* buffer, UINT32 blockId, UINT16 symbolId, UINT16 blockLen); + UINT32 (*unpackBlockId)(const UINT32* buffer); + UINT16 (*unpackSymbolId)(const UINT32* buffer); + UINT16 (*unpackBlockLength)(const UINT32* buffer); +}; + +class NormEncoder; +class NormDecoder; +typedef NormEncoder* (*NormEncoderFactory)(); +typedef class NormDecoder* (*NormDecoderFactory)(); + +NORM_API_LINKAGE +bool NormRegisterFecCoder(NormSessionHandle sessionHandle, + UINT8 fecId, + NormEncoderFactory encoderFactory, + NormDecoderFactory decoderFactory, + bool isRateless); + +NORM_API_LINKAGE +void NormRegisterFecLayout(NormInstanceHandle instance, UINT8 fecId, const NormFecLayout* layout); + } // end extern "C" #endif /* __cplusplus */ diff --git a/include/normEncoder.h b/include/normEncoder.h index 3c87875..4216198 100755 --- a/include/normEncoder.h +++ b/include/normEncoder.h @@ -35,6 +35,17 @@ #include "protokit.h" // protolib stuff +class NormTelemetryContext +{ + public: + virtual ~NormTelemetryContext() {} + virtual double GetGrtt() const = 0; + virtual double GetTxRate() const = 0; + virtual unsigned int GetGroupSize() const = 0; + virtual double GetCurrentTime() const = 0; +}; + + class NormEncoder { public: @@ -42,6 +53,10 @@ class NormEncoder virtual bool Init(unsigned int numData, unsigned int numParity, UINT16 vectorSize) = 0; virtual void Destroy() = 0; virtual void Encode(unsigned int segmentId, const char *dataVector, char **parityVectorList) = 0; + virtual void EncodeParity(unsigned int parityId, char* parityVector) { (void)parityId; (void)parityVector; } + virtual bool IsRateless() const { return false; } + virtual UINT16 CalculateProactiveParity(unsigned int blockId, UINT16 numData, const NormTelemetryContext& context) { return 0; } + virtual UINT16 CalculateReactiveParity(unsigned int blockId, UINT16 requestedErasures, const NormTelemetryContext& context) { return requestedErasures; } }; // end class NormEncoder class NormDecoder diff --git a/include/normMessage.h b/include/normMessage.h index 954c5e0..ddc8e95 100755 --- a/include/normMessage.h +++ b/include/normMessage.h @@ -3,6 +3,7 @@ // PROTOLIB includes #include "protokit.h" +#include "normApi.h" // standard includes #include // for memcpy(), etc @@ -393,6 +394,8 @@ class NormHeaderExtension // FEC Payload Id content. The FEC Payload // Id format is dependent upon the "fec_id" (FEC Type) // and, in some cases, its field size ("m") parameter +struct NormFecLayout; + class NormPayloadId { public: @@ -400,31 +403,39 @@ class NormPayloadId { RS = 2, // fully-specified, general purpose Reed-Solomon RS8 = 5, // fully-specified 8-bit Reed-Solmon per RFC 5510 - SB = 129 // partially-specified "small block" codes + SB = 129, // partially-specified "small block" codes + RL = 131 // partially-specified "rateless" codes }; - static bool IsValid(UINT8 fecId) + static bool IsValid(UINT8 fecId, const NormFecLayout* layout = NULL) { + if (layout != NULL) + return true; switch (fecId) { case 2: case 5: case 129: + case RL: return true; default: return false; } } + NormPayloadId(UINT8 fecId, UINT8 m, UINT32* theBuffer) : fec_id(fecId), fec_m(m), buffer(theBuffer) {} NormPayloadId(UINT8 fecId, UINT8 m, const UINT32* theBuffer) : fec_id(fecId), fec_m(m), cbuffer(theBuffer) {} - static UINT16 GetLength(UINT8 fecId) + static UINT16 GetLength(UINT8 fecId, const NormFecLayout* layout = NULL) { + if (layout != NULL) + return layout->payloadIdLength; switch (fecId) { case 2: case 5: + case RL: return 4; case 129: return 8; @@ -433,8 +444,10 @@ class NormPayloadId } } - static UINT32 GetFecBlockMask(UINT8 fecId, UINT8 fecM) + static UINT32 GetFecBlockMask(UINT8 fecId, UINT8 fecM, const NormFecLayout* layout = NULL) { + if (layout != NULL) + return layout->blockMask; switch (fecId) { case 2: @@ -446,13 +459,20 @@ class NormPayloadId return 0x00ffffff; // 24-bit blockId case 129: return 0xffffffff; // 32-bit blockId + case RL: + return 0x0000ffff; // 16-bit blockId default: return 0x00000000; // invalid fecId } } - void SetFecPayloadId(UINT32 blockId, UINT16 symbolId, UINT16 blockLen) + void SetFecPayloadId(UINT32 blockId, UINT16 symbolId, UINT16 blockLen, const NormFecLayout* layout = NULL) { + if (layout != NULL && layout->packPayloadId != NULL) + { + layout->packPayloadId(buffer, blockId, symbolId, blockLen); + return; + } switch (fec_id) { case 2: @@ -473,17 +493,30 @@ class NormPayloadId *buffer = htonl(blockId); // 3 + 1 bytes break; case 129: + { *buffer = htonl(blockId); // 4 bytes UINT16* ptr = (UINT16*)(buffer + 1); ptr[0] = htons(blockLen); // 2 bytes ptr[1] = htons(symbolId); // 2 bytes break; + } + case RL: + { + UINT16* payloadId = (UINT16*)buffer; + payloadId[0] = htons(blockId); // 2 bytes + payloadId[1] = htons(symbolId); // 2 bytes + break; + } } } // Message processing methods - NormBlockId GetFecBlockId() const + NormBlockId GetFecBlockId(const NormFecLayout* layout = NULL) const { + if (layout != NULL && layout->unpackBlockId != NULL) + { + return layout->unpackBlockId(cbuffer); + } switch (fec_id) { case 2: @@ -504,14 +537,23 @@ class NormPayloadId } case 129: return ntohl(*cbuffer); + case RL: + { + UINT16* blockId = (UINT16*)cbuffer; + return ntohs(*blockId); + } default: ASSERT(0); return 0; } } - UINT16 GetFecSymbolId() const + UINT16 GetFecSymbolId(const NormFecLayout* layout = NULL) const { + if (layout != NULL && layout->unpackSymbolId != NULL) + { + return layout->unpackSymbolId(cbuffer); + } switch (fec_id) { case 2: @@ -535,14 +577,23 @@ class NormPayloadId UINT16* ptr = (UINT16*)(cbuffer + 1); return ntohs(ptr[1]); } + case RL: + { + UINT16* payloadId = (UINT16*)cbuffer; + return ntohs(payloadId[1]); + } default: ASSERT(0); return 0; } } - UINT16 GetFecBlockLength() const + UINT16 GetFecBlockLength(const NormFecLayout* layout = NULL) const { + if (layout != NULL && layout->unpackBlockLength != NULL) + { + return layout->unpackBlockLength(cbuffer); + } if (129 == fec_id) { UINT16* blockLen = (UINT16*)(cbuffer + 1); @@ -553,7 +604,6 @@ class NormPayloadId return 0; } } - private: @@ -1028,6 +1078,57 @@ class NormFtiExtension129 : public NormHeaderExtension }; }; // end class NormFtiExtension129 +class NormFtiExtension131 : public NormHeaderExtension +{ + public: + virtual void Init(UINT32* theBuffer, UINT16 numBytes) + { + AttachBuffer(theBuffer, numBytes); + SetType(FTI); + SetWords(4); + } + void SetFecInstanceId(UINT16 instanceId) + { ((UINT16*)buffer)[FEC_INSTANCE_OFFSET] = htons(instanceId);} + void SetFecMaxBlockLen(UINT16 ndata) + {((UINT16*)buffer)[FEC_NDATA_OFFSET] = htons(ndata);} + void SetFecNumParity(UINT16 nparity) + {((UINT16*)buffer)[FEC_NPARITY_OFFSET] = htons(nparity);} + void SetSegmentSize(UINT16 segmentSize) + {((UINT16*)buffer)[SEG_SIZE_OFFSET] = htons(segmentSize);} + void SetObjectSize(const NormObjectSize& objectSize) + { + ((UINT16*)buffer)[OBJ_SIZE_MSB_OFFSET] = htons(objectSize.MSB()); + buffer[OBJ_SIZE_LSB_OFFSET] = htonl(objectSize.LSB()); + } + + UINT16 GetFecInstanceId() const + { + return (ntohs(((UINT16*)buffer)[FEC_INSTANCE_OFFSET])); + } + UINT16 GetFecMaxBlockLen() const + {return (ntohs(((UINT16*)buffer)[FEC_NDATA_OFFSET]));} + UINT16 GetFecNumParity() const + {return (ntohs(((UINT16*)buffer)[FEC_NPARITY_OFFSET]));} + UINT16 GetSegmentSize() const + {return (ntohs(((UINT16*)buffer)[SEG_SIZE_OFFSET]));} + NormObjectSize GetObjectSize() const + { + return NormObjectSize(ntohs(((UINT16*)buffer)[OBJ_SIZE_MSB_OFFSET]), + ntohl(buffer[OBJ_SIZE_LSB_OFFSET])); + } + + private: + enum + { + OBJ_SIZE_MSB_OFFSET = (LENGTH_OFFSET + 1)/2, + OBJ_SIZE_LSB_OFFSET = ((OBJ_SIZE_MSB_OFFSET*2)+2)/4, + FEC_INSTANCE_OFFSET = ((OBJ_SIZE_LSB_OFFSET*4)+4)/2, + SEG_SIZE_OFFSET = ((FEC_INSTANCE_OFFSET*2)+2)/2, + FEC_NDATA_OFFSET = ((SEG_SIZE_OFFSET*2)+2)/2, + FEC_NPARITY_OFFSET = ((FEC_NDATA_OFFSET*2)+2)/2 + }; +}; // end class NormFtiExtension131 + class NormInfoMsg : public NormObjectMsg { diff --git a/include/normSegment.h b/include/normSegment.h index cf88173..cb68527 100755 --- a/include/normSegment.h +++ b/include/normSegment.h @@ -238,14 +238,16 @@ class NormBlock NormBlockId finalBlockId, UINT16 finalSegmentSize) const; - bool AppendRepairRequest(NormNackMsg& nack, + bool AppendRepairRequest(class NormNackMsg& nack, UINT8 fecId, UINT8 fecM, UINT16 numData, UINT16 numParity, NormObjectId objectId, bool pendingInfo, - UINT16 payloadMax); + UINT16 payloadMax, + UINT16 fecOverhead = 2, + bool isRateless = false); void SetLastNackTime(const ProtoTime& theTime) diff --git a/include/normSession.h b/include/normSession.h index 38971c6..5ee2d14 100755 --- a/include/normSession.h +++ b/include/normSession.h @@ -4,9 +4,9 @@ #include "normMessage.h" #include "normObject.h" #include "normNode.h" -#include "normEncoder.h" -#include "protokit.h" +#include "normEncoder.h" +#include #include "protoCap.h" // for ProtoCap for ECN_SUPPORT @@ -27,6 +27,10 @@ class NormController { public: virtual ~NormController() {} + NormController() { memset(fec_layouts, 0, sizeof(fec_layouts)); } + + void SetFecLayout(UINT8 id, const NormFecLayout* layout) { fec_layouts[id] = layout; } + const NormFecLayout* GetFecLayout(UINT8 id) const { return fec_layouts[id]; } enum Event { EVENT_INVALID = 0, @@ -68,6 +72,7 @@ class NormController class NormNode* node, class NormObject* object) = 0; + const NormFecLayout* fec_layouts[256]; }; // end class NormController class NormSessionMgr @@ -123,10 +128,13 @@ class NormSessionMgr }; // end class NormSessionMgr +typedef class NormEncoder* (*NormEncoderFactory)(); +typedef class NormDecoder* (*NormDecoderFactory)(); -class NormSession +class NormSession : public NormTelemetryContext { friend class NormSessionMgr; + public: enum {DEFAULT_MESSAGE_POOL_DEPTH = 16}; @@ -465,6 +473,13 @@ class NormSession void SenderSetExtraParity(UINT16 extraParity) {extra_parity = extraParity;} + void RegisterFecCoder(UINT8 fecId, NormEncoderFactory encoderFactory, NormDecoderFactory decoderFactory, bool isRateless); + NormEncoderFactory GetEncoderFactory(UINT8 fecId) const; + NormDecoderFactory GetDecoderFactory(UINT8 fecId) const; + bool IsRatelessFec(UINT8 fecId) const { return is_rateless_codec[fecId]; } + + NormEncoder* GetEncoder() const { return encoder; } + INT32 Difference(NormBlockId a, NormBlockId b) const {return NormBlockId::Difference(a, b, fec_block_mask);} int Compare(NormBlockId a, NormBlockId b) const @@ -521,8 +536,21 @@ class NormSession void SenderSetFtiMode(FtiMode ftiMode) {fti_mode = ftiMode;} + void SetReceiverMaxDelay(unsigned int msec) + {rcvr_max_delay = msec;} + unsigned int GetReceiverMaxDelay() const + {return rcvr_max_delay;} + + // NormTelemetryContext Implementation + double GetGrtt() const override { return SenderGrtt(); } + double GetTxRate() const override { return tx_rate; } + virtual unsigned int GetGroupSize() const override { return (unsigned int)((NormNodeList*)&cc_node_list)->GetCount(); } + double GetCurrentTime() const override { ProtoTime t; t.GetCurrentTime(); return t.GetValue(); } + void SenderEncode(unsigned int segmentId, const char* segment, char** parityVectorList) {encoder->Encode(segmentId, segment, parityVectorList);} + void SenderEncodeParity(unsigned int parityId, char* parityVector) + {encoder->EncodeParity(parityId, parityVector);} NormBlock* SenderGetFreeBlock(NormObjectId objectId, NormBlockId blockId); @@ -789,6 +817,9 @@ class NormSession NormBlockPool block_pool; NormSegmentPool segment_pool; NormEncoder* encoder; + NormEncoderFactory encoder_factories[256]; + NormDecoderFactory decoder_factories[256]; + bool is_rateless_codec[256]; UINT8 fec_id; UINT8 fec_m; INT32 fec_block_mask; diff --git a/makefiles/Makefile.common b/makefiles/Makefile.common index fb18d78..c747a78 100755 --- a/makefiles/Makefile.common +++ b/makefiles/Makefile.common @@ -234,6 +234,15 @@ normClient: $(CLIENT_OBJ) libnorm.a $(LIBPROTO) $(CC) $(CFLAGS) -o $@ $(CLIENT_OBJ) $(LDFLAGS) libnorm.a $(LIBPROTO) $(LIBS) mkdir -p ../bin cp $@ ../bin/$@ + +# (normRatelessTest) rateless fec tester +RATELESS_SRC = $(EXAMPLE)/normRatelessTest.cpp +RATELESS_OBJ = $(RATELESS_SRC:.cpp=.o) + +normRatelessTest: $(RATELESS_OBJ) libnorm.a $(LIBPROTO) + $(CC) $(CFLAGS) -o $@ $(RATELESS_OBJ) $(LDFLAGS) libnorm.a $(LIBPROTO) $(LIBS) + mkdir -p ../bin + cp $@ ../bin/$@ @@ -308,4 +317,3 @@ distclean: clean # DO NOT DELETE THIS LINE -- mkdep uses it. # DO NOT PUT ANYTHING AFTER THIS LINE, IT WILL GO AWAY. - diff --git a/src/common/normApi.cpp b/src/common/normApi.cpp index a151e94..2fc8146 100755 --- a/src/common/normApi.cpp +++ b/src/common/normApi.cpp @@ -1676,6 +1676,36 @@ void NormSetAutoParity(NormSessionHandle sessionHandle, unsigned char autoParity } } // end NormSetAutoParity() +NORM_API_LINKAGE +bool NormRegisterFecCoder(NormSessionHandle sessionHandle, + UINT8 fecId, + NormEncoderFactory encoderFactory, + NormDecoderFactory decoderFactory, + bool isRateless) +{ + NormInstance* instance = NormInstance::GetInstanceFromSession(sessionHandle); + if (instance && instance->dispatcher.SuspendThread()) + { + NormSession* session = (NormSession*)sessionHandle; + if (session) + { + session->RegisterFecCoder(fecId, encoderFactory, decoderFactory, isRateless); + instance->dispatcher.ResumeThread(); + return true; + } + instance->dispatcher.ResumeThread(); + } + return false; +} // end NormRegisterFecCoder() + +NORM_API_LINKAGE +void NormRegisterFecLayout(NormInstanceHandle instanceHandle, UINT8 fecId, const NormFecLayout* layout) +{ + NormInstance* instance = (NormInstance*)instanceHandle; + if (instance) + instance->SetFecLayout(fecId, layout); +} // end NormRegisterFecLayout() + NORM_API_LINKAGE void NormSetGrttEstimate(NormSessionHandle sessionHandle, double grttEstimate) diff --git a/src/common/normNode.cpp b/src/common/normNode.cpp index 63619f0..34e8c56 100755 --- a/src/common/normNode.cpp +++ b/src/common/normNode.cpp @@ -287,72 +287,85 @@ bool NormSenderNode::AllocateBuffers(unsigned int bufferSpace, if (0 != numParity) { - switch (fecId) + NormDecoderFactory decoderFactory = session.GetDecoderFactory(fecId); + if (NULL != decoderFactory) { - case 2: - if (8 == fecM) - { - if (NULL == (decoder = new NormDecoderRS8)) - { - PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() new NormDecoderRS8 error: %s\n", GetErrorString()); - Close(); - return false; - } - } - else if (16 == fecM) - { - if (NULL == (decoder = new NormDecoderRS16)) - { - PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() new NormDecoderRS16 error: %s\n", GetErrorString()); - Close(); - return false; - } - } - else - { - PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() error: unsupported fecId=2 'm' value %d!\n", fecM); - Close(); - return false; - } - break; - case 5: - if (NULL == (decoder = new NormDecoderRS8)) - { - PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() new NormDecoderRS8 error: %s\n", GetErrorString()); - Close(); - return false; - } - break; - case 129: -#ifdef ASSUME_MDP_FEC - if (NULL == (decoder = new NormDecoderMDP)) - { - PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() new NormDecoderMDP error: %s\n", GetErrorString()); - Close(); - return false; - } -#else - if (0 == fecInstanceId) - { - if (NULL == (decoder = new NormDecoderRS8)) - { - PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() new NormDecoderRS8 error: %s\n", GetErrorString()); - Close(); - return false; - } - } - else - { - PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() error: unknown fecId=129 instanceId!\n"); - Close(); - return false; - } -#endif // if/else ASSUME_MDP_FEC - break; - default: - PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() error: unknown fecId>%d!\n", fecId); + if (NULL == (decoder = decoderFactory())) + { + PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() new custom decoder error\n"); Close(); - return false; + return false; + } + } + else + { + switch (fecId) + { + case 2: + if (8 == fecM) + { + if (NULL == (decoder = new NormDecoderRS8)) + { + PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() new NormDecoderRS8 error: %s\n", GetErrorString()); + Close(); + return false; + } + } + else if (16 == fecM) + { + if (NULL == (decoder = new NormDecoderRS16)) + { + PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() new NormDecoderRS16 error: %s\n", GetErrorString()); + Close(); + return false; + } + } + else + { + PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() error: unsupported fecId=2 'm' value %d!\n", fecM); + Close(); + return false; + } + break; + case 5: + if (NULL == (decoder = new NormDecoderRS8)) + { + PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() new NormDecoderRS8 error: %s\n", GetErrorString()); + Close(); + return false; + } + break; + case 129: + #ifdef ASSUME_MDP_FEC + if (NULL == (decoder = new NormDecoderMDP)) + { + PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() new NormDecoderMDP error: %s\n", GetErrorString()); + Close(); + return false; + } + #else + if (0 == fecInstanceId) + { + if (NULL == (decoder = new NormDecoderRS8)) + { + PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() new NormDecoderRS8 error: %s\n", GetErrorString()); + Close(); + return false; + } + } + else + { + PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() error: unknown fecId=129 instanceId!\n"); + Close(); + return false; + } + #endif // if/else ASSUME_MDP_FEC + break; + default: + PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() error: unknown fecId>%d!\n", fecId); + Close(); + return false; + } } if (!decoder->Init(numData, numParity, segmentSize+NormDataMsg::GetStreamPayloadHeaderLength())) { @@ -1431,6 +1444,24 @@ bool NormSenderNode::GetFtiData(const NormObjectMsg& msg, NormFtiData& ftiData) } break; } + case 131: + { + NormFtiExtension131 fti; + while (msg.GetNextExtension(fti)) + { + if (NormHeaderExtension::FTI == fti.GetType()) + { + ftiData.SetFecInstanceId(fti.GetFecInstanceId()); + ftiData.SetFecFieldSize(16); + ftiData.SetSegmentSize(fti.GetSegmentSize()); + ftiData.SetFecMaxBlockLen(fti.GetFecMaxBlockLen()); + ftiData.SetFecNumParity(fti.GetFecNumParity()); + ftiData.SetObjectSize(fti.GetObjectSize()); + return true; + } + } + break; + } default: PLOG(PL_ERROR, "NormSenderNode::GetFtiData() node>%lu sender>%lu unknown fec_id type:%d\n", (unsigned long)LocalNodeId(), (unsigned long)GetId(), (int)fecId); diff --git a/src/common/normObject.cpp b/src/common/normObject.cpp index fdda3da..eca1717 100755 --- a/src/common/normObject.cpp +++ b/src/common/normObject.cpp @@ -1305,11 +1305,13 @@ bool NormObject::AppendRepairRequest(NormNackMsg& nack, requestAppended = true; } bool blockRequestAppended; + bool isRateless = session.IsRatelessFec(fec_id); if (flush || (nextId != max_pending_block)) { blockRequestAppended = block->AppendRepairRequest(nack, fec_id, fec_m, numData, nparity, - transport_id, pending_info, payloadMax); + transport_id, pending_info, payloadMax, + 2, isRateless); } else { @@ -1317,13 +1319,15 @@ bool NormObject::AppendRepairRequest(NormNackMsg& nack, { blockRequestAppended = block->AppendRepairRequest(nack, fec_id, fec_m, max_pending_segment, 0, - transport_id, pending_info, payloadMax); + transport_id, pending_info, payloadMax, + 2, isRateless); } else { blockRequestAppended = block->AppendRepairRequest(nack, fec_id, fec_m, numData, nparity, - transport_id, pending_info, payloadMax); + transport_id, pending_info, payloadMax, + 2, isRateless); } } if (blockRequestAppended) @@ -1850,6 +1854,17 @@ bool NormObject::NextSenderMsg(NormObjectMsg* msg) fti.SetFecNumParity(nparity); break; } + case 131: // RL + { + NormFtiExtension131 fti; + msg->AttachExtension(fti); + fti.SetObjectSize(object_size); + fti.SetFecInstanceId(0); + fti.SetSegmentSize(segment_size); + fti.SetFecMaxBlockLen(ndata); + fti.SetFecNumParity(nparity); + break; + } default: ASSERT(0); return false; @@ -1907,7 +1922,8 @@ bool NormObject::NextSenderMsg(NormObjectMsg* msg) } // Load block with zero initialized parity segments UINT16 totalBlockLen = numData + nparity; - for (UINT16 i = numData; i < totalBlockLen; i++) + bool doPreallocate = (!session.GetEncoder() || !session.GetEncoder()->IsRateless()); + for (UINT16 i = numData; doPreallocate && (i < totalBlockLen); i++) { char* s = session.SenderGetFreeSegment(transport_id, blockId); if (s) @@ -1927,7 +1943,11 @@ bool NormObject::NextSenderMsg(NormObjectMsg* msg) return false; } } - block->TxInit(blockId, numData, session.SenderAutoParity()); + UINT16 autoParity = session.SenderAutoParity(); + if (session.GetEncoder() && session.GetEncoder()->IsRateless()) { + autoParity += session.GetEncoder()->CalculateProactiveParity(blockId.GetValue(), numData, session); + } + block->TxInit(blockId, numData, autoParity); //if (blockId < max_pending_block) if (Compare(blockId, max_pending_block) < 0) block->SetFlag(NormBlock::IN_REPAIR); @@ -2060,6 +2080,28 @@ bool NormObject::NextSenderMsg(NormObjectMsg* msg) CalculateBlockParity(block); } char* segment = block->GetSegment(segmentId); + if (session.GetEncoder() && session.GetEncoder()->IsRateless()) + { + if (NULL == segment) + { + segment = session.SenderGetFreeSegment(transport_id, blockId); + if (segment) + { + UINT16 payloadMax = segment_size + NormDataMsg::GetStreamPayloadHeaderLength(); +#ifdef SIMULATE + payloadMax = MIN(payloadMax, SIM_PAYLOAD_MAX); +#endif // SIMULATE + memset(segment, 0, payloadMax); + session.SenderEncodeParity(segmentId - numData, segment); + block->AttachSegment(segmentId, segment); + } + else + { + PLOG(PL_INFO, "NormObject::NextSenderMsg() node>%lu warning: sender resource constrained (no free segments for on-demand parity).\n", (unsigned long)LocalNodeId()); + return false; + } + } + } ASSERT(NULL != segment); // We only need to send FEC content to cover the biggest segment // sent for the block. @@ -2202,7 +2244,7 @@ NormBlockId NormStreamObject::RepairWindowLo() const bool NormObject::CalculateBlockParity(NormBlock* block) { - if (0 == nparity) return true; + if (0 == nparity && (!session.GetEncoder() || !session.GetEncoder()->IsRateless())) return true; char buffer[NormMsg::MAX_SIZE]; UINT16 numData = GetBlockSize(block->GetId()); for (UINT16 i = 0; i < numData; i++) @@ -2238,7 +2280,8 @@ NormBlock* NormObject::SenderRecoverBlock(NormBlockId blockId) block->TxRecover(blockId, numData, nparity); // Fill block with zero initialized parity segments UINT16 totalBlockLen = numData + nparity; - for (UINT16 i = numData; i < totalBlockLen; i++) + bool doPreallocate = (!session.GetEncoder() || !session.GetEncoder()->IsRateless()); + for (UINT16 i = numData; doPreallocate && (i < totalBlockLen); i++) { char* s = session.SenderGetFreeSegment(transport_id, blockId); if (s) diff --git a/src/common/normSegment.cpp b/src/common/normSegment.cpp index e2a2fb7..d31a2da 100755 --- a/src/common/normSegment.cpp +++ b/src/common/normSegment.cpp @@ -1,4 +1,5 @@ #include "normSegment.h" +#include "normEncoder.h" NormSegmentPool::NormSegmentPool() : seg_size(0), seg_count(0), seg_total(0), seg_list(NULL), seg_pool(NULL), @@ -528,8 +529,35 @@ bool NormBlock::AppendRepairRequest(NormNackMsg& nack, UINT16 numParity, NormObjectId objectId, bool pendingInfo, - UINT16 payloadMax) + UINT16 payloadMax, + UINT16 fecOverhead, + bool isRateless) { + if (isRateless) + { + int needed = (int)erasure_count - (int)parity_count + (int)fecOverhead; + if (needed <= 0) return false; + + UINT16 needed_clamped = (needed > 65535) ? 65535 : (UINT16)needed; + + NormRepairRequest req; + nack.AttachRepairRequest(req, payloadMax); + req.SetForm(NormRepairRequest::ERASURES); + req.SetFlag(NormRepairRequest::SEGMENT); + if (pendingInfo) req.SetFlag(NormRepairRequest::INFO); + + if (req.AppendErasureCount(fecId, fecM, objectId, blk_id, numData, needed_clamped)) + { + if (0 == nack.PackRepairRequest(req)) + { + PLOG(PL_WARN, "NormBlock::AppendRepairRequest() warning: full NACK msg\n"); + return false; + } + return true; + } + return false; + } + bool requestAppended = false; NormSegmentId nextId = 0; NormSegmentId endId; diff --git a/src/common/normSession.cpp b/src/common/normSession.cpp index 6c73855..c06edd5 100755 --- a/src/common/normSession.cpp +++ b/src/common/normSession.cpp @@ -133,6 +133,11 @@ NormSession::NormSession(NormSessionMgr &sessionMgr, NormNodeId localNodeId) user_timer.SetListener(this, &NormSession::OnUserTimeout); user_timer.SetInterval(0.0); user_timer.SetRepeat(0); + + memset(encoder_factories, 0, sizeof(encoder_factories)); + memset(decoder_factories, 0, sizeof(decoder_factories)); + memset(is_rateless_codec, 0, sizeof(is_rateless_codec)); + is_rateless_codec[NormPayloadId::RL] = true; // Built-in RL codec } NormSession::~NormSession() @@ -147,6 +152,23 @@ NormSession::~NormSession() Close(); } +void NormSession::RegisterFecCoder(UINT8 fecId, NormEncoderFactory encoderFactory, NormDecoderFactory decoderFactory, bool isRateless) +{ + encoder_factories[fecId] = encoderFactory; + decoder_factories[fecId] = decoderFactory; + is_rateless_codec[fecId] = isRateless; +} + +NormEncoderFactory NormSession::GetEncoderFactory(UINT8 fecId) const +{ + return encoder_factories[fecId]; +} + +NormDecoderFactory NormSession::GetDecoderFactory(UINT8 fecId) const +{ + return decoder_factories[fecId]; +} + bool NormSession::Open() { ASSERT(address.IsValid()); @@ -836,7 +858,18 @@ bool NormSession::StartSender(UINT16 instanceId, if (NULL != encoder) delete encoder; - if (blockSize <= 255) + NormEncoderFactory encoderFactory = GetEncoderFactory(fecId); + if (NULL != encoderFactory) + { + if (NULL == (encoder = encoderFactory())) + { + PLOG(PL_FATAL, "NormSession::StartSender() new custom encoder error\n"); + StopSender(); + return false; + } + fec_id = fecId; + } + else if (blockSize <= 255) { #ifdef ASSUME_MDP_FEC if (NULL == (encoder = new NormEncoderMDP)) @@ -4036,6 +4069,15 @@ void NormSession::SenderHandleNackMessage(const struct timeval ¤tTime, Nor } break; case SEGMENT: + { + UINT16 numErasuresVal = 0; + if (NormRepairRequest::ERASURES == requestForm) + { + numErasuresVal = nextSegmentId; + if (numErasuresVal > nparity) numErasuresVal = nparity; + nextSegmentId = ndata; + lastSegmentId = ndata; + } PLOG(PL_DETAIL, "NormSession::SenderHandleNackMessage(SEGMENT) obj>%hu blk>%lu segs>%hu:%hu\n", (UINT16)nextObjectId, (unsigned long)nextBlockId.GetValue(), (UINT16)nextSegmentId, (UINT16)lastSegmentId); @@ -4098,12 +4140,13 @@ void NormSession::SenderHandleNackMessage(const struct timeval ¤tTime, Nor { // mark nack time for potential flow control static_cast(object)->SetLastNackTime(nextBlockId, ProtoTime(currentTime)); - if (nextSegmentId < ndata) + if ((nextSegmentId < ndata) || (NormRepairRequest::ERASURES == requestForm)) { bool attemptLock = true; - NormSegmentId firstLockId = nextSegmentId; + NormSegmentId firstLockId = (NormRepairRequest::ERASURES == requestForm) ? 0 : nextSegmentId; NormSegmentId lastLockId = ndata - 1; - lastLockId = MIN(lastLockId, lastSegmentId); + if (NormRepairRequest::ERASURES != requestForm) + lastLockId = MIN(lastLockId, lastSegmentId); if (holdoff) { if (nextObjectId == txObjectIndex) @@ -4164,7 +4207,10 @@ void NormSession::SenderHandleNackMessage(const struct timeval ¤tTime, Nor // With a series of SEGMENT repair requests for a block, "numErasures" will // eventually total the number of missing segments in the block. - numErasures += (lastSegmentId - nextSegmentId + 1); + if (NormRepairRequest::ERASURES == requestForm) + numErasures = numErasuresVal; + else + numErasures += (lastSegmentId - nextSegmentId + 1); if (holdoff) { if (nextObjectId > txObjectIndex) @@ -4238,11 +4284,16 @@ void NormSession::SenderHandleNackMessage(const struct timeval ¤tTime, Nor tx_repair_block_min = nextBlockId; tx_repair_segment_min = (nextSegmentId < nextBlockSize) ? nextSegmentId : (nextBlockSize - 1); } + if (GetEncoder() && GetEncoder()->IsRateless()) { + numErasures = GetEncoder()->CalculateReactiveParity(nextBlockId.GetValue(), numErasures, *this); + if (numErasures > nparity) numErasures = nparity; + } block->HandleSegmentRequest(nextSegmentId, lastSegmentId, nextBlockSize, nparity, numErasures); startTimer = true; } // end if/else (holdoff) + } break; case INFO: // We already dealt with INFO request above with respect to initiating repair From c7d943aea82e9f412f1418601bc11fb9d649a77b Mon Sep 17 00:00:00 2001 From: Joseph Calderon Date: Sun, 12 Jul 2026 20:19:06 -0700 Subject: [PATCH 2/3] move fec codec factory registry to NormSessionMgr --- include/normSession.h | 48 ++++++++++++++++++++++---------------- src/common/normApi.cpp | 2 +- src/common/normSession.cpp | 33 +++++++++----------------- 3 files changed, 40 insertions(+), 43 deletions(-) diff --git a/include/normSession.h b/include/normSession.h index 5ee2d14..a27350c 100755 --- a/include/normSession.h +++ b/include/normSession.h @@ -75,6 +75,9 @@ class NormController const NormFecLayout* fec_layouts[256]; }; // end class NormController +typedef class NormEncoder* (*NormEncoderFactory)(); +typedef class NormDecoder* (*NormDecoderFactory)(); + class NormSessionMgr { friend class NormSession; @@ -116,20 +119,27 @@ class NormSessionMgr {data_free_func = freeFunc;} NormDataObject::DataFreeFunctionHandle GetDataFreeFunction() const {return data_free_func;} - - private: - ProtoTimerMgr& timer_mgr; - ProtoSocket::Notifier& socket_notifier; - ProtoChannel::Notifier* channel_notifier; - NormController* controller; - NormDataObject::DataFreeFunctionHandle data_free_func; - - class NormSession* top_session; // top of NormSession list - -}; // end class NormSessionMgr -typedef class NormEncoder* (*NormEncoderFactory)(); -typedef class NormDecoder* (*NormDecoderFactory)(); + // FEC codec factory registry (shared by all sessions this manager owns) + void RegisterFecCoder(UINT8 fecId, NormEncoderFactory encoderFactory, NormDecoderFactory decoderFactory, bool isRateless); + NormEncoderFactory GetEncoderFactory(UINT8 fecId) const {return encoder_factories[fecId];} + NormDecoderFactory GetDecoderFactory(UINT8 fecId) const {return decoder_factories[fecId];} + bool IsRatelessFec(UINT8 fecId) const {return is_rateless_codec[fecId];} + + private: + ProtoTimerMgr& timer_mgr; + ProtoSocket::Notifier& socket_notifier; + ProtoChannel::Notifier* channel_notifier; + NormController* controller; + NormDataObject::DataFreeFunctionHandle data_free_func; + + class NormSession* top_session; // top of NormSession list + + NormEncoderFactory encoder_factories[256]; + NormDecoderFactory decoder_factories[256]; + bool is_rateless_codec[256]; + +}; // end class NormSessionMgr class NormSession : public NormTelemetryContext { @@ -473,10 +483,11 @@ class NormSession : public NormTelemetryContext void SenderSetExtraParity(UINT16 extraParity) {extra_parity = extraParity;} - void RegisterFecCoder(UINT8 fecId, NormEncoderFactory encoderFactory, NormDecoderFactory decoderFactory, bool isRateless); - NormEncoderFactory GetEncoderFactory(UINT8 fecId) const; - NormDecoderFactory GetDecoderFactory(UINT8 fecId) const; - bool IsRatelessFec(UINT8 fecId) const { return is_rateless_codec[fecId]; } + // FEC codec factories are registered/owned by the NormSessionMgr so a + // single registry is shared across all sessions of a NORM API instance. + NormEncoderFactory GetEncoderFactory(UINT8 fecId) const {return session_mgr.GetEncoderFactory(fecId);} + NormDecoderFactory GetDecoderFactory(UINT8 fecId) const {return session_mgr.GetDecoderFactory(fecId);} + bool IsRatelessFec(UINT8 fecId) const {return session_mgr.IsRatelessFec(fecId);} NormEncoder* GetEncoder() const { return encoder; } @@ -817,9 +828,6 @@ class NormSession : public NormTelemetryContext NormBlockPool block_pool; NormSegmentPool segment_pool; NormEncoder* encoder; - NormEncoderFactory encoder_factories[256]; - NormDecoderFactory decoder_factories[256]; - bool is_rateless_codec[256]; UINT8 fec_id; UINT8 fec_m; INT32 fec_block_mask; diff --git a/src/common/normApi.cpp b/src/common/normApi.cpp index 2fc8146..29b071b 100755 --- a/src/common/normApi.cpp +++ b/src/common/normApi.cpp @@ -1689,7 +1689,7 @@ bool NormRegisterFecCoder(NormSessionHandle sessionHandle, NormSession* session = (NormSession*)sessionHandle; if (session) { - session->RegisterFecCoder(fecId, encoderFactory, decoderFactory, isRateless); + session->GetSessionMgr().RegisterFecCoder(fecId, encoderFactory, decoderFactory, isRateless); instance->dispatcher.ResumeThread(); return true; } diff --git a/src/common/normSession.cpp b/src/common/normSession.cpp index c06edd5..d4ce9ca 100755 --- a/src/common/normSession.cpp +++ b/src/common/normSession.cpp @@ -133,11 +133,6 @@ NormSession::NormSession(NormSessionMgr &sessionMgr, NormNodeId localNodeId) user_timer.SetListener(this, &NormSession::OnUserTimeout); user_timer.SetInterval(0.0); user_timer.SetRepeat(0); - - memset(encoder_factories, 0, sizeof(encoder_factories)); - memset(decoder_factories, 0, sizeof(decoder_factories)); - memset(is_rateless_codec, 0, sizeof(is_rateless_codec)); - is_rateless_codec[NormPayloadId::RL] = true; // Built-in RL codec } NormSession::~NormSession() @@ -152,23 +147,6 @@ NormSession::~NormSession() Close(); } -void NormSession::RegisterFecCoder(UINT8 fecId, NormEncoderFactory encoderFactory, NormDecoderFactory decoderFactory, bool isRateless) -{ - encoder_factories[fecId] = encoderFactory; - decoder_factories[fecId] = decoderFactory; - is_rateless_codec[fecId] = isRateless; -} - -NormEncoderFactory NormSession::GetEncoderFactory(UINT8 fecId) const -{ - return encoder_factories[fecId]; -} - -NormDecoderFactory NormSession::GetDecoderFactory(UINT8 fecId) const -{ - return decoder_factories[fecId]; -} - bool NormSession::Open() { ASSERT(address.IsValid()); @@ -5841,8 +5819,19 @@ NormSessionMgr::NormSessionMgr(ProtoTimerMgr &timerMgr, : timer_mgr(timerMgr), socket_notifier(socketNotifier), channel_notifier(channelNotifier), controller(NULL), data_free_func(NULL), top_session(NULL) { + memset(encoder_factories, 0, sizeof(encoder_factories)); + memset(decoder_factories, 0, sizeof(decoder_factories)); + memset(is_rateless_codec, 0, sizeof(is_rateless_codec)); + is_rateless_codec[NormPayloadId::RL] = true; // Built-in RL codec } +void NormSessionMgr::RegisterFecCoder(UINT8 fecId, NormEncoderFactory encoderFactory, NormDecoderFactory decoderFactory, bool isRateless) +{ + encoder_factories[fecId] = encoderFactory; + decoder_factories[fecId] = decoderFactory; + is_rateless_codec[fecId] = isRateless; +} // end NormSessionMgr::RegisterFecCoder() + NormSessionMgr::~NormSessionMgr() { Destroy(); From 2d653628ce7849120c53d558c5db106f295d1988 Mon Sep 17 00:00:00 2001 From: Joseph Calderon Date: Sun, 12 Jul 2026 20:19:34 -0700 Subject: [PATCH 3/3] support partial rateless decode with re-NACK + enhance mock --- examples/normRatelessTest.cpp | 283 +++++++++++++++++++++++++++------- include/normEncoder.h | 17 +- include/normSegment.h | 6 + include/normSession.h | 4 +- src/common/normObject.cpp | 118 ++++++++++---- src/common/normSegment.cpp | 14 +- 6 files changed, 348 insertions(+), 94 deletions(-) diff --git a/examples/normRatelessTest.cpp b/examples/normRatelessTest.cpp index ba4b8a8..8921c22 100644 --- a/examples/normRatelessTest.cpp +++ b/examples/normRatelessTest.cpp @@ -1,40 +1,128 @@ #include #include + #include #include #include +#include +#include +#include -class MockRatelessEncoder : public NormEncoder { -public: - virtual bool Init(unsigned int numData, unsigned int numParity, UINT16 vectorSize) { - ndata = numData; - vec_size = vectorSize; - return true; +// FEC ID for the (partially-specified) rateless code family. +static const UINT8 RATELESS_FEC_ID = 131; + +// Deterministic permutation of [0, n) shared by encoder and decoder. +// The seed depends only on "n", so both sides produce the identical ordering. +static void BuildRatelessPermutation(unsigned int* perm, unsigned int n) +{ + for (unsigned int i = 0; i < n; i++) perm[i] = i; + unsigned int state = 0x9e3779b9u ^ n; + for (unsigned int i = n; i > 1; i--) + { + state = state * 1103515245u + 12345u; + unsigned int j = (state >> 8) % i; + unsigned int t = perm[i - 1]; perm[i - 1] = perm[j]; perm[j] = t; } - virtual void Destroy() {} - virtual void Encode(unsigned int segmentId, const char* dataVector, char** parityVectorList) {} - virtual bool IsRateless() const { return true; } +} -private: - unsigned int ndata; - UINT16 vec_size; +class MockRatelessEncoder : public NormEncoder +{ + public: + MockRatelessEncoder() : ndata(0), vec_size(0), perm(NULL) {} + virtual ~MockRatelessEncoder() { Destroy(); } + + virtual bool Init(unsigned int numData, unsigned int /*numParity*/, UINT16 vectorSize) + { + Destroy(); + ndata = numData; + vec_size = vectorSize; + perm = new unsigned int[ndata ? ndata : 1]; + return (NULL != perm); + } + virtual void Destroy() { delete[] perm; perm = NULL; } + + // Block-oriented encode is unused by this rateless surrogate; repair + // symbols are synthesized on demand in EncodeParity() below. + virtual void Encode(unsigned int, const char*, char**) {} + + // Generate the "parityId"-th repair symbol as a copy of a source symbol + // selected by the shared permutation. + virtual void EncodeParity(unsigned int parityId, const char** sourceVectorList, + unsigned int numData, char* parityVector) + { + if (0 == numData) return; + BuildRatelessPermutation(perm, numData); + unsigned int k = perm[parityId % numData]; + if ((k < numData) && (NULL != sourceVectorList[k])) + memcpy(parityVector, sourceVectorList[k], vec_size); + } + + virtual bool IsRateless() const { return true; } + + private: + unsigned int ndata; + UINT16 vec_size; + unsigned int* perm; // scratch buffer sized to the block size }; -class MockRatelessDecoder : public NormDecoder { -public: - virtual bool Init(unsigned int numData, unsigned int numParity, UINT16 vectorSize) { - ndata = numData; - vec_size = vectorSize; - return true; - } - virtual void Destroy() {} - virtual int Decode(char** vectorList, unsigned int numData, unsigned int erasureCount, unsigned int* erasureLocs) { - return erasureCount; - } +class MockRatelessDecoder : public NormDecoder +{ + public: + MockRatelessDecoder() : ndata(0), nparity(0), vec_size(0), perm(NULL) {} + virtual ~MockRatelessDecoder() { Destroy(); } -private: - unsigned int ndata; - UINT16 vec_size; + virtual bool Init(unsigned int numData, unsigned int numParity, UINT16 vectorSize) + { + Destroy(); + ndata = numData; + nparity = numParity; + vec_size = vectorSize; + perm = new unsigned int[ndata ? ndata : 1]; + return (NULL != perm); + } + virtual void Destroy() { delete[] perm; perm = NULL; } + + // Fill erased source symbols using received repair symbols. Returns the + // number of source erasures that could NOT be recovered (0 == success), + // which NORM uses to decide whether to NACK for more repair symbols. + virtual int Decode(char** vectorList, unsigned int numData, + unsigned int erasureCount, unsigned int* erasureLocs) + { + unsigned int total = numData + nparity; + bool* erased = new bool[total]; + for (unsigned int i = 0; i < total; i++) erased[i] = false; + for (unsigned int i = 0; i < erasureCount; i++) + if (erasureLocs[i] < total) erased[erasureLocs[i]] = true; + + unsigned int sourceErasures = 0; + for (unsigned int i = 0; i < numData; i++) + if (erased[i]) sourceErasures++; + + BuildRatelessPermutation(perm, numData); + + unsigned int recovered = 0; + for (unsigned int p = 0; (p < nparity) && (recovered < sourceErasures); p++) + { + unsigned int pos = numData + p; + if (erased[pos] || (NULL == vectorList[pos])) continue; // repair symbol not received + unsigned int k = perm[p % numData]; // source symbol this repair carries + if ((k < numData) && erased[k] && (NULL != vectorList[k])) + { + memcpy(vectorList[k], vectorList[pos], vec_size); + erased[k] = false; + recovered++; + } + } + unsigned int stillMissing = sourceErasures - recovered; + delete[] erased; + return (int)stillMissing; + } + + private: + unsigned int ndata; + unsigned int nparity; + UINT16 vec_size; + unsigned int* perm; }; extern "C" { @@ -42,47 +130,126 @@ extern "C" { NormDecoder* CreateMockDecoder() { return new MockRatelessDecoder(); } } -int main(int argc, char* argv[]) { +int main(int /*argc*/, char* /*argv*/[]) +{ NormInstanceHandle instance = NormCreateInstance(); - if (!instance) return -1; - - NormSessionHandle session = NormCreateSession(instance, "127.0.0.1", 6003, 1); - - // Register Rateless Coder (FEC ID 131) - if (!NormRegisterFecCoder(session, 131, CreateMockEncoder, CreateMockDecoder, true)) { - fprintf(stderr, "Failed to register custom FEC codec.\n"); + if (NORM_INSTANCE_INVALID == instance) return -1; + + if (getenv("NORM_DEBUG") != NULL) NormSetDebugLevel((unsigned int)atoi(getenv("NORM_DEBUG"))); + srand((unsigned int)time(NULL)); + + // Use two sessions with DISTINCT node ids on the same multicast group so the + // receiver treats the sender as a genuine remote peer. (A single session + // acting as both sender and receiver would hear its own NACKs and suppress + // subsequent ones, stalling multi-round rateless repair.) + const char* groupAddr = "224.1.2.3"; + const UINT16 groupPort = 6003; + + NormSessionHandle txSession = NormCreateSession(instance, groupAddr, groupPort, 1); + NormSessionHandle rxSession = NormCreateSession(instance, groupAddr, groupPort, 2); + if ((NORM_SESSION_INVALID == txSession) || (NORM_SESSION_INVALID == rxSession)) return -1; + + // Register the mock rateless codec on both the sender and receiver sessions. + if (!NormRegisterFecCoder(txSession, RATELESS_FEC_ID, CreateMockEncoder, CreateMockDecoder, true) || + !NormRegisterFecCoder(rxSession, RATELESS_FEC_ID, CreateMockEncoder, CreateMockDecoder, true)) + { + fprintf(stderr, "normRatelessTest error: failed to register rateless FEC codec\n"); return -1; } - - NormSetRxPortReuse(session, true); - NormSetMulticastLoopback(session, true); - - // Start Sender using FEC 131 - if (!NormStartSender(session, 1, 1024*1024, 1024, 64, 0, 131)) { - fprintf(stderr, "Failed to start sender.\n"); + + NormSetRxPortReuse(txSession, true); + NormSetRxPortReuse(rxSession, true); + NormSetMulticastLoopback(txSession, true); // let our data reach the rx session on this host + NormSetMulticastLoopback(rxSession, true); // let our NACKs reach the tx session on this host + NormSetTxRate(txSession, 10.0e+06); // 10 Mbps for a quick loopback run + + // Induce loss on the receiver so it must repair via rateless parity. + double lossPct = (getenv("NORM_RX_LOSS") != NULL) ? atof(getenv("NORM_RX_LOSS")) : 10.0; + NormSetRxLoss(rxSession, lossPct); + + if (!NormStartReceiver(rxSession, 1024 * 1024)) + { + fprintf(stderr, "normRatelessTest error: NormStartReceiver() failed\n"); return -1; } - - printf("Sender started with mock rateless codec. Transmitting...\n"); - - NormObjectHandle obj = NormDataEnqueue(session, "testdata", 8); - - if (obj) { - bool running = true; - while (running) { - NormEvent event; - if (NormGetNextEvent(instance, &event)) { - if (event.type == NORM_TX_FLUSH_COMPLETED) { - printf("Transmission flushed completely.\n"); + + // 16 source symbols/block, up to 64 repair symbols/block, 512-byte segments. + if (!NormStartSender(txSession, 1, 1024 * 1024, 512, 16, 64, RATELESS_FEC_ID)) + { + fprintf(stderr, "normRatelessTest error: NormStartSender() failed\n"); + return -1; + } + + // Build a known payload spanning several blocks (incl. a short final block). + const UINT32 dataLen = 20000; + char* txData = new char[dataLen]; + for (UINT32 i = 0; i < dataLen; i++) + txData[i] = (char)((i * 31 + 7) & 0xff); + + NormObjectHandle txObj = NormDataEnqueue(txSession, txData, dataLen); + if (NORM_OBJECT_INVALID == txObj) + { + fprintf(stderr, "normRatelessTest error: NormDataEnqueue() failed\n"); + return -1; + } + printf("normRatelessTest: sending %u bytes via mock rateless codec (10%% loss)...\n", dataLen); + + int result = -1; + // NormGetNextEvent() blocks, so drive it via select() on the NORM descriptor + // to enforce a wall-clock watchdog (the demo must never hang). + NormDescriptor normFd = NormGetDescriptor(instance); + time_t deadline = time(NULL) + 30; + bool running = true; + while (running && (time(NULL) <= deadline)) + { + fd_set fdSet; + FD_ZERO(&fdSet); + FD_SET(normFd, &fdSet); + struct timeval timeout; + timeout.tv_sec = 1; + timeout.tv_usec = 0; + int n = select((int)normFd + 1, &fdSet, NULL, NULL, &timeout); + if (n <= 0) continue; // timeout tick (re-check deadline) or interrupted + + NormEvent event; + while (running && NormGetNextEvent(instance, &event, false)) + { + switch (event.type) + { + case NORM_RX_OBJECT_COMPLETED: + { + unsigned int rxLen = (unsigned int)NormObjectGetSize(event.object); + const char* rxData = NormDataAccessData(event.object); + if ((rxLen == dataLen) && (NULL != rxData) && (0 == memcmp(rxData, txData, dataLen))) + { + printf("normRatelessTest: received %u bytes, contents verified. PASS\n", rxLen); + result = 0; + } + else + { + fprintf(stderr, "normRatelessTest: received object mismatch (len %u vs %u). FAIL\n", + rxLen, dataLen); + } running = false; + break; } + case NORM_RX_OBJECT_ABORTED: + fprintf(stderr, "normRatelessTest error: NORM_RX_OBJECT_ABORTED\n"); + running = false; + break; + default: + break; } } } - - NormStopSender(session); + if (running) + fprintf(stderr, "normRatelessTest error: timed out waiting for object reception\n"); + + NormStopSender(txSession); + NormStopReceiver(rxSession); NormDestroyInstance(instance); - - printf("Success.\n"); - return 0; + delete[] txData; + + printf("normRatelessTest: %s\n", (0 == result) ? "SUCCESS" : "FAILURE"); + return result; } diff --git a/include/normEncoder.h b/include/normEncoder.h index 4216198..bd0ed73 100755 --- a/include/normEncoder.h +++ b/include/normEncoder.h @@ -52,8 +52,13 @@ class NormEncoder virtual ~NormEncoder(); virtual bool Init(unsigned int numData, unsigned int numParity, UINT16 vectorSize) = 0; virtual void Destroy() = 0; - virtual void Encode(unsigned int segmentId, const char *dataVector, char **parityVectorList) = 0; - virtual void EncodeParity(unsigned int parityId, char* parityVector) { (void)parityId; (void)parityVector; } + virtual void Encode(unsigned int segmentId, const char *dataVector, char **parityVectorList) = 0; + // Rateless codes generate parity symbols on demand. Unlike the block-oriented + // Encode() above, EncodeParity() is handed the block's source symbol vectors so a + // fountain/rateless encoder can synthesize the "parityId"-th (innovative) repair + // symbol without having to cache source state across calls. + virtual void EncodeParity(unsigned int parityId, const char** sourceVectorList, unsigned int numData, char* parityVector) + { (void)parityId; (void)sourceVectorList; (void)numData; (void)parityVector; } virtual bool IsRateless() const { return false; } virtual UINT16 CalculateProactiveParity(unsigned int blockId, UINT16 numData, const NormTelemetryContext& context) { return 0; } virtual UINT16 CalculateReactiveParity(unsigned int blockId, UINT16 requestedErasures, const NormTelemetryContext& context) { return requestedErasures; } @@ -65,7 +70,13 @@ class NormDecoder virtual ~NormDecoder(); virtual bool Init(unsigned int numData, unsigned int numParity, UINT16 vectorSize) = 0; virtual void Destroy() = 0; - virtual int Decode(char** vectorList, unsigned int numData, unsigned int erasureCount, unsigned int* erasureLocs) = 0; + // Decode in place, filling any erased source symbol vectors in "vectorList". + // For ideal (MDS) codes (e.g. Reed-Solomon) the return value is unused by the + // receiver, which assumes success whenever it holds >= erasureCount repair symbols. + // For rateless/non-ideal codes the return value MUST be the number of source + // symbol erasures that could NOT be recovered (0 == fully decoded); the receiver + // uses this to keep the block pending and NACK for additional repair symbols. + virtual int Decode(char** vectorList, unsigned int numData, unsigned int erasureCount, unsigned int* erasureLocs) = 0; }; // end class NormDecoder #endif // _NORM_ENCODER diff --git a/include/normSegment.h b/include/normSegment.h index cb68527..2a437c5 100755 --- a/include/normSegment.h +++ b/include/normSegment.h @@ -163,6 +163,7 @@ class NormBlock erasure_count = ndata; parity_count = 0; parity_offset = 0; + decode_overhead = 0; flags = 0; } // Note: This invalidates the repair_mask state. @@ -172,6 +173,10 @@ class NormBlock UINT16 ErasureCount() const {return erasure_count;} void IncrementParityCount() {parity_count++;} UINT16 ParityCount() const {return parity_count;} + // Extra repair symbols a (rateless) receiver needs beyond ErasureCount() because + // prior decode attempts were short of innovative symbols (0 for ideal MDS codes). + void IncrementDecodeOverhead() {decode_overhead++;} + UINT16 DecodeOverhead() const {return decode_overhead;} bool GetFirstPending(NormSymbolId& symbolId) const { @@ -279,6 +284,7 @@ class NormBlock int flags; UINT16 erasure_count; UINT16 parity_count; // how many fresh parity we are currently planning to send + UINT16 decode_overhead; // extra repair symbols a rateless rcvr needs after a short decode UINT16 parity_offset; // offset from where our fresh parity will be sent UINT16 seg_size_max; diff --git a/include/normSession.h b/include/normSession.h index a27350c..bcc0c87 100755 --- a/include/normSession.h +++ b/include/normSession.h @@ -560,8 +560,8 @@ class NormSession : public NormTelemetryContext void SenderEncode(unsigned int segmentId, const char* segment, char** parityVectorList) {encoder->Encode(segmentId, segment, parityVectorList);} - void SenderEncodeParity(unsigned int parityId, char* parityVector) - {encoder->EncodeParity(parityId, parityVector);} + void SenderEncodeParity(unsigned int parityId, const char** sourceVectorList, unsigned int numData, char* parityVector) + {encoder->EncodeParity(parityId, sourceVectorList, numData, parityVector);} NormBlock* SenderGetFreeBlock(NormObjectId objectId, NormBlockId blockId); diff --git a/src/common/normObject.cpp b/src/common/normObject.cpp index eca1717..5fdd6e5 100755 --- a/src/common/normObject.cpp +++ b/src/common/normObject.cpp @@ -1609,42 +1609,66 @@ void NormObject::HandleObjectMessage(const NormObjectMsg& msg, } // end if (nextErasure < numData) } // end if (block->GetFirstPending(nextErasure)) + bool ratelessDecodeIncomplete = false; if (erasureCount) { - sender->Decode(block->SegmentList(), numData, erasureCount); - for (UINT16 i = 0; i < erasureCount; i++) + // The decoder returns the number of source erasures it could NOT + // recover. Ideal (MDS) codes always fully decode here, so their + // return is effectively ignored; rateless/non-ideal codes may need + // more repair symbols than the "ParityCount >= ErasureCount" guard + // above assumes, so we must honor a partial-decode result. + UINT16 unrecovered = sender->Decode(block->SegmentList(), numData, erasureCount); + ratelessDecodeIncomplete = session.IsRatelessFec(fec_id) && (0 != unrecovered); + if (!ratelessDecodeIncomplete) { - NormSegmentId sid = sender->GetErasureLoc(i); - if (sid < numData) + for (UINT16 i = 0; i < erasureCount; i++) { - if (WriteSegment(blockId, sid, block->GetSegment(sid))) + NormSegmentId sid = sender->GetErasureLoc(i); + if (sid < numData) { - objectUpdated = true; - // For statistics only (TBD) #ifdef NORM_DEBUG - // "segmentLength" is not necessarily correct here (TBD - fix this) - sender->IncrementRecvGoodput(segmentLength); - } + if (WriteSegment(blockId, sid, block->GetSegment(sid))) + { + objectUpdated = true; + // For statistics only (TBD) #ifdef NORM_DEBUG + // "segmentLength" is not necessarily correct here (TBD - fix this) + sender->IncrementRecvGoodput(segmentLength); + } + else + { + if (IsStream()) + PLOG(PL_DEBUG, "NormObject::HandleObjectMessage() WriteSegment() error\n"); + else + PLOG(PL_ERROR, "NormObject::HandleObjectMessage() WriteSegment() error\n"); + } + } else { - if (IsStream()) - PLOG(PL_DEBUG, "NormObject::HandleObjectMessage() WriteSegment() error\n"); - else - PLOG(PL_ERROR, "NormObject::HandleObjectMessage() WriteSegment() error\n"); - } - } - else - { - break; + break; + } } } + else + { + // Not enough innovative repair symbols yet. Note the shortfall so + // the receiver's next NACK requests additional parity, and leave the + // block pending (its missing source symbols are still marked pending). + block->IncrementDecodeOverhead(); + PLOG(PL_DEBUG, "NormObject::HandleObjectMessage() node>%lu sender>%lu obj>%hu blk>%lu " + "rateless decode incomplete (%hu source symbol(s) short); awaiting more parity\n", + (unsigned long)LocalNodeId(), (unsigned long)sender->GetId(), + (UINT16)transport_id, (unsigned long)blockId.GetValue(), unrecovered); + } } // Clear any temporarily retrieved segments for the block - for (UINT16 i = 0; i < retrievalCount; i++) + for (UINT16 i = 0; i < retrievalCount; i++) block->DetachSegment(sender->GetRetrievalLoc(i)); - // OK, we're done with this block - pending_mask.Unset(blockId.GetValue()); - block_buffer.Remove(block); - sender->PutFreeBlock(block); + if (!ratelessDecodeIncomplete) + { + // OK, we're done with this block + pending_mask.Unset(blockId.GetValue()); + block_buffer.Remove(block); + sender->PutFreeBlock(block); + } } // if erasureCount <= parityCount (i.e., block complete) // Notify application of new data available // (TBD) this could be improved for stream objects @@ -2068,8 +2092,27 @@ bool NormObject::NextSenderMsg(NormObjectMsg* msg) memset(buffer+payloadLength, 0, payloadMax-payloadLength); // (TBD) the encode routine could update the block's parity readiness block->UpdateSegSizeMax(payloadLength); - session.SenderEncode(segmentId, data->AccessPayload(), block->SegmentList(numData)); - block->IncreaseParityReadiness(); + session.SenderEncode(segmentId, data->AccessPayload(), block->SegmentList(numData)); + block->IncreaseParityReadiness(); + // Rateless codecs synthesize repair symbols on demand from the block's + // source symbols, so retain each source vector in the block as it is sent. + // (This incremental path drives ParityReadiness to completion, after which + // CalculateBlockParity() is skipped, so caching must happen here too.) + if (session.GetEncoder() && session.GetEncoder()->IsRateless() && + (NULL == block->GetSegment(segmentId))) + { + char* srcSeg = session.SenderGetFreeSegment(transport_id, block->GetId()); + if (NULL != srcSeg) + { + memcpy(srcSeg, buffer, payloadMax); + block->AttachSegment(segmentId, srcSeg); + } + else + { + PLOG(PL_INFO, "NormObject::NextSenderMsg() node>%lu warning: no free segment to cache " + "rateless source symbol.\n", (unsigned long)LocalNodeId()); + } + } } } else @@ -2092,7 +2135,7 @@ bool NormObject::NextSenderMsg(NormObjectMsg* msg) payloadMax = MIN(payloadMax, SIM_PAYLOAD_MAX); #endif // SIMULATE memset(segment, 0, payloadMax); - session.SenderEncodeParity(segmentId - numData, segment); + session.SenderEncodeParity(segmentId - numData, (const char**)block->SegmentList(), numData, segment); block->AttachSegment(segmentId, segment); } else @@ -2260,10 +2303,29 @@ bool NormObject::CalculateBlockParity(NormBlock* block) memset(buffer+payloadLength, 0, payloadMax-payloadLength+1); block->UpdateSegSizeMax(payloadLength); session.SenderEncode(i, buffer, block->SegmentList(numData)); + // Rateless codecs synthesize repair symbols on demand (EncodeParity) from + // the block's source symbols, so cache the source vectors in the block. + // (Block codes like Reed-Solomon accumulate parity above and don't need this.) + if (session.GetEncoder() && session.GetEncoder()->IsRateless() && + (NULL == block->GetSegment(i))) + { + char* srcSeg = session.SenderGetFreeSegment(transport_id, block->GetId()); + if (NULL != srcSeg) + { + memcpy(srcSeg, buffer, payloadMax); + block->AttachSegment(i, srcSeg); + } + else + { + PLOG(PL_INFO, "NormObject::CalculateBlockParity() node>%lu warning: no free segment to cache " + "rateless source symbol.\n", (unsigned long)LocalNodeId()); + return false; + } + } } else { - return false; + return false; } } block->SetParityReadiness(numData); diff --git a/src/common/normSegment.cpp b/src/common/normSegment.cpp index d31a2da..10f381e 100755 --- a/src/common/normSegment.cpp +++ b/src/common/normSegment.cpp @@ -208,10 +208,16 @@ bool NormBlock::IsRepairPending(UINT16 numData, UINT16 numParity) } else { + // We need "erasure_count" repair symbols, plus "decode_overhead" more for + // rateless codes whose earlier repair symbols weren't innovative enough to + // decode (decode_overhead is always 0 for ideal MDS codes, preserving the + // original behavior). + UINT16 parityNeed = erasure_count + decode_overhead; + if (parityNeed > numParity) parityNeed = numParity; repair_mask.SetBits(0, numData); - repair_mask.SetBits(numData+erasure_count, numParity-erasure_count); + repair_mask.SetBits(numData+parityNeed, numParity-parityNeed); } - // Calculate repair_mask = pending_mask - repair_mask + // Calculate repair_mask = pending_mask - repair_mask repair_mask.XCopy(pending_mask); return (repair_mask.IsSet()); } // end NormBlock::IsRepairPending() @@ -535,7 +541,9 @@ bool NormBlock::AppendRepairRequest(NormNackMsg& nack, { if (isRateless) { - int needed = (int)erasure_count - (int)parity_count + (int)fecOverhead; + // "decode_overhead" grows when a prior decode attempt fell short of innovative + // symbols, so the receiver keeps asking for more parity until the block decodes. + int needed = (int)erasure_count - (int)parity_count + (int)fecOverhead + (int)decode_overhead; if (needed <= 0) return false; UINT16 needed_clamped = (needed > 65535) ? 65535 : (UINT16)needed;