Skip to content

AddUpdateViewport() overwrites per-window FramebufferScale every frame (mixed-DPI viewports render at wrong scale) #9502

Description

@lailoken

Version/Branch of Dear ImGui:

1.93.0 WIP, docking branch (regression from 98a66d8, 2026-08-04)

Back-ends:

any backend implementing Platform_GetWindowFramebufferScale (observed with imgui_impl_sdl2 + imgui_impl_metal)

Compiler, OS:

macOS, mixed-DPI monitors (1x main + 2x secondary)

Full config/build information:

// (Copy from the next line. Keep the ``` markers for formatting.)

Dear ImGui 1.93.0 WIP (19292)
--------------------------------
sizeof(size_t): 8, sizeof(ImDrawIdx): 4, sizeof(ImDrawVert): 20
define: __cplusplus=202002
define: __APPLE__
define: __GNUC__=4
define: __clang_version__=21.0.0 (clang-2100.1.1.101)
define: IMGUI_HAS_VIEWPORT
define: IMGUI_HAS_DOCK
define: NDEBUG
IM_ASSERT: runs expression: KO. expand size: KO (MAYBE DISABLED?!) (?)
--------------------------------
io.BackendPlatformName: imgui_impl_sdl2 (2.32.70, 2.32.70) (cocoa)
io.BackendRendererName: imgui_impl_metal
io.ConfigFlags: 0x00000481
 NavEnableKeyboard
 DockingEnable
 ViewportsEnable
io.ConfigDpiScaleFonts
io.ConfigDpiScaleViewports
io.ConfigViewportsNoAutoMerge
io.ConfigViewportsNoDefaultParent
io.ConfigDockingWithShift
io.ConfigNavCaptureKeyboard
io.ConfigInputTextCursorBlink
io.ConfigWindowsResizeFromEdges
io.ConfigMemoryCompactTimer = 60.0
io.BackendFlags: 0x00000C1E
 HasMouseCursors
 HasSetMousePos
 PlatformHasViewports
 RendererHasVtxOffset
 RendererHasTextures
 RendererHasViewports
--------------------------------
io.Fonts: 5 fonts, Flags: 0x00000004, TexSize: 512,512
io.Fonts->FontLoaderName: stb_truetype
io.DisplaySize: 1.00,1.00
io.DisplayFramebufferScale: 1.00,1.00
--------------------------------
style.WindowPadding: 8.00,8.00
style.WindowBorderSize: 0.00
style.FramePadding: 4.00,3.00
style.FrameRounding: 0.00
style.FrameBorderSize: 1.00
style.ItemSpacing: 6.00,3.00
style.ItemInnerSpacing: 4.00,2.00

Details:

Description:

98a66d8 added this line to the tail of AddUpdateViewport():

    viewport->FramebufferScale = g.IO.DisplayFramebufferScale; // Later overidden by Platform_GetWindowFramebufferScale() for platform having it

AddUpdateViewport() runs every frame for every window's viewport, and the tail runs for
existing viewports too, so this overwrites the per-window value each frame. The
"later overridden" part happens in UpdatePlatformWindows(), i.e. after Render() - so by
the time SetCurrentWindow() picks the pixel density and InitViewportDrawData() stamps
draw_data->FramebufferScale, every viewport carries the main viewport's scale instead of
its own. Before 98a66d8 the handler's value persisted across frames (nothing reset it), and
InitViewportDrawData() fell back to io.DisplayFramebufferScale only when the value was 0.

Symptom: with a 1x main monitor and 2x secondaries, dragging a viewport window onto a 2x
monitor renders all its content at half size (draw data built with scale 1.0 while the
renderer's drawable is 2x). The inverse setup renders at double size.

Steps to reproduce:

SDL2 (or any backend with Platform_GetWindowFramebufferScale) + viewports on macOS with
monitors of different scales; drag a window from the main monitor to one with a different
scale.

Fix: initialize only on viewport creation, keeping the intent of the original comment —
plus a fallback for the viewports-disabled case:

    else
    {
        // New viewport
        ...
        viewport->DpiScale = GetViewportPlatformMonitor(viewport)->DpiScale;
        viewport->FramebufferScale = g.IO.DisplayFramebufferScale; // Later overidden by Platform_GetWindowFramebufferScale() for platform having it
    }

    viewport->Window = window;
    if (!(g.ConfigFlagsCurrFrame & ImGuiConfigFlags_ViewportsEnable))
        viewport->FramebufferScale = g.IO.DisplayFramebufferScale;
    viewport->LastFrameActive = g.FrameCount;

The fallback is required: the main viewport is created in Initialize() with
FramebufferScale = (0,0) (memset) and never passes through the new-viewport branch of
AddUpdateViewport(), and Platform_GetWindowFramebufferScale() is only polled when
viewports are enabled. Without it, any app running without ImGuiConfigFlags_ViewportsEnable
stamps draw_data->FramebufferScale = (0,0) (InitViewportDrawData() no longer falls back to
io.DisplayFramebufferScale since 1.93), so every renderer backend hits its
fb_width <= 0 early-out and the window renders blank.

(An alternative would be applying the Platform_GetWindowFramebufferScale() value during
UpdateViewportsNewFrame() rather than in UpdatePlatformWindows(), so it is fresh at the time
draw data and pixel density consume it.)

Screenshots/Video:

No response

Minimal, Complete and Verifiable Example code:

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions