diff --git a/.github/workflows/create-test-plan.py b/.github/workflows/create-test-plan.py index 53ca8fa2b2..903715c060 100755 --- a/.github/workflows/create-test-plan.py +++ b/.github/workflows/create-test-plan.py @@ -182,6 +182,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) @@ -231,6 +232,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 = { @@ -264,6 +266,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), @@ -299,6 +302,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 != ""} @@ -687,13 +691,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 1cd6b62411..57ba44b088 100644 --- a/.github/workflows/generic.yml +++ b/.github/workflows/generic.yml @@ -209,7 +209,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 }} \ @@ -240,9 +240,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 }} 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 }} diff --git a/CMakeLists.txt b/CMakeLists.txt index db9b6f66e2..efceda6cb0 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) @@ -654,6 +657,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") 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/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; diff --git a/include/SDL3/SDL_audio.h b/include/SDL3/SDL_audio.h index c0aa7f7c82..be5eb32e9b 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. @@ -2032,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/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; * 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/include/SDL3/SDL_gpu.h b/include/SDL3/SDL_gpu.h index b3ff20221c..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; @@ -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 @@ -3122,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. diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h index 30a04c4da4..09c918b13a 100644 --- a/include/SDL3/SDL_hints.h +++ b/include/SDL3/SDL_hints.h @@ -1746,6 +1746,20 @@ 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. @@ -4381,6 +4395,30 @@ extern "C" { */ #define SDL_HINT_PEN_TOUCH_EVENTS "SDL_PEN_TOUCH_EVENTS" +/** + * A variable controlling whether SDL logs some debug information. + * + * The variable can be set to the following values: + * + * - "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, 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_DEBUG_LOGGING "SDL_DEBUG_LOGGING" /** * An enumeration of hint priorities. diff --git a/include/SDL3/SDL_iostream.h b/include/SDL3/SDL_iostream.h index d1596f91d3..2e6a7b52a9 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_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. * \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_POINTER "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_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. * \param size the buffer size, in bytes. * \returns a pointer to a new SDL_IOStream structure or NULL on failure; call 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, 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. */ 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 diff --git a/include/SDL3/SDL_tray.h b/include/SDL3/SDL_tray.h index 1780b0ba52..ec7adfcf7f 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/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 diff --git a/include/build_config/SDL_build_config_windows.h b/include/build_config/SDL_build_config_windows.h index 24c578b6f8..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 @@ -156,7 +157,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 @@ -214,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 */ 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); 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. */ diff --git a/src/SDL_utils.c b/src/SDL_utils.c index f2090747a8..47fa28c06d 100644 --- a/src/SDL_utils.c +++ b/src/SDL_utils.c @@ -552,3 +552,13 @@ char *SDL_CreateDeviceName(Uint16 vendor, Uint16 product, const char *vendor_nam return name; } + +#define SDL_DEBUG_LOG_INTRO "SDL_DEBUG: " + +void SDL_DebugLogBackend(const char *subsystem, const char *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 5e0e8f519e..b70b64e963 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_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 7dc247b193..8b0ecab202 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_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) { if (driver_name) { @@ -1162,21 +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. @@ -1214,14 +1204,12 @@ 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)); + 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 @@ -1240,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); @@ -1361,6 +1345,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; @@ -1984,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)); @@ -2022,10 +1992,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)); @@ -2060,9 +2026,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; 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/audio/aaudio/SDL_aaudio.c b/src/audio/aaudio/SDL_aaudio.c index 5436be070a..b1be69182a 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); \ + 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..af4f44bd9f 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 +#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 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/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/camera/SDL_camera.c b/src/camera/SDL_camera.c index 9f71cea0f3..48c6b500df 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_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) { if (driver_name) { 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/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/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/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); 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/dialog/cocoa/SDL_cocoadialog.m b/src/dialog/cocoa/SDL_cocoadialog.m index d12dab8718..f1b9c3bbaa 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) { @@ -155,14 +157,24 @@ void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFil // Keep behavior consistent with other platforms [dialog setAllowsOtherFileTypes:YES]; - if (default_location) { - [dialog setDirectoryURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String: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 == '/') { + [dialog setDirectoryURL:url]; + } else { + [dialog setDirectoryURL:[url URLByDeletingLastPathComponent]]; + [dialog setNameFieldStringValue:[url lastPathComponent]]; + } } NSWindow *w = NULL; 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 +198,7 @@ void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFil callback(userdata, files, -1); } + Cocoa_SetWindowHasModalDialog(window, false); ReactivateAfterDialog(); }]; } else { @@ -206,6 +219,7 @@ void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFil const char *files[1] = { NULL }; callback(userdata, files, -1); } + ReactivateAfterDialog(); } } diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index 91bd4300c7..fcae64bea6 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -1251,9 +1251,9 @@ SDL3_0.0.0 { SDL_GetGPUDeviceProperties; SDL_CreateGPURenderer; SDL_PutAudioStreamPlanarData; - 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..8873ff4734 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -1276,6 +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 60e0dfea57..f05ff2ff33 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -1284,6 +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) diff --git a/src/events/SDL_keyboard.c b/src/events/SDL_keyboard.c index fc06f07258..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; @@ -172,19 +171,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); @@ -308,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_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_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/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/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/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) 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; 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); diff --git a/src/gpu/SDL_gpu.c b/src/gpu/SDL_gpu.c index 2b5a38bdd5..d148c431bc 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_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); @@ -1566,6 +1567,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); @@ -2240,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 } 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..759cf50023 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); @@ -8592,7 +8594,7 @@ static void VULKAN_INTERNAL_BindComputeDescriptorSets( dynamicOffsetCount, dynamicOffsets); - commandBuffer->needNewVertexUniformOffsets = false; + commandBuffer->needNewComputeUniformOffsets = false; } static void VULKAN_DispatchCompute( 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/hidapi/libusb/hid.c b/src/hidapi/libusb/hid.c index 94cc50dcb4..0c4fc660ba 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 libusb_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) || + libusb_blacklist(dev_vid, dev_pid)) { continue; } 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 */ }; diff --git a/src/io/SDL_iostream.c b/src/io/SDL_iostream.c index 989f3b9c4c..7c956baf5f 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_POINTER, 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/src/io/io_uring/SDL_asyncio_liburing.c b/src/io/io_uring/SDL_asyncio_liburing.c index 07e8c2415c..4aef5f4b12 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_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_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 497456cfa4..52683c6ab2 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_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_DebugLogBackend("asyncio", "generic"); CreateAsyncIOQueue = SDL_SYS_CreateAsyncIOQueue_Generic; QuitAsyncIO = SDL_SYS_QuitAsyncIO_Generic; AsyncIOFromFile = SDL_SYS_AsyncIOFromFile_Generic; diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index 8a5ecc0ab9..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; } @@ -785,16 +785,68 @@ 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 || 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: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: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: + 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; + } + + // 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)) { @@ -1232,6 +1284,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 0e9fcab3b5..0aa742da85 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 @@ -2315,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(); @@ -2349,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); } } @@ -3201,6 +3206,19 @@ 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) +{ + 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) { 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/controller_list.h b/src/joystick/controller_list.h index 63107389cf..3d33d2236e 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 @@ -575,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/gdk/SDL_gameinputjoystick.cpp b/src/joystick/gdk/SDL_gameinputjoystick.cpp index 67cf51fa5a..bd1e9eaf63 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 @@ -47,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; @@ -78,6 +84,18 @@ static bool GAMEINPUT_InternalIsGamepad(const GameInputDeviceInfo *info) return false; } +#if GAMEINPUT_API_VERSION >= 1 +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; +} +#endif // GAMEINPUT_API_VERSION >= 1 + static bool GAMEINPUT_InternalAddOrFind(IGameInputDevice *pDevice) { GAMEINPUT_InternalDevice **devicelist = NULL; @@ -157,6 +175,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; @@ -344,7 +367,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) @@ -474,21 +497,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 +684,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/src/joystick/hidapi/SDL_hidapi_8bitdo.c b/src/joystick/hidapi/SDL_hidapi_8bitdo.c index d1167586d0..95227869da 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); @@ -255,6 +258,7 @@ static Uint64 HIDAPI_Driver8BitDo_GetIMURateForProductID(SDL_HIDAPI_Device *devi } 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 @@ -268,7 +272,6 @@ static Uint64 HIDAPI_Driver8BitDo_GetIMURateForProductID(SDL_HIDAPI_Device *devi case USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS: default: return 120; - break; } } @@ -287,6 +290,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/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; diff --git a/src/joystick/hidapi/SDL_hidapi_sinput.c b/src/joystick/hidapi/SDL_hidapi_sinput.c new file mode 100644 index 0000000000..04af49e810 --- /dev/null +++ b/src/joystick/hidapi/SDL_hidapi_sinput.c @@ -0,0 +1,928 @@ +/* + 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 + +/*****************************************************************************************************/ +// 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 +#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_GENERIC_SUBTYPE_SUPERGAMEPADPLUS 0x01 + +#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 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_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 + +#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 + +#ifndef EXTRACTUINT32 +#define EXTRACTUINT32(data, idx) ((Uint32)((data)[(idx)] | ((data)[(idx) + 1] << 8) | ((data)[(idx) + 2] << 16) | ((data)[(idx) + 3] << 24))) +#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; + Uint16 protocol_version; + 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 dpad_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]; + + 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 +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; + + // 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[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[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[3] & 0x01) != 0; + ctx->joystick_rgb_supported = (data[3] & 0x02) != 0; + + SDL_GamepadType type = SDL_GAMEPAD_TYPE_UNKNOWN; + 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[5]; + +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("SInput Face Style: %d", (data[5] & 0xF0) >> 4); + 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); + ctx->gyroRange = EXTRACTUINT16(data, 10); + + // Masks in LSB to MSB + // South, East, West, North, DUp, DDown, DLeft, DRight + 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[13]; + + // Start, Back, Guide, Capture, L Paddle 2, R Paddle 2, Touchpad L, Touchpad R + ctx->usage_masks[2] = data[14]; + + // Power, Misc 4 to 10 + ctx->usage_masks[3] = data[15]; + + // 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; + } + } + } + + // Convert DPAD to hat + 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; + ctx->buttons_count -= 4; + } + +#if defined(DEBUG_SINPUT_INIT) + SDL_Log("Buttons count: %d", ctx->buttons_count); +#endif + + // Get and validate touchpad parameters + 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[18], data[19], data[20], data[21], data[22], data[23]); +#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 + +#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; + } + + 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); +} + +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->dpad_supported) { + joystick->nhats = 1; + } + + if (ctx->accelerometer_supported) { + SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL, 1000.0f / ctx->polling_rate_ms); + } + + if (ctx->gyroscope_supported) { + SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_GYRO, 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; + } + } + } + + 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; + + // 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_UNKNOWN; + 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: + break; + } + + if (state != SDL_POWERSTATE_UNKNOWN) { + SDL_SendJoystickPowerInfo(joystick, state, percent); + } + } + + // Extract the IMU timestamp delta (in microseconds) + 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 (ctx->sensors_enabled) { + + 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) { + + 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_ns, 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_ns, 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_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/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 33a8a6cb7e..343f957812 100644 --- a/src/joystick/usb_ids.h +++ b/src/joystick/usb_ids.h @@ -59,14 +59,16 @@ #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_ULTIMATE2_WIRELESS 0x6012 #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_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 @@ -106,6 +108,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 @@ -159,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 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; 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/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..e139d3754b 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_DebugLogBackend("render", renderer->name); + } else { if (driver_name) { SDL_SetError("%s not available", driver_name); } else { @@ -4999,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; @@ -5178,7 +5196,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]; 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; 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); diff --git a/src/storage/SDL_storage.c b/src/storage/SDL_storage.c index 7c395b36a8..643a2be1c1 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_DebugLogBackend("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_DebugLogBackend("user_storage", userbootstrap[i]->name); + } else { if (driver_name) { SDL_SetError("%s not available", driver_name); } else { 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) { 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 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()) { 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) { diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c index c2bbd7de4d..b035a20cc6 100644 --- a/src/video/SDL_video.c +++ b/src/video/SDL_video.c @@ -177,9 +177,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 @@ -681,7 +682,9 @@ bool SDL_VideoInit(const char *driver_name) } } } - if (!video) { + if (video) { + SDL_DebugLogBackend("video", bootstrap[i]->name); + } else { if (driver_name) { SDL_SetError("%s not available", driver_name); goto pre_driver_error; @@ -4248,7 +4251,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, 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); 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); + } } } diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c index d3c305fb7f..227246cc92 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" @@ -304,6 +305,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 +451,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 +487,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 +557,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,108 +1414,82 @@ static const struct wl_touch_listener touch_listener = { touch_handler_orientation // Version 6 }; -typedef struct Wayland_KeymapBuilderState +static void Wayland_KeymapIterator(struct xkb_keymap *keymap, xkb_keycode_t key, void *data) { - 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 SDL_Scancode scancode = SDL_GetScancodeFromTable(SDL_SCANCODE_TABLE_XFREE86_2, (key - 8)); - if (scancode == SDL_SCANCODE_UNKNOWN) { - return; + 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; + 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; + } } - if (WAYLAND_xkb_state_key_get_syms(sdlKeymap->state, key, &syms) > 0) { - SDL_Keycode keycode = SDL_GetKeyCodeFromKeySym(syms[0], key, sdlKeymap->modstate); + 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, 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; + } + } - 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; + 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); + } } } - - 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; - } else { - // Virtual keyboards use the default keymap. - SDL_SetKeymap(NULL, true); - SDL_DestroyKeymap(seat->keyboard.sdl_keymap); - seat->keyboard.sdl_keymap = NULL; } } @@ -1520,7 +1498,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); @@ -1546,9 +1523,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); @@ -1557,6 +1534,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); @@ -1602,9 +1587,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); } /* @@ -1613,7 +1608,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) { @@ -1624,26 +1619,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); } } @@ -1651,16 +1658,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; + } } } @@ -1887,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) { @@ -1898,30 +1909,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; + } } } } @@ -1969,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) { @@ -2048,11 +2059,18 @@ static void keyboard_handle_key(void *data, struct wl_keyboard *keyboard, Wayland_UpdateImplicitGrabSerial(seat, serial); - 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 (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; } + Wayland_SeatSetKeymap(seat); + if (state == WL_KEYBOARD_KEY_STATE_PRESSED) { SDL_Window *keyboard_focus = SDL_GetKeyboardFocus(); if (keyboard_focus && SDL_TextInputActive(keyboard_focus)) { @@ -2072,9 +2090,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) { @@ -2120,13 +2156,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, @@ -2209,10 +2247,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) { @@ -2231,6 +2275,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); } @@ -2342,25 +2388,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); - } } } diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h index 8f27ae978f..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 { @@ -75,7 +74,8 @@ 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; Uint64 highres_timestamp_ns; @@ -95,6 +95,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_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_ diff --git a/src/video/wayland/SDL_waylandsym.h b/src/video/wayland/SDL_waylandsym.h index d04223f3b4..31f77fbf9e 100644 --- a/src/video/wayland/SDL_waylandsym.h +++ b/src/video/wayland/SDL_waylandsym.h @@ -150,11 +150,19 @@ 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, - 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 *) ) diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c index a1b5276648..e9a97a98f1 100644 --- a/src/video/wayland/SDL_waylandvideo.c +++ b/src/video/wayland/SDL_waylandvideo.c @@ -74,14 +74,16 @@ #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 #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 @@ -95,6 +97,20 @@ #define SDL_WL_OUTPUT_VERSION 3 #endif +#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 +#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" @@ -456,6 +472,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, @@ -466,6 +483,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) @@ -492,6 +514,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) { @@ -1261,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) { @@ -1315,6 +1341,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) @@ -1549,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; } @@ -1634,6 +1669,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; diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c index df0898e657..84270567fd 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, @@ -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); } } } @@ -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)); @@ -2322,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 @@ -2333,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); @@ -2767,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; } 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 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; } 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) 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); diff --git a/src/video/windows/SDL_windowswindow.c b/src/video/windows/SDL_windowswindow.c index fe61315acc..91737b38bd 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) { @@ -2373,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); } @@ -2422,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 { 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. diff --git a/test/gamepadutils.c b/test/gamepadutils.c index c5295c4c54..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); } @@ -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; @@ -1029,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; @@ -1047,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"); } @@ -1360,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; @@ -1389,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) @@ -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 ) @@ -1711,7 +1710,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; /* @@ -1757,7 +1756,6 @@ 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; @@ -1767,24 +1765,44 @@ 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)); + /* 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); + + /* 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)); } 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); + 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) { + 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 (!bHasCachedDriftSolution) { + 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 flNoiseFraction = SDL_clamp(SDL_sqrtf(ctx->accelerometer_noise_sq) / ACCELEROMETER_NOISE_THRESHOLD, 0.0f, 1.0f); - bool bTooMuchNoise = (flNoiseFraction == 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; @@ -1793,25 +1811,38 @@ void RenderGyroDriftCalibrationButton(GyroDisplay *ctx, GamepadDisplay *gamepad_ noise_bar_rect.w = recalibrate_button_area.w; noise_bar_rect.h = noise_bar_height; - /* Adjust the noise bar rectangle based on the accelerometer noise value */ + 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); - float noise_bar_fill_width = flNoiseFraction * noise_bar_rect.w; /* Scale the width based on the noise value */ + /* 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; 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 */ /* 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); @@ -1826,7 +1857,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; @@ -1945,11 +1976,10 @@ 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); } diff --git a/test/gamepadutils.h b/test/gamepadutils.h index 157bc9a0be..5e1dcd3093 100644 --- a/test/gamepadutils.h +++ b/test/gamepadutils.h @@ -142,16 +142,29 @@ 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 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 +/* 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 +{ + 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 void InitCirclePoints3D(void); 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/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_iostream.c b/test/testautomation_iostream.c index d1e5877111..89c4f983f1 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_POINTER, 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) */ 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, diff --git a/test/testcontroller.c b/test/testcontroller.c index 0530d1a992..a56661af5d 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,39 @@ 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 ( 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) + +/* + * 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. */ + 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; + + /* 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 +250,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 +274,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]; @@ -1375,14 +1432,14 @@ 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 #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) { @@ -1421,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; @@ -1444,7 +1501,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(); + 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) { + 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_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; /* Send the results to the frontend */ @@ -1454,8 +1522,10 @@ static void HandleGamepadSensorEvent( SDL_Event* event ) &controller->imu_state->integrated_rotation, reported_polling_rate_hz, controller->imu_state->imu_estimated_sensor_rate, - drift_calibration_progress_frac, - controller->imu_state->accelerometer_length_squared + controller->imu_state->calibration_phase, + 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) */ @@ -2145,7 +2215,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); } 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. + + + + +