Table of Contents
This 'Getting Started' guide walks through some steps to get you get to know the philosophy behind X-Forge Core. It continues from the platform-specific 'Getting Started' chapter.
If you wish to save yourself a lot of grief when porting your software between X-Forge Core platforms, you should follow these rules:
No static data is allowed.
This rule is due to the Symbian platform, where all programs must be easily ROMable. Static data includes static variables inside functions and any global variables (except for 'static const' ones). Generally speaking, if you have any pre-set data that can be altered on the fly, you have static data.
Dword reads and writes should be dword aligned
Most RISC CPUs (such as ARM) cannot cope with non-aligned dword operations.
Use slash (/) instead of backslash (\) in paths, and keep filenames case sensitive.
Linux and CFL are case sensitive, so it is best to keep all platforms that way. The Desktop Windows version will alert you if it detects an error in case sensitivity. Note that these rules also affect the #include statements you write.
In data files, use FLOAT32 or XFcFixed type instead of REAL.
REAL can be either FLOAT32 or XFcFixed; thus, make sure you store either one in your data files or in the future when REAL turns into FLOAT32 your data loading will fail.
Things that are specifically omitted from this tutorial include 3D graphics, textures, blending, networking and threads.
All the example sources can be found under xforge/core_examples/tutorial/ in the X-Forge distribution.
The core examples, including the examples in this tutorial, have been designed to demonstrate specific features, and things like cleaning up have been omitted for clarity.
While the core does its best to clean up everything after the application is quiting, there may be some resources which cannot be tracked, like open files or network connections. Properly written programs would delete all surfaces, close all files, etc. before terminating.
The only objects which should not be deleted are documented as such, ie. the application object, and plug-ins to the core (imageloader etc).
In the platform-specific tutorial you compiled a program and got it to work. Let's start from the minimal application and see what it does:
#include <xforge.h> void xfcAppPref(XFcAppPrefs &aAppPrefs) { aAppPrefs.mTotalMemoryLimit = (INT32)(1024 * 1024 * 0.5); aAppPrefs.mResourceMemoryLimit = (INT32)(1024 * 1024 * 0.5); aAppPrefs.mMinimumExtraMemory = (INT32)(1024 * 128); aAppPrefs.mUID = 0x0F470001; } INT32 xfcAppInit() { xfcUseDefaults(); return 0; }
The two functions are the two mandatory entry points to the application.
void xfcAppPref(XFcAppPrefs &aAppPrefs)
This function is used to tell the core things that it must know before initialization, including memory limits. You must not do anything else but alter the aAppPrefs structure in this function, as memory allocation has not been initialized yet.
The mUID member contains the Symbian application unique identifier. If you're developing for some Symbian platform, your application must have its own unique id. You can request a block of ids from Symbian; this is fairly automated process.
The UIDs used in this tutorial are all in the block that has been reserved for development. Applications with such UIDs should never be released in the wild. Other examples in the X-Forge package contain unique ids that have been allocated for those specific applications; you should not use those UIDs in your own applications either.
The minimum extra memory field informs the core how much extra memory, outside the memory pool, should be available. This is neccessary, since, for example, in the Symbian environment, if the operating system (or some other application) runs out of memory while our application is running, random applications start to crash. It is recommended that this is set to 128k.
The second function is the real entry point:
INT32 xfcAppInit()
This function is called after core has been initialized, and it should contain all application-specific initialization. As we have no application class yet, we can't do much here. Trying to draw into the screen here has no effect, as the secondary frame buffer is only presented into the primary one in the rendering loop, which starts after this function. If the initialization fails this function should return a non-zero value.
xfcUseDefaults();
This function call tells the X-Forge Core to initialize a default set of plugins. If these plugins are not initialized, they are never referenced, and thus will be discarded at link time, resulting in smaller binaries. The full list of xfcUse functions can be found in XFcUse.h header file.
X-Forge uses certain prefixes throughout the whole source base. All of the prefixes describe the scope and type of the symbol, instead of, for instance, variable types.
The prefixes are as follows:
mVariable - Member variable aVariable - Function argument variable XFcClass - X-Forge Core class XFuClass - X-Forge Util class XFeClass - X-Forge Engine class xfcFunction - X-Forge Core static global function XFC_FLAG - X-Forge Core define or enumeration
For portability, X-Forge defines its own variable types. These are quite straightforward, INT32 for signed 32bit integer, UINT32 for unsigned, and so on. You can find discussion about these in this 'Getting Started' chapter later in the 'Variable Types' subchapter.
Not being able to quit the application is quite awkward, so we must present three new interface classes: XFcApp, XFcInput and XFcRenderer.
Let's alter the code a bit.
#include <xforge.h> class MyApp : public XFcApp, public XFcInput, public XFcRenderer { virtual void onPointerUp(INT32 aX, INT32 aY); virtual void render(); }; void MyApp::onPointerUp(INT32 /*aX*/, INT32 /*aY*/) { XFcCore::quit(); } void MyApp::render() { } void xfcAppPref(XFcAppPrefs &aAppPrefs) { aAppPrefs.mTotalMemoryLimit = (INT32)(1024 * 1024 * 0.5); aAppPrefs.mResourceMemoryLimit = (INT32)(1024 * 1024 * 0.5); aAppPrefs.mMinimumExtraMemory = (INT32)(1024 * 128); aAppPrefs.mUID = 0x0F470002; } INT32 xfcAppInit() { xfcUseDefaults(); MyApp *a = new MyApp(); XFcCore::setRenderer(a); XFcCore::setController(a); return 0; }
First of all, we created a new class called MyApp, which extends the three interface classes XFcApp, XFcInput and XFcRenderer. This class extends the XFcInput virtual function onPointerUp() and the XFcRenderer function render().
When someone taps the screen, onPointerUp() is called and it calls XFcCore class static method quit() to exit the application. The render function is currently empty, but it is called once on every rendering loop. All the graphics rendering code should be in this function.
Finally, in the xfcAppInit() function we create a new instance of the MyApp class, and then we call two XFcCore class functions to tell the core that right now this object should receive the callbacks for rendering and controls.
If you try to run the program you will see that the application still doesn't show any sensible graphics, but if you tap on the screen the application will at least quit.
This, of course, assuming that you're developing on a platform with a touchscreen. To alter the example to quit when a key is pressed, change the onPointerUp(INT32 aX, INT32 aY) call to onKeyUp(INT32 aCode).
The XFcInput class contains the following callbacks:
virtual void onPointerDown(INT32 aX, INT32 aY); virtual void onPointerMove(INT32 aX, INT32 aY); virtual void onPointerUp(INT32 aX, INT32 aY); virtual void onKeyDown(INT32 aCode); virtual void onKeyUp(INT32 aCode); virtual void onControlDown(INT32 aCode); virtual void onControlUp(INT32 aCode); virtual void onCharEntered(CHAR aChar); virtual void onJoystickMoved(INT32 aX, INT32 aY);
The first three are for pointer (stylus, mouse) controls (onPointerMove() calls are only made when the pointer is down), onKeyDown() and onKeyUp() calls tell the actual code given by hardware, and onControlDown()/onControlUp() calls return XFCCL_KEYCODE enums (XFCCL_LEFT, XFCCL_RIGHT, XFCCL_FIRE1 etc).
onCharEntered() is called whenever the system receives character input. It is important to note that while onKey methods' scan codes usually map to the correct character keys, that may not hold true for all devices. If you're doing some text input widget, use onCharEntered() instead; this callback is called whenever X-Forge Core receives a character input signal from the operating system.
Finally, onJoystickMoved() is reserved for analog joystick input.
The application class and renderer both also have several callbacks that you can use.
Why go through all this trouble with several callbacks? With this system you can have separate rendering functions for different uses, for instance one for game rendering, one for menus, one for pause screen, and whatnot. And the same for controls.
More about the controls can be found in the 'Application Framework' chapter under 'Core'.
The XFcCore class contains system-wide static functions. Apart from the callback-setting and quitting functions it contains the following functionality, and more:
Platform information:
static const CHAR * getPlatformString(); static INT32 getPlatformId();
Getting system tick (for synchronization separate from frame rate)
static INT32 getTick();
Getting a pointer to your XFcApp-extended class object:
static XFcApp * getApp();
Device metrics:
static INT32 getDeviceWidth(); static INT32 getDeviceHeight();
Compressed File Library management (see 'compressed file libraries' for more)
static INT openCFL(const CHAR * aFileName); static void resetCFLDirectory();
Power management:
static REAL getBatteryState();
We will use some of these functions in the examples below. The significance of getApp() call is that since we may not use any global data we have to have a way to store application-wide data somewhere; this place is your application object. You can call XFcCore::getApp() anywhere in your application to get a pointer to the application object. When an XFcApp-extended class is instantiated, the core stores a pointer to the class. If you try to create more than one XFcApp-extended object, the application will quit.
2D graphics in X-Forge Core is mostly based on surfaces. We will change the example to create a new surface, fill it, and then draw that on the secondary surface, doing some simple animation.
#include <xforge.h> class MyApp : public XFcApp, public XFcInput, public XFcRenderer { virtual void onPointerUp(INT32 aX, INT32 aY); virtual void render(); virtual void onAppInit(); XFcGLSurface *mSurf; XFcGL *mGL; }; void MyApp::onPointerUp(INT32 /*aX*/, INT32 /*aY*/) { XFcCore::quit(); } void MyApp::render() { XFcGLSurface *fb; // Clear the framebuffer: mGL->clear(); // Ask GL for the secondary buffer: fb = mGL->getSecondary(); // Draw a stretched sprite: INT32 tick = abs(250 - (XFcCore::getTick() % 500)) + 1; INT32 xcenter = XFcCore::getDeviceWidth() / 2; INT32 ycenter = XFcCore::getDeviceHeight() / 2; fb->drawImage(mSurf, xcenter - tick, ycenter - tick, tick * 2, tick * 2); } void MyApp::onAppInit() { mGL = XFcGL::create(); // Create a 32x32 surface: mSurf = XFcGLSurface::create(32, 32); // Lock the surface: INT16 *map; INT32 pitch = mSurf->lock((void **)&map); // Pitch is divided by 2 because we're using a 16-bit pointer // and pitch is in bytes. pitch /= 2; // Fill the surface: INT32 ofs = 0; for (INT32 y = 0; y < mSurf->getHeight(); y++, ofs = y * pitch) { for (INT32 x = 0; x < mSurf->getWidth(); x++, ofs++) { map[ofs] = (INT16)((x << 11) + (y << 6)); } } // and remember to unlock: mSurf->unlock(); } void xfcAppPref(XFcAppPrefs &aAppPrefs) { aAppPrefs.mTotalMemoryLimit = (INT32)(1024 * 1024 * 0.5); aAppPrefs.mResourceMemoryLimit = (INT32)(1024 * 1024 * 0.5); aAppPrefs.mMinimumExtraMemory = (INT32)(1024 * 128); aAppPrefs.mUID = 0x0F470003; } INT32 xfcAppInit() { xfcUseDefaults(); MyApp *a = new MyApp(); XFcCore::setRenderer(a); XFcCore::setController(a); return 0; }
Several new things happen here. First of all, there's a new callback in use, the onAppInit(). This callback is called after all other initialization is done, so it is a good place to store any initializers for the application object.
Second, our application class now has two new members, mSurf and mGL. mSurf will be our main focus here, but the mGL is the 3D graphics library which contains the rendering device as well, so we'll need it here.
These two objects are created using a static create call. This call may fail, and return NULL. You should always check for out of memory errors.
mGL = XFcGL::create(); mSurf = XFcGLSurface::create(32, 32);
We create the renderer and a 32x32 surface. This surface is created in the default format, which is currently 16-bit on all platforms.
Still in the onAppInit() call we lock the surface, fill it and unlock it. The pitch is the distance between scanlines, in bytes. This is important as hardware may make some limitations on surface sizes. Texture surfaces, for instance, must have dimensions that are powers of two up to 256 (ie. 2, 4, 8, 16, 32, 64, 128 or 256).
Now in the render() function we finally get to create some graphics. First we clear the framebuffer, then we ask for a pointer to the secondary buffer. We could lock the secondary buffer for drawing as we did for the mSurf surface, but we see no reason to do so here. Finally we just blit the surface to the secondary buffer, using stretching.
There are two other important features for surface blits that we haven't demonstrated here: color keying and alpha blending.
With color key you can make concrete holes in the images, which is handy if you, for instance, want to write text on top of some other image. The functions to use this mechanism are called setColorKey() and enableColorKey(). The color key is set with a 32-bit color value regardless of the surface bit depth.
For blending you must use drawImageBlend() calls, which accept two additional parameters: blend type and blend value.
Blend types are different from the 3D pipeline, and the legal values are as follows:
XFCBLEND_NONE, // No blending (default) XFCBLEND_ALPHA, // Alpha blend (crossfade) XFCBLEND_ALPHA_FAST, // Fast 50:50 (avg) alpha XFCBLEND_MUL, // Multiplicative blending XFCBLEND_MUL_FAST, // Fast 100% mul blend XFCBLEND_ADD, // Additive blend XFCBLEND_ADD_FAST, // Fast 100% add blend XFCBLEND_INVMUL, // Inverse multiplication XFCBLEND_INVMUL_FAST // Fast 100% inverse mul blend
Since all surface to surface blits will, in the foreseeable future, be software only, other blending modes are unlikely to be included.
More on 2D graphics can be found in the '2D Graphics' chapter, under 'core'.
The audio library in X-Forge Core is designed like this:
.------------------------------. | Hardware | '------------------------------' [ resampling ] .------------------------------. | Primary buffer | '------------------------------' [ mixing and resampling] .-------------. .-------------. | AudioBuffer | | AudioStream | '-------------' '-------------'
The audio system always has to be initialized before trying to play any buffers or streams. The system automatically resamples and mixes the audio into primary buffer from where the data is converted into the hardware friendly format.
On some devices you can only play sound in 48KHz 16bit format, and mixing multichannel audio in that format can be somewhat CPU intensive. Thus we have the primary buffer in the middle, so you can choose the audio quality regardless of hardware limitations. The audio system works so that if you request a format, the primary buffer is set to that format regardless what the hardware can support.
Audio buffers come in two flavors: linear and cyclic. Cyclic buffers can be used for example for car engine sounds. Linear buffers play once from beginning to end and are best suited for sound effects.
Audio streams can be extended for any kind of streaming audio. The XM-player is a subclass of an audio stream for example.
The latency for sample playing under PocketPC (for example) is 0 to 64 milliseconds, partially due to hardware limitations.
#include <xforge.h> #include <xfutil/XFuXMPlayer.h> class MyApp : public XFcApp, public XFcInput, public XFcRenderer { virtual void onPointerUp(INT32 aX, INT32 aY); virtual void render(); virtual void onAppInit(); virtual void onAppDeinit(); XFcGLSurface *mSurf; XFcGL *mGL; XFuXMPlayer *mPlayer; UINT32 mPlayerId; }; void MyApp::onPointerUp(INT32 /*aX*/, INT32 /*aY*/) { XFcCore::quit(); } void MyApp::render() { XFcGLSurface *fb; // Clear the framebuffer: mGL->clear(); // Ask GL for the secondary buffer: fb = mGL->getSecondary(); // Draw a stretched sprite: INT32 tick = abs(250 - (XFcCore::getTick() % 500)) + 1; INT32 xcenter = XFcCore::getDeviceWidth() / 2; INT32 ycenter = XFcCore::getDeviceHeight() / 2; fb->drawImage(mSurf, xcenter - tick, ycenter - tick, tick * 2, tick * 2); } void MyApp::onAppInit() { mGL = XFcGL::create(); // Create a 32x32 surface: mSurf = XFcGLSurface::create(32, 32); // Lock the surface: INT16 *map; INT32 pitch = mSurf->lock((void **)&map); // Pitch is divided by 2 because we're using a 16-bit pointer // and pitch is in bytes. pitch /= 2; // Fill the surface: INT32 ofs = 0; for (INT32 y = 0; y < mSurf->getHeight(); y++, ofs = y * pitch) { for (INT32 x = 0; x < mSurf->getWidth(); x++, ofs++) { map[ofs] = (INT16)((x << 11) + (y << 6)); } } // and remember to unlock: mSurf->unlock(); XFcAudio::setAudioFormat(44100, XFCAUDIO_STEREO | XFCAUDIO_16BIT | XFCAUDIO_SIGNED, 2048, 16, 0); // create XM player: mPlayer = XFuXMPlayer::create("jenkka17.xm", 8000, 0); // start playing mPlayerId = XFcAudio::play(mPlayer); } void MyApp::onAppDeinit() { // stop playing music (one could also just call "XFcAudio::stopAll()") XFcAudio::stop(mPlayerId); } void xfcAppPref(XFcAppPrefs &aAppPrefs) { aAppPrefs.mTotalMemoryLimit = (INT32)(1024 * 1024 * 1.0); aAppPrefs.mResourceMemoryLimit = (INT32)(1024 * 1024 * 1.0); aAppPrefs.mMinimumExtraMemory = (INT32)(1024 * 128); aAppPrefs.mUID = 0x0F470004; } INT32 xfcAppInit() { xfcUseDefaults(); MyApp *a = new MyApp(); XFcCore::setRenderer(a); XFcCore::setController(a); return 0; }
Only changes here are the inclusion of xfutil/XFuXMPlayer.h, and creation of the XM player object.
The XM module file must be copied to the same directory where the application is. You do not need to link any new files to the application to get it to compile; the XM player is included in the static-link library xfutil.lib.
If the application cannot find the XM file, it will try to play a NULL pointer and this will cause an access violation. If this happens, make sure you have copied the jenkka17.xm file to the same directory as where the executable is.
All aspects of audio control happen through the class XFcAudio. One can control all necessary aspects of audio buffers and audio stream through it.
More on audio can be found in the 'Audio' chapter, under 'Core'.
Having several uncompressed data files lying around on a mobile device is generally a bad idea. X-Forge Core supports a form of file libraries called CFL:s (compressed file libraries). The file IO support is transparent to the application; as long as the application uses XFcFile class for file IO, it doesn't need to know if a data file is stored in a compressed file or not. Currently CFL first decompresses the file into memory on file open call, so it is not suitable for very large decompressed files, but it is very good for minimizing your application's "disk space" usage.
Let's create a CFL.
Create a new directory, and copy the following files into it:
core_examples/data/jenkka17.xm core_examples/data/texture.pcx tools/cflutils/bin/makecfl.exe
Now start a command prompt (or dos shell, if in win9x), and change to the directory you created earlier.
Type "makecfl makeini tutorial.ini". This will create the file tutorial.ini. In its generated form the file contains comments and some configuration data. When all of the comments, optional parameters and unneccessary configuration data is removed, the following lines remain:
LIBRARY_COMPRESS=0x0000FFFF CFLNAME=tutorial.cfl DATA=jenkka17.xm DATA=texture.pcx
Please note that makeini command also lists the .ini file in one DATA line; this line can be removed. Also, since you copied the makecfl.exe to the same directory, it will also be listed here, and can be removed.
Calling makecfl cfl tutorial.ini then creates the CFL file, called tutorial.cfl. In my case the output is as follows:
makecfl CFL creation util Copyright (c) 2001-2002 Jari Komppa and Fathammer Ltd Opening 'tutorial.cfl' for writing..ok Storing 'jenkka17.xm' as 'jenkka17.xm' with 'best' - 'Finds the best compressor by testing them all' 126320 bytes -> 42558 bytes (33.691%) Storing 'texture.pcx' as 'texture.pcx' with 'best' - 'Finds the best compressor by testing them all' 57198 bytes -> 40924 bytes (71.548%) cfl file created.
In order to use the resulting file you must first copy it to the target directory, remembering to delete the xm file from that directory as well, or it will be used instead of the compressed one, and then making the following change to the xfcAppInit() function:
INT32 xfcAppInit() { xfcUseDefaults(); XFcCore::openCFL(XFCSTR("tutorial.cfl")); MyApp *a = new MyApp(); XFcCore::setRenderer(a); XFcCore::setController(a); return 0; }
Since the XM player uses the XFcFile class for file handling, nothing else is needed. The XFCSTR() macro makes sure that when X-Forge Core supports unicode you won't need to rewrite all your string constants.
You can open several .CFL files, and if a file of the same name exists in several, only the last one will be found. Thus, you can create localized resources in separate CFL files and open the one you need last.
More on CFLs can be found in the 'File I/O' chapter, under 'Core'.
As mentioned in 'on coding conventions', in order to ensure portability, the X-Forge Core defines a set of variable types, which are preferred over the standard types, as they act the same way on all of the platforms.
The variable types are defined in the header file XFcConfig.h.
INT8 8 bit signed integer INT16 16 bit signed integer INT32 32 bit signed integer INT64 64 bit signed integer UINT8 8 bit unsigned integer UINT16 16 bit unsigned integer UINT32 32 bit unsigned integer INT Fast integer (at least 16 bit) UINT Fast unsigned integer (at least 16 bit) FLOAT32 32 bit floating point FLOAT64 64 bit floating point XFcFixed 32 bit fixed point REAL Fixed or floating point, depending on platform CHAR8 8 bit character (for text) CHAR16 16 bit character (for text) CHAR 8 or 16 bit character, depending on platform
All of the above variable types are also renamed during compile time using preprocessor macros to start with XFC in order to avoid collisions with platform-specific variable types of the same name (eg. INT32 becomes XFCINT32). In most cases this process will be totally transparent to you.
By using CHAR and REAL instead of CHAR8 and XFcFixed you will enable your code to be smoothly ported into platforms that have different character width or a fast floating point unit.
For portability reasons you should never write REAL or CHAR variables into files. XFcFixed variable type may also change. Use CHAR8, CHAR16 and FLOAT32 types on disk instead.
All of the current X-Forge platforms are little endian (x86, ARM, MIPS, SH3).
Constant character strings of CHAR type should be enclosed inside the XFCSTR() macro.
XFcFile *f = XFcFile::create(XFCSTR("myfile.dat"), XFCSTR("rb"));
This macro will take care of character width conversions.
The X-Forge API uses 'INT' as a boolean variable. If a function returns INT, it most likely only returns 0 or 1.
INT64 works slightly differently on different platforms.
Under Linux, you can only cast to INT64 by using the (INT64)(foo) notation; INT64(foo) will not work.
Under Symbian, the INT64 is a Symbian-defined class, and requires extra effort whenever it is being casted. In order to cast to an INT64 you must first cast your variable to TInt ((INT64)((TInt)foo)). In order to cast back to a 32-bit value, you must call the variable's GetTInt() method ((INT32)foo64.GetTInt()).
Another utility included in the static link library xfutil.lib is the XFuPrinter class. The XFuPrinter class is a primitive bitmap font printer, and it uses the 2D pipeline internally. In order to initialize you need to provide it with a specifically created 256-color PCX file that contains characters in ASCII order, starting from character 33 (the exclamation point). Each character must be as high as the image is wide (For example, a PCX with dimensions of 8x1016 contains 127 characters).
#include <xforge.h> #include <xfutil/XFuPrinter.h> class MyApp : public XFcApp, public XFcInput, public XFcRenderer { virtual void onPointerUp(INT32 aX, INT32 aY); virtual void render(); virtual void onAppInit(); XFcGL *mGL; XFuPrinter *mPrinter; }; void MyApp::onPointerUp(INT32 /*aX*/, INT32 /*aY*/) { XFcCore::quit(); } void MyApp::render() { XFcGLSurface *fb; // Clear the framebuffer: mGL->clear(); // Ask GL for the secondary buffer: fb = mGL->getSecondary(); // Print the classic phrase: mPrinter->print(fb, 0, 0, XFCSTR("Hello World")); } void MyApp::onAppInit() { mGL = XFcGL::create(); mPrinter = XFuPrinter::create(XFCSTR("font16.pcx"), 0); } void xfcAppPref(XFcAppPrefs &aAppPrefs) { aAppPrefs.mTotalMemoryLimit = (INT32)(1024 * 1024 * 0.5); aAppPrefs.mResourceMemoryLimit = (INT32)(1024 * 1024 * 0.5); aAppPrefs.mMinimumExtraMemory = (INT32)(1024 * 128); aAppPrefs.mUID = 0x0F470006; } INT32 xfcAppInit() { xfcUseDefaults(); MyApp *a = new MyApp(); XFcCore::setRenderer(a); XFcCore::setController(a); return 0; }
In the onAppInit() function, we create the printer:
mPrinter = XFuPrinter::create(XFCSTR("font16.pcx"), 0);
The first parameter is the filename, and the second is the transparency color mask. In here, black color will be transparent.
Internally, the printer creates a XFcGLSurface for each character in the font.
Printing is done in render() function:
mPrinter->print(fb, 0, 0, XFCSTR("Hello World"));
X-Forge Core has its own implementation of sprintf(), and this can be found in the static class XFcStringToolkit. The function is called format(). It is mostly equal in its output with the stdlib sprintf() except for floating point values, with which it has some precision limitations. Using XFcStringToolkit::format() over sprintf() is recommeded due to portability issues.
XFcStringToolkit::format(targetString, "format string %d", mValue);
If you try to use the printer and nothing gets drawn, it is most likely because the printer doesn't find the required .pcx file. Make sure you copy the font16.pcx into the same directory where the executable is.
X-Forge Core provides two different profiling methods.
First one is a simple set of timing functions. Good side is that those routines are portable, so the same profiling code works on all platforms. Bad side is that the timings are taken from the system clock, and the resolution of the system ticks on different platforms varies from one to 15 milliseconds.
The second is a simple sampling profiler that requires some preparation and only works under Wince ARM currently, but the code changes that are needed are quite trivial.
All of the profiler functionality is implemented in the static class XFcProfiler. The profiler's internal data is hidden inside the core.
In order to use the timing profiler, you must first initialize the event log by calling XFcProfiler::initEventLog() in some initialization function (xfcAppInit() is a good place).
Then, mark the beginnings of any events you want to time with XFcProfiler::addEvent() calls.
The only tricky bit is around when you want to print the log out. Before calling the actual printing, you should call XFcProfiler::frameEvent() in order to store frame rate counter in the end of the log. Then, print out the pointer returned from XFcProfiler::getEventLog().
After printing out, you must call XFcProfiler::resetEvent(). If you do not do this, the log will eventually get filled and will overflow, crashing the system. The log is relatively small, only several kilobytes long.
The addEvent() function takes one character string parameter, which is the name of the event that is about to begin. The resetEvent() function takes two parameters: first is the event that just passed (most probably printing) and the event that is just beginning.
Be sure to add an addEvent() call before you leave the render() function, or otherwise you will be adding the whole core timings into your last event.
#include <xforge.h> #include <xfutil/XFuPrinter.h> class MyApp : public XFcApp, public XFcInput, public XFcRenderer { virtual void onPointerUp(INT32 aX, INT32 aY); virtual void render(); virtual void onAppInit(); XFcGL *mGL; XFuPrinter *mPrinter; }; void MyApp::onPointerUp(INT32 /*aX*/, INT32 /*aY*/) { XFcCore::quit(); } void MyApp::render() { XFcGLSurface *fb; // Mark the beginning of 'clear' event XFcProfiler::addEvent("clear"); // Clear the framebuffer: mGL->clear(); // Mark the beginning of 'waste' event (we'll waste some time here) XFcProfiler::addEvent("waste"); INT k; for (INT i = 0; i < 256; i++) for (INT j = 0; j < 256; j++) k = i * i / (j + 1); // Ask GL for the secondary buffer: fb = mGL->getSecondary(); // Make the frame event XFcProfiler::frameEvent(); // Print the debug info: mPrinter->print(fb, 0, 0, XFcProfiler::getEventLog()); // Reset event log; last event was called 'print', next one is 'core ' XFcProfiler::resetEvent("print", "core "); } void MyApp::onAppInit() { mGL = XFcGL::create(); mPrinter = XFuPrinter::create(XFCSTR("font16.pcx"), 0); } void xfcAppPref(XFcAppPrefs &aAppPrefs) { aAppPrefs.mTotalMemoryLimit = (INT32)(1024 * 1024 * 0.5); aAppPrefs.mResourceMemoryLimit = (INT32)(1024 * 1024 * 0.5); aAppPrefs.mMinimumExtraMemory = (INT32)(1024 * 128); aAppPrefs.mUID = 0x0F470007; } INT32 xfcAppInit() { xfcUseDefaults(); XFcProfiler::initEventLog(); MyApp *a = new MyApp(); XFcCore::setRenderer(a); XFcCore::setController(a); return 0; }
The output from the above program looks something like this:
print 1 core 15 clear 6 waste 20 FPS 23 (the timings here are not relative to any real device)
All the other values except for the FPS are in milliseconds.
To use the sampling profiler (which currently only works under PocketPC and only on real hardware), you must first link your application with the sampling-enabled static library xfcorep.lib and toolhelp.lib. Next, set the compiler to output a .map file (project / settings / link, 'generate mapfile').
Next, add the following somewhere in the start of your application:
XFcProfiler::startSampler();
and the following somewhere in the deinitialization:
XFcProfiler::endSampler();
After these steps build your application, and run it on some ARM based PocketPC device. If the application does not run, the device does not contain toolhelp.dll. You can find this by searching for it in the 'Windows CE Tools' tree on your hard drive.
Let the application run for 15 minutes or longer, and then quit it. You should then have the sampling data on the device. Copy this into a directory along with the .map file that the compiler generated, and run profileanalyzer (included in the 'tools' directory) to generate profiling report.
The report is generated by running a thread at maximum priority and sampling the current instruction pointer addresses of all the threads in the program. As such the sampling is not even, plus if you have a thread that loops a single function this will be shown as a huge time eater. The sampling profiler does, however, give some kind of overview on where all the time is spent.
Using the sampling profiler slows applications down a lot. Thus, if you're running the whole game graph, with physics loop being run more often when the framerate is slow, you will find that the sampled data will show that the physics seem to take majority of CPU time, even if that is not the case when the application is running without the profiler. It is also highly probable that the profiled application will grind to a halt with the physics enabled, as each consequent frame requires more and more physics calculations.
From the interface, XFcFile class looks like a thin wrapper on top of fopen()-style functions with some small add-ons.
Instead of create(), XFcFile object is created using open() call. This is to follow the stdlib fopen() file function style. In addition to the plain open(), we have openFromDisk() and openFromCFL() functions. open() itself will try to open from disk and then from the CFL, if file was not found on disk. When writing or appending, the file will always be opened from disk directly.
static XFcFile * open(const CHAR *aFilename, const CHAR *aMode); static XFcFile * openFromDisk(const CHAR *aFilename, const CHAR *aMode); static XFcFile * openFromCFL(const CHAR *aFilename, const CHAR *aMode);
The other standard functions look and work as you'd expect them to, after using the stdlib functions:
INT32 seek(INT32 aDisplacement, INT32 aMode); INT32 tell(); INT32 getChar(); INT32 putChar(INT32 aChar); INT32 read(void * aBuf, INT32 aSize, INT32 aCount); INT32 write(const void * aBuf, INT32 aSize, INT32 aCount); INT32 close();
The close() method also deletes the object.
The following convenience functions are provided to make it easier to make portable files (unsafe variable types omitted, variable type conversion applied):
INT32 writeFLOAT32(FLOAT32 aValue); FLOAT32 readFLOAT32(); INT32 writeINT32(INT32 aValue); INT32 readINT32(); INT32 writeINT16(INT16 aValue); INT16 readINT16(); INT32 writeINT8(INT8 aValue); INT8 readINT8(); void writeCHARString(CHAR * aValue); CHAR * readCHARString();
Finally, the following functions are thin wrappers on top of the CFL system, built so that they work on disk files as well:
static INT8 * getFile(const CHAR *aFilename); static INT32 getFileSize(const CHAR *aFilename); static INT fileExists(const CHAR *aFilename);
Using getFile() is more efficient than reading the whole file into memory manually using read() function, because internally the CFL system currently loads the whole file into memory. This is good to keep in mind when figuring out runtime memory requirements.
More about file handling can be found in the 'File I/O' chapter, under 'Core'.
You can bypass the whole XFcFile mechanism by creating a XFcCFL object. XFcCFL only supports loading whole files at once, which is not as handy as the XFcFile method, but having this mechanism available may prove to be useful.
You may want to make level files or localization files into separate CFL files, and mount these as needed.
You can also create CFLs at runtime. This is handy for compressed savegames for instance. CFLs are created using the XFcCFLMaker class.
XFcCFLMaker *rm = XFcCFLMaker::create(XFCSTR("mydatafile.cfl")); rm->store(XFCSTR("mydata1.dat"), mMydata1, mMydata1Size, XFCCFLCOMPRESS_ZLIB); rm->store(XFCSTR("mydata2.dat"), mMydata2, mMydata2Size, XFCCFLCOMPRESS_ZLIB); rm->store(XFCSTR("mydata3.dat"), mMydata3, mMydata3Size, XFCCFLCOMPRESS_ZLIB); rm->store(XFCSTR("mydata4.dat"), mMydata4, mMydata4Size, XFCCFLCOMPRESS_ZLIB); rm->finish(RFCOMPRESS_ZLIB);
The above code snippet would create a file called mydatafile.cfl that would contain resources mydata1.dat - mydata4.dat. All of the resources and the library information would be compressed with zlib using default options.
The finish() call also deletes the object, so no 'delete rm;' is needed.
XFcStringToolkit class is a collection of static string methods using CHAR* data type. This is not a string class, however. The methods' functionality is close to their stdlib counterparts'.
There are several overloaded versions of string duplicating function. Their purpose is to make conversions from unicode or ascii to the currently used character format as painless as possible. As usual, the functions return NULL if there was insufficient memory to allocate the new string.
static CHAR * copy(const CHAR * aString); static CHAR * copy(const CHAR8 * aString); static CHAR * copy(const CHAR16 * aString);
Another two duplication functions convert from the internal format to either 8 bit or 16 bit character format. You might use these to write platform-independent files, for instance.
static CHAR8 * copyToCHAR8(const CHAR * aString); static CHAR16 * copyToCHAR16(const CHAR * aString);
String comparison functions - case sensitive and case insensitive. They work like their stdlib counterparts, eg. 0 means equal.
static int compare(const CHAR * aString1, const CHAR * aString2); static int compareIgnoreCase(const CHAR * aString1, const CHAR * aString2);
Currently, the following searching functions exist; string in string, and reverse character search.
static const CHAR * find(const CHAR * aStringSrc, const CHAR * aStringToFind); static CHAR * findLast(const CHAR * aString, const CHAR aChar);
The following functions return the size of the string in characters and in bytes:
static INT32 getLength(const CHAR * aString); static INT32 getLengthInBytes(const CHAR * aString);
And finally, the next function joins two strings together to form a longer string. Optionally you can set a delimiter character. If zero, no delimiter is inserted between the two strings.
static CHAR * concat(const CHAR *aString1, const CHAR *aString2, CHAR aDelimiter=0);
The XFuTokenizer utility can be used to tokenize strings. This class works slightly differently from other classes in the core in that it does not follow the new/create pattern. Instead, you can use it as a local variable, and reuse it without deletion.
Example of use:
const CHAR blah = XFCSTR("some; string with ; different tokens"); XFuTokenizer myTokens; myTokens.tokenize(blah, XFCSTR(";")); for (int i = 0; i < myTokens.getTokenCount(); i++) if (tokenEqualsNocase(XFCSTR("String wIth")) printf("token number %d is our token",i);
The tokenizer object has the following methods:
void tokenize(const CHAR *aBuffer, const CHAR *aSeparators); void tokenize(const CHAR *aBuffer); INT tokenEquals(INT32 aIdx, const CHAR *aCompareString); INT tokenEqualsNocase(INT32 aIdx, const CHAR *aCompareString); INT32 getTokenCount(); const CHAR * getToken(INT32 aIdx); CHAR * duplicateToken(INT32 aIdx);
The getToken() returns a pointer to the tokenizer's internal data. Thus, if you call tokenize() again, or if the object leaves scope, the pointer will be invalid. In this case you should call duplicateToken() instead.
A simple parser can be made with the tokenizer by first reading a whole text file into a buffer, tokenizing that by the newline characters, and then looping through the lines, tokenizing each line separately.