图形系统设计心得

最近做的两个项目都跟图形系统设计有关系。慢慢的总结出来一些体会,记录下来以后方便自我批评。。

假如让我写一个操作系统的话,我绝不会把图形界面放到内核里面。否则图形界面挂起居然会导致系统死机,这太恐怖了。

然而,提供一套统一的方式让应用程序管理自己的图形接口是绝对正确的。提供API给应用程序的方法很纯粹,但是也很危险。经常遇到的各种程序挂起,不能更新界面,不相应鼠标等等问题,其实都是事务处理和图形处理过度耦合造成的。他需要事务处理和图形处理以协作的方式由应用程序自行实现。假如应用程序的winproc里面写一个死循环,那么界面更新是无论如何也做不了的了。这样的一个操作系统,自然而然会得到“不稳定”的美名。就算仅仅是他的应用程序不稳定,也是你把“不稳定”的权限交给他的。

最近做的项目是widget嘛,其实这个东西完全可以做整个操作系统的图形界面。让图形界面和事务处理在两个分离的线程里面完成,这是非常必要的。当然你必须提供一套合适的信号系统,让两个线程得以合理的通讯。这会遇到很多复杂或者可能会有很多bug的问题,不过这些问题可以慢慢研究嘛。我们先看好的方面。

首先就是可以用类html的东西来定义显示形式,首先html已经非常成熟,有大批熟练程序员,所以用这个东西的成本最低廉,另外这种东西也方便修改,同时修改又对已经编译好的程序没有影响,也就是说界面换肤会成为一种非常低成本的事情。应用程序内部逻辑其实是完全的事务处理,做一些重要的事情。然后界面和这些事务处理采用js之类的脚本语言控制,方便开发维护。

是否需要一个脚本-二进制粘合层是一个可以商量的问题。其实最基本的就是让应用程序定义一套界面接口的名称,并在内操纵这些接口。这些接口可不是原始的图形渲染逻辑,决不能让应用程序去做渲染的实质工作,这套接口应该是充分抽象的。比如应该定义出listbox, textbox, checkbox等类型,要避免应用程序自己去考虑绘制的实际问题。应用程序只需要说明逻辑。向某个列表里面插入数据,或者显示某幅图片。具体的渲染逻辑要放到图形引擎里面去做。

为什么要充分抽象。因为渲染是一个全局的事情。尤其是加上半透明处理之后。渲染不能光顾着自己的程序,还要和其他的窗体融合。单独的应用程序是不应该知道,除了自己之外操作系统还有那些其他窗口的。他应该只顾着自己。但是图形绘制又需要跟其他窗口耦合,因此这两部分事情绝不能在一个单一的模块里面完成,否则一定会出问题。哪怕应用程序已经死循环了,当鼠标移动到他的界面上的各个按钮的时候,按钮仍然应该拥有原有的视觉效果,mouseover, mousedown, 这些跟应用程序无关。窗口应该还是可以拖动,可以随时关掉。不能仅仅因为应用程序在buzy,就连最基本的图形显示都做不到了。只有把图形渲染的任务交给另外一个线程来做,这才是有可能的。

并且将listbox之类的图形接口由统一的引擎来完成,还会有很多额外的好处。就让我们关注listbox吧,因为要包含大量数据的东西,或者说“容器”,需要考虑的东西往往比第一反应能想到的东西要多。比如你的listbox还有10K数据,但一次只能显示100条,你会一次就把这些数据都读取到内存吗?显然不能。我们会采用cache。然后所有cache相关的东西都要往里面放,什么时候读,一次读多少条,什么时候写回,脏比特位,等等。然后listbox还有滚动条。滚动的时候应该怎么做。动画怎么显示。等等等等。每个含有这类逻辑的应用程序都去重新实现一遍这套逻辑是不可思议的。你会说用库嘛,用MFC, 用ATL,问题没那么简单。因为这些东西的实现是复杂的,需要考虑的问题很多。往往是使用不当的时候你又不知道究竟哪里用得不当。文档永远是不够详细的,同时,用户是永远都不会看文档的。并且文档越详细,用户越不会看(太长了)。因此文档是没有意义的。需要提供服务,而非类库。让这些复杂难解的事情由别人直接解决掉,从一开始就不要把跟渲染相关的东西或编译或link到应用程序里面来,从物理上就要做decoup,应用程序只需提供数据和逻辑就好了。

当然这可能又会产生新的问题。比如数据提供的接口怎么定义。同是listbox,数据可能来自内存,可能来自文件,可能来自数据库,可能来自网络,等等。 需要提供一个通用接口绑定界面和数据,要保证使用字符串就能定位到相应的属性名(比如可以采取类似IDispatch的模式)。之后又应用程序自己实现这个接口,实现内部就可以随心所欲了,网络怎么取,数据库怎么访问,等等。但是从性能考虑,你还不能仅仅定义一个单独item数据访问的接口。你需要让应用程序一次就返回给你批量的数据。比如你有10K数据,但你要应用程序只给你从第200条到第300条。需要有这类区间型的接口。另外还需要有数据写回的接口,比如某个属性可以设置成可写的。然后数据就变成一段cache。当析构函数被调用的时候,先写回数据,然后析构。

一旦这些接口被定义好,让脚本来控制接口就变得简单。无非是在不同接口之间不断回调嘛。并且这样的脚本还可以编译成二进制代码段。其实也就是把一个一个的func call组合起来而已了。

同样的,类html的解析其实也是可以编译出来的。你解析过一次html之后,生成了一段图形类的thunk,你只需把这段thunk二进制写进某个dll,要用的时候直接读进来就可以显示了,无需反复parse html,性能其实跟纯用系统调用写出来的程序性能不会差很多。

当然这样的应用程序就不再是一个exe文件。他其实是一个dll。或者通用的说,它是一个服务。整个操作系统都是基于服务的。系统底层提供访问硬件的各种服务,图形引擎提供图形处理的服务,应用程序提供事务处理和交互逻辑的服务。而exe其实是一个简单的binder。他把不同的服务启用起来,在之间传递各种消息(因为不同的服务是跑在不同的线程里面的,所以需要消息)。前面说的那么多图形处理和数据访问的接口,都应该定义在这个exe里面,实质上就是向不同的服务发送不同的请求,并且返回应答而已。这样设计的结果不仅仅减少了耦合,增加了系统鲁棒性,减少了程序员的压力和重复代码,并且也大大增加了程序的灵活性。比如一个阅读器,本来是读取硬盘上的文件的。界面无需变化,只需要实现一个新的获取网络数据的服务,就可以变成一个读取网络图书的在线阅读器。如此之类的可能性还有很多。并且应用程序因为都采取统一的接口,因此进程间通讯也会变得更方便,增加新的UI也会更简便(不是说GUI),比如原来是鼠标点击的程序,要想增加一套用键盘或者用啥啥新式设备的交互逻辑,也会很简单。

好吧,其实以上说的这么多东西也没有什么创建性,所谓MVC嘛,几十年前就有了。只不过从来没有仔细想过它究竟意味着什么。这次实习还有写毕设,慢慢让我对这套东西理解深刻了。只是自我记录记录。以后发现有错了回过头来批评批评,之类的。

以上

过年回来没干劲了

博客也荒废了

不想干事情的状态再持续下去的话,整个人就会完全废掉了。

其实需要干的事情很多很多啊。结果都扔在那里。然后自己一个人呆在一边发呆。

需要动力啊。

最近在做图形库的接口抽象。之前做的widget引擎和图形库的耦合过于紧密了,现在想要加上DX的支持的话就相当于整个项目重写一遍。没有办法,只好想办法把图形库的通用接口抽离出来。之后用DX实现一下那套接口。说起来貌似挺容易。但是从哪个层次抽离接口呢。由于我们原来使用的是Cairo图形库,最初的想法是把Cairo的每一个API都当作是接口的一个函数。这样做改动不大,原来的代码可能几乎不怎么动就能把接口抽离。问题在于抽离了接口之后,怎么去实现…Cairo是一套平面矢量库,DX的API跟它差的十万八千里。要用非常别扭的方法才能实现那些接口。那样做的结果只有性能下降。本来用DX是要提高性能的,这样做实在得不偿失。

于是只好在较高的层面来做抽象。分析widget里面每个DOMObject衍生类使用到Cairo的函数,发现主要使用到的其实只有render和translate两个函数(其实translate本来也是属于render的一部分,后来因为通用性强才抽出来做独立的函数的),除此之外还有一些使用到的,比如getCurentRect,或者img::setSRC,这些函数其实本可以不使用Cairo,比如setSRC完全可以只是设置一个字符串路径。 到render的时候才去读取文件。这样一方面能提高些许效率(假如有人反复setSRC却不显示那个图片,其实本不需要读取那个文件),另一方面也能减少DOMObject和图形库的耦合。至于getCurrentRect之类,应该在每次render的时候将位置,大小还有mask(用于hittest)的信息存储在某处,想要获取的时候直接去用就好了。这样做就几乎把所有跟图形库的耦合全都缩小到render函数里面了。之后只需写一个公共接口,然后用工厂方法对不同的DOMObject子类生成不同的render实现传指针进去就好了。这样在有DX支持的系统上,可以使用DX,而在没有DX支持的板子上,程序可以动态适应,换用Cairo版的render实现。比较灵活。

这两天总算拖着拖着做了这么一点,接口基本上抽离出来了,但是抽出来之后发生了一些bug, 主要是TextArea和Colorize部分出了问题。争取尽快把bug解决掉好提交代码吧。。还有太多太多事情等着我做呢!!