1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial |
3 | |
4 | #include <deque> |
5 | #include <mutex> |
6 | #include "slint-esp.h" |
7 | #include "slint-platform.h" |
8 | #include "esp_lcd_panel_ops.h" |
9 | #if __has_include("soc/soc_caps.h") |
10 | # include "soc/soc_caps.h" |
11 | #endif |
12 | #if SOC_LCD_RGB_SUPPORTED && ESP_IDF_VERSION_MAJOR >= 5 |
13 | # include "esp_lcd_panel_rgb.h" |
14 | #endif |
15 | #include "esp_log.h" |
16 | |
17 | static const char *TAG = "slint_platform" ; |
18 | |
19 | using RepaintBufferType = slint::platform::SoftwareRenderer::RepaintBufferType; |
20 | |
21 | struct EspPlatform : public slint::platform::Platform |
22 | { |
23 | EspPlatform(slint::PhysicalSize size, esp_lcd_panel_handle_t panel, |
24 | std::optional<esp_lcd_touch_handle_t> touch, |
25 | std::span<slint::platform::Rgb565Pixel> buffer1, |
26 | std::optional<std::span<slint::platform::Rgb565Pixel>> buffer2 = {} |
27 | #ifdef SLINT_FEATURE_EXPERIMENTAL |
28 | , |
29 | slint::platform::SoftwareRenderer::RenderingRotation rotation = {} |
30 | #endif |
31 | ) |
32 | : size(size), |
33 | panel_handle(panel), |
34 | touch_handle(touch), |
35 | buffer1(buffer1), |
36 | buffer2(buffer2) |
37 | #ifdef SLINT_FEATURE_EXPERIMENTAL |
38 | , |
39 | rotation(rotation) |
40 | #endif |
41 | { |
42 | } |
43 | |
44 | std::unique_ptr<slint::platform::WindowAdapter> create_window_adapter() override; |
45 | |
46 | std::chrono::milliseconds duration_since_start() override; |
47 | void run_event_loop() override; |
48 | void quit_event_loop() override; |
49 | void run_in_event_loop(Task) override; |
50 | |
51 | private: |
52 | slint::PhysicalSize size; |
53 | esp_lcd_panel_handle_t panel_handle; |
54 | std::optional<esp_lcd_touch_handle_t> touch_handle; |
55 | std::span<slint::platform::Rgb565Pixel> buffer1; |
56 | std::optional<std::span<slint::platform::Rgb565Pixel>> buffer2; |
57 | #ifdef SLINT_FEATURE_EXPERIMENTAL |
58 | slint::platform::SoftwareRenderer::RenderingRotation rotation; |
59 | #endif |
60 | class EspWindowAdapter *m_window = nullptr; |
61 | |
62 | // Need to be static because we can't pass user data to the touch interrupt callback |
63 | static TaskHandle_t task; |
64 | std::mutex queue_mutex; |
65 | std::deque<slint::platform::Platform::Task> queue; // protected by queue_mutex |
66 | bool quit = false; // protected by queue_mutex |
67 | }; |
68 | |
69 | class EspWindowAdapter : public slint::platform::WindowAdapter |
70 | { |
71 | public: |
72 | slint::platform::SoftwareRenderer m_renderer; |
73 | bool needs_redraw = true; |
74 | const slint::PhysicalSize m_size; |
75 | |
76 | explicit EspWindowAdapter(RepaintBufferType buffer_type, slint::PhysicalSize size) |
77 | : m_renderer(buffer_type), m_size(size) |
78 | { |
79 | } |
80 | |
81 | slint::platform::AbstractRenderer &renderer() override { return m_renderer; } |
82 | |
83 | slint::PhysicalSize size() override { return m_size; } |
84 | |
85 | void request_redraw() override { needs_redraw = true; } |
86 | }; |
87 | |
88 | std::unique_ptr<slint::platform::WindowAdapter> EspPlatform::create_window_adapter() |
89 | { |
90 | if (m_window != nullptr) { |
91 | ESP_LOGI(TAG, "FATAL: create_window_adapter called multiple times" ); |
92 | return nullptr; |
93 | } |
94 | |
95 | auto buffer_type = |
96 | buffer2 ? RepaintBufferType::SwappedBuffers : RepaintBufferType::ReusedBuffer; |
97 | auto window = std::make_unique<EspWindowAdapter>(args&: buffer_type, args&: size); |
98 | m_window = window.get(); |
99 | #ifdef SLINT_FEATURE_EXPERIMENTAL |
100 | m_window->m_renderer.set_rendering_rotation(rotation); |
101 | #endif |
102 | return window; |
103 | } |
104 | |
105 | std::chrono::milliseconds EspPlatform::duration_since_start() |
106 | { |
107 | auto ticks = xTaskGetTickCount(); |
108 | return std::chrono::milliseconds(pdTICKS_TO_MS(ticks)); |
109 | } |
110 | |
111 | #if SOC_LCD_RGB_SUPPORTED && ESP_IDF_VERSION_MAJOR >= 5 |
112 | static SemaphoreHandle_t sem_vsync_end; |
113 | static SemaphoreHandle_t sem_gui_ready; |
114 | |
115 | extern "C" bool on_vsync_event(esp_lcd_panel_handle_t panel, |
116 | const esp_lcd_rgb_panel_event_data_t *edata, void *) |
117 | { |
118 | BaseType_t high_task_awoken = pdFALSE; |
119 | if (xSemaphoreTakeFromISR(sem_gui_ready, &high_task_awoken) == pdTRUE) { |
120 | xSemaphoreGiveFromISR(sem_vsync_end, &high_task_awoken); |
121 | } |
122 | return high_task_awoken == pdTRUE; |
123 | } |
124 | #endif |
125 | |
126 | void EspPlatform::run_event_loop() |
127 | { |
128 | task = xTaskGetCurrentTaskHandle(); |
129 | |
130 | esp_lcd_panel_disp_on_off(panel_handle, true); |
131 | |
132 | if (touch_handle) { |
133 | esp_lcd_touch_register_interrupt_callback( |
134 | *touch_handle, [](auto) { vTaskNotifyGiveFromISR(task, nullptr); }); |
135 | } |
136 | #if SOC_LCD_RGB_SUPPORTED && ESP_IDF_VERSION_MAJOR >= 5 |
137 | if (buffer2) { |
138 | sem_vsync_end = xSemaphoreCreateBinary(); |
139 | sem_gui_ready = xSemaphoreCreateBinary(); |
140 | esp_lcd_rgb_panel_event_callbacks_t cbs = {}; |
141 | cbs.on_vsync = on_vsync_event; |
142 | esp_lcd_rgb_panel_register_event_callbacks(panel_handle, &cbs, this); |
143 | } |
144 | #endif |
145 | |
146 | int last_touch_x = 0; |
147 | int last_touch_y = 0; |
148 | bool touch_down = false; |
149 | |
150 | while (true) { |
151 | slint::platform::update_timers_and_animations(); |
152 | |
153 | std::optional<slint::platform::Platform::Task> event; |
154 | { |
155 | std::unique_lock lock(queue_mutex); |
156 | if (queue.empty()) { |
157 | if (quit) { |
158 | quit = false; |
159 | break; |
160 | } |
161 | } else { |
162 | event = std::move(queue.front()); |
163 | queue.pop_front(); |
164 | } |
165 | } |
166 | if (event) { |
167 | std::move(*event).run(); |
168 | event.reset(); |
169 | continue; |
170 | } |
171 | |
172 | if (m_window) { |
173 | |
174 | if (touch_handle) { |
175 | uint16_t touchpad_x[1] = { 0 }; |
176 | uint16_t touchpad_y[1] = { 0 }; |
177 | uint8_t touchpad_cnt = 0; |
178 | |
179 | /* Read touch controller data */ |
180 | esp_lcd_touch_read_data(*touch_handle); |
181 | |
182 | /* Get coordinates */ |
183 | bool touchpad_pressed = esp_lcd_touch_get_coordinates( |
184 | *touch_handle, touchpad_x, touchpad_y, NULL, &touchpad_cnt, 1); |
185 | |
186 | if (touchpad_pressed && touchpad_cnt > 0) { |
187 | // ESP_LOGI(TAG, "x: %i, y: %i", touchpad_x[0], touchpad_y[0]); |
188 | last_touch_x = touchpad_x[0]; |
189 | last_touch_y = touchpad_y[0]; |
190 | m_window->window().dispatch_pointer_move_event( |
191 | pos: slint::LogicalPosition({ .x: float(last_touch_x), .y: float(last_touch_y) })); |
192 | if (!touch_down) { |
193 | m_window->window().dispatch_pointer_press_event( |
194 | pos: slint::LogicalPosition( |
195 | { .x: float(last_touch_x), .y: float(last_touch_y) }), |
196 | button: slint::PointerEventButton::Left); |
197 | } |
198 | touch_down = true; |
199 | } else if (touch_down) { |
200 | m_window->window().dispatch_pointer_release_event( |
201 | pos: slint::LogicalPosition({ .x: float(last_touch_x), .y: float(last_touch_y) }), |
202 | button: slint::PointerEventButton::Left); |
203 | m_window->window().dispatch_pointer_exit_event(); |
204 | touch_down = false; |
205 | } |
206 | } |
207 | |
208 | if (std::exchange(obj&: m_window->needs_redraw, new_val: false)) { |
209 | auto rotated = false |
210 | #ifdef SLINT_FEATURE_EXPERIMENTAL |
211 | || rotation |
212 | == slint::platform::SoftwareRenderer::RenderingRotation::Rotate90 |
213 | || rotation |
214 | == slint::platform::SoftwareRenderer::RenderingRotation::Rotate270 |
215 | #endif |
216 | ; |
217 | auto region = |
218 | m_window->m_renderer.render(buffer: buffer1, pixel_stride: rotated ? size.height : size.width); |
219 | auto o = region.bounding_box_origin(); |
220 | auto s = region.bounding_box_size(); |
221 | if (s.width > 0 && s.height > 0) { |
222 | if (buffer2) { |
223 | #if SOC_LCD_RGB_SUPPORTED && ESP_IDF_VERSION_MAJOR >= 5 |
224 | xSemaphoreGive(sem_gui_ready); |
225 | xSemaphoreTake(sem_vsync_end, portMAX_DELAY); |
226 | #endif |
227 | |
228 | // Assuming that using double buffer means that the buffer comes from the |
229 | // driver and we need to pass the exact pointer. |
230 | // https://github.com/espressif/esp-idf/blob/53ff7d43dbff642d831a937b066ea0735a6aca24/components/esp_lcd/src/esp_lcd_panel_rgb.c#L681 |
231 | esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, size.width, size.height, |
232 | buffer1.data()); |
233 | std::swap(a&: buffer1, b&: buffer2.value()); |
234 | } else { |
235 | for (int y = o.y; y < o.y + s.height; y++) { |
236 | for (int x = o.x; x < o.x + s.width; x++) { |
237 | // Swap endianess to big endian |
238 | auto px = |
239 | reinterpret_cast<uint16_t *>(&buffer1[y * size.width + x]); |
240 | *px = (*px << 8) | (*px >> 8); |
241 | } |
242 | esp_lcd_panel_draw_bitmap(panel_handle, o.x, y, o.x + s.width, y + 1, |
243 | buffer1.data() + y * size.width + o.x); |
244 | } |
245 | } |
246 | } |
247 | } |
248 | |
249 | if (m_window->window().has_active_animations()) { |
250 | continue; |
251 | } |
252 | } |
253 | |
254 | TickType_t ticks_to_wait; |
255 | if (auto wait_time = slint::platform::duration_until_next_timer_update()) { |
256 | ticks_to_wait = pdMS_TO_TICKS(wait_time->count()); |
257 | } else { |
258 | ticks_to_wait = portMAX_DELAY; |
259 | } |
260 | |
261 | ulTaskNotifyTake(/*reset to zero*/ pdTRUE, ticks_to_wait); |
262 | } |
263 | |
264 | vTaskDelete(NULL); |
265 | } |
266 | |
267 | void EspPlatform::quit_event_loop() |
268 | { |
269 | { |
270 | const std::unique_lock lock(queue_mutex); |
271 | quit = true; |
272 | } |
273 | vTaskNotifyGiveFromISR(task, nullptr); |
274 | } |
275 | |
276 | void EspPlatform::run_in_event_loop(slint::platform::Platform::Task event) |
277 | { |
278 | { |
279 | const std::unique_lock lock(queue_mutex); |
280 | queue.push_back(x: std::move(event)); |
281 | } |
282 | vTaskNotifyGiveFromISR(task, nullptr); |
283 | } |
284 | |
285 | TaskHandle_t EspPlatform::task = {}; |
286 | |
287 | void slint_esp_init(slint::PhysicalSize size, esp_lcd_panel_handle_t panel, |
288 | std::optional<esp_lcd_touch_handle_t> touch, |
289 | std::span<slint::platform::Rgb565Pixel> buffer1, |
290 | std::optional<std::span<slint::platform::Rgb565Pixel>> buffer2 |
291 | #ifdef SLINT_FEATURE_EXPERIMENTAL |
292 | , |
293 | slint::platform::SoftwareRenderer::RenderingRotation rotation |
294 | #endif |
295 | ) |
296 | { |
297 | slint::platform::set_platform(std::make_unique<EspPlatform>(size, panel, touch, buffer1, buffer2 |
298 | #ifdef SLINT_FEATURE_EXPERIMENTAL |
299 | , |
300 | rotation |
301 | #endif |
302 | )); |
303 | } |
304 | |