显示标签为“Nebula3”的博文。显示所有博文
显示标签为“Nebula3”的博文。显示所有博文

2009年4月3日星期五

Nebula3基础层-App


1.控制台程序需要继承ConsoleApplication类,如果需要做一些初始化可以覆写ConsoleApplication::Open()方法,在覆写该方法时要记得调用父类相同的方法来初始化。程序的逻辑代码可以在ConsoleApplication::Run()方法中实现。

2.ConsoleApplication::Open()方法的主要是初始化核心子系统,IO子系统和脚本系统。

Nebula3基础层-IoInterface

从类图上我们可以看出,通过扩展消息模块我们很轻松地让IO子系统运行在一个单独的线程中。其它子系统通过发送消息来和它进行交互。

2009年3月31日星期二

Nebula3基础层-消息子系统


Message子系统是Nebula3实现多线程架构的核心。

Port:1.消息接收端口。消息被立即处理并且程序将被阻塞直到消息处理完成。消息是通过增加到Port中的Handler来处理的,Port可以增加一个或多个Handler。当接收到一个消息,增加到Port中的每个Handler将被调用来处理消息,直到有一个Handler处理完消息后返回true,这意味着消息处理完成了。

2.Port::RegisterMessage(const Id& msgId)方法用来注册该Port能处理的消息类型。

AsyncPort:在一个分开的线程中调用handlers来处理消息,因此不会阻塞主线程。AsyncPort的子类覆写AsyncPort::OnCreateHandlers()方法,在这个方法中创建Handler并把它添加到AsyncPort中。

Handler:实际处理消息的类。用户需要继承Handler类并覆写Handler::HandleMessage()方法。

Id:消息类型标识符。在消息中使用DeclareMsgId和ImplementMsgId宏来实现。

Message:包装数据并可以被发送到Port和AsyncPort。这实现了一个通用的通信机制,不仅可以用于相同线程,线程之间,甚至还可以用在不同机器之间的通讯。Message作为一个普通的C++对象,可以被持久化。用户继承Message类实现自己需要的不同消息。

Dispatcher:是一个特殊的消息接收端口。Dispatcher把消息分发到能处理该消息的端口上。具体实现可以参考类图和以下图示:

Nebula3基础层-线程子系统


线程子系统主要是对各种平台的多线程部分进行抽象和封装。

Thread:对平台线程的封装,调用Thread::Start()开启一个新的线程。用户需要在子类覆写Thread::DoWork()方法来执行自己需要的操作,当用户需要在子类的DoWork()方法中执行循环操作时就必须调用Thread::ThreadStopRequested()方法来判断线程是否停止。调用Thread::Stop()停止一个线程,线程会等待DoWork()方法处理完成后才退出。

Event:用来处理线程同步。当一个线程调用Event::Wait()时,线程将处于等待状态。调用Event::Signal()将激活一个等待的线程。Event::Peek()用来检查线程当前的状态,并立即返回结果。

CriticalSection:同步代码块,处于CriticalSection::Enter()和CriticalSection::Leave()之间的代码,同时只能由一个线程来执行。

Interlocked:提供一些简单的原子操作。

2009年3月30日星期一

Nebula3基础层-IO子系统


Io子系统的整个设计主要是参考了.net的io设计。一个比较重要的设计是对路径的抽象。

Stream:一个抽象的数据流,接着分别实现了文件流和内存流,zip流。用户可以根据自己的需要实现其它类型的流。

StreamReader,StreamWriter:提供了读写流的抽象。

FSWrapper:包装了不同平台的io操作。

IoServer:Io子系统对外提供服务的接口,主要有以下操作:

1.根据不同的URI scheme关联到不同的流类。
2.根据给定的URI创建正确的流。
3.对于ZIP压缩包提供透明的支持。
4.路径别名管理。
5.提供全局的文件系统操作和查询方法。

2009年2月26日星期四

Generating Shaders From HLSL Fragments

Shaders are cool. You can do all sorts of interesting things with them: this and previous ShaderX books are full of examples. Alongside their power, however, programmable shaders can lead to an explosion of permutations: my last Xbox game contained 89 different pixel shaders, and my current project already has far more. Many of these shaders are variations on a few basic themes, for instance level-of-detail approximations of a material, or the same lighting model both with and without animation skinning. The total number of combinations is huge and is increasing all the time. Typing everything out by hand would be time consuming, error prone, and a maintenance nightmare.

