gEvas:GTK+2 到 Evas 的桥梁

作者:Ben Martin

Evas 库提供了一个画布,用于快速渲染具有 alpha 混合支持的栅格图形。Evas 是 Enlightenment Foundation Libraries (EFL) 的一部分,EFL 最初是为了支持 Enlightenment DR 17 而构建的库套件。EFL 中与 Evas 互补的其他库包括 Edje 和 Embryo 的组合以及 Emotion 库。Edje 允许您将字体、图形和功能封装到可移植的主题类文件中。Embryo 是一种简单但图灵完备的脚本语言,它允许将简单的脚本嵌入到 Edje 文件中。Emotion 允许您将多个视频流作为一流的画布对象。这意味着您可以对视频进行 alpha 混合、移动视频对象、调整它们的大小并在播放时在画布中分层。

gEvas 是一个包装器和胶合库,旨在使 Evas 能够轻松地从 GTK+2.x 应用程序中使用。为了激励您尝试 gEvas,我在此介绍最初促使我使用 Evas 并随后创建 gEvas 的要点。我使用 Evas 的主要动机是其简单的 API 和 alpha 混合画布的快速渲染速度。

不幸的是,声称高渲染速度需要简要地进入基准测试的世界。来自 Evas 发行版的 evas_bench 应用程序涉及许多像素图画布项、像素图项的缩放和混合以及文本元素。图像缩放远不及上述调整大小基准测试那么极端。我已经将 evas_bench 移植到使用 GNOME Canvas。

图 1 显示了 evas_bench 的 GNOME Canvas 移植版本的屏幕截图。我还创建了一些更简单的测试,用于画布缩放算法,包括强制和不强制在每一帧上进行 alpha 混合的情况。对于非 alpha 混合版本,将叶子图像从大于全画布宽度调整为 0x0,然后再循环返回;参见图 2。对于 alpha 混合版本,使用与叶子图像相同大小的红色矩形图像,alpha 值从左上角的 0 到右下角的完全 alpha。

gEvas: the GTK+2 to Evas Bridge

图 1. evas_bench 工具的 GNOME Canvas 移植版本。许多图像和文本在画布上移动和调整大小。

gEvas: the GTK+2 to Evas Bridge

图 2. gnome_canvas 缩放和合成基准测试的 gEvas 版本。叶子逐渐缩小到画布的左上角,然后又恢复到原始大小。

如上所述,我创建了 gEvas,一些读者可能会注意到我是 Enlightenment 开发者团队的一员。虽然这是事实,但我已尽力确保基准测试没有偏差。基准测试源代码是可用的(参见在线资源)。阅读源代码的读者,请原谅在快速代码hack中过度使用不太优化的编码约定。

对于那些不熟悉 GNOME Canvas 的人来说,它有两个渲染后端。来自 GNOME Canvas 开发者文档(参见资源):“...它[GNOME Canvas]提供了两种渲染后端选择,一种基于 Xlib 以实现极快的显示,另一种基于 Libart,一种复杂的、抗锯齿的、alpha 合成引擎。” Evas 试图提供两全其美的方案。我针对 GNOME Canvas 渲染引擎对 Evas 进行了基准测试。

也有一些原因说明 Evas 可能不是合适的选择。GNOME Canvas 支持 Bezier 路径画布项,而目前 Evas 不支持。此外,Evas 和 gEvas 比 GNOME Canvas 更不可能预装。

Evas 未来可能会支持贝塞尔曲线。Raster(又名 Carsten Haitzler)对此点的总结是:“如果您追求矢量编辑器套件或其他类似的东西,GNOME Canvas 会更好。如果您想要在所有目标上实时快速显示 alpha 混合对象,Evas 是一个不错的选择。”

Evas 本身支持多个后端渲染目标,包括 framebuffer、XLib 和 OpenGL。目前,gEvas 仅使用 XLib Evas 后端。但是,由于 GTK+2 可以在 framebuffer 上运行,您应该也可以在 framebuffer 上使用 gEvas,但这尚未经过测试。

