diff --git a/Prowl.Editor/EditorApplication.cs b/Prowl.Editor/EditorApplication.cs index cc4348d4..dddee26e 100644 --- a/Prowl.Editor/EditorApplication.cs +++ b/Prowl.Editor/EditorApplication.cs @@ -235,17 +235,19 @@ public void InitializeFont() protected override void PreparePaperFrame() { var winSize = Window.InternalWindow.Size; - float cs = Math.Max(0.01f, Window.ContentScale * EditorTheme.UserScale); - PaperInstance.SetResolution(winSize.X / cs, winSize.Y / cs); - PaperInstance.DisplayFramebufferScale = new Float2(cs, cs); - + float cs = Math.Max(0.01f, Window.ContentScale); + float us = Math.Max(0.01f, EditorTheme.UserScale); + // Resolution is divided only by UserScale (UI zoom), not by ContentScale. + // cs * us together form DisplayFramebufferScale so vertices reach [0, fbSize]. + PaperInstance.SetResolution(winSize.X / us, winSize.Y / us); + PaperInstance.DisplayFramebufferScale = new Float2(cs * us, cs * us); } protected override Float2 GetPaperMousePosition() { var p = Input.MousePosition; - float cs = Math.Max(0.01f, Window.ContentScale * EditorTheme.UserScale); - return new Float2(p.X / cs, p.Y / cs); + float us = Math.Max(0.01f, EditorTheme.UserScale); + return new Float2(p.X / us, p.Y / us); } private void ApplyDarkTitleBar() diff --git a/Prowl.Editor/SceneView/EditorCamera.cs b/Prowl.Editor/SceneView/EditorCamera.cs index 3c8396a1..18f8f58d 100644 --- a/Prowl.Editor/SceneView/EditorCamera.cs +++ b/Prowl.Editor/SceneView/EditorCamera.cs @@ -12,22 +12,21 @@ namespace Prowl.Editor; /// Manages a hidden runtime Camera that renders the scene to a RenderTexture. /// /// -/// Cursor lock context for the scene view locks to the center of the scene panel. -/// Panel coordinates are in Paper-logical space; the OS cursor position expects -/// window-logical pixels, so we multiply by Window.ContentScale on the way out. +/// Cursor lock context for the scene view, locks to the center of the scene panel. +/// Paper-logical coordinates now equal window-logical pixels (winSize space), which +/// is also what the OS expects for cursor position on all platforms, so no scaling needed. /// public class SceneViewLockContext : CursorLockContext { - /// Panel origin in Paper-logical coordinates. + /// Panel origin in Paper-logical (= window-logical) coordinates. public Float2 PanelOrigin; - /// Panel size in Paper-logical coordinates. + /// Panel size in Paper-logical (= window-logical) coordinates. public Float2 PanelSize; public override Int2 GetLockCenter() { - float cs = Window.ContentScale; - float centerX = (PanelOrigin.X + PanelSize.X / 2) * cs; - float centerY = (PanelOrigin.Y + PanelSize.Y / 2) * cs; + float centerX = PanelOrigin.X + PanelSize.X / 2; + float centerY = PanelOrigin.Y + PanelSize.Y / 2; return new Int2((int)centerX, (int)centerY); } } diff --git a/Prowl.Runtime/Game.cs b/Prowl.Runtime/Game.cs index d8064658..215986d1 100644 --- a/Prowl.Runtime/Game.cs +++ b/Prowl.Runtime/Game.cs @@ -251,41 +251,38 @@ public virtual void Resize(int width, int height) { } public virtual void Closing() { } /// - /// Called each frame right before Paper.BeginFrame. Compresses Paper's logical - /// window by so widgets sit in "points" (1 unit = - /// 1 logical dot at 1× density, matching the OS convention), and sets - /// DisplayFramebufferScale to so the canvas emits - /// pixel-space vertices that land correctly in the framebuffer and font atlases rasterize - /// at native density. + /// Called each frame right before Paper.BeginFrame. Sets Paper's logical resolution + /// to the window's logical size (OS points) and DisplayFramebufferScale to + /// . Quill's TransformPoint multiplies every vertex + /// by DisplayFramebufferScale, so vertices span [0, winSize × cs] = + /// [0, fbSize], exactly matching the orthographic projection set up by + /// . /// - /// Under this scheme a widget declared Width(100) occupies ~100 physical pixels at - /// 1× DPI and 200 physical pixels at 2× DPI i.e. the same physical inches on the screen - /// regardless of display density. On non-DPI-aware platforms (Windows without the per- - /// monitor manifest) the OS bitmap-upscales the framebuffer on top of this, which makes - /// widgets look proportionally larger; on DPI-aware platforms (macOS retina, modern - /// Windows) the app renders straight into the physical framebuffer and sizes match native - /// apps. + /// A widget declared Width(100) occupies 100 logical points = 100 physical pixels at + /// 1× DPI and 200 physical pixels at 2× (Retina) DPI — the same physical size on screen + /// regardless of display density, with higher pixel quality on HiDPI displays. /// /// protected virtual void PreparePaperFrame() { var winSize = Window.InternalWindow.Size; float cs = Math.Max(0.01f, Window.ContentScale); - _paper.SetResolution(winSize.X / cs, winSize.Y / cs); + // Paper resolution = window logical size (points). DisplayFramebufferScale = cs + // makes Quill's TransformPoint multiply by cs, producing vertices in [0, fbSize] + // that land exactly in the physical framebuffer covered by the projection. + _paper.SetResolution(winSize.X, winSize.Y); _paper.DisplayFramebufferScale = new Float2(cs, cs); } /// - /// Returns the current mouse position in Paper-logical units. Because - /// compresses Paper's logical space by , - /// the mouse (reported in window-logical pixels by Silk.NET on every platform) is divided - /// by the same factor so clicks land on widgets in the same space. + /// Returns the current mouse position in Paper-logical units. + /// Input.MousePosition is in window-logical pixels, which now matches Paper's + /// resolution (winSize), so no conversion is needed. /// protected virtual Float2 GetPaperMousePosition() { var p = Input.MousePosition; - float cs = Math.Max(0.01f, Window.ContentScale); - return new Float2(p.X / cs, p.Y / cs); + return new Float2(p.X, p.Y); } [RequiresDynamicCode("Calls System.Enum.GetValues(Type)")] diff --git a/Prowl.Runtime/Window.cs b/Prowl.Runtime/Window.cs index 48c2da66..f6fa48ca 100644 --- a/Prowl.Runtime/Window.cs +++ b/Prowl.Runtime/Window.cs @@ -49,62 +49,28 @@ private static void EnsureDpiAwareOnWindows() public static event Action? StateChanged; public static event Action? FileDrop; - private static nint s_contentScaleProc; - private static bool s_contentScaleResolved; - /// - /// System content scale factor (1.0 = 100%, 1.25 = 125%, 1.5 = 150%, 2.0 = retina, etc.). + /// Physical-to-logical pixel ratio for the window's current display. + /// Computed as FramebufferSize / Size — the only value that is + /// always correct regardless of platform or DPI-awareness mode. /// - /// Silk.NET.GLFW 2.22 does not expose glfwGetWindowContentScale as a managed method, - /// so this resolves the symbol via and calls it - /// directly. If that fails (non-GLFW backend, older GLFW, or the symbol isn't reachable), - /// we fall back to the FramebufferSize / Size ratio which is also the correct - /// value on macOS retina and on DPI-aware Windows (FB in physical pixels, Size in points). + /// On macOS Retina this is 2 (3024 fb / 1512 logical points). + /// On a 1× display or a DPI-unaware Windows process (where the OS virtualises + /// the framebuffer so fb == win) this is 1, even if the system DPI is higher. + /// On a DPI-aware Windows process at 150% this is 1.5 (1800 fb / 1200 points). /// /// - public static unsafe float ContentScale + public static float ContentScale { get { if (InternalWindow == null) return 1f; - - nint? nativeGlfw = InternalWindow.Native?.Glfw; - if (nativeGlfw.HasValue && nativeGlfw.Value != 0 && TryGetContentScaleViaProc(nativeGlfw.Value, out float scale)) - return scale; - var fb = InternalWindow.FramebufferSize; var win = InternalWindow.Size; return win.X > 0 ? (float)fb.X / win.X : 1f; } } - private static unsafe bool TryGetContentScaleViaProc(nint glfwWindow, out float scale) - { - scale = 1f; - - if (!s_contentScaleResolved) - { - s_contentScaleResolved = true; - try - { - // Silk.NET's INativeContext GetProcAddress falls through to the GLFW shared - // library's own symbol table when glfwGetProcAddress returns null (i.e. for - // non-GL symbols). Works on Windows in our testing; on macOS / Linux this path - // may return 0 and we'll use the FB/Size fallback in the caller. - s_contentScaleProc = Silk.NET.GLFW.GlfwProvider.GLFW.Value.Context.GetProcAddress("glfwGetWindowContentScale"); - } - catch { } - } - - if (s_contentScaleProc == 0) return false; - - float x, y; - ((delegate* unmanaged[Cdecl])s_contentScaleProc)(glfwWindow, &x, &y); - if (x <= 0) return false; - scale = x; - return true; - } - public static Vector2D Position { get { return InternalWindow.Position; }