Repository: adafruit/circuitpython
Pull request #10113: tile palette mapper module and class
Author: login: FoamyGuy
Created: 2025-03-05T15:19:06Z | Merged: 2025-03-11T18:39:32Z

All testing was performed on a pyportal titano with this script:
```
import time

from displayio import Palette
from tilepalettemapper import TilePaletteMapper
import board
import terminalio
import displayio

time.sleep(1)
p = Palette(2)
p[0] = 0x000000
p[1] = 0xffffff

bbox = terminalio.FONT.get_bounding_box()
main_group = displayio.Group()
board.DISPLAY.root_group = main_group

tpm = TilePaletteMapper(p, 10,4)

tg = displayio.TileGrid(terminalio.FONT.bitmap, pixel_shader=tpm,
                        tile_width=bbox[0], tile_height=bbox[1],
                        width=10, height=4, default_tile=0)
#tg.pixel_shader = tpm
main_group.append(tg)

for i in range(40):
    tg[i] = i

# print(tpm.width)
# print(tpm.palette[1])
tpm[1,0] = [1,0]
# print(tpm[1,0])

while True:
    for y in range(tg.height):
        for x in range(tg.width):
            cur = (x, y)
            tpm[cur] = [1,0] if tpm[cur] == [0,1] else [0,1]
            time.sleep(0.025)
```


Diff:
diff --git a/ports/atmel-samd/boards/circuitbrains_deluxe_m4/mpconfigboard.mk b/ports/atmel-samd/boards/circuitbrains_deluxe_m4/mpconfigboard.mk
index 46aa0b7dc659f..b3cb3d73015db 100755
--- a/ports/atmel-samd/boards/circuitbrains_deluxe_m4/mpconfigboard.mk
+++ b/ports/atmel-samd/boards/circuitbrains_deluxe_m4/mpconfigboard.mk
@@ -14,3 +14,4 @@ CIRCUITPY_PS2IO = 1
 CIRCUITPY_JPEGIO = 0
 CIRCUITPY_SPITARGET = 0
 CIRCUITPY_SYNTHIO = 0
+CIRCUITPY_TILEPALETTEMAPPER = 0
diff --git a/ports/atmel-samd/boards/datalore_ip_m4/mpconfigboard.mk b/ports/atmel-samd/boards/datalore_ip_m4/mpconfigboard.mk
index 8f06d8d1e5168..f6c07e7aaafb4 100644
--- a/ports/atmel-samd/boards/datalore_ip_m4/mpconfigboard.mk
+++ b/ports/atmel-samd/boards/datalore_ip_m4/mpconfigboard.mk
@@ -12,3 +12,4 @@ LONGINT_IMPL = MPZ
 CIRCUITPY_SYNTHIO = 0
 CIRCUITPY_JPEGIO = 0
 CIRCUITPY_SPITARGET = 0
+CIRCUITPY_TILEPALETTEMAPPER = 0
diff --git a/ports/atmel-samd/boards/hallowing_m4_express/mpconfigboard.mk b/ports/atmel-samd/boards/hallowing_m4_express/mpconfigboard.mk
index 74f1d3e55993e..3a0eeb3f2130e 100644
--- a/ports/atmel-samd/boards/hallowing_m4_express/mpconfigboard.mk
+++ b/ports/atmel-samd/boards/hallowing_m4_express/mpconfigboard.mk
@@ -13,3 +13,4 @@ LONGINT_IMPL = MPZ
 CIRCUITPY_AESIO = 0
 CIRCUITPY_JPEGIO = 0
 CIRCUITPY_SYNTHIO = 0
+CIRCUITPY_TILEPALETTEMAPPER = 0
diff --git a/ports/atmel-samd/boards/itsybitsy_m4_express/mpconfigboard.mk b/ports/atmel-samd/boards/itsybitsy_m4_express/mpconfigboard.mk
index de70d4fe0e552..27bf8c165da88 100644
--- a/ports/atmel-samd/boards/itsybitsy_m4_express/mpconfigboard.mk
+++ b/ports/atmel-samd/boards/itsybitsy_m4_express/mpconfigboard.mk
@@ -14,6 +14,7 @@ CIRCUITPY_CODEOP = 0
 CIRCUITPY_FLOPPYIO = 0
 CIRCUITPY_GIFIO = 0
 CIRCUITPY_JPEGIO = 0
+CIRCUITPY_TILEPALETTEMAPPER = 0
 
 CIRCUITPY_BITBANG_APA102 = 1
 
diff --git a/ports/atmel-samd/boards/openbook_m4/mpconfigboard.mk b/ports/atmel-samd/boards/openbook_m4/mpconfigboard.mk
index a0f5ada80be5a..20e9cda0eb480 100644
--- a/ports/atmel-samd/boards/openbook_m4/mpconfigboard.mk
+++ b/ports/atmel-samd/boards/openbook_m4/mpconfigboard.mk
@@ -15,3 +15,4 @@ CIRCUITPY_SYNTHIO = 0
 CIRCUITPY_JPEGIO = 0
 CIRCUITPY_FLOPPYIO = 0
 CIRCUITPY_TERMINALIO_VT100 = 0
+CIRCUITPY_TILEPALETTEMAPPER = 0
diff --git a/ports/atmel-samd/boards/pewpew_lcd/mpconfigboard.mk b/ports/atmel-samd/boards/pewpew_lcd/mpconfigboard.mk
index 823f5698156cd..c734323a6ab62 100644
--- a/ports/atmel-samd/boards/pewpew_lcd/mpconfigboard.mk
+++ b/ports/atmel-samd/boards/pewpew_lcd/mpconfigboard.mk
@@ -54,6 +54,7 @@ CIRCUITPY_PULSEIO = 0
 CIRCUITPY_RGBMATRIX = 0
 CIRCUITPY_ROTARYIO = 0
 CIRCUITPY_RTC = 0
+CIRCUITPY_TILEPALETTEMAPPER = 0
 CIRCUITPY_ULAB = 0
 CIRCUITPY_USB_HID = 0
 CIRCUITPY_USB_MIDI = 0
diff --git a/ports/atmel-samd/boards/silicognition-m4-shim/mpconfigboard.mk b/ports/atmel-samd/boards/silicognition-m4-shim/mpconfigboard.mk
index 16e439d3ef91f..214099ccc1651 100644
--- a/ports/atmel-samd/boards/silicognition-m4-shim/mpconfigboard.mk
+++ b/ports/atmel-samd/boards/silicognition-m4-shim/mpconfigboard.mk
@@ -13,3 +13,4 @@ LONGINT_IMPL = MPZ
 CIRCUITPY_JPEGIO = 0
 CIRCUITPY_SPITARGET = 0
 CIRCUITPY_SYNTHIO = 0
+CIRCUITPY_TILEPALETTEMAPPER = 0
diff --git a/ports/atmel-samd/boards/uartlogger2/mpconfigboard.mk b/ports/atmel-samd/boards/uartlogger2/mpconfigboard.mk
index c3626ab98f3bb..82d7f0f43ba59 100644
--- a/ports/atmel-samd/boards/uartlogger2/mpconfigboard.mk
+++ b/ports/atmel-samd/boards/uartlogger2/mpconfigboard.mk
@@ -13,3 +13,4 @@ CIRCUITPY_SPITARGET = 0
 CIRCUITPY_SYNTHIO = 0
 CIRCUITPY_JPEGIO = 0
 CIRCUITPY_TERMINALIO_VT100 = 0
+CIRCUITPY_TILEPALETTEMAPPER = 0
diff --git a/ports/atmel-samd/mpconfigport.mk b/ports/atmel-samd/mpconfigport.mk
index c2961e4165f23..15013b2d831ca 100644
--- a/ports/atmel-samd/mpconfigport.mk
+++ b/ports/atmel-samd/mpconfigport.mk
@@ -60,6 +60,7 @@ CIRCUITPY_ZLIB = 0
 # Turn off a few more things that don't fit in 192kB
 
 CIRCUITPY_TERMINALIO_VT100 = 0
+CIRCUITPY_TILEPALETTEMAPPER = 0
 
 ifeq ($(INTERNAL_FLASH_FILESYSTEM),1)
 CIRCUITPY_ONEWIREIO ?= 0
diff --git a/ports/stm/boards/meowbit_v121/mpconfigboard.mk b/ports/stm/boards/meowbit_v121/mpconfigboard.mk
index 52c191b5b2efb..bde49ad13ef49 100644
--- a/ports/stm/boards/meowbit_v121/mpconfigboard.mk
+++ b/ports/stm/boards/meowbit_v121/mpconfigboard.mk
@@ -29,6 +29,7 @@ CIRCUITPY_BLEIO_HCI = 0
 CIRCUITPY_EPAPERDISPLAY = 0
 CIRCUITPY_KEYPAD_DEMUX = 0
 CIRCUITPY_SHARPDISPLAY = 0
+CIRCUITPY_TILEPALETTEMAPPER = 0
 CIRCUITPY_ULAB = 0
 CIRCUITPY_ZLIB = 0
 
diff --git a/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml b/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml
index a521b62fb4c30..ba0291f64300a 100644
--- a/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml
@@ -90,6 +90,7 @@ struct = true
 supervisor = false
 synthio = false
 terminalio = false
+tilepalettemapper = false
 time = true
 touchio = false
 traceback = false
diff --git a/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml b/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml
index d8266a9b8515f..88178af3b28c2 100644
--- a/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml
@@ -90,6 +90,7 @@ struct = true
 supervisor = false
 synthio = false
 terminalio = false
+tilepalettemapper = false
 time = true
 touchio = false
 traceback = false
diff --git a/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml b/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml
index baacdff5dd1e7..85b5a9c7e0fad 100644
--- a/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml
@@ -90,6 +90,7 @@ struct = true
 supervisor = false
 synthio = false
 terminalio = false
+tilepalettemapper = false
 time = true
 touchio = false
 traceback = false
diff --git a/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml b/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml
index 31e979864ac1d..674a19ab107c9 100644
--- a/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml
@@ -90,6 +90,7 @@ struct = true
 supervisor = false
 synthio = false
 terminalio = false
+tilepalettemapper = false
 time = true
 touchio = false
 traceback = false
