给逸珑8089安装JWM窗口管理器&一些其它事情

前几个星期把逸珑8089上面的Debian 8弄得启动不了图形界面了,同时也发现更新过的Iceweasel不能在8089上运行(每次一打开就说非法指令),于是就重新装了GNewSense 3,希望能进入图形界面以及启动旧版的Iceweasel。安装的过程和先前Debian的网络安装是一样的,为了方便也用了大笔记本(Precision M4400)充当路由器来分享WiFi。安装完成后的默认图形界面是Gnome 2,有WiFi,有电源,挺好用的。不过有一些性能问题,就是Gnome 2用起来有点慢,且命令行打开时会需要等待约2秒的时间。以下描述上面两个问题的解决方法,再描述一些其它的事宜。

这些事宜的解决是为了让龙芯笔记本不要处于闲置的状态,而要去达成一个有意义的目的。目前这个目的就设定为按照《Ray Tracing from the Ground Up》撰写光线追踪程序,体验一下在龙芯笔记本上编写程序的感觉。

  • 把Gnome 2换成更轻量级的JWM
    Gnome 2在8089上光是打开菜单这么简单的操作都会让屏幕一闪一闪的,给人感觉要费老大力气才把屏幕刷新好。真是不好用。所以考虑把窗口管理器换成Puppy Linux中用的JWM。(为什么不换i3之类的平铺窗口管理器呢?主要是平铺会强行改变OpenGL程序的窗口大小,让编写OpenGL程序时不方便。)更换的过程好简单的,从JWM的主页上下载最新的源代码,然后装上所有的依赖项(GNewSense 3里面都有的),然后编译安装,就有了执行程序。但是有了执行程序还不够,还要进行系统方面的设定,也就是更换 /usr/share/xsessions/Jwm.desktop 之类的文件。我不太会进行这方面的设定,所以是先安装了源里的JWM(是旧版),让系统自己完成设定,然后把 /usr/bin/jwm 换成自己编译完成的新版JWM,这样就能在欢迎界面中选择JWM了。在安装完JWM之后,还需要费心进行一些设置。进行设置的过程,其实各种桌面环境都差不多吧,都是一些麻烦的事。只不过有的像XFCE这样能够通过图形界面进行设置,有的只能像JWM这样通过改配置文件进行设置了。磨合一段时间后,就会越来越好用。JWM在龙芯上用比较好用,在大笔记本Precision M4400上和Gimp按上画图板一起用时会有些小的问题,有时候画图板会失去响应甚至造成整个桌面没有响应,所以在大笔记本上还是用XFCE。(XFCE对龙芯来说也太重量级了。)
  • 减小Bash启动时的等待时间
    对于在龙芯上的GNewSense 3,Bash启动时的等待时间是由于 /etc/bash_completion 这个脚本造成的。这个脚本有“第一版”和“第二版”之分。在新近的系统中,都是第二版的。但是在龙芯上的GNewSense 3它还是第一版的。第一版比第二版速度慢,如果换成了第二版的,速度就能快很多。第二版bash-completion以deb包的形式存在,安装Debian Wheezy所配的DEB是可以的。安装完以后打开终端所需要等待的时间就从2秒减小到半秒左右了。在安装时,会有以下画面。
    Screenshot
  • 其它事情
  • 以下几条是在折腾龙芯电脑时在大笔记本上也遇到了的事。

    • 编译安装Conky
      在编译Conky时,如果想要显示NVidia显卡的状态或查看WiFi的信号强度,需要分别启用USE_NVIDIA和WLAN这两个变量。(Todo: 详情
    • 鼠标消失
      鼠标消失是因为启动了gnome-settings-daemon。如果把它砍掉,指针就会回来。
  • 以下几条是在Macbook上安装过Linux之后遇到的一些事。
    • 模仿Mac OS中的缩放模式,设置高分辩率显示屏 + 普通显示屏的双输出

      这一条讲的是如何设定 X Server ,使得在笔记本上有高DPI的显示屏、又外接了一个普通DPI的显示屏时,不会出现普通显示屏上的文字过大的情况。具体而言,就是在Macbook Pro上面安装了Linux以后如何配置外接显示屏。
      首先,我们知道,普通显示屏上的文字过大是因为屏幕缓冲区不经缩放直接点对点显示在所有显示器上。所以,一个290像素高的字,在290PPI的显示屏上高度就是1英寸,到了145PPI的显示屏上就变成2英寸了,就是比正常情况大了一倍。根据这个现象,只要把对应着普通显示屏那部分的缓冲区在显示时用普通显示屏上的1个像素显示缓冲区中的4个象素,就能使得在145PPI的屏幕上显示的290像素高的字还是1英寸高了。

      因为1个普通显示屏上的像素对应着4个缓冲区中的像素,所以缓冲区的像素数也是普通屏幕的像素数的4倍。这个4倍,会在X的设定中用到。具体而言,就是“输入的Viewport”的像素是“输出的Viewport”的像素的4倍。

      (Todo)

  • Ray Tracing from the Ground Up
    搭框架时发现用glut创建窗口是会segfault的。像NeHe的教程中就没有用到glut,而是用了glfw。用了NeHe的创建窗口的方法,就能在龙芯上正常运行了。
    有了Ray Tracer,就能对龙芯的性能有一个更好的了解,这个龙芯本就能逐渐地完成它的历史使命了。
    Screenshot-Untitled Window

 

Posted in Uncategorized | Leave a comment

字幕君

字幕君(字幕君只是一个代号,其窗口名字还是「Fluid Demo」)是用于在舞台上显示歌词烘托演出气氛的一个小程序,最近的一次亮相是……在俺们的「胶囊乐队」第一次演出时登台,用于显示歌名和歌词。

mmexport1416941092981–>Screenshot - 11292014 - 05:02:09 AM

 

(对的,在现场那天,连FPS Counter都没有拿掉。因为俺感觉跳动的示数看起来就会让人觉得兴奋哈哈)

字幕君是以[1]为蓝本修改的。原先的程序模拟了一个热源、一个密度源作用下带有障碍物的网格中流体流动的现象。字幕君以[1]为蓝本修改的过程中,增加了以下内容:

  1. 将文字输出到密度场中参与流体相关计算、以及配套的非常非常简陋的设置歌词、颜色、转场时间的脚本。
  2. 经由boost::shared_memory_object来使得通过ssh遥控字幕君成为可能。

这两项功能是独立的、不互相依赖的。同时,在制作字幕君的过程中,还略微对Gauss-Seidel法和Jacobi法有了一些了解。

【Gauss-Seidel、Jacobi法和共轭梯度法】

这三种方法的用处都是为了解以下方程,亦即Navier-Stokes方程中的Diffuse项:

Diffuse项的连续形式:
Diffuse Term Continuous Form
将其转换成网格上的离散形式,就能看出 Jacobi 法和 Gauss-Seidel 法的端倪:
Diffuse Term Discrete Form
式子右边的是上一时刻的u场中的值,左边的是欲求的本时刻的u场中的值。经过多次迭代以后,上一时刻的u场就变成了本时刻的u场。

 

这个程序中解线性方程使用的是Jacobi法,[2]中用的是Gauss-Seidel法。Jacobi法与Gauss-Seidel法的区别在于在一次迭代中,是否使用已经在这次迭代中覆写过的元素。对应到程序中就是在最内层循环中的赋值语句的左侧写入到哪里: Jacobi法是在每一次循环结束时才统一将x场覆写一次,但是 Gauss-Seidel 法在循环过程中就将 x 覆写一次:

// Gauss-Seidel
for(int i=1; i<=w; i++) {
    for(int j=1; j<=h; j++) {         
       x[IX(i, j)] = (a*(x[IX(i-1,j)] + 
                         x[IX(i+1,j)] +
                         x[IX(i,j-1)] + 
                         x[IX(i,j+1)]) +                                x0[IX(i,j)]) / c;
   }
}
// Jacobian 
for(int i=1; i<=w; i++) {
    for(int j=1; j<=h; j++) {
      aux[IX(i, j)] = (a*(x[IX(i-1,j)] + 
                          x[IX(i+1,j)] +
                          x[IX(i,j-1)] + 
                          x[IX(i,j+1)]) +
                       x0[IX(i,j)]) / c;
    } 
} 
memcpy(x, aux, sizeof(float)*(w+2)*(h+2));

至共轭梯度法的实现则和这两者不太相同。[4]在讲述共轭梯度法时将问题描述为了一个线性系统Ax=b。一开始费了一番力气才明白其中的A、x和b分别是什么。虽然花了一番力气,但是有了这样的一般化描述,以后面对类似问题时就更容易明白了吧。在具体实现过程中,矩阵A是以一个函数FluidSolver::multiplyASolver存在的(对于N*N的网格来说,A矩阵的大小是N^2 * N^2,但是它是一个稀疏矩阵,大量的元素是0),x是下一时时刻的x场,b是这一时刻的x场。将展开后的离散形式中的下一时刻的x全部移到左边,这一时间的x全部移到右边,即可得到A的撰写方法。

// Conjugate Gradient (来自[4])
0. 设置A和b
1. r <- b - Ax
2. p = r
3. rho = dot(r, r)
4. rho0 = rho
5. for(iter=0; iter<max_iter; iter++) {
6.   if(rho ==0 || rho <= tolerance) break;
7.   q <- A p
8.   alpha = rho / dot(p, q)
9.   x += alpha cdot p
10.  r -= alpha cdot q
11.  rho_old = rho
12.  rho <- dot(r, r)
13.  beta <- rho / rho_old
14.  p <- r + beta cdot p
15.  setBounds(x)
16. }

Jacobi法的好处是每个格子之间没有相互依赖,适合GPU实现。Gauss-Seidel法的好处是收敛速度快一些。共轭梯度法的收敛速度更快,如下图所示:

收敛速度

以我自身的使用情况看来,[1]所提供的GPU实现可以在320×320的格子上达到60fps的更新速度,但是[2]的CPU实现只要超过300×90就会迟滞了。不过字幕君一开始的想法是在[2]上试验的,因为[2]中流体参与运算的所有过程都是在CPU上进行,不涉及帧缓冲对象和贴图对象之类的OpenGL Clinet和Host之间的数据交换,比较简单。

[4]说,共轭梯度法在GPU上的表现比红+黑的Gauss-Seidel法和Jacobi法都慢。至于为何尚不可知,但是若是在CPU上,凭借其高速收敛的特性,共轭梯度法应当是最快的。

【将文字内容与流体模拟连起来】

将文字输出到密度场中借用了freetype-gl的范例。freeglut-gl完成了读入TTF字形、将矢量字形转为顶点缓冲并将文字输出到一张贴图的工作。在这之后只需将含有文字的贴图以Shader写入[1]中的循环,既可做成一个以字形为形状的密度源。

目前的脚本支持三个不同的贴图同时在屏幕上出现,而这套脚本就是用于管理这三个不同的贴图,使其能在{不可见、淡入、可见、淡出}这四种状态间转换,另外控制每个状态中所对应的参数({淡入/出时间、处于可见状态时每帧向密度场添加的流量})。与以上截图相对应的脚本如下所示:

CapsuleBegin
Pen Color(1,1,1) Delay(1000)
Silo0 Text Pos(35,190) Delay(0) Weights(0.01,0.01) " 啦啦啦 啦啦啦 啦"
Brush0 Rect Velocity Pos(480,0,460,270) Value(-30,0,0,1) Frames(200000)
Silo0 FadeIn Duration(1000)
Silo1 Text Pos(35,90) Delay(1000) Weights(0.01,0.01) " 在生命中\n 最美丽的一天 "
Silo1 FadeIn Duration(1000)
CapsuleEnd

CapsuleBegin
Silo0 FadeOut Duration(1000) Impulse(0)
Silo1 FadeOut Duration(1000) Impulse(0)
Brush0 Stop
CapsuleEnd

从中可见在脚本是由一些位于CapsuleBegin和CapsuleEnd之间的命令组成的,若干连续执行的命令构成一个动作,在实际上台演出时,按一下空格键就会执行一个动作,即是包含在此动作中的所有命令。CapsuleBegin命令可以有标签,若有的话,则可通过B与N键来定位。在这回使用的脚本中,标签全部位于17首曲子各自的开始处。这样在遇到意外需要重来一曲或是字幕放映出错时,就可以及时调回到正确的位置了。

【远程控制】

若想在表演场地显示字幕君,必须将参与计算的电脑连接至调音台上的VGA Port上。由于调音台离舞台很远,所以人在舞台上时就不能直接操作参与计算的电脑。由于表演场地有Wi-Fi覆盖,所以通过Wi-Fi进行远程操控就成为一种可行方案。所以,将另外一台电脑相连之后,就有了以下的拓扑图:

逸珑8089D  XPS M1330 舞台上的银幕

这台逸珑8089D是去年团购时买的,重装了Debian 7。鉴于龙芯笔记本图形性能有限且无线网络带宽可能有限的问题,带图形的远程桌面不是好选择,所以只考虑了以文字界面的SSH登录这种方式。

SSH登录以后,还剩下的问题就是如何将正在运行中的程序的状态告知用户和如何响应用户的输入。这两个问题都可以通过为字幕君再编写一个配套的控制程序来解决。通过参照[3]的范例,达成了字幕君与字幕君的控制程序之间以boost::shared_memory_object通信的功能。在以上的画面呈现时,字幕君控制器所在的控制台中会显示如下内容:

Received: Heart Beat
Silo 0: [  KEEP      ]  啦啦啦 啦啦啦 啦
Silo 1: [  KEEP      ]  在生命中\n 最美丽的一天 
Silo 2: [  KEEP      ] 10.
Action -1/215, Line1005/1455

这样字幕君的功能就差不多齐全了。

【杂项】

※ 因为歌词有中文,所以在程序中有很多地方都需要使用wchar_t*,所以相应的字串处理函数也需要使用宽字符的对应版,如strcmp的对应即是wcscmp(Wide Character String Comparison)。有一个例,即是在printf时,可以不像其它与char*对应的函数那样使用wchar_t的对应版wprintf,而是只需使用printf(“%ls”)。其原因是wprintf与printf是不推荐混用的。另外,读取中文文本目前还不清楚除了使用C++ STL中的<wifstream>以外的方法,因此也只能将CPP文件宣告为extern “C”以后在C文件中使用。

※ 表演场地的舞台的VGA接口支持的分辨率有很多,当时选定的是960×540,可是实际测量之后,发现只有正中间的大小约为720×480的区域是可见的,周围全被裁剪掉了。为了照顾不同的屏幕大小,字幕君可将整个窗口的内容缩放,而相应的渲染流程也从直接渲染到窗口的帧缓存修改为先渲染到某贴图,然后将各个贴图贴到窗口上。这样做的原因是有些Shader依赖窗口位置和大小,而若直接从Clip Space的坐标(-1, 1),就可避免绝对窗口大小带来的问题。

【接下来的打算】

接下来我还打算将字幕君用作自己想翻唱的一首歌里,可是目前的状态是我唱功很不行,大概还需要多爬爬关于唱歌方法的文章和播客内容才能知晓如何突破吧。

【参考】

[1] Simple Fluid Simulation at The Little Grasshopper
[2] Stable Fluids at Caltech Multi-Res Modelling Group – Stam’s Stable Fluids
[3] Create two C++ programms using the Boost library for a shared memory example
[4] Goncalo Amador, A. G. (2009). Linear Solvers for Stable Fluids: GPU vs CPU. In 17th Encontro Portugues de Computacao Grafica (EPCG’09).

Posted in Programming, Uncategorized | Leave a comment

Thoughts on MidiSheetMusicMemo

最近我觉得《MidiSheetMusicMemo》有一个问题:在“问答模式”中玩一次的时间对于正常的谱子而言太冗长,以至于坚持到最后是一件很辛苦的事情。这本身并不是很大的问题,但是因为以现在的程序的状况,如果不坚持到最后,统计数据就不会更新,就不会有成就感,那就会让用户体验很不好。目前连俺作为开发者自身都没法认真玩完,对于其它用户可想而知。
就拿最近的《二部创意曲8》和《二部创意曲13》来说,虽然小节数不是很多,各有35个小节和25个小节,但是却要至少五分钟甚至是十分钟才能完成。即使这样的小曲子时间都如此之长,其原因之一可能是因为选项的个数太多:八选一从绝对数量来看是很多的,俺自己在使用时都会有一种“即使我能背下这个谱子,也不能很快地从八个选项中找到正确的选项,因为错误选项的干扰效应太强了。”的感觉。应该试试看三选一、四选一和六选一,不应该就从一开始就吊死在八选一这棵树上。
话说最近想尝试一种新的转场效果,并修复一些bug。希望年底之前有空做完吧。

Posted in Uncategorized | Leave a comment

Cangjie input method mini game

It's here —> http://edgeofmap.com/cj
It's still under development.
I would want to gamify the learning process, creating a fun and rewarding experience. When I started learning 倉頡 in February I used a Win3.1 program (which was 16-bit!) called 「倉頡教室」.
It's first exercise was about the parts-keys. The purpose was to help remember which parts are on which [a-z] keys. The parts in Cangjie were divided into groups (Group 1=日月金木水火土, group 2=竹戈十大中一弓, group 3=人心手口, group 4=尸廿山女田卜) and so was the first exercise.
The second exercise teaches simple compound characters. Example: 明=日+月.
The web game has so far replicated the first exercise and part of the second exercise.
I was wondering this may be useful for not only those who're learning the input method, but also those who are learning Chinese characters, even without prior exposure to those characters.
Still this web app has a long way to go, either to becoming a fully usable Cangjie tutorial or a complete introduction to Chinese characters.

Posted in Uncategorized | Leave a comment

PA9

PA9的简单的代码生成器

【确立「先把操作数读入临时寄存器、再将临时寄存器中的值写回目标操作数中」的code generator结构】

自从开始做第一个测试用例时,就会发现,开门第一件事情就是要知道会出现不同的搬运数据的情况。对于每个cy86指令,通常都有一个操作数(operand)。其可来自于寄存器([x,y,z,t][64,32,16,8])或来自内存地址([寄存器+常数] 或 [Label+常数]),或是立即数(immediate)。所以,对于每一个cy86的指令,都一定会是在以下的一个表格中的一种情况。

到哪里去\从哪里来 寄存器 内存地址 立即数
寄存器 情况1 情况2 情况3
内存地址 情况4 情况5 情况6

对于上面的6种情况的每一种,都要有一种相应的对策。需要相应的对策的原因是,用x86指令并不能直接达成某些功能,比如说mov就不能将某个内存地址中的数据移到某个内存地址,而是需要先从内存移到寄存器,再从寄存器移到内存。那个寄存器就是一个「临时寄存器」。
也就是说,对于同一个cy86指令,在产生x86 code的时候都需要分清楚这6种情况(来的时候三种、去的时候三种)。但那样就会有很多重复的code了。如果都经由临时寄存器,就可以避免在每一个非终结符的处理函数中都分六种情况,就可以简化code generator的撰写过程。
那不妨就将「经由临时寄存器」的手法套用到全部六种情况之上。这样的话,这六种情况会变成以下这样,虽然变麻烦了一点,但是却比较简单容易理解:

到哪里去\从哪里来 寄存器 内存地址 立即数
寄存器 先把源寄存器的数据存入临时寄存器
然后把临时寄存器存入目标寄存器。

先把源内存地址中的数据存入临时寄存器
然后把临时寄存器存入目标寄存器。

先把立即数存入临时寄存器
然后把临时寄存器的值存入目标寄存器。
内存地址 先把源寄存器的数据存入临时寄存器

然后把临时寄存器存入目标内存地址。

先把源内存地址中的数据存入临时寄存器

然后把临时寄存器存入目标内存地址。

先把立即数存入临时寄存器
然后把临时寄存器存入目标内存地址。

在以上的每个格子中,蓝色的部分是用于「读入源操作数」的。而红色的部分是用于「写入目标操作数」的。所谓「操作数」,在parser里面看到的其实是一个ASTNode*=语法树结点。

因为每个蓝色的情况都是读入操作数,所以这些就可以都用一套类似的函数来表示。
同样因为每个红色的情况都是将临时寄存器写入操作数,所以这些也都可以用一套类似的函数来表示。如下所示。

把源操作数的数据读入临时寄存器
(包括了所有蓝色的情况)
codeOperandToRegister(const char*, ASTNode*)

把临时寄存器写入目标操作数
(包括了所有红色的情况)

codeRegisterToOperand(ASTNode*, const char*)

以上的const char*表示临时寄存器的名字。
只要有了这些,就可以不用在每个非终结符中都处理六种情况了,而只要通通写codeOperandToRegister,然后对register进行相应的运算,然后再codeRegisterToOperand就可以了。
接下来的问题就是针对六种情况来撰写所需的x86 code。所以第一件事情就是需要知道,一共有哪些x86寄存器被用到了。

【为上文所述的结构来撰写所需的x86 code】

临时寄存器即用来存输入参数、也用来存输出参数。
所用过的临时寄存器有以下这些:

用途 所用过的临时寄存器 说明和例子
充当移动数据时的临时寄存器 ・rax, eax, ax, al ・move8 x8, y8
充当二元运算符的临时寄存器 ・rax, rbx, eax, ebx, ax, bx, al, bl
・rdx, ebx, dx, dl
・isub8 x8, y8, z8
・umod8 x8, y8, z8
充当一元运算符的临时寄存器 ・rsi, esi, si, sil
・al, rbx
・rax, eax, ax, al, cl
・rax, rbx
・s8convf80 f80, s8
・jumpif br8, ar64
・lshift16 x16
 
充当syscall的参数 ・r8, r9, r10, r12, rdi, rsi, rax, rdx ・syscall6

对于这些寄存器,根据情况1~6,可知、需要撰写以下的x86 指令。

  • 需要撰写把一个寄存器的值赋给另一寄存器的x86的mov指令。(寄存器–>寄存器)
  • 需要撰写将以上的寄存器中的值存入内存的x86的mov指令。(寄存器–>内存)
  • 需要撰写将内存中的值存入以上的寄存器的x86的mov指令。(内存–>寄存器)
  • 需要撰写将立即数存入内存的mov指令。(立即数–>寄存器)

一些其它的组合,比如立即数–>内存和内存–>内存,就是由两个操作头尾相接来完成的:立即数–>内存 就是 立即数–>寄存器–>内存;内存–>内存 就是 内存–>寄存器–>内存。
于是在实作中,就有了以下函数。

立即数–>寄存器 void emitMovabxInst(const char* destreg, unsigned char* data, unsigned datasize, unsigned reg_width);
寄存器–>寄存器 void emitMovRegReg(const char*, const char*);
寄存器–>内存

这要由两步来完成,第一步是将内存地址存入rdi中,然后再将寄存器存入[rdi]中。
第一步:codeMemoryAddressToRDI(ASTNode* mem);
第二步:void emitMovToXXXXPtrRDI(const char*);
其中XXXX可以为Byte、Word、Dword、Qword,依数据长短而定。

内存–>寄存器

同样是由两步来完成,第一步是将内存地址存入rdi中,然后再将寄存器存入[rdi]中。
第一步:codeMemoryAddressToRDI(ASTNode* mem);

第二步:void emitMovFromXXXXPtrRDI(const char*);
其中XXXX可以为Byte、Word、Dword、Qword,依数据长短而定。

这样,就完成了数据搬移的部分,撰写code generator的思路也清晰了很多,可以看见Hello World了。

Posted in Uncategorized | Leave a comment