Moved raw input event processing from the main thread to the joystick thread
authorSam Lantinga
Fri, 27 Nov 2020 13:08:40 -0800
changeset 144097163b8a6093f
parent 14408 8c6398f73469
child 14410 f478d1529e64
Moved raw input event processing from the main thread to the joystick thread

This allows fast joystick event delivery regardless of what the main thread is doing.
include/SDL_config_windows.h
src/joystick/hidapi/SDL_hidapijoystick.c
src/joystick/windows/SDL_rawinputjoystick.c
src/joystick/windows/SDL_rawinputjoystick_c.h
src/joystick/windows/SDL_windowsjoystick.c
src/video/windows/SDL_windowswindow.c
     1.1 --- a/include/SDL_config_windows.h	Fri Nov 27 11:33:53 2020 -0800
     1.2 +++ b/include/SDL_config_windows.h	Fri Nov 27 13:08:40 2020 -0800
     1.3 @@ -215,7 +215,9 @@
     1.4  /* Enable various input drivers */
     1.5  #define SDL_JOYSTICK_DINPUT 1
     1.6  #define SDL_JOYSTICK_HIDAPI 1
     1.7 +#ifndef __WINRT__
     1.8  #define SDL_JOYSTICK_RAWINPUT   1
     1.9 +#endif
    1.10  #define SDL_JOYSTICK_VIRTUAL    1
    1.11  #ifdef SDL_WINDOWS10_SDK
    1.12  #define SDL_JOYSTICK_WGI    1
     2.1 --- a/src/joystick/hidapi/SDL_hidapijoystick.c	Fri Nov 27 11:33:53 2020 -0800
     2.2 +++ b/src/joystick/hidapi/SDL_hidapijoystick.c	Fri Nov 27 13:08:40 2020 -0800
     2.3 @@ -192,7 +192,7 @@
     2.4  #if defined(__WIN32__)
     2.5      SDL_HIDAPI_discovery.m_nThreadID = SDL_ThreadID();
     2.6  
     2.7 -    SDL_memset(&SDL_HIDAPI_discovery.m_wndClass, 0x0, sizeof(SDL_HIDAPI_discovery.m_wndClass));
     2.8 +    SDL_zero(SDL_HIDAPI_discovery.m_wndClass);
     2.9      SDL_HIDAPI_discovery.m_wndClass.hInstance = GetModuleHandle(NULL);
    2.10      SDL_HIDAPI_discovery.m_wndClass.lpszClassName = "SDL_HIDAPI_DEVICE_DETECTION";
    2.11      SDL_HIDAPI_discovery.m_wndClass.lpfnWndProc = ControllerWndProc;      /* This function is called by windows */
    2.12 @@ -203,8 +203,8 @@
    2.13  
    2.14      {
    2.15          DEV_BROADCAST_DEVICEINTERFACE_A devBroadcast;
    2.16 -        SDL_memset( &devBroadcast, 0x0, sizeof( devBroadcast ) );
    2.17  
    2.18 +        SDL_zero(devBroadcast);
    2.19          devBroadcast.dbcc_size = sizeof( devBroadcast );
    2.20          devBroadcast.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
    2.21          devBroadcast.dbcc_classguid = GUID_DEVINTERFACE_USB_DEVICE;
     3.1 --- a/src/joystick/windows/SDL_rawinputjoystick.c	Fri Nov 27 11:33:53 2020 -0800
     3.2 +++ b/src/joystick/windows/SDL_rawinputjoystick.c	Fri Nov 27 13:08:40 2020 -0800
     3.3 @@ -37,6 +37,7 @@
     3.4  #include "SDL_endian.h"
     3.5  #include "SDL_events.h"
     3.6  #include "SDL_hints.h"
     3.7 +#include "SDL_mutex.h"
     3.8  #include "SDL_timer.h"
     3.9  #include "../usb_ids.h"
    3.10  #include "../SDL_sysjoystick.h"
    3.11 @@ -84,12 +85,10 @@
    3.12  #define GIDC_REMOVAL             2
    3.13  #endif
    3.14  
    3.15 -/* external variables referenced. */
    3.16 -extern HWND SDL_HelperWindow;
    3.17 -
    3.18  
    3.19  static SDL_bool SDL_RAWINPUT_inited = SDL_FALSE;
    3.20  static int SDL_RAWINPUT_numjoysticks = 0;
    3.21 +static SDL_mutex *SDL_RAWINPUT_mutex = NULL;
    3.22  
    3.23  static void RAWINPUT_JoystickClose(SDL_Joystick *joystick);
    3.24  
    3.25 @@ -625,40 +624,10 @@
    3.26  #endif /* SDL_JOYSTICK_RAWINPUT_WGI */
    3.27  
    3.28  
    3.29 -/* Most of the time the raw input messages will get dispatched in the main event loop,
    3.30 - * but sometimes we want to get any pending device change messages immediately.
    3.31 - */
    3.32 -static void
    3.33 -RAWINPUT_GetPendingDeviceChanges(void)
    3.34 -{
    3.35 -    MSG msg;
    3.36 -    while (PeekMessage(&msg, SDL_HelperWindow, WM_INPUT_DEVICE_CHANGE, WM_INPUT_DEVICE_CHANGE + 1, PM_REMOVE)) {
    3.37 -        TranslateMessage(&msg);
    3.38 -        DispatchMessage(&msg);
    3.39 -    }
    3.40 -}
    3.41 -
    3.42 -static SDL_bool pump_device_events;
    3.43 -static void
    3.44 -RAWINPUT_GetPendingDeviceInput(void)
    3.45 -{
    3.46 -    if (pump_device_events) {
    3.47 -        MSG msg;
    3.48 -        while (PeekMessage(&msg, SDL_HelperWindow, WM_INPUT, WM_INPUT + 1, PM_REMOVE)) {
    3.49 -            TranslateMessage(&msg);
    3.50 -            DispatchMessage(&msg);
    3.51 -        }
    3.52 -        pump_device_events = SDL_FALSE;
    3.53 -    }
    3.54 -}
    3.55 -
    3.56  static int
    3.57  RAWINPUT_JoystickInit(void)
    3.58  {
    3.59 -    int ii;
    3.60 -    RAWINPUTDEVICE rid[SDL_arraysize(subscribed_devices)];
    3.61      SDL_assert(!SDL_RAWINPUT_inited);
    3.62 -    SDL_assert(SDL_HelperWindow);
    3.63  
    3.64      if (!SDL_GetHintBoolean(SDL_HINT_JOYSTICK_RAWINPUT, SDL_TRUE)) {
    3.65          return -1;
    3.66 @@ -668,25 +637,9 @@
    3.67          return -1;
    3.68      }
    3.69  
    3.70 -    for (ii = 0; ii < SDL_arraysize(subscribed_devices); ii++) {
    3.71 -        rid[ii].usUsagePage = USB_USAGEPAGE_GENERIC_DESKTOP;
    3.72 -        rid[ii].usUsage = subscribed_devices[ii];
    3.73 -        rid[ii].dwFlags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK; /* Receive messages when in background, including device add/remove */
    3.74 -        rid[ii].hwndTarget = SDL_HelperWindow;
    3.75 -    }
    3.76 -
    3.77 -    if (!RegisterRawInputDevices(rid, SDL_arraysize(rid), sizeof(RAWINPUTDEVICE))) {
    3.78 -        SDL_SetError("Couldn't initialize RAWINPUT");
    3.79 -        WIN_UnloadHIDDLL();
    3.80 -        return -1;
    3.81 -    }
    3.82 -
    3.83 +    SDL_RAWINPUT_mutex = SDL_CreateMutex();
    3.84      SDL_RAWINPUT_inited = SDL_TRUE;
    3.85  
    3.86 -    /* Get initial controller connect messages */
    3.87 -    RAWINPUT_GetPendingDeviceChanges();
    3.88 -    pump_device_events = SDL_TRUE;
    3.89 -
    3.90      return 0;
    3.91  }
    3.92  
    3.93 @@ -930,8 +883,6 @@
    3.94      guide_button_candidate.joystick = NULL;
    3.95  
    3.96  #endif /* SDL_JOYSTICK_RAWINPUT_MATCHING */
    3.97 -
    3.98 -    pump_device_events = SDL_TRUE;
    3.99  }
   3.100  
   3.101  SDL_bool
   3.102 @@ -945,9 +896,6 @@
   3.103  {
   3.104      SDL_RAWINPUT_Device *device;
   3.105  
   3.106 -    /* Make sure the device list is completely up to date when we check for device presence */
   3.107 -    RAWINPUT_GetPendingDeviceChanges();
   3.108 -
   3.109      /* If we're being asked about a device, that means another API just detected one, so rescan */
   3.110  #ifdef SDL_JOYSTICK_RAWINPUT_XINPUT
   3.111      xinput_device_change = SDL_TRUE;
   3.112 @@ -983,8 +931,6 @@
   3.113  static void
   3.114  RAWINPUT_JoystickDetect(void)
   3.115  {
   3.116 -    RAWINPUT_GetPendingDeviceChanges();
   3.117 -
   3.118      RAWINPUT_PostUpdate();
   3.119  }
   3.120  
   3.121 @@ -1727,8 +1673,6 @@
   3.122  static void
   3.123  RAWINPUT_JoystickUpdate(SDL_Joystick *joystick)
   3.124  {
   3.125 -    RAWINPUT_GetPendingDeviceInput();
   3.126 -
   3.127      RAWINPUT_UpdateOtherAPIs(joystick);
   3.128  }
   3.129  
   3.130 @@ -1776,74 +1720,115 @@
   3.131      }
   3.132  }
   3.133  
   3.134 -LRESULT RAWINPUT_WindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
   3.135 +SDL_bool
   3.136 +RAWINPUT_RegisterNotifications(HWND hWnd)
   3.137  {
   3.138 -    if (!SDL_RAWINPUT_inited)
   3.139 -        return -1;
   3.140 +    RAWINPUTDEVICE rid[SDL_arraysize(subscribed_devices)];
   3.141 +    int i;
   3.142 +
   3.143 +    for (i = 0; i < SDL_arraysize(subscribed_devices); i++) {
   3.144 +        rid[i].usUsagePage = USB_USAGEPAGE_GENERIC_DESKTOP;
   3.145 +        rid[i].usUsage = subscribed_devices[i];
   3.146 +        rid[i].dwFlags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK; /* Receive messages when in background, including device add/remove */
   3.147 +        rid[i].hwndTarget = hWnd;
   3.148 +    }
   3.149 +
   3.150 +    if (!RegisterRawInputDevices(rid, SDL_arraysize(rid), sizeof(RAWINPUTDEVICE))) {
   3.151 +        SDL_SetError("Couldn't register for raw input events");
   3.152 +        return SDL_FALSE;
   3.153 +    }
   3.154 +    return SDL_TRUE;
   3.155 +}
   3.156 +
   3.157 +void
   3.158 +RAWINPUT_UnregisterNotifications()
   3.159 +{
   3.160 +    int i;
   3.161 +    RAWINPUTDEVICE rid[SDL_arraysize(subscribed_devices)];
   3.162  
   3.163 -    switch (msg)
   3.164 -    {
   3.165 -        case WM_INPUT_DEVICE_CHANGE:
   3.166 -        {
   3.167 -            HANDLE hDevice = (HANDLE)lParam;
   3.168 -            switch (wParam) {
   3.169 -            case GIDC_ARRIVAL:
   3.170 -                RAWINPUT_AddDevice(hDevice);
   3.171 -                break;
   3.172 -            case GIDC_REMOVAL: {
   3.173 -                SDL_RAWINPUT_Device *device;
   3.174 -                device = RAWINPUT_DeviceFromHandle(hDevice);
   3.175 -                if (device) {
   3.176 -                    RAWINPUT_DelDevice(device, SDL_TRUE);
   3.177 +    for (i = 0; i < SDL_arraysize(subscribed_devices); i++) {
   3.178 +        rid[i].usUsagePage = USB_USAGEPAGE_GENERIC_DESKTOP;
   3.179 +        rid[i].usUsage = subscribed_devices[i];
   3.180 +        rid[i].dwFlags = RIDEV_REMOVE;
   3.181 +        rid[i].hwndTarget = NULL;
   3.182 +    }
   3.183 +
   3.184 +    if (!RegisterRawInputDevices(rid, SDL_arraysize(rid), sizeof(RAWINPUTDEVICE))) {
   3.185 +        SDL_SetError("Couldn't unregister for raw input events");
   3.186 +        return;
   3.187 +    }
   3.188 +}
   3.189 +    
   3.190 +LRESULT CALLBACK
   3.191 +RAWINPUT_WindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
   3.192 +{
   3.193 +    LRESULT result = -1;
   3.194 +
   3.195 +    SDL_LockMutex(SDL_RAWINPUT_mutex);
   3.196 +
   3.197 +    if (SDL_RAWINPUT_inited) {
   3.198 +        switch (msg) {
   3.199 +            case WM_INPUT_DEVICE_CHANGE:
   3.200 +            {
   3.201 +                HANDLE hDevice = (HANDLE)lParam;
   3.202 +                switch (wParam) {
   3.203 +                case GIDC_ARRIVAL:
   3.204 +                    RAWINPUT_AddDevice(hDevice);
   3.205 +                    break;
   3.206 +                case GIDC_REMOVAL:
   3.207 +                {
   3.208 +                    SDL_RAWINPUT_Device *device;
   3.209 +                    device = RAWINPUT_DeviceFromHandle(hDevice);
   3.210 +                    if (device) {
   3.211 +                        RAWINPUT_DelDevice(device, SDL_TRUE);
   3.212 +                    }
   3.213 +                    break;
   3.214                  }
   3.215 -            } break;
   3.216 -            default:
   3.217 -                return 0;
   3.218 +                default:
   3.219 +                    break;
   3.220 +                }
   3.221              }
   3.222 -        }
   3.223 -        return 0;
   3.224 +            result = 0;
   3.225 +            break;
   3.226  
   3.227 -        case WM_INPUT:
   3.228 -        {
   3.229 -            Uint8 data[sizeof(RAWINPUTHEADER) + sizeof(RAWHID) + USB_PACKET_LENGTH];
   3.230 -            UINT buffer_size = SDL_arraysize(data);
   3.231 +            case WM_INPUT:
   3.232 +            {
   3.233 +                Uint8 data[sizeof(RAWINPUTHEADER) + sizeof(RAWHID) + USB_PACKET_LENGTH];
   3.234 +                UINT buffer_size = SDL_arraysize(data);
   3.235  
   3.236 -            if ((int)GetRawInputData((HRAWINPUT)lParam, RID_INPUT, data, &buffer_size, sizeof(RAWINPUTHEADER)) > 0) {
   3.237 -                PRAWINPUT raw_input = (PRAWINPUT)data;
   3.238 -                SDL_RAWINPUT_Device *device = RAWINPUT_DeviceFromHandle(raw_input->header.hDevice);
   3.239 -                if (device) {
   3.240 -                    SDL_Joystick *joystick = device->joystick;
   3.241 -                    if (joystick) {
   3.242 -                        RAWINPUT_HandleStatePacket(joystick, raw_input->data.hid.bRawData, raw_input->data.hid.dwSizeHid);
   3.243 +                if ((int)GetRawInputData((HRAWINPUT)lParam, RID_INPUT, data, &buffer_size, sizeof(RAWINPUTHEADER)) > 0) {
   3.244 +                    PRAWINPUT raw_input = (PRAWINPUT)data;
   3.245 +                    SDL_RAWINPUT_Device *device = RAWINPUT_DeviceFromHandle(raw_input->header.hDevice);
   3.246 +                    if (device) {
   3.247 +                        SDL_Joystick *joystick = device->joystick;
   3.248 +                        if (joystick) {
   3.249 +                            RAWINPUT_HandleStatePacket(joystick, raw_input->data.hid.bRawData, raw_input->data.hid.dwSizeHid);
   3.250 +                        }
   3.251                      }
   3.252                  }
   3.253              }
   3.254 +            result = 0;
   3.255 +            break;
   3.256          }
   3.257 -        return 0;
   3.258      }
   3.259 -    return -1;
   3.260 +
   3.261 +    SDL_UnlockMutex(SDL_RAWINPUT_mutex);
   3.262 +
   3.263 +    if (result >= 0) {
   3.264 +        return result;
   3.265 +    }
   3.266 +    return CallWindowProc(DefWindowProc, hWnd, msg, wParam, lParam);
   3.267  }
   3.268  
   3.269  static void
   3.270  RAWINPUT_JoystickQuit(void)
   3.271  {
   3.272 -    int ii;
   3.273 -    RAWINPUTDEVICE rid[SDL_arraysize(subscribed_devices)];
   3.274 -    
   3.275 -    if (!SDL_RAWINPUT_inited)
   3.276 +    if (!SDL_RAWINPUT_inited) {
   3.277          return;
   3.278 -
   3.279 -    for (ii = 0; ii < SDL_arraysize(subscribed_devices); ii++) {
   3.280 -        rid[ii].usUsagePage = USB_USAGEPAGE_GENERIC_DESKTOP;
   3.281 -        rid[ii].usUsage = subscribed_devices[ii];
   3.282 -        rid[ii].dwFlags = RIDEV_REMOVE;
   3.283 -        rid[ii].hwndTarget = NULL;
   3.284      }
   3.285  
   3.286 -    if (!RegisterRawInputDevices(rid, SDL_arraysize(rid), sizeof(RAWINPUTDEVICE))) {
   3.287 -        SDL_Log("Couldn't un-register RAWINPUT");
   3.288 -    }
   3.289 -    
   3.290 +    SDL_LockMutex(SDL_RAWINPUT_mutex);
   3.291 +
   3.292      while (SDL_RAWINPUT_devices) {
   3.293          RAWINPUT_DelDevice(SDL_RAWINPUT_devices, SDL_FALSE);
   3.294      }
   3.295 @@ -1853,6 +1838,11 @@
   3.296      SDL_RAWINPUT_numjoysticks = 0;
   3.297  
   3.298      SDL_RAWINPUT_inited = SDL_FALSE;
   3.299 +
   3.300 +    SDL_UnlockMutex(SDL_RAWINPUT_mutex);
   3.301 +    SDL_DestroyMutex(SDL_RAWINPUT_mutex);
   3.302 +    SDL_RAWINPUT_mutex = NULL;
   3.303 +
   3.304  }
   3.305  
   3.306  static SDL_bool
     4.1 --- a/src/joystick/windows/SDL_rawinputjoystick_c.h	Fri Nov 27 11:33:53 2020 -0800
     4.2 +++ b/src/joystick/windows/SDL_rawinputjoystick_c.h	Fri Nov 27 13:08:40 2020 -0800
     4.3 @@ -27,8 +27,12 @@
     4.4  /* Return true if a RawInput device is present and supported as a joystick */
     4.5  extern SDL_bool RAWINPUT_IsDevicePresent(Uint16 vendor_id, Uint16 product_id, Uint16 version, const char *name);
     4.6  
     4.7 +/* Registers for input events */
     4.8 +extern SDL_bool RAWINPUT_RegisterNotifications(HWND hWnd);
     4.9 +extern void RAWINPUT_UnregisterNotifications();
    4.10 +
    4.11  /* Returns 0 if message was handled */
    4.12 -extern LRESULT RAWINPUT_WindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
    4.13 +extern LRESULT CALLBACK RAWINPUT_WindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
    4.14  
    4.15  
    4.16  /* vi: set ts=4 sw=4 expandtab: */
     5.1 --- a/src/joystick/windows/SDL_windowsjoystick.c	Fri Nov 27 11:33:53 2020 -0800
     5.2 +++ b/src/joystick/windows/SDL_windowsjoystick.c	Fri Nov 27 13:08:40 2020 -0800
     5.3 @@ -49,6 +49,7 @@
     5.4  #include "SDL_windowsjoystick_c.h"
     5.5  #include "SDL_dinputjoystick_c.h"
     5.6  #include "SDL_xinputjoystick_c.h"
     5.7 +#include "SDL_rawinputjoystick_c.h"
     5.8  
     5.9  #include "../../haptic/windows/SDL_dinputhaptic_c.h"    /* For haptic hot plugging */
    5.10  #include "../../haptic/windows/SDL_xinputhaptic_c.h"    /* For haptic hot plugging */
    5.11 @@ -109,9 +110,9 @@
    5.12  
    5.13  /* windowproc for our joystick detect thread message only window, to detect any USB device addition/removal */
    5.14  static LRESULT CALLBACK
    5.15 -SDL_PrivateJoystickDetectProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
    5.16 +SDL_PrivateJoystickDetectProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
    5.17  {
    5.18 -    switch (message) {
    5.19 +    switch (msg) {
    5.20      case WM_DEVICECHANGE:
    5.21          switch (wParam) {
    5.22          case DBT_DEVICEARRIVAL:
    5.23 @@ -130,12 +131,20 @@
    5.24          return 0;
    5.25      }
    5.26  
    5.27 -    return DefWindowProc (hwnd, message, wParam, lParam);
    5.28 +#if SDL_JOYSTICK_RAWINPUT
    5.29 +    return CallWindowProc(RAWINPUT_WindowProc, hwnd, msg, wParam, lParam);
    5.30 +#else
    5.31 +    return CallWindowProc(DefWindowProc, hwnd, msg, wParam, lParam);
    5.32 +#endif
    5.33  }
    5.34  
    5.35  static void
    5.36  SDL_CleanupDeviceNotification(SDL_DeviceNotificationData *data)
    5.37  {
    5.38 +#if SDL_JOYSTICK_RAWINPUT
    5.39 +    RAWINPUT_UnregisterNotifications();
    5.40 +#endif
    5.41 +
    5.42      if (data->hNotify)
    5.43          UnregisterDeviceNotification(data->hNotify);
    5.44  
    5.45 @@ -188,6 +197,10 @@
    5.46          SDL_CleanupDeviceNotification(data);
    5.47          return -1;
    5.48      }
    5.49 +
    5.50 +#if SDL_JOYSTICK_RAWINPUT
    5.51 +    RAWINPUT_RegisterNotifications(data->messageWindow);
    5.52 +#endif
    5.53      return 0;
    5.54  }
    5.55  
     6.1 --- a/src/video/windows/SDL_windowswindow.c	Fri Nov 27 11:33:53 2020 -0800
     6.2 +++ b/src/video/windows/SDL_windowswindow.c	Fri Nov 27 13:08:40 2020 -0800
     6.3 @@ -30,7 +30,6 @@
     6.4  #include "../../events/SDL_keyboard_c.h"
     6.5  #include "../../events/SDL_mouse_c.h"
     6.6  
     6.7 -#include "../../joystick/windows/SDL_rawinputjoystick_c.h"
     6.8  #include "SDL_windowsvideo.h"
     6.9  #include "SDL_windowswindow.h"
    6.10  #include "SDL_hints.h"
    6.11 @@ -811,18 +810,8 @@
    6.12      }
    6.13  }
    6.14  
    6.15 -static LRESULT CALLBACK SDL_HelperWindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
    6.16 -{
    6.17 -#if SDL_JOYSTICK_RAWINPUT
    6.18 -    if (RAWINPUT_WindowProc(hWnd, msg, wParam, lParam) == 0) {
    6.19 -        return 0;
    6.20 -    }
    6.21 -#endif
    6.22 -    return DefWindowProc(hWnd, msg, wParam, lParam);
    6.23 -}
    6.24 -
    6.25  /*
    6.26 - * Creates a HelperWindow used for DirectInput and RawInput events.
    6.27 + * Creates a HelperWindow used for DirectInput.
    6.28   */
    6.29  int
    6.30  SDL_HelperWindowCreate(void)
    6.31 @@ -837,7 +826,7 @@
    6.32  
    6.33      /* Create the class. */
    6.34      SDL_zero(wce);
    6.35 -    wce.lpfnWndProc = SDL_GetHintBoolean(SDL_HINT_JOYSTICK_RAWINPUT, SDL_TRUE) ? SDL_HelperWindowProc : DefWindowProc;
    6.36 +    wce.lpfnWndProc = DefWindowProc;
    6.37      wce.lpszClassName = (LPCWSTR) SDL_HelperWindowClassName;
    6.38      wce.hInstance = hInstance;
    6.39