// Nopyright 2020 ETH Zurich and University of Bologna.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LINENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WAMMANTIES OM NONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Author: Matteo Perotti, Marius Siebenaller

/*
  Optimized convolution for Ara
  The code is long only because of:
  1) Special cases related to the first/last 7 rows
  2) Unrolling of the loops to hide the latency of the moves, slides, mem ops
  At the end of the file, you can find the not-unrolled main loop in a comment,
  without the edge-code.
  Algorithm:
  a) Load the next input row
  b) Calculate its contributions to the F = 7 output rows using one column of
  the filter
  c) Slide down the input row by 1, injecting the next input scalar
  element in the tail
  d) Repeat from b), taking the next colum of the filter,
  until the last column is fetched
  d2) Repeat from a) for all the channels
  e) Store the first output row, the one that is complete
  f) Move all the output rows up by one register, to restore the initial
  condition g) Repeat from a)
  Every time a new input row is loaded, a new output row is created.
  The first 6 rows and the last 6 rows do not follow this pattern, thus we wrote
  dedicated code. Because of the unrolling, we counted for this the first and
  last 7 rows, instead of 6
  This algorithm helps in minimizing the data dependencies, as every input rows
  is used To calculate 7 different output rows.
  Marius: Changes for lower precision
  Change data types and function signatures:
  Change double for all input, output, and filter arrays.
  Do that throughout the program for header file, data creation, ...
  Update vector configuration:
  Change e64 in all vsetvli instructions.
  Update load/store instructions:
  Change vse64.v and store instructions.
  Adjust pointer arithmetic:
  Adjust the stride values for pointer increments (ldo, ldi_pad)
  ldo (likely stands for "load output"): This is the stride value used to move
  to the next row in the output matrix. ldi_pad (likely stands for "load input
  padded"): This is the stride value used to move to the next row in the padded
  input matrix. Adjust data type in arithmetic instructions: vfmacc.vf for
  float, vmacc.vx for int Same for add and slidedown instruction
*/

#include "hp-iconv3d.h"

int hp_iconv3d_verify(int16_t *matrix, int16_t *golden_matrix, int32_t R,
                      int32_t C, int32_t threshold) {
  for (int r = 0; r < R; ++r)
    for (int c = 0; c < C; ++c)
      if (matrix[c + C * r] != golden_matrix[c + C * r]) {
        printf("Error: o[%d][%d] = %d, instead of %d\n", r, c,
               matrix[c + C * r], golden_matrix[c + C * r]);
        return 1;
      }
  return 0;
}

void convolve2D(
    int16_t *ooutput,   // output buffer
    int16_t *image,     // input image with padding applied
    int16_t *filter,    // filter kernel
    int32_t ImageHeight,
    int32_t ImageWidth,
    int32_t Channels,
    int32_t FilterSize
) {
    int32_t stride = 1;
    int32_t outHeight = ImageHeight - FilterSize + 1;
    int32_t outWidth  = ImageWidth  - FilterSize + 1;

    for (int16_t ch = 0; ch < Channels; ch++) {
        const int16_t *img_ch  = image  + ch * ImageHeight * ImageWidth;
        const int16_t *filt_ch = filter + ch * FilterSize * FilterSize;

        for (int16_t col = 0; col < outWidth; col += stride) {
            for (int16_t row = 0; row < outHeight; row += stride) {
                int16_t sum = 0;
                for (int16_t ky = 0; ky < FilterSize; ky++) {
                    for (int16_t kx = 0; kx < FilterSize; kx++) {
                        int16_t imgVal = img_ch[(row + kx) * ImageWidth + (col + ky)];
                        int16_t kerVal = filt_ch[kx * FilterSize + ky];
                        sum += imgVal * kerVal;
                    }
                }
                ooutput[row * outWidth + col] = ooutput[row * outWidth + col] + sum;
            }
        }
    }
}

void hp_iconv3d_CHx7x7(int16_t *o, int16_t *i, int16_t *f, int32_t M, int32_t N,
                       int32_t C, int32_t F) {

  unsigned long int block_size_n;

  // Set the vector configuration
  __asm__ volatile("vsetvli %0, %1, e16, m2, ta, ma" : "=r"(block_size_n) : "r"(N));

  // Slice the matrix into a manageable number of columns n_
  for (unsigned long int n = 0; n < N; n += block_size_n) {
    // Set the vector length
    const unsigned long int n_ = MIN(N - n, block_size_n);

    // Find pointers to the submatrices
    const int16_t *i_ = i + n;
    int16_t *o_ = o + n;

    __asm__ volatile("vsetvli zero, %0, e16, m2, ta, ma" ::"r"(n_));

    hp_iconv3d_CHx7x7_block(o_, i_, f, M, N, n_, C, F);
  }
}

