mesa3d源代码阅读笔记
最近准备去阅读Mesa3d(采用7.6.1版本)的源代码,代码量很大,感觉到十分困难。以后是否能够读下去,并且理解它的整个实现过程,不能确定。但既然阅读了,就应该积累起来,特写下本系列笔记。1 编译Mesa3d 7.6.1是很容易的事情,里边包含有VC8的sln解决方案,打开它直接编译即可完成;2 如何调试编译出来的opengl32.dll文件呢?其实Windows系统在检索exe所需要的dll文
最近准备去阅读Mesa3d(采用7.6.1版本)的源代码,代码量很大,感觉到十分困难。以后是否能够读下去,并且理解它的整个实现过程,不能确定。但既然阅读了,就应该积累起来,特写下本系列笔记。
1 编译Mesa3d 7.6.1是很容易的事情,里边包含有VC8的sln解决方案,打开它直接编译即可完成;
2 如何调试编译出来的opengl32.dll文件呢?其实Windows系统在检索exe所需要的dll文件时,第一顺序是在当前exe文件所在的目录,所以只要保证编译出的opengl32.dll与调试用的exe文件生成在同一目录,就可以在调试时进入到对应opengl函数的实现代码环节,并不需要将opengl32.dll拷贝到系统目录(这样做可以避免对系统目录不必要的更改);
3 阅读源代码时,我首先是找opengl32.dll中导出的函数它具体对应的代码函数在哪儿。严格地说这比较难找,因为在mesa里是使用函数分发表来处理的。在glapitemp.h头文件里以宏的方式定义了许多函数,该文件被两个.c文件包含,一个是dispatch.c,一个是glapi.c。前者结合dispatch.h包含glapitemp.h将对应宏展开成为对应opengl32.dll所导出的函数体定义;而后者则是定义成为一组对应opengl到处函数的相应NoOp*函数,并且将这些函数保存到__glapi_noop_table结构变量里。实际上在运行过程中,一直会有一个extern struct _glapi_table *_glapi_Dispatch;内部函数成员地址会被修正成为相应的mesa实现函数。调用opengl函数实际上是通过dispatch.c呼叫_glapi_Dispatch里所存储的对应函数,这一过程即分发,所以说dispatch table呗;
4 当然这里wgl*系列函数和部分gl*函数都比较好找,而glBegin、glEnd以及用在它们之间的glColor*、glVertex*系列函数的分发过程就比较复杂。到现在为止,虽然调试过好几遍mesa源代码,但是仍然晕头转向的。
5 Mesa3d 7.6.1源代码里有部分文件写有一些说明,真的很宝贵,但遗憾的是实在太少;整个工程模块又存有许多模块,阅读量很大;在光栅渲染环节存在许多函数分发表,搞得晕头转向。
不知道阅读这样的源代码怎样去做比较好?
2011-04-19 附加阅读源码时的opengl程序代码
共计三个文件,代码是在Nehe教程基础上,附加面向对象封装之后而成,不一定好,仅供参考。
从属于demo项目,添加到mesa工程解决方案之下。

OpenGLDC.h/*****************************************************************************
封装OpenGL的专用类
*****************************************************************************/
#ifndef __OPENGL_DC_H_INCLUDED
#define __OPENGL_DC_H_INCLUDED
#ifdef _DEBUG
#include "gl/gl.h"
#include "gl/glu.h"
#pragma comment (lib, "opengl32.lib")
#pragma comment (lib, "glu32.lib")
#else
#include <gl/gl.h>
#include <gl/glu.h>
#pragma comment (lib, "opengl32.lib")
#pragma comment (lib, "glu32.lib")
#endif // _DEBUG */
class COpenGLDC
{
public:
COpenGLDC();
~COpenGLDC();
public:
GLboolean GLSetupRC(HWND hWnd);
GLvoid GLRelease(GLvoid);
GLboolean GLInit(GLvoid);
GLvoid GLResize(GLsizei nWidth, GLsizei nHeight);
GLboolean GLDrawScene(GLvoid);
GLvoid ProcessKeys(bool keys[256]);
protected:
HWND m_hWnd;
HGLRC m_hRC;
HDC m_hDC;
};
#endif // __OPENGL_DC_H_INCLUDED
第二个文件

OpenGLDC.cpp#ifndef VC_EXTRALEAN
#define VC_EXTRALEAN // 从Windows 头中排除极少使用的资料
#endif
#include <windows.h>
#include <assert.h>
#include "OpenGLDC.h"
COpenGLDC::COpenGLDC()
{
}
COpenGLDC::~COpenGLDC()
{
GLRelease();
}
GLboolean COpenGLDC::GLSetupRC(HWND hWnd)
{
int nPixelFormat;
PIXELFORMATDESCRIPTOR pfd = {
sizeof(PIXELFORMATDESCRIPTOR), // 上述格式描述符的大小
1, // 版本号
PFD_DRAW_TO_WINDOW | // 支持在窗口中绘图
PFD_SUPPORT_OPENGL | // 支持OpenGL
PFD_DOUBLEBUFFER, // 双缓存模式
PFD_TYPE_RGBA, // RGBA 颜色模式
24, // 24 位颜色深度
0, 0, 0, 0, 0, 0, // 忽略颜色位
0, // 没有Alpha非透明度缓存
0, // 忽略移位位Shift Bit
0, // 无累加缓存
0, 0, 0, 0, // 忽略累加位
32, // 32 位深度缓存
0, // 无模板缓存
0, // 无辅助缓存
PFD_MAIN_PLANE, // 主层
0, // 保留
0, 0, 0 // 忽略层,可见性和损毁掩模
};
m_hWnd = hWnd;
if ((m_hDC = ::GetDC(m_hWnd)) == NULL)
{
MessageBox(NULL, "获取设备模式句柄失败","错误",MB_OK|MB_ICONEXCLAMATION);
return false;
}
if(!(nPixelFormat = ChoosePixelFormat(m_hDC, &pfd)))
{
MessageBox(NULL, "没找到合适的显示模式","错误",MB_OK|MB_ICONEXCLAMATION);
return false;
}
if(!SetPixelFormat(m_hDC, nPixelFormat, &pfd)) // 能够设置像素格式么
{
MessageBox(NULL, "不能设置像素格式", "错误", MB_OK|MB_ICONEXCLAMATION);
return false;
}
if(!(m_hRC = wglCreateContext(m_hDC))) // 获取渲染描述句柄
{
MessageBox(NULL, "不能创建OpenGL渲染描述表", "错误",
MB_OK|MB_ICONEXCLAMATION);
return false;
}
if(!wglMakeCurrent(m_hDC, m_hRC)) // 尝试激活着色描述表
{
MessageBox(NULL, "不能激活当前的OpenGL渲染描述表", "错误",
MB_OK|MB_ICONEXCLAMATION);
return false;
}
return true;
}
GLvoid COpenGLDC::GLRelease()
{
if(m_hRC)
{
if(!wglMakeCurrent(NULL, NULL)) // 我们能否释放DC和RC描述表
{
MessageBox(NULL, "释放DC或RC失败.", "关闭错误",
MB_OK | MB_ICONINFORMATION);
}
if(!wglDeleteContext(m_hRC)) // 我们能否删除RC?
{
MessageBox(NULL, "释放RC失败.", "关闭错误",
MB_OK | MB_ICONINFORMATION);
}
m_hRC = NULL;
}
if(m_hDC != NULL && !::ReleaseDC(m_hWnd, m_hDC)) // 我们能否释放DC
{
MessageBox(NULL, "释放DC失败.", "关闭错误",
MB_OK | MB_ICONINFORMATION);
}
m_hDC = NULL;
}
GLvoid COpenGLDC::GLResize(GLsizei nWidth, GLsizei nHeight)
{// 重置OpenGL窗口大小
if(nHeight == 0) // 防止被零除
nHeight = 1;
glViewport(0, 0, nWidth, nHeight);
glMatrixMode(GL_PROJECTION); // 选择投影矩阵
glLoadIdentity(); // 重置投影矩阵
gluPerspective(45.0f, // 透视角设置为45 度
(GLfloat)nWidth/(GLfloat)nHeight, // 窗口的宽与高比
0.1f, 100.0f); // 视野透视深度:近点.1f远点.0f
glMatrixMode(GL_MODELVIEW); // 选择模型观察矩阵
glLoadIdentity();
}
//
//
// 主要改变增减代码存放地
//
//
//
GLboolean COpenGLDC::GLInit(GLvoid)
{
glShadeModel(GL_SMOOTH); // 启用阴影平滑
glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // 黑色背景
glClearDepth(1.0f); // 设置深度缓存
glEnable(GL_DEPTH_TEST); // 启用深度测试
glDepthFunc(GL_LEQUAL); // 所作深度测试的类型
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // 告诉系统对透视进行修正
return true; // 初始化OK
}
GLboolean COpenGLDC::GLDrawScene(GLvoid)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清除屏幕和深度缓存
glLoadIdentity(); // 如果不添加这句,那么所画的都会由大缩小,无法看见
//
glTranslatef(-1.5f,0.0f,-6.0f); // 左移1.5 单位,并移入屏幕6.0
glBegin(GL_LINE_LOOP); // 绘制三角形
glColor3f(1.0f,0.0f,0.0f); // 设置当前色为红色
glVertex3f( 0.0f, 1.0f, 0.0f); // 上顶点
glColor3f(0.0f,1.0f,0.0f); // 设置当前色为绿色
glVertex3f(-1.0f,-1.0f, 0.0f); // 左下
glColor3f(0.0f,0.0f,1.0f); // 设置当前色为蓝色
glVertex3f( 1.0f,-1.0f, 0.0f); // 右下
glEnd(); // 三角形绘制结束
glTranslatef(3.0f,0.0f,0.0f); // 右移单位
glColor3f(0.5f,0.5f,1.0f); // 一次性将当前色设置为蓝色
glBegin(GL_QUADS); // 绘制正方形
glVertex3f(-1.0f, 1.0f, 0.0f); // 左上
glVertex3f( 1.0f, 1.0f, 0.0f); // 右上
glVertex3f( 1.0f,-1.0f, 0.0f); // 左下
glVertex3f(-1.0f,-1.0f, 0.0f); // 右下
glEnd(); // 正方形绘制结束
//
SwapBuffers(m_hDC); // 切换缓冲区,没有切换的话,窗口不会绘制什么
return true;
}
GLvoid COpenGLDC::ProcessKeys(bool keys[256])
{
}
第三个文件

