|
| 1 | +# Viewport High DPI Scaling Fix |
| 2 | + |
| 3 | +## Problem Statement |
| 4 | +On scaled high DPI displays, the viewport geometry was not fitted correctly and the view was not centered. Objects would appear smaller than expected, and zoom-to-extents would not properly fit the geometry to the viewport. |
| 5 | + |
| 6 | +## Root Cause |
| 7 | +The viewport code was inconsistently using physical pixel dimensions (`RenderWidth`/`RenderHeight`) and logical dimensions (`Width`/`Height`) throughout the rendering pipeline. |
| 8 | + |
| 9 | +On high DPI displays: |
| 10 | +- **Logical dimensions** (`Width`/`Height`): The control size in logical pixels (e.g., 800x600) |
| 11 | +- **Physical dimensions** (`RenderWidth`/`RenderHeight`): The actual framebuffer size in physical pixels, scaled by DPI (e.g., 1600x1200 on a 2x DPI display) |
| 12 | + |
| 13 | +The issue was that some parts of the code used physical dimensions while others used logical dimensions, causing: |
| 14 | +1. The orthographic projection to be calculated based on physical pixels, making the view frustum too large in world space |
| 15 | +2. The zoom calculations to divide world space extents by physical pixels, making objects appear smaller |
| 16 | +3. Mouse coordinate conversions to scale by DPI when they should use logical coordinates |
| 17 | + |
| 18 | +## Solution |
| 19 | +Changed the viewport to consistently use **logical dimensions** throughout: |
| 20 | + |
| 21 | +### Files Modified |
| 22 | + |
| 23 | +#### 1. `Eto/Eto.VeldridSurface/VeldridDriver_Draw.cs` |
| 24 | +- **Draw() method**: Changed orthographic projection to use `Surface.Width`/`Surface.Height` instead of `Surface.RenderWidth`/`Surface.RenderHeight` |
| 25 | +- **drawGrid() method**: Changed grid extent calculations to use logical dimensions |
| 26 | +- **drawAxes() method**: Changed axis extent calculations to use logical dimensions |
| 27 | + |
| 28 | +#### 2. `Eto/Eto.VeldridSurface/VeldridDriver_Public.cs` |
| 29 | +- **zoomExtents() method**: Changed zoom level calculations to use `Surface.Width`/`Surface.Height` instead of `Surface.RenderWidth`/`Surface.RenderHeight` |
| 30 | + |
| 31 | +#### 3. `Eto/Eto.VeldridSurface/VeldridDriver_Conversions.cs` |
| 32 | +- **ScreenToWorld() method**: Changed coordinate conversions to use logical dimensions |
| 33 | +- **WorldToScreen() method**: Changed coordinate conversions to use logical dimensions |
| 34 | + |
| 35 | +#### 4. `Eto/Eto.VeldridSurface/VeldridDriver_Handlers.cs` |
| 36 | +- **dragHandler() method**: Removed scaling of mouse coordinates by `LogicalPixelSize` |
| 37 | +- **selectByClick() method**: Removed scaling of mouse coordinates by `LogicalPixelSize` |
| 38 | + |
| 39 | +## Technical Details |
| 40 | + |
| 41 | +### Before (Incorrect) |
| 42 | +```csharp |
| 43 | +// Orthographic projection using physical pixels |
| 44 | +float left = ovpSettings.getCameraX() - (float)Surface!.RenderWidth / 2 * zoom; |
| 45 | +float right = ovpSettings.getCameraX() + (float)Surface!.RenderWidth / 2 * zoom; |
| 46 | + |
| 47 | +// Zoom calculation using physical pixels |
| 48 | +float zoomLevel_x = dX / Surface!.RenderWidth; |
| 49 | + |
| 50 | +// Mouse coordinates scaled by DPI |
| 51 | +PointF scaledLocation = e.Location * Surface!.ParentWindow.LogicalPixelSize; |
| 52 | +``` |
| 53 | + |
| 54 | +### After (Correct) |
| 55 | +```csharp |
| 56 | +// Orthographic projection using logical dimensions |
| 57 | +float left = ovpSettings.getCameraX() - (float)Surface!.Width / 2 * zoom; |
| 58 | +float right = ovpSettings.getCameraX() + (float)Surface!.Width / 2 * zoom; |
| 59 | + |
| 60 | +// Zoom calculation using logical dimensions |
| 61 | +float zoomLevel_x = dX / Surface!.Width; |
| 62 | + |
| 63 | +// Mouse coordinates in logical pixels (no scaling needed) |
| 64 | +PointF scaledLocation = e.Location; |
| 65 | +``` |
| 66 | + |
| 67 | +## Why This Works |
| 68 | + |
| 69 | +1. **Orthographic Projection**: By using logical dimensions, the view frustum size in world space remains consistent regardless of DPI scaling. The GPU automatically handles the mapping to the physical framebuffer. |
| 70 | + |
| 71 | +2. **Zoom Calculations**: Using logical dimensions ensures that the same world space extent results in the same zoom level, regardless of DPI. |
| 72 | + |
| 73 | +3. **Coordinate Conversions**: Mouse events report coordinates in logical pixels, and by using logical dimensions throughout, we maintain consistency between input coordinates and rendered coordinates. |
| 74 | + |
| 75 | +4. **Framebuffer**: The framebuffer size (`RenderWidth`/`RenderHeight`) is still correctly set by the platform handlers (GTK/WPF) to match the physical pixel count, ensuring sharp rendering on high DPI displays. |
| 76 | + |
| 77 | +## Testing |
| 78 | +- All 437 existing unit tests pass |
| 79 | +- Build completes without errors |
| 80 | +- Code is ready for manual testing on different DPI settings (1x, 1.5x, 2x, etc.) |
| 81 | + |
| 82 | +## Expected Behavior After Fix |
| 83 | +- Geometry is properly fitted to the viewport on zoom-to-extents |
| 84 | +- View is correctly centered on the geometry |
| 85 | +- Zoom levels are consistent across different DPI settings |
| 86 | +- Mouse panning and selection work correctly on high DPI displays |
| 87 | +- Grid and axes render at appropriate scales |
0 commit comments