ESPHome 2026.10.0-dev
Loading...
Searching...
No Matches
sdl_esphome.cpp
Go to the documentation of this file.
1#ifdef USE_HOST
2#include "sdl_esphome.h"
4
5#include <cstdlib>
6
7namespace esphome::sdl {
8
9namespace {
10
11// Key under which each window keeps a pointer back to its Sdl instance.
12constexpr const char *const WINDOW_DATA_KEY = "esphome_sdl";
13
14} // namespace
15
27
39
41 // Reverse order of creation: the renderer refers to the window or surface it was made from.
42 if (this->shot_target_ != nullptr) {
43 SDL_DestroyTexture(this->shot_target_);
44 this->shot_target_ = nullptr;
45 }
46 if (this->texture_ != nullptr) {
47 SDL_DestroyTexture(this->texture_);
48 this->texture_ = nullptr;
49 }
50 if (this->renderer_ != nullptr) {
51 SDL_DestroyRenderer(this->renderer_);
52 this->renderer_ = nullptr;
53 }
54 if (this->window_ != nullptr) {
55 SDL_DestroyWindow(this->window_);
56 this->window_ = nullptr;
57 }
58 if (this->surface_ != nullptr) {
59 SDL_FreeSurface(this->surface_);
60 this->surface_ = nullptr;
61 }
62}
63
64bool Sdl::setup_failed_(const char *what) {
65 ESP_LOGE(TAG, "%s: %s", what, SDL_GetError());
66 // Give back whatever was created before the failure. Without this a half set up display leaves an
67 // empty window on screen for the life of the process, still registered as an event target.
68 this->destroy_renderer_();
69 return false;
70}
71
73 SDL_SetMainReady();
74 if (this->headless_) {
75 // SDL_INIT_VIDEO is deliberately not requested: a software renderer bound to a surface needs no
76 // video device, so this works on a machine with no display server at all.
77 if (SDL_Init(0) != 0)
78 return this->setup_failed_("SDL_Init failed");
79 this->surface_ = SDL_CreateRGBSurfaceWithFormat(0, this->width_, this->height_, 16, SDL_PIXELFORMAT_RGB565);
80 if (this->surface_ == nullptr)
81 return this->setup_failed_("Could not create offscreen surface");
82 this->renderer_ = SDL_CreateSoftwareRenderer(this->surface_);
83 } else {
84 if (SDL_Init(SDL_INIT_VIDEO) != 0)
85 return this->setup_failed_("SDL_Init failed");
86 this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_,
87 this->window_options_);
88 if (this->window_ == nullptr)
89 return this->setup_failed_("Could not create window");
90 // Lets loop() find the display an event belongs to, so one display does not act on another's
91 // input when several windows are open.
92 SDL_SetWindowData(this->window_, WINDOW_DATA_KEY, this);
93 this->renderer_ = SDL_CreateRenderer(this->window_, -1, SDL_RENDERER_SOFTWARE);
94 }
95 if (this->renderer_ == nullptr)
96 return this->setup_failed_("Could not create renderer");
97 if (SDL_RenderSetLogicalSize(this->renderer_, this->width_, this->height_) != 0)
98 return this->setup_failed_("Could not set renderer logical size");
99 this->texture_ =
100 SDL_CreateTexture(this->renderer_, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STATIC, this->width_, this->height_);
101 if (this->texture_ == nullptr)
102 return this->setup_failed_("Could not create texture");
103 // The texture has no alpha channel, so blending is pointless. Headless it would also force a
104 // different software blit path onto the 16 bit target surface.
105 if (SDL_SetTextureBlendMode(this->texture_, this->headless_ ? SDL_BLENDMODE_NONE : SDL_BLENDMODE_BLEND) != 0)
106 return this->setup_failed_("Could not set texture blend mode");
107 return true;
108}
109
111 if (!this->setup_renderer_()) {
112 this->mark_failed();
113 return;
114 }
115 if (this->headless_) {
116 // Nothing generates events, so there is nothing for loop() to do.
117 this->disable_loop();
118 } else if (this->snapshot_key_ != 0) {
119 this->add_key_listener(this->snapshot_key_, [this](bool down) {
120 if (down && !this->take_snapshot(nullptr)) {
121 ESP_LOGW(TAG, "snapshot key did not write a file");
122 }
123 });
124 }
125}
126
128 if (this->texture_ == nullptr)
129 return;
130 this->do_update_();
131 if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_))
132 return;
133 SDL_Rect rect{this->x_low_, this->y_low_, this->x_high_ + 1 - this->x_low_, this->y_high_ + 1 - this->y_low_};
134 this->x_low_ = this->width_;
135 this->y_low_ = this->height_;
136 this->x_high_ = 0;
137 this->y_high_ = 0;
138 this->redraw_(rect);
139}
140
141void Sdl::redraw_(SDL_Rect &rect) {
142 // Nothing to present when headless - a snapshot blits the whole texture when it needs it, so
143 // doing it here as well would just burn CPU. draw_pixels_at() calls this on every partial
144 // update, so it is worth skipping.
145 if (this->headless_)
146 return;
147 SDL_RenderCopy(this->renderer_, this->texture_, &rect, &rect);
148 SDL_RenderPresent(this->renderer_);
149}
150
151void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
152 display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
153 if (this->texture_ == nullptr)
154 return;
155 SDL_Rect rect{x_start, y_start, w, h};
156 if (this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || bitness != display::COLOR_BITNESS_565 || big_endian) {
157 Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
158 } else {
159 auto stride = x_offset + w + x_pad;
160 auto data = ptr + (stride * y_offset + x_offset) * 2;
161 SDL_UpdateTexture(this->texture_, &rect, data, stride * 2);
162 }
163 this->redraw_(rect);
164}
165
166void Sdl::draw_pixel_at(int x, int y, Color color) {
167 if (this->texture_ == nullptr || !this->get_clipping().inside(x, y))
168 return;
169
171 x = this->width_ - x - 1;
172 y = this->height_ - y - 1;
174 auto tmp = x;
175 x = this->width_ - y - 1;
176 y = tmp;
178 auto tmp = y;
179 y = this->height_ - x - 1;
180 x = tmp;
181 }
182
183 SDL_Rect rect{x, y, 1, 1};
185 SDL_UpdateTexture(this->texture_, &rect, &data, 2);
187 this->x_low_ = x;
189 this->y_low_ = y;
190 if (x > this->x_high_)
191 this->x_high_ = x;
192 if (y > this->y_high_)
193 this->y_high_ = y;
194}
195
196void Sdl::process_key(uint32_t keycode, bool down) {
197 auto callback = this->key_callbacks_.find(keycode);
198 if (callback != this->key_callbacks_.end())
199 callback->second(down);
200}
201
203 SDL_Window *window = SDL_GetWindowFromID(window_id);
204 if (window == nullptr)
205 return nullptr;
206 return static_cast<Sdl *>(SDL_GetWindowData(window, WINDOW_DATA_KEY));
207}
208
209void Sdl::handle_event_(const SDL_Event &event) {
210 switch (event.type) {
211 case SDL_MOUSEBUTTONDOWN:
212 case SDL_MOUSEBUTTONUP:
213 if (event.button.button == 1) {
214 this->mouse_x = event.button.x;
215 this->mouse_y = event.button.y;
216 this->mouse_down = event.button.state != 0;
217 }
218 break;
219
220 case SDL_MOUSEMOTION:
221 if (event.motion.state & 1) {
222 this->mouse_x = event.motion.x;
223 this->mouse_y = event.motion.y;
224 this->mouse_down = true;
225 } else {
226 this->mouse_down = false;
227 }
228 break;
229
230 case SDL_KEYDOWN:
231 // Ignore auto-repeat, otherwise holding a key floods the listeners.
232 if (event.key.repeat != 0)
233 break;
234 ESP_LOGD(TAG, "keydown %d", event.key.keysym.sym);
235 this->process_key(event.key.keysym.sym, true);
236 break;
237
238 case SDL_KEYUP:
239 ESP_LOGD(TAG, "keyup %d", event.key.keysym.sym);
240 this->process_key(event.key.keysym.sym, false);
241 break;
242
243 case SDL_WINDOWEVENT:
244 switch (event.window.event) {
245 case SDL_WINDOWEVENT_SIZE_CHANGED:
246 case SDL_WINDOWEVENT_EXPOSED:
247 case SDL_WINDOWEVENT_RESIZED: {
248 SDL_Rect rect{0, 0, this->width_, this->height_};
249 this->redraw_(rect);
250 break;
251 }
252 default:
253 break;
254 }
255 break;
256
257 default:
258 break;
259 }
260}
261
262void Sdl::loop() {
263 SDL_Event e;
264 // Take everything that is waiting, not one event per loop. A touch drag produces a burst of
265 // motion events, and consuming them one at a time lets the queue grow without bound, so the
266 // pointer ends up acting on input from further and further in the past. Draining collapses a
267 // burst to the position it ended at, which is the one the user is asking for anyway.
268 while (SDL_PollEvent(&e)) {
269 if (e.type == SDL_QUIT)
270 exit(0);
271
272 // Events carry the window they happened in, so send each one to the display that owns it.
273 uint32_t window_id;
274 switch (e.type) {
275 case SDL_MOUSEBUTTONDOWN:
276 case SDL_MOUSEBUTTONUP:
277 window_id = e.button.windowID;
278 break;
279 case SDL_MOUSEMOTION:
280 window_id = e.motion.windowID;
281 break;
282 case SDL_KEYDOWN:
283 case SDL_KEYUP:
284 window_id = e.key.windowID;
285 break;
286 case SDL_WINDOWEVENT:
287 window_id = e.window.windowID;
288 break;
289 default:
290 // Anything else, including the touch events SDL reports alongside the mouse events it
291 // synthesises from them, is not used here.
292 ESP_LOGV(TAG, "Event %d", e.type);
293 continue;
294 }
295
296 Sdl *target = instance_for_window_(window_id);
297 if (target == nullptr) {
298 // Nothing to route this to: the window has gone, or it is not one of ours. Say so, otherwise
299 // input that stops working leaves no trace at all.
300 ESP_LOGV(TAG, "Event %d for unknown window %u", e.type, window_id);
301 continue;
302 }
303 target->handle_event_(e);
304 }
305}
306
307bool Sdl::capture_bgr(uint8_t *dest, size_t row_stride) {
308 if (this->texture_ == nullptr || this->renderer_ == nullptr) {
309 ESP_LOGE(TAG, "Snapshot requested but SDL is not set up");
310 return false;
311 }
312 if (this->shot_target_ == nullptr) {
313 this->shot_target_ = SDL_CreateTexture(this->renderer_, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_TARGET,
314 this->width_, this->height_);
315 if (this->shot_target_ == nullptr) {
316 ESP_LOGE(TAG, "Could not create capture texture: %s", SDL_GetError());
317 return false;
318 }
319 SDL_SetTextureBlendMode(this->shot_target_, SDL_BLENDMODE_NONE);
320 }
321
322 // Render into an offscreen target first. SDL_RenderReadPixels works in physical output pixels and
323 // ignores the logical size, so reading straight off a resizable window would read more pixels than
324 // there is room for.
325 // Every step is checked: a failed clear or copy would otherwise be read back as a blank or stale
326 // picture, written out, and reported as a snapshot that worked.
327 bool ok = false;
328 if (SDL_SetRenderTarget(this->renderer_, this->shot_target_) == 0) {
329 ok = SDL_SetRenderDrawColor(this->renderer_, 0, 0, 0, SDL_ALPHA_OPAQUE) == 0 &&
330 SDL_RenderClear(this->renderer_) == 0 &&
331 SDL_RenderCopy(this->renderer_, this->texture_, nullptr, nullptr) == 0 &&
332 SDL_RenderReadPixels(this->renderer_, nullptr, SDL_PIXELFORMAT_BGR24, dest, static_cast<int>(row_stride)) == 0;
333 if (SDL_SetRenderTarget(this->renderer_, nullptr) != 0) {
334 // Stuck rendering into shot_target_ from here on, so there's no point continuing.
335 ESP_LOGE(TAG, "Could not restore the render target: %s", SDL_GetError());
336 this->mark_failed();
337 return false;
338 }
339 }
340 if (!ok) {
341 ESP_LOGE(TAG, "Could not capture the screen: %s", SDL_GetError());
342 }
343 return ok;
344}
345
346} // namespace esphome::sdl
347#endif
uint8_t h
Definition bl0906.h:2
const StringRef & get_name() const
Get the name of this Application set by pre_setup().
void mark_failed()
Mark this component as failed.
void disable_loop()
Disable this component's loop.
constexpr const char * c_str() const
Definition string_ref.h:73
static uint16_t color_to_565(Color color, ColorOrder color_order=ColorOrder::COLOR_ORDER_RGB)
Rect get_clipping() const
Get the current the clipping rectangle.
Definition display.cpp:763
DisplayRotation rotation_
Definition display.h:789
void setup() override
int get_width() override
int get_height() override
SDL_Surface * surface_
Definition sdl_esphome.h:76
SDL_Renderer * renderer_
Definition sdl_esphome.h:70
void add_key_listener(int32_t keycode, F &&callback)
Definition sdl_esphome.h:44
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override
int32_t snapshot_key_
Definition sdl_esphome.h:85
static Sdl * instance_for_window_(uint32_t window_id)
The display owning the given window, or nullptr if it is not one of ours.
int get_height_internal() override
Definition sdl_esphome.h:57
void process_key(uint32_t keycode, bool down)
SDL_Texture * texture_
Definition sdl_esphome.h:72
void redraw_(SDL_Rect &rect)
void loop() override
bool setup_failed_(const char *what)
Log an SDL failure during setup, release anything already created, and return false.
void handle_event_(const SDL_Event &event)
bool capture_bgr(uint8_t *dest, size_t row_stride) override
void destroy_renderer_()
Release the window, surface, renderer and textures, and forget them.
void draw_pixel_at(int x, int y, Color color) override
int get_width_internal() override
Definition sdl_esphome.h:56
void update() override
SDL_Texture * shot_target_
Definition sdl_esphome.h:78
SDL_Window * window_
Definition sdl_esphome.h:71
std::map< int32_t, CallbackManager< void(bool)> > key_callbacks_
Definition sdl_esphome.h:79
bool take_snapshot(const char *filename)
Write the current picture to a BMP file in the snapshot directory.
Definition snapshot.cpp:198
@ DISPLAY_ROTATION_0_DEGREES
Definition display.h:134
@ DISPLAY_ROTATION_270_DEGREES
Definition display.h:137
@ DISPLAY_ROTATION_180_DEGREES
Definition display.h:136
@ DISPLAY_ROTATION_90_DEGREES
Definition display.h:135
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
uint16_t x
Definition tt21100.cpp:5
uint16_t y
Definition tt21100.cpp:6