WinMain.cpp#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <assert.h>
#include "OpenGLDC.h"
COpenGLDC *s_pOpenGLDC = NULL;
BOOL s_bFullScreen = FALSE; // 全屏标志缺省,缺省设定为不全屏
bool s_keys[256]; // 保存键盘按键的数组
BOOL s_bActive = TRUE; // 窗口的活动标志,缺省为TRUE
HINSTANCE s_hInstance;
HWND s_hWnd;
char* s_pszTitle = "NeHe's颜色实例";
// 窗口回调函数声明
LRESULT CALLBACK WndProc (HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_ACTIVATE: // 监视窗口激活消息
if (!HIWORD(wParam)) // 检查最小化状态
s_bActive = TRUE; // 程序处于激活状态
else
s_bActive = FALSE; // 程序不再激活
return 0;
case WM_SYSCOMMAND: // 系统中断命令
switch (wParam) // 检查系统调用
{
case SC_SCREENSAVE: // 屏保要运行?
case SC_MONITORPOWER: // 显示器要进入节电模式?
return 0; // 阻止发生
}
break; // 退出
case WM_SIZE:
if (s_pOpenGLDC != NULL)
s_pOpenGLDC->GLResize(LOWORD(lParam), HIWORD(lParam));
return 0;
case WM_CLOSE:
PostQuitMessage(0);
return 0;
case WM_KEYDOWN:
s_keys[wParam] = true;
return 0;
case WM_KEYUP:
s_keys[wParam] = false;
return 0;
}
return DefWindowProc (hWnd, uMsg, wParam, lParam);
}
void KillGLWindow(void)
{
if (s_bFullScreen) // 我们处于全屏模式吗?
{
ChangeDisplaySettings(NULL, 0); // 是的话,切换回桌面
ShowCursor(TRUE); // 显示鼠标指针
}
if (s_pOpenGLDC != NULL)
{
s_pOpenGLDC->GLRelease();
delete s_pOpenGLDC;
s_pOpenGLDC = NULL;
}
if (s_hWnd && !DestroyWindow(s_hWnd))
{
MessageBox(NULL, "释放窗口句柄失败!", "关闭错误", MB_OK | MB_ICONINFORMATION);
s_hWnd = NULL;
}
if (!UnregisterClass("OpenG", s_hInstance)) // 能否注销类?
{
MessageBox(NULL,"不能注销窗口类。","关闭错误",MB_OK | MB_ICONINFORMATION);
s_hInstance = NULL;
DWORD nReturnCode = GetLastError();
}
}
/* 这个函数创建我们OpenGL窗口,参数为: *
* title - 窗口标题 *
* width - 窗口宽度 *
* height - 窗口高度 *
* bits - 颜色的位深(/16/32) *
* fullscreenflag - 是否使用全屏模式,全屏模式(TRUE),窗口模式(FALSE) */
BOOL CreateGLWindow(const char* lpszTitle, int nWidth, int nHeight, int nBits, BOOL bFullScreenFlag)
{
WNDCLASS wndclass; // 窗口类结构
DWORD dwExStyle; // 扩展窗口风格
DWORD dwStyle; // 窗口风格
RECT WindowRect; // 取得矩形的左上角和右下角的坐标值
HWND hWnd; // 创建窗口的句柄
HINSTANCE hInstance = GetModuleHandle(NULL);
wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; // 移动时重画,并为窗口取得DC
wndclass.lpfnWndProc = (WNDPROC) WndProc; // WndProc处理消息
wndclass.cbClsExtra = 0; // 无额外窗口数据
wndclass.cbWndExtra = 0; // 无额外窗口数据
wndclass.hInstance = hInstance; // 设置实例
wndclass.hIcon = LoadIcon(NULL, IDI_WINLOGO); // 装入缺省图标
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); // 装入鼠标指针
wndclass.hbrBackground = NULL; // GL不需要背景
wndclass.lpszMenuName = NULL; // 不需要菜单
wndclass.lpszClassName = "OpenG"; // 设定类名字
if (!RegisterClass(&wndclass)) // 尝试注册窗口类
{
MessageBox(NULL,"注册窗口失败","错误",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 退出并返回FALSE
}
s_bFullScreen = bFullScreenFlag; // 设置全局全屏标志
if (s_bFullScreen) // 要尝试全屏模式吗?
{
DEVMODE dmScr; // 设备模式
memset(&dmScr,0,sizeof(dmScr)); // 确保内存分配
dmScr.dmSize = sizeof(dmScr); // Devmode 结构的大小
dmScr.dmPelsWidth = nWidth; // 屏幕宽
dmScr.dmPelsHeight= nHeight; // 屏幕高
dmScr.dmBitsPerPel= nBits; // 色彩深度
dmScr.dmDisplayFrequency = 75; // 刷屏速度
dmScr.dmFields = DM_BITSPERPEL|DM_PELSWIDTH|DM_PELSHEIGHT|DM_DISPLAYFREQUENCY;
// 尝试设置显示模式并返回结果,注:CDC_FULLSCREEN移去了状态条
if (ChangeDisplaySettings(&dmScr, CDS_FULLSCREEN) != DISP_CHANGE_SUCCESSFUL)
{
// 若模式失败,提供两个选项:退出或在窗口内运行
if (MessageBox(NULL,"全屏模式在当前显卡上设置失败!\n使用窗口模式?",
lpszTitle, MB_YESNO|MB_ICONEXCLAMATION)==IDYES)
{
//如果用户选择窗口模式,变量fullscreen 的值变为FALSE,程序继续运行
s_bFullScreen = FALSE; // 选择窗口模式
}
else
{
//如果用户选择退出,弹出消息窗口告知用户程序将结束。
//并返回FALSE告诉程序窗口未能成功创建。程序退出。
MessageBox(NULL,"程序将被关闭","错误",MB_OK|MB_ICONSTOP);
return FALSE; // 退出并返回FALSE
}
}
}
if (s_bFullScreen)
{
dwExStyle=WS_EX_APPWINDOW; // Window 扩展风格
dwStyle=WS_POPUP; // Window 窗口风格
ShowCursor(FALSE); // 隐藏鼠标
}
else
{
dwExStyle = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE; // 扩展窗体风格
dwStyle = WS_OVERLAPPEDWINDOW; // 窗体风格
}
WindowRect.left=(long)0; // 将Left 设为0
WindowRect.right=(long)nWidth; // 将Right 设为要求的宽度
WindowRect.top=(long)0; // 将Top 设为0
WindowRect.bottom=(long)nHeight; // 将Bottom 设为要求的高度
AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle); // 调整窗口达到真正要求的大小
// 创建窗口
if (!(hWnd=CreateWindowEx( dwExStyle, // 扩展窗体风格
"OpenG", // 类名字
lpszTitle, // 窗口标题
dwStyle | // 必须的窗体风格属性
WS_CLIPSIBLINGS | // 必须的窗体风格属性
WS_CLIPCHILDREN, // 必须的窗体风格属性
CW_USEDEFAULT, CW_USEDEFAULT, // 窗口位置
WindowRect.right-WindowRect.left, // 计算调整好的窗口宽度
WindowRect.bottom-WindowRect.top, // 计算调整好的窗口高度
NULL, // 无父窗口
NULL, // 无菜单
hInstance, // 实例
NULL))) // 不向WM_CREATE传递任何东东
{// 如果创建失败
KillGLWindow();
MessageBox(NULL,"窗口创建错误","错误",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回FALSE
}
s_hInstance = hInstance;
s_hWnd = hWnd;
ShowWindow(hWnd,SW_SHOW); // 显示窗口
SetForegroundWindow(hWnd); // 略略提高优先级
SetFocus(hWnd); // 设置键盘的焦点至此窗口
UpdateWindow(hWnd);
s_pOpenGLDC = new COpenGLDC;
assert(s_pOpenGLDC != NULL);
s_pOpenGLDC->GLSetupRC(hWnd);
s_pOpenGLDC->GLResize(nWidth, nHeight);
if (!s_pOpenGLDC->GLInit()) // 初始化新建的GL窗口
{
KillGLWindow(); // 重置显示区
MessageBox(NULL, "调用OpenGLDC初始化函数失败!", "错误", MB_OK | MB_ICONEXCLAMATION);
return FALSE;
}
return TRUE;
}
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
MSG msg;
BOOL done = FALSE; // 用来退出循环的BOOL变量
// 提示用户选择运行模式
// if (MessageBox(NULL, "你想在全屏模式下运行么?", "设置全屏模式", MB_YESNO | MB_ICONQUESTION) == IDYES)
// s_bFullScreen = TRUE;
// else
// s_bFullScreen = FALSE;
// 创建OpenGL窗口
if (!CreateGLWindow(s_pszTitle, 640, 480, 32, s_bFullScreen))
return -1; // 失败退出
while (!done)
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) // 有消息在等待吗?
{
if (msg.message == WM_QUIT) // 收到退出消息?
done = TRUE; // 是,则done = TRUE
else // 不是,处理窗口消息
{
TranslateMessage(&msg); // 翻译消息
DispatchMessage(&msg); // 发送消息
}
}
else
{
// 绘制场景。监视ESC键和来自DrawGLScene()的退出消息
if (s_bActive)
{
if (s_keys[VK_ESCAPE])
done = TRUE;
else if (s_pOpenGLDC != NULL)
{
s_pOpenGLDC->GLDrawScene();
s_pOpenGLDC->ProcessKeys(s_keys);
}
}
if (s_keys[VK_F1])
{
s_keys[VK_F1] = false;
KillGLWindow();
s_bFullScreen = !s_bFullScreen;
// 重建OpenGL窗口
if (!CreateGLWindow(s_pszTitle, 640, 480, 32, s_bFullScreen))
return -1; // 失败退出
}
}
}
// 关闭程序
KillGLWindow(); // 销毁窗口
return msg.wParam; // 退出程序
}
自己编写一个小的OpenGL程序,附加在mesa工程里调试,开始进入:
// OpenGLDC.cpp文件,从第59行起
// 本代码块之前会调用ChoosePixelFormat/SetPixelFormat但不会运行mesa里的对应发布函数
/*59*/ if(!(m_hRC = wglCreateContext(m_hDC))) // 获取渲染描述句柄
{
MessageBox(NULL, "不能创建OpenGL渲染描述表", "错误",
MB_OK|MB_ICONEXCLAMATION);
return false;
}
if(!wglMakeCurrent(m_hDC, m_hRC)) // 尝试激活着色描述表
{
MessageBox(NULL, "不能激活当前的OpenGL渲染描述表", "错误",
MB_OK|MB_ICONEXCLAMATION);
return false;
/*70*/ }
经第59行处的wglCreateContext函数,进入wgl.c里第179行位置的wglCreateContext,此即mesa工程的wglCreateContext发布函数,该处代码如下所示:

wglCreateContext// wgl.c 从第179行到203行
WINGDIAPI HGLRC GLAPIENTRY wglCreateContext(HDC hdc)
{
int i = 0;
if (!ctx_count) {
for(i=0;i<MESAWGL_CTX_MAX_COUNT;i++) {
wgl_ctx[i].ctx = NULL;
}
}
for( i = 0; i < MESAWGL_CTX_MAX_COUNT; i++ ) {
if ( wgl_ctx[i].ctx == NULL ) {
wgl_ctx[i].ctx =
WMesaCreateContext(hdc, NULL, (GLboolean)GL_TRUE,
(GLboolean) (pfd[curPFD-1].doubleBuffered ?
GL_TRUE : GL_FALSE),
(GLboolean)(pfd[curPFD-1].pfd.cAlphaBits ?
GL_TRUE : GL_FALSE) );
if (wgl_ctx[i].ctx == NULL)
break;
ctx_count++;
return ((HGLRC)wgl_ctx[i].ctx);
}
}
SetLastError(0);
return(NULL);
}
在wgl.c文件声明有wgl_ctx数组(限定大小MESAWGL_CTX_MAX_COUNT,当前取值20),ctx_count存储该数组被初始化的数量。
第182行处判定若ctx_count大于0,则第一次初始化wgl_ctx数组所有元素全部为NULL;
第187处的for循环,目的在于寻找到一个空的wgl_ctx数组元素存储即将创建的WMesaContext结构地址,只要创建一个,即增加ctx_count计数1,并返回所创建的结构地址(即HGLRC句柄);
第201处是不应该被执行的,当已创建的WMesaContext数量大于wgl_ctx数组宽度时,就会到达此处。所以不能创建超过20个HGLRC句柄,就本工程而言。
WMesaCreateContext函数在wmesa.c第1418行起始,该函数代码如下所示;

