Quick answer: Set your stretch mode to canvas_items or viewport in Project Settings > Display > Window > Stretch. If you’re using SubViewports, enable the stretch property on SubViewportContainer or manually update the SubViewport size when the window resizes.

You resize your game window during testing and suddenly the entire scene looks frozen, cropped, or offset. The game is still running — you can hear audio and input still works — but the visual output hasn’t updated to match the new window dimensions. This is one of the most common Godot issues, and it almost always comes down to how your project’s stretch settings are configured.

Why the Viewport Freezes on Resize

Godot’s rendering pipeline separates the concept of the window size (the OS-level window dimensions) from the viewport size (the internal rendering resolution). When you leave the stretch mode set to disabled, Godot does not adjust the viewport when the window changes size. The rendered frame stays locked to the original resolution, and the window simply shows whatever portion of that frame fits.

This is the default behavior in new Godot 4.x projects, and it catches a lot of developers off guard. The engine assumes you might want full manual control over viewport sizing — useful for some advanced rendering setups, but confusing for the majority of game projects.

There are also cases where the viewport does resize but the content appears stretched or squished. This happens when the stretch mode is set but the aspect ratio setting doesn’t match your game’s design. A 16:9 game stretched into a 4:3 window will distort unless you’ve told Godot how to handle the mismatch.

Configuring Stretch Mode and Aspect

Open Project Settings > Display > Window > Stretch and configure these two critical fields:

Stretch Mode determines how the viewport responds to window size changes:

Stretch Aspect determines how mismatched aspect ratios are handled:

For most 2D games, canvas_items with expand is a solid starting point. For pixel art, use viewport with keep.

Handling SubViewports Correctly

If your game uses SubViewport nodes — for minimaps, split-screen, render-to-texture effects, or picture-in-picture views — you’ll find they do not respond to the stretch settings above. SubViewports maintain their own independent size, and you need to handle their resize behavior explicitly.

The simplest approach is to use a SubViewportContainer with its stretch property enabled:

# In your scene tree:
# SubViewportContainer (stretch = true)
#   └── SubViewport
#         └── Your scene content

When stretch is enabled on the container, the SubViewport automatically matches the container’s size, which in turn follows the layout system. This is the recommended approach for most use cases.

If you need more control — for example, rendering a minimap at a fixed lower resolution — you can connect to the root viewport’s size_changed signal and update the SubViewport manually:

func _ready():
    get_tree().root.size_changed.connect(_on_window_resized)

func _on_window_resized():
    var window_size = get_tree().root.size
    # Scale the SubViewport to half the window size for a minimap
    $SubViewport.size = window_size / 2

Without either of these approaches, the SubViewport will render at whatever size it was set to in the editor and never update, even as the window changes around it.

Common Edge Cases

Even with the correct stretch settings, a few scenarios can still cause viewport issues:

Cameras with fixed limits: If your Camera2D has limit_left, limit_right, limit_top, and limit_bottom set tightly around your level, resizing the window to a larger aspect ratio might reveal empty space beyond the camera limits. Set your limits to account for the widest aspect ratio you plan to support, or use keep aspect to prevent the viewport from ever showing beyond your intended bounds.

Fullscreen toggling: Switching between windowed and fullscreen can trigger the same resize issues, especially on multi-monitor setups where the displays have different resolutions. Test fullscreen transitions explicitly with:

func _input(event):
    if event.is_action_pressed("toggle_fullscreen"):
        if DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_FULLSCREEN:
            DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
        else:
            DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)

Custom viewport shaders: If you’re applying a shader to a ColorRect that covers the viewport (for post-processing effects like CRT filters or vignettes), make sure the ColorRect uses full_rect anchors so it resizes with the viewport. A fixed-size ColorRect will leave the shader effect cropped after a resize.

Testing Resize Behavior Reliably

Don’t rely on dragging the window corner during testing. Different operating systems handle live resize differently — some send continuous resize events, others batch them. Instead, test with explicit sizes using the command line or a debug script:

# Test common resolutions programmatically
func _ready():
    if OS.is_debug_build():
        # Test at 720p
        DisplayServer.window_set_size(Vector2i(1280, 720))
        await get_tree().create_timer(2.0).timeout
        # Test at 1080p
        DisplayServer.window_set_size(Vector2i(1920, 1080))
        await get_tree().create_timer(2.0).timeout
        # Test at ultrawide
        DisplayServer.window_set_size(Vector2i(2560, 1080))

This lets you verify that your UI anchors, camera limits, and SubViewport sizes all behave correctly at each resolution without manually dragging the window. If you’re shipping on Steam Deck or other fixed-resolution devices, add those resolutions to your test list as well.

Stretch mode is one of those settings that takes 10 seconds to fix once you know where it is — and hours of confusion before that.