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
18 changes: 11 additions & 7 deletions Prowl.Editor/EditorApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,18 +234,22 @@ 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);

var fbSize = Window.InternalWindow.FramebufferSize;
float cs = Math.Max(0.01f, Window.ContentScale);
float us = Math.Max(0.01f, EditorTheme.UserScale);
PaperInstance.SetResolution(fbSize.X / (cs * us), fbSize.Y / (cs * 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);
var fb = Window.InternalWindow.FramebufferSize;
var win = Window.InternalWindow.Size;
float cs = Math.Max(0.01f, Window.ContentScale);
float csFbWin = win.X > 0 ? (float)fb.X / win.X : 1f;
float us = Math.Max(0.01f, EditorTheme.UserScale);
return new Float2(p.X * csFbWin / (cs * us), p.Y * csFbWin / (cs * us));
}

private void ApplyDarkTitleBar()
Expand Down
20 changes: 13 additions & 7 deletions Prowl.Editor/SceneView/EditorCamera.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,28 @@ 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()
{
var fb = Window.InternalWindow.FramebufferSize;
var win = Window.InternalWindow.Size;
float cs = Window.ContentScale;
float centerX = (PanelOrigin.X + PanelSize.X / 2) * cs;
float centerY = (PanelOrigin.Y + PanelSize.Y / 2) * cs;
float csFbWin = win.X > 0 ? (float)fb.X / win.X : 1f;
// Paper coords are in [0, fbSize/cs]; OS cursor expects winSize coords.
// scale = cs/csFbWin converts paper → winSize (== 1 on macOS, == cs on DPI-unaware Windows).
float scale = csFbWin > 0 ? cs / csFbWin : 1f;
float centerX = (PanelOrigin.X + PanelSize.X / 2) * scale;
float centerY = (PanelOrigin.Y + PanelSize.Y / 2) * scale;
return new Int2((int)centerX, (int)centerY);
}
}
Expand Down
44 changes: 22 additions & 22 deletions Prowl.Runtime/Game.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,41 +251,41 @@ 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;
var fbSize = Window.InternalWindow.FramebufferSize;
float cs = Math.Max(0.01f, Window.ContentScale);
_paper.SetResolution(winSize.X / cs, winSize.Y / cs);
// resolution × cs = fbSize, so vertices always span exactly [0, fbSize].
// Using fbSize (not winSize) handles DPI-unaware Windows where winSize == fbSize
// but glfwGetWindowContentScale still returns the system DPI factor (e.g. 1.25).
_paper.SetResolution(fbSize.X / cs, fbSize.Y / cs);
_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.
/// </summary>
protected virtual Float2 GetPaperMousePosition()
{
var p = Input.MousePosition;
var fb = Window.InternalWindow.FramebufferSize;
var win = Window.InternalWindow.Size;
float cs = Math.Max(0.01f, Window.ContentScale);
return new Float2(p.X / cs, p.Y / cs);
// Mouse is in winSize coords; paper space is [0, fbSize/cs].
// csFbWin converts winSize → fbSize; dividing by cs then lands in paper space.
// On macOS cs == csFbWin so the ratio is 1. On DPI-unaware Windows csFbWin == 1
// and cs is the system DPI, so we divide mouse by cs.
float csFbWin = win.X > 0 ? (float)fb.X / win.X : 1f;
return new Float2(p.X * csFbWin / cs, p.Y * csFbWin / cs);
}

[RequiresDynamicCode("Calls System.Enum.GetValues(Type)")]
Expand Down
30 changes: 17 additions & 13 deletions Prowl.Runtime/Window.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,18 @@ private static void EnsureDpiAwareOnWindows()
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.
/// <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 Windows, resolves <c>glfwGetWindowContentScale</c> via the native GLFW context
/// to capture the system DPI factor (e.g. 1.25 at 125%). This is needed because
/// <c>FramebufferSize / Size</c> only reflects the ratio when the process is
/// per-monitor DPI-aware; if DPI awareness failed at startup the OS virtualises
/// the framebuffer (fb == win) and the ratio collapses to 1.
/// </para>
/// <para>
/// On macOS and Linux the GLFW proc address is not reachable via this path, so we
/// fall back to <c>FramebufferSize / Size</c> which is always correct on those
/// platforms (Retina: 3024 fb / 1512 points = 2; 1× monitor: 1).
/// </para>
/// </summary>
public static unsafe float ContentScale
Expand All @@ -68,9 +73,12 @@ public static unsafe float ContentScale
{
if (InternalWindow == null) return 1f;

nint? nativeGlfw = InternalWindow.Native?.Glfw;
if (nativeGlfw.HasValue && nativeGlfw.Value != 0 && TryGetContentScaleViaProc(nativeGlfw.Value, out float scale))
return scale;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
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;
Expand All @@ -87,10 +95,6 @@ private static unsafe bool TryGetContentScaleViaProc(nint glfwWindow, out float
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 { }
Expand Down
Loading