Repository: godotengine/godot (language: cpp)

Issue:
Window content_scale_factor causes jitter in Control size
### Tested versions

Reproducible in 4.4-beta3, 4.3-stable

### System information

Godot v4.4.beta3 - Ubuntu 24.04.1 LTS 24.04 on Wayland - X11 display driver, Multi-window, 1 monitor - Vulkan (Forward+) - integrated Intel(R) UHD Graphics (CML GT2) - Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz (8 threads)

### Issue description

Using the recommended way to scale UI to screen resolution by setting `get_tree().root.content_scale_factor` (either in the project settings or from script) causes Control node pixels to jitter on resizing the window. This is true even when the "Snap Controls to Pixels" option is enabled.

Interestingly, using the node's own scale factor (or scaling a parent node to rescale the entire UI) doesn't exhibit this behaviour, but causes a host of other issues with GUIs so it's not a possible workaround.

Screenshot of misaligned grid pixels from the MRP:

![Image](https://github.com/user-attachments/assets/c906e43d-2bce-4ecd-b48a-aae6f9bb0015)

Screenshot of the grid looking perfect at a different resolution:

![Image](https://github.com/user-attachments/assets/fb2dea18-5215-41d1-b84b-93b6cceea017)

I also managed to reproduce this in my game with all kinds of control nodes, including fonts. In that case the jitter is a lot more visible even when resizing at high resolutions.

**Update:**

Setting `content_scale_factor = 8.0` shows that this is not even a jitter, the control node's scale is just completely off when resizing to certain resolutions. There's no need for Nearest filtering either, the effect is clearly visible even on Linear filtered textures.

https://github.com/user-attachments/assets/3cf1038a-1958-488d-acf5-fd5148140f8e

### Steps to reproduce

1. Place a control node (Confirmed with: TextureRect, Label).
3. Optionally, set the window resolution to a small value, such as 480x360. For me small window sizes (regardless of the base resolution) greatly exacerbate the effect, but it also happens from time to time on larger window sizes.
4. Set `display/window/stretch/scale` to a larger value, in my case 4.0 is enough, 8.0 makes it super visible.
5. Resize the game window by dragging the corner, and watch the control node size jitter.

### Minimal reproduction project (MRP)

[gui-scale.zip](https://github.com/user-attachments/files/18734113/gui-scale.zip)


Discussion / hints:
- Related to https://github.com/godotengine/godot/issues/101562 and https://github.com/godotengine/godot/issues/83679.

I'm guessing Snap Controls to Pixels is always relative to the base viewport size, not the current window size (and current viewport resolution).

However, I'm not sure this is what you are noticing here. Snap Controls to Pixels does **not** perform integer scaling, so if you are resizing the window, you will be getting fractional scale factors (which makes nearest-neighbor-filtered textures look bad). You need to set the stretch scale policy to `integer` in the Project Settings for that to happen instead.

Fix patch:
diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp
index d0054baeff25..e3979837ab69 100644
--- a/scene/main/viewport.cpp
+++ b/scene/main/viewport.cpp
@@ -1002,10 +1002,10 @@ void Viewport::update_canvas_items() {
 	_update_canvas_items(this);
 }
 
-bool Viewport::_set_size(const Size2i &p_size, const Size2i &p_size_2d_override, bool p_allocated) {
+bool Viewport::_set_size(const Size2i &p_size, const Size2 &p_size_2d_override, bool p_allocated) {
 	Transform2D stretch_transform_new = Transform2D();
 	if (is_size_2d_override_stretch_enabled() && p_size_2d_override.width > 0 && p_size_2d_override.height > 0) {
-		Size2 scale = Size2(p_size) / Size2(p_size_2d_override);
+		Size2 scale = Size2(p_size) / p_size_2d_override;
 		stretch_transform_new.scale(scale);
 	}
 
@@ -1074,7 +1074,7 @@ Size2i Viewport::_get_size() const {
 	return size;
 }
 
-Size2i Viewport::_get_size_2d_override() const {
+Size2 Viewport::_get_size_2d_override() const {
 	return size_2d_override;
 }
 
@@ -1092,7 +1092,7 @@ Rect2 Viewport::get_visible_rect() const {
 		r = Rect2(Point2(), size);
 	}
 
-	if (size_2d_override != Size2i()) {
+	if (size_2d_override != Size2()) {
 		r.size = size_2d_override;
 	}
 
@@ -5223,7 +5223,10 @@ void SubViewport::set_size_2d_override(const Size2i &p_size) {
 
 Size2i SubViewport::get_size_2d_override() const {
 	ERR_READ_THREAD_GUARD_V(Size2i());
-	return _get_size_2d_override();
+	// Rounding will cause offset issues with the
+	// exact positioning of subwindows, but changing the
+	// type of size_2d_override would break compatibility.
+	return Size2i((_get_size_2d_override() + Size2(0.5, 0.5)).floor());
 }
 
 void SubViewport::set_size_2d_override_stretch(bool p_enable) {
diff --git a/scene/main/viewport.h b/scene/main/viewport.h
index 5260b513a951..5bcb50c55546 100644
--- a/scene/main/viewport.h
+++ b/scene/main/viewport.h
@@ -252,7 +252,7 @@ class Viewport : public Node {
 	Transform2D stretch_transform;
 
 	Size2i size = Size2i(512, 512);
-	Size2i size_2d_override;
+	Size2 size_2d_override;
 	bool size_allocated = false;
 
 	RID contact_2d_debug;
@@ -492,10 +492,10 @@ class Viewport : public Node {
 	void _window_start_resize(SubWindowResize p_edge, Window *p_window);
 
 protected:
-	bool _set_size(const Size2i &p_size, const Size2i &p_size_2d_override, bool p_allocated);
+	bool _set_size(const Size2i &p_size, const Size2 &p_size_2d_override, bool p_allocated);
 
 	Size2i _get_size() const;
-	Size2i _get_size_2d_override() const;
+	Size2 _get_size_2d_override() const;
 	bool _is_size_allocated() const;
 
 	void _notification(int p_what);
diff --git a/scene/main/window.cpp b/scene/main/window.cpp
index 1bb01313e721..d6328972b894 100644
--- a/scene/main/window.cpp
+++ b/scene/main/window.cpp
@@ -1133,7 +1133,7 @@ void Window::_update_viewport_size() {
 	//update the viewport part
 
 	Size2i final_size;
-	Size2i final_size_override;
+	Size2 final_size_override;
 	Rect2i attach_to_screen_rect(Point2i(), size);
 	double font_oversampling = 1.0;
 	window_transform = Transform2D();


Test patch: