Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skwasm scene #40330

Merged
merged 46 commits into from Apr 10, 2023
Merged

Skwasm scene #40330

merged 46 commits into from Apr 10, 2023

Conversation

eyebrowsoffire
Copy link
Contributor

@eyebrowsoffire eyebrowsoffire commented Mar 15, 2023

This implements Scene and SceneBuilder in Skwasm, which lets us have golden tests and also can actually run a flutter app with skwasm, albeit missing quite a few rendering capabilities including text rendering. The subsequent changes to flutter tool that will allow compilation against the skwasm renderer will be following soon after this lands.

@flutter-dashboard flutter-dashboard bot added the platform-web Code specifically for the web engine label Mar 15, 2023
lib/web_ui/dev/build.dart Outdated Show resolved Hide resolved
lib/web_ui/dev/build.dart Outdated Show resolved Hide resolved
Copy link
Contributor

@mdebbar mdebbar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good!

I have some comments for the dart code, and I would like someone else to review the C++ code.

lib/web_ui/dev/build.dart Show resolved Hide resolved
@@ -26,7 +26,7 @@ class TestCommand extends Command<bool> with ArgUtils<bool> {
TestCommand() {
argParser
..addFlag(
'debug',
'debug-browser',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flutter tool calls this --start-paused which I think is a more self-explanatory name and keeps names consistent across repos/tools.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that is probably a better name. One detail is that this option also causes us to run in a window instead of headless, but that probably is fine to just leave implied.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's how it works in the flutter tool too.

Comment on lines +208 to +210
domDocument.body!.appendChild(sceneElement);
surface = SkwasmSurface('#flt-scene');
domDocument.body!.removeChild(sceneElement);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to change to work in element embedding mode. It's fine if you want to do that later, but let's add a TODO.

cc @ditman

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, maybe I should just add a todo. This is just a very rough initial implementation to just get things working in the most basic case. We'll also have to handle multiple surfaces once we start introducing platform views, so this embedding is very much temporary.

final SurfaceHandle _handle;
OnRenderCallbackHandle _callbackHandle = nullptr;
final Map<int, Completer<void>> _pendingCallbacks = <int, Completer<void>>{};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weak suggestion: _pendingCompleters?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm how about _pendingRenders maybe?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM

lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart Outdated Show resolved Hide resolved
Comment on lines 1019 to 1042
/// Transforms the input rect and calculates the bounding box of the rect
/// after the transform.
ui.Rect transformRect(ui.Rect rect) {
double? minX;
double? minY;
double? maxX;
double? maxY;
final Float32List vector = Float32List(2);
for (final ui.Offset point in <ui.Offset>[
rect.topLeft,
rect.topRight,
rect.bottomLeft,
rect.bottomRight,
]) {
vector[0] = point.dx;
vector[1] = point.dy;
transform2(vector);
minX = minX == null ? vector[0] : math.min(minX, vector[0]);
minY = minY == null ? vector[1] : math.min(minY, vector[1]);
maxX = maxX == null ? vector[0] : math.max(maxX, vector[0]);
maxY = maxY == null ? vector[1] : math.max(maxY, vector[1]);
}
return ui.Rect.fromLTRB(minX!, minY!, maxX!, maxY!);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you forgot to remove this in favor of the existing:

ui.Rect transformRect(Matrix4 transform, ui.Rect rect) {
_tempRectData[0] = rect.left;
_tempRectData[1] = rect.top;
_tempRectData[2] = rect.right;
_tempRectData[3] = rect.bottom;
transformLTRB(transform, _tempRectData);
return ui.Rect.fromLTRB(
_tempRectData[0],
_tempRectData[1],
_tempRectData[2],
_tempRectData[3],
);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I went ahead and did some microbenchmarking comparing the two functions. As it turns out, in dart2js, my method is only very slightly slower, about 2.5% in my measurements, but in dart2wasm my method is more than twice as fast:

[chrome-dart2js-html-engine] Running...
00:00 +0: loading bench_transform_rect_test.dart                                                         
[CHROME STDERR]:
[CHROME STDERR]:DevTools listening on ws://127.0.0.1:12345/devtools/browser/207d349b-2b04-4cba-881e-23f0afc4d755
00:02 +0: benchmarks Benchmark transformRect                                                             
Counter = 1000000 elapsed time = 2197ms
00:05 +1: benchmarks Benchmark matrix.transformRect                                                      
Counter = 1000000 elapsed time = 2252ms
00:05 +2: All tests passed!                                                                              
[chrome-dart2js-html-engine] All tests passed!
[chrome-dart2wasm-html-engine] Running...
00:00 +0: loading bench_transform_rect_test.dart                                                         
[CHROME STDERR]:
[CHROME STDERR]:DevTools listening on ws://127.0.0.1:12345/devtools/browser/c52f7c4a-2c93-4e7a-84a1-b0a9dbb5c146
00:06 +0: benchmarks Benchmark transformRect                                                             
Counter = 1000000 elapsed time = 5606ms
00:08 +1: benchmarks Benchmark matrix.transformRect                                                      
Counter = 1000000 elapsed time = 2493ms
00:08 +2: All tests passed!                                                                              
[chrome-dart2wasm-html-engine] All tests passed!

I think maybe we should switch over to my method. I might also be able to make an additional change to try to get that 2.5% to go away on the JS target, let me see.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what the benchmark looks like btw:

    test('Benchmark transformRect', () {
      final Matrix4 transform = Matrix4.rotationZ(math.pi / 3.0);
      const Rect rect = Rect.fromLTRB(-100, -100, 100, 100);
      final Stopwatch stopwatch = Stopwatch()..start();
      int counter = 0;
      for (int i = 0; i < 1000000; i++) {
        final Rect transformedRect = transformRect(transform, rect);
        if (transformedRect.contains(Offset.zero)) {
          counter++;
        }
      }
      stopwatch.stop();
      print('Counter = $counter elapsed time = ${stopwatch.elapsedMilliseconds}ms');
    });

And then copy-pasta'd except with matrix.transformRect put in place of transformRect

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made some other minor changes to narrow the gap slightly in the JS case to about 1-1.5%.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, as it turns out, these are actually slightly different, as the existing one handles perspective transforms and mine does not. I think I'm going to go ahead and roll things back and use the existing implementation, and hope that binaryen can optimize the wasm side enough to make it acceptable. If it doesn't, I can always hand-tune the wasm side later, if needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Shows you how important benchmarking is.

I'm fine with either direction you want to take.

@@ -213,5 +213,5 @@ abstract class Renderer {

ui.ParagraphBuilder createParagraphBuilder(ui.ParagraphStyle style);

void renderScene(ui.Scene scene);
Future<void> renderScene(ui.Scene scene);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is being called from a synchronous method:

void render(ui.Scene scene, [ui.FlutterView? view]) {
renderer.renderScene(scene);
}

I don't think we should change the signature of Renderer.renderScene. Since skwasm is the only one that needs the asynchronicity, let's only change SkwasmRenderer.renderScene's signature? And I would argue that even SkwasmRenderer.renderScene doesn't need to be asynchronous. Nothing is awaiting it, so effectively, we are firing the future and forgetting about it.

One concern that I have here though is race conditions. E.g. what if the previous renderScene call takes a while and we receive a new one? Should we somehow force these calls to go in sequence (by pushing them into a queue for example)? Again, if this is something we plan to handle later, let's add a TODO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is fine for this to be called and not awaited from a synchronous method in a "fire and forget" manner, and that's what is still going on right now. Making this method async at the renderer level only makes things a bit more expressive and flexible. The existing renderers which do not asynchronously render can return an immediately resolved future.

The main impetus behind doing this is because the new golden screenshot capabilities in the golden tests need to wait for the rendering to be actually complete before attempting a screenshot. So really, this future is only being awaited in tests.

As for your concern in the last paragraph, I have a lot of the same thoughts. That's sort of the reason why I'd like to expose this as an async method. In situations where the render thread is slower than our target FPS, we may want to keep track of pending futures and stop actually submitting new frames if it starts getting backed up. Otherwise, if we continue pushing more frames, the render thread will get continually more backed up, when it's better to to just drop frames. We can't do this with our current "fire-and-forget" method, but making the method async opens us up to doing this in the future.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed with Mouad, this should be synchronous for consistency with the mobile Flutter engine and our public facing API. The Skwasm renderer can have a way to await for the scene to actually be rendered which can be used in tests. As for the problem with the render thread getting backed up, this is already something that can happen in Flutter mobile and I think they just drop everything but the latest frame.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a suggested alternative? This Renderer.renderScene API is web-specific, so there is nothing in the mobile Flutter engine to keep it consistent with, and I'm also not changing the public facing API (PlatformDispatcher.render is still a synchronous method). Basically the reason I want to return a Future here is:

  • To allow us to wait for the render to complete in the tests.
  • If we do want to "drop everything but the latest frame" like the native engine does, the UI thread still needs a way to tell whether we are actually getting backed up.

A Future returned directly from the method seems like the most natural way to achieve this, but if there is something different you all have in mind I'm open to suggestions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I could have Renderer.renderScene return FutureOr<void>, so that the subclasses of Renderer that aren't asynchronous can still just return void?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should've clarified my motivation for keeping things as sync as possible. It's because I want future maintainers to be aware that this is used as a sync API and it's better to stay that way. And if they really need the asynchronicity, then they can make the deliberate decision of converting it to async.

If we make all of them async today, I'm afraid it becomes too easy in the future to add async stuff in there without thinking about the consequences. I want to conversion to async to be a deliberate decision based on a real need (like in the case of skwasm).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the FutureOr<void> change not address your issue? SkwasmRenderer's render will return Future<void>, but the other renderers still return void. So it would require us to explicitly change a renderer's return value to make it async

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't address the issue. The developer making change to renderScene will think it's okay to add an await in there because the signature already "supports" async. I want them to have the extra burden of changing the signature, which will make them look around and check whether it's fine to convert it to async or not.

That said, I don't feel strongly about it, and maybe this is all just me doing premature design optimizations :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify, if a developer is editing the renderScene function on one of the existing renderers (whose renderScene method is synchronous), and they want to add an await, they will have the extra burden of changing the signature, since the signature of the existing renderers is staying void renderScene(...). They would have to explicitly change it to Future<void> renderScene(...). The abstract Renderer class has the signature FutureOr<void> renderScene(...), but the actual concrete renderer classes themselves either explicitly return void or explicitly return Future<void>

Copy link
Contributor

@mdebbar mdebbar Apr 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh okay got it now. I misunderstood your previous comment, thought you were making subclasses FutureOr<void> too (should've looked more closely at the code 🤦‍♂️)

Ok, this actually addresses my concerns, thanks!

Co-authored-by: Mouad Debbar <mdebbar@google.com>
@flutter-dashboard
Copy link

Golden file changes are available for triage from new commit, Click here to view.

Changes reported for pull request #40330 at sha 64d5728

@flutter-dashboard
Copy link

Golden file changes are available for triage from new commit, Click here to view.

Changes reported for pull request #40330 at sha a0dbca2

@mdebbar
Copy link
Contributor

mdebbar commented Apr 6, 2023

I approved the PR but I would love someone more qualified than me to review the C++ changes.

Copy link
Contributor

@harryterkelsen harryterkelsen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@flutter-dashboard
Copy link

Golden file changes are available for triage from new commit, Click here to view.

Changes reported for pull request #40330 at sha 2d52b90

@eyebrowsoffire eyebrowsoffire added the autosubmit Merge PR when tree becomes green via auto submit App label Apr 10, 2023
@auto-submit auto-submit bot merged commit 53930ff into main Apr 10, 2023
40 checks passed
@auto-submit auto-submit bot deleted the skwasm_scene branch April 10, 2023 17:39
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Apr 10, 2023
gspencergoog pushed a commit to gspencergoog/engine that referenced this pull request Apr 10, 2023
zhongwuzw pushed a commit to zhongwuzw/engine that referenced this pull request Apr 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
autosubmit Merge PR when tree becomes green via auto submit App platform-web Code specifically for the web engine will affect goldens
Projects
None yet
4 participants