[Bf-committers] Blender game transition

yul brynner you.le at live.fr
Sat Dec 23 11:46:34 CET 2017


EEVEE in BGE cache technical details

I'll try to explain how EEVEE in BGE works (functions related to the draw cache).

Currently I have issues that I'm not able to solve so I don't know if the code I did could be a suitable solution to port eevee drawing into BGE.
The issues I have are described at the end of the document.

This document is intended to people who might want to work on porting EEVEE in BGE and who have knowledge of C and blender sources (Tristan Porteries, Benoit Bolsee...)

The following explainations are done according to my comprehension of things and it is possible that it doesn't correspond exactly to how the code is working.
The code has been written only on the base of visual observations, but I didn't debugged it properly (no true knowledge about debugging)
so there are maybe mistakes in my logic.

In eevee, at each render pass, a cache is created, then at the end of render pass, the cache is freed.

To have an idea about what the cache contains, we can have a look at the function which frees the cache:

static void drw_viewport_cache_resize(void)
{
/* Release the memiter before clearing the mempools that references them */
GPU_viewport_cache_release(DST.viewport);

if (DST.vmempool != NULL) {
BLI_mempool_clear_ex(DST.vmempool->calls, BLI_mempool_count(DST.vmempool->calls));
BLI_mempool_clear_ex(DST.vmempool->calls_generate, BLI_mempool_count(DST.vmempool->calls_generate));
BLI_mempool_clear_ex(DST.vmempool->calls_dynamic, BLI_mempool_count(DST.vmempool->calls_dynamic));
BLI_mempool_clear_ex(DST.vmempool->shgroups, BLI_mempool_count(DST.vmempool->shgroups));
BLI_mempool_clear_ex(DST.vmempool->uniforms, BLI_mempool_count(DST.vmempool->uniforms));
BLI_mempool_clear_ex(DST.vmempool->attribs, BLI_mempool_count(DST.vmempool->attribs));
BLI_mempool_clear_ex(DST.vmempool->passes, BLI_mempool_count(DST.vmempool->passes));
}
}

In EEVEE in BGE, the cache must contain the shaders (DRWShadingGroup) and display arrays (Gwn_Batch)
of all collections (visibled + invisibled) we will use during BGE runtime.

I don't know yet if this is a suitable approach, but I choosed to fill the cache with all the data we need
only one time in KX_Scene constructor and to free the data only one time when we quit the scene in KX_Scene
destructor.

The code to allocate the data is:

In KX_Scene constructor:

void KX_Scene::InitEeveeData()
{
Main *bmain = KX_GetActiveEngine()->GetConverter()->GetMain();
RAS_ICanvas *canvas = KX_GetActiveEngine()->GetCanvas();
Scene *scene = GetBlenderScene();
ViewLayer *cur_view_layer = BKE_view_layer_from_scene_get(scene);
Object *maincam = BKE_view_layer_camera_find(cur_view_layer);
GPUOffScreen *tempgpuofs = GPU_offscreen_create(canvas->GetWidth(), canvas->GetHeight(), 0, nullptr);
int viewportsize[2] = { canvas->GetWidth(), canvas->GetHeight() };
DRW_game_render_loop_begin(tempgpuofs, bmain, scene,
cur_view_layer, maincam, viewportsize);
}

In draw_manager:

void DRW_game_render_loop_begin(GPUOffScreen *ofs, Main *bmain,
Scene *scene, ViewLayer *cur_view_layer, Object *maincam, int viewportsize[2])
{
memset(&DST, 0x0, sizeof(DST));

use_drw_engine(&draw_engine_eevee_type);

DST.viewport = GPU_viewport_create_from_offscreen(ofs);

GPU_viewport_engine_data_create(DST.viewport, &draw_engine_eevee_type);

ARegion ar;
ar.winx = viewportsize[0];
ar.winy = viewportsize[1];

View3D v3d;
Object *obcam = maincam;
Camera *cam = (Camera *)obcam;
v3d.camera = obcam;
v3d.lens = cam->lens;
v3d.near = cam->clipsta;
v3d.far = cam->clipend;

RegionView3D rv3d;
rv3d.camdx = 0.0f;
rv3d.camdy = 0.0f;
rv3d.camzoom = 0.0f;
rv3d.persp = RV3D_CAMOB;
rv3d.is_persp = true;
rctf cameraborder;
drw_game_camera_border(scene, &ar, &v3d, &rv3d, &cameraborder, false, false);
rv3d.viewcamtexcofac[0] = (float)ar.winx / BLI_rctf_size_x(&cameraborder);

DST.draw_ctx.ar = &ar;
DST.draw_ctx.v3d = &v3d;
DST.draw_ctx.rv3d = &rv3d;

/* We don't use bContext in bge
* (not possible or very difficult
* with blenderplayer I guess
*/
DST.draw_ctx.evil_C = NULL;

DST.draw_ctx.v3d->zbuf = true;
DST.draw_ctx.scene = scene;
DST.draw_ctx.view_layer = cur_view_layer;
DST.draw_ctx.obact = OBACT(cur_view_layer);

drw_viewport_var_init();

/* Init engines */
drw_engines_init();

drw_game_disable_double_buffer_check();
drw_game_motion_blur_init();

drw_engines_cache_init();

Depsgraph *graph = BKE_scene_get_depsgraph(scene, cur_view_layer, false);
DEG_OBJECT_ITER(graph, ob, DEG_ITER_OBJECT_FLAG_LINKED_DIRECTLY | DEG_ITER_OBJECT_FLAG_LINKED_VIA_SET | DEG_ITER_OBJECT_FLAG_DUPLI);
{
/* We want to populate cache even with objects in invisible layers.
* (we'll remove them from psl->material_pass later).
*/
bool mesh_is_invisible = (ob->base_flag & BASE_VISIBLED) == 0 && ob->type == OB_MESH;
if (mesh_is_invisible) {
ob->base_flag |= BASE_VISIBLED;
}
drw_engines_cache_populate(ob);
if (mesh_is_invisible) {
ob->base_flag &= ~BASE_VISIBLED;
}
}
DEG_OBJECT_ITER_END

drw_engines_cache_finish();

DRW_state_reset();
drw_engines_disable();
}