GNOME Canvas 和 Evas 共享类似的数据模型。Qt QCanvas 数据模型差异很大,使得进行干净的基准测试比较变得困难。使比较困难的第一个主要区别是 QCanvas 如何处理图像。要将图像放在画布上,您需要创建一个带有单帧的 QCanvasSprite。要缩放该图像,您需要使用 QImage::scale() 或 QImage::smoothScale(),这将返回一个图像,您可以使用该图像来更新精灵。这会将图像缩放和缩放图像的缓存处理放入客户端应用程序中。Evas 和 GNOME Canvas 都允许直接调整画布对象的大小,从而负责为您处理缩放图像的缓存。

第二个区别是 Qt 允许您控制更新平铺大小。Qt 文档建议:“一个好的经验法则是,大小应该比平均画布项目大小略小。如果您有移动对象,则块大小应比移动项目的平均大小略小。”

由于数据模型上的差异,我尚未创建 evas_bench 的 Qt 移植版本。我确实创建了一个画布缩放和混合客户端,尽管存在一些影响干净比较的画布设计问题。

随着 Qt 将缓存策略完全移至客户端,我选择在缩放的第一次迭代中缓存所有缩放图像,并为将仅使用缓存的进一步迭代重置基准测试开始计时器。

因此,请记住,Qt 调整大小基准测试是在所有调整大小的图像都预缓存的情况下执行的,并且许多用户指定的平铺大小用于基准测试。这实际上应该给 QCanvas 带来相对于 Evas 和 GNOME Canvas 的巨大速度优势。

让我们开始竞赛

无论使用什么硬件,结果在相对基础上都应该是相似的。为了完整性,我的测试 CPU 是 AMD XP-Mobile,运行频率为 2.4GHz,FSB 为 200MHz,配备 1GB RAM,运行频率为 400MHz 双通道 cas222 和 NVIDIA 5900 显卡。可能影响性能的软件包括 xorg-x11-6.8.2-1.FC3.13、GCC 4.0.0 20050308 (Red Hat 4.0.0-0.32) 或 GCC 3.4.3。X11 配置了 TwinView,一个 1024x768 屏幕和一个 1600x1200 屏幕,均以 85Hz 运行在 32 位颜色下。TwinView 不应影响运行时,因为所有画布都使用软件渲染路径,这些路径应该对 CPU/RAM 速度更敏感。

使用客户端库 qt-3.3.4、libgnomecanvas-2.10.0 使用以下 CFLAGS 重新编译,Evas CVS 于 2005 年 5 月 28 日检出。Evas 使用 GCC 3.4.3 和以下 CFLAGS 编译。基准测试编译代码 CFLAGS 和 CXXFLAGS 通常是

-O3 -march=athlon-xp -fomit-frame-pointer

由于上面提到的图像缓存区别和块大小优化,我单独对 qt-canvas-resize 客户端进行了基准测试。表 1 显示了 qt-canvas-resize 的基准测试,其中主循环的 Qt 部分包括

QCanvasSprite* leaf_sprite = ...;
QCanvasPixmapArray* leaf_tiles = ...;
while( running )
{
  while( app->hasPendingEvents() )
    app->processEvents();
  QImage im = ... from cache ...;
  QCanvasPixmap* qpix = new QCanvasPixmap( im );
  leaf_tiles->setImage( 0, qpix );
  leaf_sprite->setFrame(0);
  canvas->update();
}

客户端有几个命令行选项:--alpha-blend-image 用于 alpha 混合红色矩形而不是叶子,--chunk-size 用于指定非默认块大小。--alpha-blend-image 选项是 qt-canvas-resize、gnome-canvas-resize 和 (g)evas-resize 共有的。

表 1. 具有不同平铺大小的 qt-canvas-resize 基准测试