WMesaCreateContext// wmesa.c 从第1418行至1531行
WMesaContext WMesaCreateContext(HDC hDC,
HPALETTE* Pal,
GLboolean rgb_flag,
GLboolean db_flag,
GLboolean alpha_flag)
{
WMesaContext c;
struct dd_function_table functions;
GLint red_bits, green_bits, blue_bits, alpha_bits;
GLcontext *ctx;
GLvisual *visual;
(void) Pal;
/* Indexed mode not supported */
if (!rgb_flag)
return NULL;
/* Allocate wmesa context */
c = CALLOC_STRUCT(wmesa_context);
if (!c)
return NULL;
#if 0
/* I do not understand this contributed code */
/* Support memory and device contexts */
if(WindowFromDC(hDC) != NULL) {
c->hDC = GetDC(WindowFromDC(hDC)); /* huh ???? */
}
else {
c->hDC = hDC;
}
#else
c->hDC = hDC;
#endif
/* Get data for visual */
/* Dealing with this is actually a bit of overkill because Mesa will end
* up treating all color component size requests less than 8 by using
* a single byte per channel. In addition, the interface to the span
* routines passes colors as an entire byte per channel anyway, so there
* is nothing to be saved by telling the visual to be 16 bits if the device
* is 16 bits. That is, Mesa is going to compute colors down to 8 bits per
* channel anyway.
* But we go through the motions here anyway.
*/
switch (GetDeviceCaps(c->hDC, BITSPIXEL)) {
case 16:
red_bits = green_bits = blue_bits = 5;
alpha_bits = 0;
break;
default:
red_bits = green_bits = blue_bits = 8;
alpha_bits = 8;
break;
}
/* Create visual based on flags */
visual = _mesa_create_visual(rgb_flag,
db_flag, /* db_flag */
GL_FALSE, /* stereo */
red_bits, green_bits, blue_bits, /* color RGB */
alpha_flag ? alpha_bits : 0, /* color A */
0, /* index bits */
DEFAULT_SOFTWARE_DEPTH_BITS, /* depth_bits */
8, /* stencil_bits */
16,16,16, /* accum RGB */
alpha_flag ? 16 : 0, /* accum A */
1); /* num samples */
if (!visual) {
_mesa_free(c);
return NULL;
}
/* Set up driver functions */
_mesa_init_driver_functions(&functions);
functions.GetString = wmesa_get_string;
functions.UpdateState = wmesa_update_state;
functions.GetBufferSize = wmesa_get_buffer_size;
functions.Flush = wmesa_flush;
functions.Clear = clear;
functions.ClearIndex = clear_index;
functions.ClearColor = clear_color;
functions.ResizeBuffers = wmesa_resize_buffers;
functions.Viewport = wmesa_viewport;
/* initialize the Mesa context data */
ctx = &c->gl_ctx;
_mesa_initialize_context(ctx, visual, NULL, &functions, (void *)c);
/* visual no longer needed - it was copied by _mesa_initialize_context() */
_mesa_destroy_visual(visual);
_mesa_enable_sw_extensions(ctx);
_mesa_enable_1_3_extensions(ctx);
_mesa_enable_1_4_extensions(ctx);
_mesa_enable_1_5_extensions(ctx);
_mesa_enable_2_0_extensions(ctx);
_mesa_enable_2_1_extensions(ctx);
/* Initialize the software rasterizer and helper modules. */
if (!_swrast_CreateContext(ctx) ||
!_vbo_CreateContext(ctx) ||
!_tnl_CreateContext(ctx) ||
!_swsetup_CreateContext(ctx)) {
_mesa_free_context_data(ctx);
_mesa_free(c);
return NULL;
}
_swsetup_Wakeup(ctx);
TNL_CONTEXT(ctx)->Driver.RunPipeline = _tnl_run_pipeline;
return c;
}
第1493行的_mesa_init_driver_functions(&functions)初始化dd_function_table结构,即设置结构的函数指针地址,该结构定义在dd.h头文件第68行起,Device driver functions table. (设备驱动函数表),这些里的绝大多数函数直接与OpenGL状态指令对应。
第1506行的_mesa_initialize_context函数初始化一个GLcontext结构,该结构为mesa的三大核心结构之一,成员很多。参见后文的解释。
第1519行有4个内部模块在做创建初始化:_swrast、_vbo、_tnl、_swsetup。
第1528行处设置_tnl管道函数,它引导了后来的对顶点变换、窗口裁剪、光照纹理计算、直至最终的渲染。
暂时停止对这些中途被呼叫内部函数的分析,来看wglMakeCurrent函数会做什么,wgl.c第234行起是工程对应的wglMakeCurrent发布函数。

wglMakeCurrent// wgl.c文件 第234行至254行
WINGDIAPI BOOL GLAPIENTRY wglMakeCurrent(HDC hdc, HGLRC hglrc)
{
int i;
CurrentHDC = hdc;
if (!hdc || !hglrc) {
WMesaMakeCurrent(NULL, NULL);
ctx_current = -1;
return TRUE;
}
for ( i = 0; i < MESAWGL_CTX_MAX_COUNT; i++ ) {
if ( wgl_ctx[i].ctx == (WMesaContext) hglrc ) {
WMesaMakeCurrent( (WMesaContext) hglrc, hdc );
ctx_current = i;
return TRUE;
}
}
return FALSE;
}
第238行保存了当前设备DC句柄;
第240行检查是否是要取消hDC与hglrc之间的关联,如果是,则以NULL为参数调用WMesaMakeCurrent;
第246行起,for循环检查输入参数hglrc是否有效,即必须是先前创建时存储过的变量地址,如果能够找到,则调用WMesaMakeCurrent关联hglrc与hdc设备DC;
ctx_current显然是指向当前有效的hglrc对应的wgl_ctx数组处的序号。
之后,程序转入到wmesa.c里的WMesaMakeCurrent函数,源代码为:

WMesaMakeCurrent// wmesa.c 第1592行起至1648行
void WMesaMakeCurrent(WMesaContext c, HDC hdc)
{
WMesaFramebuffer pwfb;
{
/* return if already current */
GET_CURRENT_CONTEXT(ctx);
WMesaContext pwc = wmesa_context(ctx);
if (pwc && c == pwc && pwc->hDC == hdc)
return;
}
pwfb = wmesa_lookup_framebuffer(hdc);
/* Lazy creation of framebuffers */
if (c && !pwfb && hdc) {
struct gl_renderbuffer *rb;
GLvisual *visual = &c->gl_ctx.Visual;
GLuint width, height;
get_window_size(hdc, &width, &height);
c->clearPen = CreatePen(PS_SOLID, 1, 0);
c->clearBrush = CreateSolidBrush(0);
pwfb = wmesa_new_framebuffer(hdc, visual);
/* Create back buffer if double buffered */
if (visual->doubleBufferMode == 1) {
wmCreateBackingStore(pwfb, width, height);
}
/* make render buffers */
if (visual->doubleBufferMode == 1) {
rb = wmesa_new_renderbuffer();
_mesa_add_renderbuffer(&pwfb->Base, BUFFER_BACK_LEFT, rb);
wmesa_set_renderbuffer_funcs(rb, pwfb->pixelformat, pwfb->cColorBits, 1);
}
rb = wmesa_new_renderbuffer();
_mesa_add_renderbuffer(&pwfb->Base, BUFFER_FRONT_LEFT, rb);
wmesa_set_renderbuffer_funcs(rb, pwfb->pixelformat, pwfb->cColorBits, 0);
/* Let Mesa own the Depth, Stencil, and Accum buffers */
_mesa_add_soft_renderbuffers(&pwfb->Base,
GL_FALSE, /* color */
visual->depthBits > 0,
visual->stencilBits > 0,
visual->accumRedBits > 0,
visual->alphaBits >0,
GL_FALSE);
}
if (c && pwfb)
_mesa_make_current(&c->gl_ctx, &pwfb->Base, &pwfb->Base);
else
_mesa_make_current(NULL, NULL, NULL);
}
第1596行至第1602行,显然在确认hglrc与hdc是否已经是关联的,如果是,不用做什么事直接返回即可;
第1604行查找已创建的且使用hdc的WMesaFramebuffer,若找不到则,返回NULL;
第1607处的if判断:c(hglrc)与hdc有效,即不是要删除,而pwfb为NULL的话,则执行第1608至1642的代码体;
第1644行的if判断,c(hglrc)与pwfb是否有效,若有效则调用_mesa_make_current关联,否则用NULL参数取消关联。
接前一文,来看WMesaMakeCurrent函数在调用_mesa_make_current之后所发生的事情:

_mesa_make_current// context.c 从第1310行到第1436行
GLboolean
_mesa_make_current( GLcontext *newCtx, GLframebuffer *drawBuffer,
GLframebuffer *readBuffer )
{
if (MESA_VERBOSE & VERBOSE_API)
_mesa_debug(newCtx, "_mesa_make_current()\n");
/* Check that the context's and framebuffer's visuals are compatible.
*/
if (newCtx && drawBuffer && newCtx->WinSysDrawBuffer != drawBuffer) {
if (!check_compatible(newCtx, drawBuffer)) {
_mesa_warning(newCtx,
"MakeCurrent: incompatible visuals for context and drawbuffer");
return GL_FALSE;
}
}
if (newCtx && readBuffer && newCtx->WinSysReadBuffer != readBuffer) {
if (!check_compatible(newCtx, readBuffer)) {
_mesa_warning(newCtx,
"MakeCurrent: incompatible visuals for context and readbuffer");
return GL_FALSE;
}
}
/* We used to call _glapi_check_multithread() here. Now do it in drivers */
_glapi_set_context((void *) newCtx);
ASSERT(_mesa_get_current_context() == newCtx);
if (!newCtx) {
_glapi_set_dispatch(NULL); /* none current */
}
else {
_glapi_set_dispatch(newCtx->CurrentDispatch);
if (drawBuffer && readBuffer) {
/* TODO: check if newCtx and buffer's visual match??? */
ASSERT(drawBuffer->Name == 0);
ASSERT(readBuffer->Name == 0);
_mesa_reference_framebuffer(&newCtx->WinSysDrawBuffer, drawBuffer);
_mesa_reference_framebuffer(&newCtx->WinSysReadBuffer, readBuffer);
/*
* Only set the context's Draw/ReadBuffer fields if they're NULL
* or not bound to a user-created FBO.
*/
if (!newCtx->DrawBuffer || newCtx->DrawBuffer->Name == 0) {
/* KW: merge conflict here, revisit.
*/
/* fix up the fb fields - these will end up wrong otherwise
* if the DRIdrawable changes, and everything relies on them.
* This is a bit messy (same as needed in _mesa_BindFramebufferEXT)
*/
unsigned int i;
GLenum buffers[MAX_DRAW_BUFFERS];
_mesa_reference_framebuffer(&newCtx->DrawBuffer, drawBuffer);
for(i = 0; i < newCtx->Const.MaxDrawBuffers; i++) {
buffers[i] = newCtx->Color.DrawBuffer[i];
}
_mesa_drawbuffers(newCtx, newCtx->Const.MaxDrawBuffers, buffers, NULL);
}
if (!newCtx->ReadBuffer || newCtx->ReadBuffer->Name == 0) {
_mesa_reference_framebuffer(&newCtx->ReadBuffer, readBuffer);
}
/* XXX only set this flag if we're really changing the draw/read
* framebuffer bindings.
*/
newCtx->NewState |= _NEW_BUFFERS;
#if 1
/* We want to get rid of these lines: */
#if _HAVE_FULL_GL
if (!drawBuffer->Initialized) {
initialize_framebuffer_size(newCtx, drawBuffer);
}
if (readBuffer != drawBuffer && !readBuffer->Initialized) {
initialize_framebuffer_size(newCtx, readBuffer);
}
_mesa_resizebuffers(newCtx);
#endif
#else
/* We want the drawBuffer and readBuffer to be initialized by
* the driver.
* This generally means the Width and Height match the actual
* window size and the renderbuffers (both hardware and software
* based) are allocated to match. The later can generally be
* done with a call to _mesa_resize_framebuffer().
*
* It's theoretically possible for a buffer to have zero width
* or height, but for now, assert check that the driver did what's
* expected of it.
*/
ASSERT(drawBuffer->Width > 0);
ASSERT(drawBuffer->Height > 0);
#endif
if (drawBuffer) {
_mesa_check_init_viewport(newCtx,
drawBuffer->Width, drawBuffer->Height);
}
}
if (newCtx->FirstTimeCurrent) {
check_context_limits(newCtx);
/* We can use this to help debug user's problems. Tell them to set
* the MESA_INFO env variable before running their app. Then the
* first time each context is made current we'll print some useful
* information.
*/
if (_mesa_getenv("MESA_INFO")) {
_mesa_print_info();
}
newCtx->FirstTimeCurrent = GL_FALSE;
}
}
return GL_TRUE;
}
第1335行调用glapi.c里_glapi_set_context函数设定当前的ctx,即设定工程全局变量_glapi_Context的值指向context,在wmesa_context结构成员中首成员即GLcontext,所以二者的指针地址是相同的;
第1342行调用glapi.c里_glapi_set_dispatch设定当前的函数句柄,即设定工程全局变量_glapi_Dispatch指针指向。
在glapitable.h里定义了_glapi_table结构,它包括所有的OpenGL发布函数gl*函数指针成员变量,在gl*系列发布函数代码体基本上都是获取当前_glapi_Dispatch成员地址,调用该成员所指向的函数指针地址。而这些成员的赋值是在_mesa_initialize_context函数执行时所做的。而这里调用_glapi_set_dispatch将_glapi_Dispatch与具体的对应函数地址对应起来。
若没有调用_mesa_make_current来关联二者之前,_glapi_Dispatch初始指向__glapi_noop_table,而且如果使用NULL来调用_mesa_make_current也会使得_glapi_Dispatch指向__glapi_noop_table。
文件域全局变量__glapi_noop_table是在glapi.c里通过包含glapitemp.h定义宏替换来声明定义的,该宏替换一方面定义了一组空的只警告又不做什么的gl*函数的翻版,起头是Noop*,一如它们的功能一样,然后将这些函数指针收集起来即__glapi_noop_table变量的值,显然如果用户没有在事前调用wglMakeCurrent关联到可用的hglrc的话,那么就是不应该使用gl*系列的函数的。
这里暂不关注对帧缓存区的相关操作,一时半会还关注不过来。
现在回过头来看GLcontext结构中与_glapi_table有关的成员,以及它们的初始化赋值:
// mtypes.h
/*2893*/ struct __GLcontextRec
/*2894*/ {
// ......
/*2898*/ /** \name API function pointer tables */
/*2899*/ /*@{*/
/*2900*/ struct _glapi_table *Save; /**< Display list save functions */
/*2901*/ struct _glapi_table *Exec; /**< Execute functions */
/*2902*/ struct _glapi_table *CurrentDispatch; /**< == Save or Exec !! */
/*2903*/ /*@}*/
// ......
/*3094*/ };
__GLcontextRec即GLcontext结构,成员Save和Exec均在_mesa_initialize_context时初始化,有许多gl*发布函数均在这里设定对应的函数指针地址,以下代码尝试弄清楚该函数所做的具体工作:

_mesa_initialize_context// context.c 从第843行起至933行
GLboolean
_mesa_initialize_context(GLcontext *ctx,
const GLvisual *visual,
GLcontext *share_list,
const struct dd_function_table *driverFunctions,
void *driverContext)
{
struct gl_shared_state *shared;
/*ASSERT(driverContext);*/
assert(driverFunctions->NewTextureObject);
assert(driverFunctions->FreeTexImageData);
/* misc one-time initializations */
one_time_init(ctx);
ctx->Visual = *visual;
ctx->DrawBuffer = NULL;
ctx->ReadBuffer = NULL;
ctx->WinSysDrawBuffer = NULL;
ctx->WinSysReadBuffer = NULL;
/* Plug in driver functions and context pointer here.
* This is important because when we call alloc_shared_state() below
* we'll call ctx->Driver.NewTextureObject() to create the default
* textures.
*/
ctx->Driver = *driverFunctions;
ctx->DriverCtx = driverContext;
if (share_list) {
/* share state with another context */
shared = share_list->Shared;
}
else {
/* allocate new, unshared state */
shared = _mesa_alloc_shared_state(ctx);
if (!shared)
return GL_FALSE;
}
_glthread_LOCK_MUTEX(shared->Mutex);
ctx->Shared = shared;
shared->RefCount++;
_glthread_UNLOCK_MUTEX(shared->Mutex);
if (!init_attrib_groups( ctx )) {
_mesa_free_shared_state(ctx, ctx->Shared);
return GL_FALSE;
}
/* setup the API dispatch tables */
ctx->Exec = alloc_dispatch_table();
ctx->Save = alloc_dispatch_table();
if (!ctx->Exec || !ctx->Save) {
_mesa_free_shared_state(ctx, ctx->Shared);
if (ctx->Exec)
_mesa_free(ctx->Exec);
return GL_FALSE;
}
#if FEATURE_dispatch
_mesa_init_exec_table(ctx->Exec);
#endif
ctx->CurrentDispatch = ctx->Exec;
#if FEATURE_dlist
_mesa_init_dlist_table(ctx->Save);
_mesa_install_save_vtxfmt( ctx, &ctx->ListState.ListVtxfmt );
#endif
/* Neutral tnl module stuff */
_mesa_init_exec_vtxfmt( ctx );
ctx->TnlModule.Current = NULL;
ctx->TnlModule.SwapCount = 0;
ctx->FragmentProgram._MaintainTexEnvProgram
= (_mesa_getenv("MESA_TEX_PROG") != NULL);
ctx->VertexProgram._MaintainTnlProgram
= (_mesa_getenv("MESA_TNL_PROG") != NULL);
if (ctx->VertexProgram._MaintainTnlProgram) {
/* this is required... */
ctx->FragmentProgram._MaintainTexEnvProgram = GL_TRUE;
}
#ifdef FEATURE_extra_context_init
_mesa_initialize_context_extra(ctx);
#endif
ctx->FirstTimeCurrent = GL_TRUE;
return GL_TRUE;
}
第895行,申请_glapi_table结构,并以警告函数指针初始化,赋给ctx->Exec。而第896行与此一样;
第904行,调用在api_exec.c里第151行的_mesa_init_exec_table函数设定ctx->Exec各成员指针指向对应的工程函数;
第905行,设定当前的函数分发表指向ctx->Exec;
第908行、第909行都是对ctx->Save的初始化,暂时不去解析;
第912行则是调用在vtxfmt.c里的_mesa_init_exec_vtxfmt函数设定ctx->Exec中对应GLvertexformat结构的成员函数指针初始化;
GLvertexformat结构在dd.h头文件里定义,它所包含的函数都是有可能在glBegin()/glEnd()之间使用的函数。
neutral_vtxfmt是vtxfmt.c定义宏定义包含vtxfmt_tmp.h而得到的文件域全局变量,这些函数的代码体都差不多,估计是与tnl模块的交互;
第913行与914行初始化表明vtxfmt与tnl模块之间还没有交换过成员值。
执行完前面的函数之后就进入GLResize()函数,这里的它自身的源代码如下:
// OpenGLDC.cpp 第99行至第115行
GLvoid COpenGLDC::GLResize(GLsizei nWidth, GLsizei nHeight)
{// 重置OpenGL窗口大小
if(nHeight == 0) // 防止被零除
nHeight = 1;
glViewport(0, 0, nWidth, nHeight);
glMatrixMode(GL_PROJECTION); // 选择投影矩阵
glLoadIdentity(); // 重置投影矩阵
gluPerspective(45.0f, // 透视角设置为45 度
(GLfloat)nWidth/(GLfloat)nHeight, // 窗口的宽与高比
0.6f, 100.0f); // 视野透视深度:近点.1f远点.0f
glMatrixMode(GL_MODELVIEW); // 选择模型观察矩阵
glLoadIdentity();
}
GLResize()函数第104行呼叫glViewport,这时将转移到viewport.c处第45行_mesa_Viewport函数:

_mesa_Viewport// viewport.c 从第44行至50行
void GLAPIENTRY
_mesa_Viewport(GLint x, GLint y, GLsizei width, GLsizei height)
{
GET_CURRENT_CONTEXT(ctx);
ASSERT_OUTSIDE_BEGIN_END_AND_FLUSH(ctx);
_mesa_set_viewport(ctx, x, y, width, height);
}
第47行宏替换,即取_glapi_Context的值赋给声明的GLcontext类型的ctx变量。
第48行处,则是断言检查,要求保证本函数在glBegin()/glEnd()之外使用,并且立即输出还存在缓冲区的顶点FLUSH_VERTICES;
第49行调用_mesa_set_viewport正式处理需求glViewport需求;

_mesa_set_viewportvoid
_mesa_set_viewport(GLcontext *ctx, GLint x, GLint y,
GLsizei width, GLsizei height)
{
if (MESA_VERBOSE & VERBOSE_API)
_mesa_debug(ctx, "glViewport %d %d %d %d\n", x, y, width, height);
if (width < 0 || height < 0) {
_mesa_error(ctx, GL_INVALID_VALUE,
"glViewport(%d, %d, %d, %d)", x, y, width, height);
return;
}
/* clamp width and height to the implementation dependent range */
width = MIN2(width, (GLsizei) ctx->Const.MaxViewportWidth);
height = MIN2(height, (GLsizei) ctx->Const.MaxViewportHeight);
ctx->Viewport.X = x;
ctx->Viewport.Width = width;
ctx->Viewport.Y = y;
ctx->Viewport.Height = height;
ctx->NewState |= _NEW_VIEWPORT;
#if 1
/* XXX remove this someday. Currently the DRI drivers rely on
* the WindowMap matrix being up to date in the driver's Viewport
* and DepthRange functions.
*/
_math_matrix_viewport(&ctx->Viewport._WindowMap,
ctx->Viewport.X, ctx->Viewport.Y,
ctx->Viewport.Width, ctx->Viewport.Height,
ctx->Viewport.Near, ctx->Viewport.Far,
ctx->DrawBuffer->_DepthMaxF);
#endif
if (ctx->Driver.Viewport) {
/* Many drivers will use this call to check for window size changes
* and reallocate the z/stencil/accum/etc buffers if needed.
*/
ctx->Driver.Viewport(ctx, x, y, width, height);
}
}
第79行起开始设置新的VIEWPORT参数到ctx->Viewport,并写入状态_NEW_VIEWPORT;
第90行起调用_math_matrix_viewport来计算视口矩阵,计算公式为暂记在本子上;
第101行调用ctx所指向的Driver驱动Viewport函数地址指针,即wmesa.c里的wmesa_viewport函数,具体做一些重设帧缓存区大小之类的工作吧;
_math_matrix_viewport计算视口矩阵公式:以列为矩阵即第一行元素地址 0 4 8 12
[ width / 2 0 0 width /2 + x ]
[ 0 height / 2 0 height /2 + y ]
[ 0 0 depthMax * (zFar – zNear) / 2 0 depthMax * (zFar – zNear) / 2 0 + zNear]
[ 0 0 0 0 ]
glViewport执行之后,就开始glMatrixMode(GL_PROJECTION);语句的执行了,在matrix.c第146行处的_mesa_MatrixMode:

_mesa_MatrixMode// matrix.c 从第145行至228行
void GLAPIENTRY
_mesa_MatrixMode( GLenum mode )
{
GET_CURRENT_CONTEXT(ctx);
ASSERT_OUTSIDE_BEGIN_END(ctx);
if (ctx->Transform.MatrixMode == mode && mode != GL_TEXTURE)
return;
FLUSH_VERTICES(ctx, _NEW_TRANSFORM);
switch (mode) {
case GL_MODELVIEW:
ctx->CurrentStack = &ctx->ModelviewMatrixStack;
break;
case GL_PROJECTION:
ctx->CurrentStack = &ctx->ProjectionMatrixStack;
break;
case GL_TEXTURE:
/* This error check is disabled because if we're called from
* glPopAttrib() when the active texture unit is >= MaxTextureCoordUnits
* we'll generate an unexpected error.
* From the GL_ARB_vertex_shader spec it sounds like we should instead
* do error checking in other places when we actually try to access
* texture matrices beyond MaxTextureCoordUnits.
*/
#if 0
if (ctx->Texture.CurrentUnit >= ctx->Const.MaxTextureCoordUnits) {
_mesa_error(ctx, GL_INVALID_OPERATION, "glMatrixMode(invalid tex unit %d)",
ctx->Texture.CurrentUnit);
return;
}
#endif
ASSERT(ctx->Texture.CurrentUnit < Elements(ctx->TextureMatrixStack));
ctx->CurrentStack = &ctx->TextureMatrixStack[ctx->Texture.CurrentUnit];
break;
case GL_COLOR:
ctx->CurrentStack = &ctx->ColorMatrixStack;
break;
case GL_MATRIX0_NV:
case GL_MATRIX1_NV:
case GL_MATRIX2_NV:
case GL_MATRIX3_NV:
case GL_MATRIX4_NV:
case GL_MATRIX5_NV:
case GL_MATRIX6_NV:
case GL_MATRIX7_NV:
if (ctx->Extensions.NV_vertex_program) {
ctx->CurrentStack = &ctx->ProgramMatrixStack[mode - GL_MATRIX0_NV];
}
else {
_mesa_error( ctx, GL_INVALID_ENUM, "glMatrixMode(mode)" );
return;
}
break;
case GL_MATRIX0_ARB:
case GL_MATRIX1_ARB:
case GL_MATRIX2_ARB:
case GL_MATRIX3_ARB:
case GL_MATRIX4_ARB:
case GL_MATRIX5_ARB:
case GL_MATRIX6_ARB:
case GL_MATRIX7_ARB:
if (ctx->Extensions.ARB_vertex_program ||
ctx->Extensions.ARB_fragment_program) {
const GLuint m = mode - GL_MATRIX0_ARB;
if (m > ctx->Const.MaxProgramMatrices) {
_mesa_error(ctx, GL_INVALID_ENUM,
"glMatrixMode(GL_MATRIX%d_ARB)", m);
return;
}
ctx->CurrentStack = &ctx->ProgramMatrixStack[m];
}
else {
_mesa_error( ctx, GL_INVALID_ENUM, "glMatrixMode(mode)" );
return;
}
break;
default:
_mesa_error( ctx, GL_INVALID_ENUM, "glMatrixMode(mode)" );
return;
}
ctx->Transform.MatrixMode = mode;
}
第151行处检查如果当前的模式正好匹配,而且不是纹理模式的话,即可返回,不需要重复做事;
第155行根据当前mode的设定值,设定当前所要操作的矩阵栈,当前只需要对投影矩阵栈和模型视图矩阵栈有所了解即可;
第227行设定ctx->Transform.MatrixMode表征当前的矩阵模式;
然后开始OpenGLDC.cpp里第107行的glLoadIdentity();语句执行,对应在matrix.c第319行处的_mesa_LoadIdentity()函数:

_mesa_LoadIdentity// matrix 从第318行至329行
void GLAPIENTRY
_mesa_LoadIdentity( void )
{
GET_CURRENT_CONTEXT(ctx);
ASSERT_OUTSIDE_BEGIN_END_AND_FLUSH(ctx);
if (MESA_VERBOSE & VERBOSE_API)
_mesa_debug(ctx, "glLoadIdentity()");
_math_matrix_set_identity( ctx->CurrentStack->Top );
ctx->NewState |= ctx->CurrentStack->DirtyFlag;
}
第327行调用_math_matrix_set_identity函数将当前矩阵栈顶矩阵设为Identity矩阵,它在m_matrix.c的第1142行处,采取MEMCPY内存拷贝的形式将一个Identity矩阵的数据拷贝至GLmatrix矩阵;
然后就是OpenGLDC.cpp的第109行处的gluPerspective语句了,这是属于glu工程的发布函数,在project.c第65行处的gluPerspective位置,其矩阵计算暂时记在本子里,然后使用gluMultMatrixd将矩阵乘至当前的矩阵栈顶ctx->CurrentStack->Top;
gluPerspective视图投影矩阵计算公式:
[ cos(fovy / 2) / (sin(fovy / 2) * aspect) 0 0 0]
[ 0 cos(fovy/2)/sin(fovy/2) 0 0]
[ 0 0 -(zFar + zNear) / (zFar – zNear) -(2*zFar*zNear)/(zFar - zNear)]
[ 0 0 -1 0]
然后再执行OpenGLDC.cpp的第113行处glMatrixMode(GL_MODELVIEW);它将当前矩阵栈ctx->CurrentStack指向ModelviewMatrixStack,然后接下来的glLoadIdentity会使栈顶矩阵重置为单位矩阵;
在GLResize()函数执行完之后,会进入GLInit执行用户的初始化语句,但这里暂时略过这一部分的内容。
重点来看GLDrawScene()函数的调试执行:
// OpenGLDC.cpp 从第136行到161行
GLboolean COpenGLDC::GLDrawScene(GLvoid)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清除屏幕和深度缓存
glLoadIdentity(); // 如果不添加这句,那么所画的都会由大缩小,无法看见
//
glTranslatef(-1.5f,0.0f,-6.0f); // 左移1.5 单位,并移入屏幕6.0
glBegin(GL_LINE_LOOP); // 绘制三角形
glColor3f(1.0f,0.0f,0.0f); // 设置当前色为红色
glVertex3f( 0.0f, 1.0f, 0.0f); // 上顶点
glColor3f(0.0f,1.0f,0.0f); // 设置当前色为绿色
glVertex3f(-1.0f,-1.0f, 0.0f); // 左下
glColor3f(0.0f,0.0f,1.0f); // 设置当前色为蓝色
glVertex3f( 1.0f,-1.0f, 0.0f); // 右下
glEnd(); // 三角形绘制结束
glTranslatef(3.0f,0.0f,0.0f); // 右移单位
glColor3f(0.5f,0.5f,1.0f); // 一次性将当前色设置为蓝色
glBegin(GL_QUADS); // 绘制正方形
glVertex3f(-1.0f, 1.0f, 0.0f); // 左上
glVertex3f( 1.0f, 1.0f, 0.0f); // 右上
glVertex3f( 1.0f,-1.0f, 0.0f); // 左下
glVertex3f(-1.0f,-1.0f, 0.0f); // 右下
glEnd(); // 正方形绘制结束
//
SwapBuffers(m_hDC); // 切换缓冲区,没有切换的话,窗口不会绘制什么
return true;
}
首先是glClear语句,此语句关联clear.c里第112行处的_mesa_Clear函数,它做了不少事情,但当下并非我注意的重点对象,因为我还不大明白自己要去关注什么。收集一下当下可能需要的关注方向:
(1) 在_mesa_update_state_locked里调用的_mesa_update_modelview_project计算模型视图投影矩阵;需要了解一下ctx->Transform与cull position有关的成员;后面会计算ctx->_ModelProjectMatrix,因为此时的ModelViewMatrixStack.Top为单位矩阵,所以结果就是投影矩阵;
(2) 该处也会调用update_viewport_matrix函数;
(3) 最后会调用ctx所带驱动函数地址处记录的Clear,即wmesa.c处第289行的clear函数;
再就是glLoadIdentity();语句的执行,前面已经看到过两次了,因为此时矩阵模式还时GL_MODELVIEW,所以它执行的结果就是将ctx->ModelviewMatrixStack重置为单位矩阵;
接下来就是glTranslatef(-1.5f, 0.0f, -6.0f);语句,它对应执行matrix.c里第454行处的_mesa_Translatef函数,它的重点是检查是否需要做一点别的事情,然后调用_math_matrix_translate来完成平移矩阵,因为前面为单位矩阵,而这里仅仅修改矩阵的平移项:
最后是重头戏,glBegin()/glEnd()以及在它们之间调用的函数。
glBegin()函数首先由ctx->Exec所指向的函数指针转移至neutral_Begin,neutral_Begin由宏包裹着,展开之后将是这样的语句:
static void __stdcall neutral_Begin( GLenum mode )
{
{
GLcontext *ctx = (GLcontext *) _glapi_Context;
struct gl_tnl_module * const tnl = &(ctx->TnlModule);
const int tmp_offset = 7 ;
; ; ;
if (tnl->SwapCount == 0)
ctx->Driver.BeginVertices( ctx );
tnl->Swapped[tnl->SwapCount].location = & (((_glapi_proc *)ctx->Exec)[tmp_offset]);
tnl->Swapped[tnl->SwapCount].function = (_glapi_proc)neutral_Begin;
tnl->SwapCount++;
if ( 0 )
_mesa_debug(ctx, " swapping gl" "Begin""...\n" );
((ctx->Exec)->Begin = tnl->Current->Begin);
};
(*((_glapi_Dispatch)->Begin)) ( mode );
}
执行过程是这样的,如果是首次被调用,则会通过ctx->Driver来调用BeginVertices(即vbo_exec_api.c里第750行处的vbo_exec_BeginVertices),然后在tnl模块的Swapped处记录交交换的函数地址,从而将vbo_exec_Begin换至当前ctx->Exec处的对应glBegin函数指针位置处,然后调用vbo_exec_Begin;这就是neutral_Begin的执行过程。
vbo_exec_BeginVertices函数的执行过程,暂略。
vbo_exec_Begin函数的执行过程,在vbo_exec_api.c第492行处:

vbo_exec_Begin// vbo_exec_api.c 从第492行至532行
static void GLAPIENTRY vbo_exec_Begin( GLenum mode )
{
GET_CURRENT_CONTEXT( ctx );
if (ctx->Driver.CurrentExecPrimitive == PRIM_OUTSIDE_BEGIN_END) {
struct vbo_exec_context *exec = &vbo_context(ctx)->exec;
int i;
if (ctx->NewState) {
_mesa_update_state( ctx );
CALL_Begin(ctx->Exec, (mode));
return;
}
if (!_mesa_valid_to_render(ctx, "glBegin")) {
return;
}
/* Heuristic: attempt to isolate attributes occuring outside
* begin/end pairs.
*/
if (exec->vtx.vertex_size && !exec->vtx.attrsz[0])
vbo_exec_FlushVertices_internal( ctx, GL_FALSE );
i = exec->vtx.prim_count++;
exec->vtx.prim[i].mode = mode;
exec->vtx.prim[i].begin = 1;
exec->vtx.prim[i].end = 0;
exec->vtx.prim[i].indexed = 0;
exec->vtx.prim[i].weak = 0;
exec->vtx.prim[i].pad = 0;
exec->vtx.prim[i].start = exec->vtx.vert_count;
exec->vtx.prim[i].count = 0;
ctx->Driver.CurrentExecPrimitive = mode;
}
else
_mesa_error( ctx, GL_INVALID_OPERATION, "glBegin" );
}
第496行处检查是否在glBegin()/glEnd()之外调用,如果不是,则调用_mesa_error函数非法操作;
第500行处检查是否有新的状态变化,发现有ctx->NewState,则调用_mesa_update_state来更新状态,这个函数就是先前glClear也会调用来更新状态的那个函数,然后再调用一次vbo_exec_Begin之后退出程序;
实际上更新状态之后的调用与没有状态时的调用除了前者会调用状态更新函数之外,其余部分没有区别,从第514行起到第527行处的代码语句都要执行。
这就是glBegin()的执行过程。
glColor3f(1.0f, 0.0f, 0.0f);语句的对应执行流程,与glBegin()一样经过ctx->Exec指向对应的neutral_Color3f函数,该函数被包裹在宏的声明里,展开之后的语句是这样的:

neutral_Color3f// vtxfmt.c 包含vtxfmt_tmp.h展开宏而产生
static void __stdcall neutral_Color3f( GLfloat r, GLfloat g, GLfloat b )
{
{
GLcontext *ctx = (GLcontext *) _glapi_Context;
struct gl_tnl_module * const tnl = &(ctx->TnlModule);
const int tmp_offset = 13 ;
; ; ;
if (tnl->SwapCount == 0)
ctx->Driver.BeginVertices( ctx );
tnl->Swapped[tnl->SwapCount].location = & (((_glapi_proc *)ctx->Exec)[tmp_offset]);
tnl->Swapped[tnl->SwapCount].function = (_glapi_proc)neutral_Color3f;
tnl->SwapCount++;
if ( 0 )
_mesa_debug(ctx, " swapping gl" "Color3f""...\n" );
((ctx->Exec)->Color3f = tnl->Current->Color3f);
};
(*((_glapi_Dispatch)->Color3f)) ( r, g, b );
}
样式与neutral_Begin差不多,毕竟是同一批宏展开。这里不会再执行ctx->Driver.BeginVertices,记录使用过的函数地址之后,转而执行vbo_Color3f。

vbo_Color3f// vbo_exec_api.c 通过包含vbo_attrib_tmp.h头文件产生
static void __stdcall vbo_Color3f( GLfloat x, GLfloat y, GLfloat z )
{
GLcontext *ctx = (GLcontext *) _glapi_Context;
do {
struct vbo_exec_context *exec = &vbo_context(ctx)->exec;
if (exec->vtx.active_sz[VBO_ATTRIB_COLOR0] != 3)
vbo_exec_fixup_vertex(ctx, VBO_ATTRIB_COLOR0, 3);
{
GLfloat *dest = exec->vtx.attrptr[VBO_ATTRIB_COLOR0];
if (3>0) dest[0] = x; if (3>1) dest[1] = y;
if (3>2) dest[2] = z; if (3>3) dest[3] = 1;
}
if ((VBO_ATTRIB_COLOR0) == 0)
{
GLuint i;
for (i = 0; i < exec->vtx.vertex_size; i++)
exec->vtx.buffer_ptr[i] = exec->vtx.vertex[i];
exec->vtx.buffer_ptr += exec->vtx.vertex_size;
exec->ctx->Driver.NeedFlush |= 0x1;
if (++exec->vtx.vert_count >= exec->vtx.max_vert)
vbo_exec_vtx_wrap( exec );
}
} while (0);
}
第4行获取vbo_exec_context指针;
第5行比对exec->vtx.active_sz[VBO_ATTRIB_COLOR0]是否等于3,若不等于3,则要修正顶点数据,在第一次调用这个函数时,该值为0,故会执行该函数;
而glVertex3f(0.0f, 1.0f, 0.0f);语句呢,也基本与glColor3f相似,其中neutral_Vertex3f对应的源代码如下:

neutral_Vertex3fstatic void __stdcall neutral_Vertex3f( GLfloat x, GLfloat y, GLfloat z )
{
{
GLcontext *ctx = (GLcontext *) _glapi_Context;
struct gl_tnl_module * const tnl = &(ctx->TnlModule);
const int tmp_offset = 136 ;
; ; ;
if (tnl->SwapCount == 0)
ctx->Driver.BeginVertices( ctx );
tnl->Swapped[tnl->SwapCount].location = & (((_glapi_proc *)ctx->Exec)[tmp_offset]);
tnl->Swapped[tnl->SwapCount].function = (_glapi_proc)neutral_Vertex3f;
tnl->SwapCount++;
if ( 0 )
_mesa_debug(ctx, " swapping gl" "Vertex3f""...\n" );
((ctx->Exec)->Vertex3f = tnl->Current->Vertex3f);
};
(*((_glapi_Dispatch)->Vertex3f)) ( x, y, z );
}
该函数执行后,会转至vbo_Vertex3f函数执行:

vbo_Vertex3fstatic void __stdcall vbo_Vertex3f( GLfloat x, GLfloat y, GLfloat z )
{
GLcontext *ctx = (GLcontext *) _glapi_Context;
do {
struct vbo_exec_context *exec = &vbo_context(ctx)->exec;
if (exec->vtx.active_sz[VBO_ATTRIB_POS] != 3)
vbo_exec_fixup_vertex(ctx, VBO_ATTRIB_POS, 3);
{
GLfloat *dest = exec->vtx.attrptr[VBO_ATTRIB_POS];
if (3>0) dest[0] = x; if (3>1) dest[1] = y;
if (3>2) dest[2] = z; if (3>3) dest[3] = 1;
}
if ((VBO_ATTRIB_POS) == 0)
{
GLuint i;
for (i = 0; i < exec->vtx.vertex_size; i++)
exec->vtx.buffer_ptr[i] = exec->vtx.vertex[i];
exec->vtx.buffer_ptr += exec->vtx.vertex_size;
exec->ctx->Driver.NeedFlush |= 0x1;
if (++exec->vtx.vert_count >= exec->vtx.max_vert)
vbo_exec_vtx_wrap( exec );
}
} while (0);
}
其执行与前面vbo_Color3f相似,但这里我一直弄不大清楚到底数据存储到哪里去了呢?而程序在后面又是怎样找到这些存储的数据呢?
再一次重复执行glColor3f和glVertex3f时,因为前面已经登记交换函数地址,所以会直接转到vbo_Color3f和vbo_Vertex3f执行,与前类似。
最后是glEnd();语句的执行,与前类似,首先是neutral_End:

neutral_Endstatic void __stdcall neutral_End( void )
{
{
GLcontext *ctx = (GLcontext *) _glapi_Context;
struct gl_tnl_module * const tnl = &(ctx->TnlModule);
const int tmp_offset = 43 ;
; ; ;
if (tnl->SwapCount == 0)
ctx->Driver.BeginVertices( ctx );
tnl->Swapped[tnl->SwapCount].location = & (((_glapi_proc *)ctx->Exec)[tmp_offset]);
tnl->Swapped[tnl->SwapCount].function = (_glapi_proc)neutral_End;
tnl->SwapCount++;
if ( 0 )
_mesa_debug(ctx, " swapping gl" "End""...\n" );
((ctx->Exec)->End = tnl->Current->End);
};
(*((_glapi_Dispatch)->End)) ();
}
不像neutral_Begin()在之前还要执行ctx->Driver.BeginVertices,neutral_End()首先会登记交换函数地址,然后转移至vbo_exec_End执行。

vbo_exec_Endstatic void GLAPIENTRY vbo_exec_End( void )
{
GET_CURRENT_CONTEXT( ctx );
if (ctx->Driver.CurrentExecPrimitive != PRIM_OUTSIDE_BEGIN_END) {
struct vbo_exec_context *exec = &vbo_context(ctx)->exec;
int idx = exec->vtx.vert_count;
int i = exec->vtx.prim_count - 1;
exec->vtx.prim[i].end = 1;
exec->vtx.prim[i].count = idx - exec->vtx.prim[i].start;
ctx->Driver.CurrentExecPrimitive = PRIM_OUTSIDE_BEGIN_END;
if (exec->vtx.prim_count == VBO_MAX_PRIM)
vbo_exec_vtx_flush( exec, GL_FALSE );
}
else
_mesa_error( ctx, GL_INVALID_OPERATION, "glEnd" );
}
vbo_exec_End只会记录消息,除非数据满了,否则不会刷新数据。
从当前的GLDrawScene()执行过程来看,第一个glBegin()/glEnd()之后不会立即刷新,但由于接下来的glTranslatef(3.0f, 0.0f, 0.0f);因为模型视图矩阵的变更会触导数据刷新。而来具体的过程来看,都是通过vbo_exec_vtx_flush函数来完成的。而这将是后续分析的起点。
vbo_exec_vtx_flush定义在vbo_exec_draw.c第353行处,具体代码如下:

vbo_exec_vtx_flush// vbo_exec_draw.c 从第349行至417行
/**
* Execute the buffer and save copied verts.
*/
void
vbo_exec_vtx_flush( struct vbo_exec_context *exec, GLboolean unmap )
{
if (0)
vbo_exec_debug_verts( exec );
if (exec->vtx.prim_count &&
exec->vtx.vert_count) {
exec->vtx.copied.nr = vbo_copy_vertices( exec );
if (exec->vtx.copied.nr != exec->vtx.vert_count) {
GLcontext *ctx = exec->ctx;
/* Before the update_state() as this may raise _NEW_ARRAY
* from _mesa_set_varying_vp_inputs().
*/
vbo_exec_bind_arrays( ctx );
if (ctx->NewState)
_mesa_update_state( ctx );
if (_mesa_is_bufferobj(exec->vtx.bufferobj)) {
vbo_exec_vtx_unmap( exec );
}
if (0)
_mesa_printf("%s %d %d\n", __FUNCTION__, exec->vtx.prim_count,
exec->vtx.vert_count);
vbo_context(ctx)->draw_prims( ctx,
exec->vtx.inputs,
exec->vtx.prim,
exec->vtx.prim_count,
NULL,
GL_TRUE,
0,
exec->vtx.vert_count - 1);
/* If using a real VBO, get new storage -- unless asked not to.
*/
if (_mesa_is_bufferobj(exec->vtx.bufferobj) && !unmap) {
vbo_exec_vtx_map( exec );
}
}
}
/* May have to unmap explicitly if we didn't draw:
*/
if (unmap &&
_mesa_is_bufferobj(exec->vtx.bufferobj) &&
exec->vtx.buffer_map) {
vbo_exec_vtx_unmap( exec );
}
if (unmap || exec->vtx.vertex_size == 0)
exec->vtx.max_vert = 0;
else
exec->vtx.max_vert = ((VBO_VERT_BUFFER_SIZE - exec->vtx.buffer_used) /
(exec->vtx.vertex_size * sizeof(GLfloat)));
exec->vtx.buffer_ptr = exec->vtx.buffer_map;
exec->vtx.prim_count = 0;
exec->vtx.vert_count = 0;
}
第382行是开始准备进入管道绘制处的语句,具体对应t_draw.c第363行处的_tnl_vbo_draw_prims,然后到达_tnl_draw_prims函数;

_tnl_draw_prims// t_draw.c 从第378行到450行
/* This is the main entrypoint into the slimmed-down software tnl
* module. In a regular swtnl driver, this can be plugged straight
* into the vbo->Driver.DrawPrims() callback.
*/
void _tnl_draw_prims( GLcontext *ctx,
const struct gl_client_array *arrays[],
const struct _mesa_prim *prim,
GLuint nr_prims,
const struct _mesa_index_buffer *ib,
GLuint min_index,
GLuint max_index)
{
TNLcontext *tnl = TNL_CONTEXT(ctx);
const GLuint TEST_SPLIT = 0;
const GLint max = TEST_SPLIT ? 8 : tnl->vb.Size - MAX_CLIPPED_VERTICES;
if (0)
{
GLuint i;
_mesa_printf("%s %d..%d\n", __FUNCTION__, min_index, max_index);
for (i = 0; i < nr_prims; i++)
_mesa_printf("prim %d: %s start %d count %d\n", i,
_mesa_lookup_enum_by_nr(prim[i].mode),
prim[i].start,
prim[i].count);
}
if (min_index) {
/* We always translate away calls with min_index != 0.
*/
vbo_rebase_prims( ctx, arrays, prim, nr_prims, ib,
min_index, max_index,
_tnl_vbo_draw_prims );
return;
}
else if (max_index > max) {
/* The software TNL pipeline has a fixed amount of storage for
* vertices and it is necessary to split incoming drawing commands
* if they exceed that limit.
*/
struct split_limits limits;
limits.max_verts = max;
limits.max_vb_size = ~0;
limits.max_indices = ~0;
/* This will split the buffers one way or another and
* recursively call back into this function.
*/
vbo_split_prims( ctx, arrays, prim, nr_prims, ib,
0, max_index,
_tnl_vbo_draw_prims,
&limits );
}
else {
/* May need to map a vertex buffer object for every attribute plus
* one for the index buffer.
*/
struct gl_buffer_object *bo[VERT_ATTRIB_MAX + 1];
GLuint nr_bo = 0;
/* Binding inputs may imply mapping some vertex buffer objects.
* They will need to be unmapped below.
*/
bind_inputs(ctx, arrays, max_index+1, bo, &nr_bo);
bind_indices(ctx, ib, bo, &nr_bo);
bind_prims(ctx, prim, nr_prims );
TNL_CONTEXT(ctx)->Driver.RunPipeline(ctx);
unmap_vbos(ctx, bo, nr_bo);
free_space(ctx);
}
}
实际执行时只会执行第431行起的else {}代码块,而第445行处的TNL_CONTEXT(ctx)->Driver.RunPipeline(ctx);是正式的进入管道的入口语句,它对应在t_pipeline.c第121行处的_tnl_run_pipeline。