This article will describe how to automatically generate large numbers of shader permutations from a smaller set of handwritten input fragments.

全文:Generating Shaders From HLSL Fragments

2009年2月23日星期一

Nebula3的点和向量

在Nebula3中严格区分点和向量并分别使用point和vector来表示点和向量。point和vector都是继承float4,两者的区别在于point的w分量为1,而vector的w分量为0。在Nebula3中点和向量分别提供了一些直观的操作:

点:

点+向量=点
点-向量=点
点-点=向量

向量:

向量+向量=向量
向量-向量=向量
向量*常量=向量

Nebula3使用的一些关键字

__forceinline:

在VC++中可使用另一关键字_forceinline 代替inline 关键字.这个关键字将命令编译器跳过一般的ROI 分析(Return On Investment --一种编程缩略语),将所对应的代码强行内联.在有写时候,编译器会拒绝将一个函数内联,使用这个关键字,用户只得到一个编译警告,就可强行内联.

在使用内联函数时,是由编译器决定它们是按普通函数处理还是将调用函数部分用实际的函数体代码替换。不允许将递归函数进行内联(VC++可进行编译器选项设置,允许内联扩展到一定深度)

下面情况不宜使用内联:

1.如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
2.如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

volatile:

如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化。

一般说来,volatile用在如下的几个地方:

1、中断服务程序中修改的供其它程序检测的变量需要加volatile。
2、多任务环境下各任务间共享的标志应该加volatile。
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同意义。

__cdecl:

C,C++的默认调用规范,参数从右到左传替,由调用函数管理堆栈。

2009年2月19日星期四

Nebula3单例

很多重要的Nebula3对象都是单例,在应用程序中这些单例仅仅存在一次并且可以被其它对象所获取。

可以通过静态的Instance()方法获得单例对象,该方法返回单例类的单一实例。保证返回的指针是有效的。在Instance()方法被调用时候如果单例对象不存在,那么将抛出一个断言。

// obtain a pointer to the Core::Server singleton
Ptr = Core::Server::Instance();

你也可以检查给定的单例是否存在:

// does the Core::Server object exist?
if (Core::Server::HasInstance())
{
// yep, the core server exists
}

Nebula3提供了一些宏帮助实现单例类:

// declare a singleton class
class MySingletonClass : public Core::RefCounted
{
DeclareClass(MySingletonClass);
DeclareSingleton(MySingletonClass);
public:
/// constructor
MySingletonClass();
/// destructor
virtual ~MySingletonClass();
...
};

// implement the singleton class
ImplementClass(MyNamespace::MySingletonClass, 'MYSC', Core::RefCounted);
ImplementSingleton(MyNamespace::MySingletonClass);

//------------------------------------------------------------------------------
/**
Implements the Singleton constructor.
*/
MySingletonClass::MySingletonClass()
{
ConstructSingleton;
}

//------------------------------------------------------------------------------
/**
Implements the Singleton destructor.
*/
MySingletonClass:~MySingletonClass()
{
DestructSingleton;
}

DeclareSingleton()和ImplementSingleton()宏跟DeclareClass()和ImplementClass()宏相似。它们往类里增加了一些静态方法(Instance()和HasInstance()方法)。类的构造函数和析构函数必须包含ConstructSingleton和DestructSingleton宏。ConstructSingleton宏初始化一个私有的静态单例指针并确定这个类没有其它的实例存在(否则,将抛出一个断言)。DestructSingleton宏让静态单例指针无效。

默认是从本地线程获取一个单例。这意味着单例是创建在Nebula3应用程序的一个线程中,并且其它线程不能访问该单例。这种方式遵循着“并行Nebula”的设计,让多线程编程变得更加简单。隐藏在“并行Nebula3”背后的构想是,一个典型的Nebula3应用程序包含一些运行在分开的CPU核上的“胖线程”("Fat Threads")。胖线程实现例如异步IO,渲染,物理等等。每一个胖线程初始化它自己的Nebula3运行库,这个库包含胖线程执行特定任务所需的最小Nebula3环境。这基本上消除了大部分Nebula3代码所需的细粒度同步,并且把“线程相关”的代码集中在几个定义明确的范围内用于胖线程之间的通讯。“并行Nebulas”设计的另外一个有用效果是,让程序员不必太关注代码运行在一个多线程环境。典型的Nebula3代码就像普通的单线程代码,然而它可以运行在它自己的胖线程内。

