diff --git a/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/HitMapGenerator.cpp b/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/HitMapGenerator.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..cdcf694ee04e7f21ce01861d3b749b7a5332b0f5
--- /dev/null
+++ b/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/HitMapGenerator.cpp
@@ -0,0 +1,75 @@
+/*
+* Author: Ondra Kovanda, ondrej.kovanda at cern.ch
+* Date: 02/2024
+* Description: Generation of hits for ITkPix* emulation
+*/
+#include "HitMapGenerator.h"
+
+HitMapGenerator::HitMapGenerator(const uint nCol, const uint nRow, const uint nColInCCol, const uint nRowInQRow, const uint seed): m_nCol(nCol), m_nRow(nRow), m_nColInCCol(nColInCCol), m_nRowInQRow(nRowInQRow), m_seed(seed){
+    //initialize the pdfs for assigning a hit and for
+    //the ToT value of that hit
+    m_hitProb = std::uniform_real_distribution<float>(0., 1.);
+    m_totProb = std::uniform_int_distribution<uint16_t>(1, 15); 
+
+    m_nCCol = nCol/m_nColInCCol;
+    m_nQRow = nRow/m_nRowInQRow;
+
+    generator.seed(m_seed);
+
+}
+
+void HitMapGenerator::setSeed(const uint seed){
+    m_seed = seed;
+    generator.seed(m_seed);
+}
+
+void HitMapGenerator::randomQCore(const uint CCol, const uint QRow){
+    //These are the coordinates of the top left corner of the QCore
+    const int m_col = CCol * m_nColInCCol;
+    const int m_row = QRow * m_nRowInQRow;
+
+    //Loop through pixels in that QCore, generate a random hit for each
+    //and store the truth accordingly. Loop "backwards" to get the
+    //truth hits in the same order as the default Yarr decoder
+    for (int pixRow = m_row + m_nRowInQRow -1; pixRow >= m_row; pixRow--){
+        for (int pixCol = m_col + m_nColInCCol -1; pixCol >= m_col; pixCol--){
+            //does the pixel have a hit?
+            float hitprob = m_hitProb(generator);
+            
+            //std::cout << hitprob << " vs occupancy " << m_occupancy << "\n";
+            if (hitprob < m_occupancy){
+                //Then give it a tot!
+                uint16_t tot = m_totProb(generator);
+                m_hitMap(pixCol, pixRow) = tot;//m_totProb(generator);
+                
+                //and save it into the truth output, respecting the
+                //decoder numbering conventions
+                m_truthEvt.addHit(pixRow + 1, pixCol + 1, m_hitMap(pixCol, pixRow) - 1);
+            }
+        }
+    }
+}
+
+
+void HitMapGenerator::randomHitMap(float occupancy){
+    //Generate uniformly random hit map for dev/testing purposes
+    //initialize the hit map with all zeros
+    m_hitMap   = ItkpixLayout<uint16_t>();
+    m_truthEvt = FrontEndEvent(0, 0, m_nGenerated);
+    m_occupancy = occupancy;
+    
+    //Loop over the hit map and fill random tot values.
+    //In order to preserve the order of hits as it emerges
+    //from the decoder & ensure easy comparison, the generation
+    //has to be per-QCore, and keep reverse order of the hits
+    //compared to the encoding recipe in each QCore. This happens
+    //in the randomQCore(...) function
+    for (uint CCol = 0; CCol < m_nCCol; CCol++){
+        for (uint QRow = 0; QRow < m_nQRow; QRow++){
+            randomQCore(CCol, QRow);  
+        }
+        
+    }
+    
+    m_nGenerated++;
+}
\ No newline at end of file
diff --git a/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/HitMapGenerator.h b/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/HitMapGenerator.h
new file mode 100644
index 0000000000000000000000000000000000000000..de1b0e7a1edcf42e51b32650dd6a97ea64f7d35f
--- /dev/null
+++ b/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/HitMapGenerator.h
@@ -0,0 +1,52 @@
+/*
+* Author: Ondra Kovanda, ondrej.kovanda at cern.ch
+* Date: 02/2024
+* Description: Generation of hits for ITkPix* emulation
+*/
+
+#ifndef HITMAPGENERATOR_H
+#define HITMAPGENERATOR_H
+
+#include <vector>
+#include <random>
+#include <iostream>
+#include "EventData.h"
+#include "ItkpixLayout.h"
+
+
+class HitMapGenerator{
+
+    typedef ItkpixLayout<uint16_t> HitMap;
+
+
+    public:
+        HitMapGenerator(const uint nCol = 400, const uint nRow = 384, const uint nColInCCol = 8, const uint nRowInQrow = 2, const uint seed = 0);
+
+        void setSeed(const uint seed = 0);
+
+        void randomHitMap(const float occupancy = 1e-3);
+
+        void randomQCore(const uint CCol, const uint QRow);
+
+        HitMap& outHits(){return m_hitMap;}
+
+        FrontEndEvent& outTruth(){return m_truthEvt;}
+
+    private:
+        //generation machinery
+        std::mt19937 generator;
+        std::uniform_real_distribution<float> m_hitProb;
+        std::uniform_int_distribution<uint16_t>   m_totProb;
+
+
+        //geometry and config
+        uint m_seed, m_nCol, m_nRow, m_nCCol, m_nQRow, m_nColInCCol, m_nRowInQRow;
+        float m_occupancy;
+
+        //output
+        HitMap m_hitMap;
+        FrontEndEvent m_truthEvt;
+        uint m_nGenerated;
+};
+
+#endif
\ No newline at end of file
diff --git a/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/ItkpixEncoder.cpp b/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/ItkpixEncoder.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ff755e0b821a62f1428369c47d8e457e497de8de
--- /dev/null
+++ b/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/ItkpixEncoder.cpp
@@ -0,0 +1,196 @@
+/*
+* Author: Ondra Kovanda, ondrej.kovanda at cern.ch
+* Date: 03/2024
+* Description: ITkPix* encoding base class
+*/
+
+#include "ItkpixEncoder.h"
+#include "ItkpixQCoreEncodingLUT.h"
+
+//Constructor sets up the geometry for all future loops
+
+ItkpixEncoder::ItkpixEncoder(const uint nCol, const uint nRow, const uint nColInCCol, const uint nRowInQRow, const uint nEventsPerStream, const bool plainHitMap, const bool dropToT): m_nCol(nCol), m_nRow(nRow), m_nColInCCol(nColInCCol), m_nRowInQRow(nRowInQRow), m_nEventsPerStream(nEventsPerStream), m_plainHitMap(plainHitMap), m_dropToT(dropToT){
+    m_nCCol = nCol/m_nColInCCol;
+    m_nQRow = nRow/m_nRowInQRow;
+    m_currBlock  = 0x0ULL;
+    m_currBit    = 0;
+    m_currEvent  = 0;
+    m_currStream = 0;
+}
+
+void ItkpixEncoder::addBits64(const uint64_t value, const uint8_t length){
+    //This adds 'length' lowest bits to the current block. If the current
+    //block gets filled, push it to the output and start a new block with
+    //the rest of the bits that didn't make it. Need to keep track of the
+    //remaining space in the current word. Also, we only have 63 bits for
+    //the added data, as the first bit is EoS.
+    //Case 1: there's enough space for the entire information to be added
+    if (length <= (63 - m_currBit)){
+        
+        //The position at which the new bits should be inserted into the block
+        //is (length of the block - 1) - (currently last bit) - (length)
+        //We need to keep the first bit for EoS, hence the -1
+        m_currBlock |= (value << (63 - m_currBit - length));
+        m_currBit += length;
+        return;
+    }
+
+    //Case 2: there's not enough space, so we put what we can, push the block
+    //to the output, and the rest to a new block
+    else {
+
+        //How much space do we have?
+        uint8_t remainingBits = 63 - m_currBit;
+
+        //Add that many bits
+        m_currBlock |= ((value >> (length - remainingBits)));
+
+        //Push the current block to the output and reset it and the current
+        //bit counter. The block needs to be split in 32-bit halves before the output
+        pushWords32();
+
+        //What are we left with?
+        uint8_t leftoverBits = length - remainingBits;
+
+        //Now we can add the remainder
+        //Make sure that the remainder we're adding is really only the bits we've not yet
+        //added, otherwise there can be a rogue "1" in the EoS bit, which was already added
+        //in the previous block
+        addBits64((value & (0xFFFFFFFFFFFFFFFF >> (64 - leftoverBits))), leftoverBits);
+        
+    }
+}
+
+void ItkpixEncoder::pushWords32(){
+    //whenever the current block is ready for output,
+    //split it into two 32-bit words and push them to
+    //the output container. Reset the current bloc/bit
+    uint32_t word1 = m_currBlock >> 32;
+    uint32_t word2 = m_currBlock & 0xFFFFFFFF;
+    m_words.push_back(word1);
+    m_words.push_back(word2);
+    m_currBlock = 0x0ULL;
+    m_currBit   = 0;
+}
+
+void ItkpixEncoder::encodeQCore(const uint nCCol, const uint nQRow){
+    //produce hit map and ToTs
+    //First, get the top-left pixel in the QCore
+    uint m_col = nCCol * m_nColInCCol;
+    uint m_row = nQRow * m_nRowInQRow;
+
+    //now loop, store ToTs, and build index of the
+    //compressed hit map in the LUT
+    uint16_t lutIndex = 0x0000;
+    std::vector<uint16_t> tots;
+    tots.reserve(16);
+    int pix = 0;
+    for (uint pixRow = m_row; pixRow < m_row + m_nRowInQRow; pixRow++){
+        for (uint pixCol = m_col; pixCol < m_col + m_nColInCCol; pixCol++){
+            if (m_hitMap(pixCol, pixRow)){
+                lutIndex |= 0x1 << pix;
+                tots.push_back(m_hitMap(pixCol, pixRow) - 1);
+            }
+            pix++;
+        }
+    }
+
+    //now add the binary-tree encoded & compressed map
+    //from the LUT to the stream. If, instead, the plain
+    //hit map is requested, add the index (which is the
+    //plain hit map in fact)
+    
+    m_plainHitMap ? addBits64(lutIndex, 16) : addBits64(ItkpixEncoding::Itkpixv2QCoreEncodingLUT_Tree[lutIndex], ItkpixEncoding::Itkpixv2QCoreEncodingLUT_Length[lutIndex]);
+
+    //if dropToT is requested, we can return here
+    if (m_dropToT) return;
+
+    //and add the four-bit ToT information for each hit
+    for (auto& tot : tots){
+        addBits64(tot, 4);
+    }
+}
+
+bool ItkpixEncoder::hitInQCore(const uint CCol, const uint QRow){
+    //Was there a hit in this QCore?
+
+    uint m_col = CCol * m_nColInCCol;
+    uint m_row = QRow * m_nRowInQRow;
+
+    for (uint pixRow = m_row; pixRow < m_row + m_nRowInQRow; pixRow++){
+        for (uint pixCol = m_col; pixCol < m_col + m_nColInCCol; pixCol++){
+            if (m_hitMap(pixCol, pixRow)) return true;
+        }
+    }
+
+    return false;
+}
+
+void ItkpixEncoder::scanHitMap(){
+    //Fill in a helper map of hit QCores and a vector of last qrow in each ccol
+    m_hitQCores = std::vector<std::vector<bool>>(m_nCCol, std::vector<bool>(m_nQRow, false));
+    m_lastQRow  = std::vector<uint>(m_nCCol, 0);
+
+    for (uint CCol = 0; CCol < m_nCCol; CCol++){
+        for (uint QRow = 0; QRow < m_nQRow; QRow++){
+            //if there's a hit in the qcore, flag the helper map
+            m_hitQCores[CCol][QRow] = hitInQCore(CCol, QRow);
+            
+            //and keep track of the last qrow in each CCol, so that we can
+            //easily set the isLast bit. Numbering starts at 1, in order to
+            //keep the m_lastQRow[CCol] == 0 case denoting "no hit" in the CCol
+            //to save some looping/ifs later on
+            if (m_hitQCores[CCol][QRow]) m_lastQRow[CCol] = QRow + 1;
+        }
+    }
+
+}
+
+void ItkpixEncoder::encodeEvent(){
+    //This produces the bits for one event.
+    //First, scan the map and produce helpers
+    scanHitMap();
+
+    for (uint CCol = 0; CCol < m_nCCol; CCol++){
+        //if there are no hits in this CCol, continue
+        if (m_lastQRow[CCol] == 0) continue;
+        //add the 6-bit (CCol + 1) address
+        addBits64(CCol + 1, 6);    
+
+        int previousQRow = -666;
+        for (uint QRow = 0; QRow < m_nQRow; QRow++){
+            //if there's no hit in this row, continue
+            if (!m_hitQCores[CCol][QRow]) continue;            
+            
+            //add the isLast bit
+            QRow + 1 == m_lastQRow[CCol] ? addBits64(0x1, 1) : addBits64(0x0, 1);
+
+            //add the isNeighbor bit. If false, add the QRow address as well.
+            if (QRow == previousQRow + 1){
+                addBits64(0x1, 1);
+            }
+            else {
+                addBits64(0x0, 1);
+                addBits64(QRow, 8);
+            };
+
+            //add the map and ToT
+            encodeQCore(CCol, QRow);
+
+            //update the previous QRow
+            previousQRow = QRow;
+        }
+    }    
+}
+
+void ItkpixEncoder::streamTag(const uint8_t nStream){
+    //this adds the 8-bit 'global' stream tag
+    addBits64(nStream, 8);
+}
+
+void ItkpixEncoder::intTag(const uint16_t nEvt){
+    //this adds 11 bits of interal tagging between events.
+    //does the tag always need to start with 111?
+    uint16_t tag = nEvt | (0b111 << 8);
+    addBits64(tag, 11);
+}
\ No newline at end of file
diff --git a/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/ItkpixEncoder.h b/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/ItkpixEncoder.h
new file mode 100644
index 0000000000000000000000000000000000000000..7a856e286078d938f00626abab8ecd936c14b123
--- /dev/null
+++ b/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/ItkpixEncoder.h
@@ -0,0 +1,68 @@
+/*
+* Author: Ondra Kovanda, ondrej.kovanda at cern.ch
+* Date: 03/2024
+* Description: ITkPix* encoding base class
+*/
+
+#ifndef ITKPIXENCODER_H
+#define ITKPIXENCODER_H
+
+#include <vector>
+#include <iostream>
+#include <cstdint>
+#include "ItkpixLayout.h"
+
+class ItkpixEncoder{
+    public:
+        typedef ItkpixLayout<uint16_t> HitMap;
+    
+        ItkpixEncoder(const uint nCol = 400, const uint nRow = 384, const uint nColInCCol = 8, const uint nRowInQRow = 2, const uint nEventsPerStream = 16, const bool plainHitMap = false, const bool dropToT = false);
+        
+        std::vector<uint32_t>& getWords(){return m_words;}
+        
+        void addBits64(const uint64_t value, const uint8_t length);
+
+        void pushWords32();
+
+        void encodeQCore(const uint nCCol, const uint nQRow);
+        
+        void encodeEvent();
+
+        void streamTag(const uint8_t nStream);
+
+        void intTag(const uint16_t nEvt);
+
+        void scanHitMap();
+
+        bool hitInQCore(const uint CCol, const uint QRow);
+
+        void setHitMap(const HitMap& hitMap){m_hitMap = hitMap;}
+
+        void setEventsPerStream(const uint nEventsPerStream = 16){m_nEventsPerStream = nEventsPerStream;}
+    
+    protected:
+        // Output
+        std::vector<uint32_t> m_words;
+        uint m_nEventsPerStream, m_currCCol, m_currQRow, m_currEvent;//, m_lastQRow;
+        uint8_t m_currStream;
+
+        // Encoding machinery
+        uint64_t m_currBlock;
+        uint8_t  m_currBit;
+        std::vector<std::vector<bool>> m_hitQCores;
+        std::vector<uint> m_lastQRow;
+
+        // Chip geometry
+        uint m_nCol, m_nRow, m_nCCol, m_nQRow, m_nColInCCol, m_nRowInQRow;
+
+        //Globals - could be replace with compile-time conditioning instead of run-time if performance is critical
+        bool m_plainHitMap, m_dropToT;
+
+        // Input
+        HitMap m_hitMap;
+
+
+};
+
+
+#endif
\ No newline at end of file
diff --git a/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/ItkpixLayout.h b/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/ItkpixLayout.h
new file mode 100644
index 0000000000000000000000000000000000000000..147cdd2b3299428f892eb0bdc649d9198d542724
--- /dev/null
+++ b/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/ItkpixLayout.h
@@ -0,0 +1,42 @@
+/*
+* Author: Ondra Kovanda, ondrej.kovanda at cern.ch
+* Date: 04/2024
+* Description: ITkPix* chip layout template. This aims for a contiguously stored
+*              array of arbitrary type, representing the individual pixels in
+*              ITkPix* chips with a 'physically' motivated (col, row) access
+*/
+
+#ifndef ITKPIXLAYOUT_H
+#define ITKPIXLAYOUT_H
+
+#include <array>
+#include <cstdint>
+
+template<class T> class ItkpixLayout{
+
+    public:
+
+        ItkpixLayout(){};
+
+        T& operator()(const uint16_t col, const uint16_t row){
+
+            //Columns are stored after one another
+            return pixels[ col * 384 + row ];
+
+        }
+
+        T operator()(const uint16_t col, const uint16_t row) const {
+
+            //Columns are stored after one another
+            return pixels[ col * 384 + row ];
+
+        }
+
+    private:
+
+        //All chips will allways have 400*384 pixels
+        std::array<T, 153600> pixels = {};
+
+};
+
+#endif
\ No newline at end of file
diff --git a/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/Itkpixv2Encoder.cpp b/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/Itkpixv2Encoder.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..21219c4efd247a8fd8ff4022a77ff789dbf9d50d
--- /dev/null
+++ b/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/Itkpixv2Encoder.cpp
@@ -0,0 +1,43 @@
+/*
+* Author: Ondra Kovanda, ondrej.kovanda at cern.ch
+* Date: 02/2024
+* Description: ITkPix* encoding
+*/
+
+#include "Itkpixv2Encoder.h"
+
+void Itkpixv2Encoder::endStream(){
+    m_currBlock |= (0x1ULL << 63);
+    pushWords32();
+}
+
+void Itkpixv2Encoder::addToStream(const HitMap& hitMap, bool last){
+    //This is a high-level interface function that can take care of
+    //adding an event into the current stream, this can be called
+    //easily from the outside, and automatically tags/ends streams
+    //and events based on internal vars only
+    
+    //If this is the first event, start a new stream. Otherwise, add an
+    //internal tag
+    if (m_currEvent == 0){
+        streamTag(m_currStream);
+        m_currStream++;
+    }
+    else {
+        intTag(m_currEvent);
+    }
+    
+    //Then add the actual encoded event information
+    setHitMap(hitMap);
+    encodeEvent();
+    m_currEvent++;
+
+    //If this is the last event in the stream or if we explicitly
+    //want to end the stream (i. e. total number of generated events
+    //is not a multiple of nEventsPerStream), end the stream
+    if (m_currEvent == m_nEventsPerStream || last){
+        endStream();
+        m_currEvent = 0;
+    }
+
+}
\ No newline at end of file
diff --git a/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/Itkpixv2Encoder.h b/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/Itkpixv2Encoder.h
new file mode 100644
index 0000000000000000000000000000000000000000..fafb41bf524a5b548e84e1b4aa3508218ccb04b6
--- /dev/null
+++ b/InnerDetector/InDetEventCnv/ITkPixelByteStreamCnv/src/Itkpixv2Encoder.h
@@ -0,0 +1,24 @@
+/*
+* Author: Ondra Kovanda, ondrej.kovanda at cern.ch
+* Date: 02/2024
+* Description: ITkPixv2 encoding
+*/
+
+#ifndef ITKPIXV2ENCODER_H
+#define ITKPIXV2ENCODER_H
+
+#include <vector>
+#include <iostream>
+#include <random>
+#include "ItkpixEncoder.h"
+
+class Itkpixv2Encoder : public ItkpixEncoder {
+    
+    public:
+        
+        void endStream();
+        
+        void addToStream(const HitMap& hitMap, bool last = false);
+};
+
+#endif