about summary refs log tree commit diff
diff options
context:
space:
mode:
authormrcodetastic <12006953+mrcodetastic@users.noreply.github.com>2025-02-18 01:36:58 +0000
committermrcodetastic <12006953+mrcodetastic@users.noreply.github.com>2025-02-18 01:36:58 +0000
commitd77a90f809456b06d10ad28f2b5cc74c2e087db5 (patch)
tree9430376aa59a5b58e252be51c0b1f77630b11171
parent3a78a9dfb68c695a4ee194cad992ef6296525d5b (diff)
Changes to Virtual Display
Templating based for performance and extendibility.
-rw-r--r--doc/VirtualMatrixPanel.odpbin240953 -> 191552 bytes
-rw-r--r--src/ESP32-HUB75-MatrixPanel-I2S-DMA.h85
-rw-r--r--src/ESP32-VirtualMatrixPanel-I2S-DMA.h21
-rw-r--r--src/HUB75-VirtualMatrixPanelT.hpp450
-rw-r--r--src/platforms/esp32s3/gdma_lcd_parallel16.cpp2
5 files changed, 473 insertions, 85 deletions
diff --git a/doc/VirtualMatrixPanel.odp b/doc/VirtualMatrixPanel.odp
index 748a211..c9ce91f 100644
--- a/doc/VirtualMatrixPanel.odp
+++ b/doc/VirtualMatrixPanel.odp
Binary files differdiff --git a/src/ESP32-HUB75-MatrixPanel-I2S-DMA.h b/src/ESP32-HUB75-MatrixPanel-I2S-DMA.h
index f0c60ec..43e6178 100644
--- a/src/ESP32-HUB75-MatrixPanel-I2S-DMA.h
+++ b/src/ESP32-HUB75-MatrixPanel-I2S-DMA.h
@@ -196,33 +196,27 @@ struct frameStruct
 };
 
 /***************************************************************************************/
-// C/p'ed from https://ledshield.wordpress.com/2012/11/13/led-brightness-to-your-eye-gamma-correction-no/
-//  Example calculator: https://gist.github.com/mathiasvr/19ce1d7b6caeab230934080ae1f1380e
-//  need to make sure this would end up in RAM for fastest access
 #ifndef NO_CIE1931
-/*
-static const uint8_t DRAM_ATTR lumConvTab[]={
-    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 16, 16, 17, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 24, 25, 25, 26, 27, 27, 28, 28, 29, 30, 30, 31, 31, 32, 33, 33, 34, 35, 35, 36, 37, 38, 38, 39, 40, 41, 41, 42, 43, 44, 45, 45, 46, 47, 48, 49, 50, 51, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 73, 74, 75, 76, 77, 78, 80, 81, 82, 83, 84, 86, 87, 88, 90, 91, 92, 93, 95, 96, 98, 99, 100, 102, 103, 105, 106, 107, 109, 110, 112, 113, 115, 116, 118, 120, 121, 123, 124, 126, 128, 129, 131, 133, 134, 136, 138, 139, 141, 143, 145, 146, 148, 150, 152, 154, 156, 157, 159, 161, 163, 165, 167, 169, 171, 173, 175, 177, 179, 181, 183, 185, 187, 189, 192, 194, 196, 198, 200, 203, 205, 207, 209, 212, 214, 216, 218, 221, 223, 226, 228, 230, 233, 235, 238, 240, 243, 245, 248, 250, 253, 255, 255};
-*/
-// This is 16-bit version of the table,
-// the constants taken from the example in the article above, each entries subtracted from 65535:
-static const uint16_t DRAM_ATTR lumConvTab[] = {
-    0, 27, 56, 84, 113, 141, 170, 198, 227, 255, 284, 312, 340, 369, 397, 426,
-    454, 483, 511, 540, 568, 597, 626, 657, 688, 720, 754, 788, 824, 860, 898, 936,
-    976, 1017, 1059, 1102, 1146, 1191, 1238, 1286, 1335, 1385, 1436, 1489, 1543, 1598, 1655, 1713,
-    1772, 1833, 1895, 1958, 2023, 2089, 2156, 2225, 2296, 2368, 2441, 2516, 2592, 2670, 2750, 2831,
-    2914, 2998, 3084, 3171, 3260, 3351, 3443, 3537, 3633, 3731, 3830, 3931, 4034, 4138, 4245, 4353,
-    4463, 4574, 4688, 4803, 4921, 5040, 5161, 5284, 5409, 5536, 5665, 5796, 5929, 6064, 6201, 6340,
-    6482, 6625, 6770, 6917, 7067, 7219, 7372, 7528, 7687, 7847, 8010, 8174, 8341, 8511, 8682, 8856,
-    9032, 9211, 9392, 9575, 9761, 9949, 10139, 10332, 10527, 10725, 10925, 11127, 11332, 11540, 11750, 11963,
-    12178, 12395, 12616, 12839, 13064, 13292, 13523, 13757, 13993, 14231, 14473, 14717, 14964, 15214, 15466, 15722,
-    15980, 16240, 16504, 16771, 17040, 17312, 17587, 17865, 18146, 18430, 18717, 19006, 19299, 19595, 19894, 20195,
-    20500, 20808, 21119, 21433, 21750, 22070, 22393, 22720, 23049, 23382, 23718, 24057, 24400, 24745, 25094, 25446,
-    25802, 26160, 26522, 26888, 27256, 27628, 28004, 28382, 28765, 29150, 29539, 29932, 30328, 30727, 31130, 31536,
-    31946, 32360, 32777, 33197, 33622, 34049, 34481, 34916, 35354, 35797, 36243, 36692, 37146, 37603, 38064, 38528,
-    38996, 39469, 39945, 40424, 40908, 41395, 41886, 42382, 42881, 43383, 43890, 44401, 44916, 45434, 45957, 46484,
-    47014, 47549, 48088, 48630, 49177, 49728, 50283, 50842, 51406, 51973, 52545, 53120, 53700, 54284, 54873, 55465,
-    56062, 56663, 57269, 57878, 58492, 59111, 59733, 60360, 60992, 61627, 62268, 62912, 63561, 64215, 64873, 65535};
+// C/p'ed from https://ledshield.wordpress.com/2012/11/13/led-brightness-to-your-eye-gamma-correction-no/
+// 16 bit resolution of https://gist.github.com/mathiasvr/19ce1d7b6caeab230934080ae1f1380e
+static const uint16_t lumConvTab[] = {
+0,   28,   57,   85,  114,  142,  171,  199,  228,  256,  285,  313,  342,  370,  399,  427,
+456,  484,  513,  541,  570,  598,  627,  658,  689,  721,  755,  789,  825,  861,  899,  937,
+977, 1018, 1060, 1103, 1147, 1192, 1239, 1287, 1336, 1386, 1437, 1490, 1544, 1599, 1656, 1714,
+1773, 1834, 1896, 1959, 2024, 2090, 2157, 2226, 2297, 2369, 2442, 2517, 2593, 2671, 2751, 2832,
+2914, 2999, 3085, 3172, 3261, 3352, 3444, 3538, 3634, 3732, 3831, 3932, 4035, 4139, 4245, 4354,
+4464, 4575, 4689, 4804, 4922, 5041, 5162, 5285, 5410, 5537, 5666, 5797, 5930, 6065, 6202, 6341,
+6482, 6626, 6771, 6918, 7068, 7220, 7373, 7529, 7687, 7848, 8010, 8175, 8342, 8512, 8683, 8857,
+9033, 9212, 9393, 9576, 9762, 9949, 10140, 10333, 10528, 10725, 10926, 11128, 11333, 11541, 11751, 11963,
+12179, 12396, 12617, 12840, 13065, 13293, 13524, 13757, 13993, 14232, 14474, 14718, 14965, 15215, 15467, 15722,
+15980, 16241, 16505, 16771, 17041, 17313, 17588, 17866, 18147, 18431, 18717, 19007, 19300, 19596, 19894, 20196,
+20501, 20809, 21119, 21433, 21750, 22071, 22394, 22720, 23050, 23383, 23719, 24058, 24400, 24746, 25095, 25447,
+25802, 26161, 26523, 26888, 27257, 27629, 28004, 28383, 28765, 29151, 29540, 29932, 30328, 30728, 31131, 31537,
+31947, 32360, 32777, 33198, 33622, 34050, 34481, 34916, 35355, 35797, 36243, 36693, 37146, 37603, 38064, 38529,
+38997, 39469, 39945, 40425, 40908, 41396, 41887, 42382, 42881, 43384, 43891, 44401, 44916, 45435, 45957, 46484,
+47015, 47549, 48088, 48631, 49178, 49728, 50283, 50843, 51406, 51973, 52545, 53120, 53700, 54284, 54873, 55465,
+56062, 56663, 57269, 57878, 58492, 59111, 59733, 60360, 60992, 61627, 62268, 62912, 63561, 64215, 64873, 65535,
+};
 #endif
 
 /** @brief - configuration values for HUB75_I2S driver
@@ -588,13 +582,8 @@ public:
 
   // Colour 444 is a 4 bit scale, so 0 to 15, colour 565 takes a 0-255 bit value, so scale up by 255/15 (i.e. 17)!
   static uint16_t color444(uint8_t r, uint8_t g, uint8_t b) { return color565(r * 17, g * 17, b * 17); }
-
-  // Converts RGB888 to RGB565
   static uint16_t color565(uint8_t r, uint8_t g, uint8_t b); // This is what is used by Adafruit GFX!
 
-  // Converts RGB333 to RGB565
-  static uint16_t color333(uint8_t r, uint8_t g, uint8_t b); // This is what is used by Adafruit GFX! Not sure why they have a capital 'C' for this particular function.
-
   /**
    * @brief - convert RGB565 to RGB888
    * @param uint16_t colour - RGB565 input colour
@@ -953,40 +942,6 @@ inline uint16_t MatrixPanel_I2S_DMA::color565(uint8_t r, uint8_t g, uint8_t b)
   return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
 }
 
-// Promote 3/3/3 RGB to Adafruit_GFX 5/6/5 RRRrrGGGgggBBBbb
-inline uint16_t MatrixPanel_I2S_DMA::color333(uint8_t r, uint8_t g, uint8_t b)
-{
-  return ((r & 0x7) << 13) | ((r & 0x6) << 10) | ((g & 0x7) << 8) | ((g & 0x7) << 5) | ((b & 0x7) << 2) | ((b & 0x6) >> 1);
-}
-
-inline void MatrixPanel_I2S_DMA::drawIcon(int *ico, int16_t x, int16_t y, int16_t cols, int16_t rows)
-{
-  /*  drawIcon draws a C style bitmap.
-  //  Example 10x5px bitmap of a yellow sun
-  //
-    int half_sun [50] = {
-        0x0000, 0x0000, 0x0000, 0xffe0, 0x0000, 0x0000, 0xffe0, 0x0000, 0x0000, 0x0000,
-        0x0000, 0xffe0, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xffe0, 0x0000,
-        0x0000, 0x0000, 0x0000, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0x0000, 0x0000, 0x0000,
-        0xffe0, 0x0000, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0x0000, 0xffe0,
-        0x0000, 0x0000, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0x0000, 0x0000,
-    };
-
-    MatrixPanel_I2S_DMA matrix;
-
-    matrix.drawIcon (half_sun, 0,0,10,5);
-  */
-
-  int i, j;
-  for (i = 0; i < rows; i++)
-  {
-    for (j = 0; j < cols; j++)
-    {
-      drawPixel(x + j, y + i, (uint16_t)ico[i * cols + j]);
-    }
-  }
-}
-
 #endif
 
 // Credits: Louis Beaudoin <https://github.com/pixelmatix/SmartMatrix/tree/teensylc>
diff --git a/src/ESP32-VirtualMatrixPanel-I2S-DMA.h b/src/ESP32-VirtualMatrixPanel-I2S-DMA.h
index 0ad453b..7a01bab 100644
--- a/src/ESP32-VirtualMatrixPanel-I2S-DMA.h
+++ b/src/ESP32-VirtualMatrixPanel-I2S-DMA.h
@@ -74,6 +74,7 @@ enum PANEL_CHAIN_TYPE
     CHAIN_BOTTOM_LEFT_UP_ZZ
 };
 
+[[deprecated("VirtualMatrixPanel is depreciated. Please include 'HUB75-VirtualMatrixPanelT.hpp' and use VirtualMatrixPanelT instead. Refer to the documentation.")]]
 #ifdef USE_GFX_LITE
 class VirtualMatrixPanel : public GFX
 #elif !defined NO_GFX
@@ -140,12 +141,8 @@ public:
     inline uint16_t height() const { return _virtualResY; }
 #endif
 
-    uint16_t color444(uint8_t r, uint8_t g, uint8_t b)
-    {
-        return display->color444(r, g, b);
-    }
+    uint16_t color444(uint8_t r, uint8_t g, uint8_t b) { return display->color444(r, g, b); }
     uint16_t color565(uint8_t r, uint8_t g, uint8_t b) { return display->color565(r, g, b); }
-    uint16_t color333(uint8_t r, uint8_t g, uint8_t b) { return display->color333(r, g, b); }
 
     void flipDMABuffer() { display->flipDMABuffer(); }
     void drawDisplayTest();
