跳转到主要内容

主循环

在本节中,您将学习更多关于TouchGFX中图形引擎的工作方式特别是主循环的内容。 回想一下,图形引擎的主要任务是将应用的图形(ui模型)渲染到帧缓冲。 此过程反复发生,以便在显示屏上产生新的帧。

图形引擎采集部事件,例如显示屏触摸或按钮按下事件。 这些事件经过筛选后被转发到应用。 应用可使用这些事件更新UI模型。 如 在用户触摸屏幕上的按钮时将按钮更改为按下状态,然后在用户不再触摸屏幕时将按钮改回释放状态。

最后,图形引擎将更新后的模型渲染到帧缓冲。 此过程无限循环。

在渲染帧后,帧缓冲被传输到显示屏,用户可从显示屏上看到图形。 为避免显示屏上的抖动毛刺干扰,传输过程必须与显示屏同步。 对于某些显示屏,必须以最小时间间隔定期进行传输。 对于其余显示屏,必须在显示屏发出信号时进行传输。

图形引擎通过等待硬件抽象层发出的“开始”信号实现此同步。 点击此处阅读关于硬件抽象层的更多内容

在伪代码中,TouchGFX图形引擎内部的主循环大致是这样的:

while(true) {
collect(); // Collect events from outside
update(); // Update the application ui model
render(); // Render new updated graphics to the framebuffer
wait(); // Wait for 'go' from display
}

代码更多地涉及实际实现,但上述伪代码有助于理解引擎的主要部分。

下面我们将详细讨论这四个阶段。

采集

在该阶段,图形引擎从外部环境采集事件。 这些事件通常是触摸事件和按钮。

TouchGFX对事件进行采集并传递到应用。 原始触摸事件会被转换为更具体的触摸事件:

  • 点击:用户用手指在屏幕上按下或松开
  • 拖曳:用户在屏幕上移动其手指(在触摸屏幕的同时)。
  • 手势:用户沿某个方向快速移动其手指然后释放。 这被称为滑动,由图形引擎识别。

事件被转发到当前活动的UI元素(如控件)。

引擎还转发tick事件。 此事件表示新的帧(或时间节拍),会一直发送,在没有其他外部输入时也会发送。 应用使用此事件驱动动画或其他基于时间的操作,例如在特定时间结束后切换到暂停界面。

更新

图形引擎与应用一起更新UI,以便反映采集的事件。 图形引擎知晓当前活动的界面,并将事件传递给该对象。

基本原理是引擎将事件通知应用(即UI模型中的Screen和Widget对象)。 作为响应,应用请求重绘界面的特定部分。 应用不以直接绘制的方式响应事件,而是更改控件的属性并请求重绘。

如果发生诸如Click这样的事件,图形引擎将搜索Screen对象的场景模型,以便找到应接收事件的控件(触控点下方的最顶层控件)。 一些Widget(如Image和TextArea)不希望接收Click事件,因此被忽略。 它们有一个空事件处理程序。

其他Widget(如Button)会响应Click事件(按下或释放)。 Button控件在被按下时更改其状态并显示另一幅图像,并在触摸再次释放时变回原始状态。

Image控件在背景层,Button控件在顶层

当Widget(如Button)改变其状态时,也必须在帧缓冲中重绘它。 作为对事件的响应,Widget负责将此信息传回图形引擎。 图形引擎本身不基于采集的事件重绘任何控件。 Widget持续跟踪自身的内部状态(对于Button,为要绘制的图像),并指示图形引擎重绘Widget覆盖的界面部分(矩形)。

应用本身也能对事件做出响应。 通常使用以下两种方式中的一种:

  • 在TouchGFX Designer中为Widget配置交互 例如,我们可以配置交互,让另一个Widget在Button被按下时可见。 此交互在Button更改其状态并从图形引擎请求重绘自身之后执行。 如果使用此交互显示另一个(不可见)Widget,应用还应从图形引擎请求重绘。
  • 在界面上响应事件 也可以直接在界面上响应事件。 事件处理器是Screen类上的虚函数(见下方清单)。 这些函数可在应用的Screen中重复实现。 例如,可用于在用户触摸屏幕时执行操作(无论哪里,只要触摸的是控件)。

