跪拜 Guibai
← Back to the summary

Flutter Windows Multi-Window Focus Bug Fixed, but Impeller Breaks Thin Lines

Because I've been using Flutter multi-window for a while, there was a Windows multi-window bug that was quite annoying. When switching to a project child window, it would activate the main window by default, and only then could you click again to return to the child window:

More precisely, activating a child RegularWindow would cause the main window to come back to the foreground and cover it.

This problem had many triggers, but the main issue was that the code was written too carelessly.

One reason was in the Engine. When the Engine received the WM_ACTIVATE message from Windows, regardless of whether the window was being activated or deactivated, it would execute:

FocusRootViewOf(this);

That is, it would give focus to the Flutter view in the current window. But the problem was, when the main window was losing focus, it would also execute this line, causing the main window, which should have retreated to the back, to actively SetFocus itself, and then Windows would reactivate the main window and bring it to the front.

This Bug aside, the main issue was that the Flutter framework also attached the focus trees of multiple windows at the wrong level. In Flutter's multi-window setup, each window has its own View, but in the Framework, a child window's FocusScope could still be attached under the parent window's FocusScope. This created a new problem:

When a child window gained focus, the main window's focus scope was also considered "focused," and then the main window would again notify the engine: "I have focus, activate my native window," and again, it would pull the main window to the front...

Moreover, the entire process did not check whether another window had already taken focus, which meant this Bug was caused by a combination of several issues and couldn't be fundamentally fixed for a long time.

I could only write fallback code to force a switch.

However, these three problems were finally fixed recently:

After I merged this part of the code, I immediately fell into another pit: Flutter Windows started enabling Impeller. Yes, Flutter is starting to adapt Impeller on Windows using OpenGLESSDF:

Then the problem appeared again. When drawing on a canvas, thin lines appeared roughly 1.3–1.6 physical pixels thicker than in Skia, and they were also darker:

If alpha was overlaid, it would also directly fail. These thin lines, which were previously faintly visible in my project, became thick and bold after Impeller was introduced:

According to calculations from the issue, the change in thickness varies at different scales on Impeller:

Stroke Width (logical→physical) Skia Coverage Impeller Coverage Multiplier
0.5 → 0.75 px 0.75 2.00 2.7×
1.0 → 1.5 px 1.50 2.78 1.9×
2.0 → 3.0 px 3.00 4.59 1.5×
4.0 → 6.0 px 6.00 7.50 1.25×

And thickness isn't even the main problem. The main problems are:

This problem looks like an issue with Impeller's SDFAlpha implementation. The smoothstep range is a fixed physical pixel width. For example, smoothstep(-fade_size, fade_size, sdf) produces a gradient transition band of about half a pixel_size * aa_pixels on each side of the stroke edge, and this transition band is a fixed width (depending on the aa_pixels parameter, which is set as a constant in the code).

For analytical AA like Skia's, coverage = stroke width. But for SDF's smoothstep, coverage = stroke width + fixed edge transition band. So when the stroke is only 0.75 physical pixels wide, this fixed ~1.2px transition band becomes very noticeable, causing the rendered line to be a "solid wide line + blur on both sides."

Additionally, the minimum width clamping in float half_stroke = max(stroke_width, base_pixel_size) * 0.5 does not compensate for alpha. When stroke_width < 1 pixel, this forces the width to be clamped to at least 1 pixel (base_pixel_size), but it doesn't proportionally reduce alpha to compensate.

Skia's approach is sub-pixel line width: it turns the line into a 1-pixel wide line but proportionally reduces alpha (a 0.75px line ≈ 38% opaque 1px line). Impeller's SDF implementation directly gives a fully opaque 1px+ wide line, so it looks darker and thicker.

That is, SDFStroke does not incorporate alpha into the calculation at all. The alpha value is processed elsewhere by the time it reaches the fragment shader and is not linked to the width clamping.

Current temporary workarounds include:

It seems that Impeller on Windows will likely have to go through the same issues it had on Android. It must be said, the maintenance cost of a self-rendering engine is indeed high. However, multi-window on Windows is now truly usable. Since the bug was fixed, there have been basically no major problems.

image.png

Links

https://github.com/flutter/flutter/issues/187436

https://github.com/flutter/flutter/issues/188911

Comments

Top 2 of 3 from juejin.cn, machine-translated. The original thread is authoritative.

coclyun

Will multi-window still not make it to the stable release next time?

恋猫de小郭

A bit difficult.

若离若离

Front row [strong]