diff --git a/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml b/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml
index 0f366c079bb6b..4f920c59f11d9 100644
--- a/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml
@@ -90,6 +90,7 @@ struct = true
 supervisor = false
 synthio = false
 terminalio = false
+tilepalettemapper = false
 time = true
 touchio = false
 traceback = false
diff --git a/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml b/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml
index cb20c3e59300e..000155eafa744 100644
--- a/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml
@@ -90,6 +90,7 @@ struct = true
 supervisor = false
 synthio = false
 terminalio = false
+tilepalettemapper = false
 time = true
 touchio = false
 traceback = false
diff --git a/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml b/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml
index c755838d554a2..ac0aff61a59c3 100644
--- a/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml
@@ -90,6 +90,7 @@ struct = true
 supervisor = false
 synthio = false
 terminalio = false
+tilepalettemapper = false
 time = true
 touchio = false
 traceback = false
diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk
index 166256817ad04..513f7d2d64d97 100644
--- a/py/circuitpy_defns.mk
+++ b/py/circuitpy_defns.mk
@@ -396,6 +396,9 @@ endif
 ifeq ($(CIRCUITPY_FONTIO),1)
 SRC_PATTERNS += fontio/%
 endif
+ifeq ($(CIRCUITPY_TILEPALETTEMAPPER),1)
+SRC_PATTERNS += tilepalettemapper/%
+endif
 ifeq ($(CIRCUITPY_TIME),1)
 SRC_PATTERNS += time/%
 endif
@@ -733,6 +736,8 @@ SRC_SHARED_MODULE_ALL = \
 	synthio/__init__.c \
 	terminalio/Terminal.c \
 	terminalio/__init__.c \
+	tilepalettemapper/__init__.c \
+	tilepalettemapper/TilePaletteMapper.c \
 	time/__init__.c \
 	traceback/__init__.c \
 	uheap/__init__.c \
diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk
index 69ebd03e8b295..831c1ec245fc3 100644
--- a/py/circuitpy_mpconfig.mk
+++ b/py/circuitpy_mpconfig.mk
@@ -558,6 +558,9 @@ CFLAGS += -DCIRCUITPY_TERMINALIO_VT100=$(CIRCUITPY_TERMINALIO_VT100)
 CIRCUITPY_FONTIO ?= $(call enable-if-all,$(CIRCUITPY_DISPLAYIO) $(CIRCUITPY_TERMINALIO))
 CFLAGS += -DCIRCUITPY_FONTIO=$(CIRCUITPY_FONTIO)
 
+CIRCUITPY_TILEPALETTEMAPPER ?= $(CIRCUITPY_DISPLAYIO)
+CFLAGS += -DCIRCUITPY_TILEPALETTEMAPPER=$(CIRCUITPY_TILEPALETTEMAPPER)
+
 CIRCUITPY_TIME ?= 1
 CFLAGS += -DCIRCUITPY_TIME=$(CIRCUITPY_TIME)
 
diff --git a/shared-bindings/displayio/TileGrid.c b/shared-bindings/displayio/TileGrid.c
index 99f1699fbe89c..a8757fbcc17f2 100644
--- a/shared-bindings/displayio/TileGrid.c
+++ b/shared-bindings/displayio/TileGrid.c
@@ -17,6 +17,25 @@
 #include "shared-bindings/displayio/ColorConverter.h"
 #include "shared-bindings/displayio/OnDiskBitmap.h"
 #include "shared-bindings/displayio/Palette.h"
+#ifdef CIRCUITPY_TILEPALETTEMAPPER
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+#endif
+
+
+void displayio_tilegrid_validate_pixel_shader(mp_obj_t pixel_shader) {
+    bool valid_type = true;
+    if (!mp_obj_is_type(pixel_shader, &displayio_palette_type) && !mp_obj_is_type(pixel_shader, &displayio_colorconverter_type)) {
+        valid_type = false;
+    }
+    #if CIRCUITPY_TILEPALETTEMAPPER
+    if (mp_obj_is_type(pixel_shader, &tilepalettemapper_tilepalettemapper_type)) {
+        valid_type = true;
+    }
+    #endif
+    if (!valid_type) {
+        mp_raise_TypeError_varg(MP_ERROR_TEXT("unsupported %q type"), MP_QSTR_pixel_shader);
+    }
+}
 
 //| class TileGrid:
 //|     """A grid of tiles sourced out of one bitmap
@@ -89,10 +108,7 @@ static mp_obj_t displayio_tilegrid_make_new(const mp_obj_type_t *type, size_t n_
         mp_raise_TypeError_varg(MP_ERROR_TEXT("unsupported %q type"), MP_QSTR_bitmap);
     }
     mp_obj_t pixel_shader = args[ARG_pixel_shader].u_obj;