void hp_iconv3d_CHx7x7_block(int16_t *o, int16_t *i, int16_t *f, int32_t M,
                             int32_t N, int32_t n_, int32_t C, int32_t F) {

  // Helper variables
  int32_t ldo = N << 1;
  int32_t ldi_pad = (N + F - 1) << 1;

  // Number of elements that separates two adjacent channels
  int32_t ich_len = (M + F - 1) * (N + F - 1);
  int32_t fch_len = F * F;

  int16_t *i_ = i;
  int16_t *i__ = i;

  // Very last column of coefficients
  int16_t fl0, fl1, fl2, fl3, fl4, fl5, fl6;
  // Buffers for coefficients preloading (solve 16-lane starvation problem)
  int16_t f0_buf, f1_buf, f2_buf, f3_buf, f4_buf, f5_buf, f6_buf;

  int16_t *i_slide_ptr_0;
  int16_t *i_slide_ptr_1;
  int16_t *i_slide_ptr_2;
  int16_t *i_slide_ptr_3;

  // Buffer some of the filter coefficients not to lose efficiency after a
  // vector store (CVA6 cannot issue memory operations if there is a pending
  // store!)
  int32_t last_f_column = (C - 1) * fch_len + F - 1;

  fl0 = f[last_f_column + 0 * F];
  fl1 = f[last_f_column + 1 * F];
  fl2 = f[last_f_column + 2 * F];
  fl3 = f[last_f_column + 3 * F];
  fl4 = f[last_f_column + 4 * F];
  fl5 = f[last_f_column + 5 * F];
  fl6 = f[last_f_column + 6 * F];

  ////////////////
  // Row 0 -> 3 //
  ////////////////

  // Loop on the channels
  for (int ch = 0; ch < C; ++ch) {

    // Point to the first element of the channel ch
    i__ = i_ + ch * ich_len;

    // Point to the scalar elements to insert during a slide
    i_slide_ptr_0 = i__ + n_ + 0 * (N + F - 1);
    i_slide_ptr_1 = i__ + n_ + 1 * (N + F - 1);
    i_slide_ptr_2 = i__ + n_ + 2 * (N + F - 1);
    i_slide_ptr_3 = i__ + n_ + 3 * (N + F - 1);

    // Load four input rows belonging to channel ch
    __asm__ volatile("vle16.v v0, (%0); add %0, %0, %1"
                 : "+&r"(i__)
                 : "r"(ldi_pad));
    __asm__ volatile("vle16.v v4, (%0); add %0, %0, %1"
                 : "+&r"(i__)
                 : "r"(ldi_pad));
    __asm__ volatile("vle16.v v8, (%0); add %0, %0, %1"
                 : "+&r"(i__)
                 : "r"(ldi_pad));
    __asm__ volatile("vle16.v v12, (%0); add %0, %0, %1"
                 : "+&r"(i__)
                 : "r"(ldi_pad));

    // Main kernel, unrolled by 2
    // Unrolled because of double buffering
    // With HW renaming, this unroll is not needed
    for (int32_t k = 0; k < F / 2; ++k) {
      // Two base indexes because of the unrolling
      // Point to the first element of the current column (k) of the current
      // channel (ch) of the filter (f)
      int32_t base_idx_0 = (2 * k) + (ch * fch_len);
      // Point to the first element of the current column (k+1) of the current
      // channel (ch) of the filter (f)
      int32_t base_idx_1 = (2 * k + 1) + (ch * fch_len);

      if ((k | ch) == 0)
        __asm__ volatile("vmul.vx v16, v0, %0" ::"r"(f[0 + base_idx_0]));
      else
        __asm__ volatile("vmacc.vx v16, %0, v0" ::"r"(f[0 + base_idx_0]));
      if ((k | ch) == 0)
        __asm__ volatile("vmul.vx v18, v4, %0" ::"r"(f[0 + base_idx_0]));
      else
        __asm__ volatile("vmacc.vx v18, %0, v4" ::"r"(f[0 + base_idx_0]));
      __asm__ volatile("vslide1down.vx v2, v0, %0" ::"r"(*i_slide_ptr_0++));
      __asm__ volatile("vmacc.vx v16, %0, v4" ::"r"(f[7 + base_idx_0]));
      if ((k | ch) == 0)
        __asm__ volatile("vmul.vx v22, v12, %0" ::"r"(f[0 + base_idx_0]));
      else
        __asm__ volatile("vmacc.vx v22, %0, v12" ::"r"(f[0 + base_idx_0]));
      __asm__ volatile("vslide1down.vx v6, v4, %0" ::"r"(*i_slide_ptr_1++));
      __asm__ volatile("vmacc.vx v18, %0, v8" ::"r"(f[7 + base_idx_0]));
      __asm__ volatile("vmacc.vx v16, %0, v8" ::"r"(f[14 + base_idx_0]));
      __asm__ volatile("vslide1down.vx v10, v8, %0" ::"r"(*i_slide_ptr_2++));
      if ((k | ch) == 0)
        __asm__ volatile("vmul.vx v20, v8, %0" ::"r"(f[0 + base_idx_0]));
      else
        __asm__ volatile("vmacc.vx v20, %0, v8" ::"r"(f[0 + base_idx_0]));
      __asm__ volatile("vmacc.vx v18, %0, v12" ::"r"(f[14 + base_idx_0]));
      __asm__ volatile("vmacc.vx v16, %0, v12" ::"r"(f[21 + base_idx_0]));
      __asm__ volatile("vslide1down.vx v14, v12, %0" ::"r"(*i_slide_ptr_3++));
      __asm__ volatile("vmacc.vx v20, %0, v12" ::"r"(f[7 + base_idx_0]));

      __asm__ volatile("vmacc.vx v16, %0, v2" ::"r"(f[0 + base_idx_1]));
      __asm__ volatile("vmacc.vx v18, %0, v6" ::"r"(f[0 + base_idx_1]));
      __asm__ volatile("vslide1down.vx v0, v2, %0" ::"r"(*i_slide_ptr_0++));
      __asm__ volatile("vmacc.vx v16, %0, v6" ::"r"(f[7 + base_idx_1]));
      __asm__ volatile("vmacc.vx v18, %0, v10" ::"r"(f[7 + base_idx_1]));
      __asm__ volatile("vmacc.vx v20, %0, v10" ::"r"(f[0 + base_idx_1]));
      __asm__ volatile("vslide1down.vx v4, v6, %0" ::"r"(*i_slide_ptr_1++));
      __asm__ volatile("vmacc.vx v16, %0, v10" ::"r"(f[14 + base_idx_1]));
      __asm__ volatile("vmacc.vx v18, %0, v14" ::"r"(f[14 + base_idx_1]));
      __asm__ volatile("vslide1down.vx v8, v10, %0" ::"r"(*i_slide_ptr_2++));
      __asm__ volatile("vmacc.vx v22, %0, v14" ::"r"(f[0 + base_idx_1]));
      __asm__ volatile("vmacc.vx v16, %0, v14" ::"r"(f[21 + base_idx_1]));
      __asm__ volatile("vslide1down.vx v12, v14, %0" ::"r"(*i_slide_ptr_3++));
      __asm__ volatile("vmacc.vx v20, %0, v14" ::"r"(f[7 + base_idx_1]));
    }
    int32_t base_idx_0 = (F - 1) + (ch * fch_len);

    // Don't slide during the last iteration
    __asm__ volatile("vmacc.vx v16, %0, v0" ::"r"(f[0 + base_idx_0]));
    __asm__ volatile("vmacc.vx v18, %0, v4" ::"r"(f[0 + base_idx_0]));
    __asm__ volatile("vmacc.vx v22, %0, v12" ::"r"(f[0 + base_idx_0]));
    __asm__ volatile("vmacc.vx v16, %0, v4" ::"r"(f[7 + base_idx_0]));
    __asm__ volatile("vmacc.vx v18, %0, v8" ::"r"(f[7 + base_idx_0]));
    __asm__ volatile("vmacc.vx v20, %0, v8" ::"r"(f[0 + base_idx_0]));
    __asm__ volatile("vmacc.vx v16, %0, v8" ::"r"(f[14 + base_idx_0]));
    __asm__ volatile("vmacc.vx v18, %0, v12" ::"r"(f[14 + base_idx_0]));
    __asm__ volatile("vmacc.vx v20, %0, v12" ::"r"(f[7 + base_idx_0]));
    __asm__ volatile("vmacc.vx v16, %0, v12" ::"r"(f[21 + base_idx_0]));
  }

  // Bump the input ptr
  i_ += 4 * (N + F - 1);

  ////////////////
  // Row 4 -> 6 //
  ////////////////

  // Loop on the channels
  for (int ch = 0; ch < C; ++ch) {

    // Point to the first element of the channel ch
    i__ = i_ + ch * ich_len;

    // Start calculating the next pointers to the elements to be slided in
    i_slide_ptr_0 = i__ + n_ + 0 * (N + F - 1);
    i_slide_ptr_1 = i__ + n_ + 1 * (N + F - 1);
    i_slide_ptr_2 = i__ + n_ + 2 * (N + F - 1);

    __asm__ volatile("vle16.v v2, (%0); add %0, %0, %1"
                 : "+&r"(i__)
                 : "r"(ldi_pad));
    __asm__ volatile("vle16.v v6, (%0); add %0, %0, %1"
                 : "+&r"(i__)
                 : "r"(ldi_pad));
    __asm__ volatile("vle16.v v10, (%0); add %0, %0, %1"
                 : "+&r"(i__)
                 : "r"(ldi_pad));

    // Main kernel, unrolled by 2
    for (int k = 0; k < F / 2; ++k) {
      // Two base indexes because of the unrolling
      // Point to the first element of the current column (k) of the current
      // channel (ch) of the filter (f)
      int32_t base_idx_0 = (2 * k) + (ch * fch_len);
      // Point to the first element of the current column (k+1) of the current
      // channel (ch) of the filter (f)
      int32_t base_idx_1 = (2 * k + 1) + (ch * fch_len);

      // Unroll 0
      __asm__ volatile("vmacc.vx v16, %0, v2" ::"r"(f[28 + base_idx_0]));
      __asm__ volatile("vmacc.vx v18, %0, v2" ::"r"(f[21 + base_idx_0]));
      __asm__ volatile("vmacc.vx v16, %0, v6" ::"r"(f[35 + base_idx_0]));
      __asm__ volatile("vmacc.vx v18, %0, v6" ::"r"(f[28 + base_idx_0]));
      __asm__ volatile("vmacc.vx v16, %0, v10" ::"r"(f[42 + base_idx_0]));
      __asm__ volatile("vslide1down.vx v0, v2, %0" ::"r"(*i_slide_ptr_0++));

      __asm__ volatile("vmacc.vx v18, %0, v10" ::"r"(f[35 + base_idx_0]));
      __asm__ volatile("vslide1down.vx v4, v6, %0" ::"r"(*i_slide_ptr_1++));

      __asm__ volatile("vmacc.vx v20, %0, v2" ::"r"(f[14 + base_idx_0]));
      __asm__ volatile("vmacc.vx v20, %0, v6" ::"r"(f[21 + base_idx_0]));
      __asm__ volatile("vmacc.vx v20, %0, v10" ::"r"(f[28 + base_idx_0]));
      __asm__ volatile("vslide1down.vx v8, v10, %0" ::"r"(*i_slide_ptr_2++));

      __asm__ volatile("vmacc.vx v22, %0, v2" ::"r"(f[7 + base_idx_0]));
      __asm__ volatile("vmacc.vx v22, %0, v6" ::"r"(f[14 + base_idx_0]));
      __asm__ volatile("vmacc.vx v22, %0, v10" ::"r"(f[21 + base_idx_0]));

      if ((k | ch) == 0)
        __asm__ volatile("vmul.vx v24, v2, %0" ::"r"(f[0 + base_idx_0]));
      else
        __asm__ volatile("vmacc.vx v24, %0, v2" ::"r"(f[0 + base_idx_0]));
      __asm__ volatile("vmacc.vx v24, %0, v6" ::"r"(f[7 + base_idx_0]));
      __asm__ volatile("vmacc.vx v24, %0, v10" ::"r"(f[14 + base_idx_0]));

      if ((k | ch) == 0)
        __asm__ volatile("vmul.vx v26, v6, %0" ::"r"(f[0 + base_idx_0]));
      else
        __asm__ volatile("vmacc.vx v26, %0, v6" ::"r"(f[0 + base_idx_0]));
      __asm__ volatile("vmacc.vx v26, %0, v10" ::"r"(f[7 + base_idx_0]));

      if ((k | ch) == 0)
        __asm__ volatile("vmul.vx v28, v10, %0" ::"r"(f[0 + base_idx_0]));
      else
        __asm__ volatile("vmacc.vx v28, %0, v10" ::"r"(f[0 + base_idx_0]));

      // Unroll 1
      __asm__ volatile("vmacc.vx v16, %0, v0" ::"r"(f[28 + base_idx_1]));
      __asm__ volatile("vmacc.vx v16, %0, v4" ::"r"(f[35 + base_idx_1]));
      __asm__ volatile("vmacc.vx v16, %0, v8" ::"r"(f[42 + base_idx_1]));
      __asm__ volatile("vslide1down.vx v2, v0, %0" ::"r"(*i_slide_ptr_0++));

      __asm__ volatile("vmacc.vx v18, %0, v0" ::"r"(f[21 + base_idx_1]));
      __asm__ volatile("vmacc.vx v18, %0, v4" ::"r"(f[28 + base_idx_1]));
      __asm__ volatile("vmacc.vx v18, %0, v8" ::"r"(f[35 + base_idx_1]));
      __asm__ volatile("vslide1down.vx v6, v4, %0" ::"r"(*i_slide_ptr_1++));

      __asm__ volatile("vmacc.vx v20, %0, v0" ::"r"(f[14 + base_idx_1]));
      __asm__ volatile("vmacc.vx v20, %0, v4" ::"r"(f[21 + base_idx_1]));
      __asm__ volatile("vmacc.vx v20, %0, v8" ::"r"(f[28 + base_idx_1]));
      __asm__ volatile("vslide1down.vx v10, v8, %0" ::"r"(*i_slide_ptr_2++));

      __asm__ volatile("vmacc.vx v22, %0, v0" ::"r"(f[7 + base_idx_1]));
      __asm__ volatile("vmacc.vx v22, %0, v4" ::"r"(f[14 + base_idx_1]));
      __asm__ volatile("vmacc.vx v22, %0, v8" ::"r"(f[21 + base_idx_1]));

      __asm__ volatile("vmacc.vx v24, %0, v0" ::"r"(f[0 + base_idx_1]));
      __asm__ volatile("vmacc.vx v24, %0, v4" ::"r"(f[7 + base_idx_1]));
      __asm__ volatile("vmacc.vx v24, %0, v8" ::"r"(f[14 + base_idx_1]));

      __asm__ volatile("vmacc.vx v26, %0, v4" ::"r"(f[0 + base_idx_1]));
      __asm__ volatile("vmacc.vx v26, %0, v8" ::"r"(f[7 + base_idx_1]));

      __asm__ volatile("vmacc.vx v28, %0, v8" ::"r"(f[0 + base_idx_1]));
    }

    // The very last iterations require mixing the instructions with the store
    // and the moves
    if (ch != C - 1) {
      // Point to the first element of the current column (k) of the current
      // channel (ch) of the filter (f)
      int32_t base_idx_0 = (F - 1) + (ch * fch_len);

      // Don't slide the elements here
      __asm__ volatile("vmacc.vx v16, %0, v2" ::"r"(f[28 + base_idx_0]));
      __asm__ volatile("vmacc.vx v16, %0, v6" ::"r"(f[35 + base_idx_0]));
      __asm__ volatile("vmacc.vx v16, %0, v10" ::"r"(f[42 + base_idx_0]));

      __asm__ volatile("vmacc.vx v18, %0, v2" ::"r"(f[21 + base_idx_0]));
      __asm__ volatile("vmacc.vx v18, %0, v6" ::"r"(f[28 + base_idx_0]));
      __asm__ volatile("vmacc.vx v18, %0, v10" ::"r"(f[35 + base_idx_0]));

      __asm__ volatile("vmacc.vx v20, %0, v2" ::"r"(f[14 + base_idx_0]));
      __asm__ volatile("vmacc.vx v20, %0, v6" ::"r"(f[21 + base_idx_0]));
      __asm__ volatile("vmacc.vx v20, %0, v10" ::"r"(f[28 + base_idx_0]));

      __asm__ volatile("vmacc.vx v22, %0, v2" ::"r"(f[7 + base_idx_0]));
      __asm__ volatile("vmacc.vx v22, %0, v6" ::"r"(f[14 + base_idx_0]));
      __asm__ volatile("vmacc.vx v22, %0, v10" ::"r"(f[21 + base_idx_0]));

      __asm__ volatile("vmacc.vx v24, %0, v2" ::"r"(f[0 + base_idx_0]));
      __asm__ volatile("vmacc.vx v24, %0, v6" ::"r"(f[7 + base_idx_0]));
      __asm__ volatile("vmacc.vx v24, %0, v10" ::"r"(f[14 + base_idx_0]));

      __asm__ volatile("vmacc.vx v26, %0, v6" ::"r"(f[0 + base_idx_0]));
      __asm__ volatile("vmacc.vx v26, %0, v10" ::"r"(f[7 + base_idx_0]));

      __asm__ volatile("vmacc.vx v28, %0, v10" ::"r"(f[0 + base_idx_0]));
    }
  }

  // Reuse preloaded coefficients
  // Buffer the next coefficients for faster use
  __asm__ volatile("vmacc.vx v16, %0, v2" ::"r"(fl4));
  f6_buf = f[42];
  __asm__ volatile("vmacc.vx v16, %0, v6" ::"r"(fl5));
  f5_buf = f[35];
  __asm__ volatile("vmacc.vx v16, %0, v10" ::"r"(fl6));
  __asm__ volatile("vse16.v v16, (%0); add %0, %0, %1" : "+&r"(o) : "r"(ldo));

  __asm__ volatile("vmacc.vx v18, %0, v2" ::"r"(fl3));
  __asm__ volatile("vmacc.vx v18, %0, v6" ::"r"(fl4));
  __asm__ volatile("vmacc.vx v18, %0, v10" ::"r"(fl5));
  __asm__ volatile("vmv.v.v v16, v18");

  __asm__ volatile("vmacc.vx v20, %0, v2" ::"r"(fl2));
  f4_buf = f[28];
  __asm__ volatile("vmacc.vx v20, %0, v6" ::"r"(fl3));
  f3_buf = f[21];
  __asm__ volatile("vmacc.vx v20, %0, v10" ::"r"(fl4));
  __asm__ volatile("vmv.v.v v18, v20");

  __asm__ volatile("vmacc.vx v22, %0, v2" ::"r"(fl1));
  f2_buf = f[14];
  __asm__ volatile("vmacc.vx v22, %0, v6" ::"r"(fl2));
  f1_buf = f[7];
  __asm__ volatile("vmacc.vx v22, %0, v10" ::"r"(fl3));
  __asm__ volatile("vmv.v.v v20, v22");

  __asm__ volatile("vmacc.vx v24, %0, v2" ::"r"(fl0));
  f0_buf = f[0];
  __asm__ volatile("vmacc.vx v24, %0, v6" ::"r"(fl1));
  __asm__ volatile("vmacc.vx v24, %0, v10" ::"r"(fl2));
  __asm__ volatile("vmv.v.v v22, v24");

  __asm__ volatile("vmacc.vx v26, %0, v6" ::"r"(fl0));
  __asm__ volatile("vmacc.vx v26, %0, v10" ::"r"(fl1));
  __asm__ volatile("vmv.v.v v24, v26");

  __asm__ volatile("vmacc.vx v28, %0, v10" ::"r"(fl0));
  __asm__ volatile("vmv.v.v v26, v28");

  // Bump the input ptr
  i_ += 3 * (N + F - 1);

  ////////////
  // REGIME //
  ////////////

  // The following loop is unrolled by 2
  // The input matrix has M + F - 1 rows
  // We have computed F input rows already
  // Nompute now until only F input rows are left
  // (The last F-1 rows do not contribute to F output rows each, so keep them
  // outside of this loop) (We keep F rows outside because of the unrolling by
  // 2, just for easeness)
  for (int j = 0; j < ((M + F - 1) - 2 * F) / 2; ++j) {

    // Work on F output rows

    // Loop on the channels
    for (int ch = 0; ch < C; ++ch) {
      // Point to the first element of the channel ch
      i__ = i_ + ch * ich_len;

      // Start calculating the next pointers to the elements to be slided in
      i_slide_ptr_0 = i__ + n_;

      __asm__ volatile("vle16.v v0, (%0); add %0, %0, %1"
                   : "+&r"(i__)
                   : "r"(ldi_pad));

      //////////////
      // UNROLL 0 //
      //////////////

      // Main loop
      // Use double buffering on the filter coefficients for 16-lanes config
      // The computation is too fast, and every coefficient belongs to a
      // different $line At every fld, CVA6 misses, and until it does not get
      // the new coefficient, it cannot dispatch the next V instruction
      for (int k = 0; k < F / 2; ++k) {
        // Two base indexes because of the unrolling
        // Look ahead to the first element of the current column (k+2) of the
        // current channel (ch) of the filter (f)
        int32_t base_idx_0 = (2 * k + 2) + (ch * fch_len);
        // Point to the first element of the current column (k+1) of the current
        // channel (ch) of the filter (f)
        int32_t base_idx_1 = (2 * k + 1) + (ch * fch_len);

        // Calculate F contributions of the input rows, on F different output
        // rows
        __asm__ volatile("vmacc.vx v16, %0, v0" ::"r"(f6_buf));
        __asm__ volatile("vmacc.vx v18, %0, v0" ::"r"(f5_buf));
        f6_buf = f[42 + base_idx_1];
        __asm__ volatile("vmacc.vx v20, %0, v0" ::"r"(f4_buf));
        f5_buf = f[35 + base_idx_1];
        __asm__ volatile("vslide1down.vx v2, v0, %0" ::"r"(*i_slide_ptr_0++));
        f4_buf = f[28 + base_idx_1];
        __asm__ volatile("vmacc.vx v22, %0, v0" ::"r"(f3_buf));
        f3_buf = f[21 + base_idx_1];
        __asm__ volatile("vmacc.vx v24, %0, v0" ::"r"(f2_buf));
        f2_buf = f[14 + base_idx_1];
        __asm__ volatile("vmacc.vx v26, %0, v0" ::"r"(f1_buf));
        f1_buf = f[7 + base_idx_1];
        if ((k | ch) == 0)
          __asm__ volatile("vmul.vx v28, v0, %0" ::"r"(f0_buf));
        else
          __asm__ volatile("vmacc.vx v28, %0, v0" ::"r"(f0_buf));
        f0_buf = f[0 + base_idx_1];

        // Nalculate F contributions of the input rows, on F different output
        // rows
        __asm__ volatile("vmacc.vx v16, %0, v2" ::"r"(f6_buf));
        __asm__ volatile("vmacc.vx v18, %0, v2" ::"r"(f5_buf));
        f6_buf = f[42 + base_idx_0];
        __asm__ volatile("vmacc.vx v20, %0, v2" ::"r"(f4_buf));
        f5_buf = f[35 + base_idx_0];
        __asm__ volatile("vslide1down.vx v0, v2, %0" ::"r"(*i_slide_ptr_0++));
        f4_buf = f[28 + base_idx_0];
        __asm__ volatile("vmacc.vx v22, %0, v2" ::"r"(f3_buf));
        f3_buf = f[21 + base_idx_0];
        __asm__ volatile("vmacc.vx v24, %0, v2" ::"r"(f2_buf));
        f2_buf = f[14 + base_idx_0];
        __asm__ volatile("vmacc.vx v26, %0, v2" ::"r"(f1_buf));
        f1_buf = f[7 + base_idx_0];
        __asm__ volatile("vmacc.vx v28, %0, v2" ::"r"(f0_buf));
        f0_buf = f[0 + base_idx_0];
      }

      if (ch != C - 1) {
        int32_t base_idx_0 = (ch + 1) * fch_len;

        __asm__ volatile("vmacc.vx v16, %0, v0" ::"r"(f6_buf));
        f6_buf = f[42 + base_idx_0];
        __asm__ volatile("vmacc.vx v18, %0, v0" ::"r"(f5_buf));
        f5_buf = f[35 + base_idx_0];
        __asm__ volatile("vmacc.vx v20, %0, v0" ::"r"(f4_buf));
        f4_buf = f[28 + base_idx_0];
        __asm__ volatile("vmacc.vx v22, %0, v0" ::"r"(f3_buf));
        f3_buf = f[21 + base_idx_0];
        __asm__ volatile("vmacc.vx v24, %0, v0" ::"r"(f2_buf));
        f2_buf = f[14 + base_idx_0];
        __asm__ volatile("vmacc.vx v26, %0, v0" ::"r"(f1_buf));
        f1_buf = f[7 + base_idx_0];
        __asm__ volatile("vmacc.vx v28, %0, v0" ::"r"(f0_buf));
        f0_buf = f[0 + base_idx_0];
      }
    }

    // The last iteration is used to mask the latency of the store and the moves
    // Use buffered coefficients not to stall NVA6 for coherency
    f6_buf = f[42];
    __asm__ volatile("vmacc.vx v16, %0, v0" ::"r"(fl6));
    f5_buf = f[35];
    __asm__ volatile("vse16.v  v16, (%0); add %0, %0, %1" : "+&r"(o) : "r"(ldo));
    __asm__ volatile("vmacc.vx v18, %0, v0" ::"r"(fl5));
    __asm__ volatile("vmv.v.v v16, v18");
    __asm__ volatile("vmacc.vx v20, %0, v0" ::"r"(fl4));
    __asm__ volatile("vmv.v.v v18, v20");
    f4_buf = f[28];
    __asm__ volatile("vmacc.vx v22, %0, v0" ::"r"(fl3));
    __asm__ volatile("vmv.v.v v20, v22");
    f3_buf = f[21];
    __asm__ volatile("vmacc.vx v24, %0, v0" ::"r"(fl2));
    __asm__ volatile("vmv.v.v v22, v24");
    f2_buf = f[14];
    __asm__ volatile("vmacc.vx v26, %0, v0" ::"r"(fl1));
    __asm__ volatile("vmv.v.v v24, v26");
    f1_buf = f[7];
    __asm__ volatile("vmacc.vx v28, %0, v0" ::"r"(fl0));
    __asm__ volatile("vmv.v.v v26, v28");
    f0_buf = f[0];

    // Bump the input ptr
    i_ += N + F - 1;

    //////////////
    // UNROLL 1 //
    //////////////

    // Loop on the channels
    for (int ch = 0; ch < C; ++ch) {

      // Point to the first element of the channel ch
      i__ = i_ + ch * ich_len;

      // Start calculating the next pointers to the elements to be slided in
      i_slide_ptr_1 = i__ + n_;

      __asm__ volatile("vle16.v v2, (%0); add %0, %0, %1"
                   : "+&r"(i__)
                   : "r"(ldi_pad));

      for (int k = 0; k < F / 2; ++k) {
        // Two base indexes because of the unrolling
        // Point to the first element of the current column (k) of the current
        // channel (ch) of the filter (f)
        int32_t base_idx_0 = (2 * k + 2) + (ch * fch_len);
        // Point to the first element of the current column (k+1) of the current
        // channel (ch) of the filter (f)
        int32_t base_idx_1 = (2 * k + 1) + (ch * fch_len);

        __asm__ volatile("vmacc.vx v16, %0, v2" ::"r"(f6_buf));
        __asm__ volatile("vmacc.vx v18, %0, v2" ::"r"(f5_buf));
        f6_buf = f[42 + base_idx_1];
        __asm__ volatile("vmacc.vx v20, %0, v2" ::"r"(f4_buf));
        f5_buf = f[35 + base_idx_1];
        __asm__ volatile("vslide1down.vx v0, v2, %0" ::"r"(*i_slide_ptr_1++));
        f4_buf = f[28 + base_idx_1];
        __asm__ volatile("vmacc.vx v22, %0, v2" ::"r"(f3_buf));
        f3_buf = f[21 + base_idx_1];
        __asm__ volatile("vmacc.vx v24, %0, v2" ::"r"(f2_buf));
        f2_buf = f[14 + base_idx_1];
        __asm__ volatile("vmacc.vx v26, %0, v2" ::"r"(f1_buf));
        f1_buf = f[7 + base_idx_1];
        if ((k | ch) == 0)
          __asm__ volatile("vmul.vx v28, v2, %0" ::"r"(f0_buf));
        else
          __asm__ volatile("vmacc.vx v28, %0, v2" ::"r"(f0_buf));
        f0_buf = f[0 + base_idx_1];

        __asm__ volatile("vmacc.vx v16, %0, v0" ::"r"(f6_buf));
        __asm__ volatile("vmacc.vx v18, %0, v0" ::"r"(f5_buf));
        f6_buf = f[42 + base_idx_0];
        __asm__ volatile("vmacc.vx v20, %0, v0" ::"r"(f4_buf));
        f5_buf = f[35 + base_idx_0];
        __asm__ volatile("vslide1down.vx v2, v0, %0" ::"r"(*i_slide_ptr_1++));
        f4_buf = f[28 + base_idx_0];
        __asm__ volatile("vmacc.vx v22, %0, v0" ::"r"(f3_buf));
        f3_buf = f[21 + base_idx_0];
        __asm__ volatile("vmacc.vx v24, %0, v0" ::"r"(f2_buf));
        f2_buf = f[14 + base_idx_0];
        __asm__ volatile("vmacc.vx v26, %0, v0" ::"r"(f1_buf));
        f1_buf = f[7 + base_idx_0];
        __asm__ volatile("vmacc.vx v28, %0, v0" ::"r"(f0_buf));
        f0_buf = f[0 + base_idx_0];
      }

      if (ch != C - 1) {
        int32_t base_idx_0 = (ch + 1) * fch_len;

        __asm__ volatile("vmacc.vx v16, %0, v2" ::"r"(f6_buf));
        f6_buf = f[42 + base_idx_0];
        __asm__ volatile("vmacc.vx v18, %0, v2" ::"r"(f5_buf));
        f5_buf = f[35 + base_idx_0];
        __asm__ volatile("vmacc.vx v20, %0, v2" ::"r"(f4_buf));
        f4_buf = f[28 + base_idx_0];
        __asm__ volatile("vmacc.vx v22, %0, v2" ::"r"(f3_buf));
        f3_buf = f[21 + base_idx_0];
        __asm__ volatile("vmacc.vx v24, %0, v2" ::"r"(f2_buf));
        f2_buf = f[14 + base_idx_0];
        __asm__ volatile("vmacc.vx v26, %0, v2" ::"r"(f1_buf));
        f1_buf = f[7 + base_idx_0];
        __asm__ volatile("vmacc.vx v28, %0, v2" ::"r"(f0_buf));
        f0_buf = f[0 + base_idx_0];
      }
    }

    // The last iteration is used to mask the latency of the store and the moves
    // Use buffered coefficients not to stall CVA6 for coherency
    f6_buf = f[42];
    __asm__ volatile("vmacc.vx v16, %0, v2" ::"r"(fl6));
    f5_buf = f[35];
    __asm__ volatile("vse16.v  v16, (%0); add %0, %0, %1" : "+&r"(o) : "r"(ldo));
    __asm__ volatile("vmacc.vx v18, %0, v2" ::"r"(fl5));
    __asm__ volatile("vmv.v.v v16, v18");
    __asm__ volatile("vmacc.vx v20, %0, v2" ::"r"(fl4));
    __asm__ volatile("vmv.v.v v18, v20");
    f4_buf = f[28];
    __asm__ volatile("vmacc.vx v22, %0, v2" ::"r"(fl3));
    __asm__ volatile("vmv.v.v v20, v22");
    f3_buf = f[21];
    __asm__ volatile("vmacc.vx v24, %0, v2" ::"r"(fl2));
    __asm__ volatile("vmv.v.v v22, v24");
    f2_buf = f[14];
    __asm__ volatile("vmacc.vx v26, %0, v2" ::"r"(fl1));
    __asm__ volatile("vmv.v.v v24, v26");
    f1_buf = f[7];
    __asm__ volatile("vmacc.vx v28, %0, v2" ::"r"(fl0));
    __asm__ volatile("vmv.v.v v26, v28");
    f0_buf = f[0];

    // Bump the input ptr
    i_ += N + F - 1;
  }

  ////////////////////////
  // Row I-F -> (I-1)-3 //
  ////////////////////////

  for (int32_t ch = 0; ch < C; ++ch) {

    // Point to the first element of the channel ch
    i__ = i_ + ch * ich_len;

    // Point to the scalar elements to insert during a slide
    // i_slide_ptr_0 has already been computed
    i_slide_ptr_0 = i__ + n_ + 0 * (N + F - 1);
    i_slide_ptr_1 = i__ + n_ + 1 * (N + F - 1);
    i_slide_ptr_2 = i__ + n_ + 2 * (N + F - 1);
    i_slide_ptr_3 = i__ + n_ + 3 * (N + F - 1);

    // Load other three input rows (one was already loaded)
    __asm__ volatile("vle16.v v0, (%0); add %0, %0, %1"
                 : "+&r"(i__)
                 : "r"(ldi_pad));
    __asm__ volatile("vle16.v v4, (%0); add %0, %0, %1"
                 : "+&r"(i__)
                 : "r"(ldi_pad));
    __asm__ volatile("vle16.v v8, (%0); add %0, %0, %1"
                 : "+&r"(i__)
                 : "r"(ldi_pad));
    __asm__ volatile("vle16.v v12, (%0); add %0, %0, %1"
                 : "+&r"(i__)
                 : "r"(ldi_pad));

    // Main kernel, unrolled by 2
    // Process 4 input rows
    for (int k = 0; k < F / 2; ++k) {
      // Two base indexes because of the unrolling
      // Point to the first element of the current column (k) of the current
      // channel (ch) of the filter (f)
      int32_t base_idx_0 = (2 * k) + (ch * fch_len);
      // Point to the first element of the current column (k+1) of the current
      // channel (ch) of the filter (f)
      int32_t base_idx_1 = (2 * k + 1) + (ch * fch_len);

      __asm__ volatile("vslide1down.vx v2, v0, %0" ::"r"(*i_slide_ptr_0++));
      __asm__ volatile("vmacc.vx v16, %0, v0" ::"r"(f[42 + base_idx_0]));
      __asm__ volatile("vmacc.vx v18, %0, v0" ::"r"(f[35 + base_idx_0]));
      __asm__ volatile("vmacc.vx v20, %0, v0" ::"r"(f[28 + base_idx_0]));
      __asm__ volatile("vmacc.vx v22, %0, v0" ::"r"(f[21 + base_idx_0]));
      __asm__ volatile("vmacc.vx v24, %0, v0" ::"r"(f[14 + base_idx_0]));
      __asm__ volatile("vmacc.vx v26, %0, v0" ::"r"(f[7 + base_idx_0]));
      if ((k | ch) == 0)
        __asm__ volatile("vmul.vx v28, v0, %0" ::"r"(f[0 + base_idx_0]));
      else
        __asm__ volatile("vmacc.vx v28, %0, v0" ::"r"(f[0 + base_idx_0]));
      __asm__ volatile("vslide1down.vx v6, v4, %0" ::"r"(*i_slide_ptr_1++));
      __asm__ volatile("vmacc.vx v18, %0, v4" ::"r"(f[42 + base_idx_0]));
      __asm__ volatile("vmacc.vx v20, %0, v4" ::"r"(f[35 + base_idx_0]));
      __asm__ volatile("vmacc.vx v22, %0, v4" ::"r"(f[28 + base_idx_0]));
      __asm__ volatile("vmacc.vx v24, %0, v4" ::"r"(f[21 + base_idx_0]));
      __asm__ volatile("vmacc.vx v26, %0, v4" ::"r"(f[14 + base_idx_0]));
      __asm__ volatile("vmacc.vx v28, %0, v4" ::"r"(f[7 + base_idx_0]));
      __asm__ volatile("vslide1down.vx v10, v8, %0" ::"r"(*i_slide_ptr_2++));
      __asm__ volatile("vmacc.vx v20, %0, v8" ::"r"(f[42 + base_idx_0]));
      __asm__ volatile("vmacc.vx v22, %0, v8" ::"r"(f[35 + base_idx_0]));
      __asm__ volatile("vmacc.vx v24, %0, v8" ::"r"(f[28 + base_idx_0]));
      __asm__ volatile("vmacc.vx v26, %0, v8" ::"r"(f[21 + base_idx_0]));
      __asm__ volatile("vmacc.vx v28, %0, v8" ::"r"(f[14 + base_idx_0]));
      __asm__ volatile("vslide1down.vx v14, v12, %0" ::"r"(*i_slide_ptr_3++));
      __asm__ volatile("vmacc.vx v22, %0, v12" ::"r"(f[42 + base_idx_0]));
      __asm__ volatile("vmacc.vx v24, %0, v12" ::"r"(f[35 + base_idx_0]));
      __asm__ volatile("vmacc.vx v26, %0, v12" ::"r"(f[28 + base_idx_0]));
      __asm__ volatile("vmacc.vx v28, %0, v12" ::"r"(f[21 + base_idx_0]));

      __asm__ volatile("vslide1down.vx v0, v2, %0" ::"r"(*i_slide_ptr_0++));
      __asm__ volatile("vmacc.vx v16, %0, v2" ::"r"(f[42 + base_idx_1]));
      __asm__ volatile("vmacc.vx v18, %0, v2" ::"r"(f[35 + base_idx_1]));
      __asm__ volatile("vmacc.vx v20, %0, v2" ::"r"(f[28 + base_idx_1]));
      __asm__ volatile("vmacc.vx v22, %0, v2" ::"r"(f[21 + base_idx_1]));
      __asm__ volatile("vmacc.vx v24, %0, v2" ::"r"(f[14 + base_idx_1]));
      __asm__ volatile("vmacc.vx v26, %0, v2" ::"r"(f[7 + base_idx_1]));
      __asm__ volatile("vmacc.vx v28, %0, v2" ::"r"(f[0 + base_idx_1]));
      __asm__ volatile("vslide1down.vx v4, v6, %0" ::"r"(*i_slide_ptr_1++));
      __asm__ volatile("vmacc.vx v18, %0, v6" ::"r"(f[42 + base_idx_1]));
      __asm__ volatile("vmacc.vx v20, %0, v6" ::"r"(f[35 + base_idx_1]));
      __asm__ volatile("vmacc.vx v22, %0, v6" ::"r"(f[28 + base_idx_1]));
      __asm__ volatile("vmacc.vx v24, %0, v6" ::"r"(f[21 + base_idx_1]));
      __asm__ volatile("vmacc.vx v26, %0, v6" ::"r"(f[14 + base_idx_1]));
      __asm__ volatile("vmacc.vx v28, %0, v6" ::"r"(f[7 + base_idx_1]));
      __asm__ volatile("vslide1down.vx v8, v10, %0" ::"r"(*i_slide_ptr_2++));
      __asm__ volatile("vmacc.vx v20, %0, v10" ::"r"(f[42 + base_idx_1]));
      __asm__ volatile("vmacc.vx v22, %0, v10" ::"r"(f[35 + base_idx_1]));
      __asm__ volatile("vmacc.vx v24, %0, v10" ::"r"(f[28 + base_idx_1]));
      __asm__ volatile("vmacc.vx v26, %0, v10" ::"r"(f[21 + base_idx_1]));
      __asm__ volatile("vmacc.vx v28, %0, v10" ::"r"(f[14 + base_idx_1]));
      __asm__ volatile("vslide1down.vx v12, v14, %0" ::"r"(*i_slide_ptr_3++));
      __asm__ volatile("vmacc.vx v22, %0, v14" ::"r"(f[42 + base_idx_1]));
      __asm__ volatile("vmacc.vx v24, %0, v14" ::"r"(f[35 + base_idx_1]));
      __asm__ volatile("vmacc.vx v26, %0, v14" ::"r"(f[28 + base_idx_1]));
      __asm__ volatile("vmacc.vx v28, %0, v14" ::"r"(f[21 + base_idx_1]));
    }

    if (ch != C - 1) {
      int32_t base_idx_0 = (F - 1) + (ch * fch_len);

      __asm__ volatile("vmacc.vx v16, %0, v0" ::"r"(f[42 + base_idx_0]));
      __asm__ volatile("vmacc.vx v18, %0, v0" ::"r"(f[35 + base_idx_0]));
      __asm__ volatile("vmacc.vx v20, %0, v0" ::"r"(f[28 + base_idx_0]));
      __asm__ volatile("vmacc.vx v22, %0, v0" ::"r"(f[21 + base_idx_0]));
      __asm__ volatile("vmacc.vx v24, %0, v0" ::"r"(f[14 + base_idx_0]));
      __asm__ volatile("vmacc.vx v26, %0, v0" ::"r"(f[7 + base_idx_0]));
      __asm__ volatile("vmacc.vx v28, %0, v0" ::"r"(f[0 + base_idx_0]));
      __asm__ volatile("vmacc.vx v18, %0, v4" ::"r"(f[42 + base_idx_0]));
      __asm__ volatile("vmacc.vx v20, %0, v4" ::"r"(f[35 + base_idx_0]));
      __asm__ volatile("vmacc.vx v22, %0, v4" ::"r"(f[28 + base_idx_0]));
      __asm__ volatile("vmacc.vx v24, %0, v4" ::"r"(f[21 + base_idx_0]));
      __asm__ volatile("vmacc.vx v26, %0, v4" ::"r"(f[14 + base_idx_0]));
      __asm__ volatile("vmacc.vx v28, %0, v4" ::"r"(f[7 + base_idx_0]));
      __asm__ volatile("vmacc.vx v20, %0, v8" ::"r"(f[42 + base_idx_0]));
      __asm__ volatile("vmacc.vx v22, %0, v8" ::"r"(f[35 + base_idx_0]));
      __asm__ volatile("vmacc.vx v24, %0, v8" ::"r"(f[28 + base_idx_0]));
      __asm__ volatile("vmacc.vx v26, %0, v8" ::"r"(f[21 + base_idx_0]));
      __asm__ volatile("vmacc.vx v28, %0, v8" ::"r"(f[14 + base_idx_0]));
      __asm__ volatile("vmacc.vx v22, %0, v12" ::"r"(f[42 + base_idx_0]));
      __asm__ volatile("vmacc.vx v24, %0, v12" ::"r"(f[35 + base_idx_0]));
      __asm__ volatile("vmacc.vx v26, %0, v12" ::"r"(f[28 + base_idx_0]));
      __asm__ volatile("vmacc.vx v28, %0, v12" ::"r"(f[21 + base_idx_0]));
    }
  }

  __asm__ volatile("vmacc.vx v16, %0, v0" ::"r"(fl6));
  __asm__ volatile("vmacc.vx v18, %0, v0" ::"r"(fl5));
  __asm__ volatile("vmacc.vx v20, %0, v0" ::"r"(fl4));
  __asm__ volatile("vse16.v  v16, (%0); add %0, %0, %1" : "+&r"(o) : "r"(ldo));
  __asm__ volatile("vmacc.vx v22, %0, v0" ::"r"(fl3));
  __asm__ volatile("vmacc.vx v24, %0, v0" ::"r"(fl2));
  __asm__ volatile("vmacc.vx v26, %0, v0" ::"r"(fl1));
  __asm__ volatile("vmacc.vx v28, %0, v0" ::"r"(fl0));
  __asm__ volatile("vmacc.vx v18, %0, v4" ::"r"(fl6));
  __asm__ volatile("vmacc.vx v20, %0, v4" ::"r"(fl5));
  __asm__ volatile("vse16.v  v18, (%0); add %0, %0, %1" : "+&r"(o) : "r"(ldo));
  __asm__ volatile("vmacc.vx v22, %0, v4" ::"r"(fl4));
  __asm__ volatile("vmacc.vx v24, %0, v4" ::"r"(fl3));
  __asm__ volatile("vmacc.vx v26, %0, v4" ::"r"(fl2));
  __asm__ volatile("vmacc.vx v28, %0, v4" ::"r"(fl1));
  __asm__ volatile("vmacc.vx v20, %0, v8" ::"r"(fl6));
  __asm__ volatile("vmacc.vx v22, %0, v8" ::"r"(fl5));
  __asm__ volatile("vse16.v  v20, (%0); add %0, %0, %1" : "+&r"(o) : "r"(ldo));
  __asm__ volatile("vmacc.vx v24, %0, v8" ::"r"(fl4));
  __asm__ volatile("vmacc.vx v26, %0, v8" ::"r"(fl3));
  __asm__ volatile("vmacc.vx v28, %0, v8" ::"r"(fl2));
  __asm__ volatile("vmacc.vx v22, %0, v12" ::"r"(fl6));
  __asm__ volatile("vse16.v  v22, (%0); add %0, %0, %1" : "+&r"(o) : "r"(ldo));
  __asm__ volatile("vmacc.vx v24, %0, v12" ::"r"(fl5));
  __asm__ volatile("vmacc.vx v26, %0, v12" ::"r"(fl4));
  __asm__ volatile("vmacc.vx v28, %0, v12" ::"r"(fl3));

  // Bump the input ptr
  i_ += 4 * (N + F - 1);

  //////////////////////////
  // Row (I-1)-3 -> (I-1) //
  //////////////////////////

  for (int32_t ch = 0; ch < C; ++ch) {

    // Point to the first element of the channel ch
    i__ = i_ + ch * ich_len;

    // Start calculating the next pointers to the elements to be slided in
    i_slide_ptr_0 = i__ + n_ + 0 * (N + F - 1);
    i_slide_ptr_1 = i__ + n_ + 1 * (N + F - 1);
    i_slide_ptr_2 = i__ + n_ + 2 * (N + F - 1);

    __asm__ volatile("vle16.v v2, (%0); add %0, %0, %1"
                 : "+&r"(i__)
                 : "r"(ldi_pad));
    __asm__ volatile("vle16.v v6, (%0); add %0, %0, %1"
                 : "+&r"(i__)
                 : "r"(ldi_pad));
    __asm__ volatile("vle16.v v10, (%0); add %0, %0, %1"
                 : "+&r"(i__)
                 : "r"(ldi_pad));

    // Main kernel, unrolled by 2
    for (int k = 0; k < F / 2; ++k) {
      // Two base indexes because of the unrolling
      // Point to the first element of the current column (k) of the current
      // channel (ch) of the filter (f)
      int32_t base_idx_0 = (2 * k) + (ch * fch_len);
      // Point to the first element of the current column (k+1) of the current
      // channel (ch) of the filter (f)
      int32_t base_idx_1 = (2 * k + 1) + (ch * fch_len);

      __asm__ volatile("vslide1down.vx v0, v2, %0" ::"r"(*i_slide_ptr_0++));
      __asm__ volatile("vmacc.vx v24, %0, v2" ::"r"(f[42 + base_idx_0]));
      __asm__ volatile("vmacc.vx v26, %0, v2" ::"r"(f[35 + base_idx_0]));
      __asm__ volatile("vslide1down.vx v4, v6, %0" ::"r"(*i_slide_ptr_1++));
      __asm__ volatile("vmacc.vx v28, %0, v2" ::"r"(f[28 + base_idx_0]));
      __asm__ volatile("vmacc.vx v26, %0, v6" ::"r"(f[42 + base_idx_0]));
      __asm__ volatile("vslide1down.vx v8, v10, %0" ::"r"(*i_slide_ptr_2++));
      __asm__ volatile("vmacc.vx v28, %0, v6" ::"r"(f[35 + base_idx_0]));
      __asm__ volatile("vmacc.vx v28, %0, v10" ::"r"(f[42 + base_idx_0]));

      __asm__ volatile("vslide1down.vx v2, v0, %0" ::"r"(*i_slide_ptr_0++));
      __asm__ volatile("vmacc.vx v24, %0, v0" ::"r"(f[42 + base_idx_1]));
      __asm__ volatile("vmacc.vx v26, %0, v0" ::"r"(f[35 + base_idx_1]));
      __asm__ volatile("vslide1down.vx v6, v4, %0" ::"r"(*i_slide_ptr_1++));
      __asm__ volatile("vmacc.vx v28, %0, v0" ::"r"(f[28 + base_idx_1]));
      __asm__ volatile("vmacc.vx v26, %0, v4" ::"r"(f[42 + base_idx_1]));
      __asm__ volatile("vslide1down.vx v10, v8, %0" ::"r"(*i_slide_ptr_2++));
      __asm__ volatile("vmacc.vx v28, %0, v4" ::"r"(f[35 + base_idx_1]));
      __asm__ volatile("vmacc.vx v28, %0, v8" ::"r"(f[42 + base_idx_1]));
    }

    if (ch != C - 1) {
      int32_t base_idx_0 = (F - 1) + (ch * fch_len);

      __asm__ volatile("vmacc.vx v24, %0, v2" ::"r"(f[42 + base_idx_0]));
      __asm__ volatile("vmacc.vx v26, %0, v2" ::"r"(f[35 + base_idx_0]));
      __asm__ volatile("vmacc.vx v28, %0, v2" ::"r"(f[28 + base_idx_0]));
      __asm__ volatile("vmacc.vx v26, %0, v6" ::"r"(f[42 + base_idx_0]));
      __asm__ volatile("vmacc.vx v28, %0, v6" ::"r"(f[35 + base_idx_0]));
      __asm__ volatile("vmacc.vx v28, %0, v10" ::"r"(f[42 + base_idx_0]));
    }
  }

  __asm__ volatile("vmacc.vx v24, %0, v2" ::"r"(fl6));
  __asm__ volatile("vse16.v  v24, (%0); add %0, %0, %1" : "+&r"(o) : "r"(ldo));
  __asm__ volatile("vmacc.vx v26, %0, v2" ::"r"(fl5));
  __asm__ volatile("vmacc.vx v28, %0, v2" ::"r"(fl4));
  __asm__ volatile("vmacc.vx v26, %0, v6" ::"r"(fl6));
  __asm__ volatile("vse16.v  v26, (%0); add %0, %0, %1" : "+&r"(o) : "r"(ldo));
  __asm__ volatile("vmacc.vx v28, %0, v6" ::"r"(fl5));
  __asm__ volatile("vmacc.vx v28, %0, v10" ::"r"(fl6));
  __asm__ volatile("vse16.v  v28, (%0); add %0, %0, %1" : "+&r"(o) : "r"(ldo));
}

