/* vim: set ft=vimwiki : */ = RGE Documentation = = RGE = In order to use the engine, you must first *configure it*. First define the following functions: {{{txt game_boot game_init game_update game_render game_object_handler game_event_handler }}} Second, you must define the memory section of the engine via calls to macros `set_num_slots` and `calc_mem_chunk` and define `RGE_MAX_LAYERS` Third, you must define the `RG_ANIM_SPRITE_OFFSET` macro. This is an offset into a custom object structure that locates the start of the *AnimSprite* data the engine uses to render your objects. Further details of the first 2 steps is explained bellow. The best way to do this is via a _config.inc_ file which you *pre-include* with the `-p` flag to NASM while *assembling* your code *and the engine code*. === Required extern functions === game_boot - this function is called right before initialization code to set things like title of the window and other pre-run options game_update - this is called each frame with a `dt` parameter in `areg1` (see bellow) game_render - any *extra* rendering one wishes to do. Also gets the `dt` parameter game_object_handler - this routine is also called during the main *update* loop in `RG_Update` during list processing in `RG_ObjectList_Process` It simply passes in the pointer to the structure allocated in its memory space. This routine then must determine which update function to call for which object. A typical setup will look something like this {{{asm global game_object_handler game_object_handler: mov rbx, [areg1 + Object.type] cmp rbx, 1 ;<- one object id jz object_update cmp rbx, 2 ;<- another id jz enemy_update cmp rbx, 3 ; etc. jz other_entity_update }}} Each of the forementioned jump addresses lead to the actual routine that gets called. The second parameter is the delta time in this case. It therefore is important to do modify too many registers in the handler. I was debating if I simply should make an indirect call after a calculation for the offset but decided to opt in for the jump-table function as indirect calls usually incur performance penalties and as this is called each frame, it could be needlessly slow. game_event_handler - Function that handles SDL_Event pointers. Main keyboard handling and other code go here. First parameter is a pointer to the SDL_Event struct on stack, second parameter is event type extracted from said structure. === Before you begin === Before you start writting code for your game, it is important to set up the memory and possibly configure the engine to reflect your project needs. RGE manages its own memory in the `.bss` section. It uses a very simple allocation scheme which basically amounts to pre-defining the needed data size for the game, instead of allocation via `malloc` at runtime. It consists of 2 main parts. The `RG_OList_Bitmask` and `RG_Objects` `RG_Objects` defines a label which is the location of the first *memory chunk*. The size of the chunk must first be setup with a call to `calc_mem_chunk`. Failing to do so will assign a default value of 128 to memory chunks which might/might not be enough for your game, and thus it generates a warning on the terminal. The parameters to `calc_mem_chunk` is a list of defined structures. Example: {{{asm struc Object .x resq 1 .y resq 1 endstruc struc Player .obj resb Object_size .hp resq 1 endstruc ; ... etc ... calc_mem_chunk Object, Enemy, Player, StaticObject, Other ; Sets RG_OBJECT_SIZE to whichever is the largest in bytes ; aligned to the 16byte boundary }}} It will simply use the largest structure out of all of these, and its size becomes the memory chunk size that will be used inside the allocator. It is also aligned to the 16 byte boundary for some instructions require it and also in case you'll want to deal with them on the stack (which *must* be aligned to the 16 byte boundary for call instructions to work correctly, otherwise a segfault occurs) It also defines the macro `LargestStruct` and `LargestStruct_size` to reference the largets structure and its size respectivelly as well as `RG_OBJECT_SIZE` for internal use. This scheme exists for several reasons: 1) It prevents any sort of memory fragmentation 2) It makes it easier to loop through structures due to a fixed size which removes the need to store the chunk size in order to advance to the next chunk. It also improves cache-locality inside the CPU due to its stride-based nature. 3) No need to resize the address space. It is set upon binary load. The disadvantage is that it can't change later. But you can simply recompile with a larger size. 4) No need to initialize this space as it is automatically initialized to 0s by the OS 5) No memory leaks *outside* the engine. (Leaks can still happen within this space though. But the underlying system will not be affected. The game will eventually stop/crash if it runs out of memory) `RG_OList_Bitmask` is a series of bits indicating which index in the memory space is free or occupied. The size of this bitmask is the size of `RG_Objects`/chunk_size and hence why its important to set it up *first* by calling `calc_mem_chunk` This index is always the size of the number of elements in the address space. The size in bytes is the number of elements multiplied by the number of desired layers and divided by 64 bits (for qword alignment) and hence the number of slots available *must be a multiple of 64* To set the desired size of this space, another macro `set_num_slots` is provided to make this alignment easier. In general though, the formula is very simple: `(((Size/Alignment)+1)*Alignment)` Because NASM doesn't use floating point for division, the remainder gets truncated as if one already had called a `floor` function. It is also important to specify the number of layers one wishes to have. For this simply define the RGE_MAX_LAYERS macro to a reasonable value and it will be done automatically by a *stride* in the allocator whenever you specify a layer number. Setting RGE_MAX_LAYERS to 1 effecitvelly disables this feature, as the layers are 0 indexed the *stride offset* will always evaluate to 0 Setting RGE_MAX_LAYERS to 0 is not allowed as it effecitvelly translates all offsets as 0 and will *break the allocator*. The default value for this macro is *3* === RG_Alloc and RG_Dealloc === {{{c rg_alloc * RG_Alloc (struct *, layer_num) void RG_Dealloc (rg_alloc *, layer_num) }}} To allocate a new object to be managed by the engine, use the `RG_Aalloc` and `RG_Dealloc` to add and remove them from the memory space respecitvelly. Make sure to specify a layer number when adding it. (This might change as objects might want to track their layer) It is ideal to define your structures on the stack and then just use a pointer to the stack structure. It gets *automatically copied* into the .bss section so there is no need to use `malloc` or a pointer returned by `malloc` for this operation. `RG_Alloc` returns a pointer to this structure which will point into the engines .bss section. But you may deal with it as if it was created by `malloc` with the exception that you do not call `free` on it but rather `RG_Dealloc`. When you wish to destroy the object, call `RG_Dealloc` on the pointer returned by `RG_Alloc` Any other pointer passed to `RG_Dealloc` is invalid and will lead to undefined behavior. It is also important to make sure your objects have a '.layer' field as that will be used to determine on which layer the object is to be drawn. The allocation scheme is tightly integrated into a layering system. You may define any number of layers by setting the *RGE_MAX_LAYERS* macro. It is recommended to keep this number rather low (in single digits) as it will multiply the amount of memory needed by the allocator. As of this writting, `RG_Alloc` expects 2 parameters: 1) pointer to the structure to be coppied 2) the layer to use for it The same parameters are required for `RG_Dealloc` except the first pointer must be the pointer as returned by `RG_Alloc` === RGE Predefined macros === ==== Single-line macros ==== `areg1` to `areg4` are available as the first argument registers for windows and linux. Linux has additional registers `areg5` to `areg7` which should not be used if the binary is expected to be assembled for windows. ==== Multi-line macros ==== Plenty of other macros are available for ease of use. The most important ones are `set_num_slots` and `calc_mem_chunk` set_num_slots - This macro calculates the number of slots required by the game aligned to a 64bit boundary calc_mem_chunk - This macro calculates the size of 1 memory chunk needed per slot aligned to a 16byte boundary exmcall - This is a convenience cross-platform macro that declares its function `extern` and passes any parameters in the appropriate order for the given OS (either Linux or Windows) Its name is an aberviation of *external macro call* - This macro will change in the future to accomodate for other OSes that run on amd64 architectures. Currently it only supports integer arguments (floating point arguments are planed in the future) ==== Function macros ==== The _generic.inc_ macro package provides easy cross-platform definitions of variables on the stack. The general way of dealing with functions is as follows: {{{asm MyFunction: funcstart saveregs rbx var 8, %$myvar, %$othervar, $globalvar stksetup mov qword [%$myvar], 0x11_22_33_44_55_66_77_88 mov rbx, [%$myvar] mov [%$othervar], rbx mov rax, rbx %undef $globalvar funcend }}} funcstart - This macro takes care of the function prologue in a platform independent way. It does not yet setup the stack because first, variables and register addresses on the stack need to be given a chance to be defined stksetup - Called *after* the `var` and `saveregs` macros. It takes care of allocating space on the stack and aligning it to a 16 byte boundary saveregs - The registers specified on the argument list are tracked to be saved and restored upon function enter and exit respectivelly. Keep in mind that when saving *xmm* registers, these *must* always go first* otherwise, a #GP is raised causing a segfault because they might not be aligned to a 16 byte boundary when they go *after* any registers. This macro also *must* be called *before* any variable declaration for the same reason. If not using xmm registers, this macro can go *after* variable declarations. - The saved register location can be accessed inside the function as `[%$saved_rdx]` for `rdx`, `[%$saved_xmm1]` and so on. - To access the registers aliased as `areg1,areg2,...` etc, you must macro-expand the aliased name first, like so `mov areg1, [%$saved_%[areg1]]`, which will expand to the correct register. On linux, this would be `rdi` - Make sure you *don't* actually *write* to that location, as that defeats the purpose of preserving said register. var - Defines local variables. The first parameter is the size of the variables. The rest of the parameters are *context-local* variables. It is important to include the `%$` sigils as it gets passed to the *NASM* `%define` pre-processor directive. The `var` macro is actually an alias for the macro `variables` so both can be used interchangibly. - The macro may be used more than once in the function prologue but not afterwards. - If one does not desire to have only context local variables you can easily just define them under any other name, like `$myvar` or `myvar` but remember to undefine it at the end of the function (with `%undef`) or else it will be globaly accessible and likely invalid. funcend - Takes care of the function epilogue as well as restoring registers specified in the `saveregs` macro. It is important that no `ret` instruction be placed in the function itself, as that will leave behind a _corrupted stack_ due to the various stack manipulations taking places inside these macros. .