Screen类具有下列事件处理器。 在采集到相应外部事件后,图形引擎会调用这些函数:

framework/include/touchgfx/Screen.hpp

virtual void handleClickEvent(const ClickEvent& event);

virtual void handleDragEvent(const DragEvent& event);

virtual void handleGestureEvent(const GestureEvent& event);

virtual void handleTickEvent();

virtual void handleKeyEvent(uint8_t key);

可以在这些事件处理器中插入任何C++代码。 应用通常会更新一些Widget的状态和/或调用一些应用特定的函数(业务逻辑)。

基于时间的更新

handleTickEvent事件处理器在每一帧都会被调用。 这使得应用能够基于时间更新用户界面。 例如,在10秒后Widget渐隐。 假设我们在一秒钟内有60帧(10秒就是600帧),代码可能如下所示:

void handleTickEvent()
{
Screen1ViewBase::handleTickEvent(); // Call superclass eventhandler
tickCounter += 1;
if (tickCounter == 600)
{
myWidget.startFadeAnimation(0, 20); // Fade to 0 = invisible in 20 frames
}
}

图形引擎还将调用Model类上的事件处理器。 此事件处理器通常用于执行重复操作,如检查消息队列或进行GPIO采样:

void Model::tick()
{
bool b = sampleGPIO_Input1(); // Sample polled IO
if (b)
{
...
}
}

只有当特定Screen子类是活动屏时,才会调用该屏幕上的事件处理程序。 始终调用Model类上的事件处理程序(与活动Screen无关)。 从而使得模型事件处理程序适合“应用程序范围”逻辑。

请求重绘

正如上文所述,以Button为例,Widget负责在其状态改变时请求重绘。 这背后的机制称为无效区域

当Button改变状态(如从释放变为按下)并需要重绘时,Button Widget覆盖的区域即为无效区域。 图形引擎保留了为帧请求的这些无效区域的列表。 采集的所有事件(触摸、按钮和tick)可能导致一个或多个无效区域,因此每一帧可能有许多个无效区域。

Screen类上的事件处理器也可以请求区域重绘。 下面我们更改第10帧的Box控件box1的色彩,并通过调用Box上的Invalidate方法请求重绘:

void handleTickEvent()
{
Screen1ViewBase::handleTickEvent(); // Call superclass eventhandler
tickCounter += 1;
if (tickCounter == 10)
{
box1.setColor(Color::getColorFromRGB(0xFF, 0x00, 0x00)); // Set color to red
box1.invalidate(); // Request redraw
}
}

在本例中,图形引擎将在每一帧中调用handleTickEventhandler。 在第10帧,应用代码请求重绘box1覆盖的区域。 作为对该请求的响应,图形引擎将在帧缓冲中用box1控件中保存的色彩重绘该区域。

在下面的用户界面中,背景图像上方有一个Button Widget和一个Box Widget。 如果我们在Button上插入一个交互,以便在Button被点击时更改Box的色彩,那么当用户点击Button时,我们会得到两个无效区域(用红色表示):

两个无效区域

为了获得帧缓冲中绘制的新色彩,需要先将Box的区域无效化。 为了重新绘制释放状态,Button还将自身无效化。

渲染

如前文所述,更新阶段的结果是待重绘区域(无效区域)的列表。 渲染阶段的任务实际上是遍历此列表,并将覆盖这些区域的Widget绘制到帧缓冲。

此阶段由图形引擎自动处理。 应用已经定义了场景模型(ui中的Widget)并使一些区域无效化。 其余的工作由引擎来处理。

图形引擎逐一处理无效区域。 引擎扫描每个区域的场景模型,并采集区域覆盖(部分或全部)的Widget的列表。

根据此Widget列表,图形引擎调用Widget上的绘制方法。 从背景层中的Widget开始,到最前面的Widget结束。

在绘制到帧缓冲时,Widget的绘制方法会用到Widget的状态(如色彩)。 在更新阶段,绘制Widget所需的任何信息都必须保存到Widget。 否则,在渲染阶段将无法获取此信息。

Wait