_tnl_run_pipeline// t_pipeline.c 从第121行至162行
void _tnl_run_pipeline( GLcontext *ctx )
{
TNLcontext *tnl = TNL_CONTEXT(ctx);
unsigned short __tmp;
GLuint i;
if (!tnl->vb.Count)
return;
/* Check for changed input sizes or change in stride to/from zero
* (ie const or non-const).
*/
if (check_input_changes( ctx ) || tnl->pipeline.new_state) {
if (ctx->VertexProgram._MaintainTnlProgram)
_tnl_UpdateFixedFunctionProgram( ctx );
for (i = 0; i < tnl->pipeline.nr_stages ; i++) {
struct tnl_pipeline_stage *s = &tnl->pipeline.stages[i];
if (s->validate)
s->validate( ctx, s );
}
tnl->pipeline.new_state = 0;
tnl->pipeline.input_changes = 0;
/* Pipeline can only change its output in response to either a
* statechange or an input size/stride change. No other changes
* are allowed.
*/
if (check_output_changes( ctx ))
_tnl_notify_pipeline_output_change( ctx );
}
START_FAST_MATH(__tmp);
for (i = 0; i < tnl->pipeline.nr_stages ; i++) {
struct tnl_pipeline_stage *s = &tnl->pipeline.stages[i];
if (!s->run( ctx, s ))
break;
}
END_FAST_MATH(__tmp);
}
其中对应的管道结构变量是:
const struct tnl_pipeline_stage *_tnl_default_pipeline[] = {
&_tnl_vertex_transform_stage,
&_tnl_normal_transform_stage,
&_tnl_lighting_stage,
&_tnl_texgen_stage,
&_tnl_texture_transform_stage,
&_tnl_point_attenuation_stage,
&_tnl_vertex_program_stage,
&_tnl_fog_coordinate_stage,
&_tnl_render_stage,
NULL
};
由上可见首先计算顶点,然后变换,再光照等等,最后运行渲染。
好久没有写博客,同时也放下对mesa源码的阅读很久了。这一篇会是我最后的一篇,有不尽到之处也会放下了,毕竟OpenGL不是我的强项,后面可能没有这么多时间来关注了。
这篇文章有点长,而我也不知道自己究竟说清楚了没有,放在这里,一方面是作为自己阅读的一个总结,另一方面也期望它能够对人有所助益。
0 数据结构的预备
1) __GLcontextRect
__GLcontextRect 也被定义为 GLcontext
Mesa rendering context,在这个数据结构里几乎包括所有的OpenGL状态
struct gl_transform_attrib Transform; 对应Transformation属性
struct gl_viewport_attrib Viewport; 对应Viewprt属性,glViewport()负责初始化它
struct gl_matrix_stack ModelviewMatrixStack;
struct gl_matrix_stack ProjectionMatrixStack;
struct gl_matrix_stack ColorMatrixStack;
struct gl_matrix_stack TextureMatrixStack[MAX_TEXTURE_UNITS];
struct gl_matrix_stack ProgramMatrixStack[MAX_PROGRAM_MATRICES];
struct gl_matrix_stack *CurrentStack; // 指向上面矩阵堆栈中的一个
GLmatrix _ModelProjectMatrix; // 联合模型视图矩阵与投影矩阵
2) wmesa_context 与 wmesa_framebuffer
wmesa_context // The Windows Mesa rendering context, derived from GLcontext;
它存储与Windows窗口有关的数据成员,除GLcontext之外,比如HDC, 清除颜色,清除画笔,清除画刷等等
wmeas_framebuffer // Windows framebuffer, derived from gl_framebuffer
除gl_framebuffer之外,窗口句柄,像素格式,颜色位数,位图,以及像素指针,下一wmesa_framebuffer*next指针等等
真正地像素颜色等数据会存储在这里,因而也就比较与平台相关,而GLcontext则是能够比较独立于平台的,如何做到的呢,依靠这里的数据结构了吧。
GLvisual 亦即 __GLcontextModesRes 看起来它是支持很多渲染模式的,比如RGB,累积缓存,深度缓存,各种GLX, ARB扩展等等,这个暂时不管它
dd_function_table Mesa内核使用这些函数指针以呼叫设备驱动,许多函数直接对应OpenGL状态命令,Mesa内核会在完成错误检查之后再调用它们,所以驱动不用再检查
顶点transformation/clipping/lighting存放入 T&L模块
Rasterization光栅化函数存放入 swrast模块
3) glapi_table
gdi项目下的 mesa.def 对应 OpenGL.dll 输出的API函数列表,该列表与 glapi_table 很能对应上
_glapi_set_dispatch() 是对应的设置捆绑接应函数,比如 对上 __glapi_noop_table 可表征空操作
_mesa_init_exec_table() 是对应的初始化 glBegin()/glEnd() 之间的函数
mesa里实现gl*的API函数是比较独特的,通过宏替换的形式来进行。未初始化的时候,对应的都是空的函数体,在wglMakeCurrent()里才会被初始化。
1 前面的测试工程,调试后可发现其基本的执行流程如下:
(1) 窗口WM_CREATE之后调用 GLSetupRC(),此时也可以进行那些属于一次性的初始化GLInit()
ChoosePixelFormat()
SetPixelFormat()
wglCreateContext
wglMakeCurrent()
(2) 窗口WM_SIZE消息处调用 GLResize()
glViewport()
glMatrixMode()
glLoadIdentity()
gluPerspective()
glMatrixMode()
glLoadIdentity()
(3) 在消息循环之外调用 GLDrawScene() 绘制
glClear()
glLoadIdentity()
glTranslatef()
glBegin()
glColor3f()
glVertex3f()
glEnd()
SwapBuffers()
(4) 退出窗口之前销毁该销毁的
略过
2 GLSetupRC() 处的执行流程
函数 wglCreateContext()
1) 若当前无有效Ctx数量,则初始化所有 wgl_ctx[i].ctx 为NULL
2) 循环遍历所有 wgl_ctx[i],发现一个空的,则调用 WMesaCreateContext() 创建,若创建成功,则 ctx_count += 1,然后返回该句柄 HGLRC 形式
WMesaCreateContext()
1) 申请wmesa_context内存空间,然后初始化窗口句柄,像素位数,GLvisual(暂不理会)等
2) 调用 _mesa_init_driver_functions() 初始化驱动函数指针 dd_function_table 类型
3) 调用 _mesa_initialize_context() 初始化 GLcontext(这是一个重要类型)
4) 启用一些扩展,暂时忽略
5) 初始化 software rasterizer and helper module, 四个模块的初始化,重要!
6) 管道初始化,重要,_tnl_run_pipeline
7) 返回
_mesa_initialize_context()
1) 赋值初始化一些成员 GLcontext
2) share_list 的申请,(不大明白,暂时忽略处理)
3) 调用 init_attrib_groupd() 重要函数,会初始化很多东西,比如 buffer_objects, color, eval, depth, line, matrix, pixel, point, polygon, program, scissor, transform, viewport 等等
4) 申请内存空间,并初始化 ctx->Exec, ctx->Save,这个与glBegin(), glEnd() 有关
5) 再调用 _mesa_init_exec_vtxfmt() 初始化 Neutral tnl module stuff,与顶点有关的API了
6) 返回
wglMakeCurrent()
1) 检查若二者当中有一个为NULL,则调用 WMesaMakeCurrent(NULL, NULL))
2) 否则找到对应的句柄,必须能找到,否则失败,再调用 WMesaMakeCurrent()
WMesaMakeCurrent()
1) 如果hDC与hRC二者已经有关联,不用做什么退出即可
2) 寻找对应 hDC 的 WMesaFramebuffer
3) 如果hDC, hRC有效,却没有framebuffer,则进入以下步骤创建
==> 1) 获取窗口大小
==> 2) 调用 wmesa_new_framebuffer() 创建 WMesaFramebuffer
==> 3) 如果为双缓存,还要创建 back buffer
==> 4) 然后make render buffers,在wmesa_set_renderbuffer_funcs()处应该注意,它初始化了读写像素的函数指针,比如rb->PutRow(), rb->PutRowRGB(), rb->PutMonoRow(), rb->PutValues(), rb->PubMonoValues(), rb->GetRow(), rb->GetValues() 可以从后面看出,这些函数指针完成对渲染场景像素的读写
双缓存的时候,会建二个render buffers, 目前还没有精力去关注这一方面的内容
==> 5) _mesa_add_soft_renderbuffers() 添加深度缓存,累积缓存,模板
4) 上一步之后,若hRC与framebuffer有效,则传递相关参数,调用 _mesa_make_current()
5) 若以上均不符合,则判定为取消置为当前,传递NULL,调用 _mesa_make_current()
_mesa_make_current()
1) 若传入的为有效值(非空),则检查newCtx与drawBuffer, readBuffer之间的兼容性
2) 若可行,则置全局变量 _glapi_Context 值(该值会到处被用到),否则的话会置空
3) 还有对 _glapi_set_dispatch() (它直接对应OpenGL各API函数的) 的初始化,若有效,置各函数指针 newCtx->CurrentDispatch 指针,最后初始化各种 framebuffer size,主要是drawBuffer, readBuffer这两个
4) 后面还有一些初始化,默认也包括了 _mesa_set_viewport() _mesa_set_scissor 函数等
3 GLResize() 处的执行流程
注意:m_matrix.c 文件里的注释
-- # 4x4 变换矩阵以列为优先存储
-- # 点/顶点被认为是列矢量
-- # 点经矩阵的变换是 p' = M * p
glViewport()
1) 经过函数指针分派转至 _mesa_Viewport()
2) 获取当前 context
3) 确认是否在 glBegin(), glEnd() 外部使用
4) 调用 _mesa_set_viewport()
_mesa_set_viewport()
1) 在最大可能 MaxViewportWidth, MaxViewportHeigth 与给定值之间取最小值
2) 赋值改变 ctx->Viewport 参数,并置新状态 _NEW_VIEWPORT
3) 调用 _math_matrix_viewport() 初始化 ctx->Viewport._WindowMap
glViewport(0, 0, nWidth, nHeight)转换ctx->Viewport._WindowMap矩阵为
width/2 0 0 width/2 + x
0 height/2 0 height/2 + y
0 0 depthMax*((zFar - zNear)/2) depthMax*((zFar - zNear)/2 + zNear)
0 0 0 1
其中(x, y, width, height)对应上面函数的参数
zNear/zFar对应ctx->Viewport.Near/Far,初始值为0.0/1.0
depthMax值等于ctx->DrawBuffer->_DepthMaxF,该值初始值为65535.0
320 0 0 320
0 240 0 240
0 0 32767.5 32767.5
0 0 0 1
以列为优先存储的方式,故矩阵 0, 1, 2, 3 4, 5, 6, 7, 8,9,10,11, 12, 13, 14, 15内部值为
{ 320, 0, 0, 0, 0, 240, 0, 0, 0, 0, 32767.5, 0 320, 240, 32767.5, 1 }
4) 若存在驱动.Viewport,则调用它,对应 wmesa_viewport() 它负责resize_buffers()
glMatrixMode()
1) 经过函数指针分派转至 _mesa_MatrixMode()
2) 获取当前的 context
3) 确保在Begin与End() 之外使用,这个范围跟 glViewport() 有差别,但具体怎样暂时忽略
4) 如果ctx->Transform.MatrixMode已为给定模式,并且给定模式不等于 GL_TEXTURE,退出,不需要后面的处理
5) 因为 _NEW_TRANSFORM特性,需要完成以前的绘制,如果存在的话,FLUSH_VERTICES
6) 置 ctx->CurrentStack 为对应模式的矩阵堆栈,比如投影矩阵堆栈,模型视图堆栈
7) 改变 ctx->Transform.MatrixMode 为给定模式
8) 返回
glLoadIdentity()
1) 经过函数指针分派转至 _mesa_LoadIdentity()
2) 获取当前的 context
3) 确保在Begin, End, Flush之外使用本函数
4) 调用 _math_matrix_set_identity() 置 ctx->CurrentStack->Top 指针为单位矩阵
5) 给 ctx->NewState 添加新状态,后面会根据这个来调用相关的初始化函数
gluPerspective()
这个是glu里面的函数,设置透视投影矩阵
double radians = fovy / 2 * __glPi / 180 = 0.392699081698...;
double deltaZ = zFar - zNear = 99.4;
矩阵各数据为:
cos(radians)/sin(radians)/aspect 0 0 0
0 cos(radians)/sin(radians) 0 0
0 0 -(zFar+zNear)/deltaZ -2*zNear*zFar/deltaZ
0 0 -1 0
其中fovy = 45.0; aspect = 1.3333; zNear = 0.6; zFar = 100.0f;
1.810660171 0 0 0
0 2.41421356 0 0
0 0 -1.0120724350 -1.2072435090
0 0 -1 0
1) 调用 __gluMakeIdentityd() 初始化 m[4][4] 为单位矩阵
2) 改变 m[0][0], m[1][1], m[2][2], m[2][3], m[3][2], m[3][3] 的值
注意从这里可以看出 m[4][4] 与 OpenGL内部矩阵的存放次序是相反的, m[2][3] 对应mtx[11]位置,而m[3][2] 对应 mtx[14]位置,这一点很需要注意
{ 1.810660171 0 0 0, 0, 2.41421356 0 0, 0 0 -1.0120724350 -1, 0 0 -1.2072435090, 0 }
3) 调用 glMultMatrixd()
glMultMatrixd()
1) 经过函数指针分派转至 _mesa_MultMatrixd
2) 转换数据精度为 float型,调用 _mesa_MultMatrixf()
3) _mesa_MultMatrixf() 获取当前ctx,确保参数有效以及被调用位置
4) 然后调用 _math_matrix_mul_floats() 将矩阵乘至 ctx->CurrentStack->Top()
矩阵相乘,以列为优先,故 A(row, col) = A[col*4 + row] 这一点同 UGOPEN 一致
5) 给 ctx->NewState 添加新状态
以上已经可以完成对 GLResize() 的分析
4 GLDrawScene() 处场景绘制流程
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
此函数有点复杂,会完成很多事情
1) 经函数指针分派转至 _mesa_Clear()
2) 获取当前 context
3) 确保在Begin, End, Flush() 之外被调用
4) 完成当前场景 FLUSH_CURRENT()
5) 确保 mask 参数正确
6) 如果有新状态ctx->NewState,则调用 _mesa_update_state() 更新
更新过程很多,这个函数复杂也就复杂在这里了,更新完后,ctx->NewState会置为0
7) 如果有DrawBuffer, ctx->DrawBuffer有效,继续,否则退出
8) 如果渲染模式为 GL_RENDER,ctx->RenderMode的值,继续
9) 各种缓存模式,最后调用驱动的Clear函数,对应 wmesa.c 里的clear()函数
这里也有一些复杂,不是十分清楚,它内部具体在做什么,仔细分析,里边应该有一个环节将记录像素颜色的地址指针全部初始化为背景颜色的过程
glLoadIdentity() 置当前矩阵堆栈Top为单位矩阵
glTranslatef()
1) 经过函数指针分派转至 _mesa_Translatef()
2) 获取当前 context
3) 确保在Begin, End, Flush之外调用
4) 调用 _math_matrix_translate(),它相当于一个矩阵相乘,只是平移变换矩阵只需要更新有关的那三个值而已,可以具体化为矩阵相乘效果的
m[12] = m[0] * x + m[4] * y + m[8] * z + m[12];
m[13] = m[1] * x + m[5] * y + m[9] * z + m[13];
m[14] = m[2] * x + m[6] * y + m[10] * z + m[14];
m[15] = m[3] * x + m[7] * y + m[11] * z + m[15];
从而当前模型视图矩阵变为:
1 0 0 -1.5
0 1 0 0
0 0 1 -6
0 0 0 1
在内部的存储很明显是 { 1 0 0 0, 0 1 0 0, 0 0 1 0, -1.5 0 -6 1 }
5) 变换矩阵状态,给 ctx->NewState添加新状态
glBegin()
glColor3f()
glVertex3f()
glEnd()
这里的函数会走另一个指派过程,有点复杂,难以理清头绪
小专题1) 这里相关的函数所经过的指派过程,扩而广之,应对里边几乎所有地方都有一个记录吧,还包括所有 OpenGL API 函数的指派位置
1) neutral_**前缀指派
在 vtxfmt.c 内,然后包括 vtxfmt_tmp.h头文件 定义PRE_LOOPBACK()宏,再转而调用当前 GET_DISPATCH() 对应的位置,而在上面的 PRE_LOOPBACK()里有对这个位置的设置与改变的,会进入到 tnl模块
2) vbo_***前缀指派,在 vbo_exec_api.c 内,然后包括 vbo_attrib_tmp.h 暂时忽略 vbo_save_api.c 里的
在第一次执行 neutral_**之后,以后会直接转至 vbo_**来执行
glBegin()
1) 经过指派,转至 neutral_Begin() vtxfmt_tmp.h 文件内
2) 再又指派至 vbo_exec_**处执行
在这个函数里主要是记录和模式
glEnd()
1) 大体类似 先 neutral_End(),再 vbo_exec_End()
2) 并不会立即进入绘制管道
2012/06/05 星期二
初步的认定是 neutral_**() 会去fix顶点数据格式,vbo_**() 会记录数据
glTranslatef()
再次执行一个矩阵变换之后,会因为检测到需要Flush了,从而调用 驱动指针 ctx->Driver.FlushVertices, 对应 vbo_exec_FlushVertices() 函数
vbo_exec_FlushVertices()
内部确认参数之后,转而调用 vbo_exec_FlushVertices_internal()
可以这么说,不带 _internal的负责对上下文环境的处理,带_internal的职司 FlushVertices
本函数在后面还负责恢复函数指针,让它们重新首先指向 neutral_**()
5 结束场景绘制,准备进入管道
glEnd()之后未必会立即转向实际的绘制,mesa会缓存下前面已发生的指令,当下一步发现必须绘制时(比如在glEnd()之后调用glTranlate()变换视图矩阵了)或者缓冲区已满,才会转入管道准备绘制。
vbo_exec_FlushVertices_internal()
1) 若存在顶点 exec->vtx.vert_count > 0 或强制要求 flush
2) 调用 vbo_exec_vtx_flush() 转到这里去了
3) 再回复 exec内的某些成员,方便接受下一次输入
vbo_exec_vtx_flush() // Execute the buffer and save copied verts
1) 若存在 prim_count 以及 vert_count,亦即调用过 glBegin(), glVertex*() 则执行以下语句
==> 调用 vbo_copy_vertices() 获取某个指针
==> 如果与 exec->vtx.vert_count 不等则
==> ==> vbo_exec_bind_arrays()
==> ==> 若 ctx->NewState 有新状态,则调用 _mesa_update_state() 更新之
==> ==> 若 _mesa_is_bufferobj() 则调用 vbo_exec_vtx_unmap() 调试发现这里不执行
==> ==> 调用 vbo_context(ctx)-> draw_prims() 函数,转至 _tnl_vbo_draw_prims() 执行,然后转至 tnl_draw_prims() 完成,开始进入管道了,在 t_draw.c 文件之内
==> 上面完成之后,从调试来看,没再执行什么了
2) 后面恢复数据,比如 exec->vtx.buffer_ptr 指向 exec->vtx.buffer_map,以前验证过 glVertex*()命令所操作对应的位置就是在这里的
通过以上步骤之后,就可以结束 顶点的flush() 了
tnl_draw_prims()
进入 软件 tnl 模块的最主要入口
1) 如果min_index不为0,做一些设定,因为总是假定从0序号开始的
2) 如果max_index > max,顶点太多了,需要分割一下再处理
3) 普通情形下,现在只针对这一情形阅读:
==> bind_inputs()
==> bind_indices()
==> bind_prims()
==> TNL_CONTEXT(ctx)->Driver.RunPipeline() 对应 t_pipeline.c 下的 _tnl_run_pipeline()
==> unmap_vbos()
==> free_space()
前面的过程都很明显的在为管道初始化数据地址
中间的函数进入 管道运行 _tnl_run_pipeline()
最后面再清理一些东西
6 管道绘制
这一个环节会把glBegin()/glEnd()之间的指令转换成为最终输出上的像素,牵涉到好几个模快,弄懂它们也才能最终明白绘制指令是如何被具体实现的,以前的过程中对这里虽然也花了不少精力,但并没有完全弄明白。
在矩阵变换方面,直线绘制(好像用的是中点绘制算法,但不是非常肯定这一点),超出边界裁剪算法等地方用了不少的宏替换生成方法,如果觉得难以读懂,建议打开VC设定里的存储宏替换之后的.i文件。
矩阵变换方面的指针在 m_xform.c 内初始化,会包括好几个文件来构建数组
数据结构 SWvertex 在软件光栅化时存储顶点的数据结构,这个很重要,最终绘制时输入就是这个。
wpos = attr[FRAG_ATTRIB_WPOS] 在顶点里必须是第一个值,因为 tnl clipping code的缘故
wpos[0] 和 wpoa[1] 是 SWvertex 的屏幕点
wpos[2] 是 z-buffer 坐标 (如果16-位的Zbuffer,在范围 [0, 65535] 之内)
wpos[3] 是 1/w,其中 w是W坐标的倒数,这是 ndc[XYZ]必须乘以而得到的 clip[XYZ]值
管道是什么样的?
在 t_context.h 头文件内,数据结构 tnl_pipeline_stage 描述单一的管道操作,包括create, destroy, validate, run几个函数指针以及少量的数据成员
数据结构 tnl_pipeline 包括所有管道的数组容器,默认值在 t_pipeline.c 的最后,亦即 _tnl_default_pipeline[] 数组,从中可以看出先做顶点变换,再光照,纹理等,最后是运行渲染,渲染部分与管道部分之间可以再进一步分开
各个管道的初始化,可以在源代码中找到
安装管道
由 _tnl_install_pipeline() 来完成,可能在最开始的wglCreateContext()里就调用此函数了,此段代码的执行应该是比较靠前的。
管道运行 _tnl_run_pipeline()
1) 如果 tnl->vb.Count 为0,则可退出,对应顶点数量
2) 检查输入变换,校验tnl->pipeline.new_state状态
3) 遍历每一个管道,执行里边的 run函数
顶点变换管道 t_vb_vertec.c 内
run_vertex_stage() 函数执行流程
1) 如果使用了 顶点编程,则退出
2) 如果 ctx->_NeedEyeCoords 不为0,则执行xxx (这里不大明白,具体不做什么事情)
3) VB->ClipPtr = TransformRaw() 用 ctx->_ModelProjectMatrix 矩阵变换输入顶点,在 glBegin()与glEnd()之间所用到的值
矩阵里也对应有不少宏指派,这里会转至 m_xform_tmp.h 内的 transform_points3_general
这里所对应的就是用模型投影矩阵变换输入顶点
1.8106601 0 0 -2.7159901
0 2.4142137 0 0
0 0 -1.0120724 4.8651910
0 0 -1 6
(1) 顶点变换结果情况列表如下上一矩阵右乘列向量获得 p' = M * p
(0.0 1.0 0.0) 变换为 (-2.7159901 2.4142137 4.8651910 6)
(-1.0 -1.0 0.0) (-4.5266504 -2.4142137 4.8651910 6)
(1.0 -1.0 0.0) (-0.90532994 -2.4142137 4.8651910 6)
上述变换结果点为VB->ClipPtr取值
4) 不管 tnl->NeedNdcCoords 是否需要,都会再进行如下一个转换,具体是 点从
(x, y, z, w) 变换成为 (x/w y/w z/w 1/w) ,赋值给 VB->NdcPtr
从上一步再变换为
(-0.45266503 0.40236896 0.81086516 0.16666667)
(-0.75444174 -0.40236896 0.81086516 0.1666667)
(-0.15088832 -0.40236896 0.81086516 0.1666667)
以上顶点变换过程就完成了。
中间的这些管道因为没有被启用,比如光照,纹理,雾等等,故而不需要执行什么,内部会返回GL_TRUE
光栅渲染管道 t_vb_render.c 内
run_render()
对于这一过程,我想重要的二个地方在:内部如何建构屏幕点SWvertex;从屏幕点绘制算法;目前对后一问题有所追索,而前一问题总结得还不够,好像层次比较复杂。
1) 调用 tnl->Driver.Render.Start() 函数 对应_swsetup_RenderStart()
==> 这里会在 _swsetup_choose_trifuncs() 初始化 tnl->Driver.Render. 三角形,四边形,直线绘制 swsetup_line(),点绘制函数指针
==> 还有调用 setup_vertex_format() 以明确如何构建 SWvertex
内部通过 tnl_attr_map 的map数组,记录每一个需要拷贝的值,比如总是拷贝第一个位置的 EMIT_ATTR(_TNL_ATTRIB_POS, EMIT_4F_VIEWPORT, attrib[FRAG_ATTRIB_WPOS]),然后如果有颜色设定,则拷贝颜色 EMIT_ATTR(_TNL_ATTRIB_COLOR0, EMIT_4CHAN_4F_RGBA, color);等等,范例只有这2个有设定, 最后调用 _tnl_install_attrs() 设置顶点格式
这个过程有调用 invalidate_funcs() 设置函数指针,比如 emit 指向 choose_emit_func,
2) assert() 确认 Render内的指针有值
3) 调用 tnl->Driver.Render.BuildVertices() 亦即 _tnl_build_vertices(),这里会对顶点有一个变换,但是这个变换却又藏得很深 insert_4f_viewport_4() 函数
关键是在这里基于先前的输入开始构建起后面所需要的顶点,所以它执行了一个系列的转换流程,变换到最终的屏幕点,再最后从屏幕点开始绘制,所以这里的变换过程也很关键的,值得深入探讨一番
==> 获取 tnl_clipspace 指针
==> 调用 update_input_ptrs() 根据被设定的属性数量,更新对应的数据指针地址,然后窗口viewport矩阵设置 vtx->vp_scale vtx->vp_xlate 的几个值
==> 调用 vtx->emit()指针函数,亦即 choose_emit_func(),它会根据 vtx->attr (数据类型 tnl_clipspace_aatr ),设置对应的 a[j].emit 函数指针,a[j].insert[] 而这里的编排可能需要一点时间,对于viewport,设置的是 insert_4f_viewport_4 函数
然后再尽可能重置 vtx->emit, 若最后没有值,则使用 _tnl_generic_emit() 函数,再调用此函数,该函数所完成的工作亦即遍历所有顶点,调用先前在a[j].emit()里设置的函数,因而也就会调用 insert_4f_viewport_4(), 完毕之后,至此,本函数的运行结束了
顶点再一步变换为
(175.14719 336.56854 59337.523 0.1666667)
(78.578644 143.43146 59337.523 0.1666667)
(271.71573 143.43146 59337.523 0.1666667)
4) 获取合适的渲染函数指针地址
5) 遍历所有的prim以及顶点,绘制它们,这里又使用了宏指令来得到不同的函数指针,如果是绘制直线并且带颜色的,会到达 rgba_line() (此函数定义经过宏替换产生),里边的算法很像 中点绘制算法,不过不再严格去比对确认是否真为中点绘制算法了
6) 调用 tnl->Driver.Render.Finish(ctx)
以上再次对 OpenGL变换与绘制 又重新有所了解了,我在学习OpenGL的时候,有时候头昏脑涨,搞不清楚最终这些指令会是怎样被执行的,所以过来阅读mesa源代码,应该说有所收获吧,看到管道的一种设计,看到函数的规划与宏替换,真正体会到,C语言的简洁与强大,当然更强的是mesa开发者对内部模型的设计,如果搞清楚了设计模型,里边很多代码就顺理成章了。所以阅读之后,我的感受是开发语言并不重要,项目的规划与设计才是最重要的。
更多推荐



所有评论(0)