-    if (!mp_obj_is_type(pixel_shader, &displayio_colorconverter_type) &&
-        !mp_obj_is_type(pixel_shader, &displayio_palette_type)) {
-        mp_raise_TypeError_varg(MP_ERROR_TEXT("unsupported %q type"), MP_QSTR_pixel_shader);
-    }
+    displayio_tilegrid_validate_pixel_shader(pixel_shader);
     uint16_t tile_width = args[ARG_tile_width].u_int;
     if (tile_width == 0) {
         tile_width = bitmap_width;
@@ -323,10 +339,7 @@ MP_DEFINE_CONST_FUN_OBJ_1(displayio_tilegrid_get_pixel_shader_obj, displayio_til
 
 static mp_obj_t displayio_tilegrid_obj_set_pixel_shader(mp_obj_t self_in, mp_obj_t pixel_shader) {
     displayio_tilegrid_t *self = native_tilegrid(self_in);
-    if (!mp_obj_is_type(pixel_shader, &displayio_palette_type) && !mp_obj_is_type(pixel_shader, &displayio_colorconverter_type)) {
-        mp_raise_TypeError_varg(MP_ERROR_TEXT("unsupported %q type"), MP_QSTR_pixel_shader);
-    }
-
+    displayio_tilegrid_validate_pixel_shader(pixel_shader);
     common_hal_displayio_tilegrid_set_pixel_shader(self, pixel_shader);
 
     return mp_const_none;
diff --git a/shared-bindings/tilepalettemapper/TilePaletteMapper.c b/shared-bindings/tilepalettemapper/TilePaletteMapper.c
new file mode 100644
index 0000000000000..ee1475d16b50f
--- /dev/null
+++ b/shared-bindings/tilepalettemapper/TilePaletteMapper.c
@@ -0,0 +1,167 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+#include <stdint.h>
+#include <stdlib.h>
+#include "shared/runtime/context_manager_helpers.h"
+#include "py/binary.h"
+#include "py/objproperty.h"
+#include "py/runtime.h"
+#include "shared-bindings/util.h"
+#include "shared-bindings/displayio/Palette.h"
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+
+//| class TilePaletteMapper:
+//|     """Remaps color indices from the source bitmap to alternate indices on a
+//|     per-tile basis. This allows for altering coloring of tiles based on
+//|     their tilegrid location. It also allows for using a limited color
+//|     bitmap with a wider array of colors."""
+//|
+//|     def __init__(
+//|         self, palette: displayio.Palette, input_color_count: int, width: int, height: int
+//|     ) -> None:
+//|         """Create a TilePaletteMApper object to store a set of color mappings for tiles.
+//|
+//|         :param displayio.Palette palette: The palette to get mapped colors from.
+//|         :param int input_color_count: The number of colors in in the input bitmap.
+//|         :param int width: The width of the grid in tiles.
+//|         :param int height: The height of the grid in tiles."""
+//|
+
+static mp_obj_t tilepalettemapper_tilepalettemapper_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
+    enum { ARG_palette, ARG_input_color_count, ARG_width, ARG_height };
+    static const mp_arg_t allowed_args[] = {
+        { MP_QSTR_palette, MP_ARG_OBJ | MP_ARG_REQUIRED },
+        { MP_QSTR_input_color_count, MP_ARG_INT | MP_ARG_REQUIRED },
+        { MP_QSTR_width, MP_ARG_INT | MP_ARG_REQUIRED },
+        { MP_QSTR_height, MP_ARG_INT | MP_ARG_REQUIRED },
+    };
+    mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
+    mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
+    mp_obj_t palette = args[ARG_palette].u_obj;
+    if (!mp_obj_is_type(palette, &displayio_palette_type)) {
+        mp_raise_TypeError_varg(MP_ERROR_TEXT("unsupported %q type"), MP_QSTR_pixel_shader);
+    }
+    tilepalettemapper_tilepalettemapper_t *self = mp_obj_malloc(tilepalettemapper_tilepalettemapper_t, &tilepalettemapper_tilepalettemapper_type);
+    common_hal_tilepalettemapper_tilepalettemapper_construct(self, palette, args[ARG_input_color_count].u_int, args[ARG_width].u_int, args[ARG_height].u_int);
+
+    return MP_OBJ_FROM_PTR(self);
+}
+
+//|     width: int
+//|     """Width of the tile palette mapper in tiles."""
+static mp_obj_t tilepalettemapper_tilepalettemapper_obj_get_width(mp_obj_t self_in) {
+    tilepalettemapper_tilepalettemapper_t *self = MP_OBJ_TO_PTR(self_in);
+    return MP_OBJ_NEW_SMALL_INT(common_hal_tilepalettemapper_tilepalettemapper_get_width(self));
+}
+MP_DEFINE_CONST_FUN_OBJ_1(tilepalettemapper_tilepalettemapper_get_width_obj, tilepalettemapper_tilepalettemapper_obj_get_width);
+
+MP_PROPERTY_GETTER(tilepalettemapper_tilepalettemapper_width_obj,
+    (mp_obj_t)&tilepalettemapper_tilepalettemapper_get_width_obj);
+
+//|     height: int
+//|     """Height of the tile palette mapper in tiles."""
+static mp_obj_t tilepalettemapper_tilepalettemapper_obj_get_height(mp_obj_t self_in) {
+    tilepalettemapper_tilepalettemapper_t *self = MP_OBJ_TO_PTR(self_in);
+    return MP_OBJ_NEW_SMALL_INT(common_hal_tilepalettemapper_tilepalettemapper_get_height(self));
+}
+MP_DEFINE_CONST_FUN_OBJ_1(tilepalettemapper_tilepalettemapper_get_height_obj, tilepalettemapper_tilepalettemapper_obj_get_height);
+
+MP_PROPERTY_GETTER(tilepalettemapper_tilepalettemapper_height_obj,
+    (mp_obj_t)&tilepalettemapper_tilepalettemapper_get_height_obj);
+
+
+//|     palette: displayio.Palette
+//|     """The palette that the mapper uses."""
+//|
+static mp_obj_t tilepalettemapper_tilepalettemapper_obj_get_palette(mp_obj_t self_in) {
+    tilepalettemapper_tilepalettemapper_t *self = MP_OBJ_TO_PTR(self_in);
+    return common_hal_tilepalettemapper_tilepalettemapper_get_palette(self);
+}
+MP_DEFINE_CONST_FUN_OBJ_1(tilepalettemapper_tilepalettemapper_get_palette_obj, tilepalettemapper_tilepalettemapper_obj_get_palette);
+
+MP_PROPERTY_GETTER(tilepalettemapper_tilepalettemapper_palette_obj,
+    (mp_obj_t)&tilepalettemapper_tilepalettemapper_get_palette_obj);
+
+
+//|     def __getitem__(self, index: Union[Tuple[int, int], int]) -> Tuple[int]:
+//|         """Returns the mapping for the given index. The index can either be an x,y tuple or an int equal
+//|         to ``y * width + x``.
+//|
+//|         This allows you to::
+//|
+//|           print(tpm[0])"""
+//|         ...
+//|
+//|     def __setitem__(self, index: Union[Tuple[int, int], int], value: List[int]) -> None:
+//|         """Sets the mapping at the given tile index. The index can either be an x,y tuple or an int equal
+//|         to ``y * width + x``.
+//|
+//|         This allows you to::
+//|
+//|           tpm[0] = [1,0]
+//|
+//|         or::
+//|
+//|           tpm[0,0] = [1,0]"""
+//|         ...
+//|
+//|
+static mp_obj_t tilepalettemapper_subscr(mp_obj_t self_in, mp_obj_t index_obj, mp_obj_t value_obj) {
+    tilepalettemapper_tilepalettemapper_t *self = MP_OBJ_TO_PTR(self_in);
+    if (mp_obj_is_type(index_obj, &mp_type_slice)) {
+        mp_raise_NotImplementedError(MP_ERROR_TEXT("Slices not supported"));
+    } else {
+        uint16_t x = 0;
+        uint16_t y = 0;
+        if (mp_obj_is_small_int(index_obj)) {
+            mp_int_t i = MP_OBJ_SMALL_INT_VALUE(index_obj);
+            uint16_t width = common_hal_tilepalettemapper_tilepalettemapper_get_width(self);
+            x = i % width;
+            y = i / width;
+        } else {
+            mp_obj_t *items;
+            mp_obj_get_array_fixed_n(index_obj, 2, &items);
+            x = mp_obj_get_int(items[0]);
+            y = mp_obj_get_int(items[1]);
+        }
+        if (x >= common_hal_tilepalettemapper_tilepalettemapper_get_width(self) ||
+            y >= common_hal_tilepalettemapper_tilepalettemapper_get_height(self)) {
+            mp_raise_IndexError(MP_ERROR_TEXT("Tile index out of bounds"));
+        }
+
+        if (value_obj == MP_OBJ_SENTINEL) {
+            // load
+            return common_hal_tilepalettemapper_tilepalettemapper_get_mapping(self, x, y);
+        } else if (value_obj == mp_const_none) {
+            return MP_OBJ_NULL; // op not supported
+        } else {
+            size_t len = 0;
+            mp_obj_t *items;
+            mp_obj_list_get(value_obj, &len, &items);
+            mp_arg_validate_int_range(len, 0, self->input_color_count, MP_QSTR_mapping_length);
+            common_hal_tilepalettemapper_tilepalettemapper_set_mapping(self, x, y, len, items);
+        }
+    }
+    return mp_const_none;
+}
+
+
+static const mp_rom_map_elem_t tilepalettemapper_tilepalettemapper_locals_dict_table[] = {
+    { MP_ROM_QSTR(MP_QSTR_width), MP_ROM_PTR(&tilepalettemapper_tilepalettemapper_width_obj) },
+    { MP_ROM_QSTR(MP_QSTR_height), MP_ROM_PTR(&tilepalettemapper_tilepalettemapper_height_obj) },
+    { MP_ROM_QSTR(MP_QSTR_palette), MP_ROM_PTR(&tilepalettemapper_tilepalettemapper_palette_obj) },
+};
+static MP_DEFINE_CONST_DICT(tilepalettemapper_tilepalettemapper_locals_dict, tilepalettemapper_tilepalettemapper_locals_dict_table);
+
+MP_DEFINE_CONST_OBJ_TYPE(
+    tilepalettemapper_tilepalettemapper_type,
+    MP_QSTR_TilePaletteMapper,
+    MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS,
+    make_new, tilepalettemapper_tilepalettemapper_make_new,
+    locals_dict, &tilepalettemapper_tilepalettemapper_locals_dict,
+    subscr, tilepalettemapper_subscr,
+    iter, mp_obj_generic_subscript_getiter
+    );
diff --git a/shared-bindings/tilepalettemapper/TilePaletteMapper.h b/shared-bindings/tilepalettemapper/TilePaletteMapper.h
new file mode 100644
index 0000000000000..0e4eced23b7c3
--- /dev/null
+++ b/shared-bindings/tilepalettemapper/TilePaletteMapper.h
@@ -0,0 +1,18 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+#include "shared-module/tilepalettemapper/TilePaletteMapper.h"
+
+extern const mp_obj_type_t tilepalettemapper_tilepalettemapper_type;
+
+void common_hal_tilepalettemapper_tilepalettemapper_construct(tilepalettemapper_tilepalettemapper_t *self,
+    mp_obj_t paltte, uint16_t input_color_count, uint16_t bitmap_width_in_tiles, uint16_t bitmap_height_in_tiles);
+
+
+uint16_t common_hal_tilepalettemapper_tilepalettemapper_get_width(tilepalettemapper_tilepalettemapper_t *self);
+uint16_t common_hal_tilepalettemapper_tilepalettemapper_get_height(tilepalettemapper_tilepalettemapper_t *self);
+mp_obj_t common_hal_tilepalettemapper_tilepalettemapper_get_palette(tilepalettemapper_tilepalettemapper_t *self);
+mp_obj_t common_hal_tilepalettemapper_tilepalettemapper_get_mapping(tilepalettemapper_tilepalettemapper_t *self, uint16_t x, uint16_t y);
+void common_hal_tilepalettemapper_tilepalettemapper_set_mapping(tilepalettemapper_tilepalettemapper_t *self, uint16_t x, uint16_t y, size_t len, mp_obj_t *items);
diff --git a/shared-bindings/tilepalettemapper/__init__.c b/shared-bindings/tilepalettemapper/__init__.c
new file mode 100644
index 0000000000000..966de95076659
--- /dev/null
+++ b/shared-bindings/tilepalettemapper/__init__.c
@@ -0,0 +1,21 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks
+//
+// SPDX-License-Identifier: MIT
+#include "py/obj.h"
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+
+
+static const mp_rom_map_elem_t tilepalettemapper_module_globals_table[] = {
+    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_tilepalettemapper) },
+    { MP_ROM_QSTR(MP_QSTR_TilePaletteMapper), MP_ROM_PTR(&tilepalettemapper_tilepalettemapper_type) },
+};
+static MP_DEFINE_CONST_DICT(tilepalettemapper_module_globals, tilepalettemapper_module_globals_table);
+
+const mp_obj_module_t tilepalettemapper_module = {
+    .base = {&mp_type_module },
+    .globals = (mp_obj_dict_t *)&tilepalettemapper_module_globals,
+};
+
+MP_REGISTER_MODULE(MP_QSTR_tilepalettemapper, tilepalettemapper_module);
diff --git a/shared-module/displayio/TileGrid.c b/shared-module/displayio/TileGrid.c
index 926637c11fbdd..e0d18d0accfb8 100644
--- a/shared-module/displayio/TileGrid.c
+++ b/shared-module/displayio/TileGrid.c
@@ -11,6 +11,9 @@
 #include "shared-bindings/displayio/ColorConverter.h"
 #include "shared-bindings/displayio/OnDiskBitmap.h"
 #include "shared-bindings/displayio/Palette.h"
