本来也不是专门为了写这篇文章,只不过觉得已这样的形式发表比较合适。同时好久都没有写过教程了,以往都以简单的发表作品或者通报一些事情为主。总的来说这篇文章还是有点参考价值的,希望能有所帮助。
转载注意:
请注明原文出处: http://www.csksoft.net/blog/post/demo_scene_tip1.html
Demo Scene我就不想再介绍了,对他还不了解或者听说过但不明白原理和背景的可以参考我以前写的一些文章:
Demo Scene:Principles,Techniques,and Tools (http://www.csksoft.net/blog/post/154.html)
模块音乐(Mod)的制作和使用,Demo程序的主体之一 (http://www.csksoft.net/blog/post/intro2track_music.html)
目前国内Demo Scene基本处在0起步的阶段,已经有了一些小团体打算去参加欧洲的比赛,但是还没有一定的规模。同时,对于制作这类程序网上也没有系统的资料。使得制作Demo Scene被看成一种高深的事情。
下面我就说说目前在Windows平台下,使用最常用的开发工具(Visual C++)如何来制作一个符合64kb demo的程序框架和常用技巧。当然这只是一些次要手法,最核心的还是3d引擎、mod音乐的设计。因为那些资料很好找。所以就不再涉及。
我将介绍下面几个方面的技术:
1.如何产生体积最小的程序
2.如何不使用C运行库开发程序
3.如何实现高速GDI绘图
4.对于NT5.0提供的LayeredWindow的使用--不规则窗体、窗口的AlphaBlend渲染、鼠标事件穿透
5.如何将所有数据(代码、图片等)整合在一个C文件中
6.其他的一些编译技巧
7.一个完整的示例程序代码
问题和需求
Scene Demo中有一个项目为4kb-intro 或者 64kb-intro。 他要求Demo的程序体积必须小于或者正好等于4/64kb。而往往正是这类Demo程序在国内流传最广。因为大家都认为那么小的体积能播放长时间的高品质3d动画和音乐是不可思议得。甚至有人将45分钟的demo动画看成是avi视频,45分钟的音乐算作44KHz采样的wav。计算出将他们压缩到64kb完全是不可能的(见farbrausch的作品: the product 中的说明字幕)。当然这只是忽悠外行的吓人话。其实写过游戏引擎的人都知道那只是通过实时渲染的到的,而音乐本身就是体积在12kb左右的mod音乐序列(见我以前写过的文章)。
目前很多机器都已安装最新版本的DirectX,而OpenGL是windows的默认库之一。这样Demo Scene设计者一般就不需要自己去编写基本的3d引擎。动画部分几个基本特效的代码不会超过30kb(这里假设开发者具有较高的设计素质),而一些复杂网格模型的纹理贴图即时采用bmp保存,也在100kb-300kb左右。加上mod音乐和其播放引擎。一个64kb DemoScence程序的原始体积一般应在600kb。而不是通过用等效为avi文件计算方法算出的几个G的天文数字。
不过,问题就产生了,实际程序体积只有64kb。在这600kb->64kb还是有相当距离的。如何尽可能的去减少这部分文件大小,以及其中伴随的一些技巧就是本文所要讨论的。
Some Tricks
对于减小程序代码体积,自然对coding的技巧有一定要求。不过这不是问题所在。大家都知道使用VC编译产生的程序,即使就写了句printf("HelloWorld");,也会产生出100kb以上的代码。但是实际上这行语句的有效代码只是:
WriteFile只是句API call, 实际上对应汇编大致为(这里只是说明性描述):
push NULL
push NULL
push 12
push offset "Hello World"
push StdOut
call WriteFile
就这么几行汇编指令,大致也就几十字节的数据,但VC却占用了大量的体积。而那些多余的文件数据主要是下面这些内容:
1. C运行库
这应该是最主要的因素,程序中会编译近大部分的c函数库的代码信息。同时,在程序的开始执行点到逻辑上认为起始位置:main函数之间也填充了大块的C运行库代码完成初始化工作(初始化堆数据、获取命令行数据)。对于完成通常编程任务来说,这些代码是十分必要的。但是对于一个需要尽可能小体积,且具有足够经验的Demo Scene开发者来说,这些代码绝对是鸡肋。
因此,减少程序体积的第一要务就是将C运行库完全从程序代码中剥离。不过要实现这个需要满足几个前提:
a.程序不能使用C运行库,这是当然的。不过有人会问一些很常用的函数,诸如printf没有了该怎么办。回答只有是:自己使用等效的API实现。不过后文也会介绍一些办法
b.尽量不要使用C++语言,原因是对于class的一些操作,诸如析构操作。new/delete运算符。这些本该是语言特性的语句,实际上在编译时会去调用c运行库来完成。
c.不要使用tchar.h
d.关闭VC后续版本提供的堆栈安全检查、异常处理等特性
e.完全采用Win32 API
对于很多人来说,要满足这些条件已经无法正常编写程序了。可能这也是Demo Scene的一个门槛。这里有一个变通办法,就是采用微软提供的精简版C运行库(LIBCTINY.LIB)或者使用ATL/WTL中的精简版C运行库。也能大幅减小体积。(实际上,在kernel.dll和ntdll.dll中也提供了C运行库的API接口)
在符合上述条件后,就能大胆的将C运行库去除。具体办法就是在链接设置中取消默认库,或者用下面语句:
此时,不会有任何的C运行库被编译进程序,但是基本的windows API还是需要链接的。因此起码需要kernel32.lib。
接下来可以按照需要添加相关的lib或者用LoadLibrary自行加载其他库。具体相信也不需要我废话了。
不过需要注意的是几个特列。位于Winnt.h中有如下定义:
#define RtlZeroMemory(Destination,Length) memset((Destination),0,(Length))
相信MS这样做无非是考虑到运行效率和debug的需要。但是这样也给我们的工作造成了麻烦。如果在程序中直接或间接的使用了这2个函数(还有其它情况)的话,仍旧会被linker告知symbol _memset不存在。最直接的办法可以去修改这个winnt.h。但相信这是个十分愚蠢的做法。因此,推荐的办法是先undefine这些定义,再重新import。
#undef memset
#undef RtlFillMemory
extern "C" NTSYSAPI BOOL NTAPI
RtlFillMemory ( VOID *Source1,DWORD Source2,BYTE Fill );
#define memset(Destination,Fill,Length) RtlFillMemory((Destination),(Length),(Fill))
其他类似的情况也可以这样处理,同时大家也可以开动智慧将部分C运行库用kernel32.dll或者ntdll.dll中现成的等价函数替换。这样对于暂时不习惯完全采用WINAPI编写的开发者来说能带来些便利。
到目前为止,在代码逻辑上已经将C运行库从程序剥离了,但实际上编译还是不会通过。原因在于目前程序的真正起始位置还不是int main()或者int WinMain(...)这些。在执行这些逻辑上的开始位置前,还有不少的C运行库Wrapper。
要将这个Wrapper去除,直接办法就是在link选项中修改入口地址到目前的main(WinMain)函数。或者用等效语句:
此时,如果程序中有MyMain这个函数,那么他将真正作为程序的入口点(OEP)。不过要注意的是,作为OEP的函数不能带参数。因为传统的main函数或者WinMain中那些命令行信息的参数,都是由之前的C运行库Wrapper获取提供的,而直接从OEP启动时候,是没有参数提供给程序的。这样将造成堆栈的不平衡(但实际影响不是很大)。
在做到这一步后,就可以尝试编译程序。以VC中创建的SDK的helloworld示例程序为例。经过目前的操作,产生的程序应该在2kb左右或更小(作者并未测试,只是估计值)。不过这里要注意的是,目前的程序有一个问题:进程无法结束。这是因为在原始的C运行库Wrapper中,执行完了WinMain等程序,会调用ExitProcess()终止进程。因此,在我们的新OEP程序的适当地方,加入此语句即可。
对于原先WinMain等函数中参数的获取
要获取诸如命令行参数或者HINSTANCE值,其实可以调用相应的API实现。对于命令行,可以调用函数:
对于HINSTANCE,可以调用
其他参数这里就不列举了,可以查阅相关文档或者反汇编原始的C Wrapper分析。
2.段合并问题
段(Section)是一个PE文件格式中的术语,具体可以参照MSDN中的一篇很完善的PE格式介绍文章"An In-Depth Look into the Win32 Portable Executable File Format"(http://msdn2.microsoft.com/en-us/magazine/cc301805.aspx),对于采用VC生成的程序文件,一般会带有下面3个段:
.text
.data
.rdata
一般而言,代码主体将被置于.text,而一些常量数据和资源文件会分配在其余2个段中。对于一个段来说,它的大小不是随意的。因此当程序代码或者资源较少时,就会在段内存在大量的冗余数据(一般都是0)。这样就白白占用了程序体积。解决办法就是把它们合并到一个或多个段中,压缩体积。
采用如下语句合并2个段:
也可以重新定义一个段,然后将所有段合并到新定义的段中
#pragma comment(linker, "/MERGE:.data=tiny")
#pragma comment(linker, "/MERGE:.text=tiny")
#pragma comment(linker, "/MERGE:.rdata=tiny")
这样也能在一定程度上减少体积。
不过注意的是,这个办法适用范围并不大,尤其是对于含有资源的程序,基本很难成功运行。而且他对程序体积的减少贡献不是很大。
3.将程序加壳
如果仅仅利用上述手段,实际上只是得到了一个程序本应该具有的体积。但是就像前面提到的一个典型的demo程序也要占用600kb的空间。那么如果做近一步缩减呢?
方法其实就是数据压缩。对于程序的代码本身、纹理贴图(假设为非压缩的bmp)、MOD音乐(其中音色为wav格式)。这些都含有很高的冗余信息,具有很大的压缩潜力。因此可以使用一些压缩加壳工具将程序加壳。就Demo程序而言,一般采用UPX和由farbrausch专门为demo程序设计的kkrunchy。同时也十分推荐国内由Dwing制作的加壳工具:WinUpack 。
这几个工具均能从网络上免费获取,下面我做一点简要的点评
对于UPX和WinUnpack
采用的是LZ系列的压缩算法,而WinUnpack采用的一些改进措施,所以压缩效果比UPX好。同样采用LZ系列压缩算法的是人人皆知的WinZip和WinRAR工具。对于这2个压缩壳而言,因为定位于通用的程序加壳,所以具有较高的稳定性。而UPX历史悠久,因此基本上可以应对所有情况。
对于kkrunchy
他采用的是目前号称压缩比最高的PAQ7-9算法。
参考:
对于各类压缩算法的比较:http://www.maximumcompression.com/index.html
PAQ算法在wikipedia的介绍:http://en.wikipedia.org/wiki/PAQ (大陆需要设置代理访问)
PAQ算法的相关说明和论文:http://www.cs.fit.edu/~mmahoney/compression/
PAQ虽然具有很高的压缩比,但是代价就是缓慢的解压缩和压缩速度。有人曾测试说解压缩1MB数据需要半分钟。但是因为很多demo程序体积都不大,而且自身加载后还需要近1分钟的预先计算过程,所以这还可以接受。
不过kkrunchy还有一个缺陷,就是他会采用上面提到的段合并技术。因此很多带有资源的程序加壳后便无法执行。
这里谈谈我本人的看法,我比较倾向使用前2者。第一是这些加壳工具在压缩比率上差别不是很大。不过UPX目前可以看成是开发的技术,WinUnpack为国人开发。而kkrunchy,他的开发者farbrausch自身就是Demo Scene的参赛者。
不过这只是本人的看法。
在经过了加壳处理后,原先600kb的程序很容易的缩小到了64kb左右。这样,又一个“神奇”的demo程序诞生了。(当然真正能让人称的上神奇的应该是本身的画面和音乐)。
案例分析和其他一些技巧
最近进入MSRA实习不久,任务还不是非常忙。MSRA在每天下午3点会供应水果,但是我时常忘记。因此就写了一个小程序,在特定时间会在屏幕上显示半透明的水果图片。同时为了不能打扰正常的工作操作,所以这个半透明的图片不会干扰正常的鼠标操作。下面就提供出这个程序代码,同时附上一些讲解。(这个程序是下班后在我笔记本上编写的,不涉及任何著作权问题)。
程序的运行效果如图所示:
为了不影响当前我进行的工作,在显示画面的时候,可以看到原有的操作不会打断。或者说这个窗口会将事件穿透。实际上就是利用了WinNT5.0以后提供的LayeredWindows特性。但很多人只是以为LayeredWindow只是简单的实现了窗口半透明。实际上他能通过对alpha的识别作高级的hittest和不规则窗口的创建。对于用LayeredWindow产生的不规则窗口效率要比传统的SetWindowRgn方法快出很多(我猜可能有20倍)。
而目前一些桌面Widget也正是利用这个特性制作的。实际也没什么技术含量,感兴趣的可以参考msdn的一篇文章:
http://msdn2.microsoft.com/en-us/library/ms997507.aspx
本程序正是利用前面所说的将C运行库剔除的办法编写的,同时还有一个特别之处,就是仅采用了单个c文件实现了程序的所有功能。这样的好处之一就是可以使用前文提到的段合并,而不需要额外的资源文件。其实就是将图片文件保存为了数组信息作为代码附加在程序中。
这里顺便说下GDI显示的效率问题,很多人都认为GDI效率低下,尤其是在对像素级别进行操作的时候。其实不然,最好的例子就是Flash播放器。目前Youtube类型的网站十分普及。那些flash实时渲染得视频、flash实现的3d引擎正是直接用GDI绘图实现的。关于如何高效进行GDI绘图的论述网上已经很多。在这里提这个一方面是因为这个程序中用到了同样办法:采用CreateDibSection实现创建出图像的内存数据缓冲,接下来就可以直接对内存操作实现像素的修改。而不必使用SetPixel这种缓慢的函数。同时,目前使用GDI直接渲染的Demo也是有的。
下面就给出本程序的主要代码,完整的代码和程序在文章末尾提供:
如果对本程序感兴趣的话,也可以通过修改其中的一些Define定义作简单修改。当然如果你能基于他写个更加强大的提醒程序,也欢迎给我看看:-)
本程序采用VS2005/VS2008在xp系统中成功运行。
代码和程序:http://www.csksoft.net/data/legacyftp/Products/Misc/Fruit_Alert.rar
By CSK