-
Notifications
You must be signed in to change notification settings - Fork 389
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
F2F Followup: Render Loop Proposal #188
Comments
One of the essential aspects about RAF is that it allows the user agent to throttle queuing of unnecessary work. For instance, the traditional RAF associated with the window is not raised when the document is on a hidden tab, minimized, or otherwise invisible. In Edge, if the GPU is bogged down with work, we throttle the RAF so that the GPU can keep up with the work the CPU is providing it. Failure to do this can lead to poor performance as frames are dropped on the floor and prevent the CPU/GPU from rendering other windows in the system in a timely manner. The nice thing about RAF is that it is a "pit of success" when it comes to expectations around frames. One callback == one frame; no more, no less. You can't get into a situation where you queue more than one frame of work because the API simply won't let you. Given this, it is essential that we prevent commit from queuing more than one frame of work during JavaScript execution. Rejecting the promise is one avenue. I would prefer that we throw an exception to make it clear to the developer that they're doing it wrong. Dropping frames means wasting CPU/GPU cycles for frames the user never sees, a problem we do not have with today's approach. Another disadvantage of the new approach is there is no way to signal the last frame or an "I am done now". Committing the last frame means you're forced to sign up for another one. I suppose you can call exitPresent right after you call Commit, but we still have to run the Commit's promise rejection code unnecessarily. The current approach also does not have this problem. I agree having a similar look to the traditional game loop is attractive but the new approach seems to create more problems than it solves. What am I missing? |
Thanks for the speedy feedback! I figured this one would spur some good discussion. :) First off, I'd like to point out that I consider there to be effectively two different but related proposals here: The frame data lifetime and the render loop shape. I totally understand having concerns about the Putting that aside, though, I'm not sure if I'm clear on the potential issues with a promise-based loop.
Is there a reason I'm not seeing that this can't be done with a promise? We can delay resolving it indefinitely.
With rAF today you can't queue up multiple frames but you can queue up multiple callbacks per frame by calling rAF repeatedly. What I was suggesting was that repeated calls to
Is equivalent to:
Actually we could probably have the I guess the bigger question, and what you may have been getting at, is what do if the developer does something like this:
But we've already established that to be a problem with the current
Actually, now that you bring that up I'm suddenly concerned about a flaw in the current system. If we call The concept of "last frame" is a funny one anyway, because I have a hard time imagining a scenario where Anyway, I'm not actually trying to argue that this is unquestionably better than a traditional rAF, I'd just like to understand your points a bit better. I think we could evaluate a few different options here as well. For example, we could keep the submit and rAF as a single call but do it callback-based if the use of promises prove to be a sticking point. We could also discuss having an implicit submit at the end of the animation loop callback, WebGL style, but that making mirroring trickier AND makes controlling latency harder, so I've been assuming the other UAs would not be in favor of it. |
I like the idea of using Promises and I think it could emulate a the benefits offered by raf currently. Also, I believe cancellable promises have been worked on for some time, and we could also standardised on some boolean check that the developer could set to false when they want to stop the promise based render loop for the meantime |
FYI: It's my understanding that there were significant technical issues with cancellable promises and that they are no longer being considered for addition to the spec. I don't object to a boolean indicating that you don't need the next frame, but I also don't see much harm in simply letting the developer ignore the returned promise when they're done? |
Yup, I agree that the promise based implementation doesn't seem to have any major drawbacks and I wasn't a big fan of re implementing raf and calling it the same |
@toji , The main thing I do not want to have happen is for the following case:
to render DrawSomeMoreStuff on some hardware but force user agents to drop frames on other hardware if the GPU cannot keep up with the work. Instead, I think we should be clear to developers that following this pattern means DrawSomeMoreStuff gets completely dropped on the floor in all instances. Returning the previous promise on commit and warning in the console for this scenario seems reasonable to me. Good point about the last frame being normally triggered as a result of some other async event. I agree this is a relatively minor drawback of the promise based approach. I agree we should tie updates to the FrameData with resolution of the promise. One thing I thought about after my initial reply. Suppose you have the case of the Three.js application with disparate pieces of code. Does each piece of code hook requestAnimationFrame on the VRDisplay itself and some primary code hooks it last and calls submitFrame? If so, the new approach will mean that the master code will need to do the moral equivalent of requestAnimationFrame to get everyone started. It will also have to pass around the subsequent promise so that the subordinate pieces of code can call then(). So, potentially more bookkeeping if that's how Three.js behaves, albeit relatively minor. |
Talking with @bfgeek about this particular issue, there's still a desire to merge this functionality with the It's useful to consider where we want things to go in the future with other types of layers. Let's presume that we have a Coming back to the present, we can look at the This implies two things about the design:
These two items probably require a formal concept of a VR compositor (no JS interface, just a spec description.) With that in mind, I'll propose a slightly different flow: Use either Everything committed in the space of a single frame is guaranteed to show up simultaneously on the display. Frame boundaries are defined by the VR compositor notifying the page when a frame has been completed. Since pages are single threaded they won't process that until javascript releases control back to the page, giving the app a simple mechanism for sync. (If you want multiple layers to display at the same time commit them all within a single callback. Note that committing and rendering don't necessarily have to happen at the same time.) Pose updates are available on the frame barriers. Mirroring continues to work effectively the same way it does now. Anything you draw to a normal canvas without calling commit gets presented to the screen at the end of the callback. For offscreen canvases you can mirror with It's a pretty big shift, but this feels to me like a pretty forward looking pattern. I'd love everyone's thoughts on it! |
One random follow up thought from my musing on the subject last night: One concrete benefit I could see to having |
@toji If you commit from the session can you not do everything you want, or do really need explicit control per layer for commits? // 3-DoF video player, no WebGL
var skyboxLayer = new VRSkyboxLayer(imgEl);
session.layers.push(skyboxLayer);
var videoLayer = new VRVideoLayer(videoEl, w, h, frameOfReference, position, /*etc*/); // 2D
session.layers.push(videoLayer);
session.commit(); // set and forget no?
// every frame of VR compositor uses latest state of videlLayer.videoEl, skyboxLayer.imgEl, etc. Come to think of it scenarios like this (no WebGL canvas) what is (any) explicit commit doing? Do different layer types have different commit behavior, where some don't need commit per frame but others do? -- var l1 = new VRLayer(canvas, mainBounds);
var l2 = new VRLayer(canvas, gazeCursorBounds);
session.layers.push(l1, l2);
renderLoop() {
drawMainScene(mainBounds);
drawCursor(gazeCursorBounds);
canvas.commit().then(renderLoop);
} |
In your first code snippet I really don't think there should be a There's pros and cons for both per-layer commits and session level commits. I think we could plausibly go either way and have solid reasoning for the choice. As far as I can see it's mostly a question of more explicit layer synchronization control vs. more flexible layer composibility. (Of course none of this matters until we support multiple layers. Up to that point the two methods are identical, but it's worth considering the multi-layer case now so we don't have to change or complicate things down the road.) I think it's probably a given that we want a way to guarantee that you can update N layers and have the new imagery for each layer be displayed on the HMD simultaneously. Relatedly, a way to indicate that all layers have been updated and can be submitted early is desirable, so we can have the imagery in flight while the page is doing physics simulations and such. A session-level var sceneLayer = new VRCanvasLayer(canvas1);
var cursorLayer = new VRCanvasLayer(canvas2, { headlocked: true });
function renderLoop() {
drawScene();
drawCursor();
session.commit().then(renderLoop);
updatePhysics();
} It can also handle sparse layer updating by speccing that we only update imagery from canvases that are currently dirty (which is a well established concept already). Where it gets tricky is if we want to allow layers to be updated outside the main rendering loop. Picture a WebVR performance HUD that runs in an extension or as a drop in utility library. // main.js
var sceneLayer = new VRCanvasLayer(canvas1);
function renderLoop() {
drawScene();
session.commit().then(renderLoop); // Hey, has the hud updated yet?
updatePhysics();
}
// webvr-perf-hud.js
var hudLayer = new VRCanvasLayer(canvas2);
function renderHudLoop() {
drawHud();
// Whoops! Can't call session.commit() here.
// Needs main.js to be aware of this file, which limits injectability.
// Could monkeypatch session.commit() to do this drawing at the last second, I suppose?
} A per layer commit allows this scenario to work out really nicely, though! // main.js
var sceneLayer = new VRCanvasLayer(canvas1);
function renderLoop() {
drawScene();
sceneLayer.commit().then(renderLoop);
updatePhysics();
}
// webvr-perf-hud.js
var hudLayer = new VRCanvasLayer(canvas2);
function renderHudLoop() {
drawHud();
hudLayer.commit().then(renderHudLoop);
} Both commits would get the same promise back, so they'd stay in sync nicely even though they have effectively no knowledge of each other. BUT! There's a less obvious issue here which is that we still want everything updated within a single frame to be presented to the HMD at the same time. This means I can't just shove a new image into the VR compositor immediately on calling You can work around that without too much trouble by doing something along the lines of function renderLoop() {
drawScene();
sceneLayer.commit().then(renderLoop);
setTimeout(updatePhysics, 0); // Force this to happen outside the commit promise resolve.
} But that is a bit awkward. Now is this style of loosely coupled, independently updating layers something that we must support? No. I personally like it, and I feel that the developer community will be able to use it to good effect, but I'm also certain that the API can survive and thrive without it. We've also faced questions in the past, however, about how to handle things like multiple calls to |
Following up on the sync point issue from the previous comment if we went the per-layer commit route, I wanted to take a stab at trying to describe how it would work more formally to help clarify it. The idea being that this is the type of text I'd like to see in the spec: The VR CompositorEvery UA which implements the WebVR spec MUST maintain a VR compositor which handles layer composition and frame timing. There are no direct interfaces to the compositor, but applications may submit imagery to it via the layer system and observe the frame timing via calls to The compositor consists of two different loops, assumed to be running is separate threads or processes. The Frame Loop, which drives the page script, and the Render Loop, which continuously presents imagery provided by the frame loop to the VR Display. The render loop maintains it's own copy of the session's layer list. Communication between the two loops is synchronized with a lock that limits access to the render loop's layer list. Both loops are started when a session is successfully created. The compositor's render loop goes through the following steps:
The render loop MUST throttle it's output to the refresh rate of the VR Display. The exact point in the loop that is most effective to block at may differ between platforms, so no prescription is made for when that should happen. Upon session creation, the following steps are taken to start the frame loop:
Then, the frame loop performs the following steps while the session is active:
|
@toji , thank you very much for driving this issue and being so thorough. I agree that everything committed in the space of a single frame should show up on the display at the same time. When the user agent resolves the (same) promise returned from all of the layers, all of the However, we need to decide what happens if the web developer calls getFrameData/draws between the time the previous frame has retired but the new frame isn't ready yet. This corresponds to the time between step 3 and step 4 in the frameloop. In practice, I expect this to happen if the web developer draws during an onload handler or adds a Separately. Drawing to multiple layers that point to the same canvas, with a commit call in between, seems like it will work. Since the user agent has no idea which layer a canvas draw is destined for, it will have to keep its own backing texture for each layer that is separate from the canvas. At In an ideal world, we would provide web developers with a way to take a drawing context and set it on any canvas element via a new |
Alternatively to my previous issue, we could have the frame data get passed as a parameter of the fulfillment. That makes it more clear to the web developer where and when they can get updated data. However, only |
Well crumb. I had a nice long response here and lost it in a browser crash. 😢 So here's my abbreviated version: Rafael already told me personally that he realized an issue with the frame data getting passed as a parameter for the frame promise resolve, but for the sake of getting it down in text: That would prevent us from being able to use multiple coordinate systems within a single frame, which can be useful now (for things like tracking loss fallbacks) but will become much more important in the future with AR and tracking anchors. A possible alternative is to pass an object into the frame promise resolve that you query poses off of. I think that's an interesting idea, but I'm not sure how well it would work with magic window mode, since there's not really a need to create a layer in that case. Actually, I just realized that I've got this wrong in the explainer as a result of the API shifting around and we should figure out what the pattern we want for that is. Should a non-exclusive session still set up a layer just for the purpose of calling If we assume that the pose still comes off the session to avoid magic window layer weirdness, the question of what happens to commits made outside the frame resolve is still valid. For one, we don't want to outright reject the commit: It should still get a valid promise, because that would be the only way to begin observing the main frame loop again if you had stopped for a while for whatever reason. The imagery is stuck in a weird spot, though. It wouldn't actually be accepted until, at earliest, the resolve of the next frame but it would be rendered with the pose from the last one. If it's a headlocked layer that wouldn't really matter. Outside of that, though, seems like the options are:
1 the most restrictive but easiest to support. 3 is probably best from a developers perspective, but I don't know if that could be supported on all platforms. 2 is an awkward middle ground that is probably supported anywhere, but I'm honestly not sure how objectionable the artifacts from that would be. Gut says 1 is the best thing to do right now, and maybe we can loosen it up in the future if we see the need? Semi-Unrelated: One thing I think I'd like to change from my proposal earlier in the thread is that only the first commit for a layer should be respected, instead of only the last. That way we can support a fast path that says "Once every committable layer has been committed the frame can be submitted early without waiting for the current callback to end." (Parting thought: I apparently have a strange idea of an "abbreviated version".) |
Oh, and thanks for pointing out that you can do a |
Reading the explainer, I see that in both the magic window and exclusive mode branches of One way we can make the in-between state more manageable is to have a Another advantage of |
Well, let me say up front that based on feedback we've gotten from web platform leads in the past I think that a begin/end call is probably something they'll protest. Just so we're all aware. Putting that aside, I think the idea has merit. I especially like the idea of a concrete signal that we're about to start drawing VR content for the exact reason you stated. I'm not clear if you intend for I have a few more thoughts but I'm going to have to come back to them later, wanted to give @RafaelCintron a chance to answer my question in the meantime. |
Brought up the idea of Stewed on the general topic of render loops some more over the weekend. I wanted to dive into magic window's relationship to the pose production a bit more. The explainer currently uses a layer and I should note that as far as magic window mode goes I think that perfect, consistent, low latency pose delivery is far less important than when you've got a screen strapped to your eyeballs, so I wouldn't stress too much about using The important thing to note is that any reliance on |
Clarification request: what happens to window.requestAnimationFrame while VR presentation is in progress? Presumably, if there's an external display with a visible tab, window.rAF will continue firing at the appropriate rate independently from the VR display's refresh rate. But what about the case if there's no external display such as on mobile? Since the native display is no longer visible while presenting, it would be natural to stop processing window.rAF while presenting (Chrome Canary currently does so), but this may be surprising to developers who are expecting it to keep running to handle background processing. This could happen indirectly if a non-VR-aware third party library uses its own window.rAF callbacks. |
That's basically my situation: i have a few libraries that rely on window.rAF for queuing and stepping processes, and don't really need to be aware if there's a VR display, or if the display has a refresh rate of 60 or 90 Hz. It's been a pretty common pattern to have rAF as a replacement for setTimeout/setInterval, ever since rAF was first introduced. Some of this libraries work completely encapsulated and isolated, so it's messy to provide them with a changing rAF method. |
Edge will work correctly without BeginDraw. However, lack of BeginDraw means we will be forced to do a buffer copy in the case where the same Canvas texture is used for multiple layers. I want to give as much performance back to the web developer as I can. Out of curiosity, when you say that you are going to move to a system where "the intended render target is bound to any source canvases right before the commit promise is resolves", how are you going to know what the intended render target is in the case where one Canvas is bound to multiple layers? For pose data, I think we need to provide stronger spec guarantees than "whenever pose data is available". With this logic, a conformant implementation can provide different, updated poses with each call to For Edge, the plan for implementing The reason I suggested that Unfortunately, since |
@klausw and @spite: @RafaelCintron: Is there a reason I'm not seeing that even if the pose was only updated in the commit promise resolve you couldn't make the stale value available outside those callbacks? I think I can get behind making The single canvas for multiple layer case is one I admittedly didn't think of re: the need for |
Yes, we can make the stale value available outside of the callbacks if we keep the
In any mode (exclusive or magic window), I think the frame loop should run at the rate of the VRDisplay that the layer is attached to. Depending on the display, that may or may not be the same rate as window.requestAnimationFrame.
Specifying that commit always clears Canvases set on VRLayers regardless of the mode prevents web developers from setting true on preserveDrawingBuffer and adding it to the DOM for debugging purposes. Is that a scenario we want to keep working?
OK. Let me know what you hear back. |
I have zero qualms about requiring that fresh poses can only be exposed within the commit resolve (phrased that way to allow late querying within the callback if a UA wants to do it, though it should still return the same value after the first query.) Agreed that it would be bad to allow implementations to do a fresh query whenever.
Okay, I don't object to that either. It does mean that Magic Window should be done with
I think we're both saying the same thing here, I just explained it poorly. My point was that if |
The point I was trying to make was that My motivation for standardizing on Did you ever get an answer to |
Okay, I totally read your first comment wrong then. Sorry!
Yeah, things are a bit of a mess in this regard right now. 😞 So your goal (and mine, at this point) is to unify the Magic Window and VR rendering paths as much as possible, right? How's this sound?
So now the setup and render loop looks identical for Magic Window and VR presentation: function OnSessionCreated(session) {
if (session.creationParameters.exclusive) {
// This seems to be the only divergent point?
let sourceProperties = vrSession.getSourceProperties();
glCanvas.width = sourceProperties.width;
glCanvas.height = sourceProperties.height;
}
vrSession.depthNear = 0.1;
vrSession.depthFar = 100.0;
vrCanvasLayer = new VRCanvasLayer(vrSession, glCanvas);
vrSession.baseLayer = vrCanvasLayer;
// Kick start the draw loop. If glCanvas is not dirty this wouldn't actually display anything.
vrCanvasLayer.commit().then(OnDrawFrame);
}
function OnDrawFrame() {
if (vrSession) {
let pose = vrSession.getDevicePose(frameOfRef);
// Only render if a new pose is available
if (pose) {
for (let i in pose.views) {
let view = pose.views[i];
let bounds = vrCanvasLayer.getViewBounds(i);
// Remember that bounds are UVs and viewports are pixels
gl.viewport(bounds[0] * gl.drawingBufferWidth,
bounds[1] * gl.drawingBufferHeight,
bounds[2] * gl.drawingBufferWidth,
bounds[3] * gl.drawingBufferHeight);
DrawScene(view.viewMatrix, view.projectionMatrix);
}
}
vrCanvasLayer.commit().then(OnDrawFrame);
} else {
// WebVR not supported or no session available. Render some fallback.
}
} That actually opens up a few interesting new possibilities: When browsing in-VR the matrices returned could take into account the canvas' position on the page and the position of the headset relative to the page to create an actual magic window through to the scene behind it. (ZSpace would probably also work well with that.) Additionally, the N viewport support allows even magic window to request stereo rendering. A nice feature here is that it actually leaves quite a bit of the decision making for how content is requested and presented in the hands of the UA/platform. Thoughts? |
Accounts for the latest discussion in #188.
Updated the explainer (3e22c49) to take into account the discussion above, though I've left the N viewport stuff out of it for the moment. Let me know how that sits with everyone. |
I like the unification you're proposing with regard to Magic Window and other session types! I also like the N-viewport proposal as well as the "interesting possibilities" you pointed out near the end. Internally, several of us have been brainstorming ways we can improve the layer proposal with an eye towards leveraging multi-GPU scenarios and better solve the buffer copy problem with the current commit approach. @NellWaliczek has volunteered to write up a summary as a separate issue. |
Looking forward to seeing it, thanks! |
This has been incorporated into the explainer now, so closing. |
A persistent issue that has come up with WebVR as it exists today is the details of how the render loop works. Things like when the frame data is allowed to update and what should happen if
submitFrame
is not called have remained somewhat stubbornly unresolved. Additionally, there's calls from the web platform side to tie the canvas presentation mechanism more closely to thecommit()
ortransferToImageBitmap()
patterns being established byOffscreenCanvas
. This proposal attempts to give us a basic frame flow that we can work off of, and usefully extend in the future.Issues being addressed:
VRFrameData
is updated (When is getFrameData/getPose allowed to query new values when not presenting? #81, Clarify ambiguity regarding when VRFrameData is refreshed #112)submitFrame
(Describe what happens when submitFrame is not called every frame. #69, Clarify what happens when submitFrame is called at the wrong time #138, Feedback: VRDisplay:submitFrame() should be dropped #170)requestAnimationFrame
variants. (Feedback: Duplicate requestAnimationFrame API is confusing #171)Overview:
The first big change proposed here is to get rid of
VRDisplay.requestAnimationFrame
andVRDisplay.submitFrame
and combine them into a single, promise-returning call:VRSession.commit
.commmit
would function likesubmitFrame
does today, taking any dirty canvases from the layer list and pushing their contents to the VR compositor. It's important that we can continue doing this explicitly so that the render loop can dispatch it's rendering work as soon as possible to reduce latency, then continue doing other work (rendering a mirrored view, simulation logic, etc.) in the same loop. The promise returned bycommit
resolves when the next frame is ready to be rendered, effectively allowing it to take the place ofrequestAnimationFrame
as well.(For the record this isn't too far off from what's being discussed for OffscreenCanvas)
To pre-emptively answer the question of why we wouldn't just want to use
gl.commit()
for this, as suggested by #174: We do plan on supporting multiple layers, each potentially with their own canvas eventually. In that scenario how would you update them all in sync? Whichcommit
do you use to drive the render loop? When does the contextcommit
switch from running at the monitor framerate to theVRDisplay
framerate? Having the commit reside with the VR system that's using it rather than one of it's sources appears to be a more consistent solution.As for frame lifetime the WebVR spec currently defines that the
VRFrameData
returned is the same between calls tosubmitFrame
. This is so that apps composed of multiple libraries that all query their own frame data are in sync. This was problematic becausesubmitFrame
wasn't always called, such as in the case of magic window uses. This proposal changes that to say that the frame data is update with eachcommit
resolve. So in essence the app must pump the render loop in order to continue receiving new frame data. This works better than it would have previously due to the proposal in #179 that makes even magic window mode into an explicit session that must be started and ended. So now any use ofVRFrameData
can be considered to occur while "presenting" (with different platforms able to make different choices about when that's allowed.)I would expect that on many systems (like Chrome) the implementation will actually resolve the commit promise when new frame data has been made available, possibly over an IPC channel, but other platforms may worry that retrieving the frame data at the beginning of the callback like that may introduce unwanted latency. For those platforms they can simply defer fetching the frame data until the first call to
getFrameData
, and only reset the dirty bit on that data when the nextrequestVRFrame
callback is ready.Side note: While it's not part of this proposal future API revisions could also introduce an 'updateFramePrediciton' function as has been suggested by Microsoft in the past to force a mid-frame frame data refresh if we felt it was necessary.
Combined these changes create a more tightly controlled render loop with more predictable behavior that's harder to use "wrong". Hopefully that outweighs a couple of minor oddities listed below.
Rough IDL:
Example usage:
Also, one really nice side effect of using promises is that with async/await support we can get something that looks an awful lot like a more traditional native game loop, complete with blocking swap!
Oustanding questions:
rAF
mechanism it doesn't leave us with an analog forcancelAnimationFrame
. Do we care?commit
multiple times? Reasonable options are to reject the promise or silently discard the rendering and resolve with the previously scheduled frame.commit()
without necessarily having rendered anything. Is that a problem?The text was updated successfully, but these errors were encountered: