From ecdc6f2adbe20d46ae6e2b7cf05c95ed9bdd369e Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Mon, 30 Jun 2025 11:40:20 -0400 Subject: [PATCH 001/103] wayland: Ensure that the xdg_surface is always configured after creation The spec states that xdg_surface must have seen an initial configure event before attaching a buffer, however, this was only being done when initially showing the window, and not after show->hide->show cycle. Always wait for the initial configure event when (re)creating an xdg_surface as part of the show window sequence. --- src/video/wayland/SDL_waylandwindow.c | 54 ++++++++++++--------------- src/video/wayland/SDL_waylandwindow.h | 2 - 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c index df0898e657..ec7bffe6bb 100644 --- a/src/video/wayland/SDL_waylandwindow.c +++ b/src/video/wayland/SDL_waylandwindow.c @@ -181,7 +181,7 @@ static void SetMinMaxDimensions(SDL_Window *window) #ifdef HAVE_LIBDECOR_H if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) { - if (!wind->shell_surface.libdecor.initial_configure_seen || !wind->shell_surface.libdecor.frame) { + if (!wind->shell_surface.libdecor.frame) { return; // Can't do anything yet, wait for ShowWindow } /* No need to change these values if the window is non-resizable, @@ -198,7 +198,7 @@ static void SetMinMaxDimensions(SDL_Window *window) } else #endif if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_TOPLEVEL) { - if (wind->shell_surface.xdg.toplevel.xdg_toplevel == NULL) { + if (!wind->shell_surface.xdg.toplevel.xdg_toplevel) { return; // Can't do anything yet, wait for ShowWindow } xdg_toplevel_set_min_size(wind->shell_surface.xdg.toplevel.xdg_toplevel, @@ -750,7 +750,9 @@ static void handle_configure_xdg_shell_surface(void *data, struct xdg_surface *x xdg_surface_ack_configure(xdg, serial); } - wind->shell_surface.xdg.initial_configure_seen = true; + if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE) { + wind->shell_surface_status = WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_FRAME; + } } static const struct xdg_surface_listener shell_surface_listener_xdg = { @@ -980,10 +982,6 @@ static void handle_configure_xdg_toplevel(void *data, wind->active = active; window->tiled = tiled; wind->resizing = resizing; - - if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE) { - wind->shell_surface_status = WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_FRAME; - } } static void handle_close_xdg_toplevel(void *data, struct xdg_toplevel *xdg_toplevel) @@ -1132,9 +1130,7 @@ static void handle_configure_zxdg_decoration(void *data, WAYLAND_wl_display_roundtrip(internal->waylandData->display); Wayland_HideWindow(device, window); - SDL_zero(internal->shell_surface); internal->shell_surface_type = WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR; - Wayland_ShowWindow(device, window); } } @@ -1417,11 +1413,8 @@ static void decoration_frame_configure(struct libdecor_frame *frame, libdecor_state_free(state); } - if (!wind->shell_surface.libdecor.initial_configure_seen) { - LibdecorGetMinContentSize(frame, &wind->system_limits.min_width, &wind->system_limits.min_height); - wind->shell_surface.libdecor.initial_configure_seen = true; - } if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE) { + LibdecorGetMinContentSize(frame, &wind->system_limits.min_width, &wind->system_limits.min_height); wind->shell_surface_status = WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_FRAME; } @@ -1830,12 +1823,10 @@ void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window) } } - /* The window was hidden, but the sync point hasn't yet been reached. - * Pump events to avoid a possible protocol violation. - */ - if (data->show_hide_sync_required) { + // Always roundtrip to ensure there are no pending buffer attachments. + do { WAYLAND_wl_display_roundtrip(c->display); - } + } while (data->show_hide_sync_required); data->shell_surface_status = WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE; @@ -1996,7 +1987,7 @@ void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window) #ifdef HAVE_LIBDECOR_H if (data->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) { if (data->shell_surface.libdecor.frame) { - while (!data->shell_surface.libdecor.initial_configure_seen) { + while (data->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE) { WAYLAND_wl_display_flush(c->display); WAYLAND_wl_display_dispatch(c->display); } @@ -2010,7 +2001,7 @@ void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window) */ wl_surface_commit(data->surface); if (data->shell_surface.xdg.surface) { - while (!data->shell_surface.xdg.initial_configure_seen) { + while (data->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE) { WAYLAND_wl_display_flush(c->display); WAYLAND_wl_display_dispatch(c->display); } @@ -2159,26 +2150,27 @@ void Wayland_HideWindow(SDL_VideoDevice *_this, SDL_Window *window) if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_LIBDECOR) { if (wind->shell_surface.libdecor.frame) { libdecor_frame_unref(wind->shell_surface.libdecor.frame); - wind->shell_surface.libdecor.frame = NULL; SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_SURFACE_POINTER, NULL); SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_POINTER, NULL); } } else #endif + { if (wind->shell_surface_type == WAYLAND_SHELL_SURFACE_TYPE_XDG_POPUP) { - Wayland_ReleasePopup(_this, window); - } else if (wind->shell_surface.xdg.toplevel.xdg_toplevel) { - xdg_toplevel_destroy(wind->shell_surface.xdg.toplevel.xdg_toplevel); - wind->shell_surface.xdg.toplevel.xdg_toplevel = NULL; - SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_POINTER, NULL); - } - if (wind->shell_surface.xdg.surface) { - xdg_surface_destroy(wind->shell_surface.xdg.surface); - wind->shell_surface.xdg.surface = NULL; - SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_SURFACE_POINTER, NULL); + Wayland_ReleasePopup(_this, window); + } else if (wind->shell_surface.xdg.toplevel.xdg_toplevel) { + xdg_toplevel_destroy(wind->shell_surface.xdg.toplevel.xdg_toplevel); + SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_POINTER, NULL); + } + + if (wind->shell_surface.xdg.surface) { + xdg_surface_destroy(wind->shell_surface.xdg.surface); + SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_XDG_SURFACE_POINTER, NULL); + } } + SDL_zero(wind->shell_surface); wind->show_hide_sync_required = true; struct wl_callback *cb = wl_display_sync(_this->internal->display); wl_callback_add_listener(cb, &show_hide_sync_listener, (void *)((uintptr_t)window->id)); diff --git a/src/video/wayland/SDL_waylandwindow.h b/src/video/wayland/SDL_waylandwindow.h index 7099462abd..945b753365 100644 --- a/src/video/wayland/SDL_waylandwindow.h +++ b/src/video/wayland/SDL_waylandwindow.h @@ -46,7 +46,6 @@ struct SDL_WindowData struct { struct libdecor_frame *frame; - bool initial_configure_seen; } libdecor; #endif struct @@ -64,7 +63,6 @@ struct SDL_WindowData struct xdg_positioner *xdg_positioner; } popup; }; - bool initial_configure_seen; } xdg; } shell_surface; enum From 343ad3eddda6df6ba2cef1321299e75168e832a1 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Sat, 5 Jul 2025 12:19:21 -0400 Subject: [PATCH 002/103] ngage: SDL_GetPrefPath allows a NULL `org` parameter. Reference Issue #13322. --- src/filesystem/ngage/SDL_sysfilesystem.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/filesystem/ngage/SDL_sysfilesystem.c b/src/filesystem/ngage/SDL_sysfilesystem.c index bc33a2af05..d234bfaafd 100644 --- a/src/filesystem/ngage/SDL_sysfilesystem.c +++ b/src/filesystem/ngage/SDL_sysfilesystem.c @@ -32,11 +32,11 @@ char *SDL_SYS_GetBasePath(void) char *SDL_SYS_GetPrefPath(const char *org, const char *app) { - char *pref_path; - if (SDL_asprintf(&pref_path, "C:/System/Apps/%s/%s/", org, app) < 0) + char *pref_path = NULL; + if (SDL_asprintf(&pref_path, "C:/System/Apps/%s/%s/", org ? org : "SDL_App", app) < 0) { return NULL; - else - return pref_path; + } + return pref_path; } char *SDL_SYS_GetUserFolder(SDL_Folder folder) From b9ab8cf03dd286f3db7ef305fa6a043800fd4d50 Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Fri, 13 Sep 2024 10:41:26 -0400 Subject: [PATCH 003/103] wayland: Add support for the key repeat event (seat v10) The internal key repeat mechanism already disables itself if the key repeat interval is 0, and SDL tracks and handles the flagging of repeated keys itself, so just map the 'repeated' event to 'pressed'. --- src/video/wayland/SDL_waylandevents.c | 10 + src/video/wayland/SDL_waylandvideo.c | 4 +- wayland-protocols/wayland.xml | 292 +++++++++++++++++++------- 3 files changed, 233 insertions(+), 73 deletions(-) diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c index d3c305fb7f..2710928c81 100644 --- a/src/video/wayland/SDL_waylandevents.c +++ b/src/video/wayland/SDL_waylandevents.c @@ -2048,6 +2048,16 @@ static void keyboard_handle_key(void *data, struct wl_keyboard *keyboard, Wayland_UpdateImplicitGrabSerial(seat, serial); + if (state == WL_KEYBOARD_KEY_STATE_REPEATED) { + // If this key shouldn't be repeated, just return. + if (seat->keyboard.xkb.keymap && !WAYLAND_xkb_keymap_key_repeats(seat->keyboard.xkb.keymap, key + 8)) { + return; + } + + // SDL automatically handles key tracking and repeat status, so just map 'repeated' to 'pressed'. + state = WL_KEYBOARD_KEY_STATE_PRESSED; + } + if (seat->keyboard.sdl_keymap != SDL_GetCurrentKeymap(true)) { SDL_SetKeymap(seat->keyboard.sdl_keymap, true); SDL_SetModState(seat->keyboard.pressed_modifiers | seat->keyboard.locked_modifiers); diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c index a1b5276648..adcd00dd04 100644 --- a/src/video/wayland/SDL_waylandvideo.c +++ b/src/video/wayland/SDL_waylandvideo.c @@ -81,7 +81,9 @@ #define SDL_WL_COMPOSITOR_VERSION 4 #endif -#if SDL_WAYLAND_CHECK_VERSION(1, 22, 0) +#if SDL_WAYLAND_CHECK_VERSION(1, 24, 0) +#define SDL_WL_SEAT_VERSION 10 +#elif SDL_WAYLAND_CHECK_VERSION(1, 22, 0) #define SDL_WL_SEAT_VERSION 9 #elif SDL_WAYLAND_CHECK_VERSION(1, 21, 0) #define SDL_WL_SEAT_VERSION 8 diff --git a/wayland-protocols/wayland.xml b/wayland-protocols/wayland.xml index 10e039d6ec..bee74a1008 100644 --- a/wayland-protocols/wayland.xml +++ b/wayland-protocols/wayland.xml @@ -46,7 +46,7 @@ compositor after the callback is fired and as such the client must not attempt to use it after that point. - The callback_data passed in the callback is the event serial. + The callback_data passed in the callback is undefined and should be ignored. @@ -212,7 +212,7 @@ - + The wl_shm_pool object encapsulates a piece of memory shared between the compositor and client. Through the wl_shm_pool @@ -262,17 +262,17 @@ created, but using the new size. This request can only be used to make the pool bigger. - This request only changes the amount of bytes that are mmapped - by the server and does not touch the file corresponding to the - file descriptor passed at creation time. It is the client's - responsibility to ensure that the file is at least as big as - the new pool size. + This request only changes the amount of bytes that are mmapped + by the server and does not touch the file corresponding to the + file descriptor passed at creation time. It is the client's + responsibility to ensure that the file is at least as big as + the new pool size. - + A singleton global object that provides support for shared memory. @@ -419,6 +419,21 @@ + + + + + + + + + + + + + + + @@ -442,6 +457,17 @@ + + + + + + Using this request a client can tell the server that it is not going to + use the shm object anymore. + + Objects created via this interface remain unaffected. + + @@ -453,9 +479,11 @@ client provides and updates the contents is defined by the buffer factory interface. - If the buffer uses a format that has an alpha channel, the alpha channel - is assumed to be premultiplied in the color channels unless otherwise - specified. + Color channels are assumed to be electrical rather than optical (in other + words, encoded with a transfer function) unless otherwise specified. If + the buffer uses a format that has an alpha channel, the alpha channel is + assumed to be premultiplied into the electrical color channel values + (after transfer function encoding) unless otherwise specified. Note, because wl_buffer objects are created from multiple independent factory interfaces, the wl_buffer interface is frozen at version 1. @@ -473,8 +501,10 @@ Sent when this wl_buffer is no longer used by the compositor. - The client is now free to reuse or destroy this buffer and its - backing storage. + + For more information on when release events may or may not be sent, + and what consequences it has, please see the description of + wl_surface.attach. If a client receives a release event before the frame callback requested in the same wl_surface.commit that attaches this @@ -847,6 +877,7 @@ + @@ -868,7 +899,7 @@ The icon surface is an optional (can be NULL) surface that provides an icon to be moved around with the cursor. Initially, the top-left corner of the icon surface is placed at the cursor - hotspot, but subsequent wl_surface.attach request can move the + hotspot, but subsequent wl_surface.offset requests can move the relative position. Attach requests must be confirmed with wl_surface.commit as usual. The icon surface is given the role of a drag-and-drop icon. If the icon surface already has another role, @@ -876,6 +907,10 @@ The input region is ignored for wl_surfaces with the role of a drag-and-drop icon. + + The given source may not be used in any further set_selection or + start_drag requests. Attempting to reuse a previously-used source + may send a used_source error. @@ -889,6 +924,10 @@ to the data from the source on behalf of the client. To unset the selection, set the source to NULL. + + The given source may not be used in any further set_selection or + start_drag requests. Attempting to reuse a previously-used source + may send a used_source error. @@ -1411,7 +1450,7 @@ + summary="surface was destroyed before its role object"/> @@ -1440,9 +1479,9 @@ When the bound wl_surface version is 5 or higher, passing any non-zero x or y is a protocol violation, and will result in an - 'invalid_offset' error being raised. The x and y arguments are ignored - and do not change the pending state. To achieve equivalent semantics, - use wl_surface.offset. + 'invalid_offset' error being raised. The x and y arguments are ignored + and do not change the pending state. To achieve equivalent semantics, + use wl_surface.offset. Surface contents are double-buffered state, see wl_surface.commit. @@ -1467,7 +1506,8 @@ the delivery of wl_buffer.release events becomes undefined. A well behaved client should not rely on wl_buffer.release events in this case. Alternatively, a client could create multiple wl_buffer objects - from the same backing storage or use wp_linux_buffer_release. + from the same backing storage or use a protocol extension providing + per-commit release notifications. Destroying the wl_buffer after wl_buffer.release does not change the surface contents. Destroying the wl_buffer before wl_buffer.release @@ -1479,6 +1519,13 @@ If wl_surface.attach is sent with a NULL wl_buffer, the following wl_surface.commit will remove the surface content. + + If a pending wl_buffer has been destroyed, the result is not specified. + Many compositors are known to remove the surface content on the following + wl_surface.commit, but this behaviour is not universal. Clients seeking to + maximise compatibility should not destroy pending buffers and should + ensure that they explicitly remove content from surfaces, even after + destroying buffers. @@ -1618,16 +1665,18 @@ Surface state (input, opaque, and damage regions, attached buffers, etc.) is double-buffered. Protocol requests modify the pending state, - as opposed to the current state in use by the compositor. A commit - request atomically applies all pending state, replacing the current - state. After commit, the new pending state is as documented for each - related request. + as opposed to the active state in use by the compositor. - On commit, a pending wl_buffer is applied first, and all other state - second. This means that all coordinates in double-buffered state are - relative to the new wl_buffer coming into use, except for - wl_surface.attach itself. If there is no pending wl_buffer, the - coordinates are relative to the current surface contents. + A commit request atomically creates a content update from the pending + state, even if the pending state has not been touched. The content + update is placed in a queue until it becomes active. After commit, the + new pending state is as documented for each related request. + + When the content update is applied, the wl_buffer is applied before all + other state. This means that all coordinates in double-buffered state + are relative to the newly attached wl_buffers, except for + wl_surface.attach itself. If there is no newly attached wl_buffer, the + coordinates are relative to the previous content update. All requests that need a commit to become effective are documented to affect double-buffered state. @@ -1666,10 +1715,12 @@ - This request sets an optional transformation on how the compositor - interprets the contents of the buffer attached to the surface. The - accepted values for the transform parameter are the values for - wl_output.transform. + This request sets the transformation that the client has already applied + to the content of the buffer. The accepted values for the transform + parameter are the values for wl_output.transform. + + The compositor applies the inverse of this transformation whenever it + uses the buffer contents. Buffer transform is double-buffered state, see wl_surface.commit. @@ -1725,11 +1776,11 @@ a buffer that is larger (by a factor of scale in each dimension) than the desired surface size. - If scale is not positive the invalid_scale protocol error is + If scale is not greater than 0 the invalid_scale protocol error is raised. + summary="scale for interpreting buffer contents"/> @@ -1784,6 +1835,9 @@ x and y, combined with the new surface size define in which directions the surface's size changes. + The exact semantics of wl_surface.offset are role-specific. Refer to + the documentation of specific roles for more information. + Surface location offset is double-buffered state, see wl_surface.commit. @@ -1802,10 +1856,15 @@ This event indicates the preferred buffer scale for this surface. It is sent whenever the compositor's preference changes. + Before receiving this event the preferred buffer scale for this surface + is 1. + It is intended that scaling aware clients use this event to scale their content and use wl_surface.set_buffer_scale to indicate the scale they have rendered with. This allows clients to supply a higher detail buffer. + + The compositor shall emit a scale value greater than 0. @@ -1815,16 +1874,19 @@ This event indicates the preferred buffer transform for this surface. It is sent whenever the compositor's preference changes. - It is intended that transform aware clients use this event to apply the - transform to their content and use wl_surface.set_buffer_transform to - indicate the transform they have rendered with. + Before receiving this event the preferred buffer transform for this + surface is normal. + + Applying this transformation to the surface buffer contents and using + wl_surface.set_buffer_transform might allow the compositor to use the + surface buffer more efficiently. - + A seat is a group of keyboards, pointer and touch devices. This object is published as a global during start up, or when such a @@ -1852,9 +1914,10 @@ - This is emitted whenever a seat gains or loses the pointer, - keyboard or touch capabilities. The argument is a capability - enum containing the complete set of capabilities this seat has. + This is sent on binding to the seat global or whenever a seat gains + or loses the pointer, keyboard or touch capabilities. + The argument is a capability enum containing the complete set of + capabilities this seat has. When the pointer capability is added, a client may create a wl_pointer object using the wl_seat.get_pointer request. This object @@ -1936,9 +1999,9 @@ The same seat names are used for all clients. Thus, the name can be shared across processes to refer to a specific wl_seat global. - The name event is sent after binding to the seat global. This event is - only sent once per seat object, and the name does not change over the - lifetime of the wl_seat global. + The name event is sent after binding to the seat global, and should be sent + before announcing capabilities. This event only sent once per seat object, + and the name does not change over the lifetime of the wl_seat global. Compositors may re-use the same seat name if the wl_seat global is destroyed and re-created later. @@ -1957,7 +2020,7 @@ - + The wl_pointer interface represents one or more input devices, such as mice, which control the pointer location and pointer_focus @@ -1992,9 +2055,9 @@ where (x, y) are the coordinates of the pointer location, in surface-local coordinates. - On surface.attach requests to the pointer surface, hotspot_x + On wl_surface.offset requests to the pointer surface, hotspot_x and hotspot_y are decremented by the x and y parameters - passed to the request. Attach must be confirmed by + passed to the request. The offset must be applied by wl_surface.commit as usual. The hotspot can also be updated by passing the currently set @@ -2248,7 +2311,7 @@ - + Discrete step information for scroll and other axes. @@ -2370,10 +2433,20 @@ - + The wl_keyboard interface represents one or more keyboards associated with a seat. + + Each wl_keyboard has the following logical state: + + - an active surface (possibly null), + - the keys currently logically down, + - the active modifiers, + - the active group. + + By default, the active surface is null, the keys currently logically down + are empty, the active modifiers and the active group are 0. @@ -2408,10 +2481,18 @@ The compositor must send the wl_keyboard.modifiers event after this event. + + In the wl_keyboard logical state, this event sets the active surface to + the surface argument and the keys currently logically down to the keys + in the keys argument. The compositor must not send this event if the + wl_keyboard already had an active surface immediately before this event. + + Clients should not use the list of pressed keys to emulate key-press + events. The order of keys in the list is unspecified. - + @@ -2422,8 +2503,10 @@ The leave notification is sent before the enter notification for the new focus. - After this event client must assume that all keys, including modifiers, - are lifted and also it must stop key repeating if there's some going on. + In the wl_keyboard logical state, this event resets all values to their + defaults. The compositor must not send this event if the active surface + of the wl_keyboard was not equal to the surface argument immediately + before this event. @@ -2432,9 +2515,18 @@ Describes the physical state of a key that produced the key event. + + Since version 10, the key can be in a "repeated" pseudo-state which + means the same as "pressed", but is used to signal repetition in the + key event. + + The key may only enter the repeated state after entering the pressed + state and before entering the released state. This event may be + generated multiple times while the key is down. + @@ -2448,6 +2540,20 @@ If this event produces a change in modifiers, then the resulting wl_keyboard.modifiers event must be sent after this event. + + In the wl_keyboard logical state, this event adds the key to the keys + currently logically down (if the state argument is pressed) or removes + the key from the keys currently logically down (if the state argument is + released). The compositor must not send this event if the wl_keyboard + did not have an active surface immediately before this event. The + compositor must not send this event if state is pressed (resp. released) + and the key was already logically down (resp. was not logically down) + immediately before this event. + + Since version 10, compositors may send key events with the "repeated" + key state when a wl_keyboard.repeat_info event with a rate argument of + 0 has been received. This allows the compositor to take over the + responsibility of key repetition. @@ -2459,6 +2565,17 @@ Notifies clients that the modifier and/or group state has changed, and it should update its local state. + + The compositor may send this event without a surface of the client + having keyboard focus, for example to tie modifier information to + pointer focus instead. If a modifier event with pressed modifiers is sent + without a prior enter event, the client can assume the modifier state is + valid until it receives the next wl_keyboard.modifiers event. In order to + reset the modifier state again, the compositor can send a + wl_keyboard.modifiers event with no pressed modifiers. + + In the wl_keyboard logical state, this event updates the modifiers and + group. @@ -2497,7 +2614,7 @@ - + The wl_touch interface represents a touchscreen associated with a seat. @@ -2566,6 +2683,8 @@ currently active on this client's surface. The client is responsible for finalizing the touch points, future touch points on this surface may reuse the touch point ID. + + No frame event is required after the cancel event. @@ -2665,10 +2784,9 @@ - - This describes the transform that a compositor will apply to a - surface to compensate for the rotation or mirroring of an - output device. + + This describes transformations that clients and compositors apply to + buffer contents. The flipped values correspond to an initial flip around a vertical axis followed by rotation. @@ -2700,6 +2818,10 @@ The geometry event will be followed by a done event (starting from version 2). + Clients should use wl_surface.preferred_buffer_transform instead of the + transform advertised by this event to find the preferred buffer + transform to use for a surface. + Note: wl_output only advertises partial information about the output position and identification. Some compositors, for instance those not implementing a desktop-style output layout or those exposing virtual @@ -2722,7 +2844,7 @@ + summary="additional transformation applied to buffer contents during presentation"/> @@ -2795,8 +2917,9 @@ This event contains scaling geometry information that is not in the geometry event. It may be sent after binding the output object or if the output scale changes - later. If it is not sent, the client should assume a - scale of 1. + later. The compositor will emit a non-zero, positive + value for scale. If it is not sent, the client should + assume a scale of 1. A scale larger than 1 means that the compositor will automatically scale surface buffers by this amount @@ -2804,12 +2927,9 @@ displays where applications rendering at the native resolution would be too small to be legible. - It is intended that scaling aware clients track the - current output of a surface, and if it is on a scaled - output it should use wl_surface.set_buffer_scale with - the scale of the output. That way the compositor can - avoid scaling the surface, and the client can supply - a higher detail image. + Clients should use wl_surface.preferred_buffer_scale + instead of this event to find the preferred buffer + scale to use for a surface. The scale event will be followed by a done event. @@ -3035,6 +3155,11 @@ If the parent wl_surface object is destroyed, the sub-surface is unmapped. + + A sub-surface never has the keyboard focus of any seat. + + The wl_surface.offset request is ignored: clients must use set_position + instead to move the sub-surface. @@ -3060,9 +3185,7 @@ surface area. Negative values are allowed. The scheduled coordinates will take effect whenever the state of the - parent surface is applied. When this happens depends on whether the - parent surface is in synchronized mode or not. See - wl_subsurface.set_sync and wl_subsurface.set_desync for details. + parent surface is applied. If more than one set_position request is invoked by the client before the commit of the parent surface, the position of a new request always @@ -3085,9 +3208,7 @@ The z-order is double-buffered. Requests are handled in order and applied immediately to a pending state. The final pending state is copied to the active state the next time the state of the parent - surface is applied. When this happens depends on whether the parent - surface is in synchronized mode or not. See wl_subsurface.set_sync and - wl_subsurface.set_desync for details. + surface is applied. A new sub-surface is initially added as the top-most in the stack of its siblings and parent. @@ -3148,4 +3269,31 @@ + + + This global fixes problems with other core-protocol interfaces that + cannot be fixed in these interfaces themselves. + + + + + + + + + This request destroys a wl_registry object. + + The client should no longer use the wl_registry after making this + request. + + The compositor will emit a wl_display.delete_id event with the object ID + of the registry and will no longer emit any events on the registry. The + client should re-use the object ID once it receives the + wl_display.delete_id event. + + + + + From 45fa9dba16373defa29f5e07ef8d85a1829b986e Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Sat, 7 Dec 2024 13:28:01 -0500 Subject: [PATCH 004/103] wayland: Use wl_fixes for registry destruction --- src/video/wayland/SDL_waylandvideo.c | 28 ++++++++++++++++++++++++++++ src/video/wayland/SDL_waylandvideo.h | 1 + 2 files changed, 29 insertions(+) diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c index adcd00dd04..2986d374de 100644 --- a/src/video/wayland/SDL_waylandvideo.c +++ b/src/video/wayland/SDL_waylandvideo.c @@ -97,6 +97,14 @@ #define SDL_WL_OUTPUT_VERSION 3 #endif +// The SDL wayland-client minimum is 1.18, which supports version 3. +#define SDL_WL_DATA_DEVICE_VERSION 3 + +// wl_fixes was introduced in 1.24.0 +#if SDL_WAYLAND_CHECK_VERSION(1, 24, 0) +#define SDL_WL_FIXES_VERSION 1 +#endif + #ifdef SDL_USE_LIBDBUS #include "../../core/linux/SDL_dbus.h" @@ -458,6 +466,7 @@ static void Wayland_DeleteDevice(SDL_VideoDevice *device) typedef struct { bool has_fifo_v1; + struct wl_fixes *wl_fixes; } SDL_WaylandPreferredData; static void wayland_preferred_check_handle_global(void *data, struct wl_registry *registry, uint32_t id, @@ -468,6 +477,11 @@ static void wayland_preferred_check_handle_global(void *data, struct wl_registry if (SDL_strcmp(interface, "wp_fifo_manager_v1") == 0) { d->has_fifo_v1 = true; } +#ifdef SDL_WL_FIXES_VERSION + else if (SDL_strcmp(interface, "wl_fixes") == 0) { + d->wl_fixes = wl_registry_bind(registry, id, &wl_fixes_interface, SDL_min(SDL_WL_FIXES_VERSION, version)); + } +#endif } static void wayland_preferred_check_remove_global(void *data, struct wl_registry *registry, uint32_t id) @@ -494,6 +508,10 @@ static bool Wayland_IsPreferred(struct wl_display *display) WAYLAND_wl_display_roundtrip(display); + if (preferred_data.wl_fixes) { + wl_fixes_destroy_registry(preferred_data.wl_fixes, registry); + wl_fixes_destroy(preferred_data.wl_fixes); + } wl_registry_destroy(registry); if (!preferred_data.has_fifo_v1) { @@ -1317,6 +1335,11 @@ static void display_handle_global(void *data, struct wl_registry *registry, uint } else if (SDL_strcmp(interface, "wp_pointer_warp_v1") == 0) { d->wp_pointer_warp_v1 = wl_registry_bind(d->registry, id, &wp_pointer_warp_v1_interface, 1); } +#ifdef SDL_WL_FIXES_VERSION + else if (SDL_strcmp(interface, "wl_fixes") == 0) { + d->wl_fixes = wl_registry_bind(d->registry, id, &wl_fixes_interface, SDL_min(SDL_WL_FIXES_VERSION, version)); + } +#endif } static void display_remove_global(void *data, struct wl_registry *registry, uint32_t id) @@ -1636,6 +1659,11 @@ static void Wayland_VideoCleanup(SDL_VideoDevice *_this) } if (data->registry) { + if (data->wl_fixes) { + wl_fixes_destroy_registry(data->wl_fixes, data->registry); + wl_fixes_destroy(data->wl_fixes); + data->wl_fixes = NULL; + } wl_registry_destroy(data->registry); data->registry = NULL; } diff --git a/src/video/wayland/SDL_waylandvideo.h b/src/video/wayland/SDL_waylandvideo.h index e964731137..837df3e4a4 100644 --- a/src/video/wayland/SDL_waylandvideo.h +++ b/src/video/wayland/SDL_waylandvideo.h @@ -85,6 +85,7 @@ struct SDL_VideoData struct frog_color_management_factory_v1 *frog_color_management_factory_v1; struct wp_color_manager_v1 *wp_color_manager_v1; struct zwp_tablet_manager_v2 *tablet_manager; + struct wl_fixes *wl_fixes; struct xkb_context *xkb_context; From 03fcbb4e46f321c5103ba6057219fb8022d40c45 Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Wed, 28 May 2025 12:37:59 -0400 Subject: [PATCH 005/103] wayland/events: Names will always be sent before devices and capabilities Wayland previously didn't specify that the seat name preceded the capabilities, but it is now specified that the name event must always come first. Remove the 'SDL_SetName()' functions that were only added to accommodate the case of compositors sending the name after the seat capabilities, as this clarification means that they are no longer needed. --- src/events/SDL_keyboard.c | 13 ------------- src/events/SDL_keyboard_c.h | 3 --- src/events/SDL_mouse.c | 13 ------------- src/events/SDL_mouse_c.h | 3 --- src/events/SDL_touch.c | 10 ---------- src/events/SDL_touch_c.h | 3 --- src/video/wayland/SDL_waylandevents.c | 16 ---------------- 7 files changed, 61 deletions(-) diff --git a/src/events/SDL_keyboard.c b/src/events/SDL_keyboard.c index fc06f07258..e281f5daaa 100644 --- a/src/events/SDL_keyboard.c +++ b/src/events/SDL_keyboard.c @@ -172,19 +172,6 @@ void SDL_RemoveKeyboard(SDL_KeyboardID keyboardID, bool send_event) } } -void SDL_SetKeyboardName(SDL_KeyboardID keyboardID, const char *name) -{ - SDL_assert(keyboardID != 0); - - const int keyboard_index = SDL_GetKeyboardIndex(keyboardID); - - if (keyboard_index >= 0) { - SDL_KeyboardInstance *instance = &SDL_keyboards[keyboard_index]; - SDL_free(instance->name); - instance->name = SDL_strdup(name ? name : ""); - } -} - bool SDL_HasKeyboard(void) { return (SDL_keyboard_count > 0); diff --git a/src/events/SDL_keyboard_c.h b/src/events/SDL_keyboard_c.h index 9a6589ddb2..ddfb5c5747 100644 --- a/src/events/SDL_keyboard_c.h +++ b/src/events/SDL_keyboard_c.h @@ -43,9 +43,6 @@ extern void SDL_AddKeyboard(SDL_KeyboardID keyboardID, const char *name, bool se // A keyboard has been removed from the system extern void SDL_RemoveKeyboard(SDL_KeyboardID keyboardID, bool send_event); -// Set or update the name of a keyboard instance. -extern void SDL_SetKeyboardName(SDL_KeyboardID keyboardID, const char *name); - // Set the mapping of scancode to key codes extern void SDL_SetKeymap(SDL_Keymap *keymap, bool send_event); diff --git a/src/events/SDL_mouse.c b/src/events/SDL_mouse.c index 5838d47cf8..54611bf36d 100644 --- a/src/events/SDL_mouse.c +++ b/src/events/SDL_mouse.c @@ -413,19 +413,6 @@ void SDL_RemoveMouse(SDL_MouseID mouseID, bool send_event) } } -void SDL_SetMouseName(SDL_MouseID mouseID, const char *name) -{ - SDL_assert(mouseID != 0); - - const int mouse_index = SDL_GetMouseIndex(mouseID); - - if (mouse_index >= 0) { - SDL_MouseInstance *instance = &SDL_mice[mouse_index]; - SDL_free(instance->name); - instance->name = SDL_strdup(name ? name : ""); - } -} - bool SDL_HasMouse(void) { return (SDL_mouse_count > 0); diff --git a/src/events/SDL_mouse_c.h b/src/events/SDL_mouse_c.h index 0bc913b94e..c25974ba82 100644 --- a/src/events/SDL_mouse_c.h +++ b/src/events/SDL_mouse_c.h @@ -169,9 +169,6 @@ extern void SDL_AddMouse(SDL_MouseID mouseID, const char *name, bool send_event) // A mouse has been removed from the system extern void SDL_RemoveMouse(SDL_MouseID mouseID, bool send_event); -// Set or update the name of a mouse instance. -extern void SDL_SetMouseName(SDL_MouseID mouseID, const char *name); - // Get the mouse state structure extern SDL_Mouse *SDL_GetMouse(void); diff --git a/src/events/SDL_touch.c b/src/events/SDL_touch.c index bf3cc02354..e825117c82 100644 --- a/src/events/SDL_touch.c +++ b/src/events/SDL_touch.c @@ -205,16 +205,6 @@ int SDL_AddTouch(SDL_TouchID touchID, SDL_TouchDeviceType type, const char *name return index; } -// Set or update the name of a touch. -void SDL_SetTouchName(SDL_TouchID id, const char *name) -{ - SDL_Touch *touch = SDL_GetTouch(id); - if (touch) { - SDL_free(touch->name); - touch->name = SDL_strdup(name ? name : ""); - } -} - static bool SDL_AddFinger(SDL_Touch *touch, SDL_FingerID fingerid, float x, float y, float pressure) { SDL_Finger *finger; diff --git a/src/events/SDL_touch_c.h b/src/events/SDL_touch_c.h index e46ba68197..db2d64b85f 100644 --- a/src/events/SDL_touch_c.h +++ b/src/events/SDL_touch_c.h @@ -42,9 +42,6 @@ extern bool SDL_TouchDevicesAvailable(void); // Add a touch, returning the index of the touch, or -1 if there was an error. extern int SDL_AddTouch(SDL_TouchID id, SDL_TouchDeviceType type, const char *name); -// Set or update the name of a touch. -extern void SDL_SetTouchName(SDL_TouchID id, const char *name); - // Get the touch with a given id extern SDL_Touch *SDL_GetTouch(SDL_TouchID id); diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c index 2710928c81..5f640321eb 100644 --- a/src/video/wayland/SDL_waylandevents.c +++ b/src/video/wayland/SDL_waylandevents.c @@ -2352,25 +2352,9 @@ static void seat_handle_capabilities(void *data, struct wl_seat *wl_seat, enum w static void seat_handle_name(void *data, struct wl_seat *wl_seat, const char *name) { SDL_WaylandSeat *seat = (SDL_WaylandSeat *)data; - char name_fmt[256]; if (name && *name != '\0') { seat->name = SDL_strdup(name); - - if (seat->keyboard.wl_keyboard) { - SDL_snprintf(name_fmt, sizeof(name_fmt), "%s (%s)", WAYLAND_DEFAULT_KEYBOARD_NAME, seat->name); - SDL_SetKeyboardName(seat->keyboard.sdl_id, name_fmt); - } - - if (seat->pointer.wl_pointer) { - SDL_snprintf(name_fmt, sizeof(name_fmt), "%s (%s)", WAYLAND_DEFAULT_POINTER_NAME, seat->name); - SDL_SetMouseName(seat->pointer.sdl_id, name_fmt); - } - - if (seat->touch.wl_touch) { - SDL_snprintf(name_fmt, sizeof(name_fmt), "%s (%s)", WAYLAND_DEFAULT_TOUCH_NAME, seat->name); - SDL_SetTouchName((SDL_TouchID)(uintptr_t)seat->touch.wl_touch, name_fmt); - } } } From ef4b7489ffdda21cc7e9ae9474f543a3421c3d5b Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Wed, 28 May 2025 13:02:27 -0400 Subject: [PATCH 006/103] wayland: Use wl_shm_release when available --- src/video/wayland/SDL_waylandvideo.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c index 2986d374de..e9a97a98f1 100644 --- a/src/video/wayland/SDL_waylandvideo.c +++ b/src/video/wayland/SDL_waylandvideo.c @@ -74,7 +74,7 @@ #define WAYLANDVID_DRIVER_NAME "wayland" -// Clamp certain core protocol versions on older versions of libwayland. +// Clamp core protocol versions on older versions of libwayland. #if SDL_WAYLAND_CHECK_VERSION(1, 22, 0) #define SDL_WL_COMPOSITOR_VERSION 6 #else @@ -97,7 +97,13 @@ #define SDL_WL_OUTPUT_VERSION 3 #endif -// The SDL wayland-client minimum is 1.18, which supports version 3. +#if SDL_WAYLAND_CHECK_VERSION(1, 24, 0) +#define SDL_WL_SHM_VERSION 2 +#else +#define SDL_WL_SHM_VERSION 1 +#endif + +// The SDL libwayland-client minimum is 1.18, which supports version 3. #define SDL_WL_DATA_DEVICE_VERSION 3 // wl_fixes was introduced in 1.24.0 @@ -1281,7 +1287,7 @@ static void display_handle_global(void *data, struct wl_registry *registry, uint d->shell.xdg = wl_registry_bind(d->registry, id, &xdg_wm_base_interface, SDL_min(version, 7)); xdg_wm_base_add_listener(d->shell.xdg, &shell_listener_xdg, NULL); } else if (SDL_strcmp(interface, "wl_shm") == 0) { - d->shm = wl_registry_bind(registry, id, &wl_shm_interface, 1); + d->shm = wl_registry_bind(registry, id, &wl_shm_interface, SDL_min(SDL_WL_SHM_VERSION, version)); } else if (SDL_strcmp(interface, "zwp_relative_pointer_manager_v1") == 0) { d->relative_pointer_manager = wl_registry_bind(d->registry, id, &zwp_relative_pointer_manager_v1_interface, 1); } else if (SDL_strcmp(interface, "zwp_pointer_constraints_v1") == 0) { @@ -1574,7 +1580,11 @@ static void Wayland_VideoCleanup(SDL_VideoDevice *_this) } if (data->shm) { - wl_shm_destroy(data->shm); + if (wl_shm_get_version(data->shm) >= WL_SHM_RELEASE_SINCE_VERSION) { + wl_shm_release(data->shm); + } else { + wl_shm_destroy(data->shm); + } data->shm = NULL; } From 63867813516961dd724d88582997588c778ca38f Mon Sep 17 00:00:00 2001 From: Aleksey Sakovets Date: Mon, 7 Jul 2025 21:56:07 +0300 Subject: [PATCH 007/103] README-macos.md: replace old API calls --- docs/README-macos.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/README-macos.md b/docs/README-macos.md index fb0639b66e..e5c75c1c81 100644 --- a/docs/README-macos.md +++ b/docs/README-macos.md @@ -49,7 +49,7 @@ NSApplicationDelegate implementation: ```objc - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { - if (SDL_GetEventState(SDL_EVENT_QUIT) == SDL_ENABLE) { + if (SDL_EventEnabled(SDL_EVENT_QUIT)) { SDL_Event event; SDL_zero(event); event.type = SDL_EVENT_QUIT; @@ -61,7 +61,7 @@ NSApplicationDelegate implementation: - (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename { - if (SDL_GetEventState(SDL_EVENT_DROP_FILE) == SDL_ENABLE) { + if (SDL_EventEnabled(SDL_EVENT_DROP_FILE)) { SDL_Event event; SDL_zero(event); event.type = SDL_EVENT_DROP_FILE; From fb0e03f262c74f025b19f9748fa720e83f04a179 Mon Sep 17 00:00:00 2001 From: Ozkan Sezer Date: Tue, 8 Jul 2025 06:47:47 +0300 Subject: [PATCH 008/103] fix ARM64 linkage with Visual Studio >= 17.14 when SDL_LIBC is disabled Reference issue: https://github.com/libsdl-org/SDL/issues/13254 (cherry picked from commit 2fb6abb9ad27cd0948a47fd53ced0ab138d7d12a) --- CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index f66b772605..8898be36a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -654,6 +654,11 @@ if(MSVC) # Mark SDL3.dll as compatible with Control-flow Enforcement Technology (CET) sdl_shared_link_options("-CETCOMPAT") endif() + + # for VS >= 17.14 targeting ARM64: inline the Interlocked funcs + if(MSVC_VERSION GREATER 1943 AND SDL_CPU_ARM64 AND NOT SDL_LIBC) + sdl_compile_options(PRIVATE "/forceInterlockedFunctions-") + endif() endif() if(CMAKE_C_COMPILER_ID STREQUAL "MSVC") From c64518f3004cff13f15280a8eef6a83a60321741 Mon Sep 17 00:00:00 2001 From: Wouter Wijsman Date: Mon, 7 Jul 2025 19:07:33 +0200 Subject: [PATCH 009/103] PSP: Truncate thread name when passing to sceKernelCreateThread --- src/thread/psp/SDL_systhread.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/thread/psp/SDL_systhread.c b/src/thread/psp/SDL_systhread.c index 3d6071893b..2c500cb8ad 100644 --- a/src/thread/psp/SDL_systhread.c +++ b/src/thread/psp/SDL_systhread.c @@ -32,6 +32,8 @@ #include #include +#define PSP_THREAD_NAME_MAX 32 + static int ThreadEntry(SceSize args, void *argp) { SDL_RunThread(*(SDL_Thread **)argp); @@ -44,6 +46,7 @@ bool SDL_SYS_CreateThread(SDL_Thread *thread, { SceKernelThreadInfo status; int priority = 32; + char thread_name[PSP_THREAD_NAME_MAX]; // Set priority of new thread to the same as the current thread status.size = sizeof(SceKernelThreadInfo); @@ -51,7 +54,12 @@ bool SDL_SYS_CreateThread(SDL_Thread *thread, priority = status.currentPriority; } - thread->handle = sceKernelCreateThread(thread->name, ThreadEntry, + SDL_strlcpy(thread_name, "SDL thread", PSP_THREAD_NAME_MAX); + if (thread->name) { + SDL_strlcpy(thread_name, thread->name, PSP_THREAD_NAME_MAX); + } + + thread->handle = sceKernelCreateThread(thread_name, ThreadEntry, priority, thread->stacksize ? ((int)thread->stacksize) : 0x8000, PSP_THREAD_ATTR_VFPU, NULL); if (thread->handle < 0) { From 11ec0c7a8f5ccbfaffed5152dfad49b1335786ca Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Wed, 9 Jul 2025 12:38:16 -0400 Subject: [PATCH 010/103] hashtable: Fix documentation typos --- src/SDL_hashtable.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/SDL_hashtable.h b/src/SDL_hashtable.h index 598a6d6be1..7a5957bc9a 100644 --- a/src/SDL_hashtable.h +++ b/src/SDL_hashtable.h @@ -219,10 +219,10 @@ typedef void (SDLCALL *SDL_HashDestroyCallback)(void *userdata, const void *key, * \returns true to keep iterating, false to stop iteration. * * \threadsafety A read lock is held during iteration, so other threads can - * still access the the hash table, but threads attempting to - * make changes will be blocked until iteration completes. If - * this is a concern, do as little in the callback as possible - * and finish iteration quickly. + * still access the hash table, but threads attempting to make + * changes will be blocked until iteration completes. If this + * is a concern, do as little in the callback as possible and + * finish iteration quickly. * * \since This datatype is available since SDL 3.4.0. * @@ -248,7 +248,7 @@ typedef bool (SDLCALL *SDL_HashTableIterateCallback)(void *userdata, const SDL_H * * You can specify an estimate of the number of items expected to be stored * in the table, which can help make the table run more efficiently. The table - * will preallocate resources to accomodate this number of items, which is + * will preallocate resources to accommodate this number of items, which is * most useful if you intend to fill the table with a lot of data right after * creating it. Otherwise, it might make more sense to specify the _minimum_ * you expect the table to hold and let it grow as necessary from there. This @@ -422,7 +422,7 @@ extern bool SDL_HashTableEmpty(SDL_HashTable *table); * \param table the hash table to iterate. * \param callback the function pointer to call for each value. * \param userdata a pointer that is passed to `callback`. - * \returns true if iteration happened, false if not (bogus parameter, etc). + * \returns true if iteration happened, false if not (bogus parameter, etc.). * * \since This function is available since SDL 3.4.0. */ From cfb8e591cb99068a39f834900863610ba3780553 Mon Sep 17 00:00:00 2001 From: Anonymous Maarten Date: Thu, 10 Jul 2025 22:52:56 +0200 Subject: [PATCH 011/103] cmake: remove /RTC1 from CXX flags when building with SDL_LIBC=OFF --- CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8898be36a2..b891873c65 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -189,9 +189,12 @@ if(MSVC) # Make sure /RTC1 is disabled, otherwise it will use functions from the CRT foreach(flag_var CMAKE_C_FLAGS CMAKE_C_FLAGS_DEBUG CMAKE_C_FLAGS_RELEASE - CMAKE_C_FLAGS_MINSIZEREL CMAKE_C_FLAGS_RELWITHDEBINFO) + CMAKE_C_FLAGS_MINSIZEREL CMAKE_C_FLAGS_RELWITHDEBINFO + CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE + CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELWITHDEBINFO) string(REGEX REPLACE "/RTC(su|[1su])" "" ${flag_var} "${${flag_var}}") endforeach(flag_var) + set(CMAKE_MSVC_RUNTIME_CHECKS "") endif() if(MSVC_CLANG) From f286558baef3aa9e54a491d744935c8603a90194 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Fri, 11 Jul 2025 12:29:12 -0400 Subject: [PATCH 012/103] windows: Use wglSwapLayerBuffers if available. It apparently works better (or can work better?) on multimonitor setups than SwapBuffers. This should be available back to Windows 95, but just in case, it falls back to standard SwapBuffers if not available. Fixes #13269. --- src/video/windows/SDL_windowsopengl.c | 13 +++++++++++-- src/video/windows/SDL_windowsopengl.h | 2 ++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/video/windows/SDL_windowsopengl.c b/src/video/windows/SDL_windowsopengl.c index c458796044..4588e2531c 100644 --- a/src/video/windows/SDL_windowsopengl.c +++ b/src/video/windows/SDL_windowsopengl.c @@ -141,6 +141,9 @@ bool WIN_GL_LoadLibrary(SDL_VideoDevice *_this, const char *path) SDL_LoadFunction(handle, "wglMakeCurrent"); _this->gl_data->wglShareLists = (BOOL (WINAPI *)(HGLRC, HGLRC)) SDL_LoadFunction(handle, "wglShareLists"); + _this->gl_data->wglSwapLayerBuffers = (BOOL (WINAPI *)(HDC, UINT)) + SDL_LoadFunction(handle, "wglSwapLayerBuffers"); + /* *INDENT-ON* */ // clang-format on #if defined(SDL_PLATFORM_XBOXONE) || defined(SDL_PLATFORM_XBOXSERIES) @@ -886,8 +889,14 @@ bool WIN_GL_SwapWindow(SDL_VideoDevice *_this, SDL_Window *window) { HDC hdc = window->internal->hdc; - if (!SwapBuffers(hdc)) { - return WIN_SetError("SwapBuffers()"); + if (_this->gl_data->wglSwapLayerBuffers) { + if (!_this->gl_data->wglSwapLayerBuffers(hdc, WGL_SWAP_MAIN_PLANE)) { + return WIN_SetError("wglSwapLayerBuffers()"); + } + } else { + if (!SwapBuffers(hdc)) { + return WIN_SetError("SwapBuffers()"); + } } return true; } diff --git a/src/video/windows/SDL_windowsopengl.h b/src/video/windows/SDL_windowsopengl.h index 23e2f3a58f..7d6abece1f 100644 --- a/src/video/windows/SDL_windowsopengl.h +++ b/src/video/windows/SDL_windowsopengl.h @@ -85,6 +85,8 @@ struct SDL_GLDriverData BOOL (WINAPI *wglGetPixelFormatAttribivARB)(HDC hdc, int iPixelFormat, int iLayerPlane, UINT nAttributes, const int *piAttributes, int *piValues); BOOL (WINAPI *wglSwapIntervalEXT)(int interval); int (WINAPI *wglGetSwapIntervalEXT)(void); + BOOL (WINAPI *wglSwapLayerBuffers)(HDC hdc, UINT flags); + #if defined(SDL_PLATFORM_XBOXONE) || defined(SDL_PLATFORM_XBOXSERIES) BOOL (WINAPI *wglSwapBuffers)(HDC hdc); int (WINAPI *wglDescribePixelFormat)(HDC hdc, From 515433aa8a8fdeb0be4d0e2e1ee733ad16fc7d6e Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Fri, 11 Jul 2025 14:16:18 -0400 Subject: [PATCH 013/103] android: If various POSIX fsops functions fail, try using AAssetManager. This specifically affects SDL_EnumerateDirectory and SDL_GetPathInfo. Android assets are read-only, so no need to do this for things like SDL_CreateDirectory, etc, and the POSIX SDL_CopyFile() uses SDL_IOStream behind the scenes, which already supports Android assets. Fixes #13050. --- src/core/android/SDL_android.c | 56 +++++++++++++++++++++++++++++ src/core/android/SDL_android.h | 2 ++ src/filesystem/posix/SDL_sysfsops.c | 16 ++++++++- 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/core/android/SDL_android.c b/src/core/android/SDL_android.c index 6cdffef245..655da3179c 100644 --- a/src/core/android/SDL_android.c +++ b/src/core/android/SDL_android.c @@ -1853,6 +1853,62 @@ bool Android_JNI_FileClose(void *userdata) return true; } +bool Android_JNI_EnumerateAssetDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata) +{ + SDL_assert((*path == '\0') || (path[SDL_strlen(path) - 1] == '/')); // SDL_SYS_EnumerateDirectory() should have verified this. + + if (!asset_manager) { + Internal_Android_Create_AssetManager(); + if (!asset_manager) { + return SDL_SetError("Couldn't create asset manager"); + } + } + + AAssetDir *adir = AAssetManager_openDir(asset_manager, path); + if (!adir) { + return SDL_SetError("AAssetManager_openDir failed"); + } + + SDL_EnumerationResult result = SDL_ENUM_CONTINUE; + const char *ent; + while ((result == SDL_ENUM_CONTINUE) && ((ent = AAssetDir_getNextFileName(adir)) != NULL)) { + result = cb(userdata, path, ent); + } + + AAssetDir_close(adir); + + return (result != SDL_ENUM_FAILURE); +} + +bool Android_JNI_GetAssetPathInfo(const char *path, SDL_PathInfo *info) +{ + if (!asset_manager) { + Internal_Android_Create_AssetManager(); + if (!asset_manager) { + return SDL_SetError("Couldn't create asset manager"); + } + } + + // this is sort of messy, but there isn't a stat()-like interface to the Assets. + AAssetDir *adir = AAssetManager_openDir(asset_manager, path); + if (adir) { // it's a directory! + AAssetDir_close(adir); + info->type = SDL_PATHTYPE_DIRECTORY; + info->size = 0; + return true; + } + + AAsset *aasset = AAssetManager_open(asset_manager, path, AASSET_MODE_UNKNOWN); + if (aasset) { // it's a file! + info->type = SDL_PATHTYPE_FILE; + info->size = (Uint64) AAsset_getLength64(aasset); + AAsset_close(aasset); + return true; + } + + return SDL_SetError("Couldn't open asset '%s'", path); +} + bool Android_JNI_SetClipboardText(const char *text) { JNIEnv *env = Android_JNI_GetEnv(); diff --git a/src/core/android/SDL_android.h b/src/core/android/SDL_android.h index 925cc19994..b89dbb9f32 100644 --- a/src/core/android/SDL_android.h +++ b/src/core/android/SDL_android.h @@ -88,6 +88,8 @@ Sint64 Android_JNI_FileSeek(void *userdata, Sint64 offset, SDL_IOWhence whence); size_t Android_JNI_FileRead(void *userdata, void *buffer, size_t size, SDL_IOStatus *status); size_t Android_JNI_FileWrite(void *userdata, const void *buffer, size_t size, SDL_IOStatus *status); bool Android_JNI_FileClose(void *userdata); +bool Android_JNI_EnumerateAssetDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata); +bool Android_JNI_GetAssetPathInfo(const char *path, SDL_PathInfo *info); // Environment support void Android_JNI_GetManifestEnvironmentVariables(void); diff --git a/src/filesystem/posix/SDL_sysfsops.c b/src/filesystem/posix/SDL_sysfsops.c index 015b8d4b5a..64e47886a1 100644 --- a/src/filesystem/posix/SDL_sysfsops.c +++ b/src/filesystem/posix/SDL_sysfsops.c @@ -35,6 +35,10 @@ #include #include +#ifdef SDL_PLATFORM_ANDROID +#include "../../core/android/SDL_android.h" +#endif + bool SDL_SYS_EnumerateDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata) { char *pathwithsep = NULL; @@ -51,8 +55,14 @@ bool SDL_SYS_EnumerateDirectory(const char *path, SDL_EnumerateDirectoryCallback DIR *dir = opendir(pathwithsep); if (!dir) { + #ifdef SDL_PLATFORM_ANDROID // Maybe it's an asset...? + const bool retval = Android_JNI_EnumerateAssetDirectory(pathwithsep, cb, userdata); + SDL_free(pathwithsep); + return retval; + #else SDL_free(pathwithsep); return SDL_SetError("Can't open directory: %s", strerror(errno)); + #endif } // make sure there's a path separator at the end now for the actual callback. @@ -173,7 +183,11 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info) struct stat statbuf; const int rc = stat(path, &statbuf); if (rc < 0) { + #ifdef SDL_PLATFORM_ANDROID // Maybe it's an asset...? + return Android_JNI_GetAssetPathInfo(path, info); + #else return SDL_SetError("Can't stat: %s", strerror(errno)); + #endif } else if (S_ISREG(statbuf.st_mode)) { info->type = SDL_PATHTYPE_FILE; info->size = (Uint64) statbuf.st_size; @@ -203,7 +217,7 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info) return true; } -// Note that this isn't actually part of filesystem, not fsops, but everything that uses posix fsops uses this implementation, even with separate filesystem code. +// Note that this is actually part of filesystem, not fsops, but everything that uses posix fsops uses this implementation, even with separate filesystem code. char *SDL_SYS_GetCurrentDirectory(void) { size_t buflen = 64; From 45cc80f02c14bfa4b4aa438e3c862b8b632e48fc Mon Sep 17 00:00:00 2001 From: SDL Wiki Bot Date: Fri, 11 Jul 2025 18:20:01 +0000 Subject: [PATCH 014/103] Sync SDL3 wiki -> header [ci skip] --- include/SDL3/SDL_video.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h index 81bb1c7dec..09b55ad1c6 100644 --- a/include/SDL3/SDL_video.h +++ b/include/SDL3/SDL_video.h @@ -180,6 +180,12 @@ typedef struct SDL_Window SDL_Window; * changed on existing windows by the app, and some of it might be altered by * the user or system outside of the app's control. * + * When creating windows with `SDL_WINDOW_RESIZABLE`, SDL will constrain + * resizable windows to the dimensions recommended by the compositor to fit it + * within the usable desktop space, although some compositors will do this + * automatically without intervention as well. Use `SDL_SetWindowResizable` + * after creation instead if you wish to create a window with a specific size. + * * \since This datatype is available since SDL 3.2.0. * * \sa SDL_GetWindowFlags From a2dcdfcb2d75f3230ec3e75220c46e4124400e83 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Fri, 11 Jul 2025 14:23:52 -0400 Subject: [PATCH 015/103] stdinc: Docs said "macro" but meant "datatype." --- include/SDL3/SDL_stdinc.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/SDL3/SDL_stdinc.h b/include/SDL3/SDL_stdinc.h index f198de75ef..206ff1dbf4 100644 --- a/include/SDL3/SDL_stdinc.h +++ b/include/SDL3/SDL_stdinc.h @@ -492,7 +492,7 @@ typedef uint64_t Uint64; * and SDL_SECONDS_TO_NS(), and between Windows FILETIME values with * SDL_TimeToWindows() and SDL_TimeFromWindows(). * - * \since This macro is available since SDL 3.2.0. + * \since This datatype is available since SDL 3.2.0. * * \sa SDL_MAX_SINT64 * \sa SDL_MIN_SINT64 From 72f4dd17bed986ccf448c5ad59056ab7f7a41988 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Fri, 11 Jul 2025 14:27:12 -0400 Subject: [PATCH 016/103] x11: Avoid duplicate mouse events when using a pen device. Fixes #12968. --- src/video/x11/SDL_x11xinput2.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/video/x11/SDL_x11xinput2.c b/src/video/x11/SDL_x11xinput2.c index afe4a7c85b..98080ccfd8 100644 --- a/src/video/x11/SDL_x11xinput2.c +++ b/src/video/x11/SDL_x11xinput2.c @@ -406,11 +406,15 @@ void X11_HandleXinput2Event(SDL_VideoDevice *_this, XGenericEventCookie *cookie) case XI_ButtonRelease: { const XIDeviceEvent *xev = (const XIDeviceEvent *)cookie->data; - X11_PenHandle *pen = X11_FindPenByDeviceID(xev->deviceid); + X11_PenHandle *pen = X11_FindPenByDeviceID(xev->sourceid); const int button = xev->detail; const bool down = (cookie->evtype == XI_ButtonPress); if (pen) { + if (xev->deviceid != xev->sourceid) { + // Discard events from "Master" devices to avoid duplicates. + break; + } // Only report button event; if there was also pen movement / pressure changes, we expect an XI_Motion event first anyway. SDL_Window *window = xinput2_get_sdlwindow(videodata, xev->event); if (button == 1) { // button 1 is the pen tip @@ -422,11 +426,6 @@ void X11_HandleXinput2Event(SDL_VideoDevice *_this, XGenericEventCookie *cookie) // Otherwise assume a regular mouse SDL_WindowData *windowdata = xinput2_get_sdlwindowdata(videodata, xev->event); - if (xev->deviceid != xev->sourceid) { - // Discard events from "Master" devices to avoid duplicates. - break; - } - if (down) { X11_HandleButtonPress(_this, windowdata, (SDL_MouseID)xev->sourceid, button, (float)xev->event_x, (float)xev->event_y, xev->time); @@ -449,7 +448,8 @@ void X11_HandleXinput2Event(SDL_VideoDevice *_this, XGenericEventCookie *cookie) videodata->global_mouse_changed = true; - X11_PenHandle *pen = X11_FindPenByDeviceID(xev->deviceid); + X11_PenHandle *pen = X11_FindPenByDeviceID(xev->sourceid); + if (pen) { if (xev->deviceid != xev->sourceid) { // Discard events from "Master" devices to avoid duplicates. From f199aafaeb79cbe084496269632537318d2e044e Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Fri, 11 Jul 2025 11:55:20 -0700 Subject: [PATCH 017/103] Fixed long delay when enumerating the Razer Huntsman keyboard Fixes https://github.com/libsdl-org/SDL/issues/13236 --- src/hidapi/windows/hid.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hidapi/windows/hid.c b/src/hidapi/windows/hid.c index 3376ba96e5..87aa639e24 100644 --- a/src/hidapi/windows/hid.c +++ b/src/hidapi/windows/hid.c @@ -949,6 +949,7 @@ static int hid_blacklist(unsigned short vendor_id, unsigned short product_id) { 0x0D8C, 0x0014 }, /* Sharkoon Skiller SGH2 headset - causes deadlock asking for device details */ { 0x1532, 0x0109 }, /* Razer Lycosa Gaming keyboard - causes deadlock asking for device details */ { 0x1532, 0x010B }, /* Razer Arctosa Gaming keyboard - causes deadlock asking for device details */ + { 0x1532, 0x0227 }, /* Razer Huntsman Gaming keyboard - long delay asking for device details */ { 0x1B1C, 0x1B3D }, /* Corsair Gaming keyboard - causes deadlock asking for device details */ { 0x1CCF, 0x0000 } /* All Konami Amusement Devices - causes deadlock asking for device details */ }; From 9af93abd4f94ed52c6dd2859805aa832fafc4a3b Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Fri, 11 Jul 2025 15:03:01 -0400 Subject: [PATCH 018/103] cocoa: Don't minimize fullscreen windows for a modal file dialog. macOS sends a focus loss event when the dialog is created, which causes SDL to try to minimize the window, which confuses the entire system. So in this special case, don't do the minimization. Fixes #13168. --- src/dialog/cocoa/SDL_cocoadialog.m | 7 +++++++ src/video/SDL_video.c | 7 +++++-- src/video/cocoa/SDL_cocoawindow.h | 1 + src/video/cocoa/SDL_cocoawindow.m | 12 ++++++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/dialog/cocoa/SDL_cocoadialog.m b/src/dialog/cocoa/SDL_cocoadialog.m index d12dab8718..671ca887c5 100644 --- a/src/dialog/cocoa/SDL_cocoadialog.m +++ b/src/dialog/cocoa/SDL_cocoadialog.m @@ -27,6 +27,8 @@ #import #import +extern void Cocoa_SetWindowHasModalDialog(SDL_Window *window, bool has_modal); + static void AddFileExtensionType(NSMutableArray *types, const char *pattern_ptr) { if (!*pattern_ptr) { @@ -163,6 +165,9 @@ void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFil if (window) { w = (__bridge NSWindow *)SDL_GetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, NULL); + if (w) { + Cocoa_SetWindowHasModalDialog(window, true); + } } if (w) { @@ -186,6 +191,7 @@ void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFil callback(userdata, files, -1); } + Cocoa_SetWindowHasModalDialog(window, false); ReactivateAfterDialog(); }]; } else { @@ -206,6 +212,7 @@ void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFil const char *files[1] = { NULL }; callback(userdata, files, -1); } + ReactivateAfterDialog(); } } diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c index 44a0463f4b..e54a685396 100644 --- a/src/video/SDL_video.c +++ b/src/video/SDL_video.c @@ -174,9 +174,10 @@ static VideoBootStrap *bootstrap[] = { } #if defined(SDL_PLATFORM_MACOS) && defined(SDL_VIDEO_DRIVER_COCOA) -// Support for macOS fullscreen spaces +// Support for macOS fullscreen spaces, etc. extern bool Cocoa_IsWindowInFullscreenSpace(SDL_Window *window); extern bool Cocoa_SetWindowFullscreenSpace(SDL_Window *window, bool state, bool blocking); +extern bool Cocoa_IsShowingModalDialog(SDL_Window *window); #endif #ifdef SDL_VIDEO_DRIVER_UIKIT @@ -4245,7 +4246,9 @@ static bool SDL_ShouldMinimizeOnFocusLoss(SDL_Window *window) #if defined(SDL_PLATFORM_MACOS) && defined(SDL_VIDEO_DRIVER_COCOA) if (SDL_strcmp(_this->name, "cocoa") == 0) { // don't do this for X11, etc - if (Cocoa_IsWindowInFullscreenSpace(window)) { + if (Cocoa_IsShowingModalDialog(window)) { + return false; // modal system dialogs can live over fullscreen windows, don't minimize. + } else if (Cocoa_IsWindowInFullscreenSpace(window)) { return false; } } diff --git a/src/video/cocoa/SDL_cocoawindow.h b/src/video/cocoa/SDL_cocoawindow.h index 67f1519eec..e4ab6efed4 100644 --- a/src/video/cocoa/SDL_cocoawindow.h +++ b/src/video/cocoa/SDL_cocoawindow.h @@ -152,6 +152,7 @@ typedef enum @property(nonatomic) bool pending_size; @property(nonatomic) bool pending_position; @property(nonatomic) bool border_toggled; +@property(nonatomic) bool has_modal_dialog; #ifdef SDL_VIDEO_OPENGL_EGL @property(nonatomic) EGLSurface egl_surface; diff --git a/src/video/cocoa/SDL_cocoawindow.m b/src/video/cocoa/SDL_cocoawindow.m index 12135e5d91..b67bafb0c6 100644 --- a/src/video/cocoa/SDL_cocoawindow.m +++ b/src/video/cocoa/SDL_cocoawindow.m @@ -432,6 +432,18 @@ bool Cocoa_IsWindowZoomed(SDL_Window *window) return zoomed; } +bool Cocoa_IsShowingModalDialog(SDL_Window *window) +{ + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + return data.has_modal_dialog; +} + +void Cocoa_SetWindowHasModalDialog(SDL_Window *window, bool has_modal) +{ + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + data.has_modal_dialog = has_modal; +} + typedef enum CocoaMenuVisibility { COCOA_MENU_VISIBILITY_AUTO = 0, From 937e8d55a44c3f37cea99d8aff3a59b18a9e17de Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Fri, 11 Jul 2025 12:14:01 -0700 Subject: [PATCH 019/103] Set hwndTarget to NULL when unregistering raw input Fixes https://github.com/libsdl-org/SDL/issues/13335 --- src/video/windows/SDL_windowsrawinput.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/video/windows/SDL_windowsrawinput.c b/src/video/windows/SDL_windowsrawinput.c index fa249914d0..64c612cae9 100644 --- a/src/video/windows/SDL_windowsrawinput.c +++ b/src/video/windows/SDL_windowsrawinput.c @@ -112,7 +112,9 @@ static DWORD WINAPI WIN_RawInputThread(LPVOID param) } devices[0].dwFlags |= RIDEV_REMOVE; + devices[0].hwndTarget = NULL; devices[1].dwFlags |= RIDEV_REMOVE; + devices[1].hwndTarget = NULL; RegisterRawInputDevices(devices, count, sizeof(devices[0])); DestroyWindow(window); From 0b2e389ee3c55bb39fad524632f4f5259882e7f3 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Fri, 11 Jul 2025 12:34:14 -0700 Subject: [PATCH 020/103] Fixed long delay when enumerating the Razer Huntsman keyboard Fixes https://github.com/libsdl-org/SDL/issues/13236 --- src/hidapi/libusb/hid.c | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/hidapi/libusb/hid.c b/src/hidapi/libusb/hid.c index 94cc50dcb4..61638a9abc 100644 --- a/src/hidapi/libusb/hid.c +++ b/src/hidapi/libusb/hid.c @@ -927,6 +927,22 @@ static int should_enumerate_interface(unsigned short vendor_id, const struct lib return 0; } +static int hid_blacklist(unsigned short vendor_id, unsigned short product_id) +{ + size_t i; + static const struct { unsigned short vid; unsigned short pid; } known_bad[] = { + { 0x1532, 0x0227 } /* Razer Huntsman Gaming keyboard - long delay asking for device details */ + }; + + for (i = 0; i < (sizeof(known_bad)/sizeof(known_bad[0])); i++) { + if ((vendor_id == known_bad[i].vid) && (product_id == known_bad[i].pid || known_bad[i].pid == 0x0000)) { + return 1; + } + } + + return 0; +} + struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) { libusb_device **devs; @@ -957,7 +973,8 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short dev_pid = desc.idProduct; if ((vendor_id != 0x0 && vendor_id != dev_vid) || - (product_id != 0x0 && product_id != dev_pid)) { + (product_id != 0x0 && product_id != dev_pid) || + hid_blacklist(dev_vid, dev_pid)) { continue; } From 92e8224d3298bc9bb2cfd5826c414daee403eaab Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Fri, 11 Jul 2025 13:05:37 -0700 Subject: [PATCH 021/103] Fixed build --- src/hidapi/libusb/hid.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hidapi/libusb/hid.c b/src/hidapi/libusb/hid.c index 61638a9abc..0c4fc660ba 100644 --- a/src/hidapi/libusb/hid.c +++ b/src/hidapi/libusb/hid.c @@ -927,7 +927,7 @@ static int should_enumerate_interface(unsigned short vendor_id, const struct lib return 0; } -static int hid_blacklist(unsigned short vendor_id, unsigned short product_id) +static int libusb_blacklist(unsigned short vendor_id, unsigned short product_id) { size_t i; static const struct { unsigned short vid; unsigned short pid; } known_bad[] = { @@ -974,7 +974,7 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, if ((vendor_id != 0x0 && vendor_id != dev_vid) || (product_id != 0x0 && product_id != dev_pid) || - hid_blacklist(dev_vid, dev_pid)) { + libusb_blacklist(dev_vid, dev_pid)) { continue; } From a81cf566f4efae124ef0a9c84f8b299899740e0d Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Fri, 11 Jul 2025 15:35:30 -0400 Subject: [PATCH 022/103] wasapi: Force enumerated audio devices to report themselves as float32 format. This is what they'll end up being when used through WASAPI in shared mode, regardless of what the hardware actually expects. Reference Issue #12914. --- src/audio/directsound/SDL_directsound.c | 2 +- src/audio/wasapi/SDL_wasapi.c | 2 +- src/core/windows/SDL_immdevice.c | 22 +++++++++++++--------- src/core/windows/SDL_immdevice.h | 2 +- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/audio/directsound/SDL_directsound.c b/src/audio/directsound/SDL_directsound.c index 7b5cb11ebd..ab4dd0cc53 100644 --- a/src/audio/directsound/SDL_directsound.c +++ b/src/audio/directsound/SDL_directsound.c @@ -206,7 +206,7 @@ static void DSOUND_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDe { #ifdef HAVE_MMDEVICEAPI_H if (SupportsIMMDevice) { - SDL_IMMDevice_EnumerateEndpoints(default_playback, default_recording); + SDL_IMMDevice_EnumerateEndpoints(default_playback, default_recording, SDL_AUDIO_UNKNOWN); } else #endif { diff --git a/src/audio/wasapi/SDL_wasapi.c b/src/audio/wasapi/SDL_wasapi.c index 4b782d3409..46e2ac005f 100644 --- a/src/audio/wasapi/SDL_wasapi.c +++ b/src/audio/wasapi/SDL_wasapi.c @@ -337,7 +337,7 @@ typedef struct static bool mgmtthrtask_DetectDevices(void *userdata) { mgmtthrtask_DetectDevicesData *data = (mgmtthrtask_DetectDevicesData *)userdata; - SDL_IMMDevice_EnumerateEndpoints(data->default_playback, data->default_recording); + SDL_IMMDevice_EnumerateEndpoints(data->default_playback, data->default_recording, SDL_AUDIO_F32); return true; } diff --git a/src/core/windows/SDL_immdevice.c b/src/core/windows/SDL_immdevice.c index 802a412e15..cc6945b1bc 100644 --- a/src/core/windows/SDL_immdevice.c +++ b/src/core/windows/SDL_immdevice.c @@ -120,7 +120,7 @@ void SDL_IMMDevice_FreeDeviceHandle(SDL_AudioDevice *device) } } -static SDL_AudioDevice *SDL_IMMDevice_Add(const bool recording, const char *devname, WAVEFORMATEXTENSIBLE *fmt, LPCWSTR devid, GUID *dsoundguid) +static SDL_AudioDevice *SDL_IMMDevice_Add(const bool recording, const char *devname, WAVEFORMATEXTENSIBLE *fmt, LPCWSTR devid, GUID *dsoundguid, SDL_AudioFormat force_format) { /* You can have multiple endpoints on a device that are mutually exclusive ("Speakers" vs "Line Out" or whatever). In a perfect world, things that are unplugged won't be in this collection. The only gotcha is probably for @@ -162,7 +162,7 @@ static SDL_AudioDevice *SDL_IMMDevice_Add(const bool recording, const char *devn SDL_zero(spec); spec.channels = (Uint8)fmt->Format.nChannels; spec.freq = fmt->Format.nSamplesPerSec; - spec.format = SDL_WaveFormatExToSDLFormat((WAVEFORMATEX *)fmt); + spec.format = (force_format != SDL_AUDIO_UNKNOWN) ? force_format : SDL_WaveFormatExToSDLFormat((WAVEFORMATEX *)fmt); device = SDL_AddAudioDevice(recording, devname, &spec, handle); if (!device) { @@ -183,6 +183,7 @@ typedef struct SDLMMNotificationClient { const IMMNotificationClientVtbl *lpVtbl; SDL_AtomicInt refcount; + SDL_AudioFormat force_format; } SDLMMNotificationClient; static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_QueryInterface(IMMNotificationClient *client, REFIID iid, void **ppv) @@ -241,6 +242,7 @@ static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceRemoved(IMMNoti static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceStateChanged(IMMNotificationClient *iclient, LPCWSTR pwstrDeviceId, DWORD dwNewState) { + SDLMMNotificationClient *client = (SDLMMNotificationClient *)iclient; IMMDevice *device = NULL; if (SUCCEEDED(IMMDeviceEnumerator_GetDevice(enumerator, pwstrDeviceId, &device))) { @@ -255,7 +257,7 @@ static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceStateChanged(IM GUID dsoundguid; GetMMDeviceInfo(device, &utf8dev, &fmt, &dsoundguid); if (utf8dev) { - SDL_IMMDevice_Add(recording, utf8dev, &fmt, pwstrDeviceId, &dsoundguid); + SDL_IMMDevice_Add(recording, utf8dev, &fmt, pwstrDeviceId, &dsoundguid, client->force_format); SDL_free(utf8dev); } } else { @@ -286,7 +288,7 @@ static const IMMNotificationClientVtbl notification_client_vtbl = { SDLMMNotificationClient_OnPropertyValueChanged }; -static SDLMMNotificationClient notification_client = { ¬ification_client_vtbl, { 1 } }; +static SDLMMNotificationClient notification_client = { ¬ification_client_vtbl, { 1 }, SDL_AUDIO_UNKNOWN }; bool SDL_IMMDevice_Init(const SDL_IMMDevice_callbacks *callbacks) { @@ -363,7 +365,7 @@ bool SDL_IMMDevice_Get(SDL_AudioDevice *device, IMMDevice **immdevice, bool reco return true; } -static void EnumerateEndpointsForFlow(const bool recording, SDL_AudioDevice **default_device) +static void EnumerateEndpointsForFlow(const bool recording, SDL_AudioDevice **default_device, SDL_AudioFormat force_format) { /* Note that WASAPI separates "adapter devices" from "audio endpoint devices" ...one adapter device ("SoundBlaster Pro") might have multiple endpoint devices ("Speakers", "Line-Out"). */ @@ -405,7 +407,7 @@ static void EnumerateEndpointsForFlow(const bool recording, SDL_AudioDevice **de SDL_zero(dsoundguid); GetMMDeviceInfo(immdevice, &devname, &fmt, &dsoundguid); if (devname) { - SDL_AudioDevice *sdldevice = SDL_IMMDevice_Add(recording, devname, &fmt, devid, &dsoundguid); + SDL_AudioDevice *sdldevice = SDL_IMMDevice_Add(recording, devname, &fmt, devid, &dsoundguid, force_format); if (default_device && default_devid && SDL_wcscmp(default_devid, devid) == 0) { *default_device = sdldevice; } @@ -422,10 +424,12 @@ static void EnumerateEndpointsForFlow(const bool recording, SDL_AudioDevice **de IMMDeviceCollection_Release(collection); } -void SDL_IMMDevice_EnumerateEndpoints(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording) +void SDL_IMMDevice_EnumerateEndpoints(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording, SDL_AudioFormat force_format) { - EnumerateEndpointsForFlow(false, default_playback); - EnumerateEndpointsForFlow(true, default_recording); + EnumerateEndpointsForFlow(false, default_playback, force_format); + EnumerateEndpointsForFlow(true, default_recording, force_format); + + notification_client.force_format = force_format; // if this fails, we just won't get hotplug events. Carry on anyhow. IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *)¬ification_client); diff --git a/src/core/windows/SDL_immdevice.h b/src/core/windows/SDL_immdevice.h index 66fdf13b81..0582bc0ded 100644 --- a/src/core/windows/SDL_immdevice.h +++ b/src/core/windows/SDL_immdevice.h @@ -37,7 +37,7 @@ typedef struct SDL_IMMDevice_callbacks bool SDL_IMMDevice_Init(const SDL_IMMDevice_callbacks *callbacks); void SDL_IMMDevice_Quit(void); bool SDL_IMMDevice_Get(struct SDL_AudioDevice *device, IMMDevice **immdevice, bool recording); -void SDL_IMMDevice_EnumerateEndpoints(struct SDL_AudioDevice **default_playback, struct SDL_AudioDevice **default_recording); +void SDL_IMMDevice_EnumerateEndpoints(struct SDL_AudioDevice **default_playback, struct SDL_AudioDevice **default_recording, SDL_AudioFormat force_format); LPGUID SDL_IMMDevice_GetDirectSoundGUID(struct SDL_AudioDevice *device); LPCWSTR SDL_IMMDevice_GetDevID(struct SDL_AudioDevice *device); void SDL_IMMDevice_FreeDeviceHandle(struct SDL_AudioDevice *device); From 190afc0f4fd584028bbdba2f6d2c12c40c2825a6 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Fri, 11 Jul 2025 18:18:47 -0400 Subject: [PATCH 023/103] gpu: Fixed uninitialized variable in SDL_AcquireGPUCommandBuffer(). Fixes #13191. --- src/gpu/SDL_gpu.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gpu/SDL_gpu.c b/src/gpu/SDL_gpu.c index 2b5a38bdd5..ebf0b333e9 100644 --- a/src/gpu/SDL_gpu.c +++ b/src/gpu/SDL_gpu.c @@ -1566,6 +1566,7 @@ SDL_GPUCommandBuffer *SDL_AcquireGPUCommandBuffer( commandBufferHeader->copy_pass.in_progress = false; commandBufferHeader->swapchain_texture_acquired = false; commandBufferHeader->submitted = false; + commandBufferHeader->ignore_render_pass_texture_validation = false; SDL_zeroa(commandBufferHeader->render_pass.vertex_sampler_bound); SDL_zeroa(commandBufferHeader->render_pass.vertex_storage_texture_bound); SDL_zeroa(commandBufferHeader->render_pass.vertex_storage_buffer_bound); From 5e787555e8f6393a0452bcf7cd244416b17affbc Mon Sep 17 00:00:00 2001 From: Anonymous Maarten Date: Sat, 12 Jul 2025 01:14:25 +0200 Subject: [PATCH 024/103] ci: build MSVC release binary on windows-2025 (cherry picked from commit 554f08bac34309bf52aef3488adbe761218792ac) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b0215a528c..d507b69d9f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -256,7 +256,7 @@ jobs: msvc: needs: [src] - runs-on: windows-2019 + runs-on: windows-2025 outputs: VC-x86: ${{ steps.releaser.outputs.VC-x86 }} VC-x64: ${{ steps.releaser.outputs.VC-x64 }} From a190e3b5149e736919715264502fba6574ed9720 Mon Sep 17 00:00:00 2001 From: Kyle Sylvestre Date: Sat, 12 Jul 2025 00:42:17 -0400 Subject: [PATCH 025/103] move SDL_HelperWindow outside of video move to SDL_window.c to prevent relying on SDL_VIDEO --- src/core/windows/SDL_windows.c | 72 +++++++++++++++++++++++++++ src/video/windows/SDL_windowswindow.c | 72 --------------------------- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/src/core/windows/SDL_windows.c b/src/core/windows/SDL_windows.c index d96d8e0bba..cc8c8327ce 100644 --- a/src/core/windows/SDL_windows.c +++ b/src/core/windows/SDL_windows.c @@ -53,6 +53,78 @@ typedef enum RO_INIT_TYPE #define WC_ERR_INVALID_CHARS 0x00000080 #endif +// Fake window to help with DirectInput events. +HWND SDL_HelperWindow = NULL; +static const TCHAR *SDL_HelperWindowClassName = TEXT("SDLHelperWindowInputCatcher"); +static const TCHAR *SDL_HelperWindowName = TEXT("SDLHelperWindowInputMsgWindow"); +static ATOM SDL_HelperWindowClass = 0; + +/* + * Creates a HelperWindow used for DirectInput. + */ +bool SDL_HelperWindowCreate(void) +{ + HINSTANCE hInstance = GetModuleHandle(NULL); + WNDCLASS wce; + + // Make sure window isn't created twice. + if (SDL_HelperWindow != NULL) { + return true; + } + + // Create the class. + SDL_zero(wce); + wce.lpfnWndProc = DefWindowProc; + wce.lpszClassName = SDL_HelperWindowClassName; + wce.hInstance = hInstance; + + // Register the class. + SDL_HelperWindowClass = RegisterClass(&wce); + if (SDL_HelperWindowClass == 0 && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) { + return WIN_SetError("Unable to create Helper Window Class"); + } + + // Create the window. + SDL_HelperWindow = CreateWindowEx(0, SDL_HelperWindowClassName, + SDL_HelperWindowName, + WS_OVERLAPPED, CW_USEDEFAULT, + CW_USEDEFAULT, CW_USEDEFAULT, + CW_USEDEFAULT, HWND_MESSAGE, NULL, + hInstance, NULL); + if (!SDL_HelperWindow) { + UnregisterClass(SDL_HelperWindowClassName, hInstance); + return WIN_SetError("Unable to create Helper Window"); + } + + return true; +} + +/* + * Destroys the HelperWindow previously created with SDL_HelperWindowCreate. + */ +void SDL_HelperWindowDestroy(void) +{ + HINSTANCE hInstance = GetModuleHandle(NULL); + + // Destroy the window. + if (SDL_HelperWindow != NULL) { + if (DestroyWindow(SDL_HelperWindow) == 0) { + WIN_SetError("Unable to destroy Helper Window"); + return; + } + SDL_HelperWindow = NULL; + } + + // Unregister the class. + if (SDL_HelperWindowClass != 0) { + if ((UnregisterClass(SDL_HelperWindowClassName, hInstance)) == 0) { + WIN_SetError("Unable to destroy Helper Window Class"); + return; + } + SDL_HelperWindowClass = 0; + } +} + // Sets an error message based on an HRESULT bool WIN_SetErrorFromHRESULT(const char *prefix, HRESULT hr) { diff --git a/src/video/windows/SDL_windowswindow.c b/src/video/windows/SDL_windowswindow.c index fe61315acc..a6435aa8ef 100644 --- a/src/video/windows/SDL_windowswindow.c +++ b/src/video/windows/SDL_windowswindow.c @@ -96,12 +96,6 @@ typedef void (NTAPI *RtlGetVersion_t)(NT_OSVERSIONINFOW *); // #define HIGHDPI_DEBUG -// Fake window to help with DirectInput events. -HWND SDL_HelperWindow = NULL; -static const TCHAR *SDL_HelperWindowClassName = TEXT("SDLHelperWindowInputCatcher"); -static const TCHAR *SDL_HelperWindowName = TEXT("SDLHelperWindowInputMsgWindow"); -static ATOM SDL_HelperWindowClass = 0; - /* For borderless Windows, still want the following flag: - WS_MINIMIZEBOX: window will respond to Windows minimize commands sent to all windows, such as windows key + m, shaking title bar, etc. Additionally, non-fullscreen windows can add: @@ -1538,72 +1532,6 @@ void WIN_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window) CleanupWindowData(_this, window); } -/* - * Creates a HelperWindow used for DirectInput. - */ -bool SDL_HelperWindowCreate(void) -{ - HINSTANCE hInstance = GetModuleHandle(NULL); - WNDCLASS wce; - - // Make sure window isn't created twice. - if (SDL_HelperWindow != NULL) { - return true; - } - - // Create the class. - SDL_zero(wce); - wce.lpfnWndProc = DefWindowProc; - wce.lpszClassName = SDL_HelperWindowClassName; - wce.hInstance = hInstance; - - // Register the class. - SDL_HelperWindowClass = RegisterClass(&wce); - if (SDL_HelperWindowClass == 0 && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) { - return WIN_SetError("Unable to create Helper Window Class"); - } - - // Create the window. - SDL_HelperWindow = CreateWindowEx(0, SDL_HelperWindowClassName, - SDL_HelperWindowName, - WS_OVERLAPPED, CW_USEDEFAULT, - CW_USEDEFAULT, CW_USEDEFAULT, - CW_USEDEFAULT, HWND_MESSAGE, NULL, - hInstance, NULL); - if (!SDL_HelperWindow) { - UnregisterClass(SDL_HelperWindowClassName, hInstance); - return WIN_SetError("Unable to create Helper Window"); - } - - return true; -} - -/* - * Destroys the HelperWindow previously created with SDL_HelperWindowCreate. - */ -void SDL_HelperWindowDestroy(void) -{ - HINSTANCE hInstance = GetModuleHandle(NULL); - - // Destroy the window. - if (SDL_HelperWindow != NULL) { - if (DestroyWindow(SDL_HelperWindow) == 0) { - WIN_SetError("Unable to destroy Helper Window"); - return; - } - SDL_HelperWindow = NULL; - } - - // Unregister the class. - if (SDL_HelperWindowClass != 0) { - if ((UnregisterClass(SDL_HelperWindowClassName, hInstance)) == 0) { - WIN_SetError("Unable to destroy Helper Window Class"); - return; - } - SDL_HelperWindowClass = 0; - } -} - #if !defined(SDL_PLATFORM_XBOXONE) && !defined(SDL_PLATFORM_XBOXSERIES) void WIN_OnWindowEnter(SDL_VideoDevice *_this, SDL_Window *window) { From d42217ba26433ad71378124f7f1571de2aa8ac45 Mon Sep 17 00:00:00 2001 From: Kyle Sylvestre Date: Sat, 12 Jul 2025 00:44:40 -0400 Subject: [PATCH 026/103] check SDL_PLATFORM_WINDOWS instead of SDL_VIDEO_DRIVER_WINDOWS when using SDL_HelperWindow --- src/SDL.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SDL.c b/src/SDL.c index 34e15251b1..5bc133245d 100644 --- a/src/SDL.c +++ b/src/SDL.c @@ -65,7 +65,7 @@ // Initialization/Cleanup routines #include "timer/SDL_timer_c.h" -#ifdef SDL_VIDEO_DRIVER_WINDOWS +#ifdef SDL_PLATFORM_WINDOWS extern bool SDL_HelperWindowCreate(void); extern void SDL_HelperWindowDestroy(void); #endif @@ -317,7 +317,7 @@ bool SDL_InitSubSystem(SDL_InitFlags flags) SDL_DBus_Init(); #endif -#ifdef SDL_VIDEO_DRIVER_WINDOWS +#ifdef SDL_PLATFORM_WINDOWS if (flags & (SDL_INIT_HAPTIC | SDL_INIT_JOYSTICK)) { if (!SDL_HelperWindowCreate()) { goto quit_and_error; @@ -653,7 +653,7 @@ void SDL_Quit(void) SDL_bInMainQuit = true; // Quit all subsystems -#ifdef SDL_VIDEO_DRIVER_WINDOWS +#ifdef SDL_PLATFORM_WINDOWS SDL_HelperWindowDestroy(); #endif SDL_QuitSubSystem(SDL_INIT_EVERYTHING); From 0f061ff154dafb2f65fd882aa69beb119f99318d Mon Sep 17 00:00:00 2001 From: Kyle Sylvestre Date: Sat, 12 Jul 2025 00:46:53 -0400 Subject: [PATCH 027/103] remove spoofed SDL_HelperWindow when SDL_VIDEO is off --- src/haptic/windows/SDL_dinputhaptic.c | 4 ---- src/joystick/windows/SDL_dinputjoystick.c | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/haptic/windows/SDL_dinputhaptic.c b/src/haptic/windows/SDL_dinputhaptic.c index 255aac015d..79c4b3501f 100644 --- a/src/haptic/windows/SDL_dinputhaptic.c +++ b/src/haptic/windows/SDL_dinputhaptic.c @@ -31,11 +31,7 @@ /* * External stuff. */ -#ifdef SDL_VIDEO_DRIVER_WINDOWS extern HWND SDL_HelperWindow; -#else -static const HWND SDL_HelperWindow = NULL; -#endif /* * Internal stuff. diff --git a/src/joystick/windows/SDL_dinputjoystick.c b/src/joystick/windows/SDL_dinputjoystick.c index a96388239f..f90894ff2f 100644 --- a/src/joystick/windows/SDL_dinputjoystick.c +++ b/src/joystick/windows/SDL_dinputjoystick.c @@ -40,11 +40,7 @@ #define CONVERT_MAGNITUDE(x) (((x)*10000) / 0x7FFF) // external variables referenced. -#ifdef SDL_VIDEO_DRIVER_WINDOWS extern HWND SDL_HelperWindow; -#else -static const HWND SDL_HelperWindow = NULL; -#endif // local variables static bool coinitialized = false; From 536126fdcf6b507074f9a9240c0630e748658e1f Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Sun, 13 Jul 2025 14:57:48 -0400 Subject: [PATCH 028/103] emscripten: Move over to using Pointer Events for all mouse, pen, and touch. This allows us to avoid browser mouse emulation for touches, since we provide our own anyhow. The other option is to "prevent default" in the legacy touch event handlers we historically used, to tell the browser not to supply emulation, but we can't currently tell Emscripten to mark those handlers as not "passive," so as it stands they are unable to prevent default. Using Pointer Events bypasses this problem entirely. Fixes #13161. --- src/video/emscripten/SDL_emscriptenevents.c | 599 +++++++++++--------- 1 file changed, 332 insertions(+), 267 deletions(-) diff --git a/src/video/emscripten/SDL_emscriptenevents.c b/src/video/emscripten/SDL_emscriptenevents.c index cc999a74dd..69c685ba6f 100644 --- a/src/video/emscripten/SDL_emscriptenevents.c +++ b/src/video/emscripten/SDL_emscriptenevents.c @@ -310,135 +310,6 @@ static EM_BOOL Emscripten_HandlePointerLockChangeGlobal(int eventType, const Ems return prevent_default; } -static EM_BOOL Emscripten_HandleMouseMove(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) -{ - SDL_WindowData *window_data = userData; - const bool isPointerLocked = window_data->has_pointer_lock; - float mx, my; - - // rescale (in case canvas is being scaled) - double client_w, client_h, xscale, yscale; - emscripten_get_element_css_size(window_data->canvas_id, &client_w, &client_h); - xscale = window_data->window->w / client_w; - yscale = window_data->window->h / client_h; - - if (isPointerLocked) { - mx = (float)(mouseEvent->movementX * xscale); - my = (float)(mouseEvent->movementY * yscale); - } else { - mx = (float)(mouseEvent->targetX * xscale); - my = (float)(mouseEvent->targetY * yscale); - } - - SDL_SendMouseMotion(0, window_data->window, SDL_DEFAULT_MOUSE_ID, isPointerLocked, mx, my); - return 0; -} - -static EM_BOOL Emscripten_HandleMouseButton(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) -{ - SDL_WindowData *window_data = userData; - Uint8 sdl_button; - bool sdl_button_state; - double css_w, css_h; - bool prevent_default = false; // needed for iframe implementation in Chrome-based browsers. - - switch (mouseEvent->button) { - case 0: - sdl_button = SDL_BUTTON_LEFT; - break; - case 1: - sdl_button = SDL_BUTTON_MIDDLE; - break; - case 2: - sdl_button = SDL_BUTTON_RIGHT; - break; - default: - return 0; - } - - const SDL_Mouse *mouse = SDL_GetMouse(); - SDL_assert(mouse != NULL); - - if (eventType == EMSCRIPTEN_EVENT_MOUSEDOWN) { - if (mouse->relative_mode && !window_data->has_pointer_lock) { - emscripten_request_pointerlock(window_data->canvas_id, 0); // try to regrab lost pointer lock. - } - sdl_button_state = true; - } else { - sdl_button_state = false; - prevent_default = SDL_EventEnabled(SDL_EVENT_MOUSE_BUTTON_UP); - } - - SDL_SendMouseButton(0, window_data->window, SDL_DEFAULT_MOUSE_ID, sdl_button, sdl_button_state); - - // We have an imaginary mouse capture, because we need SDL to not drop our imaginary mouse focus when we leave the canvas. - if (mouse->auto_capture) { - if (SDL_GetMouseState(NULL, NULL) != 0) { - window_data->window->flags |= SDL_WINDOW_MOUSE_CAPTURE; - } else { - window_data->window->flags &= ~SDL_WINDOW_MOUSE_CAPTURE; - } - } - - if ((eventType == EMSCRIPTEN_EVENT_MOUSEUP) && window_data->mouse_focus_loss_pending) { - window_data->mouse_focus_loss_pending = (window_data->window->flags & SDL_WINDOW_MOUSE_CAPTURE) != 0; - if (!window_data->mouse_focus_loss_pending) { - SDL_SetMouseFocus(NULL); - } - } else { - // Do not consume the event if the mouse is outside of the canvas. - emscripten_get_element_css_size(window_data->canvas_id, &css_w, &css_h); - if (mouseEvent->targetX < 0 || mouseEvent->targetX >= css_w || - mouseEvent->targetY < 0 || mouseEvent->targetY >= css_h) { - return 0; - } - } - - return prevent_default; -} - -static EM_BOOL Emscripten_HandleMouseButtonGlobal(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) -{ - SDL_VideoDevice *device = userData; - bool prevent_default = false; - SDL_Window *window; - - for (window = device->windows; window; window = window->next) { - prevent_default |= Emscripten_HandleMouseButton(eventType, mouseEvent, window->internal); - } - - return prevent_default; -} - -static EM_BOOL Emscripten_HandleMouseFocus(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) -{ - SDL_WindowData *window_data = userData; - - const bool isPointerLocked = window_data->has_pointer_lock; - - if (!isPointerLocked) { - // rescale (in case canvas is being scaled) - float mx, my; - double client_w, client_h; - emscripten_get_element_css_size(window_data->canvas_id, &client_w, &client_h); - - mx = (float)(mouseEvent->targetX * (window_data->window->w / client_w)); - my = (float)(mouseEvent->targetY * (window_data->window->h / client_h)); - SDL_SendMouseMotion(0, window_data->window, SDL_GLOBAL_MOUSE_ID, isPointerLocked, mx, my); - } - - const bool isenter = (eventType == EMSCRIPTEN_EVENT_MOUSEENTER); - if (isenter && window_data->mouse_focus_loss_pending) { - window_data->mouse_focus_loss_pending = false; // just drop the state, but don't send the enter event. - } else if (!isenter && (window_data->window->flags & SDL_WINDOW_MOUSE_CAPTURE)) { - window_data->mouse_focus_loss_pending = true; // waiting on a mouse button to let go before we send the mouse focus update. - } else { - SDL_SetMouseFocus(isenter ? window_data->window : NULL); - } - - return SDL_EventEnabled(SDL_EVENT_MOUSE_MOTION); // !!! FIXME: should this be MOUSE_MOTION or something else? -} - static EM_BOOL Emscripten_HandleWheel(int eventType, const EmscriptenWheelEvent *wheelEvent, void *userData) { SDL_WindowData *window_data = userData; @@ -483,62 +354,6 @@ static EM_BOOL Emscripten_HandleFocus(int eventType, const EmscriptenFocusEvent return SDL_EventEnabled(sdl_event_type); } -static EM_BOOL Emscripten_HandleTouch(int eventType, const EmscriptenTouchEvent *touchEvent, void *userData) -{ - SDL_WindowData *window_data = (SDL_WindowData *)userData; - int i; - double client_w, client_h; - int preventDefault = 0; - - const SDL_TouchID deviceId = 1; - if (SDL_AddTouch(deviceId, SDL_TOUCH_DEVICE_DIRECT, "") < 0) { - return 0; - } - - emscripten_get_element_css_size(window_data->canvas_id, &client_w, &client_h); - - for (i = 0; i < touchEvent->numTouches; i++) { - SDL_FingerID id; - float x, y; - - if (!touchEvent->touches[i].isChanged) { - continue; - } - - id = touchEvent->touches[i].identifier + 1; - if (client_w <= 1) { - x = 0.5f; - } else { - x = touchEvent->touches[i].targetX / (client_w - 1); - } - if (client_h <= 1) { - y = 0.5f; - } else { - y = touchEvent->touches[i].targetY / (client_h - 1); - } - - if (eventType == EMSCRIPTEN_EVENT_TOUCHSTART) { - SDL_SendTouch(0, deviceId, id, window_data->window, SDL_EVENT_FINGER_DOWN, x, y, 1.0f); - - // disable browser scrolling/pinch-to-zoom if app handles touch events - if (!preventDefault && SDL_EventEnabled(SDL_EVENT_FINGER_DOWN)) { - preventDefault = 1; - } - } else if (eventType == EMSCRIPTEN_EVENT_TOUCHMOVE) { - SDL_SendTouchMotion(0, deviceId, id, window_data->window, x, y, 1.0f); - } else if (eventType == EMSCRIPTEN_EVENT_TOUCHEND) { - SDL_SendTouch(0, deviceId, id, window_data->window, SDL_EVENT_FINGER_UP, x, y, 1.0f); - - // block browser's simulated mousedown/mouseup on touchscreen devices - preventDefault = 1; - } else if (eventType == EMSCRIPTEN_EVENT_TOUCHCANCEL) { - SDL_SendTouch(0, deviceId, id, window_data->window, SDL_EVENT_FINGER_CANCELED, x, y, 1.0f); - } - } - - return preventDefault; -} - static bool IsFunctionKey(SDL_Scancode scancode) { if (scancode >= SDL_SCANCODE_F1 && scancode <= SDL_SCANCODE_F12) { @@ -783,9 +598,13 @@ static EM_BOOL Emscripten_HandleOrientationChange(int eventType, const Emscripte return 0; } -// IF YOU CHANGE THIS STRUCTURE, YOU NEED TO UPDATE THE JAVASCRIPT THAT FILLS IT IN: makePointerEventCStruct, below. +// IF YOU CHANGE THIS STRUCTURE, YOU NEED TO UPDATE THE JAVASCRIPT THAT FILLS IT IN: SDL3.makePointerEventCStruct, below. +#define PTRTYPE_MOUSE 1 +#define PTRTYPE_TOUCH 2 +#define PTRTYPE_PEN 3 typedef struct Emscripten_PointerEvent { + int pointer_type; int pointerid; int button; int buttons; @@ -800,8 +619,120 @@ typedef struct Emscripten_PointerEvent float rotation; } Emscripten_PointerEvent; -static void Emscripten_UpdatePointerFromEvent(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) +static void Emscripten_HandleMouseButton(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) { + Uint8 sdl_button; + bool down; + switch (event->button) { + #define CHECK_MOUSE_BUTTON(jsbutton, downflag, sdlbutton) case jsbutton: down = (event->buttons & downflag) != 0; ; sdl_button = SDL_BUTTON_##sdlbutton; break + CHECK_MOUSE_BUTTON(0, 1, LEFT); + CHECK_MOUSE_BUTTON(1, 4, MIDDLE); + CHECK_MOUSE_BUTTON(2, 2, RIGHT); + CHECK_MOUSE_BUTTON(3, 8, X1); + CHECK_MOUSE_BUTTON(4, 16, X2); + #undef CHECK_MOUSE_BUTTON + default: sdl_button = 0; break; + } + + if (sdl_button) { + const SDL_Mouse *mouse = SDL_GetMouse(); + SDL_assert(mouse != NULL); + + if (down) { + if (mouse->relative_mode && !window_data->has_pointer_lock) { + emscripten_request_pointerlock(window_data->canvas_id, 0); // try to regrab lost pointer lock. + } + } + + SDL_SendMouseButton(0, window_data->window, SDL_DEFAULT_MOUSE_ID, sdl_button, down); + + // We have an imaginary mouse capture, because we need SDL to not drop our imaginary mouse focus when we leave the canvas. + if (mouse->auto_capture) { + if (SDL_GetMouseState(NULL, NULL) != 0) { + window_data->window->flags |= SDL_WINDOW_MOUSE_CAPTURE; + } else { + window_data->window->flags &= ~SDL_WINDOW_MOUSE_CAPTURE; + } + } + + if (!down && window_data->mouse_focus_loss_pending) { + window_data->mouse_focus_loss_pending = (window_data->window->flags & SDL_WINDOW_MOUSE_CAPTURE) != 0; + if (!window_data->mouse_focus_loss_pending) { + SDL_SetMouseFocus(NULL); + } + } + } +} + +static void Emscripten_UpdateMouseFromEvent(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) +{ + SDL_assert(event->pointer_type == PTRTYPE_MOUSE); + + // rescale (in case canvas is being scaled) + double client_w, client_h; + emscripten_get_element_css_size(window_data->canvas_id, &client_w, &client_h); + const double xscale = window_data->window->w / client_w; + const double yscale = window_data->window->h / client_h; + + const bool isPointerLocked = window_data->has_pointer_lock; + float mx, my; + if (isPointerLocked) { + mx = (float)(event->movementX * xscale); + my = (float)(event->movementY * yscale); + } else { + mx = (float)(event->targetX * xscale); + my = (float)(event->targetY * yscale); + } + + SDL_SendMouseMotion(0, window_data->window, SDL_DEFAULT_MOUSE_ID, isPointerLocked, mx, my); + + Emscripten_HandleMouseButton(window_data, event); +} + +static void Emscripten_UpdateTouchFromEvent(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) +{ + SDL_assert(event->pointer_type == PTRTYPE_TOUCH); + + const SDL_TouchID deviceId = 1; + if (SDL_AddTouch(deviceId, SDL_TOUCH_DEVICE_DIRECT, "") < 0) { + return; + } + + double client_w, client_h; + emscripten_get_element_css_size(window_data->canvas_id, &client_w, &client_h); + + const SDL_FingerID id = event->pointerid + 1; + float x, y; + if (client_w <= 1) { + x = 0.5f; + } else { + x = event->targetX / (client_w - 1); + } + if (client_h <= 1) { + y = 0.5f; + } else { + y = event->targetY / (client_h - 1); + } + + const bool down = (event->buttons & 1) != 0; + if (event->button == 0) { // touch is starting or ending if this is zero (-1 means "no change"). + if (down) { + SDL_SendTouch(0, deviceId, id, window_data->window, SDL_EVENT_FINGER_DOWN, x, y, 1.0f); + } + } + + SDL_SendTouchMotion(0, deviceId, id, window_data->window, x, y, 1.0f); + + if (event->button == 0) { // touch is starting or ending if this is zero (-1 means "no change"). + if (!down) { + SDL_SendTouch(0, deviceId, id, window_data->window, SDL_EVENT_FINGER_UP, x, y, 1.0f); + } + } +} + +static void Emscripten_UpdatePenFromEvent(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) +{ + SDL_assert(event->pointer_type == PTRTYPE_PEN); const SDL_PenID pen = SDL_FindPenByHandle((void *) (size_t) event->pointerid); if (pen) { // rescale (in case canvas is being scaled) @@ -844,8 +775,49 @@ static void Emscripten_UpdatePointerFromEvent(SDL_WindowData *window_data, const } } -EMSCRIPTEN_KEEPALIVE void Emscripten_HandlePointerEnter(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) +static void Emscripten_UpdatePointerFromEvent(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) { + SDL_assert(event != NULL); + if (event->pointer_type == PTRTYPE_MOUSE) { + Emscripten_UpdateMouseFromEvent(window_data, event); + } else if (event->pointer_type == PTRTYPE_TOUCH) { + Emscripten_UpdateTouchFromEvent(window_data, event); + } else if (event->pointer_type == PTRTYPE_PEN) { + Emscripten_UpdatePenFromEvent(window_data, event); + } else { + SDL_assert(!"Unexpected pointer event type"); + } +} + +static void Emscripten_HandleMouseFocus(SDL_WindowData *window_data, const Emscripten_PointerEvent *event, bool isenter) +{ + SDL_assert(event->pointer_type == PTRTYPE_MOUSE); + + const bool isPointerLocked = window_data->has_pointer_lock; + + if (!isPointerLocked) { + // rescale (in case canvas is being scaled) + float mx, my; + double client_w, client_h; + emscripten_get_element_css_size(window_data->canvas_id, &client_w, &client_h); + + mx = (float)(event->targetX * (window_data->window->w / client_w)); + my = (float)(event->targetY * (window_data->window->h / client_h)); + SDL_SendMouseMotion(0, window_data->window, SDL_GLOBAL_MOUSE_ID, isPointerLocked, mx, my); + } + + if (isenter && window_data->mouse_focus_loss_pending) { + window_data->mouse_focus_loss_pending = false; // just drop the state, but don't send the enter event. + } else if (!isenter && (window_data->window->flags & SDL_WINDOW_MOUSE_CAPTURE)) { + window_data->mouse_focus_loss_pending = true; // waiting on a mouse button to let go before we send the mouse focus update. + } else { + SDL_SetMouseFocus(isenter ? window_data->window : NULL); + } +} + +static void Emscripten_HandlePenEnter(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) +{ + SDL_assert(event->pointer_type == PTRTYPE_PEN); // Web browsers offer almost none of this information as specifics, but can without warning offer any of these specific things. SDL_PenInfo peninfo; SDL_zero(peninfo); @@ -854,10 +826,24 @@ EMSCRIPTEN_KEEPALIVE void Emscripten_HandlePointerEnter(SDL_WindowData *window_d peninfo.num_buttons = 2; peninfo.subtype = SDL_PEN_TYPE_PEN; SDL_AddPenDevice(0, NULL, &peninfo, (void *) (size_t) event->pointerid); - Emscripten_UpdatePointerFromEvent(window_data, event); + Emscripten_UpdatePenFromEvent(window_data, event); } -EMSCRIPTEN_KEEPALIVE void Emscripten_HandlePointerLeave(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) +EMSCRIPTEN_KEEPALIVE void Emscripten_HandlePointerEnter(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) +{ + SDL_assert(event != NULL); + if (event->pointer_type == PTRTYPE_MOUSE) { + Emscripten_HandleMouseFocus(window_data, event, true); + } else if (event->pointer_type == PTRTYPE_PEN) { + Emscripten_HandlePenEnter(window_data, event); + } else if (event->pointer_type == PTRTYPE_TOUCH) { + // do nothing. + } else { + SDL_assert(!"Unexpected pointer event type"); + } +} + +static void Emscripten_HandlePenLeave(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) { const SDL_PenID pen = SDL_FindPenByHandle((void *) (size_t) event->pointerid); if (pen) { @@ -866,37 +852,87 @@ EMSCRIPTEN_KEEPALIVE void Emscripten_HandlePointerLeave(SDL_WindowData *window_d } } +static void Emscripten_HandleTouchCancel(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) +{ + SDL_assert(event->pointer_type == PTRTYPE_TOUCH); + + const SDL_TouchID deviceId = 1; + if (SDL_AddTouch(deviceId, SDL_TOUCH_DEVICE_DIRECT, "") < 0) { + return; + } + + double client_w, client_h; + emscripten_get_element_css_size(window_data->canvas_id, &client_w, &client_h); + + const SDL_FingerID id = event->pointerid + 1; + float x, y; + if (client_w <= 1) { + x = 0.5f; + } else { + x = event->targetX / (client_w - 1); + } + if (client_h <= 1) { + y = 0.5f; + } else { + y = event->targetY / (client_h - 1); + } + + SDL_SendTouch(0, deviceId, id, window_data->window, SDL_EVENT_FINGER_CANCELED, x, y, 1.0f); +} + +EMSCRIPTEN_KEEPALIVE void Emscripten_HandlePointerLeave(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) +{ + SDL_assert(event != NULL); + if (event->pointer_type == PTRTYPE_MOUSE) { + Emscripten_HandleMouseFocus(window_data, event, false); + } else if (event->pointer_type == PTRTYPE_PEN) { + Emscripten_HandlePenLeave(window_data, event); + } else if (event->pointer_type == PTRTYPE_TOUCH) { + Emscripten_HandleTouchCancel(window_data, event); + } else { + SDL_assert(!"Unexpected pointer event type"); + } +} + EMSCRIPTEN_KEEPALIVE void Emscripten_HandlePointerGeneric(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) { + SDL_assert(event != NULL); Emscripten_UpdatePointerFromEvent(window_data, event); } -static void Emscripten_set_pointer_event_callbacks(SDL_WindowData *data) +static void Emscripten_prep_pointer_event_callbacks(void) { MAIN_THREAD_EM_ASM({ - var target = document.querySelector(UTF8ToString($1)); - if (target) { - var data = $0; + if (typeof(Module['SDL3']) === 'undefined') { + Module['SDL3'] = {}; + } + var SDL3 = Module['SDL3']; - if (typeof(Module['SDL3']) === 'undefined') { - Module['SDL3'] = {}; - } - var SDL3 = Module['SDL3']; + if (SDL3.makePointerEventCStruct === undefined) { + SDL3.makePointerEventCStruct = function(left, top, event) { + var ptrtype = 0; + if (event.pointerType == "mouse") { + ptrtype = 1; + } else if (event.pointerType == "touch") { + ptrtype = 2; + } else if (event.pointerType == "pen") { + ptrtype = 3; + } else { + return 0; + } - var makePointerEventCStruct = function(event) { - var ptr = 0; - if (event.pointerType == "pen") { - ptr = _SDL_malloc($2); - if (ptr != 0) { - var rect = target.getBoundingClientRect(); - var idx = ptr >> 2; - HEAP32[idx++] = event.pointerId; - HEAP32[idx++] = (typeof(event.button) !== "undefined") ? event.button : -1; - HEAP32[idx++] = event.buttons; - HEAPF32[idx++] = event.movementX; - HEAPF32[idx++] = event.movementY; - HEAPF32[idx++] = event.clientX - rect.left; - HEAPF32[idx++] = event.clientY - rect.top; + var ptr = _SDL_malloc($0); + if (ptr != 0) { + var idx = ptr >> 2; + HEAP32[idx++] = ptrtype; + HEAP32[idx++] = event.pointerId; + HEAP32[idx++] = (typeof(event.button) !== "undefined") ? event.button : -1; + HEAP32[idx++] = event.buttons; + HEAPF32[idx++] = event.movementX; + HEAPF32[idx++] = event.movementY; + HEAPF32[idx++] = event.clientX - left; + HEAPF32[idx++] = event.clientY - top; + if (ptrtype == 3) { HEAPF32[idx++] = event.pressure; HEAPF32[idx++] = event.tangentialPressure; HEAPF32[idx++] = event.tiltX; @@ -906,26 +942,41 @@ static void Emscripten_set_pointer_event_callbacks(SDL_WindowData *data) } return ptr; }; - - SDL3.eventHandlerPointerEnter = function(event) { - var d = makePointerEventCStruct(event); if (d != 0) { _Emscripten_HandlePointerEnter(data, d); _SDL_free(d); } - }; - target.addEventListener("pointerenter", SDL3.eventHandlerPointerEnter); - - SDL3.eventHandlerPointerLeave = function(event) { - var d = makePointerEventCStruct(event); if (d != 0) { _Emscripten_HandlePointerLeave(data, d); _SDL_free(d); } - }; - target.addEventListener("pointerleave", SDL3.eventHandlerPointerLeave); - target.addEventListener("pointercancel", SDL3.eventHandlerPointerLeave); // catch this, just in case. - - SDL3.eventHandlerPointerGeneric = function(event) { - var d = makePointerEventCStruct(event); if (d != 0) { _Emscripten_HandlePointerGeneric(data, d); _SDL_free(d); } - }; - target.addEventListener("pointerdown", SDL3.eventHandlerPointerGeneric); - target.addEventListener("pointerup", SDL3.eventHandlerPointerGeneric); - target.addEventListener("pointermove", SDL3.eventHandlerPointerGeneric); } - }, data, data->canvas_id, sizeof (Emscripten_PointerEvent)); + }, sizeof (Emscripten_PointerEvent)); +} + +static void Emscripten_set_pointer_event_callbacks(SDL_WindowData *data) +{ + Emscripten_prep_pointer_event_callbacks(); + + MAIN_THREAD_EM_ASM({ + var target = document.querySelector(UTF8ToString($1)); + if (target) { + var SDL3 = Module['SDL3']; + var data = $0; + target.sdlEventHandlerPointerEnter = function(event) { + var rect = target.getBoundingClientRect(); + var d = SDL3.makePointerEventCStruct(rect.left, rect.top, event); if (d != 0) { _Emscripten_HandlePointerEnter(data, d); _SDL_free(d); } + }; + target.sdlEventHandlerPointerLeave = function(event) { + var rect = target.getBoundingClientRect(); + var d = SDL3.makePointerEventCStruct(rect.left, rect.top, event); if (d != 0) { _Emscripten_HandlePointerLeave(data, d); _SDL_free(d); } + }; + target.sdlEventHandlerPointerGeneric = function(event) { + var rect = target.getBoundingClientRect(); + var d = SDL3.makePointerEventCStruct(rect.left, rect.top, event); if (d != 0) { _Emscripten_HandlePointerGeneric(data, d); _SDL_free(d); } + }; + + target.style.touchAction = "none"; // or mobile devices will scroll as your touch moves across the element. + target.addEventListener("pointerenter", target.sdlEventHandlerPointerEnter); + target.addEventListener("pointerleave", target.sdlEventHandlerPointerLeave); + target.addEventListener("pointercancel", target.sdlEventHandlerPointerLeave); + target.addEventListener("pointerdown", target.sdlEventHandlerPointerGeneric); + target.addEventListener("pointermove", target.sdlEventHandlerPointerGeneric); + target.addEventListener("pointerup", target.sdlEventHandlerPointerGeneric); + } + }, data, data->canvas_id); } static void Emscripten_unset_pointer_event_callbacks(SDL_WindowData *data) @@ -933,20 +984,58 @@ static void Emscripten_unset_pointer_event_callbacks(SDL_WindowData *data) MAIN_THREAD_EM_ASM({ var target = document.querySelector(UTF8ToString($0)); if (target) { - var SDL3 = Module['SDL3']; - target.removeEventListener("pointerenter", SDL3.eventHandlerPointerEnter); - target.removeEventListener("pointerleave", SDL3.eventHandlerPointerLeave); - target.removeEventListener("pointercancel", SDL3.eventHandlerPointerLeave); - target.removeEventListener("pointerdown", SDL3.eventHandlerPointerGeneric); - target.removeEventListener("pointerup", SDL3.eventHandlerPointerGeneric); - target.removeEventListener("pointermove", SDL3.eventHandlerPointerGeneric); - SDL3.eventHandlerPointerEnter = undefined; - SDL3.eventHandlerPointerLeave = undefined; - SDL3.eventHandlerPointerGeneric = undefined; + target.removeEventListener("pointerenter", target.sdlEventHandlerPointerEnter); + target.removeEventListener("pointerleave", target.sdlEventHandlerPointerLeave); + target.removeEventListener("pointercancel", target.sdlEventHandlerPointerLeave); + target.removeEventListener("pointerdown", target.sdlEventHandlerPointerGeneric); + target.removeEventListener("pointermove", target.sdlEventHandlerPointerGeneric); + target.removeEventListener("pointerup", target.sdlEventHandlerPointerGeneric); + target.style.touchAction = ""; // let mobile devices scroll again as your touch moves across the element. + target.sdlEventHandlerPointerEnter = undefined; + target.sdlEventHandlerPointerLeave = undefined; + target.sdlEventHandlerPointerGeneric = undefined; } }, data->canvas_id); } +EMSCRIPTEN_KEEPALIVE void Emscripten_HandleMouseButtonUpGlobal(SDL_VideoDevice *device, const Emscripten_PointerEvent *event) +{ + SDL_assert(device != NULL); + SDL_assert(event != NULL); + if (event->pointer_type == PTRTYPE_MOUSE) { + for (SDL_Window *window = device->windows; window; window = window->next) { + Emscripten_HandleMouseButton(window->internal, event); + } + } +} + +static void Emscripten_set_global_mouseup_callback(SDL_VideoDevice *device) +{ + Emscripten_prep_pointer_event_callbacks(); + + MAIN_THREAD_EM_ASM({ + var target = document; + if (target) { + target.sdlEventHandlerMouseButtonUpGlobal = function(event) { + var SDL3 = Module['SDL3']; + var d = SDL3.makePointerEventCStruct(0, 0, event); if (d != 0) { _Emscripten_HandleMouseButtonUpGlobal($0, d); _SDL_free(d); } + }; + target.addEventListener("pointerup", target.sdlEventHandlerMouseButtonUpGlobal); + } + }, device); +} + +static void Emscripten_unset_global_mouseup_callback(SDL_VideoDevice *device) +{ + MAIN_THREAD_EM_ASM({ + var target = document; + if (target) { + target.removeEventListener("pointerup", target.sdlEventHandlerMouseButtonUpGlobal); + target.sdlEventHandlerMouseButtonUpGlobal = undefined; + } + }); +} + // IF YOU CHANGE THIS STRUCTURE, YOU NEED TO UPDATE THE JAVASCRIPT THAT FILLS IT IN: makeDropEventCStruct, below. typedef struct Emscripten_DropEvent { @@ -1102,7 +1191,7 @@ static const char *Emscripten_GetKeyboardTargetElement(const char *target) void Emscripten_RegisterGlobalEventHandlers(SDL_VideoDevice *device) { - emscripten_set_mouseup_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, device, 0, Emscripten_HandleMouseButtonGlobal); + Emscripten_set_global_mouseup_callback(device); emscripten_set_focus_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, device, 0, Emscripten_HandleFocus); emscripten_set_blur_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, device, 0, Emscripten_HandleFocus); @@ -1116,7 +1205,7 @@ void Emscripten_RegisterGlobalEventHandlers(SDL_VideoDevice *device) void Emscripten_UnregisterGlobalEventHandlers(SDL_VideoDevice *device) { - emscripten_set_mouseup_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, NULL, 0, NULL); + Emscripten_unset_global_mouseup_callback(device); emscripten_set_focus_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, 0, NULL); emscripten_set_blur_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, 0, NULL); @@ -1133,22 +1222,10 @@ void Emscripten_RegisterEventHandlers(SDL_WindowData *data) const char *keyElement; // There is only one window and that window is the canvas - emscripten_set_mousemove_callback(data->canvas_id, data, 0, Emscripten_HandleMouseMove); - - emscripten_set_mousedown_callback(data->canvas_id, data, 0, Emscripten_HandleMouseButton); - - emscripten_set_mouseenter_callback(data->canvas_id, data, 0, Emscripten_HandleMouseFocus); - emscripten_set_mouseleave_callback(data->canvas_id, data, 0, Emscripten_HandleMouseFocus); - emscripten_set_wheel_callback(data->canvas_id, data, 0, Emscripten_HandleWheel); emscripten_set_orientationchange_callback(data, 0, Emscripten_HandleOrientationChange); - emscripten_set_touchstart_callback(data->canvas_id, data, 0, Emscripten_HandleTouch); - emscripten_set_touchend_callback(data->canvas_id, data, 0, Emscripten_HandleTouch); - emscripten_set_touchmove_callback(data->canvas_id, data, 0, Emscripten_HandleTouch); - emscripten_set_touchcancel_callback(data->canvas_id, data, 0, Emscripten_HandleTouch); - keyElement = Emscripten_GetKeyboardTargetElement(data->keyboard_element); if (keyElement) { emscripten_set_keydown_callback(keyElement, data, 0, Emscripten_HandleKey); @@ -1180,22 +1257,10 @@ void Emscripten_UnregisterEventHandlers(SDL_WindowData *data) Emscripten_unset_pointer_event_callbacks(data); // only works due to having one window - emscripten_set_mousemove_callback(data->canvas_id, NULL, 0, NULL); - - emscripten_set_mousedown_callback(data->canvas_id, NULL, 0, NULL); - - emscripten_set_mouseenter_callback(data->canvas_id, NULL, 0, NULL); - emscripten_set_mouseleave_callback(data->canvas_id, NULL, 0, NULL); - emscripten_set_wheel_callback(data->canvas_id, NULL, 0, NULL); emscripten_set_orientationchange_callback(NULL, 0, NULL); - emscripten_set_touchstart_callback(data->canvas_id, NULL, 0, NULL); - emscripten_set_touchend_callback(data->canvas_id, NULL, 0, NULL); - emscripten_set_touchmove_callback(data->canvas_id, NULL, 0, NULL); - emscripten_set_touchcancel_callback(data->canvas_id, NULL, 0, NULL); - keyElement = Emscripten_GetKeyboardTargetElement(data->keyboard_element); if (keyElement) { emscripten_set_keydown_callback(keyElement, NULL, 0, NULL); From 2d8fd6bee12041f0375aab8b63564246d67fce2c Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Sun, 13 Jul 2025 21:40:42 -0400 Subject: [PATCH 029/103] Revert "windows: Use wglSwapLayerBuffers if available." This reverts commit f286558baef3aa9e54a491d744935c8603a90194. --- src/video/windows/SDL_windowsopengl.c | 13 ++----------- src/video/windows/SDL_windowsopengl.h | 2 -- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/video/windows/SDL_windowsopengl.c b/src/video/windows/SDL_windowsopengl.c index 4588e2531c..c458796044 100644 --- a/src/video/windows/SDL_windowsopengl.c +++ b/src/video/windows/SDL_windowsopengl.c @@ -141,9 +141,6 @@ bool WIN_GL_LoadLibrary(SDL_VideoDevice *_this, const char *path) SDL_LoadFunction(handle, "wglMakeCurrent"); _this->gl_data->wglShareLists = (BOOL (WINAPI *)(HGLRC, HGLRC)) SDL_LoadFunction(handle, "wglShareLists"); - _this->gl_data->wglSwapLayerBuffers = (BOOL (WINAPI *)(HDC, UINT)) - SDL_LoadFunction(handle, "wglSwapLayerBuffers"); - /* *INDENT-ON* */ // clang-format on #if defined(SDL_PLATFORM_XBOXONE) || defined(SDL_PLATFORM_XBOXSERIES) @@ -889,14 +886,8 @@ bool WIN_GL_SwapWindow(SDL_VideoDevice *_this, SDL_Window *window) { HDC hdc = window->internal->hdc; - if (_this->gl_data->wglSwapLayerBuffers) { - if (!_this->gl_data->wglSwapLayerBuffers(hdc, WGL_SWAP_MAIN_PLANE)) { - return WIN_SetError("wglSwapLayerBuffers()"); - } - } else { - if (!SwapBuffers(hdc)) { - return WIN_SetError("SwapBuffers()"); - } + if (!SwapBuffers(hdc)) { + return WIN_SetError("SwapBuffers()"); } return true; } diff --git a/src/video/windows/SDL_windowsopengl.h b/src/video/windows/SDL_windowsopengl.h index 7d6abece1f..23e2f3a58f 100644 --- a/src/video/windows/SDL_windowsopengl.h +++ b/src/video/windows/SDL_windowsopengl.h @@ -85,8 +85,6 @@ struct SDL_GLDriverData BOOL (WINAPI *wglGetPixelFormatAttribivARB)(HDC hdc, int iPixelFormat, int iLayerPlane, UINT nAttributes, const int *piAttributes, int *piValues); BOOL (WINAPI *wglSwapIntervalEXT)(int interval); int (WINAPI *wglGetSwapIntervalEXT)(void); - BOOL (WINAPI *wglSwapLayerBuffers)(HDC hdc, UINT flags); - #if defined(SDL_PLATFORM_XBOXONE) || defined(SDL_PLATFORM_XBOXSERIES) BOOL (WINAPI *wglSwapBuffers)(HDC hdc); int (WINAPI *wglDescribePixelFormat)(HDC hdc, From 0a50b798bf1fce3b195f22e38d9a3eecdbad1907 Mon Sep 17 00:00:00 2001 From: Josh Dowell Date: Mon, 14 Jul 2025 01:39:11 +0100 Subject: [PATCH 030/103] windows: Fix crash when using a system that reports itself as Windows 17763 or newer, but is missing many of the newer dark mode window functions (Linux Mint Cinnamon w/ Proton 7.0.6) --- src/video/windows/SDL_windowswindow.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/video/windows/SDL_windowswindow.c b/src/video/windows/SDL_windowswindow.c index a6435aa8ef..91737b38bd 100644 --- a/src/video/windows/SDL_windowswindow.c +++ b/src/video/windows/SDL_windowswindow.c @@ -2301,38 +2301,38 @@ bool WIN_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, bool foc void WIN_UpdateDarkModeForHWND(HWND hwnd) { #if !defined(SDL_PLATFORM_XBOXONE) && !defined(SDL_PLATFORM_XBOXSERIES) - SDL_SharedObject *ntdll = SDL_LoadObject("ntdll.dll"); + HMODULE ntdll = LoadLibrary(TEXT("ntdll.dll")); if (!ntdll) { return; } // There is no function to get Windows build number, so let's get it here via RtlGetVersion - RtlGetVersion_t RtlGetVersionFunc = (RtlGetVersion_t)SDL_LoadFunction(ntdll, "RtlGetVersion"); + RtlGetVersion_t RtlGetVersionFunc = (RtlGetVersion_t)GetProcAddress(ntdll, "RtlGetVersion"); NT_OSVERSIONINFOW os_info; os_info.dwOSVersionInfoSize = sizeof(NT_OSVERSIONINFOW); os_info.dwBuildNumber = 0; if (RtlGetVersionFunc) { RtlGetVersionFunc(&os_info); } - SDL_UnloadObject(ntdll); + FreeLibrary(ntdll); os_info.dwBuildNumber &= ~0xF0000000; if (os_info.dwBuildNumber < 17763) { // Too old to support dark mode return; } - SDL_SharedObject *uxtheme = SDL_LoadObject("uxtheme.dll"); + HMODULE uxtheme = LoadLibrary(TEXT("uxtheme.dll")); if (!uxtheme) { return; } - RefreshImmersiveColorPolicyState_t RefreshImmersiveColorPolicyStateFunc = (RefreshImmersiveColorPolicyState_t)SDL_LoadFunction(uxtheme, MAKEINTRESOURCEA(104)); - ShouldAppsUseDarkMode_t ShouldAppsUseDarkModeFunc = (ShouldAppsUseDarkMode_t)SDL_LoadFunction(uxtheme, MAKEINTRESOURCEA(132)); - AllowDarkModeForWindow_t AllowDarkModeForWindowFunc = (AllowDarkModeForWindow_t)SDL_LoadFunction(uxtheme, MAKEINTRESOURCEA(133)); + RefreshImmersiveColorPolicyState_t RefreshImmersiveColorPolicyStateFunc = (RefreshImmersiveColorPolicyState_t)GetProcAddress(uxtheme, MAKEINTRESOURCEA(104)); + ShouldAppsUseDarkMode_t ShouldAppsUseDarkModeFunc = (ShouldAppsUseDarkMode_t)GetProcAddress(uxtheme, MAKEINTRESOURCEA(132)); + AllowDarkModeForWindow_t AllowDarkModeForWindowFunc = (AllowDarkModeForWindow_t)GetProcAddress(uxtheme, MAKEINTRESOURCEA(133)); if (os_info.dwBuildNumber < 18362) { - AllowDarkModeForApp_t AllowDarkModeForAppFunc = (AllowDarkModeForApp_t)SDL_LoadFunction(uxtheme, MAKEINTRESOURCEA(135)); + AllowDarkModeForApp_t AllowDarkModeForAppFunc = (AllowDarkModeForApp_t)GetProcAddress(uxtheme, MAKEINTRESOURCEA(135)); if (AllowDarkModeForAppFunc) { AllowDarkModeForAppFunc(true); } } else { - SetPreferredAppMode_t SetPreferredAppModeFunc = (SetPreferredAppMode_t)SDL_LoadFunction(uxtheme, MAKEINTRESOURCEA(135)); + SetPreferredAppMode_t SetPreferredAppModeFunc = (SetPreferredAppMode_t)GetProcAddress(uxtheme, MAKEINTRESOURCEA(135)); if (SetPreferredAppModeFunc) { SetPreferredAppModeFunc(UXTHEME_APPMODE_ALLOW_DARK); } @@ -2350,7 +2350,7 @@ void WIN_UpdateDarkModeForHWND(HWND hwnd) } else { value = (SDL_GetSystemTheme() == SDL_SYSTEM_THEME_DARK) ? TRUE : FALSE; } - SDL_UnloadObject(uxtheme); + FreeLibrary(uxtheme); if (os_info.dwBuildNumber < 18362) { SetProp(hwnd, TEXT("UseImmersiveDarkModeColors"), SDL_reinterpret_cast(HANDLE, SDL_static_cast(INT_PTR, value))); } else { From a07cf3ecdce32b7e8fb8aa906aa3e097b06e2b31 Mon Sep 17 00:00:00 2001 From: "Andon M. Coleman" Date: Mon, 7 Jul 2025 16:14:47 -0400 Subject: [PATCH 031/103] Allow 1 kHz sample rate for DualSense Edge over USB DualSense Edge natively reports at 1 kHz for all connection types, but gyro sample rate was limited to 250 Hz for USB. --- src/joystick/hidapi/SDL_hidapi_ps5.c | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/joystick/hidapi/SDL_hidapi_ps5.c b/src/joystick/hidapi/SDL_hidapi_ps5.c index e40170086e..4249578e80 100644 --- a/src/joystick/hidapi/SDL_hidapi_ps5.c +++ b/src/joystick/hidapi/SDL_hidapi_ps5.c @@ -812,14 +812,19 @@ static void HIDAPI_DriverPS5_SetEnhancedModeAvailable(SDL_DriverPS5_Context *ctx } if (ctx->sensors_supported) { + // Standard DualSense sensor update rate is 250 Hz over USB + float update_rate = 250.0f; + if (ctx->device->is_bluetooth) { // Bluetooth sensor update rate appears to be 1000 Hz - SDL_PrivateJoystickAddSensor(ctx->joystick, SDL_SENSOR_GYRO, 1000.0f); - SDL_PrivateJoystickAddSensor(ctx->joystick, SDL_SENSOR_ACCEL, 1000.0f); - } else { - SDL_PrivateJoystickAddSensor(ctx->joystick, SDL_SENSOR_GYRO, 250.0f); - SDL_PrivateJoystickAddSensor(ctx->joystick, SDL_SENSOR_ACCEL, 250.0f); + update_rate = 1000.0f; + } else if (SDL_IsJoystickDualSenseEdge(ctx->device->vendor_id, ctx->device->product_id)) { + // DualSense Edge sensor update rate is 1000 Hz over USB + update_rate = 1000.0f; } + + SDL_PrivateJoystickAddSensor(ctx->joystick, SDL_SENSOR_GYRO, update_rate); + SDL_PrivateJoystickAddSensor(ctx->joystick, SDL_SENSOR_ACCEL, update_rate); } ctx->report_battery = true; From 277f91c3176c88193648760a651f8dd6407035dc Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Mon, 14 Jul 2025 11:30:15 -0700 Subject: [PATCH 032/103] Removed the Mayflash GameCube adapter from the PS3 controller list --- src/joystick/controller_list.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/joystick/controller_list.h b/src/joystick/controller_list.h index 63107389cf..61811f1f32 100644 --- a/src/joystick/controller_list.h +++ b/src/joystick/controller_list.h @@ -21,7 +21,6 @@ static const ControllerDescription_t arrControllers[] = { { MAKE_CONTROLLER_ID( 0x0079, 0x181a ), k_eControllerType_PS3Controller, NULL }, // Venom Arcade Stick - { MAKE_CONTROLLER_ID( 0x0079, 0x1844 ), k_eControllerType_PS3Controller, NULL }, // From SDL { MAKE_CONTROLLER_ID( 0x044f, 0xb315 ), k_eControllerType_PS3Controller, NULL }, // Firestorm Dual Analog 3 { MAKE_CONTROLLER_ID( 0x044f, 0xd007 ), k_eControllerType_PS3Controller, NULL }, // Thrustmaster wireless 3-1 { MAKE_CONTROLLER_ID( 0x046d, 0xcad1 ), k_eControllerType_PS3Controller, NULL }, // Logitech Chillstream From 10004ab0eab0defe2d65802e4d861ca2a6c2ba0b Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Mon, 14 Jul 2025 00:14:15 -0400 Subject: [PATCH 033/103] hints: Added SDL_HINT_LOG_BACKENDS. Fixes #13354. --- include/SDL3/SDL_hints.h | 21 +++++++++++++++++++++ src/SDL_utils.c | 9 +++++++++ src/SDL_utils_c.h | 3 +++ src/audio/SDL_audio.c | 4 +++- src/camera/SDL_camera.c | 4 +++- src/gpu/SDL_gpu.c | 1 + src/io/io_uring/SDL_asyncio_liburing.c | 2 ++ src/io/windows/SDL_asyncio_windows_ioring.c | 2 ++ src/power/SDL_power.c | 2 +- src/render/SDL_render.c | 4 +++- src/storage/SDL_storage.c | 8 ++++++-- src/video/SDL_video.c | 4 +++- 12 files changed, 57 insertions(+), 7 deletions(-) diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h index 30a04c4da4..51505fe651 100644 --- a/include/SDL3/SDL_hints.h +++ b/include/SDL3/SDL_hints.h @@ -4381,6 +4381,27 @@ extern "C" { */ #define SDL_HINT_PEN_TOUCH_EVENTS "SDL_PEN_TOUCH_EVENTS" +/** + * A variable controlling whether SDL backend information is logged. + * + * The variable can be set to the following values: + * + * - "0": Subsystem information will not be logged. (default) + * - "1": Subsystem information will be logged. + * + * This is generally meant to be used as an environment variable to let + * end-users report what subsystems were chosen on their system, to aid + * in debugging. Logged information is sent through SDL_Log(), which + * means by default they appear on stdout on most platforms or maybe + * OutputDebugString() on Windows, and can be funneled by the app with + * SDL_SetLogOutputFunction(), etc. + * + * This hint can be set anytime, but the specific logs are generated + * during subsystem init. + * + * \since This hint is available since SDL 3.4.0. + */ +#define SDL_HINT_LOG_BACKENDS "SDL_LOG_BACKENDS" /** * An enumeration of hint priorities. diff --git a/src/SDL_utils.c b/src/SDL_utils.c index f2090747a8..ec2c435a92 100644 --- a/src/SDL_utils.c +++ b/src/SDL_utils.c @@ -552,3 +552,12 @@ char *SDL_CreateDeviceName(Uint16 vendor, Uint16 product, const char *vendor_nam return name; } + +void SDL_LogBackend(const char *subsystem, const char *backend) +{ + if (SDL_GetHintBoolean(SDL_HINT_LOG_BACKENDS, false)) { + SDL_Log("SDL_BACKEND: %s -> '%s'", subsystem, backend); + } +} + + diff --git a/src/SDL_utils_c.h b/src/SDL_utils_c.h index 5e0e8f519e..2929e7f1f1 100644 --- a/src/SDL_utils_c.h +++ b/src/SDL_utils_c.h @@ -75,4 +75,7 @@ extern const char *SDL_GetPersistentString(const char *string); extern char *SDL_CreateDeviceName(Uint16 vendor, Uint16 product, const char *vendor_name, const char *product_name, const char *default_name); +// Log what backend a subsystem chose, if a hint was set to do so. Useful for debugging. +extern void SDL_LogBackend(const char *subsystem, const char *backend); + #endif // SDL_utils_h_ diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c index 7dc247b193..a450bbee84 100644 --- a/src/audio/SDL_audio.c +++ b/src/audio/SDL_audio.c @@ -1006,7 +1006,9 @@ bool SDL_InitAudio(const char *driver_name) } } - if (!initialized) { + if (initialized) { + SDL_LogBackend("audio", current_audio.name); + } else { // specific drivers will set the error message if they fail, but otherwise we do it here. if (!tried_to_init) { if (driver_name) { diff --git a/src/camera/SDL_camera.c b/src/camera/SDL_camera.c index 9f71cea0f3..7385426afc 100644 --- a/src/camera/SDL_camera.c +++ b/src/camera/SDL_camera.c @@ -1524,7 +1524,9 @@ bool SDL_CameraInit(const char *driver_name) } } - if (!initialized) { + if (initialized) { + SDL_LogBackend("camera", camera_driver.name); + } else { // specific drivers will set the error message if they fail, but otherwise we do it here. if (!tried_to_init) { if (driver_name) { diff --git a/src/gpu/SDL_gpu.c b/src/gpu/SDL_gpu.c index ebf0b333e9..3e0002f058 100644 --- a/src/gpu/SDL_gpu.c +++ b/src/gpu/SDL_gpu.c @@ -711,6 +711,7 @@ SDL_GPUDevice *SDL_CreateGPUDeviceWithProperties(SDL_PropertiesID props) selectedBackend = SDL_GPUSelectBackend(props); if (selectedBackend != NULL) { + SDL_LogBackend("gpu", selectedBackend->name); debug_mode = SDL_GetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_DEBUGMODE_BOOLEAN, true); preferLowPower = SDL_GetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_PREFERLOWPOWER_BOOLEAN, false); diff --git a/src/io/io_uring/SDL_asyncio_liburing.c b/src/io/io_uring/SDL_asyncio_liburing.c index 07e8c2415c..8b4738f9ca 100644 --- a/src/io/io_uring/SDL_asyncio_liburing.c +++ b/src/io/io_uring/SDL_asyncio_liburing.c @@ -512,10 +512,12 @@ static void MaybeInitializeLibUring(void) { if (SDL_ShouldInit(&liburing_init)) { if (LoadLibUring()) { + SDL_LogBackend("asyncio", "liburing"); CreateAsyncIOQueue = SDL_SYS_CreateAsyncIOQueue_liburing; QuitAsyncIO = SDL_SYS_QuitAsyncIO_liburing; AsyncIOFromFile = SDL_SYS_AsyncIOFromFile_liburing; } else { // can't use liburing? Use the "generic" threadpool implementation instead. + SDL_LogBackend("asyncio", "generic"); CreateAsyncIOQueue = SDL_SYS_CreateAsyncIOQueue_Generic; QuitAsyncIO = SDL_SYS_QuitAsyncIO_Generic; AsyncIOFromFile = SDL_SYS_AsyncIOFromFile_Generic; diff --git a/src/io/windows/SDL_asyncio_windows_ioring.c b/src/io/windows/SDL_asyncio_windows_ioring.c index 497456cfa4..fd65921015 100644 --- a/src/io/windows/SDL_asyncio_windows_ioring.c +++ b/src/io/windows/SDL_asyncio_windows_ioring.c @@ -511,10 +511,12 @@ static void MaybeInitializeWinIoRing(void) { if (SDL_ShouldInit(&ioring_init)) { if (LoadWinIoRing()) { + SDL_LogBackend("asyncio", "ioring"); CreateAsyncIOQueue = SDL_SYS_CreateAsyncIOQueue_ioring; QuitAsyncIO = SDL_SYS_QuitAsyncIO_ioring; AsyncIOFromFile = SDL_SYS_AsyncIOFromFile_ioring; } else { // can't use ioring? Use the "generic" threadpool implementation instead. + SDL_LogBackend("asyncio", "generic"); CreateAsyncIOQueue = SDL_SYS_CreateAsyncIOQueue_Generic; QuitAsyncIO = SDL_SYS_QuitAsyncIO_Generic; AsyncIOFromFile = SDL_SYS_AsyncIOFromFile_Generic; diff --git a/src/power/SDL_power.c b/src/power/SDL_power.c index 5dde3b145d..3d91c9a931 100644 --- a/src/power/SDL_power.c +++ b/src/power/SDL_power.c @@ -84,7 +84,7 @@ static SDL_GetPowerInfo_Impl implementations[] = { SDL_PowerState SDL_GetPowerInfo(int *seconds, int *percent) { #ifndef SDL_POWER_DISABLED - const int total = sizeof(implementations) / sizeof(implementations[0]); + const int total = SDL_arraysize(implementations); SDL_PowerState result = SDL_POWERSTATE_UNKNOWN; int i; #endif diff --git a/src/render/SDL_render.c b/src/render/SDL_render.c index 60265b81c3..3fa627b13f 100644 --- a/src/render/SDL_render.c +++ b/src/render/SDL_render.c @@ -1063,7 +1063,9 @@ SDL_Renderer *SDL_CreateRendererWithProperties(SDL_PropertiesID props) } } - if (!rc) { + if (rc) { + SDL_LogBackend("render", renderer->name); + } else { if (driver_name) { SDL_SetError("%s not available", driver_name); } else { diff --git a/src/storage/SDL_storage.c b/src/storage/SDL_storage.c index 7c395b36a8..dd3343b0e2 100644 --- a/src/storage/SDL_storage.c +++ b/src/storage/SDL_storage.c @@ -118,7 +118,9 @@ SDL_Storage *SDL_OpenTitleStorage(const char *override, SDL_PropertiesID props) } } } - if (!storage) { + if (storage) { + SDL_LogBackend("title_storage", titlebootstrap[i]->name); + } else { if (driver_name) { SDL_SetError("%s not available", driver_name); } else { @@ -160,7 +162,9 @@ SDL_Storage *SDL_OpenUserStorage(const char *org, const char *app, SDL_Propertie } } } - if (!storage) { + if (storage) { + SDL_LogBackend("user_storage", userbootstrap[i]->name); + } else { if (driver_name) { SDL_SetError("%s not available", driver_name); } else { diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c index e54a685396..1b29fc42d3 100644 --- a/src/video/SDL_video.c +++ b/src/video/SDL_video.c @@ -679,7 +679,9 @@ bool SDL_VideoInit(const char *driver_name) } } } - if (!video) { + if (video) { + SDL_LogBackend("video", bootstrap[i]->name); + } else { if (driver_name) { SDL_SetError("%s not available", driver_name); goto pre_driver_error; From d5efb11f97787ac074b65979e8244c0815253afc Mon Sep 17 00:00:00 2001 From: SDL Wiki Bot Date: Mon, 14 Jul 2025 23:50:35 +0000 Subject: [PATCH 034/103] Sync SDL3 wiki -> header [ci skip] --- include/SDL3/SDL_hints.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h index 51505fe651..0900b36eb4 100644 --- a/include/SDL3/SDL_hints.h +++ b/include/SDL3/SDL_hints.h @@ -4390,14 +4390,14 @@ extern "C" { * - "1": Subsystem information will be logged. * * This is generally meant to be used as an environment variable to let - * end-users report what subsystems were chosen on their system, to aid - * in debugging. Logged information is sent through SDL_Log(), which - * means by default they appear on stdout on most platforms or maybe + * end-users report what subsystems were chosen on their system, to aid in + * debugging. Logged information is sent through SDL_Log(), which means by + * default they appear on stdout on most platforms or maybe * OutputDebugString() on Windows, and can be funneled by the app with * SDL_SetLogOutputFunction(), etc. * - * This hint can be set anytime, but the specific logs are generated - * during subsystem init. + * This hint can be set anytime, but the specific logs are generated during + * subsystem init. * * \since This hint is available since SDL 3.4.0. */ From 3c04c88c6ed09a8a9ccffad8c32d4001ae44c9f3 Mon Sep 17 00:00:00 2001 From: 8BitDo Date: Tue, 15 Jul 2025 11:24:18 +0800 Subject: [PATCH 035/103] Add support for Pro3 Add support for Pro3 --- src/joystick/hidapi/SDL_hidapi_8bitdo.c | 5 +++++ src/joystick/usb_ids.h | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/joystick/hidapi/SDL_hidapi_8bitdo.c b/src/joystick/hidapi/SDL_hidapi_8bitdo.c index d1167586d0..563ab96287 100644 --- a/src/joystick/hidapi/SDL_hidapi_8bitdo.c +++ b/src/joystick/hidapi/SDL_hidapi_8bitdo.c @@ -145,6 +145,7 @@ static bool HIDAPI_Driver8BitDo_IsSupportedDevice(SDL_HIDAPI_Device *device, con case USB_PRODUCT_8BITDO_SN30_PRO_BT: case USB_PRODUCT_8BITDO_PRO_2: case USB_PRODUCT_8BITDO_PRO_2_BT: + case USB_PRODUCT_8BITDO_PRO_3: case USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS: return true; default: @@ -219,6 +220,8 @@ static bool HIDAPI_Driver8BitDo_InitDevice(SDL_HIDAPI_Device *device) HIDAPI_SetDeviceName(device, "8BitDo SN30 Pro"); } else if (device->product_id == USB_PRODUCT_8BITDO_PRO_2 || device->product_id == USB_PRODUCT_8BITDO_PRO_2_BT) { HIDAPI_SetDeviceName(device, "8BitDo Pro 2"); + } else if (device->product_id == USB_PRODUCT_8BITDO_PRO_3) { + HIDAPI_SetDeviceName(device, "8BitDo Pro 3"); } return HIDAPI_JoystickConnected(device, NULL); @@ -253,6 +256,7 @@ static Uint64 HIDAPI_Driver8BitDo_GetIMURateForProductID(SDL_HIDAPI_Device *devi // This firmware appears to update at 100 Hz over USB return 100; } + case USB_PRODUCT_8BITDO_PRO_3: case USB_PRODUCT_8BITDO_PRO_2: case USB_PRODUCT_8BITDO_PRO_2_BT: // Note, labeled as "BT" but appears this way when wired. if (device->is_bluetooth) { @@ -287,6 +291,7 @@ static bool HIDAPI_Driver8BitDo_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joys // Initialize the joystick capabilities if (device->product_id == USB_PRODUCT_8BITDO_PRO_2 || device->product_id == USB_PRODUCT_8BITDO_PRO_2_BT || + device->product_id == USB_PRODUCT_8BITDO_PRO_3 || device->product_id == USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS) { // This controller has additional buttons joystick->nbuttons = SDL_GAMEPAD_NUM_8BITDO_BUTTONS; diff --git a/src/joystick/usb_ids.h b/src/joystick/usb_ids.h index 33a8a6cb7e..d9ff54f3cd 100644 --- a/src/joystick/usb_ids.h +++ b/src/joystick/usb_ids.h @@ -60,7 +60,8 @@ #define USB_VENDOR_VALVE 0x28de #define USB_VENDOR_ZEROPLUS 0x0c12 -#define USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS 0x6012 +#define USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS 0x6012 // mode switch to BT +#define USB_PRODUCT_8BITDO_PRO_3 0x6009 // mode switch to D #define USB_PRODUCT_8BITDO_SF30_PRO 0x6000 // B + START #define USB_PRODUCT_8BITDO_SF30_PRO_BT 0x6100 // B + START #define USB_PRODUCT_8BITDO_SN30_PRO 0x6001 // B + START From f2ae6503c0ee2d02b22bfee8be3f346a74fcc1c3 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Tue, 15 Jul 2025 06:20:10 -0400 Subject: [PATCH 036/103] audio: Binding an SDL_AudioStream will set missing formats. It _must_ have the format set for the opposite side from the device (so playback needs the src format set, and recording needs the dst format set), since the stream gets mangled by the device thread if not. So if it has never been set (stream created with NULL audiospec), just set it to match the device. If the stream is just meant to buffer and not convert, this is desired behavior, even if it didn't also fix a bug. Binding the audio stream will always set the device side's format, as usual; this does not need to be set by the caller at all. Fixes #13363. --- include/SDL3/SDL_audio.h | 5 ++++- src/audio/SDL_audio.c | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/include/SDL3/SDL_audio.h b/include/SDL3/SDL_audio.h index c0aa7f7c82..ba01d7d11a 100644 --- a/include/SDL3/SDL_audio.h +++ b/include/SDL3/SDL_audio.h @@ -942,7 +942,10 @@ extern SDL_DECLSPEC void SDLCALL SDL_CloseAudioDevice(SDL_AudioDeviceID devid); * Binding a stream to a device will set its output format for playback * devices, and its input format for recording devices, so they match the * device's settings. The caller is welcome to change the other end of the - * stream's format at any time with SDL_SetAudioStreamFormat(). + * stream's format at any time with SDL_SetAudioStreamFormat(). If the other + * end of the stream's format has never been set (the audio stream was created + * with a NULL audio spec), this function will set it to match the device + * end's format. * * \param devid an audio device to bind a stream to. * \param streams an array of audio streams to bind. diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c index a450bbee84..56033912c4 100644 --- a/src/audio/SDL_audio.c +++ b/src/audio/SDL_audio.c @@ -1165,6 +1165,8 @@ bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device) // We should have updated this elsewhere if the format changed! SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &device->spec, NULL, NULL)); + SDL_assert(stream->src_spec.format != SDL_AUDIO_UNKNOWN); + int br = 0; if (!SDL_GetAtomicInt(&logdev->paused)) { @@ -1224,6 +1226,8 @@ bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device) // We should have updated this elsewhere if the format changed! SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &outspec, NULL, NULL)); + SDL_assert(stream->src_spec.format != SDL_AUDIO_UNKNOWN); + /* this will hold a lock on `stream` while getting. We don't explicitly lock the streams for iterating here because the binding linked list can only change while the device lock is held. (we _do_ lock the stream during binding/unbinding to make sure that two threads can't try to bind @@ -1363,6 +1367,7 @@ bool SDL_RecordingAudioThreadIterate(SDL_AudioDevice *device) SDL_assert(stream->src_spec.format == ((logdev->postmix || (logdev->gain != 1.0f)) ? SDL_AUDIO_F32 : device->spec.format)); SDL_assert(stream->src_spec.channels == device->spec.channels); SDL_assert(stream->src_spec.freq == device->spec.freq); + SDL_assert(stream->dst_spec.format != SDL_AUDIO_UNKNOWN); void *final_buf = output_buffer; @@ -2024,10 +2029,6 @@ bool SDL_BindAudioStreams(SDL_AudioDeviceID devid, SDL_AudioStream * const *stre } else if (logdev->simplified) { result = SDL_SetError("Cannot change stream bindings on device opened with SDL_OpenAudioDeviceStream"); } else { - - // !!! FIXME: We'll set the device's side's format below, but maybe we should refuse to bind a stream if the app's side doesn't have a format set yet. - // !!! FIXME: Actually, why do we allow there to be an invalid format, again? - // make sure start of list is sane. SDL_assert(!logdev->bound_streams || (logdev->bound_streams->prev_binding == NULL)); @@ -2062,9 +2063,17 @@ bool SDL_BindAudioStreams(SDL_AudioDeviceID devid, SDL_AudioStream * const *stre if (result) { // Now that everything is verified, chain everything together. + const bool recording = device->recording; for (int i = 0; i < num_streams; i++) { SDL_AudioStream *stream = streams[i]; if (stream) { // shouldn't be NULL, but just in case... + // if the stream never had its non-device-end format set, just set it to the device end's format. + if (recording && (stream->dst_spec.format == SDL_AUDIO_UNKNOWN)) { + SDL_copyp(&stream->dst_spec, &device->spec); + } else if (!recording && (stream->src_spec.format == SDL_AUDIO_UNKNOWN)) { + SDL_copyp(&stream->src_spec, &device->spec); + } + stream->bound_device = logdev; stream->prev_binding = NULL; stream->next_binding = logdev->bound_streams; From d53fcab609f081e5d8cc5883f1c29d94973fce1f Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Tue, 15 Jul 2025 10:09:06 -0700 Subject: [PATCH 037/103] Reordered the 8BitDo Pro 3 controller --- src/joystick/hidapi/SDL_hidapi_8bitdo.c | 2 +- src/joystick/usb_ids.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/joystick/hidapi/SDL_hidapi_8bitdo.c b/src/joystick/hidapi/SDL_hidapi_8bitdo.c index 563ab96287..d48e46671b 100644 --- a/src/joystick/hidapi/SDL_hidapi_8bitdo.c +++ b/src/joystick/hidapi/SDL_hidapi_8bitdo.c @@ -256,9 +256,9 @@ static Uint64 HIDAPI_Driver8BitDo_GetIMURateForProductID(SDL_HIDAPI_Device *devi // This firmware appears to update at 100 Hz over USB return 100; } - case USB_PRODUCT_8BITDO_PRO_3: case USB_PRODUCT_8BITDO_PRO_2: case USB_PRODUCT_8BITDO_PRO_2_BT: // Note, labeled as "BT" but appears this way when wired. + case USB_PRODUCT_8BITDO_PRO_3: if (device->is_bluetooth) { // Note, This is estimated by observation of Bluetooth packets received in the testcontroller tool return 85; // Observed Bluetooth packet rate seems to be 80-90hz diff --git a/src/joystick/usb_ids.h b/src/joystick/usb_ids.h index d9ff54f3cd..ba75bbb8d6 100644 --- a/src/joystick/usb_ids.h +++ b/src/joystick/usb_ids.h @@ -60,14 +60,14 @@ #define USB_VENDOR_VALVE 0x28de #define USB_VENDOR_ZEROPLUS 0x0c12 -#define USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS 0x6012 // mode switch to BT -#define USB_PRODUCT_8BITDO_PRO_3 0x6009 // mode switch to D #define USB_PRODUCT_8BITDO_SF30_PRO 0x6000 // B + START #define USB_PRODUCT_8BITDO_SF30_PRO_BT 0x6100 // B + START #define USB_PRODUCT_8BITDO_SN30_PRO 0x6001 // B + START #define USB_PRODUCT_8BITDO_SN30_PRO_BT 0x6101 // B + START #define USB_PRODUCT_8BITDO_PRO_2 0x6003 // mode switch to D #define USB_PRODUCT_8BITDO_PRO_2_BT 0x6006 // mode switch to D +#define USB_PRODUCT_8BITDO_PRO_3 0x6009 // mode switch to D +#define USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS 0x6012 // mode switch to BT #define USB_PRODUCT_AMAZON_LUNA_CONTROLLER 0x0419 #define USB_PRODUCT_ASTRO_C40_XBOX360 0x0024 #define USB_PRODUCT_BACKBONE_ONE_IOS 0x0103 From 08e3758e3f4fdfbe0f0e4caa6263bb904da5a983 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Tue, 15 Jul 2025 10:15:52 -0700 Subject: [PATCH 038/103] Added paddle bindings for the 8BitDo Pro 3 controller --- src/joystick/SDL_gamepad.c | 5 ++++- src/joystick/usb_ids.h | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index 8a5ecc0ab9..28aaf6a23d 100644 --- a/src/joystick/SDL_gamepad.c +++ b/src/joystick/SDL_gamepad.c @@ -785,10 +785,13 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid) product == USB_PRODUCT_8BITDO_SN30_PRO || product == USB_PRODUCT_8BITDO_SN30_PRO_BT || product == USB_PRODUCT_8BITDO_PRO_2 || - product == USB_PRODUCT_8BITDO_PRO_2_BT)) { + product == USB_PRODUCT_8BITDO_PRO_2_BT || + product == USB_PRODUCT_8BITDO_PRO_3)) { SDL_strlcat(mapping_string, "a:b1,b:b0,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b3,y:b2,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", sizeof(mapping_string)); if (product == USB_PRODUCT_8BITDO_PRO_2 || product == USB_PRODUCT_8BITDO_PRO_2_BT) { SDL_strlcat(mapping_string, "paddle1:b14,paddle2:b13,", sizeof(mapping_string)); + } else if (product == USB_PRODUCT_8BITDO_PRO_3) { + SDL_strlcat(mapping_string, "paddle1:b12,paddle2:b11,paddle3:b14,paddle4:b13,", sizeof(mapping_string)); } } else if (vendor == USB_VENDOR_8BITDO && (product == USB_PRODUCT_8BITDO_SF30_PRO || diff --git a/src/joystick/usb_ids.h b/src/joystick/usb_ids.h index ba75bbb8d6..35c92a5882 100644 --- a/src/joystick/usb_ids.h +++ b/src/joystick/usb_ids.h @@ -64,10 +64,10 @@ #define USB_PRODUCT_8BITDO_SF30_PRO_BT 0x6100 // B + START #define USB_PRODUCT_8BITDO_SN30_PRO 0x6001 // B + START #define USB_PRODUCT_8BITDO_SN30_PRO_BT 0x6101 // B + START -#define USB_PRODUCT_8BITDO_PRO_2 0x6003 // mode switch to D -#define USB_PRODUCT_8BITDO_PRO_2_BT 0x6006 // mode switch to D -#define USB_PRODUCT_8BITDO_PRO_3 0x6009 // mode switch to D -#define USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS 0x6012 // mode switch to BT +#define USB_PRODUCT_8BITDO_PRO_2 0x6003 // mode switch to D +#define USB_PRODUCT_8BITDO_PRO_2_BT 0x6006 // mode switch to D +#define USB_PRODUCT_8BITDO_PRO_3 0x6009 // mode switch to D +#define USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS 0x6012 // mode switch to BT #define USB_PRODUCT_AMAZON_LUNA_CONTROLLER 0x0419 #define USB_PRODUCT_ASTRO_C40_XBOX360 0x0024 #define USB_PRODUCT_BACKBONE_ONE_IOS 0x0103 From 1b65f254655ee2e97755b95e4d43bada72d1b3da Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Tue, 15 Jul 2025 15:52:40 -0700 Subject: [PATCH 039/103] testcontroller: use the correct label for face buttons --- test/gamepadutils.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/gamepadutils.c b/test/gamepadutils.c index c5295c4c54..3b68342223 100644 --- a/test/gamepadutils.c +++ b/test/gamepadutils.c @@ -369,6 +369,7 @@ struct GamepadImage bool showing_front; bool showing_touchpad; SDL_GamepadType type; + SDL_GamepadButtonLabel east_label; ControllerDisplayMode display_mode; bool elements[SDL_GAMEPAD_ELEMENT_MAX]; @@ -674,6 +675,7 @@ void UpdateGamepadImageFromGamepad(GamepadImage *ctx, SDL_Gamepad *gamepad) } ctx->type = SDL_GetGamepadType(gamepad); + ctx->east_label = SDL_GetGamepadButtonLabel(gamepad, SDL_GAMEPAD_BUTTON_EAST); char *mapping = SDL_GetGamepadMapping(gamepad); if (mapping) { if (SDL_strstr(mapping, "SDL_GAMECONTROLLER_USE_BUTTON_LABELS")) { @@ -795,7 +797,7 @@ void RenderGamepadImage(GamepadImage *ctx) dst.w = ctx->face_width; dst.h = ctx->face_height; - switch (SDL_GetGamepadButtonLabelForType(ctx->type, SDL_GAMEPAD_BUTTON_EAST)) { + switch (ctx->east_label) { case SDL_GAMEPAD_BUTTON_LABEL_B: SDL_RenderTexture(ctx->renderer, ctx->face_abxy_texture, NULL, &dst); break; From b3af72f69e1e958fd058eac35e77c1c06f0e406c Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Tue, 15 Jul 2025 15:48:22 -0400 Subject: [PATCH 040/103] emscripten: Respect SDL_HINT_MAIN_CALLBACK_RATE. Fixes #13345. --- src/main/emscripten/SDL_sysmain_callbacks.c | 67 ++++++++++++++++++- src/video/emscripten/SDL_emscriptenopengles.c | 13 ++-- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/src/main/emscripten/SDL_sysmain_callbacks.c b/src/main/emscripten/SDL_sysmain_callbacks.c index babffb3b75..059e910c74 100644 --- a/src/main/emscripten/SDL_sysmain_callbacks.c +++ b/src/main/emscripten/SDL_sysmain_callbacks.c @@ -24,9 +24,61 @@ #include +// For Emscripten, we let you use SDL_HINT_MAIN_CALLBACK_RATE, because it might be useful to drop it super-low for +// things like loopwave that don't really do much but wait on the audio device, but be warned that browser timers +// are super-unreliable in modern times, so you likely won't hit your desired callback rate with good precision. +// Almost all apps should leave this alone, so we can use requestAnimationFrame, which is intended to run reliably +// at the refresh rate of the user's display. +static Uint32 callback_rate_increment = 0; +static bool iterate_after_waitevent = false; +static bool callback_rate_changed = false; +static void SDLCALL MainCallbackRateHintChanged(void *userdata, const char *name, const char *oldValue, const char *newValue) +{ + callback_rate_changed = true; + iterate_after_waitevent = newValue && (SDL_strcmp(newValue, "waitevent") == 0); + if (iterate_after_waitevent) { + callback_rate_increment = 0; + } else { + const double callback_rate = newValue ? SDL_atof(newValue) : 0.0; + if (callback_rate > 0.0) { + callback_rate_increment = (Uint32) SDL_NS_TO_MS((double) SDL_NS_PER_SECOND / callback_rate); + } else { + callback_rate_increment = 0; + } + } +} + +// just tell us when any new event is pushed on the queue, so we can check a flag for "waitevent" mode. +static bool saw_new_event = false; +static bool SDLCALL EmscriptenMainCallbackEventWatcher(void *userdata, SDL_Event *event) +{ + saw_new_event = true; + return true; +} + static void EmscriptenInternalMainloop(void) { - const SDL_AppResult rc = SDL_IterateMainCallbacks(true); + // callback rate changed? Update emscripten's mainloop iteration speed. + if (callback_rate_changed) { + callback_rate_changed = false; + if (callback_rate_increment == 0) { + emscripten_set_main_loop_timing(EM_TIMING_RAF, 1); + } else { + emscripten_set_main_loop_timing(EM_TIMING_SETTIMEOUT, callback_rate_increment); + } + } + + if (iterate_after_waitevent) { + SDL_PumpEvents(); + if (!saw_new_event) { + // do nothing yet. Note that we're still going to iterate here because we can't block, + // but we can stop the app's iteration from progressing until there's an event. + return; + } + saw_new_event = false; + } + + const SDL_AppResult rc = SDL_IterateMainCallbacks(!iterate_after_waitevent); if (rc != SDL_APP_CONTINUE) { SDL_QuitMainCallbacks(rc); emscripten_cancel_main_loop(); // kill" the mainloop, so it stops calling back into it. @@ -36,9 +88,18 @@ static void EmscriptenInternalMainloop(void) int SDL_EnterAppMainCallbacks(int argc, char *argv[], SDL_AppInit_func appinit, SDL_AppIterate_func appiter, SDL_AppEvent_func appevent, SDL_AppQuit_func appquit) { - const SDL_AppResult rc = SDL_InitMainCallbacks(argc, argv, appinit, appiter, appevent, appquit); + SDL_AppResult rc = SDL_InitMainCallbacks(argc, argv, appinit, appiter, appevent, appquit); if (rc == SDL_APP_CONTINUE) { - emscripten_set_main_loop(EmscriptenInternalMainloop, 0, 0); // run at refresh rate, don't throw an exception since we do an orderly return. + if (!SDL_AddEventWatch(EmscriptenMainCallbackEventWatcher, NULL)) { + rc = SDL_APP_FAILURE; + } else { + SDL_AddHintCallback(SDL_HINT_MAIN_CALLBACK_RATE, MainCallbackRateHintChanged, NULL); + callback_rate_changed = false; + emscripten_set_main_loop(EmscriptenInternalMainloop, 0, 0); // don't throw an exception since we do an orderly return. + if (callback_rate_increment > 0.0) { + emscripten_set_main_loop_timing(EM_TIMING_SETTIMEOUT, callback_rate_increment); + } + } } else { SDL_QuitMainCallbacks(rc); } diff --git a/src/video/emscripten/SDL_emscriptenopengles.c b/src/video/emscripten/SDL_emscriptenopengles.c index bb490bb016..39faf2fb01 100644 --- a/src/video/emscripten/SDL_emscriptenopengles.c +++ b/src/video/emscripten/SDL_emscriptenopengles.c @@ -28,6 +28,7 @@ #include "SDL_emscriptenvideo.h" #include "SDL_emscriptenopengles.h" +#include "../../main/SDL_main_callbacks.h" bool Emscripten_GLES_LoadLibrary(SDL_VideoDevice *_this, const char *path) { @@ -50,10 +51,14 @@ bool Emscripten_GLES_SetSwapInterval(SDL_VideoDevice *_this, int interval) } if (Emscripten_ShouldSetSwapInterval(interval)) { - if (interval == 0) { - emscripten_set_main_loop_timing(EM_TIMING_SETTIMEOUT, 0); - } else { - emscripten_set_main_loop_timing(EM_TIMING_RAF, interval); + // don't change the mainloop timing if the app is also driving a main callback with this hint, + // as we assume that was the more deliberate action. + if (!SDL_HasMainCallbacks() || !SDL_GetHint(SDL_HINT_MAIN_CALLBACK_RATE)) { + if (interval == 0) { + emscripten_set_main_loop_timing(EM_TIMING_SETTIMEOUT, 0); + } else { + emscripten_set_main_loop_timing(EM_TIMING_RAF, interval); + } } } From a53eb5221b083c331e93074cf644773c829ec8b0 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Tue, 15 Jul 2025 17:41:32 -0700 Subject: [PATCH 041/103] Added support for the PDP REALMz Wireless Controller for Switch --- src/joystick/SDL_joystick.c | 1 + src/joystick/controller_list.h | 1 + src/joystick/hidapi/SDL_hidapi_switch.c | 9 ++++++--- src/joystick/usb_ids.h | 1 + 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c index 0e9fcab3b5..1e29015d6f 100644 --- a/src/joystick/SDL_joystick.c +++ b/src/joystick/SDL_joystick.c @@ -439,6 +439,7 @@ static Uint32 initial_blacklist_devices[] = { MAKE_VIDPID(0x04d9, 0x8009), // OBINLB USB-HID Keyboard (Anne Pro II) MAKE_VIDPID(0x04d9, 0xa292), // OBINLB USB-HID Keyboard (Anne Pro II) MAKE_VIDPID(0x04d9, 0xa293), // OBINLB USB-HID Keyboard (Anne Pro II) + MAKE_VIDPID(0x0e6f, 0x018a), // PDP REALMz Wireless Controller for Switch, USB charging MAKE_VIDPID(0x1532, 0x0266), // Razer Huntsman V2 Analog, non-functional DInput device MAKE_VIDPID(0x1532, 0x0282), // Razer Huntsman Mini Analog, non-functional DInput device MAKE_VIDPID(0x26ce, 0x01a2), // ASRock LED Controller diff --git a/src/joystick/controller_list.h b/src/joystick/controller_list.h index 61811f1f32..3d33d2236e 100644 --- a/src/joystick/controller_list.h +++ b/src/joystick/controller_list.h @@ -574,6 +574,7 @@ static const ControllerDescription_t arrControllers[] = { { MAKE_CONTROLLER_ID( 0x0e6f, 0x0186 ), k_eControllerType_SwitchProController, NULL }, // PDP Afterglow Wireless Switch Controller - working gyro. USB is for charging only. Many later "Wireless" line devices w/ gyro also use this vid/pid { MAKE_CONTROLLER_ID( 0x0e6f, 0x0187 ), k_eControllerType_SwitchInputOnlyController, NULL }, // PDP Rockcandy Wired Controller { MAKE_CONTROLLER_ID( 0x0e6f, 0x0188 ), k_eControllerType_SwitchInputOnlyController, NULL }, // PDP Afterglow Wired Deluxe+ Audio Controller + { MAKE_CONTROLLER_ID( 0x0e6f, 0x018c ), k_eControllerType_SwitchProController, "PDP REALMz Wireless Controller" }, // PDP REALMz Wireless Controller for Switch { MAKE_CONTROLLER_ID( 0x0f0d, 0x00aa ), k_eControllerType_SwitchInputOnlyController, NULL }, // HORI Real Arcade Pro V Hayabusa in Switch Mode { MAKE_CONTROLLER_ID( 0x20d6, 0xa711 ), k_eControllerType_SwitchInputOnlyController, NULL }, // PowerA Wired Controller Plus/PowerA Wired Controller Nintendo GameCube Style { MAKE_CONTROLLER_ID( 0x20d6, 0xa712 ), k_eControllerType_SwitchInputOnlyController, NULL }, // PowerA Nintendo Switch Fusion Fight Pad diff --git a/src/joystick/hidapi/SDL_hidapi_switch.c b/src/joystick/hidapi/SDL_hidapi_switch.c index 36865508b4..7038e4f405 100644 --- a/src/joystick/hidapi/SDL_hidapi_switch.c +++ b/src/joystick/hidapi/SDL_hidapi_switch.c @@ -34,7 +34,9 @@ #ifdef SDL_JOYSTICK_HIDAPI_SWITCH // Define this if you want to log all packets from the controller -// #define DEBUG_SWITCH_PROTOCOL +#if 0 +#define DEBUG_SWITCH_PROTOCOL +#endif // Define this to get log output for rumble logic // #define DEBUG_RUMBLE @@ -957,7 +959,7 @@ static bool LoadStickCalibration(SDL_DriverSwitch_Context *ctx) if (user_reply && user_reply->stickUserCalibration.rgucRightMagic[0] == 0xB2 && user_reply->stickUserCalibration.rgucRightMagic[1] == 0xA1) { userParamsReadSuccessCount += 1; pRightStickCal = user_reply->stickUserCalibration.rgucRightCalibration; - } + } // Only read the factory calibration info if we failed to receive the correct magic bytes if (userParamsReadSuccessCount < 2) { @@ -1583,7 +1585,8 @@ static bool HIDAPI_DriverSwitch_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joys ctx->m_eControllerType != k_eSwitchDeviceInfoControllerType_NESRight && ctx->m_eControllerType != k_eSwitchDeviceInfoControllerType_SNES && ctx->m_eControllerType != k_eSwitchDeviceInfoControllerType_N64 && - ctx->m_eControllerType != k_eSwitchDeviceInfoControllerType_SEGA_Genesis) { + ctx->m_eControllerType != k_eSwitchDeviceInfoControllerType_SEGA_Genesis && + !(device->vendor_id == USB_VENDOR_PDP && device->product_id == USB_PRODUCT_PDP_REALMZ_WIRELESS)) { if (LoadIMUCalibration(ctx)) { ctx->m_bSensorsSupported = true; } diff --git a/src/joystick/usb_ids.h b/src/joystick/usb_ids.h index 35c92a5882..14b5fb52b0 100644 --- a/src/joystick/usb_ids.h +++ b/src/joystick/usb_ids.h @@ -107,6 +107,7 @@ #define USB_PRODUCT_NVIDIA_SHIELD_CONTROLLER_V103 0x7210 #define USB_PRODUCT_NVIDIA_SHIELD_CONTROLLER_V104 0x7214 #define USB_PRODUCT_PDP_ROCK_CANDY 0x0246 +#define USB_PRODUCT_PDP_REALMZ_WIRELESS 0x018c #define USB_PRODUCT_POWERA_MINI 0x541a #define USB_PRODUCT_RAZER_ATROX 0x0a00 #define USB_PRODUCT_RAZER_KITSUNE 0x1012 From 18eeaea054c9b0708afefabcffcee7b5b2d6cf5d Mon Sep 17 00:00:00 2001 From: mitchellcairns Date: Tue, 15 Jul 2025 18:35:47 -0700 Subject: [PATCH 042/103] Implement SInput Device Support (#13343) This implements a new SDL HID driver for a format developed by Hand Held Legend for their gamepad devices called SInput Devices that are supported by this change with well-defined mappings GC Ultimate ( https://gcultimate.com ) ProGCC ( https://handheldlegend.com/products/progcc-kit-wireless-wired-bundle ) The SInput format is documented here: https://github.com/HandHeldLegend/SInput-HID --- VisualC-GDK/SDL/SDL.vcxproj | 1 + VisualC-GDK/SDL/SDL.vcxproj.filters | 1 + VisualC/SDL/SDL.vcxproj | 3 +- VisualC/SDL/SDL.vcxproj.filters | 5 +- Xcode/SDL/SDL.xcodeproj/project.pbxproj | 4 + include/SDL3/SDL_hints.h | 12 + src/joystick/SDL_gamepad.c | 49 ++ src/joystick/SDL_joystick.c | 11 + src/joystick/SDL_joystick_c.h | 3 + src/joystick/hidapi/SDL_hidapi_sinput.c | 809 +++++++++++++++++++++ src/joystick/hidapi/SDL_hidapijoystick.c | 3 + src/joystick/hidapi/SDL_hidapijoystick_c.h | 2 + src/joystick/usb_ids.h | 5 + 13 files changed, 906 insertions(+), 2 deletions(-) create mode 100644 src/joystick/hidapi/SDL_hidapi_sinput.c diff --git a/VisualC-GDK/SDL/SDL.vcxproj b/VisualC-GDK/SDL/SDL.vcxproj index 58194e7246..a9024fc683 100644 --- a/VisualC-GDK/SDL/SDL.vcxproj +++ b/VisualC-GDK/SDL/SDL.vcxproj @@ -723,6 +723,7 @@ + diff --git a/VisualC-GDK/SDL/SDL.vcxproj.filters b/VisualC-GDK/SDL/SDL.vcxproj.filters index 8a988ace96..9a0ce21537 100644 --- a/VisualC-GDK/SDL/SDL.vcxproj.filters +++ b/VisualC-GDK/SDL/SDL.vcxproj.filters @@ -74,6 +74,7 @@ + diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj index 843f8e61ce..f59c31f9e2 100644 --- a/VisualC/SDL/SDL.vcxproj +++ b/VisualC/SDL/SDL.vcxproj @@ -613,6 +613,7 @@ + @@ -774,4 +775,4 @@ - + \ No newline at end of file diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters index 375c175c0e..4a1b31c884 100644 --- a/VisualC/SDL/SDL.vcxproj.filters +++ b/VisualC/SDL/SDL.vcxproj.filters @@ -1215,6 +1215,9 @@ joystick\hidapi + + joystick\hidapi + joystick\hidapi @@ -1615,4 +1618,4 @@ - + \ No newline at end of file diff --git a/Xcode/SDL/SDL.xcodeproj/project.pbxproj b/Xcode/SDL/SDL.xcodeproj/project.pbxproj index ffe8fa75a6..a6434e435a 100644 --- a/Xcode/SDL/SDL.xcodeproj/project.pbxproj +++ b/Xcode/SDL/SDL.xcodeproj/project.pbxproj @@ -76,6 +76,7 @@ 89E580242D03606400DAF6D3 /* SDL_hidapihaptic_lg4ff.c in Sources */ = {isa = PBXBuildFile; fileRef = 89E580212D03606400DAF6D3 /* SDL_hidapihaptic_lg4ff.c */; }; 89E580252D03606400DAF6D3 /* SDL_hidapihaptic_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 89E580202D03606400DAF6D3 /* SDL_hidapihaptic_c.h */; }; 9846B07C287A9020000C35C8 /* SDL_hidapi_shield.c in Sources */ = {isa = PBXBuildFile; fileRef = 9846B07B287A9020000C35C8 /* SDL_hidapi_shield.c */; }; + 02D6A1C228A84B8F00A7F002 /* SDL_hidapi_sinput.c in Sources */ = {isa = PBXBuildFile; fileRef = 02D6A1C128A84B8F00A7F001 /* SDL_hidapi_sinput.c */; }; A1626A3E2617006A003F1973 /* SDL_triangle.c in Sources */ = {isa = PBXBuildFile; fileRef = A1626A3D2617006A003F1973 /* SDL_triangle.c */; }; A1626A522617008D003F1973 /* SDL_triangle.h in Headers */ = {isa = PBXBuildFile; fileRef = A1626A512617008C003F1973 /* SDL_triangle.h */; }; A1BB8B6327F6CF330057CFA8 /* SDL_list.c in Sources */ = {isa = PBXBuildFile; fileRef = A1BB8B6127F6CF320057CFA8 /* SDL_list.c */; }; @@ -620,6 +621,7 @@ 89E580202D03606400DAF6D3 /* SDL_hidapihaptic_c.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDL_hidapihaptic_c.h; sourceTree = ""; }; 89E580212D03606400DAF6D3 /* SDL_hidapihaptic_lg4ff.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapihaptic_lg4ff.c; sourceTree = ""; }; 9846B07B287A9020000C35C8 /* SDL_hidapi_shield.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_shield.c; sourceTree = ""; }; + 02D6A1C128A84B8F00A7F001 /* SDL_hidapi_sinput.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_sinput.c; sourceTree = ""; }; A1626A3D2617006A003F1973 /* SDL_triangle.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_triangle.c; sourceTree = ""; }; A1626A512617008C003F1973 /* SDL_triangle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_triangle.h; sourceTree = ""; }; A1BB8B6127F6CF320057CFA8 /* SDL_list.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_list.c; sourceTree = ""; }; @@ -1943,6 +1945,7 @@ A75FDBC323EA380300529352 /* SDL_hidapi_rumble.h */, A75FDBC423EA380300529352 /* SDL_hidapi_rumble.c */, 9846B07B287A9020000C35C8 /* SDL_hidapi_shield.c */, + 02D6A1C128A84B8F00A7F001 /* SDL_hidapi_sinput.c */, F3984CCF25BCC92800374F43 /* SDL_hidapi_stadia.c */, A75FDAAC23E2795C00529352 /* SDL_hidapi_steam.c */, F3FD042D2C9B755700824C4C /* SDL_hidapi_steam_hori.c */, @@ -2877,6 +2880,7 @@ A7D8B62F23E2514300DCD162 /* SDL_sysfilesystem.m in Sources */, A7D8B41C23E2514300DCD162 /* SDL_systls.c in Sources */, 9846B07C287A9020000C35C8 /* SDL_hidapi_shield.c in Sources */, + 02D6A1C228A84B8F00A7F002 /* SDL_hidapi_sinput.c in Sources */, F31013C72C24E98200FBE946 /* SDL_keymap.c in Sources */, F3A9AE992C8A13C100AAC390 /* SDL_render_gpu.c in Sources */, A7D8BBD923E2574800DCD162 /* SDL_uikitmessagebox.m in Sources */, diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h index 0900b36eb4..878dd2d486 100644 --- a/include/SDL3/SDL_hints.h +++ b/include/SDL3/SDL_hints.h @@ -1746,6 +1746,18 @@ extern "C" { */ #define SDL_HINT_JOYSTICK_HIDAPI_8BITDO "SDL_JOYSTICK_HIDAPI_8BITDO" +/** + * A variable controlling whether the HIDAPI driver for SInput controllers + * should be used. More info - https://github.com/HandHeldLegend/SInput-HID + * + * This variable can be set to the following values: + * + * "0" - HIDAPI driver is not used. "1" - HIDAPI driver is used. + * + * The default is the value of SDL_HINT_JOYSTICK_HIDAPI + */ +#define SDL_HINT_JOYSTICK_HIDAPI_SINPUT "SDL_JOYSTICK_HIDAPI_SINPUT" + /** * A variable controlling whether the HIDAPI driver for Flydigi controllers * should be used. diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index 28aaf6a23d..59d50bffa2 100644 --- a/src/joystick/SDL_gamepad.c +++ b/src/joystick/SDL_gamepad.c @@ -798,6 +798,54 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid) product == USB_PRODUCT_8BITDO_SF30_PRO_BT)) { // This controller has no guide button SDL_strlcat(mapping_string, "a:b1,b:b0,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b3,y:b2,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", sizeof(mapping_string)); + } else if (SDL_IsJoystickSInputController(vendor, product)) { + Uint8 face_style = (guid.data[15] & 0xF0) >> 4; + Uint8 u_id = guid.data[15] & 0x0F; + + switch (product) { + case USB_PRODUCT_HANDHELDLEGEND_PROGCC: + // ProGCC Mapping + SDL_strlcat(mapping_string, "a:b1,b:b0,back:b15,dpdown:b5,dpleft:b6,dpright:b7,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b8,lefttrigger:b12,leftx:a0,lefty:a1,misc1:b17,rightshoulder:b11,rightstick:b9,righttrigger:b13,rightx:a2,righty:a3,start:b14,x:b3,y:b2,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", sizeof(mapping_string)); + break; + + case USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE: + // GC Ultimate Map + SDL_strlcat(mapping_string, "a:b0,b:b2,back:b15,dpdown:b5,dpleft:b6,dpright:b7,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b8,lefttrigger:a4,leftx:a0,lefty:a1,misc1:b17,misc3:b18,paddle1:b13,paddle2:b12,rightshoulder:b11,rightstick:b9,righttrigger:a5,rightx:a2,righty:a3,start:b14,x:b1,y:b3,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", sizeof(mapping_string)); + break; + + case USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC: + if (u_id != 1) { + return NULL; + } + + // SuperGamepad+ Map + if (u_id == 1) { + SDL_strlcat(mapping_string, "a:b1,b:b0,back:b11,dpdown:b5,dpleft:b6,dpright:b7,dpup:b4,leftshoulder:b8,rightshoulder:b9,start:b10,x:b3,y:b2,", sizeof(mapping_string)); + } + + // Apply face style + switch (face_style) { + default: + case 1: + SDL_strlcat(mapping_string, "face:abxy,", sizeof(mapping_string)); + break; + case 2: + SDL_strlcat(mapping_string, "face:axby,", sizeof(mapping_string)); + break; + case 3: + SDL_strlcat(mapping_string, "face:bayx,", sizeof(mapping_string)); + break; + case 4: + SDL_strlcat(mapping_string, "face:sony,", sizeof(mapping_string)); + break; + } + break; + + default: + case USB_PRODUCT_BONJIRICHANNEL_FIREBIRD: + // Unmapped devices + return NULL; + } } else { // All other gamepads have the standard set of 19 buttons and 6 axes if (SDL_IsJoystickGameCube(vendor, product)) { @@ -1235,6 +1283,7 @@ static bool SDL_PrivateParseGamepadElement(SDL_Gamepad *gamepad, const char *szG if (SDL_strstr(gamepad->mapping->mapping, ",hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1") != NULL) { baxy_mapping = true; } + // FIXME: We fix these up when loading the mapping, does this ever get hit? //SDL_assert(!axby_mapping && !baxy_mapping); diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c index 1e29015d6f..0a1b13c338 100644 --- a/src/joystick/SDL_joystick.c +++ b/src/joystick/SDL_joystick.c @@ -3202,6 +3202,17 @@ bool SDL_IsJoystickHoriSteamController(Uint16 vendor_id, Uint16 product_id) return vendor_id == USB_VENDOR_HORI && (product_id == USB_PRODUCT_HORI_STEAM_CONTROLLER || product_id == USB_PRODUCT_HORI_STEAM_CONTROLLER_BT); } +bool SDL_IsJoystickSInputController(Uint16 vendor_id, Uint16 product_id) +{ + bool vendor_match = (vendor_id == USB_VENDOR_RASPBERRYPI); + bool product_match = + (product_id == USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC) | + (product_id == USB_PRODUCT_HANDHELDLEGEND_PROGCC) | + (product_id == USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE) | + (product_id == USB_PRODUCT_BONJIRICHANNEL_FIREBIRD); + return (vendor_match && product_match); +} + bool SDL_IsJoystickFlydigiController(Uint16 vendor_id, Uint16 product_id) { return vendor_id == USB_VENDOR_FLYDIGI && product_id == USB_PRODUCT_FLYDIGI_GAMEPAD; diff --git a/src/joystick/SDL_joystick_c.h b/src/joystick/SDL_joystick_c.h index cbc33608c4..c6e1a7b792 100644 --- a/src/joystick/SDL_joystick_c.h +++ b/src/joystick/SDL_joystick_c.h @@ -135,6 +135,9 @@ extern bool SDL_IsJoystickSteamController(Uint16 vendor_id, Uint16 product_id); // Function to return whether a joystick is a HORI Steam controller extern bool SDL_IsJoystickHoriSteamController(Uint16 vendor_id, Uint16 product_id); +// Function to return whether a joystick is an SInput (Open Format) controller +extern bool SDL_IsJoystickSInputController(Uint16 vendor_id, Uint16 product_id); + // Function to return whether a joystick is a Flydigi controller extern bool SDL_IsJoystickFlydigiController(Uint16 vendor_id, Uint16 product_id); diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c new file mode 100644 index 0000000000..142e45c1ef --- /dev/null +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -0,0 +1,809 @@ +/* + Simple DirectMedia Layer + Copyright (C) 2025 Mitchell Cairns + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_JOYSTICK_HIDAPI + +#include "../../SDL_hints_c.h" +#include "../SDL_sysjoystick.h" + +#include "SDL_hidapijoystick_c.h" +#include "SDL_hidapi_rumble.h" + +#ifdef SDL_JOYSTICK_HIDAPI_SINPUT + +/*****************************************************************************************************/ + +// Define this if you want to log all packets from the controller +#if 0 +#define DEBUG_SINPUT_PROTOCOL +#endif + +#if 0 +#define DEBUG_SINPUT_INIT +#endif + +#define SINPUT_DEVICE_REPORT_SIZE 64 // Size of input reports (And CMD Input reports) +#define SINPUT_DEVICE_REPORT_COMMAND_SIZE 48 // Size of command OUTPUT reports + +#define SINPUT_DEVICE_REPORT_ID_JOYSTICK_INPUT 0x01 +#define SINPUT_DEVICE_REPORT_ID_INPUT_CMDDAT 0x02 +#define SINPUT_DEVICE_REPORT_ID_OUTPUT_CMDDAT 0x03 + +#define SINPUT_DEVICE_COMMAND_HAPTIC 0x01 +#define SINPUT_DEVICE_COMMAND_FEATURES 0x02 +#define SINPUT_DEVICE_COMMAND_PLAYERLED 0x03 +#define SINPUT_DEVICE_COMMAND_JOYSTICKRGB 0x04 + +#define SINPUT_HAPTIC_TYPE_PRECISE 0x01 +#define SINPUT_HAPTIC_TYPE_ERMSIMULATION 0x02 + +#define SINPUT_DEFAULT_GYRO_SENS 2000 +#define SINPUT_DEFAULT_ACCEL_SENS 8 + +#define SINPUT_REPORT_IDX_BUTTONS_0 3 +#define SINPUT_REPORT_IDX_BUTTONS_1 4 +#define SINPUT_REPORT_IDX_BUTTONS_2 5 +#define SINPUT_REPORT_IDX_BUTTONS_3 6 +#define SINPUT_REPORT_IDX_LEFT_X 7 +#define SINPUT_REPORT_IDX_LEFT_Y 9 +#define SINPUT_REPORT_IDX_RIGHT_X 11 +#define SINPUT_REPORT_IDX_RIGHT_Y 13 +#define SINPUT_REPORT_IDX_LEFT_TRIGGER 15 +#define SINPUT_REPORT_IDX_RIGHT_TRIGGER 17 +#define SINPUT_REPORT_IDX_IMU_TIMESTAMP 19 +#define SINPUT_REPORT_IDX_IMU_ACCEL_X 21 +#define SINPUT_REPORT_IDX_IMU_ACCEL_Y 23 +#define SINPUT_REPORT_IDX_IMU_ACCEL_Z 25 +#define SINPUT_REPORT_IDX_IMU_GYRO_X 27 +#define SINPUT_REPORT_IDX_IMU_GYRO_Y 29 +#define SINPUT_REPORT_IDX_IMU_GYRO_Z 31 +#define SINPUT_REPORT_IDX_TOUCH1_X 33 +#define SINPUT_REPORT_IDX_TOUCH1_Y 35 +#define SINPUT_REPORT_IDX_TOUCH1_P 37 +#define SINPUT_REPORT_IDX_TOUCH2_X 39 +#define SINPUT_REPORT_IDX_TOUCH2_Y 41 +#define SINPUT_REPORT_IDX_TOUCH2_P 43 + +#define SINPUT_REPORT_IDX_COMMAND_RESPONSE_ID 1 +#define SINPUT_REPORT_IDX_COMMAND_RESPONSE_BULK 2 + +#define SINPUT_REPORT_IDX_PLUG_STATUS 1 +#define SINPUT_REPORT_IDX_CHARGE_LEVEL 2 + +#define SINPUT_MAX_ALLOWED_TOUCHPADS 2 + +#ifndef EXTRACTSINT16 +#define EXTRACTSINT16(data, idx) ((Sint16)((data)[(idx)] | ((data)[(idx) + 1] << 8))) +#endif + +#ifndef EXTRACTUINT16 +#define EXTRACTUINT16(data, idx) ((Uint16)((data)[(idx)] | ((data)[(idx) + 1] << 8))) +#endif + + +typedef struct +{ + uint8_t type; + + union { + // Frequency Amplitude pairs + struct { + struct { + uint16_t frequency_1; + uint16_t amplitude_1; + uint16_t frequency_2; + uint16_t amplitude_2; + } left; + + struct { + uint16_t frequency_1; + uint16_t amplitude_1; + uint16_t frequency_2; + uint16_t amplitude_2; + } right; + + } type_1; + + // Basic ERM simulation model + struct { + struct { + uint8_t amplitude; + bool brake; + } left; + + struct { + uint8_t amplitude; + bool brake; + } right; + + } type_2; + }; +} SINPUT_HAPTIC_S; + +typedef struct +{ + SDL_HIDAPI_Device *device; + bool sensors_enabled; + + Uint8 player_idx; + + bool player_leds_supported; + bool joystick_rgb_supported; + bool rumble_supported; + bool accelerometer_supported; + bool gyroscope_supported; + bool left_analog_stick_supported; + bool right_analog_stick_supported; + bool left_analog_trigger_supported; + bool right_analog_trigger_supported; + bool touchpad_supported; + + Uint8 touchpad_count; // 2 touchpads maximum + Uint8 touchpad_finger_count; // 2 fingers for one touchpad, or 1 per touchpad (2 max) + + Uint8 polling_rate_ms; + Uint8 sub_type; // Subtype of the device, 0 in most cases + + Uint16 accelRange; // Example would be 2,4,8,16 +/- (g-force) + Uint16 gyroRange; // Example would be 1000,2000,4000 +/- (degrees per second) + + float accelScale; // Scale factor for accelerometer values + float gyroScale; // Scale factor for gyroscope values + Uint8 last_state[USB_PACKET_LENGTH]; + + Uint8 buttons_count; + Uint8 usage_masks[4]; + + Uint64 imu_timestamp; // Nanoseconds. We accumulate with received deltas +} SDL_DriverSInput_Context; + +// Converts raw int16_t gyro scale setting +static inline float CalculateGyroScale(uint16_t dps_range) +{ + return SDL_PI_F / 180.0f / (32768.0f / (float)dps_range); +} + +// Converts raw int16_t accel scale setting +static inline float CalculateAccelScale(uint16_t g_range) +{ + return SDL_STANDARD_GRAVITY / (32768.0f / (float)g_range); +} + +static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) +{ + SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)device->context; + + // Bitfields are not portable, so we unpack them into a struct value + ctx->rumble_supported = (data[0] & 0x01) != 0; + ctx->player_leds_supported = (data[0] & 0x02) != 0; + ctx->accelerometer_supported = (data[0] & 0x04) != 0; + ctx->gyroscope_supported = (data[0] & 0x08) != 0; + + ctx->left_analog_stick_supported = (data[0] & 0x10) != 0; + ctx->right_analog_stick_supported = (data[0] & 0x20) != 0; + ctx->left_analog_trigger_supported = (data[0] & 0x40) != 0; + ctx->right_analog_trigger_supported = (data[0] & 0x80) != 0; + + ctx->touchpad_supported = (data[1] & 0x01) != 0; + ctx->joystick_rgb_supported = (data[1] & 0x02) != 0; + + SDL_GamepadType type = SDL_GAMEPAD_TYPE_UNKNOWN; + type = (SDL_GamepadType)SDL_clamp(data[2], SDL_GAMEPAD_TYPE_UNKNOWN, SDL_GAMEPAD_TYPE_COUNT); + device->type = type; + + // The 4 MSB represent a button layout style SDL_GamepadFaceStyle + // The 4 LSB represent a device sub-type + device->guid.data[15] = data[3]; + +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("SInput Face Style: %d", (data[3] & 0xF0) >> 4); + SDL_Log("SInput Sub-type: %d", (data[3] & 0xF)); +#endif + + ctx->polling_rate_ms = data[4]; + + ctx->accelRange = EXTRACTUINT16(data, 6); + ctx->gyroRange = EXTRACTUINT16(data, 8); + + // Masks in LSB to MSB + // South, East, West, North, DUp, DDown, DLeft, DRight + ctx->usage_masks[0] = data[10]; + + // Stick Left, Stick Right, L Shoulder, R Shoulder, + // L Trigger, R Trigger, L Paddle 1, R Paddle 1 + ctx->usage_masks[1] = data[11]; + + // Start, Back, Guide, Capture, L Paddle 2, R Paddle 2, Touchpad L, Touchpad R + ctx->usage_masks[2] = data[12]; + + // Power, Misc 4 to 10 + ctx->usage_masks[3] = data[13]; + + // Derive button count from mask + for (Uint8 byte = 0; byte < 4; ++byte) { + for (Uint8 bit = 0; bit < 8; ++bit) { + if ((ctx->usage_masks[byte] & (1 << bit)) != 0) { + ++ctx->buttons_count; + } + } + } + +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("Buttons count: %d", ctx->buttons_count); +#endif + + // Get and validate touchpad parameters + ctx->touchpad_count = data[14]; + ctx->touchpad_finger_count = data[15]; + +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("Accelerometer Range: %d", ctx->accelRange); +#endif + +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("Gyro Range: %d", ctx->gyroRange); +#endif + + ctx->accelScale = CalculateAccelScale(ctx->accelRange); + ctx->gyroScale = CalculateGyroScale(ctx->gyroRange); +} + +static bool RetrieveSDLFeatures(SDL_HIDAPI_Device *device) +{ + int written = 0; + + // Attempt to send the SDL features get command. + for (int attempt = 0; attempt < 8; ++attempt) { + const Uint8 featuresGetCommand[SINPUT_DEVICE_REPORT_COMMAND_SIZE] = { SINPUT_DEVICE_REPORT_ID_OUTPUT_CMDDAT, SINPUT_DEVICE_COMMAND_FEATURES }; + // This write will occasionally return -1, so ignore failure here and try again + written = SDL_hid_write(device->dev, featuresGetCommand, sizeof(featuresGetCommand)); + + if (written == SINPUT_DEVICE_REPORT_COMMAND_SIZE) { + break; + } + } + + if (written < SINPUT_DEVICE_REPORT_COMMAND_SIZE) { + SDL_SetError("SInput device SDL Features GET command could not write"); + return false; + } + + int read = 0; + + // Read the reply + for (int i = 0; i < 100; ++i) { + SDL_Delay(1); + + Uint8 data[USB_PACKET_LENGTH]; + read = SDL_hid_read_timeout(device->dev, data, sizeof(data), 0); + if (read < 0) { + SDL_SetError("SInput device SDL Features GET command could not read"); + return false; + } + if (read == 0) { + continue; + } + +#ifdef DEBUG_SINPUT_PROTOCOL + HIDAPI_DumpPacket("SInput packet: size = %d", data, size); +#endif + + if ((read == USB_PACKET_LENGTH) && (data[0] == SINPUT_DEVICE_REPORT_ID_INPUT_CMDDAT) && (data[1] == SINPUT_DEVICE_COMMAND_FEATURES)) { + ProcessSDLFeaturesResponse(device, &(data[SINPUT_REPORT_IDX_COMMAND_RESPONSE_BULK])); +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("Received SInput SDL Features command response"); +#endif + return true; + } + } + + return false; +} + +// Type 2 haptics are for more traditional rumble such as +// ERM motors or simulated ERM motors +static inline void HapticsType2Pack(SINPUT_HAPTIC_S *in, Uint8 *out) +{ + // Type of haptics + out[0] = 2; + + out[1] = in->type_2.left.amplitude; + out[2] = in->type_2.left.brake; + + out[3] = in->type_2.right.amplitude; + out[4] = in->type_2.right.brake; +} + +static void HIDAPI_DriverSInput_RegisterHints(SDL_HintCallback callback, void *userdata) +{ + SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_SINPUT, callback, userdata); +} + +static void HIDAPI_DriverSInput_UnregisterHints(SDL_HintCallback callback, void *userdata) +{ + SDL_RemoveHintCallback(SDL_HINT_JOYSTICK_HIDAPI_SINPUT, callback, userdata); +} + +static bool HIDAPI_DriverSInput_IsEnabled(void) +{ + return SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_SINPUT, SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI, SDL_HIDAPI_DEFAULT)); +} + +static bool HIDAPI_DriverSInput_IsSupportedDevice(SDL_HIDAPI_Device *device, const char *name, SDL_GamepadType type, Uint16 vendor_id, Uint16 product_id, Uint16 version, int interface_number, int interface_class, int interface_subclass, int interface_protocol) +{ + return SDL_IsJoystickSInputController(vendor_id, product_id); +} + +static bool HIDAPI_DriverSInput_InitDevice(SDL_HIDAPI_Device *device) +{ +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("SInput device Init"); +#endif + + SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)SDL_calloc(1, sizeof(*ctx)); + if (!ctx) { + return false; + } + + ctx->device = device; + device->context = ctx; + + if (!RetrieveSDLFeatures(device)) { + return false; + } + + return HIDAPI_JoystickConnected(device, NULL); +} + +static int HIDAPI_DriverSInput_GetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id) +{ + return -1; +} + +static void HIDAPI_DriverSInput_SetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id, int player_index) +{ + SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)device->context; + + if (ctx->player_leds_supported) { + player_index = SDL_clamp(player_index + 1, 0, 255); + Uint8 player_num = (Uint8)player_index; + + ctx->player_idx = player_num; + + // Set player number, finalizing the setup + Uint8 playerLedCommand[SINPUT_DEVICE_REPORT_COMMAND_SIZE] = { SINPUT_DEVICE_REPORT_ID_OUTPUT_CMDDAT, SINPUT_DEVICE_COMMAND_PLAYERLED, ctx->player_idx }; + int playerNumBytesWritten = SDL_hid_write(device->dev, playerLedCommand, SINPUT_DEVICE_REPORT_COMMAND_SIZE); + + if (playerNumBytesWritten < 0) { + SDL_SetError("SInput device player led command could not write"); + } + } +} + +#ifndef DEG2RAD +#define DEG2RAD(x) ((float)(x) * (float)(SDL_PI_F / 180.f)) +#endif + + +static bool HIDAPI_DriverSInput_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick) +{ +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("SInput device Open"); +#endif + + SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)device->context; + + SDL_AssertJoysticksLocked(); + + joystick->nbuttons = ctx->buttons_count; + + SDL_zeroa(ctx->last_state); + + int axes = 0; + if (ctx->left_analog_stick_supported) { + axes += 2; + } + + if (ctx->right_analog_stick_supported) { + axes += 2; + } + + if (ctx->left_analog_trigger_supported) { + ++axes; + } + + if (ctx->right_analog_trigger_supported) { + ++axes; + } + + joystick->naxes = axes; + + if (ctx->accelerometer_supported) { + SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL, (float)1000.0f/ctx->polling_rate_ms); + } + + if (ctx->gyroscope_supported) { + SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_GYRO, (float)1000.0f / ctx->polling_rate_ms); + } + + if (ctx->touchpad_supported) { + // If touchpad is supported, minimum 1, max is capped + ctx->touchpad_count = SDL_clamp(ctx->touchpad_count, 1, SINPUT_MAX_ALLOWED_TOUCHPADS); + + if (ctx->touchpad_count > 1) { + // Support two separate touchpads with 1 finger each + // or support one touchpad with 2 fingers max + ctx->touchpad_finger_count = 1; + } + + if (ctx->touchpad_count > 0) { + SDL_PrivateJoystickAddTouchpad(joystick, ctx->touchpad_finger_count); + } + + if (ctx->touchpad_count > 1) { + SDL_PrivateJoystickAddTouchpad(joystick, ctx->touchpad_finger_count); + } + } + + return true; +} + +static bool HIDAPI_DriverSInput_RumbleJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble) +{ + + SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)device->context; + + if (ctx->rumble_supported) { + SINPUT_HAPTIC_S hapticData = { 0 }; + Uint8 hapticReport[SINPUT_DEVICE_REPORT_COMMAND_SIZE] = { SINPUT_DEVICE_REPORT_ID_OUTPUT_CMDDAT, SINPUT_DEVICE_COMMAND_HAPTIC }; + + // Low Frequency = Left + // High Frequency = Right + hapticData.type_2.left.amplitude = (Uint8) (low_frequency_rumble >> 8); + hapticData.type_2.right.amplitude = (Uint8)(high_frequency_rumble >> 8); + + HapticsType2Pack(&hapticData, &(hapticReport[2])); + + SDL_HIDAPI_SendRumble(device, hapticReport, SINPUT_DEVICE_REPORT_COMMAND_SIZE); + + return true; + } + + return SDL_Unsupported(); +} + +static bool HIDAPI_DriverSInput_RumbleJoystickTriggers(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble) +{ + return SDL_Unsupported(); +} + +static Uint32 HIDAPI_DriverSInput_GetJoystickCapabilities(SDL_HIDAPI_Device *device, SDL_Joystick *joystick) +{ + SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)device->context; + + Uint32 caps = 0; + if (ctx->rumble_supported) { + caps |= SDL_JOYSTICK_CAP_RUMBLE; + } + + if (ctx->player_leds_supported) { + caps |= SDL_JOYSTICK_CAP_PLAYER_LED; + } + + if (ctx->joystick_rgb_supported) { + caps |= SDL_JOYSTICK_CAP_RGB_LED; + } + + return caps; +} + +static bool HIDAPI_DriverSInput_SetJoystickLED(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue) +{ + SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)device->context; + + if (ctx->player_leds_supported) { + + // Set player number, finalizing the setup + Uint8 joystickRGBCommand[SINPUT_DEVICE_REPORT_COMMAND_SIZE] = { SINPUT_DEVICE_REPORT_ID_OUTPUT_CMDDAT, SINPUT_DEVICE_COMMAND_JOYSTICKRGB, red, green, blue }; + int joystickRGBBytesWritten = SDL_hid_write(device->dev, joystickRGBCommand, SINPUT_DEVICE_REPORT_COMMAND_SIZE); + + if (joystickRGBBytesWritten < 0) { + SDL_SetError("SInput device joystick rgb command could not write"); + return false; + } + + return true; + } + return SDL_Unsupported(); +} + +static bool HIDAPI_DriverSInput_SendJoystickEffect(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, const void *data, int size) +{ + return SDL_Unsupported(); +} + +static bool HIDAPI_DriverSInput_SetJoystickSensorsEnabled(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, bool enabled) +{ + SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)device->context; + + if (ctx->accelerometer_supported || ctx->gyroscope_supported) { + ctx->sensors_enabled = enabled; + return true; + } + return SDL_Unsupported(); +} + +static void HIDAPI_DriverSInput_HandleStatePacket(SDL_Joystick *joystick, SDL_DriverSInput_Context *ctx, Uint8 *data, int size) +{ + Sint16 axis = 0; + Sint16 accel = 0; + Sint16 gyro = 0; + Uint64 timestamp = SDL_GetTicksNS(); + float imu_values[3] = { 0 }; + Uint8 output_idx = 0; + + // Process digital buttons according to the supplied + // button mask to create a contiguous button input set + for (Uint8 processes = 0; processes < 4; ++processes) { + + Uint8 button_idx = SINPUT_REPORT_IDX_BUTTONS_0 + processes; + + for (Uint8 buttons = 0; buttons < 8; ++buttons) { + + // If a button is enabled by our usage mask + const Uint8 mask = (0x01 << buttons); + if ((ctx->usage_masks[processes] & mask) != 0) { + + bool down = (data[button_idx] & mask) != 0; + + if ( (output_idx < SDL_GAMEPAD_BUTTON_COUNT) && (ctx->last_state[button_idx] != data[button_idx]) ) { + SDL_SendJoystickButton(timestamp, joystick, output_idx, down); + } + + ++output_idx; + } + } + } + + // Analog inputs map to a signed Sint16 range of -32768 to 32767 from the device. + // Use an axis index because not all gamepads will have the same axis inputs. + Uint8 axis_idx = 0; + + // Left Analog Stick + axis = 0; // Reset axis value for joystick + if (ctx->left_analog_stick_supported) { + axis = EXTRACTSINT16(data, SINPUT_REPORT_IDX_LEFT_X); + SDL_SendJoystickAxis(timestamp, joystick, axis_idx, axis); + ++axis_idx; + + axis = EXTRACTSINT16(data, SINPUT_REPORT_IDX_LEFT_Y); + SDL_SendJoystickAxis(timestamp, joystick, axis_idx, axis); + ++axis_idx; + } + + // Right Analog Stick + axis = 0; // Reset axis value for joystick + if (ctx->right_analog_stick_supported) { + axis = EXTRACTSINT16(data, SINPUT_REPORT_IDX_RIGHT_X); + SDL_SendJoystickAxis(timestamp, joystick, axis_idx, axis); + ++axis_idx; + + axis = EXTRACTSINT16(data, SINPUT_REPORT_IDX_RIGHT_Y); + SDL_SendJoystickAxis(timestamp, joystick, axis_idx, axis); + ++axis_idx; + } + + // Left Analog Trigger + axis = SDL_MIN_SINT16; // Reset axis value for trigger + if (ctx->left_analog_trigger_supported) { + axis = EXTRACTSINT16(data, SINPUT_REPORT_IDX_LEFT_TRIGGER); + SDL_SendJoystickAxis(timestamp, joystick, axis_idx, axis); + ++axis_idx; + } + + // Right Analog Trigger + axis = SDL_MIN_SINT16; // Reset axis value for trigger + if (ctx->right_analog_trigger_supported) { + axis = EXTRACTSINT16(data, SINPUT_REPORT_IDX_RIGHT_TRIGGER); + SDL_SendJoystickAxis(timestamp, joystick, axis_idx, axis); + } + + // Battery/Power state handling + if (ctx->last_state[SINPUT_REPORT_IDX_PLUG_STATUS] != data[SINPUT_REPORT_IDX_PLUG_STATUS] || + ctx->last_state[SINPUT_REPORT_IDX_CHARGE_LEVEL] != data[SINPUT_REPORT_IDX_CHARGE_LEVEL]) { + + SDL_PowerState state = SDL_POWERSTATE_NO_BATTERY; + Uint8 status = data[SINPUT_REPORT_IDX_PLUG_STATUS]; + int percent = data[SINPUT_REPORT_IDX_CHARGE_LEVEL]; + + percent = SDL_clamp(percent, 0, 100); // Ensure percent is within valid range + + switch (status) { + case 1: + state = SDL_POWERSTATE_NO_BATTERY; + percent = 0; + break; + case 2: + state = SDL_POWERSTATE_CHARGING; + break; + case 3: + state = SDL_POWERSTATE_CHARGED; + percent = 100; + break; + case 4: + state = SDL_POWERSTATE_ON_BATTERY; + break; + default: // Wired/No Battery Supported + state = SDL_POWERSTATE_UNKNOWN; + percent = 0; + break; + } + + if (state > 0) { + SDL_SendJoystickPowerInfo(joystick, state, percent); + } + } + + // Extract the IMU timestamp delta (in microseconds) + Uint16 imu_timestamp_delta = EXTRACTUINT16(data, SINPUT_REPORT_IDX_IMU_TIMESTAMP); + + // Check if we should process IMU data and if sensors are enabled + if ((imu_timestamp_delta > 0) && (ctx->sensors_enabled)) { + + // Process IMU timestamp by adding the delta to the accumulated timestamp and converting to nanoseconds + ctx->imu_timestamp += ((Uint64) imu_timestamp_delta * 1000); + + // Process Accelerometer + if (ctx->accelerometer_supported) { + + accel = EXTRACTSINT16(data, SINPUT_REPORT_IDX_IMU_ACCEL_Y); + imu_values[2] = -(float)accel * ctx->accelScale; // Y-axis acceleration + + accel = EXTRACTSINT16(data, SINPUT_REPORT_IDX_IMU_ACCEL_Z); + imu_values[1] = (float)accel * ctx->accelScale; // Z-axis acceleration + + accel = EXTRACTSINT16(data, SINPUT_REPORT_IDX_IMU_ACCEL_X); + imu_values[0] = -(float)accel * ctx->accelScale; // X-axis acceleration + + SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_ACCEL, ctx->imu_timestamp, imu_values, 3); + } + + // Process Gyroscope + if (ctx->gyroscope_supported) { + + gyro = EXTRACTSINT16(data, SINPUT_REPORT_IDX_IMU_GYRO_Y); + imu_values[2] = -(float)gyro * ctx->gyroScale; // Y-axis rotation + + gyro = EXTRACTSINT16(data, SINPUT_REPORT_IDX_IMU_GYRO_Z); + imu_values[1] = (float)gyro * ctx->gyroScale; // Z-axis rotation + + gyro = EXTRACTSINT16(data, SINPUT_REPORT_IDX_IMU_GYRO_X); + imu_values[0] = -(float)gyro * ctx->gyroScale; // X-axis rotation + + SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_GYRO, ctx->imu_timestamp, imu_values, 3); + } + } + + // Check if we should process touchpad + if (ctx->touchpad_supported && ctx->touchpad_count > 0) { + Uint8 touchpad = 0; + Uint8 finger = 0; + + Sint16 touch1X = EXTRACTSINT16(data, SINPUT_REPORT_IDX_TOUCH1_X); + Sint16 touch1Y = EXTRACTSINT16(data, SINPUT_REPORT_IDX_TOUCH1_Y); + Uint16 touch1P = EXTRACTUINT16(data, SINPUT_REPORT_IDX_TOUCH1_P); + + Sint16 touch2X = EXTRACTSINT16(data, SINPUT_REPORT_IDX_TOUCH2_X); + Sint16 touch2Y = EXTRACTSINT16(data, SINPUT_REPORT_IDX_TOUCH2_Y); + Uint16 touch2P = EXTRACTUINT16(data, SINPUT_REPORT_IDX_TOUCH2_P); + + SDL_SendJoystickTouchpad(timestamp, joystick, touchpad, finger, + touch1P > 0, + touch1X / 65536.0f + 0.5f, + touch1Y / 65536.0f + 0.5f, + touch1P / 32768.0f); + + if (ctx->touchpad_count > 1) { + ++touchpad; + } else if (ctx->touchpad_finger_count > 1) { + ++finger; + } + + if ((touchpad > 0) || (finger > 0)) { + SDL_SendJoystickTouchpad(timestamp, joystick, touchpad, finger, + touch2P > 0, + touch2X / 65536.0f + 0.5f, + touch2Y / 65536.0f + 0.5f, + touch2P / 32768.0f); + } + } + + SDL_memcpy(ctx->last_state, data, SDL_min(size, sizeof(ctx->last_state))); +} + +static bool HIDAPI_DriverSInput_UpdateDevice(SDL_HIDAPI_Device *device) +{ + SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)device->context; + SDL_Joystick *joystick = NULL; + Uint8 data[USB_PACKET_LENGTH]; + int size = 0; + + if (device->num_joysticks > 0) { + joystick = SDL_GetJoystickFromID(device->joysticks[0]); + } else { + return false; + } + + while ((size = SDL_hid_read_timeout(device->dev, data, sizeof(data), 0)) > 0) { +#ifdef DEBUG_SINPUT_PROTOCOL + HIDAPI_DumpPacket("SInput packet: size = %d", data, size); +#endif + if (!joystick) { + continue; + } + + // Handle command response information + if (data[0] == SINPUT_DEVICE_REPORT_ID_JOYSTICK_INPUT) { + HIDAPI_DriverSInput_HandleStatePacket(joystick, ctx, data, size); + } + } + + if (size < 0) { + // Read error, device is disconnected + HIDAPI_JoystickDisconnected(device, device->joysticks[0]); + } + return (size >= 0); +} + +static void HIDAPI_DriverSInput_CloseJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick) +{ +} + +static void HIDAPI_DriverSInput_FreeDevice(SDL_HIDAPI_Device *device) +{ +} + +SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverSInput = { + SDL_HINT_JOYSTICK_HIDAPI_SINPUT, + true, + HIDAPI_DriverSInput_RegisterHints, + HIDAPI_DriverSInput_UnregisterHints, + HIDAPI_DriverSInput_IsEnabled, + HIDAPI_DriverSInput_IsSupportedDevice, + HIDAPI_DriverSInput_InitDevice, + HIDAPI_DriverSInput_GetDevicePlayerIndex, + HIDAPI_DriverSInput_SetDevicePlayerIndex, + HIDAPI_DriverSInput_UpdateDevice, + HIDAPI_DriverSInput_OpenJoystick, + HIDAPI_DriverSInput_RumbleJoystick, + HIDAPI_DriverSInput_RumbleJoystickTriggers, + HIDAPI_DriverSInput_GetJoystickCapabilities, + HIDAPI_DriverSInput_SetJoystickLED, + HIDAPI_DriverSInput_SendJoystickEffect, + HIDAPI_DriverSInput_SetJoystickSensorsEnabled, + HIDAPI_DriverSInput_CloseJoystick, + HIDAPI_DriverSInput_FreeDevice, +}; + +#endif // SDL_JOYSTICK_HIDAPI_SINPUT + +#endif // SDL_JOYSTICK_HIDAPI diff --git a/src/joystick/hidapi/SDL_hidapijoystick.c b/src/joystick/hidapi/SDL_hidapijoystick.c index 5d26deafe8..5124d97a91 100644 --- a/src/joystick/hidapi/SDL_hidapijoystick.c +++ b/src/joystick/hidapi/SDL_hidapijoystick.c @@ -97,6 +97,9 @@ static SDL_HIDAPI_DeviceDriver *SDL_HIDAPI_drivers[] = { #ifdef SDL_JOYSTICK_HIDAPI_FLYDIGI &SDL_HIDAPI_DriverFlydigi, #endif +#ifdef SDL_JOYSTICK_HIDAPI_SINPUT + &SDL_HIDAPI_DriverSInput, +#endif }; static int SDL_HIDAPI_numdrivers = 0; static SDL_AtomicInt SDL_HIDAPI_updating_devices; diff --git a/src/joystick/hidapi/SDL_hidapijoystick_c.h b/src/joystick/hidapi/SDL_hidapijoystick_c.h index f6b8ebfae4..e280c86aa7 100644 --- a/src/joystick/hidapi/SDL_hidapijoystick_c.h +++ b/src/joystick/hidapi/SDL_hidapijoystick_c.h @@ -44,6 +44,7 @@ #define SDL_JOYSTICK_HIDAPI_8BITDO #define SDL_JOYSTICK_HIDAPI_FLYDIGI #define SDL_JOYSTICK_HIDAPI_GIP +#define SDL_JOYSTICK_HIDAPI_SINPUT // Joystick capability definitions #define SDL_JOYSTICK_CAP_MONO_LED 0x00000001 @@ -165,6 +166,7 @@ extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverSteamHori; extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverLg4ff; extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_Driver8BitDo; extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverFlydigi; +extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverSInput; // Return true if a HID device is present and supported as a joystick of the given type extern bool HIDAPI_IsDeviceTypePresent(SDL_GamepadType type); diff --git a/src/joystick/usb_ids.h b/src/joystick/usb_ids.h index 14b5fb52b0..343f957812 100644 --- a/src/joystick/usb_ids.h +++ b/src/joystick/usb_ids.h @@ -59,6 +59,7 @@ #define USB_VENDOR_SWITCH 0x2563 #define USB_VENDOR_VALVE 0x28de #define USB_VENDOR_ZEROPLUS 0x0c12 +#define USB_VENDOR_RASPBERRYPI 0x2e8a // Commercial hardware from various companies are registered under this VID #define USB_PRODUCT_8BITDO_SF30_PRO 0x6000 // B + START #define USB_PRODUCT_8BITDO_SF30_PRO_BT 0x6100 // B + START @@ -161,6 +162,10 @@ #define USB_PRODUCT_XBOX_SERIES_X_BLE 0x0b13 #define USB_PRODUCT_XBOX_ONE_XBOXGIP_CONTROLLER 0x02ff // XBOXGIP driver software PID #define USB_PRODUCT_STEAM_VIRTUAL_GAMEPAD 0x11ff +#define USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC 0x10c6 +#define USB_PRODUCT_HANDHELDLEGEND_PROGCC 0x10df +#define USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE 0x10dd +#define USB_PRODUCT_BONJIRICHANNEL_FIREBIRD 0x10e0 // USB usage pages #define USB_USAGEPAGE_GENERIC_DESKTOP 0x0001 From 504107ad0ee945888368414cb231585a9d976f9c Mon Sep 17 00:00:00 2001 From: SDL Wiki Bot Date: Wed, 16 Jul 2025 01:36:42 +0000 Subject: [PATCH 043/103] Sync SDL3 wiki -> header [ci skip] --- include/SDL3/SDL_hints.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h index 878dd2d486..d330c1d681 100644 --- a/include/SDL3/SDL_hints.h +++ b/include/SDL3/SDL_hints.h @@ -1748,7 +1748,9 @@ extern "C" { /** * A variable controlling whether the HIDAPI driver for SInput controllers - * should be used. More info - https://github.com/HandHeldLegend/SInput-HID + * should be used. + * + * More info - https://github.com/HandHeldLegend/SInput-HID * * This variable can be set to the following values: * From 8e5fe0ea61dc87b29ca9a6119324221df0113bcf Mon Sep 17 00:00:00 2001 From: mitchellcairns Date: Wed, 16 Jul 2025 10:12:38 -0700 Subject: [PATCH 044/103] SInput Timestamp and Protocol Version (#13371) * Implement Uint32 microseconds timestamp for IMU reporting instead of deltas * Implement protocol version in feature request response --- src/joystick/hidapi/SDL_hidapi_sinput.c | 105 ++++++++++++++---------- 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c index 142e45c1ef..f9af55aec6 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.c +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -70,18 +70,18 @@ #define SINPUT_REPORT_IDX_LEFT_TRIGGER 15 #define SINPUT_REPORT_IDX_RIGHT_TRIGGER 17 #define SINPUT_REPORT_IDX_IMU_TIMESTAMP 19 -#define SINPUT_REPORT_IDX_IMU_ACCEL_X 21 -#define SINPUT_REPORT_IDX_IMU_ACCEL_Y 23 -#define SINPUT_REPORT_IDX_IMU_ACCEL_Z 25 -#define SINPUT_REPORT_IDX_IMU_GYRO_X 27 -#define SINPUT_REPORT_IDX_IMU_GYRO_Y 29 -#define SINPUT_REPORT_IDX_IMU_GYRO_Z 31 -#define SINPUT_REPORT_IDX_TOUCH1_X 33 -#define SINPUT_REPORT_IDX_TOUCH1_Y 35 -#define SINPUT_REPORT_IDX_TOUCH1_P 37 -#define SINPUT_REPORT_IDX_TOUCH2_X 39 -#define SINPUT_REPORT_IDX_TOUCH2_Y 41 -#define SINPUT_REPORT_IDX_TOUCH2_P 43 +#define SINPUT_REPORT_IDX_IMU_ACCEL_X 23 +#define SINPUT_REPORT_IDX_IMU_ACCEL_Y 25 +#define SINPUT_REPORT_IDX_IMU_ACCEL_Z 27 +#define SINPUT_REPORT_IDX_IMU_GYRO_X 29 +#define SINPUT_REPORT_IDX_IMU_GYRO_Y 31 +#define SINPUT_REPORT_IDX_IMU_GYRO_Z 33 +#define SINPUT_REPORT_IDX_TOUCH1_X 35 +#define SINPUT_REPORT_IDX_TOUCH1_Y 37 +#define SINPUT_REPORT_IDX_TOUCH1_P 39 +#define SINPUT_REPORT_IDX_TOUCH2_X 41 +#define SINPUT_REPORT_IDX_TOUCH2_Y 43 +#define SINPUT_REPORT_IDX_TOUCH2_P 45 #define SINPUT_REPORT_IDX_COMMAND_RESPONSE_ID 1 #define SINPUT_REPORT_IDX_COMMAND_RESPONSE_BULK 2 @@ -99,6 +99,10 @@ #define EXTRACTUINT16(data, idx) ((Uint16)((data)[(idx)] | ((data)[(idx) + 1] << 8))) #endif +#ifndef EXTRACTUINT32 +#define EXTRACTUINT32(data, idx) ((Uint32)((data)[(idx)] | ((data)[(idx) + 1] << 8) | ((data)[(idx) + 2] << 16) | ((data)[(idx) + 3] << 24))) +#endif + typedef struct { @@ -142,6 +146,7 @@ typedef struct typedef struct { SDL_HIDAPI_Device *device; + Uint16 protocol_version; bool sensors_enabled; Uint8 player_idx; @@ -173,7 +178,9 @@ typedef struct Uint8 buttons_count; Uint8 usage_masks[4]; - Uint64 imu_timestamp; // Nanoseconds. We accumulate with received deltas + Uint32 last_imu_timestamp_us; + + Uint64 imu_timestamp_ns; // Nanoseconds. We accumulate with received deltas } SDL_DriverSInput_Context; // Converts raw int16_t gyro scale setting @@ -192,51 +199,54 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) { SDL_DriverSInput_Context *ctx = (SDL_DriverSInput_Context *)device->context; + // Obtain protocol version + ctx->protocol_version = EXTRACTUINT16(data, 0); + // Bitfields are not portable, so we unpack them into a struct value - ctx->rumble_supported = (data[0] & 0x01) != 0; - ctx->player_leds_supported = (data[0] & 0x02) != 0; - ctx->accelerometer_supported = (data[0] & 0x04) != 0; - ctx->gyroscope_supported = (data[0] & 0x08) != 0; + ctx->rumble_supported = (data[2] & 0x01) != 0; + ctx->player_leds_supported = (data[2] & 0x02) != 0; + ctx->accelerometer_supported = (data[2] & 0x04) != 0; + ctx->gyroscope_supported = (data[2] & 0x08) != 0; - ctx->left_analog_stick_supported = (data[0] & 0x10) != 0; - ctx->right_analog_stick_supported = (data[0] & 0x20) != 0; - ctx->left_analog_trigger_supported = (data[0] & 0x40) != 0; - ctx->right_analog_trigger_supported = (data[0] & 0x80) != 0; + ctx->left_analog_stick_supported = (data[2] & 0x10) != 0; + ctx->right_analog_stick_supported = (data[2] & 0x20) != 0; + ctx->left_analog_trigger_supported = (data[2] & 0x40) != 0; + ctx->right_analog_trigger_supported = (data[2] & 0x80) != 0; - ctx->touchpad_supported = (data[1] & 0x01) != 0; - ctx->joystick_rgb_supported = (data[1] & 0x02) != 0; + ctx->touchpad_supported = (data[3] & 0x01) != 0; + ctx->joystick_rgb_supported = (data[3] & 0x02) != 0; SDL_GamepadType type = SDL_GAMEPAD_TYPE_UNKNOWN; - type = (SDL_GamepadType)SDL_clamp(data[2], SDL_GAMEPAD_TYPE_UNKNOWN, SDL_GAMEPAD_TYPE_COUNT); + type = (SDL_GamepadType)SDL_clamp(data[4], SDL_GAMEPAD_TYPE_UNKNOWN, SDL_GAMEPAD_TYPE_COUNT); device->type = type; // The 4 MSB represent a button layout style SDL_GamepadFaceStyle // The 4 LSB represent a device sub-type - device->guid.data[15] = data[3]; + device->guid.data[15] = data[5]; #if defined(DEBUG_SINPUT_INIT) - SDL_Log("SInput Face Style: %d", (data[3] & 0xF0) >> 4); - SDL_Log("SInput Sub-type: %d", (data[3] & 0xF)); + SDL_Log("SInput Face Style: %d", (data[5] & 0xF0) >> 4); + SDL_Log("SInput Sub-type: %d", (data[5] & 0xF)); #endif - ctx->polling_rate_ms = data[4]; + ctx->polling_rate_ms = data[6]; - ctx->accelRange = EXTRACTUINT16(data, 6); - ctx->gyroRange = EXTRACTUINT16(data, 8); + ctx->accelRange = EXTRACTUINT16(data, 8); + ctx->gyroRange = EXTRACTUINT16(data, 10); // Masks in LSB to MSB // South, East, West, North, DUp, DDown, DLeft, DRight - ctx->usage_masks[0] = data[10]; + ctx->usage_masks[0] = data[12]; // Stick Left, Stick Right, L Shoulder, R Shoulder, // L Trigger, R Trigger, L Paddle 1, R Paddle 1 - ctx->usage_masks[1] = data[11]; + ctx->usage_masks[1] = data[13]; // Start, Back, Guide, Capture, L Paddle 2, R Paddle 2, Touchpad L, Touchpad R - ctx->usage_masks[2] = data[12]; + ctx->usage_masks[2] = data[14]; // Power, Misc 4 to 10 - ctx->usage_masks[3] = data[13]; + ctx->usage_masks[3] = data[15]; // Derive button count from mask for (Uint8 byte = 0; byte < 4; ++byte) { @@ -252,8 +262,8 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) #endif // Get and validate touchpad parameters - ctx->touchpad_count = data[14]; - ctx->touchpad_finger_count = data[15]; + ctx->touchpad_count = data[16]; + ctx->touchpad_finger_count = data[17]; #if defined(DEBUG_SINPUT_INIT) SDL_Log("Accelerometer Range: %d", ctx->accelRange); @@ -664,13 +674,24 @@ static void HIDAPI_DriverSInput_HandleStatePacket(SDL_Joystick *joystick, SDL_Dr } // Extract the IMU timestamp delta (in microseconds) - Uint16 imu_timestamp_delta = EXTRACTUINT16(data, SINPUT_REPORT_IDX_IMU_TIMESTAMP); + Uint32 imu_timestamp_us = EXTRACTUINT32(data, SINPUT_REPORT_IDX_IMU_TIMESTAMP); + Uint32 imu_time_delta_us = 0; // Check if we should process IMU data and if sensors are enabled - if ((imu_timestamp_delta > 0) && (ctx->sensors_enabled)) { + if (ctx->sensors_enabled) { - // Process IMU timestamp by adding the delta to the accumulated timestamp and converting to nanoseconds - ctx->imu_timestamp += ((Uint64) imu_timestamp_delta * 1000); + if (imu_timestamp_us >= ctx->last_imu_timestamp_us) { + imu_time_delta_us = (imu_timestamp_us - ctx->last_imu_timestamp_us); + } else { + // Handle rollover case + imu_time_delta_us = (UINT32_MAX - ctx->last_imu_timestamp_us) + imu_timestamp_us + 1; + } + + // Convert delta to nanoseconds and update running timestamp + ctx->imu_timestamp_ns += (Uint64)imu_time_delta_us * 1000; + + // Update last timestamp + ctx->last_imu_timestamp_us = imu_timestamp_us; // Process Accelerometer if (ctx->accelerometer_supported) { @@ -684,7 +705,7 @@ static void HIDAPI_DriverSInput_HandleStatePacket(SDL_Joystick *joystick, SDL_Dr accel = EXTRACTSINT16(data, SINPUT_REPORT_IDX_IMU_ACCEL_X); imu_values[0] = -(float)accel * ctx->accelScale; // X-axis acceleration - SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_ACCEL, ctx->imu_timestamp, imu_values, 3); + SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_ACCEL, ctx->imu_timestamp_ns, imu_values, 3); } // Process Gyroscope @@ -699,7 +720,7 @@ static void HIDAPI_DriverSInput_HandleStatePacket(SDL_Joystick *joystick, SDL_Dr gyro = EXTRACTSINT16(data, SINPUT_REPORT_IDX_IMU_GYRO_X); imu_values[0] = -(float)gyro * ctx->gyroScale; // X-axis rotation - SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_GYRO, ctx->imu_timestamp, imu_values, 3); + SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_GYRO, ctx->imu_timestamp_ns, imu_values, 3); } } From 62d82ffc15b21fcc432fd9e0499c4585cd8d89c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Toma=C5=BEi=C4=8D?= Date: Mon, 14 Jul 2025 13:43:01 +0200 Subject: [PATCH 045/103] fix: don't use `CLOCK_MONOTONIC_RAW` on Android Older Android phones have a kernel bug where time is not properly calculated when calling `clock_gettime(CLOCK_MONOTONIC_RAW, ...)`. The returned time either has nanoseconds out of range (outside of [0, 999999999]) or the returned time is not monotonic in regards to previous call or both. The issue is reproducible in Android Studio on arm64 emulators from at least Android 7.0 (didn't try older versions) up to including Android 8.1 (kernel 3.18.94). The issue is in the kernel, these are just the versions of Android emulator with buggy kernels. The kernel commit that fixed this is [1]. Because this fix was backported to various LTS releases of kernels it's hard to find out exactly which kernels are buggy and which not. Therefore always use `CLOCK_MONOTONIC` on Android. The `CLOCK_MONOTONIC` is slowly adjusted in case of NTP changes but not for more than 0.5ms per second [2]. So it should be good enough for measuring elapsed time. --- An option would be to dynamically select `CLOCK_MONOTONIC_RAW`/`CLOCK_MONOTONIC` at runtime by checking at init time that `clock_gettime(CLOCK_MONOTONIC_RAW, ...)` returns nanoseconds in range. Testing showed that nanosecons are out of range for 999/1000 cases. I guess this could be good enough for support on older phones. But because this would introduce a small perf hit by always reading a global variable for first argument to `clock_gettime` and because `CLOCK_MONOTONIC` does not have that high time deviation, this commit uses `CLOCK_MONOTONIC` on Android always. It is also less burden to maintain. --- [1] https://github.com/torvalds/linux/commit/dbb236c1ceb697a559e0694ac4c9e7b9131d0b16 [2] https://github.com/torvalds/linux/blob/master/include/linux/timex.h#L136 --- src/timer/unix/SDL_systimer.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/timer/unix/SDL_systimer.c b/src/timer/unix/SDL_systimer.c index 0f96319af7..51bd0cf6c5 100644 --- a/src/timer/unix/SDL_systimer.c +++ b/src/timer/unix/SDL_systimer.c @@ -53,7 +53,9 @@ // Use CLOCK_MONOTONIC_RAW, if available, which is not subject to adjustment by NTP #ifdef HAVE_CLOCK_GETTIME -#ifdef CLOCK_MONOTONIC_RAW +// Older Android phones have a buggy CLOCK_MONOTONIC_RAW, use CLOCK_MONOTONIC +// See fix: https://github.com/torvalds/linux/commit/dbb236c1ceb697a559e0694ac4c9e7b9131d0b16 +#if defined(CLOCK_MONOTONIC_RAW) && !defined(__ANDROID__) #define SDL_MONOTONIC_CLOCK CLOCK_MONOTONIC_RAW #else #define SDL_MONOTONIC_CLOCK CLOCK_MONOTONIC From 7510a67159d1a852162db00cba49cf29b8ced35a Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Wed, 16 Jul 2025 19:31:10 -0700 Subject: [PATCH 046/103] Fixed typo Fixes https://github.com/libsdl-org/SDL/issues/13373 --- src/render/SDL_render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/render/SDL_render.c b/src/render/SDL_render.c index 3fa627b13f..d66a8987c6 100644 --- a/src/render/SDL_render.c +++ b/src/render/SDL_render.c @@ -5180,7 +5180,7 @@ bool SDL_RenderGeometryRaw(SDL_Renderer *renderer, texture_address_mode_v = renderer->texture_address_mode_v; if (texture && (texture_address_mode_u == SDL_TEXTURE_ADDRESS_AUTO || - texture_address_mode_u == SDL_TEXTURE_ADDRESS_AUTO)) { + texture_address_mode_v == SDL_TEXTURE_ADDRESS_AUTO)) { for (i = 0; i < num_vertices; ++i) { const float *uv_ = (const float *)((const char *)uv + i * uv_stride); float u = uv_[0]; From 1b4fd3aa83ba24860a22058ba7cbc76b41bdf08a Mon Sep 17 00:00:00 2001 From: SDL Wiki Bot Date: Thu, 17 Jul 2025 07:05:50 +0000 Subject: [PATCH 047/103] Sync SDL3 wiki -> header [ci skip] --- include/SDL3/SDL_gpu.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/include/SDL3/SDL_gpu.h b/include/SDL3/SDL_gpu.h index b3ff20221c..a06b4995d3 100644 --- a/include/SDL3/SDL_gpu.h +++ b/include/SDL3/SDL_gpu.h @@ -2667,7 +2667,8 @@ extern SDL_DECLSPEC SDL_GPUShader * SDLCALL SDL_CreateGPUShader( * Creates a texture object to be used in graphics or compute workflows. * * The contents of this texture are undefined until data is written to the - * texture. + * texture, either via SDL_UploadToGPUTexture or by performing a render or + * compute pass with this texture as a target. * * Note that certain combinations of usage flags are invalid. For example, a * texture cannot have both the SAMPLER and GRAPHICS_STORAGE_READ flags. @@ -2709,6 +2710,8 @@ extern SDL_DECLSPEC SDL_GPUShader * SDLCALL SDL_CreateGPUShader( * * \sa SDL_UploadToGPUTexture * \sa SDL_DownloadFromGPUTexture + * \sa SDL_BeginGPURenderPass + * \sa SDL_BeginGPUComputePass * \sa SDL_BindGPUVertexSamplers * \sa SDL_BindGPUVertexStorageTextures * \sa SDL_BindGPUFragmentSamplers From 1d9fc5f2c892e8205358eb424c46e1d8148d09a3 Mon Sep 17 00:00:00 2001 From: SDL Wiki Bot Date: Thu, 17 Jul 2025 07:20:27 +0000 Subject: [PATCH 048/103] Sync SDL3 wiki -> header [ci skip] --- include/SDL3/SDL_gpu.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/include/SDL3/SDL_gpu.h b/include/SDL3/SDL_gpu.h index a06b4995d3..b61b4d3b64 100644 --- a/include/SDL3/SDL_gpu.h +++ b/include/SDL3/SDL_gpu.h @@ -3125,6 +3125,14 @@ extern SDL_DECLSPEC void SDLCALL SDL_PushGPUComputeUniformData( * is called. You cannot begin another render pass, or begin a compute pass or * copy pass until you have ended the render pass. * + * Using SDL_GPU_LOADOP_LOAD before any contents have been written to the + * texture subresource will result in undefined behavior. SDL_GPU_LOADOP_CLEAR + * will set the contents of the texture subresource to a single value before + * any rendering is performed. It's fine to do an empty render pass using + * SDL_GPU_STOREOP_STORE to clear a texture, but in general it's better to + * think of clearing not as an independent operation but as something that's + * done as the beginning of a render pass. + * * \param command_buffer a command buffer. * \param color_target_infos an array of texture subresources with * corresponding clear values and load/store ops. From bc5c9a686ca01c7cffa45e223efcd0cb7ef1efb7 Mon Sep 17 00:00:00 2001 From: Evan Hemsley <2342303+thatcosmonaut@users.noreply.github.com> Date: Thu, 17 Jul 2025 00:21:34 -0700 Subject: [PATCH 049/103] GPU: Clean up properties in SDL_ReleaseGPUTexture (#13378) --- src/gpu/d3d12/SDL_gpu_d3d12.c | 2 ++ src/gpu/metal/SDL_gpu_metal.m | 1 + src/gpu/vulkan/SDL_gpu_vulkan.c | 2 ++ 3 files changed, 5 insertions(+) diff --git a/src/gpu/d3d12/SDL_gpu_d3d12.c b/src/gpu/d3d12/SDL_gpu_d3d12.c index 09f6f34841..9d4130c3ae 100644 --- a/src/gpu/d3d12/SDL_gpu_d3d12.c +++ b/src/gpu/d3d12/SDL_gpu_d3d12.c @@ -1420,6 +1420,8 @@ static void D3D12_INTERNAL_ReleaseTextureContainer( container->textures[i]); } + SDL_DestroyProperties(container->header.info.props); + // Containers are just client handles, so we can destroy immediately if (container->debugName) { SDL_free(container->debugName); diff --git a/src/gpu/metal/SDL_gpu_metal.m b/src/gpu/metal/SDL_gpu_metal.m index c854f983cc..fff9282f86 100644 --- a/src/gpu/metal/SDL_gpu_metal.m +++ b/src/gpu/metal/SDL_gpu_metal.m @@ -914,6 +914,7 @@ static void METAL_INTERNAL_DestroyTextureContainer( container->textures[i]->handle = nil; SDL_free(container->textures[i]); } + SDL_DestroyProperties(container->header.info.props); if (container->debugName != NULL) { SDL_free(container->debugName); } diff --git a/src/gpu/vulkan/SDL_gpu_vulkan.c b/src/gpu/vulkan/SDL_gpu_vulkan.c index 7721004490..f94dc25638 100644 --- a/src/gpu/vulkan/SDL_gpu_vulkan.c +++ b/src/gpu/vulkan/SDL_gpu_vulkan.c @@ -6954,6 +6954,8 @@ static void VULKAN_ReleaseTexture( VULKAN_INTERNAL_ReleaseTexture(renderer, vulkanTextureContainer->textures[i]); } + SDL_DestroyProperties(vulkanTextureContainer->header.info.props); + // Containers are just client handles, so we can destroy immediately if (vulkanTextureContainer->debugName != NULL) { SDL_free(vulkanTextureContainer->debugName); From 855d28e97aff349474ddba02d1d6a7a98a17bb1b Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Thu, 17 Jul 2025 08:47:12 -0700 Subject: [PATCH 050/103] Fixed crash if a clipboard event was sent with video uninitialized This can happen if you're using SDL on Android without using the video subsystem. --- src/video/SDL_clipboard.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/video/SDL_clipboard.c b/src/video/SDL_clipboard.c index 105c2889e3..9d8c27d451 100644 --- a/src/video/SDL_clipboard.c +++ b/src/video/SDL_clipboard.c @@ -42,6 +42,10 @@ void SDL_CancelClipboardData(Uint32 sequence) { SDL_VideoDevice *_this = SDL_GetVideoDevice(); + if (!_this) { + return; + } + if (sequence && sequence != _this->clipboard_sequence) { // This clipboard data was already canceled return; @@ -62,6 +66,10 @@ bool SDL_SaveClipboardMimeTypes(const char **mime_types, size_t num_mime_types) { SDL_VideoDevice *_this = SDL_GetVideoDevice(); + if (!_this) { + return SDL_UninitializedVideo(); + } + SDL_FreeClipboardMimeTypes(_this); if (mime_types && num_mime_types > 0) { @@ -234,13 +242,11 @@ bool SDL_HasClipboardData(const char *mime_type) SDL_VideoDevice *_this = SDL_GetVideoDevice(); if (!_this) { - SDL_UninitializedVideo(); - return false; + return SDL_UninitializedVideo(); } if (!mime_type) { - SDL_InvalidParamError("mime_type"); - return false; + return SDL_InvalidParamError("mime_type"); } if (_this->HasClipboardData) { From ee6d8f78f469c6acf1271518d4d8ab79d5119a13 Mon Sep 17 00:00:00 2001 From: BurntRanch <69512353+BurntRanch@users.noreply.github.com> Date: Thu, 17 Jul 2025 18:53:35 +0300 Subject: [PATCH 051/103] Clarify SDL_GPUVertexBufferDescription.pitch comment (#13381) --- include/SDL3/SDL_gpu.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/SDL3/SDL_gpu.h b/include/SDL3/SDL_gpu.h index b61b4d3b64..0e82e2b040 100644 --- a/include/SDL3/SDL_gpu.h +++ b/include/SDL3/SDL_gpu.h @@ -1627,7 +1627,7 @@ typedef struct SDL_GPUSamplerCreateInfo typedef struct SDL_GPUVertexBufferDescription { Uint32 slot; /**< The binding slot of the vertex buffer. */ - Uint32 pitch; /**< The byte pitch between consecutive elements of the vertex buffer. */ + Uint32 pitch; /**< The size of a single element + the offset between elements. */ SDL_GPUVertexInputRate input_rate; /**< Whether attribute addressing is a function of the vertex index or instance index. */ Uint32 instance_step_rate; /**< Reserved for future use. Must be set to 0. */ } SDL_GPUVertexBufferDescription; From 8451ce86c1167ea67c1118cfb896fd764c166472 Mon Sep 17 00:00:00 2001 From: Marcin Serwin Date: Wed, 16 Jul 2025 20:21:41 +0200 Subject: [PATCH 052/103] iostream: Add optional free_func pointer property to memory streams Fixes https://github.com/libsdl-org/SDL/issues/13368 Signed-off-by: Marcin Serwin --- include/SDL3/SDL_iostream.h | 21 +++++++++++--- src/io/SDL_iostream.c | 8 ++++++ test/testautomation_iostream.c | 52 +++++++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/include/SDL3/SDL_iostream.h b/include/SDL3/SDL_iostream.h index d1596f91d3..ec7ab76086 100644 --- a/include/SDL3/SDL_iostream.h +++ b/include/SDL3/SDL_iostream.h @@ -286,8 +286,7 @@ extern SDL_DECLSPEC SDL_IOStream * SDLCALL SDL_IOFromFile(const char *file, cons * certain size, for both read and write access. * * This memory buffer is not copied by the SDL_IOStream; the pointer you - * provide must remain valid until you close the stream. Closing the stream - * will not free the original buffer. + * provide must remain valid until you close the stream. * * If you need to make sure the SDL_IOStream never writes to the memory * buffer, you should use SDL_IOFromConstMem() with a read-only buffer of @@ -300,6 +299,13 @@ extern SDL_DECLSPEC SDL_IOStream * SDLCALL SDL_IOFromFile(const char *file, cons * - `SDL_PROP_IOSTREAM_MEMORY_SIZE_NUMBER`: this will be the `size` parameter * that was passed to this function. * + * Additionally, the following properties are recognized: + * + * - `SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC`: if this property is set to a non-NULL + * value it will be interpreted as a function of SDL_free_func type and called + * with the passed `mem` pointer when closing the stream. By default it is + * unset, i.e., the memory will not be freed. + * * \param mem a pointer to a buffer to feed an SDL_IOStream stream. * \param size the buffer size, in bytes. * \returns a pointer to a new SDL_IOStream structure or NULL on failure; call @@ -321,6 +327,7 @@ extern SDL_DECLSPEC SDL_IOStream * SDLCALL SDL_IOFromMem(void *mem, size_t size) #define SDL_PROP_IOSTREAM_MEMORY_POINTER "SDL.iostream.memory.base" #define SDL_PROP_IOSTREAM_MEMORY_SIZE_NUMBER "SDL.iostream.memory.size" +#define SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC "SDL.iostream.memory.free" /** * Use this function to prepare a read-only memory buffer for use with @@ -333,8 +340,7 @@ extern SDL_DECLSPEC SDL_IOStream * SDLCALL SDL_IOFromMem(void *mem, size_t size) * without writing to the memory buffer. * * This memory buffer is not copied by the SDL_IOStream; the pointer you - * provide must remain valid until you close the stream. Closing the stream - * will not free the original buffer. + * provide must remain valid until you close the stream. * * If you need to write to a memory buffer, you should use SDL_IOFromMem() * with a writable buffer of memory instead. @@ -346,6 +352,13 @@ extern SDL_DECLSPEC SDL_IOStream * SDLCALL SDL_IOFromMem(void *mem, size_t size) * - `SDL_PROP_IOSTREAM_MEMORY_SIZE_NUMBER`: this will be the `size` parameter * that was passed to this function. * + * Additionally, the following properties are recognized: + * + * - `SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC`: if this property is set to a non-NULL + * value it will be interpreted as a function of SDL_free_func type and called + * with the passed `mem` pointer when closing the stream. By default it is + * unset, i.e., the memory will not be freed. + * * \param mem a pointer to a read-only buffer to feed an SDL_IOStream stream. * \param size the buffer size, in bytes. * \returns a pointer to a new SDL_IOStream structure or NULL on failure; call diff --git a/src/io/SDL_iostream.c b/src/io/SDL_iostream.c index 989f3b9c4c..2e63b9aefb 100644 --- a/src/io/SDL_iostream.c +++ b/src/io/SDL_iostream.c @@ -716,6 +716,7 @@ typedef struct IOStreamMemData Uint8 *base; Uint8 *here; Uint8 *stop; + SDL_PropertiesID props; } IOStreamMemData; static Sint64 SDLCALL mem_size(void *userdata) @@ -779,6 +780,11 @@ static size_t SDLCALL mem_write(void *userdata, const void *ptr, size_t size, SD static bool SDLCALL mem_close(void *userdata) { + IOStreamMemData *iodata = (IOStreamMemData *) userdata; + SDL_free_func free_func = SDL_GetPointerProperty(iodata->props, SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC, NULL); + if (free_func) { + free_func(iodata->base); + } SDL_free(userdata); return true; } @@ -950,6 +956,7 @@ SDL_IOStream *SDL_IOFromMem(void *mem, size_t size) } else { const SDL_PropertiesID props = SDL_GetIOProperties(iostr); if (props) { + iodata->props = props; SDL_SetPointerProperty(props, SDL_PROP_IOSTREAM_MEMORY_POINTER, mem); SDL_SetNumberProperty(props, SDL_PROP_IOSTREAM_MEMORY_SIZE_NUMBER, size); } @@ -990,6 +997,7 @@ SDL_IOStream *SDL_IOFromConstMem(const void *mem, size_t size) } else { const SDL_PropertiesID props = SDL_GetIOProperties(iostr); if (props) { + iodata->props = props; SDL_SetPointerProperty(props, SDL_PROP_IOSTREAM_MEMORY_POINTER, (void *)mem); SDL_SetNumberProperty(props, SDL_PROP_IOSTREAM_MEMORY_SIZE_NUMBER, size); } diff --git a/test/testautomation_iostream.c b/test/testautomation_iostream.c index d1e5877111..23a0f28f24 100644 --- a/test/testautomation_iostream.c +++ b/test/testautomation_iostream.c @@ -312,6 +312,52 @@ static int SDLCALL iostrm_testConstMem(void *arg) return TEST_COMPLETED; } +static int free_call_count; +void SDLCALL test_free(void* mem) { + free_call_count++; + SDL_free(mem); +} + +static int SDLCALL iostrm_testMemWithFree(void *arg) +{ + void *mem; + SDL_IOStream *rw; + int result; + + /* Allocate some memory */ + mem = SDL_malloc(sizeof(IOStreamHelloWorldCompString) - 1); + if (mem == NULL) { + return TEST_ABORTED; + } + + /* Open handle */ + rw = SDL_IOFromMem(mem, sizeof(IOStreamHelloWorldCompString) - 1); + SDLTest_AssertPass("Call to SDL_IOFromMem() succeeded"); + SDLTest_AssertCheck(rw != NULL, "Verify opening memory with SDL_IOFromMem does not return NULL"); + + /* Bail out if NULL */ + if (rw == NULL) { + return TEST_ABORTED; + } + + /* Set the free function */ + free_call_count = 0; + result = SDL_SetPointerProperty(SDL_GetIOProperties(rw), SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC, test_free); + SDLTest_AssertPass("Call to SDL_SetPointerProperty() succeeded"); + SDLTest_AssertCheck(result == true, "Verify result value is true; got %d", result); + + /* Run generic tests */ + testGenericIOStreamValidations(rw, true); + + /* Close handle */ + result = SDL_CloseIO(rw); + SDLTest_AssertPass("Call to SDL_CloseIO() succeeded"); + SDLTest_AssertCheck(result == true, "Verify result value is true; got: %d", result); + SDLTest_AssertCheck(free_call_count == 1, "Verify the custom free function was called once; call count: %d", free_call_count); + + return TEST_COMPLETED; +} + /** * Tests dynamic memory * @@ -686,10 +732,14 @@ static const SDLTest_TestCaseReference iostrmTest9 = { iostrm_testCompareRWFromMemWithRWFromFile, "iostrm_testCompareRWFromMemWithRWFromFile", "Compare RWFromMem and RWFromFile IOStream for read and seek", TEST_ENABLED }; +static const SDLTest_TestCaseReference iostrmTest10 = { + iostrm_testMemWithFree, "iostrm_testMemWithFree", "Tests opening from memory with free on close", TEST_ENABLED +}; + /* Sequence of IOStream test cases */ static const SDLTest_TestCaseReference *iostrmTests[] = { &iostrmTest1, &iostrmTest2, &iostrmTest3, &iostrmTest4, &iostrmTest5, &iostrmTest6, - &iostrmTest7, &iostrmTest8, &iostrmTest9, NULL + &iostrmTest7, &iostrmTest8, &iostrmTest9, &iostrmTest10, NULL }; /* IOStream test suite (global) */ From 631aa697e6e97ba9553228244cd2768466043420 Mon Sep 17 00:00:00 2001 From: SDL Wiki Bot Date: Thu, 17 Jul 2025 16:00:47 +0000 Subject: [PATCH 053/103] Sync SDL3 wiki -> header [ci skip] --- include/SDL3/SDL_iostream.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/include/SDL3/SDL_iostream.h b/include/SDL3/SDL_iostream.h index ec7ab76086..f12339032e 100644 --- a/include/SDL3/SDL_iostream.h +++ b/include/SDL3/SDL_iostream.h @@ -301,10 +301,10 @@ extern SDL_DECLSPEC SDL_IOStream * SDLCALL SDL_IOFromFile(const char *file, cons * * Additionally, the following properties are recognized: * - * - `SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC`: if this property is set to a non-NULL - * value it will be interpreted as a function of SDL_free_func type and called - * with the passed `mem` pointer when closing the stream. By default it is - * unset, i.e., the memory will not be freed. + * - `SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC`: if this property is set to a + * non-NULL value it will be interpreted as a function of SDL_free_func type + * and called with the passed `mem` pointer when closing the stream. By + * default it is unset, i.e., the memory will not be freed. * * \param mem a pointer to a buffer to feed an SDL_IOStream stream. * \param size the buffer size, in bytes. @@ -354,10 +354,10 @@ extern SDL_DECLSPEC SDL_IOStream * SDLCALL SDL_IOFromMem(void *mem, size_t size) * * Additionally, the following properties are recognized: * - * - `SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC`: if this property is set to a non-NULL - * value it will be interpreted as a function of SDL_free_func type and called - * with the passed `mem` pointer when closing the stream. By default it is - * unset, i.e., the memory will not be freed. + * - `SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC`: if this property is set to a + * non-NULL value it will be interpreted as a function of SDL_free_func type + * and called with the passed `mem` pointer when closing the stream. By + * default it is unset, i.e., the memory will not be freed. * * \param mem a pointer to a read-only buffer to feed an SDL_IOStream stream. * \param size the buffer size, in bytes. From 3b9db3dd62dee79da8f01e64ac51769081bac639 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Thu, 17 Jul 2025 15:38:39 -0700 Subject: [PATCH 054/103] Added support for Windows GameInput 2.0 --- src/core/windows/SDL_gameinput.h | 4 +- src/joystick/gdk/SDL_gameinputjoystick.cpp | 66 +++++++++++----------- test/testcontroller.c | 6 +- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/src/core/windows/SDL_gameinput.h b/src/core/windows/SDL_gameinput.h index 4d2beb5647..9d44de8109 100644 --- a/src/core/windows/SDL_gameinput.h +++ b/src/core/windows/SDL_gameinput.h @@ -31,7 +31,9 @@ #define GAMEINPUT_API_VERSION 0 #endif -#if GAMEINPUT_API_VERSION == 1 +#if GAMEINPUT_API_VERSION == 2 +using namespace GameInput::v2; +#elif GAMEINPUT_API_VERSION == 1 using namespace GameInput::v1; #endif diff --git a/src/joystick/gdk/SDL_gameinputjoystick.cpp b/src/joystick/gdk/SDL_gameinputjoystick.cpp index 67cf51fa5a..3405816990 100644 --- a/src/joystick/gdk/SDL_gameinputjoystick.cpp +++ b/src/joystick/gdk/SDL_gameinputjoystick.cpp @@ -34,6 +34,11 @@ #define SDL_GAMEINPUT_DEFAULT false #endif +// Enable sensor support in GameInput 2.0, once we have a device that can be used for testing +#if GAMEINPUT_API_VERSION >= 2 +//#define GAMEINPUT_SENSOR_SUPPORT +#endif + enum { SDL_GAMEPAD_BUTTON_GAMEINPUT_SHARE = 11 @@ -474,21 +479,15 @@ static bool GAMEINPUT_JoystickOpen(SDL_Joystick *joystick, int device_index) SDL_SetBooleanProperty(SDL_GetJoystickProperties(joystick), SDL_PROP_JOYSTICK_CAP_TRIGGER_RUMBLE_BOOLEAN, true); } -#if 0 - if (info->supportedInput & GameInputKindTouch) { - SDL_PrivateJoystickAddTouchpad(joystick, info->touchPointCount); - } - - if (info->supportedInput & GameInputKindMotion) { +#ifdef GAMEINPUT_SENSOR_SUPPORT + if (info->supportedInput & GameInputKindSensors) { // FIXME: What's the sensor update rate? - SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_GYRO, 250.0f); - SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL, 250.0f); - } - - if (info->capabilities & GameInputDeviceCapabilityWireless) { - joystick->connection_state = SDL_JOYSTICK_CONNECTION_WIRELESS; - } else { - joystick->connection_state = SDL_JOYSTICK_CONNECTION_WIRED; + if (info->sensorsInfo->supportedSensors & GameInputSensorsGyrometer) { + SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_GYRO, 250.0f); + } + if (info->sensorsInfo->supportedSensors & GameInputSensorsAccelerometer) { + SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL, 250.0f); + } } #endif return true; @@ -667,29 +666,30 @@ static void GAMEINPUT_JoystickUpdate(SDL_Joystick *joystick) } } -#if 0 - if (info->supportedInput & GameInputKindTouch) { - GameInputTouchState *touch_state = SDL_stack_alloc(GameInputTouchState, info->touchPointCount); - if (touch_state) { - uint32_t i; - uint32_t touch_count = IGameInputReading_GetTouchState(reading, info->touchPointCount, touch_state); - for (i = 0; i < touch_count; ++i) { - GameInputTouchState *touch = &touch_state[i]; - // FIXME: We should use touch->touchId to track fingers instead of using i below - SDL_SendJoystickTouchpad(timestamp, joystick, 0, i, true, touch->positionX * info->touchSensorInfo[i].resolutionX, touch->positionY * info->touchSensorInfo[0].resolutionY, touch->pressure); - } - SDL_stack_free(touch_state); - } - } - +#ifdef GAMEINPUT_SENSOR_SUPPORT if (hwdata->report_sensors) { - GameInputMotionState motion_state; + GameInputSensorsState sensor_state; - if (IGameInputReading_GetMotionState(reading, &motion_state)) { - // FIXME: How do we interpret the motion data? + if (reading->GetSensorsState(&sensor_state)) { + if ((info->sensorsInfo->supportedSensors & GameInputSensorsGyrometer) != 0) { + float data[3] = { + sensor_state.angularVelocityInRadPerSecX, + sensor_state.angularVelocityInRadPerSecY, + sensor_state.angularVelocityInRadPerSecZ + }; + SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_GYRO, timestamp, data, SDL_arraysize(data)); + } + if ((info->sensorsInfo->supportedSensors & GameInputSensorsAccelerometer) != 0) { + float data[3] = { + sensor_state.accelerationInGX * SDL_STANDARD_GRAVITY, + sensor_state.accelerationInGY * SDL_STANDARD_GRAVITY, + sensor_state.accelerationInGZ * SDL_STANDARD_GRAVITY + }; + SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_ACCEL, timestamp, data, SDL_arraysize(data)); + } } } -#endif +#endif // GAMEINPUT_SENSOR_SUPPORT reading->Release(); diff --git a/test/testcontroller.c b/test/testcontroller.c index 0530d1a992..ca8d297065 100644 --- a/test/testcontroller.c +++ b/test/testcontroller.c @@ -2376,12 +2376,16 @@ SDL_AppResult SDLCALL SDL_AppInit(void **appstate, int argc, char *argv[]) return SDL_APP_FAILURE; } - SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "1"); + SDL_SetHint(SDL_HINT_XINPUT_ENABLED, "0"); + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "0"); + SDL_SetHint(SDL_HINT_JOYSTICK_DIRECTINPUT, "0"); + SDL_SetHint(SDL_HINT_JOYSTICK_WGI, "0"); SDL_SetHint(SDL_HINT_JOYSTICK_ENHANCED_REPORTS, "auto"); SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_STEAM, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_ROG_CHAKRAM, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_LINUX_DEADZONES, "1"); + SDL_SetHint(SDL_HINT_JOYSTICK_GAMEINPUT, "1"); /* Enable input debug logging */ SDL_SetLogPriority(SDL_LOG_CATEGORY_INPUT, SDL_LOG_PRIORITY_DEBUG); From ada44eaa109f40c4fd6cea18c2068245bf2501a8 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Thu, 17 Jul 2025 18:41:43 -0700 Subject: [PATCH 055/103] testcontroller: reverted GameInput test code --- test/testcontroller.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/testcontroller.c b/test/testcontroller.c index ca8d297065..0530d1a992 100644 --- a/test/testcontroller.c +++ b/test/testcontroller.c @@ -2376,16 +2376,12 @@ SDL_AppResult SDLCALL SDL_AppInit(void **appstate, int argc, char *argv[]) return SDL_APP_FAILURE; } - SDL_SetHint(SDL_HINT_XINPUT_ENABLED, "0"); - SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "0"); - SDL_SetHint(SDL_HINT_JOYSTICK_DIRECTINPUT, "0"); - SDL_SetHint(SDL_HINT_JOYSTICK_WGI, "0"); + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_ENHANCED_REPORTS, "auto"); SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_STEAM, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_ROG_CHAKRAM, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_LINUX_DEADZONES, "1"); - SDL_SetHint(SDL_HINT_JOYSTICK_GAMEINPUT, "1"); /* Enable input debug logging */ SDL_SetLogPriority(SDL_LOG_CATEGORY_INPUT, SDL_LOG_PRIORITY_DEBUG); From de20b731f22c7983eb334fc4146c6927a82a792e Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Thu, 17 Jul 2025 19:32:29 -0700 Subject: [PATCH 056/103] Removed unnecessary cast --- src/joystick/hidapi/SDL_hidapi_sinput.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c index f9af55aec6..a551945f48 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.c +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -448,11 +448,11 @@ static bool HIDAPI_DriverSInput_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joys joystick->naxes = axes; if (ctx->accelerometer_supported) { - SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL, (float)1000.0f/ctx->polling_rate_ms); + SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL, 1000.0f / ctx->polling_rate_ms); } if (ctx->gyroscope_supported) { - SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_GYRO, (float)1000.0f / ctx->polling_rate_ms); + SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_GYRO, 1000.0f / ctx->polling_rate_ms); } if (ctx->touchpad_supported) { From 8f79a6185adbea31707986f7de68f3bdb07220a7 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Thu, 17 Jul 2025 19:32:46 -0700 Subject: [PATCH 057/103] Fixed the mapping for the GC Ultimate controller --- src/joystick/SDL_gamepad.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index 59d50bffa2..67cdc8f4be 100644 --- a/src/joystick/SDL_gamepad.c +++ b/src/joystick/SDL_gamepad.c @@ -810,7 +810,7 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid) case USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE: // GC Ultimate Map - SDL_strlcat(mapping_string, "a:b0,b:b2,back:b15,dpdown:b5,dpleft:b6,dpright:b7,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b8,lefttrigger:a4,leftx:a0,lefty:a1,misc1:b17,misc3:b18,paddle1:b13,paddle2:b12,rightshoulder:b11,rightstick:b9,righttrigger:a5,rightx:a2,righty:a3,start:b14,x:b1,y:b3,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", sizeof(mapping_string)); + SDL_strlcat(mapping_string, "a:b0,b:b2,back:b15,dpdown:b5,dpleft:b6,dpright:b7,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b8,lefttrigger:a4,leftx:a0,lefty:a1,misc1:b17,misc2:b18,rightshoulder:b11,rightstick:b9,righttrigger:a5,rightx:a2,righty:a3,start:b14,x:b1,y:b3,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", sizeof(mapping_string)); break; case USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC: From 45674d00244a3b42ba5b9cf1963d45a9737973e9 Mon Sep 17 00:00:00 2001 From: Mitch Cairns Date: Thu, 17 Jul 2025 19:54:27 -0700 Subject: [PATCH 058/103] GC Ultimate Misc Re-implement digital button press for GC Ultimate (misc3:b12,misc4:b13) --- src/joystick/SDL_gamepad.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index 67cdc8f4be..5871a7fd95 100644 --- a/src/joystick/SDL_gamepad.c +++ b/src/joystick/SDL_gamepad.c @@ -810,7 +810,7 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid) case USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE: // GC Ultimate Map - SDL_strlcat(mapping_string, "a:b0,b:b2,back:b15,dpdown:b5,dpleft:b6,dpright:b7,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b8,lefttrigger:a4,leftx:a0,lefty:a1,misc1:b17,misc2:b18,rightshoulder:b11,rightstick:b9,righttrigger:a5,rightx:a2,righty:a3,start:b14,x:b1,y:b3,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", sizeof(mapping_string)); + SDL_strlcat(mapping_string, "a:b0,b:b2,back:b15,dpdown:b5,dpleft:b6,dpright:b7,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b8,lefttrigger:a4,leftx:a0,lefty:a1,misc1:b17,misc2:b18,rightshoulder:b11,rightstick:b9,righttrigger:a5,rightx:a2,righty:a3,start:b14,x:b1,y:b3,misc3:b12,misc4:b13,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", sizeof(mapping_string)); break; case USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC: From e70ecb37c19e0f0ca32ea4f16954cbb0dac30dd1 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Thu, 17 Jul 2025 20:51:29 -0700 Subject: [PATCH 059/103] Implement the D-pad as a hat for SInput controllers This lets games that use the joystick API handle the D-pad the same way as other controllers --- src/joystick/SDL_gamepad.c | 17 ++--- src/joystick/hidapi/SDL_hidapi_sinput.c | 86 ++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 16 deletions(-) diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index 5871a7fd95..36ea97e004 100644 --- a/src/joystick/SDL_gamepad.c +++ b/src/joystick/SDL_gamepad.c @@ -805,24 +805,25 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid) switch (product) { case USB_PRODUCT_HANDHELDLEGEND_PROGCC: // ProGCC Mapping - SDL_strlcat(mapping_string, "a:b1,b:b0,back:b15,dpdown:b5,dpleft:b6,dpright:b7,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b8,lefttrigger:b12,leftx:a0,lefty:a1,misc1:b17,rightshoulder:b11,rightstick:b9,righttrigger:b13,rightx:a2,righty:a3,start:b14,x:b3,y:b2,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", sizeof(mapping_string)); + SDL_strlcat(mapping_string, "a:b1,b:b0,back:b11,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b4,lefttrigger:b8,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b7,rightstick:b5,righttrigger:b9,rightx:a2,righty:a3,start:b10,x:b3,y:b2,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", sizeof(mapping_string)); break; case USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE: // GC Ultimate Map - SDL_strlcat(mapping_string, "a:b0,b:b2,back:b15,dpdown:b5,dpleft:b6,dpright:b7,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b8,lefttrigger:a4,leftx:a0,lefty:a1,misc1:b17,misc2:b18,rightshoulder:b11,rightstick:b9,righttrigger:a5,rightx:a2,righty:a3,start:b14,x:b1,y:b3,misc3:b12,misc4:b13,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", sizeof(mapping_string)); + SDL_strlcat(mapping_string, "a:b0,b:b2,back:b11,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b4,lefttrigger:a4,leftx:a0,lefty:a1,misc1:b13,misc2:b14,rightshoulder:b7,rightstick:b5,righttrigger:a5,rightx:a2,righty:a3,start:b10,x:b1,y:b3,misc3:b8,misc4:b9,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", sizeof(mapping_string)); break; case USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC: - if (u_id != 1) { + switch (u_id) { + case 1: + // SuperGamepad+ Map + SDL_strlcat(mapping_string, "a:b1,b:b0,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,rightshoulder:b5,start:b6,x:b3,y:b2,", sizeof(mapping_string)); + break; + default: + // Unknown mapping return NULL; } - // SuperGamepad+ Map - if (u_id == 1) { - SDL_strlcat(mapping_string, "a:b1,b:b0,back:b11,dpdown:b5,dpleft:b6,dpright:b7,dpup:b4,leftshoulder:b8,rightshoulder:b9,start:b10,x:b3,y:b2,", sizeof(mapping_string)); - } - // Apply face style switch (face_style) { default: diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c index a551945f48..6a616821e6 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.c +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -30,6 +30,9 @@ #ifdef SDL_JOYSTICK_HIDAPI_SINPUT +/*****************************************************************************************************/ +// This protocol is documented at: +// https://docs.handheldlegend.com/s/sinput/doc/sinput-hid-protocol-TkPYWlDMAg /*****************************************************************************************************/ // Define this if you want to log all packets from the controller @@ -83,6 +86,39 @@ #define SINPUT_REPORT_IDX_TOUCH2_Y 43 #define SINPUT_REPORT_IDX_TOUCH2_P 45 +#define SINPUT_BUTTON_IDX_SOUTH 0 +#define SINPUT_BUTTON_IDX_EAST 1 +#define SINPUT_BUTTON_IDX_WEST 2 +#define SINPUT_BUTTON_IDX_NORTH 3 +#define SINPUT_BUTTON_IDX_DPAD_UP 4 +#define SINPUT_BUTTON_IDX_DPAD_DOWN 5 +#define SINPUT_BUTTON_IDX_DPAD_LEFT 6 +#define SINPUT_BUTTON_IDX_DPAD_RIGHT 7 +#define SINPUT_BUTTON_IDX_LEFT_STICK 8 +#define SINPUT_BUTTON_IDX_RIGHT_STICK 9 +#define SINPUT_BUTTON_IDX_LEFT_BUMPER 10 +#define SINPUT_BUTTON_IDX_RIGHT_BUMPER 11 +#define SINPUT_BUTTON_IDX_LEFT_TRIGGER 12 +#define SINPUT_BUTTON_IDX_RIGHT_TRIGGER 13 +#define SINPUT_BUTTON_IDX_LEFT_PADDLE1 14 +#define SINPUT_BUTTON_IDX_RIGHT_PADDLE1 15 +#define SINPUT_BUTTON_IDX_START 16 +#define SINPUT_BUTTON_IDX_BACK 17 +#define SINPUT_BUTTON_IDX_GUIDE 18 +#define SINPUT_BUTTON_IDX_CAPTURE 19 +#define SINPUT_BUTTON_IDX_LEFT_PADDLE2 20 +#define SINPUT_BUTTON_IDX_RIGHT_PADDLE2 21 +#define SINPUT_BUTTON_IDX_TOUCHPAD1 22 +#define SINPUT_BUTTON_IDX_TOUCHPAD2 23 +#define SINPUT_BUTTON_IDX_POWER 24 +#define SINPUT_BUTTON_IDX_MISC4 25 +#define SINPUT_BUTTON_IDX_MISC5 26 +#define SINPUT_BUTTON_IDX_MISC6 27 +#define SINPUT_BUTTON_IDX_MISC7 28 +#define SINPUT_BUTTON_IDX_MISC8 29 +#define SINPUT_BUTTON_IDX_MISC9 30 +#define SINPUT_BUTTON_IDX_MISC10 31 + #define SINPUT_REPORT_IDX_COMMAND_RESPONSE_ID 1 #define SINPUT_REPORT_IDX_COMMAND_RESPONSE_BULK 2 @@ -160,6 +196,7 @@ typedef struct bool right_analog_stick_supported; bool left_analog_trigger_supported; bool right_analog_trigger_supported; + bool dpad_supported; bool touchpad_supported; Uint8 touchpad_count; // 2 touchpads maximum @@ -257,6 +294,17 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) } } + // Convert DPAD to hat + const DPAD_MASK = (1 << SINPUT_BUTTON_IDX_DPAD_UP) | + (1 << SINPUT_BUTTON_IDX_DPAD_DOWN) | + (1 << SINPUT_BUTTON_IDX_DPAD_LEFT) | + (1 << SINPUT_BUTTON_IDX_DPAD_RIGHT); + if ((ctx->usage_masks[0] & DPAD_MASK) == DPAD_MASK) { + ctx->dpad_supported = true; + ctx->usage_masks[0] &= ~DPAD_MASK; + ctx->buttons_count -= 4; + } + #if defined(DEBUG_SINPUT_INIT) SDL_Log("Buttons count: %d", ctx->buttons_count); #endif @@ -281,7 +329,7 @@ static bool RetrieveSDLFeatures(SDL_HIDAPI_Device *device) { int written = 0; - // Attempt to send the SDL features get command. + // Attempt to send the SDL features get command. for (int attempt = 0; attempt < 8; ++attempt) { const Uint8 featuresGetCommand[SINPUT_DEVICE_REPORT_COMMAND_SIZE] = { SINPUT_DEVICE_REPORT_ID_OUTPUT_CMDDAT, SINPUT_DEVICE_COMMAND_FEATURES }; // This write will occasionally return -1, so ignore failure here and try again @@ -295,7 +343,7 @@ static bool RetrieveSDLFeatures(SDL_HIDAPI_Device *device) if (written < SINPUT_DEVICE_REPORT_COMMAND_SIZE) { SDL_SetError("SInput device SDL Features GET command could not write"); return false; - } + } int read = 0; @@ -380,7 +428,7 @@ static bool HIDAPI_DriverSInput_InitDevice(SDL_HIDAPI_Device *device) if (!RetrieveSDLFeatures(device)) { return false; } - + return HIDAPI_JoystickConnected(device, NULL); } @@ -447,6 +495,10 @@ static bool HIDAPI_DriverSInput_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joys joystick->naxes = axes; + if (ctx->dpad_supported) { + joystick->nhats = 1; + } + if (ctx->accelerometer_supported) { SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL, 1000.0f / ctx->polling_rate_ms); } @@ -522,7 +574,7 @@ static Uint32 HIDAPI_DriverSInput_GetJoystickCapabilities(SDL_HIDAPI_Device *dev if (ctx->joystick_rgb_supported) { caps |= SDL_JOYSTICK_CAP_RGB_LED; } - + return caps; } @@ -594,6 +646,24 @@ static void HIDAPI_DriverSInput_HandleStatePacket(SDL_Joystick *joystick, SDL_Dr } } + if (ctx->dpad_supported) { + Uint8 hat = SDL_HAT_CENTERED; + + if (data[SINPUT_REPORT_IDX_BUTTONS_0] & (1 << SINPUT_BUTTON_IDX_DPAD_UP)) { + hat |= SDL_HAT_UP; + } + if (data[SINPUT_REPORT_IDX_BUTTONS_0] & (1 << SINPUT_BUTTON_IDX_DPAD_DOWN)) { + hat |= SDL_HAT_DOWN; + } + if (data[SINPUT_REPORT_IDX_BUTTONS_0] & (1 << SINPUT_BUTTON_IDX_DPAD_LEFT)) { + hat |= SDL_HAT_LEFT; + } + if (data[SINPUT_REPORT_IDX_BUTTONS_0] & (1 << SINPUT_BUTTON_IDX_DPAD_RIGHT)) { + hat |= SDL_HAT_RIGHT; + } + SDL_SendJoystickHat(timestamp, joystick, 0, hat); + } + // Analog inputs map to a signed Sint16 range of -32768 to 32767 from the device. // Use an axis index because not all gamepads will have the same axis inputs. Uint8 axis_idx = 0; @@ -609,7 +679,7 @@ static void HIDAPI_DriverSInput_HandleStatePacket(SDL_Joystick *joystick, SDL_Dr SDL_SendJoystickAxis(timestamp, joystick, axis_idx, axis); ++axis_idx; } - + // Right Analog Stick axis = 0; // Reset axis value for joystick if (ctx->right_analog_stick_supported) { @@ -629,7 +699,7 @@ static void HIDAPI_DriverSInput_HandleStatePacket(SDL_Joystick *joystick, SDL_Dr SDL_SendJoystickAxis(timestamp, joystick, axis_idx, axis); ++axis_idx; } - + // Right Analog Trigger axis = SDL_MIN_SINT16; // Reset axis value for trigger if (ctx->right_analog_trigger_supported) { @@ -670,7 +740,7 @@ static void HIDAPI_DriverSInput_HandleStatePacket(SDL_Joystick *joystick, SDL_Dr if (state > 0) { SDL_SendJoystickPowerInfo(joystick, state, percent); - } + } } // Extract the IMU timestamp delta (in microseconds) @@ -710,7 +780,7 @@ static void HIDAPI_DriverSInput_HandleStatePacket(SDL_Joystick *joystick, SDL_Dr // Process Gyroscope if (ctx->gyroscope_supported) { - + gyro = EXTRACTSINT16(data, SINPUT_REPORT_IDX_IMU_GYRO_Y); imu_values[2] = -(float)gyro * ctx->gyroScale; // Y-axis rotation From ad52ebf985bdeec339f3bc46cbdc7dc43b5f3062 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Thu, 17 Jul 2025 20:56:41 -0700 Subject: [PATCH 060/103] Fixed build --- src/joystick/hidapi/SDL_hidapi_sinput.c | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c index 6a616821e6..bc4aa79845 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.c +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -295,10 +295,10 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) } // Convert DPAD to hat - const DPAD_MASK = (1 << SINPUT_BUTTON_IDX_DPAD_UP) | - (1 << SINPUT_BUTTON_IDX_DPAD_DOWN) | - (1 << SINPUT_BUTTON_IDX_DPAD_LEFT) | - (1 << SINPUT_BUTTON_IDX_DPAD_RIGHT); + const int DPAD_MASK = (1 << SINPUT_BUTTON_IDX_DPAD_UP) | + (1 << SINPUT_BUTTON_IDX_DPAD_DOWN) | + (1 << SINPUT_BUTTON_IDX_DPAD_LEFT) | + (1 << SINPUT_BUTTON_IDX_DPAD_RIGHT); if ((ctx->usage_masks[0] & DPAD_MASK) == DPAD_MASK) { ctx->dpad_supported = true; ctx->usage_masks[0] &= ~DPAD_MASK; @@ -711,7 +711,7 @@ static void HIDAPI_DriverSInput_HandleStatePacket(SDL_Joystick *joystick, SDL_Dr if (ctx->last_state[SINPUT_REPORT_IDX_PLUG_STATUS] != data[SINPUT_REPORT_IDX_PLUG_STATUS] || ctx->last_state[SINPUT_REPORT_IDX_CHARGE_LEVEL] != data[SINPUT_REPORT_IDX_CHARGE_LEVEL]) { - SDL_PowerState state = SDL_POWERSTATE_NO_BATTERY; + SDL_PowerState state = SDL_POWERSTATE_UNKNOWN; Uint8 status = data[SINPUT_REPORT_IDX_PLUG_STATUS]; int percent = data[SINPUT_REPORT_IDX_CHARGE_LEVEL]; @@ -732,13 +732,11 @@ static void HIDAPI_DriverSInput_HandleStatePacket(SDL_Joystick *joystick, SDL_Dr case 4: state = SDL_POWERSTATE_ON_BATTERY; break; - default: // Wired/No Battery Supported - state = SDL_POWERSTATE_UNKNOWN; - percent = 0; + default: break; } - if (state > 0) { + if (state != SDL_POWERSTATE_UNKNOWN) { SDL_SendJoystickPowerInfo(joystick, state, percent); } } From 46ea7aa80eade24eebd9f9a5be6da3ce916dd6ff Mon Sep 17 00:00:00 2001 From: Mitch Cairns Date: Thu, 17 Jul 2025 21:11:49 -0700 Subject: [PATCH 061/103] SInput Serial from MAC --- src/joystick/hidapi/SDL_hidapi_sinput.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c index bc4aa79845..9c4e827447 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.c +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -313,6 +313,15 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) ctx->touchpad_count = data[16]; ctx->touchpad_finger_count = data[17]; + // Get device Serial - MAC address + char serial[18]; + (void)SDL_snprintf(serial, sizeof(serial), "%.2x-%.2x-%.2x-%.2x-%.2x-%.2x", + data[23], data[22], data[21], data[20], data[19], data[18]); +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("Serial num: %s", serial); +#endif + HIDAPI_SetDeviceSerial(device, serial); + #if defined(DEBUG_SINPUT_INIT) SDL_Log("Accelerometer Range: %d", ctx->accelRange); #endif From 34d9db365716d32882493f33c7346451676368f0 Mon Sep 17 00:00:00 2001 From: mitchellcairns Date: Fri, 18 Jul 2025 08:08:24 -0700 Subject: [PATCH 062/103] SInput Serial MAC Fix (#13388) * Resolve MAC address Order --- src/joystick/hidapi/SDL_hidapi_sinput.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c index 9c4e827447..eae3f12ae5 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.c +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -316,7 +316,7 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) // Get device Serial - MAC address char serial[18]; (void)SDL_snprintf(serial, sizeof(serial), "%.2x-%.2x-%.2x-%.2x-%.2x-%.2x", - data[23], data[22], data[21], data[20], data[19], data[18]); + data[18], data[19], data[20], data[21], data[22], data[23]); #if defined(DEBUG_SINPUT_INIT) SDL_Log("Serial num: %s", serial); #endif From 9b00f3a72885d71c69f56cd14402e6af872bbc2e Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Wed, 9 Jul 2025 12:57:14 -0400 Subject: [PATCH 063/103] wayland: Optimize keymap creation Iterate over the list of keys only once while assembling all related state for key levels and modifiers. --- src/video/wayland/SDL_waylandevents.c | 133 +++++++++++--------------- src/video/wayland/SDL_waylandsym.h | 9 +- 2 files changed, 62 insertions(+), 80 deletions(-) diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c index 5f640321eb..fcf43e6e0f 100644 --- a/src/video/wayland/SDL_waylandevents.c +++ b/src/video/wayland/SDL_waylandevents.c @@ -1410,103 +1410,78 @@ static const struct wl_touch_listener touch_listener = { touch_handler_orientation // Version 6 }; -typedef struct Wayland_KeymapBuilderState -{ - SDL_Keymap *keymap; - struct xkb_state *state; - SDL_Keymod modstate; -} Wayland_KeymapBuilderState; - static void Wayland_keymap_iter(struct xkb_keymap *keymap, xkb_keycode_t key, void *data) { - Wayland_KeymapBuilderState *sdlKeymap = (Wayland_KeymapBuilderState *)data; + SDL_WaylandSeat *seat = (SDL_WaylandSeat *)data; const xkb_keysym_t *syms; + const xkb_mod_mask_t xkb_valid_mod_mask = seat->keyboard.xkb.shift_mask | + seat->keyboard.xkb.alt_mask | + seat->keyboard.xkb.gui_mask | + seat->keyboard.xkb.level3_mask | + seat->keyboard.xkb.level5_mask | + seat->keyboard.xkb.caps_mask; const SDL_Scancode scancode = SDL_GetScancodeFromTable(SDL_SCANCODE_TABLE_XFREE86_2, (key - 8)); if (scancode == SDL_SCANCODE_UNKNOWN) { return; } - if (WAYLAND_xkb_state_key_get_syms(sdlKeymap->state, key, &syms) > 0) { - SDL_Keycode keycode = SDL_GetKeyCodeFromKeySym(syms[0], key, sdlKeymap->modstate); + const xkb_level_index_t num_levels = WAYLAND_xkb_keymap_num_levels_for_key(seat->keyboard.xkb.keymap, key, seat->keyboard.xkb.current_layout); + for (xkb_level_index_t level = 0; level < num_levels; ++level) { + if (WAYLAND_xkb_keymap_key_get_syms_by_level(seat->keyboard.xkb.keymap, key, seat->keyboard.xkb.current_layout, level, &syms) > 0) { + xkb_mod_mask_t xkb_mod_masks[16]; + const size_t num_masks = WAYLAND_xkb_keymap_key_get_mods_for_level(seat->keyboard.xkb.keymap, key, seat->keyboard.xkb.current_layout, level, xkb_mod_masks, SDL_arraysize(xkb_mod_masks)); + for (size_t mask = 0; mask < num_masks; ++mask) { + // Ignore this modifier set if it uses unsupported modifier types. + if ((xkb_mod_masks[mask] | xkb_valid_mod_mask) != xkb_valid_mod_mask) { + continue; + } - if (!keycode) { - switch (scancode) { - case SDL_SCANCODE_RETURN: - keycode = SDLK_RETURN; - break; - case SDL_SCANCODE_ESCAPE: - keycode = SDLK_ESCAPE; - break; - case SDL_SCANCODE_BACKSPACE: - keycode = SDLK_BACKSPACE; - break; - case SDL_SCANCODE_DELETE: - keycode = SDLK_DELETE; - break; - default: - keycode = SDL_SCANCODE_TO_KEYCODE(scancode); - break; + const SDL_Keymod sdl_mod = (xkb_mod_masks[mask] & seat->keyboard.xkb.shift_mask ? SDL_KMOD_SHIFT : 0) | + (xkb_mod_masks[mask] & seat->keyboard.xkb.alt_mask ? SDL_KMOD_ALT : 0) | + (xkb_mod_masks[mask] & seat->keyboard.xkb.gui_mask ? SDL_KMOD_GUI : 0) | + (xkb_mod_masks[mask] & seat->keyboard.xkb.level3_mask ? SDL_KMOD_MODE : 0) | + (xkb_mod_masks[mask] & seat->keyboard.xkb.level5_mask ? SDL_KMOD_LEVEL5 : 0) | + (xkb_mod_masks[mask] & seat->keyboard.xkb.caps_mask ? SDL_KMOD_CAPS : 0); + + SDL_Keycode keycode = SDL_GetKeyCodeFromKeySym(syms[0], key, sdl_mod); + + if (!keycode) { + switch (scancode) { + case SDL_SCANCODE_RETURN: + keycode = SDLK_RETURN; + break; + case SDL_SCANCODE_ESCAPE: + keycode = SDLK_ESCAPE; + break; + case SDL_SCANCODE_BACKSPACE: + keycode = SDLK_BACKSPACE; + break; + case SDL_SCANCODE_DELETE: + keycode = SDLK_DELETE; + break; + default: + keycode = SDL_SCANCODE_TO_KEYCODE(scancode); + break; + } + } + + SDL_SetKeymapEntry(seat->keyboard.sdl_keymap, scancode, sdl_mod, keycode); } } - - SDL_SetKeymapEntry(sdlKeymap->keymap, scancode, sdlKeymap->modstate, keycode); } } static void Wayland_UpdateKeymap(SDL_WaylandSeat *seat) { - struct Keymod_masks - { - SDL_Keymod sdl_mask; - xkb_mod_mask_t xkb_mask; - } const keymod_masks[] = { - { SDL_KMOD_NONE, 0 }, - { SDL_KMOD_SHIFT, seat->keyboard.xkb.shift_mask }, - { SDL_KMOD_CAPS, seat->keyboard.xkb.caps_mask }, - { SDL_KMOD_SHIFT | SDL_KMOD_CAPS, seat->keyboard.xkb.shift_mask | seat->keyboard.xkb.caps_mask }, - { SDL_KMOD_MODE, seat->keyboard.xkb.level3_mask }, - { SDL_KMOD_MODE | SDL_KMOD_SHIFT, seat->keyboard.xkb.level3_mask | seat->keyboard.xkb.shift_mask }, - { SDL_KMOD_MODE | SDL_KMOD_CAPS, seat->keyboard.xkb.level3_mask | seat->keyboard.xkb.caps_mask }, - { SDL_KMOD_MODE | SDL_KMOD_SHIFT | SDL_KMOD_CAPS, seat->keyboard.xkb.level3_mask | seat->keyboard.xkb.shift_mask | seat->keyboard.xkb.caps_mask }, - { SDL_KMOD_LEVEL5, seat->keyboard.xkb.level5_mask }, - { SDL_KMOD_LEVEL5 | SDL_KMOD_SHIFT, seat->keyboard.xkb.level5_mask | seat->keyboard.xkb.shift_mask }, - { SDL_KMOD_LEVEL5 | SDL_KMOD_CAPS, seat->keyboard.xkb.level5_mask | seat->keyboard.xkb.caps_mask }, - { SDL_KMOD_LEVEL5 | SDL_KMOD_SHIFT | SDL_KMOD_CAPS, seat->keyboard.xkb.level5_mask | seat->keyboard.xkb.shift_mask | seat->keyboard.xkb.caps_mask }, - { SDL_KMOD_LEVEL5 | SDL_KMOD_MODE, seat->keyboard.xkb.level5_mask | seat->keyboard.xkb.level3_mask }, - { SDL_KMOD_LEVEL5 | SDL_KMOD_MODE | SDL_KMOD_SHIFT, seat->keyboard.xkb.level5_mask | seat->keyboard.xkb.level3_mask | seat->keyboard.xkb.shift_mask }, - { SDL_KMOD_LEVEL5 | SDL_KMOD_MODE | SDL_KMOD_CAPS, seat->keyboard.xkb.level5_mask | seat->keyboard.xkb.level3_mask | seat->keyboard.xkb.caps_mask }, - { SDL_KMOD_LEVEL5 | SDL_KMOD_MODE | SDL_KMOD_SHIFT | SDL_KMOD_CAPS, seat->keyboard.xkb.level5_mask | seat->keyboard.xkb.level3_mask | seat->keyboard.xkb.shift_mask | seat->keyboard.xkb.caps_mask }, - }; - if (!seat->keyboard.is_virtual) { - Wayland_KeymapBuilderState keymap; - - keymap.keymap = SDL_CreateKeymap(false); - if (!keymap.keymap) { - return; - } - - keymap.state = WAYLAND_xkb_state_new(seat->keyboard.xkb.keymap); - if (!keymap.state) { - SDL_SetError("failed to create XKB state"); - SDL_DestroyKeymap(keymap.keymap); - return; - } - - for (int i = 0; i < SDL_arraysize(keymod_masks); ++i) { - keymap.modstate = keymod_masks[i].sdl_mask; - WAYLAND_xkb_state_update_mask(keymap.state, - keymod_masks[i].xkb_mask & (seat->keyboard.xkb.shift_mask | seat->keyboard.xkb.level3_mask | seat->keyboard.xkb.level5_mask), 0, keymod_masks[i].xkb_mask & seat->keyboard.xkb.caps_mask, - 0, 0, seat->keyboard.xkb.current_layout); - WAYLAND_xkb_keymap_key_for_each(seat->keyboard.xkb.keymap, - Wayland_keymap_iter, - &keymap); - } - - WAYLAND_xkb_state_unref(keymap.state); - SDL_SetKeymap(keymap.keymap, true); SDL_DestroyKeymap(seat->keyboard.sdl_keymap); - seat->keyboard.sdl_keymap = keymap.keymap; + seat->keyboard.sdl_keymap = SDL_CreateKeymap(false); + if (!seat->keyboard.sdl_keymap) { + return; + } + + WAYLAND_xkb_keymap_key_for_each(seat->keyboard.xkb.keymap, Wayland_keymap_iter, seat); + SDL_SetKeymap(seat->keyboard.sdl_keymap, true); } else { // Virtual keyboards use the default keymap. SDL_SetKeymap(NULL, true); diff --git a/src/video/wayland/SDL_waylandsym.h b/src/video/wayland/SDL_waylandsym.h index d04223f3b4..5b0a0939c5 100644 --- a/src/video/wayland/SDL_waylandsym.h +++ b/src/video/wayland/SDL_waylandsym.h @@ -153,8 +153,15 @@ SDL_WAYLAND_SYM(void, xkb_keymap_key_for_each, (struct xkb_keymap *, xkb_keymap_ SDL_WAYLAND_SYM(int, xkb_keymap_key_get_syms_by_level, (struct xkb_keymap *, xkb_keycode_t, xkb_layout_index_t, - xkb_layout_index_t, + xkb_level_index_t, const xkb_keysym_t **) ) +SDL_WAYLAND_SYM(xkb_level_index_t, xkb_keymap_num_levels_for_key, (struct xkb_keymap *, xkb_keycode_t, xkb_layout_index_t) ) +SDL_WAYLAND_SYM(size_t, xkb_keymap_key_get_mods_for_level, (struct xkb_keymap *, + xkb_keycode_t, + xkb_layout_index_t, + xkb_level_index_t, + xkb_mod_mask_t *, + size_t masks_size) ) SDL_WAYLAND_SYM(uint32_t, xkb_keysym_to_utf32, (xkb_keysym_t) ) SDL_WAYLAND_SYM(uint32_t, xkb_keymap_mod_get_index, (struct xkb_keymap *, const char *) ) From 9c9bb9cec7e50ccac6e6a75447ca1604ea69ca5e Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Fri, 11 Jul 2025 11:10:09 -0400 Subject: [PATCH 064/103] wayland: Only create/destroy the compose table when necessary Initializing the compose table is a very expensive operation, and only necessary if the locale envvar changed, which is often not the case when just changing the keymap, so don't destroy and recreate it whenever the keymap changes. The state only needs to be reset in this case. --- src/video/wayland/SDL_waylandevents.c | 51 ++++++++++++++++--------- src/video/wayland/SDL_waylandevents_c.h | 1 + 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c index fcf43e6e0f..08288718dd 100644 --- a/src/video/wayland/SDL_waylandevents.c +++ b/src/video/wayland/SDL_waylandevents.c @@ -1495,7 +1495,6 @@ static void keyboard_handle_keymap(void *data, struct wl_keyboard *keyboard, { SDL_WaylandSeat *seat = data; char *map_str; - const char *locale; if (!data) { close(fd); @@ -1588,7 +1587,7 @@ static void keyboard_handle_keymap(void *data, struct wl_keyboard *keyboard, */ // Look up the preferred locale, falling back to "C" as default - locale = SDL_getenv("LC_ALL"); + const char *locale = SDL_getenv("LC_ALL"); if (!locale) { locale = SDL_getenv("LC_CTYPE"); if (!locale) { @@ -1599,26 +1598,38 @@ static void keyboard_handle_keymap(void *data, struct wl_keyboard *keyboard, } } - // Set up XKB compose table - if (seat->keyboard.xkb.compose_table != NULL) { - WAYLAND_xkb_compose_table_unref(seat->keyboard.xkb.compose_table); - seat->keyboard.xkb.compose_table = NULL; - } - seat->keyboard.xkb.compose_table = WAYLAND_xkb_compose_table_new_from_locale(seat->display->xkb_context, - locale, XKB_COMPOSE_COMPILE_NO_FLAGS); - if (seat->keyboard.xkb.compose_table) { - // Set up XKB compose state - if (seat->keyboard.xkb.compose_state != NULL) { - WAYLAND_xkb_compose_state_unref(seat->keyboard.xkb.compose_state); - seat->keyboard.xkb.compose_state = NULL; - } - seat->keyboard.xkb.compose_state = WAYLAND_xkb_compose_state_new(seat->keyboard.xkb.compose_table, - XKB_COMPOSE_STATE_NO_FLAGS); - if (!seat->keyboard.xkb.compose_state) { - SDL_SetError("could not create XKB compose state"); + /* Set up the XKB compose table. + * + * This is a very slow operation, so it is only done during initialization, + * or if the locale envvar changed during runtime. + */ + if (!seat->keyboard.current_locale || SDL_strcmp(seat->keyboard.current_locale, locale) != 0) { + // Cache the current locale for later comparison. + SDL_free(seat->keyboard.current_locale); + seat->keyboard.current_locale = SDL_strdup(locale); + + if (seat->keyboard.xkb.compose_table != NULL) { WAYLAND_xkb_compose_table_unref(seat->keyboard.xkb.compose_table); seat->keyboard.xkb.compose_table = NULL; } + seat->keyboard.xkb.compose_table = WAYLAND_xkb_compose_table_new_from_locale(seat->display->xkb_context, + locale, XKB_COMPOSE_COMPILE_NO_FLAGS); + if (seat->keyboard.xkb.compose_table) { + // Set up XKB compose state + if (seat->keyboard.xkb.compose_state != NULL) { + WAYLAND_xkb_compose_state_unref(seat->keyboard.xkb.compose_state); + seat->keyboard.xkb.compose_state = NULL; + } + seat->keyboard.xkb.compose_state = WAYLAND_xkb_compose_state_new(seat->keyboard.xkb.compose_table, + XKB_COMPOSE_STATE_NO_FLAGS); + if (!seat->keyboard.xkb.compose_state) { + SDL_SetError("could not create XKB compose state"); + WAYLAND_xkb_compose_table_unref(seat->keyboard.xkb.compose_table); + seat->keyboard.xkb.compose_table = NULL; + } + } + } else if (seat->keyboard.xkb.compose_state) { + WAYLAND_xkb_compose_state_reset(seat->keyboard.xkb.compose_state); } } @@ -2216,6 +2227,8 @@ static void Wayland_SeatDestroyKeyboard(SDL_WaylandSeat *seat, bool send_event) } } + SDL_free(seat->keyboard.current_locale); + if (seat->keyboard.xkb.compose_state) { WAYLAND_xkb_compose_state_unref(seat->keyboard.xkb.compose_state); } diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h index 8f27ae978f..ffa779e250 100644 --- a/src/video/wayland/SDL_waylandevents_c.h +++ b/src/video/wayland/SDL_waylandevents_c.h @@ -76,6 +76,7 @@ typedef struct SDL_WaylandSeat struct zwp_keyboard_shortcuts_inhibitor_v1 *key_inhibitor; SDL_WindowData *focus; SDL_Keymap *sdl_keymap; + char *current_locale; SDL_WaylandKeyboardRepeat repeat; Uint64 highres_timestamp_ns; From ce9c6e40fd40f49f241549799129d0574b325056 Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Wed, 9 Jul 2025 13:32:14 -0400 Subject: [PATCH 065/103] wayland: Refactor keyboard layout handling Build all the available keyboard layouts at once; this adds a negligible bit of overhead when initially handling the keymap (which was already significantly lowered by previous commits), but reduces the later cost of changing layouts to just swapping a pointer. Additionally, handling of unknown keysyms, particularly when dealing with virtual keyboards, is improved, as keys generating valid Unicode values with no corresponding scancode will be dynamically added to the keymap with reserved scancodes, allowing for proper round-trip lookup behavior. --- src/events/SDL_keyboard.c | 11 +- src/events/SDL_keymap.c | 18 ++ src/events/SDL_keymap_c.h | 16 +- src/video/wayland/SDL_waylandevents.c | 289 ++++++++++++++---------- src/video/wayland/SDL_waylandevents_c.h | 3 +- src/video/wayland/SDL_waylandsym.h | 1 + 6 files changed, 201 insertions(+), 137 deletions(-) diff --git a/src/events/SDL_keyboard.c b/src/events/SDL_keyboard.c index e281f5daaa..6023c88608 100644 --- a/src/events/SDL_keyboard.c +++ b/src/events/SDL_keyboard.c @@ -61,7 +61,6 @@ typedef struct SDL_Keyboard Uint32 keycode_options; bool autorelease_pending; Uint64 hardware_timestamp; - int next_reserved_scancode; } SDL_Keyboard; static SDL_Keyboard SDL_keyboard; @@ -295,16 +294,12 @@ void SDL_SetKeymap(SDL_Keymap *keymap, bool send_event) static SDL_Scancode GetNextReservedScancode(void) { SDL_Keyboard *keyboard = &SDL_keyboard; - SDL_Scancode scancode; - if (keyboard->next_reserved_scancode && keyboard->next_reserved_scancode < SDL_SCANCODE_RESERVED + 100) { - scancode = (SDL_Scancode)keyboard->next_reserved_scancode; - } else { - scancode = SDL_SCANCODE_RESERVED; + if (!keyboard->keymap) { + keyboard->keymap = SDL_CreateKeymap(true); } - keyboard->next_reserved_scancode = (int)scancode + 1; - return scancode; + return SDL_GetKeymapNextReservedScancode(keyboard->keymap); } static void SetKeymapEntry(SDL_Scancode scancode, SDL_Keymod modstate, SDL_Keycode keycode) diff --git a/src/events/SDL_keymap.c b/src/events/SDL_keymap.c index 318fc6864b..21d287ccaa 100644 --- a/src/events/SDL_keymap.c +++ b/src/events/SDL_keymap.c @@ -183,6 +183,24 @@ SDL_Scancode SDL_GetKeymapScancode(SDL_Keymap *keymap, SDL_Keycode keycode, SDL_ return scancode; } +SDL_Scancode SDL_GetKeymapNextReservedScancode(SDL_Keymap *keymap) +{ + SDL_Scancode scancode; + + if (!keymap) { + return SDL_SCANCODE_UNKNOWN; + } + + if (keymap->next_reserved_scancode && keymap->next_reserved_scancode < SDL_SCANCODE_RESERVED + 100) { + scancode = keymap->next_reserved_scancode; + } else { + scancode = SDL_SCANCODE_RESERVED; + } + keymap->next_reserved_scancode = scancode + 1; + + return scancode; +} + void SDL_DestroyKeymap(SDL_Keymap *keymap) { if (!keymap) { diff --git a/src/events/SDL_keymap_c.h b/src/events/SDL_keymap_c.h index 311e397e67..8819924ab5 100644 --- a/src/events/SDL_keymap_c.h +++ b/src/events/SDL_keymap_c.h @@ -25,13 +25,14 @@ typedef struct SDL_Keymap { - SDL_HashTable *scancode_to_keycode; - SDL_HashTable *keycode_to_scancode; - bool auto_release; - bool layout_determined; - bool french_numbers; - bool latin_letters; - bool thai_keyboard; + SDL_HashTable *scancode_to_keycode; + SDL_HashTable *keycode_to_scancode; + SDL_Scancode next_reserved_scancode; + bool auto_release; + bool layout_determined; + bool french_numbers; + bool latin_letters; + bool thai_keyboard; } SDL_Keymap; /* This may return null even when a keymap is bound, depending on the current keyboard mapping options. @@ -42,6 +43,7 @@ SDL_Keymap *SDL_CreateKeymap(bool auto_release); void SDL_SetKeymapEntry(SDL_Keymap *keymap, SDL_Scancode scancode, SDL_Keymod modstate, SDL_Keycode keycode); SDL_Keycode SDL_GetKeymapKeycode(SDL_Keymap *keymap, SDL_Scancode scancode, SDL_Keymod modstate); SDL_Scancode SDL_GetKeymapScancode(SDL_Keymap *keymap, SDL_Keycode keycode, SDL_Keymod *modstate); +SDL_Scancode SDL_GetKeymapNextReservedScancode(SDL_Keymap *keymap); void SDL_DestroyKeymap(SDL_Keymap *keymap); #endif // SDL_keymap_c_h_ diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c index 08288718dd..78162a3eae 100644 --- a/src/video/wayland/SDL_waylandevents.c +++ b/src/video/wayland/SDL_waylandevents.c @@ -304,6 +304,16 @@ void Wayland_DisplayInitCursorShapeManager(SDL_VideoData *display) } } +static void Wayland_SeatSetKeymap(SDL_WaylandSeat *seat) +{ + if (seat->keyboard.sdl_keymap && + seat->keyboard.xkb.current_layout < seat->keyboard.xkb.num_layouts && + seat->keyboard.sdl_keymap[seat->keyboard.xkb.current_layout] != SDL_GetCurrentKeymap(true)) { + SDL_SetKeymap(seat->keyboard.sdl_keymap[seat->keyboard.xkb.current_layout], true); + SDL_SetModState(seat->keyboard.pressed_modifiers | seat->keyboard.locked_modifiers); + } +} + // Returns true if a key repeat event was due static bool keyboard_repeat_handle(SDL_WaylandKeyboardRepeat *repeat_info, Uint64 elapsed) { @@ -440,10 +450,7 @@ int Wayland_WaitEventTimeout(SDL_VideoDevice *_this, Sint64 timeoutNS) // If key repeat is active, we'll need to cap our maximum wait time to handle repeats wl_list_for_each (seat, &d->seat_list, link) { if (keyboard_repeat_is_set(&seat->keyboard.repeat)) { - if (seat->keyboard.sdl_keymap != SDL_GetCurrentKeymap(true)) { - SDL_SetKeymap(seat->keyboard.sdl_keymap, true); - SDL_SetModState(seat->keyboard.pressed_modifiers | seat->keyboard.locked_modifiers); - } + Wayland_SeatSetKeymap(seat); const Uint64 elapsed = SDL_GetTicksNS() - seat->keyboard.repeat.sdl_press_time_ns; if (keyboard_repeat_handle(&seat->keyboard.repeat, elapsed)) { @@ -479,14 +486,13 @@ int Wayland_WaitEventTimeout(SDL_VideoDevice *_this, Sint64 timeoutNS) // If key repeat is active, we might have woken up to generate a key event if (key_repeat_active) { wl_list_for_each (seat, &d->seat_list, link) { - if (seat->keyboard.sdl_keymap != SDL_GetCurrentKeymap(true)) { - SDL_SetKeymap(seat->keyboard.sdl_keymap, true); - SDL_SetModState(seat->keyboard.pressed_modifiers | seat->keyboard.locked_modifiers); - } + if (keyboard_repeat_is_set(&seat->keyboard.repeat)) { + Wayland_SeatSetKeymap(seat); - const Uint64 elapsed = SDL_GetTicksNS() - seat->keyboard.repeat.sdl_press_time_ns; - if (keyboard_repeat_handle(&seat->keyboard.repeat, elapsed)) { - ++ret; + const Uint64 elapsed = SDL_GetTicksNS() - seat->keyboard.repeat.sdl_press_time_ns; + if (keyboard_repeat_handle(&seat->keyboard.repeat, elapsed)) { + ++ret; + } } } } @@ -550,10 +556,7 @@ void Wayland_PumpEvents(SDL_VideoDevice *_this) wl_list_for_each (seat, &d->seat_list, link) { if (keyboard_repeat_is_set(&seat->keyboard.repeat)) { - if (seat->keyboard.sdl_keymap != SDL_GetCurrentKeymap(true)) { - SDL_SetKeymap(seat->keyboard.sdl_keymap, true); - SDL_SetModState(seat->keyboard.pressed_modifiers | seat->keyboard.locked_modifiers); - } + Wayland_SeatSetKeymap(seat); const Uint64 elapsed = SDL_GetTicksNS() - seat->keyboard.repeat.sdl_press_time_ns; keyboard_repeat_handle(&seat->keyboard.repeat, elapsed); @@ -1410,7 +1413,7 @@ static const struct wl_touch_listener touch_listener = { touch_handler_orientation // Version 6 }; -static void Wayland_keymap_iter(struct xkb_keymap *keymap, xkb_keycode_t key, void *data) +static void Wayland_KeymapIterator(struct xkb_keymap *keymap, xkb_keycode_t key, void *data) { SDL_WaylandSeat *seat = (SDL_WaylandSeat *)data; const xkb_keysym_t *syms; @@ -1420,76 +1423,75 @@ static void Wayland_keymap_iter(struct xkb_keymap *keymap, xkb_keycode_t key, vo seat->keyboard.xkb.level3_mask | seat->keyboard.xkb.level5_mask | seat->keyboard.xkb.caps_mask; - const SDL_Scancode scancode = SDL_GetScancodeFromTable(SDL_SCANCODE_TABLE_XFREE86_2, (key - 8)); - if (scancode == SDL_SCANCODE_UNKNOWN) { - return; + SDL_Scancode scancode = SDL_SCANCODE_UNKNOWN; + + // Look up the scancode for hardware keyboards. Virtual keyboards get the scancode from the keysym. + if (!seat->keyboard.is_virtual) { + scancode = SDL_GetScancodeFromTable(SDL_SCANCODE_TABLE_XFREE86_2, (key - 8)); + if (scancode == SDL_SCANCODE_UNKNOWN) { + return; + } } - const xkb_level_index_t num_levels = WAYLAND_xkb_keymap_num_levels_for_key(seat->keyboard.xkb.keymap, key, seat->keyboard.xkb.current_layout); - for (xkb_level_index_t level = 0; level < num_levels; ++level) { - if (WAYLAND_xkb_keymap_key_get_syms_by_level(seat->keyboard.xkb.keymap, key, seat->keyboard.xkb.current_layout, level, &syms) > 0) { - xkb_mod_mask_t xkb_mod_masks[16]; - const size_t num_masks = WAYLAND_xkb_keymap_key_get_mods_for_level(seat->keyboard.xkb.keymap, key, seat->keyboard.xkb.current_layout, level, xkb_mod_masks, SDL_arraysize(xkb_mod_masks)); - for (size_t mask = 0; mask < num_masks; ++mask) { - // Ignore this modifier set if it uses unsupported modifier types. - if ((xkb_mod_masks[mask] | xkb_valid_mod_mask) != xkb_valid_mod_mask) { - continue; - } - - const SDL_Keymod sdl_mod = (xkb_mod_masks[mask] & seat->keyboard.xkb.shift_mask ? SDL_KMOD_SHIFT : 0) | - (xkb_mod_masks[mask] & seat->keyboard.xkb.alt_mask ? SDL_KMOD_ALT : 0) | - (xkb_mod_masks[mask] & seat->keyboard.xkb.gui_mask ? SDL_KMOD_GUI : 0) | - (xkb_mod_masks[mask] & seat->keyboard.xkb.level3_mask ? SDL_KMOD_MODE : 0) | - (xkb_mod_masks[mask] & seat->keyboard.xkb.level5_mask ? SDL_KMOD_LEVEL5 : 0) | - (xkb_mod_masks[mask] & seat->keyboard.xkb.caps_mask ? SDL_KMOD_CAPS : 0); - - SDL_Keycode keycode = SDL_GetKeyCodeFromKeySym(syms[0], key, sdl_mod); - - if (!keycode) { - switch (scancode) { - case SDL_SCANCODE_RETURN: - keycode = SDLK_RETURN; - break; - case SDL_SCANCODE_ESCAPE: - keycode = SDLK_ESCAPE; - break; - case SDL_SCANCODE_BACKSPACE: - keycode = SDLK_BACKSPACE; - break; - case SDL_SCANCODE_DELETE: - keycode = SDLK_DELETE; - break; - default: - keycode = SDL_SCANCODE_TO_KEYCODE(scancode); - break; + for (xkb_layout_index_t layout = 0; layout < seat->keyboard.xkb.num_layouts; ++layout) { + const xkb_level_index_t num_levels = WAYLAND_xkb_keymap_num_levels_for_key(seat->keyboard.xkb.keymap, key, seat->keyboard.xkb.current_layout); + for (xkb_level_index_t level = 0; level < num_levels; ++level) { + if (WAYLAND_xkb_keymap_key_get_syms_by_level(seat->keyboard.xkb.keymap, key, layout, level, &syms) > 0) { + /* If the keyboard is virtual or the key didn't have a corresponding hardware scancode, try to + * look it up from the keysym. If there is still no corresponding scancode, skip this mapping + * for now, as it will be dynamically added with a reserved scancode on first use. + */ + if (scancode == SDL_SCANCODE_UNKNOWN) { + scancode = SDL_GetScancodeFromKeySym(syms[0], key); + if (scancode == SDL_SCANCODE_UNKNOWN) { + continue; } } - SDL_SetKeymapEntry(seat->keyboard.sdl_keymap, scancode, sdl_mod, keycode); + xkb_mod_mask_t xkb_mod_masks[16]; + const size_t num_masks = WAYLAND_xkb_keymap_key_get_mods_for_level(seat->keyboard.xkb.keymap, key, layout, level, xkb_mod_masks, SDL_arraysize(xkb_mod_masks)); + for (size_t mask = 0; mask < num_masks; ++mask) { + // Ignore this modifier set if it uses unsupported modifier types. + if ((xkb_mod_masks[mask] | xkb_valid_mod_mask) != xkb_valid_mod_mask) { + continue; + } + + const SDL_Keymod sdl_mod = (xkb_mod_masks[mask] & seat->keyboard.xkb.shift_mask ? SDL_KMOD_SHIFT : 0) | + (xkb_mod_masks[mask] & seat->keyboard.xkb.alt_mask ? SDL_KMOD_ALT : 0) | + (xkb_mod_masks[mask] & seat->keyboard.xkb.gui_mask ? SDL_KMOD_GUI : 0) | + (xkb_mod_masks[mask] & seat->keyboard.xkb.level3_mask ? SDL_KMOD_MODE : 0) | + (xkb_mod_masks[mask] & seat->keyboard.xkb.level5_mask ? SDL_KMOD_LEVEL5 : 0) | + (xkb_mod_masks[mask] & seat->keyboard.xkb.caps_mask ? SDL_KMOD_CAPS : 0); + + SDL_Keycode keycode = SDL_GetKeyCodeFromKeySym(syms[0], key, sdl_mod); + + if (!keycode) { + switch (scancode) { + case SDL_SCANCODE_RETURN: + keycode = SDLK_RETURN; + break; + case SDL_SCANCODE_ESCAPE: + keycode = SDLK_ESCAPE; + break; + case SDL_SCANCODE_BACKSPACE: + keycode = SDLK_BACKSPACE; + break; + case SDL_SCANCODE_DELETE: + keycode = SDLK_DELETE; + break; + default: + keycode = SDL_SCANCODE_TO_KEYCODE(scancode); + break; + } + } + + SDL_SetKeymapEntry(seat->keyboard.sdl_keymap[layout], scancode, sdl_mod, keycode); + } } } } } -static void Wayland_UpdateKeymap(SDL_WaylandSeat *seat) -{ - if (!seat->keyboard.is_virtual) { - SDL_DestroyKeymap(seat->keyboard.sdl_keymap); - seat->keyboard.sdl_keymap = SDL_CreateKeymap(false); - if (!seat->keyboard.sdl_keymap) { - return; - } - - WAYLAND_xkb_keymap_key_for_each(seat->keyboard.xkb.keymap, Wayland_keymap_iter, seat); - SDL_SetKeymap(seat->keyboard.sdl_keymap, true); - } else { - // Virtual keyboards use the default keymap. - SDL_SetKeymap(NULL, true); - SDL_DestroyKeymap(seat->keyboard.sdl_keymap); - seat->keyboard.sdl_keymap = NULL; - } -} - static void keyboard_handle_keymap(void *data, struct wl_keyboard *keyboard, uint32_t format, int fd, uint32_t size) { @@ -1520,9 +1522,9 @@ static void keyboard_handle_keymap(void *data, struct wl_keyboard *keyboard, seat->keyboard.xkb.keymap = NULL; } seat->keyboard.xkb.keymap = WAYLAND_xkb_keymap_new_from_string(seat->display->xkb_context, - map_str, - XKB_KEYMAP_FORMAT_TEXT_V1, - 0); + map_str, + XKB_KEYMAP_FORMAT_TEXT_V1, + 0); munmap(map_str, size); close(fd); @@ -1531,6 +1533,14 @@ static void keyboard_handle_keymap(void *data, struct wl_keyboard *keyboard, return; } + // Clear the old layouts. + for (xkb_layout_index_t i = 0; i < seat->keyboard.xkb.num_layouts; ++i) { + SDL_DestroyKeymap(seat->keyboard.sdl_keymap[i]); + } + SDL_free(seat->keyboard.sdl_keymap); + seat->keyboard.sdl_keymap = NULL; + seat->keyboard.xkb.num_layouts = 0; + #if SDL_XKBCOMMON_CHECK_VERSION(1, 10, 0) seat->keyboard.xkb.shift_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_MOD_NAME_SHIFT); seat->keyboard.xkb.ctrl_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_MOD_NAME_CTRL); @@ -1576,9 +1586,19 @@ static void keyboard_handle_keymap(void *data, struct wl_keyboard *keyboard, */ seat->keyboard.is_virtual = WAYLAND_xkb_keymap_layout_get_name(seat->keyboard.xkb.keymap, 0) == NULL; - // Update the keymap if changed. - if (seat->keyboard.xkb.current_layout != XKB_LAYOUT_INVALID) { - Wayland_UpdateKeymap(seat); + // Allocate and populate the new layout maps. + seat->keyboard.xkb.num_layouts = WAYLAND_xkb_keymap_num_layouts(seat->keyboard.xkb.keymap); + if (seat->keyboard.xkb.num_layouts) { + seat->keyboard.sdl_keymap = SDL_calloc(seat->keyboard.xkb.num_layouts, sizeof(SDL_Keymap *)); + for (xkb_layout_index_t i = 0; i < seat->keyboard.xkb.num_layouts; ++i) { + seat->keyboard.sdl_keymap[i] = SDL_CreateKeymap(false); + if (!seat->keyboard.sdl_keymap) { + return; + } + } + + WAYLAND_xkb_keymap_key_for_each(seat->keyboard.xkb.keymap, Wayland_KeymapIterator, seat); + Wayland_SeatSetKeymap(seat); } /* @@ -1637,16 +1657,20 @@ static void keyboard_handle_keymap(void *data, struct wl_keyboard *keyboard, * Virtual keyboards can have arbitrary layouts, arbitrary scancodes/keycodes, etc... * Key presses from these devices must be looked up by their keysym value. */ -static SDL_Scancode Wayland_GetScancodeForKey(SDL_WaylandSeat *seat, uint32_t key) +static SDL_Scancode Wayland_GetScancodeForKey(SDL_WaylandSeat *seat, uint32_t key, const xkb_keysym_t **syms) { SDL_Scancode scancode = SDL_SCANCODE_UNKNOWN; if (!seat->keyboard.is_virtual) { scancode = SDL_GetScancodeFromTable(SDL_SCANCODE_TABLE_XFREE86_2, key); - } else { - const xkb_keysym_t *syms; - if (WAYLAND_xkb_keymap_key_get_syms_by_level(seat->keyboard.xkb.keymap, key + 8, seat->keyboard.xkb.current_layout, 0, &syms) > 0) { - scancode = SDL_GetScancodeFromKeySym(syms[0], key); + } + if (scancode == SDL_SCANCODE_UNKNOWN) { + const xkb_keysym_t *keysym; + if (WAYLAND_xkb_state_key_get_syms(seat->keyboard.xkb.state, key + 8, &keysym) > 0) { + scancode = SDL_GetScancodeFromKeySym(keysym[0], key + 8); + if (syms) { + *syms = keysym; + } } } @@ -1884,30 +1908,30 @@ static void keyboard_handle_enter(void *data, struct wl_keyboard *keyboard, Uint64 timestamp = SDL_GetTicksNS(); window->last_focus_event_time_ns = timestamp; - if (SDL_GetCurrentKeymap(true) != seat->keyboard.sdl_keymap) { - SDL_SetKeymap(seat->keyboard.sdl_keymap, true); - } + Wayland_SeatSetKeymap(seat); wl_array_for_each (key, keys) { - const SDL_Scancode scancode = Wayland_GetScancodeForKey(seat, *key); - const SDL_Keycode keycode = SDL_GetKeyFromScancode(scancode, SDL_KMOD_NONE, false); + const SDL_Scancode scancode = Wayland_GetScancodeForKey(seat, *key, NULL); + if (scancode != SDL_SCANCODE_UNKNOWN) { + const SDL_Keycode keycode = SDL_GetKeyFromScancode(scancode, SDL_KMOD_NONE, false); - switch (keycode) { - case SDLK_LSHIFT: - case SDLK_RSHIFT: - case SDLK_LCTRL: - case SDLK_RCTRL: - case SDLK_LALT: - case SDLK_RALT: - case SDLK_LGUI: - case SDLK_RGUI: - case SDLK_MODE: - case SDLK_LEVEL5_SHIFT: - Wayland_HandleModifierKeys(seat, scancode, true); - SDL_SendKeyboardKeyIgnoreModifiers(timestamp, seat->keyboard.sdl_id, *key, scancode, true); - break; - default: - break; + switch (keycode) { + case SDLK_LSHIFT: + case SDLK_RSHIFT: + case SDLK_LCTRL: + case SDLK_RCTRL: + case SDLK_LALT: + case SDLK_RALT: + case SDLK_LGUI: + case SDLK_RGUI: + case SDLK_MODE: + case SDLK_LEVEL5_SHIFT: + Wayland_HandleModifierKeys(seat, scancode, true); + SDL_SendKeyboardKeyIgnoreModifiers(timestamp, seat->keyboard.sdl_id, *key, scancode, true); + break; + default: + break; + } } } } @@ -2044,10 +2068,7 @@ static void keyboard_handle_key(void *data, struct wl_keyboard *keyboard, state = WL_KEYBOARD_KEY_STATE_PRESSED; } - if (seat->keyboard.sdl_keymap != SDL_GetCurrentKeymap(true)) { - SDL_SetKeymap(seat->keyboard.sdl_keymap, true); - SDL_SetModState(seat->keyboard.pressed_modifiers | seat->keyboard.locked_modifiers); - } + Wayland_SeatSetKeymap(seat); if (state == WL_KEYBOARD_KEY_STATE_PRESSED) { SDL_Window *keyboard_focus = SDL_GetKeyboardFocus(); @@ -2068,9 +2089,27 @@ static void keyboard_handle_key(void *data, struct wl_keyboard *keyboard, keyboard_input_get_text(text, seat, key, false, &handled_by_ime); } - const SDL_Scancode scancode = Wayland_GetScancodeForKey(seat, key); + const xkb_keysym_t *syms = NULL; + SDL_Scancode scancode = Wayland_GetScancodeForKey(seat, key, &syms); Wayland_HandleModifierKeys(seat, scancode, state == WL_KEYBOARD_KEY_STATE_PRESSED); + // If we have a key with unknown scancode, check if the keysym corresponds to a valid Unicode value, and assign it a reserved scancode. + if (scancode == SDL_SCANCODE_UNKNOWN && syms) { + const SDL_Keycode keycode = (SDL_Keycode)SDL_KeySymToUcs4(syms[0]); + if (keycode != SDLK_UNKNOWN) { + SDL_Keymod modstate = SDL_KMOD_NONE; + + // Check if this keycode already exists in the keymap. + scancode = SDL_GetKeymapScancode(seat->keyboard.sdl_keymap[seat->keyboard.xkb.current_layout], keycode, &modstate); + + // Make sure we have this keycode in our keymap + if (scancode == SDL_SCANCODE_UNKNOWN && keycode < SDLK_SCANCODE_MASK) { + scancode = SDL_GetKeymapNextReservedScancode(seat->keyboard.sdl_keymap[seat->keyboard.xkb.current_layout]); + SDL_SetKeymapEntry(seat->keyboard.sdl_keymap[seat->keyboard.xkb.current_layout], scancode, modstate, keycode); + } + } + } + SDL_SendKeyboardKeyIgnoreModifiers(timestamp_ns, seat->keyboard.sdl_id, key, scancode, state == WL_KEYBOARD_KEY_STATE_PRESSED); if (state == WL_KEYBOARD_KEY_STATE_PRESSED) { @@ -2116,13 +2155,15 @@ static void keyboard_handle_modifiers(void *data, struct wl_keyboard *keyboard, } } - if (group == seat->keyboard.xkb.current_layout) { - return; - } + if (group != seat->keyboard.xkb.current_layout) { + seat->keyboard.xkb.current_layout = group; + Wayland_SeatSetKeymap(seat); - // The layout changed, remap and fire an event. Virtual keyboards use the default keymap. - seat->keyboard.xkb.current_layout = group; - Wayland_UpdateKeymap(seat); + if (seat->keyboard.xkb.compose_state) { + // Reset the compose state so composite and dead keys don't carry over. + WAYLAND_xkb_compose_state_reset(seat->keyboard.xkb.compose_state); + } + } } static void keyboard_handle_repeat_info(void *data, struct wl_keyboard *wl_keyboard, @@ -2205,10 +2246,16 @@ static void Wayland_SeatDestroyKeyboard(SDL_WaylandSeat *seat, bool send_event) SDL_RemoveKeyboard(seat->keyboard.sdl_id, send_event); if (seat->keyboard.sdl_keymap) { - if (seat->keyboard.sdl_keymap == SDL_GetCurrentKeymap(true)) { + if (seat->keyboard.xkb.current_layout < seat->keyboard.xkb.num_layouts && + seat->keyboard.sdl_keymap[seat->keyboard.xkb.current_layout] == SDL_GetCurrentKeymap(true)) { SDL_SetKeymap(NULL, false); + SDL_SetModState(SDL_KMOD_NONE); } - SDL_DestroyKeymap(seat->keyboard.sdl_keymap); + for (xkb_layout_index_t i = 0; i < seat->keyboard.xkb.num_layouts; ++i) { + SDL_DestroyKeymap(seat->keyboard.sdl_keymap[i]); + } + SDL_free(seat->keyboard.sdl_keymap); + seat->keyboard.sdl_keymap = NULL; } if (seat->keyboard.key_inhibitor) { diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h index ffa779e250..c43958b9a6 100644 --- a/src/video/wayland/SDL_waylandevents_c.h +++ b/src/video/wayland/SDL_waylandevents_c.h @@ -75,7 +75,7 @@ typedef struct SDL_WaylandSeat struct zwp_input_timestamps_v1 *timestamps; struct zwp_keyboard_shortcuts_inhibitor_v1 *key_inhibitor; SDL_WindowData *focus; - SDL_Keymap *sdl_keymap; + SDL_Keymap **sdl_keymap; char *current_locale; SDL_WaylandKeyboardRepeat repeat; @@ -96,6 +96,7 @@ typedef struct SDL_WaylandSeat struct xkb_compose_state *compose_state; // Current keyboard layout (aka 'group') + xkb_layout_index_t num_layouts; xkb_layout_index_t current_layout; // Modifier bitshift values diff --git a/src/video/wayland/SDL_waylandsym.h b/src/video/wayland/SDL_waylandsym.h index 5b0a0939c5..31f77fbf9e 100644 --- a/src/video/wayland/SDL_waylandsym.h +++ b/src/video/wayland/SDL_waylandsym.h @@ -150,6 +150,7 @@ SDL_WAYLAND_SYM(enum xkb_compose_feed_result, xkb_compose_state_feed, (struct xk SDL_WAYLAND_SYM(enum xkb_compose_status, xkb_compose_state_get_status, (struct xkb_compose_state *) ) SDL_WAYLAND_SYM(xkb_keysym_t, xkb_compose_state_get_one_sym, (struct xkb_compose_state *) ) SDL_WAYLAND_SYM(void, xkb_keymap_key_for_each, (struct xkb_keymap *, xkb_keymap_key_iter_t, void *) ) +SDL_WAYLAND_SYM(xkb_layout_index_t, xkb_keymap_num_layouts, (struct xkb_keymap *) ) SDL_WAYLAND_SYM(int, xkb_keymap_key_get_syms_by_level, (struct xkb_keymap *, xkb_keycode_t, xkb_layout_index_t, From 8bd29f7ca3612af10fc2ca54bce6b5ca01091e97 Mon Sep 17 00:00:00 2001 From: Acclution <42910256+Acclution@users.noreply.github.com> Date: Fri, 18 Jul 2025 22:27:31 +0000 Subject: [PATCH 066/103] GPU: Fix Vulkan compute uniform descriptor not being marked as set (#13389) --- src/gpu/vulkan/SDL_gpu_vulkan.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gpu/vulkan/SDL_gpu_vulkan.c b/src/gpu/vulkan/SDL_gpu_vulkan.c index f94dc25638..759cf50023 100644 --- a/src/gpu/vulkan/SDL_gpu_vulkan.c +++ b/src/gpu/vulkan/SDL_gpu_vulkan.c @@ -8594,7 +8594,7 @@ static void VULKAN_INTERNAL_BindComputeDescriptorSets( dynamicOffsetCount, dynamicOffsets); - commandBuffer->needNewVertexUniformOffsets = false; + commandBuffer->needNewComputeUniformOffsets = false; } static void VULKAN_DispatchCompute( From 735f0cc300ab1fd5fb150f1d16a0928adc65bb2f Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Sat, 19 Jul 2025 10:08:56 -0400 Subject: [PATCH 067/103] wayland: Handle text input per-seat When changing the text input mode on a window, only update the seats that currently hold keyboard focus on that window, otherwise, text input might be inadvertently enabled or disabled on a seat focused on another window. --- src/video/wayland/SDL_waylandevents.c | 5 +- src/video/wayland/SDL_waylandevents_c.h | 1 - src/video/wayland/SDL_waylandkeyboard.c | 123 +++++++++++++----------- src/video/wayland/SDL_waylandkeyboard.h | 2 +- 4 files changed, 69 insertions(+), 62 deletions(-) diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c index 78162a3eae..bb92b1ca47 100644 --- a/src/video/wayland/SDL_waylandevents.c +++ b/src/video/wayland/SDL_waylandevents.c @@ -35,6 +35,7 @@ #include "SDL_waylandwindow.h" #include "SDL_waylandmouse.h" #include "SDL_waylandclipboard.h" +#include "SDL_waylandkeyboard.h" #include "pointer-constraints-unstable-v1-client-protocol.h" #include "relative-pointer-unstable-v1-client-protocol.h" @@ -1897,7 +1898,7 @@ static void keyboard_handle_enter(void *data, struct wl_keyboard *keyboard, Wayland_DisplayUpdatePointerGrabs(seat->display, window); // Update text input and IME focus. - Wayland_UpdateTextInput(seat->display); + Wayland_SeatUpdateTextInput(seat); #ifdef SDL_USE_IME if (!seat->text_input.zwp_text_input) { @@ -1979,7 +1980,7 @@ static void keyboard_handle_leave(void *data, struct wl_keyboard *keyboard, seat->keyboard.pressed_modifiers = SDL_KMOD_NONE; // Update text input and IME focus. - Wayland_UpdateTextInput(seat->display); + Wayland_SeatUpdateTextInput(seat); #ifdef SDL_USE_IME if (!seat->text_input.zwp_text_input && !window->keyboard_focus_count) { diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h index c43958b9a6..ef560b4e15 100644 --- a/src/video/wayland/SDL_waylandevents_c.h +++ b/src/video/wayland/SDL_waylandevents_c.h @@ -31,7 +31,6 @@ #include "SDL_waylandvideo.h" #include "SDL_waylandwindow.h" #include "SDL_waylanddatamanager.h" -#include "SDL_waylandkeyboard.h" enum SDL_WaylandAxisEvent { diff --git a/src/video/wayland/SDL_waylandkeyboard.c b/src/video/wayland/SDL_waylandkeyboard.c index ae5bb13b37..d4aab41cad 100644 --- a/src/video/wayland/SDL_waylandkeyboard.c +++ b/src/video/wayland/SDL_waylandkeyboard.c @@ -51,65 +51,59 @@ void Wayland_QuitKeyboard(SDL_VideoDevice *_this) #endif } -void Wayland_UpdateTextInput(SDL_VideoData *display) +void Wayland_SeatUpdateTextInput(SDL_WaylandSeat *seat) { - SDL_WaylandSeat *seat = NULL; + if (seat->text_input.zwp_text_input) { + SDL_WindowData *focus = seat->keyboard.focus; - if (display->text_input_manager) { - wl_list_for_each(seat, &display->seat_list, link) { - SDL_WindowData *focus = seat->keyboard.focus; + if (focus && focus->text_input_props.active) { + SDL_Window *window = focus->sdlwindow; - if (seat->text_input.zwp_text_input) { - if (focus && focus->text_input_props.active) { - SDL_Window *window = focus->sdlwindow; + // Enabling will reset all state, so don't do it redundantly. + if (!seat->text_input.enabled) { + seat->text_input.enabled = true; + zwp_text_input_v3_enable(seat->text_input.zwp_text_input); - // Enabling will reset all state, so don't do it redundantly. - if (!seat->text_input.enabled) { - seat->text_input.enabled = true; - zwp_text_input_v3_enable(seat->text_input.zwp_text_input); + // Now that it's enabled, set the input properties + zwp_text_input_v3_set_content_type(seat->text_input.zwp_text_input, focus->text_input_props.hint, focus->text_input_props.purpose); + if (!SDL_RectEmpty(&window->text_input_rect)) { + const SDL_Rect scaled_rect = { + (int)SDL_floor(window->text_input_rect.x / focus->pointer_scale.x), + (int)SDL_floor(window->text_input_rect.y / focus->pointer_scale.y), + (int)SDL_ceil(window->text_input_rect.w / focus->pointer_scale.x), + (int)SDL_ceil(window->text_input_rect.h / focus->pointer_scale.y) + }; + const int scaled_cursor = (int)SDL_floor(window->text_input_cursor / focus->pointer_scale.x); - // Now that it's enabled, set the input properties - zwp_text_input_v3_set_content_type(seat->text_input.zwp_text_input, focus->text_input_props.hint, focus->text_input_props.purpose); - if (!SDL_RectEmpty(&window->text_input_rect)) { - const SDL_Rect scaled_rect = { - (int)SDL_floor(window->text_input_rect.x / focus->pointer_scale.x), - (int)SDL_floor(window->text_input_rect.y / focus->pointer_scale.y), - (int)SDL_ceil(window->text_input_rect.w / focus->pointer_scale.x), - (int)SDL_ceil(window->text_input_rect.h / focus->pointer_scale.y) - }; - const int scaled_cursor = (int)SDL_floor(window->text_input_cursor / focus->pointer_scale.x); + SDL_copyp(&seat->text_input.text_input_rect, &scaled_rect); + seat->text_input.text_input_cursor = scaled_cursor; - SDL_copyp(&seat->text_input.text_input_rect, &scaled_rect); - seat->text_input.text_input_cursor = scaled_cursor; - - // Clamp the x value so it doesn't run too far past the end of the text input area. - zwp_text_input_v3_set_cursor_rectangle(seat->text_input.zwp_text_input, - SDL_min(scaled_rect.x + scaled_cursor, scaled_rect.x + scaled_rect.w), - scaled_rect.y, - 1, - scaled_rect.h); - } - zwp_text_input_v3_commit(seat->text_input.zwp_text_input); - - if (seat->keyboard.xkb.compose_state) { - // Reset compose state so composite and dead keys don't carry over - WAYLAND_xkb_compose_state_reset(seat->keyboard.xkb.compose_state); - } - } - } else { - if (seat->text_input.enabled) { - seat->text_input.enabled = false; - SDL_zero(seat->text_input.text_input_rect); - seat->text_input.text_input_cursor = 0; - zwp_text_input_v3_disable(seat->text_input.zwp_text_input); - zwp_text_input_v3_commit(seat->text_input.zwp_text_input); - } - - if (seat->keyboard.xkb.compose_state) { - // Reset compose state so composite and dead keys don't carry over - WAYLAND_xkb_compose_state_reset(seat->keyboard.xkb.compose_state); - } + // Clamp the x value so it doesn't run too far past the end of the text input area. + zwp_text_input_v3_set_cursor_rectangle(seat->text_input.zwp_text_input, + SDL_min(scaled_rect.x + scaled_cursor, scaled_rect.x + scaled_rect.w), + scaled_rect.y, + 1, + scaled_rect.h); } + zwp_text_input_v3_commit(seat->text_input.zwp_text_input); + + if (seat->keyboard.xkb.compose_state) { + // Reset compose state so composite and dead keys don't carry over + WAYLAND_xkb_compose_state_reset(seat->keyboard.xkb.compose_state); + } + } + } else { + if (seat->text_input.enabled) { + seat->text_input.enabled = false; + SDL_zero(seat->text_input.text_input_rect); + seat->text_input.text_input_cursor = 0; + zwp_text_input_v3_disable(seat->text_input.zwp_text_input); + zwp_text_input_v3_commit(seat->text_input.zwp_text_input); + } + + if (seat->keyboard.xkb.compose_state) { + // Reset compose state so composite and dead keys don't carry over + WAYLAND_xkb_compose_state_reset(seat->keyboard.xkb.compose_state); } } } @@ -182,12 +176,18 @@ bool Wayland_StartTextInput(SDL_VideoDevice *_this, SDL_Window *window, SDL_Prop } wind->text_input_props.active = true; - Wayland_UpdateTextInput(display); + + SDL_WaylandSeat *seat; + wl_list_for_each (seat, &display->seat_list, link) { + if (seat->keyboard.focus == wind) { + Wayland_SeatUpdateTextInput(seat); + } + } return true; } - return false; + return SDL_SetError("wayland: cannot enable text input; compositor lacks support for the required zwp_text_input_v3 protocol"); } bool Wayland_StopTextInput(SDL_VideoDevice *_this, SDL_Window *window) @@ -195,8 +195,15 @@ bool Wayland_StopTextInput(SDL_VideoDevice *_this, SDL_Window *window) SDL_VideoData *display = _this->internal; if (display->text_input_manager) { - window->internal->text_input_props.active = false; - Wayland_UpdateTextInput(display); + SDL_WaylandSeat *seat; + SDL_WindowData *wind = window->internal; + wind->text_input_props.active = false; + + wl_list_for_each (seat, &display->seat_list, link) { + if (seat->keyboard.focus == wind) { + Wayland_SeatUpdateTextInput(seat); + } + } } #ifdef SDL_USE_IME else { @@ -212,10 +219,10 @@ bool Wayland_UpdateTextInputArea(SDL_VideoDevice *_this, SDL_Window *window) SDL_VideoData *internal = _this->internal; if (internal->text_input_manager) { SDL_WaylandSeat *seat; + SDL_WindowData *wind = window->internal; wl_list_for_each (seat, &internal->seat_list, link) { - if (seat->text_input.zwp_text_input && seat->keyboard.focus == window->internal) { - SDL_WindowData *wind = window->internal; + if (seat->text_input.zwp_text_input && seat->keyboard.focus == wind) { const SDL_Rect scaled_rect = { (int)SDL_floor(window->text_input_rect.x / wind->pointer_scale.x), (int)SDL_floor(window->text_input_rect.y / wind->pointer_scale.y), diff --git a/src/video/wayland/SDL_waylandkeyboard.h b/src/video/wayland/SDL_waylandkeyboard.h index b1897b8ba6..c560dffb8f 100644 --- a/src/video/wayland/SDL_waylandkeyboard.h +++ b/src/video/wayland/SDL_waylandkeyboard.h @@ -28,7 +28,7 @@ extern void Wayland_QuitKeyboard(SDL_VideoDevice *_this); extern bool Wayland_StartTextInput(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props); extern bool Wayland_StopTextInput(SDL_VideoDevice *_this, SDL_Window *window); extern bool Wayland_UpdateTextInputArea(SDL_VideoDevice *_this, SDL_Window *window); -extern void Wayland_UpdateTextInput(SDL_VideoData *display); +extern void Wayland_SeatUpdateTextInput(SDL_WaylandSeat *seat); extern bool Wayland_HasScreenKeyboardSupport(SDL_VideoDevice *_this); #endif // SDL_waylandkeyboard_h_ From 47d8bdd1c3e9c799ecaa7cb10d84ae8d88165b5c Mon Sep 17 00:00:00 2001 From: Semphris Date: Sun, 25 May 2025 17:49:53 -0400 Subject: [PATCH 068/103] Add SDL_IsTraySupported --- include/SDL3/SDL_tray.h | 19 +++++++++++++++++++ src/dynapi/SDL_dynapi.sym | 1 + src/dynapi/SDL_dynapi_overrides.h | 1 + src/dynapi/SDL_dynapi_procs.h | 1 + src/tray/cocoa/SDL_tray.m | 10 ++++++++++ src/tray/dummy/SDL_tray.c | 5 +++++ src/tray/unix/SDL_tray.c | 18 ++++++++++++++++++ src/tray/windows/SDL_tray.c | 10 ++++++++++ 8 files changed, 65 insertions(+) diff --git a/include/SDL3/SDL_tray.h b/include/SDL3/SDL_tray.h index 1780b0ba52..8d2c7b1bb1 100644 --- a/include/SDL3/SDL_tray.h +++ b/include/SDL3/SDL_tray.h @@ -96,6 +96,25 @@ typedef Uint32 SDL_TrayEntryFlags; */ typedef void (SDLCALL *SDL_TrayCallback)(void *userdata, SDL_TrayEntry *entry); +/** + * Check whether or not tray icons can be created. + * + * Note that this function does not guarantee that SDL_CreateTray() will or will + * not work; you should still check SDL_CreateTray() for errors. + * + * Using tray icons require the video subsystem. + * + * \returns true if trays are available, false otherwise. + * + * \threadsafety This function should only be called on the main thread. It will + * return false if not called on the main thread. + * + * \since This function is available since SDL 3.4.0. + * + * \sa SDL_CreateTray + */ +extern SDL_DECLSPEC bool SDLCALL SDL_IsTraySupported(void); + /** * Create an icon to be placed in the operating system's tray, or equivalent. * diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index 91bd4300c7..86abbbd12f 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -1254,6 +1254,7 @@ SDL3_0.0.0 { SDL_SetAudioIterationCallbacks; SDL_GetEventDescription; SDL_PutAudioStreamDataNoCopy; + SDL_IsTraySupported; # extra symbols go here (don't modify this line) local: *; }; diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h index fcc0bad7b2..a2f02b2708 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -1279,3 +1279,4 @@ #define SDL_SetAudioIterationCallbacks SDL_SetAudioIterationCallbacks_REAL #define SDL_GetEventDescription SDL_GetEventDescription_REAL #define SDL_PutAudioStreamDataNoCopy SDL_PutAudioStreamDataNoCopy_REAL +#define SDL_IsTraySupported SDL_IsTraySupported_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index 60e0dfea57..08ba54c2cb 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -1287,3 +1287,4 @@ SDL_DYNAPI_PROC(bool,SDL_PutAudioStreamPlanarData,(SDL_AudioStream *a,const void SDL_DYNAPI_PROC(bool,SDL_SetAudioIterationCallbacks,(SDL_AudioDeviceID a,SDL_AudioIterationCallback b,SDL_AudioIterationCallback c,void *d),(a,b,c,d),return) SDL_DYNAPI_PROC(int,SDL_GetEventDescription,(const SDL_Event *a,char *b,int c),(a,b,c),return) SDL_DYNAPI_PROC(bool,SDL_PutAudioStreamDataNoCopy,(SDL_AudioStream *a,const void *b,int c,SDL_AudioStreamDataCompleteCallback d,void *e),(a,b,c,d,e),return) +SDL_DYNAPI_PROC(bool,SDL_IsTraySupported,(void),(),return) diff --git a/src/tray/cocoa/SDL_tray.m b/src/tray/cocoa/SDL_tray.m index fd7f95517c..d093972a2d 100644 --- a/src/tray/cocoa/SDL_tray.m +++ b/src/tray/cocoa/SDL_tray.m @@ -82,6 +82,16 @@ void SDL_UpdateTrays(void) { } +bool SDL_IsTraySupported(void) +{ + if (!SDL_IsMainThread()) { + SDL_SetError("This function should be called on the main thread"); + return false; + } + + return true; +} + SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) { if (!SDL_IsMainThread()) { diff --git a/src/tray/dummy/SDL_tray.c b/src/tray/dummy/SDL_tray.c index 766fb92584..3a95c6575b 100644 --- a/src/tray/dummy/SDL_tray.c +++ b/src/tray/dummy/SDL_tray.c @@ -29,6 +29,11 @@ void SDL_UpdateTrays(void) { } +bool SDL_IsTraySupported(void) +{ + return false; +} + SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) { SDL_Unsupported(); diff --git a/src/tray/unix/SDL_tray.c b/src/tray/unix/SDL_tray.c index 4e010ec89e..0cc4390b72 100644 --- a/src/tray/unix/SDL_tray.c +++ b/src/tray/unix/SDL_tray.c @@ -413,6 +413,24 @@ void SDL_UpdateTrays(void) } } +bool SDL_IsTraySupported(void) +{ + if (!SDL_IsMainThread()) { + SDL_SetError("This function should be called on the main thread"); + return false; + } + + static bool has_trays = false; + static bool has_been_detected_once = false; + + if (!has_been_detected_once) { + has_trays = init_gtk(); + has_been_detected_once = true; + } + + return has_trays; +} + SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) { if (!SDL_IsMainThread()) { diff --git a/src/tray/windows/SDL_tray.c b/src/tray/windows/SDL_tray.c index 15021ac798..a3bd81ff10 100644 --- a/src/tray/windows/SDL_tray.c +++ b/src/tray/windows/SDL_tray.c @@ -216,6 +216,16 @@ void SDL_UpdateTrays(void) { } +bool SDL_IsTraySupported(void) +{ + if (!SDL_IsMainThread()) { + SDL_SetError("This function should be called on the main thread"); + return false; + } + + return true; +} + SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) { if (!SDL_IsMainThread()) { From b0b12b3b09fe96dc7f9545c014adca79cd73530d Mon Sep 17 00:00:00 2001 From: SDL Wiki Bot Date: Mon, 21 Jul 2025 16:55:47 +0000 Subject: [PATCH 069/103] Sync SDL3 wiki -> header [ci skip] --- include/SDL3/SDL_tray.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/include/SDL3/SDL_tray.h b/include/SDL3/SDL_tray.h index 8d2c7b1bb1..ec7adfcf7f 100644 --- a/include/SDL3/SDL_tray.h +++ b/include/SDL3/SDL_tray.h @@ -99,15 +99,15 @@ typedef void (SDLCALL *SDL_TrayCallback)(void *userdata, SDL_TrayEntry *entry); /** * Check whether or not tray icons can be created. * - * Note that this function does not guarantee that SDL_CreateTray() will or will - * not work; you should still check SDL_CreateTray() for errors. + * Note that this function does not guarantee that SDL_CreateTray() will or + * will not work; you should still check SDL_CreateTray() for errors. * * Using tray icons require the video subsystem. * * \returns true if trays are available, false otherwise. * - * \threadsafety This function should only be called on the main thread. It will - * return false if not called on the main thread. + * \threadsafety This function should only be called on the main thread. It + * will return false if not called on the main thread. * * \since This function is available since SDL 3.4.0. * From 4a30ee58cae98caf8124375b30bf726e1d75486d Mon Sep 17 00:00:00 2001 From: mitchellcairns Date: Mon, 21 Jul 2025 10:08:27 -0700 Subject: [PATCH 070/103] Implement SInput Device Namings (#13391) --- src/joystick/hidapi/SDL_hidapi_sinput.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c index eae3f12ae5..04af49e810 100644 --- a/src/joystick/hidapi/SDL_hidapi_sinput.c +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -56,6 +56,8 @@ #define SINPUT_DEVICE_COMMAND_PLAYERLED 0x03 #define SINPUT_DEVICE_COMMAND_JOYSTICKRGB 0x04 +#define SINPUT_GENERIC_SUBTYPE_SUPERGAMEPADPLUS 0x01 + #define SINPUT_HAPTIC_TYPE_PRECISE 0x01 #define SINPUT_HAPTIC_TYPE_ERMSIMULATION 0x02 @@ -266,6 +268,8 @@ static void ProcessSDLFeaturesResponse(SDL_HIDAPI_Device *device, Uint8 *data) SDL_Log("SInput Sub-type: %d", (data[5] & 0xF)); #endif + ctx->sub_type = (data[5] & 0xF); + ctx->polling_rate_ms = data[6]; ctx->accelRange = EXTRACTUINT16(data, 8); @@ -438,6 +442,23 @@ static bool HIDAPI_DriverSInput_InitDevice(SDL_HIDAPI_Device *device) return false; } + switch (device->product_id) { + case USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE: + HIDAPI_SetDeviceName(device, "HHL GC Ultimate"); + break; + case USB_PRODUCT_HANDHELDLEGEND_PROGCC: + HIDAPI_SetDeviceName(device, "HHL ProGCC"); + break; + case USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC: + if (ctx->sub_type == SINPUT_GENERIC_SUBTYPE_SUPERGAMEPADPLUS) { + HIDAPI_SetDeviceName(device, "HHL SuperGamepad+"); + } + break; + default: + // Use the USB product name + break; + } + return HIDAPI_JoystickConnected(device, NULL); } From 08fd165dd27366b91254dfb8ed1d403a76ffdd4f Mon Sep 17 00:00:00 2001 From: Maia Date: Sat, 19 Jul 2025 16:23:57 +0200 Subject: [PATCH 071/103] Add POINTER suffix to SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC --- include/SDL3/SDL_iostream.h | 6 +++--- src/io/SDL_iostream.c | 2 +- test/testautomation_iostream.c | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/include/SDL3/SDL_iostream.h b/include/SDL3/SDL_iostream.h index f12339032e..bc7db6c54e 100644 --- a/include/SDL3/SDL_iostream.h +++ b/include/SDL3/SDL_iostream.h @@ -301,7 +301,7 @@ extern SDL_DECLSPEC SDL_IOStream * SDLCALL SDL_IOFromFile(const char *file, cons * * Additionally, the following properties are recognized: * - * - `SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC`: if this property is set to a + * - `SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC_POINTER`: if this property is set to a * non-NULL value it will be interpreted as a function of SDL_free_func type * and called with the passed `mem` pointer when closing the stream. By * default it is unset, i.e., the memory will not be freed. @@ -327,7 +327,7 @@ extern SDL_DECLSPEC SDL_IOStream * SDLCALL SDL_IOFromMem(void *mem, size_t size) #define SDL_PROP_IOSTREAM_MEMORY_POINTER "SDL.iostream.memory.base" #define SDL_PROP_IOSTREAM_MEMORY_SIZE_NUMBER "SDL.iostream.memory.size" -#define SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC "SDL.iostream.memory.free" +#define SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC_POINTER "SDL.iostream.memory.free" /** * Use this function to prepare a read-only memory buffer for use with @@ -354,7 +354,7 @@ extern SDL_DECLSPEC SDL_IOStream * SDLCALL SDL_IOFromMem(void *mem, size_t size) * * Additionally, the following properties are recognized: * - * - `SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC`: if this property is set to a + * - `SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC_POINTER`: if this property is set to a * non-NULL value it will be interpreted as a function of SDL_free_func type * and called with the passed `mem` pointer when closing the stream. By * default it is unset, i.e., the memory will not be freed. diff --git a/src/io/SDL_iostream.c b/src/io/SDL_iostream.c index 2e63b9aefb..7c956baf5f 100644 --- a/src/io/SDL_iostream.c +++ b/src/io/SDL_iostream.c @@ -781,7 +781,7 @@ static size_t SDLCALL mem_write(void *userdata, const void *ptr, size_t size, SD static bool SDLCALL mem_close(void *userdata) { IOStreamMemData *iodata = (IOStreamMemData *) userdata; - SDL_free_func free_func = SDL_GetPointerProperty(iodata->props, SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC, NULL); + SDL_free_func free_func = SDL_GetPointerProperty(iodata->props, SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC_POINTER, NULL); if (free_func) { free_func(iodata->base); } diff --git a/test/testautomation_iostream.c b/test/testautomation_iostream.c index 23a0f28f24..89c4f983f1 100644 --- a/test/testautomation_iostream.c +++ b/test/testautomation_iostream.c @@ -342,7 +342,7 @@ static int SDLCALL iostrm_testMemWithFree(void *arg) /* Set the free function */ free_call_count = 0; - result = SDL_SetPointerProperty(SDL_GetIOProperties(rw), SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC, test_free); + result = SDL_SetPointerProperty(SDL_GetIOProperties(rw), SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC_POINTER, test_free); SDLTest_AssertPass("Call to SDL_SetPointerProperty() succeeded"); SDLTest_AssertCheck(result == true, "Verify result value is true; got %d", result); From 55e14a2cedb555940a81e9a528cfc9d9c7539dbe Mon Sep 17 00:00:00 2001 From: SDL Wiki Bot Date: Mon, 21 Jul 2025 17:16:40 +0000 Subject: [PATCH 072/103] Sync SDL3 wiki -> header [ci skip] --- include/SDL3/SDL_iostream.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/include/SDL3/SDL_iostream.h b/include/SDL3/SDL_iostream.h index bc7db6c54e..2e6a7b52a9 100644 --- a/include/SDL3/SDL_iostream.h +++ b/include/SDL3/SDL_iostream.h @@ -301,9 +301,9 @@ extern SDL_DECLSPEC SDL_IOStream * SDLCALL SDL_IOFromFile(const char *file, cons * * Additionally, the following properties are recognized: * - * - `SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC_POINTER`: if this property is set to a - * non-NULL value it will be interpreted as a function of SDL_free_func type - * and called with the passed `mem` pointer when closing the stream. By + * - `SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC_POINTER`: if this property is set to + * a non-NULL value it will be interpreted as a function of SDL_free_func + * type and called with the passed `mem` pointer when closing the stream. By * default it is unset, i.e., the memory will not be freed. * * \param mem a pointer to a buffer to feed an SDL_IOStream stream. @@ -354,9 +354,9 @@ extern SDL_DECLSPEC SDL_IOStream * SDLCALL SDL_IOFromMem(void *mem, size_t size) * * Additionally, the following properties are recognized: * - * - `SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC_POINTER`: if this property is set to a - * non-NULL value it will be interpreted as a function of SDL_free_func type - * and called with the passed `mem` pointer when closing the stream. By + * - `SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC_POINTER`: if this property is set to + * a non-NULL value it will be interpreted as a function of SDL_free_func + * type and called with the passed `mem` pointer when closing the stream. By * default it is unset, i.e., the memory will not be freed. * * \param mem a pointer to a read-only buffer to feed an SDL_IOStream stream. From 86200d1203f3739d10deb6b72f1ce643b7c1bfb2 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Mon, 21 Jul 2025 10:03:49 -0700 Subject: [PATCH 073/103] Fixed clamp texture address mode in software renderer --- src/render/SDL_render.c | 16 ++++++++ src/render/software/SDL_triangle.c | 64 +++++++----------------------- 2 files changed, 30 insertions(+), 50 deletions(-) diff --git a/src/render/SDL_render.c b/src/render/SDL_render.c index d66a8987c6..a3351ce1fc 100644 --- a/src/render/SDL_render.c +++ b/src/render/SDL_render.c @@ -5001,6 +5001,22 @@ static bool SDLCALL SDL_SW_RenderGeometryRaw(SDL_Renderer *renderer, } } + // Check if UVs within range + if (is_quad) { + const float *uv0_ = (const float *)((const char *)uv + A * color_stride); + const float *uv1_ = (const float *)((const char *)uv + B * color_stride); + const float *uv2_ = (const float *)((const char *)uv + C * color_stride); + const float *uv3_ = (const float *)((const char *)uv + C2 * color_stride); + if (uv0_[0] >= 0.0f && uv0_[0] <= 1.0f && + uv1_[0] >= 0.0f && uv1_[0] <= 1.0f && + uv2_[0] >= 0.0f && uv2_[0] <= 1.0f && + uv3_[0] >= 0.0f && uv3_[0] <= 1.0f) { + // ok + } else { + is_quad = 0; + } + } + // Start rendering rect if (is_quad) { SDL_FRect s; diff --git a/src/render/software/SDL_triangle.c b/src/render/software/SDL_triangle.c index 2a4d150b88..265dc46c81 100644 --- a/src/render/software/SDL_triangle.c +++ b/src/render/software/SDL_triangle.c @@ -149,19 +149,6 @@ static void bounding_rect_fixedpoint(const SDL_Point *a, const SDL_Point *b, con r->h = (max_y - min_y) >> FP_BITS; } -// bounding rect of three points -static void bounding_rect(const SDL_Point *a, const SDL_Point *b, const SDL_Point *c, SDL_Rect *r) -{ - int min_x = SDL_min(a->x, SDL_min(b->x, c->x)); - int max_x = SDL_max(a->x, SDL_max(b->x, c->x)); - int min_y = SDL_min(a->y, SDL_min(b->y, c->y)); - int max_y = SDL_max(a->y, SDL_max(b->y, c->y)); - r->x = min_x; - r->y = min_y; - r->w = (max_x - min_x); - r->h = (max_y - min_y); -} - /* Triangle rendering, using Barycentric coordinates (w0, w1, w2) * * The cross product isn't computed from scratch at each iteration, @@ -186,13 +173,25 @@ static void bounding_rect(const SDL_Point *a, const SDL_Point *b, const SDL_Poin #define TRIANGLE_GET_TEXTCOORD \ int srcx = (int)(((Sint64)w0 * s2s0_x + (Sint64)w1 * s2s1_x + s2_x_area.x) / area); \ int srcy = (int)(((Sint64)w0 * s2s0_y + (Sint64)w1 * s2s1_y + s2_x_area.y) / area); \ - if (texture_address_mode_u == SDL_TEXTURE_ADDRESS_WRAP) { \ + if (texture_address_mode_u == SDL_TEXTURE_ADDRESS_CLAMP) { \ + if (srcx < 0) { \ + srcx = 0; \ + } else if (srcx >= src_surface->w) { \ + srcx = src_surface->w - 1; \ + } \ + } else if (texture_address_mode_u == SDL_TEXTURE_ADDRESS_WRAP) { \ srcx %= src_surface->w; \ if (srcx < 0) { \ srcx += (src_surface->w - 1); \ } \ } \ - if (texture_address_mode_v == SDL_TEXTURE_ADDRESS_WRAP) { \ + if (texture_address_mode_v == SDL_TEXTURE_ADDRESS_CLAMP) { \ + if (srcy < 0) { \ + srcy = 0; \ + } else if (srcy >= src_surface->h) { \ + srcy = src_surface->h - 1; \ + } \ + } else if (texture_address_mode_v == SDL_TEXTURE_ADDRESS_WRAP) { \ srcy %= src_surface->h; \ if (srcy < 0) { \ srcy += (src_surface->h - 1); \ @@ -543,41 +542,6 @@ bool SDL_SW_BlitTriangle( SDL_GetSurfaceBlendMode(src, &blend); - // TRIANGLE_GET_TEXTCOORD interpolates up to the max values included, so reduce by 1 - if (texture_address_mode_u == SDL_TEXTURE_ADDRESS_CLAMP || - texture_address_mode_v == SDL_TEXTURE_ADDRESS_CLAMP) { - SDL_Rect srcrect; - bounding_rect(s0, s1, s2, &srcrect); - if (texture_address_mode_u == SDL_TEXTURE_ADDRESS_CLAMP) { - int maxx = srcrect.x + srcrect.w; - if (srcrect.w > 0) { - if (s0->x == maxx) { - s0->x--; - } - if (s1->x == maxx) { - s1->x--; - } - if (s2->x == maxx) { - s2->x--; - } - } - } - if (texture_address_mode_v == SDL_TEXTURE_ADDRESS_CLAMP) { - int maxy = srcrect.y + srcrect.h; - if (srcrect.h > 0) { - if (s0->y == maxy) { - s0->y--; - } - if (s1->y == maxy) { - s1->y--; - } - if (s2->y == maxy) { - s2->y--; - } - } - } - } - if (is_uniform) { // SDL_GetSurfaceColorMod(src, &r, &g, &b); has_modulation = c0.r != 255 || c0.g != 255 || c0.b != 255 || c0.a != 255; From 64b19fc504ac680da209741cadcfe51f0d7d66c4 Mon Sep 17 00:00:00 2001 From: Thomas Stehle Date: Sat, 19 Jul 2025 14:02:41 +0200 Subject: [PATCH 074/103] Added missing handling of texture address mode to SDL render Vulkan backend --- src/render/vulkan/SDL_render_vulkan.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/render/vulkan/SDL_render_vulkan.c b/src/render/vulkan/SDL_render_vulkan.c index 0fe78973df..6eff6c4daf 100644 --- a/src/render/vulkan/SDL_render_vulkan.c +++ b/src/render/vulkan/SDL_render_vulkan.c @@ -3735,6 +3735,28 @@ static VkSampler VULKAN_GetSampler(VULKAN_RenderData *data, SDL_ScaleMode scale_ SDL_SetError("Unknown scale mode: %d", scale_mode); return VK_NULL_HANDLE; } + switch (address_u) { + case SDL_TEXTURE_ADDRESS_CLAMP: + samplerCreateInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + break; + case SDL_TEXTURE_ADDRESS_WRAP: + samplerCreateInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; + break; + default: + SDL_SetError("Unknown texture address mode: %d", address_u); + return VK_NULL_HANDLE; + } + switch (address_v) { + case SDL_TEXTURE_ADDRESS_CLAMP: + samplerCreateInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + break; + case SDL_TEXTURE_ADDRESS_WRAP: + samplerCreateInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT; + break; + default: + SDL_SetError("Unknown texture address mode: %d", address_v); + return VK_NULL_HANDLE; + } VkResult result = vkCreateSampler(data->device, &samplerCreateInfo, NULL, &data->samplers[key]); if (result != VK_SUCCESS) { SET_ERROR_CODE("vkCreateSampler()", result); From 0eaf28ed4d66083f61d9703707535af5f1cfad71 Mon Sep 17 00:00:00 2001 From: Thomas Stehle Date: Sat, 19 Jul 2025 16:33:53 +0200 Subject: [PATCH 075/103] Added test and test image for clamped texture address mode to render testautomation --- test/testautomation_images.c | 435 +++++++++++++++++++++++++++++++++++ test/testautomation_images.h | 1 + test/testautomation_render.c | 109 +++++++++ 3 files changed, 545 insertions(+) diff --git a/test/testautomation_images.c b/test/testautomation_images.c index 839c2a2756..b9b2d78d31 100644 --- a/test/testautomation_images.c +++ b/test/testautomation_images.c @@ -1501,6 +1501,441 @@ SDL_Surface *SDLTest_ImageBlitColor(void) return surface; } + +static const SDLTest_SurfaceImage_t SDLTest_imageClampedSprite = { + 80, + 60, + 3, + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377\0\377" + "\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377\0\377\377" + "\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0" + "\377\377\0\377\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0" + "\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377" + "\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377" + "\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0" + "\377\377\0\377\377\0\377\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377\0\377" + "\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377" + "\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0" + "\377\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377\0\377\377\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\377\377\0\377\377\0\377\377\0\377\377\0\0\0\0\0\0\0\0\0\0\0\0\0\377" + "\377\0\377\377\0\377\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377\0\377\377\0\377\377\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377\0\377\377\0\377\377\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\377\377\0\377\377\0\377\377\0\377\377\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377" + "\0\377\377\0\377\377\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377\0\377\377" + "\0\377\377\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377\0\377\377\0\377\377" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0" + "\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377" + "\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377" + "\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0" + "\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377" + "\377\0\377\377\0\377\377\0\377\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377\0\377\377\0\377\377" + "\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\0\0\0\0\0\0\377\377" + "\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0" + "\377\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377" + "\377\0\377\377\0\377\377\0\0\0\0\0\0\0\377\377\0\377\377\0\377\377\0\377" + "\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377" + "\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0" + "\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377" + "\377\0\377\377\0\377\377\0\377\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377\0\377\377\0" + "\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377" + "\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\377\377\0\377\377\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377\0" + "\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\0\0\0\0\0\0" + "\0\0\0\377\377\0\377\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377" + "\0\377\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377\0\377\377\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377\0\377\377\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\0\377\377\0" + "\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\377\377\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", +}; + +/** + * \brief Returns the clamped rendering sprite test image as an SDL_Surface. + */ +SDL_Surface *SDLTest_ImageClampedSprite(void) +{ + SDL_Surface *surface = SDL_CreateSurfaceFrom( + SDLTest_imageClampedSprite.width, + SDLTest_imageClampedSprite.height, + SDL_PIXELFORMAT_RGB24, + (void *)SDLTest_imageClampedSprite.pixel_data, + SDLTest_imageClampedSprite.width * SDLTest_imageClampedSprite.bytes_per_pixel); + return surface; +} + /* GIMP RGBA C-Source image dump (face.c) */ static const SDLTest_SurfaceImage_t SDLTest_imageFace = { diff --git a/test/testautomation_images.h b/test/testautomation_images.h index f5b2c9af2b..7af58c835f 100644 --- a/test/testautomation_images.h +++ b/test/testautomation_images.h @@ -26,6 +26,7 @@ typedef struct SDLTest_SurfaceImage_s { extern SDL_Surface *SDLTest_ImageBlit(void); extern SDL_Surface *SDLTest_ImageBlitTiled(void); extern SDL_Surface *SDLTest_ImageBlitColor(void); +extern SDL_Surface *SDLTest_ImageClampedSprite(void); extern SDL_Surface *SDLTest_ImageFace(void); extern SDL_Surface *SDLTest_ImagePrimitives(void); extern SDL_Surface *SDLTest_ImageBlendingBackground(void); diff --git a/test/testautomation_render.c b/test/testautomation_render.c index 714a34acbb..3bf8662baa 100644 --- a/test/testautomation_render.c +++ b/test/testautomation_render.c @@ -1757,6 +1757,110 @@ clearScreen(void) return 0; } +/** + * Tests geometry UV clamping + */ +static int SDLCALL render_testUVClamping(void *arg) +{ + SDL_Vertex vertices[6]; + SDL_Vertex *verts = vertices; + SDL_FColor color = { 1.0f, 1.0f, 1.0f, 1.0f }; + SDL_FRect rect; + float min_U = -0.5f; + float max_U = 1.5f; + float min_V = -0.5f; + float max_V = 1.5f; + SDL_Texture *tface; + SDL_Surface *referenceSurface = NULL; + + /* Clear surface. */ + clearScreen(); + + /* Create face surface. */ + tface = loadTestFace(); + SDLTest_AssertCheck(tface != NULL, "Verify loadTestFace() result"); + if (tface == NULL) { + return TEST_ABORTED; + } + + rect.w = (float)tface->w * 2; + rect.h = (float)tface->h * 2; + rect.x = (TESTRENDER_SCREEN_W - rect.w) / 2; + rect.y = (TESTRENDER_SCREEN_H - rect.h) / 2; + + /* + * 0--1 + * | /| + * |/ | + * 3--2 + * + * Draw sprite2 as triangles that can be recombined as rect by software renderer + */ + + /* 0 */ + verts->position.x = rect.x; + verts->position.y = rect.y; + verts->color = color; + verts->tex_coord.x = min_U; + verts->tex_coord.y = min_V; + verts++; + /* 1 */ + verts->position.x = rect.x + rect.w; + verts->position.y = rect.y; + verts->color = color; + verts->tex_coord.x = max_U; + verts->tex_coord.y = min_V; + verts++; + /* 2 */ + verts->position.x = rect.x + rect.w; + verts->position.y = rect.y + rect.h; + verts->color = color; + verts->tex_coord.x = max_U; + verts->tex_coord.y = max_V; + verts++; + /* 0 */ + verts->position.x = rect.x; + verts->position.y = rect.y; + verts->color = color; + verts->tex_coord.x = min_U; + verts->tex_coord.y = min_V; + verts++; + /* 2 */ + verts->position.x = rect.x + rect.w; + verts->position.y = rect.y + rect.h; + verts->color = color; + verts->tex_coord.x = max_U; + verts->tex_coord.y = max_V; + verts++; + /* 3 */ + verts->position.x = rect.x; + verts->position.y = rect.y + rect.h; + verts->color = color; + verts->tex_coord.x = min_U; + verts->tex_coord.y = max_V; + verts++; + + /* Set texture address mode to clamp */ + SDL_SetRenderTextureAddressMode(renderer, SDL_TEXTURE_ADDRESS_CLAMP, SDL_TEXTURE_ADDRESS_CLAMP); + + /* Blit sprites as triangles onto the screen */ + SDL_RenderGeometry(renderer, tface, vertices, 6, NULL, 0); + + /* See if it's the same */ + referenceSurface = SDLTest_ImageClampedSprite(); + compare(referenceSurface, ALLOWABLE_ERROR_OPAQUE); + + /* Make current */ + SDL_RenderPresent(renderer); + + /* Clean up. */ + SDL_DestroyTexture(tface); + SDL_DestroySurface(referenceSurface); + referenceSurface = NULL; + + return TEST_COMPLETED; +} + /** * Tests geometry UV wrapping */ @@ -2035,6 +2139,10 @@ static const SDLTest_TestCaseReference renderTestLogicalSize = { render_testLogicalSize, "render_testLogicalSize", "Tests logical size", TEST_ENABLED }; +static const SDLTest_TestCaseReference renderTestUVClamping = { + render_testUVClamping, "render_testUVClamping", "Tests geometry UV clamping", TEST_ENABLED +}; + static const SDLTest_TestCaseReference renderTestUVWrapping = { render_testUVWrapping, "render_testUVWrapping", "Tests geometry UV wrapping", TEST_ENABLED }; @@ -2065,6 +2173,7 @@ static const SDLTest_TestCaseReference *renderTests[] = { &renderTestViewport, &renderTestClipRect, &renderTestLogicalSize, + &renderTestUVClamping, &renderTestUVWrapping, &renderTestTextureState, &renderTestGetSetTextureScaleMode, From fc19ae343cc1fd912aec157451e58d4340f58f13 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Mon, 21 Jul 2025 11:38:54 -0700 Subject: [PATCH 076/103] Updated SDL_IsJoystickSInputController() to match style in file --- src/joystick/SDL_joystick.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c index 0a1b13c338..70c952b78a 100644 --- a/src/joystick/SDL_joystick.c +++ b/src/joystick/SDL_joystick.c @@ -3204,13 +3204,15 @@ bool SDL_IsJoystickHoriSteamController(Uint16 vendor_id, Uint16 product_id) bool SDL_IsJoystickSInputController(Uint16 vendor_id, Uint16 product_id) { - bool vendor_match = (vendor_id == USB_VENDOR_RASPBERRYPI); - bool product_match = - (product_id == USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC) | - (product_id == USB_PRODUCT_HANDHELDLEGEND_PROGCC) | - (product_id == USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE) | - (product_id == USB_PRODUCT_BONJIRICHANNEL_FIREBIRD); - return (vendor_match && product_match); + if (vendor_id == USB_VENDOR_RASPBERRYPI) { + if (product_id == USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC || + product_id == USB_PRODUCT_HANDHELDLEGEND_PROGCC || + product_id == USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE || + product_id == USB_PRODUCT_BONJIRICHANNEL_FIREBIRD) { + return true; + } + } + return false; } bool SDL_IsJoystickFlydigiController(Uint16 vendor_id, Uint16 product_id) From b2d152e51f6c0ac32b559335e0d0da3c8301f0c9 Mon Sep 17 00:00:00 2001 From: Anon Ymous <73802848+ancientstraits@users.noreply.github.com> Date: Fri, 18 Jul 2025 00:45:42 +0530 Subject: [PATCH 077/103] dialog: fix default file name on Cocoa --- src/dialog/cocoa/SDL_cocoadialog.m | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/dialog/cocoa/SDL_cocoadialog.m b/src/dialog/cocoa/SDL_cocoadialog.m index 671ca887c5..4ae894cddf 100644 --- a/src/dialog/cocoa/SDL_cocoadialog.m +++ b/src/dialog/cocoa/SDL_cocoadialog.m @@ -158,7 +158,14 @@ void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFil [dialog setAllowsOtherFileTypes:YES]; if (default_location) { - [dialog setDirectoryURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String:default_location]]]; + char last = default_location[SDL_strlen(default_location) - 1]; + NSURL* url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:default_location]]; + if (last == '/') { + [dialog setDirectoryURL:url]; + } else { + [dialog setDirectoryURL:[url URLByDeletingLastPathComponent]]; + [dialog setNameFieldStringValue:[url lastPathComponent]]; + } } NSWindow *w = NULL; From 27caa5769530cd2f003b4c8a81a8e5e1d5a4fd68 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Mon, 21 Jul 2025 16:02:42 -0400 Subject: [PATCH 078/103] dialog: Make sure we don't underflow a string in Cocoa backend. Fixes #13014. --- src/dialog/cocoa/SDL_cocoadialog.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialog/cocoa/SDL_cocoadialog.m b/src/dialog/cocoa/SDL_cocoadialog.m index 4ae894cddf..f1b9c3bbaa 100644 --- a/src/dialog/cocoa/SDL_cocoadialog.m +++ b/src/dialog/cocoa/SDL_cocoadialog.m @@ -157,7 +157,7 @@ void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFil // Keep behavior consistent with other platforms [dialog setAllowsOtherFileTypes:YES]; - if (default_location) { + if (default_location && *default_location) { char last = default_location[SDL_strlen(default_location) - 1]; NSURL* url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:default_location]]; if (last == '/') { From a977a11fa6662a5f8002aa2ca2e3e21b93d0538d Mon Sep 17 00:00:00 2001 From: L zard Date: Mon, 21 Jul 2025 19:27:41 +0200 Subject: [PATCH 079/103] `build_config_windows`: fix `HAVE_VSSCANF` defined regardless of MSVC version. [sdl-ci-filter msvc-*] --- include/build_config/SDL_build_config_windows.h | 1 - 1 file changed, 1 deletion(-) diff --git a/include/build_config/SDL_build_config_windows.h b/include/build_config/SDL_build_config_windows.h index 24c578b6f8..4272d9b094 100644 --- a/include/build_config/SDL_build_config_windows.h +++ b/include/build_config/SDL_build_config_windows.h @@ -156,7 +156,6 @@ typedef unsigned int uintptr_t; #define HAVE_STRCMP 1 #define HAVE_STRNCMP 1 #define HAVE_STRPBRK 1 -#define HAVE_VSSCANF 1 #define HAVE_VSNPRINTF 1 #define HAVE_ACOS 1 #define HAVE_ASIN 1 From ea995b1694e2af30a9dea1a1dadaa0658540d848 Mon Sep 17 00:00:00 2001 From: L zard Date: Mon, 21 Jul 2025 19:33:56 +0200 Subject: [PATCH 080/103] `build_config_windows`: define `HAVE_STDARG/STDDEF_H` outside of condition. They are defined in both `#if HAVE_LIBC` and its `#else` anyway. [sdl-ci-filter msvc-*] --- include/build_config/SDL_build_config_windows.h | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/include/build_config/SDL_build_config_windows.h b/include/build_config/SDL_build_config_windows.h index 4272d9b094..2e5a0d5cde 100644 --- a/include/build_config/SDL_build_config_windows.h +++ b/include/build_config/SDL_build_config_windows.h @@ -114,6 +114,9 @@ typedef unsigned int uintptr_t; # define SDL_DISABLE_AVX 1 #endif +#define HAVE_STDARG_H 1 +#define HAVE_STDDEF_H 1 + /* This can be disabled to avoid C runtime dependencies and manifest requirements */ #ifndef HAVE_LIBC #define HAVE_LIBC 1 @@ -125,8 +128,6 @@ typedef unsigned int uintptr_t; #define HAVE_LIMITS_H 1 #define HAVE_MATH_H 1 #define HAVE_SIGNAL_H 1 -#define HAVE_STDARG_H 1 -#define HAVE_STDDEF_H 1 #define HAVE_STDIO_H 1 #define HAVE_STDLIB_H 1 #define HAVE_STRING_H 1 @@ -213,10 +214,7 @@ typedef unsigned int uintptr_t; #if _MSC_VER >= 1400 #define HAVE__FSEEKI64 1 #endif -#endif /* _MSC_VER */ -#else -#define HAVE_STDARG_H 1 -#define HAVE_STDDEF_H 1 +#endif /* _MSC_VER */ #endif /* Enable various audio drivers */ From 279a50cc2654c67814f118eefe1d2a75d9c46850 Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Thu, 17 Jul 2025 18:17:14 -0400 Subject: [PATCH 081/103] wayland: Fall-back to the compositor for fullscreen-desktop window placement Only use the specified output if an exclusive mode is being used, or a position was explicitly requested before entering fullscreen desktop. Otherwise, let the compositor handle placement, as it has more information about where the window is and where it should go, especially if fullscreen was requested before the window was fully mapped, or the window spans multiple outputs. --- src/video/wayland/SDL_waylandwindow.c | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c index ec7bffe6bb..84270567fd 100644 --- a/src/video/wayland/SDL_waylandwindow.c +++ b/src/video/wayland/SDL_waylandwindow.c @@ -589,7 +589,7 @@ static void Wayland_move_window(SDL_Window *window) } } -static void SetFullscreen(SDL_Window *window, struct wl_output *output) +static void SetFullscreen(SDL_Window *window, struct wl_output *output, bool fullscreen) { SDL_WindowData *wind = window->internal; SDL_VideoData *viddata = wind->waylandData; @@ -602,7 +602,7 @@ static void SetFullscreen(SDL_Window *window, struct wl_output *output) wind->fullscreen_exclusive = output ? window->fullscreen_exclusive : false; ++wind->fullscreen_deadline_count; - if (output) { + if (fullscreen) { Wayland_SetWindowResizable(SDL_GetVideoDevice(), window, true); wl_surface_commit(wind->surface); @@ -619,7 +619,7 @@ static void SetFullscreen(SDL_Window *window, struct wl_output *output) wind->fullscreen_exclusive = output ? window->fullscreen_exclusive : false; ++wind->fullscreen_deadline_count; - if (output) { + if (fullscreen) { Wayland_SetWindowResizable(SDL_GetVideoDevice(), window, true); wl_surface_commit(wind->surface); @@ -655,7 +655,7 @@ static void UpdateWindowFullscreen(SDL_Window *window, bool fullscreen) SDL_VideoDisplay *disp = SDL_GetVideoDisplay(window->current_fullscreen_mode.displayID); if (disp) { wind->fullscreen_was_positioned = true; - SetFullscreen(window, disp->internal->output); + SetFullscreen(window, disp->internal->output, true); } } } @@ -2314,7 +2314,19 @@ SDL_FullscreenResult Wayland_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Win // Don't send redundant fullscreen set/unset events. if (!!fullscreen != wind->is_fullscreen) { wind->fullscreen_was_positioned = !!fullscreen; - SetFullscreen(window, fullscreen ? output : NULL); + + /* Only use the specified output if an exclusive mode is being used, or a position was explicitly requested + * before entering fullscreen desktop. Otherwise, let the compositor handle placement, as it has more + * information about where the window is and where it should go, particularly if fullscreen is being requested + * before the window is mapped, or the window spans multiple outputs. + */ + if (!window->fullscreen_exclusive) { + if (window->undefined_x || window->undefined_y || + (wind->num_outputs && !window->last_position_pending)) { + output = NULL; + } + } + SetFullscreen(window, output, !!fullscreen); } else if (wind->is_fullscreen) { /* * If the window is already fullscreen, this is likely a request to switch between @@ -2325,7 +2337,7 @@ SDL_FullscreenResult Wayland_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Win */ if (wind->last_displayID != display->id) { wind->fullscreen_was_positioned = true; - SetFullscreen(window, output); + SetFullscreen(window, output, true); } else { ConfigureWindowGeometry(window); CommitLibdecorFrame(window); @@ -2759,7 +2771,7 @@ bool Wayland_SetWindowPosition(SDL_VideoDevice *_this, SDL_Window *window) SDL_VideoDisplay *display = SDL_GetVideoDisplayForFullscreenWindow(window); if (display && wind->last_displayID != display->id) { struct wl_output *output = display->internal->output; - SetFullscreen(window, output); + SetFullscreen(window, output, true); return true; } From af1c05fd58d67e3a3c3acbbdf7d9aea533ead697 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Tue, 22 Jul 2025 12:28:01 -0400 Subject: [PATCH 082/103] filesystem: Check SDL_GetPrefPath parameters at the higher level. ...so the backends don't have to do it. Also added a stern warning about `org` being omitted, but leaving it as allowed so as not to break existing apps (more than they are already broken, at least). Fixes #13322. --- include/SDL3/SDL_filesystem.h | 6 ++++++ src/filesystem/SDL_filesystem.c | 10 ++++++++++ src/filesystem/cocoa/SDL_sysfilesystem.m | 11 +---------- src/filesystem/emscripten/SDL_sysfilesystem.c | 12 +----------- src/filesystem/gdk/SDL_sysfilesystem.cpp | 5 ----- src/filesystem/haiku/SDL_sysfilesystem.cc | 8 -------- src/filesystem/n3ds/SDL_sysfilesystem.c | 5 ----- src/filesystem/ps2/SDL_sysfilesystem.c | 10 ---------- src/filesystem/psp/SDL_sysfilesystem.c | 13 +------------ src/filesystem/riscos/SDL_sysfilesystem.c | 11 +---------- src/filesystem/unix/SDL_sysfilesystem.c | 11 +---------- src/filesystem/vita/SDL_sysfilesystem.c | 14 +------------- src/filesystem/windows/SDL_sysfilesystem.c | 8 -------- 13 files changed, 22 insertions(+), 102 deletions(-) diff --git a/include/SDL3/SDL_filesystem.h b/include/SDL3/SDL_filesystem.h index 031feaf98e..e428258218 100644 --- a/include/SDL3/SDL_filesystem.h +++ b/include/SDL3/SDL_filesystem.h @@ -134,6 +134,12 @@ extern SDL_DECLSPEC const char * SDLCALL SDL_GetBasePath(void); * - ...only use letters, numbers, and spaces. Avoid punctuation like "Game * Name 2: Bad Guy's Revenge!" ... "Game Name 2" is sufficient. * + * Due to historical mistakes, `org` is allowed to be NULL or "". In such + * cases, SDL will omit the org subdirectory, including on platforms where it + * shouldn't, and including on platforms where this would make your app fail + * certification for an app store. New apps should definitely specify a real + * string for `org`. + * * The returned path is guaranteed to end with a path separator ('\\' on * Windows, '/' on most other platforms). * diff --git a/src/filesystem/SDL_filesystem.c b/src/filesystem/SDL_filesystem.c index b115019ba5..8bd7980aaa 100644 --- a/src/filesystem/SDL_filesystem.c +++ b/src/filesystem/SDL_filesystem.c @@ -502,6 +502,16 @@ const char *SDL_GetUserFolder(SDL_Folder folder) char *SDL_GetPrefPath(const char *org, const char *app) { + if (!app) { + SDL_InvalidParamError("app"); + return NULL; + } + + // if org is NULL, just make it "" so backends don't have to check both. + if (!org) { + org = ""; + } + return SDL_SYS_GetPrefPath(org, app); } diff --git a/src/filesystem/cocoa/SDL_sysfilesystem.m b/src/filesystem/cocoa/SDL_sysfilesystem.m index 6ecef5dfc2..d0b4ba9553 100644 --- a/src/filesystem/cocoa/SDL_sysfilesystem.m +++ b/src/filesystem/cocoa/SDL_sysfilesystem.m @@ -69,14 +69,6 @@ char *SDL_SYS_GetPrefPath(const char *org, const char *app) char *result = NULL; NSArray *array; - if (!app) { - SDL_InvalidParamError("app"); - return NULL; - } - if (!org) { - org = ""; - } - #ifndef SDL_PLATFORM_TVOS array = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); #else @@ -106,13 +98,12 @@ char *SDL_SYS_GetPrefPath(const char *org, const char *app) const size_t len = SDL_strlen(base) + SDL_strlen(org) + SDL_strlen(app) + 4; result = (char *)SDL_malloc(len); if (result != NULL) { - char *ptr; if (*org) { SDL_snprintf(result, len, "%s/%s/%s/", base, org, app); } else { SDL_snprintf(result, len, "%s/%s/", base, app); } - for (ptr = result + 1; *ptr; ptr++) { + for (char *ptr = result + 1; *ptr; ptr++) { if (*ptr == '/') { *ptr = '\0'; mkdir(result, 0700); diff --git a/src/filesystem/emscripten/SDL_sysfilesystem.c b/src/filesystem/emscripten/SDL_sysfilesystem.c index 29dc053511..afb9705b47 100644 --- a/src/filesystem/emscripten/SDL_sysfilesystem.c +++ b/src/filesystem/emscripten/SDL_sysfilesystem.c @@ -42,17 +42,7 @@ char *SDL_SYS_GetPrefPath(const char *org, const char *app) const char *append = "/libsdl/"; char *result; char *ptr = NULL; - size_t len = 0; - - if (!app) { - SDL_InvalidParamError("app"); - return NULL; - } - if (!org) { - org = ""; - } - - len = SDL_strlen(append) + SDL_strlen(org) + SDL_strlen(app) + 3; + const size_t len = SDL_strlen(append) + SDL_strlen(org) + SDL_strlen(app) + 3; result = (char *)SDL_malloc(len); if (!result) { return NULL; diff --git a/src/filesystem/gdk/SDL_sysfilesystem.cpp b/src/filesystem/gdk/SDL_sysfilesystem.cpp index 9a97f1873b..17baafb720 100644 --- a/src/filesystem/gdk/SDL_sysfilesystem.cpp +++ b/src/filesystem/gdk/SDL_sysfilesystem.cpp @@ -90,11 +90,6 @@ char *SDL_SYS_GetPrefPath(const char *org, const char *app) HRESULT result; const char *csid = SDL_GetHint("SDL_GDK_SERVICE_CONFIGURATION_ID"); - if (!app) { - SDL_InvalidParamError("app"); - return NULL; - } - // This should be set before calling SDL_GetPrefPath! if (!csid) { SDL_LogWarn(SDL_LOG_CATEGORY_SYSTEM, "Set SDL_GDK_SERVICE_CONFIGURATION_ID before calling SDL_GetPrefPath!"); diff --git a/src/filesystem/haiku/SDL_sysfilesystem.cc b/src/filesystem/haiku/SDL_sysfilesystem.cc index af8b5ab4d3..1c8a0acc4d 100644 --- a/src/filesystem/haiku/SDL_sysfilesystem.cc +++ b/src/filesystem/haiku/SDL_sysfilesystem.cc @@ -72,14 +72,6 @@ char *SDL_SYS_GetPrefPath(const char *org, const char *app) const char *append = "/config/settings/"; size_t len = SDL_strlen(home); - if (!app) { - SDL_InvalidParamError("app"); - return NULL; - } - if (!org) { - org = ""; - } - if (!len || (home[len - 1] == '/')) { ++append; // home empty or ends with separator, skip the one from append } diff --git a/src/filesystem/n3ds/SDL_sysfilesystem.c b/src/filesystem/n3ds/SDL_sysfilesystem.c index 8386a91c5c..36b8c23126 100644 --- a/src/filesystem/n3ds/SDL_sysfilesystem.c +++ b/src/filesystem/n3ds/SDL_sysfilesystem.c @@ -43,11 +43,6 @@ char *SDL_SYS_GetBasePath(void) char *SDL_SYS_GetPrefPath(const char *org, const char *app) { char *pref_path = NULL; - if (!app) { - SDL_InvalidParamError("app"); - return NULL; - } - pref_path = MakePrefPath(app); if (!pref_path) { return NULL; diff --git a/src/filesystem/ps2/SDL_sysfilesystem.c b/src/filesystem/ps2/SDL_sysfilesystem.c index ca69c2bd0a..8b4644e79a 100644 --- a/src/filesystem/ps2/SDL_sysfilesystem.c +++ b/src/filesystem/ps2/SDL_sysfilesystem.c @@ -80,15 +80,6 @@ char *SDL_SYS_GetPrefPath(const char *org, const char *app) char *result = NULL; size_t len; - if (!app) { - SDL_InvalidParamError("app"); - return NULL; - } - - if (!org) { - org = ""; - } - const char *base = SDL_GetBasePath(); if (!base) { return NULL; @@ -102,7 +93,6 @@ char *SDL_SYS_GetPrefPath(const char *org, const char *app) } else { SDL_snprintf(result, len, "%s%s/", base, app); } - recursive_mkdir(result); } diff --git a/src/filesystem/psp/SDL_sysfilesystem.c b/src/filesystem/psp/SDL_sysfilesystem.c index 4b40055b35..eb9356a988 100644 --- a/src/filesystem/psp/SDL_sysfilesystem.c +++ b/src/filesystem/psp/SDL_sysfilesystem.c @@ -49,22 +49,12 @@ char *SDL_SYS_GetBasePath(void) char *SDL_SYS_GetPrefPath(const char *org, const char *app) { char *result = NULL; - size_t len; - if (!app) { - SDL_InvalidParamError("app"); - return NULL; - } - const char *base = SDL_GetBasePath(); if (!base) { return NULL; } - if (!org) { - org = ""; - } - - len = SDL_strlen(base) + SDL_strlen(org) + SDL_strlen(app) + 4; + const size_t len = SDL_strlen(base) + SDL_strlen(org) + SDL_strlen(app) + 4; result = (char *)SDL_malloc(len); if (result) { if (*org) { @@ -72,7 +62,6 @@ char *SDL_SYS_GetPrefPath(const char *org, const char *app) } else { SDL_snprintf(result, len, "%s%s/", base, app); } - mkdir(result, 0755); } diff --git a/src/filesystem/riscos/SDL_sysfilesystem.c b/src/filesystem/riscos/SDL_sysfilesystem.c index 95ed80fda9..d394a6473b 100644 --- a/src/filesystem/riscos/SDL_sysfilesystem.c +++ b/src/filesystem/riscos/SDL_sysfilesystem.c @@ -155,23 +155,14 @@ char *SDL_SYS_GetBasePath(void) char *SDL_SYS_GetPrefPath(const char *org, const char *app) { char *canon, *dir, *result; - size_t len; _kernel_oserror *error; - if (!app) { - SDL_InvalidParamError("app"); - return NULL; - } - if (!org) { - org = ""; - } - canon = canonicalisePath("", "Run$Path"); if (!canon) { return NULL; } - len = SDL_strlen(canon) + SDL_strlen(org) + SDL_strlen(app) + 4; + const size_t len = SDL_strlen(canon) + SDL_strlen(org) + SDL_strlen(app) + 4; dir = (char *)SDL_malloc(len); if (!dir) { SDL_free(canon); diff --git a/src/filesystem/unix/SDL_sysfilesystem.c b/src/filesystem/unix/SDL_sysfilesystem.c index 858aaa2c54..751cc8a45a 100644 --- a/src/filesystem/unix/SDL_sysfilesystem.c +++ b/src/filesystem/unix/SDL_sysfilesystem.c @@ -269,15 +269,6 @@ char *SDL_SYS_GetPrefPath(const char *org, const char *app) const char *append; char *result = NULL; char *ptr = NULL; - size_t len = 0; - - if (!app) { - SDL_InvalidParamError("app"); - return NULL; - } - if (!org) { - org = ""; - } if (!envr) { // You end up with "$HOME/.local/share/Game Name 2" @@ -292,7 +283,7 @@ char *SDL_SYS_GetPrefPath(const char *org, const char *app) append = "/"; } - len = SDL_strlen(envr); + size_t len = SDL_strlen(envr); if (envr[len - 1] == '/') { append += 1; } diff --git a/src/filesystem/vita/SDL_sysfilesystem.c b/src/filesystem/vita/SDL_sysfilesystem.c index 8b65e8ae41..d99a83d125 100644 --- a/src/filesystem/vita/SDL_sysfilesystem.c +++ b/src/filesystem/vita/SDL_sysfilesystem.c @@ -46,19 +46,7 @@ char *SDL_SYS_GetPrefPath(const char *org, const char *app) const char *envr = "ux0:/data/"; char *result = NULL; char *ptr = NULL; - size_t len = 0; - - if (!app) { - SDL_InvalidParamError("app"); - return NULL; - } - if (!org) { - org = ""; - } - - len = SDL_strlen(envr); - - len += SDL_strlen(org) + SDL_strlen(app) + 3; + size_t len = SDL_strlen(envr) + SDL_strlen(org) + SDL_strlen(app) + 3; result = (char *)SDL_malloc(len); if (!result) { return NULL; diff --git a/src/filesystem/windows/SDL_sysfilesystem.c b/src/filesystem/windows/SDL_sysfilesystem.c index a4c033f068..85bf1ceb96 100644 --- a/src/filesystem/windows/SDL_sysfilesystem.c +++ b/src/filesystem/windows/SDL_sysfilesystem.c @@ -110,14 +110,6 @@ char *SDL_SYS_GetPrefPath(const char *org, const char *app) size_t new_wpath_len = 0; BOOL api_result = FALSE; - if (!app) { - SDL_InvalidParamError("app"); - return NULL; - } - if (!org) { - org = ""; - } - hr = SHGetFolderPathW(NULL, CSIDL_APPDATA | CSIDL_FLAG_CREATE, NULL, 0, path); if (!SUCCEEDED(hr)) { WIN_SetErrorFromHRESULT("Couldn't locate our prefpath", hr); From 07ef5326817c4d9cf4d7d1ec0368edb729359f42 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Tue, 22 Jul 2025 13:19:30 -0400 Subject: [PATCH 083/103] hints: Renamed SDL_HINT_LOG_BACKENDS to SDL_DEBUG_LOGGING. This still logs backend choices, but we might use it for other things. For example, what Android device is being used, or all the devices we enumerated, etc. Ideally this eventually logs all the stuff we often have to ask followup questions about. --- include/SDL3/SDL_hints.h | 21 ++++++++++++--------- src/SDL_utils.c | 9 +++++---- src/SDL_utils_c.h | 2 +- src/audio/SDL_audio.c | 2 +- src/camera/SDL_camera.c | 2 +- src/gpu/SDL_gpu.c | 2 +- src/io/io_uring/SDL_asyncio_liburing.c | 4 ++-- src/io/windows/SDL_asyncio_windows_ioring.c | 4 ++-- src/render/SDL_render.c | 2 +- src/storage/SDL_storage.c | 4 ++-- src/video/SDL_video.c | 2 +- 11 files changed, 29 insertions(+), 25 deletions(-) diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h index d330c1d681..09c918b13a 100644 --- a/include/SDL3/SDL_hints.h +++ b/include/SDL3/SDL_hints.h @@ -4396,26 +4396,29 @@ extern "C" { #define SDL_HINT_PEN_TOUCH_EVENTS "SDL_PEN_TOUCH_EVENTS" /** - * A variable controlling whether SDL backend information is logged. + * A variable controlling whether SDL logs some debug information. * * The variable can be set to the following values: * - * - "0": Subsystem information will not be logged. (default) - * - "1": Subsystem information will be logged. + * - "0": SDL debug information will not be logged. (default) + * - "1": SDL debug information will be logged. * * This is generally meant to be used as an environment variable to let - * end-users report what subsystems were chosen on their system, to aid in - * debugging. Logged information is sent through SDL_Log(), which means by - * default they appear on stdout on most platforms or maybe - * OutputDebugString() on Windows, and can be funneled by the app with - * SDL_SetLogOutputFunction(), etc. + * end-users report what subsystems were chosen on their system, perhaps what + * sort of hardware they are running on, etc, to aid in debugging. Logged + * information is sent through SDL_Log(), which means by default they appear + * on stdout on most platforms, or maybe OutputDebugString() on Windows, and + * can be funneled by the app with SDL_SetLogOutputFunction(), etc. + * + * The specific output might change between SDL versions; more information + * might be deemed useful in the future. * * This hint can be set anytime, but the specific logs are generated during * subsystem init. * * \since This hint is available since SDL 3.4.0. */ -#define SDL_HINT_LOG_BACKENDS "SDL_LOG_BACKENDS" +#define SDL_HINT_DEBUG_LOGGING "SDL_DEBUG_LOGGING" /** * An enumeration of hint priorities. diff --git a/src/SDL_utils.c b/src/SDL_utils.c index ec2c435a92..47fa28c06d 100644 --- a/src/SDL_utils.c +++ b/src/SDL_utils.c @@ -553,11 +553,12 @@ char *SDL_CreateDeviceName(Uint16 vendor, Uint16 product, const char *vendor_nam return name; } -void SDL_LogBackend(const char *subsystem, const char *backend) +#define SDL_DEBUG_LOG_INTRO "SDL_DEBUG: " + +void SDL_DebugLogBackend(const char *subsystem, const char *backend) { - if (SDL_GetHintBoolean(SDL_HINT_LOG_BACKENDS, false)) { - SDL_Log("SDL_BACKEND: %s -> '%s'", subsystem, backend); + if (SDL_GetHintBoolean(SDL_HINT_DEBUG_LOGGING, false)) { + SDL_Log(SDL_DEBUG_LOG_INTRO "chose %s backend '%s'", subsystem, backend); } } - diff --git a/src/SDL_utils_c.h b/src/SDL_utils_c.h index 2929e7f1f1..b70b64e963 100644 --- a/src/SDL_utils_c.h +++ b/src/SDL_utils_c.h @@ -76,6 +76,6 @@ extern const char *SDL_GetPersistentString(const char *string); extern char *SDL_CreateDeviceName(Uint16 vendor, Uint16 product, const char *vendor_name, const char *product_name, const char *default_name); // Log what backend a subsystem chose, if a hint was set to do so. Useful for debugging. -extern void SDL_LogBackend(const char *subsystem, const char *backend); +extern void SDL_DebugLogBackend(const char *subsystem, const char *backend); #endif // SDL_utils_h_ diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c index 56033912c4..b1956175c6 100644 --- a/src/audio/SDL_audio.c +++ b/src/audio/SDL_audio.c @@ -1007,7 +1007,7 @@ bool SDL_InitAudio(const char *driver_name) } if (initialized) { - SDL_LogBackend("audio", current_audio.name); + SDL_DebugLogBackend("audio", current_audio.name); } else { // specific drivers will set the error message if they fail, but otherwise we do it here. if (!tried_to_init) { diff --git a/src/camera/SDL_camera.c b/src/camera/SDL_camera.c index 7385426afc..48c6b500df 100644 --- a/src/camera/SDL_camera.c +++ b/src/camera/SDL_camera.c @@ -1525,7 +1525,7 @@ bool SDL_CameraInit(const char *driver_name) } if (initialized) { - SDL_LogBackend("camera", camera_driver.name); + SDL_DebugLogBackend("camera", camera_driver.name); } else { // specific drivers will set the error message if they fail, but otherwise we do it here. if (!tried_to_init) { diff --git a/src/gpu/SDL_gpu.c b/src/gpu/SDL_gpu.c index 3e0002f058..ad5a16da3f 100644 --- a/src/gpu/SDL_gpu.c +++ b/src/gpu/SDL_gpu.c @@ -711,7 +711,7 @@ SDL_GPUDevice *SDL_CreateGPUDeviceWithProperties(SDL_PropertiesID props) selectedBackend = SDL_GPUSelectBackend(props); if (selectedBackend != NULL) { - SDL_LogBackend("gpu", selectedBackend->name); + SDL_DebugLogBackend("gpu", selectedBackend->name); debug_mode = SDL_GetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_DEBUGMODE_BOOLEAN, true); preferLowPower = SDL_GetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_PREFERLOWPOWER_BOOLEAN, false); diff --git a/src/io/io_uring/SDL_asyncio_liburing.c b/src/io/io_uring/SDL_asyncio_liburing.c index 8b4738f9ca..4aef5f4b12 100644 --- a/src/io/io_uring/SDL_asyncio_liburing.c +++ b/src/io/io_uring/SDL_asyncio_liburing.c @@ -512,12 +512,12 @@ static void MaybeInitializeLibUring(void) { if (SDL_ShouldInit(&liburing_init)) { if (LoadLibUring()) { - SDL_LogBackend("asyncio", "liburing"); + SDL_DebugLogBackend("asyncio", "liburing"); CreateAsyncIOQueue = SDL_SYS_CreateAsyncIOQueue_liburing; QuitAsyncIO = SDL_SYS_QuitAsyncIO_liburing; AsyncIOFromFile = SDL_SYS_AsyncIOFromFile_liburing; } else { // can't use liburing? Use the "generic" threadpool implementation instead. - SDL_LogBackend("asyncio", "generic"); + SDL_DebugLogBackend("asyncio", "generic"); CreateAsyncIOQueue = SDL_SYS_CreateAsyncIOQueue_Generic; QuitAsyncIO = SDL_SYS_QuitAsyncIO_Generic; AsyncIOFromFile = SDL_SYS_AsyncIOFromFile_Generic; diff --git a/src/io/windows/SDL_asyncio_windows_ioring.c b/src/io/windows/SDL_asyncio_windows_ioring.c index fd65921015..52683c6ab2 100644 --- a/src/io/windows/SDL_asyncio_windows_ioring.c +++ b/src/io/windows/SDL_asyncio_windows_ioring.c @@ -511,12 +511,12 @@ static void MaybeInitializeWinIoRing(void) { if (SDL_ShouldInit(&ioring_init)) { if (LoadWinIoRing()) { - SDL_LogBackend("asyncio", "ioring"); + SDL_DebugLogBackend("asyncio", "ioring"); CreateAsyncIOQueue = SDL_SYS_CreateAsyncIOQueue_ioring; QuitAsyncIO = SDL_SYS_QuitAsyncIO_ioring; AsyncIOFromFile = SDL_SYS_AsyncIOFromFile_ioring; } else { // can't use ioring? Use the "generic" threadpool implementation instead. - SDL_LogBackend("asyncio", "generic"); + SDL_DebugLogBackend("asyncio", "generic"); CreateAsyncIOQueue = SDL_SYS_CreateAsyncIOQueue_Generic; QuitAsyncIO = SDL_SYS_QuitAsyncIO_Generic; AsyncIOFromFile = SDL_SYS_AsyncIOFromFile_Generic; diff --git a/src/render/SDL_render.c b/src/render/SDL_render.c index a3351ce1fc..e139d3754b 100644 --- a/src/render/SDL_render.c +++ b/src/render/SDL_render.c @@ -1064,7 +1064,7 @@ SDL_Renderer *SDL_CreateRendererWithProperties(SDL_PropertiesID props) } if (rc) { - SDL_LogBackend("render", renderer->name); + SDL_DebugLogBackend("render", renderer->name); } else { if (driver_name) { SDL_SetError("%s not available", driver_name); diff --git a/src/storage/SDL_storage.c b/src/storage/SDL_storage.c index dd3343b0e2..643a2be1c1 100644 --- a/src/storage/SDL_storage.c +++ b/src/storage/SDL_storage.c @@ -119,7 +119,7 @@ SDL_Storage *SDL_OpenTitleStorage(const char *override, SDL_PropertiesID props) } } if (storage) { - SDL_LogBackend("title_storage", titlebootstrap[i]->name); + SDL_DebugLogBackend("title_storage", titlebootstrap[i]->name); } else { if (driver_name) { SDL_SetError("%s not available", driver_name); @@ -163,7 +163,7 @@ SDL_Storage *SDL_OpenUserStorage(const char *org, const char *app, SDL_Propertie } } if (storage) { - SDL_LogBackend("user_storage", userbootstrap[i]->name); + SDL_DebugLogBackend("user_storage", userbootstrap[i]->name); } else { if (driver_name) { SDL_SetError("%s not available", driver_name); diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c index 1b29fc42d3..72b6149f44 100644 --- a/src/video/SDL_video.c +++ b/src/video/SDL_video.c @@ -680,7 +680,7 @@ bool SDL_VideoInit(const char *driver_name) } } if (video) { - SDL_LogBackend("video", bootstrap[i]->name); + SDL_DebugLogBackend("video", bootstrap[i]->name); } else { if (driver_name) { SDL_SetError("%s not available", driver_name); From 6bfc54508c55f70cccc6ac49a6e3d8e1f31acc0d Mon Sep 17 00:00:00 2001 From: Aubrey Hesselgren Date: Mon, 21 Jul 2025 16:39:32 -0700 Subject: [PATCH 084/103] Accelerometer Tolerance is now calibrated before Gyro Drift. --- test/gamepadutils.c | 106 +++++++++++++++++++++------------ test/gamepadutils.h | 18 ++++-- test/testcontroller.c | 134 ++++++++++++++++++++++++++++++++---------- 3 files changed, 187 insertions(+), 71 deletions(-) diff --git a/test/gamepadutils.c b/test/gamepadutils.c index 3b68342223..c27df6d790 100644 --- a/test/gamepadutils.c +++ b/test/gamepadutils.c @@ -1031,8 +1031,10 @@ struct GyroDisplay int estimated_sensor_rate_hz; /*hz - our estimation of the actual polling rate by observing packets received*/ float euler_displacement_angles[3]; /* pitch, yaw, roll */ Quaternion gyro_quaternion; /* Rotation since startup/reset, comprised of each gyro speed packet times sensor delta time. */ - float drift_calibration_progress_frac; /* [0..1] */ + EGyroCalibrationPhase current_calibration_phase; + float calibration_phase_progress_fraction; /* [0..1] */ float accelerometer_noise_sq; /* Distance between last noise and new noise. Used to indicate motion.*/ + float accelerometer_noise_tolerance_sq; /* Maximum amount of noise detected during the Noise Profiling Phase */ GamepadButton *reset_gyro_button; GamepadButton *calibrate_gyro_button; @@ -1049,6 +1051,10 @@ GyroDisplay *CreateGyroDisplay(SDL_Renderer *renderer) ctx->gyro_quaternion = quat_identity; ctx->reported_sensor_rate_hz = 0; ctx->next_reported_sensor_time = 0; + ctx->current_calibration_phase = GYRO_CALIBRATION_PHASE_OFF; + ctx->calibration_phase_progress_fraction = 0.0f; /* [0..1] */ + ctx->accelerometer_noise_sq = 0.0f; + ctx->accelerometer_noise_tolerance_sq = ACCELEROMETER_NOISE_THRESHOLD; /* Will be overwritten but this avoids divide by zero. */ ctx->reset_gyro_button = CreateGamepadButton(renderer, "Reset View"); ctx->calibrate_gyro_button = CreateGamepadButton(renderer, "Recalibrate Drift"); } @@ -1362,17 +1368,7 @@ static void RenderGamepadElementHighlight(GamepadDisplay *ctx, int element, cons } } -bool BHasCachedGyroDriftSolution(GyroDisplay *ctx) -{ - if (!ctx) { - return false; - } - return (ctx->gyro_drift_solution[0] != 0.0f || - ctx->gyro_drift_solution[1] != 0.0f || - ctx->gyro_drift_solution[2] != 0.0f); -} - -void SetGamepadDisplayIMUValues(GyroDisplay *ctx, float *gyro_drift_solution, float *euler_displacement_angles, Quaternion *gyro_quaternion, int reported_senor_rate_hz, int estimated_sensor_rate_hz, float drift_calibration_progress_frac, float accelerometer_noise_sq) +void SetGamepadDisplayIMUValues(GyroDisplay *ctx, float *gyro_drift_solution, float *euler_displacement_angles, Quaternion *gyro_quaternion, int reported_senor_rate_hz, int estimated_sensor_rate_hz, EGyroCalibrationPhase calibration_phase, float drift_calibration_progress_frac, float accelerometer_noise_sq, float accelerometer_noise_tolerance_sq) { if (!ctx) { return; @@ -1391,8 +1387,10 @@ void SetGamepadDisplayIMUValues(GyroDisplay *ctx, float *gyro_drift_solution, fl SDL_memcpy(ctx->gyro_drift_solution, gyro_drift_solution, sizeof(ctx->gyro_drift_solution)); SDL_memcpy(ctx->euler_displacement_angles, euler_displacement_angles, sizeof(ctx->euler_displacement_angles)); ctx->gyro_quaternion = *gyro_quaternion; - ctx->drift_calibration_progress_frac = drift_calibration_progress_frac; + ctx->current_calibration_phase = calibration_phase; + ctx->calibration_phase_progress_fraction = drift_calibration_progress_frac; ctx->accelerometer_noise_sq = accelerometer_noise_sq; + ctx->accelerometer_noise_tolerance_sq = accelerometer_noise_tolerance_sq; } extern GamepadButton *GetGyroResetButton(GyroDisplay *ctx) @@ -1713,7 +1711,7 @@ void RenderSensorTimingInfo(GyroDisplay *ctx, GamepadDisplay *gamepad_display) /* Sensor timing section */ char text[128]; const float new_line_height = gamepad_display->button_height + 2.0f; - const float text_offset_x = ctx->area.x + ctx->area.w / 4.0f + 40.0f; + const float text_offset_x = ctx->area.x + ctx->area.w / 4.0f + 35.0f; /* Anchor to bottom left of principle rect. */ float text_y_pos = ctx->area.y + ctx->area.h - new_line_height * 2; /* @@ -1759,7 +1757,7 @@ void RenderGyroDriftCalibrationButton(GyroDisplay *ctx, GamepadDisplay *gamepad_ float log_y = ctx->area.y + BUTTON_PADDING; const float new_line_height = gamepad_display->button_height + 2.0f; GamepadButton *start_calibration_button = GetGyroCalibrateButton(ctx); - bool bHasCachedDriftSolution = BHasCachedGyroDriftSolution(ctx); + /* Show the recalibration progress bar. */ float recalibrate_button_width = GetGamepadButtonLabelWidth(start_calibration_button) + 2 * BUTTON_PADDING; @@ -1769,24 +1767,46 @@ void RenderGyroDriftCalibrationButton(GyroDisplay *ctx, GamepadDisplay *gamepad_ recalibrate_button_area.w = GetGamepadButtonLabelWidth(start_calibration_button) + 2.0f * BUTTON_PADDING; recalibrate_button_area.h = gamepad_display->button_height + BUTTON_PADDING * 2.0f; - if (!bHasCachedDriftSolution) { - SDL_snprintf(label_text, sizeof(label_text), "Progress: %3.0f%% ", ctx->drift_calibration_progress_frac * 100.0f); - } else { - SDL_strlcpy(label_text, "Calibrate Drift", sizeof(label_text)); - } - - SetGamepadButtonLabel(start_calibration_button, label_text); - SetGamepadButtonArea(start_calibration_button, &recalibrate_button_area); - RenderGamepadButton(start_calibration_button); - /* Above button */ SDL_strlcpy(label_text, "Gyro Orientation:", sizeof(label_text)); SDLTest_DrawString(ctx->renderer, recalibrate_button_area.x, recalibrate_button_area.y - new_line_height, label_text); - if (!bHasCachedDriftSolution) { + /* Button label vs state */ + if (ctx->current_calibration_phase == GYRO_CALIBRATION_PHASE_OFF) { + SDL_strlcpy(label_text, "Start Gyro Calibration", sizeof(label_text)); + } else if (ctx->current_calibration_phase == GYRO_CALIBRATION_PHASE_NOISE_PROFILING) { + SDL_snprintf(label_text, sizeof(label_text), "Noise Progress: %3.0f%% ", ctx->calibration_phase_progress_fraction * 100.0f); + } else if (ctx->current_calibration_phase == GYRO_CALIBRATION_PHASE_DRIFT_PROFILING) { + SDL_snprintf(label_text, sizeof(label_text), "Drift Progress: %3.0f%% ", ctx->calibration_phase_progress_fraction * 100.0f); + } else if (ctx->current_calibration_phase == GYRO_CALIBRATION_PHASE_COMPLETE) { + SDL_strlcpy(label_text, "Recalibrate Gyro", sizeof(label_text)); + } - float flNoiseFraction = SDL_clamp(SDL_sqrtf(ctx->accelerometer_noise_sq) / ACCELEROMETER_NOISE_THRESHOLD, 0.0f, 1.0f); - bool bTooMuchNoise = (flNoiseFraction == 1.0f); + SetGamepadButtonLabel(start_calibration_button, label_text); + SetGamepadButtonArea(start_calibration_button, &recalibrate_button_area); + RenderGamepadButton(start_calibration_button); + + const float flAbsoluteMaxAccelerationG = 0.125f; + bool bExtremeNoise = ctx->accelerometer_noise_sq > (flAbsoluteMaxAccelerationG * flAbsoluteMaxAccelerationG); + /* Explicit warning message if we detect too much movement */ + if (ctx->current_calibration_phase == GYRO_CALIBRATION_PHASE_OFF) { + + if (bExtremeNoise) + { + SDL_strlcpy(label_text, "GamePad Must Be Still", sizeof(label_text)); + SDLTest_DrawString(ctx->renderer, recalibrate_button_area.x, recalibrate_button_area.y + recalibrate_button_area.h + new_line_height, label_text); + SDL_strlcpy(label_text, "Place GamePad On Table", sizeof(label_text)); + SDLTest_DrawString(ctx->renderer, recalibrate_button_area.x, recalibrate_button_area.y + recalibrate_button_area.h + new_line_height * 2, label_text); + } + } + + if (ctx->current_calibration_phase == GYRO_CALIBRATION_PHASE_NOISE_PROFILING + || ctx->current_calibration_phase == GYRO_CALIBRATION_PHASE_DRIFT_PROFILING) + { + float flAbsoluteNoiseFraction = SDL_clamp(ctx->accelerometer_noise_sq / (flAbsoluteMaxAccelerationG * flAbsoluteMaxAccelerationG), 0.0f, 1.0f); + float flAbsoluteToleranceFraction = SDL_clamp(ctx->accelerometer_noise_tolerance_sq / (flAbsoluteMaxAccelerationG * flAbsoluteMaxAccelerationG), 0.0f, 1.0f); + float flRelativeNoiseFraction = SDL_clamp(ctx->accelerometer_noise_sq / ctx->accelerometer_noise_tolerance_sq, 0.0f, 1.0f); + bool bTooMuchNoise = (flAbsoluteNoiseFraction == 1.0f); float noise_bar_height = gamepad_display->button_height; SDL_FRect noise_bar_rect; @@ -1795,21 +1815,35 @@ void RenderGyroDriftCalibrationButton(GyroDisplay *ctx, GamepadDisplay *gamepad_ noise_bar_rect.w = recalibrate_button_area.w; noise_bar_rect.h = noise_bar_height; + //SDL_strlcpy(label_text, "Place GamePad On Table", sizeof(label_text)); + SDL_snprintf(label_text, sizeof(label_text), "Noise Tolerance: %3.3fG ", SDL_sqrtf(ctx->accelerometer_noise_tolerance_sq) ); + SDLTest_DrawString(ctx->renderer, recalibrate_button_area.x, recalibrate_button_area.y + recalibrate_button_area.h + new_line_height * 2, label_text); + /* Adjust the noise bar rectangle based on the accelerometer noise value */ - float noise_bar_fill_width = flNoiseFraction * noise_bar_rect.w; /* Scale the width based on the noise value */ + float noise_bar_fill_width = flAbsoluteNoiseFraction * noise_bar_rect.w; /* Scale the width based on the noise value */ SDL_FRect noise_bar_fill_rect; noise_bar_fill_rect.x = noise_bar_rect.x + (noise_bar_rect.w - noise_bar_fill_width) * 0.5f; noise_bar_fill_rect.y = noise_bar_rect.y; noise_bar_fill_rect.w = noise_bar_fill_width; noise_bar_fill_rect.h = noise_bar_height; - /* Set the color based on the noise value */ - Uint8 red = (Uint8)(flNoiseFraction * 255.0f); - Uint8 green = (Uint8)((1.0f - flNoiseFraction) * 255.0f); + /* Set the color based on the noise value vs the tolerance */ + Uint8 red = (Uint8)(flRelativeNoiseFraction * 255.0f); + Uint8 green = (Uint8)((1.0f - flRelativeNoiseFraction) * 255.0f); SDL_SetRenderDrawColor(ctx->renderer, red, green, 0, 255); /* red when high noise, green when low noise */ SDL_RenderFillRect(ctx->renderer, &noise_bar_fill_rect); /* draw the filled rectangle */ + float tolerance_bar_fill_width = flAbsoluteToleranceFraction * noise_bar_rect.w; /* Scale the width based on the noise value */ + SDL_FRect tolerance_bar_rect; + tolerance_bar_rect.x = noise_bar_rect.x + (noise_bar_rect.w - tolerance_bar_fill_width) * 0.5f; + tolerance_bar_rect.y = noise_bar_rect.y; + tolerance_bar_rect.w = tolerance_bar_fill_width; + tolerance_bar_rect.h = noise_bar_height; + + SDL_SetRenderDrawColor(ctx->renderer, 128, 128, 0, 255); + SDL_RenderRect(ctx->renderer, &tolerance_bar_rect); /* draw the tolerance rectangle */ + SDL_SetRenderDrawColor(ctx->renderer, 100, 100, 100, 255); /* gray box */ SDL_RenderRect(ctx->renderer, &noise_bar_rect); /* draw the outline rectangle */ @@ -1828,7 +1862,7 @@ void RenderGyroDriftCalibrationButton(GyroDisplay *ctx, GamepadDisplay *gamepad_ progress_bar_rect.h = BUTTON_PADDING * 0.5f; /* Adjust the drift bar rectangle based on the drift calibration progress fraction */ - float drift_bar_fill_width = bTooMuchNoise ? 1.0f : ctx->drift_calibration_progress_frac * progress_bar_rect.w; + float drift_bar_fill_width = bTooMuchNoise ? 1.0f : ctx->calibration_phase_progress_fraction * progress_bar_rect.w; SDL_FRect progress_bar_fill; progress_bar_fill.x = progress_bar_rect.x; progress_bar_fill.y = progress_bar_rect.y; @@ -1947,14 +1981,14 @@ void RenderGyroDisplay(GyroDisplay *ctx, GamepadDisplay *gamepadElements, SDL_Ga SDL_GetRenderDrawColor(ctx->renderer, &r, &g, &b, &a); RenderSensorTimingInfo(ctx, gamepadElements); - RenderGyroDriftCalibrationButton(ctx, gamepadElements); - bool bHasCachedDriftSolution = BHasCachedGyroDriftSolution(ctx); - if (bHasCachedDriftSolution) { + /* Render Gyro calibration phases */ + if (ctx->current_calibration_phase == GYRO_CALIBRATION_PHASE_COMPLETE) { float bottom = RenderEulerReadout(ctx, gamepadElements); RenderGyroGizmo(ctx, gamepad, bottom); } + SDL_SetRenderDrawColor(ctx->renderer, r, g, b, a); } diff --git a/test/gamepadutils.h b/test/gamepadutils.h index 157bc9a0be..743d4c39ff 100644 --- a/test/gamepadutils.h +++ b/test/gamepadutils.h @@ -142,16 +142,26 @@ extern void RenderGamepadButton(GamepadButton *ctx); extern void DestroyGamepadButton(GamepadButton *ctx); /* Gyro element Display */ -/* If you want to calbirate against a known rotation (i.e. a turn table test) Increase ACCELEROMETER_NOISE_THRESHOLD to about 5, or drift correction will be constantly reset.*/ -#define ACCELEROMETER_NOISE_THRESHOLD 0.5f + +/* This is used as the initial noise tolernace threshold. It's set very close to zero to avoid divide by zero while we're evaluating the noise profile. Each controller may have a very different noise profile.*/ +#define ACCELEROMETER_NOISE_THRESHOLD 1e-6f + +/* Gyro Calibration Phases */ +typedef enum +{ + GYRO_CALIBRATION_PHASE_OFF, /* Calibration has not yet been evaluated - signal to the user to put the controller on a flat surface before beginning the calibration process */ + GYRO_CALIBRATION_PHASE_NOISE_PROFILING, /* Find the max accelerometer noise for a fixed period */ + GYRO_CALIBRATION_PHASE_DRIFT_PROFILING, /* Find the drift while the accelerometer is below the accelerometer noise tolerance */ + GYRO_CALIBRATION_PHASE_COMPLETE, /* Calibration has finished */ +} EGyroCalibrationPhase; + typedef struct Quaternion Quaternion; typedef struct GyroDisplay GyroDisplay; extern void InitCirclePoints3D(); extern GyroDisplay *CreateGyroDisplay(SDL_Renderer *renderer); extern void SetGyroDisplayArea(GyroDisplay *ctx, const SDL_FRect *area); -extern bool BHasCachedGyroDriftSolution(GyroDisplay *ctx); -extern void SetGamepadDisplayIMUValues(GyroDisplay *ctx, float *gyro_drift_solution, float *euler_displacement_angles, Quaternion *gyro_quaternion, int reported_senor_rate_hz, int estimated_sensor_rate_hz, float drift_calibration_progress_frac, float accelerometer_noise_sq); +extern void SetGamepadDisplayIMUValues(GyroDisplay *ctx, float *gyro_drift_solution, float *euler_displacement_angles, Quaternion *gyro_quaternion, int reported_senor_rate_hz, int estimated_sensor_rate_hz, EGyroCalibrationPhase calibration_phase, float drift_calibration_progress_frac, float accelerometer_noise_sq, float accelerometer_noise_tolerance_sq); extern GamepadButton *GetGyroResetButton(GyroDisplay *ctx); extern GamepadButton *GetGyroCalibrateButton(GyroDisplay *ctx); extern void RenderGyroDisplay(GyroDisplay *ctx, GamepadDisplay *gamepadElements, SDL_Gamepad *gamepad); diff --git a/test/testcontroller.c b/test/testcontroller.c index 0530d1a992..2c41e01d8f 100644 --- a/test/testcontroller.c +++ b/test/testcontroller.c @@ -156,23 +156,39 @@ typedef struct float gyro_data[3]; /* Degrees per second, i.e. 100.0f means 100 degrees per second */ float last_accel_data[3];/* Needed to detect motion (and inhibit drift calibration) */ - float accelerometer_length_squared; + float accelerometer_length_squared; /* The current length squared from last packet to this packet */ + float accelerometer_tolerance_squared; /* In phase one of calibration we calculate this as the largest accelerometer_length_squared over the time period */ + float gyro_drift_accumulator[3]; - bool is_calibrating_drift; /* Starts on, but can be turned back on by the user to restart the drift calibration. */ + + EGyroCalibrationPhase calibration_phase; /* [ GYRO_CALIBRATION_PHASE_OFF, GYRO_CALIBRATION_PHASE_NOISE_PROFILING, GYRO_CALIBRATION_PHASE_DRIFT_PROFILING,GYRO_CALIBRATION_PHASE_COMPLETE ] */ + Uint64 calibration_phase_start_time_ticks_ns; /* Set each time a calibration phase begins so that we can a real time number for evaluation of drift. Previously we would use a fixed number of packets but given that gyro polling rates vary wildly this made the duration very different. */ + int gyro_drift_sample_count; float gyro_drift_solution[3]; /* Non zero if calibration is complete. */ Quaternion integrated_rotation; /* Used to help test whether the time stamps and gyro degrees per second are set up correctly by the HID implementation */ } IMUState; -/* Reset the Drift calculation state */ -void StartGyroDriftCalibration(IMUState *imustate) +/* First stage of calibration - get the noise profile of the accelerometer */ +void BeginNoiseCalibrationPhase(IMUState *imustate) { - imustate->is_calibrating_drift = true; + imustate->accelerometer_tolerance_squared = ACCELEROMETER_NOISE_THRESHOLD; + imustate->calibration_phase = GYRO_CALIBRATION_PHASE_NOISE_PROFILING; + imustate->calibration_phase_start_time_ticks_ns = SDL_GetTicksNS(); +} + +/* Reset the Drift calculation state */ +void BeginDriftCalibrationPhase(IMUState *imustate) +{ + imustate->calibration_phase = GYRO_CALIBRATION_PHASE_DRIFT_PROFILING; + imustate->calibration_phase_start_time_ticks_ns = SDL_GetTicksNS(); imustate->gyro_drift_sample_count = 0; SDL_zeroa(imustate->gyro_drift_solution); SDL_zeroa(imustate->gyro_drift_accumulator); } + +/* Initial/full reset of state */ void ResetIMUState(IMUState *imustate) { imustate->gyro_packet_number = 0; @@ -180,10 +196,13 @@ void ResetIMUState(IMUState *imustate) imustate->starting_time_stamp_ns = SDL_GetTicksNS(); imustate->integrated_rotation = quat_identity; imustate->accelerometer_length_squared = 0.0f; + imustate->accelerometer_tolerance_squared = ACCELEROMETER_NOISE_THRESHOLD; + imustate->calibration_phase = GYRO_CALIBRATION_PHASE_OFF; + imustate->calibration_phase_start_time_ticks_ns = SDL_GetTicksNS(); imustate->integrated_rotation = quat_identity; SDL_zeroa(imustate->last_accel_data); SDL_zeroa(imustate->gyro_drift_solution); - StartGyroDriftCalibration(imustate); + SDL_zeroa(imustate->gyro_drift_accumulator); } void ResetGyroOrientation(IMUState *imustate) @@ -191,8 +210,40 @@ void ResetGyroOrientation(IMUState *imustate) imustate->integrated_rotation = quat_identity; } -/* More samples = more accurate drift correction, but also more time to calibrate.*/ -#define SDL_GAMEPAD_IMU_MIN_GYRO_DRIFT_SAMPLE_COUNT 1024 +/* More time = more accurate drift correction*/ +#define SDL_GAMEPAD_IMU_NOISE_SETTLING_PERIOD_NS (1 * SDL_NS_PER_SECOND) +#define SDL_GAMEPAD_IMU_NOISE_EVALUATION_PERIOD_NS (4 * SDL_NS_PER_SECOND) +#define SDL_GAMEPAD_IMU_NOISE_PROFILING_PHASE_DURATION_NS (SDL_GAMEPAD_IMU_NOISE_SETTLING_PERIOD_NS + SDL_GAMEPAD_IMU_NOISE_EVALUATION_PERIOD_NS) +#define SDL_GAMEPAD_IMU_CALIBRATION_PHASE_DURATION_NS (5 * SDL_NS_PER_SECOND) + +/* + * Find the maximum accelerometer noise over the duration of the GYRO_CALIBRATION_PHASE_NOISE_PROFILING phase. + */ +void CalibrationPhase_NoiseProfiling(IMUState *imustate) +{ + /* If we have really large movement (i.e. greater than a fraction of G), then we want to start noise evaluation over. The frontend will warn the user to put down the controller. */ + const float flAbsoluteMaxAccelerationG = 0.125f; + if (imustate->accelerometer_length_squared > (flAbsoluteMaxAccelerationG * flAbsoluteMaxAccelerationG) ) { + BeginNoiseCalibrationPhase(imustate); + return; + } + + Uint64 now = SDL_GetTicksNS(); + Uint64 delta_ns = now - imustate->calibration_phase_start_time_ticks_ns; + + /* Nuanced behavior - give the evaluation system some time to settle after placing the controller down before _actually_ evaluating, as the accelerometer could still be "ringing" after the user has placed it down, resulting in exaggerated tolerances */ + if (delta_ns > SDL_GAMEPAD_IMU_NOISE_SETTLING_PERIOD_NS) { + /* Get the largest noise spike in the period of evaluation */ + if (imustate->accelerometer_length_squared > imustate->accelerometer_tolerance_squared) { + imustate->accelerometer_tolerance_squared = imustate->accelerometer_length_squared; + } + } + + /* Switch phase if we go over the time limit */ + if (delta_ns >= SDL_GAMEPAD_IMU_NOISE_PROFILING_PHASE_DURATION_NS) { + BeginDriftCalibrationPhase(imustate); + } +} /* * Average drift _per packet_ as opposed to _per second_ @@ -200,36 +251,22 @@ void ResetGyroOrientation(IMUState *imustate) */ void FinalizeDriftSolution(IMUState *imustate) { - if (imustate->gyro_drift_sample_count >= SDL_GAMEPAD_IMU_MIN_GYRO_DRIFT_SAMPLE_COUNT) { + if (imustate->gyro_drift_sample_count >= 0) { imustate->gyro_drift_solution[0] = imustate->gyro_drift_accumulator[0] / (float)imustate->gyro_drift_sample_count; imustate->gyro_drift_solution[1] = imustate->gyro_drift_accumulator[1] / (float)imustate->gyro_drift_sample_count; imustate->gyro_drift_solution[2] = imustate->gyro_drift_accumulator[2] / (float)imustate->gyro_drift_sample_count; } - imustate->is_calibrating_drift = false; + imustate->calibration_phase = GYRO_CALIBRATION_PHASE_COMPLETE; ResetGyroOrientation(imustate); } -/* Sample gyro packet in order to calculate drift*/ -void SampleGyroPacketForDrift( IMUState *imustate ) +void CalibrationPhase_DriftProfiling(IMUState *imustate) { - if ( !imustate->is_calibrating_drift ) - return; - - /* Get the length squared difference of the last accelerometer data vs. the new one */ - float accelerometer_difference[3]; - accelerometer_difference[0] = imustate->accel_data[0] - imustate->last_accel_data[0]; - accelerometer_difference[1] = imustate->accel_data[1] - imustate->last_accel_data[1]; - accelerometer_difference[2] = imustate->accel_data[2] - imustate->last_accel_data[2]; - SDL_memcpy(imustate->last_accel_data, imustate->accel_data, sizeof(imustate->last_accel_data)); - - imustate->accelerometer_length_squared = accelerometer_difference[0] * accelerometer_difference[0] + accelerometer_difference[1] * accelerometer_difference[1] + accelerometer_difference[2] * accelerometer_difference[2]; - /* Ideal threshold will vary considerably depending on IMU. PS5 needs a low value (0.05f). Nintendo Switch needs a higher value (0.15f). */ - const float flAccelerometerMovementThreshold = ACCELEROMETER_NOISE_THRESHOLD; - if (imustate->accelerometer_length_squared > flAccelerometerMovementThreshold * flAccelerometerMovementThreshold) { + if (imustate->accelerometer_length_squared > imustate->accelerometer_tolerance_squared) { /* Reset the drift calibration if the accelerometer has moved significantly */ - StartGyroDriftCalibration(imustate); + BeginDriftCalibrationPhase(imustate); } else { /* Sensor is stationary enough to evaluate for drift.*/ ++imustate->gyro_drift_sample_count; @@ -238,12 +275,33 @@ void SampleGyroPacketForDrift( IMUState *imustate ) imustate->gyro_drift_accumulator[1] += imustate->gyro_data[1]; imustate->gyro_drift_accumulator[2] += imustate->gyro_data[2]; - if (imustate->gyro_drift_sample_count >= SDL_GAMEPAD_IMU_MIN_GYRO_DRIFT_SAMPLE_COUNT) { + /* Finish phase if we go over the time limit */ + Uint64 now = SDL_GetTicksNS(); + Uint64 delta_ns = now - imustate->calibration_phase_start_time_ticks_ns; + if (delta_ns >= SDL_GAMEPAD_IMU_CALIBRATION_PHASE_DURATION_NS) { FinalizeDriftSolution(imustate); } } } +/* Sample gyro packet in order to calculate drift*/ +void SampleGyroPacketForDrift(IMUState *imustate) +{ + /* Get the length squared difference of the last accelerometer data vs. the new one */ + float accelerometer_difference[3]; + accelerometer_difference[0] = imustate->accel_data[0] - imustate->last_accel_data[0]; + accelerometer_difference[1] = imustate->accel_data[1] - imustate->last_accel_data[1]; + accelerometer_difference[2] = imustate->accel_data[2] - imustate->last_accel_data[2]; + SDL_memcpy(imustate->last_accel_data, imustate->accel_data, sizeof(imustate->last_accel_data)); + imustate->accelerometer_length_squared = accelerometer_difference[0] * accelerometer_difference[0] + accelerometer_difference[1] * accelerometer_difference[1] + accelerometer_difference[2] * accelerometer_difference[2]; + + if (imustate->calibration_phase == GYRO_CALIBRATION_PHASE_NOISE_PROFILING) + CalibrationPhase_NoiseProfiling(imustate); + + if (imustate->calibration_phase == GYRO_CALIBRATION_PHASE_DRIFT_PROFILING) + CalibrationPhase_DriftProfiling(imustate); +} + void ApplyDriftSolution(float *gyro_data, const float *drift_solution) { gyro_data[0] -= drift_solution[0]; @@ -1444,7 +1502,18 @@ static void HandleGamepadSensorEvent( SDL_Event* event ) float display_euler_angles[3]; QuaternionToYXZ(controller->imu_state->integrated_rotation, &display_euler_angles[0], &display_euler_angles[1], &display_euler_angles[2]); - float drift_calibration_progress_frac = controller->imu_state->gyro_drift_sample_count / (float)SDL_GAMEPAD_IMU_MIN_GYRO_DRIFT_SAMPLE_COUNT; + /* Show how far we are through the current phase. When off, just default to zero progress */ + Uint64 now = SDL_GetTicksNS(); + float duration = 0.0f; + if (controller->imu_state->calibration_phase == GYRO_CALIBRATION_PHASE_NOISE_PROFILING) { + duration = SDL_GAMEPAD_IMU_NOISE_PROFILING_PHASE_DURATION_NS; + } else if (controller->imu_state->calibration_phase == GYRO_CALIBRATION_PHASE_DRIFT_PROFILING) { + duration = SDL_GAMEPAD_IMU_CALIBRATION_PHASE_DURATION_NS; + } + + Uint64 delta_ns = now - controller->imu_state->calibration_phase_start_time_ticks_ns; + float drift_calibration_progress_frac = duration > 0.0f ? ((float)delta_ns / (float)duration) : 0.0f; + int reported_polling_rate_hz = sensorTimeStampDelta_ns > 0 ? (int)(SDL_NS_PER_SECOND / sensorTimeStampDelta_ns) : 0; /* Send the results to the frontend */ @@ -1454,8 +1523,11 @@ static void HandleGamepadSensorEvent( SDL_Event* event ) &controller->imu_state->integrated_rotation, reported_polling_rate_hz, controller->imu_state->imu_estimated_sensor_rate, + controller->imu_state->calibration_phase, drift_calibration_progress_frac, - controller->imu_state->accelerometer_length_squared + controller->imu_state->accelerometer_length_squared, + controller->imu_state->accelerometer_tolerance_squared + ); /* Also show the gyro correction next to the gyro speed - this is useful in turntable tests as you can use a turntable to calibrate for drift, and that drift correction is functionally the same as the turn table speed (ignoring drift) */ @@ -2145,7 +2217,7 @@ SDL_AppResult SDLCALL SDL_AppEvent(void *appstate, SDL_Event *event) if (GamepadButtonContains(GetGyroResetButton(gyro_elements), event->button.x, event->button.y)) { ResetGyroOrientation(controller->imu_state); } else if (GamepadButtonContains(GetGyroCalibrateButton(gyro_elements), event->button.x, event->button.y)) { - StartGyroDriftCalibration(controller->imu_state); + BeginNoiseCalibrationPhase(controller->imu_state); } else if (GamepadButtonContains(setup_mapping_button, event->button.x, event->button.y)) { SetDisplayMode(CONTROLLER_MODE_BINDING); } From 8863e5ee677bc7a74655c181f1d26e5e5d5165a6 Mon Sep 17 00:00:00 2001 From: Aubrey Hesselgren Date: Tue, 22 Jul 2025 10:39:25 -0700 Subject: [PATCH 085/103] Made the maximum noise during accelerometer noise profiling a define, in terms of "G" Also removed a // comment which was causing the build to error. --- test/gamepadutils.c | 11 ++++------- test/gamepadutils.h | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/test/gamepadutils.c b/test/gamepadutils.c index c27df6d790..de2a6fffc9 100644 --- a/test/gamepadutils.c +++ b/test/gamepadutils.c @@ -1786,8 +1786,7 @@ void RenderGyroDriftCalibrationButton(GyroDisplay *ctx, GamepadDisplay *gamepad_ SetGamepadButtonArea(start_calibration_button, &recalibrate_button_area); RenderGamepadButton(start_calibration_button); - const float flAbsoluteMaxAccelerationG = 0.125f; - bool bExtremeNoise = ctx->accelerometer_noise_sq > (flAbsoluteMaxAccelerationG * flAbsoluteMaxAccelerationG); + bool bExtremeNoise = ctx->accelerometer_noise_sq > ACCELEROMETER_MAX_NOISE_G_SQ; /* Explicit warning message if we detect too much movement */ if (ctx->current_calibration_phase == GYRO_CALIBRATION_PHASE_OFF) { @@ -1803,10 +1802,9 @@ void RenderGyroDriftCalibrationButton(GyroDisplay *ctx, GamepadDisplay *gamepad_ if (ctx->current_calibration_phase == GYRO_CALIBRATION_PHASE_NOISE_PROFILING || ctx->current_calibration_phase == GYRO_CALIBRATION_PHASE_DRIFT_PROFILING) { - float flAbsoluteNoiseFraction = SDL_clamp(ctx->accelerometer_noise_sq / (flAbsoluteMaxAccelerationG * flAbsoluteMaxAccelerationG), 0.0f, 1.0f); - float flAbsoluteToleranceFraction = SDL_clamp(ctx->accelerometer_noise_tolerance_sq / (flAbsoluteMaxAccelerationG * flAbsoluteMaxAccelerationG), 0.0f, 1.0f); + float flAbsoluteNoiseFraction = SDL_clamp(ctx->accelerometer_noise_sq / ACCELEROMETER_MAX_NOISE_G_SQ, 0.0f, 1.0f); + float flAbsoluteToleranceFraction = SDL_clamp(ctx->accelerometer_noise_tolerance_sq / ACCELEROMETER_MAX_NOISE_G_SQ, 0.0f, 1.0f); float flRelativeNoiseFraction = SDL_clamp(ctx->accelerometer_noise_sq / ctx->accelerometer_noise_tolerance_sq, 0.0f, 1.0f); - bool bTooMuchNoise = (flAbsoluteNoiseFraction == 1.0f); float noise_bar_height = gamepad_display->button_height; SDL_FRect noise_bar_rect; @@ -1815,12 +1813,10 @@ void RenderGyroDriftCalibrationButton(GyroDisplay *ctx, GamepadDisplay *gamepad_ noise_bar_rect.w = recalibrate_button_area.w; noise_bar_rect.h = noise_bar_height; - //SDL_strlcpy(label_text, "Place GamePad On Table", sizeof(label_text)); SDL_snprintf(label_text, sizeof(label_text), "Noise Tolerance: %3.3fG ", SDL_sqrtf(ctx->accelerometer_noise_tolerance_sq) ); SDLTest_DrawString(ctx->renderer, recalibrate_button_area.x, recalibrate_button_area.y + recalibrate_button_area.h + new_line_height * 2, label_text); /* Adjust the noise bar rectangle based on the accelerometer noise value */ - float noise_bar_fill_width = flAbsoluteNoiseFraction * noise_bar_rect.w; /* Scale the width based on the noise value */ SDL_FRect noise_bar_fill_rect; noise_bar_fill_rect.x = noise_bar_rect.x + (noise_bar_rect.w - noise_bar_fill_width) * 0.5f; @@ -1848,6 +1844,7 @@ void RenderGyroDriftCalibrationButton(GyroDisplay *ctx, GamepadDisplay *gamepad_ SDL_RenderRect(ctx->renderer, &noise_bar_rect); /* draw the outline rectangle */ /* Explicit warning message if we detect too much movement */ + bool bTooMuchNoise = (flAbsoluteNoiseFraction == 1.0f); if (bTooMuchNoise) { SDL_strlcpy(label_text, "Place GamePad Down!", sizeof(label_text)); SDLTest_DrawString(ctx->renderer, recalibrate_button_area.x, noise_bar_rect.y + noise_bar_rect.h + new_line_height, label_text); diff --git a/test/gamepadutils.h b/test/gamepadutils.h index 743d4c39ff..c829c376e1 100644 --- a/test/gamepadutils.h +++ b/test/gamepadutils.h @@ -145,7 +145,7 @@ extern void DestroyGamepadButton(GamepadButton *ctx); /* This is used as the initial noise tolernace threshold. It's set very close to zero to avoid divide by zero while we're evaluating the noise profile. Each controller may have a very different noise profile.*/ #define ACCELEROMETER_NOISE_THRESHOLD 1e-6f - +#define ACCELEROMETER_MAX_NOISE_G_SQ ( 0.125f * 0.125f ) /* Gyro Calibration Phases */ typedef enum { From 34616d1b009e666cbb6fb17af11dc967ec0f8957 Mon Sep 17 00:00:00 2001 From: Aubrey Hesselgren Date: Tue, 22 Jul 2025 11:01:57 -0700 Subject: [PATCH 086/103] A little more tidying. Better notes around how the absolute maximum threshold was arrived at. --- test/gamepadutils.c | 25 +++++++++++-------------- test/gamepadutils.h | 7 +++++-- test/testcontroller.c | 18 ++++++++---------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/test/gamepadutils.c b/test/gamepadutils.c index de2a6fffc9..21206b071f 100644 --- a/test/gamepadutils.c +++ b/test/gamepadutils.c @@ -236,7 +236,7 @@ void DrawGyroDebugAxes(SDL_Renderer *renderer, const Quaternion *orientation, co SDL_RenderLine(renderer, origin_screen.x, origin_screen.y, up_screen.x, up_screen.y); SDL_SetRenderDrawColor(renderer, GYRO_COLOR_BLUE); SDL_RenderLine(renderer, origin_screen.x, origin_screen.y, back_screen.x, back_screen.y); - + /* Restore current color */ SDL_SetRenderDrawColor(renderer, r, g, b, a); } @@ -1053,7 +1053,7 @@ GyroDisplay *CreateGyroDisplay(SDL_Renderer *renderer) ctx->next_reported_sensor_time = 0; ctx->current_calibration_phase = GYRO_CALIBRATION_PHASE_OFF; ctx->calibration_phase_progress_fraction = 0.0f; /* [0..1] */ - ctx->accelerometer_noise_sq = 0.0f; + ctx->accelerometer_noise_sq = 0.0f; ctx->accelerometer_noise_tolerance_sq = ACCELEROMETER_NOISE_THRESHOLD; /* Will be overwritten but this avoids divide by zero. */ ctx->reset_gyro_button = CreateGamepadButton(renderer, "Reset View"); ctx->calibrate_gyro_button = CreateGamepadButton(renderer, "Recalibrate Drift"); @@ -1678,7 +1678,6 @@ void RenderGamepadDisplay(GamepadDisplay *ctx, SDL_Gamepad *gamepad) SDLTest_DrawString(ctx->renderer, x + center - SDL_strlen(text) * FONT_CHARACTER_SIZE, y, text); SDL_snprintf(text, sizeof(text), "[%.2f,%.2f,%.2f]%s/s", ctx->gyro_data[0] * RAD_TO_DEG, ctx->gyro_data[1] * RAD_TO_DEG, ctx->gyro_data[2] * RAD_TO_DEG, DEGREE_UTF8); SDLTest_DrawString(ctx->renderer, x + center + 2.0f, y, text); - /* Display the testcontroller tool's evaluation of drift. This is also useful to get an average rate of turn in calibrated turntable tests. */ if (ctx->gyro_drift_correction_data[0] != 0.0f && ctx->gyro_drift_correction_data[2] != 0.0f && ctx->gyro_drift_correction_data[2] != 0.0f ) @@ -1758,7 +1757,6 @@ void RenderGyroDriftCalibrationButton(GyroDisplay *ctx, GamepadDisplay *gamepad_ const float new_line_height = gamepad_display->button_height + 2.0f; GamepadButton *start_calibration_button = GetGyroCalibrateButton(ctx); - /* Show the recalibration progress bar. */ float recalibrate_button_width = GetGamepadButtonLabelWidth(start_calibration_button) + 2 * BUTTON_PADDING; SDL_FRect recalibrate_button_area; @@ -1784,14 +1782,12 @@ void RenderGyroDriftCalibrationButton(GyroDisplay *ctx, GamepadDisplay *gamepad_ SetGamepadButtonLabel(start_calibration_button, label_text); SetGamepadButtonArea(start_calibration_button, &recalibrate_button_area); - RenderGamepadButton(start_calibration_button); + RenderGamepadButton(start_calibration_button); bool bExtremeNoise = ctx->accelerometer_noise_sq > ACCELEROMETER_MAX_NOISE_G_SQ; /* Explicit warning message if we detect too much movement */ if (ctx->current_calibration_phase == GYRO_CALIBRATION_PHASE_OFF) { - - if (bExtremeNoise) - { + if (bExtremeNoise) { SDL_strlcpy(label_text, "GamePad Must Be Still", sizeof(label_text)); SDLTest_DrawString(ctx->renderer, recalibrate_button_area.x, recalibrate_button_area.y + recalibrate_button_area.h + new_line_height, label_text); SDL_strlcpy(label_text, "Place GamePad On Table", sizeof(label_text)); @@ -1799,12 +1795,14 @@ void RenderGyroDriftCalibrationButton(GyroDisplay *ctx, GamepadDisplay *gamepad_ } } - if (ctx->current_calibration_phase == GYRO_CALIBRATION_PHASE_NOISE_PROFILING - || ctx->current_calibration_phase == GYRO_CALIBRATION_PHASE_DRIFT_PROFILING) + if (ctx->current_calibration_phase == GYRO_CALIBRATION_PHASE_NOISE_PROFILING || + ctx->current_calibration_phase == GYRO_CALIBRATION_PHASE_DRIFT_PROFILING) { float flAbsoluteNoiseFraction = SDL_clamp(ctx->accelerometer_noise_sq / ACCELEROMETER_MAX_NOISE_G_SQ, 0.0f, 1.0f); float flAbsoluteToleranceFraction = SDL_clamp(ctx->accelerometer_noise_tolerance_sq / ACCELEROMETER_MAX_NOISE_G_SQ, 0.0f, 1.0f); - float flRelativeNoiseFraction = SDL_clamp(ctx->accelerometer_noise_sq / ctx->accelerometer_noise_tolerance_sq, 0.0f, 1.0f); + + float flMaxNoiseForThisPhase = ctx->current_calibration_phase == GYRO_CALIBRATION_PHASE_NOISE_PROFILING ? ACCELEROMETER_MAX_NOISE_G_SQ : ctx->accelerometer_noise_tolerance_sq; + float flRelativeNoiseFraction = SDL_clamp(ctx->accelerometer_noise_sq / flMaxNoiseForThisPhase, 0.0f, 1.0f); float noise_bar_height = gamepad_display->button_height; SDL_FRect noise_bar_rect; @@ -1813,7 +1811,7 @@ void RenderGyroDriftCalibrationButton(GyroDisplay *ctx, GamepadDisplay *gamepad_ noise_bar_rect.w = recalibrate_button_area.w; noise_bar_rect.h = noise_bar_height; - SDL_snprintf(label_text, sizeof(label_text), "Noise Tolerance: %3.3fG ", SDL_sqrtf(ctx->accelerometer_noise_tolerance_sq) ); + SDL_snprintf(label_text, sizeof(label_text), "Accelerometer Noise Tolerance: %3.3fG ", SDL_sqrtf(ctx->accelerometer_noise_tolerance_sq) ); SDLTest_DrawString(ctx->renderer, recalibrate_button_area.x, recalibrate_button_area.y + recalibrate_button_area.h + new_line_height * 2, label_text); /* Adjust the noise bar rectangle based on the accelerometer noise value */ @@ -1837,7 +1835,7 @@ void RenderGyroDriftCalibrationButton(GyroDisplay *ctx, GamepadDisplay *gamepad_ tolerance_bar_rect.w = tolerance_bar_fill_width; tolerance_bar_rect.h = noise_bar_height; - SDL_SetRenderDrawColor(ctx->renderer, 128, 128, 0, 255); + SDL_SetRenderDrawColor(ctx->renderer, 128, 128, 0, 255); SDL_RenderRect(ctx->renderer, &tolerance_bar_rect); /* draw the tolerance rectangle */ SDL_SetRenderDrawColor(ctx->renderer, 100, 100, 100, 255); /* gray box */ @@ -1985,7 +1983,6 @@ void RenderGyroDisplay(GyroDisplay *ctx, GamepadDisplay *gamepadElements, SDL_Ga float bottom = RenderEulerReadout(ctx, gamepadElements); RenderGyroGizmo(ctx, gamepad, bottom); } - SDL_SetRenderDrawColor(ctx->renderer, r, g, b, a); } diff --git a/test/gamepadutils.h b/test/gamepadutils.h index c829c376e1..19eeefcff5 100644 --- a/test/gamepadutils.h +++ b/test/gamepadutils.h @@ -143,9 +143,12 @@ extern void DestroyGamepadButton(GamepadButton *ctx); /* Gyro element Display */ -/* This is used as the initial noise tolernace threshold. It's set very close to zero to avoid divide by zero while we're evaluating the noise profile. Each controller may have a very different noise profile.*/ +/* This is used as the initial noise tolerance threshold. It's set very close to zero to avoid divide by zero while we're evaluating the noise profile. Each controller may have a very different noise profile.*/ #define ACCELEROMETER_NOISE_THRESHOLD 1e-6f -#define ACCELEROMETER_MAX_NOISE_G_SQ ( 0.125f * 0.125f ) +/* The value below is based on observation of a Dualshock controller. Of all gamepads observed, the Dualshock (PS4) tends to have one of the noisiest accelerometers. Increase this threshold if a controller is failing to pass the noise profiling stage while stationary on a table. */ +#define ACCELEROMETER_MAX_NOISE_G 0.075f +#define ACCELEROMETER_MAX_NOISE_G_SQ (ACCELEROMETER_MAX_NOISE_G * ACCELEROMETER_MAX_NOISE_G) + /* Gyro Calibration Phases */ typedef enum { diff --git a/test/testcontroller.c b/test/testcontroller.c index 2c41e01d8f..6b0fdea27f 100644 --- a/test/testcontroller.c +++ b/test/testcontroller.c @@ -211,7 +211,7 @@ void ResetGyroOrientation(IMUState *imustate) } /* More time = more accurate drift correction*/ -#define SDL_GAMEPAD_IMU_NOISE_SETTLING_PERIOD_NS (1 * SDL_NS_PER_SECOND) +#define SDL_GAMEPAD_IMU_NOISE_SETTLING_PERIOD_NS ( SDL_NS_PER_SECOND / 2) #define SDL_GAMEPAD_IMU_NOISE_EVALUATION_PERIOD_NS (4 * SDL_NS_PER_SECOND) #define SDL_GAMEPAD_IMU_NOISE_PROFILING_PHASE_DURATION_NS (SDL_GAMEPAD_IMU_NOISE_SETTLING_PERIOD_NS + SDL_GAMEPAD_IMU_NOISE_EVALUATION_PERIOD_NS) #define SDL_GAMEPAD_IMU_CALIBRATION_PHASE_DURATION_NS (5 * SDL_NS_PER_SECOND) @@ -222,12 +222,11 @@ void ResetGyroOrientation(IMUState *imustate) void CalibrationPhase_NoiseProfiling(IMUState *imustate) { /* If we have really large movement (i.e. greater than a fraction of G), then we want to start noise evaluation over. The frontend will warn the user to put down the controller. */ - const float flAbsoluteMaxAccelerationG = 0.125f; - if (imustate->accelerometer_length_squared > (flAbsoluteMaxAccelerationG * flAbsoluteMaxAccelerationG) ) { + if (imustate->accelerometer_length_squared > ACCELEROMETER_MAX_NOISE_G_SQ) { BeginNoiseCalibrationPhase(imustate); return; } - + Uint64 now = SDL_GetTicksNS(); Uint64 delta_ns = now - imustate->calibration_phase_start_time_ticks_ns; @@ -1433,7 +1432,7 @@ static void HandleGamepadGyroEvent(SDL_Event *event) /* Two strategies for evaluating polling rate - one based on a fixed packet count, and one using a fixed time window. * Smaller values in either will give you a more responsive polling rate estimate, but this may fluctuate more. * Larger values in either will give you a more stable average but they will require more time to evaluate. - * Generally, wired connections tend to give much more stable + * Generally, wired connections tend to give much more stable */ /* #define SDL_USE_FIXED_PACKET_COUNT_FOR_ESTIMATION */ #define SDL_GAMEPAD_IMU_MIN_POLLING_RATE_ESTIMATION_COUNT 2048 @@ -1479,7 +1478,7 @@ static void UpdateGamepadOrientation( Uint64 delta_time_ns ) static void HandleGamepadSensorEvent( SDL_Event* event ) { if (!controller) - return; + return; if (controller->id != event->gsensor.which) return; @@ -1504,7 +1503,7 @@ static void HandleGamepadSensorEvent( SDL_Event* event ) /* Show how far we are through the current phase. When off, just default to zero progress */ Uint64 now = SDL_GetTicksNS(); - float duration = 0.0f; + Uint64 duration = 0; if (controller->imu_state->calibration_phase == GYRO_CALIBRATION_PHASE_NOISE_PROFILING) { duration = SDL_GAMEPAD_IMU_NOISE_PROFILING_PHASE_DURATION_NS; } else if (controller->imu_state->calibration_phase == GYRO_CALIBRATION_PHASE_DRIFT_PROFILING) { @@ -1512,7 +1511,7 @@ static void HandleGamepadSensorEvent( SDL_Event* event ) } Uint64 delta_ns = now - controller->imu_state->calibration_phase_start_time_ticks_ns; - float drift_calibration_progress_frac = duration > 0.0f ? ((float)delta_ns / (float)duration) : 0.0f; + float drift_calibration_progress_fraction = duration > 0.0f ? ((float)delta_ns / (float)duration) : 0.0f; int reported_polling_rate_hz = sensorTimeStampDelta_ns > 0 ? (int)(SDL_NS_PER_SECOND / sensorTimeStampDelta_ns) : 0; @@ -1524,10 +1523,9 @@ static void HandleGamepadSensorEvent( SDL_Event* event ) reported_polling_rate_hz, controller->imu_state->imu_estimated_sensor_rate, controller->imu_state->calibration_phase, - drift_calibration_progress_frac, + drift_calibration_progress_fraction, controller->imu_state->accelerometer_length_squared, controller->imu_state->accelerometer_tolerance_squared - ); /* Also show the gyro correction next to the gyro speed - this is useful in turntable tests as you can use a turntable to calibrate for drift, and that drift correction is functionally the same as the turn table speed (ignoring drift) */ From acb3b0b4be8c6bd2dff1aceaca69598557eea099 Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Tue, 22 Jul 2025 12:32:46 -0400 Subject: [PATCH 087/103] win32: Implement keymap caching Keymap construction is an expensive process, so keymaps are cached to facilitate fast switching, as they are static after initial construction, and do not need to be rebuilt every time. --- src/video/windows/SDL_windowskeyboard.c | 113 +++++++++++++++++++----- 1 file changed, 92 insertions(+), 21 deletions(-) diff --git a/src/video/windows/SDL_windowskeyboard.c b/src/video/windows/SDL_windowskeyboard.c index 560437598f..c8342c3cef 100644 --- a/src/video/windows/SDL_windowskeyboard.c +++ b/src/video/windows/SDL_windowskeyboard.c @@ -54,36 +54,59 @@ static void IME_SetTextInputArea(SDL_VideoData *videodata, HWND hwnd, const SDL_ #define MAPVK_VSC_TO_VK 1 #endif -// Alphabetic scancodes for PC keyboards -void WIN_InitKeyboard(SDL_VideoDevice *_this) +/* Building keymaps is expensive, so keep a reasonably-sized LRU cache to + * enable fast switching between commonly used ones. + */ +static struct WIN_KeymapCache { -#ifndef SDL_DISABLE_WINDOWS_IME - SDL_VideoData *data = _this->internal; + HKL keyboard_layout; + SDL_Keymap *keymap; +} keymap_cache[4]; - data->ime_candlistindexbase = 1; - data->ime_composition_length = 32 * sizeof(WCHAR); - data->ime_composition = (WCHAR *)SDL_calloc(data->ime_composition_length, sizeof(WCHAR)); -#endif // !SDL_DISABLE_WINDOWS_IME +static int keymap_cache_size; - WIN_UpdateKeymap(false); +static SDL_Keymap *WIN_GetCachedKeymap(HKL layout) +{ + SDL_Keymap *keymap = NULL; + for (int i = 0; i < keymap_cache_size; ++i) { + if (keymap_cache[i].keyboard_layout == layout) { + keymap = keymap_cache[i].keymap; - SDL_SetScancodeName(SDL_SCANCODE_APPLICATION, "Menu"); - SDL_SetScancodeName(SDL_SCANCODE_LGUI, "Left Windows"); - SDL_SetScancodeName(SDL_SCANCODE_RGUI, "Right Windows"); - - // Are system caps/num/scroll lock active? Set our state to match. - SDL_ToggleModState(SDL_KMOD_CAPS, (GetKeyState(VK_CAPITAL) & 0x0001) ? true : false); - SDL_ToggleModState(SDL_KMOD_NUM, (GetKeyState(VK_NUMLOCK) & 0x0001) ? true : false); - SDL_ToggleModState(SDL_KMOD_SCROLL, (GetKeyState(VK_SCROLL) & 0x0001) ? true : false); + // Move the map to the front of the list. + if (i) { + SDL_memmove(keymap_cache + 1, keymap_cache, sizeof(struct WIN_KeymapCache) * i); + keymap_cache[0].keyboard_layout = layout; + keymap_cache[0].keymap = keymap; + } + break; + } + } + return keymap; } -void WIN_UpdateKeymap(bool send_event) +static void WIN_CacheKeymap(HKL layout, SDL_Keymap *keymap) +{ + // If the cache is full, evict the last keymap. + if (keymap_cache_size == SDL_arraysize(keymap_cache)) { + SDL_DestroyKeymap(keymap_cache[--keymap_cache_size].keymap); + } + + // Move all elements down by one. + if (keymap_cache_size) { + SDL_memmove(keymap_cache + 1, keymap_cache, sizeof(struct WIN_KeymapCache) * keymap_cache_size); + } + + keymap_cache[0].keyboard_layout = layout; + keymap_cache[0].keymap = keymap; + ++keymap_cache_size; +} + +static SDL_Keymap *WIN_BuildKeymap() { SDL_Scancode scancode; - SDL_Keymap *keymap; BYTE keyboardState[256] = { 0 }; WCHAR buffer[16]; - SDL_Keymod mods[] = { + const SDL_Keymod mods[] = { SDL_KMOD_NONE, SDL_KMOD_SHIFT, SDL_KMOD_CAPS, @@ -96,7 +119,10 @@ void WIN_UpdateKeymap(bool send_event) WIN_ResetDeadKeys(); - keymap = SDL_CreateKeymap(true); + SDL_Keymap *keymap = SDL_CreateKeymap(false); + if (!keymap) { + return NULL; + } for (int m = 0; m < SDL_arraysize(mods); ++m) { for (int i = 0; i < SDL_arraysize(windows_scancode_table); i++) { @@ -160,9 +186,47 @@ void WIN_UpdateKeymap(bool send_event) } } + return keymap; +} + +void WIN_UpdateKeymap(bool send_event) +{ + HKL layout = GetKeyboardLayout(0); + SDL_Keymap *keymap = WIN_GetCachedKeymap(layout); + if (!keymap) { + keymap = WIN_BuildKeymap(); + if (keymap) { + WIN_CacheKeymap(layout, keymap); + } + } + SDL_SetKeymap(keymap, send_event); } +// Alphabetic scancodes for PC keyboards +void WIN_InitKeyboard(SDL_VideoDevice *_this) +{ +#ifndef SDL_DISABLE_WINDOWS_IME + SDL_VideoData *data = _this->internal; + + data->ime_candlistindexbase = 1; + data->ime_composition_length = 32 * sizeof(WCHAR); + data->ime_composition = (WCHAR *)SDL_calloc(data->ime_composition_length, sizeof(WCHAR)); +#endif // !SDL_DISABLE_WINDOWS_IME + + // Build and bind the current keymap. + WIN_UpdateKeymap(false); + + SDL_SetScancodeName(SDL_SCANCODE_APPLICATION, "Menu"); + SDL_SetScancodeName(SDL_SCANCODE_LGUI, "Left Windows"); + SDL_SetScancodeName(SDL_SCANCODE_RGUI, "Right Windows"); + + // Are system caps/num/scroll lock active? Set our state to match. + SDL_ToggleModState(SDL_KMOD_CAPS, (GetKeyState(VK_CAPITAL) & 0x0001) ? true : false); + SDL_ToggleModState(SDL_KMOD_NUM, (GetKeyState(VK_NUMLOCK) & 0x0001) ? true : false); + SDL_ToggleModState(SDL_KMOD_SCROLL, (GetKeyState(VK_SCROLL) & 0x0001) ? true : false); +} + void WIN_QuitKeyboard(SDL_VideoDevice *_this) { #ifndef SDL_DISABLE_WINDOWS_IME @@ -175,6 +239,13 @@ void WIN_QuitKeyboard(SDL_VideoDevice *_this) data->ime_composition = NULL; } #endif // !SDL_DISABLE_WINDOWS_IME + + SDL_SetKeymap(NULL, false); + for (int i = 0; i < keymap_cache_size; ++i) { + SDL_DestroyKeymap(keymap_cache[i].keymap); + } + SDL_memset(keymap_cache, 0, sizeof(keymap_cache)); + keymap_cache_size = 0; } void WIN_ResetDeadKeys(void) From f5a0222a8eed4264f080f7095506fbb3dd42fca4 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Wed, 23 Jul 2025 22:38:28 -0400 Subject: [PATCH 088/103] aaudio: Try to select a more-useful microphone for recording. Fixes #13402. --- src/audio/aaudio/SDL_aaudio.c | 15 +++++++++++++-- src/audio/aaudio/SDL_aaudiofuncs.h | 7 ++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/audio/aaudio/SDL_aaudio.c b/src/audio/aaudio/SDL_aaudio.c index 5436be070a..5026e6a5a1 100644 --- a/src/audio/aaudio/SDL_aaudio.c +++ b/src/audio/aaudio/SDL_aaudio.c @@ -65,11 +65,16 @@ static bool AAUDIO_LoadFunctions(AAUDIO_Data *data) { #define SDL_PROC(ret, func, params) \ do { \ - data->func = (ret (*) params)SDL_LoadFunction(data->handle, #func); \ - if (!data->func) { \ + data->func = (ret (*) params)SDL_LoadFunction(data->handle, #func); \ + if (!data->func) { \ return SDL_SetError("Couldn't load AAUDIO function %s: %s", #func, SDL_GetError()); \ } \ } while (0); + +#define SDL_PROC_OPTIONAL(ret, func, params) \ + do { \ + data->func = (ret (*) params)SDL_LoadFunction(data->handle, #func); /* if it fails, okay. */ \ + } while (0); #include "SDL_aaudiofuncs.h" return true; } @@ -327,6 +332,12 @@ static bool BuildAAudioStream(SDL_AudioDevice *device) SDL_Log("Low latency audio disabled"); } + if (recording && ctx.AAudioStreamBuilder_setInputPreset) { // optional API: requires Android 28 + // try to use a microphone that is for recording external audio. Otherwise Android might choose the mic used for talking + // on the telephone when held to the user's ear, which is often not useful at any distance from the device. + ctx.AAudioStreamBuilder_setInputPreset(builder, AAUDIO_INPUT_PRESET_CAMCORDER); + } + LOGI("AAudio Try to open %u hz %s %u channels samples %u", device->spec.freq, SDL_GetAudioFormatName(device->spec.format), device->spec.channels, device->sample_frames); diff --git a/src/audio/aaudio/SDL_aaudiofuncs.h b/src/audio/aaudio/SDL_aaudiofuncs.h index 1d9f71044c..2ddacf0ea0 100644 --- a/src/audio/aaudio/SDL_aaudiofuncs.h +++ b/src/audio/aaudio/SDL_aaudiofuncs.h @@ -19,6 +19,10 @@ 3. This notice may not be removed or altered from any source distribution. */ +#ifndef SDL_PROC_OPTIONAL(ret, func, params) +#define SDL_PROC_OPTIONAL(ret, func, params) SDL_PROC(ret, func, params) +#endif + #define SDL_PROC_UNUSED(ret, func, params) SDL_PROC(const char *, AAudio_convertResultToText, (aaudio_result_t returnCode)) @@ -35,7 +39,7 @@ SDL_PROC(void, AAudioStreamBuilder_setBufferCapacityInFrames, (AAudioStreamBuild SDL_PROC(void, AAudioStreamBuilder_setPerformanceMode, (AAudioStreamBuilder * builder, aaudio_performance_mode_t mode)) SDL_PROC_UNUSED(void, AAudioStreamBuilder_setUsage, (AAudioStreamBuilder * builder, aaudio_usage_t usage)) // API 28 SDL_PROC_UNUSED(void, AAudioStreamBuilder_setContentType, (AAudioStreamBuilder * builder, aaudio_content_type_t contentType)) // API 28 -SDL_PROC_UNUSED(void, AAudioStreamBuilder_setInputPreset, (AAudioStreamBuilder * builder, aaudio_input_preset_t inputPreset)) // API 28 +SDL_PROC_OPTIONAL(void, AAudioStreamBuilder_setInputPreset, (AAudioStreamBuilder * builder, aaudio_input_preset_t inputPreset)) // API 28 SDL_PROC_UNUSED(void, AAudioStreamBuilder_setAllowedCapturePolicy, (AAudioStreamBuilder * builder, aaudio_allowed_capture_policy_t capturePolicy)) // API 29 SDL_PROC_UNUSED(void, AAudioStreamBuilder_setSessionId, (AAudioStreamBuilder * builder, aaudio_session_id_t sessionId)) // API 28 SDL_PROC_UNUSED(void, AAudioStreamBuilder_setPrivacySensitive, (AAudioStreamBuilder * builder, bool privacySensitive)) // API 30 @@ -80,3 +84,4 @@ SDL_PROC_UNUSED(bool, AAudioStream_isPrivacySensitive, (AAudioStream * stream)) #undef SDL_PROC #undef SDL_PROC_UNUSED +#undef SDL_PROC_OPTIONAL From 39e9ac6d1fae985359fbb740cc0a15f806646879 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Wed, 23 Jul 2025 23:53:46 -0400 Subject: [PATCH 089/103] ci: Patched to compile on Android. --- src/audio/aaudio/SDL_aaudio.c | 2 +- src/audio/aaudio/SDL_aaudiofuncs.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/audio/aaudio/SDL_aaudio.c b/src/audio/aaudio/SDL_aaudio.c index 5026e6a5a1..b1be69182a 100644 --- a/src/audio/aaudio/SDL_aaudio.c +++ b/src/audio/aaudio/SDL_aaudio.c @@ -66,7 +66,7 @@ static bool AAUDIO_LoadFunctions(AAUDIO_Data *data) #define SDL_PROC(ret, func, params) \ do { \ data->func = (ret (*) params)SDL_LoadFunction(data->handle, #func); \ - if (!data->func) { \ + if (!data->func) { \ return SDL_SetError("Couldn't load AAUDIO function %s: %s", #func, SDL_GetError()); \ } \ } while (0); diff --git a/src/audio/aaudio/SDL_aaudiofuncs.h b/src/audio/aaudio/SDL_aaudiofuncs.h index 2ddacf0ea0..af4f44bd9f 100644 --- a/src/audio/aaudio/SDL_aaudiofuncs.h +++ b/src/audio/aaudio/SDL_aaudiofuncs.h @@ -19,7 +19,7 @@ 3. This notice may not be removed or altered from any source distribution. */ -#ifndef SDL_PROC_OPTIONAL(ret, func, params) +#ifndef SDL_PROC_OPTIONAL #define SDL_PROC_OPTIONAL(ret, func, params) SDL_PROC(ret, func, params) #endif From 6b9dfcc2fd28236efbcd2c68fb28286db673e0c1 Mon Sep 17 00:00:00 2001 From: SDL Wiki Bot Date: Thu, 24 Jul 2025 15:52:42 +0000 Subject: [PATCH 090/103] Sync SDL3 wiki -> header [ci skip] --- include/SDL3/SDL_properties.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/SDL3/SDL_properties.h b/include/SDL3/SDL_properties.h index 1f47d5f4ad..0054e069e9 100644 --- a/include/SDL3/SDL_properties.h +++ b/include/SDL3/SDL_properties.h @@ -119,7 +119,9 @@ extern SDL_DECLSPEC SDL_PropertiesID SDLCALL SDL_CreateProperties(void); * \returns true on success or false on failure; call SDL_GetError() for more * information. * - * \threadsafety It is safe to call this function from any thread. + * \threadsafety It is safe to call this function from any thread. This + * function acquires simultaneous mutex locks on both the source + * and destination property sets. * * \since This function is available since SDL 3.2.0. */ From 3fdd15adaa8394ab409e1c80ee11f565bb34399b Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Thu, 24 Jul 2025 10:33:37 -0700 Subject: [PATCH 091/103] Fixed double-release of GameInput at shutdown --- src/video/windows/SDL_windowsgameinput.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/video/windows/SDL_windowsgameinput.cpp b/src/video/windows/SDL_windowsgameinput.cpp index 265cb6dd25..78c19a57f9 100644 --- a/src/video/windows/SDL_windowsgameinput.cpp +++ b/src/video/windows/SDL_windowsgameinput.cpp @@ -580,11 +580,6 @@ void WIN_QuitGameInput(SDL_VideoDevice *_this) GAMEINPUT_InternalRemoveByIndex(data, 0); } - data->pGameInput->Release(); - data->pGameInput = NULL; - } - - if (data->pGameInput) { SDL_QuitGameInput(); data->pGameInput = NULL; } From 6babade7586416680b460a4a2b3ae03a6afcb9df Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Thu, 24 Jul 2025 10:34:44 -0700 Subject: [PATCH 092/103] Fixed double SDL_EVENT_GAMEPAD_ADDED for controllers with automatic gamepad mappings --- src/joystick/SDL_gamepad.c | 2 +- src/joystick/SDL_joystick.c | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index 36ea97e004..a7b885ad81 100644 --- a/src/joystick/SDL_gamepad.c +++ b/src/joystick/SDL_gamepad.c @@ -312,7 +312,7 @@ void SDL_PrivateGamepadAdded(SDL_JoystickID instance_id) { SDL_Event event; - if (!SDL_gamepads_initialized) { + if (!SDL_gamepads_initialized || SDL_IsJoystickBeingAdded()) { return; } diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c index 70c952b78a..0aa742da85 100644 --- a/src/joystick/SDL_joystick.c +++ b/src/joystick/SDL_joystick.c @@ -2316,6 +2316,7 @@ void SDL_PrivateJoystickAdded(SDL_JoystickID instance_id) SDL_JoystickDriver *driver; int device_index; int player_index = -1; + bool is_gamepad; SDL_AssertJoysticksLocked(); @@ -2350,9 +2351,12 @@ void SDL_PrivateJoystickAdded(SDL_JoystickID instance_id) } } + // This might create an automatic gamepad mapping, so wait to send the event + is_gamepad = SDL_IsGamepad(instance_id); + SDL_joystick_being_added = false; - if (SDL_IsGamepad(instance_id)) { + if (is_gamepad) { SDL_PrivateGamepadAdded(instance_id); } } From 66dad9c21f62db17bfde2a8b6f3debb18f6e1e79 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Thu, 24 Jul 2025 10:35:38 -0700 Subject: [PATCH 093/103] Added Steam Virtual Gamepad support to the GameInput driver --- src/joystick/gdk/SDL_gameinputjoystick.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/joystick/gdk/SDL_gameinputjoystick.cpp b/src/joystick/gdk/SDL_gameinputjoystick.cpp index 3405816990..bf1d5ef4b6 100644 --- a/src/joystick/gdk/SDL_gameinputjoystick.cpp +++ b/src/joystick/gdk/SDL_gameinputjoystick.cpp @@ -52,6 +52,7 @@ typedef struct GAMEINPUT_InternalDevice SDL_GUID guid; // generated by SDL SDL_JoystickID device_instance; // generated by SDL const GameInputDeviceInfo *info; + int steam_virtual_gamepad_slot; bool isAdded; bool isDeleteRequested; } GAMEINPUT_InternalDevice; @@ -83,6 +84,16 @@ static bool GAMEINPUT_InternalIsGamepad(const GameInputDeviceInfo *info) return false; } +static int GetSteamVirtualGamepadSlot(const char *device_path) +{ + int slot = -1; + + // The format for the raw input device path is documented here: + // https://partner.steamgames.com/doc/features/steam_controller/steam_input_gamepad_emulation_bestpractices + (void)SDL_sscanf(device_path, "\\\\.\\pipe\\HID#VID_045E&PID_028E&IG_00#%*X&%*X&%*X#%d#%*u", &slot); + return slot; +} + static bool GAMEINPUT_InternalAddOrFind(IGameInputDevice *pDevice) { GAMEINPUT_InternalDevice **devicelist = NULL; @@ -162,6 +173,7 @@ static bool GAMEINPUT_InternalAddOrFind(IGameInputDevice *pDevice) elem->guid = SDL_CreateJoystickGUID(bus, vendor, product, version, manufacturer_string, product_string, 'g', 0); elem->device_instance = SDL_GetNextObjectID(); elem->info = info; + elem->steam_virtual_gamepad_slot = GetSteamVirtualGamepadSlot(info->pnpPath); g_GameInputList.devices = devicelist; g_GameInputList.devices[g_GameInputList.count++] = elem; @@ -349,7 +361,7 @@ static const char *GAMEINPUT_JoystickGetDevicePath(int device_index) static int GAMEINPUT_JoystickGetDeviceSteamVirtualGamepadSlot(int device_index) { - return -1; + return GAMEINPUT_InternalFindByIndex(device_index)->steam_virtual_gamepad_slot; } static int GAMEINPUT_JoystickGetDevicePlayerIndex(int device_index) From 0ee0fe157218476ff39d2213d726d260b6468f7d Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Thu, 24 Jul 2025 10:42:28 -0700 Subject: [PATCH 094/103] Fixed building with GameInput v1.0 --- src/joystick/gdk/SDL_gameinputjoystick.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/joystick/gdk/SDL_gameinputjoystick.cpp b/src/joystick/gdk/SDL_gameinputjoystick.cpp index bf1d5ef4b6..96f0daea59 100644 --- a/src/joystick/gdk/SDL_gameinputjoystick.cpp +++ b/src/joystick/gdk/SDL_gameinputjoystick.cpp @@ -173,7 +173,11 @@ static bool GAMEINPUT_InternalAddOrFind(IGameInputDevice *pDevice) elem->guid = SDL_CreateJoystickGUID(bus, vendor, product, version, manufacturer_string, product_string, 'g', 0); elem->device_instance = SDL_GetNextObjectID(); elem->info = info; +#if GAMEINPUT_API_VERSION >= 1 elem->steam_virtual_gamepad_slot = GetSteamVirtualGamepadSlot(info->pnpPath); +#else + elem->steam_virtual_gamepad_slot = -1; +#endif g_GameInputList.devices = devicelist; g_GameInputList.devices[g_GameInputList.count++] = elem; From e5d57d8ad6e6081675545463a912c9d305847dea Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Thu, 24 Jul 2025 10:52:09 -0700 Subject: [PATCH 095/103] Fixed building with GameInput v1.0 --- src/joystick/gdk/SDL_gameinputjoystick.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/joystick/gdk/SDL_gameinputjoystick.cpp b/src/joystick/gdk/SDL_gameinputjoystick.cpp index 96f0daea59..bd1e9eaf63 100644 --- a/src/joystick/gdk/SDL_gameinputjoystick.cpp +++ b/src/joystick/gdk/SDL_gameinputjoystick.cpp @@ -84,6 +84,7 @@ static bool GAMEINPUT_InternalIsGamepad(const GameInputDeviceInfo *info) return false; } +#if GAMEINPUT_API_VERSION >= 1 static int GetSteamVirtualGamepadSlot(const char *device_path) { int slot = -1; @@ -93,6 +94,7 @@ static int GetSteamVirtualGamepadSlot(const char *device_path) (void)SDL_sscanf(device_path, "\\\\.\\pipe\\HID#VID_045E&PID_028E&IG_00#%*X&%*X&%*X#%d#%*u", &slot); return slot; } +#endif // GAMEINPUT_API_VERSION >= 1 static bool GAMEINPUT_InternalAddOrFind(IGameInputDevice *pDevice) { From c80d6954cbf63da37657bdda68da471e6885c29d Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Fri, 25 Jul 2025 01:29:13 -0400 Subject: [PATCH 096/103] Revert "audio: Added SDL_SetAudioIterationCallbacks()." This reverts commit 608f706a954cb856c8ae2f18d3a6a0339ff7b0ae. Didn't end up using this in SDL3_mixer, and it's a super-awkward API if we don't need it. I _might_ bite the bullet and let people lock a physical audio device, though, as I could see that being useful but less awkward for the same reasons I originally wanted it. --- include/SDL3/SDL_audio.h | 79 ------------------------------- src/audio/SDL_audio.c | 39 +-------------- src/audio/SDL_sysaudio.h | 7 --- src/dynapi/SDL_dynapi.sym | 1 - src/dynapi/SDL_dynapi_overrides.h | 1 - src/dynapi/SDL_dynapi_procs.h | 1 - 6 files changed, 1 insertion(+), 127 deletions(-) diff --git a/include/SDL3/SDL_audio.h b/include/SDL3/SDL_audio.h index ba01d7d11a..be5eb32e9b 100644 --- a/include/SDL3/SDL_audio.h +++ b/include/SDL3/SDL_audio.h @@ -2035,85 +2035,6 @@ extern SDL_DECLSPEC void SDLCALL SDL_DestroyAudioStream(SDL_AudioStream *stream) */ extern SDL_DECLSPEC SDL_AudioStream * SDLCALL SDL_OpenAudioDeviceStream(SDL_AudioDeviceID devid, const SDL_AudioSpec *spec, SDL_AudioStreamCallback callback, void *userdata); -/** - * A callback that fires around an audio device's processing work. - * - * This callback fires when a logical audio device is about to start accessing - * its bound audio streams, and fires again when it has finished accessing - * them. It covers the range of one "iteration" of the audio device. - * - * It can be useful to use this callback to update state that must apply to - * all bound audio streams atomically: to make sure state changes don't happen - * while half of the streams are already processed for the latest audio - * buffer. - * - * This callback should run as quickly as possible and not block for any - * significant time, as this callback delays submission of data to the audio - * device, which can cause audio playback problems. This callback delays all - * audio processing across a single physical audio device: all its logical - * devices and all bound audio streams. Use it carefully. - * - * \param userdata a pointer provided by the app through - * SDL_SetAudioPostmixCallback, for its own use. - * \param devid the audio device this callback is running for. - * \param start true if this is the start of the iteration, false if the end. - * - * \threadsafety This will run from a background thread owned by SDL. The - * application is responsible for locking resources the callback - * touches that need to be protected. - * - * \since This datatype is available since SDL 3.4.0. - * - * \sa SDL_SetAudioIterationCallbacks - */ -typedef void (SDLCALL *SDL_AudioIterationCallback)(void *userdata, SDL_AudioDeviceID devid, bool start); - -/** - * Set callbacks that fire around a new iteration of audio device processing. - * - * Two callbacks are provided here: one that runs when a device is about to - * process its bound audio streams, and another that runs when the device has - * finished processing them. - * - * These callbacks can run at any time, and from any thread; if you need to - * serialize access to your app's data, you should provide and use a mutex or - * other synchronization device. - * - * Generally these callbacks are used to apply state that applies to multiple - * bound audio streams, with a guarantee that the audio device's thread isn't - * halfway through processing them. Generally a finer-grained lock through - * SDL_LockAudioStream() is more appropriate. - * - * The callbacks are extremely time-sensitive; the callback should do the - * least amount of work possible and return as quickly as it can. The longer - * the callback runs, the higher the risk of audio dropouts or other problems. - * - * This function will block until the audio device is in between iterations, - * so any existing callback that might be running will finish before this - * function sets the new callback and returns. - * - * Physical devices do not accept these callbacks, only logical devices - * created through SDL_OpenAudioDevice() can be. - * - * Setting a NULL callback function disables any previously-set callback. - * Either callback may be NULL, and the same callback is permitted to be used - * for both. - * - * \param devid the ID of an opened audio device. - * \param start a callback function to be called at the start of an iteration. - * Can be NULL. - * \param end a callback function to be called at the end of an iteration. Can - * be NULL. - * \param userdata app-controlled pointer passed to callback. Can be NULL. - * \returns true on success or false on failure; call SDL_GetError() for more - * information. - * - * \threadsafety It is safe to call this function from any thread. - * - * \since This function is available since SDL 3.4.0. - */ -extern SDL_DECLSPEC bool SDLCALL SDL_SetAudioIterationCallbacks(SDL_AudioDeviceID devid, SDL_AudioIterationCallback start, SDL_AudioIterationCallback end, void *userdata); - /** * A callback that fires when data is about to be fed to an audio device. * diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c index b1956175c6..8b0ecab202 100644 --- a/src/audio/SDL_audio.c +++ b/src/audio/SDL_audio.c @@ -1164,23 +1164,9 @@ bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device) // We should have updated this elsewhere if the format changed! SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &device->spec, NULL, NULL)); - SDL_assert(stream->src_spec.format != SDL_AUDIO_UNKNOWN); - int br = 0; - - if (!SDL_GetAtomicInt(&logdev->paused)) { - if (logdev->iteration_start) { - logdev->iteration_start(logdev->iteration_userdata, logdev->instance_id, true); - } - - br = SDL_GetAudioStreamDataAdjustGain(stream, device_buffer, buffer_size, logdev->gain); - - if (logdev->iteration_end) { - logdev->iteration_end(logdev->iteration_userdata, logdev->instance_id, false); - } - } - + const int br = SDL_GetAtomicInt(&logdev->paused) ? 0 : SDL_GetAudioStreamDataAdjustGain(stream, device_buffer, buffer_size, logdev->gain); if (br < 0) { // Probably OOM. Kill the audio device; the whole thing is likely dying soon anyhow. failed = true; SDL_memset(device_buffer, device->silence_value, buffer_size); // just supply silence to the device before we die. @@ -1218,10 +1204,6 @@ bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device) SDL_memset(mix_buffer, '\0', work_buffer_size); // start with silence. } - if (logdev->iteration_start) { - logdev->iteration_start(logdev->iteration_userdata, logdev->instance_id, true); - } - for (SDL_AudioStream *stream = logdev->bound_streams; stream; stream = stream->next_binding) { // We should have updated this elsewhere if the format changed! SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &outspec, NULL, NULL)); @@ -1246,10 +1228,6 @@ bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device) } } - if (logdev->iteration_end) { - logdev->iteration_end(logdev->iteration_userdata, logdev->instance_id, false); - } - if (postmix) { SDL_assert(mix_buffer == device->postmix_buffer); postmix(logdev->postmix_userdata, &outspec, mix_buffer, work_buffer_size); @@ -1991,21 +1969,6 @@ bool SDL_SetAudioPostmixCallback(SDL_AudioDeviceID devid, SDL_AudioPostmixCallba return result; } -bool SDL_SetAudioIterationCallbacks(SDL_AudioDeviceID devid, SDL_AudioIterationCallback iter_start, SDL_AudioIterationCallback iter_end, void *userdata) -{ - SDL_AudioDevice *device = NULL; - SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid, &device); - bool result = false; - if (logdev) { - logdev->iteration_start = iter_start; - logdev->iteration_end = iter_end; - logdev->iteration_userdata = userdata; - result = true; - } - ReleaseAudioDevice(device); - return result; -} - bool SDL_BindAudioStreams(SDL_AudioDeviceID devid, SDL_AudioStream * const *streams, int num_streams) { const bool islogical = !(devid & (1<<1)); diff --git a/src/audio/SDL_sysaudio.h b/src/audio/SDL_sysaudio.h index 603eaab197..3b3cb9b37e 100644 --- a/src/audio/SDL_sysaudio.h +++ b/src/audio/SDL_sysaudio.h @@ -264,13 +264,6 @@ struct SDL_LogicalAudioDevice // true if device was opened with SDL_OpenAudioDeviceStream (so it forbids binding changes, etc). bool simplified; - // If non-NULL, callback into the app that alerts it to start/end of device iteration. - SDL_AudioIterationCallback iteration_start; - SDL_AudioIterationCallback iteration_end; - - // App-supplied pointer for iteration callbacks. - void *iteration_userdata; - // If non-NULL, callback into the app that lets them access the final postmix buffer. SDL_AudioPostmixCallback postmix; diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index 86abbbd12f..fcae64bea6 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -1251,7 +1251,6 @@ SDL3_0.0.0 { SDL_GetGPUDeviceProperties; SDL_CreateGPURenderer; SDL_PutAudioStreamPlanarData; - SDL_SetAudioIterationCallbacks; SDL_GetEventDescription; SDL_PutAudioStreamDataNoCopy; SDL_IsTraySupported; diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h index a2f02b2708..8873ff4734 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -1276,7 +1276,6 @@ #define SDL_GetGPUDeviceProperties SDL_GetGPUDeviceProperties_REAL #define SDL_CreateGPURenderer SDL_CreateGPURenderer_REAL #define SDL_PutAudioStreamPlanarData SDL_PutAudioStreamPlanarData_REAL -#define SDL_SetAudioIterationCallbacks SDL_SetAudioIterationCallbacks_REAL #define SDL_GetEventDescription SDL_GetEventDescription_REAL #define SDL_PutAudioStreamDataNoCopy SDL_PutAudioStreamDataNoCopy_REAL #define SDL_IsTraySupported SDL_IsTraySupported_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index 08ba54c2cb..f05ff2ff33 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -1284,7 +1284,6 @@ SDL_DYNAPI_PROC(bool,SDL_GetRenderTextureAddressMode,(SDL_Renderer *a,SDL_Textur SDL_DYNAPI_PROC(SDL_PropertiesID,SDL_GetGPUDeviceProperties,(SDL_GPUDevice *a),(a),return) SDL_DYNAPI_PROC(SDL_Renderer*,SDL_CreateGPURenderer,(SDL_Window *a,SDL_GPUShaderFormat b,SDL_GPUDevice **c),(a,b,c),return) SDL_DYNAPI_PROC(bool,SDL_PutAudioStreamPlanarData,(SDL_AudioStream *a,const void * const*b,int c,int d),(a,b,c,d),return) -SDL_DYNAPI_PROC(bool,SDL_SetAudioIterationCallbacks,(SDL_AudioDeviceID a,SDL_AudioIterationCallback b,SDL_AudioIterationCallback c,void *d),(a,b,c,d),return) SDL_DYNAPI_PROC(int,SDL_GetEventDescription,(const SDL_Event *a,char *b,int c),(a,b,c),return) SDL_DYNAPI_PROC(bool,SDL_PutAudioStreamDataNoCopy,(SDL_AudioStream *a,const void *b,int c,SDL_AudioStreamDataCompleteCallback d,void *e),(a,b,c,d,e),return) SDL_DYNAPI_PROC(bool,SDL_IsTraySupported,(void),(),return) From c8e2d13173d6ddbaf18316a376f40cdca42e4679 Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Fri, 25 Jul 2025 10:09:55 -0400 Subject: [PATCH 097/103] wayland: Fix the key level request layout parameter Use the layout loop index instead of the current layout, which could be invalid if no layout event was received before the keymap event. Fixes #13418 --- src/video/wayland/SDL_waylandevents.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c index bb92b1ca47..227246cc92 100644 --- a/src/video/wayland/SDL_waylandevents.c +++ b/src/video/wayland/SDL_waylandevents.c @@ -1435,7 +1435,7 @@ static void Wayland_KeymapIterator(struct xkb_keymap *keymap, xkb_keycode_t key, } for (xkb_layout_index_t layout = 0; layout < seat->keyboard.xkb.num_layouts; ++layout) { - const xkb_level_index_t num_levels = WAYLAND_xkb_keymap_num_levels_for_key(seat->keyboard.xkb.keymap, key, seat->keyboard.xkb.current_layout); + const xkb_level_index_t num_levels = WAYLAND_xkb_keymap_num_levels_for_key(seat->keyboard.xkb.keymap, key, layout); for (xkb_level_index_t level = 0; level < num_levels; ++level) { if (WAYLAND_xkb_keymap_key_get_syms_by_level(seat->keyboard.xkb.keymap, key, layout, level, &syms) > 0) { /* If the keyboard is virtual or the key didn't have a corresponding hardware scancode, try to From 970234d62df8f1cec5e2b2d8ed10446fb9180082 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Fri, 25 Jul 2025 13:33:20 -0700 Subject: [PATCH 098/103] Fixed documentation for aligned structure members --- include/SDL3/SDL_begin_code.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/SDL3/SDL_begin_code.h b/include/SDL3/SDL_begin_code.h index e306a90652..e6e3c689a5 100644 --- a/include/SDL3/SDL_begin_code.h +++ b/include/SDL3/SDL_begin_code.h @@ -298,7 +298,7 @@ * // make sure this one field in a struct is aligned to 16 bytes for SIMD access. * typedef struct { * SomeStuff stuff; - * float position[4] SDL_ALIGNED(16); + * float SDL_ALIGNED(16) position[4]; * SomeOtherStuff other_stuff; * } MyStruct; * From 72b7fd10b44c029d3095e7e8605ad407a5735871 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Fri, 25 Jul 2025 16:20:44 -0700 Subject: [PATCH 099/103] Fixed warning: 'break' will never be executed --- src/joystick/hidapi/SDL_hidapi_8bitdo.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/joystick/hidapi/SDL_hidapi_8bitdo.c b/src/joystick/hidapi/SDL_hidapi_8bitdo.c index d48e46671b..95227869da 100644 --- a/src/joystick/hidapi/SDL_hidapi_8bitdo.c +++ b/src/joystick/hidapi/SDL_hidapi_8bitdo.c @@ -272,7 +272,6 @@ static Uint64 HIDAPI_Driver8BitDo_GetIMURateForProductID(SDL_HIDAPI_Device *devi case USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS: default: return 120; - break; } } From e6d200e51c88a0406c770c52bf7f415181243189 Mon Sep 17 00:00:00 2001 From: Anonymous Maarten Date: Sat, 26 Jul 2025 13:53:26 +0200 Subject: [PATCH 100/103] ci+n3ds: avoid apt-get package manager - use Unix Makefiles (with parallelization) CMake generator - use binutils strings binary from devkitpro --- .github/workflows/create-test-plan.py | 11 +++++++++-- .github/workflows/generic.yml | 6 +++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/create-test-plan.py b/.github/workflows/create-test-plan.py index 219a4de10e..79a6996b18 100755 --- a/.github/workflows/create-test-plan.py +++ b/.github/workflows/create-test-plan.py @@ -177,6 +177,7 @@ class JobDetails: brew_packages: list[str] = dataclasses.field(default_factory=list) cmake_toolchain_file: str = "" cmake_arguments: list[str] = dataclasses.field(default_factory=list) + cmake_generator: str = "Ninja" cmake_build_arguments: list[str] = dataclasses.field(default_factory=list) clang_tidy: bool = True cppflags: list[str] = dataclasses.field(default_factory=list) @@ -226,6 +227,7 @@ class JobDetails: setup_python: bool = False pypi_packages: list[str] = dataclasses.field(default_factory=list) setup_gage_sdk_path: str = "" + binutils_strings: str = "strings" def to_workflow(self, enable_artifacts: bool) -> dict[str, str|bool]: data = { @@ -259,6 +261,7 @@ class JobDetails: "cflags": my_shlex_join(self.cppflags + self.cflags), "cxxflags": my_shlex_join(self.cppflags + self.cxxflags), "ldflags": my_shlex_join(self.ldflags), + "cmake-generator": self.cmake_generator, "cmake-toolchain-file": self.cmake_toolchain_file, "clang-tidy": self.clang_tidy, "cmake-arguments": my_shlex_join(self.cmake_arguments), @@ -294,6 +297,7 @@ class JobDetails: "setup-python": self.setup_python, "pypi-packages": my_shlex_join(self.pypi_packages), "setup-ngage-sdk-path": self.setup_gage_sdk_path, + "binutils-strings": self.binutils_strings, } return {k: v for k, v in data.items() if v != ""} @@ -682,13 +686,16 @@ def spec_to_job(spec: JobSpec, key: str, trackmem_symbol_names: bool) -> JobDeta job.shared_lib = SharedLibType.SO_0 job.static_lib = StaticLibType.A case SdlPlatform.N3ds: - job.ccache = True + job.cmake_generator = "Unix Makefiles" + job.cmake_build_arguments.append("-j$(nproc)") + job.ccache = False job.shared = False - job.apt_packages = ["ccache", "ninja-build", "binutils"] + job.apt_packages = [] job.clang_tidy = False job.run_tests = False job.cc_from_cmake = True job.cmake_toolchain_file = "${DEVKITPRO}/cmake/3DS.cmake" + job.binutils_strings = "/opt/devkitpro/devkitARM/bin/arm-none-eabi-strings" job.static_lib = StaticLibType.A case SdlPlatform.Msys2: job.ccache = True diff --git a/.github/workflows/generic.yml b/.github/workflows/generic.yml index 083859b341..7902035f7d 100644 --- a/.github/workflows/generic.yml +++ b/.github/workflows/generic.yml @@ -206,7 +206,7 @@ jobs: #shell: ${{ matrix.platform.shell }} run: | ${{ matrix.platform.source-cmd }} - ${{ matrix.platform.cmake-config-emulator }} cmake -S . -B build -GNinja \ + ${{ matrix.platform.cmake-config-emulator }} cmake -S . -B build -G "${{ matrix.platform.cmake-generator }}" \ -Wdeprecated -Wdev -Werror \ ${{ matrix.platform.cmake-toolchain-file != '' && format('-DCMAKE_TOOLCHAIN_FILE={0}', matrix.platform.cmake-toolchain-file) || '' }} \ -DSDL_WERROR=${{ matrix.platform.werror }} \ @@ -237,9 +237,9 @@ jobs: run: | echo "This should show us the SDL_REVISION" echo "Shared library:" - ${{ (matrix.platform.shared-lib && format('strings build/{0} | grep "Github Workflow"', matrix.platform.shared-lib)) || 'echo ""' }} + ${{ (matrix.platform.shared-lib && format('{0} build/{1} | grep "Github Workflow"', matrix.platform.binutils-strings, matrix.platform.shared-lib)) || 'echo ""' }} echo "Static library:" - ${{ (matrix.platform.static-lib && format('strings build/{0} | grep "Github Workflow"', matrix.platform.static-lib)) || 'echo ""' }} + ${{ (matrix.platform.static-lib && format('{0} build/{1} | grep "Github Workflow"', matrix.platform.binutils-strings, matrix.platform.static-lib)) || 'echo ""' }} - name: 'Run build-time tests (CMake)' id: tests if: ${{ !matrix.platform.no-cmake && matrix.platform.run-tests }} From 2c2c2c5a4878dabe3aa350b5aa184c2c55b79e9a Mon Sep 17 00:00:00 2001 From: Petar Popovic Date: Sat, 26 Jul 2025 19:03:54 +0200 Subject: [PATCH 101/103] Fixed a few "-Wstrict-prototypes" warnings --- src/audio/alsa/SDL_alsa_audio.c | 8 ++++---- src/core/linux/SDL_progressbar.c | 4 ++-- test/gamepadutils.h | 2 +- test/testcontroller.c | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/audio/alsa/SDL_alsa_audio.c b/src/audio/alsa/SDL_alsa_audio.c index f93433e641..a0fb617ccd 100644 --- a/src/audio/alsa/SDL_alsa_audio.c +++ b/src/audio/alsa/SDL_alsa_audio.c @@ -1470,7 +1470,7 @@ static void ALSA_udev_callback(SDL_UDEV_deviceevent udev_type, int udev_class, c } } -static bool ALSA_start_udev() +static bool ALSA_start_udev(void) { udev_initialized = SDL_UDEV_Init(); if (udev_initialized) { @@ -1483,7 +1483,7 @@ static bool ALSA_start_udev() return udev_initialized; } -static void ALSA_stop_udev() +static void ALSA_stop_udev(void) { if (udev_initialized) { SDL_UDEV_DelCallback(ALSA_udev_callback); @@ -1494,12 +1494,12 @@ static void ALSA_stop_udev() #else -static bool ALSA_start_udev() +static bool ALSA_start_udev(void) { return false; } -static void ALSA_stop_udev() +static void ALSA_stop_udev(void) { } diff --git a/src/core/linux/SDL_progressbar.c b/src/core/linux/SDL_progressbar.c index e50f8361ca..ac0789b2d2 100644 --- a/src/core/linux/SDL_progressbar.c +++ b/src/core/linux/SDL_progressbar.c @@ -32,7 +32,7 @@ #define UnityLauncherAPI_DBUS_INTERFACE "com.canonical.Unity.LauncherEntry" #define UnityLauncherAPI_DBUS_SIGNAL "Update" -static char *GetDBUSObjectPath() +static char *GetDBUSObjectPath(void) { char *app_id = SDL_strdup(SDL_GetAppID()); @@ -62,7 +62,7 @@ static char *GetDBUSObjectPath() return SDL_strdup(path); } -static char *GetAppDesktopPath() +static char *GetAppDesktopPath(void) { const char *desktop_suffix = ".desktop"; const char *app_id = SDL_GetAppID(); diff --git a/test/gamepadutils.h b/test/gamepadutils.h index 19eeefcff5..5e1dcd3093 100644 --- a/test/gamepadutils.h +++ b/test/gamepadutils.h @@ -161,7 +161,7 @@ typedef enum typedef struct Quaternion Quaternion; typedef struct GyroDisplay GyroDisplay; -extern void InitCirclePoints3D(); +extern void InitCirclePoints3D(void); extern GyroDisplay *CreateGyroDisplay(SDL_Renderer *renderer); extern void SetGyroDisplayArea(GyroDisplay *ctx, const SDL_FRect *area); extern void SetGamepadDisplayIMUValues(GyroDisplay *ctx, float *gyro_drift_solution, float *euler_displacement_angles, Quaternion *gyro_quaternion, int reported_senor_rate_hz, int estimated_sensor_rate_hz, EGyroCalibrationPhase calibration_phase, float drift_calibration_progress_frac, float accelerometer_noise_sq, float accelerometer_noise_tolerance_sq); diff --git a/test/testcontroller.c b/test/testcontroller.c index 6b0fdea27f..a56661af5d 100644 --- a/test/testcontroller.c +++ b/test/testcontroller.c @@ -1439,7 +1439,7 @@ static void HandleGamepadGyroEvent(SDL_Event *event) #define SDL_GAMEPAD_IMU_MIN_POLLING_RATE_ESTIMATION_TIME_NS (SDL_NS_PER_SECOND * 2) -static void EstimatePacketRate() +static void EstimatePacketRate(void) { Uint64 now_ns = SDL_GetTicksNS(); if (controller->imu_state->imu_packet_counter == 0) { From 6a5af95364f7014f99ec6a524e467d3bc5b27aa4 Mon Sep 17 00:00:00 2001 From: Petar Popovic Date: Sat, 26 Jul 2025 20:33:12 +0200 Subject: [PATCH 102/103] SDL_gpu.c: Fixed deref-before-check warning --- src/gpu/SDL_gpu.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gpu/SDL_gpu.c b/src/gpu/SDL_gpu.c index ad5a16da3f..d148c431bc 100644 --- a/src/gpu/SDL_gpu.c +++ b/src/gpu/SDL_gpu.c @@ -2242,14 +2242,14 @@ void SDL_DrawGPUIndexedPrimitivesIndirect( void SDL_EndGPURenderPass( SDL_GPURenderPass *render_pass) { - CommandBufferCommonHeader *commandBufferCommonHeader; - commandBufferCommonHeader = (CommandBufferCommonHeader *)RENDERPASS_COMMAND_BUFFER; - if (render_pass == NULL) { SDL_InvalidParamError("render_pass"); return; } + CommandBufferCommonHeader *commandBufferCommonHeader; + commandBufferCommonHeader = (CommandBufferCommonHeader *)RENDERPASS_COMMAND_BUFFER; + if (RENDERPASS_DEVICE->debug_mode) { CHECK_RENDERPASS } From 2ed1c35ca6c667296bf3fa073ccb81085493661c Mon Sep 17 00:00:00 2001 From: SDL Wiki Bot Date: Sat, 26 Jul 2025 21:11:11 +0000 Subject: [PATCH 103/103] Sync SDL3 wiki -> header [ci skip] --- include/SDL3/SDL_main.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/SDL3/SDL_main.h b/include/SDL3/SDL_main.h index 305a1a3838..1278b3788f 100644 --- a/include/SDL3/SDL_main.h +++ b/include/SDL3/SDL_main.h @@ -449,8 +449,8 @@ extern SDLMAIN_DECLSPEC SDL_AppResult SDLCALL SDL_AppEvent(void *appstate, SDL_E * * This function is called once by SDL before terminating the program. * - * This function will be called no matter what, even if SDL_AppInit requests - * termination. + * This function will be called in all cases, even if SDL_AppInit requests + * termination at startup. * * This function should not go into an infinite mainloop; it should * deinitialize any resources necessary, perform whatever shutdown activities,