应用程序块大小叶子图像 FPSAlpha 矩形 FPS
qt-canvas-resize默认11472
qt-canvas-resize3212880
qt-canvas-resize6413682
qt-canvas-resize12814281

通过 valgrind 的 callgrind 运行默认块大小叶子图像 qt-canvas-resize 几分钟后,发现 QCanvas::update() 占总运行时的 30%,而 QCanvasPixmap::init() 使用了 59% 的运行时。因此,如果预缓存图像存储在精灵的 QCanvasPixmapArray 中,则基准测试可能会显着提高。

为了测试这种预缓存级别,我添加了 -Z 选项,将所有缓存图像放入单个 QCanvasPixmapArray 中,这是 QCanvasSprite 的后备。通过这种优化,可以实现 559 FPS,其中 78% 的运行时在 QCanvas::update() 中,7% 在 QCanvasSprite::setFrame() 中。必须指出的是,这种级别的预缓存为 QCanvas 的渲染速度带来了不公平的优势。

用于图像缩放和混合的 GNOME Canvas 客户端是 gnome-canvas-resize,它具有 --aa 选项来选择 GNOME Canvas alpha 混合后端。evas-resize 没有自定义选项。

表 2. Resize 基准测试中 GNOME Canvas 与 (g)Evas 的比较

应用程序叶子图像 FPSAlpha 矩形 FPS
gnome-canvas-resize2121
gnome-canvas-resize --aa149127
evas-resize190184
gevas-resize185177

在没有 --aa 选项的情况下,gnome-canvas-resize 将 99% 的时间花费在 gtk_widget_send_expose() 中,它是从 g_main_context_iteration() 以某种方式调用的。我认为非 aa GTK+2 引擎不喜欢以完全基准测试的方式使用。在 --aa GNOME Canvas 后端上使用 callgrind 发现 96% 的时间花费在 gtk_widget_send_expose() 中,尽管现在我们可以看到 66% 的时间在 gdk_pixbuf_composite() 中,它是从 gtk_widget_send_expose() 间接调用的。

evas-resize 将 99% 的时间花费在 evas_render_updates() 中。在从 evas_render_updates() 调用的函数中,91% 的时间花费在缩放函数中。

我还将 evas-resize 移植到使用 gEvas 及其 API 调用。尽管由于 GTK+ 信号胶水和其他 gEvas 修剪而导致一些速度损失,但损失并不太显着。

此比较表明,对于直接图像缩放,GNOME Canvas 和 QCanvas 相似,并且都比 Evas 慢。当缩放图像还需要 alpha 混合到背景时,Evas 获得了更大的优势。

我修改了原始的 evas_bench 应用程序,以删除不易在 GNOME Canvas 中复制的功能。其他功能变为可选,以衡量其对整体性能的影响。在 Evas 中设置剪切区域不是一件容易移植到 GNOME Canvas 的事情,因此在 evas_bench 中禁用了这些区域。平滑缩放也可能对性能产生很大影响,因此添加了一个选项来关闭 Evas 版本的平滑缩放。

表 3. evas_bench 及其 GNOME Canvas 移植版本正面交锋

应用程序FPSEVAS_BENCH
gnome-canvas-port-evas-bench --aa901.49
evas_software_x11_main1642.75
evas_software_x11_main --smooth-off-for-some2003.32
evas_buffer_test2904.83

应该注意的是,Evas 目前没有实现缩放图像的缓存。因此,Evas 基准测试中的每一帧都在执行图像缩放和混合。

evas_buffer_test 客户端执行相同的工作,但仅将输出渲染到内存中的 32 位 RGBA 图像缓冲区。

gEvas