The code to free the data is:

In KX_Scene destructor:

void KX_Scene::FreeEeveeData()
{
DRW_game_render_loop_end();
}

In draw_manager:

void DRW_game_render_loop_end()
{
drw_viewport_cache_resize();

GPU_viewport_free(DST.viewport);
MEM_freeN(DST.viewport);

release_ubo_slots();
release_texture_slots();

draw_engine_eevee_type.engine_free();
drw_game_eevee_view_layer_data_free();

memset(&DST, 0xFF, sizeof(DST));
}

There are surely mistakes because it generates memory leaks.


Once the cache has been created, we are able to draw the scene.

As far I understood, to draw the scene,

1) EEVEE uses several DRWPass:

- Each DRWPass has 1 or several OpenGL states associated (DRW_STATE_WRITE_COLOR, DRW_STATE_WRITE_DEPTH...)
  which are set each time the pass is drawn.

2) Each DRWPass contains a list of DRWShadingGroup

- Each DRWShadingGroup has a shader and uniforms associated which are bound when the parent DRWPass is drawn.

3) Each DRWShadingGroup contains a list of DRWCalls

- The DRWCall struct in EEVEE in BGE looks like:

typedef struct DRWCall {
DRWCallHeader head;

float obmat[4][4];
Gwn_Batch *geometry;

void *kxob; // Game engine transition (pointer to KX_GameObject)
bool culled; // Game engine transition

Object *ob; /* Optional */
ID *ob_data; /* Optional. */
} DRWCall;

So once the DRWShadingGroup shader is bound and uniforms are updated, the Gwn_Batch (call->geometry)
contained inside the DRWCall is drawn with the DRWCall obmat.



What is handy in EEVEE is that when the cache is filled, most of DRWCalls are added with DRW_shgroup_call_object_add:

void DRW_shgroup_call_object_add(DRWShadingGroup *shgroup, Gwn_Batch *geom, Object *ob)
{
BLI_assert(geom != NULL);

DRWCall *call = BLI_mempool_alloc(DST.vmempool->calls);

CALL_PREPEND(shgroup, call);

call->head.type = DRW_CALL_SINGLE;
#ifdef USE_GPU_SELECT
call->head.select_id = g_DRW_select_id;
#endif

copy_m4_m4(call->obmat, ob->obmat);
call->geometry = geom;
call->ob_data = ob->data;

call->ob = ob; // Game engine transition
call->culled = false; // Game engine transition
}

Here we can add a pointer to Object which will allow us to find the DRWCall later.

If we use EEVEE's drawing and the cache allocated in KX_Scene constructor, what we need
to move objects is to set the DRWCall obmat with the KX_GameObject obmat. KX_GameObject obmat is set with game logic.

We can access the Object diplay arrays (Gwn_Batch) list with:

/* Get per-material split surface */
struct Gwn_Batch **mat_geom = DRW_cache_object_surface_material_get(ob, gpumat_array, materials_len);

So in BlenderDataConversion, when the Objects are converted to KX_GameObjects, we add to each KX_GameObject its own list of
display arrays (Gwn_Batch). As the Gwn_Batch is inside the DRWShadingGroup, we add to each KX_GameObject its own list of
DRWShadingGroup with the following code:

