Monado Session状态流转
先前在《Monado Out of Process流程分析》一文中有提到了XrSession的部分,只是简单描述了一下整个XrSession的创建流程,对于OpenXR来说,XrSession是类似于Android中Activity的存在,是一个比较重要的概念,因此打算特别写一篇文档来讲述一下XrSession的几个状态流转过程和对应的具体场景。
在OpenXR的spec中,XrSession是有定义的:
A session represents an application’s intention to display XR content to the user.
需要速成的可以直接到最后看时序图《Flow Chart》
Life Cycle
官方Spec的定义中,一共给出了8个状态,我把这8个状态分为了三种类型,简单称为:初始,可见,退出。
- 初始:IDLE,READY,SYNCHRONIZED
- 可见:VISIBLE,FOCUS
- 退出:STOPPING,EXITING,LOSS_PENDING
以下是Spec附上的XrSession Life Cycle流转图:
State Defined
各个State的详细定义在Spec上是有专门介绍的,这里我们逐条理解一下。
A typical XR session coordinates the application and the runtime through session control functions and session state events.
- OpenXR Session的state event是跟随着Session相关的控制函数调用而改变的(等于没说)
- The application creates a session by choosing a system and a graphics API and passing them into xrCreateSession. The newly created session is in the
XR_SESSION_STATE_IDLE
state.
- 应用在通过
XrCreateSession
创建Session的时候,需要带入System和Graphic API,其中System ID是在XrCreateInstance
后可以通过XrGetSystem
来获取,而Graphic API的部分,以OpenGLES为例,就是填充它:XrGraphicsBindingOpenGLESAndroidKHR
- 新创建出来的Session是处于
XR_SESSION_STATE_IDLE
状态
- The application then monitors for session state changes via XrEventDataSessionStateChanged events.
- 在Session创建完成之后,应用程序可以通过监听
xrPollEvent
中type
为XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED
的事件来知晓Session状态的变化
- When the runtime determines that the system is ready to start transitioning to this session’s XR content, the application receives a notification of session state change to
XR_SESSION_STATE_READY
. Once the application is also ready to proceed and display its XR content, it calls xrBeginSession and starts its frame loop, which begins a running session.
- 这段文字就非常奥妙了,它表达了两层含义:
- 第一层含义是,如果Runtime的System已经ready了,并可以开始处理,那么应用端应该会收到
XR_SESSION_STATE_READY
的通知,显然,这个通知应该由Runtime负责发出来。 - 第二层含义是,如果应用端已经准备生产(画东西)和显示XR的内容时,那么它需要调用
xrBeginSession
并开始做frame loop
,通过调用xrBegionSession
,然后把这个Session视为a running session
- 第一层含义是,如果Runtime的System已经ready了,并可以开始处理,那么应用端应该会收到
- While the session is running, the application is expected to continuously execute its frame loop by calling xrWaitFrame, xrBeginFrame and xrEndFrame each frame, establishing synchronization with the runtime. Once the runtime is synchronized with the application’s frame loop and ready to display application’s frames, the session moves into the
XR_SESSION_STATE_SYNCHRONIZED
state. In this state, the submitted frames will not be displayed or visible to the user yet.
- 在Session进入到running状态后,应用就可以开启
xrWaitFrame
,xrBeginFrame
,xrEndFrame
的循环了,通过这个流程跟Runtime端建立synchronization
机制,就是一个frame
的生产/消费同步。 - 当这个同步机制建立以后,那就意味着Runtime端已经做好了显示应用
frame
的准备了,这个时候Session需要切换到XR_SESSION_STATE_SYNCHRONIZED
的状态。 - 同时,当Session处于
XR_SESSION_STATE_SYNCHRONIZED
状态时,应用提交的frame是不会被显示出来的。
- When the runtime intends to display frames from the application, it notifies with
XR_SESSION_STATE_VISIBLE
state, and sets XrFrameState::shouldRender
totrue
in xrWaitFrame. The application should render XR content and submit the composition layers to xrEndFrame.
- 当Runtime已经准备好显示来自应用的Frame时,就可以把Session的状态切换到
XR_SESSION_STATE_VISIBLE
。 - 同时,当Session处于
XR_SESSION_STATE_VISIBLE
的状态时,应用在调用xrWaitFrame
时的,其获取的返回值:XrFrameState::shouldRender
为True - 对于应用来说,需要绘制的所有内容会以
layer
的形式一并提交给Session,也就是当应用调用xrEndFrane
的时候,Runtime那边才会收到来自应用的绘制内容。
- When the runtime determines the application is eligible to receive XR inputs, e.g. motion controller or hand tracking inputs, it notifies with
XR_SESSION_STATE_FOCUSED
state. The application can expect to receive active action inputs.
- Runtime会判断哪个应用可以收到来自XR设备的input事件,这个时候就会发送
XR_SESSION_STATE_FOCUSED
,从这之后,应用就可以期望收到action input
的事件了。- 这一段描述中,可以看到理论上只有一个Session可以处于
Focus
的状态了,望文生义determines the application is
而不是applications
?
- 这一段描述中,可以看到理论上只有一个Session可以处于
- When the runtime determines the application has lost XR input focus, it moves the session state from
XR_SESSION_STATE_FOCUSED
toXR_SESSION_STATE_VISIBLE
state. The application may need to change its own internal state while input is unavailable. Since the session is still visible, the application needs to render and submit frames at full frame rate, but may wish to change visually to indicate its input suspended state. When the runtime returns XR focus back to the application, it moves the session state back toXR_SESSION_STATE_FOCUSED
.
- Runtime会根据一些情况来判断当前application是否可以继续收到
input
事件,如果丢失了input focus
,那么就会从XR_SESSION_STATE_FOCUSED
切换到XR_SESSION_STATE_VISIBLE
,这个时候spec的建议是应用需要自行决断,做一些适当的业务行为的变更。同时,由于这个时候应用的Session实际还是处于visible
的状态,因此还是要保证full frame rate
。 - 如果Runtime在后续的行为中,把
focus
交还给了这个应用,那么还是同样需要把Session的状态切回到XR_SESSION_STATE_FOCUSED
的状态。
- When the runtime needs to end a running session due to the user closing or switching the application, the runtime will change the session state through appropriate intermediate ones and finally to
XR_SESSION_STATE_STOPPING
. When the application receives theXR_SESSION_STATE_STOPPING
event, it should stop its frame loop and then call xrEndSession to tell the runtime to stop the running session.
- 当Runtime因为收到来自用户的退出行为,或者是需要切换到其他应用时,就需要切换到
XR_SESSION_STATE_STOPPING
的状态 - 当应用接收到来自Runtime的
XR_SESSION_STATE_STOPPING
通知时,需要停掉它的frame loop
并调用xrEndSession
,以告知Runtime当前Session需要stop。
- After xrEndSession, the runtime transitions the session state to
XR_SESSION_STATE_IDLE
. If the XR session is temporarily paused in the background, the runtime will keep the session state atXR_SESSION_STATE_IDLE
and later transition the session state back toXR_SESSION_STATE_READY
when the XR session is resumed. If the runtime determines that its use of this XR session has concluded, it will transition the session state fromXR_SESSION_STATE_IDLE
toXR_SESSION_STATE_EXITING
.
- 当应用端调用了
xrEndSession
之后,Runtime会把当前Session的状态变更为XR_SESSION_STATE_IDLE
。如果只是临时的pasue,那么后面Runtime会继续保持Session为XR_SESSION_STATE_IDLE
的状态,并在适当时候重新切到XR_SESSION_STATE_READY
的状态。 - 如果Runtime认为当前的Session已经是完全结束了,那么就会把它的状态从
XR_SESSION_STATE_IDLE
切换到XR_SESSION_STATE_EXITING
- When the application receives the
XR_SESSION_STATE_EXITING
event, it releases the resources related to the session and calls xrDestroySession.
- 当应用收到
XR_SESSION_STATE_EXITING
的事件后,需要调用xrDestroySession
来完成整个闭环
Code Impl
Help Func
在看整个Session流转的flow之前,我们需要先看一下对应的push和poll是怎么做的。
State Push
在函数调用上,是直接通过oxr_session_change_state
来开启整个流程的。
|
|
而push函数则比较单纯了:
|
|
State Pop
|
|
Application Code Flow
以HelloXR的代码为例,描述一下整个应用端对于Session的是怎么做流转的。
android main loop
|
|
init session
|
|
Generate IDLE -> READY
IDLE和READY的事件是在xrCreateSession的时候push进去的。
- xrCreateSession
|
|
xr events loop
- PollEvents函数
|
|
render loop
|
|
From IDLE -> READY
当应用端第一次调用
xrCreateSession
的时候,在Runtime Client会直接push两个事件到event队列:XR_SESSION_STATE_IDLE
XR_SESSION_STATE_READY
在应用进行第一次
program->PollEvents(&exitRenderLoop, &requestRestart);
的时候,会拿到XR_SESSION_STATE_IDLE
- 由于在
PollEvents
中是循环调用的xrPolLEvents
,因此在处理完IDLE事件后,会马上收到:XR_SESSION_STATE_READY
- 这个时候会进入到Session Changed业务函数:
HandleSessionStateChangedEvent
- 调用
xrBeginSession
- 并把
并置m_sessionRunning=true
- 调用
- 这个时候应用端就会进入到render loop中,调用到
xrWaitFrame
至此,整个流程已经走到了READY的状态,那么根据spec第4步的描述,需要建议SYNC的机制:
- While the session is running, the application is expected to continuously execute its frame loop by calling xrWaitFrame, xrBeginFrame and xrEndFrame each frame, establishing synchronization with the runtime. Once the runtime is synchronized with the application’s frame loop and ready to display application’s frames, the session moves into the
XR_SESSION_STATE_SYNCHRONIZED
state. In this state, the submitted frames will not be displayed or visible to the user yet.
- 在Session进入到running状态后,应用就可以开启
xrWaitFrame
,xrBeginFrame
,xrEndFrame
的循环了,通过这个流程跟Runtime端建立synchronization
机制,就是一个frame
的生产/消费同步。- 当这个同步机制建立以后,那就意味着Runtime端已经做好了显示应用
frame
的准备了,这个时候Session需要切换到XR_SESSION_STATE_SYNCHRONIZED
的状态。- 同时,当Session处于
XR_SESSION_STATE_SYNCHRONIZED
状态时,应用提交的frame是不会被显示出来的。
From READY -> SYNC
当进入到第一个render loop,我们按照正常的流程运行xrWaitFrame
,xrBeginFrame
,xrEndFrame
。
- 由于我们当前的状态为READY,所以在应用调用
xrWaitFrame
的时候shouldRender=False
。
|
|
- 因此在应用端进行
xrBeginFrame
和xrEndFrame
时,会有一次空layer
的情况。
|
|
- 跑到Runtime Client端在做
xrEndFrame
时,会因为layer size = 0
进入到一个特殊的分支frameEndInfo->layerCount == 0
|
|
- 从而完成了一个
XR_SESSION_STATE_SYNCHRONIZED
的事件入队,这个也是符合spec第4步的定义:
- Once the runtime is synchronized with the application’s frame loop and ready to display application’s frames, the session moves into the
XR_SESSION_STATE_SYNCHRONIZED
state. In this state, the submitted frames will not be displayed or visible to the user yet.
From SYNC -> VISIBLE -> FOCUS
由于在空Layer
的xrEndFrame
中产生了一个XR_SESSION_STATE_SYNCHRONIZED
,因此在下一个轮询,也就是第三次:program->PollEvents(&exitRenderLoop, &requestRestart);
的时候:
- 应用端会收到:
XR_SESSION_STATE_SYNCHRONIZED
,但是在业务流程中并没有涉及,因此不做处理
|
|
因此在这个poll的轮询中,我们又push了两个事件:XR_SESSION_STATE_VISIBLE
,XR_SESSION_STATE_FOCUSED
。
所以应用端也就会马上响应到这两个事件了。
- 在代码中,
oxr_session_poll
的似乎存在一个bug,就是无法从FOCUS状态回退到VISIBLE状态,加入我们出现了VISIBLE为true,但是FOCUSE为false的情况,当XRT_COMPOSITOR_EVENT_STATE_CHANGE事件发生时:
|
|
而实际情况,在Runtime进程端:
ics->client_state.session_visible = visible;
ics->client_state.session_focused = focused;
永远是相同的情况,也就是永远为ture或者永远为false。
之后的步骤实际是从FOCUS的回退了。
From FOCUS -> … -> EXITING
在HelloXr
的Sample Code中,需要通过用户按下quit
键来实现退出的功能,但实际在手机端进行操作时由于Action
章节还没有完全看明白,所以无法接上这一块的代码,只能通过修改一下代码,做patch方案。
|
|
这个其实就对应了Spec中的第8步,收到了来自用户的退出行为。
- When the runtime needs to end a running session due to the user closing or switching the application, the runtime will change the session state through appropriate intermediate ones and finally to
XR_SESSION_STATE_STOPPING
. When the application receives theXR_SESSION_STATE_STOPPING
event, it should stop its frame loop and then call xrEndSession to tell the runtime to stop the running session.
- 当Runtime因为收到来自用户的退出行为,或者是需要切换到其他应用时,就需要切换到
XR_SESSION_STATE_STOPPING
的状态- 当应用接收到来自Runtime的
XR_SESSION_STATE_STOPPING
通知时,需要停掉它的frame loop
并调用xrEndSession
,以告知Runtime当前Session需要stop。
- 实际代码的话,也确实是这样,首先是
xrRequestExitSession
|
|
- 因此应用端在做
xrPollEvent
的时候就会依次收到:XR_SESSION_STATE_VISIBLE,XR_SESSION_STATE_SYNCHRONIZED,XR_SESSION_STATE_STOPPING
|
|
当调用到
xrEndSession
,详细的Runtime进程的行为可以参考《Monaod Out of Process流程分析》一问的《Session-xrEndSession》一节。这边需要重新看一下spec的第9步,代码的部分就是这些说明描述的还原
- After xrEndSession, the runtime transitions the session state to
XR_SESSION_STATE_IDLE
. If the XR session is temporarily paused in the background, the runtime will keep the session state atXR_SESSION_STATE_IDLE
and later transition the session state back toXR_SESSION_STATE_READY
when the XR session is resumed. If the runtime determines that its use of this XR session has concluded, it will transition the session state fromXR_SESSION_STATE_IDLE
toXR_SESSION_STATE_EXITING
.
- 当应用端调用了
xrEndSession
之后,Runtime会把当前Session的状态变更为XR_SESSION_STATE_IDLE
。如果只是临时的pasue,那么后面Runtime会继续保持Session为XR_SESSION_STATE_IDLE
的状态,并在适当时候重新切到XR_SESSION_STATE_READY
的状态。 - 如果Runtime认为当前的Session已经是完全结束了,那么就会把它的状态从
XR_SESSION_STATE_IDLE
切换到XR_SESSION_STATE_EXITING
- After xrEndSession, the runtime transitions the session state to
|
|
- 那么,很自然的之后的XR_SESSION_STATE_IDLE,XR_SESSION_STATE_EXITING,就马上在应用端做完了。
- 因为此时整个调用的链路在events loop中,应用poll的事件还没有完全poll完,所以这一连串的session state change就在events loop中直接消化了。
|
|
写到这里,整个HelloXR
的流程就走完了,这个sample code中其实并没有对instance做处理,因此整个流程上还是有缺的。Session End以后也不会有机会重新走到Create的流程,所以,当你退出的那一刻,这个demo其实就走完了。
Flow Chart
文字的代码实现虽然很行云流水,但是没有实际看过工程的人还是会一头雾水,另外有关多个普通Session启动的时候在哪里会skip掉,或者是block在SYNC上的细节,这部分的实现可以参考:ipc_server_process.c::ipc_server_activate_session
,其中也包含了Overlay Extensions的Session VISIBLE/FOCUS状态切换的实现。
正如上述所说的,文字的部分容易让人一头雾水,所以这里补一张HelloXR
的Session时序图来做一个整体描述。
sequenceDiagram autonumber rect rgb(191, 223, 255) HelloXR ->>+ RuntimeClient: xrCreateSession RuntimeClient -->> EventsList: push IDLE note over RuntimeClient: change to IDLE RuntimeClient --> EventsList: push READY note over RuntimeClient: change to READY RuntimeClient --> RuntimeClient: session is running RuntimeClient -->>- HelloXR: xrCreateSession Done HelloXR ->>+ RuntimeClient: pop Event RuntimeClient --> RuntimeServer: RPC RuntimeClient -->>- HelloXR: get IDLE HelloXR ->>+ RuntimeClient: pop Event RuntimeClient --> RuntimeServer: RPC RuntimeClient -->>- HelloXR: get READY end #rect rgb(191, 223, 255) rect rgb(255, 127, 127) HelloXR ->>+ RuntimeClient: xrBeginSession RuntimeClient -->> RuntimeServer: RPC RuntimeClient -->>- HelloXR: session running HelloXR ->>+ RuntimeClient: xrWaitFrame RuntimeClient ->>+ RuntimeServer: RPC:xrt_comp_wait_frame RuntimeServer -->> EventsList: XRT_COMPOSITOR
_EVENT_STATE_CHANGE note over EventsList : visible & focus is true RuntimeServer -->>- RuntimeClient: RPC Done RuntimeClient -->>- HelloXR: xrWaitFrame Done HelloXR ->>+ RuntimeClient: xrBeginFrame RuntimeClient --> RuntimeServer: RPC RuntimeClient -->>- HelloXR: xrBeginFrame Done HelloXR ->>+ RuntimeClient: xrEndFrame RuntimeClient -->> EventsList: push SYNC note over RuntimeClient: change to SYNC RuntimeClient --> RuntimeServer: RPC RuntimeClient -->>- HelloXR: xrEndFrame Done HelloXR ->>+ RuntimeClient: pop Event RuntimeClient ->>+ RuntimeServer: RPC:poll_events RuntimeServer -->>- RuntimeClient: XRT_COMPOSITOR
_EVENT_STATE_CHANGE RuntimeClient -->> EventsList : push visible note over RuntimeClient: change to VISIBLE RuntimeClient -->> EventsList : push focus note over RuntimeClient: change to FOCUS RuntimeClient -->>- HelloXR: get SYNC end #rect rgb(255, 127, 127) rect rgb(191, 223, 0) HelloXR ->>+ RuntimeClient: pop Event RuntimeClient --> RuntimeServer: RPC RuntimeClient -->>- HelloXR: get VISIBLE HelloXR ->>+ RuntimeClient: pop Event RuntimeClient --> RuntimeServer: RPC RuntimeClient -->>- HelloXR: get FOCUS note over HelloXR,RuntimeClient: Frame Loop
......
xrWaitFrame
xrBeginFrame
xrEndFrame end #rect rgb(191, 223, 0) rect rgb(191, 223, 255) HelloXR -->>+ RuntimeClient:xrRequestExitSession RuntimeClient --> EventsList: push VISIBLE note over RuntimeClient: change to VISIBLE RuntimeClient --> EventsList: push SYNC note over RuntimeClient: change to SYNC RuntimeClient --> EventsList: push STOPPING note over RuntimeClient: change to STOPPING RuntimeClient --> RuntimeClient : session is exiting RuntimeClient -->>- HelloXR: xrRequestExitSession Done HelloXR ->>+ RuntimeClient: pop Event RuntimeClient --> RuntimeServer: RPC RuntimeClient -->>- HelloXR: get VISIBLE HelloXR ->>+ RuntimeClient: pop Event RuntimeClient --> RuntimeServer: RPC RuntimeClient -->>- HelloXR: get SYNC HelloXR ->>+ RuntimeClient: pop Event RuntimeClient --> RuntimeServer: RPC RuntimeClient -->>- HelloXR: get STOPPING HelloXR ->>+ RuntimeClient: xrEndSession RuntimeClient --> RuntimeServer: RPC RuntimeClient --> EventsList: push EXITING note over RuntimeClient: change to EXITING RuntimeClient -->>- HelloXR: xrEndSession Done HelloXR ->>+ RuntimeClient: pop Event RuntimeClient --> RuntimeServer: RPC RuntimeClient -->>- HelloXR: get EXITING end #rect rgb(191, 223, 255)
总结
通过整个flow的代码走查,在Monado的实现下,其实状态是存在绑定的,比如:
- 在xrCreateSession的时候会一次性发出IDLE和READY的状态
- 在Runtime进程端发现Session Ready以后,会同时设置visible和focuse为ture,从而导致VISIBLE和FOCUS也是一次性发出
- 从而不会出现spec中step 7的情况,即visible为true,focus为false
- 在收到用户的退出行为后,整个状态机的流转事件是直接:visible,sync,idle/stopping,一下子出来
这个部分在后续实现中需要结合来看看,如果是多普通session并存(非overlay方案)的话,就需要去修改这里面的状态机了。
TODO
[ X ] Compositor Thread中对于Session的绘制管理细节
[ X ] Input / Action事件在Session中的消费/处理