原文:<<The Nebula Device 3 Document>>Nebula3 Singletons

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

Nebula3运行时类型信息系统

Nebula3的RTTI系统允许你在运行时取得一个对象类类型并让你检查一个对象是否正是一个类的实例,或者是继承类的实例。你也可以直接从一个对象中取得类名或类fourcc标识符。所有这些功能都是在DeclareClass()和ImplementClass()背后实现的。Nebula3的RTTI机制比Nebula1和Nebula2的RTTI机制要来得更高效和更简单。

例子:

using namespace Util;
using namespace Core;

// check whether an object is instance of a specific class
if (myObj->IsInstanceOf(MyClass::RTTI))
{
// it's a MyClass object
}

// check whether an object is instance of a derived class
if (myObj->IsA(RefCounted::RTTI))
{
// it's a RefCounted instance or some RefCounted-derived instance
}

// get the class name of my object, this yields "MyNamespace::MyClass"
const String& className = myObj->GetClassName();

// get the fourcc class identifier of my object, this yields 'MYCL'
const FourCC& fourcc = myObj->GetClassFourCC();

你也可以通过中心工厂对象查询给定的类是否注册了:

using namespace Core;

// check if a class has been registered by class name
if (Factory::Instance()->ClassExists("MyNamespace::MyClass"))
{
// yep, the class exists
}

// check if a class has been registered by class fourcc code
if (Factory::Instance()->ClassExists(FourCC('MYCL')))
{
// yep, the class exists
}

原文:<<The Nebula Device 3 Document>>The Nebula3 Runtime Type Information System

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

2009年2月18日星期三

创建Nebula3对象

从Core::RefCounted继承下来的Nebula3对象可以通过3中不同的方式创建:

1.直接使用静态创建方法:

Ptr< myObj> = MyClass::Create();

静态Create()方法是在声明的前面通过DeclareClass()宏添加到类中的。这是C++ operator::new()的语法糖衣。实际上,Create()方法内部除了调用new操作以外什么也没有。另外,正确使用智能指针持有新对象。

2.另外一种方式是使用类名创建对象:

using namespace Core;

Ptr< myObj> = (MyClass*) Factory::Instance()->Create("MyNamespace::MyClass");

如果你在编译时不知道对象类,通过它的字符串类名创建一个对象是很有用的。这种方式常常用于恢复序列化对象,或者使用某种脚本接口。注意类型转换,因为Create()方法返回一个通用的指向Core::RefCounted对象的指针。

3.通过类的fourcc类标识符创建对象:

using namespace Core;
using namespace Util;
Ptr<> = (MyClass*) Factory::Instance()->Create(FourCC('MYCL'));

这种方式看起来比较不直观,但它创建对象的速度比使用类名快,并且fourcc类标识符(4bytes)比字符串类名占用更少的空间。当一个对象被编码/解码到二进制流的时候这种方式创建对象方式有很多的优点。

原文:<<The Nebula Device 3 Document>>Creating Nebula3 Objects

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

引用计数和智能指针

Nebula3使用传统的引用计数来管理对象的生命周期。从程序员的角度来看一个模板智能指针类Ptr<>的存在隐藏了引用计数的细节。做为一般规则,总是使用智能指针指向从RefCounted继承的对象,除非你可以确定在给定的代码块,对象的引用计数不会被改变。

智能指针比普通指针具有的优点:

1.获取一个空指针时,智能指针将给你一个简单调试断言而不是内存错误
2.在引用计数对象中你从不必调用AddRef()或者Release()方法(实际上如果你调用了,会有一些严重的错误)
3.智能指针可以很好地工作在容器类内,一个智能指针数组代替普通指针消除了各种生命周期管理问题,你从不需要关心指针后面对象的释放,数组的行为看起来像包含真的C++对象
4.使用智能指针,你一般不需要像普通指针那样经常定义“对象所属关系”(谁要负责删除对象,等等...)

智能指针的缺点:

1.性能:拷贝和赋值智能指针时包含调用AddRef()和/或Release()方法,间接引用一个智能指针包含一个检查智能指针所包含的对象指针是否有效的断言检查。由此产生的性能问题一般被忽略,但在内部循环你必须意识到这一点。

2.原本要销毁的对象还一直存在着:由于使用智能指针管理对象,只有当最后一个客户放弃了所有权后对象才会被删除,对象可能比预期存在更长时间。往往这是一个错误点。Nebula3将提示你有关任何的引用计数泄露。

原文:<<The Nebula Device 3 Document>>RefCounting And Smart Pointers

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

2009年2月17日星期二

实现一个新的Nebula3类

当实现一个新类时要做的第一个决定是该新类是继承Core::RefCounted类或者是一个传统的C++类。下面几点将有助于你找到答案:

1.如果新类打算使用Nebula3对象模型扩展的特性如refcounting,RTTI等等,那么它必须继承Core::RefCounted类。

2.如果新类是一个典型很小的帮助类或工具类,像一个动态数组类,一个数学向量类,或者其它类似的。那么它从Core::RefCounted继承下来也没有什么意义。

继承Core::RefCounted的类会有一些限制:

1.继承Core::RefCounted的类不能直接在本地C++上下文创建栈对象,因为栈对象的生命周期是由C++管理的(当离开当前C++上下文它们将自动销毁,这样就完全绕开了Nebula3的引用计数生命周期管理)。

2.继承Core::RefCounted的类只有一个默认的构造函数。

3.继承Core::RefCounted的类必须有一个虚拟的析构函数。

4.继承Core::RefCounted的类必须不能拷贝,因为这样将搞乱引用计数机制。

为了使用Nebula3对象模型特性,首先是要继承Core::RefCounted类,其次是要在新类的声明和头部文件注释一些额外的信息:

一个标准继承Core::RefCounted类的声明看起来像这样:

namespace MyNamespace
{
class MyClass : public Core::RefCounted
{
DeclareClass(MyClass);
public:
/// constructor
MyClass();
/// destructor
virtual ~MyClass();
...
};
RegisterClass(MyClass);

注意DeclaredClass()宏,默认构造函数和虚拟析构函数和在类声明外的RegisterClass()宏。DeclareClass()宏为实现RTTI和工厂机制在类声明中增加了一点点Nebula3特性信息。从程序员的角度来看DeclareClass()宏通常是隐藏在Nebula3对象模型内部,因此,对象模型内部可以在不影响已存在类的情况下被改变。RegisterClass()宏是可选的,它注册类到中心工厂对象。如果你知道类对象将永远不会通过字符串类名或fourcc代码创建,那么RegisterClass()宏可以被忽略。

在类的.cc文件中需要包含以下Nebula3特殊信息:

namespace MyNamespace
{
ImplementClass(MyNamespace::MyClass, 'MYCL', Core::RefCounted);
}

ImplementClass()宏注册了类的RTTI机制,第一个参数是C++类名(注意,类名必须包含命名空间)。第二个参数是的类的fourcc代码,fourcc代码在所有类中必须是唯一的(在程序启动时如果有2个类注册了相同的fourcc代码将会产生一个运行时错误)。第三个参数是父类的名称。这个被RTTI机制用来重建类树。

(说明:在N3的最新版本中DeclareClass改为__DeclareClass,ImplementClass改为__ImplementClasClass改为__RegisterClass)

原文:<<The Nebula Device 3 Document>>Implementing A New Nebula3 Class

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

Nebula3对象模型

Nebula3实现了基本对象模型,该对象模型在c++对象模型的基础上实现了以下几个新的特性:

1.通过引用计数和智能指针实现对象生命周期管理
2.通过字符串类名或fourcc类标识符创建对象
3.一个运行时类型信息系统

原文:<<The Nebula Device 3 Document>>The Nebula3 Object Model

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

2009年2月16日星期一

Nebula3核心子系统

Nebula3核心子系统(顾名思义)实现了Nebula3的核心概念,如下:

1.一个RefCounted基类用于实现一个强大的引用计数机制
2.一个运行时类型信息系统
3.一个模板智能指针类Ptr<>用于管理RefCounted对象的声明周期
4.一个工厂机制,允许使用字符串类名创建c++对象
5.一个中心服务对象用于设置基本的Nebula3运行环境

原文:<<The Nebula Device 3 Document>>The Nebula3 Core Subsystem

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

2009年2月12日星期四

关于关卡设计和构建系统的思考(二)

整篇帖子的重点是:怎么样才能让关卡设计变得有趣呢?如果能让关卡设计师立即看到设计的结果那么他将会开心很多,并且还可以直接和其他的设计师一同工作。如果关卡设计可以像Wiki和多人游戏混合起来那会怎么样呢?

这是我们设想未来关卡设计的工作方式:

1.关卡设计师早上来上班,启动游戏到关卡设计模式。
2.游戏通知关卡设计师有需要更新,仅更新一个可执行文件。
3.游戏连接到中心游戏服务器,数据库中存储实际游戏数据,并且图形/音频内容通过网络共享。
4.关卡设计师直接在游戏中创建,放置和销毁游戏对象,所有这一些改变将通过游戏服务器分布到附近工作的其他关卡设计师。
5.为测试改动,关卡设计师按一下开始按键,几秒钟后,编辑器将转到游戏模式(严格区分编辑模式和游戏模式是很重要的,因为应用程序员不关心关卡设计器的东西)。
6.内置游戏编辑器是用C#写的专门工具窗体。
7.晚上,关卡设计师关掉机器并回家。

因此我们将放弃Maya作为关卡设计工具并赞同使用“collaborative ingame level editor”。这个协助/多人部分听起来有点像骗人的玩意,但它实际上是很重要的因为它可以解决数据冲突问题。由于所有的改变可以立即分布到所有的关卡设计师那里,创建相冲突的数据就没有什么危险了。

直到几天前我放弃了整个设想并宣布这是不可能实现的。实现一个适合所有不同类型的内置游戏编辑器听取来是一件很困难的事情。但到最后它也不是那么困难。我们已经有许多的基础模块可用:

1.我们可以从我们当前的“Remote Level Design”获得很多想法。目前,我们可以一边运行Maya一边运行游戏,在Maya中的改变可以立即在游戏中显示,例如这对于调节灯光参数是很有用的。

2.游戏数据已经完全存储在一个轻量本地数据库中(SQLite)。这给了我们很多优势:

2.1一个游戏实体通过一个简单的命名和类型属性就可以完整描述
2.2一个游戏实体在数据库表中总是占据一行
2.3所有“其它数据”已经存储在数据库表中
2.4所有数据的操作可以用一个很小的SQL子集来表式(INSERT,UPDATE and DELETE ROW)

3.The only operations that must be supported by the generic ingame level editor must be "Navigate", "Create Entity", "Update Entity", "Destroy Entity", where creation is always a duplication either from a template or from another entity. More complex operations, or different views on the game data, will be implemented in C# tools which are connected through a standardized plugin interface.

4. 使用Nebula3的TcpServer/TcpClient类和IO子系统作为基础将比较容易实现游戏中客户/服务系统的需求

5.我们已经使用一些用C#写的专门编辑工具(之前我们是用MEL来写的,使用C#来开发用户图形界面比MEL有很大的效率提高)

当然了魔鬼总是在细节中。但我想这是一个不错的计划,在将来的项目上从根本改善我们关卡设计流程。

原文:Level Design And Build System Thoughts

限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

关于关卡设计和构建系统的思考(一)

最近我经常和Bernd讨论如何在接下来几个月改善我们的构建系统和关卡设计流程。对于一个“大”的项目如Drakensang,完整的构建周期和增量构建周期,还有关卡设计人员的“local turnaround”开始变得很重要了。一个完整(每夜)构建现在大约要11小时(包括重新编译和重新构建每一样东西,生成一个安装包并把结果上传到FTP站点上)。一个增量构建(在白天)要花费至少半个小时到2个小时(没有生成安装包和上传)。进一步看,Drakensang大约有7000个纹理和大约4500到5000个3D模型(因为我现在不在radon Labs所以不知道确切的数字),整个游戏的运行数据现在大约4GB大小。

对于关卡设计师,这里有两个分开的时间周期问题:从最近的每夜构建中更新工作机的数据(这可能需要半小时到一个小时),和在实际游戏中测试改动的时间(我们现在使用Maya作为关卡设计器,而不是一个游戏内置的编辑器)。

在Radon Labs我们有一些规定:

1.每日构建:每个人必须工作在最新的数据上,至多不能超过一天。
2“Make Game:在中心构建机器上创建一个完全自动的完整构建。
3.The Toyota Rip Cord(不知道这个翻译是否正确,在德文是“Toyota Reißleine”):如果不能构建,生产必须被停下来,直到问题被找到并解决掉。
4.一个工作只使用一件工具:对于相同的工作不能使用几个不同的工具(例如所有的3D模型都是使用Maya来做)。

我们还有其它一些规定,但它们对于构建系统或者关卡设计工作没有影响,因此我就不在这里介绍了:)