/* GET + CREATE IF DOESN'T EXIST */
std::vector<DRWShadingGroup *>KX_GameObject::GetMaterialShadingGroups()
{
if (m_materialShGroups.size() > 0) {
return m_materialShGroups;
}
KX_Scene *scene = GetScene();
std::vector<DRWPass *>allPasses = scene->GetMaterialPasses(); (all the passes which contains display arrays (except hair passes not supported))
for (DRWPass *pass : allPasses) {
for (DRWShadingGroup *shgroup = DRW_game_shgroups_from_pass_get(pass); shgroup; shgroup = DRW_game_shgroup_next(shgroup)) {
std::vector<DRWShadingGroup *>::iterator it = std::find(m_materialShGroups.begin(), m_materialShGroups.end(), shgroup);
if (it != m_materialShGroups.end()) {
continue;
}
for (Gwn_Batch *batch : GetMaterialBatches()) {
if (DRW_game_batch_belongs_to_shgroup(shgroup, batch)) {
m_materialShGroups.push_back(shgroup);
break;
}
}
}
}
return m_materialShGroups;
}

Once the KX_GameObject has its own list of Gwn_Batch and its own list of DRWShadingGroup,
we can add to each DRWCall a pointer to the KX_GameObject with this code:

void KX_GameObject::SetKXGameObjectCallsPointer()
{
for (Gwn_Batch *b : m_materialBatches) {
for (DRWShadingGroup *sh : m_materialShGroups) {
DRW_game_call_set_kxob_pointer(sh, b, GetBlenderObject(), (void *)this);
}
}
}

void DRW_game_call_set_kxob_pointer(DRWShadingGroup *shgroup, Gwn_Batch *batch, Object *ob, void *kxob)
{
for (DRWCall *call = shgroup->calls_first; call; call = call->head.prev) {
if (call->geometry == batch && call->ob == ob) {
call->kxob = kxob;
}
}
}

This pointer to KX_GameObject will allow us to find the DRWCall in the cache to update its obmat.

To update the DRWCall obmat, we can use the following code:

in KX_GameObject:

for (Gwn_Batch *batch : m_materialBatches) {
for (DRWShadingGroup *sh : GetMaterialShadingGroups()) {
DRW_game_call_update_obmat(sh, batch, (void *)this, obmat);
}
}

in draw_manager:

void DRW_game_call_update_obmat(DRWShadingGroup *shgroup, Gwn_Batch *batch, void *kxob, float obmat[4][4])
{
for (DRWCall *call = shgroup->calls_first; call; call = call->head.prev) {
if (call->kxob == kxob) {
copy_m4_m4(call->obmat, obmat);
}
}
}

When we want to add a new KX_GameObject, we can use the following code:

/* Use for AddObject */
void KX_GameObject::AddNewMaterialBatchesToPasses(float obmat[4][4])
{
for (Gwn_Batch *b : m_materialBatches) {
for (DRWShadingGroup *sh : GetMaterialShadingGroups()) {
if (DRW_game_batch_belongs_to_shgroup(sh, b)) {
DRW_game_shgroup_call_add(sh, b, (void *)this, obmat);
}
}
}
}

void DRW_game_shgroup_call_add(DRWShadingGroup *shgroup, Gwn_Batch *geom, void *kxob, float(*obmat)[4])
{
BLI_assert(geom != NULL);

DRWCall *call = BLI_mempool_alloc(DST.vmempool->calls);

CALL_PREPEND(shgroup, call);

call->head.type = DRW_CALL_SINGLE;
#ifdef USE_GPU_SELECT
call->head.select_id = g_DRW_select_id;
#endif

if (obmat != NULL) {
copy_m4_m4(call->obmat, obmat);
}

call->geometry = geom;
call->ob_data = NULL;

call->kxob = kxob; // Game engine transition
call->culled = false; // Game engine transition
}

I also added functions to discard and restore KX_GameObject geometry.

THE ISSUES WITH THIS APPROACH:

- All DRWCalls are not added with DRW_shgroup_call_object_add

-> for these calls (like calls added to draw hair geometry (DRW_shgroup_call_add)), we can't set
the pointer to Object we need to identify the DRWCall. (unless we modify eevee's sources)
-> iirc Clay uses DRW_shgroup_call_add and not DRW_shgroup_call_object_add too and this might be an issue
if we want to use Clay in bge I guess.

- For End Object (remove KX_GameObject), I didn't found a way to completely remove a DRWCall from a DRWPass.
I think this requires C knowledge and I don't know C. Maybe this is possible with BLI_mempool,
I don't really know... As a temporarly solution, I only discard the DRWCall (dont draw it) but this
can't be a suitable solution because if we add too much DRWCalls without removing them properly when we end object, BGE becomes very laggy.

- In the current state, This is working for single calls only

The code can be found here: https://github.com/youle31/EEVEEinUPBGE





More information about the Bf-committers mailing list