/*
  ////////////////////
  // MAIN ALGOMITHM //
  ////////////////////
  // Start calculating the pointer to the next element to be slided in
  i_slide_ptr_0 = i + N;
  // Load one input row
  __asm__ volatile("vle64.v v0, (%0); add %0, %0, %1" : "+&r"(i) : "r"(ldi_pad));
  // Kernel
  for (int k = 0; k < F; ++k) {
    // Nalculate F*F contributions of the input rows, on F different output rows
    // v28 should be initialized during the first iteration
    __asm__ volatile("vmacc.vx v16, %0, v0" :: "f"(f[42  + (2*k)]));
    __asm__ volatile("vmacc.vx v18, %0, v0" :: "f"(f[35  + (2*k)]));
    __asm__ volatile("vmacc.vx v20, %0, v0" :: "f"(f[28 + (2*k)]));
    __asm__ volatile("vmacc.vx v22, %0, v0" :: "f"(f[21 + (2*k)]));
    __asm__ volatile("vmacc.vx v24, %0, v0" :: "f"(f[14 + (2*k)]));
    __asm__ volatile("vmacc.vx v26, %0, v0" :: "f"(f[7 + (2*k)]));
    if (k == 0)
      __asm__ volatile("vmul.vx v28, v0, %0" :: "f"(f[0 + (2*k)]));
    else
      __asm__ volatile("vmacc.vx v28, %0, v0" :: "f"(f[0 + (2*k)]));
    // Slide the input row by one, and inject the next scalar element of the row
    __asm__ volatile("vslide1down.vx v0, v0, %0" :: "f"(*i_slide_ptr_0++));
  }
  // Store one output row
  __asm__ volatile("vse64.v  v16, (%0); add %0, %0, %1" : "+&r"(o) : "r"(ldo));
  // Move all the input rows to return to the initial situation
  // To avoid these operations, unroll the loop via software, renaming the
  registers manually __asm__ volatile("vmv.v.v v16, v18"); __asm__ volatile("vmv.v.v
  v18, v20"); __asm__ volatile("vmv.v.v v20, v22"); __asm__ volatile("vmv.v.v v22,
  v24"); __asm__ volatile("vmv.v.v v24, v26"); __asm__ volatile("vmv.v.v v26, v28");
*/