@@ -558,19 +555,5 @@ inline void VirtualMatrixPanel::drawDisplayTest()
 }
 #endif
 
-/*
-// need to recreate this one, as it wouldn't work to just map where it starts.
-inline void VirtualMatrixPanel::drawIcon (int *ico, int16_t x, int16_t y, int16_t icon_cols, int16_t icon_rows) {
-  int i, j;
-  for (i = 0; i < icon_rows; i++) {
-    for (j = 0; j < icon_cols; j++) {
-      // This is a call to this libraries version of drawPixel
-      // which will map each pixel, which is what we want.
-      //drawPixelRGB565 (x + j, y + i, ico[i * module_cols + j]);
-    drawPixel (x + j, y + i, ico[i * icon_cols + j]);
-    }
-  }
-}
-*/
 
 #endif
diff --git a/src/HUB75-VirtualMatrixPanelT.hpp b/src/HUB75-VirtualMatrixPanelT.hpp
new file mode 100644
index 0000000..b852ea6
--- /dev/null
+++ b/src/HUB75-VirtualMatrixPanelT.hpp
@@ -0,0 +1,450 @@
+/**
+ * @file VirtualMatrixPanelT.hpp
+ * @brief Templated Virtual Matrix Panel class for HUB75 displays.
+ *
+ * This header defines the VirtualMatrixPanelT template class which maps virtual pixel
+ * coordinates to physical LED coordinates. It supports compile‐time configuration for:
+ *   - Panel chain type (PANEL_CHAIN_TYPE)
+ *   - Scan type mapping (via a policy class, default is y)
+ *   - A compile‐time scale factor (each virtual pixel is drawn as a block)
+ *
+ * Runtime rotation is supported via setRotation(). Depending on the build options,
+ * the class conditionally inherits from Adafruit_GFX, GFX_Lite, or stands alone.
+ *
+ * @tparam ChainType       Compile‐time panel chain configuration.
+ * @tparam ScanType        Policy type implementing a static apply() function for mapping.
+ *                         Default is DefaultScanType<STANDARD_TWO_SCAN>.
+ * @tparam ScaleFactor     Compile‐time zoom factor (each virtual pixel becomes a
+ *                         ScaleFactor x ScaleFactor block).
+ *
+ * @note The enum PANEL_SCAN_TYPE replaces the former PANEL_SCAN_RATE.
+ */
+ 
+
+
+#ifndef VIRTUAL_MATRIX_PANEL_TEMPLATE_H
+#define VIRTUAL_MATRIX_PANEL_TEMPLATE_H
+
+//#include <cstdint>
+#include "ESP32-HUB75-MatrixPanel-I2S-DMA.h"
+
+#ifdef USE_GFX_LITE
+  #include "GFX_Lite.h"
+  #include <Fonts/FreeSansBold12pt7b.h>
+#elif !defined(NO_GFX)
+  #include "Adafruit_GFX.h"
+  #include <Fonts/FreeSansBold12pt7b.h>
+#endif
+
+// ----------------------------------------------------------------------
+// Data structures and enums
+
+/**
+ * @brief Structure holding virtual/physical coordinate mapping.
+ */
+struct VirtualCoords {
+    int16_t x;
+    int16_t y;
+    int16_t virt_row; // chain of panels row (optional)
+    int16_t virt_col; // chain of panels col (optional)
+    VirtualCoords() : x(0), y(0), virt_row(0), virt_col(0) {}
+};
+
+/**
+ * @brief Panel scan types.
+ *
+ * Defines the different scanning modes.
+ */
+enum PANEL_SCAN_TYPE {
+    STANDARD_TWO_SCAN,
+    FOUR_SCAN_32PX_HIGH,   ///< Four-scan mode, 32-pixel high panels.
+    FOUR_SCAN_16PX_HIGH,   ///< Four-scan mode, 16-pixel high panels.
+    FOUR_SCAN_64PX_HIGH,   ///< Four-scan mode, 64-pixel high panels.
+    FOUR_SCAN_40PX_HIGH    ///< Four-scan mode, 40-pixel high panels.
+};
+
+/**
+ * @brief Panel chain types.
+ *
+ * Defines the physical chain configuration for multiple panels.
+ */
+enum PANEL_CHAIN_TYPE {
+    CHAIN_NONE,                 ///< No chaining.
+    CHAIN_TOP_LEFT_DOWN,        ///< Chain starting top-left, going down.
+    CHAIN_TOP_RIGHT_DOWN,       ///< Chain starting top-right, going down.
+    CHAIN_BOTTOM_LEFT_UP,       ///< Chain starting bottom-left, going up.
+    CHAIN_BOTTOM_RIGHT_UP,      ///< Chain starting bottom-right, going up.
+    CHAIN_TOP_LEFT_DOWN_ZZ,     ///< Zigzag chain starting top-left.
+    CHAIN_TOP_RIGHT_DOWN_ZZ,    ///< Zigzag chain starting top-right.
+    CHAIN_BOTTOM_RIGHT_UP_ZZ,   ///< Zigzag chain starting bottom-right.
+    CHAIN_BOTTOM_LEFT_UP_ZZ     ///< Zigzag chain starting bottom-left.
+};
+
+// ----------------------------------------------------------------------
+// Default Scan Rate Policy
+/**
+ * @brief Default policy for scan type mapping.
+ *
+ * This templated policy implements the static function apply() to remap
+ * coordinates according to the panel scan type. It uses the panel's pixel base
+ * to calculate offsets.
+ *
+ * @tparam Type The compile-time scan type (of type PANEL_SCAN_TYPE).
+ */
+template <PANEL_SCAN_TYPE ScanType>
+struct ScanTypeMapping {
+    static constexpr VirtualCoords apply(VirtualCoords coords, int virt_y, int panel_pixel_base) 
+	{
+        log_v("ScanTypeMapping: coords.x: %d, coords.y: %d, virt_y: %d, pixel_base: %d", coords.x, coords.y, virt_y, panel_pixel_base);
+
+        if constexpr (ScanType == FOUR_SCAN_64PX_HIGH || ScanType == FOUR_SCAN_32PX_HIGH) 
+		{
+            int adjusted_y = virt_y;
+            if constexpr (ScanType == FOUR_SCAN_64PX_HIGH) 
+			{
+                // As in the original code (with extra remapping for 64px high panels)
+                if ((virt_y & 8) != ((virt_y & 16) >> 1))
+                    adjusted_y = (((virt_y & 0b11000) ^ 0b11000) + (virt_y & 0b11100111));
+            }
+
+            if ((coords.y & 8) == 0) {
+                coords.x += (((coords.x / panel_pixel_base) + 1) * panel_pixel_base);
+			} else {
+                coords.x += ((coords.x / panel_pixel_base) * panel_pixel_base);
+			}
+			
+            coords.y = (adjusted_y >> 4) * 8 + (adjusted_y & 0b00000111);
+        }
+        else if constexpr (ScanType == FOUR_SCAN_16PX_HIGH) {
+            if ((coords.y & 4) == 0) {
+                coords.x += (((coords.x / panel_pixel_base) + 1) * panel_pixel_base);
+			} else {
+                coords.x += ((coords.x / panel_pixel_base) * panel_pixel_base);
+			}
+			
+            coords.y = (coords.y >> 3) * 4 + (coords.y & 0b00000011);
+        }
+        else if constexpr (ScanType == FOUR_SCAN_40PX_HIGH) {
+            
+            if (((coords.y) / 10) % 2 == 0) {				
+                coords.x += (((coords.x / panel_pixel_base) + 1) * panel_pixel_base);
+			} else {
+                coords.x += ((coords.x / panel_pixel_base) * panel_pixel_base);
+			}
+            coords.y = (coords.y / 20) * 10 + (coords.y % 10);
+        }
+        // For STANDARD_TWO_SCAN / NORMAL_ONE_SIXTEEN no remapping is done.
+        return coords;
+    }
+};
+
+// ----------------------------------------------------------------------
+// VirtualMatrixPanelT Declaration
+//
+// Template parameters:
+//   - ChainScanType: compile–time panel chain configuration.
+//   - ScanTypeMapping: a policy type implementing a static "apply" function 
+//                     (default is ScanTypeMapping<STANDARD_TWO_SCAN>).
+//   - ScaleFactor: a compile–time zoom factor (must be >= 1).
+#ifdef USE_GFX_LITE
+template <PANEL_CHAIN_TYPE ChainScanType,
+          class ScanTypeMapping = ScanTypeMapping<STANDARD_TWO_SCAN>,
+          int ScaleFactor = 1>
+class VirtualMatrixPanelT : public GFX {
+public:
+#elif !defined(NO_GFX)
+template <PANEL_CHAIN_TYPE ChainScanType,
+          class ScanTypeMapping = ScanTypeMapping<STANDARD_TWO_SCAN>,
+          int ScaleFactor = 1>
+class VirtualMatrixPanelT : public Adafruit_GFX {
+public:
+#else
+template <PANEL_CHAIN_TYPE ChainScanType,
+          class ScanTypeMapping = ScanTypeMapping<STANDARD_TWO_SCAN>,
+          int ScaleFactor = 1>
+class VirtualMatrixPanelT {
+public:
+#endif
+
+    // Constructor: pass the underlying MatrixPanel_I2S_DMA display,
+    // virtual module dimensions, and physical panel resolution.
+    // (Chain type is chosen at compile time.)
+    VirtualMatrixPanelT(uint8_t _vmodule_rows,
+                        uint8_t _vmodule_cols,
+                        uint8_t _panel_res_x,
+                        uint8_t _panel_res_y)
+#ifdef USE_GFX_LITE
+      : GFX(_vmodule_cols * _panel_res_x, _vmodule_rows * _panel_res_y),
+#elif !defined(NO_GFX)
+      : Adafruit_GFX(_vmodule_cols * _panel_res_x, _vmodule_rows * _panel_res_y),
+#endif
+        panel_res_x(_panel_res_x),
+        panel_res_y(_panel_res_y),
+        panel_pixel_base(_panel_res_x), // default pixel base is panel_res_x
+        vmodule_rows(_vmodule_rows),
+        vmodule_cols(_vmodule_cols),
+        virtual_res_x(_vmodule_cols * _panel_res_x),
+        virtual_res_y(_vmodule_rows * _panel_res_y),
+        dma_res_x(_panel_res_x * _vmodule_rows * _vmodule_cols - 1),
+        _virtual_res_x(virtual_res_x),
+        _virtual_res_y(virtual_res_y),
+        _rotate(0)
+    {
+        // Initialize with an invalid coordinate.
+        coords.x = coords.y = -1;
+    }
+
+    // ------------------------------------------------------------------
+    // Drawing methods
+    inline void drawPixel(int16_t x, int16_t y, uint16_t color) {
+        if constexpr (ScaleFactor > 1) 
+		{
+            for (int dx = 0; dx < ScaleFactor; dx++) {
+                for (int dy = 0; dy < ScaleFactor; dy++) {
+                    //irtualCoords v = getCoords(x * ScaleFactor + dx, y * ScaleFactor + dy);
+					// display->drawPixel(v.x, v.y, color);
+					calcCoords(x * ScaleFactor + dx, y * ScaleFactor + dy);
+					display->drawPixel(coords.x, coords.y, color);
+					
+                }
+            }
+        } else {
+            //VirtualCoords v = getCoords(x, y);
+            //display->drawPixel(v.x, v.y, color);
+			
+			calcCoords(x , y);
+			display->drawPixel(coords.x, coords.y, color);			
+        }
+
+        log_v("x: %d, y: %d -> coords.x: %d, coords.y: %d", x, y, coords.x, coords.y);
+    }
+
+    inline void fillScreen(uint16_t color) {
+        display->fillScreen(color);
+    }
+
+    inline void fillScreenRGB888(uint8_t r, uint8_t g, uint8_t b) {
+        display->fillScreenRGB888(r, g, b);
+    }
+
+    inline void drawPixelRGB888(int16_t x, int16_t y, uint8_t r, uint8_t g, uint8_t b) {
+        //VirtualCoords v = getCoords(x, y);
+        //display->drawPixelRGB888(v.x, v.y, r, g, b);
+		
+		calcCoords(x , y);
+		display->drawPixelRGB888(coords.x, coords.y, r, g, b);	
+    }
+
+#ifdef USE_GFX_LITE
+    inline void drawPixel(int16_t x, int16_t y, CRGB color) {
+        //VirtualCoords v = getCoords(x, y);
+        //display->drawPixel(v.x, v.y, color);
+		
+		calcCoords(x , y);
+		display->drawPixel(coords.x, coords.y, color);
+		
+    }
+
+    inline void fillScreen(CRGB color) {
+        display->fillScreen(color);
+    }
+#endif
+
+#ifndef NO_GFX
+    inline void drawDisplayTest() {
+        display->setFont(&FreeSansBold12pt7b);
+        display->setTextColor(display->color565(255, 255, 0));
+        display->setTextSize(1);
+        for (int panel = 0; panel < vmodule_cols * vmodule_rows; panel++) {
+            int top_left_x = (panel == 0) ? 0 : (panel * panel_res_x);
+            display->drawRect(top_left_x, 0, panel_res_x, panel_res_y, display->color565(0, 255, 0));
+            display->setCursor((panel * panel_res_x) + 2, panel_res_y - 4);
+            display->print((vmodule_cols * vmodule_rows) - panel);
+        }
+    }
+#endif
+
+    inline void clearScreen() { display->clearScreen(); }
+
+    inline uint16_t color444(uint8_t r, uint8_t g, uint8_t b) { return display->color444(r, g, b); }
+    inline uint16_t color565(uint8_t r, uint8_t g, uint8_t b) { return display->color565(r, g, b); }
+
+    inline void flipDMABuffer() { display->flipDMABuffer(); }
+
+    // ------------------------------------------------------------------
+    // Rotation (runtime)
+    inline void setRotation(uint8_t rotate) {
+        if (rotate < 4)
+            _rotate = rotate;
+#ifdef NO_GFX
+        // When NO_GFX is defined, update _virtual_res_x/_virtual_res_y as needed.
+#else
+        uint8_t rotation = (rotate & 3);
+        switch (rotation) {
+            case 0:
+            case 2:
+                _virtual_res_x = virtual_res_x;
+                _virtual_res_y = virtual_res_y;
+                _width = virtual_res_x;
+                _height = virtual_res_y;
+                break;
+            case 1:
+            case 3:
+                _virtual_res_x = virtual_res_y;
+                _virtual_res_y = virtual_res_x;
+                _width = virtual_res_y;
+                _height = virtual_res_x;
+                break;
+        }
+#endif
+    }
+
+    // ------------------------------------------------------------------
+    // Panel scan–type configuration (runtime adjustment of pixel base)
+    inline void setPixelBase(uint8_t pixel_base) {
+        panel_pixel_base = pixel_base;
+    }
+
+    // ------------------------------------------------------------------
+    // calcCoords() maps a virtual (x,y) coordinate to a physical coordinate.
+    //VirtualCoords getCoords(int16_t virt_x, int16_t virt_y) {
+	void calcCoords(int16_t virt_x, int16_t virt_y) {
+		
+#ifdef NO_GFX
+        if (virt_x < 0 || virt_x >= _virtual_res_x || virt_y < 0 || virt_y >= _virtual_res_y) {
+#else
+        if (virt_x < 0 || virt_x >= _width || virt_y < 0 || virt_y >= _height) {
+#endif
+            coords.x = coords.y = -1;
+			return;
+            //return coords;
+        }
+
+        //log_d("calcCoords pre-chain: virt_x: %d, virt_y: %d", virt_x, virt_y);
+
+        // --- Runtime rotation ---
+        switch (_rotate) {
+            case 1: {
+                int16_t temp = virt_x;
+                virt_x = virt_y;
+                virt_y = virtual_res_y - 1 - temp;
+                break;
+            }
+            case 2: {
+                virt_x = virtual_res_x - 1 - virt_x;
+                virt_y = virtual_res_y - 1 - virt_y;
+                break;
+            }
+            case 3: {
+                int16_t temp = virt_x;
+                virt_x = virtual_res_x - 1 - virt_y;
+                virt_y = temp;
+                break;
+            }
+            default:
+                break;
+        }
+
+        // --- Chain mapping ---
+        int row = virt_y / panel_res_y; // 0-indexed row in the virtual module
+        if constexpr (ChainScanType == CHAIN_TOP_RIGHT_DOWN) {
+            if ((row & 1) == 1) {
+                coords.x = dma_res_x - virt_x - (row * virtual_res_x);
+                coords.y = panel_res_y - 1 - (virt_y % panel_res_y);
+            } else {
+                coords.x = ((vmodule_rows - (row + 1)) * virtual_res_x) + virt_x;
+                coords.y = (virt_y % panel_res_y);
+            }
+        }
+        else if constexpr (ChainScanType == CHAIN_TOP_RIGHT_DOWN_ZZ) {
+            coords.x = ((vmodule_rows - (row + 1)) * virtual_res_x) + virt_x;
+            coords.y = (virt_y % panel_res_y);
+        }
+        else if constexpr (ChainScanType == CHAIN_TOP_LEFT_DOWN) {
+            if ((row & 1) == 0) {
+                coords.x = dma_res_x - virt_x - (row * virtual_res_x);
+                coords.y = panel_res_y - 1 - (virt_y % panel_res_y);
+            } else {
+                coords.x = ((vmodule_rows - (row + 1)) * virtual_res_x) + virt_x;
+                coords.y = (virt_y % panel_res_y);
+            }
+        }
+        else if constexpr (ChainScanType == CHAIN_TOP_LEFT_DOWN_ZZ) {
+            coords.x = ((vmodule_rows - (row + 1)) * virtual_res_x) + virt_x;
+            coords.y = (virt_y % panel_res_y);
+        }
+        else if constexpr (ChainScanType == CHAIN_BOTTOM_LEFT_UP) {
+            row = vmodule_rows - row - 1;
+            if ((row & 1) == 1) {
+                coords.x = ((vmodule_rows - (row + 1)) * virtual_res_x) + virt_x;
+                coords.y = (virt_y % panel_res_y);
+            } else {
+                coords.x = dma_res_x - (row * virtual_res_x) - virt_x;
+                coords.y = panel_res_y - 1 - (virt_y % panel_res_y);
+            }
+        }
+        else if constexpr (ChainScanType == CHAIN_BOTTOM_LEFT_UP_ZZ) {
+            row = vmodule_rows - row - 1;
+            coords.x = ((vmodule_rows - (row + 1)) * virtual_res_x) + virt_x;
+            coords.y = (virt_y % panel_res_y);
+        }
+        else if constexpr (ChainScanType == CHAIN_BOTTOM_RIGHT_UP) {
+            row = vmodule_rows - row - 1;
+            if ((row & 1) == 0) {
+                coords.x = ((vmodule_rows - (row + 1)) * virtual_res_x) + virt_x;
+                coords.y = (virt_y % panel_res_y);
+            } else {
+                coords.x = dma_res_x - (row * virtual_res_x) - virt_x;
+                coords.y = panel_res_y - 1 - (virt_y % panel_res_y);
+            }
+        }
+        else if constexpr (ChainScanType == CHAIN_BOTTOM_RIGHT_UP_ZZ) {
+            row = vmodule_rows - row - 1;
+            coords.x = ((vmodule_rows - (row + 1)) * virtual_res_x) + virt_x;
+            coords.y = (virt_y % panel_res_y);
+        }
+        else { // CHAIN_NONE (default)
+            coords.x = virt_x;
+            coords.y = virt_y;
+        }
+
+        //log_d("calcCoords post-chain: virt_x: %d, virt_y: %d", virt_x, virt_y);    
+
+        // --- Apply scan–type mapping ---
+        coords = ScanTypeMapping::apply(coords, virt_y, panel_pixel_base);
+
+        //return coords;
+    }
+
+#ifdef NO_GFX
+    inline uint16_t width()  const { return _virtual_res_x; }
+    inline uint16_t height() const { return _virtual_res_y; }
+#endif
+
+    // ------------------------------------------------------------------
+    // Data members (public for compatibility)
+    VirtualCoords coords;
+	
+    uint8_t panel_res_x;       // physical panel resolution X
+    uint8_t panel_res_y;       // physical panel resolution Y
+    uint8_t panel_pixel_base; // used for scan–type mapping
+
+    inline void setDisplay(MatrixPanel_I2S_DMA &disp) {
+        display = &disp;
+    }
+
+private:
+    MatrixPanel_I2S_DMA *display;
+    // Note: panel_chain_type is now fixed via the compile–time template parameter 'ChainScanType'.
+    uint16_t virtual_res_x;    // virtual display width (combination of panels)
+    uint16_t virtual_res_y;    // virtual display height (combination of panels)
+    uint16_t _virtual_res_x;   // width adjusted by current rotation
+    uint16_t _virtual_res_y;   // height adjusted by current rotation
+    uint8_t  vmodule_rows;    // virtual module rows
+    uint8_t  vmodule_cols;    // virtual module columns
+    uint16_t dma_res_x;        // width as seen by the DMA engine
+
+    int _rotate;             // runtime rotation (0 to 3)
+};
+
+#endif  // VIRTUAL_MATRIX_PANEL_TEMPLATE_H
diff --git a/src/platforms/esp32s3/gdma_lcd_parallel16.cpp b/src/platforms/esp32s3/gdma_lcd_parallel16.cpp
index 9d15565..944db95 100644
--- a/src/platforms/esp32s3/gdma_lcd_parallel16.cpp
+++ b/src/platforms/esp32s3/gdma_lcd_parallel16.cpp
@@ -257,7 +257,7 @@
     };
     gdma_apply_strategy(dma_chan, &strategy_config);
 
-#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0)
+#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
     gdma_transfer_config_t transfer_config = {
 #ifdef SPIRAM_DMA_BUFFER
       .max_data_burst_size = 64,