Touch Input Architecture
This document describes how resistive and capacitive touch integrate with PixelRoot32: data flow, calibration, and platform responsibilities.
For public API details (methods, parameters), see API Reference — Input Module.
1. Design principles
- Touch is optional. Enable with
PIXELROOT32_ENABLE_TOUCH=1in build flags (default: disabled). Saves ~200 bytes when disabled. - Engine optionally owns touch processing. When
PIXELROOT32_ENABLE_TOUCH=1andsetTouchManager()is called, Engine automatically processes touch events inEngine::update()and sends them toScene::processTouchEvents(). When disabled or unset, use the manual integration pattern (section 4). - Single coordinate space. After the active adapter runs, coordinates are screen pixels in the same range as
PHYSICAL_DISPLAY_WIDTH×PHYSICAL_DISPLAY_HEIGHT(or yourTouchManagerconstructor bounds). Game logic, UI hit tests, and debug overlays should use this space. - UI before gameplay.
Scene::processTouchEventsrunsUIManager::processEventsfirst (whenPIXELROOT32_ENABLE_UI_SYSTEM), marks events consumed, then invokesonUnconsumedTouchEventfor each remaining event. - Scene owns touch widgets. Construct
UITouchButton/UITouchSlider/UITouchCheckbox(and layouts) ininit(), keep them instd::unique_ptror the scene arena, callUIManager::addElementfor hit testing and dispatch, andaddEntityon a layout (or entity) soupdate/drawrun with the rest of the scene.UIManagerdoes not allocate or destroy widgets.
2. Pipeline (high level)
With Engine Integration (PIXELROOT32_ENABLE_TOUCH=1 + setTouchManager())
Hardware (XPT2046, GT911, …)
→ Adapter readImpl() [median sample, optional GPIO bit-bang SPI]
→ TouchCalibration / compile-time calibration macros
→ TouchPoint (x, y, pressed)
→ TouchManager::update()
→ clamp to display bounds
→ TouchManager::getTouchPoints()
→ Engine::setTouchManager(&touchManager) [called once in setup()]
→ Engine::update() [automatic each frame]
→ Engine polls getTouchPoints()
→ Detects release (count: >0 → 0)
→ TouchEventDispatcher processes points (gestures: down, drag, up, …)
→ Scene::processTouchEvents()
→ UIManager (optional)
→ onUnconsumedTouchEvent()Input abstraction (buttons + touch + PC)
Without Engine Integration (PIXELROOT32_ENABLE_TOUCH=0)
Hardware (XPT2046, GT911, …)
→ TouchManager::update()
→ TouchManager::getTouchPoints() [only raw active touches]
→ Manual release detection required (track state externally)
→ Manual touch injection to engine.getTouchDispatcher()
→ Scene::processTouchEvents() [manual call in user loop]
→ UIManager (optional)
→ onUnconsumedTouchEvent()Optional gameplay layer: ActorTouchController consumes TouchEvents in onUnconsumedTouchEvent to drag registered Actors (hit test, drag threshold, position update).
3. Key components
| Component | Role |
|---|---|
TouchManager | Polls adapter, clamps points, exposes getTouchPoints(). |
TouchCalibration | forResolution(w,h), transform, rotation; shared with adapter via setCalibration. |
XPT2046Adapter | ESP32 XPT2046: shared TFT SPI or GPIO bit-bang (XPT2046_USE_GPIO_SPI) for boards like ESP32-2432S028R. |
TouchEventDispatcher (in Engine) | Converts point stream into TouchEvent gestures (TouchDown, DragMove, TouchUp, …). |
ActorTouchController | Drags actors from touch; optional hit slop for resistive alignment. |
Scene::processTouchEvents | Central entry for a frame's touch batch; runs UIManager::processEvents then virtual onUnconsumedTouchEvent. |
UIManager | Non-owning registry (addElement); processEvents calls UITouchElement::processEvent. Does not draw or own widget memory. |
4. Per-frame integration (recommended)
Call once per frame, in order:
touchManager.update(deltaTimeMs);TouchEvent buf[TOUCH_EVENT_QUEUE_SIZE];uint8_t n = touchManager.getEvents(buf, …);- If
n > 0and a scene is active:scene->processTouchEvents(buf, n); engine.run();(or your update/draw split).
Touch is intentionally processed before engine.run() in this pattern so gameplay reacts in the same frame as the sample.
5. Initialization order (critical on ESP32)
TouchManager::init() forwards the internal TouchCalibration to the adapter. The default TouchCalibration struct uses 320×240 defaults until you call forResolution.
Always set calibration for your real panel before init():
TouchCalibration cal = TouchCalibration::forResolution(PHYSICAL_DISPLAY_WIDTH, PHYSICAL_DISPLAY_HEIGHT);
touchManager.setCalibration(cal);
touchManager.init();Wrong dimensions break horizontal mirror span (displayWidth - x), raw-to-screen mapping width/height, and clamping.
6. XPT2046 GPIO path (e.g. Sunton 2432S028R / CYD)
When -D XPT2046_USE_GPIO_SPI is set, the adapter uses a separate bit-banged bus (pins overridable via XPT2046_GPIO_* macros). After reading raw ADC channels and optional axis swap, coordinates go through this order (see XPT2046Adapter.cpp):
- Map to screen (either
TouchCalibration::transformor linearXPT2046_GPIO_USE_RAW_RANGEusingXPT2046_RAW_*_LO/HI). - Optional
XPT2046_GPIO_VENDOR_COORDSrotation-style swap. XPT2046_GPIO_MIRROR_X— horizontal flip in screen space (x = displayWidth - x).XPT2046_CAL_OFFSET_X/Y— final nudge in post-mirror pixels.- Clamp to
calibration.displayWidth/displayHeight.
Typical alignment flags for CYD-class boards (exact values are project-specific):
XPT2046_GPIO_SWAP_AXES— finger vertical/horizontal matched wrong screen axes.XPT2046_GPIO_MIRROR_X— left/right inverted after swap.XPT2046_GPIO_USE_RAW_RANGE— map real ADC span to full screen instead of assuming 0–4095.
7. Gameplay hit testing and slop
Resistive stacks often report a cluster of pixels offset from the visible sprite. ActorTouchController::setTouchHitSlop(n) expands the hit rectangle by n pixels per side for picking only (not rendering).
8. Cross-platform vs. board-specific configuration
To port touch features to a new board, it's important to understand which parts of this architecture apply generally and which are specific to particular hardware setups.
Generic engine components (apply to all boards)
The following aspects of the touch system are hardware-agnostic and work exactly the same regardless of your board:
- The data flow:
TouchManager->TouchEventDispatcher->Scene::processTouchEvents-> UI/Gameplay. - The coordinate space: Your game logic always receives touch events in scaled screen pixels.
- Gameplay tools:
ActorTouchControllerand its hit-slop mechanics work identically on any touchscreen. - Initialization: You must always call
TouchCalibration::forResolutionandtouchManager.setCalibration()beforetouchManager.init().
Hardware-specific configuration (board-dependent)
The underlying driver and its build flags vary drastically by device. You cannot copy-paste the platformio.ini touch flags from one board to another and expect them to work without adjustment.
Example 1: ESP32-2432S028R (CYD) This board uses an XPT2046 resistive touch controller on a separate bit-banged GPIO bus, not the main TFT SPI bus. It requires specific macro overrides:
-D TOUCH_DRIVER_XPT2046-D XPT2046_USE_GPIO_SPI- Specific pins (
XPT2046_GPIO_MOSI, etc.) - Panel-specific tuning (
XPT2046_GPIO_SWAP_AXES,XPT2046_GPIO_MIRROR_X,XPT2046_GPIO_USE_RAW_RANGE).
Example 2: A standard ESP32 with shared SPI XPT2046 Many TFT shields share the main SPI bus for both the display and the touch controller.
- You would not use
XPT2046_USE_GPIO_SPI. - The
TFT_eSPIlibrary typically handles the low-level SPI sharing via theTFT_eSPI_TouchBridgeor built-in calibration. - You only need
-D TOUCH_DRIVER_XPT2046and-D PIXELROOT32_USE_TFT_ESPI_DRIVER.
Example 3: A capacitive touchscreen (e.g., GT911) Capacitive screens use I²C and usually report perfect screen coordinates out of the box, requiring no axis swapping or offset calibration.
- You would use
-D TOUCH_DRIVER_GT911instead. - Resistive calibration flags (like
SWAP_AXESorMIRROR_X) are not used.
10. Engine Integration
When PIXELROOT32_ENABLE_TOUCH=1, Engine automatically handles touch processing, eliminating the need for manual integration in your game loop.
10.1 Automatic Touch (PIXELROOT32_ENABLE_TOUCH=1 + setTouchManager)
With the flag enabled and setTouchManager() called, Engine automatically:
- Connects
SDL2_Drawerto the touch dispatcher on init (Native only) - Polls
touchManager.getTouchPoints()each frame inEngine::update() - Detects releases when count goes from >0 to 0
- Processes gesture events through internal
TouchEventDispatcher - Dispatches to
Scene::processTouchEvents()each frame
Native (PC):
// No manual setup needed - Engine handles it automatically
Engine engine(displayConfig, inputConfig);
engine.init();
engine.setScene(&myScene);
engine.run(); // Mouse events → touch events automaticallyESP32 - Using setTouchManager (recommended):
// En setup():
touchManager.init();
engine.setTouchManager(&touchManager); // 1 línea
// En loop():
touchManager.update(frameDt);
engine.run(); // Engine maneja todo automáticamenteThe developer no longer needs to manually inject touch points or track release state - Engine handles it all internally.
10.2 Manual Touch (legacy, without setTouchManager)
When PIXELROOT32_ENABLE_TOUCH=1 but setTouchManager() is not called, you can manually inject touch points:
void loop() {
touchManager.update(frameDt);
TouchPoint points[TOUCH_MAX_POINTS];
uint8_t count = touchManager.getTouchPoints(points);
static bool wasTouching = false;
static int16_t lastX = 0, lastY = 0;
auto& dispatcher = engine.getTouchDispatcher();
if (count > 0) {
for (uint8_t i = 0; i < count; i++) {
dispatcher.processTouch(points[i].id, true, points[i].x, points[i].y, points[i].ts);
}
wasTouching = true;
lastX = points[0].x;
lastY = points[0].y;
} else if (wasTouching) {
dispatcher.processTouch(0, false, lastX, lastY, millis());
wasTouching = false;
}
engine.run();
}Note: Using
setTouchManager()(section 10.1) is recommended instead of this manual pattern.
10.3 InputManager Touch API (Native only)
When PIXELROOT32_ENABLE_TOUCH=1, InputManager provides mouse-to-touch conversion for Native platforms:
processSDLEvent(const SDL_Event& event): Converts SDL mouse events to touch events (Native only).
10.4 SDL2_Drawer Touch Integration
On Native (PC), SDL2_Drawer automatically converts mouse events to touch events for compatibility with touch UI widgets. This happens automatically when PIXELROOT32_ENABLE_TOUCH=1.
// Automatic during init when PIXELROOT32_ENABLE_TOUCH=1
// SDL2_Drawer connects to Engine's touchDispatcher
// Mouse → touch coordinate conversion happens internally
// Coordinates are mapped to the logical display resolution11. Related documentation
- API Reference — Touch Input
- Architecture Index (system layer documentation)
include/core/Engine.h— documented touch loop contract in class comment
