Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions Prowl.Editor/EditorApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
15 changes: 7 additions & 8 deletions Prowl.Editor/SceneView/EditorCamera.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,21 @@ namespace Prowl.Editor;
/// Manages a hidden runtime Camera that renders the scene to a RenderTexture.
/// </summary>
/// <summary>
/// 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.
/// </summary>
public class SceneViewLockContext : CursorLockContext
{
/// <summary>Panel origin in Paper-logical coordinates.</summary>
/// <summary>Panel origin in Paper-logical (= window-logical) coordinates.</summary>
public Float2 PanelOrigin;
/// <summary>Panel size in Paper-logical coordinates.</summary>
/// <summary>Panel size in Paper-logical (= window-logical) coordinates.</summary>
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);
}
}
Expand Down
37 changes: 17 additions & 20 deletions Prowl.Runtime/Game.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,41 +251,38 @@ public virtual void Resize(int width, int height) { }
public virtual void Closing() { }

/// <summary>
/// Called each frame right before <c>Paper.BeginFrame</c>. Compresses Paper's logical
/// window by <see cref="Window.ContentScale"/> so widgets sit in "points" (1 unit =
/// 1 logical dot at 1× density, matching the OS convention), and sets
/// <c>DisplayFramebufferScale</c> to <see cref="Window.ContentScale"/> 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 <c>Paper.BeginFrame</c>. Sets Paper's logical resolution
/// to the window's logical size (OS points) and <c>DisplayFramebufferScale</c> to
/// <see cref="Window.ContentScale"/>. Quill's <c>TransformPoint</c> multiplies every vertex
/// by <c>DisplayFramebufferScale</c>, so vertices span <c>[0, winSize × cs]</c> =
/// <c>[0, fbSize]</c>, exactly matching the orthographic projection set up by
/// <see cref="PaperRenderer.UpdateProjection"/>.
/// <para>
/// Under this scheme a widget declared <c>Width(100)</c> 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 <c>Width(100)</c> 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.
/// </para>
/// </summary>
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);
}

/// <summary>
/// Returns the current mouse position in Paper-logical units. Because
/// <see cref="PreparePaperFrame"/> compresses Paper's logical space by <see cref="Window.ContentScale"/>,
/// 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.
/// </summary>
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)")]
Expand Down
50 changes: 8 additions & 42 deletions Prowl.Runtime/Window.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,62 +49,28 @@ private static void EnsureDpiAwareOnWindows()
public static event Action<WindowState>? StateChanged;
public static event Action<string[]>? FileDrop;

private static nint s_contentScaleProc;
private static bool s_contentScaleResolved;

/// <summary>
/// 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 <c>FramebufferSize / Size</c> — the only value that is
/// always correct regardless of platform or DPI-awareness mode.
/// <para>
/// Silk.NET.GLFW 2.22 does not expose <c>glfwGetWindowContentScale</c> as a managed method,
/// so this resolves the symbol via <see cref="INativeContext.GetProcAddress"/> and calls it
/// directly. If that fails (non-GLFW backend, older GLFW, or the symbol isn't reachable),
/// we fall back to the <c>FramebufferSize / Size</c> 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).
/// </para>
/// </summary>
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]<nint, float*, float*, void>)s_contentScaleProc)(glfwWindow, &x, &y);
if (x <= 0) return false;
scale = x;
return true;
}

public static Vector2D<int> Position
{
get { return InternalWindow.Position; }
Expand Down
Loading