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; }