TouchGFX图形引擎在更新和渲染下一帧之前等待一个信号。 之所以在帧之间等待而不是尽快地继续渲染帧,原因有两个:

  • 渲染与显示屏同步。 如上文所述,一些显示屏需要反复发送帧缓冲。 在发送进行时,随意地将帧渲染到帧缓存是不可取的。 因此,图形引擎会在发送开始后等待一小段时间,然后再开始渲染。 在应发送帧缓冲时,其他显示屏向微控制器发送信号。 图形引擎等待该信号。

  • 按固定速率渲染帧。 对于应用而言,按固定速率渲染帧的好处是更容易创建持续特定时间的动画。 例如,如果显示屏频率为60 Hz,则应将两秒钟的动画设定为在120帧内完成。

图形引擎的等待时间通常被应用中其他优先级较低的进程利用。 在这种情况下,时间不会被浪费,优先级较低的进程反正都应在某些时间点运行。

处理帧缓冲

如前文所述,图形引擎会在更新帧缓冲之前与显示屏同步。 在渲染到帧缓冲后,引擎还需确保显示屏显示更新后的帧缓冲。

两个帧缓冲

在最简单的设置中,有两个帧缓冲可供使用。 图形引擎在两个帧缓冲之间切换。 在将帧绘制到一个帧缓冲的同时,将另一个帧缓冲传输到(并显示在)显示屏上。

双帧缓冲

在此次绘制中,假设并行RGB显示屏连接了LTDC控制器。 这意味着在每一帧中都必须将帧缓冲发送到显示屏。 由于有两个帧缓冲,图形引擎可以在发送一个帧缓冲的同时将帧绘制到另一个帧缓冲。 此方案效果很好,如可能,应作为首选方案。

由于图形引擎在每一帧都进行绘制,在上面的绘制中,我们也在所有帧发送新的帧缓冲。

常常会有应用不更新帧任何内容的情况。 这表示不进行任何渲染。 因此,在下一帧会再次发送相同的帧缓冲。

2号帧中无更新

应用在2号帧未绘制任何内容,因此图形引擎在3号帧再次重发2号帧缓冲。

典型的并行RGB显示屏的刷新率约为60 Hz。 此更新频率必须由微控制器来维护。 此更新频率意味着在再次开始发送前,有16 ms的时间可用来渲染新帧。 在某些情况下,渲染新帧的时间超过16 ms。 此时,图形引擎只再次重发相同的帧(同之前一样):

渲染时间长

1号帧的渲染时间超过16 ms,因此重发之前渲染到1号帧缓冲的0号帧。 在3号帧发送2号帧缓冲中的新帧。 当有两个帧缓冲可供使用时,渲染时间可能会非常长。 在有新帧可用之前,会一直重发上一帧。

一个帧缓冲

在某些系统中,由于存储空间的限制,只能使用1个帧缓冲。 如果使用并行RGB显示屏,则必须在每一帧发送1号帧缓冲。

由于图形引擎不得不在向显示屏发送帧缓冲的同时将帧绘制到同一个帧缓冲,因此会产生问题。 如果不加注意就这样做,会有一个极大的风险,即显示屏会显示上一帧与新帧的混合帧。

一种解决方案是在传输完成前不进行绘制,只在传输再次开始前的时隙内绘制。 由于传输占用了整个帧时间的很大一部分,因此可用于绘制帧的时间极少。 另一个缺点是如果在下一次传输开始时绘制未完成,则仍可能出现不完整的帧(撕裂)。

一种更有潜力的解决方案是监测帧缓冲已发送的量,然后将渲染限制在帧缓冲的合适部分。 虽然传输的进行,帧缓冲有越来越多的部分可供渲染算法使用。

图形引擎包含帮助程序员确保绘制正确执行的算法。

应用在每一帧更新并渲染帧缓冲:

在每一帧重发一个帧缓冲

如果帧没有更新任何内容,则重发帧缓冲,不做任何更改。

如果渲染时间超过16 ms,当再次开始重发时,渲染尚未结束:

渲染时间长

在这种情况下,图形引擎必须确保正在发送的部分已完成渲染。 否则,显示屏将显示未完成的帧缓冲。

在下一节中,我们将讨论各个Widget的渲染时间。 这将有助于程序员编写出高性能的应用。