gEvas 的核心提供了五个方面:它告诉 Evas 何时重绘自身,协助将 Evas 事件和 glib2 信号粘合在一起,处理 Edje 定时器调用以支持动画,帮助 Evas 与 GTK+2 小部件友好相处,以及协助 Evas 使用的代码。由于 Evas 也以嵌入式系统为目标,因此核心 Evas 中遗漏了一些方便的代码以使其精简。由于 gEvas 以桌面为目标,因此它为桌面应用程序添加了一些方便的功能。

以下代码在可滚动区域内创建一个 gEvas 画布,并将其附加到 GTK+2 窗口。由于并非每个可滚动的 gEvas 都希望允许中间按钮拖动画布位置(如在 GIMP 中),因此您必须在 gevas_new_gtkscrolledwindow() 之外设置它

GtkWidget* window = 0;
GtkWidget* scw    = 0;
GtkWidget* gevas  = 0;

window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gevas_new_gtkscrolledwindow(
   (GtkgEvas**)(&gevas), &scw );
gtk_container_add(GTK_CONTAINER(window), scw);

gtk_scrolled_window_set_policy(
   GTK_SCROLLED_WINDOW(scw),
   GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

gevas_set_middleb_scrolls(GTK_GEVAS(gevas), 1,
  gtk_scrolled_window_get_hadjustment(
     GTK_SCROLLED_WINDOW(scw)),
  gtk_scrolled_window_get_vadjustment(
     GTK_SCROLLED_WINDOW(scw)));

用于创建对象的 gEvas API 从标准 GTK+ 编码中借鉴了一个方法,即将一些方法附加到通用的 GtkgEvasObj 类。其他特殊类(例如 GtkgEvasImage)派生自 GtkgEvasObj。不幸的是,这也带来了 ANSI C GTK+ 编程通常的强制类型转换风格。

以下代码创建一个图像,显示您的 PNG 文件,其原始宽度和高度。然后,我们移动图像并提高其在画布中的图层

GtkgEvasImage* gi;
GtkgEvasObj* go;

gi = gevasimage_new_from_metadata(
        GTK_GEVAS(gevas), "/my/path/foo.png" );
go = GTK_GEVASOBJ( gi );

int x = 100, y = 50;
gevasobj_move(      go, x, y );
gevasobj_set_layer( go, 1 );

我创建了一个简单的客户端,展示了如何将来自画布的 Evas 事件连接到 gEvas 小部件外部的一些 GTK+2 小部件。在 gEvas 包的 demo 目录中查找 signalconnect.c。有关更高级的示例,请查看 testgevas 客户端,了解原始 Evas 回调和被编组到 glib 信号的 Evas 触发回调的用法。Signalconnect 如图 3 所示。

gEvas: the GTK+2 to Evas Bridge

图 3. 连接 Evas 和 GTK+ 信号。恐龙图像可以直接拖动,也可以通过移动滑块栏来拖动。

连接到 Evas 事件是通过 GtkgEvasEvHClass 子类处理的。以下代码片段导致 evh 将 Evas 的鼠标按下/释放事件编组到 glib 信号中,然后将其连接到报告函数。此外,当用户移动 raptor 图像时,会通过 glib2 信号调用 raptor_moved(),以使用图像的当前坐标更新各种 GTK+2 小部件

static gint raptor_moved(
    GtkgEvasObj* o,
    Evas_Coord* x, Evas_Coord* y,
	gpointer user_data )
{
 gtk_progress_bar_set_fraction( x_coord_tracker,
      (1.0 * (*x)) / CANVAS_WIDTH );
 gtk_range_set_value(
      GTK_RANGE(y_coord_tracker), *y );

 return GEVASOBJ_SIG_OK;
}

static gboolean
gtk_mouse_down_cb(GtkObject * object,
 GtkObject * gevasobj, gint _b, gint _x, gint _y,
 gpointer data)
{
 char buffer[1024];
 snprintf(buffer,1000,"mouse_down b:%d x:%d y:%d",
          _b, _x, _y);
 gtk_label_set_text( e_logo_label, buffer );
 return FALSE;
}

...
gi = gevasimage_new();
go = GTK_GEVASOBJ( gi );
gevasimage_set_image_name( gi, "raptor.png" );
...

/** Let the user drag the raptor around **/
GtkObject *evh = gevasevh_drag_new();
gevasobj_add_evhandler( GTK_GEVASOBJ( gi ), evh );

gtk_signal_connect( go, "move_absolute",
   GTK_SIGNAL_FUNC( raptor_moved ), go );

gi = gevasimage_new();
go = GTK_GEVASOBJ( gi );
gevasimage_set_image_name( gi, "e_logo.png" );
...

evh = gevasevh_to_gtk_signal_new();
gevasobj_add_evhandler( GTK_GEVASOBJ( gi ), evh );

gtk_signal_connect(GTK_OBJECT(evh), "mouse_down",
  GTK_SIGNAL_FUNC(gtk_mouse_down_cb), NULL);
gtk_signal_connect(GTK_OBJECT(evh), "mouse_up",
  GTK_SIGNAL_FUNC(gtk_mouse_up_cb), NULL);

以下是一些可以附加的更实用的事件处理程序

/* Standard GTK+ popup menu creation + handling */
static gboolean
gtk_popup_activate_cb(GtkObject * object,
   GtkObject * gevasobj, gint _b, gint _x, gint _y,
   gpointer data)
{
 static GtkMenu *menu = 0;
 ...
}

GtkgEvasObj* go  = ...;
GtkObject*   evh = 0;

/* Make the object throb when mouse is over it */
GtkgEvasEvHThrob* evht = gevasevh_throb_new( go );

/* Allow the user to drag the object around  */
evh = gevasevh_drag_new();
gevasobj_add_evhandler( go, evh );

/* Make a popup menu appear on right mouse click */
evh = gevasevh_popup_new();
gevasobj_add_evhandler( go, evh );

gtk_signal_connect(GTK_OBJECT(evh),"popup_activate",
   GTK_SIGNAL_FUNC(gtk_popup_activate_cb), NULL);

在画布中处理选择比上述事件处理程序稍微复杂一些。这是因为选择过程涉及多个对象。您创建一个 GtkgEvasEvHGroupSelector 类的选择器事件处理程序对象,该对象附加到您想要作为背景不可选对象的对象。您可以将此对象视为绘制橡皮筋矩形以指示应选择哪些对象的位置。橡皮筋始终绘制在比可选对象更高的图层上。然后,画布上的每个可选对象都附加了一个 GtkgEvasEvHSelectable 对象,该对象与 GtkgEvasEvHGroupSelector 对象通信

GtkWidget*     gevas = ...;
GtkObject*     evh_selector = 0;
GtkgEvasImage* gevas_image;

gevas_image = gevasimage_new();
gevasobj_set_gevas(gevas_image, gevas);
gevasimage_set_image_name(gevas_image,".../bg.png");

/* Make this a group_selector */
evh_selector = gevasevh_group_selector_new();
gevasevh_group_selector_set_object(
  (GtkgEvasEvHGroupSelector*)evh_selector,
  GTK_GEVASOBJ(gevas_image));

GtkgEvasObj* go  = ...;
make_selectable( gevas, go, evh_selector );

...

/* lets make this object also selectable */
void make_selectable( GtkgEvasObj* object,
                      GtkObject* evhsel )
{
 GtkgEvasObj* ct  = 0;
 GtkObject* evh = gevasevh_selectable_new( evhsel );
 gevasevh_selectable_set_confine(
    GTK_GEVASEVH_SELECTABLE(evh), 1 );

 gevasobj_add_evhandler(object, evh);
 gevasevh_selectable_set_normal_gevasobj(
    GTK_GEVASEVH_SELECTABLE(evh),  object);

 ct = (GtkgEvasObj*)gevasgrad_new(
     gevasobj_get_gevas( GTK_OBJECT(object)));

 gevasobj_set_color( ct, 255, 200, 255, 200);

 gevasgrad_add_color(ct, 120, 150, 170, 45, 8);
 gevasgrad_add_color(ct, 200, 170, 90, 150, 16);

 gevasgrad_set_angle(ct, 150);
 gevasobj_resize( ct, 200,100);
 gevasobj_set_layer(ct, 9999);

 gevasevh_selectable_set_selected_gevasobj(evh,ct);
}

然后,您可以轻松测试对象是否被选中,或获取集合对象来对所有选定对象执行操作

GtkgEvasEvHGroupSelector* ev = ...;
GtkgEvasEvHSelectable*    o  = ...;
GtkgEvasObjCollection*    col = 0;

gboolean yn = gevasevh_group_selector_isinsel(ev,o);
col = gevasevh_group_selector_get_collection( ev );
gevas_obj_collection_move_relative( col, 100, 200 );

此外,还创建了一些对象(例如 geTransAlphaWipe)以在 Edje 存在之前执行图像过渡。虽然 Edje 是未来的发展方向,但 alphawipe 代码允许您在不涉及 Edje 的情况下执行常见的简单过渡。这在 gevasanim 中用于创建精灵类对象,该对象使用 alpha 混合在其帧之间过渡。

gEvas 中的 xxx_from_metadata() 函数允许您使用单个字符串为新对象设置位置、图像文件名、可见性和其他属性。from_metadata() 和过渡代码都复制了现在 Edje 中也提供的功能

sprite = gevas_sprite_new( GTK_GEVAS(gevas) );

for( i=1; i<frame_count; ++i )
{
 gchar* md = g_strdup_printf(
   "cell%ld.png?x=120&y=120&visible=0&fill_size=1"
   ,i);
 gi = gevasimage_new_from_metadata( GTK_GEVAS(gevas), md );
 g_free( md );
 gevas_sprite_add( sprite, GTK_GEVASOBJ( gi ) );
}

gevas_sprite_set_default_frame_delay( sprite,2000 );
gevas_sprite_play_forever( sprite );

/* frame transitions */
geTransAlphaWipe* trans = 0;
trans = gevastrans_alphawipe_new();
for( i=0; i<frame_count; ++i )
 gevas_sprite_set_transition_function(
     sprite, i, trans );

为了盛大的结局,我将一个 Edje 对象放在 gEvas 画布上。我为本文创建的演示客户端是 gEvas 发行版中的 demo/gevasedje。有趣的部分如下所示。Edje 本身有一个 Enlightenment 徽标,它旋转,还有一个在鼠标点击下跳动

/* init engines */
ecore_init();
edje_init();
gtk_init(&argc, &argv);

...
gevas = ...;
/* allow edje to update the canvas as well */
gevas_setup_ecore( (GtkgEvas*)gevas );

/* place an edje object */
GtkgEvasEdje* gedje
 = gevasedje_new_with_canvas( gevas );

/* eet files can contain many edje objects */
gevasedje_set_file( gedje, "e_logo.eet", "test" );
go = GTK_GEVASOBJ(gedje);
gevasobj_move(      go, 300, 300 );
gevasobj_resize(    go, 370, 350 );
gevasobj_set_layer( go, 10 );
gevasobj_show(      go );

总结

为了节省空间,我从文章中省略了内存消耗数据,因为这些数据对于现代桌面机器来说不太重要。未来的基准测试将比较 QTCanvas 调整大小与 Edje 调整大小程序,以便在这两者之间提供更公平的比较。

如果您正在为新的 GTK+2 应用程序寻找画布,请在进行黑客攻击之前考虑 GNOME Canvas 和 gEvas 以及 Edje。

本文的资源: /article/8647

Ben Martin 大部分时间都在研究虚拟文件系统和数据挖掘,尽管他也以玩转他人图像艺术的二维和三维动画而闻名。

加载 Disqus 评论