例如我们很容易因为害怕而放弃每日构建。但这将很有可能在公司内部建立一个“象牙塔”。很多时候事情总是朝着这样的趋势发生,它们对于项目是有害的,在它们出现的时候必须制止住。

相反我们退回去并思考一下关于完美的构建系统和完美关卡设计系统看起来是什么样的。这整个问题可以分为3问题:

1.降低构建时间
2.分布构建数据到各个工作台
3.降低关卡设计师设计的周期时间

观点(1)是相对容易的。我想我们唯一能获得提高的是分布工作量到几台构建机器上。对于我们的Maya输出插件我们已经做了很多优化,因此不可能再有什么提高了。设置一个分布式构建系统是一项有趣的工作,如果你控制所有的构建工具这也不会太复杂。

观点(2)比较有趣。这里的问题是“我们真的需要把所有的构建数据分布到各个工作台吗?”每个工作台每天有4GB未压缩的数据,但一个具有代表性的关卡设计师每天正常的工作仅仅需要很少的数据,像下面这样:

1.关卡设计师早上来上班,从每夜构建中取得最近的构建数据。
2.关卡设计师cvs-edits他工作需要的文件。
3.关卡设计师使用Maya和几个专门的工具工作,像dialog and quest编辑器。
4.关卡设计师频繁的检入在游戏中他做的改动。
5.晚上,关卡设计师cvs-commits他的工作并回家。
6.构建机器开始一个完整的构建。

这里有几个问题:

1.在早上,很多时间浪费在更新工作机器上的运行数据。
2.在游戏中检查更改的时间周期太长了。
3.当关卡设计师每天晚上检入他的工作时,会和其他关卡设计师的工作发生难以意料的冲突。

根据具体项目的大小和复杂度,关卡设计变得越来越让人沮丧,因为越来越多的时间花在等待结果上。

原文:Level Design And Build System Thoughts

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

2009年2月10日星期二

Nebula3渲染层:图形核心系统(CoreGraphics)

图形核心子系统主要的功能是兼容包装主机3d渲染API。它被设计成在不损失功能或运行性能的前提下支持带有可编着色的Direct3D/OpenGL类型API。图形核心子系统的基本功能和Nebula2 gfx2-子系统一样,然而图形核心系统修复了许多Nebula2图形系统的问题。

乍一看,图形核心子系统因为有更多的类看起来比Nebula2复杂。这个原因很简单,因为图形核心系统的类更小并且更专门化。大多数图形核心系统的类的功能一句话就可以说清。而Nebula2中的每个类要同时处理几件事情,所以它的类个头大,数量少。

典型的Nebula3应用程序并不需要过多地与图形核心子系统打交道,而是与更高级的子系统,如图形子系统(我们将在下一个帖子详细讨论),发生关系。

图形核心子系统一些重要的设计目标:

1.无限任何条件就可以移植到Direct3D9,Direct3D10和Xbox360:

图形核心系统允许更自由地移植到其它平台,Nebula2使用虚拟方法的方式(porting-through-virtual-functions)进行移植,而图形核心子系统使用条件预定义(porting-by-conditional-typedefs),在不损失任何性能的情况下,移植可以自由地覆写任何一个类(例如:平台相关的方法可以做为内联方法)。

2.改善资源管理:

Nebula3将降低资源的使用和资源的初始化。通过ResourceLoader类初始化资源,保持实际的资源类小且紧凑,并且资源系统更加地模块化(想要弄清楚为什么必须要解决上述问题,看看Nebula2中的nTexture2类)。

3.减少集中化:

现在使用更多的专门的单例类代替一个大的nGfxServer2类:

RenderDevice: handles rendering of primitive groups to a render target。
DisplayDevice: 处理显示设置和显示管理。
TransformDevice:管理渲染需要的变换矩阵,处理视图矩阵,投影矩阵和模型矩阵的输入,并且提供矩阵的求逆和组合。
ShaderServer:着色系统的关键,下面有详细说明。

4.改善离屏渲染

5.更大地提升着色系统:

5.1 为渲染带有许多不同对象和材质的典型场景提供降低“切换和更新着色”费用的基础。

5.2 跟Nebula2一样,一个着色基本上是一个Direct3D效果(effect)(一族的技术(techniques),每种技术包含多个渲染过程,每个渲染过程包括多个渲染状态)。

5.3 ShaderInstances是带有自己着色参数值的效果(effect)拷贝。

5.4 现在更多的是通过ShaderVariables来设置着色参数。

5.5 ShaderVariations和ShaderFeature:通过特性位掩码一个着色可以提供不同专门变量以供选择。例如,一个特征可能叫做“Depth”,“Color”,“Opauqe”,“Translucent”,“Skinned”,“Unlit”,“PointLight”,一个着色可以为特征组合如“Depth | Skinned”,“Color | Skinned | Unlit”,“Color | Skinned | PointLight”,提供专门变量。高层渲染代码将视需要来设置特征位,并依赖当前的特征位掩码,在渲染时相应的着色变量将自动被选择。结合适当的工具,ShaderVariations和ShaderFeatures将对修复各种与可着色编程相关的维护和运行问题有很大的帮助。

相比较gfx2,图形核心系统的一些其它小改动是:

1.现在可以通过EventHandlers代替原来固定在图形系统处理DeviceLost/Restored和鼠标和键盘消息处理的方式。

2.VertexBuffer和IndexBuffer现在作为公开类。

3.Vertex组件现在支持压缩格式像Short2,Short4,UBYTE4N,等等...

4.DisplayDevice提供了几个便利的方法用于获得支持显示模式列表或当前桌面显示模式,且用于获得当前显示的详细信息(硬件,厂商和驱动的版本信息)。

5.现在可以在打开应用程序窗口之前调用静态的RenderDevice::CanCreate()方法实际检查当前主机是否支持3d渲染。

原文: The Nebula3 Render Layer: CoreGraphics

[声明]:限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;

关于Nebula3的单例

Nebula3中有两种形式的单例:

1.使用__DeclareSingleton宏定义的单例,这是一种在线程中的单例。在每个线程中只有一个对象,但在整个应用程序中可能会有多个。

把__DeclareSingleton宏展开如下:

public:
ThreadLocal static type * Singleton;
static type * Instance() { n_assert(0 != Singleton); return Singleton; };
static bool HasInstance() { return 0 != Singleton; };

可以看见我们定义了一个由ThreadLocal修饰的静态属性,在types.h中,我们可以看到ThreadLocal是如下定义的:

#if __WIN32__
#define ThreadLocal __declspec(thread)

简单地说__declspec(thread)声明一个线程局部变量并具有线程存储时限,以便链接器安排在创建线程时自动分配的存储。

在Nebual3引擎中大量地使用这种形式的单例,这和整个的Nebula3的多线程架构设计相关。

2.使用__DeclareInterfaceSingleton宏定义单例,这就是全局单例了,在整个应用程序中只存在一个对象。

展开__DeclareInterfaceSingleton宏:

static type * Singleton;
static type * Instance() { n_assert(0 != Singleton); return Singleton; };
static bool HasInstance() { return 0 != Singleton; };

2009年2月9日星期一

Nebula3内存模块-内存池(二)

在上一篇blog中我们已经讨论完关于内存池的初始化,这里我们来看看是如何从内存池中取得所需的内存块。要从内存池中分配一块内存块,我们需要调用void* MemoryPool::Alloc()方法,接下来我们详细介绍该方法是如何执行的:

1.首先判断存放可分配内存块的内存页的数组roomyPages是否为空,如果为空就调用void MemoryPool::AllocPage()创建一个新的内存页。

if (this->roomyPages.IsEmpty())
{
// all pages are full, or no pages exist yet, need to allocate new page
this->AllocPage();
}

2.从roomyPages中取得一个内存页,并调用void* MemoryPoolPage::Alloc()方法实际分配一块内存块。

MemoryPoolPage* page = this->roomyPages.Back();
n_assert(page->GetNumFreeBlocks() > 0);

