Bladeren bron

cocoadisplay: Add support for high-dpi screens (#1308)

LD 2 jaren geleden
bovenliggende
commit
ae3cbe4b12

+ 1 - 0
direct/src/dist/commands.py

@@ -717,6 +717,7 @@ class build_apps(setuptools.Command):
             'CFBundlePackageType': 'APPL',
             'CFBundleSignature': '', #TODO
             'CFBundleExecutable': self.macos_main_app,
+            'NSHighResolutionCapable': 'True',
         }
 
         icon = self.icon_objects.get(

+ 46 - 5
panda/src/cocoadisplay/cocoaGraphicsPipe.mm

@@ -40,12 +40,36 @@ CocoaGraphicsPipe(CGDirectDisplayID display) : _display(display) {
   [thread start];
   [thread autorelease];
 
+  // If the application is dpi-aware, iterate over all the screens to find the
+  // one with our display ID and get the backing scale factor to configure the
+  // detected display zoom. Otherwise the detected display zoom keeps its
+  // default value of 1.0
+
+  if (dpi_aware) {
+    NSScreen *screen;
+    NSEnumerator *e = [[NSScreen screens] objectEnumerator];
+    while (screen = (NSScreen *) [e nextObject]) {
+      NSNumber *num = [[screen deviceDescription] objectForKey: @"NSScreenNumber"];
+      if (_display == (CGDirectDisplayID) [num longValue]) {
+        set_detected_display_zoom([screen backingScaleFactor]);
+        if (cocoadisplay_cat.is_debug()) {
+          cocoadisplay_cat.debug()
+            << "Display zoom is " << [screen backingScaleFactor] << "\n";
+        }
+        break;
+      }
+    }
+  }
+
   // We used to also obtain the corresponding NSScreen here, but this causes
   // the application icon to start bouncing, which may be undesirable for
   // apps that will never open a window.
 
-  _display_width = CGDisplayPixelsWide(_display);
-  _display_height = CGDisplayPixelsHigh(_display);
+  // Although the name of these functions mention pixels, they actually return
+  // display points, we use the detected display zoom to transform the values
+  // into pixels.
+  _display_width = CGDisplayPixelsWide(_display) * _detected_display_zoom;
+  _display_height = CGDisplayPixelsHigh(_display) * _detected_display_zoom;
   load_display_information();
 
   if (cocoadisplay_cat.is_debug()) {
@@ -64,19 +88,36 @@ load_display_information() {
   // _display_information->_device_id = CGDisplaySerialNumber(_display);
 
   // Display modes
+  CFDictionaryRef options = NULL;
+  const CFStringRef dictkeys[] = {kCGDisplayShowDuplicateLowResolutionModes};
+  const CFBooleanRef dictvalues[] = {kCFBooleanTrue};
+  options = CFDictionaryCreate(NULL,
+                               (const void **)dictkeys,
+                               (const void **)dictvalues,
+                               1,
+                               &kCFCopyStringDictionaryKeyCallBacks,
+                               &kCFTypeDictionaryValueCallBacks);
   size_t num_modes = 0;
-  CFArrayRef modes = CGDisplayCopyAllDisplayModes(_display, NULL);
+  CFArrayRef modes = CGDisplayCopyAllDisplayModes(_display, options);
   if (modes != NULL) {
     num_modes = CFArrayGetCount(modes);
     _display_information->_total_display_modes = num_modes;
     _display_information->_display_mode_array = new DisplayMode[num_modes];
   }
+  if (options != NULL) {
+    CFRelease(options);
+  }
 
   for (size_t i = 0; i < num_modes; ++i) {
     CGDisplayModeRef mode = (CGDisplayModeRef) CFArrayGetValueAtIndex(modes, i);
 
-    _display_information->_display_mode_array[i].width = CGDisplayModeGetWidth(mode);
-    _display_information->_display_mode_array[i].height = CGDisplayModeGetHeight(mode);
+    if (dpi_aware) {
+      _display_information->_display_mode_array[i].width = CGDisplayModeGetPixelWidth(mode);
+      _display_information->_display_mode_array[i].height = CGDisplayModeGetPixelHeight(mode);
+    } else {
+      _display_information->_display_mode_array[i].width = CGDisplayModeGetWidth(mode);
+      _display_information->_display_mode_array[i].height = CGDisplayModeGetHeight(mode);
+    }
     _display_information->_display_mode_array[i].refresh_rate = CGDisplayModeGetRefreshRate(mode);
     _display_information->_display_mode_array[i].fullscreen_only = false;
 

+ 1 - 0
panda/src/cocoadisplay/cocoaGraphicsWindow.h

@@ -63,6 +63,7 @@ public:
   void handle_minimize_event(bool minimized);
   void handle_maximize_event(bool maximized);
   void handle_foreground_event(bool foreground);
+  void handle_backing_change_event();
   bool handle_close_request();
   void handle_close_event();
   void handle_key_event(NSEvent *event);

+ 209 - 105
panda/src/cocoadisplay/cocoaGraphicsWindow.mm

@@ -132,13 +132,20 @@ move_pointer(int device, int x, int y) {
     return true;
   }
 
+  // Mouse position is expressed in screen points and not pixels, but in Panda3D
+  // we are using pixel coordinates.
+  // Instead of using convertPointFromBacking and have complex logic to cope with
+  // the change of coordinate system, we cheat and directly use the contents scale
+  // of the view layer to convert pixel coordinates into screen point coordinates.
+  CGFloat contents_scale = _view.layer.contentsScale;
   if (device == 0) {
     CGPoint point;
     if (_properties.get_fullscreen()) {
-      point = CGPointMake(x, y);
+      point = CGPointMake(float(x) / contents_scale,
+                          float(y) / contents_scale);
     } else {
-      point = CGPointMake(x + _properties.get_x_origin(),
-                          y + _properties.get_y_origin());
+      point = CGPointMake((float(x) + _properties.get_x_origin()) / contents_scale,
+                          (float(y) + _properties.get_y_origin()) / contents_scale);
     }
 
     if (CGWarpMouseCursorPosition(point) == kCGErrorSuccess) {
@@ -302,35 +309,89 @@ open_window() {
     }
   }
 
+  // Configure the origin and the size of the window.
+  // On macOS, screen coordinates are expressed in "points" which are independant
+  // of the pixel density of the screen. Panda3D however, expresses the size and
+  // origin of a window in pixels.
+  // So, when opening a window, we need to convert the origin and size from pixel
+  // units into point units. However, this conversion depends on the pixel density
+  // of the screen, the backing scale factor.
+  // As the origin and size of a window depends on the size of the screen of the
+  // parent view, their size must be converted first from points to pixels.
+  // If a window (or a view) is not configured to support high-dpi screen, macOS
+  // will upscale the window (or view) when displayed on a high-dpi screen.
+  // Therefore its backing scale factor will always be 1.0
+  // In a Panda3D application, windows are always configured as high resolution
+  // capable, but the view is only configured as high resolution if the dpi-aware
+  // configuration flag is set.
+  // If the app is not dpi-aware, we must upscale its size and origin from points
+  // into pixels as the window is always high resolution capable.
+
   // Center the window if coordinates were set to -1 or -2 TODO: perhaps in
   // future, in the case of -1, it should use the origin used in a previous
   // run of Panda
+
+  // Size of the requested window
+  NSSize size = NSMakeSize(_properties.get_x_size(), _properties.get_y_size());
   NSRect container;
+  CGFloat backing_scale_factor = screen.backingScaleFactor;
   if (parent_nsview != NULL) {
-    container = [parent_nsview bounds];
+    // Convert parent view bounds into pixel units.
+    container = [parent_nsview convertRectToBacking:[parent_nsview bounds]];
+    // If the app is not dpi-aware, we must convert its size from points into
+    // pixels as the window is always high resolution capable
+    if (!dpi_aware) {
+      size = [parent_nsview convertSizeToBacking:size];
+    }
   } else {
     container = [screen frame];
     container.origin = NSMakePoint(0, 0);
+    container = [screen convertRectToBacking:container];
+    if (!dpi_aware) {
+      // Weirdly NSScreen does not respond to convertSizeToBacking, so we have to
+      // create a dummy rect just for converting the window size.
+      NSRect rect;
+      rect.origin = NSMakePoint(0, 0);
+      rect.size = size;
+      rect = [screen convertRectToBacking:rect];
+      size = rect.size;
+    }
   }
   int x = _properties.get_x_origin();
   int y = _properties.get_y_origin();
 
+  // As we are converting a single value and the view is not created yet, it's
+  // easier to simply use the backing  scale factor and don't bother with
+  // coordinate system transformations.
   if (x < 0) {
-    x = floor(container.size.width / 2 - _properties.get_x_size() / 2);
+    x = floor(container.size.width / 2 - size.width / 2);
+  } else if (!dpi_aware) {
+    x *= backing_scale_factor;
   }
   if (y < 0) {
-    y = floor(container.size.height / 2 - _properties.get_y_size() / 2);
+    y = floor(container.size.height / 2 - size.height / 2);
+  } else if (!dpi_aware) {
+    y *= backing_scale_factor;
+  }
+  if (dpi_aware) {
+    _properties.set_origin(x, y);
+  } else {
+    _properties.set_origin(x / backing_scale_factor, y / backing_scale_factor);
   }
-  _properties.set_origin(x, y);
 
   if (_parent_window_handle == (WindowHandle *)NULL) {
     // Content rectangle
     NSRect rect;
     if (_properties.get_fullscreen()) {
-      rect = container;
+      rect = [screen convertRectFromBacking:container];
     } else {
-      rect = NSMakeRect(x, container.size.height - _properties.get_y_size() - y,
-                        _properties.get_x_size(), _properties.get_y_size());
+      rect = NSMakeRect(x, container.size.height - size.height - y,
+                        size.width, size.height);
+      if (parent_nsview != NULL) {
+        rect = [parent_nsview convertRectFromBacking:rect];
+      } else {
+        rect = [screen convertRectFromBacking:rect];
+      }
     }
 
     // Configure the window decorations
@@ -388,8 +449,10 @@ open_window() {
     _parent_window_handle->attach_child(_window_handle);
   }
 
-  // Always disable application HiDPI support, Cocoa will do the eventual upscaling for us.
-  [_view setWantsBestResolutionOpenGLSurface:NO];
+  // Configure the view to be high resolution capable using the dpi-aware
+  // configuration flag. If dpi-aware is false, macOS will upscale the view
+  // for us.
+  [_view setWantsBestResolutionOpenGLSurface:dpi_aware];
   if (_properties.has_icon_filename()) {
     NSImage *image = load_image(_properties.get_icon_filename());
     if (image != nil) {
@@ -596,22 +659,6 @@ set_properties_now(WindowProperties &properties) {
           }
 
           if (switched) {
-            if (_window != nil) {
-              // For some reason, setting the style mask makes it give up its
-              // first-responder status.  And for some reason, we need to first
-              // restore the window to normal level before we switch fullscreen,
-              // otherwise we may get a black bar if we're currently on Z_top.
-              if (_properties.get_z_order() != WindowProperties::Z_normal) {
-                [_window setLevel: NSNormalWindowLevel];
-              }
-              if ([_window respondsToSelector:@selector(setStyleMask:)]) {
-                [_window setStyleMask:NSBorderlessWindowMask];
-              }
-              [_window makeFirstResponder:_view];
-              [_window setLevel:CGShieldingWindowLevel()];
-              [_window makeKeyAndOrderFront:nil];
-            }
-
             // We've already set the size property this way; clear it.
             properties.clear_size();
             _properties.set_size(width, height);
@@ -684,6 +731,9 @@ set_properties_now(WindowProperties &properties) {
                                 NSMiniaturizableWindowMask | NSResizableWindowMask ];
         }
         [_window makeFirstResponder:_view];
+        // Resize event fired by makeFirstResponder has an invalid backing scale factor
+        // The actual size must be reset afterward
+        handle_resize_event();
       }
     }
 
@@ -705,6 +755,9 @@ set_properties_now(WindowProperties &properties) {
                                NSMiniaturizableWindowMask | NSResizableWindowMask ];
       }
       [_window makeFirstResponder:_view];
+      // Resize event fired by makeFirstResponder has an invalid backing scale factor
+      // The actual size must be reset afterward
+      handle_resize_event();
     }
 
     properties.clear_undecorated();
@@ -715,10 +768,13 @@ set_properties_now(WindowProperties &properties) {
     int height = properties.get_y_size();
 
     if (!_properties.get_fullscreen()) {
+      // We use the view, not the window, to convert the frame size, expressed
+      // in pixels, into points as the "dpi awareness" is managed by the view.
+      NSSize size = [_view convertSizeFromBacking:NSMakeSize(width, height)];
       if (_window != nil) {
-        [_window setContentSize:NSMakeSize(width, height)];
+        [_window setContentSize:size];
       }
-      [_view setFrameSize:NSMakeSize(width, height)];
+      [_view setFrameSize:size];
 
       if (cocoadisplay_cat.is_debug()) {
         cocoadisplay_cat.debug()
@@ -768,12 +824,14 @@ set_properties_now(WindowProperties &properties) {
     // Get the frame for the screen
     NSRect frame;
     NSRect container;
+    // Note again that we are using the view to convert the frame and container
+    // size from points into pixels.
     if (_window != nil) {
       NSRect window_frame = [_window frame];
-      frame = [_window contentRectForFrameRect:window_frame];
+      frame = [_view convertRectToBacking:[_window contentRectForFrameRect:window_frame]];
       NSScreen *screen = [_window screen];
       nassertv(screen != nil);
-      container = [screen frame];
+      container = [_view convertRectToBacking:[screen frame]];
 
       // Prevent the centering from overlapping the Dock
       if (y < 0) {
@@ -783,8 +841,8 @@ set_properties_now(WindowProperties &properties) {
         }
       }
     } else {
-      frame = [_view frame];
-      container = [[_view superview] frame];
+      frame = [_view convertRectToBacking:[_view frame]];
+      container = [[_view superview] convertRectToBacking:[[_view superview] frame]];
     }
 
     if (x < 0) {
@@ -795,22 +853,22 @@ set_properties_now(WindowProperties &properties) {
     }
     _properties.set_origin(x, y);
 
-    if (!_properties.get_fullscreen()) {
-      // Remember, Mac OS X coordinates are flipped in the vertical axis.
-      frame.origin.x = x;
-      frame.origin.y = container.size.height - y - frame.size.height;
+    frame.origin.x = x;
+    // Y coordinate in backing store is not flipped, but origin is still at the bottom left
+    frame.origin.y = y - container.size.height;
 
-      if (cocoadisplay_cat.is_debug()) {
-        cocoadisplay_cat.debug()
-          << "Setting window content origin to "
-          << frame.origin.x << ", " << frame.origin.y << "\n";
-      }
+    if (cocoadisplay_cat.is_debug()) {
+      cocoadisplay_cat.debug()
+        << "Setting window content origin to "
+        << frame.origin.x << ", " << frame.origin.y << "\n";
+    }
 
-      if (_window != nil) {
-        [_window setFrame:[_window frameRectForContentRect:frame] display:NO];
-      } else {
-        [_view setFrame:frame];
-      }
+    if (_window != nil) {
+      frame = [_view convertRectFromBacking:frame];
+      [_window setFrame:[_window frameRectForContentRect:frame] display:NO];
+    } else {
+      frame = [_view convertRectFromBacking:frame];
+      [_view setFrame:frame];
     }
     properties.clear_origin();
   }
@@ -957,24 +1015,20 @@ unbind_context() {
 CFMutableArrayRef CocoaGraphicsWindow::
 find_display_modes(int width, int height) {
   CFDictionaryRef options = NULL;
-  // On macOS 10.15+ (Catalina), we want to select the display mode with the
-  // samescaling factor as the current view to avoid cropping or scaling issues.
-  // This is a workaround until HiDPI display or scaling factor is properly
-  // handled. CGDisplayCopyAllDisplayModes() does not return upscaled display
-  // mode unless explicitly asked with kCGDisplayShowDuplicateLowResolutionModes
+  // We want to select the display mode with the same scaling factor as the
+  // current view to avoid cropping or scaling issues.
+  // CGDisplayCopyAllDisplayModes() does not return upscaled display modes
+  // nor the current mode, unless explicitly asked with
+  // kCGDisplayShowDuplicateLowResolutionModes
   // (which is undocumented...).
-  bool macos_10_15_or_higher = false;
-  if (@available(macOS 10.15, *)) {
-    const CFStringRef dictkeys[] = {kCGDisplayShowDuplicateLowResolutionModes};
-    const CFBooleanRef dictvalues[] = {kCFBooleanTrue};
-    options = CFDictionaryCreate(NULL,
-                                 (const void **)dictkeys,
-                                 (const void **)dictvalues,
-                                 1,
-                                 &kCFCopyStringDictionaryKeyCallBacks,
-                                 &kCFTypeDictionaryValueCallBacks);
-    macos_10_15_or_higher = true;
-  }
+  const CFStringRef dictkeys[] = {kCGDisplayShowDuplicateLowResolutionModes};
+  const CFBooleanRef dictvalues[] = {kCFBooleanTrue};
+  options = CFDictionaryCreate(NULL,
+                               (const void **)dictkeys,
+                               (const void **)dictvalues,
+                               1,
+                               &kCFCopyStringDictionaryKeyCallBacks,
+                               &kCFTypeDictionaryValueCallBacks);
   CFMutableArrayRef valid_modes;
   valid_modes = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
 
@@ -985,29 +1039,39 @@ find_display_modes(int width, int height) {
 
   size_t num_modes = CFArrayGetCount(modes);
   CGDisplayModeRef mode;
-
-  // Get the current refresh rate and pixel encoding.
-  CFStringRef current_pixel_encoding;
-  double refresh_rate;
   mode = CGDisplayCopyDisplayMode(_display);
 
+  // Calculate requested display size and pixel size
+  CGSize display_size;
+  CGSize pixel_size;
+  if (dpi_aware) {
+    pixel_size = NSMakeSize(width, height);
+    display_size = [_view convertSizeFromBacking:pixel_size];
+  } else {
+    display_size = NSMakeSize(width, height);
+    // Calculate the pixel width and height of the fullscreen mode we want using
+    // the current display mode dimensions and pixel dimensions.
+    size_t pixel_width = (size_t(width) * CGDisplayModeGetPixelWidth(mode)) / CGDisplayModeGetWidth(mode);
+    size_t pixel_height = (size_t(height) * CGDisplayModeGetPixelHeight(mode)) / CGDisplayModeGetHeight(mode);
+    pixel_size = NSMakeSize(pixel_width, pixel_height);
+  }
+
   // First check if the current mode is adequate.
-  // This test not done for macOS 10.15 and above as the mode resolution is
-  // not enough to identify a mode.
-  if (!macos_10_15_or_higher &&
-      CGDisplayModeGetWidth(mode) == width &&
-      CGDisplayModeGetHeight(mode) == height) {
+  if (CGDisplayModeGetWidth(mode) == display_size.width &&
+      CGDisplayModeGetHeight(mode) == display_size.height &&
+      CGDisplayModeGetPixelWidth(mode) == pixel_size.width &&
+      CGDisplayModeGetPixelHeight(mode) == pixel_size.height) {
     CFArrayAppendValue(valid_modes, mode);
     CGDisplayModeRelease(mode);
     return valid_modes;
   }
 
+  // Get the current refresh rate and pixel encoding.
+  CFStringRef current_pixel_encoding;
+  double refresh_rate;
+
   current_pixel_encoding = CGDisplayModeCopyPixelEncoding(mode);
   refresh_rate = CGDisplayModeGetRefreshRate(mode);
-  // Calculate the pixel width and height of the fullscreen mode we want using
-  // the currentdisplay mode dimensions and pixel dimensions.
-  size_t expected_pixel_width = (size_t(width) * CGDisplayModeGetPixelWidth(mode)) / CGDisplayModeGetWidth(mode);
-  size_t expected_pixel_height = (size_t(height) * CGDisplayModeGetPixelHeight(mode)) / CGDisplayModeGetHeight(mode);
   CGDisplayModeRelease(mode);
 
   for (size_t i = 0; i < num_modes; ++i) {
@@ -1015,17 +1079,15 @@ find_display_modes(int width, int height) {
 
     CFStringRef pixel_encoding = CGDisplayModeCopyPixelEncoding(mode);
 
-    // As explained above, we want to select the fullscreen display mode using
-    // the same scaling factor, but only for MacOS 10.15+ To do this we check
-    // the mode width and height but also actual pixel widh and height.
-    if (CGDisplayModeGetWidth(mode) == width &&
-        CGDisplayModeGetHeight(mode) == height &&
+    // We select the fullscreen display mode using he same scaling factor
+    // To do this we check the mode width and height but also actual pixel widh
+    // and height.
+    if (CGDisplayModeGetWidth(mode) == display_size.width &&
+        CGDisplayModeGetHeight(mode) == display_size.height &&
         (int)(CGDisplayModeGetRefreshRate(mode) + 0.5) == (int)(refresh_rate + 0.5) &&
-        (!macos_10_15_or_higher ||
-        (CGDisplayModeGetPixelWidth(mode) == expected_pixel_width &&
-         CGDisplayModeGetPixelHeight(mode) == expected_pixel_height)) &&
+        CGDisplayModeGetPixelWidth(mode) == pixel_size.width &&
+        CGDisplayModeGetPixelHeight(mode) == pixel_size.height &&
         CFStringCompare(pixel_encoding, current_pixel_encoding, 0) == kCFCompareEqualTo) {
-
       if (CGDisplayModeGetRefreshRate(mode) == refresh_rate) {
         // Exact match for refresh rate, prioritize this.
         CFArrayInsertValueAtIndex(valid_modes, 0, mode);
@@ -1103,14 +1165,25 @@ do_switch_fullscreen(CGDisplayModeRef mode) {
 
     NSRect frame = [[[_view window] screen] frame];
     if (cocoadisplay_cat.is_debug()) {
-      NSString *str = NSStringFromRect(frame);
+      NSString *str = NSStringFromSize([_view convertSizeToBacking:frame.size]);
       cocoadisplay_cat.debug()
-        << "Switched to fullscreen, screen rect is now " << [str UTF8String] << "\n";
+        << "Switched to fullscreen, screen size is now " << [str UTF8String] << "\n";
     }
 
     if (_window != nil) {
+      // For some reason, setting the style mask makes it give up its
+      // first-responder status.
+      if ([_window respondsToSelector:@selector(setStyleMask:)]) {
+        [_window setStyleMask:NSBorderlessWindowMask];
+      }
+      [_window makeFirstResponder:_view];
+      [_window setLevel:CGShieldingWindowLevel()];
+      [_window makeKeyAndOrderFront:nil];
+
+      // Window and view frame must be updated *after* the window reconfiguration
+      // or the size is not set properly !
       [_window setFrame:frame display:YES];
-      [_view setFrame:NSMakeRect(0, 0, frame.size.width, frame.size.height)];
+      [_view setFrame:frame];
       [_window update];
     }
   }
@@ -1252,19 +1325,25 @@ load_cursor(const Filename &filename) {
  */
 void CocoaGraphicsWindow::
 handle_move_event() {
-  // Remember, Mac OS X uses flipped coordinates
   NSRect frame;
+  NSRect container;
   int x, y;
+  // Again, we are using the view to convert the frame and container size from
+  // points to pixels.
   if (_window == nil) {
-    frame = [_view frame];
-    x = frame.origin.x;
-    y = [[_view superview] bounds].size.height - frame.origin.y - frame.size.height;
+    frame = [_view convertRectToBacking:[_view frame]];
+    container = [_view convertRectToBacking:[[_view superview] frame]];
   } else {
-    frame = [_window contentRectForFrameRect:[_window frame]];
-    x = frame.origin.x;
-    y = [[_window screen] frame].size.height - frame.origin.y - frame.size.height;
+    frame = [_view convertRectToBacking:[_window contentRectForFrameRect:[_window frame]]];
+    NSScreen *screen = [_window screen];
+    nassertv(screen != nil);
+    container = [_view convertRectToBacking:[screen frame]];
   }
 
+  // Y coordinate in backing store is not flipped, but origin is still at the bottom left
+  x = frame.origin.x;
+  y = container.size.height + frame.origin.y;
+
   if (x != _properties.get_x_origin() ||
       y != _properties.get_y_origin()) {
 
@@ -1290,7 +1369,7 @@ handle_resize_event() {
     [_view setFrameSize:contentRect.size];
   }
 
-  NSRect frame = [_view convertRect:[_view bounds] toView:nil];
+  NSRect frame = [_view convertRectToBacking:[_view bounds]];
 
   WindowProperties properties;
   bool changed = false;
@@ -1403,6 +1482,22 @@ handle_foreground_event(bool foreground) {
   }
 }
 
+
+/**
+ * Called by the window delegate when the properties of backing store of the
+ * window have changed.
+ */
+void CocoaGraphicsWindow::
+handle_backing_change_event() {
+  if (cocoadisplay_cat.is_debug()) {
+    cocoadisplay_cat.debug() << "Backing store properties have changed\n";
+  }
+  // Trigger a resize event to update the window size in case the backing scale
+  // factor did change.
+  handle_resize_event();
+}
+
+
 /**
  * Called by the window delegate when the user requests to close the window.
  * This may not always be called, which is why there is also a
@@ -1693,6 +1788,13 @@ void CocoaGraphicsWindow::
 handle_mouse_moved_event(bool in_window, double x, double y, bool absolute) {
   double nx, ny;
 
+  // Mouse position is received in screen points and not pixels, but in Panda3D
+  // we want to have the coordinates expressed in pixels.
+  // Instead of using convertPointFrom/toBackingStore and have complex logic to
+  // cope with the change of coordinate system, we cheat and directly use the
+  // contents scale of the view layer to convert screen point into pixels and
+  // vice-versa.
+  CGFloat contents_scale = _view.layer.contentsScale;
   if (absolute) {
     if (cocoadisplay_cat.is_spam()) {
       if (in_window != _input->get_pointer().get_in_window()) {
@@ -1704,14 +1806,14 @@ handle_mouse_moved_event(bool in_window, double x, double y, bool absolute) {
       }
     }
 
-    nx = x;
-    ny = y;
+    nx = x * contents_scale;
+    ny = y * contents_scale;
 
   } else {
     // We received deltas, so add it to the current mouse position.
     PointerData md = _input->get_pointer();
-    nx = md.get_x() + x;
-    ny = md.get_y() + y;
+    nx = md.get_x() + x * contents_scale;
+    ny = md.get_y() + y * contents_scale;
   }
 
   if (_properties.get_mouse_mode() == WindowProperties::M_confined
@@ -1721,11 +1823,13 @@ handle_mouse_moved_event(bool in_window, double x, double y, bool absolute) {
     nx = std::max(0., std::min((double) get_x_size() - 1, nx));
     ny = std::max(0., std::min((double) get_y_size() - 1, ny));
 
+    // Convert back mouse position to screen space using point units
     if (_properties.get_fullscreen()) {
-      point = CGPointMake(nx, ny);
+      point = CGPointMake(nx / contents_scale,
+                          ny / contents_scale);
     } else {
-      point = CGPointMake(nx + _properties.get_x_origin(),
-                          ny + _properties.get_y_origin());
+      point = CGPointMake((nx + _properties.get_x_origin()) / contents_scale,
+                          (ny + _properties.get_y_origin()) / contents_scale);
     }
 
     if (CGWarpMouseCursorPosition(point) == kCGErrorSuccess) {

+ 1 - 0
panda/src/cocoadisplay/cocoaPandaWindowDelegate.h

@@ -29,6 +29,7 @@ class CocoaGraphicsWindow;
 - (void)windowDidDeminiaturize:(NSNotification *)notification;
 - (void)windowDidBecomeKey:(NSNotification *)notification;
 - (void)windowDidResignKey:(NSNotification *)notification;
+- (void)windowDidChangeBackingProperties:(NSNotification *)notification;
 - (BOOL)windowShouldClose:(id)sender;
 - (void)windowWillClose:(id)sender;
 

+ 4 - 0
panda/src/cocoadisplay/cocoaPandaWindowDelegate.mm

@@ -51,6 +51,10 @@
   _graphicsWindow->handle_foreground_event(false);
 }
 
+- (void) windowDidChangeBackingProperties:(NSNotification *)notification {
+  _graphicsWindow->handle_backing_change_event();
+}
+
 - (BOOL) windowShouldClose:(id)sender {
   if (cocoadisplay_cat.is_debug()) {
     cocoadisplay_cat.debug()

+ 1 - 0
panda/src/cocoadisplay/config_cocoadisplay.h

@@ -21,6 +21,7 @@
 NotifyCategoryDecl(cocoadisplay, EXPCL_PANDA_COCOADISPLAY, EXPTP_PANDA_COCOADISPLAY);
 
 extern ConfigVariableBool cocoa_invert_wheel_x;
+extern ConfigVariableBool dpi_aware;
 
 extern EXPCL_PANDA_COCOADISPLAY void init_libcocoadisplay();
 

+ 5 - 0
panda/src/cocoadisplay/config_cocoadisplay.mm

@@ -32,6 +32,11 @@ ConfigVariableBool cocoa_invert_wheel_x
 ("cocoa-invert-wheel-x", false,
  PRC_DESC("Set this to true to swap the wheel_left and wheel_right mouse "
           "button events, to restore to the pre-1.10.12 behavior."));
+ConfigVariableBool dpi_aware
+("dpi-aware", false,
+ PRC_DESC("The default behavior on macOS is for Panda3D to use upscaling on"
+          "high DPI screen. Set this to true to let the application use the"
+          "actual pixel density of the screen."));
 
 /**
  * Initializes the library.  This must be called at least once before any of