+#if CIRCUITPY_TILEPALETTEMAPPER
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+#endif
 
 void common_hal_displayio_tilegrid_construct(displayio_tilegrid_t *self, mp_obj_t bitmap,
     uint16_t bitmap_width_in_tiles, uint16_t bitmap_height_in_tiles,
@@ -468,7 +471,9 @@ bool displayio_tilegrid_fill_area(displayio_tilegrid_t *self,
                 continue;
             }
             int16_t local_x = input_pixel.x / self->absolute_transform->scale;
-            uint16_t tile_location = ((local_y / self->tile_height + self->top_left_y) % self->height_in_tiles) * self->width_in_tiles + (local_x / self->tile_width + self->top_left_x) % self->width_in_tiles;
+            uint16_t x_tile_index = (local_x / self->tile_width + self->top_left_x) % self->width_in_tiles;
+            uint16_t y_tile_index = (local_y / self->tile_height + self->top_left_y) % self->height_in_tiles;
+            uint16_t tile_location = y_tile_index * self->width_in_tiles + x_tile_index;
             input_pixel.tile = tiles[tile_location];
             input_pixel.tile_x = (input_pixel.tile % self->bitmap_width_in_tiles) * self->tile_width + local_x % self->tile_width;
             input_pixel.tile_y = (input_pixel.tile / self->bitmap_width_in_tiles) * self->tile_height + local_y % self->tile_height;
@@ -485,6 +490,11 @@ bool displayio_tilegrid_fill_area(displayio_tilegrid_t *self,
             }
 
             output_pixel.opaque = true;
+            #if CIRCUITPY_TILEPALETTEMAPPER
+            if (mp_obj_is_type(self->pixel_shader, &tilepalettemapper_tilepalettemapper_type)) {
+                tilepalettemapper_tilepalettemapper_get_color(self->pixel_shader, colorspace, &input_pixel, &output_pixel, x_tile_index, y_tile_index);
+            }
+            #endif
             if (self->pixel_shader == mp_const_none) {
                 output_pixel.pixel = input_pixel.pixel;
             } else if (mp_obj_is_type(self->pixel_shader, &displayio_palette_type)) {
@@ -550,6 +560,11 @@ void displayio_tilegrid_finish_refresh(displayio_tilegrid_t *self) {
     } else if (mp_obj_is_type(self->pixel_shader, &displayio_colorconverter_type)) {
         displayio_colorconverter_finish_refresh(self->pixel_shader);
     }
+    #if CIRCUITPY_TILEPALETTEMAPPER
+    if (mp_obj_is_type(self->pixel_shader, &tilepalettemapper_tilepalettemapper_type)) {
+        tilepalettemapper_tilepalettemapper_finish_refresh(self->pixel_shader);
+    }
+    #endif
     if (mp_obj_is_type(self->bitmap, &displayio_bitmap_type)) {
         displayio_bitmap_finish_refresh(self->bitmap);
     } else if (mp_obj_is_type(self->bitmap, &displayio_ondiskbitmap_type)) {
@@ -603,6 +618,12 @@ displayio_area_t *displayio_tilegrid_get_refresh_areas(displayio_tilegrid_t *sel
             displayio_palette_needs_refresh(self->pixel_shader)) ||
         (mp_obj_is_type(self->pixel_shader, &displayio_colorconverter_type) &&
             displayio_colorconverter_needs_refresh(self->pixel_shader));
+    #if CIRCUITPY_TILEPALETTEMAPPER
+    self->full_change = self->full_change ||
+        (mp_obj_is_type(self->pixel_shader, &tilepalettemapper_tilepalettemapper_type) &&
+            tilepalettemapper_tilepalettemapper_needs_refresh(self->pixel_shader));
+    #endif
+
     if (self->full_change || first_draw) {
         self->current_area.next = tail;
         return &self->current_area;
diff --git a/shared-module/displayio/TileGrid.h b/shared-module/displayio/TileGrid.h
index c9c31696bf8f2..36d2c9a69b6cb 100644
--- a/shared-module/displayio/TileGrid.h
+++ b/shared-module/displayio/TileGrid.h
@@ -67,3 +67,4 @@ bool displayio_tilegrid_get_previous_area(displayio_tilegrid_t *self, displayio_
 void displayio_tilegrid_finish_refresh(displayio_tilegrid_t *self);
 
 bool displayio_tilegrid_get_rendered_hidden(displayio_tilegrid_t *self);
+void displayio_tilegrid_validate_pixel_shader(mp_obj_t pixel_shader);
diff --git a/shared-module/tilepalettemapper/TilePaletteMapper.c b/shared-module/tilepalettemapper/TilePaletteMapper.c
new file mode 100644
index 0000000000000..c988e908ab00b
--- /dev/null
+++ b/shared-module/tilepalettemapper/TilePaletteMapper.c
@@ -0,0 +1,78 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+#include <stdlib.h>
+#include "py/runtime.h"
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+#include "shared-bindings/displayio/Palette.h"
+
+void common_hal_tilepalettemapper_tilepalettemapper_construct(tilepalettemapper_tilepalettemapper_t *self,
+    mp_obj_t palette, uint16_t input_color_count, uint16_t width, uint16_t height) {
+
+    self->palette = palette;
+    self->width_in_tiles = width;
+    self->height_in_tiles = height;
+    self->input_color_count = input_color_count;
+    self->needs_refresh = false;
+    int mappings_len = width * height;
+    self->tile_mappings = (uint16_t **)m_malloc(mappings_len * sizeof(uint16_t *));
+    for (int i = 0; i < mappings_len; i++) {
+        self->tile_mappings[i] = (uint16_t *)m_malloc(input_color_count * sizeof(uint16_t));
+        for (uint16_t j = 0; j < input_color_count; j++) {
+            self->tile_mappings[i][j] = j;
+        }
+    }
+}
+
+uint16_t common_hal_tilepalettemapper_tilepalettemapper_get_width(tilepalettemapper_tilepalettemapper_t *self) {
+    return self->width_in_tiles;
+}
+
+uint16_t common_hal_tilepalettemapper_tilepalettemapper_get_height(tilepalettemapper_tilepalettemapper_t *self) {
+    return self->height_in_tiles;
+}
+
+mp_obj_t common_hal_tilepalettemapper_tilepalettemapper_get_palette(tilepalettemapper_tilepalettemapper_t *self) {
+    return self->palette;
+}
+
+mp_obj_t common_hal_tilepalettemapper_tilepalettemapper_get_mapping(tilepalettemapper_tilepalettemapper_t *self, uint16_t x, uint16_t y) {
+    int index = x + y * self->width_in_tiles;
+    mp_obj_t result[self->input_color_count];
+    for (uint32_t i = 0; i < self->input_color_count; i++) {
+        result[i] = mp_obj_new_int(self->tile_mappings[index][i]);
+    }
+    return mp_obj_new_tuple(self->input_color_count, result);
+}
+
+void common_hal_tilepalettemapper_tilepalettemapper_set_mapping(tilepalettemapper_tilepalettemapper_t *self, uint16_t x, uint16_t y, size_t len, mp_obj_t *items) {
+    uint32_t palette_len = common_hal_displayio_palette_get_len(self->palette);
+    for (uint16_t i = 0; i < MIN(len, self->input_color_count); i++) {
+        int mapping_val = mp_arg_validate_type_int(items[i], MP_QSTR_mapping_value);
+        mp_arg_validate_int_range(mapping_val, 0, palette_len - 1, MP_QSTR_mapping_value);
+        self->tile_mappings[y * self->width_in_tiles + x][i] = mapping_val;
+    }
+    self->needs_refresh = true;
+}
+
+void tilepalettemapper_tilepalettemapper_get_color(tilepalettemapper_tilepalettemapper_t *self, const _displayio_colorspace_t *colorspace, displayio_input_pixel_t *input_pixel, displayio_output_pixel_t *output_color, uint16_t x_tile_index, uint16_t y_tile_index) {
+    if (x_tile_index >= self->width_in_tiles || y_tile_index >= self->height_in_tiles) {
+        displayio_palette_get_color(self->palette, colorspace, input_pixel, output_color);
+        return;
+    }
+    uint16_t tile_index = y_tile_index * self->width_in_tiles + x_tile_index;
+    uint32_t mapped_index = self->tile_mappings[tile_index][input_pixel->pixel];
+    displayio_input_pixel_t tmp_pixel;
+    tmp_pixel.pixel = mapped_index;
+    displayio_palette_get_color(self->palette, colorspace, &tmp_pixel, output_color);
+}
+
+bool tilepalettemapper_tilepalettemapper_needs_refresh(tilepalettemapper_tilepalettemapper_t *self) {
+    return self->needs_refresh;
+}
+
+void tilepalettemapper_tilepalettemapper_finish_refresh(tilepalettemapper_tilepalettemapper_t *self) {
+    self->needs_refresh = false;
+}
diff --git a/shared-module/tilepalettemapper/TilePaletteMapper.h b/shared-module/tilepalettemapper/TilePaletteMapper.h
new file mode 100644
index 0000000000000..8502d4bf53601
--- /dev/null
+++ b/shared-module/tilepalettemapper/TilePaletteMapper.h
@@ -0,0 +1,29 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+
+
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "py/obj.h"
+#include "shared-module/displayio/Palette.h"
+
+typedef struct {
+    mp_obj_base_t base;
+    mp_obj_t palette;
+    uint16_t width_in_tiles;
+    uint16_t height_in_tiles;
+    uint16_t input_color_count;
+    uint16_t **tile_mappings;
+    bool needs_refresh;
+} tilepalettemapper_tilepalettemapper_t;
+
+bool tilepalettemapper_tilepalettemapper_needs_refresh(tilepalettemapper_tilepalettemapper_t *self);
+void tilepalettemapper_tilepalettemapper_finish_refresh(tilepalettemapper_tilepalettemapper_t *self);
+
+void tilepalettemapper_tilepalettemapper_get_color(tilepalettemapper_tilepalettemapper_t *self, const _displayio_colorspace_t *colorspace, displayio_input_pixel_t *input_pixel, displayio_output_pixel_t *output_color, uint16_t x_tile_index, uint16_t y_tile_index);
diff --git a/shared-module/tilepalettemapper/__init__.c b/shared-module/tilepalettemapper/__init__.c
new file mode 100644
index 0000000000000..e69de29bb2d1d


Reviews:
user: login: RetiredWizard
body: The palette and the ansi code is pushing the same boards so it probably makes sense to link the parameter flags
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T18:09:14Z
user: login: RetiredWizard
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T18:17:40Z
user: login: tannewt
body: Thanks for taking this on!

I'm thinking over how to organize the memory internally. The array of uint16_t for every tile is nice because it has a bunch of small allocations instead of one large one. It does cost the array of pointers though. This could be optimized to allocate the tile mapping only when changed. If the pointer is null then it'd just map 1:1 (or have a default mapping).

Definitely try with a palette larger than the input bitmap has. We'll want that for coloring text in addition to just inverting the colors.

To fix Zephyr you'll need to add `tilepalettemapper = false` to all of the `autogen_board_info.toml`s. Adding a python script may actually be quickest. https://github.com/adafruit/circuitpython/blob/main/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml#L92
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-05T19:34:46Z
user: login: FoamyGuy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T19:53:16Z
user: login: FoamyGuy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T19:56:02Z
user: login: tannewt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T20:09:56Z
user: login: FoamyGuy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T20:10:33Z
user: login: FoamyGuy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T20:12:30Z
user: login: tannewt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T20:12:31Z
user: login: RetiredWizard
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T20:52:40Z
user: login: tannewt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T21:01:11Z
user: login: FoamyGuy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T22:13:51Z
user: login: FoamyGuy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T14:39:07Z
user: login: FoamyGuy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T14:56:18Z
user: login: FoamyGuy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T14:56:49Z
user: login: FoamyGuy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T14:57:09Z
user: login: FoamyGuy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T14:57:21Z
user: login: tannewt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T18:41:41Z
user: login: tannewt
body: A few more small things. Thanks for the iteration! It's getting close.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-06T20:02:38Z
user: login: tannewt
body: Looks good! Thank you!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-11T18:38:13Z

Review comments:
diff_hunk: @@ -555,6 +555,9 @@ CFLAGS += -DCIRCUITPY_TERMINALIO=$(CIRCUITPY_TERMINALIO)
 CIRCUITPY_FONTIO ?= $(call enable-if-all,$(CIRCUITPY_DISPLAYIO) $(CIRCUITPY_TERMINALIO))
 CFLAGS += -DCIRCUITPY_FONTIO=$(CIRCUITPY_FONTIO)
 
+CIRCUITPY_TILEPALETTEMAPPER ?= $(CIRCUITPY_DISPLAYIO)
+CFLAGS += -DCIRCUITPY_TILEPALETTEMAPPER=$(CIRCUITPY_TILEPALETTEMAPPER)
+
path: py/circuitpy_mpconfig.mk
user: login: RetiredWizard
body: Rather than setting two parameters in the atmel-samd/boards mpconfigboard.mk files, I think the CIRCUITPY_TERMINALIO_VT100 I recently created should default to your new parameter. (After #10108 was merged, it's being set immediately following CIRCUITPY_TERMINALIO, but doesn't show up here?). I'm also setting the VT100 parameter to 0 for all samd21 boards in the atmel-samd mpconfigport.mk which could be removed if you make this change and decide to disable the new palette for all samd21 boards :grin:
created_at: 2025-03-05T18:04:00Z
author_association: NONE
diff_hunk: @@ -555,6 +555,9 @@ CFLAGS += -DCIRCUITPY_TERMINALIO=$(CIRCUITPY_TERMINALIO)
 CIRCUITPY_FONTIO ?= $(call enable-if-all,$(CIRCUITPY_DISPLAYIO) $(CIRCUITPY_TERMINALIO))
 CFLAGS += -DCIRCUITPY_FONTIO=$(CIRCUITPY_FONTIO)
 
+CIRCUITPY_TILEPALETTEMAPPER ?= $(CIRCUITPY_DISPLAYIO)
+CFLAGS += -DCIRCUITPY_TILEPALETTEMAPPER=$(CIRCUITPY_TILEPALETTEMAPPER)
+
path: py/circuitpy_mpconfig.mk
user: login: RetiredWizard
body: As I think about it, the two features aren't technically, necessarily linked so perhaps two parameters is appropriate. Trade-offs... :shrug:
created_at: 2025-03-05T18:17:40Z
author_association: NONE
diff_hunk: @@ -0,0 +1,165 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+#include <stdint.h>
+#include <stdlib.h>
+#include "shared/runtime/context_manager_helpers.h"
+#include "py/binary.h"
+#include "py/objproperty.h"
+#include "py/runtime.h"
+#include "shared-bindings/util.h"
+#include "shared-bindings/displayio/Palette.h"
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+
+//| class TilePaletteMapper:
+//|     """Contains a displayio.Palette a series of mappings from one palette color_index to another palette color_index.
path: shared-bindings/tilepalettemapper/TilePaletteMapper.c
user: login: tannewt
body: ```suggestion
//|     """Remaps color indices from the source bitmap to alternate indices on a per-tile basis. This allows for altering coloring of tiles based on their tilegrid location. It also allows for using a limited color bitmap with a wider array of colors."""
```
created_at: 2025-03-05T19:01:51Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,165 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+#include <stdint.h>
+#include <stdlib.h>
+#include "shared/runtime/context_manager_helpers.h"
+#include "py/binary.h"
+#include "py/objproperty.h"
+#include "py/runtime.h"
+#include "shared-bindings/util.h"
+#include "shared-bindings/displayio/Palette.h"
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+
+//| class TilePaletteMapper:
+//|     """Contains a displayio.Palette a series of mappings from one palette color_index to another palette color_index.
+//|     One mapping for tile in a 2D grid. Can be set as the pixel_shader for a TileGrid to re-map the colors for
+//|     specific tile(s) within the TileGrid."""
+//|
+//|     def __init__(self, palette: displayio.Palette, width: int, height: int) -> None:
path: shared-bindings/tilepalettemapper/TilePaletteMapper.c
user: login: tannewt
body: I think you'll want the number of colors used in the source bitmap too. That way you can validate the length/index of colors set. Bitmap has a `bits_per_value` property that could give you an upper bound for this. Or the palette returned by image load would give you the number in the actual image too.
created_at: 2025-03-05T19:03:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,72 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+#include <stdlib.h>
+#include "py/runtime.h"
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+#include "shared-bindings/displayio/Palette.h"
+
+void common_hal_tilepalettemapper_tilepalettemapper_construct(tilepalettemapper_tilepalettemapper_t *self,
+    mp_obj_t palette, uint16_t width, uint16_t height) {
+
+    self->palette = palette;
+    self->width_in_tiles = width;
+    self->height_in_tiles = height;
+    self->needs_refresh = false;
+    int mappings_len = width * height;
+    self->tile_mappings = (uint16_t **)m_malloc(mappings_len * sizeof(uint16_t *));
+    uint32_t palette_len = common_hal_displayio_palette_get_len(self->palette);
path: shared-module/tilepalettemapper/TilePaletteMapper.c
user: login: tannewt
body: Ah! Let's take in the number of input indices and then adjust the value length based on the number of colors in the palette.
created_at: 2025-03-05T19:08:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,72 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+#include <stdlib.h>
+#include "py/runtime.h"
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+#include "shared-bindings/displayio/Palette.h"
+
+void common_hal_tilepalettemapper_tilepalettemapper_construct(tilepalettemapper_tilepalettemapper_t *self,
+    mp_obj_t palette, uint16_t width, uint16_t height) {
+
+    self->palette = palette;
+    self->width_in_tiles = width;
+    self->height_in_tiles = height;
+    self->needs_refresh = false;
+    int mappings_len = width * height;
+    self->tile_mappings = (uint16_t **)m_malloc(mappings_len * sizeof(uint16_t *));
+    uint32_t palette_len = common_hal_displayio_palette_get_len(self->palette);
+    for (int i = 0; i < mappings_len; i++) {
+        self->tile_mappings[i] = (uint16_t *)m_malloc(palette_len * sizeof(uint16_t));
+        for (uint16_t j = 0; j < palette_len; j++) {
+            self->tile_mappings[i][j] = j;
+        }
+    }
+}
+
+uint16_t common_hal_tilepalettemapper_tilepalettemapper_get_width(tilepalettemapper_tilepalettemapper_t *self) {
+    return self->width_in_tiles;
+}
+
+uint16_t common_hal_tilepalettemapper_tilepalettemapper_get_height(tilepalettemapper_tilepalettemapper_t *self) {
+    return self->height_in_tiles;
+}
+
+mp_obj_t common_hal_tilepalettemapper_tilepalettemapper_get_palette(tilepalettemapper_tilepalettemapper_t *self) {
+    return self->palette;
+}
+
+mp_obj_t common_hal_tilepalettemapper_tilepalettemapper_get_mapping(tilepalettemapper_tilepalettemapper_t *self, uint16_t x, uint16_t y) {
+    mp_obj_list_t *list = MP_OBJ_TO_PTR(mp_obj_new_list(0, NULL));
path: shared-module/tilepalettemapper/TilePaletteMapper.c
user: login: tannewt
body: Return a `tuple` instead of a list because `tuples` aren't mutable. I'm worried that mutating the list will be expected to change behavior.
created_at: 2025-03-05T19:10:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,165 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+#include <stdint.h>
+#include <stdlib.h>
+#include "shared/runtime/context_manager_helpers.h"
+#include "py/binary.h"
+#include "py/objproperty.h"
+#include "py/runtime.h"
+#include "shared-bindings/util.h"
+#include "shared-bindings/displayio/Palette.h"
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+
+//| class TilePaletteMapper:
+//|     """Contains a displayio.Palette a series of mappings from one palette color_index to another palette color_index.
+//|     One mapping for tile in a 2D grid. Can be set as the pixel_shader for a TileGrid to re-map the colors for
+//|     specific tile(s) within the TileGrid."""
+//|
+//|     def __init__(self, palette: displayio.Palette, width: int, height: int) -> None:
+//|         """Create a TilePaletteMApper object to store a set of color mappings for tiles.
+//|
+//|         :param displayio.Palette palette: The palette to get mapped colors from.
path: shared-bindings/tilepalettemapper/TilePaletteMapper.c
user: login: tannewt
body: We may want to expand this later to take ColorConverter. That way we'd be able to do full color text rendering.
created_at: 2025-03-05T19:31:04Z
author_association: MEMBER
diff_hunk: @@ -485,6 +488,11 @@ bool displayio_tilegrid_fill_area(displayio_tilegrid_t *self,
             }
 
             output_pixel.opaque = true;
+            #if CIRCUITPY_TILEPALETTEMAPPER
+            if (mp_obj_is_type(self->pixel_shader, &tilepalettemapper_tilepalettemapper_type)) {
+                output_pixel.pixel = tilepalettemapper_tilepalettemapper_get_color(self->pixel_shader, tile_location, input_pixel.pixel);
path: shared-module/displayio/TileGrid.c
user: login: tannewt
body: I think you want to pass in the full input_pixel here instead of `tile_location`. That way if the dimensions of the tilegrid and the tilepalettemapper are different you can still map cleanly.
created_at: 2025-03-05T19:33:48Z
author_association: MEMBER
diff_hunk: @@ -485,6 +488,11 @@ bool displayio_tilegrid_fill_area(displayio_tilegrid_t *self,
             }
 
             output_pixel.opaque = true;
+            #if CIRCUITPY_TILEPALETTEMAPPER
+            if (mp_obj_is_type(self->pixel_shader, &tilepalettemapper_tilepalettemapper_type)) {
+                output_pixel.pixel = tilepalettemapper_tilepalettemapper_get_color(self->pixel_shader, tile_location, input_pixel.pixel);
path: shared-module/displayio/TileGrid.c
user: login: FoamyGuy
body: I had the assumption that it could only really work when the size of the TileGrid and the TilePaletteMapper match. What would be the expected behavior if those sizes differ?  If the mapper is bigger I suppose it doesn't matter too much beyond spending extra memory to store mappings that won't be used. If the mapping is smaller though what does it do for tiles outside the range of the mapper? I'll look at input_pixel closer, I'm not sure what all is on that object. 
created_at: 2025-03-05T19:53:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,165 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+#include <stdint.h>
+#include <stdlib.h>
+#include "shared/runtime/context_manager_helpers.h"
+#include "py/binary.h"
+#include "py/objproperty.h"
+#include "py/runtime.h"
+#include "shared-bindings/util.h"
+#include "shared-bindings/displayio/Palette.h"
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+
+//| class TilePaletteMapper:
+//|     """Contains a displayio.Palette a series of mappings from one palette color_index to another palette color_index.
+//|     One mapping for tile in a 2D grid. Can be set as the pixel_shader for a TileGrid to re-map the colors for
+//|     specific tile(s) within the TileGrid."""
+//|
+//|     def __init__(self, palette: displayio.Palette, width: int, height: int) -> None:
+//|         """Create a TilePaletteMApper object to store a set of color mappings for tiles.
+//|
+//|         :param displayio.Palette palette: The palette to get mapped colors from.
path: shared-bindings/tilepalettemapper/TilePaletteMapper.c
user: login: FoamyGuy
body: Would it have some other mechanism for mapping the colors? The way it is now I think this would require one array of length equal the number of colors in the colorspace for each tile. Is there enough room to store that?
created_at: 2025-03-05T19:56:02Z
author_association: COLLABORATOR
diff_hunk: @@ -485,6 +488,11 @@ bool displayio_tilegrid_fill_area(displayio_tilegrid_t *self,
             }
 
             output_pixel.opaque = true;
+            #if CIRCUITPY_TILEPALETTEMAPPER
+            if (mp_obj_is_type(self->pixel_shader, &tilepalettemapper_tilepalettemapper_type)) {
+                output_pixel.pixel = tilepalettemapper_tilepalettemapper_get_color(self->pixel_shader, tile_location, input_pixel.pixel);
path: shared-module/displayio/TileGrid.c
user: login: tannewt
body: I was thinking it'd repeat the mapping. So if TPM is 10 wide and tile 12 was requested then it'd match 2. 

The other way to handle it is to "pass" the value through and do no mapping. That way you could only have a portion of a tilegrid mapped. That might be clearer.
created_at: 2025-03-05T20:09:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,165 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+#include <stdint.h>
+#include <stdlib.h>
+#include "shared/runtime/context_manager_helpers.h"
+#include "py/binary.h"
+#include "py/objproperty.h"
+#include "py/runtime.h"
+#include "shared-bindings/util.h"
+#include "shared-bindings/displayio/Palette.h"
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+
+//| class TilePaletteMapper:
+//|     """Contains a displayio.Palette a series of mappings from one palette color_index to another palette color_index.
+//|     One mapping for tile in a 2D grid. Can be set as the pixel_shader for a TileGrid to re-map the colors for
+//|     specific tile(s) within the TileGrid."""
+//|
+//|     def __init__(self, palette: displayio.Palette, width: int, height: int) -> None:
+//|         """Create a TilePaletteMApper object to store a set of color mappings for tiles.
+//|
+//|         :param displayio.Palette palette: The palette to get mapped colors from.
path: shared-bindings/tilepalettemapper/TilePaletteMapper.c
user: login: FoamyGuy
body: Thinking this over and re-reading everything here a few more times I think maybe I'm understanding better. When it's changed to use the number of input colors rather than the number of output colors it wont be storing such a long array as long as the input image is still indexed. I'll attempt ColorConverter
created_at: 2025-03-05T20:10:33Z
author_association: COLLABORATOR
diff_hunk: @@ -555,6 +555,9 @@ CFLAGS += -DCIRCUITPY_TERMINALIO=$(CIRCUITPY_TERMINALIO)
 CIRCUITPY_FONTIO ?= $(call enable-if-all,$(CIRCUITPY_DISPLAYIO) $(CIRCUITPY_TERMINALIO))
 CFLAGS += -DCIRCUITPY_FONTIO=$(CIRCUITPY_FONTIO)
 
+CIRCUITPY_TILEPALETTEMAPPER ?= $(CIRCUITPY_DISPLAYIO)
+CFLAGS += -DCIRCUITPY_TILEPALETTEMAPPER=$(CIRCUITPY_TILEPALETTEMAPPER)
+
path: py/circuitpy_mpconfig.mk
user: login: FoamyGuy
body: I think keeping them separate makes sense, but I also don't have a strong preference one way or the other. If it's possible to set it in one spot as disabled for all SAMD21 boards that sounds more convenient than adding to each of board individually though.
created_at: 2025-03-05T20:12:30Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,165 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+#include <stdint.h>
+#include <stdlib.h>
+#include "shared/runtime/context_manager_helpers.h"
+#include "py/binary.h"
+#include "py/objproperty.h"
+#include "py/runtime.h"
+#include "shared-bindings/util.h"
+#include "shared-bindings/displayio/Palette.h"
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+
+//| class TilePaletteMapper:
+//|     """Contains a displayio.Palette a series of mappings from one palette color_index to another palette color_index.
+//|     One mapping for tile in a 2D grid. Can be set as the pixel_shader for a TileGrid to re-map the colors for
+//|     specific tile(s) within the TileGrid."""
+//|
+//|     def __init__(self, palette: displayio.Palette, width: int, height: int) -> None:
+//|         """Create a TilePaletteMApper object to store a set of color mappings for tiles.
+//|
+//|         :param displayio.Palette palette: The palette to get mapped colors from.
path: shared-bindings/tilepalettemapper/TilePaletteMapper.c
user: login: tannewt
body: You'd store one 24 bit color for every input value. So if it is 1 bit color, you store 2 RGB colors that then are converted via ColorConverter. This is how non-indexed bitmaps, OnDiskBitmaps works. The value *is* the color in that case. ColorConverter just switches it to the output color space.
created_at: 2025-03-05T20:12:31Z
author_association: MEMBER
diff_hunk: @@ -555,6 +555,9 @@ CFLAGS += -DCIRCUITPY_TERMINALIO=$(CIRCUITPY_TERMINALIO)
 CIRCUITPY_FONTIO ?= $(call enable-if-all,$(CIRCUITPY_DISPLAYIO) $(CIRCUITPY_TERMINALIO))
 CFLAGS += -DCIRCUITPY_FONTIO=$(CIRCUITPY_FONTIO)
 
+CIRCUITPY_TILEPALETTEMAPPER ?= $(CIRCUITPY_DISPLAYIO)
+CFLAGS += -DCIRCUITPY_TILEPALETTEMAPPER=$(CIRCUITPY_TILEPALETTEMAPPER)
+
path: py/circuitpy_mpconfig.mk
user: login: RetiredWizard
body: Putting your assignment here will turn it off for all samd21 boards by default. The board files can still turn it back on if needed.

https://github.com/adafruit/circuitpython/blob/e8de36a9f8a2a27a77a1888a6db389c548f426e3/ports/atmel-samd/mpconfigport.mk#L60-L62

Edit: There were a couple of SAMD51 boards you overflowed as well though :grin: If you want to turn it off for all samd21 & samd51 boards you can use the same file and just move it outside the if block.
created_at: 2025-03-05T20:52:40Z
author_association: NONE
diff_hunk: @@ -0,0 +1,165 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+#include <stdint.h>
+#include <stdlib.h>
+#include "shared/runtime/context_manager_helpers.h"
+#include "py/binary.h"
+#include "py/objproperty.h"
+#include "py/runtime.h"
+#include "shared-bindings/util.h"
+#include "shared-bindings/displayio/Palette.h"
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+
+//| class TilePaletteMapper:
+//|     """Contains a displayio.Palette a series of mappings from one palette color_index to another palette color_index.
+//|     One mapping for tile in a 2D grid. Can be set as the pixel_shader for a TileGrid to re-map the colors for
+//|     specific tile(s) within the TileGrid."""
+//|
+//|     def __init__(self, palette: displayio.Palette, width: int, height: int) -> None:
+//|         """Create a TilePaletteMApper object to store a set of color mappings for tiles.
+//|
+//|         :param displayio.Palette palette: The palette to get mapped colors from.
path: shared-bindings/tilepalettemapper/TilePaletteMapper.c
user: login: tannewt
body: > Thinking this over and re-reading everything here a few more times I think maybe I'm understanding better. When it's changed to use the number of input colors rather than the number of output colors it wont be storing such a long array as long as the input image is still indexed. I'll attempt ColorConverter

Right the input array length is dictated by the number of input indices to map. The size of the values in the array is dictated by what you are outputting. (The size of the palette or a full color for conversion.)
created_at: 2025-03-05T21:01:11Z
author_association: MEMBER
diff_hunk: @@ -485,6 +488,11 @@ bool displayio_tilegrid_fill_area(displayio_tilegrid_t *self,
             }
 
             output_pixel.opaque = true;
+            #if CIRCUITPY_TILEPALETTEMAPPER
+            if (mp_obj_is_type(self->pixel_shader, &tilepalettemapper_tilepalettemapper_type)) {
+                output_pixel.pixel = tilepalettemapper_tilepalettemapper_get_color(self->pixel_shader, tile_location, input_pixel.pixel);
path: shared-module/displayio/TileGrid.c
user: login: FoamyGuy
body: @tannewt I'm trying to change get_color() to take the input_pixel, and the output pixel, basically trying to match  displayio_palette_get_color() signature. 

It seems to me that input_pixel doesn't have enough information on it for TilePaletteMapper to determine the appropriate `tile_location`. `input_pixel.tile_x` and `input_pixel.tile_y` sound promising, but the values within seem to be in pixel units, not tile units, or else I'm misunderstanding what they represent. With my 10x4 TileGrid I'm finding that `tile_x` is going all the way up to 59.

If those are pixel unit values we could theoretically divide them by the `tile_width` and `tile_height` in order to arrive at the value in tile index units instead of pixels. But `input_pixel` doesn't have the `tile_width` or `tile_height` and it'd be wasteful to store those on each pixel instance anyhow. 

Should get_color take the `tile_location` in addition to the `input_pixel` and `output_pixel`? Or should it take the `tile_width` and `tile_height` in addition and calculate `tile_location` internally using them?
created_at: 2025-03-05T22:13:51Z
author_association: COLLABORATOR
diff_hunk: @@ -485,6 +488,11 @@ bool displayio_tilegrid_fill_area(displayio_tilegrid_t *self,
             }
 
             output_pixel.opaque = true;
+            #if CIRCUITPY_TILEPALETTEMAPPER
+            if (mp_obj_is_type(self->pixel_shader, &tilepalettemapper_tilepalettemapper_type)) {
+                output_pixel.pixel = tilepalettemapper_tilepalettemapper_get_color(self->pixel_shader, tile_location, input_pixel.pixel);
path: shared-module/displayio/TileGrid.c
user: login: FoamyGuy
body: Tinkering with it some more this morning, I'm not sure what `input_pixel.tile_x` and `input_pixel.tile_y` represent, they are perhaps influenced by scale. I tried passing in `tile_width` and `tile_height` and dividing but that led to the wrong tile_location result, printing out the values I realized they don't seem to be matching up with pixels how I thought when I wrote the above, they are larger than I they would be if they were what I understood them to be.

passing in `tile_location` is also not sufficient I think if it needs to handle gracefully when the grid size of the TPM is different from the TileGrid. 

I've broken out `x_tile_index` and `y_tile_index` from the line that calculates `tile_location` and passed those into the `get_color()` function. With that and a bit of logic inside to check the bounds it seems to work as expected for the cases when the grid size differs. I went with no mapping for any tiles outside the size of the TPM, having it wrap could potentially lead to some interesting visual effects, but I think it could be more confusing, maybe it could be added as an option later if we want it.
created_at: 2025-03-06T14:39:07Z
author_association: COLLABORATOR
diff_hunk: @@ -555,6 +555,9 @@ CFLAGS += -DCIRCUITPY_TERMINALIO=$(CIRCUITPY_TERMINALIO)
 CIRCUITPY_FONTIO ?= $(call enable-if-all,$(CIRCUITPY_DISPLAYIO) $(CIRCUITPY_TERMINALIO))
 CFLAGS += -DCIRCUITPY_FONTIO=$(CIRCUITPY_FONTIO)
 
+CIRCUITPY_TILEPALETTEMAPPER ?= $(CIRCUITPY_DISPLAYIO)
+CFLAGS += -DCIRCUITPY_TILEPALETTEMAPPER=$(CIRCUITPY_TILEPALETTEMAPPER)
+
path: py/circuitpy_mpconfig.mk
user: login: FoamyGuy
body: Thank you! I do want to keep it on for any samd51s that have room. The pyportal titano for instance is what all my testing was on and it seems fine on that device. 
created_at: 2025-03-06T14:56:18Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,165 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+#include <stdint.h>
+#include <stdlib.h>
+#include "shared/runtime/context_manager_helpers.h"
+#include "py/binary.h"
+#include "py/objproperty.h"
+#include "py/runtime.h"
+#include "shared-bindings/util.h"
+#include "shared-bindings/displayio/Palette.h"
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+
+//| class TilePaletteMapper:
+//|     """Contains a displayio.Palette a series of mappings from one palette color_index to another palette color_index.
path: shared-bindings/tilepalettemapper/TilePaletteMapper.c
user: login: FoamyGuy
body: updated with the latest commit
created_at: 2025-03-06T14:56:48Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,165 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+#include <stdint.h>
+#include <stdlib.h>
+#include "shared/runtime/context_manager_helpers.h"
+#include "py/binary.h"
+#include "py/objproperty.h"
+#include "py/runtime.h"
+#include "shared-bindings/util.h"
+#include "shared-bindings/displayio/Palette.h"
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+
+//| class TilePaletteMapper:
+//|     """Contains a displayio.Palette a series of mappings from one palette color_index to another palette color_index.
+//|     One mapping for tile in a 2D grid. Can be set as the pixel_shader for a TileGrid to re-map the colors for
+//|     specific tile(s) within the TileGrid."""
+//|
+//|     def __init__(self, palette: displayio.Palette, width: int, height: int) -> None:
path: shared-bindings/tilepalettemapper/TilePaletteMapper.c
user: login: FoamyGuy
body: updated with latest commit to accept `input_color_count`
created_at: 2025-03-06T14:57:08Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,72 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+#include <stdlib.h>
+#include "py/runtime.h"
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+#include "shared-bindings/displayio/Palette.h"
+
+void common_hal_tilepalettemapper_tilepalettemapper_construct(tilepalettemapper_tilepalettemapper_t *self,
+    mp_obj_t palette, uint16_t width, uint16_t height) {
+
+    self->palette = palette;
+    self->width_in_tiles = width;
+    self->height_in_tiles = height;
+    self->needs_refresh = false;
+    int mappings_len = width * height;
+    self->tile_mappings = (uint16_t **)m_malloc(mappings_len * sizeof(uint16_t *));
+    uint32_t palette_len = common_hal_displayio_palette_get_len(self->palette);
+    for (int i = 0; i < mappings_len; i++) {
+        self->tile_mappings[i] = (uint16_t *)m_malloc(palette_len * sizeof(uint16_t));
+        for (uint16_t j = 0; j < palette_len; j++) {
+            self->tile_mappings[i][j] = j;
+        }
+    }
+}
+
+uint16_t common_hal_tilepalettemapper_tilepalettemapper_get_width(tilepalettemapper_tilepalettemapper_t *self) {
+    return self->width_in_tiles;
+}
+
+uint16_t common_hal_tilepalettemapper_tilepalettemapper_get_height(tilepalettemapper_tilepalettemapper_t *self) {
+    return self->height_in_tiles;
+}
+
+mp_obj_t common_hal_tilepalettemapper_tilepalettemapper_get_palette(tilepalettemapper_tilepalettemapper_t *self) {
+    return self->palette;
+}
+
+mp_obj_t common_hal_tilepalettemapper_tilepalettemapper_get_mapping(tilepalettemapper_tilepalettemapper_t *self, uint16_t x, uint16_t y) {
+    mp_obj_list_t *list = MP_OBJ_TO_PTR(mp_obj_new_list(0, NULL));
path: shared-module/tilepalettemapper/TilePaletteMapper.c
user: login: FoamyGuy
body: updated in latest commit to run a tuple
created_at: 2025-03-06T14:57:21Z
author_association: COLLABORATOR
diff_hunk: @@ -485,6 +488,11 @@ bool displayio_tilegrid_fill_area(displayio_tilegrid_t *self,
             }
 
             output_pixel.opaque = true;
+            #if CIRCUITPY_TILEPALETTEMAPPER
+            if (mp_obj_is_type(self->pixel_shader, &tilepalettemapper_tilepalettemapper_type)) {
+                output_pixel.pixel = tilepalettemapper_tilepalettemapper_get_color(self->pixel_shader, tile_location, input_pixel.pixel);
path: shared-module/displayio/TileGrid.c
user: login: tannewt
body: Ah! `tile_x` must be for dithering computation. Seems like you figured out a good approach!
created_at: 2025-03-06T18:41:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,167 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+#include <stdint.h>
+#include <stdlib.h>
+#include "shared/runtime/context_manager_helpers.h"
+#include "py/binary.h"
+#include "py/objproperty.h"
+#include "py/runtime.h"
+#include "shared-bindings/util.h"
+#include "shared-bindings/displayio/Palette.h"
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+
+//| class TilePaletteMapper:
+//|     """Remaps color indices from the source bitmap to alternate indices on a
+//|     per-tile basis. This allows for altering coloring of tiles based on
+//|     their tilegrid location. It also allows for using a limited color
+//|     bitmap with a wider array of colors."""
+//|
+//|     def __init__(
+//|         self, palette: displayio.Palette, input_color_count: int, width: int, height: int
+//|     ) -> None:
+//|         """Create a TilePaletteMApper object to store a set of color mappings for tiles.
+//|
+//|         :param displayio.Palette palette: The palette to get mapped colors from.
+//|         :param int input_color_count: The number of colors in in the input bitmap.
+//|         :param int width: The width of the grid in tiles.
+//|         :param int height: The height of the grid in tiles."""
+//|
+
+static mp_obj_t tilepalettemapper_tilepalettemapper_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
+    enum { ARG_palette, ARG_input_color_count, ARG_width, ARG_height };
+    static const mp_arg_t allowed_args[] = {
+        { MP_QSTR_palette, MP_ARG_OBJ | MP_ARG_REQUIRED },
+        { MP_QSTR_input_color_count, MP_ARG_INT | MP_ARG_REQUIRED, {.u_int = 1} },
+        { MP_QSTR_width, MP_ARG_INT | MP_ARG_REQUIRED, {.u_int = 1} },
+        { MP_QSTR_height, MP_ARG_INT | MP_ARG_REQUIRED, {.u_int = 1} },
path: shared-bindings/tilepalettemapper/TilePaletteMapper.c
user: login: tannewt
body: No need for defaults on required args.

```suggestion
        { MP_QSTR_input_color_count, MP_ARG_INT | MP_ARG_REQUIRED },
        { MP_QSTR_width, MP_ARG_INT | MP_ARG_REQUIRED },
        { MP_QSTR_height, MP_ARG_INT | MP_ARG_REQUIRED },
```
created_at: 2025-03-06T19:55:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,167 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+#include <stdint.h>
+#include <stdlib.h>
+#include "shared/runtime/context_manager_helpers.h"
+#include "py/binary.h"
+#include "py/objproperty.h"
+#include "py/runtime.h"
+#include "shared-bindings/util.h"
+#include "shared-bindings/displayio/Palette.h"
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+
+//| class TilePaletteMapper:
+//|     """Remaps color indices from the source bitmap to alternate indices on a
+//|     per-tile basis. This allows for altering coloring of tiles based on
+//|     their tilegrid location. It also allows for using a limited color
+//|     bitmap with a wider array of colors."""
+//|
+//|     def __init__(
+//|         self, palette: displayio.Palette, input_color_count: int, width: int, height: int
+//|     ) -> None:
+//|         """Create a TilePaletteMApper object to store a set of color mappings for tiles.
+//|
+//|         :param displayio.Palette palette: The palette to get mapped colors from.
+//|         :param int input_color_count: The number of colors in in the input bitmap.
+//|         :param int width: The width of the grid in tiles.
+//|         :param int height: The height of the grid in tiles."""
+//|
+
+static mp_obj_t tilepalettemapper_tilepalettemapper_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
+    enum { ARG_palette, ARG_input_color_count, ARG_width, ARG_height };
+    static const mp_arg_t allowed_args[] = {
+        { MP_QSTR_palette, MP_ARG_OBJ | MP_ARG_REQUIRED },
+        { MP_QSTR_input_color_count, MP_ARG_INT | MP_ARG_REQUIRED, {.u_int = 1} },
+        { MP_QSTR_width, MP_ARG_INT | MP_ARG_REQUIRED, {.u_int = 1} },
+        { MP_QSTR_height, MP_ARG_INT | MP_ARG_REQUIRED, {.u_int = 1} },
+    };
+    mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
+    mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
+    mp_obj_t palette = args[ARG_palette].u_obj;
+    if (!mp_obj_is_type(palette, &displayio_palette_type)) {
+        mp_raise_TypeError_varg(MP_ERROR_TEXT("unsupported %q type"), MP_QSTR_pixel_shader);
+    }
+    tilepalettemapper_tilepalettemapper_t *self = mp_obj_malloc(tilepalettemapper_tilepalettemapper_t, &tilepalettemapper_tilepalettemapper_type);
+    common_hal_tilepalettemapper_tilepalettemapper_construct(self, palette, args[ARG_input_color_count].u_int, args[ARG_width].u_int, args[ARG_height].u_int);
+
+    return MP_OBJ_FROM_PTR(self);
+}
+
+//|     width: int
+//|     """Width of the tile palette mapper in tiles."""
+static mp_obj_t tilepalettemapper_tilepalettemapper_obj_get_width(mp_obj_t self_in) {
+    tilepalettemapper_tilepalettemapper_t *self = MP_OBJ_TO_PTR(self_in);
+    return MP_OBJ_NEW_SMALL_INT(common_hal_tilepalettemapper_tilepalettemapper_get_width(self));
+}
+MP_DEFINE_CONST_FUN_OBJ_1(tilepalettemapper_tilepalettemapper_get_width_obj, tilepalettemapper_tilepalettemapper_obj_get_width);
+
+MP_PROPERTY_GETTER(tilepalettemapper_tilepalettemapper_width_obj,
+    (mp_obj_t)&tilepalettemapper_tilepalettemapper_get_width_obj);
+
+//|     height: int
+//|     """Height of the tile palette mapper in tiles."""
+static mp_obj_t tilepalettemapper_tilepalettemapper_obj_get_height(mp_obj_t self_in) {
+    tilepalettemapper_tilepalettemapper_t *self = MP_OBJ_TO_PTR(self_in);
+    return MP_OBJ_NEW_SMALL_INT(common_hal_tilepalettemapper_tilepalettemapper_get_height(self));
+}
+MP_DEFINE_CONST_FUN_OBJ_1(tilepalettemapper_tilepalettemapper_get_height_obj, tilepalettemapper_tilepalettemapper_obj_get_height);
+
+MP_PROPERTY_GETTER(tilepalettemapper_tilepalettemapper_height_obj,
+    (mp_obj_t)&tilepalettemapper_tilepalettemapper_get_height_obj);
+
+
+//|     palette: displayio.Palette
+//|     """The palette that the mapper uses."""
+//|
+static mp_obj_t tilepalettemapper_tilepalettemapper_obj_get_palette(mp_obj_t self_in) {
+    tilepalettemapper_tilepalettemapper_t *self = MP_OBJ_TO_PTR(self_in);
+    return common_hal_tilepalettemapper_tilepalettemapper_get_palette(self);
+}
+MP_DEFINE_CONST_FUN_OBJ_1(tilepalettemapper_tilepalettemapper_get_palette_obj, tilepalettemapper_tilepalettemapper_obj_get_palette);
+
+MP_PROPERTY_GETTER(tilepalettemapper_tilepalettemapper_palette_obj,
+    (mp_obj_t)&tilepalettemapper_tilepalettemapper_get_palette_obj);
+
+
+//|     def __getitem__(self, index: Union[Tuple[int, int], int]) -> List[int]:
path: shared-bindings/tilepalettemapper/TilePaletteMapper.c
user: login: tannewt
body: ```suggestion
//|     def __getitem__(self, index: Union[Tuple[int, int], int]) -> Tuple[int]:
```
created_at: 2025-03-06T19:55:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,78 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+#include <stdlib.h>
+#include "py/runtime.h"
+#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
+#include "shared-bindings/displayio/Palette.h"
+
+void common_hal_tilepalettemapper_tilepalettemapper_construct(tilepalettemapper_tilepalettemapper_t *self,
+    mp_obj_t palette, uint16_t input_color_count, uint16_t width, uint16_t height) {
+
+    self->palette = palette;
+    self->width_in_tiles = width;
+    self->height_in_tiles = height;
+    self->input_color_count = input_color_count;
+    self->needs_refresh = false;
+    int mappings_len = width * height;
+    self->tile_mappings = (uint16_t **)m_malloc(mappings_len * sizeof(uint16_t *));
+    for (int i = 0; i < mappings_len; i++) {
+        self->tile_mappings[i] = (uint16_t *)m_malloc(input_color_count * sizeof(uint16_t));
+        for (uint16_t j = 0; j < input_color_count; j++) {
+            self->tile_mappings[i][j] = j;
+        }
+    }
+}
+
+uint16_t common_hal_tilepalettemapper_tilepalettemapper_get_width(tilepalettemapper_tilepalettemapper_t *self) {
+    return self->width_in_tiles;
+}
+
+uint16_t common_hal_tilepalettemapper_tilepalettemapper_get_height(tilepalettemapper_tilepalettemapper_t *self) {
+    return self->height_in_tiles;
+}
+
+mp_obj_t common_hal_tilepalettemapper_tilepalettemapper_get_palette(tilepalettemapper_tilepalettemapper_t *self) {
+    return self->palette;
+}
+
+mp_obj_t common_hal_tilepalettemapper_tilepalettemapper_get_mapping(tilepalettemapper_tilepalettemapper_t *self, uint16_t x, uint16_t y) {
+    int index = x + y * self->width_in_tiles;
+    mp_obj_t result[self->input_color_count];
+    for (uint32_t i = 0; i < self->input_color_count; i++) {
+        result[i] = mp_obj_new_int(self->tile_mappings[index][i]);
+    }
+    return mp_obj_new_tuple(self->input_color_count, result);
+}
+
+void common_hal_tilepalettemapper_tilepalettemapper_set_mapping(tilepalettemapper_tilepalettemapper_t *self, uint16_t x, uint16_t y, size_t len, mp_obj_t *items) {
+    uint32_t palette_len = common_hal_displayio_palette_get_len(self->palette);
+    for (uint16_t i = 0; i < len; i++) {
path: shared-module/tilepalettemapper/TilePaletteMapper.c
user: login: tannewt
body: Make sure you can't assign more than input color count.

```suggestion
    for (uint16_t i = 0; i < MIN(len, self->input_color_count); i++) {
```
created_at: 2025-03-06T20:00:00Z
author_association: MEMBER

PR comments:
user: login: FoamyGuy
created_at: 2025-03-05T17:40:56Z
body: a handful of atmel boards overflowed and I disabled the new module in them. I am not sure how to fix the zephyr builds that failed. 
author_association: COLLABORATOR
user: login: FoamyGuy
created_at: 2025-03-06T15:22:40Z
body: The latest commit contains a bunch of the requested changes, as well as fixing the functionality for more colors, it turned out that was not behaving correctly before. 

This is the updated test script i've been using the test the color functionality:
```
import time
from displayio import Palette
from tilepalettemapper import TilePaletteMapper
import board
import terminalio
import displayio
import rainbowio


time.sleep(1)
p = Palette(258)
p[0] = 0x000000
p[1] = 0xffffff
for i in range(256):
    p[i+2] = rainbowio.colorwheel(i)

# for i in range(258):
#     print(f"color {i}: {p[i]:06x}")
bbox = terminalio.FONT.get_bounding_box()
main_group = displayio.Group(scale=4)
board.DISPLAY.root_group = main_group

#tpm = TilePaletteMapper(p, 2, 20,3)
#tpm = TilePaletteMapper(p, 2, 9,3)
tpm = TilePaletteMapper(p, 2,10,4)

FONT_BMP = terminalio.FONT.bitmap

tg = displayio.TileGrid(FONT_BMP, pixel_shader=tpm,
                        tile_width=bbox[0], tile_height=bbox[1],
                        width=10, height=4, default_tile=0)
#tg.pixel_shader = tpm
main_group.append(tg)

for i in range(40):
    tg[i] = i

# print(tpm.width)
# print(tpm.palette[1])
print(tpm[1,0])
tpm[1,0] = [1,0]
print(tpm[1,0])


cur_offset = 2
while True:

    for y in range(tg.height):
        for x in range(tg.width):
            cur = (x, y)
            try:
                tpm[cur] = [0, cur_offset]
            except IndexError:
                pass
            cur_offset = max(2, (cur_offset + 2) % 258)
            time.sleep(0.005)
    for y in range(tg.height-1, -1, -1):
        for x in range(tg.width-1, -1, -1):
            cur = (x, y)
            try:
                tpm[cur] = [0, cur_offset]
            except IndexError:
                pass
            cur_offset = max(2, (cur_offset + 2) % 258)
            time.sleep(0.005)
```
There are also commented out lines that I've used to test the functionality when the TPM size does not match the TileGrid size. 

@tannewt I think the two remaining things that you mentioned that haven't been implemented yet are ColorConverter support, and optimizing by using null to mean "no mapping" for a tile. I'm wondering if it's okay to work on those as a followup to this?  I'd love to get the palette support solidified and exercised more with user code before venturing into ColorConverter if possible.  

The null for no mapping efficiency improvement I'd like to attempt as well, but I don't have high confidence that I'll do it successfully without wrecking the functionality a bit first. I'll probably start that on a new branch anyhow. In addition to keeping this one functional it would also let me reset my local repo state which seems to have gotten some unrelated changes intermixed in that I'm having to uncheck boxes for every time I commit. 

author_association: COLLABORATOR
user: login: tannewt
created_at: 2025-03-06T19:52:52Z
body: > I'm wondering if it's okay to work on those as a followup to this?

Yup, totally!
author_association: MEMBER
user: login: FoamyGuy
created_at: 2025-03-06T20:11:32Z
body: Thank you, updated all of those in the latest commits. 
author_association: COLLABORATOR