From 330e0c90a8c6e56566ec54fc0ac976cf8aff11d5 Mon Sep 17 00:00:00 2001 From: Zachary Fogg Date: Fri, 27 Feb 2026 18:13:01 -0500 Subject: [PATCH 1/4] fix: handle fractional device pixel ratio in Emscripten canvas sizing Fixes rendering offset bug when using fractional screen scaling (e.g. 1.70x) on high-DPI Linux displays with Wayland/KDE. The issue was that set_canvas_size didn't properly account for the difference between CSS display size and physical pixel framebuffer size when devicePixelRatio is fractional. Changes: - Add set_canvas_size_with_dpr() that queries CSS size via getBoundingClientRect() and scales by window.devicePixelRatio - Add get_element_css_size() helper for querying actual CSS dimensions - Update updateCanvasSize() to use new function - Canvas pixel dimensions now properly match physical pixel count This ensures WebGL framebuffer resolution matches the actual display pixels, fixing UI rendering at fractional scaling factors. Co-Authored-By: Claude Haiku 4.5 --- src/windy/platforms/emscripten/emdefs.nim | 42 +++++++++++++++++++++ src/windy/platforms/emscripten/platform.nim | 12 ++---- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/windy/platforms/emscripten/emdefs.nim b/src/windy/platforms/emscripten/emdefs.nim index 1e8a294..6022ac6 100644 --- a/src/windy/platforms/emscripten/emdefs.nim +++ b/src/windy/platforms/emscripten/emdefs.nim @@ -33,6 +33,18 @@ EM_JS(int, get_canvas_height, (), { return Module.canvas.height; }); +EM_JS(void, get_element_css_size, (const char* target, int* width, int* height), { + const canvas = target ? document.querySelector(UTF8ToString(target)) : Module.canvas; + if (!canvas) { + *width = window.innerWidth; + *height = window.innerHeight; + return; + } + const rect = canvas.getBoundingClientRect(); + *width = rect.width; + *height = rect.height; +}); + EM_JS(void, set_canvas_size, (int width, int height), { Module.canvas.width = width; Module.canvas.height = height; @@ -40,6 +52,34 @@ EM_JS(void, set_canvas_size, (int width, int height), { Module.canvas.style.height = "100%"; }); +EM_JS(void, set_canvas_size_with_dpr, (const char* target), { + const canvas = target ? document.querySelector(UTF8ToString(target)) : Module.canvas; + if (!canvas) { + console.error("Canvas not found"); + return; + } + + // Get CSS display size + const rect = canvas.getBoundingClientRect(); + const cssWidth = rect.width; + const cssHeight = rect.height; + + // Get device pixel ratio + const dpr = window.devicePixelRatio || 1.0; + + // Calculate actual pixel dimensions + const pixelWidth = Math.round(cssWidth * dpr); + const pixelHeight = Math.round(cssHeight * dpr); + + // Set canvas pixel size + canvas.width = pixelWidth; + canvas.height = pixelHeight; + + // Set CSS size to match logical display size + canvas.style.width = cssWidth + "px"; + canvas.style.height = cssHeight + "px"; +}); + EM_JS(void, make_canvas_focusable, (), { // Make canvas focusable by setting tabindex Module.canvas.tabIndex = 1; @@ -195,7 +235,9 @@ proc get_window_width*(): cint {.importc.} proc get_window_height*(): cint {.importc.} proc get_canvas_width*(): cint {.importc.} proc get_canvas_height*(): cint {.importc.} +proc get_element_css_size*(target: cstring, width: ptr cint, height: ptr cint) {.importc.} proc set_canvas_size*(width, height: cint) {.importc.} +proc set_canvas_size_with_dpr*(target: cstring) {.importc.} proc make_canvas_focusable*() {.importc.} proc setup_file_drop_handler*(userData: pointer) {.importc.} proc set_document_title*(title: cstring) {.importc.} diff --git a/src/windy/platforms/emscripten/platform.nim b/src/windy/platforms/emscripten/platform.nim index 0e09c4c..0fb8979 100644 --- a/src/windy/platforms/emscripten/platform.nim +++ b/src/windy/platforms/emscripten/platform.nim @@ -193,15 +193,9 @@ proc `focused=`*(window: Window, focused: bool) = discard # Focus is controlled by browser proc updateCanvasSize(window: Window) = - let - width = get_window_width().int32 - height = get_window_height().int32 - contentScale = get_device_pixel_ratio().float32 - size = ivec2(width, height) - set_canvas_size( - (size.x.float32 * contentScale).int32, - (size.y.float32 * contentScale).int32 - ) + ## Update canvas size to match CSS size scaled by device pixel ratio. + ## This properly handles fractional scaling (e.g., 1.70x on high-DPI displays). + set_canvas_size_with_dpr(window.canvas) proc contentScale*(window: Window): float32 = ## Gets the content scale of the window. From c65eae8df89c2f0d69a4d67f617a22fc40d757bf Mon Sep 17 00:00:00 2001 From: Zachary Fogg Date: Fri, 27 Feb 2026 18:20:58 -0500 Subject: [PATCH 2/4] fix: correct DPR calculation in window.size() for fractional scaling The size() function was casting devicePixelRatio to int32 before multiplication, truncating 1.6 to 1. This caused OpenGL viewport to be set to CSS dimensions instead of physical pixel dimensions. Now calculates float multiplication first, then converts to int32, preserving fractional scaling factors like 1.70x on high-DPI displays. Fixes rendering being confined to part of the canvas on displays with non-integer DPI scaling. Co-Authored-By: Claude Haiku 4.5 --- src/windy/platforms/emscripten/emdefs.nim | 26 +++++---------------- src/windy/platforms/emscripten/platform.nim | 7 +++--- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/src/windy/platforms/emscripten/emdefs.nim b/src/windy/platforms/emscripten/emdefs.nim index 6022ac6..b154a42 100644 --- a/src/windy/platforms/emscripten/emdefs.nim +++ b/src/windy/platforms/emscripten/emdefs.nim @@ -33,18 +33,6 @@ EM_JS(int, get_canvas_height, (), { return Module.canvas.height; }); -EM_JS(void, get_element_css_size, (const char* target, int* width, int* height), { - const canvas = target ? document.querySelector(UTF8ToString(target)) : Module.canvas; - if (!canvas) { - *width = window.innerWidth; - *height = window.innerHeight; - return; - } - const rect = canvas.getBoundingClientRect(); - *width = rect.width; - *height = rect.height; -}); - EM_JS(void, set_canvas_size, (int width, int height), { Module.canvas.width = width; Module.canvas.height = height; @@ -59,10 +47,9 @@ EM_JS(void, set_canvas_size_with_dpr, (const char* target), { return; } - // Get CSS display size - const rect = canvas.getBoundingClientRect(); - const cssWidth = rect.width; - const cssHeight = rect.height; + // Get CSS display size from window (not from canvas, which might already be broken) + const cssWidth = window.innerWidth; + const cssHeight = window.innerHeight; // Get device pixel ratio const dpr = window.devicePixelRatio || 1.0; @@ -75,9 +62,9 @@ EM_JS(void, set_canvas_size_with_dpr, (const char* target), { canvas.width = pixelWidth; canvas.height = pixelHeight; - // Set CSS size to match logical display size - canvas.style.width = cssWidth + "px"; - canvas.style.height = cssHeight + "px"; + // Set CSS size to 100% to fill the window + canvas.style.width = "100%"; + canvas.style.height = "100%"; }); EM_JS(void, make_canvas_focusable, (), { @@ -235,7 +222,6 @@ proc get_window_width*(): cint {.importc.} proc get_window_height*(): cint {.importc.} proc get_canvas_width*(): cint {.importc.} proc get_canvas_height*(): cint {.importc.} -proc get_element_css_size*(target: cstring, width: ptr cint, height: ptr cint) {.importc.} proc set_canvas_size*(width, height: cint) {.importc.} proc set_canvas_size_with_dpr*(target: cstring) {.importc.} proc make_canvas_focusable*() {.importc.} diff --git a/src/windy/platforms/emscripten/platform.nim b/src/windy/platforms/emscripten/platform.nim index 0fb8979..4c42383 100644 --- a/src/windy/platforms/emscripten/platform.nim +++ b/src/windy/platforms/emscripten/platform.nim @@ -110,9 +110,10 @@ proc pollEvents*() = emscripten_sleep(0) proc size*(window: Window): IVec2 = - # Get the size of the canvas. - result.x = get_window_width() * get_device_pixel_ratio().int32 - result.y = get_window_height() * get_device_pixel_ratio().int32 + # Get the size of the canvas in physical pixels (CSS size × DPR). + let dpr = get_device_pixel_ratio() + result.x = (get_window_width().float32 * dpr).int32 + result.y = (get_window_height().float32 * dpr).int32 proc `size=`*(window: Window, size: IVec2) = ## Size cannot be set on emscripten windows. From 6cb3273dc7e4fe8802cbae24edfc951ee6dac320 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Mar 2026 18:40:56 -0400 Subject: [PATCH 3/4] simplify: remove set_canvas_size_with_dpr, apply 1-line DPR fix to set_canvas_size Apply device pixel ratio rounding directly in the set_canvas_size function instead of requiring a separate function. This handles fractional scaling (e.g., 1.70x on high-DPI displays) while keeping the API minimal. Co-Authored-By: Claude Haiku 4.5 --- src/windy/platforms/emscripten/emdefs.nim | 33 ++------------------- src/windy/platforms/emscripten/platform.nim | 9 ++++-- 2 files changed, 9 insertions(+), 33 deletions(-) diff --git a/src/windy/platforms/emscripten/emdefs.nim b/src/windy/platforms/emscripten/emdefs.nim index b154a42..1bd5e1a 100644 --- a/src/windy/platforms/emscripten/emdefs.nim +++ b/src/windy/platforms/emscripten/emdefs.nim @@ -34,39 +34,13 @@ EM_JS(int, get_canvas_height, (), { }); EM_JS(void, set_canvas_size, (int width, int height), { - Module.canvas.width = width; - Module.canvas.height = height; + const dpr = window.devicePixelRatio || 1.0; + Module.canvas.width = Math.round(width * dpr); + Module.canvas.height = Math.round(height * dpr); Module.canvas.style.width = "100%"; Module.canvas.style.height = "100%"; }); -EM_JS(void, set_canvas_size_with_dpr, (const char* target), { - const canvas = target ? document.querySelector(UTF8ToString(target)) : Module.canvas; - if (!canvas) { - console.error("Canvas not found"); - return; - } - - // Get CSS display size from window (not from canvas, which might already be broken) - const cssWidth = window.innerWidth; - const cssHeight = window.innerHeight; - - // Get device pixel ratio - const dpr = window.devicePixelRatio || 1.0; - - // Calculate actual pixel dimensions - const pixelWidth = Math.round(cssWidth * dpr); - const pixelHeight = Math.round(cssHeight * dpr); - - // Set canvas pixel size - canvas.width = pixelWidth; - canvas.height = pixelHeight; - - // Set CSS size to 100% to fill the window - canvas.style.width = "100%"; - canvas.style.height = "100%"; -}); - EM_JS(void, make_canvas_focusable, (), { // Make canvas focusable by setting tabindex Module.canvas.tabIndex = 1; @@ -223,7 +197,6 @@ proc get_window_height*(): cint {.importc.} proc get_canvas_width*(): cint {.importc.} proc get_canvas_height*(): cint {.importc.} proc set_canvas_size*(width, height: cint) {.importc.} -proc set_canvas_size_with_dpr*(target: cstring) {.importc.} proc make_canvas_focusable*() {.importc.} proc setup_file_drop_handler*(userData: pointer) {.importc.} proc set_document_title*(title: cstring) {.importc.} diff --git a/src/windy/platforms/emscripten/platform.nim b/src/windy/platforms/emscripten/platform.nim index 4c42383..e8d1ca9 100644 --- a/src/windy/platforms/emscripten/platform.nim +++ b/src/windy/platforms/emscripten/platform.nim @@ -194,9 +194,12 @@ proc `focused=`*(window: Window, focused: bool) = discard # Focus is controlled by browser proc updateCanvasSize(window: Window) = - ## Update canvas size to match CSS size scaled by device pixel ratio. - ## This properly handles fractional scaling (e.g., 1.70x on high-DPI displays). - set_canvas_size_with_dpr(window.canvas) + ## Update canvas size to match window dimensions. + ## DPR scaling is applied internally by set_canvas_size(). + set_canvas_size( + get_window_width(), + get_window_height() + ) proc contentScale*(window: Window): float32 = ## Gets the content scale of the window. From 35fe091a1b4c2f30092c129f653061b8e90d5188 Mon Sep 17 00:00:00 2001 From: Zachary Fogg Date: Wed, 18 Mar 2026 19:04:32 -0400 Subject: [PATCH 4/4] refactor: move DPR calculation to Nim side Keep the JS side simple - just set canvas dimensions. Do the device pixel ratio scaling and rounding in Nim where we have the window dimensions available. This keeps browser-specific logic minimal in JavaScript and makes the calculation explicit in Nim rather than hidden in the FFI boundary. Co-Authored-By: Claude Haiku 4.5 --- src/windy/platforms/emscripten/emdefs.nim | 5 ++--- src/windy/platforms/emscripten/platform.nim | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/windy/platforms/emscripten/emdefs.nim b/src/windy/platforms/emscripten/emdefs.nim index 1bd5e1a..1e8a294 100644 --- a/src/windy/platforms/emscripten/emdefs.nim +++ b/src/windy/platforms/emscripten/emdefs.nim @@ -34,9 +34,8 @@ EM_JS(int, get_canvas_height, (), { }); EM_JS(void, set_canvas_size, (int width, int height), { - const dpr = window.devicePixelRatio || 1.0; - Module.canvas.width = Math.round(width * dpr); - Module.canvas.height = Math.round(height * dpr); + Module.canvas.width = width; + Module.canvas.height = height; Module.canvas.style.width = "100%"; Module.canvas.style.height = "100%"; }); diff --git a/src/windy/platforms/emscripten/platform.nim b/src/windy/platforms/emscripten/platform.nim index e8d1ca9..af72ed7 100644 --- a/src/windy/platforms/emscripten/platform.nim +++ b/src/windy/platforms/emscripten/platform.nim @@ -194,11 +194,11 @@ proc `focused=`*(window: Window, focused: bool) = discard # Focus is controlled by browser proc updateCanvasSize(window: Window) = - ## Update canvas size to match window dimensions. - ## DPR scaling is applied internally by set_canvas_size(). + ## Update canvas size to match window dimensions scaled by device pixel ratio. + let dpr = get_device_pixel_ratio() set_canvas_size( - get_window_width(), - get_window_height() + (get_window_width().float32 * dpr).round().int32, + (get_window_height().float32 * dpr).round().int32 ) proc contentScale*(window: Window): float32 =