-
-
Notifications
You must be signed in to change notification settings - Fork 196
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
[WIP/POC] Libscroll Integration #766
base: master
Are you sure you want to change the base?
Conversation
This would also want wheelEvents to be a little bit smarter than they are now, or separate events between precise pan and normal wheel events (would probably make incremental integration a bit easier too) |
@@ -5,4 +5,4 @@ | |||
(c_names file) | |||
(c_flags :standard -Wall -Wextra -Werror) | |||
(preprocess (pps ppx_deriving.show)) | |||
(libraries threads console.lib str lwt sdl2 skia flex Rench re Revery_Native revery-text-wrap timber)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're taking out skia
and putting fontkit
back?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@glennsl whoops, not intentionally. I think that file got left behind as I was jumping between branches
You also need to run Is there anything else you want reviewed here? |
I guess the plan, but that sounds like @bryphe territory to me :) |
Just wanted to make sure the plan for actually integrating it with scrollviews isn't blatantly stupid |
That bug report was resolved with the conclusion that it only applies to "old-style" events, not "new-style" events. Is this integration using old-style events? If so, why? That model does seem pretty broken. |
That bug was caused by the driver emitting scroll events for "fling", basically where if you start a fling then move the cursor over some other scrollview the first one will stop and the new one will spontaneously start moving. My original plan was to simply add libscroll at the top level and integrate with the render loop, but if it ignores what scrollview the cursor is currently over then a similar bug would occur Would happen for either new or old style events, ends up just being an implication of doing fling or various other fancy scroll stuff without the context of the specific content it's interacting with |
Oh, I see, so that bug is just in reference to needing separate tracking objects? Then why is the two-phase approach necessary? |
The two phase approach allows interpolating/extrapolating events properly (avoids jitter from misaligned or conflicting input/output refresh rates) Good rundown on the topic: https://discourse.gnome.org/t/synchronizing-input-events-to-the-screen-refresh/1422 It would theoretically be possible to do simply with timers within libscroll, but that seemed a little janky/unecessary (makes usage a lot less predictable) It basically just tells libscroll "OK, I'll be asking for the translation required for the next frame", then allows calling the functions to get that translation idempotently |
Could it not also use an external timing source, with the current time being passed in on each call? |
It definitely could, that seemed like it would expose more of the inner workings than felt convenient for consumers to use. Would that also include the prediction/overshoot period or should that be tacked on at the end of the timestamp given? As an addendum, that might make it slightly more complicated to implement acceleration or correction, probably not much harder than handling VRR tho |
The two-phased approach seemed more like exposing implementation details to me, but then I'm still not sure how that would work. Time on the other hand is an inherent part of the model.
I don't know what any of that means :) Why is prediction needed? I'm sure you can guess by now that I haven't followed the earlier discussion all that closely. Apologies for sounding like I have no clue what you're talking about, but I mostly don't. So please don't feel like you have to explain everything in excruciating detail if you don't want to. I'm sure @bryphe will be able to give just as good feedback with much less effort needed on your end. |
No worries lol, it's a surprisingly deep subject.
Doing prediction is primarily for touchscreens, but should help make trackpads feel ever so slightly snappier. This basically tries to estimate where a touch would actually be mid way through the next frame's display period, to try to keep the delta from the onscreen content to the actual touch point as low as possible (theoretically zero if scroll velocity is constant.) A nice example of this in action (as far as I can tell) is microsoft surface tablets. From what I can see, they do some of this prediction to keep them as aligned as possible with the given touch point. Android doesn't, which leads to spacial lag (even if the actual temporal lag is imperceptible)
The two phase thing is ideally to allow users to do a minimum of their own math, just say how long frames usually last, how long until the next one will be displayed on screen, and then call mark_frame() every time a render loop happens so libscroll can do its magic. I could expose a way of asking to sample(timestamp) but that would force the consumer (in this case Revery) to do all the prediction math manually and understand the mechanics behind trying to do spacial prediction |
Ah, I see. Thanks for explaining! I think the iPad also does a great job with the Apple Pencil. So the prediction period then is basically just the time between user action and display response? And the only variable there is render time/frame rate? If so I still don't understand why phases are needed. If for every frame the client asks "give position at this time", can the server not assume this call happens exactly once per frame and estimate the frame rate based on that? And in the rare case that's not true, or the client can provide a better estimate, it can do so using an optional argument? |
Good point, thinking about it I've come around to the idea of having a requested timestamp be provided by the consumer (especially around VRR cases and the like, so I can step animations by a precise delta instead of trying to speculate) for how to sample. It also probably allows for way easier testing/mocking on my part to have that be the case. I can try getting that in place a bit more tonight probably. Question: what would this look like inside of Revery? IE do elements in revery know when they're about to be rendered such that they could know when to sample? |
Yeah, I think the rendering would be driven by the element itself in this case, much like how we do animation: revery/src/UI_Hooks/Revery_UI_Hooks.re Lines 17 to 19 in 4ca3483
This will continuously trigger a re-render as long as |
Oh very cool, I'll try to look into it a bit more tonight I guess, since that example almost perfectly fits how the library itself models things (save for events/correction and such, but those are silent here) Those hooks only get called after all input events have been serviced right? Or are they called sometimes in the middle? It should still be possible to handle for libscroll, but it would improve latency/responsiveness slightly to make sure that all events come in before that hook |
I'm not entirely sure, but the application loop certainly looks like it does: Lines 210 to 250 in 4ca3483
|
f30b1dc
to
fef8273
Compare
…scroll_integration
src/UI_Components/ScrollView.re
Outdated
switch(wheelEvent.deltaX) { | ||
| Some(delta) => { | ||
Libscroll.push_pan(scrollview, Libscroll.Axis.Horizontal, delta, wheelEvent.timestamp); | ||
} | ||
| None => () | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this an option
? Wouldn't a delta
of 0
be "neutral" and effectively do nothing anyway?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it wasn't doing interpolation/prediction then yes, but with both enabled it would mean that acceleration goes to infinity and velocity drops to zero, which would cause weird stuttering if event ordering isn't consistent (and also make it so that fling sometimes wouldn't work)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you not just weed out the 0
s from the interpolation? Or does the None
have some other significance?
Like, if it was not an option, would this not be equivalent?
if (wheelEvent.deltaX != 0) {
Libscroll.push_pan(scrollview, Libscroll.Axis.Horizontal, wheelEvent.deltaX, wheelEvent.timestamp);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably, at least on Wayland I never seem to get a zero delta. I still need to figure out how to drag prediction toward zero if velocity drops but no zero event or fling event comes in.
It seems semantically odd to special case zero delta to me but it might be fine
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find it odd that it needs to be special-cased too, but I would consider both using an option
and weeding out 0
s special-casing. A delta of 0
seems completely normal to me, so I'm wondering if the problem is somewhere else in the model. Unless you work with imperfect sensor data, which of course happens sometimes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If these were polymorphic variants, you wouldn't even need to do the mapping. It's a good use case for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, how would that work? Am not super familiar with them
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Polymorphic variants are the structurally typed equivalents of ordinary variants. Their identity is derived from the type itself, not from any definition. In this case specifically that means SDL and libscroll can share a polymorphic variant type without either library knowing about the other.
By example, it looks like this:
let functionTakingPolyVariant = (poly: [`Number(int) | `Text(string)]) =>
switch (poly) {
| `Number(num) => "Number: " ++ string_of_int(num)
| `Text(text) => "Text: " ++ text
};
let str = functionTakingPolyVariant(`Number(42));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something I'm also remembering here, is that wayland sends events for when the source axis changes for a pan, and that contains an undefined delta. I probably need a way of excluding the delta as well. Might make sense to have a more general axis enum indicating horizontal, vertical, or undefined (undefined meaning the delta is meaningless, and shouldn't be interpreted)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at how this interacts with scroll stop events makes me want to bring back the guard, for single accesses.
Effectively what I want to encode is the variant/enum
Info(Source)
Fling(Axis, Source) |
Interrupt(Axis, Source) |
Pan(Axis, Source, delta)
in a way that is fairly ideomatic in c and translates well across to reasonml/rust code. What are your opinions on doing that nicely?
I might be overfitting for the linux way of handling input, but afaik it's pretty similar on OSX and can be fully described for the Windows model (just compose multiple events)
src/UI_Components/ScrollView.re
Outdated
switch(wheelEvent.isFling) { | ||
| true => Libscroll.push_fling(scrollview, wheelEvent.timestamp); | ||
| false => () | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't use pattern matching for boolean conditionals. That's what if
expressions are for. They communicate intent better, are less noisy, and also shorter in cases like this where you can skip the else
branch:
switch(wheelEvent.isFling) { | |
| true => Libscroll.push_fling(scrollview, wheelEvent.timestamp); | |
| false => () | |
}; | |
if (wheelEvent.isFling) { | |
Libscroll.push_fling(scrollview, wheelEvent.timestamp); | |
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, will change
…scroll_integration
…scroll_integration
Creating to track feedback on integration approaches
Might be a little more complicated than I thought, since each Revery scrollview needs it's own corresponding tracking object from libscroll to avoid bugs akin to this.
Usage follows 2 phases (pipelined):
Most sane way I can think to do this is to add a Revery event that marks end of frame (signals everything to move from phase 1 to phase 2), which each scrollview itself subscribes to. Pan events are not directly used to transform the viewport, but are simply passed to libscroll scrollview instances. When an end-of-frame event comes in, the scrollview is told to mark end-of-frame and then the resulting transform directive is used much as how scroll events are handled now.
I'm a bit shaky on how some areas of Revery fit together so if any part of this sounds dumb just let me know :)