// note: the page pointer is guaranteed to be the one
// from the end of the roomyPages array!
void* ptr = page->Alloc();

3.判断刚才取得的内存页是否还有可以分配的内存块。如果没有就把它从roomyPages中删除掉。

if (0 == page->GetNumFreeBlocks())
{
this->roomyPages.EraseIndex(this->roomyPages.Size() - 1);
}

4.返回指向刚才分配的内存块的指针。

我们更加详细的看一下void* MemoryPoolPage::Alloc()是如何分配一个内存块的。

1.找第一个还没被使用的内存块,并把该内存块块头的pageObject指向本内存页,代码如下:

BlockIndex newBlockIndex = this->firstFreeBlockIndex;
BlockHeader* newBlock = this->BlockIndexToPointer(newBlockIndex);
n_assert(0 == newBlock->pageObject);
newBlock->pageObject = this;

2.设置该内存块的前置和后置,让已分配的内存块和未分配的内存块通过前置和后置关系形成两个逻辑上的列表。并让firstFreeBlockIndex索引指向未分配内存块逻辑列表的第一位,firstAllocBlockIndex索引指向已分配内存块逻辑列表的第一位。并且把当前已分配的内存块放在已分配逻辑列表的第一位。

this->RemoveBlockFromList(this->firstFreeBlockIndex, newBlockIndex);
this->InsertBlockIntoList(this->firstAllocBlockIndex, newBlockIndex);

3.移动指针,让指针指向内存块体。

void* dataPtr = (void*) (newBlock + 1);

4.把已分配内存块的记录数numAllocBlocks加1,返回指向内存块体的指针。

this->numAllocBlocks++;

以上就完成了从内存池中分配一个内存块。接下来我们探讨一下如何释放一个内存块。

我们调用void MemoryPool::Free(void* ptr)来释放一个从内存池分配的内存块。执行步骤如下:

1.通过传入的指针,我们找到分配该内存块的内存页。

MemoryPoolPage* page = MemoryPoolPage::GetMemoryPoolPageFromDataPointer(ptr);

2.调用内存页的void MemoryPoolPage::Free(void* ptr)来真正释放掉内存块。

3.如果该内存页包含一个可以分配的内存块就把该内存页放入roomyPages中。如果该内存页所有的内存块都没被分配,并且内存不止一个内存页,那么就把该内存页从内存池中删除掉。

if (page->GetNumFreeBlocks() == 1)
{
// we've been full previously, but are roomy now
this->roomyPages.Append(page);
}
else if (page->GetNumAllocatedBlocks() == 0)
{
// the page no longer contains any allocated blocks, free
// the entire page, unless this is the last page to
// prevent expensive allocation/deallocations if only
// one block is allocated/freed in this memory pool
if (this->pages.Size() > 1)
{
this->FreePage(page);
}
}

void
MemoryPool::FreePage(MemoryPoolPage* page)
{
n_assert(page->GetNumAllocatedBlocks() == 0);
IndexT pagesIndex = this->pages.FindIndex(page);
n_assert(pagesIndex != InvalidIndex);
IndexT roomyPagesIndex = this->roomyPages.FindIndex(page);
n_assert(roomyPagesIndex != InvalidIndex);

// note: we don't use EraseIndexSwap to keep the page order
// in the order of allocations
this->pages.EraseIndex(pagesIndex);
this->roomyPages.EraseIndex(roomyPagesIndex);
delete page;
}

同样接下我们看看内存页是如何释放掉内存块的void MemoryPoolPage::Free(void* ptr)。

1.取得要删除内存块的索引。

BlockHeader* block = ((BlockHeader*)ptr) - 1;
this->VerifyBlockHeader(block);
BlockIndex blockIndex = this->BlockPointerToIndex(block);

2.把该内存块块头pageObject指针设置为0。

n_assert(this == block->pageObject);
block->pageObject = 0;

3.把该内存块从已分配内存块的逻辑列表断开,并把它放入未分配内存块逻辑列表中。

this->RemoveBlockFromList(this->firstAllocBlockIndex, blockIndex);
this->InsertBlockIntoList(this->firstFreeBlockIndex, blockIndex);

4.已分配内存块的数量减1。

this->numAllocBlocks--;

以上就是n3内存池实现的一个大概讨论。